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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
!/style/*.*
!/.gitattributes
!/.gitignore
!/.gitmessage
!/EEex.png
!/package_mod.bat
!/README.md
!/README.md
33 changes: 33 additions & 0 deletions .gitmessage
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# <type>(<scope>): <imperative summary>
#
# Example:
# fix(opcode): preserve natural roll for op138 crit checks
#
# Conventional Commit guidance:
# - type: feat, fix, refactor, perf, build, docs, test, chore
# - scope: use one precise EEex area
# - summary: imperative, specific, <= 72 chars, no trailing period
#
# Suggested EEex scopes:
# action, opcode, sprite, menu, ui, loader, patch, tp2, docs, packaging
#
# Why:
# - State the concrete bug, limitation, or motivation.
# - Prefer engine behavior over implementation trivia.
#
# What:
# - Summarize the code and behavior changes.
# - Call out hook sites, scripts, or data files when relevant.
#
# Validation:
# - List the checks you actually ran.
# - Include build status, luaparse/tests, runtime checks, or engine variants.
#
# Risks / Notes:
# - Record follow-up work, compatibility concerns, or deliberate tradeoffs.
#
# Refs:
# - Optional issue, artifact, dump, or investigation note.
#
# BREAKING CHANGE:
# - Optional footer when behavior or interfaces change incompatibly.
32 changes: 32 additions & 0 deletions EEex/copy/EEex_Action.lua
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,31 @@ function EEex_Action_Private_SpellObjectOffset(aiBase, curAction, bOnlySprite, r
return realActionFunc(aiBase)
end

local function EEex_Action_Private_AttackOnce(aiBase, curAction)

if not aiBase:isSprite(true) then
return EEex_Action_ReturnType.ACTION_ERROR
end

local target = aiBase:GetTargetShareType1(curAction.m_acteeID, CGameObjectType.SPRITE)
if target == nil then
return EEex_Action_ReturnType.ACTION_ERROR
end

local sprite = EEex_GameObject_CastUT(aiBase)
sprite:UpdateTarget(target)

-- Use the engine's normal Attack() path so pathing, ranged projectile creation, and
-- weapon selection behave exactly like a regular attack. The native op138 bridge stops
-- the action after the first matched real roll; doing that purely in Lua was not
-- reliable once the sprite had to walk into range first.
curAction.m_actionID = 3 -- Attack
curAction.m_dest.x = target.m_pos.x
curAction.m_dest.y = target.m_pos.y

return aiBase:virtual_ExecuteAction()
end

EEex_Action_Private_Switch = {

-- Bug Fix: Enable ForceSpellRange / ForceSpellRangeRES
Expand Down Expand Up @@ -287,6 +312,13 @@ EEex_Action_Private_Switch = {
return EEex_Action_ReturnType.ACTION_DONE
end,

-- EEex_AttackOnce
[475] = function(aiBase, curAction)
-- This is a thin wrapper around Attack(). The "exactly one swing" behavior is
-- finalized natively once the first real attack roll is consumed.
return EEex_Action_Private_AttackOnce(aiBase, curAction)
end,

-- EEex_SpellObjectOffset / EEex_SpellObjectOffsetRES
[476] = function(aiBase, curAction)
return EEex_Action_Private_SpellObjectOffset(aiBase, curAction, true, 95, CGameSprite.SpellPoint)
Expand Down
5 changes: 5 additions & 0 deletions EEex/copy/EEex_Action_Patch.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
| 473 EEex_MatchObject(S:Chunk*) |
| 473 EEex_MatchObjectEx(S:Chunk*,I:Nth*,I:Range*,I:Flags*X-MATOBJ) |
| 474 EEex_SetTarget(S:Name*,O:Target*) |
| 475 EEex_AttackOnce(O:Target*) |
| 476 EEex_SpellObjectOffset(O:Target*,I:Spell*Spell,P:Offset*) |
| 476 EEex_SpellObjectOffsetRES(S:RES*,O:Target*,P:Offset*) |
| 477 EEex_SpellObjectOffsetNoDec(O:Target*,I:Spell*Spell,P:Offset*) |
Expand Down Expand Up @@ -72,6 +73,10 @@
+----------------------------------------------------------------------------------+
--]]

-- opcode 138 depends on this native callback as well: the wrapper action can queue
-- a normal Attack(), but only the engine knows when movement/pathing has finished and
-- the real attack action has actually started for the chosen target.

EEex_HookAfterCallWithLabels(EEex_Label("CGameSprite::SetCurrAction()-LastCall"), {
{"hook_integrity_watchdog_ignore_registers", {EEex_HookIntegrityWatchdogRegister.RAX}}},
{[[
Expand Down
235 changes: 235 additions & 0 deletions EEex/copy/EEex_Fix.lua
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,241 @@ function EEex_Fix_Hook_OnSpellOrSpellPointStartedCastingGlow(sprite)
EEex_GetUDAux(sprite)["EEex_Fix_HasSpellOrSpellPointStartedCasting"] = 1
end

-----------------------------------------------------------------------------------------------------
-- Fix SPLPROT.2DA stat comparisons not respecting signed stat storage (e.g. negative resistances) --
-----------------------------------------------------------------------------------------------------

-- The patch-side assembly receives the raw stat id as an unsigned 16-bit value.
-- A 64 KiB byte map lets the hook answer "is this stat stored as signed?" with
-- one indexed load and no Lua / table lookup inside the hot compare path.
-- 0x10000 bytes = one byte for every possible 16-bit stat id value.
EEex_Fix_Private_SignedSplprotStatBitmap = EEex_Malloc(0x10000)
-- Default every entry to 0 ("not known to require signed relational compares").
-- This initial clear is a defensive safe default for freshly allocated native memory,
-- before the game-state initialization listener has had a chance to populate the bitmap.
-- Initialization below flips only the confirmed signed stat ids to 1.
EEex_Memset(EEex_Fix_Private_SignedSplprotStatBitmap, 0, 0x10000)

-- Source of truth for signedness:
-- * CDerivedStats::GetAtOffset() in the game executable
-- * CDerivedStatsTemplate in the matching PDB
--
-- The generated manifest contains the vanilla stat ids whose engine-native storage is signed.
--
-- Variants: BGEE, BG2EE, IWDEE

-- NOTE:
-- This list is generated from EXE/PDB analysis. It is embedded here so the
-- runtime hook can stay data-only: the hook just consults the bitmap and does
-- not need to know anything about engine field names or IDS labels.
EEex_Fix_Private_SignedSplprotStatIDs = {
1, -- m_nMaxHitPoints
2, -- m_nArmorClass
3, -- m_nACCrushingMod
4, -- m_nACMissileMod
5, -- m_nACPiercingMod
6, -- m_nACSlashingMod
7, -- m_nTHAC0
8, -- m_nNumberOfAttacks
9, -- m_nSaveVSDeath
10, -- m_nSaveVSWands
11, -- m_nSaveVSPoly
12, -- m_nSaveVSBreath
13, -- m_nSaveVSSpell
14, -- m_nResistFire
15, -- m_nResistCold
16, -- m_nResistElectricity
17, -- m_nResistAcid
18, -- m_nResistMagic
19, -- m_nResistMagicFire
20, -- m_nResistMagicCold
21, -- m_nResistSlashing
22, -- m_nResistCrushing
23, -- m_nResistPiercing
24, -- m_nResistMissile
25, -- m_nLore
26, -- m_nLockPicking
27, -- m_nMoveSilently
28, -- m_nTraps
29, -- m_nPickPocket
30, -- m_nFatigue
31, -- m_nIntoxication
32, -- m_nLuck
33, -- m_nTracking
35, -- m_nSex
36, -- m_nSTR
37, -- m_nSTRExtra
38, -- m_nINT
39, -- m_nWIS
40, -- m_nDEX
41, -- m_nCON
42, -- m_nCHR
48, -- m_nReputation
49, -- m_nHatedRace
50, -- m_nDamageBonus
51, -- m_nSpellFailureMage
52, -- m_nSpellFailurePriest
53, -- m_nSpellDurationModMage
54, -- m_nSpellDurationModPriest
55, -- m_nTurnUndeadLevel
56, -- m_nBackstabDamageMultiplier
57, -- m_nLayOnHandsAmount
58, -- m_bHeld
59, -- m_bPolymorphed
60, -- m_nTranslucent
61, -- m_bIdentifyMode
62, -- m_bEntangle
63, -- m_bSanctuary
64, -- m_bMinorGlobe
65, -- m_bShieldGlobe
66, -- m_bGrease
67, -- m_bWeb
70, -- m_bCasterHold
71, -- m_nEncumberance
72, -- m_nMissileTHAC0Bonus
73, -- m_nMagicDamageResistance
74, -- m_nResistPoison
75, -- m_bDoNotJump
76, -- m_bAuraCleansing
77, -- m_nMentalSpeed
78, -- m_nPhysicalSpeed
79, -- m_nCastingLevelBonusMage
80, -- m_nCastingLevelBonusCleric
81, -- m_bSeeInvisible
82, -- m_bIgnoreDialogPause
83, -- m_nMinHitPoints
84, -- m_THAC0BonusRight
85, -- m_THAC0BonusLeft
86, -- m_DamageBonusRight
87, -- m_DamageBonusLeft
88, -- m_nStoneSkins
89, -- m_nProficiencyBastardSword
90, -- m_nProficiencyLongSword
91, -- m_nProficiencyShortSword
92, -- m_nProficiencyAxe
93, -- m_nProficiencyTwoHandedSword
94, -- m_nProficiencyKatana
95, -- m_nProficiencyScimitarWakisashiNinjaTo
96, -- m_nProficiencyDagger
97, -- m_nProficiencyWarhammer
98, -- m_nProficiencySpear
99, -- m_nProficiencyHalberd
100, -- m_nProficiencyFlailMorningStar
101, -- m_nProficiencyMace
102, -- m_nProficiencyQuarterStaff
103, -- m_nProficiencyCrossbow
104, -- m_nProficiencyLongBow
105, -- m_nProficiencyShortBow
106, -- m_nProficiencyDart
107, -- m_nProficiencySling
108, -- m_nProficiencyBlackjack
109, -- m_nProficiencyGun
110, -- m_nProficiencyMartialArts
111, -- m_nProficiency2Handed
112, -- m_nProficiencySwordAndShield
113, -- m_nProficiencySingleWeapon
114, -- m_nProficiency2Weapon
115, -- m_nProficiencyClub
116, -- m_nExtraProficiency2
117, -- m_nExtraProficiency3
118, -- m_nExtraProficiency4
119, -- m_nExtraProficiency5
120, -- m_nExtraProficiency6
121, -- m_nExtraProficiency7
122, -- m_nExtraProficiency8
123, -- m_nExtraProficiency9
124, -- m_nExtraProficiency10
125, -- m_nExtraProficiency11
126, -- m_nExtraProficiency12
127, -- m_nExtraProficiency13
128, -- m_nExtraProficiency14
129, -- m_nExtraProficiency15
130, -- m_nExtraProficiency16
131, -- m_nExtraProficiency17
132, -- m_nExtraProficiency18
133, -- m_nExtraProficiency19
134, -- m_nExtraProficiency20
135, -- m_nHideInShadows
136, -- m_nDetectIllusion
137, -- m_nSetTraps
138, -- m_nPuppetMasterId
139, -- m_nPuppetMasterType
140, -- m_nPuppetType
141, -- m_nPuppetId
142, -- m_bCheckForBerserk
143, -- m_bBerserkStage1
144, -- m_bBerserkStage2
145, -- m_nDamageLuck
147, -- m_nVisualRange
148, -- m_bExplore
149, -- m_bThrullCharm
150, -- m_bSummonDisable
151, -- m_nHitBonus
153, -- m_bForceSurge
154, -- m_nSurgeMod
155, -- m_bImprovedHaste
166, -- m_nMeleeTHAC0Bonus
167, -- m_nMeleeDamageBonus
168, -- m_nMissileDamageBonus
169, -- m_bDisableCircle
170, -- m_nFistTHAC0Bonus
171, -- m_nFistDamageBonus
174, -- m_bPreventSpellProtectionEffects
175, -- m_bImmunityToBackStab
176, -- m_nLockPickingMTPBonus
177, -- m_nMoveSilentlyMTPBonus
178, -- m_nTrapsMTPBonus
179, -- m_nPickPocketMTPBonus
180, -- m_nHideInShadowsMTPBonus
181, -- m_nDetectIllusionMTPBonus
182, -- m_nSetTrapsMTPBonus
183, -- m_bPreventAISlowDown
184, -- m_nExistanceDelayOverride
185, -- m_bAnimationOnlyHaste
186, -- m_bNoPermanentDeath
187, -- m_bImmuneToTurnUndead
188, -- m_bSummonDisableAction
189, -- m_nChaosShield
190, -- m_bNPCBump
191, -- m_bUseAnyItem
192, -- m_nAssassinate
193, -- m_bSexChanged
194, -- m_nSpellFailureInnate
195, -- m_bImmuneToTracking
196, -- m_bDeadMagic
197, -- m_bImmuneToTimeStop
198, -- m_bImmuneToSequester
199, -- m_nStoneSkinsGolem
200, -- m_nLevelDrain
201, -- m_bDoNotDraw
202, -- m_bIgnoreDrainDeath
}

-- Special cases intentionally excluded from EEex_Fix_Private_SignedSplprotStatIDs:
-- Stat id 146: GetAtOffset() does not resolve this case to a single CDerivedStatsTemplate field load.
-- m_nSpellDurationModBard: signed CDerivedStatsTemplate field, but no vanilla GetAtOffset() case returns it.
-- m_nClassTypeOverrideMixed: signed CDerivedStatsTemplate field, but no vanilla GetAtOffset() case returns it.
-- m_nClassTypeOverrideLower: signed CDerivedStatsTemplate field, but no vanilla GetAtOffset() case returns it.

local function EEex_Fix_Private_InitializeSignedSplprotStatBitmap()
-- Rebuild from the manifest each time the game state initializes so the
-- bitmap always reflects the authoritative signed-id list for this build.
-- This clear is still required even though the buffer was zeroed at allocation time:
-- reinitialization only writes 1-bits for signed ids, so stale entries must be cleared first.
EEex_Memset(EEex_Fix_Private_SignedSplprotStatBitmap, 0, 0x10000)

for _, statID in ipairs(EEex_Fix_Private_SignedSplprotStatIDs) do
-- Presence means "treat relational SPLPROT compares for this stat as signed".
EEex_Write8(EEex_Fix_Private_SignedSplprotStatBitmap + statID, 1)
end
end

EEex_GameState_AddInitializedListener(function()
-- The patch code reads this bitmap from native memory, so populate it only
-- once EEex runtime state is fully available for the current game session.
EEex_Fix_Private_InitializeSignedSplprotStatBitmap()
end)

--------------------------------------------
-- Fix Baldur.lua values not escaping '\' --
--------------------------------------------
Expand Down
Loading