Skip to content

fix(rando): clamp upgrade levels so a 4th wallet can't corrupt logic - #193

Merged
Varuuna merged 1 commit into
developfrom
fix/wallet-upgrade-overflow
Aug 31, 2026
Merged

fix(rando): clamp upgrade levels so a 4th wallet can't corrupt logic#193
Varuuna merged 1 commit into
developfrom
fix/wallet-upgrade-overflow

Conversation

@Varuuna

@Varuuna Varuuna commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Problem

Seed generation failed repeatedly with advancement items stranded in OOT shop slots:

validation failed - mmAdvUnreachable=0 ootAdvUnreachable=1
stranded: oot:Gold Skulltula Token @ oot:Market Potion Shop Item 7

Root cause

UPG_WALLET is a 2-bit field (gUpgradeMasks[4] = 0x3000, max level 3), but Logic::SetUpgrade wrote level << shift with no bounds check, and the progressive-wallet effect increments without a clamp.

A 4th RG_PROGRESSIVE_WALLET application therefore wrote 4 << 12 = 0x4000 — outside the wallet's mask, into UPG_BULLET_BAG's bits — and CurrentUpgrade(UPG_WALLET) read back 0. Modelled wallet capacity collapsed 999 -> 99.

Measured in a failing generation, bracketing one oracle query:

point upgrades raw wallet nibble level capacity
after Reset 0x120000 0x0000 0 99
after SetOwnedItems (3 wallets) 0x36F4DB 0x3000 3 999
after reachability search 0x574524 0x4000 0 99

This makes logic non-monotonic — collecting more wallets makes shop checks less reachable — which an assumed fill cannot tolerate. Every shuffled shop slot priced above 99 became permanently unreachable, so the fill placed items into slots its own validation could never reach.

Why it surfaced now

The bug is old but was inert. Until the price-aware fill/oracle work (f147be263, 2026-07-13), the oracle never re-established shop prices after ItemReset, so GetCheckPrice() returned 0 and 0 <= 99 passed for every shop slot regardless of wallet state. Once prices became real, any seed with shopsanity and prices above 99 hit the overflow immediately.

Fix

Clamp the level to what the field can hold before shifting. Masking the write alone is not sufficient — (4 << 12) & 0x3000 is still 0, i.e. the same corruption.

Follow-ups (not in this PR)

  • The 4th application's origin is still unexplained: StartingWallet=0 and the pool holds 3. The clamp makes it harmless (3 is the correct ceiling), but it is worth tracing.
  • The starting inventory is applied twice per oracle query — Combo_SOH_Rando_Reset (OTRGlobals.cpp:5096) and again via ResetLogic(applyInventory=true) (fill.cpp:524 -> :329). Native applies it once, so every starting progressive is double-counted in logic. Likely the source of the 4th wallet, but fixing it changes logic for every seed with a starting inventory, so it deserves its own change.

Testing

  • Built Debug; comborando generates cleanly.
  • In-app generation confirmation still pending.

Build Artifacts

UPG_WALLET is a 2-bit field, but SetUpgrade wrote level << shift unmasked.
A 4th Progressive Wallet application wrote 0x4000 - outside the wallet's
mask and into UPG_BULLET_BAG - so CurrentUpgrade(UPG_WALLET) read back 0
and the modelled capacity collapsed from 999 to 99.

That made logic non-monotonic (more wallets = less reachable), which an
assumed fill cannot tolerate: every shuffled shop slot priced above 99
became permanently unreachable, and generation failed validation with
items stranded in shops. Masking the write is not enough (4 & 3 == 0);
the level itself has to be capped at what the field holds.
@Varuuna
Varuuna merged commit 2caad20 into develop Aug 31, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant