From 155a150d771cb1d53f5d793afcb5cc31bfbb1f93 Mon Sep 17 00:00:00 2001 From: rengstrom Date: Mon, 31 Aug 2026 18:17:12 +0000 Subject: [PATCH 1/6] world: decompile nine we_object3 functions, frame driver to controller poll renderWorldMapFrame, func_800A2D50, func_800A47A4, func_800A2920, func_800A1F10, func_800A1678, func_800A358C, func_800A39BC and func_800A0388, with the unit-privacy, naming and doc passes that followed each one: WorldSprite moves into the unit, the render callback's sentinels and the command visibility bits get names, and we_object3's exports are reached through its header. --- config/ff8.yaml | 5 +- config/symbols.world.txt | 2 +- include/world.h | 48 +- include/world/we_object1.h | 6 +- include/world/we_object10.h | 2 - include/world/we_object13.h | 9 + include/world/we_object2.h | 3 - include/world/we_object3.h | 59 +- include/world/we_object4.h | 9 +- include/world/we_object5.h | 2 - include/world/we_object7.h | 1 - include/world/we_object9.h | 6 +- src/world/we_object1.c | 15 +- src/world/we_object10.c | 19 +- src/world/we_object2.c | 6 +- src/world/we_object3.c | 2011 ++++++++++++++++++++++++++++++++++- src/world/we_object4.c | 10 +- src/world/we_object5.c | 1 + src/world/we_object7.c | 1 + src/world/we_object9.c | 15 +- 20 files changed, 2111 insertions(+), 119 deletions(-) diff --git a/config/ff8.yaml b/config/ff8.yaml index 8fcff98c..735c58b8 100644 --- a/config/ff8.yaml +++ b/config/ff8.yaml @@ -526,7 +526,10 @@ binaries: bss_size: 0x1FACB subsegments: - [0x0, rodata] - - [0xEC, .rodata, we_object3] + # Undotted: func_800A1678 is decompiled, so there is no asm function left to + # migrate this into. The copy in C tests the source alignment at run time, so + # the table has to stay an external symbol rather than a const in the .c. + - [0xEC, rodata] - [0x11C, rodata] - [0x120, .rodata, we_object4] - [0x15C, rodata] diff --git a/config/symbols.world.txt b/config/symbols.world.txt index a15f1447..c1b578f9 100644 --- a/config/symbols.world.txt +++ b/config/symbols.world.txt @@ -79,7 +79,7 @@ func_8009FEBC = 0x8009FEBC; // type:func getAngleDelta = 0x800A00B4; // type:func func_800A01DC = 0x800A01DC; // type:func func_800A0388 = 0x800A0388; // type:func -func_800A05E8 = 0x800A05E8; // type:func +renderWorldMapFrame = 0x800A05E8; // type:func setWorldMapTransVector = 0x800A1540; // type:func func_800A1678 = 0x800A1678; // type:func initWorldDoubleBuffer = 0x800A2350; // type:func diff --git a/include/world.h b/include/world.h index 38ad0ae2..7aff8f44 100644 --- a/include/world.h +++ b/include/world.h @@ -27,9 +27,10 @@ typedef struct { s32 z; /* 0x08 */ } WorldPos; -/** 4-byte slot read as either a full word or just the low halfword. */ +/** 4-byte slot read as either a full signed word (angle compares and deltas + are signed) or just the low halfword. */ typedef union { - u32 word; + s32 word; u16 half; } AngleSlot; @@ -42,7 +43,16 @@ typedef union { typedef struct WorldObject { struct WorldObject *next; /**< 0x00: Next node. */ s16 id; /**< 0x04: s16 id used by search. */ - u8 pad06[4]; + s16 key; /**< 0x06: Search key passed to worldObjectById. */ + /** 0x08: read both ways — as the byte index into the section's offsets[] + table, and as a halfword row key by the world render callback. */ + union { + struct { + u8 idx; /**< 0x08 */ + u8 pad09; /**< 0x09 */ + } b; + s16 row; /**< 0x08 */ + } slot; u8 sectionIdx; /**< 0x0A: Section index into the D_800C4D5C region table. */ } WorldObject; @@ -79,6 +89,20 @@ typedef struct { u8 param; /**< 0x0F: Command parameter byte. */ } CmdDesc; +/* Per-code visibility bits of CmdDesc.param, tested by the world placement + * gates (func_800A45D8 / func_800A4670 / func_800A2D50) after packing the + * descriptor as type | flag << 8 | param << 16. Meanings unknown beyond the + * code range each bit answers for -- placeholder names. */ +#define CMDPAR_VIS_00_09 0x80 /**< codes 0x00..0x09 and 0x80 */ +#define CMDPAR_VIS_20_28 0x40 /**< codes 0x20..0x28 and 0x84 (kind gate) */ +#define CMDPAR_VIS_30 0x20 /**< code 0x30 (kind gate) */ +#define CMDPAR_VIS_31 0x10 /**< code 0x31 (kind gate) */ +#define CMDPAR_MODE_20_28 0x04 /**< codes 0x20..0x28 and 0x84 (mode gate) */ +#define CMDPAR_MODE_30 0x02 /**< code 0x30 (mode gate) */ +#define CMDPAR_MODE_31 0x01 /**< code 0x31 (mode gate) */ +/** Mode-gate bit for code 0x32, in CmdDesc.flag. */ +#define CMDFLAG_MODE_32 0x80 + /** * @brief Slot entry in the D_800DBFB8 table (stride 40 bytes). * @@ -233,14 +257,16 @@ extern s16 D_800C53EC[]; extern u8 *D_800C96D0; extern s32 D_800C9710; extern s16 D_800C977A; -extern AngleSlot D_800C97F4; /**< Camera angle slot (word/half view). */ +extern s32 D_800C97F4; /**< World camera angle. Stored as a word; the + angular-delta helpers in we_object9 read only its + low half — hence the (u16) cast at those sites. */ extern MATRIX D_800C9838; /**< World-to-screen matrix loaded into the GTE. */ extern WorldPos D_800C9868; /**< Source camera world position (cast to VECTOR* for GTE transform func_800BC544). */ extern SVECTOR D_800C9770[2]; /**< Camera scratch: [0] is a position offset, [1] a rotation. */ extern s32 *D_800C9744; /**< Texture-strip animation block: a NULL-terminated s32 offset table; each offset, relative to this pointer, locates one strip record (see func_800A7B38). */ -extern s16 D_800C9E38[3]; /**< Per-frame camera movement deltas ([0]=x, [2]=z); cleared on world init (we_object1), fed to the particle camera-follow drift (we_object4). */ +extern SVECTOR D_800C9E38; /**< Per-frame camera movement deltas ([0]=x, [2]=z); cleared on world init (we_object1), fed to the particle camera-follow drift (we_object4). */ extern s16 D_800D239A; extern CVECTOR D_800DB0D0[3]; /**< Map-view HUD palette (copied from D_800C5448 by func_800ABC98): three RGB stops, dark to light. @@ -249,7 +275,12 @@ extern CVECTOR D_800DB0D0[3]; /**< Map-view HUD palette (copied from D @note func_800A8400 also sums stop 0's channels to centre the location-name banner; that use is not understood. */ -extern u8 D_800C5398[]; /**< 4-byte flag block. */ +/** Per-slot message/section key block. A slot holds @c WORLD_SLOT_NONE when + it is unclaimed; @c func_800A610C also returns that when no section + qualifies. */ +extern u8 D_800C5398[]; +#define WORLD_SLOT_NONE 0xFF /**< Unclaimed slot / no section found. */ +#define WORLD_SLOT_COUNT 4 /**< Slots in @c D_800C5398. */ extern s32 D_800C9718; extern s16 D_800C97E8; /**< Worldmap screen height reference. */ extern s16 D_800C97EA; /**< Worldmap screen width reference. */ @@ -490,13 +521,16 @@ typedef struct { /* 0x00 */ u8 pad00[0x18]; /* 0x18 */ Track tracks[2]; /**< Track A at 0x18, Track B at 0x24. */ /* 0x30 */ u8 pad30[0x3C]; - /* 0x6C */ s32 unk6C; /**< Flags word; bit 0x100 mirrors a CmdDesc flag. */ + /* 0x6C */ s32 unk6C; /**< Flags word; see SLOT_FLAG_CMD_MIRROR. */ /* 0x70 */ u8 pad70[0x4]; /* 0x74 */ u32 flags[2]; /**< 64-bit flag set (low/high). */ /* 0x7C */ u8 bytes[2]; /**< Action bytes selectable by 0xFF2E. */ /* 0x7E */ u8 pad7E[2]; } Slot; +/** @c Slot.unk6C bit mirroring @c CmdDesc.flag bit 0x08 (hence the shift by 5). */ +#define SLOT_FLAG_CMD_MIRROR 0x100 + /** * @brief 16-byte transform entry from @c *D_800D2128 — VECTOR translation * plus a partial (y, z) rotation pair. diff --git a/include/world/we_object1.h b/include/world/we_object1.h index 829e95cf..cdf2d89b 100644 --- a/include/world/we_object1.h +++ b/include/world/we_object1.h @@ -75,7 +75,7 @@ extern s32 D_800C4D90; extern s32 D_800C4D94; extern s32 D_800C9714; extern u8 D_800C9758[]; /* 15-byte light-matrix work buffer */ -extern s32 D_800C97A0; +extern u32 *D_800C97A0; extern s32 D_800D212C; extern s32 D_800D2458; extern s32 D_800C4CA4[]; /* source config table */ @@ -91,7 +91,7 @@ extern u32 D_800D2278[]; extern VECTOR D_800980DC; /* constant view offset {0, 0, -0x1800, 0} */ extern VECTOR D_800C9748; /* mirrored copy of the transformed position */ extern CmdDesc *D_800C4D6C; -extern s32 D_800C9778[]; /* scratch buffer passed to the visibility check */ +extern SVECTOR D_800C9778[]; /* base angles passed to the fan-placement probe */ extern RECT D_800C8698; extern u8 D_800980CC[]; /* "x:\USPC\WORLD" — dev-filesystem prefix (13 chars + NUL) */ extern POLY_FT4 D_800C8648[2]; /* double-buffered worldmap quad primitive */ @@ -123,11 +123,9 @@ extern s32 func_8009D7D8(s32 a); extern s32 func_800ACD38(MATRIX *out); extern void func_8003FD84(MATRIX *xform, VECTOR *in, VECTOR *out); extern void func_800BC544(VECTOR *src, VECTOR *dst); -extern s32 worldPosToCell(VECTOR *pos, SVECTOR *out); extern void func_8009C478(s32 *src, s32 x, s32 y); extern CmdDesc *glyphAt(GlyphQuery *v, AngleSlot *out); extern s32 func_800BEC1C(s32 kind); -extern s32 func_800A2D50(s32 a0, s32 a1, s32 *out, s32 a3, s32 a4, s32 a5); extern void func_8009D630(void); extern void func_800B3FD4(Slot *slot, s32 arg); extern void fadeOutSfxFast(s32 idx); diff --git a/include/world/we_object10.h b/include/world/we_object10.h index 15462deb..0ebe18ef 100644 --- a/include/world/we_object10.h +++ b/include/world/we_object10.h @@ -66,13 +66,11 @@ extern void func_800AEB58(XformEntry *entry, XformGroup *group); extern void func_8009C5FC(s32 *data); extern void func_800BC51C(VECTOR *src, VECTOR *dst); extern void func_800BC544(VECTOR *src, VECTOR *dst); -extern s32 worldPosToCell(VECTOR *pos, SVECTOR *out); extern ScriptOp *func_800AF004(u8 *base, s32 flag); extern s32 func_8009B358(s32 slotIdx, s32 strIdx, u8 *text); extern void func_8009D8A8(s32 a0); extern void fadeOutSfxFast(s32 idx); extern void renderAndUpdateDisplay(s32 frameCount); extern s32 renderBattleDisplayList(s32 *colorTag); -extern s32 func_800A358C(s32 a0, void *a1, void *a2, s32 a3); #endif /* WORLD_WE_OBJECT10_H */ diff --git a/include/world/we_object13.h b/include/world/we_object13.h index 2ebf9b50..2ee96342 100644 --- a/include/world/we_object13.h +++ b/include/world/we_object13.h @@ -35,4 +35,13 @@ extern u32 D_80082C14; extern void func_80047C3C(u8 *msg); +/* Advance the streaming pipeline one tick. Returns the controller's status: + 0 while idle, -1 on error, otherwise the pending block count. */ +extern s32 func_800C4558(s32 mode, s32 *out); +/* Stream one @p len -byte record from @p src through the staging buffer + @p buf. Returns 0 while the transfer is still in flight. */ +extern s32 func_800C4480(s32 src, s32 len, u32 *buf); +/* Reset the streaming controller; returns its status word. */ +extern s32 func_800C4450(void); + #endif /* WORLD_WE_OBJECT13_H */ diff --git a/include/world/we_object2.h b/include/world/we_object2.h index 43b28fd0..5dbc0d12 100644 --- a/include/world/we_object2.h +++ b/include/world/we_object2.h @@ -57,7 +57,6 @@ extern void func_800A5FD4(s32 screen); extern void func_80099EDC(s32 idx); extern void func_800B3FD4(Slot *a0, s32 a1); extern void func_8009D630(void); -extern void func_800C4450(void); extern void renderAndUpdateDisplay(s32 mode); extern s32 renderBattleDisplayList(s32 *colorTag); extern s32 func_800BD380(s16 *outLow, s16 *outHigh); @@ -68,13 +67,11 @@ extern s32 func_800A017C(SVECTOR *v); /* Shared world helper: copies @p pos's low-16-bit angle triple into @p out and * returns a 128x96 grid-cell index derived from its X/Z world coords. */ -extern s32 worldPosToCell(VECTOR *pos, SVECTOR *out); /* Reports whether a script is active and writes its current key to @p output. */ extern s32 func_800BEDF0(u8 *output); /* Helpers used by setupWorldRender (world-map sound/display setup). */ extern void func_800C3DB0(void (*cb)(void)); -extern void func_800A47A4(void); extern void sndEnableReverb(u32 a); extern void sndDisableReverb(u32 a); diff --git a/include/world/we_object3.h b/include/world/we_object3.h index cdd9a4b5..628a5358 100644 --- a/include/world/we_object3.h +++ b/include/world/we_object3.h @@ -5,17 +5,20 @@ #include "psxsdk/libgpu.h" #include "world.h" +/** One entry of the @c D_800D24A8 descriptor cache: a command descriptor and + the cell key it was resolved for, kept in most-recently-used order. */ typedef struct { - s32 val; /* +0x00 */ - s16 hval; /* +0x04 */ - s16 pad; /* +0x06 */ + CmdDesc *val; /* +0x00 */ + s16 hval; /* +0x04 — cell key the descriptor was resolved for */ + s16 pad; /* +0x06 */ } FeaEntry40C0; /* size 0x08 */ /* Object glyph header returned by func_800A5EC4: a count byte followed by an array of @c CmdDesc entries (16-byte stride). */ typedef struct { u8 count; /* 0x00 */ - u8 pad01[3]; + u8 id; /* 0x01 — glyph kind, dispatched on by func_800BF20C/func_800BFBFC */ + u8 pad02[2]; CmdDesc entries[1]; /* 0x04 — [count] entries, stride 0x10 */ } GlyphHeader; @@ -32,9 +35,10 @@ extern WorldObject D_800D3320[16]; extern WorldObject *D_800D3318; extern WorldObject *D_800D34E0; extern WorldObject *D_800D34E4; -extern u32 D_800D2284; +extern WorldObject *D_800D2284; /**< Head of the active world-object list. */ +extern WorldObject D_800C9888[]; /**< Pool the visible-cell list is built in. */ extern u32 D_800D34A0[16]; -extern u32 D_800D34F0; +extern u32 D_800D34F0[]; /**< Streamed-record staging buffer. */ extern WorldObject D_800D33E0[16]; extern WorldObject D_800C9EF0[16]; extern WorldObject *D_800CA030; @@ -61,29 +65,36 @@ extern void setWorldMapTransVector(s16 coord0, s16 coord1); /* Register master-list objects not yet present in any tracking list onto the active list. */ extern void registerNewWorldObjects(void); -/** - * @brief One placed world-map sprite produced by @c placeWorldSpriteFan (0x2C stride). - * - * @c pos is the final world position; @c cell receives the @c worldPosToCell - * projection; @c cellId/flag are the projected grid-cell id and a fixed marker. - * @note Field purpose partly uncertain — named from the access pattern. - */ -typedef struct { - VECTOR pos; /* 0x00 */ - SVECTOR cell; /* 0x10 — worldPosToCell output */ - u8 pad18[0x8]; /* 0x18 */ - s16 cellId; /* 0x20 — worldPosToCell return */ - s16 flag; /* 0x22 */ - u8 pad24[0x8]; /* 0x24 */ -} WorldSprite; /* 0x2C */ - /* Generate up to 5 spread-positioned SVECTOR offsets for the scene @p ctx. */ extern void func_800B5ADC(s32 ctx, SVECTOR *out, s32 c, s32 d); -/* Returns a per-scene angle bias used to orient the placed sprites. */ -extern s32 func_800BC5E0(s32 ctx); /* Compute a linear tile index from 2D world coordinates (32x24 grid). */ extern s32 func_800A5E40(s32 x, s32 y); +/* Visibility gate keyed on a packed command code; sibling of func_800A45D8. */ +extern s32 func_800A4670(u32 a, s32 b); + +/* Linear tile index from 2D world coordinates; sibling of func_800A5E40. */ +extern s32 func_800A5DC8(s32 x, s32 y); + +/* Rebuild the world-map sprite pool and draw every pending world object. */ +extern void renderWorldMapFrame(void); + +/* World render callback: advances the streamer and the object lists once per + frame. Registered with func_800C3DB0; returns 2 busy / 1 armed / 0 idle. */ +extern s32 func_800A47A4(void); + +/* Probe up to 8 rotations for a free 5-sprite fan placement around the camera; + returns 1 and writes sprite 0's position to hitPos when one is accepted. */ +extern s32 func_800A2D50(s32 code, s32 angZ, SVECTOR *angles, VECTOR *hitPos, s32 arg4, s32 *hitCell); + +/* Place a five-sprite fan around a slot and emit it; returns func_800B21EC's + result (negative when the fan was rejected). */ +extern s32 func_800A358C(s32 kind, SlotEntry *slot, SVECTOR *angles, s32 flag); + +/* Visibility gate: picks a CMDPAR_VIS_* bit of packed code @p a by dispatch + code @p b (only b's low 16 bits are examined; D_800C4D20 == 0 force-passes). */ +extern s32 func_800A45D8(u32 a, s32 b); + #endif /* WORLD_WE_OBJECT3_H */ diff --git a/include/world/we_object4.h b/include/world/we_object4.h index 4504a1cc..88f5b2ab 100644 --- a/include/world/we_object4.h +++ b/include/world/we_object4.h @@ -13,10 +13,17 @@ extern s32 D_800D2264; /**< Counter increment per update (read by w extern void func_800A7B38(void); /**< Step the texture-strip animations and upload changed frames. */ +/** The camera-follow reference is written as a word but published as its low + * half by we_object3, so both views need a name. */ +typedef union { + s32 word; + u16 half; +} CameraRef; + /* Camera-follow reference read by we_object1, we_object3 and we_object7. * volatile is load-bearing: it keeps the read ordered against the particle * position stores, matching the original schedule. */ -extern volatile s32 D_800C9870; +extern volatile CameraRef D_800C9870; /** * Spawn a kind-0xE particle in the D_800D9CB0 pool at @p pos (rotation diff --git a/include/world/we_object5.h b/include/world/we_object5.h index 206749be..3a2ef52e 100644 --- a/include/world/we_object5.h +++ b/include/world/we_object5.h @@ -62,10 +62,8 @@ extern VECTOR D_800DD658; /* source position for func_800BC51C extern void func_800A84D0(void); extern s32 func_8003F9F4(CVECTOR *input, CVECTOR *cue, s32 w1, s32 w2, CVECTOR *out); extern s32 func_8009CC3C(void); -extern s32 worldPosToCell(VECTOR *pos, SVECTOR *out); extern s32 func_800A4700(s32 a, s32 b); extern s32 func_800A475C(s32 a, s32 b); -extern s32 func_800A5DC8(s32 x, s32 y); extern void func_800423DC(VECTOR *a, s32 *b_pos, VECTOR *out); /* Rotates four corner offsets into place and projects them; @c outSXY takes * the four packed screen points and @c outOTZ their average depth. */ diff --git a/include/world/we_object7.h b/include/world/we_object7.h index e7f9366e..0b8de17f 100644 --- a/include/world/we_object7.h +++ b/include/world/we_object7.h @@ -43,7 +43,6 @@ extern u8 *D_800C96C8; /* func_8009B358/func_8009B550 are defined in we_object1, func_8009D8A8 in * we_object2; addItemToInventory and the func_800A5DC8/func_800B01A0/ * func_80041E84 helpers are main-binary. Caller-local prototypes. */ -extern s32 func_800A5DC8(s32 x, s32 y); extern s8 func_800B01A0(s16 viewY, s16 viewX, TrackEntry *e, u16 *posOut, s8 *unk30Out, u8 *unk2COut); extern s32 func_80041E84(s32 y, s32 x); extern s32 func_8009B358(s32 slotIdx, s32 strIdx, u8 *text); diff --git a/include/world/we_object9.h b/include/world/we_object9.h index 7f04e962..d6a94f49 100644 --- a/include/world/we_object9.h +++ b/include/world/we_object9.h @@ -93,6 +93,8 @@ extern s32 D_800DD698; extern s32 D_800DD69C; extern s32 D_800C971C; +/* Per-scene angle bias applied to a placed sprite fan. */ +extern s32 func_800BC5E0(s32 ctx); extern s32 func_8009CC3C(void); extern s32 func_800AC0A0(s32 type, PosDesc *pos, Velocity *vel, s32 flags); extern void func_800BFFEC(void); @@ -102,10 +104,6 @@ extern u8 *func_800BC974(u8 *buf, s32 id); extern void func_80047C74(u8 *dst, u8 *src); extern u8 *getStatName(s32 statId); extern u8 *func_800BCE74(u8 *buf, s32 statId); -extern s32 func_800A4670(u32 a, s32 b); -extern s32 func_800A358C(s32 a, SlotEntry *b, u8 *c, s32 d); extern s32 func_800B00D8(s32 a); -extern s32 worldPosToCell(VECTOR *pos, SVECTOR *out); -extern s32 func_800A5DC8(s32 x, s32 y); #endif /* WORLD_WE_OBJECT9_H */ diff --git a/src/world/we_object1.c b/src/world/we_object1.c index 5de745cb..e7dfe12c 100644 --- a/src/world/we_object1.c +++ b/src/world/we_object1.c @@ -6,6 +6,7 @@ #include "gamestate.h" #include "world/we_object1.h" #include "world/we_object2.h" /* getAngleDelta */ +#include "world/we_object3.h" /* func_800A2D50 */ #include "psxsdk/libapi.h" #include "psxsdk/libgpu.h" #include "psxsdk/libc.h" @@ -227,7 +228,7 @@ void func_8009A4DC(s32 count) { * @c D_800C4D30 = @c 0x1460, @c D_800C4D20 = @c 1, @c D_800C4D60 = @c 0xFFFF, the * section pointer @c D_800C4D5C = @c 0x80122000, and the "inactive" markers * @c D_800C4D78 / @c D_800C4D7C / @c D_800C4D88 / @c D_800C4D90 / @c D_800C4D94 = @c -1. - * It then fills the 4-byte @c D_800C5398 flag block with @c 0xFF and clears the + * It then fills the @c D_800C5398 slot block with @c WORLD_SLOT_NONE and clears the * light-matrix work buffer @c D_800C9758 via @c func_8009FEDC. */ void func_8009A638(void) { @@ -269,8 +270,8 @@ void func_8009A638(void) { D_800C4DAC = 0; D_800C4DB0 = 0; D_800C4DB4 = 0; - for (i = 0; i < 4; i++) { - D_800C5398[i] = 0xFF; + for (i = 0; i < WORLD_SLOT_COUNT; i++) { + D_800C5398[i] = WORLD_SLOT_NONE; } func_8009FEDC(D_800C9758, 0); } @@ -687,7 +688,7 @@ void func_8009C070(void) { * angle is stashed for @c D_800D212C) and resolves a command descriptor with * @c glyphAt. That descriptor is published to @c D_800C4D64 / @c D_800C4D74 * (and to @c D_800C4D6C / @c D_800C4D68 when @c D_800C4D38 holds 0x31). Per-frame - * scratch (@c D_800C4D48, @c D_800C9E38[0..2]) is cleared, and the rotation and + * scratch (@c D_800C4D48, @c D_800C9E38) is cleared, and the rotation and * translation matrices @c D_800C9838 are loaded into the GTE. */ void func_8009C1A4(void) { @@ -706,9 +707,9 @@ void func_8009C1A4(void) { D_800C4D68 = desc; } D_800C4D48 = 0; - D_800C9E38[0] = 0; - D_800C9E38[1] = 0; - D_800C9E38[2] = 0; + D_800C9E38.vx = 0; + D_800C9E38.vy = 0; + D_800C9E38.vz = 0; D_800D212C = buf.angle; SetRotMatrix(&D_800C9838); SetTransMatrix(&D_800C9838); diff --git a/src/world/we_object10.c b/src/world/we_object10.c index ca572303..ad1b01e4 100644 --- a/src/world/we_object10.c +++ b/src/world/we_object10.c @@ -3,8 +3,10 @@ #include "gamestate.h" #include "world.h" #include "world/we_object10.h" +#include "world/we_object3.h" /* worldPosToCell */ - +/* Defined below; the reset path above is its only caller. */ +static void func_800BEFEC(u8 *base); /** @@ -1052,8 +1054,19 @@ s32 func_800BEFC4(void) { return result; } -/** Initializes nested array with ramp values. */ -void func_800BEFEC(u8 *base) { +/** + * @brief Seed the eight records at @p base with a descending ramp. + * + * Each record is @c 0x4C bytes and carries five @c s32 at offsets @c 0x38 + * through @c 0x48. They are written back to front, @c 0x48 first, starting at + * @c -0x1000 and rising by @c 0x400 a step, so in address order the five read + * @c 0, @c -0x400, @c -0x800, @c -0xC00, @c -0x1000. + * + * @param base First record; the reset path passes @c D_800DCE78. + * @note The record is still reached by raw offset rather than a struct, so the + * 0x4C stride and the 0x38..0x48 field range are not yet named. + */ +static void func_800BEFEC(u8 *base) { s32 outer = 7; s32 inner; s32 val; diff --git a/src/world/we_object2.c b/src/world/we_object2.c index e0df1eca..3798edf9 100644 --- a/src/world/we_object2.c +++ b/src/world/we_object2.c @@ -6,6 +6,8 @@ #include "btl_sfx.h" #include "world.h" #include "world/we_object2.h" +#include "world/we_object3.h" +#include "world/we_object13.h" /** * @brief Walk a world-engine image script and blit each record to VRAM. @@ -754,7 +756,7 @@ INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object2", func_8009F594); * - @c 0x4: set @c D_800C4D40 / @c D_800C4D44 from the map id (immediate, no step). * - @c 0x8: yaw @c D_800D2390.tail.angle — shortest-arc angle step (±0x20). * - @c 0x10: pitch @c D_800D2390.tail.unk0 (±0x10); for map 0x32 the target is - * @c -D_800C97F4.word / 24 - 0x100. + * @c -D_800C97F4 / 24 - 0x100. * - @c 0x20: per-map scroll triple element [2] → @c D_800D2390.head.unk4 (±0x200). * - @c 0x80: per-map scroll triple element [1] → @c D_800D2390.head.angle (±0x200). * @@ -871,7 +873,7 @@ void func_8009F6EC(void) { if (D_800C4D2C & 0x10) { s32 mode = D_800C4D38; s32 d3c = D_800C4D3C; - s32 t = D_800C97F4.word; + s32 t = D_800C97F4; s32 target; u16 cur; s32 d; diff --git a/src/world/we_object3.c b/src/world/we_object3.c index 60631ec5..aa2a7a16 100644 --- a/src/world/we_object3.c +++ b/src/world/we_object3.c @@ -3,12 +3,765 @@ #include "world.h" #include "world/we_object1.h" #include "world/we_object3.h" +#include "world/we_object4.h" +#include "world/we_object9.h" /* func_800BC5E0 */ +#include "thread.h" + +/* ---- private to renderWorldMapFrame ------------------------------------- + * Direction bits of D_800D2278 (declared in we_object1.h); only this unit + * tests them, so they stay here rather than in the shared header. Named to + * match the field side (see FIELD_PAD_XLOW in field.h). + */ +#define WORLD_PAD_XLOW 0x8000 /**< Stick/d-pad X-low (left). */ +#define WORLD_PAD_XHIGH 0x2000 /**< Stick/d-pad X-high (right). */ + +/** Analog axes sampled per frame: X, Y and the second stick's pair, per controller. */ +#define WORLD_PAD_AXES 8 +/** @c D_800D2278 alternates between two banks so the previous frame stays readable. */ +#define WORLD_PAD_BANKS 2 +/** @c D_800D2278 slot holding the buttons that went down this frame. */ +#define WORLD_PAD_PRESSED 2 +/** Frames an unchanged input must persist before it auto-repeats. */ +#define WORLD_PAD_REPEAT_DELAY 0x1F +/** Repeats emitted before the sub-counter resets and a fresh press is reported. */ +#define WORLD_PAD_REPEAT_STEPS 3 + +/* ---- world render callback (func_800A47A4) ------------------------------ */ + +/** @c D_800C4D60 packs a slot pair: the low byte is the armed code / low slot, + the high byte its partner. Hence the @c (u8) casts (low byte, emitting + @c lbu) and the @c >>8 / @c >>9 reads below — each is a view of the pair + that the @c u16 type cannot express on its own. */ +#define WORLD_CODE_NONE 0xFFFF /**< Both bytes 0xFF: nothing armed. */ +/** Bit 0 of @c D_800D241E selects the alternate (biased) code table. */ +#define WORLD_FLAG_ALTCODE 0x1 +/** Added to a code when the alternate table is selected. */ +#define WORLD_CODE_ALT_BIAS 0xC +/** Stride and length of one message record in the @c D_800C9E58 table. */ +#define WORLD_MSG_LEN 37 +/** Length of one per-object message record (@c D_800C97E0 / @c D_800C97E4). */ +#define WORLD_OBJMSG_LEN 18 +/** Frames to wait before retrying a stalled or failed stream read. */ +#define WORLD_STREAM_RETRY 0x78 +/** @c WorldFlags::opParam value that keeps an armed code on map 0x32. */ +#define WORLD_OPPARAM_KEEP 5 +/** World-map cell grid; a cell index is @c row * WORLD_GRID_W + column, and both + axes wrap (the map is a torus). */ +#define WORLD_GRID_W 0x80 +#define WORLD_GRID_H 0x60 +/** Fixed RAM staging buffer for streamed records. It has no symbol in the map, + so the address is spelled out; the @c Tim cast at the upload call is a real + reinterpretation of that buffer, not a type fix. */ +#define WORLD_STAGE_ADDR 0x801E8000 +/** GTE-scratchpad slot where the caller stages the projected probe point for + the descriptor hit tests (scratchpad RAM has no symbol in the map). */ +#define WORLD_PROBE_POINT ((VECTOR *)0x1F8002D0) + +/** Countdown to the next stream retry; @c D_800C53AC caches the last tick. */ +extern s32 D_800C53AC; +extern s32 D_800C53B0; +/** Staged per-object message address (the buffer itself is in the header). */ +extern s32 D_800D34E8; +/** World-engine mode flags; bit 0 picks the alternate code table. */ +extern u16 D_800D241E; + +/* None of the declarations below are referenced outside this unit either. */ + +/** + * @brief One placed world-map sprite produced by @c placeWorldSpriteFan (0x2C stride). + * + * @c pos is the final world position; @c cell receives the @c worldPosToCell + * projection; @c cellId/flag are the projected grid-cell id and a fixed marker. + * @note Field purpose partly uncertain — named from the access pattern. + */ +typedef struct { + VECTOR pos; /* 0x00 */ + SVECTOR cell; /* 0x10 — worldPosToCell output */ + u8 pad18[0x4]; /* 0x18 */ + CmdDesc *cmd; /* 0x1C — installed as the current descriptor D_800C4D64 */ + s16 cellId; /* 0x20 — worldPosToCell return */ + s16 flag; /* 0x22 — one of WORLD_SPRITE_* below */ + s16 angle; /* 0x24 — heading used to bias the camera track */ + s16 code; /* 0x26 — packed type | flag<<8 of the installed descriptor */ + u16 unk28; /* 0x28 — published to D_800C4D48; its readers are still asm */ + u8 pad2A[0x2]; /* 0x2A */ +} WorldSprite; /* 0x2C */ + +/** @c WorldSprite::flag states. */ +#define WORLD_SPRITE_FREE 0 /**< Slot unused. */ +#define WORLD_SPRITE_PLACED 2 /**< Given a position this pass. */ +#define WORLD_SPRITE_CLAIMED 3 /**< Matched a glyph this frame. */ + +/** @brief Probe vector: three unsigned halfword components plus padding. + * + * Unsigned because the cell deltas wrap modulo 0x10000 as they are added + * (@c func_800A1678 reads them with @c lhu), so overflow is intended. + */ +typedef struct { + u16 vx, vy, vz, pad; +} ProbeVec; /* 0x08 */ + +/** The six probe-corner offsets, used as two batches of three. */ +typedef struct { + ProbeVec v[6]; +} CellCorners; /* 0x30 */ + +/** @brief One cell's projection scratch, laid out so a single walking pointer + * reaches the delta, the corners and the probes. + * + * @c func_800A1678 indexes this as @c ProbeVec[10] from @c &delta: element 0 is + * the delta, 1..6 the corners, 7..9 the probes. + */ +typedef struct { + ProbeVec delta; /* 0x00 */ + CellCorners c; /* 0x08 */ + ProbeVec probe[3];/* 0x38 */ +} ProbeFrame; /* 0x50 */ + +/** Projected screen position as the GTE writes it (unsigned compare against the + screen width, so not @c DVECTOR). */ +typedef struct { + u16 sx, sy; +} ScreenXY; /* 0x04 */ + +/** Sprites in one pool record: an anchor plus the four spread around it. */ +#define WORLD_FAN_SPRITES 5 + +/** A pool record: five sprites placed together as one fan. */ +typedef struct { + WorldSprite sprite[WORLD_FAN_SPRITES]; +} WorldSpriteRec; /* 0xDC */ + +/** One of the two alternating pool banks of eight records. */ +typedef struct { + WorldSpriteRec rec[8]; +} WorldSpriteBank; /* 0x6E0 */ + +extern SVECTOR D_800CA038; /**< Reference offset fed to the pool placer. */ +/** Six probe-corner offsets, used as two batches of three by @c func_800A1678. + It stays an extern rather than a file-scope @c const because the struct copy + tests the source alignment at run time, which only happens when the compiler + cannot see the object's alignment. */ +extern CellCorners D_800980EC; +extern WorldSpriteBank D_800D2508[]; /**< Two alternating banks of eight records; + index 2 is the end address (= D_800D2BE8). */ +extern WorldSpriteBank D_800D2BE8[]; /**< End marker: D_800D2508 + one bank. */ +extern WorldSpriteRec *D_800D2478[10];/**< Records queued for this frame. */ +extern WorldSpriteRec D_800D32C8[]; /**< End of both banks: D_800D2508 + 16 records. */ +extern GlyphHeader *D_800D24A0; /**< Glyph currently being dispatched, or NULL. */ +extern GlyphHeader *D_800C9740; /**< Glyph used when worldObjectById finds no object. */ +extern s32 D_800C53A4; /**< Active pool bank (0 or 1). */ +extern s32 D_800C53A8; /**< Cleared when a record is placed, bumped by the + fallback; the pass loop stops below 4. */ +extern s32 D_800C9720; /**< Third argument to the glyph renderers, which + are still asm. */ +extern s32 D_800C972C; /**< Accumulated glyph-entry count. */ +extern s32 D_800C96D8[WORLD_PAD_AXES]; /**< Analog axes sampled this frame; + element 0 is pad 0's X, compared against 0x7F + +- 0x2D/0x2E to pick a bank. */ +extern s32 D_800D2240[WORLD_PAD_AXES]; /**< Previous frame's copy of + @c D_800C96D8, tested the same way. */ +extern s32 D_800C9724; /**< Frames elapsed this tick; drives the repeat timers. */ +extern s32 D_800C9ED0; /**< Pad 0's raw frame parameter. */ +extern s32 D_800C9ED4; /**< Pad 1's raw frame parameter, packed into the high half. */ +extern s32 D_800D2470; /**< Repeat delay accumulator. */ +extern s32 D_800D2474; /**< Repeat step counter. */ + +/* getAnimFrameParam returns u16 in thread.c, but every caller uses the result as + s32 with no widening mask; thread.h documents that those callers keep their own + declaration. func_80030F10 has no owning translation unit yet. */ +extern s32 getAnimFrameParam(s32 slot, s32 sub); +extern s32 func_80030F10(s32 arg); +extern s16 D_800C9772; /**< Receives the low half of the camera-follow + reference D_800C9870. */ + +/* The matched caller passes (&D_800C9868, &D_800C9838); the function itself + takes no parameters (it reads D_800D23C0), so keep the unprototyped form + for the call-site ABI. */ +static void func_800A1F10(); +extern s32 func_800A4420(WorldSpriteRec *rec, SVECTOR *ref, SVECTOR *out, s32 size); +static WorldObject *worldObjectById(s16 id, WorldObject *head); +/* All defined below; the render callback is the first caller in the file. */ +static void buildViewportCellList(WorldPos *cam, WorldObject *out, MATRIX *m); +static void func_800A6030(WorldObject **pp); +static WorldObject *func_800A60B4(s32 key, WorldObject *head); +static void func_800A6188(Tim *tim, u8 tableIdx); +static void drainPendingObjects(void); +static s32 func_800A2920(GlyphHeader *glyph, WorldSprite *st, s16 key, CmdDesc *end); +extern WorldSpriteRec *func_800A26E8(GlyphHeader *p, WorldSpriteRec *rec, s32 v); +extern s32 func_800A3C9C(WorldSpriteRec *rec, s32 mode); +extern s32 func_800A5B48(void); +extern s32 func_800B21EC(WorldSpriteRec *rec, s32 mode, s32 c, s32 d); +/* Both below are declared in we_object10.h, which this unit cannot include: + * its func_800B0010 prototype is (void) to serve a no-argument caller there, + * conflicting with the true (u32 kind) definition in we_object6.c that + * func_800A2D50 relies on. */ +extern void *func_80047CE4(void *dst, s32 c, u32 n); +extern s32 func_800B0010(u32 kind); +static void func_800A1678(s32 col, s32 mode); +static void func_800A0388(void); +static void func_800A39BC(WorldSprite *out, s16 h); +static void placeWorldSpriteFan(WorldSprite *out, VECTOR *v, SVECTOR *angles, + s32 arg3, s32 arg4, VECTOR *origin); +static void func_800A581C(void); +static s32 func_800A6254(WorldObject *head); +static void func_800A62E0(s16 val, u16 *coarse, u16 *fine); +static void func_800A6358(void); +static void func_800A63F0(void); +static void initWorldDoubleBuffer(void); +static void setupWorldRenderParams(void); +extern void func_800ACDC4(GlyphHeader *p, BattleSceneCtx *ctx, s32 arg); +extern void func_800BF80C(GlyphHeader *p, s32 *ot, s32 arg); +extern void func_800BF2E8(GlyphHeader *p, s32 *ot, s32 arg); +extern void func_800BF20C(CmdDesc *p, s32 kind, s32 arg); +extern void func_800BFBFC(s32 kind); + INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A01DC); -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A0388); +/** + * @brief Sample both controllers for the frame and derive the newly-pressed mask. + * + * Flips @c D_800C4D04 so this frame's buttons land in the other half of + * @c D_800D2278, leaving last frame's readable alongside. Both pads' button + * masks are packed into one word (pad 1 in the high half), the eight analog + * axes are copied to @c D_800D2240 before being resampled, and + * @c D_800D2278[WORLD_PAD_PRESSED] receives the buttons that went down this + * frame. + * + * When the input is unchanged that mask would be empty, so an auto-repeat runs: + * @c D_800D2470 accumulates until @c WORLD_PAD_REPEAT_DELAY, after which + * @c D_800D2474 counts @c WORLD_PAD_REPEAT_STEPS repeats and then reports the + * held buttons as if freshly pressed. + * + * @note The pressed expression is written out per branch rather than cached in a + * local: the stores to the repeat counters sit between the compare and the + * later branches, and re-reading is what keeps those loads there. Caching + * it lets gcc fold the equal case to a constant zero. + */ +static void func_800A0388(void) { + s32 idx; + + D_800C4D04 = D_800C4D04 == 0; + func_800275D4(); + D_800C9ED0 = getAnimFrameParam(0, 0); + D_800C9ED4 = getAnimFrameParam(1, 0); + D_800D2278[D_800C4D04] = func_80030F10(D_800C9ED0) | (D_800C9ED4 << 16); + + D_800D2240[0] = D_800C96D8[0]; + D_800D2240[1] = D_800C96D8[1]; + D_800D2240[2] = D_800C96D8[2]; + D_800D2240[3] = D_800C96D8[3]; + D_800D2240[4] = D_800C96D8[4]; + D_800D2240[5] = D_800C96D8[5]; + D_800D2240[6] = D_800C96D8[6]; + D_800D2240[7] = D_800C96D8[7]; + + D_800C96D8[0] = func_80027DB4(0, 2, 0); + D_800C96D8[1] = func_80027DB4(0, 3, 0); + D_800C96D8[2] = func_80027DB4(0, 0, 0); + D_800C96D8[3] = func_80027DB4(0, 1, 0); + D_800C96D8[4] = func_80027DB4(1, 2, 0); + D_800C96D8[5] = func_80027DB4(1, 3, 0); + D_800C96D8[6] = func_80027DB4(1, 0, 0); + D_800C96D8[7] = func_80027DB4(1, 1, 0); + + idx = D_800C4D04; + + if (D_800D2278[idx] == D_800D2278[(idx + 1) % WORLD_PAD_BANKS]) { + D_800D2470 += D_800C9724; + if (D_800D2470 >= WORLD_PAD_REPEAT_DELAY) { + if (D_800D2474 >= WORLD_PAD_REPEAT_STEPS) { + D_800D2474 = 0; + D_800D2278[WORLD_PAD_PRESSED] = D_800D2278[idx]; + } else { + D_800D2474 += D_800C9724; + D_800D2278[WORLD_PAD_PRESSED] = D_800D2278[idx] & (D_800D2278[idx] ^ D_800D2278[(idx + 1) % WORLD_PAD_BANKS]); + } + } else { + D_800D2278[WORLD_PAD_PRESSED] = D_800D2278[idx] & (D_800D2278[idx] ^ D_800D2278[(idx + 1) % WORLD_PAD_BANKS]); + } + } else { + D_800D2278[WORLD_PAD_PRESSED] = D_800D2278[idx] & (D_800D2278[idx] ^ D_800D2278[(idx + 1) % WORLD_PAD_BANKS]); + D_800D2470 = 0; + } +} + +/** + * @brief World-map frame driver: rebuild the sprite pool, then draw every + * pending world object. + * + * Runs once per world-map frame, in four stages: + * + * 1. Refresh the camera basis from @c D_800C9868 and derive @c coord, the + * packed tile index -- the fine part from the camera's @c x in the low 7 + * bits, the coarse part from its @c y shifted above them -- that the + * GTE translation vector is programmed from. Both pool banks are then + * re-placed by @c func_800A4420, whose per-record size argument depends on + * the current scene mode. + * 2. Unlink each queued record's object from the active list @c D_800D2284 + * onto a local list, resolve it to its world section, and dispatch its + * glyph (@c func_800BF20C, then @c func_800BFBFC and one of + * @c func_800BF80C / @c func_800BF2E8, or @c func_800ACDC4 when the glyph + * id is out of range). + * 3. Walk both banks looking for records whose five sprites are all free, + * place a fresh fan into them and track the camera toward the result. As + * each record is taken, the outgoing descriptor's flag bit is mirrored + * into the slot state and the record's own descriptor becomes current. + * 4. Splice the local list back onto the front of @c D_800D2284 and publish + * the focused object's section record to @c D_800C97A0. + * + * @note The two @c goto statements in stage 3 leave both the record scan and the + * pass loop and skip the no-record-placed fallback — the one thing C has + * no other spelling for. A @c placed flag instead costs 11 instructions + * and no longer matches, so the jump is what the original wrote. + * + * @note The clear of @c newHead after the splice is a dead store the compiler + * removes, but it is load-bearing at compile time: it ends cse's + * @c D_800D2284 == @c newHead equivalence so the walk re-reads the global. + */ +void renderWorldMapFrame(void) { + SVECTOR probe; + s32 tr[3]; + WorldObject *newHead; + WorldObject *newTail; + s32 coord; + s32 modeBias; + s32 mode; + s32 xm; + s32 xo; + s32 bank; + s16 small; + SVECTOR *ref; + s32 pass; + s32 z; + s32 i; + WorldSpriteRec *rec; + /* The record the pool scan matched. The object walk below borrows it as + scratch for the section it is about to walk, because sharing this + variable's live range is what the allocation needs: a separate variable + moves five instructions, and borrowing sec instead moves two. */ + WorldSpriteRec *found; + WorldSection *sec; + WorldSpriteRec **slot; + WorldSpriteRec **slotEnd; + WorldSpriteRec *slotRec; + WorldSprite *sprite; + WorldObject *node; + WorldObject *prev; + WorldObject *link; + WorldObject *obj; + WorldSection *section; + GlyphHeader *hdr; + /* One past the last glyph entry -- the sibling glyphAt uses the same + expression as a loop end. The two walks need separate variables: + sharing one moves 271 instructions. */ + CmdDesc *entryEnd; + CmdDesc *entryEnd2; + s32 size; + s32 v; + u32 vyu; + s32 vy; + /* The splice test compares against these rather than against NULL: a + register-to-register compare, not an immediate one, is what the + allocation needs (constants here move 51 instructions). */ + WorldObject *emptyHead; + WorldObject *emptyTail; + s16 t; + s16 key; + s16 h; + u16 keep; + CmdDesc *cmdp; + mode = D_800C4D38; + emptyHead = NULL; + emptyTail = NULL; + newHead = emptyHead; + newTail = emptyTail; + func_800A1F10(&D_800C9868, &D_800C9838); + xo = D_800C9868.x + 0x60000; + { + s32 rnd = xo; + if (xo < 0) { + rnd = 0x3FFFF + xo; + } + rnd = (rnd >> 18) << 18; + xm = xo - rnd; + } + coord = (xm / 0x800) + ((((D_800C9868.y + 0x48000) % 0x30000) / 0x800) << 7); + ref = &D_800CA038; + modeBias = mode - 0x20; + for (bank = 0; bank < 2; bank++) { + probe.vx = 0; + probe.vy = 0; + probe.vz = 0; + rec = D_800D2508[bank].rec; + if (rec < D_800D2508[bank + 1].rec) { + small = (modeBias >= 0) && (modeBias < 9); + do { + if (mode != 0x32) { + size = 0x40; + if (((mode != 0x30) && (!small)) && (mode != 0x84)) { + size = 0x20; + } + } else { + size = 0x100; + } + func_800A4420(rec, ref, &probe, size); + /* The temp and the two full stores are load-bearing; probe.vy += 0xA0 + in place moves 845 instructions. */ + vy = probe.vy; + if (bank != 0) { + probe.vy = vy + 0xA0; + } else { + probe.vy = vy - 0xA0; + } + rec++; + } while (rec < D_800D2508[bank + 1].rec); + } + } + + SetRotMatrix(&D_800C9838); + D_800D2478[0] = D_800D2508[0].rec; + slot = D_800D2478; + slotEnd = &D_800D2478[1]; + while (slot < slotEnd) { + slotRec = *(slot++); + rec = slotRec; + node = D_800D2284; + key = rec->sprite[0].cellId; + prev = NULL; + if (node == NULL) { + break; + } + while (node != NULL) { + if (node->id == key) { + break; + } + prev = node; + node = node->next; + } + + if (node == NULL) { + break; + } + if (prev != NULL) { + prev->next = node->next; + } else { + link = node->next; + D_800D2284 = link; + } + node->next = NULL; + if (newHead == NULL) { + newTail = node; + newHead = node; + } + if (newTail == NULL) { + func_8009C528(0x82); + } + newTail->next = node; + newTail = node; + obj = worldObjectById(node->key, D_800CA030); + if (obj != NULL) { + section = &D_800C4D5C[obj->sectionIdx]; + /* offsets[] holds a byte offset from the section base, so the u32 * turns + the >> 2 into a word index; idx[offsets] == offsets[idx], and the + index-first form matches the addu operand order (as in func_800A5EC4). */ + hdr = (GlyphHeader *) (((u32 *) section) + (node->slot.b.idx[section->offsets] >> 2)); + entryEnd = &hdr->entries[hdr->count]; + setWorldMapTransVector(coord, node->id); + if (hdr->id < 0x5B) { + D_800D24A0 = hdr; + func_800BF20C(entryEnd, hdr->id, D_800C4D30); + } else { + D_800D24A0 = NULL; + } + /* i is set before pass on purpose: folding it into the for header costs + four instructions here. */ + i = 0; + pass = D_800C53A4; + for (; i < 2; i++) { + found = func_800A26E8(hdr, D_800D2508[pass].rec, key); + if (found != NULL) { + WorldSpriteRec **q = slot; + while (q < slotEnd) { + if ((*q)->sprite[0].cellId == + found->sprite[0].cellId) { + break; + } + q++; + } + + if (q >= slotEnd) { + /* D_800D2478 is bounded by the next symbol: &D_800D24A0 is + &D_800D2478[10], and naming D_800D2478 there changes the + relocation and stops matching. */ + if (slotEnd < ((WorldSpriteRec **) (&D_800D24A0))) { + *(slotEnd++) = found; + } else { + func_8009C528(0x83); + } + } + } + pass = !pass; + } + + gte_SetRotMatrix(&D_800C9838); + rec = D_800D2508[0].rec; + /* 16 records = both banks, the same address as D_800D32C8 below; the + loop needs it spelled this way (D_800D32C8 here moves 717). */ + if (rec < (D_800D2508[0].rec + 16)) { + do { + for (sprite = rec->sprite; sprite < rec[1].sprite; sprite++) { + if ((sprite->cellId == key) && (sprite->flag == WORLD_SPRITE_PLACED)) { + sprite->flag = WORLD_SPRITE_CLAIMED; + } + } + + rec++; + } while (rec < D_800D32C8); + } + if (D_800D24A0 != NULL) { + gte_sttr(tr); + gte_ldtr(0, 0, 0); + func_800BFBFC(hdr->id); + if (((mode == 0x32) && (D_800D23D8[0] != 5)) && (D_800D23D8[0] != 6)) { + D_800C972C += hdr->count; + func_800BF80C(hdr, D_800D244C->primList, D_800C9720); + } else { + func_800BF2E8(hdr, D_800D244C->primList, D_800C9720); + } + gte_SetTransVector(tr); + } else { + func_800ACDC4(hdr, D_800D244C, D_800C9720); + } + } + } + + if (((((u32) D_800C4D38) < 0xA) || (D_800C4D38 == 0x80)) || (D_800C4D38 == 0x31)) { + s32 a = D_800D2278[D_800C4D04]; + s32 b = D_800D2278[(D_800C4D04 + 1) % 2]; + s32 chg = a & (a ^ b); + if (chg & WORLD_PAD_XLOW) { + D_800C53A4 = 0; + } else if (chg & WORLD_PAD_XHIGH) { + D_800C53A4 = 1; + } else if ((D_800C96D8[0] != (-1)) && (D_800D2240[0] != (-1))) { + s32 d = D_800D2240[0] - 0x7F; + s32 c = D_800C96D8[0] - 0x7F; + if ((c < (-0x2D)) && (d >= (-0x2D))) { + D_800C53A4 = 0; + } else if ((c >= 0x2E) && (d < 0x2E)) { + D_800C53A4 = 1; + } + } + } else { + s32 a = D_800D2278[D_800C4D04]; + if (a & WORLD_PAD_XLOW) { + D_800C53A4 = 0; + } else if (a & WORLD_PAD_XHIGH) { + D_800C53A4 = 1; + } else if (D_800C96D8[0] != (-1)) { + s32 c = D_800C96D8[0] - 0x7F; + if (c < (-0x2D)) { + D_800C53A4 = 0; + } else if (c >= 0x2E) { + D_800C53A4 = 1; + } + } + } + pass = D_800C53A4; + D_800C4D48 = 0; + D_800C9E38.vx = 0; + D_800C9E38.vy = 0; + D_800C9E38.vz = 0; + for (i = 0; i < 2; i++) { + rec = D_800D2508[pass].rec; + if (rec < D_800D2508[pass + 1].rec) { + /* D_800D2BE8 is the same address as D_800D2508[pass + 1].rec, but the + relocation has to name that symbol to match. */ + do { + if (((((rec->sprite[0].flag == WORLD_SPRITE_FREE) && (rec->sprite[1].flag == WORLD_SPRITE_FREE)) && + (rec->sprite[2].flag == WORLD_SPRITE_FREE)) && (rec->sprite[3].flag == WORLD_SPRITE_FREE)) && + (rec->sprite[4].flag == WORLD_SPRITE_FREE)) { + D_800C53A8 = 0; + h = func_800A3C9C(rec, mode); + for (v = 0; v < WORLD_FAN_SPRITES; v++) { + rec->sprite[v].cell.vy = h; + rec->sprite[v].pos.vy = h; + } + + v = func_800B21EC(rec, mode, 0, 0); + D_800C4D78 = v; + gte_SetRotMatrix(&D_800C9838); + gte_SetTransMatrix(&D_800C9838); + /* (u32)(x - 1) >= 2 is the two-sided test for x outside 1..2. */ + if (((v >= 0) && (((u32) (D_800D23D8[0] - 1)) >= 2)) && + (D_800D23D8[0] != 0xD)) { + D_800C4D7C = v; + D_800C9770[0].vy = D_800C9868.z; + if (D_800C4D38 == 0x32) { + D_800C4D40 = 0; + } + goto after_pass; + } + v = func_800B21EC(rec, mode, 0xC8, 0); + D_800C4D7C = v; + if ((mode == 0x30) && (D_800D23D8[0] != 8)) { + /* The (u16)/(s16) casts below pick the load width and signedness the + heading comparison needs; each one changes the emitted code. */ + s16 cur = D_800C9868.z; + s32 want = (u16) rec->sprite[0].angle; + s32 back = ((u16) rec->sprite[0].angle) - 0x80; + u16 lo = rec->sprite[0].angle - 0x100; + keep = rec->sprite[0].pos.vy; + if (((u32) (rec->sprite[0].cmd->type - 0x21)) < 2) { + lo = want + 0x148; + if (((s16) (want + 0xC8)) > ((s16) cur)) { + cur += 0xE; + } + if (((s16) lo) < ((s16) cur)) { + cur -= 0xE; + } + } else { + if (((s16) lo) > ((s16) cur)) { + cur += 0xE; + } + if (((s16) back) < ((s16) cur)) { + cur -= 0xE; + } + } + h = cur; + for (v = 0; v < 5; v++) { + rec->sprite[v].cell.vy = h; + rec->sprite[v].pos.vy = h; + } + + v = func_800B21EC(rec, mode, 0, 0); + if (((v >= 0) && (((u32) (D_800D23D8[0] - 1)) >= 2)) && + (D_800D23D8[0] != 0xD)) { + h = keep; + for (v = 0; v < 5; v++) { + rec->sprite[v].cell.vy = h; + rec->sprite[v].pos.vy = h; + } + + } + } + if (D_800C4DC8 == 0) { + cmdp = rec->sprite[0].cmd; + if ((D_800D23D8[0] == 0) || (D_800D23D8[0] == 0xD)) { + t = (D_800C9868.z = h); + D_800C9770[0].vy = t; + D_800C9770[0].vx += + ((u16) rec->sprite[0].pos.vx) - ((u16) D_800C9868.x); + vyu = (u16) D_800C9868.y; + D_800C9770[0].vz += vyu + rec->sprite[0].pos.vz; + D_800C9868.x = rec->sprite[0].pos.vx; + D_800C9868.y = -rec->sprite[0].pos.vz; + } + D_800C4D74 = D_800C4D64; + if (D_800C4D64 != NULL) { + /* Branchless mirror of CmdDesc flag 0x08 into the slot's 0x100 bit. */ + D_800D226C->unk6C = + (D_800D226C->unk6C & (~SLOT_FLAG_CMD_MIRROR)) | + ((D_800C4D64->flag << 5) & SLOT_FLAG_CMD_MIRROR); + } + cmdp = rec->sprite[0].cmd; + D_800C4D64 = cmdp; + if (mode == 0x31) { + D_800C4D68 = cmdp; + } + D_800D212C = rec->sprite[0].cellId; + D_800C97F4 = rec->sprite[0].angle; + D_800C4D48 = rec->sprite[0].unk28; + D_800C9E38 = D_800CA038; + } + D_800C53A4 = pass; + goto after_pass; + } + rec++; + } while (rec < D_800D2BE8[pass].rec); + } + if (D_800C53A8 < 4) { + break; + } + pass = !pass; + } + + { + rec = D_800D2508[D_800C53A4].rec; + z = func_800B21EC(rec, mode, 0, 0); + D_800C4D78 = z; + gte_SetRotMatrix(&D_800C9838); + gte_SetTransMatrix(&D_800C9838); + if (((z >= 0) && (((u32) (D_800D23D8[0] - 1)) >= 2)) && (D_800D23D8[0] != 0xD)) { + D_800C4D7C = z; + D_800C9772 = D_800C9870.half; + if (D_800C4D38 == 0x32) { + D_800C4D40 = 0; + } + } else { + D_800C4D7C = func_800B21EC(rec, mode, 0xC8, 0); + D_800C53A8++; + } + } + after_pass: + func_800A1678(coord, mode); + + for (node = D_800D2284; node != NULL; node = node->next) { + obj = worldObjectById(node->key, D_800CA030); + if (obj != NULL) { + section = &D_800C4D5C[obj->sectionIdx]; + found = (WorldSpriteRec *) section; /* borrowed as scratch */ + /* index-first subscript and word scaling, as above. */ + hdr = (GlyphHeader *) (((u32 *) found) + (node->slot.b.idx[section->offsets] >> 2)); + } else { + hdr = D_800C9740; + } + setWorldMapTransVector(coord, node->id); + entryEnd2 = &hdr->entries[hdr->count]; + if (hdr->id < 0x5B) { + D_800D24A0 = hdr; + func_800BF20C(entryEnd2, hdr->id, D_800C4D30); + gte_sttr(tr); + gte_ldtr(0, 0, 0); + func_800BFBFC(hdr->id); + if (((mode == 0x32) && (D_800D23D8[0] != 5)) && (D_800D23D8[0] != 6)) { + func_800BF80C(hdr, D_800D244C->primList, D_800C9720); + } else { + func_800BF2E8(hdr, D_800D244C->primList, D_800C9720); + } + gte_SetTransVector(tr); + } else { + D_800D24A0 = NULL; + func_800ACDC4(hdr, D_800D244C, D_800C9720); + } + } + + if ((newHead != emptyHead) && (newTail != emptyTail)) { + link = D_800D2284; + D_800D2284 = newHead; + newTail->next = link; + newHead = NULL; + if (newTail == NULL) { + return; + } + for (node = D_800D2284; node != NULL; node = node->next) { + if (node->id == D_800D212C) { + break; + } + } -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A05E8); + if (node != NULL) { + obj = worldObjectById(node->key, D_800CA030); + if (obj != NULL) { + sec = &D_800C4D5C[obj->sectionIdx]; + /* index-first subscript and word scaling, as above. */ + D_800C97A0 = ((u32 *) sec) + (node->slot.b.idx[sec->offsets] >> 2); + } + } + } +} /** * @brief Program the GTE translation vector (TR, control regs 5/6/7) for world-map rendering. @@ -60,9 +813,302 @@ void setWorldMapTransVector(s16 coord0, s16 coord1) { gte_ldtr(D_800DB0E8.vx + off.vx, D_800DB0E8.vy + off.vy, D_800DB0E8.vz + off.vz); } -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A1678); +/* Wrap a signed cell delta into the shorter way round the WORLD_GRID_W x + WORLD_GRID_H torus. The scratch @c t is assigned by the macro so the argument is + evaluated once for the tests; the in-range arm deliberately re-reads @c d instead + of @c t -- that asymmetry is what the original compiles to, and using @c t there + costs two instructions. */ +#define WRAP_COL(d, t) (((t) = (d)) < WORLD_GRID_W / 2 + 1 ? ((t) < -(WORLD_GRID_W / 2) ? (t) + WORLD_GRID_W : (d)) : (t) - WORLD_GRID_W) +#define WRAP_ROW(d, t) (((t) = (d)) < WORLD_GRID_H / 2 + 1 ? ((t) < -(WORLD_GRID_H / 2) ? (t) + WORLD_GRID_H : (d)) : (t) - WORLD_GRID_H) +/** Absolute value; re-evaluates its argument, so the wrap above runs three times + per axis exactly as the original does. */ +#define CELL_ABS(x) ((x) > 0 ? (x) : 0 - (x)) +/** Column/row distance from the camera cell @p c to node @p n. The @c (s16) cast is + load-bearing: it truncates the column to 16 bits (the @c sll/sra pair) before the + subtraction. @c ROW_DELTA caches the node id in @c w, which the vertical delta + below reuses -- that shared variable is what places the id in its register. */ +#define COL_DELTA(c, n) ((c) % WORLD_GRID_W - (s16)((n)->id % WORLD_GRID_W)) +/** The same column distance measured the other way, from node @p n back to the + camera cell @p c; this is the direction the per-node delta is built in. Since + @c id is already @c s16 the cast cannot change the value -- it exists purely + to force the truncation, and dropping it costs the @c sll/sra pair. */ +#define COL_DELTA_FROM(n, c) ((s16)((n)->id % WORLD_GRID_W) - (c) % WORLD_GRID_W) +#define ROW_DELTA(c, n) ((c) / WORLD_GRID_W - (w = (n)->id) / WORLD_GRID_W) + +/** Cell size in world units; a wrapped cell delta scales up by this. */ +#define CELL_UNITS_SHIFT 11 +/** Index of the corner block and the probe block within a @c ProbeFrame, counted + in @c ProbeVec units from @c &frame.delta. Indexing off that one base is what + makes the compiler derive the two pointers the original walks. */ +#define FRAME_CORNER 1 +#define FRAME_PROBE 7 +/** First corner of the batch-0 half of @c CellCorners; batch 1 starts at 0. */ +#define CORNER_BATCH0 3 +/** A projected corner counts as on screen below this X. */ +#define SCREEN_X_MAX 0x141 +/** Depth cut: map 0x32 keeps cells twice as far out as the rest. */ +#define DEPTH_CUT_WIDE 0x2000 +#define DEPTH_CUT_NORMAL 0x1801 -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A1F10); +/** + * @brief Drop world objects whose cell has scrolled out of view. + * + * Walks the active object list @c D_800D2284 and keeps each node that is either + * near the camera cell @p col or still projects on screen; the rest are unlinked. + * + * A node within @p lim cells of the camera on both axes (torus-wrapped, so the + * short way round) is kept outright. Otherwise its eight probe corners are + * projected in two batches of three through @c rtpt: batch 0 uses corners 3..5 + * for the node itself, and batch 1 rebuilds the delta for @c node->next and uses + * corners 0..2. A node survives if any projected corner lands within screen X + * and nearer than the depth cut (wide on map 0x32, normal elsewhere). + * + * @c lim is 3 for the wide maps (0x32 and 0x40..0x42) and 1 elsewhere. + * + * @param col Camera cell index (row * 0x80 + column). + * @param mode World dispatch code, selecting @c lim and the depth cut. + */ +static void func_800A1678(s32 col, s32 mode) { + ProbeFrame f; + s32 otz; + ScreenXY *sp; + WorldObject *node; + WorldObject *prev; + s32 lim; + s32 d; + s32 tz; + ProbeVec *pp; + ProbeVec *q; + s32 j; + s32 batch; + s32 w; + s32 nz; + + f.c = D_800980EC; + if (mode == 0x32 || (mode >= 0x40 && mode < 0x43)) { + lim = 3; + } else { + lim = 1; + } + gte_SetRotMatrix(&D_800C9838); + gte_SetTransVector(D_800C9838.t); + + d = COL_DELTA_FROM(D_800D2284, col); + d = WRAP_COL(d, tz) << CELL_UNITS_SHIFT; + f.delta.vx = d; + f.delta.vy = 0; + w = D_800D2284->id; + d = w / WORLD_GRID_W - col / WORLD_GRID_W; + nz = 0 - WRAP_ROW(d, tz); + f.delta.vz = nz << CELL_UNITS_SHIFT; + /* probe[j] = corner[j] + delta */ + pp = &f.delta; + for (j = 0; j < 3; j++) { + pp[j + FRAME_PROBE].vx = pp[j + FRAME_CORNER].vx + f.delta.vx; + pp[j + FRAME_PROBE].vy = pp[j + FRAME_CORNER].vy + f.delta.vy; + pp[j + FRAME_PROBE].vz = pp[j + FRAME_CORNER].vz + f.delta.vz; + } + + prev = NULL; + node = D_800D2284; + while (node != NULL) { + if (CELL_ABS(WRAP_COL(COL_DELTA(col, node), tz)) <= lim) { + if (CELL_ABS(WRAP_ROW(ROW_DELTA(col, node), tz)) <= lim) { + batch = 0; + goto keep; + } + } + { + ScreenXY screen[3]; + + for (batch = 0; batch < 2; batch++) { + gte_ldv3c(f.probe); + gte_rtpt(); + if (batch != 0) { + d = COL_DELTA_FROM(node->next, col); + d = WRAP_COL(d, tz) << CELL_UNITS_SHIFT; + f.delta.vx = d; + f.delta.vy = 0; + w = node->next->id; + d = w / WORLD_GRID_W - col / WORLD_GRID_W; + nz = 0 - WRAP_ROW(d, tz); + f.delta.vz = nz << CELL_UNITS_SHIFT; + q = f.c.v; + } else { + q = &f.c.v[CORNER_BATCH0]; + } + pp = &f.delta; + for (j = 0; j < 3; j++) { + pp[j + FRAME_PROBE].vx = q[j].vx + f.delta.vx; + pp[j + FRAME_PROBE].vy = q[j].vy + f.delta.vy; + pp[j + FRAME_PROBE].vz = q[j].vz + f.delta.vz; + } + gte_stsxy3c(screen); + gte_stszotz(&otz); + for (j = 0, sp = screen; j < 3; j++, sp++) { + if (sp->sx < SCREEN_X_MAX) { + if (otz < (mode == 0x32 ? DEPTH_CUT_WIDE : DEPTH_CUT_NORMAL)) { + goto keep; + } + } + } + } + if (prev != NULL) { + node = (prev->next = node->next); + } else { + node = (D_800D2284 = node->next); + } + continue; + } + keep: + prev = node; + node = node->next; + if (batch == 0) { + d = COL_DELTA_FROM(node, col); + d = WRAP_COL(d, tz) << CELL_UNITS_SHIFT; + f.delta.vx = d; + f.delta.vy = 0; + w = node->id; + d = w / WORLD_GRID_W - col / WORLD_GRID_W; + /* dead: recomputing the column delta here anchors the register + allocation the original used for the vertical delta. */ + tz = COL_DELTA(col, node); + nz = 0 - WRAP_ROW(d, tz); + f.delta.vz = nz << CELL_UNITS_SHIFT; + pp = &f.delta; + for (j = 0; j < 3; j++) { + pp[j + FRAME_PROBE].vx = pp[j + FRAME_CORNER].vx + f.delta.vx; + pp[j + FRAME_PROBE].vy = pp[j + FRAME_CORNER].vy + f.delta.vy; + pp[j + FRAME_PROBE].vz = pp[j + FRAME_CORNER].vz + f.delta.vz; + } + } + } +} + +/** + * @brief Build the list of world-map cells covered by the camera's visibility box. + * + * Reads the composed world position @c D_800D23C0 and forms a symmetric box + * of half-size 0x27FF around it (the +0x5D801/+0x627FF and +0x45801/+0x4A7FF + * corner offsets are +1.5 wraps -/+ 0x27FF, which keeps the coordinates + * positive before the wrap). Corner coordinates wrap on the world torus + * (0x40000 x 0x30000 units, 0x800-unit cells, so a 128 x 96 cell grid) and + * are packed as cell indices (col + row * 128). The wrapped column/row deltas + * between the two corners give the box extent, walked inclusively per axis. + * + * For every covered cell one @c WorldObject record is appended to the pool at + * @c D_800C9888: @c id = cell index, @c key = 32x24 sector key + * ((row / 4) * 0x20 + col / 4), @c slot.b.idx = 4x4 sub-cell index + * ((row %% 4) * 4 + col %% 4). The list is then null-terminated and published + * as the active world-object list head @c D_800D2284. + */ +static void func_800A1F10(void) +{ + WorldObject *rec; + s32 x; + s32 c1; + s32 c2; + s32 col; + s32 row; + s32 ncols; + s32 nrows; + s32 i; + s32 j; + s32 d; + + rec = D_800C9888; + x = D_800D23C0.x; + + c1 = ((x + 0x5D801) % 0x40000) / 0x800 + (((D_800D23C0.y + 0x45801) % 0x30000) / 0x800) * 0x80; + c2 = ((x + 0x627FF) % 0x40000) / 0x800 + (((D_800D23C0.y + 0x4A7FF) % 0x30000) / 0x800) * 0x80; + + d = c2 % 0x80 - c1 % 0x80; + if (d < 0x41) { + if (d < -0x40) { + d += 0x80; + } + } else { + d -= 0x80; + } + if (d > 0) { + d = c2 % 0x80 - c1 % 0x80; + if (d < 0x41) { + if (d < -0x40) { + d += 0x80; + } + } else { + d -= 0x80; + } + ncols = d; + } else { + d = c2 % 0x80 - c1 % 0x80; + if (d < 0x41) { + if (d < -0x40) { + d += 0x80; + } + } else { + d -= 0x80; + } + ncols = -d; + } + d = c2 / 0x80 - c1 / 0x80; + if (d < 0x31) { + if (d < -0x30) { + d += 0x60; + } + } else { + d -= 0x60; + } + if (d > 0) { + d = c2 / 0x80 - c1 / 0x80; + if (d < 0x31) { + if (d < -0x30) { + d += 0x60; + } + } else { + d -= 0x60; + } + nrows = d; + } else { + d = c2 / 0x80 - c1 / 0x80; + if (d < 0x31) { + if (d < -0x30) { + d += 0x60; + } + } else { + d -= 0x60; + } + nrows = -d; + } + + row = c1 / 0x80; + for (i = 0; i <= nrows; i++) { + col = c1 % 0x80; + for (j = 0; j <= ncols; j++) { + while (col >= 0x80) { + col -= 0x80; + } + while (row >= 0x60) { + row -= 0x60; + } + while (col < 0) { + col += 0x80; + } + while (row < 0) { + row += 0x60; + } + rec->id = row * 0x80 + col; + rec->key = (row / 4) * 0x20 + col / 4; + rec->slot.b.idx = (row % 4) * 4 + col % 4; + rec->next = rec + 1; + rec++; + col++; + } + row++; + } + (rec - 1)->next = 0; + D_800D2284 = D_800C9888; +} /** * @brief Initialize the world's two double-buffered graphics contexts. @@ -74,7 +1120,7 @@ INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A1F10); * NTSC active area (@c y=8, @c h=224), and enables dithering. Finally installs * the first buffer as the active scene context (@c D_800D244C). */ -void initWorldDoubleBuffer(void) { +static void initWorldDoubleBuffer(void) { s32 i; SetDefDrawEnv(&(&D_800CA040)[1].drawEnv, 0, 0, D_800C97EA, D_800C97E8); @@ -103,7 +1149,7 @@ INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A246C); * via @c SetBackColor, then copies the lighting color matrix @c D_800C5428 into * @c D_800DA8B0 and loads it via @c SetColorMatrix. */ -void setupWorldRenderParams(void) { +static void setupWorldRenderParams(void) { ResetGraph(3); SetGeomOffset((s16)D_800C97EA / 2, (s16)D_800C97E8 / 2); @@ -118,11 +1164,551 @@ void setupWorldRenderParams(void) { INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A26E8); -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A2920); +/** + * @brief Pick the best command descriptor for a sprite and install it. + * + * The scoring sibling of @c glyphAt, driven by the projected probe point the + * caller stages in the GTE scratchpad. Two-phase search, both phases hit-testing + * with @c func_800BF024: + * + * 1. The descriptor cache @c D_800D24A8, filtered to entries resolved for + * @p key. For dispatch codes 0x30/0x32 the lowest hit angle wins (code + * 0x30 additionally requires type 0x1E..0x22 while the camera-follow + * reference @c D_800C9870 is active); for any other code the first hit is + * taken outright. A cache hit that fails the visibility gate + * (@c func_800A45D8) still ends the search — it only reports no pick. + * 2. Only when the cache had no hit at all: the glyph's own @c entries[]. + * Candidates must pass a mode filter (codes 0x30/0x32 as above, low types + * always accepted, otherwise the hit angle must lie within 0xC8 of the + * sprite's current @c angle). The best candidate — smallest hit angle, or + * smallest deviation from the sprite's angle for plain codes — is inserted + * at the head of the cache (older entries shift down, the oldest drops + * out), then the visibility gate decides whether the pick stands. + * + * The winning descriptor, its hit angle and its packed low code half are + * written to @p st (@c cmd / @c angle / @c code) even when the gate rejected + * the pick (then @c cmd is NULL). + * + * @param glyph Object glyph header owning the candidate @c entries[]. + * @param st Sprite receiving the pick. + * @param key Grid-cell key the cache entries must have been resolved for. + * @param end Descriptor-table end bound, passed through to the hit test. + * @return 1 when a descriptor was picked and passed the visibility gate, else 0. + */ +static s32 func_800A2920(GlyphHeader *glyph, WorldSprite *st, s16 key, CmdDesc *end) { + FeaEntry40C0 *e; + int ok; + s32 code; + CmdDesc *g; + FeaEntry40C0 *slot; + s32 found; + CmdDesc *best; + s32 bestAngle; + s32 bestMetric; + s32 mode; + s32 prevAngle; + s32 packed; + s16 want; + s32 metric; + AngleSlot res1; + AngleSlot res2; + VECTOR *point; + + point = WORLD_PROBE_POINT; + found = 0; + best = NULL; + bestAngle = 0x7FFFFFFF; + mode = D_800C4D38; + prevAngle = st->angle; + want = key; + /* regalloc: `ok` must be a real assignment — folding the test into the + `if` costs the frame layout — and `found = 1` must precede `best = ...` + in both arms or best/prevAngle swap s5/s6. The (s16) casts on the gate's + mode argument are load-bearing: the original truncates the dispatch + code at each call (the sll/sra pair). */ + for (e = D_800D24A8; e < &D_800D24A8[12]; e++) { + ok = e->val != NULL; + if (!ok) { + continue; + } + if (want != e->hval) { + continue; + } + if (!func_800BF024(e->val, point, &res1, end)) { + continue; + } + if (mode == 0x30) { + if (!(e->val->type >= 0x1E && e->val->type < 0x23)) { + if (D_800C9870.word > 0) { + continue; + } + } + } + if (found == 0) { + found = -1; + } + if (mode == 0x30 || mode == 0x32) { + code = e->val->type | e->val->flag << 8 | e->val->param << 16; + if (res1.word < bestAngle) { + packed = code; + bestAngle = res1.word; + if (func_800A45D8(code, (s16)mode)) { + found = 1; + best = e->val; + } + } + } else { + packed = e->val->type | e->val->flag << 8 | e->val->param << 16; + bestAngle = res1.word; + if (func_800A45D8(packed, (s16)mode)) { + found = 1; + best = e->val; + } + break; + } + } + + bestMetric = 0x7FFFFFFF; + if (found == 0) { + g = glyph->entries; + for (; g < &glyph->entries[glyph->count]; g++) { + if (!func_800BF024(g, point, &res2, end)) { + continue; + } + if (mode == 0x32 + || (mode == 0x30 && ((g->type >= 0x1E && g->type < 0x23) || D_800C9868.z <= 0)) + || g->type < 6 + || D_800C4D64->type < 6 + || (mode != 0x30 && mode != 0x32 + && (res2.word - prevAngle > 0 ? res2.word - prevAngle < 0xC8 + : prevAngle - res2.word < 0xC8))) { + code = g->type | g->flag << 8 | g->param << 16; + if (mode != 0x30 && mode != 0x32) { + metric = res2.word - prevAngle; + if (metric <= 0) { + metric = prevAngle - res2.word; + } + } else { + metric = res2.word; + } + if (metric < bestMetric) { + found++; + bestMetric = metric; + best = g; + packed = code; + bestAngle = res2.word; + } + } + } + + if (found != 0) { + if (best != NULL) { + for (slot = &D_800D24A8[10]; !(slot < D_800D24A8); slot--) { + slot[1] = *slot; + } + slot++; + slot->val = best; + slot->hval = key; + } + if (!func_800A45D8(packed, (s16)mode)) { + best = NULL; + found = 0; + } + } + } + st->code = packed; + st->angle = bestAngle; + st->cmd = best; + return found > 0; +} + +/** + * @brief Probe up to 8 rotations for a free placement of a 5-sprite fan + * around the camera, and report where it landed. + * + * Gate: the active command descriptor @c D_800C4D64 must enable the current + * dispatch code @c D_800C4D38 (bit chosen by code range 0x20..0x28 / 0x84, + * 0x30, 0x31, 0x32; everything else passes; @c D_800C4D20 == 0 force-passes + * the byte gates). The fan size is 0x40 for codes 0x30 / 0x20..0x28 / 0x84, + * 0x100 for 0x32, else 0x20. + * + * Each pass rotates the probe angle @c yang.vy by +0x200 (starting at + * @c angles->vy + 0x400), transforms @c ang by it, and places sprite 0 at the + * camera @c D_800C9868 plus that offset with sprites 1..4 spread by + * @c size in +-X / +-Z. Every sprite is projected with @c worldPosToCell and + * tagged @c WORLD_SPRITE_PLACED. Any glyph found under a sprite re-runs the command + * gates for that glyph and checks its angle word against the camera angle + * @c D_800C97F4 (within 0xC8); a failed gate abandons the whole pass (not + * just that sprite). Glyph hits overwrite the sprite's angle fields, and a + * hit on sprite 0 records the full angle word for the caller. + * + * The second phase re-places the fan from the camera-relative position + * (@c func_800BC544) spun by the scene angle bias (@c func_800B0010 / + * @c func_800BC5E0 / @c func_800B5ADC spread points), then asks + * @c func_800B21EC to accept the record: negative means placed — the probe + * writes sprite 0's position (Y = the recorded angle word) to @p hitPos and + * returns 1. Otherwise the accept code is written to @p hitCell and the next + * pass runs. + * + * @note Dead code as far as static analysis shows: the only callers are + * @c func_8009C294 and the asm @c func_800B4AA0, and neither is referenced by + * any jal, address word, or C call site in the executable or any overlay. + * Together with @c D_800C9778 and several functions only @c func_800B4AA0 + * calls, they form an unreferenced island — likely a feature dropped before + * ship. Dispatch from runtime-loaded data cannot be fully ruled out. + * + * @param code Command/dispatch code being placed (same domain as + * @c D_800C4D38). + * @param angZ Z rotation for the probe offset vector. + * @param angles Base angles; @c angles->vy seeds the probe rotation. + * @param hitPos Out: sprite-0 world position on success (may be NULL). + * @param arg4 Unused. + * @param hitCell Out: last non-negative accept code (may be NULL). + * @return 1 when a placement was accepted, else 0 after 8 passes. + */ +s32 func_800A2D50(s32 code, s32 angZ, SVECTOR *angles, VECTOR *hitPos, s32 arg4, s32 *hitCell) { + WorldSpriteRec rec; + s32 camY; + s32 two; + u16 angBase; + s32 d; + SVECTOR ang; + SVECTOR yang; + VECTOR camPos; + MATRIX m; + VECTOR xf; + MATRIX m2; + VECTOR xf2; + SVECTOR pts[WORLD_FAN_SPRITES - 1]; + SVECTOR spin; + AngleSlot slot; + s32 pass; + s32 saved; + SVECTOR *pang; + MATRIX *pm; + WorldSprite *base; + WorldSprite *e; + WorldSprite *w; + WorldSprite *p; + s32 size; + s32 i; + s32 j; + s32 best; + s32 ctx; + s32 ret; + u32 packed; + s32 ok; + u16 mode; + s16 kind; + u16 op; + u16 gmode; + u16 ang16; + + func_80047CE4(&ang, 0, 8); + ang.vz = angZ; + func_80047CE4(&yang, 0, 8); + + packed = D_800C4D64->type | (D_800C4D64->flag << 8) | (D_800C4D64->param << 16); + mode = D_800C4D38; + if (D_800C4D20 == 0) { + packed |= 0xFFFFFF00; + } + if (((u16)(mode - 0x20) < 9) || ((s16)mode == 0x84)) { + ok = (packed >> 16) & CMDPAR_MODE_20_28; + } else if ((s16)mode == 0x30) { + ok = (packed >> 16) & CMDPAR_MODE_30; + } else if ((s16)mode == 0x31) { + ok = (packed >> 16) & CMDPAR_MODE_31; + } else if ((s16)mode == 0x32) { + ok = (packed >> 8) & CMDFLAG_MODE_32; + } else { + ok = 1; + } + if (ok == 0) { + return 0; + } + + if (code != 0x32) { + size = 0x40; + if ((code != 0x30) && ((u32)(code - 0x20) >= 9) && (code != 0x84)) { + size = 0x20; + } + } else { + size = 0x100; + } + + pass = 0; + pm = &m; + yang.vy = angles->vy + 0x400; + + do { + best = -1; + /* base and pang are the loop's two single-set invariants: written here + * so loop.c hoists them in this order, which is what fills the size + * dispatch's delay slots with the sprite base address. */ + base = rec.sprite; + pang = ∠ + RotMatrix(&yang, pm); + gte_SetRotMatrix(pm); + m.t[2] = 0; + m.t[1] = 0; + m.t[0] = 0; + gte_SetTransMatrix(pm); + e = base; + gte_ldv0(pang); + gte_mvmva(1, 0, 0, 0, 0); + gte_stlvnl(&xf); + + for (j = 0; j < WORLD_FAN_SPRITES; j++, e++) { + /* Plain 2, not WORLD_SPRITE_PLACED: this same variable is the + index compared below, and holding it in a callee-saved + register is what lets the flag store share the constant. */ + two = 2; + e->pos.vx = D_800C9868.x + xf.vx; + e->pos.vy = D_800C9868.z + xf.vy; + camY = D_800C9868.y; + e->pos.vz = xf.vz - camY; + if (j != 0) { + if (j == 1) { + e->pos.vx -= size; + } else if (j == two) { + e->pos.vz += size; + } else if (j == 3) { + e->pos.vx += size; + } else { + e->pos.vz -= size; + } + } + e->cellId = worldPosToCell(&e->pos, &e->cell); + e->flag = WORLD_SPRITE_PLACED; + e->unk28 = yang.vy; + } + + kind = code; + for (p = rec.sprite; p < &rec.sprite[WORLD_FAN_SPRITES]; p++) { + /* the sprite's pos + cell pair coincides with the GlyphQuery layout */ + CmdDesc *g = glyphAt((GlyphQuery *)p, &slot); + if (g != NULL) { + packed = g->type | (g->flag << 8) | (g->param << 16); + op = code; + if (D_800C4D20 == 0) { + ok = 1; + } else if ((op < 0xA) || (kind == 0x80)) { + ok = (packed >> 16) & CMDPAR_VIS_00_09; + } else if (((u16)(code - 0x20) < 9) || (kind == 0x84)) { + ok = (packed >> 16) & CMDPAR_VIS_20_28; + } else if (kind == 0x30) { + ok = (packed >> 16) & CMDPAR_VIS_30; + } else if (kind == 0x31) { + ok = (packed >> 16) & CMDPAR_VIS_31; + } else { + ok = 1; + } + if (ok == 0) { + goto nextPass; /* a failed gate abandons the whole pass */ + } + gmode = D_800C4D38; + if (D_800C4D20 == 0) { + packed |= 0xFFFFFF00; + } + if (((u16)(gmode - 0x20) < 9) || ((s16)gmode == 0x84)) { + ok = (packed >> 16) & CMDPAR_MODE_20_28; + } else if ((s16)gmode == 0x30) { + ok = (packed >> 16) & CMDPAR_MODE_30; + } else if ((s16)gmode == 0x31) { + ok = (packed >> 16) & CMDPAR_MODE_31; + } else if ((s16)gmode == 0x32) { + ok = (packed >> 8) & CMDFLAG_MODE_32; + } else { + ok = 1; + } + if (ok == 0) { + goto nextPass; + } + d = slot.word - D_800C97F4; + if (d > 0) { + ok = d < 0xC8; + } else { + ok = (D_800C97F4 - (s32)slot.word) < 0xC8; + } + if (ok == 0) { + goto nextPass; + } + ang16 = slot.half; + p->angle = ang16; + p->cell.vy = ang16; + p->pos.vy = (s16)ang16; /* sign-extended on purpose */ + if (p == base) { + saved = slot.word; + } + } + } + + func_800BC544((VECTOR *)&D_800C9868, &camPos); + camPos.vy = saved; + ctx = func_800B0010(code); + angBase = angles->vy; + RotMatrix(&yang, &m2); + gte_SetRotMatrix(&m2); + m2.t[2] = 0; + m2.t[1] = 0; + m2.t[0] = 0; + gte_SetTransMatrix(&m2); + w = base; + gte_ldv0(pang); + gte_mvmva(1, 0, 0, 0, 0); + gte_stlvnl(&xf2); + + func_800B5ADC(ctx, pts, 0, 0); + ret = func_800BC5E0(ctx); + spin.vx = 0; + spin.vz = 0; + spin.vy = angBase + ret; + RotMatrix(&spin, &m2); + gte_SetRotMatrix(&m2); + gte_SetTransVector(&xf2); + + /* Sprite 0 mixes the array and pointer spellings — this reproduces the + * original's interleaved sp-constant / register addressing, and the + * literal &rec.sprite[1] re-init (rather than w++) is what lets the + * loop below walk from a constant while w itself stays a copy of base. */ + rec.sprite[0].pos.vx = camPos.vx + xf2.vx; + w->pos.vy = camPos.vy + xf2.vy; + w->pos.vz = camPos.vz + xf2.vz; + w->cellId = worldPosToCell(&w->pos, &rec.sprite[0].cell); + w->flag = WORLD_SPRITE_PLACED; + w = &rec.sprite[1]; + + for (i = 1; i < WORLD_FAN_SPRITES; i++, w++) { + gte_ldv0(&pts[i - 1]); + gte_mvmva(1, 0, 0, 0, 0); + gte_stlvnl(&xf2); + w->pos.vx = camPos.vx + xf2.vx; + w->pos.vy = camPos.vy + xf2.vy; + w->pos.vz = camPos.vz + xf2.vz; + w->cellId = worldPosToCell(&w->pos, &w->cell); + w->flag = WORLD_SPRITE_PLACED; + } + + best = func_800B21EC(&rec, code, 0, 1); + if (best < 0) { + if (hitPos != NULL) { + *hitPos = rec.sprite[0].pos; + hitPos->vy = saved; + } + gte_SetRotMatrix(&D_800C9838); + gte_SetTransMatrix(&D_800C9838); + return 1; + } + nextPass: + yang.vy += 0x200; + if (hitCell != NULL) { + if (best >= 0) { + *hitCell = best; + } + } + pass++; + } while (pass < 8); + + gte_SetRotMatrix(&D_800C9838); + gte_SetTransMatrix(&D_800C9838); + return 0; +} + +/** + * @brief Place a five-sprite fan around @p slot and hand it to the emitter. + * + * Sprite 0 sits exactly on the slot's own position: the fan is built by + * transforming the zero vector through an identity rotation, so the offset it + * yields is (0,0,0). Sprites 1..4 are spread around it — @c func_800B5ADC + * supplies four offsets for the scene, and each is rotated by the slot's + * heading (@c vec.vy) biased by @c func_800BC5E0 before being added to the + * slot position. Every sprite gets its grid cell from @c worldPosToCell and is + * marked placed (@c flag @c = @c 2). The GTE is left holding @c D_800C9838 + * again, which is what the rest of the world renderer expects. + * + * @param kind Dispatch code; selects the scene for the spread and the bias. + * @param slot Slot whose @c position anchors the fan. + * @param angles Slot heading; only @c vy is read, as an unsigned angle. + * @param flag Passed through to @c func_800B21EC. + * @return @c func_800B21EC's result — negative when the fan was rejected. + * + * @note @c base and @c pang are single-set loop invariants written before the + * rotation so loop.c hoists them in this order; @c e is a copy of + * @c base rather than @c rec.sprite directly. @c pa carries the spread + * array into the call and is then rebound, which stops cse sharing the + * array's address with the loop's base and keeps that base out of the + * registers live across these three calls. Both idioms are the ones + * @c func_800A2D50 uses for the same fan-building sequence. + */ +s32 func_800A358C(s32 kind, SlotEntry *slot, SVECTOR *angles, s32 flag) { + WorldSpriteRec rec; + SVECTOR ang; + SVECTOR yang; + MATRIX m; + VECTOR xf; + SVECTOR pts[WORLD_FAN_SPRITES - 1]; + SVECTOR spin; + SVECTOR *pang; + WorldSprite *base; + WorldSprite *e; + SVECTOR *pa; + s32 ctx; + u16 angBase; + s32 ret; + s32 i; + + func_80047CE4(&ang, 0, sizeof(ang)); + func_80047CE4(&yang, 0, sizeof(yang)); + ctx = func_800B0010(kind); + angBase = angles->vy; + base = rec.sprite; + pang = ∠ + RotMatrix(&yang, &m); + gte_SetRotMatrix(&m); + m.t[2] = 0; + m.t[1] = 0; + m.t[0] = 0; + gte_SetTransMatrix(&m); + e = base; + gte_ldv0(pang); + gte_mvmva(1, 0, 0, 0, 0); + gte_stlvnl(&xf); + + pa = pts; + func_800B5ADC(ctx, pa, 0, 0); + pa = NULL; + ret = func_800BC5E0(ctx); + spin.vx = 0; + spin.vz = 0; + spin.vy = angBase + ret; + RotMatrix(&spin, &m); + gte_SetRotMatrix(&m); + gte_SetTransVector(&xf); -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A2D50); + e->pos.vx = slot->position.vx + xf.vx; + e->pos.vy = slot->position.vy + xf.vy; + e->pos.vz = slot->position.vz + xf.vz; + e->cellId = worldPosToCell(&e->pos, &e->cell); + e->flag = WORLD_SPRITE_PLACED; + e++; -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A358C); + for (i = 1; i < WORLD_FAN_SPRITES; i++, e++) { + gte_ldv0(&pts[i - 1]); + gte_mvmva(1, 0, 0, 0, 0); + gte_stlvnl(&xf); + e->pos.vx = slot->position.vx + xf.vx; + e->pos.vy = slot->position.vy + xf.vy; + e->pos.vz = slot->position.vz + xf.vz; + e->cellId = worldPosToCell(&e->pos, &e->cell); + e->flag = WORLD_SPRITE_PLACED; + } + + ret = func_800B21EC(&rec, kind, 0, flag); + gte_SetRotMatrix(&D_800C9838); + gte_SetTransMatrix(&D_800C9838); + return ret; +} /** * @brief Resolve the command descriptor (glyph) for the projected world query @p v. @@ -151,11 +1737,11 @@ CmdDesc *glyphAt(GlyphQuery *v, AngleSlot *out) { for (; e < &D_800D24A8[12]; e++) { if (e->val != 0 && v->buf.angle == e->hval) { - if (func_800BF024((CmdDesc *)e->val, &v->buf.proj, &res1, &hdr->entries[hdr->count])) { + if (func_800BF024(e->val, &v->buf.proj, &res1, &hdr->entries[hdr->count])) { if (out != NULL) { *out = res1; } - return (CmdDesc *)e->val; + return e->val; } } } @@ -172,7 +1758,101 @@ CmdDesc *glyphAt(GlyphQuery *v, AngleSlot *out) { return NULL; } -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A39BC); +/** + * @brief Re-place an existing sprite fan around its own anchor, then flatten it. + * + * Reads the fan's own first sprite for both inputs: @c unk28 becomes the Y angle + * of the primary rotation, and @c pos is the anchor the five sprites are spread + * around. The spread itself is built exactly as @c placeWorldSpriteFan does — + * @c func_800B5ADC supplies four offsets for the current map's scene and + * @c func_800BC5E0 a per-scene angle bias applied on top of @c D_800C977A, the + * camera-follow heading. Every sprite is re-projected with @c worldPosToCell and + * marked placed. A final pass overwrites all five heights with @p h, which is + * what settles the fan onto the terrain. + * + * @param out Fan of @c WORLD_FAN_SPRITES sprites; sprite 0 supplies the inputs. + * @param h Height forced into every sprite's @c pos.vy afterwards. + * + * @note @c pv and @c pa are the fan idiom shared with @c placeWorldSpriteFan and + * @c func_800A358C: @c pv carries the zero vector's address to @c gte_ldv0, + * and @c pa carries the spread array into the call and is then rebound, so + * cse cannot share that array's address with the loop's base and strand it + * in a register across the calls. The self-truncating @c ctx assignment is + * load-bearing too — it emits the @c sll/sra pair after the scene lookup, + * and splitting it into two variables loses the match. + */ +static void func_800A39BC(WorldSprite *out, s16 h) { + SVECTOR v; + SVECTOR rot; + SVECTOR ang; + VECTOR pos; + MATRIX m; + VECTOR xf; + SVECTOR pts[WORLD_FAN_SPRITES - 1]; + SVECTOR yang; + WorldSprite *e; + s32 ctx; + u16 angBase; + s32 ret; + SVECTOR *pa; + SVECTOR *pv; + s32 i; + s32 j; + + func_80047CE4(&v, 0, sizeof(v)); + func_80047CE4(&ang, 0, sizeof(ang)); + ang.vy = out->unk28; + rot = ang; + pos = out->pos; + ctx = func_800B0010(D_800C4D38); + angBase = D_800C977A; + ctx = (s16)ctx; + pv = &v; + RotMatrix(&rot, &m); + gte_SetRotMatrix(&m); + m.t[2] = 0; + m.t[1] = 0; + m.t[0] = 0; + gte_SetTransMatrix(&m); + gte_ldv0(pv); + gte_mvmva(1, 0, 0, 0, 0); + gte_stlvnl(&xf); + + pa = pts; + func_800B5ADC(ctx, pa, 0, 0); + pa = NULL; + ret = func_800BC5E0(ctx); + yang.vx = 0; + yang.vz = 0; + yang.vy = angBase + ret; + RotMatrix(&yang, &m); + gte_SetRotMatrix(&m); + gte_SetTransVector(&xf); + + e = out; + e->pos.vx = pos.vx + xf.vx; + e->pos.vy = pos.vy + xf.vy; + e->pos.vz = pos.vz + xf.vz; + e->cellId = worldPosToCell(&e->pos, &e->cell); + e->flag = WORLD_SPRITE_PLACED; + e++; + + for (i = 1; i < WORLD_FAN_SPRITES; i++) { + gte_ldv0(&pts[i - 1]); + gte_mvmva(1, 0, 0, 0, 0); + gte_stlvnl(&xf); + e->pos.vx = pos.vx + xf.vx; + e->pos.vy = pos.vy + xf.vy; + e->pos.vz = pos.vz + xf.vz; + e->cellId = worldPosToCell(&e->pos, &e->cell); + e->flag = WORLD_SPRITE_PLACED; + e++; + } + + for (j = 0; j < WORLD_FAN_SPRITES; j++) { + out[j].pos.vy = h; + } +} INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A3C9C); @@ -245,7 +1925,7 @@ s32 worldPosToCell(VECTOR *pos, SVECTOR *out) { * from @c arg4 + that bias whose translation is the base offset, and emits one * @ref WorldSprite per source vector at @c origin + transformedOffset. Each sprite * is projected with @c worldPosToCell (filling @c cell / @c cellId) and tagged - * @c flag = 2. The first sprite uses @p v's offset directly; the remaining four + * @c WORLD_SPRITE_PLACED. The first sprite uses @p v's offset directly; the remaining four * use the spread vectors transformed by the second matrix. * * @param out Output array of 5 @ref WorldSprite entries. @@ -255,11 +1935,11 @@ s32 worldPosToCell(VECTOR *pos, SVECTOR *out) { * @param arg4 Base Y-axis angle for the secondary (spread) rotation. * @param origin World-space origin the sprites are placed relative to. */ -void placeWorldSpriteFan(WorldSprite *out, VECTOR *v, SVECTOR *angles, s32 arg3, s32 arg4, +static void placeWorldSpriteFan(WorldSprite *out, VECTOR *v, SVECTOR *angles, s32 arg3, s32 arg4, VECTOR *origin) { MATRIX m; VECTOR xf; - SVECTOR pts[4]; + SVECTOR pts[WORLD_FAN_SPRITES - 1]; SVECTOR yang; WorldSprite *e; s32 i; @@ -290,7 +1970,7 @@ void placeWorldSpriteFan(WorldSprite *out, VECTOR *v, SVECTOR *angles, s32 arg3, e->pos.vy = origin->vy + xf.vy; e->pos.vz = origin->vz + xf.vz; e->cellId = worldPosToCell(&e->pos, &e->cell); - e->flag = 2; + e->flag = WORLD_SPRITE_PLACED; e++; for (i = 1; i < 5; i++) { @@ -301,7 +1981,7 @@ void placeWorldSpriteFan(WorldSprite *out, VECTOR *v, SVECTOR *angles, s32 arg3, e->pos.vy = origin->vy + xf.vy; e->pos.vz = origin->vz + xf.vz; e->cellId = worldPosToCell(&e->pos, &e->cell); - e->flag = 2; + e->flag = WORLD_SPRITE_PLACED; e++; } } @@ -328,10 +2008,10 @@ INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A4420); s32 func_800A45D8(u32 a, s32 b) { u16 op = (u16)b; if (D_800C4D20 == 0) return 1; - if (op < 0xA || (s16)b == 0x80) return (a >> 16) & 0x80; - if ((u16)(b - 0x20) < 9 || (s16)b == 0x84) return (a >> 16) & 0x40; - if ((s16)b == 0x30) return (a >> 16) & 0x20; - if ((s16)b == 0x31) return (a >> 16) & 0x10; + if (op < 0xA || (s16)b == 0x80) return (a >> 16) & CMDPAR_VIS_00_09; + if ((u16)(b - 0x20) < 9 || (s16)b == 0x84) return (a >> 16) & CMDPAR_VIS_20_28; + if ((s16)b == 0x30) return (a >> 16) & CMDPAR_VIS_30; + if ((s16)b == 0x31) return (a >> 16) & CMDPAR_VIS_31; return 1; } @@ -358,10 +2038,10 @@ s32 func_800A4670(u32 a, s32 b) { if (D_800C4D20 == 0) { a |= 0xFFFFFF00; } - if ((u16)(b - 0x20) < 9 || (s16)b == 0x84) return (a >> 16) & 4; - if ((s16)b == 0x30) return (a >> 16) & 2; - if ((s16)b == 0x31) return (a >> 16) & 1; - if ((s16)b == 0x32) return (a >> 8) & 0x80; + if ((u16)(b - 0x20) < 9 || (s16)b == 0x84) return (a >> 16) & CMDPAR_MODE_20_28; + if ((s16)b == 0x30) return (a >> 16) & CMDPAR_MODE_30; + if ((s16)b == 0x31) return (a >> 16) & CMDPAR_MODE_31; + if ((s16)b == 0x32) return (a >> 8) & CMDFLAG_MODE_32; return 1; } @@ -415,7 +2095,247 @@ s32 func_800A475C(s32 a, s32 b) { return d; } -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A47A4); +/** + * @brief World render callback: advance the streamer and keep the world-map + * object lists and message state in step with it. + * + * Installed by @c setupWorldRender through @c func_800C3DB0, which invokes it + * once per frame through a stored pointer. Rebuilds the viewport cell list, + * recycles finished objects, then dispatches on the streaming controller's + * tick (@c func_800C4558): + * + * - **tick != 0, countdown still running, tick != -1** — a read is in + * progress and nothing is due: hand every pending object back to the free + * list @c D_800D3318 (clearing its @c D_800D34A0 slot) once + * @c func_800A6254 reports the list idle. + * - **tick != 0 otherwise** — the countdown expired or the read failed + * (@c -1). Reset the reader, then either resend the pending message + * (@c D_800D34E8), or, when no object is pending and no message is armed, + * resend the message selected by @p D_800C4D60's code. Either way the + * countdown is rearmed to @c WORLD_STREAM_RETRY. + * - **tick == 0** — the reader is idle. Drain pending objects; else claim a + * free @c D_800C5398 slot and either stage its message or publish the slot + * pair into @c D_800C4D60; else stage the section-pointer list + * @c D_800D34F0 for the queued objects; else ask @c func_800A610C for the + * object under the cursor and publish it as a slot pair. + * + * @return 2 while either object list is non-empty, 1 when a code is armed in + * @c D_800C4D60, else 0. + * + * @note Statically unreachable in the retail build — see the dead-island note + * on @c setupWorldRender. Reproduced for byte-exactness only. + */ +s32 func_800A47A4(void) { + s32 buf[2]; + WorldObject *p; + WorldObject *nx; + u32 *dst; + s32 ret; + s32 tick; + s32 sel; + s32 selIdx; + s32 pair; + s32 which; + s32 lo; + u32 code16; + s8 code; + s32 i; + + buildViewportCellList(&D_800C9868, D_800C9EF0, &D_800C9838); + func_800A6030(&D_800D34E0); + ret = 0; + if (D_800D34E4 == NULL) { + if (D_800D34E0 == NULL) { + registerNewWorldObjects(); + } + } + tick = func_800C4558(1, buf); + D_800C53AC = tick; + + if (tick != 0) { + D_800C53B0--; + if ((D_800C53B0 <= 0) || (tick == -1)) { + func_800C4450(); + if (D_800D34E4 != NULL) { + if (D_800C4D60 == WORLD_CODE_NONE) { + while (func_800C4480(D_800D34E8, WORLD_OBJMSG_LEN, D_800D34F0) == 0) { + ; + } + func_800C4558(1, buf); + goto rearm; + } + } else { + if (D_800C4D60 == WORLD_CODE_NONE) { + goto rearm; + } + } + /* Both arms above fall through to this join, so the reload and + re-test of D_800D34E4 here are real. A `do {} while (0)` with + `break` expresses the same flow but emits loop notes that change + the delay-slot scheduling, so the label is load-bearing. */ + if (D_800D34E4 == NULL) { + code16 = D_800C4D60; + if ((code16 & 0xFF) != WORLD_SLOT_NONE) { + if ((D_800D241E & WORLD_FLAG_ALTCODE) && + (((code16 & 0xFFFF) == 0x504) || ((code16 & 0xFFFF) == 0x706))) { + code = (u8)D_800C4D60 + WORLD_CODE_ALT_BIAS; + } else { + code = (u8)D_800C4D60; + } + } else { + if ((D_800D241E & WORLD_FLAG_ALTCODE) && + (((code16 & 0xFFFF) == 0x5FF) || ((code16 & 0xFFFF) == 0x7FF))) { + code = ((code16 & 0xFFFF) >> 8) + WORLD_CODE_ALT_BIAS; + } else { + code = D_800C4D60 >> 8; + } + } + D_800D34F0[0] = WORLD_STAGE_ADDR; + D_800D34F0[1] = 0; + while (func_800C4480(D_800C9E58 + code * WORLD_MSG_LEN, WORLD_MSG_LEN, + D_800D34F0) == 0) { + ; + } + func_800C4558(1, buf); + } + rearm: + D_800C53B0 = WORLD_STREAM_RETRY; + } else { + p = D_800D34E4; + if (p != NULL) { + if (D_800C4D60 == WORLD_CODE_NONE) { + if (func_800A6254(p) == 0) { + func_800C4450(); + p = D_800D34E4; + if (p != NULL) { + do { + D_800D34A0[p->sectionIdx] = 0; + nx = D_800D34E4->next; + p->next = D_800D3318; + D_800D3318 = p; + p = nx; + D_800D34E4 = p; + } while (p != NULL); + } + } + } + } + } + } else { + if (D_800D34E4 != NULL) { + drainPendingObjects(); + } else if (D_800C4D60 != WORLD_CODE_NONE) { + if (D_800C5398[0] == WORLD_SLOT_NONE) { + sel = 0; + } else if (D_800C5398[2] == WORLD_SLOT_NONE) { + sel = 2; + } else { + sel = -1; + } + selIdx = sel & 0xFF; + if (selIdx == WORLD_SLOT_NONE) { + D_800C4D60 = WORLD_CODE_NONE; + } else if ((u8)D_800C4D60 != WORLD_SLOT_NONE) { + func_800A6188((Tim *)WORLD_STAGE_ADDR, selIdx); + if ((D_800D241E & WORLD_FLAG_ALTCODE) && + ((D_800C4D60 == 0x504) || (D_800C4D60 == 0x706))) { + code = (D_800C4D60 >> 8) + WORLD_CODE_ALT_BIAS; + } else { + code = D_800C4D60 >> 8; + } + D_800D34F0[0] = WORLD_STAGE_ADDR; + D_800D34F0[1] = 0; + while (func_800C4480(D_800C9E58 + code * WORLD_MSG_LEN, WORLD_MSG_LEN, + D_800D34F0) == 0) { + ; + } + func_800C4558(1, buf); + D_800C53B0 = WORLD_STREAM_RETRY; + D_800C4D60 |= WORLD_SLOT_NONE; + } else { + func_800A6188((Tim *)WORLD_STAGE_ADDR, sel + 1); + /* Chained so both slots take one loaded copy of the code; a + temp on its own line swaps the value and address registers. */ + D_800C5398[selIdx + 1] = pair = D_800C4D60 >> 9; + D_800C5398[selIdx] = pair; + D_800C4D60 = WORLD_CODE_NONE; + } + } else if (D_800D34E0 != NULL) { + which = func_800A5B48(); + p = D_800D34E4; + if (p != NULL) { + dst = D_800D34F0; + do { + /* the staging list is words, so the section pointer is stored raw */ + *dst = (u32)&D_800C4D5C[p->sectionIdx]; + p = p->next; + dst++; + } while (p != NULL); + *dst = 0; + p = D_800D34E4; + if (which == 0) { + D_800D34E8 = D_800C97E0 + p->key * WORLD_OBJMSG_LEN; + } else { + D_800D34E8 = D_800C97E4 + p->slot.row * WORLD_OBJMSG_LEN; + } + while (func_800C4480(D_800D34E8, WORLD_OBJMSG_LEN, D_800D34F0) == 0) { + ; + } + func_800C4558(1, buf); + D_800C53B0 = WORLD_STREAM_RETRY; + } + } else { + tick = func_800A610C(D_800CA030); + /* The cast on this side only: spelling both tests `& 0xFF` lets cse + fold them into one AND, which drops the copy the target keeps. */ + if ((tick & 0xFF) != WORLD_SLOT_NONE) { + lo = ((u8)tick) * 2; + D_800C4D60 = ((lo + 1) << 8) | lo; + } else { + D_800C4D60 = WORLD_CODE_NONE; + } + if (D_800C4D38 == 0x32) { + if (((WorldFlags *)D_800D23D8)->opParam != WORLD_OPPARAM_KEEP) { + D_800C4D60 = WORLD_CODE_NONE; + } + } + if (D_800C4D60 != WORLD_CODE_NONE) { + for (i = 0; i < WORLD_SLOT_COUNT; i++) { + if (D_800C5398[i] != WORLD_SLOT_NONE) { + if (func_800A60B4(D_800C5398[i], D_800CA030) == NULL) { + D_800C5398[i] = WORLD_SLOT_NONE; + } + } + } + if ((D_800C5398[0] == WORLD_SLOT_NONE) || (D_800C5398[2] == WORLD_SLOT_NONE)) { + if ((D_800D241E & WORLD_FLAG_ALTCODE) && + ((D_800C4D60 == 0x504) || (D_800C4D60 == 0x706))) { + code = (u8)D_800C4D60 + WORLD_CODE_ALT_BIAS; + } else { + code = (u8)D_800C4D60; + } + D_800D34F0[0] = WORLD_STAGE_ADDR; + D_800D34F0[1] = 0; + while (func_800C4480(D_800C9E58 + code * WORLD_MSG_LEN, WORLD_MSG_LEN, + D_800D34F0) == 0) { + ; + } + func_800C4558(1, buf); + D_800C53B0 = WORLD_STREAM_RETRY; + } else { + D_800C4D60 = WORLD_CODE_NONE; + } + } + } + } + + if ((D_800D34E0 != NULL) || (D_800D34E4 != NULL)) { + ret = 2; + } else if (D_800C4D60 != WORLD_CODE_NONE) { + ret = 1; + } + return ret; +} /** * @brief Build a linked list of the world-map grid cells inside the camera @@ -430,10 +2350,11 @@ INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A47A4); * (@c id = @c row*32+col), chained through @c next at a 0xC-byte stride, and * the final node's @c next is cleared to terminate the list. * - * @param arg0 Unused (the incoming register is overwritten immediately). - * @param out Output buffer; receives the 0xC-byte node list, NULL-terminated. + * @param cam Unused (the incoming register is overwritten immediately). + * @param out Output buffer; receives the 0xC-byte node list, NULL-terminated. + * @param m Unused; the sole caller passes the world matrix. */ -void buildViewportCellList(s32 arg0, WorldObject *out) { +static void buildViewportCellList(WorldPos *cam, WorldObject *out, MATRIX *m) { s32 r1, r2; s32 row, col; s32 colSpan, rowSpan; @@ -485,7 +2406,6 @@ INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A50A0); INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A568C); -void func_800A62E0(s16 val, u16 *coarse, u16 *fine); /** * @brief Initialise the world-engine subsystem's object pools. @@ -498,7 +2418,7 @@ void func_800A62E0(s16 val, u16 *coarse, u16 *fine); * @c { .next = NULL, .id = -1 }. Finally sets @c D_800C4D60 to its * sentinel value 0xFFFF and zeros @c D_800D34F0. */ -void func_800A581C(void) { +static void func_800A581C(void) { s32 i; for (i = 14; i >= 0; i--) { @@ -523,7 +2443,7 @@ void func_800A581C(void) { } D_800C4D60 = 0xFFFF; - D_800D34F0 = 0; + D_800D34F0[0] = 0; } /** @@ -597,7 +2517,7 @@ void registerNewWorldObjects(void) { * requires the @c goto over the @c found=0 — a plain @c break cannot skip * it. */ -void drainPendingObjects(void) { +static void drainPendingObjects(void) { WorldObject *node; WorldObject *entry; @@ -691,7 +2611,7 @@ void func_800A5D10(void) { * @param head Head of the linked list (may be NULL). * @return Matching WorldObject, or NULL if none found. */ -WorldObject *func_800A5D8C(s16 id, WorldObject *head) { +static WorldObject *worldObjectById(s16 id, WorldObject *head) { while (head != NULL) { if (id == head->id) return head; head = head->next; @@ -860,7 +2780,7 @@ void func_800A5FD4(s32 screenIdx) { * * @param pp Address of the head pointer for the list to filter. */ -void func_800A6030(WorldObject **pp) { +static void func_800A6030(WorldObject **pp) { WorldObject *curr; WorldObject *search; s32 found; @@ -902,7 +2822,7 @@ void func_800A6030(WorldObject **pp) { * @param head Head of the WorldObject list (may be NULL). * @return Matching WorldObject, or NULL if none found. */ -WorldObject *func_800A60B4(s32 key, WorldObject *head) { +static WorldObject *func_800A60B4(s32 key, WorldObject *head) { if (head) { WorldSection *base = D_800C4D5C; key &= 0xFF; @@ -917,7 +2837,8 @@ WorldObject *func_800A60B4(s32 key, WorldObject *head) { /** * @brief Walk a WorldObject list and return the first section-byte that - * isn't 0xFF, @c D_800C5398[0], or @c D_800C5398[2]; else 0xFF. + * isn't @c WORLD_SLOT_NONE, @c D_800C5398[0], or @c D_800C5398[2]; + * else @c WORLD_SLOT_NONE. * * For each node in @p head, reads the first byte of its world-section * (@c D_800C4D5C[sectionIdx]). A byte is "interesting" when it differs from @@ -929,7 +2850,7 @@ WorldObject *func_800A60B4(s32 key, WorldObject *head) { * it's the third byte read — CSE doesn't hoist it). * * @param head Head of the WorldObject list (may be NULL). - * @return An interesting section byte, or 0xFF if none found. + * @return An interesting section byte, or @c WORLD_SLOT_NONE if none found. */ s32 func_800A610C(WorldObject *head) { if (head) { @@ -937,13 +2858,13 @@ s32 func_800A610C(WorldObject *head) { do { WorldSection *section = &D_800C4D5C[head->sectionIdx]; u8 key = section->key; - if (key != 0xFF && firstSect != key && D_800C5398[2] != key) { + if (key != WORLD_SLOT_NONE && firstSect != key && D_800C5398[2] != key) { return key; } head = head->next; } while (head != 0); } - return 0xFF; + return WORLD_SLOT_NONE; } @@ -956,7 +2877,7 @@ s32 func_800A610C(WorldObject *head) { * * Uses @c D_800D32F0 as a scratch @c RECT for each upload. */ -void func_800A6188(Tim *tim, u8 tableIdx) { +static void func_800A6188(Tim *tim, u8 tableIdx) { u32 *img1 = (u32 *)tim->clut.data; /* The tim/img1 ++/-- pairs pin img1 into a callee-saved register so it survives the LoadImage/DrawSync calls and is reused for the second @@ -993,7 +2914,7 @@ void func_800A6188(Tim *tim, u8 tableIdx) { * @param head Head of the WorldObject chain to test (may be NULL). * @return 1 if any node's id is in the D_800C9EF0 list, else 0. */ -s32 func_800A6254(WorldObject *head) { +static s32 func_800A6254(WorldObject *head) { while (head != NULL) { if (func_800A629C(head) != 0) { return 1; @@ -1040,7 +2961,7 @@ s32 func_800A629C(WorldObject *target) { * @param coarse Output — coarse bucket (high bits of q and r). * @param fine Output — fine residual (low bits of q and r). */ -void func_800A62E0(s16 val, u16 *coarse, u16 *fine) { +static void func_800A62E0(s16 val, u16 *coarse, u16 *fine) { s32 r = val % 128; s32 q = val / 128; *coarse = (q / 4) * 32 + r / 4; @@ -1059,7 +2980,7 @@ void func_800A62E0(s16 val, u16 *coarse, u16 *fine) { * @note Purpose uncertain — looks like a subsystem-reset that releases all * active world objects into a reusable pool. */ -void func_800A6358(void) { +static void func_800A6358(void) { WorldObject *head; D_800C4D60 = 0xFFFF; head = D_800D34E4; @@ -1092,7 +3013,7 @@ void func_800A6358(void) { * For any other map id, copies from @c D_800C53D0 and @c D_800C53E4 * respectively. */ -void func_800A63F0(void) { +static void func_800A63F0(void) { s32 i; if (D_800C4D2C != 0) return; if (D_800C4D38 == 0x32) { diff --git a/src/world/we_object4.c b/src/world/we_object4.c index 6a30b75e..56e1e6af 100644 --- a/src/world/we_object4.c +++ b/src/world/we_object4.c @@ -1801,8 +1801,8 @@ static void func_800A9300(void) { * @c scaleRate, integrates @c pos from the 16-bit @c vel, and then * accelerates @c vel by the kind's own @c vel and bumps @c count. * From the second tick on, particles of kind 12 or 13 additionally drift - * with the camera: 5/6 of the per-frame camera deltas (@c D_800C9E38[0], - * @c D_800C9870 - @c D_800C974C, @c D_800C9E38[2]) is added to the position + * with the camera: 5/6 of the per-frame camera deltas (@c D_800C9E38.vx, + * @c D_800C9870.word - @c D_800C974C, @c D_800C9E38.vz) is added to the position * so the effect roughly follows the view (weather-style particles). * * @param p Particle slot to update. @@ -1825,11 +1825,11 @@ static void func_800A9CC0(Slot30 *p, KindParams *q) { if (p->count != 0) { if (p->kind == 12 || p->kind == 13) { w = &p->pos.vx; - *w += D_800C9E38[0] * 5 / 6; + *w += D_800C9E38.vx * 5 / 6; w++; - *w += (D_800C9870 - D_800C974C) * 5 / 6; + *w += (D_800C9870.word - D_800C974C) * 5 / 6; w++; - *w += D_800C9E38[2] * 5 / 6; + *w += D_800C9E38.vz * 5 / 6; } } p->vel.vx += q->vel.vx; diff --git a/src/world/we_object5.c b/src/world/we_object5.c index 5e637a9d..a42c63ac 100644 --- a/src/world/we_object5.c +++ b/src/world/we_object5.c @@ -1,6 +1,7 @@ #include "common.h" #include "world.h" #include "world/we_object5.h" +#include "world/we_object3.h" /* worldPosToCell */ #include "battle.h" #include "psxsdk/libgpu.h" #include "psxsdk/libgte.h" diff --git a/src/world/we_object7.c b/src/world/we_object7.c index 73e77af1..491febde 100644 --- a/src/world/we_object7.c +++ b/src/world/we_object7.c @@ -1,6 +1,7 @@ #include "common.h" #include "world.h" #include "world/we_object7.h" +#include "world/we_object3.h" /* func_800A5DC8 */ /* ActorRecord now lives in world.h (shared across world TUs). */ diff --git a/src/world/we_object9.c b/src/world/we_object9.c index ed308148..75c7e243 100644 --- a/src/world/we_object9.c +++ b/src/world/we_object9.c @@ -3,6 +3,7 @@ #include "gamestate.h" #include "world.h" #include "world/we_object9.h" +#include "world/we_object3.h" /* worldPosToCell */ #define SPAWN_FLAG_LIFETIME_JITTER 1 #define SPAWN_FLAG_SIZE_JITTER 2 @@ -142,9 +143,9 @@ void func_800BBD74(ParticleSource *src, s32 visRange) { s16 dx; u32 worldCam; - worldCam = D_800C97F4.word; + worldCam = D_800C97F4; localPos = src->pos; - dx = D_800C97F4.half - src->pos.pos.half; + dx = ((u16) D_800C97F4) - src->pos.pos.half; localPos.pos.word = worldCam; if ((func_8009CC3C() + (dx >> 2)) >= visRange) return; absdx = (dx >= 0) ? dx : -dx; @@ -196,9 +197,9 @@ void func_800BBD74(ParticleSource *src, s32 visRange) { * s16 dx; * u32 worldCam; * - * worldCam = D_800C97F4.word; + * worldCam = D_800C97F4; * localPos = src->pos; - * dx = D_800C97F4.half - src->pos.pos.half; + * dx = ((u16) D_800C97F4) - src->pos.pos.half; * localPos.pos.word = worldCam; * if (D_800C4D4C <= (func_8009CC3C() + (dx >> 2))) return; * absdx = (dx >= 0) ? dx : -dx; @@ -225,7 +226,7 @@ INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object9", func_800BBF0C); * @brief Spawn a mirrored particle pair (type 0x14) gated on a word-angle delta. * * Sibling of @c func_800BBD74 but with word-scale angle math instead of - * halfword: computes @c (D_800C97F4.word - src->pos.pos.word) >> 4 and + * halfword: computes @c (D_800C97F4 - src->pos.pos.word) >> 4 and * rejects the spawn when @c (rnd + delta) >= @c (D_800C4D4C / 2). When * the visibility gate opens, emits two particles via @c func_800AC0A0 * with symmetric ±0x320 angle offsets and RNG jitter on angle/height. @@ -244,11 +245,11 @@ void func_800BC09C(ParticleSource *src) { s32 threshold; u32 worldCam; - worldCam = D_800C97F4.word; + worldCam = D_800C97F4; localPos = src->pos; threshold = D_800C4D4C >> 1; localPos.pos.word = worldCam; - if ((func_8009CC3C() + ((s32)(D_800C97F4.word - src->pos.pos.word) >> 4)) >= threshold) return; + if ((func_8009CC3C() + ((s32)(D_800C97F4 - src->pos.pos.word) >> 4)) >= threshold) return; /* Left-side spawn: +0x320 angle offset + RNG jitter on angle/height */ localVel = src->vel; From 222c560242dcf084ce2440efe1dfdae56190a8e6 Mon Sep 17 00:00:00 2001 From: rengstrom Date: Mon, 31 Aug 2026 18:17:12 +0000 Subject: [PATCH 2/6] thread: name the pad-axis interface and route callers through it func_80027DB4's axis selectors become the PadAxis enum in thread.h, the parameter is typed with it, and the callers in btl_anim, fe_object1, be_object4, menucfg and we_object3 pick up the declaration from the header instead of local externs. --- include/thread.h | 14 +++++++++++++- src/btl_anim.c | 11 +++++------ src/field/fe_object1.c | 15 +++++++-------- src/menu/menucfg/menucfg.c | 5 +++-- src/tripletriad/be_object4.c | 8 ++++---- src/world/we_object3.c | 16 ++++++++-------- 6 files changed, 40 insertions(+), 29 deletions(-) diff --git a/include/thread.h b/include/thread.h index 754773ee..d8bc501c 100644 --- a/include/thread.h +++ b/include/thread.h @@ -7,7 +7,19 @@ /* Public prototypes */ extern void func_800275D4(void); /**< Refresh the raw controller buffers. */ -extern s32 func_80027DB4(s32 a, s32 b, s32 c); /**< Read an analog axis (b: 2 = X, 3 = Y). */ +/* Which axis func_80027DB4 reads. These are mutually exclusive selectors, not + flags -- the value is an index, never OR-ed or masked. X/Y are as documented + below; the other pair is named from the order callers sample them in, not from + a decoded implementation. */ +typedef enum { + PAD_AXIS_X2 = 0, + PAD_AXIS_Y2 = 1, + PAD_AXIS_X = 2, + PAD_AXIS_Y = 3 +} PadAxis; + +extern s32 func_80027DB4(s32 pad, PadAxis axis, s32 c); /**< Read one analog axis. */ + extern s32 func_80027CF8(s32 a, s32 b, s32 c); /**< Fold a recentred analog stick into d-pad bits. */ /* getAnimFrameParam (thread.c) returns u16, but consumers like be_object4.c's readPads use the diff --git a/src/btl_anim.c b/src/btl_anim.c index 9ae27bec..a0481ab4 100644 --- a/src/btl_anim.c +++ b/src/btl_anim.c @@ -4,6 +4,7 @@ #include "psxsdk/libc.h" #include "battle.h" #include "btl_anim.h" +#include "thread.h" void callCdTick(void); @@ -14,8 +15,6 @@ void func_800472E4(void); void func_800472F4(void); s32 getAnimFrameParam(s32, s32); u16 remapControllerInput(s32); -s32 func_80027DB4(s32, s32, s32); -s32 func_80027CF8(s32, s32, s32); s32 getAnimFrameStatusFlags(s32, s32); s32 func_8002CF54(s32); void decrementSfxCounter(void); @@ -2116,9 +2115,9 @@ void processBattleAnimFrames(s32 frameCount, s32 mode) { for (i = count; i >= 0; i--) { param = remapControllerInput(getAnimFrameParam(0, i) & 0xFFFF) & 0xFFFF; if ((param & 0xF000) == 0) { - val = func_80027DB4(0, 2, i); + val = func_80027DB4(0, PAD_AXIS_X, i); if (val >= 0) { - param |= func_80027CF8(0, val - 128, func_80027DB4(0, 3, i) - 128); + param |= func_80027CF8(0, val - 128, func_80027DB4(0, PAD_AXIS_Y, i) - 128); } } param |= remapControllerInput(func_80027A58(0, i) & 0xFFFF) << 16; @@ -2131,9 +2130,9 @@ void processBattleAnimFrames(s32 frameCount, s32 mode) { func_800472E4(); param = remapControllerInput(getAnimFrameParam(0, 0) & 0xFFFF) & 0xFFFF; upperBits = remapControllerInput(func_80027A58(0, 0) & 0xFFFF) << 16; - val = func_80027DB4((0, 0), 2, 0); + val = func_80027DB4((0, 0), PAD_AXIS_X, 0); if (((param & 0xF000) == 0) && (val >= 0)) { - param |= func_80027CF8(0, val - 128, func_80027DB4(0, 3, 0) - 128); + param |= func_80027CF8(0, val - 128, func_80027DB4(0, PAD_AXIS_Y, 0) - 128); } func_800472F4(); i = count; diff --git a/src/field/fe_object1.c b/src/field/fe_object1.c index 2810319b..af9613b9 100644 --- a/src/field/fe_object1.c +++ b/src/field/fe_object1.c @@ -8,6 +8,7 @@ #include "field/fe_object1.h" #include "field/fe_object1_2.h" #include "field/fe_object10.h" +#include "thread.h" /** * @brief Initialize the field engine's double-buffered draw/display envs. @@ -518,10 +519,8 @@ void func_8009912C(void) { renderAndUpdateDisplay(1); } -extern void func_800275D4(void); /* thread.c: refresh raw controller buffers */ extern s32 getAnimFrameParam(s32 slot, s32 sub); /* per-pad input-frame param (s32 view) */ extern s32 func_80027A58(s32 a, s32 b); /* per-pad newly-pressed input */ -extern s32 func_80027DB4(s32 a, s32 b, s32 c); /* read an analog axis (b: 2 = X, 3 = Y) */ extern s32 func_80030F10(s32 arg); /* map pad input word to a button mask */ /** @@ -551,8 +550,8 @@ void func_80099180(void) { D_800704A8.field_0x160 = getAnimFrameParam(1, 0); D_800704A8.field_0x168 = func_80027A58(1, 0); - if (!(D_800704A8.padHeld & 0xF000) && func_80027DB4(0, 2, 0) != -1) { - r = (s16)func_80027DB4(0, 2, 0); + if (!(D_800704A8.padHeld & 0xF000) && func_80027DB4(0, PAD_AXIS_X, 0) != -1) { + r = (s16)func_80027DB4(0, PAD_AXIS_X, 0); if (r < 0x40) { D_800704A8.padHeld |= 0x8000; if (!(D_800704A8.padHeldPrev & 0x8000)) D_800704A8.padPressed |= 0x8000; @@ -560,7 +559,7 @@ void func_80099180(void) { D_800704A8.padHeld |= 0x2000; if (!(D_800704A8.padHeldPrev & 0x2000)) D_800704A8.padPressed |= 0x2000; } - r = (s16)func_80027DB4(0, 3, 0); + r = (s16)func_80027DB4(0, PAD_AXIS_Y, 0); if (r < 0x40) { D_800704A8.padHeld |= 0x1000; if (!(D_800704A8.padHeldPrev & 0x1000)) D_800704A8.padPressed |= 0x1000; @@ -1864,9 +1863,9 @@ void func_8009BEC8(Actor *ents, s32 flags) { if (D_800704A8.unk015 == 1 || (s16)D_800704A8.dialogState == 4) { continue; } - if (func_80027DB4(0, 2, 0) != -1) { - b.vx = 0x80 - func_80027DB4(0, 2, 0); - b.vy = func_80027DB4(0, 3, 0) - 0x80; + if (func_80027DB4(0, PAD_AXIS_X, 0) != -1) { + b.vx = 0x80 - func_80027DB4(0, PAD_AXIS_X, 0); + b.vy = func_80027DB4(0, PAD_AXIS_Y, 0) - 0x80; a.vx = 0; a.vy = 0; dir = func_8009A0E8((s32 *)&a, (s32 *)&b, dist); diff --git a/src/menu/menucfg/menucfg.c b/src/menu/menucfg/menucfg.c index a2effd92..4c7f64c3 100644 --- a/src/menu/menucfg/menucfg.c +++ b/src/menu/menucfg/menucfg.c @@ -1,5 +1,6 @@ #include "common.h" #include "menu.h" +#include "thread.h" /** @brief Config menu entry point — delegates to func_801F798C. */ void func_801E5800(s32 a0) { @@ -66,7 +67,7 @@ s32 func_801E5820(CfgContext *arg0) * @brief Initialize config menu availability flags. * * Sets both config availability flags at @p a0[0x2D] and @p a0[0x2E] - * to 1, then validates them. Calls func_80027DB4(0,0,0) to check + * to 1, then validates them. Calls func_80027DB4 on PAD_AXIS_X2 to check * memory card status; if negative, clears a0[0x2E]. Then calls * isAnimActive(); if it returns 0, clears a0[0x2D]. If nonzero, * calls getBattleAnimField0B(0) and clears a0[0x2D] if that returns 0. @@ -77,7 +78,7 @@ void func_801E587C(u8 *a0) { s32 val = 1; a0[0x2E] = val; a0[0x2D] = val; - if (func_80027DB4(0, 0, 0) < 0) { + if (func_80027DB4(0, PAD_AXIS_X2, 0) < 0) { a0[0x2E] = 0; } if (isAnimActive() == 0 || getBattleAnimField0B(0) == 0) { diff --git a/src/tripletriad/be_object4.c b/src/tripletriad/be_object4.c index aeffbb49..b3f5d66f 100644 --- a/src/tripletriad/be_object4.c +++ b/src/tripletriad/be_object4.c @@ -715,9 +715,9 @@ void readPads(void) padRaw = func_80030F10(getAnimFrameParam(0, 0)); oldPad = D_801D4B20[0]; - held = func_80027DB4(0, 2, 0); + held = func_80027DB4(0, PAD_AXIS_X, 0); if (!(padRaw & 0xF000) && held >= 0) { - padRaw |= func_80027CF8(0, held - 0x80, func_80027DB4(0, 3, 0) - 0x80); + padRaw |= func_80027CF8(0, held - 0x80, func_80027DB4(0, PAD_AXIS_Y, 0) - 0x80); } D_801D4B20[0] = padRaw; D_801D4B30[0] = (padRaw ^ oldPad) & padRaw; @@ -726,9 +726,9 @@ void readPads(void) padRaw = func_80030F10(getAnimFrameParam(1, 0)); oldPad = D_801D4B20[1]; - held = func_80027DB4(1, 2, 0); + held = func_80027DB4(1, PAD_AXIS_X, 0); if (!(padRaw & 0xF000) && held >= 0) { - padRaw |= func_80027CF8(0, held - 0x80, func_80027DB4(1, 3, 0) - 0x80); + padRaw |= func_80027CF8(0, held - 0x80, func_80027DB4(1, PAD_AXIS_Y, 0) - 0x80); } D_801D4B24 = padRaw; D_801D4B34 = (padRaw ^ oldPad) & padRaw; diff --git a/src/world/we_object3.c b/src/world/we_object3.c index aa2a7a16..a13303d7 100644 --- a/src/world/we_object3.c +++ b/src/world/we_object3.c @@ -257,14 +257,14 @@ static void func_800A0388(void) { D_800D2240[6] = D_800C96D8[6]; D_800D2240[7] = D_800C96D8[7]; - D_800C96D8[0] = func_80027DB4(0, 2, 0); - D_800C96D8[1] = func_80027DB4(0, 3, 0); - D_800C96D8[2] = func_80027DB4(0, 0, 0); - D_800C96D8[3] = func_80027DB4(0, 1, 0); - D_800C96D8[4] = func_80027DB4(1, 2, 0); - D_800C96D8[5] = func_80027DB4(1, 3, 0); - D_800C96D8[6] = func_80027DB4(1, 0, 0); - D_800C96D8[7] = func_80027DB4(1, 1, 0); + D_800C96D8[0] = func_80027DB4(0, PAD_AXIS_X, 0); + D_800C96D8[1] = func_80027DB4(0, PAD_AXIS_Y, 0); + D_800C96D8[2] = func_80027DB4(0, PAD_AXIS_X2, 0); + D_800C96D8[3] = func_80027DB4(0, PAD_AXIS_Y2, 0); + D_800C96D8[4] = func_80027DB4(1, PAD_AXIS_X, 0); + D_800C96D8[5] = func_80027DB4(1, PAD_AXIS_Y, 0); + D_800C96D8[6] = func_80027DB4(1, PAD_AXIS_X2, 0); + D_800C96D8[7] = func_80027DB4(1, PAD_AXIS_Y2, 0); idx = D_800C4D04; From 6b0b8ce6a29f34d665860407c35d2693071225f5 Mon Sep 17 00:00:00 2001 From: rengstrom Date: Mon, 31 Aug 2026 18:17:12 +0000 Subject: [PATCH 3/6] menu: give menucfg an owning header and internal linkage menucfg.h holds the unit's public surface; the file-local functions go static with their private types at the top of the .c, func_801E587C gets the context type it already had, and the permuter leftovers in func_801E5820 are named. --- include/menucfg.h | 21 +++++++++ src/menu/menucfg/menucfg.c | 94 +++++++++++++++++++++++--------------- 2 files changed, 79 insertions(+), 36 deletions(-) create mode 100644 include/menucfg.h diff --git a/include/menucfg.h b/include/menucfg.h new file mode 100644 index 00000000..868c191c --- /dev/null +++ b/include/menucfg.h @@ -0,0 +1,21 @@ +/** + * @file menucfg.h + * @brief Symbols and types owned by the menucfg overlay unit. + * + * The menucfg overlay draws the config screen: the list of options, their + * availability flags and the cursor that moves between them. Availability is + * decided per entry — the memory-card option is hidden when no card responds, + * and the battle-animation option when no animation is active. + * + * The overlay is a single translation unit, so nothing it defines is public and + * this header declares no prototypes. Note that other menu overlays contain + * their own functions at these same addresses, and therefore the same + * auto-generated names: a @c func_801Exxxx seen in menugf or menupty is a + * different function, not a call into this unit. + */ +#ifndef MENUCFG_H +#define MENUCFG_H + +#include "common.h" + +#endif /* MENUCFG_H */ diff --git a/src/menu/menucfg/menucfg.c b/src/menu/menucfg/menucfg.c index 4c7f64c3..b929aa29 100644 --- a/src/menu/menucfg/menucfg.c +++ b/src/menu/menucfg/menucfg.c @@ -1,12 +1,9 @@ #include "common.h" #include "menu.h" +#include "menucfg.h" #include "thread.h" -/** @brief Config menu entry point — delegates to func_801F798C. */ -void func_801E5800(s32 a0) { - func_801F798C(a0); -} - +/** One row of the config table @c D_801E7094: an option id and its state. */ typedef struct { u8 id; u8 state; @@ -18,47 +15,73 @@ typedef struct { u8 unk07; } CfgEntry; +/** The config screen's context block. The two trailing bytes gate whether an + option is offered: @c flag_2D is cleared when no battle animation is running, + @c flag_2E when the analog-axis probe reports no pad. */ typedef struct { - u8 unk00[0x2E]; + u8 unk00[0x2D]; + u8 flag_2D; u8 flag_2E; } CfgContext; +/** Terminator id closing the config option table. */ +#define CFG_ENTRY_END 0xFF + +/** The config option table, CFG_ENTRY_END-terminated. */ extern CfgEntry D_801E7094[]; +/* All file-local; the overlay is a single translation unit and only + func_801E5800 is reached from outside it. */ +static s32 func_801E5820(CfgContext *arg0); +static void func_801E587C(CfgContext *cfg); +static void func_801E58EC(s32 a0, s32 a1); +static void func_801E5918(s32 a0, s32 a1, s32 a2); +static s32 func_801E59A0(s32 a0); +static s32 func_801E59CC(s32 a0); +static void func_801E61A0(s32 flags, void *data, s32 value, s32 x, s32 y); +static s32 func_801E67A8(s32 a0, s32 a1, s32 a2, s32 a3, s32 arg4); + +/** @brief Config menu entry point — delegates to func_801F798C. */ +void func_801E5800(s32 a0) { + func_801F798C(a0); +} + /** * @brief Count available config menu entries. * - * Iterates through the D_801E7094 config entry table (8-byte stride, - * 0xFF-terminated). Counts entries where either flag_2E is set or the - * entry's state is not 1. + * Walks the @c D_801E7094 table to its @c CFG_ENTRY_END terminator, counting + * entries that are offered: every one of them when @c flag_2E is set, otherwise + * only those whose state is not 1. * - * @param arg0 Pointer to config menu context (byte 0x2E is availability flag). + * @param arg0 Config menu context. * @return Number of available config entries. */ -s32 func_801E5820(CfgContext *arg0) +static s32 func_801E5820(CfgContext *arg0) { - int count; - int new_var; + s32 count; + s32 endId; CfgEntry *entry; s32 flag; - s32 one; + s32 stateOn; count = 0; - if (D_801E7094[0].id != 0xFF) + if (D_801E7094[0].id != CFG_ENTRY_END) { flag = arg0->flag_2E; - one = 1; - new_var = 0xFF; + stateOn = 1; + endId = CFG_ENTRY_END; + /* The cached endId/stateOn and this empty statement are load-bearing: + spelling the loop with plain literals costs the match. */ do { } while (0); entry = D_801E7094; do { - if ((flag != 0) || (entry->state != one)) + if ((flag != 0) || (entry->state != stateOn)) { count++; } entry++; } - while (entry->id != new_var); + while (entry->id != endId); } return count; } @@ -66,23 +89,22 @@ s32 func_801E5820(CfgContext *arg0) /** * @brief Initialize config menu availability flags. * - * Sets both config availability flags at @p a0[0x2D] and @p a0[0x2E] - * to 1, then validates them. Calls func_80027DB4 on PAD_AXIS_X2 to check - * memory card status; if negative, clears a0[0x2E]. Then calls - * isAnimActive(); if it returns 0, clears a0[0x2D]. If nonzero, - * calls getBattleAnimField0B(0) and clears a0[0x2D] if that returns 0. + * Sets both availability flags, then clears whichever the hardware does not + * support. @c func_80027DB4 reads an analog axis, so a negative result means no + * analog pad answered and @c flag_2E is cleared. @c flag_2D is cleared unless a + * battle animation is both active and reports a nonzero field 0x0B. * - * @param a0 Pointer to config menu context. + * @param cfg Config menu context. */ -void func_801E587C(u8 *a0) { +static void func_801E587C(CfgContext *cfg) { s32 val = 1; - a0[0x2E] = val; - a0[0x2D] = val; + cfg->flag_2E = val; + cfg->flag_2D = val; if (func_80027DB4(0, PAD_AXIS_X2, 0) < 0) { - a0[0x2E] = 0; + cfg->flag_2E = 0; } if (isAnimActive() == 0 || getBattleAnimField0B(0) == 0) { - a0[0x2D] = 0; + cfg->flag_2D = 0; } } @@ -95,7 +117,7 @@ void func_801E587C(u8 *a0) { * @param a0 Render context pointer. * @param a1 Row index (0-based). */ -void func_801E58EC(s32 a0, s32 a1) { +static void func_801E58EC(s32 a0, s32 a1) { func_801F0A34(a0, 0, 0x30, a1 * 16 + 0x24); } @@ -112,19 +134,19 @@ void func_801E58EC(s32 a0, s32 a1) { * @param a1 Config entry index. * @param a2 Raw value (divided by 64 to index the table). */ -void func_801E5918(s32 a0, s32 a1, s32 a2) { +static void func_801E5918(s32 a0, s32 a1, s32 a2) { a2 = D_801FA3C8[a2 / 64]; a2 = a2 * 150 / 4096; func_801F0A34(a0, 0, a2 + 0x5A, D_801E7094[a1].unk03 + 0x2F); } /** @brief Draw inner panel with section id 0x2 and clear flag. */ -s32 func_801E59A0(s32 a0) { +static s32 func_801E59A0(s32 a0) { return func_801F08D4(1, 2, a0, 0); } /** @brief Draw inner panel with section id 0x2 and set flag. */ -s32 func_801E59CC(s32 a0) { +static s32 func_801E59CC(s32 a0) { return func_801F08D4(1, 2, a0, 1); } @@ -148,7 +170,7 @@ extern MenuDisplayConfig g_menuDisplayCfg; * @param x X position of the panel. * @param y Y position of the panel. */ -void func_801E61A0(s32 flags, void *data, s32 value, s32 x, s32 y) +static void func_801E61A0(s32 flags, void *data, s32 value, s32 x, s32 y) { MenuDisplayConfig *s = &g_menuDisplayCfg; s32 xoff = x + 10; @@ -188,7 +210,7 @@ INCLUDE_ASM("asm/ovl/menucfg/nonmatchings/menucfg", func_801E6538); * @param a3 X position for the display panel. * @param arg4 Y position for the display panel. */ -s32 func_801E67A8(s32 a0, s32 a1, s32 a2, s32 a3, s32 arg4) { +static s32 func_801E67A8(s32 a0, s32 a1, s32 a2, s32 a3, s32 arg4) { s32 cfg = (s32)&g_menuDisplayCfg; *(u8 *)(cfg + 0x10) = 0; From 048bf5423c9dec62b4e1ed3c32a2cdd2f9eea92d Mon Sep 17 00:00:00 2001 From: rengstrom Date: Mon, 31 Aug 2026 18:17:24 +0000 Subject: [PATCH 4/6] world: decompile seven more we_object3 functions, yaw picker to zone lookup func_800A3C9C, func_800A26E8, func_800A3EE4, func_800A5B48, func_800A4420, func_800A01DC and func_800A568C, each with its review pass: WORLD_BANK_RECORDS and WORLD_FAN_SPRITES get names, load-bearing casts get their rationale, and the doc blocks stop claiming things the code cannot support. --- include/world.h | 21 +- include/world/we_object1.h | 9 +- include/world/we_object3.h | 9 +- src/world/we_object3.c | 701 ++++++++++++++++++++++++++++++++++++- 4 files changed, 710 insertions(+), 30 deletions(-) diff --git a/include/world.h b/include/world.h index 7aff8f44..f2645dc7 100644 --- a/include/world.h +++ b/include/world.h @@ -295,17 +295,24 @@ extern SlotEntry D_800DBFB8[]; /**< Battle/world slot table (stride 0 * * Documents the per-byte layout observed in callers: an opcode-param * byte at offset 0, a per-actor mode byte array starting at offset 2, - * and a bit-flag byte at offset 0x66. Use via - * @c ((WorldFlags *)D_800D23D8)->field — the global itself stays + * a region-enable halfword at 0x46 and a bit-flag byte at offset 0x66. + * Use via @c ((WorldFlags *)D_800D23D8)->field — the global itself stays * declared as @c u8[] so older callers that still index it as a byte * array keep working. + * + * @note @c actorMode was previously written as running all the way to + * 0x65; @ref func_800A568C reads a signed halfword at 0x46, so the + * array is capped there. Where it really ends is still unknown — + * nothing indexes it yet. */ typedef struct { - /* 0x00 */ u8 opParam; /**< Set from script opcode @c param. */ - /* 0x01 */ u8 pad01; - /* 0x02 */ u8 actorMode[0x64]; /**< Per-actor mode byte. */ - /* 0x66 */ u8 flags; /**< Bit flags (bits 0x20, 0x40 used). */ - /* 0x67 */ u8 pad67; + /* 0x00 */ u8 opParam; /**< Set from script opcode @c param. */ + /* 0x01 */ u8 pad01; + /* 0x02 */ u8 actorMode[0x46 - 0x02]; /**< Per-actor mode byte. */ + /* 0x46 */ s16 regionMask; /**< Bit @c i enables world region @c i. */ + /* 0x48 */ u8 pad48[0x66 - 0x48]; + /* 0x66 */ u8 flags; /**< Bit flags (bits 0x20, 0x40 used). */ + /* 0x67 */ u8 pad67; } WorldFlags; /** diff --git a/include/world/we_object1.h b/include/world/we_object1.h index cdf2d89b..31d278fd 100644 --- a/include/world/we_object1.h +++ b/include/world/we_object1.h @@ -9,9 +9,14 @@ #include "main.h" /* g_activeDrawEnv */ /* Projection scratch: worldPosToCell writes @c proj and returns @c angle. The - trailing @c pad keeps the buffer 0x20 bytes (gcc reserves the full slot). */ + trailing @c pad keeps the buffer 0x20 bytes (gcc reserves the full slot). + + @c proj is an SVECTOR in a VECTOR-sized slot: worldPosToCell declares its + out parameter SVECTOR*, and func_800A3EE4 -- which inlines that projection + -- writes the three components as halfwords. */ typedef struct { - VECTOR proj; + SVECTOR proj; + u8 pad08[8]; s16 angle; s16 pad[7]; } ProjBuf; diff --git a/include/world/we_object3.h b/include/world/we_object3.h index 628a5358..2c760ca2 100644 --- a/include/world/we_object3.h +++ b/include/world/we_object3.h @@ -37,7 +37,10 @@ extern WorldObject *D_800D34E0; extern WorldObject *D_800D34E4; extern WorldObject *D_800D2284; /**< Head of the active world-object list. */ extern WorldObject D_800C9888[]; /**< Pool the visible-cell list is built in. */ -extern u32 D_800D34A0[16]; +/** Section slots the world can hold live at once; @c D_800D34A0 marks each + one busy, and @c WorldObject.sectionIdx indexes it. */ +#define WORLD_SECTION_SLOTS 16 +extern u32 D_800D34A0[WORLD_SECTION_SLOTS]; extern u32 D_800D34F0[]; /**< Streamed-record staging buffer. */ extern WorldObject D_800D33E0[16]; extern WorldObject D_800C9EF0[16]; @@ -54,10 +57,12 @@ extern u32 *func_800A5EC4(s16 id); /* Point-in-descriptor hit test: returns nonzero and writes a result word to @p out when @p point falls inside the region of command descriptor @p cand. */ -extern s32 func_800BF024(CmdDesc *cand, VECTOR *point, AngleSlot *out, CmdDesc *end); +extern s32 func_800BF024(CmdDesc *cand, SVECTOR *point, AngleSlot *out, CmdDesc *end); /* Project a world position to a grid-cell index; optionally emit its angle triple. */ extern s32 worldPosToCell(VECTOR *pos, SVECTOR *out); +/** Called from we_object7; see the definition for what the result means. */ +extern s32 func_800A3EE4(VECTOR *tr, s16 ang, s16 z); /* Program the GTE translation vector for world-map rendering from two packed coords. */ extern void setWorldMapTransVector(s16 coord0, s16 coord1); diff --git a/src/world/we_object3.c b/src/world/we_object3.c index a13303d7..5f8924b9 100644 --- a/src/world/we_object3.c +++ b/src/world/we_object3.c @@ -4,6 +4,7 @@ #include "world/we_object1.h" #include "world/we_object3.h" #include "world/we_object4.h" +#include "world/we_object5.h" /* D_800D23D0 */ #include "world/we_object9.h" /* func_800BC5E0 */ #include "thread.h" @@ -54,8 +55,13 @@ reinterpretation of that buffer, not a type fix. */ #define WORLD_STAGE_ADDR 0x801E8000 /** GTE-scratchpad slot where the caller stages the projected probe point for - the descriptor hit tests (scratchpad RAM has no symbol in the map). */ -#define WORLD_PROBE_POINT ((VECTOR *)0x1F8002D0) + the descriptor hit tests (scratchpad RAM has no symbol in the map). It is an + SVECTOR: func_800A26E8 stages it in halfwords, and it reaches func_800BF024 + as the same type ProjBuf.proj does. */ +#define WORLD_PROBE_POINT ((SVECTOR *)0x1F8002D0) +/** Descriptor-list bound handed to the hit test while the active header's + entries are staged in scratchpad by func_800BF20C. */ +#define WORLD_STAGED_ENTRIES ((CmdDesc *)0x1F800000) /** Countdown to the next stream retry; @c D_800C53AC caches the last tick. */ extern s32 D_800C53AC; @@ -89,6 +95,7 @@ typedef struct { /** @c WorldSprite::flag states. */ #define WORLD_SPRITE_FREE 0 /**< Slot unused. */ +#define WORLD_SPRITE_PENDING 1 /**< Re-probed this pass but matched no descriptor. */ #define WORLD_SPRITE_PLACED 2 /**< Given a position this pass. */ #define WORLD_SPRITE_CLAIMED 3 /**< Matched a glyph this frame. */ @@ -124,6 +131,52 @@ typedef struct { u16 sx, sy; } ScreenXY; /* 0x04 */ +/** World cell grid: cells are 0x800 units square, 128 columns by 96 rows. The + projection biases each axis by one and a half spans so the wrap modulo of a + negative coordinate still lands inside the grid. */ +#define WORLD_CELL_SIZE 0x800 +#define WORLD_CELL_MASK (WORLD_CELL_SIZE - 1) +#define WORLD_GRID_COLS 128 +#define WORLD_GRID_ROWS 96 +#define WORLD_SPAN_X (WORLD_CELL_SIZE * WORLD_GRID_COLS) +#define WORLD_SPAN_Z (WORLD_CELL_SIZE * WORLD_GRID_ROWS) + +/** Object tile grid: @c WorldObject::id is a linear tile index, + @c row*WORLD_TILE_COLS+col. Coarser than the projection grid above and + unrelated to it — this is the grid the object lists, @c func_800A5E40 and + the zone table are keyed on. */ +#define WORLD_TILE_COLS 32 +#define WORLD_TILE_ROWS 24 + +/* ---- private to func_800A568C ------------------------------------------- + * The region and zone tables. Only this unit reads them, so they stay here + * rather than in we_object3.h. + */ + +/** Regions the zone lookup tests, enabled individually by the bits of + @c WorldFlags::regionMask. Seven both because that is the loop bound and + because @c D_800C59BC starts 0x1C bytes on, with no symbol between. */ +#define WORLD_REGIONS 7 + +/** Rectangular area of the object tile grid, given as two tile indices. A tile + is inside when its column and its row both fall between the corners. */ +typedef struct { + s16 lo; /* 0x00 — low corner, packed row*WORLD_TILE_COLS+col */ + s16 hi; /* 0x02 — high corner, packed the same way */ +} WorldRegion; /* 0x04 */ + +/** One zone-table entry: the key/row pair written onto a @c WorldObject whose + @c id matches. Searched linearly with no terminator, so every id that can + reach the search must be present. */ +typedef struct { + s16 id; /* 0x00 — WorldObject::id this entry answers for */ + u16 key; /* 0x02 — written to WorldObject::key */ + u16 row; /* 0x04 — written to WorldObject::slot.row */ +} WorldZone; /* 0x06 */ + +extern WorldRegion D_800C59A0[WORLD_REGIONS]; +extern WorldZone D_800C59BC[]; + /** Sprites in one pool record: an anchor plus the four spread around it. */ #define WORLD_FAN_SPRITES 5 @@ -132,9 +185,12 @@ typedef struct { WorldSprite sprite[WORLD_FAN_SPRITES]; } WorldSpriteRec; /* 0xDC */ -/** One of the two alternating pool banks of eight records. */ +/** Records in one pool bank. */ +#define WORLD_BANK_RECORDS 8 + +/** One of the two alternating pool banks. */ typedef struct { - WorldSpriteRec rec[8]; + WorldSpriteRec rec[WORLD_BANK_RECORDS]; } WorldSpriteBank; /* 0x6E0 */ extern SVECTOR D_800CA038; /**< Reference offset fed to the pool placer. */ @@ -179,7 +235,6 @@ extern s16 D_800C9772; /**< Receives the low half of the camera-f takes no parameters (it reads D_800D23C0), so keep the unprototyped form for the call-site ABI. */ static void func_800A1F10(); -extern s32 func_800A4420(WorldSpriteRec *rec, SVECTOR *ref, SVECTOR *out, s32 size); static WorldObject *worldObjectById(s16 id, WorldObject *head); /* All defined below; the render callback is the first caller in the file. */ static void buildViewportCellList(WorldPos *cam, WorldObject *out, MATRIX *m); @@ -188,9 +243,38 @@ static WorldObject *func_800A60B4(s32 key, WorldObject *head); static void func_800A6188(Tim *tim, u8 tableIdx); static void drainPendingObjects(void); static s32 func_800A2920(GlyphHeader *glyph, WorldSprite *st, s16 key, CmdDesc *end); -extern WorldSpriteRec *func_800A26E8(GlyphHeader *p, WorldSpriteRec *rec, s32 v); -extern s32 func_800A3C9C(WorldSpriteRec *rec, s32 mode); -extern s32 func_800A5B48(void); +static WorldSpriteRec *func_800A26E8(GlyphHeader *p, WorldSpriteRec *rec, s32 v); +/* mode is unsigned: the dispatch does unsigned range tests on it. */ +static s32 func_800A3C9C(WorldSpriteRec *rec, u32 mode); +/* Not static, and not in we_object3.h either. func_800B3AB8 in we_object7 + calls it -- that unit is still INCLUDE_ASM, so the caller shows up only in + asm/, and `static` fails the link. The prototype stays file-local because + its signature needs WorldSpriteRec, which is private to this unit; moving + it to the header is a parse error until that type is made public. */ +extern void func_800A4420(WorldSpriteRec *rec, SVECTOR *ref, SVECTOR *ang, s32 size); +/* Overlay entry point -- nothing in this tree calls it, so its caller is + outside the decompiled sources. static anyway: gcc 2.8 has no unit-at-a-time + pass, so it still emits the definition and the overlay matches. */ +static void func_800A01DC(s32 skipPresent); +extern u8 D_800C53F4[3]; /* Three colour bytes handed to func_80048DD4; + only this function reads them. */ +extern s32 D_800C9738; /* Previous frame's D_800D23D0 timestamp. */ +extern void func_800488D4(s32 a); +extern void func_80048C50(s32 a); +extern void func_80048DD4(BattleSceneCtx *ctx, s32 r, s32 g, s32 b); +extern void func_80049244(s32 *tag); +extern void func_800492B4(BattleSceneCtx *ctx); +extern void func_80049480(void *disp); +extern void func_800A7E74(BattleSceneCtx *ctx); +extern void func_800AC2B8(void); +static s32 func_800A5B48(void); +/* Writes exactly one word through its out-parameter. */ +extern s32 func_800A50A0(s32 *runs); +static void func_800A568C(void); +/* func_800A5B48 is the only reference in the tree: when non-zero it skips the + func_800A50A0 scan, admits a single node and zeroes the out-word. What sets + it, and what it means, is unknown. */ +extern s32 D_800D2238; extern s32 func_800B21EC(WorldSpriteRec *rec, s32 mode, s32 c, s32 d); /* Both below are declared in we_object10.h, which this unit cannot include: * its func_800B0010 prototype is (void) to serve a no-argument caller there, @@ -217,7 +301,66 @@ extern void func_800BF20C(CmdDesc *p, s32 kind, s32 arg); extern void func_800BFBFC(s32 kind); -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A01DC); +/** + * @brief Per-frame world setup: advance the frame clock, run the renderers, and + * flip the scene context. + * + * Polls the pad, picks the counter increment for @c D_800D2264 -- 3 only while + * the dispatch code @c D_800C4D38 is 0x32 and world-state flag 0 is not 0xC, + * otherwise 2 -- and feeds it to @c func_80042634. The frame clock is then stepped: the previous + * @c D_800D23D0 is kept in @c D_800C9738, a fresh one is taken, and the delta + * lands in @c D_800C9724, which drives the pad repeat timers. + * + * @c func_800A7B38 steps the texture-strip animations when @c D_800D2458 is + * set, @c func_800A7E74 runs on the scene context when @c D_800C9714 is (what + * it does is not yet known), and the context is then prepared for drawing. Unless @p skipPresent is set the frame + * is also cleared -- to black while world-state flag 0 is 0xC, otherwise to + * @c D_800C53F4's three bytes -- its HUD tag is submitted, and @c D_800D244C flips to the + * other @c D_800CA040 context. Either way the new context's ordering table is + * reset. + * + * @note The increment must be spelled as one short-circuit test + * (@c a != 0x32 @c || @c b == 0xC) rather than nested conditionals. With + * nesting, gcc hoists the else-constant above the inner compare as a + * default; it is then live across the compare's v0/v1 and cannot use v0, + * which shifts the value and the destination address up one register each. + * + * @param skipPresent Non-zero to skip the clear, HUD submit and context flip. + */ +static void func_800A01DC(s32 skipPresent) { + func_800A0388(); + D_800C4DBC = 0; + D_800D2264 = (D_800C4D38 != 0x32 || D_800D23D8[0] == 0xC) ? 2 : 3; + func_80048C50(0); + func_80042634(D_800D2264); + D_800C9738 = D_800D23D0; + D_800D23D0 = func_80042634(-1); + D_800C9724 = D_800D23D0 - D_800C9738; + func_800488D4(1); + + if (D_800D2458 != 0) { + func_800A7B38(); + } + if (D_800C9714 != 0) { + func_800A7E74(D_800D244C); + } + func_800AC2B8(); + func_800492B4(D_800D244C); + func_80049480(&D_800D244C->disp); + + if (skipPresent == 0) { + if (D_800D23D8[0] == 0xC) { + func_80048DD4(D_800D244C, 0, 0, 0); + } else { + func_80048DD4(D_800D244C, D_800C53F4[0], D_800C53F4[1], D_800C53F4[2]); + } + if (skipPresent == 0) { + func_80049244(&D_800D244C->primList[BSC_HUD_IDX]); + D_800D244C = (D_800D244C == &D_800CA040) ? (&D_800CA040) + 1 : &D_800CA040; + } + } + ClearOTagR((u32 *)D_800D244C->primList, 0x1000); +} /** * @brief Sample both controllers for the frame and derive the newly-pressed mask. @@ -507,9 +650,9 @@ void renderWorldMapFrame(void) { gte_SetRotMatrix(&D_800C9838); rec = D_800D2508[0].rec; - /* 16 records = both banks, the same address as D_800D32C8 below; the - loop needs it spelled this way (D_800D32C8 here moves 717). */ - if (rec < (D_800D2508[0].rec + 16)) { + /* Both banks, the same address as D_800D32C8 below; the loop needs + it spelled off D_800D2508 (D_800D32C8 here moves 717). */ + if (rec < (D_800D2508[0].rec + 2 * WORLD_BANK_RECORDS)) { do { for (sprite = rec->sprite; sprite < rec[1].sprite; sprite++) { if ((sprite->cellId == key) && (sprite->flag == WORLD_SPRITE_PLACED)) { @@ -1162,7 +1305,90 @@ static void setupWorldRenderParams(void) { SetColorMatrix(&D_800DA8B0); } -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A26E8); +/** + * @brief Finds the sprite a glyph header should claim, and re-probes the ones + * already keyed to it. + * + * Walks up to eight consecutive records, five sprites each, stopping early at + * the first record whose five sprites are all free. For every placed sprite: + * + * - if its cell key matches @p v, the sprite is re-probed. Its angle is reset + * from the source camera position, less 0x140 when the @em active + * descriptor @c D_800C4D64 -- not the sprite's own @c cmd -- has a type + * below 6. Its cell is staged into the scratchpad probe slot and, when this + * header is the active one, biased by the GTE translation vector so the + * test runs in the space the caller just set up. func_800A2920 then re-runs + * the descriptor hit test: a hit frees the sprite for re-placement, a miss + * leaves it pending. Neither outcome marks it placed; + * - otherwise the first such sprite is remembered as the fallback result. + * + * @param p Glyph header being placed. + * @param rec First of the eight records to scan. + * @param v Cell key to match, truncated to 16 bits. + * @return The first placed sprite that did @em not match @p v, or NULL. The + * caller keys on @c sprite[0], which is offset 0, so the matched + * sprite doubles as a record handle -- hence the cast on the way out. + */ +static WorldSpriteRec *func_800A26E8(GlyphHeader *p, WorldSpriteRec *rec, s32 v) { + s32 tr[3]; + WorldSprite *found; + WorldSprite *sp; + SVECTOR *probe; + CmdDesc *end; + s16 angle; + s16 key; + s32 empty; + s32 i; + + i = 0; + found = NULL; + key = v; + for (; i < WORLD_BANK_RECORDS; i++) { + for (sp = rec[i].sprite; sp < rec[i].sprite + WORLD_FAN_SPRITES; sp++) { + if (sp->flag != WORLD_SPRITE_PLACED) { + continue; + } + if (sp->cellId == key) { + angle = D_800C9868.z; + sp->angle = angle; + if (D_800C4D64->type < 6) { + sp->angle = angle - 0x140; + } + probe = WORLD_PROBE_POINT; + *probe = sp->cell; + if (D_800D24A0 == p) { + /* Assigning the staged bound here rather than as a default + before the test keeps it in its own basic block: shared + with the probe address it would cse into one `lui`, and + the original materialises the two separately. */ + end = WORLD_STAGED_ENTRIES; + gte_sttr(tr); + probe->vx += tr[0]; + probe->vy += tr[1]; + probe->vz += tr[2]; + } else { + end = &p->entries[p->count]; + } + if (func_800A2920(p, sp, key, end)) { + sp->flag = WORLD_SPRITE_FREE; + } else { + sp->flag = WORLD_SPRITE_PENDING; + } + } else if (found == NULL) { + found = sp; + } + } + empty = rec[i].sprite[0].flag == WORLD_SPRITE_FREE && + rec[i].sprite[1].flag == WORLD_SPRITE_FREE && + rec[i].sprite[2].flag == WORLD_SPRITE_FREE && + rec[i].sprite[3].flag == WORLD_SPRITE_FREE && + rec[i].sprite[4].flag == WORLD_SPRITE_FREE; + if (empty) { + break; + } + } + return (WorldSpriteRec *) found; +} /** * @brief Pick the best command descriptor for a sprite and install it. @@ -1212,7 +1438,7 @@ static s32 func_800A2920(GlyphHeader *glyph, WorldSprite *st, s16 key, CmdDesc * s32 metric; AngleSlot res1; AngleSlot res2; - VECTOR *point; + SVECTOR *point; point = WORLD_PROBE_POINT; found = 0; @@ -1854,9 +2080,209 @@ static void func_800A39BC(WorldSprite *out, s16 h) { } } -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A3C9C); +/** + * @brief Picks the world-map yaw a sprite record should be drawn at. + * + * Four groups of modes want different things from the same record. Only mode + * 0x32 writes anything back; the other three just compute a value. + * + * - the plain modes (below 0xA, 0x20..0x28, 0x80, 0x84) return the sprite's + * own angle, plus 0x140 when the command type is below 6; + * - mode 0x32, while @c D_800D23D8[0] is 0 or 0xD, clamps the camera scratch + * vector's @c vy to at least -0xE00 and to at most 0xC8 below the sprite's + * angle, writes it back and returns it; + * - mode 0x30 clamps the camera-follow reference into a window around the + * sprite -- @c angle+0x40..angle+0xC0 for types below 6, otherwise + * @c angle-0x100..angle-0x80 -- and for those higher types then takes the + * sprite's angle instead if it sits below the clamped value; + * - mode 0x31 returns @c angle+0x140 for types below 6 and the bare angle for + * most others, but for types 0x1E..0x22 it steps the follow reference four + * units towards @c angle..angle+0x80 (@c D_800C4D4C picks the direction) + * and clamps it there. + * + * Falling off the end of the chain returns an uninitialised value, exactly as + * the original does; the callers only use the result for the modes above. + * + * @param rec Sprite record whose first sprite supplies the angle and command. + * @param mode Draw mode selecting one of the four groups above. + * @return The yaw to store into every sprite of the record. + */ +static s32 func_800A3C9C(WorldSpriteRec *rec, u32 mode) { + /* Never read: the original reserves 0x40 of frame it does not touch, and the + reservation is load-bearing -- dropping it costs four instructions. */ + MATRIX unused[2]; + s16 yaw; + /* The 0x31 path needs its own local: it uses the value twice, and folding it + into `type` costs the match (98.25%). The other paths share one. */ + s32 type; + s32 spinType; + + if (mode < 0xA || mode == 0x80 || mode - 0x20 < 9 || mode == 0x84) { + s16 angle; + + type = rec->sprite[0].cmd->type; + angle = rec->sprite[0].angle; + yaw = angle; + if (type < 6) { + yaw = angle + 0x140; + } + } else if (mode == 0x32 && (D_800D23D8[0] == 0 || D_800D23D8[0] == 0xD)) { + SVECTOR *cam; + s16 v; + + cam = D_800C9770; + if (cam->vy < -0xE00) { + v = -0xE00; + } else { + if (rec->sprite[0].angle - 0xC8 < cam->vy) { + v = rec->sprite[0].angle - 0xC8; + } else { + v = cam->vy; + } + } + cam->vy = v; + /* Read back through the array, not the D_800C9772 alias: naming the alias + hides the store from cse, which then hoists this load above it. */ + yaw = D_800C9770[0].vy; + } else if (mode == 0x30) { + s16 angle; + s16 camYaw; + s16 clamped; + s16 lo; + s16 hi; + + camYaw = D_800C9870.half; + angle = rec->sprite[0].angle; + type = rec->sprite[0].cmd->type; + hi = angle - 0x80; + lo = angle - 0x100; + if (type < 6) { + hi = angle + 0xC0; + lo = angle + 0x40; + } + if (camYaw < lo) { + clamped = lo; + } else if (hi < camYaw) { + clamped = hi; + } else { + clamped = camYaw; + } + /* Clamp into a temp and assign back, so camYaw carries the result too. */ + camYaw = clamped; + + type = rec->sprite[0].cmd->type; + if (type >= 6 && rec->sprite[0].angle < camYaw) { + camYaw = rec->sprite[0].angle; + } + yaw = camYaw; + } else if (mode == 0x31) { + s16 camYaw; + s16 clamped; + s16 lo; + s16 hi; + + spinType = rec->sprite[0].cmd->type; + camYaw = D_800C9870.half; + /* The second arm range-tests 0x1E..0x22 in one sltiu. spinType itself stays + signed: typing it u32 would make the < 6 test below sltiu as well. */ + if (spinType < 6) { + camYaw = rec->sprite[0].angle + 0x140; + } else if ((u32)(spinType - 0x1E) < 5) { + lo = rec->sprite[0].angle; + hi = rec->sprite[0].angle + 0x80; + if (D_800C4D4C != 0) { + if (camYaw > lo) { + camYaw -= 4; + } + } else { + if (camYaw < hi) { + camYaw += 4; + } + } + if (camYaw < lo) { + clamped = lo; + } else if (hi < camYaw) { + clamped = hi; + } else { + clamped = camYaw; + } + camYaw = clamped; + } else { + camYaw = rec->sprite[0].angle; + } + yaw = camYaw; + } + return yaw; +} -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A3EE4); +/** + * @brief Asks whether the glyph under a probed world point wants the 0x31 draw + * mode. + * + * Builds a rotation of @p ang less 0x400 -- a quarter turn, in the 0x1000 = + * 360 degree convention the world code uses -- about Y, points a vector + * @p z units down Z through it with @p tr as the GTE translation, and projects + * the result. The projected point is turned into a grid cell -- components + * wrapped into one cell, and a cell index of column plus row times the grid + * width -- and handed to @c glyphAt, whose descriptor supplies the answer. + * + * The cell arithmetic is @c worldPosToCell written out in line; the function + * fills a @c GlyphQuery directly rather than calling it. + * + * @param tr GTE translation vector for the projection. + * @param ang Yaw of the probe direction. + * @param z Distance to probe along the rotated Z axis. + * @return 1 when @c D_800C4D20 is clear, otherwise the descriptor's + * @c CMDPAR_MODE_31 bit. + */ +s32 func_800A3EE4(VECTOR *tr, s16 ang, s16 z) { + SVECTOR rot; + SVECTOR v; + MATRIX m; + GlyphQuery q; + CmdDesc *g; + /* Unsigned: the result is extracted with a logical shift, and a signed + code makes it arithmetic (99.50%). */ + u32 code; + s32 col; + s32 row; + + /* Each vector is cleared and filled before the next one is touched: letting + the two clears run back to back keeps `ang` alive past the point where + the original has already reused its register for &m. */ + func_80047CE4(&rot, 0, sizeof(rot)); + rot.vy = ang - 0x400; + func_80047CE4(&v, 0, sizeof(v)); + v.vz = z; + RotMatrix(&rot, &m); + SetRotMatrix(&m); + gte_SetTransVector(tr); + gte_ldv0(&v); + gte_mvmva(1, 0, 0, 0, 0); + gte_stlvnl(&q.pos); + + q.buf.proj.vx = q.pos.vx & WORLD_CELL_MASK; + q.buf.proj.vy = q.pos.vy; + q.buf.proj.vz = q.pos.vz & WORLD_CELL_MASK; + if (q.buf.proj.vz != 0) { + q.buf.proj.vz -= WORLD_CELL_SIZE; + } + if (q.buf.proj.vz < -WORLD_CELL_MASK) { + q.buf.proj.vz += WORLD_CELL_SIZE; + } + + col = ((q.pos.vx + WORLD_SPAN_X + WORLD_SPAN_X / 2) % WORLD_SPAN_X) / WORLD_CELL_SIZE; + row = ((WORLD_SPAN_Z + WORLD_SPAN_Z / 2 - q.pos.vz) % WORLD_SPAN_Z) / WORLD_CELL_SIZE; + q.buf.angle = col + row * WORLD_GRID_COLS; + + g = glyphAt(&q, NULL); + code = g->type | g->flag << 8 | g->param << 16; + if (D_800C4D20 == 0) { + /* Sets every bit above the type byte, so the tested bit reads back 1. */ + code |= ~0xFF; + } + return (code >> 16) & CMDPAR_MODE_31; +} /** Clears an array of 12 entries. */ @@ -1986,7 +2412,69 @@ static void placeWorldSpriteFan(WorldSprite *out, VECTOR *v, SVECTOR *angles, s3 } } -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A4420); +/** + * @brief Lay a record's five sprites out as a cross around one projected point. + * + * Builds a rotation from @p ang, runs @p ref through it with a zero translation, + * and turns the result into a world position relative to the source camera + * @c D_800C9868 -- @c x and @c z feed @c pos.vx / @c pos.vy, while @c pos.vz is + * the transformed depth less the camera's @c y. Sprite 0 sits on that point and + * the other four are pushed @p size away along the two horizontal axes, giving + * the anchor-plus-four fan that @c WORLD_FAN_SPRITES names. Every sprite gets + * its grid cell from @c worldPosToCell, is marked placed, and carries @p ang's + * heading in @c unk28. + * + * @note Two shapes are load-bearing, both taken from the fan idiom in + * @c placeWorldSpriteFan and @c func_800A358C: the cursor is set up + * between @c gte_SetTransMatrix and @c gte_ldv0 -- after the transform it + * costs the two parameter registers -- and the camera @c y is read into a + * local before the subtraction, which orders that load ahead of the + * transformed depth. + * + * @param rec Record whose five sprites are placed. + * @param ref Offset run through the rotation to give the anchor point. + * @param ang Rotation applied to @p ref; its @c vy is stored in every sprite. + * @param size Distance the four outer sprites sit from the anchor. + */ +void func_800A4420(WorldSpriteRec *rec, SVECTOR *ref, SVECTOR *ang, s32 size) { + MATRIX m; + VECTOR xf; + WorldSprite *e; + s32 depth; + s32 i; + + RotMatrix(ang, &m); + gte_SetRotMatrix(&m); + m.t[2] = 0; + m.t[1] = 0; + m.t[0] = 0; + gte_SetTransMatrix(&m); + e = rec->sprite; + gte_ldv0(ref); + gte_mvmva(1, 0, 0, 0, 0); + gte_stlvnl(&xf); + + for (i = 0; i < WORLD_FAN_SPRITES; i++, e++) { + e->pos.vx = D_800C9868.x + xf.vx; + e->pos.vy = D_800C9868.z + xf.vy; + depth = D_800C9868.y; + e->pos.vz = xf.vz - depth; + if (i != 0) { + if (i == 1) { + e->pos.vx -= size; + } else if (i == 2) { + e->pos.vz += size; + } else if (i == 3) { + e->pos.vx += size; + } else { + e->pos.vz -= size; + } + } + e->cellId = worldPosToCell(&e->pos, &e->cell); + e->flag = WORLD_SPRITE_PLACED; + e->unk28 = ang->vy; + } +} /** * @brief Tag-based flag lookup — larger sibling of @c func_800A4670. @@ -2404,7 +2892,84 @@ static void buildViewportCellList(WorldPos *cam, WorldObject *out, MATRIX *m) { INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A50A0); -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A568C); +/** + * @brief Give each pending world object its render key and grid row, taking + * both from the zone table when the object's tile sits inside an + * enabled region. + * + * Walks the pending list at @c D_800D34E0. A node's @c id is an object-grid + * tile index — @c row*WORLD_TILE_COLS+col, the same packing + * @ref buildViewportCellList writes. By default a node keeps its own id as + * its key and gets the transposed index @c row+col*WORLD_TILE_ROWS as its + * grid row. + * + * The @c WORLD_REGIONS entries at @c D_800C59A0 are then tested in order, + * skipping any whose bit is clear in @c WorldFlags::regionMask. A region + * matches when the node's column and row both fall between its two packed + * corners. On the first match the node's id is looked up in the + * @c D_800C59BC zone table and that entry's key/row replace the defaults. + * + * @note The zone search is unbounded — an id that reaches it without being + * present in the table would run off the end. + */ +static void func_800A568C(void) { + WorldObject *node; + + node = D_800D34E0; + if (node == NULL) { + return; + } + + /* Both table pointers live in this inner scope, set up after the + empty-list test rather than with @c node, because that is where the + original computes them. */ + { + WorldFlags *flags = (WorldFlags *)D_800D23D8; + WorldRegion *regions = &D_800C59A0[0]; + + do { + s32 id = node->id; + s16 col = id - id / WORLD_TILE_COLS * WORLD_TILE_COLS; + s16 row = id / WORLD_TILE_COLS; + s32 i; + + node->key = node->id; + node->slot.row = row + col * WORLD_TILE_ROWS; + + for (i = 0; i < WORLD_REGIONS; i++) { + /* Declared here but assigned at the point of use below: + taking the table's address any earlier lets gcc hoist and + share the %hi, which costs the match. */ + WorldZone *zones; + + if ((flags->regionMask >> i) & 1) { + s32 lo = regions[i].lo; + s16 loCol = lo - lo / WORLD_TILE_COLS * WORLD_TILE_COLS; + + if (col >= loCol) { + s32 hi = regions[i].hi; + s16 hiCol = hi - hi / WORLD_TILE_COLS * WORLD_TILE_COLS; + + if (hiCol >= col && row >= lo / WORLD_TILE_COLS && + hi / WORLD_TILE_COLS >= row) { + s32 z = 0; + + while (node->id != D_800C59BC[z].id) { + z++; + } + zones = &D_800C59BC[0]; + node->key = zones[z].key; + node->slot.row = zones[z].row; + break; + } + } + } + } + + node = node->next; + } while (node != NULL); + } +} /** @@ -2567,7 +3132,105 @@ static void drainPendingObjects(void) { } while (node != 0); } -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A5B48); +/** + * @brief Give each newly visible object a section slot, evicting one if the + * table is full. + * + * Runs the zone lookup @c func_800A568C, then asks @c func_800A50A0 how many + * nodes to admit -- unless @c D_800D2238 is set, in which case exactly one is + * admitted and the out-word is zeroed. That many nodes are moved from the head + * of the active list @c D_800D34E0 onto the pending list @c D_800D34E4, each + * taking the first free entry of the @c D_800D34A0 table. + * + * When every slot is taken, a victim is found on the @c D_800CA030 active list: + * the first node whose @c id is @em not present in the @c D_800C9EF0 id list. + * The new node inherits that victim's section, and the rest of the active list + * shifts down over it, unlinking the last entry. If the walk reaches the end of + * the list without a victim, @c func_8009C528(0x6F) raises the error and the + * search restarts. + * + * @note Like @c drainPendingObjects, the match needs the id search to leave its + * result in a @c found flag set only at the two loop exits — 1 on a hit, + * 0 on exhaustion — which needs the @c goto to skip the @c found=0. A + * plain @c break sets the flag one instruction too early and costs the + * delay slot of the slot-table test. + * + * @return The word @c func_800A50A0 wrote, or 0 when it was skipped. + */ +static s32 func_800A5B48(void) { + s32 runs; + WorldObject *node; + WorldObject *p; + WorldObject *q; + s32 found; + s32 n; + s32 i; + s32 k; + + func_800A568C(); + n = 1; + if (D_800D2238 == 0) { + n = func_800A50A0(&runs); + } else { + runs = 0; + } + + node = D_800D34E0; + for (i = 0; i < n; i++) { + D_800D34E0 = node->next; + + for (k = 0; k < WORLD_SECTION_SLOTS; k++) { + if (D_800D34A0[k] == 0) { + break; + } + } + + if (k < WORLD_SECTION_SLOTS) { + D_800D34A0[k] = 1; + node->sectionIdx = k; + } else { + p = D_800CA030; + for (;;) { + q = &D_800C9EF0[0]; + while (q != NULL) { + if (p->id == q->id) { + found = 1; + goto matched; + } + q = q->next; + } + found = 0; + matched: + if (!found) { + node->sectionIdx = p->sectionIdx; + while (p->next != NULL) { + p->sectionIdx = p->next->sectionIdx; + p->id = p->next->id; + p = p->next; + } + /* drainPendingObjects grows this list in place through + D_800D33E0 with `entry->next = entry + 1`, so its nodes + are adjacent and the one before the tail is what has to + be unlinked. (D_800C9EF0 is the id list the search above + walks -- a different array.) */ + p[-1].next = NULL; + break; + } + if (p->next != NULL) { + p = p->next; + } else { + func_8009C528(0x6F); + } + } + } + + node->next = D_800D34E4; + D_800D34E4 = node; + node = D_800D34E0; + } + + return runs; +} /** From 9176f2d32855d860db0ffc3414e248209d44caf3 Mon Sep 17 00:00:00 2001 From: rengstrom Date: Mon, 31 Aug 2026 18:17:24 +0000 Subject: [PATCH 5/6] world: split the entry loop out of the leading blob and surface its callees The 4112-byte function at 0x800987D8 sat inside the [0x7C0] rodata blob, invisible to the split -- it is the overlay's main loop, and it jals every "dead island" root. Give it a unit (we_object0), pin D_800D2452 (its undefined_syms entry vanished once the func_800A84xx family decompiled), and make the fifteen callees it reaches public: prototypes move to the owning unit headers, and the stale dead-code notes they carried are corrected. Also integrates func_800A246C (prim-pool init, 100% match) with the SDK setPolyFT3 macro it revealed. Coverage 89.48% -> 91.55%; all 25 targets Match. --- Makefile | 1 + config/ff8.yaml | 1 + config/symbols.world.txt | 2 + include/psxsdk/libgpu.h | 5 +- include/world/we_object3.h | 27 ++++++ include/world/we_object4.h | 16 ++- src/world/we_object0.c | 9 ++ src/world/we_object3.c | 194 ++++++++++++++++++++++++++++++++----- src/world/we_object4.c | 89 +++++++---------- 9 files changed, 261 insertions(+), 83 deletions(-) create mode 100644 src/world/we_object0.c diff --git a/Makefile b/Makefile index 87fa1e93..69a30a7d 100644 --- a/Makefile +++ b/Makefile @@ -55,6 +55,7 @@ PSYQ43_MASPSXFLAGS := --aspsx-version=2.77 # used by PSYQ43_SRCS # Source files compiled with PsyQ 4.3 (default is PsyQ 4.1) PSYQ43_SRCS := src/snd_init.c src/snd_dma.c src/snd_voice.c src/snd_bank.c src/snd_param.c src/snd_note.c src/snd_track.c src/snd_cmd.c \ + src/world/we_object0.c \ src/world/we_object1.c src/world/we_object2.c src/world/we_object3.c \ src/world/we_object4.c src/world/we_object5.c src/world/we_object6.c \ src/world/we_object7.c src/world/we_object8.c src/world/we_object9.c \ diff --git a/config/ff8.yaml b/config/ff8.yaml index 735c58b8..5d390ff2 100644 --- a/config/ff8.yaml +++ b/config/ff8.yaml @@ -538,6 +538,7 @@ binaries: - [0x770, .rodata, we_object9] - [0x784, .rodata, we_object11] - [0x7C0, rodata] + - [0x7D8, c, we_object0] - [0x17E8, c, we_object1] - [0x4CE8, c, we_object2] - [0x81DC, c, we_object3] diff --git a/config/symbols.world.txt b/config/symbols.world.txt index c1b578f9..4b4630a8 100644 --- a/config/symbols.world.txt +++ b/config/symbols.world.txt @@ -23,6 +23,7 @@ g_activeDrawEnv = 0x8005F134; g_battleConfig = 0x80082C08; g_battleChars = 0x80078720; func_80098000 = 0x80098000; // type:func +func_800987D8 = 0x800987D8; // type:func func_800997E8 = 0x800997E8; // type:func func_80099B48 = 0x80099B48; // type:func func_80099C84 = 0x80099C84; // type:func @@ -278,3 +279,4 @@ func_800C4AE4 = 0x800C4AE4; // type:func func_800C7F34 = 0x800C7F34; // type:func g_fieldVars = 0x800562C4; g_chocoboWorld = 0x800786D8; +D_800D2452 = 0x800D2452; diff --git a/include/psxsdk/libgpu.h b/include/psxsdk/libgpu.h index 90385b82..b18295ed 100644 --- a/include/psxsdk/libgpu.h +++ b/include/psxsdk/libgpu.h @@ -91,6 +91,9 @@ typedef struct { (p)->u2 = (_u0), (p)->v2 = (_v0) + (_h), \ (p)->u3 = (_u0) + (_w), (p)->v3 = (_v0) + (_h)) +/* Initialise a textured 3-vertex polygon (len=7 words, code=0x24). */ +#define setPolyFT3(p) setlen(p, 7), setcode(p, 0x24) + /* Initialise a flat-shaded 4-vertex polygon (len=5 words, code=0x28). */ #define setPolyF4(p) setlen(p, 5), setcode(p, 0x28) @@ -100,10 +103,10 @@ typedef struct { /* Initialise a gouraud-shaded 4-vertex polygon (len=8 words, code=0x38). */ #define setPolyG4(p) setlen(p, 8), setcode(p, 0x38) -/* Initialise a gouraud-shaded textured 3-vertex polygon (len=9 words, code=0x34). */ /* Initialise a 1x1 tile primitive (len=2 words, code=0x68). */ #define setTile1(p) setlen(p, 2), setcode(p, 0x68) +/* Initialise a gouraud-shaded textured 3-vertex polygon (len=9 words, code=0x34). */ #define setPolyGT3(p) setlen(p, 9), setcode(p, 0x34) /* Initialise a gouraud-shaded textured 4-vertex polygon (len=12 words, code=0x3C). */ diff --git a/include/world/we_object3.h b/include/world/we_object3.h index 2c760ca2..5495f418 100644 --- a/include/world/we_object3.h +++ b/include/world/we_object3.h @@ -102,4 +102,31 @@ extern s32 func_800A358C(s32 kind, SlotEntry *slot, SVECTOR *angles, s32 flag); code @p b (only b's low 16 bits are examined; D_800C4D20 == 0 force-passes). */ extern s32 func_800A45D8(u32 a, s32 b); +/* The seven entry points below are called from the world entry loop + * func_800987D8 (we_object0), which is still assembly: it reaches them + * through the linker rather than this header. */ + +/* Per-frame world setup: advance the frame clock, run the renderers and flip + the scene context. */ +extern void func_800A01DC(s32 skipPresent); + +/* Lay out the primitive pools over work RAM and pre-tag every packet. */ +extern void func_800A246C(void); + +/* Initialise the world-engine subsystem's object pools and free lists. */ +extern void func_800A581C(void); + +/* Free the WorldObject list at D_800D34E4 back to the free pool. */ +extern void func_800A6358(void); + +/* Gated table swap: copy one of two source halfword tables by map id. */ +extern void func_800A63F0(void); + +/* Initialize the world's two double-buffered graphics contexts. */ +extern void initWorldDoubleBuffer(void); + +/* Program the GTE for world-map rendering: screen offset, back color, + color matrix. */ +extern void setupWorldRenderParams(void); + #endif /* WORLD_WE_OBJECT3_H */ diff --git a/include/world/we_object4.h b/include/world/we_object4.h index 88f5b2ab..f635627c 100644 --- a/include/world/we_object4.h +++ b/include/world/we_object4.h @@ -3,6 +3,7 @@ #include "common.h" #include "psxsdk/libgpu.h" +#include "world.h" /* we_object4's public surface. Everything else the unit owns is private to * src/world/we_object4.c and declared at the top of that file. The callers @@ -13,6 +14,17 @@ extern s32 D_800D2264; /**< Counter increment per update (read by w extern void func_800A7B38(void); /**< Step the texture-strip animations and upload changed frames. */ +/* Entry points called from the world entry loop func_800987D8 (we_object0). */ +extern void func_800A64DC(void); /**< Build the two worldmap strip sub-OTs. */ +extern void func_800A6A74(BattleSceneCtx *ctx); /**< Splice two of @p ctx's bone prims into the strip sub-OTs. */ +extern void func_800A6BE0(void); /**< Prime every worldmap strip pool from VRAM. */ +extern void func_800A7590(BattleSceneCtx *ctx); /**< Link the worldmap backdrop prims into the scene's OT. */ +extern void func_800A7CD0(s32 *block); /**< Load a VRAM row animation block. */ +extern void func_800A8400(void); /**< Draw the map-view HUD layer (panel, stars, gradient). */ +extern void func_800A8C1C(void); /**< Re-blend the world palette for the camera's position. */ +extern void func_800A9300(void); /**< Draw the D_800D9CB0 particle pool. */ +extern void func_800A9F54(WorldPos *pos, s32 x, s32 y); /**< Draw the world-map inset mesh at @p pos. */ + /** The camera-follow reference is written as a word but published as its low * half by we_object3, so both views need a name. */ typedef union { @@ -27,8 +39,8 @@ extern volatile CameraRef D_800C9870; /** * Spawn a kind-0xE particle in the D_800D9CB0 pool at @p pos (rotation - * zeroed, life/limit RNG-jittered). Dead code in the retail build — the - * only caller is the unreferenced ambient spawner func_800B99A4. + * zeroed, life/limit RNG-jittered). Its only caller is the ambient spawner + * func_800B99A4, which the world entry loop (func_800987D8) calls. */ extern void func_800AB2D4(VECTOR *pos); diff --git a/src/world/we_object0.c b/src/world/we_object0.c new file mode 100644 index 00000000..9b2930ee --- /dev/null +++ b/src/world/we_object0.c @@ -0,0 +1,9 @@ +#include "common.h" + +/* The world overlay's entry unit: one ~4.1KB function, called from main.s as + * the overlay's main loop. It sat inside the leading rodata blob until + * 2026-08-31, hidden from the split — which is why its callees (the render + * callback registration, the object-list machinery) once looked like dead + * islands. */ + +INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object0", func_800987D8); diff --git a/src/world/we_object3.c b/src/world/we_object3.c index 5f8924b9..6f2ff801 100644 --- a/src/world/we_object3.c +++ b/src/world/we_object3.c @@ -177,6 +177,85 @@ typedef struct { extern WorldRegion D_800C59A0[WORLD_REGIONS]; extern WorldZone D_800C59BC[]; +/* ---- private to func_800A246C ------------------------------------------- + * The GPU primitive pools. This unit is the only one that lays them out, so + * they stay here rather than in we_object3.h. The glyph renderers do allocate + * from a bank, but they are still asm and reach it through the file-local + * prototypes below; once we_object6/10/11 are decompiled these types will have + * to move to a shared header. + */ + +/** A primitive pool is a bump allocator over a run of work RAM holding packets + of one kind. @c cur is the next free packet — @c func_800BF80C loads and + stores it, so that is the field the renderers bump — and @c end is one past + the last. @c base is written here and read by nothing yet decompiled; it + holds the same address @c cur starts at, so rewinding is the obvious use. + The packets are tagged once at start-up, so allocating one is a pointer bump + and the renderers need only fill in vertices. One struct per kind, because + the packet type is what gives each pool its stride. */ +typedef struct { + POLY_GT3 *cur; /* 0x00 */ + POLY_GT3 *base; /* 0x04 */ + POLY_GT3 *end; /* 0x08 */ +} Gt3Pool; /* 0x0C */ + +typedef struct { + POLY_FT3 *cur; /* 0x00 */ + POLY_FT3 *base; /* 0x04 */ + POLY_FT3 *end; /* 0x08 */ +} Ft3Pool; /* 0x0C */ + +typedef struct { + POLY_FT4 *cur; /* 0x00 */ + POLY_FT4 *base; /* 0x04 */ + POLY_FT4 *end; /* 0x08 */ +} Ft4Pool; /* 0x0C */ + +/** The three pools of one bank, one per primitive kind. Both banks are laid out + at start-up and @c D_800C9720 is pointed at the table; picking a bank out of + it is left to the glyph renderers, which are still asm, so what drives that + choice is not yet known. (Not @c D_800C53A4 — that one indexes the sprite + record banks @c D_800D2508.) */ +typedef struct { + Gt3Pool gt3; /* 0x00 */ + Ft3Pool ft3; /* 0x0C */ + Ft4Pool ft4; /* 0x18 */ +} WorldPrimBank; /* 0x24 */ + +extern WorldPrimBank D_800C9E88[2]; /**< The two banks. No other code in the + tree names this array; everything + reaches it through @c D_800C9720. */ + +/** Work RAM the pools are carved from: one contiguous run per primitive kind, + each split between the two banks — @c BASE starts bank 0, @c MID is bank 0's + end and bank 1's base, @c TOP ends bank 1. The runs sit back to back, so + @c FT3_TOP and @c FT4_BASE are the one boundary spelled from either side, as + are @c FT4_TOP and @c GT3_BASE; the last run stops where the streamed-record + staging buffer starts. + + Addresses rather than declared arrays, for two reasons. The runs sit in + time-shared RAM (see docs/memory-map.md): from 0x801B0000 it is the CD-read + staging buffer — SPU sample uploads, TIM images, field script copy-in, and + the framebuffer snapshot during transitions — and the long GT3 run carries + on up through the fixed menu sub-overlays and the per-screen menu overlay + slots, all of which are free while the world map is the resident mode. RAM + that several subsystems take turns owning belongs to none of them and + carries no symbol, the same reason @c WORLD_STAGE_ADDR above is spelled out. + And the runs are not whole numbers of packets: only the FT3 runs divide + evenly (128 each), while GT3 gets 2457.6 and FT4 204.8, so those two end + mid-packet and the fill just stops once the cursor is no longer below + @c end, leaving the tail unused. An array declaration would have to round, + which would move the boundaries. */ +#define WORLD_PRIM_FT3_BASE ((POLY_FT3 *)0x801B2000) +#define WORLD_PRIM_FT3_MID ((POLY_FT3 *)0x801B3000) +#define WORLD_PRIM_FT3_TOP ((POLY_FT3 *)0x801B4000) +#define WORLD_PRIM_FT4_BASE ((POLY_FT4 *)0x801B4000) +#define WORLD_PRIM_FT4_MID ((POLY_FT4 *)0x801B6000) +#define WORLD_PRIM_FT4_TOP ((POLY_FT4 *)0x801B8000) +#define WORLD_PRIM_GT3_BASE ((POLY_GT3 *)0x801B8000) +#define WORLD_PRIM_GT3_MID ((POLY_GT3 *)0x801D0000) +#define WORLD_PRIM_GT3_TOP ((POLY_GT3 *)WORLD_STAGE_ADDR) + /** Sprites in one pool record: an anchor plus the four spread around it. */ #define WORLD_FAN_SPRITES 5 @@ -209,8 +288,11 @@ extern GlyphHeader *D_800C9740; /**< Glyph used when worldObjectById finds extern s32 D_800C53A4; /**< Active pool bank (0 or 1). */ extern s32 D_800C53A8; /**< Cleared when a record is placed, bumped by the fallback; the pass loop stops below 4. */ -extern s32 D_800C9720; /**< Third argument to the glyph renderers, which - are still asm. */ +extern WorldPrimBank *D_800C9720; /**< Primitive banks the glyph renderers + allocate from. @c func_800A246C is the + only writer in the tree and points it at + the table once; the readers (we_object6, + we_object7) are still asm. */ extern s32 D_800C972C; /**< Accumulated glyph-entry count. */ extern s32 D_800C96D8[WORLD_PAD_AXES]; /**< Analog axes sampled this frame; element 0 is pad 0's X, compared against 0x7F @@ -255,7 +337,7 @@ extern void func_800A4420(WorldSpriteRec *rec, SVECTOR *ref, SVECTOR /* Overlay entry point -- nothing in this tree calls it, so its caller is outside the decompiled sources. static anyway: gcc 2.8 has no unit-at-a-time pass, so it still emits the definition and the overlay matches. */ -static void func_800A01DC(s32 skipPresent); +void func_800A01DC(s32 skipPresent); extern u8 D_800C53F4[3]; /* Three colour bytes handed to func_80048DD4; only this function reads them. */ extern s32 D_800C9738; /* Previous frame's D_800D23D0 timestamp. */ @@ -287,16 +369,11 @@ static void func_800A0388(void); static void func_800A39BC(WorldSprite *out, s16 h); static void placeWorldSpriteFan(WorldSprite *out, VECTOR *v, SVECTOR *angles, s32 arg3, s32 arg4, VECTOR *origin); -static void func_800A581C(void); static s32 func_800A6254(WorldObject *head); static void func_800A62E0(s16 val, u16 *coarse, u16 *fine); -static void func_800A6358(void); -static void func_800A63F0(void); -static void initWorldDoubleBuffer(void); -static void setupWorldRenderParams(void); -extern void func_800ACDC4(GlyphHeader *p, BattleSceneCtx *ctx, s32 arg); -extern void func_800BF80C(GlyphHeader *p, s32 *ot, s32 arg); -extern void func_800BF2E8(GlyphHeader *p, s32 *ot, s32 arg); +extern void func_800ACDC4(GlyphHeader *p, BattleSceneCtx *ctx, WorldPrimBank *pools); +extern void func_800BF80C(GlyphHeader *p, s32 *ot, WorldPrimBank *pools); +extern void func_800BF2E8(GlyphHeader *p, s32 *ot, WorldPrimBank *pools); extern void func_800BF20C(CmdDesc *p, s32 kind, s32 arg); extern void func_800BFBFC(s32 kind); @@ -327,7 +404,7 @@ extern void func_800BFBFC(s32 kind); * * @param skipPresent Non-zero to skip the clear, HUD submit and context flip. */ -static void func_800A01DC(s32 skipPresent) { +void func_800A01DC(s32 skipPresent) { func_800A0388(); D_800C4DBC = 0; D_800D2264 = (D_800C4D38 != 0x32 || D_800D23D8[0] == 0xC) ? 2 : 3; @@ -1263,7 +1340,7 @@ static void func_800A1F10(void) * NTSC active area (@c y=8, @c h=224), and enables dithering. Finally installs * the first buffer as the active scene context (@c D_800D244C). */ -static void initWorldDoubleBuffer(void) { +void initWorldDoubleBuffer(void) { s32 i; SetDefDrawEnv(&(&D_800CA040)[1].drawEnv, 0, 0, D_800C97EA, D_800C97E8); @@ -1281,7 +1358,79 @@ static void initWorldDoubleBuffer(void) { D_800D244C = &D_800CA040; } -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A246C); +/** + * @brief Lay out the primitive pools over work RAM and pre-tag every packet. + * + * Carves @c WORLD_PRIM_FT3_BASE .. @c WORLD_PRIM_GT3_TOP into three pools per + * bank — one each for @c POLY_GT3, @c POLY_FT3 and @c POLY_FT4 — then walks + * every pool writing the packet length and command code into each slot. + * Because those tags never change, a renderer allocates a primitive by bumping + * the pool's @c cur and only fills in vertices. The table is published in + * @c D_800C9720 last, which is the only write to it anywhere. + * + * A pool whose @c cur already meets its @c end contributes nothing, hence the + * guard in front of each fill loop. + */ +void func_800A246C(void) { + WorldPrimBank *bank; + WorldPrimBank *pools; + s32 i; + POLY_GT3 *pg; + POLY_FT3 *pf3; + POLY_FT4 *pf4; + + D_800C9E88[0].gt3.base = WORLD_PRIM_GT3_BASE; + D_800C9E88[0].gt3.cur = WORLD_PRIM_GT3_BASE; + D_800C9E88[1].gt3.base = WORLD_PRIM_GT3_MID; + D_800C9E88[1].gt3.cur = WORLD_PRIM_GT3_MID; + D_800C9E88[0].gt3.end = WORLD_PRIM_GT3_MID; + D_800C9E88[1].gt3.end = WORLD_PRIM_GT3_TOP; + D_800C9E88[0].ft3.base = WORLD_PRIM_FT3_BASE; + D_800C9E88[0].ft3.cur = WORLD_PRIM_FT3_BASE; + D_800C9E88[1].ft3.base = WORLD_PRIM_FT3_MID; + D_800C9E88[1].ft3.cur = WORLD_PRIM_FT3_MID; + D_800C9E88[0].ft3.end = WORLD_PRIM_FT3_MID; + D_800C9E88[1].ft3.end = WORLD_PRIM_FT3_TOP; + D_800C9E88[0].ft4.base = WORLD_PRIM_FT4_BASE; + D_800C9E88[0].ft4.cur = WORLD_PRIM_FT4_BASE; + D_800C9E88[1].ft4.base = WORLD_PRIM_FT4_MID; + D_800C9E88[1].ft4.cur = WORLD_PRIM_FT4_MID; + D_800C9E88[0].ft4.end = WORLD_PRIM_FT4_MID; + D_800C9E88[1].ft4.end = WORLD_PRIM_FT4_TOP; + + for (i = 0; i < 2; i++) { + bank = &D_800C9E88[i]; + + /* Each bound is read back through the table rather than through + @c bank, so the table address is rematerialised at every loop instead + of being hoisted into one register — hoisting it costs the match. + @c pools only exists to force that; its value is never read, and + publishing through it below costs the match too. */ + pg = bank->gt3.cur; + if (pg < bank->gt3.end) { + do { + setPolyGT3(pg); + pg++; + } while (pg < (pools = D_800C9E88)[i].gt3.end); + } + pf3 = bank->ft3.cur; + if (pf3 < bank->ft3.end) { + do { + setPolyFT3(pf3); + pf3++; + } while (pf3 < (pools = D_800C9E88)[i].ft3.end); + } + pf4 = bank->ft4.cur; + if (pf4 < bank->ft4.end) { + do { + setPolyFT4(pf4); + pf4++; + } while (pf4 < (pools = D_800C9E88)[i].ft4.end); + } + } + + D_800C9720 = D_800C9E88; +} /** * @brief Program the GTE for world-map rendering: screen offset, back color, color matrix. @@ -1292,7 +1441,7 @@ INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object3", func_800A246C); * via @c SetBackColor, then copies the lighting color matrix @c D_800C5428 into * @c D_800DA8B0 and loads it via @c SetColorMatrix. */ -static void setupWorldRenderParams(void) { +void setupWorldRenderParams(void) { ResetGraph(3); SetGeomOffset((s16)D_800C97EA / 2, (s16)D_800C97E8 / 2); @@ -1576,12 +1725,9 @@ static s32 func_800A2920(GlyphHeader *glyph, WorldSprite *st, s16 key, CmdDesc * * returns 1. Otherwise the accept code is written to @p hitCell and the next * pass runs. * - * @note Dead code as far as static analysis shows: the only callers are - * @c func_8009C294 and the asm @c func_800B4AA0, and neither is referenced by - * any jal, address word, or C call site in the executable or any overlay. - * Together with @c D_800C9778 and several functions only @c func_800B4AA0 - * calls, they form an unreferenced island — likely a feature dropped before - * ship. Dispatch from runtime-loaded data cannot be fully ruled out. + * @note Reached through @c func_8009C294 and the asm @c func_800B4AA0, both + * called from the world entry loop @c func_800987D8 (we_object0) — the old + * "unreferenced island" reading predated that function's split. * * @param code Command/dispatch code being placed (same domain as * @c D_800C4D38). @@ -2983,7 +3129,7 @@ static void func_800A568C(void) { * @c { .next = NULL, .id = -1 }. Finally sets @c D_800C4D60 to its * sentinel value 0xFFFF and zeros @c D_800D34F0. */ -static void func_800A581C(void) { +void func_800A581C(void) { s32 i; for (i = 14; i >= 0; i--) { @@ -3643,7 +3789,7 @@ static void func_800A62E0(s16 val, u16 *coarse, u16 *fine) { * @note Purpose uncertain — looks like a subsystem-reset that releases all * active world objects into a reusable pool. */ -static void func_800A6358(void) { +void func_800A6358(void) { WorldObject *head; D_800C4D60 = 0xFFFF; head = D_800D34E4; @@ -3676,7 +3822,7 @@ static void func_800A6358(void) { * For any other map id, copies from @c D_800C53D0 and @c D_800C53E4 * respectively. */ -static void func_800A63F0(void) { +void func_800A63F0(void) { s32 i; if (D_800C4D2C != 0) return; if (D_800C4D38 == 0x32) { diff --git a/src/world/we_object4.c b/src/world/we_object4.c index 56e1e6af..12e24528 100644 --- a/src/world/we_object4.c +++ b/src/world/we_object4.c @@ -212,29 +212,16 @@ typedef struct { * unreferenced entirely in the retail build; each such function says so in an * @note on its own definition. */ static void func_800A688C(u16 *src, RECT *area, u16 *dst, s32 count); -static void func_800A7CD0(s32 *block); static void func_800A8024(void); static void func_800A8524(s32 scrollX, s16 topY, s32 brightness); static void func_800A8868(s32 phase, s16 y); static void func_800A8A28(s16 y); -static void func_800A9F54(WorldPos *pos, s32 x, s32 y); -static void func_800A6A74(BattleSceneCtx *ctx); static void func_800A9CC0(Slot30 *p, KindParams *q); static void func_800AAD48(WorldVtx *vtx, TriShade *shade); static void func_800AAEAC(WorldVtx *vtx, QuadShade *shade); /* Emits one mesh polygon from the assembled vertices into the current prim * slot; @c isQuad selects four vertices over three. */ static void func_800AA210(WorldTessVert *verts, s32 isQuad); -/* Draws the D_800D9CB0 particle pool; defined further down. */ -static void func_800A9300(void); -/* Re-blends the world palette for the camera's position; defined below. */ -static void func_800A8C1C(void); -/* Links the worldmap backdrop prims into the scene's OT; defined below. */ -static void func_800A7590(BattleSceneCtx *ctx); -/* Builds the two worldmap strip sub-OTs; defined below. */ -static void func_800A64DC(void); -/* Primes every worldmap strip pool from VRAM; defined below. */ -static void func_800A6BE0(void); /* Spawns a kind-11 particle at a fixed world position; defined below. */ static void func_800AB06C(void); @@ -268,10 +255,9 @@ extern s32 D_800D880C; /**< Mesh centre screen y (corner + @c MESH_CENTRE_BIAS) * The second band repeats the whole thing one VRAM row lower, into the second * pool. * - * @note Dead code in the retail build: nothing calls this and its address - * appears nowhere in world.bin. + * @note Called from the world entry loop @c func_800987D8 (we_object0). */ -static void func_800A64DC(void) { +void func_800A64DC(void) { RECT src; RECT near; RECT far; @@ -433,10 +419,9 @@ static void func_800A688C(u16 *src, RECT *area, u16 *dst, s32 count) { * * @param ctx Scene context whose @c primList holds the bone prims. * - * @note Dead code in the retail build: nothing calls this and its address - * appears nowhere in world.bin. + * @note Called from the world entry loop @c func_800987D8 (we_object0). */ -static void func_800A6A74(BattleSceneCtx *ctx) { +void func_800A6A74(BattleSceneCtx *ctx) { s32 cond; s32 i; @@ -478,12 +463,11 @@ static const VramXY D_80098120[STRIP_LOAD_COUNT] = { * Finally every @c DR_LOAD in the @c D_800D42D0 pool is pointed at its 1x1 * source pixel, so drawing one uploads a single texel. * - * @note Dead code in the retail build: nothing calls this and its address - * appears nowhere in world.bin. It is the filler for the pools that - * @ref func_800A7590 links and @ref func_800A64DC chains, both equally - * unreferenced. + * @note Called from the world entry loop @c func_800987D8 (we_object0). It + * is the filler for the pools that @ref func_800A7590 links and + * @ref func_800A64DC chains. */ -static void func_800A6BE0(void) { +void func_800A6BE0(void) { RECT rect; VramXY coords[STRIP_LOAD_COUNT]; s32 i, j, k; @@ -660,11 +644,10 @@ void func_800A735C(BattleSceneCtx *a0) { * @param ctx Scene to link into; the @c D_800CA040 sentinel selects bank 0 and * any live scene selects bank 1. * - * @note Dead code in the retail build: nothing calls this and its address - * appears nowhere in world.bin. The pools it links are filled by - * func_800A6BE0, which is equally unreferenced. + * @note Called from the world entry loop @c func_800987D8 (we_object0). + * The pools it links are filled by func_800A6BE0. */ -static void func_800A7590(BattleSceneCtx *ctx) { +void func_800A7590(BattleSceneCtx *ctx) { s32 bank; s32 extra; s32 group; @@ -788,10 +771,9 @@ void func_800A7B38(void) { * @param block Animation block (offset table at its start), or a table * whose first entry is 0 to just disable all slots. * - * @note Dead code in the retail build: nothing calls this and its address - * appears nowhere in world.bin. + * @note Called from the world entry loop @c func_800987D8 (we_object0). */ -static void func_800A7CD0(s32 *block) { +void func_800A7CD0(s32 *block) { WorldTexAnim *anim; s32 count; u16 *p; @@ -1054,10 +1036,9 @@ void func_800A8270(SVECTOR *out) { * b0..b2 are the width bytes of @c D_800DB0D0. Always finishes with * @c func_800A8A28 at the raw coordinate. * - * @note Dead code in the retail build: nothing calls this, and its address - * appears in no pointer table, so the three drawers below it — the - * map panel, the star field and the backdrop gradient — never run - * either. They have no other caller. + * @note Called from the world entry loop @c func_800987D8 (we_object0); + * the three drawers below it — the map panel, the star field and the + * backdrop gradient — run only through it. * * @note Purpose uncertain — appears to drive the world-map map-view HUD: * a map panel that wraps with the camera heading (@c func_800A8868), @@ -1128,7 +1109,7 @@ void func_800A84D0(void) { * is emitted as a @c TILE_1 on the HUD layer, and a @c DR_TPAGE closes the * layer. * - * @note Dead code: reached only from the unreferenced @ref func_800A8400. + * @note Reached only through @ref func_800A8400. * * @note Four spellings here are matching devices for gcc 2.8.0 rather than * intent. The @c nextColX temp lets the column step fill the inner @@ -1250,7 +1231,7 @@ static void func_800A8524(s32 scrollX, s16 topY, s32 brightness) { * the page at VRAM x 0x380 with the CLUT at (0x340, 0xE0), and linked * into the HUD layer @c BSC_HUD_IDX of the active scene. * - * @note Dead code: reached only from the unreferenced @ref func_800A8400. + * @note Reached only through @ref func_800A8400. * * @param phase Wrap offset from the camera heading (see above). * @param y Top edge of the panel row, in screen coordinates. @@ -1328,7 +1309,7 @@ static void func_800A8868(s32 phase, s16 y) { * The quads come from the pool @ref func_800ABC98 primed, picking the * @c D_800CA040 sentinel's pair when no battle scene is active. * - * @note Dead code: reached only from the unreferenced @ref func_800A8400. + * @note Reached only through @ref func_800A8400. * * @param y Baseline of the gradient, in screen coordinates. */ @@ -1409,11 +1390,10 @@ static void func_800A8A28(s16 y) { * @note The @c goto is load-bearing: it is how the matched build skips the * default-install block. Rewriting it as a "matched" flag costs three * instructions and no longer matches. - * @note Dead code in the retail build: nothing calls this, and its address - * appears nowhere in world.bin, so the @c D_800C4D24 early-out never - * gets the chance to matter. + * @note Called from the world entry loop @c func_800987D8 (we_object0), + * gated by the @c D_800C4D24 early-out. */ -static void func_800A8C1C(void) { +void func_800A8C1C(void) { SVECTOR near; VECTOR work; WorldZone *def; @@ -1589,12 +1569,11 @@ extern s32 func_800B01A0(s16 headingA, s16 headingB, Slot30 *slot, * The GTE rotation matrix and translation vector are saved on entry and put * back on exit, so callers see the transform they set up. * - * @note Dead code in the retail build: nothing calls this and its address - * appears nowhere in world.bin -- the whole particle/backdrop layer - * (this, @ref func_800A7590 and the pool filler func_800A6BE0) is - * unreachable, like the map-view HUD layer above. + * @note Called from the world entry loop @c func_800987D8 (we_object0), + * alongside the rest of the particle/backdrop layer + * (@ref func_800A7590 and the pool filler func_800A6BE0). */ -static void func_800A9300(void) { +void func_800A9300(void) { DVECTOR quad[4]; DVECTOR centre; SVECTOR corners[4]; @@ -1925,15 +1904,14 @@ void func_800A9ED4(void) { * assembled vertices go to @ref func_800AA210 — a quad for the first * @c MESH_QUAD_COUNT rows, a triangle for the rest. * - * @note Dead code in the retail build: nothing calls this and its address - * appears in no pointer table, so the mesh -- and @ref func_800AA210 - * below it, which only this reaches -- never runs. + * @note Called from the world entry loop @c func_800987D8 (we_object0); + * @ref func_800AA210 below is reached only through this. * * @param pos World position the mesh is centred on. * @param x Screen x of the mesh's top-left corner. * @param y Screen y of the mesh's top-left corner. */ -static void func_800A9F54(WorldPos *pos, s32 x, s32 y) { +void func_800A9F54(WorldPos *pos, s32 x, s32 y) { WorldTessVert verts[4]; BattleSceneCtx *ctx; WorldPolyGT4 *gt4; @@ -2011,8 +1989,7 @@ static void func_800A9F54(WorldPos *pos, s32 x, s32 y) { * @note The four seam interpolations read the base screen coordinate as * @c u16 on purpose -- the original loads it unsigned and only the * interpolated term is signed. - * @note Dead code in the retail build: the only caller is - * @ref func_800A9F54, which is itself unreachable. + * @note Reached only through @ref func_800A9F54. */ static void func_800AA210(WorldTessVert *verts, s32 isQuad) { WorldTessVert loHalf[4]; @@ -2452,9 +2429,9 @@ static void func_800AB06C(void) { * velocity. Always restores @c D_800C9838 as the GTE rotation and * translation matrix on exit. * - * @note Dead code in the retail build: the only caller, func_800B99A4 - * (we_object8, an ambient spawner that picks a random world cell - * and checks its terrain glyph), is itself never referenced. + * @note The only caller is func_800B99A4 (we_object8, an ambient spawner + * that picks a random world cell and checks its terrain glyph), which + * the world entry loop @c func_800987D8 calls. * * @note Three spellings below are load-bearing for the byte-exact match * (gcc 2.8.0 register allocation), not behavior: From 7be9495731b149bef5d0c6a77676dc3b51595913 Mon Sep 17 00:00:00 2001 From: rengstrom Date: Mon, 31 Aug 2026 18:17:24 +0000 Subject: [PATCH 6/6] tools: classify dead and unreachable functions in list_nonmatching Five-vector reachability: split-asm jal/%lo/%hi/.word references, a raw address-word scan of the built images, and a jal-encoded-word scan minus the split-asm jal counts (calls from unsplit regions). dead = zero inbound references; unreachable = referenced only from dead roots. --seed adds live roots by hand; --no-reach/--no-deep disable the passes. --- tools/list_nonmatching.py | 503 +++++++++++++++++++++++++++++++++++--- 1 file changed, 467 insertions(+), 36 deletions(-) diff --git a/tools/list_nonmatching.py b/tools/list_nonmatching.py index 0e760fb3..617aba77 100755 --- a/tools/list_nonmatching.py +++ b/tools/list_nonmatching.py @@ -1,84 +1,515 @@ #!/usr/bin/env python3 """List remaining nonmatching functions sorted by size. -Scans asm/**/nonmatchings/**/*.s and prints each function's size, -name, and path. Handwritten files are reported with size 0. +Discovery is by path: every ``.s`` under a ``nonmatchings/`` directory is one +unmatched function, and the filename is its name. That is the whole backlog -- +splat writes exactly one glabel per file, and every stub in ``src`` resolves to +one of these. With ``--include-matched`` the already-decompiled functions under +``matchings/`` are listed too, so you can see a whole object's shape at once. + +Do NOT go back to trusting the first line of the file. A function that owns a +jump table has its ``.rodata`` emitted *ahead* of the ``.text``, so the file +opens with the jtbl's header instead of the function's; keying discovery off +that header silently dropped all 136 such functions -- i.e. exactly the jtbl +cases, which are the expensive ones. + +The header is still read, but only for the size, and only as a hint: we look for +the ``nonmatching , 0xSIZE`` line belonging to the file's own symbol +anywhere in the file, and fall back to counting instructions when it is absent. +Handwritten functions (splat prepends ``/* Handwritten function */``) are +flagged but still listed. + +To work on one object, ``--object `` scopes the listing to that splat +object and sorts by address. Add ``--include-matched`` and you get the layout of +the original .c file: every function, matched and not, in source order. +``--by-object`` is the index for finding the name. + +A reachability column labels statically-orphaned functions: ``dead`` means +nothing references the function by any vector -- jal/%lo/%hi/.word in split +asm, raw address words in the built images, or jal-encoded words in excess of +the split-asm jal count (calls from UNSPLIT regions; this last vector is what +proved the 2026-08 world "dead islands" false). ``unreachable`` means it has +referrers, but every reference chain starts at dead code. Liveness propagates +from data-referenced functions, entry seeds (--seed to extend) and +unsplit-region callers. Caveat: disc-streamed dispatch words are invisible, so +both labels mean *statically* dead. ``--no-deep`` skips the binary scans +(build/ images required), ``--no-reach`` skips the analysis entirely. Usage: - python3 tools/list_nonmatching.py [--ascending] [--exclude PATTERN]... - [--limit N] [--offset N] [--all] + python3 tools/list_nonmatching.py [--ascending] [--limit N] [--offset N] + [--all] [--include-matched] + [--exclude-dir NAME]... [--exclude SUBSTR]... + [--handwritten | --no-handwritten] + [--color auto|always|never] + + # the game-code backlog, biggest first, no vendored SDK + python3 tools/list_nonmatching.py --exclude-dir psxsdk + + # one object's backlog, in address order + python3 tools/list_nonmatching.py --object we_object3 + + # ... and the same object as the .c file lays it out, matched (green) too + python3 tools/list_nonmatching.py --object we_object3 --include-matched + + # which objects still have work in them + python3 tools/list_nonmatching.py --by-object --exclude-dir psxsdk """ import argparse import os import re import sys +from collections import defaultdict -HEADER_RE = re.compile(r'^(?:nonmatching|Handwritten)\s+(\S+?)(?:,\s*0x([0-9A-Fa-f]+))?\s*$') +# "nonmatching func_800BD82C, 0xEC" -- may appear after a leading .rodata block. +# splat uses the same header word for matched functions, so this covers both. +HEADER_RE = re.compile( + r'^nonmatching\s+(\S+?)(?:,\s*0x([0-9A-Fa-f]+))?\s*$', re.MULTILINE) +# Reference forms that tie one function to another in splat asm. The asm tree +# carries the resolved call graph for matched and unmatched functions alike +# (matchings/*.s is the original binary's disassembly), so no C parsing is +# needed. jal covers direct calls; %lo/%hi covers address materialisation +# (jalr callbacks are invisible to jal-greps -- the registration site's %lo is +# their only textual trace); .word covers splat-resolved pointer tables. +JAL_RE = re.compile(r'\bjal\s+([A-Za-z_]\w*)') +LOHI_RE = re.compile(r'%(?:lo|hi)\(([A-Za-z_]\w*)\)') +WORD_RE = re.compile(r'^\s*\.word\s+([A-Za-z_]\w*)\s*$', re.MULTILINE) -def scan(root): - """Yield (size, name, path) for every nonmatching .s file under root. +# Live-by-construction entry points that nothing in the asm tree references +# textually (boot entry; overlay entries reached through raw address words the +# splitter left unresolved). Extend with --seed. +DEFAULT_SEEDS = ('main', 'start', '__start', 'func_80010000', 'func_801F04E8') - Handwritten files have no size in the header — they're reported as 0. - """ - for dirpath, _, filenames in os.walk(root): - if os.path.sep + 'nonmatchings' + os.path.sep not in dirpath + os.path.sep: +# Built binaries for the deep pass, mapped to the domain whose address space +# they carry. Only raw images -- .elf symtabs would count every symbol once and +# drown the signal. Overlays reuse address space, so a hit only counts inside +# the binary that actually holds that domain (plus the main exe, which stores +# overlay entry addresses in its load tables). The raw SLUS has no extension -- +# a *.bin glob misses it, so it is named explicitly. +DEEP_BINARIES = ( + ('build/SLUS_008.92', 'main'), + ('build/ovl/world/world.bin', 'world'), + ('build/ovl/battle/battle.bin', 'battle'), + ('build/ovl/battle_render/battle_render.bin', 'battle_render'), + ('build/ovl/tripletriad/tripletriad.bin', 'tripletriad'), + ('build/ovl/field_init/field_init.bin', 'field_init'), + ('build/field/field.bin', 'field'), + ('build/intro/intro.bin', 'intro'), +) + + +def deep_seeds(defs, addr_by_node, root='asm'): + """Vector 4 of the island method: little-endian address-word scan over the + raw built images. A function whose address appears as a data word is + runtime-dispatchable (callback tables, state machines) and therefore a live + root even with zero textual references. Code cannot false-positive: lui/ + addiu pairs split the address and j/jal encode target>>2, so a raw 32-bit + match is a data word. Hits only count in the binary of the function's own + domain or the main exe (overlays reuse address space).""" + import glob as _glob + import struct + words = {} # domain -> set of aligned LE words + binaries = list(DEEP_BINARIES) + for p in _glob.glob('build/ovl/*/*.ovl'): + binaries.append((p, os.path.basename(p)[:-4])) + for path, dom in binaries: + try: + with open(path, 'rb') as f: + data = f.read() + except OSError: + continue + n = len(data) // 4 + ws = set(struct.unpack('<%dI' % n, data[:n * 4])) + words.setdefault(dom, set()).update(ws) + # Overlays with unsplit entry regions (world: everything below 0x800997E8 + # is still raw bytes) make calls no text scan can see and would orphan the + # whole overlay. jal encodes its target as 0x0C000000 | (addr>>2), so scan + # the raw images for jal-encoded words and subtract the jal count already + # visible in split asm: any EXCESS is a call from unsplit code -- a live + # root. Plain subtraction keeps islands intact: an island's internal jals + # are all in split asm and cancel exactly. + import collections + word_counts = {} # domain -> Counter of raw words + for path, dom in binaries: + try: + with open(path, 'rb') as f: + data = f.read() + except OSError: continue + n = len(data) // 4 + word_counts.setdefault(dom, collections.Counter()).update( + struct.unpack('<%dI' % n, data[:n * 4])) + + asm_jals = collections.Counter() # (domain, target-name) -> jal lines + for dirpath, _dirnames, filenames in os.walk(root): for fn in filenames: if not fn.endswith('.s'): continue path = os.path.join(dirpath, fn) + dom = domain_of(path) + try: + with open(path, errors='replace') as f: + text = f.read() + except OSError: + continue + for t in JAL_RE.findall(text): + asm_jals[(dom, t)] += 1 + + seeds = set() + for node in defs: + dom, name = node + addr = addr_by_node.get(node, 0) + if not addr: + continue + if addr in words.get(dom, ()) or addr in words.get('main', ()): + seeds.add(node) + continue + jw = 0x0C000000 | ((addr >> 2) & 0x03FFFFFF) + for bdom in (dom, 'main'): + raw = word_counts.get(bdom, {}).get(jw, 0) + if raw > asm_jals.get((bdom, name), 0): + seeds.add(node) + break + return seeds + +# /* 25830 800BD830 1400A28F */ lw $v0, 0x14($sp) +INSN_RE = re.compile( + r'^\s*/\*\s*[0-9A-Fa-f]+\s+([0-9A-Fa-f]+)\s+[0-9A-Fa-f]+\s*\*/\s+\S', re.MULTILINE) + +RED = '\033[31m' +GREEN = '\033[32m' +DIM = '\033[2m' +RESET = '\033[0m' + + +def addr_of(text, name): + """Load address of `name`: the first instruction after its glabel. + + The filename carries the address for a ``func_XXXXXXXX``, but not once the + function has been named, so read it out of the asm instead. + """ + body = text.split('glabel ' + name, 1) + if len(body) == 2: + m = INSN_RE.search(body[1]) + if m: + return int(m.group(1), 16) + return 0 + + +def size_of(text, name): + """Byte size of `name` in this file: its own header, else instruction count.""" + for sym, size in HEADER_RE.findall(text): + if sym == name and size: + return int(size, 16) + # No header for our symbol (or no size on it) -- count the .text we can see. + body = text.split('glabel ' + name, 1) + if len(body) == 2: + return 4 * len(INSN_RE.findall(body[1])) + return 0 + + +def scan(root, exclude_dirs=()): + """Yield a dict per function .s found under root. + + A path is part of the backlog when it has a ``nonmatchings`` component, and + already decompiled when it has a ``matchings`` one. Both are always walked + -- the full tree costs well under a second, and reading it whole is what + lets every view report matched bytes as well as remaining ones. + ``--include-matched`` then only decides which rows get printed. + + Directories named in `exclude_dirs` are pruned by path component, so + ``psxsdk`` drops the whole vendored SDK without also matching some unrelated + substring. + """ + for dirpath, dirnames, filenames in os.walk(root): + dirnames[:] = [d for d in dirnames if d not in exclude_dirs] + parts = dirpath.split(os.path.sep) + if 'nonmatchings' in parts: + matched = False + elif 'matchings' in parts: + matched = True + else: + continue + for fn in sorted(filenames): + if not fn.endswith('.s'): + continue + path = os.path.join(dirpath, fn) + name = fn[:-2] try: - with open(path) as f: - # Skip leading comment lines (splat prepends - # "/* Handwritten function */" for GTE/COP2 funcs). - head = f.readline() - while head.lstrip().startswith('/*'): - head = f.readline() + with open(path, errors='replace') as f: + text = f.read() except OSError: continue - m = HEADER_RE.match(head) - if not m: + yield { + 'size': size_of(text, name), + 'addr': addr_of(text, name), + 'name': name, + 'obj': os.path.basename(dirpath), + 'matched': matched, + 'handwritten': 'Handwritten function' in text, + 'path': path, + } + + +def domain_of(path): + """Which program a path belongs to: an overlay name, field/intro, or main. + + Overlays are exclusive address spaces that reuse names (the overlay-conflict + prototypes), so reachability must never leak between two overlays -- but + every overlay may call into main. + """ + parts = path.split(os.path.sep) + try: + i = parts.index('asm') + except ValueError: + return 'main' + rest = parts[i + 1:] + if rest and rest[0] == 'ovl' and len(rest) > 1: + return rest[1] + if rest and rest[0] in ('field', 'intro'): + return rest[0] + return 'main' + + +def build_reach(root, defs, seeds, extra_live=()): + """Classify every defined function as '', 'dead' or 'unreachable'. + + Definitions (user's, from the world dead-island hunt): a function nothing + references at all is *dead*; a function whose every reference chain starts + only at dead roots is *unreachable* (it belongs to a dead island). Everything + reachable from a live seed is live (''). + + Live seeds are (a) functions referenced from data/rodata -- pointer tables + are consumed at runtime -- and (b) DEFAULT_SEEDS/--seed entry points. + Liveness then propagates along jal/%lo/%hi/.word edges to a fixpoint. + + Caveat carried over from the island hunt: FF8 streams world data from disc, + so a runtime-loaded dispatch word can never be excluded from the repo alone; + 'dead'/'unreachable' here means *statically* so. + """ + by_name = defaultdict(set) # name -> {domain} + for dom, name in defs: + by_name[name].add(dom) + + def resolve(dom, name): + """A reference in `dom` to `name`: same domain first, then main, then + (for names only defined elsewhere, e.g. main data naming overlay + symbols) every defining domain.""" + if name in by_name: + if dom in by_name[name]: + return [(dom, name)] + if 'main' in by_name[name]: + return [('main', name)] + return [(d, name) for d in by_name[name]] + return [] + + edges = defaultdict(set) # (dom,func) -> {(dom,func)} + inbound = defaultdict(int) # (dom,func) -> textual reference count + live = set() # BFS worklist seeds + + for dirpath, dirnames, filenames in os.walk(root): + for fn in filenames: + if not fn.endswith('.s'): + continue + path = os.path.join(dirpath, fn) + dom = domain_of(path) + owner = fn[:-2] + is_func_file = (dom, owner) in defs + try: + with open(path, errors='replace') as f: + text = f.read() + except OSError: continue - size = int(m.group(2), 16) if m.group(2) else 0 - yield size, m.group(1), path + targets = set(JAL_RE.findall(text)) | set(LOHI_RE.findall(text)) \ + | set(WORD_RE.findall(text)) + targets.discard(owner) # self-recursion must not self-revive + for t in targets: + for node in resolve(dom, t): + inbound[node] += 1 + if is_func_file: + edges[(dom, owner)].add(node) + else: + live.add(node) # data/rodata reference = live root + + for name in seeds: + for d in by_name.get(name, ()): + live.add((d, name)) + live.update(extra_live) + + work = list(live) + while work: + n = work.pop() + for m in edges.get(n, ()): + if m not in live: + live.add(m) + work.append(m) + + reach = {} + for node in defs: + if node in live: + reach[node] = '' + elif inbound.get(node, 0) == 0: + reach[node] = 'dead' + else: + reach[node] = 'unreachable' + return reach def main(): ap = argparse.ArgumentParser(description=__doc__) ap.add_argument('--root', default='asm', help='asm directory to scan') + ap.add_argument('--object', action='append', default=[], metavar='NAME', + help='scope to this splat object, e.g. we_object3 ' + '(repeatable); lists it all, in address order. Add ' + '--include-matched for the layout of the original .c file') + ap.add_argument('--by-object', action='store_true', + help='summarize per object instead of listing functions') + ap.add_argument('--sort', choices=('size', 'addr', 'name'), default=None, + help='sort key (default: size, or addr under --object)') ap.add_argument('--ascending', action='store_true', - help='smallest first (default: largest first)') - ap.add_argument('--exclude', action='append', default=[], + help='smallest first (default: largest first; address and ' + 'name order are always ascending)') + ap.add_argument('--include-matched', action='store_true', + help='also list already-decompiled functions (matchings/)') + ap.add_argument('--exclude-dir', action='append', default=[], metavar='NAME', + help='prune this directory name anywhere in the tree ' + "(repeatable); 'psxsdk' drops the vendored SDK") + ap.add_argument('--exclude', action='append', default=[], metavar='SUBSTR', help='substring to exclude from path (repeatable)') ap.add_argument('--limit', type=int, default=100, help='max rows to print (default 100, use --all to print all)') ap.add_argument('--offset', type=int, default=0, help='skip the first N rows (after sort/filter)') ap.add_argument('--all', action='store_true', help='print all rows') + ap.add_argument('--color', choices=('auto', 'always', 'never'), default='auto', + help='red = nonmatching, green = matched (default auto)') + hw = ap.add_mutually_exclusive_group() + hw.add_argument('--handwritten', action='store_true', + help='only handwritten functions') + hw.add_argument('--no-handwritten', action='store_true', + help='hide handwritten functions') + ap.add_argument('--no-reach', action='store_true', + help='skip the reachability analysis (dead/unreachable column)') + ap.add_argument('--no-deep', action='store_true', + help='skip the raw-binary address-word scan (build/ images); ' + 'without it, functions reached only through unsplit ' + 'pointer tables are falsely dead') + ap.add_argument('--seed', action='append', default=[], metavar='NAME', + help='treat this function as a live entry point (repeatable); ' + 'extends the built-in boot/overlay-entry seeds') args = ap.parse_args() + # --object scopes to one file's worth of functions; matched rows stay opt-in + # via --include-matched, exactly as they are for every other view. + objects = set(args.object) + if objects: + args.all = True + sort = args.sort or ('addr' if objects else 'size') + + color = args.color == 'always' or (args.color == 'auto' and sys.stdout.isatty()) + def paint(line, matched): + return (GREEN if matched else RED) + line + RESET if color else line + + # The reachability graph must always cover the WHOLE tree (liveness flows + # through matched functions and other objects), so it is built from an + # unfiltered scan even when the listing itself is scoped. + reach = {} + if not args.no_reach: + full = list(scan(args.root, set(args.exclude_dir))) + all_defs = {(domain_of(r['path']), r['name']) for r in full} + addr_by_node = {(domain_of(r['path']), r['name']): r['addr'] + for r in full} + extra = () if args.no_deep else deep_seeds(all_defs, addr_by_node, + args.root) + reach = build_reach(args.root, all_defs, + tuple(args.seed) + DEFAULT_SEEDS, extra) + rows = [] - for size, name, path in scan(args.root): - if any(p in path for p in args.exclude): + for r in scan(args.root, set(args.exclude_dir)): + if objects and r['obj'] not in objects: + continue + if any(p in r['path'] for p in args.exclude): + continue + if args.handwritten and not r['handwritten']: + continue + if args.no_handwritten and r['handwritten']: continue - rows.append((size, name, path)) + r['reach'] = reach.get((domain_of(r['path']), r['name']), '') + rows.append(r) - rows.sort(key=lambda r: r[0], reverse=not args.ascending) + if sort == 'size': + rows.sort(key=lambda r: (r['size'], r['name']), reverse=not args.ascending) + elif sort == 'addr': + rows.sort(key=lambda r: (r['addr'], r['name'])) + else: + rows.sort(key=lambda r: r['name']) + # Totals always cover both halves of the tree -- that is what makes a + # "bytes matched" figure meaningful even in the backlog-only view. + # `rows` is only what gets printed. + done = sum(1 for r in rows if r['matched']) + hand = sum(1 for r in rows if r['handwritten']) + todo_bytes = sum(r['size'] for r in rows if not r['matched']) + done_bytes = sum(r['size'] for r in rows if r['matched']) + all_bytes = todo_bytes + done_bytes + pct_bytes = 100.0 * done_bytes / all_bytes if all_bytes else 0.0 + stats = rows + todo = len(rows) - done + if not args.include_matched: + rows = [r for r in rows if not r['matched']] total = len(rows) - if args.offset: - rows = rows[args.offset:] - if not args.all: - rows = rows[:args.limit] - for size, name, path in rows: - print(f'{size:6d} {name} {path}') + if args.by_object: + by = {} + for r in stats: + o = by.setdefault(r['obj'], {'n': 0, 'done': 0, 'todo': 0, + 'bytes': 0, 'bytes_done': 0}) + o['n'] += 1 + o['done' if r['matched'] else 'todo'] += 1 + o['bytes_done' if r['matched'] else 'bytes'] += r['size'] + print(f"{'object':<20} {'todo':>5} {'done':>5} {'total':>6} " + f"{'bytes left':>11} {'bytes done':>11} {'pct':>6}") + print('-' * 76) + for name, o in sorted(by.items(), key=lambda kv: -kv[1]['bytes']): + tot = o['bytes'] + o['bytes_done'] + pct = 100.0 * o['bytes_done'] / tot if tot else 0.0 + line = (f"{name:<20} {o['todo']:>5} {o['done']:>5} {o['n']:>6} " + f"{o['bytes']:>11} {o['bytes_done']:>11} {pct:>5.1f}%") + print(paint(line, o['todo'] == 0)) + print('-' * 76) + else: + if args.offset: + rows = rows[args.offset:] + if not args.all: + rows = rows[:args.limit] + for r in rows: + mark = ('+' if r['matched'] else '-') if args.include_matched else ' ' + rc = r.get('reach', '') + line = (f"{mark} {r['addr']:08X} {r['size']:6d} {rc or '':<11} " + f"{r['name']} {r['path']}") + if r['handwritten']: + line += ' [handwritten]' + print(paint(line, r['matched'])) - print(f'\n# showing {len(rows)} of {total} rows ' - f'(--offset={args.offset}, --root={args.root})', - file=sys.stderr) + # --by-object aggregates the whole scan, not just the printable rows. + if args.by_object: + shown = total = len(stats) + else: + shown = len(rows) + ndead = sum(1 for r in stats if r.get('reach') == 'dead') + nunreach = sum(1 for r in stats if r.get('reach') == 'unreachable') + reach_note = ('' if args.no_reach + else f'; {ndead} dead, {nunreach} unreachable (static)') + summary = (f'# showing {shown} of {total} rows ' + f'({todo} nonmatching, {done} matched, {hand} handwritten' + f'{reach_note}; ' + f'{done_bytes} of {all_bytes} bytes matched = {pct_bytes:.2f}%, ' + f'{todo_bytes} left) ' + f'(--offset={args.offset}, --root={args.root})') + if args.color == 'always' or (args.color == 'auto' and sys.stderr.isatty()): + summary = DIM + summary + RESET + print('\n' + summary, file=sys.stderr) if __name__ == '__main__':