"""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)