diff --git a/src/engine/best-action.ts b/src/engine/best-action.ts index 0cdcef17b..b027dcbd7 100644 --- a/src/engine/best-action.ts +++ b/src/engine/best-action.ts @@ -1,9 +1,8 @@ import { OvaleActionBarClass } from "./action-bar"; import { OvaleDataClass } from "./data"; -import { OvaleEquipmentClass } from "../states/Equipment"; +import { OvaleEquipmentClass, SlotName } from "../states/Equipment"; import aceEvent, { AceEvent } from "@wowts/ace_event-3.0"; import { pairs, tonumber, tostring } from "@wowts/lua"; -import { upper } from "@wowts/string"; import { GetActionCooldown, GetActionTexture, @@ -11,7 +10,6 @@ import { GetItemCooldown, GetItemSpell, GetSpellTexture, - InventorySlotName, IsActionInRange, IsItemInRange, IsUsableAction, @@ -88,7 +86,7 @@ export class OvaleBestActionClass { const result = node.result; setResultType(result, "action"); if (!isNumber(itemId)) { - const slot = upper(tostring(itemId)); + const slot = tostring(itemId) as SlotName; const itemIdFromSlot = this.ovaleEquipment.getEquippedItemId(slot); if (!itemIdFromSlot) { this.tracer.log("Unknown item '%s'.", itemId); diff --git a/src/states/AzeriteArmor.ts b/src/states/AzeriteArmor.ts index 4dea54c0a..20cf0cb2c 100644 --- a/src/states/AzeriteArmor.ts +++ b/src/states/AzeriteArmor.ts @@ -11,13 +11,12 @@ import { } from "@wowts/lua"; import { sort, insert, concat } from "@wowts/table"; import { - InventorySlotName, C_AzeriteEmpoweredItem, GetSpellInfo, AzeriteEmpoweredItemSelectionUpdatedEvent, PlayerEnteringWorldEvent, } from "@wowts/wow-mock"; -import { InventorySlotNameMap, OvaleEquipmentClass } from "./Equipment"; +import { OvaleEquipmentClass, SlotName } from "./Equipment"; import { AceModule } from "@wowts/tsaddon"; import { OvaleClass } from "../Ovale"; import { DebugTools } from "../engine/debug"; @@ -30,10 +29,11 @@ import { } from "../engine/condition"; import { AstFunctionNode, NamedParametersOf } from "../engine/ast"; -const azeriteSlots: InventorySlotNameMap = { - HEADSLOT: true, - SHOULDERSLOT: true, - CHESTSLOT: true, +type SlotNameMap = { [key in SlotName]?: boolean }; +const azeriteSlots: SlotNameMap = { + headslot: true, + shoulderslot: true, + chestslot: true, }; interface Trait { @@ -116,13 +116,8 @@ export class OvaleAzeriteArmor { this.module.UnregisterEvent("PLAYER_ENTERING_WORLD"); }; - private handleOvaleEquipmentChanged = ( - event: string, - slot?: InventorySlotName - ) => { - if (slot == undefined) { - this.updateTraits(); - } else if (azeriteSlots[slot]) { + private handleOvaleEquipmentChanged = (event: string, slot: SlotName) => { + if (azeriteSlots[slot]) { this.updateTraits(); } }; diff --git a/src/states/Equipment.ts b/src/states/Equipment.ts index c2089d1b4..083391d9e 100644 --- a/src/states/Equipment.ts +++ b/src/states/Equipment.ts @@ -1,5 +1,4 @@ import aceEvent, { AceEvent } from "@wowts/ace_event-3.0"; -import aceTimer, { AceTimer } from "@wowts/ace_timer-3.0"; import { LuaArray, LuaObj, @@ -143,16 +142,21 @@ const slotNameByName: LuaObj = { wristslot: "WRISTSLOT", }; +// Map InventorySlotName to Ovale slot names. +type OvaleSlotNameMap = { [key in InventorySlotName]?: SlotName }; +const ovaleSlotNameByName: OvaleSlotNameMap = {}; + interface ItemInfo { exists: boolean; guid: string; + pending?: number; + id?: number; link?: string; location?: ItemLocationMixin; name?: string; quality?: number; // Enum.ItemQuality type?: number; // Enum.InventoryType // The properties below are populated by parseItemLink(). - id?: number; gem: LuaArray; bonus: LuaArray; modifier: LuaArray; @@ -161,6 +165,7 @@ interface ItemInfo { function resetItemInfo(item: ItemInfo) { item.exists = false; item.guid = ""; + delete item.pending; delete item.link; delete item.location; delete item.name; @@ -198,7 +203,7 @@ export class OvaleEquipmentClass { }, }, }; - private module: AceModule & AceEvent & AceTimer; + private module: AceModule & AceEvent; private tracer: Tracer; private profiler: Profiler; @@ -212,8 +217,7 @@ export class OvaleEquipmentClass { "OvaleEquipment", this.handleInitialize, this.handleDisable, - aceEvent, - aceTimer + aceEvent ); this.tracer = ovaleDebug.create("OvaleEquipment"); this.profiler = ovaleProfiler.create(this.module.GetName()); @@ -223,6 +227,7 @@ export class OvaleEquipmentClass { } for (const [slot] of kpairs(inventorySlotNames)) { + ovaleSlotNameByName[slot] = lower(slot) as SlotName; const [slotId] = GetInventorySlotInfo(slot); slotIdByName[slot] = slotId; slotNameById[slotId] = slot; @@ -236,81 +241,14 @@ export class OvaleEquipmentClass { } } - registerConditions(ovaleCondition: OvaleConditionClass) { - ovaleCondition.registerCondition( - "hasequippeditem", - false, - this.hasItemEquipped - ); - ovaleCondition.registerCondition("hasshield", false, this.hasShield); - ovaleCondition.registerCondition("hastrinket", false, this.hasTrinket); - const slotParameter: ParameterInfo = { - type: "string", - name: "slot", - checkTokens: checkSlotName, - optional: true, - }; - const itemParameter: ParameterInfo = { - name: "item", - type: "number", - optional: true, - isItem: true, - }; - ovaleCondition.register( - "itemcooldown", - this.itemCooldown, - { type: "number" }, - itemParameter, - slotParameter, - { name: "shared", type: "string", optional: true } - ); - ovaleCondition.register( - "itemrppm", - this.itemRppm, - { type: "number" }, - { type: "number", name: "item", optional: true }, - { - type: "string", - name: "slot", - checkTokens: checkSlotName, - optional: true, - } - ); - ovaleCondition.register( - "itemcooldownduration", - this.itemCooldownDuration, - { type: "number" }, - itemParameter, - slotParameter - ); - ovaleCondition.registerCondition( - "weaponenchantexpires", - false, - this.weaponEnchantExpires - ); - ovaleCondition.registerCondition( - "weaponenchantpresent", - false, - this.weaponEnchantPresent - ); - ovaleCondition.register( - "iteminslot", - this.itemInSlot, - { type: "number" }, - { - type: "string", - optional: false, - name: "slot", - checkTokens: checkSlotName, - } - ); - } - private handleInitialize = () => { - this.module.RegisterEvent("PLAYER_LOGIN", this.handlePlayerLogin); + this.module.RegisterEvent( + "ITEM_DATA_LOAD_RESULT", + this.handleItemDataLoadResult + ); this.module.RegisterEvent( "PLAYER_ENTERING_WORLD", - this.updateEquippedItems + this.handlePlayerEnteringWorld ); this.module.RegisterEvent( "PLAYER_EQUIPMENT_CHANGED", @@ -318,17 +256,38 @@ export class OvaleEquipmentClass { ); }; private handleDisable = () => { - this.module.UnregisterEvent("PLAYER_LOGIN"); + this.module.UnregisterEvent("ITEM_DATA_LOAD_RESULT"); this.module.UnregisterEvent("PLAYER_ENTERING_WORLD"); this.module.UnregisterEvent("PLAYER_EQUIPMENT_CHANGED"); }; - private handlePlayerLogin = (event: string) => { - /* Update all equipped items at 3 seconds after player login. - This is to workaround a delay in the item information being - available to the game client. - */ - this.module.ScheduleTimer(this.updateEquippedItems, 3); + private handleItemDataLoadResult = ( + event: string, + itemId: number, + success: boolean + ) => { + if (success && this.isEquippedItemById[itemId]) { + for (const [slot, item] of pairs(this.equippedItem)) { + if (item.pending) { + const slotId = slotIdByName[slot]; + const location = + ItemLocation.CreateFromEquipmentSlot(slotId); + if (location.IsValid() && item.pending == itemId) { + this.finishUpdateForSlot( + slot as InventorySlotName, + itemId, + location + ); + } + } + } + } + }; + + private handlePlayerEnteringWorld = (event: string) => { + for (const [slot] of kpairs(inventorySlotNames)) { + this.queueUpdateForSlot(slot); + } }; private handlePlayerEquipmentChanged = ( @@ -336,15 +295,8 @@ export class OvaleEquipmentClass { slotId: number, hasCurrent: boolean ) => { - this.profiler.startProfiling("OvaleEquipment_PLAYER_EQUIPMENT_CHANGED"); const slot = slotNameById[slotId]; - const changed = this.updateEquippedItem(slot); - if (changed) { - //this.UpdateArmorSetCount(); - this.ovale.needRefresh(); - this.module.SendMessage("Ovale_EquipmentChanged", slot); - } - this.profiler.stopProfiling("OvaleEquipment_PLAYER_EQUIPMENT_CHANGED"); + this.queueUpdateForSlot(slot); }; // Armor sets are retiring after Legion; for now, return 0 @@ -362,8 +314,9 @@ export class OvaleEquipmentClass { return 0; } - getEquippedItemId(slot: InventorySlotName): number | undefined { - const item = this.equippedItem[slot]; + getEquippedItemId(slot: SlotName): number | undefined { + const invSlot = slotNameByName[slot]; + const item = this.equippedItem[invSlot]; return (item.exists && item.id) || undefined; } @@ -373,21 +326,27 @@ export class OvaleEquipmentClass { return this.equippedItemBySharedCooldown[sharedCooldown]; } - getEquippedItemLocation( - slot: InventorySlotName - ): ItemLocationMixin | undefined { - const item = this.equippedItem[slot]; - return (item.exists && item.location) || undefined; + getEquippedItemLocation(slot: SlotName): ItemLocationMixin | undefined { + const invSlot = slotNameByName[slot]; + const item = this.equippedItem[invSlot]; + if (item.exists) { + if (item.location && item.location.IsValid()) { + return item.location; + } + } + return undefined; } - getEquippedItemQuality(slot: InventorySlotName): number | undefined { - const item = this.equippedItem[slot]; + getEquippedItemQuality(slot: SlotName): number | undefined { + const invSlot = slotNameByName[slot]; + const item = this.equippedItem[invSlot]; return (item.exists && item.quality) || undefined; } - getEquippedItemBonusIds(slot: InventorySlotName): LuaArray { + getEquippedItemBonusIds(slot: SlotName): LuaArray { // Returns the array of bonus IDs for the slot. - const item = this.equippedItem[slot]; + const invSlot = slotNameByName[slot]; + const item = this.equippedItem[invSlot]; return item.bonus; } @@ -417,14 +376,14 @@ export class OvaleEquipmentClass { item.id = token; } else if (3 <= index && index <= 5) { if (token != 0) { - let gem = item.gem || {}; + const gem = item.gem || {}; insert(gem, token); item.gem = gem; } } else if (index == 13) { numBonus = token; } else if (index > 13 && index <= 13 + numBonus) { - let bonus = item.bonus || {}; + const bonus = item.bonus || {}; insert(bonus, token); item.bonus = bonus; } else if (index == 13 + numBonus + 1) { @@ -433,7 +392,7 @@ export class OvaleEquipmentClass { index > 13 + numBonus + 1 && index <= 13 + numBonus + 1 + numModifiers ) { - let modifier = item.modifier || {}; + const modifier = item.modifier || {}; insert(modifier, token); item.modifier = modifier; } @@ -447,37 +406,57 @@ export class OvaleEquipmentClass { // Ignore the last token since we don't need it for Ovale. }; - private updateEquippedItem = (slot: InventorySlotName): boolean => { - this.tracer.debug(`Updating slot ${slot}`); - let item = this.equippedItem[slot]; - const prevGUID = item.guid; - const prevItemId = item.id; - if (prevItemId != undefined) { - delete this.isEquippedItemById[prevItemId]; - } - resetItemInfo(item); + private queueUpdateForSlot = (slot: InventorySlotName) => { const slotId = slotIdByName[slot]; const location = ItemLocation.CreateFromEquipmentSlot(slotId); - const exists = C_Item.DoesItemExist(location); - if (exists) { + const item = this.equippedItem[slot]; + if (location.IsValid()) { + const itemId = C_Item.GetItemID(location); + this.isEquippedItemById[itemId] = true; + const link = C_Item.GetItemLink(location); + if (link) { + // Item link is available, so data is already loaded. + this.finishUpdateForSlot(slot, itemId, location); + } else { + // Save pending itemID to be checked in event handler. + item.pending = itemId; + C_Item.RequestLoadItemData(location); + this.tracer.debug(`Slot ${slot}, item ${itemId}: queued`); + } + } else { + this.tracer.debug(`Slot ${slot}: empty`); + resetItemInfo(item); + } + }; + + private finishUpdateForSlot = ( + slot: InventorySlotName, + itemId: number, + location: ItemLocationMixin + ) => { + this.profiler.startProfiling( + "OvaleEquipment_finishUpdateForEquippedItem" + ); + this.tracer.debug(`Slot ${slot}, item ${itemId}: finished`); + const item = this.equippedItem[slot]; + if (location.IsValid()) { + const prevGUID = item.guid; + const prevItemId = item.id; + if (prevItemId != undefined) { + delete this.isEquippedItemById[prevItemId]; + } + resetItemInfo(item); item.exists = true; item.guid = C_Item.GetItemGUID(location); + item.id = itemId; item.location = location; item.name = C_Item.GetItemName(location); item.quality = C_Item.GetItemQuality(location); item.type = C_Item.GetItemInventoryType(location); const link = C_Item.GetItemLink(location); - if (link != undefined) { + if (link) { item.link = link; this.parseItemLink(link, item); - const id = item.id; - if (id != undefined) { - this.isEquippedItemById[id] = true; - const info = this.data.itemInfo[id]; - if (info != undefined && info.shared_cd != undefined) { - this.equippedItemBySharedCooldown[info.shared_cd] = id; - } - } if (slot == "MAINHANDSLOT" || slot == "SECONDARYHANDSLOT") { const stats = GetItemStats(link); if (stats != undefined) { @@ -491,22 +470,24 @@ export class OvaleEquipmentClass { } } } - } - return prevGUID != item.guid; - }; + this.isEquippedItemById[itemId] = true; + const info = this.data.itemInfo[itemId]; + if (info != undefined && info.shared_cd != undefined) { + this.equippedItemBySharedCooldown[info.shared_cd] = itemId; + } - private updateEquippedItems = () => { - this.profiler.startProfiling("OvaleEquipment_UpdateEquippedItems"); - let anyChanged = false; - for (const [slot] of kpairs(inventorySlotNames)) { - const changed = this.updateEquippedItem(slot); - anyChanged = anyChanged || changed; - } - if (anyChanged) { - this.ovale.needRefresh(); - this.module.SendMessage("Ovale_EquipmentChanged"); + if (prevGUID != item.guid) { + //this.UpdateArmorSetCount(); + this.ovale.needRefresh(); + const slotName = ovaleSlotNameByName[slot]; + this.module.SendMessage("Ovale_EquipmentChanged", slotName); + } + } else { + resetItemInfo(item); } - this.profiler.stopProfiling("OvaleEquipment_UpdateEquippedItems"); + this.profiler.stopProfiling( + "OvaleEquipment_finishUpdateForEquippedItem" + ); }; private debugEquipment = () => { @@ -562,7 +543,75 @@ export class OvaleEquipmentClass { return concat(output, "\n"); }; - // CONDITIONS: + registerConditions(ovaleCondition: OvaleConditionClass) { + ovaleCondition.registerCondition( + "hasequippeditem", + false, + this.hasItemEquipped + ); + ovaleCondition.registerCondition("hasshield", false, this.hasShield); + ovaleCondition.registerCondition("hastrinket", false, this.hasTrinket); + const slotParameter: ParameterInfo = { + type: "string", + name: "slot", + checkTokens: checkSlotName, + optional: true, + }; + const itemParameter: ParameterInfo = { + name: "item", + type: "number", + optional: true, + isItem: true, + }; + ovaleCondition.register( + "itemcooldown", + this.itemCooldown, + { type: "number" }, + itemParameter, + slotParameter, + { name: "shared", type: "string", optional: true } + ); + ovaleCondition.register( + "itemrppm", + this.itemRppm, + { type: "number" }, + { type: "number", name: "item", optional: true }, + { + type: "string", + name: "slot", + checkTokens: checkSlotName, + optional: true, + } + ); + ovaleCondition.register( + "itemcooldownduration", + this.itemCooldownDuration, + { type: "number" }, + itemParameter, + slotParameter + ); + ovaleCondition.registerCondition( + "weaponenchantexpires", + false, + this.weaponEnchantExpires + ); + ovaleCondition.registerCondition( + "weaponenchantpresent", + false, + this.weaponEnchantPresent + ); + ovaleCondition.register( + "iteminslot", + this.itemInSlot, + { type: "number" }, + { + type: "string", + optional: false, + name: "slot", + checkTokens: checkSlotName, + } + ); + } /** Test if the player has a particular item equipped. @name HasEquippedItem @@ -662,8 +711,7 @@ export class OvaleEquipmentClass { itemId = this.getEquippedItemIdBySharedCooldown(sharedCooldown); } if (slot != undefined) { - const invSlot = slotNameByName[slot]; - itemId = this.getEquippedItemId(invSlot); + itemId = this.getEquippedItemId(slot); } if (itemId) { const [start, duration] = GetItemCooldown(itemId); @@ -681,8 +729,7 @@ export class OvaleEquipmentClass { slot: SlotName | undefined ) => { if (slot !== undefined) { - const invSlot = slotNameByName[slot]; - itemId = this.getEquippedItemId(invSlot); + itemId = this.getEquippedItemId(slot); } if (!itemId) return returnConstant(0); @@ -699,8 +746,7 @@ export class OvaleEquipmentClass { }; private itemInSlot = (atTime: number, slot: SlotName) => { - const invSlot = slotNameByName[slot]; - const itemId = this.getEquippedItemId(invSlot); + const itemId = this.getEquippedItemId(slot); return returnConstant(itemId); }; @@ -766,8 +812,7 @@ export class OvaleEquipmentClass { slot: SlotName | undefined ): ConditionResult => { if (slot) { - const invSlot = slotNameByName[slot]; - itemId = this.getEquippedItemId(invSlot); + itemId = this.getEquippedItemId(slot); } if (itemId) { const rppm = this.data.getItemInfoProperty(itemId, atTime, "rppm"); diff --git a/src/states/runeforge.ts b/src/states/runeforge.ts index 397d90f7e..b0e0529a1 100644 --- a/src/states/runeforge.ts +++ b/src/states/runeforge.ts @@ -1,13 +1,5 @@ import aceEvent, { AceEvent } from "@wowts/ace_event-3.0"; -import { - LuaArray, - ipairs, - kpairs, - lualength, - tonumber, - unpack, - wipe, -} from "@wowts/lua"; +import { LuaArray, kpairs, lualength, pairs, tonumber } from "@wowts/lua"; import { concat, insert } from "@wowts/table"; import { AceModule } from "@wowts/tsaddon"; import { C_LegendaryCrafting, Enum } from "@wowts/wow-mock"; @@ -18,14 +10,13 @@ import { returnBoolean, } from "../engine/condition"; import { DebugTools } from "../engine/debug"; -import { isNumber, oneTimeMessage } from "../tools/tools"; import { OptionUiGroup } from "../ui/acegui-helpers"; -import { OvaleEquipmentClass, inventorySlotNames } from "./Equipment"; +import { OvaleEquipmentClass, SlotName } from "./Equipment"; export class Runeforge { private module: AceModule & AceEvent; - private equippedLegendaryById: LuaArray = {}; - private equippedRuneforgeById: LuaArray = {}; + private equippedLegendaryById: LuaArray = {}; + private equippedRuneforgeById: LuaArray = {}; private debugRuneforges: OptionUiGroup = { type: "group", @@ -40,7 +31,7 @@ export class Runeforge { const ids = C_LegendaryCrafting.GetRuneforgePowers(undefined); const output: LuaArray = {}; - for (const [, id] of ipairs(ids)) { + for (const [, id] of pairs(ids)) { const runeforgePower = C_LegendaryCrafting.GetRuneforgePowerInfo(id); if (runeforgePower != undefined) { @@ -106,31 +97,37 @@ export class Runeforge { this.module.UnregisterMessage("Ovale_EquipmentChanged"); }; - private handleOvaleEquipmentChanged = (event: string) => { - wipe(this.equippedLegendaryById); - wipe(this.equippedRuneforgeById); - for (const [slot] of kpairs(inventorySlotNames)) { - // Update bonus IDs list in equippedLegendaryById. - const quality = this.equipment.getEquippedItemQuality(slot); - if (quality == Enum.ItemQuality.Legendary) { - // XXX Assume the first bonus ID is the legendary bonus ID. - const bonusIds = this.equipment.getEquippedItemBonusIds(slot); - if (lualength(bonusIds) > 0) { - const id = bonusIds[1]; - this.equippedLegendaryById[id] = true; - } + private handleOvaleEquipmentChanged = (event: string, slot: SlotName) => { + // Update bonus IDs list in equippedLegendaryById. + for (const [id, slotName] of pairs(this.equippedLegendaryById)) { + if (slotName == slot) { + delete this.equippedLegendaryById[id]; } - // Update power IDs list in equippedRuneforgeById. - const location = this.equipment.getEquippedItemLocation(slot); - if (location != undefined) { - if (C_LegendaryCrafting.IsRuneforgeLegendary(location)) { - const componentInfo = - C_LegendaryCrafting.GetRuneforgeLegendaryComponentInfo( - location - ); - const id = tonumber(componentInfo.powerID); - this.equippedRuneforgeById[id] = true; - } + } + const quality = this.equipment.getEquippedItemQuality(slot); + if (quality == Enum.ItemQuality.Legendary) { + // XXX Assume the first bonus ID is the legendary bonus ID. + const bonusIds = this.equipment.getEquippedItemBonusIds(slot); + if (lualength(bonusIds) > 0) { + const id = bonusIds[1]; + this.equippedLegendaryById[id] = slot; + } + } + // Update power IDs list in equippedRuneforgeById. + for (const [id, slotName] of pairs(this.equippedRuneforgeById)) { + if (slotName == slot) { + delete this.equippedRuneforgeById[id]; + } + } + const location = this.equipment.getEquippedItemLocation(slot); + if (location != undefined) { + if (C_LegendaryCrafting.IsRuneforgeLegendary(location)) { + const componentInfo = + C_LegendaryCrafting.GetRuneforgeLegendaryComponentInfo( + location + ); + const id = tonumber(componentInfo.powerID); + this.equippedRuneforgeById[id] = slot; } } }; @@ -145,16 +142,13 @@ export class Runeforge { } private equippedRuneforge: ConditionFunction = (positionalParameters) => { - const [id] = unpack(positionalParameters); - if (!isNumber(id)) { - oneTimeMessage(`${id} is not defined in EquippedRuneforge`); - return []; - } + const id = positionalParameters[1] as number; /* Check both lists and return true if the ID is in either of them. * Technically could be incorrect, but chance of collision is very low. */ - return returnBoolean( - this.equippedLegendaryById[id] || this.equippedRuneforgeById[id] - ); + if (this.equippedLegendaryById[id] || this.equippedRuneforgeById[id]) { + return returnBoolean(true); + } + return returnBoolean(false); }; }