mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-12 12:57:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
260d40f49b | ||
|
|
8f6ff33a39 | ||
|
|
5082f3c829 | ||
|
|
5d0568f759 | ||
|
|
9a78e159e7 | ||
|
|
503368623f | ||
|
|
735d304a20 | ||
|
|
29ddd78a80 | ||
|
|
b6c21216e3 | ||
|
|
6cc9d0db90 | ||
|
|
c6c201e575 | ||
|
|
77b304ae68 | ||
|
|
c965e49117 | ||
|
|
ead52e79ce | ||
|
|
7cb6cda101 | ||
|
|
5ab497f2c7 | ||
|
|
205d1fd43b | ||
|
|
950c9deb76 |
@@ -69,8 +69,9 @@ 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` |
|
||||
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid`, `GetFramesRegisteredForEvent` |
|
||||
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetAccountExpansionLevel`, `GetClassicExpansionLevel`, `GetExpansionLevel`, `GetMaxPlayerLevel` |
|
||||
@@ -85,7 +86,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Frame: StatusBar](docs/API.md#statusbarsetreversefillisreversefill--statusbargetreversefill) | `:SetReverseFill` / `:GetReverseFill`, `:SetRotatesTexture` / `:GetRotatesTexture`, `:SetFillStyle` / `:GetFillStyle`, `:SetColorFill`, `:SetStatusBarDesaturated` / `:IsStatusBarDesaturated`, `:SetStatusBarDesaturation` / `:GetStatusBarDesaturation` |
|
||||
| [Frame: Slider](docs/API.md#sliderenable--sliderdisable--sliderisenabled) | `:Enable` / `:Disable` / `:IsEnabled` |
|
||||
| [Frame: Cooldown](docs/API.md#createframecooldown--name-parent-template) | `CreateFrame("Cooldown")`, `:SetCooldown`, `:SetCooldownDuration`, `:SetCooldownFromExpirationTime`, `:Clear`, `:GetCooldownTimes`, `:GetCooldownDuration`, `:GetCooldownDisplayDuration`, `:Pause` / `:Resume`, `:SetPaused` / `:IsPaused`, `:SetReverse` / `:GetReverse`, `:SetDrawEdge` / `:GetDrawEdge`, `:SetDrawSwipe` / `:GetDrawSwipe`, `:SetDrawBling` / `:GetDrawBling`, `:SetSwipeColor`, `:SetEdgeColor`, `:SetSwipeTexture`, `:SetEdgeTexture`, `:SetBlingTexture`, `:SetRotation` / `:GetRotation`, `:SetHideCountdownNumbers` / `:GetHideCountdownNumbers`, `:GetCountdownFontString`, `:SetCountdownAbbrevThreshold` / `:GetCountdownAbbrevThreshold`, `:SetCountdownMillisecondsThreshold` / `:GetCountdownMillisecondsThreshold`, `:SetMinimumCountdownDuration` / `:GetMinimumCountdownDuration`, `OnCooldownDone`, `CooldownFrame_Set`, `CooldownFrame_Clear`, `CooldownFrame_SetDisplayAsPercentage` |
|
||||
| [Frame: ScrollingMessageFrame](docs/API.md#smfsethyperlinksenabledenabled--smfgethyperlinksenabled) | `:SetHyperlinksEnabled` / `:GetHyperlinksEnabled` |
|
||||
| [Frame: ScrollingMessageFrame](docs/API.md#smfsethyperlinksenabledenabled--smfgethyperlinksenabled) | `:SetHyperlinksEnabled` / `:GetHyperlinksEnabled`, `:GetMessageInfo` (with the extra `:AddMessage` values), `:BackFillMessage` (adds a message as the oldest), `:RemoveMessagesByPredicate`, `:SetScrollOffset` / `:GetScrollOffset`, `:GetMaxScrollRange`, `:ScrollByAmount`, `:SetInsertMode` / `:GetInsertMode`, `:AddOnDisplayRefreshedCallback` |
|
||||
| [Frame: scripts](docs/API.md#onenable-and-ondisable-scripts) | `OnEnable`, `OnDisable`, `OnMinMaxChanged`, `OnAttributeChanged`, `OnCharComposition`, `OnTextChanged` (`userInput`), `PreClick`, `PostClick`, `SetModernScriptArgs` / `GetModernScriptArgs` |
|
||||
| [FriendList](docs/API.md#friendlist) | `AddOrRemoveFriend`, `C_FriendList.GetFriendInfo`, `C_FriendList.GetFriendInfoByIndex`, `C_FriendList.GetNumFriends`, `C_FriendList.GetNumOnlineFriends`, `C_FriendList.GetNumWhoResults`, `C_FriendList.GetWhoInfo`, `C_FriendList.IsFriend`, `C_FriendList.IsIgnored`, `C_FriendList.IsIgnoredByGuid`, `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName`, `C_FriendList.SetFriendNotes`, `C_FriendList.SetFriendNotesByIndex` |
|
||||
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID`, `ClosestGameObjectPosition` |
|
||||
@@ -99,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` |
|
||||
@@ -232,7 +234,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. |
|
||||
@@ -313,6 +317,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `QUEST_QUERY_COMPLETE` | *(none)* (Turtle WoW only) |
|
||||
| `QUEST_REMOVED` | `questID` |
|
||||
| `QUEST_TURNED_IN` | `questID, xpReward, moneyReward` |
|
||||
| `SELF_RES_SPELL_CHANGED` | *(none)* |
|
||||
| `SOUNDKIT_FINISHED` | `soundHandle` (only for sounds played with `runFinishCallback`) |
|
||||
| `UNIT_AURA_DELTA` | `unitTarget, updateInfo` (the instance IDs of the auras added, updated, and removed) |
|
||||
| `UNIT_SPELLCAST_SENT` | `"player", target, castGUID, spellID, spellName, rank` |
|
||||
@@ -360,6 +365,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| Spellbook bank | `Enum.SpellBookSpellBank.{Player,Pet}` (0–1) |
|
||||
| Spellbook item type | `Enum.SpellBookItemType.{None,Spell,FutureSpell,PetAction,Flyout}` (0–4; 1.12 only yields `Spell`/`PetAction`) |
|
||||
| Player swing type | `Enum.PlayerSwingType.{MainHand,OffHand,Ranged}` (0–2) |
|
||||
| Self-resurrect option type | `Enum.SelfResurrectOptionType.{Spell,Item}` (0–1) |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -429,8 +435,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
|
||||
|
||||
@@ -4081,9 +4081,22 @@ Each entry still needs the usual check of how the engine does it before building
|
||||
`GetRotatesTexture` (`CSimpleStatusBar.cpp:135`, the vertical texcoord path
|
||||
at `:285`). Retail adds `SetFillStyle` (STANDARD / CENTER / REVERSE),
|
||||
`SetColorFill`, `SetStatusBarDesaturated` / `Desaturation`.
|
||||
- **ScrollingMessageFrame** `SetInsertMode` / `GetInsertMode`,
|
||||
`GetMessageInfo`, `RemoveMessagesByAccessID`, `RemoveMessagesByExtraData`,
|
||||
`SetScrollOffset` — in C in 4.3.4 (retail moved the type to Lua).
|
||||
- **DONE** (`src/scrollingmessageframe/`, retail's shapes).
|
||||
**ScrollingMessageFrame** `GetMessageInfo`, `SetScrollOffset` (1.12's
|
||||
`SetScrollFromBottom`, 0x00793070, under its later name, clamped as
|
||||
retail), `GetScrollOffset`, `GetMaxScrollRange`, `ScrollByAmount`,
|
||||
`AddOnDisplayRefreshedCallback`, `SetInsertMode` / `GetInsertMode` (TOP
|
||||
through a redirect of the layout's one line-anchor call, 0x00788975),
|
||||
`BackFillMessage` (writes 4.3.4's addToTop slot of the ring, the free one
|
||||
before the oldest; a full frame drops it, as retail's CircularBuffer
|
||||
`PushBack` does — 4.3.4 overwrites the oldest there).
|
||||
- **DONE** (`src/scrollingmessageframe/MessageInfo.cpp`).
|
||||
**ScrollingMessageFrame** `RemoveMessagesByPredicate` (retail's shape; it
|
||||
covers 4.3.4's `RemoveMessagesByAccessID` / `RemoveMessagesByExtraData`,
|
||||
whose int fields our retail-shaped records do not have). The compaction
|
||||
is 4.3.4's `RemoveLines` loop over 1.12's ring, then the engine's own
|
||||
layout (0x00788750) at the kept scroll offset and re-arm (0x00788B80),
|
||||
or Clear (0x007882B0) when nothing is kept.
|
||||
- **DONE** (`src/editbox/Focus.cpp`, `src/animation/Framerate.cpp`).
|
||||
**Globals** (`SimpleFrameScript_Scripts.cpp`): `GetCurrentKeyBoardFocus()`
|
||||
(the focused edit box's wrapper, or nil); `SetMaxAnimFramerate(fps)` /
|
||||
|
||||
+402
-2
@@ -165,6 +165,7 @@ build instructions.
|
||||
- [Currency](#currency)
|
||||
- [`GetCoinText(amount [, separator])` / `C_CurrencyInfo.GetCoinText(amount [, separator])`](#getcointextamount--separator--c_currencyinfogetcointextamount--separator)
|
||||
- [`GetCoinTextureString(amount [, fontHeight])` / `C_CurrencyInfo.GetCoinTextureString(amount [, fontHeight])`](#getcointexturestringamount--fontheight--c_currencyinfogetcointexturestringamount--fontheight)
|
||||
- [`Constants.MoneyFormattingConstants`](#constantsmoneyformattingconstants)
|
||||
|
||||
- [CVar](#cvar)
|
||||
- [`GetCVar(name)`](#getcvarname)
|
||||
@@ -182,6 +183,10 @@ build instructions.
|
||||
- [`GetCursorInfo()`](#getcursorinfo)
|
||||
- [`CursorHasMacro()`](#cursorhasmacro)
|
||||
|
||||
- [DeathInfo](#deathinfo)
|
||||
- [`C_DeathInfo.GetSelfResurrectOptions()`](#c_deathinfogetselfresurrectoptions)
|
||||
- [`C_DeathInfo.UseSelfResurrectOption(optionType, id)`](#c_deathinfouseselfresurrectoptionoptiontype-id)
|
||||
|
||||
- [EquipmentSet](#equipmentset)
|
||||
- [Overview & file format](#overview--file-format)
|
||||
- [`C_EquipmentSet.CanUseEquipmentSets()`](#c_equipmentsetcanuseequipmentsets)
|
||||
@@ -241,6 +246,7 @@ build instructions.
|
||||
- [`QUEST_QUERY_COMPLETE` event](#quest_query_complete-event)
|
||||
- [`QUEST_REMOVED` event](#quest_removed-event)
|
||||
- [`QUEST_TURNED_IN` event](#quest_turned_in-event)
|
||||
- [`SELF_RES_SPELL_CHANGED` event](#self_res_spell_changed-event)
|
||||
- [`SOUNDKIT_FINISHED` event](#soundkit_finished-event)
|
||||
- [`UNIT_AURA_DELTA` event](#unit_aura_delta-event)
|
||||
- [`UNIT_FACTION` event (fire-coverage fix)](#unit_faction-event-fire-coverage-fix)
|
||||
@@ -380,6 +386,12 @@ build instructions.
|
||||
- [`frame:HookScript(scriptType, handler)`](#framehookscriptscripttype-handler)
|
||||
- [Script binding types](#script-binding-types)
|
||||
- [`smf:SetHyperlinksEnabled(enabled)` / `smf:GetHyperlinksEnabled()`](#smfsethyperlinksenabledenabled--smfgethyperlinksenabled)
|
||||
- [`smf:GetMessageInfo(index)`](#smfgetmessageinfoindex)
|
||||
- [`smf:BackFillMessage(text, r, g, b, ...)`](#smfbackfillmessagetext-r-g-b-)
|
||||
- [`smf:RemoveMessagesByPredicate(predicate)`](#smfremovemessagesbypredicatepredicate)
|
||||
- [`smf:SetScrollOffset(offset)` / `smf:GetScrollOffset()` / `smf:GetMaxScrollRange()` / `smf:ScrollByAmount(amount)`](#smfsetscrolloffsetoffset--smfgetscrolloffset--smfgetmaxscrollrange--smfscrollbyamountamount)
|
||||
- [`smf:SetInsertMode(mode)` / `smf:GetInsertMode()`](#smfsetinsertmodemode--smfgetinsertmode)
|
||||
- [`smf:AddOnDisplayRefreshedCallback(callback)`](#smfaddondisplayrefreshedcallbackcallback)
|
||||
- [`frame:IsEventRegistered(event)`](#frameiseventregisteredevent)
|
||||
- [`frame:RegisterUnitEvent(event, ...units)`](#frameregisteruniteventevent-units)
|
||||
- [`frame:GetEffectiveAlpha()`](#framegeteffectivealpha)
|
||||
@@ -481,7 +493,7 @@ build instructions.
|
||||
|
||||
- [Hooks](#hooks)
|
||||
- [`hooksecurefunc(name, callback)` / `hooksecurefunc(table, name, callback)`](#hooksecurefuncname-callback--hooksecurefunctable-name-callback)
|
||||
- [`securecall(func, ...)` / `securecallfunction(func, ...)` / `secureexecuterange(tbl, func, ...)`](#securecallfunc---securecallfunctionfunc---secureexecuterangetbl-func-)
|
||||
- [`securecall(func, ...)` / `securecallfunction(func, ...)` / `securecallmethod(object, method, ...)` / `secureexecuterange(tbl, func, ...)`](#securecallfunc---securecallfunctionfunc---securecallmethodobject-method---secureexecuterangetbl-func-)
|
||||
|
||||
- [Input](#input)
|
||||
- [`IsLeftShiftKeyDown()` / `IsRightShiftKeyDown()`](#isleftshiftkeydown--isrightshiftkeydown)
|
||||
@@ -569,6 +581,13 @@ build instructions.
|
||||
- [`-config <name>` (launch switch)](#-config-name-launch-switch)
|
||||
- [`-gluescript` / `-gluescriptFile` / `-gamescript` / `-gamescriptFile` (launch switches)](#-gluescript---gluescriptfile---gamescript---gamescriptfile-launch-switches)
|
||||
|
||||
- [Localization](#localization)
|
||||
- [`BreakUpLargeNumbers(largeNumber [, natural])`](#breakuplargenumberslargenumber--natural)
|
||||
- [`AbbreviateNumbers(number [, options])`](#abbreviatenumbersnumber--options)
|
||||
- [`AbbreviateLargeNumbers(number [, options])`](#abbreviatelargenumbersnumber--options)
|
||||
- [`C_StringUtil.GetDefaultAbbreviationBreakpoints([locale])`](#c_stringutilgetdefaultabbreviationbreakpointslocale)
|
||||
- [`breakUpLargeNumbers` (CVar)](#breakuplargenumbers-cvar)
|
||||
|
||||
- [Loot](#loot)
|
||||
- [`C_Loot.GetNearbyLootableUnits()`](#c_lootgetnearbylootableunits)
|
||||
- [`C_Loot.LootUnit(guid)`](#c_lootlootunitguid)
|
||||
@@ -4031,6 +4050,19 @@ The per-denomination formats are the GlobalStrings
|
||||
`COPPER_AMOUNT_TEXTURE`, backported in the embedded `!!!ClassicAPI`
|
||||
addon's locale layer — a locale can override them.
|
||||
|
||||
### `Constants.MoneyFormattingConstants`
|
||||
|
||||
A table of constants for money text.
|
||||
|
||||
| Field | Value | Description |
|
||||
|-------|-------|-------------|
|
||||
| `GOLD_REWARD_THRESHOLD_TO_HIDE_COPPER` | `10` | With `checkGoldThreshold`, `GetMoneyString` shows no copper for an amount of more than this number of gold. |
|
||||
|
||||
```lua
|
||||
GetMoneyString(1234567, nil, true) -- "123g 45s" with coin icons, no copper
|
||||
GetMoneyString(51234, nil, true) -- "5g 12s 34c" with coin icons
|
||||
```
|
||||
|
||||
## CVar
|
||||
|
||||
### `GetCVar(name)`
|
||||
@@ -4359,6 +4391,57 @@ Changes to what the cursor holds are reported by the
|
||||
|
||||
Returns `true` if the cursor holds a macro, and `false` if it does not.
|
||||
|
||||
## DeathInfo
|
||||
|
||||
### `C_DeathInfo.GetSelfResurrectOptions()`
|
||||
|
||||
```
|
||||
options = C_DeathInfo.GetSelfResurrectOptions()
|
||||
```
|
||||
|
||||
Returns an array of the ways that the dead player can resurrect themselves.
|
||||
Each entry is a `SelfResurrectOption` table:
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `name` | string | The name of the spell or the item. |
|
||||
| `optionType` | number | `Enum.SelfResurrectOptionType.Spell` (0) or `Enum.SelfResurrectOptionType.Item` (1). |
|
||||
| `id` | number | The spell ID or the item ID. |
|
||||
| `canUse` | boolean | Always `true`. |
|
||||
| `isLimited` | boolean | Always `false`. |
|
||||
| `priority` | number | Always `0`. |
|
||||
|
||||
The array has one entry at most. A self-resurrection spell, such as
|
||||
Soulstone Resurrection or Reincarnation, comes before an item. The array is
|
||||
empty when the player is alive or a ghost, or has no option. The function
|
||||
returns nothing before the player enters the world.
|
||||
|
||||
```lua
|
||||
local options = C_DeathInfo.GetSelfResurrectOptions()
|
||||
if options and options[1] then
|
||||
print("You can resurrect with", options[1].name)
|
||||
end
|
||||
```
|
||||
|
||||
[`SELF_RES_SPELL_CHANGED`](#self_res_spell_changed-event) fires when the
|
||||
self-resurrection spell changes.
|
||||
|
||||
### `C_DeathInfo.UseSelfResurrectOption(optionType, id)`
|
||||
|
||||
Resurrects the dead player with an option from
|
||||
[`C_DeathInfo.GetSelfResurrectOptions`](#c_deathinfogetselfresurrectoptions).
|
||||
`optionType` and `id` must match an entry that it returns now. If they do
|
||||
not match, the call does nothing. An item option uses the item, and this
|
||||
stops a spell cast in progress.
|
||||
|
||||
```lua
|
||||
local options = C_DeathInfo.GetSelfResurrectOptions()
|
||||
local option = options and options[1]
|
||||
if option then
|
||||
C_DeathInfo.UseSelfResurrectOption(option.optionType, option.id)
|
||||
end
|
||||
```
|
||||
|
||||
## EquipmentSet
|
||||
|
||||
`C_EquipmentSet.*` — named sets of gear you can save and equip again
|
||||
@@ -5783,6 +5866,14 @@ quest invalidated, etc.). Only fires on a real successful turn-in.
|
||||
> done, quest removed from log). Hooking the packet handler gives
|
||||
> us a clean turn-in signal.
|
||||
|
||||
### `SELF_RES_SPELL_CHANGED` event
|
||||
|
||||
Fires with no payload when the self-resurrection spell of the player
|
||||
changes. For example, it fires when the player dies with a Soulstone, and
|
||||
again when the player comes back to life. When it fires, read
|
||||
[`C_DeathInfo.GetSelfResurrectOptions`](#c_deathinfogetselfresurrectoptions)
|
||||
again. A change to an item option does not fire this event.
|
||||
|
||||
### `SOUNDKIT_FINISHED` event
|
||||
|
||||
Fires when a sound finishes playing.
|
||||
@@ -8599,6 +8690,151 @@ ChatFrame1:SetHyperlinksEnabled(false) -- links in this chat window stop react
|
||||
ChatFrame1:GetHyperlinksEnabled() -- false
|
||||
```
|
||||
|
||||
### `smf:GetMessageInfo(index)`
|
||||
|
||||
On ScrollingMessageFrame. Returns one message of the frame: its text, its
|
||||
color, and the extra values that `AddMessage` got for the message. Index 1 is
|
||||
the oldest message, and `smf:GetNumMessages()` is the index of the newest
|
||||
message. If no message has the index, the function returns nothing. If
|
||||
`index` is not a number, the function gives an error.
|
||||
|
||||
- `text` is the text that the frame keeps, with its color codes.
|
||||
- `r`, `g` and `b` are the color that the frame shows for the message. After
|
||||
`UpdateColorByID` changes the color, these values also change. The frame
|
||||
keeps each color value in steps of 1/255, so `0.5` returns as `0.50196…`.
|
||||
- `smf:AddMessage(text, r, g, b, ...)` keeps all the values after `b`.
|
||||
`GetMessageInfo` returns these values in the same order, nil values
|
||||
included.
|
||||
|
||||
An addon that replaces `AddMessage` and gives only `text, r, g, b` to the
|
||||
original function removes the extra values.
|
||||
|
||||
```lua
|
||||
ChatFrame1:AddMessage("hello", 1, 0.5, 0, 7, "extra")
|
||||
ChatFrame1:GetMessageInfo(ChatFrame1:GetNumMessages())
|
||||
-- "hello", 1, 0.50196078431373, 0, 7, "extra"
|
||||
|
||||
for i = 1, ChatFrame1:GetNumMessages() do
|
||||
local text, r, g, b = ChatFrame1:GetMessageInfo(i) -- oldest message first
|
||||
end
|
||||
```
|
||||
|
||||
### `smf:BackFillMessage(text, r, g, b, ...)`
|
||||
|
||||
On ScrollingMessageFrame. Adds a message as the oldest message of the frame.
|
||||
`AddMessage` adds the newest message. `BackFillMessage` adds history behind
|
||||
the messages that the frame already has. An addon that rebuilds a frame from
|
||||
a history walks the history from the newest entry to the oldest and adds each
|
||||
entry with this function. The messages that the frame already has do not
|
||||
move.
|
||||
|
||||
- `text`, `r`, `g`, `b` and the extra values work as in `AddMessage`.
|
||||
Without `r`, `g` and `b`, the message takes the color of the frame's font.
|
||||
`GetMessageInfo` returns the extra values after the color.
|
||||
- If the frame is at its line limit (`SetMaxLines`), the function drops the
|
||||
message, because the message is older than each message that the frame
|
||||
keeps.
|
||||
- If the frame is scrolled to the bottom, the frame shows the message when
|
||||
there is room above its other lines. If the frame is scrolled up, the frame
|
||||
keeps its view.
|
||||
|
||||
```lua
|
||||
ChatFrame1:BackFillMessage("older", 1, 1, 1)
|
||||
ChatFrame1:GetMessageInfo(1) -- "older", 1, 1, 1
|
||||
```
|
||||
|
||||
### `smf:RemoveMessagesByPredicate(predicate)`
|
||||
|
||||
On ScrollingMessageFrame. Removes the messages that `predicate` selects. The
|
||||
function calls `predicate(text, r, g, b, ...)` for each message, from the
|
||||
oldest to the newest, with the values that `GetMessageInfo` returns for the
|
||||
message. If `predicate` returns true, the function removes the message. The
|
||||
other messages keep their order. If `predicate` is not a function, the
|
||||
function gives an error.
|
||||
|
||||
- The function calls `predicate` for all messages before it removes any
|
||||
message. If `predicate` gives an error for a message, the frame keeps that
|
||||
message.
|
||||
- The frame keeps its scroll offset (`GetScrollOffset`). If fewer messages
|
||||
remain than the offset, the offset becomes the oldest message.
|
||||
- The frame shows the lines that remain at once. If no message remains, the
|
||||
result is the same as `smf:Clear()`.
|
||||
|
||||
```lua
|
||||
-- remove the messages that were added with the extra value 7
|
||||
ChatFrame1:RemoveMessagesByPredicate(function(text, r, g, b, id)
|
||||
return id == 7
|
||||
end)
|
||||
```
|
||||
|
||||
### `smf:SetScrollOffset(offset)` / `smf:GetScrollOffset()` / `smf:GetMaxScrollRange()` / `smf:ScrollByAmount(amount)`
|
||||
|
||||
On ScrollingMessageFrame. The scroll offset is the number of lines that the
|
||||
frame is scrolled away from the newest message. When the frame is not
|
||||
scrolled, the offset is 0. The largest offset is `GetMaxScrollRange()`, that
|
||||
is `GetNumMessages() - 1`, or 0 for an empty frame. At that offset, the oldest
|
||||
message is the bottom line, or the top line in TOP insert mode.
|
||||
|
||||
- `SetScrollOffset(offset)` scrolls to `offset`. An offset less than 0 or
|
||||
more than `GetMaxScrollRange()` changes to the nearest limit.
|
||||
- `ScrollByAmount(amount)` adds `amount` to the offset. A positive amount
|
||||
scrolls to older messages. The function also shows the faded lines
|
||||
again, also when the offset does not change.
|
||||
- If `offset` or `amount` is not a number, the function gives an error.
|
||||
|
||||
```lua
|
||||
ChatFrame1:ScrollByAmount(3) -- 3 lines up
|
||||
ChatFrame1:GetScrollOffset() -- 3
|
||||
ChatFrame1:SetScrollOffset(ChatFrame1:GetMaxScrollRange()) -- the oldest message
|
||||
ChatFrame1:SetScrollOffset(0) -- the newest message again
|
||||
```
|
||||
|
||||
### `smf:SetInsertMode(mode)` / `smf:GetInsertMode()`
|
||||
|
||||
On ScrollingMessageFrame. The insert mode tells where the newest message
|
||||
shows.
|
||||
|
||||
- `SCROLLING_MESSAGE_FRAME_INSERT_MODE_BOTTOM` (2) shows the newest message
|
||||
at the bottom, with older messages above it. This is the default.
|
||||
- `SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP` (1) shows the newest message at
|
||||
the top, with older messages below it.
|
||||
|
||||
In both modes, the scroll offset counts lines from the newest message. A
|
||||
change of mode moves the lines that show and runs the display-refreshed
|
||||
callbacks. If `mode` is the current mode, `SetInsertMode` does nothing. If
|
||||
`mode` is not one of the two values, the function gives an error.
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("ScrollingMessageFrame", nil, UIParent)
|
||||
f:SetInsertMode(SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP)
|
||||
f:AddMessage("first")
|
||||
f:AddMessage("second") -- shows above "first"
|
||||
f:GetInsertMode() -- 1
|
||||
```
|
||||
|
||||
### `smf:AddOnDisplayRefreshedCallback(callback)`
|
||||
|
||||
On ScrollingMessageFrame. Adds a function that the frame calls as
|
||||
`callback(frame)` after it refreshes the messages that it shows. The frame
|
||||
refreshes after a new message, a scroll, a resize, a font change, or
|
||||
`Clear()`.
|
||||
|
||||
- The callbacks run one time, in the next update of the frame, in the order
|
||||
that you added them.
|
||||
- A hidden frame runs its callbacks when it shows again.
|
||||
- An error in one callback goes to the error handler, and the other callbacks
|
||||
still run.
|
||||
- A change that a callback makes to the frame does not run the callbacks
|
||||
again.
|
||||
- If `callback` is not a function, the method gives an error. You cannot
|
||||
remove a callback.
|
||||
|
||||
```lua
|
||||
ChatFrame1:AddOnDisplayRefreshedCallback(function(frame)
|
||||
MyScrollLabel:SetText(frame:GetScrollOffset() .. " / " .. frame:GetMaxScrollRange())
|
||||
end)
|
||||
```
|
||||
|
||||
### `frame:IsEventRegistered(event)`
|
||||
|
||||
Returns `true` if the frame is registered for `event`, else `false` (also
|
||||
@@ -11030,7 +11266,7 @@ but they only report "any shift/ctrl/alt" — there's no built-in way to
|
||||
tell left from right. These seven functions add the missing distinction
|
||||
plus an `IsModifierKeyDown()` rollup.
|
||||
|
||||
### `securecall(func, ...)` / `securecallfunction(func, ...)` / `secureexecuterange(tbl, func, ...)`
|
||||
### `securecall(func, ...)` / `securecallfunction(func, ...)` / `securecallmethod(object, method, ...)` / `secureexecuterange(tbl, func, ...)`
|
||||
|
||||
Call a function so that an error in it does not stop the code that called
|
||||
it. The error goes to the error handler (`geterrorhandler()`).
|
||||
@@ -11040,6 +11276,10 @@ it. The error goes to the error handler (`geterrorhandler()`).
|
||||
`securecall` returns nothing.
|
||||
- `securecallfunction(func, ...)` is the same, but `func` must be a
|
||||
function.
|
||||
- `securecallmethod(object, method, ...)` calls
|
||||
`object[method](object, ...)` and returns its results. The lookup of
|
||||
`method` ignores the metatable of `object`. If `object` is not a table,
|
||||
or `method` is not a function, the error goes to the error handler.
|
||||
- `secureexecuterange(tbl, func, ...)` calls `func(key, value, ...)` for
|
||||
each entry of `tbl`. After an error in one call, the other entries still
|
||||
run. It returns nothing.
|
||||
@@ -13509,6 +13749,166 @@ first, then the file.
|
||||
is reported the same way any other script error is. It does not stop the
|
||||
client, and it does not stop the other scripts.
|
||||
|
||||
## Localization
|
||||
|
||||
Functions that write numbers as text, with the separators and abbreviations
|
||||
of the client language.
|
||||
|
||||
### `BreakUpLargeNumbers(largeNumber [, natural])`
|
||||
|
||||
Returns the number as text, with a separator between each group of three
|
||||
digits.
|
||||
|
||||
```lua
|
||||
BreakUpLargeNumbers(1234567) -- "1,234,567"
|
||||
BreakUpLargeNumbers(1234567.891) -- "1,234,567"
|
||||
BreakUpLargeNumbers(123.456) -- "123.5"
|
||||
BreakUpLargeNumbers(5) -- "5"
|
||||
BreakUpLargeNumbers(-1234567) -- "-1,234,567"
|
||||
BreakUpLargeNumbers(12345678, true) -- "12.3 million"
|
||||
BreakUpLargeNumbers(123456, true) -- "123,456"
|
||||
```
|
||||
|
||||
| Arg | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `largeNumber` | number | The number to write. |
|
||||
| `natural` | boolean, optional | `true` writes a number of one million or more with a word, for example `"1.2 million"`. The default is `false`. |
|
||||
|
||||
A whole number, or a number of 1000 or more, loses its decimals. The
|
||||
function removes them and does not round. Any other number keeps one
|
||||
decimal, rounded to the nearest tenth. A half rounds away from zero: `0.25`
|
||||
becomes `"0.3"`. A number with a decimal has no separator, so `999.96`
|
||||
becomes `"1000.0"`.
|
||||
|
||||
The separator is the global string `LARGE_NUMBER_SEPERATOR`, and the decimal
|
||||
point is `DECIMAL_SEPERATOR`. When the
|
||||
[`breakUpLargeNumbers`](#breakuplargenumbers-cvar) CVar is `0`, the function
|
||||
writes the digits with no separator.
|
||||
|
||||
With `natural`, a positive number of one million or more gets the largest
|
||||
word that fits: million, billion or trillion. The number before the word
|
||||
keeps one decimal when it is less than 100. The function removes the other
|
||||
decimals and does not round, and it does not show a decimal of `.0`:
|
||||
`2000000` becomes `"2 million"`. Smaller numbers and negative numbers are
|
||||
whole numbers with separators.
|
||||
|
||||
The words are the global strings `SECOND_NUMBER`, `THIRD_NUMBER` and
|
||||
`FOURTH_NUMBER`. On Chinese and Korean clients, these words count in steps
|
||||
of ten thousand, so the words start at 10,000. On German, Spanish and French
|
||||
clients, the words contain a `|7` plural code, which shows as written.
|
||||
|
||||
### `AbbreviateNumbers(number [, options])`
|
||||
|
||||
Returns a short form of the number, for example `"1.2K"` for 1234.
|
||||
|
||||
```lua
|
||||
AbbreviateNumbers(999.9) -- "999.9"
|
||||
AbbreviateNumbers(1234) -- "1.2K"
|
||||
AbbreviateNumbers(12345) -- "12K"
|
||||
AbbreviateNumbers(1999999) -- "1.9M"
|
||||
AbbreviateNumbers(-12345) -- "-12345"
|
||||
AbbreviateNumbers(1234, { breakpointData = {
|
||||
{ breakpoint = 100, abbreviation = "h", significandDivisor = 10,
|
||||
fractionDivisor = 10, abbreviationIsGlobal = false },
|
||||
} }) -- "12.3h"
|
||||
```
|
||||
|
||||
| Arg | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `number` | number | The number to shorten. |
|
||||
| `options` | table, optional | A `NumberAbbrevOptions` table. See below. |
|
||||
|
||||
The function uses a list of breakpoints, from the largest to the smallest.
|
||||
It uses the first breakpoint that the number reaches. The result is
|
||||
`floor(number / significandDivisor) / fractionDivisor`, followed by the
|
||||
abbreviation. Because the function rounds down, `1999999` becomes `"1.9M"`.
|
||||
When the number is less than all breakpoints, the function returns the
|
||||
number as Lua writes it. This is always true for a negative number.
|
||||
|
||||
The default breakpoints give one decimal below ten of a unit, and no decimal
|
||||
from ten up. The units are thousands (`K`), millions (`M`), billions and
|
||||
trillions. The abbreviations for billions and trillions name global strings
|
||||
that do not exist, so these numbers have no letter:
|
||||
`AbbreviateNumbers(1.5e10)` returns `"15"`. On Chinese and Korean clients,
|
||||
the default units are ten thousand and one hundred million.
|
||||
|
||||
`NumberAbbrevOptions` fields:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `breakpointData` | table, optional | A list of `NumberAbbreviationBreakpoint` tables, from the largest breakpoint to the smallest. The list replaces the default breakpoints. |
|
||||
| `locale` | string, optional | The language of the default breakpoints. `"koKR"`, `"zhCN"` and `"zhTW"` give the Chinese and Korean breakpoints. The default is the language of the client. |
|
||||
|
||||
A `config` field has no effect.
|
||||
|
||||
`NumberAbbreviationBreakpoint` fields:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `breakpoint` | number | The smallest number for this entry. |
|
||||
| `abbreviation` | string | The name of the global string that comes after the number. |
|
||||
| `significandDivisor` | number | The function divides the number by this value and rounds down. |
|
||||
| `fractionDivisor` | number | The function then divides by this value, which gives the decimals. |
|
||||
| `abbreviationIsGlobal` | boolean, optional | `false` puts the `abbreviation` text itself after the number. The default is `true`. |
|
||||
|
||||
When an abbreviation names a global string that does not exist, nothing
|
||||
comes after the number. When an entry of `breakpointData` is not a table, or
|
||||
a field has the wrong type, the function raises an error.
|
||||
|
||||
### `AbbreviateLargeNumbers(number [, options])`
|
||||
|
||||
Returns a short form of a large number, in whole thousands or millions.
|
||||
|
||||
```lua
|
||||
AbbreviateLargeNumbers(12345) -- "12,345"
|
||||
AbbreviateLargeNumbers(123456) -- "123 K"
|
||||
AbbreviateLargeNumbers(12345678) -- "12345 K"
|
||||
AbbreviateLargeNumbers(123456789) -- "123 M"
|
||||
AbbreviateLargeNumbers(-12345) -- "-12,345"
|
||||
```
|
||||
|
||||
The arguments are the same as for
|
||||
[`AbbreviateNumbers`](#abbreviatenumbersnumber--options), and the function
|
||||
uses breakpoints in the same way. The default breakpoints start at 100,000
|
||||
for thousands (`FIRST_NUMBER_CAP`, `" K"`) and at 100,000,000 for millions
|
||||
(`SECOND_NUMBER_CAP`, `" M"`). When the number is less than all breakpoints,
|
||||
the result is `BreakUpLargeNumbers(number)`.
|
||||
|
||||
The function rounds the result to the nearest whole number. With the
|
||||
breakpoint from the `AbbreviateNumbers` example, `123456` becomes `"1235h"`.
|
||||
On Chinese and Korean clients, or with one of their `locale` values, the
|
||||
result keeps its decimals. There, the default breakpoints use ten thousand
|
||||
and one hundred million: `123456789` becomes `1.23` and the
|
||||
`SECOND_NUMBER_CAP` string.
|
||||
|
||||
### `C_StringUtil.GetDefaultAbbreviationBreakpoints([locale])`
|
||||
|
||||
Returns a new list of the default breakpoints of `AbbreviateNumbers`, as
|
||||
`NumberAbbreviationBreakpoint` tables. You can change the list and give it
|
||||
as `breakpointData`.
|
||||
|
||||
| Arg | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `locale` | string, optional | The language, for example `"zhCN"`. The default is the language of the client. |
|
||||
|
||||
```lua
|
||||
local breakpoints = C_StringUtil.GetDefaultAbbreviationBreakpoints()
|
||||
-- breakpoints[1] = { breakpoint = 10000000000000,
|
||||
-- abbreviation = "FOURTH_NUMBER_CAP_NO_SPACE",
|
||||
-- significandDivisor = 1000000000000,
|
||||
-- fractionDivisor = 1, abbreviationIsGlobal = true }
|
||||
```
|
||||
|
||||
### `breakUpLargeNumbers` (CVar)
|
||||
|
||||
A setting for `BreakUpLargeNumbers` and the functions that use it. `1` (the
|
||||
default) puts a separator between each group of three digits. `0` writes the
|
||||
digits with no separator.
|
||||
|
||||
```
|
||||
/console breakUpLargeNumbers 0
|
||||
```
|
||||
|
||||
## Loot
|
||||
|
||||
Programmatic loot operations that the stock Lua surface doesn't
|
||||
|
||||
@@ -86,9 +86,11 @@ The DLL hooks the engine's own text pipeline — no companion addon:
|
||||
(`49934b0`, `b9d0705`).
|
||||
- **Chat fade** — icons fade in lockstep with their chat line (`3ca3b7e`).
|
||||
- **Edit boxes** — render the icons in their text, as 4.3.4 does; the caret,
|
||||
the mouse and the arrow keys treat an icon as one character, and Ctrl+C
|
||||
drops icons (4.3.4's 0x1500 strip mask). Typed markup stays literal by the
|
||||
engine's own input escape (below).
|
||||
the mouse and the arrow keys treat an icon as one character, Ctrl+C
|
||||
drops icons (4.3.4's 0x1500 strip mask), and a multi-line box's caret,
|
||||
selection and click rows follow the lines a tall icon moves (below;
|
||||
4.3.4's own rows do not). Typed markup stays literal by the engine's own
|
||||
input escape (below).
|
||||
- **Payload** — the full
|
||||
`|Tpath:height:width:offsetX:offsetY:texW:texH:left:right:top:bottom:r:g:b|t`:
|
||||
size, pen offset, sprite-sheet crop, and vertex tint. BLP and uncompressed
|
||||
@@ -377,7 +379,7 @@ funnel for exactly 4 callers:
|
||||
|
||||
- `Script_GetStringWidth` (0x0079E510)
|
||||
- the GameTooltip auto-size (0x00530640)
|
||||
- the editbox caret positioner (0x0077DE70)
|
||||
- the editbox selection-piece placer (0x0077DE70, a whole last line's width)
|
||||
- the layout effective-width vmethod (`FUN_00772930` = fs+0x24 vtbl +0x1C,
|
||||
which feeds auto-width fontstring layout)
|
||||
|
||||
@@ -523,6 +525,22 @@ the same input escape, so ClassicAPI's edit boxes need no special case:
|
||||
icon with the following character into one step; ours is its own step.
|
||||
- **Caret x** — `FUN_0077DA80` measures the line through
|
||||
`FUN_00772AE0`, whose co-hook adds the icons.
|
||||
- **Rows (multi-line)** — `PlaceCaret` (`0x77da80`), `PlaceSelectionPiece`
|
||||
(`0x77de70`) and `GetIndexAtPoint` (`0x77d0d0`) step rows of one height
|
||||
(font height + spacing, pixel-aligned), as 4.3.4's `UpdateVisibleCursor`,
|
||||
`UpdateHighlightArea` and `ConvertCoordinateToIndex` do — while the render
|
||||
moves a tall-icon line and every line after it down by the overflow
|
||||
(`InitializeText`; the emitter co-hook) and the box grows to match (the
|
||||
string-height co-hook). 4.3.4 has that fault too; `EditBox::IconRows`
|
||||
(`src/editbox/IconRows.cpp`) gives each placer the render's move, from
|
||||
`Text::InlineTexture::LineIconShift` over the box's own line table: the
|
||||
click's y is moved onto the uniform row of the icon-aware row under it
|
||||
before the engine maps it; a selection piece is re-anchored after the
|
||||
engine placed it, with the engine's own x; the caret likewise, then
|
||||
`OnCursorChanged` fired once with the moved y through
|
||||
`FUN_FRAME_RUN_SCRIPT_VARIADIC` exactly as `PlaceCaret` fires it (its own
|
||||
fire muted for the call by emptying the slot), so
|
||||
`ScrollingEditBoxMixin:ScrollCursorIntoView` scrolls to the real caret.
|
||||
- **Click → cursor, single-line window** — `GetIndexAtPoint` (`0x77d0d0`),
|
||||
`RebuildLabel` and `MeasureWindow` count characters with
|
||||
`FUN_FONTSTRING_FIT_TEXT` (`0x772bc0` → `0x44d860` → `0x5c6c50`), whose
|
||||
|
||||
+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
|
||||
|
||||
+190
-4
@@ -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
|
||||
@@ -988,7 +1002,8 @@ enum Offsets {
|
||||
// message. Every new message shifts the newest index, so EVERY visible slot
|
||||
// is re-SetText'd -> its inline icons must be recomputed; the engine builds
|
||||
// the line lazily at paint, so icons land one frame after the glyphs (the
|
||||
// one-frame lag). Text::InlineTexture co-hooks this and, post-refresh,
|
||||
// one-frame lag). ScrollingMessageFrame::Layout owns the hook and fans
|
||||
// out after it; Text::InlineTexture listens and, post-refresh,
|
||||
// forces each icon-bearing line to resolve+build and applies its icon
|
||||
// placement synchronously — pre-render (the refresh runs from the SMF's
|
||||
// Lua/event-driven methods, never mid-render), so icons draw with their
|
||||
@@ -2425,6 +2440,16 @@ enum Offsets {
|
||||
// right after that same unlock — Player::Equipment co-hooks it to do so.
|
||||
FUN_INV_SLOT_CHANGED_OBSERVER = 0x005DDCF0,
|
||||
|
||||
// CGPlayer_C::AddActiveWatchers / its removal — `void __thiscall
|
||||
// (CGPlayer_C *)`, plain `ret`. Register and remove the active player's
|
||||
// bank-PLAYER observers (inventory, quests, coinage, skills, the
|
||||
// 0x005DE4B0 event fields); 4.3.4's SetActiveMirrorHandlers (0x0056E430)
|
||||
// / UnsetActiveMirrorHandlers. Add runs on arrival (0x005DEA58) and
|
||||
// reactivation (0x005E9C00); remove only at 0x005E9B50, for the active
|
||||
// player.
|
||||
FUN_PLAYER_ADD_ACTIVE_WATCHERS = 0x005DD8A0,
|
||||
FUN_PLAYER_REMOVE_ACTIVE_WATCHERS = 0x005DE5B0,
|
||||
|
||||
// PLAYER_FIELD_INV_SLOT_HEAD — first of the 19 equipment-slot u64 GUID
|
||||
// fields in the player descriptor (0x4A8..0x538, stride 8; 0x540 is
|
||||
// the first equipped-bag slot). 0-based slot = (offset - 0x4A8) >> 3;
|
||||
@@ -4582,6 +4607,12 @@ enum Offsets {
|
||||
// (see OFF_UNIT_FIELD_CHANNEL_SPELL below); PLAYER_FLAGS only
|
||||
// exists on the +0xE68 sub-struct here.
|
||||
OFF_CGPLAYER_INFO = 0xE68,
|
||||
// PLAYER_SELF_RES_SPELL in that block (UpdateField table: player index
|
||||
// 0x40C, owner-only): the spell the server offers a dead player to raise
|
||||
// themselves (Soulstone Resurrection, Reincarnation), 0 for none. Also
|
||||
// its offset in observer bank DESC_OBSERVER_BANK_PLAYER. Read by
|
||||
// HasSoulstone (0x0048ACC6) and UseSoulstone (0x0048ADB6).
|
||||
OFF_CGPLAYER_INFO_SELF_RES_SPELL = 0x1030,
|
||||
// The engine's explored-area bit test (benilla `IsAreaExplored`):
|
||||
// __thiscall(this = player + OFF_CGPLAYER_INFO, int areaBit) → nonzero
|
||||
// when explored, ret 4. Reads `[this] + 0xE6C` (the 0x100-byte
|
||||
@@ -4993,6 +5024,16 @@ enum Offsets {
|
||||
// target is passed, so passing zero (no target) is fine for most
|
||||
// items. `flag` is 0 in every Script_UseContainerItem call site.
|
||||
FUN_ITEM_USE = 0x005D8D00,
|
||||
// The player's self-resurrect item — `CGItem_C *__thiscall(CGPlayer_C *)`,
|
||||
// plain `ret`: the first carried item (equipment, bags, backpack,
|
||||
// keyring) with an on-use spell of effect 94 (SELF_RESURRECT), or null.
|
||||
// HasSoulstone and UseSoulstone ask it when PLAYER_SELF_RES_SPELL is 0.
|
||||
FUN_PLAYER_SELF_RES_ITEM = 0x005ED630,
|
||||
// Script_UseSoulstone — the Lua C function itself, `int __fastcall(L)`;
|
||||
// reads no argument and pushes nothing. Sends CMSG_SELF_RES (0x2B3, no
|
||||
// body) when PLAYER_SELF_RES_SPELL is set, else cancels the cast
|
||||
// (0x006E4940(1, 1, 0x1C)) and uses FUN_PLAYER_SELF_RES_ITEM's item.
|
||||
FUN_SCRIPT_USE_SOULSTONE = 0x0048AD70,
|
||||
// `CGPlayer::AutoEquipCursorItem` — `__thiscall(CGPlayer *this, int flag)`.
|
||||
// The engine-internal helper that `Script_AutoEquipCursorItem`
|
||||
// (`0x0048A040`) is a thin wrapper around: that wrapper just
|
||||
@@ -12709,6 +12750,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.
|
||||
@@ -12811,6 +12864,93 @@ enum Offsets {
|
||||
OFF_SIMPLEHTML_FONTS = 0x350,
|
||||
VAR_SIMPLEHTML_LUA_TYPE_ID = 0x00CF4F64,
|
||||
FUN_SIMPLEHTML_READ_TEXT_TYPE = 0x00795D80,
|
||||
// The ScrollingMessageFrame's message ring. GetNumMessages (0x007934D0)
|
||||
// pushes the count. The ring's one writer, 0x00788150 (`__thiscall(smf,
|
||||
// text, const CImVector *color, int id)`, `ret 0xC`), called only by the
|
||||
// AddMessage method (FUN_SCRIPT_..._ADD_MESSAGE, `int __fastcall(L)`),
|
||||
// raises the count up to the limit, advances the position (wrapping at
|
||||
// the limit) and fills the record there. So message i, 1 the oldest, is
|
||||
// record (position - count + i), plus the limit when negative. Records
|
||||
// are 0x18 bytes: the text (an SStrDupA copy), the colour (a CImVector,
|
||||
// the dword 0xAARRGGBB: AddMessage builds it R, G, B with alpha 0xFF at
|
||||
// 0x00792ADD..0x00792B11), shown +0x8, the visible and fade times +0xC and
|
||||
// +0x10, and the integer id +0x14 (benilla-re ui/messageframe).
|
||||
FUN_SCRIPT_SCROLLINGMESSAGEFRAME_ADD_MESSAGE = 0x00792900,
|
||||
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT = 0x33C,
|
||||
OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES = 0x340,
|
||||
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS = 0x344, // the newest record; -1 when empty
|
||||
OFF_SCROLLINGMESSAGEFRAME_MESSAGES = 0x394, // the record array
|
||||
SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE = 0x18,
|
||||
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT = 0x0,
|
||||
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COLOR = 0x4,
|
||||
// The rest of the record as the ring writer 0x00788150 fills it: shown
|
||||
// (1), the frame's visible and fade times, and the integer id.
|
||||
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_SHOWN = 0x8,
|
||||
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_VISIBLE_TIME = 0xC,
|
||||
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_FADE_TIME = 0x10,
|
||||
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_ID = 0x14,
|
||||
// The bottom flag (4.3.4's m_atBottom): the ring writer lays the lines
|
||||
// out from the newest record when it is set (0x00788150 tail), and
|
||||
// ScrollToBottom re-arms the shown lines instead of laying out
|
||||
// (0x007886D0; benilla-re ui/messageframe "Scrolling brings lines back").
|
||||
OFF_SCROLLINGMESSAGEFRAME_AT_BOTTOM = 0x354,
|
||||
// The visible and fade times the writer copies into each record
|
||||
// (0x00788150: record+0xC = +0x364, record+0x10 = +0x368; the setters
|
||||
// 0x00788090 / 0x007880F0 store them).
|
||||
OFF_SCROLLINGMESSAGEFRAME_VISIBLE_TIME = 0x364,
|
||||
OFF_SCROLLINGMESSAGEFRAME_FADE_TIME = 0x368,
|
||||
// The colour (CImVector) of the object at OFF_SCROLLINGMESSAGEFRAME_FONT,
|
||||
// the message colour AddMessage takes without r, g, b
|
||||
// (0x00792B3C..0x00792B45: `[[smf+0x35C]+0x58]`).
|
||||
OFF_SCROLLINGMESSAGEFRAME_FONT_COLOR = 0x58,
|
||||
// The frame's scroll in retail's units, the lines it is scrolled up from
|
||||
// the newest message. ScrollToOffset (`void __thiscall(smf, int offset)`,
|
||||
// `ret 4`) is the body of SetScrollFromBottom (0x00793070), 4.3.4's
|
||||
// SetScrollOffset (0x0051E6F0) step for step: the record (position -
|
||||
// offset + count) % count, then it re-arms the shown lines (0x00788B80)
|
||||
// when that is the current scroll and lays the lines out from it
|
||||
// (FUN_SMF_DISPLAY_REFRESH) otherwise. ScrollOffset (`int
|
||||
// __thiscall(smf)`) is the inverse, (position - scroll + count) % count,
|
||||
// 0 when empty; the GetCurrentScroll method pushes it.
|
||||
FUN_SCROLLINGMESSAGEFRAME_SCROLL_TO_OFFSET = 0x00788700,
|
||||
FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET = 0x00788AC0,
|
||||
// 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,
|
||||
// RearmLines (`void __thiscall(smf)`, 4.3.4's RefreshMessages): every
|
||||
// shown line back to alpha 0xFF, shown, the frame's two times, and its
|
||||
// node shown (benilla-re ui/messageframe "Scrolling brings lines back").
|
||||
// FUN_SMF_DISPLAY_REFRESH (LayoutLines) owns +0x348 (the scroll record)
|
||||
// and +0x354 (at bottom = record == position) itself, and re-arms only
|
||||
// when it moves off the bottom; a layout at the bottom keeps the fade
|
||||
// state, so 4.3.4 re-arms explicitly after a removal's layout.
|
||||
FUN_SCROLLINGMESSAGEFRAME_REARM_LINES = 0x00788B80,
|
||||
FUN_SCRIPT_SCROLLINGMESSAGEFRAME_GET_CURRENT_SCROLL = 0x00793650,
|
||||
// The frame's OnFrameUpdate override (vtable+0x38, see
|
||||
// FUN_FRAME_ON_UPDATE), `void __thiscall(smf, float elapsed)`, `ret 4`:
|
||||
// FUN_FRAME_ON_UPDATE (OnUpdate), then the fader.
|
||||
FUN_SCROLLINGMESSAGEFRAME_ON_UPDATE = 0x00788460,
|
||||
// The line layout (FUN_SMF_DISPLAY_REFRESH) anchors a line once, when it
|
||||
// creates the line's font string, through the one SetPoint call at
|
||||
// CALL_SMF_LAYOUT_ANCHOR_LINE (`E8 rel32` to FUN_REGION_SET_POINT; ESI the
|
||||
// frame, EDI the font string, its parent at OFF_REGION_PARENT already set
|
||||
// by SetRegionParent 0x0077FD10): the first line (BOTTOMLEFT, the frame,
|
||||
// BOTTOMLEFT, left inset, bottom inset), each next one (BOTTOMLEFT, the
|
||||
// line before, TOPLEFT, 0, FUN_PIXEL_ALIGN_Y(spacing)), then
|
||||
// FUN_LAYOUT_RESIZE(1). The layout is the only maker of lines (the array
|
||||
// grow 0x00789060 and the node constructor 0x00788D90 have no other
|
||||
// caller). OnRectChanged (0x00788310) destroys every line when the message
|
||||
// area changes size and lays them out again; Clear (0x007882B0) only hides
|
||||
// them. The insets are the four floats SetInsets (0x00787FB0) stores and
|
||||
// OnRectChanged subtracts from the rect: bottom, left, top, right.
|
||||
CALL_SMF_LAYOUT_ANCHOR_LINE = 0x00788975,
|
||||
OFF_SCROLLINGMESSAGEFRAME_INSET_BOTTOM = 0x37C,
|
||||
OFF_SCROLLINGMESSAGEFRAME_INSET_LEFT = 0x380,
|
||||
OFF_SCROLLINGMESSAGEFRAME_INSET_TOP = 0x384,
|
||||
OFF_FONTSTRING_SPACING = 0xF4, // float, the line spacing
|
||||
// PixelAlignY - `float __stdcall(float)`, `ret 4`: rounds a vertical
|
||||
// length up to whole device pixels (benilla-re ui/regions).
|
||||
FUN_PIXEL_ALIGN_Y = 0x00766750,
|
||||
|
||||
// Focused-editbox global — holds the CSimpleEditBox that currently has
|
||||
// keyboard focus (the one showing a cursor), or 0 when no input field is
|
||||
@@ -12979,6 +13119,51 @@ enum Offsets {
|
||||
// ending at 0x0077D49D.
|
||||
PATCH_EDITBOX_TEXT_CHANGED_FIRE = 0x0077D485,
|
||||
OFF_EDITBOX_ONTEXTCHANGED_SLOT = 0x418,
|
||||
// --- Edit box rows (EditBox::IconRows) ---
|
||||
// The mode flags byte (benilla-re ui/editbox): bit 0 autoFocus, 1
|
||||
// multiLine, 2 numeric, 3 password, 4 ignoreArrows. Every row placer
|
||||
// below tests it (`TEST byte ptr [ESI+0x318], 0x2 / 0x8`). Not the dirty
|
||||
// word OFF_EDITBOX_FLAGS (+0x31c).
|
||||
OFF_EDITBOX_MODE_FLAGS = 0x318,
|
||||
EDITBOX_MODE_MULTILINE = 0x02,
|
||||
EDITBOX_MODE_PASSWORD = 0x08,
|
||||
// The password copy of the text (every byte '*'), the buffer the label
|
||||
// and the rows read with EDITBOX_MODE_PASSWORD set (4.3.4's
|
||||
// m_textHidden); OFF_EDITBOX_TEXT otherwise. RebuildLabel 0x0077D4D0
|
||||
// refreshes it.
|
||||
OFF_EDITBOX_TEXT_MASKED = 0x334,
|
||||
// The visible window in bytes: its first byte and its length (4.3.4's
|
||||
// m_visiblePosition / m_visibleLength). PlaceCaret hides the caret and
|
||||
// fires nothing for a cursor outside it (0x0077DA89..0x0077DAA7).
|
||||
OFF_EDITBOX_VISIBLE_START = 0x348,
|
||||
OFF_EDITBOX_VISIBLE_LENGTH = 0x34C,
|
||||
// The caret texture (4.3.4's m_cursorTexture), anchored by PlaceCaret.
|
||||
OFF_EDITBOX_CARET = 0x368,
|
||||
// OnCursorChanged's script slot; PlaceCaret fires it when non-zero
|
||||
// (0x0077DD8C).
|
||||
OFF_EDITBOX_ONCURSORCHANGED_SLOT = 0x428,
|
||||
// The three row placers, all `__thiscall`: PlaceCaret (no args; 4.3.4's
|
||||
// UpdateVisibleCursor), PlaceSelectionPiece `(editbox, texture, from, to)`
|
||||
// `ret 0xC` (UpdateHighlightArea) and GetIndexAtPoint `(editbox, float x,
|
||||
// float y, int *index)` `ret 0xC` (ConvertCoordinateToIndex). Each steps
|
||||
// rows of FUN_PIXEL_ALIGN_Y(fontH × scale) + FUN_PIXEL_ALIGN_Y(spacing ×
|
||||
// scale), the first two divided by the box's OFF_LAYOUT_SCALE (benilla-re
|
||||
// ui/editbox "The caret" / "Selection"; Ghidra). Co-hooked by
|
||||
// EditBox::IconRows: tall inline icons move the rows.
|
||||
FUN_EDITBOX_PLACE_CARET = 0x0077DA80,
|
||||
FUN_EDITBOX_PLACE_SELECTION_PIECE = 0x0077DE70,
|
||||
FUN_EDITBOX_INDEX_AT_POINT = 0x0077D0D0,
|
||||
// "%f%f%f%f" (.data; verified in the file at 0x479CB8) — the format
|
||||
// PlaceCaret fires OnCursorChanged with through
|
||||
// FUN_FRAME_RUN_SCRIPT_VARIADIC: x, y, width, height, each a double of
|
||||
// FUN_COORD_DIVIDE_BY_SCREEN_SCALE_X(FUN_COORD_GET_SCALED_SCREEN_ASPECT()
|
||||
// × [FLOAT_SCRIPT_COORD_SCALE] × value) — 4.3.4's
|
||||
// RunOnCursorChangedScript (0x0077DDA0..0x0077DE20).
|
||||
VAR_SCRIPT_FMT_FFFF = 0x00879CB8,
|
||||
// 1024.0f (.rdata; verified in the file at 0x3FFD68): the anchor→script
|
||||
// push factor every script coordinate push multiplies by (the `1024` of
|
||||
// the GetFont note at FUN_COORD_GET_SCALED_SCREEN_ASPECT).
|
||||
FLOAT_SCRIPT_COORD_SCALE = 0x007FFD68,
|
||||
// `void __thiscall(editbox, int maxLines)` — the SetHistoryLines internal
|
||||
// (FUN_0077cd90). Reallocates the history ring to maxLines; maxLines == 0
|
||||
// frees the whole buffer and zeroes the container. ClearHistory calls it
|
||||
@@ -12996,7 +13181,8 @@ enum Offsets {
|
||||
// core FUN_0044D670(fontHandle fs+0xE0, text, len, fontH*[fs+0x7C], &out,
|
||||
// fs+0x108, 0, flags fs+0x120), then `fs+0xFC = out / [fs+0x7C]`. Exactly 4
|
||||
// callers: Script_GetStringWidth (0x0079E510), GameTooltip auto-size
|
||||
// (0x00530640), the editbox caret positioner (0x0077DE70), and the layout
|
||||
// (0x00530640), the editbox selection-piece placer (0x0077DE70, a whole
|
||||
// last line's width), and the layout
|
||||
// effective-width vmethod (FUN_00772930 = fs+0x24 vtbl slot +0x1C: explicit
|
||||
// rect width else string width). Prologue PUSH EBX; PUSH ESI; MOV ESI,ECX;
|
||||
// FLD [ESI+0xFC] — MinHook-safe. Co-hooked by Text::InlineTexture to add
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,218 @@
|
||||
// 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_DeathInfo.GetSelfResurrectOptions()`, `C_DeathInfo.UseSelfResurrectOption
|
||||
// (optionType, id)` and `SELF_RES_SPELL_CHANGED` — the self-resurrection that
|
||||
// 1.12 serves through HasSoulstone (0x0048AC80) and UseSoulstone (0x0048AD70).
|
||||
//
|
||||
// 1.12 offers at most one way for a dead player to raise themselves, and both
|
||||
// functions pick it the same way: PLAYER_SELF_RES_SPELL when the server set it
|
||||
// (Soulstone Resurrection, Reincarnation), else the first carried item whose
|
||||
// on-use spell is a self-resurrect. So the list holds that one option, under
|
||||
// HasSoulstone's gate (a living player has none), and using it calls
|
||||
// UseSoulstone itself. Nothing in 1.12 limits a self-resurrection, so every
|
||||
// option can be used and none is limited.
|
||||
//
|
||||
// SELF_RES_SPELL_CHANGED is 4.3.4's: CGPlayer_C::SetActiveMirrorHandlers
|
||||
// watches PLAYER_SELF_RES_SPELL with OnUpdatePlayerScriptEvent. 1.12 watches
|
||||
// no such field, so the same watcher joins 1.12's AddActiveWatchers and
|
||||
// leaves with its removal, the lifetime of the engine's own player watchers.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "item/Link.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Death::SelfResurrect {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int OPTION_TYPE_SPELL = 0;
|
||||
constexpr int OPTION_TYPE_ITEM = 1;
|
||||
|
||||
const Game::Doc::Event kChangedDoc{
|
||||
"DeathInfo",
|
||||
"The player's self-resurrection spell changed. Read C_DeathInfo.GetSelfResurrectOptions.",
|
||||
{}};
|
||||
const Event::Custom::AutoReserve kChanged{"SELF_RES_SPELL_CHANGED", &kChangedDoc};
|
||||
|
||||
struct Option {
|
||||
const char *name;
|
||||
int optionType;
|
||||
int id;
|
||||
};
|
||||
|
||||
// HasSoulstone's answer: the option, or false while the player lives or has
|
||||
// none. `itemName` holds an item option's name.
|
||||
bool CurrentOption(Option &out, char *itemName, size_t itemNameSize) {
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
const uint8_t *desc = Unit::Identity::PlayerDescriptor();
|
||||
if (player == nullptr || desc == nullptr ||
|
||||
Game::Read<int32_t>(desc, Offsets::OFF_UNIT_FIELD_HEALTH) > 0)
|
||||
return false;
|
||||
const auto *info = Game::Read<const uint8_t *>(player, Offsets::OFF_CGPLAYER_INFO);
|
||||
const int spellID =
|
||||
info != nullptr ? Game::Read<int32_t>(info, Offsets::OFF_CGPLAYER_INFO_SELF_RES_SPELL) : 0;
|
||||
if (spellID != 0) {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
out = {rec != nullptr
|
||||
? Game::Read<const char *>(rec, Offsets::OFF_SPELL_NAMES +
|
||||
Game::Read<int>(Offsets::VAR_LOCALE_INDEX) *
|
||||
4)
|
||||
: "UNKNOWN",
|
||||
OPTION_TYPE_SPELL, spellID};
|
||||
return true;
|
||||
}
|
||||
using SelfResItem_t = const uint8_t *(__fastcall *)(const void *player);
|
||||
const uint8_t *item =
|
||||
reinterpret_cast<SelfResItem_t>(Offsets::FUN_PLAYER_SELF_RES_ITEM)(player);
|
||||
if (item == nullptr)
|
||||
return false;
|
||||
Item::Link::NameFromCGItem(item, itemName, itemNameSize);
|
||||
const auto *fields = Game::Read<const uint8_t *>(item, Offsets::OFF_UNIT_GUID_PTR);
|
||||
out = {itemName, OPTION_TYPE_ITEM,
|
||||
fields != nullptr ? Game::Read<int32_t>(fields, Offsets::OFF_OBJECT_FIELD_ENTRY) : 0};
|
||||
return true;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetSelfResurrectOptions(void *L) {
|
||||
if (Unit::Identity::PlayerObject() == nullptr)
|
||||
return 0;
|
||||
Game::Lua::NewTable(L);
|
||||
char itemName[0x400];
|
||||
Option option;
|
||||
if (CurrentOption(option, itemName, sizeof(itemName))) {
|
||||
Game::Lua::PushNumber(L, 1);
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "name", option.name);
|
||||
Game::Lua::SetFieldNumber(L, "optionType", option.optionType);
|
||||
Game::Lua::SetFieldNumber(L, "id", option.id);
|
||||
Game::Lua::SetFieldBool(L, "canUse", true);
|
||||
Game::Lua::SetFieldBool(L, "isLimited", false);
|
||||
Game::Lua::SetFieldNumber(L, "priority", 0);
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_UseSelfResurrectOption(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: C_DeathInfo.UseSelfResurrectOption(optionType, id)");
|
||||
return 0;
|
||||
}
|
||||
char itemName[0x400];
|
||||
Option option;
|
||||
if (CurrentOption(option, itemName, sizeof(itemName)) &&
|
||||
option.optionType == static_cast<int>(Game::Lua::ToNumber(L, 1)) &&
|
||||
option.id == static_cast<int>(Game::Lua::ToNumber(L, 2)))
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_USE_SOULSTONE)(L);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Observer callback ABI per the FUN_DESC_OBSERVER_REGISTER block in
|
||||
// Offsets.h: __fastcall(fieldOffset /*ecx*/, size /*edx*/, guidLo, guidHi,
|
||||
// oldValues*, userArg), returns 1, callee cleans 0x10. It runs only when the
|
||||
// field's bytes changed.
|
||||
int __fastcall SelfResSpellChanged(uint32_t /*fieldOffset*/, uint32_t /*size*/,
|
||||
uint32_t /*guidLo*/, uint32_t /*guidHi*/,
|
||||
const void * /*oldValues*/, void * /*userArg*/) {
|
||||
Event::Custom::Fire(kChanged.Slot(), "");
|
||||
return 1;
|
||||
}
|
||||
|
||||
using Register_t = void(__fastcall *)(int bank, uint32_t fieldOffset, uint32_t guidLo,
|
||||
uint32_t guidHi, int size, const void *callback,
|
||||
void *userArg1, void *userArg2);
|
||||
using Unregister_t = void(__fastcall *)(int bank, uint32_t fieldOffset, uint32_t guidLo,
|
||||
uint32_t guidHi, const void *callback, void *userArg1);
|
||||
using Watchers_t = void(__fastcall *)(void *player, void *edx);
|
||||
|
||||
Watchers_t g_origAddWatchers = nullptr;
|
||||
Watchers_t g_origRemoveWatchers = nullptr;
|
||||
|
||||
void __fastcall AddActiveWatchers_h(void *player, void *edx) {
|
||||
g_origAddWatchers(player, edx);
|
||||
const uint64_t guid = Unit::Identity::GuidForObject(player);
|
||||
reinterpret_cast<Register_t>(Offsets::FUN_DESC_OBSERVER_REGISTER)(
|
||||
Offsets::DESC_OBSERVER_BANK_PLAYER, Offsets::OFF_CGPLAYER_INFO_SELF_RES_SPELL,
|
||||
static_cast<uint32_t>(guid), static_cast<uint32_t>(guid >> 32), 4,
|
||||
reinterpret_cast<const void *>(&SelfResSpellChanged), nullptr, nullptr);
|
||||
}
|
||||
|
||||
// Removed before the original, while the registry is as the engine's own
|
||||
// removals find it.
|
||||
void __fastcall RemoveActiveWatchers_h(void *player, void *edx) {
|
||||
const uint64_t guid = Unit::Identity::GuidForObject(player);
|
||||
reinterpret_cast<Unregister_t>(Offsets::FUN_DESC_OBSERVER_UNREGISTER)(
|
||||
Offsets::DESC_OBSERVER_BANK_PLAYER, Offsets::OFF_CGPLAYER_INFO_SELF_RES_SPELL,
|
||||
static_cast<uint32_t>(guid), static_cast<uint32_t>(guid >> 32),
|
||||
reinterpret_cast<const void *>(&SelfResSpellChanged), nullptr);
|
||||
g_origRemoveWatchers(player, edx);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _addHook{Offsets::FUN_PLAYER_ADD_ACTIVE_WATCHERS,
|
||||
reinterpret_cast<void *>(&AddActiveWatchers_h),
|
||||
reinterpret_cast<void **>(&g_origAddWatchers)};
|
||||
const Game::HookAutoRegister _removeHook{Offsets::FUN_PLAYER_REMOVE_ACTIVE_WATCHERS,
|
||||
reinterpret_cast<void *>(&RemoveActiveWatchers_h),
|
||||
reinterpret_cast<void **>(&g_origRemoveWatchers)};
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kOptionTypeEntries[] = {
|
||||
{"Spell", OPTION_TYPE_SPELL},
|
||||
{"Item", OPTION_TYPE_ITEM},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kOptionFields[] = {
|
||||
Game::Doc::Req("name", "string"),
|
||||
Game::Doc::Req("optionType", "SelfResurrectOptionType"),
|
||||
Game::Doc::Req("id", "number", "The spell ID or item ID."),
|
||||
Game::Doc::Req("canUse", "bool"),
|
||||
Game::Doc::Req("isLimited", "bool"),
|
||||
Game::Doc::Req("priority", "number", "Lower comes first."),
|
||||
};
|
||||
const Game::Doc::Structure kOption{"SelfResurrectOption", "DeathInfo", kOptionFields,
|
||||
"A way for the dead player to resurrect themselves."};
|
||||
|
||||
const Game::Doc::Field kGetRets[] = {
|
||||
Game::Doc::Req("options", "table", "Array of SelfResurrectOption; empty while alive."),
|
||||
};
|
||||
const Game::Doc::Function kGetSelfResurrectOptions{
|
||||
"The ways the dead player can resurrect themselves.", {}, kGetRets};
|
||||
|
||||
const Game::Doc::Field kUseArgs[] = {
|
||||
Game::Doc::Req("optionType", "SelfResurrectOptionType"),
|
||||
Game::Doc::Req("id", "number"),
|
||||
};
|
||||
const Game::Doc::Function kUseSelfResurrectOption{
|
||||
"Resurrects the dead player with one of the options from GetSelfResurrectOptions.",
|
||||
kUseArgs, {}};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_DeathInfo", "GetSelfResurrectOptions",
|
||||
&Script_GetSelfResurrectOptions, &kGetSelfResurrectOptions);
|
||||
Game::Lua::RegisterTableFunction("C_DeathInfo", "UseSelfResurrectOption",
|
||||
&Script_UseSelfResurrectOption, &kUseSelfResurrectOption);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "SelfResurrectOptionType", kOptionTypeEntries,
|
||||
sizeof(kOptionTypeEntries) / sizeof(kOptionTypeEntries[0]),
|
||||
"DeathInfo");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Death::SelfResurrect
|
||||
@@ -0,0 +1,308 @@
|
||||
// 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/>.
|
||||
|
||||
// Tall inline icons move the edit box's rows.
|
||||
//
|
||||
// A multi-line edit box places its caret, its selection pieces and a click's
|
||||
// cursor by rows of one height — the label's font height plus its spacing,
|
||||
// pixel-aligned (FUN_EDITBOX_PLACE_CARET, FUN_EDITBOX_PLACE_SELECTION_PIECE,
|
||||
// FUN_EDITBOX_INDEX_AT_POINT; 4.3.4's UpdateVisibleCursor, UpdateHighlightArea
|
||||
// and ConvertCoordinateToIndex step the same rows). The label's text leaves
|
||||
// those rows once a line holds an icon taller than the font: the render moves
|
||||
// that line and every line after it down by the overflow (4.3.4's
|
||||
// InitializeText; the emitter co-hook in Text::InlineTexture), and the box
|
||||
// grows by the same amount through the string-height co-hook. So below the
|
||||
// first tall-icon line the caret and the highlight sat above their text by
|
||||
// the summed overflow, and OnCursorChanged reported that y, which
|
||||
// ScrollingEditBoxMixin:ScrollCursorIntoView scrolls by — pfUI's chat copy
|
||||
// box with faction-emblem icons. 4.3.4 has the same fault; nothing it ships
|
||||
// puts a tall icon in an edit box.
|
||||
//
|
||||
// Each placer keeps its engine code and gets the move the render makes, from
|
||||
// Text::InlineTexture::LineIconShift over the box's own line table (the
|
||||
// breaks the render lays out), split by the label's vertical justify as the
|
||||
// emitter splits it:
|
||||
// - the click: its y is moved onto the uniform row that the icon-aware row
|
||||
// under it has, before the engine maps it;
|
||||
// - a selection piece: re-anchored after the engine placed it, with the
|
||||
// engine's own x, lower by the move;
|
||||
// - the caret: the same re-anchor, then OnCursorChanged fired once with the
|
||||
// moved y through the engine's own forwarder. The engine's fire is muted
|
||||
// for the call by emptying the script slot: it reads the row y before
|
||||
// anything can correct it.
|
||||
// A single-line box has one row and needs nothing: its label is justified
|
||||
// MIDDLE in a rect of fixed height, so its text stays centred on the caret.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "text/InlineTexture.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace EditBox::IconRows {
|
||||
|
||||
namespace {
|
||||
|
||||
using PlaceCaret_t = void(__fastcall *)(void *editBox);
|
||||
using PlaceSelectionPiece_t = void(__fastcall *)(void *editBox, void *edx, void *piece, int from, int to);
|
||||
using IndexAtPoint_t = void(__fastcall *)(void *editBox, void *edx, float x, float y, int *index);
|
||||
using FontHeight_t = float(__fastcall *)(void *fs, void *edx, int mode);
|
||||
using MeasureRun_t = float(__fastcall *)(void *fs, void *edx, const uint8_t *text, int len);
|
||||
using PixelAlignY_t = float(__stdcall *)(float length);
|
||||
using GetRect_t = int(__fastcall *)(void *anchor, void *edx, float *out);
|
||||
using ClearAllPoints_t = void(__fastcall *)(void *anchor);
|
||||
using SetPoint_t = void(__thiscall *)(void *anchor, int point, void *relAnchor, int relPoint, float x, float y,
|
||||
int flag);
|
||||
using Resize_t = void(__fastcall *)(void *anchor, void *edx, int force);
|
||||
using Extent_t = float(__fastcall *)(void *anchor, void *edx);
|
||||
using ScreenAspect_t = float(__cdecl *)();
|
||||
using DivideByScreenScaleX_t = float(__stdcall *)(float value);
|
||||
using FireVariadic_t = void(__cdecl *)(void *frame, uint32_t *slotPtr, const char *fmt, ...);
|
||||
|
||||
PlaceCaret_t g_origPlaceCaret = nullptr;
|
||||
PlaceSelectionPiece_t g_origPlaceSelectionPiece = nullptr;
|
||||
IndexAtPoint_t g_origIndexAtPoint = nullptr;
|
||||
|
||||
void *AnchorOf(void *region) { return static_cast<uint8_t *>(region) + Offsets::OFF_REGION_ANCHOR; }
|
||||
|
||||
// A multi-line box's rows: its label, the buffer the line table indexes (the
|
||||
// '*' copy of a password box) and the table — each line's first byte, the
|
||||
// last entry the end.
|
||||
struct Rows {
|
||||
void *fs = nullptr;
|
||||
const uint8_t *text = nullptr;
|
||||
const uint32_t *starts = nullptr;
|
||||
int count = 0; // entries; lines = count - 1
|
||||
float scale = 1.0f; // the box's OFF_LAYOUT_SCALE
|
||||
};
|
||||
|
||||
bool MultiLineRows(void *editBox, Rows &rows) {
|
||||
const uint8_t mode = Game::Read<uint8_t>(editBox, Offsets::OFF_EDITBOX_MODE_FLAGS);
|
||||
if ((mode & Offsets::EDITBOX_MODE_MULTILINE) == 0)
|
||||
return false;
|
||||
rows.fs = Game::Read<void *>(editBox, Offsets::OFF_EDITBOX_FONTSTRING);
|
||||
rows.text = Game::Read<const uint8_t *>(editBox, (mode & Offsets::EDITBOX_MODE_PASSWORD)
|
||||
? Offsets::OFF_EDITBOX_TEXT_MASKED
|
||||
: Offsets::OFF_EDITBOX_TEXT);
|
||||
rows.starts = Game::Read<const uint32_t *>(editBox, Offsets::OFF_EDITBOX_LINE_STARTS);
|
||||
rows.count = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_LINE_COUNT);
|
||||
rows.scale = Game::Read<float>(editBox, Offsets::OFF_LAYOUT_SCALE);
|
||||
return rows.fs != nullptr && rows.text != nullptr && rows.starts != nullptr && rows.count >= 2 &&
|
||||
rows.scale > 0.0f;
|
||||
}
|
||||
|
||||
// The line holding byte `pos` — the placers' walk down the table.
|
||||
int LineOf(const Rows &rows, uint32_t pos) {
|
||||
int line = 0;
|
||||
while (pos >= rows.starts[line + 1]) {
|
||||
if (line >= rows.count - 2)
|
||||
break;
|
||||
++line;
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
bool LineShift(const Rows &rows, int line, float &before, float &after) {
|
||||
before = 0.0f;
|
||||
after = 0.0f;
|
||||
const uint32_t start = rows.starts[line];
|
||||
const uint32_t end = rows.starts[line + 1];
|
||||
return end > start &&
|
||||
Text::InlineTexture::LineIconShift(rows.fs, rows.text + start, static_cast<int>(end - start), before, after);
|
||||
}
|
||||
|
||||
// How far the render moves line `line`'s text below its uniform row: the
|
||||
// moves of every line above it, and its own text's move.
|
||||
float ShiftOf(const Rows &rows, int line) {
|
||||
float shift = 0.0f;
|
||||
for (int i = 0; i <= line; ++i) {
|
||||
float before = 0.0f;
|
||||
float after = 0.0f;
|
||||
if (!LineShift(rows, i, before, after))
|
||||
continue;
|
||||
shift += before;
|
||||
if (i < line)
|
||||
shift += after;
|
||||
}
|
||||
return shift;
|
||||
}
|
||||
|
||||
// The placers' row in the box's parent units: the label's font height and
|
||||
// spacing, each pixel-aligned at the box's scale (PlaceCaret
|
||||
// 0x0077DAEB..0x0077DB0E).
|
||||
float RowHeight(const Rows &rows) {
|
||||
const float fontH = reinterpret_cast<FontHeight_t>(Offsets::FUN_FONTSTRING_FONT_HEIGHT)(rows.fs, nullptr, 1);
|
||||
const float spacing = Game::Read<float>(rows.fs, Offsets::OFF_FONTSTRING_SPACING);
|
||||
const auto align = reinterpret_cast<PixelAlignY_t>(Offsets::FUN_PIXEL_ALIGN_Y);
|
||||
return align(fontH * rows.scale) + align(spacing * rows.scale);
|
||||
}
|
||||
|
||||
float MeasureRun(const Rows &rows, uint32_t from, uint32_t to) {
|
||||
return reinterpret_cast<MeasureRun_t>(Offsets::FUN_FONTSTRING_MEASURE_SUBSTRING)(rows.fs, nullptr, rows.text + from,
|
||||
static_cast<int>(to - from));
|
||||
}
|
||||
|
||||
// The x the placers anchor byte `pos` of `line` at, and the label point they
|
||||
// anchor to, by the label's horizontal justify (OFF_FONTSTRING_FLAGS & 7).
|
||||
// False for none, which anchors nothing.
|
||||
bool AnchorX(const Rows &rows, int line, uint32_t pos, float &x, int &relPoint) {
|
||||
const uint32_t start = rows.starts[line];
|
||||
const uint32_t end = rows.starts[line + 1];
|
||||
switch (Game::Read<uint32_t>(rows.fs, Offsets::OFF_FONTSTRING_FLAGS) & 7u) {
|
||||
case 1: // LEFT
|
||||
x = (pos == start) ? 0.0f : MeasureRun(rows, start, pos);
|
||||
relPoint = Offsets::FRAMEPOINT_TOPLEFT;
|
||||
return true;
|
||||
case 2: // CENTER
|
||||
x = (pos == start) ? 0.0f : MeasureRun(rows, start, pos);
|
||||
x -= MeasureRun(rows, start, end) * 0.5f;
|
||||
relPoint = Offsets::FRAMEPOINT_TOP;
|
||||
return true;
|
||||
case 4: // RIGHT
|
||||
x = (pos == end) ? 0.0f : -MeasureRun(rows, pos, end);
|
||||
relPoint = Offsets::FRAMEPOINT_TOPRIGHT;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Anchors `region` as the placer did, at `y`.
|
||||
void Anchor(const Rows &rows, void *region, int relPoint, float x, float y, int flag) {
|
||||
void *anchor = AnchorOf(region);
|
||||
reinterpret_cast<ClearAllPoints_t>(Offsets::FUN_LAYOUT_CLEAR_ALL_POINTS)(anchor);
|
||||
reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT)(anchor, Offsets::FRAMEPOINT_TOPLEFT, AnchorOf(rows.fs),
|
||||
relPoint, x, y, flag);
|
||||
}
|
||||
|
||||
// --- the click ---------------------------------------------------------------
|
||||
|
||||
void __fastcall IndexAtPoint_h(void *editBox, void *edx, float x, float y, int *index) {
|
||||
Rows rows;
|
||||
if (!MultiLineRows(editBox, rows))
|
||||
return g_origIndexAtPoint(editBox, edx, x, y, index);
|
||||
float rect[4] = {0.0f, 0.0f, 0.0f, 0.0f}; // bottom, left, top, right
|
||||
if (reinterpret_cast<GetRect_t>(Offsets::FUN_REGION_GET_RECT)(AnchorOf(rows.fs), nullptr, rect) == 0)
|
||||
return g_origIndexAtPoint(editBox, edx, x, y, index);
|
||||
// The engine walks rows down from the label's top while the click is
|
||||
// below the row (strictly), stopping at the last line. Walk the rows as
|
||||
// the render lays them out: each taller by its line's moves.
|
||||
const float row = RowHeight(rows);
|
||||
const float below = rect[2] - y;
|
||||
int line = 0;
|
||||
float top = 0.0f; // the icon-aware top of `line`, below the label's top
|
||||
bool moved = false;
|
||||
for (;;) {
|
||||
float before = 0.0f;
|
||||
float after = 0.0f;
|
||||
if (LineShift(rows, line, before, after))
|
||||
moved = true;
|
||||
const float height = row + (before + after) * rows.scale;
|
||||
if (!(below > top + height) || line >= rows.count - 2)
|
||||
break;
|
||||
top += height;
|
||||
++line;
|
||||
}
|
||||
// Without a move the rows are the engine's own; with one, the click is
|
||||
// moved onto the middle of the uniform row the engine maps to `line`.
|
||||
const float yMapped = moved ? rect[2] - (static_cast<float>(line) + 0.5f) * row : y;
|
||||
g_origIndexAtPoint(editBox, edx, x, yMapped, index);
|
||||
}
|
||||
|
||||
// --- a selection piece --------------------------------------------------------
|
||||
|
||||
void __fastcall PlaceSelectionPiece_h(void *editBox, void *edx, void *piece, int from, int to) {
|
||||
Rows rows;
|
||||
if (!MultiLineRows(editBox, rows) || from < 0)
|
||||
return g_origPlaceSelectionPiece(editBox, edx, piece, from, to);
|
||||
const int line = LineOf(rows, static_cast<uint32_t>(from));
|
||||
const float shift = ShiftOf(rows, line);
|
||||
g_origPlaceSelectionPiece(editBox, edx, piece, from, to);
|
||||
if (!(shift > 0.0f) || piece == nullptr)
|
||||
return;
|
||||
// The piece starts at `from`, clamped to its line.
|
||||
uint32_t pos = static_cast<uint32_t>(from);
|
||||
if (pos < rows.starts[line])
|
||||
pos = rows.starts[line];
|
||||
float x = 0.0f;
|
||||
int relPoint = 0;
|
||||
if (!AnchorX(rows, line, pos, x, relPoint))
|
||||
return;
|
||||
const float y = -(RowHeight(rows) / rows.scale) * static_cast<float>(line) - shift;
|
||||
Anchor(rows, piece, relPoint, x, y, 0);
|
||||
reinterpret_cast<Resize_t>(Offsets::FUN_LAYOUT_RESIZE)(AnchorOf(piece), nullptr, 1);
|
||||
}
|
||||
|
||||
// --- the caret ----------------------------------------------------------------
|
||||
|
||||
void __fastcall PlaceCaret_h(void *editBox) {
|
||||
Rows rows;
|
||||
if (!MultiLineRows(editBox, rows))
|
||||
return g_origPlaceCaret(editBox);
|
||||
const int cursor = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_CURSOR_BYTE);
|
||||
const int visibleStart = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_VISIBLE_START);
|
||||
const int visibleLength = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_VISIBLE_LENGTH);
|
||||
// Outside the window the engine hides the caret and fires nothing.
|
||||
if (cursor < visibleStart || cursor > visibleStart + visibleLength)
|
||||
return g_origPlaceCaret(editBox);
|
||||
const int line = LineOf(rows, static_cast<uint32_t>(cursor));
|
||||
const float shift = ShiftOf(rows, line);
|
||||
if (!(shift > 0.0f))
|
||||
return g_origPlaceCaret(editBox);
|
||||
uint32_t *slot = Game::Ptr<uint32_t>(editBox, Offsets::OFF_EDITBOX_ONCURSORCHANGED_SLOT);
|
||||
const uint32_t handler = slot[0];
|
||||
slot[0] = 0;
|
||||
g_origPlaceCaret(editBox);
|
||||
slot[0] = handler;
|
||||
void *caret = Game::Read<void *>(editBox, Offsets::OFF_EDITBOX_CARET);
|
||||
if (caret == nullptr)
|
||||
return;
|
||||
const float y = -(RowHeight(rows) / rows.scale) * static_cast<float>(line) - shift;
|
||||
float x = 0.0f;
|
||||
int relPoint = 0;
|
||||
if (AnchorX(rows, line, static_cast<uint32_t>(cursor), x, relPoint))
|
||||
Anchor(rows, caret, relPoint, x, y, 1);
|
||||
if (handler == 0)
|
||||
return;
|
||||
// OnCursorChanged(x, y, width, height) as the engine fires it: each
|
||||
// value by the anchor→script factor, pushed as a double.
|
||||
void *anchor = AnchorOf(caret);
|
||||
void **vtable = *static_cast<void ***>(anchor);
|
||||
const float width = reinterpret_cast<Extent_t>(vtable[7])(anchor, nullptr); // +0x1C GetWidth
|
||||
const float height = reinterpret_cast<Extent_t>(vtable[8])(anchor, nullptr); // +0x20 GetHeight
|
||||
const auto aspect = reinterpret_cast<ScreenAspect_t>(Offsets::FUN_COORD_GET_SCALED_SCREEN_ASPECT);
|
||||
const auto divide = reinterpret_cast<DivideByScreenScaleX_t>(Offsets::FUN_COORD_DIVIDE_BY_SCREEN_SCALE_X);
|
||||
const float factor = Game::Read<float>(Offsets::FLOAT_SCRIPT_COORD_SCALE);
|
||||
const auto push = [&](float value) { return static_cast<double>(divide(aspect() * factor * value)); };
|
||||
const double args[4] = {push(x), push(y), push(width), push(height)};
|
||||
void *L = Game::Lua::State();
|
||||
const int savedTop = (L != nullptr) ? Game::Lua::GetTop(L) : 0;
|
||||
reinterpret_cast<FireVariadic_t>(Offsets::FUN_FRAME_RUN_SCRIPT_VARIADIC)(
|
||||
editBox, slot, reinterpret_cast<const char *>(Offsets::VAR_SCRIPT_FMT_FFFF), args[0], args[1], args[2],
|
||||
args[3]);
|
||||
if (L != nullptr)
|
||||
Game::Lua::SetTop(L, savedTop);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _caretHook{Offsets::FUN_EDITBOX_PLACE_CARET, reinterpret_cast<void *>(&PlaceCaret_h),
|
||||
reinterpret_cast<void **>(&g_origPlaceCaret)};
|
||||
const Game::HookAutoRegister _pieceHook{Offsets::FUN_EDITBOX_PLACE_SELECTION_PIECE,
|
||||
reinterpret_cast<void *>(&PlaceSelectionPiece_h),
|
||||
reinterpret_cast<void **>(&g_origPlaceSelectionPiece)};
|
||||
const Game::HookAutoRegister _clickHook{Offsets::FUN_EDITBOX_INDEX_AT_POINT,
|
||||
reinterpret_cast<void *>(&IndexAtPoint_h),
|
||||
reinterpret_cast<void **>(&g_origIndexAtPoint)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace EditBox::IconRows
|
||||
@@ -45,12 +45,13 @@ Util\ItemUtil.lua
|
||||
Util\AddOnCompat.lua
|
||||
Util\FunctionUtil.lua
|
||||
Util\CacheUtil.lua
|
||||
Util\Pools.lua
|
||||
Util\SecureTypes.lua
|
||||
Util\ProxyUtil.lua
|
||||
Util\Pools.lua
|
||||
Util\FrameWatcher.lua
|
||||
Util\TimedCallback.lua
|
||||
Util\EasingUtil.lua
|
||||
Util\ScriptAnimationUtil.lua
|
||||
Util\Interpolator.lua
|
||||
Util\AnchorUtil.lua
|
||||
Util\RegionUtil.lua
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -100,6 +100,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";
|
||||
|
||||
@@ -101,6 +101,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 +189,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";
|
||||
|
||||
@@ -100,6 +100,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";
|
||||
|
||||
@@ -100,6 +100,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"/>
|
||||
|
||||
@@ -100,6 +100,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 = "Новый комплект";
|
||||
|
||||
@@ -100,6 +100,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 = "新的方案";
|
||||
|
||||
@@ -100,6 +100,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 = "新設定";
|
||||
|
||||
+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,
|
||||
|
||||
@@ -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,182 @@
|
||||
// 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/>.
|
||||
|
||||
// `smf:AddOnDisplayRefreshedCallback(callback)`, retail's: callback(frame)
|
||||
// runs each time the frame refreshes what it shows.
|
||||
//
|
||||
// Retail's mixin (Blizzard_SharedXML/ScrollingMessageFrame.lua) marks its
|
||||
// display dirty on each change - a message, a scroll, a resize, a fade reset -
|
||||
// and refreshes in its OnPostUpdate. So the callbacks run once per update of
|
||||
// a shown frame that changed, in the order they were added, each under the
|
||||
// error handler (secureexecuterange). The mark is cleared after them, so a
|
||||
// change a callback makes (a scroll bar setting the offset) does not run them
|
||||
// again.
|
||||
//
|
||||
// 1.12 refreshes what the frame shows in its line layout
|
||||
// (ScrollingMessageFrame::Layout), which marks the frame. A message added
|
||||
// while the frame is scrolled up changes no line and runs no layout, but it
|
||||
// moves the scroll offset and range, so AddMessage marks the frame too, as
|
||||
// ScrollByAmount does for retail's fade reset. The callbacks run after the
|
||||
// frame's own update (FUN_SCROLLINGMESSAGEFRAME_ON_UPDATE: OnUpdate, then the
|
||||
// fader), which the engine's update pass calls once per frame for each shown
|
||||
// frame - where retail's OnPostUpdate runs.
|
||||
|
||||
#include "scrollingmessageframe/DisplayRefreshed.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "scrollingmessageframe/Layout.h"
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
namespace ScrollingMessageFrame::DisplayRefreshed {
|
||||
|
||||
namespace {
|
||||
|
||||
// Frames with callbacks, and whether each has a refresh pending. Every frame
|
||||
// is rebuilt on /reload, so the map is cleared then.
|
||||
std::unordered_map<void *, bool> g_frames;
|
||||
|
||||
// The frame-table → callback-list table, in the registry of the Lua state
|
||||
// that made it. LUA_NOREF until first use in each state.
|
||||
constexpr int kNoRef = -2;
|
||||
int g_callbacksRef = kNoRef;
|
||||
|
||||
// Runs the callbacks of the frame, each as callback(frame) under the
|
||||
// engine's script error handler.
|
||||
void RunCallbacks(void *smf) {
|
||||
using namespace Game::Lua;
|
||||
void *L = State();
|
||||
if (L == nullptr || g_callbacksRef == kNoRef)
|
||||
return;
|
||||
const int top = GetTop(L);
|
||||
PushObject(L, smf);
|
||||
const int frame = GetTop(L);
|
||||
RawGetI(L, REGISTRY_INDEX, g_callbacksRef);
|
||||
PushValue(L, frame);
|
||||
RawGet(L, -2);
|
||||
const int callbacks = GetTop(L);
|
||||
if (Type(L, callbacks) == TYPE_TABLE) {
|
||||
const int errRef = Game::Read<int>(static_cast<uintptr_t>(Offsets::VAR_FRAMESCRIPT_ERROR_HANDLER_REF));
|
||||
for (int i = 1;; ++i) {
|
||||
int errIdx = 0;
|
||||
if (errRef > 0) {
|
||||
RawGetI(L, REGISTRY_INDEX, errRef);
|
||||
errIdx = GetTop(L);
|
||||
}
|
||||
RawGetI(L, callbacks, i);
|
||||
if (Type(L, -1) == TYPE_NIL)
|
||||
break;
|
||||
PushValue(L, frame);
|
||||
PCall(L, 1, 0, errIdx);
|
||||
SetTop(L, callbacks);
|
||||
}
|
||||
}
|
||||
SetTop(L, top);
|
||||
}
|
||||
|
||||
using OnUpdate_t = void(__fastcall *)(void *smf, void *edx, float elapsed);
|
||||
OnUpdate_t g_origOnUpdate = nullptr;
|
||||
|
||||
void __fastcall OnUpdate_h(void *smf, void *edx, float elapsed) {
|
||||
g_origOnUpdate(smf, edx, elapsed);
|
||||
const auto it = g_frames.find(smf);
|
||||
if (it == g_frames.end() || !it->second)
|
||||
return;
|
||||
RunCallbacks(smf);
|
||||
const auto again = g_frames.find(smf); // a callback can add frames
|
||||
if (again != g_frames.end())
|
||||
again->second = false;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _updateHook{Offsets::FUN_SCROLLINGMESSAGEFRAME_ON_UPDATE,
|
||||
reinterpret_cast<void *>(&OnUpdate_h),
|
||||
reinterpret_cast<void **>(&g_origOnUpdate)};
|
||||
|
||||
const ScrollingMessageFrame::Layout::AutoSubscribe _layoutSub{&MarkDirty};
|
||||
|
||||
int __fastcall Script_AddOnDisplayRefreshedCallback(void *L) {
|
||||
using namespace Game::Lua;
|
||||
void *frame = ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
|
||||
if (frame == nullptr)
|
||||
return 0;
|
||||
if (Type(L, 2) != TYPE_FUNCTION) {
|
||||
Error(L, "Usage: %s:AddOnDisplayRefreshedCallback(callback)", ObjectDisplayName(frame));
|
||||
return 0;
|
||||
}
|
||||
if (g_callbacksRef == kNoRef) {
|
||||
NewTable(L);
|
||||
g_callbacksRef = Ref(L, REGISTRY_INDEX);
|
||||
}
|
||||
RawGetI(L, REGISTRY_INDEX, g_callbacksRef);
|
||||
const int frames = GetTop(L);
|
||||
PushValue(L, 1);
|
||||
RawGet(L, frames);
|
||||
if (Type(L, -1) != TYPE_TABLE) {
|
||||
SetTop(L, frames);
|
||||
NewTable(L);
|
||||
PushValue(L, 1);
|
||||
PushValue(L, -2);
|
||||
RawSet(L, frames); // frames[self] = callbacks
|
||||
}
|
||||
const int callbacks = GetTop(L);
|
||||
int n = 1;
|
||||
for (;; ++n) {
|
||||
RawGetI(L, callbacks, n);
|
||||
const bool end = Type(L, -1) == TYPE_NIL;
|
||||
SetTop(L, -2);
|
||||
if (end)
|
||||
break;
|
||||
}
|
||||
PushNumber(L, n);
|
||||
PushValue(L, 2);
|
||||
RawSet(L, callbacks);
|
||||
g_frames.emplace(frame, false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry kMethods[] = {
|
||||
{"AddOnDisplayRefreshedCallback", &Script_AddOnDisplayRefreshedCallback},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("callback", "function", "Called as callback(frame)."),
|
||||
};
|
||||
const Game::Doc::Function kAddOnDisplayRefreshedCallback{
|
||||
"Adds a function that runs after the frame refreshes the messages it shows.", kArgs, {}};
|
||||
|
||||
const Game::Doc::Method kDocs[] = {
|
||||
{"AddOnDisplayRefreshedCallback", &kAddOnDisplayRefreshedCallback},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
// Each registration pass is a new Lua state; the old state's ref is gone.
|
||||
g_callbacksRef = kNoRef;
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY),
|
||||
kMethods, 1, kDocs, 1);
|
||||
}
|
||||
|
||||
void PrepareForReload() { g_frames.clear(); }
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
const Game::ReloadAutoRegister _reload{&PrepareForReload};
|
||||
|
||||
} // namespace
|
||||
|
||||
void MarkDirty(void *smf) {
|
||||
const auto it = g_frames.find(smf);
|
||||
if (it != g_frames.end())
|
||||
it->second = true;
|
||||
}
|
||||
|
||||
} // namespace ScrollingMessageFrame::DisplayRefreshed
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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
|
||||
|
||||
namespace ScrollingMessageFrame::DisplayRefreshed {
|
||||
|
||||
// Marks a change to what the frame shows that its line layout does not
|
||||
// report; the frame's display-refreshed callbacks run at its next update.
|
||||
void MarkDirty(void *smf);
|
||||
|
||||
} // namespace ScrollingMessageFrame::DisplayRefreshed
|
||||
@@ -0,0 +1,194 @@
|
||||
// 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 insert mode on ScrollingMessageFrame: `SetInsertMode(mode)`,
|
||||
// `GetInsertMode()` and the SCROLLING_MESSAGE_FRAME_INSERT_MODE_* globals.
|
||||
//
|
||||
// Retail's mixin (Blizzard_SharedXML/ScrollingMessageFrame.lua, RefreshLayout)
|
||||
// stacks the lines up from the bottom in BOTTOM mode, the newest message at
|
||||
// the bottom, and down from the top in TOP mode: the first line's TOPLEFT on
|
||||
// the frame's TOPLEFT, each next line's TOPLEFT on the BOTTOMLEFT of the line
|
||||
// before, the spacing below it. The scroll offset counts from the newest
|
||||
// message in both modes. Setting the mode the frame has does nothing; a change
|
||||
// lays every line out again and refreshes the display.
|
||||
//
|
||||
// 1.12's line layout is retail's BOTTOM mode. It anchors a line once, when it
|
||||
// makes the line, through one SetPoint call (Offsets.h,
|
||||
// CALL_SMF_LAYOUT_ANCHOR_LINE), so that call is redirected: a line of a frame
|
||||
// in TOP mode takes the TOP anchors, every other line the engine's own. The
|
||||
// layout makes the lines again when the frame changes size, which the
|
||||
// redirect covers too. When a frame's mode changes, its lines are anchored
|
||||
// again in order with the layout's own steps: clear the points, set the
|
||||
// point, lay the line out now. The hyperlink buttons and inline textures are
|
||||
// anchored to the lines, so they move with them. Retail's frame has no text
|
||||
// insets; the TOP mode's first line sits the top inset below the frame's top,
|
||||
// as the BOTTOM mode's sits the bottom inset above its bottom.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/Destruction.h"
|
||||
#include "scrollingmessageframe/DisplayRefreshed.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace ScrollingMessageFrame::InsertMode {
|
||||
|
||||
namespace {
|
||||
|
||||
// SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP / _BOTTOM.
|
||||
constexpr int kInsertModeTop = 1;
|
||||
constexpr int kInsertModeBottom = 2;
|
||||
|
||||
using SetPoint_t = void(__thiscall *)(void *anchor, int point, void *relAnchor, int relPoint, float x, float y,
|
||||
int flag);
|
||||
using ClearAllPoints_t = void(__fastcall *)(void *anchor);
|
||||
using Resize_t = void(__fastcall *)(void *anchor, void *edx, int force);
|
||||
using PixelAlignY_t = float(__stdcall *)(float length);
|
||||
|
||||
// The frames in TOP mode. Every frame is rebuilt on /reload, so the set is
|
||||
// cleared then.
|
||||
std::unordered_set<void *> g_topFrames;
|
||||
|
||||
bool IsTop(void *smf) { return g_topFrames.count(smf) != 0; }
|
||||
|
||||
void *AnchorOf(void *region) { return static_cast<uint8_t *>(region) + Offsets::OFF_REGION_ANCHOR; }
|
||||
|
||||
// Sets a line's anchor as the layout gives it - (BOTTOMLEFT, the frame,
|
||||
// BOTTOMLEFT, x, y) for the first line, (BOTTOMLEFT, the line before, TOPLEFT,
|
||||
// x, y) for the others - or, for a frame in TOP mode, its mirror.
|
||||
void SetLinePoint(void *smf, void *anchor, int point, void *relative, int relativePoint, float x, float y,
|
||||
int flag) {
|
||||
if (IsTop(smf)) {
|
||||
point = Offsets::FRAMEPOINT_TOPLEFT;
|
||||
if (relativePoint == Offsets::FRAMEPOINT_BOTTOMLEFT) { // the first line, on the frame
|
||||
relativePoint = Offsets::FRAMEPOINT_TOPLEFT;
|
||||
y = -Game::Read<float>(smf, Offsets::OFF_SCROLLINGMESSAGEFRAME_INSET_TOP);
|
||||
} else {
|
||||
relativePoint = Offsets::FRAMEPOINT_BOTTOMLEFT;
|
||||
y = -y;
|
||||
}
|
||||
}
|
||||
reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT)(anchor, point, relative, relativePoint, x, y, flag);
|
||||
}
|
||||
|
||||
// The layout's SetPoint for a line it has just made; the line's parent is
|
||||
// the frame.
|
||||
void __fastcall AnchorNewLine(void *anchor, void *, int point, void *relative, int relativePoint, float x, float y,
|
||||
int flag) {
|
||||
void *line = static_cast<uint8_t *>(anchor) - Offsets::OFF_REGION_ANCHOR;
|
||||
SetLinePoint(Game::Read<void *>(line, Offsets::OFF_REGION_PARENT), anchor, point, relative, relativePoint, x, y,
|
||||
flag);
|
||||
}
|
||||
|
||||
// Anchors the frame's lines again, in order, with the layout's arguments and
|
||||
// steps.
|
||||
void AnchorLines(void *smf) {
|
||||
const int count = Game::Read<int>(smf, Offsets::OFF_SMF_LINE_COUNT);
|
||||
const auto *lines = Game::Read<const uint8_t *>(smf, Offsets::OFF_SMF_LINE_ARRAY);
|
||||
void *previous = nullptr;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
void *line = Game::Read<void *>(lines + i * Offsets::SMF_LINE_STRIDE, Offsets::OFF_SMF_LINE_FONTSTRING);
|
||||
void *anchor = AnchorOf(line);
|
||||
reinterpret_cast<ClearAllPoints_t>(Offsets::FUN_LAYOUT_CLEAR_ALL_POINTS)(anchor);
|
||||
if (previous == nullptr)
|
||||
SetLinePoint(smf, anchor, Offsets::FRAMEPOINT_BOTTOMLEFT, AnchorOf(smf), Offsets::FRAMEPOINT_BOTTOMLEFT,
|
||||
Game::Read<float>(smf, Offsets::OFF_SCROLLINGMESSAGEFRAME_INSET_LEFT),
|
||||
Game::Read<float>(smf, Offsets::OFF_SCROLLINGMESSAGEFRAME_INSET_BOTTOM), 1);
|
||||
else
|
||||
SetLinePoint(smf, anchor, Offsets::FRAMEPOINT_BOTTOMLEFT, AnchorOf(previous), Offsets::FRAMEPOINT_TOPLEFT,
|
||||
0.0f,
|
||||
reinterpret_cast<PixelAlignY_t>(Offsets::FUN_PIXEL_ALIGN_Y)(
|
||||
Game::Read<float>(line, Offsets::OFF_FONTSTRING_SPACING)),
|
||||
1);
|
||||
reinterpret_cast<Resize_t>(Offsets::FUN_LAYOUT_RESIZE)(anchor, nullptr, 1);
|
||||
previous = line;
|
||||
}
|
||||
}
|
||||
|
||||
int __fastcall Script_SetInsertMode(void *L) {
|
||||
void *frame = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
|
||||
if (frame == nullptr)
|
||||
return 0;
|
||||
const double mode = Game::Lua::IsNumber(L, 2) ? Game::Lua::ToNumber(L, 2) : 0;
|
||||
if (mode != kInsertModeTop && mode != kInsertModeBottom) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: %s:SetInsertMode(SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP or "
|
||||
"SCROLLING_MESSAGE_FRAME_INSERT_MODE_BOTTOM)",
|
||||
Game::Lua::ObjectDisplayName(frame));
|
||||
return 0;
|
||||
}
|
||||
const bool top = mode == kInsertModeTop;
|
||||
if (top == IsTop(frame))
|
||||
return 0;
|
||||
if (top)
|
||||
g_topFrames.insert(frame);
|
||||
else
|
||||
g_topFrames.erase(frame);
|
||||
AnchorLines(frame);
|
||||
DisplayRefreshed::MarkDirty(frame);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetInsertMode(void *L) {
|
||||
void *frame = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
|
||||
if (frame == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, IsTop(frame) ? kInsertModeTop : kInsertModeBottom);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry kMethods[] = {
|
||||
{"SetInsertMode", &Script_SetInsertMode},
|
||||
{"GetInsertMode", &Script_GetInsertMode},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kModeArgs[] = {
|
||||
Game::Doc::Req("mode", "number",
|
||||
"SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP or SCROLLING_MESSAGE_FRAME_INSERT_MODE_BOTTOM."),
|
||||
};
|
||||
const Game::Doc::Field kModeRets[] = {
|
||||
Game::Doc::Req("mode", "number", "BOTTOM until SetInsertMode changes it."),
|
||||
};
|
||||
const Game::Doc::Function kSetInsertMode{
|
||||
"Sets where the newest message shows: at the top, with older messages below it, or at the bottom.", kModeArgs,
|
||||
{}};
|
||||
const Game::Doc::Function kGetInsertMode{"Returns where the newest message shows.", {}, kModeRets};
|
||||
|
||||
const Game::Doc::Method kDocs[] = {
|
||||
{"SetInsertMode", &kSetInsertMode},
|
||||
{"GetInsertMode", &kGetInsertMode},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
void *L = Game::Lua::State();
|
||||
Game::Lua::SetGlobalNumber(L, "SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP", kInsertModeTop);
|
||||
Game::Lua::SetGlobalNumber(L, "SCROLLING_MESSAGE_FRAME_INSERT_MODE_BOTTOM", kInsertModeBottom);
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY),
|
||||
kMethods, 2, kDocs, 2);
|
||||
Common::RedirectCall(Offsets::CALL_SMF_LAYOUT_ANCHOR_LINE, Offsets::FUN_REGION_SET_POINT,
|
||||
reinterpret_cast<const void *>(&AnchorNewLine));
|
||||
}
|
||||
|
||||
void PrepareForReload() { g_topFrames.clear(); }
|
||||
|
||||
void OnFrameDestroyed(void *frame) { g_topFrames.erase(frame); }
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
const Game::ReloadAutoRegister _reload{&PrepareForReload};
|
||||
const Frame::Destruction::AutoSubscribe _destructionSub{&OnFrameDestroyed};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace ScrollingMessageFrame::InsertMode
|
||||
@@ -0,0 +1,41 @@
|
||||
// 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/>.
|
||||
|
||||
#include "scrollingmessageframe/Layout.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace ScrollingMessageFrame::Layout {
|
||||
|
||||
namespace {
|
||||
|
||||
AutoSubscribe *g_head = nullptr;
|
||||
|
||||
using Layout_t = void(__fastcall *)(void *smf, void *edx, int newestRecord);
|
||||
Layout_t g_origLayout = nullptr;
|
||||
|
||||
void __fastcall Layout_h(void *smf, void *edx, int newestRecord) {
|
||||
g_origLayout(smf, edx, newestRecord);
|
||||
for (const AutoSubscribe *s = g_head; s != nullptr; s = s->next)
|
||||
s->fn(smf);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hook{Offsets::FUN_SMF_DISPLAY_REFRESH, reinterpret_cast<void *>(&Layout_h),
|
||||
reinterpret_cast<void **>(&g_origLayout)};
|
||||
|
||||
} // namespace
|
||||
|
||||
AutoSubscribe::AutoSubscribe(Listener fn) : fn(fn), next(g_head) { g_head = this; }
|
||||
|
||||
} // namespace ScrollingMessageFrame::Layout
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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
|
||||
|
||||
// Shared observer for the ScrollingMessageFrame line layout
|
||||
// FUN_SMF_DISPLAY_REFRESH (Offsets.h), which lays out the lines the frame
|
||||
// shows after every change to them: a message added at the bottom, a scroll,
|
||||
// a resize, a font change, Clear. A listener runs after it, with the frame.
|
||||
//
|
||||
// MinHook allows ONE hook per address and more than one feature follows the
|
||||
// layout (Text::InlineTexture's early icon apply, the display-refreshed
|
||||
// callbacks), so this module owns the hook and fans out. Declare a file-scope
|
||||
// static const ScrollingMessageFrame::Layout::AutoSubscribe _sub{&OnLayout};
|
||||
namespace ScrollingMessageFrame::Layout {
|
||||
|
||||
using Listener = void (*)(void *smf);
|
||||
|
||||
struct AutoSubscribe {
|
||||
explicit AutoSubscribe(Listener fn);
|
||||
Listener fn;
|
||||
AutoSubscribe *next;
|
||||
};
|
||||
|
||||
} // namespace ScrollingMessageFrame::Layout
|
||||
@@ -0,0 +1,494 @@
|
||||
// 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/>.
|
||||
|
||||
// `ScrollingMessageFrame:GetMessageInfo(index)`, retail's: the message's
|
||||
// text, its colour, then every argument AddMessage took after the colour.
|
||||
//
|
||||
// Retail's ScrollingMessageFrame is a Lua mixin whose AddMessage(message, r,
|
||||
// g, b, ...) packs the call into an entry and GetMessageInfo unpacks it
|
||||
// (Blizzard_SharedXML/ScrollingMessageFrame.lua). Index 1 is the oldest
|
||||
// message; the extra arguments come back as given, nils and count included;
|
||||
// an index outside 1..GetNumMessages() returns nothing. (4.3.4's C method
|
||||
// returns text, accessID, typeID, extraData instead.)
|
||||
//
|
||||
// The text and colour come from the engine's own record of the message
|
||||
// (Offsets.h, OFF_SCROLLINGMESSAGEFRAME_MESSAGES), so a colour
|
||||
// UpdateColorByID changed reads back changed, as retail's AdjustMessageColors
|
||||
// does. The record keeps the colour as bytes, so a component reads back as
|
||||
// byte/255. It keeps the first extra argument only as an integer (0 for
|
||||
// anything else), so the extra arguments are kept beside the ring instead.
|
||||
//
|
||||
// AddMessage is registered again on the ScrollingMessageFrame registry, over
|
||||
// the engine's. The registrar (0x00701D80) adds a node for the name whatever
|
||||
// is there; Storm's TSHashTable::InternalNew links a new node at the head of
|
||||
// its bucket (NewNode(1)), and the method lookup (0x00702000) returns the
|
||||
// first match, so this one answers. It runs the engine's AddMessage, then
|
||||
// records the extra arguments for the record the engine wrote. The engine
|
||||
// adds a message exactly when the text is a nonempty string
|
||||
// (0x0079299C..0x007929C7: lua_isstring, lua_tostring, first byte); a bad
|
||||
// `self` raises a Lua error instead. The ring's one writer has no other
|
||||
// caller, so every record was written through here.
|
||||
//
|
||||
// The extra arguments live in a registry table keyed by the frame's Lua table,
|
||||
// then by record. A record's table is refilled when the record is overwritten,
|
||||
// so a full ring makes no garbage.
|
||||
//
|
||||
// `BackFillMessage(text, r, g, b, ...)`, retail's, adds a message as the
|
||||
// OLDEST — the combat log's refilter walks its history newest to oldest and
|
||||
// fills the frame behind the lines it already shows (Blizzard_CombatLog's
|
||||
// RefilterUpdate). It writes the ring itself; see the function.
|
||||
//
|
||||
// `RemoveMessagesByPredicate(predicate)`, retail's, removes the messages the
|
||||
// predicate selects by their GetMessageInfo values. The ring has no
|
||||
// compaction of its own; 4.3.4's RemoveLines loop is run over it, the
|
||||
// extra-argument tables moving with their records. See the function.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "scrollingmessageframe/DisplayRefreshed.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace ScrollingMessageFrame::MessageInfo {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kFirstExtra = 6; // self, text, r, g, b, then the extra arguments
|
||||
|
||||
// The frame-table → record → extra-arguments table, in the registry of the Lua
|
||||
// state that made it. LUA_NOREF until first use in each state.
|
||||
constexpr int kNoRef = -2;
|
||||
int g_extrasRef = kNoRef;
|
||||
|
||||
// Pushes the record → extra-arguments table of the frame whose Lua table is
|
||||
// at stack slot 1, or nil when it has none and `create` is false.
|
||||
void PushRecords(void *L, bool create) {
|
||||
using namespace Game::Lua;
|
||||
if (g_extrasRef == kNoRef) {
|
||||
if (!create) {
|
||||
PushNil(L);
|
||||
return;
|
||||
}
|
||||
NewTable(L);
|
||||
g_extrasRef = Ref(L, REGISTRY_INDEX);
|
||||
}
|
||||
RawGetI(L, REGISTRY_INDEX, g_extrasRef); // [frames]
|
||||
PushValue(L, 1);
|
||||
RawGet(L, -2); // [frames, records]
|
||||
if (Type(L, -1) != TYPE_TABLE && create) {
|
||||
SetTop(L, -2);
|
||||
NewTable(L);
|
||||
PushValue(L, 1);
|
||||
PushValue(L, -2);
|
||||
RawSet(L, -4); // frames[self] = records
|
||||
}
|
||||
Remove(L, -2); // [records]
|
||||
}
|
||||
|
||||
// Pushes the extra-argument table of record `record` of the frame whose Lua
|
||||
// table is at stack slot 1, or nil when it has none and `create` is false.
|
||||
void PushExtras(void *L, int record, bool create) {
|
||||
using namespace Game::Lua;
|
||||
PushRecords(L, create); // [records]
|
||||
if (Type(L, -1) != TYPE_TABLE)
|
||||
return;
|
||||
RawGetI(L, -1, record); // [records, extras]
|
||||
if (Type(L, -1) != TYPE_TABLE && create) {
|
||||
SetTop(L, -2);
|
||||
NewTable(L);
|
||||
PushNumber(L, record);
|
||||
PushValue(L, -2);
|
||||
RawSet(L, -4); // records[record] = extras
|
||||
}
|
||||
Remove(L, -2); // [extras]
|
||||
}
|
||||
|
||||
// The `n` of the extra-argument table at absolute stack slot `extras`.
|
||||
int ExtraCount(void *L, int extras) {
|
||||
using namespace Game::Lua;
|
||||
PushString(L, "n");
|
||||
RawGet(L, extras);
|
||||
const int n = static_cast<int>(ToNumber(L, -1));
|
||||
SetTop(L, -2);
|
||||
return n;
|
||||
}
|
||||
|
||||
// Stores stack slots kFirstExtra..top as the extra arguments of `record`.
|
||||
void StoreExtras(void *L, int record, int top) {
|
||||
using namespace Game::Lua;
|
||||
const int count = top >= kFirstExtra ? top - kFirstExtra + 1 : 0;
|
||||
PushExtras(L, record, count > 0);
|
||||
const int extras = GetTop(L);
|
||||
if (Type(L, extras) == TYPE_TABLE) {
|
||||
const int was = ExtraCount(L, extras);
|
||||
for (int i = 1; i <= count; ++i) {
|
||||
PushNumber(L, i);
|
||||
PushValue(L, kFirstExtra - 1 + i);
|
||||
RawSet(L, extras);
|
||||
}
|
||||
for (int i = count + 1; i <= was; ++i) {
|
||||
PushNumber(L, i);
|
||||
PushNil(L);
|
||||
RawSet(L, extras);
|
||||
}
|
||||
PushString(L, "n");
|
||||
PushNumber(L, count);
|
||||
RawSet(L, extras);
|
||||
}
|
||||
SetTop(L, -2);
|
||||
}
|
||||
|
||||
int __fastcall Script_AddMessage(void *L) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_SCROLLINGMESSAGEFRAME_ADD_MESSAGE)(L);
|
||||
const char *text = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : nullptr;
|
||||
if (text == nullptr || *text == '\0')
|
||||
return 0;
|
||||
void *frame = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
|
||||
if (frame == nullptr)
|
||||
return 0;
|
||||
StoreExtras(L, Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS), top);
|
||||
// Scrolled up, the engine adds the message without a layout.
|
||||
DisplayRefreshed::MarkDirty(frame);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// AddMessage's colour byte (0x00792A1B..0x00792ACC): the value clamped to
|
||||
// 0..1 (NaN to 1), then trunc(v × 255 + 0.5).
|
||||
uint32_t ColorByte(double value) {
|
||||
double clamped = 1.0;
|
||||
if (value < 0.0)
|
||||
clamped = 0.0;
|
||||
else if (value < 1.0)
|
||||
clamped = static_cast<float>(value);
|
||||
return static_cast<uint32_t>(static_cast<int>(clamped * 255.0 + 0.5)) & 0xFFu;
|
||||
}
|
||||
|
||||
// The colour AddMessage builds from stack slots 3..5 — alpha 0xFF, R, G, B —
|
||||
// or the frame font's when they are not all numbers (0x007929CD..0x00792B45).
|
||||
uint32_t MessageColor(void *L, void *frame) {
|
||||
using namespace Game::Lua;
|
||||
if (IsNumber(L, 3) && IsNumber(L, 4) && IsNumber(L, 5))
|
||||
return 0xFF000000u | (ColorByte(ToNumber(L, 3)) << 16) | (ColorByte(ToNumber(L, 4)) << 8) |
|
||||
ColorByte(ToNumber(L, 5));
|
||||
const void *font = Game::Read<const void *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_FONT);
|
||||
return font != nullptr ? Game::Read<uint32_t>(font, Offsets::OFF_SCROLLINGMESSAGEFRAME_FONT_COLOR) : 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
// `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.
|
||||
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);
|
||||
using StormFree_t = int(__stdcall *)(void *ptr, const char *file, int line, int flags);
|
||||
using Layout_t = void(__fastcall *)(void *smf, void *edx, int newestRecord);
|
||||
static const char kStormFile[] = "ClassicAPI ScrollingMessageFrame:BackFillMessage";
|
||||
|
||||
const int top = GetTop(L);
|
||||
void *frame = ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
|
||||
if (frame == nullptr)
|
||||
return 0;
|
||||
const char *text = IsString(L, 2) ? ToString(L, 2) : nullptr;
|
||||
if (text == nullptr || *text == '\0')
|
||||
return 0;
|
||||
const int max = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES);
|
||||
int &count = Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
|
||||
if (max <= 0 || count >= max)
|
||||
return 0; // full: the message is older than every kept one
|
||||
const uint32_t color = MessageColor(L, frame);
|
||||
const size_t bytes = std::strlen(text) + 1;
|
||||
char *copy = static_cast<char *>(
|
||||
reinterpret_cast<StormAlloc_t>(Offsets::FUN_STORM_SMEM_ALLOC)(bytes, kStormFile, __LINE__, 0));
|
||||
if (copy == nullptr)
|
||||
return 0;
|
||||
std::memcpy(copy, text, bytes);
|
||||
|
||||
++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;
|
||||
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);
|
||||
stored = copy;
|
||||
*reinterpret_cast<uint32_t *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COLOR) = color;
|
||||
*reinterpret_cast<int *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_SHOWN) = 1;
|
||||
*reinterpret_cast<uint32_t *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_VISIBLE_TIME) =
|
||||
Game::Read<uint32_t>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_VISIBLE_TIME);
|
||||
*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);
|
||||
|
||||
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.
|
||||
DisplayRefreshed::MarkDirty(frame);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The record of message `index`, 1 the oldest: (position - count + index),
|
||||
// plus the limit when negative.
|
||||
int RecordOf(void *frame, int index) {
|
||||
const int count = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
|
||||
int record = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS) - count + index;
|
||||
if (record < 0)
|
||||
record += Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES);
|
||||
return record;
|
||||
}
|
||||
|
||||
// Pushes a record's GetMessageInfo values — its text, r, g, b, then its extra
|
||||
// arguments — for the frame whose Lua table is at stack slot 1, and returns
|
||||
// how many.
|
||||
int PushMessage(void *L, void *frame, int record) {
|
||||
using namespace Game::Lua;
|
||||
const auto *message = Game::Read<const uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES) +
|
||||
record * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
|
||||
const uint32_t color = Game::Read<uint32_t>(message, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COLOR);
|
||||
PushExtras(L, record, false);
|
||||
const int extras = GetTop(L);
|
||||
const int n = Type(L, extras) == TYPE_TABLE ? ExtraCount(L, extras) : 0;
|
||||
CheckStack(L, 4 + n);
|
||||
PushString(L, Game::Read<const char *>(message, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT));
|
||||
PushNumber(L, ((color >> 16) & 0xFF) / 255.0);
|
||||
PushNumber(L, ((color >> 8) & 0xFF) / 255.0);
|
||||
PushNumber(L, (color & 0xFF) / 255.0);
|
||||
for (int i = 1; i <= n; ++i)
|
||||
RawGetI(L, extras, i);
|
||||
Remove(L, extras);
|
||||
return 4 + n;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetMessageInfo(void *L) {
|
||||
using namespace Game::Lua;
|
||||
void *frame = ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
|
||||
if (frame == nullptr)
|
||||
return 0;
|
||||
if (!IsNumber(L, 2)) {
|
||||
Error(L, "Usage: %s:GetMessageInfo(index)", ObjectDisplayName(frame));
|
||||
return 0;
|
||||
}
|
||||
const double index = ToNumber(L, 2);
|
||||
const int count = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
|
||||
if (!(index >= 1 && index <= count) || index != static_cast<int>(index))
|
||||
return 0;
|
||||
return PushMessage(L, frame, RecordOf(frame, static_cast<int>(index)));
|
||||
}
|
||||
|
||||
// `ScrollingMessageFrame:RemoveMessagesByPredicate(predicate)`, retail's: the
|
||||
// predicate is called with each message's GetMessageInfo values, oldest
|
||||
// first, and the messages it returns true for are removed; the rest keep
|
||||
// their order. Retail's mixin rebuilds its history buffer and re-renders at
|
||||
// the kept scroll offset. The removal is 4.3.4's RemoveLines over 1.12's
|
||||
// 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
|
||||
// (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.
|
||||
//
|
||||
// The predicate runs for every message before anything moves (retail builds
|
||||
// its new list first). A predicate that errors keeps that message, as
|
||||
// retail's safesecurecall does; the engine's error handler reports it.
|
||||
int __fastcall Script_RemoveMessagesByPredicate(void *L) {
|
||||
using namespace Game::Lua;
|
||||
using Layout_t = void(__fastcall *)(void *smf, void *edx, int newestRecord);
|
||||
using Smf_t = void(__fastcall *)(void *smf);
|
||||
using ScrollOffset_t = int(__fastcall *)(void *smf);
|
||||
using StormFree_t = int(__stdcall *)(void *ptr, const char *file, int line, int flags);
|
||||
static const char kStormFile[] = "ClassicAPI ScrollingMessageFrame:RemoveMessagesByPredicate";
|
||||
|
||||
void *frame = ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
|
||||
if (frame == nullptr)
|
||||
return 0;
|
||||
if (Type(L, 2) != TYPE_FUNCTION) {
|
||||
Error(L, "Usage: %s:RemoveMessagesByPredicate(predicate)", ObjectDisplayName(frame));
|
||||
return 0;
|
||||
}
|
||||
const int count = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
|
||||
const int max = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES);
|
||||
if (count <= 0 || max <= 0)
|
||||
return 0;
|
||||
const int top = GetTop(L);
|
||||
|
||||
// The predicate over every message, through the engine's error handler.
|
||||
int errIdx = 0;
|
||||
const int errRef = Game::Read<int>(static_cast<uintptr_t>(Offsets::VAR_FRAMESCRIPT_ERROR_HANDLER_REF));
|
||||
if (errRef > 0) {
|
||||
RawGetI(L, REGISTRY_INDEX, errRef);
|
||||
errIdx = GetTop(L);
|
||||
}
|
||||
std::vector<uint8_t> remove(static_cast<size_t>(count), 0);
|
||||
int kept = count;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
CheckStack(L, 2);
|
||||
PushValue(L, 2);
|
||||
const int args = PushMessage(L, frame, RecordOf(frame, i + 1));
|
||||
if (PCall(L, args, 1, errIdx) == 0 && ToBoolean(L, -1) != 0) {
|
||||
remove[static_cast<size_t>(i)] = 1;
|
||||
--kept;
|
||||
}
|
||||
SetTop(L, -2);
|
||||
}
|
||||
SetTop(L, top);
|
||||
if (kept == count)
|
||||
return 0;
|
||||
|
||||
// 4.3.4's RemoveLines: compact the kept records forward in ring order.
|
||||
const int offset = reinterpret_cast<ScrollOffset_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET)(frame);
|
||||
int &pos = Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS);
|
||||
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;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (!remove[static_cast<size_t>(i)]) {
|
||||
if (src != dst) {
|
||||
uint8_t *to = records + dst * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
|
||||
uint8_t *from = records + src * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
|
||||
char *&landedOn = *reinterpret_cast<char **>(to + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT);
|
||||
if (landedOn != nullptr)
|
||||
reinterpret_cast<StormFree_t>(Offsets::FUN_STORM_SMEM_FREE)(landedOn, kStormFile, __LINE__, 0);
|
||||
std::memcpy(to, from, Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE);
|
||||
*reinterpret_cast<char **>(from + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT) = nullptr;
|
||||
if (extras) {
|
||||
PushNumber(L, dst);
|
||||
RawGetI(L, table, src);
|
||||
RawSet(L, table); // records[dst] = records[src]
|
||||
PushNumber(L, src);
|
||||
PushNil(L);
|
||||
RawSet(L, table); // records[src] = nil
|
||||
}
|
||||
}
|
||||
dst = (dst + 1) % max;
|
||||
} else if (extras) {
|
||||
PushNumber(L, src);
|
||||
PushNil(L);
|
||||
RawSet(L, table); // records[src] = nil
|
||||
}
|
||||
src = (src + 1) % max;
|
||||
}
|
||||
SetTop(L, top);
|
||||
|
||||
if (kept == 0) {
|
||||
reinterpret_cast<Smf_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_CLEAR)(frame);
|
||||
DisplayRefreshed::MarkDirty(frame);
|
||||
return 0;
|
||||
}
|
||||
pos += kept - count;
|
||||
if (pos < 0)
|
||||
pos += max;
|
||||
Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT) = kept;
|
||||
// The lines again from the record at the kept offset, then re-armed.
|
||||
const int shown = (offset < kept) ? (offset < 0 ? 0 : offset) : kept - 1;
|
||||
int record = pos - shown;
|
||||
if (record < 0)
|
||||
record += max;
|
||||
reinterpret_cast<Layout_t>(Offsets::FUN_SMF_DISPLAY_REFRESH)(frame, nullptr, record);
|
||||
reinterpret_cast<Smf_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_REARM_LINES)(frame);
|
||||
DisplayRefreshed::MarkDirty(frame);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry kMethods[] = {
|
||||
{"AddMessage", &Script_AddMessage},
|
||||
{"BackFillMessage", &Script_BackFillMessage},
|
||||
{"GetMessageInfo", &Script_GetMessageInfo},
|
||||
{"RemoveMessagesByPredicate", &Script_RemoveMessagesByPredicate},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kAddMessageArgs[] = {
|
||||
Game::Doc::Req("text", "string"),
|
||||
Game::Doc::Opt("r", "number", nullptr, "Without r, g and b the message takes the frame's font colour."),
|
||||
Game::Doc::Opt("g", "number"),
|
||||
Game::Doc::Opt("b", "number"),
|
||||
Game::Doc::Vararg("any", "Kept with the message; GetMessageInfo returns them after its colour."),
|
||||
};
|
||||
const Game::Doc::Function kAddMessage{"Adds a message to the frame.", kAddMessageArgs, {}};
|
||||
|
||||
const Game::Doc::Field kBackFillMessageArgs[] = {
|
||||
Game::Doc::Req("text", "string"),
|
||||
Game::Doc::Opt("r", "number", nullptr, "Without r, g and b the message takes the frame's font colour."),
|
||||
Game::Doc::Opt("g", "number"),
|
||||
Game::Doc::Opt("b", "number"),
|
||||
Game::Doc::Vararg("any", "Kept with the message; GetMessageInfo returns them after its colour."),
|
||||
};
|
||||
const Game::Doc::Function kBackFillMessage{
|
||||
"Adds a message as the oldest, to fill in history behind the messages the frame has. A frame at "
|
||||
"its line limit drops it.",
|
||||
kBackFillMessageArgs, {}};
|
||||
|
||||
const Game::Doc::Field kGetMessageInfoArgs[] = {
|
||||
Game::Doc::Req("index", "luaIndex", "1 is the oldest message."),
|
||||
};
|
||||
const Game::Doc::Field kGetMessageInfoRets[] = {
|
||||
Game::Doc::Req("text", "string"),
|
||||
Game::Doc::Req("r", "number"),
|
||||
Game::Doc::Req("g", "number"),
|
||||
Game::Doc::Req("b", "number"),
|
||||
Game::Doc::Vararg("any", "The arguments AddMessage took after the colour."),
|
||||
};
|
||||
const Game::Doc::Function kGetMessageInfo{
|
||||
"Returns a message's text, its colour, and the extra arguments it was added with, or nothing "
|
||||
"when there is no message at that index.",
|
||||
kGetMessageInfoArgs, kGetMessageInfoRets};
|
||||
|
||||
const Game::Doc::Field kRemoveMessagesByPredicateArgs[] = {
|
||||
Game::Doc::Req("predicate", "function",
|
||||
"Called with each message's GetMessageInfo values, oldest first; true removes the message."),
|
||||
};
|
||||
const Game::Doc::Function kRemoveMessagesByPredicate{
|
||||
"Removes the messages the predicate returns true for. The rest keep their order, and the frame "
|
||||
"keeps its scroll offset.",
|
||||
kRemoveMessagesByPredicateArgs, {}};
|
||||
|
||||
const Game::Doc::Method kDocs[] = {
|
||||
{"AddMessage", &kAddMessage},
|
||||
{"BackFillMessage", &kBackFillMessage},
|
||||
{"GetMessageInfo", &kGetMessageInfo},
|
||||
{"RemoveMessagesByPredicate", &kRemoveMessagesByPredicate},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
// Each registration pass is a new Lua state; the old state's ref is gone.
|
||||
g_extrasRef = kNoRef;
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY),
|
||||
kMethods, 4, kDocs, 4);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace ScrollingMessageFrame::MessageInfo
|
||||
@@ -0,0 +1,142 @@
|
||||
// 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 scroll offset on ScrollingMessageFrame: `SetScrollOffset(offset)`,
|
||||
// `GetScrollOffset()`, `GetMaxScrollRange()` and `ScrollByAmount(amount)`.
|
||||
//
|
||||
// Retail's offset (Blizzard_SharedXML/ScrollingMessageFrame.lua) counts the
|
||||
// lines the frame is scrolled up from the newest message, 0 at the bottom, up
|
||||
// to GetMaxScrollRange() = max(GetNumMessages() - 1, 0), where the oldest
|
||||
// message is the bottom line - the place 1.12's own ScrollToTop goes. 1.12
|
||||
// keeps the same offset: ScrollToOffset and ScrollOffset (Offsets.h) are its
|
||||
// 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.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "scrollingmessageframe/DisplayRefreshed.h"
|
||||
|
||||
namespace ScrollingMessageFrame::Scroll {
|
||||
|
||||
namespace {
|
||||
|
||||
using ScrollToOffset_t = void(__fastcall *)(void *smf, void *edx, int offset);
|
||||
using ScrollOffset_t = int(__fastcall *)(void *smf);
|
||||
|
||||
int ScrollOffset(void *smf) {
|
||||
return reinterpret_cast<ScrollOffset_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET)(smf);
|
||||
}
|
||||
|
||||
int MaxScrollRange(void *smf) {
|
||||
const int count = Game::Read<int>(smf, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
|
||||
return count > 1 ? count - 1 : 0;
|
||||
}
|
||||
|
||||
// `offset` clamped to 0..MaxScrollRange (NaN to 0).
|
||||
int Clamped(void *smf, double offset) {
|
||||
const int max = MaxScrollRange(smf);
|
||||
return offset >= 0 ? (offset < max ? static_cast<int>(offset) : max) : 0;
|
||||
}
|
||||
|
||||
void ScrollTo(void *smf, int offset) {
|
||||
reinterpret_cast<ScrollToOffset_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_TO_OFFSET)(smf, nullptr, offset);
|
||||
}
|
||||
|
||||
void *ResolveSmf(void *L) {
|
||||
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
|
||||
}
|
||||
|
||||
int __fastcall Script_SetScrollOffset(void *L) {
|
||||
void *frame = ResolveSmf(L);
|
||||
if (frame == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetScrollOffset(offset)", Game::Lua::ObjectDisplayName(frame));
|
||||
return 0;
|
||||
}
|
||||
const int offset = Clamped(frame, Game::Lua::ToNumber(L, 2));
|
||||
if (offset != ScrollOffset(frame))
|
||||
ScrollTo(frame, offset);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetMaxScrollRange(void *L) {
|
||||
void *frame = ResolveSmf(L);
|
||||
if (frame == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, MaxScrollRange(frame));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_ScrollByAmount(void *L) {
|
||||
void *frame = ResolveSmf(L);
|
||||
if (frame == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
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)));
|
||||
DisplayRefreshed::MarkDirty(frame);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry kMethods[] = {
|
||||
{"SetScrollOffset", &Script_SetScrollOffset},
|
||||
{"GetScrollOffset",
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_SCROLLINGMESSAGEFRAME_GET_CURRENT_SCROLL)},
|
||||
{"GetMaxScrollRange", &Script_GetMaxScrollRange},
|
||||
{"ScrollByAmount", &Script_ScrollByAmount},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kOffsetArgs[] = {
|
||||
Game::Doc::Req("offset", "number", "Lines from the newest message; clamped to 0..GetMaxScrollRange()."),
|
||||
};
|
||||
const Game::Doc::Field kOffsetRets[] = {
|
||||
Game::Doc::Req("offset", "number", "Lines from the newest message; 0 when not scrolled."),
|
||||
};
|
||||
const Game::Doc::Field kRangeRets[] = {
|
||||
Game::Doc::Req("maxScrollRange", "number"),
|
||||
};
|
||||
const Game::Doc::Field kAmountArgs[] = {
|
||||
Game::Doc::Req("amount", "number", "Positive scrolls toward older messages."),
|
||||
};
|
||||
const Game::Doc::Function kSetScrollOffset{"Scrolls the frame to a number of lines from the newest message.",
|
||||
kOffsetArgs, {}};
|
||||
const Game::Doc::Function kGetScrollOffset{"Returns how many lines the frame is scrolled from the newest message.",
|
||||
{}, 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::Method kDocs[] = {
|
||||
{"SetScrollOffset", &kSetScrollOffset},
|
||||
{"GetScrollOffset", &kGetScrollOffset},
|
||||
{"GetMaxScrollRange", &kGetMaxScrollRange},
|
||||
{"ScrollByAmount", &kScrollByAmount},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY),
|
||||
kMethods, 4, kDocs, 4);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace ScrollingMessageFrame::Scroll
|
||||
@@ -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
|
||||
@@ -79,6 +79,7 @@
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "scrollingmessageframe/Layout.h"
|
||||
#include "text/InlineTexturePool.h"
|
||||
#include "text/PtrProbe.h"
|
||||
#include "texture/Atlas.h"
|
||||
@@ -2085,7 +2086,8 @@ static const Game::HookAutoRegister _paintHook{Offsets::FUN_TEXT_PAINT,
|
||||
// So a line's icons are recorded — and its regions placed — one frame AFTER its
|
||||
// glyphs draw, because a frame's textures (our icon regions) draw before its
|
||||
// fontstring glyphs and the placement isn't computed until the glyph build runs
|
||||
// mid-paint. This co-hook closes it: after the refresh sets up the lines (still
|
||||
// mid-paint. This layout listener (ScrollingMessageFrame::Layout, which owns
|
||||
// the hook) closes it: after the refresh sets up the lines (still
|
||||
// PRE-RENDER — the refresh runs from the SMF's Lua/event-driven methods, never
|
||||
// mid-render), force each icon-bearing line to resolve+build and apply its icon
|
||||
// placement synchronously, so the icons draw with their glyphs the same frame.
|
||||
@@ -2096,9 +2098,6 @@ static const Game::HookAutoRegister _paintHook{Offsets::FUN_TEXT_PAINT,
|
||||
// disabled independently of the whole feature; the SEH latch trips it on a fault.
|
||||
bool g_earlyApply = true;
|
||||
|
||||
using SmfRefresh_t = void(__fastcall *)(void *smf, void *edx, int newestIdx);
|
||||
SmfRefresh_t g_smfRefreshOriginal = nullptr;
|
||||
|
||||
using EnsureBuilt_t = int(__fastcall *)(void *node);
|
||||
using RealizeAnchor_t = void(__thiscall *)(void *anchor, int flag);
|
||||
using RebuildStringFs_t = void(__fastcall *)(void *fs);
|
||||
@@ -2183,15 +2182,12 @@ void SafeSmfEarlyApply(void *smf) {
|
||||
}
|
||||
}
|
||||
|
||||
void __fastcall SmfRefresh_h(void *smf, void *edx, int newestIdx) {
|
||||
g_smfRefreshOriginal(smf, edx, newestIdx);
|
||||
void OnSmfLayout(void *smf) {
|
||||
if (g_inlineEnabled && g_earlyApply)
|
||||
SafeSmfEarlyApply(smf);
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _smfRefreshHook{
|
||||
Offsets::FUN_SMF_DISPLAY_REFRESH, reinterpret_cast<void *>(&SmfRefresh_h),
|
||||
reinterpret_cast<void **>(&g_smfRefreshOriginal)};
|
||||
static const ScrollingMessageFrame::Layout::AutoSubscribe _smfLayoutSub{&OnSmfLayout};
|
||||
|
||||
// --- Lua control surface ---------------------------------------------------
|
||||
|
||||
@@ -2238,6 +2234,32 @@ void PrepareForReload() {
|
||||
g_nodeBakedColor.clear();
|
||||
}
|
||||
|
||||
bool LineIconShift(void *fs, const uint8_t *text, int len, float &before, float &after) {
|
||||
before = 0.0f;
|
||||
after = 0.0f;
|
||||
if (!g_inlineEnabled || !LooksReadable(fs) || !LooksReadable(text) || len <= 0 ||
|
||||
!HasInlineTexture(text, len))
|
||||
return false;
|
||||
FsPenFont pf;
|
||||
if (!ResolveFsPenFont(fs, pf))
|
||||
return false;
|
||||
const float overflow = IconOverflowPx(text, len, pf.fontHPen, pf.layoutScale);
|
||||
if (!(overflow > 0.0f))
|
||||
return false;
|
||||
// The node's justify-V as the block creator (FUN_0044d420) reads the
|
||||
// style word: TOP (8) wins, then BOTTOM (0x20), else middle — the value
|
||||
// the emitter reads from the node to split the move.
|
||||
const uint32_t style = Game::Read<uint32_t>(fs, Offsets::OFF_FONTSTRING_FLAGS);
|
||||
const int justifyV = (style & 0x8u) ? 0 : (style & 0x20u) ? 2 : 1;
|
||||
const float move = (justifyV == 1) ? std::floor(overflow * 0.5f) : overflow;
|
||||
const float toFs = pf.fontHInt / pf.fontHPen; // pen px → the getter's units
|
||||
if (justifyV != 0)
|
||||
before = move * toFs;
|
||||
if (justifyV != 2)
|
||||
after = move * toFs;
|
||||
return true;
|
||||
}
|
||||
|
||||
static const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
|
||||
|
||||
} // namespace Text::InlineTexture
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Text::InlineTexture {
|
||||
|
||||
// Called from FrameScript_Initialize_h before the /reload teardown: the reload
|
||||
@@ -20,4 +22,13 @@ namespace Text::InlineTexture {
|
||||
// icon records are dropped (they rebuild as the reloaded UI re-emits its text).
|
||||
void PrepareForReload();
|
||||
|
||||
// The room the tall inline icons on the line [text, text + len) take when
|
||||
// `fs` renders it, in the font string's own units (its height getter's):
|
||||
// `before` is how far the render moves that line's text down, `after` how
|
||||
// far it moves the lines after it — 4.3.4's InitializeText offsets, split by
|
||||
// the font string's vertical justify (top: none / all; middle: half / half,
|
||||
// floored; bottom: all / none). Both 0, and false, without an icon taller
|
||||
// than the font or with the feature off.
|
||||
bool LineIconShift(void *fs, const uint8_t *text, int len, float &before, float &after);
|
||||
|
||||
} // namespace Text::InlineTexture
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user