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:
Patryk Gensch
2026-05-31 19:53:47 +02:00
parent ba9db82a4c
commit 67cbc32a2c
5 changed files with 19247 additions and 3 deletions

View File

@@ -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