Compare commits

..
Author SHA1 Message Date
Brues 260d40f49b fix(frame): let the engine runner parse script formats
Frame::ScriptArgs carried its own copy of the script runner's format
switch (0x704F10, %d/%u/%f/%s) so it could pass positional args.
SuperWoWhook extends the live switch at runtime: it points 'e' at a new
jump slot (0x7052CC) that pushes registry[ref] for a 4-byte ref, and
fires CREATE_CHATBUBBLE with "%e%s". Our copy skipped 'e' without
consuming its value, so %s read the ref as a string pointer and
crashed in lua_pushstring (0x6F38A5) for any handler that declares
parameters.

Both runners now run unchanged, and only their own lua_pcall is
redirected (0x705199, 0x704E79) through a thunk that reads the handler
ref, frame and arg count from the runner's frame. RunnerPCall reads
arg1..argN back from the globals the runner set and passes them as
(self, [event,] arg1..argN [, trailing]). The format parser and both
runner reimplementations are gone; RunCore remains for value fires.

Format fires now keep the runner's 19-argument limit and its
lua_gettable saves of this/argN, as vanilla handlers always had. No
engine or ClassicAPI format has more than 12 values; value fires
(FireValues, RunWithValues) are unchanged.

Verified in game: a RegisterAllEvents frame with a modern OnEvent
crashed on a chat bubble with the old build and prints
CREATE_CHATBUBBLE and the speaker GUID with this one.
2026-10-30 15:54:53 -05:00
Brues 8f6ff33a39 refactor(lua): CallbackRegistry in retail's own text
6cc9d0d already gave CallbackRegistry retail's behavior; the file is now
retail Blizzard_SharedXMLBase/CallbackRegistry.lua in retail's own syntax
(select("#", ...), `...` forwarding, retail's comments), with three
differences noted in the header:

- No AttributeDelegate: 1.12 has no taint or SetForbidden, so
  SecureInsertEvent and GetMutableCallbacksByEvent do the delegate's
  work directly.
- TriggerEvent calls securecallfunction from its own loops instead of
  handing secureexecuterange a vararg helper, which in Lua 5.0 would
  build an `arg` table per callback per event.
- UnregisterEvents uses table.wipe; 1.12 has no global wipe.

The one behavior change: TriggerEvent on a registry with no Event table
and undefined events disallowed now fails on self.Event as retail does,
instead of with the "doesn't exist" message. EventRegistry, and registries
derived from it, allow undefined events.

Verified in game: a closure gets its bound argument and a leading nil
payload, an erroring callback goes to the error handler while the others
run, a callback registered mid-dispatch runs from the next trigger, and
UnregisterEvents clears every callback.
2026-10-30 14:01:40 -05:00
Brues 5082f3c829 feat(lua): TableUtil follows retail
The file is now retail Blizzard_SharedXMLBase/TableUtil.lua in retail's
own syntax: the length operator, `...`, select, math.huge, table.wipe and
5.1's unpack(t, i, j) all work here now.

New from retail: TableUtil.SafeCountTable / SafeCountIndexTable,
tInvertToArray and CollectTableDifferences. Fixed by following retail:
TableUtil.Transform returned after the first value, ContainsAllKeys now
also fails on a key only the second table has, and SafeUnpack honors its
startIndex. TableUtil.ExecuteOnKeys stays as the one addition retail does
not have.

Verified in game: ContainsAllKeys, Transform, SafePack / SafeUnpack from
index 2, GetTableValueListFromEnumeration over ipairs,
CollectTableDifferences, SafeCountTable and ExecuteOnKeys all return the
expected values.
2026-10-30 13:57:09 -05:00
Brues 5d0568f759 feat(lua): Pools follows retail, with securecallmethod
Pools.lua is now retail Blizzard_SharedXMLBase/Pools.lua, on the
SecureTypes and ProxyUtil ports the Menu backport already uses.
CreateObjectPool, CreateFramePool and the rest alias the secure variants
as on retail, so they return proxies that expose only the pool methods.
CreateFramePool takes retail's (frameType, parent, template, resetFunc,
forbidden, postCreate, capacity); a table specialization goes through
FrameUtil.SpecializeFrameWithMixins; CreateObjectPool works without a
reset function; Pool_HideAndSetToDefaults is new. The ClassicAPI-only
CreateUnsecuredActorPool and CreateUnsecuredFontStringPoolCollection are
gone (nothing used them).

Two differences, noted in the header: no secret-value check on release
(1.12 has no secret values), and a forbidden pool makes normal frames
(no CreateForbiddenFrame).

The TOC loads SecureTypes and ProxyUtil before Pools, as retail's does;
Pools reads both at load.

SecurePoolCollectionMixin:Release calls securecallmethod, now in
src/baselib/SecureCall.cpp next to securecallfunction, on both Lua
states: object[method](object, ...) with a raw lookup, an error goes to
the error handler and the call returns nothing. That follows retail's
API documentation; it is not probed. docs/API.md covers it.

Verified in game: securecallmethod returns the method's result, a frame
pool hides its internals behind the proxy while acquire and release keep
the count, and a frame pool collection runs a table specialization's
OnLoad, acquires, and releases through securecallmethod.
2026-10-30 13:53:24 -05:00
Brues 9a78e159e7 feat(lua): FrameUtil follows retail
The file is now retail Blizzard_SharedXMLBase/FrameUtil.lua. New from
retail: FrameUtil.RegisterUpdateFunction / UnregisterUpdateFunction,
DialogStyleGlobalMouseDown, SpecializeFrameWithMixins /
ReflectStandardScriptHandlers, RegisterForVariablesLoaded,
FrameUtil.CreateFrame, UpdateScaleForFit / UpdateScaleForFitSpecific,
GetScaledCenter, FitToParent, RegisterForTopLevelParentChanged /
UpdateTopLevelParent and AnimTransitionMixin. RegisterFrameForUnitEvents
forwards `...` as retail does.

AnimTransitionMixin runs on retail's Blizzard_SharedXML/
ScriptAnimationUtil.lua, ported verbatim. Its ShakeFrame scales by
retail's ShakeStrengthUI cvar, which src/ui/ShakeStrength.cpp registers
with retail's help, category and default ("1.000000"); nothing in the
engine reads it.

Not ported: the fade and flash functions, which 1.12's own UIParent.lua
defines and UIParent's OnUpdate drives (Turtle has also changed them);
RegisterFrameForEventCallbacks and its two siblings (no
frame:RegisterEventCallback); ApplyDefaultScale (no GetDefaultScale).
SetParentMaintainRenderLayering does not carry the window: 1.12 has one.

README.md lists FrameUtil, ScriptAnimationUtil and the cvar.

Verified in game: SpecializeFrameWithMixins sets and calls OnLoad and
OnShow with the frame, a SlideLeft StartAnimIn ends at its anchor
(CENTER 0 0), and ShakeFrameRandom with ShakeStrengthUI at 1.000000
returns its cancel function.
2026-10-30 13:53:15 -05:00
Brues 503368623f feat(localization): BreakUpLargeNumbers, AbbreviateNumbers, AbbreviateLargeNumbers
Retail's number formatters (LocalizationDocumentation.lua) and
C_StringUtil.GetDefaultAbbreviationBreakpoints. Retail moved them from
FrameXML into C in 12.0.0, so the behavior is taken from probes of an enUS
retail client, recorded in src/localization/Numbers.cpp:

- BreakUpLargeNumbers drops the fraction of a whole number or one 1000 or
  more in size and groups it with LARGE_NUMBER_SEPERATOR while the
  breakUpLargeNumbers cvar (retail's, default on) is set; other numbers
  keep one decimal, rounded half away from zero. natural writes a million
  or more in words ("1.2 million", "123 million").
- AbbreviateNumbers walks retail's default breakpoints, or
  options.breakpointData, or the Asian table for options.locale koKR /
  zhCN / zhTW. Retail's defaults name THIRD_ and FOURTH_NUMBER_CAP_NO_SPACE,
  which its clients do not define, so billions and trillions carry no
  letter there and here.
- AbbreviateLargeNumbers walks FrameXML's old thresholds, rounds to a whole
  number outside the Asian locales, and falls back to BreakUpLargeNumbers.

The asian natural-word scales (1e4 / 1e8 / 1e12) are read off the strings,
not probed. options.config (CreateAbbreviateConfig) is not backported.

Every locale file, and a new koKR.lua, gets DECIMAL_SEPERATOR, FIRST/SECOND_NUMBER_CAP[_NO_SPACE]
and FIRST..FOURTH_NUMBER from the matching Classic GlobalStrings; the
German, Spanish and French words carry |7 plural tokens, which the grammar
pass does not expand yet.

FormattingUtil gains retail's GetCostString and GetItemCostString on top.

docs/API.md documents the four functions, the cvar and
Constants.MoneyFormattingConstants; README.md lists them.

Verified in game: every probed retail value reproduces exactly (BreakUp
with and without natural, AbbreviateNumbers with defaults, Asian locale
and custom breakpoints, AbbreviateLargeNumbers), the cvar toggles the
separators, and GetItemCostString shows the amount and item icon.
2026-10-30 13:33:40 -05:00
Brues 735d304a20 feat(currency): Constants.MoneyFormattingConstants and GetMoneyString's gold threshold
Retail exposes engine constant tables as Constants.<Name>, documented with
Type "Constants" and a Values list (MoneyConstantsDocumentation.lua).
Game::Lua::RegisterConstants builds Constants[sub] with the same integer
entries RegisterIntegerEnum takes, and the documentation recorder has a
constants kind next to the enum kind, so the API browser lists the table as
constants rather than as an enumeration.

Constants.MoneyFormattingConstants.GOLD_REWARD_THRESHOLD_TO_HIDE_COPPER is
10, retail's value. FormattingUtil's GetMoneyString now applies
checkGoldThreshold through retail's FormatDisplayCopper: above 10 gold the
copper is dropped.

Verified in game: the table is present, GetMoneyString(1234567, nil, true)
shows gold and silver only, and GetMoneyString(51234, nil, true) keeps its
copper.
2026-10-30 13:17:01 -05:00
Brues 29ddd78a80 feat(lua): FormattingUtil follows retail
The file is now retail Blizzard_SharedXML/FormattingUtil.lua (identical in
retail, Classic and Classic Era), in retail's own syntax: the length and
modulo operators and string methods all work here now.

New from retail: FormatValueWithSign (with SYMBOLIC_NEGATIVE_NUMBER "-%s" /
SYMBOLIC_POSITIVE_NUMBER "+%s", the same in every locale's GlobalStrings),
MoneyStringConstants, FormatPercentageRounded,
GetHighlightedNumberDifferenceString, FormatUnreadMailTooltip,
FormattingUtil.AddLeadingZeroes, ReplaceGenderTokens, and GetMoneyString's
color argument.

GetMoneyString gives the coin-icon form, retail's default; retail uses the
text-symbol form only under its colorblind mode, which 1.12 does not have.
checkGoldThreshold is still not applied (it needs
Constants.MoneyFormattingConstants), and GetCostString / GetItemCostString
wait on BreakUpLargeNumbers / AbbreviateNumbers. GetCurrencyString and
GetCurrenciesString are not ported: 1.12 has no currencies.

Verified in game: coin-icon money strings with and without thousands
separators, signed values, FormatLargeNumber, the highlighted number
difference, and gray leading zeroes.
2026-10-30 13:05:13 -05:00
Brues b6c21216e3 fix(lua): EventUtil follows retail; queued variables-loaded callbacks are error-isolated
TriggerOnVariablesLoaded ran the ContinueOnVariablesLoaded queue in a plain
loop, so one erroring callback skipped the rest, and the queue drains only
once, so they never ran that session. It now uses secureexecuterange as
retail does: each error goes to the error handler and the walk goes on.

AreVariablesLoaded reads UIParent.variablesLoaded, as on retail, instead of
a private flag. Retail's UIParent sets that field and calls
TriggerOnVariablesLoaded on VARIABLES_LOADED; 1.12's FrameXML does
neither, so EventUtil's own frame does both.

The header no longer says 1.12 lacks select, C_AddOns or
secureexecuterange; ClassicAPI provides all three. The unknown-event
message uses :format and IsEmpty uses #, as on retail.

Verified in game: AreVariablesLoaded and UIParent.variablesLoaded are
true, a late ContinueOnVariablesLoaded callback runs at once, IsEmpty
works, and ContinueAfterAllEvents rejects an unknown event.
2026-10-30 12:58:01 -05:00
Brues 6cc9d0db90 fix(lua): CallbackRegistry isolates callback errors and defers mid-dispatch registrations
TriggerEvent called each callback directly, so one error left the frame's
OnEvent script and skipped every callback after it in that dispatch. pfUI
now runs many listeners through one EventRegistry dispatch, so one failing
listener could silence the rest. Each callback now runs through
securecallfunction: the error goes to the engine's script error handler and
the loop goes on, which is retail's secureexecuterange contract. It is
called from TriggerEvent's own loop, not through a vararg helper, because a
vararg Lua function allocates an arg table per call on Lua 5.0.

A callback registered for an event while that event is dispatching now
waits in deferredCallbacks and joins after the outermost dispatch, as on
retail. Before, its key went into the table the pairs loop was walking.
UnregisterCallback clears only owners that are present, and drops pending
deferrals.

The rest now follows retail Blizzard_SharedXMLBase/CallbackRegistry.lua:
UnregisterEvents() clears every event and UnregisterEventsByEventTable
clears the listed ones; GenerateCallbackEvents checks its argument; the
file's own GenerateClosure copy gives way to FunctionUtil's global (one
table per call instead of two, with retail's owner + 4 argument limit).

EventRegistry:OnLoad keeps all instance state, since addons derive their
own registry from the instance (pfUI: CreateFromMixins(EventRegistry)).

Verified in game: an erroring callback reports to the handler and the next
one still runs; a callback registered mid-dispatch first runs on the next
TriggerEvent.
2026-10-30 12:49:21 -05:00
Brues c6c201e575 fix(text): auto-height font strings keep their last line past 4 KB
A font string with no height of its own is as tall as its text, and that
height (0x5C2070) is a product, n * fontHeight + (n - 1) * gap. FitText's
count of what fits (0x5C21C0) adds the lines up as a running float sum and
stops at the first line where blockHeight + 2^-20 < sum + fontHeight. Over
many lines the sum drifts from the product by more than 2^-20 at most UI
scales, the last line "does not fit", and FitText cuts the text to its
first 4092 bytes plus "...", with the rest of the box blank. A multi-line
edit box hit this once its text passed 4 KB (ScrollingEditBoxTemplate,
pfUI chat copy).

4.3.4 keeps the product and grows the margin per line: blockHeight +
numLines * 2^-20 (GxuFontGetMaxCharsWithinWidthAndHeight). Text::FitHeight
replaces the loop's fadd of the engine's 2^-20 with a thunk that adds
numLines times it. The first line keeps 1.12's margin; each further line
adds a millionth of the screen height, so real overflow is still cut. A
float32 model of the chain lost the last line in 1276 of 3000 multi-line
fits before and in none after; in game a 30783-byte copy box draws all of
its text.
2026-10-30 11:39:02 -05:00
54 changed files with 3277 additions and 1988 deletions
+7 -3
View File
@@ -69,7 +69,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Cursor](docs/API.md#cursor) | `CursorHasMacro`, `GetCursorInfo` |
| [Container](docs/API.md#container) | `C_Container.AutoStoreItem`, `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemEquipmentSetInfo`, `C_Container.GetContainerItemQuestInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerFreeSlots`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetBackpackAutosortDisabled`, `C_Container.GetBankAutosortDisabled`, `C_Container.GetItemCooldown`, `C_Container.GetSortBagsRightToLeft`, `C_Container.HasContainerItem`, `C_Container.IsContainerFiltered`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SetBackpackAutosortDisabled`, `C_Container.SetBagPortraitTexture`, `C_Container.SetBankAutosortDisabled`, `C_Container.SetItemSearch`, `C_Container.SetSortBagsRightToLeft`, `C_Container.SortBags`, `C_Container.SortBankBags`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown`, `SetBagPortraitTexture` |
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID`, `C_CreatureInfo.GetRaceInfo`, `C_CreatureInfo.GetClassInfo`, `C_CreatureInfo.GetCreatureFamilyInfo`, `C_CreatureInfo.GetCreatureFamilyIDs`, `C_CreatureInfo.GetFactionInfo`, `C_CreatureInfo.GetCreatureTypeInfo`, `C_CreatureInfo.GetCreatureTypeIDs` |
| [Currency](docs/API.md#currency) | `GetCoinText`, `C_CurrencyInfo.GetCoinText`, `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString` |
| [Currency](docs/API.md#currency) | `GetCoinText`, `C_CurrencyInfo.GetCoinText`, `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString`, `Constants.MoneyFormattingConstants` |
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
| [DeathInfo](docs/API.md#deathinfo) | `C_DeathInfo.GetSelfResurrectOptions`, `C_DeathInfo.UseSelfResurrectOption` |
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.PickupEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` |
@@ -100,6 +100,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Item: info](docs/API.md#getitemclassinfoclassid) | `GetItemClassInfo`, `GetItemSubClassInfo`, `GetItemIcon`, `IsConsumableItem`, `IsCurrentItem`, `IsDressableItem`, `IsEquippedItemType`, `IsHarmfulItem`, `IsHelpfulItem`, `IsUsableItem` |
| [Item: inventory](docs/API.md#getaverageitemlevel) | `GetAverageItemLevel`, `GetInventoryItemID`, `GetInventoryItemDurability`, `GetInventoryItemRepairCost`, `GetInventoryItemsForSlot`, `IsDualWielding`, `SetInventoryPortraitTexture` |
| [Item: item IDs](docs/API.md#getitemid--companions-to-the-engines-getitemlink-family) | `GetAuctionItemID`, `GetAuctionSellItemID`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeTargetItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID` |
| [Localization](docs/API.md#localization) | `AbbreviateLargeNumbers`, `AbbreviateNumbers`, `BreakUpLargeNumbers`, `C_StringUtil.GetDefaultAbbreviationBreakpoints` |
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootAllCorpses`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount`, `C_LossOfControl.GetSchoolLockout` |
@@ -233,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. |
@@ -432,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
+187 -2
View File
@@ -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)
@@ -492,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)
@@ -580,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)
@@ -4042,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)`
@@ -11245,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()`).
@@ -11255,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.
@@ -13724,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
+18 -5
View File
@@ -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
View File
@@ -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
+28 -2
View File
@@ -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
@@ -12736,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.
+56 -9
View File
@@ -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
+2
View File
@@ -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
+14 -190
View File
@@ -702,216 +702,40 @@ const Game::Doc::Function kCancelUnitBuff{
"Cancels one of your own auras by index.", kCancelUnitBuffArgs, {},
"SpellGlobals"};
const Game::Doc::Field kAuraDataFields[] = {
Game::Doc::Req("name", "string"),
Game::Doc::Req("icon", "string", "The icon's texture path."),
Game::Doc::Req("applications", "number", "The stack count."),
Game::Doc::Req("spellId", "number"),
Game::Doc::Req("dispelName", "string",
"Magic, Curse, Disease, or Poison; empty when the aura has no dispel type."),
Game::Doc::Req("isHelpful", "bool"),
Game::Doc::Req("isHarmful", "bool"),
Game::Doc::Req("duration", "number", "In seconds; 0 when the duration is not known."),
Game::Doc::Req("expirationTime", "number",
"When the aura ends, in GetTime() seconds; 0 when not known."),
Game::Doc::Opt("sourceUnit", "UnitToken", nullptr,
"Nil when the cast was not seen or the caster has no unit token."),
Game::Doc::Opt("sourceGUID", "WOWGUID", nullptr,
"The caster's GUID, a ClassicAPI extension; nil when the cast was not seen."),
Game::Doc::Opt("auraInstanceID", "number", nullptr,
"Nil for an aura that is not in the unit's aura list."),
Game::Doc::Req("charges", "number", "Always 0."),
Game::Doc::Req("maxCharges", "number", "Always 0."),
Game::Doc::Req("timeMod", "number", "Always 1."),
Game::Doc::Req("isFromPlayerOrPlayerPet", "bool",
"True when any player or a player's pet applied the aura."),
Game::Doc::Req("canApplyAura", "bool",
"True when the player or the pet knows the spell that applies the aura."),
Game::Doc::Req("isRaid", "bool", "True when the aura passes the RAID filter."),
Game::Doc::Req("isStealable", "bool",
"True when the player can remove this buff from an enemy."),
Game::Doc::Req("isBossAura", "bool", "Always false."),
Game::Doc::Req("isNameplateOnly", "bool", "Always false."),
Game::Doc::Req("nameplateShowAll", "bool", "Always false."),
Game::Doc::Req("nameplateShowPersonal", "bool", "Always false."),
Game::Doc::Req("shouldConsolidate", "bool", "Always false."),
};
const Game::Doc::Structure kAuraData{"AuraData", "UnitAuras", kAuraDataFields,
"One aura on a unit."};
// The Classic Era `UnitAura` tuple, shared by the four positional getters.
// `Data::PushPositional` is the source of the order.
const Game::Doc::Field kPositionalRets[] = {
Game::Doc::Opt("name", "string", nullptr,
"Nil, as the only return, when there is no such aura."),
Game::Doc::Opt("icon", "string"),
Game::Doc::Opt("count", "number"),
Game::Doc::Opt("dispelType", "string", nullptr,
"Empty when the aura has no dispel type."),
Game::Doc::Opt("duration", "number"),
Game::Doc::Opt("expirationTime", "number"),
Game::Doc::Opt("source", "UnitToken", nullptr, "Nil when the caster is not known."),
Game::Doc::Opt("isStealable", "bool"),
Game::Doc::Opt("nameplateShowPersonal", "bool", nullptr, "Always false."),
Game::Doc::Opt("spellId", "number"),
Game::Doc::Opt("canApplyAura", "bool"),
Game::Doc::Opt("isBossDebuff", "bool", nullptr, "Always false."),
Game::Doc::Opt("castByPlayer", "bool", nullptr,
"True when a player applied the aura; false for a pet."),
Game::Doc::Opt("nameplateShowAll", "bool", nullptr, "Always false."),
Game::Doc::Opt("timeMod", "number", nullptr, "Always 1."),
};
const Game::Doc::Field kAuraRets[] = {
Game::Doc::Opt("aura", "AuraData", nullptr, "Nil when there is no such aura."),
};
const Game::Doc::Field kByIndexArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("index", "luaIndex"),
Game::Doc::Opt("filter", "AuraFilters", nullptr,
"Tokens such as HELPFUL, HARMFUL, PLAYER, RAID; HELPFUL when no range is given."),
};
const Game::Doc::Function kGetAuraDataByIndex{
"Returns the aura at an index among a unit's auras that match a filter.", kByIndexArgs,
kAuraRets};
const Game::Doc::Function kUnitAura{
"Returns the aura at an index as positional values, without making a table.",
kByIndexArgs, kPositionalRets};
const Game::Doc::Field kLockedIndexArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("index", "luaIndex"),
};
const Game::Doc::Function kGetBuffDataByIndex{
"Returns the buff at an index on a unit.", kLockedIndexArgs, kAuraRets};
const Game::Doc::Function kGetDebuffDataByIndex{
"Returns the debuff at an index on a unit.", kLockedIndexArgs, kAuraRets};
const Game::Doc::Field kLockedFilterArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("index", "luaIndex"),
Game::Doc::Opt("filter", "AuraFilters", nullptr,
"Further tokens such as PLAYER or RAID; the range is fixed."),
};
const Game::Doc::Function kUnitBuff{
"Returns the buff at an index as positional values, without making a table.",
kLockedFilterArgs, kPositionalRets};
const Game::Doc::Function kUnitDebuff{
"Returns the debuff at an index as positional values, without making a table.",
kLockedFilterArgs, kPositionalRets};
const Game::Doc::Field kSlotsArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Opt("filter", "AuraFilters"),
Game::Doc::Opt("maxSlots", "number", nullptr, "Nil or 0 returns all of them."),
Game::Doc::Opt("continuationToken", "number", nullptr,
"The first return of the previous call, to get the next batch."),
Game::Doc::Opt("buf", "table", nullptr,
"A table to fill with the slot IDs instead; the call then returns the "
"token and the count. A ClassicAPI extension."),
};
const Game::Doc::Field kSlotsRets[] = {
Game::Doc::Opt("outContinuationToken", "number", nullptr,
"Nil when this batch reaches the last aura."),
Game::Doc::Vararg("number", "The slot IDs, valid only in the frame they are returned."),
};
const Game::Doc::Function kGetAuraSlots{
"Returns the slot IDs of a unit's auras that match a filter, in batches.", kSlotsArgs,
kSlotsRets};
const Game::Doc::Field kBySlotArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("slot", "number", "A slot ID from C_UnitAuras.GetAuraSlots."),
};
const Game::Doc::Function kGetAuraDataBySlot{
"Returns the aura that a slot ID names.", kBySlotArgs, kAuraRets};
const Game::Doc::Function kUnitAuraBySlot{
"Returns the aura that a slot ID names as positional values, without making a table.",
kBySlotArgs, kPositionalRets};
const Game::Doc::Field kBySpellIDArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("spellID", "number"),
Game::Doc::Opt("filter", "AuraFilters", nullptr, "Nil searches buffs, then debuffs."),
};
const Game::Doc::Function kGetUnitAuraBySpellID{
"Returns the first aura on a unit with a spell ID; the player's own aura comes first.",
kBySpellIDArgs, kAuraRets};
const Game::Doc::Field kPlayerBySpellIDArgs[] = {
Game::Doc::Req("spellID", "number"),
};
const Game::Doc::Function kGetPlayerAuraBySpellID{
"Returns the first aura on the player with a spell ID.", kPlayerBySpellIDArgs, kAuraRets};
const Game::Doc::Field kBySpellNameArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("spellName", "cstring", "The localized name; case does not matter."),
Game::Doc::Opt("filter", "AuraFilters", nullptr, "Nil searches buffs, then debuffs."),
};
const Game::Doc::Function kGetAuraDataBySpellName{
"Returns the first aura on a unit with a spell name; the player's own aura comes first.",
kBySpellNameArgs, kAuraRets};
const Game::Doc::Field kUnitAurasArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Opt("filter", "AuraFilters", nullptr,
"With no HELPFUL or HARMFUL token, both are returned."),
};
const Game::Doc::Field kUnitAurasRets[] = {
Game::Doc::Req("auras", "table", "An array of AuraData; empty when the unit has none."),
};
const Game::Doc::Function kGetUnitAuras{
"Returns all of a unit's auras that match a filter, buffs first.", kUnitAurasArgs,
kUnitAurasRets};
const Game::Doc::Field kDispelColorArgs[] = {
Game::Doc::Opt("dispelName", "string", nullptr,
"Magic, Curse, Disease, Poison, or Enrage; any other value gives the "
"no-type color."),
};
const Game::Doc::Field kDispelColorRets[] = {
Game::Doc::Req("color", "colorRGBA"),
};
const Game::Doc::Function kGetAuraDispelTypeColor{
"Returns the border color for an aura's dispel type.", kDispelColorArgs,
kDispelColorRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataByIndex",
&Script_GetAuraDataByIndex, &kGetAuraDataByIndex);
&Script_GetAuraDataByIndex);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetBuffDataByIndex",
&Script_GetBuffDataByIndex, &kGetBuffDataByIndex);
&Script_GetBuffDataByIndex);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetDebuffDataByIndex",
&Script_GetDebuffDataByIndex, &kGetDebuffDataByIndex);
&Script_GetDebuffDataByIndex);
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAura",
&Script_UnitAura, &kUnitAura);
&Script_UnitAura);
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitBuff",
&Script_UnitBuff, &kUnitBuff);
&Script_UnitBuff);
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitDebuff",
&Script_UnitDebuff, &kUnitDebuff);
&Script_UnitDebuff);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraSlots",
&Script_GetAuraSlots, &kGetAuraSlots);
&Script_GetAuraSlots);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySlot",
&Script_GetAuraDataBySlot, &kGetAuraDataBySlot);
&Script_GetAuraDataBySlot);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataByAuraInstanceID",
&Script_GetAuraDataByAuraInstanceID,
&kGetAuraDataByAuraInstanceID);
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAuraBySlot",
&Script_UnitAuraBySlot, &kUnitAuraBySlot);
&Script_UnitAuraBySlot);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuraBySpellID",
&Script_GetUnitAuraBySpellID, &kGetUnitAuraBySpellID);
&Script_GetUnitAuraBySpellID);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetPlayerAuraBySpellID",
&Script_GetPlayerAuraBySpellID, &kGetPlayerAuraBySpellID);
&Script_GetPlayerAuraBySpellID);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySpellName",
&Script_GetAuraDataBySpellName, &kGetAuraDataBySpellName);
&Script_GetAuraDataBySpellName);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuras",
&Script_GetUnitAuras, &kGetUnitAuras);
&Script_GetUnitAuras);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDispelTypeColor",
&Script_GetAuraDispelTypeColor, &kGetAuraDispelTypeColor);
&Script_GetAuraDispelTypeColor);
Game::Lua::RegisterGlobalFunction("CancelUnitBuff", &Script_CancelUnitBuff,
&kCancelUnitBuff);
}
+1 -15
View File
@@ -193,23 +193,9 @@ int __fastcall Script_RegisterComboDuration(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("spellID", "number"),
Game::Doc::Req("baseSeconds", "number",
"The duration at zero combo points; each point adds a fifth of the "
"difference to maxSeconds."),
Game::Doc::Req("maxSeconds", "number", "The duration at five combo points."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("success", "bool"),
};
const Game::Doc::Function kRegisterComboDuration{
"Sets the combo point durations of a finisher whose client data is missing or wrong.",
kArgs, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_UnitAuras", "RegisterComboDuration",
&Script_RegisterComboDuration, &kRegisterComboDuration);
&Script_RegisterComboDuration);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -22
View File
@@ -946,31 +946,10 @@ int __fastcall Script_RegisterAuraDurationModifierByTrigger(void *L) {
return 1;
}
const Game::Doc::Field kDurationModArgs[] = {
Game::Doc::Req("triggerFamily", "number", "The SpellFamilyName of the triggering cast."),
Game::Doc::Req("triggerSchool", "number",
"The school index the trigger must have, or below 0 for any school."),
Game::Doc::Req("affectedFamily", "number", "The SpellFamilyName of the aura to change."),
Game::Doc::Req("affectedFamilyFlags", "number",
"A SpellFamilyFlags mask the aura must overlap, or 0 to match on the icon."),
Game::Doc::Req("affectedIcon", "number",
"The SpellIconID the aura must have, or 0 for any icon."),
Game::Doc::Req("op", "string", "refresh, reduce, set, or remove."),
Game::Doc::Opt("valueSeconds", "number", nullptr, "The amount for reduce and set."),
};
const Game::Doc::Field kDurationModRets[] = {
Game::Doc::Req("success", "bool"),
};
const Game::Doc::Function kRegisterAuraDurationModifierByTrigger{
"Changes the shown duration of the caster's aura on a unit when a matching cast by "
"that caster lands.",
kDurationModArgs, kDurationModRets, nullptr, true};
void RegisterDurationModLua() {
Game::Lua::RegisterTableFunction("C_UnitAuras",
"RegisterAuraDurationModifierByTrigger",
&Script_RegisterAuraDurationModifierByTrigger,
&kRegisterAuraDurationModifierByTrigger);
&Script_RegisterAuraDurationModifierByTrigger);
}
const Game::ModuleAutoRegister _autoregDurationMod{&RegisterDurationModLua};
+31 -1
View File
@@ -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);
}
+1 -15
View File
@@ -62,23 +62,9 @@ int __fastcall Script_C_Container_AutoStoreItem(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("srcBag", "BagIndex"),
Game::Doc::Req("srcSlot", "luaIndex"),
Game::Doc::Opt("dstBag", "BagIndex", "0",
"0 for anywhere in the bags, 1 to 4 for one bag, -1 for the bank."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("sent", "bool", "False when the server cannot be asked for this move."),
};
const Game::Doc::Function kAutoStoreItem{
"Moves an item and lets the server choose where it goes, merging it into "
"existing stacks first.",
kArgs, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "AutoStoreItem",
&Script_C_Container_AutoStoreItem, &kAutoStoreItem);
&Script_C_Container_AutoStoreItem);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -13
View File
@@ -43,21 +43,9 @@ int __fastcall Script_C_Container_GetContainerItemCharges(void *L) {
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("uses", "number", nullptr, "Nil when the slot is empty."),
};
const Game::Doc::Function kGetContainerItemCharges{
"Returns the uses left on the item in a bag slot: its charges times its stack size.",
kArgs, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemCharges",
&Script_C_Container_GetContainerItemCharges,
&kGetContainerItemCharges);
&Script_C_Container_GetContainerItemCharges);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -14
View File
@@ -33,22 +33,9 @@ int __fastcall Script_C_Container_GetItemCooldown(void *L) {
return 3;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("itemID", "number"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("startTime", "number",
"When the cooldown started, in GetTime() seconds; 0 when none."),
Game::Doc::Req("duration", "number", "Length in seconds; 0 when there is no cooldown."),
Game::Doc::Req("enable", "number",
"0 while the cooldown waits to start, as for a potion used in combat; else 1."),
};
const Game::Doc::Function kGetItemCooldown{
"Returns the cooldown of an item's use effect.", kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetItemCooldown",
&Script_C_Container_GetItemCooldown, &kGetItemCooldown);
&Script_C_Container_GetItemCooldown);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -14
View File
@@ -40,22 +40,9 @@ int __fastcall Script_C_Container_GetContainerItemDurability(void *L) {
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("durability", "number", nullptr,
"Nil when the slot is empty or the item has no durability."),
Game::Doc::Opt("maxDurability", "number"),
};
const Game::Doc::Function kGetContainerItemDurability{
"Returns the current and maximum durability of the item in a bag slot.", kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemDurability",
&Script_C_Container_GetContainerItemDurability,
&kGetContainerItemDurability);
&Script_C_Container_GetContainerItemDurability);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -16
View File
@@ -80,25 +80,10 @@ int __fastcall Script_C_Container_GetContainerItemEquipmentSetInfo(void *L) {
return 2;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("inSet", "bool"),
Game::Doc::Opt("setList", "string", nullptr,
"The set names joined by PLAYER_LIST_DELIMITER; nil when inSet is false."),
};
const Game::Doc::Function kGetContainerItemEquipmentSetInfo{
"Returns whether the item in a bag slot is saved in an equipment set, and the "
"names of those sets.",
kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction(
"C_Container", "GetContainerItemEquipmentSetInfo",
&Script_C_Container_GetContainerItemEquipmentSetInfo,
&kGetContainerItemEquipmentSetInfo);
&Script_C_Container_GetContainerItemEquipmentSetInfo);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+3 -33
View File
@@ -182,45 +182,15 @@ int __fastcall Script_C_Container_CalculateTotalNumberOfFreeBagSlots(void *L) {
return 1;
}
const Game::Doc::Field kBagArgs[] = {
Game::Doc::Req("bagIndex", "BagIndex", "0 for the backpack, 1 to 4 for the equipped bags."),
};
const Game::Doc::Field kNumFreeSlotsRets[] = {
Game::Doc::Req("numFreeSlots", "number", "0 when the bag does not exist."),
Game::Doc::Req("bagFamily", "number",
"The bag's family bitmask; 0 for a bag that takes any item."),
};
const Game::Doc::Function kGetContainerNumFreeSlots{
"Returns the number of empty slots in a bag and the kind of items the bag takes.",
kBagArgs, kNumFreeSlotsRets};
const Game::Doc::Field kFreeSlotsRets[] = {
Game::Doc::Opt("freeSlots", "table", nullptr,
"The empty slot numbers in ascending order; nil when the bag does not exist."),
};
const Game::Doc::Function kGetContainerFreeSlots{
"Returns the numbers of the empty slots in a bag.", kBagArgs, kFreeSlotsRets};
const Game::Doc::Field kTotalRets[] = {
Game::Doc::Req("totalFreeSlots", "number"),
};
const Game::Doc::Function kCalculateTotalNumberOfFreeBagSlots{
"Returns the number of empty slots in the backpack and the equipped bags that take "
"any item.",
{}, kTotalRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerNumFreeSlots",
&Script_C_Container_GetContainerNumFreeSlots,
&kGetContainerNumFreeSlots);
&Script_C_Container_GetContainerNumFreeSlots);
Game::Lua::RegisterTableFunction("C_Container", "GetContainerFreeSlots",
&Script_C_Container_GetContainerFreeSlots,
&kGetContainerFreeSlots);
&Script_C_Container_GetContainerFreeSlots);
Game::Lua::RegisterTableFunction("C_Container", "CalculateTotalNumberOfFreeBagSlots",
&Script_C_Container_CalculateTotalNumberOfFreeBagSlots,
&kCalculateTotalNumberOfFreeBagSlots);
&Script_C_Container_CalculateTotalNumberOfFreeBagSlots);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -12
View File
@@ -40,20 +40,9 @@ int __fastcall Script_C_Container_HasContainerItem(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("hasItem", "bool"),
};
const Game::Doc::Function kHasContainerItem{
"Returns whether a bag slot holds an item, even one whose data is not cached yet.",
kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "HasContainerItem",
&Script_C_Container_HasContainerItem, &kHasContainerItem);
&Script_C_Container_HasContainerItem);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+2 -16
View File
@@ -117,27 +117,13 @@ int __fastcall Script_C_Container_UseHearthstone(void *L) {
return 1;
}
const Game::Doc::Field kHasRets[] = {
Game::Doc::Opt("itemID", "number", nullptr, "Nil when no hearthstone is in the bags."),
};
const Game::Doc::Function kPlayerHasHearthstone{
"Returns the ID of the hearthstone in the player's bags.", {}, kHasRets};
const Game::Doc::Field kUseRets[] = {
Game::Doc::Req("used", "bool",
"True when a hearthstone was found; the cast itself can still fail."),
};
const Game::Doc::Function kUseHearthstone{"Uses the hearthstone in the player's bags.", {},
kUseRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "PlayerHasHearthstone",
&Script_C_Container_PlayerHasHearthstone,
&kPlayerHasHearthstone);
&Script_C_Container_PlayerHasHearthstone);
Game::Lua::RegisterTableFunction("C_Container", "UseHearthstone",
&Script_C_Container_UseHearthstone, &kUseHearthstone);
&Script_C_Container_UseHearthstone);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -11
View File
@@ -41,19 +41,9 @@ int __fastcall Script_C_Container_GetContainerItemID(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("itemID", "number", nullptr, "Nil when the slot is empty."),
};
const Game::Doc::Function kGetContainerItemID{"Returns the ID of the item in a bag slot.",
kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemID",
&Script_C_Container_GetContainerItemID, &kGetContainerItemID);
&Script_C_Container_GetContainerItemID);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+2 -16
View File
@@ -57,23 +57,9 @@ int __fastcall Script_C_Container_MoveItem(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("srcBag", "BagIndex"),
Game::Doc::Req("srcSlot", "luaIndex"),
Game::Doc::Req("dstBag", "BagIndex"),
Game::Doc::Req("dstSlot", "luaIndex"),
Game::Doc::Req("count", "number", "From 1 to 255."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("sent", "bool", "False when an argument is not valid."),
};
const Game::Doc::Function kMoveItem{
"Moves part of a stack to another bag slot without the cursor.", kArgs, kRets, nullptr,
true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "MoveItem", &Script_C_Container_MoveItem,
&kMoveItem);
Game::Lua::RegisterTableFunction("C_Container", "MoveItem",
&Script_C_Container_MoveItem);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -16
View File
@@ -37,24 +37,9 @@ int __fastcall Script_C_Container_IsContainerItemOpenable(void *L) {
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("isOpenable", "bool", nullptr,
"Nil when the slot is empty or the item's data is not cached yet."),
Game::Doc::Opt("canOpen", "bool", nullptr, "False while the item is still locked."),
};
const Game::Doc::Function kIsContainerItemOpenable{
"Returns whether the item in a bag slot can be opened, and whether the player can "
"open it now.",
kArgs, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "IsContainerItemOpenable",
&Script_C_Container_IsContainerItemOpenable,
&kIsContainerItemOpenable);
&Script_C_Container_IsContainerItemOpenable);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -21
View File
@@ -97,29 +97,9 @@ int __fastcall Script_C_Container_GetContainerItemQuestInfo(void *L) {
return 1;
}
const Game::Doc::Field kItemQuestInfoFields[] = {
Game::Doc::Req("isQuestItem", "bool"),
Game::Doc::Opt("questID", "number", nullptr,
"The quest the item begins; nil when it begins none."),
Game::Doc::Req("isActive", "bool", "True when the quest in questID is in the quest log."),
};
const Game::Doc::Structure kItemQuestInfo{"ItemQuestInfo", "Container", kItemQuestInfoFields};
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("questInfo", "ItemQuestInfo",
"Always a table; an empty slot reports no quest."),
};
const Game::Doc::Function kGetContainerItemQuestInfo{
"Returns how the item in a bag slot relates to quests.", kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemQuestInfo",
&Script_C_Container_GetContainerItemQuestInfo,
&kGetContainerItemQuestInfo);
&Script_C_Container_GetContainerItemQuestInfo);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -13
View File
@@ -37,21 +37,9 @@ int __fastcall Script_C_Container_GetContainerItemRepairCost(void *L) {
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("repairCost", "number", "In copper; 0 when the item needs no repair."),
};
const Game::Doc::Function kGetContainerItemRepairCost{
"Returns what a merchant charges to repair the item in a bag slot.", kArgs, kRets,
nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemRepairCost",
&Script_C_Container_GetContainerItemRepairCost,
&kGetContainerItemRepairCost);
&Script_C_Container_GetContainerItemRepairCost);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+2 -14
View File
@@ -49,21 +49,9 @@ int __fastcall Script_C_Container_SwapItems(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("srcBag", "BagIndex"),
Game::Doc::Req("srcSlot", "luaIndex"),
Game::Doc::Req("dstBag", "BagIndex"),
Game::Doc::Req("dstSlot", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("sent", "bool", "False when an argument is not valid."),
};
const Game::Doc::Function kSwapItems{
"Swaps the contents of two bag slots without the cursor.", kArgs, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "SwapItems", &Script_C_Container_SwapItems,
&kSwapItems);
Game::Lua::RegisterTableFunction("C_Container", "SwapItems",
&Script_C_Container_SwapItems);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+40
View File
@@ -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
@@ -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
+353 -50
View File
@@ -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);
+463 -118
View File
@@ -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 = "新設定";
+18 -152
View File
@@ -543,180 +543,46 @@ int __fastcall Script_UseEquipmentSet(void *L) {
return 1;
}
const Game::Doc::Field kSetIDArgs[] = {
Game::Doc::Req("setID", "number"),
};
const Game::Doc::Field kSlotArgs[] = {
Game::Doc::Req("slot", "luaIndex", "An equipment slot, from 1 to 19."),
};
const Game::Doc::Field kCanUseRets[] = {
Game::Doc::Req("canUseEquipmentSets", "bool", "False while a set swap is in progress."),
};
const Game::Doc::Function kCanUseEquipmentSets{
"Returns whether an equipment set can be used now.", {}, kCanUseRets};
const Game::Doc::Field kNumRets[] = {
Game::Doc::Req("numEquipmentSets", "number"),
};
const Game::Doc::Function kGetNumEquipmentSets{
"Returns the number of equipment sets the character has.", {}, kNumRets};
const Game::Doc::Field kIDsRets[] = {
Game::Doc::Req("setIDs", "table", "The set IDs in the order the sets were created."),
};
const Game::Doc::Function kGetEquipmentSetIDs{
"Returns the IDs of all the character's equipment sets.", {}, kIDsRets};
const Game::Doc::Field kIDArgs[] = {
Game::Doc::Req("name", "cstring", "The exact name; case matters."),
};
const Game::Doc::Field kIDRets[] = {
Game::Doc::Opt("setID", "number", nullptr, "Nil when no set has that name."),
};
const Game::Doc::Function kGetEquipmentSetID{
"Returns the ID of the equipment set with a name.", kIDArgs, kIDRets};
const Game::Doc::Field kInfoRets[] = {
Game::Doc::Opt("name", "string", nullptr,
"Nil, with the rest, when the set does not exist."),
Game::Doc::Opt("icon", "string", nullptr, "The icon's texture name."),
Game::Doc::Opt("setID", "number"),
Game::Doc::Opt("isEquipped", "bool", nullptr,
"True when every slot that is not ignored holds what the set saved."),
Game::Doc::Opt("numItems", "number", nullptr,
"numEquipped plus numInInventory plus numMissing."),
Game::Doc::Opt("numEquipped", "number"),
Game::Doc::Opt("numInInventory", "number", nullptr,
"In the bags, or in the bank while the bank is open."),
Game::Doc::Opt("numMissing", "number"),
Game::Doc::Opt("numIgnored", "number"),
};
const Game::Doc::Function kGetEquipmentSetInfo{
"Returns an equipment set's name, icon, and where its items are.", kSetIDArgs,
kInfoRets};
const Game::Doc::Field kIgnoredRets[] = {
Game::Doc::Opt("ignoredSlots", "table", nullptr,
"The numbers of the ignored slots; nil when the set does not exist."),
};
const Game::Doc::Function kGetIgnoredSlots{
"Returns the slots an equipment set leaves alone.", kSetIDArgs, kIgnoredRets};
const Game::Doc::Field kItemIDsRets[] = {
Game::Doc::Opt("itemIDs", "table", nullptr,
"Keyed by slot: the item ID, 0 for an empty slot, 1 for an ignored slot, "
"or -1 when not known."),
};
const Game::Doc::Function kGetItemIDs{
"Returns the item ID an equipment set saved for each slot.", kSetIDArgs, kItemIDsRets};
const Game::Doc::Field kLocationsRets[] = {
Game::Doc::Opt("locations", "table", nullptr,
"Keyed by slot: a packed location, 0 for an empty slot, 1 for an ignored "
"slot, or -1 for a missing item."),
};
const Game::Doc::Function kGetItemLocations{
"Returns where each item of an equipment set is now.", kSetIDArgs, kLocationsRets};
const Game::Doc::Field kCreateArgs[] = {
Game::Doc::Req("name", "cstring", "Shorter than 32 characters, with no line break."),
Game::Doc::Opt("icon", "string", "\"INV_Misc_QuestionMark\"", "A texture name."),
};
const Game::Doc::Field kCreateRets[] = {
Game::Doc::Opt("setID", "number", nullptr, "Nil when the name is already in use."),
};
const Game::Doc::Function kCreateEquipmentSet{
"Saves the equipped items as a new equipment set.", kCreateArgs, kCreateRets};
const Game::Doc::Field kSaveArgs[] = {
Game::Doc::Req("setID", "number"),
Game::Doc::Opt("icon", "string", nullptr, "A new texture name; nil keeps the icon."),
};
const Game::Doc::Function kSaveEquipmentSet{
"Saves the equipped items into an existing equipment set.", kSaveArgs, {}};
const Game::Doc::Field kModifyArgs[] = {
Game::Doc::Req("setID", "number"),
Game::Doc::Req("newName", "cstring"),
};
const Game::Doc::Function kModifyEquipmentSet{
"Renames an equipment set; does nothing when another set has the name.", kModifyArgs,
{}};
const Game::Doc::Function kDeleteEquipmentSet{
"Deletes an equipment set and removes it from the action bars.", kSetIDArgs, {}};
const Game::Doc::Function kIgnoreSlotForSave{
"Makes the next save of an equipment set leave a slot alone.", kSlotArgs, {}};
const Game::Doc::Function kUnignoreSlotForSave{
"Makes the next save of an equipment set include a slot again.", kSlotArgs, {}};
const Game::Doc::Field kIsIgnoredRets[] = {
Game::Doc::Req("isSlotIgnored", "bool"),
};
const Game::Doc::Function kIsSlotIgnoredForSave{
"Returns whether the next save of an equipment set leaves a slot alone.", kSlotArgs,
kIsIgnoredRets};
const Game::Doc::Function kClearIgnoredSlotsForSave{
"Makes the next save of an equipment set include every slot.", {}, {}};
const Game::Doc::Field kLockedRets[] = {
Game::Doc::Opt("hasLockedItems", "bool", nullptr, "Nil when the set does not exist."),
};
const Game::Doc::Function kEquipmentSetContainsLockedItems{
"Returns whether an item of an equipment set is locked by a move in progress.",
kSetIDArgs, kLockedRets};
const Game::Doc::Field kUseRets[] = {
Game::Doc::Req("setWasEquipped", "bool",
"True when the swap starts; EQUIPMENT_SWAP_FINISHED gives the result."),
};
const Game::Doc::Function kUseEquipmentSet{
"Equips the items of an equipment set.", kSetIDArgs, kUseRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_EquipmentSet", "CanUseEquipmentSets",
&Script_CanUseEquipmentSets, &kCanUseEquipmentSets);
&Script_CanUseEquipmentSets);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetNumEquipmentSets",
&Script_GetNumEquipmentSets, &kGetNumEquipmentSets);
&Script_GetNumEquipmentSets);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetEquipmentSetIDs",
&Script_GetEquipmentSetIDs, &kGetEquipmentSetIDs);
&Script_GetEquipmentSetIDs);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetEquipmentSetID",
&Script_GetEquipmentSetID, &kGetEquipmentSetID);
&Script_GetEquipmentSetID);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetEquipmentSetInfo",
&Script_GetEquipmentSetInfo, &kGetEquipmentSetInfo);
&Script_GetEquipmentSetInfo);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetIgnoredSlots",
&Script_GetIgnoredSlots, &kGetIgnoredSlots);
&Script_GetIgnoredSlots);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetItemIDs",
&Script_GetItemIDs, &kGetItemIDs);
&Script_GetItemIDs);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetItemLocations",
&Script_GetItemLocations, &kGetItemLocations);
&Script_GetItemLocations);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "CreateEquipmentSet",
&Script_CreateEquipmentSet, &kCreateEquipmentSet);
&Script_CreateEquipmentSet);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "SaveEquipmentSet",
&Script_SaveEquipmentSet, &kSaveEquipmentSet);
&Script_SaveEquipmentSet);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "ModifyEquipmentSet",
&Script_ModifyEquipmentSet, &kModifyEquipmentSet);
&Script_ModifyEquipmentSet);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "DeleteEquipmentSet",
&Script_DeleteEquipmentSet, &kDeleteEquipmentSet);
&Script_DeleteEquipmentSet);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "IgnoreSlotForSave",
&Script_IgnoreSlotForSave, &kIgnoreSlotForSave);
&Script_IgnoreSlotForSave);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "UnignoreSlotForSave",
&Script_UnignoreSlotForSave, &kUnignoreSlotForSave);
&Script_UnignoreSlotForSave);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "IsSlotIgnoredForSave",
&Script_IsSlotIgnoredForSave, &kIsSlotIgnoredForSave);
&Script_IsSlotIgnoredForSave);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "ClearIgnoredSlotsForSave",
&Script_ClearIgnoredSlotsForSave,
&kClearIgnoredSlotsForSave);
&Script_ClearIgnoredSlotsForSave);
Game::Lua::RegisterTableFunction("C_EquipmentSet",
"EquipmentSetContainsLockedItems",
&Script_EquipmentSetContainsLockedItems,
&kEquipmentSetContainsLockedItems);
&Script_EquipmentSetContainsLockedItems);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "UseEquipmentSet",
&Script_UseEquipmentSet, &kUseEquipmentSet);
&Script_UseEquipmentSet);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+3 -25
View File
@@ -151,30 +151,8 @@ constexpr int LOC_IGNORED = 1;
// when the swap settles — see
// `EquipmentSet::Pending`. `success` is
// 1 or nil, never 0 (a Lua 0 is true).
inline const Game::Doc::Event kSetsChangedDoc{
"EquipmentSet",
"Fires when an equipment set is created, saved, renamed, or deleted, or when the slots "
"ignored for the next save change.",
{}};
inline const Game::Doc::Field kSwapPendingPayload[] = {
Game::Doc::Req("setID", "number"),
};
inline const Game::Doc::Event kSwapPendingDoc{
"EquipmentSet", "Fires when C_EquipmentSet.UseEquipmentSet has sent all its moves.",
kSwapPendingPayload};
inline const Game::Doc::Field kSwapFinishedPayload[] = {
Game::Doc::Opt("result", "number", nullptr, "1 when the swap succeeded, else nil."),
Game::Doc::Req("setID", "number"),
};
inline const Game::Doc::Event kSwapFinishedDoc{
"EquipmentSet", "Fires when the moves of an equipment set swap are done.",
kSwapFinishedPayload};
inline const Event::Custom::AutoReserve kEvtSetsChanged{"EQUIPMENT_SETS_CHANGED",
&kSetsChangedDoc};
inline const Event::Custom::AutoReserve kEvtSwapPending{"EQUIPMENT_SWAP_PENDING",
&kSwapPendingDoc};
inline const Event::Custom::AutoReserve kEvtSwapFinished{"EQUIPMENT_SWAP_FINISHED",
&kSwapFinishedDoc};
inline const Event::Custom::AutoReserve kEvtSetsChanged{"EQUIPMENT_SETS_CHANGED"};
inline const Event::Custom::AutoReserve kEvtSwapPending{"EQUIPMENT_SWAP_PENDING"};
inline const Event::Custom::AutoReserve kEvtSwapFinished{"EQUIPMENT_SWAP_FINISHED"};
} // namespace EquipmentSet
+1 -12
View File
@@ -218,16 +218,6 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
{"SetEquipmentSet", &Script_GameTooltipSetEquipmentSet},
};
const Game::Doc::Field kSetEquipmentSetArgs[] = {
Game::Doc::Req("name", "cstring", "The name of the equipment set."),
};
const Game::Doc::Function kSetEquipmentSet{
"Shows an equipment set in the tooltip: its name and where its items are.",
kSetEquipmentSetArgs, {}};
const Game::Doc::Method g_methodDocs[] = {
{"SetEquipmentSet", &kSetEquipmentSet},
};
} // namespace
static void RegisterLuaFunctions() {
@@ -236,8 +226,7 @@ static void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY),
g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+19 -207
View File
@@ -669,242 +669,54 @@ static int __fastcall Script_C_Reputation_SetWatchedFactionByID(void *L) {
return 0;
}
const Game::Doc::Field kFactionDataFields[] = {
Game::Doc::Req("factionID", "number"),
Game::Doc::Req("name", "string"),
Game::Doc::Req("description", "string", "Empty when the faction has none."),
Game::Doc::Req("reaction", "luaIndex", "The rank, from 1 for Hated to 8 for Exalted."),
Game::Doc::Req("currentReactionThreshold", "number",
"The standing at the bottom of the current rank."),
Game::Doc::Req("nextReactionThreshold", "number",
"The standing at the top of the current rank."),
Game::Doc::Req("currentStanding", "number"),
Game::Doc::Req("atWarWith", "bool"),
Game::Doc::Req("canToggleAtWar", "bool",
"False below -3000 standing, when the war state is fixed, or with no "
"reputation slot."),
Game::Doc::Req("isChild", "bool", "Always false."),
Game::Doc::Req("isHeader", "bool", "True when the faction is a header in the reputation list."),
Game::Doc::Req("isHeaderWithRep", "bool", "Always false."),
Game::Doc::Req("isCollapsed", "bool", "True for a collapsed header."),
Game::Doc::Req("isWatched", "bool", "True when the faction is shown above the experience bar."),
Game::Doc::Req("hasBonusRepGain", "bool", "Always false."),
Game::Doc::Req("canSetInactive", "bool",
"True when the faction has a reputation slot and is not a header."),
Game::Doc::Req("isAccountWide", "bool", "Always false."),
};
const Game::Doc::Structure kFactionData{"FactionData", "Reputation", kFactionDataFields,
"A faction's reputation data."};
const Game::Doc::Field kFactionIDArgs[] = {
Game::Doc::Req("factionID", "number"),
};
const Game::Doc::Field kSortIndexArgs[] = {
Game::Doc::Req("factionSortIndex", "luaIndex", "A position in the reputation list."),
};
const Game::Doc::Field kFactionIDByIndexArgs[] = {
Game::Doc::Req("factionIndex", "luaIndex", "A position in the reputation list."),
};
const Game::Doc::Field kFactionIDByIndexRets[] = {
Game::Doc::Opt("factionID", "number", nullptr,
"0 for a header with no faction of its own, such as Other or Inactive; nil "
"when the index is out of range."),
};
const Game::Doc::Function kGetFactionIDByIndex{
"Returns the faction ID of the entry at a position in the reputation list.",
kFactionIDByIndexArgs, kFactionIDByIndexRets, "ReputationGlobals", true};
const Game::Doc::Field kFactionInfoByIDRets[] = {
Game::Doc::Opt("name", "string", nullptr, "Nil, with the rest, when no faction has the ID."),
Game::Doc::Opt("description", "string", nullptr, "Empty when the faction has none."),
Game::Doc::Opt("standingID", "number", nullptr, "The rank, from 1 for Hated to 8 for Exalted."),
Game::Doc::Opt("barMin", "number", nullptr, "The standing at the bottom of the current rank."),
Game::Doc::Opt("barMax", "number", nullptr, "The standing at the top of the current rank."),
Game::Doc::Opt("barValue", "number", nullptr, "The current standing."),
Game::Doc::Opt("atWarWith", "number", nullptr,
"1 when the player is at war with the faction, else nil."),
Game::Doc::Opt("canToggleAtWar", "number", nullptr,
"1 when the at-war state can be changed, else nil."),
Game::Doc::Opt("isHeader", "number", nullptr,
"1 when the faction is a header in the reputation list, else nil."),
Game::Doc::Opt("isCollapsed", "number", nullptr, "1 for a collapsed header, else nil."),
Game::Doc::Opt("isWatched", "number", nullptr,
"1 when the faction is shown above the experience bar, else nil."),
};
const Game::Doc::Function kGetFactionInfoByID{
"Returns the same values as GetFactionInfo for a faction ID, whether or not the faction "
"is in the reputation list.",
kFactionIDArgs, kFactionInfoByIDRets, "ReputationGlobals"};
const Game::Doc::Field kFactionParentIDRets[] = {
Game::Doc::Opt("parentFactionID", "number", nullptr,
"0 for a faction with no parent; nil when no faction has the ID."),
};
const Game::Doc::Function kGetFactionParentID{
"Returns the ID of a faction's parent faction, such as Alliance for Stormwind.",
kFactionIDArgs, kFactionParentIDRets, "ReputationGlobals", true};
const Game::Doc::Field kSetWatchedArgs[] = {
Game::Doc::Req("factionID", "number",
"0, or an ID with no reputation slot, clears the watched faction; a negative "
"ID does nothing."),
};
const Game::Doc::Function kSetWatchedFactionByID{
"Sets the faction shown above the experience bar.", kSetWatchedArgs, {}};
const Game::Doc::Field kSetSelectedByIDArgs[] = {
Game::Doc::Req("factionID", "number", "0 clears the selection; a negative ID does nothing."),
};
const Game::Doc::Function kSetSelectedFactionByID{
"Selects a faction in the reputation pane by ID, even when the faction is not listed.",
kSetSelectedByIDArgs, {}, nullptr, true};
const Game::Doc::Function kToggleFactionAtWarByID{
"Toggles whether the player is at war with a faction, by ID; the same limits apply as "
"for C_Reputation.ToggleFactionAtWar.",
kFactionIDArgs, {}, nullptr, true};
const Game::Doc::Field kIsActiveRets[] = {
Game::Doc::Req("isActive", "bool",
"False under the Inactive header, and for anything with no reputation slot."),
};
const Game::Doc::Function kIsFactionActive{
"Returns whether the faction at a position in the reputation list is active, that is, "
"not under the Inactive header.",
kSortIndexArgs, kIsActiveRets};
const Game::Doc::Function kIsFactionActiveByID{
"Returns whether a faction is active, that is, not under the Inactive header.",
kFactionIDArgs, kIsActiveRets, nullptr, true};
const Game::Doc::Field kIsInactiveRets[] = {
Game::Doc::Opt("isInactive", "number", nullptr,
"1 when the faction is under the Inactive header, else nil."),
};
const Game::Doc::Function kIsFactionInactive{
"Returns whether the faction at a position in the reputation list is under the Inactive "
"header, as 1 or nil.",
kSortIndexArgs, kIsInactiveRets, nullptr, true};
const Game::Doc::Function kSetFactionInactiveByID{
"Moves a faction under the Inactive header of the reputation list, by ID.",
kFactionIDArgs, {}, nullptr, true};
const Game::Doc::Function kSetFactionActiveByID{
"Moves a faction out from under the Inactive header of the reputation list, by ID.",
kFactionIDArgs, {}, nullptr, true};
const Game::Doc::Function kSetFactionInactive{
"Moves the faction at a position in the reputation list under the Inactive header.",
kSortIndexArgs, {}, nullptr, true};
const Game::Doc::Function kSetFactionActive{
"Moves the faction at a position in the reputation list out from under the Inactive "
"header.",
kSortIndexArgs, {}};
const Game::Doc::Function kToggleFactionAtWar{
"Toggles whether the player is at war with the faction at a position in the reputation "
"list.",
kSortIndexArgs, {}};
const Game::Doc::Function kSetSelectedFaction{
"Selects the faction at a position in the reputation list.", kSortIndexArgs, {}};
const Game::Doc::Field kWatchedFactionDataRets[] = {
Game::Doc::Opt("watchedFactionData", "FactionData", nullptr,
"Nil when no faction is watched."),
};
const Game::Doc::Function kGetWatchedFactionData{
"Returns the faction shown above the experience bar.", {}, kWatchedFactionDataRets};
const Game::Doc::Field kStandingsRets[] = {
Game::Doc::Req("standings", "table",
"Keyed by faction ID; each value is the faction's standing."),
};
const Game::Doc::Function kGetFactionStandings{
"Returns the standing of every faction the character has reputation with, keyed by "
"faction ID.",
{}, kStandingsRets, nullptr, true};
const Game::Doc::Field kFactionDataByIDRets[] = {
Game::Doc::Opt("factionData", "FactionData", nullptr, "Nil when no faction has the ID."),
};
const Game::Doc::Function kGetFactionDataByID{
"Returns a faction's reputation data by ID, whether or not the faction is in the "
"reputation list.",
kFactionIDArgs, kFactionDataByIDRets};
const Game::Doc::Field kFactionDataByIndexRets[] = {
Game::Doc::Opt("factionData", "FactionData", nullptr,
"Nil when the index is out of range or names the Other or Inactive header."),
};
const Game::Doc::Function kGetFactionDataByIndex{
"Returns the reputation data of the faction at a position in the reputation list.",
kSortIndexArgs, kFactionDataByIndexRets};
static void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("GetFactionIDByIndex", &Script_GetFactionIDByIndex,
&kGetFactionIDByIndex);
Game::Lua::RegisterGlobalFunction("GetFactionInfoByID", &Script_GetFactionInfoByID,
&kGetFactionInfoByID);
Game::Lua::RegisterGlobalFunction("GetFactionParentID", &Script_GetFactionParentID,
&kGetFactionParentID);
Game::Lua::RegisterGlobalFunction("GetFactionIDByIndex", &Script_GetFactionIDByIndex);
Game::Lua::RegisterGlobalFunction("GetFactionInfoByID", &Script_GetFactionInfoByID);
Game::Lua::RegisterGlobalFunction("GetFactionParentID", &Script_GetFactionParentID);
Game::Lua::RegisterTableFunction("C_Reputation", "SetWatchedFactionByID",
&Script_C_Reputation_SetWatchedFactionByID,
&kSetWatchedFactionByID);
&Script_C_Reputation_SetWatchedFactionByID);
Game::Lua::RegisterTableFunction("C_Reputation", "SetSelectedFactionByID",
&Script_C_Reputation_SetSelectedFactionByID,
&kSetSelectedFactionByID);
&Script_C_Reputation_SetSelectedFactionByID);
Game::Lua::RegisterTableFunction("C_Reputation", "ToggleFactionAtWarByID",
&Script_C_Reputation_ToggleFactionAtWarByID,
&kToggleFactionAtWarByID);
&Script_C_Reputation_ToggleFactionAtWarByID);
Game::Lua::RegisterTableFunction("C_Reputation", "IsFactionActive",
&Script_C_Reputation_IsFactionActive, &kIsFactionActive);
&Script_C_Reputation_IsFactionActive);
Game::Lua::RegisterTableFunction("C_Reputation", "IsFactionActiveByID",
&Script_C_Reputation_IsFactionActiveByID,
&kIsFactionActiveByID);
&Script_C_Reputation_IsFactionActiveByID);
Game::Lua::RegisterTableFunction(
"C_Reputation", "IsFactionInactive",
reinterpret_cast<Game::Lua::CFunction>(
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_IS_FACTION_INACTIVE)),
&kIsFactionInactive);
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_IS_FACTION_INACTIVE)));
Game::Lua::RegisterTableFunction("C_Reputation", "SetFactionInactiveByID",
&Script_C_Reputation_SetFactionInactiveByID,
&kSetFactionInactiveByID);
&Script_C_Reputation_SetFactionInactiveByID);
Game::Lua::RegisterTableFunction("C_Reputation", "SetFactionActiveByID",
&Script_C_Reputation_SetFactionActiveByID,
&kSetFactionActiveByID);
&Script_C_Reputation_SetFactionActiveByID);
Game::Lua::RegisterTableFunction(
"C_Reputation", "SetFactionInactive",
reinterpret_cast<Game::Lua::CFunction>(
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_INACTIVE)),
&kSetFactionInactive);
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_INACTIVE)));
Game::Lua::RegisterTableFunction(
"C_Reputation", "SetFactionActive",
reinterpret_cast<Game::Lua::CFunction>(
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_ACTIVE)),
&kSetFactionActive);
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_ACTIVE)));
Game::Lua::RegisterTableFunction(
"C_Reputation", "ToggleFactionAtWar",
reinterpret_cast<Game::Lua::CFunction>(
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_FACTION_TOGGLE_AT_WAR)),
&kToggleFactionAtWar);
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_FACTION_TOGGLE_AT_WAR)));
// The stock global, mirrored into the namespace — the engine's own
// handler already has the Lua C ABI, so it registers as-is.
Game::Lua::RegisterTableFunction(
"C_Reputation", "SetSelectedFaction",
reinterpret_cast<Game::Lua::CFunction>(
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_SELECTED_FACTION)),
&kSetSelectedFaction);
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_SELECTED_FACTION)));
Game::Lua::RegisterTableFunction("C_Reputation", "GetWatchedFactionData",
&Script_C_Reputation_GetWatchedFactionData,
&kGetWatchedFactionData);
&Script_C_Reputation_GetWatchedFactionData);
Game::Lua::RegisterTableFunction("C_Reputation", "GetFactionStandings",
&Script_C_Reputation_GetFactionStandings,
&kGetFactionStandings);
&Script_C_Reputation_GetFactionStandings);
Game::Lua::RegisterTableFunction("C_Reputation", "GetFactionDataByID",
&Script_C_Reputation_GetFactionDataByID,
&kGetFactionDataByID);
&Script_C_Reputation_GetFactionDataByID);
Game::Lua::RegisterTableFunction("C_Reputation", "GetFactionDataByIndex",
&Script_C_Reputation_GetFactionDataByIndex,
&kGetFactionDataByIndex);
&Script_C_Reputation_GetFactionDataByIndex);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+2 -23
View File
@@ -50,15 +50,7 @@ using GetStanding_t = int(__fastcall *)(int factionID);
constexpr const char *kEventName = "FACTION_STANDING_CHANGED";
const Game::Doc::Field kPayload[] = {
Game::Doc::Req("factionID", "number"),
Game::Doc::Req("newStanding", "number", "The faction's standing after the change."),
Game::Doc::Req("repGained", "number", "Negative for a loss."),
};
const Game::Doc::Event kEventDoc{"Reputation", "Fires when the player's reputation changes.",
kPayload};
const Event::Custom::AutoReserve _reserve{kEventName, &kEventDoc};
const Event::Custom::AutoReserve _reserve{kEventName};
// In-flight snapshot of the current rep change. Set on hook entry,
// cleared on hook exit. WoW's main thread is the only one that runs
@@ -75,22 +67,9 @@ int __fastcall Script_C_Reputation_GetLastStandingChange(void *L) {
return 3;
}
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("factionID", "number", nullptr,
"Nil, with the rest, outside a FACTION_STANDING_CHANGED or "
"CHAT_MSG_COMBAT_FACTION_CHANGE handler."),
Game::Doc::Opt("newStanding", "number", nullptr, "The faction's standing after the change."),
Game::Doc::Opt("repGained", "number", nullptr, "Negative for a loss."),
};
const Game::Doc::Function kGetLastStandingChange{
"Returns the reputation change that FACTION_STANDING_CHANGED and "
"CHAT_MSG_COMBAT_FACTION_CHANGE report, while their handlers run.",
{}, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Reputation", "GetLastStandingChange",
&Script_C_Reputation_GetLastStandingChange,
&kGetLastStandingChange);
&Script_C_Reputation_GetLastStandingChange);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+170 -236
View File
@@ -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);
}
+2 -1
View File
@@ -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,
+446
View File
@@ -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
+84
View File
@@ -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
+36
View File
@@ -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