diff --git a/EEex/copy/EEex_Fix_Patch.lua b/EEex/copy/EEex_Fix_Patch.lua index 5e44e6b..630606c 100644 --- a/EEex/copy/EEex_Fix_Patch.lua +++ b/EEex/copy/EEex_Fix_Patch.lua @@ -176,6 +176,151 @@ cmp ax, -1 ]]}) + --[[ + +------------------------------------------------------------------------------------------------------------------+ + | BUG: v2.6.6.0 - opcode 180 (CGameEffectRestrictEquipItem) is enforced by equip validation, but inventory UI | + | paths only query CImmunitiesItemTypeEquipList::OnList(), so item-specific restrictions never get the usual red | + | tint / usability denial in inventory. | + +------------------------------------------------------------------------------------------------------------------+ + | Fix the UI-side checks in CInfGame::CheckItemUsable(short, ...) and CInfGame::GetItemTint(CItem*) so they | + | consult CImmunitiesItemEquipList::OnList() first, then fall back to the engine's original item-type check. | + | | + | This is intentionally limited to the UI helper paths. The actual equip validation code already checks both | + | opcode 180's item-resref list and opcode 181's item-type list; the missing red overlay was caused by the UI | + | only consulting the latter. | + +------------------------------------------------------------------------------------------------------------------+ + --]] + + local hookRestrictEquipItemUI = function(address, spriteRegister, itemRegister) + -- `address` is the original call to CImmunitiesItemTypeEquipList::OnList(). + -- Replace that call with a small shim that: + -- 1) checks CImmunitiesItemEquipList::OnList() using the item's resref + -- 2) if no opcode 180 match is found, replays the engine's original item-type call + -- + -- The caller-specific registers differ between the two UI helpers, so the + -- sprite/item registers are supplied by the two call sites below. + -- + -- Original helper signatures: + -- CImmunitiesItemEquipList::OnList( + -- rcx = CImmunitiesItemEquipList*, + -- rdx = CResRef*, + -- r8 = unsigned long* outRef, + -- r9 = CGameEffect** outEffect + -- ) + -- + -- CImmunitiesItemTypeEquipList::OnList( + -- rcx = CImmunitiesItemTypeEquipList*, + -- edx = unsigned long itemType, + -- r8 = unsigned long* outRef, + -- r9 = CGameEffect** outEffect + -- ) + -- + -- This means only rcx/rdx differ between the opcode 180 and opcode 181 checks. + -- r8/r9 must be preserved so the surrounding engine code sees the exact same + -- out-parameter locations no matter which helper returns first. + -- + -- Use EEex_HookRemoveCall() specifically because this patch is replacing a + -- single direct `call CImmunitiesItemTypeEquipList::OnList` instruction. + -- That gives us two things we want: + -- 1) the original call is suppressed unless we explicitly replay it + -- 2) `#L(original)` still remains available, so we can fall back to the + -- engine's original opcode 181 behavior after our opcode 180 check misses + -- + -- Other common hook styles are a worse fit here: + -- - EEex_HookAfterCall(): too late, because the type-only check would already + -- have run before we could inject the item-resref check + -- - EEex_HookBeforeCall(): can run before the original call, but it is built + -- around preserving the original call rather than replacing it conditionally + -- - EEex_HookBeforeRestore()/EEex_HookAfterRestore(): workable in principle, + -- but unnecessarily low-level here because the target is already a clean + -- disp32 call site and HookRemoveCall gives us the original target for free + EEex_HookRemoveCall(address, EEex_FlattenTable({ + {[[ + ; Allocate 0x40 bytes so we have: + ; - 0x20 bytes of Windows x64 shadow space for our nested calls + ; - 0x20 bytes of scratch space to save the original rcx/rdx/r8/r9 + sub rsp, 40h + + ; Preserve the original call arguments. If the item-resref check misses, + ; we must invoke the engine's original item-type helper with the same + ; rcx/rdx/r8/r9 argument bundle it was about to receive. + mov qword ptr ss:[rsp+20h], rcx + mov qword ptr ss:[rsp+28h], rdx + mov qword ptr ss:[rsp+30h], r8 + mov qword ptr ss:[rsp+38h], r9 + + ; Mirror the engine's equip-list base selection from the equip-validation + ; paths that already support opcode 180: + ; 0x1588 -> m_derivedStats.m_cImmunitiesItemEquip + ; 0x2230 -> m_tempStats.m_cImmunitiesItemEquip (alternate copy used by the same UI path) + ; + ; The engine uses `ebx` here as the same derived/base-state selector that + ; the nearby item-type check already uses. + mov eax, 2230h + mov ecx, 1588h + test ebx, ebx + cmove ecx, eax + add rcx, ]], spriteRegister, [[ + + ; CImmunitiesItemEquipList::OnList() is keyed by the item's CResRef, + ; which begins at item + 0x10. + lea rdx, qword ptr ds:[]], itemRegister, [[+10h] + + ; Reuse the caller's original out-parameter storage. + mov r8, qword ptr ss:[rsp+30h] + mov r9, qword ptr ss:[rsp+38h] + call #L(CImmunitiesItemEquipList::OnList) + + ; A hit means opcode 180 already populated the out-parameters and return + ; value exactly as the surrounding UI code expects. Skip the original + ; item-type helper in that case. + test eax, eax + jnz return_from_hook + + ; No opcode 180 match: restore the original arguments and fall back to + ; the engine's item-type helper so opcode 181 behavior remains unchanged. + mov rcx, qword ptr ss:[rsp+20h] + mov rdx, qword ptr ss:[rsp+28h] + mov r8, qword ptr ss:[rsp+30h] + mov r9, qword ptr ss:[rsp+38h] + call #L(original) + + return_from_hook: + ; Match the stack depth expected by the hook trampoline before jumping + ; back to the instruction after the original removed call. + add rsp, 40h + jmp #L(return) + ]]}, + })) + end + + -- CInfGame::CheckItemUsable(short, CItem*, ...) stores: + -- rsi -> sprite + -- rdi -> item + -- + -- The loader DB label for this hook lands directly on the original + -- `call CImmunitiesItemTypeEquipList::OnList` inside the short-portrait + -- usability helper. + hookRestrictEquipItemUI( + EEex_Label("Hook-CInfGame::CheckItemUsable(short)-CImmunitiesItemTypeEquipList::OnList()"), + "rsi", + "rdi" + ) + + -- CInfGame::GetItemTint(CItem*) stores: + -- r13 -> sprite + -- rsi -> item + -- + -- This is the path responsible for the red inventory overlay itself. + -- The hook shape is the same as CheckItemUsable(short, ...); only the source + -- registers differ because the surrounding function uses a different register + -- allocation. + hookRestrictEquipItemUI( + EEex_Label("Hook-CInfGame::GetItemTint()-CImmunitiesItemTypeEquipList::OnList()"), + "r13", + "rsi" + ) + --[[ +--------------------------------------------------------------------------------------------------------------------------------+ | Fix a couple of regressions in v2.6 regarding op206/op232/op256 | diff --git a/EEex/loader/InfinityLoader.db b/EEex/loader/InfinityLoader.db index 4d0aa32..0085e5f 100644 --- a/EEex/loader/InfinityLoader.db +++ b/EEex/loader/InfinityLoader.db @@ -379,6 +379,11 @@ Operations=ADD -16 Pattern=4889742410574883EC30488B5908 Operations=ADD -5 +; Used by the opcode 180 inventory-overlay fix in EEex_Fix_Patch.lua. +; This helper is keyed by item CResRef, unlike CImmunitiesItemTypeEquipList::OnList(). +[CImmunitiesItemEquipList::OnList] +Pattern=40534883EC204C8B5108498BD94533C94C890B + [CInfButtonArray::SetQuickSlot] Pattern=48896C24104889742418574881ECE0000000 @@ -2099,6 +2104,60 @@ Operations=ADD 72 Pattern=413B7C2434 Operations=ADD 14 +; UI-only opcode 180 fix: +; replace the original item-type immunity call in CInfGame::CheckItemUsable(short, ...) +; with a shim that checks the item-resref immunity list first, then falls back to the +; engine's original item-type helper. +; +; Address arithmetic used to compute this entry: +; pattern starts at 0x140271688 +; target call is 0x1402716A7 +; delta = 0x1F = 31 +; +; Byte-count arithmetic for the matched sequence: +; 0FB7D0 -> 3 +; 4C8D4C2440 -> 5 +; B868220000 -> 5 +; 4C8D442448 -> 5 +; 85DB -> 2 +; B9C0150000 -> 5 +; 0F44C8 -> 3 +; 4803CE -> 3 +; total -> 31 +; +; The pattern therefore ends on `add rcx, rsi`, and ADD 31 lands on the next byte: +; the E8 opcode of the original `call CImmunitiesItemTypeEquipList::OnList`. +[Hook-CInfGame::CheckItemUsable(short)-CImmunitiesItemTypeEquipList::OnList()] +Pattern=0FB7D04C8D4C2440B8682200004C8D44244885DBB9C01500000F44C84803CE +Operations=ADD 31 + +; UI-only opcode 180 fix: +; replace the original item-type immunity call in CInfGame::GetItemTint(CItem*). +; This is the inventory tint helper that drives the red overlay. +; +; Address arithmetic used to compute this entry: +; pattern starts at 0x1402777FD +; target call is 0x14027781F +; delta = 0x22 = 34 +; +; Byte-count arithmetic for the matched sequence: +; 0FB7D0 -> 3 +; 4C8D4C2430 -> 5 +; B868220000 -> 5 +; 4C8D842498000000 -> 8 +; 85DB -> 2 +; B9C0150000 -> 5 +; 0F44C8 -> 3 +; 4903CD -> 3 +; total -> 34 +; +; This pattern is 3 bytes longer than the CheckItemUsable(short, ...) entry because +; `lea r8, [rsp+98h]` encodes to 8 bytes here, whereas `lea r8, [rsp+48h]` encoded to +; 5 bytes there. That is why this hook needs ADD 34 rather than ADD 31. +[Hook-CInfGame::GetItemTint()-CImmunitiesItemTypeEquipList::OnList()] +Pattern=0FB7D04C8D4C2430B8682200004C8D84249800000085DBB9C01500000F44C84903CD +Operations=ADD 34 + [Hook-CItem::GetUsabilityText()-IsOp319InvertedJmp] Pattern=4183BC2488000000000F84A4000000 Operations=ADD 55