diff --git a/tools/editor/turntable_headers.py b/tools/editor/turntable_headers.py new file mode 100755 index 00000000..8a81b128 --- /dev/null +++ b/tools/editor/turntable_headers.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Writes //$e descriptions into the turntable templates of the scenery, so the editor places them as turntables. + +usage: turntable_headers.py [--write] [--zip ] +without --write it only reports what it would write; --zip packs every described turntable template with its scenery/ path +""" + +import os +import re +import sys +import tarfile +import time +import zipfile + +LINEMARK = '//$e' +FOLDER = 'scenery' +ROTATION = re.compile(r'^event\s+\S*tor(\d+)rot\s+animation\s+\S+\s+\S+\s+rotate\s+\S+\s+\S+\s+\S+\s+(\S+)', re.I | re.M) +MODEL = re.compile(r'model\s+0\s+0\s+0\s+0\s+\S*?obrotnica(\d+)m\.t3d\s+\(p6\)', re.I) + + +def decode(raw): + try: + text = raw.decode('utf-8') + encoding = 'utf-8' if raw.startswith(b'\xef\xbb\xbf') or max(raw, default=0) > 127 else 'cp1250' + except UnicodeDecodeError: + text = raw.decode('cp1250') + encoding = 'cp1250' + return text, encoding + + +def examine(path, relative): + raw = open(path, 'rb').read() # NOSONAR + if re.search(rb'tor\d+rot\s+animation', raw, re.I) is None: + return None, None + text, encoding = decode(raw) + marks = [line for line in text.splitlines() if line.startswith(LINEMARK)] + if any('category: turntable' in line for line in marks): + return {'raw': raw, 'header': None, 'described': True}, '' + body = '\n'.join(line for line in text.splitlines() if not line.startswith(LINEMARK)) + lower = body.lower() + if 'origin (p2) (p3) (p4)' not in lower or 'rotate 0 (p5) 0' not in lower: + return None, 'not origin (p2) (p3) (p4) rotate 0 (p5) 0' + model = MODEL.search(body) + if model is None: + return None, 'no obrotnicam.t3d model with surface (p6)' + positions = {} + for number, angle in ROTATION.findall(body): + positions.setdefault(int(number), float(angle)) + if not positions: + return None, 'no positions' + count = max(int(n) for n in re.findall(r'\(p(\d+)\)', lower)) + if count != 6: + return None, 'uses %d parameters, not 6' % count + length = int(model.group(1)) + angles = [positions[n] for n in sorted(positions)] + folder, file = os.path.split(relative) + name = 'obrotnica %d m, %d %s' % (length, len(angles), 'stałe położenia' if len(angles) < 5 else 'stałych położeń') + if folder != 'eng': + name += ' (%s)' % os.path.splitext(file)[0] + description = 'Obrotnica %d m o stałych położeniach mostu: %s' % (length, ' '.join('%g' % a for a in angles)) + header = [ + 'version: 1', + 'name: ' + name, + 'category: turntable', + 'description: "' + description + '"', + 'turntable: {length: %d}' % length, + 'params:', + ' - {id: 1, role: track, label: Name}', + ' - {id: 2, role: pos.x}', + ' - {id: 3, role: pos.y}', + ' - {id: 4, role: pos.z}', + ' - {id: 5, role: rot.y}', + ' - {id: 6, label: Surface, default: eng/most1-jezdniablach}', + ] + return {'raw': raw, 'text': text, 'encoding': encoding, 'header': header, 'described': False, 'length': length, 'angles': angles}, '' + + +def rewrite(info): + if info['header'] is None: + return info['raw'] + text = info['text'] + eol = '\r\n' if '\r\n' in text else '\n' + bom = text.startswith('') + if bom: + text = text[1:] + kept = [line for line in text.split(eol) if not line.startswith(LINEMARK)] + block = [LINEMARK + ' ' + line for line in info['header']] + out = eol.join(block + kept) + return (('' if bom else '') + out).encode(info['encoding']) + + +def main(): + if len(sys.argv) < 2: + print(__doc__) + return 1 + data = sys.argv[1] + folder = os.path.join(data, FOLDER) + write = '--write' in sys.argv + pack = sys.argv[sys.argv.index('--zip') + 1] if '--zip' in sys.argv else None + done = [] + skipped = [] + for root, _, files in os.walk(folder): + for file in sorted(files): + if not file.lower().endswith('.inc'): + continue + relative = os.path.relpath(os.path.join(root, file), folder).replace(os.sep, '/') + info, reason = examine(os.path.join(root, file), relative) + if info is not None: + done.append((relative, info)) + elif reason: + skipped.append((relative, reason)) + done.sort() + for file, info in done: + if info['described']: + print('%-40s already described' % file) + else: + print('%-40s %d m, positions %s' % (file, info['length'], ' '.join('%g' % a for a in info['angles']))) + for file, reason in skipped: + print('skipped %s: %s' % (file, reason)) + fresh = [(file, info) for file, info in done if not info['described']] + print('%d described now, %d already described, %d skipped' % (len(fresh), len(done) - len(fresh), len(skipped))) + if write and fresh: + backup = os.path.join(data, 'editor_backup') + os.makedirs(backup, exist_ok=True) + archive = os.path.join(backup, 'turntables_%s.tar' % time.strftime('%Y%m%d_%H%M%S')) + with tarfile.open(archive, 'w') as tar: + for file, _ in fresh: + tar.add(os.path.join(folder, file), arcname=FOLDER + '/' + file) + for file, info in fresh: + with open(os.path.join(folder, file), 'wb') as out: + out.write(rewrite(info)) + print('written, originals in ' + archive) + if pack: + with zipfile.ZipFile(pack, 'w', zipfile.ZIP_DEFLATED) as out: + for file, info in done: + out.writestr(FOLDER + '/' + file, rewrite(info)) + print('packed into ' + pack) + return 0 + + +if __name__ == '__main__': + sys.exit(main())