diff --git a/EEex/copy/EEex_scripts/EEex_Item.lua b/EEex/copy/EEex_scripts/EEex_Item.lua new file mode 100644 index 0000000..ce1e861 --- /dev/null +++ b/EEex/copy/EEex_scripts/EEex_Item.lua @@ -0,0 +1,354 @@ +local EEex_Item_Private_ItemExtension = "ITM" +local EEex_Item_Private_MarshalVersion = "V1" +local EEex_Item_Private_PersistentResrefs = {} +local EEex_Item_Private_ItemType = EEex_Resource_ExtToType(EEex_Item_Private_ItemExtension) + +local function EEex_Item_Private_ValidateString(value, errorPrefix, minLength, maxLength) + if type(value) ~= "string" then + EEex_Error(errorPrefix .. tostring(value)) + end + + local length = #value + if length < minLength or length > maxLength then + EEex_Error(errorPrefix .. tostring(value)) + end +end + +local function EEex_Item_Private_ValidateBoolean(value, errorPrefix) + if type(value) ~= "boolean" then + EEex_Error(errorPrefix .. tostring(value)) + end +end + +local function EEex_Item_Private_NormalizeResref(value, fieldName, allowEmpty) + if type(value) == "userdata" and value.get then + value = value:get() + end + if allowEmpty and value == nil then + return "" + end + if type(value) ~= "string" then + EEex_Error(fieldName .. " must be a string or CResRef!") + end + value = value:upper() + if #value > 8 or (not allowEmpty and #value == 0) then + EEex_Error(fieldName .. " must be between " .. (allowEmpty and "0" or "1") .. " and 8 characters!") + end + return value +end + +local function EEex_Item_Private_MakeFourCC(value) + EEex_Item_Private_ValidateString(value, "Invalid FourCC: ", 4, 4) + local byte1, byte2, byte3, byte4 = value:byte(1, 4) + return byte1 + byte2 * 0x100 + byte3 * 0x10000 + byte4 * 0x1000000 +end + +local function EEex_Item_Private_ValidateItem(item) + if type(item) ~= "userdata" or item.pRes == nil then + EEex_Error("item must be a CItem!") + end + return item +end + +-- Runtime ITMs can exist in dimm without a demanded header. Centralize the +-- demand-or-error path so create, copy, marshal, and unmarshal stay consistent. +local function EEex_Item_Private_DemandResourceHeader(resource, errorMessage) + if resource.pHeader == nil then + if resource:Demand() == nil or resource.pHeader == nil then + EEex_Error(errorMessage) + end + end + return EEex_CastUD(resource.pHeader, "Item_Header_st") +end + +local function EEex_Item_Private_GetResource(item) + item = EEex_Item_Private_ValidateItem(item) + + local itemResource = item.pRes + EEex_Item_Private_DemandResourceHeader(itemResource, "Unable to demand item resource!") + + return itemResource +end + +local function EEex_Item_Private_GetOrCreateRuntimeResource(itemResref) + local itemResource + EEex_RunWithStack(CResRef.sizeof, function(stackMem) + local resrefUD = EEex_PtrToUD(stackMem, "CResRef") + resrefUD:set(itemResref) + -- const CResRef* cResRef, int nType, bool createIfNotExists + itemResource = EEex_CastUD(EngineGlobals.dimmGetResObject(resrefUD, EEex_Item_Private_ItemType, true), "CResItem") + end) + if not itemResource then + EEex_Error("Failed to create runtime item resource: " .. itemResref) + end + return itemResource +end + +local function EEex_Item_Private_MarshalResourceHex(resource) + local itemHeader = EEex_Item_Private_DemandResourceHeader(resource, "Unable to demand item resource before marshal!") + + local address = EEex_UDToPtr(itemHeader) + local encoded = {} + for i = 0, resource.nSize - 1 do + encoded[#encoded + 1] = string.format("%02X", EEex_ReadU8(address + i)) + end + return table.concat(encoded) +end + +local function EEex_Item_Private_UnmarshalResourceHex(resource, marshaledHex) + EEex_Item_Private_ValidateString(marshaledHex, "Invalid marshaled item blob: ", 0, math.huge) + if #marshaledHex % 2 ~= 0 then + EEex_Error("Marshaled item blob must have an even hex length!") + end + + local byteCount = #marshaledHex / 2 + if byteCount == 0 then + EEex_Error("Marshaled item blob may not be empty!") + end + + EEex_RunWithStack(byteCount, function(bufferBase) + local writeOffset = 0 + for i = 1, #marshaledHex, 2 do + local byteValue = tonumber(marshaledHex:sub(i, i + 1), 16) + if byteValue == nil then + EEex_Error("Marshaled item blob contains invalid hex!") + end + EEex_WriteU8(bufferBase + writeOffset, byteValue) + writeOffset = writeOffset + 1 + end + + -- CRes* pRes, void* pData, int nSize, bool useTempOverride, bool makeCopy + EngineGlobals.dimmServiceFromMemory(resource, EEex_PtrToUD(bufferBase, "VariableArray"), byteCount, false, true) + end) + + EEex_Item_Private_DemandResourceHeader(resource, "Failed to demand unmarshaled item resource!") +end + +-- Applies simple header overrides. +local function EEex_Item_Private_ApplyHeaderArgs(itemHeader, args) + if args == nil then + return + end + + if type(args) ~= "table" then + EEex_Error("item args must be a table!") + end + + local numericFields = { + "genericName", + "identifiedName", + "itemFlags", + "itemType", + "notUsableBy", + "minLevelRequired", + "minSTRRequired", + "minSTRBonusRequired", + "notUsableBy2a", + "minINTRequired", + "notUsableBy2b", + "minDEXRequired", + "notUsableBy2c", + "minWISRequired", + "notUsableBy2d", + "minCONRequired", + "proficiencyType", + "minCHRRequired", + "baseValue", + "maxStackable", + "loreValue", + "baseWeight", + "genericDescription", + "identifiedDescription", + "attributes", + } + + for _, fieldName in ipairs(numericFields) do + local value = args[fieldName] + if value ~= nil then + itemHeader[fieldName] = value + end + end + + local resrefFields = { + "usedUpItemID", + "itemIcon", + "groundIcon", + "descriptionPicture", + } + + for _, fieldName in ipairs(resrefFields) do + local value = args[fieldName] + if value ~= nil then + itemHeader[fieldName]:set(EEex_Item_Private_NormalizeResref(value, fieldName, true)) + end + end + + if args.animationType ~= nil then + local animationType = args.animationType + EEex_Item_Private_ValidateString(animationType, "Invalid animationType: ", 0, 2) + itemHeader.animationType:set(animationType) + end +end + +local function EEex_Item_Private_GetPersistentResRefs() + local itemResrefs = {} + for itemResref in pairs(EEex_Item_Private_PersistentResrefs) do + itemResrefs[#itemResrefs + 1] = itemResref + end + table.sort(itemResrefs) + return itemResrefs +end + +local function EEex_Item_Private_SetPersistenceEnabled(itemResref, persist) + persist = EEex_Utility_Default(persist, false) + EEex_Item_Private_ValidateBoolean(persist, "Invalid persist value: ") + + if persist then + EEex_Item_Private_PersistentResrefs[itemResref] = true + else + EEex_Item_Private_PersistentResrefs[itemResref] = nil + end +end + +-- Both creation paths finish the same way: demand the new ITM, apply any +-- caller-provided header overrides, and record whether it should survive saves. +local function EEex_Item_Private_FinalizeCreatedResource(itemResource, itemResref, args, persist) + local itemHeader = EEex_Item_Private_DemandResourceHeader(itemResource, "Unable to demand item resource: " .. itemResref) + EEex_Item_Private_ApplyHeaderArgs(itemHeader, args) + EEex_Item_Private_SetPersistenceEnabled(itemResref, persist) + return itemHeader +end + +-- Unmarshal always starts from a clean slate: dump the old persisted ITMs from +-- dimm, then forget the registry so the next marshal payload is authoritative. +local function EEex_Item_Private_DumpPersistentResources() + for itemResref in pairs(EEex_Item_Private_PersistentResrefs) do + local itemResource = EEex_Resource_Fetch(itemResref, EEex_Item_Private_ItemExtension) + if itemResource then + -- CRes* pRes + EngineGlobals.dimmDump(itemResource) + end + end + EEex_Item_Private_PersistentResrefs = {} +end + +-- @bubb_doc { EEex_Item_CreateFromResref } +-- +-- @summary: Creates a new in-memory ITM resource under ``itemResref`` and returns its demanded header. +-- +-- @param { itemResref / type=string }: The resref to assign to the new in-memory item. +-- +-- @param { args / type=table|nil }: +-- Optional scalar header overrides. @EOL +-- +-- @param { persist / type=boolean / default=false }: +-- If ``true``, registers the ITM for marshal/unmarshal persistence; otherwise it remains session-only. @EOL +-- +-- @return { type=Item_Header_st }: The demanded header of the created item. + +function EEex_Item_CreateFromResref(itemResref, args, persist) + itemResref = EEex_Item_Private_NormalizeResref(itemResref, "itemResref") + local itemResource = EEex_Item_Private_GetOrCreateRuntimeResource(itemResref) + + EEex_RunWithStack(Item_Header_st.sizeof, function(bufferBase) + EEex_Memset(bufferBase, 0, Item_Header_st.sizeof) + local itemHeader = EEex_PtrToUD(bufferBase, "Item_Header_st") + itemHeader.nFileType = EEex_Item_Private_MakeFourCC("ITM ") + itemHeader.nFileVersion = EEex_Item_Private_MakeFourCC("V1 ") + itemHeader.abilityOffset = Item_Header_st.sizeof + itemHeader.abilityCount = 0 + itemHeader.effectsOffset = Item_Header_st.sizeof + itemHeader.equipedStartingEffect = 0 + itemHeader.equipedEffectCount = 0 + itemHeader.maxStackable = 1 + + -- CRes* pRes, void* pData, int nSize, bool useTempOverride, bool makeCopy + EngineGlobals.dimmServiceFromMemory(itemResource, EEex_PtrToUD(bufferBase, "VariableArray"), Item_Header_st.sizeof, false, true) + end) + + return EEex_Item_Private_FinalizeCreatedResource(itemResource, itemResref, args, persist) +end + +-- @bubb_doc { EEex_Item_CreateCopy } +-- +-- @summary: Copies the full demanded ITM blob backing ``sourceItem`` into a new in-memory ITM resource under ``itemResref``. +-- +-- @param { itemResref / type=string }: The resref to assign to the new in-memory item. +-- +-- @param { sourceItem / type=CItem }: The source item to copy. +-- +-- @param { args / type=table|nil }: +-- Optional scalar header overrides. @EOL +-- +-- @param { persist / type=boolean / default=false }: +-- If ``true``, registers the ITM for marshal/unmarshal persistence; otherwise it remains session-only. @EOL +-- +-- @return { type=Item_Header_st }: The demanded header of the created item. + +function EEex_Item_CreateCopy(itemResref, sourceItem, args, persist) + itemResref = EEex_Item_Private_NormalizeResref(itemResref, "itemResref") + local sourceResource = EEex_Item_Private_GetResource(sourceItem) + local sourceHeader = EEex_Item_Private_DemandResourceHeader(sourceResource, "Unable to demand item resource!") + local itemResource = EEex_Item_Private_GetOrCreateRuntimeResource(itemResref) + + EEex_RunWithStack(sourceResource.nSize, function(bufferBase) + -- Clone the entire demanded ITM payload, including equipped effects, abilities, + -- and ability-linked effect blocks. + EEex_Memcpy(bufferBase, EEex_UDToPtr(sourceHeader), sourceResource.nSize) + -- CRes* pRes, void* pData, int nSize, bool useTempOverride, bool makeCopy + EngineGlobals.dimmServiceFromMemory(itemResource, EEex_PtrToUD(bufferBase, "VariableArray"), sourceResource.nSize, false, true) + end) + + return EEex_Item_Private_FinalizeCreatedResource(itemResource, itemResref, args, persist) +end + +-------------------------------------------------------------- +-- EEex_Marshal.lua calls these cross-file helpers directly -- +-------------------------------------------------------------- + +function EEex_Item_Private_MarshalPersistentItems() + local itemResrefs = EEex_Item_Private_GetPersistentResRefs() + local marshaled = { EEex_Item_Private_MarshalVersion } + + for _, itemResref in ipairs(itemResrefs) do + local itemResource = EEex_Resource_Fetch(itemResref, EEex_Item_Private_ItemExtension) + if not itemResource then + EEex_Error("Persistent item resource is missing: " .. itemResref) + end + marshaled[#marshaled + 1] = itemResref .. ":" .. EEex_Item_Private_MarshalResourceHex(itemResource) + end + + return table.concat(marshaled, "\n") +end + +function EEex_Item_Private_UnmarshalPersistentItems(marshaled) + EEex_Item_Private_DumpPersistentResources() + + if marshaled == nil or marshaled == "" then + return + end + + EEex_Item_Private_ValidateString(marshaled, "Invalid marshaled item payload: ", 1, math.huge) + + local version + -- The item marshal payload is line-oriented: the first line is the marshal + -- version and each following line is RESREF:HEX for one persisted ITM. + for line in marshaled:gmatch("[^\r\n]+") do + if version == nil then + version = line + if version ~= EEex_Item_Private_MarshalVersion then + EEex_Error("Unsupported item marshal version: " .. tostring(version)) + end + else + local itemResref, marshaledHex = line:match("^(.-):([0-9A-F]*)$") + if itemResref == nil then + EEex_Error("Item marshal payload is corrupted!") + end + itemResref = EEex_Item_Private_NormalizeResref(itemResref, "marshaled item resref") + local itemResource = EEex_Item_Private_GetOrCreateRuntimeResource(itemResref) + EEex_Item_Private_UnmarshalResourceHex(itemResource, marshaledHex) + EEex_Item_Private_SetPersistenceEnabled(itemResref, true) + end + end +end + diff --git a/EEex/copy/EEex_scripts/EEex_Main.lua b/EEex/copy/EEex_scripts/EEex_Main.lua index e183464..0048d94 100644 --- a/EEex/copy/EEex_scripts/EEex_Main.lua +++ b/EEex/copy/EEex_scripts/EEex_Main.lua @@ -36,6 +36,7 @@ EEex_Main_Private_StartupFiles = { "EEex_Projectile", -- "EEex_Projectile_Patch", -- "EEex_Resource", -- + "EEex_Item", -- "EEex_Script", -- "EEex_Script_Patch", -- "EEex_Sprite", -- diff --git a/EEex/copy/EEex_scripts/EEex_Marshal.lua b/EEex/copy/EEex_scripts/EEex_Marshal.lua index 84c18d5..f639d2a 100644 --- a/EEex/copy/EEex_scripts/EEex_Marshal.lua +++ b/EEex/copy/EEex_scripts/EEex_Marshal.lua @@ -15,6 +15,43 @@ EEex_Sprite_AddMarshalHandlers("EEex", end ) +local EEex_Marshal_Private_HasUnmarshaledPersistentItems = false + +EEex_Sprite_AddMarshalHandlers("EEex_Item", + function(sprite) + local toMarshal = {} + if not EEex.IsMarshallingCopy() then + -- Persistent item state lives in one global registry, so only the first + -- portrait sprite carries the marshal payload. This avoids emitting the same + -- blob on every CRE. + if EEex_Sprite_GetInPortraitID(0) == sprite.m_id then + toMarshal["EEex_ItemMarshal"] = EEex_Item_Private_MarshalPersistentItems() + -- Avoid emitting an empty EEex_ItemMarshal payload when there are zero persisted items. + if string.find(toMarshal["EEex_ItemMarshal"], ":", 1, true) == nil then + return nil + end + end + end + return toMarshal + end, + function(sprite, read) + local marshaled = read["EEex_ItemMarshal"] + if marshaled == nil or EEex_Marshal_Private_HasUnmarshaledPersistentItems then + return + end + -- Every sprite importer sees the same global payload. Unmarshal it once per + -- game-state lifetime so later importers do not repeatedly dump and rebuild ITMs. + EEex_Item_Private_UnmarshalPersistentItems(marshaled) + EEex_Marshal_Private_HasUnmarshaledPersistentItems = true + end +) + +EEex_GameState_AddDestroyedListener(function() + -- The load-once gate is game-state scoped because the persistent item registry + -- is global rather than attached to a specific sprite instance. + EEex_Marshal_Private_HasUnmarshaledPersistentItems = false +end) + function EEex_Marshal_Private_OnSummonerLoaded(sprite, loadedSprite) sprite.m_lSummonedBy:Set(loadedSprite:virtual_GetAIType()) end diff --git a/EEex/copy/EEex_scripts/EEex_Opcode_Patch.lua b/EEex/copy/EEex_scripts/EEex_Opcode_Patch.lua index 3e547c9..88f112b 100644 --- a/EEex/copy/EEex_scripts/EEex_Opcode_Patch.lua +++ b/EEex/copy/EEex_scripts/EEex_Opcode_Patch.lua @@ -85,6 +85,62 @@ -- Opcode Changes -- -------------------------------------- + --[[ + +----------------------------------------------------------------------------------------------------------------------------------+ + | CGameEffectCreateItem() [opcode #122 / #255] | + +----------------------------------------------------------------------------------------------------------------------------------+ + | param2 > 0 -> Override the created CItem's flags before the engine places it | + | (special & 1) != 0 -> Save the placed inventory/equipment slot to EEex_GetUDAux(sprite).EEex_CGameEffectCreateItem_Slot | + +----------------------------------------------------------------------------------------------------------------------------------+ + | [EEex.dll] EEex::Opcode_Hook_CGameEffectCreateItem_BeforePlaceItem(pEffect: CGameEffect*, pItem: CItem*) | + | [EEex.dll] EEex::Opcode_Hook_CGameEffectCreateItem_AfterPlaceItem(pEffect: CGameEffect*, pSprite: CGameSprite*, pItem: CItem*) | + +----------------------------------------------------------------------------------------------------------------------------------+ + --]] + + -------------------------------------------------------------------------- + -- [EEex.dll] EEex::Opcode_Hook_CGameEffectCreateItem_BeforePlaceItem() -- + -------------------------------------------------------------------------- + + EEex_HookBeforeAndAfterCallWithLabels(EEex_Label("Hook-CGameEffectCreateItem::ApplyEffect()-CGameAIBase::PlaceItem()"), { + {"hook_integrity_watchdog_ignore_registers_0", { + EEex_HookIntegrityWatchdogRegister.R10, EEex_HookIntegrityWatchdogRegister.R11 + }}}, + {[[ + #MAKE_SHADOW_SPACE(32) + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)], rcx + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-16)], rdx + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-24)], r8 + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-32)], r9 + + mov rdx, rbx ; pItem + mov rcx, r14 ; pEffect + call #L(EEex::Opcode_Hook_CGameEffectCreateItem_BeforePlaceItem) + + mov r9, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-32)] + mov r8, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-24)] + mov rdx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-16)] + mov rcx, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)] + #DESTROY_SHADOW_SPACE + ]]}, + + ------------------------------------------------------------------------- + -- [EEex.dll] EEex::Opcode_Hook_CGameEffectCreateItem_AfterPlaceItem() -- + ------------------------------------------------------------------------- + + {[[ + #MAKE_SHADOW_SPACE(8) + mov qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)], rax + + mov r8, rbx ; pItem + mov rdx, r12 ; pSprite + mov rcx, r14 ; pEffect + call #L(EEex::Opcode_Hook_CGameEffectCreateItem_AfterPlaceItem) + + mov rax, qword ptr ss:[rsp+#SHADOW_SPACE_BOTTOM(-8)] + #DESTROY_SHADOW_SPACE + ]]} + ) + --[[ +------------------------------------------------------------------------------------+ | Opcode #146 | diff --git a/EEex/copy/EEex_scripts/EEex_Resource.lua b/EEex/copy/EEex_scripts/EEex_Resource.lua index 5ecd3c2..7f538eb 100644 --- a/EEex/copy/EEex_scripts/EEex_Resource.lua +++ b/EEex/copy/EEex_scripts/EEex_Resource.lua @@ -162,8 +162,8 @@ end Spell_Header_st.getAbility = EEex_Resource_GetSpellAbility function EEex_Resource_GetItemAbility(itemHeader, abilityIndex) - if itemHeader.abilityCount <= abilityIndex then return end - return EEex_PtrToUD(EEex_UDToPtr(itemHeader) + itemHeader.abilityOffset + Item_Header_st.sizeof * abilityIndex, "Item_ability_st") + if abilityIndex < 0 or itemHeader.abilityCount <= abilityIndex then return end + return EEex_PtrToUD(EEex_UDToPtr(itemHeader) + itemHeader.abilityOffset + Item_ability_st.sizeof * abilityIndex, "Item_ability_st") end Item_Header_st.getAbility = EEex_Resource_GetItemAbility @@ -172,6 +172,697 @@ function EEex_Resource_GetCItemAbility(item, abilityIndex) end CItem.getAbility = EEex_Resource_GetCItemAbility +-------------------------------------- +-- Item Runtime Mutation Helpers -- +-------------------------------------- + +-- This is intentionally looser than EEex_Item.lua's resref normalization: +-- item mutators use nil to mean "clear this field on the rebuilt ITM". +local function EEex_Resource_Private_NormalizeOptionalResRef(value, fieldName) + if value == nil then + return "" + end + if type(value) == "string" then + return value + end + if type(value) == "userdata" and value.get then + return value:get() + end + EEex_Error(fieldName.." must be a string or CResRef!") +end + +local function EEex_Resource_Private_NormalizeAttackProbability(value) + -- Accept either a Lua table or the engine's fixed-size attack-probability + -- array, then normalize both shapes into one plain Lua array for staging. + local attackProbability = {0, 0, 0, 0, 0, 0} + if value == nil then + return attackProbability + end + if type(value) == "table" then + for i = 1, 6 do + local entry = value[i] + if entry == nil then + entry = value[i - 1] + end + if entry ~= nil then + attackProbability[i] = entry + end + end + return attackProbability + end + if type(value) == "userdata" and value.get then + for i = 0, 5 do + attackProbability[i + 1] = value:get(i) + end + return attackProbability + end + EEex_Error("attackProbability must be a table or Array!") +end + +local function EEex_Resource_Private_CopyItemEffectData(effectData) + -- Staging happens in pure Lua tables so effect templates can be copied, + -- inserted, and fanned out across abilities without aliasing userdata. + return { + effectID = effectData.effectID, + targetType = effectData.targetType, + spellLevel = effectData.spellLevel, + effectAmount = effectData.effectAmount, + dwFlags = effectData.dwFlags, + durationType = effectData.durationType, + duration = effectData.duration, + probabilityUpper = effectData.probabilityUpper, + probabilityLower = effectData.probabilityLower, + res = effectData.res, + numDice = effectData.numDice, + diceSize = effectData.diceSize, + savingThrow = effectData.savingThrow, + saveMod = effectData.saveMod, + special = effectData.special, + } +end + +local function EEex_Resource_Private_CopyItemEffect(effect) + return EEex_Resource_Private_CopyItemEffectData({ + effectID = effect.effectID, + targetType = effect.targetType, + spellLevel = effect.spellLevel, + effectAmount = effect.effectAmount, + dwFlags = effect.dwFlags, + durationType = effect.durationType, + duration = effect.duration, + probabilityUpper = effect.probabilityUpper, + probabilityLower = effect.probabilityLower, + res = effect.res:get(), + numDice = effect.numDice, + diceSize = effect.diceSize, + savingThrow = effect.savingThrow, + saveMod = effect.saveMod, + special = effect.special, + }) +end + +local function EEex_Resource_Private_CopyItemAbility(ability) + return { + type = ability.type, + quickSlotType = ability.quickSlotType, + largeDamageDice = ability.largeDamageDice, + quickSlotIcon = ability.quickSlotIcon:get(), + actionType = ability.actionType, + actionCount = ability.actionCount, + range = ability.range, + launcherType = ability.launcherType, + largeDamageDiceCount = ability.largeDamageDiceCount, + speedFactor = ability.speedFactor, + largeDamageDiceBonus = ability.largeDamageDiceBonus, + thac0Bonus = ability.thac0Bonus, + damageDice = ability.damageDice, + school = ability.school, + damageDiceCount = ability.damageDiceCount, + secondaryType = ability.secondaryType, + damageDiceBonus = ability.damageDiceBonus, + damageType = ability.damageType, + effectCount = ability.effectCount, + startingEffect = ability.startingEffect, + maxUsageCount = ability.maxUsageCount, + usageFlags = ability.usageFlags, + abilityFlags = ability.abilityFlags, + missileType = ability.missileType, + attackProbability = EEex_Resource_Private_NormalizeAttackProbability(ability.attackProbability), + } +end + +local function EEex_Resource_Private_GetItemEffect(itemHeader, effectIndex) + if effectIndex < 0 then return end + return EEex_PtrToUD(EEex_UDToPtr(itemHeader) + itemHeader.effectsOffset + Item_effect_st.sizeof * effectIndex, "Item_effect_st") +end + +local function EEex_Resource_Private_FindItemResource(itemHeader) + if not itemHeader then + EEex_Error("itemHeader must be defined!") + end + + -- Mutators receive only an Item_Header_st pointer. Recover the owning CResItem + -- by pointer identity so the rebuilt bytes get written back to the right ITM. + local targetPtr = EEex_UDToPtr(itemHeader) + local itemType = EEex_Resource_ExtToType("ITM") + local resources = EngineGlobals.resources + local resourceData = resources.m_pData + + for i = 0, resources.m_nSize - 1 do + local res = resourceData:get(i) + if res and res.type == itemType and res.bLoaded then + local itemRes = EEex_CastUD(res, "CResItem") + if itemRes.pHeader and EEex_UDToPtr(itemRes.pHeader) == targetPtr then + return itemRes + end + end + end + + EEex_Error("itemHeader must reference a demanded, engine-owned ITM resource!") +end + +local function EEex_Resource_Private_ValidateNonNegativeIndex(index, name) + if type(index) ~= "number" or index % 1 ~= 0 then + EEex_Error(name.." must be an integer!") + end + if index < 0 then + EEex_Error(name.." must be >= 0!") + end + return index +end + +local function EEex_Resource_Private_ValidateAbilityIndexForInsert(abilityIndex) + if abilityIndex == nil then + return -1 + end + if type(abilityIndex) ~= "number" or abilityIndex % 1 ~= 0 then + EEex_Error("abilityIndex must be an integer!") + end + return abilityIndex +end + +local function EEex_Resource_Private_ValidateWildcardIndex(index, name) + if index == nil then + return -1 + end + if type(index) ~= "number" or index % 1 ~= 0 then + EEex_Error(name.." must be an integer!") + end + return index +end + +local function EEex_Resource_Private_NormalizeItemAbilityArgs(abilityArgs) + -- Ability inserts are expressed as plain Lua data first; the ITM is rebuilt + -- only after all defaults and caller overrides have been resolved. + abilityArgs = abilityArgs or {} + if type(abilityArgs) ~= "table" then + EEex_Error("abilityArgs must be a table!") + end + if abilityArgs.effectCount ~= nil then + EEex_Error("effectCount may not be defined!") + end + if abilityArgs.startingEffect ~= nil then + EEex_Error("startingEffect may not be defined!") + end + + local abilityData = { + type = EEex_PackWord(0x3, 0x1), -- Type: Magical, Type flags: Usable after Identification + quickSlotType = 3, -- Ability location: Quick-item slot / Use Item button + largeDamageDice = 0, + quickSlotIcon = "", + actionType = 1, + actionCount = 0, + range = 100, + launcherType = 0, + largeDamageDiceCount = 0, + speedFactor = 0, + largeDamageDiceBonus = 0, + thac0Bonus = 0, + damageDice = 0, + school = 0, + damageDiceCount = 0, + secondaryType = 0, + damageDiceBonus = 0, + damageType = 0, + effectCount = 0, + startingEffect = 0, + maxUsageCount = 5, + usageFlags = 1, -- When drained: Item vanishes + abilityFlags = 0, + missileType = 1, + attackProbability = {0, 0, 0, 0, 0, 0}, + effects = {}, + } + + local numericKeys = { + "type", + "quickSlotType", + "largeDamageDice", + "actionType", + "actionCount", + "range", + "launcherType", + "largeDamageDiceCount", + "speedFactor", + "largeDamageDiceBonus", + "thac0Bonus", + "damageDice", + "school", + "damageDiceCount", + "secondaryType", + "damageDiceBonus", + "damageType", + "maxUsageCount", + "usageFlags", + "abilityFlags", + "missileType", + } + + for _, key in ipairs(numericKeys) do + if abilityArgs[key] ~= nil then + abilityData[key] = abilityArgs[key] + end + end + + abilityData.quickSlotIcon = EEex_Resource_Private_NormalizeOptionalResRef(abilityArgs.quickSlotIcon, "quickSlotIcon") + abilityData.attackProbability = EEex_Resource_Private_NormalizeAttackProbability(abilityArgs.attackProbability) + return abilityData +end + +local function EEex_Resource_Private_NormalizeItemEffectArgs(effectArgs, defaultTargetType, defaultDurationType) + -- Effect inserts use the same staged-table pattern as abilities so callers can + -- describe one effect template before it is copied into the rebuilt ITM. + effectArgs = effectArgs or {} + if type(effectArgs) ~= "table" then + EEex_Error("effectArgs must be a table!") + end + if effectArgs.effectID == nil then + EEex_Error("effectID must be defined!") + end + + local effectData = { + effectID = effectArgs.effectID, + targetType = defaultTargetType, + spellLevel = 0, + effectAmount = 0, + dwFlags = 0, + durationType = defaultDurationType, + duration = 0, + probabilityUpper = 100, + probabilityLower = 0, + res = "", + numDice = 0, + diceSize = 0, + savingThrow = 0, + saveMod = 0, + special = 0, + } + + local numericKeys = { + "targetType", + "spellLevel", + "effectAmount", + "dwFlags", + "durationType", + "duration", + "probabilityUpper", + "probabilityLower", + "numDice", + "diceSize", + "savingThrow", + "saveMod", + "special", + } + + for _, key in ipairs(numericKeys) do + if effectArgs[key] ~= nil then + effectData[key] = effectArgs[key] + end + end + + effectData.res = EEex_Resource_Private_NormalizeOptionalResRef(effectArgs.res, "res") + return effectData +end + +local function EEex_Resource_Private_StageItem(itemHeader) + -- Mutators cannot safely edit the demanded ITM in place. Adding or removing + -- abilities / effects changes block sizes, offsets, and startingEffect indices, + -- so the entire resource has to be re-laid out as one fresh byte stream. + -- + -- This staging pass snapshots every mutable piece into plain Lua tables before + -- the rebuild happens. That keeps the read phase separate from the write phase, + -- avoids chasing pointers into memory that will be replaced by dimmServiceFromMemory, + -- and preserves the opaque bytes around the structured blocks verbatim. + local itemRes = EEex_Resource_Private_FindItemResource(itemHeader) + local itemDataBase = EEex_UDToPtr(itemHeader) + local prefixSize = itemHeader.abilityOffset + local oldAbilityBlockSize = itemHeader.abilityCount * Item_ability_st.sizeof + local oldGapStart = itemDataBase + prefixSize + oldAbilityBlockSize + local gapSize = itemHeader.effectsOffset - (prefixSize + oldAbilityBlockSize) + -- Preserve the raw gap between ability and effect blocks byte-for-byte. The + -- engine can leave padding or opaque data there, and rebuilds should not guess. + if gapSize < 0 then + EEex_Error("itemHeader contains overlapping ability and effect blocks!") + end + + local maxEffectEnd = itemHeader.equipedStartingEffect + itemHeader.equipedEffectCount + local equippedEffects = {} + for localEffectIndex = 0, itemHeader.equipedEffectCount - 1 do + local globalEffectIndex = itemHeader.equipedStartingEffect + localEffectIndex + table.insert(equippedEffects, EEex_Resource_Private_CopyItemEffect(EEex_Resource_Private_GetItemEffect(itemHeader, globalEffectIndex))) + end + + local abilities = {} + for abilityIndex = 0, itemHeader.abilityCount - 1 do + local ability = itemHeader:getAbility(abilityIndex) + if not ability then + EEex_Error("itemHeader ability traversal failed at index "..tostring(abilityIndex).."!") + end + + local abilityData = EEex_Resource_Private_CopyItemAbility(ability) + abilityData.effects = {} + local abilityEffectEnd = abilityData.startingEffect + abilityData.effectCount + if abilityEffectEnd > maxEffectEnd then + maxEffectEnd = abilityEffectEnd + end + + for localEffectIndex = 0, abilityData.effectCount - 1 do + local globalEffectIndex = abilityData.startingEffect + localEffectIndex + table.insert(abilityData.effects, EEex_Resource_Private_CopyItemEffect(EEex_Resource_Private_GetItemEffect(itemHeader, globalEffectIndex))) + end + + table.insert(abilities, abilityData) + end + + local oldEffectsEnd = itemHeader.effectsOffset + maxEffectEnd * Item_effect_st.sizeof + if oldEffectsEnd > itemRes.nSize then + EEex_Error("itemHeader effect block exceeds its owning ITM resource size!") + end + + -- The returned table is the mutators' working copy: pure Lua data for every + -- editable structure, plus the untouched byte ranges needed to reassemble the + -- final ITM without guessing about engine-owned padding or trailing payloads. + return { + res = itemRes, + dataBase = itemDataBase, + prefixSize = prefixSize, + oldAbilityBlockSize = oldAbilityBlockSize, + gapSize = gapSize, + oldGapAddress = oldGapStart, + oldSuffixAddress = itemDataBase + oldEffectsEnd, + suffixSize = itemRes.nSize - oldEffectsEnd, + abilities = abilities, + equippedEffects = equippedEffects, + } +end + +local function EEex_Resource_Private_WriteItemAbility(ability, abilityData) + EEex_Memset(EEex_UDToPtr(ability), 0, Item_ability_st.sizeof) + ability.type = abilityData.type + ability.quickSlotType = abilityData.quickSlotType + ability.largeDamageDice = abilityData.largeDamageDice + ability.quickSlotIcon:set(abilityData.quickSlotIcon) + ability.actionType = abilityData.actionType + ability.actionCount = abilityData.actionCount + ability.range = abilityData.range + ability.launcherType = abilityData.launcherType + ability.largeDamageDiceCount = abilityData.largeDamageDiceCount + ability.speedFactor = abilityData.speedFactor + ability.largeDamageDiceBonus = abilityData.largeDamageDiceBonus + ability.thac0Bonus = abilityData.thac0Bonus + ability.damageDice = abilityData.damageDice + ability.school = abilityData.school + ability.damageDiceCount = abilityData.damageDiceCount + ability.secondaryType = abilityData.secondaryType + ability.damageDiceBonus = abilityData.damageDiceBonus + ability.damageType = abilityData.damageType + ability.effectCount = abilityData.effectCount + ability.startingEffect = abilityData.startingEffect + ability.maxUsageCount = abilityData.maxUsageCount + ability.usageFlags = abilityData.usageFlags + ability.abilityFlags = abilityData.abilityFlags + ability.missileType = abilityData.missileType + for i = 0, 5 do + ability.attackProbability:set(i, abilityData.attackProbability[i + 1] or 0) + end +end + +local function EEex_Resource_Private_WriteItemEffect(effect, effectData) + EEex_Memset(EEex_UDToPtr(effect), 0, Item_effect_st.sizeof) + effect.effectID = effectData.effectID + effect.targetType = effectData.targetType + effect.spellLevel = effectData.spellLevel + effect.effectAmount = effectData.effectAmount + effect.dwFlags = effectData.dwFlags + effect.durationType = effectData.durationType + effect.duration = effectData.duration + effect.probabilityUpper = effectData.probabilityUpper + effect.probabilityLower = effectData.probabilityLower + effect.res:set(effectData.res) + effect.numDice = effectData.numDice + effect.diceSize = effectData.diceSize + effect.savingThrow = effectData.savingThrow + effect.saveMod = effectData.saveMod + effect.special = effectData.special +end + +local function EEex_Resource_Private_RebuildItem(stagedItem) + local rebuiltHeader + local newAbilityBlockSize = #stagedItem.abilities * Item_ability_st.sizeof + local newEffectsCount = #stagedItem.equippedEffects + for _, abilityData in ipairs(stagedItem.abilities) do + newEffectsCount = newEffectsCount + #abilityData.effects + end + + local newEffectsBlockSize = newEffectsCount * Item_effect_st.sizeof + local newEffectsOffset = stagedItem.prefixSize + newAbilityBlockSize + stagedItem.gapSize + local newSuffixAddress = newEffectsOffset + newEffectsBlockSize + local newSize = newSuffixAddress + stagedItem.suffixSize + + -- Rebuild the demanded ITM in stack memory, flattening equipped effects and all + -- ability effect lists into one contiguous effect block with fresh indices. + EEex_RunWithStack(newSize, function(bufferBase) + EEex_Memset(bufferBase, 0, newSize) + EEex_Memcpy(bufferBase, stagedItem.dataBase, stagedItem.prefixSize) + + local newHeader = EEex_PtrToUD(bufferBase, "Item_Header_st") + newHeader.abilityOffset = stagedItem.prefixSize + newHeader.abilityCount = #stagedItem.abilities + newHeader.effectsOffset = newEffectsOffset + newHeader.equipedStartingEffect = 0 + newHeader.equipedEffectCount = #stagedItem.equippedEffects + + local nextAbilityBase = bufferBase + stagedItem.prefixSize + local nextEffectIndex = #stagedItem.equippedEffects + for _, abilityData in ipairs(stagedItem.abilities) do + abilityData.effectCount = #abilityData.effects + abilityData.startingEffect = nextEffectIndex + EEex_Resource_Private_WriteItemAbility(EEex_PtrToUD(nextAbilityBase, "Item_ability_st"), abilityData) + nextAbilityBase = nextAbilityBase + Item_ability_st.sizeof + nextEffectIndex = nextEffectIndex + abilityData.effectCount + end + + if stagedItem.gapSize > 0 then + EEex_Memcpy(bufferBase + stagedItem.prefixSize + newAbilityBlockSize, stagedItem.oldGapAddress, stagedItem.gapSize) + end + + local nextEffectBase = bufferBase + newEffectsOffset + for _, effectData in ipairs(stagedItem.equippedEffects) do + EEex_Resource_Private_WriteItemEffect(EEex_PtrToUD(nextEffectBase, "Item_effect_st"), effectData) + nextEffectBase = nextEffectBase + Item_effect_st.sizeof + end + for _, abilityData in ipairs(stagedItem.abilities) do + for _, effectData in ipairs(abilityData.effects) do + EEex_Resource_Private_WriteItemEffect(EEex_PtrToUD(nextEffectBase, "Item_effect_st"), effectData) + nextEffectBase = nextEffectBase + Item_effect_st.sizeof + end + end + + if stagedItem.suffixSize > 0 then + EEex_Memcpy(bufferBase + newSuffixAddress, stagedItem.oldSuffixAddress, stagedItem.suffixSize) + end + + EngineGlobals.dimmServiceFromMemory(stagedItem.res, EEex_PtrToUD(bufferBase, "VariableArray"), newSize, false, true) + local demanded = stagedItem.res:Demand() + if not demanded then + EEex_Error("EEex_Resource_Private_RebuildItem: failed to redemand rebuilt ITM resource!") + end + rebuiltHeader = EEex_CastUD(demanded, "Item_Header_st") + end) + return rebuiltHeader +end + +local function EEex_Resource_Private_GetStagedAbility(stagedItem, abilityIndex, funcName) + abilityIndex = EEex_Resource_Private_ValidateNonNegativeIndex(abilityIndex, "abilityIndex") + local abilityData = stagedItem.abilities[abilityIndex + 1] + if not abilityData then + EEex_Error(funcName..": abilityIndex is out of range!") + end + return abilityData +end + +-- These mutators rebuild the ITM resource in-place. Previously held pointers into the +-- demanded header, ability, or effect blocks should be treated as stale after a mutation. +-- They all start from EEex_Resource_Private_StageItem() so traversal happens against the +-- original demanded resource exactly once, before any rewrite invalidates those pointers. +-- Callers should use the returned Item_Header_st for any follow-up mutation. + +-- @bubb_doc { EEex_Resource_AddItemAbility / instance_name=addAbility } +-- +-- @summary: Appends a new ability to ``itemHeader`` and returns the rebuilt demanded ITM header. +-- +-- @self { itemHeader / usertype=Item_Header_st }: The demanded item header to mutate. +-- +-- @param { abilityArgs / type=table|nil }: Optional ability fields for the inserted ability. +-- +-- @return { type=Item_Header_st }: The rebuilt demanded item header. + +function EEex_Resource_AddItemAbility(itemHeader, abilityArgs) + local stagedItem = EEex_Resource_Private_StageItem(itemHeader) + table.insert(stagedItem.abilities, EEex_Resource_Private_NormalizeItemAbilityArgs(abilityArgs)) + return EEex_Resource_Private_RebuildItem(stagedItem) +end +Item_Header_st.addAbility = EEex_Resource_AddItemAbility + +-- @bubb_doc { EEex_Resource_AddItemEqEffect / instance_name=addEqEffect } +-- +-- @summary: Appends an equipped effect to ``itemHeader`` and returns the rebuilt demanded ITM header. +-- +-- @self { itemHeader / usertype=Item_Header_st }: The demanded item header to mutate. +-- +-- @param { effectArgs / type=table|nil }: Optional effect fields for the inserted equipped effect. +-- +-- @return { type=Item_Header_st }: The rebuilt demanded item header. + +function EEex_Resource_AddItemEqEffect(itemHeader, effectArgs) + local stagedItem = EEex_Resource_Private_StageItem(itemHeader) + table.insert(stagedItem.equippedEffects, EEex_Resource_Private_NormalizeItemEffectArgs( + effectArgs, + 1, + EEex_PackWord(0x2, 0x0) -- Timing mode: Instant/While equipped, Dispel/Resistance: Natural/Nonmagical + )) + return EEex_Resource_Private_RebuildItem(stagedItem) +end +Item_Header_st.addEqEffect = EEex_Resource_AddItemEqEffect + +-- @bubb_doc { EEex_Resource_AddItemEffect / instance_name=addEffect } +-- +-- @summary: Adds an ability effect to one ability or to every ability when ``abilityIndex`` is ``nil``. +-- +-- @self { itemHeader / usertype=Item_Header_st }: The demanded item header to mutate. +-- +-- @param { effectArgs / type=table|nil }: Optional effect fields for the inserted ability effect. +-- +-- @param { abilityIndex / type=number|nil / default=nil }: +-- The zero-based ability index to target. If ``nil``, the effect is copied into every ability. @EOL +-- +-- @return { type=Item_Header_st }: The rebuilt demanded item header. + +function EEex_Resource_AddItemEffect(itemHeader, effectArgs, abilityIndex) + abilityIndex = EEex_Resource_Private_ValidateAbilityIndexForInsert(abilityIndex) + local stagedItem = EEex_Resource_Private_StageItem(itemHeader) + local effectData = EEex_Resource_Private_NormalizeItemEffectArgs(effectArgs, 2, 0) + + if abilityIndex < 0 then + -- Insert into every ability. Copy the staged effect table for each target so + -- later edits or rebuild bookkeeping never share the same table instance. + if #stagedItem.abilities == 0 then + EEex_Error("EEex_Resource_AddItemEffect: itemHeader has no abilities!") + end + for _, abilityData in ipairs(stagedItem.abilities) do + table.insert(abilityData.effects, EEex_Resource_Private_CopyItemEffectData(effectData)) + end + else + table.insert(EEex_Resource_Private_GetStagedAbility(stagedItem, abilityIndex, "EEex_Resource_AddItemEffect").effects, + EEex_Resource_Private_CopyItemEffectData(effectData)) + end + + return EEex_Resource_Private_RebuildItem(stagedItem) +end +Item_Header_st.addEffect = EEex_Resource_AddItemEffect + +-- @bubb_doc { EEex_Resource_RemoveItemAbility / instance_name=removeAbility } +-- +-- @summary: Removes one ability or all abilities when ``abilityIndex`` is ``nil``, then returns the rebuilt demanded ITM header. +-- +-- @self { itemHeader / usertype=Item_Header_st }: The demanded item header to mutate. +-- +-- @param { abilityIndex / type=number|nil / default=nil }: +-- The zero-based ability index to remove. If ``nil``, removes every ability. @EOL +-- +-- @return { type=Item_Header_st }: The rebuilt demanded item header. + +function EEex_Resource_RemoveItemAbility(itemHeader, abilityIndex) + local stagedItem = EEex_Resource_Private_StageItem(itemHeader) + abilityIndex = EEex_Resource_Private_ValidateWildcardIndex(abilityIndex, "abilityIndex") + if abilityIndex < 0 then + stagedItem.abilities = {} + elseif stagedItem.abilities[abilityIndex + 1] == nil then + EEex_Error("EEex_Resource_RemoveItemAbility: abilityIndex is out of range!") + else + table.remove(stagedItem.abilities, abilityIndex + 1) + end + return EEex_Resource_Private_RebuildItem(stagedItem) +end +Item_Header_st.removeAbility = EEex_Resource_RemoveItemAbility + +-- @bubb_doc { EEex_Resource_RemoveItemEqEffect / instance_name=removeEqEffect } +-- +-- @summary: Removes one equipped effect or all equipped effects when ``effectIndex`` is ``nil``, then returns the rebuilt demanded ITM header. +-- +-- @self { itemHeader / usertype=Item_Header_st }: The demanded item header to mutate. +-- +-- @param { effectIndex / type=number|nil / default=nil }: +-- The zero-based equipped-effect index to remove. If ``nil``, removes every equipped effect. @EOL +-- +-- @return { type=Item_Header_st }: The rebuilt demanded item header. + +function EEex_Resource_RemoveItemEqEffect(itemHeader, effectIndex) + local stagedItem = EEex_Resource_Private_StageItem(itemHeader) + effectIndex = EEex_Resource_Private_ValidateWildcardIndex(effectIndex, "effectIndex") + if effectIndex < 0 then + stagedItem.equippedEffects = {} + elseif stagedItem.equippedEffects[effectIndex + 1] == nil then + EEex_Error("EEex_Resource_RemoveItemEqEffect: effectIndex is out of range!") + else + table.remove(stagedItem.equippedEffects, effectIndex + 1) + end + return EEex_Resource_Private_RebuildItem(stagedItem) +end +Item_Header_st.removeEqEffect = EEex_Resource_RemoveItemEqEffect + +-- @bubb_doc { EEex_Resource_RemoveItemEffect / instance_name=removeEffect } +-- +-- @summary: Removes one ability effect, or a wildcard-selected set of ability effects, then returns the rebuilt demanded ITM header. +-- +-- @self { itemHeader / usertype=Item_Header_st }: The demanded item header to mutate. +-- +-- @param { effectIndex / type=number|nil / default=nil }: +-- The zero-based ability-effect index to remove. If ``nil``, clears the matched ability effect lists. @EOL +-- +-- @param { abilityIndex / type=number|nil / default=nil }: +-- The zero-based ability index to target. If ``nil``, applies the removal across every ability. @EOL +-- +-- @return { type=Item_Header_st }: The rebuilt demanded item header. + +function EEex_Resource_RemoveItemEffect(itemHeader, effectIndex, abilityIndex) + local stagedItem = EEex_Resource_Private_StageItem(itemHeader) + effectIndex = EEex_Resource_Private_ValidateWildcardIndex(effectIndex, "effectIndex") + abilityIndex = EEex_Resource_Private_ValidateWildcardIndex(abilityIndex, "abilityIndex") + + -- Negative indices mean "all matched entries": abilityIndex < 0 fans out across + -- every ability, and effectIndex < 0 clears the whole targeted effect list. + if abilityIndex < 0 then + if effectIndex < 0 then + for _, abilityData in ipairs(stagedItem.abilities) do + abilityData.effects = {} + end + else + local matchedAny = false + for _, abilityData in ipairs(stagedItem.abilities) do + if abilityData.effects[effectIndex + 1] ~= nil then + table.remove(abilityData.effects, effectIndex + 1) + matchedAny = true + end + end + if not matchedAny then + EEex_Error("EEex_Resource_RemoveItemEffect: effectIndex is out of range for all matched abilities!") + end + end + else + local abilityData = EEex_Resource_Private_GetStagedAbility(stagedItem, abilityIndex, "EEex_Resource_RemoveItemEffect") + if effectIndex < 0 then + abilityData.effects = {} + elseif abilityData.effects[effectIndex + 1] == nil then + EEex_Error("EEex_Resource_RemoveItemEffect: effectIndex is out of range!") + else + table.remove(abilityData.effects, effectIndex + 1) + end + end + return EEex_Resource_Private_RebuildItem(stagedItem) +end +Item_Header_st.removeEffect = EEex_Resource_RemoveItemEffect + function EEex_Resource_GetSpellAbilityForLevel(spellHeader, casterLevel) local abilitiesCount = spellHeader.abilityCount diff --git a/EEex/copy/EEex_scripts/EEex_Test.lua b/EEex/copy/EEex_scripts/EEex_Test.lua index c1c9df9..69e957f 100644 --- a/EEex/copy/EEex_scripts/EEex_Test.lua +++ b/EEex/copy/EEex_scripts/EEex_Test.lua @@ -70,3 +70,80 @@ function EEex_Test_2DAFunctions() print(string.format(" [%d] = %s", i, str)) end) end + +function EEex_Test_CreateRuntimeItem() + + local sprite = EEex_Sprite_GetSelected() -- CGameSprite + if not sprite then + return + end + + -- New item from scratch + do + local pHeader = EEex_Item_CreateFromResref("B3NEWITM", { + ["genericName"] = 1080, + ["identifiedName"] = 1080, + ["itemFlags"] = 0x2C, -- droppable, displayable, not copyable + ["itemType"] = 10, -- rings + ["minINTRequired"] = 9, + ["baseValue"] = 900, + ["genericDescription"] = 17054, + ["identifiedDescription"] = 17054, + ["itemIcon"] = "IRING01", + ["groundIcon"] = "GRING01", + }, true) -- set last boolean parameter to ``false`` if you do not want the item to survive load/save + -- + pHeader = EEex_Resource_AddItemAbility(pHeader, { + ["quickSlotIcon"] = "IRING01", + }) + -- + pHeader = EEex_Resource_AddItemEqEffect(pHeader, { + ["effectID"] = 0x8E, -- type: display portrait icon + ["dwFlags"] = 94, -- icon: magnetized + }) + -- + pHeader = EEex_Resource_AddItemEffect(pHeader, { + ["effectID"] = 0x92, -- type: Cast Spell (at Creature) + ["dwFlags"] = 1, -- mode: Cast instantly (caster level) + ["res"] = "SPWI112", + }) + -- + sprite:applyEffect({ + ["effectID"] = 0x7A, -- Create inventory item + ["durationType"] = 1, + ["effectAmount"] = Infinity_RandomNumber(1, 5), + ["res"] = "B3NEWITM", + ["sourceID"] = sprite.m_id, + ["sourceTarget"] = sprite.m_id, + }) + end + + -- Clone existing item + do + local items = sprite.m_equipment.m_items -- Array + for i = 0, 38 do + local item = items:get(i) -- CItem + if item then -- sanity check + local resref = item.pRes.resref:get() + if resref:upper() == "STAF01" then + local pHeader = EEex_Item_CreateCopy("B3STAF01", item, { + ["attributes"] = 9, -- enchantment: +9 + }, true) -- set last boolean parameter to ``false`` if you do not want the item to survive load/save + -- + pHeader.itemFlags = EEex_BOr(pHeader.itemFlags, 0x6) -- add magical flag + pHeader.baseValue = pHeader.baseValue + 1000 -- increase base value by 1000 + -- + sprite:applyEffect({ + ["effectID"] = 0x7A, -- Create inventory item + ["durationType"] = 1, + ["res"] = "B3STAF01", + ["sourceID"] = sprite.m_id, + ["sourceTarget"] = sprite.m_id, + }) + -- + break + end + end + end + end +end diff --git a/EEex/loader/EEex.dll b/EEex/loader/EEex.dll index 6229ff9..bd2bbd0 100644 Binary files a/EEex/loader/EEex.dll and b/EEex/loader/EEex.dll differ diff --git a/EEex/loader/InfinityLoader.db b/EEex/loader/InfinityLoader.db index 1e444e9..5128819 100644 --- a/EEex/loader/InfinityLoader.db +++ b/EEex/loader/InfinityLoader.db @@ -2159,6 +2159,10 @@ Operations=ADD -68 Pattern=4533C948899C2440010000 Operations=ADD 64 +[Hook-CGameEffectCreateItem::ApplyEffect()-CGameAIBase::PlaceItem()] +Pattern=488BDE41B901000000 +Operations=ADD 30 + [Hook-CGameEffectDamage::ApplyEffect()-OnDone] Pattern=448964242041FF92E8000000 Operations=ADD 23 diff --git a/EEex/loader/InfinityLoader.exe b/EEex/loader/InfinityLoader.exe index 120fbb1..96ae572 100644 Binary files a/EEex/loader/InfinityLoader.exe and b/EEex/loader/InfinityLoader.exe differ diff --git a/EEex/loader/InfinityLoaderCommon.dll b/EEex/loader/InfinityLoaderCommon.dll index 618af44..773a5b8 100644 Binary files a/EEex/loader/InfinityLoaderCommon.dll and b/EEex/loader/InfinityLoaderCommon.dll differ diff --git a/EEex/loader/InfinityLoaderDLL.dll b/EEex/loader/InfinityLoaderDLL.dll index a3312cb..9cc1130 100644 Binary files a/EEex/loader/InfinityLoaderDLL.dll and b/EEex/loader/InfinityLoaderDLL.dll differ diff --git a/EEex/loader/InfinityLoaderUtil.dll b/EEex/loader/InfinityLoaderUtil.dll index 3d58284..12daea8 100644 Binary files a/EEex/loader/InfinityLoaderUtil.dll and b/EEex/loader/InfinityLoaderUtil.dll differ diff --git a/EEex/loader/Lua52/LuaProvider.dll b/EEex/loader/Lua52/LuaProvider.dll index 8741dbc..9d75838 100644 Binary files a/EEex/loader/Lua52/LuaProvider.dll and b/EEex/loader/Lua52/LuaProvider.dll differ diff --git a/EEex/loader/LuaBindings-v2.6.6.0.dll b/EEex/loader/LuaBindings-v2.6.6.0.dll index cb4cd06..8f58dca 100644 Binary files a/EEex/loader/LuaBindings-v2.6.6.0.dll and b/EEex/loader/LuaBindings-v2.6.6.0.dll differ diff --git a/EEex/loader/LuaBindingsCore.dll b/EEex/loader/LuaBindingsCore.dll index 8ab6669..576aa7a 100644 Binary files a/EEex/loader/LuaBindingsCore.dll and b/EEex/loader/LuaBindingsCore.dll differ diff --git a/EEex/loader/LuaJIT/LuaProvider.dll b/EEex/loader/LuaJIT/LuaProvider.dll index ebf6e19..3377946 100644 Binary files a/EEex/loader/LuaJIT/LuaProvider.dll and b/EEex/loader/LuaJIT/LuaProvider.dll differ