Support Piklib 6.1/7.1: CMC_Scene::resolve factory + tag-based types
Earlier text-script engines (Piklib 6.1/7.1, added text scripts in 6.1) keep the type factory on CMC_Scene::resolve, not CMC_ObjectsContainer::resolve — so the extractor bailed with "resolve not found". find_factory() now tries both anchors. 6.1's factory is also tag-based: each branch is operator==(NAME) -> new(0x74) -> store tag -> jmp, with the ctor in a separate tag switch (no inline ctor). extract_types gains a pre-emit: when the next operator== arrives still armed, it records the pending type by name (size known, ctor/cpp_class not). The 8.x inline-ctor factory clears `armed` first, so it's untouched (golden pair unchanged). Per-version reality: 6.1 = 23 types / 0 methods (no prepareMthHashSet yet) / 103 events / 80 fields; 7.1 = 26 / 322 / 102 / 86 / 288 dispatch (full); type names line up across 6.1->7.1->8.x so version diffs work. - snapshots/PIKLib61 + PIKLIB71 added as golden fixtures (evolution chain) - tests/test_versions.py: 6.1 partial surface, 7.1 full, 61->71 diff -> 38/38 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -101,7 +101,7 @@ działa na MSVC6 (Piklib) i MSVC8 (BlooMoo) mimo różnego kodu wynikowego.
|
||||
|
||||
| Co | Kotwica w Ghidrze | Status |
|
||||
|----|-------------------|--------|
|
||||
| Typy | `CMC_ObjectsContainer::resolve`: `operator==("NAME")` → `operator_new(SIZE)` → `CMC_X::CMC_X` | ✅ (+ `dispatch_addr`, `via_module_iface`) |
|
||||
| Typy | `CMC_ObjectsContainer::resolve` (8.x/BlooMoo) lub `CMC_Scene::resolve` (Piklib 6.1/7.1): `operator==("NAME")` → `operator_new(SIZE)` → ctor | ✅ (8.x inline; 6.1 tag-based = nazwy bez ctora) |
|
||||
| Metody | `CMC_*_Runner::prepareMthHashSet`: `CInteger(id)` + `CStringHashCode("NAME")` + `CHashtable::put` | ✅ (+ `method_inheritance`) |
|
||||
| Eventy | `CMC_*::getBehavioursList`: lista literałów `CXString` | ✅ (lista per klasa, bez dziedziczenia) |
|
||||
| Pola (skryptowe) | ctory `CMC_*`: literały czytane przez `CMElement::getProperty<T>Value` → nazwa + typ pola (FPS, PRELOAD, VISIBLE…) | ✅ (+ `field_inheritance`) |
|
||||
|
||||
@@ -206,6 +206,23 @@ def extract_types(program, factory):
|
||||
f = call_target(program, instr)
|
||||
tname = f.getName() if f is not None else None
|
||||
if tname == OP_EQ:
|
||||
# Tag-based factory (Piklib 6.1/7.1's CMC_Scene::resolve): a branch is
|
||||
# `operator==(NAME) -> new(SIZE) -> store tag -> jmp`, with the ctor in a
|
||||
# separate tag switch, so no inline ctor ever fires `elif armed`. If we reach
|
||||
# the *next* operator== still armed, record the pending type by name (size known,
|
||||
# ctor not). The inline-ctor factory (8.x) clears `armed` first, so it's untouched.
|
||||
if armed and pending_name is not None:
|
||||
types.append({
|
||||
"script_name": pending_name,
|
||||
"cpp_class": None,
|
||||
"ctor_addr": None,
|
||||
"object_size": pending_size,
|
||||
"dispatch_addr": None,
|
||||
"via_module_iface": _branch_uses_field_load(branch),
|
||||
})
|
||||
pending_name = None
|
||||
pending_size = None
|
||||
armed = False
|
||||
s = lookback_string(program, recent)
|
||||
if s is not None:
|
||||
pending_name = s
|
||||
@@ -839,11 +856,26 @@ def sha256_of(program):
|
||||
|
||||
# --------------------------------------------------------------------------- main
|
||||
|
||||
def find_factory(program):
|
||||
"""The type-dispatch factory (the operator==("NAME") -> new -> ctor ladder). Its home class
|
||||
moved across versions: the script factory lived on CMC_Scene in early text-script Piklib
|
||||
(6.1 / 7.1), then was hoisted into CMC_ObjectsContainer from Piklib 8.x onward (and BlooMoo)."""
|
||||
for class_name, method_name in (
|
||||
("CMC_ObjectsContainer", "resolve"), # Piklib 8.x / BlooMoo
|
||||
("CMC_Scene", "resolve"), # Piklib 6.1 / 7.1
|
||||
):
|
||||
f = find_function_by_qualified(program, class_name, method_name)
|
||||
if f is not None:
|
||||
return f
|
||||
return None
|
||||
|
||||
|
||||
def run():
|
||||
program = currentProgram # GhidraScript/pyghidra inject this global, not `program`
|
||||
factory = find_function_by_qualified(program, "CMC_ObjectsContainer", "resolve")
|
||||
factory = find_factory(program)
|
||||
if factory is None:
|
||||
print("[!] CMC_ObjectsContainer::resolve not found - is this a Piklib/BlooMoo DLL?")
|
||||
print("[!] factory not found (CMC_ObjectsContainer::resolve / CMC_Scene::resolve)"
|
||||
" - is this a Piklib/BlooMoo DLL with text-script support? (added in Piklib 6.1)")
|
||||
return
|
||||
|
||||
engine, compiler = detect_engine(factory) # namespace lives on the symbol/stub
|
||||
|
||||
13254
snapshots/PIKLIB71.dll.snapshot.json
Normal file
13254
snapshots/PIKLIB71.dll.snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5906
snapshots/PIKLib61.dll.snapshot.json
Normal file
5906
snapshots/PIKLib61.dll.snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
52
tests/test_versions.py
Normal file
52
tests/test_versions.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Per-version coverage on the Piklib evolution 6.1 -> 7.1 (-> 8.x). Documents what each
|
||||
early engine actually exposes, and guards the tagged-factory type extraction (6.1)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from ams.diff import compute_diff
|
||||
from ams.snapshot import Snapshot
|
||||
|
||||
SNAP_DIR = Path(__file__).resolve().parents[1] / "snapshots"
|
||||
P61 = SNAP_DIR / "PIKLib61.dll.snapshot.json"
|
||||
P71 = SNAP_DIR / "PIKLIB71.dll.snapshot.json"
|
||||
|
||||
pytestmark = pytest.mark.skipif(not P61.exists(), reason="6.1/7.1 snapshots not present")
|
||||
|
||||
|
||||
def _load(p: Path) -> Snapshot:
|
||||
with open(p, encoding="utf-8") as fh:
|
||||
return Snapshot(json.load(fh))
|
||||
|
||||
|
||||
def test_piklib61_early_engine_partial_surface():
|
||||
s = _load(P61)
|
||||
assert s.binary["engine"] == "Piklib"
|
||||
# types come from the tag-based CMC_Scene::resolve ladder (names recovered, ctor/size not)
|
||||
names = {t["script_name"] for t in s.types}
|
||||
assert len(names) >= 20 and {"ANIMO", "ARRAY", "BUTTON"} <= names
|
||||
assert all(t.get("cpp_class") is None or t["cpp_class"].startswith("CMC_") for t in s.types)
|
||||
# events + script fields work; method registration (prepareMthHashSet) doesn't exist yet
|
||||
assert s.events and s.fields
|
||||
assert s.methods == [] and s.method_dispatch == []
|
||||
|
||||
|
||||
def test_piklib71_full_surface():
|
||||
s = _load(P71)
|
||||
assert s.binary["engine"] == "Piklib"
|
||||
assert s.types and s.methods and s.events and s.fields and s.method_dispatch
|
||||
# 7.1 uses the inline-ctor factory, so most types resolve their C++ class
|
||||
assert sum(1 for t in s.types if t.get("cpp_class")) > len(s.types) // 2
|
||||
|
||||
|
||||
def test_61_to_71_diff_adds_methods():
|
||||
s61, s71 = _load(P61), _load(P71)
|
||||
shared = {t["script_name"] for t in s61.types} & {t["script_name"] for t in s71.types}
|
||||
assert {"ANIMO", "ARRAY", "BUTTON"} <= shared # stable core across the two early versions
|
||||
# 7.1 introduces the registered-method machinery 6.1 lacked entirely
|
||||
d = compute_diff(s61, s71)
|
||||
assert len(d["methods"]["added"]) > 100 and d["methods"]["removed"] == []
|
||||
Reference in New Issue
Block a user