9 Commits
Author SHA1 Message Date
132nd-Professor 018717ead7 gitignore: added release/ 2021-06-08 22:24:16 +02:00
132nd-Professor 782ada131b replaced gnu-make with pyinvoke 2021-06-08 22:23:45 +02:00
132nd-Professor ab01a14719 zip files: use filename as provided by zip archive
We assumed that the txt file is always at the top level of the zip file
The tacview files from Hoggit have them nested under multiple directories
fixes #1
2021-06-08 21:30:38 +02:00
132nd-Professor 18329155b3 last cleanup, added some docstrings 2021-06-08 15:45:54 +02:00
132nd-Professor abc5584856 even more refactoring to improve code quality 2021-06-08 15:32:14 +02:00
132nd-Professor b42a711b0c removed unused constant 2021-06-07 23:29:15 +02:00
132nd-Professor 15fd64210f Pycharm: adjusted compatiblity checks 2021-06-07 23:23:03 +02:00
132nd-Professor 3b26c32922 major refactoring to improve readability 2021-06-07 23:21:59 +02:00
132nd-Professor 130ceb60d8 added simple Makefile 2021-06-07 20:19:00 +02:00
5 changed files with 246 additions and 133 deletions
+2
View File
@@ -1,6 +1,8 @@
*.zip.acmi *.zip.acmi
*.txt.acmi *.txt.acmi
release/
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
+1
View File
@@ -1,5 +1,6 @@
<component name="InspectionProjectProfileManager"> <component name="InspectionProjectProfileManager">
<settings> <settings>
<option name="PROJECT_PROFILE" value="Default" />
<option name="USE_PROJECT_PROFILE" value="false" /> <option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" /> <version value="1.0" />
</settings> </settings>
+3
View File
@@ -1,4 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (tacview-splitter)" project-jdk-type="Python SDK" /> <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (tacview-splitter)" project-jdk-type="Python SDK" />
<component name="PythonCompatibilityInspectionAdvertiser">
<option name="version" value="3" />
</component>
</project> </project>
+193 -133
View File
@@ -1,156 +1,216 @@
#!/usr/bin/env ipython #!/usr/bin/env ipython
from __future__ import annotations
from dataclasses import dataclass
from os import listdir as os_listdir from os import listdir as os_listdir
import zipfile from typing import Tuple
from zipfile import ZipFile, ZIP_DEFLATED
EXTENSION_TXT = '.txt.acmi' EXTENSION_TXT = '.txt.acmi'
EXTENSION_ZIP = '.zip.acmi' EXTENSION_ZIP = '.zip.acmi'
is_zip = None
all_files = os_listdir('.') def main():
for filename in all_files: filename_input, is_zip = find_input_file()
filename_lower = filename.lower() print('Processing ' + str(filename_input))
if filename_lower.endswith(EXTENSION_ZIP): filenames = Filenames(filename_input, is_zip)
is_zip = True tacview_lines = read_data(filenames)
break # set up all the file descriptors we will need
elif filename_lower.endswith(EXTENSION_TXT): descriptors = Descriptors(filenames)
is_zip = False # replicate the header from the input file into the output files
break tacview_lines_no_header = move_header_to_output_files(tacview_lines, descriptors)
else: undecided_ids = move_content_to_output_files(tacview_lines_no_header, descriptors)
raise FileNotFoundError('Could not find a tacview file in this directory.') descriptors.close()
# sanity check
if len(undecided_ids) != 0:
print('There were units that are neither BLUE, RED nor NEUTRAL. Please investigate.')
print(undecided_ids)
filename_input = filename
print('Processing ' + str(filename_input))
if is_zip: def move_content_to_output_files(tacview_lines_no_header: list[str], descriptors: Descriptors) -> list[str]:
filename_no_extension = filename_input.replace(EXTENSION_ZIP, '') """
else: core routine: process all telemetry lines, put them in the correct output file
filename_no_extension = filename_input.replace(EXTENSION_TXT, '') for lines that are time stamps, we put these in all output files
:param tacview_lines_no_header: tacview telemetry, header removed
:param descriptors: holding all the opened descriptors
:return: list of unit ids that belong to neither blue, red, nor violet
"""
#
blue_ids, red_ids, violet_ids, undecided_ids = (list() for _ in range(4))
# violet = neutral faction, used for chaffs, flares, decoys and shrapnel. we can't decide easily which faction
# they belong to. we would need to find the blue or red object with the least distance to violet objects
# around their spawn time
continued = False
for line in tacview_lines_no_header:
# tacview introduced continued lines, signified by a single backslash at EOL
# example: DCS briefing is copied into tacview file (begins with `0,Briefing=`)
# if the line was not continued, we need to extract the unit ID from the line
# otherwise we reuse the ID from the previous loop
if not continued:
# the first time a unit appears it has the Color in its line.
# The first part of the line (before the first comma) is the unique ID of the unit.
if line[0] == '#': # check if line is a time stamp
id_ = 'both'
elif line[0] == '-': # negative id is used to indicate a destroyed unit
id_ = line[1:].strip()
else: # otherwise it is a standard unit line
id_, rest = line.split(',', 1)
if 'Color=Red' in line:
red_ids.append(id_)
elif 'Color=Blue' in line:
blue_ids.append(id_)
elif 'Color=Violet' in line:
violet_ids.append(id_)
elif 'Color=' in line:
undecided_ids.append(id_)
filename_blue_no_extension, filename_red_no_extension, filename_violet_no_extension = \ if descriptors.filenames.input.is_zip:
(f'{filename_no_extension}_{color}' for color in ('blue', 'red', 'violet')) line_output = line.encode()
else:
line_output = line
# code checker thinks that id_ can be unbound, which it cannot
# noinspection PyUnboundLocalVariable
if id_ in blue_ids or id_ == 'both':
descriptors.blue_txt.write(line_output)
if id_ in red_ids or id_ == 'both':
descriptors.red_txt.write(line_output)
if id_ in violet_ids or id_ == 'both':
descriptors.violet_txt.write(line_output)
if is_zip: if line.endswith('\\\n'):
filename_blue_zip, filename_red_zip, filename_violet_zip = \ continued = True
(f'{arg}{EXTENSION_ZIP}' for arg in else:
(filename_blue_no_extension, filename_red_no_extension, filename_violet_no_extension) continued = False
) return undecided_ids
filename_blue_txt, filename_red_txt, filename_violet_txt = \
(f'{arg}{EXTENSION_TXT}' for arg in
(filename_blue_no_extension, filename_red_no_extension, filename_violet_no_extension)
)
if is_zip: def move_header_to_output_files(tacview_lines: list[str], descriptors: Descriptors) -> list[str]:
with zipfile.ZipFile(filename_input) as fd_zip: """
with fd_zip.open(filename_no_extension + EXTENSION_TXT) as fd_tacview: finds the tacview header in tacview_lines and writes it to the three output files
tacview_binary_lines = fd_tacview.readlines() afterwards removes the header from the input data and returns the remaining content (actual telemetry)
tacview_raw_lines = [] :param tacview_lines: content of tacview file with header
for line in tacview_binary_lines: :param descriptors: object of Descriptor class
tacview_raw_lines.append(line.decode()) :return: content of tacview file without header
else: """
with open(filename_input) as fd_tacview: for i, line in enumerate(tacview_lines):
tacview_raw_lines = fd_tacview.readlines() if descriptors.filenames.input.is_zip:
line_header = line.encode()
if is_zip: else:
# noinspection PyUnboundLocalVariable line_header = line
fd_blue_zip = zipfile.ZipFile(filename_blue_zip, 'w', zipfile.ZIP_DEFLATED) if not line[0] == '#': # header is everything before the first '#'
# noinspection PyUnboundLocalVariable descriptors.blue_txt.write(line_header)
fd_red_zip = zipfile.ZipFile(filename_red_zip, 'w', zipfile.ZIP_DEFLATED) descriptors.red_txt.write(line_header)
# noinspection PyUnboundLocalVariable descriptors.violet_txt.write(line_header)
fd_violet_zip = zipfile.ZipFile(filename_violet_zip, 'w', zipfile.ZIP_DEFLATED) else:
break
fd_blue_txt = fd_blue_zip.open(filename_blue_txt, 'w')
fd_red_txt = fd_red_zip.open(filename_red_txt, 'w')
fd_violet_txt = fd_violet_zip.open(filename_violet_txt, 'w')
else:
fd_blue_txt = open(filename_blue_txt, 'w')
fd_red_txt = open(filename_red_txt, 'w')
fd_violet_txt = open(filename_violet_txt, 'w')
for i, line in enumerate(tacview_raw_lines):
if is_zip:
line_header = line.encode()
else: else:
line_header = line raise IOError('Tacview file seems to be empty')
if not line[0] == '#': # header is everything before the first '#' return tacview_lines[i:]
fd_blue_txt.write(line_header)
fd_red_txt.write(line_header)
fd_violet_txt.write(line_header) def read_data(filenames: Filenames) -> list[str]:
"""
get the tacview data out of the file
:param filenames: object of class Filenames
:return: file content as list of strings, one line per item
"""
if filenames.input.is_zip:
with ZipFile(filenames.input.zip) as fd_zip:
with fd_zip.open(fd_zip.filelist[0].filename) as fd_tacview:
tacview_lines_binary = fd_tacview.readlines()
tacview_lines_ascii = []
for line in tacview_lines_binary:
tacview_lines_ascii.append(line.decode())
else: else:
break with open(filenames.input.txt) as fd_tacview:
else: tacview_lines_ascii = fd_tacview.readlines()
raise IOError('Tacview file seems to be empty') return tacview_lines_ascii
tacview_raw_lines = tacview_raw_lines[i:] # remove the header, we don't need it anymore
blue_ids = [] def find_input_file() -> Tuple[str, bool]:
red_ids = [] all_files = os_listdir('.')
# violet = neutral faction, used for chaffs, flares, decoys and shrapnel for filename in all_files:
# we can't decide easily which faction they belong to filename_lower = filename.lower()
# we would need to find the blue or red object with the least distance if filename_lower.endswith(EXTENSION_ZIP):
# to violet objects around their spawn time is_zip = True
violet_ids = [] break
undecided_ids = [] elif filename_lower.endswith(EXTENSION_TXT):
is_zip = False
continued = False break
for line in tacview_raw_lines:
# tacview introduced continued lines, signified by a single backslash at EOL
# example: DCS briefing is copied into tacview file (begins with `0,Briefing=`)
# if the line was not continued, we need to extract the unit ID from the line
# otherwise we reuse the ID from the previous loop
if not continued:
# the first time a unit appears it has the Color in its line.
# The first part of the line (before the first comma) is the unique ID of the unit.
if line[0] == '#': # check if line is a time stamp
id_ = 'both'
elif line[0] == '-': # negative id is used to indicate a destroyed unit
id_ = line[1:].strip()
else: # otherwise it is a standard unit line
id_, rest = line.split(',', 1)
if 'Color=Red' in line:
red_ids.append(id_)
elif 'Color=Blue' in line:
blue_ids.append(id_)
elif 'Color=Violet' in line:
violet_ids.append(id_)
elif 'Color=' in line:
undecided_ids.append(id_)
if is_zip:
line_output = line.encode()
else: else:
line_output = line raise FileNotFoundError('Could not find a tacview file in this directory.')
# noinspection PyUnboundLocalVariable return filename, is_zip
if id_ in blue_ids:
fd_blue_txt.write(line_output)
elif id_ in red_ids:
fd_red_txt.write(line_output)
elif id_ in violet_ids:
fd_violet_txt.write(line_output)
else: # id_ == 'both', has timestamps
fd_blue_txt.write(line_output)
fd_red_txt.write(line_output)
fd_violet_txt.write(line_output)
if line.endswith('\\\n'):
continued = True
else:
continued = False
fd_blue_txt.close() @dataclass
fd_red_txt.close() class Filenames:
fd_violet_txt.close() @dataclass
class _Input:
def __init__(self, filename_input: str, is_zip: bool):
self.zip: str = ''
self.txt: str = ''
self.no_extension: str = ''
self.is_zip: bool = is_zip
if is_zip:
self.zip = filename_input
self.txt = filename_input.replace(EXTENSION_ZIP, EXTENSION_TXT)
self.no_extension = filename_input.replace(EXTENSION_ZIP, '')
else:
self.zip = ''
self.txt = filename_input
self.no_extension = filename_input.replace(EXTENSION_TXT, '')
if is_zip: @dataclass
# noinspection PyUnboundLocalVariable class _Output:
fd_blue_zip.close() @dataclass
# noinspection PyUnboundLocalVariable class _Coalition:
fd_red_zip.close() def __init__(self, filename_input_no_extension: str, color: str):
# noinspection PyUnboundLocalVariable self.no_extension: str = f'{filename_input_no_extension}_{color}'
fd_violet_zip.close() self.zip: str = f'{self.no_extension}{EXTENSION_ZIP}'
self.txt: str = f'{self.no_extension}{EXTENSION_TXT}'
if len(undecided_ids) != 0: def __init__(self, filename_input_no_extension: str):
print('There were units that are neither BLUE, RED nor NEUTRAL. Please investigate.') self.blue = self._Coalition(filename_input_no_extension, 'blue')
print(undecided_ids) self.red = self._Coalition(filename_input_no_extension, 'red')
self.violet = self._Coalition(filename_input_no_extension, 'violet')
def __init__(self, filename_input: str, is_zip: bool):
self.input = self._Input(filename_input, is_zip)
self.output = self._Output(self.input.no_extension)
@dataclass
class Descriptors:
def __init__(self, filenames: Filenames):
self.filenames = filenames
if filenames.input.is_zip:
self._blue_zip = ZipFile(filenames.output.blue.zip, 'w', ZIP_DEFLATED)
self._red_zip = ZipFile(filenames.output.red.zip, 'w', ZIP_DEFLATED)
self._violet_zip = ZipFile(filenames.output.violet.zip, 'w', ZIP_DEFLATED)
self.blue_txt = self._blue_zip.open(filenames.output.blue.txt, 'w')
self.red_txt = self._red_zip.open(filenames.output.blue.txt, 'w')
self.violet_txt = self._violet_zip.open(filenames.output.blue.txt, 'w')
else:
self._blue_zip = None
self._red_zip = None
self._violet_zip = None
self.blue_txt = open(filenames.output.blue.txt, 'w')
self.red_txt = open(filenames.output.red.txt, 'w')
self.violet_txt = open(filenames.output.violet.txt, 'w')
def close(self):
self.blue_txt.close()
self.red_txt.close()
self.violet_txt.close()
if self.filenames.input.is_zip:
self._blue_zip.close()
self._red_zip.close()
self._violet_zip.close()
if __name__ == '__main__':
main()
+47
View File
@@ -0,0 +1,47 @@
from invoke import task
@task
def build(c):
c.run("pyinstaller --onefile tacview-splitter.py")
@task
def clean(c):
c.run("rm -rf build __pycache__ tacview-splitter.spec")
@task(pre=[clean])
def distclean(c):
c.run("rm -rf dist release")
@task(pre=[build])
def release(c, version):
c.run(f"""
mkdir -p release/linux/tacview-splitter-{version}
mkdir -p release/win/tacview-splitter-{version}
cd release/linux/tacview-splitter-{version}
cp ../../../README.md .
cp ../../../LICENSE .
cp ../../../dist/tacview-splitter .
cd ..
tar -cvzf tacview-splitter-linux_x86-64.tar.gz tacview-splitter-{version}
mv tacview-splitter-linux_x86-64.tar.gz ..
cd ../win/tacview-splitter-{version}
cp ../../../README.md .
cp ../../../LICENSE LICENSE.txt
echo Copy the windows release file into the following directory:
pwd
echo -n then press enter ...
read
cd ..
7z a tacview-splitter-win_x86-64.zip tacview-splitter-{version}
mv tacview-splitter-win_x86-64.zip ..
cd ..
rm -f sha256sums.txt
sha256sum tacview-splitter-win_x86-64.zip tacview-splitter-linux_x86-64.tar.gz >> sha256sums.txt
sha256sum --check sha256sums.txt
""")