Compare commits

...
18 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
Brues 77b304ae68 feat(smf): RemoveMessagesByPredicate
ScrollingMessageFrame:RemoveMessagesByPredicate(predicate), retail's: the
predicate is called with each message's GetMessageInfo values, oldest
first, and the messages it returns true for are removed; the rest keep
their order. It covers 4.3.4's RemoveMessagesByAccessID / ByExtraData,
whose int fields the retail-shaped records here do not have.

1.12's ring has no compaction of its own; the removal is 4.3.4's
RemoveLines loop over it: every kept record moves forward in ring order to
the first free slot (freeing the text it lands on, nulling the one it
left), the position moves back by the removed count, and the
extra-argument tables move with their records. Then the engine's own
layout (0x00788750) from the record at the kept scroll offset, clamped to
the oldest, and its re-arm (0x00788B80) - a layout at the bottom keeps the
fade state, so 4.3.4 re-arms after - or its Clear (0x007882B0) when
nothing is kept.

The predicate runs for every message before anything moves, through the
engine's error handler; a predicate that errors keeps that message, as
retail's safesecurecall does. GetMessageInfo shares its record lookup and
value push with it.
2026-10-30 02:14:08 -05:00
Brues c965e49117 feat(smf): BackFillMessage
ScrollingMessageFrame:BackFillMessage(text, r, g, b, ...), retail's: the
message becomes the oldest, for rebuilding a frame from history newest to
oldest (Blizzard_CombatLog's refilter adds each entry with it).

The ring's writer 0x00788150 has one slot rule: the one after the newest.
BackFillMessage writes the slot 4.3.4's AddMessage(..., addToTop) picks —
the free one before the oldest, (pos - count + 1) mod max once the count
has grown — and fills the record as the writer does: a Storm copy of the
text, AddMessage's colour (clamped, x255 + 0.5 truncated, alpha 0xFF; the
font's without r, g, b), shown, the frame's two times, id 0, and the extra
arguments kept for GetMessageInfo. Nothing already in the ring moves, so a
scrolled-up frame keeps its view; at the bottom the lines are laid out
again from the newest, as the writer does.

A frame at its line limit drops the message, as retail's CircularBuffer
PushBack does ("Won't overwrite front"); 4.3.4 overwrites the oldest slot
there, which Blizzard_CombatLog never reaches.
2026-10-30 01:45:47 -05:00
Brues ead52e79ce fix(editbox): caret, selection and click rows follow tall inline icons
A multi-line edit box places its caret, selection pieces and a click's
cursor by rows of one height (font height + spacing, pixel-aligned), while
the render moves a line holding an icon taller than the font, and every
line after it, down by the overflow, and the box grows to match. Below the
first tall-icon line the caret and highlight sat above their text, and
OnCursorChanged reported that y, so ScrollingEditBoxMixin's scroll-into-
view stopped short (pfUI chat copy with faction emblems).

4.3.4's CSimpleEditBox has the same fault: UpdateVisibleCursor,
UpdateHighlightArea and ConvertCoordinateToIndex are icon-blind too. So
this goes past 4.3.4, as editbox/CharClass does. EditBox::IconRows co-hooks
the three placers and gives each the render's move, from the new
Text::InlineTexture::LineIconShift over the box's own line table: the
click's y is mapped onto the uniform row before the engine reads it; the
selection piece and caret are re-anchored after, with the engine's own x;
the caret's OnCursorChanged is muted for the original call and fired once
with the corrected y through the engine's forwarder, with the same packing
PlaceCaret uses (verified in the disassembly and the binary's constants).

Also fixes the "caret positioner" misnomer for 0x77DE70 (the selection-
piece placer) in Offsets.h and the docs.
2026-10-30 01:15:01 -05:00
Brues 7cb6cda101 feat(smf): TOP insert mode
SetInsertMode(SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP) now shows the
newest message at the top, older ones below it, as retail's mixin does.

1.12's line layout anchors each line once, when it makes it, through one
SetPoint call (0x00788975). That call is redirected: lines of a TOP frame
take the TOP anchors, every other line keeps the engine's own arguments.
The layout remakes the lines when the frame resizes, so the redirect
covers that too. A mode change re-anchors the existing lines with the
layout's own steps and refreshes the display.

Insert mode moves out of Scroll.cpp into InsertMode.cpp.
2026-10-30 00:20:19 -05:00
Brues 5ab497f2c7 feat(smf): scroll offset, insert mode, display-refreshed callbacks
Retail's ScrollingMessageFrame SetScrollOffset, GetScrollOffset,
GetMaxScrollRange, ScrollByAmount, SetInsertMode / GetInsertMode with the
SCROLLING_MESSAGE_FRAME_INSERT_MODE_* globals, and
AddOnDisplayRefreshedCallback, which ScrollUtil's
InitScrollingMessageFrameWithScrollBar calls.

The offset is the engine's own: SetScrollFromBottom's body (0x00788700)
is 4.3.4's SetScrollOffset step for step, and GetCurrentScroll is
GetScrollOffset under retail's name. The setters clamp to
0..GetNumMessages()-1 first, as retail does; the engine wraps. 1.12's
layout stacks lines bottom-up only, so the insert mode is BOTTOM and
SetInsertMode raises an error for anything else.

The callbacks run after the frame's own update (0x00788460, OnUpdate then
the fader), where retail's OnPostUpdate runs them, for a frame the line
layout, AddMessage or ScrollByAmount marked. The layout hook moves out of
Text::InlineTexture into ScrollingMessageFrame::Layout, which owns it and
fans out to both.
2026-10-29 23:56:24 -05:00
Brues 205d1fd43b feat(smf): ScrollingMessageFrame GetMessageInfo
Retail's smf:GetMessageInfo(index): text, r, g, b, then every argument
AddMessage took after the colour. Index 1 is the oldest message, and an
index outside 1..GetNumMessages() returns nothing.

The text and colour come from the engine's record in the frame's message
ring, so UpdateColorByID's changes read back. The record keeps only an
integer id after the colour, so AddMessage is registered again over the
engine's: Storm's hash links a new method node at the head of its bucket
and the lookup takes the first match. It runs the engine's AddMessage,
then keeps the extra arguments for the record just written, in a
registry table reused per record.
2026-10-29 23:43:45 -05:00
Brues 950c9deb76 feat(death): C_DeathInfo self-resurrect options, SELF_RES_SPELL_CHANGED
C_DeathInfo.GetSelfResurrectOptions, C_DeathInfo.UseSelfResurrectOption
and Enum.SelfResurrectOptionType over what 1.12 already serves through
HasSoulstone and UseSoulstone. Both pick one option the same way:
PLAYER_SELF_RES_SPELL when the server set it, else the first carried
item whose on-use spell is a self-resurrect. The list holds that option
under HasSoulstone's alive gate, and using it calls the engine's
UseSoulstone. Nothing in 1.12 limits a self-resurrection, so canUse is
true, isLimited false and priority 0.

SELF_RES_SPELL_CHANGED is 4.3.4's watcher on PLAYER_SELF_RES_SPELL from
SetActiveMirrorHandlers. 1.12 watches no such field, so the watcher joins
CGPlayer_C::AddActiveWatchers and leaves before its removal, the lifetime
of the engine's own player watchers.
2026-10-29 20:37:40 -05:00
45 changed files with 5296 additions and 1137 deletions
+11 -4
View File
@@ -69,8 +69,9 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Cursor](docs/API.md#cursor) | `CursorHasMacro`, `GetCursorInfo` |
| [Container](docs/API.md#container) | `C_Container.AutoStoreItem`, `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemEquipmentSetInfo`, `C_Container.GetContainerItemQuestInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerFreeSlots`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetBackpackAutosortDisabled`, `C_Container.GetBankAutosortDisabled`, `C_Container.GetItemCooldown`, `C_Container.GetSortBagsRightToLeft`, `C_Container.HasContainerItem`, `C_Container.IsContainerFiltered`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SetBackpackAutosortDisabled`, `C_Container.SetBagPortraitTexture`, `C_Container.SetBankAutosortDisabled`, `C_Container.SetItemSearch`, `C_Container.SetSortBagsRightToLeft`, `C_Container.SortBags`, `C_Container.SortBankBags`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown`, `SetBagPortraitTexture` |
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID`, `C_CreatureInfo.GetRaceInfo`, `C_CreatureInfo.GetClassInfo`, `C_CreatureInfo.GetCreatureFamilyInfo`, `C_CreatureInfo.GetCreatureFamilyIDs`, `C_CreatureInfo.GetFactionInfo`, `C_CreatureInfo.GetCreatureTypeInfo`, `C_CreatureInfo.GetCreatureTypeIDs` |
| [Currency](docs/API.md#currency) | `GetCoinText`, `C_CurrencyInfo.GetCoinText`, `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString` |
| [Currency](docs/API.md#currency) | `GetCoinText`, `C_CurrencyInfo.GetCoinText`, `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString`, `Constants.MoneyFormattingConstants` |
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
| [DeathInfo](docs/API.md#deathinfo) | `C_DeathInfo.GetSelfResurrectOptions`, `C_DeathInfo.UseSelfResurrectOption` |
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.PickupEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` |
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid`, `GetFramesRegisteredForEvent` |
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetAccountExpansionLevel`, `GetClassicExpansionLevel`, `GetExpansionLevel`, `GetMaxPlayerLevel` |
@@ -85,7 +86,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Frame: StatusBar](docs/API.md#statusbarsetreversefillisreversefill--statusbargetreversefill) | `:SetReverseFill` / `:GetReverseFill`, `:SetRotatesTexture` / `:GetRotatesTexture`, `:SetFillStyle` / `:GetFillStyle`, `:SetColorFill`, `:SetStatusBarDesaturated` / `:IsStatusBarDesaturated`, `:SetStatusBarDesaturation` / `:GetStatusBarDesaturation` |
| [Frame: Slider](docs/API.md#sliderenable--sliderdisable--sliderisenabled) | `:Enable` / `:Disable` / `:IsEnabled` |
| [Frame: Cooldown](docs/API.md#createframecooldown--name-parent-template) | `CreateFrame("Cooldown")`, `:SetCooldown`, `:SetCooldownDuration`, `:SetCooldownFromExpirationTime`, `:Clear`, `:GetCooldownTimes`, `:GetCooldownDuration`, `:GetCooldownDisplayDuration`, `:Pause` / `:Resume`, `:SetPaused` / `:IsPaused`, `:SetReverse` / `:GetReverse`, `:SetDrawEdge` / `:GetDrawEdge`, `:SetDrawSwipe` / `:GetDrawSwipe`, `:SetDrawBling` / `:GetDrawBling`, `:SetSwipeColor`, `:SetEdgeColor`, `:SetSwipeTexture`, `:SetEdgeTexture`, `:SetBlingTexture`, `:SetRotation` / `:GetRotation`, `:SetHideCountdownNumbers` / `:GetHideCountdownNumbers`, `:GetCountdownFontString`, `:SetCountdownAbbrevThreshold` / `:GetCountdownAbbrevThreshold`, `:SetCountdownMillisecondsThreshold` / `:GetCountdownMillisecondsThreshold`, `:SetMinimumCountdownDuration` / `:GetMinimumCountdownDuration`, `OnCooldownDone`, `CooldownFrame_Set`, `CooldownFrame_Clear`, `CooldownFrame_SetDisplayAsPercentage` |
| [Frame: ScrollingMessageFrame](docs/API.md#smfsethyperlinksenabledenabled--smfgethyperlinksenabled) | `:SetHyperlinksEnabled` / `:GetHyperlinksEnabled` |
| [Frame: ScrollingMessageFrame](docs/API.md#smfsethyperlinksenabledenabled--smfgethyperlinksenabled) | `:SetHyperlinksEnabled` / `:GetHyperlinksEnabled`, `:GetMessageInfo` (with the extra `:AddMessage` values), `:BackFillMessage` (adds a message as the oldest), `:RemoveMessagesByPredicate`, `:SetScrollOffset` / `:GetScrollOffset`, `:GetMaxScrollRange`, `:ScrollByAmount`, `:SetInsertMode` / `:GetInsertMode`, `:AddOnDisplayRefreshedCallback` |
| [Frame: scripts](docs/API.md#onenable-and-ondisable-scripts) | `OnEnable`, `OnDisable`, `OnMinMaxChanged`, `OnAttributeChanged`, `OnCharComposition`, `OnTextChanged` (`userInput`), `PreClick`, `PostClick`, `SetModernScriptArgs` / `GetModernScriptArgs` |
| [FriendList](docs/API.md#friendlist) | `AddOrRemoveFriend`, `C_FriendList.GetFriendInfo`, `C_FriendList.GetFriendInfoByIndex`, `C_FriendList.GetNumFriends`, `C_FriendList.GetNumOnlineFriends`, `C_FriendList.GetNumWhoResults`, `C_FriendList.GetWhoInfo`, `C_FriendList.IsFriend`, `C_FriendList.IsIgnored`, `C_FriendList.IsIgnoredByGuid`, `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName`, `C_FriendList.SetFriendNotes`, `C_FriendList.SetFriendNotesByIndex` |
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID`, `ClosestGameObjectPosition` |
@@ -99,6 +100,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Item: info](docs/API.md#getitemclassinfoclassid) | `GetItemClassInfo`, `GetItemSubClassInfo`, `GetItemIcon`, `IsConsumableItem`, `IsCurrentItem`, `IsDressableItem`, `IsEquippedItemType`, `IsHarmfulItem`, `IsHelpfulItem`, `IsUsableItem` |
| [Item: inventory](docs/API.md#getaverageitemlevel) | `GetAverageItemLevel`, `GetInventoryItemID`, `GetInventoryItemDurability`, `GetInventoryItemRepairCost`, `GetInventoryItemsForSlot`, `IsDualWielding`, `SetInventoryPortraitTexture` |
| [Item: item IDs](docs/API.md#getitemid--companions-to-the-engines-getitemlink-family) | `GetAuctionItemID`, `GetAuctionSellItemID`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeTargetItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID` |
| [Localization](docs/API.md#localization) | `AbbreviateLargeNumbers`, `AbbreviateNumbers`, `BreakUpLargeNumbers`, `C_StringUtil.GetDefaultAbbreviationBreakpoints` |
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootAllCorpses`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount`, `C_LossOfControl.GetSchoolLockout` |
@@ -232,7 +234,9 @@ menu and click AddOn Options, then ClassicAPI.
| | `SoftTargetIconEnemy` / `SoftTargetIconFriend` | `0` | `1` shows an icon over `softenemy` / `softfriend`. |
| | `SoftTargetLowPriorityIcons` | `0` | `1` also shows the normal pointer as an icon. |
| | `SoftTargetNameplateSize` | `19` | Size of all soft-target icons. |
| [Localization](docs/API.md#breakuplargenumbers-cvar) | `breakUpLargeNumbers` | `1` | `1` puts a separator between each group of three digits in `BreakUpLargeNumbers`. `0` writes no separator. |
| [Cooldown](docs/API.md#createframecooldown--name-parent-template) | `countdownForCooldowns` | `0` | `1` shows the time left as numbers on cooldown frames. |
| `ScriptAnimationUtil` | `ShakeStrengthUI` | `1` | Scales how far `ScriptAnimationUtil.ShakeFrame` moves a frame. `0` turns the shake off. |
| [NamePlate](docs/API.md#nameplate) | `nameplateNotSelectedAlpha` | `0.5` | Alpha of a non-target default nameplate, `0` to `1`. |
| | `nameplateSelectedAlpha` | `1` | Alpha of the target's default nameplate, `0` to `1`. |
| [Text to speech](docs/API.md#tts-cvars) | `ttsVoice` | `0` | The voice that `C_VoiceChat.SpeakText` uses. |
@@ -313,6 +317,7 @@ when launching with `-console`), not as Lua functions. See the
| `QUEST_QUERY_COMPLETE` | *(none)* (Turtle WoW only) |
| `QUEST_REMOVED` | `questID` |
| `QUEST_TURNED_IN` | `questID, xpReward, moneyReward` |
| `SELF_RES_SPELL_CHANGED` | *(none)* |
| `SOUNDKIT_FINISHED` | `soundHandle` (only for sounds played with `runFinishCallback`) |
| `UNIT_AURA_DELTA` | `unitTarget, updateInfo` (the instance IDs of the auras added, updated, and removed) |
| `UNIT_SPELLCAST_SENT` | `"player", target, castGUID, spellID, spellName, rank` |
@@ -360,6 +365,7 @@ when launching with `-console`), not as Lua functions. See the
| Spellbook bank | `Enum.SpellBookSpellBank.{Player,Pet}` (0–1) |
| Spellbook item type | `Enum.SpellBookItemType.{None,Spell,FutureSpell,PetAction,Flyout}` (0–4; 1.12 only yields `Spell`/`PetAction`) |
| Player swing type | `Enum.PlayerSwingType.{MainHand,OffHand,Ranged}` (0–2) |
| Self-resurrect option type | `Enum.SelfResurrectOptionType.{Spell,Item}` (0–1) |
</details>
@@ -429,8 +435,9 @@ functions but that consumer code still expects to find as globals —
`CallbackRegistryMixin`, `EventRegistry`, `ColorMixin` + `CreateColor`,
`Item` / `ItemLocation`, `MathUtil`(`Lerp` / `Clamp` / `CreateCounter`),
`TableUtil` (`tCompare`, `MergeTable`, `SafePack`, etc.), `EventUtil`
(`ContinueOnAddOnLoaded` and more), `DataProvider`, `CircularBuffer`,
`EasingUtil`, `Interpolator`, `EnumUtil`, `StringUtil`, `AnchorUtil`,
(`ContinueOnAddOnLoaded` and more), `FormattingUtil` (`GetMoneyString`,
`FormatLargeNumber` and more), `FrameUtil`, `DataProvider`, `CircularBuffer`,
`EasingUtil`, `ScriptAnimationUtil`, `Interpolator`, `EnumUtil`, `StringUtil`, `AnchorUtil`,
`GridLayoutUtil`, and the layout frame templates (`VerticalLayoutFrame`,
`HorizontalLayoutFrame`, `ResizeLayoutFrame`, `GridLayoutFrame`,
`StaticGridLayoutFrame`). It also holds the conditional slash
+16 -3
View File
@@ -4081,9 +4081,22 @@ Each entry still needs the usual check of how the engine does it before building
`GetRotatesTexture` (`CSimpleStatusBar.cpp:135`, the vertical texcoord path
at `:285`). Retail adds `SetFillStyle` (STANDARD / CENTER / REVERSE),
`SetColorFill`, `SetStatusBarDesaturated` / `Desaturation`.
- **ScrollingMessageFrame** `SetInsertMode` / `GetInsertMode`,
`GetMessageInfo`, `RemoveMessagesByAccessID`, `RemoveMessagesByExtraData`,
`SetScrollOffset` — in C in 4.3.4 (retail moved the type to Lua).
- **DONE** (`src/scrollingmessageframe/`, retail's shapes).
**ScrollingMessageFrame** `GetMessageInfo`, `SetScrollOffset` (1.12's
`SetScrollFromBottom`, 0x00793070, under its later name, clamped as
retail), `GetScrollOffset`, `GetMaxScrollRange`, `ScrollByAmount`,
`AddOnDisplayRefreshedCallback`, `SetInsertMode` / `GetInsertMode` (TOP
through a redirect of the layout's one line-anchor call, 0x00788975),
`BackFillMessage` (writes 4.3.4's addToTop slot of the ring, the free one
before the oldest; a full frame drops it, as retail's CircularBuffer
`PushBack` does — 4.3.4 overwrites the oldest there).
- **DONE** (`src/scrollingmessageframe/MessageInfo.cpp`).
**ScrollingMessageFrame** `RemoveMessagesByPredicate` (retail's shape; it
covers 4.3.4's `RemoveMessagesByAccessID` / `RemoveMessagesByExtraData`,
whose int fields our retail-shaped records do not have). The compaction
is 4.3.4's `RemoveLines` loop over 1.12's ring, then the engine's own
layout (0x00788750) at the kept scroll offset and re-arm (0x00788B80),
or Clear (0x007882B0) when nothing is kept.
- **DONE** (`src/editbox/Focus.cpp`, `src/animation/Framerate.cpp`).
**Globals** (`SimpleFrameScript_Scripts.cpp`): `GetCurrentKeyBoardFocus()`
(the focused edit box's wrapper, or nil); `SetMaxAnimFramerate(fps)` /
+402 -2
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)
@@ -182,6 +183,10 @@ build instructions.
- [`GetCursorInfo()`](#getcursorinfo)
- [`CursorHasMacro()`](#cursorhasmacro)
- [DeathInfo](#deathinfo)
- [`C_DeathInfo.GetSelfResurrectOptions()`](#c_deathinfogetselfresurrectoptions)
- [`C_DeathInfo.UseSelfResurrectOption(optionType, id)`](#c_deathinfouseselfresurrectoptionoptiontype-id)
- [EquipmentSet](#equipmentset)
- [Overview & file format](#overview--file-format)
- [`C_EquipmentSet.CanUseEquipmentSets()`](#c_equipmentsetcanuseequipmentsets)
@@ -241,6 +246,7 @@ build instructions.
- [`QUEST_QUERY_COMPLETE` event](#quest_query_complete-event)
- [`QUEST_REMOVED` event](#quest_removed-event)
- [`QUEST_TURNED_IN` event](#quest_turned_in-event)
- [`SELF_RES_SPELL_CHANGED` event](#self_res_spell_changed-event)
- [`SOUNDKIT_FINISHED` event](#soundkit_finished-event)
- [`UNIT_AURA_DELTA` event](#unit_aura_delta-event)
- [`UNIT_FACTION` event (fire-coverage fix)](#unit_faction-event-fire-coverage-fix)
@@ -380,6 +386,12 @@ build instructions.
- [`frame:HookScript(scriptType, handler)`](#framehookscriptscripttype-handler)
- [Script binding types](#script-binding-types)
- [`smf:SetHyperlinksEnabled(enabled)` / `smf:GetHyperlinksEnabled()`](#smfsethyperlinksenabledenabled--smfgethyperlinksenabled)
- [`smf:GetMessageInfo(index)`](#smfgetmessageinfoindex)
- [`smf:BackFillMessage(text, r, g, b, ...)`](#smfbackfillmessagetext-r-g-b-)
- [`smf:RemoveMessagesByPredicate(predicate)`](#smfremovemessagesbypredicatepredicate)
- [`smf:SetScrollOffset(offset)` / `smf:GetScrollOffset()` / `smf:GetMaxScrollRange()` / `smf:ScrollByAmount(amount)`](#smfsetscrolloffsetoffset--smfgetscrolloffset--smfgetmaxscrollrange--smfscrollbyamountamount)
- [`smf:SetInsertMode(mode)` / `smf:GetInsertMode()`](#smfsetinsertmodemode--smfgetinsertmode)
- [`smf:AddOnDisplayRefreshedCallback(callback)`](#smfaddondisplayrefreshedcallbackcallback)
- [`frame:IsEventRegistered(event)`](#frameiseventregisteredevent)
- [`frame:RegisterUnitEvent(event, ...units)`](#frameregisteruniteventevent-units)
- [`frame:GetEffectiveAlpha()`](#framegeteffectivealpha)
@@ -481,7 +493,7 @@ build instructions.
- [Hooks](#hooks)
- [`hooksecurefunc(name, callback)` / `hooksecurefunc(table, name, callback)`](#hooksecurefuncname-callback--hooksecurefunctable-name-callback)
- [`securecall(func, ...)` / `securecallfunction(func, ...)` / `secureexecuterange(tbl, func, ...)`](#securecallfunc---securecallfunctionfunc---secureexecuterangetbl-func-)
- [`securecall(func, ...)` / `securecallfunction(func, ...)` / `securecallmethod(object, method, ...)` / `secureexecuterange(tbl, func, ...)`](#securecallfunc---securecallfunctionfunc---securecallmethodobject-method---secureexecuterangetbl-func-)
- [Input](#input)
- [`IsLeftShiftKeyDown()` / `IsRightShiftKeyDown()`](#isleftshiftkeydown--isrightshiftkeydown)
@@ -569,6 +581,13 @@ build instructions.
- [`-config <name>` (launch switch)](#-config-name-launch-switch)
- [`-gluescript` / `-gluescriptFile` / `-gamescript` / `-gamescriptFile` (launch switches)](#-gluescript---gluescriptfile---gamescript---gamescriptfile-launch-switches)
- [Localization](#localization)
- [`BreakUpLargeNumbers(largeNumber [, natural])`](#breakuplargenumberslargenumber--natural)
- [`AbbreviateNumbers(number [, options])`](#abbreviatenumbersnumber--options)
- [`AbbreviateLargeNumbers(number [, options])`](#abbreviatelargenumbersnumber--options)
- [`C_StringUtil.GetDefaultAbbreviationBreakpoints([locale])`](#c_stringutilgetdefaultabbreviationbreakpointslocale)
- [`breakUpLargeNumbers` (CVar)](#breakuplargenumbers-cvar)
- [Loot](#loot)
- [`C_Loot.GetNearbyLootableUnits()`](#c_lootgetnearbylootableunits)
- [`C_Loot.LootUnit(guid)`](#c_lootlootunitguid)
@@ -4031,6 +4050,19 @@ The per-denomination formats are the GlobalStrings
`COPPER_AMOUNT_TEXTURE`, backported in the embedded `!!!ClassicAPI`
addon's locale layer — a locale can override them.
### `Constants.MoneyFormattingConstants`
A table of constants for money text.
| Field | Value | Description |
|-------|-------|-------------|
| `GOLD_REWARD_THRESHOLD_TO_HIDE_COPPER` | `10` | With `checkGoldThreshold`, `GetMoneyString` shows no copper for an amount of more than this number of gold. |
```lua
GetMoneyString(1234567, nil, true) -- "123g 45s" with coin icons, no copper
GetMoneyString(51234, nil, true) -- "5g 12s 34c" with coin icons
```
## CVar
### `GetCVar(name)`
@@ -4359,6 +4391,57 @@ Changes to what the cursor holds are reported by the
Returns `true` if the cursor holds a macro, and `false` if it does not.
## DeathInfo
### `C_DeathInfo.GetSelfResurrectOptions()`
```
options = C_DeathInfo.GetSelfResurrectOptions()
```
Returns an array of the ways that the dead player can resurrect themselves.
Each entry is a `SelfResurrectOption` table:
| Field | Type | Description |
|---|---|---|
| `name` | string | The name of the spell or the item. |
| `optionType` | number | `Enum.SelfResurrectOptionType.Spell` (0) or `Enum.SelfResurrectOptionType.Item` (1). |
| `id` | number | The spell ID or the item ID. |
| `canUse` | boolean | Always `true`. |
| `isLimited` | boolean | Always `false`. |
| `priority` | number | Always `0`. |
The array has one entry at most. A self-resurrection spell, such as
Soulstone Resurrection or Reincarnation, comes before an item. The array is
empty when the player is alive or a ghost, or has no option. The function
returns nothing before the player enters the world.
```lua
local options = C_DeathInfo.GetSelfResurrectOptions()
if options and options[1] then
print("You can resurrect with", options[1].name)
end
```
[`SELF_RES_SPELL_CHANGED`](#self_res_spell_changed-event) fires when the
self-resurrection spell changes.
### `C_DeathInfo.UseSelfResurrectOption(optionType, id)`
Resurrects the dead player with an option from
[`C_DeathInfo.GetSelfResurrectOptions`](#c_deathinfogetselfresurrectoptions).
`optionType` and `id` must match an entry that it returns now. If they do
not match, the call does nothing. An item option uses the item, and this
stops a spell cast in progress.
```lua
local options = C_DeathInfo.GetSelfResurrectOptions()
local option = options and options[1]
if option then
C_DeathInfo.UseSelfResurrectOption(option.optionType, option.id)
end
```
## EquipmentSet
`C_EquipmentSet.*` — named sets of gear you can save and equip again
@@ -5783,6 +5866,14 @@ quest invalidated, etc.). Only fires on a real successful turn-in.
> done, quest removed from log). Hooking the packet handler gives
> us a clean turn-in signal.
### `SELF_RES_SPELL_CHANGED` event
Fires with no payload when the self-resurrection spell of the player
changes. For example, it fires when the player dies with a Soulstone, and
again when the player comes back to life. When it fires, read
[`C_DeathInfo.GetSelfResurrectOptions`](#c_deathinfogetselfresurrectoptions)
again. A change to an item option does not fire this event.
### `SOUNDKIT_FINISHED` event
Fires when a sound finishes playing.
@@ -8599,6 +8690,151 @@ ChatFrame1:SetHyperlinksEnabled(false) -- links in this chat window stop react
ChatFrame1:GetHyperlinksEnabled() -- false
```
### `smf:GetMessageInfo(index)`
On ScrollingMessageFrame. Returns one message of the frame: its text, its
color, and the extra values that `AddMessage` got for the message. Index 1 is
the oldest message, and `smf:GetNumMessages()` is the index of the newest
message. If no message has the index, the function returns nothing. If
`index` is not a number, the function gives an error.
- `text` is the text that the frame keeps, with its color codes.
- `r`, `g` and `b` are the color that the frame shows for the message. After
`UpdateColorByID` changes the color, these values also change. The frame
keeps each color value in steps of 1/255, so `0.5` returns as `0.50196…`.
- `smf:AddMessage(text, r, g, b, ...)` keeps all the values after `b`.
`GetMessageInfo` returns these values in the same order, nil values
included.
An addon that replaces `AddMessage` and gives only `text, r, g, b` to the
original function removes the extra values.
```lua
ChatFrame1:AddMessage("hello", 1, 0.5, 0, 7, "extra")
ChatFrame1:GetMessageInfo(ChatFrame1:GetNumMessages())
-- "hello", 1, 0.50196078431373, 0, 7, "extra"
for i = 1, ChatFrame1:GetNumMessages() do
local text, r, g, b = ChatFrame1:GetMessageInfo(i) -- oldest message first
end
```
### `smf:BackFillMessage(text, r, g, b, ...)`
On ScrollingMessageFrame. Adds a message as the oldest message of the frame.
`AddMessage` adds the newest message. `BackFillMessage` adds history behind
the messages that the frame already has. An addon that rebuilds a frame from
a history walks the history from the newest entry to the oldest and adds each
entry with this function. The messages that the frame already has do not
move.
- `text`, `r`, `g`, `b` and the extra values work as in `AddMessage`.
Without `r`, `g` and `b`, the message takes the color of the frame's font.
`GetMessageInfo` returns the extra values after the color.
- If the frame is at its line limit (`SetMaxLines`), the function drops the
message, because the message is older than each message that the frame
keeps.
- If the frame is scrolled to the bottom, the frame shows the message when
there is room above its other lines. If the frame is scrolled up, the frame
keeps its view.
```lua
ChatFrame1:BackFillMessage("older", 1, 1, 1)
ChatFrame1:GetMessageInfo(1) -- "older", 1, 1, 1
```
### `smf:RemoveMessagesByPredicate(predicate)`
On ScrollingMessageFrame. Removes the messages that `predicate` selects. The
function calls `predicate(text, r, g, b, ...)` for each message, from the
oldest to the newest, with the values that `GetMessageInfo` returns for the
message. If `predicate` returns true, the function removes the message. The
other messages keep their order. If `predicate` is not a function, the
function gives an error.
- The function calls `predicate` for all messages before it removes any
message. If `predicate` gives an error for a message, the frame keeps that
message.
- The frame keeps its scroll offset (`GetScrollOffset`). If fewer messages
remain than the offset, the offset becomes the oldest message.
- The frame shows the lines that remain at once. If no message remains, the
result is the same as `smf:Clear()`.
```lua
-- remove the messages that were added with the extra value 7
ChatFrame1:RemoveMessagesByPredicate(function(text, r, g, b, id)
return id == 7
end)
```
### `smf:SetScrollOffset(offset)` / `smf:GetScrollOffset()` / `smf:GetMaxScrollRange()` / `smf:ScrollByAmount(amount)`
On ScrollingMessageFrame. The scroll offset is the number of lines that the
frame is scrolled away from the newest message. When the frame is not
scrolled, the offset is 0. The largest offset is `GetMaxScrollRange()`, that
is `GetNumMessages() - 1`, or 0 for an empty frame. At that offset, the oldest
message is the bottom line, or the top line in TOP insert mode.
- `SetScrollOffset(offset)` scrolls to `offset`. An offset less than 0 or
more than `GetMaxScrollRange()` changes to the nearest limit.
- `ScrollByAmount(amount)` adds `amount` to the offset. A positive amount
scrolls to older messages. The function also shows the faded lines
again, also when the offset does not change.
- If `offset` or `amount` is not a number, the function gives an error.
```lua
ChatFrame1:ScrollByAmount(3) -- 3 lines up
ChatFrame1:GetScrollOffset() -- 3
ChatFrame1:SetScrollOffset(ChatFrame1:GetMaxScrollRange()) -- the oldest message
ChatFrame1:SetScrollOffset(0) -- the newest message again
```
### `smf:SetInsertMode(mode)` / `smf:GetInsertMode()`
On ScrollingMessageFrame. The insert mode tells where the newest message
shows.
- `SCROLLING_MESSAGE_FRAME_INSERT_MODE_BOTTOM` (2) shows the newest message
at the bottom, with older messages above it. This is the default.
- `SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP` (1) shows the newest message at
the top, with older messages below it.
In both modes, the scroll offset counts lines from the newest message. A
change of mode moves the lines that show and runs the display-refreshed
callbacks. If `mode` is the current mode, `SetInsertMode` does nothing. If
`mode` is not one of the two values, the function gives an error.
```lua
local f = CreateFrame("ScrollingMessageFrame", nil, UIParent)
f:SetInsertMode(SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP)
f:AddMessage("first")
f:AddMessage("second") -- shows above "first"
f:GetInsertMode() -- 1
```
### `smf:AddOnDisplayRefreshedCallback(callback)`
On ScrollingMessageFrame. Adds a function that the frame calls as
`callback(frame)` after it refreshes the messages that it shows. The frame
refreshes after a new message, a scroll, a resize, a font change, or
`Clear()`.
- The callbacks run one time, in the next update of the frame, in the order
that you added them.
- A hidden frame runs its callbacks when it shows again.
- An error in one callback goes to the error handler, and the other callbacks
still run.
- A change that a callback makes to the frame does not run the callbacks
again.
- If `callback` is not a function, the method gives an error. You cannot
remove a callback.
```lua
ChatFrame1:AddOnDisplayRefreshedCallback(function(frame)
MyScrollLabel:SetText(frame:GetScrollOffset() .. " / " .. frame:GetMaxScrollRange())
end)
```
### `frame:IsEventRegistered(event)`
Returns `true` if the frame is registered for `event`, else `false` (also
@@ -11030,7 +11266,7 @@ but they only report "any shift/ctrl/alt" — there's no built-in way to
tell left from right. These seven functions add the missing distinction
plus an `IsModifierKeyDown()` rollup.
### `securecall(func, ...)` / `securecallfunction(func, ...)` / `secureexecuterange(tbl, func, ...)`
### `securecall(func, ...)` / `securecallfunction(func, ...)` / `securecallmethod(object, method, ...)` / `secureexecuterange(tbl, func, ...)`
Call a function so that an error in it does not stop the code that called
it. The error goes to the error handler (`geterrorhandler()`).
@@ -11040,6 +11276,10 @@ it. The error goes to the error handler (`geterrorhandler()`).
`securecall` returns nothing.
- `securecallfunction(func, ...)` is the same, but `func` must be a
function.
- `securecallmethod(object, method, ...)` calls
`object[method](object, ...)` and returns its results. The lookup of
`method` ignores the metatable of `object`. If `object` is not a table,
or `method` is not a function, the error goes to the error handler.
- `secureexecuterange(tbl, func, ...)` calls `func(key, value, ...)` for
each entry of `tbl`. After an error in one call, the other entries still
run. It returns nothing.
@@ -13509,6 +13749,166 @@ first, then the file.
is reported the same way any other script error is. It does not stop the
client, and it does not stop the other scripts.
## Localization
Functions that write numbers as text, with the separators and abbreviations
of the client language.
### `BreakUpLargeNumbers(largeNumber [, natural])`
Returns the number as text, with a separator between each group of three
digits.
```lua
BreakUpLargeNumbers(1234567) -- "1,234,567"
BreakUpLargeNumbers(1234567.891) -- "1,234,567"
BreakUpLargeNumbers(123.456) -- "123.5"
BreakUpLargeNumbers(5) -- "5"
BreakUpLargeNumbers(-1234567) -- "-1,234,567"
BreakUpLargeNumbers(12345678, true) -- "12.3 million"
BreakUpLargeNumbers(123456, true) -- "123,456"
```
| Arg | Type | Description |
|-----|------|-------------|
| `largeNumber` | number | The number to write. |
| `natural` | boolean, optional | `true` writes a number of one million or more with a word, for example `"1.2 million"`. The default is `false`. |
A whole number, or a number of 1000 or more, loses its decimals. The
function removes them and does not round. Any other number keeps one
decimal, rounded to the nearest tenth. A half rounds away from zero: `0.25`
becomes `"0.3"`. A number with a decimal has no separator, so `999.96`
becomes `"1000.0"`.
The separator is the global string `LARGE_NUMBER_SEPERATOR`, and the decimal
point is `DECIMAL_SEPERATOR`. When the
[`breakUpLargeNumbers`](#breakuplargenumbers-cvar) CVar is `0`, the function
writes the digits with no separator.
With `natural`, a positive number of one million or more gets the largest
word that fits: million, billion or trillion. The number before the word
keeps one decimal when it is less than 100. The function removes the other
decimals and does not round, and it does not show a decimal of `.0`:
`2000000` becomes `"2 million"`. Smaller numbers and negative numbers are
whole numbers with separators.
The words are the global strings `SECOND_NUMBER`, `THIRD_NUMBER` and
`FOURTH_NUMBER`. On Chinese and Korean clients, these words count in steps
of ten thousand, so the words start at 10,000. On German, Spanish and French
clients, the words contain a `|7` plural code, which shows as written.
### `AbbreviateNumbers(number [, options])`
Returns a short form of the number, for example `"1.2K"` for 1234.
```lua
AbbreviateNumbers(999.9) -- "999.9"
AbbreviateNumbers(1234) -- "1.2K"
AbbreviateNumbers(12345) -- "12K"
AbbreviateNumbers(1999999) -- "1.9M"
AbbreviateNumbers(-12345) -- "-12345"
AbbreviateNumbers(1234, { breakpointData = {
{ breakpoint = 100, abbreviation = "h", significandDivisor = 10,
fractionDivisor = 10, abbreviationIsGlobal = false },
} }) -- "12.3h"
```
| Arg | Type | Description |
|-----|------|-------------|
| `number` | number | The number to shorten. |
| `options` | table, optional | A `NumberAbbrevOptions` table. See below. |
The function uses a list of breakpoints, from the largest to the smallest.
It uses the first breakpoint that the number reaches. The result is
`floor(number / significandDivisor) / fractionDivisor`, followed by the
abbreviation. Because the function rounds down, `1999999` becomes `"1.9M"`.
When the number is less than all breakpoints, the function returns the
number as Lua writes it. This is always true for a negative number.
The default breakpoints give one decimal below ten of a unit, and no decimal
from ten up. The units are thousands (`K`), millions (`M`), billions and
trillions. The abbreviations for billions and trillions name global strings
that do not exist, so these numbers have no letter:
`AbbreviateNumbers(1.5e10)` returns `"15"`. On Chinese and Korean clients,
the default units are ten thousand and one hundred million.
`NumberAbbrevOptions` fields:
| Field | Type | Description |
|-------|------|-------------|
| `breakpointData` | table, optional | A list of `NumberAbbreviationBreakpoint` tables, from the largest breakpoint to the smallest. The list replaces the default breakpoints. |
| `locale` | string, optional | The language of the default breakpoints. `"koKR"`, `"zhCN"` and `"zhTW"` give the Chinese and Korean breakpoints. The default is the language of the client. |
A `config` field has no effect.
`NumberAbbreviationBreakpoint` fields:
| Field | Type | Description |
|-------|------|-------------|
| `breakpoint` | number | The smallest number for this entry. |
| `abbreviation` | string | The name of the global string that comes after the number. |
| `significandDivisor` | number | The function divides the number by this value and rounds down. |
| `fractionDivisor` | number | The function then divides by this value, which gives the decimals. |
| `abbreviationIsGlobal` | boolean, optional | `false` puts the `abbreviation` text itself after the number. The default is `true`. |
When an abbreviation names a global string that does not exist, nothing
comes after the number. When an entry of `breakpointData` is not a table, or
a field has the wrong type, the function raises an error.
### `AbbreviateLargeNumbers(number [, options])`
Returns a short form of a large number, in whole thousands or millions.
```lua
AbbreviateLargeNumbers(12345) -- "12,345"
AbbreviateLargeNumbers(123456) -- "123 K"
AbbreviateLargeNumbers(12345678) -- "12345 K"
AbbreviateLargeNumbers(123456789) -- "123 M"
AbbreviateLargeNumbers(-12345) -- "-12,345"
```
The arguments are the same as for
[`AbbreviateNumbers`](#abbreviatenumbersnumber--options), and the function
uses breakpoints in the same way. The default breakpoints start at 100,000
for thousands (`FIRST_NUMBER_CAP`, `" K"`) and at 100,000,000 for millions
(`SECOND_NUMBER_CAP`, `" M"`). When the number is less than all breakpoints,
the result is `BreakUpLargeNumbers(number)`.
The function rounds the result to the nearest whole number. With the
breakpoint from the `AbbreviateNumbers` example, `123456` becomes `"1235h"`.
On Chinese and Korean clients, or with one of their `locale` values, the
result keeps its decimals. There, the default breakpoints use ten thousand
and one hundred million: `123456789` becomes `1.23` and the
`SECOND_NUMBER_CAP` string.
### `C_StringUtil.GetDefaultAbbreviationBreakpoints([locale])`
Returns a new list of the default breakpoints of `AbbreviateNumbers`, as
`NumberAbbreviationBreakpoint` tables. You can change the list and give it
as `breakpointData`.
| Arg | Type | Description |
|-----|------|-------------|
| `locale` | string, optional | The language, for example `"zhCN"`. The default is the language of the client. |
```lua
local breakpoints = C_StringUtil.GetDefaultAbbreviationBreakpoints()
-- breakpoints[1] = { breakpoint = 10000000000000,
-- abbreviation = "FOURTH_NUMBER_CAP_NO_SPACE",
-- significandDivisor = 1000000000000,
-- fractionDivisor = 1, abbreviationIsGlobal = true }
```
### `breakUpLargeNumbers` (CVar)
A setting for `BreakUpLargeNumbers` and the functions that use it. `1` (the
default) puts a separator between each group of three digits. `0` writes the
digits with no separator.
```
/console breakUpLargeNumbers 0
```
## Loot
Programmatic loot operations that the stock Lua surface doesn't
+22 -4
View File
@@ -86,9 +86,11 @@ The DLL hooks the engine's own text pipeline — no companion addon:
(`49934b0`, `b9d0705`).
- **Chat fade** — icons fade in lockstep with their chat line (`3ca3b7e`).
- **Edit boxes** — render the icons in their text, as 4.3.4 does; the caret,
the mouse and the arrow keys treat an icon as one character, and Ctrl+C
drops icons (4.3.4's 0x1500 strip mask). Typed markup stays literal by the
engine's own input escape (below).
the mouse and the arrow keys treat an icon as one character, Ctrl+C
drops icons (4.3.4's 0x1500 strip mask), and a multi-line box's caret,
selection and click rows follow the lines a tall icon moves (below;
4.3.4's own rows do not). Typed markup stays literal by the engine's own
input escape (below).
- **Payload** — the full
`|Tpath:height:width:offsetX:offsetY:texW:texH:left:right:top:bottom:r:g:b|t`:
size, pen offset, sprite-sheet crop, and vertex tint. BLP and uncompressed
@@ -377,7 +379,7 @@ funnel for exactly 4 callers:
- `Script_GetStringWidth` (0x0079E510)
- the GameTooltip auto-size (0x00530640)
- the editbox caret positioner (0x0077DE70)
- the editbox selection-piece placer (0x0077DE70, a whole last line's width)
- the layout effective-width vmethod (`FUN_00772930` = fs+0x24 vtbl +0x1C,
which feeds auto-width fontstring layout)
@@ -523,6 +525,22 @@ the same input escape, so ClassicAPI's edit boxes need no special case:
icon with the following character into one step; ours is its own step.
- **Caret x** — `FUN_0077DA80` measures the line through
`FUN_00772AE0`, whose co-hook adds the icons.
- **Rows (multi-line)** — `PlaceCaret` (`0x77da80`), `PlaceSelectionPiece`
(`0x77de70`) and `GetIndexAtPoint` (`0x77d0d0`) step rows of one height
(font height + spacing, pixel-aligned), as 4.3.4's `UpdateVisibleCursor`,
`UpdateHighlightArea` and `ConvertCoordinateToIndex` do — while the render
moves a tall-icon line and every line after it down by the overflow
(`InitializeText`; the emitter co-hook) and the box grows to match (the
string-height co-hook). 4.3.4 has that fault too; `EditBox::IconRows`
(`src/editbox/IconRows.cpp`) gives each placer the render's move, from
`Text::InlineTexture::LineIconShift` over the box's own line table: the
click's y is moved onto the uniform row of the icon-aware row under it
before the engine maps it; a selection piece is re-anchored after the
engine placed it, with the engine's own x; the caret likewise, then
`OnCursorChanged` fired once with the moved y through
`FUN_FRAME_RUN_SCRIPT_VARIADIC` exactly as `PlaceCaret` fires it (its own
fire muted for the call by emptying the slot), so
`ScrollingEditBoxMixin:ScrollCursorIntoView` scrolls to the real caret.
- **Click → cursor, single-line window** — `GetIndexAtPoint` (`0x77d0d0`),
`RebuildLabel` and `MeasureWindow` count characters with
`FUN_FONTSTRING_FIT_TEXT` (`0x772bc0` → `0x44d860` → `0x5c6c50`), whose
+18 -5
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
+190 -4
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
@@ -988,7 +1002,8 @@ enum Offsets {
// message. Every new message shifts the newest index, so EVERY visible slot
// is re-SetText'd -> its inline icons must be recomputed; the engine builds
// the line lazily at paint, so icons land one frame after the glyphs (the
// one-frame lag). Text::InlineTexture co-hooks this and, post-refresh,
// one-frame lag). ScrollingMessageFrame::Layout owns the hook and fans
// out after it; Text::InlineTexture listens and, post-refresh,
// forces each icon-bearing line to resolve+build and applies its icon
// placement synchronously — pre-render (the refresh runs from the SMF's
// Lua/event-driven methods, never mid-render), so icons draw with their
@@ -2425,6 +2440,16 @@ enum Offsets {
// right after that same unlock — Player::Equipment co-hooks it to do so.
FUN_INV_SLOT_CHANGED_OBSERVER = 0x005DDCF0,
// CGPlayer_C::AddActiveWatchers / its removal — `void __thiscall
// (CGPlayer_C *)`, plain `ret`. Register and remove the active player's
// bank-PLAYER observers (inventory, quests, coinage, skills, the
// 0x005DE4B0 event fields); 4.3.4's SetActiveMirrorHandlers (0x0056E430)
// / UnsetActiveMirrorHandlers. Add runs on arrival (0x005DEA58) and
// reactivation (0x005E9C00); remove only at 0x005E9B50, for the active
// player.
FUN_PLAYER_ADD_ACTIVE_WATCHERS = 0x005DD8A0,
FUN_PLAYER_REMOVE_ACTIVE_WATCHERS = 0x005DE5B0,
// PLAYER_FIELD_INV_SLOT_HEAD — first of the 19 equipment-slot u64 GUID
// fields in the player descriptor (0x4A8..0x538, stride 8; 0x540 is
// the first equipped-bag slot). 0-based slot = (offset - 0x4A8) >> 3;
@@ -4582,6 +4607,12 @@ enum Offsets {
// (see OFF_UNIT_FIELD_CHANNEL_SPELL below); PLAYER_FLAGS only
// exists on the +0xE68 sub-struct here.
OFF_CGPLAYER_INFO = 0xE68,
// PLAYER_SELF_RES_SPELL in that block (UpdateField table: player index
// 0x40C, owner-only): the spell the server offers a dead player to raise
// themselves (Soulstone Resurrection, Reincarnation), 0 for none. Also
// its offset in observer bank DESC_OBSERVER_BANK_PLAYER. Read by
// HasSoulstone (0x0048ACC6) and UseSoulstone (0x0048ADB6).
OFF_CGPLAYER_INFO_SELF_RES_SPELL = 0x1030,
// The engine's explored-area bit test (benilla `IsAreaExplored`):
// __thiscall(this = player + OFF_CGPLAYER_INFO, int areaBit) → nonzero
// when explored, ret 4. Reads `[this] + 0xE6C` (the 0x100-byte
@@ -4993,6 +5024,16 @@ enum Offsets {
// target is passed, so passing zero (no target) is fine for most
// items. `flag` is 0 in every Script_UseContainerItem call site.
FUN_ITEM_USE = 0x005D8D00,
// The player's self-resurrect item — `CGItem_C *__thiscall(CGPlayer_C *)`,
// plain `ret`: the first carried item (equipment, bags, backpack,
// keyring) with an on-use spell of effect 94 (SELF_RESURRECT), or null.
// HasSoulstone and UseSoulstone ask it when PLAYER_SELF_RES_SPELL is 0.
FUN_PLAYER_SELF_RES_ITEM = 0x005ED630,
// Script_UseSoulstone — the Lua C function itself, `int __fastcall(L)`;
// reads no argument and pushes nothing. Sends CMSG_SELF_RES (0x2B3, no
// body) when PLAYER_SELF_RES_SPELL is set, else cancels the cast
// (0x006E4940(1, 1, 0x1C)) and uses FUN_PLAYER_SELF_RES_ITEM's item.
FUN_SCRIPT_USE_SOULSTONE = 0x0048AD70,
// `CGPlayer::AutoEquipCursorItem` — `__thiscall(CGPlayer *this, int flag)`.
// The engine-internal helper that `Script_AutoEquipCursorItem`
// (`0x0048A040`) is a thin wrapper around: that wrapper just
@@ -12709,6 +12750,18 @@ enum Offsets {
CALL_TEXT_FIT_HEIGHT_STEP = 0x005C22B1,
CALL_TEXT_ENUMERATE_LINES_STEP = 0x005C24F9,
DRAW_BUILDER_FRAME_LINE_X = 0x48,
// The fit-to-height count's line test (0x005C21C0, after its stepper
// call): `fld [ebp+10h]` (the block height), then this `fadd dword ptr
// [VAR_TEXT_FIT_HEIGHT_EPSILON]` (D8 05 BC 26 80 00), then FCOMPP against
// the running height + font height; it stops at the first line that
// fails. [ebp-8] is the bytes fitted so far, 0 until a line fits. EAX is
// dead here (FNSTSW AX, then both paths reload it from [ebp-8]).
// Text::FitHeight replaces the fadd.
SITE_TEXT_FIT_HEIGHT_TOLERANCE = 0x005C22CB,
TEXT_FIT_HEIGHT_FRAME_FITTED_BYTES = 0x8,
// Exactly 2^-20 (0x35800000, .rdata): the fit's line margin above. A
// shared float, also read by collision, movement and matrix code.
VAR_TEXT_FIT_HEIGHT_EPSILON = 0x008026BC,
// The draw builder's right / centre x: `FUN_TEXT_SCREEN_TO_PIXEL_WIDTH(raw,
// -width)` (or -width / 2), `float __fastcall(int raw, float)`, `ret 4`,
// result in ST0, stored to the line x at 0x005CDE28.
@@ -12811,6 +12864,93 @@ enum Offsets {
OFF_SIMPLEHTML_FONTS = 0x350,
VAR_SIMPLEHTML_LUA_TYPE_ID = 0x00CF4F64,
FUN_SIMPLEHTML_READ_TEXT_TYPE = 0x00795D80,
// The ScrollingMessageFrame's message ring. GetNumMessages (0x007934D0)
// pushes the count. The ring's one writer, 0x00788150 (`__thiscall(smf,
// text, const CImVector *color, int id)`, `ret 0xC`), called only by the
// AddMessage method (FUN_SCRIPT_..._ADD_MESSAGE, `int __fastcall(L)`),
// raises the count up to the limit, advances the position (wrapping at
// the limit) and fills the record there. So message i, 1 the oldest, is
// record (position - count + i), plus the limit when negative. Records
// are 0x18 bytes: the text (an SStrDupA copy), the colour (a CImVector,
// the dword 0xAARRGGBB: AddMessage builds it R, G, B with alpha 0xFF at
// 0x00792ADD..0x00792B11), shown +0x8, the visible and fade times +0xC and
// +0x10, and the integer id +0x14 (benilla-re ui/messageframe).
FUN_SCRIPT_SCROLLINGMESSAGEFRAME_ADD_MESSAGE = 0x00792900,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT = 0x33C,
OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES = 0x340,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS = 0x344, // the newest record; -1 when empty
OFF_SCROLLINGMESSAGEFRAME_MESSAGES = 0x394, // the record array
SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE = 0x18,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT = 0x0,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COLOR = 0x4,
// The rest of the record as the ring writer 0x00788150 fills it: shown
// (1), the frame's visible and fade times, and the integer id.
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_SHOWN = 0x8,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_VISIBLE_TIME = 0xC,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_FADE_TIME = 0x10,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_ID = 0x14,
// The bottom flag (4.3.4's m_atBottom): the ring writer lays the lines
// out from the newest record when it is set (0x00788150 tail), and
// ScrollToBottom re-arms the shown lines instead of laying out
// (0x007886D0; benilla-re ui/messageframe "Scrolling brings lines back").
OFF_SCROLLINGMESSAGEFRAME_AT_BOTTOM = 0x354,
// The visible and fade times the writer copies into each record
// (0x00788150: record+0xC = +0x364, record+0x10 = +0x368; the setters
// 0x00788090 / 0x007880F0 store them).
OFF_SCROLLINGMESSAGEFRAME_VISIBLE_TIME = 0x364,
OFF_SCROLLINGMESSAGEFRAME_FADE_TIME = 0x368,
// The colour (CImVector) of the object at OFF_SCROLLINGMESSAGEFRAME_FONT,
// the message colour AddMessage takes without r, g, b
// (0x00792B3C..0x00792B45: `[[smf+0x35C]+0x58]`).
OFF_SCROLLINGMESSAGEFRAME_FONT_COLOR = 0x58,
// The frame's scroll in retail's units, the lines it is scrolled up from
// the newest message. ScrollToOffset (`void __thiscall(smf, int offset)`,
// `ret 4`) is the body of SetScrollFromBottom (0x00793070), 4.3.4's
// SetScrollOffset (0x0051E6F0) step for step: the record (position -
// offset + count) % count, then it re-arms the shown lines (0x00788B80)
// when that is the current scroll and lays the lines out from it
// (FUN_SMF_DISPLAY_REFRESH) otherwise. ScrollOffset (`int
// __thiscall(smf)`) is the inverse, (position - scroll + count) % count,
// 0 when empty; the GetCurrentScroll method pushes it.
FUN_SCROLLINGMESSAGEFRAME_SCROLL_TO_OFFSET = 0x00788700,
FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET = 0x00788AC0,
// Clear (`void __thiscall(smf)`, 4.3.4's Clear): count 0, position 0, a
// layout from 0, +0x350 = 0, position -1. The Clear method's body.
FUN_SCROLLINGMESSAGEFRAME_CLEAR = 0x007882B0,
// RearmLines (`void __thiscall(smf)`, 4.3.4's RefreshMessages): every
// shown line back to alpha 0xFF, shown, the frame's two times, and its
// node shown (benilla-re ui/messageframe "Scrolling brings lines back").
// FUN_SMF_DISPLAY_REFRESH (LayoutLines) owns +0x348 (the scroll record)
// and +0x354 (at bottom = record == position) itself, and re-arms only
// when it moves off the bottom; a layout at the bottom keeps the fade
// state, so 4.3.4 re-arms explicitly after a removal's layout.
FUN_SCROLLINGMESSAGEFRAME_REARM_LINES = 0x00788B80,
FUN_SCRIPT_SCROLLINGMESSAGEFRAME_GET_CURRENT_SCROLL = 0x00793650,
// The frame's OnFrameUpdate override (vtable+0x38, see
// FUN_FRAME_ON_UPDATE), `void __thiscall(smf, float elapsed)`, `ret 4`:
// FUN_FRAME_ON_UPDATE (OnUpdate), then the fader.
FUN_SCROLLINGMESSAGEFRAME_ON_UPDATE = 0x00788460,
// The line layout (FUN_SMF_DISPLAY_REFRESH) anchors a line once, when it
// creates the line's font string, through the one SetPoint call at
// CALL_SMF_LAYOUT_ANCHOR_LINE (`E8 rel32` to FUN_REGION_SET_POINT; ESI the
// frame, EDI the font string, its parent at OFF_REGION_PARENT already set
// by SetRegionParent 0x0077FD10): the first line (BOTTOMLEFT, the frame,
// BOTTOMLEFT, left inset, bottom inset), each next one (BOTTOMLEFT, the
// line before, TOPLEFT, 0, FUN_PIXEL_ALIGN_Y(spacing)), then
// FUN_LAYOUT_RESIZE(1). The layout is the only maker of lines (the array
// grow 0x00789060 and the node constructor 0x00788D90 have no other
// caller). OnRectChanged (0x00788310) destroys every line when the message
// area changes size and lays them out again; Clear (0x007882B0) only hides
// them. The insets are the four floats SetInsets (0x00787FB0) stores and
// OnRectChanged subtracts from the rect: bottom, left, top, right.
CALL_SMF_LAYOUT_ANCHOR_LINE = 0x00788975,
OFF_SCROLLINGMESSAGEFRAME_INSET_BOTTOM = 0x37C,
OFF_SCROLLINGMESSAGEFRAME_INSET_LEFT = 0x380,
OFF_SCROLLINGMESSAGEFRAME_INSET_TOP = 0x384,
OFF_FONTSTRING_SPACING = 0xF4, // float, the line spacing
// PixelAlignY - `float __stdcall(float)`, `ret 4`: rounds a vertical
// length up to whole device pixels (benilla-re ui/regions).
FUN_PIXEL_ALIGN_Y = 0x00766750,
// Focused-editbox global — holds the CSimpleEditBox that currently has
// keyboard focus (the one showing a cursor), or 0 when no input field is
@@ -12979,6 +13119,51 @@ enum Offsets {
// ending at 0x0077D49D.
PATCH_EDITBOX_TEXT_CHANGED_FIRE = 0x0077D485,
OFF_EDITBOX_ONTEXTCHANGED_SLOT = 0x418,
// --- Edit box rows (EditBox::IconRows) ---
// The mode flags byte (benilla-re ui/editbox): bit 0 autoFocus, 1
// multiLine, 2 numeric, 3 password, 4 ignoreArrows. Every row placer
// below tests it (`TEST byte ptr [ESI+0x318], 0x2 / 0x8`). Not the dirty
// word OFF_EDITBOX_FLAGS (+0x31c).
OFF_EDITBOX_MODE_FLAGS = 0x318,
EDITBOX_MODE_MULTILINE = 0x02,
EDITBOX_MODE_PASSWORD = 0x08,
// The password copy of the text (every byte '*'), the buffer the label
// and the rows read with EDITBOX_MODE_PASSWORD set (4.3.4's
// m_textHidden); OFF_EDITBOX_TEXT otherwise. RebuildLabel 0x0077D4D0
// refreshes it.
OFF_EDITBOX_TEXT_MASKED = 0x334,
// The visible window in bytes: its first byte and its length (4.3.4's
// m_visiblePosition / m_visibleLength). PlaceCaret hides the caret and
// fires nothing for a cursor outside it (0x0077DA89..0x0077DAA7).
OFF_EDITBOX_VISIBLE_START = 0x348,
OFF_EDITBOX_VISIBLE_LENGTH = 0x34C,
// The caret texture (4.3.4's m_cursorTexture), anchored by PlaceCaret.
OFF_EDITBOX_CARET = 0x368,
// OnCursorChanged's script slot; PlaceCaret fires it when non-zero
// (0x0077DD8C).
OFF_EDITBOX_ONCURSORCHANGED_SLOT = 0x428,
// The three row placers, all `__thiscall`: PlaceCaret (no args; 4.3.4's
// UpdateVisibleCursor), PlaceSelectionPiece `(editbox, texture, from, to)`
// `ret 0xC` (UpdateHighlightArea) and GetIndexAtPoint `(editbox, float x,
// float y, int *index)` `ret 0xC` (ConvertCoordinateToIndex). Each steps
// rows of FUN_PIXEL_ALIGN_Y(fontH × scale) + FUN_PIXEL_ALIGN_Y(spacing ×
// scale), the first two divided by the box's OFF_LAYOUT_SCALE (benilla-re
// ui/editbox "The caret" / "Selection"; Ghidra). Co-hooked by
// EditBox::IconRows: tall inline icons move the rows.
FUN_EDITBOX_PLACE_CARET = 0x0077DA80,
FUN_EDITBOX_PLACE_SELECTION_PIECE = 0x0077DE70,
FUN_EDITBOX_INDEX_AT_POINT = 0x0077D0D0,
// "%f%f%f%f" (.data; verified in the file at 0x479CB8) — the format
// PlaceCaret fires OnCursorChanged with through
// FUN_FRAME_RUN_SCRIPT_VARIADIC: x, y, width, height, each a double of
// FUN_COORD_DIVIDE_BY_SCREEN_SCALE_X(FUN_COORD_GET_SCALED_SCREEN_ASPECT()
// × [FLOAT_SCRIPT_COORD_SCALE] × value) — 4.3.4's
// RunOnCursorChangedScript (0x0077DDA0..0x0077DE20).
VAR_SCRIPT_FMT_FFFF = 0x00879CB8,
// 1024.0f (.rdata; verified in the file at 0x3FFD68): the anchor→script
// push factor every script coordinate push multiplies by (the `1024` of
// the GetFont note at FUN_COORD_GET_SCALED_SCREEN_ASPECT).
FLOAT_SCRIPT_COORD_SCALE = 0x007FFD68,
// `void __thiscall(editbox, int maxLines)` — the SetHistoryLines internal
// (FUN_0077cd90). Reallocates the history ring to maxLines; maxLines == 0
// frees the whole buffer and zeroes the container. ClearHistory calls it
@@ -12996,7 +13181,8 @@ enum Offsets {
// core FUN_0044D670(fontHandle fs+0xE0, text, len, fontH*[fs+0x7C], &out,
// fs+0x108, 0, flags fs+0x120), then `fs+0xFC = out / [fs+0x7C]`. Exactly 4
// callers: Script_GetStringWidth (0x0079E510), GameTooltip auto-size
// (0x00530640), the editbox caret positioner (0x0077DE70), and the layout
// (0x00530640), the editbox selection-piece placer (0x0077DE70, a whole
// last line's width), and the layout
// effective-width vmethod (FUN_00772930 = fs+0x24 vtbl slot +0x1C: explicit
// rect width else string width). Prologue PUSH EBX; PUSH ESI; MOV ESI,ECX;
// FLD [ESI+0xFC] — MinHook-safe. Co-hooked by Text::InlineTexture to add
+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
+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);
}
+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
+218
View File
@@ -0,0 +1,218 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_DeathInfo.GetSelfResurrectOptions()`, `C_DeathInfo.UseSelfResurrectOption
// (optionType, id)` and `SELF_RES_SPELL_CHANGED` — the self-resurrection that
// 1.12 serves through HasSoulstone (0x0048AC80) and UseSoulstone (0x0048AD70).
//
// 1.12 offers at most one way for a dead player to raise themselves, and both
// functions pick it the same way: PLAYER_SELF_RES_SPELL when the server set it
// (Soulstone Resurrection, Reincarnation), else the first carried item whose
// on-use spell is a self-resurrect. So the list holds that one option, under
// HasSoulstone's gate (a living player has none), and using it calls
// UseSoulstone itself. Nothing in 1.12 limits a self-resurrection, so every
// option can be used and none is limited.
//
// SELF_RES_SPELL_CHANGED is 4.3.4's: CGPlayer_C::SetActiveMirrorHandlers
// watches PLAYER_SELF_RES_SPELL with OnUpdatePlayerScriptEvent. 1.12 watches
// no such field, so the same watcher joins 1.12's AddActiveWatchers and
// leaves with its removal, the lifetime of the engine's own player watchers.
#include "Game.h"
#include "Offsets.h"
#include "event/Custom.h"
#include "item/Link.h"
#include "spell/Lookup.h"
#include "unit/Identity.h"
#include <cstdint>
namespace Death::SelfResurrect {
namespace {
constexpr int OPTION_TYPE_SPELL = 0;
constexpr int OPTION_TYPE_ITEM = 1;
const Game::Doc::Event kChangedDoc{
"DeathInfo",
"The player's self-resurrection spell changed. Read C_DeathInfo.GetSelfResurrectOptions.",
{}};
const Event::Custom::AutoReserve kChanged{"SELF_RES_SPELL_CHANGED", &kChangedDoc};
struct Option {
const char *name;
int optionType;
int id;
};
// HasSoulstone's answer: the option, or false while the player lives or has
// none. `itemName` holds an item option's name.
bool CurrentOption(Option &out, char *itemName, size_t itemNameSize) {
const uint8_t *player = Unit::Identity::PlayerObject();
const uint8_t *desc = Unit::Identity::PlayerDescriptor();
if (player == nullptr || desc == nullptr ||
Game::Read<int32_t>(desc, Offsets::OFF_UNIT_FIELD_HEALTH) > 0)
return false;
const auto *info = Game::Read<const uint8_t *>(player, Offsets::OFF_CGPLAYER_INFO);
const int spellID =
info != nullptr ? Game::Read<int32_t>(info, Offsets::OFF_CGPLAYER_INFO_SELF_RES_SPELL) : 0;
if (spellID != 0) {
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
out = {rec != nullptr
? Game::Read<const char *>(rec, Offsets::OFF_SPELL_NAMES +
Game::Read<int>(Offsets::VAR_LOCALE_INDEX) *
4)
: "UNKNOWN",
OPTION_TYPE_SPELL, spellID};
return true;
}
using SelfResItem_t = const uint8_t *(__fastcall *)(const void *player);
const uint8_t *item =
reinterpret_cast<SelfResItem_t>(Offsets::FUN_PLAYER_SELF_RES_ITEM)(player);
if (item == nullptr)
return false;
Item::Link::NameFromCGItem(item, itemName, itemNameSize);
const auto *fields = Game::Read<const uint8_t *>(item, Offsets::OFF_UNIT_GUID_PTR);
out = {itemName, OPTION_TYPE_ITEM,
fields != nullptr ? Game::Read<int32_t>(fields, Offsets::OFF_OBJECT_FIELD_ENTRY) : 0};
return true;
}
int __fastcall Script_GetSelfResurrectOptions(void *L) {
if (Unit::Identity::PlayerObject() == nullptr)
return 0;
Game::Lua::NewTable(L);
char itemName[0x400];
Option option;
if (CurrentOption(option, itemName, sizeof(itemName))) {
Game::Lua::PushNumber(L, 1);
Game::Lua::NewTable(L);
Game::Lua::SetFieldString(L, "name", option.name);
Game::Lua::SetFieldNumber(L, "optionType", option.optionType);
Game::Lua::SetFieldNumber(L, "id", option.id);
Game::Lua::SetFieldBool(L, "canUse", true);
Game::Lua::SetFieldBool(L, "isLimited", false);
Game::Lua::SetFieldNumber(L, "priority", 0);
Game::Lua::SetTable(L, -3);
}
return 1;
}
int __fastcall Script_UseSelfResurrectOption(void *L) {
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: C_DeathInfo.UseSelfResurrectOption(optionType, id)");
return 0;
}
char itemName[0x400];
Option option;
if (CurrentOption(option, itemName, sizeof(itemName)) &&
option.optionType == static_cast<int>(Game::Lua::ToNumber(L, 1)) &&
option.id == static_cast<int>(Game::Lua::ToNumber(L, 2)))
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_USE_SOULSTONE)(L);
return 0;
}
// Observer callback ABI per the FUN_DESC_OBSERVER_REGISTER block in
// Offsets.h: __fastcall(fieldOffset /*ecx*/, size /*edx*/, guidLo, guidHi,
// oldValues*, userArg), returns 1, callee cleans 0x10. It runs only when the
// field's bytes changed.
int __fastcall SelfResSpellChanged(uint32_t /*fieldOffset*/, uint32_t /*size*/,
uint32_t /*guidLo*/, uint32_t /*guidHi*/,
const void * /*oldValues*/, void * /*userArg*/) {
Event::Custom::Fire(kChanged.Slot(), "");
return 1;
}
using Register_t = void(__fastcall *)(int bank, uint32_t fieldOffset, uint32_t guidLo,
uint32_t guidHi, int size, const void *callback,
void *userArg1, void *userArg2);
using Unregister_t = void(__fastcall *)(int bank, uint32_t fieldOffset, uint32_t guidLo,
uint32_t guidHi, const void *callback, void *userArg1);
using Watchers_t = void(__fastcall *)(void *player, void *edx);
Watchers_t g_origAddWatchers = nullptr;
Watchers_t g_origRemoveWatchers = nullptr;
void __fastcall AddActiveWatchers_h(void *player, void *edx) {
g_origAddWatchers(player, edx);
const uint64_t guid = Unit::Identity::GuidForObject(player);
reinterpret_cast<Register_t>(Offsets::FUN_DESC_OBSERVER_REGISTER)(
Offsets::DESC_OBSERVER_BANK_PLAYER, Offsets::OFF_CGPLAYER_INFO_SELF_RES_SPELL,
static_cast<uint32_t>(guid), static_cast<uint32_t>(guid >> 32), 4,
reinterpret_cast<const void *>(&SelfResSpellChanged), nullptr, nullptr);
}
// Removed before the original, while the registry is as the engine's own
// removals find it.
void __fastcall RemoveActiveWatchers_h(void *player, void *edx) {
const uint64_t guid = Unit::Identity::GuidForObject(player);
reinterpret_cast<Unregister_t>(Offsets::FUN_DESC_OBSERVER_UNREGISTER)(
Offsets::DESC_OBSERVER_BANK_PLAYER, Offsets::OFF_CGPLAYER_INFO_SELF_RES_SPELL,
static_cast<uint32_t>(guid), static_cast<uint32_t>(guid >> 32),
reinterpret_cast<const void *>(&SelfResSpellChanged), nullptr);
g_origRemoveWatchers(player, edx);
}
const Game::HookAutoRegister _addHook{Offsets::FUN_PLAYER_ADD_ACTIVE_WATCHERS,
reinterpret_cast<void *>(&AddActiveWatchers_h),
reinterpret_cast<void **>(&g_origAddWatchers)};
const Game::HookAutoRegister _removeHook{Offsets::FUN_PLAYER_REMOVE_ACTIVE_WATCHERS,
reinterpret_cast<void *>(&RemoveActiveWatchers_h),
reinterpret_cast<void **>(&g_origRemoveWatchers)};
const Game::Lua::EnumIntegerEntry kOptionTypeEntries[] = {
{"Spell", OPTION_TYPE_SPELL},
{"Item", OPTION_TYPE_ITEM},
};
const Game::Doc::Field kOptionFields[] = {
Game::Doc::Req("name", "string"),
Game::Doc::Req("optionType", "SelfResurrectOptionType"),
Game::Doc::Req("id", "number", "The spell ID or item ID."),
Game::Doc::Req("canUse", "bool"),
Game::Doc::Req("isLimited", "bool"),
Game::Doc::Req("priority", "number", "Lower comes first."),
};
const Game::Doc::Structure kOption{"SelfResurrectOption", "DeathInfo", kOptionFields,
"A way for the dead player to resurrect themselves."};
const Game::Doc::Field kGetRets[] = {
Game::Doc::Req("options", "table", "Array of SelfResurrectOption; empty while alive."),
};
const Game::Doc::Function kGetSelfResurrectOptions{
"The ways the dead player can resurrect themselves.", {}, kGetRets};
const Game::Doc::Field kUseArgs[] = {
Game::Doc::Req("optionType", "SelfResurrectOptionType"),
Game::Doc::Req("id", "number"),
};
const Game::Doc::Function kUseSelfResurrectOption{
"Resurrects the dead player with one of the options from GetSelfResurrectOptions.",
kUseArgs, {}};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_DeathInfo", "GetSelfResurrectOptions",
&Script_GetSelfResurrectOptions, &kGetSelfResurrectOptions);
Game::Lua::RegisterTableFunction("C_DeathInfo", "UseSelfResurrectOption",
&Script_UseSelfResurrectOption, &kUseSelfResurrectOption);
Game::Lua::RegisterIntegerEnum("Enum", "SelfResurrectOptionType", kOptionTypeEntries,
sizeof(kOptionTypeEntries) / sizeof(kOptionTypeEntries[0]),
"DeathInfo");
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Death::SelfResurrect
+308
View File
@@ -0,0 +1,308 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Tall inline icons move the edit box's rows.
//
// A multi-line edit box places its caret, its selection pieces and a click's
// cursor by rows of one height — the label's font height plus its spacing,
// pixel-aligned (FUN_EDITBOX_PLACE_CARET, FUN_EDITBOX_PLACE_SELECTION_PIECE,
// FUN_EDITBOX_INDEX_AT_POINT; 4.3.4's UpdateVisibleCursor, UpdateHighlightArea
// and ConvertCoordinateToIndex step the same rows). The label's text leaves
// those rows once a line holds an icon taller than the font: the render moves
// that line and every line after it down by the overflow (4.3.4's
// InitializeText; the emitter co-hook in Text::InlineTexture), and the box
// grows by the same amount through the string-height co-hook. So below the
// first tall-icon line the caret and the highlight sat above their text by
// the summed overflow, and OnCursorChanged reported that y, which
// ScrollingEditBoxMixin:ScrollCursorIntoView scrolls by — pfUI's chat copy
// box with faction-emblem icons. 4.3.4 has the same fault; nothing it ships
// puts a tall icon in an edit box.
//
// Each placer keeps its engine code and gets the move the render makes, from
// Text::InlineTexture::LineIconShift over the box's own line table (the
// breaks the render lays out), split by the label's vertical justify as the
// emitter splits it:
// - the click: its y is moved onto the uniform row that the icon-aware row
// under it has, before the engine maps it;
// - a selection piece: re-anchored after the engine placed it, with the
// engine's own x, lower by the move;
// - the caret: the same re-anchor, then OnCursorChanged fired once with the
// moved y through the engine's own forwarder. The engine's fire is muted
// for the call by emptying the script slot: it reads the row y before
// anything can correct it.
// A single-line box has one row and needs nothing: its label is justified
// MIDDLE in a rect of fixed height, so its text stays centred on the caret.
#include "Game.h"
#include "Offsets.h"
#include "text/InlineTexture.h"
#include <cstdint>
namespace EditBox::IconRows {
namespace {
using PlaceCaret_t = void(__fastcall *)(void *editBox);
using PlaceSelectionPiece_t = void(__fastcall *)(void *editBox, void *edx, void *piece, int from, int to);
using IndexAtPoint_t = void(__fastcall *)(void *editBox, void *edx, float x, float y, int *index);
using FontHeight_t = float(__fastcall *)(void *fs, void *edx, int mode);
using MeasureRun_t = float(__fastcall *)(void *fs, void *edx, const uint8_t *text, int len);
using PixelAlignY_t = float(__stdcall *)(float length);
using GetRect_t = int(__fastcall *)(void *anchor, void *edx, float *out);
using ClearAllPoints_t = void(__fastcall *)(void *anchor);
using SetPoint_t = void(__thiscall *)(void *anchor, int point, void *relAnchor, int relPoint, float x, float y,
int flag);
using Resize_t = void(__fastcall *)(void *anchor, void *edx, int force);
using Extent_t = float(__fastcall *)(void *anchor, void *edx);
using ScreenAspect_t = float(__cdecl *)();
using DivideByScreenScaleX_t = float(__stdcall *)(float value);
using FireVariadic_t = void(__cdecl *)(void *frame, uint32_t *slotPtr, const char *fmt, ...);
PlaceCaret_t g_origPlaceCaret = nullptr;
PlaceSelectionPiece_t g_origPlaceSelectionPiece = nullptr;
IndexAtPoint_t g_origIndexAtPoint = nullptr;
void *AnchorOf(void *region) { return static_cast<uint8_t *>(region) + Offsets::OFF_REGION_ANCHOR; }
// A multi-line box's rows: its label, the buffer the line table indexes (the
// '*' copy of a password box) and the table — each line's first byte, the
// last entry the end.
struct Rows {
void *fs = nullptr;
const uint8_t *text = nullptr;
const uint32_t *starts = nullptr;
int count = 0; // entries; lines = count - 1
float scale = 1.0f; // the box's OFF_LAYOUT_SCALE
};
bool MultiLineRows(void *editBox, Rows &rows) {
const uint8_t mode = Game::Read<uint8_t>(editBox, Offsets::OFF_EDITBOX_MODE_FLAGS);
if ((mode & Offsets::EDITBOX_MODE_MULTILINE) == 0)
return false;
rows.fs = Game::Read<void *>(editBox, Offsets::OFF_EDITBOX_FONTSTRING);
rows.text = Game::Read<const uint8_t *>(editBox, (mode & Offsets::EDITBOX_MODE_PASSWORD)
? Offsets::OFF_EDITBOX_TEXT_MASKED
: Offsets::OFF_EDITBOX_TEXT);
rows.starts = Game::Read<const uint32_t *>(editBox, Offsets::OFF_EDITBOX_LINE_STARTS);
rows.count = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_LINE_COUNT);
rows.scale = Game::Read<float>(editBox, Offsets::OFF_LAYOUT_SCALE);
return rows.fs != nullptr && rows.text != nullptr && rows.starts != nullptr && rows.count >= 2 &&
rows.scale > 0.0f;
}
// The line holding byte `pos` — the placers' walk down the table.
int LineOf(const Rows &rows, uint32_t pos) {
int line = 0;
while (pos >= rows.starts[line + 1]) {
if (line >= rows.count - 2)
break;
++line;
}
return line;
}
bool LineShift(const Rows &rows, int line, float &before, float &after) {
before = 0.0f;
after = 0.0f;
const uint32_t start = rows.starts[line];
const uint32_t end = rows.starts[line + 1];
return end > start &&
Text::InlineTexture::LineIconShift(rows.fs, rows.text + start, static_cast<int>(end - start), before, after);
}
// How far the render moves line `line`'s text below its uniform row: the
// moves of every line above it, and its own text's move.
float ShiftOf(const Rows &rows, int line) {
float shift = 0.0f;
for (int i = 0; i <= line; ++i) {
float before = 0.0f;
float after = 0.0f;
if (!LineShift(rows, i, before, after))
continue;
shift += before;
if (i < line)
shift += after;
}
return shift;
}
// The placers' row in the box's parent units: the label's font height and
// spacing, each pixel-aligned at the box's scale (PlaceCaret
// 0x0077DAEB..0x0077DB0E).
float RowHeight(const Rows &rows) {
const float fontH = reinterpret_cast<FontHeight_t>(Offsets::FUN_FONTSTRING_FONT_HEIGHT)(rows.fs, nullptr, 1);
const float spacing = Game::Read<float>(rows.fs, Offsets::OFF_FONTSTRING_SPACING);
const auto align = reinterpret_cast<PixelAlignY_t>(Offsets::FUN_PIXEL_ALIGN_Y);
return align(fontH * rows.scale) + align(spacing * rows.scale);
}
float MeasureRun(const Rows &rows, uint32_t from, uint32_t to) {
return reinterpret_cast<MeasureRun_t>(Offsets::FUN_FONTSTRING_MEASURE_SUBSTRING)(rows.fs, nullptr, rows.text + from,
static_cast<int>(to - from));
}
// The x the placers anchor byte `pos` of `line` at, and the label point they
// anchor to, by the label's horizontal justify (OFF_FONTSTRING_FLAGS & 7).
// False for none, which anchors nothing.
bool AnchorX(const Rows &rows, int line, uint32_t pos, float &x, int &relPoint) {
const uint32_t start = rows.starts[line];
const uint32_t end = rows.starts[line + 1];
switch (Game::Read<uint32_t>(rows.fs, Offsets::OFF_FONTSTRING_FLAGS) & 7u) {
case 1: // LEFT
x = (pos == start) ? 0.0f : MeasureRun(rows, start, pos);
relPoint = Offsets::FRAMEPOINT_TOPLEFT;
return true;
case 2: // CENTER
x = (pos == start) ? 0.0f : MeasureRun(rows, start, pos);
x -= MeasureRun(rows, start, end) * 0.5f;
relPoint = Offsets::FRAMEPOINT_TOP;
return true;
case 4: // RIGHT
x = (pos == end) ? 0.0f : -MeasureRun(rows, pos, end);
relPoint = Offsets::FRAMEPOINT_TOPRIGHT;
return true;
default:
return false;
}
}
// Anchors `region` as the placer did, at `y`.
void Anchor(const Rows &rows, void *region, int relPoint, float x, float y, int flag) {
void *anchor = AnchorOf(region);
reinterpret_cast<ClearAllPoints_t>(Offsets::FUN_LAYOUT_CLEAR_ALL_POINTS)(anchor);
reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT)(anchor, Offsets::FRAMEPOINT_TOPLEFT, AnchorOf(rows.fs),
relPoint, x, y, flag);
}
// --- the click ---------------------------------------------------------------
void __fastcall IndexAtPoint_h(void *editBox, void *edx, float x, float y, int *index) {
Rows rows;
if (!MultiLineRows(editBox, rows))
return g_origIndexAtPoint(editBox, edx, x, y, index);
float rect[4] = {0.0f, 0.0f, 0.0f, 0.0f}; // bottom, left, top, right
if (reinterpret_cast<GetRect_t>(Offsets::FUN_REGION_GET_RECT)(AnchorOf(rows.fs), nullptr, rect) == 0)
return g_origIndexAtPoint(editBox, edx, x, y, index);
// The engine walks rows down from the label's top while the click is
// below the row (strictly), stopping at the last line. Walk the rows as
// the render lays them out: each taller by its line's moves.
const float row = RowHeight(rows);
const float below = rect[2] - y;
int line = 0;
float top = 0.0f; // the icon-aware top of `line`, below the label's top
bool moved = false;
for (;;) {
float before = 0.0f;
float after = 0.0f;
if (LineShift(rows, line, before, after))
moved = true;
const float height = row + (before + after) * rows.scale;
if (!(below > top + height) || line >= rows.count - 2)
break;
top += height;
++line;
}
// Without a move the rows are the engine's own; with one, the click is
// moved onto the middle of the uniform row the engine maps to `line`.
const float yMapped = moved ? rect[2] - (static_cast<float>(line) + 0.5f) * row : y;
g_origIndexAtPoint(editBox, edx, x, yMapped, index);
}
// --- a selection piece --------------------------------------------------------
void __fastcall PlaceSelectionPiece_h(void *editBox, void *edx, void *piece, int from, int to) {
Rows rows;
if (!MultiLineRows(editBox, rows) || from < 0)
return g_origPlaceSelectionPiece(editBox, edx, piece, from, to);
const int line = LineOf(rows, static_cast<uint32_t>(from));
const float shift = ShiftOf(rows, line);
g_origPlaceSelectionPiece(editBox, edx, piece, from, to);
if (!(shift > 0.0f) || piece == nullptr)
return;
// The piece starts at `from`, clamped to its line.
uint32_t pos = static_cast<uint32_t>(from);
if (pos < rows.starts[line])
pos = rows.starts[line];
float x = 0.0f;
int relPoint = 0;
if (!AnchorX(rows, line, pos, x, relPoint))
return;
const float y = -(RowHeight(rows) / rows.scale) * static_cast<float>(line) - shift;
Anchor(rows, piece, relPoint, x, y, 0);
reinterpret_cast<Resize_t>(Offsets::FUN_LAYOUT_RESIZE)(AnchorOf(piece), nullptr, 1);
}
// --- the caret ----------------------------------------------------------------
void __fastcall PlaceCaret_h(void *editBox) {
Rows rows;
if (!MultiLineRows(editBox, rows))
return g_origPlaceCaret(editBox);
const int cursor = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_CURSOR_BYTE);
const int visibleStart = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_VISIBLE_START);
const int visibleLength = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_VISIBLE_LENGTH);
// Outside the window the engine hides the caret and fires nothing.
if (cursor < visibleStart || cursor > visibleStart + visibleLength)
return g_origPlaceCaret(editBox);
const int line = LineOf(rows, static_cast<uint32_t>(cursor));
const float shift = ShiftOf(rows, line);
if (!(shift > 0.0f))
return g_origPlaceCaret(editBox);
uint32_t *slot = Game::Ptr<uint32_t>(editBox, Offsets::OFF_EDITBOX_ONCURSORCHANGED_SLOT);
const uint32_t handler = slot[0];
slot[0] = 0;
g_origPlaceCaret(editBox);
slot[0] = handler;
void *caret = Game::Read<void *>(editBox, Offsets::OFF_EDITBOX_CARET);
if (caret == nullptr)
return;
const float y = -(RowHeight(rows) / rows.scale) * static_cast<float>(line) - shift;
float x = 0.0f;
int relPoint = 0;
if (AnchorX(rows, line, static_cast<uint32_t>(cursor), x, relPoint))
Anchor(rows, caret, relPoint, x, y, 1);
if (handler == 0)
return;
// OnCursorChanged(x, y, width, height) as the engine fires it: each
// value by the anchor→script factor, pushed as a double.
void *anchor = AnchorOf(caret);
void **vtable = *static_cast<void ***>(anchor);
const float width = reinterpret_cast<Extent_t>(vtable[7])(anchor, nullptr); // +0x1C GetWidth
const float height = reinterpret_cast<Extent_t>(vtable[8])(anchor, nullptr); // +0x20 GetHeight
const auto aspect = reinterpret_cast<ScreenAspect_t>(Offsets::FUN_COORD_GET_SCALED_SCREEN_ASPECT);
const auto divide = reinterpret_cast<DivideByScreenScaleX_t>(Offsets::FUN_COORD_DIVIDE_BY_SCREEN_SCALE_X);
const float factor = Game::Read<float>(Offsets::FLOAT_SCRIPT_COORD_SCALE);
const auto push = [&](float value) { return static_cast<double>(divide(aspect() * factor * value)); };
const double args[4] = {push(x), push(y), push(width), push(height)};
void *L = Game::Lua::State();
const int savedTop = (L != nullptr) ? Game::Lua::GetTop(L) : 0;
reinterpret_cast<FireVariadic_t>(Offsets::FUN_FRAME_RUN_SCRIPT_VARIADIC)(
editBox, slot, reinterpret_cast<const char *>(Offsets::VAR_SCRIPT_FMT_FFFF), args[0], args[1], args[2],
args[3]);
if (L != nullptr)
Game::Lua::SetTop(L, savedTop);
}
const Game::HookAutoRegister _caretHook{Offsets::FUN_EDITBOX_PLACE_CARET, reinterpret_cast<void *>(&PlaceCaret_h),
reinterpret_cast<void **>(&g_origPlaceCaret)};
const Game::HookAutoRegister _pieceHook{Offsets::FUN_EDITBOX_PLACE_SELECTION_PIECE,
reinterpret_cast<void *>(&PlaceSelectionPiece_h),
reinterpret_cast<void **>(&g_origPlaceSelectionPiece)};
const Game::HookAutoRegister _clickHook{Offsets::FUN_EDITBOX_INDEX_AT_POINT,
reinterpret_cast<void *>(&IndexAtPoint_h),
reinterpret_cast<void **>(&g_origIndexAtPoint)};
} // namespace
} // namespace EditBox::IconRows
@@ -45,12 +45,13 @@ Util\ItemUtil.lua
Util\AddOnCompat.lua
Util\FunctionUtil.lua
Util\CacheUtil.lua
Util\Pools.lua
Util\SecureTypes.lua
Util\ProxyUtil.lua
Util\Pools.lua
Util\FrameWatcher.lua
Util\TimedCallback.lua
Util\EasingUtil.lua
Util\ScriptAnimationUtil.lua
Util\Interpolator.lua
Util\AnchorUtil.lua
Util\RegionUtil.lua
@@ -1,214 +1,267 @@
-- Backport of 3.3.5 FrameXML/CallbackRegistryMixin.lua to vanilla 1.12 / Lua 5.0.
-- Backport of retail Blizzard_SharedXMLBase/CallbackRegistry.lua to vanilla 1.12 / Lua 5.0.
--
-- Implementation differences from the 3.3.5 source:
-- - 1.12 has no taint or Secure-Templates system, so the entire
-- AttributeDelegate/SetForbidden machinery for inserting event keys
-- into the callback tables is unnecessary. Sub-tables are allocated
-- lazily inside RegisterCallback instead.
-- - securecallfunction/secureexecuterange don't exist; they reduce to
-- direct calls / explicit for-loops here.
-- - GenerateClosure doesn't exist in 1.12; provide a local one that
-- captures registration-time varargs via SafePack-style storage.
-- - Lua 5.0 has no select() or `wipe()`; we use the implicit `arg`
-- table and assign-nil to clear tables.
-- Implementation differences from the retail source:
-- - No AttributeDelegate. Retail inserts event keys and creates deferred
-- callback tables through a forbidden frame's OnAttributeChanged, as a
-- taint barrier. 1.12 has no taint and no SetForbidden, so
-- SecureInsertEvent and GetMutableCallbacksByEvent do that work directly.
-- - TriggerEvent calls securecallfunction from its own loops, where retail
-- passes a vararg helper to secureexecuterange. The contract is the same
-- (an error goes to the error handler and the other callbacks still run),
-- but every call to a vararg function builds an `arg` table in Lua 5.0,
-- which would cost a table per callback per event.
-- - 1.12 has no global wipe, so UnregisterEvents uses table.wipe.
local generateOwnerID = CreateCounter()
local secureexecuterange = secureexecuterange;
local securecallfunction = securecallfunction;
local unpack = unpack;
local error = error;
local pairs = pairs;
local rawset = rawset;
local next = next;
local wipe = table.wipe;
local GetOrCreateTableEntry = GetOrCreateTableEntry;
local CallbackType = EnumUtil.MakeEnum("Closure", "Function")
local CallbackType = EnumUtil.MakeEnum("Closure", "Function");
-- Local GenerateClosure: binds `func` plus all of `...` so a later call
-- invokes func(boundArgs..., callArgs...). Mirrors 3.3.5's helper of the
-- same name. SafePack/SafeUnpack semantics so embedded nils survive.
local function GenerateClosure(func, ...)
local boundN = arg.n
local bound = {}
for i = 1, boundN do bound[i] = arg[i] end
return function(...)
local callN = arg.n
-- Forward bound + call args faithfully, embedded/leading nils and all.
-- The ONLY thing that makes unpack span holes on this Lua 5.0 build is
-- a `.n` field: unpack takes its count from getn (which returns t.n)
-- and ignores any explicit i/j, so a `.n`-less table stops dead at the
-- first hole (verified: unpack({1, 1, nil, 1}, 1, 4) yields just 1, 1).
local all = {}
local n = 0
for i = 1, boundN do n = n + 1; all[n] = bound[i] end
for i = 1, callN do n = n + 1; all[n] = arg[i] end
all.n = n
return func(unpack(all))
end
end
-- Callbacks can be registered without an owner as a matter of convenience. Generally this is fine when you never
-- intend to release the callback.
local generateOwnerID = CreateCounter();
CallbackRegistryMixin = CallbackRegistryMixin or {}
CallbackRegistryMixin = {};
function CallbackRegistryMixin:OnLoad()
local callbackTables = {}
for _, value in pairs(CallbackType) do
callbackTables[value] = {}
end
self.callbackTables = callbackTables
local callbackTables = {};
for callbackType, value in pairs(CallbackType) do
local callbacks = {};
callbackTables[value] = callbacks;
end
self.callbackTables = callbackTables;
self.executingEvents = {};
self.deferredCallbacks = {};
end
function CallbackRegistryMixin:SetUndefinedEventsAllowed(allowed)
self.isUndefinedEventAllowed = allowed
self.isUndefinedEventAllowed = allowed;
end
function CallbackRegistryMixin:GetCallbackTables()
return self.callbackTables
return self.callbackTables;
end
function CallbackRegistryMixin:GetCallbackTable(callbackType)
return self.callbackTables[callbackType]
return self.callbackTables[callbackType];
end
function CallbackRegistryMixin:GetCallbacksByEvent(callbackType, event)
local callbackTable = self:GetCallbackTable(callbackType)
return callbackTable[event]
local callbackTable = self:GetCallbackTable(callbackType);
return callbackTable[event];
end
-- Returns either the underlying callback table or a temporary table depending
-- on if the event is currently being dispatched. The callbacks stored in the
-- temporary table will be merged into the underlying callback table after the
-- dispatch is complete.
function CallbackRegistryMixin:GetMutableCallbacksByEvent(callbackType, event)
if securecallfunction(rawget, self.executingEvents, event) == nil then
return self:GetCallbacksByEvent(callbackType, event);
end
local deferrals = GetOrCreateTableEntry(self.deferredCallbacks, event);
return GetOrCreateTableEntry(deferrals, callbackType);
end
function CallbackRegistryMixin:HasRegistrantsForEvent(event)
for _, callbackTable in pairs(self:GetCallbackTables()) do
local callbacks = callbackTable[event]
if callbacks and next(callbacks) then
return true
end
end
return false
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
local callbacks = callbackTable[event];
if callbacks and securecallfunction(next, callbacks) then
return true;
end
end
return false;
end
-- Lazy allocation of per-event sub-tables. In 3.3.5 this went through a
-- forbidden attribute frame for taint-isolation reasons; that's a no-op
-- consideration on 1.12, so we just assign here.
function CallbackRegistryMixin:SecureInsertEvent(event)
for _, callbackTable in pairs(self:GetCallbackTables()) do
if not callbackTable[event] then
callbackTable[event] = {}
end
end
if not self:HasRegistrantsForEvent(event) then
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
if not callbackTable[event] then
rawset(callbackTable, event, {});
end
end
end
end
function CallbackRegistryMixin:RegisterCallback(event, func, owner, ...)
if type(event) ~= "string" then
error("CallbackRegistryMixin::RegisterCallback 'event' requires string type.")
elseif type(func) ~= "function" then
error("CallbackRegistryMixin::RegisterCallback 'func' requires function type.")
else
if owner == nil then
owner = generateOwnerID()
elseif type(owner) == "number" then
error("CallbackRegistryMixin:RegisterCallback 'owner' as number is reserved internally.")
end
end
if type(event) ~= "string" then
error("CallbackRegistryMixin::RegisterCallback 'event' requires string type.");
elseif type(func) ~= "function" then
error("CallbackRegistryMixin::RegisterCallback 'func' requires function type.");
else
if owner == nil then
owner = generateOwnerID();
elseif type(owner) == "number" then
error("CallbackRegistryMixin:RegisterCallback 'owner' as number is reserved internally.")
end
end
self:SecureInsertEvent(event)
-- Taint barrier for inserting event key into callback tables.
self:SecureInsertEvent(event);
for _, callbackTable in pairs(self:GetCallbackTables()) do
local callbacks = callbackTable[event]
callbacks[owner] = nil
end
-- An owner can have a single callback per event. The simpliest way to ensure
-- this is to remove all callbacks for the owner prior to new registration.
self:UnregisterCallback(event, owner);
if arg.n > 0 then
local callbacks = self:GetCallbacksByEvent(CallbackType.Closure, event)
callbacks[owner] = GenerateClosure(func, owner, unpack(arg))
else
local callbacks = self:GetCallbacksByEvent(CallbackType.Function, event)
callbacks[owner] = func
end
local count = select("#", ...);
if count > 0 then
local callbacks = self:GetMutableCallbacksByEvent(CallbackType.Closure, event);
callbacks[owner] = GenerateClosure(func, owner, ...);
else
local callbacks = self:GetMutableCallbacksByEvent(CallbackType.Function, event);
callbacks[owner] = func;
end
return owner
return owner;
end
local function CreateCallbackHandle(cbr, event, owner)
local handle =
{
Unregister = function()
cbr:UnregisterCallback(event, owner)
end,
}
return handle
-- Wrapped in a table for future flexibility.
local handle =
{
Unregister = function()
cbr:UnregisterCallback(event, owner);
end,
};
return handle;
end
function CallbackRegistryMixin:RegisterCallbackWithHandle(event, func, owner, ...)
owner = self:RegisterCallback(event, func, owner, unpack(arg))
return CreateCallbackHandle(self, event, owner)
owner = self:RegisterCallback(event, func, owner, ...);
return CreateCallbackHandle(self, event, owner);
end
function CallbackRegistryMixin:ReconcileDeferredCallbacks(event, closures, funcs)
self:CopyDeferredCallbacks(CallbackType.Closure, event, closures);
self:CopyDeferredCallbacks(CallbackType.Function, event, funcs);
self.deferredCallbacks[event] = nil;
end
function CallbackRegistryMixin:CopyDeferredCallbacks(callbackType, event, target)
local deferrals = self.deferredCallbacks[event];
if deferrals == nil then
return;
end
local callbacks = deferrals[callbackType];
if callbacks == nil then
return;
end
local function ExecuteAssignCallback(owner, callback)
target[owner] = callback;
end
secureexecuterange(callbacks, ExecuteAssignCallback);
end
function CallbackRegistryMixin:TriggerEvent(event, ...)
if type(event) ~= "string" then
error("CallbackRegistryMixin:TriggerEvent 'event' requires string type.")
elseif not self.isUndefinedEventAllowed and not (self.Event and self.Event[event]) then
error(string.format("CallbackRegistryMixin:TriggerEvent event '%s' doesn't exist.", event))
end
if type(event) ~= "string" then
error("CallbackRegistryMixin:TriggerEvent 'event' requires string type.");
elseif not self.isUndefinedEventAllowed and not self.Event[event] then
error(string.format("CallbackRegistryMixin:TriggerEvent event '%s' doesn't exist.", event));
end
local closures = self:GetCallbacksByEvent(CallbackType.Closure, event)
if closures then
for _, closure in pairs(closures) do
closure(unpack(arg))
end
end
-- TriggerEvent appears to need to support reentrant calls for now.
local count = (self.executingEvents[event] or 0) + 1;
local funcs = self:GetCallbacksByEvent(CallbackType.Function, event)
if funcs then
for owner, func in pairs(funcs) do
-- Stock 3.3.5 passes the owner as the first arg so callers can
-- distinguish multiple registrations of the same function. `arg`
-- carries its own `.n`, so unpack(arg) forwards the payload with
-- leading/embedded nils intact. Do NOT instead merge into a
-- `{ owner, ... }` table and unpack that WITHOUT setting `.n`: this
-- build's unpack counts via getn, so a `.n`-less table collapses at
-- the first hole (verified in-game: reload's PLAYER_ENTERING_WORLD,
-- arg1=nil arg2=1, dropped the payload).
func(owner, unpack(arg))
end
end
-- Set before invoking any callback so a reentrant call does not
-- attempt to call ReconcileDeferredCallbacks.
self.executingEvents[event] = count;
local closures = self:GetCallbacksByEvent(CallbackType.Closure, event);
if closures then
for owner, closure in pairs(closures) do
securecallfunction(closure, ...);
end
end
local funcs = self:GetCallbacksByEvent(CallbackType.Function, event);
if funcs then
for owner, func in pairs(funcs) do
securecallfunction(func, owner, ...);
end
end
-- The local count is the only value we care about for the purpose
-- of flushing the event key and reconciling the deferred callbacks.
if count == 1 then
self.executingEvents[event] = nil;
self:ReconcileDeferredCallbacks(event, closures, funcs);
end
end
function CallbackRegistryMixin:UnregisterCallback(event, owner)
if type(event) ~= "string" then
error("CallbackRegistryMixin:UnregisterCallback 'event' requires string type.")
elseif owner == nil then
error("CallbackRegistryMixin:UnregisterCallback 'owner' is required.")
end
if type(event) ~= "string" then
error("CallbackRegistryMixin:UnregisterCallback 'event' requires string type.");
elseif owner == nil then
error("CallbackRegistryMixin:UnregisterCallback 'owner' is required.");
end
for _, callbackTable in pairs(self:GetCallbackTables()) do
local callbacks = callbackTable[event]
if callbacks then
callbacks[owner] = nil
end
end
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
-- Only assign nil if the owner is present in the table, otherwise
-- its insertion could cause a rehash during iteration by secureexecuterange.
local callbacks = callbackTable[event];
if callbacks and callbacks[owner] then
callbacks[owner] = nil;
end
end
local deferrals = self.deferredCallbacks[event];
if deferrals then
for callbackType, callbacks in pairs(deferrals) do
-- Freely assign nil because deferredCallbacks are not subject
-- to secureexecuterange table mutilation stress disorder.
callbacks[owner] = nil;
end
end
end
function CallbackRegistryMixin:UnregisterEvents(eventTable)
if eventTable then
for _, callbackTable in pairs(self:GetCallbackTables()) do
for event in pairs(eventTable) do
if callbackTable[event] then
callbackTable[event] = nil
end
end
end
else
for _, callbackTable in pairs(self:GetCallbackTables()) do
for k in pairs(callbackTable) do
callbackTable[k] = nil
end
end
end
function CallbackRegistryMixin:UnregisterEvents()
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
wipe(callbackTable);
end
end
function CallbackRegistryMixin:GenerateCallbackEvents(events)
if not self.Event then
self.Event = {}
end
function CallbackRegistryMixin:UnregisterEventsByEventTable(eventTable)
if type(eventTable) ~= "table" then
error("CallbackRegistryMixin:UnregisterEventsByEventTable 'eventTable' requires table type.");
end
for eventIndex, eventName in ipairs(events) do
if self.Event[eventName] then
error(string.format("CallbackRegistryMixin:GenerateCallbackEvents: event '%s' already exists.", eventName))
end
self.Event[eventName] = eventName
end
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
for event in pairs(eventTable) do
callbackTable[event] = nil;
end
end
end
function CallbackRegistryMixin:GenerateCallbackEvents(eventTable)
if not self.Event then
self.Event = {};
end
if type(eventTable) ~= "table" then
error("CallbackRegistryMixin:GenerateCallbackEvents 'eventTable' requires table type.");
end
for eventIndex, eventName in ipairs(eventTable) do
if self.Event[eventName] then
error(string.format("CallbackRegistryMixin:GenerateCallbackEvents: event '%s' already exists.", eventName));
end
self.Event[eventName] = eventName;
end
end
function CallbackRegistryMixin.DoesFrameHaveEvent(frame, event)
return frame.Event and frame.Event[event]
return frame.Event and frame.Event[event];
end
@@ -1,14 +1,12 @@
-- Backport of 3.3.5 FrameXML/EventUtil.lua to vanilla 1.12 / Lua 5.0.
-- Backport of retail Blizzard_SharedXML/EventUtil.lua to vanilla 1.12 / Lua 5.0.
--
-- Implementation differences from the 3.3.5 source:
-- - Lua 5.0 has no select(); we work off the implicit `arg` table
-- in vararg functions.
-- - 1.12 has no C_AddOns; the legacy global IsAddOnLoaded(name)
-- fills the same role.
-- - 1.12 has no secureexecuterange; queued callbacks are dispatched
-- by a plain ipairs loop.
-- - AreVariablesLoaded in 3.3.5 sniffs GlueParent / UIParent.firstTimeLoaded.
-- 1.12 has neither; we track a private flag flipped on VARIABLES_LOADED.
-- Implementation differences from the retail source:
-- - Retail's UIParent sets UIParent.variablesLoaded and calls
-- EventUtil.TriggerOnVariablesLoaded on VARIABLES_LOADED. 1.12's
-- FrameXML does neither, so the frame at the bottom of this file does
-- both.
-- - Varargs are read from the implicit `arg` table, which every Lua 5.0
-- vararg function already has, rather than walked with select().
EventUtil = {}
@@ -19,7 +17,7 @@ function ContinueAfterAllEventsMixin:Init(callback, ...)
for index = 1, arg.n do
local event = arg[index]
assert(C_EventUtils.IsEventValid(event), "Unknown event \"" .. event .. "\"")
assert(C_EventUtils.IsEventValid(event), ("Unknown event \"%s\""):format(event))
self.events[event] = false
local function OnEventReceived()
@@ -50,13 +48,8 @@ function EventUtil.ContinueAfterAllEvents(callback, ...)
obj:Init(callback, unpack(arg))
end
-- Tracked locally because 1.12 has no GlueParent / UIParent.firstTimeLoaded.
-- Bootstrap frame at the bottom of the file listens for VARIABLES_LOADED
-- and flips the flag, then drains the queued callbacks via the
-- TriggerOnVariablesLoaded path.
local variablesLoaded = false
function EventUtil.AreVariablesLoaded()
return variablesLoaded
return GlueParent or (UIParent and UIParent.variablesLoaded)
end
local eventUtilVariablesLoadedCallbacks = {}
@@ -69,25 +62,26 @@ function EventUtil.ContinueOnVariablesLoaded(callback)
table.insert(eventUtilVariablesLoadedCallbacks, callback)
end
local secureexecuterange = secureexecuterange
local eventUtilContinueOnVariablesLoadedTriggered = false
function EventUtil.TriggerOnVariablesLoaded()
if eventUtilContinueOnVariablesLoadedTriggered then
-- If TriggerOnVariablesLoaded has been called once already, don't do anything further.
return
end
eventUtilContinueOnVariablesLoadedTriggered = true
variablesLoaded = true
for _, callback in ipairs(eventUtilVariablesLoadedCallbacks) do
secureexecuterange(eventUtilVariablesLoadedCallbacks, function(_, callback)
callback()
end
end)
end
function EventUtil.ContinueOnAddOnLoaded(addOnName, callback)
local isLoadedOrLoading, isLoaded = C_AddOns.IsAddOnLoaded(addOnName)
if isLoaded then
callback();
return;
end
local isLoadedOrLoading, isLoaded = C_AddOns.IsAddOnLoaded(addOnName)
if isLoaded then
callback()
return
end
EventUtil.RegisterOnceFrameEventAndCallback("ADDON_LOADED", callback, addOnName)
end
@@ -125,6 +119,7 @@ function CallbackHandleContainerMixin:Init()
self.handles = {}
end
-- Accepts any CBR and stores a handle to the registered callback.
function CallbackHandleContainerMixin:RegisterCallback(cbr, event, callback, owner)
self:AddHandle(cbr:RegisterCallbackWithHandle(event, callback, owner))
end
@@ -141,7 +136,7 @@ function CallbackHandleContainerMixin:Unregister()
end
function CallbackHandleContainerMixin:IsEmpty()
return table.getn(self.handles) == 0
return #self.handles == 0
end
function EventUtil.CreateCallbackHandleContainer()
@@ -150,13 +145,13 @@ function EventUtil.CreateCallbackHandleContainer()
return cbrHandles
end
-- VARIABLES_LOADED bootstrap. Drains anything queued via
-- ContinueOnVariablesLoaded the first time the event fires. We use a
-- dedicated frame so we don't pollute EventRegistry's payload routing.
-- Retail UIParent's VARIABLES_LOADED work for EventUtil: mark the variables
-- loaded, then run what ContinueOnVariablesLoaded queued.
do
local f = CreateFrame("Frame")
f:RegisterEvent("VARIABLES_LOADED")
f:SetScript("OnEvent", function()
UIParent.variablesLoaded = true
EventUtil.TriggerOnVariablesLoaded()
f:UnregisterEvent("VARIABLES_LOADED")
end)
@@ -1,110 +1,244 @@
-- Backport of FrameXML/FormattingUtil.lua (feasible subset) to vanilla
-- 1.12 / Lua 5.0.
-- Backport of retail Blizzard_SharedXML/FormattingUtil.lua to vanilla 1.12 / Lua 5.0.
--
-- 5.0 notes: no `#`/`%`/string-method syntax — uses string.*, math.mod.
-- GetMoneyString gives the text-symbol form ("12g 34s 56c"). The coin-icon form
-- (GetCoinTextureString / C_CurrencyInfo.GetCoinTextureString) is provided by the
-- ClassicAPI DLL in C++ (src/currency/CoinText.cpp) — like retail, where it's a
-- C function — now that the inline-texture backport renders |T markup in 1.12.
-- Implementation differences from the retail source:
-- - GetMoneyString always gives the coin-icon form, retail's default. Retail
-- switches to the text-symbol form ("12g 34s 56c") under its colorblind
-- mode, which 1.12 does not have.
-- - Not ported: GetCurrencyString / GetCurrenciesString (1.12 has no
-- currencies).
--
-- The FrameXML format-string globals these functions consume
-- (LARGE_NUMBER_SEPERATOR, PERCENTAGE_STRING, GOLD_AMOUNT_SYMBOL,
-- GOLD_AMOUNT_TEXTURE, …) are defined in the locale layer (locales/enUS.lua),
-- which loads first.
-- The GlobalStrings these functions consume (LARGE_NUMBER_SEPERATOR,
-- PERCENTAGE_STRING, GOLD_AMOUNT_TEXTURE, SYMBOLIC_NEGATIVE_NUMBER, ...) are
-- defined in the locale layer (locales/*.lua), which loads first. The coin
-- formats there crop the one 1.12 sprite sheet, UI-MoneyIcons.
function SplitTextIntoLines(text, delimiter)
local lines = {};
local startIndex = 1;
local foundIndex = string.find(text, delimiter);
while foundIndex do
table.insert(lines, text:sub(startIndex, foundIndex - 1));
startIndex = foundIndex + 2;
foundIndex = string.find(text, delimiter, startIndex);
end
if startIndex <= #text then
table.insert(lines, text:sub(startIndex));
end
return lines;
end
function SplitTextIntoHeaderAndNonHeader(text)
local foundIndex = string.find(text, "|n");
if not foundIndex then
-- There was no newline...the whole thing is a header
return text;
elseif #text == 2 then
-- There was a newline, but that was all that was in the string.
return nil;
elseif foundIndex == 1 then
-- There was a newline at the very beginning...the whole rest of the string is a header
return text:sub(3);
elseif foundIndex == #text - 1 then
-- There was a newline at the very end...the whole rest of the string is a header
return text:sub(1, foundIndex - 1);
else
-- There was a newline somewhere in the middle...everything before it is the header and everything after it is the non-header
return text:sub(1, foundIndex - 1), text:sub(foundIndex + 2);
end
end
function FormatValueWithSign(value)
local formatString = value < 0 and SYMBOLIC_NEGATIVE_NUMBER or SYMBOLIC_POSITIVE_NUMBER;
return formatString:format(math.abs(value));
end
function FormatLargeNumber(amount)
amount = tostring(amount);
local newDisplay = "";
local strlen = amount:len();
--Add each thing behind a comma
for i=4, strlen, 3 do
newDisplay = LARGE_NUMBER_SEPERATOR..amount:sub(-(i - 1), -(i - 3))..newDisplay;
end
--Add everything before the first comma
newDisplay = amount:sub(1, (strlen % 3 == 0) and 3 or (strlen % 3))..newDisplay;
return newDisplay;
end
COPPER_PER_SILVER = 100;
SILVER_PER_GOLD = 100;
COPPER_PER_GOLD = COPPER_PER_SILVER * SILVER_PER_GOLD;
function FormatLargeNumber(amount)
amount = tostring(amount);
local newDisplay = "";
local strlen = string.len(amount);
-- Insert a separator before each group of three trailing digits.
for i = 4, strlen, 3 do
newDisplay = LARGE_NUMBER_SEPERATOR ..
string.sub(amount, -(i - 1), -(i - 3)) .. newDisplay;
end
-- Prepend everything before the first separator.
local lead = (math.mod(strlen, 3) == 0) and 3 or math.mod(strlen, 3);
newDisplay = string.sub(amount, 1, lead) .. newDisplay;
return newDisplay;
local COPPER_PER_SILVER = COPPER_PER_SILVER;
local SILVER_PER_GOLD = SILVER_PER_GOLD;
local COPPER_PER_GOLD = COPPER_PER_GOLD;
MoneyStringConstants =
{
SeparateThousands = true,
DontSeparateThousands = false,
CheckGoldThreshold = true,
DontCheckGoldThreshold = false,
ShowZeroAsGold = true,
ShowZeroAsLowerDenomination = false,
};
function GetMoneyString(money, separateThousands, checkGoldThreshold, showZeroAsGold, color)
local goldString, silverString, copperString;
local gold = floor(money / (COPPER_PER_SILVER * SILVER_PER_GOLD));
local silver = floor((money - (gold * COPPER_PER_SILVER * SILVER_PER_GOLD)) / COPPER_PER_SILVER);
local copper = mod(money, COPPER_PER_SILVER);
if (separateThousands) then
goldString = GOLD_AMOUNT_TEXTURE_STRING:format(FormatLargeNumber(gold), 0, 0);
else
goldString = GOLD_AMOUNT_TEXTURE:format(gold, 0, 0);
end
silverString = SILVER_AMOUNT_TEXTURE:format(silver, 0, 0);
copperString = COPPER_AMOUNT_TEXTURE:format(copper, 0, 0);
if showZeroAsGold and ( money == 0 ) then
return color and color:WrapTextInColorCode(goldString) or goldString;
end
copper = FormatDisplayCopper(checkGoldThreshold, gold, silver, copper);
local moneyString = "";
local separator = "";
if ( gold > 0 ) then
moneyString = goldString;
separator = " ";
end
if ( silver > 0 ) then
moneyString = moneyString..separator..silverString;
separator = " ";
end
if ( copper > 0 or moneyString == "" ) then
moneyString = moneyString..separator..copperString;
end
return color and color:WrapTextInColorCode(moneyString) or moneyString;
end
function FormatDisplayCopper(checkGoldThreshold, gold, silver, copper)
return (checkGoldThreshold and gold > Constants.MoneyFormattingConstants.GOLD_REWARD_THRESHOLD_TO_HIDE_COPPER ) and 0 or copper;
end
function FormatPercentage(percentage, roundToNearestInteger)
if roundToNearestInteger then
percentage = Round(percentage * 100);
else
percentage = percentage * 100;
end
return string.format(PERCENTAGE_STRING, percentage);
if roundToNearestInteger then
percentage = Round(percentage * 100);
else
percentage = percentage * 100;
end
return PERCENTAGE_STRING:format(percentage);
end
function FormatPercentageRounded(value)
local roundToNearestInteger = true;
return FormatPercentage(value, roundToNearestInteger);
end
function FormatFraction(numerator, denominator)
return string.format(GENERIC_FRACTION_STRING, numerator, denominator);
return GENERIC_FRACTION_STRING:format(numerator, denominator);
end
function SplitTextIntoLines(text, delimiter)
local lines = {};
local startIndex = 1;
local foundIndex = string.find(text, delimiter);
while foundIndex do
table.insert(lines, string.sub(text, startIndex, foundIndex - 1));
startIndex = foundIndex + 2;
foundIndex = string.find(text, delimiter, startIndex);
end
if startIndex <= string.len(text) then
table.insert(lines, string.sub(text, startIndex));
end
return lines;
function GetHighlightedNumberDifferenceString(baseString, newString)
local outputString = "";
-- output string is being built from the new string
local newStringIndex = 1;
-- find a stretch of digits (including . and , because of different locales) - but has to end in a digit
local PATTERN = "([,%.%d]*%d+)";
local start1, end1, baseNumberString = string.find(baseString, PATTERN);
local start2, end2, newNumberString = string.find(newString, PATTERN);
while start1 and start2 do
-- add from the new string until the matched spot
outputString = outputString .. string.sub(newString, newStringIndex, start2 - 1);
newStringIndex = end2 + 1;
if baseNumberString ~= newNumberString then
-- need to remove , and . before comparing numbers because of locales
local scrubbedBaseNumberString = gsub(baseNumberString, "[,%.]", "");
local scrubbedNewNumberString = gsub(newNumberString, "[,%.]", "");
local baseNumber = tonumber(scrubbedBaseNumberString);
local newNumber = tonumber(scrubbedNewNumberString);
if baseNumber and newNumber then
local delta = newNumber - baseNumber;
if delta > 0 then
newNumberString = GREEN_FONT_COLOR_CODE..string.format(newNumberString)..FONT_COLOR_CODE_CLOSE;
elseif delta < 0 then
newNumberString = RED_FONT_COLOR_CODE..string.format(newNumberString)..FONT_COLOR_CODE_CLOSE;
end
end
end
outputString = outputString..newNumberString;
start1, end1, baseNumberString = string.find(baseString, PATTERN, end1 + 1);
start2, end2, newNumberString = string.find(newString, PATTERN, end2 + 1);
end
outputString = outputString .. string.sub(newString, newStringIndex, string.len(newString));
return outputString;
end
function SplitTextIntoHeaderAndNonHeader(text)
local foundIndex = string.find(text, "|n");
local len = string.len(text);
if not foundIndex then
return text; -- no newline: whole thing is a header
elseif len == 2 then
return nil; -- the string was just a newline
elseif foundIndex == 1 then
return string.sub(text, 3); -- leading newline: rest is a header
elseif foundIndex == len - 1 then
return string.sub(text, 1, foundIndex - 1); -- trailing newline
else
return string.sub(text, 1, foundIndex - 1), string.sub(text, foundIndex + 2);
end
function FormatUnreadMailTooltip(tooltip, headerText, senders)
for i, sender in ipairs(senders) do
headerText = headerText.."\n"..sender;
end
tooltip:SetText(headerText);
end
-- (money, separateThousands [, checkGoldThreshold, showZeroAsGold])
-- Text-symbol form ("12g 34s 56c"); for the coin-icon form use
-- GetCoinTextureString below. The checkGoldThreshold arg is accepted for
-- signature compatibility but not applied (it relied on modern Constants tables).
function GetMoneyString(money, separateThousands, checkGoldThreshold, showZeroAsGold)
local gold = math.floor(money / COPPER_PER_GOLD);
local silver = math.floor(math.mod(money, COPPER_PER_GOLD) / COPPER_PER_SILVER);
local copper = math.mod(money, COPPER_PER_SILVER);
FormattingUtil = {};
local goldString = (separateThousands and FormatLargeNumber(gold) or tostring(gold)) ..
GOLD_AMOUNT_SYMBOL;
local silverString = silver .. SILVER_AMOUNT_SYMBOL;
local copperString = copper .. COPPER_AMOUNT_SYMBOL;
function FormattingUtil.GetCostString(icon, quantity, colorCode, abbreviate)
colorCode = colorCode or HIGHLIGHT_FONT_COLOR_CODE;
if showZeroAsGold and money == 0 then
return goldString;
end
local moneyString = "";
local separator = "";
if gold > 0 then
moneyString = goldString;
separator = " ";
end
if silver > 0 then
moneyString = moneyString .. separator .. silverString;
separator = " ";
end
if copper > 0 or moneyString == "" then
moneyString = moneyString .. separator .. copperString;
end
return moneyString;
local markup = CreateTextureMarkup(icon, 64, 64, 16, 16, 0, 1, 0, 1);
local amountString;
if abbreviate then
amountString = AbbreviateNumbers(quantity);
else
amountString = BreakUpLargeNumbers(quantity);
end
return ("%s%s %s|r"):format(colorCode, amountString, markup);
end
function FormattingUtil.GetItemCostString(itemID, quantity, colorCode, abbreviate)
local icon = C_Item.GetItemIconByID(itemID);
if icon then
return FormattingUtil.GetCostString(icon, quantity, colorCode, abbreviate);
end
return "";
end
function FormattingUtil.AddLeadingZeroes(digits, numZeroes, zeroesColor)
numZeroes = numZeroes or 7;
zeroesColor = zeroesColor or GRAY_FONT_COLOR;
local digitsString = tostring(digits);
local numDigits = #digitsString;
if numDigits < numZeroes then
local leadingZeroes = "";
for i = 1, (numZeroes - numDigits) do
leadingZeroes = leadingZeroes.."0";
end
leadingZeroes = zeroesColor:WrapTextInColorCode(leadingZeroes);
return leadingZeroes .. digits;
end
return digits;
end
function ReplaceGenderTokens(string, gender)
if not strfind(string, "%$") then
return string;
end
-- This is a very simple parser that will only handle $G/$g tokens
return gsub(string, "$[Gg]([^:]+):([^;]+);", "%"..gender);
end
+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 = "新設定";
+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
@@ -0,0 +1,182 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `smf:AddOnDisplayRefreshedCallback(callback)`, retail's: callback(frame)
// runs each time the frame refreshes what it shows.
//
// Retail's mixin (Blizzard_SharedXML/ScrollingMessageFrame.lua) marks its
// display dirty on each change - a message, a scroll, a resize, a fade reset -
// and refreshes in its OnPostUpdate. So the callbacks run once per update of
// a shown frame that changed, in the order they were added, each under the
// error handler (secureexecuterange). The mark is cleared after them, so a
// change a callback makes (a scroll bar setting the offset) does not run them
// again.
//
// 1.12 refreshes what the frame shows in its line layout
// (ScrollingMessageFrame::Layout), which marks the frame. A message added
// while the frame is scrolled up changes no line and runs no layout, but it
// moves the scroll offset and range, so AddMessage marks the frame too, as
// ScrollByAmount does for retail's fade reset. The callbacks run after the
// frame's own update (FUN_SCROLLINGMESSAGEFRAME_ON_UPDATE: OnUpdate, then the
// fader), which the engine's update pass calls once per frame for each shown
// frame - where retail's OnPostUpdate runs.
#include "scrollingmessageframe/DisplayRefreshed.h"
#include "Game.h"
#include "Offsets.h"
#include "scrollingmessageframe/Layout.h"
#include <unordered_map>
namespace ScrollingMessageFrame::DisplayRefreshed {
namespace {
// Frames with callbacks, and whether each has a refresh pending. Every frame
// is rebuilt on /reload, so the map is cleared then.
std::unordered_map<void *, bool> g_frames;
// The frame-table → callback-list table, in the registry of the Lua state
// that made it. LUA_NOREF until first use in each state.
constexpr int kNoRef = -2;
int g_callbacksRef = kNoRef;
// Runs the callbacks of the frame, each as callback(frame) under the
// engine's script error handler.
void RunCallbacks(void *smf) {
using namespace Game::Lua;
void *L = State();
if (L == nullptr || g_callbacksRef == kNoRef)
return;
const int top = GetTop(L);
PushObject(L, smf);
const int frame = GetTop(L);
RawGetI(L, REGISTRY_INDEX, g_callbacksRef);
PushValue(L, frame);
RawGet(L, -2);
const int callbacks = GetTop(L);
if (Type(L, callbacks) == TYPE_TABLE) {
const int errRef = Game::Read<int>(static_cast<uintptr_t>(Offsets::VAR_FRAMESCRIPT_ERROR_HANDLER_REF));
for (int i = 1;; ++i) {
int errIdx = 0;
if (errRef > 0) {
RawGetI(L, REGISTRY_INDEX, errRef);
errIdx = GetTop(L);
}
RawGetI(L, callbacks, i);
if (Type(L, -1) == TYPE_NIL)
break;
PushValue(L, frame);
PCall(L, 1, 0, errIdx);
SetTop(L, callbacks);
}
}
SetTop(L, top);
}
using OnUpdate_t = void(__fastcall *)(void *smf, void *edx, float elapsed);
OnUpdate_t g_origOnUpdate = nullptr;
void __fastcall OnUpdate_h(void *smf, void *edx, float elapsed) {
g_origOnUpdate(smf, edx, elapsed);
const auto it = g_frames.find(smf);
if (it == g_frames.end() || !it->second)
return;
RunCallbacks(smf);
const auto again = g_frames.find(smf); // a callback can add frames
if (again != g_frames.end())
again->second = false;
}
const Game::HookAutoRegister _updateHook{Offsets::FUN_SCROLLINGMESSAGEFRAME_ON_UPDATE,
reinterpret_cast<void *>(&OnUpdate_h),
reinterpret_cast<void **>(&g_origOnUpdate)};
const ScrollingMessageFrame::Layout::AutoSubscribe _layoutSub{&MarkDirty};
int __fastcall Script_AddOnDisplayRefreshedCallback(void *L) {
using namespace Game::Lua;
void *frame = ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
if (Type(L, 2) != TYPE_FUNCTION) {
Error(L, "Usage: %s:AddOnDisplayRefreshedCallback(callback)", ObjectDisplayName(frame));
return 0;
}
if (g_callbacksRef == kNoRef) {
NewTable(L);
g_callbacksRef = Ref(L, REGISTRY_INDEX);
}
RawGetI(L, REGISTRY_INDEX, g_callbacksRef);
const int frames = GetTop(L);
PushValue(L, 1);
RawGet(L, frames);
if (Type(L, -1) != TYPE_TABLE) {
SetTop(L, frames);
NewTable(L);
PushValue(L, 1);
PushValue(L, -2);
RawSet(L, frames); // frames[self] = callbacks
}
const int callbacks = GetTop(L);
int n = 1;
for (;; ++n) {
RawGetI(L, callbacks, n);
const bool end = Type(L, -1) == TYPE_NIL;
SetTop(L, -2);
if (end)
break;
}
PushNumber(L, n);
PushValue(L, 2);
RawSet(L, callbacks);
g_frames.emplace(frame, false);
return 0;
}
const Game::Lua::FrameMethodEntry kMethods[] = {
{"AddOnDisplayRefreshedCallback", &Script_AddOnDisplayRefreshedCallback},
};
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("callback", "function", "Called as callback(frame)."),
};
const Game::Doc::Function kAddOnDisplayRefreshedCallback{
"Adds a function that runs after the frame refreshes the messages it shows.", kArgs, {}};
const Game::Doc::Method kDocs[] = {
{"AddOnDisplayRefreshedCallback", &kAddOnDisplayRefreshedCallback},
};
void RegisterLuaFunctions() {
// Each registration pass is a new Lua state; the old state's ref is gone.
g_callbacksRef = kNoRef;
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY),
kMethods, 1, kDocs, 1);
}
void PrepareForReload() { g_frames.clear(); }
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
const Game::ReloadAutoRegister _reload{&PrepareForReload};
} // namespace
void MarkDirty(void *smf) {
const auto it = g_frames.find(smf);
if (it != g_frames.end())
it->second = true;
}
} // namespace ScrollingMessageFrame::DisplayRefreshed
@@ -0,0 +1,22 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
namespace ScrollingMessageFrame::DisplayRefreshed {
// Marks a change to what the frame shows that its line layout does not
// report; the frame's display-refreshed callbacks run at its next update.
void MarkDirty(void *smf);
} // namespace ScrollingMessageFrame::DisplayRefreshed
+194
View File
@@ -0,0 +1,194 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Retail's insert mode on ScrollingMessageFrame: `SetInsertMode(mode)`,
// `GetInsertMode()` and the SCROLLING_MESSAGE_FRAME_INSERT_MODE_* globals.
//
// Retail's mixin (Blizzard_SharedXML/ScrollingMessageFrame.lua, RefreshLayout)
// stacks the lines up from the bottom in BOTTOM mode, the newest message at
// the bottom, and down from the top in TOP mode: the first line's TOPLEFT on
// the frame's TOPLEFT, each next line's TOPLEFT on the BOTTOMLEFT of the line
// before, the spacing below it. The scroll offset counts from the newest
// message in both modes. Setting the mode the frame has does nothing; a change
// lays every line out again and refreshes the display.
//
// 1.12's line layout is retail's BOTTOM mode. It anchors a line once, when it
// makes the line, through one SetPoint call (Offsets.h,
// CALL_SMF_LAYOUT_ANCHOR_LINE), so that call is redirected: a line of a frame
// in TOP mode takes the TOP anchors, every other line the engine's own. The
// layout makes the lines again when the frame changes size, which the
// redirect covers too. When a frame's mode changes, its lines are anchored
// again in order with the layout's own steps: clear the points, set the
// point, lay the line out now. The hyperlink buttons and inline textures are
// anchored to the lines, so they move with them. Retail's frame has no text
// insets; the TOP mode's first line sits the top inset below the frame's top,
// as the BOTTOM mode's sits the bottom inset above its bottom.
#include "Common.h"
#include "Game.h"
#include "Offsets.h"
#include "frame/Destruction.h"
#include "scrollingmessageframe/DisplayRefreshed.h"
#include <cstdint>
#include <unordered_set>
namespace ScrollingMessageFrame::InsertMode {
namespace {
// SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP / _BOTTOM.
constexpr int kInsertModeTop = 1;
constexpr int kInsertModeBottom = 2;
using SetPoint_t = void(__thiscall *)(void *anchor, int point, void *relAnchor, int relPoint, float x, float y,
int flag);
using ClearAllPoints_t = void(__fastcall *)(void *anchor);
using Resize_t = void(__fastcall *)(void *anchor, void *edx, int force);
using PixelAlignY_t = float(__stdcall *)(float length);
// The frames in TOP mode. Every frame is rebuilt on /reload, so the set is
// cleared then.
std::unordered_set<void *> g_topFrames;
bool IsTop(void *smf) { return g_topFrames.count(smf) != 0; }
void *AnchorOf(void *region) { return static_cast<uint8_t *>(region) + Offsets::OFF_REGION_ANCHOR; }
// Sets a line's anchor as the layout gives it - (BOTTOMLEFT, the frame,
// BOTTOMLEFT, x, y) for the first line, (BOTTOMLEFT, the line before, TOPLEFT,
// x, y) for the others - or, for a frame in TOP mode, its mirror.
void SetLinePoint(void *smf, void *anchor, int point, void *relative, int relativePoint, float x, float y,
int flag) {
if (IsTop(smf)) {
point = Offsets::FRAMEPOINT_TOPLEFT;
if (relativePoint == Offsets::FRAMEPOINT_BOTTOMLEFT) { // the first line, on the frame
relativePoint = Offsets::FRAMEPOINT_TOPLEFT;
y = -Game::Read<float>(smf, Offsets::OFF_SCROLLINGMESSAGEFRAME_INSET_TOP);
} else {
relativePoint = Offsets::FRAMEPOINT_BOTTOMLEFT;
y = -y;
}
}
reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT)(anchor, point, relative, relativePoint, x, y, flag);
}
// The layout's SetPoint for a line it has just made; the line's parent is
// the frame.
void __fastcall AnchorNewLine(void *anchor, void *, int point, void *relative, int relativePoint, float x, float y,
int flag) {
void *line = static_cast<uint8_t *>(anchor) - Offsets::OFF_REGION_ANCHOR;
SetLinePoint(Game::Read<void *>(line, Offsets::OFF_REGION_PARENT), anchor, point, relative, relativePoint, x, y,
flag);
}
// Anchors the frame's lines again, in order, with the layout's arguments and
// steps.
void AnchorLines(void *smf) {
const int count = Game::Read<int>(smf, Offsets::OFF_SMF_LINE_COUNT);
const auto *lines = Game::Read<const uint8_t *>(smf, Offsets::OFF_SMF_LINE_ARRAY);
void *previous = nullptr;
for (int i = 0; i < count; ++i) {
void *line = Game::Read<void *>(lines + i * Offsets::SMF_LINE_STRIDE, Offsets::OFF_SMF_LINE_FONTSTRING);
void *anchor = AnchorOf(line);
reinterpret_cast<ClearAllPoints_t>(Offsets::FUN_LAYOUT_CLEAR_ALL_POINTS)(anchor);
if (previous == nullptr)
SetLinePoint(smf, anchor, Offsets::FRAMEPOINT_BOTTOMLEFT, AnchorOf(smf), Offsets::FRAMEPOINT_BOTTOMLEFT,
Game::Read<float>(smf, Offsets::OFF_SCROLLINGMESSAGEFRAME_INSET_LEFT),
Game::Read<float>(smf, Offsets::OFF_SCROLLINGMESSAGEFRAME_INSET_BOTTOM), 1);
else
SetLinePoint(smf, anchor, Offsets::FRAMEPOINT_BOTTOMLEFT, AnchorOf(previous), Offsets::FRAMEPOINT_TOPLEFT,
0.0f,
reinterpret_cast<PixelAlignY_t>(Offsets::FUN_PIXEL_ALIGN_Y)(
Game::Read<float>(line, Offsets::OFF_FONTSTRING_SPACING)),
1);
reinterpret_cast<Resize_t>(Offsets::FUN_LAYOUT_RESIZE)(anchor, nullptr, 1);
previous = line;
}
}
int __fastcall Script_SetInsertMode(void *L) {
void *frame = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
const double mode = Game::Lua::IsNumber(L, 2) ? Game::Lua::ToNumber(L, 2) : 0;
if (mode != kInsertModeTop && mode != kInsertModeBottom) {
Game::Lua::Error(L,
"Usage: %s:SetInsertMode(SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP or "
"SCROLLING_MESSAGE_FRAME_INSERT_MODE_BOTTOM)",
Game::Lua::ObjectDisplayName(frame));
return 0;
}
const bool top = mode == kInsertModeTop;
if (top == IsTop(frame))
return 0;
if (top)
g_topFrames.insert(frame);
else
g_topFrames.erase(frame);
AnchorLines(frame);
DisplayRefreshed::MarkDirty(frame);
return 0;
}
int __fastcall Script_GetInsertMode(void *L) {
void *frame = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
Game::Lua::PushNumber(L, IsTop(frame) ? kInsertModeTop : kInsertModeBottom);
return 1;
}
const Game::Lua::FrameMethodEntry kMethods[] = {
{"SetInsertMode", &Script_SetInsertMode},
{"GetInsertMode", &Script_GetInsertMode},
};
const Game::Doc::Field kModeArgs[] = {
Game::Doc::Req("mode", "number",
"SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP or SCROLLING_MESSAGE_FRAME_INSERT_MODE_BOTTOM."),
};
const Game::Doc::Field kModeRets[] = {
Game::Doc::Req("mode", "number", "BOTTOM until SetInsertMode changes it."),
};
const Game::Doc::Function kSetInsertMode{
"Sets where the newest message shows: at the top, with older messages below it, or at the bottom.", kModeArgs,
{}};
const Game::Doc::Function kGetInsertMode{"Returns where the newest message shows.", {}, kModeRets};
const Game::Doc::Method kDocs[] = {
{"SetInsertMode", &kSetInsertMode},
{"GetInsertMode", &kGetInsertMode},
};
void RegisterLuaFunctions() {
void *L = Game::Lua::State();
Game::Lua::SetGlobalNumber(L, "SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP", kInsertModeTop);
Game::Lua::SetGlobalNumber(L, "SCROLLING_MESSAGE_FRAME_INSERT_MODE_BOTTOM", kInsertModeBottom);
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY),
kMethods, 2, kDocs, 2);
Common::RedirectCall(Offsets::CALL_SMF_LAYOUT_ANCHOR_LINE, Offsets::FUN_REGION_SET_POINT,
reinterpret_cast<const void *>(&AnchorNewLine));
}
void PrepareForReload() { g_topFrames.clear(); }
void OnFrameDestroyed(void *frame) { g_topFrames.erase(frame); }
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
const Game::ReloadAutoRegister _reload{&PrepareForReload};
const Frame::Destruction::AutoSubscribe _destructionSub{&OnFrameDestroyed};
} // namespace
} // namespace ScrollingMessageFrame::InsertMode
+41
View File
@@ -0,0 +1,41 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "scrollingmessageframe/Layout.h"
#include "Game.h"
#include "Offsets.h"
namespace ScrollingMessageFrame::Layout {
namespace {
AutoSubscribe *g_head = nullptr;
using Layout_t = void(__fastcall *)(void *smf, void *edx, int newestRecord);
Layout_t g_origLayout = nullptr;
void __fastcall Layout_h(void *smf, void *edx, int newestRecord) {
g_origLayout(smf, edx, newestRecord);
for (const AutoSubscribe *s = g_head; s != nullptr; s = s->next)
s->fn(smf);
}
const Game::HookAutoRegister _hook{Offsets::FUN_SMF_DISPLAY_REFRESH, reinterpret_cast<void *>(&Layout_h),
reinterpret_cast<void **>(&g_origLayout)};
} // namespace
AutoSubscribe::AutoSubscribe(Listener fn) : fn(fn), next(g_head) { g_head = this; }
} // namespace ScrollingMessageFrame::Layout
+35
View File
@@ -0,0 +1,35 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
// Shared observer for the ScrollingMessageFrame line layout
// FUN_SMF_DISPLAY_REFRESH (Offsets.h), which lays out the lines the frame
// shows after every change to them: a message added at the bottom, a scroll,
// a resize, a font change, Clear. A listener runs after it, with the frame.
//
// MinHook allows ONE hook per address and more than one feature follows the
// layout (Text::InlineTexture's early icon apply, the display-refreshed
// callbacks), so this module owns the hook and fans out. Declare a file-scope
// static const ScrollingMessageFrame::Layout::AutoSubscribe _sub{&OnLayout};
namespace ScrollingMessageFrame::Layout {
using Listener = void (*)(void *smf);
struct AutoSubscribe {
explicit AutoSubscribe(Listener fn);
Listener fn;
AutoSubscribe *next;
};
} // namespace ScrollingMessageFrame::Layout
+494
View File
@@ -0,0 +1,494 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `ScrollingMessageFrame:GetMessageInfo(index)`, retail's: the message's
// text, its colour, then every argument AddMessage took after the colour.
//
// Retail's ScrollingMessageFrame is a Lua mixin whose AddMessage(message, r,
// g, b, ...) packs the call into an entry and GetMessageInfo unpacks it
// (Blizzard_SharedXML/ScrollingMessageFrame.lua). Index 1 is the oldest
// message; the extra arguments come back as given, nils and count included;
// an index outside 1..GetNumMessages() returns nothing. (4.3.4's C method
// returns text, accessID, typeID, extraData instead.)
//
// The text and colour come from the engine's own record of the message
// (Offsets.h, OFF_SCROLLINGMESSAGEFRAME_MESSAGES), so a colour
// UpdateColorByID changed reads back changed, as retail's AdjustMessageColors
// does. The record keeps the colour as bytes, so a component reads back as
// byte/255. It keeps the first extra argument only as an integer (0 for
// anything else), so the extra arguments are kept beside the ring instead.
//
// AddMessage is registered again on the ScrollingMessageFrame registry, over
// the engine's. The registrar (0x00701D80) adds a node for the name whatever
// is there; Storm's TSHashTable::InternalNew links a new node at the head of
// its bucket (NewNode(1)), and the method lookup (0x00702000) returns the
// first match, so this one answers. It runs the engine's AddMessage, then
// records the extra arguments for the record the engine wrote. The engine
// adds a message exactly when the text is a nonempty string
// (0x0079299C..0x007929C7: lua_isstring, lua_tostring, first byte); a bad
// `self` raises a Lua error instead. The ring's one writer has no other
// caller, so every record was written through here.
//
// The extra arguments live in a registry table keyed by the frame's Lua table,
// then by record. A record's table is refilled when the record is overwritten,
// so a full ring makes no garbage.
//
// `BackFillMessage(text, r, g, b, ...)`, retail's, adds a message as the
// OLDEST — the combat log's refilter walks its history newest to oldest and
// fills the frame behind the lines it already shows (Blizzard_CombatLog's
// RefilterUpdate). It writes the ring itself; see the function.
//
// `RemoveMessagesByPredicate(predicate)`, retail's, removes the messages the
// predicate selects by their GetMessageInfo values. The ring has no
// compaction of its own; 4.3.4's RemoveLines loop is run over it, the
// extra-argument tables moving with their records. See the function.
#include "Game.h"
#include "Offsets.h"
#include "scrollingmessageframe/DisplayRefreshed.h"
#include <cstdint>
#include <cstring>
#include <vector>
namespace ScrollingMessageFrame::MessageInfo {
namespace {
constexpr int kFirstExtra = 6; // self, text, r, g, b, then the extra arguments
// The frame-table → record → extra-arguments table, in the registry of the Lua
// state that made it. LUA_NOREF until first use in each state.
constexpr int kNoRef = -2;
int g_extrasRef = kNoRef;
// Pushes the record → extra-arguments table of the frame whose Lua table is
// at stack slot 1, or nil when it has none and `create` is false.
void PushRecords(void *L, bool create) {
using namespace Game::Lua;
if (g_extrasRef == kNoRef) {
if (!create) {
PushNil(L);
return;
}
NewTable(L);
g_extrasRef = Ref(L, REGISTRY_INDEX);
}
RawGetI(L, REGISTRY_INDEX, g_extrasRef); // [frames]
PushValue(L, 1);
RawGet(L, -2); // [frames, records]
if (Type(L, -1) != TYPE_TABLE && create) {
SetTop(L, -2);
NewTable(L);
PushValue(L, 1);
PushValue(L, -2);
RawSet(L, -4); // frames[self] = records
}
Remove(L, -2); // [records]
}
// Pushes the extra-argument table of record `record` of the frame whose Lua
// table is at stack slot 1, or nil when it has none and `create` is false.
void PushExtras(void *L, int record, bool create) {
using namespace Game::Lua;
PushRecords(L, create); // [records]
if (Type(L, -1) != TYPE_TABLE)
return;
RawGetI(L, -1, record); // [records, extras]
if (Type(L, -1) != TYPE_TABLE && create) {
SetTop(L, -2);
NewTable(L);
PushNumber(L, record);
PushValue(L, -2);
RawSet(L, -4); // records[record] = extras
}
Remove(L, -2); // [extras]
}
// The `n` of the extra-argument table at absolute stack slot `extras`.
int ExtraCount(void *L, int extras) {
using namespace Game::Lua;
PushString(L, "n");
RawGet(L, extras);
const int n = static_cast<int>(ToNumber(L, -1));
SetTop(L, -2);
return n;
}
// Stores stack slots kFirstExtra..top as the extra arguments of `record`.
void StoreExtras(void *L, int record, int top) {
using namespace Game::Lua;
const int count = top >= kFirstExtra ? top - kFirstExtra + 1 : 0;
PushExtras(L, record, count > 0);
const int extras = GetTop(L);
if (Type(L, extras) == TYPE_TABLE) {
const int was = ExtraCount(L, extras);
for (int i = 1; i <= count; ++i) {
PushNumber(L, i);
PushValue(L, kFirstExtra - 1 + i);
RawSet(L, extras);
}
for (int i = count + 1; i <= was; ++i) {
PushNumber(L, i);
PushNil(L);
RawSet(L, extras);
}
PushString(L, "n");
PushNumber(L, count);
RawSet(L, extras);
}
SetTop(L, -2);
}
int __fastcall Script_AddMessage(void *L) {
const int top = Game::Lua::GetTop(L);
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_SCROLLINGMESSAGEFRAME_ADD_MESSAGE)(L);
const char *text = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : nullptr;
if (text == nullptr || *text == '\0')
return 0;
void *frame = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
StoreExtras(L, Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS), top);
// Scrolled up, the engine adds the message without a layout.
DisplayRefreshed::MarkDirty(frame);
return 0;
}
// AddMessage's colour byte (0x00792A1B..0x00792ACC): the value clamped to
// 0..1 (NaN to 1), then trunc(v × 255 + 0.5).
uint32_t ColorByte(double value) {
double clamped = 1.0;
if (value < 0.0)
clamped = 0.0;
else if (value < 1.0)
clamped = static_cast<float>(value);
return static_cast<uint32_t>(static_cast<int>(clamped * 255.0 + 0.5)) & 0xFFu;
}
// The colour AddMessage builds from stack slots 3..5 — alpha 0xFF, R, G, B —
// or the frame font's when they are not all numbers (0x007929CD..0x00792B45).
uint32_t MessageColor(void *L, void *frame) {
using namespace Game::Lua;
if (IsNumber(L, 3) && IsNumber(L, 4) && IsNumber(L, 5))
return 0xFF000000u | (ColorByte(ToNumber(L, 3)) << 16) | (ColorByte(ToNumber(L, 4)) << 8) |
ColorByte(ToNumber(L, 5));
const void *font = Game::Read<const void *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_FONT);
return font != nullptr ? Game::Read<uint32_t>(font, Offsets::OFF_SCROLLINGMESSAGEFRAME_FONT_COLOR) : 0xFFFFFFFFu;
}
// `ScrollingMessageFrame:BackFillMessage(text, r, g, b, ...)`, retail's: the
// message becomes the OLDEST. Retail's mixin pushes the entry at the back of
// its history buffer and drops it when the buffer is full ("Won't overwrite
// front"); 4.3.4's AddMessage(..., addToTop) writes the same slot but, full,
// overwrites the oldest. The slot is 4.3.4's: once the count has grown, the
// free one before the oldest, `(pos - count + 1) mod max` (pos 0 for an empty
// ring), and the record is written as the ring writer 0x00788150 writes it —
// the text a Storm copy (the ring frees its texts with SMemFree), AddMessage's
// colour, shown, the frame's two times, id 0. Nothing already in the ring
// moves, so a scrolled-up frame keeps its view; at the bottom the lines are
// laid out again from the newest, as the writer does, which shows the new
// line when there is room above the others.
int __fastcall Script_BackFillMessage(void *L) {
using namespace Game::Lua;
using StormAlloc_t = void *(__stdcall *)(size_t size, const char *file, int line, int flags);
using StormFree_t = int(__stdcall *)(void *ptr, const char *file, int line, int flags);
using Layout_t = void(__fastcall *)(void *smf, void *edx, int newestRecord);
static const char kStormFile[] = "ClassicAPI ScrollingMessageFrame:BackFillMessage";
const int top = GetTop(L);
void *frame = ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
const char *text = IsString(L, 2) ? ToString(L, 2) : nullptr;
if (text == nullptr || *text == '\0')
return 0;
const int max = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES);
int &count = Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
if (max <= 0 || count >= max)
return 0; // full: the message is older than every kept one
const uint32_t color = MessageColor(L, frame);
const size_t bytes = std::strlen(text) + 1;
char *copy = static_cast<char *>(
reinterpret_cast<StormAlloc_t>(Offsets::FUN_STORM_SMEM_ALLOC)(bytes, kStormFile, __LINE__, 0));
if (copy == nullptr)
return 0;
std::memcpy(copy, text, bytes);
++count;
int &pos = Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS);
if (pos == -1)
pos = 0;
int slot = pos - count + 1;
if (slot < 0)
slot += max;
uint8_t *record = Game::Read<uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES) +
slot * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
char *&stored = *reinterpret_cast<char **>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT);
if (stored != nullptr)
reinterpret_cast<StormFree_t>(Offsets::FUN_STORM_SMEM_FREE)(stored, kStormFile, __LINE__, 0);
stored = copy;
*reinterpret_cast<uint32_t *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COLOR) = color;
*reinterpret_cast<int *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_SHOWN) = 1;
*reinterpret_cast<uint32_t *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_VISIBLE_TIME) =
Game::Read<uint32_t>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_VISIBLE_TIME);
*reinterpret_cast<uint32_t *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_FADE_TIME) =
Game::Read<uint32_t>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_FADE_TIME);
*reinterpret_cast<int *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_ID) = 0;
StoreExtras(L, slot, top);
if (Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_AT_BOTTOM) != 0)
reinterpret_cast<Layout_t>(Offsets::FUN_SMF_DISPLAY_REFRESH)(frame, nullptr, pos);
// Scrolled up, the message lands without a layout.
DisplayRefreshed::MarkDirty(frame);
return 0;
}
// The record of message `index`, 1 the oldest: (position - count + index),
// plus the limit when negative.
int RecordOf(void *frame, int index) {
const int count = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
int record = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS) - count + index;
if (record < 0)
record += Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES);
return record;
}
// Pushes a record's GetMessageInfo values — its text, r, g, b, then its extra
// arguments — for the frame whose Lua table is at stack slot 1, and returns
// how many.
int PushMessage(void *L, void *frame, int record) {
using namespace Game::Lua;
const auto *message = Game::Read<const uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES) +
record * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
const uint32_t color = Game::Read<uint32_t>(message, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COLOR);
PushExtras(L, record, false);
const int extras = GetTop(L);
const int n = Type(L, extras) == TYPE_TABLE ? ExtraCount(L, extras) : 0;
CheckStack(L, 4 + n);
PushString(L, Game::Read<const char *>(message, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT));
PushNumber(L, ((color >> 16) & 0xFF) / 255.0);
PushNumber(L, ((color >> 8) & 0xFF) / 255.0);
PushNumber(L, (color & 0xFF) / 255.0);
for (int i = 1; i <= n; ++i)
RawGetI(L, extras, i);
Remove(L, extras);
return 4 + n;
}
int __fastcall Script_GetMessageInfo(void *L) {
using namespace Game::Lua;
void *frame = ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
if (!IsNumber(L, 2)) {
Error(L, "Usage: %s:GetMessageInfo(index)", ObjectDisplayName(frame));
return 0;
}
const double index = ToNumber(L, 2);
const int count = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
if (!(index >= 1 && index <= count) || index != static_cast<int>(index))
return 0;
return PushMessage(L, frame, RecordOf(frame, static_cast<int>(index)));
}
// `ScrollingMessageFrame:RemoveMessagesByPredicate(predicate)`, retail's: the
// predicate is called with each message's GetMessageInfo values, oldest
// first, and the messages it returns true for are removed; the rest keep
// their order. Retail's mixin rebuilds its history buffer and re-renders at
// the kept scroll offset. The removal is 4.3.4's RemoveLines over 1.12's
// ring: every kept record moves forward in ring order to the first free slot
// (freeing the text it lands on, nulling the one it left), the position moves
// back by the removed count, and the extra-argument tables move with their
// records. Then the engine's own layout from the record at the kept offset
// (clamped to the oldest) and its re-arm — a layout at the bottom keeps the
// fade state, so 4.3.4 re-arms after — or its Clear when nothing is kept.
//
// The predicate runs for every message before anything moves (retail builds
// its new list first). A predicate that errors keeps that message, as
// retail's safesecurecall does; the engine's error handler reports it.
int __fastcall Script_RemoveMessagesByPredicate(void *L) {
using namespace Game::Lua;
using Layout_t = void(__fastcall *)(void *smf, void *edx, int newestRecord);
using Smf_t = void(__fastcall *)(void *smf);
using ScrollOffset_t = int(__fastcall *)(void *smf);
using StormFree_t = int(__stdcall *)(void *ptr, const char *file, int line, int flags);
static const char kStormFile[] = "ClassicAPI ScrollingMessageFrame:RemoveMessagesByPredicate";
void *frame = ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
if (Type(L, 2) != TYPE_FUNCTION) {
Error(L, "Usage: %s:RemoveMessagesByPredicate(predicate)", ObjectDisplayName(frame));
return 0;
}
const int count = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
const int max = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES);
if (count <= 0 || max <= 0)
return 0;
const int top = GetTop(L);
// The predicate over every message, through the engine's error handler.
int errIdx = 0;
const int errRef = Game::Read<int>(static_cast<uintptr_t>(Offsets::VAR_FRAMESCRIPT_ERROR_HANDLER_REF));
if (errRef > 0) {
RawGetI(L, REGISTRY_INDEX, errRef);
errIdx = GetTop(L);
}
std::vector<uint8_t> remove(static_cast<size_t>(count), 0);
int kept = count;
for (int i = 0; i < count; ++i) {
CheckStack(L, 2);
PushValue(L, 2);
const int args = PushMessage(L, frame, RecordOf(frame, i + 1));
if (PCall(L, args, 1, errIdx) == 0 && ToBoolean(L, -1) != 0) {
remove[static_cast<size_t>(i)] = 1;
--kept;
}
SetTop(L, -2);
}
SetTop(L, top);
if (kept == count)
return 0;
// 4.3.4's RemoveLines: compact the kept records forward in ring order.
const int offset = reinterpret_cast<ScrollOffset_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET)(frame);
int &pos = Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS);
uint8_t *records = Game::Read<uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES);
PushRecords(L, false);
const int table = GetTop(L);
const bool extras = Type(L, table) == TYPE_TABLE;
int src = pos - count + 1;
if (src < 0)
src += max;
int dst = src;
for (int i = 0; i < count; ++i) {
if (!remove[static_cast<size_t>(i)]) {
if (src != dst) {
uint8_t *to = records + dst * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
uint8_t *from = records + src * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
char *&landedOn = *reinterpret_cast<char **>(to + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT);
if (landedOn != nullptr)
reinterpret_cast<StormFree_t>(Offsets::FUN_STORM_SMEM_FREE)(landedOn, kStormFile, __LINE__, 0);
std::memcpy(to, from, Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE);
*reinterpret_cast<char **>(from + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT) = nullptr;
if (extras) {
PushNumber(L, dst);
RawGetI(L, table, src);
RawSet(L, table); // records[dst] = records[src]
PushNumber(L, src);
PushNil(L);
RawSet(L, table); // records[src] = nil
}
}
dst = (dst + 1) % max;
} else if (extras) {
PushNumber(L, src);
PushNil(L);
RawSet(L, table); // records[src] = nil
}
src = (src + 1) % max;
}
SetTop(L, top);
if (kept == 0) {
reinterpret_cast<Smf_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_CLEAR)(frame);
DisplayRefreshed::MarkDirty(frame);
return 0;
}
pos += kept - count;
if (pos < 0)
pos += max;
Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT) = kept;
// The lines again from the record at the kept offset, then re-armed.
const int shown = (offset < kept) ? (offset < 0 ? 0 : offset) : kept - 1;
int record = pos - shown;
if (record < 0)
record += max;
reinterpret_cast<Layout_t>(Offsets::FUN_SMF_DISPLAY_REFRESH)(frame, nullptr, record);
reinterpret_cast<Smf_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_REARM_LINES)(frame);
DisplayRefreshed::MarkDirty(frame);
return 0;
}
const Game::Lua::FrameMethodEntry kMethods[] = {
{"AddMessage", &Script_AddMessage},
{"BackFillMessage", &Script_BackFillMessage},
{"GetMessageInfo", &Script_GetMessageInfo},
{"RemoveMessagesByPredicate", &Script_RemoveMessagesByPredicate},
};
const Game::Doc::Field kAddMessageArgs[] = {
Game::Doc::Req("text", "string"),
Game::Doc::Opt("r", "number", nullptr, "Without r, g and b the message takes the frame's font colour."),
Game::Doc::Opt("g", "number"),
Game::Doc::Opt("b", "number"),
Game::Doc::Vararg("any", "Kept with the message; GetMessageInfo returns them after its colour."),
};
const Game::Doc::Function kAddMessage{"Adds a message to the frame.", kAddMessageArgs, {}};
const Game::Doc::Field kBackFillMessageArgs[] = {
Game::Doc::Req("text", "string"),
Game::Doc::Opt("r", "number", nullptr, "Without r, g and b the message takes the frame's font colour."),
Game::Doc::Opt("g", "number"),
Game::Doc::Opt("b", "number"),
Game::Doc::Vararg("any", "Kept with the message; GetMessageInfo returns them after its colour."),
};
const Game::Doc::Function kBackFillMessage{
"Adds a message as the oldest, to fill in history behind the messages the frame has. A frame at "
"its line limit drops it.",
kBackFillMessageArgs, {}};
const Game::Doc::Field kGetMessageInfoArgs[] = {
Game::Doc::Req("index", "luaIndex", "1 is the oldest message."),
};
const Game::Doc::Field kGetMessageInfoRets[] = {
Game::Doc::Req("text", "string"),
Game::Doc::Req("r", "number"),
Game::Doc::Req("g", "number"),
Game::Doc::Req("b", "number"),
Game::Doc::Vararg("any", "The arguments AddMessage took after the colour."),
};
const Game::Doc::Function kGetMessageInfo{
"Returns a message's text, its colour, and the extra arguments it was added with, or nothing "
"when there is no message at that index.",
kGetMessageInfoArgs, kGetMessageInfoRets};
const Game::Doc::Field kRemoveMessagesByPredicateArgs[] = {
Game::Doc::Req("predicate", "function",
"Called with each message's GetMessageInfo values, oldest first; true removes the message."),
};
const Game::Doc::Function kRemoveMessagesByPredicate{
"Removes the messages the predicate returns true for. The rest keep their order, and the frame "
"keeps its scroll offset.",
kRemoveMessagesByPredicateArgs, {}};
const Game::Doc::Method kDocs[] = {
{"AddMessage", &kAddMessage},
{"BackFillMessage", &kBackFillMessage},
{"GetMessageInfo", &kGetMessageInfo},
{"RemoveMessagesByPredicate", &kRemoveMessagesByPredicate},
};
void RegisterLuaFunctions() {
// Each registration pass is a new Lua state; the old state's ref is gone.
g_extrasRef = kNoRef;
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY),
kMethods, 4, kDocs, 4);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace ScrollingMessageFrame::MessageInfo
+142
View File
@@ -0,0 +1,142 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Retail's scroll offset on ScrollingMessageFrame: `SetScrollOffset(offset)`,
// `GetScrollOffset()`, `GetMaxScrollRange()` and `ScrollByAmount(amount)`.
//
// Retail's offset (Blizzard_SharedXML/ScrollingMessageFrame.lua) counts the
// lines the frame is scrolled up from the newest message, 0 at the bottom, up
// to GetMaxScrollRange() = max(GetNumMessages() - 1, 0), where the oldest
// message is the bottom line - the place 1.12's own ScrollToTop goes. 1.12
// keeps the same offset: ScrollToOffset and ScrollOffset (Offsets.h) are its
// two directions, and GetScrollOffset is the engine's GetCurrentScroll under
// retail's name. Retail clamps the offset to its range; the engine wraps one
// outside it, so the setters clamp first. SetScrollOffset to the offset the
// frame has does nothing, as on retail. ScrollByAmount always calls the
// engine, which re-arms the shown lines' fade when the offset stays: retail's
// ResetAllFadeTimes.
#include "Game.h"
#include "Offsets.h"
#include "scrollingmessageframe/DisplayRefreshed.h"
namespace ScrollingMessageFrame::Scroll {
namespace {
using ScrollToOffset_t = void(__fastcall *)(void *smf, void *edx, int offset);
using ScrollOffset_t = int(__fastcall *)(void *smf);
int ScrollOffset(void *smf) {
return reinterpret_cast<ScrollOffset_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET)(smf);
}
int MaxScrollRange(void *smf) {
const int count = Game::Read<int>(smf, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
return count > 1 ? count - 1 : 0;
}
// `offset` clamped to 0..MaxScrollRange (NaN to 0).
int Clamped(void *smf, double offset) {
const int max = MaxScrollRange(smf);
return offset >= 0 ? (offset < max ? static_cast<int>(offset) : max) : 0;
}
void ScrollTo(void *smf, int offset) {
reinterpret_cast<ScrollToOffset_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_TO_OFFSET)(smf, nullptr, offset);
}
void *ResolveSmf(void *L) {
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
}
int __fastcall Script_SetScrollOffset(void *L) {
void *frame = ResolveSmf(L);
if (frame == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: %s:SetScrollOffset(offset)", Game::Lua::ObjectDisplayName(frame));
return 0;
}
const int offset = Clamped(frame, Game::Lua::ToNumber(L, 2));
if (offset != ScrollOffset(frame))
ScrollTo(frame, offset);
return 0;
}
int __fastcall Script_GetMaxScrollRange(void *L) {
void *frame = ResolveSmf(L);
if (frame == nullptr)
return 0;
Game::Lua::PushNumber(L, MaxScrollRange(frame));
return 1;
}
int __fastcall Script_ScrollByAmount(void *L) {
void *frame = ResolveSmf(L);
if (frame == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: %s:ScrollByAmount(amount)", Game::Lua::ObjectDisplayName(frame));
return 0;
}
ScrollTo(frame, Clamped(frame, ScrollOffset(frame) + Game::Lua::ToNumber(L, 2)));
DisplayRefreshed::MarkDirty(frame);
return 0;
}
const Game::Lua::FrameMethodEntry kMethods[] = {
{"SetScrollOffset", &Script_SetScrollOffset},
{"GetScrollOffset",
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_SCROLLINGMESSAGEFRAME_GET_CURRENT_SCROLL)},
{"GetMaxScrollRange", &Script_GetMaxScrollRange},
{"ScrollByAmount", &Script_ScrollByAmount},
};
const Game::Doc::Field kOffsetArgs[] = {
Game::Doc::Req("offset", "number", "Lines from the newest message; clamped to 0..GetMaxScrollRange()."),
};
const Game::Doc::Field kOffsetRets[] = {
Game::Doc::Req("offset", "number", "Lines from the newest message; 0 when not scrolled."),
};
const Game::Doc::Field kRangeRets[] = {
Game::Doc::Req("maxScrollRange", "number"),
};
const Game::Doc::Field kAmountArgs[] = {
Game::Doc::Req("amount", "number", "Positive scrolls toward older messages."),
};
const Game::Doc::Function kSetScrollOffset{"Scrolls the frame to a number of lines from the newest message.",
kOffsetArgs, {}};
const Game::Doc::Function kGetScrollOffset{"Returns how many lines the frame is scrolled from the newest message.",
{}, kOffsetRets};
const Game::Doc::Function kGetMaxScrollRange{"Returns the largest scroll offset: one less than the message count.",
{}, kRangeRets};
const Game::Doc::Function kScrollByAmount{"Scrolls the frame by a number of lines and shows the faded lines again.",
kAmountArgs, {}};
const Game::Doc::Method kDocs[] = {
{"SetScrollOffset", &kSetScrollOffset},
{"GetScrollOffset", &kGetScrollOffset},
{"GetMaxScrollRange", &kGetMaxScrollRange},
{"ScrollByAmount", &kScrollByAmount},
};
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY),
kMethods, 4, kDocs, 4);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace ScrollingMessageFrame::Scroll
+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
+31 -9
View File
@@ -79,6 +79,7 @@
#include "Common.h"
#include "Game.h"
#include "Offsets.h"
#include "scrollingmessageframe/Layout.h"
#include "text/InlineTexturePool.h"
#include "text/PtrProbe.h"
#include "texture/Atlas.h"
@@ -2085,7 +2086,8 @@ static const Game::HookAutoRegister _paintHook{Offsets::FUN_TEXT_PAINT,
// So a line's icons are recorded — and its regions placed — one frame AFTER its
// glyphs draw, because a frame's textures (our icon regions) draw before its
// fontstring glyphs and the placement isn't computed until the glyph build runs
// mid-paint. This co-hook closes it: after the refresh sets up the lines (still
// mid-paint. This layout listener (ScrollingMessageFrame::Layout, which owns
// the hook) closes it: after the refresh sets up the lines (still
// PRE-RENDER — the refresh runs from the SMF's Lua/event-driven methods, never
// mid-render), force each icon-bearing line to resolve+build and apply its icon
// placement synchronously, so the icons draw with their glyphs the same frame.
@@ -2096,9 +2098,6 @@ static const Game::HookAutoRegister _paintHook{Offsets::FUN_TEXT_PAINT,
// disabled independently of the whole feature; the SEH latch trips it on a fault.
bool g_earlyApply = true;
using SmfRefresh_t = void(__fastcall *)(void *smf, void *edx, int newestIdx);
SmfRefresh_t g_smfRefreshOriginal = nullptr;
using EnsureBuilt_t = int(__fastcall *)(void *node);
using RealizeAnchor_t = void(__thiscall *)(void *anchor, int flag);
using RebuildStringFs_t = void(__fastcall *)(void *fs);
@@ -2183,15 +2182,12 @@ void SafeSmfEarlyApply(void *smf) {
}
}
void __fastcall SmfRefresh_h(void *smf, void *edx, int newestIdx) {
g_smfRefreshOriginal(smf, edx, newestIdx);
void OnSmfLayout(void *smf) {
if (g_inlineEnabled && g_earlyApply)
SafeSmfEarlyApply(smf);
}
static const Game::HookAutoRegister _smfRefreshHook{
Offsets::FUN_SMF_DISPLAY_REFRESH, reinterpret_cast<void *>(&SmfRefresh_h),
reinterpret_cast<void **>(&g_smfRefreshOriginal)};
static const ScrollingMessageFrame::Layout::AutoSubscribe _smfLayoutSub{&OnSmfLayout};
// --- Lua control surface ---------------------------------------------------
@@ -2238,6 +2234,32 @@ void PrepareForReload() {
g_nodeBakedColor.clear();
}
bool LineIconShift(void *fs, const uint8_t *text, int len, float &before, float &after) {
before = 0.0f;
after = 0.0f;
if (!g_inlineEnabled || !LooksReadable(fs) || !LooksReadable(text) || len <= 0 ||
!HasInlineTexture(text, len))
return false;
FsPenFont pf;
if (!ResolveFsPenFont(fs, pf))
return false;
const float overflow = IconOverflowPx(text, len, pf.fontHPen, pf.layoutScale);
if (!(overflow > 0.0f))
return false;
// The node's justify-V as the block creator (FUN_0044d420) reads the
// style word: TOP (8) wins, then BOTTOM (0x20), else middle — the value
// the emitter reads from the node to split the move.
const uint32_t style = Game::Read<uint32_t>(fs, Offsets::OFF_FONTSTRING_FLAGS);
const int justifyV = (style & 0x8u) ? 0 : (style & 0x20u) ? 2 : 1;
const float move = (justifyV == 1) ? std::floor(overflow * 0.5f) : overflow;
const float toFs = pf.fontHInt / pf.fontHPen; // pen px → the getter's units
if (justifyV != 0)
before = move * toFs;
if (justifyV != 2)
after = move * toFs;
return true;
}
static const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
} // namespace Text::InlineTexture
+11
View File
@@ -13,6 +13,8 @@
#pragma once
#include <cstdint>
namespace Text::InlineTexture {
// Called from FrameScript_Initialize_h before the /reload teardown: the reload
@@ -20,4 +22,13 @@ namespace Text::InlineTexture {
// icon records are dropped (they rebuild as the reloaded UI re-emits its text).
void PrepareForReload();
// The room the tall inline icons on the line [text, text + len) take when
// `fs` renders it, in the font string's own units (its height getter's):
// `before` is how far the render moves that line's text down, `after` how
// far it moves the lines after it — 4.3.4's InitializeText offsets, split by
// the font string's vertical justify (top: none / all; middle: half / half,
// floored; bottom: all / none). Both 0, and false, without an icon taller
// than the font or with the feature off.
bool LineIconShift(void *fs, const uint8_t *text, int len, float &before, float &after);
} // namespace Text::InlineTexture
+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