17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 00:29:19 +02:00
Files
maszyna/tools/editor/signal_headers.py
2026-10-09 18:40:31 +02:00

238 lines
8.6 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Writes //$e descriptions into the signal templates of the scenery directory, so the editor can place them.
usage: signal_headers.py <data root> [--write]
without --write it only reports what it would write
"""
import os
import re
import sys
import tarfile
import time
FAMILIES = [
(re.compile(r'^ss[dow]?\d'), 'main'),
(re.compile(r'^sk(\d|-un)'), 'main'),
(re.compile(r'^sbl\d'), 'block'),
(re.compile(r'^(ts\d|tk\d|top|tsosp)'), 'warning'),
(re.compile(r'^(ps\d|sp3)'), 'repeater'),
(re.compile(r'^(ms\d|zs2nb)'), 'shunt'),
(re.compile(r'^(zs1c|zs2cb|tzm|z1)'), 'stop'),
(re.compile(r'^sz(i|k|y|yn|-ksztalt\d?)\.inc$'), 'substitute'),
]
COLOURS = [('zielon', 'z'), ('czerwon', 'c'), ('pomara', 'p'), ('żółt', 'p'), ('biał', 'b'), ('mleczn', 'b'), ('niebiesk', 'n'), ('ślep', 'x'), ('brak', 'x')]
LINEMARK = '//$e'
def tokens(text):
text = re.sub(r'//[^\n]*', ' ', text)
text = re.sub(r'/\*.*?\*/', ' ', text, flags=re.S)
return re.split(r'[\s;]+', text.lower())
def statements(words, keyword):
out = []
idx = 0
while idx < len(words):
if words[idx] == keyword:
if keyword == 'event':
stop = idx
while stop < len(words) and words[stop] != 'endevent':
stop += 1
else:
stop = idx + 10
out.append(words[idx:stop])
idx += 1
return out
def examine(path, file):
raw = open(path, 'rb').read() # NOSONAR
try:
text = raw.decode('utf-8')
encoding = 'utf-8'
except UnicodeDecodeError:
text = raw.decode('cp1250')
encoding = 'cp1250'
lines = [line for line in text.splitlines() if not line.startswith(LINEMARK)]
body = '\n'.join(lines)
words = tokens(body)
count = max([int(m) for m in re.findall(r'\(p(\d+)\)', body)] or [0])
if 'origin' not in words or words[words.index('origin') + 1:words.index('origin') + 4] != ['(p2)', '(p3)', '(p4)']:
return None, 'no origin (p2) (p3) (p4)'
if 'rotate' not in words or words[words.index('rotate') + 1:words.index('rotate') + 4] != ['0', '(p5)', '0']:
return None, 'no rotate 0 (p5) 0'
if count < 5:
return None, 'fewer than 5 parameters'
models = [n for n in statements(words, 'node') if len(n) > 9 and n[4] == 'model']
head = next((n for n in models if n[3].startswith('(p1)')), None)
if head is None:
return None, 'no model named after (p1)'
model = head[9]
cells = {}
for n in statements(words, 'node'):
if len(n) > 8 and n[4] == 'memcell':
cells[n[3]] = n[8]
read = ''
for e in statements(words, 'event'):
velocity = (len(e) > 4 and e[2] == 'getvalues' and cells.get(e[4]) in ('setvelocity', 'shuntvelocity')) or (len(e) > 8 and e[2] == 'putvalues' and e[8] in ('setvelocity', 'shuntvelocity'))
if velocity and e[1].startswith('(p1)'):
if not read or e[1].endswith('_sem_info'):
read = e[1]
kind = next((k for pattern, k in FAMILIES if pattern.search(file)), None)
if kind is None:
return None, 'not a signal family'
if kind == 'repeater' and cells:
return None, 'repeater with a memory cell'
if kind in ('main', 'block', 'shunt', 'stop', 'substitute') and not read and cells:
return None, 'memory cell which no event reads for the train'
if kind in ('warning', 'repeater'):
read = ''
models_used = [n[9] for n in models]
lamps = ''
lean = ''
m = re.match(r'sem/(?:[a-z]+)_(\d)_([lpyk])_([a-z]+)[_.]', model)
if m:
lamps = m.group(3)
lean = {'l': 'left', 'y': 'right', 'p': '', 'k': ''}[m.group(2)]
else:
m = re.match(r'sem/karzelki/k(?:[a-z]*?\d|tm|to|s)([zcpbnx]+)[.+]', model) or re.match(r'sem/glowice/(?:s|to)\d(?:i|y[lp])([zcpbnx]+)_dd\.', model)
if m:
lamps = m.group(1)
if not lamps:
for line in lines:
if line.lower().lstrip('/ ').startswith('komory:'):
for word in line.split(':', 1)[1].split(','):
word = word.strip().lower()
lamps += next((c for prefix, c in COLOURS if word.startswith(prefix)), '')
break
if 'karzelki/' in model or re.search(r'k(w\d+)?(\+.*)?\.inc$', file) or 'pod_betonowa' in body:
mount = 'dwarf'
elif '_drb_' in model and not any(('slupy/' in m or 'post' in m or 'drabinki/' in m) for m in models_used):
mount = 'gantry'
else:
mount = 'mast'
description = ''
for line in lines:
stripped = line.strip()
if stripped.startswith('//'):
stripped = stripped.lstrip('/').strip()
if stripped.lower().startswith('typ:'):
stripped = stripped[4:].strip()
if stripped:
description = stripped
break
elif stripped:
break
said = description.lower()
if 'w lewo' in said:
lean = 'left'
elif 'w prawo' in said:
lean = 'right'
elif 'prost' in said:
lean = ''
params = [
"{id: 1, role: name, label: Name, default: A}",
"{id: 2, role: pos.x}",
"{id: 3, role: pos.y}",
"{id: 4, role: pos.z}",
"{id: 5, role: rot.y}",
]
if count >= 6:
params.append("{id: 6, role: texture, label: Plate, default: 0_nothing}" if 'tabl/(p6)' in body.lower() else "{id: 6, role: text, label: Symbol, default: none}")
for id in range(7, count + 1):
label = 'Linked signal' if id == 7 else 'Parameter %d' % id
params.append("{id: %d, role: text, label: %s, default: none}" % (id, label))
signal = ['kind: ' + kind, 'mount: ' + mount]
if lamps:
signal.append('lamps: ' + lamps)
if lean:
signal.append('lean: ' + lean)
if read:
signal.append('read: "' + read + '"')
header = [
'version: 1',
'name: ' + file[:-4],
'category: signal',
]
if description:
header.append('description: "' + description.replace('\\', '\\\\').replace('"', '\\"') + '"')
header.append('signal: {' + ', '.join(signal) + '}')
header.append('params:')
header += [' - ' + p for p in params]
return {'raw': raw, 'text': text, 'encoding': encoding, 'header': header, 'kind': kind, 'model': model, 'read': read, 'lamps': lamps, 'mount': mount, 'lean': lean, 'count': count}, ''
def rewrite(info):
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
root = os.path.join(sys.argv[1], 'scenery')
write = '--write' in sys.argv
files = sorted(f for f in os.listdir(root) if f.endswith('.inc'))
files += ['pkp/semafory/' + f for f in sorted(os.listdir(os.path.join(root, 'pkp', 'semafory'))) if f.endswith('.inc')]
done = []
skipped = []
for file in files:
name = os.path.basename(file).lower()
if not any(pattern.search(name) for pattern, _ in FAMILIES):
continue
info, reason = examine(os.path.join(root, file), name)
if info is None:
skipped.append((file, reason))
continue
done.append((file, info))
for file, info in done:
print('%-34s %-10s %-6s %-6s %-6s p%d %-22s %s' % (file, info['kind'], info['mount'], info['lean'] or '-', info['lamps'] or '-', info['count'], info['read'] or '-', info['model']))
for file, reason in skipped:
print('SKIP %-34s %s' % (file, reason))
kinds = {}
for _, info in done:
kinds[info['kind']] = kinds.get(info['kind'], 0) + 1
print('described: %d %s, skipped: %d' % (len(done), kinds, len(skipped)))
if write:
backup = os.path.join(sys.argv[1], 'editor_backup')
os.makedirs(backup, exist_ok=True) # NOSONAR
archive = os.path.join(backup, time.strftime('signals_%Y%m%d_%H%M%S.tar'))
with tarfile.open(archive, 'w') as tar: # NOSONAR
for file, _ in done:
tar.add(os.path.join(root, file), arcname='scenery/' + file) # NOSONAR
for file, info in done:
content = rewrite(info)
if content != info['raw']:
with open(os.path.join(root, file), 'wb') as out: # NOSONAR
out.write(content)
print('written, originals in', archive)
return 0
if __name__ == '__main__':
sys.exit(main())