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
34 changes: 34 additions & 0 deletions EEex-Core-Shared-Files/headers/EEex/op120.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#pragma once

#include <memory>

#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<Op120Attack> Op120_GetEffectAttack(CGameEffect* effect);
void Op120_SetEffectAttack(CGameEffect* effect, std::shared_ptr<Op120Attack> attack);
}
35 changes: 34 additions & 1 deletion EEex-Core-Shared-Files/source/EEex.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -128,11 +131,34 @@ std::unordered_map<void*, ExScriptData> exScriptDataMap{};
////////////

struct ExEffectInfo {
bool bypassOp120;
bool bypassOp120 = false;
#ifdef EEEX_OP120
std::shared_ptr<Op120Attack> op120Attack;
#endif
};

std::unordered_map<void*, ExEffectInfo> exEffectInfoMap{};

#ifdef EEEX_OP120
std::shared_ptr<Op120Attack> 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<Op120Attack> 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 //
////////////////
Expand Down Expand Up @@ -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();
Expand Down
22 changes: 22 additions & 0 deletions EEex-Core-Shared-Files/source/main.cpp
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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 //
///////////////////////////////////////////////
Expand Down
218 changes: 218 additions & 0 deletions EEex-Core-Shared-Files/source/op120.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
#include <algorithm>
#include <unordered_set>

#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<CSelectiveWeaponTypeList> overrides;
const int slot;
const int itemType;
const uint64_t sourceUUID;
const bool suppressFeedback;
std::unordered_set<uint64_t> 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<CSelectiveWeaponTypeList> empty(overrides->m_nBlockSize);
EEex::Op120_CopySelective(overrides, empty);
}
};

namespace {

struct SwingContext {
CGameSprite* sprite;
std::shared_ptr<Op120Attack> 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<CProjectile*>& visited) {
if (projectile == nullptr) {
return false;
}

const auto* vtable = *reinterpret_cast<const CProjectile::vtbl* const*>(projectile);
void* fire = reinterpret_cast<void*>(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<void*>(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<CProjectileMulti*>(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<Op120Attack>& 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<Op120Attack>(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<CProjectile*> 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<CString> empty("");
source->FeedBack(37, 0, 0, 0, -1, 0, empty);
}
}
return;
}
}
}
Op120_Original_AddEffect(sprite, effect, list, noSave, immediate);
}
8 changes: 6 additions & 2 deletions EEex-v2.7.3.0/EEex-v2.7.3.0.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\EEex-Core-Shared-Files\headers\common\eeex_api.h" />
<ClInclude Include="..\EEex-Core-Shared-Files\headers\EEex\op120.hpp" />
<ClInclude Include="..\EEex-Core-Shared-Files\headers\EEex\coordinate_util.hpp" />
<ClInclude Include="..\EEex-Core-Shared-Files\headers\EEex\debug_window.hpp" />
<ClInclude Include="..\EEex-Core-Shared-Files\headers\EEex\EEex.h" />
Expand Down Expand Up @@ -50,6 +51,9 @@
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\EEex-Core-Shared-Files\source\common.cpp" />
<ClCompile Include="..\EEex-Core-Shared-Files\source\op120.cpp">
<ExcludedFromBuild Condition="'$(Platform)'!='x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\EEex-Core-Shared-Files\source\coordinate_util.cpp" />
<ClCompile Include="..\EEex-Core-Shared-Files\source\debug.cpp" />
<ClCompile Include="..\EEex-Core-Shared-Files\source\debug_window.cpp" />
Expand Down Expand Up @@ -163,7 +167,7 @@
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>SHARED_MEMORY_DLL;_CRT_SECURE_NO_WARNINGS;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>EEEX_OP120;SHARED_MEMORY_DLL;_CRT_SECURE_NO_WARNINGS;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<LanguageStandard_C>stdc17</LanguageStandard_C>
Expand All @@ -188,4 +192,4 @@
<ImportGroup Label="ExtensionTargets">
<Import Project="$(VCTargetsPath)\BuildCustomizations\masm.targets" />
</ImportGroup>
</Project>
</Project>
Loading