mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
Compare commits
53
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b0aa46ff0 | ||
|
|
84171bef2d | ||
|
|
f19f13d44c | ||
|
|
4509da0966 | ||
|
|
38d5c6c7b6 | ||
|
|
be5ebd4017 | ||
|
|
44f9bcd9af | ||
|
|
f9ec92014c | ||
|
|
264af04ee3 | ||
|
|
2063507fb4 | ||
|
|
090cfc3bfd | ||
|
|
d1cdb47c3e | ||
|
|
237e5a7500 | ||
|
|
d339f4486d | ||
|
|
92025896b5 | ||
|
|
4eb214f7f1 | ||
|
|
706fb1c540 | ||
|
|
e37052c67d | ||
|
|
f8f6dc28b8 | ||
|
|
4472c81137 | ||
|
|
1e1463cf9a | ||
|
|
c21e44e086 | ||
|
|
e76616de83 | ||
|
|
92794a1822 | ||
|
|
ab0991f054 | ||
|
|
26c201b95b | ||
|
|
09693af5b9 | ||
|
|
f24b7e594b | ||
|
|
35c8ba5688 | ||
|
|
e2a34bd18a | ||
|
|
2f787889f7 | ||
|
|
9967cc0839 | ||
|
|
e47e881ae1 | ||
|
|
d9da100110 | ||
|
|
00cfe0c45b | ||
|
|
d92464b86c | ||
|
|
fec127c1af | ||
|
|
37ee71c663 | ||
|
|
6259e22480 | ||
|
|
474da1a5b5 | ||
|
|
6cee968a8a | ||
|
|
635c74d518 | ||
|
|
260d40f49b | ||
|
|
8f6ff33a39 | ||
|
|
5082f3c829 | ||
|
|
5d0568f759 | ||
|
|
9a78e159e7 | ||
|
|
503368623f | ||
|
|
735d304a20 | ||
|
|
29ddd78a80 | ||
|
|
b6c21216e3 | ||
|
|
6cc9d0db90 | ||
|
|
c6c201e575 |
@@ -1,7 +1,7 @@
|
||||
# ClassicAPI
|
||||
|
||||
ClassicAPI is a DLL for World of Warcraft 1.12.1 (Vanilla / Turtle WoW). It
|
||||
backports a large part of the modern WoW API — 550+ functions and 50+ events,
|
||||
backports a large part of the modern WoW API — 950+ functions and 80+ events,
|
||||
plus much of Lua 5.1 — into the 1.12 client, so addons written for later
|
||||
versions (3.3.5+ or Classic Era) run with little or no change.
|
||||
|
||||
@@ -45,7 +45,7 @@ heading to expand it. For signatures and return values, see the per-function
|
||||
reference in **[docs/API.md](docs/API.md)**.
|
||||
|
||||
<details>
|
||||
<summary><b>In-game Lua calls</b> — 550+ functions across ~60 namespaces</summary>
|
||||
<summary><b>In-game Lua calls</b> — 950+ functions across ~75 namespaces</summary>
|
||||
|
||||
In the Frame rows, a name that starts with `:` is a method of the widget that the row names, for example `:SetShown` is `frame:SetShown`.
|
||||
|
||||
@@ -69,7 +69,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Cursor](docs/API.md#cursor) | `CursorHasMacro`, `GetCursorInfo` |
|
||||
| [Container](docs/API.md#container) | `C_Container.AutoStoreItem`, `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemEquipmentSetInfo`, `C_Container.GetContainerItemQuestInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerFreeSlots`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetBackpackAutosortDisabled`, `C_Container.GetBankAutosortDisabled`, `C_Container.GetItemCooldown`, `C_Container.GetSortBagsRightToLeft`, `C_Container.HasContainerItem`, `C_Container.IsContainerFiltered`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SetBackpackAutosortDisabled`, `C_Container.SetBagPortraitTexture`, `C_Container.SetBankAutosortDisabled`, `C_Container.SetItemSearch`, `C_Container.SetSortBagsRightToLeft`, `C_Container.SortBags`, `C_Container.SortBankBags`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown`, `SetBagPortraitTexture` |
|
||||
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID`, `C_CreatureInfo.GetRaceInfo`, `C_CreatureInfo.GetClassInfo`, `C_CreatureInfo.GetCreatureFamilyInfo`, `C_CreatureInfo.GetCreatureFamilyIDs`, `C_CreatureInfo.GetFactionInfo`, `C_CreatureInfo.GetCreatureTypeInfo`, `C_CreatureInfo.GetCreatureTypeIDs` |
|
||||
| [Currency](docs/API.md#currency) | `GetCoinText`, `C_CurrencyInfo.GetCoinText`, `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString` |
|
||||
| [Currency](docs/API.md#currency) | `GetCoinText`, `C_CurrencyInfo.GetCoinText`, `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString`, `Constants.MoneyFormattingConstants` |
|
||||
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
|
||||
| [DeathInfo](docs/API.md#deathinfo) | `C_DeathInfo.GetSelfResurrectOptions`, `C_DeathInfo.UseSelfResurrectOption` |
|
||||
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.PickupEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` |
|
||||
@@ -100,10 +100,11 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Item: info](docs/API.md#getitemclassinfoclassid) | `GetItemClassInfo`, `GetItemSubClassInfo`, `GetItemIcon`, `IsConsumableItem`, `IsCurrentItem`, `IsDressableItem`, `IsEquippedItemType`, `IsHarmfulItem`, `IsHelpfulItem`, `IsUsableItem` |
|
||||
| [Item: inventory](docs/API.md#getaverageitemlevel) | `GetAverageItemLevel`, `GetInventoryItemID`, `GetInventoryItemDurability`, `GetInventoryItemRepairCost`, `GetInventoryItemsForSlot`, `IsDualWielding`, `SetInventoryPortraitTexture` |
|
||||
| [Item: item IDs](docs/API.md#getitemid--companions-to-the-engines-getitemlink-family) | `GetAuctionItemID`, `GetAuctionSellItemID`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeTargetItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID` |
|
||||
| [Localization](docs/API.md#localization) | `AbbreviateLargeNumbers`, `AbbreviateNumbers`, `BreakUpLargeNumbers`, `C_StringUtil.GetDefaultAbbreviationBreakpoints` |
|
||||
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootAllCorpses`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
|
||||
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
|
||||
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount`, `C_LossOfControl.GetSchoolLockout` |
|
||||
| [Lua](docs/API.md#lua) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `strcmputf8i`, `strconcat`, `string.concat`, `string.gmatch`, `string.gsub` (table replacement), `string.join`, `string.match`, `string.reverse`, `string.split`, `string.trim`, `strjoin`, `strlenutf8`, `strmatch`, `strreplace`, `strrev`, `strsplit`, `strsplittable`, `strtrim`, `table.count`, `table.maxn`, `table.wipe`, `unpack` (range args), `xpcall` (argument forwarding) |
|
||||
| [Lua](docs/API.md#lua) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `strcmputf8i`, `strconcat`, `string.concat`, `string.gmatch`, `string.gsub` (table replacement), `string.join`, `string.match`, `string.reverse`, `string.split`, `string.trim`, `strjoin`, `strlenutf8`, `strmatch`, `strreplace`, `strrev`, `strsplit`, `strsplittable`, `strtrim`, `table.count`, `table.maxn`, `table.wipe`, `unpack` (range args), `wipe`, `xpcall` (argument forwarding) |
|
||||
| [Macros](docs/API.md#macros) | `C_Macro.CreateMacro`, `C_Macro.EditMacro`, `C_Macro.SetMacroDisplay`, `DeleteMacro` / `EditMacro` / `GetMacroInfo` / `PickupMacro` by name, `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroBody`, `GetMacroIcons`, `GetMacroItem`, `GetMacroItemIcons`, `GetMacroSpell`, `GetRunningMacro`, `GetRunningMacroButton`, `RunMacroText`, `SetMacroItem`, `SetMacroSpell`, `StopMacro` |
|
||||
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
|
||||
| [Map](docs/API.md#map) | `C_Map.CanSetUserWaypointOnMap`, `C_Map.ClearUserWaypoint`, `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetFallbackWorldMapID`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapArtLayers`, `C_Map.GetMapArtLayerTextures`, `C_Map.GetMapChildrenInfo`, `C_Map.GetMapInfo`, `C_Map.GetMapInfoAtPosition`, `C_Map.GetMapOverlays`, `C_Map.GetMapPosFromWorldPos`, `C_Map.GetMapRectOnMap`, `C_Map.GetMapWorldSize`, `C_Map.GetPlayerMapPosition`, `C_Map.GetUserWaypoint`, `C_Map.GetUserWaypointFromHyperlink`, `C_Map.GetUserWaypointHyperlink`, `C_Map.GetUserWaypointPositionForMap`, `C_Map.GetWorldPosFromMapPos`, `C_Map.HasUserWaypoint`, `C_Map.MapHasArt`, `C_Map.SetUserWaypoint` |
|
||||
@@ -131,10 +132,10 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime`, `GetTickTime`, `GetTimeCached` |
|
||||
| [Totem](docs/API.md#totem) | `GetTotemInfo`, `GetTotemTimeLeft`, `GetTotemDuration`, `TargetTotem` |
|
||||
| [Tracking](docs/API.md#tracking) | `GetNumTrackingTypes`, `GetTrackingInfo`, `SetTracking` |
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes`, `GetTradeSkillDescription`, `GetTradeSkillRecipeLink`, `IsTradeSkillLinked` |
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `GetTradeSkillListLink`, `GetCraftListLink`, `GetTradeSkillDescription`, `GetTradeSkillRecipeLink`, `IsTradeSkillLinked`, `IsCraftLinked` |
|
||||
| [Trainer](docs/API.md#trainer) | `GetTrainerServiceItemLink` |
|
||||
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
|
||||
| [Unit](docs/API.md#unit) | `CanBeRaidTarget`, `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `SetUnitCursorTexture`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHasIncomingResurrection`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSelectionColor`, `UnitSelectionType`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID`, `UnitTokenFromName` |
|
||||
| [Unit](docs/API.md#unit) | `CanBeRaidTarget`, `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `SetUnitCursorTexture`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitFacing`, `UnitGUID`, `UnitHasIncomingResurrection`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSelectionColor`, `UnitSelectionType`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID`, `UnitTokenFromName` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByAuraInstanceID`, `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySlot`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetAuraSlots`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterAuraDurationModifierByTrigger`, `C_UnitAuras.RegisterComboDuration`, `C_UnitAuras.UnitAura`, `C_UnitAuras.UnitAuraBySlot`, `C_UnitAuras.UnitBuff`, `C_UnitAuras.UnitDebuff` |
|
||||
| [VoiceChat](docs/API.md#voicechat) | `C_VoiceChat.GetTtsVoices`, `C_VoiceChat.GetRemoteTtsVoices`, `C_VoiceChat.SpeakText`, `C_VoiceChat.StopSpeakingText`, `C_TTSSettings.GetSpeechRate`, `C_TTSSettings.GetSpeechVolume`, `C_TTSSettings.GetSpeechVoiceID`, `C_TTSSettings.GetVoiceOptionName`, `C_TTSSettings.SetSpeechRate`, `C_TTSSettings.SetSpeechVolume`, `C_TTSSettings.SetVoiceOption`, `C_TTSSettings.SetVoiceOptionByName`, `C_TTSSettings.SetDefaultSettings`, `C_TTSSettings.RefreshVoices` |
|
||||
| [XMLUtil](docs/API.md#xmlutil) | `C_XMLUtil.DoesTemplateExist`, `C_XMLUtil.GetTemplateInfo`, `C_XMLUtil.GetTemplates` |
|
||||
@@ -207,16 +208,21 @@ for what each command does, and
|
||||
|
||||
Each setting is saved in `WTF\Config.wtf`. Read and write them with `GetCVar`
|
||||
/ `SetCVar` or `/console`. Click a group name for the full rules. To change
|
||||
the most common settings and the player cache in the game, open the game
|
||||
menu and click AddOn Options, then ClassicAPI.
|
||||
these settings and the player cache in the game, open the game menu and
|
||||
click AddOn Options, then ClassicAPI.
|
||||
|
||||
| Group | CVar | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| [Bindings](docs/API.md#actionbuttonusekeydown) | `ActionButtonUseKeyDown` | `0` | `1` uses an action-bar key binding when you press the key. `0` uses it when you release the key. |
|
||||
| [Combat](docs/API.md#stopautoattackontargetchange) | `stopAutoAttackOnTargetChange` | `0` | `1` stops melee auto-attack, Auto Shot and wand shots when you change target. |
|
||||
| [Combat log](docs/API.md#kept-entries) | `combatLogRetentionTime` | `300` | The number of seconds that the combat log keeps an entry. |
|
||||
| [Cast settings](docs/API.md#cast-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast while you sit. |
|
||||
| | `autoDismount` | `0` | `1` makes you dismount when you cast while you are mounted. |
|
||||
| | `autoUnshift` | `0` | `1` makes you leave your form when a spell cannot be cast in it. |
|
||||
| [Bag sorting](docs/API.md#c_containergetsortbagsrighttoleft--c_containersetsortbagsrighttoleftenable) | `sortBagsRightToLeft` | `0` | `1` makes a sort fill your bags from the last slot of the last bag. |
|
||||
| | `backpackAutosortDisabled` | `0` | `1` keeps the items in your backpack in place when you sort your bags. |
|
||||
| | `bankAutosortDisabled` | `0` | `1` keeps the items in the main bank slots in place when you sort your bank. |
|
||||
| [Equipment sets](docs/API.md#equipment-sets-on-item-tooltips) | `dontShowEquipmentSetsOnItems` | `0` | `1` removes the Equipment Sets line from item tooltips. |
|
||||
| [Profiling](docs/API.md#scriptprofile) | `scriptProfile` | `0` | `1` turns on CPU profiling for the `*CPUUsage` functions. The change starts after `/reload`. |
|
||||
| [Soft interact](docs/API.md#soft-interact-softinteract--anyinteract) | `SoftTargetInteract` | `3` | Turns `softinteract` on: `2` keyboard and mouse, `3` always, `0` / `1` off. |
|
||||
| | `SoftTargetInteractArc` | `0` | Where the object can be: `0` straight ahead of the camera, `1` within 30° of it, `2` any direction. |
|
||||
@@ -233,7 +239,9 @@ menu and click AddOn Options, then ClassicAPI.
|
||||
| | `SoftTargetIconEnemy` / `SoftTargetIconFriend` | `0` | `1` shows an icon over `softenemy` / `softfriend`. |
|
||||
| | `SoftTargetLowPriorityIcons` | `0` | `1` also shows the normal pointer as an icon. |
|
||||
| | `SoftTargetNameplateSize` | `19` | Size of all soft-target icons. |
|
||||
| [Localization](docs/API.md#breakuplargenumbers-cvar) | `breakUpLargeNumbers` | `1` | `1` puts a separator between each group of three digits in `BreakUpLargeNumbers`. `0` writes no separator. |
|
||||
| [Cooldown](docs/API.md#createframecooldown--name-parent-template) | `countdownForCooldowns` | `0` | `1` shows the time left as numbers on cooldown frames. |
|
||||
| `ScriptAnimationUtil` | `ShakeStrengthUI` | `1` | Scales how far `ScriptAnimationUtil.ShakeFrame` moves a frame. `0` turns the shake off. |
|
||||
| [NamePlate](docs/API.md#nameplate) | `nameplateNotSelectedAlpha` | `0.5` | Alpha of a non-target default nameplate, `0` to `1`. |
|
||||
| | `nameplateSelectedAlpha` | `1` | Alpha of the target's default nameplate, `0` to `1`. |
|
||||
| [Text to speech](docs/API.md#tts-cvars) | `ttsVoice` | `0` | The voice that `C_VoiceChat.SpeakText` uses. |
|
||||
@@ -297,6 +305,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `PLAYER_AVG_ITEM_LEVEL_READY` | *(none)* |
|
||||
| `PLAYER_EQUIPMENT_CHANGED` | `equipmentSlot, hasCurrent` |
|
||||
| `PLAYER_FOCUS_CHANGED` | *(none)* |
|
||||
| `PLAYER_MOUNT_DISPLAY_CHANGED` | *(none)* |
|
||||
| `PLAYER_SOFT_ENEMY_CHANGED` | *(none)* |
|
||||
| `PLAYER_SOFT_FRIEND_CHANGED` | *(none)* |
|
||||
| `PLAYER_SOFT_INTERACT_CHANGED` | `oldTarget, newTarget` |
|
||||
@@ -316,7 +325,11 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `QUEST_TURNED_IN` | `questID, xpReward, moneyReward` |
|
||||
| `SELF_RES_SPELL_CHANGED` | *(none)* |
|
||||
| `SOUNDKIT_FINISHED` | `soundHandle` (only for sounds played with `runFinishCallback`) |
|
||||
| `UNIT_AREA_CHANGED` | `unitTarget` |
|
||||
| `UNIT_AURA_DELTA` | `unitTarget, updateInfo` (the instance IDs of the auras added, updated, and removed) |
|
||||
| `UNIT_CONNECTION` | `unitTarget, isConnected` |
|
||||
| `UNIT_DISTANCE_CHECK_UPDATE` | `unitTarget, isInDistance` |
|
||||
| `UNIT_IN_RANGE_UPDATE` | `unitTarget, isInRange` |
|
||||
| `UNIT_SPELLCAST_SENT` | `"player", target, castGUID, spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_START` | `unit, castGUID, spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_STOP` | `unit, castGUID, spellID, spellName, rank` |
|
||||
@@ -332,6 +345,12 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `UNIT_SPELLCAST_RETICLE_CLEAR` | `"player", "", spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_INTERRUPTIBLE` | `unit` |
|
||||
| `UNIT_SPELLCAST_NOT_INTERRUPTIBLE` | `unit` |
|
||||
| `UNIT_MAXPOWER` | `unitTarget, powerType` |
|
||||
| `UNIT_POWER_FREQUENT` | `unitTarget, powerType` |
|
||||
| `UNIT_POWER_UPDATE` | `unitTarget, powerType` |
|
||||
| `UNIT_SPELL_HASTE` | `unitTarget` |
|
||||
| `UNIT_TARGET` | `unitTarget` |
|
||||
| `UNIT_TARGETABLE_CHANGED` | `unitTarget` |
|
||||
| `UPDATE_INVENTORY_DURABILITY` | *(none)* |
|
||||
| `UPDATE_SHAPESHIFT_FORM` | *(none)* |
|
||||
| `USER_WAYPOINT_UPDATED` | *(none)* |
|
||||
@@ -432,8 +451,9 @@ functions but that consumer code still expects to find as globals —
|
||||
`CallbackRegistryMixin`, `EventRegistry`, `ColorMixin` + `CreateColor`,
|
||||
`Item` / `ItemLocation`, `MathUtil`(`Lerp` / `Clamp` / `CreateCounter`),
|
||||
`TableUtil` (`tCompare`, `MergeTable`, `SafePack`, etc.), `EventUtil`
|
||||
(`ContinueOnAddOnLoaded` and more), `DataProvider`, `CircularBuffer`,
|
||||
`EasingUtil`, `Interpolator`, `EnumUtil`, `StringUtil`, `AnchorUtil`,
|
||||
(`ContinueOnAddOnLoaded` and more), `FormattingUtil` (`GetMoneyString`,
|
||||
`FormatLargeNumber` and more), `FrameUtil`, `DataProvider`, `CircularBuffer`,
|
||||
`EasingUtil`, `ScriptAnimationUtil`, `Interpolator`, `EnumUtil`, `StringUtil`, `AnchorUtil`,
|
||||
`GridLayoutUtil`, and the layout frame templates (`VerticalLayoutFrame`,
|
||||
`HorizontalLayoutFrame`, `ResizeLayoutFrame`, `GridLayoutFrame`,
|
||||
`StaticGridLayoutFrame`). It also holds the conditional slash
|
||||
@@ -478,9 +498,10 @@ that you use one of these commands:
|
||||
|---------|---------|
|
||||
| `/dump <expr>` | Shows a Lua value in the chat frame. It shows tables and all return values. Use it to examine the return values of `GetSpellInfo`, `C_Item.GetItemInfoInstant`, and other functions. |
|
||||
| `/etrace` | Opens the event trace window. Other forms: `/etrace start`, `/etrace stop`, `/etrace add EVENT`, and more. |
|
||||
| `/framestack [showHidden [showRegions]]` (or `/fstack`) | Shows a tooltip with a list of the frames under the cursor. A `-->` mark shows one frame, with its anchors and the file and line that made it. Left Alt and Right Alt move the mark. Ctrl opens the marked frame in the table inspector. Shift shows its texture files. |
|
||||
| `/framestack [showHidden [showRegions]]` (or `/fstack`) | Shows a tooltip with a list of the frames under the cursor. A `-->` mark shows one frame, with its anchors and the file and line that made it. Left Alt and Right Alt move the mark. Ctrl opens the marked frame in the table inspector. Shift shows its texture files. The tooltip also shows the addon that made the marked frame. When `scriptProfile` is `1`, it shows the CPU time of the frame scripts. |
|
||||
| `/tinspect [expr]` (or `/tins`) | Opens a table inspector on the result of `expr`. With no table, it opens on the frame under the cursor. The window shows the anchors and the fields of the table, grouped by type. You can edit numbers, strings and booleans, filter the lines, and open a child table or frame. |
|
||||
| `/luaerrors` (or `/scripterrors`) | Opens the Lua error window. |
|
||||
| `/profiler` (or `/prof`) | Opens the profiler. The profiler shows the Lua memory, the allocations and the CPU time of each addon, frame, event and global function. `/profiler widget` shows a small window with the Lua memory, the frame rate and the addons that use the most. `/profiler reset` sets the counters to zero. CPU times need `scriptProfile` `1`. `/profiler profile on` sets it and reloads the UI. |
|
||||
|
||||
## Building
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ on a frame timer" goal landed as the request/event pair below:
|
||||
cache (or synchronously if it was already loaded).
|
||||
|
||||
Together those replace the polyfill's 180-second polling frame
|
||||
(`ItemEventListener` in `Util/ItemUtil.lua:254-372`) — addons can now
|
||||
(`ItemEventListener`, now in `Util/AsyncCallbackSystem.lua`) — addons can now
|
||||
fire `RequestLoadItemDataByID(id)` and listen for
|
||||
`ITEM_DATA_LOAD_RESULT` instead.
|
||||
|
||||
|
||||
+66
-49
@@ -3,8 +3,8 @@
|
||||
#
|
||||
# Runs at configure time. It writes two files into OUT_DIR:
|
||||
#
|
||||
# embedded.rc one `<id> RCDATA "<path>"` line per file; rc.exe
|
||||
# reads each file's gzipped copy from disk.
|
||||
# embedded.rc one `<id> RCDATA "<path>"` line per pack; rc.exe
|
||||
# reads each gzipped pack from disk.
|
||||
# embedded_resources.h the IDs the DLL looks up at runtime through
|
||||
# src/embed/Resource.h:
|
||||
#
|
||||
@@ -12,14 +12,24 @@
|
||||
# namespace Addons::Embedded::Manifest {
|
||||
# constexpr const char *kAddons[] = { ... }; // in ADDONS order
|
||||
# constexpr size_t kAddonCount = ...;
|
||||
# struct File { const char *path; int resourceId; };
|
||||
# struct File { const char *path; int resourceId; int index; };
|
||||
# constexpr File kFiles[] = { ... };
|
||||
# constexpr size_t kFileCount = ...;
|
||||
# }
|
||||
#
|
||||
# Each resource is a pack: one gzip stream of several files, which
|
||||
# pack_files.cmake joins at build time into `OUT_DIR/embed/<name>.pack.gz`.
|
||||
# Resource 1 is the bindings XML, a pack of one; then each addon is one
|
||||
# pack, in ADDONS order. A pack compresses each file against the files
|
||||
# before it, so the addon text packs to about 22% of its size, against 27%
|
||||
# for one gzip stream per file. The DLL inflates a whole pack with the
|
||||
# engine's zlib on first use (src/embed/Resource.cpp), so the
|
||||
# load-on-demand addon costs nothing until it loads.
|
||||
#
|
||||
# File paths are relative to ADDONS_ROOT, in backslash form
|
||||
# (`!!!ClassicAPI\Util\Print.lua`) — the suffix the engine builds after
|
||||
# `Interface\AddOns\`. The DLL compares them ignoring case.
|
||||
# `Interface\AddOns\`. The DLL compares them ignoring case. A file's index
|
||||
# is its place in its addon's pack, which is in path order.
|
||||
#
|
||||
# Each addon's `.toc` is the one file that is not embedded verbatim: its
|
||||
# `## Version:` line is stamped with ADDON_VERSION into a copy under
|
||||
@@ -28,30 +38,37 @@
|
||||
# never embedded: it must reflect on-disk presence only (see
|
||||
# DiskHasDevMarker in src/addons/Embedded.cpp).
|
||||
#
|
||||
# Every resource is gzipped at build time (gzip_file.cmake) into
|
||||
# `OUT_DIR/embed/<path>.gz`, and the DLL inflates it with the engine's zlib on
|
||||
# first use (src/embed/Resource.cpp). The addon text compresses to about a
|
||||
# quarter of its size.
|
||||
#
|
||||
# Rebuild triggers: adding or removing a file reconfigures through the
|
||||
# CONFIGURE_DEPENDS glob; editing a toc reconfigures through
|
||||
# CMAKE_CONFIGURE_DEPENDS; editing any other file re-gzips it through its
|
||||
# custom command, which recompiles the .rc through its OBJECT_DEPENDS.
|
||||
# CMAKE_CONFIGURE_DEPENDS; editing any other file re-packs its addon through
|
||||
# the pack's custom command, which recompiles the .rc through its
|
||||
# OBJECT_DEPENDS.
|
||||
#
|
||||
# Usage:
|
||||
# embed_resources(TARGET <t> ADDONS_ROOT <dir> ADDONS <folder names...>
|
||||
# ADDON_VERSION <v> BINDINGS_XML <file> OUT_DIR <dir>)
|
||||
|
||||
set(_EMBED_GZIP_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/gzip_file.cmake")
|
||||
set(_EMBED_PACK_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/pack_files.cmake")
|
||||
|
||||
# Add the build-time command that gzips `src` into `gz`.
|
||||
function(_embed_gzip src gz)
|
||||
# Add the build-time command that packs `ARGN` (absolute paths, in pack
|
||||
# order) into `<out_dir>/embed/<name>.pack.gz`, and set `gz` in the caller
|
||||
# to that path. The paths reach the script through a generated list file:
|
||||
# an addon's paths are too long for one command line.
|
||||
function(_embed_pack out_dir name)
|
||||
set(pack "${out_dir}/embed/${name}.pack")
|
||||
set(list "set(FILES\n")
|
||||
foreach(path ${ARGN})
|
||||
string(APPEND list " [==[${path}]==]\n")
|
||||
endforeach()
|
||||
string(APPEND list ")\n")
|
||||
_embed_write_if_changed("${pack}.cmake" "${list}")
|
||||
add_custom_command(
|
||||
OUTPUT "${gz}"
|
||||
COMMAND "${CMAKE_COMMAND}" "-DIN=${src}" "-DOUT=${gz}"
|
||||
-P "${_EMBED_GZIP_SCRIPT}"
|
||||
DEPENDS "${src}" "${_EMBED_GZIP_SCRIPT}"
|
||||
OUTPUT "${pack}.gz"
|
||||
COMMAND "${CMAKE_COMMAND}" "-DLIST=${pack}.cmake" "-DOUT=${pack}.gz"
|
||||
-P "${_EMBED_PACK_SCRIPT}"
|
||||
DEPENDS ${ARGN} "${pack}.cmake" "${_EMBED_PACK_SCRIPT}"
|
||||
VERBATIM)
|
||||
set(gz "${pack}.gz" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# Write `content` to `path` only when it differs, so an unchanged
|
||||
@@ -71,56 +88,55 @@ function(embed_resources)
|
||||
set(addon_names "")
|
||||
set(manifest "")
|
||||
set(inputs "")
|
||||
set(id 1)
|
||||
|
||||
set(gz "${E_OUT_DIR}/embed/Bindings.xml.gz")
|
||||
_embed_gzip("${E_BINDINGS_XML}" "${gz}")
|
||||
string(APPEND rc "${id} RCDATA \"${gz}\"\n")
|
||||
set(bindings_id 1)
|
||||
_embed_pack("${E_OUT_DIR}" "Bindings.xml" "${E_BINDINGS_XML}")
|
||||
string(APPEND rc "${bindings_id} RCDATA \"${gz}\"\n")
|
||||
list(APPEND inputs "${gz}")
|
||||
set(bindings_id ${id})
|
||||
math(EXPR id "${id} + 1")
|
||||
|
||||
set(files "")
|
||||
set(id 2)
|
||||
foreach(addon ${E_ADDONS})
|
||||
string(APPEND addon_names " \"${addon}\",\n")
|
||||
file(GLOB_RECURSE addon_files CONFIGURE_DEPENDS RELATIVE
|
||||
file(GLOB_RECURSE files CONFIGURE_DEPENDS RELATIVE
|
||||
"${E_ADDONS_ROOT}" "${E_ADDONS_ROOT}/${addon}/*")
|
||||
list(FILTER addon_files EXCLUDE REGEX "(^|/)\\.classicapi-dev$")
|
||||
list(APPEND files ${addon_files})
|
||||
endforeach()
|
||||
list(SORT files) # deterministic IDs
|
||||
foreach(rel ${files})
|
||||
set(abs "${E_ADDONS_ROOT}/${rel}")
|
||||
if(rel MATCHES "\\.toc$")
|
||||
file(READ "${abs}" toc)
|
||||
string(REGEX REPLACE "## Version:[^\n\r]*"
|
||||
"## Version: ${E_ADDON_VERSION}" toc "${toc}")
|
||||
set(stamped "${E_OUT_DIR}/embed/${rel}")
|
||||
_embed_write_if_changed("${stamped}" "${toc}")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${abs}")
|
||||
set(abs "${stamped}")
|
||||
endif()
|
||||
set(gz "${E_OUT_DIR}/embed/${rel}.gz")
|
||||
_embed_gzip("${abs}" "${gz}")
|
||||
list(FILTER files EXCLUDE REGEX "(^|/)\\.classicapi-dev$")
|
||||
list(SORT files) # deterministic indexes
|
||||
set(paths "")
|
||||
set(index 0)
|
||||
foreach(rel ${files})
|
||||
set(abs "${E_ADDONS_ROOT}/${rel}")
|
||||
if(rel MATCHES "\\.toc$")
|
||||
file(READ "${abs}" toc)
|
||||
string(REGEX REPLACE "## Version:[^\n\r]*"
|
||||
"## Version: ${E_ADDON_VERSION}" toc "${toc}")
|
||||
set(stamped "${E_OUT_DIR}/embed/${rel}")
|
||||
_embed_write_if_changed("${stamped}" "${toc}")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${abs}")
|
||||
set(abs "${stamped}")
|
||||
endif()
|
||||
list(APPEND paths "${abs}")
|
||||
string(REPLACE "/" "\\\\" rel_bs "${rel}")
|
||||
string(APPEND manifest " { \"${rel_bs}\", ${id}, ${index} },\n")
|
||||
math(EXPR index "${index} + 1")
|
||||
endforeach()
|
||||
_embed_pack("${E_OUT_DIR}" "${addon}" ${paths})
|
||||
string(APPEND rc "${id} RCDATA \"${gz}\"\n")
|
||||
list(APPEND inputs "${gz}")
|
||||
string(REPLACE "/" "\\\\" rel_bs "${rel}")
|
||||
string(APPEND manifest " { \"${rel_bs}\", ${id} },\n")
|
||||
math(EXPR id "${id} + 1")
|
||||
endforeach()
|
||||
|
||||
_embed_write_if_changed("${E_OUT_DIR}/embedded.rc" "${rc}")
|
||||
_embed_write_if_changed("${E_OUT_DIR}/embedded_resources.h"
|
||||
"// Generated by cmake/embed_resources.cmake — do not edit.
|
||||
// Resource IDs for the RCDATA entries in embedded.rc. Load them through
|
||||
// src/embed/Resource.h.
|
||||
// Resource IDs for the RCDATA packs in embedded.rc, and each embedded file's
|
||||
// place in them. Load them through src/embed/Resource.h.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace Embed::Ids {
|
||||
constexpr int kBindingsXml = ${bindings_id};
|
||||
constexpr int kBindingsXml = ${bindings_id}; // a pack of the one file
|
||||
} // namespace Embed::Ids
|
||||
|
||||
namespace Addons::Embedded::Manifest {
|
||||
@@ -133,7 +149,8 @@ constexpr size_t kAddonCount = sizeof(kAddons) / sizeof(kAddons[0]);
|
||||
|
||||
struct File {
|
||||
const char *path;
|
||||
int resourceId;
|
||||
int resourceId; // its addon's pack
|
||||
int index; // its place in the pack
|
||||
};
|
||||
|
||||
constexpr File kFiles[] = {
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# gzip_file.cmake — gzip one file at build time, for embed_resources.cmake.
|
||||
#
|
||||
# cmake -DIN=<file> -DOUT=<file.gz> -P gzip_file.cmake
|
||||
#
|
||||
# The raw format holds the one file's bytes with no archive entry around
|
||||
# them, so OUT is a plain gzip stream of IN.
|
||||
file(ARCHIVE_CREATE OUTPUT "${OUT}" PATHS "${IN}" FORMAT raw
|
||||
COMPRESSION GZip COMPRESSION_LEVEL 9)
|
||||
@@ -0,0 +1,50 @@
|
||||
# pack_files.cmake — join files into one gzipped pack at build time, for
|
||||
# embed_resources.cmake.
|
||||
#
|
||||
# cmake -DLIST=<list.cmake> -DOUT=<pack.gz> -P pack_files.cmake
|
||||
#
|
||||
# LIST sets FILES to the paths to join, in order. The pack is one gzip
|
||||
# stream, so each file compresses against the files before it: the addon
|
||||
# text packs about 16% smaller than when each file is gzipped alone. Inside
|
||||
# the stream is an index, each file's size as a decimal line and then an
|
||||
# empty line, followed by the files' bytes in FILES order. The DLL splits
|
||||
# it in src/embed/Resource.cpp.
|
||||
|
||||
include("${LIST}")
|
||||
|
||||
set(index "")
|
||||
foreach(file IN LISTS FILES)
|
||||
file(SIZE "${file}" size)
|
||||
string(APPEND index "${size}\n")
|
||||
endforeach()
|
||||
# Not file(WRITE): on Windows it writes each \n as \r\n.
|
||||
file(CONFIGURE OUTPUT "${OUT}.index" CONTENT "${index}\n" @ONLY NEWLINE_STYLE UNIX)
|
||||
|
||||
# Join `ARGN` byte for byte into `out`.
|
||||
function(cat out)
|
||||
execute_process(COMMAND "${CMAKE_COMMAND}" -E cat ${ARGN}
|
||||
OUTPUT_FILE "${out}" RESULT_VARIABLE rc)
|
||||
if(NOT rc EQUAL 0)
|
||||
message(FATAL_ERROR "pack_files.cmake: cannot join into ${out}: ${rc}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# `cmake -E cat` takes its files on the command line, which Windows limits
|
||||
# to 32767 characters: join in batches of 100, then join the batches.
|
||||
set(parts "${OUT}.index")
|
||||
list(LENGTH FILES count)
|
||||
set(first 0)
|
||||
while(first LESS count)
|
||||
list(SUBLIST FILES ${first} 100 batch)
|
||||
set(part "${OUT}.part${first}")
|
||||
cat("${part}" ${batch})
|
||||
list(APPEND parts "${part}")
|
||||
math(EXPR first "${first} + 100")
|
||||
endwhile()
|
||||
cat("${OUT}.raw" ${parts})
|
||||
|
||||
# The raw format holds the one file's bytes with no archive entry around
|
||||
# them, so OUT is a plain gzip stream.
|
||||
file(ARCHIVE_CREATE OUTPUT "${OUT}" PATHS "${OUT}.raw" FORMAT raw
|
||||
COMPRESSION GZip COMPRESSION_LEVEL 9)
|
||||
file(REMOVE ${parts} "${OUT}.raw")
|
||||
+1026
-133
File diff suppressed because it is too large
Load Diff
+21
-6
@@ -98,7 +98,9 @@ namespace Lua {
|
||||
F(Ref, luaL_ref, LUA_REF_REF) \
|
||||
F(Unref, luaL_unref, LUA_REF_UNREF) \
|
||||
F(CheckStack, lua_checkstack, LUA_CHECK_STACK) \
|
||||
F(Concat, lua_concat, LUA_CONCAT)
|
||||
F(Concat, lua_concat, LUA_CONCAT) \
|
||||
F(NewUserdata, lua_newuserdata, LUA_NEW_USERDATA) \
|
||||
F(ToUserdata, lua_touserdata, FUN_FRAMESCRIPT_GET_OBJECT)
|
||||
|
||||
#define CLASSICAPI_BIND_LUA(Name, Typedef, Offset) \
|
||||
const Typedef##_t Name = reinterpret_cast<Typedef##_t>(Offsets::Offset);
|
||||
@@ -401,12 +403,12 @@ void RegisterGlobalAlias(const char *alias, const char *tableName, const char *m
|
||||
Api::Documentation::RecordGlobal(alias, doc);
|
||||
}
|
||||
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count,
|
||||
const char *docSystem) {
|
||||
// `_G[parent][sub] = { entries }`, creating `_G[parent]` if needed.
|
||||
static bool SetIntegerTable(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count) {
|
||||
void *L = State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
return false;
|
||||
EnsureGlobalTable(L, parent); // [parentTbl]
|
||||
PushString(L, sub); // [parentTbl, subName]
|
||||
NewTable(L); // [parentTbl, subName, subTbl]
|
||||
@@ -417,7 +419,20 @@ void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
}
|
||||
SetTable(L, -3); // parentTbl[sub] = subTbl
|
||||
SetTop(L, -2); // pop parentTbl
|
||||
Api::Documentation::RecordEnum(parent, sub, entries, count, docSystem);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count,
|
||||
const char *docSystem) {
|
||||
if (SetIntegerTable(parent, sub, entries, count))
|
||||
Api::Documentation::RecordEnum(parent, sub, entries, count, docSystem);
|
||||
}
|
||||
|
||||
void RegisterConstants(const char *sub, const EnumIntegerEntry *entries, int count,
|
||||
const char *docSystem) {
|
||||
if (SetIntegerTable("Constants", sub, entries, count))
|
||||
Api::Documentation::RecordConstants(sub, entries, count, docSystem);
|
||||
}
|
||||
|
||||
void SetFieldNumber(void *L, const char *key, double value) {
|
||||
|
||||
+15
-3
@@ -319,6 +319,10 @@ using lua_checkstack_t = int(__fastcall *)(void *L, int size);
|
||||
// Pops the top `n` values and pushes their `..` concatenation; n == 0 pushes
|
||||
// "" and n == 1 leaves the value untouched.
|
||||
using lua_concat_t = void(__fastcall *)(void *L, int n);
|
||||
// Pushes a new full userdata of `size` bytes and returns its block.
|
||||
using lua_newuserdata_t = void *(__fastcall *)(void *L, unsigned int size);
|
||||
// The block of the full userdata at idx, the pointer of a light one, else null.
|
||||
using lua_touserdata_t = void *(__fastcall *)(void *L, int idx);
|
||||
|
||||
extern const lua_isnumber_t IsNumber;
|
||||
extern const lua_isstring_t IsString;
|
||||
@@ -376,6 +380,8 @@ extern const luaL_ref_t Ref;
|
||||
extern const luaL_unref_t Unref;
|
||||
extern const lua_checkstack_t CheckStack;
|
||||
extern const lua_concat_t Concat;
|
||||
extern const lua_newuserdata_t NewUserdata;
|
||||
extern const lua_touserdata_t ToUserdata;
|
||||
|
||||
// Returns the global `lua_State *` (read on demand from the engine's global).
|
||||
// Callable outside a Lua callback, e.g. during LoadScriptFunctions setup.
|
||||
@@ -516,9 +522,9 @@ void RegisterTableFunction(const char *tableName, const char *methodName,
|
||||
void RegisterGlobalAlias(const char *alias, const char *tableName, const char *methodName,
|
||||
const Doc::Function *doc = nullptr);
|
||||
|
||||
// Key/value pair for `RegisterIntegerEnum`. `key` becomes a field name
|
||||
// (PascalCase, matching Blizzard's `Enum.*` naming) and `value` is the
|
||||
// integer the enum field maps to.
|
||||
// Key/value pair for `RegisterIntegerEnum` and `RegisterConstants`. `key`
|
||||
// becomes a field name (PascalCase for Blizzard's `Enum.*`, UPPER_CASE for
|
||||
// its `Constants.*`) and `value` is the integer the field maps to.
|
||||
struct EnumIntegerEntry {
|
||||
const char *key;
|
||||
int value;
|
||||
@@ -533,6 +539,12 @@ void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count,
|
||||
const char *docSystem = nullptr);
|
||||
|
||||
// Registers `Constants[sub] = { entries }`, Blizzard's engine constant
|
||||
// tables (`Constants.MoneyFormattingConstants`). Documented as a "Constants"
|
||||
// table of `docSystem`.
|
||||
void RegisterConstants(const char *sub, const EnumIntegerEntry *entries, int count,
|
||||
const char *docSystem);
|
||||
|
||||
// Set `t[key] = value` on the table currently at stack[-1] (the most
|
||||
// common shape used when populating a struct-style table mid-build).
|
||||
// Pushes the key + value, calls `SetTable(L, -3)`, which pops both and
|
||||
|
||||
@@ -32,9 +32,8 @@ namespace {
|
||||
// that are not base-library functions at all. So do not derive this from
|
||||
// the engine's `luaopen_base` table (`0x00811E28`, 36 entries — see
|
||||
// BlizzardScriptAPI.md §3); that is a different set with a different
|
||||
// purpose. Names with no 1.12 counterpart (`wipe` is `table.wipe` here; the
|
||||
// secure family does not exist) stay for parity of the error an addon
|
||||
// sees. Sorted alphabetically.
|
||||
// purpose. Names with no 1.12 counterpart (the secure family does not
|
||||
// exist) stay for parity of the error an addon sees. Sorted alphabetically.
|
||||
const char *const kUnhookableNames[] = {
|
||||
"getfenv", "getmetatable", "hooksecurefunc", "ipairs", "issecurevalue",
|
||||
"issecurevariable", "next", "pairs", "pcall", "pcallwithenv", "rawget",
|
||||
|
||||
+331
-55
@@ -223,8 +223,8 @@ enum Offsets {
|
||||
// VAR_SPELL_RECORDS whose record has `+0x0C == 3` ->
|
||||
// FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP), else raises "Unknown link type"
|
||||
// (`0x00532269`). A full `|H...|h` link fails the prefix test. Co-hooked
|
||||
// by `Tooltip::Hyperlink` for full links, `spell:` / `quest:`, and
|
||||
// trade-skill `enchant:` links. SuperWoW patches a jump at `0x00532210`
|
||||
// by `Tooltip::Hyperlink` for full links, `spell:` / `quest:` / `trade:`,
|
||||
// and trade-skill `enchant:` links. SuperWoW patches a jump at `0x00532210`
|
||||
// (after the `item:` test) that adds `quest:` and resumes at `enchant:`.
|
||||
FUN_SCRIPT_GAMETOOLTIP_SET_HYPERLINK = 0x00531FD0, // slot 12
|
||||
// `SetAction(slot)`: cooldown via FUN_ACTION_SLOT_COOLDOWN, clear, then
|
||||
@@ -324,12 +324,26 @@ enum Offsets {
|
||||
// lua_pcall with **zero Lua args** and the engine error handler
|
||||
// (VAR_FRAMESCRIPT_ERROR_HANDLER_REF) as errfunc. FUN_FRAME_INVOKE_SCRIPT
|
||||
// is the same shape for the no-arg scripts (OnShow/OnHide/OnEnter/…).
|
||||
// `Frame::ScriptArgs` co-hooks both to additionally pass the handler
|
||||
// modern positional args (self, arg1..argN). The frame's own Lua object
|
||||
// `Frame::ScriptArgs` co-hooks both and redirects their pcalls (below) to
|
||||
// additionally pass the handler modern positional args (self,
|
||||
// arg1..argN). The frame's own Lua object
|
||||
// ref lives at frame+OFF_COBJECT_LUA_REF (refcount at
|
||||
// OFF_COBJECT_LUA_REFCOUNT); FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER
|
||||
// lazily creates it when the refcount is 0.
|
||||
FUN_FRAME_RUN_SCRIPT_ARGS = 0x00704F10,
|
||||
// Each runner's own `call LUA_PCALL` (E8, 5 bytes): lua_pcall(L, 0, 0, -2)
|
||||
// with [errfunc, handler] on top, after the runner has set `this` and
|
||||
// arg1..argN. Frame::ScriptArgs redirects both, so a format is only ever
|
||||
// parsed by the runner's own switch (0x704F89: index byte table 0x7052B8
|
||||
// for 'd'..'u', jump table 0x7052A4) — including a spec another DLL adds
|
||||
// at runtime: SuperWoWhook points 'e' (byte 0x7052B9) at a new slot
|
||||
// 0x7052CC whose handler pushes registry[ref] for a 4-byte ref, and fires
|
||||
// CREATE_CHATBUBBLE with "%e%s". In the args runner at this call, [ebp+8] is handlerRef,
|
||||
// [ebp+0Ch] the frame and [ebp-4] the number of argN it set (0x704F54,
|
||||
// 0x705011; at most 19, 0x704F73). In the no-arg runner, [ebp-8] is
|
||||
// handlerRef (0x704D5D) and EDI the frame (0x704D5B).
|
||||
CALL_FRAME_RUN_SCRIPT_ARGS_PCALL = 0x00705199,
|
||||
CALL_FRAME_INVOKE_SCRIPT_PCALL = 0x00704E79,
|
||||
// The base ScriptObject's OnEvent handler slot — an 8-byte
|
||||
// {handler, context} pair at frame+0x0C (from FUN_00702590, the base
|
||||
// resolver every frame-type resolver chains through, and confirmed by
|
||||
@@ -1945,7 +1959,7 @@ enum Offsets {
|
||||
// (loot sparkle), so bit 0x2000 (4.3.4's focus bit) is taken here, and
|
||||
// UnitXP references 0x0060C578 in its body. Unit::TokenEvents co-hooks
|
||||
// the recompute to keep UNIT_SCRIPT_REG_TOKENS on units that have a
|
||||
// ClassicAPI token (focus, nameplateN, markN).
|
||||
// ClassicAPI token (focus, nameplateN, markN, softenemy, softfriend).
|
||||
OFF_CGUNIT_SCRIPT_REGISTRATION = 0xC90,
|
||||
// ClassicAPI's bit for those units (4.3.4's focus bit is 0x2000, taken
|
||||
// here — see above; 0x4000 is 4.3.4's arena bit).
|
||||
@@ -2388,6 +2402,26 @@ enum Offsets {
|
||||
// nothing matches. What FUN_004F91A0 calls for an unequipped bag.
|
||||
FUN_DESC_OBSERVER_UNREGISTER = 0x00467FB0,
|
||||
|
||||
// Type-wide observers (ClntObjMgrSetTypeMirrorHandler): one record on
|
||||
// the global list of a (bank, field) at 0x00B23310, called for EVERY
|
||||
// object of that type whose watched bytes change, same callback ABI as
|
||||
// the per-object observers below. CGUnit_C::RegisterFieldWatchers
|
||||
// (0x006041F0) registers the engine's 25 unit handlers with it from
|
||||
// ClientInitializeGame, OnPowerFieldChanged (bank 3, +0x44, 0x14) among
|
||||
// them; ClearFieldWatchers (0x00605290) removes each by name. The list
|
||||
// array is referenced only by these two and the update passes
|
||||
// (0x00465443, 0x00466670): nothing frees or resets a list wholesale.
|
||||
// The record's copy offset comes from tables MirrorInitialize (0x0047F840)
|
||||
// rebuilds at the top of every ClientInitializeGame, before UI init.
|
||||
// Register — __fastcall(int bank /*ecx*/, uint32_t fieldOffset /*edx*/,
|
||||
// uint32_t size, const void *callback, void *userArg, int kind),
|
||||
// RET 0x10. kind 0 links at the pending tail, 1 at the head.
|
||||
FUN_TYPE_OBSERVER_REGISTER = 0x00468070,
|
||||
// Unregister — __fastcall(int bank /*ecx*/, uint32_t fieldOffset
|
||||
// /*edx*/, const void *callback, void *userArg), RET 8; matches
|
||||
// (callback, userArg) and no-ops when nothing matches.
|
||||
FUN_TYPE_OBSERVER_UNREGISTER = 0x00468240,
|
||||
|
||||
// Observer callback ABI (verified from the dispatcher's call site at
|
||||
// 0x004655DC + the engine callback FUN_004F8DB0's RET 0x10):
|
||||
// int __fastcall cb(uint32_t fieldOffset /*ecx, as registered*/,
|
||||
@@ -2415,6 +2449,31 @@ enum Offsets {
|
||||
// Per-index watched size (FUN_0051bbb0's switch): i∈{0,2,4,10}=8,
|
||||
// 0x29=0xD8 (UNIT_FIELD_AURA), 0x6B=0x30, 0xA7/0xAE=0x1C, else 4.
|
||||
VAR_EVENT_NAME_TABLE_STATIC = 0x00BE1198, // char*[] boot name array; slot i (base+i*4) non-null ⇒ real event i
|
||||
// Its length: world init builds the table from it with
|
||||
// `mov edx,0x225; mov ecx,0xBE1198; call 0x703D90` (0x0048FEA1).
|
||||
EVENT_NAME_TABLE_STATIC_COUNT = 0x225,
|
||||
// UnitWatchedIndex — the position of unit field `field` (bank-relative
|
||||
// index, the unit-field event id) in the unit's watched-field table
|
||||
// (0x00839E48, 0xA7 entries), the slot of its 4 bytes in the watch copy
|
||||
// every observer's mirror lives in. A field outside the table answers
|
||||
// UNIT_WATCHED_NONE, and the copy offset built from it lies past the
|
||||
// unit's 170-slot copy, inside the object: a watcher on such a field
|
||||
// writes its old value over the object (UNIT_MOD_CAST_SPEED, index 139,
|
||||
// put its 1.0f over a list link and crashed ClntObjMgrRelease). Fields
|
||||
// outside the table: 8, 9, 34..39, 123, 124, 126, 139, 140, 156, 157, 181.
|
||||
// `uint32_t __fastcall(uint32_t field, uint32_t dwords, uint32_t
|
||||
// isActivePlayer)`, RET 4; both FieldCopyOffset (0x00466830) and
|
||||
// GetWatcherCopyOffset (0x004680C0) take the unit slot from it.
|
||||
FUN_UNIT_WATCHED_INDEX = 0x0047DB20,
|
||||
UNIT_WATCHED_NONE = 0xB6,
|
||||
// The table itself (.data, u32 per entry): 166 field indices and one
|
||||
// spare entry holding UNIT_WATCHED_NONE (position 166, the copy's last
|
||||
// slot, 169 — no field asks for it: unit indices end at 181 and player
|
||||
// fields take their slots from 170 on). UnitWatchedIndex (0x0047DB25) is
|
||||
// the table's only reader and nothing writes it, so a field written into
|
||||
// the spare entry gets a copy slot of its own.
|
||||
VAR_UNIT_WATCHED_FIELDS = 0x00839E48,
|
||||
UNIT_WATCHED_FIELD_COUNT = 0xA7,
|
||||
EVENT_NAME_UNIT_MAX = 0xB6, // FUN_0051bbb0's loop bound
|
||||
|
||||
// CGPlayer_C::AddActiveWatchers' inventory-slot observer
|
||||
@@ -2570,6 +2629,14 @@ enum Offsets {
|
||||
OFF_NAMEPLATE_LINK_NEXT = 0x4E0,
|
||||
OFF_NAMEPLATE_GUID = 0x4E8,
|
||||
OFF_UNIT_NAMEPLATE = 0xE60,
|
||||
// unit+0xE64 is the unit's chat bubble (0x608AC0 fills it, 0x608C00
|
||||
// releases it; benilla-re objectlayer/unitnames.md "Chat bubbles").
|
||||
OFF_UNIT_CHAT_BUBBLE = 0xE64,
|
||||
// CGUnit_C::ShouldShowName — `int __thiscall(CGUnit_C *, uint32_t mask)`,
|
||||
// RET 4: whether the unit's overhead name shows. The mask is the UnitName*
|
||||
// cvars' (0xCE8720). Its only caller is the name draw
|
||||
// PLAYERNAMEDESC::UpdatePlate (0x6C6EE2), every frame. Disassembled.
|
||||
FUN_CGUNIT_SHOULD_SHOW_NAME = 0x006070A0,
|
||||
// CGNamePlateFrame vtable: the 40 virtual slots the plate constructor
|
||||
// stores as the frame's vfptr. Slot 10 (+0x28) is
|
||||
// FUN_FRAME_SET_FRAME_ALPHA, which is how the plate fade writes 0xFF /
|
||||
@@ -2647,6 +2714,7 @@ enum Offsets {
|
||||
// mana/rage/focus/energy/happiness order as POWER1..5). Vanilla
|
||||
// 1.12 has 5 power types; the WotLK additions (Runes / Runic Power)
|
||||
// don't exist in this descriptor layout.
|
||||
UNIT_POWER_TYPE_COUNT = 5, // POWER1..5 / MAXPOWER1..5; the slot is the power type
|
||||
|
||||
// UNIT_MOD_CAST_SPEED — the cast-time multiplier float (1.0 = normal,
|
||||
// <1.0 = faster). The server folds it into `SpellEntry::GetCastTime`
|
||||
@@ -2743,6 +2811,25 @@ enum Offsets {
|
||||
// and vmangos UnitDefines.h agree). 4.3.4 drops the focus when a unit
|
||||
// gains it (CGUnit_C::OnFlagChanged → BreakTarget); 1.12 has no reader.
|
||||
UNIT_FLAG_NOT_SELECTABLE = 0x02000000,
|
||||
// CGUnit_C::OnUnitFlagsChanged — 4.3.4's CGUnit_C::OnFlagChanged less its
|
||||
// UNIT_FLAG_NOT_SELECTABLE branch (break target, UNIT_TARGETABLE_CHANGED):
|
||||
// acts on the bits that differ from `oldFlags`.
|
||||
// `void __thiscall(CGUnit_C *unit, uint32_t oldFlags)`, RET 4 (0x005FF7F2).
|
||||
// One E8 call, from the flags field's type watcher OnFlagsFieldChanged
|
||||
// (0x006044B0), which resolves the unit and passes the old value; no
|
||||
// loaded DLL references either address.
|
||||
FUN_UNIT_FLAGS_CHANGED = 0x005FF580,
|
||||
CALL_FLAGS_FIELD_FLAGS_CHANGED = 0x006044DB,
|
||||
// CGUnit_C::OnMountDisplayChanged — dismounts from the old mount display
|
||||
// (when `oldMountDisplay` is nonzero), mounts the new one with the
|
||||
// mount-up effect, then refreshes the loop sound, quest marker and name.
|
||||
// `void __thiscall(CGUnit_C *unit, uint32_t oldMountDisplay)`, RET 4
|
||||
// (0x005FFAFF). One E8 call, from the UNIT_FIELD_MOUNTDISPLAYID type
|
||||
// watcher OnMountDisplayFieldChanged (0x00604570), which resolves the
|
||||
// unit and passes the old value. VanillaHelpers calls the function
|
||||
// directly after a local mount morph and patches neither address.
|
||||
FUN_UNIT_MOUNT_DISPLAY_CHANGED = 0x005FFA50,
|
||||
CALL_MOUNT_DISPLAY_FIELD_CHANGED = 0x0060459B,
|
||||
// Bit 20 of UNIT_FIELD_FLAGS — on a flight path (vmangos
|
||||
// UNIT_FLAG_TAXI_FLIGHT). The stand setter FUN_PLAYER_SEND_STAND_STATE
|
||||
// refuses while it is set (benilla-re objectlayer/standstate#the-sit-gate).
|
||||
@@ -4630,15 +4717,6 @@ enum Offsets {
|
||||
// `0x0048BACF` / `0x0048BADC`.
|
||||
OFF_CGOBJECT_VTBL_GET_POSITION = 0x14,
|
||||
|
||||
// CGUnit movement-block orientation (facing) in radians, 0..2*pi. Lives
|
||||
// right after the {x,y,z} position block (at +0x9B8), i.e. pos+0xC. The
|
||||
// `GetPosition` virtual copies only x/y/z into the caller's buffer, so
|
||||
// read facing off the object directly. Backs `GetPlayerFacing`. Empirically
|
||||
// located by probing the player object across a known 180-degree turn
|
||||
// (value flipped by ~pi, stayed in range); mirrored at +0x9F8 / +0xC98
|
||||
// (last-sent / render copies) — +0x9C4 is the authoritative one.
|
||||
OFF_UNIT_FACING = 0x9C4,
|
||||
|
||||
// CGObject `m_objectType` field — 0 = (base) object, 1 = item,
|
||||
// 2 = container, 3 = unit, 4 = player, 5 = gameobject, … Read as
|
||||
// `*(int *)(obj + 0x14)`. (Distinct from GET_POSITION above, which
|
||||
@@ -4648,10 +4726,16 @@ enum Offsets {
|
||||
OBJECT_TYPE_PLAYER = 4,
|
||||
|
||||
// CGObject vtable slots (byte offsets): GetFacing `float __thiscall()`
|
||||
// (slot 6, the tab collector's player facing), IsMouseoverable
|
||||
// (slot 6, the world facing; unit and player vtables 0x80C4F8 / 0x80AF78
|
||||
// both hold CGUnit_C::GetFacing 0x5F1F30 there, read from the binary —
|
||||
// backs `UnitFacing` / `GetPlayerFacing`), IsMouseoverable
|
||||
// `int __thiscall()` (slot 21, the hover dispatch's gate at 0x482982).
|
||||
OFF_CGOBJECT_VTBL_GET_FACING = 0x18,
|
||||
OFF_CGOBJECT_VTBL_IS_MOUSEOVERABLE = 0x54,
|
||||
// CanSelect `int __thiscall()` (slot 22): the unit and player vtables both
|
||||
// hold CGUnit_C::CanSelect 0x5F1EC0, which jumps through slot 21 above.
|
||||
// SelectTarget and CGUnit_C::ShouldShowName (0x6070A8) call it.
|
||||
OFF_CGOBJECT_VTBL_CAN_SELECT = 0x58,
|
||||
|
||||
// The selection circle (benilla-re objectlayer/unitdisplay.md, verified
|
||||
// against the bytes of 0x608E00 / 0x614A90). The base per-frame step
|
||||
@@ -4853,6 +4937,17 @@ enum Offsets {
|
||||
// arrays: a 1-based backpack slot S maps to linear (S - 1) + 23. Matches
|
||||
// PackBagSlot's `*param_3 += 0x17` for the backpack branch.
|
||||
BACKPACK_LINEAR_BASE = 23,
|
||||
// Lua container indices (bagID), as `PackBagSlot` reads them and FrameXML
|
||||
// names them (KEYRING_CONTAINER, BANK_CONTAINER, BACKPACK_CONTAINER,
|
||||
// NUM_BAG_SLOTS, NUM_BANKBAGSLOTS): keyring -2, bank -1, backpack 0,
|
||||
// equipped bags 1..4, bank bags 5..10. `Enum.BagIndex` publishes them.
|
||||
BAG_INDEX_KEYRING = -2,
|
||||
BAG_INDEX_BANK = -1,
|
||||
BAG_INDEX_BACKPACK = 0,
|
||||
BAG_INDEX_BAG_FIRST = 1,
|
||||
BAG_INDEX_BAG_LAST = 4,
|
||||
BAG_INDEX_BANK_BAG_FIRST = 5,
|
||||
BAG_INDEX_BANK_BAG_LAST = 10,
|
||||
// Player inventory manager layout — used for direct GUID-array reads
|
||||
// that bypass `GetItemBySlot`'s bank gate at `0x006228C1`.
|
||||
// +0x00 uint32 max slot count
|
||||
@@ -5530,6 +5625,25 @@ enum Offsets {
|
||||
// 0x009BD3C0) does not match the subclass name of these rows.
|
||||
ITEMSUBCLASS_FLAG_NO_SEARCH_NAME = 0x1,
|
||||
|
||||
// PaperDollItemFrame.dbc — the paper-doll slot table. Like ItemSubClass,
|
||||
// the engine reads a parallel (records, count) pair at the class
|
||||
// instance's +0x00 / +0x04: `Script_GetInventorySlotInfo` (0x004C81B0)
|
||||
// walks [0x00C0DA00] for [0x00C0DA04] flat 0x10-byte rows. Columns
|
||||
// (verified against the dumped DBC):
|
||||
// +0x00 char* ItemSlotName ("HeadSlot", "Bag0Slot", "AmmoSlot", ...)
|
||||
// +0x04 char* SlotIcon ("interface\paperdoll\UI-PaperDoll-Slot-Head.blp")
|
||||
// +0x08 int SlotNumber (inventory slot: 1..19 equipment, 20..23 bag
|
||||
// slots, 0 ammo)
|
||||
// The engine flags SlotNumber 18 (ranged) as the relic-check slot
|
||||
// (`SlotNumber - 1 == 0x11` at 0x004C8264).
|
||||
VAR_PAPERDOLLITEMFRAME_RECORDS = 0x00C0DA00,
|
||||
VAR_PAPERDOLLITEMFRAME_COUNT = 0x00C0DA04,
|
||||
OFF_PAPERDOLLITEMFRAME_RECORD_STRIDE = 0x10,
|
||||
OFF_PAPERDOLLITEMFRAME_SLOT_NAME = 0x00,
|
||||
OFF_PAPERDOLLITEMFRAME_SLOT_ICON = 0x04,
|
||||
OFF_PAPERDOLLITEMFRAME_SLOT_NUMBER = 0x08,
|
||||
PAPERDOLLITEMFRAME_RELIC_SLOT_NUMBER = 18,
|
||||
|
||||
// ItemDisplayInfo.dbc — standard layout. Icon path char* at record +0x14
|
||||
// (per the helper at 0x005D88B0 that Script_GetItemInfo uses).
|
||||
VAR_ITEMDISPLAYINFO_RECORDS = 0x00C0DC10,
|
||||
@@ -6068,8 +6182,8 @@ enum Offsets {
|
||||
// rank-up / passive rows have both zero. Empirically: the
|
||||
// Blacksmithing skill line has 343 SLA rows, 334 with a nonzero
|
||||
// threshold — the 9 excluded are the Apprentice..Artisan rank
|
||||
// spells and skill grants. This is the discriminator
|
||||
// `TradeSkill::Link` uses to build the canonical recipe universe.
|
||||
// spells and skill grants. The profession list builds read both
|
||||
// for a row's difficulty.
|
||||
OFF_SLA_TRIVIAL_SKILL_HIGH = 0x28,
|
||||
OFF_SLA_TRIVIAL_SKILL_LOW = 0x2C,
|
||||
// The engine's (race, class, spellID) -> SkillLineAbility record lookup:
|
||||
@@ -7822,6 +7936,61 @@ enum Offsets {
|
||||
// present (and thus connected). Both take a pointer to the 64-bit GUID.
|
||||
OFF_GROUP_MEMBER_STATUS_FLAGS = 0x08,
|
||||
GROUP_MEMBER_STATUS_ONLINE = 0x1,
|
||||
// The party record's pet: its GUID and power type, whose change bits
|
||||
// SignalPartyRecordChanges fires for the pet's GUID (0x005E5720).
|
||||
OFF_GROUP_MEMBER_PET_GUID = 0x80,
|
||||
OFF_GROUP_MEMBER_PET_POWER_TYPE = 0xD8,
|
||||
// SignalPartyRecordChanges — 4.3.4's
|
||||
// CGPlayer_C::DispatchPartyMemberStatsChanges: raises the unit events for
|
||||
// the fields in `changes` (the DiffPartyRecords 0x005E5510 mask) between
|
||||
// two party records, through FUN_UNIT_EVENT_BROADCAST.
|
||||
// `void __fastcall(const uint64_t *guid, uint32_t changes,
|
||||
// const uint8_t *oldRec, const uint8_t *newRec)`, RET 8 (0x005E5909).
|
||||
// Two callers, both after the new record is stored: ReadPartyMemberStats
|
||||
// (0x005E5110, SMSG 0x7E / 0x2F2) and CGPlayer_C::ComparePartyRecord
|
||||
// (0x005F0B80). Both E8 calls, targets checked against the exe; no loaded
|
||||
// DLL references the function or either site.
|
||||
FUN_PARTY_RECORD_SIGNAL = 0x005E5720,
|
||||
CALL_READ_PARTY_MEMBER_STATS_SIGNAL = 0x005E54FA,
|
||||
CALL_COMPARE_PARTY_RECORD_SIGNAL = 0x005F0C42,
|
||||
// DiffPartyRecords — the `changes` mask of the fields two party records
|
||||
// differ in (bit 1 status, 2 health, 0x10 power, 0x20 max power, …).
|
||||
// `uint32_t __fastcall(const uint8_t *oldRec, const uint8_t *newRec)`.
|
||||
FUN_PARTY_RECORD_DIFF = 0x005E5510,
|
||||
// The group list (SMSG 0x7D, 0x005E6A40) stores each member it keeps:
|
||||
// AddPartyMember — 4.3.4's CGPartyInfo::AddMember less its closing
|
||||
// diff-and-dispatch — copies the member's previous record (or a fresh
|
||||
// one when `oldRecord` is null) into its party slot and sets or clears
|
||||
// the status online bit from `online`.
|
||||
// `void __fastcall(uint32_t online, const uint8_t *oldRecord,
|
||||
// uint64_t guid)`, RET 8 (0x004E8482). One E8 call, in the group list.
|
||||
FUN_ADD_PARTY_MEMBER = 0x004E82D0,
|
||||
CALL_GROUP_LIST_ADD_PARTY_MEMBER = 0x005E6C4A,
|
||||
// RebuildRaidRoster — refills the raid slots from the group list: a
|
||||
// returning member's previous record is copied into slot + 0x10 (so the
|
||||
// status byte lands at OFF_RAID_SLOT_FLAGS) and its online bit is set or
|
||||
// cleared from `online[i]`; signals RAID_ROSTER_UPDATE.
|
||||
// `void __fastcall(const uint64_t *guids, const uint32_t *flags,
|
||||
// const uint8_t *online, uint32_t count)`, RET 8 (0x004BACC7). One E8
|
||||
// call, in the group list.
|
||||
FUN_REBUILD_RAID_ROSTER = 0x004BA5F0,
|
||||
CALL_GROUP_LIST_REBUILD_RAID_ROSTER = 0x005E6EB4,
|
||||
// UpdateRaidMemberRecord — a player object's raid slot as it enters or
|
||||
// leaves view: with `mode` 0 it stores a live record (FUN_005F0880) in
|
||||
// the player's raid slot and sends 0x27F; with 1 it does nothing.
|
||||
// `void __fastcall(void *player, uint32_t mode, uint32_t withPet)`, RET 4
|
||||
// (0x004BAE8B). Its four E8 callers are exactly a CGPlayer_C entering
|
||||
// and leaving the object table: PostInit and PostUpdate (reactivation)
|
||||
// with mode 1, the release slot (0x005E9AA0) with 0, and ~CGPlayer_C
|
||||
// with 0 only for an object never released (CGObject_C::IsReleased
|
||||
// 0x00614EA0). Each pairs with UpdatePartyMemberRecord (0x004E85D0) one
|
||||
// call earlier. Targets checked against the exe; no loaded DLL
|
||||
// references the function or a site.
|
||||
FUN_UPDATE_RAID_MEMBER_RECORD = 0x004BADC0,
|
||||
CALL_PLAYER_POST_INIT_RAID_RECORD = 0x005DEC43,
|
||||
CALL_PLAYER_POST_UPDATE_RAID_RECORD = 0x005DECAF,
|
||||
CALL_PLAYER_RELEASE_RAID_RECORD = 0x005E9ACF,
|
||||
CALL_PLAYER_DTOR_RAID_RECORD = 0x005DD6AB,
|
||||
// Bit 1: flagged for PvP — Script_UnitIsPVP's test (0x00516502) for a
|
||||
// token with no unit object.
|
||||
GROUP_MEMBER_STATUS_PVP = 0x2,
|
||||
@@ -7968,6 +8137,11 @@ enum Offsets {
|
||||
LUA_REF_UNREF = 0x6F5400,
|
||||
LUA_PUSH_CCLOSURE = 0x6F3920,
|
||||
LUA_NEW_TABLE = 0x6F3C90,
|
||||
// `lua_newuserdata(L, size)` — `__fastcall`, plain RET; pushes a full
|
||||
// userdata (tt 7, stamped with the exec context like every push) and
|
||||
// returns its block, the Udata + 0x10. Verified by disassembly: GC step
|
||||
// test, luaS_newudata (FUN_LUA_S_NEWUDATA), `ADD EAX,0x10`.
|
||||
LUA_NEW_USERDATA = 0x6F4560,
|
||||
LUA_GET_TABLE = 0x6F3A40, // (was 0x6F3EA0, which is lua_rawset)
|
||||
LUA_RAW_GET = 0x6F3B00,
|
||||
// `lua_rawgeti(L, idx, n)` — pushes `table_at_idx[n]` without invoking
|
||||
@@ -10161,13 +10335,18 @@ enum Offsets {
|
||||
FUN_RAISE_BAG_EVENTS = 0x004F91A0,
|
||||
FUN_RAISE_BAG_UPDATE = 0x004F9370,
|
||||
FUN_ON_BACKPACK_SLOT_CHANGED = 0x004F8DB0,
|
||||
// World-subsystem per-frame update — runs exactly once per frame
|
||||
// as part of the main world tick (`FUN_00482EA0`). Only one caller
|
||||
// in the entire binary; deep in the rendering/world pipeline,
|
||||
// nowhere near the chat/cast/transmog code regions that other
|
||||
// DLLs (SuperWoWhook / nampower / transmogfix) hook. We hook the
|
||||
// tail of this function to drain `BAG_UPDATE_DELAYED`'s pending
|
||||
// flag — exact per-frame coalescing matching modern WoW.
|
||||
// CWorld::BeginFrame, the world-subsystem per-frame update — runs
|
||||
// exactly once per frame from CGWorldFrame::OnRender
|
||||
// (`FUN_00482EA0`), its only caller, nowhere near the
|
||||
// chat/cast/transmog code regions that other DLLs (SuperWoWhook /
|
||||
// nampower / transmogfix) hook. OnRender is run by WorldFrame's
|
||||
// layer-0 draw callback (`0x00482D70`), so this executes INSIDE the
|
||||
// UI strata walk: the WORLD stratum's batches are already rebuilt and
|
||||
// its levels above 0 (the nameplates) are not drawn yet. Lua run here
|
||||
// that changes a font string in that stratum leaves a batch drawing a
|
||||
// destroyed font (fault in the atlas-page upload under 0x005C8FE0).
|
||||
// `Tick::WorldTick` only notes the tick here and runs its subscribers
|
||||
// after the draw.
|
||||
//
|
||||
// Calling convention: `__fastcall(int ecx, int edx, int stackArg)`,
|
||||
// `RET 0x4` (callee cleans the one stack arg).
|
||||
@@ -10807,13 +10986,71 @@ enum Offsets {
|
||||
// window is open. `FUN_004FC870` zeroes it on close (and fires
|
||||
// event `0x13B`), so `> 0` reliably means "trade window open".
|
||||
VAR_CURRENT_TRADESKILL_LINE = 0x00BDE040,
|
||||
// Craft window (Enchanting, pet training) open-state flag — nonzero
|
||||
// (3 for the enchanting-style craft) while open; `FUN_004F5FE0`
|
||||
// zeroes it on close (fires event `0x169`). The Craft window has no
|
||||
// skill-line-ID global — `TradeSkill::Link` derives the skill line
|
||||
// from the first craft recipe — so this is the "craft open" gate.
|
||||
// Craft window (Enchanting, pet training): the open craft type — the
|
||||
// opening spell's EffectMiscValue[0], 3 for Enchanting, 1 for Beast
|
||||
// Training — or 0 when closed. `FUN_004F5FE0` zeroes it on close (fires
|
||||
// event `0x169`). The Craft window keeps no skill line.
|
||||
VAR_CRAFT_WINDOW_STATE = 0x00BDCFB8,
|
||||
|
||||
// The profession windows' open, build and shutdown seams that
|
||||
// `TradeSkill::Window` uses to show another player's profession from a
|
||||
// `trade:` link, as 3.3.5's CGTradeSkillInfo does. Disassembly.
|
||||
//
|
||||
// `void __fastcall(uint32_t skillLine)`: opens the TradeSkill window on a
|
||||
// skill line, or closes it when that line is already open. The only
|
||||
// caller is TryCastSpell at CALL_TRY_CAST_TOGGLE_TRADE_SKILL, with the
|
||||
// cast spell ID in ESI.
|
||||
FUN_TOGGLE_TRADE_SKILL = 0x004FC910,
|
||||
CALL_TRY_CAST_TOGGLE_TRADE_SKILL = 0x006E4C22,
|
||||
// `void __fastcall(uint32_t craftType)`: the Craft window's twin. The
|
||||
// only caller is TryCastSpell at CALL_TRY_CAST_TOGGLE_CRAFT, ESI = spell.
|
||||
FUN_TOGGLE_CRAFT = 0x004F6000,
|
||||
CALL_TRY_CAST_TOGGLE_CRAFT = 0x006E4C34,
|
||||
// `void(void)`: clears the TradeSkill repeat latch and fires
|
||||
// UPDATE_TRADESKILL_RECAST (3.3.5 CGTradeSkillInfo::StopRecast).
|
||||
FUN_TRADE_SKILL_STOP_RECAST = 0x004FD790,
|
||||
// The TradeSkill list build (BuildTradeSkillList, 0x004FCA20) reads per
|
||||
// row: the open line's spell array from
|
||||
// `CGPlayer_C::FindSkillLineSpells(player, skillLine)` (`__thiscall`,
|
||||
// returns a growable array — count +0x04, int spell IDs at +0x08 — or
|
||||
// null), each spell's ability row from FUN_SKILL_LINE_ABILITY_LOOKUP
|
||||
// with the player's race and class, and the player's raw rank in the
|
||||
// ability's line from the 33 bytes at SITE_BUILD_TRADE_SKILL_RANK
|
||||
// (`call FindSkillIndex` .. `xor eax,eax`; ECX = player, the line
|
||||
// pushed, rank left in EAX at 0x004FCC49). One call site each.
|
||||
FUN_PLAYER_FIND_SKILL_LINE_SPELLS = 0x005EA2C0,
|
||||
CALL_BUILD_TRADE_SKILL_SPELLS = 0x004FCB2A,
|
||||
CALL_BUILD_TRADE_SKILL_ABILITY = 0x004FCBD2,
|
||||
SITE_BUILD_TRADE_SKILL_RANK = 0x004FCC28,
|
||||
// The Craft list build (BuildCraftList, 0x004F60C0) reads per row: the
|
||||
// type's spell array from `CGPlayer_C::GetTabSpells(player, tab)`
|
||||
// (`__thiscall`, the array at player + 0x1CD0 + tab * 16, same shape),
|
||||
// each spell's ability row from `CGUnit_C::FindSkillLineAbility(unit,
|
||||
// spell)` (`__thiscall`, the unit's race and class), and the rank from
|
||||
// `CGPlayer_C::GetSkillTotal(player, skillLine)` (`__thiscall`, base plus
|
||||
// temporary bonus). GetTabSpells' other caller is
|
||||
// Script_GetCraftDisplaySkillLine (0x004F6CF4).
|
||||
FUN_PLAYER_GET_TAB_SPELLS = 0x005EA330,
|
||||
CALL_BUILD_CRAFT_SPELLS = 0x004F6118,
|
||||
FUN_UNIT_FIND_SKILL_LINE_ABILITY = 0x0060C7C0,
|
||||
CALL_BUILD_CRAFT_ABILITY = 0x004F61A8,
|
||||
FUN_PLAYER_GET_SKILL_TOTAL = 0x005EA520,
|
||||
CALL_BUILD_CRAFT_RANK = 0x004F620E,
|
||||
// `void(void)` each: free the TradeSkill / Craft rows and zero their
|
||||
// state. CGGameUI::ShutdownGame (0x00491180) calls them once per return
|
||||
// to the character screen, as 3.3.5's calls CGTradeSkillInfo::ShutdownGame.
|
||||
FUN_SHUTDOWN_TRADE_SKILL = 0x004FC790,
|
||||
CALL_GAME_SHUTDOWN_TRADE_SKILL = 0x004911C8,
|
||||
FUN_SHUTDOWN_CRAFT = 0x004F5F00,
|
||||
CALL_GAME_SHUTDOWN_CRAFT = 0x004911D2,
|
||||
// The window getters that answer for the player: GetTradeSkillLine and
|
||||
// GetCraftDisplaySkillLine push (name, rank, maxRank) from the player's
|
||||
// skill slot; GetCraftName pushes the name of the spell the player keeps
|
||||
// for the open type. `int __fastcall(lua_State *)` each.
|
||||
FUN_SCRIPT_GET_TRADE_SKILL_LINE = 0x004FDD40,
|
||||
FUN_SCRIPT_GET_CRAFT_DISPLAY_SKILL_LINE = 0x004F6CB0,
|
||||
FUN_SCRIPT_GET_CRAFT_NAME = 0x004F6C10,
|
||||
|
||||
// Trainer window (ClassTrainerFrame.cpp). Same slot-indirected shape as
|
||||
// the trade-skill rows: `[VAR_TRAINER_SERVICES]` points at an array of
|
||||
// 0x38-byte service records, the record's `+0x00` is the spell the
|
||||
@@ -10888,6 +11125,14 @@ enum Offsets {
|
||||
// Verified from Spell.dbc: 13262 "Disenchant" has Effect[0] = 99 (the
|
||||
// only row carrying it). Effect 32 is TRIGGER_MISSILE, not disenchant.
|
||||
SPELL_EFFECT_DISENCHANT = 99,
|
||||
// A profession spell (Blacksmithing 2018, Enchanting 7411, Smelting
|
||||
// 2656): casting it opens a profession window. TryCastSpell (0x006E4B60)
|
||||
// tests Effect[0] for it (`cmp [edi+0xF4], 0x2F` at 0x006E4BCE) and
|
||||
// routes on EffectMiscValue[0]: 0 opens the TradeSkill window for the
|
||||
// spell's skill line, any other value is the Craft window's type (3
|
||||
// Enchanting, 1 Beast Training). 3.3.5's GetTradeSkillListLink takes
|
||||
// only spells whose Effect[0] is this.
|
||||
SPELL_EFFECT_TRADE_SKILL = 47,
|
||||
|
||||
// EffectBasePoints[3] — the base magnitude of each effect, stored as
|
||||
// (value - 1) for the fixed-die spells that back item stats (the
|
||||
@@ -10954,6 +11199,15 @@ enum Offsets {
|
||||
// SetHyperlink's `enchant:` on this bit, and its UnitCastingInfo reports
|
||||
// it as isTradeskill.
|
||||
SPELL_ATTR_TRADESPELL = 0x20,
|
||||
// Column 3, castUI: the tab a learned spell without SPELL_ATTR_TRADESPELL
|
||||
// is filed under. The learn function (0x005E9C20) puts a TRADESPELL
|
||||
// spell in its skill line's list (CGPlayer_C::FindSkillLineSpells) and
|
||||
// any other spell with castUI > 0 in tab array castUI
|
||||
// (CGPlayer_C::GetTabSpells, `mov esi,[edi+0xC]` at 0x005E9D83), which
|
||||
// the Craft window of that type lists. Extracted Spell.dbc: 187 of
|
||||
// Enchanting's 192 SkillLineAbility spells hold 3, 138 of Beast
|
||||
// Training's 140 hold 1.
|
||||
OFF_SPELL_RECORD_CAST_UI = 0x0C,
|
||||
// Attributes bits 2 (0x4) and 10 (0x400) — "on next melee swing" (the
|
||||
// ability replaces the next white hit instead of sending its own attack,
|
||||
// e.g. Heroic Strike / Maul / Raptor Strike). The CLIENT itself tests
|
||||
@@ -11321,14 +11575,6 @@ enum Offsets {
|
||||
FUN_GREETING_SELECT_AVAILABLE_QUEST = 0x005012A0,
|
||||
FUN_GREETING_SELECT_ACTIVE_QUEST = 0x00501320,
|
||||
|
||||
// CDataStore primitive readers — `__thiscall void(this, void *out)`.
|
||||
// Read at the current cursor (`+0x14`) and advance it by the value
|
||||
// size. We call these directly from `Unit::MirrorTimer` to peek
|
||||
// packet contents before the SMSG handler consumes them, then
|
||||
// restore the cursor so the original handler reads the same bytes.
|
||||
FUN_DATASTORE_READ_U32 = 0x00418E30,
|
||||
FUN_DATASTORE_READ_U8 = 0x00418CB0,
|
||||
OFF_DATASTORE_CURSOR = 0x14,
|
||||
// CDataStore writers and the stack-packet shape the DBCache query send
|
||||
// builds (FUN_DBCACHE_ITEMSTATS_GET_RECORD, 0x0055BB18..0x0055BB89):
|
||||
// {vtable, buffer, base, alloc, size, read} with buffer/base/alloc/size 0
|
||||
@@ -11369,28 +11615,24 @@ enum Offsets {
|
||||
// selection flag at `0x00884C80`.
|
||||
FUN_ENGINE_TICK_MS = 0x0042B750,
|
||||
|
||||
// Mirror timer (BREATH / FATIGUE-style) SMSG handler. Dispatches
|
||||
// opcodes `0x1D9` (START), `0x1DA` (PAUSE), `0x1DB` (STOP) — but
|
||||
// the vanilla engine doesn't cache any timer state, just reads
|
||||
// the packet and immediately fires the corresponding
|
||||
// MIRROR_TIMER_* Lua event. Our hook (`Unit::MirrorTimer`) peeks
|
||||
// the packet via cursor save/restore on the CDataStore and
|
||||
// builds a side cache that `GetMirrorTimerInfo` /
|
||||
// `GetMirrorTimerProgress` can read back.
|
||||
// Mirror timers (EXHAUSTION = deep-water fatigue, BREATH, FEIGNDEATH).
|
||||
// The engine's handler for all three SMSGs is `0x005E7990`,
|
||||
// `uint32_t __fastcall(param, opcode, time, CDataStore *)`; it only
|
||||
// reads each packet and signals MIRROR_TIMER_START / _PAUSE / _STOP.
|
||||
// START is `u32 type, i32 value, i32 max, i32 scale, u8 paused, u32
|
||||
// spell`; STOP is `u32 type`. `Unit::MirrorTimer` reads both through
|
||||
// `Net::PacketDispatch` (verified: Ghidra + benilla-re
|
||||
// objectlayer/playermessages#mirror-timers).
|
||||
//
|
||||
// __fastcall void(?, int opcode, ?, void *dataStore)
|
||||
//
|
||||
// The helper at `FUN_MIRROR_TIMER_TYPE_NAME` returns the
|
||||
// engine's static type-name string for IDs 0..2 (`"EXHAUSTION"`,
|
||||
// `"BREATH"`, `"FEIGNDEATH"`); `FUN_MIRROR_TIMER_LABEL` builds
|
||||
// the localized display label by looking up `{TYPE}_LABEL` in
|
||||
// the engine's localized strings, or substituting the spell
|
||||
// name when the type is FEIGNDEATH and a spellID is non-zero.
|
||||
FUN_SMSG_MIRROR_TIMER_HANDLER = 0x005E7990,
|
||||
// `FUN_MIRROR_TIMER_TYPE_NAME` is `const char *__fastcall(uint32_t
|
||||
// type)`: "EXHAUSTION" / "BREATH" / "FEIGNDEATH" for 0..2, "UNKNOWN"
|
||||
// otherwise. `FUN_MIRROR_TIMER_LABEL` is `const char *__fastcall(
|
||||
// uint32_t type, uint32_t spell)`: the spell's localized name when the
|
||||
// spell exists, else the global string `<NAME>_LABEL` (FrameScript_GetText,
|
||||
// "" when missing).
|
||||
FUN_MIRROR_TIMER_TYPE_NAME = 0x005E7AE0,
|
||||
FUN_MIRROR_TIMER_LABEL = 0x005E7B10,
|
||||
SMSG_OPCODE_MIRROR_TIMER_START = 0x1D9,
|
||||
SMSG_OPCODE_MIRROR_TIMER_PAUSE = 0x1DA,
|
||||
SMSG_OPCODE_MIRROR_TIMER_STOP = 0x1DB,
|
||||
MIRROR_TIMER_SLOT_COUNT = 3,
|
||||
|
||||
@@ -12736,6 +12978,18 @@ enum Offsets {
|
||||
CALL_TEXT_FIT_HEIGHT_STEP = 0x005C22B1,
|
||||
CALL_TEXT_ENUMERATE_LINES_STEP = 0x005C24F9,
|
||||
DRAW_BUILDER_FRAME_LINE_X = 0x48,
|
||||
// The fit-to-height count's line test (0x005C21C0, after its stepper
|
||||
// call): `fld [ebp+10h]` (the block height), then this `fadd dword ptr
|
||||
// [VAR_TEXT_FIT_HEIGHT_EPSILON]` (D8 05 BC 26 80 00), then FCOMPP against
|
||||
// the running height + font height; it stops at the first line that
|
||||
// fails. [ebp-8] is the bytes fitted so far, 0 until a line fits. EAX is
|
||||
// dead here (FNSTSW AX, then both paths reload it from [ebp-8]).
|
||||
// Text::FitHeight replaces the fadd.
|
||||
SITE_TEXT_FIT_HEIGHT_TOLERANCE = 0x005C22CB,
|
||||
TEXT_FIT_HEIGHT_FRAME_FITTED_BYTES = 0x8,
|
||||
// Exactly 2^-20 (0x35800000, .rdata): the fit's line margin above. A
|
||||
// shared float, also read by collision, movement and matrix code.
|
||||
VAR_TEXT_FIT_HEIGHT_EPSILON = 0x008026BC,
|
||||
// The draw builder's right / centre x: `FUN_TEXT_SCREEN_TO_PIXEL_WIDTH(raw,
|
||||
// -width)` (or -width / 2), `float __fastcall(int raw, float)`, `ret 4`,
|
||||
// result in ST0, stored to the line x at 0x005CDE28.
|
||||
@@ -12853,6 +13107,15 @@ enum Offsets {
|
||||
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT = 0x33C,
|
||||
OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES = 0x340,
|
||||
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS = 0x344, // the newest record; -1 when empty
|
||||
// The record the lines are laid out from (4.3.4's m_currentScroll);
|
||||
// FUN_SMF_DISPLAY_REFRESH stores its argument here.
|
||||
OFF_SCROLLINGMESSAGEFRAME_SCROLL_RECORD = 0x348,
|
||||
// A ring that is not full holds records 0..count-1: the writer fills
|
||||
// them in order from an empty ring (Clear: position -1), and only a full
|
||||
// ring wraps. ScrollOffset and ScrollToOffset (below) wrap by the count,
|
||||
// not the limit, and ScrollToTop (0x00788690) lays out from record 0
|
||||
// unless the ring is full, so each is right only for that layout. A
|
||||
// ClassicAPI writer must leave the ring in it.
|
||||
OFF_SCROLLINGMESSAGEFRAME_MESSAGES = 0x394, // the record array
|
||||
SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE = 0x18,
|
||||
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT = 0x0,
|
||||
@@ -12888,6 +13151,19 @@ enum Offsets {
|
||||
// 0 when empty; the GetCurrentScroll method pushes it.
|
||||
FUN_SCROLLINGMESSAGEFRAME_SCROLL_TO_OFFSET = 0x00788700,
|
||||
FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET = 0x00788AC0,
|
||||
// ScrollUp / ScrollDown (`int __thiscall(smf)`, plain `ret`; 4.3.4's
|
||||
// 0x0051E620 / 0x0051E660 step for step): one record older / newer and a
|
||||
// layout, answering 1. Each answers 0 without moving when empty or at its
|
||||
// end, and re-arms the shown lines there: ScrollDown at
|
||||
// OFF_SCROLLINGMESSAGEFRAME_AT_BOTTOM, ScrollUp at +0x350 (4.3.4's
|
||||
// m_atTop), which the layout sets when every message from the scroll
|
||||
// record back to the oldest fit the height (0x00788A37: laid-out count ==
|
||||
// count remaining). The layout runs only on a shown frame ([smf+0xD4],
|
||||
// m_visible), so a hidden frame's +0x350 is stale and its ScrollUp wraps
|
||||
// around the ring. PageUp (0x007885B0) is a loop of ScrollUp that ends on
|
||||
// the first 0.
|
||||
FUN_SCROLLINGMESSAGEFRAME_SCROLL_UP = 0x00788610,
|
||||
FUN_SCROLLINGMESSAGEFRAME_SCROLL_DOWN = 0x00788650,
|
||||
// Clear (`void __thiscall(smf)`, 4.3.4's Clear): count 0, position 0, a
|
||||
// layout from 0, +0x350 = 0, position -1. The Clear method's body.
|
||||
FUN_SCROLLINGMESSAGEFRAME_CLEAR = 0x007882B0,
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
//
|
||||
// How:
|
||||
//
|
||||
// 1. The CMake build embeds every file of every listed addon as an
|
||||
// RCDATA resource, plus a generated `{path, resourceId}` manifest
|
||||
// (`embedded_resources.h`).
|
||||
// 1. The CMake build embeds each listed addon as one RCDATA resource,
|
||||
// a gzipped pack of its files, plus a generated
|
||||
// `{path, resourceId, index}` manifest (`embedded_resources.h`).
|
||||
//
|
||||
// 2. We hook the engine's file-read function at `FUN_FILE_READ`.
|
||||
// When the engine asks for a path under
|
||||
@@ -153,7 +153,8 @@ int MatchAddonPath(const char *path, const char **relPath) {
|
||||
Embed::Bytes LookupEmbedded(const char *relPath) {
|
||||
for (size_t i = 0; i < Manifest::kFileCount; ++i) {
|
||||
if (PathEqualsCI(relPath, Manifest::kFiles[i].path))
|
||||
return Embed::Resource(Manifest::kFiles[i].resourceId);
|
||||
return Embed::File(Manifest::kFiles[i].resourceId,
|
||||
Manifest::kFiles[i].index);
|
||||
}
|
||||
return {nullptr, 0};
|
||||
}
|
||||
|
||||
@@ -162,8 +162,20 @@ uint32_t VertexColor(void *region) {
|
||||
return bgra;
|
||||
}
|
||||
|
||||
// An unchanged alpha writes nothing. The setter already does nothing when the
|
||||
// one entry it would store is the region's colour, but the getter reads a
|
||||
// gradient back as white, and the setter shrinks a gradient's four entries to
|
||||
// that one (benilla-re ui/texture, "Setting a vertex colour"). So restoring
|
||||
// the stored alpha (PreOnAnimUpdate, AnimDeactivated) on a gradient under an
|
||||
// ancestor's alpha group flattened it to white. 3.3.5's and 4.3.4's
|
||||
// SetVertexColor drop the gradient there too; here only a region that an
|
||||
// animation itself fades loses it. The ancestor's fade still reaches the
|
||||
// gradient: a frame's alpha change refolds every region's entries (0x77FAC0).
|
||||
void SetVertexAlpha(void *region, uint8_t alpha) {
|
||||
const uint32_t bgra = (VertexColor(region) & 0x00FFFFFF) | (static_cast<uint32_t>(alpha) << 24);
|
||||
const uint32_t color = VertexColor(region);
|
||||
if ((color >> 24) == alpha)
|
||||
return;
|
||||
const uint32_t bgra = (color & 0x00FFFFFF) | (static_cast<uint32_t>(alpha) << 24);
|
||||
reinterpret_cast<SetColor_t>(Offsets::FUN_FONTSTRING_SET_COLOR)(region, nullptr, &bgra);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
// listed by the coverage function
|
||||
// - a frame method -> Blizzard's widget system for its registry
|
||||
// ("SimpleTextureAPI", Type "ScriptObject")
|
||||
// - an enum / structure /
|
||||
// event -> the System named in its declaration
|
||||
// - an enum / constants /
|
||||
// structure / event -> the System named in its declaration
|
||||
// A system's `Environment` is "All" when its functions were recorded in both
|
||||
// Lua states, else "Game" or "Glue"; a function whose own environment is
|
||||
// narrower than its system's gets a Documentation line saying so.
|
||||
@@ -79,13 +79,20 @@ enum Kind : uint8_t {
|
||||
KIND_TABLE, // RegisterTableFunction
|
||||
KIND_FRAME_METHOD, // RegisterFrameMethods, one per entry
|
||||
KIND_ENUM, // RegisterIntegerEnum
|
||||
KIND_CONSTANTS, // RegisterConstants
|
||||
KIND_ORPHAN_METHOD_DOC // a Doc::Method naming no entry in its table
|
||||
};
|
||||
|
||||
// The value-table kinds, which share the enum* fields of Entry.
|
||||
bool IsValueTable(Kind kind) {
|
||||
return kind == KIND_ENUM || kind == KIND_CONSTANTS;
|
||||
}
|
||||
|
||||
struct Entry {
|
||||
Kind kind;
|
||||
unsigned env; // Env bits the registration was recorded in
|
||||
const char *table; // KIND_TABLE: the namespace; KIND_ENUM: the parent ("Enum")
|
||||
const char *table; // KIND_TABLE: the namespace; KIND_ENUM: the parent ("Enum");
|
||||
// KIND_CONSTANTS: "Constants"
|
||||
const char *name; // registered name / method name / enum sub-name
|
||||
const void *registry; // frame methods only
|
||||
const Function *doc; // nullptr = undocumented
|
||||
@@ -195,6 +202,7 @@ const char *SystemOf(const Entry &e) {
|
||||
case KIND_FRAME_METHOD:
|
||||
return WidgetSystemFor(e.registry);
|
||||
case KIND_ENUM:
|
||||
case KIND_CONSTANTS:
|
||||
return e.enumSystem;
|
||||
default:
|
||||
return nullptr;
|
||||
@@ -240,6 +248,7 @@ std::vector<SystemInfo> CollectSystems() {
|
||||
AddSystem(systems, SystemOf(e), nullptr, "ScriptObject", e.env);
|
||||
break;
|
||||
case KIND_ENUM:
|
||||
case KIND_CONSTANTS:
|
||||
AddSystem(systems, e.enumSystem, nullptr, nullptr, 0);
|
||||
break;
|
||||
default:
|
||||
@@ -394,6 +403,25 @@ void PushEnumeration(void *L, const Entry &e) {
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// Pushes { Name, Type = "Constants",
|
||||
// Values = { { Name, Type = "number", Value } } }.
|
||||
void PushConstants(void *L, const Entry &e) {
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", e.name);
|
||||
Lua::SetFieldString(L, "Type", "Constants");
|
||||
Lua::PushString(L, "Values");
|
||||
Lua::NewTable(L);
|
||||
for (int i = 0; i < e.enumCount; ++i) {
|
||||
Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", e.enumEntries[i].key);
|
||||
Lua::SetFieldString(L, "Type", "number");
|
||||
Lua::SetFieldNumber(L, "Value", e.enumEntries[i].value);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// Pushes { Name, Type = "Structure" [, Documentation], Fields = { … } }.
|
||||
void PushStructure(void *L, const Structure &s) {
|
||||
Lua::NewTable(L);
|
||||
@@ -479,10 +507,13 @@ void PushSystem(void *L, const SystemInfo &sys) {
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
for (const Entry &e : g_entries) {
|
||||
if (e.kind != KIND_ENUM || !SameTextNoCase(e.enumSystem, sys.name))
|
||||
if (!IsValueTable(e.kind) || !SameTextNoCase(e.enumSystem, sys.name))
|
||||
continue;
|
||||
Lua::PushNumber(L, static_cast<double>(++n));
|
||||
PushEnumeration(L, e);
|
||||
if (e.kind == KIND_ENUM)
|
||||
PushEnumeration(L, e);
|
||||
else
|
||||
PushConstants(L, e);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
@@ -527,6 +558,7 @@ std::vector<std::string> Undocumented() {
|
||||
break;
|
||||
}
|
||||
case KIND_ENUM:
|
||||
case KIND_CONSTANTS:
|
||||
if (e.enumSystem == nullptr)
|
||||
out.push_back(std::string(e.table) + "." + e.name);
|
||||
break;
|
||||
@@ -662,18 +694,21 @@ void NameWidgetSystem(const void *registry, const char *system) {
|
||||
g_ownWidgetSystems.push_back({reinterpret_cast<uintptr_t>(registry), system});
|
||||
}
|
||||
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system) {
|
||||
namespace {
|
||||
|
||||
void RecordValueTable(Kind kind, const char *parent, const char *sub,
|
||||
const Game::Lua::EnumIntegerEntry *entries, int count,
|
||||
const char *system) {
|
||||
if (g_pass == ENV_NONE || sub == nullptr)
|
||||
return;
|
||||
if (Entry *e = Find(KIND_ENUM, parent, sub, nullptr)) {
|
||||
if (Entry *e = Find(kind, parent, sub, nullptr)) {
|
||||
e->env |= g_pass;
|
||||
if (e->enumSystem == nullptr)
|
||||
e->enumSystem = system;
|
||||
return;
|
||||
}
|
||||
Entry e{};
|
||||
e.kind = KIND_ENUM;
|
||||
e.kind = kind;
|
||||
e.env = g_pass;
|
||||
e.table = parent;
|
||||
e.name = sub;
|
||||
@@ -683,6 +718,18 @@ void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntege
|
||||
g_entries.push_back(e);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system) {
|
||||
RecordValueTable(KIND_ENUM, parent, sub, entries, count, system);
|
||||
}
|
||||
|
||||
void RecordConstants(const char *sub, const Game::Lua::EnumIntegerEntry *entries, int count,
|
||||
const char *system) {
|
||||
RecordValueTable(KIND_CONSTANTS, "Constants", sub, entries, count, system);
|
||||
}
|
||||
|
||||
} // namespace Api::Documentation
|
||||
|
||||
// The Structure constructor lives here so the list head is private to this
|
||||
|
||||
@@ -39,5 +39,7 @@ void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry
|
||||
void NameWidgetSystem(const void *registry, const char *system);
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system);
|
||||
void RecordConstants(const char *sub, const Game::Lua::EnumIntegerEntry *entries, int count,
|
||||
const char *system);
|
||||
|
||||
} // namespace Api::Documentation
|
||||
|
||||
@@ -19,8 +19,9 @@
|
||||
// the `BAG_UPDATE_DELAYED` event. Subscribe here to run C++ work exactly once
|
||||
// per frame in which any bag actually changed, instead of polling every frame.
|
||||
//
|
||||
// Callbacks fire at the tail of the world tick, after every BAG_UPDATE for the
|
||||
// frame has already fired (so bag contents are settled). No firing on frames
|
||||
// Callbacks fire from the world tick's subscription, after the frame's UI draw
|
||||
// and after every BAG_UPDATE for the frame has already fired (so bag contents
|
||||
// are settled). No firing on frames
|
||||
// where nothing changed. Order across subscribers is unspecified.
|
||||
//
|
||||
// Module pattern, matching `Tick::WorldTick::AutoSubscribe`: declare a
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `securecall(func or name, ...)`, `securecallfunction(func, ...)` and
|
||||
// `securecall(func or name, ...)`, `securecallfunction(func, ...)`,
|
||||
// `securecallmethod(object, method, ...)` and
|
||||
// `secureexecuterange(tbl, func, ...)` — the later clients' protected calls,
|
||||
// less taint (1.12 has none). Retail, probed:
|
||||
// - securecall returns the function's results; a string names a global
|
||||
@@ -22,6 +23,11 @@
|
||||
// - secureexecuterange calls func(key, value, ...) for each entry of tbl in
|
||||
// `next` order; an error in one goes to the error handler and the walk
|
||||
// goes on. It returns nothing.
|
||||
// securecallmethod is from retail's API documentation, not a probe: it calls
|
||||
// object[method](object, ...) with a raw lookup (no metatable), and an error
|
||||
// in the lookup or the call goes to the error handler with no results. Here
|
||||
// a lookup that finds no function, or an object that is not a table, errors
|
||||
// the way calling that value does.
|
||||
// The error handler is the one every engine script run passes
|
||||
// (VAR_FRAMESCRIPT_ERROR_HANDLER_REF), as HookScript's wrapper uses.
|
||||
//
|
||||
@@ -70,6 +76,20 @@ int __fastcall Script_securecall(void *L) {
|
||||
|
||||
int __fastcall Script_securecallfunction(void *L) { return CallProtected(L); }
|
||||
|
||||
int __fastcall Script_securecallmethod(void *L) {
|
||||
if (Game::Lua::GetTop(L) < 2)
|
||||
Game::Lua::SetTop(L, 2);
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::PushValue(L, 2);
|
||||
Game::Lua::RawGet(L, 1);
|
||||
} else {
|
||||
Game::Lua::PushNil(L);
|
||||
}
|
||||
Game::Lua::Remove(L, 2); // [object, args..., func]
|
||||
Game::Lua::Insert(L, 1); // [func, object, args...]
|
||||
return CallProtected(L);
|
||||
}
|
||||
|
||||
int __fastcall Script_secureexecuterange(void *L) {
|
||||
const int n = Game::Lua::GetTop(L); // tbl, func, extra args 3..n
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE)
|
||||
@@ -97,6 +117,11 @@ const Game::Doc::Field kCallArgs[] = {
|
||||
const Game::Doc::Field kCallRets[] = {
|
||||
Game::Doc::Opt("...", "any", nullptr, "The function's results, or nothing after an error."),
|
||||
};
|
||||
const Game::Doc::Field kMethodArgs[] = {
|
||||
Game::Doc::Req("object", "table", "The table that holds the method. The lookup ignores its metatable."),
|
||||
Game::Doc::Req("method", "string", "The name of the method."),
|
||||
Game::Doc::Opt("...", "any", nullptr, "The arguments after object."),
|
||||
};
|
||||
const Game::Doc::Field kRangeArgs[] = {
|
||||
Game::Doc::Req("tbl", "table", "The table whose entries are visited."),
|
||||
Game::Doc::Req("func", "function", "Called as func(key, value, ...) for each entry."),
|
||||
@@ -108,6 +133,9 @@ const Game::Doc::Function kSecureCall{
|
||||
const Game::Doc::Function kSecureCallFunction{
|
||||
"Calls a function and returns its results. An error goes to the error handler and the call returns nothing.",
|
||||
kCallArgs, kCallRets, "LuaGlobals"};
|
||||
const Game::Doc::Function kSecureCallMethod{
|
||||
"Calls object[method](object, ...) and returns its results. An error goes to the error handler and the call returns nothing.",
|
||||
kMethodArgs, kCallRets, "LuaGlobals"};
|
||||
const Game::Doc::Function kSecureExecuteRange{
|
||||
"Calls a function for each entry of a table. An error goes to the error handler and the other entries still run.",
|
||||
kRangeArgs, {}, "LuaGlobals"};
|
||||
@@ -115,12 +143,14 @@ const Game::Doc::Function kSecureExecuteRange{
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("securecall", &Script_securecall, &kSecureCall);
|
||||
Game::Lua::RegisterGlobalFunction("securecallfunction", &Script_securecallfunction, &kSecureCallFunction);
|
||||
Game::Lua::RegisterGlobalFunction("securecallmethod", &Script_securecallmethod, &kSecureCallMethod);
|
||||
Game::Lua::RegisterGlobalFunction("secureexecuterange", &Script_secureexecuterange, &kSecureExecuteRange);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("securecall", &Script_securecall, &kSecureCall);
|
||||
Game::Lua::RegisterGlueFunction("securecallfunction", &Script_securecallfunction, &kSecureCallFunction);
|
||||
Game::Lua::RegisterGlueFunction("securecallmethod", &Script_securecallmethod, &kSecureCallMethod);
|
||||
Game::Lua::RegisterGlueFunction("secureexecuterange", &Script_secureexecuterange, &kSecureExecuteRange);
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ bool ExtractHeaderAttr(const char *tagStart, const char *tagEnd,
|
||||
|
||||
EmbeddedConfig ResolveEmbeddedConfig() {
|
||||
EmbeddedConfig out = {};
|
||||
const Embed::Bytes xml = Embed::Resource(Embed::Ids::kBindingsXml);
|
||||
const Embed::Bytes xml = Embed::File(Embed::Ids::kBindingsXml, 0);
|
||||
if (xml.data == nullptr) return out;
|
||||
const char *content = reinterpret_cast<const char *>(xml.data);
|
||||
const char *end = content + xml.size;
|
||||
|
||||
+17
-53
@@ -25,7 +25,8 @@
|
||||
// - a cast-time spell is cast -> SMSG_SPELL_GO, InterruptFlags & AUTOATTACK
|
||||
// - autorepeat (Auto Shot / Shoot) fires -> SMSG_SPELL_GO, AttributesEx2 & AUTOREPEAT
|
||||
// - starting a fresh melee attack -> SMSG_ATTACKSTART (off-hand only)
|
||||
// - a weapon is swapped mid-combat -> no packet; polled off the invMgr GUIDs
|
||||
// - a weapon is swapped mid-combat -> no packet; the engine's inventory-slot
|
||||
// observer (via Player::Equipment)
|
||||
// - the LOCAL PLAYER parries an attack -> SMSG_ATTACKERSTATEUPDATE, victim == us,
|
||||
// targetState == PARRY (no separate packet —
|
||||
// the server just clips OUR remaining time)
|
||||
@@ -431,64 +432,29 @@ void OnPacket(uint32_t opcode, CDataStore *packet) {
|
||||
|
||||
const Net::PacketDispatch::AutoSubscribe _sub{&OnPacket};
|
||||
|
||||
// ---- Weapon swap mid-combat (polled -- no packet, no observer needed) ---
|
||||
} // namespace
|
||||
|
||||
// ---- Weapon swap mid-combat (inventory-slot observer) -------------------
|
||||
//
|
||||
// A weapon equipped while `IsInCombat()` resets that hand's timer
|
||||
// server-side (`Player::_ApplyWeaponDependentAuraMods` -> `SetAttackTime`
|
||||
// with `resetTimer = IsInCombat()`). There's no dedicated packet for this;
|
||||
// `Player::Equipment` already sees the same invMgr GUIDs change via the
|
||||
// engine's inventory-slot observer for `PLAYER_EQUIPMENT_CHANGED`, but
|
||||
// that's a one-way Lua event fire with nothing to subscribe to in C++. Only
|
||||
// 3 slots matter here, so a plain 3-GUID compare on the existing WorldTick
|
||||
// pass is simpler than a second hook on the same observer.
|
||||
|
||||
uint64_t g_lastMainGuid = 0;
|
||||
uint64_t g_lastOffGuid = 0;
|
||||
uint64_t g_lastRangedGuid = 0;
|
||||
bool g_weaponSnapValid = false;
|
||||
|
||||
void PollWeaponSwap() {
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr) {
|
||||
g_weaponSnapValid = false;
|
||||
return;
|
||||
}
|
||||
const uint8_t *invMgr = player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
|
||||
const uint32_t count =
|
||||
*reinterpret_cast<const uint32_t *>(invMgr + Offsets::OFF_INVMGR_SLOT_COUNT);
|
||||
if (count < 18) {
|
||||
g_weaponSnapValid = false;
|
||||
return;
|
||||
}
|
||||
const uint64_t *guids = *reinterpret_cast<const uint64_t *const *>(
|
||||
invMgr + Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
if (guids == nullptr) {
|
||||
g_weaponSnapValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// `Player::Equipment`'s co-hook on the engine's inventory-slot observer calls
|
||||
// in here when a slot's GUID changes. That runs inside SMSG_UPDATE_OBJECT
|
||||
// after the whole values block is written, so `AttackTimeMs` already reads
|
||||
// the new weapon's speed when it arrives in the same block.
|
||||
void OnWeaponSlotChanged(int slot0) {
|
||||
// 0-based invMgr slots: 15 = main hand (Lua 16), 16 = off hand (17),
|
||||
// 17 = ranged (18) -- same `slot0 = luaSlot - 1` convention documented
|
||||
// in Player::Equipment.
|
||||
const uint64_t mainGuid = guids[15];
|
||||
const uint64_t offGuid = guids[16];
|
||||
const uint64_t rangedGuid = guids[17];
|
||||
|
||||
if (g_weaponSnapValid && PlayerInCombat()) {
|
||||
if (mainGuid != g_lastMainGuid)
|
||||
MarkReset(MAIN_HAND, AttackTimeMs(MAIN_HAND));
|
||||
if (offGuid != g_lastOffGuid)
|
||||
MarkReset(OFF_HAND, AttackTimeMs(OFF_HAND));
|
||||
if (rangedGuid != g_lastRangedGuid)
|
||||
MarkReset(RANGED, AttackTimeMs(RANGED));
|
||||
}
|
||||
|
||||
g_lastMainGuid = mainGuid;
|
||||
g_lastOffGuid = offGuid;
|
||||
g_lastRangedGuid = rangedGuid;
|
||||
g_weaponSnapValid = true;
|
||||
// in Player::Equipment, and in SwingType order.
|
||||
if (slot0 < 15 || slot0 > 17 || !PlayerInCombat())
|
||||
return;
|
||||
const auto type = static_cast<SwingType>(slot0 - 15);
|
||||
MarkReset(type, AttackTimeMs(type));
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// ---- PLAYER_SWING fire (WorldTick, at most once per type per frame) -----
|
||||
|
||||
const Game::Doc::Field kPayload[] = {
|
||||
@@ -504,8 +470,6 @@ const Game::Doc::Event kPlayerSwingDoc{
|
||||
const Event::Custom::AutoReserve _evt{"PLAYER_SWING", &kPlayerSwingDoc};
|
||||
|
||||
void OnWorldTick() {
|
||||
PollWeaponSwap();
|
||||
|
||||
const int slot = _evt.Slot();
|
||||
const uint32_t now = NowMs();
|
||||
for (int t = 0; t < 3; ++t) {
|
||||
|
||||
@@ -32,6 +32,12 @@ enum SwingType : int { MAIN_HAND = 0, OFF_HAND = 1, RANGED = 2 };
|
||||
// callers treat 0 as "this swing type doesn't apply right now".
|
||||
uint32_t AttackTimeMs(SwingType type);
|
||||
|
||||
// Called by `Player::Equipment` from the engine's inventory-slot observer
|
||||
// when the item GUID in 0-based equipment slot `slot0` changes. A weapon
|
||||
// slot (15 / 16 / 17) changed in combat resets that hand's swing timer; any
|
||||
// other slot is ignored.
|
||||
void OnWeaponSlotChanged(int slot0);
|
||||
|
||||
// True if `player` and `target` (raw CGUnit* object pointers, e.g. from
|
||||
// `Unit::Position::ResolveToken` or `Object::ByGuid`) are within pure 2D
|
||||
// (X/Y) melee reach of each other -- the server's own auto-attack distance
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `Enum.BagIndex` — retail's BagIndexConstants enumeration, with Classic
|
||||
// Era's names for the containers this client has and this client's own
|
||||
// indices for them. Era numbers its bank bags from 6 because its reagent bag
|
||||
// takes 5; here they start right after the equipped bags, as the stock bank
|
||||
// frame numbers them. The reagent bag, reagent bank and account bank tabs
|
||||
// have no entry.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Container::BagIndex {
|
||||
|
||||
namespace {
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kEntries[] = {
|
||||
{"Keyring", Offsets::BAG_INDEX_KEYRING},
|
||||
{"Bank", Offsets::BAG_INDEX_BANK},
|
||||
{"Backpack", Offsets::BAG_INDEX_BACKPACK},
|
||||
{"Bag_1", Offsets::BAG_INDEX_BAG_FIRST},
|
||||
{"Bag_2", Offsets::BAG_INDEX_BAG_FIRST + 1},
|
||||
{"Bag_3", Offsets::BAG_INDEX_BAG_FIRST + 2},
|
||||
{"Bag_4", Offsets::BAG_INDEX_BAG_LAST},
|
||||
{"BankBag_1", Offsets::BAG_INDEX_BANK_BAG_FIRST},
|
||||
{"BankBag_2", Offsets::BAG_INDEX_BANK_BAG_FIRST + 1},
|
||||
{"BankBag_3", Offsets::BAG_INDEX_BANK_BAG_FIRST + 2},
|
||||
{"BankBag_4", Offsets::BAG_INDEX_BANK_BAG_FIRST + 3},
|
||||
{"BankBag_5", Offsets::BAG_INDEX_BANK_BAG_FIRST + 4},
|
||||
{"BankBag_6", Offsets::BAG_INDEX_BANK_BAG_LAST},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "BagIndex", kEntries,
|
||||
sizeof(kEntries) / sizeof(kEntries[0]), "BagIndexConstants");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Container::BagIndex
|
||||
@@ -208,13 +208,6 @@ bool IsItemFiltered(const uint8_t *cgItem) {
|
||||
|
||||
namespace {
|
||||
|
||||
// Lua's BACKPACK_CONTAINER, BANK_CONTAINER, KEYRING_CONTAINER and the last
|
||||
// bank bag (NUM_BAG_SLOTS + NUM_BANKBAGSLOTS).
|
||||
constexpr int kBackpack = 0;
|
||||
constexpr int kBank = -1;
|
||||
constexpr int kKeyring = -2;
|
||||
constexpr int kLastBankBag = 10;
|
||||
|
||||
const Game::Doc::Event kSearchUpdateDoc{
|
||||
"Container", "Fires when the bag search text changes.", {}};
|
||||
const Event::Custom::AutoReserve _evt{"INVENTORY_SEARCH_UPDATE", &kSearchUpdateDoc};
|
||||
@@ -225,11 +218,12 @@ const Event::Custom::AutoReserve _evt{"INVENTORY_SEARCH_UPDATE", &kSearchUpdateD
|
||||
// get their fixed sizes. The keyring is the inventory's last range, so the
|
||||
// whole-inventory bound is its own. Any other index is no container.
|
||||
int SlotsToWalk(int bagID) {
|
||||
if (bagID == kBackpack)
|
||||
if (bagID == Offsets::BAG_INDEX_BACKPACK)
|
||||
return Offsets::BACKPACK_NUM_SLOTS;
|
||||
if (bagID == kBank)
|
||||
if (bagID == Offsets::BAG_INDEX_BANK)
|
||||
return Offsets::INVMGR_BANK_MAIN_LAST_SLOT - Offsets::INVMGR_BANK_MAIN_FIRST_SLOT + 1;
|
||||
if (bagID == kKeyring || (bagID >= 1 && bagID <= kLastBankBag))
|
||||
if (bagID == Offsets::BAG_INDEX_KEYRING ||
|
||||
(bagID >= Offsets::BAG_INDEX_BAG_FIRST && bagID <= Offsets::BAG_INDEX_BANK_BAG_LAST))
|
||||
return INT_MAX;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
// reproduction of Blizzard's: hearthstone, gear, consumables, reagents,
|
||||
// trade goods, quest items, everything else by quality, junk last. Within
|
||||
// a category, by item class, subclass, name, then fuller stacks first.
|
||||
// Gear adds two keys so like pieces sit together: the equip slot right
|
||||
// after the class (kSlotOrder), and quality, best first, before the name.
|
||||
// Gear adds two keys so like pieces sit together: the equip slot ahead of
|
||||
// the class (kSlotOrder), and quality, best first, before the name.
|
||||
//
|
||||
// An item whose data has NOT arrived, or that is locked (on the cursor,
|
||||
// in the trade window, mid-move), is pinned: left where it is, with its
|
||||
@@ -172,7 +172,10 @@ uint8_t CategoryFor(uint32_t itemID, uint32_t itemClass, uint32_t quality) {
|
||||
// shoulder. Not the equipment slot either, which lumps shields, off-hand
|
||||
// weapons and held items into one. The order is Baganator's (its
|
||||
// Sorting/ItemFields.lua): weapons by hand, then ranged and ammo, then
|
||||
// armor head to foot, then jewelry, shirt and tabard.
|
||||
// armor head to foot, then jewelry, shirt and tabard. Unlike Baganator,
|
||||
// this key runs before the item class: shields, held-in-off-hand items
|
||||
// and relics are armor class, and ranking class first would pull them
|
||||
// out of the hand group to the head of the armor block.
|
||||
constexpr uint8_t kSlotOrder[] = {
|
||||
17, 13, 21, 14, 23, 26, 22, 15, 25, 24, 27, 28, // weapons, ranged, relic
|
||||
1, 3, 16, 5, 20, 9, 10, 6, 7, 8, // head .. feet
|
||||
@@ -238,11 +241,11 @@ int CompareNames(const char *a, const char *b) {
|
||||
bool Precedes(const Entry &a, const Entry &b) {
|
||||
if (a.category != b.category)
|
||||
return a.category < b.category;
|
||||
if (a.itemClass != b.itemClass)
|
||||
return a.itemClass < b.itemClass;
|
||||
const bool gear = a.category == kCatGear;
|
||||
if (gear && a.slotRank != b.slotRank)
|
||||
return a.slotRank < b.slotRank;
|
||||
if (a.itemClass != b.itemClass)
|
||||
return a.itemClass < b.itemClass;
|
||||
if (a.subClass != b.subClass)
|
||||
return a.subClass < b.subClass;
|
||||
if (gear && a.quality != b.quality)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `Constants.MoneyFormattingConstants`, retail's MoneyConstants system
|
||||
// (Blizzard_APIDocumentationGenerated/MoneyConstantsDocumentation.lua):
|
||||
// GOLD_REWARD_THRESHOLD_TO_HIDE_COPPER = 10, the gold amount above which
|
||||
// FormattingUtil's GetMoneyString(..., checkGoldThreshold) drops the copper.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace Currency::MoneyConstants {
|
||||
|
||||
namespace {
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kMoneyFormattingConstants[] = {
|
||||
{"GOLD_REWARD_THRESHOLD_TO_HIDE_COPPER", 10},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterConstants("MoneyFormattingConstants", kMoneyFormattingConstants,
|
||||
sizeof(kMoneyFormattingConstants) /
|
||||
sizeof(kMoneyFormattingConstants[0]),
|
||||
"MoneyConstants");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Currency::MoneyConstants
|
||||
@@ -22,13 +22,18 @@ Util\SoundKitConstants.lua
|
||||
Util\TableUtil.lua
|
||||
Util\EnumUtil.lua
|
||||
Util\StringUtil.lua
|
||||
Util\TypeUtil.lua
|
||||
Util\MathUtil.lua
|
||||
Util\SortUtil.lua
|
||||
Util\Spline.lua
|
||||
Util\PixelUtil.lua
|
||||
Util\FrameUtil.lua
|
||||
Util\InputUtil.lua
|
||||
Util\FormattingUtil.lua
|
||||
Util\TimeUtil.lua
|
||||
Util\Flags.lua
|
||||
Util\TaggableObject.lua
|
||||
Util\ExportUtil.lua
|
||||
Util\ButtonStateBehavior.lua
|
||||
Util\CallbackRegistry.lua
|
||||
Util\GlobalCallbackRegistry.lua
|
||||
@@ -36,22 +41,38 @@ Util\TemplateInfoCache.lua
|
||||
Util\FrameFactory.lua
|
||||
Util\DataProvider.lua
|
||||
Util\TreeListDataProvider.lua
|
||||
Util\IndexRangeDataProvider.lua
|
||||
Util\CircularBuffer.lua
|
||||
Util\ColorUtil.lua
|
||||
Util\SharedColorConstants.lua
|
||||
Util\ItemLocation.lua
|
||||
Util\PlayerLocation.lua
|
||||
Util\EventUtil.lua
|
||||
Util\ContinuableContainer.lua
|
||||
Util\AsyncCallbackSystem.lua
|
||||
Util\Item.lua
|
||||
Util\Spell.lua
|
||||
Util\ObjectCache.lua
|
||||
Util\SpellBookItemLocation.lua
|
||||
Util\ItemUtil.lua
|
||||
Util\ContainerFrame.lua
|
||||
Util\AddOnCompat.lua
|
||||
Util\FunctionUtil.lua
|
||||
Util\MixinUtil.lua
|
||||
Util\ObjectUpdater.lua
|
||||
Util\CacheUtil.lua
|
||||
Util\Pools.lua
|
||||
Util\SecureTypes.lua
|
||||
Util\ProxyUtil.lua
|
||||
Util\Pools.lua
|
||||
Util\TableBuilder.lua
|
||||
Util\FrameWatcher.lua
|
||||
Util\TimedCallback.lua
|
||||
Util\EasingUtil.lua
|
||||
Util\ScriptAnimationUtil.lua
|
||||
Util\Interpolator.lua
|
||||
Util\Sequencer.lua
|
||||
Util\SmoothStatusBar.lua
|
||||
Util\AnimationSystem.lua
|
||||
Util\AnchorUtil.lua
|
||||
Util\RegionUtil.lua
|
||||
Util\GridLayoutUtil.lua
|
||||
@@ -89,6 +110,8 @@ FrameXML\EventEditBox.lua
|
||||
FrameXML\EventEditBox.xml
|
||||
FrameXML\EventScrollFrame.lua
|
||||
FrameXML\EventScrollFrame.xml
|
||||
FrameXML\CallbackRegistrant.lua
|
||||
FrameXML\CallbackRegistrant.xml
|
||||
FrameXML\Scroll\ScrollUtil.lua
|
||||
FrameXML\Scroll\ScrollController.lua
|
||||
FrameXML\Scroll\ScrollBoxPadding.lua
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
if not _G then
|
||||
_G = getfenv(0)
|
||||
end
|
||||
end
|
||||
|
||||
-- Retail names for what this client already has. Retail's Deprecated files
|
||||
-- alias the old globals to the C_ functions; here the old globals are the
|
||||
-- implementations.
|
||||
C_Container.GetContainerNumSlots = GetContainerNumSlots
|
||||
C_Container.PickupContainerItem = PickupContainerItem
|
||||
|
||||
C_ActionBar = C_ActionBar or {}
|
||||
|
||||
-- Retail answers per game type. This client always has a keyring.
|
||||
function C_ActionBar.ShouldShowKeyring()
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
-- Backport of retail's Blizzard_SharedXMLBase/CallbackRegistrant.lua.
|
||||
|
||||
CallbackRegistrantMixin = {};
|
||||
|
||||
function CallbackRegistrantMixin:OnShow()
|
||||
for i, eventRegistrationInfo in ipairs(self:GetDynamicCallbackRegistrantHandlers()) do
|
||||
self:RegisterFromRegistrationInfo(eventRegistrationInfo);
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:OnHide()
|
||||
self:UnregisterAllInternal(self:GetDynamicCallbackRegistrantHandlers());
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:AddEventMethodInternal(handlersTable, callbackRegistry, event, handlerMethod)
|
||||
local eventRegistrationInfo = self:CreateEventRegistrationInfo(callbackRegistry, event, handlerMethod)
|
||||
table.insert(handlersTable, eventRegistrationInfo);
|
||||
return eventRegistrationInfo;
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:AddDynamicEventMethod(callbackRegistry, event, handlerMethod)
|
||||
local eventRegistrationInfo = self:AddEventMethodInternal(self:GetDynamicCallbackRegistrantHandlers(), callbackRegistry, event, handlerMethod)
|
||||
|
||||
if self:IsShown() then
|
||||
self:RegisterFromRegistrationInfo(eventRegistrationInfo);
|
||||
end
|
||||
end
|
||||
|
||||
-- Routing calls here allows for future expansion in this template (i.e. checking duplicate registrations).
|
||||
function CallbackRegistrantMixin:AddStaticEventMethod(callbackRegistry, event, handlerMethod)
|
||||
local eventRegistrationInfo = self:AddEventMethodInternal(self:GetStaticCallbackRegistrantHandlers(), callbackRegistry, event, handlerMethod)
|
||||
self:RegisterFromRegistrationInfo(eventRegistrationInfo);
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:RemoveStaticEventMethod(callbackRegistry, event, handlerMethod)
|
||||
local handlers = self:GetStaticCallbackRegistrantHandlers();
|
||||
for i, eventRegistrationInfo in ipairs(handlers) do
|
||||
if (eventRegistrationInfo.callbackRegistry == callbackRegistry) and (eventRegistrationInfo.event == event) then
|
||||
self:UnregisterFromRegistrationInfo(eventRegistrationInfo);
|
||||
table.remove(handlers, i);
|
||||
break;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:UnregisterAllEventMethods()
|
||||
self:UnregisterAllInternal(self:GetDynamicCallbackRegistrantHandlers());
|
||||
self:UnregisterAllInternal(self:GetStaticCallbackRegistrantHandlers());
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:CreateEventRegistrationInfo(callbackRegistry, event, handlerMethod)
|
||||
return { callbackRegistry = callbackRegistry, event = event, handlerMethod = handlerMethod, registered = false, };
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:RegisterFromRegistrationInfo(eventRegistrationInfo)
|
||||
if eventRegistrationInfo.registered then
|
||||
return;
|
||||
end
|
||||
|
||||
eventRegistrationInfo.callbackRegistry:RegisterCallback(eventRegistrationInfo.event, eventRegistrationInfo.handlerMethod, self);
|
||||
eventRegistrationInfo.registered = true;
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:UnregisterFromRegistrationInfo(eventRegistrationInfo)
|
||||
if not eventRegistrationInfo.registered then
|
||||
return;
|
||||
end
|
||||
|
||||
eventRegistrationInfo.callbackRegistry:UnregisterCallback(eventRegistrationInfo.event, self);
|
||||
eventRegistrationInfo.registered = false;
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:UnregisterAllInternal(handlersTable)
|
||||
for i, eventRegistrationInfo in ipairs(handlersTable) do
|
||||
self:UnregisterFromRegistrationInfo(eventRegistrationInfo);
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:GetDynamicCallbackRegistrantHandlers()
|
||||
self.callbackRegistrantHandlers = self.callbackRegistrantHandlers or {};
|
||||
return self.callbackRegistrantHandlers;
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:GetStaticCallbackRegistrantHandlers()
|
||||
self.staticCallbackRegistrantHandlers = self.staticCallbackRegistrantHandlers or {};
|
||||
return self.staticCallbackRegistrantHandlers;
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\..\..\..\..\WoW\Data\Interface\AddOns\Blizzard_SharedXML\UI.xsd">
|
||||
<Frame name="CallbackRegistrantTemplate" mixin="CallbackRegistrantMixin" virtual="true">
|
||||
<Scripts>
|
||||
<OnShow method="OnShow"/>
|
||||
<OnHide method="OnHide"/>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -1,4 +1,9 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/Scroll/ScrollBoxListView.lua.
|
||||
-- Backport of retail's Blizzard_SharedXML/Shared/Scroll/ScrollBoxListView.lua.
|
||||
-- The one difference: wipe is ClassicAPI.wipe, so an addon's own global
|
||||
-- wipe cannot replace it.
|
||||
|
||||
local wipe = ClassicAPI.wipe;
|
||||
|
||||
local InvalidationReason = EnumUtil.MakeEnum("DataProviderReassigned", "DataProviderContentsChanged");
|
||||
|
||||
ScrollBoxListViewMixin = CreateFromMixins(ScrollBoxViewMixin, CallbackRegistryMixin);
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/Scroll/ScrollUtil.lua.
|
||||
-- Backport of retail's Blizzard_SharedXML/Shared/Scroll/ScrollUtil.lua.
|
||||
-- The one difference: wipe is ClassicAPI.wipe, so an addon's own global
|
||||
-- wipe cannot replace it.
|
||||
|
||||
local wipe = ClassicAPI.wipe;
|
||||
|
||||
local function ContainsCursor(frame, cx, cy)
|
||||
return cy <= frame:GetTop() and cy >= frame:GetBottom()
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
-- elements turn it back on where they set those scripts.
|
||||
-- - Lua 5.0 has no `...` as an expression and no `#` operator; varargs
|
||||
-- forward through unpack(arg) and lengths come from table.getn.
|
||||
-- - wipe is ClassicAPI.wipe, so an addon's own global wipe cannot
|
||||
-- replace it.
|
||||
|
||||
local CreateSecureMap = SecureTypes.CreateSecureMap;
|
||||
local CreateSecureArray = SecureTypes.CreateSecureArray;
|
||||
@@ -32,6 +34,7 @@ local ProxyConvertablePrivateMixin = CreateFromMixins(ProxyConvertableMixin);
|
||||
local CreateFromMixinsPrivate = CreateFromMixins;
|
||||
local assert = assert;
|
||||
local type = type;
|
||||
local wipe = ClassicAPI.wipe;
|
||||
|
||||
local ModifyMenuRegistry = CreateFromMixinsPrivate(CallbackRegistryMixin);
|
||||
ModifyMenuRegistry:SetUndefinedEventsAllowed(true);
|
||||
|
||||
@@ -1,53 +1,44 @@
|
||||
-- ClassicAPI's own entry in the AddOn Options window
|
||||
-- (FrameXML\InterfaceOptions.lua): a ClassicAPI page with Controls, Combat
|
||||
-- and Text To Speech pages under it.
|
||||
-- (FrameXML\InterfaceOptions.lua): a ClassicAPI page with one page under it
|
||||
-- for each feature. Every cvar that ClassicAPI registers has a control here,
|
||||
-- and so does the player cache.
|
||||
--
|
||||
-- Where retail's Settings shows a setting, the option copies its label,
|
||||
-- tooltip and value mapping (Blizzard_SettingsDefinitions_Frame: Controls,
|
||||
-- Combat, Accessibility; the TTS options use the Text To Speech window's
|
||||
-- labels). Retail has no labels for the player cache, the cast settings or
|
||||
-- friendly soft targeting, so those strings are ClassicAPI's own.
|
||||
-- Each control reads and writes exactly one setting, and writes it only when
|
||||
-- the player uses that control. So the pages have no okay, cancel or default:
|
||||
-- Okay, Cancel and Escape only close the window, and Defaults does nothing
|
||||
-- here. A page reads its settings again each time it is shown.
|
||||
--
|
||||
-- Changes apply at once. Cancel (and Escape) restores what each page held
|
||||
-- when it was first shown since the last Okay or Cancel; Defaults restores
|
||||
-- the shown page's defaults.
|
||||
-- A page's controls scroll under its heading in a ScrollBox, so a page holds
|
||||
-- as many as its feature has. Dropdowns and number boxes sit on the same row
|
||||
-- as their label.
|
||||
--
|
||||
-- Every string is a global from locales\enUS.lua: retail's GlobalStrings key
|
||||
-- where retail has the string (ENABLE_INTERACT_TEXT and the like), a
|
||||
-- CLASSICAPI_ one where it does not.
|
||||
--
|
||||
-- Lua 5.0: no `...` expression or select().
|
||||
|
||||
local L = {
|
||||
ABOUT = "Settings for the features that ClassicAPI adds to the game.",
|
||||
PLAYER_CACHE = "Player Cache",
|
||||
REMEMBER_PLAYERS = "Remember players",
|
||||
REMEMBER_PLAYERS_TOOLTIP = "Saves the name, class, race and sex of the players you meet, so addons can show them in later sessions. All your characters on this realm share the data.",
|
||||
FRIEND_TARGETING = "Enable Friendly Action Targeting",
|
||||
FRIEND_TARGETING_TOOLTIP = "Picks the nearest friendly unit where you are looking. When you have no target, your helpful spells go to it.",
|
||||
AUTO_STAND = "Auto Stand",
|
||||
AUTO_STAND_TOOLTIP = "Stand up when you cast a spell while you sit.",
|
||||
AUTO_DISMOUNT = "Auto Dismount",
|
||||
AUTO_DISMOUNT_TOOLTIP = "Dismount when you cast a spell while you are mounted.",
|
||||
AUTO_UNSHIFT = "Auto Cancel Form",
|
||||
AUTO_UNSHIFT_TOOLTIP = "Leave your form when you cast a spell that you cannot cast in it.",
|
||||
local ARC_CHOICES = {
|
||||
{ value = 0, text = CLASSICAPI_DIRECTION_FRONT },
|
||||
{ value = 1, text = CLASSICAPI_DIRECTION_CONE },
|
||||
{ value = 2, text = CLASSICAPI_DIRECTION_ANY },
|
||||
}
|
||||
|
||||
-- Enum.SoftTargetEnableFlags: retail's checkboxes switch between Any (on)
|
||||
-- and Gamepad (off).
|
||||
local SOFT_TARGET_GAMEPAD, SOFT_TARGET_ANY = 1, 3
|
||||
local FORCE_CHOICES = {
|
||||
{ value = 0, text = CLASSICAPI_LOOSE_TARGET_FOLLOWS_NONE },
|
||||
{ value = 1, text = CLASSICAPI_LOOSE_TARGET_FOLLOWS_ENEMY },
|
||||
{ value = 2, text = CLASSICAPI_LOOSE_TARGET_FOLLOWS_FRIEND },
|
||||
}
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Settings: get / set / reset over cvars or the player cache
|
||||
-- Settings: get / set over one cvar or the player cache
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local function ResetCVars(cvars)
|
||||
for _, cvar in ipairs(cvars) do
|
||||
SetCVar(cvar, GetCVarDefault(cvar))
|
||||
end
|
||||
end
|
||||
|
||||
-- On for any nonzero number.
|
||||
local function CVarSetting(cvar)
|
||||
return {
|
||||
get = function() return GetCVar(cvar) == "1" end,
|
||||
get = function() return (tonumber(GetCVar(cvar)) or 0) ~= 0 end,
|
||||
set = function(value) SetCVar(cvar, value and "1" or "0") end,
|
||||
reset = function() ResetCVars({ cvar }) end,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -55,52 +46,33 @@ local function NumberCVarSetting(cvar)
|
||||
return {
|
||||
get = function() return tonumber(GetCVar(cvar)) end,
|
||||
set = function(value) SetCVar(cvar, tostring(value)) end,
|
||||
reset = function() ResetCVars({ cvar }) end,
|
||||
}
|
||||
end
|
||||
|
||||
-- Retail's "Enable Interact Key" / "Enable Action Targeting" proxies.
|
||||
-- The soft-target enable cvars: 0 off, 1 gamepad, 2 keyboard and mouse,
|
||||
-- 3 always. 1.12 has no gamepad, so 2 and 3 are on.
|
||||
local function SoftTargetSetting(cvar)
|
||||
return {
|
||||
get = function() return tonumber(GetCVar(cvar)) == SOFT_TARGET_ANY end,
|
||||
set = function(value) SetCVar(cvar, tostring(value and SOFT_TARGET_ANY or SOFT_TARGET_GAMEPAD)) end,
|
||||
reset = function() ResetCVars({ cvar }) end,
|
||||
get = function() return (tonumber(GetCVar(cvar)) or 0) >= 2 end,
|
||||
set = function(value) SetCVar(cvar, value and "3" or "0") end,
|
||||
}
|
||||
end
|
||||
|
||||
-- Retail's "Interact Key Icons" proxy: 1 = NPCs only, 2 = all, 3 = none.
|
||||
local ICON_CVARS = { "SoftTargetIconEnemy", "SoftTargetIconInteract", "SoftTargetIconGameObject", "SoftTargetLowPriorityIcons" }
|
||||
local ICON_PRESETS = {
|
||||
[1] = { "0", "1", "0", "0" },
|
||||
[2] = { "1", "1", "1", "1" },
|
||||
[3] = { "0", "0", "0", "0" },
|
||||
}
|
||||
local interactIconsSetting = {
|
||||
get = function()
|
||||
local on, off = true, true
|
||||
for _, cvar in ipairs(ICON_CVARS) do
|
||||
if GetCVar(cvar) == "1" then off = false else on = false end
|
||||
end
|
||||
if on then return 2 elseif off then return 3 else return 1 end
|
||||
end,
|
||||
set = function(value)
|
||||
for i, cvar in ipairs(ICON_CVARS) do
|
||||
SetCVar(cvar, ICON_PRESETS[value][i])
|
||||
end
|
||||
end,
|
||||
reset = function() ResetCVars(ICON_CVARS) end,
|
||||
}
|
||||
|
||||
local rememberSetting = {
|
||||
get = function() return C_PlayerCache.IsEnabled() and true or false end,
|
||||
set = function(value) C_PlayerCache.SetEnabled(value) end,
|
||||
reset = function() C_PlayerCache.SetEnabled(false) end,
|
||||
}
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Pages
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Row labels line up with the checkbox labels; row controls start at
|
||||
-- CONTROL_X.
|
||||
local LABEL_X = 48
|
||||
local CONTROL_X = 200
|
||||
|
||||
-- pfUI's Interface Options skin finds the controls by these names.
|
||||
local numControls = 0
|
||||
local function NextName()
|
||||
numControls = numControls + 1
|
||||
@@ -115,6 +87,10 @@ local function RefreshControls(page)
|
||||
page.updating = nil
|
||||
end
|
||||
|
||||
-- The controls go on `page.content`, the one frame in the page's ScrollBox,
|
||||
-- `page.y` down from its top. The clipping of the ScrollBox also keeps the
|
||||
-- mouse off the controls that are scrolled out of view.
|
||||
-- `title` is the heading, the category name when nil.
|
||||
local function NewPage(name, parent, title, subtitle)
|
||||
local page = CreateFrame("Frame")
|
||||
page.name = name
|
||||
@@ -123,66 +99,80 @@ local function NewPage(name, parent, title, subtitle)
|
||||
|
||||
local heading = page:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
|
||||
heading:SetPoint("TOPLEFT", page, "TOPLEFT", 16, -16)
|
||||
heading:SetText(title)
|
||||
page.y = -40
|
||||
heading:SetText(title or name)
|
||||
local top = -40
|
||||
if subtitle then
|
||||
local text = page:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
|
||||
text:SetPoint("TOPLEFT", heading, "BOTTOMLEFT", 0, -8)
|
||||
text:SetPoint("RIGHT", page, "RIGHT", -32, 0)
|
||||
text:SetJustifyH("LEFT")
|
||||
text:SetText(subtitle)
|
||||
page.y = -64
|
||||
top = -64
|
||||
end
|
||||
|
||||
page.refresh = function()
|
||||
if not page.saved then
|
||||
page.saved = {}
|
||||
for i, control in ipairs(page.controls) do
|
||||
page.saved[i] = control.setting.get()
|
||||
end
|
||||
end
|
||||
RefreshControls(page)
|
||||
end
|
||||
page.okay = function()
|
||||
page.saved = nil
|
||||
end
|
||||
page.cancel = function()
|
||||
if not page.saved then return end
|
||||
for i, control in ipairs(page.controls) do
|
||||
if control.setting.get() ~= page.saved[i] then
|
||||
control.setting.set(page.saved[i])
|
||||
end
|
||||
end
|
||||
page.saved = nil
|
||||
end
|
||||
page.default = function()
|
||||
for _, control in ipairs(page.controls) do
|
||||
control.setting.reset()
|
||||
end
|
||||
RefreshControls(page)
|
||||
end
|
||||
local scrollBox = CreateFrame("Frame", nil, page, "WowScrollBox")
|
||||
scrollBox:SetPoint("TOPLEFT", page, "TOPLEFT", 0, top)
|
||||
scrollBox:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", -20, 8)
|
||||
scrollBox:EnableMouseWheel(true)
|
||||
local scrollBar = CreateFrame("EventFrame", nil, page, "MinimalScrollBar")
|
||||
scrollBar:SetPoint("TOPLEFT", scrollBox, "TOPRIGHT", 4, 0)
|
||||
scrollBar:SetPoint("BOTTOMLEFT", scrollBox, "BOTTOMRIGHT", 4, 0)
|
||||
|
||||
local content = CreateFrame("Frame", nil, scrollBox)
|
||||
content:SetHeight(1)
|
||||
content.scrollable = true
|
||||
ScrollUtil.InitScrollBoxWithScrollBar(scrollBox, scrollBar, CreateScrollBoxLinearView())
|
||||
ScrollUtil.AddManagedScrollBarVisibilityBehavior(scrollBox, scrollBar)
|
||||
|
||||
page.scrollBox = scrollBox
|
||||
page.content = content
|
||||
page.y = 0
|
||||
page.refresh = RefreshControls
|
||||
|
||||
InterfaceOptions_AddCategory(page)
|
||||
return page
|
||||
end
|
||||
|
||||
-- Moves `page.y` down past a row and makes the content that tall. The
|
||||
-- ScrollBox measures it again on its next update (a hidden page's waits for
|
||||
-- the page to be shown).
|
||||
local function Grow(page, height)
|
||||
page.y = page.y - height
|
||||
page.content:SetHeight(8 - page.y)
|
||||
page.scrollBox:FullUpdate(ScrollBoxConstants.UpdateQueued)
|
||||
end
|
||||
|
||||
local function AddControl(page, control, setting, height)
|
||||
control.setting = setting
|
||||
table.insert(page.controls, control)
|
||||
page.y = page.y - height
|
||||
Grow(page, height)
|
||||
end
|
||||
|
||||
local function AddHeader(page, text)
|
||||
local header = page:CreateFontString(nil, "ARTWORK", "GameFontNormal")
|
||||
header:SetPoint("TOPLEFT", page, "TOPLEFT", 16, page.y - 8)
|
||||
local header = page.content:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
|
||||
header:SetPoint("TOPLEFT", page.content, "TOPLEFT", 16, page.y - 6)
|
||||
header:SetText(text)
|
||||
page.y = page.y - 28
|
||||
Grow(page, 24)
|
||||
end
|
||||
|
||||
local function AddRowLabel(page, label)
|
||||
local text = page.content:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
|
||||
text:SetPoint("TOPLEFT", page.content, "TOPLEFT", LABEL_X, page.y - 10)
|
||||
text:SetText(label)
|
||||
end
|
||||
|
||||
local function SetTooltip(frame, tooltip)
|
||||
frame:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(tooltip, nil, nil, nil, nil, 1)
|
||||
end)
|
||||
frame:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
end
|
||||
|
||||
-- `enabledIf`, when given, greys the checkbox out while it returns false.
|
||||
local function AddCheckbox(page, label, tooltip, setting, enabledIf)
|
||||
local check = CreateFrame("CheckButton", NextName(), page, "OptionsCheckButtonTemplate")
|
||||
check:SetPoint("TOPLEFT", page, "TOPLEFT", 16, page.y)
|
||||
local check = CreateFrame("CheckButton", NextName(), page.content, "OptionsCheckButtonTemplate")
|
||||
check:SetPoint("TOPLEFT", page.content, "TOPLEFT", 16, page.y)
|
||||
local text = getglobal(check:GetName() .. "Text")
|
||||
text:SetText(label)
|
||||
check.tooltipText = tooltip
|
||||
@@ -201,32 +191,32 @@ local function AddCheckbox(page, label, tooltip, setting, enabledIf)
|
||||
end
|
||||
end
|
||||
AddControl(page, check, setting, 30)
|
||||
return check
|
||||
end
|
||||
|
||||
-- `options()` returns the entries as { value = ..., text = ... }.
|
||||
local function AddDropDown(page, label, tooltip, setting, options)
|
||||
local title = page:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
|
||||
title:SetPoint("TOPLEFT", page, "TOPLEFT", 20, page.y - 6)
|
||||
title:SetText(label)
|
||||
-- `choices` is a list of { value = ..., text = ... }, or a function that
|
||||
-- returns one. A value with no choice shows as the number itself.
|
||||
local function AddDropDown(page, label, tooltip, setting, choices)
|
||||
AddRowLabel(page, label)
|
||||
|
||||
local dropdown = CreateFrame("Frame", NextName(), page, "UIDropDownMenuTemplate")
|
||||
dropdown:SetPoint("TOPLEFT", page, "TOPLEFT", 0, page.y - 20)
|
||||
UIDropDownMenu_SetWidth(180, dropdown)
|
||||
local function List()
|
||||
if type(choices) == "function" then return choices() end
|
||||
return choices
|
||||
end
|
||||
|
||||
-- The template's art has a 16 pixel margin on the left.
|
||||
local dropdown = CreateFrame("Frame", NextName(), page.content, "UIDropDownMenuTemplate")
|
||||
dropdown:SetPoint("TOPLEFT", page.content, "TOPLEFT", CONTROL_X - 16, page.y)
|
||||
UIDropDownMenu_SetWidth(150, dropdown)
|
||||
dropdown:EnableMouse(true)
|
||||
dropdown:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(tooltip, nil, nil, nil, nil, 1)
|
||||
end)
|
||||
dropdown:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
SetTooltip(dropdown, tooltip)
|
||||
|
||||
local function Initialize()
|
||||
local selected = setting.get()
|
||||
for _, option in ipairs(options()) do
|
||||
for _, choice in ipairs(List()) do
|
||||
local info = {}
|
||||
info.text = option.text
|
||||
info.value = option.value
|
||||
info.checked = option.value == selected and 1 or nil
|
||||
info.text = choice.text
|
||||
info.value = choice.value
|
||||
info.checked = choice.value == selected and 1 or nil
|
||||
info.func = function()
|
||||
UIDropDownMenu_SetSelectedValue(dropdown, this.value)
|
||||
setting.set(this.value)
|
||||
@@ -236,18 +226,55 @@ local function AddDropDown(page, label, tooltip, setting, options)
|
||||
end
|
||||
dropdown.Update = function(self)
|
||||
UIDropDownMenu_Initialize(self, Initialize)
|
||||
UIDropDownMenu_SetSelectedValue(self, setting.get())
|
||||
local selected = setting.get()
|
||||
UIDropDownMenu_SetSelectedValue(self, selected)
|
||||
local text = tostring(selected or "")
|
||||
for _, choice in ipairs(List()) do
|
||||
if choice.value == selected then text = choice.text end
|
||||
end
|
||||
UIDropDownMenu_SetText(text, self)
|
||||
end
|
||||
AddControl(page, dropdown, setting, 56)
|
||||
return dropdown
|
||||
AddControl(page, dropdown, setting, 32)
|
||||
end
|
||||
|
||||
local function AddSlider(page, label, tooltip, setting, minValue, maxValue)
|
||||
local slider = CreateFrame("Slider", NextName(), page, "OptionsSliderTemplate")
|
||||
slider:SetPoint("TOPLEFT", page, "TOPLEFT", 24, page.y - 20)
|
||||
-- Writes when Enter is pressed or the box loses focus; Escape puts the
|
||||
-- current value back. Text that is not a number is not written.
|
||||
local function AddNumberBox(page, label, tooltip, setting)
|
||||
AddRowLabel(page, label)
|
||||
|
||||
local box = CreateFrame("EditBox", NextName(), page.content, "InputBoxTemplate")
|
||||
box:SetPoint("TOPLEFT", page.content, "TOPLEFT", CONTROL_X + 6, page.y - 6)
|
||||
box:SetWidth(70)
|
||||
box:SetHeight(20)
|
||||
box:SetAutoFocus(false)
|
||||
SetTooltip(box, tooltip)
|
||||
|
||||
local function ShowValue(self)
|
||||
self:SetText(tostring(setting.get() or ""))
|
||||
end
|
||||
box:SetScript("OnEnterPressed", function() this:ClearFocus() end)
|
||||
box:SetScript("OnEscapePressed", function()
|
||||
ShowValue(this)
|
||||
this:ClearFocus()
|
||||
end)
|
||||
box:SetScript("OnEditFocusLost", function()
|
||||
this:HighlightText(0, 0)
|
||||
local value = tonumber(this:GetText())
|
||||
if value and value ~= setting.get() then
|
||||
setting.set(value)
|
||||
end
|
||||
ShowValue(this)
|
||||
end)
|
||||
box.Update = ShowValue
|
||||
AddControl(page, box, setting, 32)
|
||||
end
|
||||
|
||||
local function AddSlider(page, label, tooltip, setting, minValue, maxValue, step)
|
||||
local slider = CreateFrame("Slider", NextName(), page.content, "OptionsSliderTemplate")
|
||||
slider:SetPoint("TOPLEFT", page.content, "TOPLEFT", 24, page.y - 20)
|
||||
slider:SetWidth(200)
|
||||
slider:SetMinMaxValues(minValue, maxValue)
|
||||
slider:SetValueStep(1)
|
||||
slider:SetValueStep(step)
|
||||
slider.tooltipText = tooltip
|
||||
getglobal(slider:GetName() .. "Text"):SetText(label)
|
||||
getglobal(slider:GetName() .. "Low"):SetText(minValue)
|
||||
@@ -255,21 +282,24 @@ local function AddSlider(page, label, tooltip, setting, minValue, maxValue)
|
||||
|
||||
local value = slider:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
|
||||
value:SetPoint("LEFT", slider, "RIGHT", 8, 0)
|
||||
local function Format(v)
|
||||
if step < 1 then return string.format("%.2f", v) end
|
||||
return v
|
||||
end
|
||||
|
||||
slider:SetScript("OnValueChanged", function()
|
||||
local v = math.floor(this:GetValue() + 0.5)
|
||||
value:SetText(v)
|
||||
local v = math.floor(this:GetValue() / step + 0.5) * step
|
||||
value:SetText(Format(v))
|
||||
if not page.updating and v ~= setting.get() then
|
||||
setting.set(v)
|
||||
end
|
||||
end)
|
||||
slider.Update = function(self)
|
||||
local v = setting.get()
|
||||
local v = setting.get() or minValue
|
||||
self:SetValue(v)
|
||||
value:SetText(v)
|
||||
value:SetText(Format(v))
|
||||
end
|
||||
AddControl(page, slider, setting, 56)
|
||||
return slider
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
@@ -277,57 +307,115 @@ end
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local version = GetAddOnMetadata("!!!ClassicAPI", "Version")
|
||||
local main = NewPage("ClassicAPI", nil, "ClassicAPI " .. version, L.ABOUT)
|
||||
local main = NewPage("ClassicAPI", nil, "ClassicAPI " .. version, CLASSICAPI_OPTIONS_ABOUT)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Controls
|
||||
-- Interact Key
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local controls = NewPage(CONTROLS_LABEL, main.name, CONTROLS_LABEL)
|
||||
local interact = NewPage(CLASSICAPI_INTERACT_KEY, main.name)
|
||||
|
||||
AddCheckbox(controls, ENABLE_INTERACT_TEXT, OPTION_TOOLTIP_ENABLE_INTERACT, SoftTargetSetting("SoftTargetInteract"))
|
||||
AddDropDown(controls, INTERACT_ICONS_OPTION, OPTION_TOOLTIP_INTERACT_ICONS, interactIconsSetting, function()
|
||||
return {
|
||||
{ value = 1, text = INTERACT_ICONS_DEFAULT },
|
||||
{ value = 2, text = INTERACT_ICONS_SHOW_ALL },
|
||||
{ value = 3, text = INTERACT_ICONS_SHOW_NONE },
|
||||
}
|
||||
end)
|
||||
AddCheckbox(controls, L.AUTO_STAND, L.AUTO_STAND_TOOLTIP, CVarSetting("autoStand"))
|
||||
AddCheckbox(controls, L.AUTO_DISMOUNT, L.AUTO_DISMOUNT_TOOLTIP, CVarSetting("autoDismount"))
|
||||
AddCheckbox(controls, L.AUTO_UNSHIFT, L.AUTO_UNSHIFT_TOOLTIP, CVarSetting("autoUnshift"))
|
||||
AddCheckbox(interact, ENABLE_INTERACT_TEXT, OPTION_TOOLTIP_ENABLE_INTERACT, SoftTargetSetting("SoftTargetInteract"))
|
||||
AddDropDown(interact, CLASSICAPI_INTERACT_DIRECTION, CLASSICAPI_INTERACT_DIRECTION_TOOLTIP, NumberCVarSetting("SoftTargetInteractArc"), ARC_CHOICES)
|
||||
AddNumberBox(interact, CLASSICAPI_INTERACT_RANGE, CLASSICAPI_RANGE_TOOLTIP, NumberCVarSetting("SoftTargetInteractRange"))
|
||||
AddCheckbox(interact, CLASSICAPI_STRICT_INTERACT_RANGE, CLASSICAPI_STRICT_INTERACT_RANGE_TOOLTIP, CVarSetting("SoftTargetInteractRangeIsHard"))
|
||||
local interactIcon = CVarSetting("SoftTargetIconInteract")
|
||||
AddCheckbox(interact, CLASSICAPI_INTERACT_ICON, CLASSICAPI_INTERACT_ICON_TOOLTIP, interactIcon)
|
||||
-- The object icon is drawn only while the interact icon is on.
|
||||
AddCheckbox(interact, CLASSICAPI_GAME_OBJECT_ICON, CLASSICAPI_GAME_OBJECT_ICON_TOOLTIP, CVarSetting("SoftTargetIconGameObject"), interactIcon.get)
|
||||
|
||||
AddHeader(interact, CLASSICAPI_ALL_SOFT_TARGET_ICONS)
|
||||
AddCheckbox(interact, CLASSICAPI_LOW_PRIORITY_ICONS, CLASSICAPI_LOW_PRIORITY_ICONS_TOOLTIP, CVarSetting("SoftTargetLowPriorityIcons"))
|
||||
AddNumberBox(interact, CLASSICAPI_ICON_SIZE, CLASSICAPI_ICON_SIZE_TOOLTIP, NumberCVarSetting("SoftTargetNameplateSize"))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Action Targeting
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local targeting = NewPage(CLASSICAPI_ACTION_TARGETING, main.name)
|
||||
|
||||
AddHeader(targeting, CLASSICAPI_ENEMIES)
|
||||
AddCheckbox(targeting, ACTION_TARGETING_OPTION, OPTION_TOOLTIP_ACTION_TARGETING, SoftTargetSetting("SoftTargetEnemy"))
|
||||
AddDropDown(targeting, CLASSICAPI_ENEMY_DIRECTION, CLASSICAPI_UNIT_DIRECTION_TOOLTIP, NumberCVarSetting("SoftTargetEnemyArc"), ARC_CHOICES)
|
||||
AddNumberBox(targeting, CLASSICAPI_ENEMY_RANGE, CLASSICAPI_RANGE_TOOLTIP, NumberCVarSetting("SoftTargetEnemyRange"))
|
||||
AddCheckbox(targeting, CLASSICAPI_ENEMY_ICON, CLASSICAPI_ENEMY_ICON_TOOLTIP, CVarSetting("SoftTargetIconEnemy"))
|
||||
|
||||
AddHeader(targeting, CLASSICAPI_FRIENDS)
|
||||
AddCheckbox(targeting, CLASSICAPI_FRIEND_TARGETING, CLASSICAPI_FRIEND_TARGETING_TOOLTIP, SoftTargetSetting("SoftTargetFriend"))
|
||||
AddDropDown(targeting, CLASSICAPI_FRIEND_DIRECTION, CLASSICAPI_UNIT_DIRECTION_TOOLTIP, NumberCVarSetting("SoftTargetFriendArc"), ARC_CHOICES)
|
||||
AddNumberBox(targeting, CLASSICAPI_FRIEND_RANGE, CLASSICAPI_RANGE_TOOLTIP, NumberCVarSetting("SoftTargetFriendRange"))
|
||||
AddCheckbox(targeting, CLASSICAPI_FRIEND_ICON, CLASSICAPI_FRIEND_ICON_TOOLTIP, CVarSetting("SoftTargetIconFriend"))
|
||||
|
||||
AddHeader(targeting, CLASSICAPI_WHEN_YOU_HAVE_A_TARGET)
|
||||
AddCheckbox(targeting, CLASSICAPI_KEEP_ACTION_TARGETING, CLASSICAPI_KEEP_ACTION_TARGETING_TOOLTIP, CVarSetting("SoftTargetWithLocked"))
|
||||
AddCheckbox(targeting, CLASSICAPI_PREFER_YOUR_TARGET, CLASSICAPI_PREFER_YOUR_TARGET_TOOLTIP, CVarSetting("SoftTargetMatchLocked"))
|
||||
AddDropDown(targeting, CLASSICAPI_LOOSE_TARGET_FOLLOWS, CLASSICAPI_LOOSE_TARGET_FOLLOWS_TOOLTIP, NumberCVarSetting("SoftTargetForce"), FORCE_CHOICES)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Combat
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local combat = NewPage(COMBAT_LABEL, main.name, COMBAT_LABEL)
|
||||
local combat = NewPage(COMBAT_LABEL, main.name)
|
||||
|
||||
AddCheckbox(combat, ACTION_TARGETING_OPTION, OPTION_TOOLTIP_ACTION_TARGETING, SoftTargetSetting("SoftTargetEnemy"))
|
||||
-- Retail has no friend checkbox; same mapping as its enemy one.
|
||||
AddCheckbox(combat, L.FRIEND_TARGETING, L.FRIEND_TARGETING_TOOLTIP, SoftTargetSetting("SoftTargetFriend"))
|
||||
AddHeader(combat, CLASSICAPI_ACTION_BARS)
|
||||
AddCheckbox(combat, ACTION_BUTTON_USE_KEY_DOWN, OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN, CVarSetting("ActionButtonUseKeyDown"))
|
||||
AddCheckbox(combat, COUNTDOWN_FOR_COOLDOWNS_TEXT, OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS, CVarSetting("countdownForCooldowns"))
|
||||
|
||||
AddHeader(combat, CLASSICAPI_CASTING_AND_ATTACKING)
|
||||
AddCheckbox(combat, CLASSICAPI_AUTO_STAND, CLASSICAPI_AUTO_STAND_TOOLTIP, CVarSetting("autoStand"))
|
||||
AddCheckbox(combat, CLASSICAPI_AUTO_DISMOUNT, CLASSICAPI_AUTO_DISMOUNT_TOOLTIP, CVarSetting("autoDismount"))
|
||||
AddCheckbox(combat, CLASSICAPI_AUTO_UNSHIFT, CLASSICAPI_AUTO_UNSHIFT_TOOLTIP, CVarSetting("autoUnshift"))
|
||||
AddCheckbox(combat, STOP_AUTO_ATTACK, OPTION_TOOLTIP_STOP_AUTO_ATTACK, CVarSetting("stopAutoAttackOnTargetChange"))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Bags and Items
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local bags = NewPage(CLASSICAPI_BAGS_AND_ITEMS, main.name)
|
||||
|
||||
AddCheckbox(bags, CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT, CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT_TOOLTIP, CVarSetting("sortBagsRightToLeft"))
|
||||
AddCheckbox(bags, CLASSICAPI_IGNORE_BACKPACK, CLASSICAPI_IGNORE_BACKPACK_TOOLTIP, CVarSetting("backpackAutosortDisabled"))
|
||||
AddCheckbox(bags, CLASSICAPI_IGNORE_BANK, CLASSICAPI_IGNORE_BANK_TOOLTIP, CVarSetting("bankAutosortDisabled"))
|
||||
AddCheckbox(bags, CLASSICAPI_HIDE_EQUIPMENT_SETS, CLASSICAPI_HIDE_EQUIPMENT_SETS_TOOLTIP, CVarSetting("dontShowEquipmentSetsOnItems"))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Interface
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local interface = NewPage(CLASSICAPI_INTERFACE, main.name)
|
||||
|
||||
AddHeader(interface, CLASSICAPI_NAMEPLATES)
|
||||
AddSlider(interface, CLASSICAPI_TARGET_NAMEPLATE_ALPHA, CLASSICAPI_TARGET_NAMEPLATE_ALPHA_TOOLTIP, NumberCVarSetting("nameplateSelectedAlpha"), 0, 1, 0.05)
|
||||
AddSlider(interface, CLASSICAPI_OTHER_NAMEPLATE_ALPHA, CLASSICAPI_OTHER_NAMEPLATE_ALPHA_TOOLTIP, NumberCVarSetting("nameplateNotSelectedAlpha"), 0, 1, 0.05)
|
||||
|
||||
AddHeader(interface, CLASSICAPI_NUMBERS)
|
||||
AddCheckbox(interface, CLASSICAPI_SEPARATE_LARGE_NUMBERS, CLASSICAPI_SEPARATE_LARGE_NUMBERS_TOOLTIP, CVarSetting("breakUpLargeNumbers"))
|
||||
|
||||
AddHeader(interface, CLASSICAPI_MOTION)
|
||||
AddNumberBox(interface, CLASSICAPI_UI_SHAKE_STRENGTH, CLASSICAPI_UI_SHAKE_STRENGTH_TOOLTIP, NumberCVarSetting("ShakeStrengthUI"))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Text To Speech
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local tts = NewPage(TEXT_TO_SPEECH, main.name, TEXT_TO_SPEECH)
|
||||
local tts = NewPage(TEXT_TO_SPEECH, main.name)
|
||||
|
||||
AddDropDown(tts, TEXT_TO_SPEECH_VOICE_OPTION, TEXT_TO_SPEECH_VOICE_OPTION, NumberCVarSetting("ttsVoice"), function()
|
||||
local options = {}
|
||||
local choices = {}
|
||||
for _, voice in ipairs(C_VoiceChat.GetTtsVoices()) do
|
||||
table.insert(options, { value = voice.voiceID, text = voice.name })
|
||||
table.insert(choices, { value = voice.voiceID, text = voice.name })
|
||||
end
|
||||
return options
|
||||
return choices
|
||||
end)
|
||||
AddSlider(tts, TEXT_TO_SPEECH_ADJUST_RATE, TEXT_TO_SPEECH_ADJUST_RATE, NumberCVarSetting("ttsSpeed"), -10, 10)
|
||||
AddSlider(tts, TEXT_TO_SPEECH_ADJUST_VOLUME, TEXT_TO_SPEECH_ADJUST_VOLUME, NumberCVarSetting("ttsVolume"), 0, 100)
|
||||
AddSlider(tts, TEXT_TO_SPEECH_ADJUST_RATE, TEXT_TO_SPEECH_ADJUST_RATE, NumberCVarSetting("ttsSpeed"), -10, 10, 1)
|
||||
AddSlider(tts, TEXT_TO_SPEECH_ADJUST_VOLUME, TEXT_TO_SPEECH_ADJUST_VOLUME, NumberCVarSetting("ttsVolume"), 0, 100, 1)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Player Cache
|
||||
-- Advanced
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local playerCache = NewPage(L.PLAYER_CACHE, main.name, L.PLAYER_CACHE)
|
||||
AddCheckbox(playerCache, L.REMEMBER_PLAYERS, L.REMEMBER_PLAYERS_TOOLTIP, rememberSetting)
|
||||
local advanced = NewPage(CLASSICAPI_ADVANCED, main.name)
|
||||
|
||||
AddCheckbox(advanced, CLASSICAPI_REMEMBER_PLAYERS, CLASSICAPI_REMEMBER_PLAYERS_TOOLTIP, rememberSetting)
|
||||
AddNumberBox(advanced, CLASSICAPI_COMBAT_LOG_RETENTION, CLASSICAPI_COMBAT_LOG_RETENTION_TOOLTIP, NumberCVarSetting("combatLogRetentionTime"))
|
||||
AddCheckbox(advanced, CLASSICAPI_SCRIPT_PROFILE, CLASSICAPI_SCRIPT_PROFILE_TOOLTIP, CVarSetting("scriptProfile"))
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
-- Backport of retail's Blizzard_FrameXMLUtil/AnimationSystem.lua.
|
||||
|
||||
--[[ animTable = { --Note that only consistent data should be in here. These tables are meant to be shared across "sessions" of animations. Put changing data in the frame.
|
||||
totalTime = number, --Time to complete the animation in seconds.
|
||||
updateFunc = function, --The function called to do the actual change. Takes self, elapsed fraction. Usually frame.SetPoint, frame.SetAlpha, ect.
|
||||
getPosFunc = function, --The function returning the data being passed into updateFunc. For example. might return .18 if updateFunc is frame.SetAlpha.
|
||||
--]]
|
||||
local AnimatingFrames = {};
|
||||
|
||||
local AnimUpdateFrame = CreateFrame("Frame");
|
||||
|
||||
local function Animation_UpdateFrame(self, animElapsed, animTable)
|
||||
local totalTime = animTable.totalTime
|
||||
if ( animElapsed and (animElapsed < totalTime)) then --Should be animating
|
||||
local elapsedFraction = self.animReverse and (1-animElapsed/totalTime) or (animElapsed/totalTime);
|
||||
animTable.updateFunc(self, animTable.getPosFunc(self, elapsedFraction));
|
||||
else --Just finished animating
|
||||
animTable.updateFunc(self, animTable.getPosFunc(self, self.animReverse and 0 or 1));
|
||||
self.animating = false;
|
||||
|
||||
AnimatingFrames[self][animTable.updateFunc] = 0; --We use 0 instead of nil'ing out because we don't want to mess with 'next' (used in pairs)
|
||||
|
||||
if ( self.animPostFunc ) then
|
||||
self.animPostFunc(self);
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
local totalElapsed = 0;
|
||||
local function Animation_OnUpdate(self, elapsed)
|
||||
totalElapsed = totalElapsed + elapsed;
|
||||
local isAnyFrameAnimating = false;
|
||||
for frame, frameTable in pairs(AnimatingFrames) do
|
||||
for _, animTable in pairs(frameTable) do
|
||||
if ( animTable ~= 0 ) then
|
||||
Animation_UpdateFrame(frame, totalElapsed - frame.animStartTime, animTable);
|
||||
isAnyFrameAnimating = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
if ( not isAnyFrameAnimating ) then
|
||||
table.wipe(AnimatingFrames);
|
||||
AnimUpdateFrame:SetScript("OnUpdate", nil);
|
||||
end
|
||||
end
|
||||
|
||||
function SetUpAnimation(frame, animTable, postFunc, reverse)
|
||||
if ( type(animTable.updateFunc) == "string" ) then
|
||||
animTable.updateFunc = frame[animTable.updateFunc];
|
||||
end
|
||||
if ( not AnimatingFrames[frame] ) then
|
||||
AnimatingFrames[frame] = {};
|
||||
end
|
||||
|
||||
AnimatingFrames[frame][animTable.updateFunc] = animTable;
|
||||
|
||||
frame.animStartTime = totalElapsed;
|
||||
frame.animReverse = reverse;
|
||||
frame.animPostFunc = postFunc;
|
||||
frame.animating = true;
|
||||
|
||||
animTable.updateFunc(frame, animTable.getPosFunc(frame, frame.animReverse and 1 or 0));
|
||||
|
||||
AnimUpdateFrame:SetScript("OnUpdate", Animation_OnUpdate);
|
||||
end
|
||||
|
||||
function CancelAnimations(frame)
|
||||
local anims = AnimatingFrames[frame];
|
||||
if ( anims ) then
|
||||
for k, v in pairs(anims) do
|
||||
anims[k] = 0;
|
||||
end
|
||||
frame.animating = false;
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,114 @@
|
||||
-- Backport of retail's Blizzard_ObjectAPI/Mainline/AsyncCallbackSystem.lua.
|
||||
|
||||
--[[
|
||||
Queries some data retrieval API (specifically where the data may not be currently available) and when it becomes available
|
||||
calls a user-supplied function. The callback can be canceled if necessary (e.g. the frame that would use the data becomes
|
||||
hidden before the data arrives).
|
||||
|
||||
The API is managed so that arbitrary query functions cannot be executed.
|
||||
--]]
|
||||
|
||||
AsyncCallbackAPIType = {
|
||||
ASYNC_QUEST = 1,
|
||||
ASYNC_ITEM = 2,
|
||||
ASYNC_SPELL = 3,
|
||||
}
|
||||
|
||||
local permittedAPI =
|
||||
{
|
||||
[AsyncCallbackAPIType.ASYNC_QUEST] = { event = "QUEST_DATA_LOAD_RESULT", accessor = C_QuestLog.RequestLoadQuestByID },
|
||||
[AsyncCallbackAPIType.ASYNC_ITEM] = { event = "ITEM_DATA_LOAD_RESULT", accessor = C_Item.RequestLoadItemDataByID },
|
||||
[AsyncCallbackAPIType.ASYNC_SPELL] = { event = "SPELL_DATA_LOAD_RESULT", accessor = C_Spell.RequestLoadSpellData },
|
||||
};
|
||||
|
||||
AsyncCallbackSystemMixin = {};
|
||||
|
||||
function AsyncCallbackSystemMixin:Init(apiType)
|
||||
self.callbacks = {};
|
||||
|
||||
-- API Type should be set up from key value pairs before OnLoad.
|
||||
self.api = permittedAPI[apiType];
|
||||
|
||||
self:SetScript("OnEvent",
|
||||
function(self, event, ...)
|
||||
if event == self.api.event then
|
||||
local id, success = ...;
|
||||
if success or apiType == AsyncCallbackAPIType.ASYNC_ITEM then
|
||||
self:FireCallbacks(id, success);
|
||||
else
|
||||
self:ClearCallbacks(id);
|
||||
end
|
||||
end
|
||||
end
|
||||
);
|
||||
self:RegisterEvent(self.api.event);
|
||||
end
|
||||
|
||||
local CANCELED_SENTINEL = -1;
|
||||
|
||||
function AsyncCallbackSystemMixin:AddCallback(id, callbackFunction)
|
||||
local callbacks = self:GetOrCreateCallbacks(id);
|
||||
table.insert(callbacks, callbackFunction);
|
||||
local needsAccessorCall = #callbacks == 1;
|
||||
if needsAccessorCall then
|
||||
self.api.accessor(id);
|
||||
end
|
||||
|
||||
return #callbacks, callbacks;
|
||||
end
|
||||
|
||||
function AsyncCallbackSystemMixin:AddCancelableCallback(id, callbackFunction)
|
||||
-- NOTE: If the data is currently availble then the callback will be executed and callbacks cleared, so there will be nothing to cancel.
|
||||
local index, callbacks = self:AddCallback(id, callbackFunction);
|
||||
return function()
|
||||
if #callbacks > 0 and callbacks[index] ~= CANCELED_SENTINEL then
|
||||
callbacks[index] = CANCELED_SENTINEL;
|
||||
return true;
|
||||
end
|
||||
return false;
|
||||
end;
|
||||
end
|
||||
|
||||
function AsyncCallbackSystemMixin:FireCallbacks(id, success)
|
||||
local callbacks = self:GetCallbacks(id);
|
||||
if callbacks then
|
||||
self:ClearCallbacks(id);
|
||||
for i, callback in ipairs(callbacks) do
|
||||
if callback ~= CANCELED_SENTINEL then
|
||||
xpcall(callback, CallErrorHandler, id, success);
|
||||
end
|
||||
end
|
||||
|
||||
-- The cancel functions have a reference to this table, so ensure that it's cleared out.
|
||||
for i = #callbacks, 1, -1 do
|
||||
callbacks[i] = nil;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function AsyncCallbackSystemMixin:ClearCallbacks(id)
|
||||
self.callbacks[id] = nil;
|
||||
end
|
||||
|
||||
function AsyncCallbackSystemMixin:GetCallbacks(id)
|
||||
return self.callbacks[id];
|
||||
end
|
||||
|
||||
function AsyncCallbackSystemMixin:GetOrCreateCallbacks(id)
|
||||
local callbacks = self.callbacks[id];
|
||||
if not callbacks then
|
||||
callbacks = {};
|
||||
self.callbacks[id] = callbacks;
|
||||
end
|
||||
return callbacks;
|
||||
end
|
||||
|
||||
local function CreateListener(apiType)
|
||||
local listener = Mixin(CreateFrame("Frame"), AsyncCallbackSystemMixin);
|
||||
listener:Init(apiType);
|
||||
return listener;
|
||||
end
|
||||
|
||||
ItemEventListener = CreateListener(AsyncCallbackAPIType.ASYNC_ITEM);
|
||||
SpellEventListener = CreateListener(AsyncCallbackAPIType.ASYNC_SPELL);
|
||||
QuestEventListener = CreateListener(AsyncCallbackAPIType.ASYNC_QUEST);
|
||||
@@ -1,214 +1,268 @@
|
||||
-- Backport of 3.3.5 FrameXML/CallbackRegistryMixin.lua to vanilla 1.12 / Lua 5.0.
|
||||
-- Backport of retail Blizzard_SharedXMLBase/CallbackRegistry.lua to vanilla 1.12 / Lua 5.0.
|
||||
--
|
||||
-- Implementation differences from the 3.3.5 source:
|
||||
-- - 1.12 has no taint or Secure-Templates system, so the entire
|
||||
-- AttributeDelegate/SetForbidden machinery for inserting event keys
|
||||
-- into the callback tables is unnecessary. Sub-tables are allocated
|
||||
-- lazily inside RegisterCallback instead.
|
||||
-- - securecallfunction/secureexecuterange don't exist; they reduce to
|
||||
-- direct calls / explicit for-loops here.
|
||||
-- - GenerateClosure doesn't exist in 1.12; provide a local one that
|
||||
-- captures registration-time varargs via SafePack-style storage.
|
||||
-- - Lua 5.0 has no select() or `wipe()`; we use the implicit `arg`
|
||||
-- table and assign-nil to clear tables.
|
||||
-- Implementation differences from the retail source:
|
||||
-- - No AttributeDelegate. Retail inserts event keys and creates deferred
|
||||
-- callback tables through a forbidden frame's OnAttributeChanged, as a
|
||||
-- taint barrier. 1.12 has no taint and no SetForbidden, so
|
||||
-- SecureInsertEvent and GetMutableCallbacksByEvent do that work directly.
|
||||
-- - TriggerEvent calls securecallfunction from its own loops, where retail
|
||||
-- passes a vararg helper to secureexecuterange. The contract is the same
|
||||
-- (an error goes to the error handler and the other callbacks still run),
|
||||
-- but every call to a vararg function builds an `arg` table in Lua 5.0,
|
||||
-- which would cost a table per callback per event.
|
||||
-- - wipe is ClassicAPI.wipe, so an addon's own global wipe cannot
|
||||
-- replace it.
|
||||
|
||||
local generateOwnerID = CreateCounter()
|
||||
local secureexecuterange = secureexecuterange;
|
||||
local securecallfunction = securecallfunction;
|
||||
local unpack = unpack;
|
||||
local error = error;
|
||||
local pairs = pairs;
|
||||
local rawset = rawset;
|
||||
local next = next;
|
||||
local wipe = ClassicAPI.wipe;
|
||||
local GetOrCreateTableEntry = GetOrCreateTableEntry;
|
||||
|
||||
local CallbackType = EnumUtil.MakeEnum("Closure", "Function")
|
||||
local CallbackType = EnumUtil.MakeEnum("Closure", "Function");
|
||||
|
||||
-- Local GenerateClosure: binds `func` plus all of `...` so a later call
|
||||
-- invokes func(boundArgs..., callArgs...). Mirrors 3.3.5's helper of the
|
||||
-- same name. SafePack/SafeUnpack semantics so embedded nils survive.
|
||||
local function GenerateClosure(func, ...)
|
||||
local boundN = arg.n
|
||||
local bound = {}
|
||||
for i = 1, boundN do bound[i] = arg[i] end
|
||||
return function(...)
|
||||
local callN = arg.n
|
||||
-- Forward bound + call args faithfully, embedded/leading nils and all.
|
||||
-- The ONLY thing that makes unpack span holes on this Lua 5.0 build is
|
||||
-- a `.n` field: unpack takes its count from getn (which returns t.n)
|
||||
-- and ignores any explicit i/j, so a `.n`-less table stops dead at the
|
||||
-- first hole (verified: unpack({1, 1, nil, 1}, 1, 4) yields just 1, 1).
|
||||
local all = {}
|
||||
local n = 0
|
||||
for i = 1, boundN do n = n + 1; all[n] = bound[i] end
|
||||
for i = 1, callN do n = n + 1; all[n] = arg[i] end
|
||||
all.n = n
|
||||
return func(unpack(all))
|
||||
end
|
||||
end
|
||||
-- Callbacks can be registered without an owner as a matter of convenience. Generally this is fine when you never
|
||||
-- intend to release the callback.
|
||||
local generateOwnerID = CreateCounter();
|
||||
|
||||
CallbackRegistryMixin = CallbackRegistryMixin or {}
|
||||
CallbackRegistryMixin = {};
|
||||
|
||||
function CallbackRegistryMixin:OnLoad()
|
||||
local callbackTables = {}
|
||||
for _, value in pairs(CallbackType) do
|
||||
callbackTables[value] = {}
|
||||
end
|
||||
self.callbackTables = callbackTables
|
||||
local callbackTables = {};
|
||||
for callbackType, value in pairs(CallbackType) do
|
||||
local callbacks = {};
|
||||
callbackTables[value] = callbacks;
|
||||
end
|
||||
self.callbackTables = callbackTables;
|
||||
|
||||
self.executingEvents = {};
|
||||
self.deferredCallbacks = {};
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:SetUndefinedEventsAllowed(allowed)
|
||||
self.isUndefinedEventAllowed = allowed
|
||||
self.isUndefinedEventAllowed = allowed;
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:GetCallbackTables()
|
||||
return self.callbackTables
|
||||
return self.callbackTables;
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:GetCallbackTable(callbackType)
|
||||
return self.callbackTables[callbackType]
|
||||
return self.callbackTables[callbackType];
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:GetCallbacksByEvent(callbackType, event)
|
||||
local callbackTable = self:GetCallbackTable(callbackType)
|
||||
return callbackTable[event]
|
||||
local callbackTable = self:GetCallbackTable(callbackType);
|
||||
return callbackTable[event];
|
||||
end
|
||||
|
||||
-- Returns either the underlying callback table or a temporary table depending
|
||||
-- on if the event is currently being dispatched. The callbacks stored in the
|
||||
-- temporary table will be merged into the underlying callback table after the
|
||||
-- dispatch is complete.
|
||||
function CallbackRegistryMixin:GetMutableCallbacksByEvent(callbackType, event)
|
||||
if securecallfunction(rawget, self.executingEvents, event) == nil then
|
||||
return self:GetCallbacksByEvent(callbackType, event);
|
||||
end
|
||||
|
||||
local deferrals = GetOrCreateTableEntry(self.deferredCallbacks, event);
|
||||
return GetOrCreateTableEntry(deferrals, callbackType);
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:HasRegistrantsForEvent(event)
|
||||
for _, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
local callbacks = callbackTable[event]
|
||||
if callbacks and next(callbacks) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
local callbacks = callbackTable[event];
|
||||
if callbacks and securecallfunction(next, callbacks) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
-- Lazy allocation of per-event sub-tables. In 3.3.5 this went through a
|
||||
-- forbidden attribute frame for taint-isolation reasons; that's a no-op
|
||||
-- consideration on 1.12, so we just assign here.
|
||||
function CallbackRegistryMixin:SecureInsertEvent(event)
|
||||
for _, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
if not callbackTable[event] then
|
||||
callbackTable[event] = {}
|
||||
end
|
||||
end
|
||||
if not self:HasRegistrantsForEvent(event) then
|
||||
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
if not callbackTable[event] then
|
||||
rawset(callbackTable, event, {});
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:RegisterCallback(event, func, owner, ...)
|
||||
if type(event) ~= "string" then
|
||||
error("CallbackRegistryMixin::RegisterCallback 'event' requires string type.")
|
||||
elseif type(func) ~= "function" then
|
||||
error("CallbackRegistryMixin::RegisterCallback 'func' requires function type.")
|
||||
else
|
||||
if owner == nil then
|
||||
owner = generateOwnerID()
|
||||
elseif type(owner) == "number" then
|
||||
error("CallbackRegistryMixin:RegisterCallback 'owner' as number is reserved internally.")
|
||||
end
|
||||
end
|
||||
if type(event) ~= "string" then
|
||||
error("CallbackRegistryMixin::RegisterCallback 'event' requires string type.");
|
||||
elseif type(func) ~= "function" then
|
||||
error("CallbackRegistryMixin::RegisterCallback 'func' requires function type.");
|
||||
else
|
||||
if owner == nil then
|
||||
owner = generateOwnerID();
|
||||
elseif type(owner) == "number" then
|
||||
error("CallbackRegistryMixin:RegisterCallback 'owner' as number is reserved internally.")
|
||||
end
|
||||
end
|
||||
|
||||
self:SecureInsertEvent(event)
|
||||
-- Taint barrier for inserting event key into callback tables.
|
||||
self:SecureInsertEvent(event);
|
||||
|
||||
for _, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
local callbacks = callbackTable[event]
|
||||
callbacks[owner] = nil
|
||||
end
|
||||
-- An owner can have a single callback per event. The simpliest way to ensure
|
||||
-- this is to remove all callbacks for the owner prior to new registration.
|
||||
self:UnregisterCallback(event, owner);
|
||||
|
||||
if arg.n > 0 then
|
||||
local callbacks = self:GetCallbacksByEvent(CallbackType.Closure, event)
|
||||
callbacks[owner] = GenerateClosure(func, owner, unpack(arg))
|
||||
else
|
||||
local callbacks = self:GetCallbacksByEvent(CallbackType.Function, event)
|
||||
callbacks[owner] = func
|
||||
end
|
||||
local count = select("#", ...);
|
||||
if count > 0 then
|
||||
local callbacks = self:GetMutableCallbacksByEvent(CallbackType.Closure, event);
|
||||
callbacks[owner] = GenerateClosure(func, owner, ...);
|
||||
else
|
||||
local callbacks = self:GetMutableCallbacksByEvent(CallbackType.Function, event);
|
||||
callbacks[owner] = func;
|
||||
end
|
||||
|
||||
return owner
|
||||
return owner;
|
||||
end
|
||||
|
||||
local function CreateCallbackHandle(cbr, event, owner)
|
||||
local handle =
|
||||
{
|
||||
Unregister = function()
|
||||
cbr:UnregisterCallback(event, owner)
|
||||
end,
|
||||
}
|
||||
return handle
|
||||
-- Wrapped in a table for future flexibility.
|
||||
local handle =
|
||||
{
|
||||
Unregister = function()
|
||||
cbr:UnregisterCallback(event, owner);
|
||||
end,
|
||||
};
|
||||
return handle;
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:RegisterCallbackWithHandle(event, func, owner, ...)
|
||||
owner = self:RegisterCallback(event, func, owner, unpack(arg))
|
||||
return CreateCallbackHandle(self, event, owner)
|
||||
owner = self:RegisterCallback(event, func, owner, ...);
|
||||
return CreateCallbackHandle(self, event, owner);
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:ReconcileDeferredCallbacks(event, closures, funcs)
|
||||
self:CopyDeferredCallbacks(CallbackType.Closure, event, closures);
|
||||
self:CopyDeferredCallbacks(CallbackType.Function, event, funcs);
|
||||
self.deferredCallbacks[event] = nil;
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:CopyDeferredCallbacks(callbackType, event, target)
|
||||
local deferrals = self.deferredCallbacks[event];
|
||||
if deferrals == nil then
|
||||
return;
|
||||
end
|
||||
|
||||
local callbacks = deferrals[callbackType];
|
||||
if callbacks == nil then
|
||||
return;
|
||||
end
|
||||
|
||||
local function ExecuteAssignCallback(owner, callback)
|
||||
target[owner] = callback;
|
||||
end
|
||||
|
||||
secureexecuterange(callbacks, ExecuteAssignCallback);
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:TriggerEvent(event, ...)
|
||||
if type(event) ~= "string" then
|
||||
error("CallbackRegistryMixin:TriggerEvent 'event' requires string type.")
|
||||
elseif not self.isUndefinedEventAllowed and not (self.Event and self.Event[event]) then
|
||||
error(string.format("CallbackRegistryMixin:TriggerEvent event '%s' doesn't exist.", event))
|
||||
end
|
||||
if type(event) ~= "string" then
|
||||
error("CallbackRegistryMixin:TriggerEvent 'event' requires string type.");
|
||||
elseif not self.isUndefinedEventAllowed and not self.Event[event] then
|
||||
error(string.format("CallbackRegistryMixin:TriggerEvent event '%s' doesn't exist.", event));
|
||||
end
|
||||
|
||||
local closures = self:GetCallbacksByEvent(CallbackType.Closure, event)
|
||||
if closures then
|
||||
for _, closure in pairs(closures) do
|
||||
closure(unpack(arg))
|
||||
end
|
||||
end
|
||||
-- TriggerEvent appears to need to support reentrant calls for now.
|
||||
local count = (self.executingEvents[event] or 0) + 1;
|
||||
|
||||
local funcs = self:GetCallbacksByEvent(CallbackType.Function, event)
|
||||
if funcs then
|
||||
for owner, func in pairs(funcs) do
|
||||
-- Stock 3.3.5 passes the owner as the first arg so callers can
|
||||
-- distinguish multiple registrations of the same function. `arg`
|
||||
-- carries its own `.n`, so unpack(arg) forwards the payload with
|
||||
-- leading/embedded nils intact. Do NOT instead merge into a
|
||||
-- `{ owner, ... }` table and unpack that WITHOUT setting `.n`: this
|
||||
-- build's unpack counts via getn, so a `.n`-less table collapses at
|
||||
-- the first hole (verified in-game: reload's PLAYER_ENTERING_WORLD,
|
||||
-- arg1=nil arg2=1, dropped the payload).
|
||||
func(owner, unpack(arg))
|
||||
end
|
||||
end
|
||||
-- Set before invoking any callback so a reentrant call does not
|
||||
-- attempt to call ReconcileDeferredCallbacks.
|
||||
self.executingEvents[event] = count;
|
||||
|
||||
local closures = self:GetCallbacksByEvent(CallbackType.Closure, event);
|
||||
if closures then
|
||||
for owner, closure in pairs(closures) do
|
||||
securecallfunction(closure, ...);
|
||||
end
|
||||
end
|
||||
|
||||
local funcs = self:GetCallbacksByEvent(CallbackType.Function, event);
|
||||
if funcs then
|
||||
for owner, func in pairs(funcs) do
|
||||
securecallfunction(func, owner, ...);
|
||||
end
|
||||
end
|
||||
|
||||
-- The local count is the only value we care about for the purpose
|
||||
-- of flushing the event key and reconciling the deferred callbacks.
|
||||
if count == 1 then
|
||||
self.executingEvents[event] = nil;
|
||||
|
||||
self:ReconcileDeferredCallbacks(event, closures, funcs);
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:UnregisterCallback(event, owner)
|
||||
if type(event) ~= "string" then
|
||||
error("CallbackRegistryMixin:UnregisterCallback 'event' requires string type.")
|
||||
elseif owner == nil then
|
||||
error("CallbackRegistryMixin:UnregisterCallback 'owner' is required.")
|
||||
end
|
||||
if type(event) ~= "string" then
|
||||
error("CallbackRegistryMixin:UnregisterCallback 'event' requires string type.");
|
||||
elseif owner == nil then
|
||||
error("CallbackRegistryMixin:UnregisterCallback 'owner' is required.");
|
||||
end
|
||||
|
||||
for _, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
local callbacks = callbackTable[event]
|
||||
if callbacks then
|
||||
callbacks[owner] = nil
|
||||
end
|
||||
end
|
||||
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
-- Only assign nil if the owner is present in the table, otherwise
|
||||
-- its insertion could cause a rehash during iteration by secureexecuterange.
|
||||
local callbacks = callbackTable[event];
|
||||
if callbacks and callbacks[owner] then
|
||||
callbacks[owner] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
local deferrals = self.deferredCallbacks[event];
|
||||
if deferrals then
|
||||
for callbackType, callbacks in pairs(deferrals) do
|
||||
-- Freely assign nil because deferredCallbacks are not subject
|
||||
-- to secureexecuterange table mutilation stress disorder.
|
||||
callbacks[owner] = nil;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:UnregisterEvents(eventTable)
|
||||
if eventTable then
|
||||
for _, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
for event in pairs(eventTable) do
|
||||
if callbackTable[event] then
|
||||
callbackTable[event] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
for _, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
for k in pairs(callbackTable) do
|
||||
callbackTable[k] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
function CallbackRegistryMixin:UnregisterEvents()
|
||||
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
wipe(callbackTable);
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:GenerateCallbackEvents(events)
|
||||
if not self.Event then
|
||||
self.Event = {}
|
||||
end
|
||||
function CallbackRegistryMixin:UnregisterEventsByEventTable(eventTable)
|
||||
if type(eventTable) ~= "table" then
|
||||
error("CallbackRegistryMixin:UnregisterEventsByEventTable 'eventTable' requires table type.");
|
||||
end
|
||||
|
||||
for eventIndex, eventName in ipairs(events) do
|
||||
if self.Event[eventName] then
|
||||
error(string.format("CallbackRegistryMixin:GenerateCallbackEvents: event '%s' already exists.", eventName))
|
||||
end
|
||||
self.Event[eventName] = eventName
|
||||
end
|
||||
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
for event in pairs(eventTable) do
|
||||
callbackTable[event] = nil;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:GenerateCallbackEvents(eventTable)
|
||||
if not self.Event then
|
||||
self.Event = {};
|
||||
end
|
||||
|
||||
if type(eventTable) ~= "table" then
|
||||
error("CallbackRegistryMixin:GenerateCallbackEvents 'eventTable' requires table type.");
|
||||
end
|
||||
|
||||
for eventIndex, eventName in ipairs(eventTable) do
|
||||
if self.Event[eventName] then
|
||||
error(string.format("CallbackRegistryMixin:GenerateCallbackEvents: event '%s' already exists.", eventName));
|
||||
end
|
||||
self.Event[eventName] = eventName;
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin.DoesFrameHaveEvent(frame, event)
|
||||
return frame.Event and frame.Event[event]
|
||||
return frame.Event and frame.Event[event];
|
||||
end
|
||||
|
||||
@@ -132,7 +132,7 @@ local function ApplyItemQualityColors()
|
||||
end
|
||||
|
||||
function CAPI_ApplyStandardColorGlobals()
|
||||
-- `C_UIColor.GetColors()` makes a color object for each of its 187 rows,
|
||||
-- `C_UIColor.GetColors()` makes a color object for each of its 331 rows,
|
||||
-- so keep the values from the first call. Each later pass is then one
|
||||
-- lookup per tag.
|
||||
if not standardColors then
|
||||
|
||||
@@ -87,11 +87,7 @@ function GetClassColoredTextForUnit(unit, text)
|
||||
end
|
||||
|
||||
function GetFactionColor(factionGroupTag)
|
||||
-- Both PLAYER_FACTION_GROUP (tag -> index) and PLAYER_FACTION_COLORS
|
||||
-- (index -> color) live in our Constants.lua. Guarded defensively in
|
||||
-- case a client variant leaves one unset.
|
||||
local group = PLAYER_FACTION_GROUP[factionGroupTag];
|
||||
return PLAYER_FACTION_COLORS and PLAYER_FACTION_COLORS[group];
|
||||
return PLAYER_FACTION_COLORS[PLAYER_FACTION_GROUP[factionGroupTag]];
|
||||
end
|
||||
|
||||
function RGBToColorCode(r, g, b)
|
||||
|
||||
@@ -117,12 +117,6 @@ FACTION_LABELS_FROM_STRING = {
|
||||
["Alliance"] = FACTION_ALLIANCE,
|
||||
}
|
||||
|
||||
PLAYER_FACTION_COLORS =
|
||||
{
|
||||
[0] = PLAYER_FACTION_COLOR_HORDE,
|
||||
[1] = PLAYER_FACTION_COLOR_ALLIANCE,
|
||||
};
|
||||
|
||||
--
|
||||
-- Glyph
|
||||
--
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
-- Backport of the bag-count globals from retail's
|
||||
-- Blizzard_UIPanels_Game/Mainline/ContainerFrame.lua.
|
||||
--
|
||||
-- Differences from the retail source:
|
||||
-- - NUM_TOTAL_BAG_FRAMES is NUM_BAG_FRAMES: there is no reagent bag.
|
||||
-- - ContainerFrame_GetContainerNumSlots gives the backpack no extra slots.
|
||||
-- Retail counts 4 more for an account without an authenticator; here
|
||||
-- the backpack's size does not depend on the account.
|
||||
|
||||
NUM_TOTAL_BAG_FRAMES = NUM_BAG_FRAMES;
|
||||
|
||||
function ContainerFrame_GetContainerNumSlots(bagId)
|
||||
if bagId == KEYRING_CONTAINER then
|
||||
local keyRingSize = GetKeyRingSize();
|
||||
return keyRingSize, keyRingSize;
|
||||
end
|
||||
|
||||
local currentNumSlots = C_Container.GetContainerNumSlots(bagId);
|
||||
local maxNumSlots = currentNumSlots;
|
||||
|
||||
return maxNumSlots, currentNumSlots;
|
||||
end
|
||||
@@ -0,0 +1,114 @@
|
||||
-- Backport of retail's Blizzard_ObjectAPI/Mainline/ContinuableContainer.lua.
|
||||
|
||||
ContinuableContainer = {};
|
||||
|
||||
--[[
|
||||
Expected usage:
|
||||
|
||||
local continuableContainer = ContinuableContainer:Create();
|
||||
local item = Item:CreateFromItemLocation(itemLoc);
|
||||
continuableContainer:AddContinuable(item);
|
||||
... add more continuables
|
||||
|
||||
continuableContainer:ContinueOnLoad(function()
|
||||
-- code to execute once all continuables are ready, could be immediate
|
||||
end);
|
||||
]]
|
||||
|
||||
--[[static]] function ContinuableContainer:Create()
|
||||
return CreateFromMixins(self);
|
||||
end
|
||||
|
||||
function ContinuableContainer:AddContinuables(tbl)
|
||||
for index, continuable in pairs(tbl) do
|
||||
self:AddContinuable(continuable);
|
||||
end
|
||||
end
|
||||
|
||||
function ContinuableContainer:AddContinuable(continuable)
|
||||
if not self.continuables or not self.evictableObjects then
|
||||
self.continuables = {};
|
||||
self.evictableObjects = {};
|
||||
|
||||
self.onContinuableLoadedCallback = function(id, success)
|
||||
self.numOutstanding = self.numOutstanding - 1;
|
||||
if not success then
|
||||
for i, evictableObject in ipairs(self.evictableObjects) do
|
||||
if evictableObject:GetItemID() == id then
|
||||
table.remove(self.evictableObjects, i);
|
||||
break;
|
||||
end
|
||||
end
|
||||
end
|
||||
self:CheckIfSatisfied();
|
||||
end;
|
||||
end
|
||||
|
||||
self.numOutstanding = self:GetNumOutstandingLoads() + 1;
|
||||
if continuable:IsDataEvictable() then
|
||||
table.insert(self.evictableObjects, continuable);
|
||||
end
|
||||
table.insert(self.continuables, continuable:ContinueWithCancelOnRecordLoad(self.onContinuableLoadedCallback));
|
||||
end
|
||||
|
||||
function ContinuableContainer:ContinueOnLoad(callbackFunction)
|
||||
if type(callbackFunction) ~= "function" then
|
||||
error("Usage: ContinuableContainer:ContinueOnLoad(callbackFunction)", 2);
|
||||
end
|
||||
|
||||
self.callbackFunction = callbackFunction;
|
||||
return self:CheckIfSatisfied();
|
||||
end
|
||||
|
||||
function ContinuableContainer:GetNumOutstandingLoads()
|
||||
return self.numOutstanding or 0;
|
||||
end
|
||||
|
||||
function ContinuableContainer:AreAnyLoadsOutstanding()
|
||||
return self:GetNumOutstandingLoads() > 0;
|
||||
end
|
||||
|
||||
function ContinuableContainer:Cancel()
|
||||
local continuables = self.continuables;
|
||||
if continuables then
|
||||
self.continuables = nil;
|
||||
self.evictableObjects = nil;
|
||||
self.numOutstanding = nil;
|
||||
|
||||
for i, continuable in ipairs(continuables) do
|
||||
continuable();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- "private"
|
||||
function ContinuableContainer:CheckIfSatisfied()
|
||||
if not self:AreAnyLoadsOutstanding() and self.callbackFunction and self:RecheckEvictableContinuables() then
|
||||
local callbackFunction = self.callbackFunction;
|
||||
self.callbackFunction = nil;
|
||||
|
||||
self.continuables = nil;
|
||||
self.evictableObjects = nil;
|
||||
|
||||
xpcall(callbackFunction, CallErrorHandler);
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function ContinuableContainer:RecheckEvictableContinuables()
|
||||
local areAllLoaded = true;
|
||||
if self.evictableObjects then
|
||||
for i, evictableObject in ipairs(self.evictableObjects) do
|
||||
if not evictableObject:IsRecordDataCached() then
|
||||
areAllLoaded = false;
|
||||
|
||||
self.numOutstanding = self.numOutstanding + 1;
|
||||
table.insert(self.continuables, evictableObject:ContinueWithCancelOnRecordLoad(self.onContinuableLoadedCallback));
|
||||
end
|
||||
end
|
||||
end
|
||||
return areAllLoaded;
|
||||
end
|
||||
@@ -1,14 +1,12 @@
|
||||
-- Backport of 3.3.5 FrameXML/EventUtil.lua to vanilla 1.12 / Lua 5.0.
|
||||
-- Backport of retail Blizzard_SharedXML/EventUtil.lua to vanilla 1.12 / Lua 5.0.
|
||||
--
|
||||
-- Implementation differences from the 3.3.5 source:
|
||||
-- - Lua 5.0 has no select(); we work off the implicit `arg` table
|
||||
-- in vararg functions.
|
||||
-- - 1.12 has no C_AddOns; the legacy global IsAddOnLoaded(name)
|
||||
-- fills the same role.
|
||||
-- - 1.12 has no secureexecuterange; queued callbacks are dispatched
|
||||
-- by a plain ipairs loop.
|
||||
-- - AreVariablesLoaded in 3.3.5 sniffs GlueParent / UIParent.firstTimeLoaded.
|
||||
-- 1.12 has neither; we track a private flag flipped on VARIABLES_LOADED.
|
||||
-- Implementation differences from the retail source:
|
||||
-- - Retail's UIParent sets UIParent.variablesLoaded and calls
|
||||
-- EventUtil.TriggerOnVariablesLoaded on VARIABLES_LOADED. 1.12's
|
||||
-- FrameXML does neither, so the frame at the bottom of this file does
|
||||
-- both.
|
||||
-- - Varargs are read from the implicit `arg` table, which every Lua 5.0
|
||||
-- vararg function already has, rather than walked with select().
|
||||
|
||||
EventUtil = {}
|
||||
|
||||
@@ -19,7 +17,7 @@ function ContinueAfterAllEventsMixin:Init(callback, ...)
|
||||
|
||||
for index = 1, arg.n do
|
||||
local event = arg[index]
|
||||
assert(C_EventUtils.IsEventValid(event), "Unknown event \"" .. event .. "\"")
|
||||
assert(C_EventUtils.IsEventValid(event), ("Unknown event \"%s\""):format(event))
|
||||
self.events[event] = false
|
||||
|
||||
local function OnEventReceived()
|
||||
@@ -50,13 +48,8 @@ function EventUtil.ContinueAfterAllEvents(callback, ...)
|
||||
obj:Init(callback, unpack(arg))
|
||||
end
|
||||
|
||||
-- Tracked locally because 1.12 has no GlueParent / UIParent.firstTimeLoaded.
|
||||
-- Bootstrap frame at the bottom of the file listens for VARIABLES_LOADED
|
||||
-- and flips the flag, then drains the queued callbacks via the
|
||||
-- TriggerOnVariablesLoaded path.
|
||||
local variablesLoaded = false
|
||||
function EventUtil.AreVariablesLoaded()
|
||||
return variablesLoaded
|
||||
return GlueParent or (UIParent and UIParent.variablesLoaded)
|
||||
end
|
||||
|
||||
local eventUtilVariablesLoadedCallbacks = {}
|
||||
@@ -69,25 +62,26 @@ function EventUtil.ContinueOnVariablesLoaded(callback)
|
||||
table.insert(eventUtilVariablesLoadedCallbacks, callback)
|
||||
end
|
||||
|
||||
local secureexecuterange = secureexecuterange
|
||||
local eventUtilContinueOnVariablesLoadedTriggered = false
|
||||
function EventUtil.TriggerOnVariablesLoaded()
|
||||
if eventUtilContinueOnVariablesLoadedTriggered then
|
||||
-- If TriggerOnVariablesLoaded has been called once already, don't do anything further.
|
||||
return
|
||||
end
|
||||
eventUtilContinueOnVariablesLoadedTriggered = true
|
||||
variablesLoaded = true
|
||||
|
||||
for _, callback in ipairs(eventUtilVariablesLoadedCallbacks) do
|
||||
secureexecuterange(eventUtilVariablesLoadedCallbacks, function(_, callback)
|
||||
callback()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function EventUtil.ContinueOnAddOnLoaded(addOnName, callback)
|
||||
local isLoadedOrLoading, isLoaded = C_AddOns.IsAddOnLoaded(addOnName)
|
||||
if isLoaded then
|
||||
callback();
|
||||
return;
|
||||
end
|
||||
local isLoadedOrLoading, isLoaded = C_AddOns.IsAddOnLoaded(addOnName)
|
||||
if isLoaded then
|
||||
callback()
|
||||
return
|
||||
end
|
||||
|
||||
EventUtil.RegisterOnceFrameEventAndCallback("ADDON_LOADED", callback, addOnName)
|
||||
end
|
||||
@@ -125,6 +119,7 @@ function CallbackHandleContainerMixin:Init()
|
||||
self.handles = {}
|
||||
end
|
||||
|
||||
-- Accepts any CBR and stores a handle to the registered callback.
|
||||
function CallbackHandleContainerMixin:RegisterCallback(cbr, event, callback, owner)
|
||||
self:AddHandle(cbr:RegisterCallbackWithHandle(event, callback, owner))
|
||||
end
|
||||
@@ -141,7 +136,7 @@ function CallbackHandleContainerMixin:Unregister()
|
||||
end
|
||||
|
||||
function CallbackHandleContainerMixin:IsEmpty()
|
||||
return table.getn(self.handles) == 0
|
||||
return #self.handles == 0
|
||||
end
|
||||
|
||||
function EventUtil.CreateCallbackHandleContainer()
|
||||
@@ -150,13 +145,13 @@ function EventUtil.CreateCallbackHandleContainer()
|
||||
return cbrHandles
|
||||
end
|
||||
|
||||
-- VARIABLES_LOADED bootstrap. Drains anything queued via
|
||||
-- ContinueOnVariablesLoaded the first time the event fires. We use a
|
||||
-- dedicated frame so we don't pollute EventRegistry's payload routing.
|
||||
-- Retail UIParent's VARIABLES_LOADED work for EventUtil: mark the variables
|
||||
-- loaded, then run what ContinueOnVariablesLoaded queued.
|
||||
do
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("VARIABLES_LOADED")
|
||||
f:SetScript("OnEvent", function()
|
||||
UIParent.variablesLoaded = true
|
||||
EventUtil.TriggerOnVariablesLoaded()
|
||||
f:UnregisterEvent("VARIABLES_LOADED")
|
||||
end)
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
-- Backport of retail's Blizzard_SharedXMLBase/ExportUtil.lua.
|
||||
|
||||
-- Utility for compressing and uncompressing data into strings for export and import.
|
||||
|
||||
ExportUtil = {};
|
||||
|
||||
local BitsPerChar = 6;
|
||||
|
||||
local function MakeBase64ConversionTable()
|
||||
local base64ConversionTable = {};
|
||||
base64ConversionTable[0] = 'A';
|
||||
for num = 1, 25 do
|
||||
table.insert(base64ConversionTable, string.char(65 + num));
|
||||
end
|
||||
|
||||
for num = 0, 25 do
|
||||
table.insert(base64ConversionTable, string.char(97 + num));
|
||||
end
|
||||
|
||||
for num = 0, 9 do
|
||||
table.insert(base64ConversionTable, tostring(num));
|
||||
end
|
||||
|
||||
table.insert(base64ConversionTable, '+');
|
||||
table.insert(base64ConversionTable, '/');
|
||||
return base64ConversionTable;
|
||||
end
|
||||
|
||||
local NumberToBase64CharConversionTable = MakeBase64ConversionTable();
|
||||
local Base64CharToNumberConversionTable = tInvert(MakeBase64ConversionTable());
|
||||
|
||||
function ExportUtil.ConvertToBase64(dataEntries)
|
||||
local exportString = "";
|
||||
local currentValue = 0;
|
||||
local currentReservedBits = 0;
|
||||
local totalBits = 0;
|
||||
for i, dataEntry in ipairs(dataEntries) do
|
||||
local remainingValue = dataEntry.value;
|
||||
local remainingRequiredBits = dataEntry.bitWidth;
|
||||
-- TODO: bit.lshift doesnt work with > 32 bits. Maybe use maxValue = 2^X instead?
|
||||
local maxValue = bit.lshift(1, remainingRequiredBits);
|
||||
if remainingValue >= maxValue then
|
||||
error(("Data entry has higher value than storable in bitWidth. (%d in %d bits)"):format(remainingValue, remainingRequiredBits));
|
||||
return "";
|
||||
end
|
||||
|
||||
totalBits = totalBits + remainingRequiredBits;
|
||||
while remainingRequiredBits > 0 do
|
||||
local spaceInCurrentValue = (BitsPerChar - currentReservedBits);
|
||||
local maxStorableValue = bit.lshift(1, spaceInCurrentValue);
|
||||
local remainder = remainingValue % maxStorableValue;
|
||||
remainingValue = bit.rshift(remainingValue, spaceInCurrentValue);
|
||||
currentValue = currentValue + bit.lshift(remainder, currentReservedBits);
|
||||
|
||||
if spaceInCurrentValue > remainingRequiredBits then
|
||||
currentReservedBits = (currentReservedBits + remainingRequiredBits) % BitsPerChar;
|
||||
remainingRequiredBits = 0;
|
||||
else
|
||||
exportString = exportString..NumberToBase64CharConversionTable[currentValue];
|
||||
currentValue = 0;
|
||||
currentReservedBits = 0;
|
||||
remainingRequiredBits = remainingRequiredBits - spaceInCurrentValue;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if currentReservedBits > 0 then
|
||||
exportString = exportString..NumberToBase64CharConversionTable[currentValue];
|
||||
end
|
||||
|
||||
return exportString;
|
||||
end
|
||||
|
||||
function ExportUtil.ConvertFromBase64(exportString)
|
||||
local dataValues = {};
|
||||
for i = 1, #exportString do
|
||||
table.insert(dataValues, Base64CharToNumberConversionTable[string.sub(exportString, i, i)]);
|
||||
end
|
||||
|
||||
return dataValues;
|
||||
end
|
||||
|
||||
function ExportUtil.MakeExportDataStream()
|
||||
return CreateAndInitFromMixin(ExportDataStreamMixin);
|
||||
end
|
||||
|
||||
function ExportUtil.MakeImportDataStream(exportString)
|
||||
return CreateAndInitFromMixin(ImportDataStreamMixin, exportString);
|
||||
end
|
||||
|
||||
|
||||
ExportDataStreamMixin = {};
|
||||
|
||||
function ExportDataStreamMixin:Init()
|
||||
self.dataEntries = {};
|
||||
end
|
||||
|
||||
function ExportDataStreamMixin:AddValue(bitWidth, value)
|
||||
table.insert(self.dataEntries, { bitWidth = bitWidth, value = value, });
|
||||
end
|
||||
|
||||
function ExportDataStreamMixin:GetExportString()
|
||||
return ExportUtil.ConvertToBase64(self.dataEntries);
|
||||
end
|
||||
|
||||
ImportDataStreamMixin = {};
|
||||
|
||||
function ImportDataStreamMixin:Init(exportString)
|
||||
self.dataValues = ExportUtil.ConvertFromBase64(exportString);
|
||||
self.currentIndex = 1;
|
||||
self.currentExtractedBits = 0;
|
||||
self.currentRemainingValue = self.dataValues[1];
|
||||
end
|
||||
|
||||
function ImportDataStreamMixin:ExtractValue(bitWidth)
|
||||
if self.currentIndex > #self.dataValues then
|
||||
return nil;
|
||||
end
|
||||
|
||||
local value = 0;
|
||||
local bitsNeeded = bitWidth;
|
||||
local extractedBits = 0;
|
||||
while bitsNeeded > 0 do
|
||||
local remainingBits = BitsPerChar - self.currentExtractedBits;
|
||||
local bitsToExtract = math.min(remainingBits, bitsNeeded);
|
||||
self.currentExtractedBits = self.currentExtractedBits + bitsToExtract;
|
||||
local maxStorableValue = bit.lshift(1, bitsToExtract);
|
||||
local remainder = self.currentRemainingValue % maxStorableValue;
|
||||
self.currentRemainingValue = bit.rshift(self.currentRemainingValue, bitsToExtract);
|
||||
value = value + bit.lshift(remainder, extractedBits);
|
||||
extractedBits = extractedBits + bitsToExtract;
|
||||
bitsNeeded = bitsNeeded - bitsToExtract;
|
||||
|
||||
if bitsToExtract < remainingBits then
|
||||
break;
|
||||
elseif bitsToExtract >= remainingBits then
|
||||
self.currentIndex = self.currentIndex + 1;
|
||||
self.currentExtractedBits = 0;
|
||||
self.currentRemainingValue = self.dataValues[self.currentIndex];
|
||||
end
|
||||
end
|
||||
|
||||
return value;
|
||||
end
|
||||
|
||||
function ImportDataStreamMixin:GetNumberOfBits()
|
||||
return BitsPerChar * getn(self.dataValues);
|
||||
end
|
||||
|
||||
@@ -1,110 +1,244 @@
|
||||
-- Backport of FrameXML/FormattingUtil.lua (feasible subset) to vanilla
|
||||
-- 1.12 / Lua 5.0.
|
||||
-- Backport of retail Blizzard_SharedXML/FormattingUtil.lua to vanilla 1.12 / Lua 5.0.
|
||||
--
|
||||
-- 5.0 notes: no `#`/`%`/string-method syntax — uses string.*, math.mod.
|
||||
-- GetMoneyString gives the text-symbol form ("12g 34s 56c"). The coin-icon form
|
||||
-- (GetCoinTextureString / C_CurrencyInfo.GetCoinTextureString) is provided by the
|
||||
-- ClassicAPI DLL in C++ (src/currency/CoinText.cpp) — like retail, where it's a
|
||||
-- C function — now that the inline-texture backport renders |T markup in 1.12.
|
||||
-- Implementation differences from the retail source:
|
||||
-- - GetMoneyString always gives the coin-icon form, retail's default. Retail
|
||||
-- switches to the text-symbol form ("12g 34s 56c") under its colorblind
|
||||
-- mode, which 1.12 does not have.
|
||||
-- - Not ported: GetCurrencyString / GetCurrenciesString (1.12 has no
|
||||
-- currencies).
|
||||
--
|
||||
-- The FrameXML format-string globals these functions consume
|
||||
-- (LARGE_NUMBER_SEPERATOR, PERCENTAGE_STRING, GOLD_AMOUNT_SYMBOL,
|
||||
-- GOLD_AMOUNT_TEXTURE, …) are defined in the locale layer (locales/enUS.lua),
|
||||
-- which loads first.
|
||||
-- The GlobalStrings these functions consume (LARGE_NUMBER_SEPERATOR,
|
||||
-- PERCENTAGE_STRING, GOLD_AMOUNT_TEXTURE, SYMBOLIC_NEGATIVE_NUMBER, ...) are
|
||||
-- defined in the locale layer (locales/*.lua), which loads first. The coin
|
||||
-- formats there crop the one 1.12 sprite sheet, UI-MoneyIcons.
|
||||
|
||||
function SplitTextIntoLines(text, delimiter)
|
||||
local lines = {};
|
||||
local startIndex = 1;
|
||||
local foundIndex = string.find(text, delimiter);
|
||||
while foundIndex do
|
||||
table.insert(lines, text:sub(startIndex, foundIndex - 1));
|
||||
startIndex = foundIndex + 2;
|
||||
foundIndex = string.find(text, delimiter, startIndex);
|
||||
end
|
||||
if startIndex <= #text then
|
||||
table.insert(lines, text:sub(startIndex));
|
||||
end
|
||||
return lines;
|
||||
end
|
||||
|
||||
function SplitTextIntoHeaderAndNonHeader(text)
|
||||
local foundIndex = string.find(text, "|n");
|
||||
if not foundIndex then
|
||||
-- There was no newline...the whole thing is a header
|
||||
return text;
|
||||
elseif #text == 2 then
|
||||
-- There was a newline, but that was all that was in the string.
|
||||
return nil;
|
||||
elseif foundIndex == 1 then
|
||||
-- There was a newline at the very beginning...the whole rest of the string is a header
|
||||
return text:sub(3);
|
||||
elseif foundIndex == #text - 1 then
|
||||
-- There was a newline at the very end...the whole rest of the string is a header
|
||||
return text:sub(1, foundIndex - 1);
|
||||
else
|
||||
-- There was a newline somewhere in the middle...everything before it is the header and everything after it is the non-header
|
||||
return text:sub(1, foundIndex - 1), text:sub(foundIndex + 2);
|
||||
end
|
||||
end
|
||||
|
||||
function FormatValueWithSign(value)
|
||||
local formatString = value < 0 and SYMBOLIC_NEGATIVE_NUMBER or SYMBOLIC_POSITIVE_NUMBER;
|
||||
return formatString:format(math.abs(value));
|
||||
end
|
||||
|
||||
function FormatLargeNumber(amount)
|
||||
amount = tostring(amount);
|
||||
local newDisplay = "";
|
||||
local strlen = amount:len();
|
||||
--Add each thing behind a comma
|
||||
for i=4, strlen, 3 do
|
||||
newDisplay = LARGE_NUMBER_SEPERATOR..amount:sub(-(i - 1), -(i - 3))..newDisplay;
|
||||
end
|
||||
--Add everything before the first comma
|
||||
newDisplay = amount:sub(1, (strlen % 3 == 0) and 3 or (strlen % 3))..newDisplay;
|
||||
return newDisplay;
|
||||
end
|
||||
|
||||
COPPER_PER_SILVER = 100;
|
||||
SILVER_PER_GOLD = 100;
|
||||
COPPER_PER_GOLD = COPPER_PER_SILVER * SILVER_PER_GOLD;
|
||||
|
||||
function FormatLargeNumber(amount)
|
||||
amount = tostring(amount);
|
||||
local newDisplay = "";
|
||||
local strlen = string.len(amount);
|
||||
-- Insert a separator before each group of three trailing digits.
|
||||
for i = 4, strlen, 3 do
|
||||
newDisplay = LARGE_NUMBER_SEPERATOR ..
|
||||
string.sub(amount, -(i - 1), -(i - 3)) .. newDisplay;
|
||||
end
|
||||
-- Prepend everything before the first separator.
|
||||
local lead = (math.mod(strlen, 3) == 0) and 3 or math.mod(strlen, 3);
|
||||
newDisplay = string.sub(amount, 1, lead) .. newDisplay;
|
||||
return newDisplay;
|
||||
local COPPER_PER_SILVER = COPPER_PER_SILVER;
|
||||
local SILVER_PER_GOLD = SILVER_PER_GOLD;
|
||||
local COPPER_PER_GOLD = COPPER_PER_GOLD;
|
||||
|
||||
MoneyStringConstants =
|
||||
{
|
||||
SeparateThousands = true,
|
||||
DontSeparateThousands = false,
|
||||
CheckGoldThreshold = true,
|
||||
DontCheckGoldThreshold = false,
|
||||
ShowZeroAsGold = true,
|
||||
ShowZeroAsLowerDenomination = false,
|
||||
};
|
||||
|
||||
function GetMoneyString(money, separateThousands, checkGoldThreshold, showZeroAsGold, color)
|
||||
local goldString, silverString, copperString;
|
||||
local gold = floor(money / (COPPER_PER_SILVER * SILVER_PER_GOLD));
|
||||
local silver = floor((money - (gold * COPPER_PER_SILVER * SILVER_PER_GOLD)) / COPPER_PER_SILVER);
|
||||
local copper = mod(money, COPPER_PER_SILVER);
|
||||
|
||||
if (separateThousands) then
|
||||
goldString = GOLD_AMOUNT_TEXTURE_STRING:format(FormatLargeNumber(gold), 0, 0);
|
||||
else
|
||||
goldString = GOLD_AMOUNT_TEXTURE:format(gold, 0, 0);
|
||||
end
|
||||
silverString = SILVER_AMOUNT_TEXTURE:format(silver, 0, 0);
|
||||
copperString = COPPER_AMOUNT_TEXTURE:format(copper, 0, 0);
|
||||
|
||||
if showZeroAsGold and ( money == 0 ) then
|
||||
return color and color:WrapTextInColorCode(goldString) or goldString;
|
||||
end
|
||||
|
||||
copper = FormatDisplayCopper(checkGoldThreshold, gold, silver, copper);
|
||||
|
||||
local moneyString = "";
|
||||
local separator = "";
|
||||
if ( gold > 0 ) then
|
||||
moneyString = goldString;
|
||||
separator = " ";
|
||||
end
|
||||
if ( silver > 0 ) then
|
||||
moneyString = moneyString..separator..silverString;
|
||||
separator = " ";
|
||||
end
|
||||
if ( copper > 0 or moneyString == "" ) then
|
||||
moneyString = moneyString..separator..copperString;
|
||||
end
|
||||
|
||||
return color and color:WrapTextInColorCode(moneyString) or moneyString;
|
||||
end
|
||||
|
||||
function FormatDisplayCopper(checkGoldThreshold, gold, silver, copper)
|
||||
return (checkGoldThreshold and gold > Constants.MoneyFormattingConstants.GOLD_REWARD_THRESHOLD_TO_HIDE_COPPER ) and 0 or copper;
|
||||
end
|
||||
|
||||
function FormatPercentage(percentage, roundToNearestInteger)
|
||||
if roundToNearestInteger then
|
||||
percentage = Round(percentage * 100);
|
||||
else
|
||||
percentage = percentage * 100;
|
||||
end
|
||||
return string.format(PERCENTAGE_STRING, percentage);
|
||||
if roundToNearestInteger then
|
||||
percentage = Round(percentage * 100);
|
||||
else
|
||||
percentage = percentage * 100;
|
||||
end
|
||||
|
||||
return PERCENTAGE_STRING:format(percentage);
|
||||
end
|
||||
|
||||
function FormatPercentageRounded(value)
|
||||
local roundToNearestInteger = true;
|
||||
return FormatPercentage(value, roundToNearestInteger);
|
||||
end
|
||||
|
||||
function FormatFraction(numerator, denominator)
|
||||
return string.format(GENERIC_FRACTION_STRING, numerator, denominator);
|
||||
return GENERIC_FRACTION_STRING:format(numerator, denominator);
|
||||
end
|
||||
|
||||
function SplitTextIntoLines(text, delimiter)
|
||||
local lines = {};
|
||||
local startIndex = 1;
|
||||
local foundIndex = string.find(text, delimiter);
|
||||
while foundIndex do
|
||||
table.insert(lines, string.sub(text, startIndex, foundIndex - 1));
|
||||
startIndex = foundIndex + 2;
|
||||
foundIndex = string.find(text, delimiter, startIndex);
|
||||
end
|
||||
if startIndex <= string.len(text) then
|
||||
table.insert(lines, string.sub(text, startIndex));
|
||||
end
|
||||
return lines;
|
||||
function GetHighlightedNumberDifferenceString(baseString, newString)
|
||||
local outputString = "";
|
||||
-- output string is being built from the new string
|
||||
local newStringIndex = 1;
|
||||
-- find a stretch of digits (including . and , because of different locales) - but has to end in a digit
|
||||
local PATTERN = "([,%.%d]*%d+)";
|
||||
local start1, end1, baseNumberString = string.find(baseString, PATTERN);
|
||||
local start2, end2, newNumberString = string.find(newString, PATTERN);
|
||||
while start1 and start2 do
|
||||
-- add from the new string until the matched spot
|
||||
outputString = outputString .. string.sub(newString, newStringIndex, start2 - 1);
|
||||
newStringIndex = end2 + 1;
|
||||
|
||||
if baseNumberString ~= newNumberString then
|
||||
-- need to remove , and . before comparing numbers because of locales
|
||||
local scrubbedBaseNumberString = gsub(baseNumberString, "[,%.]", "");
|
||||
local scrubbedNewNumberString = gsub(newNumberString, "[,%.]", "");
|
||||
local baseNumber = tonumber(scrubbedBaseNumberString);
|
||||
local newNumber = tonumber(scrubbedNewNumberString);
|
||||
if baseNumber and newNumber then
|
||||
local delta = newNumber - baseNumber;
|
||||
if delta > 0 then
|
||||
newNumberString = GREEN_FONT_COLOR_CODE..string.format(newNumberString)..FONT_COLOR_CODE_CLOSE;
|
||||
elseif delta < 0 then
|
||||
newNumberString = RED_FONT_COLOR_CODE..string.format(newNumberString)..FONT_COLOR_CODE_CLOSE;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
outputString = outputString..newNumberString;
|
||||
|
||||
start1, end1, baseNumberString = string.find(baseString, PATTERN, end1 + 1);
|
||||
start2, end2, newNumberString = string.find(newString, PATTERN, end2 + 1);
|
||||
end
|
||||
|
||||
outputString = outputString .. string.sub(newString, newStringIndex, string.len(newString));
|
||||
return outputString;
|
||||
end
|
||||
|
||||
function SplitTextIntoHeaderAndNonHeader(text)
|
||||
local foundIndex = string.find(text, "|n");
|
||||
local len = string.len(text);
|
||||
if not foundIndex then
|
||||
return text; -- no newline: whole thing is a header
|
||||
elseif len == 2 then
|
||||
return nil; -- the string was just a newline
|
||||
elseif foundIndex == 1 then
|
||||
return string.sub(text, 3); -- leading newline: rest is a header
|
||||
elseif foundIndex == len - 1 then
|
||||
return string.sub(text, 1, foundIndex - 1); -- trailing newline
|
||||
else
|
||||
return string.sub(text, 1, foundIndex - 1), string.sub(text, foundIndex + 2);
|
||||
end
|
||||
function FormatUnreadMailTooltip(tooltip, headerText, senders)
|
||||
for i, sender in ipairs(senders) do
|
||||
headerText = headerText.."\n"..sender;
|
||||
end
|
||||
|
||||
tooltip:SetText(headerText);
|
||||
end
|
||||
|
||||
-- (money, separateThousands [, checkGoldThreshold, showZeroAsGold])
|
||||
-- Text-symbol form ("12g 34s 56c"); for the coin-icon form use
|
||||
-- GetCoinTextureString below. The checkGoldThreshold arg is accepted for
|
||||
-- signature compatibility but not applied (it relied on modern Constants tables).
|
||||
function GetMoneyString(money, separateThousands, checkGoldThreshold, showZeroAsGold)
|
||||
local gold = math.floor(money / COPPER_PER_GOLD);
|
||||
local silver = math.floor(math.mod(money, COPPER_PER_GOLD) / COPPER_PER_SILVER);
|
||||
local copper = math.mod(money, COPPER_PER_SILVER);
|
||||
FormattingUtil = {};
|
||||
|
||||
local goldString = (separateThousands and FormatLargeNumber(gold) or tostring(gold)) ..
|
||||
GOLD_AMOUNT_SYMBOL;
|
||||
local silverString = silver .. SILVER_AMOUNT_SYMBOL;
|
||||
local copperString = copper .. COPPER_AMOUNT_SYMBOL;
|
||||
function FormattingUtil.GetCostString(icon, quantity, colorCode, abbreviate)
|
||||
colorCode = colorCode or HIGHLIGHT_FONT_COLOR_CODE;
|
||||
|
||||
if showZeroAsGold and money == 0 then
|
||||
return goldString;
|
||||
end
|
||||
|
||||
local moneyString = "";
|
||||
local separator = "";
|
||||
if gold > 0 then
|
||||
moneyString = goldString;
|
||||
separator = " ";
|
||||
end
|
||||
if silver > 0 then
|
||||
moneyString = moneyString .. separator .. silverString;
|
||||
separator = " ";
|
||||
end
|
||||
if copper > 0 or moneyString == "" then
|
||||
moneyString = moneyString .. separator .. copperString;
|
||||
end
|
||||
return moneyString;
|
||||
local markup = CreateTextureMarkup(icon, 64, 64, 16, 16, 0, 1, 0, 1);
|
||||
local amountString;
|
||||
if abbreviate then
|
||||
amountString = AbbreviateNumbers(quantity);
|
||||
else
|
||||
amountString = BreakUpLargeNumbers(quantity);
|
||||
end
|
||||
return ("%s%s %s|r"):format(colorCode, amountString, markup);
|
||||
end
|
||||
|
||||
function FormattingUtil.GetItemCostString(itemID, quantity, colorCode, abbreviate)
|
||||
local icon = C_Item.GetItemIconByID(itemID);
|
||||
if icon then
|
||||
return FormattingUtil.GetCostString(icon, quantity, colorCode, abbreviate);
|
||||
end
|
||||
|
||||
return "";
|
||||
end
|
||||
|
||||
function FormattingUtil.AddLeadingZeroes(digits, numZeroes, zeroesColor)
|
||||
numZeroes = numZeroes or 7;
|
||||
zeroesColor = zeroesColor or GRAY_FONT_COLOR;
|
||||
|
||||
local digitsString = tostring(digits);
|
||||
local numDigits = #digitsString;
|
||||
if numDigits < numZeroes then
|
||||
local leadingZeroes = "";
|
||||
for i = 1, (numZeroes - numDigits) do
|
||||
leadingZeroes = leadingZeroes.."0";
|
||||
end
|
||||
|
||||
leadingZeroes = zeroesColor:WrapTextInColorCode(leadingZeroes);
|
||||
|
||||
return leadingZeroes .. digits;
|
||||
end
|
||||
|
||||
return digits;
|
||||
end
|
||||
|
||||
function ReplaceGenderTokens(string, gender)
|
||||
if not strfind(string, "%$") then
|
||||
return string;
|
||||
end
|
||||
|
||||
-- This is a very simple parser that will only handle $G/$g tokens
|
||||
return gsub(string, "$[Gg]([^:]+):([^;]+);", "%"..gender);
|
||||
end
|
||||
|
||||
@@ -1,71 +1,191 @@
|
||||
-- Backport of FrameXML/FrameUtil.lua (core subset) to vanilla 1.12.
|
||||
-- Backport of retail Blizzard_SharedXMLBase/FrameUtil.lua to vanilla 1.12 / Lua 5.0.
|
||||
--
|
||||
-- Only the dependency-light, commonly-called helpers are ported. Omitted:
|
||||
-- - the fade/flash functions (UIFrameFade*/UIFrameFlash*) are already
|
||||
-- native in 1.12.
|
||||
-- - the scale/layout/secure helpers (rely on APIs absent here).
|
||||
-- Implementation differences from the retail source:
|
||||
-- - Not ported: the fade and flash functions (UIFrameFade*, UIFrameFlash*,
|
||||
-- FADEFRAMES, FLASHFRAMES). 1.12's FrameXML defines them in UIParent.lua,
|
||||
-- and UIParent's OnUpdate drives them.
|
||||
-- - Not ported: FrameUtil.RegisterFrameForEventCallbacks,
|
||||
-- UnregisterFrameForEventCallbacks and RegisterFrameForEventCallbackFields
|
||||
-- (no frame:RegisterEventCallback), and ApplyDefaultScale (no
|
||||
-- GetDefaultScale).
|
||||
-- - FrameUtil.SetParentMaintainRenderLayering does not carry the frame's
|
||||
-- window over. 1.12 has one window and no GetWindow/SetWindow.
|
||||
-- - IsScrollingMessageFrame, at the bottom, is from retail's
|
||||
-- Blizzard_SharedXML/ScrollingMessageFrame.lua.
|
||||
|
||||
FrameUtil = FrameUtil or {};
|
||||
FrameUtil = {};
|
||||
|
||||
function FrameUtil.RegisterFrameForEvents(frame, events)
|
||||
for i, event in ipairs(events) do
|
||||
frame:RegisterEvent(event);
|
||||
end
|
||||
function FrameUtil.RegisterUpdateFunction(frame, frequencySeconds, func)
|
||||
-- Prevents the OnUpdate handler from running the same frame it was
|
||||
-- removed.
|
||||
frame.canUpdate = true;
|
||||
|
||||
local elapsed = frequencySeconds;
|
||||
frame:SetScript("OnUpdate", function(self, dt)
|
||||
if self.canUpdate then
|
||||
elapsed = elapsed - dt;
|
||||
if elapsed <= 0 then
|
||||
elapsed = frequencySeconds;
|
||||
func(frame, dt);
|
||||
end
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
function FrameUtil.RegisterFrameForUnitEvents(frame, events, ...)
|
||||
for i, event in ipairs(events) do
|
||||
frame:RegisterUnitEvent(event, unpack(arg));
|
||||
end
|
||||
function FrameUtil.UnregisterUpdateFunction(frame)
|
||||
frame.canUpdate = false;
|
||||
frame:SetScript("OnUpdate", nil);
|
||||
end
|
||||
|
||||
function FrameUtil.RegisterFrameForEvents(frame, events)
|
||||
for i, event in ipairs(events) do
|
||||
frame:RegisterEvent(event);
|
||||
end
|
||||
end
|
||||
|
||||
function FrameUtil.UnregisterFrameForEvents(frame, events)
|
||||
for i, event in ipairs(events) do
|
||||
frame:UnregisterEvent(event);
|
||||
end
|
||||
for i, event in ipairs(events) do
|
||||
frame:UnregisterEvent(event);
|
||||
end
|
||||
end
|
||||
|
||||
function FrameUtil.RegisterFrameForUnitEvents(frame, events, ...)
|
||||
for i, event in ipairs(events) do
|
||||
frame:RegisterUnitEvent(event, ...);
|
||||
end
|
||||
end
|
||||
|
||||
function FrameUtil.DialogStyleGlobalMouseDown(frame, buttonName, ...)
|
||||
local mouseFoci = GetMouseFoci();
|
||||
if DoesAncestryIncludeAny(frame, mouseFoci) then
|
||||
return;
|
||||
end
|
||||
|
||||
for i = 1, select("#", ...) do
|
||||
local alternateFrame = select(i, ...);
|
||||
if DoesAncestryIncludeAny(alternateFrame, mouseFoci) then
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
frame:Hide();
|
||||
end
|
||||
|
||||
local StandardScriptHandlerSet = {
|
||||
OnLoad = true,
|
||||
OnShow = true,
|
||||
OnHide = true,
|
||||
OnEvent = true,
|
||||
OnEnter = true,
|
||||
OnLeave = true,
|
||||
OnClick = true,
|
||||
OnDragStart = true,
|
||||
OnReceiveDrag = true,
|
||||
|
||||
-- Other scripts can/should be added here as needed.
|
||||
|
||||
-- Many OnUpdates are set dynamically. Leave this off for now.
|
||||
-- OnUpdate = false,
|
||||
};
|
||||
|
||||
-- ... is a list of tables to mixin.
|
||||
function FrameUtil.SpecializeFrameWithMixins(frame, ...)
|
||||
Mixin(frame, ...);
|
||||
FrameUtil.ReflectStandardScriptHandlers(frame);
|
||||
end
|
||||
|
||||
function FrameUtil.ReflectStandardScriptHandlers(frame)
|
||||
for scriptHandlerKey, shouldBeSet in pairs(StandardScriptHandlerSet) do
|
||||
local scriptHandler = frame[scriptHandlerKey];
|
||||
if scriptHandler ~= nil then
|
||||
frame:SetScript(scriptHandlerKey, scriptHandler);
|
||||
end
|
||||
end
|
||||
|
||||
if frame.OnLoad then
|
||||
frame:OnLoad();
|
||||
end
|
||||
|
||||
if frame.OnShow and frame:IsVisible() then
|
||||
frame:OnShow();
|
||||
end
|
||||
end
|
||||
|
||||
-- This doesn't strictly speaking require a frame, but that's the likely usage of it so I'm leaving it here for now.
|
||||
function FrameUtil.RegisterForVariablesLoaded(frame, loadMethod)
|
||||
if frame.savedVariablesEventRegistered then
|
||||
error("Cannot re-register for variables loaded.");
|
||||
return;
|
||||
end
|
||||
|
||||
frame.savedVariablesEventRegistered = true;
|
||||
|
||||
local function VariablesLoadedCallback(callbackSelf)
|
||||
loadMethod(frame);
|
||||
end
|
||||
|
||||
EventUtil.ContinueOnVariablesLoaded(VariablesLoadedCallback);
|
||||
end
|
||||
|
||||
-- Only expected to return differently in glues and in-game.
|
||||
function FrameUtil.GetRootParent(frame)
|
||||
local parent = frame:GetParent();
|
||||
while parent do
|
||||
local nextParent = parent:GetParent();
|
||||
if not nextParent then
|
||||
break;
|
||||
end
|
||||
parent = nextParent;
|
||||
end
|
||||
return parent;
|
||||
local parent = frame:GetParent();
|
||||
while parent do
|
||||
local nextParent = parent:GetParent();
|
||||
if not nextParent then
|
||||
break;
|
||||
end
|
||||
parent = nextParent;
|
||||
end
|
||||
return parent;
|
||||
end
|
||||
|
||||
-- Reparents while preserving the frame's render layering.
|
||||
function FrameUtil.SetParentMaintainRenderLayering(frame, parent)
|
||||
local origStrata = frame:GetFrameStrata();
|
||||
local origFrameLevel = frame:GetFrameLevel();
|
||||
frame:SetParent(parent);
|
||||
frame:SetFrameStrata(origStrata);
|
||||
frame:SetFrameLevel(origFrameLevel);
|
||||
function FrameUtil.CreateFrame(name, parent, template)
|
||||
-- NOTE: This use of the template type is not strictly correct, but should mostly work.
|
||||
local templateInfo = C_XMLUtil.GetTemplateInfo(template);
|
||||
local frameType = templateInfo and templateInfo.type or "Frame";
|
||||
return CreateFrame(frameType, name, parent, template);
|
||||
end
|
||||
|
||||
function FrameUtil.UpdateScaleForFit(frame, extraWidth, extraHeight)
|
||||
extraWidth = extraWidth or 0;
|
||||
extraHeight = extraHeight or 0;
|
||||
|
||||
FrameUtil.UpdateScaleForFitSpecific(frame, frame:GetWidth() + extraWidth, frame:GetHeight() + extraHeight);
|
||||
end
|
||||
|
||||
function FrameUtil.UpdateScaleForFitSpecific(frame, specificWidth, specificHeight)
|
||||
frame:SetScale(1);
|
||||
|
||||
local topLevelParent = GetAppropriateTopLevelParent();
|
||||
local horizRatio = topLevelParent:GetWidth() / (specificWidth or frame:GetWidth());
|
||||
local vertRatio = topLevelParent:GetHeight() / (specificHeight or frame:GetHeight());
|
||||
|
||||
if (horizRatio < 1 or vertRatio < 1) then
|
||||
frame:SetScale(min(horizRatio, vertRatio));
|
||||
end
|
||||
end
|
||||
|
||||
function DoesAncestryInclude(ancestry, frame)
|
||||
if ancestry then
|
||||
local currentFrame = frame;
|
||||
while currentFrame do
|
||||
if currentFrame == ancestry then
|
||||
return true;
|
||||
end
|
||||
currentFrame = currentFrame:GetParent();
|
||||
end
|
||||
end
|
||||
return false;
|
||||
if ancestry then
|
||||
local currentFrame = frame;
|
||||
while currentFrame do
|
||||
if currentFrame == ancestry then
|
||||
return true;
|
||||
end
|
||||
currentFrame = currentFrame:GetParent();
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
function DoesAncestryIncludeAny(ancestry, frames)
|
||||
for _, frame in ipairs(frames) do
|
||||
if DoesAncestryInclude(ancestry, frame) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
return false;
|
||||
for _, frame in ipairs(frames) do
|
||||
if DoesAncestryInclude(ancestry, frame) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
function GetUnscaledFrameRect(frame, scale)
|
||||
@@ -79,6 +199,29 @@ function GetUnscaledFrameRect(frame, scale)
|
||||
return frameLeft / scale, frameBottom / scale, frameWidth / scale, frameHeight / scale;
|
||||
end
|
||||
|
||||
function GetScaledCenter(frame)
|
||||
local x, y = frame:GetCenter();
|
||||
local effectiveScale = frame:GetEffectiveScale();
|
||||
if x == nil or y == nil or effectiveScale == nil then
|
||||
-- Defaulted returned for diagnosing invalid rects in layout frames.
|
||||
local defaulted = true;
|
||||
return 0, 0, defaulted;
|
||||
end
|
||||
|
||||
return x * effectiveScale, y * effectiveScale;
|
||||
end
|
||||
|
||||
function FitToParent(parent, frame)
|
||||
local horizRatio = parent:GetWidth() / frame:GetWidth();
|
||||
local vertRatio = parent:GetHeight() / frame:GetHeight();
|
||||
|
||||
if ( horizRatio < 1 or vertRatio < 1 ) then
|
||||
frame:SetScale(min(horizRatio, vertRatio));
|
||||
frame:SetPoint("CENTER", 0, 0);
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
local alternateTopLevelParent;
|
||||
function SetAlternateTopLevelParent(parent)
|
||||
alternateTopLevelParent = parent;
|
||||
@@ -105,11 +248,171 @@ function SetAppropriateTopLevelParent(frame)
|
||||
frame:SetParent(parent);
|
||||
end
|
||||
end
|
||||
|
||||
function GetAppropriateTooltip()
|
||||
return UIParent and GameTooltip or GlueTooltip;
|
||||
end
|
||||
|
||||
-- Sets a frame parent, but retains old frame strata and frame level
|
||||
function FrameUtil.SetParentMaintainRenderLayering(frame, parent)
|
||||
local origStrata = frame:GetFrameStrata();
|
||||
local origFrameLevel = frame:GetFrameLevel();
|
||||
|
||||
frame:SetParent(parent);
|
||||
frame:SetFrameStrata(origStrata);
|
||||
frame:SetFrameLevel(origFrameLevel);
|
||||
end
|
||||
|
||||
-- Sets a frame to the appropriate top level parent.
|
||||
function FrameUtil.RegisterForTopLevelParentChanged(frame)
|
||||
EventRegistry:RegisterCallback("UI.AlternateTopLevelParentChanged", function (callbackFrame)
|
||||
FrameUtil.UpdateTopLevelParent(frame);
|
||||
end, frame);
|
||||
end
|
||||
|
||||
-- Sets a frame to the appropriate top level parent.
|
||||
function FrameUtil.UpdateTopLevelParent(frame)
|
||||
local oldParent = frame:GetParent();
|
||||
local newParent = GetAppropriateTopLevelParent();
|
||||
if newParent and newParent ~= oldParent then
|
||||
FrameUtil.SetParentMaintainRenderLayering(frame, newParent);
|
||||
end
|
||||
end
|
||||
|
||||
-- AnimTransitionMixin - A configurable mixin for animated transitions when showing and hiding.
|
||||
-- Alpha and/or scale could be added in the future if needed.
|
||||
|
||||
-- Mixes in and initializes the AnimTransitionMixin to a frame to give it animated show/hide capabilities.
|
||||
-- config: see DEFAULT_ANIM_TRANSITION_CONFIG for parameters.
|
||||
function FrameUtil.InitFrameAnimTransition(frame, config)
|
||||
Mixin(frame, AnimTransitionMixin);
|
||||
frame:InitAnimTransition(config);
|
||||
end
|
||||
|
||||
AnimTransitionMixinTransitionType = {
|
||||
SlideLeft = 1,
|
||||
SlideRight = 2,
|
||||
SlideTop = 3,
|
||||
SlideBottom = 4,
|
||||
};
|
||||
|
||||
AnimTransitionMixin = {};
|
||||
|
||||
local DEFAULT_ANIM_TRANSITION_CONFIG = {
|
||||
distanceX = nil, -- If nil, uses frame width
|
||||
distanceY = nil, -- If nil, uses frame height
|
||||
translationType = nil, -- Value from AnimTransitionMixinTransitionType enum
|
||||
duration = 0.3, -- Animation duration in seconds
|
||||
easingFunction = nil, -- Easing function from EasingUtil
|
||||
hideOnAnimOut = false, -- Hide the frame when out animation completes
|
||||
onAnimInFinish = nil, -- Callback when in animation finishes
|
||||
onAnimOutFinish = nil, -- Callback when out animation finishes
|
||||
};
|
||||
|
||||
-- Initialize the animation configuration for this instance
|
||||
-- config: A table with optional properties matching DEFAULT_ANIM_TRANSITION_CONFIG
|
||||
function AnimTransitionMixin:InitAnimTransition(config)
|
||||
config = config or {};
|
||||
|
||||
local defaultConfig = CopyTable(DEFAULT_ANIM_TRANSITION_CONFIG);
|
||||
self.animTransitionConfig = Mixin(defaultConfig, config);
|
||||
end
|
||||
|
||||
function AnimTransitionMixin:StartAnimIn()
|
||||
if not self.animTransitionConfig then
|
||||
error("AnimTransitionMixin: Must call InitAnimTransition before StartAnimIn");
|
||||
return;
|
||||
end
|
||||
|
||||
self:StartAnimTransition(false);
|
||||
end
|
||||
|
||||
function AnimTransitionMixin:StartAnimOut()
|
||||
if not self.animTransitionConfig then
|
||||
error("AnimTransitionMixin: Must call InitAnimTransition before StartAnimOut");
|
||||
return;
|
||||
end
|
||||
|
||||
self:StartAnimTransition(true);
|
||||
end
|
||||
|
||||
function AnimTransitionMixin:CancelAnimTransition()
|
||||
if self.animTransitionCancelFunc then
|
||||
self.animTransitionCancelFunc();
|
||||
self.animTransitionCancelFunc = nil;
|
||||
end
|
||||
end
|
||||
|
||||
-- Only meant to be used internally. Call StartAnimIn or StartAnimOut instead.
|
||||
function AnimTransitionMixin:StartAnimTransition(isOut)
|
||||
self:CancelAnimTransition();
|
||||
|
||||
local config = self.animTransitionConfig;
|
||||
|
||||
if not isOut then
|
||||
self:Show();
|
||||
end
|
||||
|
||||
local distanceX, distanceY = self:GetTranslationDeltas(config.translationType, isOut);
|
||||
|
||||
local needsClampRestore = false;
|
||||
if distanceX ~= 0 or distanceY ~= 0 then
|
||||
if self:IsClampedToScreen() then
|
||||
needsClampRestore = true;
|
||||
self:SetClampedToScreen(false);
|
||||
end
|
||||
|
||||
-- Set starting state for "in" animation (reverse of the final state)
|
||||
if not isOut then
|
||||
self:AdjustPointsOffset(-distanceX, -distanceY);
|
||||
end
|
||||
end
|
||||
|
||||
local variationCallback = ScriptAnimationUtil.GenerateEasedVariationCallback(config.easingFunction, distanceX, distanceY);
|
||||
|
||||
local function OnAnimFinish()
|
||||
if isOut and config.hideOnAnimOut then
|
||||
self:Hide();
|
||||
end
|
||||
|
||||
if needsClampRestore then
|
||||
self:SetClampedToScreen(true);
|
||||
end
|
||||
|
||||
if isOut then
|
||||
if distanceX ~= 0 or distanceY ~= 0 then
|
||||
self:AdjustPointsOffset(-distanceX, -distanceY);
|
||||
end
|
||||
end
|
||||
|
||||
self.animTransitionCancelFunc = nil;
|
||||
|
||||
local callback = isOut and config.onAnimOutFinish or config.onAnimInFinish;
|
||||
if callback then
|
||||
callback(self);
|
||||
end
|
||||
end
|
||||
|
||||
self.animTransitionCancelFunc = ScriptAnimationUtil.StartScriptAnimation(self, variationCallback, config.duration, OnAnimFinish);
|
||||
end
|
||||
|
||||
function AnimTransitionMixin:GetTranslationDeltas(translationType, isOut)
|
||||
if translationType == AnimTransitionMixinTransitionType.SlideLeft then
|
||||
local width = self.animTransitionConfig.distanceX or self:GetWidth();
|
||||
return isOut and -width or width, 0;
|
||||
elseif translationType == AnimTransitionMixinTransitionType.SlideRight then
|
||||
local width = self.animTransitionConfig.distanceX or self:GetWidth();
|
||||
return isOut and width or -width, 0;
|
||||
elseif translationType == AnimTransitionMixinTransitionType.SlideTop then
|
||||
local height = self.animTransitionConfig.distanceY or self:GetHeight();
|
||||
return 0, isOut and height or -height;
|
||||
elseif translationType == AnimTransitionMixinTransitionType.SlideBottom then
|
||||
local height = self.animTransitionConfig.distanceY or self:GetHeight();
|
||||
return 0, isOut and -height or height;
|
||||
end
|
||||
|
||||
return 0, 0;
|
||||
end
|
||||
|
||||
-- From retail's Blizzard_SharedXML/ScrollingMessageFrame.lua, where the key
|
||||
-- comes from the Lua intrinsic's XML. 1.12's scrolling message frames are
|
||||
-- native and have no such key.
|
||||
|
||||
@@ -91,6 +91,13 @@ function GameTooltip_ShowCompareItem(self, shift)
|
||||
end
|
||||
end
|
||||
|
||||
-- Retail's automatic comparison (the alwaysCompareItems cvar) skips a
|
||||
-- tooltip with this flag. Comparison here runs only when a caller asks for
|
||||
-- it, so nothing reads the flag.
|
||||
function GameTooltip_SuppressAutomaticCompareItem(tooltip)
|
||||
tooltip.suppressAutomaticCompareItem = true;
|
||||
end
|
||||
|
||||
function GameTooltip_SetTitle(tooltip, text, overrideColor, wrap)
|
||||
tooltip:ClearLines();
|
||||
GameTooltip_AddColoredLine(tooltip, text, overrideColor or HIGHLIGHT_FONT_COLOR, wrap)
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
|
||||
EventRegistry = CreateFromMixins(CallbackRegistryMixin);
|
||||
|
||||
-- Keep every instance field in OnLoad. Addons derive their own registry from
|
||||
-- this instance (pfUI: CreateFromMixins(EventRegistry) then :OnLoad()), and a
|
||||
-- field set anywhere else would be shared with the global registry.
|
||||
function EventRegistry:OnLoad()
|
||||
CallbackRegistryMixin.OnLoad(self);
|
||||
self:SetUndefinedEventsAllowed(true);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
-- Backport of retail's Blizzard_SharedXML/IndexRangeDataProvider.lua.
|
||||
|
||||
-- IndexRangeDataProviderMixin originally written for parity with ScrollBox API to
|
||||
-- simulate extremely large ranges (i.e. 20,000 macro icons). As a result, this
|
||||
-- data provider has a minimal API.
|
||||
IndexRangeDataProviderMixin = CreateFromMixins(CallbackRegistryMixin);
|
||||
|
||||
IndexRangeDataProviderMixin:GenerateCallbackEvents(
|
||||
{
|
||||
"OnSizeChanged",
|
||||
}
|
||||
);
|
||||
|
||||
function IndexRangeDataProviderMixin:Init(size)
|
||||
CallbackRegistryMixin.OnLoad(self);
|
||||
|
||||
self:SetSize(size);
|
||||
end
|
||||
|
||||
function IndexRangeDataProviderMixin:IsVirtual()
|
||||
return true;
|
||||
end
|
||||
|
||||
function IndexRangeDataProviderMixin:Enumerate(indexBegin, indexEnd)
|
||||
indexBegin = indexBegin and (indexBegin - 1) or 0;
|
||||
indexEnd = indexEnd or self.size;
|
||||
|
||||
local function Enumerator(invariant, index)
|
||||
index = index + 1;
|
||||
if index <= indexEnd then
|
||||
return index, index;
|
||||
end
|
||||
end
|
||||
|
||||
local invariant = nil;
|
||||
return Enumerator, invariant, indexBegin;
|
||||
end
|
||||
|
||||
function IndexRangeDataProviderMixin:EnumerateEntireRange()
|
||||
local indexBegin, indexEnd = nil, nil;
|
||||
return self:Enumerate(indexBegin, indexEnd);
|
||||
end
|
||||
|
||||
function IndexRangeDataProviderMixin:GetSize()
|
||||
return self.size;
|
||||
end
|
||||
|
||||
function IndexRangeDataProviderMixin:SetSize(size)
|
||||
self.size = math.max(0, size);
|
||||
|
||||
local pendingSort = false;
|
||||
self:TriggerEvent(IndexRangeDataProviderMixin.Event.OnSizeChanged, pendingSort);
|
||||
end
|
||||
|
||||
function IndexRangeDataProviderMixin:Flush()
|
||||
self:SetSize(0);
|
||||
end
|
||||
|
||||
function IndexRangeDataProviderMixin:Find(index)
|
||||
if index <= self:GetSize() then
|
||||
return index;
|
||||
end
|
||||
end
|
||||
|
||||
function IndexRangeDataProviderMixin:FindByPredicate(predicate)
|
||||
for index = 1, self:GetSize() do
|
||||
if predicate(index) then
|
||||
return index;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function IndexRangeDataProviderMixin:ContainsByPredicate(predicate)
|
||||
return self:FindByPredicate(predicate) ~= nil;
|
||||
end
|
||||
|
||||
function CreateIndexRangeDataProvider(size)
|
||||
return CreateAndInitFromMixin(IndexRangeDataProviderMixin, size or 0);
|
||||
end
|
||||
@@ -0,0 +1,354 @@
|
||||
-- Backport of retail's Blizzard_ObjectAPI/Mainline/Item.lua.
|
||||
--
|
||||
-- Differences from the retail source:
|
||||
-- - GetItemQualityColor reads ITEM_QUALITY_COLORS directly; retail's
|
||||
-- ColorManager.GetColorDataForItemQuality returns the same entry.
|
||||
|
||||
Item = {};
|
||||
ItemMixin = {};
|
||||
|
||||
--[[static]] function Item:CreateFromItemLocation(itemLocation)
|
||||
if type(itemLocation) ~= "table" or type(itemLocation.HasAnyLocation) ~= "function" or not itemLocation:HasAnyLocation() then
|
||||
error("Usage: Item:CreateFromItemLocation(notEmptyItemLocation)", 2);
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin);
|
||||
item:SetItemLocation(itemLocation);
|
||||
return item;
|
||||
end
|
||||
|
||||
--[[static]] function Item:CreateFromBagAndSlot(bagID, slotIndex)
|
||||
if type(bagID) ~= "number" or type(slotIndex) ~= "number" then
|
||||
error("Usage: Item:CreateFromBagAndSlot(bagID, slotIndex)", 2);
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin);
|
||||
item:SetItemLocation(ItemLocation:CreateFromBagAndSlot(bagID, slotIndex));
|
||||
return item;
|
||||
end
|
||||
|
||||
--[[static]] function Item:CreateFromEquipmentSlot(equipmentSlotIndex)
|
||||
if type(equipmentSlotIndex) ~= "number" then
|
||||
error("Usage: Item:CreateFromEquipmentSlot(equipmentSlotIndex)", 2);
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin);
|
||||
item:SetItemLocation(ItemLocation:CreateFromEquipmentSlot(equipmentSlotIndex));
|
||||
return item;
|
||||
end
|
||||
|
||||
--[[static]] function Item:CreateFromItemLink(itemLink)
|
||||
if type(itemLink) ~= "string" then
|
||||
error("Usage: Item:CreateFromItemLink(itemLinkString)", 2);
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin);
|
||||
item:SetItemLink(itemLink);
|
||||
return item;
|
||||
end
|
||||
|
||||
--[[static]] function Item:CreateFromItemID(itemID)
|
||||
if type(itemID) ~= "number" then
|
||||
error("Usage: Item:CreateFromItemID(itemID)", 2);
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin);
|
||||
item:SetItemID(itemID);
|
||||
return item;
|
||||
end
|
||||
|
||||
--[[static]] function Item:CreateFromItemGUID(itemGUID)
|
||||
if type(itemGUID) ~= "string" then
|
||||
error("Usage: Item:CreateFromItemGUID(itemGUIDString)", 2);
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin);
|
||||
item:SetItemGUID(itemGUID);
|
||||
return item;
|
||||
end
|
||||
|
||||
--[[static]] function Item:DoItemsMatch(item1, item2)
|
||||
if not item1 or not item2 then
|
||||
return false;
|
||||
end
|
||||
|
||||
return item1:Matches(item2);
|
||||
end
|
||||
|
||||
function ItemMixin:Matches(item)
|
||||
if not item then
|
||||
return false;
|
||||
end
|
||||
|
||||
local itemID = item:GetItemID();
|
||||
if (itemID ~= nil) and (self:GetItemID() == itemID) then
|
||||
return true;
|
||||
end
|
||||
|
||||
local itemLocation = item:GetItemLocation();
|
||||
if (itemLocation ~= nil) and itemLocation:IsEqualTo(self:GetItemLocation()) then
|
||||
return true;
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function ItemMixin:SetItemLocation(itemLocation)
|
||||
self:Clear();
|
||||
self.itemLocation = itemLocation;
|
||||
end
|
||||
|
||||
function ItemMixin:SetItemLink(itemLink)
|
||||
self:Clear();
|
||||
self.itemLink = itemLink;
|
||||
end
|
||||
|
||||
function ItemMixin:SetItemID(itemID)
|
||||
self:Clear();
|
||||
self.itemID = itemID;
|
||||
end
|
||||
|
||||
function ItemMixin:SetItemGUID(itemGUID)
|
||||
self:Clear();
|
||||
self.itemGUID = itemGUID;
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemLocation()
|
||||
if self.itemLocation then
|
||||
return self.itemLocation;
|
||||
end
|
||||
|
||||
if self.itemGUID then
|
||||
return C_Item.GetItemLocation(self.itemGUID);
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemGUID()
|
||||
if self.itemGUID then
|
||||
return self.itemGUID;
|
||||
end
|
||||
|
||||
if self.itemLocation then
|
||||
return C_Item.GetItemGUID(self.itemLocation);
|
||||
end
|
||||
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:HasItemLocation()
|
||||
return self.itemLocation ~= nil;
|
||||
end
|
||||
|
||||
function ItemMixin:Clear()
|
||||
self.itemLocation = nil;
|
||||
self.itemLink = nil;
|
||||
self.itemID = nil;
|
||||
self.itemGUID = nil;
|
||||
end
|
||||
|
||||
function ItemMixin:IsItemEmpty()
|
||||
if self:GetStaticBackingItem() then
|
||||
return not C_Item.DoesItemExistByID(self:GetStaticBackingItem());
|
||||
end
|
||||
|
||||
return not self:IsItemInPlayersControl();
|
||||
end
|
||||
|
||||
function ItemMixin:GetStaticBackingItem()
|
||||
return self.itemLink or self.itemID;
|
||||
end
|
||||
|
||||
function ItemMixin:IsItemInPlayersControl()
|
||||
local itemLocation = self:GetItemLocation();
|
||||
return itemLocation and C_Item.DoesItemExist(itemLocation);
|
||||
end
|
||||
|
||||
-- Item API
|
||||
function ItemMixin:GetItemID()
|
||||
local backingItem = self:GetStaticBackingItem();
|
||||
if backingItem then
|
||||
return C_Item.GetItemInfoInstant(backingItem) or C_Item.GetItemIDForItemInfo(backingItem);
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemID(self:GetItemLocation());
|
||||
end
|
||||
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:IsItemLocked()
|
||||
return self:IsItemInPlayersControl() and C_Item.IsLocked(self:GetItemLocation());
|
||||
end
|
||||
|
||||
function ItemMixin:LockItem()
|
||||
if self:IsItemInPlayersControl() then
|
||||
C_Item.LockItem(self:GetItemLocation());
|
||||
end
|
||||
end
|
||||
|
||||
function ItemMixin:UnlockItem()
|
||||
if self:IsItemInPlayersControl() then
|
||||
C_Item.UnlockItem(self:GetItemLocation());
|
||||
end
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemIcon() -- requires item data to be loaded
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.GetItemIconByID(self:GetStaticBackingItem());
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemIcon(self:GetItemLocation());
|
||||
end
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemName() -- requires item data to be loaded
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.GetItemNameByID(self:GetStaticBackingItem());
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemName(self:GetItemLocation());
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemLink() -- requires item data to be loaded
|
||||
if self.itemLink then
|
||||
return self.itemLink;
|
||||
end
|
||||
|
||||
if self.itemID then
|
||||
return (select(2, C_Item.GetItemInfo(self.itemID)));
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemLink(self:GetItemLocation());
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemQuality() -- requires item data to be loaded
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.GetItemQualityByID(self:GetStaticBackingItem());
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemQuality(self:GetItemLocation());
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:GetStackCount() -- requires item data to be loaded
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetStackCount(self:GetItemLocation());
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:GetCurrentItemLevel() -- requires item data to be loaded
|
||||
if self:GetStaticBackingItem() then
|
||||
return (C_Item.GetDetailedItemLevelInfo(self:GetStaticBackingItem()));
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetCurrentItemLevel(self:GetItemLocation());
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemQualityColor() -- requires item data to be loaded
|
||||
local itemQuality = self:GetItemQuality();
|
||||
return ITEM_QUALITY_COLORS[itemQuality]; -- may be nil if item data isn't loaded
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemQualityColorRGB() -- requires item data to be loaded
|
||||
local colorTbl = self:GetItemQualityColor();
|
||||
return colorTbl.color:GetRGB();
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemMaxStackSize() -- requires item data to be loaded
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.GetItemMaxStackSizeByID(self:GetStaticBackingItem());
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemMaxStackSize(self:GetItemLocation());
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:IsStackable() -- requires item data to be loaded
|
||||
local maxStackSize = self:GetItemMaxStackSize();
|
||||
return maxStackSize and maxStackSize > 1;
|
||||
end
|
||||
|
||||
function ItemMixin:GetInventoryType()
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.GetItemInventoryTypeByID(self:GetStaticBackingItem());
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemInventoryType(self:GetItemLocation());
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:GetInventoryTypeName()
|
||||
if not self:IsItemEmpty() then
|
||||
return select(4, C_Item.GetItemInfoInstant(self:GetItemID()));
|
||||
end
|
||||
end
|
||||
|
||||
function ItemMixin:IsItemDataCached()
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.IsItemDataCachedByID(self:GetStaticBackingItem());
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.IsItemDataCached(self:GetItemLocation());
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
function ItemMixin:IsDataEvictable()
|
||||
-- Item data could be evicted from the cache
|
||||
return true;
|
||||
end
|
||||
|
||||
function ItemMixin:ValidateForContinueOnItemLoad(methodName, callbackFunction)
|
||||
if type(callbackFunction) ~= "function" then
|
||||
error(("Usage: NonEmptyItem:%s(callbackFunction): invalid callbackFunction"):format(methodName), 3);
|
||||
end
|
||||
|
||||
if self:IsItemEmpty() then
|
||||
if self.itemLink then
|
||||
error(("Usage: NonEmptyItem:%s(callbackFunction) invalid itemLink: <%s>"):format(methodName, self.itemLink), 3);
|
||||
elseif self.itemID then
|
||||
error(("Usage: NonEmptyItem:%s(callbackFunction) invalid itemID: <%d>"):format(methodName, self.itemID), 3);
|
||||
end
|
||||
|
||||
error(("Usage: NonEmptyItem:%s(callbackFunction): invalid item"):format(methodName), 3);
|
||||
else
|
||||
if not self:GetItemID() then
|
||||
error(("Usage: NonEmptyItem:%s(callbackFunction): item appeared non-empty, but had invalid itemID. itemID: <%s>, itemLink: <%s>"):format(methodName, tostring(self.itemID), tostring(self.itemLink)), 3);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Add a callback to be executed when item data is loaded, if the item data is already loaded then execute it immediately
|
||||
function ItemMixin:ContinueOnItemLoad(callbackFunction)
|
||||
self:ValidateForContinueOnItemLoad("ContinueOnItemLoad", callbackFunction);
|
||||
ItemEventListener:AddCallback(self:GetItemID(), callbackFunction);
|
||||
end
|
||||
|
||||
-- Same as ContinueOnItemLoad, except it returns a function that when called will cancel the continue
|
||||
function ItemMixin:ContinueWithCancelOnItemLoad(callbackFunction)
|
||||
self:ValidateForContinueOnItemLoad("ContinueWithCancelOnItemLoad", callbackFunction);
|
||||
return ItemEventListener:AddCancelableCallback(self:GetItemID(), callbackFunction);
|
||||
end
|
||||
|
||||
-- Generic aliases for use with ContinuableContainer
|
||||
function ItemMixin:ContinueWithCancelOnRecordLoad(callbackFunction)
|
||||
return self:ContinueWithCancelOnItemLoad(callbackFunction);
|
||||
end
|
||||
|
||||
function ItemMixin:IsRecordDataCached()
|
||||
return self:IsItemDataCached();
|
||||
end
|
||||
@@ -1,415 +1,196 @@
|
||||
-- Backport of 3.3.5 FrameXML/ItemUtil.lua to vanilla 1.12 / Lua 5.0.
|
||||
-- Backport of retail's Blizzard_FrameXMLUtil/ItemUtil.lua: the ItemUtil table.
|
||||
--
|
||||
-- Implementation differences from the 3.3.5 source:
|
||||
-- - Many C_Item.* calls the modern code expects are not exposed by
|
||||
-- ClassicAPI.dll. We shim the missing ones at the top using the
|
||||
-- stock 1.12 globals (GetItemInfo, GetInventoryItemLink,
|
||||
-- GetContainerItemLink/Info). DLL-provided functions are left alone
|
||||
-- via `or` guards so we never clobber a real implementation.
|
||||
-- - Lua 5.0 has no select(); calls that previously did
|
||||
-- `select(N, GetItemInfo(...))` use named locals to peel off the
|
||||
-- return values instead.
|
||||
-- - CallErrorHandler isn't in 1.12; we route through geterrorhandler().
|
||||
-- - In 1.12 OnEvent scripts, event name + payload are exposed via the
|
||||
-- `event`, `arg1`, `arg2`... globals rather than as function args.
|
||||
-- - 1.12 has no concept of an item GUID and no public item-lock API,
|
||||
-- so those return nil / no-op.
|
||||
-- Differences from the retail source:
|
||||
-- - No ItemButtonUtil and no ItemUtil.DoesAnyItemSlotMatchItemContext.
|
||||
-- Retail sets the item context from frames this client does not have
|
||||
-- (scrapping, runeforging, soulbinds, keystones, item upgrades,
|
||||
-- recrafting, bank tabs), and the stock bag and character frames never
|
||||
-- ask for it.
|
||||
-- - No ItemUtil.CreateItemTransmogInfo or ItemTransmogInfoMixin: there is
|
||||
-- no transmogrification.
|
||||
-- - ItemUtil.GetItemDetails has no currency path: there are no currencies.
|
||||
-- - No ItemUtil.GetCraftingReagentCount: it picks between the reagent bank
|
||||
-- and the account bank, and neither exists.
|
||||
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Item = {}
|
||||
ItemMixin = {}
|
||||
|
||||
local ItemEventListener
|
||||
|
||||
--[[static]] function Item:CreateFromItemLocation(itemLocation)
|
||||
if type(itemLocation) ~= "table" or type(itemLocation.HasAnyLocation) ~= "function" or not itemLocation:HasAnyLocation() then
|
||||
error("Usage: Item:CreateFromItemLocation(notEmptyItemLocation)", 2)
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin)
|
||||
item:SetItemLocation(itemLocation)
|
||||
return item
|
||||
ItemUtil = {};
|
||||
function ItemUtil.GetItemDetails(itemLink, quantity)
|
||||
local itemName, itemRarity, itemTexture, _;
|
||||
itemName, itemLink, itemRarity, _, _, _, _, _, _, itemTexture = C_Item.GetItemInfo(itemLink);
|
||||
return itemName, itemTexture, quantity, itemRarity, itemLink;
|
||||
end
|
||||
|
||||
--[[static]] function Item:CreateFromBagAndSlot(bagID, slotIndex)
|
||||
if type(bagID) ~= "number" or type(slotIndex) ~= "number" then
|
||||
error("Usage: Item:CreateFromBagAndSlot(bagID, slotIndex)", 2)
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin)
|
||||
item:SetItemLocation(ItemLocation:CreateFromBagAndSlot(bagID, slotIndex))
|
||||
return item
|
||||
function ItemUtil.GetItemHyperlink(itemID)
|
||||
local _, link = C_Item.GetItemInfo(itemID);
|
||||
return link;
|
||||
end
|
||||
|
||||
--[[static]] function Item:CreateFromEquipmentSlot(equipmentSlotIndex)
|
||||
if type(equipmentSlotIndex) ~= "number" then
|
||||
error("Usage: Item:CreateFromEquipmentSlot(equipmentSlotIndex)", 2)
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin)
|
||||
item:SetItemLocation(ItemLocation:CreateFromEquipmentSlot(equipmentSlotIndex))
|
||||
return item
|
||||
end
|
||||
|
||||
--[[static]] function Item:CreateFromItemLink(itemLink)
|
||||
if type(itemLink) ~= "string" then
|
||||
error("Usage: Item:CreateFromItemLink(itemLinkString)", 2)
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin)
|
||||
item:SetItemLink(itemLink)
|
||||
return item
|
||||
end
|
||||
|
||||
--[[static]] function Item:CreateFromItemID(itemID)
|
||||
if type(itemID) ~= "number" then
|
||||
error("Usage: Item:CreateFromItemID(itemID)", 2)
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin)
|
||||
item:SetItemID(itemID)
|
||||
return item
|
||||
end
|
||||
|
||||
--[[static]] function Item:CreateFromItemGUID(itemGUID)
|
||||
if type(itemGUID) ~= "string" then
|
||||
error("Usage: Item:CreateFromItemGUID(itemGUIDString)", 2);
|
||||
function ItemUtil.PickupBagItem(itemLocation)
|
||||
local bag, slot = itemLocation:GetBagAndSlot();
|
||||
if bag and slot then
|
||||
C_Container.PickupContainerItem(bag, slot);
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin);
|
||||
item:SetItemGUID(itemGUID);
|
||||
return item;
|
||||
end
|
||||
|
||||
--[[static]] function Item:DoItemsMatch(item1, item2)
|
||||
if not item1 or not item2 then
|
||||
return false;
|
||||
function ItemUtil.IterateBagSlots(bag, callback)
|
||||
-- Only includes the backpack and held bag slots.
|
||||
for slot = 1, ContainerFrame_GetContainerNumSlots(bag) do
|
||||
local itemLocation = ItemLocation:CreateFromBagAndSlot(bag, slot);
|
||||
if C_Item.DoesItemExist(itemLocation) then
|
||||
if callback(itemLocation) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
function ItemUtil.IterateInventorySlots(firstSlot, lastSlot, callback)
|
||||
for slot = firstSlot, lastSlot do
|
||||
local itemLocation = ItemLocation:CreateFromEquipmentSlot(slot);
|
||||
if C_Item.DoesItemExist(itemLocation) then
|
||||
if callback(itemLocation) then
|
||||
return;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ItemUtil.IteratePlayerInventory(callback)
|
||||
-- Only includes the backpack and held bag slots.
|
||||
for bag = Enum.BagIndex.Backpack, NUM_TOTAL_BAG_FRAMES do
|
||||
if ItemUtil.IterateBagSlots(bag, callback) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
return item1:Matches(item2);
|
||||
end
|
||||
|
||||
function ItemMixin:Matches(item)
|
||||
if not item then
|
||||
return false;
|
||||
end
|
||||
|
||||
local itemID = item:GetItemID();
|
||||
if (itemID ~= nil) and (self:GetItemID() == itemID) then
|
||||
return true;
|
||||
end
|
||||
|
||||
local itemLocation = item:GetItemLocation();
|
||||
if (itemLocation ~= nil) and itemLocation:IsEqualTo(self:GetItemLocation()) then
|
||||
return true;
|
||||
if(C_ActionBar.ShouldShowKeyring()) then
|
||||
if ItemUtil.IterateBagSlots(KEYRING_CONTAINER, callback) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function ItemMixin:SetItemLocation(itemLocation)
|
||||
self:Clear()
|
||||
self.itemLocation = itemLocation
|
||||
function ItemUtil.IteratePlayerInventoryAndEquipment(callback)
|
||||
local found = ItemUtil.IteratePlayerInventory(callback);
|
||||
if not found then
|
||||
ItemUtil.IterateInventorySlots(INVSLOT_FIRST_EQUIPPED, INVSLOT_LAST_EQUIPPED, callback);
|
||||
end
|
||||
end
|
||||
|
||||
function ItemMixin:SetItemLink(itemLink)
|
||||
self:Clear()
|
||||
self.itemLink = itemLink
|
||||
function ItemUtil.FilterOwnedItems(itemIDs)
|
||||
local found = {};
|
||||
ItemUtil.IteratePlayerInventory(function(itemLocation)
|
||||
local itemID = C_Item.GetItemID(itemLocation);
|
||||
TableUtil.TrySet(found, itemID);
|
||||
end);
|
||||
|
||||
local filtered = {};
|
||||
for index, itemID in ipairs(itemIDs) do
|
||||
if found[itemID] then
|
||||
table.insert(filtered, itemID);
|
||||
end
|
||||
end
|
||||
return filtered;
|
||||
end
|
||||
|
||||
function ItemMixin:SetItemID(itemID)
|
||||
self:Clear()
|
||||
self.itemID = itemID
|
||||
function ItemUtil.TransformItemIDsToItems(itemIDs)
|
||||
local items = {};
|
||||
for index, itemID in ipairs(itemIDs) do
|
||||
table.insert(items, Item:CreateFromItemID(itemID));
|
||||
end
|
||||
return items;
|
||||
end
|
||||
|
||||
function ItemMixin:SetItemGUID(itemGUID)
|
||||
self:Clear();
|
||||
self.itemGUID = itemGUID;
|
||||
function ItemUtil.TransformItemLocationsToItems(itemLocations)
|
||||
local items = {};
|
||||
for index, itemLocation in ipairs(itemLocations) do
|
||||
table.insert(items, Item:CreateFromItemLocation(itemLocation));
|
||||
end
|
||||
return items;
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemLocation()
|
||||
if self.itemLocation then
|
||||
return self.itemLocation;
|
||||
function ItemUtil.TransformItemGUIDsToItems(itemGUIDs)
|
||||
local items = {};
|
||||
for index, itemGUID in ipairs(itemGUIDs) do
|
||||
local item = Item:CreateFromItemGUID(itemGUID);
|
||||
table.insert(items, item);
|
||||
end
|
||||
return items;
|
||||
end
|
||||
|
||||
function ItemUtil.TransformItemLocationItemsToGUIDItems(itemLocations)
|
||||
local items = {};
|
||||
for index, itemLocation in ipairs(itemLocations) do
|
||||
table.insert(items, Item:CreateFromItemLocation(itemLocation));
|
||||
end
|
||||
return items;
|
||||
end
|
||||
|
||||
function ItemUtil.GetValidatedItemLocation(equipSlot)
|
||||
if equipSlot then
|
||||
local itemLocation = ItemLocation:CreateFromEquipmentSlot(equipSlot);
|
||||
if itemLocation:IsValid() then
|
||||
return itemLocation;
|
||||
end
|
||||
end
|
||||
|
||||
if self.itemGUID then
|
||||
return C_Item.GetItemLocation(self.itemGUID);
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:HasItemLocation()
|
||||
return self.itemLocation ~= nil
|
||||
end
|
||||
|
||||
function ItemMixin:Clear()
|
||||
self.itemLocation = nil
|
||||
self.itemLink = nil
|
||||
self.itemID = nil
|
||||
self.itemGUID = nil
|
||||
end
|
||||
|
||||
function ItemMixin:IsItemEmpty()
|
||||
if self:GetStaticBackingItem() then
|
||||
return not C_Item.DoesItemExistByID(self:GetStaticBackingItem())
|
||||
end
|
||||
|
||||
return not self:IsItemInPlayersControl()
|
||||
end
|
||||
|
||||
function ItemMixin:GetStaticBackingItem()
|
||||
return self.itemLink or self.itemID
|
||||
end
|
||||
|
||||
function ItemMixin:IsItemInPlayersControl()
|
||||
local itemLocation = self:GetItemLocation()
|
||||
return itemLocation and C_Item.DoesItemExist(itemLocation)
|
||||
end
|
||||
|
||||
-- Item API
|
||||
function ItemMixin:GetItemID()
|
||||
if self:GetStaticBackingItem() then
|
||||
return (C_Item.GetItemInfoInstant(self:GetStaticBackingItem()))
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemID(self:GetItemLocation())
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function ItemMixin:IsItemLocked()
|
||||
return self:IsItemInPlayersControl() and C_Item.IsLocked(self:GetItemLocation())
|
||||
end
|
||||
|
||||
function ItemMixin:LockItem()
|
||||
if self:IsItemInPlayersControl() then
|
||||
C_Item.LockItem(self:GetItemLocation())
|
||||
end
|
||||
end
|
||||
|
||||
function ItemMixin:UnlockItem()
|
||||
if self:IsItemInPlayersControl() then
|
||||
C_Item.UnlockItem(self:GetItemLocation())
|
||||
end
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemIcon() -- requires item data to be loaded
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.GetItemIconByID(self:GetStaticBackingItem())
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemIcon(self:GetItemLocation())
|
||||
end
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemName() -- requires item data to be loaded
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.GetItemNameByID(self:GetStaticBackingItem())
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemName(self:GetItemLocation())
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemLink() -- requires item data to be loaded
|
||||
if self.itemLink then
|
||||
return self.itemLink
|
||||
end
|
||||
|
||||
if self.itemID then
|
||||
return (select(2, C_Item.GetItemInfo(self.itemID)));
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemLink(self:GetItemLocation())
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemQuality() -- requires item data to be loaded
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.GetItemQualityByID(self:GetStaticBackingItem())
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemQuality(self:GetItemLocation())
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function ItemMixin:GetCurrentItemLevel() -- requires item data to be loaded
|
||||
if self:GetStaticBackingItem() then
|
||||
return (C_Item.GetDetailedItemLevelInfo(self:GetStaticBackingItem()))
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetCurrentItemLevel(self:GetItemLocation())
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemQualityColor() -- requires item data to be loaded
|
||||
local itemQuality = self:GetItemQuality()
|
||||
return ITEM_QUALITY_COLORS[itemQuality] -- may be nil if item data isn't loaded
|
||||
end
|
||||
|
||||
|
||||
function ItemMixin:GetItemQualityColorRGB() -- requires item data to be loaded
|
||||
local colorTbl = self:GetItemQualityColor()
|
||||
return colorTbl.color:GetRGB()
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemMaxStackSize() -- requires item data to be loaded
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.GetItemMaxStackSizeByID(self:GetStaticBackingItem());
|
||||
function ItemUtil.GetEmptyEquipSlotTooltipForSlotName(equipSlotName)
|
||||
if equipSlotName then
|
||||
return _G[strupper(equipSlotName)];
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemMaxStackSize(self:GetItemLocation());
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:IsStackable() -- requires item data to be loaded
|
||||
local maxStackSize = self:GetItemMaxStackSize();
|
||||
return maxStackSize and maxStackSize > 1;
|
||||
function ItemUtil.GetEmptyEquipSlotTooltip(equipSlot)
|
||||
if equipSlot then
|
||||
local _, _, _, slotName = C_PaperDollInfo.GetInventorySlotInfoForInvSlot(equipSlot);
|
||||
return ItemUtil.GetEmptyEquipSlotTooltipForSlotName(slotName);
|
||||
end
|
||||
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:GetInventoryType()
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.GetItemInventoryTypeByID(self:GetStaticBackingItem())
|
||||
end
|
||||
function ItemUtil.DisplayEquipSlotTooltip(frame, tooltip, equipSlot, suppressComparison, checkRelic)
|
||||
if suppressComparison then
|
||||
GameTooltip_SuppressAutomaticCompareItem(tooltip);
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemInventoryType(self:GetItemLocation())
|
||||
end
|
||||
return nil
|
||||
local hasItem, hasCooldown, repairCost = tooltip:SetInventoryItem("player", equipSlot);
|
||||
if not hasItem then
|
||||
-- This SetOwner is needed because calling SetInventoryItem now hides tooltip if there is no item
|
||||
tooltip:SetOwner(frame, "ANCHOR_RIGHT");
|
||||
|
||||
local asRelic = checkRelic and UnitHasRelicSlot("player");
|
||||
if asRelic then
|
||||
tooltip:SetText(RELICSLOT);
|
||||
else
|
||||
local text = ItemUtil.GetEmptyEquipSlotTooltip(equipSlot);
|
||||
tooltip:SetText(text);
|
||||
tooltip:Show();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemGUID()
|
||||
if self:GetStaticBackingItem() then
|
||||
return nil
|
||||
end
|
||||
function ItemUtil.GetEquipSlotTexture(equipSlot)
|
||||
-- First try to look up the equipped item's texture from the equipped item in the slot
|
||||
-- If there's no equipment there, then use the paper doll slot texture for an empty slot
|
||||
-- If this cooldown item doesn't refer to equipment, don't return a texture.
|
||||
if equipSlot then
|
||||
local itemLocation = ItemUtil.GetValidatedItemLocation(equipSlot);
|
||||
if itemLocation then
|
||||
local icon = C_Item.GetItemIcon(itemLocation);
|
||||
return icon;
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemGUID(self:GetItemLocation())
|
||||
end
|
||||
return nil
|
||||
end
|
||||
local _, icon = C_PaperDollInfo.GetInventorySlotInfoForInvSlot(equipSlot);
|
||||
return icon;
|
||||
end
|
||||
|
||||
function ItemMixin:GetInventoryTypeName()
|
||||
if not self:IsItemEmpty() then
|
||||
return select(4, C_Item.GetItemInfoInstant(self:GetItemID()));
|
||||
end
|
||||
end
|
||||
|
||||
function ItemMixin:IsItemDataCached()
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.IsItemDataCachedByID(self:GetStaticBackingItem())
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.IsItemDataCached(self:GetItemLocation())
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function ItemMixin:IsDataEvictable()
|
||||
return true
|
||||
end
|
||||
|
||||
-- Default error handler routing: stock 1.12 has no CallErrorHandler.
|
||||
local function ItemUtilErrorHandler(err)
|
||||
local h = geterrorhandler and geterrorhandler()
|
||||
if h then return h(err) end
|
||||
end
|
||||
|
||||
function ItemMixin:ContinueOnItemLoad(callbackFunction)
|
||||
if type(callbackFunction) ~= "function" or self:IsItemEmpty() then
|
||||
error("Usage: NonEmptyItem:ContinueOnLoad(callbackFunction)", 2)
|
||||
end
|
||||
|
||||
ItemEventListener:AddCallback(self:GetItemID(), callbackFunction)
|
||||
end
|
||||
|
||||
function ItemMixin:ContinueWithCancelOnItemLoad(callbackFunction)
|
||||
if type(callbackFunction) ~= "function" or self:IsItemEmpty() then
|
||||
error("Usage: NonEmptyItem:ContinueWithCancelOnItemLoad(callbackFunction)", 2)
|
||||
end
|
||||
|
||||
return ItemEventListener:AddCancelableCallback(self:GetItemID(), callbackFunction)
|
||||
end
|
||||
|
||||
--[ Item Event Listener ]
|
||||
|
||||
ItemEventListener = CreateFrame("Frame")
|
||||
ItemEventListener.callbacks = {}
|
||||
|
||||
-- 1.12 OnEvent receives no function args; event name and payload live in
|
||||
-- the `event`, `arg1`, ... globals.
|
||||
ItemEventListener:SetScript("OnEvent", function()
|
||||
if event == "ITEM_DATA_LOAD_RESULT" then
|
||||
local itemID, success = arg1, arg2
|
||||
if success then
|
||||
ItemEventListener:FireCallbacks(itemID)
|
||||
else
|
||||
ItemEventListener:ClearCallbacks(itemID)
|
||||
end
|
||||
end
|
||||
end)
|
||||
ItemEventListener:RegisterEvent("ITEM_DATA_LOAD_RESULT")
|
||||
|
||||
local CANCELED_SENTINEL = -1
|
||||
|
||||
function ItemEventListener:AddCallback(itemID, callbackFunction)
|
||||
local callbacks = self:GetOrCreateCallbacks(itemID)
|
||||
table.insert(callbacks, callbackFunction)
|
||||
C_Item.RequestLoadItemDataByID(itemID)
|
||||
end
|
||||
|
||||
function ItemEventListener:AddCancelableCallback(itemID, callbackFunction)
|
||||
local callbacks = self:GetOrCreateCallbacks(itemID)
|
||||
table.insert(callbacks, callbackFunction)
|
||||
C_Item.RequestLoadItemDataByID(itemID)
|
||||
|
||||
local index = table.getn(callbacks)
|
||||
return function()
|
||||
if table.getn(callbacks) > 0 and callbacks[index] ~= CANCELED_SENTINEL then
|
||||
callbacks[index] = CANCELED_SENTINEL
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function ItemEventListener:FireCallbacks(itemID)
|
||||
local callbacks = self:GetCallbacks(itemID)
|
||||
if callbacks then
|
||||
self:ClearCallbacks(itemID)
|
||||
for i, callback in ipairs(callbacks) do
|
||||
if callback ~= CANCELED_SENTINEL then
|
||||
xpcall(callback, ItemUtilErrorHandler)
|
||||
end
|
||||
end
|
||||
|
||||
for i = table.getn(callbacks), 1, -1 do
|
||||
callbacks[i] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ItemEventListener:ClearCallbacks(itemID)
|
||||
self.callbacks[itemID] = nil
|
||||
end
|
||||
|
||||
function ItemEventListener:GetCallbacks(itemID)
|
||||
return self.callbacks[itemID]
|
||||
end
|
||||
|
||||
function ItemEventListener:GetOrCreateCallbacks(itemID)
|
||||
local callbacks = self.callbacks[itemID]
|
||||
if not callbacks then
|
||||
callbacks = {}
|
||||
self.callbacks[itemID] = callbacks
|
||||
end
|
||||
return callbacks
|
||||
return nil;
|
||||
end
|
||||
|
||||
@@ -78,8 +78,8 @@ function DeltaLerp(startValue, endValue, amount, timeSec)
|
||||
return Lerp(startValue, endValue, Saturate(amount * timeSec * TARGET_FRAME_PER_SEC))
|
||||
end
|
||||
|
||||
function FrameDeltaLerp(startValue, endValue, amount, elapsed)
|
||||
return DeltaLerp(startValue, endValue, amount, elapsed)
|
||||
function FrameDeltaLerp(startValue, endValue, amount)
|
||||
return DeltaLerp(startValue, endValue, amount, GetTickTime())
|
||||
end
|
||||
|
||||
function RandomFloatInRange(minValue, maxValue)
|
||||
|
||||
@@ -0,0 +1,464 @@
|
||||
-- Backport of retail's Blizzard_SharedXML/MixinUtil.lua.
|
||||
-- The one difference: wipe is ClassicAPI.wipe, so an addon's own global
|
||||
-- wipe cannot replace it.
|
||||
|
||||
local wipe = ClassicAPI.wipe;
|
||||
|
||||
-- The following function is used with permission from Daniel Stephens
|
||||
-- texture - Texture
|
||||
-- canvasFrame - Canvas Frame (for anchoring)
|
||||
-- startX,startY - Coordinate of start of line
|
||||
-- endX,endY - Coordinate of end of line
|
||||
-- lineWidth - Width of line
|
||||
-- relPoint - Relative point on canvas to interpret coords (Default BOTTOMLEFT)
|
||||
function DrawLine(texture, canvasFrame, startX, startY, endX, endY, lineWidth, lineFactor, relPoint)
|
||||
if (not relPoint) then relPoint = "BOTTOMLEFT"; end
|
||||
lineFactor = lineFactor * .5;
|
||||
|
||||
-- Determine dimensions and center point of line
|
||||
local dx,dy = endX - startX, endY - startY;
|
||||
local cx,cy = (startX + endX) / 2, (startY + endY) / 2;
|
||||
|
||||
-- Normalize direction if necessary
|
||||
if (dx < 0) then
|
||||
dx,dy = -dx,-dy;
|
||||
end
|
||||
|
||||
-- Calculate actual length of line
|
||||
local lineLength = sqrt((dx * dx) + (dy * dy));
|
||||
|
||||
-- Quick escape if it'sin zero length
|
||||
if (lineLength == 0) then
|
||||
texture:SetTexCoord(0,0,0,0,0,0,0,0);
|
||||
texture:SetPoint("BOTTOMLEFT", canvasFrame, relPoint, cx,cy);
|
||||
texture:SetPoint("TOPRIGHT", canvasFrame, relPoint, cx,cy);
|
||||
return;
|
||||
end
|
||||
|
||||
-- Sin and Cosine of rotation, and combination (for later)
|
||||
local sin, cos = -dy / lineLength, dx / lineLength;
|
||||
local sinCos = sin * cos;
|
||||
|
||||
-- Calculate bounding box size and texture coordinates
|
||||
local boundingWidth, boundingHeight, bottomLeftX, bottomLeftY, topLeftX, topLeftY, topRightX, topRightY, bottomRightX, bottomRightY;
|
||||
if (dy >= 0) then
|
||||
boundingWidth = ((lineLength * cos) - (lineWidth * sin)) * lineFactor;
|
||||
boundingHeight = ((lineWidth * cos) - (lineLength * sin)) * lineFactor;
|
||||
|
||||
bottomLeftX = (lineWidth / lineLength) * sinCos;
|
||||
bottomLeftY = sin * sin;
|
||||
bottomRightY = (lineLength / lineWidth) * sinCos;
|
||||
bottomRightX = 1 - bottomLeftY;
|
||||
|
||||
topLeftX = bottomLeftY;
|
||||
topLeftY = 1 - bottomRightY;
|
||||
topRightX = 1 - bottomLeftX;
|
||||
topRightY = bottomRightX;
|
||||
else
|
||||
boundingWidth = ((lineLength * cos) + (lineWidth * sin)) * lineFactor;
|
||||
boundingHeight = ((lineWidth * cos) + (lineLength * sin)) * lineFactor;
|
||||
|
||||
bottomLeftX = sin * sin;
|
||||
bottomLeftY = -(lineLength / lineWidth) * sinCos;
|
||||
bottomRightX = 1 + (lineWidth / lineLength) * sinCos;
|
||||
bottomRightY = bottomLeftX;
|
||||
|
||||
topLeftX = 1 - bottomRightX;
|
||||
topLeftY = 1 - bottomLeftX;
|
||||
topRightY = 1 - bottomLeftY;
|
||||
topRightX = topLeftY;
|
||||
end
|
||||
|
||||
-- Set texture coordinates and anchors
|
||||
texture:ClearAllPoints();
|
||||
texture:SetTexCoord(topLeftX, topLeftY, bottomLeftX, bottomLeftY, topRightX, topRightY, bottomRightX, bottomRightY);
|
||||
texture:SetPoint("BOTTOMLEFT", canvasFrame, relPoint, cx - boundingWidth, cy - boundingHeight);
|
||||
texture:SetPoint("TOPRIGHT", canvasFrame, relPoint, cx + boundingWidth, cy + boundingHeight);
|
||||
end
|
||||
|
||||
-- Mix this into a Texture to be able to treat it like a line
|
||||
LineMixin = {};
|
||||
|
||||
function LineMixin:SetStartPoint(x, y)
|
||||
self.startX, self.startY = x, y;
|
||||
end
|
||||
|
||||
function LineMixin:SetEndPoint(x, y)
|
||||
self.endX, self.endY = x, y;
|
||||
end
|
||||
|
||||
function LineMixin:SetThickness(thickness)
|
||||
self.thickness = thickness;
|
||||
end
|
||||
|
||||
function LineMixin:Draw()
|
||||
local parent = self:GetParent();
|
||||
local x, y = parent:GetLeft(), parent:GetBottom();
|
||||
|
||||
self:ClearAllPoints();
|
||||
DrawLine(self, parent, self.startX - x, self.startY - y, self.endX - x, self.endY - y, self.thickness or 32, 1);
|
||||
end
|
||||
|
||||
-- Mix this into a FontString to have it animate towards its value, call UpdateAnimatedValue every frame
|
||||
AnimatedNumericFontStringMixin = {};
|
||||
|
||||
-- How long should it take to animate
|
||||
function AnimatedNumericFontStringMixin:SetAnimatedDurationTimeSec(animatedDurationTimeSec)
|
||||
self.animatedDurationTimeSec = animatedDurationTimeSec;
|
||||
end
|
||||
|
||||
function AnimatedNumericFontStringMixin:GetAnimatedDurationTimeSec()
|
||||
return self.animatedDurationTimeSec or 1.0;
|
||||
end
|
||||
|
||||
function AnimatedNumericFontStringMixin:SetAnimatedValue(value)
|
||||
self.targetAnimatedValue = value;
|
||||
if not self.currentAnimatedValue then
|
||||
self.currentAnimatedValue = self.targetAnimatedValue;
|
||||
end
|
||||
self.initialAnimatedValueDelta = math.abs(self.targetAnimatedValue - self.currentAnimatedValue);
|
||||
end
|
||||
|
||||
-- Stops animating the value and just snaps to it
|
||||
function AnimatedNumericFontStringMixin:SnapToTarget()
|
||||
if self.targetAnimatedValue then
|
||||
self:SetText(BreakUpLargeNumbers(Round(self.targetAnimatedValue)));
|
||||
self.currentAnimatedValue = self.targetAnimatedValue;
|
||||
self.targetAnimatedValue = nil;
|
||||
end
|
||||
end
|
||||
|
||||
-- Call this every frame
|
||||
function AnimatedNumericFontStringMixin:UpdateAnimatedValue(elapsed)
|
||||
if self.targetAnimatedValue then
|
||||
local change = self.initialAnimatedValueDelta * (elapsed / self:GetAnimatedDurationTimeSec());
|
||||
if math.abs(self.targetAnimatedValue - self.currentAnimatedValue) <= change then
|
||||
self:SnapToTarget();
|
||||
else
|
||||
local direction = self.targetAnimatedValue > self.currentAnimatedValue and 1 or -1;
|
||||
self.currentAnimatedValue = self.currentAnimatedValue + direction * change;
|
||||
|
||||
self:SetText(BreakUpLargeNumbers(Round(self.currentAnimatedValue)));
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
SparseGridMixin = {};
|
||||
|
||||
function SparseGridMixin:OnLoad(width, height)
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
self.cells = {};
|
||||
end
|
||||
|
||||
function SparseGridMixin:SetCell(x, y, data)
|
||||
if not self:IsInRange(x, y) then
|
||||
error("index of out of range");
|
||||
end
|
||||
|
||||
local linearIndex = self:CalculateLinearIndex(x, y);
|
||||
self.cells[linearIndex] = data;
|
||||
end
|
||||
|
||||
function SparseGridMixin:GetCell(x, y)
|
||||
if not self:IsInRange(x, y) then
|
||||
error("index of out of range");
|
||||
end
|
||||
|
||||
local linearIndex = self:CalculateLinearIndex(x, y);
|
||||
return self.cells[linearIndex];
|
||||
end
|
||||
|
||||
function SparseGridMixin:IsInRange(x, y)
|
||||
return x > 0 and x <= self.width and y > 0 and y <= self.height;
|
||||
end
|
||||
|
||||
function SparseGridMixin:Clear()
|
||||
wipe(self.cells);
|
||||
end
|
||||
|
||||
function SparseGridMixin:CalculateLinearIndex(x, y)
|
||||
return x + self.width * (y - 1);
|
||||
end
|
||||
|
||||
-- Mix this in to make an object a doublyLinkedList
|
||||
DoublyLinkedListMixin = {nodeCount = 0};
|
||||
|
||||
function DoublyLinkedListMixin:PushFront(nodeToInsert)
|
||||
self:InsertBefore(nodeToInsert, self.front);
|
||||
end
|
||||
|
||||
function DoublyLinkedListMixin:PushBack(nodeToInsert)
|
||||
self:InsertAfter(nodeToInsert, self.back);
|
||||
end
|
||||
|
||||
function DoublyLinkedListMixin:PopFront()
|
||||
return self:Remove(self.front);
|
||||
end
|
||||
|
||||
function DoublyLinkedListMixin:PopBack()
|
||||
return self:Remove(self.back);
|
||||
end
|
||||
|
||||
function DoublyLinkedListMixin:EnumerateNodes()
|
||||
local index = 0;
|
||||
local currentNode = self.front;
|
||||
|
||||
return function()
|
||||
index = index + 1;
|
||||
if currentNode then
|
||||
local returnNode = currentNode;
|
||||
currentNode = currentNode.next;
|
||||
return index, returnNode;
|
||||
else
|
||||
return nil;
|
||||
end
|
||||
end;
|
||||
end
|
||||
|
||||
function DoublyLinkedListMixin:EnumerateNodesReverse()
|
||||
local index = 0;
|
||||
local currentNode = self.back;
|
||||
|
||||
return function()
|
||||
index = index + 1;
|
||||
if currentNode then
|
||||
local returnNode = currentNode;
|
||||
currentNode = currentNode.prev;
|
||||
return index, returnNode;
|
||||
else
|
||||
return nil;
|
||||
end
|
||||
end;
|
||||
end
|
||||
|
||||
function DoublyLinkedListMixin:InsertBefore(nodeToInsert, listNode)
|
||||
nodeToInsert.next = listNode;
|
||||
|
||||
if listNode then
|
||||
if listNode.prev then
|
||||
listNode.prev.next = nodeToInsert;
|
||||
end
|
||||
|
||||
nodeToInsert.prev = listNode.prev;
|
||||
listNode.prev = nodeToInsert;
|
||||
end
|
||||
|
||||
if self.front == listNode then
|
||||
self.front = nodeToInsert;
|
||||
end
|
||||
|
||||
if not self.back then
|
||||
self.back = nodeToInsert;
|
||||
end
|
||||
|
||||
self.nodeCount = self.nodeCount + 1;
|
||||
end
|
||||
|
||||
function DoublyLinkedListMixin:InsertAfter(nodeToInsert, listNode)
|
||||
nodeToInsert.prev = listNode;
|
||||
|
||||
if listNode then
|
||||
if listNode.next then
|
||||
listNode.next.prev = nodeToInsert;
|
||||
end
|
||||
|
||||
nodeToInsert.next = listNode.next;
|
||||
listNode.next = nodeToInsert;
|
||||
end
|
||||
|
||||
if self.back == listNode then
|
||||
self.back = nodeToInsert;
|
||||
end
|
||||
|
||||
if not self.front then
|
||||
self.front = nodeToInsert;
|
||||
end
|
||||
|
||||
self.nodeCount = self.nodeCount + 1;
|
||||
end
|
||||
|
||||
function DoublyLinkedListMixin:Remove(node)
|
||||
if node then
|
||||
if self.front == node then
|
||||
self.front = node.next;
|
||||
end
|
||||
|
||||
if self.back == node then
|
||||
self.back = node.prev;
|
||||
end
|
||||
|
||||
if node.prev then
|
||||
node.prev.next = node.next;
|
||||
end
|
||||
|
||||
if node.next then
|
||||
node.next.prev = node.prev;
|
||||
end
|
||||
|
||||
node.prev = nil;
|
||||
node.next = nil;
|
||||
|
||||
self.nodeCount = self.nodeCount - 1;
|
||||
end
|
||||
|
||||
return node;
|
||||
end
|
||||
|
||||
TextureLoadingGroupMixin = {};
|
||||
|
||||
function TextureLoadingGroupMixin:AddTexture(texture)
|
||||
self.textures = self.textures or {};
|
||||
self.textures[texture] = true;
|
||||
end
|
||||
|
||||
function TextureLoadingGroupMixin:RemoveTexture(texture)
|
||||
if self.textures then
|
||||
self.textures[texture] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
function TextureLoadingGroupMixin:Reset()
|
||||
self.textures = nil;
|
||||
end
|
||||
|
||||
function TextureLoadingGroupMixin:EnumerateTextures()
|
||||
if self.textures then
|
||||
return pairs(self.textures);
|
||||
end
|
||||
return nop;
|
||||
end
|
||||
|
||||
function TextureLoadingGroupMixin:IsFullyLoaded()
|
||||
if self.textures then
|
||||
for texture in pairs(self.textures) do
|
||||
if not texture:IsObjectLoaded() then
|
||||
return false;
|
||||
end
|
||||
end
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
DirtiableMixin = {};
|
||||
|
||||
function DirtiableMixin:SetDirtyMethod(method)
|
||||
self.dirtyCallback = function()
|
||||
method(self);
|
||||
self.dirty = nil;
|
||||
end;
|
||||
end
|
||||
|
||||
function DirtiableMixin:MarkDirty()
|
||||
if not self.dirty then
|
||||
RunNextFrame(self.dirtyCallback);
|
||||
end
|
||||
|
||||
self.dirty = true;
|
||||
end
|
||||
|
||||
-- This mixin implements a small ordered dirty-update runner. Consumers
|
||||
-- define their own dirty flags and phase order. Phase methods return any
|
||||
-- downstream flags that should be processed later in the same pass.
|
||||
DirtyPhaseMixin = {};
|
||||
|
||||
function DirtyPhaseMixin:InitDirtyPhases()
|
||||
self.dirty = false;
|
||||
self.dirtyFlags = CreateFlags();
|
||||
self.dirtyPhases = {};
|
||||
end
|
||||
|
||||
function DirtyPhaseMixin:OnDirtyChanged(_dirty)
|
||||
-- Override in the consumer to schedule or cancel dirty processing.
|
||||
end
|
||||
|
||||
function DirtyPhaseMixin:SetDirtyPhases(phases)
|
||||
assert(type(phases) == "table", "phases must be a table.");
|
||||
|
||||
for _index, phase in ipairs(phases) do
|
||||
assert(phase.flag ~= nil, "phase.flag must be provided.");
|
||||
assert(type(phase.handler) == "function", "phase.handler must be a function.");
|
||||
end
|
||||
|
||||
self.dirtyPhases = phases;
|
||||
end
|
||||
|
||||
function DirtyPhaseMixin:IsDirty(mask)
|
||||
if mask == nil then
|
||||
return self.dirty;
|
||||
end
|
||||
|
||||
return self.dirtyFlags:IsSet(mask);
|
||||
end
|
||||
|
||||
function DirtyPhaseMixin:UpdateDirtyState()
|
||||
local dirty = self.dirtyFlags:IsAnySet();
|
||||
|
||||
if self.dirty ~= dirty then
|
||||
self.dirty = dirty;
|
||||
self:OnDirtyChanged(dirty);
|
||||
end
|
||||
end
|
||||
|
||||
function DirtyPhaseMixin:MarkDirty(mask)
|
||||
self.dirtyFlags:Set(mask);
|
||||
|
||||
if not self.dirty then
|
||||
self:UpdateDirtyState();
|
||||
end
|
||||
end
|
||||
|
||||
function DirtyPhaseMixin:MarkClean(mask)
|
||||
self.dirtyFlags:Clear(mask);
|
||||
|
||||
if self.dirty then
|
||||
self:UpdateDirtyState();
|
||||
end
|
||||
end
|
||||
|
||||
function DirtyPhaseMixin:ProcessDirtyFlags()
|
||||
if not self:IsDirty() then
|
||||
return;
|
||||
end
|
||||
|
||||
-- Dirty processing is single-pass; phases should only return later phases.
|
||||
for _index, phase in ipairs(self.dirtyPhases) do
|
||||
if self.dirtyFlags:IsSet(phase.flag) then
|
||||
self.dirtyFlags:Clear(phase.flag);
|
||||
|
||||
local downstreamMask = phase.handler();
|
||||
if downstreamMask ~= nil then
|
||||
self.dirtyFlags:Set(downstreamMask);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self:UpdateDirtyState();
|
||||
|
||||
if self:IsDirty() then
|
||||
assertsafe(false, "Dirty flags were not fully cleared during update pass (remaining flags: %d)", self.dirtyFlags:GetFlags());
|
||||
end
|
||||
end
|
||||
|
||||
MixinUtil = {};
|
||||
|
||||
-- Calls an owned function on each entry of a table of mixin instances, checking that the instance does define that function
|
||||
function MixinUtil.CallMethodOnAllSafe(instances, methodName, ...)
|
||||
if not instances then
|
||||
return;
|
||||
end
|
||||
|
||||
for _, instance in ipairs(instances) do
|
||||
if instance[methodName] then
|
||||
instance[methodName](instance, ...);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check existence of an element, then invoke the provided method by name with its arguments if it exists.
|
||||
-- e.g. frame.Text:SetText("text") -> CallMethodSafe(frame.Text, "SetText", "text");
|
||||
function MixinUtil.CallMethodSafe(element, methodName, ...)
|
||||
if element then
|
||||
element[methodName](element, ...);
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
-- Backport of retail's Blizzard_ObjectAPI/Mainline/ObjectCache.lua.
|
||||
|
||||
function ObjectCache_Create(mixin)
|
||||
return {
|
||||
objects = {},
|
||||
|
||||
Get = function(self, key)
|
||||
local object = self.objects[key];
|
||||
if object then
|
||||
if object.CheckRefresh then
|
||||
object:CheckRefresh();
|
||||
end
|
||||
return object;
|
||||
end
|
||||
|
||||
object = CreateFromMixins(mixin);
|
||||
object:Init(key);
|
||||
self.objects[key] = object;
|
||||
return object;
|
||||
end,
|
||||
}
|
||||
end
|
||||
@@ -0,0 +1,59 @@
|
||||
-- Backport of retail's Blizzard_SharedXMLBase/ObjectUpdater.lua.
|
||||
|
||||
local States = EnumUtil.MakeEnum("Ready", "Begin", "Update", "End");
|
||||
|
||||
local ObjectUpdaterMixin = {};
|
||||
function ObjectUpdaterMixin:Init(data, updateFunc, isCompleteFunc, finishFunc)
|
||||
self.data = data;
|
||||
self:SetUpdateFunction(updateFunc);
|
||||
self:SetIsCompleteFunction(isCompleteFunc);
|
||||
self:SetFinishFunction(finishFunc);
|
||||
self.state = States.Begin;
|
||||
self.ticker = C_Timer.NewTicker(0.01, GenerateClosure(self.Advance, self));
|
||||
end
|
||||
|
||||
function ObjectUpdaterMixin:SetUpdateFunction(updateFunc)
|
||||
self.updateFunc = updateFunc;
|
||||
end
|
||||
|
||||
function ObjectUpdaterMixin:SetIsCompleteFunction(isCompleteFunc)
|
||||
self.isCompleteFunc = isCompleteFunc;
|
||||
end
|
||||
|
||||
function ObjectUpdaterMixin:SetFinishFunction(finishFunc)
|
||||
self.finishFunc = finishFunc;
|
||||
end
|
||||
|
||||
function ObjectUpdaterMixin:Cancel()
|
||||
self.ticker:Cancel();
|
||||
self.state = States.Ready;
|
||||
end
|
||||
|
||||
function ObjectUpdaterMixin:Finished()
|
||||
self:Cancel();
|
||||
if self.finishFunc then
|
||||
self.finishFunc(self.data);
|
||||
end
|
||||
end
|
||||
|
||||
function ObjectUpdaterMixin:Advance()
|
||||
if self.state == States.Begin then
|
||||
if self.isCompleteFunc(self.data) then
|
||||
self.state = States.End;
|
||||
else
|
||||
self.state = States.Update;
|
||||
end
|
||||
elseif self.state == States.Update then
|
||||
self.updateFunc(self.data);
|
||||
if self.isCompleteFunc(self.data) then
|
||||
self.state = States.End;
|
||||
end
|
||||
elseif self.state == States.End then
|
||||
self:Finished();
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
function CreateObjectUpdater(data, updateFunc, isCompleteFunc, finishFunc)
|
||||
return CreateAndInitFromMixin(ObjectUpdaterMixin, data, updateFunc, isCompleteFunc, finishFunc);
|
||||
end
|
||||
@@ -1,21 +1,26 @@
|
||||
-- Backport of FrameXML/Pools.lua to vanilla 1.12 / Lua 5.0.
|
||||
-- Backport of retail Blizzard_SharedXMLBase/Pools.lua to vanilla 1.12 / Lua 5.0.
|
||||
--
|
||||
-- Implementation differences from the modern source:
|
||||
-- - 1.12 has no taint/secure system, so the entire Secure*/Proxy
|
||||
-- machinery (SecureTypes, ProxyUtil, securecallfunction,
|
||||
-- CreateForbiddenFrame) is dropped. Only the Unsecured pools and
|
||||
-- pool collections exist; the canonical `CreateObjectPool` /
|
||||
-- `CreateFramePool` / etc. globals are aliased to them at the
|
||||
-- bottom of the file.
|
||||
-- - FrameUtil.SpecializeFrameWithMixins isn't a thing in 1.12; the
|
||||
-- "table specialization" branch is inlined as a plain Mixin().
|
||||
-- - CreateMaskTexture and CreateActor aren't in 1.12 — the
|
||||
-- corresponding pool creators are kept as stubs so chunk-load of
|
||||
-- consumer addons doesn't fail; they'll error at Acquire() time
|
||||
-- if anything actually invokes them.
|
||||
-- - Lua 5.0 has no `...` as expression; the converter mixins use
|
||||
-- `unpack(arg)` to forward varargs (spans arg[1..arg.n] via getn;
|
||||
-- this build's unpack ignores explicit i/j — the count lives in `.n`).
|
||||
-- Implementation differences from the retail source:
|
||||
-- - 1.12 has no secret values, so SecureObjectPoolMixin does not override
|
||||
-- CheckAllowReleaseObject to refuse them (as in SecureTypes.lua).
|
||||
-- - 1.12 has no CreateForbiddenFrame, so a pool asked for forbidden frames
|
||||
-- makes normal ones. It has no CreateActor, so an actor pool errors when
|
||||
-- it creates an object.
|
||||
|
||||
local SpecializeFrameWithMixins = FrameUtil.SpecializeFrameWithMixins;
|
||||
local CreateSecureMap = SecureTypes.CreateSecureMap;
|
||||
local CreateSecureStack = SecureTypes.CreateSecureStack;
|
||||
local CreateSecureNumber = SecureTypes.CreateSecureNumber;
|
||||
local CreateProxy = ProxyUtil.CreateProxy;
|
||||
local CreateProxyDirectory = ProxyUtil.CreateProxyDirectory;
|
||||
local CreateProxyMixin = ProxyUtil.CreateProxyMixin;
|
||||
local SetPrivateReference = ProxyUtil.SetPrivateReference;
|
||||
local ReleasePrivateReference = ProxyUtil.ReleasePrivateReference;
|
||||
local ProxyConvertablePrivateMixin = CreateFromMixins(ProxyConvertableMixin);
|
||||
local CreateFromMixinsPrivate = CreateFromMixins;
|
||||
|
||||
local enableProxyReporting = false; -- For debugging purposes only.
|
||||
local Proxies = CreateProxyDirectory("Pools.lua", enableProxyReporting);
|
||||
|
||||
local ObjectPoolBaseMixin = {};
|
||||
|
||||
@@ -36,9 +41,9 @@ end
|
||||
|
||||
local function GetObjectIsInvalidMsg(object, poolOrCollection)
|
||||
if poolOrCollection:DoesObjectBelongToPool(object) then
|
||||
return string.format("Attempted to release inactive object '%s'", tostring(object));
|
||||
return ("Attempted to release inactive object '%s'"):format(tostring(object));
|
||||
else
|
||||
return string.format("Attempted to release object '%s' that doesn't belong to this pool", tostring(object));
|
||||
return ("Attempted to release object '%s' that doesn't belong to this pool"):format(tostring(object));
|
||||
end
|
||||
end
|
||||
|
||||
@@ -58,6 +63,14 @@ function ObjectPoolBaseMixin:Acquire()
|
||||
]]--
|
||||
assert(type(object) == "table");
|
||||
|
||||
--[[
|
||||
The reset function will error if forbidden actions are attempted insecurely,
|
||||
particularly in scenarios involving forbidden and protected frames. If an error
|
||||
is thrown, it will do so before we make any further modifications to this pool.
|
||||
|
||||
Note this does create a potential for a dangling frame or region, but that is less of a
|
||||
concern than mutating the pool.
|
||||
]]--
|
||||
self:CallReset(object, new);
|
||||
end
|
||||
|
||||
@@ -65,6 +78,12 @@ function ObjectPoolBaseMixin:Acquire()
|
||||
return object, new;
|
||||
end
|
||||
|
||||
function ObjectPoolBaseMixin:CheckAllowReleaseObject(object)
|
||||
-- Override in a derived mixin if there's any conditional circumstances
|
||||
-- to prevent releasing an object (and probably assert, too).
|
||||
return true;
|
||||
end
|
||||
|
||||
function ObjectPoolBaseMixin:Release(object, canFailToFindObject)
|
||||
local active = self:IsActive(object);
|
||||
|
||||
@@ -76,11 +95,21 @@ function ObjectPoolBaseMixin:Release(object, canFailToFindObject)
|
||||
the case where the object was not found in any pool.
|
||||
]]--
|
||||
if not canFailToFindObject then
|
||||
assertsafe(active, GetObjectIsInvalidMsg(object, self));
|
||||
assertsafe(active, GetObjectIsInvalidMsg, object, self);
|
||||
end
|
||||
|
||||
if not self:CheckAllowReleaseObject(object) then
|
||||
return false;
|
||||
end
|
||||
|
||||
if active then
|
||||
--[[
|
||||
The reset function will error if forbidden actions are attempted insecurely,
|
||||
particularly in scenarios involving forbidden and protected frames. If an error
|
||||
is thrown, it will do so before we make any further modifications to this pool.
|
||||
]]--
|
||||
self:CallReset(object);
|
||||
|
||||
self:ReclaimObject(object);
|
||||
end
|
||||
|
||||
@@ -93,7 +122,14 @@ function ObjectPoolBaseMixin:Dump()
|
||||
end
|
||||
end
|
||||
|
||||
local ObjectPoolMixin = CreateFromMixins(ObjectPoolBaseMixin);
|
||||
--[[
|
||||
ObjectPoolMixin is not exposed for use at this time. All code requiring pools or
|
||||
pool collections are currently using the secure variants. If you need a proxyless pool
|
||||
for any reason, you'll need to expose a function to create this.
|
||||
|
||||
You'll also need to create a PoolCollectionMixin that doesn't make use of any secure types.
|
||||
]]--
|
||||
local ObjectPoolMixin = CreateFromMixinsPrivate(ObjectPoolBaseMixin);
|
||||
|
||||
function ObjectPoolMixin:Init(createFunc, resetFunc, capacity)
|
||||
self.createFunc = createFunc;
|
||||
@@ -166,6 +202,93 @@ function ObjectPoolMixin:GetNumActive()
|
||||
return self.activeObjectCount;
|
||||
end
|
||||
|
||||
local SecureObjectPoolMixin = CreateFromMixinsPrivate(ObjectPoolBaseMixin, ProxyConvertablePrivateMixin);
|
||||
|
||||
function SecureObjectPoolMixin:Init(proxy, createFunc, resetFunc, capacity)
|
||||
local tags = ProxyConvertablePrivateMixin.Init(self, proxy, Proxies);
|
||||
tags[proxy] = "SecureObjectPoolMixin";
|
||||
|
||||
self.createFunc = createFunc;
|
||||
self.resetFunc = resetFunc;
|
||||
self.activeObjects = CreateSecureMap();
|
||||
self.inactiveObjects = CreateSecureStack();
|
||||
self.activeObjectCount = CreateSecureNumber();
|
||||
|
||||
Reserve(self, capacity);
|
||||
end
|
||||
|
||||
function SecureObjectPoolMixin:CallReset(object, new)
|
||||
self.resetFunc(self:ToProxy(), object, new);
|
||||
end
|
||||
|
||||
function SecureObjectPoolMixin:CallCreate()
|
||||
return self.createFunc(self:ToProxy());
|
||||
end
|
||||
|
||||
function SecureObjectPoolMixin:PopInactiveObject()
|
||||
return self.inactiveObjects:Pop();
|
||||
end
|
||||
|
||||
function SecureObjectPoolMixin:AddObject(object)
|
||||
local dummy = true;
|
||||
self.activeObjects:SetValue(object, dummy);
|
||||
self.activeObjectCount:Increment();
|
||||
end
|
||||
|
||||
function SecureObjectPoolMixin:ReclaimObject(object)
|
||||
self.inactiveObjects:Push(object);
|
||||
self.activeObjects:ClearValue(object);
|
||||
self.activeObjectCount:Decrement();
|
||||
end
|
||||
|
||||
function SecureObjectPoolMixin:ReleaseAll()
|
||||
for object in self.activeObjects:Enumerate() do
|
||||
self:Release(object);
|
||||
end
|
||||
end
|
||||
|
||||
function SecureObjectPoolMixin:EnumerateActive()
|
||||
return self.activeObjects:Enumerate();
|
||||
end
|
||||
|
||||
function SecureObjectPoolMixin:GetNextActive(current)
|
||||
return self.activeObjects:GetNext(current);
|
||||
end
|
||||
|
||||
function SecureObjectPoolMixin:IsActive(object)
|
||||
return self.activeObjects:HasKey(object);
|
||||
end
|
||||
|
||||
function SecureObjectPoolMixin:DoesObjectBelongToPool(object)
|
||||
if self:IsActive(object) then
|
||||
return true;
|
||||
end
|
||||
|
||||
return self.inactiveObjects:Contains(object);
|
||||
end
|
||||
|
||||
function SecureObjectPoolMixin:GetNumActive()
|
||||
return self.activeObjectCount:GetValue();
|
||||
end
|
||||
|
||||
local ObjectPoolProxyMixin;
|
||||
do
|
||||
local Funcs =
|
||||
{
|
||||
"Acquire",
|
||||
"ReleaseAll",
|
||||
"Release",
|
||||
"EnumerateActive",
|
||||
"GetNextActive",
|
||||
"IsActive",
|
||||
"GetNumActive",
|
||||
"DoesObjectBelongToPool",
|
||||
};
|
||||
|
||||
ObjectPoolProxyMixin = CreateProxyMixin(Proxies, SecureObjectPoolMixin, Funcs);
|
||||
ObjectPoolProxyMixin.__index = ObjectPoolProxyMixin;
|
||||
end
|
||||
|
||||
local function GetPoolKey(template, specialization)
|
||||
if specialization == nil then
|
||||
return template.."nil";
|
||||
@@ -238,9 +361,9 @@ function PoolCollectionBaseMixin:Dump()
|
||||
end
|
||||
end
|
||||
|
||||
local PoolCollectionMixin = CreateFromMixins(PoolCollectionBaseMixin);
|
||||
local PoolCollectionMixin = CreateFromMixinsPrivate(PoolCollectionBaseMixin);
|
||||
|
||||
function PoolCollectionMixin:Init()
|
||||
function PoolCollectionMixin:Init(proxy)
|
||||
self.pools = {};
|
||||
end
|
||||
|
||||
@@ -267,7 +390,7 @@ function PoolCollectionMixin:Release(object)
|
||||
return;
|
||||
end
|
||||
end
|
||||
assertsafe(false, GetObjectIsInvalidMsg(object, self));
|
||||
assertsafe(false, GetObjectIsInvalidMsg, object, self);
|
||||
end
|
||||
|
||||
function PoolCollectionMixin:DoesObjectBelongToPool(object)
|
||||
@@ -307,21 +430,168 @@ function PoolCollectionMixin:EnumerateActive()
|
||||
end, nil;
|
||||
end
|
||||
|
||||
local SecurePoolCollectionMixin = CreateFromMixinsPrivate(PoolCollectionBaseMixin, ProxyConvertablePrivateMixin);
|
||||
|
||||
function SecurePoolCollectionMixin:Init(proxy)
|
||||
local tags = ProxyConvertablePrivateMixin.Init(self, proxy, Proxies);
|
||||
tags[proxy] = "SecurePoolCollectionMixin";
|
||||
|
||||
self.pools = CreateSecureMap();
|
||||
end
|
||||
|
||||
do
|
||||
local function Accumulate(poolKey, pool, total)
|
||||
total:Add(pool:GetNumActive());
|
||||
end
|
||||
|
||||
function SecurePoolCollectionMixin:GetNumActive()
|
||||
local total = CreateSecureNumber();
|
||||
self.pools:ExecuteRange(Accumulate, total);
|
||||
return total:GetValue();
|
||||
end
|
||||
end
|
||||
|
||||
function SecurePoolCollectionMixin:SetPool(poolKey, pool)
|
||||
self.pools:SetValue(poolKey, pool);
|
||||
end
|
||||
|
||||
function SecurePoolCollectionMixin:GetPoolByKey(poolKey)
|
||||
return self.pools:GetValue(poolKey);
|
||||
end
|
||||
|
||||
function SecurePoolCollectionMixin:Release(object)
|
||||
local canFailToFindObject = true;
|
||||
for poolKey, pool in self.pools:Enumerate() do
|
||||
if securecallmethod(pool, "Release", object, canFailToFindObject) then
|
||||
return;
|
||||
end
|
||||
end
|
||||
assertsafe(false, GetObjectIsInvalidMsg, object, self);
|
||||
end
|
||||
|
||||
do
|
||||
-- Taint protection reading insecure pool
|
||||
local function ReleaseAll(poolKey, pool)
|
||||
pool:ReleaseAll();
|
||||
end
|
||||
|
||||
function SecurePoolCollectionMixin:ReleaseAll()
|
||||
self.pools:ExecuteRange(ReleaseAll);
|
||||
end
|
||||
end
|
||||
|
||||
do
|
||||
-- Taint protection reading insecure pool
|
||||
local function GetNextActive(pool, currentObject)
|
||||
return pool:GetNextActive(currentObject);
|
||||
end
|
||||
|
||||
-- Warning: this function only returns the object, unlike a pool that also returns the key.
|
||||
function SecurePoolCollectionMixin:EnumerateActive()
|
||||
local currentObject = nil;
|
||||
local currentPoolKey, currentPool = self.pools:GetNext(currentObject);
|
||||
return function()
|
||||
if currentPool then
|
||||
currentObject = securecallfunction(GetNextActive, currentPool, currentObject);
|
||||
while not currentObject do
|
||||
currentPoolKey, currentPool = self.pools:GetNext(currentPoolKey);
|
||||
if currentPool then
|
||||
currentObject = securecallfunction(GetNextActive, currentPool, nil);
|
||||
else
|
||||
break;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return currentObject;
|
||||
end, nil;
|
||||
end
|
||||
end
|
||||
|
||||
function Pool_HideAndClearAnchors(pool, region)
|
||||
region:Hide();
|
||||
region:ClearAllPoints();
|
||||
end
|
||||
|
||||
function Pool_HideAndSetToDefaults(pool, region)
|
||||
region:SetToDefaults();
|
||||
region:Hide();
|
||||
end
|
||||
|
||||
local function CreateSecureObjectPoolInstance(createFunc, resetFunc, capacity)
|
||||
local pool = CreateFromMixinsPrivate(SecureObjectPoolMixin);
|
||||
local proxy = CreateProxy(pool, ObjectPoolProxyMixin);
|
||||
pool:Init(proxy, createFunc, resetFunc or nop, capacity);
|
||||
return pool;
|
||||
end
|
||||
|
||||
local function CreateSecureRegionPoolInstance(template, createFunc, resetFunc, capacity)
|
||||
local pool = CreateSecureObjectPoolInstance(createFunc, resetFunc or Pool_HideAndClearAnchors, capacity);
|
||||
local proxy = pool:ToProxy();
|
||||
proxy.GetTemplate = function(self)
|
||||
return template;
|
||||
end
|
||||
|
||||
return pool;
|
||||
end
|
||||
|
||||
local function CreateSecureFramePoolInstance(frameType, parent, template, resetFunc, forbidden, postCreate, capacity)
|
||||
local function Create()
|
||||
local createFrame = forbidden and CreateForbiddenFrame or CreateFrame;
|
||||
local name = nil;
|
||||
local frame = createFrame(frameType, name, parent, template);
|
||||
|
||||
if postCreate then
|
||||
postCreate(frame);
|
||||
end
|
||||
|
||||
return frame;
|
||||
end
|
||||
|
||||
return CreateSecureRegionPoolInstance(template, Create, resetFunc, capacity);
|
||||
end
|
||||
|
||||
function ActorPool_HideAndClearModel(actorPool, actor)
|
||||
actor:ClearModel();
|
||||
actor:Hide();
|
||||
end
|
||||
|
||||
local function CreateSecureActorPoolInstance(parent, template, resetFunc, capacity)
|
||||
local function Create()
|
||||
local name = nil;
|
||||
return parent:CreateActor(name, template);
|
||||
end
|
||||
return CreateSecureRegionPoolInstance(template, Create, resetFunc or ActorPool_HideAndClearModel, capacity);
|
||||
end
|
||||
|
||||
local function CreateSecureTexturePoolInstance(parent, layer, subLayer, template, resetFunc, capacity)
|
||||
local function Create()
|
||||
local name = nil;
|
||||
return parent:CreateTexture(name, layer, template, subLayer);
|
||||
end
|
||||
return CreateSecureRegionPoolInstance(template, Create, resetFunc, capacity);
|
||||
end
|
||||
|
||||
local function CreateSecureFontStringPoolInstance(parent, layer, subLayer, template, resetFunc, capacity)
|
||||
local function Create()
|
||||
local name = nil;
|
||||
return parent:CreateFontString(name, layer, template, subLayer);
|
||||
end
|
||||
return CreateSecureRegionPoolInstance(template, Create, resetFunc, capacity);
|
||||
end
|
||||
|
||||
local function CreateSecureMaskTexturePoolInstance(parent, layer, subLayer, template, resetFunc, capacity)
|
||||
local function Create()
|
||||
local name = nil;
|
||||
return parent:CreateMaskTexture(name, layer, template, subLayer);
|
||||
end
|
||||
return CreateSecureRegionPoolInstance(template, Create, resetFunc, capacity);
|
||||
end
|
||||
|
||||
--[[
|
||||
In addition to a specialization being used to create separate pools for the same template, it can
|
||||
be used to modify a frame after being created. On modern WoW this delegates to
|
||||
FrameUtil.SpecializeFrameWithMixins (which also wires up conventional script handlers); on 1.12
|
||||
we just Mixin() the table into the frame and let the consumer call SetScript() if needed.
|
||||
be used to modify a frame after being created. See FrameUtil.SpecializeFrameWithMixins
|
||||
for the behavior when providing tables with conventionally named script handlers included.
|
||||
]]--
|
||||
local function ConvertSpecializationToPostCreate(specialization)
|
||||
local specializationType = type(specialization);
|
||||
@@ -329,7 +599,7 @@ local function ConvertSpecializationToPostCreate(specialization)
|
||||
return specialization;
|
||||
elseif specializationType == "table" then
|
||||
local function Initializer(frame)
|
||||
Mixin(frame, specialization);
|
||||
SpecializeFrameWithMixins(frame, specialization);
|
||||
end
|
||||
return Initializer;
|
||||
end
|
||||
@@ -337,8 +607,49 @@ local function ConvertSpecializationToPostCreate(specialization)
|
||||
return nil;
|
||||
end
|
||||
|
||||
--[[
|
||||
Arguments are packed into a table for ease of implementation in PoolCollectionBaseMixin. which is derived with
|
||||
different argument signatures. This will also be helpful if we want to access a particular field off the table,
|
||||
such as 'parent' without dealing with argument position in ...
|
||||
]]--
|
||||
local function FramePoolCollection_ArgsToTable(frameType, parent, template, resetFunc, forbidden, specialization, capacity)
|
||||
local args =
|
||||
{
|
||||
frameType = frameType,
|
||||
parent = parent,
|
||||
template = template,
|
||||
resetFunc = resetFunc,
|
||||
forbidden = forbidden,
|
||||
specialization = specialization,
|
||||
capacity = capacity,
|
||||
};
|
||||
return args;
|
||||
end
|
||||
|
||||
local FramePoolCollectionConverterMixin = {};
|
||||
|
||||
local function SecureCreatePoolKeyFromPoolArgs(args)
|
||||
return GetPoolKey(args.template, args.specialization);
|
||||
end
|
||||
|
||||
function FramePoolCollectionConverterMixin:CreatePoolKeyFromPoolArgs(args)
|
||||
return securecallfunction(SecureCreatePoolKeyFromPoolArgs, args);
|
||||
end
|
||||
|
||||
function FramePoolCollectionConverterMixin:GetOrCreatePool(...)
|
||||
local args = FramePoolCollection_ArgsToTable(...);
|
||||
return SecurePoolCollectionMixin.GetOrCreatePoolWithArgs(self, args);
|
||||
end
|
||||
|
||||
function FramePoolCollectionConverterMixin:CreatePool(...)
|
||||
local args = FramePoolCollection_ArgsToTable(...);
|
||||
return SecurePoolCollectionMixin.CreatePoolWithArgs(self, args);
|
||||
end
|
||||
|
||||
local FramePoolCollectionMixin = CreateFromMixinsPrivate(PoolCollectionMixin, FramePoolCollectionConverterMixin);
|
||||
|
||||
function CreateUnsecuredRegionPoolInstance(template, createFunc, resetFunc, capacity)
|
||||
local pool = CreateFromMixins(ObjectPoolMixin);
|
||||
local pool = CreateFromMixinsPrivate(ObjectPoolMixin);
|
||||
pool:Init(createFunc, resetFunc or Pool_HideAndClearAnchors, capacity);
|
||||
pool.GetTemplate = function(self)
|
||||
return template;
|
||||
@@ -347,46 +658,12 @@ function CreateUnsecuredRegionPoolInstance(template, createFunc, resetFunc, capa
|
||||
return pool;
|
||||
end
|
||||
|
||||
--[[
|
||||
Args are packed into a table for ease of implementation in PoolCollectionBaseMixin, which is
|
||||
derived with different argument signatures. This also makes it easy to access named fields
|
||||
like `parent` off the table without dealing with argument position.
|
||||
]]--
|
||||
local function FramePoolCollection_ArgsToTable(frameType, parent, template, resetFunc, forbidden, specialization, capacity)
|
||||
return {
|
||||
frameType = frameType,
|
||||
parent = parent,
|
||||
template = template,
|
||||
resetFunc = resetFunc,
|
||||
forbidden = forbidden, -- ignored on 1.12 (no CreateForbiddenFrame)
|
||||
specialization = specialization,
|
||||
capacity = capacity,
|
||||
};
|
||||
end
|
||||
|
||||
local FramePoolCollectionConverterMixin = {};
|
||||
|
||||
function FramePoolCollectionConverterMixin:CreatePoolKeyFromPoolArgs(args)
|
||||
return GetPoolKey(args.template, args.specialization);
|
||||
end
|
||||
|
||||
function FramePoolCollectionConverterMixin:GetOrCreatePool(...)
|
||||
local args = FramePoolCollection_ArgsToTable(unpack(arg));
|
||||
return self:GetOrCreatePoolWithArgs(args);
|
||||
end
|
||||
|
||||
function FramePoolCollectionConverterMixin:CreatePool(...)
|
||||
local args = FramePoolCollection_ArgsToTable(unpack(arg));
|
||||
return self:CreatePoolWithArgs(args);
|
||||
end
|
||||
|
||||
local FramePoolCollectionMixin = CreateFromMixins(PoolCollectionMixin, FramePoolCollectionConverterMixin);
|
||||
|
||||
do
|
||||
local function CreateFramePoolInstance(frameType, parent, template, resetFunc, postCreate, capacity)
|
||||
local function CreateFramePoolInstance(frameType, parent, template, resetFunc, forbidden, postCreate, capacity)
|
||||
local function Create()
|
||||
local createFrame = forbidden and CreateForbiddenFrame or CreateFrame;
|
||||
local name = nil;
|
||||
local frame = CreateFrame(frameType, name, parent, template);
|
||||
local frame = createFrame(frameType, name, parent, template);
|
||||
|
||||
if postCreate then
|
||||
postCreate(frame);
|
||||
@@ -400,18 +677,26 @@ do
|
||||
|
||||
function FramePoolCollectionMixin:CreatePoolInternal(args)
|
||||
local postCreate = ConvertSpecializationToPostCreate(args.specialization);
|
||||
return CreateFramePoolInstance(args.frameType, args.parent, args.template, args.resetFunc, postCreate, args.capacity);
|
||||
return CreateFramePoolInstance(args.frameType, args.parent, args.template, args.resetFunc, args.forbidden, postCreate, args.capacity);
|
||||
end
|
||||
end
|
||||
|
||||
local FontStringPoolCollectionMixin = CreateFromMixins(PoolCollectionMixin);
|
||||
local SecureFramePoolCollectionMixin = CreateFromMixinsPrivate(SecurePoolCollectionMixin, FramePoolCollectionConverterMixin);
|
||||
|
||||
function FontStringPoolCollectionMixin:CreatePoolKeyFromPoolArgs(args)
|
||||
function SecureFramePoolCollectionMixin:CreatePoolInternal(args)
|
||||
local postCreate = ConvertSpecializationToPostCreate(args.specialization);
|
||||
return CreateSecureFramePoolInstance(args.frameType, args.parent, args.template, args.resetFunc, args.forbidden, postCreate, args.capacity);
|
||||
end
|
||||
|
||||
local SecureFontStringPoolCollectionMixin = CreateFromMixinsPrivate(SecurePoolCollectionMixin);
|
||||
|
||||
function SecureFontStringPoolCollectionMixin:CreatePoolKeyFromPoolArgs(args)
|
||||
return args.template;
|
||||
end
|
||||
|
||||
local function FontStringPoolCollection_ArgsToTable(parent, layer, subLayer, template, resetFunc, capacity)
|
||||
return {
|
||||
local args =
|
||||
{
|
||||
parent = parent,
|
||||
layer = layer,
|
||||
subLayer = subLayer,
|
||||
@@ -419,36 +704,107 @@ local function FontStringPoolCollection_ArgsToTable(parent, layer, subLayer, tem
|
||||
resetFunc = resetFunc,
|
||||
capacity = capacity,
|
||||
};
|
||||
return args;
|
||||
end
|
||||
|
||||
function FontStringPoolCollectionMixin:GetOrCreatePool(...)
|
||||
local args = FontStringPoolCollection_ArgsToTable(unpack(arg));
|
||||
return self:GetOrCreatePoolWithArgs(args);
|
||||
function SecureFontStringPoolCollectionMixin:GetOrCreatePool(...)
|
||||
local args = FontStringPoolCollection_ArgsToTable(...)
|
||||
return SecurePoolCollectionMixin.GetOrCreatePoolWithArgs(self, args);
|
||||
end
|
||||
|
||||
function FontStringPoolCollectionMixin:CreatePool(...)
|
||||
local args = FontStringPoolCollection_ArgsToTable(unpack(arg));
|
||||
return self:CreatePoolWithArgs(args);
|
||||
function SecureFontStringPoolCollectionMixin:CreatePool(...)
|
||||
local args = FontStringPoolCollection_ArgsToTable(...);
|
||||
return SecurePoolCollectionMixin.CreatePoolWithArgs(self, args);
|
||||
end
|
||||
|
||||
function FontStringPoolCollectionMixin:CreatePoolInternal(args)
|
||||
return CreateUnsecuredFontStringPool(args.parent, args.layer, args.subLayer, args.template, args.resetFunc, args.capacity);
|
||||
function SecureFontStringPoolCollectionMixin:CreatePoolInternal(args)
|
||||
return CreateSecureFontStringPoolInstance(args.parent, args.layer, args.subLayer, args.template, args.resetFunc, args.capacity);
|
||||
end
|
||||
|
||||
function CreateSecureObjectPool(createFunc, resetFunc, capacity)
|
||||
return CreateSecureObjectPoolInstance(createFunc, resetFunc, capacity):ToProxy();
|
||||
end
|
||||
|
||||
function CreateSecureFramePool(frameType, parent, template, resetFunc, forbidden, postCreate, capacity)
|
||||
return CreateSecureFramePoolInstance(frameType, parent, template, resetFunc, forbidden, postCreate, capacity):ToProxy();
|
||||
end
|
||||
|
||||
function CreateSecureTexturePool(parent, layer, subLayer, template, resetFunc, capacity)
|
||||
return CreateSecureTexturePoolInstance(parent, layer, subLayer, template, resetFunc, capacity):ToProxy();
|
||||
end
|
||||
|
||||
function CreateSecureFontStringPool(parent, layer, subLayer, template, resetFunc, capacity)
|
||||
return CreateSecureFontStringPoolInstance(parent, layer, subLayer, template, resetFunc, capacity):ToProxy();
|
||||
end
|
||||
|
||||
function CreateSecureActorPool(parent, template, resetFunc, capacity)
|
||||
return CreateSecureActorPoolInstance(parent, template, resetFunc, capacity):ToProxy();
|
||||
end
|
||||
|
||||
function CreateSecureMaskTexturePool(parent, layer, subLayer, template, resetFunc, capacity)
|
||||
return CreateSecureMaskTexturePoolInstance(parent, layer, subLayer, template, resetFunc, capacity):ToProxy();
|
||||
end
|
||||
|
||||
do
|
||||
local Funcs =
|
||||
{
|
||||
"GetNumActive",
|
||||
"Acquire",
|
||||
"Release",
|
||||
"ReleaseAll",
|
||||
"ReleaseAllByTemplate",
|
||||
"EnumerateActiveByTemplate",
|
||||
"EnumerateActive",
|
||||
"IsActive",
|
||||
"DoesObjectBelongToPool",
|
||||
--"Dump",
|
||||
};
|
||||
|
||||
local PoolCollectionProxyMixin = CreateProxyMixin(Proxies, SecurePoolCollectionMixin, Funcs);
|
||||
PoolCollectionProxyMixin.__index = PoolCollectionProxyMixin;
|
||||
|
||||
function PoolCollectionProxyMixin:GetPool(...)
|
||||
local poolCollection = Proxies:ToPrivate(self);
|
||||
local pool = poolCollection:GetPool(...);
|
||||
if not pool then
|
||||
return nil;
|
||||
end
|
||||
|
||||
return pool:ToProxy();
|
||||
end
|
||||
|
||||
function PoolCollectionProxyMixin:CreatePool(...)
|
||||
local poolCollection = Proxies:ToPrivate(self);
|
||||
return poolCollection:CreatePool(...):ToProxy();
|
||||
end
|
||||
|
||||
function PoolCollectionProxyMixin:GetOrCreatePool(...)
|
||||
local poolCollection = Proxies:ToPrivate(self);
|
||||
return poolCollection:GetOrCreatePool(...):ToProxy();
|
||||
end
|
||||
|
||||
local function CreatePoolCollectionInstance(collectionMixin)
|
||||
local poolCollection = CreateFromMixinsPrivate(collectionMixin);
|
||||
local proxy = CreateProxy(poolCollection, PoolCollectionProxyMixin);
|
||||
poolCollection:Init(proxy);
|
||||
return poolCollection;
|
||||
end
|
||||
|
||||
function CreateSecureFramePoolCollection()
|
||||
return CreatePoolCollectionInstance(SecureFramePoolCollectionMixin):ToProxy();
|
||||
end
|
||||
|
||||
function CreateSecureFontStringPoolCollection()
|
||||
return CreatePoolCollectionInstance(SecureFontStringPoolCollectionMixin):ToProxy();
|
||||
end
|
||||
end
|
||||
|
||||
function CreateUnsecuredObjectPool(createFunc, resetFunc, capacity)
|
||||
local pool = CreateFromMixins(ObjectPoolMixin);
|
||||
local pool = CreateFromMixinsPrivate(ObjectPoolMixin);
|
||||
pool:Init(createFunc, resetFunc, capacity);
|
||||
return pool;
|
||||
end
|
||||
|
||||
function CreateUnsecuredFramePool(frameType, parent, template, resetFunc, capacity)
|
||||
local function Create()
|
||||
local name = nil;
|
||||
return CreateFrame(frameType, name, parent, template);
|
||||
end
|
||||
return CreateUnsecuredRegionPoolInstance(template, Create, resetFunc, capacity);
|
||||
end
|
||||
|
||||
function CreateUnsecuredTexturePool(parent, layer, subLayer, template, resetFunc, capacity)
|
||||
local function Create()
|
||||
local name = nil;
|
||||
@@ -465,19 +821,20 @@ function CreateUnsecuredFontStringPool(parent, layer, subLayer, template, resetF
|
||||
return CreateUnsecuredRegionPoolInstance(template, Create, resetFunc, capacity);
|
||||
end
|
||||
|
||||
-- Stub: 1.12 has no CreateActor (model actors are Cataclysm+). Acquire()
|
||||
-- will raise once it actually tries to call parent:CreateActor — kept so
|
||||
-- that addon code merely referencing the symbol doesn't fail to load.
|
||||
function CreateUnsecuredActorPool(parent, template, resetFunc, capacity)
|
||||
function CreateUnsecuredFramePool(frameType, parent, template, resetFunc, capacity)
|
||||
local function Create()
|
||||
local name = nil;
|
||||
return parent:CreateActor(name, template);
|
||||
return CreateFrame(frameType, name, parent, template);
|
||||
end
|
||||
return CreateUnsecuredRegionPoolInstance(template, Create, resetFunc or ActorPool_HideAndClearModel, capacity);
|
||||
return CreateUnsecuredRegionPoolInstance(template, Create, resetFunc, capacity);
|
||||
end
|
||||
|
||||
function CreateUnsecuredFramePoolCollection()
|
||||
local poolCollection = CreateFromMixinsPrivate(FramePoolCollectionMixin);
|
||||
poolCollection:Init();
|
||||
return poolCollection;
|
||||
end
|
||||
|
||||
-- Stub: 1.12 has no CreateMaskTexture (mask textures are post-vanilla).
|
||||
-- Same caveat as CreateUnsecuredActorPool.
|
||||
function CreateUnsecuredMaskTexturePool(parent, layer, subLayer, template, resetFunc, capacity)
|
||||
local function Create()
|
||||
local name = nil;
|
||||
@@ -486,28 +843,16 @@ function CreateUnsecuredMaskTexturePool(parent, layer, subLayer, template, reset
|
||||
return CreateUnsecuredRegionPoolInstance(template, Create, resetFunc, capacity);
|
||||
end
|
||||
|
||||
function CreateUnsecuredFramePoolCollection()
|
||||
local poolCollection = CreateFromMixins(FramePoolCollectionMixin);
|
||||
poolCollection:Init();
|
||||
return poolCollection;
|
||||
end
|
||||
|
||||
function CreateUnsecuredFontStringPoolCollection()
|
||||
local poolCollection = CreateFromMixins(FontStringPoolCollectionMixin);
|
||||
poolCollection:Init();
|
||||
return poolCollection;
|
||||
end
|
||||
|
||||
-- Canonical API aliases. On modern WoW these point at the Secure variants;
|
||||
-- on 1.12 there's no taint system, so they all go straight to the Unsecured
|
||||
-- implementations.
|
||||
CreateObjectPool = CreateUnsecuredObjectPool;
|
||||
CreateFramePool = CreateUnsecuredFramePool;
|
||||
CreateTexturePool = CreateUnsecuredTexturePool;
|
||||
CreateFontStringPool = CreateUnsecuredFontStringPool;
|
||||
CreateActorPool = CreateUnsecuredActorPool;
|
||||
CreateFramePoolCollection = CreateUnsecuredFramePoolCollection;
|
||||
CreateFontStringPoolCollection = CreateUnsecuredFontStringPoolCollection;
|
||||
CreateMaskTexturePool = CreateUnsecuredMaskTexturePool;
|
||||
-- Aliases until we determine if we want to change any code to explicitly create
|
||||
-- the secure or unsecured variant of pools and pool collections.
|
||||
CreateObjectPool = CreateSecureObjectPool;
|
||||
CreateFramePool = CreateSecureFramePool;
|
||||
CreateTexturePool = CreateSecureTexturePool;
|
||||
CreateFontStringPool = CreateSecureFontStringPool;
|
||||
CreateActorPool = CreateSecureActorPool;
|
||||
CreateFramePoolCollection = CreateSecureFramePoolCollection;
|
||||
CreateFontStringPoolCollection = CreateSecureFontStringPoolCollection;
|
||||
CreateMaskTexturePool = CreateSecureMaskTexturePool;
|
||||
|
||||
-- Script objects created from this file should have their source location set to the calling function, rather than generic Pools.lua internals
|
||||
AddSourceLocationExclude("Pools.lua");
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/ScriptAnimationUtil.lua.
|
||||
-- ShakeFrame reads the ShakeStrengthUI cvar, which the DLL registers.
|
||||
|
||||
local function NoEasing(progress)
|
||||
return progress;
|
||||
end
|
||||
|
||||
ScriptAnimationUtil = {};
|
||||
|
||||
function ScriptAnimationUtil.GetScriptAnimationLock(region)
|
||||
if region.scriptedAnimatedAnchorLock then
|
||||
return false;
|
||||
end
|
||||
|
||||
region.scriptedAnimatedAnchorLock = true;
|
||||
return true;
|
||||
end
|
||||
|
||||
function ScriptAnimationUtil.ReleaseScriptAnimationLock(region)
|
||||
region.scriptedAnimatedAnchorLock = nil;
|
||||
end
|
||||
|
||||
function ScriptAnimationUtil.IsScriptAnimationLockActive(region)
|
||||
return region.scriptedAnimatedAnchorLock ~= nil;
|
||||
end
|
||||
|
||||
-- Returns a callback to cancel the animation.
|
||||
function ScriptAnimationUtil.StartFadeAnimation(region, duration, onFinish, easingFunction)
|
||||
easingFunction = easingFunction or NoEasing;
|
||||
|
||||
local distanceX = 0;
|
||||
local distanceY = 0;
|
||||
local alphaVariation = -region:GetAlpha();
|
||||
local scaleVariation = 0;
|
||||
local variationCallback = ScriptAnimationUtil.GenerateEasedVariationCallback(easingFunction, distanceX, distanceY, alphaVariation, scaleVariation);
|
||||
return ScriptAnimationUtil.StartScriptAnimation(region, variationCallback, duration, function()
|
||||
region:SetAlpha(0);
|
||||
if onFinish then
|
||||
onFinish(region);
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
function ScriptAnimationUtil.ShakeFrameRandom(region, magnitude, duration, frequency)
|
||||
if duration == 0 then
|
||||
return nop;
|
||||
end
|
||||
|
||||
if frequency <= 0 or ScriptAnimationUtil.IsScriptAnimationLockActive(region) then
|
||||
return nop;
|
||||
end
|
||||
|
||||
local shake = {};
|
||||
for i = 1, math.ceil(duration / frequency) do
|
||||
local xVariation, yVariation = RandomFloatInRange(-magnitude, magnitude), RandomFloatInRange(-magnitude, magnitude);
|
||||
shake[i] = { x = xVariation, y = yVariation };
|
||||
end
|
||||
|
||||
return ScriptAnimationUtil.ShakeFrame(region, shake, duration, frequency);
|
||||
end
|
||||
|
||||
function ScriptAnimationUtil.ShakeFrame(region, shake, maximumDuration, frequency)
|
||||
local shakeStrength = tonumber(GetCVar("ShakeStrengthUI"));
|
||||
if shakeStrength <= 0 or not ScriptAnimationUtil.GetScriptAnimationLock(region) then
|
||||
return nop;
|
||||
end
|
||||
|
||||
if shake[1] == nil then
|
||||
return nop;
|
||||
end
|
||||
|
||||
local point, relativeRegion, relativePoint, x, y = region:GetPoint(1);
|
||||
local shakeIndex = 1;
|
||||
local endTime = GetTime() + maximumDuration;
|
||||
|
||||
local function CancelShake()
|
||||
region:SetPoint(point, relativeRegion, relativePoint, x, y);
|
||||
region.shakeTicker:Cancel();
|
||||
region.shakeTicker = nil;
|
||||
ScriptAnimationUtil.ReleaseScriptAnimationLock(region);
|
||||
end
|
||||
|
||||
region.shakeTicker = C_Timer.NewTicker(frequency, function()
|
||||
local xVariation, yVariation = shake[shakeIndex].x * shakeStrength, shake[shakeIndex].y * shakeStrength;
|
||||
region:SetPoint(point, relativeRegion, relativePoint, x + xVariation, y + yVariation);
|
||||
shakeIndex = shakeIndex + 1;
|
||||
if shakeIndex > #shake or GetTime() >= endTime then
|
||||
CancelShake();
|
||||
end
|
||||
end);
|
||||
|
||||
return CancelShake;
|
||||
end
|
||||
|
||||
function ScriptAnimationUtil.GenerateEasedVariationCallback(easingFunction, distanceX, distanceY, alpha, scale)
|
||||
distanceX = distanceX or 0;
|
||||
distanceY = distanceY or 0;
|
||||
alpha = alpha or 0;
|
||||
scale = scale or 0;
|
||||
|
||||
local function VariationCallback(elapsedTime, duration)
|
||||
local progress = (easingFunction and easingFunction(elapsedTime / duration)) or (elapsedTime / duration);
|
||||
return distanceX * progress, distanceY * progress, alpha * progress, scale * progress;
|
||||
end
|
||||
|
||||
return VariationCallback;
|
||||
end
|
||||
|
||||
|
||||
function ScriptAnimationUtil.StartScriptAnimation(region, variationCallback, duration, onFinish)
|
||||
if not ScriptAnimationUtil.GetScriptAnimationLock(region) then
|
||||
if onFinish then
|
||||
onFinish(region);
|
||||
end
|
||||
return nop;
|
||||
end
|
||||
|
||||
variationCallback = variationCallback;
|
||||
|
||||
local point, relativeRegion, relativePoint, x, y = region:GetPoint(1);
|
||||
local alpha = region:GetAlpha();
|
||||
local scale = region:GetScale();
|
||||
|
||||
local function ApplyVariation(variationX, variationY, variationAlpha, variationScale)
|
||||
region:SetPoint(point, relativeRegion, relativePoint, x + (variationX or 0), y + (variationY or 0));
|
||||
region:SetAlpha(alpha + (variationAlpha or 0));
|
||||
region:SetScale(scale + (variationScale or 0));
|
||||
end
|
||||
|
||||
local startTime = GetTime();
|
||||
local endTime = startTime + duration;
|
||||
local elapsedTime = 0;
|
||||
ApplyVariation(variationCallback(elapsedTime, duration));
|
||||
|
||||
local function CancelScriptAnimation()
|
||||
ApplyVariation(variationCallback(duration, duration));
|
||||
region.translationTicker:Cancel();
|
||||
region.translationTicker = nil;
|
||||
ScriptAnimationUtil.ReleaseScriptAnimationLock(region);
|
||||
if onFinish then
|
||||
onFinish(region);
|
||||
end
|
||||
end
|
||||
|
||||
local function TranslationTickerFunction()
|
||||
local currentTime = GetTime();
|
||||
|
||||
local finished = GetTime() >= endTime;
|
||||
if finished then
|
||||
CancelScriptAnimation();
|
||||
else
|
||||
elapsedTime = currentTime - startTime;
|
||||
ApplyVariation(variationCallback(elapsedTime, duration));
|
||||
end
|
||||
end
|
||||
|
||||
region.translationTicker = C_Timer.NewTicker(0.01, TranslationTickerFunction);
|
||||
|
||||
return CancelScriptAnimation;
|
||||
end
|
||||
|
||||
function ScriptAnimationUtil.StartScriptAnimationGeneric(region, variationCallback, duration, frequency, onFinish)
|
||||
if not ScriptAnimationUtil.GetScriptAnimationLock(region) then
|
||||
if onFinish then
|
||||
onFinish(region);
|
||||
end
|
||||
return nop;
|
||||
end
|
||||
|
||||
local function CancelScriptAnimation()
|
||||
if region.scriptAnimationTicker then
|
||||
variationCallback(region, duration, duration);
|
||||
region.scriptAnimationTicker:Cancel();
|
||||
region.scriptAnimationTicker = nil;
|
||||
ScriptAnimationUtil.ReleaseScriptAnimationLock(region);
|
||||
if onFinish then
|
||||
onFinish(region);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local startTime = GetTime();
|
||||
local endTime = startTime + duration;
|
||||
|
||||
local function TranslationTickerFunction()
|
||||
local currentTime = GetTime();
|
||||
local finished = currentTime >= endTime;
|
||||
if finished then
|
||||
CancelScriptAnimation();
|
||||
else
|
||||
local elapsedTime = currentTime - startTime;
|
||||
variationCallback(region, elapsedTime, duration);
|
||||
end
|
||||
end
|
||||
|
||||
region.scriptAnimationTicker = C_Timer.NewTicker(frequency or 0.01, TranslationTickerFunction);
|
||||
|
||||
return CancelScriptAnimation;
|
||||
end
|
||||
@@ -4,7 +4,8 @@
|
||||
-- - 1.12 has no secret values, so the issecretvalue asserts are dropped.
|
||||
-- - Lua 5.0 has no `...` as an expression and no `#` operator; varargs
|
||||
-- forward through unpack(arg) and lengths come from table.getn.
|
||||
-- - wipe is table.wipe here.
|
||||
-- - wipe is ClassicAPI.wipe, so an addon's own global wipe cannot
|
||||
-- replace it.
|
||||
|
||||
local assert = assert;
|
||||
local ContainsIf = ContainsIf;
|
||||
@@ -23,7 +24,7 @@ local tCount = table.count;
|
||||
local tDeleteItem = tDeleteItem;
|
||||
local tinsert = table.insert;
|
||||
local tremove = table.remove;
|
||||
local wipe = table.wipe;
|
||||
local wipe = ClassicAPI.wipe;
|
||||
|
||||
-- Secure types are expected to be used by Blizzard code to prevent taint propagation
|
||||
-- while accessing values, particularly in cases where container types are used that can have
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
-- Backport of retail's Blizzard_SharedXML/Sequencer.lua.
|
||||
|
||||
SequencerMixin = {};
|
||||
|
||||
function SequencerMixin:Init()
|
||||
self.original = {};
|
||||
self.onFinish = GenerateClosure(self.ExecuteNext, self);
|
||||
end
|
||||
|
||||
function SequencerMixin:AddInterpolated(v1, v2, time, interpType, func)
|
||||
local interpolation = {v1 = v1, v2 = v2, time = time, interpType = interpType};
|
||||
table.insert(self.original, 1, {interpolation = interpolation, func = func});
|
||||
end
|
||||
|
||||
function SequencerMixin:Add(func)
|
||||
table.insert(self.original, 1, {func = func});
|
||||
end
|
||||
|
||||
function SequencerMixin:ExecuteNext()
|
||||
local sequence = table.remove(self.sequences);
|
||||
if sequence then
|
||||
local interpolation = sequence.interpolation;
|
||||
if interpolation then
|
||||
self.interpolator = CreateInterpolator(sequence.interpType);
|
||||
self.interpolator:Interpolate(interpolation.v1, interpolation.v2, interpolation.time, sequence.func, self.onFinish);
|
||||
else
|
||||
sequence.func();
|
||||
self:ExecuteNext();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SequencerMixin:Cancel()
|
||||
if self.interpolator then
|
||||
self.interpolator:Cancel();
|
||||
end
|
||||
self.sequences = {};
|
||||
end
|
||||
|
||||
function SequencerMixin:Play()
|
||||
local shallow = true;
|
||||
self.sequences = CopyTable(self.original, shallow);
|
||||
self:ExecuteNext();
|
||||
end
|
||||
|
||||
function CreateSequencer()
|
||||
local sequencer = CreateFromMixins(SequencerMixin);
|
||||
sequencer:Init();
|
||||
return sequencer;
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
-- Backport of retail's Blizzard_SharedXML/SharedColorConstants.lua.
|
||||
|
||||
FONT_COLOR_CODE_CLOSE = "|r";
|
||||
|
||||
FACTION_BAR_COLORS =
|
||||
{
|
||||
FACTION_RED_COLOR,
|
||||
FACTION_RED_COLOR,
|
||||
FACTION_ORANGE_COLOR,
|
||||
FACTION_YELLOW_COLOR,
|
||||
FACTION_GREEN_COLOR,
|
||||
FACTION_GREEN_COLOR,
|
||||
FACTION_GREEN_COLOR,
|
||||
FACTION_GREEN_COLOR,
|
||||
};
|
||||
|
||||
PLAYER_FACTION_COLORS =
|
||||
{
|
||||
[0] = PLAYER_FACTION_COLOR_HORDE,
|
||||
[1] = PLAYER_FACTION_COLOR_ALLIANCE,
|
||||
};
|
||||
|
||||
PLAYER_FACTION_COLORS_HEX =
|
||||
{
|
||||
[0] = PLAYER_FACTION_COLOR_HORDE:GenerateHexColor(),
|
||||
[1] = PLAYER_FACTION_COLOR_ALLIANCE:GenerateHexColor(),
|
||||
};
|
||||
|
||||
PLAYER_FACTION_BRIGHT_COLORS =
|
||||
{
|
||||
[0] = PVP_SCOREBOARD_HORDE_CELL_COLOR,
|
||||
[1] = PVP_SCOREBOARD_ALLIANCE_CELL_COLOR,
|
||||
};
|
||||
|
||||
COVENANT_COLORS =
|
||||
{
|
||||
[1] = KYRIAN_BLUE_COLOR,
|
||||
[2] = VENTHYR_RED_COLOR,
|
||||
[3] = NIGHT_FAE_BLUE_COLOR,
|
||||
[4] = NECROLORD_GREEN_COLOR,
|
||||
|
||||
Kyrian = KYRIAN_BLUE_COLOR,
|
||||
Venthyr = VENTHYR_RED_COLOR,
|
||||
NightFae = NIGHT_FAE_BLUE_COLOR,
|
||||
Necrolord = NECROLORD_GREEN_COLOR,
|
||||
}
|
||||
@@ -1064,6 +1064,14 @@ end
|
||||
SLASH_LUAERRORS1 = "/luaerrors";
|
||||
SLASH_LUAERRORS2 = "/scripterrors";
|
||||
|
||||
SlashCmdList["PROFILER"] = function(msg)
|
||||
if ( LoadDebugTools() ) then
|
||||
ClassicAPIProfiler_HandleSlashCmd(msg);
|
||||
end
|
||||
end
|
||||
SLASH_PROFILER1 = "/profiler";
|
||||
SLASH_PROFILER2 = "/prof";
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Give a contested command back to whoever already owned it
|
||||
--
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
-- Backport of retail's Blizzard_SharedXML/SmoothStatusBar.lua.
|
||||
|
||||
local g_updatingBars = {};
|
||||
|
||||
local function IsCloseEnough(bar, newValue, targetValue)
|
||||
local min, max = bar:GetMinMaxValues();
|
||||
local range = max - min;
|
||||
if range > 0.0 then
|
||||
return math.abs((newValue - targetValue) / range) < .00001;
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
local function ProcessSmoothStatusBars()
|
||||
for bar, targetValue in pairs(g_updatingBars) do
|
||||
local effectiveTargetValue = Clamp(targetValue, bar:GetMinMaxValues());
|
||||
local newValue = FrameDeltaLerp(bar:GetValue(), effectiveTargetValue, .25);
|
||||
|
||||
if IsCloseEnough(bar, newValue, effectiveTargetValue) then
|
||||
g_updatingBars[bar] = nil;
|
||||
bar:SetValue(effectiveTargetValue);
|
||||
else
|
||||
bar:SetValue(newValue);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
C_Timer.NewTicker(0, ProcessSmoothStatusBars);
|
||||
|
||||
SmoothStatusBarMixin = {};
|
||||
|
||||
function SmoothStatusBarMixin:ResetSmoothedValue(value) --If nil, tries to set to the last target value
|
||||
local targetValue = g_updatingBars[self];
|
||||
if targetValue then
|
||||
g_updatingBars[self] = nil;
|
||||
self:SetValue(value or targetValue);
|
||||
elseif value then
|
||||
self:SetValue(value);
|
||||
end
|
||||
end
|
||||
|
||||
function SmoothStatusBarMixin:SetSmoothedValue(value)
|
||||
g_updatingBars[self] = value;
|
||||
end
|
||||
|
||||
function SmoothStatusBarMixin:SetMinMaxSmoothedValue(min, max)
|
||||
self:SetMinMaxValues(min, max);
|
||||
|
||||
local targetValue = g_updatingBars[self];
|
||||
if targetValue then
|
||||
local ratio = 1;
|
||||
if max ~= 0 and self.lastSmoothedMax and self.lastSmoothedMax ~= 0 then
|
||||
ratio = max / self.lastSmoothedMax;
|
||||
end
|
||||
|
||||
g_updatingBars[self] = targetValue * ratio;
|
||||
end
|
||||
|
||||
self.lastSmoothedMin = min;
|
||||
self.lastSmoothedMax = max;
|
||||
end
|
||||
@@ -0,0 +1,92 @@
|
||||
-- Backport of retail's Blizzard_SharedXML/SortUtil.lua.
|
||||
|
||||
SortUtil = {};
|
||||
|
||||
function SortUtil.CompareNumeric(lhs, rhs)
|
||||
return Sign(lhs - rhs);
|
||||
end
|
||||
|
||||
function SortUtil.CompareUtf8i(lhs, rhs)
|
||||
return Sign(strcmputf8i(lhs, rhs));
|
||||
end
|
||||
|
||||
local SortManagerMixin = {};
|
||||
|
||||
function SortManagerMixin:Init()
|
||||
self.comparatorsTbls = {};
|
||||
end
|
||||
|
||||
function SortManagerMixin:InsertComparator(sortOrder, comparator)
|
||||
assert(sortOrder, comparator);
|
||||
table.insert(self.comparatorsTbls, {sortOrder = sortOrder, comparator = comparator, ascending = false});
|
||||
end
|
||||
|
||||
function SortManagerMixin:SetDefaultComparator(defaultComparatorFunc)
|
||||
self.defaultComparatorFunc = defaultComparatorFunc;
|
||||
end
|
||||
|
||||
function SortManagerMixin:SetSortOrderFunc(sortOrderFunc)
|
||||
self.sortOrderFunc = sortOrderFunc;
|
||||
end
|
||||
|
||||
function SortManagerMixin:FindComparatorTbl(sortOrder)
|
||||
return FindValueInTableIf(self.comparatorsTbls, function(comparatorsTbl)
|
||||
return comparatorsTbl.sortOrder == sortOrder;
|
||||
end);
|
||||
end
|
||||
|
||||
function SortManagerMixin:ToggleSortAscending(sortOrder)
|
||||
local comparatorTbl = self:FindComparatorTbl(sortOrder);
|
||||
comparatorTbl.ascending = not comparatorTbl.ascending;
|
||||
end
|
||||
|
||||
function SortManagerMixin:SetSortAscending(sortOrder, ascending)
|
||||
local comparatorTbl = self:FindComparatorTbl(sortOrder);
|
||||
comparatorTbl.ascending = ascending;
|
||||
end
|
||||
|
||||
function SortManagerMixin:IsSortAscending(sortOrder)
|
||||
local comparatorTbl = self:FindComparatorTbl(sortOrder);
|
||||
return comparatorTbl.ascending;
|
||||
end
|
||||
|
||||
function SortManagerMixin:CreateComparator()
|
||||
return function(lhs, rhs)
|
||||
local comparatorsTbls = self.comparatorsTbls;
|
||||
|
||||
local result = 0;
|
||||
local sortOrder = self.sortOrderFunc();
|
||||
if sortOrder then
|
||||
local comparatorTbl = self:FindComparatorTbl(sortOrder);
|
||||
if comparatorTbl then
|
||||
result = NegateIf(comparatorTbl.comparator(lhs, rhs), comparatorTbl.ascending);
|
||||
end
|
||||
end
|
||||
|
||||
if result == 0 then
|
||||
for _, comparatorTbl in ipairs(comparatorsTbls) do
|
||||
if comparatorTbl.sortOrder ~= sortOrder then
|
||||
result = NegateIf(comparatorTbl.comparator(lhs, rhs), comparatorTbl.ascending);
|
||||
if result < 0 then
|
||||
return true;
|
||||
elseif result > 0 then
|
||||
return false;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not self.defaultComparatorFunc then
|
||||
error("Provide a default comparator that guarantees strict ordering.");
|
||||
end
|
||||
return self.defaultComparatorFunc(lhs, rhs);
|
||||
end
|
||||
|
||||
return result < 0;
|
||||
end;
|
||||
end
|
||||
|
||||
function SortUtil.CreateSortManager()
|
||||
local sortManager = CreateFromMixins(SortManagerMixin);
|
||||
sortManager:Init();
|
||||
return sortManager;
|
||||
end
|
||||
@@ -0,0 +1,88 @@
|
||||
-- Backport of retail's Blizzard_ObjectAPI/Mainline/Spell.lua.
|
||||
|
||||
Spell = {};
|
||||
SpellMixin = {};
|
||||
|
||||
--[[static]] function Spell:CreateFromSpellID(spellID)
|
||||
local spell = CreateFromMixins(SpellMixin);
|
||||
spell:SetSpellID(spellID);
|
||||
return spell;
|
||||
end
|
||||
|
||||
function SpellMixin:SetSpellID(spellID)
|
||||
self:Clear();
|
||||
self.spellID = spellID;
|
||||
end
|
||||
|
||||
function SpellMixin:GetSpellID()
|
||||
return self.spellID;
|
||||
end
|
||||
|
||||
function SpellMixin:Clear()
|
||||
self.spellID = nil;
|
||||
end
|
||||
|
||||
function SpellMixin:IsSpellEmpty()
|
||||
local spellID = self:GetSpellID();
|
||||
return not spellID or not C_Spell.DoesSpellExist(spellID);
|
||||
end
|
||||
|
||||
-- Spell API
|
||||
function SpellMixin:IsSpellDataCached()
|
||||
if not self:IsSpellEmpty() then
|
||||
return C_Spell.IsSpellDataCached(self:GetSpellID());
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
function SpellMixin:IsDataEvictable()
|
||||
-- Spell data could be evicted from the cache
|
||||
return true;
|
||||
end
|
||||
|
||||
function SpellMixin:GetSpellName()
|
||||
return (C_Spell.GetSpellName(self:GetSpellID()));
|
||||
end
|
||||
|
||||
function SpellMixin:GetSpellTexture()
|
||||
return (C_Spell.GetSpellTexture(self:GetSpellID()));
|
||||
end
|
||||
|
||||
function SpellMixin:GetSpellSubtext()
|
||||
return C_Spell.GetSpellSubtext(self:GetSpellID());
|
||||
end
|
||||
|
||||
function SpellMixin:GetSpellDescription()
|
||||
return C_Spell.GetSpellDescription(self:GetSpellID());
|
||||
end
|
||||
|
||||
function SpellMixin:GetSpellDescriptionForItemLocation(itemLocation)
|
||||
return C_Spell.GetSpellDescriptionForItemLocation(self:GetSpellID(), itemLocation);
|
||||
end
|
||||
|
||||
-- Add a callback to be executed when spell data is loaded, if the spell data is already loaded then execute it immediately
|
||||
function SpellMixin:ContinueOnSpellLoad(callbackFunction)
|
||||
if type(callbackFunction) ~= "function" or self:IsSpellEmpty() then
|
||||
error("Usage: NonEmptySpell:ContinueOnLoad(callbackFunction)", 2);
|
||||
end
|
||||
|
||||
SpellEventListener:AddCallback(self:GetSpellID(), callbackFunction);
|
||||
end
|
||||
|
||||
-- Same as ContinueOnSpellLoad, except it returns a function that when called will cancel the continue
|
||||
function SpellMixin:ContinueWithCancelOnSpellLoad(callbackFunction)
|
||||
if type(callbackFunction) ~= "function" or self:IsSpellEmpty() then
|
||||
error("Usage: NonEmptySpell:ContinueWithCancelOnSpellLoad(callbackFunction)", 2);
|
||||
end
|
||||
|
||||
return SpellEventListener:AddCancelableCallback(self:GetSpellID(), callbackFunction);
|
||||
end
|
||||
|
||||
-- Generic aliases for use with ContinuableContainer
|
||||
function SpellMixin:ContinueWithCancelOnRecordLoad(callbackFunction)
|
||||
return self:ContinueWithCancelOnSpellLoad(callbackFunction);
|
||||
end
|
||||
|
||||
function SpellMixin:IsRecordDataCached()
|
||||
return self:IsSpellDataCached();
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
-- Backport of retail's Blizzard_ObjectAPI/Mainline/SpellBookItemLocation.lua.
|
||||
|
||||
SpellBookItemLocation = {};
|
||||
SpellBookItemLocationMixin = {};
|
||||
|
||||
--[[static]] function SpellBookItemLocation:CreateEmpty()
|
||||
local spelLBookItemLocation = CreateFromMixins(SpellBookItemLocationMixin);
|
||||
return spelLBookItemLocation;
|
||||
end
|
||||
|
||||
-- slotIndex: int
|
||||
-- spellBank: Enum.SpellBookSpellBank (see SpellBookConstantsDocumentation.lua)
|
||||
--[[static]] function SpellBookItemLocation:CreateFromIndexAndBank(slotIndex, spellBank)
|
||||
local spellBookItemLocation = SpellBookItemLocation:CreateEmpty();
|
||||
spellBookItemLocation:SetIndexAndBank(slotIndex, spellBank);
|
||||
return spellBookItemLocation;
|
||||
end
|
||||
|
||||
function SpellBookItemLocationMixin:Clear()
|
||||
self.slotIndex = nil;
|
||||
self.spellBank = nil;
|
||||
end
|
||||
|
||||
function SpellBookItemLocationMixin:SetIndexAndBank(slotIndex, spellBank)
|
||||
self:Clear();
|
||||
self.slotIndex = slotIndex;
|
||||
self.spellBank = spellBank;
|
||||
end
|
||||
|
||||
function SpellBookItemLocationMixin:GetIndexAndBank()
|
||||
return self.slotIndex, self.spellBank;
|
||||
end
|
||||
|
||||
function SpellBookItemLocationMixin:IsEqualToIndexAndBank(otherSlotIndex, otherSpellBank)
|
||||
local slotIndex, spellBank = self:GetIndexAndBank();
|
||||
return slotIndex == otherSlotIndex and spellBank == otherSpellBank;
|
||||
end
|
||||
|
||||
function SpellBookItemLocationMixin:IsEqualTo(otherSpellBookItemLocation)
|
||||
if not otherSpellBookItemLocation then
|
||||
return false;
|
||||
end
|
||||
|
||||
local otherSlotIndex, otherSpellBank = otherSpellBookItemLocation:GetIndexAndBank();
|
||||
return self:IsEqualToIndexAndBank(otherSlotIndex, otherSpellBank);
|
||||
end
|
||||
@@ -0,0 +1,133 @@
|
||||
-- Backport of retail's Blizzard_SharedXML/Spline.lua.
|
||||
|
||||
local function CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1, p2, p3, p4)
|
||||
local term1 = 2.0 * p2 + (p3 - p1) * t;
|
||||
local term2 = (2.0 * p1 - 5.0 * p2 + 4.0 * p3 - p4) * tSquared;
|
||||
local term3 = (3.0 * p2 - p1 - 3.0 * p3 + p4) * tCubed;
|
||||
|
||||
return 0.5 * (term1 + term2 + term3);
|
||||
end
|
||||
|
||||
function CatmullRom_CalculatePointOnCurve(t, p1, p2, p3, p4)
|
||||
local tSquared = t * t;
|
||||
local tCubed = tSquared * t;
|
||||
return CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1, p2, p3, p4);
|
||||
end
|
||||
|
||||
function CatmullRom_Calculate2DPointOnCurve(t, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y)
|
||||
local tSquared = t * t;
|
||||
local tCubed = tSquared * t;
|
||||
return
|
||||
CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1x, p2x, p3x, p4x),
|
||||
CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1y, p2y, p3y, p4y);
|
||||
end
|
||||
|
||||
function CatmullRom_Calculate3DPointOnCurve(t, p1x, p1y, p1z, p2x, p2y, p2z, p3x, p3y, p3z, p4x, p4y, p4z)
|
||||
local tSquared = t * t;
|
||||
local tCubed = tSquared * t;
|
||||
return
|
||||
CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1x, p2x, p3x, p4x),
|
||||
CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1y, p2y, p3y, p4y),
|
||||
CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1z, p2z, p3z, p4z);
|
||||
end
|
||||
|
||||
function CatmullRom_Calculate4DPointOnCurve(t, p1x, p1y, p1z, p1w, p2x, p2y, p2z, p2w, p3x, p3y, p3z, p3w, p4x, p4y, p4z, p4w)
|
||||
local tSquared = t * t;
|
||||
local tCubed = tSquared * t;
|
||||
return
|
||||
CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1x, p2x, p3x, p4x),
|
||||
CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1y, p2y, p3y, p4y),
|
||||
CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1z, p2z, p3z, p4z),
|
||||
CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1w, p2w, p3w, p4w);
|
||||
end
|
||||
|
||||
CatmullRomSplineMixin = {};
|
||||
|
||||
function CreateCatmullRomSpline(numDimensions)
|
||||
local spline = CreateFromMixins(CatmullRomSplineMixin);
|
||||
spline:OnLoad(numDimensions)
|
||||
return spline;
|
||||
end
|
||||
|
||||
function CatmullRomSplineMixin:OnLoad(numDimensions)
|
||||
if numDimensions == 1 then
|
||||
self.calculateFunction = CatmullRom_CalculatePointOnCurve;
|
||||
elseif numDimensions == 2 then
|
||||
self.calculateFunction = CatmullRom_Calculate2DPointOnCurve;
|
||||
elseif numDimensions == 3 then
|
||||
self.calculateFunction = CatmullRom_Calculate3DPointOnCurve;
|
||||
elseif numDimensions == 4 then
|
||||
self.calculateFunction = CatmullRom_Calculate4DPointOnCurve;
|
||||
else
|
||||
-- In the interest of performance, limiting number of dimensions
|
||||
error("Unsupported number of dimensions");
|
||||
end
|
||||
|
||||
self.numDimensions = numDimensions;
|
||||
self:ClearPoints();
|
||||
end
|
||||
|
||||
function CatmullRomSplineMixin:GetNumDimensions()
|
||||
return self.numDimensions;
|
||||
end
|
||||
|
||||
local function AddPointHelper(pointData, startIndex, ...)
|
||||
local pointIndex = 1;
|
||||
for i = startIndex, startIndex + select("#", ...) do
|
||||
pointData[i] = select(pointIndex, ...);
|
||||
pointIndex = pointIndex + 1;
|
||||
end
|
||||
end
|
||||
|
||||
function CatmullRomSplineMixin:AddPoint(...)
|
||||
assert(select("#", ...) == self.numDimensions);
|
||||
|
||||
if #self.pointData == 0 then
|
||||
-- If first point, put it in the list three times times to simplify the curve calculation, at this time the curve represents a single point
|
||||
AddPointHelper(self.pointData, 1, ...);
|
||||
AddPointHelper(self.pointData, 1 + self.numDimensions, ...);
|
||||
AddPointHelper(self.pointData, 1 + self.numDimensions * 2, ...);
|
||||
else
|
||||
-- For each other point, add it to the end twice but replacing the last one that was added twice
|
||||
AddPointHelper(self.pointData, #self.pointData - self.numDimensions + 1, ...);
|
||||
AddPointHelper(self.pointData, #self.pointData + 1, ...);
|
||||
end
|
||||
|
||||
self.numPoints = self.numPoints + 1;
|
||||
end
|
||||
|
||||
function CatmullRomSplineMixin:GetPoint(pointIndex)
|
||||
if pointIndex > 0 and pointIndex <= self:GetNumPoints() then
|
||||
return unpack(self.pointData, (pointIndex + 1) * self.numDimensions + 1, (pointIndex + 2) * self.numDimensions);
|
||||
end
|
||||
end
|
||||
|
||||
function CatmullRomSplineMixin:GetNumPoints()
|
||||
return self.numPoints;
|
||||
end
|
||||
|
||||
function CatmullRomSplineMixin:ClearPoints()
|
||||
self.numPoints = 0;
|
||||
self.pointData = {};
|
||||
end
|
||||
|
||||
function CatmullRomSplineMixin:CalculatePointOnGlobalCurve(t)
|
||||
local startSegmentIndex, localT = self:FindSegmentOnGlobalCurve(t);
|
||||
if startSegmentIndex then
|
||||
return self:CalculatePointOnLocalCurveSegment(startSegmentIndex, localT);
|
||||
end
|
||||
end
|
||||
|
||||
function CatmullRomSplineMixin:CalculatePointOnLocalCurveSegment(startSegmentIndex, t)
|
||||
return self.calculateFunction(t, unpack(self.pointData, (startSegmentIndex - 2) * self.numDimensions + 1, (startSegmentIndex + 2) * self.numDimensions));
|
||||
end
|
||||
|
||||
function CatmullRomSplineMixin:FindSegmentOnGlobalCurve(t)
|
||||
local numPoints = self:GetNumPoints();
|
||||
if numPoints > 1 then
|
||||
local numSegments = numPoints - 1;
|
||||
local segment = numSegments * t;
|
||||
local segmentIndex = Clamp(math.floor(segment), 0, numSegments - 1) + 2;
|
||||
return segmentIndex, PercentageBetween(segment + 2, segmentIndex, segmentIndex + 1);
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,502 @@
|
||||
-- Backport of retail's Blizzard_SharedXML/TableBuilder.lua.
|
||||
|
||||
local ColumnFramePoolCollection = CreateFramePoolCollection();
|
||||
|
||||
local ColumnWidthConstraints = {
|
||||
Fill = 1, -- Width will be distributed by available space.
|
||||
Fixed = 2, -- Width is specified when initializing the column.
|
||||
};
|
||||
|
||||
-- Any row or cell is expected to initialize itself in terms of the row data. The dataIndex is provided
|
||||
-- in case the derived mixin needs to make additional CAPI calls involving it's relative index. The row
|
||||
-- data may also be needed for a tooltip, so it will be assigned to the row and cells on update.
|
||||
TableBuilderElementMixin = {};
|
||||
|
||||
--Derive
|
||||
function TableBuilderElementMixin:Init(...)
|
||||
end
|
||||
|
||||
--Derive
|
||||
function TableBuilderElementMixin:Populate(rowData, dataProviderKey)
|
||||
end
|
||||
|
||||
TableBuilderCellMixin = CreateFromMixins(TableBuilderElementMixin);
|
||||
|
||||
--Derive
|
||||
function TableBuilderCellMixin:OnLineEnter()
|
||||
end
|
||||
|
||||
--Derive
|
||||
function TableBuilderCellMixin:OnLineLeave()
|
||||
end
|
||||
|
||||
|
||||
TableBuilderRowMixin = CreateFromMixins(TableBuilderElementMixin);
|
||||
|
||||
--Derive
|
||||
function TableBuilderRowMixin:OnLineEnter()
|
||||
end
|
||||
|
||||
--Derive
|
||||
function TableBuilderRowMixin:OnLineLeave()
|
||||
end
|
||||
|
||||
|
||||
function TableBuilderRowMixin:OnEnter()
|
||||
self:OnLineEnter();
|
||||
for i, cell in ipairs(self.cells) do
|
||||
cell:OnLineEnter();
|
||||
end
|
||||
end
|
||||
|
||||
function TableBuilderRowMixin:OnLeave()
|
||||
self:OnLineLeave();
|
||||
for i, cell in ipairs(self.cells) do
|
||||
cell:OnLineLeave();
|
||||
end
|
||||
end
|
||||
|
||||
-- Defines an entire column within the table builder, by default a column's sizing constraints are set to fill.
|
||||
TableBuilderColumnMixin = {};
|
||||
function TableBuilderColumnMixin:Init(table)
|
||||
self.cells = {};
|
||||
self.table = table;
|
||||
|
||||
local fillCoefficient = 1.0;
|
||||
local padding = 0;
|
||||
self:SetFillConstraints(fillCoefficient, padding);
|
||||
end
|
||||
|
||||
-- Constructs the header frame with an optional initializer.
|
||||
function TableBuilderColumnMixin:ConstructHeader(templateType, template, ...)
|
||||
local frame = self.table:ConstructHeader(templateType, template);
|
||||
self.headerFrame = frame;
|
||||
if frame.Init then
|
||||
frame:Init(...);
|
||||
end
|
||||
frame:Show();
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:Reset()
|
||||
for row, cell in pairs(self.cells) do
|
||||
self.pool:Release(cell);
|
||||
tDeleteItem(row.cells, cell);
|
||||
end
|
||||
self.cells = {};
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:RemoveRow(row)
|
||||
local cell = self.cells[row];
|
||||
self.cells[row] = nil;
|
||||
self.pool:Release(cell);
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:ConstructCell(row, rowData, dataProviderKey)
|
||||
local cell = self.pool:Acquire();
|
||||
self.cells[row] = cell;
|
||||
|
||||
if cell.Init then
|
||||
cell:Init(unpack(self.args));
|
||||
end
|
||||
|
||||
if cell.Populate then
|
||||
cell.rowData = rowData;
|
||||
cell:Populate(rowData, dataProviderKey);
|
||||
end
|
||||
|
||||
cell:Show();
|
||||
return cell;
|
||||
end
|
||||
|
||||
-- Constructs cells corresponding to each row with an optional initializer.
|
||||
function TableBuilderColumnMixin:ConstructCells(templateType, template, ...)
|
||||
self.args = {...};
|
||||
|
||||
self.pool = ColumnFramePoolCollection:GetOrCreatePool(templateType, nil, template);
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:GetFillCoefficient()
|
||||
return self.fillCoefficient;
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:SetFillCoefficient(fillCoefficient)
|
||||
self.fillCoefficient = fillCoefficient;
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:GetPadding()
|
||||
return self.padding;
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:SetPadding(padding)
|
||||
self.padding = padding;
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:GetCellPadding()
|
||||
return self.leftCellPadding or 0, self.rightCellPadding or 0;
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:SetCellPadding(leftCellPadding, rightCellPadding)
|
||||
self.leftCellPadding = leftCellPadding;
|
||||
self.rightCellPadding = rightCellPadding;
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:GetHeaderFrame()
|
||||
return self.headerFrame;
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:SetHeaderFrame(headerFrame)
|
||||
self.headerFrame = headerFrame;
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:GetWidthConstraints()
|
||||
return self.widthConstraints;
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:GetFixedWidth()
|
||||
return self.fixedWidth;
|
||||
end
|
||||
|
||||
-- A header frame for the column is expected to be constructed or assigned prior to calling this.
|
||||
-- See ConstructHeader() or SetHeaderFrame().
|
||||
function TableBuilderColumnMixin:ConstrainToHeader(padding)
|
||||
local header = self:GetHeaderFrame();
|
||||
assert(header, "ConstrainToHeader() called with a nil header frame. Use ConstructHeader() or assign one with SetHeaderFrame(), or use SetFixedConstraints to have a headerless column.");
|
||||
self:SetFixedConstraints(header:GetWidth(), padding or 0);
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:SetFixedConstraints(fixedWidth, padding)
|
||||
self.widthConstraints = ColumnWidthConstraints.Fixed;
|
||||
self.fixedWidth = fixedWidth;
|
||||
self:SetFillCoefficient(0);
|
||||
self:SetPadding(padding or 0);
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:SetFillConstraints(fillCoefficient, padding)
|
||||
self.widthConstraints = ColumnWidthConstraints.Fill;
|
||||
self.fixedWidth = 0;
|
||||
self:SetFillCoefficient(fillCoefficient);
|
||||
self:SetPadding(padding or 0);
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:SetCalculatedWidth(calculatedWidth)
|
||||
self.calculatedWidth = calculatedWidth;
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:GetCalculatedWidth()
|
||||
return self.calculatedWidth;
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:GetCellWidth()
|
||||
local leftCellPadding, rightCellPadding = self:GetCellPadding();
|
||||
return (self.calculatedWidth - leftCellPadding) - rightCellPadding;
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:GetFullWidth()
|
||||
return self:GetCalculatedWidth() + self:GetPadding();
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:SetDisplayUnderPreviousHeader(displayUnderPreviousHeader)
|
||||
self.displayUnderPreviousHeader = displayUnderPreviousHeader;
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:GetDisplayUnderPreviousHeader()
|
||||
return self.displayUnderPreviousHeader;
|
||||
end
|
||||
|
||||
-- Constructs a table of frames within an existing set of row frames. These row frames could originate from
|
||||
-- a scroll box or statically fixed set. To populate the table, assign a data provider (CAPI or lua function)
|
||||
-- that can retrieve an object by index (number).
|
||||
TableBuilderMixin = {};
|
||||
function TableBuilderMixin:Init()
|
||||
self.rows = {};
|
||||
self.columns = {};
|
||||
self.leftMargin = 0;
|
||||
self.rightMargin = 0;
|
||||
self.columnHeaderOverlap = 0;
|
||||
self.tableWidth = 0;
|
||||
self.headerPoolCollection = CreateFramePoolCollection();
|
||||
end
|
||||
|
||||
function TableBuilderMixin:GetDataProvider()
|
||||
return self.dataProvider;
|
||||
end
|
||||
|
||||
function TableBuilderMixin:SetDataProvider(dataProvider)
|
||||
self.dataProvider = dataProvider;
|
||||
end
|
||||
|
||||
function TableBuilderMixin:GetDataProviderData(dataProviderKey)
|
||||
local dataProvider = self:GetDataProvider();
|
||||
return dataProvider and dataProvider(dataProviderKey) or nil;
|
||||
end
|
||||
|
||||
-- Controls the margins of the left-most and right-most columns within the table.
|
||||
function TableBuilderMixin:SetTableMargins(leftMargin, rightMargin)
|
||||
rightMargin = rightMargin or leftMargin; -- Use leftMargin as the default for both.
|
||||
self.leftMargin = leftMargin;
|
||||
self.rightMargin = rightMargin;
|
||||
end
|
||||
|
||||
-- Column headers overlap to make a consistent display.
|
||||
function TableBuilderMixin:SetColumnHeaderOverlap(columnHeaderOverlap)
|
||||
self.columnHeaderOverlap = columnHeaderOverlap;
|
||||
end
|
||||
|
||||
-- Can be used to set the table width, particularly if no header frames are involved.
|
||||
function TableBuilderMixin:SetTableWidth(tableWidth)
|
||||
self.tableWidth = tableWidth;
|
||||
end
|
||||
|
||||
function TableBuilderMixin:GetTableWidth()
|
||||
return self.tableWidth;
|
||||
end
|
||||
|
||||
function TableBuilderMixin:GetTableMargins()
|
||||
return self.leftMargin, self.rightMargin;
|
||||
end
|
||||
|
||||
function TableBuilderMixin:GetColumnHeaderOverlap()
|
||||
return self.columnHeaderOverlap;
|
||||
end
|
||||
|
||||
function TableBuilderMixin:GetColumns()
|
||||
return self.columns;
|
||||
end
|
||||
|
||||
function TableBuilderMixin:GetHeaderContainer()
|
||||
return self.headerContainer;
|
||||
end
|
||||
|
||||
function TableBuilderMixin:SetHeaderContainer(headerContainer)
|
||||
assert(headerContainer, "SetHeaderContainer() with a nil header container. Use ConstructHeader() or assign one with SetHeaderFrame(), or use SetFixedConstraints to have a headerless column.");
|
||||
self.headerContainer = headerContainer;
|
||||
self:SetTableWidth(headerContainer:GetWidth());
|
||||
end
|
||||
|
||||
function TableBuilderMixin:GetHeaderPoolCollection()
|
||||
return self.headerPoolCollection;
|
||||
end
|
||||
|
||||
function TableBuilderMixin:EnumerateHeaders()
|
||||
return self.headerPoolCollection:EnumerateActive();
|
||||
end
|
||||
|
||||
function TableBuilderMixin:ConstructHeader(templateType, template)
|
||||
local headerContainer = self:GetHeaderContainer();
|
||||
assert(headerContainer ~= nil, "A header container must be set with TableBuilderMixin:SetHeaderContainer before adding column headers.")
|
||||
local headerPoolCollection = self:GetHeaderPoolCollection();
|
||||
local pool = headerPoolCollection:GetOrCreatePool(templateType, headerContainer, template);
|
||||
return pool:Acquire(template);
|
||||
end
|
||||
|
||||
function TableBuilderMixin:Arrange()
|
||||
local columns = self:GetColumns();
|
||||
if columns and #columns > 0 then
|
||||
self:CalculateColumnSpacing();
|
||||
self:ArrangeHeaders();
|
||||
end
|
||||
|
||||
for columnIndex, column in ipairs(self.columns) do
|
||||
column:Reset();
|
||||
end
|
||||
|
||||
-- Repopulate cells for each row in case the column information changed.
|
||||
for index, row in ipairs(self.rows) do
|
||||
assert(TableIsEmpty(row.cells));
|
||||
for columnIndex, column in ipairs(self.columns) do
|
||||
assert(row.rowData);
|
||||
local cell = column:ConstructCell(row, row.rowData, columnIndex);
|
||||
table.insert(row.cells, cell);
|
||||
end
|
||||
|
||||
self:ArrangeCells(row);
|
||||
end
|
||||
end
|
||||
|
||||
function TableBuilderMixin:Reset()
|
||||
self:GetHeaderPoolCollection():ReleaseAll();
|
||||
|
||||
for columnIndex, column in ipairs(self.columns) do
|
||||
column:Reset();
|
||||
end
|
||||
self.columns = {};
|
||||
end
|
||||
|
||||
function TableBuilderMixin:AddRow(row, dataProviderKey)
|
||||
local rowData = self:GetDataProviderData(dataProviderKey);
|
||||
if not rowData then
|
||||
return;
|
||||
end
|
||||
row.rowData = rowData;
|
||||
table.insert(self.rows, row);
|
||||
|
||||
if row.Populate then
|
||||
row:Populate(rowData, dataProviderKey);
|
||||
end
|
||||
|
||||
row.cells = {};
|
||||
for columnIndex, column in ipairs(self.columns) do
|
||||
local cell = column:ConstructCell(row, rowData, columnIndex);
|
||||
table.insert(row.cells, cell);
|
||||
end
|
||||
|
||||
self:ArrangeCells(row);
|
||||
end
|
||||
|
||||
function TableBuilderMixin:RemoveRow(row)
|
||||
local deleted = tDeleteItem(self.rows, row) > 0;
|
||||
if not deleted then
|
||||
return;
|
||||
end
|
||||
|
||||
for columnIndex, column in ipairs(self.columns) do
|
||||
column:RemoveRow(row);
|
||||
end
|
||||
|
||||
row.rowData = nil;
|
||||
end
|
||||
|
||||
function TableBuilderMixin:ArrangeCells(row)
|
||||
local columns = self:GetColumns();
|
||||
if #columns == 0 then
|
||||
return;
|
||||
end
|
||||
|
||||
local height = row:GetHeight();
|
||||
local leftMargin, rightMargin = self:GetTableMargins();
|
||||
|
||||
local column = columns[1];
|
||||
local cell = row.cells[1];
|
||||
cell:SetParent(row);
|
||||
cell:SetHeight(height);
|
||||
local leftCellPadding, rightCellPadding = column:GetCellPadding();
|
||||
|
||||
self:ArrangeHorizontally(cell, row, column:GetCellWidth(), "TOPLEFT", "TOPLEFT", "BOTTOMLEFT", "BOTTOMLEFT", leftMargin + leftCellPadding);
|
||||
|
||||
local previousCell = cell;
|
||||
local previousRightCellPadding = rightCellPadding;
|
||||
for columnIndex = 2, #columns do
|
||||
column = columns[columnIndex];
|
||||
cell = row.cells[columnIndex];
|
||||
cell:SetParent(row);
|
||||
cell:SetHeight(height);
|
||||
leftCellPadding, rightCellPadding = column:GetCellPadding();
|
||||
|
||||
self:ArrangeHorizontally(cell, previousCell, column:GetCellWidth(), "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT", column:GetPadding() + leftCellPadding + previousRightCellPadding);
|
||||
previousCell = cell;
|
||||
previousRightCellPadding = rightCellPadding;
|
||||
end
|
||||
end
|
||||
|
||||
function TableBuilderMixin:AddColumn()
|
||||
local column = CreateAndInitFromMixin(TableBuilderColumnMixin, self);
|
||||
tinsert(self.columns, column);
|
||||
return column;
|
||||
end
|
||||
|
||||
function TableBuilderMixin:CalculateColumnSpacing()
|
||||
-- The arrangement of frames is daisy-chained left to right. The margin on the left side
|
||||
-- is created by adding the margin to it's anchor offset, and the margin on the right side
|
||||
-- is created by subtracting space from the remaining fill space.
|
||||
local columns = self:GetColumns();
|
||||
local paddingTotal = 0;
|
||||
local fillCoefficientTotal = 0;
|
||||
local fixedWidthTotal = 0;
|
||||
for columnIndex, column in ipairs(columns) do
|
||||
if column:GetWidthConstraints() == ColumnWidthConstraints.Fill then
|
||||
fillCoefficientTotal = fillCoefficientTotal + column:GetFillCoefficient();
|
||||
else
|
||||
fixedWidthTotal = fixedWidthTotal + column:GetFixedWidth();
|
||||
end
|
||||
|
||||
paddingTotal = paddingTotal + column:GetPadding();
|
||||
end
|
||||
|
||||
local tableWidth = self:GetTableWidth();
|
||||
local leftMargin, rightMargin = self:GetTableMargins();
|
||||
local fillWidthTotal = tableWidth - paddingTotal - (leftMargin + rightMargin) - fixedWidthTotal;
|
||||
for k, column in pairs(columns) do
|
||||
if fillCoefficientTotal > 0 and column:GetWidthConstraints() == ColumnWidthConstraints.Fill then
|
||||
local fillRatio = column:GetFillCoefficient() / fillCoefficientTotal;
|
||||
local width = fillRatio * fillWidthTotal;
|
||||
column:SetCalculatedWidth(width);
|
||||
else
|
||||
local width = column:GetFixedWidth();
|
||||
column:SetCalculatedWidth(width);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function TableBuilderMixin:ArrangeHorizontally(frame, relativeTo, width, pointTop, pointRelativeTop, pointBottom, pointRelativeBottom, xOffset)
|
||||
frame:SetPoint(pointTop, relativeTo, pointRelativeTop, xOffset, 0);
|
||||
frame:SetPoint(pointBottom, relativeTo, pointRelativeBottom, xOffset, 0);
|
||||
frame:SetWidth(width);
|
||||
end
|
||||
|
||||
function TableBuilderMixin:ArrangeHeaders()
|
||||
local headerOverlap = self:GetColumnHeaderOverlap();
|
||||
local leftMargin, rightMargin = self:GetTableMargins();
|
||||
local columns = self:GetColumns();
|
||||
|
||||
-- Any trailing columns without headers should add to the width of the last column with a header.
|
||||
local numColumns = #columns;
|
||||
local lastActiveHeaderIndex = numColumns;
|
||||
local trailingWidth = 0;
|
||||
for i, column in ipairs(columns) do
|
||||
if column:GetHeaderFrame() then
|
||||
trailingWidth = 0;
|
||||
lastActiveHeaderIndex = i;
|
||||
else
|
||||
trailingWidth = trailingWidth + column:GetFullWidth();
|
||||
end
|
||||
end
|
||||
|
||||
local previousHeader = nil;
|
||||
local accumulatedWidth = 0;
|
||||
local columnIndex = 1;
|
||||
|
||||
while columnIndex <= numColumns do
|
||||
local column = columns[columnIndex];
|
||||
accumulatedWidth = accumulatedWidth + column:GetCalculatedWidth();
|
||||
|
||||
local isLastIndex = columnIndex == lastActiveHeaderIndex;
|
||||
local header = column:GetHeaderFrame();
|
||||
if header then
|
||||
if isLastIndex then
|
||||
accumulatedWidth = accumulatedWidth + trailingWidth;
|
||||
else
|
||||
for j = columnIndex + 1, #columns do
|
||||
local nextColumn = columns[j];
|
||||
if nextColumn:GetDisplayUnderPreviousHeader() then
|
||||
columnIndex = columnIndex + 1;
|
||||
accumulatedWidth = accumulatedWidth + nextColumn:GetFullWidth();
|
||||
else
|
||||
break;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if previousHeader == nil then
|
||||
self:ArrangeHorizontally(header, header:GetParent(), accumulatedWidth, "TOPLEFT", "TOPLEFT", "BOTTOMLEFT", "BOTTOMLEFT", leftMargin);
|
||||
else
|
||||
self:ArrangeHorizontally(header, previousHeader, accumulatedWidth + headerOverlap, "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT", column:GetPadding() - headerOverlap);
|
||||
end
|
||||
|
||||
accumulatedWidth = 0;
|
||||
previousHeader = header;
|
||||
end
|
||||
|
||||
if isLastIndex then
|
||||
break;
|
||||
end
|
||||
|
||||
columnIndex = columnIndex + 1;
|
||||
end
|
||||
end
|
||||
|
||||
-- ... are additional mixins
|
||||
function CreateTableBuilder(rows, ...)
|
||||
local tableBuilder = CreateAndInitFromMixin(TableBuilderMixin, rows);
|
||||
Mixin(tableBuilder, ...);
|
||||
return tableBuilder;
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,87 @@
|
||||
-- Backport of retail's Blizzard_SharedXMLBase/TaggableObject.lua.
|
||||
|
||||
local TAG_BANK_SIZE = 31;
|
||||
|
||||
local function GetTagValues(tag)
|
||||
local index = math.floor(tag / TAG_BANK_SIZE);
|
||||
local tagIndexInBank = tag - (index * TAG_BANK_SIZE);
|
||||
local tagBit = bit.lshift(1, tagIndexInBank);
|
||||
|
||||
return index, tagBit;
|
||||
end
|
||||
|
||||
local function AddTagToTable(tags, tag)
|
||||
local index, tagBit = GetTagValues(tag);
|
||||
local tagValue = tags[index] or 0;
|
||||
tags[index] = bit.bor(tagValue, tagBit);
|
||||
end
|
||||
|
||||
local function RemoveTagFromTable(tags, tag)
|
||||
local index, tagBit = GetTagValues(tag);
|
||||
local tagValue = tags[index];
|
||||
if tagValue ~= nil and tagValue > 0 then
|
||||
tags[index] = bit.band(tagValue, bit.bnot(tagBit));
|
||||
end
|
||||
end
|
||||
|
||||
local function TableHasTag(tags, tag)
|
||||
local index, tagBit = GetTagValues(tag);
|
||||
local tagValue = tags[index];
|
||||
if tagValue ~= nil and tagValue > 0 then
|
||||
return bit.band(tagValue, tagBit) == tagBit;
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
TaggableObjectMixin = {};
|
||||
|
||||
function TaggableObjectMixin:AddTag(tag)
|
||||
AddTagToTable(GetOrCreateTableEntry(self, "tags"), tag);
|
||||
end
|
||||
|
||||
function TaggableObjectMixin:RemoveTag(tag)
|
||||
if self.tags then
|
||||
RemoveTagFromTable(self.tags, tag);
|
||||
end
|
||||
end
|
||||
|
||||
function TaggableObjectMixin:RemoveAllTags()
|
||||
self.tags = nil;
|
||||
end
|
||||
|
||||
function TaggableObjectMixin:MatchesTag(tag)
|
||||
if self.tags then
|
||||
return TableHasTag(self.tags, tag);
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function TaggableObjectMixin:MatchesAnyTag(...)
|
||||
if self.tags then
|
||||
for i = 1, select("#", ...) do
|
||||
local tag = select(i, ...);
|
||||
if TableHasTag(self.tags, tag) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function TaggableObjectMixin:MatchesAllTags(...)
|
||||
if self.tags then
|
||||
for i = 1, select("#", ...) do
|
||||
local tag = select(i, ...);
|
||||
if not TableHasTag(self.tags, tag) then
|
||||
return false;
|
||||
end
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
@@ -1,792 +1,27 @@
|
||||
-- Trade-skill *list* links (`|Htrade:...|h`) — backport of the 2.x+ shareable
|
||||
-- profession link. The link is built entirely in the DLL
|
||||
-- (`GetTradeSkillListLink` / `GetCraftListLink`, see src/tradeskill/Link.cpp);
|
||||
-- this file only intercepts clicks on received links and renders the linker's
|
||||
-- recipe list in a scroll frame (the UI half).
|
||||
--
|
||||
-- link: |cffffd000|Htrade:<skillLineID>:<curRank>:<maxRank>:<linkerName>:<base64 bits>|h[Name]|h|r
|
||||
--
|
||||
-- "Which recipes are known" is a bitfield over the skill line's canonical
|
||||
-- recipe list (deterministic per DBC). The viewer lists the recipes the linker
|
||||
-- knows (with an "X of Y" summary of the whole skill line), regardless of what
|
||||
-- the reader can craft themselves.
|
||||
-- Profession links (`|Htrade:...|h`), 3.3.5's UI half. The DLL builds the
|
||||
-- link (GetTradeSkillListLink / GetCraftListLink, src/tradeskill/Link.cpp)
|
||||
-- and opens a received one in the profession window itself
|
||||
-- (src/tradeskill/Window.cpp), through ItemRefTooltip:SetHyperlink as
|
||||
-- SetItemRef already calls it. This file ports the parts of 3.3.5's
|
||||
-- Blizzard_TradeSkillUI that go with that:
|
||||
-- - TradeSkillLinkButton: a button by the window title that puts the
|
||||
-- window's link in the chat box, shown when the window has a link and
|
||||
-- shows the player's own profession;
|
||||
-- - TradeSkillFrame_SetSelection's linked branch: a window that shows
|
||||
-- another player's profession hides what only works on the player's own
|
||||
-- (Create, Create All and the count controls).
|
||||
-- 3.3.5 never shows whose profession a linked window holds. 4.3.4 does, with
|
||||
-- TradeSkillLinkNameButton and TradeSkillFrame_SetLinkName, ported here too.
|
||||
-- The Craft window gets the same through GetCraftListLink and IsCraftLinked.
|
||||
|
||||
-- Frame geometry mirrors the real TradeSkillFrame. Turtle ships an HD
|
||||
-- 768x512 two-column frame (TW-TradeSkill art); stock 1.12.1 only has the
|
||||
-- vanilla 384x512 single-column art, so on that client we fall back to the
|
||||
-- native Blizzard_TradeSkillUI geometry (list on top, detail below) rather
|
||||
-- than stretch the wide art. LO holds every layout value that differs.
|
||||
local TURTLE = (TURTLE_WOW_VERSION ~= nil);
|
||||
local FRAME_W = TURTLE and 768 or 384;
|
||||
local FRAME_H = 512;
|
||||
local NUM_ROWS = TURTLE and 19 or 8;
|
||||
local ROW_HEIGHT = 16;
|
||||
|
||||
local LO = TURTLE and {
|
||||
hit = { 2, 82, 2, 73 },
|
||||
quads = {
|
||||
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-TopLeft", 512, 256, "TOPLEFT" },
|
||||
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-TopRight", 256, 256, "TOPRIGHT" },
|
||||
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-BotLeft2", 512, 256, "BOTTOMLEFT" },
|
||||
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-BotRight2", 256, 256, "BOTTOMRIGHT" },
|
||||
},
|
||||
close = { -77, -8 },
|
||||
bar = { 596, 15, 76, -44 },
|
||||
barBdr = { 604, 22 },
|
||||
summary = { "TOPRIGHT", -110, -70 },
|
||||
drop = { 40, -60 },
|
||||
scroll = { 296, 330, 37, -98 },
|
||||
-- detail scroll frame: { x, y, width, height }. Tall enough that the
|
||||
-- content fits (scrollbar auto-hides) — Turtle's right column is roomy.
|
||||
detail = { 371, -110, 340, 290 },
|
||||
exit = { "CENTER", "BOTTOMRIGHT", -128, 92 },
|
||||
} or {
|
||||
hit = { 0, 34, 0, 75 },
|
||||
quads = {
|
||||
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopLeft", 256, 256, "TOPLEFT" },
|
||||
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopRight", 128, 256, "TOPRIGHT" },
|
||||
{ "Interface\\TradeSkillFrame\\UI-TradeSkill-BotLeft", 256, 256, "BOTTOMLEFT" },
|
||||
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-BotRight", 128, 256, "BOTTOMRIGHT" },
|
||||
},
|
||||
close = { -29, -8 },
|
||||
bar = { 268, 15, 73, -37 },
|
||||
barBdr = { 278, 22 },
|
||||
summary = { "TOPRIGHT", -40, -58 },
|
||||
drop = { 12, -62 },
|
||||
scroll = { 296, 130, 21, -96 },
|
||||
-- detail scroll frame { x, y, width, height } — native TradeSkillFrame's
|
||||
-- TradeSkillDetailScrollFrame values. Small enough that many-reagent
|
||||
-- recipes scroll (clipping partial rows), like native.
|
||||
detail = { 20, -234, 297, 176 },
|
||||
exit = { "CENTER", "TOPLEFT", 305, -422 },
|
||||
sep = { 15, -221, 331 },
|
||||
local TRADE_SKILL_CREATE_CONTROLS = {
|
||||
"TradeSkillCreateButton",
|
||||
"TradeSkillCreateAllButton",
|
||||
"TradeSkillDecrementButton",
|
||||
"TradeSkillInputBox",
|
||||
"TradeSkillIncrementButton",
|
||||
};
|
||||
|
||||
-- Reagents render in a 2-column grid inside the detail scroll child (see
|
||||
-- the detail pane below). The detail is a real ScrollFrame like the
|
||||
-- native TradeSkillFrame, so the whole pane pixel-scrolls and clips
|
||||
-- partial reagent rows — `REAGENT_ROW_HEIGHT` is just the vertical stride
|
||||
-- of one reagent row.
|
||||
local REAGENT_ROW_HEIGHT = 43;
|
||||
|
||||
-- Scroll-frame templates. Stock mimics the native TradeSkillFrame, whose
|
||||
-- list/detail scrolls use the ClassTrainer templates (they add the
|
||||
-- decorative UI-ClassTrainer-ScrollBar track). Turtle's HD frame art
|
||||
-- doesn't want that groove, so it keeps the plain templates. Both detail
|
||||
-- templates are real ScrollFrames (ClassTrainerDetail... inherits
|
||||
-- UIPanelScrollFrameTemplate), so the pixel-scroll logic is identical.
|
||||
local LIST_SCROLL_TEMPLATE = TURTLE and "FauxScrollFrameTemplate"
|
||||
or "ClassTrainerListScrollFrameTemplate";
|
||||
local DETAIL_SCROLL_TEMPLATE = TURTLE and "UIPanelScrollFrameTemplate"
|
||||
or "ClassTrainerDetailScrollFrameTemplate";
|
||||
|
||||
local PROFESSION_ICONS = {
|
||||
[171] = "Trade_Alchemy",
|
||||
[164] = "Trade_BlackSmithing",
|
||||
[185] = "INV_Misc_Food_15", -- Cooking
|
||||
[333] = "Trade_Engraving", -- Enchanting
|
||||
[202] = "Trade_Engineering",
|
||||
[129] = "Spell_Holy_SealOfSacrifice", -- First Aid
|
||||
[165] = "INV_Misc_ArmorKit_17", -- Leatherworking
|
||||
[197] = "Trade_Tailoring",
|
||||
[186] = "Trade_Mining",
|
||||
[182] = "Trade_Herbalism",
|
||||
[393] = "INV_Misc_Pelt_Wolf_01", -- Skinning
|
||||
[356] = "Trade_Fishing",
|
||||
};
|
||||
|
||||
if TURTLE then
|
||||
PROFESSION_ICONS[142] = "Trade_Survival"
|
||||
PROFESSION_ICONS[755] = "INV_Misc_Gem_01" -- Jewelcrafting
|
||||
end
|
||||
|
||||
local function ProfessionIcon(skillLineID)
|
||||
local icon = PROFESSION_ICONS[skillLineID];
|
||||
return icon and ("Interface\\Icons\\" .. icon)
|
||||
or "Interface\\Icons\\INV_Misc_QuestionMark";
|
||||
end
|
||||
|
||||
local function DifficultyBand(skill, hi, lo)
|
||||
if not hi or hi <= 0 then
|
||||
return 3;
|
||||
end
|
||||
if not lo or lo == 0 then
|
||||
lo = (hi ~= 25) and (hi - 25) or 0;
|
||||
end
|
||||
local yellow = (hi + lo) / 2;
|
||||
if skill < lo then
|
||||
return 0; -- orange (optimal)
|
||||
elseif skill < yellow then
|
||||
return 1; -- yellow (medium)
|
||||
elseif skill < hi then
|
||||
return 2; -- green (easy)
|
||||
end
|
||||
return 3; -- grey (trivial)
|
||||
end
|
||||
|
||||
local BAND_COLOR = {
|
||||
[0] = DIFFICULT_DIFFICULTY_COLOR, -- orange
|
||||
[1] = FAIR_DIFFICULTY_COLOR, -- yellow
|
||||
[2] = EASY_DIFFICULTY_COLOR, -- green
|
||||
[3] = TRIVIAL_DIFFICULTY_COLOR, -- grey
|
||||
};
|
||||
|
||||
local function CraftableCount(entry)
|
||||
local reagents = entry.reagents;
|
||||
if not reagents or table.getn(reagents) == 0 then
|
||||
return nil;
|
||||
end
|
||||
local best;
|
||||
for i = 1, table.getn(reagents) do
|
||||
local r = reagents[i];
|
||||
local need = r.count or 1;
|
||||
local n = math.floor((C_Item.GetItemCount(r.itemID) or 0) / need);
|
||||
if not best or n < best then
|
||||
best = n;
|
||||
end
|
||||
end
|
||||
return best;
|
||||
end
|
||||
|
||||
local function ParseTradeLink(link)
|
||||
-- Current format, with the linker's name: trade:id:cur:max:Name:bits
|
||||
local _, _, id, cur, max, who, bits =
|
||||
string.find(link, "^trade:(%d+):(%d+):(%d+):([^:]+):([%w%+/]*)$");
|
||||
if not id then
|
||||
-- Legacy format (no name): trade:id:cur:max:bits
|
||||
who = nil;
|
||||
_, _, id, cur, max, bits =
|
||||
string.find(link, "^trade:(%d+):(%d+):(%d+):([%w%+/]*)$");
|
||||
end
|
||||
if not id then
|
||||
return nil;
|
||||
end
|
||||
return tonumber(id), tonumber(cur), tonumber(max), who, bits or "";
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Display frame (built lazily on first use)
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local frame;
|
||||
local function ResolveSubclasses(list, onLoaded)
|
||||
local present, subs, anyItem = {}, {}, false;
|
||||
for i = 1, table.getn(list) do
|
||||
local r = list[i];
|
||||
if r.createdItem and r.createdItem > 0 then
|
||||
anyItem = true;
|
||||
if not r.subclass then
|
||||
-- GetItemInfoInstant's 3rd return is the localized subclass
|
||||
-- name (or nil when uncached).
|
||||
local _, _, subType = C_Item.GetItemInfoInstant(r.createdItem);
|
||||
if subType and subType ~= "" then
|
||||
r.subclass = subType;
|
||||
elseif onLoaded then
|
||||
-- Not cached — re-derive when the item loads.
|
||||
Item:CreateFromItemID(r.createdItem):ContinueOnItemLoad(onLoaded);
|
||||
end
|
||||
end
|
||||
if r.subclass and not present[r.subclass] then
|
||||
present[r.subclass] = true;
|
||||
table.insert(subs, r.subclass);
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(subs);
|
||||
return subs, anyItem;
|
||||
end
|
||||
|
||||
local SUBCLASS_ALL = {};
|
||||
|
||||
local function SubClassButton_OnClick()
|
||||
local v = this.value;
|
||||
if v == SUBCLASS_ALL then
|
||||
v = nil;
|
||||
end
|
||||
frame.subclassFilter = v;
|
||||
UIDropDownMenu_SetText(v or ALL_SUBCLASSES, frame.dropdown);
|
||||
frame:ApplyFilter();
|
||||
end
|
||||
|
||||
local function SubClassDropDown_Initialize()
|
||||
local info = {};
|
||||
info.text = ALL_SUBCLASSES;
|
||||
info.value = SUBCLASS_ALL;
|
||||
info.checked = (frame.subclassFilter == nil);
|
||||
info.func = SubClassButton_OnClick;
|
||||
UIDropDownMenu_AddButton(info);
|
||||
local subs = frame.subclasses or {};
|
||||
for i = 1, table.getn(subs) do
|
||||
local b = {};
|
||||
b.text = subs[i];
|
||||
b.value = subs[i];
|
||||
b.checked = (frame.subclassFilter == subs[i]);
|
||||
b.func = SubClassButton_OnClick;
|
||||
UIDropDownMenu_AddButton(b);
|
||||
end
|
||||
end
|
||||
|
||||
local function RefreshSubclasses()
|
||||
if not (frame and frame:IsShown() and frame.recipes) then
|
||||
return;
|
||||
end
|
||||
local subs, anyItem = ResolveSubclasses(frame.recipes, RefreshSubclasses);
|
||||
frame.subclasses = subs;
|
||||
if anyItem then
|
||||
UIDropDownMenu_Initialize(frame.dropdown, SubClassDropDown_Initialize);
|
||||
UIDropDownMenu_SetWidth(120, frame.dropdown);
|
||||
UIDropDownMenu_SetText(frame.subclassFilter or ALL_SUBCLASSES, frame.dropdown);
|
||||
frame.dropdown:Show();
|
||||
else
|
||||
frame.dropdown:Hide();
|
||||
end
|
||||
if frame.subclassFilter then
|
||||
frame:ApplyFilter();
|
||||
end
|
||||
end
|
||||
|
||||
local function CreateFrame_TradeSkillLink()
|
||||
local f = CreateFrame("Frame", "ClassicAPITradeSkillLinkFrame", UIParent);
|
||||
f:SetSize(FRAME_W, FRAME_H);
|
||||
f:SetPoint("CENTER");
|
||||
f:SetMovable(true);
|
||||
f:EnableMouse(true);
|
||||
f:SetHitRectInsets(LO.hit[1], LO.hit[2], LO.hit[3], LO.hit[4]);
|
||||
f:RegisterForDrag("LeftButton");
|
||||
f:SetScript("OnDragStart", function() this:StartMoving(); end);
|
||||
f:SetScript("OnDragStop", function() this:StopMovingOrSizing(); end);
|
||||
f:SetFrameStrata("DIALOG");
|
||||
f:Hide();
|
||||
tinsert(UISpecialFrames, f:GetName())
|
||||
|
||||
f:SetScript("OnShow", function() PlaySound("igCharacterInfoOpen"); end);
|
||||
f:SetScript("OnHide", function() PlaySound("igCharacterInfoClose"); end);
|
||||
|
||||
local portrait = f:CreateTexture(nil, "BACKGROUND");
|
||||
portrait:SetSize(60, 60);
|
||||
portrait:SetPoint("TOPLEFT", 7, -6);
|
||||
f.portrait = portrait;
|
||||
|
||||
for i = 1, table.getn(LO.quads) do
|
||||
local q = LO.quads[i];
|
||||
local tx = f:CreateTexture(nil, "BORDER");
|
||||
tx:SetTexture(q[1]);
|
||||
tx:SetSize(q[2], q[3]);
|
||||
tx:SetPoint(q[4]);
|
||||
end
|
||||
|
||||
-- Horizontal divider between the recipe list and the detail pane.
|
||||
-- Stock only (Turtle's HD frame art already has its own).
|
||||
if LO.sep then
|
||||
-- Decoded the BLP: it's a thin divider rod whose graphic lives in the
|
||||
-- top quarter of the 256x64 texture (a 2nd rod fills the next quarter,
|
||||
-- bottom half empty). So sample the top quarter, stretched to width —
|
||||
-- the same region the native TradeSkillFrame uses for this divider.
|
||||
local bar = f:CreateTexture(nil, "ARTWORK");
|
||||
bar:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-HorizontalBar");
|
||||
bar:SetTexCoord(0, 1, 0, 0.25);
|
||||
bar:SetSize(LO.sep[3], 16);
|
||||
bar:SetPoint("TOPLEFT", LO.sep[1], LO.sep[2]);
|
||||
end
|
||||
|
||||
local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormal");
|
||||
title:SetPoint("TOP", 0, -18);
|
||||
f.title = title;
|
||||
|
||||
local close = CreateFrame("Button", nil, f, "UIPanelCloseButton");
|
||||
close:SetPoint("TOPRIGHT", LO.close[1], LO.close[2]);
|
||||
|
||||
local exit = CreateFrame("Button", nil, f, "UIPanelButtonTemplate");
|
||||
exit:SetSize(80, 22);
|
||||
exit:SetPoint(LO.exit[1], f, LO.exit[2], LO.exit[3], LO.exit[4]);
|
||||
exit:SetText(EXIT);
|
||||
exit:SetScript("OnClick", function() f:Hide(); end);
|
||||
|
||||
local bar = CreateFrame("StatusBar", nil, f);
|
||||
bar:SetSize(LO.bar[1], LO.bar[2]);
|
||||
bar:SetPoint("TOPLEFT", LO.bar[3], LO.bar[4]);
|
||||
bar:SetStatusBarTexture("Interface\\PaperDollInfoFrame\\UI-Character-Skills-Bar");
|
||||
bar:SetStatusBarColor(0.0, 0.0, 1.0, 0.5);
|
||||
bar:SetMinMaxValues(0, 1);
|
||||
local barBg = bar:CreateTexture(nil, "BACKGROUND");
|
||||
barBg:SetAllPoints(bar);
|
||||
barBg:SetTexture(1, 1, 1, 0.2);
|
||||
barBg:SetVertexColor(0.0, 0.0, 0.75, 0.5);
|
||||
-- Border: tooltip edge around the bar (TradeSkillRankFrameBorder).
|
||||
local border = CreateFrame("Frame", nil, bar);
|
||||
border:SetSize(LO.barBdr[1], LO.barBdr[2]);
|
||||
border:SetPoint("LEFT", -5, 1);
|
||||
border:SetBackdrop({
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 16,
|
||||
});
|
||||
border:SetBackdropBorderColor(0.4, 0.4, 0.4);
|
||||
local barName = bar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall");
|
||||
barName:SetPoint("LEFT", 6, 1);
|
||||
local barRank = bar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall");
|
||||
barRank:SetJustifyH("LEFT");
|
||||
barRank:SetWidth(128);
|
||||
barRank:SetPoint("LEFT", barName, "RIGHT", 13, 0);
|
||||
f.bar = bar;
|
||||
f.barName = barName;
|
||||
f.barRank = barRank;
|
||||
|
||||
local summary = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall");
|
||||
summary:SetPoint(LO.summary[1], LO.summary[2], LO.summary[3]);
|
||||
f.summary = summary;
|
||||
|
||||
local dropdown = CreateFrame("Frame", "ClassicAPITradeSkillLinkSubClass", f,
|
||||
"UIDropDownMenuTemplate");
|
||||
dropdown:SetPoint("TOPLEFT", LO.drop[1], LO.drop[2]);
|
||||
f.dropdown = dropdown;
|
||||
f.subclassFilter = nil; -- nil = all subclasses
|
||||
|
||||
local scroll = CreateFrame("ScrollFrame", "ClassicAPITradeSkillLinkScroll",
|
||||
f, LIST_SCROLL_TEMPLATE);
|
||||
scroll:SetSize(LO.scroll[1], LO.scroll[2]);
|
||||
scroll:SetPoint("TOPLEFT", LO.scroll[3], LO.scroll[4]);
|
||||
scroll:SetScript("OnVerticalScroll", function()
|
||||
FauxScrollFrame_OnVerticalScroll(ROW_HEIGHT, function() f:Refresh(); end)
|
||||
end)
|
||||
f.scroll = scroll;
|
||||
|
||||
f.rows = {};
|
||||
for i = 1, NUM_ROWS do
|
||||
local rowName = "ClassicAPITradeSkillLinkRow" .. i;
|
||||
local row = CreateFrame("Button", rowName, f,
|
||||
"ClassTrainerSkillButtonTemplate");
|
||||
row:SetWidth(290); -- height (16) comes from the template
|
||||
if i == 1 then
|
||||
row:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0);
|
||||
else
|
||||
row:SetPoint("TOPLEFT", f.rows[i - 1], "BOTTOMLEFT", 0, 0);
|
||||
end
|
||||
|
||||
row:SetNormalTexture("");
|
||||
row:SetDisabledTexture("");
|
||||
row:SetHighlightTexture("");
|
||||
|
||||
local txt = getglobal(rowName .. "Text");
|
||||
if txt then
|
||||
txt:ClearAllPoints();
|
||||
txt:SetPoint("LEFT", row, "LEFT", 4, 0);
|
||||
end
|
||||
|
||||
local sel = row:CreateTexture(nil, "BACKGROUND");
|
||||
sel:SetTexture("Interface\\Buttons\\UI-Listbox-Highlight2");
|
||||
sel:SetAllPoints(row);
|
||||
sel:Hide();
|
||||
row.select = sel;
|
||||
|
||||
-- Override the template's trainer scripts with ours.
|
||||
row:SetScript("OnClick", function()
|
||||
if this.entry then
|
||||
f:SelectRecipe(this.entry);
|
||||
end
|
||||
end);
|
||||
|
||||
f.rows[i] = row;
|
||||
end
|
||||
|
||||
-- Detail pane: a real ScrollFrame (like the native TradeSkillFrame's
|
||||
-- TradeSkillDetailScrollFrame) whose scroll child holds the product
|
||||
-- header + reagent grid, so the whole pane pixel-scrolls — clipping
|
||||
-- partial reagent rows — with the standard UIPanelScrollBar. The bar
|
||||
-- auto-hides when the content fits (scrollBarHideable).
|
||||
local detailScroll = CreateFrame("ScrollFrame",
|
||||
"ClassicAPITradeSkillLinkDetailScroll", f, DETAIL_SCROLL_TEMPLATE);
|
||||
detailScroll:SetSize(LO.detail[3], LO.detail[4]);
|
||||
detailScroll:SetPoint("TOPLEFT", LO.detail[1], LO.detail[2]);
|
||||
-- Bar visibility + wheel are driven per-render by SetDetailBarShown (from
|
||||
-- the computed overflow). `scrollBarHideable` still lets the template's
|
||||
-- ScrollFrame_OnScrollRangeChanged manage the slider on range changes.
|
||||
-- Start with the wheel off; the first RenderDetail/ClearDetail sets it.
|
||||
detailScroll.scrollBarHideable = 1;
|
||||
detailScroll:EnableMouseWheel(false);
|
||||
f.detailScroll = detailScroll;
|
||||
|
||||
local detailChild = CreateFrame("Frame",
|
||||
"ClassicAPITradeSkillLinkDetailChild", detailScroll);
|
||||
detailChild:SetSize(LO.detail[3], LO.detail[4]);
|
||||
detailScroll:SetScrollChild(detailChild);
|
||||
f.detailChild = detailChild;
|
||||
|
||||
local prodIcon = CreateFrame("Button", nil, detailChild);
|
||||
prodIcon:SetSize(37, 37); -- native TradeSkillSkillIcon (37x37 @ 8,-3)
|
||||
prodIcon:SetPoint("TOPLEFT", 8, -3);
|
||||
prodIcon:SetNormalTexture("Interface\\Icons\\INV_Misc_QuestionMark");
|
||||
local hdrLeft = detailChild:CreateTexture(nil, "BACKGROUND");
|
||||
hdrLeft:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-DetailHeaderLeft");
|
||||
hdrLeft:SetSize(256, 64); -- Blizzard's native fill width (+64 cap = 320 total)
|
||||
hdrLeft:SetPoint("TOPLEFT", prodIcon, "TOPLEFT", -8, 6);
|
||||
local hdrRight = detailChild:CreateTexture(nil, "BACKGROUND");
|
||||
hdrRight:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-DetailHeaderRight");
|
||||
hdrRight:SetSize(64, 64);
|
||||
hdrRight:SetPoint("TOPLEFT", hdrLeft, "TOPRIGHT", 0, 0);
|
||||
local prodCount = prodIcon:CreateFontString(nil, "ARTWORK", "NumberFontNormal");
|
||||
prodCount:SetPoint("BOTTOMRIGHT", -5, 2);
|
||||
prodIcon.count = prodCount;
|
||||
f.prodIcon = prodIcon;
|
||||
|
||||
local prodName = detailChild:CreateFontString(nil, "ARTWORK", "GameFontNormal");
|
||||
prodName:SetPoint("LEFT", prodIcon, "RIGHT", 5, 0);
|
||||
prodName:SetWidth(LO.detail[3] - 60);
|
||||
prodName:SetJustifyH("LEFT");
|
||||
prodName:SetJustifyV("MIDDLE");
|
||||
f.prodName = prodName;
|
||||
|
||||
local prodDesc = detailChild:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall");
|
||||
prodDesc:SetPoint("TOPLEFT", prodIcon, "BOTTOMLEFT", 0, -6);
|
||||
prodDesc:SetWidth(LO.detail[3] - 20);
|
||||
prodDesc:SetJustifyH("LEFT");
|
||||
prodDesc:SetJustifyV("TOP");
|
||||
f.prodDesc = prodDesc;
|
||||
|
||||
local reagentLabel = detailChild:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall");
|
||||
reagentLabel:SetPoint("TOPLEFT", prodDesc, "BOTTOMLEFT", 0, -8);
|
||||
reagentLabel:SetText(SPELL_REAGENTS or "Reagents:");
|
||||
f.reagentLabel = reagentLabel;
|
||||
|
||||
prodIcon:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT");
|
||||
if this.itemID then
|
||||
GameTooltip:SetHyperlink("item:"..this.itemID);
|
||||
elseif this.spellID then
|
||||
GameTooltip:SetSpellByID(this.spellID);
|
||||
end
|
||||
GameTooltip:Show();
|
||||
end);
|
||||
prodIcon:SetScript("OnLeave", GameTooltip_Hide);
|
||||
|
||||
-- Reagent buttons live in the scroll child (2 per row), built on
|
||||
-- demand so any reagent count is handled and they scroll/clip with
|
||||
-- the child.
|
||||
f.reagents = {};
|
||||
local function AcquireReagent(i)
|
||||
if f.reagents[i] then
|
||||
return f.reagents[i];
|
||||
end
|
||||
local rowN = math.floor((i - 1) / 2);
|
||||
local col = (i - 1) - rowN * 2;
|
||||
local btn = CreateFrame("Button", nil, detailChild);
|
||||
btn:SetSize(39, 39);
|
||||
btn:SetPoint("TOPLEFT", reagentLabel, "BOTTOMLEFT",
|
||||
col * 134 - 5, -3 - rowN * REAGENT_ROW_HEIGHT);
|
||||
-- Extend the hit area rightward over the name plate so the tooltip
|
||||
-- fires anywhere on the element, not just the icon. Stop short of the
|
||||
-- next column (pitch 134 − icon 39) so hovers don't bleed across.
|
||||
btn:SetHitRectInsets(0, -(134 - 39), 0, 0);
|
||||
btn:SetNormalTexture("Interface\\Icons\\INV_Misc_QuestionMark");
|
||||
local plate = btn:CreateTexture(nil, "BACKGROUND");
|
||||
plate:SetTexture("Interface\\QuestFrame\\UI-QuestItemNameFrame");
|
||||
plate:SetSize(114, 64);
|
||||
plate:SetPoint("LEFT", btn, "RIGHT", -10, 0);
|
||||
local cnt = btn:CreateFontString(nil, "ARTWORK", "NumberFontNormal");
|
||||
cnt:SetPoint("BOTTOMRIGHT", -4, 1);
|
||||
btn.count = cnt;
|
||||
local nm = btn:CreateFontString(nil, "ARTWORK", "GameFontHighlight");
|
||||
nm:SetPoint("LEFT", plate, "LEFT", 15, 0);
|
||||
nm:SetSize(80, 36);
|
||||
nm:SetJustifyH("LEFT");
|
||||
nm:SetJustifyV("MIDDLE");
|
||||
btn.nameText = nm;
|
||||
btn:SetScript("OnEnter", function()
|
||||
if this.itemID then
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT");
|
||||
GameTooltip:SetHyperlink("item:"..this.itemID);
|
||||
GameTooltip:Show();
|
||||
end
|
||||
end);
|
||||
btn:SetScript("OnClick", function()
|
||||
if this.itemLink and IsShiftKeyDown() and ChatFrameEditBox:IsVisible() then
|
||||
ChatFrameEditBox:Insert(this.itemLink);
|
||||
end
|
||||
end);
|
||||
btn:SetScript("OnLeave", GameTooltip_Hide);
|
||||
f.reagents[i] = btn;
|
||||
return btn;
|
||||
end
|
||||
|
||||
-- Show the detail bar/borders only when the content overflows the pane.
|
||||
-- Driven by our own computed overflow, NOT the engine: scrollBarHideable's
|
||||
-- auto-hide only fires on a range CHANGE (so content that fits from the
|
||||
-- start never hides), and GetVerticalScrollRange isn't reliably readable
|
||||
-- the same tick as UpdateScrollChildRect.
|
||||
function f:SetDetailBarShown(show)
|
||||
self.detailScroll:EnableMouseWheel(show);
|
||||
local base = "ClassicAPITradeSkillLinkDetailScroll";
|
||||
local sb = getglobal(base .. "ScrollBar");
|
||||
if sb then sb:SetShown(show) end
|
||||
local top = getglobal(base .. "Top");
|
||||
if top then top:SetShown(show) end
|
||||
local bottom = getglobal(base .. "Bottom");
|
||||
if bottom then bottom:SetShown(show) end
|
||||
end
|
||||
|
||||
function f:RenderDetail(entry)
|
||||
local function itemInfo(id)
|
||||
local item = Item:CreateFromItemID(id);
|
||||
if not item:IsItemDataCached() then
|
||||
item:ContinueOnItemLoad(function()
|
||||
if self:IsShown() and self.selectedSpellID == entry.spellID then
|
||||
self:RenderDetail(entry);
|
||||
end
|
||||
end);
|
||||
end
|
||||
return item:GetItemName(), item:GetItemIcon(), item:GetItemLink();
|
||||
end
|
||||
|
||||
local prodTex, prodText, prodLink;
|
||||
local created = entry.createdItem or 0;
|
||||
if created > 0 then
|
||||
local name, tex, link = itemInfo(created);
|
||||
prodText = name or entry.name;
|
||||
prodTex = tex;
|
||||
prodLink = link;
|
||||
else
|
||||
prodText = entry.name;
|
||||
prodTex = C_Spell.GetSpellTexture(entry.spellID);
|
||||
end
|
||||
local descText = (created > 0) and ""
|
||||
or (C_Spell.GetSpellDescription(entry.spellID) or "");
|
||||
self.prodDesc:SetText(descText);
|
||||
self.prodIcon:SetNormalTexture(prodTex or
|
||||
"Interface\\Icons\\INV_Misc_QuestionMark");
|
||||
self.prodName:SetText(prodText or "");
|
||||
self.prodName:SetTextColor(NORMAL_FONT_COLOR:GetRGB());
|
||||
self.prodIcon.itemLink = prodLink; -- item tooltip when created > 0
|
||||
self.prodIcon.spellID = entry.spellID; -- else spell tooltip
|
||||
local made = entry.numMade or 0;
|
||||
self.prodIcon.count:SetText(made > 1 and tostring(made) or "");
|
||||
self.prodIcon:Show();
|
||||
|
||||
-- Fill every reagent slot; the scroll child clips/scrolls the
|
||||
-- overflow. Buttons past the count are hidden.
|
||||
local reagents = entry.reagents or {};
|
||||
local total = table.getn(reagents);
|
||||
for i = 1, total do
|
||||
local btn = AcquireReagent(i);
|
||||
local reagent = reagents[i];
|
||||
local name, tex, link = itemInfo(reagent.itemID);
|
||||
btn:SetNormalTexture(tex or "Interface\\Icons\\INV_Misc_QuestionMark");
|
||||
btn.itemLink = link;
|
||||
btn.itemID = reagent.itemID
|
||||
btn.nameText:SetText(name or ("item:" .. reagent.itemID));
|
||||
local need = reagent.count or 1;
|
||||
local have = C_Item.GetItemCount(reagent.itemID) or 0;
|
||||
if have >= need then
|
||||
local r, g, b = HIGHLIGHT_FONT_COLOR:GetRGB()
|
||||
btn:GetNormalTexture():SetVertexColor(r, g, b);
|
||||
btn.nameText:SetTextColor(r, g, b);
|
||||
else
|
||||
local r, g, b = GRAY_FONT_COLOR:GetRGB()
|
||||
btn:GetNormalTexture():SetVertexColor(r, g, b);
|
||||
btn.nameText:SetTextColor(r, g, b);
|
||||
end
|
||||
local haveText = (have >= 100) and "*" or tostring(have);
|
||||
btn.count:SetText(haveText .. " /" .. need);
|
||||
btn:Show();
|
||||
end
|
||||
for i = total + 1, table.getn(self.reagents) do
|
||||
self.reagents[i].itemLink = nil;
|
||||
self.reagents[i].itemID = nil;
|
||||
self.reagents[i]:Hide();
|
||||
end
|
||||
|
||||
local function applyContentHeight()
|
||||
local contentH = LO.detail[4];
|
||||
if total > 0 then
|
||||
local childTop = self.detailChild:GetTop();
|
||||
local lastBtn = self.reagents[total];
|
||||
local lastBottom = lastBtn and lastBtn:GetBottom();
|
||||
if childTop and lastBottom then
|
||||
-- +22: ~12px name-plate overhang below the icon + ~10px pad.
|
||||
contentH = (childTop - lastBottom) + 22;
|
||||
end
|
||||
end
|
||||
self.detailChild:SetHeight(math.max(contentH, LO.detail[4]));
|
||||
-- Recompute the scroll range from the child's new height, then
|
||||
-- show the bar only if the content actually overflows the pane.
|
||||
self.detailScroll:UpdateScrollChildRect();
|
||||
self:SetDetailBarShown(contentH > LO.detail[4] + 0.5);
|
||||
end
|
||||
applyContentHeight();
|
||||
local sb = getglobal("ClassicAPITradeSkillLinkDetailScrollScrollBar");
|
||||
if sb then
|
||||
sb:SetValue(0);
|
||||
end
|
||||
self.detailScroll:SetVerticalScroll(0);
|
||||
RunNextFrame(function()
|
||||
if self:IsShown() and self.selectedSpellID == entry.spellID then
|
||||
applyContentHeight();
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function f:SelectRecipe(entry)
|
||||
self.selectedSpellID = entry.spellID;
|
||||
self.selectedEntry = entry;
|
||||
self:RenderDetail(entry); -- also resets the detail scroll to top
|
||||
self:Refresh();
|
||||
end
|
||||
|
||||
function f:ClearDetail()
|
||||
self.selectedSpellID = nil;
|
||||
self.selectedEntry = nil;
|
||||
self.prodIcon:Hide();
|
||||
self.prodName:SetText("");
|
||||
self.prodDesc:SetText("");
|
||||
for i = 1, table.getn(self.reagents) do
|
||||
self.reagents[i].itemLink = nil;
|
||||
self.reagents[i].itemID = nil;
|
||||
self.reagents[i]:Hide();
|
||||
end
|
||||
self.detailChild:SetHeight(LO.detail[4]);
|
||||
self.detailScroll:UpdateScrollChildRect();
|
||||
self:SetDetailBarShown(false);
|
||||
self.detailScroll:SetVerticalScroll(0);
|
||||
end
|
||||
|
||||
function f:Refresh()
|
||||
local recipes = self.displayed or {};
|
||||
local total = table.getn(recipes);
|
||||
FauxScrollFrame_Update(self.scroll, total, NUM_ROWS, ROW_HEIGHT);
|
||||
local offset = FauxScrollFrame_GetOffset(self.scroll);
|
||||
for i = 1, NUM_ROWS do
|
||||
local row = self.rows[i];
|
||||
local entry = recipes[offset + i];
|
||||
if entry then
|
||||
-- Append " [N]" when the reader can craft any (Blizzard's list).
|
||||
local craftable = CraftableCount(entry);
|
||||
local label = entry.name or "";
|
||||
if craftable and craftable > 0 then
|
||||
label = label .. " [" .. craftable .. "]";
|
||||
end
|
||||
row:SetText(label);
|
||||
local r, g, b = BAND_COLOR[entry.band or 3]:GetRGB();
|
||||
if self.selectedSpellID and entry.spellID == self.selectedSpellID then
|
||||
row.select:SetVertexColor(r, g, b);
|
||||
row.select:Show();
|
||||
row:SetTextColor(1, 1, 1);
|
||||
else
|
||||
row.select:Hide();
|
||||
row:SetTextColor(r, g, b);
|
||||
end
|
||||
row.spellID = entry.spellID;
|
||||
row.entry = entry;
|
||||
row:Show();
|
||||
else
|
||||
row.spellID = nil;
|
||||
row.entry = nil;
|
||||
row.select:Hide();
|
||||
row:Hide();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function f:ApplyFilter()
|
||||
local src = self.recipes or {};
|
||||
if not self.subclassFilter then
|
||||
self.displayed = src;
|
||||
else
|
||||
local out = {};
|
||||
for i = 1, table.getn(src) do
|
||||
if src[i].subclass == self.subclassFilter then
|
||||
table.insert(out, src[i]);
|
||||
end
|
||||
end
|
||||
self.displayed = out;
|
||||
end
|
||||
local sb = getglobal("ClassicAPITradeSkillLinkScrollScrollBar");
|
||||
if sb then
|
||||
sb:SetValue(0);
|
||||
end
|
||||
self:Refresh();
|
||||
end
|
||||
|
||||
f:RegisterEvent("BAG_UPDATE_DELAYED");
|
||||
f:SetScript("OnEvent", function()
|
||||
if not f:IsShown() then
|
||||
return;
|
||||
end
|
||||
f:Refresh();
|
||||
if f.selectedEntry then
|
||||
f:RenderDetail(f.selectedEntry);
|
||||
end
|
||||
end);
|
||||
|
||||
return f;
|
||||
end
|
||||
|
||||
local function ShowTradeSkillLink(link, text)
|
||||
local id, cur, max, who, bits = ParseTradeLink(link);
|
||||
if not id then
|
||||
return false;
|
||||
end
|
||||
local recipes = C_TradeSkillUI.GetTradeSkillListRecipes(id, bits);
|
||||
if not recipes then
|
||||
return false;
|
||||
end
|
||||
|
||||
local prof = text and string.match(text, "|h%[(.+)%]|h") or "Trade Skills";
|
||||
local total = table.getn(recipes);
|
||||
local knownList = {};
|
||||
for i = 1, total do
|
||||
local r = recipes[i];
|
||||
if r.isKnown then
|
||||
r.band = DifficultyBand(cur or 0, r.trivialLevel, r.greenLevel);
|
||||
r.name = C_Spell.GetSpellName(r.spellID) or ("spell:" .. r.spellID);
|
||||
table.insert(knownList, r);
|
||||
end
|
||||
end
|
||||
table.sort(knownList, function(a, b)
|
||||
if a.band ~= b.band then
|
||||
return a.band < b.band;
|
||||
end
|
||||
return a.name < b.name;
|
||||
end);
|
||||
local known = table.getn(knownList);
|
||||
|
||||
if not frame then
|
||||
frame = CreateFrame_TradeSkillLink();
|
||||
end
|
||||
frame.title:SetText(who and who ~= "" and (who .. " - " .. prof) or prof);
|
||||
SetPortraitToTexture(frame.portrait, ProfessionIcon(id));
|
||||
frame.bar:SetMinMaxValues(0, (max and max > 0) and max or 1);
|
||||
frame.bar:SetValue(cur or 0);
|
||||
frame.barName:SetText(prof);
|
||||
frame.barRank:SetFormattedText("%d/%d", cur or 0, max or 0);
|
||||
frame.summary:SetFormattedText("%d of %d recipes known", known, total);
|
||||
frame.recipes = knownList;
|
||||
frame.subclassFilter = nil;
|
||||
|
||||
frame:ClearDetail();
|
||||
frame:Show();
|
||||
RefreshSubclasses();
|
||||
frame:ApplyFilter();
|
||||
local first = frame.displayed and frame.displayed[1];
|
||||
if first then
|
||||
frame:SelectRecipe(first);
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Click interception: handle `trade:` before the engine's SetHyperlink (which
|
||||
-- in 1.12 only knows item:/enchant: and would reject the link). Registered
|
||||
-- on LinkUtil's SetItemRef dispatch (Util/LinkUtil.lua) instead of wrapping
|
||||
-- SetItemRef a second time, so there is exactly one override to chain.
|
||||
------------------------------------------------------------------------------
|
||||
LinkUtil.RegisterLinkHandler("trade", function(link, text, linkData, contextData)
|
||||
if IsShiftKeyDown() and ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
|
||||
ChatFrameEditBox:Insert(text);
|
||||
return LinkProcessorResponse.Handled;
|
||||
end
|
||||
if ShowTradeSkillLink(link, text) then
|
||||
return LinkProcessorResponse.Handled;
|
||||
end
|
||||
return LinkProcessorResponse.Unhandled;
|
||||
end);
|
||||
|
||||
local function CreateProfessionLinkButton(parentName, getLink)
|
||||
local parent = _G[parentName];
|
||||
local btnName = parentName .. "LinkButton";
|
||||
@@ -832,24 +67,84 @@ local function CreateProfessionLinkButton(parentName, getLink)
|
||||
GameTooltip:Show();
|
||||
end);
|
||||
btn:SetScript("OnLeave", GameTooltip_Hide);
|
||||
return btn;
|
||||
end
|
||||
|
||||
local TRADE_SKILL_LINKED_NAME_WIDTH = 120;
|
||||
|
||||
-- 4.3.4's TradeSkillLinkNameButton: "[name]" after the title of a linked
|
||||
-- window, a whisper to that player on click. The returned function is
|
||||
-- TradeSkillFrame_SetLinkName. It moves the title left by half the name, from
|
||||
-- the title's own anchor, so the pair stays centered where the title was.
|
||||
local function CreateLinkNameButton(parentName, isLinked)
|
||||
local title = _G[parentName .. "TitleText"];
|
||||
local point, relativeTo, relativePoint, x, y = title:GetPoint();
|
||||
local btn = CreateFrame("Button", parentName .. "LinkNameButton", _G[parentName]);
|
||||
btn:SetHeight(20);
|
||||
btn:SetPoint("LEFT", title, "RIGHT", 5, 0);
|
||||
local text = btn:CreateFontString(btn:GetName() .. "TitleText", "OVERLAY", "GameFontHighlight");
|
||||
text:SetAllPoints(btn);
|
||||
btn:SetScript("OnClick", function()
|
||||
PlaySound("igMainMenuOptionCheckBoxOn");
|
||||
if this.linkedName then
|
||||
ChatFrame_OpenChat(SLASH_WHISPER1 .. " " .. this.linkedName .. " ", DEFAULT_CHAT_FRAME);
|
||||
end
|
||||
end);
|
||||
btn:Hide();
|
||||
|
||||
return function()
|
||||
local _, linkedName = isLinked();
|
||||
title:ClearAllPoints();
|
||||
if linkedName then
|
||||
btn:Show();
|
||||
text:SetText("[" .. linkedName .. "]");
|
||||
local linkedNameWidth = math.min(text:GetStringWidth(), TRADE_SKILL_LINKED_NAME_WIDTH);
|
||||
btn:SetWidth(linkedNameWidth);
|
||||
btn.linkedName = linkedName;
|
||||
title:SetPoint(point, relativeTo, relativePoint, x - linkedNameWidth / 2, y);
|
||||
else
|
||||
btn:Hide();
|
||||
title:SetPoint(point, relativeTo, relativePoint, x, y);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
EventUtil.ContinueOnAddOnLoaded("Blizzard_TradeSkillUI", function()
|
||||
CreateProfessionLinkButton("TradeSkillFrame", C_TradeSkillUI.GetTradeSkillListLink);
|
||||
local linkButton = CreateProfessionLinkButton("TradeSkillFrame", GetTradeSkillListLink);
|
||||
local setLinkName = CreateLinkNameButton("TradeSkillFrame", IsTradeSkillLinked);
|
||||
hooksecurefunc("TradeSkillFrame_SetSelection", function()
|
||||
local linked = IsTradeSkillLinked();
|
||||
for i = 1, table.getn(TRADE_SKILL_CREATE_CONTROLS) do
|
||||
_G[TRADE_SKILL_CREATE_CONTROLS[i]]:SetShown(not linked);
|
||||
end
|
||||
if linkButton then
|
||||
linkButton:SetShown(not linked and GetTradeSkillListLink() ~= nil);
|
||||
end
|
||||
setLinkName();
|
||||
end);
|
||||
end)
|
||||
|
||||
EventUtil.ContinueOnAddOnLoaded("Blizzard_CraftUI", function()
|
||||
CreateProfessionLinkButton("CraftFrame", C_TradeSkillUI.GetCraftListLink)
|
||||
local linkButton = CreateProfessionLinkButton("CraftFrame", GetCraftListLink);
|
||||
local setLinkName = CreateLinkNameButton("CraftFrame", IsCraftLinked);
|
||||
hooksecurefunc("CraftFrame_SetSelection", function()
|
||||
local linked = IsCraftLinked();
|
||||
CraftCreateButton:SetShown(not linked);
|
||||
if linkButton then
|
||||
linkButton:SetShown(not linked and GetCraftListLink() ~= nil);
|
||||
end
|
||||
setLinkName();
|
||||
end);
|
||||
end)
|
||||
|
||||
EventUtil.ContinueOnAddOnLoaded("AdvancedTradeSkillWindow", function()
|
||||
CreateProfessionLinkButton("ATSWFrame", function()
|
||||
return atsw_oldmode and C_TradeSkillUI.GetCraftListLink() or C_TradeSkillUI.GetTradeSkillListLink()
|
||||
return atsw_oldmode and GetCraftListLink() or GetTradeSkillListLink()
|
||||
end)
|
||||
end)
|
||||
|
||||
EventUtil.ContinueOnAddOnLoaded("AdvancedTradeSkillWindow2", function()
|
||||
CreateProfessionLinkButton("ATSWFrame", function()
|
||||
return ATSW_TradeSkill() and C_TradeSkillUI.GetTradeSkillListLink() or C_TradeSkillUI.GetCraftListLink()
|
||||
return ATSW_TradeSkill() and GetTradeSkillListLink() or GetCraftListLink()
|
||||
end)
|
||||
end)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
-- Backport of retail's Blizzard_SharedXML/TypeUtil.lua.
|
||||
|
||||
--These functions are terrible, but they support legacy slash commands.
|
||||
function ValueToBoolean(valueToCheck, defaultValue, defaultReturn)
|
||||
if ( type(valueToCheck) == "nil" ) then
|
||||
return false;
|
||||
elseif ( type(valueToCheck) == "boolean" ) then
|
||||
return valueToCheck;
|
||||
elseif ( type(valueToCheck) == "number" ) then
|
||||
return valueToCheck ~= 0;
|
||||
elseif ( type(valueToCheck) == "string" ) then
|
||||
return StringToBoolean(valueToCheck, defaultReturn);
|
||||
else
|
||||
return defaultReturn;
|
||||
end
|
||||
end
|
||||
|
||||
function StringToBoolean(stringToCheck, defaultReturn)
|
||||
stringToCheck = string.lower(stringToCheck);
|
||||
local firstChar = string.sub(stringToCheck, 1, 1);
|
||||
|
||||
if ( firstChar == "0" or firstChar == "n" or firstChar == "f" or stringToCheck == "off" or stringToCheck == "disabled" ) then
|
||||
return false;
|
||||
elseif ( firstChar == "1" or firstChar == "2" or firstChar == "3" or firstChar == "4" or firstChar == "5" or
|
||||
firstChar == "6" or firstChar == "7" or firstChar == "8" or firstChar == "9" or firstChar == "y" or
|
||||
firstChar == "t" or stringToCheck == "on" or stringToCheck == "enabled" ) then
|
||||
return true;
|
||||
end
|
||||
|
||||
return defaultReturn;
|
||||
end
|
||||
@@ -9,6 +9,7 @@ BINDING_NAME_INTERACTMOUSEOVER = "Mit Mouseover interagieren";
|
||||
BINDING_NAME_INTERACTTARGET = "Mit Ziel interagieren";
|
||||
BINDING_NAME_TARGETFOCUS = "Fokus anvisieren";
|
||||
COMBAT_LABEL = "Kampf";
|
||||
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Zahlen für Abklingzeiten anzeigen";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Seid Ihr sicher, dass Ihr das Ausrüstungsset \"%s\" löschen wollt?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Möchtet Ihr das Ausrüstungsset \"%s\" speichern?";
|
||||
DEBUFF_SYMBOL_CURSE = "Fl";
|
||||
@@ -40,6 +41,7 @@ INTERACT_ICONS_SHOW_ALL = "Alles anzeigen";
|
||||
INTERACT_ICONS_SHOW_NONE = "Nichts anzeigen";
|
||||
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Vordefinierte Tastaturbelegungen für Aktionen reagieren bei gedrückter statt bei losgelassener Taste.";
|
||||
OPTION_TOOLTIP_ACTION_TARGETING = "Ein Zielerfassungssystem, das Gegner auf Grundlage Eurer Blickrichtung dynamisch anvisiert. Funktioniert im Zusammenspiel mit dem Standardzielerfassungssystem.";
|
||||
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Markieren, um für Abklingzeiten einen numerischen Countdown auf Aktionsleisten anzuzeigen.";
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "Erlaubt Interaktion mit NSCs und Objekten per Tastendruck";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "Legt fest, welche Symbole über NSCs und Gegenständen angezeigt werden, wenn die Interaktionstastenfunktion aktiviert ist.";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Automatischen Angriff stoppen, sobald das Ziel gewechselt wird.";
|
||||
@@ -100,6 +102,15 @@ ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "Rüstungsdurchschlag";
|
||||
ITEM_MOD_HEALTH_REGEN_SHORT = "Gesundheitsregeneration";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "Schaden pro Sekunde";
|
||||
LARGE_NUMBER_SEPERATOR = ".";
|
||||
DECIMAL_SEPERATOR = ",";
|
||||
FIRST_NUMBER = "Tausend";
|
||||
FIRST_NUMBER_CAP = " K";
|
||||
FIRST_NUMBER_CAP_NO_SPACE = "K";
|
||||
SECOND_NUMBER = "|7Million:Millionen;";
|
||||
SECOND_NUMBER_CAP = " M";
|
||||
SECOND_NUMBER_CAP_NO_SPACE = "M";
|
||||
THIRD_NUMBER = "|7Milliarde:Milliarden;";
|
||||
FOURTH_NUMBER = "|7Billion:Billionen;";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "Ausrüstungsmanager";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "Neues Set";
|
||||
|
||||
@@ -8,7 +8,88 @@ BINDING_NAME_FOCUSTARGET = "Focus Target";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Interact With Mouseover";
|
||||
BINDING_NAME_INTERACTTARGET = "Interact With Target";
|
||||
BINDING_NAME_TARGETFOCUS = "Target Focus";
|
||||
CLASSICAPI_ACTION_BARS = "Action Bars";
|
||||
CLASSICAPI_ACTION_TARGETING = "Action Targeting";
|
||||
CLASSICAPI_ADVANCED = "Advanced";
|
||||
CLASSICAPI_ALL_SOFT_TARGET_ICONS = "Icons for All Soft Targets";
|
||||
CLASSICAPI_AUTO_DISMOUNT = "Auto Dismount";
|
||||
CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "Dismount when you cast a spell while you are mounted.";
|
||||
CLASSICAPI_AUTO_STAND = "Auto Stand";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "Stand up when you cast a spell while you sit.";
|
||||
CLASSICAPI_AUTO_UNSHIFT = "Auto Cancel Form";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Leave your form when you cast a spell that you cannot cast in it.";
|
||||
CLASSICAPI_BAGS_AND_ITEMS = "Bags and Items";
|
||||
CLASSICAPI_CASTING_AND_ATTACKING = "Casting and Attacking";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION = "Combat Log Retention";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION_TOOLTIP = "How many seconds the combat log keeps entries for addons.";
|
||||
CLASSICAPI_DIRECTION_ANY = "Any direction";
|
||||
CLASSICAPI_DIRECTION_CONE = "Within 30 degrees";
|
||||
CLASSICAPI_DIRECTION_FRONT = "Directly in front";
|
||||
CLASSICAPI_ENEMIES = "Enemies";
|
||||
CLASSICAPI_ENEMY_DIRECTION = "Enemy Direction";
|
||||
CLASSICAPI_ENEMY_ICON = "Show Enemy Icon";
|
||||
CLASSICAPI_ENEMY_ICON_TOOLTIP = "Shows an icon over the enemy that action targeting picks.";
|
||||
CLASSICAPI_ENEMY_RANGE = "Enemy Range";
|
||||
CLASSICAPI_FRIEND_DIRECTION = "Friend Direction";
|
||||
CLASSICAPI_FRIEND_ICON = "Show Friend Icon";
|
||||
CLASSICAPI_FRIEND_ICON_TOOLTIP = "Shows an icon over the friendly unit that friendly action targeting picks.";
|
||||
CLASSICAPI_FRIEND_RANGE = "Friend Range";
|
||||
CLASSICAPI_FRIEND_TARGETING = "Enable Friendly Action Targeting";
|
||||
CLASSICAPI_FRIEND_TARGETING_TOOLTIP = "Picks the nearest friendly unit where you are looking. When you have no target, your helpful spells go to it.";
|
||||
CLASSICAPI_FRIENDS = "Friends";
|
||||
CLASSICAPI_GAME_OBJECT_ICON = "Show Interact Icon on Objects";
|
||||
CLASSICAPI_GAME_OBJECT_ICON_TOOLTIP = "Also shows the interact icon over objects that you cannot target, such as mailboxes and chests.";
|
||||
CLASSICAPI_HIDE_EQUIPMENT_SETS = "Hide Equipment Sets on Items";
|
||||
CLASSICAPI_HIDE_EQUIPMENT_SETS_TOOLTIP = "Removes the Equipment Sets line from item tooltips.";
|
||||
CLASSICAPI_ICON_SIZE = "Icon Size";
|
||||
CLASSICAPI_ICON_SIZE_TOOLTIP = "The size of the interact, enemy and friend icons.";
|
||||
CLASSICAPI_IGNORE_BACKPACK = "Ignore Backpack When Sorting";
|
||||
CLASSICAPI_IGNORE_BACKPACK_TOOLTIP = "Sorting your bags leaves the items in your backpack where they are.";
|
||||
CLASSICAPI_IGNORE_BANK = "Ignore Main Bank When Sorting";
|
||||
CLASSICAPI_IGNORE_BANK_TOOLTIP = "Sorting your bank leaves the items in the main bank slots where they are.";
|
||||
CLASSICAPI_INTERACT_DIRECTION = "Interact Direction";
|
||||
CLASSICAPI_INTERACT_DIRECTION_TOOLTIP = "Where an NPC or object must be for the Interact Key to pick it. The direction is the camera's, not your character's.";
|
||||
CLASSICAPI_INTERACT_ICON = "Show Interact Icon";
|
||||
CLASSICAPI_INTERACT_ICON_TOOLTIP = "Shows an icon over the NPC or object that the Interact Key uses.";
|
||||
CLASSICAPI_INTERACT_KEY = "Interact Key";
|
||||
CLASSICAPI_INTERACT_RANGE = "Interact Range";
|
||||
CLASSICAPI_INTERFACE = "Interface";
|
||||
CLASSICAPI_KEEP_ACTION_TARGETING = "Keep Action Targeting";
|
||||
CLASSICAPI_KEEP_ACTION_TARGETING_TOOLTIP = "Action targeting still picks enemies and friends while you have a target that you chose.";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS = "Loose Target Follows";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_ENEMY = "Soft enemy";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_FRIEND = "Soft friend";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_NONE = "Nothing";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_TOOLTIP = "A target that action targeting set changes with the soft enemy or the soft friend. A target that you chose stays.";
|
||||
CLASSICAPI_LOW_PRIORITY_ICONS = "Show Low Priority Icons";
|
||||
CLASSICAPI_LOW_PRIORITY_ICONS_TOOLTIP = "Also shows an icon over a unit that you can do nothing with, such as a party member. The icon is the normal pointer.";
|
||||
CLASSICAPI_MOTION = "Motion";
|
||||
CLASSICAPI_NAMEPLATES = "Nameplates";
|
||||
CLASSICAPI_NUMBERS = "Numbers";
|
||||
CLASSICAPI_OPTIONS_ABOUT = "Settings for the features that ClassicAPI adds to the game. Each option changes one setting, and the change applies at once.";
|
||||
CLASSICAPI_OTHER_NAMEPLATE_ALPHA = "Other Nameplate Opacity";
|
||||
CLASSICAPI_OTHER_NAMEPLATE_ALPHA_TOOLTIP = "The opacity of the nameplates of units that are not your target.";
|
||||
CLASSICAPI_PREFER_YOUR_TARGET = "Prefer Your Target";
|
||||
CLASSICAPI_PREFER_YOUR_TARGET_TOOLTIP = "When the target that you chose can be picked, action targeting picks it. When this is off, it always picks the nearest unit.";
|
||||
CLASSICAPI_RANGE_TOOLTIP = "The maximum distance in yards. It cannot be more than the targetNearestDistance setting.";
|
||||
CLASSICAPI_REMEMBER_PLAYERS = "Remember Players";
|
||||
CLASSICAPI_REMEMBER_PLAYERS_TOOLTIP = "Saves the name, class, race and sex of the players you meet, so addons can show them in later sessions. All your characters on this realm share the data.";
|
||||
CLASSICAPI_SCRIPT_PROFILE = "Script Profiling";
|
||||
CLASSICAPI_SCRIPT_PROFILE_TOOLTIP = "Measures the CPU time that addons use. The change starts after you reload the interface.";
|
||||
CLASSICAPI_SEPARATE_LARGE_NUMBERS = "Separate Large Numbers";
|
||||
CLASSICAPI_SEPARATE_LARGE_NUMBERS_TOOLTIP = "Puts a separator between each group of three digits, as in 1,234,567, where addons format numbers with BreakUpLargeNumbers.";
|
||||
CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT = "Sort Bags Right to Left";
|
||||
CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT_TOOLTIP = "Sorting puts your items at the end of your last bag, and junk at the start of your backpack.";
|
||||
CLASSICAPI_STRICT_INTERACT_RANGE = "Strict Interact Range";
|
||||
CLASSICAPI_STRICT_INTERACT_RANGE_TOOLTIP = "Never picks an NPC or object past the interact range. When this is off, one that you can use now is picked also when it is farther away.";
|
||||
CLASSICAPI_TARGET_NAMEPLATE_ALPHA = "Target Nameplate Opacity";
|
||||
CLASSICAPI_TARGET_NAMEPLATE_ALPHA_TOOLTIP = "The opacity of your target's nameplate.";
|
||||
CLASSICAPI_UI_SHAKE_STRENGTH = "UI Shake Strength";
|
||||
CLASSICAPI_UI_SHAKE_STRENGTH_TOOLTIP = "Scales how far shaking effects move frames. 0 turns the shake off.";
|
||||
CLASSICAPI_UNIT_DIRECTION_TOOLTIP = "Where a unit must be for action targeting to pick it. The direction is the camera's, not your character's.";
|
||||
CLASSICAPI_WHEN_YOU_HAVE_A_TARGET = "When You Have a Target";
|
||||
COMBAT_LABEL = "Combat";
|
||||
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Show Numbers for Cooldowns";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Are you sure you want to delete the equipment set %s?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Would you like to save the equipment set \"%s\"?";
|
||||
COPPER_AMOUNT = "%d Copper";
|
||||
@@ -41,8 +122,9 @@ INTERACT_ICONS_DEFAULT = "NPCs Only (Default)";
|
||||
INTERACT_ICONS_OPTION = "Interact Key Icons";
|
||||
INTERACT_ICONS_SHOW_ALL = "Show All";
|
||||
INTERACT_ICONS_SHOW_NONE = "Show None";
|
||||
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Action button keybinds will respond on key down, rather than on key up.";
|
||||
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Blizzard Action button keybinds will respond on key down, rather than on key up.";
|
||||
OPTION_TOOLTIP_ACTION_TARGETING = "A targeting system that dynamically targets enemies based on where you're looking. Works alongside the standard targeting system.";
|
||||
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Check this to show a number countdown for action button cooldowns.";
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "Allows you to interact with NPCs and objects using a keypress";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "Controls which icons appear above NPCs and objects when the Interact Key feature is enabled";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Stop auto attacking when you switch targets.";
|
||||
@@ -101,6 +183,15 @@ ITEMS_VARIABLE_QUANTITY = "%d |4Item:Items;";
|
||||
ITEM_MISSING = "%s missing";
|
||||
ITEM_SLOTS_IGNORED = "%d |4slot:slots; ignored";
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
DECIMAL_SEPERATOR = ".";
|
||||
FIRST_NUMBER = "thousand";
|
||||
FIRST_NUMBER_CAP = " K";
|
||||
FIRST_NUMBER_CAP_NO_SPACE = "K";
|
||||
SECOND_NUMBER = "million";
|
||||
SECOND_NUMBER_CAP = " M";
|
||||
SECOND_NUMBER_CAP_NO_SPACE = "M";
|
||||
THIRD_NUMBER = "billion";
|
||||
FOURTH_NUMBER = "trillion";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "Equipment Manager";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "New Set";
|
||||
@@ -180,6 +271,8 @@ SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/use";
|
||||
STOP_AUTO_ATTACK = "Stop Auto Attack";
|
||||
SYMBOLIC_NEGATIVE_NUMBER = "-%s";
|
||||
SYMBOLIC_POSITIVE_NUMBER = "+%s";
|
||||
TARGETFOCUS = "Target Focus";
|
||||
LINK_TRADESKILL_TOOLTIP = "Click here to create a link to your profession.";
|
||||
TEXT_TO_SPEECH = "Text To Speech";
|
||||
|
||||
@@ -9,6 +9,7 @@ BINDING_NAME_INTERACTMOUSEOVER = "Interactuar al pasar el ratón";
|
||||
BINDING_NAME_INTERACTTARGET = "Interactuar con objetivo";
|
||||
BINDING_NAME_TARGETFOCUS = "Seleccionar enfocado";
|
||||
COMBAT_LABEL = "Combate";
|
||||
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Mostrar tiempos de reutilización en cifras";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "¿Seguro que quieres eliminar el equipamiento %s?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "¿Quieres guardar el conjunto de equipamiento \"%s\"?";
|
||||
DEBUFF_SYMBOL_CURSE = "Mal";
|
||||
@@ -40,6 +41,7 @@ INTERACT_ICONS_SHOW_ALL = "Mostrar todos";
|
||||
INTERACT_ICONS_SHOW_NONE = "No mostrar ninguno";
|
||||
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Los atajos de los botones de acción responderán al pulsar una tecla y no al soltarla.";
|
||||
OPTION_TOOLTIP_ACTION_TARGETING = "Un sistema de selección de objetivo que selecciona de forma dinámica a los enemigos en función de la dirección en la que mires. Funciona junto al sistema de selección de objetivos estándar.";
|
||||
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Activa esta opción para mostrar una cuenta atrás numérica para el tiempo de reutilización de los botones de acción.";
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "Te permite interactuar con PNJ y objetos pulsando una tecla.";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "Controla los iconos que aparecen sobre los PNJ y objetos cuando se habilita la opción de tecla Interactuar";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Detiene el ataque automático al cambiar de objetivo.";
|
||||
@@ -100,6 +102,15 @@ ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "Penetración de armadura";
|
||||
ITEM_MOD_HEALTH_REGEN_SHORT = "Regeneración de salud";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "daño por segundo";
|
||||
LARGE_NUMBER_SEPERATOR = "";
|
||||
DECIMAL_SEPERATOR = ",";
|
||||
FIRST_NUMBER = "mil";
|
||||
FIRST_NUMBER_CAP = "mil";
|
||||
FIRST_NUMBER_CAP_NO_SPACE = "mil";
|
||||
SECOND_NUMBER = "|7millón:millones;";
|
||||
SECOND_NUMBER_CAP = "mill.";
|
||||
SECOND_NUMBER_CAP_NO_SPACE = "mill.";
|
||||
THIRD_NUMBER = "mil millones";
|
||||
FOURTH_NUMBER = "|7billón:billones;";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "Gestor de equipamiento";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "Nuevo conjunto";
|
||||
|
||||
@@ -9,6 +9,7 @@ BINDING_NAME_INTERACTMOUSEOVER = "Interagir par survol de la souris";
|
||||
BINDING_NAME_INTERACTTARGET = "Interagir avec la cible";
|
||||
BINDING_NAME_TARGETFOCUS = "Reprendre cible";
|
||||
COMBAT_LABEL = "Combat";
|
||||
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Temps de recharge en chiffres";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Voulez-vous vraiment supprimer l'ensemble d'équipement %s ?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Voulez-vous sauvegarder l'ensemble d'équipement %s ?";
|
||||
DEBUFF_SYMBOL_CURSE = "Mn";
|
||||
@@ -40,6 +41,7 @@ INTERACT_ICONS_SHOW_ALL = "Tout afficher";
|
||||
INTERACT_ICONS_SHOW_NONE = "Ne rien afficher";
|
||||
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Les boutons d’action associés à un raccourci clavier se déclencheront au moment où la touche est enfoncée, et non relâchée.";
|
||||
OPTION_TOOLTIP_ACTION_TARGETING = "Un système de ciblage qui vise en temps réel les adversaires en fonction de l’endroit où vous regardez. Fonctionne en parallèle du système de ciblage standard.";
|
||||
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Cochez pour afficher les temps de recharge sous forme de chiffres sur vos boutons d'action.";
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "Vous permet d’interagir avec les PNJ et les objets en appuyant sur une touche";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "Contrôle les icônes qui s’affichent au-dessus des PNJ et des objets quand la fonction Commande d’interaction est activée";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Interrompt l'attaque automatique lorsque vous changez de cible.";
|
||||
@@ -100,6 +102,15 @@ ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "Pénétration d'armure";
|
||||
ITEM_MOD_HEALTH_REGEN_SHORT = "Régénération des points de vie";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "Dégâts par seconde";
|
||||
LARGE_NUMBER_SEPERATOR = "";
|
||||
DECIMAL_SEPERATOR = ",";
|
||||
FIRST_NUMBER = "|7millier:milliers;";
|
||||
FIRST_NUMBER_CAP = " K";
|
||||
FIRST_NUMBER_CAP_NO_SPACE = " K";
|
||||
SECOND_NUMBER = "|7million:millions;";
|
||||
SECOND_NUMBER_CAP = " M";
|
||||
SECOND_NUMBER_CAP_NO_SPACE = " M";
|
||||
THIRD_NUMBER = "|7milliard:milliards;";
|
||||
FOURTH_NUMBER = "|7millier:milliers; de milliards";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "Gestionnaire d'équipement";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "Nouvel ensemble";
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
if GetLocale() ~= "koKR" then return end
|
||||
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
DECIMAL_SEPERATOR = ".";
|
||||
FIRST_NUMBER = "천";
|
||||
FIRST_NUMBER_CAP = "만";
|
||||
FIRST_NUMBER_CAP_NO_SPACE = "만";
|
||||
SECOND_NUMBER = "만";
|
||||
SECOND_NUMBER_CAP = "억";
|
||||
SECOND_NUMBER_CAP_NO_SPACE = "억";
|
||||
THIRD_NUMBER = "억";
|
||||
FOURTH_NUMBER = "조";
|
||||
@@ -3,6 +3,7 @@
|
||||
<Script file="deDE.lua"/>
|
||||
<Script file="esES.lua"/>
|
||||
<Script file="frFR.lua"/>
|
||||
<Script file="koKR.lua"/>
|
||||
<Script file="ruRU.lua"/>
|
||||
<Script file="zhCN.lua"/>
|
||||
<Script file="zhTW.lua"/>
|
||||
|
||||
@@ -9,6 +9,7 @@ BINDING_NAME_INTERACTMOUSEOVER = "Взаимодействовать указа
|
||||
BINDING_NAME_INTERACTTARGET = "Взаимодействовать с целью";
|
||||
BINDING_NAME_TARGETFOCUS = "Восстановить цель";
|
||||
COMBAT_LABEL = "Бой";
|
||||
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Отсчет времени восстановления";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Вы действительно хотите удалить комплект экипировки \"%s\"?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Хотите сохранить комплект экипировки \"%s\"?";
|
||||
DEBUFF_SYMBOL_CURSE = "Пр";
|
||||
@@ -40,6 +41,7 @@ INTERACT_ICONS_SHOW_ALL = "Показать все";
|
||||
INTERACT_ICONS_SHOW_NONE = "Не показывать";
|
||||
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Способности будут задействоваться, когда пользователь нажимает, а не отпускает клавишу.";
|
||||
OPTION_TOOLTIP_ACTION_TARGETING = "Система захвата целей, динамически выбирающая цели в поле зрения. Работает параллельно со стандартной системой наведения.";
|
||||
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Отображение обратного отсчета для времени восстановления на панели команд.";
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "Позволяет взаимодействовать с NPC и объектами по нажатию клавиши.";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "Определяет, какие значки появляются над NPC и объектами, если включен параметр клавиши взаимодействия.";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Прекращение автоматической атаки при смене цели.";
|
||||
@@ -100,6 +102,15 @@ ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "к пробиванию брони";
|
||||
ITEM_MOD_HEALTH_REGEN_SHORT = "к восполнению здоровья";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "к урону в секунду";
|
||||
LARGE_NUMBER_SEPERATOR = "";
|
||||
DECIMAL_SEPERATOR = ",";
|
||||
FIRST_NUMBER = "тыс.";
|
||||
FIRST_NUMBER_CAP = "Т";
|
||||
FIRST_NUMBER_CAP_NO_SPACE = "Т";
|
||||
SECOND_NUMBER = "млн";
|
||||
SECOND_NUMBER_CAP = "М";
|
||||
SECOND_NUMBER_CAP_NO_SPACE = "М";
|
||||
THIRD_NUMBER = "млрд";
|
||||
FOURTH_NUMBER = "трлн";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "Комплекты экипировки";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "Новый комплект";
|
||||
|
||||
@@ -9,6 +9,7 @@ BINDING_NAME_INTERACTMOUSEOVER = "与鼠标悬停处互动";
|
||||
BINDING_NAME_INTERACTTARGET = "与目标互动";
|
||||
BINDING_NAME_TARGETFOCUS = "目标焦点";
|
||||
COMBAT_LABEL = "战斗";
|
||||
COUNTDOWN_FOR_COOLDOWNS_TEXT = "显示冷却时间";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "你确认要删除装备方案%s吗?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "你想要保存装备方案\"%s\"吗?";
|
||||
DEBUFF_SYMBOL_CURSE = "诅";
|
||||
@@ -40,6 +41,7 @@ INTERACT_ICONS_SHOW_ALL = "显示全部";
|
||||
INTERACT_ICONS_SHOW_NONE = "全不显示";
|
||||
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "在按下快捷键时施法,而不是在松开快捷键时施法。";
|
||||
OPTION_TOOLTIP_ACTION_TARGETING = "此瞄准系统可以以你观看的方向动态地瞄准敌人。可以和标准的瞄准系统一起使用。";
|
||||
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "勾选此项后,动作条上的按钮将以倒计时数字的方式显示冷却时间。";
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "使你可以通过按键与NPC和物体进行交互";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "控制交互按键启用后NPC和物体上方出现的图标";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "当你切换目标时停止自动攻击。";
|
||||
@@ -100,6 +102,15 @@ ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "护甲穿透";
|
||||
ITEM_MOD_HEALTH_REGEN_SHORT = "生命值恢复";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "每秒伤害";
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
DECIMAL_SEPERATOR = ".";
|
||||
FIRST_NUMBER = "千";
|
||||
FIRST_NUMBER_CAP = "万";
|
||||
FIRST_NUMBER_CAP_NO_SPACE = "万";
|
||||
SECOND_NUMBER = "万";
|
||||
SECOND_NUMBER_CAP = "亿";
|
||||
SECOND_NUMBER_CAP_NO_SPACE = "亿";
|
||||
THIRD_NUMBER = "亿";
|
||||
FOURTH_NUMBER = "兆";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "装备管理";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "新的方案";
|
||||
|
||||
@@ -9,6 +9,7 @@ BINDING_NAME_INTERACTMOUSEOVER = "與滑鼠指定的目標互動";
|
||||
BINDING_NAME_INTERACTTARGET = "與目標互動";
|
||||
BINDING_NAME_TARGETFOCUS = "選定鎖定目標";
|
||||
COMBAT_LABEL = "戰鬥";
|
||||
COUNTDOWN_FOR_COOLDOWNS_TEXT = "顯示冷卻時間";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "你確定要刪除裝備設定:%s嗎?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "你想要儲存裝備設定「%s」嗎?";
|
||||
DEBUFF_SYMBOL_CURSE = "咒";
|
||||
@@ -40,6 +41,7 @@ INTERACT_ICONS_SHOW_ALL = "顯示全部";
|
||||
INTERACT_ICONS_SHOW_NONE = "不顯示圖示";
|
||||
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "快捷鍵按鍵設定會在按下按鍵時生效,而不是在鬆開按鍵的時候。";
|
||||
OPTION_TOOLTIP_ACTION_TARGETING = "根據你的視線方向,動態鎖定敵人的鎖定系統。與標準鎖定系統相輔相成。";
|
||||
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "勾選此選項可在快捷鍵上面直接顯示冷卻時間的倒數計時。";
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "可透過按鍵與NPC和物件互動";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "在啟用互動鍵功能後,控制出現在NPC和物件上方的圖示";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "當你切換目標時停止自動攻擊。";
|
||||
@@ -100,6 +102,15 @@ ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "護甲穿透";
|
||||
ITEM_MOD_HEALTH_REGEN_SHORT = "生命力恢復";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "每秒傷害";
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
DECIMAL_SEPERATOR = ".";
|
||||
FIRST_NUMBER = "千";
|
||||
FIRST_NUMBER_CAP = "萬";
|
||||
FIRST_NUMBER_CAP_NO_SPACE = "萬";
|
||||
SECOND_NUMBER = "萬";
|
||||
SECOND_NUMBER_CAP = "億";
|
||||
SECOND_NUMBER_CAP_NO_SPACE = "億";
|
||||
THIRD_NUMBER = "億";
|
||||
FOURTH_NUMBER = "兆";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "裝備管理員";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "新設定";
|
||||
|
||||
@@ -13,3 +13,13 @@ TableInspectorDataProvider.lua
|
||||
TableInspectorAnchorDataProvider.xml
|
||||
TableInspectorAttributeDataProvider.xml
|
||||
TableInspector.xml
|
||||
Profiler\Profiler.xml
|
||||
Profiler\Core.lua
|
||||
Profiler\Track.lua
|
||||
Profiler\Widget.lua
|
||||
Profiler\Window.lua
|
||||
Profiler\AddonsTab.lua
|
||||
Profiler\FramesTab.lua
|
||||
Profiler\EventsTab.lua
|
||||
Profiler\GlobalsTab.lua
|
||||
Profiler\FrameStack.lua
|
||||
|
||||
@@ -26,9 +26,12 @@
|
||||
-- the time column without re-anchoring frames.
|
||||
--
|
||||
-- The table inspector (/tinspect, Ctrl in /fstack) is retail's
|
||||
-- Blizzard_TableInspector; see TableInspector.lua.
|
||||
-- Blizzard_TableInspector; see TableInspector.lua. The profiler (/profiler)
|
||||
-- is in Profiler/, and adds its owner and CPU lines to /fstack.
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local _, ns = ...;
|
||||
|
||||
EVENT_TRACE_EVENT_HEIGHT = 16;
|
||||
EVENT_TRACE_MAX_ENTRIES = 1000;
|
||||
-- Toolbar above the event list (Pause + Args/Time toggles + Search) eats
|
||||
@@ -1027,20 +1030,6 @@ end
|
||||
|
||||
FRAMESTACK_UPDATE_TIME = .1;
|
||||
|
||||
-- Blizzard_SharedXML's StringToBoolean, for the /framestack arguments.
|
||||
local function _StringToBoolean(stringToCheck, defaultReturn)
|
||||
stringToCheck = string.lower(stringToCheck);
|
||||
local firstChar = string.sub(stringToCheck, 1, 1);
|
||||
if (firstChar == "0" or firstChar == "n" or firstChar == "f" or
|
||||
stringToCheck == "off" or stringToCheck == "disabled") then
|
||||
return false;
|
||||
elseif (string.find(firstChar, "^[1-9yt]") or
|
||||
stringToCheck == "on" or stringToCheck == "enabled") then
|
||||
return true;
|
||||
end
|
||||
return defaultReturn;
|
||||
end
|
||||
|
||||
local function _WrapColor(text, colorHex)
|
||||
return "|c" .. colorHex .. text .. "|r";
|
||||
end
|
||||
@@ -1198,6 +1187,8 @@ function FrameStackTooltip_OnTooltipSetFrameStack(self, highlightFrame)
|
||||
if (fsobj) then
|
||||
self:AddLine(string.format("\nfsobj = %s", fsobj:GetDebugName()));
|
||||
end
|
||||
|
||||
ns.AddFrameStackLines(self, self.highlightFrame);
|
||||
end
|
||||
|
||||
function FrameStackTooltip_Show(self, showHidden, showRegions)
|
||||
@@ -1266,8 +1257,8 @@ function FrameStackTooltip_HandleSlashCmd(msg)
|
||||
showHiddenArg, msg = string.match(msg or "", "^%s*(%S+)(.*)$");
|
||||
showRegionsArg, msg = string.match(msg or "", "^%s*(%S+)(.*)$");
|
||||
|
||||
local showHidden = _StringToBoolean(showHiddenArg or "", false);
|
||||
local showRegions = _StringToBoolean(showRegionsArg or "", true);
|
||||
local showHidden = StringToBoolean(showHiddenArg or "", false);
|
||||
local showRegions = StringToBoolean(showRegionsArg or "", true);
|
||||
|
||||
FrameStackTooltip_Toggle(showHidden, showRegions);
|
||||
end
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
-- Addons tab: the memory, allocation rate and CPU time of each addon.
|
||||
local _, ns = ...
|
||||
|
||||
local stats = ns.stats
|
||||
|
||||
local function Profiled(fn)
|
||||
return function(addon)
|
||||
if ns.profiling then return fn(addon) end
|
||||
end
|
||||
end
|
||||
|
||||
local tab = ns.AddTab("Addons")
|
||||
|
||||
tab.list = ns.CreateList(tab.frame, {
|
||||
sort = 3,
|
||||
columns = {
|
||||
{
|
||||
title = "Addon",
|
||||
text = function(addon) return addon.title end,
|
||||
value = function(addon) return strlower(addon.name) end,
|
||||
},
|
||||
{
|
||||
title = "Memory", width = 70,
|
||||
tip = "The Lua memory that the addon's objects use now.",
|
||||
text = function(addon) return ns.KB(addon.mem) end,
|
||||
value = function(addon) return addon.mem end,
|
||||
},
|
||||
{
|
||||
title = "Alloc/s", width = 76,
|
||||
tip = "The Lua memory the addon allocated in the last second. Allocations become garbage that the collector must free. A second with a garbage collection keeps the previous value.",
|
||||
text = function(addon) return ns.KBRate(addon.memRate) end,
|
||||
value = function(addon) return addon.memRate end,
|
||||
},
|
||||
{
|
||||
title = "Peak/s", width = 76,
|
||||
tip = "The highest Alloc/s since the last reset.",
|
||||
text = function(addon) return ns.KBRate(addon.peakRate) end,
|
||||
value = function(addon) return addon.peakRate end,
|
||||
},
|
||||
{
|
||||
title = "CPU/s", width = 64,
|
||||
tip = "The CPU time of the addon's own functions in the last second. Needs CPU profiling.",
|
||||
text = Profiled(function(addon) return ns.MS(addon.cpuRate) end),
|
||||
value = Profiled(function(addon) return addon.cpuRate end),
|
||||
},
|
||||
{
|
||||
title = "CPU %", width = 50,
|
||||
tip = "The addon's share of the CPU time of all addons in the last second.",
|
||||
text = Profiled(function(addon) return addon.cpuShare and format("%.1f%%", addon.cpuShare) end),
|
||||
value = Profiled(function(addon) return addon.cpuShare end),
|
||||
},
|
||||
{
|
||||
title = "CPU Total", width = 70,
|
||||
tip = "The CPU time of the addon's own functions since the last reset.",
|
||||
text = Profiled(function(addon) return ns.MS(addon.cpu) end),
|
||||
value = Profiled(function(addon) return addon.cpu end),
|
||||
},
|
||||
},
|
||||
|
||||
tooltip = function(addon)
|
||||
GameTooltip:AddLine(addon.title)
|
||||
GameTooltip:AddLine(addon.name, .6, .6, .6)
|
||||
GameTooltip:AddLine(" ")
|
||||
GameTooltip:AddDoubleLine("Memory", ns.KB(addon.mem), 1, 1, 1, 1, 1, 1)
|
||||
GameTooltip:AddDoubleLine("Allocated per second", ns.KBRate(addon.memRate), 1, 1, 1, 1, 1, 1)
|
||||
GameTooltip:AddDoubleLine("Peak per second", ns.KBRate(addon.peakRate), 1, 1, 1, 1, 1, 1)
|
||||
if ns.profiling then
|
||||
GameTooltip:AddDoubleLine("CPU per second", ns.MS(addon.cpuRate), 1, 1, 1, 1, 1, 1)
|
||||
GameTooltip:AddDoubleLine("CPU since reset", ns.MS(addon.cpu), 1, 1, 1, 1, 1, 1)
|
||||
else
|
||||
GameTooltip:AddLine("Turn on CPU profiling to see CPU times.", .6, .6, .6)
|
||||
end
|
||||
GameTooltip:AddLine(" ")
|
||||
GameTooltip:AddLine("Click: show its frames. Right-click: more.", .6, .6, .6)
|
||||
end,
|
||||
|
||||
click = function(addon)
|
||||
ns.ShowIn("Frames", addon.name)
|
||||
end,
|
||||
|
||||
menu = function(addon, root)
|
||||
root:CreateTitle(addon.name)
|
||||
root:CreateButton("Show frames", function() ns.ShowIn("Frames", addon.name) end)
|
||||
root:CreateButton("Show globals", function() ns.ShowIn("Globals", addon.name) end)
|
||||
root:CreateButton("Copy name", function() ns.Copy(addon.name) end)
|
||||
end,
|
||||
})
|
||||
tab.list:SetAllPoints()
|
||||
|
||||
function tab:OnSelect()
|
||||
self.list:SetEntries(stats.list)
|
||||
end
|
||||
|
||||
function tab:OnSample()
|
||||
self.list:SetEntries(stats.list)
|
||||
end
|
||||
|
||||
function tab:Status()
|
||||
local text = format("%d addons Lua %s / %s", #stats.list, ns.KB(stats.mem or 0), ns.KB(stats.threshold or 0))
|
||||
if ns.profiling and stats.scriptRate then
|
||||
text = text .. format(" scripts %s/s events %s/s (%.0f fired/s)",
|
||||
ns.MS(stats.scriptRate), ns.MS(stats.eventRate), stats.eventCountRate)
|
||||
end
|
||||
return text
|
||||
end
|
||||
@@ -0,0 +1,283 @@
|
||||
-- Profiler core: shared helpers, the once-a-second sampler and the per-addon
|
||||
-- memory and CPU statistics every other profiler file reads.
|
||||
--
|
||||
-- Based on pfDebug (https://github.com/shagu/pfDebug):
|
||||
--
|
||||
-- MIT License
|
||||
--
|
||||
-- Copyright (c) 2018 Shagu
|
||||
--
|
||||
-- Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
-- of this software and associated documentation files (the "Software"), to deal
|
||||
-- in the Software without restriction, including without limitation the rights
|
||||
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
-- copies of the Software, and to permit persons to whom the Software is
|
||||
-- furnished to do so, subject to the following conditions:
|
||||
--
|
||||
-- The above copyright notice and this permission notice shall be included in all
|
||||
-- copies or substantial portions of the Software.
|
||||
--
|
||||
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
-- SOFTWARE.
|
||||
local _, ns = ...
|
||||
|
||||
ns.INTERVAL = 1 -- seconds between samples
|
||||
|
||||
-- whether CPU profiling runs in this session. scriptProfile takes effect at
|
||||
-- a reload and these tools load on demand, so the cvar can already hold the
|
||||
-- next session's value: ask the profiler whether it counted a call instead.
|
||||
local function Probe() end
|
||||
Probe()
|
||||
ns.profiling = select(2, GetFunctionCPUUsage(Probe)) > 0
|
||||
|
||||
-- [[ styling ]]
|
||||
function ns.CreateBackdrop(frame)
|
||||
frame:SetBackdrop({
|
||||
bgFile = "Interface\\BUTTONS\\WHITE8X8", tile = false, tileSize = 0,
|
||||
edgeFile = "Interface\\BUTTONS\\WHITE8X8", edgeSize = 1,
|
||||
insets = {left = -1, right = -1, top = -1, bottom = -1},
|
||||
})
|
||||
frame:SetBackdropColor(0, 0, 0, .75)
|
||||
frame:SetBackdropBorderColor(.1, .1, .1, 1)
|
||||
end
|
||||
|
||||
-- [[ formatting ]]
|
||||
ns.ACCENT = "|cff33ffcc"
|
||||
ns.GOLD = "|cffffcc00"
|
||||
ns.GREY = "|cffaaaaaa"
|
||||
ns.WHITE = "|cffffffff"
|
||||
ns.RED = "|cffff5555"
|
||||
|
||||
function ns.Print(msg)
|
||||
DEFAULT_CHAT_FRAME:AddMessage(ns.ACCENT .. "Profiler|r: " .. msg)
|
||||
end
|
||||
|
||||
function ns.KB(kb)
|
||||
if math.abs(kb) >= 1024 then return format("%.1f MB", kb / 1024) end
|
||||
return format("%.0f KB", kb)
|
||||
end
|
||||
|
||||
function ns.KBRate(kbps)
|
||||
if not kbps then return "-" end
|
||||
if math.abs(kbps) >= 1024 then return format("%.2f MB/s", kbps / 1024) end
|
||||
return format("%.1f KB/s", kbps)
|
||||
end
|
||||
|
||||
function ns.MS(ms)
|
||||
if not ms then return "-" end
|
||||
if ms >= 10000 then return format("%.1f s", ms / 1000) end
|
||||
if ms >= 100 then return format("%.0f ms", ms) end
|
||||
if ms >= 1 then return format("%.1f ms", ms) end
|
||||
return format("%.3f ms", ms)
|
||||
end
|
||||
|
||||
function ns.Count(n)
|
||||
if not n then return "-" end
|
||||
return BreakUpLargeNumbers(floor(n + .5))
|
||||
end
|
||||
|
||||
-- [[ ownership: which addon made a table, function or frame ]]
|
||||
-- nil for objects the game's own code made (FrameXML or the engine), "script"
|
||||
-- for objects a macro or /run made.
|
||||
function ns.Owner(value)
|
||||
local tag = _classicapi_GcTagOf(value)
|
||||
if tag and strsub(tag, 1, 2) == "0x" then return "script" end
|
||||
return tag
|
||||
end
|
||||
|
||||
function ns.OwnerLabel(owner)
|
||||
return owner or "FrameXML"
|
||||
end
|
||||
|
||||
-- a frame's name as the debug tools show it
|
||||
function ns.FrameName(frame)
|
||||
local name = frame:GetDebugName()
|
||||
if name == "" then return "<" .. frame:GetObjectType() .. ">" end
|
||||
return name
|
||||
end
|
||||
|
||||
-- [[ events of this module: sample and reset listeners ]]
|
||||
local sampleListeners, resetListeners = {}, {}
|
||||
|
||||
function ns.OnSample(fn) tinsert(sampleListeners, fn) end
|
||||
function ns.OnReset(fn) tinsert(resetListeners, fn) end
|
||||
|
||||
-- [[ per-addon statistics ]]
|
||||
-- addon rates are deltas between two samples. A garbage collection between
|
||||
-- the samples frees memory of every addon, so a window with a collection
|
||||
-- holds the previous allocation rate.
|
||||
local stats = {
|
||||
addons = {}, -- [name] = record
|
||||
list = {}, -- loaded addon records, in load order
|
||||
}
|
||||
ns.stats = stats
|
||||
|
||||
local gc = { count = 0 }
|
||||
ns.gc = gc
|
||||
|
||||
local collected = false
|
||||
local lastMem, lastThreshold = gcinfo()
|
||||
|
||||
local function NewAddon(index, name, title)
|
||||
local addon = { index = index, name = name, title = title, mem = 0, cpu = 0, peakRate = 0 }
|
||||
addon.search = strlower(name .. " " .. title)
|
||||
stats.addons[name] = addon
|
||||
tinsert(stats.list, addon)
|
||||
return addon
|
||||
end
|
||||
|
||||
-- a cumulative counter's growth since `last`, where a drop means a reset
|
||||
local function Delta(now, last)
|
||||
if not last or now < last then return now end
|
||||
return now - last
|
||||
end
|
||||
ns.Delta = Delta
|
||||
|
||||
local lastScript, lastEventTime, lastEventCount
|
||||
|
||||
local function Sample(dt)
|
||||
local mem, threshold = gcinfo()
|
||||
stats.collected = collected
|
||||
stats.memRate = not collected and stats.mem and (mem - stats.mem) / dt or nil
|
||||
stats.mem, stats.threshold = mem, threshold
|
||||
collected = false
|
||||
|
||||
UpdateAddOnMemoryUsage()
|
||||
if ns.profiling then UpdateAddOnCPUUsage() end
|
||||
|
||||
local cpuSum, topMem, topCpu = 0, nil, nil
|
||||
for i = 1, GetNumAddOns() do
|
||||
if IsAddOnLoaded(i) then
|
||||
local name, title = GetAddOnInfo(i)
|
||||
local addon = stats.addons[name] or NewAddon(i, name, title)
|
||||
|
||||
local addonMem = GetAddOnMemoryUsage(i)
|
||||
if not stats.collected and addon.sampled then
|
||||
addon.memRate = (addonMem - addon.mem) / dt
|
||||
addon.peakRate = max(addon.peakRate, addon.memRate)
|
||||
end
|
||||
addon.mem, addon.sampled = addonMem, true
|
||||
|
||||
if ns.profiling then
|
||||
local cpu = GetAddOnCPUUsage(i)
|
||||
addon.cpuRate = Delta(cpu, addon.cpu) / dt
|
||||
addon.cpu = cpu
|
||||
cpuSum = cpuSum + addon.cpuRate
|
||||
if not topCpu or addon.cpuRate > topCpu.cpuRate then topCpu = addon end
|
||||
end
|
||||
|
||||
if addon.memRate and (not topMem or addon.memRate > topMem.memRate) then topMem = addon end
|
||||
end
|
||||
end
|
||||
|
||||
for _, addon in ipairs(stats.list) do
|
||||
addon.cpuShare = cpuSum > 0 and addon.cpuRate and addon.cpuRate / cpuSum * 100 or nil
|
||||
end
|
||||
stats.cpuSum, stats.topMem, stats.topCpu = cpuSum, topMem, topCpu
|
||||
|
||||
if ns.profiling then
|
||||
local script = GetScriptCPUUsage()
|
||||
local eventTime, eventCount = GetEventCPUUsage()
|
||||
stats.scriptRate = Delta(script, lastScript) / dt
|
||||
stats.eventRate = Delta(eventTime, lastEventTime) / dt
|
||||
stats.eventCountRate = Delta(eventCount, lastEventCount) / dt
|
||||
lastScript, lastEventTime, lastEventCount = script, eventTime, eventCount
|
||||
end
|
||||
|
||||
for _, fn in ipairs(sampleListeners) do fn(dt) end
|
||||
end
|
||||
|
||||
-- the counters as they are now, so the first sample's rates cover only its
|
||||
-- own second, not the time since the UI loaded
|
||||
local function Baseline()
|
||||
stats.mem, stats.threshold = gcinfo()
|
||||
lastMem, lastThreshold = stats.mem, stats.threshold
|
||||
|
||||
UpdateAddOnMemoryUsage()
|
||||
if ns.profiling then UpdateAddOnCPUUsage() end
|
||||
for i = 1, GetNumAddOns() do
|
||||
if IsAddOnLoaded(i) then
|
||||
local name, title = GetAddOnInfo(i)
|
||||
local addon = stats.addons[name] or NewAddon(i, name, title)
|
||||
addon.mem, addon.sampled = GetAddOnMemoryUsage(i), true
|
||||
if ns.profiling then addon.cpu = GetAddOnCPUUsage(i) end
|
||||
end
|
||||
end
|
||||
|
||||
if ns.profiling then
|
||||
lastScript = GetScriptCPUUsage()
|
||||
lastEventTime, lastEventCount = GetEventCPUUsage()
|
||||
end
|
||||
end
|
||||
|
||||
function ns.Reset()
|
||||
ResetCPUUsage()
|
||||
lastScript, lastEventTime, lastEventCount = nil, nil, nil
|
||||
for _, addon in ipairs(stats.list) do
|
||||
addon.cpu, addon.peakRate = 0, 0
|
||||
end
|
||||
for _, fn in ipairs(resetListeners) do fn() end
|
||||
end
|
||||
|
||||
-- [[ the sampler: every frame, from the first open of the widget or the
|
||||
-- analyzer until /reload ]]
|
||||
-- The debug tools also load for /dump and the rest, so the profiler costs
|
||||
-- nothing until it is opened.
|
||||
local sampler = CreateFrame("Frame")
|
||||
sampler:Hide()
|
||||
local elapsedSinceSample = 0
|
||||
|
||||
-- spot garbage collections: Lua 5.0 sets a new threshold after each
|
||||
-- collection, so a changed threshold means a collection ran
|
||||
sampler:SetScript("OnUpdate", function(self, elapsed)
|
||||
local mem, threshold = gcinfo()
|
||||
if threshold ~= lastThreshold then
|
||||
collected = true
|
||||
gc.count = gc.count + 1
|
||||
gc.time, gc.clock, gc.freed = GetTime(), date("%H:%M:%S"), lastMem - mem
|
||||
end
|
||||
lastMem, lastThreshold = mem, threshold
|
||||
|
||||
elapsedSinceSample = elapsedSinceSample + elapsed
|
||||
if elapsedSinceSample >= ns.INTERVAL then
|
||||
Sample(elapsedSinceSample)
|
||||
elapsedSinceSample = 0
|
||||
end
|
||||
end)
|
||||
|
||||
function ns.StartSampling()
|
||||
if sampler:IsShown() then return end
|
||||
Baseline()
|
||||
sampler:Show()
|
||||
end
|
||||
|
||||
-- [[ CPU profiling switch: the cvar takes effect after a reload ]]
|
||||
function ns.SetProfiling(enable)
|
||||
SetCVar("scriptProfile", enable and "1" or "0")
|
||||
ReloadUI()
|
||||
end
|
||||
|
||||
-- [[ /profiler ]]
|
||||
function ClassicAPIProfiler_HandleSlashCmd(msg)
|
||||
local cmd, arg = strmatch(strlower(msg or ""), "^(%S*)%s*(.-)$")
|
||||
if cmd == "" then
|
||||
ns.window:SetShown(not ns.window:IsShown())
|
||||
elseif cmd == "widget" then
|
||||
ns.widget:SetShown(not ns.widget:IsShown())
|
||||
elseif cmd == "reset" then
|
||||
ns.Reset()
|
||||
ns.Print("CPU and allocation counters reset.")
|
||||
elseif cmd == "profile" and (arg == "on" or arg == "off") then
|
||||
ns.SetProfiling(arg == "on")
|
||||
else
|
||||
ns.Print("/profiler - toggle the analyzer")
|
||||
ns.Print("/profiler widget - toggle the memory widget")
|
||||
ns.Print("/profiler reset - reset CPU and allocation counters")
|
||||
ns.Print("/profiler profile on|off - CPU profiling (reloads the UI)")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,124 @@
|
||||
-- Events tab: how often each event fires while the analyzer is open, how
|
||||
-- many frames listen to it, and (deep tracking) what its handlers cost.
|
||||
local _, ns = ...
|
||||
|
||||
local track = ns.track
|
||||
|
||||
local function Tracked(fn)
|
||||
return function(ev)
|
||||
if track.deep then return fn(ev) end
|
||||
end
|
||||
end
|
||||
|
||||
local function Line(left, right)
|
||||
GameTooltip:AddDoubleLine(left, right, 1, 1, 1, 1, 1, 1)
|
||||
end
|
||||
|
||||
local function Tooltip(ev)
|
||||
GameTooltip:AddLine(ev.name)
|
||||
Line("Fired", ns.Count(ev.fires) .. ns.GREY .. " (" .. format("%.1f", ev.fireRate or 0) .. "/s)")
|
||||
if track.deep then
|
||||
Line("Handlers ran", ns.Count(ev.dispatches))
|
||||
if ns.profiling then Line("Handler CPU", ns.MS(ev.cpu)) end
|
||||
Line("Allocated", ns.KB(ev.alloc))
|
||||
end
|
||||
|
||||
local listeners = {}
|
||||
for _, frame in ipairs({ GetFramesRegisteredForEvent(ev.name) }) do
|
||||
if frame ~= track.capture then
|
||||
local rec = track.frames[frame]
|
||||
tinsert(listeners, { frame = frame, stat = rec and rec.events[ev.name] })
|
||||
end
|
||||
end
|
||||
if #listeners == 0 then return end
|
||||
|
||||
table.sort(listeners, function(a, b)
|
||||
local ca, cb = a.stat and a.stat.cpu or -1, b.stat and b.stat.cpu or -1
|
||||
return ca > cb
|
||||
end)
|
||||
|
||||
GameTooltip:AddLine(" ")
|
||||
GameTooltip:AddLine("Listeners", 1, .8, 0)
|
||||
for i = 1, min(#listeners, 15) do
|
||||
local frame, stat = listeners[i].frame, listeners[i].stat
|
||||
local text = ns.GREY .. ns.OwnerLabel(ns.Owner(frame:GetScript("OnEvent") or frame))
|
||||
if stat then
|
||||
if ns.profiling then text = text .. " " .. ns.WHITE .. ns.MS(stat.cpu) end
|
||||
text = text .. " " .. ns.WHITE .. ns.KB(stat.alloc)
|
||||
end
|
||||
Line(ns.FrameName(frame), text)
|
||||
end
|
||||
if #listeners > 15 then
|
||||
GameTooltip:AddLine("and " .. (#listeners - 15) .. " more", .6, .6, .6)
|
||||
end
|
||||
end
|
||||
|
||||
local tab = ns.AddTab("Events")
|
||||
|
||||
tab.list = ns.CreateList(tab.frame, {
|
||||
sort = 2,
|
||||
columns = {
|
||||
{
|
||||
title = "Event",
|
||||
text = function(ev) return ev.name end,
|
||||
value = function(ev) return ev.search end,
|
||||
},
|
||||
{
|
||||
title = "Fires/s", width = 60,
|
||||
tip = "How often the event fired in the last second.",
|
||||
text = function(ev) return ev.fireRate and format("%.1f", ev.fireRate) end,
|
||||
value = function(ev) return ev.fireRate end,
|
||||
},
|
||||
{
|
||||
title = "Listeners", width = 60,
|
||||
tip = "The frames registered for the event.",
|
||||
text = function(ev) return ev.listeners end,
|
||||
value = function(ev) return ev.listeners end,
|
||||
},
|
||||
{
|
||||
title = "CPU/s", width = 64,
|
||||
tip = "The CPU time of the event's OnEvent handlers in the last second. Needs deep tracking and CPU profiling.",
|
||||
text = Tracked(function(ev) return ns.profiling and ns.MS(ev.cpuRate) end),
|
||||
value = Tracked(function(ev) return ns.profiling and ev.cpuRate or nil end),
|
||||
},
|
||||
{
|
||||
title = "Alloc/s", width = 72,
|
||||
tip = "The Lua memory the event's OnEvent handlers allocated in the last second. Needs deep tracking.",
|
||||
text = Tracked(function(ev) return ns.KBRate(ev.allocRate) end),
|
||||
value = Tracked(function(ev) return ev.allocRate end),
|
||||
},
|
||||
{
|
||||
title = "Fired", width = 64,
|
||||
tip = "How often the event fired since the analyzer opened or the last reset.",
|
||||
text = function(ev) return ns.Count(ev.fires) end,
|
||||
value = function(ev) return ev.fires end,
|
||||
},
|
||||
},
|
||||
|
||||
tooltip = Tooltip,
|
||||
|
||||
menu = function(ev, root)
|
||||
root:CreateTitle(ev.name)
|
||||
root:CreateButton("Copy name", function() ns.Copy(ev.name) end)
|
||||
end,
|
||||
})
|
||||
tab.list:SetAllPoints()
|
||||
|
||||
local function Update(self)
|
||||
local entries = {}
|
||||
for _, ev in pairs(track.events) do tinsert(entries, ev) end
|
||||
self.list:SetEntries(entries)
|
||||
end
|
||||
|
||||
tab.OnSelect = Update
|
||||
tab.OnSample = Update
|
||||
|
||||
function tab:Status()
|
||||
local fires = 0
|
||||
for _, ev in pairs(track.events) do fires = fires + (ev.fireRate or 0) end
|
||||
local text = format("%d events seen %.0f fired/s", #self.list.entries, fires)
|
||||
if not track.deep then
|
||||
return text .. " " .. ns.GOLD .. "Turn on deep tracking for the CPU time and memory of each event."
|
||||
end
|
||||
return text
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
-- /fstack: the owner, script CPU time and allocations of the highlighted
|
||||
-- frame, which FrameStackTooltip_OnTooltipSetFrameStack adds to the tooltip.
|
||||
local _, ns = ...
|
||||
|
||||
local function Line(tooltip, left, right)
|
||||
tooltip:AddDoubleLine(left, right, .2, 1, .8, 1, 1, 1)
|
||||
end
|
||||
|
||||
function ns.AddFrameStackLines(tooltip, region)
|
||||
if not region then return end
|
||||
-- a texture or font string runs no scripts: report its frame's
|
||||
local frame = region.GetScript and region or region:GetParent()
|
||||
|
||||
tooltip:AddLine(" ")
|
||||
Line(tooltip, "Owner", ns.OwnerLabel(ns.Owner(region)))
|
||||
if not frame then return end
|
||||
|
||||
for _, script in ipairs({ "OnUpdate", "OnEvent" }) do
|
||||
local fn = frame:GetScript(script)
|
||||
if fn then
|
||||
local text = ns.OwnerLabel(ns.Owner(fn))
|
||||
if ns.profiling then
|
||||
local ms, calls = GetFunctionCPUUsage(fn)
|
||||
text = text .. ns.GREY .. " " .. ns.MS(ms) .. " in " .. ns.Count(calls) .. " calls"
|
||||
end
|
||||
Line(tooltip, script, text)
|
||||
end
|
||||
end
|
||||
|
||||
if ns.profiling then
|
||||
local ms, calls = GetFrameCPUUsage(frame, false)
|
||||
Line(tooltip, "All scripts", ns.MS(ms) .. ns.GREY .. " in " .. ns.Count(calls) .. " calls")
|
||||
end
|
||||
|
||||
local rec = ns.track.frames[frame]
|
||||
if rec and (rec.hookedUpdate or rec.hookedEvent) then
|
||||
Line(tooltip, "Allocated", ns.KB(rec.alloc) .. ns.GREY .. " (" .. ns.KBRate(rec.allocRate) .. ")")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,166 @@
|
||||
-- Frames tab: the frames that run scripts, with the CPU time of all their
|
||||
-- scripts (CPU profiling) and the memory they allocate (deep tracking).
|
||||
local _, ns = ...
|
||||
|
||||
local track = ns.track
|
||||
|
||||
local function Profiled(fn)
|
||||
return function(rec)
|
||||
if ns.profiling then return fn(rec) end
|
||||
end
|
||||
end
|
||||
|
||||
local function Tracked(fn)
|
||||
return function(rec)
|
||||
if rec.hookedUpdate or rec.hookedEvent then return fn(rec) end
|
||||
end
|
||||
end
|
||||
|
||||
local function Line(left, right)
|
||||
GameTooltip:AddDoubleLine(left, right, 1, 1, 1, 1, 1, 1)
|
||||
end
|
||||
|
||||
-- the events of a frame, most expensive first
|
||||
local function SortedEvents(rec)
|
||||
local list = {}
|
||||
for name, stat in pairs(rec.events) do
|
||||
tinsert(list, { name = name, stat = stat })
|
||||
end
|
||||
local key = ns.profiling and "cpu" or "alloc"
|
||||
table.sort(list, function(a, b) return a.stat[key] > b.stat[key] end)
|
||||
return list
|
||||
end
|
||||
|
||||
local function Tooltip(rec)
|
||||
local frame = rec.frame
|
||||
GameTooltip:AddLine(rec.name)
|
||||
Line("Type", frame:GetObjectType())
|
||||
Line("Owner", ns.OwnerLabel(rec.owner))
|
||||
Line("State", frame:IsVisible() and "visible" or frame:IsShown() and "shown, parent hidden" or "hidden")
|
||||
|
||||
local source = frame:GetSourceLocation()
|
||||
if source then
|
||||
GameTooltip:AddLine(source, .6, .6, .6, true)
|
||||
end
|
||||
|
||||
GameTooltip:AddLine(" ")
|
||||
for _, script in ipairs({ "OnUpdate", "OnEvent" }) do
|
||||
local fn = frame:GetScript(script)
|
||||
if fn then
|
||||
local text = ns.OwnerLabel(ns.Owner(fn))
|
||||
if ns.profiling then
|
||||
local ms, calls = GetFunctionCPUUsage(fn)
|
||||
text = text .. ns.GREY .. " " .. ns.MS(ms) .. " in " .. ns.Count(calls) .. " calls"
|
||||
end
|
||||
Line(script, text)
|
||||
end
|
||||
end
|
||||
|
||||
if ns.profiling then
|
||||
Line("All scripts", ns.MS(rec.cpu) .. " in " .. ns.Count(rec.calls) .. " calls")
|
||||
end
|
||||
if rec.hookedUpdate or rec.hookedEvent then
|
||||
Line("Allocated", ns.KB(rec.alloc))
|
||||
end
|
||||
|
||||
local events = SortedEvents(rec)
|
||||
if #events > 0 then
|
||||
GameTooltip:AddLine(" ")
|
||||
GameTooltip:AddLine("Events", 1, .8, 0)
|
||||
for i = 1, min(#events, 12) do
|
||||
local stat = events[i].stat
|
||||
local text = ns.Count(stat.count) .. "x"
|
||||
if ns.profiling then text = text .. " " .. ns.MS(stat.cpu) end
|
||||
Line(events[i].name, text .. " " .. ns.KB(stat.alloc))
|
||||
end
|
||||
end
|
||||
|
||||
GameTooltip:AddLine(" ")
|
||||
GameTooltip:AddLine("Right-click: highlight, inspect or copy.", .6, .6, .6)
|
||||
end
|
||||
|
||||
local tab = ns.AddTab("Frames")
|
||||
|
||||
tab.list = ns.CreateList(tab.frame, {
|
||||
sort = 3,
|
||||
columns = {
|
||||
{
|
||||
title = "Frame",
|
||||
text = function(rec) return rec.name end,
|
||||
value = function(rec) return strlower(rec.name) end,
|
||||
},
|
||||
{
|
||||
title = "Owner", width = 100, justify = "LEFT",
|
||||
tip = "The addon that made the frame's OnUpdate or OnEvent handler, or the frame itself.",
|
||||
text = function(rec) return ns.OwnerLabel(rec.owner) end,
|
||||
value = function(rec) return strlower(ns.OwnerLabel(rec.owner)) end,
|
||||
},
|
||||
{
|
||||
title = "CPU/s", width = 64,
|
||||
tip = "The CPU time of all the frame's scripts in the last second. Needs CPU profiling.",
|
||||
text = Profiled(function(rec) return ns.MS(rec.cpuRate) end),
|
||||
value = Profiled(function(rec) return rec.cpuRate end),
|
||||
},
|
||||
{
|
||||
title = "Calls/s", width = 60,
|
||||
tip = "How often the frame's scripts ran in the last second. Needs CPU profiling.",
|
||||
text = Profiled(function(rec) return ns.Count(rec.callRate) end),
|
||||
value = Profiled(function(rec) return rec.callRate end),
|
||||
},
|
||||
{
|
||||
title = "Per Call", width = 64,
|
||||
tip = "The average CPU time of one script call since the last reset.",
|
||||
text = Profiled(function(rec) return rec.calls > 0 and ns.MS(rec.cpu / rec.calls) end),
|
||||
value = Profiled(function(rec) return rec.calls > 0 and rec.cpu / rec.calls or nil end),
|
||||
},
|
||||
{
|
||||
title = "Alloc/s", width = 72,
|
||||
tip = "The Lua memory the frame's OnUpdate and OnEvent scripts allocated in the last second. Needs deep tracking.",
|
||||
text = Tracked(function(rec) return ns.KBRate(rec.allocRate) end),
|
||||
value = Tracked(function(rec) return rec.allocRate end),
|
||||
},
|
||||
{
|
||||
title = "CPU Total", width = 64,
|
||||
tip = "The CPU time of all the frame's scripts since the last reset.",
|
||||
text = Profiled(function(rec) return ns.MS(rec.cpu) end),
|
||||
value = Profiled(function(rec) return rec.cpu end),
|
||||
},
|
||||
},
|
||||
|
||||
tooltip = Tooltip,
|
||||
|
||||
menu = function(rec, root)
|
||||
root:CreateTitle(rec.name)
|
||||
root:CreateButton("Highlight", function() ns.Highlight(rec.frame) end)
|
||||
root:CreateButton("Inspect", function() ns.Inspect(rec.frame) end)
|
||||
root:CreateButton("Copy name", function() ns.Copy(rec.name) end)
|
||||
local source = rec.frame:GetSourceLocation()
|
||||
if source then
|
||||
root:CreateButton("Copy source location", function() ns.Copy(source) end)
|
||||
end
|
||||
root:CreateButton("Show " .. ns.OwnerLabel(rec.owner) .. " frames", function()
|
||||
ns.ShowIn("Frames", ns.OwnerLabel(rec.owner))
|
||||
end)
|
||||
end,
|
||||
})
|
||||
tab.list:SetAllPoints()
|
||||
|
||||
local function Update(self)
|
||||
local entries = {}
|
||||
for _, rec in pairs(track.frames) do
|
||||
if rec.calls > 0 or rec.alloc > 0 then tinsert(entries, rec) end
|
||||
end
|
||||
self.list:SetEntries(entries)
|
||||
end
|
||||
|
||||
tab.OnSelect = Update
|
||||
tab.OnSample = Update
|
||||
|
||||
function tab:Status()
|
||||
local text = format("%d frames %d listed", track.frameCount, #self.list.shown)
|
||||
if not ns.profiling and not track.deep then
|
||||
return text .. " " .. ns.GOLD .. "Turn on CPU profiling or deep tracking to measure frames."
|
||||
end
|
||||
if track.deep then text = text .. " deep tracking on" end
|
||||
return text
|
||||
end
|
||||
@@ -0,0 +1,177 @@
|
||||
-- Globals tab: the global tables, functions and frames each addon made, the
|
||||
-- ClassicAPI functions that something replaced or hooked, and (CPU
|
||||
-- profiling) the time of every global function, game API included.
|
||||
local _, ns = ...
|
||||
|
||||
-- namespaces whose functions are listed as Name.key
|
||||
local LIBRARIES = { string = true, table = true, math = true, coroutine = true }
|
||||
|
||||
local function Kind(value)
|
||||
local t = type(value)
|
||||
if t == "table" and type(rawget(value, 0)) == "userdata" and value.GetObjectType then
|
||||
return value:GetObjectType()
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
local function Line(left, right)
|
||||
GameTooltip:AddDoubleLine(left, right, 1, 1, 1, 1, 1, 1)
|
||||
end
|
||||
|
||||
local function Tooltip(entry)
|
||||
local value = entry.value
|
||||
GameTooltip:AddLine(entry.name)
|
||||
Line("Type", entry.kind)
|
||||
Line("Owner", ns.OwnerLabel(entry.owner))
|
||||
if entry.override then
|
||||
GameTooltip:AddLine("Not ClassicAPI's function: something replaced or hooked it.", 1, .3, .3, true)
|
||||
end
|
||||
|
||||
if entry.kind == "function" and ns.profiling then
|
||||
Line("CPU", ns.MS(entry.cpu))
|
||||
Line("Calls", ns.Count(entry.calls))
|
||||
if entry.calls > 0 then Line("Per call", ns.MS(entry.cpu / entry.calls)) end
|
||||
elseif entry.kind == "table" then
|
||||
Line("Entries", ns.Count(table.count(value)))
|
||||
elseif type(value) == "table" then
|
||||
Line("Debug name", ns.FrameName(value))
|
||||
local source = value:GetSourceLocation()
|
||||
if source then GameTooltip:AddLine(source, .6, .6, .6, true) end
|
||||
elseif type(value) ~= "function" and value ~= nil then
|
||||
Line("Value", strsub(tostring(value), 1, 60))
|
||||
end
|
||||
end
|
||||
|
||||
local tab = ns.AddTab("Globals")
|
||||
tab.untagged = false
|
||||
|
||||
tab.list = ns.CreateList(tab.frame, {
|
||||
sort = 3,
|
||||
columns = {
|
||||
{
|
||||
title = "Name",
|
||||
text = function(entry)
|
||||
if entry.override then return ns.RED .. entry.name .. ns.GREY .. " (replaced)" end
|
||||
return entry.name
|
||||
end,
|
||||
value = function(entry) return strlower(entry.name) end,
|
||||
},
|
||||
{
|
||||
title = "Type", width = 80, justify = "LEFT",
|
||||
text = function(entry) return entry.kind end,
|
||||
value = function(entry) return entry.kind end,
|
||||
},
|
||||
{
|
||||
title = "Owner", width = 100, justify = "LEFT",
|
||||
tip = "The addon that made the value. Strings, numbers and booleans have no owner.",
|
||||
text = function(entry) return ns.OwnerLabel(entry.owner) end,
|
||||
value = function(entry) return strlower(ns.OwnerLabel(entry.owner)) end,
|
||||
},
|
||||
{
|
||||
title = "CPU", width = 64,
|
||||
tip = "The CPU time of the function since the last reset. Needs CPU profiling.",
|
||||
text = function(entry) return entry.cpu and ns.MS(entry.cpu) end,
|
||||
value = function(entry) return entry.cpu end,
|
||||
},
|
||||
{
|
||||
title = "Calls", width = 70,
|
||||
tip = "How often the function ran since the last reset. Needs CPU profiling.",
|
||||
text = function(entry) return entry.calls and ns.Count(entry.calls) end,
|
||||
value = function(entry) return entry.calls end,
|
||||
},
|
||||
},
|
||||
|
||||
tooltip = Tooltip,
|
||||
|
||||
menu = function(entry, root)
|
||||
local value = entry.value
|
||||
root:CreateTitle(entry.name)
|
||||
if type(value) == "table" then
|
||||
root:CreateButton("Inspect", function() ns.Inspect(value) end)
|
||||
if entry.kind ~= "table" then
|
||||
root:CreateButton("Highlight", function() ns.Highlight(value) end)
|
||||
end
|
||||
end
|
||||
root:CreateButton("Copy name", function() ns.Copy(entry.name) end)
|
||||
end,
|
||||
})
|
||||
tab.list:SetPoint("TOPLEFT", 0, -26)
|
||||
tab.list:SetPoint("BOTTOMRIGHT")
|
||||
|
||||
-- [[ scan ]]
|
||||
local function Scan()
|
||||
-- the ClassicAPI functions whose global is no longer ClassicAPI's
|
||||
local overrides = {}
|
||||
for name, fn in pairs(ClassicAPI) do
|
||||
if type(fn) == "function" then
|
||||
if _G[name] ~= fn then overrides[name] = true end
|
||||
elseif type(fn) == "table" and type(_G[name]) == "table" then
|
||||
for key, sub in pairs(fn) do
|
||||
if type(sub) == "function" and _G[name][key] ~= sub then overrides[name .. "." .. key] = true end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local entries = {}
|
||||
local function Add(name, value)
|
||||
local kind = Kind(value)
|
||||
local owner = (kind ~= "string" and kind ~= "number" and kind ~= "boolean" and kind ~= "nil") and ns.Owner(value) or nil
|
||||
local override = overrides[name]
|
||||
if not owner and not override and not tab.untagged then return end
|
||||
|
||||
local entry = { name = name, value = value, kind = kind, owner = owner, override = override }
|
||||
entry.search = strlower(name .. " " .. ns.OwnerLabel(owner))
|
||||
if kind == "function" and ns.profiling then
|
||||
entry.cpu, entry.calls = GetFunctionCPUUsage(value)
|
||||
end
|
||||
tinsert(entries, entry)
|
||||
overrides[name] = nil
|
||||
end
|
||||
|
||||
for name, value in pairs(_G) do
|
||||
if type(name) == "string" then
|
||||
Add(name, value)
|
||||
if type(value) == "table" and (LIBRARIES[name] or strfind(name, "^C_")) then
|
||||
for key, fn in pairs(value) do
|
||||
if type(key) == "string" and type(fn) == "function" then Add(name .. "." .. key, fn) end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- overridden names that are gone or live outside the scanned namespaces
|
||||
for name in pairs(overrides) do
|
||||
local space, key = strmatch(name, "^([^.]+)%.(.+)$")
|
||||
Add(name, space and _G[space][key] or _G[name])
|
||||
end
|
||||
|
||||
tab.count = #entries
|
||||
tab.overrides = 0
|
||||
for _, entry in ipairs(entries) do
|
||||
if entry.override then tab.overrides = tab.overrides + 1 end
|
||||
end
|
||||
tab.list:SetEntries(entries)
|
||||
end
|
||||
|
||||
local rescan = ns.CreateButton("ClassicAPIProfilerWindowRescan", tab.frame, 80, "Rescan", Scan)
|
||||
rescan:SetPoint("TOPRIGHT", -4, -2)
|
||||
ns.Tooltip(rescan, "Read the global namespace again. Also updates the CPU times.")
|
||||
|
||||
local untagged = CreateFrame("CheckButton", "ClassicAPIProfilerWindowUntagged", tab.frame, "UICheckButtonTemplate")
|
||||
untagged:SetSize(20, 20)
|
||||
untagged:SetPoint("RIGHT", rescan, "LEFT", -110, 0)
|
||||
ClassicAPIProfilerWindowUntaggedText:SetText("Show FrameXML")
|
||||
untagged:SetScript("OnClick", function(self)
|
||||
tab.untagged = self:GetChecked() and true or false
|
||||
Scan()
|
||||
end)
|
||||
ns.Tooltip(untagged, "Also list the values the game's own code made, and plain strings, numbers and booleans. With CPU profiling this shows the cost of every game API function.")
|
||||
|
||||
function tab:OnSelect()
|
||||
if not self.count then Scan() end
|
||||
end
|
||||
|
||||
function tab:Status()
|
||||
if not self.count then return "" end
|
||||
return format("%d globals listed %d ClassicAPI functions replaced or hooked", #self.list.shown, self.overrides)
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/">
|
||||
<!--
|
||||
A thin scroll bar for ClassicAPI's ScrollBox. ScrollBarMixin needs Back and
|
||||
Forward steppers, so they exist but stay hidden.
|
||||
-->
|
||||
<EventFrame name="ClassicAPIProfilerScrollBarTemplate" inherits="VerticalScrollBarTemplate" virtual="true">
|
||||
<Size x="7"/>
|
||||
<KeyValues>
|
||||
<KeyValue key="hideIfUnscrollable" value="true" type="boolean"/>
|
||||
</KeyValues>
|
||||
<Frames>
|
||||
<Frame parentKey="Track" setAllPoints="true" enableMouse="true">
|
||||
<Frames>
|
||||
<EventButton parentKey="Thumb">
|
||||
<Size x="7"/>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<Texture setAllPoints="true">
|
||||
<Color r=".2" g="1" b=".8" a=".5"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</EventButton>
|
||||
</Frames>
|
||||
</Frame>
|
||||
<EventButton parentKey="Back" hidden="true"/>
|
||||
<EventButton parentKey="Forward" hidden="true"/>
|
||||
</Frames>
|
||||
</EventFrame>
|
||||
|
||||
<!--
|
||||
A sortable list: a column header above a WowScrollBoxList, with the scroll
|
||||
bar on the right. Window.lua fills the header and the view.
|
||||
-->
|
||||
<Frame name="ClassicAPIProfilerListTemplate" virtual="true">
|
||||
<Frames>
|
||||
<Frame parentKey="Header">
|
||||
<Size y="18"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="5" y="-5"/>
|
||||
<Anchor point="TOPRIGHT" x="-15" y="-5"/>
|
||||
</Anchors>
|
||||
</Frame>
|
||||
<EventFrame parentKey="ScrollBar" inherits="ClassicAPIProfilerScrollBarTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT" x="-5" y="-25"/>
|
||||
<Anchor point="BOTTOMRIGHT" x="-5" y="5"/>
|
||||
</Anchors>
|
||||
</EventFrame>
|
||||
<Frame parentKey="ScrollBox" inherits="WowScrollBoxList">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="5" y="-25"/>
|
||||
<Anchor point="BOTTOMRIGHT" relativeKey="$parent.ScrollBar" relativePoint="BOTTOMLEFT" x="-3"/>
|
||||
</Anchors>
|
||||
</Frame>
|
||||
</Frames>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -0,0 +1,212 @@
|
||||
-- Profiler track: per-frame and per-event statistics for the Frames and
|
||||
-- Events tabs. The analyzer runs Scan once a sample while it is open.
|
||||
--
|
||||
-- Deep tracking hooks the intrinsic precall and postcall of every OnUpdate
|
||||
-- and OnEvent script. The hooks never replace a handler, so GetScript and
|
||||
-- SetScript behave as before, and they stay until /reload. Around each
|
||||
-- script they measure the Lua memory it allocated and, with CPU profiling,
|
||||
-- the time of each OnEvent handler per event.
|
||||
local addonName, ns = ...
|
||||
|
||||
local PRECALL = LE_SCRIPT_BINDING_TYPE_INTRINSIC_PRECALL
|
||||
local POSTCALL = LE_SCRIPT_BINDING_TYPE_INTRINSIC_POSTCALL
|
||||
local Delta = ns.Delta
|
||||
|
||||
local track = {
|
||||
frames = {}, -- [frame] = record: frames that ran scripts, or are hooked
|
||||
events = {}, -- [event] = record: events fired while the analyzer is open
|
||||
frameCount = 0, -- frames in the last scan
|
||||
deep = false,
|
||||
}
|
||||
ns.track = track
|
||||
|
||||
-- the frame that counts every event while the analyzer is open
|
||||
local capture = CreateFrame("Frame")
|
||||
track.capture = capture
|
||||
|
||||
-- the addon that runs a frame's scripts: the owner of its handlers, or the
|
||||
-- frame's creator when the game's own code made the handlers
|
||||
local function OwnerOf(frame)
|
||||
local fn = frame:GetScript("OnUpdate") or frame:GetScript("OnEvent")
|
||||
return fn and ns.Owner(fn) or ns.Owner(frame)
|
||||
end
|
||||
|
||||
-- cpu and calls are the frame's totals now, so its first rate covers only
|
||||
-- the time after the record exists, not the whole session
|
||||
local function FrameRecord(frame, cpu, calls)
|
||||
local rec = {
|
||||
frame = frame, name = ns.FrameName(frame), owner = OwnerOf(frame),
|
||||
cpu = cpu or 0, calls = calls or 0, alloc = 0, events = {},
|
||||
}
|
||||
rec.search = strlower(rec.name .. " " .. ns.OwnerLabel(rec.owner))
|
||||
track.frames[frame] = rec
|
||||
return rec
|
||||
end
|
||||
|
||||
local function EventRecord(name)
|
||||
local rec = { name = name, search = strlower(name), fires = 0, dispatches = 0, cpu = 0, alloc = 0 }
|
||||
track.events[name] = rec
|
||||
return rec
|
||||
end
|
||||
|
||||
capture:SetScript("OnEvent", function(self, event)
|
||||
local rec = track.events[event] or EventRecord(event)
|
||||
rec.fires = rec.fires + 1
|
||||
end)
|
||||
|
||||
-- [[ deep tracking ]]
|
||||
-- a stack, because a script can run inside another script
|
||||
local depth = 0
|
||||
local memAt, gcAt, fnAt, cpuAt, eventAt = {}, {}, {}, {}, {}
|
||||
|
||||
local function Enter(fn, event)
|
||||
depth = depth + 1
|
||||
memAt[depth], gcAt[depth] = gcinfo()
|
||||
fnAt[depth], eventAt[depth] = fn, event
|
||||
if fn and ns.profiling then cpuAt[depth] = GetFunctionCPUUsage(fn) end
|
||||
end
|
||||
|
||||
-- the KB allocated since the matching Enter (nil when a garbage collection
|
||||
-- ran in between), the handler's CPU time in ms, and the event
|
||||
local function Leave()
|
||||
local d = depth
|
||||
if d == 0 then return end
|
||||
depth = d - 1
|
||||
|
||||
local mem, threshold = gcinfo()
|
||||
local alloc = threshold == gcAt[d] and max(mem - memAt[d], 0) or nil
|
||||
local fn, cpu = fnAt[d], nil
|
||||
if fn and ns.profiling then cpu = max(GetFunctionCPUUsage(fn) - cpuAt[d], 0) end
|
||||
fnAt[d] = nil
|
||||
return alloc, cpu, eventAt[d]
|
||||
end
|
||||
|
||||
local function EnterUpdate() Enter() end
|
||||
|
||||
local function LeaveEvent(rec)
|
||||
local alloc, cpu, name = Leave()
|
||||
if not name then return end
|
||||
|
||||
local ev = track.events[name] or EventRecord(name)
|
||||
local fe = rec.events[name]
|
||||
if not fe then
|
||||
fe = { count = 0, cpu = 0, alloc = 0 }
|
||||
rec.events[name] = fe
|
||||
end
|
||||
|
||||
fe.count = fe.count + 1
|
||||
ev.dispatches = ev.dispatches + 1
|
||||
if alloc then
|
||||
rec.alloc, fe.alloc, ev.alloc = rec.alloc + alloc, fe.alloc + alloc, ev.alloc + alloc
|
||||
end
|
||||
if cpu then
|
||||
fe.cpu, ev.cpu = fe.cpu + cpu, ev.cpu + cpu
|
||||
end
|
||||
end
|
||||
|
||||
local function Hook(frame, rec)
|
||||
if not rec.hookedUpdate and frame:GetScript("OnUpdate") then
|
||||
rec.hookedUpdate = true
|
||||
frame:HookScript("OnUpdate", EnterUpdate, PRECALL)
|
||||
frame:HookScript("OnUpdate", function()
|
||||
local alloc = Leave()
|
||||
if alloc then rec.alloc = rec.alloc + alloc end
|
||||
end, POSTCALL)
|
||||
end
|
||||
|
||||
if not rec.hookedEvent and frame:GetScript("OnEvent") then
|
||||
rec.hookedEvent = true
|
||||
-- the engine sets the `event` global before it runs the script
|
||||
frame:HookScript("OnEvent", function() Enter(frame:GetScript("OnEvent"), event) end, PRECALL)
|
||||
frame:HookScript("OnEvent", function() LeaveEvent(rec) end, POSTCALL)
|
||||
end
|
||||
end
|
||||
|
||||
function track.EnableDeep()
|
||||
track.deep = true
|
||||
end
|
||||
|
||||
-- [[ sampling ]]
|
||||
-- the frames registered for an event, without the capture frame
|
||||
local function CountListeners(...)
|
||||
local n = select("#", ...)
|
||||
for i = 1, n do
|
||||
if select(i, ...) == capture then return n - 1 end
|
||||
end
|
||||
return n
|
||||
end
|
||||
track.CountListeners = CountListeners
|
||||
|
||||
function track.Scan(dt)
|
||||
local profiling, deep = ns.profiling, track.deep
|
||||
local count = 0
|
||||
|
||||
local frame = EnumerateFrames()
|
||||
while frame do
|
||||
count = count + 1
|
||||
local rec = track.frames[frame]
|
||||
|
||||
if profiling then
|
||||
local cpu, calls = GetFrameCPUUsage(frame, false)
|
||||
if calls > 0 and not rec then rec = FrameRecord(frame, cpu, calls) end
|
||||
if rec then
|
||||
rec.cpuRate = Delta(cpu, rec.cpu) / dt
|
||||
rec.callRate = Delta(calls, rec.calls) / dt
|
||||
rec.cpu, rec.calls = cpu, calls
|
||||
end
|
||||
end
|
||||
|
||||
if deep and not (rec and rec.hookedUpdate and rec.hookedEvent) then
|
||||
if frame:GetScript("OnUpdate") or frame:GetScript("OnEvent") then
|
||||
rec = rec or FrameRecord(frame)
|
||||
-- the profiler does not measure its own addon
|
||||
if rec.owner ~= addonName then Hook(frame, rec) end
|
||||
end
|
||||
end
|
||||
|
||||
frame = EnumerateFrames(frame)
|
||||
end
|
||||
track.frameCount = count
|
||||
|
||||
for _, rec in pairs(track.frames) do
|
||||
rec.allocRate = Delta(rec.alloc, rec.lastAlloc) / dt
|
||||
rec.lastAlloc = rec.alloc
|
||||
end
|
||||
|
||||
for name, ev in pairs(track.events) do
|
||||
ev.fireRate = Delta(ev.fires, ev.lastFires) / dt
|
||||
ev.cpuRate = Delta(ev.cpu, ev.lastCpu) / dt
|
||||
ev.allocRate = Delta(ev.alloc, ev.lastAlloc) / dt
|
||||
ev.lastFires, ev.lastCpu, ev.lastAlloc = ev.fires, ev.cpu, ev.alloc
|
||||
ev.listeners = CountListeners(GetFramesRegisteredForEvent(name))
|
||||
end
|
||||
end
|
||||
|
||||
-- the frame totals and the deep hooks keep counting while the analyzer is
|
||||
-- closed, so an open takes the counters as they are now, and the first scan
|
||||
-- covers only its own second
|
||||
function track.Start()
|
||||
for frame, rec in pairs(track.frames) do
|
||||
if ns.profiling then rec.cpu, rec.calls = GetFrameCPUUsage(frame, false) end
|
||||
rec.lastAlloc = rec.alloc
|
||||
end
|
||||
for _, ev in pairs(track.events) do
|
||||
ev.lastCpu, ev.lastAlloc = ev.cpu, ev.alloc
|
||||
end
|
||||
capture:RegisterAllEvents()
|
||||
end
|
||||
|
||||
function track.Stop()
|
||||
capture:UnregisterAllEvents()
|
||||
end
|
||||
|
||||
ns.OnReset(function()
|
||||
for _, rec in pairs(track.frames) do
|
||||
rec.cpu, rec.calls, rec.alloc, rec.lastAlloc = 0, 0, 0, 0
|
||||
table.wipe(rec.events)
|
||||
end
|
||||
for _, ev in pairs(track.events) do
|
||||
ev.fires, ev.dispatches, ev.cpu, ev.alloc = 0, 0, 0, 0
|
||||
ev.lastFires, ev.lastCpu, ev.lastAlloc = 0, 0, 0
|
||||
end
|
||||
end)
|
||||
@@ -0,0 +1,180 @@
|
||||
-- Profiler widget: Lua memory, garbage collection, frame rate and the addons
|
||||
-- that make the most garbage and use the most CPU.
|
||||
local _, ns = ...
|
||||
|
||||
local LINE_HEIGHT = 14
|
||||
local stats, gc = ns.stats, ns.gc
|
||||
|
||||
local widget = CreateFrame("Button", "ClassicAPIProfilerWidget", UIParent)
|
||||
ns.widget = widget
|
||||
widget:Hide()
|
||||
widget:SetPoint("CENTER", 0, 0)
|
||||
widget:SetWidth(240)
|
||||
widget:SetMovable(true)
|
||||
widget:SetClampedToScreen(true)
|
||||
widget:EnableMouse(true)
|
||||
widget:RegisterForDrag("LeftButton")
|
||||
widget:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
ns.CreateBackdrop(widget)
|
||||
|
||||
local lines = {}
|
||||
local function AddLine(label)
|
||||
local y = -4 - #lines * LINE_HEIGHT
|
||||
|
||||
local left = widget:CreateFontString(nil, "LOW", "GameFontWhite")
|
||||
left:SetPoint("TOPLEFT", 5, y)
|
||||
left:SetText(ns.ACCENT .. label)
|
||||
|
||||
local right = widget:CreateFontString(nil, "LOW", "GameFontWhite")
|
||||
right:SetPoint("TOPRIGHT", -5, y)
|
||||
right:SetJustifyH("RIGHT")
|
||||
|
||||
tinsert(lines, right)
|
||||
return right
|
||||
end
|
||||
|
||||
local memory = AddLine("Memory")
|
||||
local rate = AddLine("Rate")
|
||||
local cleanup = AddLine("Last Cleanup")
|
||||
local framerate = AddLine("Framerate")
|
||||
local cpu = AddLine("Lua CPU")
|
||||
local topMem = AddLine("Top Alloc")
|
||||
local topCpu = AddLine("Top CPU")
|
||||
|
||||
-- the largest allocation in a tenth of a second, draining over 5 seconds
|
||||
local DRAIN = .1 / 5
|
||||
local bar = CreateFrame("StatusBar", nil, widget)
|
||||
bar:SetColorFill(1, .3, .3)
|
||||
bar:SetPoint("BOTTOMLEFT", 1, 1)
|
||||
bar:SetPoint("BOTTOMRIGHT", -1, 1)
|
||||
bar:SetHeight(18)
|
||||
bar:SetMinMaxValues(0, 1)
|
||||
bar:SetValue(0)
|
||||
|
||||
local barText = bar:CreateFontString(nil, "OVERLAY", "GameFontWhite")
|
||||
barText:SetPoint("LEFT", 4, 0)
|
||||
|
||||
widget:SetHeight(#lines * LINE_HEIGHT + 8 + 20)
|
||||
|
||||
-- lastMem is taken on the first tick while shown, so the growth while hidden
|
||||
-- or while the other addons load never counts as a peak
|
||||
local lastMem, peakElapsed = nil, 0
|
||||
widget:SetScript("OnShow", function() ns.StartSampling() end)
|
||||
widget:SetScript("OnHide", function() lastMem = nil end)
|
||||
widget:SetScript("OnUpdate", function(self, elapsed)
|
||||
peakElapsed = peakElapsed + elapsed
|
||||
if peakElapsed < .1 then return end
|
||||
peakElapsed = 0
|
||||
|
||||
local mem = gcinfo()
|
||||
local grown = lastMem and mem - lastMem or 0
|
||||
lastMem = mem
|
||||
|
||||
local value, peak = bar:GetValue(), select(2, bar:GetMinMaxValues())
|
||||
if grown > value then
|
||||
bar:SetMinMaxValues(0, grown)
|
||||
bar:SetValue(grown)
|
||||
barText:SetText(ns.ACCENT .. "Last Peak (100ms):|r " .. grown .. ns.GREY .. " KB")
|
||||
else
|
||||
bar:SetValue(max(value - peak * DRAIN, 0))
|
||||
end
|
||||
bar:SetColorFill(1, .3, .3, min(bar:GetValue() / 10, 1))
|
||||
end)
|
||||
|
||||
local function Unit(value, unit)
|
||||
return ns.WHITE .. value .. " " .. ns.GREY .. unit
|
||||
end
|
||||
|
||||
ns.OnSample(function()
|
||||
if not widget:IsShown() then return end
|
||||
|
||||
memory:SetText(ns.WHITE .. ns.KB(stats.mem) .. ns.GREY .. " / " .. ns.WHITE .. ns.KB(stats.threshold))
|
||||
|
||||
if stats.collected then
|
||||
rate:SetText(ns.ACCENT .. "collected " .. ns.KB(gc.freed or 0))
|
||||
elseif stats.memRate then
|
||||
rate:SetText(ns.WHITE .. ns.KBRate(stats.memRate))
|
||||
end
|
||||
|
||||
if gc.time then
|
||||
cleanup:SetText(ns.WHITE .. gc.clock .. ns.GREY .. " (" .. SecondsToTime(GetTime() - gc.time) .. " ago)")
|
||||
else
|
||||
cleanup:SetText(ns.GREY .. "none yet")
|
||||
end
|
||||
|
||||
local fps = GetFramerate()
|
||||
framerate:SetText(Unit(format("%.0f", fps), "fps") .. ns.GREY .. " (" .. ns.WHITE .. format("%.1f", 1000 / fps) .. ns.GREY .. " ms)")
|
||||
|
||||
if ns.profiling then
|
||||
cpu:SetText(ns.WHITE .. ns.MS(stats.scriptRate) .. ns.GREY .. "/s (" .. ns.WHITE .. format("%.1f%%", stats.scriptRate / 10) .. ns.GREY .. ")")
|
||||
topCpu:SetText(stats.topCpu and ns.WHITE .. stats.topCpu.name .. " " .. ns.GREY .. ns.MS(stats.topCpu.cpuRate) .. "/s" or "")
|
||||
else
|
||||
cpu:SetText(ns.GREY .. "profiling off")
|
||||
topCpu:SetText(ns.GREY .. "-")
|
||||
end
|
||||
|
||||
local top = stats.topMem
|
||||
if top and top.memRate > 0 then
|
||||
topMem:SetText(ns.WHITE .. top.name .. " " .. ns.GREY .. ns.KBRate(top.memRate))
|
||||
else
|
||||
topMem:SetText(ns.GREY .. "-")
|
||||
end
|
||||
|
||||
if GameTooltip:IsOwned(widget) then widget:GetScript("OnEnter")(widget) end
|
||||
end)
|
||||
|
||||
ns.OnReset(function()
|
||||
bar:SetValue(0)
|
||||
end)
|
||||
|
||||
-- the top `count` loaded addons by `key`, largest first
|
||||
local function Top(key, count)
|
||||
local sorted = {}
|
||||
for _, addon in ipairs(stats.list) do
|
||||
if (addon[key] or 0) > 0 then tinsert(sorted, addon) end
|
||||
end
|
||||
table.sort(sorted, function(a, b) return a[key] > b[key] end)
|
||||
for i = count + 1, #sorted do sorted[i] = nil end
|
||||
return sorted
|
||||
end
|
||||
|
||||
widget:SetScript("OnEnter", function(self)
|
||||
GameTooltip:SetOwner(self, "ANCHOR_BOTTOMLEFT")
|
||||
GameTooltip:ClearLines()
|
||||
GameTooltip:AddLine(ns.ACCENT .. "Profiler")
|
||||
|
||||
GameTooltip:AddLine(" ")
|
||||
GameTooltip:AddLine("Allocated per second", 1, .8, 0)
|
||||
for _, addon in ipairs(Top("memRate", 8)) do
|
||||
GameTooltip:AddDoubleLine(addon.name, ns.KBRate(addon.memRate), 1, 1, 1, 1, 1, 1)
|
||||
end
|
||||
|
||||
if ns.profiling then
|
||||
GameTooltip:AddLine(" ")
|
||||
GameTooltip:AddLine("CPU per second", 1, .8, 0)
|
||||
for _, addon in ipairs(Top("cpuRate", 8)) do
|
||||
GameTooltip:AddDoubleLine(addon.name, ns.MS(addon.cpuRate), 1, 1, 1, 1, 1, 1)
|
||||
end
|
||||
end
|
||||
|
||||
GameTooltip:AddLine(" ")
|
||||
GameTooltip:AddLine("Left-click: reset the peak", .6, .6, .6)
|
||||
GameTooltip:AddLine("Right-click: open the analyzer", .6, .6, .6)
|
||||
GameTooltip:AddLine("Drag: move", .6, .6, .6)
|
||||
GameTooltip:Show()
|
||||
end)
|
||||
|
||||
widget:SetScript("OnLeave", function()
|
||||
GameTooltip:Hide()
|
||||
end)
|
||||
|
||||
widget:SetScript("OnClick", function(self, button)
|
||||
if button == "RightButton" then
|
||||
ns.window:SetShown(not ns.window:IsShown())
|
||||
else
|
||||
bar:SetValue(0)
|
||||
end
|
||||
end)
|
||||
|
||||
widget:SetScript("OnDragStart", function(self) self:StartMoving() end)
|
||||
widget:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() end)
|
||||
@@ -0,0 +1,392 @@
|
||||
-- Profiler window: the analyzer. Its tabs share a toolbar (search, deep
|
||||
-- tracking, reset, copy) and the sortable list below.
|
||||
local _, ns = ...
|
||||
|
||||
local ROW_HEIGHT = 18
|
||||
local WHITE8X8 = "Interface\\BUTTONS\\WHITE8X8"
|
||||
|
||||
ns.search = ""
|
||||
|
||||
local window = CreateFrame("Frame", "ClassicAPIProfilerWindow", UIParent)
|
||||
ns.window = window
|
||||
window:Hide()
|
||||
window:SetSize(760, 480)
|
||||
window:SetPoint("CENTER")
|
||||
window:SetFrameStrata("DIALOG")
|
||||
window:SetMovable(true)
|
||||
window:EnableMouse(true)
|
||||
window:SetClampedToScreen(true)
|
||||
window:RegisterForDrag("LeftButton")
|
||||
window:SetScript("OnDragStart", window.StartMoving)
|
||||
window:SetScript("OnDragStop", window.StopMovingOrSizing)
|
||||
ns.CreateBackdrop(window)
|
||||
tinsert(UISpecialFrames, "ClassicAPIProfilerWindow")
|
||||
|
||||
local title = window:CreateFontString(nil, "OVERLAY", "GameFontWhite")
|
||||
title:SetFont(STANDARD_TEXT_FONT, 14)
|
||||
title:SetPoint("TOPLEFT", 10, -10)
|
||||
title:SetText(ns.ACCENT .. "Profiler|r: " .. ns.GOLD .. "Analyzer")
|
||||
|
||||
local close = CreateFrame("Button", "ClassicAPIProfilerWindowClose", window, "UIPanelCloseButton")
|
||||
close:SetSize(20, 20)
|
||||
close:SetPoint("TOPRIGHT", 0, 0)
|
||||
close:SetScript("OnClick", function() window:Hide() end)
|
||||
|
||||
local status = window:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
|
||||
status:SetPoint("BOTTOMLEFT", 10, 8)
|
||||
status:SetPoint("BOTTOMRIGHT", -10, 8)
|
||||
status:SetJustifyH("LEFT")
|
||||
|
||||
-- [[ widgets ]]
|
||||
local function Tooltip(frame, text)
|
||||
frame:SetScript("OnEnter", function(self)
|
||||
GameTooltip:SetOwner(self, "ANCHOR_BOTTOMLEFT")
|
||||
GameTooltip:SetText(type(text) == "function" and text() or text, 1, 1, 1, 1, true)
|
||||
GameTooltip:Show()
|
||||
end)
|
||||
frame:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
end
|
||||
ns.Tooltip = Tooltip
|
||||
|
||||
function ns.CreateButton(name, parent, width, text, onClick)
|
||||
local button = CreateFrame("Button", name, parent, "UIPanelButtonTemplate")
|
||||
button:SetSize(width, 20)
|
||||
button:SetText(text)
|
||||
button:SetScript("OnClick", onClick)
|
||||
return button
|
||||
end
|
||||
|
||||
-- [[ actions the tabs offer in their context menus ]]
|
||||
function ns.Copy(text)
|
||||
local bytes = CopyToClipboard(text, true)
|
||||
ns.Print("Copied " .. bytes .. " bytes to the clipboard.")
|
||||
end
|
||||
|
||||
function ns.Inspect(value)
|
||||
DisplayTableInspectorWindow(value)
|
||||
end
|
||||
|
||||
local highlight = CreateFrame("Frame", nil, UIParent)
|
||||
highlight:SetFrameStrata("TOOLTIP")
|
||||
highlight:SetBackdrop({ bgFile = WHITE8X8, edgeFile = WHITE8X8, edgeSize = 2 })
|
||||
highlight:SetBackdropColor(.2, 1, .8, .15)
|
||||
highlight:SetBackdropBorderColor(.2, 1, .8, 1)
|
||||
highlight:Hide()
|
||||
|
||||
function ns.Highlight(frame)
|
||||
if not frame:IsVisible() then
|
||||
ns.Print(ns.FrameName(frame) .. " is hidden.")
|
||||
return
|
||||
end
|
||||
highlight:ClearAllPoints()
|
||||
highlight:SetAllPoints(frame)
|
||||
highlight:Show()
|
||||
if highlight.timer then highlight.timer:Cancel() end
|
||||
highlight.timer = C_Timer.NewTimer(3, function() highlight:Hide() end)
|
||||
end
|
||||
|
||||
-- [[ list: a sortable, searchable ScrollBox ]]
|
||||
-- columns = { { title, width, justify, tip, text = fn(entry), value = fn(entry) } }
|
||||
-- The first column takes the space the others leave. `value` sorts the
|
||||
-- column and sizes the row bars; it is a number, a lowercase string or nil.
|
||||
local function LayoutColumns(parent, cells, columns)
|
||||
local right = -4
|
||||
for i = #columns, 2, -1 do
|
||||
cells[i]:SetPoint("RIGHT", parent, "RIGHT", right, 0)
|
||||
cells[i]:SetWidth(columns[i].width)
|
||||
right = right - columns[i].width - 8
|
||||
end
|
||||
cells[1]:SetPoint("LEFT", parent, "LEFT", 4, 0)
|
||||
cells[1]:SetPoint("RIGHT", parent, "RIGHT", right, 0)
|
||||
end
|
||||
|
||||
-- green to yellow to red, as `perc` goes from 0 to 1
|
||||
local function Gradient(perc)
|
||||
if perc <= .5 then return perc * 2, 1, 0 end
|
||||
return 1, 2 - perc * 2, 0
|
||||
end
|
||||
|
||||
local function RowOnEnter(row)
|
||||
local tooltip = row.list.def.tooltip
|
||||
if not tooltip then return end
|
||||
GameTooltip:SetOwner(row, "ANCHOR_RIGHT")
|
||||
GameTooltip:ClearLines()
|
||||
tooltip(row.entry)
|
||||
GameTooltip:Show()
|
||||
end
|
||||
|
||||
local function RowOnLeave()
|
||||
GameTooltip:Hide()
|
||||
end
|
||||
|
||||
local function RowOnClick(row, button)
|
||||
local def = row.list.def
|
||||
if button == "RightButton" and def.menu then
|
||||
MenuUtil.CreateContextMenu(row, function(owner, root)
|
||||
def.menu(row.entry, root)
|
||||
end)
|
||||
elseif button == "LeftButton" and def.click then
|
||||
def.click(row.entry)
|
||||
end
|
||||
end
|
||||
|
||||
local function InitRow(list, row, entry)
|
||||
local columns = list.def.columns
|
||||
|
||||
if not row.cells then
|
||||
row:EnableMouse(true)
|
||||
row:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
row:SetHighlightTexture(WHITE8X8)
|
||||
row:GetHighlightTexture():SetVertexColor(1, 1, 1, .1)
|
||||
row:SetScript("OnEnter", RowOnEnter)
|
||||
row:SetScript("OnLeave", RowOnLeave)
|
||||
row:SetScript("OnClick", RowOnClick)
|
||||
|
||||
row.bar = CreateFrame("StatusBar", nil, row)
|
||||
row.bar:SetAllPoints()
|
||||
row.bar:SetMinMaxValues(0, 1)
|
||||
|
||||
row.cells = {}
|
||||
for i, column in ipairs(columns) do
|
||||
local cell = row.bar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
|
||||
cell:SetHeight(ROW_HEIGHT)
|
||||
cell:SetWordWrap(false)
|
||||
cell:SetJustifyH(column.justify or (i == 1 and "LEFT" or "RIGHT"))
|
||||
row.cells[i] = cell
|
||||
end
|
||||
LayoutColumns(row, row.cells, columns)
|
||||
end
|
||||
|
||||
row.list, row.entry = list, entry
|
||||
for i, column in ipairs(columns) do
|
||||
row.cells[i]:SetText(column.text(entry) or "")
|
||||
end
|
||||
|
||||
local value = columns[list.sortColumn].value
|
||||
local v = value and value(entry)
|
||||
local perc = type(v) == "number" and list.maxValue > 0 and max(v, 0) / list.maxValue or 0
|
||||
local r, g, b = Gradient(perc)
|
||||
row.bar:SetColorFill(r, g, b, .2)
|
||||
row.bar:SetValue(perc)
|
||||
|
||||
if GameTooltip:IsOwned(row) then RowOnEnter(row) end
|
||||
end
|
||||
|
||||
local ListMixin = {}
|
||||
|
||||
function ListMixin:UpdateHeader()
|
||||
for i, column in ipairs(self.def.columns) do
|
||||
local text = column.title
|
||||
if i == self.sortColumn then
|
||||
text = ns.ACCENT .. text .. (self.sortDesc and " v" or " ^")
|
||||
end
|
||||
self.headers[i].text:SetText(text)
|
||||
end
|
||||
end
|
||||
|
||||
function ListMixin:SortBy(index)
|
||||
if self.sortColumn == index then
|
||||
self.sortDesc = not self.sortDesc
|
||||
else
|
||||
self.sortColumn, self.sortDesc = index, true
|
||||
end
|
||||
self:UpdateHeader()
|
||||
self:Refresh()
|
||||
end
|
||||
|
||||
function ListMixin:SetEntries(entries)
|
||||
self.entries = entries
|
||||
self:Refresh()
|
||||
end
|
||||
|
||||
-- filter by the search text, sort, and show
|
||||
function ListMixin:Refresh()
|
||||
local value = self.def.columns[self.sortColumn].value
|
||||
local needle, desc = ns.search, self.sortDesc
|
||||
local shown, keys, maxValue = {}, {}, 0
|
||||
|
||||
for _, entry in ipairs(self.entries) do
|
||||
if needle == "" or strfind(entry.search, needle, 1, true) then
|
||||
tinsert(shown, entry)
|
||||
local v = value and value(entry)
|
||||
keys[entry] = v
|
||||
if type(v) == "number" and v > maxValue then maxValue = v end
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(shown, function(a, b)
|
||||
local va, vb = keys[a], keys[b]
|
||||
if va == vb then return a.search < b.search end
|
||||
if va == nil then return false end
|
||||
if vb == nil then return true end
|
||||
if desc then return va > vb end
|
||||
return va < vb
|
||||
end)
|
||||
|
||||
self.shown, self.maxValue = shown, maxValue
|
||||
self.ScrollBox:SetDataProvider(CreateDataProvider(shown), ScrollBoxConstants.RetainScrollPosition)
|
||||
end
|
||||
|
||||
-- the shown rows as plain text
|
||||
function ListMixin:Export()
|
||||
local columns = self.def.columns
|
||||
local function Line(texts)
|
||||
local parts = {}
|
||||
for i, text in ipairs(texts) do
|
||||
text = gsub(gsub(text, "|c%x%x%x%x%x%x%x%x", ""), "|r", "")
|
||||
parts[i] = format(i == 1 and "%-48s" or "%14s", text)
|
||||
end
|
||||
return table.concat(parts, " ")
|
||||
end
|
||||
|
||||
local titles = {}
|
||||
for i, column in ipairs(columns) do titles[i] = column.title end
|
||||
local lines = { Line(titles) }
|
||||
|
||||
for _, entry in ipairs(self.shown) do
|
||||
local texts = {}
|
||||
for i, column in ipairs(columns) do texts[i] = column.text(entry) or "" end
|
||||
tinsert(lines, Line(texts))
|
||||
end
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
function ns.CreateList(parent, def)
|
||||
local list = CreateFrame("Frame", nil, parent, "ClassicAPIProfilerListTemplate")
|
||||
Mixin(list, ListMixin)
|
||||
list.def = def
|
||||
list.entries, list.shown, list.maxValue = {}, {}, 0
|
||||
list.sortColumn, list.sortDesc = def.sort or 1, true
|
||||
ns.CreateBackdrop(list)
|
||||
|
||||
list.headers = {}
|
||||
for i, column in ipairs(def.columns) do
|
||||
local header = CreateFrame("Button", nil, list.Header)
|
||||
header:SetHeight(18)
|
||||
header:EnableMouse(true)
|
||||
header.text = header:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
||||
header.text:SetAllPoints()
|
||||
header.text:SetJustifyH(column.justify or (i == 1 and "LEFT" or "RIGHT"))
|
||||
header:SetScript("OnClick", function() list:SortBy(i) end)
|
||||
if column.tip then Tooltip(header, column.tip) end
|
||||
list.headers[i] = header
|
||||
end
|
||||
LayoutColumns(list.Header, list.headers, def.columns)
|
||||
list:UpdateHeader()
|
||||
|
||||
local view = CreateScrollBoxListLinearView(0, 0, 0, 0, 1)
|
||||
view:SetElementExtent(ROW_HEIGHT)
|
||||
view:SetElementInitializer("Button", function(row, entry) InitRow(list, row, entry) end)
|
||||
ScrollUtil.InitScrollBoxListWithScrollBar(list.ScrollBox, list.ScrollBar, view)
|
||||
return list
|
||||
end
|
||||
|
||||
-- [[ tabs ]]
|
||||
-- a tab has `frame`, `list`, and optionally OnSelect(), OnSample(dt) and
|
||||
-- Status() for the line at the bottom
|
||||
local tabs = {}
|
||||
|
||||
local function UpdateStatus()
|
||||
local tab = window.active
|
||||
status:SetText(tab and tab.Status and tab:Status() or "")
|
||||
end
|
||||
|
||||
function ns.SelectTab(tab)
|
||||
for _, other in ipairs(tabs) do
|
||||
other.frame:SetShown(other == tab)
|
||||
if other == tab then other.button:LockHighlight() else other.button:UnlockHighlight() end
|
||||
end
|
||||
window.active = tab
|
||||
if tab.OnSelect then tab:OnSelect() end
|
||||
UpdateStatus()
|
||||
end
|
||||
|
||||
function ns.AddTab(name)
|
||||
local tab = { name = name }
|
||||
local index = #tabs + 1
|
||||
|
||||
tab.frame = CreateFrame("Frame", nil, window)
|
||||
tab.frame:SetPoint("TOPLEFT", 6, -84)
|
||||
tab.frame:SetPoint("BOTTOMRIGHT", -6, 24)
|
||||
tab.frame:Hide()
|
||||
|
||||
tab.button = ns.CreateButton("ClassicAPIProfilerWindowTab" .. index, window, 90, name, function() ns.SelectTab(tab) end)
|
||||
tab.button:SetPoint("TOPLEFT", 8 + (index - 1) * 94, -32)
|
||||
|
||||
tabs[index] = tab
|
||||
tabs[name] = tab
|
||||
return tab
|
||||
end
|
||||
|
||||
-- show `text` in the search box and switch to tab `name`
|
||||
function ns.ShowIn(name, text)
|
||||
window.search:SetText(text)
|
||||
ns.SelectTab(tabs[name])
|
||||
end
|
||||
|
||||
-- [[ toolbar ]]
|
||||
local search = CreateFrame("EditBox", "ClassicAPIProfilerWindowSearch", window, "InputBoxTemplate")
|
||||
window.search = search
|
||||
search:SetSize(200, 20)
|
||||
search:SetPoint("TOPLEFT", 14, -58)
|
||||
search:SetAutoFocus(false)
|
||||
search:SetScript("OnTextChanged", function(self)
|
||||
ns.search = strlower(self:GetText())
|
||||
self.placeholder:SetShown(self:GetText() == "")
|
||||
if window.active then window.active.list:Refresh() end
|
||||
end)
|
||||
|
||||
search.placeholder = search:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
|
||||
search.placeholder:SetPoint("LEFT", 2, 0)
|
||||
search.placeholder:SetText("Search name, owner or source")
|
||||
|
||||
local copy = ns.CreateButton("ClassicAPIProfilerWindowCopy", window, 70, "Copy", function()
|
||||
ns.Copy(window.active.list:Export())
|
||||
end)
|
||||
copy:SetPoint("TOPRIGHT", -8, -58)
|
||||
Tooltip(copy, "Copy the shown rows to the clipboard as plain text.")
|
||||
|
||||
local reset = ns.CreateButton("ClassicAPIProfilerWindowReset", window, 70, "Reset", function()
|
||||
ns.Reset()
|
||||
UpdateStatus()
|
||||
end)
|
||||
reset:SetPoint("RIGHT", copy, "LEFT", -4, 0)
|
||||
Tooltip(reset, "Set every CPU time, call count, allocation and event count to zero.")
|
||||
|
||||
local deep = ns.CreateButton("ClassicAPIProfilerWindowDeep", window, 150, "", function(self)
|
||||
ns.track.EnableDeep()
|
||||
self:SetText("Deep Tracking: " .. ns.ACCENT .. "ON")
|
||||
self:Disable()
|
||||
end)
|
||||
deep:SetPoint("RIGHT", reset, "LEFT", -4, 0)
|
||||
deep:SetText("Deep Tracking: " .. ns.GREY .. "OFF")
|
||||
Tooltip(deep, "Hooks the OnUpdate and OnEvent scripts of every frame to measure the Lua memory each frame allocates, and the CPU time and memory of each event. The hooks never replace a handler, but they add a little time to every script call. They stay until /reload.")
|
||||
|
||||
local profiling = ns.CreateButton("ClassicAPIProfilerWindowProfiling", window, 150, "", function()
|
||||
ns.SetProfiling(not ns.profiling)
|
||||
end)
|
||||
profiling:SetPoint("TOPRIGHT", -24, -32)
|
||||
profiling:SetText("CPU Profiling: " .. (ns.profiling and ns.ACCENT .. "ON" or ns.GREY .. "OFF"))
|
||||
Tooltip(profiling, function()
|
||||
return (ns.profiling and "Turn CPU profiling off" or "Turn CPU profiling on") ..
|
||||
" and reload the UI.\n\nProfiling measures the CPU time of every addon, frame, function and event. It makes each Lua call slower, so turn it off when you are done."
|
||||
end)
|
||||
|
||||
-- [[ sampling while open ]]
|
||||
ns.OnSample(function(dt)
|
||||
if not window:IsShown() then return end
|
||||
ns.track.Scan(dt)
|
||||
local tab = window.active
|
||||
if tab.OnSample then tab:OnSample(dt) end
|
||||
UpdateStatus()
|
||||
end)
|
||||
|
||||
window:SetScript("OnShow", function()
|
||||
ns.StartSampling()
|
||||
ns.track.Start()
|
||||
ns.SelectTab(window.active or tabs[1])
|
||||
end)
|
||||
|
||||
window:SetScript("OnHide", function()
|
||||
ns.track.Stop()
|
||||
end)
|
||||
@@ -7,6 +7,10 @@
|
||||
-- - The scroll frame is TableInspectorScrollFrameTemplate (end of file).
|
||||
-- 1.12 sizes the scroll range from the scroll child's own height, so
|
||||
-- UpdateLines sets LinesContainer's height to fit the lines.
|
||||
-- - wipe is ClassicAPI.wipe, so an addon's own global wipe cannot
|
||||
-- replace it.
|
||||
|
||||
local wipe = ClassicAPI.wipe;
|
||||
|
||||
TABLE_INSPECTOR_LINE_HEIGHT = 16;
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
-- Verbatim backport of retail's Blizzard_DebugTools/Blizzard_TableInspectorAnchorDataProvider.lua.
|
||||
-- Backport of retail's Blizzard_DebugTools/Blizzard_TableInspectorAnchorDataProvider.lua.
|
||||
-- The one difference: wipe is ClassicAPI.wipe, so an addon's own global
|
||||
-- wipe cannot replace it.
|
||||
|
||||
local wipe = ClassicAPI.wipe;
|
||||
|
||||
TableInspectorAnchorDataProviderMixin = CreateFromMixins(TableInspectorDataProviderMixin);
|
||||
|
||||
function TableInspectorAnchorDataProviderMixin:Initialize(tableInspector, parent)
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
-- Backport of retail's Blizzard_DebugTools/Blizzard_TableInspectorAttributeDataProvider.lua.
|
||||
--
|
||||
-- Difference from the retail source: ShouldShowObject shows every object.
|
||||
-- Retail hides the widgets that tainted code cannot access; 1.12 has no
|
||||
-- taint system, so every widget is accessible.
|
||||
-- Differences from the retail source:
|
||||
-- - ShouldShowObject shows every object. Retail hides the widgets that
|
||||
-- tainted code cannot access; 1.12 has no taint system, so every
|
||||
-- widget is accessible.
|
||||
-- - wipe is ClassicAPI.wipe, so an addon's own global wipe cannot
|
||||
-- replace it.
|
||||
|
||||
local wipe = ClassicAPI.wipe;
|
||||
|
||||
TableInspectorAttributeDataProviderMixin = CreateFromMixins(TableInspectorDataProviderMixin);
|
||||
|
||||
function TableInspectorAttributeDataProviderMixin:Initialize(tableInspector, parent)
|
||||
|
||||
+49
-10
@@ -26,10 +26,46 @@ extern "C" IMAGE_DOS_HEADER __ImageBase;
|
||||
|
||||
namespace Embed {
|
||||
|
||||
Bytes Resource(int id) {
|
||||
static std::unordered_map<int, std::vector<uint8_t>> inflated;
|
||||
auto it = inflated.find(id);
|
||||
if (it == inflated.end()) {
|
||||
namespace {
|
||||
|
||||
struct Pack {
|
||||
std::vector<uint8_t> bytes;
|
||||
std::vector<Bytes> files; // views into `bytes`
|
||||
};
|
||||
|
||||
// Split the inflated `pack.bytes` into `pack.files`. The pack opens with an
|
||||
// index, each file's size as a decimal `\n`-ended line and then an empty
|
||||
// line, followed by the files' bytes in index order. False unless the sizes
|
||||
// cover the bytes exactly.
|
||||
bool Split(Pack &pack) {
|
||||
const uint8_t *p = pack.bytes.data();
|
||||
const uint8_t *end = p + pack.bytes.size();
|
||||
while (p < end && *p != '\n') {
|
||||
size_t size = 0;
|
||||
while (p < end && *p >= '0' && *p <= '9')
|
||||
size = size * 10 + (*p++ - '0');
|
||||
if (p == end || *p++ != '\n')
|
||||
return false;
|
||||
pack.files.push_back({nullptr, size});
|
||||
}
|
||||
if (p == end)
|
||||
return false;
|
||||
++p; // the empty line
|
||||
for (Bytes &file : pack.files) {
|
||||
if (file.size > static_cast<size_t>(end - p))
|
||||
return false;
|
||||
file.data = p;
|
||||
p += file.size;
|
||||
}
|
||||
return p == end;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Bytes File(int id, size_t index) {
|
||||
static std::unordered_map<int, Pack> packs;
|
||||
auto it = packs.find(id);
|
||||
if (it == packs.end()) {
|
||||
const HMODULE self = reinterpret_cast<HMODULE>(&__ImageBase);
|
||||
HRSRC info = FindResourceW(self, MAKEINTRESOURCEW(id), MAKEINTRESOURCEW(10)); // RT_RCDATA
|
||||
if (info == nullptr)
|
||||
@@ -37,16 +73,19 @@ Bytes Resource(int id) {
|
||||
HGLOBAL handle = LoadResource(self, info);
|
||||
if (handle == nullptr)
|
||||
return {nullptr, 0};
|
||||
std::vector<uint8_t> bytes;
|
||||
Pack pack;
|
||||
if (!Encoding::Compress::Inflate(static_cast<const uint8_t *>(LockResource(handle)),
|
||||
SizeofResource(self, info),
|
||||
Encoding::Compress::kGzipWindowBits, bytes) ||
|
||||
bytes.empty())
|
||||
Encoding::Compress::kGzipWindowBits, pack.bytes))
|
||||
return {nullptr, 0};
|
||||
bytes.shrink_to_fit();
|
||||
it = inflated.emplace(id, std::move(bytes)).first;
|
||||
pack.bytes.shrink_to_fit();
|
||||
if (!Split(pack))
|
||||
return {nullptr, 0};
|
||||
// Moving the vector keeps its buffer, so the views stay valid.
|
||||
it = packs.emplace(id, std::move(pack)).first;
|
||||
}
|
||||
return {it->second.data(), it->second.size()};
|
||||
const std::vector<Bytes> &files = it->second.files;
|
||||
return index < files.size() ? files[index] : Bytes{nullptr, 0};
|
||||
}
|
||||
|
||||
} // namespace Embed
|
||||
|
||||
@@ -22,11 +22,12 @@ struct Bytes {
|
||||
size_t size;
|
||||
};
|
||||
|
||||
// The bytes of RCDATA resource `id` in this DLL (IDs come from the generated
|
||||
// `embedded_resources.h`). Each resource is stored gzipped
|
||||
// (cmake/embed_resources.cmake); the first call inflates it and keeps the
|
||||
// bytes, so they stay valid for the life of the DLL and must not be freed.
|
||||
// `{nullptr, 0}` when the resource is missing or does not inflate.
|
||||
Bytes Resource(int id);
|
||||
// The bytes of file `index` in the pack that is RCDATA resource `id` in this
|
||||
// DLL (both come from the generated `embedded_resources.h`). Each resource
|
||||
// is one gzipped pack of files (cmake/pack_files.cmake); the first call for
|
||||
// a resource inflates the whole pack and keeps it, so the bytes stay valid
|
||||
// for the life of the DLL and must not be freed. `{nullptr, 0}` when the
|
||||
// resource is missing, does not inflate or split, or has no file `index`.
|
||||
Bytes File(int id, size_t index);
|
||||
|
||||
} // namespace Embed
|
||||
|
||||
+64
-12
@@ -17,6 +17,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "debug/Log.h"
|
||||
#include "frame/ScriptArgs.h"
|
||||
#include "frame/UnitEvent.h"
|
||||
#include "profile/ScriptTime.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -51,6 +52,7 @@ struct Reservation {
|
||||
const char *name;
|
||||
int slot; // -1 until claimed
|
||||
const Game::Doc::Event *doc; // API documentation; nullptr = undocumented
|
||||
int fixedId; // an `EngineName`'s engine id (its slot for good); -1 = claimed
|
||||
};
|
||||
Reservation g_reserved[MAX_RESERVED];
|
||||
int g_reservedCount = 0;
|
||||
@@ -120,6 +122,15 @@ bool Claimable(const uint8_t *base, int id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reservations that need a claimed slot (an `EngineName` has its own).
|
||||
int ClaimDemand() {
|
||||
int demand = 0;
|
||||
for (int i = 0; i < g_reservedCount; ++i)
|
||||
if (g_reserved[i].fixedId < 0)
|
||||
++demand;
|
||||
return demand;
|
||||
}
|
||||
|
||||
// Walk the engine's event table from the LOW end looking for a claimable
|
||||
// slot. The bulk-init at `0x0051AB30` names 419 of the 549 slots (verified
|
||||
// against both benilla-re's tables/events.tsv and this exe's initializer);
|
||||
@@ -196,7 +207,8 @@ void Grow() {
|
||||
|
||||
const int count = Game::Read<int>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
auto *base = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
if (base == nullptr || count <= 0 || g_reservedCount <= 0)
|
||||
const int demand = ClaimDemand();
|
||||
if (base == nullptr || count <= 0 || demand <= 0)
|
||||
return;
|
||||
|
||||
int nulls = 0;
|
||||
@@ -206,7 +218,7 @@ void Grow() {
|
||||
|
||||
const bool force = g_forceGrowNext;
|
||||
g_forceGrowNext = false;
|
||||
if (nulls >= g_reservedCount && !force)
|
||||
if (nulls >= demand && !force)
|
||||
return; // plenty of room — the common (only) real case
|
||||
|
||||
// Growing moves the buffer, invalidating every entry's self-referential
|
||||
@@ -226,7 +238,7 @@ void Grow() {
|
||||
}
|
||||
|
||||
constexpr int kMargin = 16;
|
||||
const int deficit = (nulls < g_reservedCount) ? (g_reservedCount - nulls) : 0;
|
||||
const int deficit = (nulls < demand) ? (demand - nulls) : 0;
|
||||
const int newCount = count + deficit + kMargin;
|
||||
auto *newBuf = static_cast<uint8_t *>(
|
||||
SMemAlloc(static_cast<unsigned>(newCount) * Offsets::EVENT_ENTRY_STRIDE));
|
||||
@@ -260,10 +272,29 @@ void Grow() {
|
||||
SMemFree(base);
|
||||
|
||||
Debug::Log::Printf("[event] grew table %d -> %d (%d NULL, %d reserved%s)",
|
||||
count, newCount, nulls, g_reservedCount,
|
||||
count, newCount, nulls, demand,
|
||||
force ? ", FORCED" : "");
|
||||
}
|
||||
|
||||
// True when unit field `field` has a slot in the unit's watch copy, giving it
|
||||
// the watched-field table's spare entry when it has none.
|
||||
bool GiveWatchSlot(uint32_t field) {
|
||||
using WatchedIndex_t = uint32_t(__fastcall *)(uint32_t field, uint32_t dwords,
|
||||
uint32_t isActivePlayer);
|
||||
const auto index = reinterpret_cast<WatchedIndex_t>(Offsets::FUN_UNIT_WATCHED_INDEX);
|
||||
if (index(field, 1, 0) != Offsets::UNIT_WATCHED_NONE)
|
||||
return true;
|
||||
auto *table =
|
||||
reinterpret_cast<uint32_t *>(static_cast<uintptr_t>(Offsets::VAR_UNIT_WATCHED_FIELDS));
|
||||
for (int i = 0; i < Offsets::UNIT_WATCHED_FIELD_COUNT; ++i) {
|
||||
if (table[i] == Offsets::UNIT_WATCHED_NONE) {
|
||||
table[i] = field;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AutoReserve::AutoReserve(const char *name, const Game::Doc::Event *doc) {
|
||||
@@ -286,10 +317,30 @@ AutoReserve::AutoReserve(const char *name, const Game::Doc::Event *doc) {
|
||||
g_reserved[g_reservedCount].name = name;
|
||||
g_reserved[g_reservedCount].slot = -1;
|
||||
g_reserved[g_reservedCount].doc = doc;
|
||||
g_reserved[g_reservedCount].fixedId = -1;
|
||||
index_ = g_reservedCount;
|
||||
++g_reservedCount;
|
||||
}
|
||||
|
||||
EngineName::EngineName(int id, const char *name, const Game::Doc::Event *doc) {
|
||||
if (name == nullptr || id < 0 || id >= Offsets::EVENT_NAME_TABLE_STATIC_COUNT ||
|
||||
g_reservedCount >= MAX_RESERVED)
|
||||
return;
|
||||
// A named unit-field slot makes FUN_0051bbb0 watch field `id`, and a
|
||||
// watcher on a field the unit's watch copy has no slot for writes past
|
||||
// the copy, into the object (see FUN_UNIT_WATCHED_INDEX). Such a field
|
||||
// takes the table's spare entry, or the name is refused.
|
||||
if (id < Offsets::EVENT_NAME_UNIT_MAX && !GiveWatchSlot(static_cast<uint32_t>(id)))
|
||||
return;
|
||||
auto **slot = reinterpret_cast<const char **>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_EVENT_NAME_TABLE_STATIC)) + id;
|
||||
if (*slot != nullptr)
|
||||
return; // the engine names it; never take an engine event's slot
|
||||
*slot = name;
|
||||
g_reserved[g_reservedCount] = {name, id, doc, id};
|
||||
++g_reservedCount;
|
||||
}
|
||||
|
||||
int AutoReserve::Slot() const {
|
||||
return index_ >= 0 ? g_reserved[index_].slot : -1;
|
||||
}
|
||||
@@ -357,16 +408,17 @@ void FireValues(int slot, void *L, int firstArgIdx, int nargs) {
|
||||
PushString(L, "event");
|
||||
PushString(L, name);
|
||||
SetTable(L, GLOBALS_INDEX);
|
||||
uintptr_t node = Game::Read<uintptr_t>(entry, Offsets::OFF_EVENT_ENTRY_HEAD);
|
||||
while (node != 0 && (node & 1) == 0) {
|
||||
const uintptr_t next = Game::Read<uintptr_t>(node + Offsets::OFF_EVENT_NODE_NEXT);
|
||||
auto *frame = Game::Read<uint8_t *>(node + Offsets::OFF_EVENT_NODE_FRAME);
|
||||
if (frame != nullptr)
|
||||
// A string arg1 meets RegisterUnitEvent filters, which the engine's
|
||||
// dispatches meet at the runner this walk never calls.
|
||||
const char *unit = nargs > 0 && Type(L, firstArgIdx) == TYPE_STRING
|
||||
? ToString(L, firstArgIdx)
|
||||
: nullptr;
|
||||
ForEachListener(slot, [&](uint8_t *frame) {
|
||||
if (unit == nullptr || Frame::UnitEvent::Accepts(frame, name, unit))
|
||||
Frame::ScriptArgs::RunSlotWithValues(
|
||||
L, reinterpret_cast<const uint32_t *>(frame + Offsets::OFF_FRAME_ONEVENT_SLOT),
|
||||
frame, firstArgIdx, nargs);
|
||||
node = next;
|
||||
}
|
||||
});
|
||||
PushString(L, "event");
|
||||
PushValue(L, top + 1);
|
||||
SetTable(L, GLOBALS_INDEX);
|
||||
@@ -389,7 +441,7 @@ void EnableWrites() { g_writesEnabled = true; }
|
||||
|
||||
void PrepareForReload() {
|
||||
for (int i = 0; i < g_reservedCount; ++i)
|
||||
g_reserved[i].slot = -1;
|
||||
g_reserved[i].slot = g_reserved[i].fixedId;
|
||||
g_writesEnabled = false;
|
||||
g_warnedTableFull = false; // fresh table after the rebuild — re-arm
|
||||
g_growLatched = false; // re-arm the one-shot grow for the new table
|
||||
|
||||
+53
-3
@@ -64,6 +64,36 @@ private:
|
||||
int index_ = -1; // into the reservation table; -1 = dropped (overflow)
|
||||
};
|
||||
|
||||
// Names engine event `id`, an id the engine already signals by number but
|
||||
// 1.12 left unnamed. Place a static instance at file scope:
|
||||
//
|
||||
// static const Event::Custom::EngineName _n{id, "MY_EVENT", &kDoc};
|
||||
//
|
||||
// The unit-field events are the case this serves. `FUN_0051bbb0` watches unit
|
||||
// field `i` only when slot `i` of the static name array
|
||||
// (`VAR_EVENT_NAME_TABLE_STATIC`) is named, and its watcher fires event `i`
|
||||
// once per token of the unit (the field index is the event id). So naming the
|
||||
// slot is all the engine needs. Its table build
|
||||
// (`SetScriptEventTable(0x00BE1198, 0x225)`, every world init) copies the
|
||||
// name, and its own watchers fire the event. Nothing here fires anything.
|
||||
//
|
||||
// The name is written at construction, which is static init, before any world
|
||||
// init or unit watch. Watch and unwatch then always see the same set. Nothing
|
||||
// in the binary writes the array except the boot initializer `0x0051AB30`,
|
||||
// and it stores only the slots it names. A slot the engine already names is
|
||||
// left alone. A unit field the unit's watch copy has no slot for
|
||||
// (FUN_UNIT_WATCHED_INDEX) would have its watchers write over the object, so
|
||||
// it takes the watched-field table's one spare entry first, and the name is
|
||||
// refused when that is gone. The caller proves that nothing else signals
|
||||
// `id` by number while the game table is installed (the census in
|
||||
// `GLUE_EVENT_COUNT`'s comment).
|
||||
//
|
||||
// The event is documented like a reservation: `ReservedName` / `ReservedDoc`
|
||||
// list it, and its slot is `id` for good (never re-claimed on `/reload`).
|
||||
struct EngineName {
|
||||
EngineName(int id, const char *name, const Game::Doc::Event *doc);
|
||||
};
|
||||
|
||||
// Read access to the reservation table for the API documentation: the
|
||||
// number of reserved names, the literal name of the i-th (0-based), and its
|
||||
// descriptor (nullptr when undocumented). Indices are stable for the
|
||||
@@ -92,6 +122,25 @@ int LookupByName(const char *name);
|
||||
// past the live table.
|
||||
bool HasListeners(int slot);
|
||||
|
||||
// Calls `fn(frame)` for each frame registered for the event in `slot`, in
|
||||
// the dispatcher's order. Each node's successor is read before `fn` runs,
|
||||
// as the dispatcher reads it, so `fn` may run the frame's script. Nothing
|
||||
// for a slot `HasListeners` is false for.
|
||||
template <typename Fn> void ForEachListener(int slot, Fn fn) {
|
||||
if (!HasListeners(slot))
|
||||
return;
|
||||
const uint8_t *entry = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR) +
|
||||
slot * Offsets::EVENT_ENTRY_STRIDE;
|
||||
uintptr_t node = Game::Read<uintptr_t>(entry, Offsets::OFF_EVENT_ENTRY_HEAD);
|
||||
while (node != 0 && (node & 1) == 0) {
|
||||
const uintptr_t next = Game::Read<uintptr_t>(node + Offsets::OFF_EVENT_NODE_NEXT);
|
||||
auto *frame = Game::Read<uint8_t *>(node + Offsets::OFF_EVENT_NODE_FRAME);
|
||||
if (frame != nullptr)
|
||||
fn(frame);
|
||||
node = next;
|
||||
}
|
||||
}
|
||||
|
||||
// Fires the event in `slot` with `nargs` Lua values at absolute stack index
|
||||
// `firstArgIdx` as its payload — booleans and nils included, which the
|
||||
// printf-style `Fire` cannot carry. Mirrors the engine dispatcher
|
||||
@@ -99,9 +148,10 @@ bool HasListeners(int slot);
|
||||
// restored after, and each listener's OnEvent runs through
|
||||
// `Frame::ScriptArgs::RunSlotWithValues` (so `arg1..argN`, `this` and the
|
||||
// modern positional arguments are set the way the engine's runner sets
|
||||
// them). The values stay on the stack; the caller pops them. No-op for
|
||||
// `slot < 0` or an event nobody listens to. 4.3.4's equivalent is
|
||||
// `FrameScript_SignalEvent(id, L, count)`.
|
||||
// them). A string first value meets RegisterUnitEvent filters, as a `%s`
|
||||
// arg1 does on the engine's path. The values stay on the stack; the caller
|
||||
// pops them. No-op for `slot < 0` or an event nobody listens to. 4.3.4's
|
||||
// equivalent is `FrameScript_SignalEvent(id, L, count)`.
|
||||
void FireValues(int slot, void *L, int firstArgIdx, int nargs);
|
||||
|
||||
// Dispatches a custom event via the engine's printf-style event
|
||||
|
||||
+89
-10
@@ -86,6 +86,7 @@
|
||||
#include "frame/Region.h"
|
||||
#include "frame/RenderBatch.h"
|
||||
#include "frame/ScriptArgs.h"
|
||||
#include "text/NumericFormatter.h"
|
||||
#include "texture/Load.h"
|
||||
#include "time/Clock.h"
|
||||
#include "xml/FrameLoad.h"
|
||||
@@ -95,6 +96,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace Frame::Cooldown {
|
||||
|
||||
@@ -169,7 +171,8 @@ struct Fields {
|
||||
float abbrevThreshold; // s: below it, m:ss in place of whole minutes
|
||||
float millisecondsThreshold; // s: below it, tenths
|
||||
uint32_t minCountdownDuration; // ms: shorter cooldowns show no numbers
|
||||
char countdownText[16]; // what the font string holds
|
||||
Text::NumericFormatter::Formatter *countdownFormatter; // one reference, or null
|
||||
std::string *countdownText; // what the font string holds; a formatter's text has no bound
|
||||
};
|
||||
|
||||
constexpr size_t kObjectSize = Offsets::SIMPLEFRAME_SIZE + sizeof(Fields);
|
||||
@@ -595,19 +598,37 @@ void FormatCountdown(char (&out)[16], uint32_t ms, float abbrevThreshold,
|
||||
|
||||
// From the update walk: the numbers while the sweep runs, nothing otherwise.
|
||||
// SetText only when the text changes.
|
||||
//
|
||||
// A countdown formatter replaces FormatCountdown and both thresholds, and gets
|
||||
// the seconds left unrounded; the gates stay (retail probe: "%.3f" showed
|
||||
// "28.118" 1.882 s into a 30 s cooldown with the milliseconds threshold at
|
||||
// 100, and nothing on a 1.5 s one). A format error shows no numbers.
|
||||
void UpdateCountdown(void *frame) {
|
||||
using SetText_t = void(__fastcall *)(void *fs, void *edx, const char *text, int expand);
|
||||
Fields &f = F(frame);
|
||||
if (f.countdown == nullptr)
|
||||
return;
|
||||
char text[16] = "";
|
||||
if (!f.hideCountdownNumbers && !f.flashing && !f.done && f.duration > f.minCountdownDuration &&
|
||||
f.elapsed < f.duration && CVar::Factory::GetInt(g_countdownForCooldowns) != 0)
|
||||
FormatCountdown(text, f.duration - f.elapsed, f.abbrevThreshold, f.millisecondsThreshold);
|
||||
if (std::strcmp(text, f.countdownText) == 0)
|
||||
return;
|
||||
std::memcpy(f.countdownText, text, sizeof(text));
|
||||
reinterpret_cast<SetText_t>(Offsets::FUN_FONTSTRING_SET_TEXT)(f.countdown, nullptr, text, 1);
|
||||
const bool shown = !f.hideCountdownNumbers && !f.flashing && !f.done &&
|
||||
f.duration > f.minCountdownDuration && f.elapsed < f.duration &&
|
||||
CVar::Factory::GetInt(g_countdownForCooldowns) != 0;
|
||||
char numbers[16] = "";
|
||||
const char *text = numbers;
|
||||
void *L = nullptr;
|
||||
int top = 0;
|
||||
if (shown && f.countdownFormatter != nullptr && (L = Game::Lua::State()) != nullptr) {
|
||||
top = Game::Lua::GetTop(L);
|
||||
text = Text::NumericFormatter::Format(L, f.countdownFormatter, (f.duration - f.elapsed) / 1000.0);
|
||||
if (text == nullptr)
|
||||
text = "";
|
||||
} else if (shown) {
|
||||
FormatCountdown(numbers, f.duration - f.elapsed, f.abbrevThreshold, f.millisecondsThreshold);
|
||||
}
|
||||
if (*f.countdownText != text) {
|
||||
f.countdownText->assign(text);
|
||||
reinterpret_cast<SetText_t>(Offsets::FUN_FONTSTRING_SET_TEXT)(f.countdown, nullptr, text, 1);
|
||||
}
|
||||
if (L != nullptr)
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
// ---- the overrides ------------------------------------------------------------
|
||||
@@ -645,10 +666,15 @@ void RunOnCooldownDone(void *frame) {
|
||||
|
||||
// CGCooldown::~CGCooldown, then CSimpleFrame's deleting destructor.
|
||||
void *__fastcall DeletingDestructor(void *self, void *, uint32_t flags) {
|
||||
for (Slot &s : F(self).slots) {
|
||||
Fields &f = F(self);
|
||||
for (Slot &s : f.slots) {
|
||||
Texture::ReleaseHandle(s.texture);
|
||||
s.texture = nullptr;
|
||||
}
|
||||
Text::NumericFormatter::Release(f.countdownFormatter);
|
||||
f.countdownFormatter = nullptr;
|
||||
delete f.countdownText;
|
||||
f.countdownText = nullptr;
|
||||
return Base<DeletingDestructor_t>(Offsets::VTBL_FRAME_DELETING_DESTRUCTOR)(self, nullptr,
|
||||
flags);
|
||||
}
|
||||
@@ -884,6 +910,7 @@ void *__fastcall CreateCooldown(void *parent) {
|
||||
// The later clients' defaults (retail probe: 120, 2000).
|
||||
f.abbrevThreshold = 120.0f;
|
||||
f.minCountdownDuration = 2000;
|
||||
f.countdownText = new std::string;
|
||||
f.countdown = CreateCountdown(frame);
|
||||
return frame;
|
||||
}
|
||||
@@ -1218,6 +1245,42 @@ int __fastcall Script_GetMinimumCountdownDuration(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The frame shares the formatter: a later SetBreakpoints on it shows (retail
|
||||
// probe). The text follows at the next update, not at this call.
|
||||
int __fastcall Script_SetCountdownFormatter(void *L) {
|
||||
namespace Formatters = Text::NumericFormatter;
|
||||
void *frame = ResolveCooldown(L);
|
||||
if (frame == nullptr)
|
||||
return 0;
|
||||
Formatters::Formatter *formatter = nullptr;
|
||||
if (Game::Lua::Type(L, 2) > Game::Lua::TYPE_NIL) {
|
||||
formatter = Formatters::ToFormatter(L, 2);
|
||||
if (formatter == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetCountdownFormatter([formatter])",
|
||||
Game::Lua::ObjectDisplayName(frame));
|
||||
return 0;
|
||||
}
|
||||
Formatters::Retain(formatter);
|
||||
}
|
||||
Fields &f = F(frame);
|
||||
Formatters::Release(f.countdownFormatter);
|
||||
f.countdownFormatter = formatter;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// A new Lua object for the formatter each call, as retail's (probe:
|
||||
// rawequal(GetCountdownFormatter(), formatter) is false).
|
||||
int __fastcall Script_GetCountdownFormatter(void *L) {
|
||||
void *frame = ResolveCooldown(L);
|
||||
if (frame == nullptr)
|
||||
return 0;
|
||||
if (Text::NumericFormatter::Formatter *formatter = F(frame).countdownFormatter)
|
||||
Text::NumericFormatter::Push(L, formatter);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// OFFSET (4.3.4): 0x009FF650 / 0x009FF6A0
|
||||
int __fastcall Script_SetReverse(void *L) {
|
||||
void *frame = ResolveCooldown(L);
|
||||
@@ -1296,6 +1359,8 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
{"GetCountdownMillisecondsThreshold", &Script_GetCountdownMillisecondsThreshold},
|
||||
{"SetMinimumCountdownDuration", &Script_SetMinimumCountdownDuration},
|
||||
{"GetMinimumCountdownDuration", &Script_GetMinimumCountdownDuration},
|
||||
{"SetCountdownFormatter", &Script_SetCountdownFormatter},
|
||||
{"GetCountdownFormatter", &Script_GetCountdownFormatter},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kSetCooldownArgs[] = {
|
||||
@@ -1441,6 +1506,18 @@ const Game::Doc::Function kSetMinimumCountdownDuration{
|
||||
"Sets the duration a cooldown must be longer than to show numbers.", kMillisecondsArg, {}};
|
||||
const Game::Doc::Function kGetMinimumCountdownDuration{
|
||||
"Returns the duration a cooldown must be longer than to show numbers.", {}, kMillisecondsRet};
|
||||
const Game::Doc::Field kFormatterArg[] = {
|
||||
Game::Doc::Opt("formatter", "NumericRuleFormatter", nullptr,
|
||||
"Makes the text from the seconds left; nil goes back to the built-in numbers."),
|
||||
};
|
||||
const Game::Doc::Field kFormatterRet[] = {
|
||||
Game::Doc::Opt("formatter", "NumericRuleFormatter"),
|
||||
};
|
||||
const Game::Doc::Function kSetCountdownFormatter{
|
||||
"Sets the formatter that writes the countdown numbers.", kFormatterArg, {}};
|
||||
const Game::Doc::Function kGetCountdownFormatter{
|
||||
"Returns the formatter that writes the countdown numbers, or nil when there is none.", {},
|
||||
kFormatterRet};
|
||||
const Game::Doc::Function kSetBlingTexture{
|
||||
"Sets the texture and colour of the flash at the end of the cooldown.", kTextureArgs, {}};
|
||||
|
||||
@@ -1480,6 +1557,8 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
{"GetCountdownMillisecondsThreshold", &kGetCountdownMillisecondsThreshold},
|
||||
{"SetMinimumCountdownDuration", &kSetMinimumCountdownDuration},
|
||||
{"GetMinimumCountdownDuration", &kGetMinimumCountdownDuration},
|
||||
{"SetCountdownFormatter", &kSetCountdownFormatter},
|
||||
{"GetCountdownFormatter", &kGetCountdownFormatter},
|
||||
};
|
||||
|
||||
// The engine's static initializer for a class hash (Model's 0x0076F5F0).
|
||||
|
||||
+170
-236
@@ -24,26 +24,19 @@
|
||||
// the `this`/`arg1..argN` globals and then `lua_pcall(handler, /*nargs*/0)`.
|
||||
// So `function(self, delta)` gets nothing today.
|
||||
//
|
||||
// We co-hook both runners and REPLACE their pcall tail: keep setting the same
|
||||
// globals (so handlers reading `this`/`arg1` keep working), but push `self` +
|
||||
// `arg1..argN` as real arguments and `pcall` with `nargs = 1 + N`. The
|
||||
// reimplementation mirrors the engine's message-handler errfunc, but saves the
|
||||
// clobbered globals on the Lua stack (not registry refs like the engine) so an
|
||||
// out-of-memory longjmp mid-push unwinds them for free — no leaked refs, no
|
||||
// per-call ref churn. See RunModern.
|
||||
// We redirect each runner's own lua_pcall (RunnerPCall). By then the runner
|
||||
// has done everything else — parsed its format, saved and set `this` and
|
||||
// arg1..argN — so the values are read back from those globals and passed as
|
||||
// `(self, arg1..argN)` with `nargs = 1 + N`. The format is never parsed here.
|
||||
// The runner's switch is the only one, so a spec another DLL adds to it at
|
||||
// runtime keeps working: SuperWoWhook's `%e` (a registry ref) crashed the copy
|
||||
// of the switch this module used to carry.
|
||||
//
|
||||
// The reimplemented path is taken ONLY for a handler that can observe
|
||||
// positional args: a Lua function that declares a parameter or is vararg
|
||||
// (checked via its Proto), or a C function such as a frame method bound
|
||||
// directly — see FunctionWantsArgs. A Lua handler that declares NO parameters
|
||||
// can't observe positional args, so pushing
|
||||
// them is a no-op — those route through the original engine runner unchanged:
|
||||
// provably identical to vanilla, and OFF the engine's hottest Lua path (the vast
|
||||
// majority of vanilla handlers, which read `this`/`arg1`, are param-less). So a
|
||||
// param-less vanilla handler is untouched; only a param-declaring handler sees
|
||||
// the `(self, [event,] arg1..argN)` shape. The one exception is a fire with
|
||||
// more values than the engine runner's 19: 3.3.5 sets every argN, so that
|
||||
// fire takes the reimplemented path whatever the handler.
|
||||
// Positional args go only to a handler that can observe them: a Lua function
|
||||
// that declares a parameter or is vararg (checked via its Proto), or a C
|
||||
// function such as a frame method bound directly — see FunctionWantsArgs. Any
|
||||
// other handler is called exactly as the engine calls it, so a param-less
|
||||
// vanilla handler (the vast majority, reading `this`/`arg1`) is untouched.
|
||||
//
|
||||
// Each OnEvent handler also gets its own `event` global, saved before and
|
||||
// restored after it (SaveEvent), as 3.3.5 does per handler.
|
||||
@@ -66,13 +59,16 @@
|
||||
// Runtime switch, default ON — modern handler signatures are a core 5.1
|
||||
// feature, so ports that write `function(self, delta)` work out of the box.
|
||||
// `SetModernScriptArgs(false)` disables it, `GetModernScriptArgs()` reads the
|
||||
// state. While disabled both detours are a straight passthrough to the original
|
||||
// runner (exact vanilla behavior, zero reimplementation risk) — the OFF escape
|
||||
// valve, since this lands on the hottest Lua path in the engine
|
||||
// (FUN_FRAME_RUN_SCRIPT_ARGS fires for every OnUpdate frame).
|
||||
// state. While disabled the redirected pcall is the engine's call unchanged —
|
||||
// exact vanilla behavior.
|
||||
//
|
||||
// Scripts the engine never runs (RunWithValues: custom events,
|
||||
// OnAttributeChanged, …) carry arbitrary Lua values, which no runner format
|
||||
// can, so RunCore runs those itself, as 3.3.5's FrameScript_Execute does.
|
||||
|
||||
#include "frame/ScriptArgs.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "HookSecureFunc.h"
|
||||
#include "Offsets.h"
|
||||
@@ -85,9 +81,9 @@ namespace Frame::ScriptArgs {
|
||||
|
||||
namespace {
|
||||
|
||||
// Runtime switch. Default ON (core 5.1 behavior). When false, both detours
|
||||
// tail-call the original runner unchanged (exact vanilla behavior, no
|
||||
// reimplementation risk) — the OFF escape valve for this hot path.
|
||||
// Runtime switch. Default ON (core 5.1 behavior). When false, the redirected
|
||||
// pcall makes the engine's own call and RunWithValues passes no positional
|
||||
// args — exact vanilla behavior.
|
||||
bool g_enabled = true;
|
||||
|
||||
// Chained at static-init time by each `AutoObserve` constructor.
|
||||
@@ -101,109 +97,11 @@ void Notify(bool begin, int handlerRef, void *frame) {
|
||||
// Chained at static-init time by each `AutoTrailingBool` constructor.
|
||||
AutoTrailingBool *g_trailing = nullptr;
|
||||
|
||||
// The engine runner sets only arg1..arg19 (0x704F10 stops at the 19th spec,
|
||||
// `cmp [ebp-4],0x13; jge` at 0x704F73). 3.3.5's FrameScript_Execute has no
|
||||
// limit, so a fire with more specs than this takes the reimplemented path.
|
||||
constexpr int kEngineArgLimit = 19;
|
||||
|
||||
using Game::Lua::RawGetI;
|
||||
|
||||
// Writes "arg" + k into buf.
|
||||
void ArgName(char (&buf)[16], int k) { std::snprintf(buf, sizeof buf, "arg%d", k); }
|
||||
|
||||
// Counts the values in the runner's printf-style format: each %d/%u/%f/%s.
|
||||
// Any other letter after `%` is skipped and consumes no vararg, as in the
|
||||
// engine runner's switch.
|
||||
int CountArgs(const char *fmt) {
|
||||
if (fmt == nullptr)
|
||||
return 0;
|
||||
int n = 0;
|
||||
for (const char *p = fmt; *p != '\0'; ++p) {
|
||||
if (*p != '%')
|
||||
continue;
|
||||
++p;
|
||||
if (*p == '\0')
|
||||
break;
|
||||
if (*p == 'd' || *p == 'u' || *p == 'f' || *p == 's')
|
||||
++n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// Where RunCore's arguments come from: the runner's printf-style format over
|
||||
// its packed vararg buffer (%d/%u/%s 4 bytes, %f 8), or `n` Lua values already
|
||||
// on the stack from absolute index `stackBase` (they sit below everything
|
||||
// RunCore pushes, so the index holds). `hasTrailing` appends one boolean after
|
||||
// them (see AutoTrailingBool).
|
||||
struct ArgSource {
|
||||
const char *fmt = nullptr;
|
||||
const void *vaPtr = nullptr;
|
||||
int stackBase = 0;
|
||||
int n = 0;
|
||||
bool hasTrailing = false;
|
||||
bool trailing = false;
|
||||
int Count() const { return n + (hasTrailing ? 1 : 0); }
|
||||
};
|
||||
|
||||
// Pushes an ArgSource's values in order, one per PushNext; a new cursor
|
||||
// starts again from the first.
|
||||
class ArgCursor {
|
||||
public:
|
||||
explicit ArgCursor(const ArgSource &src)
|
||||
: src_(src), p_(src.fmt), cur_(static_cast<const char *>(src.vaPtr)) {}
|
||||
|
||||
void PushNext(void *L) {
|
||||
using namespace Game::Lua;
|
||||
if (k_ >= src_.n) {
|
||||
PushBool(L, src_.trailing);
|
||||
++k_;
|
||||
return;
|
||||
}
|
||||
if (p_ == nullptr) {
|
||||
PushValue(L, src_.stackBase + k_++);
|
||||
return;
|
||||
}
|
||||
for (; *p_ != '\0'; ++p_) {
|
||||
if (*p_ != '%')
|
||||
continue;
|
||||
++p_;
|
||||
const char spec = *p_;
|
||||
if (spec == '\0')
|
||||
return;
|
||||
switch (spec) {
|
||||
case 'd':
|
||||
PushNumber(L, static_cast<double>(*reinterpret_cast<const int *>(cur_)));
|
||||
cur_ += 4;
|
||||
break;
|
||||
case 'u':
|
||||
PushNumber(L, static_cast<double>(
|
||||
*reinterpret_cast<const unsigned int *>(cur_)));
|
||||
cur_ += 4;
|
||||
break;
|
||||
case 'f':
|
||||
PushNumber(L, *reinterpret_cast<const double *>(cur_));
|
||||
cur_ += 8;
|
||||
break;
|
||||
case 's': // a null string pushes nil, as lua_pushstring does
|
||||
PushString(L, *reinterpret_cast<const char *const *>(cur_));
|
||||
cur_ += 4;
|
||||
break;
|
||||
default:
|
||||
continue; // unknown spec — consume nothing
|
||||
}
|
||||
++p_;
|
||||
++k_;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
const ArgSource &src_;
|
||||
const char *p_;
|
||||
const char *cur_;
|
||||
int k_ = 0;
|
||||
};
|
||||
|
||||
// OnEvent dispatch? The event dispatchers invoke the frame's OnEvent slot
|
||||
// (frame+0x0C); matching the handler ref against that slot identifies it
|
||||
// exactly.
|
||||
@@ -239,53 +137,42 @@ void RestoreEvent(void *L, int saved) {
|
||||
SetTop(L, saved - 1);
|
||||
}
|
||||
|
||||
// Reimplements the runner tail: set the `this`/`arg1..argN` globals, invoke the
|
||||
// handler with modern positional args `(self, [event,] arg1..argN)` under the
|
||||
// engine's own message-handler errfunc, then restore the globals. `fmt`/`vaPtr`
|
||||
// are null for the no-arg runner (N == 0).
|
||||
// Runs a handler with the `n` Lua values at absolute index `firstArgIdx` (they
|
||||
// sit below everything this pushes, so the index holds): set the
|
||||
// `this`/`arg1..argN` globals, invoke the handler with modern positional args
|
||||
// `(self, [event,] arg1..argN)` under the engine's own message-handler
|
||||
// errfunc, then restore the globals.
|
||||
//
|
||||
// Unlike the engine (and our first cut), the old global values are saved on the
|
||||
// Lua STACK, not in the registry via luaL_ref. A registry save leaks its ref if
|
||||
// Unlike the engine runners, the old global values are saved on the Lua
|
||||
// STACK, not in the registry via luaL_ref. A registry save leaks its ref if
|
||||
// any push before the protected call longjmps — and out-of-memory, the one
|
||||
// failure that actually fires here, does exactly that. Stack slots need no
|
||||
// cleanup: an unwinding error resets `L->top` past them for free, so nothing
|
||||
// leaks and there's no per-call ref churn on the OnUpdate hot path. The cost is
|
||||
// cleanup: an unwinding error resets `L->top` past them for free. The cost is
|
||||
// a deeper stack (up to ~2*N slots live at once), so we CheckStack up front.
|
||||
//
|
||||
// Returns false WITHOUT side effects when it declines (no Lua state, or the
|
||||
// stack can't grow); the caller then runs the original. Every false path is
|
||||
// before the first push, so falling back never double-runs the handler.
|
||||
//
|
||||
// `positional` false sets the globals but calls the handler with no
|
||||
// arguments — the vanilla shape, for RunWithValues while the switch is off.
|
||||
bool RunCore(void *L, int handlerRef, void *frame, const ArgSource &src,
|
||||
bool positional) {
|
||||
// arguments — the vanilla shape, while the switch is off.
|
||||
void RunCore(void *L, int handlerRef, void *frame, int firstArgIdx, int n, bool positional) {
|
||||
using namespace Game::Lua;
|
||||
// Headroom for the saved-old block plus the transient set/call pushes.
|
||||
if (CheckStack(L, 2 * src.Count() + 16) == 0)
|
||||
return false;
|
||||
if (CheckStack(L, 2 * n + 16) == 0)
|
||||
return;
|
||||
|
||||
const bool haveThis = frame != nullptr;
|
||||
// When so, `event` (a global the dispatcher has set) is prepended as the
|
||||
// first positional to mirror retail's (self, event, arg1..argN).
|
||||
const bool isOnEvent = IsOnEvent(handlerRef, frame);
|
||||
const int selfRef = haveThis ? Game::Lua::ObjectLuaRef(frame) : 0;
|
||||
|
||||
const int n = src.Count();
|
||||
const int selfRef = Game::Lua::ObjectLuaRef(frame);
|
||||
char argName[16];
|
||||
|
||||
// --- save the globals we're about to clobber, onto the stack ------------
|
||||
// Absolute indices, live until the closing SetTop(base):
|
||||
// base+1 = old _G.this (if haveThis)
|
||||
// savedArgBase+1 .. +n = old _G.arg1..argN
|
||||
// base+1 = old _G.this
|
||||
// base+2 .. base+1+n = old _G.arg1..argN
|
||||
// Raw reads, as 3.3.5's FrameScript_Execute saves them: a `_G` __index
|
||||
// (a strict-globals guard) never runs for an unset argN.
|
||||
const int base = GetTop(L);
|
||||
if (haveThis) {
|
||||
PushString(L, "this");
|
||||
RawGet(L, GLOBALS_INDEX); // [.. oldThis]
|
||||
}
|
||||
const int savedArgBase = base + (haveThis ? 1 : 0);
|
||||
PushString(L, "this");
|
||||
RawGet(L, GLOBALS_INDEX); // [.. oldThis]
|
||||
for (int k = 0; k < n; ++k) {
|
||||
ArgName(argName, k + 1);
|
||||
PushString(L, argName);
|
||||
@@ -293,16 +180,13 @@ bool RunCore(void *L, int handlerRef, void *frame, const ArgSource &src,
|
||||
}
|
||||
|
||||
// --- set the new globals (each op is stack-neutral) ---------------------
|
||||
if (haveThis) {
|
||||
PushString(L, "this");
|
||||
RawGetI(L, REGISTRY_INDEX, selfRef); // frame object
|
||||
SetTable(L, GLOBALS_INDEX);
|
||||
}
|
||||
ArgCursor globals(src);
|
||||
PushString(L, "this");
|
||||
RawGetI(L, REGISTRY_INDEX, selfRef); // frame object
|
||||
SetTable(L, GLOBALS_INDEX);
|
||||
for (int k = 0; k < n; ++k) {
|
||||
ArgName(argName, k + 1);
|
||||
PushString(L, argName);
|
||||
globals.PushNext(L);
|
||||
PushValue(L, firstArgIdx + k);
|
||||
SetTable(L, GLOBALS_INDEX);
|
||||
}
|
||||
|
||||
@@ -318,17 +202,13 @@ bool RunCore(void *L, int handlerRef, void *frame, const ArgSource &src,
|
||||
RawGetI(L, REGISTRY_INDEX, handlerRef); // handler
|
||||
int nargs = 0;
|
||||
if (positional) {
|
||||
if (haveThis)
|
||||
RawGetI(L, REGISTRY_INDEX, selfRef); // self
|
||||
else
|
||||
PushNil(L);
|
||||
RawGetI(L, REGISTRY_INDEX, selfRef); // self
|
||||
if (isOnEvent) { // event goes before the args, retail-style
|
||||
PushString(L, "event");
|
||||
RawGet(L, GLOBALS_INDEX);
|
||||
}
|
||||
ArgCursor args(src);
|
||||
for (int k = 0; k < n; ++k)
|
||||
args.PushNext(L);
|
||||
PushValue(L, firstArgIdx + k);
|
||||
nargs = 1 + (isOnEvent ? 1 : 0) + n;
|
||||
}
|
||||
PCall(L, nargs, 0, errIdx);
|
||||
@@ -338,41 +218,22 @@ bool RunCore(void *L, int handlerRef, void *frame, const ArgSource &src,
|
||||
for (int k = 0; k < n; ++k) {
|
||||
ArgName(argName, k + 1);
|
||||
PushString(L, argName);
|
||||
PushValue(L, savedArgBase + 1 + k);
|
||||
SetTable(L, GLOBALS_INDEX);
|
||||
}
|
||||
if (haveThis) {
|
||||
PushString(L, "this");
|
||||
PushValue(L, base + 1);
|
||||
PushValue(L, base + 2 + k);
|
||||
SetTable(L, GLOBALS_INDEX);
|
||||
}
|
||||
PushString(L, "this");
|
||||
PushValue(L, base + 1);
|
||||
SetTable(L, GLOBALS_INDEX);
|
||||
SetTop(L, base);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Asks the providers for this run's trailing boolean.
|
||||
void FindTrailing(int handlerRef, void *frame, ArgSource &src) {
|
||||
for (AutoTrailingBool *t = g_trailing; t != nullptr; t = t->next) {
|
||||
bool value = false;
|
||||
if (t->fn(handlerRef, frame, &value)) {
|
||||
src.hasTrailing = true;
|
||||
src.trailing = value;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The runner path: arguments from the engine's printf-style format.
|
||||
bool RunModern(int handlerRef, void *frame, const char *fmt, const void *vaPtr) {
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return false;
|
||||
ArgSource src;
|
||||
src.fmt = fmt;
|
||||
src.vaPtr = vaPtr;
|
||||
src.n = CountArgs(fmt);
|
||||
FindTrailing(handlerRef, frame, src);
|
||||
return RunCore(L, handlerRef, frame, src, /*positional*/ true);
|
||||
// Asks the providers for this run's trailing boolean; false when none takes
|
||||
// one.
|
||||
bool FindTrailing(int handlerRef, void *frame, bool *value) {
|
||||
for (AutoTrailingBool *t = g_trailing; t != nullptr; t = t->next)
|
||||
if (t->fn(handlerRef, frame, value))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Lua 5.0 internal offsets for reading a handler's arity — verified against the
|
||||
@@ -442,69 +303,144 @@ bool FunctionWantsArgs(const void *tv, int depth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Does this handler actually observe positional args? A vanilla handler reads
|
||||
// this/arg1 globals and declares NO parameters, so pushing positional args to
|
||||
// it is observably identical to vanilla — those route through the original
|
||||
// engine runner instead (exact vanilla, and off the hottest Lua path). Only a
|
||||
// handler FunctionWantsArgs accepts takes the reimplemented positional path.
|
||||
// --- The runners' lua_pcall, redirected -------------------------------------
|
||||
// Each runner calls `lua_pcall(L, nargs, nresults, errfunc)` with [errfunc,
|
||||
// handler] on top, `this` and arg1..arg`count` already set. A handler that
|
||||
// observes positional args (FunctionWantsArgs, read straight off the top) gets
|
||||
// `(self, [event,] arg1..argN [, trailing])`, each argN read back raw from the
|
||||
// global the runner just set; any other handler, and every run while the
|
||||
// switch is off, gets the engine's call unchanged.
|
||||
//
|
||||
// Reads the closures directly (the engine exposes no arity via the C API);
|
||||
// every read is bounded — a non-function ref or a null Proto falls through to
|
||||
// "no".
|
||||
bool HandlerWantsArgs(void *L, int handlerRef) {
|
||||
RawGetI(L, Game::Lua::REGISTRY_INDEX, handlerRef); // push registry[handlerRef]
|
||||
const void *tv = reinterpret_cast<const void *>(
|
||||
// A trailing boolean (AutoTrailingBool) also becomes global arg`count+1` for
|
||||
// the call, as 3.3.5 sets it. Its old value waits on the stack below the
|
||||
// errfunc, where the runner's own pops after the call never reach it.
|
||||
int __fastcall RunnerPCall(void *L, int nargs, int nresults, int errfunc, int handlerRef,
|
||||
void *frame, int count) {
|
||||
using namespace Game::Lua;
|
||||
const void *handler = reinterpret_cast<const void *>(
|
||||
Game::Read<uintptr_t>(L, OFF_LUASTATE_TOP) - kTValueSize);
|
||||
const bool wants = FunctionWantsArgs(tv, 0);
|
||||
Game::Lua::SetTop(L, -2); // pop the handler
|
||||
return wants;
|
||||
// Headroom for self, event, the args, the trailing value and its save.
|
||||
if (!g_enabled || !FunctionWantsArgs(handler, 0) || CheckStack(L, count + 8) == 0)
|
||||
return PCall(L, nargs, nresults, errfunc);
|
||||
|
||||
bool trailing = false;
|
||||
const bool hasTrailing = FindTrailing(handlerRef, frame, &trailing);
|
||||
char trailingName[16];
|
||||
int saved = 0;
|
||||
if (hasTrailing) {
|
||||
ArgName(trailingName, count + 1);
|
||||
PushString(L, trailingName);
|
||||
RawGet(L, GLOBALS_INDEX); // [.. errfunc handler old]
|
||||
Insert(L, -3); // [.. old errfunc handler]
|
||||
saved = GetTop(L) - 2;
|
||||
PushString(L, trailingName);
|
||||
PushBool(L, trailing);
|
||||
SetTable(L, GLOBALS_INDEX);
|
||||
}
|
||||
|
||||
int pushed = 1;
|
||||
if (frame != nullptr)
|
||||
RawGetI(L, REGISTRY_INDEX, Game::Lua::ObjectLuaRef(frame)); // self
|
||||
else
|
||||
PushNil(L);
|
||||
if (IsOnEvent(handlerRef, frame)) { // event goes before the args, retail-style
|
||||
PushString(L, "event");
|
||||
RawGet(L, GLOBALS_INDEX);
|
||||
++pushed;
|
||||
}
|
||||
char argName[16];
|
||||
for (int k = 1; k <= count; ++k) {
|
||||
ArgName(argName, k);
|
||||
PushString(L, argName);
|
||||
RawGet(L, GLOBALS_INDEX);
|
||||
}
|
||||
pushed += count;
|
||||
if (hasTrailing) {
|
||||
PushBool(L, trailing);
|
||||
++pushed;
|
||||
}
|
||||
// Both runners pass errfunc = -2, relative to the top: it moves down by
|
||||
// every argument pushed above it.
|
||||
const int status = PCall(L, nargs + pushed, nresults, errfunc - pushed);
|
||||
|
||||
if (hasTrailing) {
|
||||
PushString(L, trailingName);
|
||||
PushValue(L, saved);
|
||||
SetTable(L, GLOBALS_INDEX);
|
||||
Remove(L, saved);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
// --- FUN_FRAME_RUN_SCRIPT_ARGS co-hook (scripts with values) ----------------
|
||||
// The runners reach RunnerPCall through these. ECX/EDX (L, nargs) and the
|
||||
// pcall's two stack arguments pass through; each thunk adds the run's handler
|
||||
// ref, frame and argument count from the runner's own frame (see
|
||||
// CALL_FRAME_RUN_SCRIPT_ARGS_PCALL), then returns as lua_pcall does (`ret 8`).
|
||||
//
|
||||
// FUN_FRAME_RUN_SCRIPT_ARGS: [ebp+8] handlerRef, [ebp+0Ch] frame, [ebp-4] the
|
||||
// number of argN its switch set — every spec it ran, one added at runtime
|
||||
// included.
|
||||
__declspec(naked) void RunArgsPCallThunk() {
|
||||
__asm {
|
||||
push dword ptr [ebp - 4] // count
|
||||
push dword ptr [ebp + 0Ch] // frame
|
||||
push dword ptr [ebp + 8] // handlerRef
|
||||
push dword ptr [esp + 14h] // errfunc
|
||||
push dword ptr [esp + 14h] // nresults
|
||||
call RunnerPCall
|
||||
ret 8
|
||||
}
|
||||
}
|
||||
|
||||
// FUN_FRAME_INVOKE_SCRIPT: [ebp-8] handlerRef, EDI frame; it sets no argN.
|
||||
__declspec(naked) void InvokePCallThunk() {
|
||||
__asm {
|
||||
push 0 // count
|
||||
push edi // frame
|
||||
push dword ptr [ebp - 8] // handlerRef
|
||||
push dword ptr [esp + 14h] // errfunc
|
||||
push dword ptr [esp + 14h] // nresults
|
||||
call RunnerPCall
|
||||
ret 8
|
||||
}
|
||||
}
|
||||
|
||||
// Runs on both registration passes, so the glue screen's scripts get the
|
||||
// positional args too. Idempotent.
|
||||
void PatchCode() {
|
||||
Common::RedirectCall(Offsets::CALL_FRAME_RUN_SCRIPT_ARGS_PCALL, Offsets::LUA_PCALL,
|
||||
reinterpret_cast<const void *>(&RunArgsPCallThunk));
|
||||
Common::RedirectCall(Offsets::CALL_FRAME_INVOKE_SCRIPT_PCALL, Offsets::LUA_PCALL,
|
||||
reinterpret_cast<const void *>(&InvokePCallThunk));
|
||||
}
|
||||
|
||||
const Game::GlueModuleAutoRegister _gluePatch{&PatchCode};
|
||||
|
||||
// --- Runner co-hooks: observers and the per-handler `event` ------------------
|
||||
using RunArgs_t = void(__cdecl *)(int handlerRef, void *frame, const char *fmt,
|
||||
const void *vaPtr);
|
||||
RunArgs_t g_origRunArgs = nullptr;
|
||||
|
||||
void RunArgs(void *L, int handlerRef, void *frame, const char *fmt,
|
||||
const void *vaPtr) {
|
||||
// Past the engine's limit the original would drop arg20 onward, so even a
|
||||
// handler that declares no parameters takes the uncapped path.
|
||||
if (g_enabled && L != nullptr && handlerRef != 0 && fmt != nullptr &&
|
||||
(HandlerWantsArgs(L, handlerRef) || CountArgs(fmt) > kEngineArgLimit) &&
|
||||
RunModern(handlerRef, frame, fmt, vaPtr))
|
||||
return;
|
||||
g_origRunArgs(handlerRef, frame, fmt, vaPtr);
|
||||
}
|
||||
|
||||
void __cdecl RunArgs_h(int handlerRef, void *frame, const char *fmt,
|
||||
const void *vaPtr) {
|
||||
void *L = Game::Lua::State();
|
||||
Notify(true, handlerRef, frame);
|
||||
const int savedEvent = SaveEvent(L, handlerRef, frame);
|
||||
RunArgs(L, handlerRef, frame, fmt, vaPtr);
|
||||
g_origRunArgs(handlerRef, frame, fmt, vaPtr);
|
||||
RestoreEvent(L, savedEvent);
|
||||
Notify(false, handlerRef, frame);
|
||||
}
|
||||
|
||||
// --- FUN_FRAME_INVOKE_SCRIPT co-hook (no-arg scripts) -----------------------
|
||||
// Same address Tooltip::SetEvents calls directly to fire OnTooltipSet*; with
|
||||
// the switch enabled those fire with (self) too, which is the modern shape.
|
||||
using Invoke_t = void(__fastcall *)(int handlerRef, void *frame);
|
||||
Invoke_t g_origInvoke = nullptr;
|
||||
|
||||
void Invoke(void *L, int handlerRef, void *frame) {
|
||||
if (g_enabled && L != nullptr && handlerRef != 0 && frame != nullptr &&
|
||||
HandlerWantsArgs(L, handlerRef) &&
|
||||
RunModern(handlerRef, frame, /*fmt*/ nullptr, /*vaPtr*/ nullptr))
|
||||
return;
|
||||
g_origInvoke(handlerRef, frame);
|
||||
}
|
||||
|
||||
void __fastcall Invoke_h(int handlerRef, void *frame) {
|
||||
void *L = Game::Lua::State();
|
||||
Notify(true, handlerRef, frame);
|
||||
const int savedEvent = SaveEvent(L, handlerRef, frame);
|
||||
Invoke(L, handlerRef, frame);
|
||||
g_origInvoke(handlerRef, frame);
|
||||
RestoreEvent(L, savedEvent);
|
||||
Notify(false, handlerRef, frame);
|
||||
}
|
||||
@@ -532,6 +468,7 @@ int __fastcall Script_GetModernScriptArgs(void *L) {
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
PatchCode();
|
||||
Game::Lua::RegisterGlobalFunction("SetModernScriptArgs",
|
||||
&Script_SetModernScriptArgs);
|
||||
Game::Lua::RegisterGlobalFunction("GetModernScriptArgs",
|
||||
@@ -545,12 +482,9 @@ const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
void RunWithValues(void *L, int handlerRef, void *frame, int firstArgIdx, int nargs) {
|
||||
if (handlerRef == 0 || frame == nullptr)
|
||||
return;
|
||||
ArgSource src;
|
||||
src.stackBase = firstArgIdx;
|
||||
src.n = nargs;
|
||||
Notify(true, handlerRef, frame);
|
||||
const int savedEvent = SaveEvent(L, handlerRef, frame);
|
||||
RunCore(L, handlerRef, frame, src, /*positional*/ g_enabled);
|
||||
RunCore(L, handlerRef, frame, firstArgIdx, nargs, /*positional*/ g_enabled);
|
||||
RestoreEvent(L, savedEvent);
|
||||
Notify(false, handlerRef, frame);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
namespace Frame::ScriptArgs {
|
||||
|
||||
// Runs a frame-script handler with arbitrary Lua values as its arguments —
|
||||
// the engine's runners can only pass %d/%u/%f/%s. `nargs` values at absolute
|
||||
// the engine's runners pass only what a printf-style format can carry (no
|
||||
// booleans). `nargs` values at absolute
|
||||
// stack index `firstArgIdx` become `arg1..argN` (globals, saved and restored
|
||||
// around the call) and, while modern script args are on, the positional
|
||||
// `(self, ...)`. Sets `this`, runs under the engine's script error handler,
|
||||
|
||||
+44
-17
@@ -37,6 +37,8 @@
|
||||
// of its tokens, we return without invoking the handler. Suppressing at the
|
||||
// runner also suppresses HookScript chains and Frame::ScriptArgs' positional
|
||||
// path — they all live downstream of it, which is what retail does too.
|
||||
// Event::Custom::FireValues walks the chain itself and never reaches that
|
||||
// runner, so it asks `Accepts` (UnitEvent.h) for each frame instead.
|
||||
//
|
||||
// Semantics mirror Blizzard's 5.4.8 implementation (Ghidra-verified), where the
|
||||
// filter lives ON the subscriber node and the node's kind is fixed when it is
|
||||
@@ -66,6 +68,8 @@
|
||||
// we never see), fires as with RegisterEvent — one rule that holds on every
|
||||
// dispatch path, whose failure mode is an extra event rather than a lost one.
|
||||
|
||||
#include "UnitEvent.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/RunnerHook.h"
|
||||
@@ -249,6 +253,24 @@ int __fastcall Script_IsEventRegistered(void *L) {
|
||||
|
||||
// ---- the gate ------------------------------------------------------------------
|
||||
|
||||
// The frame's filter for the event being dispatched. `eventName` is the
|
||||
// event-table entry's own name, which Filter::event was copied from, so a
|
||||
// plain strcmp matches it.
|
||||
const Filter *DispatchFilter(const std::vector<Filter> &filters, const char *eventName) {
|
||||
for (const Filter &f : filters)
|
||||
if (std::strcmp(f.event.c_str(), eventName) == 0)
|
||||
return &f;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool MatchesUnit(const Filter &f, const char *arg1) {
|
||||
if (arg1 != nullptr)
|
||||
for (const std::string &u : f.units)
|
||||
if (EqualsIgnoreCase(arg1, u.c_str()))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// True (and `*out` = arg1) when the runner's first conversion spec is `%s` —
|
||||
// the unit token of every unit event. Anything else means arg1 isn't a string
|
||||
// and the filter does not apply.
|
||||
@@ -292,24 +314,9 @@ bool OnRun(void *frame, uint32_t *slotPtr, const char *fmt, void *varargs) {
|
||||
Game::Lua::PushString(L, "event");
|
||||
Game::Lua::RawGet(L, Game::Lua::GLOBALS_INDEX);
|
||||
const char *ev = Game::Lua::ToString(L, -1);
|
||||
const Filter *match = nullptr;
|
||||
if (ev != nullptr) {
|
||||
for (const Filter &f : it->second) {
|
||||
if (std::strcmp(f.event.c_str(), ev) == 0) {
|
||||
match = &f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const Filter *match = ev != nullptr ? DispatchFilter(it->second, ev) : nullptr;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (match == nullptr)
|
||||
return false;
|
||||
if (arg1 != nullptr) {
|
||||
for (const std::string &u : match->units)
|
||||
if (EqualsIgnoreCase(arg1, u.c_str()))
|
||||
return false;
|
||||
}
|
||||
return true; // no token matched — suppress
|
||||
return match != nullptr && !MatchesUnit(*match, arg1); // no token matched — suppress
|
||||
}
|
||||
|
||||
// ---- unregister co-hooks (the filter dies with the node) ------------------------
|
||||
@@ -367,4 +374,24 @@ const Game::HookAutoRegister _hookUnregisterAll{
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Accepts(const void *frame, const char *eventName, const char *arg1) {
|
||||
if (g_filters.empty() || frame == nullptr || eventName == nullptr)
|
||||
return true;
|
||||
auto it = g_filters.find(frame);
|
||||
if (it == g_filters.end())
|
||||
return true;
|
||||
const Filter *match = DispatchFilter(it->second, eventName);
|
||||
return match == nullptr || MatchesUnit(*match, arg1);
|
||||
}
|
||||
|
||||
const std::vector<std::string> *Units(const void *frame, const char *eventName) {
|
||||
if (g_filters.empty() || frame == nullptr || eventName == nullptr)
|
||||
return nullptr;
|
||||
auto it = g_filters.find(frame);
|
||||
if (it == g_filters.end())
|
||||
return nullptr;
|
||||
const Filter *match = DispatchFilter(it->second, eventName);
|
||||
return match != nullptr ? &match->units : nullptr;
|
||||
}
|
||||
|
||||
} // namespace Frame::UnitEvent
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user