mirror of
https://github.com/132nd-vWing/tacview-splitter.git
synced 2026-08-26 11:08:36 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13187c9d9b | ||
|
|
1a6ff6fe6f | ||
|
|
8026ef1a04 | ||
|
|
a6acdbac4f | ||
|
|
61cfbd3e13 | ||
|
|
e51e57505e | ||
|
|
ea81556898 | ||
|
|
cfb9b4e8d6 | ||
|
|
24d50c2c62 | ||
|
|
126ef3708f | ||
|
|
2bf2352c67 | ||
|
|
d944942d62 | ||
|
|
ac873d385e | ||
|
|
89a79e3015 | ||
|
|
1cbf11a468 | ||
|
|
43b1bacaab | ||
|
|
f94db99810 | ||
|
|
5eccad6517 | ||
|
|
8c46e2bbcc | ||
|
|
cda33692cf | ||
|
|
c1ef22f3a3 | ||
|
|
fd800ce394 | ||
|
|
a35ff8b362 | ||
|
|
d7db52fc2d | ||
|
|
f87a3c9e06 | ||
|
|
b82c40d4ab | ||
|
|
7d66b1ff31 | ||
|
|
84f1bf7efe | ||
|
|
1a2a08515b | ||
|
|
3658dbcdf7 | ||
|
|
e9be340a6b | ||
|
|
7e788c66cf | ||
|
|
97df3b46f8 | ||
|
|
902ed24e46 | ||
|
|
64cb0879e2 | ||
|
|
f09fb480ef | ||
|
|
efa43023f4 | ||
|
|
d9695eab02 | ||
|
|
db6af20850 | ||
|
|
467274816f | ||
|
|
67c5e61d21 | ||
|
|
6e192af5ad | ||
|
|
9dc8005d39 | ||
|
|
e2ab2787e7 |
@@ -1,3 +1,9 @@
|
|||||||
|
# build system
|
||||||
|
.built
|
||||||
|
.released
|
||||||
|
.stripped
|
||||||
|
.tested
|
||||||
|
|
||||||
# ---> Rust
|
# ---> Rust
|
||||||
# Generated by Cargo
|
# Generated by Cargo
|
||||||
# will have compiled files and executables
|
# will have compiled files and executables
|
||||||
|
|||||||
+5
-3
@@ -1,12 +1,14 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tacview-splitter"
|
name = "tacview-splitter"
|
||||||
version = "0.3.0"
|
version = "0.3.2"
|
||||||
authors = ["132nd-Professor <132nd-Professor>"]
|
authors = ["132nd-Professor <132nd-Professor>"]
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
zip = ">= 0.5.13"
|
zip = {version = ">= 0.6.2", default-features = false, features = ["deflate"]}
|
||||||
|
anyhow = {version = ">= 1.0"}
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
strip = "symbols"
|
||||||
|
|||||||
@@ -6,20 +6,43 @@ RELEASE_WINDOWS=tacview-splitter-win10-x86_64.zip
|
|||||||
BIN_LINUX=target/x86_64-unknown-linux-gnu/release/tacview-splitter
|
BIN_LINUX=target/x86_64-unknown-linux-gnu/release/tacview-splitter
|
||||||
BIN_WINDOWS=target/x86_64-pc-windows-gnu/release/tacview-splitter.exe
|
BIN_WINDOWS=target/x86_64-pc-windows-gnu/release/tacview-splitter.exe
|
||||||
|
|
||||||
|
TEST_FILES=Tacview-20210606-222650-DCS-ATRM_2.7.0.443.txt.acmi Tacview-20210606-222650-DCS-ATRM_2.7.0.443.zip.acmi Tacview-20210608-085030-DCS-Georgia_At_War_v3.0.24_afternoon.txt.acmi Tacview-20210608-085030-DCS-Georgia_At_War_v3.0.24_afternoon.zip.acmi
|
||||||
|
|
||||||
default: release
|
default: release
|
||||||
|
|
||||||
build:
|
.built: src/*.rs
|
||||||
cargo build --release --target x86_64-unknown-linux-gnu
|
cargo build --release --target x86_64-unknown-linux-gnu
|
||||||
cargo build --release --target x86_64-pc-windows-gnu
|
cargo build --release --target x86_64-pc-windows-gnu
|
||||||
|
touch .built
|
||||||
|
|
||||||
strip: build
|
build: .built
|
||||||
strip ${BIN_LINUX} ${BIN_WINDOWS}
|
|
||||||
|
|
||||||
release: build strip
|
.tested: .built
|
||||||
|
for file in ${TEST_FILES}; do \
|
||||||
|
mkdir -p test; \
|
||||||
|
cp testfiles/$$file test; \
|
||||||
|
if [[ $$? != 0 ]]; then exit 1; fi;\
|
||||||
|
cd test; \
|
||||||
|
../${BIN_LINUX}; \
|
||||||
|
if [[ $$? != 0 ]]; then exit 1; fi;\
|
||||||
|
cd ../ ; \
|
||||||
|
rm -rf test ; \
|
||||||
|
done
|
||||||
|
touch .tested
|
||||||
|
|
||||||
|
test: .tested
|
||||||
|
|
||||||
|
.released: .tested
|
||||||
rm -rf release;
|
rm -rf release;
|
||||||
mkdir -p release/{windows,linux}/tacview-splitter-${VERSION};
|
mkdir -p release/{windows,linux}/tacview-splitter-${VERSION};
|
||||||
cp ${BIN_LINUX} release/linux/tacview-splitter-${VERSION}
|
cp ${BIN_LINUX} release/linux/tacview-splitter-${VERSION}
|
||||||
cp ${BIN_WINDOWS} release/windows/tacview-splitter-${VERSION}
|
cp ${BIN_WINDOWS} release/windows/tacview-splitter-${VERSION}
|
||||||
cd release/linux; tar -zcvf ${RELEASE_LINUX} tacview-splitter-${VERSION}; cp ${RELEASE_LINUX} ..
|
cd release/linux; tar -zcvf ${RELEASE_LINUX} tacview-splitter-${VERSION}; cp ${RELEASE_LINUX} ..
|
||||||
cd release/windows; 7z a ${RELEASE_WINDOWS} tacview-splitter-${VERSION}; cp ${RELEASE_WINDOWS} ..
|
cd release/windows; 7z a ${RELEASE_WINDOWS} tacview-splitter-${VERSION}; cp ${RELEASE_WINDOWS} ..
|
||||||
cd release; sha256sum ${RELEASE_LINUX} ${RELEASE_WINDOWS} > sha256sums.txt
|
cd release; rm -rf linux windows; sha256sum ${RELEASE_LINUX} ${RELEASE_WINDOWS} > sha256sums.txt
|
||||||
|
touch .released
|
||||||
|
|
||||||
|
release: .released
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf release target test .released .tested .built
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub const EXTENSION_ZIP: &str = ".zip.acmi";
|
||||||
|
pub const EXTENSION_TXT: &str = ".txt.acmi";
|
||||||
-98
@@ -1,98 +0,0 @@
|
|||||||
pub mod lib {
|
|
||||||
use std::io::Write;
|
|
||||||
use std::fs;
|
|
||||||
use zip;
|
|
||||||
|
|
||||||
const ERR_CANNOT_WRITE_DATA: &str = "Could not write data";
|
|
||||||
const ERR_CANNOT_OPEN_OUTPUT: &str = "Could not open output file";
|
|
||||||
|
|
||||||
pub struct IDs<'a> {
|
|
||||||
pub blue: Vec<&'a str>,
|
|
||||||
pub red: Vec<&'a str>,
|
|
||||||
pub violet: Vec<&'a str>,
|
|
||||||
pub unknown: Vec<&'a str>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Descriptors<T: Write> {
|
|
||||||
blue: T,
|
|
||||||
red: T,
|
|
||||||
violet: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DescriptorsTxt {
|
|
||||||
pub blue: fs::File,
|
|
||||||
pub red: fs::File,
|
|
||||||
pub violet: fs::File,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct OutputFilenames {
|
|
||||||
pub txt: FilenamesVariant,
|
|
||||||
pub zip: FilenamesVariant,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct FilenamesVariant {
|
|
||||||
pub blue: String,
|
|
||||||
pub red: String,
|
|
||||||
pub violet: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct BodiesByCoalition<'a> {
|
|
||||||
pub blue: Vec<&'a str>,
|
|
||||||
pub red: Vec<&'a str>,
|
|
||||||
pub violet: Vec<&'a str>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Handling {
|
|
||||||
fn write(&mut self, header: Vec<String>, bodies_by_coalition: BodiesByCoalition);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Write> Handling for Descriptors<T> {
|
|
||||||
fn write(&mut self, header: Vec<String>, bodies_by_coalition: BodiesByCoalition) {
|
|
||||||
for line in &header {
|
|
||||||
write!(self.blue, "{}\n", line).expect(ERR_CANNOT_WRITE_DATA);
|
|
||||||
write!(self.red, "{}\n", line).expect(ERR_CANNOT_WRITE_DATA);
|
|
||||||
write!(self.violet, "{}\n", line).expect(ERR_CANNOT_WRITE_DATA);
|
|
||||||
}
|
|
||||||
for line in &bodies_by_coalition.blue {
|
|
||||||
write!(self.blue, "{}\n", line).expect(ERR_CANNOT_WRITE_DATA);
|
|
||||||
}
|
|
||||||
for line in &bodies_by_coalition.red {
|
|
||||||
write!(self.red, "{}\n", line).expect(ERR_CANNOT_WRITE_DATA);
|
|
||||||
}
|
|
||||||
for line in &bodies_by_coalition.violet {
|
|
||||||
write!(self.violet, "{}\n", line).expect(ERR_CANNOT_WRITE_DATA);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Descriptors<fs::File> {
|
|
||||||
pub fn new_for_file(filenames: OutputFilenames) -> Descriptors<fs::File> {
|
|
||||||
let blue = fs::File::create(&filenames.txt.blue).expect(ERR_CANNOT_OPEN_OUTPUT);
|
|
||||||
let red = fs::File::create(&filenames.txt.red).expect(ERR_CANNOT_OPEN_OUTPUT);
|
|
||||||
let violet = fs::File::create(&filenames.txt.violet).expect(ERR_CANNOT_OPEN_OUTPUT);
|
|
||||||
let descriptors = Descriptors { blue, red, violet };
|
|
||||||
return descriptors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Descriptors<zip::ZipWriter<fs::File>> {
|
|
||||||
pub fn new_for_zip(filenames: OutputFilenames) -> Descriptors<zip::ZipWriter<fs::File>> {
|
|
||||||
let options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Deflated);
|
|
||||||
|
|
||||||
let file = fs::File::create(&filenames.zip.blue).expect(ERR_CANNOT_OPEN_OUTPUT);
|
|
||||||
let mut blue = zip::ZipWriter::new(file);
|
|
||||||
blue.start_file(&filenames.txt.blue, options).unwrap();
|
|
||||||
|
|
||||||
let file = fs::File::create(&filenames.zip.red).expect(ERR_CANNOT_OPEN_OUTPUT);
|
|
||||||
let mut red = zip::ZipWriter::new(file);
|
|
||||||
red.start_file(&filenames.txt.red, options).unwrap();
|
|
||||||
|
|
||||||
let file = fs::File::create(&filenames.zip.violet).expect(ERR_CANNOT_OPEN_OUTPUT);
|
|
||||||
let mut violet = zip::ZipWriter::new(file);
|
|
||||||
violet.start_file(&filenames.txt.violet, options).unwrap();
|
|
||||||
|
|
||||||
let descriptors = Descriptors{blue, red, violet};
|
|
||||||
descriptors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+20
-158
@@ -1,166 +1,28 @@
|
|||||||
use std::{fs, str};
|
mod constants;
|
||||||
|
mod processor;
|
||||||
|
mod reader;
|
||||||
|
mod tacview;
|
||||||
|
mod writer;
|
||||||
|
|
||||||
use std::io::{BufRead, BufReader};
|
use anyhow::Result;
|
||||||
use zip;
|
|
||||||
use tacview_splitter::lib;
|
|
||||||
use tacview_splitter::lib::Handling;
|
|
||||||
|
|
||||||
const COMMENT: char = '#';
|
fn main() -> Result<()> {
|
||||||
const MINUS: char = '-';
|
println!(
|
||||||
const EXTENSION_ZIP: &str = ".zip.acmi";
|
"{}, version {}",
|
||||||
const EXTENSION_TXT: &str = ".txt.acmi";
|
env!("CARGO_PKG_NAME"),
|
||||||
|
env!("CARGO_PKG_VERSION")
|
||||||
const UNKNOWN: u8 = 0;
|
);
|
||||||
const TIMESTAMP: u8 = 1;
|
let (input_filename, is_zip) = reader::find_input_file()?;
|
||||||
const DESTRUCTION: u8 = 2;
|
|
||||||
const TELEMETRY: u8 = 3;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let (input_filename, is_zip) = find_input_file();
|
|
||||||
println!("Processing {}", input_filename);
|
println!("Processing {}", input_filename);
|
||||||
let lines = read_data(&input_filename, is_zip);
|
|
||||||
let (header, body) = split_into_header_and_body(lines);
|
|
||||||
let bodies_by_coalition = divide_body_by_coalition(&body);
|
|
||||||
|
|
||||||
let output_filenames = get_output_filenames(&input_filename);
|
let lines = reader::read_data(&input_filename, is_zip)?;
|
||||||
if is_zip {
|
|
||||||
let mut descriptors = lib::Descriptors::new_for_zip(output_filenames);
|
|
||||||
descriptors.write(header, bodies_by_coalition);
|
|
||||||
} else {
|
|
||||||
let mut descriptors = lib::Descriptors::new_for_file(output_filenames);
|
|
||||||
descriptors.write(header, bodies_by_coalition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn split_into_header_and_body(lines: Vec<String>) -> (Vec<String>, Vec<String>) {
|
let (header, body) = processor::split_into_header_and_body(lines)?;
|
||||||
let mut i=0;
|
let coalition_per_line = processor::get_coalition_per_line(&body)?;
|
||||||
for line in &lines {
|
|
||||||
if line.chars().nth(0).expect("malformed line") == COMMENT {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
return (lines[..i].to_vec(), lines[i..].to_vec());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn divide_body_by_coalition(body: &Vec<String>) -> lib::BodiesByCoalition {
|
let output_data =
|
||||||
let mut bbc = lib::BodiesByCoalition{blue: Vec::new(), red: Vec::new(), violet: Vec::new()};
|
writer::OutputData::new(input_filename, is_zip, header, body, coalition_per_line);
|
||||||
let mut continued = false;
|
output_data.save_to_disk()?;
|
||||||
let mut line_type: u8 = UNKNOWN;
|
|
||||||
let mut ids = lib::IDs{blue: Vec::new(), red: Vec::new(), violet: Vec::new(), unknown: Vec::new()};
|
|
||||||
for line in body {
|
|
||||||
let result = process_line(continued, &mut ids, line, line_type);
|
|
||||||
line_type = result.0;
|
|
||||||
continued = result.1;
|
|
||||||
let id = result.2;
|
|
||||||
if line_type == TIMESTAMP {
|
|
||||||
bbc.blue.push(line);
|
|
||||||
bbc.red.push(line);
|
|
||||||
bbc.violet.push(line);
|
|
||||||
} else { // destruction or telemetry
|
|
||||||
if ids.blue.contains(&id) {
|
|
||||||
bbc.blue.push(line);
|
|
||||||
} else if ids.red.contains(&id) {
|
|
||||||
bbc.red.push(line);
|
|
||||||
} else if ids.violet.contains(&id) {
|
|
||||||
bbc.violet.push(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bbc
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_line<'a>(continued: bool, ids: &mut lib::IDs<'a>, line: &'a String, last_line_type: u8) -> (u8, bool, &'a str) {
|
Ok(())
|
||||||
let mut id = "both";
|
|
||||||
let line_type;
|
|
||||||
let will_continue: bool;
|
|
||||||
if !continued {
|
|
||||||
let first_char = line.chars().nth(0).expect("malformed line");
|
|
||||||
if first_char == COMMENT {
|
|
||||||
line_type = TIMESTAMP;
|
|
||||||
} else if first_char == MINUS {
|
|
||||||
line_type = DESTRUCTION;
|
|
||||||
} else {
|
|
||||||
line_type = TELEMETRY;
|
|
||||||
}
|
|
||||||
|
|
||||||
if line_type == TELEMETRY {
|
|
||||||
id = line
|
|
||||||
.split_once(',') // TODO catch the None
|
|
||||||
.unwrap()
|
|
||||||
.0;
|
|
||||||
//let id_str = id.to_string();
|
|
||||||
if line.contains("Color=") {
|
|
||||||
if line.contains("Color=Blue") {
|
|
||||||
ids.blue.push(id);
|
|
||||||
} else if line.contains("Color=Red") {
|
|
||||||
ids.red.push(id);
|
|
||||||
} else if line.contains("Color=Violet") {
|
|
||||||
ids.violet.push(id);
|
|
||||||
} else {
|
|
||||||
ids.unknown.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
line_type = last_line_type;
|
|
||||||
}
|
|
||||||
if line.ends_with("\\") {
|
|
||||||
will_continue = true;
|
|
||||||
} else {
|
|
||||||
will_continue = false;
|
|
||||||
}
|
|
||||||
(line_type, will_continue, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_output_filenames(input_filename: &String) -> lib::OutputFilenames {
|
|
||||||
let blue = input_filename.replace(".zip", "_blue.zip");
|
|
||||||
let red = input_filename.replace(".zip", "_red.zip");
|
|
||||||
let violet = input_filename.replace(".zip", "_violet.zip");
|
|
||||||
let output_filenames_zip = lib::FilenamesVariant{blue, red, violet};
|
|
||||||
|
|
||||||
// TODO make sure the replace was successful
|
|
||||||
let blue = input_filename.replace(".txt", "_blue.txt");
|
|
||||||
let red = input_filename.replace(".txt", "_red.txt");
|
|
||||||
let violet = input_filename.replace(".txt", "_violet.txt");
|
|
||||||
let output_filenames_txt = lib::FilenamesVariant{blue, red, violet};
|
|
||||||
|
|
||||||
let output_filenames = lib::OutputFilenames{txt: output_filenames_txt, zip: output_filenames_zip};
|
|
||||||
output_filenames
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_input_file() -> (String, bool) {
|
|
||||||
let read_dir = fs::read_dir(".").expect("Could not read current directory");
|
|
||||||
for entry_result in read_dir {
|
|
||||||
let entry = entry_result.expect("Could not parse DirEntry");
|
|
||||||
let path_buf = entry.path();
|
|
||||||
let filename = path_buf.to_string_lossy().to_string();
|
|
||||||
if filename.ends_with(EXTENSION_TXT) {
|
|
||||||
return (filename, false);
|
|
||||||
} else if filename.ends_with(EXTENSION_ZIP) {
|
|
||||||
return (filename, true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
println!("No tacview input file found in current directory.");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn read_data(filename: &String, is_zip: bool) -> Vec<String> {
|
|
||||||
let file = fs::File::open(filename).expect("Could not read from input file");
|
|
||||||
let buf = BufReader::new(file);
|
|
||||||
return if is_zip {
|
|
||||||
let mut archive = zip::ZipArchive::new(buf).expect("Could not read zip data");
|
|
||||||
let inner_file = archive.by_index(0).unwrap();
|
|
||||||
let inner_buf = BufReader::new(inner_file);
|
|
||||||
let lines: Vec<String> = inner_buf.lines()
|
|
||||||
.map(|l| l.expect("Could not parse line"))
|
|
||||||
.collect();
|
|
||||||
lines
|
|
||||||
} else {
|
|
||||||
let lines: Vec<String> = buf.lines()
|
|
||||||
.map(|l| l.expect("Could not parse line"))
|
|
||||||
.collect();
|
|
||||||
lines
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
|
||||||
|
use crate::tacview::{Coalition, CoalitionIDs};
|
||||||
|
|
||||||
|
const COMMENT: char = '#';
|
||||||
|
const MINUS: char = '-';
|
||||||
|
const ZERO: char = '0';
|
||||||
|
const UTF8_BOM: char = '\u{FEFF}'; // byte order mark. a String can begin with that, in which case
|
||||||
|
// we need to discard it.
|
||||||
|
|
||||||
|
pub fn split_into_header_and_body<S>(mut lines: Vec<S>) -> Result<(Vec<S>, Vec<S>)>
|
||||||
|
where
|
||||||
|
S: AsRef<str> + From<String>,
|
||||||
|
{
|
||||||
|
let mut split_idx = 0;
|
||||||
|
for line in lines.iter() {
|
||||||
|
if line
|
||||||
|
.as_ref()
|
||||||
|
.chars()
|
||||||
|
.find(|c| *c != UTF8_BOM)
|
||||||
|
.with_context(|| "found an empty line")?
|
||||||
|
== COMMENT
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
split_idx += 1;
|
||||||
|
}
|
||||||
|
let body = lines.split_off(split_idx);
|
||||||
|
Ok((lines, body))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_coalition_per_line<S>(body: &[S]) -> Result<Vec<Coalition>>
|
||||||
|
where
|
||||||
|
S: AsRef<str>,
|
||||||
|
{
|
||||||
|
let mut result = vec![];
|
||||||
|
let mut coalition_ids: CoalitionIDs = CoalitionIDs::new();
|
||||||
|
|
||||||
|
let mut old_line = Line::default();
|
||||||
|
|
||||||
|
for line_s in body {
|
||||||
|
let processed = Line::process_line(old_line, line_s, &mut coalition_ids)?;
|
||||||
|
result.push(processed.coalition.clone());
|
||||||
|
old_line = processed;
|
||||||
|
}
|
||||||
|
sanity_check(&result, body)?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sanity_check<S, T>(v1: &[S], v2: &[T]) -> Result<()> {
|
||||||
|
if v1.len() != v2.len() {
|
||||||
|
bail!("Warning: data is inconsistent\n{}\n{}", v1.len(), v2.len())
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Line {
|
||||||
|
line_type: LineType,
|
||||||
|
continued: bool,
|
||||||
|
coalition: Coalition,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Line {
|
||||||
|
fn process_line<'a, S: AsRef<str>>(
|
||||||
|
old_line: Line,
|
||||||
|
current_line: &'a S,
|
||||||
|
coalition_ids: &mut CoalitionIDs<'a>,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let line_type = match old_line.continued {
|
||||||
|
true => old_line.line_type,
|
||||||
|
false => LineType::find_type(current_line)
|
||||||
|
.with_context(|| "could not get line type for this line: {current_line}")?,
|
||||||
|
};
|
||||||
|
|
||||||
|
match line_type {
|
||||||
|
LineType::ArbitraryData => Ok(Line::new(
|
||||||
|
line_type,
|
||||||
|
Self::will_line_continue(current_line),
|
||||||
|
Coalition::All,
|
||||||
|
)),
|
||||||
|
LineType::Telemetry => Ok(Line::from_content(line_type, current_line, coalition_ids)
|
||||||
|
.with_context(|| "could not parse telemetry line")?),
|
||||||
|
LineType::Timestamp => Ok(Line::new(line_type, false, Coalition::All)),
|
||||||
|
LineType::Destruction => Ok(Line::from_content(line_type, current_line, coalition_ids)
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"could not parse this destruction line: {}",
|
||||||
|
current_line.as_ref()
|
||||||
|
)
|
||||||
|
})?),
|
||||||
|
LineType::Unknown => bail!("unknown line type"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_content<'a, S: AsRef<str>>(
|
||||||
|
line_type: LineType,
|
||||||
|
current_line: &'a S,
|
||||||
|
coalition_ids: &mut CoalitionIDs<'a>,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let id = Self::get_id_from_line(current_line, &line_type)?;
|
||||||
|
let continued = Self::will_line_continue(current_line);
|
||||||
|
let coalition = Self::assign_id_to_coalitions(coalition_ids, current_line, id)
|
||||||
|
.unwrap_or(Coalition::Unknown); // happens for the authentication line at the end of
|
||||||
|
// a file that has verification enabled
|
||||||
|
Ok(Line::new(line_type, continued, coalition))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assign_id_to_coalitions<'a, S: AsRef<str>>(
|
||||||
|
coalition_ids: &mut CoalitionIDs<'a>,
|
||||||
|
line: &S,
|
||||||
|
id: &'a str,
|
||||||
|
) -> Option<Coalition> {
|
||||||
|
if Coalition::line_contains_coalition(line) {
|
||||||
|
let coalition = Coalition::from_line(line);
|
||||||
|
coalition_ids.insert(id, &coalition);
|
||||||
|
Some(coalition)
|
||||||
|
} else {
|
||||||
|
coalition_ids.get(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn will_line_continue<S: AsRef<str>>(current_line: &S) -> bool {
|
||||||
|
current_line.as_ref().ends_with('\\')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_id_from_line<'a, S>(line: &'a S, line_type: &LineType) -> Result<&'a str>
|
||||||
|
where
|
||||||
|
S: AsRef<str>,
|
||||||
|
{
|
||||||
|
let local_line = line.as_ref();
|
||||||
|
let result = if line_type == &LineType::Destruction {
|
||||||
|
&local_line[1..local_line.len()]
|
||||||
|
} else {
|
||||||
|
local_line
|
||||||
|
.split_once(',')
|
||||||
|
.with_context(|| format!("cannot get ID from line {local_line}"))?
|
||||||
|
.0
|
||||||
|
};
|
||||||
|
if result.is_empty() {
|
||||||
|
bail!("cannot get ID from this line\n{local_line}")
|
||||||
|
} else {
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(line_type: LineType, continued: bool, coalition: Coalition) -> Self {
|
||||||
|
Self {
|
||||||
|
line_type,
|
||||||
|
continued,
|
||||||
|
coalition,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Line {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(LineType::Unknown, false, Coalition::Unknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Debug)]
|
||||||
|
enum LineType {
|
||||||
|
Unknown,
|
||||||
|
Timestamp,
|
||||||
|
Destruction,
|
||||||
|
Telemetry,
|
||||||
|
ArbitraryData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LineType {
|
||||||
|
fn find_type<S: AsRef<str>>(line: &S) -> Result<Self> {
|
||||||
|
let first_char = line
|
||||||
|
.as_ref()
|
||||||
|
.chars()
|
||||||
|
.find(|c| *c != UTF8_BOM)
|
||||||
|
.with_context(|| "line in tacview file is empty")?;
|
||||||
|
let mut buffer: [u8; 4] = [0; 4];
|
||||||
|
first_char.encode_utf8(&mut buffer);
|
||||||
|
if first_char == COMMENT {
|
||||||
|
Ok(Self::Timestamp)
|
||||||
|
} else if first_char == MINUS {
|
||||||
|
Ok(Self::Destruction)
|
||||||
|
} else if first_char == ZERO {
|
||||||
|
Ok(LineType::ArbitraryData)
|
||||||
|
} else if first_char.is_whitespace() {
|
||||||
|
bail!("found line beginning with whitespace")
|
||||||
|
} else {
|
||||||
|
Ok(LineType::Telemetry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::{Line, LineType};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_find_type() {
|
||||||
|
assert_eq!(LineType::find_type(&"#0").unwrap(), LineType::Timestamp);
|
||||||
|
assert_eq!(
|
||||||
|
LineType::find_type(&"102,T=5.0785362|6.203").unwrap(),
|
||||||
|
LineType::Telemetry
|
||||||
|
);
|
||||||
|
assert_eq!(LineType::find_type(&"-102").unwrap(), LineType::Destruction);
|
||||||
|
assert_eq!(
|
||||||
|
LineType::find_type(&"0,Authentication").unwrap(),
|
||||||
|
LineType::ArbitraryData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_find_type_for_space() {
|
||||||
|
LineType::find_type(&" ").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_id_from_line() {
|
||||||
|
let lines = vec![
|
||||||
|
("102,T=5.0785362|6.203", "102"),
|
||||||
|
("2d02,T=", "2d02"),
|
||||||
|
("248b02,T=6.70283", "248b02"),
|
||||||
|
];
|
||||||
|
for l in lines {
|
||||||
|
assert_eq!(
|
||||||
|
&Line::get_id_from_line(&l.0, &LineType::Telemetry).unwrap(),
|
||||||
|
&l.1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
let lines = vec![("-102", "102"), ("-2d02", "2d02"), ("-248b02", "248b02")];
|
||||||
|
for l in lines {
|
||||||
|
assert_eq!(
|
||||||
|
&Line::get_id_from_line(&l.0, &LineType::Destruction).unwrap(),
|
||||||
|
&l.1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::{BufRead, BufReader};
|
||||||
|
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
|
||||||
|
use crate::constants::{EXTENSION_TXT, EXTENSION_ZIP};
|
||||||
|
|
||||||
|
pub fn find_input_file() -> Result<(String, bool)> {
|
||||||
|
let read_dir = fs::read_dir(".").with_context(|| "could not open the current directory")?;
|
||||||
|
|
||||||
|
for entry_result in read_dir {
|
||||||
|
let path_buf = entry_result
|
||||||
|
.with_context(|| "error while iterating over directory")?
|
||||||
|
.path();
|
||||||
|
let filename = path_buf.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
if filename.ends_with(EXTENSION_TXT) {
|
||||||
|
return Ok((filename, false));
|
||||||
|
} else if filename.ends_with(EXTENSION_ZIP) {
|
||||||
|
return Ok((filename, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bail!("No tacview input file found in current directory.")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_data(filename: &str, is_zip: bool) -> Result<Vec<String>> {
|
||||||
|
let file = fs::File::open(filename).with_context(|| "could not open {filename}")?;
|
||||||
|
let buf = BufReader::new(file);
|
||||||
|
if is_zip {
|
||||||
|
Ok(read_zip(buf)?)
|
||||||
|
} else {
|
||||||
|
Ok(read_txt(buf)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_zip(buf: BufReader<File>) -> Result<Vec<String>> {
|
||||||
|
let mut archive = zip::ZipArchive::new(buf).with_context(|| "could not open zip archive")?;
|
||||||
|
let inner_file = archive
|
||||||
|
.by_index(0)
|
||||||
|
.with_context(|| "could not get the txt file in the zip archive")?;
|
||||||
|
let inner_buf = BufReader::new(inner_file);
|
||||||
|
read_txt(inner_buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_txt(buf: impl BufRead) -> Result<Vec<String>> {
|
||||||
|
let mut lines = vec![];
|
||||||
|
for line in buf.lines() {
|
||||||
|
lines.push(line.with_context(|| "could not read line from file, is it valid UTF-8?")?);
|
||||||
|
}
|
||||||
|
Ok(lines)
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone)]
|
||||||
|
pub enum Coalition {
|
||||||
|
Blue,
|
||||||
|
Red,
|
||||||
|
Violet,
|
||||||
|
All,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Coalition {
|
||||||
|
pub fn from_line<S: AsRef<str>>(line: &S) -> Self {
|
||||||
|
let line = line.as_ref();
|
||||||
|
if line.contains("Color=Blue") {
|
||||||
|
Self::Blue
|
||||||
|
} else if line.contains("Color=Red") {
|
||||||
|
Self::Red
|
||||||
|
} else if line.contains("Color=Violet") {
|
||||||
|
Self::Violet
|
||||||
|
} else {
|
||||||
|
Self::Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn line_contains_coalition<S: AsRef<str>>(line: &S) -> bool {
|
||||||
|
line.as_ref().contains("Color=")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Coalition {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
Self::Blue => "blue".to_string(),
|
||||||
|
Self::Red => "red".to_string(),
|
||||||
|
Self::Violet => "violet".to_string(),
|
||||||
|
Self::All => "all".to_string(),
|
||||||
|
Self::Unknown => "unknown".to_string(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CoalitionIDs<'a> {
|
||||||
|
pub blue: HashSet<&'a str>,
|
||||||
|
pub red: HashSet<&'a str>,
|
||||||
|
pub violet: HashSet<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> CoalitionIDs<'a> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (blue, red, violet) = (HashSet::new(), HashSet::new(), HashSet::new());
|
||||||
|
Self { blue, red, violet }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, id: &'a str, coalition: &Coalition) {
|
||||||
|
match coalition {
|
||||||
|
Coalition::Blue => self.blue.insert(id),
|
||||||
|
Coalition::Red => self.red.insert(id),
|
||||||
|
Coalition::Violet => self.violet.insert(id),
|
||||||
|
Coalition::All | Coalition::Unknown => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, id: &'a str) -> Option<Coalition> {
|
||||||
|
if self.blue.contains(id) {
|
||||||
|
Some(Coalition::Blue)
|
||||||
|
} else if self.red.contains(id) {
|
||||||
|
Some(Coalition::Red)
|
||||||
|
} else if self.violet.contains(id) {
|
||||||
|
Some(Coalition::Violet)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+176
@@ -0,0 +1,176 @@
|
|||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use zip::write::FileOptions;
|
||||||
|
use zip::ZipWriter;
|
||||||
|
|
||||||
|
use crate::constants::{EXTENSION_TXT, EXTENSION_ZIP};
|
||||||
|
use crate::tacview::Coalition;
|
||||||
|
|
||||||
|
pub fn create_writer(
|
||||||
|
is_zip: bool,
|
||||||
|
filename: &str,
|
||||||
|
coalition: &Coalition,
|
||||||
|
) -> Result<Box<dyn Write>> {
|
||||||
|
let base_name = remove_extension(filename, is_zip);
|
||||||
|
let txt_name = format!("{base_name}_{coalition}{EXTENSION_TXT}");
|
||||||
|
|
||||||
|
println!("writing {coalition}");
|
||||||
|
if is_zip {
|
||||||
|
let zip_name = format!("{base_name}_{coalition}{EXTENSION_ZIP}");
|
||||||
|
Ok(Box::new(create_zipwriter(&zip_name, &txt_name)?))
|
||||||
|
} else {
|
||||||
|
Ok(Box::new(create_textwriter(&txt_name)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_extension(filename: &str, is_zip: bool) -> &str {
|
||||||
|
if is_zip {
|
||||||
|
&filename[..(filename.len() - EXTENSION_ZIP.len())]
|
||||||
|
} else {
|
||||||
|
&filename[..(filename.len() - EXTENSION_TXT.len())]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_textwriter(filename: &str) -> Result<File> {
|
||||||
|
File::create(filename).with_context(|| "could not create text file")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_zipwriter(outer_name: &str, inner_name: &str) -> Result<ZipWriter<File>> {
|
||||||
|
let mut writer = ZipWriter::new(create_textwriter(outer_name)?);
|
||||||
|
writer.start_file(
|
||||||
|
inner_name,
|
||||||
|
FileOptions::default().compression_method(zip::CompressionMethod::Deflated),
|
||||||
|
)?;
|
||||||
|
Ok(writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait StringWriter
|
||||||
|
where
|
||||||
|
Self: Write,
|
||||||
|
{
|
||||||
|
fn write_line<S>(&mut self, line: &S) -> Result<()>
|
||||||
|
where
|
||||||
|
S: AsRef<str>,
|
||||||
|
{
|
||||||
|
writeln!(self, "{}", line.as_ref()).with_context(|| "could not write line")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_strings<S: AsRef<str>>(&mut self, lines: &[S]) -> Result<()> {
|
||||||
|
for line in lines {
|
||||||
|
self.write_line(line)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_for_coalition<S: AsRef<str>>(
|
||||||
|
&mut self,
|
||||||
|
lines: &[S],
|
||||||
|
coalition_per_line: &[Coalition],
|
||||||
|
coalition: &Coalition,
|
||||||
|
) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> StringWriter for T
|
||||||
|
where
|
||||||
|
T: Write,
|
||||||
|
{
|
||||||
|
fn write_for_coalition<S: AsRef<str>>(
|
||||||
|
&mut self,
|
||||||
|
lines: &[S],
|
||||||
|
coalition_per_line: &[Coalition],
|
||||||
|
coalition: &Coalition,
|
||||||
|
) -> Result<()> {
|
||||||
|
assert_eq!(lines.len(), coalition_per_line.len());
|
||||||
|
|
||||||
|
let iter = lines
|
||||||
|
.iter()
|
||||||
|
.zip(coalition_per_line)
|
||||||
|
.filter(|(_, c)| *c == coalition || **c == Coalition::All)
|
||||||
|
.map(|(l, _)| l);
|
||||||
|
|
||||||
|
for line in iter {
|
||||||
|
self.write_line(line)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct OutputData {
|
||||||
|
input_filename: Arc<String>,
|
||||||
|
is_zip: bool,
|
||||||
|
header: Arc<Vec<String>>,
|
||||||
|
body: Arc<Vec<String>>,
|
||||||
|
coalition_per_line: Arc<Vec<Coalition>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputData {
|
||||||
|
pub fn new(
|
||||||
|
input_filename: String,
|
||||||
|
is_zip: bool,
|
||||||
|
header: Vec<String>,
|
||||||
|
body: Vec<String>,
|
||||||
|
coalition_per_line: Vec<Coalition>,
|
||||||
|
) -> Self {
|
||||||
|
let body = Arc::new(body);
|
||||||
|
let coalition_per_line = Arc::new(coalition_per_line);
|
||||||
|
let header = Arc::new(header);
|
||||||
|
let input_filename = Arc::new(input_filename);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
is_zip,
|
||||||
|
body,
|
||||||
|
coalition_per_line,
|
||||||
|
header,
|
||||||
|
input_filename,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clippy wants us to combine both ierators into one. this is not what we want, because then
|
||||||
|
// we would spawn a thread, join it, and only then spawn a new one.
|
||||||
|
#[allow(clippy::needless_collect)]
|
||||||
|
pub fn save_to_disk(&self) -> Result<()> {
|
||||||
|
let coalitions = vec![Coalition::Blue, Coalition::Red, Coalition::Violet];
|
||||||
|
|
||||||
|
let handles: Vec<_> = coalitions
|
||||||
|
.iter()
|
||||||
|
.map(|coalition| {
|
||||||
|
let thread_self = self.clone();
|
||||||
|
let thread_coalition = coalition.clone();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
let mut writer = create_writer(
|
||||||
|
thread_self.is_zip,
|
||||||
|
&thread_self.input_filename,
|
||||||
|
&thread_coalition,
|
||||||
|
)
|
||||||
|
.with_context(|| format!("could not create writer for {thread_coalition}"))
|
||||||
|
.unwrap();
|
||||||
|
writer
|
||||||
|
.write_strings(&thread_self.header)
|
||||||
|
.with_context(|| format!("could not write header for {thread_coalition}"))
|
||||||
|
.unwrap();
|
||||||
|
writer
|
||||||
|
.write_for_coalition(
|
||||||
|
&thread_self.body,
|
||||||
|
&thread_self.coalition_per_line,
|
||||||
|
&thread_coalition,
|
||||||
|
)
|
||||||
|
.with_context(|| format!("could not write body for {thread_coalition}"))
|
||||||
|
.unwrap();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
handles.into_iter().zip(coalitions).for_each(|(h, c)| {
|
||||||
|
h.join()
|
||||||
|
.unwrap_or_else(|_| println!("could not write data for {c}"))
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user