Toolchain

  • Ghidra 12.x in blacktop/ghidra:latest. Bundles OpenJDK 21 and stock Ghidra at /ghidra.
  • Jython 2.7 (bundled with Ghidra) for analysis scripts. Scripts must be ASCII-only - Jython 2 chokes on Unicode in source unless an encoding declaration is added.
  • PCSX-Redux for runtime tracing. See overlay capture.

Bringing the service up

Ghidra runs headlessly inside the blacktop/ghidra:latest Docker image, wrapped by docker/ghidra.Dockerfile to map the container user to the host's UID/GID - so files written into the bind-mounted /projects and /scripts directories come back owned by the host user.

# Build (auto-uses USER_ID / GROUP_ID from .env or defaults to 1000:1000)
docker compose build ghidra

# Start the long-running container
docker compose up -d ghidra

The service uses these mounts (from docker-compose.yml):

Mount Mode Purpose
./extracted/data read-only Disc-extracted files (BIN, TIM, TMD, etc.)
./ghidra/projects/projects read-write Ghidra project DB (gitignored)
./ghidra/scripts/scripts read-write Analysis scripts + per-function dumps

If you've never built the wrapper before, first run also handles UID/GID matching - see the comment at the top of docker-compose.yml for .env overrides.

Importing SCUS_942.54

PSX executables are PSX-EXE format: skip the 0x800-byte header, base address 0x80010000.

docker compose exec ghidra /ghidra/support/analyzeHeadless \
    /projects legaia \
    -import /data/SCUS_942.54 \
    -loader BinaryLoader \
    -loader-baseAddr 0x80010000 \
    -processor MIPS:LE:32:default

After import, run analysis:

docker compose exec ghidra /ghidra/support/analyzeHeadless \
    /projects legaia -process SCUS_942.54

This takes a few minutes and populates the database with functions, references, and decompilation results.

The LUI+ADDIU gotcha

MIPS has no instruction that loads a full 32-bit constant, so compilers build every address from two 16-bit immediates - and Ghidra's cross-reference database does not stitch the pair back together.

The idiom in question:

lui   r1, 0x801C       ; r1 = 0x801C0000
addiu r1, r1, 0x70F0   ; r1 = 0x801C70F0

Workaround: ghidra/scripts/find_lui_writers.py walks instructions, tracks per-register LUI immediates, and flags addiu / load / store offsets that combine with a tracked LUI to land in a target range.

docker compose exec ghidra /ghidra/support/analyzeHeadless \
    /projects legaia -process SCUS_942.54 -noanalysis \
    -postScript /scripts/find_lui_writers.py

Modify LO / HI constants in the script to scan a different range.

Computed addresses are still missed - lw r4, 0x18(r3) where r3 = 0x80080000 + index*4 can't be statically resolved when index is only known at runtime. Functions reading from arrays via runtime-computed indexing won't appear in xref lists; for these, dynamic analysis with watchpoints is the only static-tool-free path.

Investigation patterns

"Find what writes / reads a global"

Use find_lui_writers.py with LO / HI narrowed to the target address - it catches the LUI+ADDIU/load/store combos that Ghidra's reference manager misses.

"Find callers of a function"

Use find_callers_of.py (edit TARGETS_HEX to the entry point) or dispatcher_callers.py for the asset-dispatcher / LZS chain specifically.

"Is this function actually called?"

The reference manager is unreliable for indirect calls. Use:

  • find_callers_of.py for direct jal references.
  • find_addr_data.py to find the address as data (function-pointer tables, callbacks).

If both return zero hits, the function has no static caller in the program currently loaded into Ghidra - that's NOT the same as "dead code in retail". Most game logic lives in RAM-loaded overlays at 0x801C0000+ that aren't part of SCUS_942.54. The negative result bounds where the caller can possibly live, but doesn't prove the function unreachable.

"Where does this constant address get used?"

If the address is referenced via lui+addiu, the reference manager will miss it. Use find_lui_writers.py with LO/HI narrowed to your target range.

"Ghidra says nothing writes / reads this global, but I know something does"

Common when the address is materialized by lui+addiu and then passed to a helper (so the actual sw/lw is in the helper, against $a0/$a1), OR when it's stored as a function-argument base that an addu reroutes (so the constant tracker bails and the final sw doesn't appear in the xref database).

Use find_addr_materializers.py to walk every instruction in a program, track per-register lui + addiu pairs, and report every site where the combined value lands on one of your target addresses - plus the next 6 instructions for use-classification (store base = writer, load base = reader, jal/jalr follows = address passed as argument).

docker compose exec ghidra /ghidra/support/analyzeHeadless \
    /projects legaia -process SCUS_942.54 -noanalysis \
    -postScript /scripts/find_addr_materializers.py \
    0x8007C018 0x8007BB38 0x8007B7DC

Arguments may be decimal or hex (0x... prefix). Multiple addresses are scanned in a single pass. Alternative: set GHIDRA_FIND_ADDRS='0x8007c018,0x8007bb38' and run without args.

The pattern this catches (the actual installer for DAT_8007C018 at FUN_80026B4C - missed by the reference manager):

lui   v0, 0x8008
lui   v1, 0x8008
lw    v1, -0x488c(v1)     ; v1 = *DAT_8007B774 (index counter)
addiu v0, v0, -0x3fe8     ; v0 = 0x8007C018   <-- the materializer site
sll   v1, v1, 0x2         ; v1 = idx * 4
addu  v1, v1, v0          ; v1 = idx*4 + 0x8007C018
sw    a0, 0(v1)           ; store to table    <-- the missed writer

The reference manager tracks lui+addiu pairs but bails when addu mixes the propagated constant with a value loaded from memory. So sw a0, 0(v1) is invisible to it - but the addiu v0, v0, -0x3fe8 site IS visible to a manual scanner that knows the combination forms the target address. Once you see the materializer, the surrounding 6 instructions usually make the role obvious.

"What format does this PROT entry use?"

Empirical workflow:

  1. xxd extracted/PROT/<entry>.BIN | head -5 - eyeball the header.
  2. Try each known parser:
    • asset stream <file> - DATA_FIELD streaming.
    • asset describe <file> - descriptor format (when applicable).
    • lzs-decode raw --size N <file> - top-level LZS.
    • asset categorize <DIR> - runs every detector and emits a per-class breakdown.
  3. If nothing matches, dig into the function that loads it (find by reversing the call site).

Adding a new function dump

  1. Edit ghidra/scripts/dump_funcs.py's TARGETS list to add the entry-point address.
  2. Run the dump:
    docker compose exec ghidra /ghidra/support/analyzeHeadless \
        /projects legaia -process SCUS_942.54 -noanalysis \
        -postScript /scripts/dump_funcs.py
  3. Open ghidra/scripts/funcs/<addr>.txt and analyze.
  4. Update reference/functions.md if it's a notable entry point.

Script catalogue

The Ghidra-side scripts (Jython, run inside the container) live in ghidra/scripts/. Edit the TARGETS / LO / HI constants at the top of any script to point at the addresses you want to trace.

Symbol re-application (apply_known_symbols.py)

Re-applies this project's pinned function names to a fresh import of SCUS_942.54. A raw-blob import names every function FUN_<addr>; the script reads the curated (address, name, role-comment) table in known_symbols.py and names each function + sets a one-line PLATE comment, so the asset/loader/CD/dispatch cluster is readable immediately. The clean-room counterpart to a PsyQ FidDB pass (replays our own RE labels, no external SDK). SCUS-resident (0x80010000..0x8007C000) only - RAM overlays alias by address, so naming them blind would mislabel.

docker compose exec ghidra /ghidra/support/analyzeHeadless /projects legaia -process SCUS_942.54 -noanalysis -postScript /scripts/apply_known_symbols.py
Per-function dumps - the core dumpers that write disassembly + decompiled C per entry point.
Script Purpose
dump_funcs.py Dump disassembly + decompiled C for a list of function entry points. Output goes to ghidra/scripts/funcs/<addr>.txt.
force_disasm_dump.py Force-disassemble + create-function at addresses Ghidra didn't auto-detect (JALR-only entry points), then dump. Validates the result has >=8 instructions ending in jr $ra before committing the function.
dump_pending_helpers.py Catch-all dumper for helpers / dispatcher leaves under active investigation; the TARGETS list rotates as RE threads come and go. The template every per-overlay dump script follows (in_program() guard + out_path_for() prefixing).
dump_globals.py Dump the program's defined data-symbol table to funcs/globals_<program>.txt, in the header format scripts/pcsx-redux/build-symbols.py parses.
resolve_render_tail.py Companion to the trace-driven coverage program: for a list of overlay trace-hit addresses, reports getFunctionContaining + memory.contains per hit in the currently-open overlay program - separating "in-program but un-analyzed" (a create+dump target) from "out-of-program" (a different co-resident overlay). This containment check is how VA-aliased overlay hits get attributed to the right overlay.
dump_battle_rendertail.py Disassemble + create-function + dump the in-0898 battle render-tail functions the trace found un-analyzed (e.g. FUN_801E0080). Output naming matches the overlay dumps; run against overlay_battle_action.bin.
dump_battle_rendertail_0x801f.py Dump the 0x801F render tail the windowed overlay_battle_action.bin import stops short of. Run against a full-length re-import of the 0898 blob at base 0x801CE818 (span 0x801CE818..0x801F8018); resolves the 0x801F0xxx hits cleanly. The 0x801F6xxx/0x801F7xxx sub-cluster is not 0898 - it is the co-resident sparring-tutorial overlay PROT 0967 (next row).
dump_effect_overlay_0967.py Dump the battle sparring-tutorial overlay PROT 0967, co-resident at base 0x801F69D8 during the Tetsu tutorial fight (overlapping 0898's rodata tail). Run against a fresh import of the 0967 entry at -loader-baseAddr 0x801F69D8; create+dumps the 0x801F6xxx/0x801F7xxx hit functions (message-pacing driver FUN_801F71E0 + the step text emitters) as overlay_effect_0967_<addr>.txt.
dump_menu_inventory_refs.py Content-grep dumper: decompiles every function in the current program and dumps the C for any whose body mentions a configurable needle list (default: the inventory array 0x80085958 + the SCUS accessor family + the gp+0x2D2/0x2D4/0x2D6 window registers). Robust against the LUI+ADDIU xref gap (matches decompiled text, not the reference manager). Audits overlay_menu.bin for raw-index inventory writes - every mutation goes through the bounds-checked helpers.
dump_terrain_trigger.py Per-overlay-aware dumper for the world-map render-pipeline chain (FUN_801D7EA0 / FUN_801D8258 / FUN_801D1344 / FUN_80016444 + SCUS callers and the 0897 relocation copy). Uses prog.getMemory().contains(addr) to skip any TARGET that isn't mapped in the current program, so the same script can be run against SCUS plus each overlay and only emits files for the addresses that exist there. Output naming: <program_label>_<addr>.txt.
trace_field_loader.py Targeted trace of the per-scene field-file loader FUN_8001f7c0; pins the loader's dual-mode dispatch - retail resolves the .MAP by PROT index (FUN_8003e8a8), while the break 0x103 path is the dev-host fopen of DATA\FIELD\<scene>.MAP (never taken on retail).
dump_port_catalog_worklist.py Dumps the "cited but not dumped" worklist surfaced by scripts/ci/port-catalog.py - each address is referenced from at least one existing dump but has no dump of its own; filling them in closes the BFS frontier on the citation graph.
Per-overlay capture dumps - one script per captured overlay image, following the dump_pending_helpers.py pattern.
Script Purpose
dump_shop_overlay.py Menu-overlay capture: shop, save-screen, status, inn and related UI subsystems (all functions).
dump_levelup_overlay.py Battle-overlay capture hosting the level-up sequencer + XP/stat-gain calculations (all functions).
dump_levelup_data_section.py Data section of the level-up capture - the key data addresses above the code (gauge tables read by FUN_801d388c and siblings).
dump_magic_capture_overlay.py Ra-Seru capture-mechanic overlay (grabbing Gimard and other Seru).
dump_cutscene_overlay.py Actor-scripted cutscene captures of the field overlay (which also hosts the field VM FUN_801DE840, world-map controller FUN_801E76D4, dev-menu renderer FUN_801EAD98).
dump_str_fmv_overlay.py STR/MDEC FMV overlay (game modes 26/27) - distinct from the actor-scripted cutscene captures.
dump_dialog_overlay.py Top dialog-overlay candidates for the MES bytecode dispatcher search.
dump_dialog_mc4_overlay.py Top functions of the dialog_mc4 capture (taken during a world-map walk; same function layout as the walk capture).
dump_dialog_typing_overlay.py Top functions of the capture taken while a dialog box was actively open and advancing text.
dump_field_battle_intro_overlay.py Field→battle transition capture - the 3D camera spin between the field and the battle load.
dump_fishing_overlay.py Fishing-minigame capture of the minigame-hub overlay family (the debug-menu capture is its superset).
dump_slot_machine_overlay.py Slot-machine minigame capture: coin input, reel-stop, bonus-game logic.
dump_baka_fighter_overlay.py Baka Fighter duel-minigame capture: round dispatcher, AI, player input.
dump_dance_overlay.py Dance-minigame capture: main step-input handler + siblings.
dump_muscle_dome_overlay.py Muscle Dome card-battle capture: round dispatcher, main game state machine, card resolution.
dump_debug_menu_overlay.py Debug/dev-menu capture - the fullest superset of the minigame-hub overlay family ("DEBUG MODE" dispatcher, minigame selector, FOG/MAP/TMD/POLY debug readouts).
dump_save_ui_select_overlay.py Save-slot-selection screen capture (before a slot is chosen).
dump_save_ui_saving_overlay.py Mid-save capture (after slot selection, while writing).
dump_summon_overlay.py Every function of an imported summon-stager overlay; flags functions referencing the record table or calling the part-stager FUN_80021B04, so the staging logic is easy to find.
dump_state_resume_overlay.py The 0897 STATE_RESUME effect-actor handler that field-VM op 0x49 spawns and that signals completion via _DAT_8007B450 = 1.
dump_title_overlay.py Title-overlay tick function + its caller (pinned by the countdown watchpoint capture).
dump_world_map_overlay.py Top functions of the world-map overlay capture.
dump_world_map_walk_overlay.py Top functions of the full walking world-map capture (includes the main dispatcher + dev menu).
dump_world_map_top_overlay.py Top functions of the top-view / aerial-camera world-map capture.
Targeted cluster dumpers - one-shot dumpers aimed at a specific function cluster or dispatch table.
Script Purpose
dump_arm_width.py The two functions that read the equipped weapon during arts INPUT (0x801D3AD0 width writer + 0x801ECC00 jump-table dispatch), pinned by a live read-watch.
dump_arts_input.py Battle-overlay (0898) arts-combo execution cluster: the Arms resolver FUN_801EC3E4 (with its caller list) plus every function referencing the move-power tables (0x801F4F5C / 0x801F64E4 / 0x801F4E63). Confirms the resolver is dispatched by a runtime function pointer and the move-power referrers are damage/action-step builders.
dump_battle_backdrop_draw.py The battle-overlay backdrop draw func_0x801d02c0 (called by FUN_80026f50 for game mode 0x15) plus one level of callees, for the dome-instancing / back-fill trace.
dump_dat_8007c018_helpers.py Overlay-side functions referencing the head of the DAT_8007C018 pool table (the dual-ref lw + 4-byte-stride pattern).
dump_gp148_candidates.py The three SCUS functions that write gp[0x148] - the drawable-list head consumed by FUN_80031D00's walker → the continent-terrain emitter FUN_8002C69C.
dump_house_door_cluster.py / dump_house_door2.py The intra-town (house / interior) door cluster in the 0897 field overlay: the locomotion + look-ahead callers of the reposition helper FUN_801d2404, plus follow-up functions.
dump_slot4_handler_table.py The slot-4 record-kind handler jump tables used by FUN_80043390 (SCUS table 0x8007657C + overlay table 0x801F8968; index = (record_word_0 >> 0x11) * 4).
dump_slot4_handlers.py Force-create + dump the per-kind slot-4 record handlers tail-called from FUN_80043390.
dump_save_screen_table.py The save-screen dispatch-table base PTR_FUN_801e4f40 used by the save-screen state machine FUN_801DC6B4 (indexed by DAT_801e46a4).
dump_save_ui_handlers.py Every unique sub-state handler body in the PTR_FUN_801e4f40 save-screen dispatch table.
dump_save_ui_handler_0x15.py The save-screen sub-state 0x15 handler at 0x801DA2A0 (the one table entry without a prior dump).
dump_world_map_emitter_callers.py The six world-map functions that jal the POLY_FT4/SPRT emitter FUN_8002C69C inline (bypassing the gp[0x148] drawable-list walker).
dump_world_map_top_ext_caller.py The overlay function that calls FUN_80043390 (slot-4 / cluster-A display-list dispatcher) during the warp-into-world-map transition.
dump_world_map_top_installers.py Every world-map-top function calling FUN_80034B78 (the top-view drawable installer), plus the installer itself from SCUS.
dump_world_map_top_prim_leaves.py The eight overlay-resident high-mode renderers the world-map top view swaps in via FUN_80043390's overlay path.
dump_world_map_vm_jt.py The FUN_801D362C world-map drawing-script VM jump table at 0x801D1E94 (0x3D cases), walking each case-start's prelude to identify opcode args + side effects.
dump_field_overlay_terrain_emitter.py Field-overlay (0897) functions caught writing the world-map prim pool by the LZS/bundle Lua write-probe.
dump_field_locomotion_cluster.py Re-decompile the 0897 field camera / region cluster (801db81c / 801dbec4 / 801f5748) + raw-disassemble the surrounding window. Read-only; surfaces the data holes that corrupt the decompiles.
fix_field_locomotion_flow.py DB-modifying repair for the same cluster: force-disassemble the jal 0x8003ce9c (non-returning operand reader) data holes, drop mid-block fake FUN_ entries, re-create functions at real addiu sp,sp,-N prologues, then re-decompile. General pattern for any overlay region split into bogus mid-block functions by a non-returning-call hole.
dump_player_locomotion_integrator.py Dumps the player free-movement controller FUN_801d01b0 + collision FUN_801cfe4c / FUN_801cf9f4 + pad-remap func_0x800467e8 / FUN_80046494, pinned by the autorun_player_pos_watch.lua write-watchpoint. in_program guards run it across SCUS + overlay_0897. See subsystems/field-locomotion.md.
dump_4c_jumptables.py Dumps the field-VM main dispatcher JT (0x801E00F4) + the 0x4C outer-nibble JT (0x801CEE60, 16 entries) with each target's containing function. Use to pin a 0x4C sub-opcode's exact nibble when the decompiler's reconstructed case numbering is ambiguous - e.g. confirmed the collision-grid paint is nibble-7 (0x801e1c64), not the decompile's misleading "case 5".
find_mesh_chain_writer.py Finds the writer of the field/world-map actor's mesh-chain pointer actor+0x44 (the chain FUN_8001ADA4 case 5 draws). Scans for non-stack sw/sh …,0x44(reg), scores each containing function by pool-table refs / TMD object-stride math / actor-field reads, dumps the top candidates. Pins the resolver chain: FUN_80024d78 builds actor+0x44 from DAT_8007C018[*(u16*)(actor+0x64)], and FUN_80020f88 sets actor+0x64 = .MAP_record[+0x10] + prefix.
analyze_addprim_candidates.py Companion to find_addprim_emitters.py: dumps each candidate emitter's disassembly + decompiled C and lists its direct callers (run against SCUS, where the high-hit POLY_FT4 emitter lives).
analyze_continent_emitter.py Walks the continent-terrain emitter chain rooted at FUN_8002C69C: the function + callers + direct callees, plus the static tile-atlas table at DAT_80073A00 and the byte-indexed (tile_id, flag) tables.
Coverage-batch dumpers - batches that close out the function-coverage tracker's missing-helper lists.
Script Purpose
dump_remaining_battle_action.py Batch-dump the battle-action-overlay functions not yet represented under funcs/overlay_battle_action_<addr>.txt.
dump_remaining_menu.py Batch-dump the menu-overlay functions not yet represented under funcs/overlay_menu_<addr>.txt.
dump_remaining_misses.py Dump the remaining "missing helper" entry points the function-coverage tracker reports.
dump_round8.py Citation-graph closer: 0897 tail-called helpers cited from the second-pass tracker, plus two BIOS B-vector thunks in SCUS.
dump_round_28_helpers.py Dialog-overlay missing helpers (one SCUS target + six in overlay_dialog_mc4; run twice with each -process).
dump_final_8_helpers.py The last SCUS-range functions in the coverage tracker's missing list (world-map sprite batcher and siblings).
LUI+ADDIU and address-resolution helpers - find the references Ghidra's reference manager misses.
Script Purpose
find_lui_writers.py Generic LUI+ADDIU resolver. Walks instructions, tracks per-register LUI immediates, reports any combined access landing in [LO, HI]. Critical for finding references the ref manager misses. Edit LO/HI per run.
find_addr_materializers.py Per-address LUI+ADDIU materializer finder. Reports every addiu whose combined value lands on one of the targets, plus the next 6 instructions for use-classification. Accepts addresses via getScriptArgs() or the GHIDRA_FIND_ADDRS env var - no source edit needed per invocation. See the LUI+ADDIU + ADDU+SW investigation pattern above.
find_addr_materializer_dat_8007c018.py Fixed-target shim from the original DAT_8007C018 / DAT_8007BB38 / DAT_8007B7DC materializer hunt; the generic tool is find_addr_materializers.py.
find_addr_data.py Search the program memory for any 4-byte LE word equal to a target address - catches function-pointer tables.
find_addr_data_xref.py One-program search for an address as a stored data word OR as the combined target of a lui+addiu/lui+ori pair.
find_addr_const_allprogs.py Sweep every program in the project for a target address as (a) a stored 32-bit LE word (pointer / jump-table entry) and (b) a LUI+ADDIU materialization.
find_jal_target.py Every jal instruction whose encoded 26-bit target equals TARGET in the currently-loaded program.
find_jal_allprogs.py Project-wide jal (and jump/branch) sweep to TARGET - walks the project's domain files and opens each program read-only from a single headless invocation.
find_jal_rawbytes_allprogs.py Raw-byte project-wide sweep for the encoded jal TARGET word in initialized memory - catches call sites in bytes Ghidra never disassembled.
find_refs_allprogs.py For every program, ask the reference manager for ALL references to each TARGET (data refs, computed-call refs) and report whether a function/symbol is defined there.
find_data_word.py Generic u32-LE-literal scanner across every initialized memory block, with surrounding-dword context. Useful when you suspect a function pointer is stuffed in a dispatch table somewhere; reports the containing function (if any) plus 8 dwords of surrounding data so the table structure is visible.
find_terrain_emitter_caller.py Combined ref-manager + LUI+ADDIU + ori + jal / j direct-target sweep against a configurable target-address set. Reports every overlay where each target is loaded as an immediate, stored / loaded via base+offset, or called directly. Useful pattern for any "who calls function X across the overlay set?" question: edit TARGET_ADDRS and TARGETS_HEX, run against each -process <overlay> in turn. The cross-program jal sweep is the unlock - Ghidra's ref manager only sees refs internal to one program.
find_string_xrefs.py Resolve dev-path string literals (h:\\prot\\...) to RAM addresses and dump every code site that references them.
find_dat_8007c018_writers.py LUI+ADDIU writers/readers/addressors of DAT_8007C018 - the per-kingdom-per-kind data pointer table.
write_only_dat_8007c018.py Tight writes-only filter for the DAT_8007C018 / DAT_8007BB38 globals, so the output stays manageable across every overlay.
xref_dat_8007c018.py Reference-manager query over the DAT_8007C018 table window - catches refs the lui+addiu pattern-matchers miss.
xref_world_map_globals.py Reference-manager probe for a configurable set of world-map subsystem globals (the DAT_8007C018 installer + companions).
Caller / xref helpers - answer "who calls this?" for direct and indirect call sites.
Script Purpose
find_callers_of.py Generic "callers of these target functions" tool. Edit TARGETS_HEX.
find_callers_of.py + find_addr_data.py Combined check for "is this function actually called?" - direct jal plus address-as-data.
dispatcher_callers.py Callers of FUN_8001f05c (asset dispatcher) and FUN_8001a55c (LZS).
find_jalr_handlers.py Locate dispatch-table indirect calls (lw R, +0x10(...) followed by jalr R).
Subsystem-targeted scanners - pre-aimed hunts for a specific format or subsystem's consumers.
Script Purpose
find_sound_path_builders.py LUI+ADDIU pairs landing in the sound-driver string cluster 0x8007B380..0x8007B3D0 (see docs/formats/sound-driver.md).
find_debug_flag_writers.py Two-pass scan for writers/readers of the documented debug-flag RAM band 0x8007B400..0x8007BCFF.
find_move_table_consumers.py Readers of the MOVE / MOVE2 buffers (0x8007B888 / 0x8007B840).
find_anm_buffer_users.py Readers/writers of the ANM buffer pointer (_DAT_8007b7c8).
find_anm_tick_walker.py Candidate ANM per-frame walkers: functions reading +0x4C (anm_pc), +0x56 (anm_state) and +0x68 (anm_timer) off the same base register.
find_mes_buffer_users.py Readers/writers of the MES dialog buffer pointer (_DAT_8007b8a8).
find_tmd_renderer.py Readers of the TMD pointer table at 0x8007C018 + idx*4.
find_gte_users.py Count COP2 / GTE instructions per function - surfaces renderer + transform candidates.
find_addprim_emitters.py Sweep for PSX-GPU POLY_FT4 / POLY_GT4 emitter sites (the textured-quad code bytes 0x2C..0x2F written at packet+7).
find_gp_drawable_list_writers.py Every write into the $gp-relative window around the drawable-list head at gp[0x148].
find_streaming_consumers.py DATA_FIELD streaming buffer trail: callers of FUN_8002541c plus direct readers of 0x8007b85c.
find_field_pack_consumers.py Runtime consumers of the field-pack 97-slot schema: any function loading from one of the schema's interior slot offsets.
find_field_pack_magic.py Code that materialises the field-pack magic word 0x01059B84 - as a 32-bit immediate pair or byte-by-byte loads.
find_scene_v12_consumers.py Runtime consumers of the scene-v12 cluster (the PROT entries with the strict 16-byte header signature).
find_npc_clut_writers.py Every function in every program that reads/writes/computes an address in the NPC-CLUT staging buffer at 0x800F19B0.
find_row_479_dma.py Functions that build a (y << 16) | x PSX GPU DMA destination for row 479 - the row holding the runtime-generated NPC CLUT slots.
find_xp_table_readers.py LUI+ADDIU resolver kept for its generic pattern; its default target range is not the XP table - the retail XP curve is DAT_80076AF4, read by the overlay applier FUN_801E9504. Retarget LO/HI before running.
find_xp_table_all_overlays.py Same scan, run recursively across every imported program (SCUS + overlays).
find_prot_consumers.py Static map of every call site that passes a constant PROT index to the LBA resolver chain.
find_scene_name_writers.py Writers of the scene-name buffer at 0x80084548.
find_field_loader_callers.py Callers of the field/town asset loaders (FUN_8001f7c0 / FUN_800255b8) with arg-prep context.
asset_table_xrefs.py Xrefs to and around 0x801C70F0 (the in-RAM PROT TOC).
find_effect_bundle_consumers.py Effect-bundle init / spawn / walker (run on an imported battle overlay).
Game-mode state-machine recon - the hunts that pinned the 28-mode dispatcher.
Script Purpose
find_field_program_xrefs.py Resolve the field-program / mode-name string literals and dump xrefs.
find_game_mode_dispatcher.py Hunt for the game-mode dispatcher via the documented mode strings.
find_game_mode_writers.py Writers of the game-mode register at gp[0x524] / gp[0x494].
find_gp_init_and_mode_table.py Locate $gp initialization and readers of the 28-entry mode table at 0x8007078C.
find_per_mode_callers.py Direct or indirect callers of any handler in the mode table.
Overlay capture and analysis - import + inventory the RAM-resident overlay images.
Script Purpose
find_overlay_candidates.py Stand-alone Python (no Ghidra) - scans extracted PROT entries for MIPS-code-likelihood and ranks candidates.
dump_overlay.lua PCSX-Redux Lua: dump the runtime overlay code window 0x801C0000..0x801EFFFF to /tmp/.
import_overlay.sh Bash wrapper that imports + analyzes a captured overlay dump as Raw Binary at base 0x801C0000.
find_overlay_calls.py Every call (jal or resolved jalr) into the RAM-resident overlay region 0x801C0000..0x801FFFFF.
find_overlay_asset_loads.py Run on an imported overlay program: const-track every jal to a known SCUS asset loader and emit a CSV of loader,prot_index_or_string,caller_func,call_site.
inventory_overlay.py Per-program function inventory. Emits inventory_<programname>.csv with one row per function (entry / size / outgoing / incoming / top callees).
list_overlay_functions.py List functions in the active overlay program sorted by size, with outgoing-call counts.
list_overlay_memory.py List every memory block in the current program - confirms the address layout of an import.
list_programs.py List every program currently in the Ghidra project.
list_all_programs.py List every program in the open project including overlays (recursive) - confirms what exists before a multi-program sweep.
list_programs_quiet.py Print every program name as name [size] - for discovering -process argument names.
Static-analysis utilities - whole-program reports.
Script Purpose
explore.py Dump a JSON report of SCUS_942.54: every function with an LZSS-decoder fingerprint score, plus every defined string and its inbound xrefs.

Cross-cutting helpers under scripts/ (host-side, not Ghidra):

Script Purpose
scripts/ci/function-coverage.py Citation-ranked missing-helper tracker over the function dumps.
scripts/ghidra-analysis/call-graph.py callees / callers / xref over the dumps; replaces grep-across-files.
scripts/asset-investigation/scene-asset-detect.py Joins categorize.json with TIM/TMD scan hits to surface unknown-bucket entries that look like scene bundles.
scripts/ghidra-analysis/bulk-import-overlays.sh Reads find-overlay output, imports each high-score candidate, runs analysis + the inventory dumper.
scripts/ghidra-analysis/extract-mednafen-overlay.py Slices 0x801C0000-0x80200000 (256 KB) out of a gzipped mednafen save state.
scripts/ghidra-analysis/analyze-overlay.sh One-shot capture pipeline: decompress save → slice → import → emit asset-load CSV.

Known dev paths in the binary

SCUS_942.54 contains leftover Windows paths from the dev environment. Useful for guessing format families:

h:\PROT\FIELD\
DATA\FIELD\
data\field\player.lzs
h:\prot\all\data\field\player.lzs
h:\prot\field\card\tim.dat
h:\prot\battle\etim.dat
\tim.dat
\move.mdt

The h:\ prefix indicates a Windows dev box. The runtime doesn't actually open these paths in retail (no real h:\ drive on a PSX); the strings are leftover format artefacts that point at where each subsystem's data lives in PROT.

See also