Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.DS_Store
6 changes: 3 additions & 3 deletions tools/randomizer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion tools/randomizer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions tools/randomizer/azurik_gui/tab_qol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
2 changes: 1 addition & 1 deletion tools/randomizer/azurik_gui/tab_randomizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
57 changes: 26 additions & 31 deletions tools/randomizer/azurik_mod.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand All @@ -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",
Expand Down