forked from Ports/goldeneye-pc-port
2-4 player split-screen (#99): D416-D419, D422, D428, D448, D450
Virtual seats, per-seat camera and input routing, aim modes, hot-plugged controllers reopening via a coalesced rescan (D450, dolent's PR #122, merged as the original commit). Rule-2 sign-off for D417. Refs: #99 D416 D417 D418 D419 D422 D428 D448 D450 #122
This commit is contained in:
commit
00fdd94c8d
24 files changed
+1090
-226
No files matched your search
+1
-1
@@ -119,7 +119,7 @@ below).
|
||||
|
||||
| Item | Status | Refs | Notes |
|
||||
|---|---|---|---|
|
||||
| **2–4 player split-screen multiplayer** | parked | #99, D416–D423, branch `feat/mp-splitscreen-99` | The largest missing N64 feature. 2P has been live-tested (M&K + pad, respawn, 16:9 HUD). Before merge: Rule-2 review of `PROP__PORT_SIGNED` (D417) and a golden gate run on the maintainer's console. Open on the branch: D420 (stats screen shows a large number), D421 (gadget cycling off; needs a Rule-2 game-thread hook), D422 (no centred aim for P2+), D423 (black arrowhead in P2 view), and D428's sway accumulator is player-0-only. Untested: 3P/4P, pads-only mode, pause/watch/F10 during a match, MP audio, most stages, and the full N64 MP setup screens (scenarios, handicaps, teams). Needs per-pad seats (§5). An outside restore exists for reference: birdturtle `feature/local-mp-input`. |
|
||||
| **2–4 player split-screen multiplayer** | parked | #99, D416–D423, branch `feat/mp-splitscreen-99` | The largest missing N64 feature. 2P has been live-tested (M&K + pad, respawn, 16:9 HUD). Before merge: Rule-2 review of `PROP__PORT_SIGNED` (D417) and a golden gate run on the maintainer's console. Open on the branch: D422 (no centred aim for P2+), and D428's sway accumulator is player-0-only. D420 (Scores page big number) FIXED, D421 closed (moot), D423 closed (faithful level shadow), all 2026-10-01; pause/unpause, watch-menu pages, exit-confirm and the F10 overlay during a match run clean headlessly in 2P/3P/4P. Untested: pads-only mode, MP audio, most stages, and the full N64 MP setup screens (scenarios, handicaps, teams). Needs per-pad seats (§5). An outside restore exists for reference: birdturtle `feature/local-mp-input`. |
|
||||
| **PAL and JP ROM support** | open | D258, D75 (pal/jpn sidecar regen), #85 | **Scheduled for Stage B (decided 2026-09-30):** done just before 1.0 so that region-specific issues can be unwound in isolation. Conversion is broken at the source-data level (filelist naming). A repair path has been verified but not applied. Needs PAL/JP ROMs to test. #85 was closed as "retired for now", not won't-do, so public users currently have no tracker. Reopen it or file a fresh one when work starts. |
|
||||
| Drop-in ROM converter, remaining parts | decision | #6, [`dev/release-dropin-rom-plan.md`](dev/release-dropin-rom-plan.md) | NTSC-U drop-in shipped. Left: per-region output names and re-exec guard (with PAL/JP), native C emitters to replace the PyInstaller converter. |
|
||||
| Cut content (TCRF weapons/items) | decision | [`dev/CUT-CONTENT-BACKLOG.md`](dev/CUT-CONTENT-BACKLOG.md) | Not part of the shipped game. Tier 0 already works via the debug/cheat menus. Tier 1 (config-toggle grants) is small and port-only. Tier 2/3 (placing props, re-enabling unused MP maps) need game data changes and are out of scope for this repo. |
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ to d69's output):
|
||||
```sh
|
||||
python3 tools_pc/d43_emit.py ntsc-final # -> data/pcmodels-ntsc-final/{pcmodels.bin,manifest.csv} (~1.3 MB)
|
||||
python3 tools_pc/d69_emit.py ntsc-final # -> data/pccg-ntsc-final/{pccg.bin,manifest.csv} (bg + stan)
|
||||
python3 tools_pc/d88_emit.py ntsc-final --regen # appends the 21 per-level Usetup*Z stage-setup files -> ~3.6 MB
|
||||
python3 tools_pc/d88_emit.py ntsc-final --regen # appends the 21 solo Usetup*Z + 13 multiplayer Ump_setup*Z stage-setup files -> ~3.6 MB
|
||||
```
|
||||
|
||||
**PAL / JP note:** sidecar generation for these regions is currently broken at
|
||||
|
||||
@@ -23,6 +23,8 @@ Two classes:
|
||||
| `GE_INPUTSCRIPT` | `port/src/input.c:306` | Headless scripted controller-0 input: `"<frame>:<tok>,…;…"` (`SUP/SDOWN/SLEFT/SRIGHT/SNONE` sustain, buttons pulse; `ZHOLD`/`ZREL` sustain fire, D207 M-201). Sole input source when set. | **live** |
|
||||
| `GE_INPUTLOG` | `port/src/input.c:329`, `port/src/config.c:53` (`configGetInputLog`) | Logs computed pad state (buttons + stick) each poll when non-zero. Also `[Debug] InputLog` ini. Also traces D165 `menuptr est/tgt/eff/stick`. | **live** |
|
||||
| `GE_STARTMENU` (+ `GE_STARTMENU_PAGE`, `GE_STARTMENU_DIFF`) | `src/game/lv.c:398-402` (`#ifdef PORT`) | Boot straight into a front-end menu id (13=MISSION_COMPLETE, 10=BRIEFING, 7=MISSION_SELECT, 12=MISSION_FAILED, 6=MODE_SELECT); `_PAGE`=folder row (def 1=Dam), `_DIFF`=0..3. Crash-test a screen fast. | **live** |
|
||||
| `GE_STARTMP` | `src/game/lv.c:403` (`#ifdef PORT`) | D416: `GE_STARTMP=<players>[,<mpStageIdx>]` starts a local multiplayer match with the menu's default options (as MP options -> START does); pair with `GE_MPVIRT=<players-1>` for the controller count. Unattended split-screen testing. | **live** (D416) |
|
||||
| `GE_MPVIRT` | `port/src/input.c:686` | D416: report `n` extra idle controllers (1..3) so the MP menu unlocks / a 2P+ stage loads on a machine with no gamepads. Test harness only; P2..Pn get no input. | **live** (D416) |
|
||||
| `GE_UNLOCK_ALL` | `src/game/debugmenu_handler.c:1098` (`get_debug_enable_all_levels_flag`, `#ifdef PORT`) | Mission-select shows every solo level (playtest jump-to-any-level aid). Cached in a `static int c`. | **live** |
|
||||
| `GE_DEBUG_UNLOCKALL` | `src/game/file2.c:404` (`#ifdef PORT`) | Forces the "all levels unlocked" flag TRUE regardless of the shipped save — same effect as `GE_UNLOCK_ALL` but at the file2 unlock-query site. Cached in a `static int`. | **live** (playtest aid) |
|
||||
| `GE_DEBUGMENU` | `src/boss.c:566` (`#ifdef PORT`) | Makes the L+R debug menu respond to live controller input (C-up+C-down open/close, stick + buttons read from pad 0) instead of the `#ifndef DEBUGMENU` path; unset reproduces that path exactly. Cached in a `static int`. | **live** (playtest aid) |
|
||||
|
||||
@@ -286,6 +286,14 @@ D406,RESOLVED,front-end PC Options screen:,"RESOLVED (2026-09-28, port-layer onl
|
||||
D407,CLOSED,Tanks cannot be boarded/exited on PC —,CLOSED (2026-09-28 bookkeeping:
|
||||
D408,FIXED,Game.SkipIntro skips the post-mission failure dossier:,FIXED 2026-09-30 (SkipIntro hook gated to first boot; live dossier check owed).
|
||||
D415,FIXED,"Shooting a light fixture does not ""kill"" its flickering light (Bunker, Caverns):",FIXED (2026-09-29 bookkeeping:
|
||||
D416,RESOLVED,"2-player split-screen crashes (issue #99, v0.3.0 4827bc8, fault addr 0xa43a0ed4, PC 0x14009e758):",RESOLVED in-harness (converter + D417 + D418 fix the 2P crash and split-screen flicker; 18/18 2P/3P/4P x 6-stage load runs clean; live pad play still owed).
|
||||
D417,RESOLVED,enum PROP unsigned on GCC:,"RESOLVED (src/bondconstants.h PROP__PORT_SIGNED = -1, D3/D142 precedent; Rule-2 review owed)."
|
||||
D418,RESOLVED,"Split-screen: fast3d safe-area crop stretched the last-set (half-height) viewport over the whole window, so each player's view filled the screen and the last-d…","RESOLVED (gfx_set_split_screen, port-only)."
|
||||
D419,RESOLVED,Split-screen input shared between players / P1 lost solo controls:,"RESOLVED (per-slot slotPlayer(), port-only; no current-player swaps from the poll thread; live-tested OK)."
|
||||
D420,FIXED,MP score / stats screen shows an odd big number (issue #99) —,FIXED 2026-10-01 (mp/virtual-seats); 2P/3P/4P watch-menu runs show 0s.
|
||||
D421,CLOSED,Gadget cycling (pad B / gadget key) disabled in 2+ player matches —,"CLOSED 2026-10-01: moot, no MP inventory holds a gadget (mp_weapon.c); hook reviewed + reverted."
|
||||
D422,FIXED,P2+ have no centred-aim mode in split-screen (single-slot aim state) —,FIXED in tree (commit ac92e0ab:
|
||||
D423,CLOSED,Unidentified black arrowhead in P2's split-screen view (stage 38) —,"CLOSED 2026-10-01: faithful (level shadow at a doorway top), maintainer-confirmed live."
|
||||
D424,FIXED,WeaponStats.RecoilSpeed/b44[] byte order:,"FIXED in tree (WS_B44, port-guarded); user-verified vs GEPD/1964 2026-09-29."
|
||||
D425,FIXED,Tank run-over death sound loops:,FIXED (2026-09-29 bookkeeping:
|
||||
D426,RESOLVED,Tank cannot be steered with the mouse out of aim mode (issue #116).,RESOLVED (user 2026-09-29:
|
||||
@@ -309,6 +317,10 @@ D455,FIXED,Binary-first census of exe-static addresses truncated to 32 bits (D45
|
||||
D447,FIXED,Pillarbox output mode (Video.AspectMode = Original) for the Original N64 preset; crop overscan no longer cancels Wide/Cinema.,"FIXED (build + 3 captures verified; Wide/Cinema, 16:9 Ratio, mouse and 1964 comparison owed to a human)"
|
||||
D445,FIXED,Crouched-rifle firing animation (crouched_rifle_firing_animation_groupA) never resolved on PC -> SIGSEGV in modelConstrainOrWrapAnimFrame when an MP player cro…,"FIXED — layout only (§D5 linker adjacency, D164 class):"
|
||||
D446,FIXED,Couch 2P watch-menu text (the MP pause page) differs by +-1-2 per channel between builds whose only difference is data layout (2026-09-29).,FIXED — port-only (fast3d):
|
||||
D448,PARTIAL,Split-screen pad seats:,"PARTIAL (committed 918ac5f9 on feat/mp-splitscreen-99, build + 2P smoke OK; plug/unplug behaviour not exercised at runtime)."
|
||||
D456,?,"Font-pointer / text-string-pointer slots retyped from s32 to real pointers (ABI/layout exception; front.c, options.c, language.c, bondview*, mpmenu.c, propobj.…",
|
||||
D453,?,"memp allocator API made 64-bit clean (D441 step 0; ABI/layout exception, §A1).",2026-09-30
|
||||
D451,FIXED,Automatic-fire gate at unsteady frame rates (D427 follow-up) and D173 residual.,"FIXED (RULE-2-SIGNOFF, maintainer 2026-09-30:"
|
||||
D463,FIXED,fast3d divided by zero (0xc0000094) importing a texture for a zero-area TextureRectangle on a tile with line = 0:,FIXED (port-layer guard in import_texture; 8 repeats of the crashing config + the 18-run sweep + solo -level_09 + menu boot all rc=0 with no crash; the guard fired in 3 of 8 repeats).
|
||||
D464,PARTIAL,"GE_MEMPREDZONE=1: 64-byte red-zone checker for the memp bump allocator (all banks), side table + checks at level unload / every 60 frames / orderly quit; re-in…","TOOL LANDED (local, feat/memp-redzone); MP sweep owed."
|
||||
D450,FIXED,"Unplugging and replugging a controller was not picked up (user, GameSir-G7 Pro; 2026-09-29).","FIXED — port-only: the render thread's gfx_sdl_handle_events drains the same SDL queue as video.c's host pump but had no case for SDL_CONTROLLERDEVICEADDED/REMOVED, so it usually swallowed them and i…"
|
||||
|
@@ -694,6 +694,14 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D407 | **Tanks cannot be boarded/exited on PC — the v0.4.0 use/reload split (D378/D393) removed the B-button tap the engine's tank handlers in `bondview2.c` consume (user report 2026-09-28, Runway/Streets)** — full `## D407` entry at file tail | **CLOSED (2026-09-28 bookkeeping: tank board/exit accepted by the user; shipped in v0.4.0).** Earlier: PARTIAL (fix landed in v0.4.0, port-only): present `GE_CONT_B` on the use (E / pad A) rising edge only while `g_PlayerIsInTank == 1` or `g_BondCanEnterTank != 0`; E keeps its D378 no-reload-fallback semantics elsewhere, N64 layout unaffected. D407(b) same cycle: front PC Options page-edge highlight clamp + mouse-wheel / W-S paging (wheel queue consumed on the menu, D223); its wheel mapping shipped inverted and was fixed to match W/S (wheel up = step up). D407(c): board-animation lockout, menu-accept B gate in tank states, in-tank aim routed through the legacy velocity stick + `Input.TankAimScale` knob. RESOLVED 2026-09-28: all port-only (src/game zero-diff), 10 TANKDBG probes stripped, release binary verified clean, user live tank drive signed off (board + exit OK, aim feel good). |
|
||||
| D408 | **`Game.SkipIntro` skips the post-mission failure dossier: with it on, entering a level then aborting (watch Z+A) or dying (KIA) returns straight to the menus — no REPORT / "Mission status: KILLED IN ACTION / ABORTED" screen** (user report 2026-09-28; save/AllUnlocked ruled out) — full `## D408` entry at file tail | FIXED 2026-09-30 (SkipIntro hook gated to first boot; live dossier check owed). |
|
||||
| D415 | **Shooting a light fixture does not "kill" its flickering light (Bunker, Caverns): the fixture takes the hit but the flicker keeps going (community report, issue #87 comment by the reporter, 2026-09-28, 00 Agent playthrough of v0.4.0).** — full `## D415` entry at file tail | FIXED (2026-09-29 bookkeeping: root-caused and fixed as D430 + D431, user-verified vs GEPD/1964 on Caverns 2026-09-29; #119). Earlier: ROOT-CAUSED as D430 (see there); fix in tree, live check owed. |
|
||||
| D416 | **2-player split-screen crashes (issue #99, v0.3.0 `4827bc8`, fault addr 0xa43a0ed4, PC 0x14009e758): MP setup files `Ump_setup*Z` were never converted N64→PC** — full `## D416` entry at file tail | RESOLVED in-harness (converter + D417 + D418 fix the 2P crash and split-screen flicker; 18/18 2P/3P/4P x 6-stage load runs clean; live pad play still owed). Also D417, D418.
|
||||
| D417 | **`enum PROP` unsigned on GCC: `prop >= 0` passed for `getPropForHeldItem()`'s -1 (fist) -> `modelLoad(-1)` null deref in any 2P+ stage (player.c `sub_GAME_7F09B398`)** — full `## D417` entry at file tail | RESOLVED (src/bondconstants.h `PROP__PORT_SIGNED = -1`, D3/D142 precedent; **Rule-2 review owed**). |
|
||||
| D418 | **Split-screen: fast3d safe-area crop stretched the last-set (half-height) viewport over the whole window, so each player's view filled the screen and the last-drawn (shuffled) player won -> alternating full-screen flicker** — full `## D418` entry at file tail | RESOLVED (`gfx_set_split_screen`, port-only). |
|
||||
| D419 | **Split-screen input shared between players / P1 lost solo controls: poll thread used g_CurrentPlayer for per-slot aim/stance/style/use** — full `## D419` entry at file tail | RESOLVED (per-slot `slotPlayer()`, port-only; no current-player swaps from the poll thread; live-tested OK). |
|
||||
| D420 | **MP score / stats screen shows an odd big number (issue #99)** — `char rankbuffer[4]` overrun ("Rank: 1st") clobbered the adjacent `scores[]` (D8 class); widened under `#ifdef PORT` — full `## D420` entry at file tail | FIXED 2026-10-01 (`mp/virtual-seats`); 2P/3P/4P watch-menu runs show 0s. |
|
||||
| D421 | **Gadget cycling (pad B / gadget key) disabled in 2+ player matches** — full `## D421` entry at file tail | CLOSED 2026-10-01: moot, no MP inventory holds a gadget (mp_weapon.c); hook reviewed + reverted. |
|
||||
| D422 | **P2+ have no centred-aim mode in split-screen (single-slot aim state)** — full `## D422` entry at file tail | FIXED in tree (commit `ac92e0ab`: per-slot aim state `s_aimSt[4]`, `aimModeGet()` returns the configured mode for every slot); live split-screen verification of centred aim on slot 1+ still owed (needs a non-slot-0 aim device). |
|
||||
| D423 | **Unidentified black arrowhead in P2's split-screen view (stage 38)** — full `## D423` entry at file tail | CLOSED 2026-10-01: faithful (level shadow at a doorway top), maintainer-confirmed live. |
|
||||
| D424 | **`WeaponStats.RecoilSpeed`/`b44[]` byte order: no grenade-launcher recoil, shortened recoil on every gun (issue #118).** — full `## D424` entry at file tail | FIXED in tree (`WS_B44`, port-guarded); user-verified vs GEPD/1964 2026-09-29. |
|
||||
| D425 | **Tank run-over death sound loops: `(chrflags << 7) >= 0` is always true on unsigned GCC enum (issue #115).** — full `## D425` entry at file tail | FIXED (2026-09-29 bookkeeping: use-site `(s32)` cast; user-verified vs GEPD/1964 2026-09-29, #115). Earlier: FIXED in tree (use-site `(s32)` cast); live check owed. |
|
||||
| D426 | **Tank cannot be steered with the mouse out of aim mode (issue #116).** — full `## D426` entry at file tail | RESOLVED (user 2026-09-29: tank controls now match GEPD/1964; see D429). |
|
||||
@@ -717,9 +725,15 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D447 | **Pillarbox output mode (`Video.AspectMode` = Original) for the Original N64 preset; crop overscan no longer cancels Wide/Cinema.** — full `## D447` entry at file tail | FIXED (build + 3 captures verified; Wide/Cinema, 16:9 Ratio, mouse and 1964 comparison owed to a human) |
|
||||
| D445 | **Crouched-rifle firing animation (`crouched_rifle_firing_animation_groupA`) never resolved on PC -> SIGSEGV in modelConstrainOrWrapAnimFrame when an MP player crouch-fires a rifle (2026-09-28).** — full `## D445` entry at file tail | FIXED — layout only (§D5 linker adjacency, D164 class): `crouched_rifle_firing_animation_group1` has no zero terminator and on N64 falls through into `...groupA` (source-order .data); GCC places groupA first, so groupA[0] kept the raw anim offset 0xB84. `#ifdef PORT` one array {right, left, terminator} + `groupA` alias (chr.c/chr.h); N64 resolution restored. |
|
||||
| D446 | **Couch 2P watch-menu text (the MP pause page) differs by +-1-2 per channel between builds whose only difference is data layout (2026-09-29).** — full `## D446` entry at file tail | FIXED — port-only (fast3d): `GL_TEXTURE_MAX_ANISOTROPY` is per-texture-object state that `gfx_opengl_set_sampler_parameters` set only when mipmapped and never reset; a GL texture id freed by `gfx_texture_cache_delete_range` (unordered_map order = texture address order) and reused for a non-mipmapped glyph kept 4x. Now always written (level or 1). Render-only; sim never affected. |
|
||||
| D448 | **Split-screen pad seats: `pads[]` indexed by seat and keyed by SDL instance id; seats compact outside a match and stay reserved during one (an unplug no longer shifts other players or flips keyboard/mouse-as-P1 routing; a rescan no longer reopens attached pads).** — full `## D448` entry at file tail | PARTIAL (committed 918ac5f9 on `feat/mp-splitscreen-99`, build + 2P smoke OK; plug/unplug behaviour not exercised at runtime). |
|
||||
| D456 | **Font-pointer / text-string-pointer slots retyped from `s32` to real pointers (ABI/layout exception; front.c, options.c, language.c, bondview*, mpmenu.c, propobj.c:3909 alias replaced by named field).** `frontPrintText`/`display_aligned_white_text_to_screen` font params, `copy_1st/2ndfonttable`, `setFontTables`, ~26 options.c locals, `g_LangBanks[]` + `langGet` result, `player.ptr_text_*_mp_award`. front.c 186->2, options.c 139->0 int-conversion warnings. FIXED (static + 1 launch; not gameplay-verified). |
|
||||
| D453 | **memp allocator API made 64-bit clean (D441 step 0; ABI/layout exception, §A1).** `mempCheckMemflagTokens` took the arena start as `s32`, `mempSetBankStarts` carried it in an `s32 mempStart`, and `mempAllocPackedBytesInBank` returned a pointer as `u32`; all truncate/sign-extend an arena at or above 0x80000000. Widened to `uintptr_t`/`void*` under `#ifdef PORT` (N64 arm unchanged). mema.c NOT changed (structural, see entry). | 2026-09-30 | fix/abi-memp-widen | [H] for memp, mema deferred |
|
||||
| D451 | **Automatic-fire gate at unsteady frame rates (D427 follow-up) and D173 residual.** — full `## D451` entry at file tail | FIXED (**RULE-2-SIGNOFF**, maintainer 2026-09-30: covered by the D427 grant; tick-crossing gate, `gunfire.c` `d427Gate`); headless A/B at FpsCap 45 measured; maintainer hold-fire test at 60/30 fps showed no perceptible difference (expected: identical at steady rates). D173 not the same class (see entry). |
|
||||
| D463 | **fast3d divided by zero (0xc0000094) importing a texture for a zero-area TextureRectangle on a tile with `line = 0`: the outlined-text path drew a zeroed font glyph (`w=h=0`, `pixeldata=NULL`), found by the scripted 3P/4P stability sweep (crash rate roughly 2 in 14 on the Library stage; section D).** Full `## D463` entry at file tail | FIXED (port-layer guard in `import_texture`; 8 repeats of the crashing config + the 18-run sweep + solo `-level_09` + menu boot all rc=0 with no crash; the guard fired in 3 of 8 repeats). |
|
||||
| D464 | **`GE_MEMPREDZONE=1`: 64-byte red-zone checker for the `memp` bump allocator (all banks), side table + checks at level unload / every 60 frames / orderly quit; re-introducing the D462 `0x14` stride is detected (falsifier PASS); 21 solo levels + menu clean (0 hits); MP matrix not run (GE_STARTMP lives only on feat/mp-splitscreen-99).** Full `## D464` entry at file tail | TOOL LANDED (local, `feat/memp-redzone`); MP sweep owed. |
|
||||
| D407 | **Tanks cannot be boarded/exited on PC — the v0.4.0 use/reload split (D378/D393) removed the B-button tap the engine's tank handlers in `bondview2.c` consume (user report 2026-09-28, Runway/Streets)** — full `## D407` entry at file tail | PARTIAL (fix landed in v0.4.0, port-only): present `GE_CONT_B` on the use (E / pad A) rising edge only while `g_PlayerIsInTank == 1` or `g_BondCanEnterTank != 0`; E keeps its D378 no-reload-fallback semantics elsewhere, N64 layout unaffected. D407(b) same cycle: front PC Options page-edge highlight clamp + mouse-wheel / W-S paging (wheel queue consumed on the menu, D223); its wheel mapping shipped inverted and was fixed to match W/S (wheel up = step up). D407(c): board-animation lockout, menu-accept B gate in tank states, in-tank aim routed through the legacy velocity stick + `Input.TankAimScale` knob. RESOLVED 2026-09-28: all port-only (src/game zero-diff), 10 TANKDBG probes stripped, release binary verified clean, user live tank drive signed off (board + exit OK, aim feel good). |
|
||||
| D408 | **`Game.SkipIntro` skips the post-mission failure dossier: with it on, entering a level then aborting (watch Z+A) or dying (KIA) returns straight to the menus — no REPORT / "Mission status: KILLED IN ACTION / ABORTED" screen** (user report 2026-09-28; save/AllUnlocked ruled out) — full `## D408` entry at file tail | OPEN (cosmetic, not root-caused past the handoff site). v0.4.0 decision: SkipIntro stays EXPERIMENTAL / not recommended for regular users — F10 row relabelled "Skip intro (EXPERIMENTAL)". |
|
||||
| D450 | **Unplugging and replugging a controller was not picked up (user, GameSir-G7 Pro; 2026-09-29).** — full `## D450` entry at file tail | FIXED — port-only: the render thread's `gfx_sdl_handle_events` drains the same SDL queue as video.c's host pump but had no case for `SDL_CONTROLLERDEVICEADDED/REMOVED`, so it usually swallowed them and `inputRescanPads` never ran. Both pumps now post a request; the rescan runs at the top of `inputUpdate`, on the thread that reads the pads. |
|
||||
|
||||
|
||||
Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS
|
||||
@@ -15632,6 +15646,89 @@ flickering fixture), then repro on PC.
|
||||
**Current status (2026-09-29 bookkeeping):** the Status line above is historical; the authoritative verdict is the §F row — FIXED (2026-09-29 bookkeeping: root-caused and fixed as D430 + D431, user-verified vs GEPD/1964 on Caverns 2026-09-29; #119). Earlier: ROOT-CAUSED as D430 (see there); fix in tree, live check owed.
|
||||
|
||||
|
||||
## D416 — Local split-screen multiplayer crash: MP setup files never converted (issue #99)
|
||||
|
||||
**Report:** issue #99, 2P split screen, v0.3.0 `4827bc8`, fault addr `0xa43a0ed4`, crash PC `0x14009e758`.
|
||||
|
||||
**Symbolised (2026-09-29, `dist/goldeneye-pc-port-0.3.0-win64` exe, addr2line):** `load_proptype` (prop.c) called from `proplvreset2` (prop.c) — the propDefs record walk over `g_CurrentSetup`, i.e. the setup-file load, not menu code.
|
||||
|
||||
**Root cause:** `prop.c` loads `"U" + "mp_" + <stage>` (`Ump_setup*Z`) whenever `getPlayerCount() >= 2`. `tools_pc/d88_emit.py` (and `d88_propdefs.py`, `d88_propdef_scan.py`, `d125_check.py`) only matched `Usetup*Z`, so the 13 MP setups in `file_resource_table.inc.c` came out of pccg as raw big-endian N64 data and the walk read garbage. (Not the D190 class: D190 is closed as `-level_45` being MP-only with no solo setup.)
|
||||
|
||||
**Fix (tools only, no game code):** single predicate `is_setup_name()` = (`Usetup`|`Ump_setup`) + `Z`, used for the name list, the `--regen` row-drop, and the analysis tools. `convert_usetup()`'s full-tiling assertion is the gate that MP files share the solo format.
|
||||
|
||||
**Verified:** `d88_emit.py ntsc-final --regen` → `converted: 34/34`, ALL CHECKS PASSED; `d125_check.py` MATCH on all 13 MP + 21 solo files; pccg manifest 73→86 rows with all 73 pre-existing rows byte-identical. PAL/JP not run (NTSC ROM only).
|
||||
|
||||
**Follow-up (2026-09-29):** in-game 2P now runs (see D417, D418). Also added: `Input.MPMode` (Auto/PadsOnly/KbmP1) + slot->pad shift so one pad or kbm+pad can be P1/P2 and the game's `joyGetControllerCount() >= 2` MP gate opens; `GE_MPVIRT` / `GE_STARTMP` harness flags (this machine has 0 SDL gamepads). Input layer is still single-player shaped (see memory: mp-input-layer-direction). Separately, PC input hooks are pad-0-only and `input.c` re-asserts control type on the current player (can rewrite another player's control style in MP) — port-layer work planned for v0.5.0.
|
||||
|
||||
**Status:** RESOLVED in-harness (see header row).
|
||||
|
||||
|
||||
## D417 — `enum PROP` unsigned: fist held-prop lookup (-1) passes `>= 0` and crashes 2P+ (issue #99)
|
||||
|
||||
**Crash (gdb, 2026-09-29):** `modelLoad(modelid=-1)` <- `something_with_generating_object(propid=-1, itemid=ITEM_FIST)` <- `sub_GAME_7F09B398` (player.c) <- `gunTickHandState` <- `bondviewProcessInput`. Fault: `PitemZ_entries[-1].header->RootNode`, header NULL.
|
||||
|
||||
**Root cause:** `getPropForHeldItem()` returns -1 for items with no held model (fist, taser). `sub_GAME_7F09B398` guards with `if (prop >= 0)`; `PROP` is an all-non-negative enum, so GCC gives it an unsigned type and -1 passes. Solo never runs this path (only other players' visible weapons need it). Same class as porting-notes D3 / D142.
|
||||
|
||||
**Fix:** `PROP__PORT_SIGNED = -1` sentinel under `#ifdef PORT` at the head of `enum PROP` (`src/bondconstants.h`), following the `SP_LEVEL__PORT_SIGNED` precedent. `PROP_ALARM1` stays 0, sizeof unchanged, no stored value changes. Also corrects `drop_inventory` (`propid >= 0`). **This is a `src/game`-header edit: Rule-2 review owed.**
|
||||
|
||||
**Status:** RESOLVED.
|
||||
|
||||
## D418 — Split-screen alternating full-screen views: safe-area crop remapped sub-rect viewports (issue #99)
|
||||
|
||||
**Symptom (user, live):** in 2P the view flickers between both players constantly. GE_MPDUMP (temp) frame capture: each frame is ONE player's view filling the whole window, alternating in shuffled draw order (`get_nth_player_from_shuffled`) — no split visible.
|
||||
|
||||
**Root cause:** `gfx_adjust_viewport_or_scissor` (fast3d) maps the last-set SP viewport's Y range onto the whole window when `Video.SafeAreaCrop` is on (default; designed for the solo 220-line viewport). In 2P each player's viewport is 109 lines, so each was stretched to full height and the last drawn covered the other.
|
||||
|
||||
**Fix (port only):** `gfx_set_split_screen()` turns the crop (Y and X margin) off while a 2+ player stage runs; `videoSyncSplitScreen()` (video.c) sets it from `getPlayerCount()` + stage != title, called before every `gfx_run` — **including the scheduler's direct call in `libultra.c`**, which bypasses `videoSubmitCommands` (my first attempt hooked only that and had no effect: worth remembering, see porting-notes). Verified by frame capture: two stable stacked views over 4 consecutive frames, original N64 letterbox, viewports y=119/230 h=109; solo golden gate 3/3.
|
||||
|
||||
**Status:** RESOLVED (harness); live pad play + HUD/widescreen (plan 2c, D334/D335 in MP) still owed.
|
||||
|
||||
**2026-09-30 note (merge of release/v0.4.1, D447 interaction):** D447 gated the crop cache so only viewports >= 90% of the canvas height define the crop band (Wide/Cinema and split halves keep their own letterbox) and added an output rect (Original aspect = centred 4:3/16:9 rect, `gfx_current_game_window_viewport` = that rect). The two compose without overlap: in a 2+ player stage D418's `g_split_screen` still bypasses the crop entirely (`safeTop/safeHeight = SCREEN_HEIGHT`, no X margin), so the N64 split layout maps 1:1 onto the canvas; D447's seed/gating only touches the cached band, which is unused while split. The canvas itself is then placed in the window or, in Original mode, inside the pillarboxed output rect by `gfx_start_frame`. Merge conflict was textual only (adjacent globals in gfx_pc.cpp).
|
||||
|
||||
## D419 — Split-screen input shared between players / P1 lost solo controls (issue #99)
|
||||
|
||||
**Symptom (user, live):** in a 2P match one player's input drove both views, and P1 keyboard/mouse or pad did not behave like solo (no mouse look, no SOLITARE layout, no crouch/use/reload).
|
||||
|
||||
**Root cause:** the slot plumbing (`contSnapshotFromKeyboard` polls 0..3, player N reads controller N) was already per-slot, but `inputComputePad` ran on the scheduler thread and read/wrote `g_CurrentPlayer` — whichever player the game thread last selected — for aim, stance, style and use/reload. The D416 stopgap disabled those paths in a match, which removed them from P1 too.
|
||||
|
||||
**Fix (port only, `port/src/input.c`):** `slotPlayer()` resolves the player that owns the slot being polled (`s_slot`, set by a thin `inputComputePad` wrapper); all player-struct access goes through it. In a 2+ player stage the poll thread NEVER calls a game entry point that acts on `g_CurrentPlayer`, and never re-points the current-player globals: control style is written straight onto the slot's player (mirror of `cur_player_set_control_type`), the crouch weapon check reads `p->hands[GUNRIGHT].weaponnum`, use/reload go out as the native B button, gadget cycling is solo-only. Crouch state is per slot; slots 1+ get pad A/X = B, stick click = crouch; slots 1+ use the N64 aim integrator (centred aim state is single-slot); `portMouseAimPdGetTurn` applies to player 0 only; unused slots return neutral.
|
||||
|
||||
**Lesson (first attempt hung the game):** the first version wrapped those calls in a save/swap/restore of `g_CurrentPlayer`/`player_num`/`g_playerPerm`. The poll thread is a real thread, not the game thread, and the style check ran the swap every poll. When the game thread did its own `set_cur_player` inside the window, the restore left it on the wrong player and `bondinvCycleForward` (bondinv.c:523) looped forever (circular list walked against another player's list head; kernel heartbeat dump showed mainThread spinning there after a respawn). Never swap the current-player globals from the poll thread. See porting-notes.
|
||||
|
||||
**Status:** RESOLVED (user live-tested M&K P1 + pad P2 and the kill/respawn sequence, 2026-09-29). Open: P2+ centred aim; gadget cycling in matches; MP score screen shows an odd big number (not investigated).
|
||||
|
||||
## D420 — MP score / stats screen shows an odd big number (issue #99)
|
||||
|
||||
**FIXED 2026-10-01 (`mp/virtual-seats`): a D8-class stack overrun, not a scoring bug.** Reproduced headlessly (2P, stage 38, `GE_STARTMP=2 GE_MPVIRT=1 GE_INPUTSCRIPT="400:START"`, frame 450): the watch menu's Scores page showed `1932599354` / `116` at 0 kills. A temp probe in `display_text_for_playerdata_on_MP_menu` showed `get_points_for_mp_player(0/1)` = 0 / 0 while the passed `points` were the garbage values. `1932599354` = 0x7331203A = bytes `": 1s"` and `116` = `'t'`: the tail of **"Rank: 1st"**. `mp_watch_menu_display` (mpmenu.c) declares `char rankbuffer[4]` (decomp size) but `write_playerrank_to_buffer` writes "Rank: 1st" (10 bytes incl. NUL) into it, and `sprintf` writes "P<n> KILLS"/"P<n> LOSSES"; GCC places `scores[4]` right after it, so the rank text overwrote `scores[0..1]` after they were filled. On N64 the IDO frame layout absorbs the overrun (the menu shows 0). **Fix:** `rankbuffer[64]` under `#ifdef PORT` (D8 policy: pure local stack scratch, zero ABI role, same as D295's `difficultytext`); the N64 build keeps `[4]`. Not a Rule-2 change (removes a PC-only clobber). **Verified:** the same 2P frame shows 0 / 0; 2P/3P (Library)/4P watch-menu runs with pause, unpause, exit-cancel and exit-confirm show 0s in every layout, all rc=0, `GE_MEMPREDZONE` 0 hits. Not checked: the post-match awards screen (the `GE_STARTMP` harness relaunches a match instead of returning to the menus), and the maintainer's original live report (assumed to be this page). The `u16 *text` sprintf slot in `display_text_for_playerdata_on_MP_menu` (8 bytes on PC) still overruns for a score of 8+ chars (only possible far beyond any point limit); left as is.
|
||||
|
||||
**Symptom (user, live, 2026-09-29):** in a 2P match the player score / position display shows "a big number"; at match start both players show 1st place. Still present after a kill + respawn.
|
||||
|
||||
**Read so far (code only, no capture):** `write_playerrank_to_buffer` / `get_points_for_mp_player` (mpmenu.c ~903/826) rank by kills-minus-suicides with ties sharing the better rank, so 0-0 at start = both 1st is N64-faithful. A big number could be the stats screen's `shortest_inning` (init `S32_MAX` = 2147483647 in lv.c:548, only lowered on a death), but it persisted after a kill/respawn, so that is NOT confirmed. Nothing 64-bit-suspect found in the rank/points path (`kill_counts` is a plain `int[4]`); `display_text_for_playerdata_on_MP_menu` does `sprintf(&text, "%d", …)` into a `u16 *` local (8 bytes on x64, 4 on N64) and `char rankbuffer[4]` holds "1st" exactly — both worth a second look if a capture shows garbage.
|
||||
|
||||
**Next:** get the exact screen (pause/watch menu vs end-of-match) via `GE_PCDUMP` or a screenshot; identify which field prints the number; check against the 1964/N64 reference before deciding whether it is a port bug.
|
||||
|
||||
**2026-09-30 code-only pass (no fix, hypothesis unconfirmed):** the in-match number is drawn by `display_text_for_playerdata_on_MP_menu` (mpmenu.c:760) from `get_points_for_mp_player` (mpmenu.c:826) = kill_counts[] (s32, player.h:19) minus suicides + `killed_gg_owner_count*(n-2)`; all are zeroed per player in initBondDATA.c:267-269, so no uninitialised read found and the struct fields are plain s32/int (no 32->64 widening). Candidates not ruled out: (1) mpmenu.c:771 `u16 *text; sprintf(&text, "%d", points)` uses a pointer-sized local as a char buffer (4 bytes on N64, 8 on PC; a value with >7 chars overruns it and the neighbouring locals) -- any large/negative score would print big and could corrupt adjacent stack; (2) `s32 q/h2 = (s32)langGet(...)` then `(char*)q` (mpmenu.c:1408-1410, 1478-1480) truncates string pointers on 64-bit (works only while strings sit below 4 GB; the label prints correctly, so probably benign here). Next step: dump `scores[]` at mpmenu.c:1240 with GE_PCDUMP at the moment the number shows.
|
||||
|
||||
## D421 — Gadget cycling disabled in 2+ player matches
|
||||
|
||||
**CLOSED 2026-10-01: moot, no fix needed.** The pad-B cycle only selects items `ITEM_BOMBCASE..ITEM_WATCHMAGNETATTRACT` (mission gadgets). No MP weapon set (`src/game/mp_weapon.c`; every `INLINE_S_MP_WEAPON_SET` entry tallied) contains one: MP 'devices' are the timed/proximity/remote mines, which sit below `ITEM_BOMBCASE` and already cycle natively. So a match inventory never holds a gadget, and B would select nothing even with a working hook. A design review (Sonnet subagent, read-only, delegated by the maintainer) had approved a game-thread drain hook (`inputGadgetDrain()` inside `if (canCycleWeapons)` in `MoveBond`'s input step, bondview2.c ~6325, per-seat `SDL_atomic_t` one-shot, gated on alive/watch/lock/stop/game-over). It was implemented and built, then reverted unmerged once the empty gadget range was found. The early return in `inputCycleGadget()` stays. If a mode ever puts gadgets into an MP inventory, that design is the one to use. Side note from the review: solo's `inputCycleGadget()` also runs on the poll thread and mutates `hands[]` while the game thread may read it. No hang is possible in solo (`g_CurrentPlayer` never changes), but it is a latent data race; low priority.
|
||||
|
||||
`inputCycleGadget()` (port/src/input.c) acts on `g_CurrentPlayer` via game entry points with no player argument; calling it from the poll thread raced the game tick (D419 hang), so it now returns immediately in a match. Solo unchanged. Restoring it needs a game-thread hook (src/game edit, Rule-2 sign-off) or a native-button equivalent. Native N64 weapon cycling (C-buttons / pad Y) still works.
|
||||
|
||||
## D422 — No centred aim for P2+ in split-screen
|
||||
|
||||
**Update (2026-09-30, verified by code reading):** fixed by `ac92e0ab` (an ancestor of `mp/virtual-seats`): `s_aimSt[4]` with the `AIMS` slot accessor, a stateless `padDirectCompute` on `slotPlayer()`, and `aimModeGet()` no longer forces `AIMMODE_N64` for slots > 0. Slot 0 stays byte-for-byte (`AIMS` falls back to slot 0). This entry was left saying 'parked'. Untested live on slots 1+.
|
||||
|
||||
Centred aim (`aimModeGet()==AIMMODE_CENTRED`, `padDirectCompute`, `s_gepdCross*`) keeps single-slot static state, so `aimModeGet()` returns `AIMMODE_N64` for slots > 0 (port/src/input.c). P1 keeps its full aim set. Making the state per slot is mechanical but touches the aim accumulators; parked as low priority.
|
||||
|
||||
## D423 — Unidentified black arrowhead in P2's split-screen view
|
||||
|
||||
**CLOSED 2026-10-01: faithful, not a bug.** The maintainer played a live 4P Temple match (mouse/keyboard P1 + Xbox pad P2, split-screen build) and confirmed the black triangle is the level's own shadow/shading at the top of a doorway. The session exited cleanly (window closed, rc=0, no crash log).
|
||||
|
||||
**2026-10-01:** seen again headlessly in ONE frame of a 4P run on stage 38 (`GE_STARTMP=4 GE_MPVIRT=3`, scripted seats): P4's quadrant at frame 920, a flat, untextured near-black (RGB ~10-13) right-pointing triangle with sharp edges at a wall/ceiling edge; it looks in-world, not HUD. (An earlier note here also claimed P2 at frame 380; that was a thumbnail misread, and the full-res crop shows nothing.) So it is real rendered pixels and not 2P-only, but it has not been shown to be a port bug: it could be legitimate geometry, e.g. part of another player's model or a level object, until checked against 1964 or traced.
|
||||
|
||||
Seen in a `GE_PCDUMP` frame (2P, stage 38, frame 500) as a black right-pointing triangle at the left of the lower (P2) view, and again in live play (user: "not sure what it is"). Not identified: could be an in-world object/marker or a HUD/radar element drawn with a bad texture. Next: capture several frames, `GE_DTEX`/DL dump around that screen position, check whether it also appears in P1's view or in solo MP-stage loads.
|
||||
|
||||
## D424 — `WeaponStats.RecoilSpeed`/`b44[]` byte order: no recoil animation on the grenade launcher, shortened recoil on every gun (issue #118)
|
||||
|
||||
**Root cause:** `b44[4]` aliases the four bytes of the big-endian `RecoilSpeed` word (`gun.h`). Byte 0 is the MSB on N64 but the LSB on little-endian PC, so `gunfire.c` read rise/fall/retrigger/offset reversed. Grenade launcher `0x081000FF` (8,16,0,-1) read as (-1,0,16,8): `field_890 >= sp1A4+sp1A0` was true at once, so the recoil phase never ran. KF7/Klobb 4+12 -> 6+0 ticks, PP7 4+8 -> 0+3.
|
||||
@@ -15683,6 +15780,8 @@ After D426, non-aim mouse turning in the tank is good. Still open: (1) right-mou
|
||||
|
||||
## D428 second pass (2026-09-29)
|
||||
|
||||
**Split-screen update (2026-09-30):** the sway accumulator is now one per player (`s_swayThetaMdeg[4]`, `ac92e0ab`). The game-side drain in `bondview2.c` is still gated to player 0 (`get_cur_playernum() == 0`), which is correct today: `swayFeedTheta` is only called from the slot-0 mouse block, so slots > 0 are never fed. Widening the gate is only needed if a non-zero slot ever gets a mouse (a `src/game` edit inside the D428 Rule-2 block; ask for sign-off then).
|
||||
|
||||
Probe (`GE_D428`) showed the hook firing with the input clamped at 0.7 on most fast turns. The game's natural stick turn sets `speedtheta = (stick/70)^2 * fov/60` with NO ramp (max ~1.0) and integrates `vv_theta += speedtheta*dt*3.5`, so the exact inverse for a mouse turn is `deg / (3.5*dt)` clamped to `fov/60`. The first pass used the vertical axis' `delta/(2*dt)` clamped to 0.7: too weak at full speed, too strong at slow speed. Switched; user reports a slight difference turning fast, which this addresses.
|
||||
|
||||
## D433 — Automatic-gun shot sound still audibly differs slightly from GEPD/1964 (user by-ear, Caverns, 2026-09-29)
|
||||
@@ -15925,6 +16024,13 @@ About 55-922 pixels differ, a few glyphs only, each by +-1-2 (mostly green). Eve
|
||||
|
||||
The general fix would reset the whole object on reuse, or sort the ids that `delete_range` frees. Neither is needed for this symptom. **Confidence: [H]** for the cause and the fix (A/B reproduced, fixed, bisected to the one GL parameter).
|
||||
|
||||
## D448 — Split-screen pad seats keyed by SDL instance id (issue #99, D416 lineage, 2026-09-30)
|
||||
|
||||
**Status: PARTIAL.** Committed as `918ac5f9` on `feat/mp-splitscreen-99` (the commit message calls it "DNEW-mp-1"; this is its label). It was recovered from a crashed session's uncommitted worktree (archive `2026-09-30-recovery/mp-uncommitted-input-bg.patch`). Its TEMP-VPADSEAT probe was removed before commit.
|
||||
|
||||
**What:** `port/src/input.c` `pads[]` is indexed by *seat*, not by SDL device index, and each seat remembers its SDL joystick instance id. Outside a match, seats compact (the first attached pad is seat 0, and so on). During a match they stay reserved, so unplugging one pad no longer shifts the other players' seats or flips the Auto → keyboard/mouse-as-P1 routing. A rescan keeps pads that are still attached open instead of closing and reopening them. Port-only.
|
||||
|
||||
**Verified:** build, plus a 2P smoke (`GE_STARTMP=2 GE_MPVIRT=1`, 1280x720) in Window and Original aspect modes after merging `release/v0.4.1`. **Owed (maintainer, live):** plug/unplug pads mid-match (the seats must not shift), and check the keyboard/mouse-as-P1 routing is unchanged after an unplug. Related: outside PR #122 (hot-plug reopen, dolent) overlaps this area. Review it against this design before merging it.
|
||||
|
||||
## D454 — knife slash crash: keyframe pointer held in `u32` (ABI/layout exception, 2026-09-30)
|
||||
|
||||
@@ -16005,6 +16111,23 @@ The general fix would reset the whole object on reuse, or sort the ids that `del
|
||||
**By-eye test (maintainer):** AK47/Uzi held fire with `FpsCap` 60, 45 (or any unsteady load) and 30; empties the magazine in about the same wall time as 1964/GEPD at every setting; no stutter-bursts.
|
||||
**Probes left in tree:** none (env-gated `GE_D427`/`GE_D427_OLD` probes stripped).
|
||||
|
||||
## D450 — a replugged controller is never reopened: the render thread's event pump swallows the hot-plug events (2026-09-29)
|
||||
|
||||
**Status:** FIXED, port-only. User report (GameSir-G7 Pro): "disconnecting the controller, then reconnecting it did not seem to be picked up". The play log had only the startup `input: rescanned pads` line.
|
||||
|
||||
**Root cause.** Two threads drain the one SDL event queue:
|
||||
- `port/src/video.c` `videoPumpEvents`: `case SDL_CONTROLLERDEVICEADDED / REMOVED: inputRescanPads();`.
|
||||
- `port/fast3d/gfx_sdl2.cpp` `gfx_sdl_handle_events` (called from `gfx_run` on the render / scheduler thread every frame): **no case for them**. Whatever it dequeues is dropped.
|
||||
|
||||
Whichever loop runs `SDL_PollEvent` first gets the event, and the render loop usually wins (the same race WI-1 documented for ESC). So the pads were never rescanned and `pads[]` kept the closed controller's handle. Headless repro with a virtual pad (attach, detach, re-attach): one rescan at startup, none at the detach, and at the re-attach the process aborted with `malloc(): unaligned tcache chunk detected` (stale handle). A second latent problem: `inputRescanPads` ran on the host thread while the scheduler thread reads `pads[]` in `inputComputePad`; closing pads under a reader is a race.
|
||||
|
||||
**Fix.**
|
||||
- `gfx_sdl2.cpp`: new `case SDL_CONTROLLERDEVICEADDED / REMOVED: inputRequestRescan();`.
|
||||
- `video.c`: the case now calls `inputRequestRescan()` instead of `inputRescanPads()`.
|
||||
- `input.c`: `inputRequestRescan()` sets an atomic; `inputUpdate()` consumes it first thing and runs `inputRescanPads()` there, i.e. on the thread that is about to read the pads (the scheduler thread, or the game thread under tick input).
|
||||
|
||||
**Verified** (on the netplay line, headless, virtual pads through a test seam): attach -> detach -> attach -> detach -> attach with no tick input: 5 rescans (startup + one per event), no crash, against 1 rescan then a heap-corruption abort before the fix; with tick input the pad's buttons are seen before, after the first and after the second re-attach; solo `-level_09` frame 1800 byte-identical to the base. Real-pad retest by the user (GameSir-G7 Pro, 2026-09-29): passed.
|
||||
|
||||
|
||||
## D461 — `p_itemcur` allocated with the N64 stride; overran into the first prop model file (ABI/layout exception, 2026-09-30)
|
||||
|
||||
@@ -16028,3 +16151,34 @@ The general fix would reset the whole object on reuse, or sort the ids that `del
|
||||
**Found by.** The local-Qwen read-only audit of every `mempAlloc*` size expression (top suspects #1 and #2); the lead verified the literal at `vtxstore.c`, the C-stride indexing, and the D457(d) diff.
|
||||
|
||||
**Verified.** Build clean; solo `-level_09` to frame 420 and a 4-player `GE_STARTMP=4 GE_MPVIRT=3` run to frame 500 both exit rc=0 with no crash log. **Confidence: [H]** on the overrun, [M] on any visible effect (none expected). Remaining audit suspects: `size_item_buffer` weapon model buffers (`initBondDATA.c`), `model.c` rwdata word count vs converted data, `image_bank.c` +0x1000 slack: all UNKNOWN, need a converted-size check.
|
||||
|
||||
**Residual (pre-existing):** `inputRumble` can run on the game thread while a rescan runs on the scheduler thread in non-tick mode: one poll of exposure instead of an arbitrary host-thread moment.
|
||||
|
||||
|
||||
## D463 — zero-line tile texrect divided by zero in the texture importers (fast3d, 2026-10-01)
|
||||
|
||||
**Symptom.** The scripted split-screen sweep (`GE_STARTMP`, `GE_MPVIRT`, per-slot `GE_INPUTSCRIPT_P1..P3`) crashed once in 18 runs (3P, stage_id 48, Library) with `EXCEPTION 0xc0000094` = integer divide by zero (the sweep report's 'stack-buffer-overrun/fastfail' label was wrong; fastfail is 0xc0000409). A repro with `GE_D116=1` gave 2 crashes in 14 runs (3P and 4P, stage index 4), so it is not player-count specific.
|
||||
|
||||
**Cause.** Crash PC = `import_texture_i8` (`gfx_pc.cpp`, `height = size_bytes / rdp.texture_tile[tile].line_size_bytes`) with `line_size_bytes == 0`, reached via `gfx_dp_texture_rectangle` -> `gfx_draw_rectangle` -> `gfx_sp_tri1` -> `import_texture`. In both crashes the log shows, immediately before the fault, `[D116] O-glyph ... idx=0 w=0 loadw=0 h=0 pd=0000000000000000` (a zeroed font record drawn by `textRenderGlyphOutlined`, `src/game/textrelated.c`) followed by a zero-area `[D116/f3d]` texrect on a tile with `line_bytes=0`. `gDPLoadTextureBlock` with width 0 expands to `gsDPTile(line=0)` plus a zero-area `gSPTextureRectangle`; the N64 RDP rasterises nothing for it, so the decomp's output is faithful and fast3d must tolerate it. All the importers share the unguarded division (rgba16, ia8, i8, ci8, ...). **Not established:** which text produces the zeroed glyph (the outlined-text path reads `chars[*text - 0x21]`, so a character below 0x21 or an unpopulated font slot is the suspect). Rare because it depends on the string being drawn.
|
||||
|
||||
**Fix (port-layer, `port/fast3d/gfx_pc.cpp`, commit `81b57504`).** At the top of `import_texture`, if the tile's `line_size_bytes == 0`: log one capped line (`D463: texrect on zero-line tile -- import skipped`, at most 8 per run) and return without importing (no cache entry, no upload). The downstream triangle path already treats a zero line size as 1, and the degenerate draw rasterises no fragments. Nonzero line sizes take the original path unchanged. No game-logic change.
|
||||
|
||||
**Verified (local Qwen driver, lead re-read the logs).** After the fix: 8 repeats of the crashing 3P/stage-4 configuration, the full 18-run 2P/3P/4P matrix, solo `-level_09` and a menu boot all exit rc=0 with no crash log; the guard fired in 3 of the 8 repeats (so the condition recurs and is now harmless). 3P renders three viewports with an empty fourth quadrant (as on the N64) and 4P a clean 2x2. **Confidence: [H]** for the crash and the guard; [M] on the upstream glyph cause.
|
||||
|
||||
**Generalisable.** Any importer that divides by a tile field must tolerate 0; the game emits zero-sized loads legitimately. `0xc0000094` is a divide by zero, `0xc0000005` an access violation, `0xc0000409` a fastfail.
|
||||
|
||||
|
||||
|
||||
## D464 — GE_MEMPREDZONE: red-zone checker for the bump allocator (diagnostic tool, 2026-10-01)
|
||||
|
||||
**Falsifier (written before any run).** Re-introduce the D462 bug in a throwaway, uncommitted edit (`tmp = 0x14;` in `src/game/vtxstore.c` instead of `sizeof(struct unk_09B7A0_struct_parent)`), build, run `GE_MEMPREDZONE=1 GE_QUITFRAME=300 ./data/ge007.x86_64.exe -level_09`. Expected: at least one `MEMPREDZONE: HIT` whose `caller=` resolves (addr2line) to `vtxstore.c`'s allocation of the two parent tables, with `size` = count*0x14. If no hit names vtxstore, the checker is not trustworthy.
|
||||
|
||||
**What was built.** `src/memp.c` under `#ifdef PORT`, gated by `GE_ENVFLAG("GE_MEMPREDZONE")` (cached; one branch per allocation when unset, the original allocator body is kept verbatim as `mempAllocBytesInBankRaw`). When set, every allocation with room is padded by a 64-byte trailer (pattern `0xA5 + 13*i`), recorded in an 8192-entry side table (block, size, trailer, id, caller = `__builtin_return_address(0)` of `mempAllocBytesInBank` / `mempAllocPackedBytesInBank`, bank). Alignment is unchanged (pos advances 64 more). `mempRedzoneCheck(why)` logs one `[ERROR] MEMPREDZONE: HIT id= bank= size= block= rz_first_off= changed=n/64 caller=<raw addr>` per damaged entry and one `[INFO]` summary per check. Check points: `mempResetBank`, `mempNullNextEntryInBank` (level unload; the bank's entries are dropped afterwards), `videoEndFrame` every 60th frame, and the orderly-quit path (`videoHostExitIfRequested`, render parked). Resolve a caller with `addr2line -f -C -i -e build-pc/ge007.x86_64.exe 0x<addr>` (PE image base 0x140000000 is already in the logged address). Covers all banks (the bank is derived from the block address). No `src/game` edit.
|
||||
|
||||
**Bump-pointer users audited.** `mempAddEntryOfSizeToBank` (callers: `fileIndexLoadToBank`, `fileSetSize` in ob.c) shrinks the *last* allocation, and `fileIndexLoadToBank` allocates the whole remaining bank (`mempGetBankSizeLeft`) then shrinks. A trailer cannot fit on the whole-bank allocation, so that block is left unpadded (`untracked(no room)` in the summary) and gains its trailer when the shrink leaves room; `mempAddEntryOfSizeToBank` re-places the trailer for tracked blocks (`newsize + 64` accounting). Nothing else computes sizes from pointer differences or reads `prevpos` (grep: only memp.c). `bg.c:612` only logs pool pointers. No bank had to be excluded.
|
||||
|
||||
**Falsifier result: PASS.** With `tmp = 0x14` temporarily restored in `vtxstore.c` (uncommitted; reverted, `git diff -- src` empty afterwards), `GE_MEMPREDZONE=1 GE_QUITFRAME=300 -level_09` reported `HIT id=21 bank=4 size=400 ... caller=0x1400d7c85` and `HIT id=23 size=800 ... caller=0x1400d7cbc`; addr2line resolved both to `sub_GAME_7F09B820`, `src/game/vtxstore.c:117` and `:119` (the two table allocations). So the checker catches the D462 class on a normal solo load. (Hit trailer damage was only 6/64 bytes: the overrun writes -1/0 into a few fields, so a pattern-equal byte can hide, but any real overrun beyond the first stride row hits.)
|
||||
|
||||
**Sweep (build with the D462 fix restored, flag set, 37 launches).** All 21 solo levels (`-level_XX`, 420 frames; names/numbers from `kSoloLevels`): rc=0, no `ge007.crash.log`, **0 red-zone hits**, tracked blocks 126 (Cuba) to 788 (Jungle), 19..59 untracked-no-room. Menu boot (900 frames): rc=0, 0 hits. **The MP matrix did not exercise multiplayer:** `GE_STARTMP`/`GE_MPVIRT` exist only on `feat/mp-splitscreen-99` (`src/game/lv.c`), not on `release/v0.4.1`, so those 15 runs were menu boots (identical 84-tracked-block logs). The split-screen branch is too far behind (pre-D453, conflicts in memp.h) to cherry-pick onto; the launch cap (40) was already nearly spent. **Owed:** rebuild the checker onto the split-screen branch and run the 2P/3P/4P matrix there (the D461 victim was an MP-only `equipmaxitems`-sized path, and MP stage allocations differ).
|
||||
|
||||
**Verdict.** On the solo levels and the front end, no remaining allocation overrun is visible at the 64-byte granularity, including after D461/D462. Not covered: overruns landing in a block's *interior* neighbour that never reaches the trailer (stride error smaller than the trailer is caught; one far beyond 64 bytes skips the trailer and corrupts the next block's data, but still damages the next block's own trailer only if it reaches it), writes into the unpadded whole-bank allocations, blocks beyond the 8192-entry table (none reached it), under-runs (no leading zone). **Confidence: [H]** that the tool detects the D462 class; [M] that solo levels are clean (420 frames only, load-time allocations; runtime allocations after frame 420 not covered).
|
||||
+41
-1
@@ -1016,7 +1016,10 @@ for (s = SP_LEVEL_EGYPT; s >= SP_LEVEL_DAM /* == 0 */; s--) // never ends
|
||||
|
||||
- Symptom: a silent hang (kernel-heartbeat stall, no crash log) inside a
|
||||
loop over an enum range; the counter holds a huge value in gdb.
|
||||
- Instances: **D142**; `LEVEL_SOLO_SEQUENCE` in
|
||||
- Instances: **D417** (`enum PROP`: `getPropForHeldItem()` returns -1 for the
|
||||
fist and callers test `prop >= 0` -> unsigned enum lets -1 through ->
|
||||
`modelLoad(-1)` crash in any 2P+ stage; sentinel `PROP__PORT_SIGNED`);
|
||||
**D142**; `LEVEL_SOLO_SEQUENCE` in
|
||||
`fileGetHighestStageDifficultyCompletedForFolder` froze the SELECT FILE
|
||||
screen. `DIFFICULTY` was already safe (`DIFFICULTY_MULTI = -1`).
|
||||
- Fix: add a never-used negative sentinel enumerator under `#ifdef PORT`
|
||||
@@ -1204,6 +1207,11 @@ and turns every such overrun into a fatal `*** stack smashing detected ***`
|
||||
the identical overflow is cosmetic (the JPN-cache pointer is valid memory in
|
||||
any language); the crash severity is a PC artifact of that global being NULL
|
||||
outside Japanese mode.
|
||||
- Instance (D420): `mpmenu.c` `mp_watch_menu_display` `char rankbuffer[4]`
|
||||
receives "Rank: 1st" / "P<n> KILLS"; GCC puts `scores[4]` next to it, so
|
||||
the MP Scores page printed the rank text's bytes as numbers (1932599354 =
|
||||
": 1s"). Tell: a displayed number whose hex is printable ASCII from a
|
||||
neighbouring string. Widened to `[64]` under `#ifdef PORT`.
|
||||
- Fix policy: **`-fno-stack-protector` globally** (`CMakeLists.txt`) so Linux
|
||||
matches the N64 and MinGW builds; the code is byte-matched to the ROM and
|
||||
cannot be "fixed" per site without diverging. Where a case is cleanly
|
||||
@@ -1350,6 +1358,27 @@ darkening from a nonzero `ENV_ALPHA` LERP factor is very likely faithful
|
||||
(possibly authored/original-dev-intentional tinting), not a bug — do not
|
||||
"fix" it without an N64/1964-GEPD reference screenshot proving otherwise.
|
||||
|
||||
## D16. Split-screen: port-side "one full-screen viewport" assumptions break 2P+ (D418)
|
||||
|
||||
Two traps from the #99 split-screen bring-up:
|
||||
|
||||
- **Safe-area crop (`gfx_adjust_viewport_or_scissor`) caches the LAST SP
|
||||
viewport's Y range and stretches it over the whole window.** Right for the
|
||||
solo 220-line viewport, wrong for 2P+ (each player's viewport is a sub-rect):
|
||||
every player's view filled the window and the last-drawn (order is shuffled
|
||||
per frame, `get_nth_player_from_shuffled`) covered the rest -> alternating
|
||||
full-screen flicker. Fix = `gfx_set_split_screen()`. Any new port feature that
|
||||
assumes "the viewport is the screen" needs the same 2P+ gate.
|
||||
- **Per-frame port hooks must sit on the path frames really take.** The
|
||||
scheduler calls `gfx_run` directly (`port/src/libultra.c`), NOT through
|
||||
`videoSubmitCommands`; a hook placed only in the latter silently never ran.
|
||||
Log once on the first state change when adding such a hook.
|
||||
|
||||
Debug recipe that found it: capture consecutive frames after the stage starts
|
||||
(GE_PCDUMP range, or the harness `GE_STARTMP=<players>[,<stageIdx>]` +
|
||||
`GE_MPVIRT=<players-1>` to start a match with no one at the menu) and look at
|
||||
them — the symptom was obvious in one montage.
|
||||
|
||||
## D17. Un-stubbing a `-1` sentinel re-activates every dormant consumer of the value (D431/D434)
|
||||
|
||||
A port stub that forces a value to `-1` (D154 `texnum`, D135) can hide latent layout bugs in its other readers. When restoring a real value, grep ALL readers of the field first and check each for raw-layout reads and table-bounds assumptions. Instance: D431 made light-fixture hits return real image ids, which reached `chrprop.c`/`propobj.c` reading `((u8*)&g_Textures[n])[0] & 0xf` as a hit type -- on PC byte 0 is the low byte of `dataoffset` (LE bitfield layout, D67), so it indexed `g_HitTypeSounds[13]` with 0..15. Use the `.hitTexture`/`.hitSound` bitfields (as `chr.c` does).
|
||||
@@ -1368,6 +1397,11 @@ A port stub that forces a value to `-1` (D154 `texnum`, D135) can hide latent la
|
||||
`[Input] MouseEnabled = 0` in the pinned test ini. Script "frames" were
|
||||
about one per rendered frame on the file select screen; calibrate with a
|
||||
log, not by assumption.
|
||||
- **Per-slot headless scripts: `GE_INPUTSCRIPT_P1..P3` drive seats 2-4.**
|
||||
Same entry syntax as `GE_INPUTSCRIPT` (slot 0); "frame" counts that
|
||||
controller's reads. Mouse tokens and CHOLD/CREL are ignored on slots 1-3
|
||||
(one warning per script). Recipe for a scripted 4P split-screen run:
|
||||
`GE_STARTMP=4,<stage> GE_MPVIRT=3 GE_INPUTSCRIPT_P1=... GE_INPUTSCRIPT_P2=... GE_INPUTSCRIPT_P3=... GE_QUITFRAME=...`.
|
||||
- **`textMeasure` height is 0 without a trailing newline (D343).** It only
|
||||
counts completed lines. Measure `"text\n"` when you need a height (front.c's
|
||||
folder text does this); the width is unaffected.
|
||||
@@ -1770,4 +1804,10 @@ A port stub that forces a value to `-1` (D154 `texnum`, D135) can hide latent la
|
||||
(different Windows dirs). Never trust `command -v cmake` for the MSYS2
|
||||
toolchain either: a pip-installed cmake can shadow it (2026-09-30).
|
||||
|
||||
### Poll-thread input must never re-point `g_CurrentPlayer` (D419)
|
||||
|
||||
`inputComputePad` runs on the scheduler thread (`joyPoll`), a real pthread, not the game thread. Any save/swap/restore of `g_CurrentPlayer`/`player_num`/`g_playerPerm` from there races the game tick's own `set_cur_player`: the restore can leave the game thread on the wrong player and it then spins in list walks that compare against `g_CurrentPlayer->...` (seen: `bondinvCycleForward` infinite loop after a split-screen respawn). Resolve the owning player explicitly (`g_playerPointers[slot]`) and touch its fields directly; for game functions that take no player argument, present a native button bit instead of calling them.
|
||||
|
||||
**A1 cross-tag (D455, 2026-09-30):** an exe-static address (array/global/function in `.data`/`.rodata`/`.text`) held in a `u32`/`s32` truncates on Windows (image base `0x140000000`; Linux 0x20000000 hides it). Binary tell: `mov r64,[rip+.refptr.X]` ... `mov r32,r32` before the pointer is used or passed. The address load may precede a `jmp` join, so an adjacent-instruction scan misses it; follow jumps. A census of the whole exe found only the D454 site; `_*Segment*` linker symbols are ROM constants (`.set` in `romassets_u.s`), not exe addresses, and are benign in `(u32)` casts.
|
||||
|
||||
**`GE_MEMPREDZONE=1` (D464): catch allocator overruns deterministically.** Set it before any run to pad every `mempAlloc*` block with a 64-byte pattern trailer and check at level unload, every 60 frames and on orderly quit. A hit prints `MEMPREDZONE: HIT ... caller=0x<addr>`; resolve with `addr2line -f -C -i -e build-pc/ge007.x86_64.exe 0x<addr>` to the allocation site, then compare its size expression with `sizeof` of how the buffer is indexed (an N64 literal stride vs a widened struct is the D461/D462 class). Zero hits is only "no overrun reached the trailer"; whole-bank allocations are unpadded until shrunk. Unset = one cached branch per allocation.
|
||||
@@ -50,6 +50,7 @@ void gfx_set_fix_mip_textures(int on);
|
||||
void gfx_set_detail_base_tile(int on); /* D236: sample the base image of a DETAIL binding */
|
||||
void gfx_set_wrap_fix(int on);
|
||||
void gfx_set_anisotropy_level(int level); /* 1 = off; clamped to GL max */
|
||||
void gfx_set_split_screen(int on); /* D416: 2+ player stage running -> viewports are sub-rects; safe-area crop must not remap them */
|
||||
void gfx_set_safe_area_crop(int on); /* crop the N64 TV-overscan safe-area margin instead of showing it as black bars */
|
||||
/* On-window pixel rect (top-left origin) the full VI logical canvas (0,0)-
|
||||
* (SCREEN_WIDTH, SCREEN_HEIGHT) currently maps to, honoring the safe-area
|
||||
|
||||
+36
-3
@@ -262,6 +262,17 @@ extern "C" void gfx_set_safe_area_crop(int on) {
|
||||
g_safe_area_crop_enabled = !!on;
|
||||
}
|
||||
|
||||
/* D416: the crop above stretches the LAST set viewport's Y range over the
|
||||
* whole window. Correct for one full-screen viewport; in split-screen every
|
||||
* player's (half/quarter) viewport got stretched to fill the window, so the
|
||||
* last-drawn player covered the others (and the shuffled draw order made the
|
||||
* winner alternate frame to frame). While a 2+ player stage is running the
|
||||
* N64's own split layout is used unmodified. */
|
||||
static bool g_split_screen = false;
|
||||
extern "C" void gfx_set_split_screen(int on) {
|
||||
g_split_screen = !!on;
|
||||
}
|
||||
|
||||
/* D447: output rect (Video.AspectMode = Original). 0 = fill the window (the
|
||||
* historical behaviour, bit-identical). >0 = letter/pillarbox the frame to
|
||||
* exactly this aspect, centred; set once per frame by the port layer before
|
||||
@@ -1037,6 +1048,28 @@ static void import_texture(int i, int tile, bool importReplacement) {
|
||||
const uint32_t tex_flags = loaded_texture.tex_flags;
|
||||
const uint8_t palette_index = rdp.texture_tile[tile].palette;
|
||||
|
||||
/* D463: a SETTILE with line=0 makes every importer divide by zero
|
||||
* (height = size_bytes / line_size_bytes -- 0xc0000094 integer-divide on
|
||||
* Windows x64; the crash PC was import_texture_i8). The game emits this
|
||||
* legitimately: textrelated.c's outlined-text path renders ANY byte
|
||||
* < 0x21 through chars[*text - 0x21], a zeroed fontchar (w=h=0,
|
||||
* pixeldata=NULL), and gDPLoadTextureBlock(width=0) then expands to
|
||||
* gsDPTile(line=0) + a zero-area gSPTextureRectangle. The N64 RDP draws
|
||||
* nothing for a zero-area rect, so the decomp is correct -- fast3d must
|
||||
* tolerate it. Skip the import entirely (no cache entry, no upload);
|
||||
* the degenerate draw rasterizes no fragments, so whatever stays bound
|
||||
* on this unit is never sampled. Nonzero line sizes take the exact
|
||||
* original path below. */
|
||||
if (rdp.texture_tile[tile].line_size_bytes == 0) {
|
||||
static int d463_warned = 0;
|
||||
if (d463_warned < 8)
|
||||
sysLogPrintf(LOG_NOTE, "D463: texrect on zero-line tile -- import skipped "
|
||||
"(tile=%d fmt=%u siz=%u tmem=%u)",
|
||||
tile, fmt, siz, rdp.texture_tile[tile].tmem);
|
||||
d463_warned++;
|
||||
return;
|
||||
}
|
||||
|
||||
// D74: only fall back when the tmem slot was never written by a load
|
||||
// command. The old `rdp.tex_lod && tile >= first+detail` branch also
|
||||
// overwrote valid gDPLoadBlock data with line*tile.height, which (a) dropped
|
||||
@@ -2831,7 +2864,7 @@ static void gfx_adjust_viewport_or_scissor(XYWidthHeight* area, bool preserve_as
|
||||
// g_gpSafeTop already plays the exact role SCREEN_HEIGHT plays below
|
||||
// (both are the bottom-up Y value of the mapped region's TOP edge) --
|
||||
// this reduces to the untouched original formula when crop is off.
|
||||
const bool crop = g_safe_area_crop_enabled && g_gpSafeHeight > 0.0f;
|
||||
const bool crop = g_safe_area_crop_enabled && !g_split_screen && g_gpSafeHeight > 0.0f;
|
||||
const float safeTop = crop ? g_gpSafeTop : (float)SCREEN_HEIGHT;
|
||||
const float safeHeight = crop ? g_gpSafeHeight : (float)SCREEN_HEIGHT;
|
||||
const float ratioY = gfx_current_dimensions.height / safeHeight;
|
||||
@@ -2849,8 +2882,8 @@ static void gfx_adjust_viewport_or_scissor(XYWidthHeight* area, bool preserve_as
|
||||
// TV-overscan margin) and fold it into the same toggle: trim the same
|
||||
// fixed 1-unit margin from both edges rather than trying to force
|
||||
// content to reach a boundary it may never actually be drawn to.
|
||||
const float safeLeft = g_safe_area_crop_enabled ? 1.0f : 0.0f;
|
||||
const float safeWidth = g_safe_area_crop_enabled ? (float)SCREEN_WIDTH - 2.0f : (float)SCREEN_WIDTH;
|
||||
const float safeLeft = (g_safe_area_crop_enabled && !g_split_screen) ? 1.0f : 0.0f;
|
||||
const float safeWidth = (g_safe_area_crop_enabled && !g_split_screen) ? (float)SCREEN_WIDTH - 2.0f : (float)SCREEN_WIDTH;
|
||||
const float ratioX = gfx_current_dimensions.width / safeWidth;
|
||||
|
||||
float x1 = (area->x - safeLeft) * ratioX;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "input.h"
|
||||
|
||||
extern "C" void videoRequestQuit(const char *why); // port/src/video.c (D344)
|
||||
extern "C" void inputRequestRescan(void); // port/src/input.c (D450)
|
||||
|
||||
static SDL_Window* wnd;
|
||||
static SDL_GLContext ctx;
|
||||
@@ -339,6 +340,10 @@ static void gfx_sdl_handle_events(void) {
|
||||
case SDL_MOUSEWHEEL:
|
||||
inputPostWheel(event.wheel.y); // weapon cycle
|
||||
break;
|
||||
case SDL_CONTROLLERDEVICEADDED: // D450: this pump and video.c's drain the same
|
||||
case SDL_CONTROLLERDEVICEREMOVED: // SDL queue; request a rescan, never rescan here
|
||||
inputRequestRescan();
|
||||
break;
|
||||
case SDL_WINDOWEVENT:
|
||||
if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
||||
SDL_GL_GetDrawableSize(wnd, &window_width, &window_height);
|
||||
|
||||
@@ -87,6 +87,7 @@ int inputMenuPointerLive(void);
|
||||
|
||||
/* Re-enumerate gamepads after a hotplug (SDL_CONTROLLERDEVICEADDED/REMOVED). */
|
||||
void inputRescanPads(void);
|
||||
void inputRequestRescan(void); /* D450: from either SDL event pump; consumed by inputUpdate() */
|
||||
|
||||
/* D401: Rumble Pak -> real gamepad haptics. The osMotor* shims in
|
||||
* libultra.c (game's src/joy.c rumble state machine) route here.
|
||||
|
||||
+555
-208
File diff suppressed because it is too large.
Load diff
@@ -59,6 +59,7 @@
|
||||
|
||||
/* fast3d (C++): the software RSP entry point. */
|
||||
extern void gfx_run(Gfx *commands);
|
||||
extern void videoSyncSplitScreen(void); /* D416 */
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
/* Globals the game expects to exist (normally set by osInitialize). */
|
||||
@@ -1327,6 +1328,7 @@ void osSpTaskStartGo(OSTask *t)
|
||||
/* Graphics task: run the software RSP on the display list. */
|
||||
uint64_t t0 = sysGetMicroseconds();
|
||||
videoStartFrame();
|
||||
videoSyncSplitScreen();
|
||||
gfx_run((Gfx *)t->t.data_ptr);
|
||||
videoEndFrame();
|
||||
g_lastFrameUs = sysGetMicroseconds();
|
||||
|
||||
+25
-1
@@ -950,6 +950,10 @@ static void videoHostExitIfRequested(void)
|
||||
SDL_AtomicGet(&s_parked) ? "parked"
|
||||
: (SDL_AtomicGet(&s_inFrame) ? "STILL IN FRAME (timeout)" : "idle"),
|
||||
(unsigned)(SDL_GetTicks() - start));
|
||||
{
|
||||
extern void mempRedzoneCheck(const char *why);
|
||||
mempRedzoneCheck("orderly quit");
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
|
||||
@@ -979,6 +983,11 @@ void videoStartFrame(void)
|
||||
}
|
||||
/* D344: enter the frame first, then check for a quit (see above). */
|
||||
SDL_AtomicSet(&s_inFrame, 1);
|
||||
/* D464: GE_MEMPREDZONE periodic stage-bank red-zone check (cached env). */
|
||||
if (frames % 60 == 59) {
|
||||
extern void mempRedzoneCheck(const char *why);
|
||||
mempRedzoneCheck("videoEndFrame/60");
|
||||
}
|
||||
if (SDL_AtomicGet(&s_quitReq)) {
|
||||
videoRenderPark();
|
||||
}
|
||||
@@ -1082,7 +1091,7 @@ void videoPumpEvents(void)
|
||||
break;
|
||||
case SDL_CONTROLLERDEVICEADDED:
|
||||
case SDL_CONTROLLERDEVICEREMOVED:
|
||||
inputRescanPads();
|
||||
inputRequestRescan(); /* D450: the rescan runs on the pad-reading thread */
|
||||
break;
|
||||
case SDL_WINDOWEVENT:
|
||||
if (ev.window.event == SDL_WINDOWEVENT_CLOSE) {
|
||||
@@ -1116,11 +1125,26 @@ void videoPumpEvents(void)
|
||||
inputApplyMouseRequests();
|
||||
}
|
||||
|
||||
/* D416: split-screen viewports are sub-rects; tell fast3d so its safe-area
|
||||
* crop stays off. Called before every gfx_run (scheduler + videoSubmitCommands). */
|
||||
void videoSyncSplitScreen(void)
|
||||
{
|
||||
{
|
||||
extern s32 getPlayerCount(void);
|
||||
extern s32 lvlGetCurrentStageToLoad(void);
|
||||
static int lastSplit = -1;
|
||||
int split = getPlayerCount() >= 2 && lvlGetCurrentStageToLoad() != LEVELID_TITLE;
|
||||
if (split != lastSplit) { sysLogPrintf(LOG_NOTE, "D416 split=%d players=%d stage=%d", split, (int)getPlayerCount(), (int)lvlGetCurrentStageToLoad()); lastSplit = split; }
|
||||
gfx_set_split_screen(split);
|
||||
}
|
||||
}
|
||||
|
||||
void videoSubmitCommands(Gfx *cmds)
|
||||
{
|
||||
if (!initDone) {
|
||||
return;
|
||||
}
|
||||
videoSyncSplitScreen();
|
||||
gfx_run(cmds);
|
||||
}
|
||||
|
||||
|
||||
@@ -3126,6 +3126,14 @@ enum CCRMLUT
|
||||
//Canonical name and style "ai_destroyobj 2 : (def->obj == PROP_ELVIS_SAUCER)\n"
|
||||
typedef enum PROP
|
||||
{
|
||||
#ifdef PORT
|
||||
/* D417 (porting-notes D3): getPropForHeldItem() returns -1 for items with
|
||||
* no held model (fist), and player.c tests `prop >= 0`. With all-non-negative
|
||||
* enumerators GCC makes this enum unsigned so -1 passed the test and
|
||||
* modelLoad(-1) crashed in 2P. Never-used negative sentinel forces it signed;
|
||||
* PROP_ALARM1 stays 0, sizeof stays 4, no stored value changes. */
|
||||
PROP__PORT_SIGNED = -1,
|
||||
#endif
|
||||
PROP_ALARM1, /* Beta Alarm / Default Multi Weapon */
|
||||
PROP_ALARM2, /* Alarm */
|
||||
PROP_EXPLOSIONBIT, /* White Pyramid (Explosion Bit) */
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <math.h>
|
||||
#ifdef PORT
|
||||
#include <stdlib.h>
|
||||
#include <string.h> /* D416 GE_STARTMP harness: strchr */
|
||||
#endif
|
||||
#include <os_extension.h>
|
||||
#include <PR/libaudio.h>
|
||||
@@ -396,6 +397,31 @@ void lvlStageLoad(s32 stage)
|
||||
* Not compiled without PORT; no effect unless the env var is set. */
|
||||
{
|
||||
const char *sm = getenv("GE_STARTMENU");
|
||||
/* D416 harness: GE_STARTMP="<players>[,<mpStageIdx>]" starts a local
|
||||
* multiplayer match with the menu's own default options (what
|
||||
* MP options -> START does), so split-screen can be tested with no
|
||||
* one at the menu. Needs GE_MPVIRT=<n-1> for the controller count. */
|
||||
const char *smp = getenv("GE_STARTMP");
|
||||
if (smp && *smp) {
|
||||
extern s32 MP_stage_selected;
|
||||
extern struct mp_stage_setup multi_stage_setups[];
|
||||
extern void init_mp_options_for_scenario(s32 numplayers);
|
||||
extern s32 selected_num_players;
|
||||
s32 np = (s32)strtol(smp, NULL, 0);
|
||||
const char *comma = strchr(smp, ',');
|
||||
if (np < 2) np = 2;
|
||||
if (np > 4) np = 4;
|
||||
gamemode = GAMEMODE_MULTI;
|
||||
init_mp_options_for_scenario(np);
|
||||
if (comma) MP_stage_selected = (s32)strtol(comma + 1, NULL, 0);
|
||||
selected_stage = multi_stage_setups[MP_stage_selected].stage_id;
|
||||
briefingpage = -1;
|
||||
prev_keypresses = TRUE;
|
||||
maybe_is_in_menu = TRUE;
|
||||
menu_update = MENU_RUN_STAGE;
|
||||
osSyncPrintf("GE_STARTMP: players=%d mpstage=%d stage_id=%d\n",
|
||||
np, MP_stage_selected, selected_stage);
|
||||
} else
|
||||
if (sm && *sm) {
|
||||
s32 want = (s32)strtol(sm, NULL, 0);
|
||||
const char *pg = getenv("GE_STARTMENU_PAGE");
|
||||
|
||||
@@ -1065,7 +1065,16 @@ Gfx *mp_watch_menu_display(Gfx *gdl)
|
||||
#else
|
||||
s32 h2;
|
||||
#endif
|
||||
#ifdef PORT
|
||||
// D420 (D8 class): the decomp's [4] receives "Rank: 1st" (10 bytes incl.
|
||||
// NUL) and "P<n> KILLS"/"P<n> LOSSES"; GCC places scores[] right after
|
||||
// it, so the rank text overwrote the Scores page values ("1932599354" =
|
||||
// ": 1s", "116" = 't'). Pure local stack scratch, zero ABI role; N64
|
||||
// build keeps the decomp size.
|
||||
char rankbuffer[64];
|
||||
#else
|
||||
char rankbuffer[4];
|
||||
#endif
|
||||
s32 two_player_x_offset;
|
||||
char *text;
|
||||
s32 scores[4];
|
||||
|
||||
+196
-2
@@ -175,7 +175,11 @@ void mempSetBankStarts(s32 poolSizes[MEMPOOL_COUNT+1])
|
||||
}
|
||||
|
||||
|
||||
#ifdef PORT
|
||||
static void *mempAllocBytesInBankRaw(u32 bytes, u8 poolnum)
|
||||
#else
|
||||
void *mempAllocBytesInBank(u32 bytes, u8 poolnum)
|
||||
#endif
|
||||
{
|
||||
/*
|
||||
* Retain this address expression. Using
|
||||
@@ -220,7 +224,11 @@ void *mempAllocBytesInBank(u32 bytes, u8 poolnum)
|
||||
|
||||
needmemallocation = TRUE;
|
||||
|
||||
#ifdef PORT
|
||||
return mempAllocBytesInBankRaw(bytes, MEMPOOL_PERMANENT);
|
||||
#else
|
||||
return mempAllocBytesInBank(bytes, MEMPOOL_PERMANENT);
|
||||
#endif
|
||||
}
|
||||
|
||||
nulled_mempLoopAllMemBanks();
|
||||
@@ -237,6 +245,159 @@ void *mempAllocBytesInBank(u32 bytes, u8 poolnum)
|
||||
}
|
||||
|
||||
|
||||
#ifdef PORT
|
||||
/* D464: GE_MEMPREDZONE=1 -- red-zone checker for the bump allocator.
|
||||
* Every allocation that has room is padded with MEMP_RZ_SIZE trailing
|
||||
* pattern bytes and recorded in a side table; mempRedzoneCheck() reports
|
||||
* blocks whose trailer was scribbled (an overrun: N64-sized allocation
|
||||
* indexed at the larger PC stride, the D461/D462 class). The returned
|
||||
* pointer, 16-byte alignment and pool bookkeeping are otherwise unchanged
|
||||
* (pos just advances RZ more, a multiple of 16). With the flag unset, the
|
||||
* only cost is one cached branch per allocation. Game-thread design; the
|
||||
* periodic check from the video hook can race a resize and report a
|
||||
* transient false positive, so confirm a hit by re-running. */
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include "envflag.h"
|
||||
#include "system.h"
|
||||
|
||||
#define MEMP_RZ_SIZE 64
|
||||
#define MEMP_RZ_MAX 8192
|
||||
|
||||
typedef struct MempRzEntry {
|
||||
u8 *block;
|
||||
u8 *rz;
|
||||
u32 size;
|
||||
u32 id;
|
||||
void *caller;
|
||||
u8 bank;
|
||||
u8 reported;
|
||||
} MempRzEntry;
|
||||
|
||||
static MempRzEntry s_rz[MEMP_RZ_MAX];
|
||||
static u32 s_rzCount;
|
||||
static u32 s_rzNextId;
|
||||
static u32 s_rzNoRoom; /* allocations that had no room for a red zone */
|
||||
static int s_rzFullSaid;
|
||||
|
||||
static u8 mempRzPat(u32 i) { return (u8)(0xA5u + i * 13u); }
|
||||
|
||||
static void mempRzFill(u8 *rz)
|
||||
{
|
||||
u32 i;
|
||||
for (i = 0; i < MEMP_RZ_SIZE; i++) rz[i] = mempRzPat(i);
|
||||
}
|
||||
|
||||
static int mempRzBankOf(u8 *p)
|
||||
{
|
||||
int i;
|
||||
for (i = MEMPOOL_MF; i < MEMPOOL_COUNT; i++) {
|
||||
if (g_mempPools[i].start != g_mempPools[i].end &&
|
||||
p >= g_mempPools[i].start && p < g_mempPools[i].end) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void mempRzRecord(u8 *block, u32 size, void *caller)
|
||||
{
|
||||
MempRzEntry *e;
|
||||
u32 id = s_rzNextId++;
|
||||
if (s_rzCount >= MEMP_RZ_MAX) {
|
||||
if (!s_rzFullSaid) {
|
||||
s_rzFullSaid = 1;
|
||||
sysLogPrintf(LOG_WARNING, "MEMPREDZONE: side table full (%d); later allocations are padded but not tracked", MEMP_RZ_MAX);
|
||||
}
|
||||
return;
|
||||
}
|
||||
e = &s_rz[s_rzCount];
|
||||
e->block = block; e->rz = block + size; e->size = size; e->id = id;
|
||||
e->caller = caller; e->bank = (u8)mempRzBankOf(block); e->reported = 0;
|
||||
s_rzCount++;
|
||||
}
|
||||
|
||||
static int mempRzFindLast(u8 *block)
|
||||
{
|
||||
int i;
|
||||
for (i = (int)s_rzCount - 1; i >= 0; i--) {
|
||||
if (s_rz[i].block == block) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void mempRzDropBank(u8 bank)
|
||||
{
|
||||
u32 i, j = 0;
|
||||
for (i = 0; i < s_rzCount; i++) {
|
||||
if (s_rz[i].bank != bank) s_rz[j++] = s_rz[i];
|
||||
}
|
||||
s_rzCount = j;
|
||||
}
|
||||
|
||||
void mempRedzoneCheck(const char *why)
|
||||
{
|
||||
u32 i, k, nbad = 0;
|
||||
if (!GE_ENVFLAG("GE_MEMPREDZONE")) return;
|
||||
for (i = 0; i < s_rzCount; i++) {
|
||||
MempRzEntry *e = &s_rz[i];
|
||||
u32 first = 0xFFFFFFFFu, changed = 0;
|
||||
if (e->reported) continue;
|
||||
for (k = 0; k < MEMP_RZ_SIZE; k++) {
|
||||
if (e->rz[k] != mempRzPat(k)) {
|
||||
if (first == 0xFFFFFFFFu) first = k;
|
||||
changed++;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
e->reported = 1;
|
||||
nbad++;
|
||||
sysLogPrintf(LOG_ERROR,
|
||||
"MEMPREDZONE: HIT id=%u bank=%u size=%u block=%p rz_first_off=%u changed=%u/%d caller=%p (%s)",
|
||||
e->id, (unsigned)e->bank, e->size, (void *)e->block, first, changed, MEMP_RZ_SIZE, e->caller,
|
||||
why ? why : "?");
|
||||
}
|
||||
}
|
||||
sysLogPrintf(LOG_INFO, "MEMPREDZONE: check '%s': %u tracked, %u new hit(s), %u untracked(no room)",
|
||||
why ? why : "?", s_rzCount, nbad, s_rzNoRoom);
|
||||
}
|
||||
|
||||
static void *mempAllocImpl(u32 bytes, u8 poolnum, void *caller)
|
||||
{
|
||||
MemoryPool *pool = &g_mempPools[poolnum];
|
||||
u8 *p;
|
||||
|
||||
if (!GE_ENVFLAG("GE_MEMPREDZONE")) {
|
||||
return mempAllocBytesInBankRaw(bytes, poolnum);
|
||||
}
|
||||
if (pool->pos != NULL && pool->pos <= pool->end &&
|
||||
pool->pos + bytes + MEMP_RZ_SIZE <= pool->end) {
|
||||
p = (u8 *)mempAllocBytesInBankRaw(bytes + MEMP_RZ_SIZE, poolnum);
|
||||
mempRzFill(p + bytes);
|
||||
mempRzRecord(p, bytes, caller);
|
||||
return p;
|
||||
}
|
||||
s_rzNoRoom++;
|
||||
return mempAllocBytesInBankRaw(bytes, poolnum);
|
||||
}
|
||||
|
||||
static void mempRzResized(u8 *block, u32 newsize, int idx)
|
||||
{
|
||||
mempRzFill(block + newsize);
|
||||
if (idx >= 0) {
|
||||
s_rz[idx].size = newsize;
|
||||
s_rz[idx].rz = block + newsize;
|
||||
s_rz[idx].reported = 0;
|
||||
} else {
|
||||
mempRzRecord(block, newsize, __builtin_return_address(0));
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((noinline)) void *mempAllocBytesInBank(u32 bytes, u8 poolnum)
|
||||
{
|
||||
return mempAllocImpl(bytes, poolnum, __builtin_return_address(0));
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Resize the most recent allocation in a pool without moving it.
|
||||
*/
|
||||
@@ -245,6 +406,10 @@ MEMP_ADD_ENTRY_RESULT mempAddEntryOfSizeToBank(void *allocation, s32 newsize, u8
|
||||
MemoryPool *pool;
|
||||
s32 origsize;
|
||||
s32 growsize;
|
||||
#ifdef PORT
|
||||
int rzIdx = -1;
|
||||
int rzNew = 0;
|
||||
#endif
|
||||
|
||||
if (needmemallocation && allocation == g_mempPools[MEMPOOL_PERMANENT].prevpos)
|
||||
{
|
||||
@@ -265,11 +430,27 @@ MEMP_ADD_ENTRY_RESULT mempAddEntryOfSizeToBank(void *allocation, s32 newsize, u8
|
||||
}
|
||||
|
||||
origsize = pool->pos - pool->prevpos;
|
||||
#ifdef PORT
|
||||
/* D464: a tracked block carries a trailer; keep it around the new size.
|
||||
* An untracked block (whole-bank allocation, shrunk right after the
|
||||
* load) gains one when the shrink leaves room for it. */
|
||||
if (GE_ENVFLAG("GE_MEMPREDZONE")) {
|
||||
rzIdx = mempRzFindLast((u8 *)allocation);
|
||||
if (rzIdx >= 0 || (origsize >= newsize + MEMP_RZ_SIZE)) {
|
||||
rzNew = 1;
|
||||
}
|
||||
}
|
||||
growsize = newsize + (rzNew ? MEMP_RZ_SIZE : 0) - origsize;
|
||||
#else
|
||||
growsize = newsize - origsize;
|
||||
#endif
|
||||
|
||||
if (growsize <= 0)
|
||||
{
|
||||
pool->pos += growsize;
|
||||
#ifdef PORT
|
||||
if (rzNew) mempRzResized((u8 *)allocation, newsize, rzIdx);
|
||||
#endif
|
||||
return MEMP_ADD_ENTRY_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -286,6 +467,9 @@ MEMP_ADD_ENTRY_RESULT mempAddEntryOfSizeToBank(void *allocation, s32 newsize, u8
|
||||
}
|
||||
|
||||
pool->pos += growsize;
|
||||
#ifdef PORT
|
||||
if (rzNew) mempRzResized((u8 *)allocation, newsize, rzIdx);
|
||||
#endif
|
||||
return MEMP_ADD_ENTRY_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -311,19 +495,29 @@ s32 mempGetBankSizeLeft(u8 bank) {
|
||||
|
||||
// Last three bits contains the bank, the rest contains the size.
|
||||
#ifdef PORT
|
||||
void *mempAllocPackedBytesInBank(u32 sizeandbank) { /* D453: was u32 */
|
||||
__attribute__((noinline)) void *mempAllocPackedBytesInBank(u32 sizeandbank) { /* D453: was u32 */
|
||||
return mempAllocImpl((sizeandbank >> 3), (sizeandbank & 7), __builtin_return_address(0));
|
||||
}
|
||||
#else
|
||||
u32 mempAllocPackedBytesInBank(u32 sizeandbank) {
|
||||
#endif
|
||||
return mempAllocBytesInBank((sizeandbank >> 3), (sizeandbank & 7));
|
||||
}
|
||||
#endif
|
||||
|
||||
void mempResetBank(u8 bank) {
|
||||
#ifdef PORT
|
||||
mempRedzoneCheck("mempResetBank");
|
||||
mempRzDropBank(bank);
|
||||
#endif
|
||||
g_mempPools[bank].prevpos = 0;
|
||||
g_mempPools[bank].pos = g_mempPools[bank].start;
|
||||
}
|
||||
|
||||
void mempNullNextEntryInBank(u8 bank) {
|
||||
#ifdef PORT
|
||||
mempRedzoneCheck("mempNullNextEntryInBank");
|
||||
mempRzDropBank(bank);
|
||||
#endif
|
||||
nulled_mempLoopAllMemBanks();
|
||||
if (g_mempPools[bank].pos != 0) {
|
||||
g_mempPools[bank].pos = 0;
|
||||
|
||||
@@ -61,6 +61,9 @@ void *mempAllocPackedBytesInBank(u32 param_1);
|
||||
#else
|
||||
u32 mempAllocPackedBytesInBank(u32 param_1);
|
||||
#endif
|
||||
#ifdef PORT
|
||||
void mempRedzoneCheck(const char *why); /* D464: GE_MEMPREDZONE */
|
||||
#endif
|
||||
void mempResetBank(u8 bank);
|
||||
void mempNullNextEntryInBank(u8 bank);
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ investigation artifacts.
|
||||
| `d43_emit.py` | Offline N64→PC model-file converter → RZ sidecar + `manifest.csv` (D50 / Plan B). Regen after any model-format change. |
|
||||
| `d43_convert.py` | Reference single-file model converter + full-512 layout/pointer validator (D43). |
|
||||
| `d69_emit.py` | Offline converter for stage `bg/*.seg` + `Tbg_*_stanZ` → concatenated sidecar (D69/D78–D82). |
|
||||
| `d88_emit.py` | Offline converter for per-level `Usetup*Z` stage-setup files → appended to the `pccg.bin` sidecar (D88). |
|
||||
| `d88_emit.py` | Offline converter for per-level `Usetup*Z` (solo, 21) and `Ump_setup*Z` (multiplayer, 13; D416) stage-setup files → appended to the `pccg.bin` sidecar (D88). |
|
||||
| `d88_propdefs.py` | The `propDefs` polymorphic-record stream N64→PC converter used by `d88_emit.py` (D88.4). |
|
||||
| `d69_emit.py` / `d88_emit.py` / `d43_emit.py` | Run all three (+ `d88_propdefs`) to rebuild `data/` sidecars from the ROM. |
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ for r in csv.reader(open("scripts/filelist.u.csv")):
|
||||
|
||||
man = {}
|
||||
for r in csv.reader(open(f"data/pccg-{REGION}/manifest.csv")):
|
||||
if r and r[0].startswith("Usetup"):
|
||||
if r and (r[0].startswith("Usetup") or r[0].startswith("Ump_setup")):
|
||||
man[r[0]] = (int(r[1]), int(r[2]))
|
||||
sidebin = open(f"data/pccg-{REGION}/pccg.bin", "rb").read()
|
||||
|
||||
|
||||
@@ -494,8 +494,11 @@ for line in open(TABLE):
|
||||
if m:
|
||||
table_names.append(m.group(1))
|
||||
|
||||
usetup_names = sorted(set(n for n in table_names
|
||||
if n.startswith("Usetup") and n.endswith("Z")))
|
||||
def is_setup_name(n):
|
||||
# Solo "Usetup*Z" and multiplayer "Ump_setup*Z" share the setup format.
|
||||
return (n.startswith("Usetup") or n.startswith("Ump_setup")) and n.endswith("Z")
|
||||
|
||||
usetup_names = sorted(set(n for n in table_names if is_setup_name(n)))
|
||||
|
||||
manifest = []
|
||||
chunks = []
|
||||
@@ -505,7 +508,7 @@ os.makedirs(OUT_DIR, exist_ok=True)
|
||||
bin_path = os.path.join(OUT_DIR, "pccg.bin")
|
||||
man_path = os.path.join(OUT_DIR, "manifest.csv")
|
||||
def is_usetup_row(nm):
|
||||
return nm.startswith("Usetup") and nm.endswith("Z")
|
||||
return is_setup_name(nm)
|
||||
|
||||
def emit(name, data):
|
||||
global cur_off
|
||||
@@ -573,7 +576,7 @@ for name in usetup_names:
|
||||
emit(name, comp_out)
|
||||
n_ok += 1
|
||||
|
||||
print(f"converted: {n_ok}/{len(usetup_names)} Usetup*Z files")
|
||||
print(f"converted: {n_ok}/{len(usetup_names)} setup*Z files (solo + MP)")
|
||||
|
||||
if errors:
|
||||
print(f"\n{len(errors)} ERRORS:")
|
||||
|
||||
@@ -76,7 +76,7 @@ SIZEOF_N64 = {
|
||||
from collections import Counter
|
||||
|
||||
for name, (addr, size) in sorted(fl_by_base.items()):
|
||||
if not (name.startswith("Usetup") and name.endswith("Z")) or size == 0:
|
||||
if not ((name.startswith("Usetup") or name.startswith("Ump_setup")) and name.endswith("Z")) or size == 0:
|
||||
continue
|
||||
comp = rom[addr:addr + size]
|
||||
if comp[:2] != b"\x11\x72":
|
||||
|
||||
@@ -409,7 +409,7 @@ if __name__ == "__main__":
|
||||
b = r[2].rsplit("/", 1)[-1]
|
||||
b = b[:-4] if b.endswith(".bin") else b
|
||||
fl[b] = (int(r[0]), int(r[1]))
|
||||
names = sorted(n for n in fl if n.startswith("Usetup") and n.endswith("Z"))
|
||||
names = sorted(n for n in fl if (n.startswith("Usetup") or n.startswith("Ump_setup")) and n.endswith("Z"))
|
||||
fails = 0
|
||||
for nm in names:
|
||||
a, s = fl[nm]
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ ROM_SHA1 = {
|
||||
}
|
||||
|
||||
# (script, args, produces) — run in order. d88 has no output of its own: it
|
||||
# appends the 21 per-level stage-setup files to the pccg dir d69 just made,
|
||||
# appends the 21 solo + 13 multiplayer stage-setup files to the pccg dir d69 just made,
|
||||
# and --regen makes that idempotent.
|
||||
EMIT_STEPS = (
|
||||
("d43_emit.py", (), "pcmodels-{region}/pcmodels.bin"),
|
||||
|
||||
Reference in new issue
Block a user