From f446daee580f350399cd8ed7b3d9ea195569ff88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Andr=C3=A9asson?= Date: Sun, 30 Aug 2026 08:33:38 +0200 Subject: [PATCH] fix(rando): clamp upgrade levels so a 4th wallet can't corrupt logic 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. --- soh/soh/Enhancements/randomizer/logic.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/soh/soh/Enhancements/randomizer/logic.cpp b/soh/soh/Enhancements/randomizer/logic.cpp index a54573606..20a2fe5c5 100644 --- a/soh/soh/Enhancements/randomizer/logic.cpp +++ b/soh/soh/Enhancements/randomizer/logic.cpp @@ -2664,6 +2664,13 @@ uint32_t Logic::CurrentInventory(uint32_t item) { } void Logic::SetUpgrade(uint32_t upgrade, uint8_t level) { + // ComboShip: clamp to what the field can hold. UPG_WALLET is 2 bits, so an unclamped 4th + // Progressive Wallet wrote 0x4000 into UPG_BULLET_BAG and the wallet read back as 0 — logic went + // non-monotonic (more wallets = smaller capacity) and priced shop checks became unreachable. + const uint8_t maxLevel = static_cast(gUpgradeMasks[upgrade] >> gUpgradeShifts[upgrade]); + if (level > maxLevel) { + level = maxLevel; + } mSaveContext->inventory.upgrades &= gUpgradeNegMasks[upgrade]; mSaveContext->inventory.upgrades |= level << gUpgradeShifts[upgrade]; }