mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
Compare commits
40
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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,6 +100,7 @@ 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` |
|
||||
@@ -131,7 +132,7 @@ 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` |
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
+847
-94
File diff suppressed because it is too large
Load Diff
+18
-5
@@ -401,12 +401,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 +417,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) {
|
||||
|
||||
+9
-3
@@ -516,9 +516,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 +533,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
|
||||
|
||||
+296
-18
@@ -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
|
||||
@@ -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
|
||||
@@ -2647,6 +2706,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 +2803,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).
|
||||
@@ -4853,6 +4932,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 +5620,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 +6177,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 +7931,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,
|
||||
@@ -10161,13 +10325,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 +10976,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 +11115,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 +11189,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
|
||||
@@ -12736,6 +12980,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 +13109,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 +13153,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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
@@ -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,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,267 @@
|
||||
-- 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.
|
||||
-- - 1.12 has no global wipe, so UnregisterEvents uses table.wipe.
|
||||
|
||||
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 = table.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,465 @@
|
||||
-- Backport of retail's Blizzard_SharedXML/MixinUtil.lua.
|
||||
--
|
||||
-- Implementation differences from the retail source:
|
||||
-- - wipe is table.wipe here.
|
||||
|
||||
local wipe = table.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
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 = "新設定";
|
||||
|
||||
@@ -1027,20 +1027,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
|
||||
@@ -1266,8 +1252,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
|
||||
|
||||
+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
|
||||
|
||||
+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
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Frame::UnitEvent {
|
||||
|
||||
// The RegisterUnitEvent filter for one fire whose first argument is the
|
||||
// string `arg1`: false when `frame` registered `eventName` (the event-table
|
||||
// entry's own name) with units and `arg1` matches none of them. True for a
|
||||
// plain registration. The engine's dispatches meet this filter at the runner;
|
||||
// a dispatcher of our own that walks the subscriber chain itself
|
||||
// (Event::Custom::FireValues) asks here for each frame.
|
||||
bool Accepts(const void *frame, const char *eventName, const char *arg1);
|
||||
|
||||
// The units `frame` gave RegisterUnitEvent for `eventName` (the event-table
|
||||
// entry's own name), or nullptr for a plain registration. Valid until the
|
||||
// frame's registrations change.
|
||||
const std::vector<std::string> *Units(const void *frame, const char *eventName);
|
||||
|
||||
} // namespace Frame::UnitEvent
|
||||
@@ -0,0 +1,240 @@
|
||||
// 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/>.
|
||||
|
||||
// `UNIT_CONNECTION`, `UNIT_AREA_CHANGED` and the generic power events for a
|
||||
// group member's party record.
|
||||
//
|
||||
// A group member's stats packet (ReadPartyMemberStats, 0x005E5110) and the
|
||||
// player's own record check (ComparePartyRecord, 0x005F0B80) end in
|
||||
// SignalPartyRecordChanges (FUN_PARTY_RECORD_SIGNAL), which raises the unit
|
||||
// events for the fields that differ between the old and the new record. This
|
||||
// is the 1.12 copy of 4.3.4's CGPlayer_C::DispatchPartyMemberStatsChanges
|
||||
// (Player_C.cpp), which in the same places also signals UNIT_CONNECTION
|
||||
// `(token, isConnected)` when the online bit of the status flags flips, and
|
||||
// UNIT_POWER / UNIT_POWER_FREQUENT / UNIT_MAXPOWER with the power token
|
||||
// beside each per-type power event. Retail adds UNIT_AREA_CHANGED `(token)`
|
||||
// for the zone; DiffPartyRecords sets its bit (0x80, record +0x14) and the
|
||||
// raiser reads nothing for it. The server sends the zone only to members
|
||||
// that cannot see the player (tortoise-wow's Group::UpdatePlayerOutOfRange).
|
||||
// A member in view is built with the player's own zone (BuildPartyRecord
|
||||
// stores s_zoneId), which is also the zone GetRaidRosterInfo gives for a
|
||||
// held object, so ComparePartyRecord sets the bit when a member comes into
|
||||
// view from another zone. It never runs for the active player
|
||||
// (CGPlayer_C::PostInit skips it), so "player" gets no event.
|
||||
//
|
||||
// Both calls point here instead (Common::RedirectCall): the function calls
|
||||
// the raiser, then adds those events for the same change bits, after the
|
||||
// engine's own events and with the new record already stored, so
|
||||
// UnitIsConnected and UnitPower read the new values. A status change also
|
||||
// raises the power event, and the raiser then drops the health and power
|
||||
// bits, so the power events follow that order exactly.
|
||||
//
|
||||
// A member who logs off is reported only in the group list (SMSG 0x7D):
|
||||
// tortoise-wow's Group::UpdatePlayerOnlineStatus sends SendUpdate alone for
|
||||
// a logout. 4.3.4's CGPartyInfo::AddMember ends that path with the same
|
||||
// diff and dispatch; 1.12's AddPartyMember stops after storing the record,
|
||||
// so its call gets the missing tail. A raid member outside the player's
|
||||
// party has no party record: RebuildRaidRoster sets its online bit in the
|
||||
// raid slot, so the online bits before and after that call are compared,
|
||||
// and a flip fires UNIT_CONNECTION, as retail's raid frames expect. Party
|
||||
// members are left to AddPartyMember, which ran first.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "unit/Identity.h"
|
||||
#include "unit/PowerEvents.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Group::MemberEvents {
|
||||
|
||||
namespace {
|
||||
|
||||
// DiffPartyRecords (0x005E5510) change bits the raiser reads.
|
||||
constexpr uint32_t CHANGE_STATUS = 0x1;
|
||||
constexpr uint32_t CHANGE_HEALTH = 0x2;
|
||||
constexpr uint32_t CHANGE_POWER = 0x10;
|
||||
constexpr uint32_t CHANGE_MAX_POWER = 0x20;
|
||||
constexpr uint32_t CHANGE_ZONE = 0x80;
|
||||
constexpr uint32_t CHANGE_PET_POWER = 0x20000;
|
||||
constexpr uint32_t CHANGE_PET_MAX_POWER = 0x40000;
|
||||
|
||||
const Game::Doc::Field kConnectionPayload[] = {
|
||||
Game::Doc::Req("unitTarget", "UnitToken"),
|
||||
Game::Doc::Req("isConnected", "bool"),
|
||||
};
|
||||
const Game::Doc::Event kConnectionDoc{
|
||||
"Unit", "Fires when a group member goes offline or comes back online.", kConnectionPayload};
|
||||
const Event::Custom::AutoReserve kConnection{"UNIT_CONNECTION", &kConnectionDoc};
|
||||
|
||||
const Game::Doc::Field kAreaPayload[] = {
|
||||
Game::Doc::Req("unitTarget", "UnitToken"),
|
||||
};
|
||||
const Game::Doc::Event kAreaDoc{"Unit", "Fires when a group member moves to a different zone.",
|
||||
kAreaPayload};
|
||||
const Event::Custom::AutoReserve kArea{"UNIT_AREA_CHANGED", &kAreaDoc};
|
||||
|
||||
using Signal_t = void(__fastcall *)(const uint64_t *guid, uint32_t changes, const uint8_t *oldRec,
|
||||
const uint8_t *newRec);
|
||||
using Diff_t = uint32_t(__fastcall *)(const uint8_t *oldRec, const uint8_t *newRec);
|
||||
using StatsLookup_t = const uint8_t *(__fastcall *)(const uint64_t *guid);
|
||||
using AddMember_t = void(__fastcall *)(uint32_t online, const uint8_t *oldRecord, uint64_t guid);
|
||||
using RebuildRaid_t = void(__fastcall *)(const uint64_t *guids, const uint32_t *flags,
|
||||
const uint8_t *online, uint32_t count);
|
||||
|
||||
void SignalConnection(uint64_t guid, bool connected) {
|
||||
const int slot = kConnection.Slot();
|
||||
if (!Event::Custom::HasListeners(slot))
|
||||
return;
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
char tokens[16][32];
|
||||
const int n = Unit::Identity::TokensForGUID(guid, tokens, 16);
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
Game::Lua::PushString(L, tokens[i]);
|
||||
Game::Lua::PushBool(L, connected);
|
||||
Event::Custom::FireValues(slot, L, top + 1, 2);
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
}
|
||||
|
||||
void SignalArea(uint64_t guid) {
|
||||
const int slot = kArea.Slot();
|
||||
if (!Event::Custom::HasListeners(slot))
|
||||
return;
|
||||
char tokens[16][32];
|
||||
const int n = Unit::Identity::TokensForGUID(guid, tokens, 16);
|
||||
for (int i = 0; i < n; ++i)
|
||||
Event::Custom::Fire(slot, "%s", tokens[i]);
|
||||
}
|
||||
|
||||
void __fastcall SignalSite(const uint64_t *guid, uint32_t changes, const uint8_t *oldRec,
|
||||
const uint8_t *newRec) {
|
||||
reinterpret_cast<Signal_t>(Offsets::FUN_PARTY_RECORD_SIGNAL)(guid, changes, oldRec, newRec);
|
||||
const uint64_t member = *guid;
|
||||
const int powerType = Game::Read<uint8_t>(newRec, Offsets::OFF_GROUP_MEMBER_STATS_POWER_TYPE);
|
||||
if (changes & CHANGE_STATUS) {
|
||||
Unit::PowerEvents::SignalPower(member, powerType);
|
||||
const uint8_t status = Game::Read<uint8_t>(newRec, Offsets::OFF_GROUP_MEMBER_STATUS_FLAGS);
|
||||
const uint8_t flips =
|
||||
status ^ Game::Read<uint8_t>(oldRec, Offsets::OFF_GROUP_MEMBER_STATUS_FLAGS);
|
||||
if (flips & Offsets::GROUP_MEMBER_STATUS_ONLINE)
|
||||
SignalConnection(member, (status & Offsets::GROUP_MEMBER_STATUS_ONLINE) != 0);
|
||||
changes &= ~(CHANGE_HEALTH | CHANGE_POWER);
|
||||
}
|
||||
if (changes & CHANGE_POWER)
|
||||
Unit::PowerEvents::SignalPower(member, powerType);
|
||||
if (changes & CHANGE_MAX_POWER)
|
||||
Unit::PowerEvents::SignalMaxPower(member, powerType);
|
||||
if (changes & CHANGE_ZONE)
|
||||
SignalArea(member);
|
||||
const uint64_t pet = Game::Read<uint64_t>(newRec, Offsets::OFF_GROUP_MEMBER_PET_GUID);
|
||||
if (pet == 0)
|
||||
return;
|
||||
const int petPowerType = Game::Read<uint8_t>(newRec, Offsets::OFF_GROUP_MEMBER_PET_POWER_TYPE);
|
||||
if (changes & CHANGE_PET_POWER)
|
||||
Unit::PowerEvents::SignalPower(pet, petPowerType);
|
||||
if (changes & CHANGE_PET_MAX_POWER)
|
||||
Unit::PowerEvents::SignalMaxPower(pet, petPowerType);
|
||||
}
|
||||
|
||||
void __fastcall AddMemberSite(uint32_t online, const uint8_t *oldRecord, uint64_t guid) {
|
||||
reinterpret_cast<AddMember_t>(Offsets::FUN_ADD_PARTY_MEMBER)(online, oldRecord, guid);
|
||||
if (oldRecord == nullptr)
|
||||
return;
|
||||
const uint8_t *record =
|
||||
reinterpret_cast<StatsLookup_t>(Offsets::FUN_GROUP_MEMBER_STATS_LOOKUP)(&guid);
|
||||
if (record == nullptr)
|
||||
return;
|
||||
const uint32_t changes =
|
||||
reinterpret_cast<Diff_t>(Offsets::FUN_PARTY_RECORD_DIFF)(oldRecord, record);
|
||||
if (changes != 0)
|
||||
SignalSite(&guid, changes, oldRecord, record);
|
||||
}
|
||||
|
||||
struct Presence {
|
||||
uint64_t guid;
|
||||
bool online;
|
||||
};
|
||||
|
||||
// The raid slots' GUIDs and online bits.
|
||||
int RaidPresence(Presence (&out)[Offsets::RAID_MAX_SLOTS]) {
|
||||
int count = Game::Read<int>(Offsets::VAR_RAID_MEMBER_COUNT);
|
||||
if (count > Offsets::RAID_MAX_SLOTS)
|
||||
count = Offsets::RAID_MAX_SLOTS;
|
||||
const auto *slots = reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_RAID_MEMBER_PTRS));
|
||||
int n = 0;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (slots[i] == nullptr)
|
||||
continue;
|
||||
const uint8_t flags = Game::Read<uint8_t>(slots[i], Offsets::OFF_RAID_SLOT_FLAGS);
|
||||
out[n++] = {Game::Read<uint64_t>(slots[i], 0),
|
||||
(flags & Offsets::RAID_SLOT_FLAG_ONLINE) != 0};
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
bool IsPartyMember(uint64_t guid) {
|
||||
const auto *party =
|
||||
reinterpret_cast<const uint64_t *>(static_cast<uintptr_t>(Offsets::VAR_PARTY_GUIDS));
|
||||
for (int i = 0; i < Offsets::PARTY_MAX_SLOTS; ++i)
|
||||
if (party[i] == guid)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void __fastcall RaidRosterSite(const uint64_t *guids, const uint32_t *flags,
|
||||
const uint8_t *online, uint32_t count) {
|
||||
Presence before[Offsets::RAID_MAX_SLOTS];
|
||||
const int was = RaidPresence(before);
|
||||
reinterpret_cast<RebuildRaid_t>(Offsets::FUN_REBUILD_RAID_ROSTER)(guids, flags, online, count);
|
||||
if (!Event::Custom::HasListeners(kConnection.Slot()))
|
||||
return;
|
||||
Presence after[Offsets::RAID_MAX_SLOTS];
|
||||
const int now = RaidPresence(after);
|
||||
for (int i = 0; i < now; ++i) {
|
||||
if (IsPartyMember(after[i].guid))
|
||||
continue;
|
||||
for (int j = 0; j < was; ++j) {
|
||||
if (before[j].guid == after[i].guid) {
|
||||
if (before[j].online != after[i].online)
|
||||
SignalConnection(after[i].guid, after[i].online);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Register() {
|
||||
const auto *site = reinterpret_cast<const void *>(&SignalSite);
|
||||
Common::RedirectCall(Offsets::CALL_READ_PARTY_MEMBER_STATS_SIGNAL,
|
||||
Offsets::FUN_PARTY_RECORD_SIGNAL, site);
|
||||
Common::RedirectCall(Offsets::CALL_COMPARE_PARTY_RECORD_SIGNAL,
|
||||
Offsets::FUN_PARTY_RECORD_SIGNAL, site);
|
||||
Common::RedirectCall(Offsets::CALL_GROUP_LIST_ADD_PARTY_MEMBER, Offsets::FUN_ADD_PARTY_MEMBER,
|
||||
reinterpret_cast<const void *>(&AddMemberSite));
|
||||
Common::RedirectCall(Offsets::CALL_GROUP_LIST_REBUILD_RAID_ROSTER,
|
||||
Offsets::FUN_REBUILD_RAID_ROSTER,
|
||||
reinterpret_cast<const void *>(&RaidRosterSite));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Group::MemberEvents
|
||||
+1
-1
@@ -42,7 +42,7 @@ static int __fastcall Script_C_Item_DoesItemExist(void *L) {
|
||||
// optimistically accept any positive itemID; callers needing the
|
||||
// "is the cache populated?" check should use `IsItemDataCachedByID`.
|
||||
//
|
||||
// This split matters for `ItemMixin:IsItemEmpty` (FrameXML ItemUtil),
|
||||
// This split matters for `ItemMixin:IsItemEmpty` (Util/Item.lua),
|
||||
// which uses `DoesItemExistByID` to gate `ContinueOnItemLoad` — the
|
||||
// whole point of `ContinueOnItemLoad` is to wait for an uncached item
|
||||
// to load, so the existence check must succeed for items that aren't
|
||||
|
||||
@@ -120,6 +120,28 @@ static int __fastcall Script_GetItemInfoInstant(void *L) {
|
||||
return 7;
|
||||
}
|
||||
|
||||
// `C_Item.GetItemIDForItemInfo(itemInfo)` — the itemID alone, from the same
|
||||
// resolver `GetItemInfoInstant` echoes: an itemID, an `"item:N"` string or an
|
||||
// item link. ItemMixin:GetItemID falls back to it when GetItemInfoInstant
|
||||
// returns nothing. Returns nothing for input that names no item.
|
||||
static const Game::Doc::Field kGetItemIDForItemInfoArgs[] = {
|
||||
Game::Doc::Req("itemInfo", "ItemInfo"),
|
||||
};
|
||||
static const Game::Doc::Field kGetItemIDForItemInfoRets[] = {
|
||||
Game::Doc::Req("itemID", "number"),
|
||||
};
|
||||
static const Game::Doc::Function kGetItemIDForItemInfo{
|
||||
"Returns the item ID of an item ID, item string or item link.", kGetItemIDForItemInfoArgs,
|
||||
kGetItemIDForItemInfoRets};
|
||||
|
||||
static int __fastcall Script_GetItemIDForItemInfo(void *L) {
|
||||
const int itemID = Item::Arg::ResolveItemID(L, 1);
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(itemID));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Pushes the icon path for `itemID` and returns 1, or returns 0 (= nil
|
||||
// to Lua) if the item isn't cached, the displayInfoID is missing, or
|
||||
// the icon record's path slot is empty. Shared by all three
|
||||
@@ -315,6 +337,8 @@ static int __fastcall Script_C_Item_GetItemInfo(void *L) {
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemInfo", &Script_C_Item_GetItemInfo);
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemInfoInstant", &Script_GetItemInfoInstant);
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemIDForItemInfo",
|
||||
&Script_GetItemIDForItemInfo, &kGetItemIDForItemInfo);
|
||||
Game::Lua::RegisterGlobalFunction("GetItemIcon", &Script_GetItemIcon);
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemIcon", &Script_C_Item_GetItemIcon);
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemIconByID", &Script_C_Item_GetItemIconByID);
|
||||
|
||||
@@ -28,6 +28,7 @@ constexpr int INVSLOT_RANGED = 18;
|
||||
|
||||
struct EnchantInfo {
|
||||
bool has;
|
||||
bool hasExpiration;
|
||||
uint32_t expirationMs;
|
||||
uint32_t charges;
|
||||
uint32_t enchantID;
|
||||
@@ -47,7 +48,7 @@ struct EnchantInfo {
|
||||
// — the same block `spell/BonusDamage.cpp` walks for enchant-granted
|
||||
// spell power.
|
||||
EnchantInfo ReadTempEnchant(const uint8_t *cgItem) {
|
||||
EnchantInfo r{false, 0, 0, 0};
|
||||
EnchantInfo r{false, false, 0, 0, 0};
|
||||
if (cgItem == nullptr)
|
||||
return r;
|
||||
const uint8_t *desc = Item::ObjectFields(cgItem);
|
||||
@@ -66,6 +67,7 @@ EnchantInfo ReadTempEnchant(const uint8_t *cgItem) {
|
||||
desc, kSlot + Offsets::DESCRIPTOR_ENCHANTMENT_CHARGES_DELTA);
|
||||
if (Game::Read<uint32_t>(
|
||||
desc, kSlot + Offsets::DESCRIPTOR_ENCHANTMENT_DURATION_DELTA) != 0) {
|
||||
r.hasExpiration = true;
|
||||
using TimeLeft_t = uint32_t(__fastcall *)(const uint8_t *item, void *edx, int slot);
|
||||
r.expirationMs = reinterpret_cast<TimeLeft_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_ITEM_ENCHANTMENT_TIME_LEFT))(
|
||||
@@ -157,11 +159,60 @@ int __fastcall Script_C_Item_GetItemTempEnchantInfo(void *L) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
// `C_PaperDollInfo.GetTemporaryEnchantmentInfo(slot)` — retail's per-slot
|
||||
// temporary enchant, the one its deprecated `GetWeaponEnchantInfo` is now
|
||||
// built from. `slot` is an equipment slot (INVSLOT_*). Returns a
|
||||
// `TemporaryItemEnchantInfo` table, or nothing when the slot has no
|
||||
// temporary enchant (or no item, or is not an equipment slot).
|
||||
// `hasExpirationTime` is the descriptor duration gate `ReadTempEnchant`
|
||||
// reads; without it `remainingTimeMs` is 0.
|
||||
const Game::Doc::Field kTemporaryItemEnchantInfoFields[] = {
|
||||
Game::Doc::Req("enchantID", "number"),
|
||||
Game::Doc::Req("remainingTimeMs", "number"),
|
||||
Game::Doc::Req("chargesRemaining", "number"),
|
||||
Game::Doc::Req("hasExpirationTime", "bool"),
|
||||
};
|
||||
const Game::Doc::Structure kTemporaryItemEnchantInfo{
|
||||
"TemporaryItemEnchantInfo", "PaperDollInfo", kTemporaryItemEnchantInfoFields};
|
||||
|
||||
const Game::Doc::Field kGetTemporaryEnchantmentInfoArgs[] = {
|
||||
Game::Doc::Req("slot", "number", "An INVSLOT_ constant."),
|
||||
};
|
||||
const Game::Doc::Field kGetTemporaryEnchantmentInfoRets[] = {
|
||||
Game::Doc::Req("enchantInfo", "TemporaryItemEnchantInfo"),
|
||||
};
|
||||
const Game::Doc::Function kGetTemporaryEnchantmentInfo{
|
||||
"Returns the temporary enchantment on an equipped item, or nothing if there is none.",
|
||||
kGetTemporaryEnchantmentInfoArgs, kGetTemporaryEnchantmentInfoRets};
|
||||
|
||||
int __fastcall Script_GetTemporaryEnchantmentInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_PaperDollInfo.GetTemporaryEnchantmentInfo(slot)");
|
||||
return 0;
|
||||
}
|
||||
const int slot = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (slot < Offsets::EQUIPMENT_SLOT_FIRST || slot > Offsets::EQUIPMENT_SLOT_LAST)
|
||||
return 0;
|
||||
|
||||
const EnchantInfo e = ReadEquippedTempEnchant(slot);
|
||||
if (!e.has)
|
||||
return 0;
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "enchantID", static_cast<double>(e.enchantID));
|
||||
Game::Lua::SetFieldNumber(L, "remainingTimeMs", static_cast<double>(e.expirationMs));
|
||||
Game::Lua::SetFieldNumber(L, "chargesRemaining", static_cast<double>(e.charges));
|
||||
Game::Lua::SetFieldBool(L, "hasExpirationTime", e.hasExpiration);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetWeaponEnchantInfo",
|
||||
&Script_C_Item_GetWeaponEnchantInfo);
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemTempEnchantInfo",
|
||||
&Script_C_Item_GetItemTempEnchantInfo);
|
||||
Game::Lua::RegisterTableFunction("C_PaperDollInfo", "GetTemporaryEnchantmentInfo",
|
||||
&Script_GetTemporaryEnchantmentInfo,
|
||||
&kGetTemporaryEnchantmentInfo);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
// 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/>.
|
||||
|
||||
// Retail's number formatters (LocalizationDocumentation.lua):
|
||||
// `BreakUpLargeNumbers`, `AbbreviateNumbers`, `AbbreviateLargeNumbers` and
|
||||
// `C_StringUtil.GetDefaultAbbreviationBreakpoints`. Retail moved them from
|
||||
// FrameXML into C (12.0.0) and that C is not available, so what follows is
|
||||
// retail's behavior as probed in an enUS retail client (2026-10-30).
|
||||
//
|
||||
// BreakUpLargeNumbers(n [, natural])
|
||||
// - A whole number, or one 1000 or more in size, loses its fraction (toward
|
||||
// zero) and gets LARGE_NUMBER_SEPERATOR every three digits while the
|
||||
// breakUpLargeNumbers cvar is on: 1234567.891 -> "1,234,567",
|
||||
// 1234567890 -> "1,234,567,890", -1234.5 -> "-1,234"; cvar off ->
|
||||
// "1234567".
|
||||
// - Any other number keeps one decimal, its tenths rounded half away from
|
||||
// zero, ungrouped: 999.96 -> "1000.0", 0.25 -> "0.3", -5.55 -> "-5.6",
|
||||
// 5.04 -> "5.0", 123.456 -> "123.5".
|
||||
// - natural: a value of a million or more becomes the largest number word
|
||||
// that fits, with one decimal below 100, truncated, and ".0" dropped:
|
||||
// 1290000 -> "1.2 million", 12345678 -> "12.3 million", 99990000 ->
|
||||
// "99.9 million", 123456789 -> "123 million", 2000000 -> "2 million",
|
||||
// 1.5e13 -> "15 trillion". Anything smaller, and every negative, is the
|
||||
// whole number grouped: 0.5 -> "0", 999999 -> "999,999", -1234567 ->
|
||||
// "-1,234,567".
|
||||
// The Asian locales' words are 10^4-based (SECOND_NUMBER is 万), so their
|
||||
// scales are 1e4 / 1e8 / 1e12. That is read off the strings, not probed.
|
||||
// AbbreviateNumbers(n [, options])
|
||||
// - The first breakpoint n reaches gives floor(n / significandDivisor) /
|
||||
// fractionDivisor, written as Lua writes a number, then the abbreviation:
|
||||
// 1234 -> "1.2K", 12345 -> "12K", 1999999 -> "1.9M". Below every
|
||||
// breakpoint, so for every negative, n is written as is: 999.9 ->
|
||||
// "999.9", -12345 -> "-12345", 0 -> "0".
|
||||
// - The abbreviation is the global string it names unless
|
||||
// abbreviationIsGlobal is false (12.3h from {100, "h", 10, 10}); a name
|
||||
// with no global adds nothing. Retail's defaults name THIRD_ and
|
||||
// FOURTH_NUMBER_CAP_NO_SPACE, which its clients do not define, so
|
||||
// 1.5e10 -> "15" and 1.5e12 -> "1.5".
|
||||
// - options.breakpointData replaces the defaults. options.locale picks the
|
||||
// defaults (the Asian table for koKR, zhCN and zhTW) and falls back to the
|
||||
// client's locale: 12345 with locale "zhCN" -> "1.2K".
|
||||
// AbbreviateLargeNumbers(n [, options])
|
||||
// - The same walk over its own defaults, FrameXML's old thresholds
|
||||
// (123456 -> "123 K", 12345678 -> "12345 K", 123456789 -> "123 M",
|
||||
// 1234567890 -> "1234 M"), and BreakUpLargeNumbers(n) below them: 1234.5
|
||||
// -> "1,234", -12345 -> "-12,345".
|
||||
// - Outside the Asian locales the value is rounded to a whole number: 123456
|
||||
// over {100, "h", 10, 10} -> "1235h". The Asian default keeps its
|
||||
// decimals: 123456789 with locale "zhCN" -> "1.23 M".
|
||||
// options.config (CreateAbbreviateConfig's cached object) is not backported.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cvar/Factory.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
|
||||
namespace Localization::Numbers {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
struct Breakpoint {
|
||||
double breakpoint;
|
||||
const char *abbreviation;
|
||||
double significandDivisor;
|
||||
double fractionDivisor;
|
||||
};
|
||||
|
||||
// C_StringUtil.GetDefaultAbbreviationBreakpoints(), as retail returns it.
|
||||
constexpr Breakpoint kNumberBreakpoints[] = {
|
||||
{1e13, "FOURTH_NUMBER_CAP_NO_SPACE", 1e12, 1},
|
||||
{1e12, "FOURTH_NUMBER_CAP_NO_SPACE", 1e11, 10},
|
||||
{1e10, "THIRD_NUMBER_CAP_NO_SPACE", 1e9, 1},
|
||||
{1e9, "THIRD_NUMBER_CAP_NO_SPACE", 1e8, 10},
|
||||
{1e7, "SECOND_NUMBER_CAP_NO_SPACE", 1e6, 1},
|
||||
{1e6, "SECOND_NUMBER_CAP_NO_SPACE", 1e5, 10},
|
||||
{1e4, "FIRST_NUMBER_CAP_NO_SPACE", 1e3, 1},
|
||||
{1e3, "FIRST_NUMBER_CAP_NO_SPACE", 1e2, 10},
|
||||
};
|
||||
// C_StringUtil.GetDefaultAbbreviationBreakpoints("zhCN").
|
||||
constexpr Breakpoint kNumberBreakpointsAsian[] = {
|
||||
{1e9, "SECOND_NUMBER_CAP_NO_SPACE", 1e8, 1},
|
||||
{1e8, "SECOND_NUMBER_CAP_NO_SPACE", 1e7, 10},
|
||||
{1e5, "FIRST_NUMBER_CAP_NO_SPACE", 1e4, 1},
|
||||
{1e4, "FIRST_NUMBER_CAP_NO_SPACE", 1e3, 10},
|
||||
};
|
||||
// AbbreviateLargeNumbers' defaults: FrameXML's digit-count thresholds
|
||||
// (more than 8 digits -> millions, more than 5 -> thousands).
|
||||
constexpr Breakpoint kLargeBreakpoints[] = {
|
||||
{1e8, "SECOND_NUMBER_CAP", 1e6, 1},
|
||||
{1e5, "FIRST_NUMBER_CAP", 1e3, 1},
|
||||
};
|
||||
// FrameXML's LocalizeNumberAbbreviation_Asian thresholds: 11+ digits ->
|
||||
// 亿, 9+ -> 亿 with two decimals, 7+ -> 万.
|
||||
constexpr Breakpoint kLargeBreakpointsAsian[] = {
|
||||
{1e10, "SECOND_NUMBER_CAP", 1e8, 1},
|
||||
{1e8, "SECOND_NUMBER_CAP", 1e6, 100},
|
||||
{1e6, "FIRST_NUMBER_CAP", 1e4, 1},
|
||||
};
|
||||
|
||||
struct Scale {
|
||||
double value;
|
||||
const char *word;
|
||||
};
|
||||
constexpr Scale kScales[] = {
|
||||
{1e12, "FOURTH_NUMBER"},
|
||||
{1e9, "THIRD_NUMBER"},
|
||||
{1e6, "SECOND_NUMBER"},
|
||||
};
|
||||
constexpr Scale kScalesAsian[] = {
|
||||
{1e12, "FOURTH_NUMBER"},
|
||||
{1e8, "THIRD_NUMBER"},
|
||||
{1e4, "SECOND_NUMBER"},
|
||||
};
|
||||
|
||||
CVar::Factory::Handle g_breakUpLargeNumbers = nullptr;
|
||||
|
||||
// VAR_LOCALE_INDEX order: enUS, koKR, frFR, deDE, zhCN, zhTW, esES, esMX, ruRU.
|
||||
bool ClientIsAsian() {
|
||||
const int locale = Game::Read<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
return locale == 1 || locale == 4 || locale == 5;
|
||||
}
|
||||
|
||||
bool IsAsianLocale(const char *locale) {
|
||||
return std::strcmp(locale, "koKR") == 0 || std::strcmp(locale, "zhCN") == 0 ||
|
||||
std::strcmp(locale, "zhTW") == 0;
|
||||
}
|
||||
|
||||
// `_G[name]` when it is a string, else "".
|
||||
std::string GlobalString(void *L, const char *name) {
|
||||
Lua::PushString(L, name);
|
||||
Lua::GetTable(L, Lua::GLOBALS_INDEX);
|
||||
std::string s;
|
||||
if (Lua::Type(L, -1) == Lua::TYPE_STRING)
|
||||
s = Lua::ToString(L, -1);
|
||||
Lua::SetTop(L, -2);
|
||||
return s;
|
||||
}
|
||||
|
||||
// A number as Lua's tostring writes it.
|
||||
std::string LuaNumber(double v) {
|
||||
char buf[64];
|
||||
std::snprintf(buf, sizeof buf, "%.14g", v);
|
||||
return buf;
|
||||
}
|
||||
|
||||
std::string Whole(double w) {
|
||||
char buf[400];
|
||||
std::snprintf(buf, sizeof buf, "%.0f", w);
|
||||
return buf;
|
||||
}
|
||||
|
||||
// The whole number `w` (>= 0), grouped while the cvar is on.
|
||||
std::string Grouped(void *L, double w) {
|
||||
const std::string digits = Whole(w);
|
||||
if (g_breakUpLargeNumbers != nullptr && CVar::Factory::GetInt(g_breakUpLargeNumbers, 1) == 0)
|
||||
return digits;
|
||||
const std::string sep = GlobalString(L, "LARGE_NUMBER_SEPERATOR");
|
||||
std::string out;
|
||||
const size_t n = digits.size();
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
if (i > 0 && (n - i) % 3 == 0)
|
||||
out += sep;
|
||||
out += digits[i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// `v` truncated toward zero and grouped.
|
||||
std::string SignedWhole(void *L, double v) {
|
||||
const double w = std::floor(std::fabs(v));
|
||||
return (v < 0 && w != 0 ? "-" : "") + Grouped(L, w);
|
||||
}
|
||||
|
||||
std::string BreakUp(void *L, double v, bool natural) {
|
||||
if (natural) {
|
||||
const bool asian = ClientIsAsian();
|
||||
for (const Scale &s : asian ? kScalesAsian : kScales) {
|
||||
if (v < s.value)
|
||||
continue;
|
||||
const double tenths = std::floor(v / (s.value / 10));
|
||||
const double whole = std::floor(tenths / 10);
|
||||
std::string num = Whole(whole);
|
||||
const double digit = tenths - whole * 10;
|
||||
if (tenths < 1000 && digit != 0)
|
||||
num += GlobalString(L, "DECIMAL_SEPERATOR") + Whole(digit);
|
||||
return num + " " + GlobalString(L, s.word);
|
||||
}
|
||||
return SignedWhole(L, v);
|
||||
}
|
||||
const double a = std::fabs(v);
|
||||
if (a >= 1000 || a == std::floor(a))
|
||||
return SignedWhole(L, v);
|
||||
const double tenths = std::floor(a * 10 + 0.5);
|
||||
const double whole = std::floor(tenths / 10);
|
||||
return (v < 0 ? "-" : "") + Whole(whole) + GlobalString(L, "DECIMAL_SEPERATOR") +
|
||||
Whole(tenths - whole * 10);
|
||||
}
|
||||
|
||||
// One breakpoint's result for `v`, which reached it.
|
||||
std::string Abbreviate(double v, double significandDivisor, double fractionDivisor,
|
||||
const std::string &label, bool roundWhole) {
|
||||
double q = std::floor(v / significandDivisor) / fractionDivisor;
|
||||
if (roundWhole)
|
||||
q = std::floor(q + 0.5);
|
||||
return LuaNumber(q) + label;
|
||||
}
|
||||
|
||||
// The options table's fields this module reads.
|
||||
struct Options {
|
||||
bool asian;
|
||||
int breakpointData; // stack index of options.breakpointData, 0 when absent
|
||||
};
|
||||
|
||||
// Reads the options at stack index 2; may push options.breakpointData.
|
||||
Options ReadOptions(void *L) {
|
||||
Options o{ClientIsAsian(), 0};
|
||||
if (Lua::Type(L, 2) != Lua::TYPE_TABLE)
|
||||
return o;
|
||||
Lua::PushString(L, "locale");
|
||||
Lua::GetTable(L, 2);
|
||||
if (Lua::Type(L, -1) == Lua::TYPE_STRING)
|
||||
o.asian = IsAsianLocale(Lua::ToString(L, -1));
|
||||
Lua::SetTop(L, -2);
|
||||
Lua::PushString(L, "breakpointData");
|
||||
Lua::GetTable(L, 2);
|
||||
if (Lua::Type(L, -1) == Lua::TYPE_TABLE)
|
||||
o.breakpointData = Lua::GetTop(L);
|
||||
else
|
||||
Lua::SetTop(L, -2);
|
||||
return o;
|
||||
}
|
||||
|
||||
double FieldNumber(void *L, int table, const char *key, const char *fn) {
|
||||
Lua::PushString(L, key);
|
||||
Lua::GetTable(L, table);
|
||||
if (!Lua::IsNumber(L, -1))
|
||||
Lua::Error(L, "%s: breakpointData entry needs a number '%s'", fn, key);
|
||||
const double v = Lua::ToNumber(L, -1);
|
||||
Lua::SetTop(L, -2);
|
||||
return v;
|
||||
}
|
||||
|
||||
// Walks options.breakpointData; true with `out` set at the first entry `v`
|
||||
// reaches.
|
||||
bool WalkData(void *L, int data, double v, bool roundWhole, const char *fn, std::string &out) {
|
||||
for (int i = 1;; ++i) {
|
||||
Lua::RawGetI(L, data, i);
|
||||
if (Lua::Type(L, -1) == Lua::TYPE_NIL) {
|
||||
Lua::SetTop(L, -2);
|
||||
return false;
|
||||
}
|
||||
if (Lua::Type(L, -1) != Lua::TYPE_TABLE)
|
||||
Lua::Error(L, "%s: breakpointData entry %d is not a table", fn, i);
|
||||
const int entry = Lua::GetTop(L);
|
||||
const double breakpoint = FieldNumber(L, entry, "breakpoint", fn);
|
||||
if (v >= breakpoint) {
|
||||
const double significandDivisor = FieldNumber(L, entry, "significandDivisor", fn);
|
||||
const double fractionDivisor = FieldNumber(L, entry, "fractionDivisor", fn);
|
||||
Lua::PushString(L, "abbreviation");
|
||||
Lua::GetTable(L, entry);
|
||||
if (Lua::Type(L, -1) != Lua::TYPE_STRING)
|
||||
Lua::Error(L, "%s: breakpointData entry needs a string 'abbreviation'", fn);
|
||||
const std::string abbreviation = Lua::ToString(L, -1);
|
||||
Lua::PushString(L, "abbreviationIsGlobal");
|
||||
Lua::GetTable(L, entry);
|
||||
const bool isGlobal = Lua::Type(L, -1) == Lua::TYPE_NIL || Lua::ToBoolean(L, -1);
|
||||
out = Abbreviate(v, significandDivisor, fractionDivisor,
|
||||
isGlobal ? GlobalString(L, abbreviation.c_str()) : abbreviation,
|
||||
roundWhole);
|
||||
Lua::SetTop(L, entry - 1);
|
||||
return true;
|
||||
}
|
||||
Lua::SetTop(L, entry - 1);
|
||||
}
|
||||
}
|
||||
|
||||
template <size_t N>
|
||||
bool WalkDefaults(void *L, const Breakpoint (&data)[N], double v, bool roundWhole,
|
||||
std::string &out) {
|
||||
for (const Breakpoint &b : data) {
|
||||
if (v >= b.breakpoint) {
|
||||
out = Abbreviate(v, b.significandDivisor, b.fractionDivisor,
|
||||
GlobalString(L, b.abbreviation), roundWhole);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
double CheckNumber(void *L, const char *usage) {
|
||||
if (!Lua::IsNumber(L, 1))
|
||||
Lua::Error(L, "Usage: %s", usage);
|
||||
return Lua::ToNumber(L, 1);
|
||||
}
|
||||
|
||||
int __fastcall Script_BreakUpLargeNumbers(void *L) {
|
||||
const double v = CheckNumber(L, "BreakUpLargeNumbers(largeNumber [, natural])");
|
||||
const std::string s = BreakUp(L, v, Lua::ToBoolean(L, 2) != 0);
|
||||
Lua::PushString(L, s.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_AbbreviateNumbers(void *L) {
|
||||
constexpr const char *kFn = "AbbreviateNumbers";
|
||||
const double v = CheckNumber(L, "AbbreviateNumbers(number [, options])");
|
||||
const Options o = ReadOptions(L);
|
||||
std::string s;
|
||||
const bool hit =
|
||||
o.breakpointData != 0
|
||||
? WalkData(L, o.breakpointData, v, false, kFn, s)
|
||||
: (o.asian ? WalkDefaults(L, kNumberBreakpointsAsian, v, false, s)
|
||||
: WalkDefaults(L, kNumberBreakpoints, v, false, s));
|
||||
if (!hit)
|
||||
s = LuaNumber(v);
|
||||
Lua::PushString(L, s.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_AbbreviateLargeNumbers(void *L) {
|
||||
constexpr const char *kFn = "AbbreviateLargeNumbers";
|
||||
const double v = CheckNumber(L, "AbbreviateLargeNumbers(number [, options])");
|
||||
const Options o = ReadOptions(L);
|
||||
const bool roundWhole = !o.asian;
|
||||
std::string s;
|
||||
const bool hit =
|
||||
o.breakpointData != 0
|
||||
? WalkData(L, o.breakpointData, v, roundWhole, kFn, s)
|
||||
: (o.asian ? WalkDefaults(L, kLargeBreakpointsAsian, v, roundWhole, s)
|
||||
: WalkDefaults(L, kLargeBreakpoints, v, roundWhole, s));
|
||||
if (!hit)
|
||||
s = BreakUp(L, v, false);
|
||||
Lua::PushString(L, s.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetDefaultAbbreviationBreakpoints(void *L) {
|
||||
const bool asian = Lua::Type(L, 1) == Lua::TYPE_STRING ? IsAsianLocale(Lua::ToString(L, 1))
|
||||
: ClientIsAsian();
|
||||
const Breakpoint *data = asian ? kNumberBreakpointsAsian : kNumberBreakpoints;
|
||||
const size_t count = asian ? std::size(kNumberBreakpointsAsian) : std::size(kNumberBreakpoints);
|
||||
Lua::NewTable(L);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldNumber(L, "breakpoint", data[i].breakpoint);
|
||||
Lua::SetFieldString(L, "abbreviation", data[i].abbreviation);
|
||||
Lua::SetFieldNumber(L, "significandDivisor", data[i].significandDivisor);
|
||||
Lua::SetFieldNumber(L, "fractionDivisor", data[i].fractionDivisor);
|
||||
Lua::SetFieldBool(L, "abbreviationIsGlobal", true);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kBreakpointFields[] = {
|
||||
Game::Doc::Req("breakpoint", "number", "The smallest value this entry applies to."),
|
||||
Game::Doc::Req("abbreviation", "string",
|
||||
"The name of the global string to add, or the text itself."),
|
||||
Game::Doc::Req("significandDivisor", "number",
|
||||
"The value is divided by this and rounded down."),
|
||||
Game::Doc::Req("fractionDivisor", "number",
|
||||
"The rounded result is divided by this, which leaves its decimals."),
|
||||
Game::Doc::Opt("abbreviationIsGlobal", "bool", "true",
|
||||
"False to add the abbreviation text as it is."),
|
||||
};
|
||||
const Game::Doc::Structure kBreakpointStructure{"NumberAbbreviationBreakpoint", "Localization",
|
||||
kBreakpointFields};
|
||||
|
||||
const Game::Doc::Field kOptionsFields[] = {
|
||||
Game::Doc::Opt("breakpointData", "table", nullptr,
|
||||
"NumberAbbreviationBreakpoint entries, largest first. Replaces the defaults."),
|
||||
Game::Doc::Opt("locale", "string", nullptr,
|
||||
"The locale whose defaults to use. koKR, zhCN and zhTW use the Asian ones."),
|
||||
};
|
||||
const Game::Doc::Structure kOptionsStructure{"NumberAbbrevOptions", "Localization",
|
||||
kOptionsFields};
|
||||
|
||||
const Game::Doc::Field kBreakUpArgs[] = {
|
||||
Game::Doc::Req("largeNumber", "number"),
|
||||
Game::Doc::Opt("natural", "bool", "false",
|
||||
"True to write a million or more in words, such as \"1.2 million\"."),
|
||||
};
|
||||
const Game::Doc::Field kResultRets[] = {
|
||||
Game::Doc::Req("result", "string"),
|
||||
};
|
||||
const Game::Doc::Function kBreakUpLargeNumbers{
|
||||
"Formats a number with separators between groups of three digits.", kBreakUpArgs,
|
||||
kResultRets, "Localization"};
|
||||
|
||||
const Game::Doc::Field kAbbreviateArgs[] = {
|
||||
Game::Doc::Req("number", "number"),
|
||||
Game::Doc::Opt("options", "NumberAbbrevOptions"),
|
||||
};
|
||||
const Game::Doc::Function kAbbreviateNumbers{
|
||||
"Shortens a number with an abbreviation, such as \"1.2K\".", kAbbreviateArgs, kResultRets,
|
||||
"Localization"};
|
||||
const Game::Doc::Function kAbbreviateLargeNumbers{
|
||||
"Shortens a large number to whole thousands or millions, such as \"123 K\".",
|
||||
kAbbreviateArgs, kResultRets, "Localization"};
|
||||
|
||||
const Game::Doc::Field kDefaultsArgs[] = {
|
||||
Game::Doc::Opt("locale", "string", nullptr, "The locale; the client's when not given."),
|
||||
};
|
||||
const Game::Doc::Field kDefaultsRets[] = {
|
||||
Game::Doc::Req("breakpoints", "table", "NumberAbbreviationBreakpoint entries, largest first."),
|
||||
};
|
||||
const Game::Doc::Function kGetDefaultAbbreviationBreakpoints{
|
||||
"Returns the breakpoints AbbreviateNumbers uses by default.", kDefaultsArgs, kDefaultsRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
g_breakUpLargeNumbers =
|
||||
CVar::Factory::Register("breakUpLargeNumbers", "Toggles using commas in large numbers",
|
||||
Offsets::CVAR_CATEGORY_GAME, "1");
|
||||
Lua::RegisterGlobalFunction("BreakUpLargeNumbers", &Script_BreakUpLargeNumbers,
|
||||
&kBreakUpLargeNumbers);
|
||||
Lua::RegisterGlobalFunction("AbbreviateNumbers", &Script_AbbreviateNumbers,
|
||||
&kAbbreviateNumbers);
|
||||
Lua::RegisterGlobalFunction("AbbreviateLargeNumbers", &Script_AbbreviateLargeNumbers,
|
||||
&kAbbreviateLargeNumbers);
|
||||
Lua::RegisterTableFunction("C_StringUtil", "GetDefaultAbbreviationBreakpoints",
|
||||
&Script_GetDefaultAbbreviationBreakpoints,
|
||||
&kGetDefaultAbbreviationBreakpoints);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Localization::Numbers
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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/>.
|
||||
|
||||
// `C_PaperDollInfo.GetInventorySlotInfoForInvSlot(invSlot)` — the stock
|
||||
// `GetInventorySlotInfo(slotName)` looked up the other way: from a slot
|
||||
// number to its paper-doll row. It walks the same PaperDollItemFrame.dbc rows
|
||||
// the engine's own function walks (Script_GetInventorySlotInfo, 0x004C81B0),
|
||||
// in file order, takes the first row with that SlotNumber, and returns what
|
||||
// the engine returns for it plus the row's name:
|
||||
//
|
||||
// invSlot the SlotNumber
|
||||
// slotTexture the SlotIcon path as stored (this client has no file IDs)
|
||||
// checkRelic true for the ranged slot, the engine's own relic test
|
||||
// slotName the ItemSlotName ("HeadSlot", "MainHandSlot", ...)
|
||||
//
|
||||
// A slot number with no row returns nothing.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace PaperDoll::SlotInfo {
|
||||
|
||||
namespace {
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("invSlotValue", "number", "An inventory slot number, not a luaIndex."),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("invSlot", "number"),
|
||||
Game::Doc::Req("slotTexture", "string", "The empty-slot texture path."),
|
||||
Game::Doc::Req("checkRelic", "bool"),
|
||||
Game::Doc::Req("slotName", "cstring"),
|
||||
};
|
||||
const Game::Doc::Function kGetInventorySlotInfoForInvSlot{
|
||||
"Returns the paper doll slot for an inventory slot number, or nothing if there is none.",
|
||||
kArgs, kRets};
|
||||
|
||||
int __fastcall Script_GetInventorySlotInfoForInvSlot(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_PaperDollInfo.GetInventorySlotInfoForInvSlot(invSlotValue)");
|
||||
return 0;
|
||||
}
|
||||
const int invSlot = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
|
||||
const auto *records = Game::Read<const uint8_t *>(Offsets::VAR_PAPERDOLLITEMFRAME_RECORDS);
|
||||
const int count = Game::Read<int>(Offsets::VAR_PAPERDOLLITEMFRAME_COUNT);
|
||||
if (records == nullptr)
|
||||
return 0;
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const uint8_t *row = records + i * Offsets::OFF_PAPERDOLLITEMFRAME_RECORD_STRIDE;
|
||||
const int slotNumber = Game::Read<int>(row, Offsets::OFF_PAPERDOLLITEMFRAME_SLOT_NUMBER);
|
||||
if (slotNumber != invSlot)
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slotNumber));
|
||||
Game::Lua::PushString(L, Game::Read<const char *>(row, Offsets::OFF_PAPERDOLLITEMFRAME_SLOT_ICON));
|
||||
Game::Lua::PushBool(L, slotNumber == Offsets::PAPERDOLLITEMFRAME_RELIC_SLOT_NUMBER);
|
||||
Game::Lua::PushString(L, Game::Read<const char *>(row, Offsets::OFF_PAPERDOLLITEMFRAME_SLOT_NAME));
|
||||
return 4;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_PaperDollInfo", "GetInventorySlotInfoForInvSlot",
|
||||
&Script_GetInventorySlotInfoForInvSlot,
|
||||
&kGetInventorySlotInfoForInvSlot);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace PaperDoll::SlotInfo
|
||||
@@ -59,6 +59,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "combat/Swing.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "event/Custom.h"
|
||||
#include "object/Resolve.h"
|
||||
@@ -176,6 +177,7 @@ int __fastcall SlotChanged_h(uint32_t fieldOffset, uint32_t size,
|
||||
// that changes two weapon slots yields one event.
|
||||
if (IsWeaponSlot(slot0))
|
||||
g_weaponDirty = true;
|
||||
Combat::Swing::OnWeaponSlotChanged(slot0);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// 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/>.
|
||||
|
||||
// `PLAYER_MOUNT_DISPLAY_CHANGED` — the player mounted, dismounted or changed
|
||||
// mount (retail's no-payload event; its action bars refresh usability on it).
|
||||
//
|
||||
// 1.12 reacts to a unit's new UNIT_FIELD_MOUNTDISPLAYID in
|
||||
// CGUnit_C::OnMountDisplayChanged (FUN_UNIT_MOUNT_DISPLAY_CHANGED), which the
|
||||
// field's type watcher calls with the old value from one E8 call. That call
|
||||
// points here instead: the function runs the handler, then fires the event
|
||||
// when the unit is the player. The watcher calls only on a real change.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Player::MountDisplay {
|
||||
|
||||
namespace {
|
||||
|
||||
const Game::Doc::Event kDoc{"Unit", "Fires when the player mounts, dismounts or changes mount.",
|
||||
{}};
|
||||
const Event::Custom::AutoReserve kChanged{"PLAYER_MOUNT_DISPLAY_CHANGED", &kDoc};
|
||||
|
||||
using MountDisplayChanged_t = void(__thiscall *)(void *unit, uint32_t oldMountDisplay);
|
||||
|
||||
void __fastcall MountDisplayChangedSite(void *unit, void *, uint32_t oldMountDisplay) {
|
||||
reinterpret_cast<MountDisplayChanged_t>(Offsets::FUN_UNIT_MOUNT_DISPLAY_CHANGED)(
|
||||
unit, oldMountDisplay);
|
||||
if (unit == Unit::Identity::PlayerObject())
|
||||
Event::Custom::Fire(kChanged.Slot(), "");
|
||||
}
|
||||
|
||||
void Register() {
|
||||
Common::RedirectCall(Offsets::CALL_MOUNT_DISPLAY_FIELD_CHANGED,
|
||||
Offsets::FUN_UNIT_MOUNT_DISPLAY_CHANGED,
|
||||
reinterpret_cast<const void *>(&MountDisplayChangedSite));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Player::MountDisplay
|
||||
@@ -125,6 +125,45 @@ int ExtraCount(void *L, int extras) {
|
||||
return n;
|
||||
}
|
||||
|
||||
// Moves the records of slots 0..span-1 so that slot i holds what slot
|
||||
// (i + by) % span held, with their extra-argument tables and the scroll
|
||||
// record. Whole records move, so every text keeps one owner, a free slot's
|
||||
// too (Clear and the writer leave a free slot's old text for the writer to
|
||||
// free when it reuses the slot). The lines still show the records they were
|
||||
// laid out from; the caller lays them out again. Self at stack slot 1.
|
||||
void RotateRecords(void *L, void *frame, int span, int by) {
|
||||
using namespace Game::Lua;
|
||||
by %= span;
|
||||
if (span <= 1 || by == 0)
|
||||
return;
|
||||
constexpr size_t kStride = Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
|
||||
uint8_t *records = Game::Read<uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES);
|
||||
const std::vector<uint8_t> was(records, records + span * kStride);
|
||||
for (int i = 0; i < span; ++i)
|
||||
std::memcpy(records + i * kStride, was.data() + ((i + by) % span) * kStride, kStride);
|
||||
int &scroll = Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_SCROLL_RECORD);
|
||||
if (scroll >= 0 && scroll < span)
|
||||
scroll = (scroll - by + span) % span;
|
||||
|
||||
PushRecords(L, false);
|
||||
const int table = GetTop(L);
|
||||
if (Type(L, table) == TYPE_TABLE) {
|
||||
NewTable(L);
|
||||
const int moved = GetTop(L);
|
||||
for (int i = 0; i < span; ++i) {
|
||||
PushNumber(L, i);
|
||||
RawGetI(L, table, (i + by) % span);
|
||||
RawSet(L, moved);
|
||||
}
|
||||
for (int i = 0; i < span; ++i) {
|
||||
PushNumber(L, i);
|
||||
RawGetI(L, moved, i);
|
||||
RawSet(L, table);
|
||||
}
|
||||
}
|
||||
SetTop(L, table - 1);
|
||||
}
|
||||
|
||||
// Stores stack slots kFirstExtra..top as the extra arguments of `record`.
|
||||
void StoreExtras(void *L, int record, int top) {
|
||||
using namespace Game::Lua;
|
||||
@@ -190,15 +229,18 @@ uint32_t MessageColor(void *L, void *frame) {
|
||||
// `ScrollingMessageFrame:BackFillMessage(text, r, g, b, ...)`, retail's: the
|
||||
// message becomes the OLDEST. Retail's mixin pushes the entry at the back of
|
||||
// its history buffer and drops it when the buffer is full ("Won't overwrite
|
||||
// front"); 4.3.4's AddMessage(..., addToTop) writes the same slot but, full,
|
||||
// overwrites the oldest. The slot is 4.3.4's: once the count has grown, the
|
||||
// free one before the oldest, `(pos - count + 1) mod max` (pos 0 for an empty
|
||||
// ring), and the record is written as the ring writer 0x00788150 writes it —
|
||||
// the text a Storm copy (the ring frees its texts with SMemFree), AddMessage's
|
||||
// colour, shown, the frame's two times, id 0. Nothing already in the ring
|
||||
// moves, so a scrolled-up frame keeps its view; at the bottom the lines are
|
||||
// laid out again from the newest, as the writer does, which shows the new
|
||||
// line when there is room above the others.
|
||||
// front"). The ring is then not full, so the engine keeps it in records
|
||||
// 0..count-1 (Offsets.h, OFF_SCROLLINGMESSAGEFRAME_MESSAGES), and the new
|
||||
// oldest goes in record 0: every record moves up one, the free record at the
|
||||
// count coming round to 0. (4.3.4's AddMessage(..., addToTop) writes the
|
||||
// record before the oldest instead, which wraps to the end of the array and
|
||||
// leaves 4.3.4's own count-wrapped scroll math wrong.) The record is written
|
||||
// as the ring writer 0x00788150 writes it: the text a Storm copy (the ring
|
||||
// frees its texts with SMemFree), AddMessage's colour, shown, the frame's two
|
||||
// times, id 0. The lines are laid out again: at the bottom from the newest,
|
||||
// as the writer does, which shows the new line when there is room above the
|
||||
// others; scrolled up from the scroll record, which moved with its message,
|
||||
// so the view stays.
|
||||
int __fastcall Script_BackFillMessage(void *L) {
|
||||
using namespace Game::Lua;
|
||||
using StormAlloc_t = void *(__stdcall *)(size_t size, const char *file, int line, int flags);
|
||||
@@ -225,15 +267,11 @@ int __fastcall Script_BackFillMessage(void *L) {
|
||||
return 0;
|
||||
std::memcpy(copy, text, bytes);
|
||||
|
||||
RotateRecords(L, frame, count + 1, count);
|
||||
++count;
|
||||
int &pos = Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS);
|
||||
if (pos == -1)
|
||||
pos = 0;
|
||||
int slot = pos - count + 1;
|
||||
if (slot < 0)
|
||||
slot += max;
|
||||
uint8_t *record = Game::Read<uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES) +
|
||||
slot * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
|
||||
pos = count - 1;
|
||||
uint8_t *record = Game::Read<uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES);
|
||||
char *&stored = *reinterpret_cast<char **>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT);
|
||||
if (stored != nullptr)
|
||||
reinterpret_cast<StormFree_t>(Offsets::FUN_STORM_SMEM_FREE)(stored, kStormFile, __LINE__, 0);
|
||||
@@ -245,11 +283,13 @@ int __fastcall Script_BackFillMessage(void *L) {
|
||||
*reinterpret_cast<uint32_t *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_FADE_TIME) =
|
||||
Game::Read<uint32_t>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_FADE_TIME);
|
||||
*reinterpret_cast<int *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_ID) = 0;
|
||||
StoreExtras(L, slot, top);
|
||||
StoreExtras(L, 0, top);
|
||||
|
||||
if (Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_AT_BOTTOM) != 0)
|
||||
reinterpret_cast<Layout_t>(Offsets::FUN_SMF_DISPLAY_REFRESH)(frame, nullptr, pos);
|
||||
// Scrolled up, the message lands without a layout.
|
||||
reinterpret_cast<Layout_t>(Offsets::FUN_SMF_DISPLAY_REFRESH)(
|
||||
frame, nullptr,
|
||||
Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_AT_BOTTOM) != 0
|
||||
? pos
|
||||
: Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_SCROLL_RECORD));
|
||||
DisplayRefreshed::MarkDirty(frame);
|
||||
return 0;
|
||||
}
|
||||
@@ -310,7 +350,11 @@ int __fastcall Script_GetMessageInfo(void *L) {
|
||||
// ring: every kept record moves forward in ring order to the first free slot
|
||||
// (freeing the text it lands on, nulling the one it left), the position moves
|
||||
// back by the removed count, and the extra-argument tables move with their
|
||||
// records. Then the engine's own layout from the record at the kept offset
|
||||
// records. The ring is turned first so that the oldest is record 0, so the
|
||||
// kept ones end in records 0..kept-1, the engine's layout of a ring that is
|
||||
// not full (Offsets.h, OFF_SCROLLINGMESSAGEFRAME_MESSAGES); 4.3.4 compacts
|
||||
// from the oldest's record, which leaves a full ring's survivors anywhere.
|
||||
// Then the engine's own layout from the record at the kept offset
|
||||
// (clamped to the oldest) and its re-arm — a layout at the bottom keeps the
|
||||
// fade state, so 4.3.4 re-arms after — or its Clear when nothing is kept.
|
||||
//
|
||||
@@ -361,17 +405,21 @@ int __fastcall Script_RemoveMessagesByPredicate(void *L) {
|
||||
if (kept == count)
|
||||
return 0;
|
||||
|
||||
// 4.3.4's RemoveLines: compact the kept records forward in ring order.
|
||||
// 4.3.4's RemoveLines: compact the kept records forward in ring order,
|
||||
// the oldest first turned to record 0.
|
||||
const int offset = reinterpret_cast<ScrollOffset_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET)(frame);
|
||||
int &pos = Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS);
|
||||
int oldest = pos - count + 1;
|
||||
if (oldest < 0)
|
||||
oldest += max;
|
||||
RotateRecords(L, frame, max, oldest);
|
||||
pos = count - 1;
|
||||
uint8_t *records = Game::Read<uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES);
|
||||
PushRecords(L, false);
|
||||
const int table = GetTop(L);
|
||||
const bool extras = Type(L, table) == TYPE_TABLE;
|
||||
int src = pos - count + 1;
|
||||
if (src < 0)
|
||||
src += max;
|
||||
int dst = src;
|
||||
int src = 0;
|
||||
int dst = 0;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (!remove[static_cast<size_t>(i)]) {
|
||||
if (src != dst) {
|
||||
|
||||
@@ -22,9 +22,17 @@
|
||||
// two directions, and GetScrollOffset is the engine's GetCurrentScroll under
|
||||
// retail's name. Retail clamps the offset to its range; the engine wraps one
|
||||
// outside it, so the setters clamp first. SetScrollOffset to the offset the
|
||||
// frame has does nothing, as on retail. ScrollByAmount always calls the
|
||||
// engine, which re-arms the shown lines' fade when the offset stays: retail's
|
||||
// ResetAllFadeTimes.
|
||||
// frame has does nothing, as on retail.
|
||||
//
|
||||
// Retail's ScrollUp and ScrollDown are ScrollByAmount(1) and (-1), so
|
||||
// ScrollByAmount steps the engine's own ScrollUp / ScrollDown, the loop its
|
||||
// PageUp runs. The engine's ScrollUp stops once the oldest message is on
|
||||
// screen, short of the range's end, where that message sits alone on the
|
||||
// bottom line under empty space; moving the offset straight there scrolls
|
||||
// chat up into that space (pfUI's chat wheel did). The clamped target still
|
||||
// bounds the steps, since a hidden frame never lays out and its ScrollUp never
|
||||
// finds the top. Every step re-arms the shown lines' fade, and an amount that
|
||||
// takes no step re-arms through ScrollToOffset: retail's ResetAllFadeTimes.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
@@ -35,10 +43,19 @@ namespace ScrollingMessageFrame::Scroll {
|
||||
namespace {
|
||||
|
||||
using ScrollToOffset_t = void(__fastcall *)(void *smf, void *edx, int offset);
|
||||
using ScrollOffset_t = int(__fastcall *)(void *smf);
|
||||
using SmfInt_t = int(__fastcall *)(void *smf);
|
||||
|
||||
int ScrollOffset(void *smf) {
|
||||
return reinterpret_cast<ScrollOffset_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET)(smf);
|
||||
return reinterpret_cast<SmfInt_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET)(smf);
|
||||
}
|
||||
|
||||
// One step of the engine's ScrollUp / ScrollDown; false at their end.
|
||||
bool ScrollUp(void *smf) {
|
||||
return reinterpret_cast<SmfInt_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_UP)(smf) != 0;
|
||||
}
|
||||
|
||||
bool ScrollDown(void *smf) {
|
||||
return reinterpret_cast<SmfInt_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_DOWN)(smf) != 0;
|
||||
}
|
||||
|
||||
int MaxScrollRange(void *smf) {
|
||||
@@ -90,7 +107,14 @@ int __fastcall Script_ScrollByAmount(void *L) {
|
||||
Game::Lua::Error(L, "Usage: %s:ScrollByAmount(amount)", Game::Lua::ObjectDisplayName(frame));
|
||||
return 0;
|
||||
}
|
||||
ScrollTo(frame, Clamped(frame, ScrollOffset(frame) + Game::Lua::ToNumber(L, 2)));
|
||||
const int offset = ScrollOffset(frame);
|
||||
int steps = Clamped(frame, offset + Game::Lua::ToNumber(L, 2)) - offset;
|
||||
if (steps == 0)
|
||||
ScrollTo(frame, offset);
|
||||
while (steps > 0 && ScrollUp(frame))
|
||||
--steps;
|
||||
while (steps < 0 && ScrollDown(frame))
|
||||
++steps;
|
||||
DisplayRefreshed::MarkDirty(frame);
|
||||
return 0;
|
||||
}
|
||||
@@ -121,8 +145,10 @@ const Game::Doc::Function kGetScrollOffset{"Returns how many lines the frame is
|
||||
{}, kOffsetRets};
|
||||
const Game::Doc::Function kGetMaxScrollRange{"Returns the largest scroll offset: one less than the message count.",
|
||||
{}, kRangeRets};
|
||||
const Game::Doc::Function kScrollByAmount{"Scrolls the frame by a number of lines and shows the faded lines again.",
|
||||
kAmountArgs, {}};
|
||||
const Game::Doc::Function kScrollByAmount{
|
||||
"Scrolls the frame by a number of lines, stopping where ScrollUp and ScrollDown stop, and shows the faded lines "
|
||||
"again.",
|
||||
kAmountArgs, {}};
|
||||
const Game::Doc::Method kDocs[] = {
|
||||
{"SetScrollOffset", &kSetScrollOffset},
|
||||
{"GetScrollOffset", &kGetScrollOffset},
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// 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/>.
|
||||
|
||||
// `C_Spell.IsSpellDataCached(spellIdentifier)` and
|
||||
// `C_Spell.RequestLoadSpellData(spellIdentifier)` + `SPELL_DATA_LOAD_RESULT`
|
||||
// — the spell half of retail's async data-load trio (item and quest are in
|
||||
// `item/Data.cpp` / `quest/Data.cpp`), which `SpellMixin` and the
|
||||
// `SpellEventListener` in the embedded addon's AsyncCallbackSystem drive.
|
||||
//
|
||||
// Retail streams spell records on demand. This client keeps all of
|
||||
// Spell.dbc resident, so a spell's data is "cached" exactly when its record
|
||||
// exists, and a load request resolves at once. Retail marks
|
||||
// SPELL_DATA_LOAD_RESULT a SynchronousEvent and AsyncCallbackSystem relies
|
||||
// on that (a cached request fires and clears the callbacks before
|
||||
// `AddCancelableCallback` returns), so the request fires the event inside
|
||||
// the call: `(spellID, true)` for a real record, `(spellID, nil)` for an ID
|
||||
// with none, so a listener's pending callbacks are cleared rather than kept
|
||||
// forever. An identifier that resolves to no spell ID fires nothing.
|
||||
|
||||
#include "Arg.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "event/Custom.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Spell::Data {
|
||||
|
||||
namespace {
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kSpellArgs[] = {
|
||||
Game::Doc::Req("spellIdentifier", "SpellIdentifier", "A spell ID, spell link, or spell name."),
|
||||
};
|
||||
const Game::Doc::Field kIsCachedRets[] = {
|
||||
Game::Doc::Req("isCached", "bool", "True when the client holds data for the spell."),
|
||||
};
|
||||
const Game::Doc::Function kIsSpellDataCached{
|
||||
"Whether the data for a spell is loaded and ready to read.", kSpellArgs, kIsCachedRets};
|
||||
const Game::Doc::Function kRequestLoadSpellData{
|
||||
"Asks for a spell's data to be loaded; SPELL_DATA_LOAD_RESULT reports when it is ready.",
|
||||
kSpellArgs, {}};
|
||||
|
||||
const Game::Doc::Field kLoadResultPayload[] = {
|
||||
Game::Doc::Req("spellID", "number"),
|
||||
Game::Doc::Req("success", "bool"),
|
||||
};
|
||||
const Game::Doc::Event kLoadResultDoc{
|
||||
"Spell", "A spell data request finished; success is false when no such spell exists.",
|
||||
kLoadResultPayload};
|
||||
const Event::Custom::AutoReserve _reserveLoadResult{"SPELL_DATA_LOAD_RESULT", &kLoadResultDoc};
|
||||
|
||||
bool HasRecord(int spellID) {
|
||||
return DBC::Record(Offsets::VAR_SPELL_RECORDS, Offsets::VAR_SPELL_RECORD_COUNT,
|
||||
static_cast<uint32_t>(spellID)) != nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static int __fastcall Script_IsSpellDataCached(void *L) {
|
||||
const int spellID = Spell::Arg::ResolveSpellID(L, 1);
|
||||
Game::Lua::PushBool(L, spellID > 0 && HasRecord(spellID));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int __fastcall Script_RequestLoadSpellData(void *L) {
|
||||
const int spellID = Spell::Arg::ResolveSpellID(L, 1);
|
||||
if (spellID > 0)
|
||||
Event::Custom::FireIdSuccess(_reserveLoadResult.Slot(), spellID, HasRecord(spellID));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "IsSpellDataCached",
|
||||
&Script_IsSpellDataCached, &kIsSpellDataCached);
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "RequestLoadSpellData",
|
||||
&Script_RequestLoadSpellData, &kRequestLoadSpellData);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Spell::Data
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Location.h"
|
||||
#include "spell/Arg.h"
|
||||
#include "spell/Lookup.h"
|
||||
|
||||
@@ -58,6 +59,22 @@ static int __fastcall Script_GetSpellDescription(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_Spell.GetSpellDescriptionForItemLocation(spell, itemLocation)` — the
|
||||
// description as an item at `itemLocation` shows it. The item tooltip's
|
||||
// spell lines (0x0052DA24) expand the spell's text with the same arguments
|
||||
// `Expand` passes (pet 0, level 0, aura 0, stack 1, unmodified 0 —
|
||||
// benilla-re ui/spelltokens), so the item contributes nothing to the text
|
||||
// and the result is GetSpellDescription's. The location is still required,
|
||||
// as the retail signature makes it.
|
||||
static int __fastcall Script_GetSpellDescriptionForItemLocation(void *L) {
|
||||
if (!Item::Location::IsLocationArg(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: local description = "
|
||||
"C_Spell.GetSpellDescriptionForItemLocation(spellIdentifier, itemLocation)");
|
||||
return 0;
|
||||
}
|
||||
return Script_GetSpellDescription(L);
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
static const Game::Doc::Field kGetSpellDescriptionArgs[] = {
|
||||
@@ -72,9 +89,20 @@ static const Game::Doc::Function kGetSpellDescription{
|
||||
"The spell's description text, with its placeholders filled in at the base rank.",
|
||||
kGetSpellDescriptionArgs, kGetSpellDescriptionRets};
|
||||
|
||||
static const Game::Doc::Field kForItemLocationArgs[] = {
|
||||
Game::Doc::Req("spell", "SpellIdentifier", "A spell ID, spell link, or spell name."),
|
||||
Game::Doc::Req("itemLocation", "ItemLocation", "The item that shows the spell."),
|
||||
};
|
||||
static const Game::Doc::Function kGetSpellDescriptionForItemLocation{
|
||||
"The spell's description text as an item's tooltip shows it.",
|
||||
kForItemLocationArgs, kGetSpellDescriptionRets};
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellDescription",
|
||||
&Script_GetSpellDescription, &kGetSpellDescription);
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellDescriptionForItemLocation",
|
||||
&Script_GetSpellDescriptionForItemLocation,
|
||||
&kGetSpellDescriptionForItemLocation);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// 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/>.
|
||||
|
||||
// A font string as tall as its text keeps all of its lines.
|
||||
//
|
||||
// A font string with a rect height cuts its text to the lines that fit and
|
||||
// ends it with "..." (CSimpleFontString::FitText, 0x771EC0). The count of
|
||||
// what fits (GxuFontGetMaxCharsWithinHeight, 0x5C21C0) adds each line's
|
||||
// height and gap to a running float sum, and stops at the first line where
|
||||
// `blockHeight + 2^-20 < sum + fontHeight`. A font string with no height of
|
||||
// its own gets its text's height (0x5C2070), and that height is a product,
|
||||
// n * fontHeight + (n - 1) * gap. Over many lines the sum drifts from the
|
||||
// product by more than 2^-20 at most UI scales. Then the last line "does not
|
||||
// fit", and FitText cuts the text: a long one to its first 4092 bytes plus
|
||||
// "...", with the rest of the box blank. A multi-line edit box shows this
|
||||
// once its text passes 4 KB (ScrollingEditBoxTemplate, chat copy boxes).
|
||||
//
|
||||
// 4.3.4 keeps the product and lets the margin grow with the lines tried:
|
||||
// `blockHeight + numLines * 2^-20` (GxuFontGetMaxCharsWithinWidthAndHeight).
|
||||
// The thunk below does the same with the engine's own 2^-20. The first line
|
||||
// keeps 1.12's margin, and each line adds a millionth of the screen height,
|
||||
// so text that really overflows is still cut. A float32 model of the chain
|
||||
// (fit after the round trip through the font string's cached height) lost
|
||||
// the last line in 1276 of 3000 multi-line fits with 1.12's margin and in
|
||||
// none with 4.3.4's.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Text::FitHeight {
|
||||
|
||||
namespace {
|
||||
|
||||
// 4.3.4's numLines: the line the fit is trying.
|
||||
int g_line = 0;
|
||||
|
||||
constexpr uintptr_t addrEpsilon = Offsets::VAR_TEXT_FIT_HEIGHT_EPSILON;
|
||||
static_assert(Offsets::TEXT_FIT_HEIGHT_FRAME_FITTED_BYTES == 8, "LineMarginThunk reads [ebp - 8]");
|
||||
|
||||
// Stands in for `fadd dword ptr [VAR_TEXT_FIT_HEIGHT_EPSILON]`, with the
|
||||
// block height in ST0. The bytes fitted so far ([ebp - 8]) are 0 only while
|
||||
// the first line is tried.
|
||||
__declspec(naked) void LineMarginThunk() {
|
||||
__asm {
|
||||
cmp dword ptr [ebp - 8], 0
|
||||
jne counted
|
||||
mov dword ptr [g_line], 0
|
||||
counted:
|
||||
inc dword ptr [g_line]
|
||||
fild dword ptr [g_line]
|
||||
mov eax, addrEpsilon
|
||||
fmul dword ptr [eax]
|
||||
faddp st(1), st(0)
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// Runs on every registration pass; a site already patched no longer matches
|
||||
// and is skipped.
|
||||
void Apply() {
|
||||
static const uint8_t kMarginAdd[6] = {0xD8, 0x05, 0xBC, 0x26, 0x80, 0x00};
|
||||
Common::ReplaceWithCall(Offsets::SITE_TEXT_FIT_HEIGHT_TOLERANCE, kMarginAdd, sizeof kMarginAdd,
|
||||
reinterpret_cast<const void *>(&LineMarginThunk));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Apply};
|
||||
const Game::GlueModuleAutoRegister _glueAutoreg{&Apply};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Text::FitHeight
|
||||
@@ -35,8 +35,9 @@
|
||||
//
|
||||
// Subscribers run at the **tail** of the render callback (after the UI strata
|
||||
// walk completes, so no frame is mid-draw and mutating regions is safe;
|
||||
// mutations take effect next frame — same latency class as WorldTick). Order
|
||||
// across subscribers is unspecified.
|
||||
// mutations take effect next frame). WorldTick's subscribers run from here
|
||||
// too, in a frame whose walk drew the world. Order across subscribers is
|
||||
// unspecified.
|
||||
//
|
||||
// Module pattern (identical to WorldTick): declare a file-scope
|
||||
// `static const Tick::FrameTick::AutoSubscribe` in your TU; the constructor
|
||||
@@ -56,8 +57,9 @@ struct AutoSubscribe {
|
||||
// update — the value `OnUpdate` receives. Latched on entry to the render
|
||||
// callback, before any `OnUpdate` runs, so every reader in the pass sees its
|
||||
// own frame's delta. Code that runs earlier in the engine tick (timers,
|
||||
// packets and their events, the world tick and `C_Timer`) runs before the
|
||||
// pass and sees the previous pass's delta. The engine keeps no persisting
|
||||
// packets and their events) runs before the pass and sees the previous pass's
|
||||
// delta; WorldTick's subscribers, `C_Timer` among them, run at the pass's tail
|
||||
// and see its own. The engine keeps no persisting
|
||||
// copy: its accumulator `DAT_00885608` is zeroed once the pass presents.
|
||||
// 0 before the first pass.
|
||||
float Elapsed();
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include "WorldTick.h"
|
||||
|
||||
#include "FrameTick.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
@@ -18,12 +19,29 @@ namespace Tick::WorldTick {
|
||||
namespace {
|
||||
|
||||
AutoSubscribe *g_head = nullptr;
|
||||
bool g_ticked = false;
|
||||
|
||||
using WorldTick_t = void(__fastcall *)(int, int, int);
|
||||
WorldTick_t WorldTick_o = nullptr;
|
||||
|
||||
// The world tick runs inside the UI draw: WorldFrame's layer-0 draw callback
|
||||
// calls it in the middle of the strata walk, after the WORLD stratum's batches
|
||||
// are rebuilt and before its levels above 0 — the nameplates — are drawn.
|
||||
// Script work done there that changes a font string in that stratum (an addon
|
||||
// restyling a plate on NAME_PLATE_CREATED) closes the font a built batch still
|
||||
// draws, and the draw then uploads a freed font's atlas pages. So only note
|
||||
// that the world ran.
|
||||
void __fastcall WorldTick_h(int a, int b, int c) {
|
||||
WorldTick_o(a, b, c);
|
||||
g_ticked = true;
|
||||
}
|
||||
|
||||
// From the UI render root's tail: the strata walk is over, so whatever the
|
||||
// subscribers change is rebuilt before it is drawn again, as with OnUpdate.
|
||||
void Drain() {
|
||||
if (!g_ticked)
|
||||
return;
|
||||
g_ticked = false;
|
||||
for (auto *node = g_head; node != nullptr; node = node->next)
|
||||
node->cb();
|
||||
}
|
||||
@@ -39,4 +57,6 @@ static const Game::HookAutoRegister _hook{
|
||||
reinterpret_cast<void *>(&WorldTick_h),
|
||||
reinterpret_cast<void **>(&WorldTick_o)};
|
||||
|
||||
static const Tick::FrameTick::AutoSubscribe _drain{&Drain};
|
||||
|
||||
} // namespace Tick::WorldTick
|
||||
|
||||
@@ -18,11 +18,13 @@
|
||||
// per-frame tick must go through this module instead of registering
|
||||
// its own `HookAutoRegister`.
|
||||
//
|
||||
// Subscribers receive a callback at the **tail** of each frame:
|
||||
// the engine's original world-tick runs first, then this module
|
||||
// walks the subscriber list. Order across subscribers is unspecified
|
||||
// (LIFO of static-init order across TUs), so subscribers must not
|
||||
// depend on each other.
|
||||
// Subscribers run once per frame in which the world ticked, at the
|
||||
// **tail** of that frame's UI draw (from `Tick::FrameTick`), never at
|
||||
// the hook itself: the world tick runs inside the UI strata walk, where
|
||||
// script work frees fonts that already-built batches still draw (see
|
||||
// WorldTick.cpp). Order across subscribers is unspecified (LIFO of
|
||||
// static-init order across TUs), so subscribers must not depend on
|
||||
// each other.
|
||||
//
|
||||
// Module pattern, matching `HookAutoRegister` / `ModuleAutoRegister`:
|
||||
// declare a file-scope `static const Tick::WorldTick::AutoSubscribe`
|
||||
|
||||
@@ -11,8 +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/>.
|
||||
|
||||
// `GameTooltip:SetHyperlink(link)` takes a full chat link, and `spell:` and
|
||||
// `quest:` links, as on 3.x clients.
|
||||
// `GameTooltip:SetHyperlink(link)` takes a full chat link, and `spell:`,
|
||||
// `quest:` and `trade:` links, as on 3.x clients.
|
||||
//
|
||||
// Vanilla's Script_GameTooltip_SetHyperlink knows two payloads, `item:` and
|
||||
// `enchant:`, matched against the start of the argument. Anything else -
|
||||
@@ -43,6 +43,10 @@
|
||||
// the tooltip empty and queues the server query the quest log would
|
||||
// make (once per quest - see Quest::Cache::Lookup's `unique`); the next
|
||||
// SetHyperlink after the answer shows it.
|
||||
// - `trade:` opens the linked profession in the profession window
|
||||
// (TradeSkill::Link::Open) and leaves the tooltip alone, as 3.3.5's
|
||||
// SetHyperlink does. That is how a click on a chat link opens it: the
|
||||
// SetItemRef of both 1.12 and 3.3.5 ends in ItemRefTooltip:SetHyperlink.
|
||||
// - Everything else - `item:`, other `enchant:` links, and the error for link types
|
||||
// vanilla has no data for (`achievement:`, `talent:`, ...) - is the
|
||||
// engine's own, run on the payload.
|
||||
@@ -56,11 +60,13 @@
|
||||
#include "spell/Lookup.h"
|
||||
#include "spell/Tooltip.h"
|
||||
#include "tooltip/Lines.h"
|
||||
#include "tradeskill/Link.h"
|
||||
#include "ui/ColorData.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace Tooltip::Hyperlink {
|
||||
|
||||
@@ -165,6 +171,10 @@ int __fastcall SetHyperlink_h(void *L) {
|
||||
ShowQuest(L, static_cast<uint32_t>(std::strtoul(payload + 6, nullptr, 10)));
|
||||
return 0;
|
||||
}
|
||||
if (HasPrefix(payload, len, "trade:")) {
|
||||
TradeSkill::Link::Open(std::string(payload + 6, len - 6).c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (payload != link) {
|
||||
// Push the payload while the full link is still on the stack (the
|
||||
|
||||
+232
-381
@@ -11,437 +11,288 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Trade-skill *list* links (`|Htrade:...|h`) — the shareable "here's my
|
||||
// profession and which recipes I know" link. Vanilla 1.12 has NO such link
|
||||
// type (the engine's SetHyperlink parser only knows `item:` and `enchant:`);
|
||||
// 2.x introduced it. We backport the whole thing: the link is built entirely
|
||||
// in C here; only the click-to-display viewer lives in the embedded addon
|
||||
// (Util/TradeSkillLink.lua), since that's UI.
|
||||
// The `trade:` link: a player's profession and the recipes they know, built
|
||||
// and read as 3.3.5 does.
|
||||
//
|
||||
// Wire format (a ClassicAPI-private shape — nothing native emits/reads it, so
|
||||
// we're free to define it; modeled on the 2.x/3.3.5 `trade:` link, with the
|
||||
// linker's name in the slot 2.x used for the GUID):
|
||||
// |cffffd000|Htrade:<spellID>:<rank>:<maxRank>:<GUID>:<bits>|h[<spell name>]|h|r
|
||||
//
|
||||
// |cffffd000|Htrade:<skillLineID>:<curRank>:<maxRank>:<linkerName>:<base64 bits>|h[Name]|h|r
|
||||
// Mirrors 3.3.5's CGTradeSkillInfo::GetTradeSkillListLink (0x005DB060) and
|
||||
// SetLink (0x005DE300):
|
||||
// - `spellID` is the profession spell the window was opened with (Effect[0]
|
||||
// SPELL_EFFECT_TRADE_SKILL). The reader maps it to a skill line through
|
||||
// the SkillLineAbility row for the linker's race and class.
|
||||
// - `rank` / `maxRank` are the linker's, as GetTradeSkillLine gives them.
|
||||
// - `GUID` is the linker's, `%I64X`. The reader takes the linker's name,
|
||||
// race and class from the name cache, and asks the server for a linker
|
||||
// it does not hold, opening the link when the answer arrives.
|
||||
// - `bits` hold one bit per row of the skill line's link rows (Abilities),
|
||||
// six per base64 character, low bit first. A bit is set when the linker
|
||||
// knows the row's spell and the row's race and class masks take the
|
||||
// linker; 3.3.5's encoder ignores the rows' exclude flags.
|
||||
//
|
||||
// - skillLineID — SkillLine.dbc row (e.g. 164 = Blacksmithing)
|
||||
// - curRank/maxRank — the linker's skill level (cosmetic; viewer title)
|
||||
// - linkerName — the linking character's name
|
||||
// - base64 bits — a bitfield over the skill line's *canonical recipe list*:
|
||||
// one bit per possible recipe, set = the linker knows it.
|
||||
// The link rows are 3.3.5's SpellTableGetSkillLineLinkInfo: the run of
|
||||
// SkillLineAbility records of the skill line. 3.3.5's file holds each
|
||||
// profession in a single run, so the run is every row of the line in file
|
||||
// order (checked against ChromieCraft's SkillLineAbility.dbc: one run per
|
||||
// linkable line). 1.12's file is sorted by row ID and scatters a line
|
||||
// (Blacksmithing over 73 runs), so here the rows are every row of the line in
|
||||
// ID order: the same set, in an order every client with this DBC agrees on.
|
||||
// The reader rejects bits whose length does not match its own row count.
|
||||
//
|
||||
// The canonical recipe list is every SkillLineAbility.dbc row for the skill
|
||||
// line whose spell actually produces a craft (see IsCraftRecipe — an item,
|
||||
// enchant, or Turtle LEARN_SPELL recipe; this excludes profession abilities
|
||||
// like Disenchant), in ascending record-ID order. That ordering is
|
||||
// deterministic and identical on every client with the same DBC, so builder
|
||||
// and reader agree on the bit-index space without any handshake. "Which
|
||||
// recipes does the linker know" comes from the player-spell bitmap
|
||||
// (VAR_PLAYER_SPELL_BITMAP — the same store IsPlayerSpell reads, which covers
|
||||
// profession recipes), NOT from the trade-skill window's server list.
|
||||
// What links: a profession spell, on both ends. 3.3.5 also requires the
|
||||
// skill line's canLink column, which 1.12's SkillLine.dbc does not have, so
|
||||
// every profession spell links: Smelting, Poisons and Beast Training too.
|
||||
//
|
||||
// The TradeSkill and Craft windows are separate frames with separate storage
|
||||
// and can both be open at once, so each gets its own builder (matching
|
||||
// WotLK) — each reads only its own window's open-state global (both zeroed by
|
||||
// the engine on close), so "both open" is unambiguous and neither guesses:
|
||||
//
|
||||
// - `C_TradeSkillUI.GetTradeSkillListLink()` — the TradeSkill window
|
||||
// - `C_TradeSkillUI.GetCraftListLink()` — the Craft window (Enchanting)
|
||||
// - `C_TradeSkillUI.GetTradeSkillListRecipes(skillLineID, bits)` — decoder
|
||||
// for the viewer.
|
||||
// The reader shows the link in the engine's own profession windows
|
||||
// (Window.cpp). GetTradeSkillListLink() is 3.3.5's, for the TradeSkill
|
||||
// window. GetCraftListLink() is its twin for the Craft window (Enchanting),
|
||||
// which 3.3.5 does not have.
|
||||
|
||||
#include "Link.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "Window.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "spell/Known.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace TradeSkill::Link {
|
||||
|
||||
namespace {
|
||||
|
||||
// 6 recipes per character, LSB-first (mirrors 3.x's encoder).
|
||||
constexpr char kBase64[] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
constexpr int kRecipesPerChar = 6;
|
||||
constexpr int kMaxRecipes = 1024; // largest vanilla skill line is ~340
|
||||
constexpr int kBitsBufSize = kMaxRecipes / kRecipesPerChar + 2;
|
||||
using AbilityLookup_t = const uint8_t *(__fastcall *)(uint32_t race, uint32_t classId,
|
||||
uint32_t spellID);
|
||||
using SlotForSkill_t = int(__thiscall *)(const void *player, uint32_t skillLine);
|
||||
using NameCacheGet_t = const uint8_t *(__thiscall *)(void *cache, uint32_t lo, uint32_t hi,
|
||||
const uint64_t *owner, void *callback,
|
||||
void *userData, char dedupe);
|
||||
|
||||
constexpr int kRowsPerChar = 6;
|
||||
|
||||
// 3.3.5's s_base64Encode (0x00A1B640).
|
||||
constexpr char kBase64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
// 3.3.5's s_base64Decode (0x00A1B688): an unknown character reads as 0.
|
||||
int Base64Value(char c) {
|
||||
for (int i = 0; i < 64; ++i)
|
||||
if (kBase64[i] == c)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// A spell is a craftable recipe (vs. a profession ability like Disenchant, or
|
||||
// a rank-up / skill-grant spell) iff one of its effects produces a craft: an
|
||||
// item (CREATE_ITEM), an enchant (ENCHANT_ITEM / _TEMPORARY), or Turtle's
|
||||
// custom recipes (LEARN_SPELL). Filtering on the skill-up threshold alone
|
||||
// wrongly kept Disenchant (which carries thresholds but isn't a recipe).
|
||||
bool IsCraftRecipe(int spellID) {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
const int32_t *eff = Game::Ptr<const int32_t>(
|
||||
rec, Offsets::OFF_SPELL_RECORD_EFFECT);
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
const int e = eff[i];
|
||||
if (e == Offsets::SPELL_EFFECT_CREATE_ITEM ||
|
||||
e == Offsets::SPELL_EFFECT_LEARN_SPELL ||
|
||||
e == Offsets::SPELL_EFFECT_ENCHANT_ITEM ||
|
||||
e == Offsets::SPELL_EFFECT_ENCHANT_ITEM_TEMPORARY)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fills `outSpell` with the recipe spellIDs for `skillLineID`, in canonical
|
||||
// (ascending record-ID) order, and — when non-null — `outGrey`/`outGreen` with
|
||||
// the skill levels the recipe turns grey / green at (its difficulty tiers; the
|
||||
// required-skill field 7 is uselessly 1 for nearly every trainer-taught
|
||||
// recipe). Returns the count (capped at `maxOut`). Same walk builder and reader
|
||||
// use, so the bit-index space is identical.
|
||||
int BuildRecipeList(uint32_t skillLineID, int *outSpell, int *outGrey,
|
||||
int *outGreen, int maxOut) {
|
||||
auto *const *records = Game::Read<const uint8_t *const *>(
|
||||
Offsets::VAR_SKILL_LINE_ABILITY_RECORDS);
|
||||
const int count = Game::Read<int>(
|
||||
Offsets::VAR_SKILL_LINE_ABILITY_COUNT);
|
||||
if (records == nullptr || count <= 0)
|
||||
return 0;
|
||||
|
||||
int n = 0;
|
||||
for (int i = 1; i <= count && n < maxOut; ++i) {
|
||||
const uint8_t *rec = records[i];
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
if (static_cast<uint32_t>(Game::Read<int>(
|
||||
rec, Offsets::OFF_SLA_SKILL_ID)) != skillLineID)
|
||||
continue;
|
||||
const int spellID =
|
||||
Game::Read<int>(rec, Offsets::OFF_SLA_SPELL_ID);
|
||||
if (!IsCraftRecipe(spellID))
|
||||
continue;
|
||||
if (outGrey != nullptr)
|
||||
outGrey[n] = Game::Read<int>(
|
||||
rec, Offsets::OFF_SLA_TRIVIAL_SKILL_HIGH);
|
||||
if (outGreen != nullptr)
|
||||
outGreen[n] = Game::Read<int>(
|
||||
rec, Offsets::OFF_SLA_TRIVIAL_SKILL_LOW);
|
||||
outSpell[n++] = spellID;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// Same bit the engine (and our IsPlayerSpell) reads — covers profession
|
||||
// recipes, not just trained abilities.
|
||||
bool IsKnownSpell(int spellID) {
|
||||
if (spellID <= 0)
|
||||
return false;
|
||||
auto *bitmap = Game::Read<const uint32_t *>(
|
||||
Offsets::VAR_PLAYER_SPELL_BITMAP);
|
||||
if (bitmap == nullptr)
|
||||
return false;
|
||||
return (bitmap[static_cast<uint32_t>(spellID) >> 5] &
|
||||
(1u << (spellID & 31))) != 0;
|
||||
}
|
||||
|
||||
// Skill line (SkillLine.dbc row) of the first SLA row that teaches `spellID`,
|
||||
// or 0 if none. Used to identify the Craft window's profession, which — unlike
|
||||
// the TradeSkill window — has no skill-line global (the engine derives it from
|
||||
// the recipe, see `GetCraftDisplaySkillLine` at `0x004F6CB0`).
|
||||
uint32_t SkillForSpell(int spellID) {
|
||||
if (spellID <= 0)
|
||||
return 0;
|
||||
auto *const *records = Game::Read<const uint8_t *const *>(
|
||||
Offsets::VAR_SKILL_LINE_ABILITY_RECORDS);
|
||||
const int count = Game::Read<int>(
|
||||
Offsets::VAR_SKILL_LINE_ABILITY_COUNT);
|
||||
if (records == nullptr || count <= 0)
|
||||
return 0;
|
||||
for (int i = 1; i <= count; ++i) {
|
||||
const uint8_t *rec = records[i];
|
||||
if (rec != nullptr &&
|
||||
Game::Read<int>(rec, Offsets::OFF_SLA_SPELL_ID) ==
|
||||
spellID)
|
||||
return static_cast<uint32_t>(
|
||||
Game::Read<int>(rec, Offsets::OFF_SLA_SKILL_ID));
|
||||
}
|
||||
if (c >= 'A' && c <= 'Z')
|
||||
return c - 'A';
|
||||
if (c >= 'a' && c <= 'z')
|
||||
return c - 'a' + 26;
|
||||
if (c >= '0' && c <= '9')
|
||||
return c - '0' + 52;
|
||||
if (c == '+')
|
||||
return 62;
|
||||
if (c == '/')
|
||||
return 63;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Skill line of the open Craft window (Enchanting, pet training) — derived
|
||||
// from its first real recipe entry, mirroring the engine. 0 if none open.
|
||||
uint32_t CraftSkillLine() {
|
||||
const int count = Game::Read<int>(Offsets::VAR_CRAFT_COUNT);
|
||||
if (count <= 0)
|
||||
return 0;
|
||||
const int cap = (count < kMaxRecipes) ? count : kMaxRecipes;
|
||||
for (int slot = 0; slot < cap; ++slot) {
|
||||
const int spellID = Spell::Lookup::RecipeSlotSpellID(
|
||||
Offsets::VAR_CRAFT_ENTRIES, Offsets::VAR_CRAFT_COUNT, slot);
|
||||
if (spellID > 0) {
|
||||
const uint32_t skill = SkillForSpell(spellID);
|
||||
if (skill != 0)
|
||||
return skill;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
// 3.3.5's SpellTableMatchRaceClass with no exclusions, as its encoder calls it.
|
||||
bool MatchRaceClass(uint32_t race, uint32_t classId, uint32_t raceMask, uint32_t classMask) {
|
||||
return (raceMask == 0 || (raceMask & (1u << (race - 1))) != 0) &&
|
||||
(classMask == 0 || (classMask & (1u << (classId - 1))) != 0);
|
||||
}
|
||||
|
||||
// Encodes the local player's known-recipe bitfield for `skillLineID` into
|
||||
// `out` (must hold kBitsBufSize). Returns the recipe count, or -1 if the skill
|
||||
// line has no recipes.
|
||||
int EncodeKnownBits(uint32_t skillLineID, char *out) {
|
||||
int spells[kMaxRecipes];
|
||||
const int n = BuildRecipeList(skillLineID, spells, nullptr, nullptr,
|
||||
kMaxRecipes);
|
||||
if (n <= 0)
|
||||
return -1;
|
||||
|
||||
int nb = 0;
|
||||
unsigned acc = 0;
|
||||
int used = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (IsKnownSpell(spells[i]))
|
||||
acc |= (1u << used);
|
||||
if (++used == kRecipesPerChar) {
|
||||
out[nb++] = kBase64[acc];
|
||||
acc = 0;
|
||||
used = 0;
|
||||
}
|
||||
}
|
||||
if (used != 0)
|
||||
out[nb++] = kBase64[acc];
|
||||
out[nb] = '\0';
|
||||
return n;
|
||||
}
|
||||
|
||||
// Current / max skill rank for the local player in `skillLine`, read from the
|
||||
// CGPlayer `+0xE68` sub-struct exactly as the engine's own accessors do (see
|
||||
// the FUN_SKILL_LINE_TO_SLOT block in Offsets.h). Leaves 0/0 when the player
|
||||
// isn't resolvable or doesn't have the skill.
|
||||
void ReadPlayerSkillRank(int skillLine, int *outCur, int *outMax) {
|
||||
*outCur = 0;
|
||||
*outMax = 0;
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return;
|
||||
using SlotFn = int(__thiscall *)(const void *, int);
|
||||
const int slot = reinterpret_cast<SlotFn>(
|
||||
Offsets::FUN_SKILL_LINE_TO_SLOT)(player, skillLine);
|
||||
// The player's rank and maximum in `skillLine` as GetTradeSkillLine pushes
|
||||
// them: each plus the permanent bonus when it is not 0, both 0 without a
|
||||
// slot (3.3.5's GetSkillValue / GetSkillMaxRank, which its encoder writes).
|
||||
void PlayerRank(const uint8_t *player, uint32_t skillLine, int *rank, int *maxRank) {
|
||||
*rank = 0;
|
||||
*maxRank = 0;
|
||||
const int slot =
|
||||
reinterpret_cast<SlotForSkill_t>(Offsets::FUN_SKILL_LINE_TO_SLOT)(player, skillLine);
|
||||
if (slot < 0)
|
||||
return;
|
||||
const uint8_t *base = Game::Read<const uint8_t *>(
|
||||
player, Offsets::OFF_CGPLAYER_INFO);
|
||||
if (base == nullptr)
|
||||
return;
|
||||
const uint8_t *rec =
|
||||
base + Offsets::OFF_SKILL_INFO_TABLE + slot * Offsets::SKILL_INFO_STRIDE;
|
||||
uint32_t cur = Game::Read<uint16_t>(rec, Offsets::OFF_SKILL_INFO_CUR);
|
||||
uint32_t mx = Game::Read<uint16_t>(rec, Offsets::OFF_SKILL_INFO_MAX);
|
||||
const uint32_t bonus =
|
||||
Game::Read<uint16_t>(rec, Offsets::OFF_SKILL_INFO_BONUS);
|
||||
if (cur != 0)
|
||||
cur += bonus;
|
||||
if (mx != 0)
|
||||
mx += bonus;
|
||||
*outCur = static_cast<int>(cur);
|
||||
*outMax = static_cast<int>(mx);
|
||||
const uint8_t *skills = Game::Read<const uint8_t *>(player, Offsets::OFF_CGPLAYER_INFO) +
|
||||
Offsets::OFF_SKILL_INFO_TABLE + slot * Offsets::SKILL_INFO_STRIDE;
|
||||
const int bonus = Game::Read<uint16_t>(skills, Offsets::OFF_SKILL_INFO_BONUS);
|
||||
const int cur = Game::Read<uint16_t>(skills, Offsets::OFF_SKILL_INFO_CUR);
|
||||
const int max = Game::Read<uint16_t>(skills, Offsets::OFF_SKILL_INFO_MAX);
|
||||
*rank = cur != 0 ? cur + bonus : 0;
|
||||
*maxRank = max != 0 ? max + bonus : 0;
|
||||
}
|
||||
|
||||
// The local player's character name via the engine's object name getter.
|
||||
const char *PlayerCharacterName() {
|
||||
// 3.3.5's CGTradeSkillInfo::GetTradeSkillListLink: the local player's link
|
||||
// for profession `spellID` into `buf`, or false.
|
||||
bool Build(int spellID, char *buf, size_t size) {
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return "";
|
||||
using GetName_t = const char *(__thiscall *)(const void *, int *);
|
||||
const char *name = reinterpret_cast<GetName_t>(
|
||||
Offsets::FUN_OBJECT_GET_NAME)(player, nullptr);
|
||||
return (name != nullptr) ? name : "";
|
||||
return false;
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr ||
|
||||
Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_EFFECT) !=
|
||||
Offsets::SPELL_EFFECT_TRADE_SKILL)
|
||||
return false;
|
||||
|
||||
const uint8_t *desc = Unit::Identity::PlayerDescriptor();
|
||||
if (desc == nullptr)
|
||||
return false;
|
||||
const uint32_t race = desc[Offsets::OFF_UNIT_DESCRIPTOR_RACE_BYTE];
|
||||
const uint32_t classId = desc[Offsets::OFF_UNIT_DESCRIPTOR_CLASS_BYTE];
|
||||
const uint8_t *ability = reinterpret_cast<AbilityLookup_t>(
|
||||
Offsets::FUN_SKILL_LINE_ABILITY_LOOKUP)(race, classId, static_cast<uint32_t>(spellID));
|
||||
if (ability == nullptr)
|
||||
return false;
|
||||
const uint32_t skillLine = Game::Read<uint32_t>(ability, Offsets::OFF_SLA_SKILL_ID);
|
||||
if (DBC::Record(Offsets::VAR_SKILL_LINE_RECORDS, Offsets::VAR_SKILL_LINE_COUNT, skillLine) ==
|
||||
nullptr)
|
||||
return false;
|
||||
|
||||
const uint8_t *rows[MAX_ABILITIES];
|
||||
const int count = Abilities(skillLine, rows);
|
||||
if (count == 0)
|
||||
return false;
|
||||
|
||||
int rank, maxRank;
|
||||
PlayerRank(player, skillLine, &rank, &maxRank);
|
||||
const int prefix = std::snprintf(buf, size, "|cffffd000|Htrade:%d:%d:%d:%I64X:", spellID,
|
||||
rank, maxRank, Unit::Identity::PlayerGuid());
|
||||
if (prefix < 0 || static_cast<size_t>(prefix + BitChars(count) + 1) >= size)
|
||||
return false;
|
||||
|
||||
char *out = buf + prefix;
|
||||
unsigned bits = 0;
|
||||
int bit = 0;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (Spell::Known::IsKnown(Game::Read<uint32_t>(rows[i], Offsets::OFF_SLA_SPELL_ID)) &&
|
||||
MatchRaceClass(race, classId, Game::Read<uint32_t>(rows[i], Offsets::OFF_SLA_RACE_MASK),
|
||||
Game::Read<uint32_t>(rows[i], Offsets::OFF_SLA_CLASS_MASK)))
|
||||
bits |= 1u << bit;
|
||||
if (++bit == kRowsPerChar) {
|
||||
*out++ = kBase64[bits];
|
||||
bits = 0;
|
||||
bit = 0;
|
||||
}
|
||||
}
|
||||
if (bit != 0)
|
||||
*out++ = kBase64[bits];
|
||||
|
||||
const char *name = DBC::LocalizedField(Offsets::VAR_SPELL_RECORDS,
|
||||
Offsets::VAR_SPELL_RECORD_COUNT,
|
||||
static_cast<uint32_t>(spellID), Offsets::OFF_SPELL_NAMES);
|
||||
std::snprintf(out, static_cast<size_t>(buf + size - out), "|h[%s]|h|r",
|
||||
name != nullptr ? name : "");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Builds the full `|Htrade:...|h` link for whichever window `useCraft`
|
||||
// selects. Pushes the link string (returns 1), or nothing (returns 0 → nil)
|
||||
// when that window is closed or has no recipes.
|
||||
int BuildLink(void *L, bool useCraft) {
|
||||
int skillLine;
|
||||
if (useCraft) {
|
||||
// Craft-window open-state flag; zeroed by the engine on close.
|
||||
if (Game::Read<int>(Offsets::VAR_CRAFT_WINDOW_STATE) == 0)
|
||||
return 0;
|
||||
skillLine = static_cast<int>(CraftSkillLine());
|
||||
} else {
|
||||
// TradeSkill skill-line global; zeroed by the engine on close.
|
||||
skillLine = Game::Read<int>(
|
||||
Offsets::VAR_CURRENT_TRADESKILL_LINE);
|
||||
}
|
||||
if (skillLine <= 0)
|
||||
return 0;
|
||||
// 3.3.5's TradeSkillLinkNameCallback: the name query for a link's linker was
|
||||
// answered; `data` is the copy of the link Open made.
|
||||
void __stdcall LinkerNameLoaded(void *data, char success) {
|
||||
if (success)
|
||||
Open(static_cast<const char *>(data));
|
||||
std::free(data);
|
||||
}
|
||||
|
||||
char bits[kBitsBufSize];
|
||||
if (EncodeKnownBits(static_cast<uint32_t>(skillLine), bits) < 0)
|
||||
return 0;
|
||||
|
||||
const char *prof = DBC::LocalizedField(
|
||||
Offsets::VAR_SKILL_LINE_RECORDS, Offsets::VAR_SKILL_LINE_COUNT,
|
||||
static_cast<uint32_t>(skillLine), Offsets::OFF_SKILL_LINE_NAME);
|
||||
if (prof == nullptr || *prof == '\0')
|
||||
return 0;
|
||||
|
||||
int cur = 0, mx = 0;
|
||||
ReadPlayerSkillRank(skillLine, &cur, &mx);
|
||||
const char *who = PlayerCharacterName();
|
||||
|
||||
char link[512];
|
||||
std::snprintf(link, sizeof link,
|
||||
"|cffffd000|Htrade:%d:%d:%d:%s:%s|h[%s]|h|r", skillLine, cur,
|
||||
mx, who, bits, prof);
|
||||
Game::Lua::PushString(L, link);
|
||||
int PushLink(void *L, int spellID) {
|
||||
char link[256];
|
||||
if (Build(spellID, link, sizeof link))
|
||||
Game::Lua::PushString(L, link);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_TradeSkillUI.GetTradeSkillListLink()` -> link for the open TradeSkill
|
||||
// window, or nil.
|
||||
// `GetTradeSkillListLink()` — 3.3.5's Script_GetTradeSkillListLink.
|
||||
int __fastcall Script_GetTradeSkillListLink(void *L) {
|
||||
return BuildLink(L, /*useCraft=*/false);
|
||||
return PushLink(L, Window::TradeSkillSpell());
|
||||
}
|
||||
|
||||
// `C_TradeSkillUI.GetCraftListLink()` -> link for the open Craft window, or nil.
|
||||
int __fastcall Script_GetCraftListLink(void *L) {
|
||||
return BuildLink(L, /*useCraft=*/true);
|
||||
}
|
||||
// `GetCraftListLink()` — the Craft window's twin.
|
||||
int __fastcall Script_GetCraftListLink(void *L) { return PushLink(L, Window::CraftSpell()); }
|
||||
|
||||
// `C_TradeSkillUI.GetTradeSkillListRecipes(skillLineID, bits)`
|
||||
// -> { { spellID, isKnown, trivialLevel, greenLevel, createdItem,
|
||||
// numMade, reagents = { { itemID, count }, ... } }, ... }
|
||||
// `trivialLevel` / `greenLevel` are the skill levels the recipe turns
|
||||
// grey / green at — its difficulty tiers, for sorting and colouring.
|
||||
// `createdItem` is the crafted item's ID (0 for enchants) — the viewer
|
||||
// derives its subclass filter from it. `numMade` is how many the recipe
|
||||
// yields (CREATE_ITEM base points + dice; 0 for enchants). `reagents` lists
|
||||
// each consumed item + required count in DBC slot order (stopping at the
|
||||
// first empty slot, the engine's own convention), for the detail pane.
|
||||
//
|
||||
// Rebuilds the canonical recipe list for `skillLineID` and decodes `bits`
|
||||
// against it. Recipes past the end of `bits` (shorter/garbled string) decode
|
||||
// as not-known rather than erroring. The Lua caller (the viewer) turns each
|
||||
// spellID into a name/icon via GetSpellInfo and each reagent itemID into
|
||||
// name/texture/on-hand-count via GetItemInfo / GetItemCount.
|
||||
//
|
||||
// The real 3.3.5 reagent API (GetTradeSkillReagentInfo etc.) can't serve this:
|
||||
// it's indexed by position in the player's *own* open trade window, so it
|
||||
// can't describe a linked recipe from someone else's profession. We read the
|
||||
// reagents straight from Spell.dbc instead.
|
||||
int __fastcall Script_GetTradeSkillListRecipes(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(
|
||||
L, "Usage: C_TradeSkillUI.GetTradeSkillListRecipes(skillLineID, bits)");
|
||||
return 0;
|
||||
}
|
||||
const uint32_t skillLine =
|
||||
static_cast<uint32_t>(Game::Lua::ToNumber(L, 1));
|
||||
const char *bits = Game::Lua::ToString(L, 2);
|
||||
const int blen = (bits != nullptr) ? static_cast<int>(std::strlen(bits)) : 0;
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
int spells[kMaxRecipes];
|
||||
int greys[kMaxRecipes];
|
||||
int greens[kMaxRecipes];
|
||||
const int n = BuildRecipeList(skillLine, spells, greys, greens, kMaxRecipes);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const int ci = i / kRecipesPerChar;
|
||||
const int bi = i % kRecipesPerChar;
|
||||
bool known = false;
|
||||
if (ci < blen) {
|
||||
const int v = Base64Value(bits[ci]);
|
||||
if (v >= 0)
|
||||
known = (v & (1 << bi)) != 0;
|
||||
}
|
||||
|
||||
// The recipe's crafted item (0 for enchants) — the viewer groups the
|
||||
// subclass filter by this item's subclass — plus its yield count.
|
||||
const uint8_t *srec = Spell::Lookup::RecordForID(spells[i]);
|
||||
int createdItem = 0;
|
||||
int numMade = 0;
|
||||
if (srec != nullptr) {
|
||||
createdItem = Game::Read<int>(
|
||||
srec, Offsets::OFF_SPELL_RECORD_CREATED_ITEM);
|
||||
// Yield: the CREATE_ITEM effect's magnitude (base points + dice),
|
||||
// mirroring the engine's item-creation math. 0 for enchants.
|
||||
const int32_t *eff = Game::Ptr<const int32_t>(
|
||||
srec, Offsets::OFF_SPELL_RECORD_EFFECT);
|
||||
const int32_t *basePts = Game::Ptr<const int32_t>(
|
||||
srec, Offsets::OFF_SPELL_RECORD_EFFECT_BASE_POINTS);
|
||||
const int32_t *baseDice = Game::Ptr<const int32_t>(
|
||||
srec, Offsets::OFF_SPELL_RECORD_EFFECT_BASE_DICE);
|
||||
for (int e = 0; e < Offsets::SPELL_RECORD_EFFECT_COUNT; ++e) {
|
||||
if (eff[e] == Offsets::SPELL_EFFECT_CREATE_ITEM) {
|
||||
numMade = basePts[e] + baseDice[e];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "spellID", static_cast<double>(spells[i]));
|
||||
Game::Lua::SetFieldBool(L, "isKnown", known);
|
||||
Game::Lua::SetFieldNumber(L, "trivialLevel",
|
||||
static_cast<double>(greys[i]));
|
||||
Game::Lua::SetFieldNumber(L, "greenLevel",
|
||||
static_cast<double>(greens[i]));
|
||||
Game::Lua::SetFieldNumber(L, "createdItem",
|
||||
static_cast<double>(createdItem));
|
||||
Game::Lua::SetFieldNumber(L, "numMade", static_cast<double>(numMade));
|
||||
|
||||
// reagents = { {itemID, count}, ... } — nested array under "reagents"
|
||||
// (no SetField helper takes a table value, so push key + build + SetTable).
|
||||
Game::Lua::PushString(L, "reagents");
|
||||
Game::Lua::NewTable(L);
|
||||
if (srec != nullptr) {
|
||||
const int32_t *rids = Game::Ptr<const int32_t>(
|
||||
srec, Offsets::OFF_SPELL_REAGENT_ID);
|
||||
const int32_t *rcounts = Game::Ptr<const int32_t>(
|
||||
srec, Offsets::OFF_SPELL_REAGENT_COUNT);
|
||||
int outIdx = 0;
|
||||
for (int r = 0; r < Offsets::SPELL_MAX_REAGENTS; ++r) {
|
||||
if (rids[r] == 0)
|
||||
break; // first empty slot ends the list
|
||||
Game::Lua::PushNumber(L, static_cast<double>(++outIdx));
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "itemID",
|
||||
static_cast<double>(rids[r]));
|
||||
Game::Lua::SetFieldNumber(L, "count",
|
||||
static_cast<double>(rcounts[r]));
|
||||
Game::Lua::SetTable(L, -3); // reagents[outIdx] = {itemID,count}
|
||||
}
|
||||
}
|
||||
Game::Lua::SetTable(L, -3); // entry.reagents = { ... }
|
||||
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
const Game::Doc::Field kListLinkRets[] = {
|
||||
Game::Doc::Opt("link", "string", nullptr,
|
||||
"A trade: link; nil when the window is closed or its profession cannot be "
|
||||
"linked."),
|
||||
};
|
||||
const Game::Doc::Function kGetTradeSkillListLink{
|
||||
"Returns a link to the player's profession in the trade-skill window, with the recipes the "
|
||||
"player knows.",
|
||||
{}, kListLinkRets, "TradeSkillGlobals"};
|
||||
const Game::Doc::Function kGetCraftListLink{
|
||||
"Returns a link to the player's profession in the craft window, with the recipes the player "
|
||||
"knows.",
|
||||
{}, kListLinkRets, "TradeSkillGlobals", true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_TradeSkillUI", "GetTradeSkillListLink",
|
||||
&Script_GetTradeSkillListLink);
|
||||
Game::Lua::RegisterTableFunction("C_TradeSkillUI", "GetCraftListLink",
|
||||
&Script_GetCraftListLink);
|
||||
Game::Lua::RegisterTableFunction("C_TradeSkillUI",
|
||||
"GetTradeSkillListRecipes",
|
||||
&Script_GetTradeSkillListRecipes);
|
||||
Game::Lua::RegisterGlobalFunction("GetTradeSkillListLink", &Script_GetTradeSkillListLink,
|
||||
&kGetTradeSkillListLink);
|
||||
Game::Lua::RegisterGlobalFunction("GetCraftListLink", &Script_GetCraftListLink,
|
||||
&kGetCraftListLink);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
int Abilities(uint32_t skillLine, const uint8_t **out) {
|
||||
auto *const *records =
|
||||
Game::Read<const uint8_t *const *>(Offsets::VAR_SKILL_LINE_ABILITY_RECORDS);
|
||||
const int count = Game::Read<int>(Offsets::VAR_SKILL_LINE_ABILITY_COUNT);
|
||||
if (records == nullptr)
|
||||
return 0;
|
||||
int n = 0;
|
||||
for (int id = 1; id <= count && n < MAX_ABILITIES; ++id) {
|
||||
const uint8_t *row = records[id];
|
||||
if (row != nullptr && Game::Read<uint32_t>(row, Offsets::OFF_SLA_SKILL_ID) == skillLine)
|
||||
out[n++] = row;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
int BitChars(int count) { return (count + kRowsPerChar - 1) / kRowsPerChar; }
|
||||
|
||||
bool IsSet(const char *bits, int index) {
|
||||
return (Base64Value(bits[index / kRowsPerChar]) & (1 << (index % kRowsPerChar))) != 0;
|
||||
}
|
||||
|
||||
void Open(const char *data) {
|
||||
if (data == nullptr || *data == '\0')
|
||||
return;
|
||||
const int spellID = std::atoi(data);
|
||||
if (spellID == 0)
|
||||
return;
|
||||
const char *fields = std::strchr(data, ':');
|
||||
if (fields == nullptr)
|
||||
return;
|
||||
++fields;
|
||||
// 3.3.5 reads `%d:%d:%I64X:`; a field after a malformed one stays 0.
|
||||
int rank = 0;
|
||||
int maxRank = 0;
|
||||
uint64_t guid = 0;
|
||||
char *end = nullptr;
|
||||
rank = static_cast<int>(std::strtol(fields, &end, 10));
|
||||
if (*end == ':') {
|
||||
maxRank = static_cast<int>(std::strtol(end + 1, &end, 10));
|
||||
if (*end == ':')
|
||||
guid = std::strtoull(end + 1, nullptr, 16);
|
||||
}
|
||||
if (guid == 0)
|
||||
return;
|
||||
|
||||
// A miss keeps the copy with the query and opens the link on the answer.
|
||||
char *copy = _strdup(data);
|
||||
if (copy == nullptr)
|
||||
return;
|
||||
const uint64_t none = 0;
|
||||
const uint8_t *linker = reinterpret_cast<NameCacheGet_t>(Offsets::FUN_PLAYER_INFO_LOOKUP)(
|
||||
reinterpret_cast<void *>(Offsets::VAR_PLAYER_NAME_CACHE), static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), &none, reinterpret_cast<void *>(&LinkerNameLoaded),
|
||||
copy, 1);
|
||||
if (linker == nullptr)
|
||||
return;
|
||||
std::free(copy);
|
||||
if (const char *bits = std::strrchr(fields, ':'))
|
||||
Window::ShowLinked(spellID, guid, bits + 1, rank, maxRank);
|
||||
}
|
||||
|
||||
} // namespace TradeSkill::Link
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// The `trade:` profession link, 3.3.5's format and bit space. See Link.cpp.
|
||||
namespace TradeSkill::Link {
|
||||
|
||||
// Upper bound on a skill line's SkillLineAbility rows (Blacksmithing, the
|
||||
// largest, has 343).
|
||||
constexpr int MAX_ABILITIES = 1024;
|
||||
|
||||
// Fills `out` with the SkillLineAbility records a link's bits cover for
|
||||
// `skillLine`, one bit each, in DBC order; returns the count (at most
|
||||
// MAX_ABILITIES). 3.3.5's SpellTableGetSkillLineLinkInfo.
|
||||
int Abilities(uint32_t skillLine, const uint8_t **out);
|
||||
|
||||
// The number of bit characters a link carries for `count` rows.
|
||||
int BitChars(int count);
|
||||
|
||||
// Whether row `index` is set in a link's bit characters.
|
||||
bool IsSet(const char *bits, int index);
|
||||
|
||||
// 3.3.5's CGTradeSkillInfo::SetLink: shows the profession a received link
|
||||
// carries. `data` is the link payload after `trade:`. A linker the name
|
||||
// cache does not hold yet is queried, and the window opens when the answer
|
||||
// arrives.
|
||||
void Open(const char *data);
|
||||
|
||||
} // namespace TradeSkill::Link
|
||||
@@ -12,8 +12,7 @@
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The trade-skill window's recipe functions from 3.x:
|
||||
// `GetTradeSkillRecipeLink(index)`, `GetTradeSkillDescription(index)` and
|
||||
// `IsTradeSkillLinked()`.
|
||||
// `GetTradeSkillRecipeLink(index)` and `GetTradeSkillDescription(index)`.
|
||||
//
|
||||
// `GetTradeSkillRecipeLink(index)` links the recipe in row `index`: the
|
||||
// recipe spell, as an `enchant:` link. Vanilla's window links only the item a
|
||||
@@ -44,15 +43,6 @@
|
||||
// then the spell-text parser into a 1024-byte buffer (Spell::Description).
|
||||
// Most recipes that make an item have no description; enchants do. 1.12's
|
||||
// Craft window already has this as GetCraftDescription.
|
||||
//
|
||||
// `IsTradeSkillLinked()` says whether the window shows a profession opened
|
||||
// from another player's link, and whose. On this client it never does. A
|
||||
// received `trade:` link opens ClassicAPI's own viewer
|
||||
// (Util/TradeSkillLink.lua), which the trade-skill functions never describe,
|
||||
// and the window shows the player's own professions only. So it returns
|
||||
// (nil, nil), 3.3.5's answer for the player's own window. Addons check it to
|
||||
// keep a linked profession out of the player's own records; Zygor skips its
|
||||
// recipe scan on it.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
@@ -160,12 +150,6 @@ int __fastcall Script_GetTradeSkillDescription(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsTradeSkillLinked(void *L) {
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNil(L);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kRecipeLinkArgs[] = {
|
||||
@@ -187,24 +171,12 @@ const Game::Doc::Function kGetTradeSkillDescription{
|
||||
"Returns the description of the recipe in a row of the trade-skill window.",
|
||||
kRecipeLinkArgs, kDescriptionRets, "TradeSkillGlobals"};
|
||||
|
||||
const Game::Doc::Field kLinkedRets[] = {
|
||||
Game::Doc::Opt("isLinked", "bool", nullptr,
|
||||
"True when the window shows another player's linked profession."),
|
||||
Game::Doc::Opt("linkedPlayerName", "string", nullptr,
|
||||
"The name of the player whose profession is shown."),
|
||||
};
|
||||
const Game::Doc::Function kIsTradeSkillLinked{
|
||||
"Whether the trade-skill window shows a profession opened from another player's link.",
|
||||
{}, kLinkedRets, "TradeSkillGlobals"};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetTradeSkillRecipeLink", &Script_GetTradeSkillRecipeLink,
|
||||
&kGetTradeSkillRecipeLink);
|
||||
Game::Lua::RegisterGlobalFunction("GetTradeSkillDescription",
|
||||
&Script_GetTradeSkillDescription,
|
||||
&kGetTradeSkillDescription);
|
||||
Game::Lua::RegisterGlobalFunction("IsTradeSkillLinked", &Script_IsTradeSkillLinked,
|
||||
&kIsTradeSkillLinked);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -0,0 +1,460 @@
|
||||
// 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/>.
|
||||
|
||||
// Whose profession the TradeSkill and Craft windows show: the player's own,
|
||||
// or another player's from a `trade:` link, as 3.3.5's trade-skill window
|
||||
// does.
|
||||
//
|
||||
// 3.3.5's CGTradeSkillInfo keeps whose profession the window shows
|
||||
// (m_playerGuid, m_spellID, m_skillRank / m_skillMaxRank, m_linkData), and its
|
||||
// list build RefreshList (0x005DD5A0) takes the rows from the link when
|
||||
// m_linkData is set. 1.12's BuildTradeSkillList (0x004FCA20) is the same build
|
||||
// without the link branch. This module adds the branch at the build's three
|
||||
// per-row reads, and the same for the Craft window (Enchanting), which 3.3.5
|
||||
// does not have:
|
||||
// - The spell list: the link's rows instead of the player's list
|
||||
// (CGPlayer_C::FindSkillLineSpells, or GetTabSpells for the Craft
|
||||
// window). The rows are the set bits whose spell would be in that list
|
||||
// had the linker learned it: the engine's learn function (0x005E9C20)
|
||||
// files a SPELL_ATTR_TRADESPELL spell under its skill line and any other
|
||||
// under its castUI tab. RefreshList keeps the TRADESPELL rows, the same
|
||||
// filter.
|
||||
// - The ability row: looked up for the linker's race and class. RefreshList
|
||||
// takes the link's row itself.
|
||||
// - The rank: the linker's, unless the link is the player's own.
|
||||
// Everything else in the builds runs as for the player's own window:
|
||||
// available counts from the player's bags, headers, filters, order, events.
|
||||
//
|
||||
// Opening (3.3.5 SetTradeSkill, 0x005DE120): a link opens the window its
|
||||
// spell opens when cast (TryCastSpell routes on EffectMiscValue[0]), through
|
||||
// the engine's own toggle, with the window marked closed first so that a link
|
||||
// to the line already open replaces it instead of closing it. Casting a
|
||||
// profession while a link shows opens the player's own list, again without
|
||||
// closing. Both casts reach the toggles from one call each in TryCastSpell,
|
||||
// whose cast spell (ESI) is 3.3.5's m_spellID. As in 3.3.5, closing keeps the
|
||||
// link, so IsTradeSkillLinked answers until the next open, and the return to
|
||||
// the character screen drops it (CGTradeSkillInfo::ShutdownGame; here
|
||||
// CGGameUI::ShutdownGame's calls of the two window shutdowns).
|
||||
//
|
||||
// The Lua getters that answer for the player answer for the linker while a
|
||||
// link shows: GetTradeSkillLine (3.3.5's), and the Craft window's
|
||||
// GetCraftDisplaySkillLine and GetCraftName. IsTradeSkillLinked is 3.3.5's;
|
||||
// IsCraftLinked is its Craft window twin.
|
||||
//
|
||||
// Each window takes links only when every one of its seams is patched, so a
|
||||
// seam another DLL holds can't show a link half-way.
|
||||
|
||||
#include "Window.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Link.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace TradeSkill::Window {
|
||||
|
||||
namespace {
|
||||
|
||||
using Toggle_t = void(__fastcall *)(uint32_t key);
|
||||
using Void_t = void(__cdecl *)();
|
||||
using AbilityLookup_t = const uint8_t *(__fastcall *)(uint32_t race, uint32_t classId,
|
||||
uint32_t spellID);
|
||||
using PlayerList_t = const void *(__thiscall *)(void *player, uint32_t key);
|
||||
using UnitAbility_t = const uint8_t *(__thiscall *)(void *unit, uint32_t spellID);
|
||||
using SkillTotal_t = int(__thiscall *)(void *player, uint32_t skillLine);
|
||||
using SlotForSkill_t = int(__thiscall *)(const void *player, uint32_t skillLine);
|
||||
using Script_t = int(__fastcall *)(void *L);
|
||||
|
||||
// The only Craft type GetCraftDisplaySkillLine answers for (Enchanting).
|
||||
constexpr uint32_t kCraftTypeEnscribe = 3;
|
||||
|
||||
// The engine's growable array, as the list builds read it.
|
||||
struct SpellArray {
|
||||
uint32_t capacity;
|
||||
uint32_t count;
|
||||
const int *data;
|
||||
uint32_t granularity;
|
||||
};
|
||||
static_assert(offsetof(SpellArray, count) == 0x04 && offsetof(SpellArray, data) == 0x08,
|
||||
"the builds read the count at +0x04 and the spells at +0x08");
|
||||
|
||||
// What a window shows: 3.3.5's CGTradeSkillInfo state.
|
||||
struct Shown {
|
||||
int spellID = 0; // the profession spell (m_spellID)
|
||||
uint64_t guid = 0; // whose profession (m_playerGuid)
|
||||
bool linked = false;
|
||||
uint32_t opened = 0; // what the window opened on: the skill line, or the Craft type
|
||||
uint32_t skillLine = 0;
|
||||
uint32_t race = 0; // the linker's
|
||||
uint32_t classId = 0;
|
||||
int rank = 0; // the linker's (m_skillRank / m_skillMaxRank)
|
||||
int maxRank = 0;
|
||||
std::vector<int> spells; // the link's rows
|
||||
SpellArray list{};
|
||||
};
|
||||
|
||||
Shown g_tradeSkill;
|
||||
Shown g_craft;
|
||||
bool g_tradeSkillReady = false;
|
||||
bool g_craftReady = false;
|
||||
|
||||
uint32_t TradeSkillOpen() { return Game::Read<uint32_t>(Offsets::VAR_CURRENT_TRADESKILL_LINE); }
|
||||
uint32_t CraftOpen() { return Game::Read<uint32_t>(Offsets::VAR_CRAFT_WINDOW_STATE); }
|
||||
|
||||
const uint8_t *Ability(uint32_t race, uint32_t classId, uint32_t spellID) {
|
||||
return reinterpret_cast<AbilityLookup_t>(Offsets::FUN_SKILL_LINE_ABILITY_LOOKUP)(race, classId,
|
||||
spellID);
|
||||
}
|
||||
|
||||
const char *SpellName(int spellID) {
|
||||
return DBC::LocalizedField(Offsets::VAR_SPELL_RECORDS, Offsets::VAR_SPELL_RECORD_COUNT,
|
||||
static_cast<uint32_t>(spellID), Offsets::OFF_SPELL_NAMES);
|
||||
}
|
||||
|
||||
// The window open on `open` builds its rows from the link.
|
||||
bool BuildsLink(const Shown &s, uint32_t open) { return s.linked && open == s.opened; }
|
||||
|
||||
// The window open on `open` shows another player's link, whose ranks it
|
||||
// answers with (3.3.5: m_playerGuid is not the active player).
|
||||
bool ShowsOtherLink(const Shown &s, uint32_t open) {
|
||||
return open != 0 && BuildsLink(s, open) && s.guid != Unit::Identity::PlayerGuid();
|
||||
}
|
||||
|
||||
void Unlink(Shown &s) {
|
||||
s.linked = false;
|
||||
s.spells.clear();
|
||||
s.list = {};
|
||||
}
|
||||
|
||||
// Whether the player's own list for the window holds `spellID` once learned:
|
||||
// the TradeSkill window's list takes TRADESPELL spells, a Craft window's the
|
||||
// other spells of its castUI tab.
|
||||
bool InPlayerList(int spellID, uint32_t craftType) {
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
const bool tradeSpell = (Game::Read<uint32_t>(record, Offsets::OFF_SPELL_RECORD_ATTRIBUTES) &
|
||||
Offsets::SPELL_ATTR_TRADESPELL) != 0;
|
||||
if (craftType == 0)
|
||||
return tradeSpell;
|
||||
return !tradeSpell && Game::Read<uint32_t>(record, Offsets::OFF_SPELL_RECORD_CAST_UI) == craftType;
|
||||
}
|
||||
|
||||
// --- Opening ------------------------------------------------------------------
|
||||
|
||||
// The player cast profession `spellID`, which opens the window on `key`.
|
||||
void Cast(Shown &s, uint32_t key, int spellID, uintptr_t openVar, uintptr_t toggle) {
|
||||
if (s.linked) {
|
||||
Unlink(s);
|
||||
if (Game::Read<uint32_t>(openVar) == key)
|
||||
Game::Ref<uint32_t>(openVar) = 0;
|
||||
}
|
||||
s.spellID = spellID;
|
||||
s.guid = Unit::Identity::PlayerGuid();
|
||||
s.opened = key;
|
||||
reinterpret_cast<Toggle_t>(toggle)(key);
|
||||
}
|
||||
|
||||
void __fastcall CastTradeSkill(uint32_t skillLine, int spellID) {
|
||||
Cast(g_tradeSkill, skillLine, spellID, Offsets::VAR_CURRENT_TRADESKILL_LINE,
|
||||
Offsets::FUN_TOGGLE_TRADE_SKILL);
|
||||
}
|
||||
|
||||
void __fastcall CastCraft(uint32_t craftType, int spellID) {
|
||||
Cast(g_craft, craftType, spellID, Offsets::VAR_CRAFT_WINDOW_STATE, Offsets::FUN_TOGGLE_CRAFT);
|
||||
}
|
||||
|
||||
// Stand in for TryCastSpell's toggle calls: the toggle's key in ECX, the cast
|
||||
// spell in ESI.
|
||||
__declspec(naked) void CastTradeSkillThunk() {
|
||||
__asm {
|
||||
mov edx, esi
|
||||
jmp CastTradeSkill
|
||||
}
|
||||
}
|
||||
|
||||
__declspec(naked) void CastCraftThunk() {
|
||||
__asm {
|
||||
mov edx, esi
|
||||
jmp CastCraft
|
||||
}
|
||||
}
|
||||
|
||||
// --- The list builds ----------------------------------------------------------
|
||||
|
||||
// CALL_BUILD_TRADE_SKILL_SPELLS: CGPlayer_C::FindSkillLineSpells.
|
||||
const void *__fastcall TradeSkillSpells(void *player, void *, uint32_t skillLine) {
|
||||
if (BuildsLink(g_tradeSkill, skillLine))
|
||||
return &g_tradeSkill.list;
|
||||
return reinterpret_cast<PlayerList_t>(Offsets::FUN_PLAYER_FIND_SKILL_LINE_SPELLS)(player,
|
||||
skillLine);
|
||||
}
|
||||
|
||||
// CALL_BUILD_TRADE_SKILL_ABILITY: the ability lookup for the player's race
|
||||
// and class.
|
||||
const uint8_t *__fastcall TradeSkillAbility(uint32_t race, uint32_t classId, uint32_t spellID) {
|
||||
const Shown &s = g_tradeSkill;
|
||||
if (BuildsLink(s, TradeSkillOpen()))
|
||||
return Ability(s.race, s.classId, spellID);
|
||||
return Ability(race, classId, spellID);
|
||||
}
|
||||
|
||||
// SITE_BUILD_TRADE_SKILL_RANK: the player's raw rank in the row's line, 0
|
||||
// without a slot, as the 33 bytes it replaces read it.
|
||||
int __fastcall TradeSkillRank(const uint8_t *player, void *, uint32_t skillLine) {
|
||||
if (ShowsOtherLink(g_tradeSkill, TradeSkillOpen()))
|
||||
return g_tradeSkill.rank;
|
||||
const int slot =
|
||||
reinterpret_cast<SlotForSkill_t>(Offsets::FUN_SKILL_LINE_TO_SLOT)(player, skillLine);
|
||||
if (slot < 0)
|
||||
return 0;
|
||||
return Game::Read<uint16_t>(Game::Read<const uint8_t *>(player, Offsets::OFF_CGPLAYER_INFO) +
|
||||
Offsets::OFF_SKILL_INFO_TABLE + slot * Offsets::SKILL_INFO_STRIDE,
|
||||
Offsets::OFF_SKILL_INFO_CUR);
|
||||
}
|
||||
|
||||
// CALL_BUILD_CRAFT_SPELLS: CGPlayer_C::GetTabSpells.
|
||||
const void *__fastcall CraftSpells(void *player, void *, uint32_t tab) {
|
||||
if (BuildsLink(g_craft, tab))
|
||||
return &g_craft.list;
|
||||
return reinterpret_cast<PlayerList_t>(Offsets::FUN_PLAYER_GET_TAB_SPELLS)(player, tab);
|
||||
}
|
||||
|
||||
// CALL_BUILD_CRAFT_ABILITY: CGUnit_C::FindSkillLineAbility on the player.
|
||||
const uint8_t *__fastcall CraftAbility(void *unit, void *, uint32_t spellID) {
|
||||
const Shown &s = g_craft;
|
||||
if (BuildsLink(s, CraftOpen()))
|
||||
return Ability(s.race, s.classId, spellID);
|
||||
return reinterpret_cast<UnitAbility_t>(Offsets::FUN_UNIT_FIND_SKILL_LINE_ABILITY)(unit,
|
||||
spellID);
|
||||
}
|
||||
|
||||
// CALL_BUILD_CRAFT_RANK: CGPlayer_C::GetSkillTotal.
|
||||
int __fastcall CraftRank(void *player, void *, uint32_t skillLine) {
|
||||
if (ShowsOtherLink(g_craft, CraftOpen()))
|
||||
return g_craft.rank;
|
||||
return reinterpret_cast<SkillTotal_t>(Offsets::FUN_PLAYER_GET_SKILL_TOTAL)(player, skillLine);
|
||||
}
|
||||
|
||||
// CGGameUI::ShutdownGame's calls of the window shutdowns.
|
||||
void __cdecl ShutdownTradeSkill() {
|
||||
g_tradeSkill = Shown{};
|
||||
reinterpret_cast<Void_t>(Offsets::FUN_SHUTDOWN_TRADE_SKILL)();
|
||||
}
|
||||
|
||||
void __cdecl ShutdownCraft() {
|
||||
g_craft = Shown{};
|
||||
reinterpret_cast<Void_t>(Offsets::FUN_SHUTDOWN_CRAFT)();
|
||||
}
|
||||
|
||||
// --- Lua ----------------------------------------------------------------------
|
||||
|
||||
int PushLinked(void *L, const Shown &s) {
|
||||
if (!s.linked) {
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNil(L);
|
||||
return 2;
|
||||
}
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
if (const char *name = Unit::Identity::NameForGuid(s.guid))
|
||||
Game::Lua::PushString(L, name);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// `IsTradeSkillLinked()` — 3.3.5's Script_IsTradeSkillLinked (0x005DA310).
|
||||
int __fastcall Script_IsTradeSkillLinked(void *L) { return PushLinked(L, g_tradeSkill); }
|
||||
|
||||
// `IsCraftLinked()` — the Craft window's twin.
|
||||
int __fastcall Script_IsCraftLinked(void *L) { return PushLinked(L, g_craft); }
|
||||
|
||||
// `GetTradeSkillLine()`: 3.3.5 answers with the linker's ranks.
|
||||
int __fastcall Script_GetTradeSkillLine(void *L) {
|
||||
const int pushed = reinterpret_cast<Script_t>(Offsets::FUN_SCRIPT_GET_TRADE_SKILL_LINE)(L);
|
||||
if (ShowsOtherLink(g_tradeSkill, TradeSkillOpen())) {
|
||||
Game::Lua::SetTop(L, -3); // the player's rank and maximum
|
||||
Game::Lua::PushNumber(L, g_tradeSkill.rank);
|
||||
Game::Lua::PushNumber(L, g_tradeSkill.maxRank);
|
||||
}
|
||||
return pushed;
|
||||
}
|
||||
|
||||
// `GetCraftDisplaySkillLine()`: the engine reads the line from the player's
|
||||
// own list, which may not hold it.
|
||||
int __fastcall Script_GetCraftDisplaySkillLine(void *L) {
|
||||
const Shown &s = g_craft;
|
||||
const uint32_t open = CraftOpen();
|
||||
const char *name = open == kCraftTypeEnscribe && ShowsOtherLink(s, open)
|
||||
? DBC::LocalizedField(Offsets::VAR_SKILL_LINE_RECORDS,
|
||||
Offsets::VAR_SKILL_LINE_COUNT, s.skillLine,
|
||||
Offsets::OFF_SKILL_LINE_NAME)
|
||||
: nullptr;
|
||||
if (name == nullptr)
|
||||
return reinterpret_cast<Script_t>(Offsets::FUN_SCRIPT_GET_CRAFT_DISPLAY_SKILL_LINE)(L);
|
||||
Game::Lua::PushString(L, name);
|
||||
Game::Lua::PushNumber(L, s.rank);
|
||||
Game::Lua::PushNumber(L, s.maxRank);
|
||||
return 3;
|
||||
}
|
||||
|
||||
// `GetCraftName()`: the engine names the spell the player keeps for the type,
|
||||
// which may be none.
|
||||
int __fastcall Script_GetCraftName(void *L) {
|
||||
const char *name = ShowsOtherLink(g_craft, CraftOpen()) ? SpellName(g_craft.spellID) : nullptr;
|
||||
if (name == nullptr)
|
||||
return reinterpret_cast<Script_t>(Offsets::FUN_SCRIPT_GET_CRAFT_NAME)(L);
|
||||
Game::Lua::PushString(L, name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// --- Documentation ------------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kLinkedRets[] = {
|
||||
Game::Doc::Opt("isLinked", "number", nullptr,
|
||||
"1 when the window shows a profession from a link, else nil."),
|
||||
Game::Doc::Opt("linkedPlayerName", "string", nullptr,
|
||||
"The name of the player whose profession the window shows."),
|
||||
};
|
||||
const Game::Doc::Function kIsTradeSkillLinked{
|
||||
"Returns whether the trade-skill window shows a profession from a link, and whose.", {},
|
||||
kLinkedRets, "TradeSkillGlobals"};
|
||||
const Game::Doc::Function kIsCraftLinked{
|
||||
"Returns whether the craft window shows a profession from a link, and whose.", {},
|
||||
kLinkedRets, "TradeSkillGlobals", true};
|
||||
|
||||
void Register() {
|
||||
static const uint8_t kRankRead[33] = {
|
||||
0xE8, 0xC3, 0xD7, 0x0E, 0x00, // call FindSkillIndex
|
||||
0x85, 0xC0, // test eax, eax
|
||||
0x7C, 0x16, // jl -> xor eax, eax
|
||||
0x8D, 0x94, 0x40, 0x13, 0x02, 0x00, 0x00, // lea edx, [eax+eax*2+0x213]
|
||||
0x8B, 0x45, 0xF8, // mov eax, [ebp-8] (player)
|
||||
0x8B, 0x88, 0x68, 0x0E, 0x00, 0x00, // mov ecx, [eax+0xE68]
|
||||
0x0F, 0xB7, 0x04, 0x91, // movzx eax, word [ecx+edx*4]
|
||||
0xEB, 0x02, // jmp past
|
||||
0x33, 0xC0, // xor eax, eax
|
||||
};
|
||||
|
||||
g_tradeSkillReady =
|
||||
Common::RedirectCall(Offsets::CALL_GAME_SHUTDOWN_TRADE_SKILL,
|
||||
Offsets::FUN_SHUTDOWN_TRADE_SKILL,
|
||||
reinterpret_cast<const void *>(&ShutdownTradeSkill)) &&
|
||||
Common::RedirectCall(Offsets::CALL_TRY_CAST_TOGGLE_TRADE_SKILL,
|
||||
Offsets::FUN_TOGGLE_TRADE_SKILL,
|
||||
reinterpret_cast<const void *>(&CastTradeSkillThunk)) &&
|
||||
Common::RedirectCall(Offsets::CALL_BUILD_TRADE_SKILL_SPELLS,
|
||||
Offsets::FUN_PLAYER_FIND_SKILL_LINE_SPELLS,
|
||||
reinterpret_cast<const void *>(&TradeSkillSpells)) &&
|
||||
Common::RedirectCall(Offsets::CALL_BUILD_TRADE_SKILL_ABILITY,
|
||||
Offsets::FUN_SKILL_LINE_ABILITY_LOOKUP,
|
||||
reinterpret_cast<const void *>(&TradeSkillAbility)) &&
|
||||
Common::ReplaceWithCall(Offsets::SITE_BUILD_TRADE_SKILL_RANK, kRankRead, sizeof kRankRead,
|
||||
reinterpret_cast<const void *>(&TradeSkillRank));
|
||||
|
||||
g_craftReady =
|
||||
Common::RedirectCall(Offsets::CALL_GAME_SHUTDOWN_CRAFT, Offsets::FUN_SHUTDOWN_CRAFT,
|
||||
reinterpret_cast<const void *>(&ShutdownCraft)) &&
|
||||
Common::RedirectCall(Offsets::CALL_TRY_CAST_TOGGLE_CRAFT, Offsets::FUN_TOGGLE_CRAFT,
|
||||
reinterpret_cast<const void *>(&CastCraftThunk)) &&
|
||||
Common::RedirectCall(Offsets::CALL_BUILD_CRAFT_SPELLS, Offsets::FUN_PLAYER_GET_TAB_SPELLS,
|
||||
reinterpret_cast<const void *>(&CraftSpells)) &&
|
||||
Common::RedirectCall(Offsets::CALL_BUILD_CRAFT_ABILITY,
|
||||
Offsets::FUN_UNIT_FIND_SKILL_LINE_ABILITY,
|
||||
reinterpret_cast<const void *>(&CraftAbility)) &&
|
||||
Common::RedirectCall(Offsets::CALL_BUILD_CRAFT_RANK, Offsets::FUN_PLAYER_GET_SKILL_TOTAL,
|
||||
reinterpret_cast<const void *>(&CraftRank));
|
||||
|
||||
Game::Lua::RegisterGlobalFunction("IsTradeSkillLinked", &Script_IsTradeSkillLinked,
|
||||
&kIsTradeSkillLinked);
|
||||
Game::Lua::RegisterGlobalFunction("IsCraftLinked", &Script_IsCraftLinked, &kIsCraftLinked);
|
||||
Game::Lua::RegisterGlobalFunction("GetTradeSkillLine", &Script_GetTradeSkillLine);
|
||||
Game::Lua::RegisterGlobalFunction("GetCraftDisplaySkillLine", &Script_GetCraftDisplaySkillLine);
|
||||
Game::Lua::RegisterGlobalFunction("GetCraftName", &Script_GetCraftName);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
int TradeSkillSpell() { return TradeSkillOpen() != 0 ? g_tradeSkill.spellID : 0; }
|
||||
|
||||
int CraftSpell() { return CraftOpen() != 0 ? g_craft.spellID : 0; }
|
||||
|
||||
void ShowLinked(int spellID, uint64_t guid, const char *bits, int rank, int maxRank) {
|
||||
if (Unit::Identity::PlayerObject() == nullptr)
|
||||
return;
|
||||
const uint8_t *linker = Unit::Identity::PlayerInfoRecord(guid);
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
if (linker == nullptr || record == nullptr ||
|
||||
Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_EFFECT) !=
|
||||
Offsets::SPELL_EFFECT_TRADE_SKILL)
|
||||
return;
|
||||
const uint32_t race = Game::Read<uint32_t>(linker, Offsets::OFF_PLAYER_INFO_RACE);
|
||||
const uint32_t classId = Game::Read<uint32_t>(linker, Offsets::OFF_PLAYER_INFO_CLASS);
|
||||
const uint8_t *ability = Ability(race, classId, static_cast<uint32_t>(spellID));
|
||||
if (ability == nullptr)
|
||||
return;
|
||||
const uint32_t skillLine = Game::Read<uint32_t>(ability, Offsets::OFF_SLA_SKILL_ID);
|
||||
if (DBC::Record(Offsets::VAR_SKILL_LINE_RECORDS, Offsets::VAR_SKILL_LINE_COUNT, skillLine) ==
|
||||
nullptr)
|
||||
return;
|
||||
const uint8_t *rows[Link::MAX_ABILITIES];
|
||||
const int count = Link::Abilities(skillLine, rows);
|
||||
if (count == 0 || static_cast<size_t>(Link::BitChars(count)) != std::strlen(bits))
|
||||
return;
|
||||
|
||||
const uint32_t craftType =
|
||||
Game::Read<uint32_t>(record, Offsets::OFF_SPELL_RECORD_EFFECT_MISC_VALUE);
|
||||
if (!(craftType == 0 ? g_tradeSkillReady : g_craftReady))
|
||||
return;
|
||||
Shown &s = craftType == 0 ? g_tradeSkill : g_craft;
|
||||
s.spells.clear();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const int spell = Game::Read<int>(rows[i], Offsets::OFF_SLA_SPELL_ID);
|
||||
if (Link::IsSet(bits, i) && InPlayerList(spell, craftType))
|
||||
s.spells.push_back(spell);
|
||||
}
|
||||
const uint32_t n = static_cast<uint32_t>(s.spells.size());
|
||||
s.list = {n, n, s.spells.data(), 0};
|
||||
s.spellID = spellID;
|
||||
s.guid = guid;
|
||||
s.linked = true;
|
||||
s.opened = craftType == 0 ? skillLine : craftType;
|
||||
s.skillLine = skillLine;
|
||||
s.race = race;
|
||||
s.classId = classId;
|
||||
s.rank = rank;
|
||||
s.maxRank = maxRank;
|
||||
|
||||
if (craftType == 0) {
|
||||
reinterpret_cast<Void_t>(Offsets::FUN_TRADE_SKILL_STOP_RECAST)();
|
||||
if (TradeSkillOpen() == skillLine)
|
||||
Game::Ref<uint32_t>(Offsets::VAR_CURRENT_TRADESKILL_LINE) = 0;
|
||||
reinterpret_cast<Toggle_t>(Offsets::FUN_TOGGLE_TRADE_SKILL)(skillLine);
|
||||
} else {
|
||||
if (CraftOpen() == craftType)
|
||||
Game::Ref<uint32_t>(Offsets::VAR_CRAFT_WINDOW_STATE) = 0;
|
||||
reinterpret_cast<Toggle_t>(Offsets::FUN_TOGGLE_CRAFT)(craftType);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace TradeSkill::Window
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Whose profession the TradeSkill and Craft windows show. See Window.cpp.
|
||||
namespace TradeSkill::Window {
|
||||
|
||||
// The profession spell the TradeSkill window shows (3.3.5's
|
||||
// CGTradeSkillInfo::m_spellID), or 0 while the window is closed.
|
||||
int TradeSkillSpell();
|
||||
|
||||
// The Craft window's twin of TradeSkillSpell.
|
||||
int CraftSpell();
|
||||
|
||||
// 3.3.5's CGTradeSkillInfo::SetTradeSkill for a received link: shows
|
||||
// profession `spellID` of player `guid`, at `rank` / `maxRank`, with the
|
||||
// rows set in the link's `bits`, in the window the spell opens when cast.
|
||||
// The name cache must hold `guid`.
|
||||
void ShowLinked(int spellID, uint64_t guid, const char *bits, int rank, int maxRank);
|
||||
|
||||
} // namespace TradeSkill::Window
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
// `CreateColor(r,g,b,a)` and assign the result to globals like
|
||||
// `ACHIEVEMENT_COLOR`. Vanilla 1.12 has no `GlobalColor.dbc` (the
|
||||
// table was added in TBC) and no `C_UIColor` namespace, so we embed
|
||||
// a Blizzard-provided snapshot (BC Classic 2.5.5 build 67323) and
|
||||
// a Blizzard-provided snapshot (build 1.60.1.70707) and
|
||||
// register the function ourselves. The companion addon
|
||||
// `!!!ClassicAPI/Util/Color.lua` defines `ColorMixin` and `CreateColor`
|
||||
// in pure Lua — same as `SharedXMLBase/Color.lua` does on modern —
|
||||
|
||||
+342
-195
@@ -15,7 +15,8 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Snapshot of `GlobalColor.dbc` from BC Classic 2.5.5 (build 67323).
|
||||
// Snapshot of `GlobalColor.dbc` from build 1.60.1.70707, in the table's
|
||||
// own row order.
|
||||
// Vanilla 1.12 has no `GlobalColor.dbc`, so we embed Blizzard's modern
|
||||
// color table verbatim and expose it via `C_UIColor.GetColors`. The
|
||||
// addon companion (`!!!ClassicAPI/Util/Color.lua`) loops it the same
|
||||
@@ -35,9 +36,11 @@
|
||||
// b = ( v & 0xFF) / 255.0
|
||||
//
|
||||
// Duplicate baseTags in the source DBC (PURE_RED_COLOR, INVASION_*,
|
||||
// AREA_DESCRIPTION_FONT_COLOR, REFUND_TOOLTIP_COLOR) have been
|
||||
// deduplicated keeping the higher-ID entry — that's the value modern
|
||||
// WoW sees after iterating `ipairs(DBColors)` to its end.
|
||||
// AREA_DESCRIPTION_FONT_COLOR, REFUND_TOOLTIP_COLOR,
|
||||
// COMPACT_UNIT_FRAME_FRIENDLY_HEALTH_COLOR,
|
||||
// CHARACTER_FRAME_SIDE_PANEL_SKILL_COLOR) keep only their last row, at
|
||||
// that row's place — that's the value modern WoW sees after iterating
|
||||
// `ipairs(DBColors)` to its end.
|
||||
|
||||
namespace UI::ColorData {
|
||||
|
||||
@@ -47,197 +50,341 @@ struct Entry {
|
||||
};
|
||||
|
||||
constexpr Entry kColors[] = {
|
||||
{"NORMAL_FONT_COLOR", -12032},
|
||||
{"WHITE_FONT_COLOR", -1},
|
||||
{"HIGHLIGHT_FONT_COLOR", -1},
|
||||
{"RED_FONT_COLOR", -59111},
|
||||
{"DIM_RED_FONT_COLOR", -3401447},
|
||||
{"DULL_RED_FONT_COLOR", -4250074},
|
||||
{"BLUE_FONT_COLOR", -16728077},
|
||||
{"GREEN_FONT_COLOR", -15073511},
|
||||
{"GRAY_FONT_COLOR", -8355712},
|
||||
{"YELLOW_FONT_COLOR", -256},
|
||||
{"LIGHTYELLOW_FONT_COLOR", -103},
|
||||
{"DARKYELLOW_FONT_COLOR", -12032},
|
||||
{"ORANGE_FONT_COLOR", -32704},
|
||||
{"PASSIVE_SPELL_FONT_COLOR", -3890432},
|
||||
{"BATTLENET_FONT_COLOR", -8206849},
|
||||
{"TRANSMOGRIFY_FONT_COLOR", -32513},
|
||||
{"DISABLED_FONT_COLOR", -8355712},
|
||||
{"WARNING_FONT_COLOR", -47104},
|
||||
{"BRIGHTBLUE_FONT_COLOR", -10044417},
|
||||
{"LIGHTBLUE_FONT_COLOR", -7885825},
|
||||
{"LIGHTGRAY_FONT_COLOR", -6710887},
|
||||
{"GOLD_FONT_COLOR", -858471},
|
||||
{"PAPER_FRAME_EXPANDED_COLOR", -1193570},
|
||||
{"PAPER_FRAME_COLLAPSED_COLOR", -2905994},
|
||||
{"PAPER_FRAME_DARK_COLOR", -12573179},
|
||||
{"PAPER_FRAME_TITLE_COLOR", -13303808},
|
||||
{"PAPER_FRAME_TEXT_COLOR", -13303808},
|
||||
{"INVALID_EQUIPMENT_COLOR", -6815744},
|
||||
{"ACTIONBAR_HOTKEY_FONT_COLOR", -3355444},
|
||||
{"ARTIFACT_BAR_COLOR", -1651559},
|
||||
{"WARBOARD_OPTION_TEXT_COLOR", -12122875},
|
||||
{"DEFAULT_CHAT_CHANNEL_COLOR", -16192},
|
||||
{"DIM_GREEN_FONT_COLOR", -12533696},
|
||||
{"BLACK_FONT_COLOR", -16777216},
|
||||
{"LINK_FONT_COLOR", -10044417},
|
||||
{"SEPIA_COLOR", -7315416},
|
||||
{"HIGHLIGHT_LIGHT_BLUE", -12676609},
|
||||
{"CORRUPTION_COLOR", -6984239},
|
||||
{"LORE_TEXT_BODY_COLOR", -3488067},
|
||||
{"RARE_MISSION_COLOR", -16776374},
|
||||
{"TUTORIAL_FONT_COLOR", -3355444},
|
||||
{"DARKGRAY_COLOR", -10066330},
|
||||
{"SCENARIO_STAGE_COLOR", 16771502},
|
||||
{"SCENARIO_SUBTITLE_COLOR", -11167},
|
||||
{"CHALLENGE_MODE_TOAST_TITLE_COLOR", -2175844},
|
||||
{"TRADESKILL_EXPERIENCE_COLOR", -10813663},
|
||||
{"SUBSCRIPTION_INTERSTITIAL_COLOR", -2501170},
|
||||
{"GLUE_DIALOG_FONT_COLOR", -13303808},
|
||||
{"NEW_FEATURE_SHADOW_COLOR", -11370241},
|
||||
{"QUEST_OBJECTIVE_FONT_COLOR", -3355444},
|
||||
{"QUEST_OBJECTIVE_HIGHLIGHT_FONT_COLOR", -1},
|
||||
{"QUEST_OBJECTIVE_DISABLED_FONT_COLOR", -8421505},
|
||||
{"QUEST_OBJECTIVE_DISABLED_HIGHLIGHT_FONT_COLOR", -6710887},
|
||||
{"AREA_NAME_FONT_COLOR", -5438},
|
||||
{"FACTION_RED_COLOR", -3388360},
|
||||
{"FACTION_ORANGE_COLOR", -4242176},
|
||||
{"FACTION_YELLOW_COLOR", -1658112},
|
||||
{"FACTION_GREEN_COLOR", -16738023},
|
||||
{"FRIENDS_BNET_NAME_COLOR", -8206849},
|
||||
{"FRIENDS_BNET_BACKGROUND_COLOR", 201377264},
|
||||
{"FRIENDS_WOW_NAME_COLOR", -73380},
|
||||
{"FRIENDS_WOW_BACKGROUND_COLOR", 218092032},
|
||||
{"FRIENDS_GRAY_COLOR", -8616822},
|
||||
{"FRIENDS_OFFLINE_BACKGROUND_COLOR", 210866577},
|
||||
{"COMMON_GRAY_COLOR", -5723992},
|
||||
{"UNCOMMON_GREEN_COLOR", -15355136},
|
||||
{"RARE_BLUE_COLOR", -16739854},
|
||||
{"EPIC_PURPLE_COLOR", -3652102},
|
||||
{"LEGENDARY_ORANGE_COLOR", -32768},
|
||||
{"ARTIFACT_GOLD_COLOR", -1651584},
|
||||
{"HEIRLOOM_BLUE_COLOR", -16724737},
|
||||
{"PLAYER_FACTION_COLOR_HORDE", -1701104},
|
||||
{"PLAYER_FACTION_COLOR_ALLIANCE", -11905816},
|
||||
{"TOOLTIP_DEFAULT_COLOR", -1},
|
||||
{"TOOLTIP_DEFAULT_BACKGROUND_COLOR", -15263952},
|
||||
{"KYRIAN_BLUE_COLOR", -13982977},
|
||||
{"VENTHYR_RED_COLOR", -1831666},
|
||||
{"NIGHT_FAE_BLUE_COLOR", -8342019},
|
||||
{"NECROLORD_GREEN_COLOR", -15218588},
|
||||
{"ADVENTURES_HEALING_GREEN", -16711918},
|
||||
{"ADVENTURES_BUFF_BLUE", -16721665},
|
||||
{"ADVENTURES_COMBAT_LOG_GREY", -4605511},
|
||||
{"ADVENTURES_COMBAT_LOG_BLUE", -11620905},
|
||||
{"ADVENTURES_COMBAT_LOG_ORANGE", -3775413},
|
||||
{"ADVENTURES_COMBAT_LOG_YELLOW", -11776},
|
||||
{"ENCOUNTER_JOURNAL_SCROLL_BAR_BACKGROUND_COLOR", 1073684756},
|
||||
{"RUNEFORGE_LEGEDARY_SPEC_COLOR", -4747414},
|
||||
{"SOULBIND_CONDUIT_ENHANCED_COLOR", -16724737},
|
||||
{"VERY_DARK_GRAY_COLOR", -14277082},
|
||||
{"VERY_LIGHT_GRAY_COLOR", -1710619},
|
||||
{"PURE_GREEN_COLOR", -16711936},
|
||||
{"PURE_RED_COLOR", -65536},
|
||||
{"TRIVIAL_DIFFICULTY_COLOR", -8355712},
|
||||
{"EASY_DIFFICULTY_COLOR", -12533696},
|
||||
{"FAIR_DIFFICULTY_COLOR", -256},
|
||||
{"DIFFICULT_DIFFICULTY_COLOR", -32704},
|
||||
{"IMPOSSIBLE_DIFFICULTY_COLOR", -57312},
|
||||
{"ACHIEVEMENT_INCOMPLETE_COLOR", -256},
|
||||
{"ACHIEVEMENT_COMPLETE_COLOR", -32704},
|
||||
{"NOT_ON_THREAT_COLOR", -1},
|
||||
{"NO_THREAT_COLOR", -5197648},
|
||||
{"YELLOW_THREAT_COLOR", -137},
|
||||
{"ORANGE_THREAT_COLOR", -26368},
|
||||
{"RED_THREAT_COLOR", -65536},
|
||||
{"ITEM_POOR_COLOR", -6447715},
|
||||
{"ITEM_STANDARD_COLOR", -1},
|
||||
{"ITEM_GOOD_COLOR", -14745856},
|
||||
{"ITEM_WOW_TOKEN_COLOR", -16724737},
|
||||
{"ITEM_EPIC_COLOR", -6081042},
|
||||
{"ITEM_LEGENDARY_COLOR", -32768},
|
||||
{"ITEM_ARTIFACT_COLOR", -1651584},
|
||||
{"ITEM_SCALING_STAT_COLOR", -16724737},
|
||||
{"ITEM_SUPERIOR_COLOR", -16748323},
|
||||
{"ERROR_COLOR", -57312},
|
||||
{"CONTEXT_FEEDBACK_COLOR", -7820545},
|
||||
{"FRIENDLY_STATUS_COLOR", -16711936},
|
||||
{"NEUTRAL_STATUS_COLOR", -256},
|
||||
{"HOSTILE_STATUS_COLOR", -65536},
|
||||
{"SET_ITEM_COLOR", -105},
|
||||
{"INCREASE_STAT_COLOR", -16711936},
|
||||
{"DECREASE_STAT_COLOR", -57312},
|
||||
{"GLYPH_LINK_COLOR", -10044417},
|
||||
{"TOY_LINK_COLOR", -10044417},
|
||||
{"AZERITE_ESSENCE_COLOR", -10044417},
|
||||
{"SPELL_LINK_COLOR", -9316865},
|
||||
{"EJ_SPELL_COLOR", -14067245},
|
||||
{"ENCHANT_COLOR", -12288},
|
||||
{"TALENT_LINK_COLOR", -11626761},
|
||||
{"INSTANCE_LOCK_LINK_COLOR", -32768},
|
||||
{"JOURNAL_LINK_COLOR", -10044417},
|
||||
{"BATTLEPET_ABILITY_LINK_COLOR", -11626761},
|
||||
{"ENHANCED_CONDUIT_COLOR", -16724737},
|
||||
{"SPELL_SUBTEXT_COLOR", -10931456},
|
||||
{"TRANSMOGRIFY_COLOR", -32513},
|
||||
{"BIND_TRADE_TOOLTIP_COLOR", -16724737},
|
||||
{"USED_IN_TRADESKILL_COLOR", -10044417},
|
||||
{"REFUND_TOOLTIP_COLOR", -16724737},
|
||||
{"AZERITE_SUBTEXT_COLOR", -10044417},
|
||||
{"FRAMESTACK_FRAME_COLOR", -10053121},
|
||||
{"FRAMESTACK_HIDDEN_COLOR", -10066330},
|
||||
{"FRAMESTACK_REGION_COLOR", -7807523},
|
||||
{"ACHIEVEMENT_COLOR", -256},
|
||||
{"FRIENDS_BROADCAST_TIME_COLOR", -12353112},
|
||||
{"FRIENDS_OTHER_NAME_COLOR", -8682359},
|
||||
{"DEFAULT_MATERIAL_TEXT_COLOR", -13754609},
|
||||
{"DEFAULT_MATERIAL_TITLETEXT_COLOR", -16777216},
|
||||
{"STONE_MATERIAL_TITLETEXT_COLOR", -1191680},
|
||||
{"STONE_MATERIAL_TEXT_COLOR", -1},
|
||||
{"PARCHMENT_MATERIAL_TEXT_COLOR", -13754609},
|
||||
{"PARCHMENT_MATERIAL_TITLETEXT_COLOR", -16777216},
|
||||
{"MARBLE_MATERIAL_TEXT_COLOR", -16777216},
|
||||
{"MARBLE_MATERIAL_TITLETEXT_COLOR", -1191680},
|
||||
{"SILVER_MATERIAL_TITLETEXT_COLOR", -1191680},
|
||||
{"BRONZE_MATERIAL_TITLETEXT_COLOR", -1191680},
|
||||
{"SILVER_MATERIAL_TEXT_COLOR", -14737633},
|
||||
{"BRONZE_MATERIAL_TEXT_COLOR", -13754609},
|
||||
{"PARCHMENTLARGE_MATERIAL_TEXT_COLOR", -14417920},
|
||||
{"PARCHMENTLARGE_MATERIAL_TITLETEXT_COLOR", -13303808},
|
||||
{"PROGENITOR_MATERIAL_TEXT_COLOR", -1},
|
||||
{"PROGENITOR_MATERIAL_TITLETEXT_COLOR", -1},
|
||||
{"ARENA_NAME_FONT_COLOR", -4670},
|
||||
{"AREA_DESCRIPTION_FONT_COLOR", -1},
|
||||
{"INVASION_FONT_COLOR", -3670272},
|
||||
{"INVASION_DESCRIPTION_FONT_COLOR", -2039},
|
||||
{"INACTIVE_COLOR", -8355712},
|
||||
{"EDIT_MODE_LAYOUT_LINK_COLOR", -10044417},
|
||||
{"LIGHTGREEN_FONT_COLOR", -10027168},
|
||||
{"EDIT_MODE_GRID_LINE_COLOR", 1073741823},
|
||||
{"EDIT_MODE_GRID_CENTER_LINE_COLOR", 2143831546},
|
||||
{"PANEL_BACKGROUND_COLOR", -870375903},
|
||||
{"HEALTHBAR_MY_HEAL_PREDICTION_COLOR", -16723005},
|
||||
{"HEALTHBAR_OTHER_HEAL_PREDICTION_COLOR", -16735858},
|
||||
{"CINEMATIC_SUBTITLES_BLACK_BACKGROUND_COLOR", -16777216},
|
||||
{"CINEMATIC_SUBTITLES_LIGHT_BACKGROUND_COLOR", -1},
|
||||
{"HEALTHBAR_MY_HEAL_PREDICTION_GRADIENT_COLOR1", -16229048},
|
||||
{"HEALTHBAR_MY_HEAL_PREDICTION_GRADIENT_COLOR2", -16021399},
|
||||
{"HEALTHBAR_OTHER_HEAL_PREDICTION_GRADIENT_COLOR1", -16042709},
|
||||
{"HEALTHBAR_OTHER_HEAL_PREDICTION_GRADIENT_COLOR2", -15378104},
|
||||
{"DEBUFF_TYPE_MAGIC_COLOR", -16743937},
|
||||
{"DEBUFF_TYPE_POISON_COLOR", -8665344},
|
||||
{"DEBUFF_TYPE_CURSE_COLOR", -6355228},
|
||||
{"DEBUFF_TYPE_BLEED_COLOR", -4718577},
|
||||
{"DEBUFF_TYPE_DISEASE_COLOR", -955895},
|
||||
{"DEBUFF_TYPE_NONE_COLOR", -3407872},
|
||||
// ClassicAPI extension — Enrage was dropped from GlobalColor.dbc in
|
||||
// BC Classic. We re-add it so `GetAuraDispelTypeColor("Enrage")`
|
||||
// resolves to a proper ColorMixin via the same Color.lua pipeline.
|
||||
// rgb(1.00, 0.55, 0.00) → 0xFFFF8C00.
|
||||
{"DEBUFF_TYPE_ENRAGE_COLOR", -29696},
|
||||
{"VISITABLE_URL_DEFAULT_CHAT_LINK_COLOR", -16733697},
|
||||
{"EVENTTRACE_SECRET_COLOR", -7798904},
|
||||
{"NORMAL_FONT_COLOR", -11776},
|
||||
{"WHITE_FONT_COLOR", -1},
|
||||
{"HIGHLIGHT_FONT_COLOR", -1},
|
||||
{"RED_FONT_COLOR", -57312},
|
||||
{"DIM_RED_FONT_COLOR", -3401447},
|
||||
{"DULL_RED_FONT_COLOR", -4250074},
|
||||
{"BLUE_FONT_COLOR", -16728077},
|
||||
{"GREEN_FONT_COLOR", -15073511},
|
||||
{"GRAY_FONT_COLOR", -8355712},
|
||||
{"YELLOW_FONT_COLOR", -256},
|
||||
{"LIGHTYELLOW_FONT_COLOR", -103},
|
||||
{"DARKYELLOW_FONT_COLOR", -12032},
|
||||
{"ORANGE_FONT_COLOR", -32704},
|
||||
{"PASSIVE_SPELL_FONT_COLOR", -3890432},
|
||||
{"BATTLENET_FONT_COLOR", -8206849},
|
||||
{"TRANSMOGRIFY_FONT_COLOR", -32513},
|
||||
{"DISABLED_FONT_COLOR", -8355712},
|
||||
{"WARNING_FONT_COLOR", -47104},
|
||||
{"BRIGHTBLUE_FONT_COLOR", -10044417},
|
||||
{"LIGHTBLUE_FONT_COLOR", -7885825},
|
||||
{"LIGHTGRAY_FONT_COLOR", -6710887},
|
||||
{"GOLD_FONT_COLOR", -858471},
|
||||
{"PAPER_FRAME_EXPANDED_COLOR", -1193570},
|
||||
{"PAPER_FRAME_COLLAPSED_COLOR", -2905994},
|
||||
{"PAPER_FRAME_DARK_COLOR", -12573179},
|
||||
{"PAPER_FRAME_TITLE_COLOR", -13303808},
|
||||
{"PAPER_FRAME_TEXT_COLOR", -13303808},
|
||||
{"INVALID_EQUIPMENT_COLOR", -6815744},
|
||||
{"ACTIONBAR_HOTKEY_FONT_COLOR", -3355444},
|
||||
{"ARTIFACT_BAR_COLOR", -1651559},
|
||||
{"WARBOARD_OPTION_TEXT_COLOR", -12122875},
|
||||
{"DEFAULT_CHAT_CHANNEL_COLOR", -16192},
|
||||
{"DIM_GREEN_FONT_COLOR", -12533696},
|
||||
{"BLACK_FONT_COLOR", -16777216},
|
||||
{"LINK_FONT_COLOR", -10044417},
|
||||
{"SEPIA_COLOR", -7315416},
|
||||
{"HIGHLIGHT_LIGHT_BLUE", -12676609},
|
||||
{"CORRUPTION_COLOR", -6984239},
|
||||
{"LORE_TEXT_BODY_COLOR", -3488067},
|
||||
{"RARE_MISSION_COLOR", -16776374},
|
||||
{"TUTORIAL_FONT_COLOR", -3355444},
|
||||
{"DARKGRAY_COLOR", -10066330},
|
||||
{"SCENARIO_STAGE_COLOR", -5714},
|
||||
{"SCENARIO_SUBTITLE_COLOR", -11167},
|
||||
{"CHALLENGE_MODE_TOAST_TITLE_COLOR", -2175844},
|
||||
{"TRADESKILL_EXPERIENCE_COLOR", -10813663},
|
||||
{"SUBSCRIPTION_INTERSTITIAL_COLOR", -2501170},
|
||||
{"GLUE_DIALOG_FONT_COLOR", -13303808},
|
||||
{"NEW_FEATURE_SHADOW_COLOR", -11370241},
|
||||
{"QUEST_OBJECTIVE_FONT_COLOR", -3355444},
|
||||
{"QUEST_OBJECTIVE_HIGHLIGHT_FONT_COLOR", -1},
|
||||
{"QUEST_OBJECTIVE_DISABLED_FONT_COLOR", -8421505},
|
||||
{"QUEST_OBJECTIVE_DISABLED_HIGHLIGHT_FONT_COLOR", -6710887},
|
||||
{"AREA_NAME_FONT_COLOR", -5438},
|
||||
{"FACTION_RED_COLOR", -3388104},
|
||||
{"FACTION_ORANGE_COLOR", -4242176},
|
||||
{"FACTION_YELLOW_COLOR", -1658112},
|
||||
{"FACTION_GREEN_COLOR", -16738022},
|
||||
{"FRIENDS_BNET_NAME_COLOR", -8206849},
|
||||
{"FRIENDS_BNET_BACKGROUND_COLOR", 201377264},
|
||||
{"FRIENDS_WOW_NAME_COLOR", -73380},
|
||||
{"FRIENDS_WOW_BACKGROUND_COLOR", 218092032},
|
||||
{"FRIENDS_GRAY_COLOR", -8616822},
|
||||
{"FRIENDS_OFFLINE_BACKGROUND_COLOR", 210866577},
|
||||
{"COMMON_GRAY_COLOR", -5723992},
|
||||
{"UNCOMMON_GREEN_COLOR", -15355136},
|
||||
{"RARE_BLUE_COLOR", -16739854},
|
||||
{"EPIC_PURPLE_COLOR", -3652102},
|
||||
{"LEGENDARY_ORANGE_COLOR", -32768},
|
||||
{"ARTIFACT_GOLD_COLOR", -1651584},
|
||||
{"HEIRLOOM_BLUE_COLOR", -16724737},
|
||||
{"PLAYER_FACTION_COLOR_HORDE", -54988},
|
||||
{"PLAYER_FACTION_COLOR_ALLIANCE", -16732688},
|
||||
{"TOOLTIP_DEFAULT_COLOR", -1},
|
||||
{"TOOLTIP_DEFAULT_BACKGROUND_COLOR", -15263952},
|
||||
{"KYRIAN_BLUE_COLOR", -13982977},
|
||||
{"VENTHYR_RED_COLOR", -1831666},
|
||||
{"NIGHT_FAE_BLUE_COLOR", -8342019},
|
||||
{"NECROLORD_GREEN_COLOR", -15218588},
|
||||
{"ADVENTURES_HEALING_GREEN", -16711918},
|
||||
{"ADVENTURES_BUFF_BLUE", -16721665},
|
||||
{"ADVENTURES_COMBAT_LOG_GREY", -4605511},
|
||||
{"ADVENTURES_COMBAT_LOG_BLUE", -11620905},
|
||||
{"ADVENTURES_COMBAT_LOG_ORANGE", -3775413},
|
||||
{"ADVENTURES_COMBAT_LOG_YELLOW", -11776},
|
||||
{"ENCOUNTER_JOURNAL_SCROLL_BAR_BACKGROUND_COLOR", 1073684756},
|
||||
{"RUNEFORGE_LEGEDARY_SPEC_COLOR", -4747414},
|
||||
{"SOULBIND_CONDUIT_ENHANCED_COLOR", -16724737},
|
||||
{"VERY_DARK_GRAY_COLOR", -14277082},
|
||||
{"VERY_LIGHT_GRAY_COLOR", -1710619},
|
||||
{"PURE_GREEN_COLOR", -16711936},
|
||||
{"PURE_RED_COLOR", -65536},
|
||||
{"TRIVIAL_DIFFICULTY_COLOR", -8355712},
|
||||
{"EASY_DIFFICULTY_COLOR", -12533696},
|
||||
{"FAIR_DIFFICULTY_COLOR", -256},
|
||||
{"DIFFICULT_DIFFICULTY_COLOR", -32704},
|
||||
{"IMPOSSIBLE_DIFFICULTY_COLOR", -57312},
|
||||
{"ACHIEVEMENT_INCOMPLETE_COLOR", -256},
|
||||
{"ACHIEVEMENT_COMPLETE_COLOR", -32704},
|
||||
{"NOT_ON_THREAT_COLOR", -1},
|
||||
{"NO_THREAT_COLOR", -5197648},
|
||||
{"YELLOW_THREAT_COLOR", -137},
|
||||
{"ORANGE_THREAT_COLOR", -26368},
|
||||
{"RED_THREAT_COLOR", -65536},
|
||||
{"ITEM_POOR_COLOR", -6447715},
|
||||
{"ITEM_STANDARD_COLOR", -1},
|
||||
{"ITEM_GOOD_COLOR", -14745856},
|
||||
{"ITEM_WOW_TOKEN_COLOR", -16724737},
|
||||
{"ITEM_EPIC_COLOR", -6081042},
|
||||
{"ITEM_LEGENDARY_COLOR", -32768},
|
||||
{"ITEM_ARTIFACT_COLOR", -1651584},
|
||||
{"ITEM_SCALING_STAT_COLOR", -16724737},
|
||||
{"ITEM_SUPERIOR_COLOR", -16748323},
|
||||
{"ERROR_COLOR", -57312},
|
||||
{"CONTEXT_FEEDBACK_COLOR", -7820545},
|
||||
{"FRIENDLY_STATUS_COLOR", -16711936},
|
||||
{"NEUTRAL_STATUS_COLOR", -256},
|
||||
{"HOSTILE_STATUS_COLOR", -65536},
|
||||
{"SET_ITEM_COLOR", -105},
|
||||
{"INCREASE_STAT_COLOR", -16711936},
|
||||
{"DECREASE_STAT_COLOR", -57312},
|
||||
{"GLYPH_LINK_COLOR", -10044417},
|
||||
{"TOY_LINK_COLOR", -10044417},
|
||||
{"AZERITE_ESSENCE_COLOR", -10044417},
|
||||
{"SPELL_LINK_COLOR", -9316865},
|
||||
{"EJ_SPELL_COLOR", -14067245},
|
||||
{"ENCHANT_COLOR", -12288},
|
||||
{"TALENT_LINK_COLOR", -9316865},
|
||||
{"INSTANCE_LOCK_LINK_COLOR", -32768},
|
||||
{"JOURNAL_LINK_COLOR", -10044417},
|
||||
{"BATTLEPET_ABILITY_LINK_COLOR", -11626761},
|
||||
{"ENHANCED_CONDUIT_COLOR", -16724737},
|
||||
{"SPELL_SUBTEXT_COLOR", -10931456},
|
||||
{"TRANSMOGRIFY_COLOR", -32513},
|
||||
{"BIND_TRADE_TOOLTIP_COLOR", -16724737},
|
||||
{"USED_IN_TRADESKILL_COLOR", -10044417},
|
||||
{"AZERITE_SUBTEXT_COLOR", -10044417},
|
||||
{"FRAMESTACK_FRAME_COLOR", -10053121},
|
||||
{"FRAMESTACK_HIDDEN_COLOR", -10066330},
|
||||
{"FRAMESTACK_REGION_COLOR", -7807523},
|
||||
{"ACHIEVEMENT_COLOR", -256},
|
||||
{"FRIENDS_BROADCAST_TIME_COLOR", -12353112},
|
||||
{"FRIENDS_OTHER_NAME_COLOR", -8682359},
|
||||
{"DEFAULT_MATERIAL_TEXT_COLOR", -13754609},
|
||||
{"DEFAULT_MATERIAL_TITLETEXT_COLOR", -16777216},
|
||||
{"STONE_MATERIAL_TITLETEXT_COLOR", -1},
|
||||
{"STONE_MATERIAL_TEXT_COLOR", -1},
|
||||
{"PARCHMENT_MATERIAL_TEXT_COLOR", -13754609},
|
||||
{"PARCHMENT_MATERIAL_TITLETEXT_COLOR", -16777216},
|
||||
{"MARBLE_MATERIAL_TEXT_COLOR", -16777216},
|
||||
{"MARBLE_MATERIAL_TITLETEXT_COLOR", -1191680},
|
||||
{"SILVER_MATERIAL_TITLETEXT_COLOR", -1191680},
|
||||
{"BRONZE_MATERIAL_TITLETEXT_COLOR", -1191680},
|
||||
{"SILVER_MATERIAL_TEXT_COLOR", -1},
|
||||
{"BRONZE_MATERIAL_TEXT_COLOR", -13754609},
|
||||
{"PARCHMENTLARGE_MATERIAL_TEXT_COLOR", -14417920},
|
||||
{"PARCHMENTLARGE_MATERIAL_TITLETEXT_COLOR", -13303808},
|
||||
{"PROGENITOR_MATERIAL_TEXT_COLOR", -1},
|
||||
{"PROGENITOR_MATERIAL_TITLETEXT_COLOR", -1},
|
||||
{"ARENA_NAME_FONT_COLOR", -4670},
|
||||
{"INACTIVE_COLOR", -8355712},
|
||||
{"QUEST_INELIGIBLE_GRAY", -13421773},
|
||||
{"DISABLED_REAGENT_COLOR", -6250336},
|
||||
{"EDIT_MODE_LAYOUT_LINK_COLOR", -10044417},
|
||||
{"LIGHTGREEN_FONT_COLOR", -10027168},
|
||||
{"EDIT_MODE_GRID_LINE_COLOR", 1073741823},
|
||||
{"EDIT_MODE_GRID_CENTER_LINE_COLOR", 2143831546},
|
||||
{"PROFESSION_RECIPE_COLOR", -1909546},
|
||||
{"PANEL_BACKGROUND_COLOR", -870375903},
|
||||
{"LIGHTERBLUE_FONT_COLOR", -3875332},
|
||||
{"PROFESSION_SPEC_COLOR_BLACKSMITHING", -2340082},
|
||||
{"PROFESSION_SPEC_COLOR_JEWELCRAFTING", -4125125},
|
||||
{"PROFESSION_SPEC_COLOR_ENCHANTING", -5541661},
|
||||
{"PROFESSION_SPEC_COLOR_TAILORING", -14335797},
|
||||
{"PROFESSION_SPEC_COLOR_HERBALISM", -14575033},
|
||||
{"PROFESSION_SPEC_COLOR_MINING", -8814464},
|
||||
{"PROFESSION_SPEC_COLOR_SKINNING", -6928350},
|
||||
{"PROFESSION_SPEC_COLOR_INSCRIPTION", -11600427},
|
||||
{"PROFESSION_SPEC_COLOR_COOKING", -2321310},
|
||||
{"PROFESSION_SPEC_COLOR_FISHING", -9197636},
|
||||
{"PROFESSION_SPEC_COLOR_FIRST_AID", -1},
|
||||
{"PROFESSION_SPEC_COLOR_ARCHAEOLOGY", -4549289},
|
||||
{"PROFESSION_SPEC_COLOR_LEATHERWORKING", -10077659},
|
||||
{"PROFESSION_SPEC_COLOR_ALCHEMY", -5761091},
|
||||
{"PROFESSION_SPEC_COLOR_ENGINEERING", -1923018},
|
||||
{"EXPEDITION_MAJOR_FACTION_COLOR", -3924421},
|
||||
{"VALDRAKKEN_MAJOR_FACTION_COLOR", -5863442},
|
||||
{"CENTAUR_MAJOR_FACTION_COLOR", -11776},
|
||||
{"TUSKARR_MAJOR_FACTION_COLOR", -15891981},
|
||||
{"CAMPAIGN_COMPLETE_COLOR", -8520},
|
||||
{"DRAGONFLIGHT_RED_COLOR", 16736609},
|
||||
{"DRAGONFLIGHT_BLUE_COLOR", 3907043},
|
||||
{"DRAGONFLIGHT_GREEN_COLOR", 2993754},
|
||||
{"DRAGONFLIGHT_BRONZE_COLOR", 11768335},
|
||||
{"DRAGONFLIGHT_BLACK_COLOR", 9868950},
|
||||
{"BANK_BAG_BACKGROUND_COLOR", -866623904},
|
||||
{"INFLUENCE_COLOR", -16724737},
|
||||
{"RAF_VERSION_TWO_COLOR", 184600048},
|
||||
{"RAF_VERSION_THREE_COLOR", 426357069},
|
||||
{"PURE_BLUE_COLOR", -16776961},
|
||||
{"NIFFEN_MAJOR_FACTION_COLOR", -1461955},
|
||||
{"PVP_SCOREBOARD_ALLIANCE_ROW_COLOR", -14925182},
|
||||
{"PVP_SCOREBOARD_HORDE_ROW_COLOR", -9105111},
|
||||
{"PVP_SCOREBOARD_HORDE_ALT_ROW_COLOR", -8703534},
|
||||
{"PVP_SCOREBOARD_ALLIANCE_ALT_ROW_COLOR", -2509502},
|
||||
{"PVP_SCOREBOARD_ALLIANCE_CELL_COLOR", -16732688},
|
||||
{"PVP_SCOREBOARD_HORDE_CELL_COLOR", -54988},
|
||||
{"PVP_SCOREBOARD_ALLIANCE_ALT_CELL_COLOR", -12032},
|
||||
{"PVP_SCOREBOARD_HORDE_ALT_CELL_COLOR", -3305473},
|
||||
{"LOOT_LINK_COLOR", -9316865},
|
||||
{"TUTORIAL_BLUE_FONT_COLOR", -16724737},
|
||||
{"FACTION_AT_WAR_COLOR", -9895168},
|
||||
{"DREAM_MAJOR_FACTION_COLOR", -16007562},
|
||||
{"SPELLBOOK_FONT_COLOR", -13755633},
|
||||
{"OBJECTIVE_TRACKER_BLOCK_HEADER_COLOR", 12557312},
|
||||
{"HEALTHBAR_MY_HEAL_PREDICTION_COLOR", -16723005},
|
||||
{"HEALTHBAR_OTHER_HEAL_PREDICTION_COLOR", -16735858},
|
||||
{"HEALTHBAR_HEAL_ABSORB_COLOR", -9656819},
|
||||
{"HEALTHBAR_TOTAL_ABSORB_COLOR", -1},
|
||||
{"POWERBAR_PREDICTION_COLOR_MANA", -16765592},
|
||||
{"POWERBAR_PREDICTION_COLOR_RAGE", -10092544},
|
||||
{"POWERBAR_PREDICTION_COLOR_FOCUS", -10083840},
|
||||
{"POWERBAR_PREDICTION_COLOR_ENERGY", -9214464},
|
||||
{"POWERBAR_PREDICTION_COLOR_RUNIC_POWER", -16755866},
|
||||
{"POWERBAR_PREDICTION_COLOR_LUNAR_POWER", -15782041},
|
||||
{"POWERBAR_PREDICTION_COLOR_MAELSTROM", -16764058},
|
||||
{"POWERBAR_PREDICTION_COLOR_INSANITY", -13434778},
|
||||
{"POWERBAR_PREDICTION_COLOR_FURY", -12058012},
|
||||
{"POWERBAR_PREDICTION_COLOR_PAIN", -10076416},
|
||||
{"UI_TOOLS_DISABLED_COLOR", -13813664},
|
||||
{"UI_TOOLS_PRESSED_BUTTON_COLOR", -3355444},
|
||||
{"UI_TOOLS_LINE_COLOR", -13024930},
|
||||
{"COMBAT_TEXT_SPELL_COLOR", -34},
|
||||
{"QUEST_OBJECTIVE_COMPLETED_FONT_COLOR_DARK_BACKGROUND", -2832453},
|
||||
{"QUEST_OBJECTIVE_COMPLETED_FONT_COLOR", -13421773},
|
||||
{"ARCHAEOLOGY_LIGHT_BROWN", 16772796},
|
||||
{"ARCHAEOLOGY_BROWN", 14267759},
|
||||
{"PROFESSIONS_JEWELCRAFTING_GEMSTONES_RUBY", 15354699},
|
||||
{"PROFESSIONS_JEWELCRAFTING_GEMSTONES_AMBER", 16292383},
|
||||
{"PROFESSIONS_JEWELCRAFTING_GEMSTONES_CITRINE", 16308767},
|
||||
{"PROFESSIONS_JEWELCRAFTING_GEMSTONES_EMERALD", 4448291},
|
||||
{"PROFESSIONS_JEWELCRAFTING_GEMSTONES_SAPPHIRE", 2337760},
|
||||
{"PROFESSIONS_JEWELCRAFTING_GEMSTONES_ONYX", 14032873},
|
||||
{"ACCOUNT_WIDE_FONT_COLOR", -16724737},
|
||||
{"QUEST_ACTIVE_TRIVIAL_GRAY", -4210753},
|
||||
{"PLAYER_NAMEPLATE_ALTERNATE_HEALTH_COLOR", -16732688},
|
||||
{"PLUNDERSTORM_WANTED_COLOR", -7578816},
|
||||
{"QUEST_REWARD_CONTEXT_FONT_COLOR", -7820545},
|
||||
{"EXPANSION_COLOR_1", 16777215},
|
||||
{"EXPANSION_COLOR_2", 16711935},
|
||||
{"EXPANSION_COLOR_3", 16711935},
|
||||
{"EXPANSION_COLOR_4", 16711935},
|
||||
{"EXPANSION_COLOR_5", 16711935},
|
||||
{"EXPANSION_COLOR_6", 16711935},
|
||||
{"EXPANSION_COLOR_7", 16711935},
|
||||
{"EXPANSION_COLOR_8", 16711935},
|
||||
{"EXPANSION_COLOR_9", 16711935},
|
||||
{"EXPANSION_COLOR_10", 16711935},
|
||||
{"EXPANSION_COLOR_11", 16711935},
|
||||
{"EXPANSION_COLOR_12", 16711935},
|
||||
{"EXPANSION_COLOR_13", 16711935},
|
||||
{"QUEST_ACCOUNT_COMPLETED_NOTICE_FONT_COLOR", -13303808},
|
||||
{"SPELLBOOK_UNLEARNED_TINT_COLOR", -5470115},
|
||||
{"COMBAT_TEXT_PET_COLOR", 16745472},
|
||||
{"RPE_FONT_COLOR", -5570646},
|
||||
{"WEB_MAJOR_FACTION_COLOR", -6798664},
|
||||
{"STORM_MAJOR_FACTION_COLOR", -12415234},
|
||||
{"CANDLE_MAJOR_FACTION_COLOR", -35301},
|
||||
{"FLAME_MAJOR_FACTION_COLOR", -2704473},
|
||||
{"LIGHTERGRAY_FONT_COLOR", -8816263},
|
||||
{"DELVES_SCENARIO_NAME_COLOR", -137299},
|
||||
{"DELVES_SEASONAL_LEVEL_BLUE_COLOR", -16724737},
|
||||
{"CUSTOMIZATION_CHOICE_ELIGIBLE_COLOR", -3223858},
|
||||
{"CUSTOMIZATION_CHOICE_INELIGIBLE_COLOR", -11119018},
|
||||
{"ROCKET_MAJOR_FACTION_COLOR", -6246854},
|
||||
{"STARS_MAJOR_FACTION_COLOR", -6246854},
|
||||
{"EVENT_SCHEDULER_LOCATION_COLOR", -4219904},
|
||||
{"EVENT_SCHEDULER_NAME_COLOR", -3355444},
|
||||
{"HOUSING_PLACEMENT_GRID_ROOM_CROSS_COLOR", 872415231},
|
||||
{"HOUSING_PLACEMENT_GRID_VALID_COLOR", -2567732},
|
||||
{"HOUSING_PLACEMENT_GRID_INVALID_COLOR", -65536},
|
||||
{"BATTLE_TAG_SUFFIX_FONT_COLOR", -12491904},
|
||||
{"BATTLE_TAG_FONT_COLOR", -10966307},
|
||||
{"CINEMATIC_SUBTITLES_BLACK_BACKGROUND_COLOR", -16777216},
|
||||
{"CINEMATIC_SUBTITLES_LIGHT_BACKGROUND_COLOR", -1},
|
||||
{"RECENT_ALLY_FONT_COLOR", -11286017},
|
||||
{"GAMEPAD_TAB_INDICATOR_BUTTON_INPUT_ICON_DISABLED_COLOR", -1717986919},
|
||||
{"ANALOGUE_TARGETING_COLOR", -666333},
|
||||
{"COMPACT_UNIT_FRAME_FRIENDLY_HEALTH_COLOR", 2855685},
|
||||
{"DEBUFF_TYPE_MAGIC_COLOR", -16743937},
|
||||
{"DEBUFF_TYPE_POISON_COLOR", -8665344},
|
||||
{"DEBUFF_TYPE_CURSE_COLOR", -6355228},
|
||||
{"DEBUFF_TYPE_BLEED_COLOR", -4718577},
|
||||
{"DEBUFF_TYPE_DISEASE_COLOR", -955895},
|
||||
{"DEBUFF_TYPE_NONE_COLOR", -3407872},
|
||||
// ClassicAPI extension: GlobalColor.dbc has no Enrage row. We add it
|
||||
// so `GetAuraDispelTypeColor("Enrage")` resolves to a proper ColorMixin
|
||||
// through the same Color.lua pipeline.
|
||||
// rgb(1.00, 0.55, 0.00) -> 0xFFFF8C00.
|
||||
{"DEBUFF_TYPE_ENRAGE_COLOR", -29696},
|
||||
{"VISITABLE_URL_DEFAULT_CHAT_LINK_COLOR", -16733697},
|
||||
{"EVENTTRACE_SECRET_COLOR", -7798904},
|
||||
{"COMPACT_UNIT_FRAME_FRIENDLY_HEALTH_COLOR_BG", -15461356},
|
||||
{"DAMAGE_METER_STATUS_BAR_DEFAULT_COLOR", -10619},
|
||||
{"DAMAGE_METER_STATUS_BAR_CREATURE_COLOR", -57312},
|
||||
{"DAMAGE_METER_STATUS_BAR_ALLY_COLOR", -3514380},
|
||||
{"DAMAGE_METER_STATUS_BAR_ENEMY_COLOR", -2421},
|
||||
{"PERSONAL_RESOURCE_DISPLAY_DEFAULT_HEALTH_COLOR", -16724992},
|
||||
{"LOW_THREAT_COLOR", -5197648},
|
||||
{"GAINING_THREAT_COLOR", -477952},
|
||||
{"HIGH_THREAT_COLOR", -47861},
|
||||
{"CASTBAR_CLASSIC_YELLOW", 16757504},
|
||||
{"CASTBAR_CLASSIC_GREEN", 65280},
|
||||
{"CASTBAR_CLASSIC_GRAY", 11776947},
|
||||
{"CASTBAR_CLASSIC_RED", 16711680},
|
||||
{"VISUAL_ALERT_COLOR_GOLD", -10496},
|
||||
{"VISUAL_ALERT_COLOR_CYAN", -12599340},
|
||||
{"VISUAL_ALERT_COLOR_RED", -3532262},
|
||||
{"VISUAL_ALERT_COLOR_GREEN", -11686045},
|
||||
{"VISUAL_ALERT_COLOR_BLUE", -12620089},
|
||||
{"UNIT_LEVEL_NON_ATTACKABLE", 16765184},
|
||||
{"SWING_TIMER_COLOR_MAIN_HAND", -10135922},
|
||||
{"SWING_TIMER_COLOR_OFF_HAND", -10235175},
|
||||
{"SWING_TIMER_COLOR_RANGED", -13702833},
|
||||
{"GAMEPAD_SMARTNAV_CURSOR_COLOR", -6656},
|
||||
{"GAMEPAD_SMARTNAV_TARGETING_CURSOR_COLOR", -16718081},
|
||||
{"CHARACTER_FRAME_SIDE_PANEL_REPUTATION_COLOR", -1},
|
||||
{"CHARACTER_FRAME_SIDE_PANEL_PVP_COLOR", -1},
|
||||
{"CHARACTER_FRAME_SIDE_PANEL_CURRENCY_COLOR", -1},
|
||||
{"EMPTY_BAG_SLOT_EFFECT_COLOR", 2141557887},
|
||||
{"AREA_DESCRIPTION_FONT_COLOR", -1},
|
||||
{"INVASION_FONT_COLOR", -3670272},
|
||||
{"INVASION_DESCRIPTION_FONT_COLOR", -2039},
|
||||
{"REFUND_TOOLTIP_COLOR", -16724737},
|
||||
{"CHARACTER_FRAME_SIDE_PANEL_SKILL_COLOR", -1},
|
||||
};
|
||||
|
||||
constexpr int kColorCount = sizeof(kColors) / sizeof(kColors[0]);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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/>.
|
||||
|
||||
// The `ShakeStrengthUI` cvar. Nothing in the engine reads it: the embedded
|
||||
// addon's ScriptAnimationUtil.ShakeFrame scales each shake offset by it and
|
||||
// skips the shake at 0 or less. Help, category and default are retail's.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cvar/Factory.h"
|
||||
|
||||
namespace UI::ShakeStrength {
|
||||
|
||||
namespace {
|
||||
|
||||
void Register() {
|
||||
CVar::Factory::Register("ShakeStrengthUI",
|
||||
"Motion sickness control for how much effects can shake in 2D UI",
|
||||
Offsets::CVAR_CATEGORY_GAME, "1.000000");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace UI::ShakeStrength
|
||||
@@ -0,0 +1,114 @@
|
||||
// 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/>.
|
||||
|
||||
// `UNIT_DISTANCE_CHECK_UPDATE` — a group member came into view or left it.
|
||||
//
|
||||
// Retail's one reader, CompactUnitFrame_GetCenterStatusIconState, shows the
|
||||
// other-phase icon only for a member within 250 yards
|
||||
// (DISTANCE_THRESHOLD_SQUARED): close enough that the client would draw it.
|
||||
// That is the server's view distance (tortoise-wow's
|
||||
// Visibility.Distance.Continents and .Instances default to 250), so in 1.12
|
||||
// a member is in distance exactly while the client holds its object.
|
||||
//
|
||||
// UpdateRaidMemberRecord (FUN_UPDATE_RAID_MEMBER_RECORD) has four callers,
|
||||
// which are a player object entering the object table (PostInit, and
|
||||
// PostUpdate on reactivation) and leaving it (the release slot, and
|
||||
// ~CGPlayer_C for an object never released). Each call points here: a
|
||||
// group member's GUID is noted, and the next world tick fires for each of
|
||||
// its tokens with whether the object table holds it then. The release slot
|
||||
// runs before the manager moves the object to the released table, so a fire
|
||||
// in place would show the member still in view to UnitIsVisible.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Unit::DistanceCheck {
|
||||
|
||||
namespace {
|
||||
|
||||
const Game::Doc::Field kPayload[] = {
|
||||
Game::Doc::Req("unitTarget", "UnitToken"),
|
||||
Game::Doc::Req("isInDistance", "bool"),
|
||||
};
|
||||
const Game::Doc::Event kDoc{"Unit", "Fires when a group member comes into view or leaves it.",
|
||||
kPayload};
|
||||
const Event::Custom::AutoReserve kEvent{"UNIT_DISTANCE_CHECK_UPDATE", &kDoc};
|
||||
|
||||
using UpdateRaidRecord_t = void(__fastcall *)(void *player, uint32_t mode, uint32_t withPet);
|
||||
using IsGroupMember_t = int(__fastcall *)(const uint64_t *guid);
|
||||
|
||||
std::vector<uint64_t> g_changed;
|
||||
|
||||
void __fastcall UpdateRaidRecordSite(void *player, uint32_t mode, uint32_t withPet) {
|
||||
reinterpret_cast<UpdateRaidRecord_t>(Offsets::FUN_UPDATE_RAID_MEMBER_RECORD)(player, mode,
|
||||
withPet);
|
||||
if (!Event::Custom::HasListeners(kEvent.Slot()))
|
||||
return;
|
||||
const uint64_t guid = Unit::Identity::GuidForObject(player);
|
||||
if (guid == 0 || guid == Unit::Identity::PlayerGuid() ||
|
||||
reinterpret_cast<IsGroupMember_t>(Offsets::FUN_UNIT_IS_GROUP_MEMBER_OR_PET)(&guid) == 0)
|
||||
return;
|
||||
if (std::find(g_changed.begin(), g_changed.end(), guid) == g_changed.end())
|
||||
g_changed.push_back(guid);
|
||||
}
|
||||
|
||||
void OnWorldTick() {
|
||||
if (g_changed.empty())
|
||||
return;
|
||||
std::vector<uint64_t> changed;
|
||||
changed.swap(g_changed);
|
||||
const int slot = kEvent.Slot();
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr || !Event::Custom::HasListeners(slot))
|
||||
return;
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
char tokens[16][32];
|
||||
for (const uint64_t guid : changed) {
|
||||
const bool inView = Object::ByGuid(Offsets::TYPEMASK_PLAYER, guid) != nullptr;
|
||||
const int n = Unit::Identity::TokensForGUID(guid, tokens, 16);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
Game::Lua::PushString(L, tokens[i]);
|
||||
Game::Lua::PushBool(L, inView);
|
||||
Event::Custom::FireValues(slot, L, top + 1, 2);
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Register() {
|
||||
const auto *site = reinterpret_cast<const void *>(&UpdateRaidRecordSite);
|
||||
Common::RedirectCall(Offsets::CALL_PLAYER_POST_INIT_RAID_RECORD,
|
||||
Offsets::FUN_UPDATE_RAID_MEMBER_RECORD, site);
|
||||
Common::RedirectCall(Offsets::CALL_PLAYER_POST_UPDATE_RAID_RECORD,
|
||||
Offsets::FUN_UPDATE_RAID_MEMBER_RECORD, site);
|
||||
Common::RedirectCall(Offsets::CALL_PLAYER_RELEASE_RAID_RECORD,
|
||||
Offsets::FUN_UPDATE_RAID_MEMBER_RECORD, site);
|
||||
Common::RedirectCall(Offsets::CALL_PLAYER_DTOR_RAID_RECORD,
|
||||
Offsets::FUN_UPDATE_RAID_MEMBER_RECORD, site);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
const Tick::WorldTick::AutoSubscribe _tick{&OnWorldTick};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Unit::DistanceCheck
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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/>.
|
||||
|
||||
// The flags field's type watcher (OnFlagsFieldChanged, 0x006044B0) resolves
|
||||
// the unit and calls CGUnit_C::OnUnitFlagsChanged with the old flags from
|
||||
// one E8 call (CALL_FLAGS_FIELD_FLAGS_CHANGED). That call points here
|
||||
// instead: the function runs the engine's handler, then each subscriber with
|
||||
// the flags before and after. A call site can be redirected once, so every
|
||||
// module that acts on a flag change subscribes here.
|
||||
|
||||
#include "FlagsChanged.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Unit::FlagsChanged {
|
||||
|
||||
namespace {
|
||||
|
||||
AutoSubscribe *g_head = nullptr;
|
||||
|
||||
using FlagsChanged_t = void(__thiscall *)(void *unit, uint32_t oldFlags);
|
||||
|
||||
void __fastcall FlagsChangedSite(void *unit, void *, uint32_t oldFlags) {
|
||||
reinterpret_cast<FlagsChanged_t>(Offsets::FUN_UNIT_FLAGS_CHANGED)(unit, oldFlags);
|
||||
const auto *desc = Game::Read<const uint8_t *>(unit, Offsets::OFF_CGUNIT_OBJECT_FIELDS);
|
||||
const uint32_t flags = Game::Read<uint32_t>(desc, Offsets::OFF_UNIT_FIELD_FLAGS);
|
||||
for (auto *node = g_head; node != nullptr; node = node->next)
|
||||
node->cb(unit, oldFlags, flags);
|
||||
}
|
||||
|
||||
void Register() {
|
||||
Common::RedirectCall(Offsets::CALL_FLAGS_FIELD_FLAGS_CHANGED, Offsets::FUN_UNIT_FLAGS_CHANGED,
|
||||
reinterpret_cast<const void *>(&FlagsChangedSite));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
AutoSubscribe::AutoSubscribe(Callback cb) : cb(cb), next(g_head) {
|
||||
g_head = this;
|
||||
}
|
||||
|
||||
} // namespace Unit::FlagsChanged
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Unit::FlagsChanged {
|
||||
|
||||
// Runs after 1.12's CGUnit_C::OnUnitFlagsChanged (FUN_UNIT_FLAGS_CHANGED),
|
||||
// the engine's reaction to a changed UNIT_FIELD_FLAGS, for every unit: the
|
||||
// place 4.3.4's CGUnit_C::OnFlagChanged acts on the bits 1.12 ignores.
|
||||
// `unit` is the CGUnit_C, `oldFlags` / `flags` the field before and after.
|
||||
using Callback = void (*)(void *unit, uint32_t oldFlags, uint32_t flags);
|
||||
|
||||
// Place a static instance at file scope:
|
||||
//
|
||||
// static const Unit::FlagsChanged::AutoSubscribe _sub{&OnFlags};
|
||||
//
|
||||
// The constructor chains the callback at static init; order across
|
||||
// subscribers is unspecified.
|
||||
struct AutoSubscribe {
|
||||
explicit AutoSubscribe(Callback cb);
|
||||
Callback cb;
|
||||
AutoSubscribe *next;
|
||||
};
|
||||
|
||||
} // namespace Unit::FlagsChanged
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user