Repository navigation
Add .rmgr replay file recording - #195
Open
hopskipnfall wants to merge 9 commits into
Open
hopskipnfall wants to merge 9 commits into
hopskipnfall wants to merge 9 commits into
Conversation
hopskipnfall
force-pushed
the
master
branch
from
September 13, 2026 18:46
03918a4 to
29af175
Compare
1 of 2 tasks
* Add new game entries to mupen64plus.ini (Jay-Day#185) Extra MK64 Roms and Duke Nukem * Replace rollback lobby custom UDP punching with ICE and add per-player frame delay (Jay-Day#189) * integrate libjuice into rollback p2p * more accurate ping timings * Better ping messages in UI * Measure complete room-wide peer latency * Add opt-in ICE diagnostics with redacted libjuice logging * Show ICE connection failures in lobby seat status * Avoid naming conflicts with socket read and write functions * Filter additional noisy STUN connection trace logs * test individual frame delay choice * Fixed bug so that players who choose lower frame delay bare the rollback burden, rather than everyone else in the lobby * Make frame delay player-local and fix prediction window at 9 * 2-20 char name limit * ICE fixes / QoL updates (Jay-Day#190) * Ice fixes (#1) Improve p2p connection reliability Prediction frames are back! Per-player Kicked people stay kicked, within the lifetime of that room * Adjust rollback lobby defaults and ping display * Separate local frame delay from room prediction settings * Hide 0-frame delay again and fix Auto label after delay sentinel change (#2) Upstream re-added 0 to the delay dropdown, but GekkoNet's zero-delay path still has an open host crash (baseline rollback with divergent frame-0 inputs), so it stays hidden until that's fixed. Also point setAutoComboLabel at the delay combo's new -1 Auto sentinel: since the sentinel moved off 0 it was relabeling the "0 f" item instead, so the resolved "Auto (N f)" value never showed on the Auto entry. --------- Co-authored-by: Jeremy Daley <93jd@pm.me> * Lobby UI: remember window size, unified resizable column clamp (Jay-Day#192) * Remember lobby window size and clamp browse tree columns to the viewport * Enable DEBUGGER=1 and expose DebugMem* API in CoreApi Pure build-infrastructure groundwork, extracted from the shared-memory GameStats work (PR #1) without its feature code: enables the core's DEBUGGER=1 build flag (required for DebugMemRead*/DebugMemGetPointer to actually work, they're no-ops without it) and hooks those functions into RMG-Core's CoreApi, following the existing HOOK_FUNC pattern. * Fix SDL.h not found when building core with DEBUGGER=1 DEBUGGER=1 pulls src/debugger/*.c into the mupen64plus-core build, which needs SDL.h. The core Makefile's own pkg-config auto-detection for this doesn't reliably resolve when invoked from inside the ExternalProject build step, so pass the already-known-good SDL3 flags (from the pkg_check_modules(SDL3 ...) call already relied on for SDL3_net) through explicitly as SDL_CFLAGS/SDL_LDLIBS instead. SDL3's pkg-config Cflags only points at the parent include dir (expecting the modern #include <SDL3/SDL.h> style); the core's debugger sources predate SDL3 and use the legacy bare #include <SDL.h>, so an extra -I directly into the SDL3/ subdirectory is added for that to resolve too. * Fix write/read symbol conflict in vendored SDL_net.c SDL_net.c defines local static write()/read() shims for Windows sockets (SOCKET-typed handles aren't POSIX file descriptors), but their names collide with mingw's own io.h declarations of write()/read(), which a newer GCC now flags as a hard error. Rename the shims and use the same poll()/WindowsPoll(), instead of touching the (POSIX-only, unaffected) call sites. Pre-existing bug, unrelated to the DEBUGGER=1 changes in this branch -- surfaced because it blocks every Windows build regardless of that flag. * Port dbg_debugger.c's semaphore usage to the SDL3 API SDL3 renamed SDL_sem -> SDL_Semaphore, SDL_SemWait -> SDL_WaitSemaphore, and SDL_SemPost -> SDL_SignalSemaphore (SDL_CreateSemaphore/ SDL_DestroySemaphore keep their names, just the pointer type changed). SDL3's own SDL_oldnames.h compatibility shim deliberately does not alias these -- it replaces the old names with macros that fail to compile with a self-documenting error, to force an explicit migration -- so this updates the (previously never-compiled-on-SDL3) debugger code directly rather than relying on the shim. * Fix TRUE/FALSE undeclared in dbg_memory.c's disassembler path TRUE/FALSE aren't defined by current binutils/libbfd headers (this file apparently relied on them coming in transitively before). get_has_recompiled already returns plain int, so use 0/1 directly instead of adding another macro dependency. Verified this file (and its siblings dbg_debugger.c, dbg_decoder.c, dbg_breakpoints.c, and api/debugger.c) against the real MSYS2 UCRT64 SDL3 and binutils packages locally with the mingw-w64 cross compiler before pushing, to avoid another full CI round-trip. * Fix missing binutils transitive link deps on Windows Newer libbfd.a/libopcodes.a pull in libsframe (stack trace metadata), zstd + zlib (compressed debug sections), libiberty (xstrerror), and libintl (translated messages) internally, but static linking needs these listed explicitly -- the existing -lopcodes -lbfd alone left several symbols (xstrerror, ZSTD_isError, libintl_sprintf, sframe_*, compress/uncompress2) unresolved at link time on MinGW. Verified with a minimal link test locally against the real MSYS2 UCRT64 binutils/zstd/gettext-runtime/zlib packages before pushing. * Gate DEBUGGER=1 behind a new GAME_STATS CMake option DEBUGGER=1 was unconditional, permanently pulling in binutils (libopcodes/libbfd, plus libsframe/zstd/libintl/zlib transitively on Windows) as a link dependency for every RMG-K build even though nothing used the debugger API yet. Make it opt-in (default OFF) so only builds that actually want replay-file recording pay that cost. Also adds the GameStats_ReplayEnabled setting the recording feature will read to decide whether to record a match. * Add ReplayMemory: Smash Remix RAM-layout reader for replay recording Isolates the hand-derived pointer-chase (match info block, per-port match struct, player object -> player struct, controller/position sub-structs) from the event-stream writer that will consume it, so the "hairy N64 addresses" stay in one small, independently reasoned about file. All addresses come from prior RAM-map research reproduced in rmgk-replay-file-agent-prompt.md section 6, not re-derived here. * Add Replay: Slippi-style event-stream writer for .rmgr replay files Slippi-modeled design agreed in the handoff doc: 1-byte command code + fixed payload per event, an Event Payloads event declaring every other event's size up front for forward compatibility, and a length field kept at 0 and patched at match end for crash-safety during live recording. Explicitly not FlatBuffers (see handoff doc section 3.3). The per-match state machine lives entirely in OnFrame(), driven by ReplayMemory's game_status/current_screen reads - open a new file when a VS match starts, write Pre/Post-Frame events every real frame, finalize on a 1->5 game_status transition or on unexpected screen change / emulation stop. * Add independent "Record replay file" checkbox to Settings > Core A single persistent setting rather than a per-lobby-dialog checkbox like krec's - this feature also covers offline play, which has no lobby/room UI to attach a per-session checkbox to. Disabled with an explanatory tooltip in builds compiled without GAME_STATS=ON. * Hook Replay:: into the emulation lifecycle Replay::OnEmulationStart() arms the per-match state machine right after the frame callback is registered (same success-path branch krec piggybacks its own frame-counter reset on). Replay::OnFrame() runs from FrameCallback(), which mupen64plus-core already skips during GekkoNet rollback resimulation - so this is exactly once per real frame with no extra filtering needed on this side. Replay::OnEmulationStop() finalizes any still-open file when the user quits mid-match. * Fix cross-thread data race and lifecycle gaps found in final review - Replay::OnEmulationStop() runs on the UI thread while OnFrame() runs on the emulation thread; both touched s_File/s_State with no synchronization. Added a mutex covering all three public entry points. - OnEmulationStart() didn't handle a leftover open file from an abnormal prior session end, which silently disabled recording for the rest of the process (open() on an already-open ofstream just sets failbit). Also wired OnEmulationStop() into MainWindow::on_Emulation_Finished, mirroring the existing n02::recordingClose() call there for the same reason. - The Settings "Restore Defaults" button didn't reset replayEnabledCheckBox (wrong tab's C++ functions), and could silently reset it to false from an unrelated tab. Moved the wiring to the Core-tab-scoped functions. - OpenNewFile() now reports success/failure so a failed file open doesn't silently transition into a fake "Recording" state. - Added warning/info logging on open failure/success. - localtime() -> thread-safe platform variant. * Default GAME_STATS to ON Restores the behavior the branch's first 6 commits already had (DEBUGGER=1 unconditional, CI-verified) before this session gated it behind an opt-in flag. No CI changes needed: build.yml never references GAME_STATS and already installs binutils-dev / MSYS2's binutils transitively, so it just inherits the new default. Build.sh gains --no-game-stats as the opt-out (matching the existing --no-cheats/--no-angrylion pattern) and folds it into --fast-dev. * Add .rmgr format spec and implementation plan RMGR_SPEC.md is the authoritative byte-layout reference for the .rmgr replay format, written for external tooling (e.g. the TypeScript reader/writer, which will live in its own repo) that has no reason to read Replay.cpp/ReplayMemory.cpp to know the wire format. The implementation plan documents how the feature was built task by task, for anyone tracing a design decision back to its reasoning. * Export Replay:: functions across the RMG-Core/RMG DLL boundary CI caught it: RMG.exe failed to link on Windows with "undefined reference to Replay::OnEmulationStop()". MainWindow.cpp (the RMG target) calls it directly from on_Emulation_Finished(), but the function wasn't marked CORE_EXPORT, so it never made it into RMG-Core.dll's export table for a non-NETPLAY build (NETPLAY=ON builds happened to be masked by the --export-all-symbols link flag n02 pulls in). Same fix applied to OnEmulationStart()/OnFrame() for consistency with the rest of this file's public API, matching the existing convention elsewhere in RMG-Core (decorate the .cpp definition only, per Emulation.cpp/Emulation.hpp). Also corrected a stale comment: OnEmulationStart() runs on the emulation thread, not the UI thread as previously claimed - caught in the final review but left as a non-blocking doc nit at the time. * Move .rmgr output to a top-level "replays" folder, add arming log Was CoreGetUserDataDirectory()/"Replays" (nested under the per-platform user-data dir). Changed to a bare "replays" relative path, matching krec's own "records" directory convention exactly (Source/n02/n02_client.cpp) - both now land as siblings next to the executable in a portable install instead of .rmgr being buried a level deeper than .krec. Also logs when the feature actually arms at emulation start ("Replay: enabled, watching for a match to start"), since there was previously no observable signal between "checkbox is on" and "a match was detected" - needed for diagnosing the report that no replays/ folder was appearing. * Track team/handicap match settings in GameStart Adds the gameplay-affecting settings already present in the verified RAM map research but not yet captured: global teamsEnabled (MI+0x02) and handicapMode (MI+0x08), plus per-port team (playerStruct+0x0C), handicap (+0x12), and CPU level (+0x13). The per-port fields need the same player-object/player-struct chase as Post-Frame data, so they're read via ReadPortPlayerState and left at their zero default if a port's characters haven't spawned yet when GameStart is captured (e.g. file opened during the pre-match countdown) - same tolerant "not currently available" handling as everywhere else in this file. Pure field-append per the format's own versioning rule (docs/RMGR_SPEC.md section 5): GameStart grows from 150 to 164 bytes, with the new fields strictly after the original v1 layout so an old parser reading a new file is unaffected. Did not attempt a wholesale "copy every Remix setting" - only fields with already-verified memory offsets from the prior RAM-map research went in; anything beyond that would need new reverse-engineering this session has no tooling to safely do. * Move replay recording to per-room toggles, add playback opt-in Record replay file was a single always-on-by-default Settings switch that applied to every match, unlike krec's per-room "Record game" checkbox + "enable by default" pattern. Give .rmgr the same shape: a checkbox in each netplay room/lobby dialog seeded from a new Kaillera-tab default, an independent checkbox in the krec Playback dialog for generating a .rmgr while replaying a recording, and a consume-once override (Replay::SetEnabledOverride) so each dialog's checkbox controls only its own launch without leaking into unrelated offline launches. Also restrict all of it to Smash Remix 2.0.1: the feature only has meaning for that game, and GoodName mismatches would otherwise record garbage metadata for anything else. * Record native combo hit count and combo damage in .rmgr Adds comboHitCount/comboDamage to PostFrameUpdate, read from the same per-port match struct Smash Remix's own combo meter reads (not something this feature derives itself) - it already tracks "hits taken in the current unbroken chain" per victim, including surviving grabs/wall-bounces/ tech-chases the way vanilla wouldn't, which is exactly what's needed to tell a fresh neutral hit from a combo continuation without guessing at it from action-state/hitstun heuristics. Pure field-append per the format's own versioning rules (§5): grows PostFrameEvent from 42 to 50 bytes, no header version bump. * Record goodName and recorderSchemaVersion in the .rmgr header The file previously carried zero information about which ROM/game produced it - IsSupportedGame()'s GoodName check only gated whether recording happened, then discarded the value. That's fine for a single hardcoded game, but doesn't scale to "a new Remix version" or "a different ROM/game entirely", both of which are on the roadmap. Adds two fields, deliberately on separate axes: goodName identifies which ROM build produced the file, recorderSchemaVersion (its own counter per goodName) identifies which revision of this recorder's interpretation of that ROM's memory layout wrote it - independent because a bugfix to an existing field's offset changes recorded values without changing any event's declared byte size, which the existing per-event EventPayloads mechanism has no way to signal on its own. This grows FileHeader itself (12 -> 80 bytes), which the existing field-append/new-event versioning mechanisms don't cover, so FileHeader.version bumps 1 -> 2. Breaking on purpose: no version-1 file has any external consumer yet. * Record wall-clock start time in the .rmgr header, tidy filename format Adds recordedAtEpochSeconds (seconds since Unix epoch, UTC) to the header - the filename's timestamp already existed but can't be trusted the same way (files get renamed/copied/re-shared), and there was no in-file record of when a recording actually started. Also switches the filename's own timestamp from krec's compact YYMMDDHHMMSS to YYYYMMDD-HHMMSS (4-digit year, dashed) for something that reads clearly at a glance, computed from the same instant written to the header field so the two can never disagree. BuildFileName() now takes that instant as a parameter rather than reading the clock itself, specifically so the caller can reuse one `now` for both. Another breaking FileHeader change (2 -> 3, 80 -> 88 bytes) on top of the last one - same reasoning, still pre-adoption. * Temp diagnostic: print SDL_CFLAGS_FOR_CORE inputs on Linux CI dbg_debugger.c fails to find SDL.h on Linux CI despite the SDL_CFLAGS passthrough added for exactly this. No Linux machine to reproduce locally against - printing what's actually computed there instead of guessing. Will be removed once the real fix lands. * Fix Linux CI: SDL3's pkg-config prefix didn't match where it was installed Root cause of the Linux "SDL.h: No such file or directory" failures in dbg_debugger.c (DEBUGGER=1's SDL_CFLAGS passthrough in Source/3rdParty/CMakeLists.txt) wasn't a flaw in that passthrough logic - diagnostic output confirmed it was correctly transforming whatever pkg-config reported into a legacy -I.../SDL3 flag. The actual bug was one step upstream: the "Build SDL3" CI step never set CMAKE_INSTALL_PREFIX at configure time, so SDL3's generated sdl3.pc baked in CMake's default (/usr/local) even though `cmake --install --prefix /usr/` was placing the real files under /usr. pkg-config kept reporting -I/usr/local/include/SDL3, which didn't exist, while the real headers sat one prefix over. Windows never hit this because it installs SDL3 via a prebuilt MSYS2 package instead of building from source. Removes the temporary diagnostic message() calls added to track this down. * Fix Linux ARM64 build broken by the DEBUGGER=1 SDL_CFLAGS fix The previous fix force-included SDL3/SDL_oldnames.h and stdio.h into every compilation unit via SDL_CFLAGS -> CFLAGS, to patch a couple of debugger/*.c files without touching vendored source. But CFLAGS is shared by every rule in this Makefile, including new-dynarec's arm64/*.S files - routed through the C compiler for preprocessing (see the core Makefile's %.o: %.S rule), so those forced #includes landed there too. glibc's stdio.h injected real declarations into what's supposed to be a near-empty assembly preprocessing pass, and the assembler choked on the result (confirmed on real Linux CI, both x86_64 and aarch64). Turns out neither forced-include was actually needed for what's in this vendored source snapshot: dbg_debugger.c already calls SDL3's native semaphore names (SDL_CreateSemaphore/SDL_WaitSemaphore/...), not the old SDL2 short names SDL_oldnames.h exists to shim - so that one was dropped outright. dbg_breakpoints.c's sprintf() calls without <stdio.h> were real, so that one's now a direct #include in the file itself instead of a global force-include, avoiding the collateral damage. -I/-D flags are still added globally (safe for a .S file, since they don't inject content) - only -include was the problem. * Add session handoff doc for match-stats brainstorm Summarizes what's landed vs. still uncommitted, and the current state of the edge-guard/recovery stat brainstorm so a fresh session can pick up without re-deriving context. * Add headless .rmgr export from the Playback dialog Mirrors the existing "Export MP4" feature but skips video/audio, running EmulatorProxy + PifReplay at high speed to just produce a .rmgr file. ReplayHook isolates RMG-Core's Replay:: API from EmulatorProxy's self-contained mupen64plus type definitions, which collide if both headers land in the same translation unit. Output paths are collision-checked (FindCollisionFreePath) so two same-second exports don't overwrite each other. * Fix replay export speed, player names, and UI polish - Fix speed: mupen64plus-core's main_speedset() silently rejects any speed factor above 1000% and leaves it unchanged, so the export's 2000% request was rejected every frame and it ran at 1x the whole time. Now requests 1000%, the core's actual ceiling. - Fix player names: the headless export process never runs through n02's live netplay UI code, so recording_player_names was always empty for it. Added Replay::SetPlayerNamesOverride() (mirrors the existing output-path override) so the export can pass through the names already stored in the .krec being replayed. - Remove the "Also record replay" checkbox from the Playback dialog, superseded by the dedicated Export Replays button. - Rename "Export Replay" to "Export Replays" throughout, and match Export MP4's blue primary-button styling. * Fix Export Replays output location and naming The export was writing its .rmgr next to the source .krec (records/), using the krec's own basename verbatim - so re-exporting the same recording silently overwrote the same file in records/ every time, and .rmgr output never ended up alongside other .rmgr files at all. Now builds the output path the same way a live-recorded .rmgr is named: replays/<YYYYMMDD-HHMMSS>-Player1-Player2.rmgr, reading the timestamp and player names straight from the .krec's own header so re-exporting the same recording reproduces the same base name. The existing collision-avoidance (FindCollisionFreePath in ReplayFileExport.cpp) still applies unchanged. * Fix Linux build: gate KailleraExport:: usage behind _WIN32 onPlaybackExportReplay() referenced KailleraExport::KrecData/ ParseKrecFile unconditionally, but KrecParser.hpp is only included under #ifdef _WIN32 - the function still has to type-check on any GAME_STATS build regardless of platform, even though it's only ever reachable on Windows (its button/connect are both * Fix multi-match .krec export naming; raise export speed to 2000% Replay.cpp's output-path and player-names overrides were consumed on the first OpenNewFile() call, but a headless export of a .krec spanning multiple matches opens one .rmgr per match - so only the first match's file got the override-derived name; every later match silently fell back to the plain default naming (no names) and, before this, a fixed records/ path that got overwritten on every subsequent match. Both overrides now persist for the whole emulation session (cleared at OnEmulationStop() instead of per-file), and every OpenNewFile() call - override path or default - runs through a new FindCollisionFreePath() so each match still gets a distinct, correctly-named file instead of overwriting the last one. Also raises mupen64plus-core's main_speedset() ceiling from 1000% to 2000% (a hard-coded upstream limit, not anything export-specific) and requests 2000% for replay export accordingly. * Name exported replays after their source .krec + game number Previously exported .rmgr files were named from the krec's own recorded timestamp + player names, re-derived independently of the krec's actual on-disk filename - correct in the common case, but not guaranteed to match if the krec had been renamed, making it harder to trace an exported file back to its source. Now names them "<krec name>-<game number>.rmgr" (game 1, 2, 3, ...) directly from the krec's own filename, so the export always plainly corresponds to its source .krec. Replay.cpp's OpenNewFile() does the numbering (SetOutputPathOverride is now the base path, not the full per-file path), which also simplifies KailleraPlaybackDialog.cpp - it no longer needs to parse the krec's header at all for filename purposes. * Extrapolate per-match recording timestamps from krec, add ItemUpdate event Headless .krec export now stamps each exported match's recordedAtEpochSeconds from the krec's own recording-start time (embedded header timestamp, or a filename-derived fallback) plus elapsed krec input frames / 60, instead of wall-clock time at export (which drifts arbitrarily since headless replay runs at up to 2000% speed). Adds a new ItemUpdate event (code 0x06, recorder schema v2) capturing the shared item/hazard/projectile object list's position and raw type ID per frame, per docs/smashremix ram-map.md section 14. Backward compatible: new event type, not a header version bump, per RMGR_SPEC.md section 5. * Fix ItemUpdate's type field (schema v3), add Whispy wind tracking Schema v2's ItemUpdate.typeId was wrong, not just incomplete: +0x0C on the raw GObj is four packed single bytes (link_id/dl_link_id/frame_draw_last/ obj_kind), not a 32-bit type ID - reading it as a u32 produced a large, constantly-changing value (confirmed empirically: 39 recorded games, only Link's bomb ever showing up, with garbage-looking values like 0x40B6B01). Corrected against a real SSB64 decompilation (VetriTheRetri/ssb-decomp-re): read link_id as a single byte (4=Item, 5=Weapon), then chase GObj+0x84 -> ITStruct*/WPStruct* -> +0x0C for the real, named kind (ITKind or WPKind). Both enums are now fully documented in RMGR_SPEC.md section 7.6. Any data captured under schema v2 should be discarded, not migrated. Also adds StageHazardUpdate (schema v3), currently tracking just Whispy Woods' wind on Dream Land (docs/smashremix ram-map.md section 10.3, now fully confirmed via the same decomp) - a sparse per-frame event, extensible to more hazards later via the field-append mechanism. * Add hitbox and hurtbox recording (schema v5); make it opt-in **Hitbox/Hurtbox Recording (schema v5)** - `ReadHitboxes()`/`ReadHurtboxes()` collect FTAttackColl/ITAttackColl/WPAttackColl (4/2/2 slots) and FTDamageColl (11 slots) per frame - New `HitboxUpdate`/`HurtboxUpdate` events (codes 0x08/0x09) record all active slots every frame - Deliberately verbose/exhaustive for now, as future goal is to compute geometry from (characterId, actionStateId, actionFrameCounter) alone - See docs/RMGR_SPEC.md §4.8/§4.9 for full layout and confidence caveats on Item/Weapon offsets **Opt-In Setting (off by default)** - New `GameStats_RecordHitboxData` setting (Settings.hpp/cpp) - "Include hitbox and hurtbox data (large files)" checkbox in SettingsDialog, disabled unless replay recording is on - When off, `ReadHitboxes()`/`ReadHurtboxes()` are not called at all (not just skipped) **Bug fixes** (incidental to hitbox/hurtbox work) - Fixed Weapons never being recorded: were walking only gGCCommonLinks[4] (Item list), Weapons live on separate gGCCommonLinks[5] - Fixed held Items recorded with garbage positions: now check ITStruct::owner_gobj != NULL and skip held items **Schema history (§5 in RMGR_SPEC.md)** - v4→v5: added HitboxUpdate/HurtboxUpdate events; bumped recorderSchemaVersion to 5 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * Fix held-item detection (schema v6); owner_gobj never actually cleared ITStruct::owner_gobj was being used as a "currently held" proxy since schema v4, but decomp confirms (itMainSetFighterRelease()) it's deliberately retained across a throw/drop for later damage/KO attribution - it reads non-NULL for essentially an item's entire lifetime, held or not. This meant the filter silently swallowed nearly every thrown/dropped Item's flight, not just its brief held phase. Replaced with a position-based proxy: while held, the engine re-parents the item's DObj onto the holding fighter's hand-bone joint, so its position reads near (0,0,0) instead of a real world coordinate - itMainSetFighterRelease() writes the real coordinates back at the exact release moment (lbCommonEjectTreeDObj()). IsHeldItemPosition() uses "position still reads near (0,0,0)" as the signal instead, no new offset work required. Bumped recorderSchemaVersion to 6. Item ItemUpdate data recorded under schema 4/5 is missing most or all of every thrown/dropped Item's real flight and should be re-recorded (Weapons unaffected - this check never applied to them). Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * Fix jumpsUsed read width (u32 -> u8); export jumpsRemaining (schema v7) PostFrameUpdate.jumpsUsed was read at the wrong width - playerStruct+0x148 is a single byte (u8), confirmed by the decomp's own naming convention (the very next field is literally named unk_ft_0x149, encoding its own byte offset); +0x14A-0x14B is padding. An earlier u32 read there, on a word-swapped emulator where a *byte* read needs its address XORed with 3, landed on that padding instead of the real field - reading a constant 0 for an entire match, every port, no matter how much jumping happened. Fixed the read width AND switched what gets exported at this same wire position: jumpsRemaining (jumpsMax, chased from the per-character FTAttributes+0x64, minus the corrected jumps_used) instead of jumpsUsed directly - more directly useful, and what this project wanted to export in the first place. Bumped recorderSchemaVersion to 7. Schema v6 and earlier files' byte here is meaningless (the old bug's output, not real data) - not that byte-layout changed, since it's still a u8 at the same wire position. * Replace recordedAtEpochSeconds with recordedAtEpochMillis + nanosOffset (schema v4) Second-resolution timestamps aren't precise enough to align multiple .rmgr recordings from the same session against each other. Header version 3 -> 4 (88 -> 92 bytes): recordedAtEpochSeconds (u64 seconds) is replaced with recordedAtEpochMillis (u64 millis) plus a new recordedAtNanosOffset (u32, nanoseconds within that millisecond, best-effort). Live recording now reads std::chrono::system_clock::now() instead of time(nullptr); the .krec-derived override path converts its frame-based elapsed offset to milliseconds (rounded) instead of truncating to whole seconds, and always writes a 0 nanosOffset since it has no sub-millisecond information to offer. * Document optional version-3 read-compat in the versioning history rmgr-ts's parser now keeps reading version-3 files by synthesizing recordedAtEpochMillis/recordedAtNanosOffset from their recordedAtEpochSeconds (*1000, nanosOffset 0), instead of the usual hard cutover - a reader-side choice, since this project's own writer never writes below the current version regardless. * Fix hurtbox-slot gate (schema v8); decouple hitbox/hurtbox checkbox from live-record default ReadHurtboxes() gated on IsValidRdramPointer(joint), but decomp confirms (ftmanager.c) an unused FTDamageColl slot leaves joint untouched garbage, not NULL - hitStatus (explicitly 0/nGMHitStatusNone for unused slots) is the real signal. The wrong gate filtered out every slot, used or not, every frame; a real match with confirmed combat recorded zero HurtboxUpdate events the entire time. Fixed by checking hitStatus first. PS_ATTACK_COLL_BASE/PS_DAMAGE_COLL_BASE/strides were independently re-verified against the decomp and are correct - not the bug. Also notes that HitboxUpdate is still producing zero events post-fix for reasons not yet isolated (sampling timing vs. a runtime settings issue - see docs/RMGR_SPEC.md §8), and documents that FTDamageColl.size is stored pre-halved (half-extents) for whenever hurtbox rendering gets built. Separately: the "Include hitbox and hurtbox data" checkbox was only enabled when "Enable replay recording (.rmgr) by default" was checked, even though it has real effect during headless replay export (which bypasses that default entirely via SetEnabledOverride) - decoupled them and renamed the checkbox to end in "(debug)". * Add hitbox/hurtbox diagnostic logging The schema v8 gate fix didn't resolve the empty-hitbox/hurtbox symptom on re-test (still zero events across a real match with confirmed combat) - static review of the settings/gating pipeline turns up nothing wrong, so this adds two log lines to make the runtime state directly observable instead of guessing further: - Once per session (OnEmulationStart): whether s_RecordHitboxData actually ended up true. - Every 5 seconds during recording (RecordFrame): how many hitbox/hurtbox objects ReadHitboxes()/ReadHurtboxes() actually found that frame. Diagnostic only, no behavior change. Remove once the root cause is found. * Wire up RMG-Core's debug callback during headless .rmgr export Found while chasing the hitbox/hurtbox diagnostic: RMG-Core's own CoreAddCallbackMessage() (Replay.cpp's "Replay: ..." info/warning messages, including the new hitbox/hurtbox diagnostics) was completely invisible during "Export Replays" - not just the new diagnostics, ANY message from that mechanism, past or future. The headless export subprocess (relaunches the app with --export-rmgr-output, see main.cpp) never reaches MainWindow::Init(), which is the only place CoreSetupCallbacks() gets called normally, so every message silently queued into RMG-Core's pending buffer and was discarded when the process exited - the export progress dialog only ever showed this file's own explicit fprintf(stderr, ...) progress lines, nothing from RMG-Core itself. Registers a no-op callback (just to satisfy RMG-Core's "has anyone registered yet" gate) and turns on CoreSetPrintDebugCallback so messages print to stdout, which the GUI's QProcess (MergedChannels) already captures and shows in the same progress dialog as the existing frame-count progress lines. Real, permanent visibility fix, not scoped to this one debugging session. * Add "View Log" button to the export-finished dialog Found while chasing the hitbox/hurtbox diagnostic: even with the previous commit's fix wiring RMG-Core's debug callback into the export subprocess's stdout, the captured output (m_exportLog) was only ever surfaced on a FAILED export (summarizeExportLog() in the failure message box) - a successful export just showed "Export finished. Saved to: <path>" with no way to see what was logged along the way. Adds a "View Log" button next to Open Folder/Open File that shows the full captured log in a read-only text view, so RMG-Core's "Replay: ..." diagnostics (and anything else printed during export) are actually visible regardless of whether the export succeeded or failed. * Record Whispy Woods' wind direction (schema v9) Adds StageHazardUpdate.hazardFlags bit 0x02: which way the wind is blowing (0 = left, 1 = right), alongside the existing bit 0x01 (currently blowing). Read from the fixed s8 global smashremix docs/ram-map.md section 10.3.1 documents (gGRCommonStruct + 0x2A, lr_players) - the engine's own already-decided direction for the current wind cycle, no recomputation needed. Only set (and only meaningful) when the blowing bit is also set. Bumps kRecorderSchemaVersion 8 -> 9; documented in docs/RMGR_SPEC.md's section 4.7 bit table and section 5 versioning history. * Add .rmgr v2 design doc: compression, core/game-family event layering Design for the breaking rewrite ahead of real-user rollout - buffered single-shot deflate compression, a game-agnostic core input layer plus a per-game-family extension layer, and removal of hitbox/hurtbox recording. Not yet implemented. * Rewrite RMGR_SPEC.md for format v5 (design only, no code changes) Ground-up rewrite reflecting the approved v2 design: buffered single-shot deflate compression, a game-agnostic core input layer plus a per-game-family (smash64) extension layer, hitbox/hurtbox recording removed, and dead schema-history narration cut. version continues incrementing (4 -> 5) rather than resetting, per feedback, so an old reader unambiguously rejects the new format instead of the number colliding with an unrelated earlier meaning. Source/RMG-Core/Replay.cpp and ReplayMemory.cpp still implement v4 - this commit is spec-only, matching the explicit request not to touch code yet. * Address review notes on the v5 spec: drop nanos field, fix framing - version no longer resets to 1; keeps incrementing (5), so an old reader unambiguously rejects the file instead of colliding in value with an unrelated earlier meaning. (Already the case, just tightened the wording.) - Stop describing this as superseding a real "version 4" spec - this is the first time .rmgr has been formally specified at all; the fork's prior in-development code was never published or seen by the upstream maintainers. - Soften the "deterministic replay" claim: no RNG seed is captured, so full re-simulation isn't actually guaranteed - cross-referenced to the existing limitation in what's now SS9. - Drop recordedAtNanosOffset entirely (108-byte header, down from 112); millisecond resolution is kept. - Reformat SS8.1/8.2/8.3/8.4 (character IDs, stage IDs, action state IDs, button bits) from prose/code-block layouts into proper markdown tables. * Call out RNG and unmapped Remix-settings gaps in known limitations Sharpens the existing "no RNG seed" bullet with a cross-reference to the SS1 determinism caveat, and adds a new bullet noting that most of Smash Remix's own match-settings additions beyond the original SSB64 set aren't mapped into MatchSettings yet. * Implement .rmgr v5: buffered+compressed writer, core/smash64 event split Rewrites Source/RMG-Core/Replay.cpp / ReplayMemory.cpp / .hpp to match the format-v5 spec: the whole match now buffers in memory and is written as a single deflate-compressed block at match end (no more seek-and-patch streamLength trick); the event catalog splits into a game-agnostic core layer (MatchStart/InputFrame/MatchEnd) and a smash64-family extension layer (MatchSettings/StateFrame/ItemUpdate/ StageHazardUpdate/MatchResult), gated on a new header gameFamily field; recordedAtNanosOffset is dropped; hitbox/hurtbox recording is removed entirely, including its Settings dialog checkbox. Verified by compiling RMG-Core and RMG (Settings.cpp/.hpp, SettingsDialog.cpp/.ui, Replay.cpp, ReplayMemory.cpp) from scratch - all touched files build with zero errors; the only remaining build failure in this environment (VidExt.cpp, missing Vulkan headers) is pre-existing and unrelated. The reader side (rmgr-ts, rmgr-viewer) is not updated yet - tracked as follow-up work per the design doc's SS7. * Drop internal planning docs from this PR, keep only RMGR_SPEC.md Removes the superpowers handoff/plan/design-doc markdown files this branch accumulated - not worth carrying into the PR. RMGR_SPEC.md is the only markdown deliverable; its few references to the now-removed design doc are folded inline so it stays self-contained. * Address review findings: async match-write, filename override, reuse - FinalizeFile() now hands compression + the file write off to a detached worker thread instead of doing both synchronously on whatever thread calls OnEmulationStop() (frequently the UI thread via CoreStopEmulation()) - stopping emulation or ending a match no longer blocks the UI for however long a multi-MB match takes to compress and write. - OpenNewFile() now test-opens the actual destination file (not just its parent directory) before any events are buffered, so a write failure is caught before a whole match is recorded into memory instead of only being discovered - and discarded - at match end. - BuildFileName() now honors SetPlayerNamesOverride() like MatchStart already did, via a new shared ResolvePlayerNames() helper - headless .krec export no longer produces files with correct in-header player names but a blank filename suffix. - FindCollisionFreePath, duplicated verbatim in Replay.cpp and ReplayFileExport.cpp, is now one shared CoreFindCollisionFreePath in RMG-Core's File.cpp/.hpp. - Removed a redundant per-port-per-frame characterId memory read (ReadPortMatchInfo and ReadPortPlayerState were both reading the same byte); consolidated three copy-pasted buffer-append blocks in Replay.cpp into one AppendBytes()/AppendValue() helper; reserve() the event buffer upfront to cut reallocation churn during a match. Verified by rebuilding RMG-Core from scratch - all six touched files compile with zero errors (only pre-existing macOS-only linker path quirks remain, unrelated to this change). * Fix bad rebase conflict resolution in vendored SDL_net.c The rebase onto upstream/master hit a conflict in this vendored file between an old commit on this branch (renaming the Windows write/read helpers to SDLNet_PlatformWrite/SDLNet_PlatformRead) and upstream's own, different fix for the same symbol collision (just naming them SocketWrite/SocketRead directly, matching the POSIX branch's #define target). I resolved the conflict by keeping this branch's old commit instead of upstream's - backwards, and it left SocketWrite/SocketRead completely undefined on Windows (the '#define SocketWrite write' line only exists in the POSIX #else branch), breaking every Windows build. CI caught it. Reverting the file to match upstream/master exactly. --------- Co-authored-by: denvar-labs <denvarkart@gmail.com> Co-authored-by: Ownasaurus <Ownasaurus@gmail.com> Co-authored-by: Jeremy Daley <93jd@pm.me> Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Reads existing recordings with a self-contained legacy reader (not part of @rmg-k/rmgr, which only reads v5 by design) and rewrites them with the new package's serializeReplay. Reports per-file and total size and parse-time comparisons. Run against this session's 40 sample recordings: 43.29 MB -> 12.31 MB (28.4% of original, 3.52x smaller) from compression alone, at the cost of slightly higher average parse time (9.93ms -> 11.30ms per file) from the added decompression step - both comfortably sub-frame-budget in absolute terms.
This tool converts .rmgr recordings for use with rmgr-viewer and depends on the sibling rmgr-ts package the same way rmgr-viewer's other scripts do, so it belongs alongside them rather than in RMG-K.
… armor ItemUpdate gains trailing scaleX/scaleY (DObj render scale; Samus's Charge Shot grows with charge). StateFrame gains trailing: - scaleX/scaleY: fighter root-joint scale (Giant/Tiny; Kirby's aerial up-B growth, which also affects ECB and ledge-grab reach) - characterSpecific: passive_vars word - Samus/DK stored charge, Kirby's copied ability (8 = none) - shieldHealth - specialHitStatus / starHitStatus: the timed (respawn etc.) and Star hit statuses, separate from hurtboxState's motion-script one - knockbackResist: temporary knockback armor (Yoshi's double jump) Backwards compatible via EventPayloads-declared sizes: schema-1 files keep 25-byte ItemUpdate / 50-byte StateFrame and older readers skip the trailing bytes. Also corrects the hurtboxState docs (2 = invincible, 3 = intangible). Offsets are from the decomp and not yet live-verified.
Super Star item invincibility isn't needed; knockbackResist moves to offset 0x43. Respawn invincibility stays covered by specialHitStatus.
hopskipnfall
force-pushed
the
master
branch
from
September 18, 2026 10:25
42b8655 to
26a04b4
Compare
#5) * Fix duplicate Internal_InputPluginSwitchRequested enumerator in SettingsID Settings.hpp declared this enumerator twice in the same enum class, which fails to compile unconditionally. Left over from a rebase that introduced GameStats_ReplayEnabled between the two occurrences instead of removing the duplicate. * Record RNG seed and Smash Remix Toggles.asm settings in MatchSettings Bumps .rmgr format to version 6. MatchSettingsEvent grows from 32 to 75 bytes: - rngSeed: sSYUtilsRandomSeed at match start, captured once. Paired with this match's recorded inputs, it's sufficient to reproduce RNG-dependent outcomes (item spawns, move variance, etc.) on replay. - 31 Gameplay Settings + 8 Stage Settings from Remix's Toggles.asm menu-configured mutators (hitstun, hitlag, DI, Z-cancel, rage, tripping, game speed, hazard mode, camera mode, and so on) - match properties captured once, not per-frame data. Addresses and enum meanings verified directly against the Smash Remix 2.0.1 source build. See docs/RMGR_SPEC.md $5.1/$5.1.1/$5.1.2 for the full field list and value tables. Deliberately out of scope: the ~18 named + ~170 auto-generated per-stage/per-track random-pool toggles, Music Settings, and Player Tags - can be added later as ordinary field-appends if needed.
* Add design doc: queued Export Replays Design for turning the blocking, one-at-a-time 'Export Replays' (.krec -> .rmgr) flow into a queue - select a recording, export, select another, export again without waiting for the first to finish. See the doc for the full design (scope, data model, UI, error handling). * Queue Export Replays instead of blocking on one at a time Clicking Export Replays no longer refuses while an export is running - it now starts immediately if idle, or is appended to a FIFO queue (m_replayExportQueue) that drains automatically as each export finishes. This lets you keep selecting recordings and clicking Export Replays without waiting for the current one to finish. Export MP4 is untouched - same modal QProgressDialog, same one-at-a- time behavior, still sharing the underlying m_exportProcess slot (mutually exclusive with Export Replays, as before). Replaces Export Replays' modal QProgressDialog with a non-modal panel (m_replayExportPanel: status label + progress bar + queue-count label + a capped history log) that appears on the first export and stays visible, updating in place, rather than blocking the dialog. Finished exports no longer show a popup (success or failure) - they're logged to the panel's history instead, so several queued exports finishing close together don't stack dialogs. See docs/superpowers/specs/2026-09-20-queued-replay-export-design.md for the full design.
) Adds §8.3.1: exhaustive, decomp-verified per-character action state ID tables for Mario, Luigi, Fox, Donkey Kong, Samus, Link, Yoshi, Captain Falcon, Pikachu, Jigglypuff, Ness, and Kirby (largest at 83 states - one full copied move-set slot per copyable neutral-B). Every character independently restarts its own status enum at 0x0DC; there is no cross-character ID sharing except Luigi/Mario, whose tables are byte-identical and point at the same functions. A few states in this range per character (Attack13, Attack100Start/Loop/End) are jab-combo continuations rather than actual special moves, despite sharing the block - flagged inline rather than blanket-labeling the whole range "special moves" as the doc previously did. Scoped to the original 12 characters only, including their same- moveset Polygon/JP/EU variant IDs - not extended to any Remix-added unique character (Dr. Mario, Dark Samus, Giant DK, etc.), since those aren't confirmed to share an original character's code. Updates the corresponding Known Limitations bullet (§9) accordingly.
Smash Remix's Salty Runback (hold Start on the no-contest/results screen to instantly restart, bypassing the normal win/loss flow) wasn't being detected by the recorder's state machine: it doesn't transition through the screen change or necessarily the observable game_status==5 tick our two existing signals depend on, since it can skip both on its way straight back into a fresh battle. Adds ReplayMemory::IsSaltyRunbackActive(), reading GameEnd.asm's is_salty_runback flag (0x8048B174) - confirmed via a bass -sym build against real Remix source, not inferred. OnFrame() now finalizes the current match and starts a new one on a 0->1 edge of this flag, in addition to the existing game_status==5 check. Edge-detected rather than a level check, since the flag likely stays set for the entire replacement match too - re-baselined to the current value whenever a new recording starts, so it can't misfire on that match's own first frame. Known gap, documented in docs/RMGR_SPEC.md's Known Limitations: two runbacks triggered back-to-back with no non-runback match end between them may not be independently caught, since it's unconfirmed whether the flag reliably drops back to 0 in between.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note: This is a giant PR and I would normally split this up to make it easier
to review, but then GitHub wouldn't build the binaries for us.
The important file to read is
docs/RMGR_SPEC.md.All of the code is based on the decisions in that document. This PR
description also gives background on aspects of the implementation.
Consider this PR just a review of that SPEC document.
Summary
Adds an optional, per-match replay recording feature: while a Smash Remix
match is in progress, RMG-K can record every seated port's controller
inputs and (for Smash Remix specifically) a snapshot of game state -
position, damage, stocks, action state, items, stage hazards, match
settings (including RNG seed and Remix's own gameplay/stage mutators) - to
a self-contained binary file (
.rmgr), independent of and unrelated tothe existing Kaillera
.krecrecording. The intent is to supportdownstream analysis tooling (e.g. the companion web viewer,
rmgr-viewer) withoutneeding to re-run the emulator to reconstruct what happened in a match.
The feature is fully optional at both build time and runtime:
GAME_STATSCMake option (on bydefault;
./Build.sh --no-game-statsor-DGAME_STATS=OFFdisables it).It requires building
mupen64plus-corewithDEBUGGER=1, which pulls ina
binutils(libopcodes/libbfd) link dependency - the only newexternal dependency this feature introduces.
checkbox in Settings controls the default; per-session overrides exist
for netplay/playback dialogs and headless export tooling.
The format is currently at version 5 (
docs/RMGR_SPEC.md§title) -several incompatible revisions happened during development before this
spec document existed to pin the layout down; there is no migration path
from anything recorded before this document, and none is planned. Since
this document was written, the container format itself hasn't changed
again, but
MatchSettings/StateFrame/ItemUpdate's ownrecorderSchemaVersion(scoped togoodName == "SmashRemix2.0.1") has:character-specific state field, shield health, and a special-hit-status
byte to
StateFrame/ItemUpdate- see §5.2/§5.3 of the spec.sSYUtilsRandomSeed, captured once atmatch start - sufficient for deterministic replay-from-inputs, since
loading it and replaying this match's recorded controller inputs
reproduces the same RNG-dependent outcomes) and all 31 Gameplay Settings
plus 8 (core) Stage Settings from Remix's own
Toggles.asmmutators(hitstun, hitlag, DI, Z-cancel, rage, tripping, game speed, hazard mode,
camera mode, and so on) to
MatchSettings- see §5.1/§5.1.1/§5.1.2.Design
The on-disk format is fully specified in
docs/RMGR_SPEC.md(added by this PR) - that documentis the source of truth for the byte layout; this section is a summary of
the design decisions behind it.
Self-describing binary event stream, modeled on
Project Slippi's
.slpformat forMelee (not byte-compatible, several deliberate departures - see the spec's
§1). A small fixed header is followed by a sequence of typed events, each
a 1-byte command code plus a payload whose size was declared up front by
the very first event in the stream. A reader that doesn't recognize a
command code can still skip it correctly using that declared size, so the
format can grow new event types or new trailing fields on existing events
without breaking old parsers.
Buffered, then compressed once. The recorder holds the whole match's
events in memory as it plays and writes the file in a single pass -
header, then a deflate-compressed event-stream block - only once the
match ends. A crash or force-quit mid-match produces no file at all (an
accepted trade-off - the format has no partial/truncated-file case to
reason about). This keeps files small: per-frame data is highly
redundant frame-to-frame, and compressing the whole stream captures that
well without any bespoke encoding. In practice, across 157 real matches
recorded during development on the current format version: ~2.2 KB per
second of match (median and mean agree closely; ~2.0-4.0 KB/s range
depending on how much is happening on screen) - a typical ~2-minute match
is roughly 260 KB.
Two-layer event catalog, so the format isn't Smash-specific at the
container level:
match end/frame count) that's always recorded for any N64 title RMG-K
can drive - conceptually close to a
.krec-equivalent recording on itsown.
smash64game-family extension layer (match settings, per-frameposition/damage/stocks/action-state, items, stage hazards, final
placements) that's only recorded when the loaded ROM is a recognized
build (
goodName == "SmashRemix2.0.1"today). The header's newgameFamilyfield tells a reader which extension events, if any, applyevery
goodNamestring that's ever meant "this is Smash."This means a future second game (or a different Smash Remix/vanilla SSB64
build) can define its own extension layer without touching this one, and
(once a game-agnostic recording trigger is wired up - see Known
limitations below) an unrecognized game still produces a valid,
input-only recording rather than nothing at all.
goodName/gameFamily/recorderSchemaVersionas three independentaxes (spec §3.2):
gameFamilysays which extension events apply,goodNamepins the exact ROM build, andrecorderSchemaVersionis aper-
goodNamecounter bumped whenever this recorder's understanding ofthat build's memory layout changes - independent of the container
format's own version, and independent of
gameFamily. This is themechanism schema 2 and schema 3 (above) both used, rather than bumping the
container
version.Architecture
Source/RMG-Core/Replay.cpp/.hpp- the writer and its small publicAPI (
OnEmulationStart/OnFrame/OnEmulationStop, plus per-sessionoverrides for output path/player names/timestamp used by headless
export). Internally: a per-match state machine (
Idle/WaitingForMatch/Recording), an in-memory event buffer, and adetached worker thread for the match-end compress-and-write step so
stopping emulation (frequently on the UI thread, via
CoreStopEmulation()) never blocks on it.Source/RMG-Core/ReplayMemory.cpp/.hpp- the Smash RemixRDRAM pointer-chase that feeds the writer; not part of the container
format itself, the source of truth for every extension field's
semantics. Fully separate from the writer so a future second game family
doesn't need to touch this file.
Source/RMG/Utilities/KailleraExport/- headless.krec→.rmgrexport tooling (
ReplayFileExport.*,ReplayHook.*), for backfillingrecordings from existing Kaillera netplay recordings without replaying
them interactively.
GameStats_ReplayEnabledsetting and itscheckbox in
SettingsDialog; per-dialog override plumbing in theKaillera playback/lobby dialogs.
Testing
GAME_STATS=ON) against real Smash Remixmatches, including a match recorded on the schema-3 (RNG seed + Remix
settings) build; confirmed the writer produces well-formed files
(header, compression, event stream all parse back out correctly against
the spec).
rmgr-ts(the TypeScriptreader/writer) has been updated through schema 3 and correctly parses
files this writer produces, including the schema-1/2 backward-compat
path (older files still parse with the new fields simply absent).
rmgr-viewer(whichconsumes
rmgr-ts) surfaces the RNG seed and Remix Gameplay/StageSettings in its Replay Info panel.
build-linux/build-windowsmatrices exercise bothGAME_STATS=ONand
GAME_STATS=OFFacross Debug/Release.Known limitations
Called out in
docs/RMGR_SPEC.md§9 in full; the notable ones:game family. The wire format supports a game-agnostic, input-only
recording for any N64 title, but the actual recording trigger
(
OnFrame()'s state machine) still depends on Smash-specific memoryreads (
ReplayMemory::IsInVsMatchScreen()/ReadMatchInfo()) with nogeneric replacement wired up yet - flagged explicitly in code comments
as a follow-up (a game-agnostic PIF-level controller/frame source, of
the kind already used for Kaillera netplay sync, would be the natural
fit).
still isn't captured.
MatchSettingsnow records the RNG seed plusall 31 Gameplay Settings and 8 (core) Stage Settings, but Remix also has
Music Settings, Player Tags, and ~18 named plus ~170 auto-generated
per-stage/per-track random-pool toggles that are deliberately out of
scope for now.
expiration data yet -
ItemUpdaterecords a named kind, position, and(as of schema 2) render scale, but nothing further has been mapped in
memory.
on Dream Land.
field, even though the emulator exposes them.
MatchEnd.endReasoncannot currently distinguish time-out fromstock-out - both collapse to a generic "normal end".
verifies the loaded ROM matches
gameFamily's expectations beyond thegoodNamestring comparison; a mismatch would produce an extension layerfull of garbage bytes rather than falling back to core-only recording.
This is a correctness gap to close, not an accepted design limitation.