Compare commits

...
Author SHA1 Message Date
Brues e47e881ae1 fix(event): FireValues applies RegisterUnitEvent filters
FireValues walks the subscriber chain itself and runs each OnEvent
through RunSlotWithValues, so it never reached the runner where
Frame::UnitEvent's gate sits. RegisterUnitEvent("UNIT_AURA_DELTA", unit)
fired for every unit's deltas.

Frame::UnitEvent::Accepts shares the gate's match. FireValues asks it
for each frame when the first value is a string, the same rule the
engine path uses for a %s arg1.
2026-10-31 00:21:18 -05:00
Brues d9da100110 feat(tradeskill): linked windows show the crafter's name
3.3.5's interface never shows whose profession a linked window holds.
Port 4.3.4's TradeSkillLinkNameButton and TradeSkillFrame_SetLinkName
to TradeSkillFrame and CraftFrame: "[name]" after the title, and a
click on it starts a whisper. The title moves left by half the name,
from its own anchor, so Turtle's -30 offset stays.
2026-10-30 22:48:40 -05:00
Brues 00cfe0c45b fix(smf): BackFill and RemoveMessagesByPredicate keep the engine's ring
A ring that is not full holds records 0..count-1: the engine's writer
fills them in order from an empty ring, and only a full ring wraps.
ScrollOffset and ScrollToOffset wrap by the count, and ScrollToTop
(0x00788690) lays out from record 0 unless the ring is full, so each one
is right only for that layout.

BackFillMessage wrote the new oldest message to the record before the
oldest, as 4.3.4's addToTop does. In a ring that is not full, that record
wraps to the end of the array. From then on, GetScrollOffset,
SetScrollOffset, ScrollByAmount and ScrollToTop read the wrong offset.
pfUI's chat wheel scrolled up into the backfilled lines and then could
not scroll back down.

RemoveMessagesByPredicate compacted from the oldest's record, so after a
removal from a full ring the kept messages began anywhere in the array.

BackFillMessage now rotates records 0..count by one, so the free record
comes round to 0, and writes the message there. RemoveMessagesByPredicate
first turns the ring so that the oldest is record 0, then compacts. Whole
records move, so every text keeps one owner. Clear leaves a free record's
old text for the writer to free. The extra-argument tables and the scroll
record (OFF_SCROLLINGMESSAGEFRAME_SCROLL_RECORD, +0x348) move with their
records. A scrolled-up frame is laid out again from its moved scroll
record, so the view stays.

A model of the engine's three functions agrees on 377,485 random
add, backfill, remove and scroll states. Not yet verified in game.
2026-10-30 22:36:53 -05:00
Brues d92464b86c feat(tradeskill): trade links follow 3.3.5
Profession links are now built and opened the way 3.3.5's
CGTradeSkillInfo does it. A received link opens in the engine's own
TradeSkillFrame. The ClassicAPI viewer is gone.

The link is 3.3.5's format:
trade:<spellID>:<rank>:<maxRank>:<GUID>:<bits>|h[<spell name>]. The
first field is the profession spell, not the skill line. The GUID
replaces the name. A reader whose name cache does not hold the linker
sends the name query and opens the link when the answer arrives. The
bits cover every SkillLineAbility row of the profession, as 3.3.5's link
run does. 1.12's DBC is sorted by ID and does not group a profession's
rows into one run, so the rows here are taken in ID order. A link opens
only when the reader's row count matches it.

The 1.12 list build (0x004FCA20) is 3.3.5's RefreshList without the
link branch. Three call-site patches add that branch:
- the spell list comes from the link,
- the ability rows use the linker's race and class,
- the rank is the linker's.
Two patches on TryCastSpell's toggle calls record the cast spell (3.3.5's
m_spellID). They also make a cast while a link shows open the player's
own list instead of closing the window. The game shutdown drops the link,
as 3.3.5's does.

Enchanting gets the same in the Craft window. Its rows are filtered by
castUI, because its recipes have no TRADESPELL bit. GetCraftListLink and
IsCraftLinked are the Craft window's twins of the 3.3.5 functions.
GetTradeSkillLine, GetCraftDisplaySkillLine and GetCraftName give the
linker's values while a link shows. Util/TradeSkillLink.lua now ports
3.3.5's linked mode, which hides Create, Create All and the count
controls, and keeps the link buttons.

Any spell whose Effect[0] is TRADE_SKILL can be linked. 1.12's
SkillLine.dbc has no canLink column.

Removed: C_TradeSkillUI.GetTradeSkillListLink, GetCraftListLink and
GetTradeSkillListRecipes. Old links no longer open.

Not yet verified in game.
2026-10-30 22:16:16 -05:00
Brues fec127c1af docs(item): DoesItemExistByID is true for any positive ID
The entry said a cache miss returns false and starts a load. The
function returns true for any positive item ID, cached or not, and
loads nothing. ContinueOnItemLoad depends on that.
2026-10-30 21:47:34 -05:00
Brues 37ee71c663 feat(item): C_PaperDollInfo.GetTemporaryEnchantmentInfo
Retail's per-slot temporary enchant, the function its deprecated
GetWeaponEnchantInfo is now built from. It reads the same temporary
enchant slot as C_Item.GetItemTempEnchantInfo, in retail's shape: an
INVSLOT_ number in, and a TemporaryItemEnchantInfo table out (enchantID,
remainingTimeMs, chargesRemaining, hasExpirationTime), or nothing when
the slot has no temporary enchant. A slot outside 1..19 also returns
nothing, so the engine never sees it.

hasExpirationTime is the descriptor duration gate that the shared reader
already tested before it asked for the time left.

C_Item.GetItemTempEnchantInfo stays as the ClassicAPI extension for a
weapon in a bag (#47). The docs link the two.
2026-10-30 21:20:05 -05:00
Brues 6259e22480 feat(lua): ItemUtil from retail
Util/ItemUtil.lua is now the ItemUtil table of retail
Blizzard_FrameXMLUtil/ItemUtil.lua, in retail's own text. As its header
lists, it leaves out ItemButtonUtil and DoesAnyItemSlotMatchItemContext
(retail drives them from frames this client does not have), the transmog
mixin, the currency path of GetItemDetails, and GetCraftingReagentCount
(reagent and account banks).

New for what it calls:
- Enum.BagIndex in the DLL, with Classic Era's names and this client's
  indices: Keyring -2, Bank -1, Backpack 0, Bag_1..4 = 1..4 and
  BankBag_1..6 = 5..10. Era's bank bags start at 6, after its reagent
  bag. The indices live in Offsets.h now, and container/Search.cpp drops
  its local copies.
- C_PaperDollInfo.GetInventorySlotInfoForInvSlot in the DLL. It takes the
  first PaperDollItemFrame.dbc row with that SlotNumber, and walks the
  rows the way the engine's GetInventorySlotInfo (0x004C81B0) does.
- C_Container.GetContainerNumSlots and C_Container.PickupContainerItem as
  Lua aliases of the stock globals in Compat.lua, next to
  C_ActionBar.ShouldShowKeyring, which is always true.
- ContainerFrame_GetContainerNumSlots and NUM_TOTAL_BAG_FRAMES from
  retail's ContainerFrame.lua, and GameTooltip_SuppressAutomaticCompareItem.
  Nothing reads that flag, because nothing compares items automatically.

docs/API.md covers all of it, plus Item objects and
C_Item.GetItemIDForItemInfo.

Verified in game: Enum.BagIndex.Keyring is -2, the backpack has 16 slots,
ShouldShowKeyring is true, slot 18 returns the ranged row with checkRelic
true, GetEmptyEquipSlotTooltip(1) is "Head", and FilterOwnedItems({6948})
finds a Hearthstone in the bags.
2026-10-30 21:12:37 -05:00
Brues 474da1a5b5 feat(lua): Item follows retail
Util/Item.lua is now retail Blizzard_ObjectAPI/Mainline/Item.lua in
retail's own text, and its listener is retail's AsyncCallbackSystem.lua
with the item and quest listeners. There is no spell listener: every
spell is client-side data here, so there is nothing to wait for. The one
other difference is that GetItemQualityColor reads ITEM_QUALITY_COLORS,
the same entry retail's ColorManager returns.

What follows retail now:
- Load callbacks get (id, success).
- An item callback also runs when the load fails, with success nil.
  TradeSkillLink's two callbacks now stop on a failure, because
  re-rendering would request the missing item again.
- C_Item.RequestLoadItemDataByID runs once per item, not once per
  callback.
- New methods: GetStackCount, ContinueWithCancelOnRecordLoad and
  IsRecordDataCached. ContinueOnItemLoad's usage errors name the bad
  itemID or link, and GetItemGUID returns the GUID an item was made from.

C_Item.GetItemIDForItemInfo is new in the DLL for ItemMixin:GetItemID's
fallback. It uses the resolver that GetItemInfoInstant uses.

Verified in game: Item:CreateFromItemID(19019) calls back with
(19019, 1), and C_Item.GetItemIDForItemInfo("item:6948") returns 6948.
2026-10-30 21:12:22 -05:00
Brues 6cee968a8a refactor(lua): ItemUtil.lua is Item.lua
The file holds retail's Item mixin (Blizzard_ObjectAPI/Mainline/Item.lua)
and its ItemEventListener, not retail's ItemUtil, so it takes Item.lua's
name. No code changes.
2026-10-30 21:12:07 -05:00
Brues 635c74d518 fix(smf): ScrollByAmount stops where ScrollUp stops
ScrollByAmount moved the offset straight to the clamped target, and the
clamp is retail's count - 1, where the oldest message sits alone on the
bottom line under empty space. 1.12's own ScrollUp (0x788610) stops
sooner, at +0x350 (4.3.4's m_atTop), which the layout sets once every
message back to the oldest fits the height (0x788A37). A chat wheel
built on ScrollByAmount (pfUI's) scrolled chat up until it was nearly
empty.

The engine has two scroll families, in 1.12 and 4.3.4 alike: ScrollUp
and PageUp stop at m_atTop, and ScrollToTop and SetScrollOffset go to
the oldest record. Retail defines ScrollUp as ScrollByAmount(1) and
ScrollToTop as SetScrollOffset(GetMaxScrollRange()), so ScrollByAmount
now steps the engine's ScrollUp / ScrollDown (0x788610 / 0x788650), the
loop PageUp (0x7885B0) runs, and SetScrollOffset / GetMaxScrollRange
keep count - 1. The clamped target still bounds the steps: a hidden
frame never lays out, so its ScrollUp never finds the top and wraps.

Verified in game: ScrollByAmount(1000) on ChatFrame1 stops with the
oldest message on the top line and an offset below GetMaxScrollRange().
2026-10-30 19:56:21 -05:00
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
58 changed files with 5637 additions and 2864 deletions
+8 -4
View File
@@ -69,7 +69,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Cursor](docs/API.md#cursor) | `CursorHasMacro`, `GetCursorInfo` |
| [Container](docs/API.md#container) | `C_Container.AutoStoreItem`, `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemEquipmentSetInfo`, `C_Container.GetContainerItemQuestInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerFreeSlots`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetBackpackAutosortDisabled`, `C_Container.GetBankAutosortDisabled`, `C_Container.GetItemCooldown`, `C_Container.GetSortBagsRightToLeft`, `C_Container.HasContainerItem`, `C_Container.IsContainerFiltered`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SetBackpackAutosortDisabled`, `C_Container.SetBagPortraitTexture`, `C_Container.SetBankAutosortDisabled`, `C_Container.SetItemSearch`, `C_Container.SetSortBagsRightToLeft`, `C_Container.SortBags`, `C_Container.SortBankBags`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown`, `SetBagPortraitTexture` |
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID`, `C_CreatureInfo.GetRaceInfo`, `C_CreatureInfo.GetClassInfo`, `C_CreatureInfo.GetCreatureFamilyInfo`, `C_CreatureInfo.GetCreatureFamilyIDs`, `C_CreatureInfo.GetFactionInfo`, `C_CreatureInfo.GetCreatureTypeInfo`, `C_CreatureInfo.GetCreatureTypeIDs` |
| [Currency](docs/API.md#currency) | `GetCoinText`, `C_CurrencyInfo.GetCoinText`, `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString` |
| [Currency](docs/API.md#currency) | `GetCoinText`, `C_CurrencyInfo.GetCoinText`, `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString`, `Constants.MoneyFormattingConstants` |
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
| [DeathInfo](docs/API.md#deathinfo) | `C_DeathInfo.GetSelfResurrectOptions`, `C_DeathInfo.UseSelfResurrectOption` |
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.PickupEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` |
@@ -100,6 +100,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Item: info](docs/API.md#getitemclassinfoclassid) | `GetItemClassInfo`, `GetItemSubClassInfo`, `GetItemIcon`, `IsConsumableItem`, `IsCurrentItem`, `IsDressableItem`, `IsEquippedItemType`, `IsHarmfulItem`, `IsHelpfulItem`, `IsUsableItem` |
| [Item: inventory](docs/API.md#getaverageitemlevel) | `GetAverageItemLevel`, `GetInventoryItemID`, `GetInventoryItemDurability`, `GetInventoryItemRepairCost`, `GetInventoryItemsForSlot`, `IsDualWielding`, `SetInventoryPortraitTexture` |
| [Item: item IDs](docs/API.md#getitemid--companions-to-the-engines-getitemlink-family) | `GetAuctionItemID`, `GetAuctionSellItemID`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeTargetItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID` |
| [Localization](docs/API.md#localization) | `AbbreviateLargeNumbers`, `AbbreviateNumbers`, `BreakUpLargeNumbers`, `C_StringUtil.GetDefaultAbbreviationBreakpoints` |
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootAllCorpses`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount`, `C_LossOfControl.GetSchoolLockout` |
@@ -131,7 +132,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime`, `GetTickTime`, `GetTimeCached` |
| [Totem](docs/API.md#totem) | `GetTotemInfo`, `GetTotemTimeLeft`, `GetTotemDuration`, `TargetTotem` |
| [Tracking](docs/API.md#tracking) | `GetNumTrackingTypes`, `GetTrackingInfo`, `SetTracking` |
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes`, `GetTradeSkillDescription`, `GetTradeSkillRecipeLink`, `IsTradeSkillLinked` |
| [TradeSkillUI](docs/API.md#tradeskillui) | `GetTradeSkillListLink`, `GetCraftListLink`, `GetTradeSkillDescription`, `GetTradeSkillRecipeLink`, `IsTradeSkillLinked`, `IsCraftLinked` |
| [Trainer](docs/API.md#trainer) | `GetTrainerServiceItemLink` |
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
| [Unit](docs/API.md#unit) | `CanBeRaidTarget`, `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `SetUnitCursorTexture`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHasIncomingResurrection`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSelectionColor`, `UnitSelectionType`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID`, `UnitTokenFromName` |
@@ -233,7 +234,9 @@ menu and click AddOn Options, then ClassicAPI.
| | `SoftTargetIconEnemy` / `SoftTargetIconFriend` | `0` | `1` shows an icon over `softenemy` / `softfriend`. |
| | `SoftTargetLowPriorityIcons` | `0` | `1` also shows the normal pointer as an icon. |
| | `SoftTargetNameplateSize` | `19` | Size of all soft-target icons. |
| [Localization](docs/API.md#breakuplargenumbers-cvar) | `breakUpLargeNumbers` | `1` | `1` puts a separator between each group of three digits in `BreakUpLargeNumbers`. `0` writes no separator. |
| [Cooldown](docs/API.md#createframecooldown--name-parent-template) | `countdownForCooldowns` | `0` | `1` shows the time left as numbers on cooldown frames. |
| `ScriptAnimationUtil` | `ShakeStrengthUI` | `1` | Scales how far `ScriptAnimationUtil.ShakeFrame` moves a frame. `0` turns the shake off. |
| [NamePlate](docs/API.md#nameplate) | `nameplateNotSelectedAlpha` | `0.5` | Alpha of a non-target default nameplate, `0` to `1`. |
| | `nameplateSelectedAlpha` | `1` | Alpha of the target's default nameplate, `0` to `1`. |
| [Text to speech](docs/API.md#tts-cvars) | `ttsVoice` | `0` | The voice that `C_VoiceChat.SpeakText` uses. |
@@ -432,8 +435,9 @@ functions but that consumer code still expects to find as globals —
`CallbackRegistryMixin`, `EventRegistry`, `ColorMixin` + `CreateColor`,
`Item` / `ItemLocation`, `MathUtil`(`Lerp` / `Clamp` / `CreateCounter`),
`TableUtil` (`tCompare`, `MergeTable`, `SafePack`, etc.), `EventUtil`
(`ContinueOnAddOnLoaded` and more), `DataProvider`, `CircularBuffer`,
`EasingUtil`, `Interpolator`, `EnumUtil`, `StringUtil`, `AnchorUtil`,
(`ContinueOnAddOnLoaded` and more), `FormattingUtil` (`GetMoneyString`,
`FormatLargeNumber` and more), `FrameUtil`, `DataProvider`, `CircularBuffer`,
`EasingUtil`, `ScriptAnimationUtil`, `Interpolator`, `EnumUtil`, `StringUtil`, `AnchorUtil`,
`GridLayoutUtil`, and the layout frame templates (`VerticalLayoutFrame`,
`HorizontalLayoutFrame`, `ResizeLayoutFrame`, `GridLayoutFrame`,
`StaticGridLayoutFrame`). It also holds the conditional slash
+1 -1
View File
@@ -149,7 +149,7 @@ on a frame timer" goal landed as the request/event pair below:
cache (or synchronously if it was already loaded).
Together those replace the polyfill's 180-second polling frame
(`ItemEventListener` in `Util/ItemUtil.lua:254-372`) — addons can now
(`ItemEventListener`, now in `Util/AsyncCallbackSystem.lua`) — addons can now
fire `RequestLoadItemDataByID(id)` and listen for
`ITEM_DATA_LOAD_RESULT` instead.
+552 -94
View File
@@ -13,6 +13,7 @@ build instructions.
- [`GetActionInfo(slot)`](#getactioninfoslot)
- [`IsItemAction(slot)`](#isitemactionslot)
- [`HasBonusActionBar()`](#hasbonusactionbar)
- [`C_ActionBar.ShouldShowKeyring()`](#c_actionbarshouldshowkeyring)
- [AddOns](#addons)
- [`C_AddOns.GetAddOnName(indexOrName)`](#c_addonsgetaddonnameindexorname)
@@ -149,6 +150,8 @@ build instructions.
- [`C_Container.GetBackpackAutosortDisabled()` / `C_Container.SetBackpackAutosortDisabled(disable)`](#c_containergetbackpackautosortdisabled--c_containersetbackpackautosortdisableddisable)
- [`C_Container.GetBankAutosortDisabled()` / `C_Container.SetBankAutosortDisabled(disable)`](#c_containergetbankautosortdisabled--c_containersetbankautosortdisableddisable)
- [`SetBagPortraitTexture(texture, bagIndex)` / `C_Container.SetBagPortraitTexture(texture, bagIndex)`](#setbagportraittexturetexture-bagindex--c_containersetbagportraittexturetexture-bagindex)
- [`C_Container.GetContainerNumSlots(containerIndex)` / `C_Container.PickupContainerItem(containerIndex, slotIndex)`](#c_containergetcontainernumslotscontainerindex--c_containerpickupcontaineritemcontainerindex-slotindex)
- [`ContainerFrame_GetContainerNumSlots(bagID)` / `NUM_TOTAL_BAG_FRAMES`](#containerframe_getcontainernumslotsbagid--num_total_bag_frames)
- [Creature](#creature)
- [`C_CreatureInfo.GetCreatureID(guid)`](#c_creatureinfogetcreatureidguid)
@@ -165,6 +168,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)
@@ -462,6 +466,7 @@ build instructions.
- [`LE_ITEM_QUALITY_*`](#le_item_quality_)
- [`LE_UNIT_STAT_*`](#le_unit_stat_)
- [`Enum.AddOnSecurityStatus`](#enumaddonsecuritystatus)
- [`Enum.BagIndex`](#enumbagindex)
- [`Enum.InventoryType`](#enuminventorytype)
- [`Enum.ItemClass`](#enumitemclass)
- [`Enum.ItemQuality`](#enumitemquality)
@@ -492,7 +497,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)
@@ -519,6 +524,7 @@ build instructions.
- [`C_Item.GetItemFamily(item)`](#c_itemgetitemfamilyitem)
- [`C_Item.GetItemGUID(itemLocation)`](#c_itemgetitemguiditemlocation)
- [`C_Item.GetItemID(itemLocation)`](#c_itemgetitemiditemlocation)
- [`C_Item.GetItemIDForItemInfo(itemInfo)`](#c_itemgetitemidforiteminfoiteminfo)
- [`C_Item.GetItemInfo(itemInfo)`](#c_itemgetiteminfoiteminfo)
- [`C_Item.GetItemInfoInstant(item)`](#c_itemgetiteminfoinstantitem)
- [`C_Item.GetItemInventorySlotInfo(inventorySlot)`](#c_itemgetiteminventoryslotinfoinventoryslot)
@@ -575,11 +581,30 @@ build instructions.
- [`SetInventoryPortraitTexture(texture, unit, slot)`](#setinventoryportraittexturetexture-unit-slot)
- [`GetItemIcon(itemID)` / `C_Item.GetItemIcon(itemLocation)` / `C_Item.GetItemIconByID(item)`](#getitemiconitemid--c_itemgetitemiconitemlocation--c_itemgetitemiconbyiditem)
- [`IsDualWielding()`](#isdualwielding)
- [`Item` objects](#item-objects)
- [ItemUtil](#itemutil)
- [`ItemUtil.GetItemDetails(itemLink, quantity)` / `ItemUtil.GetItemHyperlink(itemID)`](#itemutilgetitemdetailsitemlink-quantity--itemutilgetitemhyperlinkitemid)
- [`ItemUtil.PickupBagItem(itemLocation)`](#itemutilpickupbagitemitemlocation)
- [`ItemUtil.IterateBagSlots(bag, callback)` / `ItemUtil.IterateInventorySlots(firstSlot, lastSlot, callback)`](#itemutiliteratebagslotsbag-callback--itemutiliterateinventoryslotsfirstslot-lastslot-callback)
- [`ItemUtil.IteratePlayerInventory(callback)` / `ItemUtil.IteratePlayerInventoryAndEquipment(callback)`](#itemutiliterateplayerinventorycallback--itemutiliterateplayerinventoryandequipmentcallback)
- [`ItemUtil.FilterOwnedItems(itemIDs)`](#itemutilfilterowneditemsitemids)
- [`ItemUtil.TransformItemIDsToItems(itemIDs)` and related functions](#itemutiltransformitemidstoitemsitemids-and-related-functions)
- [`ItemUtil.GetValidatedItemLocation(equipSlot)` / `ItemUtil.GetEquipSlotTexture(equipSlot)`](#itemutilgetvalidateditemlocationequipslot--itemutilgetequipslottextureequipslot)
- [`ItemUtil.GetEmptyEquipSlotTooltip(equipSlot)` / `ItemUtil.GetEmptyEquipSlotTooltipForSlotName(slotName)`](#itemutilgetemptyequipslottooltipequipslot--itemutilgetemptyequipslottooltipforslotnameslotname)
- [`ItemUtil.DisplayEquipSlotTooltip(frame, tooltip, equipSlot, suppressComparison, checkRelic)`](#itemutildisplayequipslottooltipframe-tooltip-equipslot-suppresscomparison-checkrelic)
- [Launch](#launch)
- [`-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)
@@ -741,6 +766,10 @@ build instructions.
- [`C_NewItems.RemoveNewItem(bagID, slotIndex)`](#c_newitemsremovenewitembagid-slotindex)
- [`C_NewItems.ClearAll()`](#c_newitemsclearall)
- [PaperDollInfo](#paperdollinfo)
- [`C_PaperDollInfo.GetInventorySlotInfoForInvSlot(invSlot)`](#c_paperdollinfogetinventoryslotinfoforinvslotinvslot)
- [`C_PaperDollInfo.GetTemporaryEnchantmentInfo(slot)`](#c_paperdollinfogettemporaryenchantmentinfoslot)
- [PartyInfo](#partyinfo)
- [`InviteUnit(targetName)`](#inviteunittargetname)
- [`UninviteUnit(name)`](#uninviteunitname)
@@ -974,12 +1003,12 @@ build instructions.
- [`SetTracking([index])`](#settrackingindex)
- [TradeSkillUI](#tradeskillui)
- [`C_TradeSkillUI.GetTradeSkillListLink()`](#c_tradeskilluigettradeskilllistlink)
- [`C_TradeSkillUI.GetCraftListLink()`](#c_tradeskilluigetcraftlistlink)
- [`C_TradeSkillUI.GetTradeSkillListRecipes(skillLineID, bits)`](#c_tradeskilluigettradeskilllistrecipesskilllineid-bits)
- [`GetTradeSkillListLink()`](#gettradeskilllistlink)
- [`GetCraftListLink()`](#getcraftlistlink)
- [`GetTradeSkillRecipeLink(index)`](#gettradeskillrecipelinkindex)
- [`GetTradeSkillDescription(index)`](#gettradeskilldescriptionindex)
- [`IsTradeSkillLinked()`](#istradeskilllinked)
- [`IsCraftLinked()`](#iscraftlinked)
- [Trainer](#trainer)
- [`GetTrainerServiceItemLink(index)`](#gettrainerserviceitemlinkindex)
@@ -1229,6 +1258,17 @@ if HasBonusActionBar() then
end
```
### `C_ActionBar.ShouldShowKeyring()`
Returns `true`. The client always has a keyring. When this value is true,
bag code includes the keyring (`KEYRING_CONTAINER`).
```lua
if C_ActionBar.ShouldShowKeyring() then
ItemUtil.IterateBagSlots(KEYRING_CONTAINER, callback)
end
```
## AddOns
`C_AddOns.*` getters that splat the
@@ -3741,6 +3781,27 @@ C_Container.SetBagPortraitTexture(myTexture, 5) -- the first bank bag
If the bag slot is empty, the texture is cleared. The error messages
spell the name as `SetBagPortaitTexture`.
### `C_Container.GetContainerNumSlots(containerIndex)` / `C_Container.PickupContainerItem(containerIndex, slotIndex)`
These are the stock `GetContainerNumSlots` and `PickupContainerItem` with
their `C_Container` names. Each pair is the same function.
```lua
local numSlots = C_Container.GetContainerNumSlots(Enum.BagIndex.Backpack) -- 16
C_Container.PickupContainerItem(Enum.BagIndex.Backpack, 1)
```
### `ContainerFrame_GetContainerNumSlots(bagID)` / `NUM_TOTAL_BAG_FRAMES`
`ContainerFrame_GetContainerNumSlots(bagID)` returns two values:
`maxNumSlots, currentNumSlots`. For the keyring, both values are
`GetKeyRingSize()`. For other bags, both values are
`C_Container.GetContainerNumSlots(bagID)`.
`NUM_TOTAL_BAG_FRAMES` is the number of equipped bags (`4`). The bag
indices from `Enum.BagIndex.Backpack` to `NUM_TOTAL_BAG_FRAMES` are the
backpack and the equipped bags.
## Creature
### `C_CreatureInfo.GetCreatureID(guid)`
@@ -4042,6 +4103,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)`
@@ -8757,8 +8831,11 @@ 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.
scrolls to older messages. After the oldest message shows on the frame,
the offset does not increase. `ScrollUp()` stops at the same position.
As a result, the offset can stay less than `GetMaxScrollRange()`. 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
@@ -10795,6 +10872,28 @@ if C_AddOns.GetAddOnSecurity(name) == Enum.AddOnSecurityStatus.Secure then
end
```
### `Enum.BagIndex`
The container indices that the `C_Container` functions and the stock
container functions take:
| Value | Field |
|------:|-------|
| `-2` | `Keyring` |
| `-1` | `Bank` |
| `0` | `Backpack` |
| `1` to `4` | `Bag_1` to `Bag_4` |
| `5` to `10` | `BankBag_1` to `BankBag_6` |
`Keyring` and `Bank` have the same values as `KEYRING_CONTAINER` and
`BANK_CONTAINER`.
```lua
for bag = Enum.BagIndex.Backpack, NUM_TOTAL_BAG_FRAMES do
-- the backpack and the equipped bags
end
```
### `Enum.InventoryType`
The equip-type enum — the numeric `inventoryType` reported by
@@ -11245,7 +11344,7 @@ but they only report "any shift/ctrl/alt" — there's no built-in way to
tell left from right. These seven functions add the missing distinction
plus an `IsModifierKeyDown()` rollup.
### `securecall(func, ...)` / `securecallfunction(func, ...)` / `secureexecuterange(tbl, func, ...)`
### `securecall(func, ...)` / `securecallfunction(func, ...)` / `securecallmethod(object, method, ...)` / `secureexecuterange(tbl, func, ...)`
Call a function so that an error in it does not stop the code that called
it. The error goes to the error handler (`geterrorhandler()`).
@@ -11255,6 +11354,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.
@@ -11487,20 +11590,21 @@ settings, exists but stays empty.
### `C_Item.DoesItemExist(itemLocation)` / `C_Item.DoesItemExistByID(item)`
Existence checks straight off the engine's inventory manager (location
form) and item cache (ID form). No `GetItemInfo` chaining — both
functions read directly from the structures they need.
Existence checks for an inventory location or an item ID.
- `C_Item.DoesItemExist(itemLocation)` — `true` iff the equipment-slot
or `(bagID, slotIndex)` location resolves to a populated inventory
slot on the active player. Empty slots, missing bags, or malformed
tables return `false`.
- `C_Item.DoesItemExistByID(item)` — `true` iff the cache currently
has data for `item`. Cache miss returns `false` and kicks off the
network query in the background; a follow-up call after
[`GET_ITEM_INFO_RECEIVED`](#c_itemrequestloaditemdatabyiditem--c_itemrequestloaditemdataitemlocation)
will succeed. Accepts a numeric `itemID`, a bare `"item:NNN..."`
string, or a full chat link.
- `C_Item.DoesItemExistByID(item)` — `true` for any positive item ID.
The item does not have to be in the cache, and the call does not
load it. The function does not check that the ID belongs to a real
item.
[`C_Item.IsItemDataCachedByID`](#c_itemisitemdatacachedbyiditem--c_itemisitemdatacacheditemlocation)
tells you if the data is cached.
[`C_Item.RequestLoadItemDataByID`](#c_itemrequestloaditemdatabyiditem--c_itemrequestloaditemdataitemlocation)
loads it. Accepts a numeric `itemID`, a bare `"item:NNN..."` string,
or a full chat link. Other input returns `false`.
```lua
if C_Item.DoesItemExist({equipmentSlotIndex = INVSLOT_HEAD}) then ... end
@@ -11901,6 +12005,17 @@ if id then
end
```
### `C_Item.GetItemIDForItemInfo(itemInfo)`
Returns the itemID for an itemID, an `"item:N"` string or an item link. It
does not need the item's data in the cache. An item name, or input that
names no item, returns nothing.
```lua
/dump C_Item.GetItemIDForItemInfo("item:6948")
-- 6948
```
### `C_Item.GetItemInfo(itemInfo)`
The full `GetItemInfo` tuple, sourced from the client-side item cache
@@ -12566,6 +12681,10 @@ That enchant is a separate field.
An empty slot returns `false, 0, 0, 0`. An item you do not own returns
the same, and so does a location that resolves to nothing.
For an equipped item,
[`C_PaperDollInfo.GetTemporaryEnchantmentInfo`](#c_paperdollinfogettemporaryenchantmentinfoslot)
gives the same enchant as a table.
This call is a ClassicAPI extension.
### `C_Item.GetEnchantInfo(enchantID)`
@@ -13643,6 +13762,147 @@ A shield or a held item (tome, orb, libram) in the off hand does not
count. The result is also `false` until the client has the off-hand
item's data.
### `Item` objects
These functions make an item object (`ItemMixin`):
- `Item:CreateFromItemID(itemID)`
- `Item:CreateFromItemLink(itemLink)`
- `Item:CreateFromItemGUID(itemGUID)`
- `Item:CreateFromBagAndSlot(bagID, slotIndex)`
- `Item:CreateFromEquipmentSlot(equipmentSlotIndex)`
- `Item:CreateFromItemLocation(itemLocation)`
Its methods read the item through `C_Item`. Some of them are `GetItemID`,
`GetItemName`, `GetItemLink`, `GetItemIcon`, `GetItemQuality` and
`GetStackCount`. The methods that read the item's data return `nil` until
that data is in the cache.
When the load of the item's data ends, `item:ContinueOnItemLoad(callback)`
calls `callback(itemID, success)`. If the data is already in the cache, the
call occurs immediately. If the data loaded, `success` is `1`. If the load
fails (for example, the server has no item with this ID), the callback also
occurs, with `success` set to `nil`. If the item is empty (for example, an
empty bag slot), `ContinueOnItemLoad` raises an error.
`item:ContinueWithCancelOnItemLoad(callback)` does the same, and returns a
function. A call to that function cancels the callback before it occurs.
```lua
local item = Item:CreateFromItemID(19019)
item:ContinueOnItemLoad(function(itemID, success)
if success then
print(item:GetItemLink())
end
end)
```
`ItemEventListener` and `QuestEventListener` are the listeners behind these
callbacks. When the data of a quest loads,
`QuestEventListener:AddCallback(questID, callback)` calls
`callback(questID, success)`. If the load fails, the callback does not occur.
## ItemUtil
Helper functions for items, bags and equipment slots, backported from
FrameXML. Each `itemLocation` is an `ItemLocation` object (see
[`itemLocation` argument shapes](#itemlocation-argument-shapes)).
### `ItemUtil.GetItemDetails(itemLink, quantity)` / `ItemUtil.GetItemHyperlink(itemID)`
`ItemUtil.GetItemDetails` returns `itemName, itemTexture, quantity,
itemRarity, itemLink` from [`C_Item.GetItemInfo`](#c_itemgetiteminfoiteminfo).
It returns `quantity` as you gave it. `ItemUtil.GetItemHyperlink` returns
only the item link. Both return `nil` for an item that is not in the cache.
### `ItemUtil.PickupBagItem(itemLocation)`
Picks up the item at a bag location, with
[`C_Container.PickupContainerItem`](#c_containergetcontainernumslotscontainerindex--c_containerpickupcontaineritemcontainerindex-slotindex).
An equipment location does nothing.
### `ItemUtil.IterateBagSlots(bag, callback)` / `ItemUtil.IterateInventorySlots(firstSlot, lastSlot, callback)`
These functions call `callback(itemLocation)` for each slot that holds an
item, in slot order. When `callback` returns a true value, the walk stops.
- `ItemUtil.IterateBagSlots` walks one bag. If `callback` stopped the walk,
it returns `true`. Otherwise it returns `false`.
- `ItemUtil.IterateInventorySlots` walks the equipment slots from `firstSlot`
to `lastSlot`. It returns nothing.
### `ItemUtil.IteratePlayerInventory(callback)` / `ItemUtil.IteratePlayerInventoryAndEquipment(callback)`
`ItemUtil.IteratePlayerInventory` walks the backpack, the equipped bags and
then the keyring, with the same `callback` rule as
`ItemUtil.IterateBagSlots`. If `callback` stopped the walk, it returns `true`.
`ItemUtil.IteratePlayerInventoryAndEquipment` walks the same bags. If
`callback` does not stop the walk, it then walks the equipped slots
(`INVSLOT_FIRST_EQUIPPED` to `INVSLOT_LAST_EQUIPPED`). It returns nothing.
```lua
ItemUtil.IteratePlayerInventory(function(itemLocation)
print(C_Item.GetItemLink(itemLocation))
end)
```
### `ItemUtil.FilterOwnedItems(itemIDs)`
Returns a new array with the IDs from `itemIDs` that are in the player's
bags or keyring. Equipped items do not count.
```lua
/dump ItemUtil.FilterOwnedItems({6948, 19019})
-- {6948} with a Hearthstone in the bags
```
### `ItemUtil.TransformItemIDsToItems(itemIDs)` and related functions
These functions return a new array of [`Item` objects](#item-objects), one
for each entry of the input array:
- `ItemUtil.TransformItemIDsToItems(itemIDs)`
- `ItemUtil.TransformItemGUIDsToItems(itemGUIDs)`
- `ItemUtil.TransformItemLocationsToItems(itemLocations)`
- `ItemUtil.TransformItemLocationItemsToGUIDItems(itemLocations)`, which
gives the same result as `ItemUtil.TransformItemLocationsToItems`
### `ItemUtil.GetValidatedItemLocation(equipSlot)` / `ItemUtil.GetEquipSlotTexture(equipSlot)`
If the equipment slot holds an item, `ItemUtil.GetValidatedItemLocation`
returns an `ItemLocation` for that slot. Otherwise it returns `nil`.
`ItemUtil.GetEquipSlotTexture` returns the icon of the item in the
equipment slot. If the slot is empty, it returns the texture of the empty
slot from
[`C_PaperDollInfo.GetInventorySlotInfoForInvSlot`](#c_paperdollinfogetinventoryslotinfoforinvslotinvslot).
### `ItemUtil.GetEmptyEquipSlotTooltip(equipSlot)` / `ItemUtil.GetEmptyEquipSlotTooltipForSlotName(slotName)`
Both functions return the display name of an equipment slot, for example
`"Head"`. `ItemUtil.GetEmptyEquipSlotTooltipForSlotName` takes a slot name,
for example `"HeadSlot"`, and returns the global string with that name in
upper case (`HEADSLOT`). `ItemUtil.GetEmptyEquipSlotTooltip` takes a slot
number.
```lua
/dump ItemUtil.GetEmptyEquipSlotTooltip(1)
-- "Head"
```
### `ItemUtil.DisplayEquipSlotTooltip(frame, tooltip, equipSlot, suppressComparison, checkRelic)`
Shows the item in an equipment slot of the player on `tooltip`. If the slot
is empty, the tooltip shows the name of the slot at the right of `frame`.
If `checkRelic` is true and the player has a relic slot, the name is
`RELICSLOT` ("Relic").
If `suppressComparison` is true, the function calls
`GameTooltip_SuppressAutomaticCompareItem(tooltip)`. That function sets
`tooltip.suppressAutomaticCompareItem`. No ClassicAPI code compares items
automatically, so the flag has no effect.
## Launch
Options you add to the command line that starts the game, alongside
@@ -13724,6 +13984,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
@@ -17353,6 +17773,52 @@ slot.
Clears every new flag at once. Fires `BAG_NEW_ITEMS_UPDATED` if anything was
flagged.
## PaperDollInfo
### `C_PaperDollInfo.GetInventorySlotInfoForInvSlot(invSlot)`
Returns the paper doll slot for an inventory slot number. It is the reverse
of `GetInventorySlotInfo(slotName)`, which starts from the slot name. It
returns four values:
| Return | Value |
|--------|-------|
| `invSlot` | The slot number. |
| `slotTexture` | The texture path of the empty slot. |
| `checkRelic` | `true` for the ranged slot (`18`), which can also hold a relic. Otherwise `false`. |
| `slotName` | The slot name, for example `"HeadSlot"`. |
If no paper doll slot has this number, it returns nothing. An argument that
is not a number raises an error.
```lua
/dump C_PaperDollInfo.GetInventorySlotInfoForInvSlot(18)
-- 18, "interface\\paperdoll\\UI-PaperDoll-Slot-Ranged.blp", true, "RangedSlot"
```
### `C_PaperDollInfo.GetTemporaryEnchantmentInfo(slot)`
Returns the temporary enchant on an equipped item as a table. `slot` is an
`INVSLOT_` constant, for example `INVSLOT_MAINHAND`. If the slot has no
temporary enchant, the call returns nothing.
| Field | Value |
|-------|-------|
| `enchantID` | The enchant. [`C_Item.GetEnchantInfo`](#c_itemgetenchantinfoenchantid) gives its name and effects. |
| `remainingTimeMs` | The time left in milliseconds. If the enchant has no time limit, the value is `0`. |
| `chargesRemaining` | The charges left. An oil has `0`. |
| `hasExpirationTime` | `true` if the enchant has a time limit. |
```lua
local info = C_PaperDollInfo.GetTemporaryEnchantmentInfo(INVSLOT_MAINHAND)
if info then
print(info.enchantID, info.remainingTimeMs, info.chargesRemaining)
end
```
[`C_Item.GetItemTempEnchantInfo`](#c_itemgetitemtempenchantinfoitemlocation)
reads the same enchant on a weapon in a bag.
## PartyInfo
These functions invite players to your group, remove players from your
@@ -22207,93 +22673,77 @@ end
## TradeSkillUI
The **trade-skill list link** — the shareable
`|Htrade:...|h[Profession]|h` link that shows a player's profession and
which recipes they know. The engine has no such link type (its
hyperlink parser only knows `item:` and `enchant:`), so both the link and the
click-to-view UI are synthesized. Clicking a received link opens a
crafting-window-style frame (the real `TradeSkillFrame` parchment art) listing
the recipes the linker knows — difficulty-coloured and sorted high-craft-first
— with a skill-rank bar and an "X of Y recipes known" summary.
A **profession link** (`|Htrade:...|h[Blacksmithing]|h`) shows the
profession of a player and the recipes that the player knows. The link also
carries the skill rank of the player.
The "known recipes" data is a bitfield over the skill line's **canonical
recipe list** — every `SkillLineAbility.dbc` row for the skill line that is a
craftable recipe, in ascending record-ID order. That ordering is identical on
every client with the same DBC, so the sender and reader agree on the
bit-index space with no handshake. Whether the sender knows a given recipe
comes from the same player-spell bitmap `IsPlayerSpell` reads (which covers
profession recipes), so the link is self-contained — the reader needs neither
the sender online nor the profession itself.
When you click a received link in chat, the profession window opens with
that profession. The window lists the recipes of the linked player. The
difficulty colors use the rank of the linked player. The reagent counts come
from your bags.
### `C_TradeSkillUI.GetTradeSkillListLink()`
While the window shows a profession from a link, the window does not show its
Create, Create All, and count controls. `GetTradeSkillLine` gives the rank of
the linked player. When you open a profession from your spellbook, the window
shows your own profession again.
Returns the `|Htrade:...|h` link for the **currently-open** trade skill
window, or `nil` if no profession window is open.
Enchanting opens in the craft window. The craft window has two functions of
its own: [`GetCraftListLink`](#getcraftlistlink) and
[`IsCraftLinked`](#iscraftlinked). While the craft window shows a link,
`GetCraftName` and `GetCraftDisplaySkillLine` give the linked profession and
rank.
You can link every profession that opens from the spellbook. This includes
Smelting, Poisons, and Beast Training.
The client must know the player who sent the link. If the client does not
know the player, it asks the server for the name. The link opens when the
answer arrives.
A link holds one bit for each recipe of the profession, in the order of the
DBC files. If two clients have different DBC files, the link does not open,
or it shows the wrong recipes.
### `GetTradeSkillListLink()`
Returns the link for your profession in the trade-skill window. The link has
your skill rank and the recipes that you know. When the window is closed, the
function returns nil.
```lua
-- with a profession window open:
local link = C_TradeSkillUI.GetTradeSkillListLink()
-- "|cffffd000|Htrade:164:300:300:Bob:eE0C...|h[Blacksmithing]|h|r"
ChatEdit_InsertLink(link) -- or SendChatMessage(link, "GUILD"), etc.
local link = GetTradeSkillListLink()
-- "|cffffd000|Htrade:9785:300:300:2A6F1:Ug/E...|h[Blacksmithing]|h|r"
ChatFrameEditBox:Insert(link)
```
Built entirely in the DLL — no addon wiring. The window only needs to be open
long enough to identify the profession; the recipe data is read from the spell
bitmap, not the window's server list. Wire format (a ClassicAPI-private shape
modeled on the 2.x link, with the linker's name in the slot 2.x used for the
GUID):
The trade-skill window has a button next to its title. The button puts this
link in the chat box.
The link has this format:
```
|cffffd000|Htrade:<skillLineID>:<curRank>:<maxRank>:<linkerName>:<base64 bits>|h[Name]|h|r
|cffffd000|Htrade:<spellID>:<rank>:<maxRank>:<GUID>:<bits>|h[<profession>]|h|r
```
`skillLineID` is the `SkillLine.dbc` row (164 = Blacksmithing); `curRank` /
`maxRank` are the linker's skill level (cosmetic); `linkerName` is the linking
character's name (shown in the viewer title); the base64 tail packs 6 recipes
per character, LSB-first. Links from before this field was added (no
`linkerName`) still decode — the viewer just omits the name from its title.
- `spellID`: the profession spell that opened the window.
- `rank`, `maxRank`: your skill rank and maximum rank, as `GetTradeSkillLine`
gives them.
- `GUID`: the GUID of your character, in hexadecimal.
- `bits`: the recipes that you know, six recipes for each base64 character.
Clicking a `trade:` link is handled automatically (an override of
`SetItemRef`) — it opens the recipe viewer. No addon wiring needed.
### `GetCraftListLink()`
### `C_TradeSkillUI.GetCraftListLink()`
Same as `GetTradeSkillListLink`, but for the **Craft window** — the
separate profession frame used by Enchanting and pet training. Returns `nil`
unless `CraftFrame` is shown. The two windows have separate storage and can be
open simultaneously, so they get separate builders; the resulting link is
the same `|Htrade:...|h` format and opens the same viewer.
Returns the link for your profession in the craft window, for example
Enchanting. When the craft window is closed, the function returns nil. This
function works like
[`GetTradeSkillListLink`](#gettradeskilllistlink). This is a ClassicAPI
extension.
```lua
local link = C_TradeSkillUI.GetCraftListLink() -- with the enchanting window open
-- "|cffffd000|Htrade:333:300:300:Bob:...|h[Enchanting]|h|r"
local link = GetCraftListLink() -- with Enchanting open
-- "|cffffd000|Htrade:13920:300:300:2A6F1:...|h[Enchanting]|h|r"
```
### `C_TradeSkillUI.GetTradeSkillListRecipes(skillLineID, bits)`
Decodes a link's bitfield against a skill line's canonical recipe list.
Returns an array table:
```lua
local recipes = C_TradeSkillUI.GetTradeSkillListRecipes(164, "eE0C...")
-- recipes[i] = { spellID, isKnown, trivialLevel, greenLevel, createdItem }
for i = 1, table.getn(recipes) do
local name = GetSpellInfo(recipes[i].spellID)
print(name, recipes[i].isKnown and "known" or "unknown")
end
```
`trivialLevel` / `greenLevel` are the skill levels the recipe turns grey /
green at — the recipe's difficulty band, coloured against the linker's skill
exactly as the default UI does (the required-skill field is uselessly `1` for
nearly every trainer-taught recipe). `createdItem` is the crafted item's ID
(`0` for enchants), which the viewer uses to build its subclass filter.
The array is in canonical recipe order (one entry per *possible* recipe in the
skill line, not just the known ones). Turn a `spellID` into a name/icon with
[`GetSpellInfo`](#getspellinfospellid--getspellinfoslot-booktype). Recipes past the end of a short/garbled
`bits` string decode as not-known rather than erroring.
### `GetTradeSkillRecipeLink(index)`
Returns the link for the recipe in row `index` of the trade-skill window.
@@ -22337,20 +22787,28 @@ The Craft window (Enchanting, pet training) already has this in
### `IsTradeSkillLinked()`
Returns whether the trade-skill window shows a profession that was opened
from another player's link. The second return value is that player's name.
On this client, both return values are always nil. A received `trade:` link
opens its own viewer, and the trade-skill window and its functions only
show the player's own professions. Addons use this check to keep a linked
profession out of the player's own records.
When the trade-skill window shows a profession from a link, the function
returns 1 and the name of the linked player. Otherwise, it returns nil and
nil. If the client does not know the name, the second value is nil.
After the window closes, the function gives the same values until the window
opens again. Addons use this function to keep a linked profession out of the
recipe records of the player.
```lua
local isLinked, playerName = IsTradeSkillLinked() -- nil, nil
local isLinked, playerName = IsTradeSkillLinked()
if not isLinked then
-- the rows are the player's own recipes
-- the rows are your own recipes
end
```
### `IsCraftLinked()`
When the craft window shows a profession from a link, the function returns 1
and the name of the linked player. Otherwise, it returns nil and nil. This
function works like [`IsTradeSkillLinked`](#istradeskilllinked). This is a
ClassicAPI extension.
## Trainer
### `GetTrainerServiceItemLink(index)`
+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
+164 -11
View File
@@ -223,8 +223,8 @@ enum Offsets {
// VAR_SPELL_RECORDS whose record has `+0x0C == 3` ->
// FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP), else raises "Unknown link type"
// (`0x00532269`). A full `|H...|h` link fails the prefix test. Co-hooked
// by `Tooltip::Hyperlink` for full links, `spell:` / `quest:`, and
// trade-skill `enchant:` links. SuperWoW patches a jump at `0x00532210`
// by `Tooltip::Hyperlink` for full links, `spell:` / `quest:` / `trade:`,
// and trade-skill `enchant:` links. SuperWoW patches a jump at `0x00532210`
// (after the `item:` test) that adds `quest:` and resumes at `enchant:`.
FUN_SCRIPT_GAMETOOLTIP_SET_HYPERLINK = 0x00531FD0, // slot 12
// `SetAction(slot)`: cooldown via FUN_ACTION_SLOT_COOLDOWN, clear, then
@@ -324,12 +324,26 @@ enum Offsets {
// lua_pcall with **zero Lua args** and the engine error handler
// (VAR_FRAMESCRIPT_ERROR_HANDLER_REF) as errfunc. FUN_FRAME_INVOKE_SCRIPT
// is the same shape for the no-arg scripts (OnShow/OnHide/OnEnter/…).
// `Frame::ScriptArgs` co-hooks both to additionally pass the handler
// modern positional args (self, arg1..argN). The frame's own Lua object
// `Frame::ScriptArgs` co-hooks both and redirects their pcalls (below) to
// additionally pass the handler modern positional args (self,
// arg1..argN). The frame's own Lua object
// ref lives at frame+OFF_COBJECT_LUA_REF (refcount at
// OFF_COBJECT_LUA_REFCOUNT); FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER
// lazily creates it when the refcount is 0.
FUN_FRAME_RUN_SCRIPT_ARGS = 0x00704F10,
// Each runner's own `call LUA_PCALL` (E8, 5 bytes): lua_pcall(L, 0, 0, -2)
// with [errfunc, handler] on top, after the runner has set `this` and
// arg1..argN. Frame::ScriptArgs redirects both, so a format is only ever
// parsed by the runner's own switch (0x704F89: index byte table 0x7052B8
// for 'd'..'u', jump table 0x7052A4) — including a spec another DLL adds
// at runtime: SuperWoWhook points 'e' (byte 0x7052B9) at a new slot
// 0x7052CC whose handler pushes registry[ref] for a 4-byte ref, and fires
// CREATE_CHATBUBBLE with "%e%s". In the args runner at this call, [ebp+8] is handlerRef,
// [ebp+0Ch] the frame and [ebp-4] the number of argN it set (0x704F54,
// 0x705011; at most 19, 0x704F73). In the no-arg runner, [ebp-8] is
// handlerRef (0x704D5D) and EDI the frame (0x704D5B).
CALL_FRAME_RUN_SCRIPT_ARGS_PCALL = 0x00705199,
CALL_FRAME_INVOKE_SCRIPT_PCALL = 0x00704E79,
// The base ScriptObject's OnEvent handler slot — an 8-byte
// {handler, context} pair at frame+0x0C (from FUN_00702590, the base
// resolver every frame-type resolver chains through, and confirmed by
@@ -4853,6 +4867,17 @@ enum Offsets {
// arrays: a 1-based backpack slot S maps to linear (S - 1) + 23. Matches
// PackBagSlot's `*param_3 += 0x17` for the backpack branch.
BACKPACK_LINEAR_BASE = 23,
// Lua container indices (bagID), as `PackBagSlot` reads them and FrameXML
// names them (KEYRING_CONTAINER, BANK_CONTAINER, BACKPACK_CONTAINER,
// NUM_BAG_SLOTS, NUM_BANKBAGSLOTS): keyring -2, bank -1, backpack 0,
// equipped bags 1..4, bank bags 5..10. `Enum.BagIndex` publishes them.
BAG_INDEX_KEYRING = -2,
BAG_INDEX_BANK = -1,
BAG_INDEX_BACKPACK = 0,
BAG_INDEX_BAG_FIRST = 1,
BAG_INDEX_BAG_LAST = 4,
BAG_INDEX_BANK_BAG_FIRST = 5,
BAG_INDEX_BANK_BAG_LAST = 10,
// Player inventory manager layout — used for direct GUID-array reads
// that bypass `GetItemBySlot`'s bank gate at `0x006228C1`.
// +0x00 uint32 max slot count
@@ -5530,6 +5555,25 @@ enum Offsets {
// 0x009BD3C0) does not match the subclass name of these rows.
ITEMSUBCLASS_FLAG_NO_SEARCH_NAME = 0x1,
// PaperDollItemFrame.dbc — the paper-doll slot table. Like ItemSubClass,
// the engine reads a parallel (records, count) pair at the class
// instance's +0x00 / +0x04: `Script_GetInventorySlotInfo` (0x004C81B0)
// walks [0x00C0DA00] for [0x00C0DA04] flat 0x10-byte rows. Columns
// (verified against the dumped DBC):
// +0x00 char* ItemSlotName ("HeadSlot", "Bag0Slot", "AmmoSlot", ...)
// +0x04 char* SlotIcon ("interface\paperdoll\UI-PaperDoll-Slot-Head.blp")
// +0x08 int SlotNumber (inventory slot: 1..19 equipment, 20..23 bag
// slots, 0 ammo)
// The engine flags SlotNumber 18 (ranged) as the relic-check slot
// (`SlotNumber - 1 == 0x11` at 0x004C8264).
VAR_PAPERDOLLITEMFRAME_RECORDS = 0x00C0DA00,
VAR_PAPERDOLLITEMFRAME_COUNT = 0x00C0DA04,
OFF_PAPERDOLLITEMFRAME_RECORD_STRIDE = 0x10,
OFF_PAPERDOLLITEMFRAME_SLOT_NAME = 0x00,
OFF_PAPERDOLLITEMFRAME_SLOT_ICON = 0x04,
OFF_PAPERDOLLITEMFRAME_SLOT_NUMBER = 0x08,
PAPERDOLLITEMFRAME_RELIC_SLOT_NUMBER = 18,
// ItemDisplayInfo.dbc — standard layout. Icon path char* at record +0x14
// (per the helper at 0x005D88B0 that Script_GetItemInfo uses).
VAR_ITEMDISPLAYINFO_RECORDS = 0x00C0DC10,
@@ -6068,8 +6112,8 @@ enum Offsets {
// rank-up / passive rows have both zero. Empirically: the
// Blacksmithing skill line has 343 SLA rows, 334 with a nonzero
// threshold — the 9 excluded are the Apprentice..Artisan rank
// spells and skill grants. This is the discriminator
// `TradeSkill::Link` uses to build the canonical recipe universe.
// spells and skill grants. The profession list builds read both
// for a row's difficulty.
OFF_SLA_TRIVIAL_SKILL_HIGH = 0x28,
OFF_SLA_TRIVIAL_SKILL_LOW = 0x2C,
// The engine's (race, class, spellID) -> SkillLineAbility record lookup:
@@ -10807,13 +10851,71 @@ enum Offsets {
// window is open. `FUN_004FC870` zeroes it on close (and fires
// event `0x13B`), so `> 0` reliably means "trade window open".
VAR_CURRENT_TRADESKILL_LINE = 0x00BDE040,
// Craft window (Enchanting, pet training) open-state flag — nonzero
// (3 for the enchanting-style craft) while open; `FUN_004F5FE0`
// zeroes it on close (fires event `0x169`). The Craft window has no
// skill-line-ID global — `TradeSkill::Link` derives the skill line
// from the first craft recipe — so this is the "craft open" gate.
// Craft window (Enchanting, pet training): the open craft type — the
// opening spell's EffectMiscValue[0], 3 for Enchanting, 1 for Beast
// Training — or 0 when closed. `FUN_004F5FE0` zeroes it on close (fires
// event `0x169`). The Craft window keeps no skill line.
VAR_CRAFT_WINDOW_STATE = 0x00BDCFB8,
// The profession windows' open, build and shutdown seams that
// `TradeSkill::Window` uses to show another player's profession from a
// `trade:` link, as 3.3.5's CGTradeSkillInfo does. Disassembly.
//
// `void __fastcall(uint32_t skillLine)`: opens the TradeSkill window on a
// skill line, or closes it when that line is already open. The only
// caller is TryCastSpell at CALL_TRY_CAST_TOGGLE_TRADE_SKILL, with the
// cast spell ID in ESI.
FUN_TOGGLE_TRADE_SKILL = 0x004FC910,
CALL_TRY_CAST_TOGGLE_TRADE_SKILL = 0x006E4C22,
// `void __fastcall(uint32_t craftType)`: the Craft window's twin. The
// only caller is TryCastSpell at CALL_TRY_CAST_TOGGLE_CRAFT, ESI = spell.
FUN_TOGGLE_CRAFT = 0x004F6000,
CALL_TRY_CAST_TOGGLE_CRAFT = 0x006E4C34,
// `void(void)`: clears the TradeSkill repeat latch and fires
// UPDATE_TRADESKILL_RECAST (3.3.5 CGTradeSkillInfo::StopRecast).
FUN_TRADE_SKILL_STOP_RECAST = 0x004FD790,
// The TradeSkill list build (BuildTradeSkillList, 0x004FCA20) reads per
// row: the open line's spell array from
// `CGPlayer_C::FindSkillLineSpells(player, skillLine)` (`__thiscall`,
// returns a growable array — count +0x04, int spell IDs at +0x08 — or
// null), each spell's ability row from FUN_SKILL_LINE_ABILITY_LOOKUP
// with the player's race and class, and the player's raw rank in the
// ability's line from the 33 bytes at SITE_BUILD_TRADE_SKILL_RANK
// (`call FindSkillIndex` .. `xor eax,eax`; ECX = player, the line
// pushed, rank left in EAX at 0x004FCC49). One call site each.
FUN_PLAYER_FIND_SKILL_LINE_SPELLS = 0x005EA2C0,
CALL_BUILD_TRADE_SKILL_SPELLS = 0x004FCB2A,
CALL_BUILD_TRADE_SKILL_ABILITY = 0x004FCBD2,
SITE_BUILD_TRADE_SKILL_RANK = 0x004FCC28,
// The Craft list build (BuildCraftList, 0x004F60C0) reads per row: the
// type's spell array from `CGPlayer_C::GetTabSpells(player, tab)`
// (`__thiscall`, the array at player + 0x1CD0 + tab * 16, same shape),
// each spell's ability row from `CGUnit_C::FindSkillLineAbility(unit,
// spell)` (`__thiscall`, the unit's race and class), and the rank from
// `CGPlayer_C::GetSkillTotal(player, skillLine)` (`__thiscall`, base plus
// temporary bonus). GetTabSpells' other caller is
// Script_GetCraftDisplaySkillLine (0x004F6CF4).
FUN_PLAYER_GET_TAB_SPELLS = 0x005EA330,
CALL_BUILD_CRAFT_SPELLS = 0x004F6118,
FUN_UNIT_FIND_SKILL_LINE_ABILITY = 0x0060C7C0,
CALL_BUILD_CRAFT_ABILITY = 0x004F61A8,
FUN_PLAYER_GET_SKILL_TOTAL = 0x005EA520,
CALL_BUILD_CRAFT_RANK = 0x004F620E,
// `void(void)` each: free the TradeSkill / Craft rows and zero their
// state. CGGameUI::ShutdownGame (0x00491180) calls them once per return
// to the character screen, as 3.3.5's calls CGTradeSkillInfo::ShutdownGame.
FUN_SHUTDOWN_TRADE_SKILL = 0x004FC790,
CALL_GAME_SHUTDOWN_TRADE_SKILL = 0x004911C8,
FUN_SHUTDOWN_CRAFT = 0x004F5F00,
CALL_GAME_SHUTDOWN_CRAFT = 0x004911D2,
// The window getters that answer for the player: GetTradeSkillLine and
// GetCraftDisplaySkillLine push (name, rank, maxRank) from the player's
// skill slot; GetCraftName pushes the name of the spell the player keeps
// for the open type. `int __fastcall(lua_State *)` each.
FUN_SCRIPT_GET_TRADE_SKILL_LINE = 0x004FDD40,
FUN_SCRIPT_GET_CRAFT_DISPLAY_SKILL_LINE = 0x004F6CB0,
FUN_SCRIPT_GET_CRAFT_NAME = 0x004F6C10,
// Trainer window (ClassTrainerFrame.cpp). Same slot-indirected shape as
// the trade-skill rows: `[VAR_TRAINER_SERVICES]` points at an array of
// 0x38-byte service records, the record's `+0x00` is the spell the
@@ -10888,6 +10990,14 @@ enum Offsets {
// Verified from Spell.dbc: 13262 "Disenchant" has Effect[0] = 99 (the
// only row carrying it). Effect 32 is TRIGGER_MISSILE, not disenchant.
SPELL_EFFECT_DISENCHANT = 99,
// A profession spell (Blacksmithing 2018, Enchanting 7411, Smelting
// 2656): casting it opens a profession window. TryCastSpell (0x006E4B60)
// tests Effect[0] for it (`cmp [edi+0xF4], 0x2F` at 0x006E4BCE) and
// routes on EffectMiscValue[0]: 0 opens the TradeSkill window for the
// spell's skill line, any other value is the Craft window's type (3
// Enchanting, 1 Beast Training). 3.3.5's GetTradeSkillListLink takes
// only spells whose Effect[0] is this.
SPELL_EFFECT_TRADE_SKILL = 47,
// EffectBasePoints[3] — the base magnitude of each effect, stored as
// (value - 1) for the fixed-die spells that back item stats (the
@@ -10954,6 +11064,15 @@ enum Offsets {
// SetHyperlink's `enchant:` on this bit, and its UnitCastingInfo reports
// it as isTradeskill.
SPELL_ATTR_TRADESPELL = 0x20,
// Column 3, castUI: the tab a learned spell without SPELL_ATTR_TRADESPELL
// is filed under. The learn function (0x005E9C20) puts a TRADESPELL
// spell in its skill line's list (CGPlayer_C::FindSkillLineSpells) and
// any other spell with castUI > 0 in tab array castUI
// (CGPlayer_C::GetTabSpells, `mov esi,[edi+0xC]` at 0x005E9D83), which
// the Craft window of that type lists. Extracted Spell.dbc: 187 of
// Enchanting's 192 SkillLineAbility spells hold 3, 138 of Beast
// Training's 140 hold 1.
OFF_SPELL_RECORD_CAST_UI = 0x0C,
// Attributes bits 2 (0x4) and 10 (0x400) — "on next melee swing" (the
// ability replaces the next white hit instead of sending its own attack,
// e.g. Heroic Strike / Maul / Raptor Strike). The CLIENT itself tests
@@ -12736,6 +12855,18 @@ enum Offsets {
CALL_TEXT_FIT_HEIGHT_STEP = 0x005C22B1,
CALL_TEXT_ENUMERATE_LINES_STEP = 0x005C24F9,
DRAW_BUILDER_FRAME_LINE_X = 0x48,
// The fit-to-height count's line test (0x005C21C0, after its stepper
// call): `fld [ebp+10h]` (the block height), then this `fadd dword ptr
// [VAR_TEXT_FIT_HEIGHT_EPSILON]` (D8 05 BC 26 80 00), then FCOMPP against
// the running height + font height; it stops at the first line that
// fails. [ebp-8] is the bytes fitted so far, 0 until a line fits. EAX is
// dead here (FNSTSW AX, then both paths reload it from [ebp-8]).
// Text::FitHeight replaces the fadd.
SITE_TEXT_FIT_HEIGHT_TOLERANCE = 0x005C22CB,
TEXT_FIT_HEIGHT_FRAME_FITTED_BYTES = 0x8,
// Exactly 2^-20 (0x35800000, .rdata): the fit's line margin above. A
// shared float, also read by collision, movement and matrix code.
VAR_TEXT_FIT_HEIGHT_EPSILON = 0x008026BC,
// The draw builder's right / centre x: `FUN_TEXT_SCREEN_TO_PIXEL_WIDTH(raw,
// -width)` (or -width / 2), `float __fastcall(int raw, float)`, `ret 4`,
// result in ST0, stored to the line x at 0x005CDE28.
@@ -12853,6 +12984,15 @@ enum Offsets {
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT = 0x33C,
OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES = 0x340,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS = 0x344, // the newest record; -1 when empty
// The record the lines are laid out from (4.3.4's m_currentScroll);
// FUN_SMF_DISPLAY_REFRESH stores its argument here.
OFF_SCROLLINGMESSAGEFRAME_SCROLL_RECORD = 0x348,
// A ring that is not full holds records 0..count-1: the writer fills
// them in order from an empty ring (Clear: position -1), and only a full
// ring wraps. ScrollOffset and ScrollToOffset (below) wrap by the count,
// not the limit, and ScrollToTop (0x00788690) lays out from record 0
// unless the ring is full, so each is right only for that layout. A
// ClassicAPI writer must leave the ring in it.
OFF_SCROLLINGMESSAGEFRAME_MESSAGES = 0x394, // the record array
SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE = 0x18,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT = 0x0,
@@ -12888,6 +13028,19 @@ enum Offsets {
// 0 when empty; the GetCurrentScroll method pushes it.
FUN_SCROLLINGMESSAGEFRAME_SCROLL_TO_OFFSET = 0x00788700,
FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET = 0x00788AC0,
// ScrollUp / ScrollDown (`int __thiscall(smf)`, plain `ret`; 4.3.4's
// 0x0051E620 / 0x0051E660 step for step): one record older / newer and a
// layout, answering 1. Each answers 0 without moving when empty or at its
// end, and re-arms the shown lines there: ScrollDown at
// OFF_SCROLLINGMESSAGEFRAME_AT_BOTTOM, ScrollUp at +0x350 (4.3.4's
// m_atTop), which the layout sets when every message from the scroll
// record back to the oldest fit the height (0x00788A37: laid-out count ==
// count remaining). The layout runs only on a shown frame ([smf+0xD4],
// m_visible), so a hidden frame's +0x350 is stale and its ScrollUp wraps
// around the ring. PageUp (0x007885B0) is a loop of ScrollUp that ends on
// the first 0.
FUN_SCROLLINGMESSAGEFRAME_SCROLL_UP = 0x00788610,
FUN_SCROLLINGMESSAGEFRAME_SCROLL_DOWN = 0x00788650,
// Clear (`void __thiscall(smf)`, 4.3.4's Clear): count 0, position 0, a
// layout from 0, +0x350 = 0, position -1. The Clear method's body.
FUN_SCROLLINGMESSAGEFRAME_CLEAR = 0x007882B0,
+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);
}
+53
View File
@@ -0,0 +1,53 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `Enum.BagIndex` — retail's BagIndexConstants enumeration, with Classic
// Era's names for the containers this client has and this client's own
// indices for them. Era numbers its bank bags from 6 because its reagent bag
// takes 5; here they start right after the equipped bags, as the stock bank
// frame numbers them. The reagent bag, reagent bank and account bank tabs
// have no entry.
#include "Game.h"
#include "Offsets.h"
namespace Container::BagIndex {
namespace {
const Game::Lua::EnumIntegerEntry kEntries[] = {
{"Keyring", Offsets::BAG_INDEX_KEYRING},
{"Bank", Offsets::BAG_INDEX_BANK},
{"Backpack", Offsets::BAG_INDEX_BACKPACK},
{"Bag_1", Offsets::BAG_INDEX_BAG_FIRST},
{"Bag_2", Offsets::BAG_INDEX_BAG_FIRST + 1},
{"Bag_3", Offsets::BAG_INDEX_BAG_FIRST + 2},
{"Bag_4", Offsets::BAG_INDEX_BAG_LAST},
{"BankBag_1", Offsets::BAG_INDEX_BANK_BAG_FIRST},
{"BankBag_2", Offsets::BAG_INDEX_BANK_BAG_FIRST + 1},
{"BankBag_3", Offsets::BAG_INDEX_BANK_BAG_FIRST + 2},
{"BankBag_4", Offsets::BAG_INDEX_BANK_BAG_FIRST + 3},
{"BankBag_5", Offsets::BAG_INDEX_BANK_BAG_FIRST + 4},
{"BankBag_6", Offsets::BAG_INDEX_BANK_BAG_LAST},
};
void RegisterLuaFunctions() {
Game::Lua::RegisterIntegerEnum("Enum", "BagIndex", kEntries,
sizeof(kEntries) / sizeof(kEntries[0]), "BagIndexConstants");
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Container::BagIndex
+4 -10
View File
@@ -208,13 +208,6 @@ bool IsItemFiltered(const uint8_t *cgItem) {
namespace {
// Lua's BACKPACK_CONTAINER, BANK_CONTAINER, KEYRING_CONTAINER and the last
// bank bag (NUM_BAG_SLOTS + NUM_BANKBAGSLOTS).
constexpr int kBackpack = 0;
constexpr int kBank = -1;
constexpr int kKeyring = -2;
constexpr int kLastBankBag = 10;
const Game::Doc::Event kSearchUpdateDoc{
"Container", "Fires when the bag search text changes.", {}};
const Event::Custom::AutoReserve _evt{"INVENTORY_SEARCH_UPDATE", &kSearchUpdateDoc};
@@ -225,11 +218,12 @@ const Event::Custom::AutoReserve _evt{"INVENTORY_SEARCH_UPDATE", &kSearchUpdateD
// get their fixed sizes. The keyring is the inventory's last range, so the
// whole-inventory bound is its own. Any other index is no container.
int SlotsToWalk(int bagID) {
if (bagID == kBackpack)
if (bagID == Offsets::BAG_INDEX_BACKPACK)
return Offsets::BACKPACK_NUM_SLOTS;
if (bagID == kBank)
if (bagID == Offsets::BAG_INDEX_BANK)
return Offsets::INVMGR_BANK_MAIN_LAST_SLOT - Offsets::INVMGR_BANK_MAIN_FIRST_SLOT + 1;
if (bagID == kKeyring || (bagID >= 1 && bagID <= kLastBankBag))
if (bagID == Offsets::BAG_INDEX_KEYRING ||
(bagID >= Offsets::BAG_INDEX_BAG_FIRST && bagID <= Offsets::BAG_INDEX_BANK_BAG_LAST))
return INT_MAX;
return 0;
}
+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
@@ -41,16 +41,20 @@ Util\ColorUtil.lua
Util\ItemLocation.lua
Util\PlayerLocation.lua
Util\EventUtil.lua
Util\AsyncCallbackSystem.lua
Util\Item.lua
Util\ItemUtil.lua
Util\ContainerFrame.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
+14 -1
View File
@@ -1,3 +1,16 @@
if not _G then
_G = getfenv(0)
end
end
-- Retail names for what this client already has. Retail's Deprecated files
-- alias the old globals to the C_ functions; here the old globals are the
-- implementations.
C_Container.GetContainerNumSlots = GetContainerNumSlots
C_Container.PickupContainerItem = PickupContainerItem
C_ActionBar = C_ActionBar or {}
-- Retail answers per game type. This client always has a keyring.
function C_ActionBar.ShouldShowKeyring()
return true
end
@@ -0,0 +1,116 @@
-- Backport of retail's Blizzard_ObjectAPI/Mainline/AsyncCallbackSystem.lua.
--
-- Differences from the retail source:
-- - No ASYNC_SPELL type and no SpellEventListener. This client keeps every
-- spell in its own data files, so there is no spell data to wait for and
-- no C_Spell.RequestLoadSpellData / SPELL_DATA_LOAD_RESULT.
--[[
Queries some data retrieval API (specifically where the data may not be currently available) and when it becomes available
calls a user-supplied function. The callback can be canceled if necessary (e.g. the frame that would use the data becomes
hidden before the data arrives).
The API is managed so that arbitrary query functions cannot be executed.
--]]
AsyncCallbackAPIType = {
ASYNC_QUEST = 1,
ASYNC_ITEM = 2,
}
local permittedAPI =
{
[AsyncCallbackAPIType.ASYNC_QUEST] = { event = "QUEST_DATA_LOAD_RESULT", accessor = C_QuestLog.RequestLoadQuestByID },
[AsyncCallbackAPIType.ASYNC_ITEM] = { event = "ITEM_DATA_LOAD_RESULT", accessor = C_Item.RequestLoadItemDataByID },
};
AsyncCallbackSystemMixin = {};
function AsyncCallbackSystemMixin:Init(apiType)
self.callbacks = {};
-- API Type should be set up from key value pairs before OnLoad.
self.api = permittedAPI[apiType];
self:SetScript("OnEvent",
function(self, event, ...)
if event == self.api.event then
local id, success = ...;
if success or apiType == AsyncCallbackAPIType.ASYNC_ITEM then
self:FireCallbacks(id, success);
else
self:ClearCallbacks(id);
end
end
end
);
self:RegisterEvent(self.api.event);
end
local CANCELED_SENTINEL = -1;
function AsyncCallbackSystemMixin:AddCallback(id, callbackFunction)
local callbacks = self:GetOrCreateCallbacks(id);
table.insert(callbacks, callbackFunction);
local needsAccessorCall = #callbacks == 1;
if needsAccessorCall then
self.api.accessor(id);
end
return #callbacks, callbacks;
end
function AsyncCallbackSystemMixin:AddCancelableCallback(id, callbackFunction)
-- NOTE: If the data is currently availble then the callback will be executed and callbacks cleared, so there will be nothing to cancel.
local index, callbacks = self:AddCallback(id, callbackFunction);
return function()
if #callbacks > 0 and callbacks[index] ~= CANCELED_SENTINEL then
callbacks[index] = CANCELED_SENTINEL;
return true;
end
return false;
end;
end
function AsyncCallbackSystemMixin:FireCallbacks(id, success)
local callbacks = self:GetCallbacks(id);
if callbacks then
self:ClearCallbacks(id);
for i, callback in ipairs(callbacks) do
if callback ~= CANCELED_SENTINEL then
xpcall(callback, CallErrorHandler, id, success);
end
end
-- The cancel functions have a reference to this table, so ensure that it's cleared out.
for i = #callbacks, 1, -1 do
callbacks[i] = nil;
end
end
end
function AsyncCallbackSystemMixin:ClearCallbacks(id)
self.callbacks[id] = nil;
end
function AsyncCallbackSystemMixin:GetCallbacks(id)
return self.callbacks[id];
end
function AsyncCallbackSystemMixin:GetOrCreateCallbacks(id)
local callbacks = self.callbacks[id];
if not callbacks then
callbacks = {};
self.callbacks[id] = callbacks;
end
return callbacks;
end
local function CreateListener(apiType)
local listener = Mixin(CreateFrame("Frame"), AsyncCallbackSystemMixin);
listener:Init(apiType);
return listener;
end
ItemEventListener = CreateListener(AsyncCallbackAPIType.ASYNC_ITEM);
QuestEventListener = CreateListener(AsyncCallbackAPIType.ASYNC_QUEST);
@@ -1,214 +1,267 @@
-- Backport of 3.3.5 FrameXML/CallbackRegistryMixin.lua to vanilla 1.12 / Lua 5.0.
-- Backport of retail Blizzard_SharedXMLBase/CallbackRegistry.lua to vanilla 1.12 / Lua 5.0.
--
-- Implementation differences from the 3.3.5 source:
-- - 1.12 has no taint or Secure-Templates system, so the entire
-- AttributeDelegate/SetForbidden machinery for inserting event keys
-- into the callback tables is unnecessary. Sub-tables are allocated
-- lazily inside RegisterCallback instead.
-- - securecallfunction/secureexecuterange don't exist; they reduce to
-- direct calls / explicit for-loops here.
-- - GenerateClosure doesn't exist in 1.12; provide a local one that
-- captures registration-time varargs via SafePack-style storage.
-- - Lua 5.0 has no select() or `wipe()`; we use the implicit `arg`
-- table and assign-nil to clear tables.
-- Implementation differences from the retail source:
-- - No AttributeDelegate. Retail inserts event keys and creates deferred
-- callback tables through a forbidden frame's OnAttributeChanged, as a
-- taint barrier. 1.12 has no taint and no SetForbidden, so
-- SecureInsertEvent and GetMutableCallbacksByEvent do that work directly.
-- - TriggerEvent calls securecallfunction from its own loops, where retail
-- passes a vararg helper to secureexecuterange. The contract is the same
-- (an error goes to the error handler and the other callbacks still run),
-- but every call to a vararg function builds an `arg` table in Lua 5.0,
-- which would cost a table per callback per event.
-- - 1.12 has no global wipe, so UnregisterEvents uses table.wipe.
local generateOwnerID = CreateCounter()
local secureexecuterange = secureexecuterange;
local securecallfunction = securecallfunction;
local unpack = unpack;
local error = error;
local pairs = pairs;
local rawset = rawset;
local next = next;
local wipe = table.wipe;
local GetOrCreateTableEntry = GetOrCreateTableEntry;
local CallbackType = EnumUtil.MakeEnum("Closure", "Function")
local CallbackType = EnumUtil.MakeEnum("Closure", "Function");
-- Local GenerateClosure: binds `func` plus all of `...` so a later call
-- invokes func(boundArgs..., callArgs...). Mirrors 3.3.5's helper of the
-- same name. SafePack/SafeUnpack semantics so embedded nils survive.
local function GenerateClosure(func, ...)
local boundN = arg.n
local bound = {}
for i = 1, boundN do bound[i] = arg[i] end
return function(...)
local callN = arg.n
-- Forward bound + call args faithfully, embedded/leading nils and all.
-- The ONLY thing that makes unpack span holes on this Lua 5.0 build is
-- a `.n` field: unpack takes its count from getn (which returns t.n)
-- and ignores any explicit i/j, so a `.n`-less table stops dead at the
-- first hole (verified: unpack({1, 1, nil, 1}, 1, 4) yields just 1, 1).
local all = {}
local n = 0
for i = 1, boundN do n = n + 1; all[n] = bound[i] end
for i = 1, callN do n = n + 1; all[n] = arg[i] end
all.n = n
return func(unpack(all))
end
end
-- Callbacks can be registered without an owner as a matter of convenience. Generally this is fine when you never
-- intend to release the callback.
local generateOwnerID = CreateCounter();
CallbackRegistryMixin = CallbackRegistryMixin or {}
CallbackRegistryMixin = {};
function CallbackRegistryMixin:OnLoad()
local callbackTables = {}
for _, value in pairs(CallbackType) do
callbackTables[value] = {}
end
self.callbackTables = callbackTables
local callbackTables = {};
for callbackType, value in pairs(CallbackType) do
local callbacks = {};
callbackTables[value] = callbacks;
end
self.callbackTables = callbackTables;
self.executingEvents = {};
self.deferredCallbacks = {};
end
function CallbackRegistryMixin:SetUndefinedEventsAllowed(allowed)
self.isUndefinedEventAllowed = allowed
self.isUndefinedEventAllowed = allowed;
end
function CallbackRegistryMixin:GetCallbackTables()
return self.callbackTables
return self.callbackTables;
end
function CallbackRegistryMixin:GetCallbackTable(callbackType)
return self.callbackTables[callbackType]
return self.callbackTables[callbackType];
end
function CallbackRegistryMixin:GetCallbacksByEvent(callbackType, event)
local callbackTable = self:GetCallbackTable(callbackType)
return callbackTable[event]
local callbackTable = self:GetCallbackTable(callbackType);
return callbackTable[event];
end
-- Returns either the underlying callback table or a temporary table depending
-- on if the event is currently being dispatched. The callbacks stored in the
-- temporary table will be merged into the underlying callback table after the
-- dispatch is complete.
function CallbackRegistryMixin:GetMutableCallbacksByEvent(callbackType, event)
if securecallfunction(rawget, self.executingEvents, event) == nil then
return self:GetCallbacksByEvent(callbackType, event);
end
local deferrals = GetOrCreateTableEntry(self.deferredCallbacks, event);
return GetOrCreateTableEntry(deferrals, callbackType);
end
function CallbackRegistryMixin:HasRegistrantsForEvent(event)
for _, callbackTable in pairs(self:GetCallbackTables()) do
local callbacks = callbackTable[event]
if callbacks and next(callbacks) then
return true
end
end
return false
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
local callbacks = callbackTable[event];
if callbacks and securecallfunction(next, callbacks) then
return true;
end
end
return false;
end
-- Lazy allocation of per-event sub-tables. In 3.3.5 this went through a
-- forbidden attribute frame for taint-isolation reasons; that's a no-op
-- consideration on 1.12, so we just assign here.
function CallbackRegistryMixin:SecureInsertEvent(event)
for _, callbackTable in pairs(self:GetCallbackTables()) do
if not callbackTable[event] then
callbackTable[event] = {}
end
end
if not self:HasRegistrantsForEvent(event) then
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
if not callbackTable[event] then
rawset(callbackTable, event, {});
end
end
end
end
function CallbackRegistryMixin:RegisterCallback(event, func, owner, ...)
if type(event) ~= "string" then
error("CallbackRegistryMixin::RegisterCallback 'event' requires string type.")
elseif type(func) ~= "function" then
error("CallbackRegistryMixin::RegisterCallback 'func' requires function type.")
else
if owner == nil then
owner = generateOwnerID()
elseif type(owner) == "number" then
error("CallbackRegistryMixin:RegisterCallback 'owner' as number is reserved internally.")
end
end
if type(event) ~= "string" then
error("CallbackRegistryMixin::RegisterCallback 'event' requires string type.");
elseif type(func) ~= "function" then
error("CallbackRegistryMixin::RegisterCallback 'func' requires function type.");
else
if owner == nil then
owner = generateOwnerID();
elseif type(owner) == "number" then
error("CallbackRegistryMixin:RegisterCallback 'owner' as number is reserved internally.")
end
end
self:SecureInsertEvent(event)
-- Taint barrier for inserting event key into callback tables.
self:SecureInsertEvent(event);
for _, callbackTable in pairs(self:GetCallbackTables()) do
local callbacks = callbackTable[event]
callbacks[owner] = nil
end
-- An owner can have a single callback per event. The simpliest way to ensure
-- this is to remove all callbacks for the owner prior to new registration.
self:UnregisterCallback(event, owner);
if arg.n > 0 then
local callbacks = self:GetCallbacksByEvent(CallbackType.Closure, event)
callbacks[owner] = GenerateClosure(func, owner, unpack(arg))
else
local callbacks = self:GetCallbacksByEvent(CallbackType.Function, event)
callbacks[owner] = func
end
local count = select("#", ...);
if count > 0 then
local callbacks = self:GetMutableCallbacksByEvent(CallbackType.Closure, event);
callbacks[owner] = GenerateClosure(func, owner, ...);
else
local callbacks = self:GetMutableCallbacksByEvent(CallbackType.Function, event);
callbacks[owner] = func;
end
return owner
return owner;
end
local function CreateCallbackHandle(cbr, event, owner)
local handle =
{
Unregister = function()
cbr:UnregisterCallback(event, owner)
end,
}
return handle
-- Wrapped in a table for future flexibility.
local handle =
{
Unregister = function()
cbr:UnregisterCallback(event, owner);
end,
};
return handle;
end
function CallbackRegistryMixin:RegisterCallbackWithHandle(event, func, owner, ...)
owner = self:RegisterCallback(event, func, owner, unpack(arg))
return CreateCallbackHandle(self, event, owner)
owner = self:RegisterCallback(event, func, owner, ...);
return CreateCallbackHandle(self, event, owner);
end
function CallbackRegistryMixin:ReconcileDeferredCallbacks(event, closures, funcs)
self:CopyDeferredCallbacks(CallbackType.Closure, event, closures);
self:CopyDeferredCallbacks(CallbackType.Function, event, funcs);
self.deferredCallbacks[event] = nil;
end
function CallbackRegistryMixin:CopyDeferredCallbacks(callbackType, event, target)
local deferrals = self.deferredCallbacks[event];
if deferrals == nil then
return;
end
local callbacks = deferrals[callbackType];
if callbacks == nil then
return;
end
local function ExecuteAssignCallback(owner, callback)
target[owner] = callback;
end
secureexecuterange(callbacks, ExecuteAssignCallback);
end
function CallbackRegistryMixin:TriggerEvent(event, ...)
if type(event) ~= "string" then
error("CallbackRegistryMixin:TriggerEvent 'event' requires string type.")
elseif not self.isUndefinedEventAllowed and not (self.Event and self.Event[event]) then
error(string.format("CallbackRegistryMixin:TriggerEvent event '%s' doesn't exist.", event))
end
if type(event) ~= "string" then
error("CallbackRegistryMixin:TriggerEvent 'event' requires string type.");
elseif not self.isUndefinedEventAllowed and not self.Event[event] then
error(string.format("CallbackRegistryMixin:TriggerEvent event '%s' doesn't exist.", event));
end
local closures = self:GetCallbacksByEvent(CallbackType.Closure, event)
if closures then
for _, closure in pairs(closures) do
closure(unpack(arg))
end
end
-- TriggerEvent appears to need to support reentrant calls for now.
local count = (self.executingEvents[event] or 0) + 1;
local funcs = self:GetCallbacksByEvent(CallbackType.Function, event)
if funcs then
for owner, func in pairs(funcs) do
-- Stock 3.3.5 passes the owner as the first arg so callers can
-- distinguish multiple registrations of the same function. `arg`
-- carries its own `.n`, so unpack(arg) forwards the payload with
-- leading/embedded nils intact. Do NOT instead merge into a
-- `{ owner, ... }` table and unpack that WITHOUT setting `.n`: this
-- build's unpack counts via getn, so a `.n`-less table collapses at
-- the first hole (verified in-game: reload's PLAYER_ENTERING_WORLD,
-- arg1=nil arg2=1, dropped the payload).
func(owner, unpack(arg))
end
end
-- Set before invoking any callback so a reentrant call does not
-- attempt to call ReconcileDeferredCallbacks.
self.executingEvents[event] = count;
local closures = self:GetCallbacksByEvent(CallbackType.Closure, event);
if closures then
for owner, closure in pairs(closures) do
securecallfunction(closure, ...);
end
end
local funcs = self:GetCallbacksByEvent(CallbackType.Function, event);
if funcs then
for owner, func in pairs(funcs) do
securecallfunction(func, owner, ...);
end
end
-- The local count is the only value we care about for the purpose
-- of flushing the event key and reconciling the deferred callbacks.
if count == 1 then
self.executingEvents[event] = nil;
self:ReconcileDeferredCallbacks(event, closures, funcs);
end
end
function CallbackRegistryMixin:UnregisterCallback(event, owner)
if type(event) ~= "string" then
error("CallbackRegistryMixin:UnregisterCallback 'event' requires string type.")
elseif owner == nil then
error("CallbackRegistryMixin:UnregisterCallback 'owner' is required.")
end
if type(event) ~= "string" then
error("CallbackRegistryMixin:UnregisterCallback 'event' requires string type.");
elseif owner == nil then
error("CallbackRegistryMixin:UnregisterCallback 'owner' is required.");
end
for _, callbackTable in pairs(self:GetCallbackTables()) do
local callbacks = callbackTable[event]
if callbacks then
callbacks[owner] = nil
end
end
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
-- Only assign nil if the owner is present in the table, otherwise
-- its insertion could cause a rehash during iteration by secureexecuterange.
local callbacks = callbackTable[event];
if callbacks and callbacks[owner] then
callbacks[owner] = nil;
end
end
local deferrals = self.deferredCallbacks[event];
if deferrals then
for callbackType, callbacks in pairs(deferrals) do
-- Freely assign nil because deferredCallbacks are not subject
-- to secureexecuterange table mutilation stress disorder.
callbacks[owner] = nil;
end
end
end
function CallbackRegistryMixin:UnregisterEvents(eventTable)
if eventTable then
for _, callbackTable in pairs(self:GetCallbackTables()) do
for event in pairs(eventTable) do
if callbackTable[event] then
callbackTable[event] = nil
end
end
end
else
for _, callbackTable in pairs(self:GetCallbackTables()) do
for k in pairs(callbackTable) do
callbackTable[k] = nil
end
end
end
function CallbackRegistryMixin:UnregisterEvents()
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
wipe(callbackTable);
end
end
function CallbackRegistryMixin:GenerateCallbackEvents(events)
if not self.Event then
self.Event = {}
end
function CallbackRegistryMixin:UnregisterEventsByEventTable(eventTable)
if type(eventTable) ~= "table" then
error("CallbackRegistryMixin:UnregisterEventsByEventTable 'eventTable' requires table type.");
end
for eventIndex, eventName in ipairs(events) do
if self.Event[eventName] then
error(string.format("CallbackRegistryMixin:GenerateCallbackEvents: event '%s' already exists.", eventName))
end
self.Event[eventName] = eventName
end
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
for event in pairs(eventTable) do
callbackTable[event] = nil;
end
end
end
function CallbackRegistryMixin:GenerateCallbackEvents(eventTable)
if not self.Event then
self.Event = {};
end
if type(eventTable) ~= "table" then
error("CallbackRegistryMixin:GenerateCallbackEvents 'eventTable' requires table type.");
end
for eventIndex, eventName in ipairs(eventTable) do
if self.Event[eventName] then
error(string.format("CallbackRegistryMixin:GenerateCallbackEvents: event '%s' already exists.", eventName));
end
self.Event[eventName] = eventName;
end
end
function CallbackRegistryMixin.DoesFrameHaveEvent(frame, event)
return frame.Event and frame.Event[event]
return frame.Event and frame.Event[event];
end
@@ -0,0 +1,22 @@
-- Backport of the bag-count globals from retail's
-- Blizzard_UIPanels_Game/Mainline/ContainerFrame.lua.
--
-- Differences from the retail source:
-- - NUM_TOTAL_BAG_FRAMES is NUM_BAG_FRAMES: there is no reagent bag.
-- - ContainerFrame_GetContainerNumSlots gives the backpack no extra slots.
-- Retail counts 4 more for an account without an authenticator; here
-- the backpack's size does not depend on the account.
NUM_TOTAL_BAG_FRAMES = NUM_BAG_FRAMES;
function ContainerFrame_GetContainerNumSlots(bagId)
if bagId == KEYRING_CONTAINER then
local keyRingSize = GetKeyRingSize();
return keyRingSize, keyRingSize;
end
local currentNumSlots = C_Container.GetContainerNumSlots(bagId);
local maxNumSlots = currentNumSlots;
return maxNumSlots, currentNumSlots;
end
@@ -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.
@@ -91,6 +91,13 @@ function GameTooltip_ShowCompareItem(self, shift)
end
end
-- Retail's automatic comparison (the alwaysCompareItems cvar) skips a
-- tooltip with this flag. Comparison here runs only when a caller asks for
-- it, so nothing reads the flag.
function GameTooltip_SuppressAutomaticCompareItem(tooltip)
tooltip.suppressAutomaticCompareItem = true;
end
function GameTooltip_SetTitle(tooltip, text, overrideColor, wrap)
tooltip:ClearLines();
GameTooltip_AddColoredLine(tooltip, text, overrideColor or HIGHLIGHT_FONT_COLOR, wrap)
@@ -13,6 +13,9 @@
EventRegistry = CreateFromMixins(CallbackRegistryMixin);
-- Keep every instance field in OnLoad. Addons derive their own registry from
-- this instance (pfUI: CreateFromMixins(EventRegistry) then :OnLoad()), and a
-- field set anywhere else would be shared with the global registry.
function EventRegistry:OnLoad()
CallbackRegistryMixin.OnLoad(self);
self:SetUndefinedEventsAllowed(true);
@@ -0,0 +1,354 @@
-- Backport of retail's Blizzard_ObjectAPI/Mainline/Item.lua.
--
-- Differences from the retail source:
-- - GetItemQualityColor reads ITEM_QUALITY_COLORS directly; retail's
-- ColorManager.GetColorDataForItemQuality returns the same entry.
Item = {};
ItemMixin = {};
--[[static]] function Item:CreateFromItemLocation(itemLocation)
if type(itemLocation) ~= "table" or type(itemLocation.HasAnyLocation) ~= "function" or not itemLocation:HasAnyLocation() then
error("Usage: Item:CreateFromItemLocation(notEmptyItemLocation)", 2);
end
local item = CreateFromMixins(ItemMixin);
item:SetItemLocation(itemLocation);
return item;
end
--[[static]] function Item:CreateFromBagAndSlot(bagID, slotIndex)
if type(bagID) ~= "number" or type(slotIndex) ~= "number" then
error("Usage: Item:CreateFromBagAndSlot(bagID, slotIndex)", 2);
end
local item = CreateFromMixins(ItemMixin);
item:SetItemLocation(ItemLocation:CreateFromBagAndSlot(bagID, slotIndex));
return item;
end
--[[static]] function Item:CreateFromEquipmentSlot(equipmentSlotIndex)
if type(equipmentSlotIndex) ~= "number" then
error("Usage: Item:CreateFromEquipmentSlot(equipmentSlotIndex)", 2);
end
local item = CreateFromMixins(ItemMixin);
item:SetItemLocation(ItemLocation:CreateFromEquipmentSlot(equipmentSlotIndex));
return item;
end
--[[static]] function Item:CreateFromItemLink(itemLink)
if type(itemLink) ~= "string" then
error("Usage: Item:CreateFromItemLink(itemLinkString)", 2);
end
local item = CreateFromMixins(ItemMixin);
item:SetItemLink(itemLink);
return item;
end
--[[static]] function Item:CreateFromItemID(itemID)
if type(itemID) ~= "number" then
error("Usage: Item:CreateFromItemID(itemID)", 2);
end
local item = CreateFromMixins(ItemMixin);
item:SetItemID(itemID);
return item;
end
--[[static]] function Item:CreateFromItemGUID(itemGUID)
if type(itemGUID) ~= "string" then
error("Usage: Item:CreateFromItemGUID(itemGUIDString)", 2);
end
local item = CreateFromMixins(ItemMixin);
item:SetItemGUID(itemGUID);
return item;
end
--[[static]] function Item:DoItemsMatch(item1, item2)
if not item1 or not item2 then
return false;
end
return item1:Matches(item2);
end
function ItemMixin:Matches(item)
if not item then
return false;
end
local itemID = item:GetItemID();
if (itemID ~= nil) and (self:GetItemID() == itemID) then
return true;
end
local itemLocation = item:GetItemLocation();
if (itemLocation ~= nil) and itemLocation:IsEqualTo(self:GetItemLocation()) then
return true;
end
return false;
end
function ItemMixin:SetItemLocation(itemLocation)
self:Clear();
self.itemLocation = itemLocation;
end
function ItemMixin:SetItemLink(itemLink)
self:Clear();
self.itemLink = itemLink;
end
function ItemMixin:SetItemID(itemID)
self:Clear();
self.itemID = itemID;
end
function ItemMixin:SetItemGUID(itemGUID)
self:Clear();
self.itemGUID = itemGUID;
end
function ItemMixin:GetItemLocation()
if self.itemLocation then
return self.itemLocation;
end
if self.itemGUID then
return C_Item.GetItemLocation(self.itemGUID);
end
return nil;
end
function ItemMixin:GetItemGUID()
if self.itemGUID then
return self.itemGUID;
end
if self.itemLocation then
return C_Item.GetItemGUID(self.itemLocation);
end
return nil;
end
function ItemMixin:HasItemLocation()
return self.itemLocation ~= nil;
end
function ItemMixin:Clear()
self.itemLocation = nil;
self.itemLink = nil;
self.itemID = nil;
self.itemGUID = nil;
end
function ItemMixin:IsItemEmpty()
if self:GetStaticBackingItem() then
return not C_Item.DoesItemExistByID(self:GetStaticBackingItem());
end
return not self:IsItemInPlayersControl();
end
function ItemMixin:GetStaticBackingItem()
return self.itemLink or self.itemID;
end
function ItemMixin:IsItemInPlayersControl()
local itemLocation = self:GetItemLocation();
return itemLocation and C_Item.DoesItemExist(itemLocation);
end
-- Item API
function ItemMixin:GetItemID()
local backingItem = self:GetStaticBackingItem();
if backingItem then
return C_Item.GetItemInfoInstant(backingItem) or C_Item.GetItemIDForItemInfo(backingItem);
end
if not self:IsItemEmpty() then
return C_Item.GetItemID(self:GetItemLocation());
end
return nil;
end
function ItemMixin:IsItemLocked()
return self:IsItemInPlayersControl() and C_Item.IsLocked(self:GetItemLocation());
end
function ItemMixin:LockItem()
if self:IsItemInPlayersControl() then
C_Item.LockItem(self:GetItemLocation());
end
end
function ItemMixin:UnlockItem()
if self:IsItemInPlayersControl() then
C_Item.UnlockItem(self:GetItemLocation());
end
end
function ItemMixin:GetItemIcon() -- requires item data to be loaded
if self:GetStaticBackingItem() then
return C_Item.GetItemIconByID(self:GetStaticBackingItem());
end
if not self:IsItemEmpty() then
return C_Item.GetItemIcon(self:GetItemLocation());
end
end
function ItemMixin:GetItemName() -- requires item data to be loaded
if self:GetStaticBackingItem() then
return C_Item.GetItemNameByID(self:GetStaticBackingItem());
end
if not self:IsItemEmpty() then
return C_Item.GetItemName(self:GetItemLocation());
end
return nil;
end
function ItemMixin:GetItemLink() -- requires item data to be loaded
if self.itemLink then
return self.itemLink;
end
if self.itemID then
return (select(2, C_Item.GetItemInfo(self.itemID)));
end
if not self:IsItemEmpty() then
return C_Item.GetItemLink(self:GetItemLocation());
end
return nil;
end
function ItemMixin:GetItemQuality() -- requires item data to be loaded
if self:GetStaticBackingItem() then
return C_Item.GetItemQualityByID(self:GetStaticBackingItem());
end
if not self:IsItemEmpty() then
return C_Item.GetItemQuality(self:GetItemLocation());
end
return nil;
end
function ItemMixin:GetStackCount() -- requires item data to be loaded
if not self:IsItemEmpty() then
return C_Item.GetStackCount(self:GetItemLocation());
end
return nil;
end
function ItemMixin:GetCurrentItemLevel() -- requires item data to be loaded
if self:GetStaticBackingItem() then
return (C_Item.GetDetailedItemLevelInfo(self:GetStaticBackingItem()));
end
if not self:IsItemEmpty() then
return C_Item.GetCurrentItemLevel(self:GetItemLocation());
end
return nil;
end
function ItemMixin:GetItemQualityColor() -- requires item data to be loaded
local itemQuality = self:GetItemQuality();
return ITEM_QUALITY_COLORS[itemQuality]; -- may be nil if item data isn't loaded
end
function ItemMixin:GetItemQualityColorRGB() -- requires item data to be loaded
local colorTbl = self:GetItemQualityColor();
return colorTbl.color:GetRGB();
end
function ItemMixin:GetItemMaxStackSize() -- requires item data to be loaded
if self:GetStaticBackingItem() then
return C_Item.GetItemMaxStackSizeByID(self:GetStaticBackingItem());
end
if not self:IsItemEmpty() then
return C_Item.GetItemMaxStackSize(self:GetItemLocation());
end
return nil;
end
function ItemMixin:IsStackable() -- requires item data to be loaded
local maxStackSize = self:GetItemMaxStackSize();
return maxStackSize and maxStackSize > 1;
end
function ItemMixin:GetInventoryType()
if self:GetStaticBackingItem() then
return C_Item.GetItemInventoryTypeByID(self:GetStaticBackingItem());
end
if not self:IsItemEmpty() then
return C_Item.GetItemInventoryType(self:GetItemLocation());
end
return nil;
end
function ItemMixin:GetInventoryTypeName()
if not self:IsItemEmpty() then
return select(4, C_Item.GetItemInfoInstant(self:GetItemID()));
end
end
function ItemMixin:IsItemDataCached()
if self:GetStaticBackingItem() then
return C_Item.IsItemDataCachedByID(self:GetStaticBackingItem());
end
if not self:IsItemEmpty() then
return C_Item.IsItemDataCached(self:GetItemLocation());
end
return true;
end
function ItemMixin:IsDataEvictable()
-- Item data could be evicted from the cache
return true;
end
function ItemMixin:ValidateForContinueOnItemLoad(methodName, callbackFunction)
if type(callbackFunction) ~= "function" then
error(("Usage: NonEmptyItem:%s(callbackFunction): invalid callbackFunction"):format(methodName), 3);
end
if self:IsItemEmpty() then
if self.itemLink then
error(("Usage: NonEmptyItem:%s(callbackFunction) invalid itemLink: <%s>"):format(methodName, self.itemLink), 3);
elseif self.itemID then
error(("Usage: NonEmptyItem:%s(callbackFunction) invalid itemID: <%d>"):format(methodName, self.itemID), 3);
end
error(("Usage: NonEmptyItem:%s(callbackFunction): invalid item"):format(methodName), 3);
else
if not self:GetItemID() then
error(("Usage: NonEmptyItem:%s(callbackFunction): item appeared non-empty, but had invalid itemID. itemID: <%s>, itemLink: <%s>"):format(methodName, tostring(self.itemID), tostring(self.itemLink)), 3);
end
end
end
-- Add a callback to be executed when item data is loaded, if the item data is already loaded then execute it immediately
function ItemMixin:ContinueOnItemLoad(callbackFunction)
self:ValidateForContinueOnItemLoad("ContinueOnItemLoad", callbackFunction);
ItemEventListener:AddCallback(self:GetItemID(), callbackFunction);
end
-- Same as ContinueOnItemLoad, except it returns a function that when called will cancel the continue
function ItemMixin:ContinueWithCancelOnItemLoad(callbackFunction)
self:ValidateForContinueOnItemLoad("ContinueWithCancelOnItemLoad", callbackFunction);
return ItemEventListener:AddCancelableCallback(self:GetItemID(), callbackFunction);
end
-- Generic aliases for use with ContinuableContainer
function ItemMixin:ContinueWithCancelOnRecordLoad(callbackFunction)
return self:ContinueWithCancelOnItemLoad(callbackFunction);
end
function ItemMixin:IsRecordDataCached()
return self:IsItemDataCached();
end
+154 -373
View File
@@ -1,415 +1,196 @@
-- Backport of 3.3.5 FrameXML/ItemUtil.lua to vanilla 1.12 / Lua 5.0.
-- Backport of retail's Blizzard_FrameXMLUtil/ItemUtil.lua: the ItemUtil table.
--
-- Implementation differences from the 3.3.5 source:
-- - Many C_Item.* calls the modern code expects are not exposed by
-- ClassicAPI.dll. We shim the missing ones at the top using the
-- stock 1.12 globals (GetItemInfo, GetInventoryItemLink,
-- GetContainerItemLink/Info). DLL-provided functions are left alone
-- via `or` guards so we never clobber a real implementation.
-- - Lua 5.0 has no select(); calls that previously did
-- `select(N, GetItemInfo(...))` use named locals to peel off the
-- return values instead.
-- - CallErrorHandler isn't in 1.12; we route through geterrorhandler().
-- - In 1.12 OnEvent scripts, event name + payload are exposed via the
-- `event`, `arg1`, `arg2`... globals rather than as function args.
-- - 1.12 has no concept of an item GUID and no public item-lock API,
-- so those return nil / no-op.
-- Differences from the retail source:
-- - No ItemButtonUtil and no ItemUtil.DoesAnyItemSlotMatchItemContext.
-- Retail sets the item context from frames this client does not have
-- (scrapping, runeforging, soulbinds, keystones, item upgrades,
-- recrafting, bank tabs), and the stock bag and character frames never
-- ask for it.
-- - No ItemUtil.CreateItemTransmogInfo or ItemTransmogInfoMixin: there is
-- no transmogrification.
-- - ItemUtil.GetItemDetails has no currency path: there are no currencies.
-- - No ItemUtil.GetCraftingReagentCount: it picks between the reagent bank
-- and the account bank, and neither exists.
----------------------------------------------------------------------
Item = {}
ItemMixin = {}
local ItemEventListener
--[[static]] function Item:CreateFromItemLocation(itemLocation)
if type(itemLocation) ~= "table" or type(itemLocation.HasAnyLocation) ~= "function" or not itemLocation:HasAnyLocation() then
error("Usage: Item:CreateFromItemLocation(notEmptyItemLocation)", 2)
end
local item = CreateFromMixins(ItemMixin)
item:SetItemLocation(itemLocation)
return item
ItemUtil = {};
function ItemUtil.GetItemDetails(itemLink, quantity)
local itemName, itemRarity, itemTexture, _;
itemName, itemLink, itemRarity, _, _, _, _, _, _, itemTexture = C_Item.GetItemInfo(itemLink);
return itemName, itemTexture, quantity, itemRarity, itemLink;
end
--[[static]] function Item:CreateFromBagAndSlot(bagID, slotIndex)
if type(bagID) ~= "number" or type(slotIndex) ~= "number" then
error("Usage: Item:CreateFromBagAndSlot(bagID, slotIndex)", 2)
end
local item = CreateFromMixins(ItemMixin)
item:SetItemLocation(ItemLocation:CreateFromBagAndSlot(bagID, slotIndex))
return item
function ItemUtil.GetItemHyperlink(itemID)
local _, link = C_Item.GetItemInfo(itemID);
return link;
end
--[[static]] function Item:CreateFromEquipmentSlot(equipmentSlotIndex)
if type(equipmentSlotIndex) ~= "number" then
error("Usage: Item:CreateFromEquipmentSlot(equipmentSlotIndex)", 2)
end
local item = CreateFromMixins(ItemMixin)
item:SetItemLocation(ItemLocation:CreateFromEquipmentSlot(equipmentSlotIndex))
return item
end
--[[static]] function Item:CreateFromItemLink(itemLink)
if type(itemLink) ~= "string" then
error("Usage: Item:CreateFromItemLink(itemLinkString)", 2)
end
local item = CreateFromMixins(ItemMixin)
item:SetItemLink(itemLink)
return item
end
--[[static]] function Item:CreateFromItemID(itemID)
if type(itemID) ~= "number" then
error("Usage: Item:CreateFromItemID(itemID)", 2)
end
local item = CreateFromMixins(ItemMixin)
item:SetItemID(itemID)
return item
end
--[[static]] function Item:CreateFromItemGUID(itemGUID)
if type(itemGUID) ~= "string" then
error("Usage: Item:CreateFromItemGUID(itemGUIDString)", 2);
function ItemUtil.PickupBagItem(itemLocation)
local bag, slot = itemLocation:GetBagAndSlot();
if bag and slot then
C_Container.PickupContainerItem(bag, slot);
end
local item = CreateFromMixins(ItemMixin);
item:SetItemGUID(itemGUID);
return item;
end
--[[static]] function Item:DoItemsMatch(item1, item2)
if not item1 or not item2 then
return false;
function ItemUtil.IterateBagSlots(bag, callback)
-- Only includes the backpack and held bag slots.
for slot = 1, ContainerFrame_GetContainerNumSlots(bag) do
local itemLocation = ItemLocation:CreateFromBagAndSlot(bag, slot);
if C_Item.DoesItemExist(itemLocation) then
if callback(itemLocation) then
return true;
end
end
end
return false;
end
function ItemUtil.IterateInventorySlots(firstSlot, lastSlot, callback)
for slot = firstSlot, lastSlot do
local itemLocation = ItemLocation:CreateFromEquipmentSlot(slot);
if C_Item.DoesItemExist(itemLocation) then
if callback(itemLocation) then
return;
end
end
end
end
function ItemUtil.IteratePlayerInventory(callback)
-- Only includes the backpack and held bag slots.
for bag = Enum.BagIndex.Backpack, NUM_TOTAL_BAG_FRAMES do
if ItemUtil.IterateBagSlots(bag, callback) then
return true;
end
end
return item1:Matches(item2);
end
function ItemMixin:Matches(item)
if not item then
return false;
end
local itemID = item:GetItemID();
if (itemID ~= nil) and (self:GetItemID() == itemID) then
return true;
end
local itemLocation = item:GetItemLocation();
if (itemLocation ~= nil) and itemLocation:IsEqualTo(self:GetItemLocation()) then
return true;
if(C_ActionBar.ShouldShowKeyring()) then
if ItemUtil.IterateBagSlots(KEYRING_CONTAINER, callback) then
return true;
end
end
return false;
end
function ItemMixin:SetItemLocation(itemLocation)
self:Clear()
self.itemLocation = itemLocation
function ItemUtil.IteratePlayerInventoryAndEquipment(callback)
local found = ItemUtil.IteratePlayerInventory(callback);
if not found then
ItemUtil.IterateInventorySlots(INVSLOT_FIRST_EQUIPPED, INVSLOT_LAST_EQUIPPED, callback);
end
end
function ItemMixin:SetItemLink(itemLink)
self:Clear()
self.itemLink = itemLink
function ItemUtil.FilterOwnedItems(itemIDs)
local found = {};
ItemUtil.IteratePlayerInventory(function(itemLocation)
local itemID = C_Item.GetItemID(itemLocation);
TableUtil.TrySet(found, itemID);
end);
local filtered = {};
for index, itemID in ipairs(itemIDs) do
if found[itemID] then
table.insert(filtered, itemID);
end
end
return filtered;
end
function ItemMixin:SetItemID(itemID)
self:Clear()
self.itemID = itemID
function ItemUtil.TransformItemIDsToItems(itemIDs)
local items = {};
for index, itemID in ipairs(itemIDs) do
table.insert(items, Item:CreateFromItemID(itemID));
end
return items;
end
function ItemMixin:SetItemGUID(itemGUID)
self:Clear();
self.itemGUID = itemGUID;
function ItemUtil.TransformItemLocationsToItems(itemLocations)
local items = {};
for index, itemLocation in ipairs(itemLocations) do
table.insert(items, Item:CreateFromItemLocation(itemLocation));
end
return items;
end
function ItemMixin:GetItemLocation()
if self.itemLocation then
return self.itemLocation;
function ItemUtil.TransformItemGUIDsToItems(itemGUIDs)
local items = {};
for index, itemGUID in ipairs(itemGUIDs) do
local item = Item:CreateFromItemGUID(itemGUID);
table.insert(items, item);
end
return items;
end
function ItemUtil.TransformItemLocationItemsToGUIDItems(itemLocations)
local items = {};
for index, itemLocation in ipairs(itemLocations) do
table.insert(items, Item:CreateFromItemLocation(itemLocation));
end
return items;
end
function ItemUtil.GetValidatedItemLocation(equipSlot)
if equipSlot then
local itemLocation = ItemLocation:CreateFromEquipmentSlot(equipSlot);
if itemLocation:IsValid() then
return itemLocation;
end
end
if self.itemGUID then
return C_Item.GetItemLocation(self.itemGUID);
end
return nil;
end
function ItemMixin:HasItemLocation()
return self.itemLocation ~= nil
end
function ItemMixin:Clear()
self.itemLocation = nil
self.itemLink = nil
self.itemID = nil
self.itemGUID = nil
end
function ItemMixin:IsItemEmpty()
if self:GetStaticBackingItem() then
return not C_Item.DoesItemExistByID(self:GetStaticBackingItem())
end
return not self:IsItemInPlayersControl()
end
function ItemMixin:GetStaticBackingItem()
return self.itemLink or self.itemID
end
function ItemMixin:IsItemInPlayersControl()
local itemLocation = self:GetItemLocation()
return itemLocation and C_Item.DoesItemExist(itemLocation)
end
-- Item API
function ItemMixin:GetItemID()
if self:GetStaticBackingItem() then
return (C_Item.GetItemInfoInstant(self:GetStaticBackingItem()))
end
if not self:IsItemEmpty() then
return C_Item.GetItemID(self:GetItemLocation())
end
return nil
end
function ItemMixin:IsItemLocked()
return self:IsItemInPlayersControl() and C_Item.IsLocked(self:GetItemLocation())
end
function ItemMixin:LockItem()
if self:IsItemInPlayersControl() then
C_Item.LockItem(self:GetItemLocation())
end
end
function ItemMixin:UnlockItem()
if self:IsItemInPlayersControl() then
C_Item.UnlockItem(self:GetItemLocation())
end
end
function ItemMixin:GetItemIcon() -- requires item data to be loaded
if self:GetStaticBackingItem() then
return C_Item.GetItemIconByID(self:GetStaticBackingItem())
end
if not self:IsItemEmpty() then
return C_Item.GetItemIcon(self:GetItemLocation())
end
end
function ItemMixin:GetItemName() -- requires item data to be loaded
if self:GetStaticBackingItem() then
return C_Item.GetItemNameByID(self:GetStaticBackingItem())
end
if not self:IsItemEmpty() then
return C_Item.GetItemName(self:GetItemLocation())
end
return nil
end
function ItemMixin:GetItemLink() -- requires item data to be loaded
if self.itemLink then
return self.itemLink
end
if self.itemID then
return (select(2, C_Item.GetItemInfo(self.itemID)));
end
if not self:IsItemEmpty() then
return C_Item.GetItemLink(self:GetItemLocation())
end
return nil
end
function ItemMixin:GetItemQuality() -- requires item data to be loaded
if self:GetStaticBackingItem() then
return C_Item.GetItemQualityByID(self:GetStaticBackingItem())
end
if not self:IsItemEmpty() then
return C_Item.GetItemQuality(self:GetItemLocation())
end
return nil
end
function ItemMixin:GetCurrentItemLevel() -- requires item data to be loaded
if self:GetStaticBackingItem() then
return (C_Item.GetDetailedItemLevelInfo(self:GetStaticBackingItem()))
end
if not self:IsItemEmpty() then
return C_Item.GetCurrentItemLevel(self:GetItemLocation())
end
return nil
end
function ItemMixin:GetItemQualityColor() -- requires item data to be loaded
local itemQuality = self:GetItemQuality()
return ITEM_QUALITY_COLORS[itemQuality] -- may be nil if item data isn't loaded
end
function ItemMixin:GetItemQualityColorRGB() -- requires item data to be loaded
local colorTbl = self:GetItemQualityColor()
return colorTbl.color:GetRGB()
end
function ItemMixin:GetItemMaxStackSize() -- requires item data to be loaded
if self:GetStaticBackingItem() then
return C_Item.GetItemMaxStackSizeByID(self:GetStaticBackingItem());
function ItemUtil.GetEmptyEquipSlotTooltipForSlotName(equipSlotName)
if equipSlotName then
return _G[strupper(equipSlotName)];
end
if not self:IsItemEmpty() then
return C_Item.GetItemMaxStackSize(self:GetItemLocation());
end
return nil;
end
function ItemMixin:IsStackable() -- requires item data to be loaded
local maxStackSize = self:GetItemMaxStackSize();
return maxStackSize and maxStackSize > 1;
function ItemUtil.GetEmptyEquipSlotTooltip(equipSlot)
if equipSlot then
local _, _, _, slotName = C_PaperDollInfo.GetInventorySlotInfoForInvSlot(equipSlot);
return ItemUtil.GetEmptyEquipSlotTooltipForSlotName(slotName);
end
return nil;
end
function ItemMixin:GetInventoryType()
if self:GetStaticBackingItem() then
return C_Item.GetItemInventoryTypeByID(self:GetStaticBackingItem())
end
function ItemUtil.DisplayEquipSlotTooltip(frame, tooltip, equipSlot, suppressComparison, checkRelic)
if suppressComparison then
GameTooltip_SuppressAutomaticCompareItem(tooltip);
end
if not self:IsItemEmpty() then
return C_Item.GetItemInventoryType(self:GetItemLocation())
end
return nil
local hasItem, hasCooldown, repairCost = tooltip:SetInventoryItem("player", equipSlot);
if not hasItem then
-- This SetOwner is needed because calling SetInventoryItem now hides tooltip if there is no item
tooltip:SetOwner(frame, "ANCHOR_RIGHT");
local asRelic = checkRelic and UnitHasRelicSlot("player");
if asRelic then
tooltip:SetText(RELICSLOT);
else
local text = ItemUtil.GetEmptyEquipSlotTooltip(equipSlot);
tooltip:SetText(text);
tooltip:Show();
end
end
end
function ItemMixin:GetItemGUID()
if self:GetStaticBackingItem() then
return nil
end
function ItemUtil.GetEquipSlotTexture(equipSlot)
-- First try to look up the equipped item's texture from the equipped item in the slot
-- If there's no equipment there, then use the paper doll slot texture for an empty slot
-- If this cooldown item doesn't refer to equipment, don't return a texture.
if equipSlot then
local itemLocation = ItemUtil.GetValidatedItemLocation(equipSlot);
if itemLocation then
local icon = C_Item.GetItemIcon(itemLocation);
return icon;
end
if not self:IsItemEmpty() then
return C_Item.GetItemGUID(self:GetItemLocation())
end
return nil
end
local _, icon = C_PaperDollInfo.GetInventorySlotInfoForInvSlot(equipSlot);
return icon;
end
function ItemMixin:GetInventoryTypeName()
if not self:IsItemEmpty() then
return select(4, C_Item.GetItemInfoInstant(self:GetItemID()));
end
end
function ItemMixin:IsItemDataCached()
if self:GetStaticBackingItem() then
return C_Item.IsItemDataCachedByID(self:GetStaticBackingItem())
end
if not self:IsItemEmpty() then
return C_Item.IsItemDataCached(self:GetItemLocation())
end
return true
end
function ItemMixin:IsDataEvictable()
return true
end
-- Default error handler routing: stock 1.12 has no CallErrorHandler.
local function ItemUtilErrorHandler(err)
local h = geterrorhandler and geterrorhandler()
if h then return h(err) end
end
function ItemMixin:ContinueOnItemLoad(callbackFunction)
if type(callbackFunction) ~= "function" or self:IsItemEmpty() then
error("Usage: NonEmptyItem:ContinueOnLoad(callbackFunction)", 2)
end
ItemEventListener:AddCallback(self:GetItemID(), callbackFunction)
end
function ItemMixin:ContinueWithCancelOnItemLoad(callbackFunction)
if type(callbackFunction) ~= "function" or self:IsItemEmpty() then
error("Usage: NonEmptyItem:ContinueWithCancelOnItemLoad(callbackFunction)", 2)
end
return ItemEventListener:AddCancelableCallback(self:GetItemID(), callbackFunction)
end
--[ Item Event Listener ]
ItemEventListener = CreateFrame("Frame")
ItemEventListener.callbacks = {}
-- 1.12 OnEvent receives no function args; event name and payload live in
-- the `event`, `arg1`, ... globals.
ItemEventListener:SetScript("OnEvent", function()
if event == "ITEM_DATA_LOAD_RESULT" then
local itemID, success = arg1, arg2
if success then
ItemEventListener:FireCallbacks(itemID)
else
ItemEventListener:ClearCallbacks(itemID)
end
end
end)
ItemEventListener:RegisterEvent("ITEM_DATA_LOAD_RESULT")
local CANCELED_SENTINEL = -1
function ItemEventListener:AddCallback(itemID, callbackFunction)
local callbacks = self:GetOrCreateCallbacks(itemID)
table.insert(callbacks, callbackFunction)
C_Item.RequestLoadItemDataByID(itemID)
end
function ItemEventListener:AddCancelableCallback(itemID, callbackFunction)
local callbacks = self:GetOrCreateCallbacks(itemID)
table.insert(callbacks, callbackFunction)
C_Item.RequestLoadItemDataByID(itemID)
local index = table.getn(callbacks)
return function()
if table.getn(callbacks) > 0 and callbacks[index] ~= CANCELED_SENTINEL then
callbacks[index] = CANCELED_SENTINEL
return true
end
return false
end
end
function ItemEventListener:FireCallbacks(itemID)
local callbacks = self:GetCallbacks(itemID)
if callbacks then
self:ClearCallbacks(itemID)
for i, callback in ipairs(callbacks) do
if callback ~= CANCELED_SENTINEL then
xpcall(callback, ItemUtilErrorHandler)
end
end
for i = table.getn(callbacks), 1, -1 do
callbacks[i] = nil
end
end
end
function ItemEventListener:ClearCallbacks(itemID)
self.callbacks[itemID] = nil
end
function ItemEventListener:GetCallbacks(itemID)
return self.callbacks[itemID]
end
function ItemEventListener:GetOrCreateCallbacks(itemID)
local callbacks = self.callbacks[itemID]
if not callbacks then
callbacks = {}
self.callbacks[itemID] = callbacks
end
return callbacks
return nil;
end
+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
@@ -1,792 +1,27 @@
-- Trade-skill *list* links (`|Htrade:...|h`) — backport of the 2.x+ shareable
-- profession link. The link is built entirely in the DLL
-- (`GetTradeSkillListLink` / `GetCraftListLink`, see src/tradeskill/Link.cpp);
-- this file only intercepts clicks on received links and renders the linker's
-- recipe list in a scroll frame (the UI half).
--
-- link: |cffffd000|Htrade:<skillLineID>:<curRank>:<maxRank>:<linkerName>:<base64 bits>|h[Name]|h|r
--
-- "Which recipes are known" is a bitfield over the skill line's canonical
-- recipe list (deterministic per DBC). The viewer lists the recipes the linker
-- knows (with an "X of Y" summary of the whole skill line), regardless of what
-- the reader can craft themselves.
-- Profession links (`|Htrade:...|h`), 3.3.5's UI half. The DLL builds the
-- link (GetTradeSkillListLink / GetCraftListLink, src/tradeskill/Link.cpp)
-- and opens a received one in the profession window itself
-- (src/tradeskill/Window.cpp), through ItemRefTooltip:SetHyperlink as
-- SetItemRef already calls it. This file ports the parts of 3.3.5's
-- Blizzard_TradeSkillUI that go with that:
-- - TradeSkillLinkButton: a button by the window title that puts the
-- window's link in the chat box, shown when the window has a link and
-- shows the player's own profession;
-- - TradeSkillFrame_SetSelection's linked branch: a window that shows
-- another player's profession hides what only works on the player's own
-- (Create, Create All and the count controls).
-- 3.3.5 never shows whose profession a linked window holds. 4.3.4 does, with
-- TradeSkillLinkNameButton and TradeSkillFrame_SetLinkName, ported here too.
-- The Craft window gets the same through GetCraftListLink and IsCraftLinked.
-- Frame geometry mirrors the real TradeSkillFrame. Turtle ships an HD
-- 768x512 two-column frame (TW-TradeSkill art); stock 1.12.1 only has the
-- vanilla 384x512 single-column art, so on that client we fall back to the
-- native Blizzard_TradeSkillUI geometry (list on top, detail below) rather
-- than stretch the wide art. LO holds every layout value that differs.
local TURTLE = (TURTLE_WOW_VERSION ~= nil);
local FRAME_W = TURTLE and 768 or 384;
local FRAME_H = 512;
local NUM_ROWS = TURTLE and 19 or 8;
local ROW_HEIGHT = 16;
local LO = TURTLE and {
hit = { 2, 82, 2, 73 },
quads = {
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-TopLeft", 512, 256, "TOPLEFT" },
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-TopRight", 256, 256, "TOPRIGHT" },
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-BotLeft2", 512, 256, "BOTTOMLEFT" },
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-BotRight2", 256, 256, "BOTTOMRIGHT" },
},
close = { -77, -8 },
bar = { 596, 15, 76, -44 },
barBdr = { 604, 22 },
summary = { "TOPRIGHT", -110, -70 },
drop = { 40, -60 },
scroll = { 296, 330, 37, -98 },
-- detail scroll frame: { x, y, width, height }. Tall enough that the
-- content fits (scrollbar auto-hides) — Turtle's right column is roomy.
detail = { 371, -110, 340, 290 },
exit = { "CENTER", "BOTTOMRIGHT", -128, 92 },
} or {
hit = { 0, 34, 0, 75 },
quads = {
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopLeft", 256, 256, "TOPLEFT" },
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopRight", 128, 256, "TOPRIGHT" },
{ "Interface\\TradeSkillFrame\\UI-TradeSkill-BotLeft", 256, 256, "BOTTOMLEFT" },
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-BotRight", 128, 256, "BOTTOMRIGHT" },
},
close = { -29, -8 },
bar = { 268, 15, 73, -37 },
barBdr = { 278, 22 },
summary = { "TOPRIGHT", -40, -58 },
drop = { 12, -62 },
scroll = { 296, 130, 21, -96 },
-- detail scroll frame { x, y, width, height } — native TradeSkillFrame's
-- TradeSkillDetailScrollFrame values. Small enough that many-reagent
-- recipes scroll (clipping partial rows), like native.
detail = { 20, -234, 297, 176 },
exit = { "CENTER", "TOPLEFT", 305, -422 },
sep = { 15, -221, 331 },
local TRADE_SKILL_CREATE_CONTROLS = {
"TradeSkillCreateButton",
"TradeSkillCreateAllButton",
"TradeSkillDecrementButton",
"TradeSkillInputBox",
"TradeSkillIncrementButton",
};
-- Reagents render in a 2-column grid inside the detail scroll child (see
-- the detail pane below). The detail is a real ScrollFrame like the
-- native TradeSkillFrame, so the whole pane pixel-scrolls and clips
-- partial reagent rows — `REAGENT_ROW_HEIGHT` is just the vertical stride
-- of one reagent row.
local REAGENT_ROW_HEIGHT = 43;
-- Scroll-frame templates. Stock mimics the native TradeSkillFrame, whose
-- list/detail scrolls use the ClassTrainer templates (they add the
-- decorative UI-ClassTrainer-ScrollBar track). Turtle's HD frame art
-- doesn't want that groove, so it keeps the plain templates. Both detail
-- templates are real ScrollFrames (ClassTrainerDetail... inherits
-- UIPanelScrollFrameTemplate), so the pixel-scroll logic is identical.
local LIST_SCROLL_TEMPLATE = TURTLE and "FauxScrollFrameTemplate"
or "ClassTrainerListScrollFrameTemplate";
local DETAIL_SCROLL_TEMPLATE = TURTLE and "UIPanelScrollFrameTemplate"
or "ClassTrainerDetailScrollFrameTemplate";
local PROFESSION_ICONS = {
[171] = "Trade_Alchemy",
[164] = "Trade_BlackSmithing",
[185] = "INV_Misc_Food_15", -- Cooking
[333] = "Trade_Engraving", -- Enchanting
[202] = "Trade_Engineering",
[129] = "Spell_Holy_SealOfSacrifice", -- First Aid
[165] = "INV_Misc_ArmorKit_17", -- Leatherworking
[197] = "Trade_Tailoring",
[186] = "Trade_Mining",
[182] = "Trade_Herbalism",
[393] = "INV_Misc_Pelt_Wolf_01", -- Skinning
[356] = "Trade_Fishing",
};
if TURTLE then
PROFESSION_ICONS[142] = "Trade_Survival"
PROFESSION_ICONS[755] = "INV_Misc_Gem_01" -- Jewelcrafting
end
local function ProfessionIcon(skillLineID)
local icon = PROFESSION_ICONS[skillLineID];
return icon and ("Interface\\Icons\\" .. icon)
or "Interface\\Icons\\INV_Misc_QuestionMark";
end
local function DifficultyBand(skill, hi, lo)
if not hi or hi <= 0 then
return 3;
end
if not lo or lo == 0 then
lo = (hi ~= 25) and (hi - 25) or 0;
end
local yellow = (hi + lo) / 2;
if skill < lo then
return 0; -- orange (optimal)
elseif skill < yellow then
return 1; -- yellow (medium)
elseif skill < hi then
return 2; -- green (easy)
end
return 3; -- grey (trivial)
end
local BAND_COLOR = {
[0] = DIFFICULT_DIFFICULTY_COLOR, -- orange
[1] = FAIR_DIFFICULTY_COLOR, -- yellow
[2] = EASY_DIFFICULTY_COLOR, -- green
[3] = TRIVIAL_DIFFICULTY_COLOR, -- grey
};
local function CraftableCount(entry)
local reagents = entry.reagents;
if not reagents or table.getn(reagents) == 0 then
return nil;
end
local best;
for i = 1, table.getn(reagents) do
local r = reagents[i];
local need = r.count or 1;
local n = math.floor((C_Item.GetItemCount(r.itemID) or 0) / need);
if not best or n < best then
best = n;
end
end
return best;
end
local function ParseTradeLink(link)
-- Current format, with the linker's name: trade:id:cur:max:Name:bits
local _, _, id, cur, max, who, bits =
string.find(link, "^trade:(%d+):(%d+):(%d+):([^:]+):([%w%+/]*)$");
if not id then
-- Legacy format (no name): trade:id:cur:max:bits
who = nil;
_, _, id, cur, max, bits =
string.find(link, "^trade:(%d+):(%d+):(%d+):([%w%+/]*)$");
end
if not id then
return nil;
end
return tonumber(id), tonumber(cur), tonumber(max), who, bits or "";
end
------------------------------------------------------------------------------
-- Display frame (built lazily on first use)
------------------------------------------------------------------------------
local frame;
local function ResolveSubclasses(list, onLoaded)
local present, subs, anyItem = {}, {}, false;
for i = 1, table.getn(list) do
local r = list[i];
if r.createdItem and r.createdItem > 0 then
anyItem = true;
if not r.subclass then
-- GetItemInfoInstant's 3rd return is the localized subclass
-- name (or nil when uncached).
local _, _, subType = C_Item.GetItemInfoInstant(r.createdItem);
if subType and subType ~= "" then
r.subclass = subType;
elseif onLoaded then
-- Not cached — re-derive when the item loads.
Item:CreateFromItemID(r.createdItem):ContinueOnItemLoad(onLoaded);
end
end
if r.subclass and not present[r.subclass] then
present[r.subclass] = true;
table.insert(subs, r.subclass);
end
end
end
table.sort(subs);
return subs, anyItem;
end
local SUBCLASS_ALL = {};
local function SubClassButton_OnClick()
local v = this.value;
if v == SUBCLASS_ALL then
v = nil;
end
frame.subclassFilter = v;
UIDropDownMenu_SetText(v or ALL_SUBCLASSES, frame.dropdown);
frame:ApplyFilter();
end
local function SubClassDropDown_Initialize()
local info = {};
info.text = ALL_SUBCLASSES;
info.value = SUBCLASS_ALL;
info.checked = (frame.subclassFilter == nil);
info.func = SubClassButton_OnClick;
UIDropDownMenu_AddButton(info);
local subs = frame.subclasses or {};
for i = 1, table.getn(subs) do
local b = {};
b.text = subs[i];
b.value = subs[i];
b.checked = (frame.subclassFilter == subs[i]);
b.func = SubClassButton_OnClick;
UIDropDownMenu_AddButton(b);
end
end
local function RefreshSubclasses()
if not (frame and frame:IsShown() and frame.recipes) then
return;
end
local subs, anyItem = ResolveSubclasses(frame.recipes, RefreshSubclasses);
frame.subclasses = subs;
if anyItem then
UIDropDownMenu_Initialize(frame.dropdown, SubClassDropDown_Initialize);
UIDropDownMenu_SetWidth(120, frame.dropdown);
UIDropDownMenu_SetText(frame.subclassFilter or ALL_SUBCLASSES, frame.dropdown);
frame.dropdown:Show();
else
frame.dropdown:Hide();
end
if frame.subclassFilter then
frame:ApplyFilter();
end
end
local function CreateFrame_TradeSkillLink()
local f = CreateFrame("Frame", "ClassicAPITradeSkillLinkFrame", UIParent);
f:SetSize(FRAME_W, FRAME_H);
f:SetPoint("CENTER");
f:SetMovable(true);
f:EnableMouse(true);
f:SetHitRectInsets(LO.hit[1], LO.hit[2], LO.hit[3], LO.hit[4]);
f:RegisterForDrag("LeftButton");
f:SetScript("OnDragStart", function() this:StartMoving(); end);
f:SetScript("OnDragStop", function() this:StopMovingOrSizing(); end);
f:SetFrameStrata("DIALOG");
f:Hide();
tinsert(UISpecialFrames, f:GetName())
f:SetScript("OnShow", function() PlaySound("igCharacterInfoOpen"); end);
f:SetScript("OnHide", function() PlaySound("igCharacterInfoClose"); end);
local portrait = f:CreateTexture(nil, "BACKGROUND");
portrait:SetSize(60, 60);
portrait:SetPoint("TOPLEFT", 7, -6);
f.portrait = portrait;
for i = 1, table.getn(LO.quads) do
local q = LO.quads[i];
local tx = f:CreateTexture(nil, "BORDER");
tx:SetTexture(q[1]);
tx:SetSize(q[2], q[3]);
tx:SetPoint(q[4]);
end
-- Horizontal divider between the recipe list and the detail pane.
-- Stock only (Turtle's HD frame art already has its own).
if LO.sep then
-- Decoded the BLP: it's a thin divider rod whose graphic lives in the
-- top quarter of the 256x64 texture (a 2nd rod fills the next quarter,
-- bottom half empty). So sample the top quarter, stretched to width —
-- the same region the native TradeSkillFrame uses for this divider.
local bar = f:CreateTexture(nil, "ARTWORK");
bar:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-HorizontalBar");
bar:SetTexCoord(0, 1, 0, 0.25);
bar:SetSize(LO.sep[3], 16);
bar:SetPoint("TOPLEFT", LO.sep[1], LO.sep[2]);
end
local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormal");
title:SetPoint("TOP", 0, -18);
f.title = title;
local close = CreateFrame("Button", nil, f, "UIPanelCloseButton");
close:SetPoint("TOPRIGHT", LO.close[1], LO.close[2]);
local exit = CreateFrame("Button", nil, f, "UIPanelButtonTemplate");
exit:SetSize(80, 22);
exit:SetPoint(LO.exit[1], f, LO.exit[2], LO.exit[3], LO.exit[4]);
exit:SetText(EXIT);
exit:SetScript("OnClick", function() f:Hide(); end);
local bar = CreateFrame("StatusBar", nil, f);
bar:SetSize(LO.bar[1], LO.bar[2]);
bar:SetPoint("TOPLEFT", LO.bar[3], LO.bar[4]);
bar:SetStatusBarTexture("Interface\\PaperDollInfoFrame\\UI-Character-Skills-Bar");
bar:SetStatusBarColor(0.0, 0.0, 1.0, 0.5);
bar:SetMinMaxValues(0, 1);
local barBg = bar:CreateTexture(nil, "BACKGROUND");
barBg:SetAllPoints(bar);
barBg:SetTexture(1, 1, 1, 0.2);
barBg:SetVertexColor(0.0, 0.0, 0.75, 0.5);
-- Border: tooltip edge around the bar (TradeSkillRankFrameBorder).
local border = CreateFrame("Frame", nil, bar);
border:SetSize(LO.barBdr[1], LO.barBdr[2]);
border:SetPoint("LEFT", -5, 1);
border:SetBackdrop({
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 16,
});
border:SetBackdropBorderColor(0.4, 0.4, 0.4);
local barName = bar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall");
barName:SetPoint("LEFT", 6, 1);
local barRank = bar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall");
barRank:SetJustifyH("LEFT");
barRank:SetWidth(128);
barRank:SetPoint("LEFT", barName, "RIGHT", 13, 0);
f.bar = bar;
f.barName = barName;
f.barRank = barRank;
local summary = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall");
summary:SetPoint(LO.summary[1], LO.summary[2], LO.summary[3]);
f.summary = summary;
local dropdown = CreateFrame("Frame", "ClassicAPITradeSkillLinkSubClass", f,
"UIDropDownMenuTemplate");
dropdown:SetPoint("TOPLEFT", LO.drop[1], LO.drop[2]);
f.dropdown = dropdown;
f.subclassFilter = nil; -- nil = all subclasses
local scroll = CreateFrame("ScrollFrame", "ClassicAPITradeSkillLinkScroll",
f, LIST_SCROLL_TEMPLATE);
scroll:SetSize(LO.scroll[1], LO.scroll[2]);
scroll:SetPoint("TOPLEFT", LO.scroll[3], LO.scroll[4]);
scroll:SetScript("OnVerticalScroll", function()
FauxScrollFrame_OnVerticalScroll(ROW_HEIGHT, function() f:Refresh(); end)
end)
f.scroll = scroll;
f.rows = {};
for i = 1, NUM_ROWS do
local rowName = "ClassicAPITradeSkillLinkRow" .. i;
local row = CreateFrame("Button", rowName, f,
"ClassTrainerSkillButtonTemplate");
row:SetWidth(290); -- height (16) comes from the template
if i == 1 then
row:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0);
else
row:SetPoint("TOPLEFT", f.rows[i - 1], "BOTTOMLEFT", 0, 0);
end
row:SetNormalTexture("");
row:SetDisabledTexture("");
row:SetHighlightTexture("");
local txt = getglobal(rowName .. "Text");
if txt then
txt:ClearAllPoints();
txt:SetPoint("LEFT", row, "LEFT", 4, 0);
end
local sel = row:CreateTexture(nil, "BACKGROUND");
sel:SetTexture("Interface\\Buttons\\UI-Listbox-Highlight2");
sel:SetAllPoints(row);
sel:Hide();
row.select = sel;
-- Override the template's trainer scripts with ours.
row:SetScript("OnClick", function()
if this.entry then
f:SelectRecipe(this.entry);
end
end);
f.rows[i] = row;
end
-- Detail pane: a real ScrollFrame (like the native TradeSkillFrame's
-- TradeSkillDetailScrollFrame) whose scroll child holds the product
-- header + reagent grid, so the whole pane pixel-scrolls — clipping
-- partial reagent rows — with the standard UIPanelScrollBar. The bar
-- auto-hides when the content fits (scrollBarHideable).
local detailScroll = CreateFrame("ScrollFrame",
"ClassicAPITradeSkillLinkDetailScroll", f, DETAIL_SCROLL_TEMPLATE);
detailScroll:SetSize(LO.detail[3], LO.detail[4]);
detailScroll:SetPoint("TOPLEFT", LO.detail[1], LO.detail[2]);
-- Bar visibility + wheel are driven per-render by SetDetailBarShown (from
-- the computed overflow). `scrollBarHideable` still lets the template's
-- ScrollFrame_OnScrollRangeChanged manage the slider on range changes.
-- Start with the wheel off; the first RenderDetail/ClearDetail sets it.
detailScroll.scrollBarHideable = 1;
detailScroll:EnableMouseWheel(false);
f.detailScroll = detailScroll;
local detailChild = CreateFrame("Frame",
"ClassicAPITradeSkillLinkDetailChild", detailScroll);
detailChild:SetSize(LO.detail[3], LO.detail[4]);
detailScroll:SetScrollChild(detailChild);
f.detailChild = detailChild;
local prodIcon = CreateFrame("Button", nil, detailChild);
prodIcon:SetSize(37, 37); -- native TradeSkillSkillIcon (37x37 @ 8,-3)
prodIcon:SetPoint("TOPLEFT", 8, -3);
prodIcon:SetNormalTexture("Interface\\Icons\\INV_Misc_QuestionMark");
local hdrLeft = detailChild:CreateTexture(nil, "BACKGROUND");
hdrLeft:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-DetailHeaderLeft");
hdrLeft:SetSize(256, 64); -- Blizzard's native fill width (+64 cap = 320 total)
hdrLeft:SetPoint("TOPLEFT", prodIcon, "TOPLEFT", -8, 6);
local hdrRight = detailChild:CreateTexture(nil, "BACKGROUND");
hdrRight:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-DetailHeaderRight");
hdrRight:SetSize(64, 64);
hdrRight:SetPoint("TOPLEFT", hdrLeft, "TOPRIGHT", 0, 0);
local prodCount = prodIcon:CreateFontString(nil, "ARTWORK", "NumberFontNormal");
prodCount:SetPoint("BOTTOMRIGHT", -5, 2);
prodIcon.count = prodCount;
f.prodIcon = prodIcon;
local prodName = detailChild:CreateFontString(nil, "ARTWORK", "GameFontNormal");
prodName:SetPoint("LEFT", prodIcon, "RIGHT", 5, 0);
prodName:SetWidth(LO.detail[3] - 60);
prodName:SetJustifyH("LEFT");
prodName:SetJustifyV("MIDDLE");
f.prodName = prodName;
local prodDesc = detailChild:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall");
prodDesc:SetPoint("TOPLEFT", prodIcon, "BOTTOMLEFT", 0, -6);
prodDesc:SetWidth(LO.detail[3] - 20);
prodDesc:SetJustifyH("LEFT");
prodDesc:SetJustifyV("TOP");
f.prodDesc = prodDesc;
local reagentLabel = detailChild:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall");
reagentLabel:SetPoint("TOPLEFT", prodDesc, "BOTTOMLEFT", 0, -8);
reagentLabel:SetText(SPELL_REAGENTS or "Reagents:");
f.reagentLabel = reagentLabel;
prodIcon:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT");
if this.itemID then
GameTooltip:SetHyperlink("item:"..this.itemID);
elseif this.spellID then
GameTooltip:SetSpellByID(this.spellID);
end
GameTooltip:Show();
end);
prodIcon:SetScript("OnLeave", GameTooltip_Hide);
-- Reagent buttons live in the scroll child (2 per row), built on
-- demand so any reagent count is handled and they scroll/clip with
-- the child.
f.reagents = {};
local function AcquireReagent(i)
if f.reagents[i] then
return f.reagents[i];
end
local rowN = math.floor((i - 1) / 2);
local col = (i - 1) - rowN * 2;
local btn = CreateFrame("Button", nil, detailChild);
btn:SetSize(39, 39);
btn:SetPoint("TOPLEFT", reagentLabel, "BOTTOMLEFT",
col * 134 - 5, -3 - rowN * REAGENT_ROW_HEIGHT);
-- Extend the hit area rightward over the name plate so the tooltip
-- fires anywhere on the element, not just the icon. Stop short of the
-- next column (pitch 134 − icon 39) so hovers don't bleed across.
btn:SetHitRectInsets(0, -(134 - 39), 0, 0);
btn:SetNormalTexture("Interface\\Icons\\INV_Misc_QuestionMark");
local plate = btn:CreateTexture(nil, "BACKGROUND");
plate:SetTexture("Interface\\QuestFrame\\UI-QuestItemNameFrame");
plate:SetSize(114, 64);
plate:SetPoint("LEFT", btn, "RIGHT", -10, 0);
local cnt = btn:CreateFontString(nil, "ARTWORK", "NumberFontNormal");
cnt:SetPoint("BOTTOMRIGHT", -4, 1);
btn.count = cnt;
local nm = btn:CreateFontString(nil, "ARTWORK", "GameFontHighlight");
nm:SetPoint("LEFT", plate, "LEFT", 15, 0);
nm:SetSize(80, 36);
nm:SetJustifyH("LEFT");
nm:SetJustifyV("MIDDLE");
btn.nameText = nm;
btn:SetScript("OnEnter", function()
if this.itemID then
GameTooltip:SetOwner(this, "ANCHOR_RIGHT");
GameTooltip:SetHyperlink("item:"..this.itemID);
GameTooltip:Show();
end
end);
btn:SetScript("OnClick", function()
if this.itemLink and IsShiftKeyDown() and ChatFrameEditBox:IsVisible() then
ChatFrameEditBox:Insert(this.itemLink);
end
end);
btn:SetScript("OnLeave", GameTooltip_Hide);
f.reagents[i] = btn;
return btn;
end
-- Show the detail bar/borders only when the content overflows the pane.
-- Driven by our own computed overflow, NOT the engine: scrollBarHideable's
-- auto-hide only fires on a range CHANGE (so content that fits from the
-- start never hides), and GetVerticalScrollRange isn't reliably readable
-- the same tick as UpdateScrollChildRect.
function f:SetDetailBarShown(show)
self.detailScroll:EnableMouseWheel(show);
local base = "ClassicAPITradeSkillLinkDetailScroll";
local sb = getglobal(base .. "ScrollBar");
if sb then sb:SetShown(show) end
local top = getglobal(base .. "Top");
if top then top:SetShown(show) end
local bottom = getglobal(base .. "Bottom");
if bottom then bottom:SetShown(show) end
end
function f:RenderDetail(entry)
local function itemInfo(id)
local item = Item:CreateFromItemID(id);
if not item:IsItemDataCached() then
item:ContinueOnItemLoad(function()
if self:IsShown() and self.selectedSpellID == entry.spellID then
self:RenderDetail(entry);
end
end);
end
return item:GetItemName(), item:GetItemIcon(), item:GetItemLink();
end
local prodTex, prodText, prodLink;
local created = entry.createdItem or 0;
if created > 0 then
local name, tex, link = itemInfo(created);
prodText = name or entry.name;
prodTex = tex;
prodLink = link;
else
prodText = entry.name;
prodTex = C_Spell.GetSpellTexture(entry.spellID);
end
local descText = (created > 0) and ""
or (C_Spell.GetSpellDescription(entry.spellID) or "");
self.prodDesc:SetText(descText);
self.prodIcon:SetNormalTexture(prodTex or
"Interface\\Icons\\INV_Misc_QuestionMark");
self.prodName:SetText(prodText or "");
self.prodName:SetTextColor(NORMAL_FONT_COLOR:GetRGB());
self.prodIcon.itemLink = prodLink; -- item tooltip when created > 0
self.prodIcon.spellID = entry.spellID; -- else spell tooltip
local made = entry.numMade or 0;
self.prodIcon.count:SetText(made > 1 and tostring(made) or "");
self.prodIcon:Show();
-- Fill every reagent slot; the scroll child clips/scrolls the
-- overflow. Buttons past the count are hidden.
local reagents = entry.reagents or {};
local total = table.getn(reagents);
for i = 1, total do
local btn = AcquireReagent(i);
local reagent = reagents[i];
local name, tex, link = itemInfo(reagent.itemID);
btn:SetNormalTexture(tex or "Interface\\Icons\\INV_Misc_QuestionMark");
btn.itemLink = link;
btn.itemID = reagent.itemID
btn.nameText:SetText(name or ("item:" .. reagent.itemID));
local need = reagent.count or 1;
local have = C_Item.GetItemCount(reagent.itemID) or 0;
if have >= need then
local r, g, b = HIGHLIGHT_FONT_COLOR:GetRGB()
btn:GetNormalTexture():SetVertexColor(r, g, b);
btn.nameText:SetTextColor(r, g, b);
else
local r, g, b = GRAY_FONT_COLOR:GetRGB()
btn:GetNormalTexture():SetVertexColor(r, g, b);
btn.nameText:SetTextColor(r, g, b);
end
local haveText = (have >= 100) and "*" or tostring(have);
btn.count:SetText(haveText .. " /" .. need);
btn:Show();
end
for i = total + 1, table.getn(self.reagents) do
self.reagents[i].itemLink = nil;
self.reagents[i].itemID = nil;
self.reagents[i]:Hide();
end
local function applyContentHeight()
local contentH = LO.detail[4];
if total > 0 then
local childTop = self.detailChild:GetTop();
local lastBtn = self.reagents[total];
local lastBottom = lastBtn and lastBtn:GetBottom();
if childTop and lastBottom then
-- +22: ~12px name-plate overhang below the icon + ~10px pad.
contentH = (childTop - lastBottom) + 22;
end
end
self.detailChild:SetHeight(math.max(contentH, LO.detail[4]));
-- Recompute the scroll range from the child's new height, then
-- show the bar only if the content actually overflows the pane.
self.detailScroll:UpdateScrollChildRect();
self:SetDetailBarShown(contentH > LO.detail[4] + 0.5);
end
applyContentHeight();
local sb = getglobal("ClassicAPITradeSkillLinkDetailScrollScrollBar");
if sb then
sb:SetValue(0);
end
self.detailScroll:SetVerticalScroll(0);
RunNextFrame(function()
if self:IsShown() and self.selectedSpellID == entry.spellID then
applyContentHeight();
end
end)
end
function f:SelectRecipe(entry)
self.selectedSpellID = entry.spellID;
self.selectedEntry = entry;
self:RenderDetail(entry); -- also resets the detail scroll to top
self:Refresh();
end
function f:ClearDetail()
self.selectedSpellID = nil;
self.selectedEntry = nil;
self.prodIcon:Hide();
self.prodName:SetText("");
self.prodDesc:SetText("");
for i = 1, table.getn(self.reagents) do
self.reagents[i].itemLink = nil;
self.reagents[i].itemID = nil;
self.reagents[i]:Hide();
end
self.detailChild:SetHeight(LO.detail[4]);
self.detailScroll:UpdateScrollChildRect();
self:SetDetailBarShown(false);
self.detailScroll:SetVerticalScroll(0);
end
function f:Refresh()
local recipes = self.displayed or {};
local total = table.getn(recipes);
FauxScrollFrame_Update(self.scroll, total, NUM_ROWS, ROW_HEIGHT);
local offset = FauxScrollFrame_GetOffset(self.scroll);
for i = 1, NUM_ROWS do
local row = self.rows[i];
local entry = recipes[offset + i];
if entry then
-- Append " [N]" when the reader can craft any (Blizzard's list).
local craftable = CraftableCount(entry);
local label = entry.name or "";
if craftable and craftable > 0 then
label = label .. " [" .. craftable .. "]";
end
row:SetText(label);
local r, g, b = BAND_COLOR[entry.band or 3]:GetRGB();
if self.selectedSpellID and entry.spellID == self.selectedSpellID then
row.select:SetVertexColor(r, g, b);
row.select:Show();
row:SetTextColor(1, 1, 1);
else
row.select:Hide();
row:SetTextColor(r, g, b);
end
row.spellID = entry.spellID;
row.entry = entry;
row:Show();
else
row.spellID = nil;
row.entry = nil;
row.select:Hide();
row:Hide();
end
end
end
function f:ApplyFilter()
local src = self.recipes or {};
if not self.subclassFilter then
self.displayed = src;
else
local out = {};
for i = 1, table.getn(src) do
if src[i].subclass == self.subclassFilter then
table.insert(out, src[i]);
end
end
self.displayed = out;
end
local sb = getglobal("ClassicAPITradeSkillLinkScrollScrollBar");
if sb then
sb:SetValue(0);
end
self:Refresh();
end
f:RegisterEvent("BAG_UPDATE_DELAYED");
f:SetScript("OnEvent", function()
if not f:IsShown() then
return;
end
f:Refresh();
if f.selectedEntry then
f:RenderDetail(f.selectedEntry);
end
end);
return f;
end
local function ShowTradeSkillLink(link, text)
local id, cur, max, who, bits = ParseTradeLink(link);
if not id then
return false;
end
local recipes = C_TradeSkillUI.GetTradeSkillListRecipes(id, bits);
if not recipes then
return false;
end
local prof = text and string.match(text, "|h%[(.+)%]|h") or "Trade Skills";
local total = table.getn(recipes);
local knownList = {};
for i = 1, total do
local r = recipes[i];
if r.isKnown then
r.band = DifficultyBand(cur or 0, r.trivialLevel, r.greenLevel);
r.name = C_Spell.GetSpellName(r.spellID) or ("spell:" .. r.spellID);
table.insert(knownList, r);
end
end
table.sort(knownList, function(a, b)
if a.band ~= b.band then
return a.band < b.band;
end
return a.name < b.name;
end);
local known = table.getn(knownList);
if not frame then
frame = CreateFrame_TradeSkillLink();
end
frame.title:SetText(who and who ~= "" and (who .. " - " .. prof) or prof);
SetPortraitToTexture(frame.portrait, ProfessionIcon(id));
frame.bar:SetMinMaxValues(0, (max and max > 0) and max or 1);
frame.bar:SetValue(cur or 0);
frame.barName:SetText(prof);
frame.barRank:SetFormattedText("%d/%d", cur or 0, max or 0);
frame.summary:SetFormattedText("%d of %d recipes known", known, total);
frame.recipes = knownList;
frame.subclassFilter = nil;
frame:ClearDetail();
frame:Show();
RefreshSubclasses();
frame:ApplyFilter();
local first = frame.displayed and frame.displayed[1];
if first then
frame:SelectRecipe(first);
end
return true;
end
------------------------------------------------------------------------------
-- Click interception: handle `trade:` before the engine's SetHyperlink (which
-- in 1.12 only knows item:/enchant: and would reject the link). Registered
-- on LinkUtil's SetItemRef dispatch (Util/LinkUtil.lua) instead of wrapping
-- SetItemRef a second time, so there is exactly one override to chain.
------------------------------------------------------------------------------
LinkUtil.RegisterLinkHandler("trade", function(link, text, linkData, contextData)
if IsShiftKeyDown() and ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
ChatFrameEditBox:Insert(text);
return LinkProcessorResponse.Handled;
end
if ShowTradeSkillLink(link, text) then
return LinkProcessorResponse.Handled;
end
return LinkProcessorResponse.Unhandled;
end);
local function CreateProfessionLinkButton(parentName, getLink)
local parent = _G[parentName];
local btnName = parentName .. "LinkButton";
@@ -832,24 +67,84 @@ local function CreateProfessionLinkButton(parentName, getLink)
GameTooltip:Show();
end);
btn:SetScript("OnLeave", GameTooltip_Hide);
return btn;
end
local TRADE_SKILL_LINKED_NAME_WIDTH = 120;
-- 4.3.4's TradeSkillLinkNameButton: "[name]" after the title of a linked
-- window, a whisper to that player on click. The returned function is
-- TradeSkillFrame_SetLinkName. It moves the title left by half the name, from
-- the title's own anchor, so the pair stays centered where the title was.
local function CreateLinkNameButton(parentName, isLinked)
local title = _G[parentName .. "TitleText"];
local point, relativeTo, relativePoint, x, y = title:GetPoint();
local btn = CreateFrame("Button", parentName .. "LinkNameButton", _G[parentName]);
btn:SetHeight(20);
btn:SetPoint("LEFT", title, "RIGHT", 5, 0);
local text = btn:CreateFontString(btn:GetName() .. "TitleText", "OVERLAY", "GameFontHighlight");
text:SetAllPoints(btn);
btn:SetScript("OnClick", function()
PlaySound("igMainMenuOptionCheckBoxOn");
if this.linkedName then
ChatFrame_OpenChat(SLASH_WHISPER1 .. " " .. this.linkedName .. " ", DEFAULT_CHAT_FRAME);
end
end);
btn:Hide();
return function()
local _, linkedName = isLinked();
title:ClearAllPoints();
if linkedName then
btn:Show();
text:SetText("[" .. linkedName .. "]");
local linkedNameWidth = math.min(text:GetStringWidth(), TRADE_SKILL_LINKED_NAME_WIDTH);
btn:SetWidth(linkedNameWidth);
btn.linkedName = linkedName;
title:SetPoint(point, relativeTo, relativePoint, x - linkedNameWidth / 2, y);
else
btn:Hide();
title:SetPoint(point, relativeTo, relativePoint, x, y);
end
end
end
EventUtil.ContinueOnAddOnLoaded("Blizzard_TradeSkillUI", function()
CreateProfessionLinkButton("TradeSkillFrame", C_TradeSkillUI.GetTradeSkillListLink);
local linkButton = CreateProfessionLinkButton("TradeSkillFrame", GetTradeSkillListLink);
local setLinkName = CreateLinkNameButton("TradeSkillFrame", IsTradeSkillLinked);
hooksecurefunc("TradeSkillFrame_SetSelection", function()
local linked = IsTradeSkillLinked();
for i = 1, table.getn(TRADE_SKILL_CREATE_CONTROLS) do
_G[TRADE_SKILL_CREATE_CONTROLS[i]]:SetShown(not linked);
end
if linkButton then
linkButton:SetShown(not linked and GetTradeSkillListLink() ~= nil);
end
setLinkName();
end);
end)
EventUtil.ContinueOnAddOnLoaded("Blizzard_CraftUI", function()
CreateProfessionLinkButton("CraftFrame", C_TradeSkillUI.GetCraftListLink)
local linkButton = CreateProfessionLinkButton("CraftFrame", GetCraftListLink);
local setLinkName = CreateLinkNameButton("CraftFrame", IsCraftLinked);
hooksecurefunc("CraftFrame_SetSelection", function()
local linked = IsCraftLinked();
CraftCreateButton:SetShown(not linked);
if linkButton then
linkButton:SetShown(not linked and GetCraftListLink() ~= nil);
end
setLinkName();
end);
end)
EventUtil.ContinueOnAddOnLoaded("AdvancedTradeSkillWindow", function()
CreateProfessionLinkButton("ATSWFrame", function()
return atsw_oldmode and C_TradeSkillUI.GetCraftListLink() or C_TradeSkillUI.GetTradeSkillListLink()
return atsw_oldmode and GetCraftListLink() or GetTradeSkillListLink()
end)
end)
EventUtil.ContinueOnAddOnLoaded("AdvancedTradeSkillWindow2", function()
CreateProfessionLinkButton("ATSWFrame", function()
return ATSW_TradeSkill() and C_TradeSkillUI.GetTradeSkillListLink() or C_TradeSkillUI.GetCraftListLink()
return ATSW_TradeSkill() and GetTradeSkillListLink() or GetCraftListLink()
end)
end)
@@ -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 = "新設定";
+7 -1
View File
@@ -17,6 +17,7 @@
#include "Offsets.h"
#include "debug/Log.h"
#include "frame/ScriptArgs.h"
#include "frame/UnitEvent.h"
#include "profile/ScriptTime.h"
#include <cstdint>
@@ -357,11 +358,16 @@ void FireValues(int slot, void *L, int firstArgIdx, int nargs) {
PushString(L, "event");
PushString(L, name);
SetTable(L, GLOBALS_INDEX);
// A string arg1 meets RegisterUnitEvent filters, which the engine's
// dispatches meet at the runner this walk never calls.
const char *unit = nargs > 0 && Type(L, firstArgIdx) == TYPE_STRING
? ToString(L, firstArgIdx)
: nullptr;
uintptr_t node = Game::Read<uintptr_t>(entry, Offsets::OFF_EVENT_ENTRY_HEAD);
while (node != 0 && (node & 1) == 0) {
const uintptr_t next = Game::Read<uintptr_t>(node + Offsets::OFF_EVENT_NODE_NEXT);
auto *frame = Game::Read<uint8_t *>(node + Offsets::OFF_EVENT_NODE_FRAME);
if (frame != nullptr)
if (frame != nullptr && (unit == nullptr || Frame::UnitEvent::Accepts(frame, name, unit)))
Frame::ScriptArgs::RunSlotWithValues(
L, reinterpret_cast<const uint32_t *>(frame + Offsets::OFF_FRAME_ONEVENT_SLOT),
frame, firstArgIdx, nargs);
+4 -3
View File
@@ -99,9 +99,10 @@ bool HasListeners(int slot);
// restored after, and each listener's OnEvent runs through
// `Frame::ScriptArgs::RunSlotWithValues` (so `arg1..argN`, `this` and the
// modern positional arguments are set the way the engine's runner sets
// them). The values stay on the stack; the caller pops them. No-op for
// `slot < 0` or an event nobody listens to. 4.3.4's equivalent is
// `FrameScript_SignalEvent(id, L, count)`.
// them). A string first value meets RegisterUnitEvent filters, as a `%s`
// arg1 does on the engine's path. The values stay on the stack; the caller
// pops them. No-op for `slot < 0` or an event nobody listens to. 4.3.4's
// equivalent is `FrameScript_SignalEvent(id, L, count)`.
void FireValues(int slot, void *L, int firstArgIdx, int nargs);
// Dispatches a custom event via the engine's printf-style event
+170 -236
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,
+34 -17
View File
@@ -37,6 +37,8 @@
// of its tokens, we return without invoking the handler. Suppressing at the
// runner also suppresses HookScript chains and Frame::ScriptArgs' positional
// path — they all live downstream of it, which is what retail does too.
// Event::Custom::FireValues walks the chain itself and never reaches that
// runner, so it asks `Accepts` (UnitEvent.h) for each frame instead.
//
// Semantics mirror Blizzard's 5.4.8 implementation (Ghidra-verified), where the
// filter lives ON the subscriber node and the node's kind is fixed when it is
@@ -66,6 +68,8 @@
// we never see), fires as with RegisterEvent — one rule that holds on every
// dispatch path, whose failure mode is an extra event rather than a lost one.
#include "UnitEvent.h"
#include "Game.h"
#include "Offsets.h"
#include "frame/RunnerHook.h"
@@ -249,6 +253,24 @@ int __fastcall Script_IsEventRegistered(void *L) {
// ---- the gate ------------------------------------------------------------------
// The frame's filter for the event being dispatched. `eventName` is the
// event-table entry's own name, which Filter::event was copied from, so a
// plain strcmp matches it.
const Filter *DispatchFilter(const std::vector<Filter> &filters, const char *eventName) {
for (const Filter &f : filters)
if (std::strcmp(f.event.c_str(), eventName) == 0)
return &f;
return nullptr;
}
bool MatchesUnit(const Filter &f, const char *arg1) {
if (arg1 != nullptr)
for (const std::string &u : f.units)
if (EqualsIgnoreCase(arg1, u.c_str()))
return true;
return false;
}
// True (and `*out` = arg1) when the runner's first conversion spec is `%s` —
// the unit token of every unit event. Anything else means arg1 isn't a string
// and the filter does not apply.
@@ -292,24 +314,9 @@ bool OnRun(void *frame, uint32_t *slotPtr, const char *fmt, void *varargs) {
Game::Lua::PushString(L, "event");
Game::Lua::RawGet(L, Game::Lua::GLOBALS_INDEX);
const char *ev = Game::Lua::ToString(L, -1);
const Filter *match = nullptr;
if (ev != nullptr) {
for (const Filter &f : it->second) {
if (std::strcmp(f.event.c_str(), ev) == 0) {
match = &f;
break;
}
}
}
const Filter *match = ev != nullptr ? DispatchFilter(it->second, ev) : nullptr;
Game::Lua::SetTop(L, -2);
if (match == nullptr)
return false;
if (arg1 != nullptr) {
for (const std::string &u : match->units)
if (EqualsIgnoreCase(arg1, u.c_str()))
return false;
}
return true; // no token matched — suppress
return match != nullptr && !MatchesUnit(*match, arg1); // no token matched — suppress
}
// ---- unregister co-hooks (the filter dies with the node) ------------------------
@@ -367,4 +374,14 @@ const Game::HookAutoRegister _hookUnregisterAll{
} // namespace
bool Accepts(const void *frame, const char *eventName, const char *arg1) {
if (g_filters.empty() || frame == nullptr || eventName == nullptr)
return true;
auto it = g_filters.find(frame);
if (it == g_filters.end())
return true;
const Filter *match = DispatchFilter(it->second, eventName);
return match == nullptr || MatchesUnit(*match, arg1);
}
} // namespace Frame::UnitEvent
+26
View File
@@ -0,0 +1,26 @@
// 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 Frame::UnitEvent {
// The RegisterUnitEvent filter for one fire whose first argument is the
// string `arg1`: false when `frame` registered `eventName` (the event-table
// entry's own name) with units and `arg1` matches none of them. True for a
// plain registration. The engine's dispatches meet this filter at the runner;
// a dispatcher of our own that walks the subscriber chain itself
// (Event::Custom::FireValues) asks here for each frame.
bool Accepts(const void *frame, const char *eventName, const char *arg1);
} // namespace Frame::UnitEvent
+1 -1
View File
@@ -42,7 +42,7 @@ static int __fastcall Script_C_Item_DoesItemExist(void *L) {
// optimistically accept any positive itemID; callers needing the
// "is the cache populated?" check should use `IsItemDataCachedByID`.
//
// This split matters for `ItemMixin:IsItemEmpty` (FrameXML ItemUtil),
// This split matters for `ItemMixin:IsItemEmpty` (Util/Item.lua),
// which uses `DoesItemExistByID` to gate `ContinueOnItemLoad` — the
// whole point of `ContinueOnItemLoad` is to wait for an uncached item
// to load, so the existence check must succeed for items that aren't
+24
View File
@@ -120,6 +120,28 @@ static int __fastcall Script_GetItemInfoInstant(void *L) {
return 7;
}
// `C_Item.GetItemIDForItemInfo(itemInfo)` — the itemID alone, from the same
// resolver `GetItemInfoInstant` echoes: an itemID, an `"item:N"` string or an
// item link. ItemMixin:GetItemID falls back to it when GetItemInfoInstant
// returns nothing. Returns nothing for input that names no item.
static const Game::Doc::Field kGetItemIDForItemInfoArgs[] = {
Game::Doc::Req("itemInfo", "ItemInfo"),
};
static const Game::Doc::Field kGetItemIDForItemInfoRets[] = {
Game::Doc::Req("itemID", "number"),
};
static const Game::Doc::Function kGetItemIDForItemInfo{
"Returns the item ID of an item ID, item string or item link.", kGetItemIDForItemInfoArgs,
kGetItemIDForItemInfoRets};
static int __fastcall Script_GetItemIDForItemInfo(void *L) {
const int itemID = Item::Arg::ResolveItemID(L, 1);
if (itemID <= 0)
return 0;
Game::Lua::PushNumber(L, static_cast<double>(itemID));
return 1;
}
// Pushes the icon path for `itemID` and returns 1, or returns 0 (= nil
// to Lua) if the item isn't cached, the displayInfoID is missing, or
// the icon record's path slot is empty. Shared by all three
@@ -315,6 +337,8 @@ static int __fastcall Script_C_Item_GetItemInfo(void *L) {
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Item", "GetItemInfo", &Script_C_Item_GetItemInfo);
Game::Lua::RegisterTableFunction("C_Item", "GetItemInfoInstant", &Script_GetItemInfoInstant);
Game::Lua::RegisterTableFunction("C_Item", "GetItemIDForItemInfo",
&Script_GetItemIDForItemInfo, &kGetItemIDForItemInfo);
Game::Lua::RegisterGlobalFunction("GetItemIcon", &Script_GetItemIcon);
Game::Lua::RegisterTableFunction("C_Item", "GetItemIcon", &Script_C_Item_GetItemIcon);
Game::Lua::RegisterTableFunction("C_Item", "GetItemIconByID", &Script_C_Item_GetItemIconByID);
+52 -1
View File
@@ -28,6 +28,7 @@ constexpr int INVSLOT_RANGED = 18;
struct EnchantInfo {
bool has;
bool hasExpiration;
uint32_t expirationMs;
uint32_t charges;
uint32_t enchantID;
@@ -47,7 +48,7 @@ struct EnchantInfo {
// — the same block `spell/BonusDamage.cpp` walks for enchant-granted
// spell power.
EnchantInfo ReadTempEnchant(const uint8_t *cgItem) {
EnchantInfo r{false, 0, 0, 0};
EnchantInfo r{false, false, 0, 0, 0};
if (cgItem == nullptr)
return r;
const uint8_t *desc = Item::ObjectFields(cgItem);
@@ -66,6 +67,7 @@ EnchantInfo ReadTempEnchant(const uint8_t *cgItem) {
desc, kSlot + Offsets::DESCRIPTOR_ENCHANTMENT_CHARGES_DELTA);
if (Game::Read<uint32_t>(
desc, kSlot + Offsets::DESCRIPTOR_ENCHANTMENT_DURATION_DELTA) != 0) {
r.hasExpiration = true;
using TimeLeft_t = uint32_t(__fastcall *)(const uint8_t *item, void *edx, int slot);
r.expirationMs = reinterpret_cast<TimeLeft_t>(
static_cast<uintptr_t>(Offsets::FUN_ITEM_ENCHANTMENT_TIME_LEFT))(
@@ -157,11 +159,60 @@ int __fastcall Script_C_Item_GetItemTempEnchantInfo(void *L) {
return 4;
}
// `C_PaperDollInfo.GetTemporaryEnchantmentInfo(slot)` — retail's per-slot
// temporary enchant, the one its deprecated `GetWeaponEnchantInfo` is now
// built from. `slot` is an equipment slot (INVSLOT_*). Returns a
// `TemporaryItemEnchantInfo` table, or nothing when the slot has no
// temporary enchant (or no item, or is not an equipment slot).
// `hasExpirationTime` is the descriptor duration gate `ReadTempEnchant`
// reads; without it `remainingTimeMs` is 0.
const Game::Doc::Field kTemporaryItemEnchantInfoFields[] = {
Game::Doc::Req("enchantID", "number"),
Game::Doc::Req("remainingTimeMs", "number"),
Game::Doc::Req("chargesRemaining", "number"),
Game::Doc::Req("hasExpirationTime", "bool"),
};
const Game::Doc::Structure kTemporaryItemEnchantInfo{
"TemporaryItemEnchantInfo", "PaperDollInfo", kTemporaryItemEnchantInfoFields};
const Game::Doc::Field kGetTemporaryEnchantmentInfoArgs[] = {
Game::Doc::Req("slot", "number", "An INVSLOT_ constant."),
};
const Game::Doc::Field kGetTemporaryEnchantmentInfoRets[] = {
Game::Doc::Req("enchantInfo", "TemporaryItemEnchantInfo"),
};
const Game::Doc::Function kGetTemporaryEnchantmentInfo{
"Returns the temporary enchantment on an equipped item, or nothing if there is none.",
kGetTemporaryEnchantmentInfoArgs, kGetTemporaryEnchantmentInfoRets};
int __fastcall Script_GetTemporaryEnchantmentInfo(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::Error(L, "Usage: C_PaperDollInfo.GetTemporaryEnchantmentInfo(slot)");
return 0;
}
const int slot = static_cast<int>(Game::Lua::ToNumber(L, 1));
if (slot < Offsets::EQUIPMENT_SLOT_FIRST || slot > Offsets::EQUIPMENT_SLOT_LAST)
return 0;
const EnchantInfo e = ReadEquippedTempEnchant(slot);
if (!e.has)
return 0;
Game::Lua::NewTable(L);
Game::Lua::SetFieldNumber(L, "enchantID", static_cast<double>(e.enchantID));
Game::Lua::SetFieldNumber(L, "remainingTimeMs", static_cast<double>(e.expirationMs));
Game::Lua::SetFieldNumber(L, "chargesRemaining", static_cast<double>(e.charges));
Game::Lua::SetFieldBool(L, "hasExpirationTime", e.hasExpiration);
return 1;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Item", "GetWeaponEnchantInfo",
&Script_C_Item_GetWeaponEnchantInfo);
Game::Lua::RegisterTableFunction("C_Item", "GetItemTempEnchantInfo",
&Script_C_Item_GetItemTempEnchantInfo);
Game::Lua::RegisterTableFunction("C_PaperDollInfo", "GetTemporaryEnchantmentInfo",
&Script_GetTemporaryEnchantmentInfo,
&kGetTemporaryEnchantmentInfo);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+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
+86
View File
@@ -0,0 +1,86 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_PaperDollInfo.GetInventorySlotInfoForInvSlot(invSlot)` — the stock
// `GetInventorySlotInfo(slotName)` looked up the other way: from a slot
// number to its paper-doll row. It walks the same PaperDollItemFrame.dbc rows
// the engine's own function walks (Script_GetInventorySlotInfo, 0x004C81B0),
// in file order, takes the first row with that SlotNumber, and returns what
// the engine returns for it plus the row's name:
//
// invSlot the SlotNumber
// slotTexture the SlotIcon path as stored (this client has no file IDs)
// checkRelic true for the ranged slot, the engine's own relic test
// slotName the ItemSlotName ("HeadSlot", "MainHandSlot", ...)
//
// A slot number with no row returns nothing.
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
namespace PaperDoll::SlotInfo {
namespace {
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("invSlotValue", "number", "An inventory slot number, not a luaIndex."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("invSlot", "number"),
Game::Doc::Req("slotTexture", "string", "The empty-slot texture path."),
Game::Doc::Req("checkRelic", "bool"),
Game::Doc::Req("slotName", "cstring"),
};
const Game::Doc::Function kGetInventorySlotInfoForInvSlot{
"Returns the paper doll slot for an inventory slot number, or nothing if there is none.",
kArgs, kRets};
int __fastcall Script_GetInventorySlotInfoForInvSlot(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::Error(L, "Usage: C_PaperDollInfo.GetInventorySlotInfoForInvSlot(invSlotValue)");
return 0;
}
const int invSlot = static_cast<int>(Game::Lua::ToNumber(L, 1));
const auto *records = Game::Read<const uint8_t *>(Offsets::VAR_PAPERDOLLITEMFRAME_RECORDS);
const int count = Game::Read<int>(Offsets::VAR_PAPERDOLLITEMFRAME_COUNT);
if (records == nullptr)
return 0;
for (int i = 0; i < count; ++i) {
const uint8_t *row = records + i * Offsets::OFF_PAPERDOLLITEMFRAME_RECORD_STRIDE;
const int slotNumber = Game::Read<int>(row, Offsets::OFF_PAPERDOLLITEMFRAME_SLOT_NUMBER);
if (slotNumber != invSlot)
continue;
Game::Lua::PushNumber(L, static_cast<double>(slotNumber));
Game::Lua::PushString(L, Game::Read<const char *>(row, Offsets::OFF_PAPERDOLLITEMFRAME_SLOT_ICON));
Game::Lua::PushBool(L, slotNumber == Offsets::PAPERDOLLITEMFRAME_RELIC_SLOT_NUMBER);
Game::Lua::PushString(L, Game::Read<const char *>(row, Offsets::OFF_PAPERDOLLITEMFRAME_SLOT_NAME));
return 4;
}
return 0;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_PaperDollInfo", "GetInventorySlotInfoForInvSlot",
&Script_GetInventorySlotInfoForInvSlot,
&kGetInventorySlotInfoForInvSlot);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace PaperDoll::SlotInfo
+74 -26
View File
@@ -125,6 +125,45 @@ int ExtraCount(void *L, int extras) {
return n;
}
// Moves the records of slots 0..span-1 so that slot i holds what slot
// (i + by) % span held, with their extra-argument tables and the scroll
// record. Whole records move, so every text keeps one owner, a free slot's
// too (Clear and the writer leave a free slot's old text for the writer to
// free when it reuses the slot). The lines still show the records they were
// laid out from; the caller lays them out again. Self at stack slot 1.
void RotateRecords(void *L, void *frame, int span, int by) {
using namespace Game::Lua;
by %= span;
if (span <= 1 || by == 0)
return;
constexpr size_t kStride = Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
uint8_t *records = Game::Read<uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES);
const std::vector<uint8_t> was(records, records + span * kStride);
for (int i = 0; i < span; ++i)
std::memcpy(records + i * kStride, was.data() + ((i + by) % span) * kStride, kStride);
int &scroll = Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_SCROLL_RECORD);
if (scroll >= 0 && scroll < span)
scroll = (scroll - by + span) % span;
PushRecords(L, false);
const int table = GetTop(L);
if (Type(L, table) == TYPE_TABLE) {
NewTable(L);
const int moved = GetTop(L);
for (int i = 0; i < span; ++i) {
PushNumber(L, i);
RawGetI(L, table, (i + by) % span);
RawSet(L, moved);
}
for (int i = 0; i < span; ++i) {
PushNumber(L, i);
RawGetI(L, moved, i);
RawSet(L, table);
}
}
SetTop(L, table - 1);
}
// Stores stack slots kFirstExtra..top as the extra arguments of `record`.
void StoreExtras(void *L, int record, int top) {
using namespace Game::Lua;
@@ -190,15 +229,18 @@ uint32_t MessageColor(void *L, void *frame) {
// `ScrollingMessageFrame:BackFillMessage(text, r, g, b, ...)`, retail's: the
// message becomes the OLDEST. Retail's mixin pushes the entry at the back of
// its history buffer and drops it when the buffer is full ("Won't overwrite
// front"); 4.3.4's AddMessage(..., addToTop) writes the same slot but, full,
// overwrites the oldest. The slot is 4.3.4's: once the count has grown, the
// free one before the oldest, `(pos - count + 1) mod max` (pos 0 for an empty
// ring), and the record is written as the ring writer 0x00788150 writes it —
// the text a Storm copy (the ring frees its texts with SMemFree), AddMessage's
// colour, shown, the frame's two times, id 0. Nothing already in the ring
// moves, so a scrolled-up frame keeps its view; at the bottom the lines are
// laid out again from the newest, as the writer does, which shows the new
// line when there is room above the others.
// front"). The ring is then not full, so the engine keeps it in records
// 0..count-1 (Offsets.h, OFF_SCROLLINGMESSAGEFRAME_MESSAGES), and the new
// oldest goes in record 0: every record moves up one, the free record at the
// count coming round to 0. (4.3.4's AddMessage(..., addToTop) writes the
// record before the oldest instead, which wraps to the end of the array and
// leaves 4.3.4's own count-wrapped scroll math wrong.) The record is written
// as the ring writer 0x00788150 writes it: the text a Storm copy (the ring
// frees its texts with SMemFree), AddMessage's colour, shown, the frame's two
// times, id 0. The lines are laid out again: at the bottom from the newest,
// as the writer does, which shows the new line when there is room above the
// others; scrolled up from the scroll record, which moved with its message,
// so the view stays.
int __fastcall Script_BackFillMessage(void *L) {
using namespace Game::Lua;
using StormAlloc_t = void *(__stdcall *)(size_t size, const char *file, int line, int flags);
@@ -225,15 +267,11 @@ int __fastcall Script_BackFillMessage(void *L) {
return 0;
std::memcpy(copy, text, bytes);
RotateRecords(L, frame, count + 1, count);
++count;
int &pos = Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS);
if (pos == -1)
pos = 0;
int slot = pos - count + 1;
if (slot < 0)
slot += max;
uint8_t *record = Game::Read<uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES) +
slot * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
pos = count - 1;
uint8_t *record = Game::Read<uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES);
char *&stored = *reinterpret_cast<char **>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT);
if (stored != nullptr)
reinterpret_cast<StormFree_t>(Offsets::FUN_STORM_SMEM_FREE)(stored, kStormFile, __LINE__, 0);
@@ -245,11 +283,13 @@ int __fastcall Script_BackFillMessage(void *L) {
*reinterpret_cast<uint32_t *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_FADE_TIME) =
Game::Read<uint32_t>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_FADE_TIME);
*reinterpret_cast<int *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_ID) = 0;
StoreExtras(L, slot, top);
StoreExtras(L, 0, top);
if (Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_AT_BOTTOM) != 0)
reinterpret_cast<Layout_t>(Offsets::FUN_SMF_DISPLAY_REFRESH)(frame, nullptr, pos);
// Scrolled up, the message lands without a layout.
reinterpret_cast<Layout_t>(Offsets::FUN_SMF_DISPLAY_REFRESH)(
frame, nullptr,
Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_AT_BOTTOM) != 0
? pos
: Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_SCROLL_RECORD));
DisplayRefreshed::MarkDirty(frame);
return 0;
}
@@ -310,7 +350,11 @@ int __fastcall Script_GetMessageInfo(void *L) {
// ring: every kept record moves forward in ring order to the first free slot
// (freeing the text it lands on, nulling the one it left), the position moves
// back by the removed count, and the extra-argument tables move with their
// records. Then the engine's own layout from the record at the kept offset
// records. The ring is turned first so that the oldest is record 0, so the
// kept ones end in records 0..kept-1, the engine's layout of a ring that is
// not full (Offsets.h, OFF_SCROLLINGMESSAGEFRAME_MESSAGES); 4.3.4 compacts
// from the oldest's record, which leaves a full ring's survivors anywhere.
// Then the engine's own layout from the record at the kept offset
// (clamped to the oldest) and its re-arm — a layout at the bottom keeps the
// fade state, so 4.3.4 re-arms after — or its Clear when nothing is kept.
//
@@ -361,17 +405,21 @@ int __fastcall Script_RemoveMessagesByPredicate(void *L) {
if (kept == count)
return 0;
// 4.3.4's RemoveLines: compact the kept records forward in ring order.
// 4.3.4's RemoveLines: compact the kept records forward in ring order,
// the oldest first turned to record 0.
const int offset = reinterpret_cast<ScrollOffset_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET)(frame);
int &pos = Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS);
int oldest = pos - count + 1;
if (oldest < 0)
oldest += max;
RotateRecords(L, frame, max, oldest);
pos = count - 1;
uint8_t *records = Game::Read<uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES);
PushRecords(L, false);
const int table = GetTop(L);
const bool extras = Type(L, table) == TYPE_TABLE;
int src = pos - count + 1;
if (src < 0)
src += max;
int dst = src;
int src = 0;
int dst = 0;
for (int i = 0; i < count; ++i) {
if (!remove[static_cast<size_t>(i)]) {
if (src != dst) {
+34 -8
View File
@@ -22,9 +22,17 @@
// two directions, and GetScrollOffset is the engine's GetCurrentScroll under
// retail's name. Retail clamps the offset to its range; the engine wraps one
// outside it, so the setters clamp first. SetScrollOffset to the offset the
// frame has does nothing, as on retail. ScrollByAmount always calls the
// engine, which re-arms the shown lines' fade when the offset stays: retail's
// ResetAllFadeTimes.
// frame has does nothing, as on retail.
//
// Retail's ScrollUp and ScrollDown are ScrollByAmount(1) and (-1), so
// ScrollByAmount steps the engine's own ScrollUp / ScrollDown, the loop its
// PageUp runs. The engine's ScrollUp stops once the oldest message is on
// screen, short of the range's end, where that message sits alone on the
// bottom line under empty space; moving the offset straight there scrolls
// chat up into that space (pfUI's chat wheel did). The clamped target still
// bounds the steps, since a hidden frame never lays out and its ScrollUp never
// finds the top. Every step re-arms the shown lines' fade, and an amount that
// takes no step re-arms through ScrollToOffset: retail's ResetAllFadeTimes.
#include "Game.h"
#include "Offsets.h"
@@ -35,10 +43,19 @@ namespace ScrollingMessageFrame::Scroll {
namespace {
using ScrollToOffset_t = void(__fastcall *)(void *smf, void *edx, int offset);
using ScrollOffset_t = int(__fastcall *)(void *smf);
using SmfInt_t = int(__fastcall *)(void *smf);
int ScrollOffset(void *smf) {
return reinterpret_cast<ScrollOffset_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET)(smf);
return reinterpret_cast<SmfInt_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET)(smf);
}
// One step of the engine's ScrollUp / ScrollDown; false at their end.
bool ScrollUp(void *smf) {
return reinterpret_cast<SmfInt_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_UP)(smf) != 0;
}
bool ScrollDown(void *smf) {
return reinterpret_cast<SmfInt_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_DOWN)(smf) != 0;
}
int MaxScrollRange(void *smf) {
@@ -90,7 +107,14 @@ int __fastcall Script_ScrollByAmount(void *L) {
Game::Lua::Error(L, "Usage: %s:ScrollByAmount(amount)", Game::Lua::ObjectDisplayName(frame));
return 0;
}
ScrollTo(frame, Clamped(frame, ScrollOffset(frame) + Game::Lua::ToNumber(L, 2)));
const int offset = ScrollOffset(frame);
int steps = Clamped(frame, offset + Game::Lua::ToNumber(L, 2)) - offset;
if (steps == 0)
ScrollTo(frame, offset);
while (steps > 0 && ScrollUp(frame))
--steps;
while (steps < 0 && ScrollDown(frame))
++steps;
DisplayRefreshed::MarkDirty(frame);
return 0;
}
@@ -121,8 +145,10 @@ const Game::Doc::Function kGetScrollOffset{"Returns how many lines the frame is
{}, kOffsetRets};
const Game::Doc::Function kGetMaxScrollRange{"Returns the largest scroll offset: one less than the message count.",
{}, kRangeRets};
const Game::Doc::Function kScrollByAmount{"Scrolls the frame by a number of lines and shows the faded lines again.",
kAmountArgs, {}};
const Game::Doc::Function kScrollByAmount{
"Scrolls the frame by a number of lines, stopping where ScrollUp and ScrollDown stop, and shows the faded lines "
"again.",
kAmountArgs, {}};
const Game::Doc::Method kDocs[] = {
{"SetScrollOffset", &kSetScrollOffset},
{"GetScrollOffset", &kGetScrollOffset},
+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
+12 -2
View File
@@ -11,8 +11,8 @@
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `GameTooltip:SetHyperlink(link)` takes a full chat link, and `spell:` and
// `quest:` links, as on 3.x clients.
// `GameTooltip:SetHyperlink(link)` takes a full chat link, and `spell:`,
// `quest:` and `trade:` links, as on 3.x clients.
//
// Vanilla's Script_GameTooltip_SetHyperlink knows two payloads, `item:` and
// `enchant:`, matched against the start of the argument. Anything else -
@@ -43,6 +43,10 @@
// the tooltip empty and queues the server query the quest log would
// make (once per quest - see Quest::Cache::Lookup's `unique`); the next
// SetHyperlink after the answer shows it.
// - `trade:` opens the linked profession in the profession window
// (TradeSkill::Link::Open) and leaves the tooltip alone, as 3.3.5's
// SetHyperlink does. That is how a click on a chat link opens it: the
// SetItemRef of both 1.12 and 3.3.5 ends in ItemRefTooltip:SetHyperlink.
// - Everything else - `item:`, other `enchant:` links, and the error for link types
// vanilla has no data for (`achievement:`, `talent:`, ...) - is the
// engine's own, run on the payload.
@@ -56,11 +60,13 @@
#include "spell/Lookup.h"
#include "spell/Tooltip.h"
#include "tooltip/Lines.h"
#include "tradeskill/Link.h"
#include "ui/ColorData.h"
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <string>
namespace Tooltip::Hyperlink {
@@ -165,6 +171,10 @@ int __fastcall SetHyperlink_h(void *L) {
ShowQuest(L, static_cast<uint32_t>(std::strtoul(payload + 6, nullptr, 10)));
return 0;
}
if (HasPrefix(payload, len, "trade:")) {
TradeSkill::Link::Open(std::string(payload + 6, len - 6).c_str());
return 0;
}
if (payload != link) {
// Push the payload while the full link is still on the stack (the
+232 -381
View File
@@ -11,437 +11,288 @@
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Trade-skill *list* links (`|Htrade:...|h`) — the shareable "here's my
// profession and which recipes I know" link. Vanilla 1.12 has NO such link
// type (the engine's SetHyperlink parser only knows `item:` and `enchant:`);
// 2.x introduced it. We backport the whole thing: the link is built entirely
// in C here; only the click-to-display viewer lives in the embedded addon
// (Util/TradeSkillLink.lua), since that's UI.
// The `trade:` link: a player's profession and the recipes they know, built
// and read as 3.3.5 does.
//
// Wire format (a ClassicAPI-private shape — nothing native emits/reads it, so
// we're free to define it; modeled on the 2.x/3.3.5 `trade:` link, with the
// linker's name in the slot 2.x used for the GUID):
// |cffffd000|Htrade:<spellID>:<rank>:<maxRank>:<GUID>:<bits>|h[<spell name>]|h|r
//
// |cffffd000|Htrade:<skillLineID>:<curRank>:<maxRank>:<linkerName>:<base64 bits>|h[Name]|h|r
// Mirrors 3.3.5's CGTradeSkillInfo::GetTradeSkillListLink (0x005DB060) and
// SetLink (0x005DE300):
// - `spellID` is the profession spell the window was opened with (Effect[0]
// SPELL_EFFECT_TRADE_SKILL). The reader maps it to a skill line through
// the SkillLineAbility row for the linker's race and class.
// - `rank` / `maxRank` are the linker's, as GetTradeSkillLine gives them.
// - `GUID` is the linker's, `%I64X`. The reader takes the linker's name,
// race and class from the name cache, and asks the server for a linker
// it does not hold, opening the link when the answer arrives.
// - `bits` hold one bit per row of the skill line's link rows (Abilities),
// six per base64 character, low bit first. A bit is set when the linker
// knows the row's spell and the row's race and class masks take the
// linker; 3.3.5's encoder ignores the rows' exclude flags.
//
// - skillLineID — SkillLine.dbc row (e.g. 164 = Blacksmithing)
// - curRank/maxRank — the linker's skill level (cosmetic; viewer title)
// - linkerName — the linking character's name
// - base64 bits — a bitfield over the skill line's *canonical recipe list*:
// one bit per possible recipe, set = the linker knows it.
// The link rows are 3.3.5's SpellTableGetSkillLineLinkInfo: the run of
// SkillLineAbility records of the skill line. 3.3.5's file holds each
// profession in a single run, so the run is every row of the line in file
// order (checked against ChromieCraft's SkillLineAbility.dbc: one run per
// linkable line). 1.12's file is sorted by row ID and scatters a line
// (Blacksmithing over 73 runs), so here the rows are every row of the line in
// ID order: the same set, in an order every client with this DBC agrees on.
// The reader rejects bits whose length does not match its own row count.
//
// The canonical recipe list is every SkillLineAbility.dbc row for the skill
// line whose spell actually produces a craft (see IsCraftRecipe — an item,
// enchant, or Turtle LEARN_SPELL recipe; this excludes profession abilities
// like Disenchant), in ascending record-ID order. That ordering is
// deterministic and identical on every client with the same DBC, so builder
// and reader agree on the bit-index space without any handshake. "Which
// recipes does the linker know" comes from the player-spell bitmap
// (VAR_PLAYER_SPELL_BITMAP — the same store IsPlayerSpell reads, which covers
// profession recipes), NOT from the trade-skill window's server list.
// What links: a profession spell, on both ends. 3.3.5 also requires the
// skill line's canLink column, which 1.12's SkillLine.dbc does not have, so
// every profession spell links: Smelting, Poisons and Beast Training too.
//
// The TradeSkill and Craft windows are separate frames with separate storage
// and can both be open at once, so each gets its own builder (matching
// WotLK) — each reads only its own window's open-state global (both zeroed by
// the engine on close), so "both open" is unambiguous and neither guesses:
//
// - `C_TradeSkillUI.GetTradeSkillListLink()` — the TradeSkill window
// - `C_TradeSkillUI.GetCraftListLink()` — the Craft window (Enchanting)
// - `C_TradeSkillUI.GetTradeSkillListRecipes(skillLineID, bits)` — decoder
// for the viewer.
// The reader shows the link in the engine's own profession windows
// (Window.cpp). GetTradeSkillListLink() is 3.3.5's, for the TradeSkill
// window. GetCraftListLink() is its twin for the Craft window (Enchanting),
// which 3.3.5 does not have.
#include "Link.h"
#include "Game.h"
#include "Offsets.h"
#include "Window.h"
#include "dbc/Lookup.h"
#include "spell/Known.h"
#include "spell/Lookup.h"
#include "unit/Identity.h"
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
namespace TradeSkill::Link {
namespace {
// 6 recipes per character, LSB-first (mirrors 3.x's encoder).
constexpr char kBase64[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
constexpr int kRecipesPerChar = 6;
constexpr int kMaxRecipes = 1024; // largest vanilla skill line is ~340
constexpr int kBitsBufSize = kMaxRecipes / kRecipesPerChar + 2;
using AbilityLookup_t = const uint8_t *(__fastcall *)(uint32_t race, uint32_t classId,
uint32_t spellID);
using SlotForSkill_t = int(__thiscall *)(const void *player, uint32_t skillLine);
using NameCacheGet_t = const uint8_t *(__thiscall *)(void *cache, uint32_t lo, uint32_t hi,
const uint64_t *owner, void *callback,
void *userData, char dedupe);
constexpr int kRowsPerChar = 6;
// 3.3.5's s_base64Encode (0x00A1B640).
constexpr char kBase64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
// 3.3.5's s_base64Decode (0x00A1B688): an unknown character reads as 0.
int Base64Value(char c) {
for (int i = 0; i < 64; ++i)
if (kBase64[i] == c)
return i;
return -1;
}
// A spell is a craftable recipe (vs. a profession ability like Disenchant, or
// a rank-up / skill-grant spell) iff one of its effects produces a craft: an
// item (CREATE_ITEM), an enchant (ENCHANT_ITEM / _TEMPORARY), or Turtle's
// custom recipes (LEARN_SPELL). Filtering on the skill-up threshold alone
// wrongly kept Disenchant (which carries thresholds but isn't a recipe).
bool IsCraftRecipe(int spellID) {
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
if (rec == nullptr)
return false;
const int32_t *eff = Game::Ptr<const int32_t>(
rec, Offsets::OFF_SPELL_RECORD_EFFECT);
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
const int e = eff[i];
if (e == Offsets::SPELL_EFFECT_CREATE_ITEM ||
e == Offsets::SPELL_EFFECT_LEARN_SPELL ||
e == Offsets::SPELL_EFFECT_ENCHANT_ITEM ||
e == Offsets::SPELL_EFFECT_ENCHANT_ITEM_TEMPORARY)
return true;
}
return false;
}
// Fills `outSpell` with the recipe spellIDs for `skillLineID`, in canonical
// (ascending record-ID) order, and — when non-null — `outGrey`/`outGreen` with
// the skill levels the recipe turns grey / green at (its difficulty tiers; the
// required-skill field 7 is uselessly 1 for nearly every trainer-taught
// recipe). Returns the count (capped at `maxOut`). Same walk builder and reader
// use, so the bit-index space is identical.
int BuildRecipeList(uint32_t skillLineID, int *outSpell, int *outGrey,
int *outGreen, int maxOut) {
auto *const *records = Game::Read<const uint8_t *const *>(
Offsets::VAR_SKILL_LINE_ABILITY_RECORDS);
const int count = Game::Read<int>(
Offsets::VAR_SKILL_LINE_ABILITY_COUNT);
if (records == nullptr || count <= 0)
return 0;
int n = 0;
for (int i = 1; i <= count && n < maxOut; ++i) {
const uint8_t *rec = records[i];
if (rec == nullptr)
continue;
if (static_cast<uint32_t>(Game::Read<int>(
rec, Offsets::OFF_SLA_SKILL_ID)) != skillLineID)
continue;
const int spellID =
Game::Read<int>(rec, Offsets::OFF_SLA_SPELL_ID);
if (!IsCraftRecipe(spellID))
continue;
if (outGrey != nullptr)
outGrey[n] = Game::Read<int>(
rec, Offsets::OFF_SLA_TRIVIAL_SKILL_HIGH);
if (outGreen != nullptr)
outGreen[n] = Game::Read<int>(
rec, Offsets::OFF_SLA_TRIVIAL_SKILL_LOW);
outSpell[n++] = spellID;
}
return n;
}
// Same bit the engine (and our IsPlayerSpell) reads — covers profession
// recipes, not just trained abilities.
bool IsKnownSpell(int spellID) {
if (spellID <= 0)
return false;
auto *bitmap = Game::Read<const uint32_t *>(
Offsets::VAR_PLAYER_SPELL_BITMAP);
if (bitmap == nullptr)
return false;
return (bitmap[static_cast<uint32_t>(spellID) >> 5] &
(1u << (spellID & 31))) != 0;
}
// Skill line (SkillLine.dbc row) of the first SLA row that teaches `spellID`,
// or 0 if none. Used to identify the Craft window's profession, which — unlike
// the TradeSkill window — has no skill-line global (the engine derives it from
// the recipe, see `GetCraftDisplaySkillLine` at `0x004F6CB0`).
uint32_t SkillForSpell(int spellID) {
if (spellID <= 0)
return 0;
auto *const *records = Game::Read<const uint8_t *const *>(
Offsets::VAR_SKILL_LINE_ABILITY_RECORDS);
const int count = Game::Read<int>(
Offsets::VAR_SKILL_LINE_ABILITY_COUNT);
if (records == nullptr || count <= 0)
return 0;
for (int i = 1; i <= count; ++i) {
const uint8_t *rec = records[i];
if (rec != nullptr &&
Game::Read<int>(rec, Offsets::OFF_SLA_SPELL_ID) ==
spellID)
return static_cast<uint32_t>(
Game::Read<int>(rec, Offsets::OFF_SLA_SKILL_ID));
}
if (c >= 'A' && c <= 'Z')
return c - 'A';
if (c >= 'a' && c <= 'z')
return c - 'a' + 26;
if (c >= '0' && c <= '9')
return c - '0' + 52;
if (c == '+')
return 62;
if (c == '/')
return 63;
return 0;
}
// Skill line of the open Craft window (Enchanting, pet training) — derived
// from its first real recipe entry, mirroring the engine. 0 if none open.
uint32_t CraftSkillLine() {
const int count = Game::Read<int>(Offsets::VAR_CRAFT_COUNT);
if (count <= 0)
return 0;
const int cap = (count < kMaxRecipes) ? count : kMaxRecipes;
for (int slot = 0; slot < cap; ++slot) {
const int spellID = Spell::Lookup::RecipeSlotSpellID(
Offsets::VAR_CRAFT_ENTRIES, Offsets::VAR_CRAFT_COUNT, slot);
if (spellID > 0) {
const uint32_t skill = SkillForSpell(spellID);
if (skill != 0)
return skill;
}
}
return 0;
// 3.3.5's SpellTableMatchRaceClass with no exclusions, as its encoder calls it.
bool MatchRaceClass(uint32_t race, uint32_t classId, uint32_t raceMask, uint32_t classMask) {
return (raceMask == 0 || (raceMask & (1u << (race - 1))) != 0) &&
(classMask == 0 || (classMask & (1u << (classId - 1))) != 0);
}
// Encodes the local player's known-recipe bitfield for `skillLineID` into
// `out` (must hold kBitsBufSize). Returns the recipe count, or -1 if the skill
// line has no recipes.
int EncodeKnownBits(uint32_t skillLineID, char *out) {
int spells[kMaxRecipes];
const int n = BuildRecipeList(skillLineID, spells, nullptr, nullptr,
kMaxRecipes);
if (n <= 0)
return -1;
int nb = 0;
unsigned acc = 0;
int used = 0;
for (int i = 0; i < n; ++i) {
if (IsKnownSpell(spells[i]))
acc |= (1u << used);
if (++used == kRecipesPerChar) {
out[nb++] = kBase64[acc];
acc = 0;
used = 0;
}
}
if (used != 0)
out[nb++] = kBase64[acc];
out[nb] = '\0';
return n;
}
// Current / max skill rank for the local player in `skillLine`, read from the
// CGPlayer `+0xE68` sub-struct exactly as the engine's own accessors do (see
// the FUN_SKILL_LINE_TO_SLOT block in Offsets.h). Leaves 0/0 when the player
// isn't resolvable or doesn't have the skill.
void ReadPlayerSkillRank(int skillLine, int *outCur, int *outMax) {
*outCur = 0;
*outMax = 0;
const uint8_t *player = Unit::Identity::PlayerObject();
if (player == nullptr)
return;
using SlotFn = int(__thiscall *)(const void *, int);
const int slot = reinterpret_cast<SlotFn>(
Offsets::FUN_SKILL_LINE_TO_SLOT)(player, skillLine);
// The player's rank and maximum in `skillLine` as GetTradeSkillLine pushes
// them: each plus the permanent bonus when it is not 0, both 0 without a
// slot (3.3.5's GetSkillValue / GetSkillMaxRank, which its encoder writes).
void PlayerRank(const uint8_t *player, uint32_t skillLine, int *rank, int *maxRank) {
*rank = 0;
*maxRank = 0;
const int slot =
reinterpret_cast<SlotForSkill_t>(Offsets::FUN_SKILL_LINE_TO_SLOT)(player, skillLine);
if (slot < 0)
return;
const uint8_t *base = Game::Read<const uint8_t *>(
player, Offsets::OFF_CGPLAYER_INFO);
if (base == nullptr)
return;
const uint8_t *rec =
base + Offsets::OFF_SKILL_INFO_TABLE + slot * Offsets::SKILL_INFO_STRIDE;
uint32_t cur = Game::Read<uint16_t>(rec, Offsets::OFF_SKILL_INFO_CUR);
uint32_t mx = Game::Read<uint16_t>(rec, Offsets::OFF_SKILL_INFO_MAX);
const uint32_t bonus =
Game::Read<uint16_t>(rec, Offsets::OFF_SKILL_INFO_BONUS);
if (cur != 0)
cur += bonus;
if (mx != 0)
mx += bonus;
*outCur = static_cast<int>(cur);
*outMax = static_cast<int>(mx);
const uint8_t *skills = Game::Read<const uint8_t *>(player, Offsets::OFF_CGPLAYER_INFO) +
Offsets::OFF_SKILL_INFO_TABLE + slot * Offsets::SKILL_INFO_STRIDE;
const int bonus = Game::Read<uint16_t>(skills, Offsets::OFF_SKILL_INFO_BONUS);
const int cur = Game::Read<uint16_t>(skills, Offsets::OFF_SKILL_INFO_CUR);
const int max = Game::Read<uint16_t>(skills, Offsets::OFF_SKILL_INFO_MAX);
*rank = cur != 0 ? cur + bonus : 0;
*maxRank = max != 0 ? max + bonus : 0;
}
// The local player's character name via the engine's object name getter.
const char *PlayerCharacterName() {
// 3.3.5's CGTradeSkillInfo::GetTradeSkillListLink: the local player's link
// for profession `spellID` into `buf`, or false.
bool Build(int spellID, char *buf, size_t size) {
const uint8_t *player = Unit::Identity::PlayerObject();
if (player == nullptr)
return "";
using GetName_t = const char *(__thiscall *)(const void *, int *);
const char *name = reinterpret_cast<GetName_t>(
Offsets::FUN_OBJECT_GET_NAME)(player, nullptr);
return (name != nullptr) ? name : "";
return false;
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
if (record == nullptr ||
Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_EFFECT) !=
Offsets::SPELL_EFFECT_TRADE_SKILL)
return false;
const uint8_t *desc = Unit::Identity::PlayerDescriptor();
if (desc == nullptr)
return false;
const uint32_t race = desc[Offsets::OFF_UNIT_DESCRIPTOR_RACE_BYTE];
const uint32_t classId = desc[Offsets::OFF_UNIT_DESCRIPTOR_CLASS_BYTE];
const uint8_t *ability = reinterpret_cast<AbilityLookup_t>(
Offsets::FUN_SKILL_LINE_ABILITY_LOOKUP)(race, classId, static_cast<uint32_t>(spellID));
if (ability == nullptr)
return false;
const uint32_t skillLine = Game::Read<uint32_t>(ability, Offsets::OFF_SLA_SKILL_ID);
if (DBC::Record(Offsets::VAR_SKILL_LINE_RECORDS, Offsets::VAR_SKILL_LINE_COUNT, skillLine) ==
nullptr)
return false;
const uint8_t *rows[MAX_ABILITIES];
const int count = Abilities(skillLine, rows);
if (count == 0)
return false;
int rank, maxRank;
PlayerRank(player, skillLine, &rank, &maxRank);
const int prefix = std::snprintf(buf, size, "|cffffd000|Htrade:%d:%d:%d:%I64X:", spellID,
rank, maxRank, Unit::Identity::PlayerGuid());
if (prefix < 0 || static_cast<size_t>(prefix + BitChars(count) + 1) >= size)
return false;
char *out = buf + prefix;
unsigned bits = 0;
int bit = 0;
for (int i = 0; i < count; ++i) {
if (Spell::Known::IsKnown(Game::Read<uint32_t>(rows[i], Offsets::OFF_SLA_SPELL_ID)) &&
MatchRaceClass(race, classId, Game::Read<uint32_t>(rows[i], Offsets::OFF_SLA_RACE_MASK),
Game::Read<uint32_t>(rows[i], Offsets::OFF_SLA_CLASS_MASK)))
bits |= 1u << bit;
if (++bit == kRowsPerChar) {
*out++ = kBase64[bits];
bits = 0;
bit = 0;
}
}
if (bit != 0)
*out++ = kBase64[bits];
const char *name = DBC::LocalizedField(Offsets::VAR_SPELL_RECORDS,
Offsets::VAR_SPELL_RECORD_COUNT,
static_cast<uint32_t>(spellID), Offsets::OFF_SPELL_NAMES);
std::snprintf(out, static_cast<size_t>(buf + size - out), "|h[%s]|h|r",
name != nullptr ? name : "");
return true;
}
// Builds the full `|Htrade:...|h` link for whichever window `useCraft`
// selects. Pushes the link string (returns 1), or nothing (returns 0 → nil)
// when that window is closed or has no recipes.
int BuildLink(void *L, bool useCraft) {
int skillLine;
if (useCraft) {
// Craft-window open-state flag; zeroed by the engine on close.
if (Game::Read<int>(Offsets::VAR_CRAFT_WINDOW_STATE) == 0)
return 0;
skillLine = static_cast<int>(CraftSkillLine());
} else {
// TradeSkill skill-line global; zeroed by the engine on close.
skillLine = Game::Read<int>(
Offsets::VAR_CURRENT_TRADESKILL_LINE);
}
if (skillLine <= 0)
return 0;
// 3.3.5's TradeSkillLinkNameCallback: the name query for a link's linker was
// answered; `data` is the copy of the link Open made.
void __stdcall LinkerNameLoaded(void *data, char success) {
if (success)
Open(static_cast<const char *>(data));
std::free(data);
}
char bits[kBitsBufSize];
if (EncodeKnownBits(static_cast<uint32_t>(skillLine), bits) < 0)
return 0;
const char *prof = DBC::LocalizedField(
Offsets::VAR_SKILL_LINE_RECORDS, Offsets::VAR_SKILL_LINE_COUNT,
static_cast<uint32_t>(skillLine), Offsets::OFF_SKILL_LINE_NAME);
if (prof == nullptr || *prof == '\0')
return 0;
int cur = 0, mx = 0;
ReadPlayerSkillRank(skillLine, &cur, &mx);
const char *who = PlayerCharacterName();
char link[512];
std::snprintf(link, sizeof link,
"|cffffd000|Htrade:%d:%d:%d:%s:%s|h[%s]|h|r", skillLine, cur,
mx, who, bits, prof);
Game::Lua::PushString(L, link);
int PushLink(void *L, int spellID) {
char link[256];
if (Build(spellID, link, sizeof link))
Game::Lua::PushString(L, link);
else
Game::Lua::PushNil(L);
return 1;
}
// `C_TradeSkillUI.GetTradeSkillListLink()` -> link for the open TradeSkill
// window, or nil.
// `GetTradeSkillListLink()` — 3.3.5's Script_GetTradeSkillListLink.
int __fastcall Script_GetTradeSkillListLink(void *L) {
return BuildLink(L, /*useCraft=*/false);
return PushLink(L, Window::TradeSkillSpell());
}
// `C_TradeSkillUI.GetCraftListLink()` -> link for the open Craft window, or nil.
int __fastcall Script_GetCraftListLink(void *L) {
return BuildLink(L, /*useCraft=*/true);
}
// `GetCraftListLink()` — the Craft window's twin.
int __fastcall Script_GetCraftListLink(void *L) { return PushLink(L, Window::CraftSpell()); }
// `C_TradeSkillUI.GetTradeSkillListRecipes(skillLineID, bits)`
// -> { { spellID, isKnown, trivialLevel, greenLevel, createdItem,
// numMade, reagents = { { itemID, count }, ... } }, ... }
// `trivialLevel` / `greenLevel` are the skill levels the recipe turns
// grey / green at — its difficulty tiers, for sorting and colouring.
// `createdItem` is the crafted item's ID (0 for enchants) — the viewer
// derives its subclass filter from it. `numMade` is how many the recipe
// yields (CREATE_ITEM base points + dice; 0 for enchants). `reagents` lists
// each consumed item + required count in DBC slot order (stopping at the
// first empty slot, the engine's own convention), for the detail pane.
//
// Rebuilds the canonical recipe list for `skillLineID` and decodes `bits`
// against it. Recipes past the end of `bits` (shorter/garbled string) decode
// as not-known rather than erroring. The Lua caller (the viewer) turns each
// spellID into a name/icon via GetSpellInfo and each reagent itemID into
// name/texture/on-hand-count via GetItemInfo / GetItemCount.
//
// The real 3.3.5 reagent API (GetTradeSkillReagentInfo etc.) can't serve this:
// it's indexed by position in the player's *own* open trade window, so it
// can't describe a linked recipe from someone else's profession. We read the
// reagents straight from Spell.dbc instead.
int __fastcall Script_GetTradeSkillListRecipes(void *L) {
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsString(L, 2)) {
Game::Lua::Error(
L, "Usage: C_TradeSkillUI.GetTradeSkillListRecipes(skillLineID, bits)");
return 0;
}
const uint32_t skillLine =
static_cast<uint32_t>(Game::Lua::ToNumber(L, 1));
const char *bits = Game::Lua::ToString(L, 2);
const int blen = (bits != nullptr) ? static_cast<int>(std::strlen(bits)) : 0;
// --- Documentation ----------------------------------------------------------
int spells[kMaxRecipes];
int greys[kMaxRecipes];
int greens[kMaxRecipes];
const int n = BuildRecipeList(skillLine, spells, greys, greens, kMaxRecipes);
Game::Lua::NewTable(L);
for (int i = 0; i < n; ++i) {
const int ci = i / kRecipesPerChar;
const int bi = i % kRecipesPerChar;
bool known = false;
if (ci < blen) {
const int v = Base64Value(bits[ci]);
if (v >= 0)
known = (v & (1 << bi)) != 0;
}
// The recipe's crafted item (0 for enchants) — the viewer groups the
// subclass filter by this item's subclass — plus its yield count.
const uint8_t *srec = Spell::Lookup::RecordForID(spells[i]);
int createdItem = 0;
int numMade = 0;
if (srec != nullptr) {
createdItem = Game::Read<int>(
srec, Offsets::OFF_SPELL_RECORD_CREATED_ITEM);
// Yield: the CREATE_ITEM effect's magnitude (base points + dice),
// mirroring the engine's item-creation math. 0 for enchants.
const int32_t *eff = Game::Ptr<const int32_t>(
srec, Offsets::OFF_SPELL_RECORD_EFFECT);
const int32_t *basePts = Game::Ptr<const int32_t>(
srec, Offsets::OFF_SPELL_RECORD_EFFECT_BASE_POINTS);
const int32_t *baseDice = Game::Ptr<const int32_t>(
srec, Offsets::OFF_SPELL_RECORD_EFFECT_BASE_DICE);
for (int e = 0; e < Offsets::SPELL_RECORD_EFFECT_COUNT; ++e) {
if (eff[e] == Offsets::SPELL_EFFECT_CREATE_ITEM) {
numMade = basePts[e] + baseDice[e];
break;
}
}
}
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
Game::Lua::NewTable(L);
Game::Lua::SetFieldNumber(L, "spellID", static_cast<double>(spells[i]));
Game::Lua::SetFieldBool(L, "isKnown", known);
Game::Lua::SetFieldNumber(L, "trivialLevel",
static_cast<double>(greys[i]));
Game::Lua::SetFieldNumber(L, "greenLevel",
static_cast<double>(greens[i]));
Game::Lua::SetFieldNumber(L, "createdItem",
static_cast<double>(createdItem));
Game::Lua::SetFieldNumber(L, "numMade", static_cast<double>(numMade));
// reagents = { {itemID, count}, ... } — nested array under "reagents"
// (no SetField helper takes a table value, so push key + build + SetTable).
Game::Lua::PushString(L, "reagents");
Game::Lua::NewTable(L);
if (srec != nullptr) {
const int32_t *rids = Game::Ptr<const int32_t>(
srec, Offsets::OFF_SPELL_REAGENT_ID);
const int32_t *rcounts = Game::Ptr<const int32_t>(
srec, Offsets::OFF_SPELL_REAGENT_COUNT);
int outIdx = 0;
for (int r = 0; r < Offsets::SPELL_MAX_REAGENTS; ++r) {
if (rids[r] == 0)
break; // first empty slot ends the list
Game::Lua::PushNumber(L, static_cast<double>(++outIdx));
Game::Lua::NewTable(L);
Game::Lua::SetFieldNumber(L, "itemID",
static_cast<double>(rids[r]));
Game::Lua::SetFieldNumber(L, "count",
static_cast<double>(rcounts[r]));
Game::Lua::SetTable(L, -3); // reagents[outIdx] = {itemID,count}
}
}
Game::Lua::SetTable(L, -3); // entry.reagents = { ... }
Game::Lua::SetTable(L, -3);
}
return 1;
}
const Game::Doc::Field kListLinkRets[] = {
Game::Doc::Opt("link", "string", nullptr,
"A trade: link; nil when the window is closed or its profession cannot be "
"linked."),
};
const Game::Doc::Function kGetTradeSkillListLink{
"Returns a link to the player's profession in the trade-skill window, with the recipes the "
"player knows.",
{}, kListLinkRets, "TradeSkillGlobals"};
const Game::Doc::Function kGetCraftListLink{
"Returns a link to the player's profession in the craft window, with the recipes the player "
"knows.",
{}, kListLinkRets, "TradeSkillGlobals", true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_TradeSkillUI", "GetTradeSkillListLink",
&Script_GetTradeSkillListLink);
Game::Lua::RegisterTableFunction("C_TradeSkillUI", "GetCraftListLink",
&Script_GetCraftListLink);
Game::Lua::RegisterTableFunction("C_TradeSkillUI",
"GetTradeSkillListRecipes",
&Script_GetTradeSkillListRecipes);
Game::Lua::RegisterGlobalFunction("GetTradeSkillListLink", &Script_GetTradeSkillListLink,
&kGetTradeSkillListLink);
Game::Lua::RegisterGlobalFunction("GetCraftListLink", &Script_GetCraftListLink,
&kGetCraftListLink);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
int Abilities(uint32_t skillLine, const uint8_t **out) {
auto *const *records =
Game::Read<const uint8_t *const *>(Offsets::VAR_SKILL_LINE_ABILITY_RECORDS);
const int count = Game::Read<int>(Offsets::VAR_SKILL_LINE_ABILITY_COUNT);
if (records == nullptr)
return 0;
int n = 0;
for (int id = 1; id <= count && n < MAX_ABILITIES; ++id) {
const uint8_t *row = records[id];
if (row != nullptr && Game::Read<uint32_t>(row, Offsets::OFF_SLA_SKILL_ID) == skillLine)
out[n++] = row;
}
return n;
}
int BitChars(int count) { return (count + kRowsPerChar - 1) / kRowsPerChar; }
bool IsSet(const char *bits, int index) {
return (Base64Value(bits[index / kRowsPerChar]) & (1 << (index % kRowsPerChar))) != 0;
}
void Open(const char *data) {
if (data == nullptr || *data == '\0')
return;
const int spellID = std::atoi(data);
if (spellID == 0)
return;
const char *fields = std::strchr(data, ':');
if (fields == nullptr)
return;
++fields;
// 3.3.5 reads `%d:%d:%I64X:`; a field after a malformed one stays 0.
int rank = 0;
int maxRank = 0;
uint64_t guid = 0;
char *end = nullptr;
rank = static_cast<int>(std::strtol(fields, &end, 10));
if (*end == ':') {
maxRank = static_cast<int>(std::strtol(end + 1, &end, 10));
if (*end == ':')
guid = std::strtoull(end + 1, nullptr, 16);
}
if (guid == 0)
return;
// A miss keeps the copy with the query and opens the link on the answer.
char *copy = _strdup(data);
if (copy == nullptr)
return;
const uint64_t none = 0;
const uint8_t *linker = reinterpret_cast<NameCacheGet_t>(Offsets::FUN_PLAYER_INFO_LOOKUP)(
reinterpret_cast<void *>(Offsets::VAR_PLAYER_NAME_CACHE), static_cast<uint32_t>(guid),
static_cast<uint32_t>(guid >> 32), &none, reinterpret_cast<void *>(&LinkerNameLoaded),
copy, 1);
if (linker == nullptr)
return;
std::free(copy);
if (const char *bits = std::strrchr(fields, ':'))
Window::ShowLinked(spellID, guid, bits + 1, rank, maxRank);
}
} // namespace TradeSkill::Link
+42
View File
@@ -0,0 +1,42 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
// The `trade:` profession link, 3.3.5's format and bit space. See Link.cpp.
namespace TradeSkill::Link {
// Upper bound on a skill line's SkillLineAbility rows (Blacksmithing, the
// largest, has 343).
constexpr int MAX_ABILITIES = 1024;
// Fills `out` with the SkillLineAbility records a link's bits cover for
// `skillLine`, one bit each, in DBC order; returns the count (at most
// MAX_ABILITIES). 3.3.5's SpellTableGetSkillLineLinkInfo.
int Abilities(uint32_t skillLine, const uint8_t **out);
// The number of bit characters a link carries for `count` rows.
int BitChars(int count);
// Whether row `index` is set in a link's bit characters.
bool IsSet(const char *bits, int index);
// 3.3.5's CGTradeSkillInfo::SetLink: shows the profession a received link
// carries. `data` is the link payload after `trade:`. A linker the name
// cache does not hold yet is queried, and the window opens when the answer
// arrives.
void Open(const char *data);
} // namespace TradeSkill::Link
+1 -29
View File
@@ -12,8 +12,7 @@
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// The trade-skill window's recipe functions from 3.x:
// `GetTradeSkillRecipeLink(index)`, `GetTradeSkillDescription(index)` and
// `IsTradeSkillLinked()`.
// `GetTradeSkillRecipeLink(index)` and `GetTradeSkillDescription(index)`.
//
// `GetTradeSkillRecipeLink(index)` links the recipe in row `index`: the
// recipe spell, as an `enchant:` link. Vanilla's window links only the item a
@@ -44,15 +43,6 @@
// then the spell-text parser into a 1024-byte buffer (Spell::Description).
// Most recipes that make an item have no description; enchants do. 1.12's
// Craft window already has this as GetCraftDescription.
//
// `IsTradeSkillLinked()` says whether the window shows a profession opened
// from another player's link, and whose. On this client it never does. A
// received `trade:` link opens ClassicAPI's own viewer
// (Util/TradeSkillLink.lua), which the trade-skill functions never describe,
// and the window shows the player's own professions only. So it returns
// (nil, nil), 3.3.5's answer for the player's own window. Addons check it to
// keep a linked profession out of the player's own records; Zygor skips its
// recipe scan on it.
#include "Game.h"
#include "Offsets.h"
@@ -160,12 +150,6 @@ int __fastcall Script_GetTradeSkillDescription(void *L) {
return 1;
}
int __fastcall Script_IsTradeSkillLinked(void *L) {
Game::Lua::PushNil(L);
Game::Lua::PushNil(L);
return 2;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kRecipeLinkArgs[] = {
@@ -187,24 +171,12 @@ const Game::Doc::Function kGetTradeSkillDescription{
"Returns the description of the recipe in a row of the trade-skill window.",
kRecipeLinkArgs, kDescriptionRets, "TradeSkillGlobals"};
const Game::Doc::Field kLinkedRets[] = {
Game::Doc::Opt("isLinked", "bool", nullptr,
"True when the window shows another player's linked profession."),
Game::Doc::Opt("linkedPlayerName", "string", nullptr,
"The name of the player whose profession is shown."),
};
const Game::Doc::Function kIsTradeSkillLinked{
"Whether the trade-skill window shows a profession opened from another player's link.",
{}, kLinkedRets, "TradeSkillGlobals"};
void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("GetTradeSkillRecipeLink", &Script_GetTradeSkillRecipeLink,
&kGetTradeSkillRecipeLink);
Game::Lua::RegisterGlobalFunction("GetTradeSkillDescription",
&Script_GetTradeSkillDescription,
&kGetTradeSkillDescription);
Game::Lua::RegisterGlobalFunction("IsTradeSkillLinked", &Script_IsTradeSkillLinked,
&kIsTradeSkillLinked);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+460
View File
@@ -0,0 +1,460 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Whose profession the TradeSkill and Craft windows show: the player's own,
// or another player's from a `trade:` link, as 3.3.5's trade-skill window
// does.
//
// 3.3.5's CGTradeSkillInfo keeps whose profession the window shows
// (m_playerGuid, m_spellID, m_skillRank / m_skillMaxRank, m_linkData), and its
// list build RefreshList (0x005DD5A0) takes the rows from the link when
// m_linkData is set. 1.12's BuildTradeSkillList (0x004FCA20) is the same build
// without the link branch. This module adds the branch at the build's three
// per-row reads, and the same for the Craft window (Enchanting), which 3.3.5
// does not have:
// - The spell list: the link's rows instead of the player's list
// (CGPlayer_C::FindSkillLineSpells, or GetTabSpells for the Craft
// window). The rows are the set bits whose spell would be in that list
// had the linker learned it: the engine's learn function (0x005E9C20)
// files a SPELL_ATTR_TRADESPELL spell under its skill line and any other
// under its castUI tab. RefreshList keeps the TRADESPELL rows, the same
// filter.
// - The ability row: looked up for the linker's race and class. RefreshList
// takes the link's row itself.
// - The rank: the linker's, unless the link is the player's own.
// Everything else in the builds runs as for the player's own window:
// available counts from the player's bags, headers, filters, order, events.
//
// Opening (3.3.5 SetTradeSkill, 0x005DE120): a link opens the window its
// spell opens when cast (TryCastSpell routes on EffectMiscValue[0]), through
// the engine's own toggle, with the window marked closed first so that a link
// to the line already open replaces it instead of closing it. Casting a
// profession while a link shows opens the player's own list, again without
// closing. Both casts reach the toggles from one call each in TryCastSpell,
// whose cast spell (ESI) is 3.3.5's m_spellID. As in 3.3.5, closing keeps the
// link, so IsTradeSkillLinked answers until the next open, and the return to
// the character screen drops it (CGTradeSkillInfo::ShutdownGame; here
// CGGameUI::ShutdownGame's calls of the two window shutdowns).
//
// The Lua getters that answer for the player answer for the linker while a
// link shows: GetTradeSkillLine (3.3.5's), and the Craft window's
// GetCraftDisplaySkillLine and GetCraftName. IsTradeSkillLinked is 3.3.5's;
// IsCraftLinked is its Craft window twin.
//
// Each window takes links only when every one of its seams is patched, so a
// seam another DLL holds can't show a link half-way.
#include "Window.h"
#include "Common.h"
#include "Game.h"
#include "Link.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include "spell/Lookup.h"
#include "unit/Identity.h"
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <vector>
namespace TradeSkill::Window {
namespace {
using Toggle_t = void(__fastcall *)(uint32_t key);
using Void_t = void(__cdecl *)();
using AbilityLookup_t = const uint8_t *(__fastcall *)(uint32_t race, uint32_t classId,
uint32_t spellID);
using PlayerList_t = const void *(__thiscall *)(void *player, uint32_t key);
using UnitAbility_t = const uint8_t *(__thiscall *)(void *unit, uint32_t spellID);
using SkillTotal_t = int(__thiscall *)(void *player, uint32_t skillLine);
using SlotForSkill_t = int(__thiscall *)(const void *player, uint32_t skillLine);
using Script_t = int(__fastcall *)(void *L);
// The only Craft type GetCraftDisplaySkillLine answers for (Enchanting).
constexpr uint32_t kCraftTypeEnscribe = 3;
// The engine's growable array, as the list builds read it.
struct SpellArray {
uint32_t capacity;
uint32_t count;
const int *data;
uint32_t granularity;
};
static_assert(offsetof(SpellArray, count) == 0x04 && offsetof(SpellArray, data) == 0x08,
"the builds read the count at +0x04 and the spells at +0x08");
// What a window shows: 3.3.5's CGTradeSkillInfo state.
struct Shown {
int spellID = 0; // the profession spell (m_spellID)
uint64_t guid = 0; // whose profession (m_playerGuid)
bool linked = false;
uint32_t opened = 0; // what the window opened on: the skill line, or the Craft type
uint32_t skillLine = 0;
uint32_t race = 0; // the linker's
uint32_t classId = 0;
int rank = 0; // the linker's (m_skillRank / m_skillMaxRank)
int maxRank = 0;
std::vector<int> spells; // the link's rows
SpellArray list{};
};
Shown g_tradeSkill;
Shown g_craft;
bool g_tradeSkillReady = false;
bool g_craftReady = false;
uint32_t TradeSkillOpen() { return Game::Read<uint32_t>(Offsets::VAR_CURRENT_TRADESKILL_LINE); }
uint32_t CraftOpen() { return Game::Read<uint32_t>(Offsets::VAR_CRAFT_WINDOW_STATE); }
const uint8_t *Ability(uint32_t race, uint32_t classId, uint32_t spellID) {
return reinterpret_cast<AbilityLookup_t>(Offsets::FUN_SKILL_LINE_ABILITY_LOOKUP)(race, classId,
spellID);
}
const char *SpellName(int spellID) {
return DBC::LocalizedField(Offsets::VAR_SPELL_RECORDS, Offsets::VAR_SPELL_RECORD_COUNT,
static_cast<uint32_t>(spellID), Offsets::OFF_SPELL_NAMES);
}
// The window open on `open` builds its rows from the link.
bool BuildsLink(const Shown &s, uint32_t open) { return s.linked && open == s.opened; }
// The window open on `open` shows another player's link, whose ranks it
// answers with (3.3.5: m_playerGuid is not the active player).
bool ShowsOtherLink(const Shown &s, uint32_t open) {
return open != 0 && BuildsLink(s, open) && s.guid != Unit::Identity::PlayerGuid();
}
void Unlink(Shown &s) {
s.linked = false;
s.spells.clear();
s.list = {};
}
// Whether the player's own list for the window holds `spellID` once learned:
// the TradeSkill window's list takes TRADESPELL spells, a Craft window's the
// other spells of its castUI tab.
bool InPlayerList(int spellID, uint32_t craftType) {
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
if (record == nullptr)
return false;
const bool tradeSpell = (Game::Read<uint32_t>(record, Offsets::OFF_SPELL_RECORD_ATTRIBUTES) &
Offsets::SPELL_ATTR_TRADESPELL) != 0;
if (craftType == 0)
return tradeSpell;
return !tradeSpell && Game::Read<uint32_t>(record, Offsets::OFF_SPELL_RECORD_CAST_UI) == craftType;
}
// --- Opening ------------------------------------------------------------------
// The player cast profession `spellID`, which opens the window on `key`.
void Cast(Shown &s, uint32_t key, int spellID, uintptr_t openVar, uintptr_t toggle) {
if (s.linked) {
Unlink(s);
if (Game::Read<uint32_t>(openVar) == key)
Game::Ref<uint32_t>(openVar) = 0;
}
s.spellID = spellID;
s.guid = Unit::Identity::PlayerGuid();
s.opened = key;
reinterpret_cast<Toggle_t>(toggle)(key);
}
void __fastcall CastTradeSkill(uint32_t skillLine, int spellID) {
Cast(g_tradeSkill, skillLine, spellID, Offsets::VAR_CURRENT_TRADESKILL_LINE,
Offsets::FUN_TOGGLE_TRADE_SKILL);
}
void __fastcall CastCraft(uint32_t craftType, int spellID) {
Cast(g_craft, craftType, spellID, Offsets::VAR_CRAFT_WINDOW_STATE, Offsets::FUN_TOGGLE_CRAFT);
}
// Stand in for TryCastSpell's toggle calls: the toggle's key in ECX, the cast
// spell in ESI.
__declspec(naked) void CastTradeSkillThunk() {
__asm {
mov edx, esi
jmp CastTradeSkill
}
}
__declspec(naked) void CastCraftThunk() {
__asm {
mov edx, esi
jmp CastCraft
}
}
// --- The list builds ----------------------------------------------------------
// CALL_BUILD_TRADE_SKILL_SPELLS: CGPlayer_C::FindSkillLineSpells.
const void *__fastcall TradeSkillSpells(void *player, void *, uint32_t skillLine) {
if (BuildsLink(g_tradeSkill, skillLine))
return &g_tradeSkill.list;
return reinterpret_cast<PlayerList_t>(Offsets::FUN_PLAYER_FIND_SKILL_LINE_SPELLS)(player,
skillLine);
}
// CALL_BUILD_TRADE_SKILL_ABILITY: the ability lookup for the player's race
// and class.
const uint8_t *__fastcall TradeSkillAbility(uint32_t race, uint32_t classId, uint32_t spellID) {
const Shown &s = g_tradeSkill;
if (BuildsLink(s, TradeSkillOpen()))
return Ability(s.race, s.classId, spellID);
return Ability(race, classId, spellID);
}
// SITE_BUILD_TRADE_SKILL_RANK: the player's raw rank in the row's line, 0
// without a slot, as the 33 bytes it replaces read it.
int __fastcall TradeSkillRank(const uint8_t *player, void *, uint32_t skillLine) {
if (ShowsOtherLink(g_tradeSkill, TradeSkillOpen()))
return g_tradeSkill.rank;
const int slot =
reinterpret_cast<SlotForSkill_t>(Offsets::FUN_SKILL_LINE_TO_SLOT)(player, skillLine);
if (slot < 0)
return 0;
return Game::Read<uint16_t>(Game::Read<const uint8_t *>(player, Offsets::OFF_CGPLAYER_INFO) +
Offsets::OFF_SKILL_INFO_TABLE + slot * Offsets::SKILL_INFO_STRIDE,
Offsets::OFF_SKILL_INFO_CUR);
}
// CALL_BUILD_CRAFT_SPELLS: CGPlayer_C::GetTabSpells.
const void *__fastcall CraftSpells(void *player, void *, uint32_t tab) {
if (BuildsLink(g_craft, tab))
return &g_craft.list;
return reinterpret_cast<PlayerList_t>(Offsets::FUN_PLAYER_GET_TAB_SPELLS)(player, tab);
}
// CALL_BUILD_CRAFT_ABILITY: CGUnit_C::FindSkillLineAbility on the player.
const uint8_t *__fastcall CraftAbility(void *unit, void *, uint32_t spellID) {
const Shown &s = g_craft;
if (BuildsLink(s, CraftOpen()))
return Ability(s.race, s.classId, spellID);
return reinterpret_cast<UnitAbility_t>(Offsets::FUN_UNIT_FIND_SKILL_LINE_ABILITY)(unit,
spellID);
}
// CALL_BUILD_CRAFT_RANK: CGPlayer_C::GetSkillTotal.
int __fastcall CraftRank(void *player, void *, uint32_t skillLine) {
if (ShowsOtherLink(g_craft, CraftOpen()))
return g_craft.rank;
return reinterpret_cast<SkillTotal_t>(Offsets::FUN_PLAYER_GET_SKILL_TOTAL)(player, skillLine);
}
// CGGameUI::ShutdownGame's calls of the window shutdowns.
void __cdecl ShutdownTradeSkill() {
g_tradeSkill = Shown{};
reinterpret_cast<Void_t>(Offsets::FUN_SHUTDOWN_TRADE_SKILL)();
}
void __cdecl ShutdownCraft() {
g_craft = Shown{};
reinterpret_cast<Void_t>(Offsets::FUN_SHUTDOWN_CRAFT)();
}
// --- Lua ----------------------------------------------------------------------
int PushLinked(void *L, const Shown &s) {
if (!s.linked) {
Game::Lua::PushNil(L);
Game::Lua::PushNil(L);
return 2;
}
Game::Lua::PushNumber(L, 1.0);
if (const char *name = Unit::Identity::NameForGuid(s.guid))
Game::Lua::PushString(L, name);
else
Game::Lua::PushNil(L);
return 2;
}
// `IsTradeSkillLinked()` — 3.3.5's Script_IsTradeSkillLinked (0x005DA310).
int __fastcall Script_IsTradeSkillLinked(void *L) { return PushLinked(L, g_tradeSkill); }
// `IsCraftLinked()` — the Craft window's twin.
int __fastcall Script_IsCraftLinked(void *L) { return PushLinked(L, g_craft); }
// `GetTradeSkillLine()`: 3.3.5 answers with the linker's ranks.
int __fastcall Script_GetTradeSkillLine(void *L) {
const int pushed = reinterpret_cast<Script_t>(Offsets::FUN_SCRIPT_GET_TRADE_SKILL_LINE)(L);
if (ShowsOtherLink(g_tradeSkill, TradeSkillOpen())) {
Game::Lua::SetTop(L, -3); // the player's rank and maximum
Game::Lua::PushNumber(L, g_tradeSkill.rank);
Game::Lua::PushNumber(L, g_tradeSkill.maxRank);
}
return pushed;
}
// `GetCraftDisplaySkillLine()`: the engine reads the line from the player's
// own list, which may not hold it.
int __fastcall Script_GetCraftDisplaySkillLine(void *L) {
const Shown &s = g_craft;
const uint32_t open = CraftOpen();
const char *name = open == kCraftTypeEnscribe && ShowsOtherLink(s, open)
? DBC::LocalizedField(Offsets::VAR_SKILL_LINE_RECORDS,
Offsets::VAR_SKILL_LINE_COUNT, s.skillLine,
Offsets::OFF_SKILL_LINE_NAME)
: nullptr;
if (name == nullptr)
return reinterpret_cast<Script_t>(Offsets::FUN_SCRIPT_GET_CRAFT_DISPLAY_SKILL_LINE)(L);
Game::Lua::PushString(L, name);
Game::Lua::PushNumber(L, s.rank);
Game::Lua::PushNumber(L, s.maxRank);
return 3;
}
// `GetCraftName()`: the engine names the spell the player keeps for the type,
// which may be none.
int __fastcall Script_GetCraftName(void *L) {
const char *name = ShowsOtherLink(g_craft, CraftOpen()) ? SpellName(g_craft.spellID) : nullptr;
if (name == nullptr)
return reinterpret_cast<Script_t>(Offsets::FUN_SCRIPT_GET_CRAFT_NAME)(L);
Game::Lua::PushString(L, name);
return 1;
}
// --- Documentation ------------------------------------------------------------
const Game::Doc::Field kLinkedRets[] = {
Game::Doc::Opt("isLinked", "number", nullptr,
"1 when the window shows a profession from a link, else nil."),
Game::Doc::Opt("linkedPlayerName", "string", nullptr,
"The name of the player whose profession the window shows."),
};
const Game::Doc::Function kIsTradeSkillLinked{
"Returns whether the trade-skill window shows a profession from a link, and whose.", {},
kLinkedRets, "TradeSkillGlobals"};
const Game::Doc::Function kIsCraftLinked{
"Returns whether the craft window shows a profession from a link, and whose.", {},
kLinkedRets, "TradeSkillGlobals", true};
void Register() {
static const uint8_t kRankRead[33] = {
0xE8, 0xC3, 0xD7, 0x0E, 0x00, // call FindSkillIndex
0x85, 0xC0, // test eax, eax
0x7C, 0x16, // jl -> xor eax, eax
0x8D, 0x94, 0x40, 0x13, 0x02, 0x00, 0x00, // lea edx, [eax+eax*2+0x213]
0x8B, 0x45, 0xF8, // mov eax, [ebp-8] (player)
0x8B, 0x88, 0x68, 0x0E, 0x00, 0x00, // mov ecx, [eax+0xE68]
0x0F, 0xB7, 0x04, 0x91, // movzx eax, word [ecx+edx*4]
0xEB, 0x02, // jmp past
0x33, 0xC0, // xor eax, eax
};
g_tradeSkillReady =
Common::RedirectCall(Offsets::CALL_GAME_SHUTDOWN_TRADE_SKILL,
Offsets::FUN_SHUTDOWN_TRADE_SKILL,
reinterpret_cast<const void *>(&ShutdownTradeSkill)) &&
Common::RedirectCall(Offsets::CALL_TRY_CAST_TOGGLE_TRADE_SKILL,
Offsets::FUN_TOGGLE_TRADE_SKILL,
reinterpret_cast<const void *>(&CastTradeSkillThunk)) &&
Common::RedirectCall(Offsets::CALL_BUILD_TRADE_SKILL_SPELLS,
Offsets::FUN_PLAYER_FIND_SKILL_LINE_SPELLS,
reinterpret_cast<const void *>(&TradeSkillSpells)) &&
Common::RedirectCall(Offsets::CALL_BUILD_TRADE_SKILL_ABILITY,
Offsets::FUN_SKILL_LINE_ABILITY_LOOKUP,
reinterpret_cast<const void *>(&TradeSkillAbility)) &&
Common::ReplaceWithCall(Offsets::SITE_BUILD_TRADE_SKILL_RANK, kRankRead, sizeof kRankRead,
reinterpret_cast<const void *>(&TradeSkillRank));
g_craftReady =
Common::RedirectCall(Offsets::CALL_GAME_SHUTDOWN_CRAFT, Offsets::FUN_SHUTDOWN_CRAFT,
reinterpret_cast<const void *>(&ShutdownCraft)) &&
Common::RedirectCall(Offsets::CALL_TRY_CAST_TOGGLE_CRAFT, Offsets::FUN_TOGGLE_CRAFT,
reinterpret_cast<const void *>(&CastCraftThunk)) &&
Common::RedirectCall(Offsets::CALL_BUILD_CRAFT_SPELLS, Offsets::FUN_PLAYER_GET_TAB_SPELLS,
reinterpret_cast<const void *>(&CraftSpells)) &&
Common::RedirectCall(Offsets::CALL_BUILD_CRAFT_ABILITY,
Offsets::FUN_UNIT_FIND_SKILL_LINE_ABILITY,
reinterpret_cast<const void *>(&CraftAbility)) &&
Common::RedirectCall(Offsets::CALL_BUILD_CRAFT_RANK, Offsets::FUN_PLAYER_GET_SKILL_TOTAL,
reinterpret_cast<const void *>(&CraftRank));
Game::Lua::RegisterGlobalFunction("IsTradeSkillLinked", &Script_IsTradeSkillLinked,
&kIsTradeSkillLinked);
Game::Lua::RegisterGlobalFunction("IsCraftLinked", &Script_IsCraftLinked, &kIsCraftLinked);
Game::Lua::RegisterGlobalFunction("GetTradeSkillLine", &Script_GetTradeSkillLine);
Game::Lua::RegisterGlobalFunction("GetCraftDisplaySkillLine", &Script_GetCraftDisplaySkillLine);
Game::Lua::RegisterGlobalFunction("GetCraftName", &Script_GetCraftName);
}
const Game::ModuleAutoRegister _autoreg{&Register};
} // namespace
int TradeSkillSpell() { return TradeSkillOpen() != 0 ? g_tradeSkill.spellID : 0; }
int CraftSpell() { return CraftOpen() != 0 ? g_craft.spellID : 0; }
void ShowLinked(int spellID, uint64_t guid, const char *bits, int rank, int maxRank) {
if (Unit::Identity::PlayerObject() == nullptr)
return;
const uint8_t *linker = Unit::Identity::PlayerInfoRecord(guid);
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
if (linker == nullptr || record == nullptr ||
Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_EFFECT) !=
Offsets::SPELL_EFFECT_TRADE_SKILL)
return;
const uint32_t race = Game::Read<uint32_t>(linker, Offsets::OFF_PLAYER_INFO_RACE);
const uint32_t classId = Game::Read<uint32_t>(linker, Offsets::OFF_PLAYER_INFO_CLASS);
const uint8_t *ability = Ability(race, classId, static_cast<uint32_t>(spellID));
if (ability == nullptr)
return;
const uint32_t skillLine = Game::Read<uint32_t>(ability, Offsets::OFF_SLA_SKILL_ID);
if (DBC::Record(Offsets::VAR_SKILL_LINE_RECORDS, Offsets::VAR_SKILL_LINE_COUNT, skillLine) ==
nullptr)
return;
const uint8_t *rows[Link::MAX_ABILITIES];
const int count = Link::Abilities(skillLine, rows);
if (count == 0 || static_cast<size_t>(Link::BitChars(count)) != std::strlen(bits))
return;
const uint32_t craftType =
Game::Read<uint32_t>(record, Offsets::OFF_SPELL_RECORD_EFFECT_MISC_VALUE);
if (!(craftType == 0 ? g_tradeSkillReady : g_craftReady))
return;
Shown &s = craftType == 0 ? g_tradeSkill : g_craft;
s.spells.clear();
for (int i = 0; i < count; ++i) {
const int spell = Game::Read<int>(rows[i], Offsets::OFF_SLA_SPELL_ID);
if (Link::IsSet(bits, i) && InPlayerList(spell, craftType))
s.spells.push_back(spell);
}
const uint32_t n = static_cast<uint32_t>(s.spells.size());
s.list = {n, n, s.spells.data(), 0};
s.spellID = spellID;
s.guid = guid;
s.linked = true;
s.opened = craftType == 0 ? skillLine : craftType;
s.skillLine = skillLine;
s.race = race;
s.classId = classId;
s.rank = rank;
s.maxRank = maxRank;
if (craftType == 0) {
reinterpret_cast<Void_t>(Offsets::FUN_TRADE_SKILL_STOP_RECAST)();
if (TradeSkillOpen() == skillLine)
Game::Ref<uint32_t>(Offsets::VAR_CURRENT_TRADESKILL_LINE) = 0;
reinterpret_cast<Toggle_t>(Offsets::FUN_TOGGLE_TRADE_SKILL)(skillLine);
} else {
if (CraftOpen() == craftType)
Game::Ref<uint32_t>(Offsets::VAR_CRAFT_WINDOW_STATE) = 0;
reinterpret_cast<Toggle_t>(Offsets::FUN_TOGGLE_CRAFT)(craftType);
}
}
} // namespace TradeSkill::Window
+34
View File
@@ -0,0 +1,34 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
// Whose profession the TradeSkill and Craft windows show. See Window.cpp.
namespace TradeSkill::Window {
// The profession spell the TradeSkill window shows (3.3.5's
// CGTradeSkillInfo::m_spellID), or 0 while the window is closed.
int TradeSkillSpell();
// The Craft window's twin of TradeSkillSpell.
int CraftSpell();
// 3.3.5's CGTradeSkillInfo::SetTradeSkill for a received link: shows
// profession `spellID` of player `guid`, at `rank` / `maxRank`, with the
// rows set in the link's `bits`, in the window the spell opens when cast.
// The name cache must hold `guid`.
void ShowLinked(int spellID, uint64_t guid, const char *bits, int rank, int maxRank);
} // namespace TradeSkill::Window
+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