sots-re/save-reader/ref/parsers/effect_txt.py

98 lines
3.2 KiB
Python

"""effect_txt.py -- reader for Effects/*.effect (particle-effect definitions).
NOT the brace-block format. Layout:
TXT # magic first line
KEY value # scalar (number, TRUE/FALSE, "quoted")
KEY # group: KEY on its own line, then
BEGIN
...nested KEY value / groups...
END
Order matters: 'PARTICLEDATATYPE n' is followed by the CREATION /
VARIATION / OVERLIFE curves that belong to that datatype, and 'MODIFIER'
repeats once per type. So each level is returned as an ordered list of
[key, value] pairs (value = scalar or nested list). to_dict() gives a
dict view (repeats -> lists) when order is not needed.
Quirks handled: one file has CRLF; 'NAME "New Emitter"' values contain
spaces; indentation is cosmetic (tabs); the format is line-based.
Stdlib only.
"""
from __future__ import annotations
from typing import Any
from flat_kv import split_tokens, strip_comment
from mars_data import coerce
__all__ = ["parse", "parse_file", "to_dict", "EffectSyntaxError"]
Pairs = list # list[[key, value]]
class EffectSyntaxError(ValueError):
pass
def parse(text: str, *, typed: bool = True) -> Pairs:
lines = text.splitlines()
if not lines or lines[0].strip() != "TXT":
raise EffectSyntaxError("missing TXT magic")
stack: list[Pairs] = [[]]
pending_key: str | None = None
for lineno, raw in enumerate(lines[1:], 2):
line = strip_comment(raw).strip()
if not line:
continue
if line == "BEGIN":
if pending_key is None:
raise EffectSyntaxError(f"line {lineno}: BEGIN without a key")
grp: Pairs = []
stack[-1].append([pending_key, grp])
stack.append(grp)
pending_key = None
continue
if line == "END":
if len(stack) == 1:
raise EffectSyntaxError(f"line {lineno}: END without BEGIN")
stack.pop()
continue
if pending_key is not None:
raise EffectSyntaxError(f"line {lineno}: key {pending_key!r} not followed by BEGIN")
toks = split_tokens(line)
key = toks[0][0]
if len(toks) == 1:
pending_key = key
continue
vals = [coerce(t) if (typed and not q) else t for t, q in toks[1:]]
stack[-1].append([key, vals[0] if len(vals) == 1 else vals])
if len(stack) != 1:
raise EffectSyntaxError(f"{len(stack) - 1} unclosed BEGIN group(s)")
if pending_key is not None:
raise EffectSyntaxError(f"trailing key {pending_key!r} without BEGIN")
return stack[0]
def to_dict(pairs: Pairs) -> dict:
d: dict = {}
for key, val in pairs:
if isinstance(val, list) and val and isinstance(val[0], list) and len(val[0]) == 2 and isinstance(val[0][0], str):
val = to_dict(val)
if key in d:
if not isinstance(d[key], list) or not getattr(d[key], "_rep", False):
d[key] = _Rep([d[key]])
d[key].append(val)
else:
d[key] = val
return d
class _Rep(list):
_rep = True
def parse_file(path, **kw) -> Pairs:
with open(path, "rb") as f:
return parse(f.read().decode("cp1252"), **kw)