Fix "Disable obsidian fist pump animation" qol patch#1
Open
MichaelJSr wants to merge 3 commits into
Open
Conversation
Patch the conditional JNZ at VA 0x413A2 in the pickup handler (file 0x313A2) to an unconditional JMP, forcing all pickups to take the "gem path" that skips the stop-pose-delay celebration animation. Found via runtime debugging (LLDB + xemu GDB server): the pickup virtual function at VA 0x41390 checks bit 0x10000000 in [this+0x168] to decide whether to celebrate. Gems have this bit set (no animation); obsidians and other collectibles have it clear (animation plays).
Indicated that the fist pump animation qol now disables the animation for all pickups (not just obsidians)
Author
|
If you accept this pull request, I think pull requests should be squashed into main, so not to bloat the commit log. I will try to keep contributions self contained with a feature per branch so that squashed PR's create 1 nice commit with that feature self contained. |
Collaborator
|
As for me, it seems correct! I’m waiting for confirmation from @JTCPP ;) |
…ations The previous patch (JNZ→JMP at VA 0x413A2) skipped the entire pickup handler block, which disabled animations but also skipped FUN_00061360 (collected flag) and FUN_0006FC90 (pickup counter decrement), breaking save persistence — collected pickups would respawn on level reload. Root cause found via LLDB + xemu GDB server: the celebration animation (anim 0x52) is driven by the pickup entity's own per-frame update (FUN_00037950 → FUN_00037AB0 → FUN_00042910), reading animation data from the entity's linked-list and counter fields at [this+0x1EC] and [[[this+0x154]+0xD8]+4]. New patch at file 0x0313EE (VA 0x0413EE) replaces the first instruction of the linked-list cleanup with JMP 0x4146F, skipping steps C (cleanup) and D (counter update) that keep the animation data live, while preserving steps A (FUN_00061360, collected flag) and B (FUN_0006FC90, pickup counter) needed for save persistence. Both null-check JZ branches (0x413CA, 0x413D1) already target 0x413EE, so they naturally hit the JMP with no additional code changes.
MichaelJSr
added a commit
to MichaelJSr/Elemental_Games_Modding
that referenced
this pull request
Apr 18, 2026
The AdreniumLogo call at 0x05F6E5 lives inside FUN_0005f620, a boot- time state machine. Case 1 reads play_movie_fn's AL return value to decide whether to enter the movie-polling state (AL != 0) or skip straight to the next movie (AL == 0): 0x05f6df: PUSH EBP ; EBP = 0 (scratch zero); arg #2 0x05f6e0: PUSH 0x0019e150 ; &"AdreniumLogo.bik"; arg JTCPP#1 0x05f6e5: CALL play_movie_fn ; __stdcall — callee pops 8 B 0x05f6ea: NEG AL SBB EAX, EAX ADD EAX, 3 ; state = 2 (poll) or 3 (skip) MOV [state], EAX The previous version of this patch NOP'd the whole 10-byte PUSH+CALL pair. That leaked 4 bytes of stack (PUSH EBP's value was never popped) and left AL holding garbage from a prior function — so the state machine drifted into case 2 and polled a movie that never started, hanging the boot on a black screen. Fix: narrow the trampoline site to the 5-byte CALL at 0x05F6E5. The two caller PUSHes now run as normal, and the shim: __attribute__((naked)) void c_skip_logo(void) { __asm__("xorb %al, %al\n\tret $8"); // 30 C0 C2 08 00 } is a precise __stdcall replacement — returns AL=0 (state = 3, skip to prophecy) and pops both args via `ret 8`. The legacy AZURIK_SKIP_LOGO_LEGACY=1 path was broken in the same way and is fixed simultaneously: SKIP_LOGO_SPEC's 10-byte patch now writes `ADD ESP, 4; XOR AL, AL; NOP x5` instead of ten NOPs, popping the 4-byte leftover from PUSH EBP and clearing AL. Tests updated: - tests/test_qol_skip_logo.py pins VA=0x05F6E5, 5-byte replaced_bytes, corrected legacy patch bytes, and asserts PUSH 0x19E150 stays live after apply on the real XBE. - tests/test_trampoline_patch.py pins the new 5-byte shim (30 C0 C2 08 00) and the untouched NEG AL at 0x05F6EA. verify-patches --strict: clean (10 bytes differ, all declared). Full regression: 94 passed. Docs: PATCHES.md updated with the full state-machine analysis and before/after disassembly; CHANGELOG documents the fix. Made-with: Cursor
MichaelJSr
added a commit
to MichaelJSr/Elemental_Games_Modding
that referenced
this pull request
Apr 19, 2026
Builds the MCP test infrastructure we were missing, then uses it
to ship tool #4 from the roadmap. The harness is the key
unblocker — with it in place, any future Ghidra-aware tool
(shim scaffolder, etc.) can be unit-tested against a synthetic
Ghidra instead of requiring a live one.
## Why we need a harness at all
The upstream MCP bridge (``Scripts/bridge_mcp_hydra.py``) wraps
an HTTP REST API that Ghidra's GhydraMCP plugin serves on ports
8192-8201. The MCP layer is great for interactive agent work
but unsuitable for CI:
- Pulls in ``mcp``, ``fastmcp``, stdio transports, JSON-RPC
framing — ~30 transitive deps
- Every call requires a live Ghidra instance (we can't spawn
one in a hermetic test)
- No way to fake failure modes (error codes, malformed
responses) from tests
This commit adds two pieces that together solve the problem:
1. **`GhidraClient`** (new): zero-dep HTTP client using stdlib
``urllib``. Same endpoint contract as the MCP bridge, but
no MCP dependency. Handles auth-origin headers + parses the
structured error envelope Ghidra returns. Typed responses
(``GhidraFunction``, ``GhidraLabel``, ``GhidraProgramInfo``),
typed errors (``GhidraClientError`` with ``status_code`` +
``code`` + ``message``).
2. **`MockGhidraServer`** (new): threaded in-process HTTP server
that re-implements just enough of the plugin's contract to
exercise our client. Bound to an ephemeral port so multiple
tests can run in parallel. Records every request in
``request_log`` so tests can assert on API call sequences.
Zero external deps — tests run on CI without Ghidra or Java
installed.
## Tool #4 — `azurik-mod ghidra-sync`
Takes every named VA we track in Python (azurik.h anchors +
vanilla_symbols.py + patch-site registry) and writes them back
into the open Ghidra project via ``PATCH /functions/{addr}``
(rename / set signature) + ``POST /memory/{addr}/comments/plate``.
Planning pass (``plan_sync``) classifies each VA as:
- ``rename`` — function still has a ``FUN_*`` name
- ``keep`` — function is already named correctly
- ``comment`` — VA isn't a function (data / BSS) but we have
a useful plate comment to leave
Apply pass (``apply_sync``) is opt-in. Refuses to overwrite
user-meaningful names (anything not matching ``FUN_*`` / ``LAB_*``
/ ``DAT_*``) unless ``--force`` is passed.
Live dry-run against Ghidra's default.xbe instance produces:
- 9 function renames (play_movie_fn, gravity_integrate_raw,
entity_lookup, config_name_lookup, dev_menu_flag_check, ...)
- 33 plate comments (patch-site annotations + data anchors)
- 0 keeps (fresh project with no prior sync)
## Bonus — `ghidra-coverage --live`
The same HTTP client plugs into ``build_coverage_report``.
``--live --port 8193`` pulls a fresh function list out of
Ghidra in ~3 s, removing the need to maintain a snapshot JSON
for the common "I'm working right now with Ghidra open"
workflow.
Label iteration is OFF by default in live mode (45k symbols in
the Azurik project take ~30 s to paginate and add no coverage
info the function list doesn't already give). Opt in via the
``include_labels=True`` Python API.
## CLI
Three new subcommands / flags:
```bash
azurik-mod ghidra-coverage --live --port 8193
azurik-mod ghidra-sync # dry-run plan
azurik-mod ghidra-sync --apply # actually modify
azurik-mod ghidra-sync --apply --force --port 8193
```
## Tests
tests/test_ghidra_harness.py (new, 20 cases):
MockServerClientRoundTrip (10):
ping / program_info / get_function / missing-function 404 /
rename / set_signature / set_comment / bad-kind rejection /
iter_labels / iter_functions pagination + request-log check
MockServerErrorPaths (1):
unknown-endpoint envelope surfaces as typed exception
GhidraSyncPlanning (4):
plan classifies rename/keep correctly, apply mutates the
mock, apply skips protected names without --force, --force
overwrites them
GhidraCoverageLiveClient (2):
unlabeled-known detection works against a live client, a
meaningful name is NOT flagged
LiveGhidraReadOnly (3):
skips when nothing answers on :8193; otherwise pings +
fetches program_info + (if default.xbe) fetches a known
function
Full suite: 528 passed, 1 skipped in 96 s (was 508 passed).
## Docs
docs/TOOLING_ROADMAP.md — tool #4 moves from planned → shipped
with the full design rationale + CLI examples.
azurik_mod/xbe_tools/__init__.py now re-exports the client +
mock types so consumers can ``from azurik_mod.xbe_tools import
GhidraClient``.
## Remaining roadmap items
Tool #6 (shim scaffolder with ABI picker) is still planned.
With the harness now in place, it's straightforward — fetch the
hook function's signature via ``GhidraClient.get_function(va)``,
translate to a C prototype, drop into a template. Left as
future work; no blocker.
All other Tier-2 items shipped:
JTCPP#1 xbe swiss-army ✅
#2 ghidra-coverage ✅ (now with --live)
#3 shim-inspect ✅
#4 ghidra-sync ✅ (this commit)
#5 plan-trampoline ✅
#6 shim scaffolder planned
#7 xbr inspect ✅
#8 entity diff ✅
#9 test-for-va ✅
#10 pin_va_* ✅
Made-with: Cursor
MichaelJSr
added a commit
to MichaelJSr/Elemental_Games_Modding
that referenced
this pull request
Apr 19, 2026
…k NOP + dev-menu v4 trampoline + unclamped slider entry Four user-reported issues fixed in one pass. ## JTCPP#1. GUI text-box unclamped past slider bounds ParametricSlider previously clamped typed values to [slider_min, slider_max] on commit. Power users who wanted walk_speed_scale=25 couldn't — the value got silently snapped to 10. The text-box is now unclamped: any finite float commits verbatim, the slider thumb rests at whichever bound is closer, and a [!] badge in the header indicates the exact value is outside the slider's visual range. get_value() returns the exact typed value. Slider drags still operate inside the declared range. ## #2. New jump_speed_scale slider Reverse-engineered the main jump initialiser FUN_00089060 (plays fx/sound/player/jump). The jump velocity scalar lives directly in the player entity at +0x140, written by five `MOV [reg+0x140], 0x41100000` instructions at VAs 0x84ECD / 0x856CE / 0x890E4 / 0x89120 / 0x8D31C (main ground jump + 4 alternate airborne-state entry paths). The new slider rewrites the 4-byte IEEE-754 imm32 at each site with 9.0 * jump_scale. No shared constants touched; no shim needed. - JUMP_SPEED_SCALE ParametricPatch added to PLAYER_PHYSICS_SITES. - CLI: --player-jump-scale (full build), --jump-speed (physics- only) accept any float. - GUI Patches page renders a new "Player jump height" slider (range 0.1-5.0, default 1.0, text-box unclamped). - Dynamic whitelist auto-whitelists the 5 imm32 sites. ## #3. Roll slider now fires on sustained WHITE-button hold Confirmed the 3x boost at VA 0x849E4 IS player-specific but its activation flag is gated by either RIGHT_THUMB (click) or WHITE (one-frame tap, then edge-locked). Result: roll_scale was effectively invisible during sustained play because the engine set the flag for only a single frame per WHITE tap. Fix: additionally NOP the 2-byte JNZ +8 at VA 0x00085200 whenever roll_scale != 1.0, removing the WHITE edge-lock so holding WHITE gives sustained 3x magnitude boost every frame. The byte patch was always correct — this change makes it observable in gameplay. ## #4. enable_dev_menu rewritten (v4) to trampoline FUN_00053750 v1-v3 post-mortem: all patched upstream branches inside dev_menu_flag_check, but the main New-Game → cutscene → first- level flow goes through FUN_00055AB0 which calls FUN_00053750 DIRECTLY with a hardcoded level name (e.g. "levels/water/w1" after prophecy), bypassing dev_menu_flag_check entirely. That's why v1-v3 looked like "nothing happens" even with bytes verified. v4 hooks the universal entry. At VA 0x00053750, install a 7-byte trampoline (5-byte JMP rel32 + 2 NOPs) that jumps to a 27-byte shim placed via _carve_shim_landing. The shim: 1. Guards: CMP [ESP+0x10], 0 + JNZ — if param_4 != 0 (bink movie path), skip the override so cutscenes still play. 2. Overwrites param_2 (level-name pointer at [ESP+8]) with the VA of "levels/selector" at 0x001A1E3C. 3. Replays the clobbered MOV EAX, [ESP+4] + MOV ECX, [EAX+0x40] so EAX/ECX match vanilla at the return point. 4. JMPs back to SUB ESP, 0x824 at VA 0x00053757. Every level transition — New Game, Load Save, cutscene-end, developer-console loadlevel — now routes to levels/selector regardless of the upstream caller. Movies (bink:) keep playing. ## Docs - docs/LEARNINGS.md: three new sections. - "WHITE-button sustained roll": documents the edge-lock mechanism at VA 0x00085200 and the NOP fix. - "Jump velocity is an imm32 at 5 call sites": documents the 5-site imm32 scan approach to finding physics parameters. - "v4: trampoline FUN_00053750 directly": post-mortem of why v1-v3 didn't work, + the trampoline-based stack-arg rewriting pattern. - CHANGELOG.md: full entry for all four issues. - azurik_mod/patches/enable_dev_menu/__init__.py + README.md: fully rewritten for the v4 trampoline design. - azurik_mod/patches/player_physics/__init__.py: module docstring extended for jump + roll edge-lock NOP behaviour. ## Tests (744 passed / 1 skipped, +7 new) - ApplyJumpSpeedBehaviour (6 tests): the 5 imm32 sites, scale multipliers, site-isolation guards, apply_player_physics routing. - test_roll_scale_nops_white_edge_lock: pins the edge-lock NOP (roll=1.0 leaves JNZ intact; roll=2.0 NOPs it). - DynamicWhitelistFromXbe updated: vanilla yields 9-11 ranges (3 instr + 1 edge-lock + 5 jump + 0-2 shared-const abs32 follows); patched yields 17 ranges. - EnableDevMenuFeature rewritten (8 tests): hook VA, vanilla prologue drift guard, selector-string VA drift guard, JMP installation, shim layout byte-by-byte decode, section- landing, idempotency, dynamic whitelist shape. Made-with: Cursor
MichaelJSr
added a commit
to MichaelJSr/Elemental_Games_Modding
that referenced
this pull request
Apr 19, 2026
…ysics diagnostic Three user-reported "still doesn't work" issues traced to root cause. In every case, v1/v2 landed bytes correctly on disk but targeted the wrong runtime semantics. ## JTCPP#1. Jump: v1 patched the WRONG physics field v1 rewrote 5 ``MOV [reg+0x140], 0x41100000`` imm32 sites, but entity+0x140 is the HORIZONTAL AIR-CONTROL speed, NOT the jump height. Used by FUN_00089480 (airborne physics) as a per-frame mid-air steering multiplier. Patching it scales air-control, not jump height — hence users saw no vertical effect. The ACTUAL jump formula is at VA 0x89160 inside FUN_00089060: FLD [0x001980A8] ; g = 9.8 (gravity) FMUL [ESI + 0x144] ; × h (jump height) FADD ST0, ST0 ; × 2 FSQRT ; v0 = sqrt(2gh) v2 rewrites the FLD at VA 0x89160 to load from an injected 9.8*jump_scale^2 constant instead of the shared gravity global. The SQRT then produces jump_scale * vanilla_v0 — linear scaling on initial velocity, quadratic on peak height. Single 6-byte FLD rewrite + 4-byte injected float. Shared gravity constant untouched (gravity slider owns it). ## #2. Roll: force-always-on so xemu users without WHITE/R3 see effect Byte patches were always applying correctly, but bit 0x40 of the input-state flags only gets set on WHITE tap or RIGHT_THUMB (R3) click — and many xemu input configs don't route those buttons. Users never saw roll_scale take effect. Added 2 × 2-byte force-always-on patches (VA 0x85214: AND AL, 0x40 -> MOV AL, 0x40; VA 0x8521C: XOR DL, AL -> OR DL, AL) that make bit 0x40 unconditionally set every frame, so the roll FMUL fires on every movement regardless of controller input. Simplified inject_roll_mult from ``3 * roll_scale / walk_scale`` to just ``roll_scale`` (the 3× factor was vanilla's WHITE boost baseline — with force-always-on that meaning no longer applies). Semantics are now cleanly compounding: velocity = 7 * walk_scale * roll_scale * raw_stick * direction Both sliders at 1.0 short-circuit to identity. roll=1 no longer installs any roll-related bytes (was wastefully writing no-op FMUL × 1.0 before). ## #3. Dev menu: trampoline bytes verified correct Re-inspected the v4 trampoline against the built XBE. Hook at VA 0x53750 installs JMP to SHIMS section at VA 0x39F000; SHIMS has flags=0x06 (EXECUTABLE | PRELOADED) so the Xbox loader maps it executable. Bytes land correctly. If users still see no effect in-game, they're almost certainly not running the patched ISO. Added a diagnostic CLI so they can verify. ## New CLI: azurik-mod inspect-physics Read-only diagnostic that reports — per slider — whether the bytes are VANILLA / PATCHED (with injected float values) or DRIFTED. Also dumps the roll edge-lock + force-on state and the enable_dev_menu trampoline. Run this FIRST when a patch seems inert: $ azurik-mod inspect-physics --iso Azurik_patched.iso Player physics sliders: gravity [VANILLA] value = 9.8000 m/s^2 walk [PATCHED] inject VA 0x1001D0 = 14.0000 roll (FMUL) [PATCHED] inject VA 0x1001D4 = 3.0000 swim [PATCHED] inject VA 0x1001D8 = 15.0000 jump (FLD) [PATCHED] inject VA 0x1001DC = 39.2000 Roll auxiliary patches: edge-lock [NOPED] bytes = 9090 (VA 0x85200) force-on JTCPP#1 [PATCHED] bytes = b040 (VA 0x85214) force-on #2 [PATCHED] bytes = 0ad0 (VA 0x8521C) enable_dev_menu trampoline: [INSTALLED] hook bytes = e9abb834009090 -> JMP to VA 0x39F000 (section 'SHIMS') ## Files - azurik_mod/patches/player_physics/__init__.py: rewrite apply_jump_speed to target VA 0x89160 FLD; add force-always- on patches; simplify inject_roll_mult; make walk/roll site patching independently opt-in when their slider != 1.0; update module docstring with the corrected jump physics. - azurik_mod/randomizer/commands.py: new cmd_inspect_physics. - azurik_mod/cli.py: register inspect-physics subcommand + update --jump-speed help text. - tests/test_player_speed.py: rewrite ApplyJumpSpeedBehaviour for the new FLD-site target; replace IndependenceSemantics with SliderSemantics reflecting the compounding semantics; add RollForceAlwaysOn pinning the 2-byte force-on patches; update DynamicWhitelistFromXbe for new site counts. - tests/test_player_physics.py: expose JUMP_SPEED_SCALE (no logic change). - docs/LEARNINGS.md: "Jump velocity: the v2 correction" section explaining the +0x140 vs +0x144 distinction + the FLD-retarget approach; keep the v1 section as historical record. - CHANGELOG.md: full entry. 748 tests passed (+4), 0 linter errors. Made-with: Cursor
MichaelJSr
added a commit
to MichaelJSr/Elemental_Games_Modding
that referenced
this pull request
Apr 19, 2026
Two new patches, bringing the pack to 7 sliders total: gravity + walk + roll + swim + jump + air-control + wing-flap. ## JTCPP#1. Horizontal air-control speed slider entity+0x140 stores a per-frame mid-air horizontal steering scalar that FUN_00089480 consumes every airborne frame as `local_16c = entity[+0x140] * magnitude`. Vanilla 9.0. Written by 5 `MOV DWORD [reg+0x140], 0x41100000` imm32 instructions at VAs 0x84ED3, 0x856D4, 0x890EA, 0x89126, 0x8D322. apply_air_control_speed rewrites each imm32 to 9.0 * air_control_scale. These are the 5 sites the pre-v2 jump patch mistakenly targeted — they DO meaningfully affect movement, but HORIZONTALLY not vertically. Jump HEIGHT remains owned by apply_jump_speed (which targets the FLD [0x001980A8] in the sqrt(2gh) formula at VA 0x89160). ## #2. Wing-flap (Air-power double-jump) height slider FUN_00089480's airborne physics adds 8.0 to velocity.z when BOTH the flap button (input flag 0x04) AND the roll flag (0x40, which apply_player_speed's force-always-on enables) are set. The FADD at VA 0x000896EA reads from the shared 0x001A25C0 (which has 4 non-player readers). apply_flap_height rewrites the FADD to FADD [inject_va] where inject_va holds 8.0 * flap_scale, via the shim-landing infrastructure. Shared 0x001A25C0 left untouched. ## User-facing changes - GUI Patches page: two new sliders (range 0.1-10.0, default 1.0, text-box unclamped like the others). - CLI: --player-air-control-scale and --player-flap-scale on randomize-full; --air-control-speed and --flap-height on apply-physics. - apply_player_physics gains air_control_scale and flap_scale kwargs. - azurik-mod inspect-physics reports both new patches. ## Files - azurik_mod/patches/player_physics/__init__.py: new constants (_AIR_CONTROL_SITE_VAS, _VANILLA_AIR_CONTROL, _AIR_CONTROL_IMM32_VANILLA, _FLAP_SITE_VA, _FLAP_SITE_VANILLA, _VANILLA_FLAP_IMPULSE); new ParametricPatches (AIR_CONTROL_SCALE, FLAP_HEIGHT_SCALE); new apply functions apply_air_control_speed and apply_flap_height; dispatcher + registry + dynamic whitelist + docstring updates. - azurik_mod/patches/__init__.py: re-export new symbols. - azurik_mod/randomizer/commands.py: cmd_randomize_full + cmd_apply_physics + cmd_inspect_physics gain the new kwargs and reporting. - azurik_mod/cli.py: --player-air-control-scale, --player-flap-scale, --air-control-speed, --flap-height flags with full help text. - gui/backend.py: forward air_control_scale + flap_height_scale from pack_params to the CLI args. - tests/test_player_speed.py: new ApplyAirControlBehaviour (5 tests) + ApplyFlapHeightBehaviour (6 tests); extended DynamicWhitelistFromXbe for new site counts. - tests/test_player_physics.py: expose AIR_CONTROL_SCALE and FLAP_HEIGHT_SCALE. - tests/test_categories.py: updated slider keys + count. - docs/LEARNINGS.md: "Airborne horizontal-control speed" and "Wing-flap (double-jump) vertical impulse" sections with the FUN_00089060/FUN_00089480 decode. - CHANGELOG.md: full entry. 759 passed (+11), 0 linter errors. Made-with: Cursor
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.
First off, thanks so much for creating this awesome project! This is honestly such cool and exciting work, and I see a ton of potential for this project. Modding Azurik will now be way more manageable now.
I have been testing the waters and trying to wrap my head around these tools and this code, and thus for my first contribution I aimed to do something fairly simple. I noticed that the qol patch for disabling the pickup animation for picking up obsidians was not working properly, so I decided to fix it using Ghidra and Xemu debugging.
The previous patch targeted the wrong code. Through runtime debugging (LLDB + xemu GDB server), I traced the full pickup collection flow:
An earlier attempt (JNZ->JMP at 0x413A2) skipped the entire block, which disabled animations but also skipped steps A+B, breaking save persistence -- collected pickups would respawn on level/save reload.
The fix replaces the first instruction of step C (file 0x0313EE, VA 0x0413EE) with JMP 0x4146F, skipping C+D while preserving A+B. Both null-check JZ branches already target 0x413EE, so they naturally hit the JMP with no additional changes needed. This disables the celebration animation for all non-gem pickups while keeping save persistence intact.
Testing done:
Ran xemu with the "-s" flag to enable the gdb server, then opened a gdb (lldb on Mac) session in order to set a breakpoint for the specific pickup function for obsidians to analyze exactly what was happening.
Proof that picking up obsidians will now not play an animation: https://github.com/user-attachments/assets/a7175e90-9864-4a5a-9b50-9d43b70b8549
Picking up other things like powers and disk fragments will also not play animations: https://github.com/user-attachments/assets/b237678c-2661-4a17-9e26-916a05540698