From 15c2b5249b06ff82835a15735355eba677ab99cd Mon Sep 17 00:00:00 2001 From: 4Luke4 <39967126+4Luke4@users.noreply.github.com> Date: Sat, 12 Sep 2026 22:20:44 +0200 Subject: [PATCH] Engine Fix Op120 --- EEex-Core-Shared-Files/headers/EEex/op120.hpp | 34 +++ EEex-Core-Shared-Files/source/EEex.cpp | 35 ++- EEex-Core-Shared-Files/source/main.cpp | 22 ++ EEex-Core-Shared-Files/source/op120.cpp | 218 ++++++++++++++++++ EEex-v2.7.3.0/EEex-v2.7.3.0.vcxproj | 8 +- EEex-v2.7.3.0/EEex-v2.7.3.0.vcxproj.filters | 10 +- 6 files changed, 323 insertions(+), 4 deletions(-) create mode 100644 EEex-Core-Shared-Files/headers/EEex/op120.hpp create mode 100644 EEex-Core-Shared-Files/source/op120.cpp diff --git a/EEex-Core-Shared-Files/headers/EEex/op120.hpp b/EEex-Core-Shared-Files/headers/EEex/op120.hpp new file mode 100644 index 0000000..ae8bd73 --- /dev/null +++ b/EEex-Core-Shared-Files/headers/EEex/op120.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include "Baldur_generated.h" + +// Compiled only by EEex-v2.7.3.0. Effect metadata is deliberately private and +// transient: it is not a new EFF field or a serialized modding interface. +struct Op120Attack; + +namespace EEex { + + extern short (*Op120_Original_Swing)(CGameSprite*, CGameSprite*); + extern void (*Op120_Original_AddEffect)(CGameSprite*, CGameEffect*, byte, int, int); + extern int (*Op120_OnList)(CImmunitiesWeapon*, const CWeaponIdentification*); + extern void (*Op120_OverrideWeaponType)(const CSelectiveWeaponTypeList*, const CAIObjectType*, int, int, CWeaponIdentification*); + extern CSelectiveWeaponTypeList* (*Op120_CopySelective)(CSelectiveWeaponTypeList*, const CSelectiveWeaponTypeList*); + extern void (*Op120_ApplyCriticals)(CGameSprite*, CGameSprite*, int, int, int, int); + extern void* Op120_MultiTargetFire[6]; + extern void* Op120_FireMulti; + extern void* Op120_AIUpdateBAM; + + short Op120_Hook_Swing(CGameSprite* sprite, CGameSprite* target); + void Op120_Hook_CaptureWeapon(const CSelectiveWeaponTypeList* overrides, const CAIObjectType* target, int slot, int itemType, CWeaponIdentification* weapon); + int Op120_Hook_RangedImmunity(CGameSprite* sprite, int blocked); + void Op120_Hook_ApplyCriticals(CGameSprite* sprite, CGameSprite* target, int attackType, int itemType, int flags, int mode); + void Op120_Hook_AddEffect(CGameSprite* sprite, CGameEffect* effect, byte list, int noSave, int immediate); + void Op120_OnProjectileAddEffect(CProjectile* projectile, CGameEffect* effect); + + // These access the existing ExEffectInfo map. Copy/destruction remain owned + // by Opcode_Hook_OnCopy/OnDestruct, including the pre-existing bypass bit. + std::shared_ptr Op120_GetEffectAttack(CGameEffect* effect); + void Op120_SetEffectAttack(CGameEffect* effect, std::shared_ptr attack); +} diff --git a/EEex-Core-Shared-Files/source/EEex.cpp b/EEex-Core-Shared-Files/source/EEex.cpp index fee6eef..422c593 100644 --- a/EEex-Core-Shared-Files/source/EEex.cpp +++ b/EEex-Core-Shared-Files/source/EEex.cpp @@ -8,6 +8,9 @@ #include "Baldur_generated.h" #include "EEex.h" +#ifdef EEEX_OP120 +#include "op120.hpp" +#endif #include "engine_function_names.hpp" #include "infinity_loader_util_api.h" #include "lua_util.hpp" @@ -128,11 +131,34 @@ std::unordered_map exScriptDataMap{}; //////////// struct ExEffectInfo { - bool bypassOp120; + bool bypassOp120 = false; +#ifdef EEEX_OP120 + std::shared_ptr op120Attack; +#endif }; std::unordered_map exEffectInfoMap{}; +#ifdef EEEX_OP120 +std::shared_ptr EEex::Op120_GetEffectAttack(CGameEffect* effect) { + // The overwhelmingly common untagged path must not allocate metadata. + if (auto found = exEffectInfoMap.find(effect); found != exEffectInfoMap.end()) { + return found->second.op120Attack; + } + return nullptr; +} + +void EEex::Op120_SetEffectAttack(CGameEffect* effect, std::shared_ptr attack) { + if (attack) { + exEffectInfoMap[effect].op120Attack = std::move(attack); + } + else if (auto found = exEffectInfoMap.find(effect); found != exEffectInfoMap.end()) { + // Do not erase the independent opcode 248/249 bypass flag. + found->second.op120Attack.reset(); + } +} +#endif + //////////////// // Projectile // //////////////// @@ -5038,6 +5064,13 @@ void EEex::Projectile_Hook_OnBeforeAddEffect(CProjectile* pProjectile, CGameAIBa STUTTER_LOG_START(void, "EEex::Projectile_Hook_OnBeforeAddEffect") +#ifdef EEEX_OP120 + // Must precede the mutator early-out: opcode 120 also applies when no + // opcode 408 mutators are installed, and copies made by a mutator must + // inherit the pending recipient check. + Op120_OnProjectileAddEffect(pProjectile, pEffect); +#endif + GUARD_GET_PROJECTILE_MUTATOR_EFFECTS(NORET) lua_State *const L = luaState(); diff --git a/EEex-Core-Shared-Files/source/main.cpp b/EEex-Core-Shared-Files/source/main.cpp index 0bbb7e7..8b0c632 100644 --- a/EEex-Core-Shared-Files/source/main.cpp +++ b/EEex-Core-Shared-Files/source/main.cpp @@ -1,6 +1,9 @@ #include "Baldur_generated.h" #include "EEex.h" +#ifdef EEEX_OP120 +#include "op120.hpp" +#endif #include "EEexLua_generated.h" #include "profiler.hpp" @@ -23,6 +26,25 @@ static constexpr void* getMemberPtr(T func) { static void exportPatterns() { +#ifdef EEEX_OP120 + // Lua validates the complete v2.7.3.0 group before wiring these pointer + // slots and installing the two entry shims. + exportPattern(TEXT("EEex::Op120_Hook_Swing"), EEex::Op120_Hook_Swing); + exportPattern(TEXT("EEex::Op120_Hook_AddEffect"), EEex::Op120_Hook_AddEffect); + exportPattern(TEXT("EEex::Op120_Hook_CaptureWeapon"), EEex::Op120_Hook_CaptureWeapon); + exportPattern(TEXT("EEex::Op120_Hook_RangedImmunity"), EEex::Op120_Hook_RangedImmunity); + exportPattern(TEXT("EEex::Op120_Hook_ApplyCriticals"), EEex::Op120_Hook_ApplyCriticals); + exportPattern(TEXT("EEex::Op120_ApplyCriticals"), &EEex::Op120_ApplyCriticals); + exportPattern(TEXT("EEex::Op120_Original_Swing"), &EEex::Op120_Original_Swing); + exportPattern(TEXT("EEex::Op120_Original_AddEffect"), &EEex::Op120_Original_AddEffect); + exportPattern(TEXT("EEex::Op120_OnList"), &EEex::Op120_OnList); + exportPattern(TEXT("EEex::Op120_OverrideWeaponType"), &EEex::Op120_OverrideWeaponType); + exportPattern(TEXT("EEex::Op120_CopySelective"), &EEex::Op120_CopySelective); + exportPattern(TEXT("EEex::Op120_MultiTargetFire"), EEex::Op120_MultiTargetFire); + exportPattern(TEXT("EEex::Op120_FireMulti"), &EEex::Op120_FireMulti); + exportPattern(TEXT("EEex::Op120_AIUpdateBAM"), &EEex::Op120_AIUpdateBAM); +#endif + /////////////////////////////////////////////// // Hook Integrity Watchdog // /////////////////////////////////////////////// diff --git a/EEex-Core-Shared-Files/source/op120.cpp b/EEex-Core-Shared-Files/source/op120.cpp new file mode 100644 index 0000000..8651b2e --- /dev/null +++ b/EEex-Core-Shared-Files/source/op120.cpp @@ -0,0 +1,218 @@ +#include +#include + +#include "EEex.h" +#include "op120.hpp" + +// Mirrored by pattern/op120/audit.py --layouts against all three PDBs. These +// assertions check the C++ declarations used here, not just the disassembly. +static_assert(sizeof(CWeaponIdentification) == 16); +static_assert(sizeof(CSelectiveWeaponType) == 48); +static_assert(sizeof(CSelectiveWeaponTypeList) == 56); +static_assert(offsetof(CWeaponIdentification, m_attributes) == 12); +static_assert(offsetof(CSelectiveWeaponType, m_type) == 8); +static_assert(offsetof(CSelectiveWeaponType, m_weapon) == 32); +static_assert(offsetof(CProjectile, m_effectList) == 0x78); +static_assert(offsetof(CProjectile, m_extFlags) == 0x188); +static_assert(offsetof(CGameSprite, m_curProjectile) == 0x4AD8); +static_assert(offsetof(CGameSprite, m_liveTypeAI) == 0x4B50); +static_assert(offsetof(CDerivedStats, m_cImmunitiesWeapon) == 0x3C0); + +namespace EEex { + short (*Op120_Original_Swing)(CGameSprite*, CGameSprite*) = nullptr; + void (*Op120_Original_AddEffect)(CGameSprite*, CGameEffect*, byte, int, int) = nullptr; + int (*Op120_OnList)(CImmunitiesWeapon*, const CWeaponIdentification*) = nullptr; + void (*Op120_OverrideWeaponType)(const CSelectiveWeaponTypeList*, const CAIObjectType*, int, int, CWeaponIdentification*) = nullptr; + CSelectiveWeaponTypeList* (*Op120_CopySelective)(CSelectiveWeaponTypeList*, const CSelectiveWeaponTypeList*) = nullptr; + void (*Op120_ApplyCriticals)(CGameSprite*, CGameSprite*, int, int, int, int) = nullptr; + void* Op120_MultiTargetFire[6]{}; + void* Op120_FireMulti = nullptr; + void* Op120_AIUpdateBAM = nullptr; +} + +struct Op120Attack { + + CWeaponIdentification weapon; + EngineVal overrides; + const int slot; + const int itemType; + const uint64_t sourceUUID; + const bool suppressFeedback; + std::unordered_set notifiedRecipients; + + Op120Attack(CGameSprite* source, const CSelectiveWeaponTypeList* original, int slotIn, int typeIn, const CWeaponIdentification& weaponIn) + : weapon(weaponIn), overrides(original->m_nBlockSize), slot(slotIn), itemType(typeIn), + sourceUUID(source->GetUUID()), suppressFeedback(source->m_curAction.m_actionID == 98) + { + // Native assignment deep-copies the CAIObjectType CString and list entries. + // A raw structure copy would retain pointers to the attacker's mutable stats. + EEex::Op120_CopySelective(overrides, original); + } + + ~Op120Attack() { + // Assignment from an empty list uses the engine's element destruction and + // allocator. EngineVal then releases the list's node blocks. ClearAll has + // identical bodies elsewhere in the EXE, so it is not signature-bound. + EngineVal empty(overrides->m_nBlockSize); + EEex::Op120_CopySelective(overrides, empty); + } +}; + +namespace { + + struct SwingContext { + CGameSprite* sprite; + std::shared_ptr attack; + CProjectile* deferredProjectile = nullptr; + bool blockedAtLaunch = false; + SwingContext* previous; + }; + + thread_local SwingContext* currentSwing = nullptr; + + struct SwingScope { + SwingContext context; + + explicit SwingScope(CGameSprite* sprite) : context{sprite, {}, nullptr, false, currentSwing} { + currentSwing = &context; + } + + ~SwingScope() { + currentSwing = context.previous; + } + }; + + bool isMultiTarget(CProjectile* projectile, std::unordered_set& visited) { + if (projectile == nullptr) { + return false; + } + + const auto* vtable = *reinterpret_cast(projectile); + void* fire = reinterpret_cast(vtable->Fire); + if (std::find(std::begin(EEex::Op120_MultiTargetFire), std::end(EEex::Op120_MultiTargetFire), fire) + != std::end(EEex::Op120_MultiTargetFire)) + { + return true; + } + + // BAM AIUpdate tests extFlags BIT1 before entering BounceAI, whose hit + // sweep enumerates recipients. Compare implementation pointers so derived + // classes (including LightningBounce) need no guessed projectile IDs. + if (reinterpret_cast(vtable->AIUpdate) == EEex::Op120_AIUpdateBAM && (projectile->m_extFlags & 2) != 0) { + return true; + } + + // Multi may contain only visual/single-target missiles, or a real area + // projectile. Inspect its already-decoded children instead of changing + // immunity timing for every multi-missile visual. Guard mutator cycles. + if (fire == EEex::Op120_FireMulti) { + // Allocate a visited set only for recursive composite projectiles. + // Ordinary attacks and direct area projectiles need no allocation. + if (!visited.insert(projectile).second) { + return false; + } + for (auto* node = static_cast(projectile)->m_projectiles.m_pNodeHead; node != nullptr; node = node->pNext) { + if (isMultiTarget(node->data, visited)) { + return true; + } + } + } + return false; + } + + void tagEffect(CGameEffect* effect, const std::shared_ptr& attack) { + // Target 9 is redirected to the caster by DeliverEffects/CreateSecondary. + // It is not a hit against the selected recipient. Other caster/party + // selectors are dispatched outside the projectile by LoadProjectile. + if (effect != nullptr && effect->m_targetType != 9) { + EEex::Op120_SetEffectAttack(effect, attack); + } + } +} + +short EEex::Op120_Hook_Swing(CGameSprite* sprite, CGameSprite* target) { + // Native Swing has many exits and can invoke mod callbacks. A native scope + // restores the previous context on every return, including nested attacks. + SwingScope scope(sprite); + return Op120_Original_Swing(sprite, target); +} + +void EEex::Op120_Hook_CaptureWeapon(const CSelectiveWeaponTypeList* overrides, const CAIObjectType* target, int slot, int itemType, CWeaponIdentification* weapon) { + if (currentSwing != nullptr) { + // Capture the raw identification before the engine specializes it for + // the primary target. Delivery replays the native specialization for + // the actual recipient, using this launch-time snapshot. + currentSwing->attack = std::make_shared(currentSwing->sprite, overrides, slot, itemType, *weapon); + } + Op120_OverrideWeaponType(overrides, target, slot, itemType, weapon); +} + +int EEex::Op120_Hook_RangedImmunity(CGameSprite* sprite, int blocked) { + if (currentSwing == nullptr || currentSwing->sprite != sprite || !currentSwing->attack) { + return blocked; + } + std::unordered_set visited; + CProjectile* projectile = sprite->m_curProjectile; + if (!isMultiTarget(projectile, visited)) { + return blocked; + } + + currentSwing->deferredProjectile = projectile; + currentSwing->blockedAtLaunch = blocked != 0; + for (auto* node = projectile->m_effectList.m_pNodeHead; node != nullptr; node = node->pNext) { + tagEffect(node->data, currentSwing->attack); + } + // Only called after the explicit BlockWeaponHit listener has allowed the + // hit. Returning zero also prevents the existing op249 helper from clearing + // the payload; ordinary damage and on-hit effects are then built normally. + return 0; +} + +void EEex::Op120_Hook_ApplyCriticals(CGameSprite* sprite, CGameSprite* target, int attackType, int itemType, int flags, int mode) { + // Swing's ranged success branch also sends immediate critical-hit spell + // messages. These are launch-time attack callbacks, not projectile payload. + // Retaining payload must not manufacture an extra critical callback that + // the original primary-target immunity result would have prevented. + if (currentSwing != nullptr && currentSwing->sprite == sprite + && currentSwing->deferredProjectile != nullptr && currentSwing->blockedAtLaunch) + { + return; + } + Op120_ApplyCriticals(sprite, target, attackType, itemType, flags, mode); +} + +void EEex::Op120_OnProjectileAddEffect(CProjectile* projectile, CGameEffect* effect) { + if (currentSwing != nullptr && projectile != nullptr && projectile == currentSwing->deferredProjectile) { + tagEffect(effect, currentSwing->attack); + } +} + +void EEex::Op120_Hook_AddEffect(CGameSprite* sprite, CGameEffect* effect, byte list, int noSave, int immediate) { + auto attack = Op120_GetEffectAttack(effect); + if (attack) { + // This instance is being delivered, not retained as a projectile template. + // Consume its check before callbacks can copy an accepted/installed effect. + // Other projectile copies retain their own shared context and are checked + // independently, including later scorch/area pulses. + Op120_SetEffectAttack(effect, nullptr); + if (effect->m_targetType != 9 && !ShouldEffectBypassOp120(effect)) { + CWeaponIdentification weapon = attack->weapon; + Op120_OverrideWeaponType(attack->overrides, &sprite->m_liveTypeAI, attack->slot, attack->itemType, &weapon); + if (Op120_OnList(&sprite->GetActiveStats()->m_cImmunitiesWeapon, &weapon)) { + // Match AddEffect's rejection ownership: the incoming effect is + // destroyed with the engine's virtual deleting destructor. In + // particular, noSave must not turn weapon immunity into acceptance. + effect->virtual_Destruct(1); + if (!attack->suppressFeedback && attack->notifiedRecipients.insert(sprite->GetUUID()).second) { + if (CGameSprite* source = GetSpriteFromUUID(attack->sourceUUID)) { + // Same feedback ID/arguments as the native ranged rejection. + EngineVal empty(""); + source->FeedBack(37, 0, 0, 0, -1, 0, empty); + } + } + return; + } + } + } + Op120_Original_AddEffect(sprite, effect, list, noSave, immediate); +} diff --git a/EEex-v2.7.3.0/EEex-v2.7.3.0.vcxproj b/EEex-v2.7.3.0/EEex-v2.7.3.0.vcxproj index c9cdfd8..884f62a 100644 --- a/EEex-v2.7.3.0/EEex-v2.7.3.0.vcxproj +++ b/EEex-v2.7.3.0/EEex-v2.7.3.0.vcxproj @@ -12,6 +12,7 @@ + @@ -50,6 +51,9 @@ + + true + @@ -163,7 +167,7 @@ true true true - SHARED_MEMORY_DLL;_CRT_SECURE_NO_WARNINGS;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + EEEX_OP120;SHARED_MEMORY_DLL;_CRT_SECURE_NO_WARNINGS;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) true stdcpp20 stdc17 @@ -188,4 +192,4 @@ - \ No newline at end of file + diff --git a/EEex-v2.7.3.0/EEex-v2.7.3.0.vcxproj.filters b/EEex-v2.7.3.0/EEex-v2.7.3.0.vcxproj.filters index 03efb42..5e35918 100644 --- a/EEex-v2.7.3.0/EEex-v2.7.3.0.vcxproj.filters +++ b/EEex-v2.7.3.0/EEex-v2.7.3.0.vcxproj.filters @@ -1,5 +1,13 @@  + + + Header Files + + + Source Files + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} @@ -283,4 +291,4 @@ Source Files\EEex-Core-Shared-Files - \ No newline at end of file +