98 lines
3.2 KiB
Python
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)
|