From 5979c718fef8ba8db590cffa5067f699733ba8e1 Mon Sep 17 00:00:00 2001 From: patryk025 Date: Fri, 6 Mar 2026 23:43:12 +0000 Subject: [PATCH] Updated script Thanks Codex --- original_code_parsing_reconstr.py | 552 ++++++++++++++++++++++++------ 1 file changed, 450 insertions(+), 102 deletions(-) diff --git a/original_code_parsing_reconstr.py b/original_code_parsing_reconstr.py index e6fc237..3f11f83 100644 --- a/original_code_parsing_reconstr.py +++ b/original_code_parsing_reconstr.py @@ -1,102 +1,450 @@ -code = "{@STRING(\"TEST\", \"\");@IF(\"2+3*4'\"2+3*4\"&&1'1\",\"{@RETURN(\"TRUE\");}\",\"{@RETURN(\"FALSE\");}\");@RETURN(TEST);}" - -def prepare_input(code: str) -> str: - # original used right and left instructions for striping code from braces (yeah, they are unnecessary) - code = code.strip() - if code[0] == '{': - code = code[1::] - if code[-1] == '}': - code = code[:-1] - code = code.strip() # yep, another strip to remove spaces after removing braces - return code - -def initializeBehaviours(code: str) -> str: - code_lines = [] - - depth = 0 - tmp_buffer = "" - for i, c in enumerate(code): - if c == '(': - depth += 1 - tmp_buffer += c - continue - elif c == ')': - depth -= 1 - tmp_buffer += c - continue - elif c == ';': - if depth == 0: - code_lines.append(tmp_buffer) - tmp_buffer = "" - continue - else: - tmp_buffer += c - continue - elif c == ' ': - continue - else: - tmp_buffer += c - - return code_lines - -def parse_line(line: str): - if line.endswith(';'): - line = line[:-1] # make sure to remove the trailing semicolon if it still exists - - current_char = line[0] - - left_paren_index = line.find('(') - - if current_char == '@': # special instruction - # get the instruction name - if left_paren_index == -1: - left_paren_index = 1 - - instruction_name = line[1:left_paren_index] - - # assign runner (not implemented here, just a placeholder) - # goto LABEL_40 - - elif current_char == "*": - close_bracket_index = line.find(']') + 1 - caret_index = line.find('^', close_bracket_index) - - if close_bracket_index >= left_paren_index: - caret_index = line.find('^') - else: - left_paren_index = line.find('(', caret_index) - - target_name = line[1:caret_index] - - if target_name[0] == "[": - target_name = target_name[1:-1] # remove the brackets - - # object = new CMC_Expression - else: - # object = CMC_ObjectsContainer::getObject - pass - - method_name = line[caret_index + 1:left_paren_index] - # goto LABEL_40 - - elif line.startswith("THIS"): - method_name = line[5:left_paren_index] - # goto LABEL_40 - - pass - -if __name__ == '__main__': - prepared_code = prepare_input(code) - - print("Prepared code:") - print(prepared_code) - - print("\nCode lines:") - code_lines = initializeBehaviours(prepared_code) - for line in code_lines: - print(line) - - for line in code_lines: - parse_line(line) - - \ No newline at end of file +from __future__ import annotations + +import argparse +import json +from dataclasses import asdict, dataclass, field +from itertools import count +from typing import Optional + + +_TEMP_COUNTER = count() +_MAX_DEPTH = 10 + + +def ghidra_slice(text: str, start: int, end: int) -> str: +    start = max(start, 0) +    end = max(end, 0) +    if end < start: +        return "" +    return text[start:end] + + +def split_args_like_ctor(args_text: str) -> list[str]: +    parts: list[str] = [] +    start = 0 +    depth = 0 +    for index, char in enumerate(args_text): +        if char == "(": +            depth += 1 +        elif char == ")": +            depth -= 1 +        elif char == "," and depth == 0: +            parts.append(args_text[start:index]) +            start = index + 1 +        if index == len(args_text) - 1: +            parts.append(args_text[start:index + 1]) +    return [part for part in parts if part] + + +@dataclass +class ExpressionAst: +    node_type: str +    raw: str +    inner: str +    source_branch: str + + +@dataclass +class BehaviourBlockAst: +    node_type: str +    raw: str +    body: str +    temp_name: str + + +@dataclass +class TargetAst: +    kind: str +    raw: str +    expression: Optional[ExpressionAst] = None +    note: Optional[str] = None + + +@dataclass +class ValueAst: +    kind: str +    raw: str +    text: Optional[str] = None +    expression: Optional[ExpressionAst] = None +    note: Optional[str] = None + + +@dataclass +class ArgumentAst: +    raw: str +    branch: str +    storage: str +    simple_argument: bool +    expression: Optional[ExpressionAst] = None +    nested_entry: Optional["EntryAst"] = None +    variable_name: Optional[str] = None +    behaviour_block: Optional[BehaviourBlockAst] = None +    trace: list[str] = field(default_factory=list) + + +@dataclass +class EntryAst: +    node_type: str +    raw: str +    normalized: str +    first_char: str +    runner_name: Optional[str] = None +    target: Optional[TargetAst] = None +    value: Optional[ValueAst] = None +    args_text: str = "" +    direct_args: list[ArgumentAst] = field(default_factory=list) +    entry_args: list[ArgumentAst] = field(default_factory=list) +    flags: dict[str, bool] = field(default_factory=dict) +    trace: list[str] = field(default_factory=list) + + +def make_expression(raw: str, source_branch: str) -> ExpressionAst: +    inner = raw[1:-1] if raw.startswith("[") and raw.endswith("]") else raw +    return ExpressionAst( +        node_type="expression", +        raw=raw, +        inner=inner, +        source_branch=source_branch, +    ) + + +def extract_call_parts(text: str) -> tuple[int, str]: +    open_paren = text.find("(") +    if open_paren < 0: +        return -1, "" +    close_paren = text.rfind(")") +    if close_paren < 0 or close_paren < open_paren: +        return open_paren, "" +    return open_paren, text[open_paren + 1:close_paren] + + +def parse_argument(token: str, depth: int) -> ArgumentAst: +    trace: list[str] = [] +    has_dollar = "$" in token +    first_char = token[0] + +    if first_char == "[" and not has_dollar: +        trace.append("Argument starts with '[' and has no '$': construct CMC_Expression and store it in +0x98.") +        return ArgumentAst( +            raw=token, +            branch="bracket_expression", +            storage="direct_args(+0x98)", +            simple_argument=True, +            expression=make_expression(token, "arg[current_char == '[']"), +            trace=trace, +        ) + +    if first_char != "@" and not has_dollar and "^" not in token: +        if first_char == "&": +            trace.append("Argument starts with '&' and has no '$'/'^': resolve variable and store it in +0x98.") +            return ArgumentAst( +                raw=token, +                branch="variable_reference", +                storage="direct_args(+0x98)", +                simple_argument=True, +                variable_name=token[1:], +                trace=trace, +            ) + +        trace.append("Plain argument token: recurse into nested CMC_Behaviour_Entry and store it in +0x9c.") +        return ArgumentAst( +            raw=token, +            branch="nested_entry", +            storage="entry_args(+0x9c)", +            simple_argument=False, +            nested_entry=parse_line(token, depth + 1), +            trace=trace, +        ) + +    quote_offset = 1 if first_char == '"' else 0 +    if len(token) > quote_offset and token[quote_offset] == "{": +        body_start = quote_offset + 1 +        body_end = len(token) - 1 - quote_offset +        body = ghidra_slice(token, body_start, body_end) +        temp_name = f"temp_{next(_TEMP_COUNTER)}" +        trace.append( +            "Dynamic or special argument opens an inline behaviour block: create temp behaviour and put temp string in +0x98." +        ) +        if has_dollar: +            trace.append("This branch clears the simple-arguments flag because '$' appears inside the inline behaviour token.") +        return ArgumentAst( +            raw=token, +            branch="inline_behaviour_block", +            storage="direct_args(+0x98)", +            simple_argument=not has_dollar, +            behaviour_block=BehaviourBlockAst( +                node_type="behaviour_block", +                raw=token, +                body=body, +                temp_name=temp_name, +            ), +            trace=trace, +        ) + +    trace.append( +        "Argument is '@'-prefixed or contains '$'/'^': build nested CMC_Behaviour_Entry and store it in +0x9c." +    ) +    return ArgumentAst( +        raw=token, +        branch="special_nested_entry", +        storage="entry_args(+0x9c)", +        simple_argument=False, +        nested_entry=parse_line(token, depth + 1), +        trace=trace, +    ) + + +def parse_line(raw_line: str, depth: int = 0) -> EntryAst: +    raw_line = raw_line.rstrip("\r\n") +    normalized = raw_line[:-1] if raw_line.endswith(";") else raw_line + +    if depth > _MAX_DEPTH: +        return EntryAst( +            node_type="behaviour_entry", +            raw=raw_line, +            normalized=normalized, +            first_char=normalized[:1], +            flags={"max_depth_reached": True}, +            trace=["Recursion guard fired. Remaining subtree was not expanded."], +        ) + +    first_char = normalized[:1] +    entry = EntryAst( +        node_type="behaviour_entry", +        raw=raw_line, +        normalized=normalized, +        first_char=first_char, +        flags={ +            "explicit_target": False, +            "this_prefix": False, +            "dynamic_target_before_caret": False, +            "top_level_bracket_runtime_text": False, +            "simple_arguments": True, +        }, +    ) + +    if raw_line.endswith(";"): +        entry.trace.append("Trailing ';' removed before further parsing.") + +    open_paren, args_text = extract_call_parts(normalized) +    entry.args_text = args_text + +    if not normalized: +        entry.trace.append("Empty input line.") +        return entry + +    if first_char == "@": +        runner_end = open_paren if open_paren >= 0 else len(normalized) +        entry.runner_name = ghidra_slice(normalized, 1, runner_end) +        entry.target = TargetAst( +            kind="behaviour_owner", +            raw="@", +            note="Uses owner behaviour as subject (+0x6c) and runner 0 as fallback.", +        ) +        entry.trace.append("current_char == '@': subject becomes owner behaviour, runner name is taken from text after '@'.") + +    elif first_char == "*": +        close_bracket = normalized.find("]") +        caret_index = normalized.find("^", close_bracket + 1 if close_bracket >= 0 else 0) +        runner_open = open_paren +        if close_bracket >= 0 and open_paren >= 0 and close_bracket + 1 < open_paren: +            runner_open = normalized.find("(", caret_index if caret_index >= 0 else 0) +        else: +            caret_index = normalized.find("^") + +        runner_end = runner_open if runner_open >= 0 else len(normalized) +        target_end = caret_index if caret_index >= 0 else runner_end +        target_text = ghidra_slice(normalized, 1, target_end) +        entry.flags["explicit_target"] = True + +        if target_text.startswith("["): +            entry.target = TargetAst( +                kind="expression_target", +                raw=target_text, +                expression=make_expression(target_text, "entry['*' explicit target]"), +                note="Sets +0xbe and stores a compiled CMC_Expression in +0xb8.", +            ) +            entry.trace.append("'*' branch with '[' target: compile explicit target expression and keep it in +0xb8.") +        else: +            entry.target = TargetAst( +                kind="object_target", +                raw=target_text, +                note="Lookup object by name and store it in +0xb8.", +            ) +            entry.trace.append("'*' branch with plain target: resolve explicit object name into +0xb8.") + +        if caret_index >= 0: +            entry.runner_name = ghidra_slice(normalized, caret_index + 1, runner_end) +        entry.trace.append("Runner name in '*' branch comes from the slice after '^'.") + +    elif normalized.startswith("THIS"): +        runner_end = open_paren if open_paren >= 0 else len(normalized) +        entry.runner_name = ghidra_slice(normalized, 5, runner_end) +        entry.target = TargetAst( +            kind="this_object", +            raw="THIS", +            note="Sets +0xa1 and treats THIS as the callee object.", +        ) +        entry.flags["this_prefix"] = True +        entry.trace.append("Line starts with 'THIS': set +0xa1 and take runner text after 'THIS'.") + +    elif first_char == "[": +        bracket_text = normalized +        if "$" in bracket_text: +            entry.value = ValueAst( +                kind="runtime_bracket_text", +                raw=bracket_text, +                text=bracket_text, +                note="This branch does not build CMC_Expression. It copies bracketed text into +0xa8 and returns early.", +            ) +            entry.flags["top_level_bracket_runtime_text"] = True +            entry.trace.append( +                "Top-level current_char == '[' with '$': keep bracketed text in +0xa8, no CMC_Expression is allocated." +            ) +        else: +            entry.value = ValueAst( +                kind="expression_value", +                raw=bracket_text, +                expression=make_expression(bracket_text, "entry[current_char == '[']"), +                note="This branch builds CMC_Expression and stores it in +0xa4.", +            ) +            entry.trace.append( +                "Top-level current_char == '[' without '$': construct CMC_Expression and store it in +0xa4." +            ) + +    else: +        caret_index = normalized.find("^") +        if first_char == '"' or caret_index < 0: +            entry.value = ValueAst( +                kind="atom_or_object_lookup", +                raw=normalized, +                text=normalized, +                note="Runtime decides whether this resolves to an existing object (+0xa4) or remains plain text in +0xa8.", +            ) +            entry.trace.append("No '^' (or quoted atom): this stays a leaf value/object lookup.") +        else: +            lhs = normalized[:caret_index] +            runner_end = open_paren if open_paren >= 0 else len(normalized) +            entry.runner_name = ghidra_slice(normalized, caret_index + 1, runner_end) +            if "$" in lhs: +                entry.flags["dynamic_target_before_caret"] = True +                entry.target = TargetAst( +                    kind="dynamic_target", +                    raw=lhs, +                    note="Sets +0xbc; target must be resolved later because '$' appears before '^'.", +                ) +                entry.trace.append("Target text before '^' contains '$': set +0xbc and defer target resolution.") +            elif "|" in lhs: +                object_name, field_name = lhs.split("|", 1) +                entry.target = TargetAst( +                    kind="structure_field", +                    raw=lhs, +                    note=f"Resolve object '{object_name}', then field '{field_name}' via CMC_Structure::getField.", +                ) +                entry.trace.append("Target before '^' contains '|': treat it as object|field.") +            else: +                entry.target = TargetAst( +                    kind="object_target", +                    raw=lhs, +                    note="Resolve callee object name before selecting the runner.", +                ) +                entry.trace.append("Target before '^' is a plain object name.") + +    if not entry.args_text: +        return entry + +    entry.trace.append("Split argument string exactly like the ctor: commas only count at parenthesis depth 0.") +    for token in split_args_like_ctor(entry.args_text): +        argument = parse_argument(token, depth) +        if argument.storage.startswith("direct_args"): +            entry.direct_args.append(argument) +        else: +            entry.entry_args.append(argument) +        if not argument.simple_argument: +            entry.flags["simple_arguments"] = False + +    return entry + +def prepare_input(code: str) -> str: +    # original used right and left instructions for striping code from braces (yeah, they are unnecessary) +    code = code.strip() +    if code[0] == '{': +        code = code[1::] +    if code[-1] == '}': +        code = code[:-1] +    code = code.strip() # yep, another strip to remove spaces after removing braces +    return code + +def initializeBehaviours(code: str) -> str: +    code_lines = [] + +    depth = 0 +    tmp_buffer = "" +    for i, c in enumerate(code): +        if c == '(': +            depth += 1 +            tmp_buffer += c +            continue +        elif c == ')': +            depth -= 1 +            tmp_buffer += c +            continue +        elif c == ';': +            if depth == 0: +                code_lines.append(tmp_buffer) +                tmp_buffer = "" +                continue +            else: +                tmp_buffer += c +                continue +        elif c == ' ': +            continue +        else: +            tmp_buffer += c + +    return code_lines + +def main() -> int: +    parser = argparse.ArgumentParser( +        description=( +            "Approximate AST reconstruction for BlooMooDLL::CMC_Behaviour_Entry::CMC_Behaviour_Entry " +            "(thunk 0x1000331e -> body 0x10042670)." +        ) +    ) +    parser.add_argument( +        "--compact", +        action="store_true", +        help="Emit compact JSON instead of pretty-printed JSON.", +    ) +    args = parser.parse_args() + +    import sys + +    lines = "{@STRING(\"TEST\", \"\");@IF(\"2+3*4'\"2+3*4\"&&1'1\",\"{@RETURN(\"TRUE\");}\",\"{@RETURN(\"FALSE\");}\");@RETURN(TEST);}" + +    prepared_code = prepare_input(lines) +    +    print("Prepared code:") +    print(prepared_code) + +    print("\nCode lines:") +    lines = initializeBehaviours(prepared_code) + +    json_data = [] + +    for index, line in enumerate(lines): +        if index and not args.compact: +            print() +        tree = parse_line(line) +        +        json_data.append(asdict(tree)) + +    if args.compact: +        print(json.dumps(json_data, ensure_ascii=True, separators=(",", ":"))) +    else: +        print(json.dumps(json_data, ensure_ascii=True, indent=2)) +    return 0 + + +if __name__ == "__main__": +    raise SystemExit(main()) \ No newline at end of file