diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/tools/randomizer/CHANGELOG.md b/tools/randomizer/CHANGELOG.md index eef523f..5c09b0f 100644 --- a/tools/randomizer/CHANGELOG.md +++ b/tools/randomizer/CHANGELOG.md @@ -32,8 +32,8 @@ - **Auto-integration** — Entity editor edits automatically included when building randomized ISO #### QoL Patches -- **Fist pump animation disabled** — Found and patched the real animation trigger in the player state machine (0x2B0F2), not just the one-time notification -- **Three QoL patches now**: gem first-pickup popups (5), obsidian first-pickup notification, per-pickup fist pump animation +- **All pickup celebration animations disabled** — JMP at VA 0x413EE skips the linked-list cleanup and counter update that keep the celebration animation data live, while FUN_00061360 (collected flag) and FUN_0006FC90 (pickup counter) still run for save persistence +- **Two QoL patches now**: gem first-pickup popups (5), pickup celebration animations #### GUI Improvements - Warning labels on Keys, Barriers, and Connections checkboxes ("may cause unsolvable seeds") @@ -64,5 +64,5 @@ - Major items, keys, gems, barriers randomization - Seed-based reproducibility - GUI with category checkboxes -- QoL patches: gem popups, obsidian animation +- QoL patches: gem popups, pickup animations - CLI and GUI interfaces diff --git a/tools/randomizer/README.md b/tools/randomizer/README.md index ba08ee9..de4efc7 100644 --- a/tools/randomizer/README.md +++ b/tools/randomizer/README.md @@ -9,7 +9,7 @@ A full-game randomizer for **Azurik: Rise of Perathia** (Xbox, 2001). Shuffles c - **Gems**: Diamond, emerald, sapphire, ruby distribution randomized per-level (custom weights supported) - **Barriers**: Element vulnerability randomized - **Custom Item Pool**: Choose exactly how many of each item type to include -- **QoL Patches**: Disables gem first-pickup popups and obsidian fist-pump animation +- **QoL Patches**: Disables gem first-pickup popups and all pickup celebration animations - **Seed-based**: Reproducible results — share a seed to share a run - **GUI and CLI**: Graphical interface or command-line for advanced users diff --git a/tools/randomizer/azurik_gui/tab_qol.py b/tools/randomizer/azurik_gui/tab_qol.py index 836ffe8..081c429 100644 --- a/tools/randomizer/azurik_gui/tab_qol.py +++ b/tools/randomizer/azurik_gui/tab_qol.py @@ -15,9 +15,9 @@ "included_in_randomizer": True, }, { - "key": "disable_obsidian_anim", - "label": "Disable obsidian pickup animation", - "description": "Skips the fist pump animation and voice line when collecting obsidian gems.", + "key": "disable_pickup_anims", + "label": "Disable pickup celebration animations", + "description": "Skips the celebration animation for all collectible pickups (obsidians, keys, powers, disc fragments) without affecting save persistence.", "default": True, "included_in_randomizer": True, }, diff --git a/tools/randomizer/azurik_gui/tab_randomizer.py b/tools/randomizer/azurik_gui/tab_randomizer.py index 7e379d9..c811947 100644 --- a/tools/randomizer/azurik_gui/tab_randomizer.py +++ b/tools/randomizer/azurik_gui/tab_randomizer.py @@ -132,7 +132,7 @@ def _build(self): ("do_gems", "Gems (diamond/emerald/sapphire/ruby per-level)", True), ("do_barriers", "Barriers (element vulnerability)", True), ("do_connections", "Level Connections (randomize exits between levels)", False), - ("do_qol", "QoL Patches (disable popups, obsidian animation)", True), + ("do_qol", "QoL Patches (disable popups, pickup animations)", True), ] for key, label, default in categories: var = tk.BooleanVar(value=default) diff --git a/tools/randomizer/azurik_mod.py b/tools/randomizer/azurik_mod.py index 5d0d158..a058b4d 100644 --- a/tools/randomizer/azurik_mod.py +++ b/tools/randomizer/azurik_mod.py @@ -912,17 +912,21 @@ def cmd_randomize_gems(args): # XBE QoL patch offsets # Gem popup string file offsets (null first byte to disable) GEM_POPUP_OFFSETS = [0x197858, 0x19783C, 0x197820, 0x197800, 0x1977D8] -# Obsidian fist pump animation: patch 6 bytes at file offset 0x0489C3 -OBSIDIAN_ANIM_OFFSET = 0x0489C3 -OBSIDIAN_ANIM_ORIGINAL = bytes([0x8B, 0x86, 0xD8, 0x01, 0x00, 0x00]) -OBSIDIAN_ANIM_PATCH = bytes([0xEB, 0x1C, 0x90, 0x90, 0x90, 0x90]) - -# Per-pickup fist pump animation: player state machine at 0x2B0F2 -# State 0x1E checks absorbed entity type (+0x148); if non-zero, plays animation 0x52 -# Patch the conditional JE to unconditional JMP to always skip the animation -FIST_PUMP_OFFSET = 0x02B0F2 -FIST_PUMP_ORIGINAL = bytes([0x74, 0x0C]) # JE +12 (skip if zero) -FIST_PUMP_PATCH = bytes([0xEB, 0x0C]) # JMP +12 (always skip) +# Disable all pickup celebration animations (file 0x0313EE, VA 0x0413EE). +# The pickup handler FUN_00041390 enters a block for non-gem pickups that: +# (A) FUN_00061360 — sets "collected" flag, adds to save list +# (B) virtual call — FUN_0006FC90, decrements pickup counter (persistence) +# (C) linked-list cleanup — zeroes [this+0x1EC/0x1F0] +# (D) counter update — writes [[[this+0x154]+0xD8]+4] +# Steps C and D keep the pickup entity's animation data live, which its +# per-frame update (FUN_00037950 → FUN_00037AB0) reads to play anim 0x52 +# (the celebration). We replace the first instruction of step C with a JMP +# to the epilog (0x4146F), skipping C+D. Steps A+B still run for persistence. +# Both null-check JZ branches (0x413CA, 0x413D1) already target 0x413EE, +# so they naturally hit our JMP — no other code changes needed. +PICKUP_ANIM_OFFSET = 0x0313EE +PICKUP_ANIM_ORIGINAL = bytes([0x8B, 0x8A, 0xEC, 0x01, 0x00]) # MOV ECX,[EDX+0x1EC] (5 of 6 bytes) +PICKUP_ANIM_PATCH = bytes([0xE9, 0x7C, 0x00, 0x00, 0x00]) # JMP 0x4146F (epilog) # Player character swap: replace "garret4" with another character model # At file offset 0x1976C8, "garret4\0d:\" = 12 bytes, can fit any name up to 11 chars @@ -1958,25 +1962,16 @@ def cmd_randomize_full(args): xbe_data[off] = 0x00 print(f" Disabled 5 gem first-pickup popups") - # Disable obsidian fist pump animation - if OBSIDIAN_ANIM_OFFSET + 6 <= len(xbe_data): - current = bytes(xbe_data[OBSIDIAN_ANIM_OFFSET:OBSIDIAN_ANIM_OFFSET + 6]) - if current == OBSIDIAN_ANIM_ORIGINAL: - xbe_data[OBSIDIAN_ANIM_OFFSET:OBSIDIAN_ANIM_OFFSET + 6] = OBSIDIAN_ANIM_PATCH - print(f" Disabled obsidian first-pickup notification") + # Disable all pickup celebration animations (skip linked-list cleanup + counter update) + patch_len = len(PICKUP_ANIM_ORIGINAL) + if PICKUP_ANIM_OFFSET + patch_len <= len(xbe_data): + current = bytes(xbe_data[PICKUP_ANIM_OFFSET:PICKUP_ANIM_OFFSET + patch_len]) + if current == PICKUP_ANIM_ORIGINAL: + xbe_data[PICKUP_ANIM_OFFSET:PICKUP_ANIM_OFFSET + patch_len] = PICKUP_ANIM_PATCH + print(f" Disabled pickup celebration animation") else: - print(f" WARNING: XBE bytes at 0x{OBSIDIAN_ANIM_OFFSET:X} don't match expected " - f"(got {current.hex()}, expected {OBSIDIAN_ANIM_ORIGINAL.hex()})") - - # Disable per-pickup fist pump animation - if FIST_PUMP_OFFSET + 2 <= len(xbe_data): - current = bytes(xbe_data[FIST_PUMP_OFFSET:FIST_PUMP_OFFSET + 2]) - if current == FIST_PUMP_ORIGINAL: - xbe_data[FIST_PUMP_OFFSET:FIST_PUMP_OFFSET + 2] = FIST_PUMP_PATCH - print(f" Disabled per-pickup fist pump animation") - else: - print(f" WARNING: XBE bytes at 0x{FIST_PUMP_OFFSET:X} don't match expected " - f"(got {current.hex()}, expected {FIST_PUMP_ORIGINAL.hex()})") + print(f" WARNING: XBE bytes at 0x{PICKUP_ANIM_OFFSET:X} don't match expected " + f"(got {current.hex()}, expected {PICKUP_ANIM_ORIGINAL.hex()})") # Player character swap (experimental) player_char = getattr(args, 'player_character', None) @@ -2194,7 +2189,7 @@ def main(): " 3. Gems: diamond/emerald/sapphire/ruby shuffled per-level\n" " 4. Barriers: element vulnerability randomized per-level\n" "\n" - "Also applies QoL patches: disable gem popups + obsidian animation.\n" + "Also applies QoL patches: disable gem popups + pickup animations.\n" "Use --no-major, --no-keys, --no-gems, --no-barriers, --no-qol to skip." )) p_full.add_argument("--iso", required=True, help="Original game .iso") @@ -2214,7 +2209,7 @@ def main(): p_full.add_argument("--no-connections", action="store_true", help="Skip level connection randomization") p_full.add_argument("--no-qol", action="store_true", - help="Skip QoL patches (gem popups, obsidian animation)") + help="Skip QoL patches (gem popups, pickup animations)") p_full.add_argument("--obsidian-cost", type=int, metavar="N", help="Obsidian cost per temple lock (default: 10 = locks at 10,20,...100)") p_full.add_argument("--item-pool",