Compare commits

..
Author SHA1 Message Date
Brues d339f4486d refactor(swing): reset on weapon swap from the inventory-slot observer
A weapon equipped in combat resets that hand's swing timer on the
server, with no packet for it. PLAYER_SWING used to catch this by
reading the main-hand, off-hand and ranged GUIDs from the player's
inventory manager (player+0x1D38) on every WorldTick and comparing
them with the previous frame.

The engine already reports these changes. CGPlayer_C::AddActiveWatchers
(0x005DD8A0) registers OnInventorySlotChanged (0x005DDCF0) on every
equipment slot, and the descriptor dispatcher calls it only when a
slot's GUID really changes. Player::Equipment already hooks that
handler for PLAYER_EQUIPMENT_CHANGED. It now also calls
Combat::Swing::OnWeaponSlotChanged, which resets the hand for slots
15/16/17 when the player is in combat.

The call runs inside SMSG_UPDATE_OBJECT, after the values block is
written, so the attack-time descriptor field already holds the new
weapon's speed. The lookups it makes (player by GUID, descriptor
reads) do not throw.

The per-frame poll and its snapshot state are removed. The WorldTick
subscriber stays: it still merges several resets in one frame into
one PLAYER_SWING per swing type.
2026-10-31 13:50:41 -05:00
Brues 92025896b5 feat(options): a control for every cvar, one cvar per control
The AddOn Options panel now has a control for each of the 38 cvars that
ClassicAPI registers, plus the player cache, on pages grouped by
feature: Interact Key, Action Targeting, Combat, Bags and Items,
Interface, Text To Speech and Advanced.

Each control reads and writes exactly one cvar, and writes it only when
the player uses that control. The "Interact Key Icons" preset dropdown
is gone: it wrote four icon cvars at once, and Cancel restored it from
a snapshot that read any mixed state as "NPCs Only", which turned
SoftTargetIconInteract back on. Okay, Cancel and Defaults no longer
write anything on these pages.

- On/off cvars are checkboxes. The soft-target enable cvars read as on
  for 2 or 3 and write 3 or 0; 1 is gamepad, which does nothing here.
- Fixed choices (the three arc cvars, SoftTargetForce) are dropdowns. A
  value outside the choices shows as its number.
- Open-ended numbers (ranges, icon size, shake strength, combat log
  retention) are number boxes, so the panel sets no maximum. Sliders
  are kept for documented ranges only.
- Each page's controls sit in a WowScrollBox with a MinimalScrollBar.
  The box clips its children, and the clip also keeps the mouse off
  controls that are scrolled out of view.

The panel's strings are globals in locales\enUS.lua, which every client
loads: CLASSICAPI_* for ClassicAPI's own, and retail's GlobalStrings
keys where retail has the string (COUNTDOWN_FOR_COOLDOWNS_TEXT and its
tooltip, with retail's translations in the other locale files).

The README cvar table gains the five cvars it was missing.
2026-10-31 12:58:22 -05:00
Brues 4eb214f7f1 fix(tick): run world-tick subscribers after the UI draw
CWorld::BeginFrame (0x0066FD50), the world tick, runs inside the UI
strata walk. Its only caller is CGWorldFrame::OnRender (0x00482EA0),
which WorldFrame's layer-0 draw callback (0x00482D70) runs. By then
0x007657D0 has already rebuilt the WORLD stratum's level batches. The
levels above 0 are not drawn yet. Nameplates sit at level 1, with
their name and level text on OVERLAY.

A batch's text run keeps a raw pointer to its font. The run's draw
(0x005C8FE0) uploads all eight of that font's atlas pages, even when
no strings are left in the run. Every subscriber ran at the hook, and
several of them run Lua (NAME_PLATE_CREATED, the C_Timer callbacks).
An addon that restyled a plate there made the plate leave level 1 and
gave its font strings a new font. The old font was destroyed while
the built batch still pointed at it. The upload then read a freed
texture's format and faulted at 0x005A09B8.

The crash dump shows this state:
- Level 1 has no frames.
- Its dirty mask is 0xFFFFFFFF, and the stratum's flags are set again.
  Both were written after the rebuild.
- Its one OVERLAY run is empty, and its font's memory holds Lua table
  nodes.

The hook now only marks that the world ran. The subscribers run from
the UI render root's tail (Tick::FrameTick), after the strata walk,
so the engine rebuilds anything they change before it is drawn.
OnUpdate and packet events still run in the same order relative to
them. GetTickTime() also reads the same pass delta as before.

Fixes #116
2026-10-31 06:06:38 -05:00
Brues 706fb1c540 docs(readme): refresh the function, namespace and event counts
The intro and the Lua-calls summary still said 550+ functions across ~60
namespaces and 50+ events, the counts from 54516ad. Counted the same way
(backticked calls in the Lua-calls table, rows in the Events table), the
README now lists 980 calls under 75 namespaces and 86 events, so the
rounded-down figures become 950+, ~75 and 80+.
2026-10-31 05:22:52 -05:00
Brues e37052c67d feat(lua): SharedColorConstants
Port of retail's Blizzard_SharedXML/SharedColorConstants.lua, in
Blizzard's own text. It loads after ColorUtil.lua, when Color.lua has
already made the GlobalColor globals it reads, and adds
PLAYER_FACTION_COLORS_HEX, PLAYER_FACTION_BRIGHT_COLORS (the PvP
scoreboard colors) and COVENANT_COLORS.

FACTION_BAR_COLORS now holds retail's color objects in place of 1.12's
plain {r, g, b} tables. The stock code that reads it (the unit tooltip
and the reputation bars) reads only .r, .g and .b, which the objects
have; the values differ by less than 1/255.

PLAYER_FACTION_COLORS moves out of Constants.lua, because retail
defines it only here, and GetFactionColor goes back to retail's
one-line body without our nil guard.

Verified in game: FACTION_BAR_COLORS[1] is ffcc4d38 and [5] is
FACTION_GREEN_COLOR, PLAYER_FACTION_COLORS_HEX[0] is ffff2934,
PLAYER_FACTION_BRIGHT_COLORS[1] is ff00adf0, GetFactionColor("Alliance")
is ff00adf0, and COVENANT_COLORS.Kyrian is ff2aa2ff.
2026-10-31 05:14:36 -05:00
Brues f8f6dc28b8 feat(ui): GlobalColor table from build 1.60.1.70707
C_UIColor.GetColors now returns the GlobalColor.dbc rows of build
1.60.1.70707 instead of the BC Classic 2.5.5 snapshot: 331 rows where
there were 187, in the table's own row order.

- 148 new colors, among them PVP_SCOREBOARD_*_CELL_COLOR, the
  EXPANSION_COLOR_* set, and the profession, threat and power-prediction
  colors.
- 10 changed values, as the newer client has them: NORMAL_FONT_COLOR
  ffd200, RED_FONT_COLOR ff2020, PLAYER_FACTION_COLOR_HORDE ff2934,
  PLAYER_FACTION_COLOR_ALLIANCE 00adf0, TALENT_LINK_COLOR 71d5ff, and
  more. The C++ tooltips that read the table (item comparison,
  equipment sets, links) take the new values. Lua globals that 1.12
  FrameXML already defines keep their own values, because Color.lua
  only creates the globals that do not exist.
- The four HEALTHBAR_*_HEAL_PREDICTION_GRADIENT_COLOR* rows are gone;
  the newer table does not have them and nothing read them.
- A name that the DBC lists twice keeps only its last row, which is
  the value the client's own Color.lua loop ends on.
  COMPACT_UNIT_FRAME_FRIENDLY_HEALTH_COLOR and
  CHARACTER_FRAME_SIDE_PANEL_SKILL_COLOR join the earlier duplicates.
- DEBUFF_TYPE_ENRAGE_COLOR stays as our own row, for
  C_UnitAuras.GetAuraDispelTypeColor("Enrage").

Verified in game: GetColors returns 331 rows,
PVP_SCOREBOARD_HORDE_CELL_COLOR is ffff2934 and the alliance one
ff00adf0, GetAuraDispelTypeColor("Enrage") is ffff8c00, and
HEALTHBAR_MY_HEAL_PREDICTION_GRADIENT_COLOR1 is nil.
2026-10-31 05:11:55 -05:00
Brues 4472c81137 feat(lua): retail SharedXML helpers, Spline, ExportUtil and MixinUtil
Ports from retail's Blizzard_ObjectAPI, SharedXML, SharedXMLBase and
FrameXMLUtil, in Blizzard's own text, from the _forever_ build:
- ObjectCache.lua and SpellBookItemLocation.lua (ObjectAPI).
- TypeUtil.lua: ValueToBoolean and StringToBoolean.
- SortUtil.lua: SortUtil.CreateSortManager and the comparators.
- Sequencer.lua, Spline.lua (Catmull-Rom splines), TaggableObject.lua,
  ObjectUpdater.lua and ExportUtil.lua (base64 export/import streams).
- AnimationSystem.lua: SetUpAnimation and CancelAnimations.
- SmoothStatusBar.lua: SmoothStatusBarMixin.
- MixinUtil.lua: DrawLine, LineMixin, SparseGridMixin,
  DoublyLinkedListMixin, DirtiableMixin, DirtyPhaseMixin and more. 1.12
  has no global wipe, so the file reads table.wipe, as the other ports do.

FrameDeltaLerp now takes retail's three arguments and reads the frame
time from GetTickTime(). The old form needed a fourth elapsed argument,
so SmoothStatusBar's per-frame ticker would have errored. Nothing else
called it.

DebugTools drops its private StringToBoolean copy and calls the global
one from TypeUtil for the /framestack arguments.

FadingFrame.lua is not ported: 1.12's own FrameXML defines the same
FadingFrame_* names with no-argument `this` signatures, and ZoneText
calls FadingFrame_OnUpdate() every frame.

Verified in game: StringToBoolean, ValueToBoolean and
SpellBookItemLocation return the expected values, a 2D spline's midpoint
is (5, 10), an export stream round-trips 200 and 9, the sort manager
orders {3,1,2}, the linked list enumerates 0 1 2, ObjectUpdater,
Sequencer and SetUpAnimation finish, a smoothed status bar reaches 100,
and /framestack still opens.
2026-10-31 05:01:37 -05:00
Brues 1e1463cf9a feat(unit): UNIT_IN_RANGE_UPDATE and UNIT_DISTANCE_CHECK_UPDATE
UNIT_IN_RANGE_UPDATE (unitTarget, isInRange). Nothing in the client
reacts to a distance: the player moves every frame while it moves, and
other units in view are moved a little each frame between their movement
packets. So a world-tick pass checks the units, only while a frame is
registered for the event. The pass walks the event's subscriber chain
(Event::Custom::ForEachListener, which FireValues now uses too): a frame
registered with RegisterUnitEvent adds its units
(Frame::UnitEvent::Units), and a plain registration adds pet and each
party and raid member and their pets. A token fires when
Unit::Range::InRange changes for it. UnitInRange now calls the same
function. A filter unit that is not a unit token is dropped after one
protected IsUnitToken check, since the engine's resolver raises a Lua
error for it.

UNIT_DISTANCE_CHECK_UPDATE (unitTarget, isInDistance) is true while the
client holds a group member's object. UpdateRaidMemberRecord (0x004BADC0)
has four E8 callers, which are exactly a player object entering the
object table (CGPlayer_C::PostInit 0x005DEC43, PostUpdate on reactivation
0x005DECAF) and leaving it (the release slot 0x005E9ACF, ~CGPlayer_C
0x005DD6AB for an object never released). The calls point at a site that
notes a party or raid member's GUID (IsRaidMemberOrPet, not the active
player), and the next world tick fires for each of its tokens with
whether the object table holds it then. The release slot runs before the
manager moves the object to the released table, so a fire in place would
still show the member to UnitIsVisible. No loaded DLL references the
function or the sites.
2026-10-31 04:54:35 -05:00
Brues c21e44e086 feat(unit): UNIT_AREA_CHANGED fires when a group member changes zone
DiffPartyRecords (0x005E5510) already sets bit 0x80 when the zone in a
party record (+0x14) differs, but SignalPartyRecordChanges (0x005E5720)
raises nothing for it. The existing SignalSite redirects (the calls in
ReadPartyMemberStats and ComparePartyRecord, and the AddPartyMember tail)
now fire UNIT_AREA_CHANGED (unitTarget) on that bit, after the engine's own
events, once per token of the member.

The server sends the zone only to members that cannot see the player.
A member in view is built with the player's own zone (BuildPartyRecord
stores s_zoneId), the same zone GetRaidRosterInfo gives for a held object,
so ComparePartyRecord sets the bit when a member comes into view from
another zone. CGPlayer_C::PostInit skips ComparePartyRecord for the active
player, so "player" gets no event.
2026-10-31 04:25:36 -05:00
Brues e76616de83 feat(player): PLAYER_MOUNT_DISPLAY_CHANGED fires when the player's mount changes
1.12.1 reacts to a unit's new UNIT_FIELD_MOUNTDISPLAYID in
CGUnit_C::OnMountDisplayChanged (0x005FFA50): dismount from the old
display, mount the new one with the mount-up effect, refresh the loop
sound, quest marker and name. The field's type watcher
OnMountDisplayFieldChanged (0x00604570) resolves the unit and calls it
with the old value from one E8 call (0x0060459B), only on a real change.

That call now reaches a function that runs the handler, then fires
PLAYER_MOUNT_DISPLAY_CHANGED (no payload) when the unit is the player.
VanillaHelpers calls the handler directly after a local mount morph and
patches neither address, so a cosmetic morph fires nothing.
2026-10-31 04:15:13 -05:00
Brues 92794a1822 feat(unit): UNIT_TARGETABLE_CHANGED fires when a unit's selectability flips
1.12.1 reacts to a changed UNIT_FIELD_FLAGS in
CGUnit_C::OnUnitFlagsChanged (0x005FF580), which the flags field's type
watcher OnFlagsFieldChanged (0x006044B0) calls with the old flags from
one E8 call (0x006044DB). It handles looting, disarm, reaction, PvP and
the aura bit, but nothing reads UNIT_FLAG_NOT_SELECTABLE (0x02000000).

Unit::FlagsChanged now owns that call: it runs the handler, then each
subscriber with the unit and the flags before and after, since a call
site can be redirected only once. Unit::Targetable subscribes and fires
UNIT_TARGETABLE_CHANGED (unitTarget) once per token of the unit when the
not-selectable bit flips.
2026-10-31 04:07:26 -05:00
Brues ab0991f054 feat(unit): UNIT_CONNECTION fires when a group member goes on or offline
A party record's status byte (+0x08) carries the online bit
UnitIsConnected reads. 1.12.1 changes it in three places, and none
signals the flip:

- The party member stats packet: ReadPartyMemberStats (0x005E5110) ends
  in SignalPartyRecordChanges (0x005E5720) with the status change bit.
  The redirected call that already adds the generic power events now
  also fires UNIT_CONNECTION (unitTarget, isConnected) when the old and
  new records differ in the online bit.
- The group list (0x7D): the server reports a logoff only there.
  AddPartyMember (0x004E82D0) copies a kept member's previous record and
  sets the online bit, then stops. Its call now diffs the previous record
  against the stored one (DiffPartyRecords, 0x005E5510) and runs the same
  SignalPartyRecordChanges path, so the engine's status events and
  UNIT_CONNECTION follow.
- A raid member outside the player's party has only a raid slot, whose
  status byte RebuildRaidRoster (0x004BA5F0) sets from the same list. Its
  call now compares each returning member's online bit before and after
  and fires UNIT_CONNECTION on a flip; party members are skipped, since
  AddPartyMember ran first.

isConnected is a real boolean, pushed through FireValues.
2026-10-31 03:50:09 -05:00
Brues 26c201b95b fix(event): a named unit field gets a watch-copy slot first
Every unit-field watcher keeps a mirror of its bytes in the object's
watch copy, at the slot UnitWatchedIndex (0x0047DB20) finds for the field
in the table at 0x00839E48. A field not in the table gets 0xB6, past the
unit's 170-slot copy. UNIT_MOD_CAST_SPEED (139), named for
UNIT_SPELL_HASTE, is such a field, so every watcher on it wrote the
field over memory past the copy: a pet's cast-speed update on its
owner's logoff put a 1.0f over a list link, and ClntObjMgrRelease
crashed on the next SMSG_DESTROY_OBJECT (ERROR #132 at 0x00464A02).

The table holds 166 fields and one spare entry (0xB6, the copy's last
slot, which no field asks for); UnitWatchedIndex is its only reader and
nothing writes it. EngineName now writes a field without a slot into
that spare before naming its slot, and refuses the name once the spare
is taken. Field 139 takes it, so UNIT_SPELL_HASTE keeps the engine's
watcher for every unit.
2026-10-31 03:47:17 -05:00
Brues 09693af5b9 feat(unit): UNIT_POWER_UPDATE, UNIT_POWER_FREQUENT and UNIT_MAXPOWER
1.12.1 calls type-wide field observers for every unit whose watched
bytes change: ClntObjMgrSetTypeMirrorHandler (0x00468070) puts a record
on the global list of a (bank, field), and CGUnit_C::RegisterFieldWatchers
(0x006041F0) uses it for the engine's own unit handlers, among them
OnPowerFieldChanged on the power block, which only refreshes usability.

Two handlers join those lists, one record per POWER1..5 and MAXPOWER1..5
slot, so the callback's field offset is the power type. They fire
UNIT_POWER_UPDATE and UNIT_POWER_FREQUENT, or UNIT_MAXPOWER, with
(unitTarget, powerType) once per token of the unit. Every client power
change is a server update, so FREQUENT fires with UPDATE.

Each in-world registration pass removes and re-adds the records through
0x00468240: the first pass of a session runs in CGGameUI::InitializeGame,
after MirrorInitialize (0x0047F840) has rebuilt the tables a record's
copy offset comes from, and the removal keeps /reload from stacking a
second record. The list array has no other writer, so nothing frees a
record behind us.

A group member the client cannot see has no unit to watch; its power
comes in the party member stats packet. ReadPartyMemberStats (0x005E5110)
and ComparePartyRecord (0x005F0B80) end in SignalPartyRecordChanges
(0x005E5720), which fires the per-type power events for the changed
record bits. Both E8 calls now reach a function that calls it and then
fires the generic events for the same bits, the member's and its pet's.
2026-10-31 03:44:28 -05:00
Brues f24b7e594b feat(unit): UNIT_SPELL_HASTE fires when a unit's spell haste changes
1.12.1 turns unit-field changes into events by index: FUN_0051bbb0
watches unit field i when slot i of the static event-name array at
0x00BE1198 is named, and FUN_0051bd50 signals event i once per token of
the unit. UNIT_MOD_CAST_SPEED, the field UnitSpellHaste reads, is index
139 (OFF_UNIT_MOD_CAST_SPEED / 4), and slot 139 has no name, so the
engine never watched it.

Event::Custom::EngineName names slot 139, and the engine's own watchers
fire the event for every token. Nothing else signals 139 by number: it
is above the glue band and not in the chat-type event table.
2026-10-31 02:33:12 -05:00
Brues 35c8ba5688 docs(readme): list UNIT_TARGET 2026-10-31 02:28:00 -05:00
Brues e2a34bd18a feat(unit): UNIT_TARGET fires when a unit's target changes
1.12.1 already turns unit-field changes into events. FUN_0051bbb0
registers a watcher on unit field i for each named slot i of the static
event-name array at 0x00BE1198, and the watcher (FUN_0051bd50) signals
event i once per token of the unit through the broadcast FUN_00515e50.
The field index is the event id. UNIT_FIELD_TARGET is index 10, and the
watcher's size selector still gives it the 8 bytes of a GUID, but slot
10 has no name, so the engine never watches the field.

Event::Custom::EngineName names such a slot. It writes the name into the
static array at load, before any world init or unit watch. World init
copies the array into the live event table (0x0048FEA1), and the
engine's own watchers then fire the event for every token, with focus,
nameplateN, markN and focustarget included. The entry joins the
reservation list with a fixed slot, so it is documented, kept across
/reload, and not counted by Grow.

Slot 10 is in the glue band. Its one by-number signal is the glue
realm-list rebuild (0x0046E73D), which only CGlueMgr::Idle reaches.
2026-10-31 02:25:04 -05:00
Brues 2f787889f7 fix(animation): a frame's alpha group keeps its textures' gradients
When an alpha group starts on a frame, every region below it stores its
vertex alpha, and every update writes that alpha back. The colour getter
(0x77F8C0) reads a gradient back as white, and the setter (0x77F750)
shrinks a gradient's four entries to that one white entry. So a gradient
under a fading frame went flat at the first update and stayed flat after
the group ended. pfUI's easteregg night lost its fade to the bottom.

SetVertexAlpha now writes nothing when the alpha is unchanged. For one
colour entry the setter already does nothing then, so only the gradient
case changes. The frame's fade still reaches the gradient: a change of
the frame's alpha refolds each region's entries (0x77FAC0).

3.3.5's and 4.3.4's PreOnAnimUpdate drop the gradient the same way. A
texture that an alpha animation itself fades still loses its gradient.

Verified in game: a black gradient on a frame fading by -0.5 over 3 s
keeps its gradient during and after the fade.
2026-10-31 01:31:29 -05:00
Brues 9967cc0839 feat(lua): Spell objects, ContinuableContainer and TableBuilder
Ports from retail's Blizzard_ObjectAPI and SharedXML, in Blizzard's own
text, from the _forever_ build (the newest interface code on disk):
- Util/Spell.lua: Spell:CreateFromSpellID and SpellMixin.
- Util/ContinuableContainer.lua: one callback for a group of item and
  spell objects.
- Util/IndexRangeDataProvider.lua: CreateIndexRangeDataProvider, which
  ScrollBoxListView already expected.
- Util/TableBuilder.lua: CreateTableBuilder, for the existing
  ScrollUtil.RegisterTableBuilder.
- FrameXML/CallbackRegistrant: CallbackRegistrantMixin and
  CallbackRegistrantTemplate.
AsyncCallbackSystem.lua gets its ASYNC_SPELL type and SpellEventListener
back, so it is the _forever_ text again.

New in the DLL for SpellMixin:
- C_Spell.IsSpellDataCached, C_Spell.RequestLoadSpellData and
  SPELL_DATA_LOAD_RESULT. Every spell is client data, so a spell that
  exists is cached, and a request fires the event before it returns:
  (spellID, 1), or (spellID, nil) for an ID with no spell. Retail marks
  the event synchronous, and AsyncCallbackSystem relies on that.
- C_Spell.GetSpellDescriptionForItemLocation. The item tooltip expands a
  spell's text with the same arguments GetSpellDescription passes, so it
  returns that text. The item location is still required.

Verified in game: IsSpellDataCached(133) and (999999) are true and false,
Spell:CreateFromSpellID(133) calls back with (133, 1), a container with
item 19019 and spell 133 calls back, CreateIndexRangeDataProvider(5)
has size 5, and GetSpellDescriptionForItemLocation(133, slot 1) equals
GetSpellDescription(133).
2026-10-31 01:26:47 -05:00
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
130 changed files with 10418 additions and 4241 deletions
+28 -8
View File
@@ -1,7 +1,7 @@
# ClassicAPI
ClassicAPI is a DLL for World of Warcraft 1.12.1 (Vanilla / Turtle WoW). It
backports a large part of the modern WoW API — 550+ functions and 50+ events,
backports a large part of the modern WoW API — 950+ functions and 80+ events,
plus much of Lua 5.1 — into the 1.12 client, so addons written for later
versions (3.3.5+ or Classic Era) run with little or no change.
@@ -45,7 +45,7 @@ heading to expand it. For signatures and return values, see the per-function
reference in **[docs/API.md](docs/API.md)**.
<details>
<summary><b>In-game Lua calls</b> — 550+ functions across ~60 namespaces</summary>
<summary><b>In-game Lua calls</b> — 950+ functions across ~75 namespaces</summary>
In the Frame rows, a name that starts with `:` is a method of the widget that the row names, for example `:SetShown` is `frame:SetShown`.
@@ -69,7 +69,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Cursor](docs/API.md#cursor) | `CursorHasMacro`, `GetCursorInfo` |
| [Container](docs/API.md#container) | `C_Container.AutoStoreItem`, `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemEquipmentSetInfo`, `C_Container.GetContainerItemQuestInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerFreeSlots`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetBackpackAutosortDisabled`, `C_Container.GetBankAutosortDisabled`, `C_Container.GetItemCooldown`, `C_Container.GetSortBagsRightToLeft`, `C_Container.HasContainerItem`, `C_Container.IsContainerFiltered`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SetBackpackAutosortDisabled`, `C_Container.SetBagPortraitTexture`, `C_Container.SetBankAutosortDisabled`, `C_Container.SetItemSearch`, `C_Container.SetSortBagsRightToLeft`, `C_Container.SortBags`, `C_Container.SortBankBags`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown`, `SetBagPortraitTexture` |
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID`, `C_CreatureInfo.GetRaceInfo`, `C_CreatureInfo.GetClassInfo`, `C_CreatureInfo.GetCreatureFamilyInfo`, `C_CreatureInfo.GetCreatureFamilyIDs`, `C_CreatureInfo.GetFactionInfo`, `C_CreatureInfo.GetCreatureTypeInfo`, `C_CreatureInfo.GetCreatureTypeIDs` |
| [Currency](docs/API.md#currency) | `GetCoinText`, `C_CurrencyInfo.GetCoinText`, `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString` |
| [Currency](docs/API.md#currency) | `GetCoinText`, `C_CurrencyInfo.GetCoinText`, `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString`, `Constants.MoneyFormattingConstants` |
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
| [DeathInfo](docs/API.md#deathinfo) | `C_DeathInfo.GetSelfResurrectOptions`, `C_DeathInfo.UseSelfResurrectOption` |
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.PickupEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` |
@@ -100,6 +100,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Item: info](docs/API.md#getitemclassinfoclassid) | `GetItemClassInfo`, `GetItemSubClassInfo`, `GetItemIcon`, `IsConsumableItem`, `IsCurrentItem`, `IsDressableItem`, `IsEquippedItemType`, `IsHarmfulItem`, `IsHelpfulItem`, `IsUsableItem` |
| [Item: inventory](docs/API.md#getaverageitemlevel) | `GetAverageItemLevel`, `GetInventoryItemID`, `GetInventoryItemDurability`, `GetInventoryItemRepairCost`, `GetInventoryItemsForSlot`, `IsDualWielding`, `SetInventoryPortraitTexture` |
| [Item: item IDs](docs/API.md#getitemid--companions-to-the-engines-getitemlink-family) | `GetAuctionItemID`, `GetAuctionSellItemID`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeTargetItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID` |
| [Localization](docs/API.md#localization) | `AbbreviateLargeNumbers`, `AbbreviateNumbers`, `BreakUpLargeNumbers`, `C_StringUtil.GetDefaultAbbreviationBreakpoints` |
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootAllCorpses`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount`, `C_LossOfControl.GetSchoolLockout` |
@@ -131,7 +132,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime`, `GetTickTime`, `GetTimeCached` |
| [Totem](docs/API.md#totem) | `GetTotemInfo`, `GetTotemTimeLeft`, `GetTotemDuration`, `TargetTotem` |
| [Tracking](docs/API.md#tracking) | `GetNumTrackingTypes`, `GetTrackingInfo`, `SetTracking` |
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes`, `GetTradeSkillDescription`, `GetTradeSkillRecipeLink`, `IsTradeSkillLinked` |
| [TradeSkillUI](docs/API.md#tradeskillui) | `GetTradeSkillListLink`, `GetCraftListLink`, `GetTradeSkillDescription`, `GetTradeSkillRecipeLink`, `IsTradeSkillLinked`, `IsCraftLinked` |
| [Trainer](docs/API.md#trainer) | `GetTrainerServiceItemLink` |
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
| [Unit](docs/API.md#unit) | `CanBeRaidTarget`, `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `SetUnitCursorTexture`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHasIncomingResurrection`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSelectionColor`, `UnitSelectionType`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID`, `UnitTokenFromName` |
@@ -207,16 +208,21 @@ for what each command does, and
Each setting is saved in `WTF\Config.wtf`. Read and write them with `GetCVar`
/ `SetCVar` or `/console`. Click a group name for the full rules. To change
the most common settings and the player cache in the game, open the game
menu and click AddOn Options, then ClassicAPI.
these settings and the player cache in the game, open the game menu and
click AddOn Options, then ClassicAPI.
| Group | CVar | Default | What it does |
|-------|------|---------|--------------|
| [Bindings](docs/API.md#actionbuttonusekeydown) | `ActionButtonUseKeyDown` | `0` | `1` uses an action-bar key binding when you press the key. `0` uses it when you release the key. |
| [Combat](docs/API.md#stopautoattackontargetchange) | `stopAutoAttackOnTargetChange` | `0` | `1` stops melee auto-attack, Auto Shot and wand shots when you change target. |
| [Combat log](docs/API.md#kept-entries) | `combatLogRetentionTime` | `300` | The number of seconds that the combat log keeps an entry. |
| [Cast settings](docs/API.md#cast-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast while you sit. |
| | `autoDismount` | `0` | `1` makes you dismount when you cast while you are mounted. |
| | `autoUnshift` | `0` | `1` makes you leave your form when a spell cannot be cast in it. |
| [Bag sorting](docs/API.md#c_containergetsortbagsrighttoleft--c_containersetsortbagsrighttoleftenable) | `sortBagsRightToLeft` | `0` | `1` makes a sort fill your bags from the last slot of the last bag. |
| | `backpackAutosortDisabled` | `0` | `1` keeps the items in your backpack in place when you sort your bags. |
| | `bankAutosortDisabled` | `0` | `1` keeps the items in the main bank slots in place when you sort your bank. |
| [Equipment sets](docs/API.md#equipment-sets-on-item-tooltips) | `dontShowEquipmentSetsOnItems` | `0` | `1` removes the Equipment Sets line from item tooltips. |
| [Profiling](docs/API.md#scriptprofile) | `scriptProfile` | `0` | `1` turns on CPU profiling for the `*CPUUsage` functions. The change starts after `/reload`. |
| [Soft interact](docs/API.md#soft-interact-softinteract--anyinteract) | `SoftTargetInteract` | `3` | Turns `softinteract` on: `2` keyboard and mouse, `3` always, `0` / `1` off. |
| | `SoftTargetInteractArc` | `0` | Where the object can be: `0` straight ahead of the camera, `1` within 30° of it, `2` any direction. |
@@ -233,7 +239,9 @@ menu and click AddOn Options, then ClassicAPI.
| | `SoftTargetIconEnemy` / `SoftTargetIconFriend` | `0` | `1` shows an icon over `softenemy` / `softfriend`. |
| | `SoftTargetLowPriorityIcons` | `0` | `1` also shows the normal pointer as an icon. |
| | `SoftTargetNameplateSize` | `19` | Size of all soft-target icons. |
| [Localization](docs/API.md#breakuplargenumbers-cvar) | `breakUpLargeNumbers` | `1` | `1` puts a separator between each group of three digits in `BreakUpLargeNumbers`. `0` writes no separator. |
| [Cooldown](docs/API.md#createframecooldown--name-parent-template) | `countdownForCooldowns` | `0` | `1` shows the time left as numbers on cooldown frames. |
| `ScriptAnimationUtil` | `ShakeStrengthUI` | `1` | Scales how far `ScriptAnimationUtil.ShakeFrame` moves a frame. `0` turns the shake off. |
| [NamePlate](docs/API.md#nameplate) | `nameplateNotSelectedAlpha` | `0.5` | Alpha of a non-target default nameplate, `0` to `1`. |
| | `nameplateSelectedAlpha` | `1` | Alpha of the target's default nameplate, `0` to `1`. |
| [Text to speech](docs/API.md#tts-cvars) | `ttsVoice` | `0` | The voice that `C_VoiceChat.SpeakText` uses. |
@@ -297,6 +305,7 @@ when launching with `-console`), not as Lua functions. See the
| `PLAYER_AVG_ITEM_LEVEL_READY` | *(none)* |
| `PLAYER_EQUIPMENT_CHANGED` | `equipmentSlot, hasCurrent` |
| `PLAYER_FOCUS_CHANGED` | *(none)* |
| `PLAYER_MOUNT_DISPLAY_CHANGED` | *(none)* |
| `PLAYER_SOFT_ENEMY_CHANGED` | *(none)* |
| `PLAYER_SOFT_FRIEND_CHANGED` | *(none)* |
| `PLAYER_SOFT_INTERACT_CHANGED` | `oldTarget, newTarget` |
@@ -316,7 +325,11 @@ when launching with `-console`), not as Lua functions. See the
| `QUEST_TURNED_IN` | `questID, xpReward, moneyReward` |
| `SELF_RES_SPELL_CHANGED` | *(none)* |
| `SOUNDKIT_FINISHED` | `soundHandle` (only for sounds played with `runFinishCallback`) |
| `UNIT_AREA_CHANGED` | `unitTarget` |
| `UNIT_AURA_DELTA` | `unitTarget, updateInfo` (the instance IDs of the auras added, updated, and removed) |
| `UNIT_CONNECTION` | `unitTarget, isConnected` |
| `UNIT_DISTANCE_CHECK_UPDATE` | `unitTarget, isInDistance` |
| `UNIT_IN_RANGE_UPDATE` | `unitTarget, isInRange` |
| `UNIT_SPELLCAST_SENT` | `"player", target, castGUID, spellID, spellName, rank` |
| `UNIT_SPELLCAST_START` | `unit, castGUID, spellID, spellName, rank` |
| `UNIT_SPELLCAST_STOP` | `unit, castGUID, spellID, spellName, rank` |
@@ -332,6 +345,12 @@ when launching with `-console`), not as Lua functions. See the
| `UNIT_SPELLCAST_RETICLE_CLEAR` | `"player", "", spellID, spellName, rank` |
| `UNIT_SPELLCAST_INTERRUPTIBLE` | `unit` |
| `UNIT_SPELLCAST_NOT_INTERRUPTIBLE` | `unit` |
| `UNIT_MAXPOWER` | `unitTarget, powerType` |
| `UNIT_POWER_FREQUENT` | `unitTarget, powerType` |
| `UNIT_POWER_UPDATE` | `unitTarget, powerType` |
| `UNIT_SPELL_HASTE` | `unitTarget` |
| `UNIT_TARGET` | `unitTarget` |
| `UNIT_TARGETABLE_CHANGED` | `unitTarget` |
| `UPDATE_INVENTORY_DURABILITY` | *(none)* |
| `UPDATE_SHAPESHIFT_FORM` | *(none)* |
| `USER_WAYPOINT_UPDATED` | *(none)* |
@@ -432,8 +451,9 @@ functions but that consumer code still expects to find as globals —
`CallbackRegistryMixin`, `EventRegistry`, `ColorMixin` + `CreateColor`,
`Item` / `ItemLocation`, `MathUtil`(`Lerp` / `Clamp` / `CreateCounter`),
`TableUtil` (`tCompare`, `MergeTable`, `SafePack`, etc.), `EventUtil`
(`ContinueOnAddOnLoaded` and more), `DataProvider`, `CircularBuffer`,
`EasingUtil`, `Interpolator`, `EnumUtil`, `StringUtil`, `AnchorUtil`,
(`ContinueOnAddOnLoaded` and more), `FormattingUtil` (`GetMoneyString`,
`FormatLargeNumber` and more), `FrameUtil`, `DataProvider`, `CircularBuffer`,
`EasingUtil`, `ScriptAnimationUtil`, `Interpolator`, `EnumUtil`, `StringUtil`, `AnchorUtil`,
`GridLayoutUtil`, and the layout frame templates (`VerticalLayoutFrame`,
`HorizontalLayoutFrame`, `ResizeLayoutFrame`, `GridLayoutFrame`,
`StaticGridLayoutFrame`). It also holds the conditional slash
+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.
+847 -94
View File
File diff suppressed because it is too large Load Diff
+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
+296 -18
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
@@ -2388,6 +2402,26 @@ enum Offsets {
// nothing matches. What FUN_004F91A0 calls for an unequipped bag.
FUN_DESC_OBSERVER_UNREGISTER = 0x00467FB0,
// Type-wide observers (ClntObjMgrSetTypeMirrorHandler): one record on
// the global list of a (bank, field) at 0x00B23310, called for EVERY
// object of that type whose watched bytes change, same callback ABI as
// the per-object observers below. CGUnit_C::RegisterFieldWatchers
// (0x006041F0) registers the engine's 25 unit handlers with it from
// ClientInitializeGame, OnPowerFieldChanged (bank 3, +0x44, 0x14) among
// them; ClearFieldWatchers (0x00605290) removes each by name. The list
// array is referenced only by these two and the update passes
// (0x00465443, 0x00466670): nothing frees or resets a list wholesale.
// The record's copy offset comes from tables MirrorInitialize (0x0047F840)
// rebuilds at the top of every ClientInitializeGame, before UI init.
// Register — __fastcall(int bank /*ecx*/, uint32_t fieldOffset /*edx*/,
// uint32_t size, const void *callback, void *userArg, int kind),
// RET 0x10. kind 0 links at the pending tail, 1 at the head.
FUN_TYPE_OBSERVER_REGISTER = 0x00468070,
// Unregister — __fastcall(int bank /*ecx*/, uint32_t fieldOffset
// /*edx*/, const void *callback, void *userArg), RET 8; matches
// (callback, userArg) and no-ops when nothing matches.
FUN_TYPE_OBSERVER_UNREGISTER = 0x00468240,
// Observer callback ABI (verified from the dispatcher's call site at
// 0x004655DC + the engine callback FUN_004F8DB0's RET 0x10):
// int __fastcall cb(uint32_t fieldOffset /*ecx, as registered*/,
@@ -2415,6 +2449,31 @@ enum Offsets {
// Per-index watched size (FUN_0051bbb0's switch): i∈{0,2,4,10}=8,
// 0x29=0xD8 (UNIT_FIELD_AURA), 0x6B=0x30, 0xA7/0xAE=0x1C, else 4.
VAR_EVENT_NAME_TABLE_STATIC = 0x00BE1198, // char*[] boot name array; slot i (base+i*4) non-null ⇒ real event i
// Its length: world init builds the table from it with
// `mov edx,0x225; mov ecx,0xBE1198; call 0x703D90` (0x0048FEA1).
EVENT_NAME_TABLE_STATIC_COUNT = 0x225,
// UnitWatchedIndex — the position of unit field `field` (bank-relative
// index, the unit-field event id) in the unit's watched-field table
// (0x00839E48, 0xA7 entries), the slot of its 4 bytes in the watch copy
// every observer's mirror lives in. A field outside the table answers
// UNIT_WATCHED_NONE, and the copy offset built from it lies past the
// unit's 170-slot copy, inside the object: a watcher on such a field
// writes its old value over the object (UNIT_MOD_CAST_SPEED, index 139,
// put its 1.0f over a list link and crashed ClntObjMgrRelease). Fields
// outside the table: 8, 9, 34..39, 123, 124, 126, 139, 140, 156, 157, 181.
// `uint32_t __fastcall(uint32_t field, uint32_t dwords, uint32_t
// isActivePlayer)`, RET 4; both FieldCopyOffset (0x00466830) and
// GetWatcherCopyOffset (0x004680C0) take the unit slot from it.
FUN_UNIT_WATCHED_INDEX = 0x0047DB20,
UNIT_WATCHED_NONE = 0xB6,
// The table itself (.data, u32 per entry): 166 field indices and one
// spare entry holding UNIT_WATCHED_NONE (position 166, the copy's last
// slot, 169 — no field asks for it: unit indices end at 181 and player
// fields take their slots from 170 on). UnitWatchedIndex (0x0047DB25) is
// the table's only reader and nothing writes it, so a field written into
// the spare entry gets a copy slot of its own.
VAR_UNIT_WATCHED_FIELDS = 0x00839E48,
UNIT_WATCHED_FIELD_COUNT = 0xA7,
EVENT_NAME_UNIT_MAX = 0xB6, // FUN_0051bbb0's loop bound
// CGPlayer_C::AddActiveWatchers' inventory-slot observer
@@ -2647,6 +2706,7 @@ enum Offsets {
// mana/rage/focus/energy/happiness order as POWER1..5). Vanilla
// 1.12 has 5 power types; the WotLK additions (Runes / Runic Power)
// don't exist in this descriptor layout.
UNIT_POWER_TYPE_COUNT = 5, // POWER1..5 / MAXPOWER1..5; the slot is the power type
// UNIT_MOD_CAST_SPEED — the cast-time multiplier float (1.0 = normal,
// <1.0 = faster). The server folds it into `SpellEntry::GetCastTime`
@@ -2743,6 +2803,25 @@ enum Offsets {
// and vmangos UnitDefines.h agree). 4.3.4 drops the focus when a unit
// gains it (CGUnit_C::OnFlagChanged → BreakTarget); 1.12 has no reader.
UNIT_FLAG_NOT_SELECTABLE = 0x02000000,
// CGUnit_C::OnUnitFlagsChanged — 4.3.4's CGUnit_C::OnFlagChanged less its
// UNIT_FLAG_NOT_SELECTABLE branch (break target, UNIT_TARGETABLE_CHANGED):
// acts on the bits that differ from `oldFlags`.
// `void __thiscall(CGUnit_C *unit, uint32_t oldFlags)`, RET 4 (0x005FF7F2).
// One E8 call, from the flags field's type watcher OnFlagsFieldChanged
// (0x006044B0), which resolves the unit and passes the old value; no
// loaded DLL references either address.
FUN_UNIT_FLAGS_CHANGED = 0x005FF580,
CALL_FLAGS_FIELD_FLAGS_CHANGED = 0x006044DB,
// CGUnit_C::OnMountDisplayChanged — dismounts from the old mount display
// (when `oldMountDisplay` is nonzero), mounts the new one with the
// mount-up effect, then refreshes the loop sound, quest marker and name.
// `void __thiscall(CGUnit_C *unit, uint32_t oldMountDisplay)`, RET 4
// (0x005FFAFF). One E8 call, from the UNIT_FIELD_MOUNTDISPLAYID type
// watcher OnMountDisplayFieldChanged (0x00604570), which resolves the
// unit and passes the old value. VanillaHelpers calls the function
// directly after a local mount morph and patches neither address.
FUN_UNIT_MOUNT_DISPLAY_CHANGED = 0x005FFA50,
CALL_MOUNT_DISPLAY_FIELD_CHANGED = 0x0060459B,
// Bit 20 of UNIT_FIELD_FLAGS — on a flight path (vmangos
// UNIT_FLAG_TAXI_FLIGHT). The stand setter FUN_PLAYER_SEND_STAND_STATE
// refuses while it is set (benilla-re objectlayer/standstate#the-sit-gate).
@@ -4853,6 +4932,17 @@ enum Offsets {
// arrays: a 1-based backpack slot S maps to linear (S - 1) + 23. Matches
// PackBagSlot's `*param_3 += 0x17` for the backpack branch.
BACKPACK_LINEAR_BASE = 23,
// Lua container indices (bagID), as `PackBagSlot` reads them and FrameXML
// names them (KEYRING_CONTAINER, BANK_CONTAINER, BACKPACK_CONTAINER,
// NUM_BAG_SLOTS, NUM_BANKBAGSLOTS): keyring -2, bank -1, backpack 0,
// equipped bags 1..4, bank bags 5..10. `Enum.BagIndex` publishes them.
BAG_INDEX_KEYRING = -2,
BAG_INDEX_BANK = -1,
BAG_INDEX_BACKPACK = 0,
BAG_INDEX_BAG_FIRST = 1,
BAG_INDEX_BAG_LAST = 4,
BAG_INDEX_BANK_BAG_FIRST = 5,
BAG_INDEX_BANK_BAG_LAST = 10,
// Player inventory manager layout — used for direct GUID-array reads
// that bypass `GetItemBySlot`'s bank gate at `0x006228C1`.
// +0x00 uint32 max slot count
@@ -5530,6 +5620,25 @@ enum Offsets {
// 0x009BD3C0) does not match the subclass name of these rows.
ITEMSUBCLASS_FLAG_NO_SEARCH_NAME = 0x1,
// PaperDollItemFrame.dbc — the paper-doll slot table. Like ItemSubClass,
// the engine reads a parallel (records, count) pair at the class
// instance's +0x00 / +0x04: `Script_GetInventorySlotInfo` (0x004C81B0)
// walks [0x00C0DA00] for [0x00C0DA04] flat 0x10-byte rows. Columns
// (verified against the dumped DBC):
// +0x00 char* ItemSlotName ("HeadSlot", "Bag0Slot", "AmmoSlot", ...)
// +0x04 char* SlotIcon ("interface\paperdoll\UI-PaperDoll-Slot-Head.blp")
// +0x08 int SlotNumber (inventory slot: 1..19 equipment, 20..23 bag
// slots, 0 ammo)
// The engine flags SlotNumber 18 (ranged) as the relic-check slot
// (`SlotNumber - 1 == 0x11` at 0x004C8264).
VAR_PAPERDOLLITEMFRAME_RECORDS = 0x00C0DA00,
VAR_PAPERDOLLITEMFRAME_COUNT = 0x00C0DA04,
OFF_PAPERDOLLITEMFRAME_RECORD_STRIDE = 0x10,
OFF_PAPERDOLLITEMFRAME_SLOT_NAME = 0x00,
OFF_PAPERDOLLITEMFRAME_SLOT_ICON = 0x04,
OFF_PAPERDOLLITEMFRAME_SLOT_NUMBER = 0x08,
PAPERDOLLITEMFRAME_RELIC_SLOT_NUMBER = 18,
// ItemDisplayInfo.dbc — standard layout. Icon path char* at record +0x14
// (per the helper at 0x005D88B0 that Script_GetItemInfo uses).
VAR_ITEMDISPLAYINFO_RECORDS = 0x00C0DC10,
@@ -6068,8 +6177,8 @@ enum Offsets {
// rank-up / passive rows have both zero. Empirically: the
// Blacksmithing skill line has 343 SLA rows, 334 with a nonzero
// threshold — the 9 excluded are the Apprentice..Artisan rank
// spells and skill grants. This is the discriminator
// `TradeSkill::Link` uses to build the canonical recipe universe.
// spells and skill grants. The profession list builds read both
// for a row's difficulty.
OFF_SLA_TRIVIAL_SKILL_HIGH = 0x28,
OFF_SLA_TRIVIAL_SKILL_LOW = 0x2C,
// The engine's (race, class, spellID) -> SkillLineAbility record lookup:
@@ -7822,6 +7931,61 @@ enum Offsets {
// present (and thus connected). Both take a pointer to the 64-bit GUID.
OFF_GROUP_MEMBER_STATUS_FLAGS = 0x08,
GROUP_MEMBER_STATUS_ONLINE = 0x1,
// The party record's pet: its GUID and power type, whose change bits
// SignalPartyRecordChanges fires for the pet's GUID (0x005E5720).
OFF_GROUP_MEMBER_PET_GUID = 0x80,
OFF_GROUP_MEMBER_PET_POWER_TYPE = 0xD8,
// SignalPartyRecordChanges — 4.3.4's
// CGPlayer_C::DispatchPartyMemberStatsChanges: raises the unit events for
// the fields in `changes` (the DiffPartyRecords 0x005E5510 mask) between
// two party records, through FUN_UNIT_EVENT_BROADCAST.
// `void __fastcall(const uint64_t *guid, uint32_t changes,
// const uint8_t *oldRec, const uint8_t *newRec)`, RET 8 (0x005E5909).
// Two callers, both after the new record is stored: ReadPartyMemberStats
// (0x005E5110, SMSG 0x7E / 0x2F2) and CGPlayer_C::ComparePartyRecord
// (0x005F0B80). Both E8 calls, targets checked against the exe; no loaded
// DLL references the function or either site.
FUN_PARTY_RECORD_SIGNAL = 0x005E5720,
CALL_READ_PARTY_MEMBER_STATS_SIGNAL = 0x005E54FA,
CALL_COMPARE_PARTY_RECORD_SIGNAL = 0x005F0C42,
// DiffPartyRecords — the `changes` mask of the fields two party records
// differ in (bit 1 status, 2 health, 0x10 power, 0x20 max power, …).
// `uint32_t __fastcall(const uint8_t *oldRec, const uint8_t *newRec)`.
FUN_PARTY_RECORD_DIFF = 0x005E5510,
// The group list (SMSG 0x7D, 0x005E6A40) stores each member it keeps:
// AddPartyMember — 4.3.4's CGPartyInfo::AddMember less its closing
// diff-and-dispatch — copies the member's previous record (or a fresh
// one when `oldRecord` is null) into its party slot and sets or clears
// the status online bit from `online`.
// `void __fastcall(uint32_t online, const uint8_t *oldRecord,
// uint64_t guid)`, RET 8 (0x004E8482). One E8 call, in the group list.
FUN_ADD_PARTY_MEMBER = 0x004E82D0,
CALL_GROUP_LIST_ADD_PARTY_MEMBER = 0x005E6C4A,
// RebuildRaidRoster — refills the raid slots from the group list: a
// returning member's previous record is copied into slot + 0x10 (so the
// status byte lands at OFF_RAID_SLOT_FLAGS) and its online bit is set or
// cleared from `online[i]`; signals RAID_ROSTER_UPDATE.
// `void __fastcall(const uint64_t *guids, const uint32_t *flags,
// const uint8_t *online, uint32_t count)`, RET 8 (0x004BACC7). One E8
// call, in the group list.
FUN_REBUILD_RAID_ROSTER = 0x004BA5F0,
CALL_GROUP_LIST_REBUILD_RAID_ROSTER = 0x005E6EB4,
// UpdateRaidMemberRecord — a player object's raid slot as it enters or
// leaves view: with `mode` 0 it stores a live record (FUN_005F0880) in
// the player's raid slot and sends 0x27F; with 1 it does nothing.
// `void __fastcall(void *player, uint32_t mode, uint32_t withPet)`, RET 4
// (0x004BAE8B). Its four E8 callers are exactly a CGPlayer_C entering
// and leaving the object table: PostInit and PostUpdate (reactivation)
// with mode 1, the release slot (0x005E9AA0) with 0, and ~CGPlayer_C
// with 0 only for an object never released (CGObject_C::IsReleased
// 0x00614EA0). Each pairs with UpdatePartyMemberRecord (0x004E85D0) one
// call earlier. Targets checked against the exe; no loaded DLL
// references the function or a site.
FUN_UPDATE_RAID_MEMBER_RECORD = 0x004BADC0,
CALL_PLAYER_POST_INIT_RAID_RECORD = 0x005DEC43,
CALL_PLAYER_POST_UPDATE_RAID_RECORD = 0x005DECAF,
CALL_PLAYER_RELEASE_RAID_RECORD = 0x005E9ACF,
CALL_PLAYER_DTOR_RAID_RECORD = 0x005DD6AB,
// Bit 1: flagged for PvP — Script_UnitIsPVP's test (0x00516502) for a
// token with no unit object.
GROUP_MEMBER_STATUS_PVP = 0x2,
@@ -10161,13 +10325,18 @@ enum Offsets {
FUN_RAISE_BAG_EVENTS = 0x004F91A0,
FUN_RAISE_BAG_UPDATE = 0x004F9370,
FUN_ON_BACKPACK_SLOT_CHANGED = 0x004F8DB0,
// World-subsystem per-frame update — runs exactly once per frame
// as part of the main world tick (`FUN_00482EA0`). Only one caller
// in the entire binary; deep in the rendering/world pipeline,
// nowhere near the chat/cast/transmog code regions that other
// DLLs (SuperWoWhook / nampower / transmogfix) hook. We hook the
// tail of this function to drain `BAG_UPDATE_DELAYED`'s pending
// flag — exact per-frame coalescing matching modern WoW.
// CWorld::BeginFrame, the world-subsystem per-frame update — runs
// exactly once per frame from CGWorldFrame::OnRender
// (`FUN_00482EA0`), its only caller, nowhere near the
// chat/cast/transmog code regions that other DLLs (SuperWoWhook /
// nampower / transmogfix) hook. OnRender is run by WorldFrame's
// layer-0 draw callback (`0x00482D70`), so this executes INSIDE the
// UI strata walk: the WORLD stratum's batches are already rebuilt and
// its levels above 0 (the nameplates) are not drawn yet. Lua run here
// that changes a font string in that stratum leaves a batch drawing a
// destroyed font (fault in the atlas-page upload under 0x005C8FE0).
// `Tick::WorldTick` only notes the tick here and runs its subscribers
// after the draw.
//
// Calling convention: `__fastcall(int ecx, int edx, int stackArg)`,
// `RET 0x4` (callee cleans the one stack arg).
@@ -10807,13 +10976,71 @@ enum Offsets {
// window is open. `FUN_004FC870` zeroes it on close (and fires
// event `0x13B`), so `> 0` reliably means "trade window open".
VAR_CURRENT_TRADESKILL_LINE = 0x00BDE040,
// Craft window (Enchanting, pet training) open-state flag — nonzero
// (3 for the enchanting-style craft) while open; `FUN_004F5FE0`
// zeroes it on close (fires event `0x169`). The Craft window has no
// skill-line-ID global — `TradeSkill::Link` derives the skill line
// from the first craft recipe — so this is the "craft open" gate.
// Craft window (Enchanting, pet training): the open craft type — the
// opening spell's EffectMiscValue[0], 3 for Enchanting, 1 for Beast
// Training — or 0 when closed. `FUN_004F5FE0` zeroes it on close (fires
// event `0x169`). The Craft window keeps no skill line.
VAR_CRAFT_WINDOW_STATE = 0x00BDCFB8,
// The profession windows' open, build and shutdown seams that
// `TradeSkill::Window` uses to show another player's profession from a
// `trade:` link, as 3.3.5's CGTradeSkillInfo does. Disassembly.
//
// `void __fastcall(uint32_t skillLine)`: opens the TradeSkill window on a
// skill line, or closes it when that line is already open. The only
// caller is TryCastSpell at CALL_TRY_CAST_TOGGLE_TRADE_SKILL, with the
// cast spell ID in ESI.
FUN_TOGGLE_TRADE_SKILL = 0x004FC910,
CALL_TRY_CAST_TOGGLE_TRADE_SKILL = 0x006E4C22,
// `void __fastcall(uint32_t craftType)`: the Craft window's twin. The
// only caller is TryCastSpell at CALL_TRY_CAST_TOGGLE_CRAFT, ESI = spell.
FUN_TOGGLE_CRAFT = 0x004F6000,
CALL_TRY_CAST_TOGGLE_CRAFT = 0x006E4C34,
// `void(void)`: clears the TradeSkill repeat latch and fires
// UPDATE_TRADESKILL_RECAST (3.3.5 CGTradeSkillInfo::StopRecast).
FUN_TRADE_SKILL_STOP_RECAST = 0x004FD790,
// The TradeSkill list build (BuildTradeSkillList, 0x004FCA20) reads per
// row: the open line's spell array from
// `CGPlayer_C::FindSkillLineSpells(player, skillLine)` (`__thiscall`,
// returns a growable array — count +0x04, int spell IDs at +0x08 — or
// null), each spell's ability row from FUN_SKILL_LINE_ABILITY_LOOKUP
// with the player's race and class, and the player's raw rank in the
// ability's line from the 33 bytes at SITE_BUILD_TRADE_SKILL_RANK
// (`call FindSkillIndex` .. `xor eax,eax`; ECX = player, the line
// pushed, rank left in EAX at 0x004FCC49). One call site each.
FUN_PLAYER_FIND_SKILL_LINE_SPELLS = 0x005EA2C0,
CALL_BUILD_TRADE_SKILL_SPELLS = 0x004FCB2A,
CALL_BUILD_TRADE_SKILL_ABILITY = 0x004FCBD2,
SITE_BUILD_TRADE_SKILL_RANK = 0x004FCC28,
// The Craft list build (BuildCraftList, 0x004F60C0) reads per row: the
// type's spell array from `CGPlayer_C::GetTabSpells(player, tab)`
// (`__thiscall`, the array at player + 0x1CD0 + tab * 16, same shape),
// each spell's ability row from `CGUnit_C::FindSkillLineAbility(unit,
// spell)` (`__thiscall`, the unit's race and class), and the rank from
// `CGPlayer_C::GetSkillTotal(player, skillLine)` (`__thiscall`, base plus
// temporary bonus). GetTabSpells' other caller is
// Script_GetCraftDisplaySkillLine (0x004F6CF4).
FUN_PLAYER_GET_TAB_SPELLS = 0x005EA330,
CALL_BUILD_CRAFT_SPELLS = 0x004F6118,
FUN_UNIT_FIND_SKILL_LINE_ABILITY = 0x0060C7C0,
CALL_BUILD_CRAFT_ABILITY = 0x004F61A8,
FUN_PLAYER_GET_SKILL_TOTAL = 0x005EA520,
CALL_BUILD_CRAFT_RANK = 0x004F620E,
// `void(void)` each: free the TradeSkill / Craft rows and zero their
// state. CGGameUI::ShutdownGame (0x00491180) calls them once per return
// to the character screen, as 3.3.5's calls CGTradeSkillInfo::ShutdownGame.
FUN_SHUTDOWN_TRADE_SKILL = 0x004FC790,
CALL_GAME_SHUTDOWN_TRADE_SKILL = 0x004911C8,
FUN_SHUTDOWN_CRAFT = 0x004F5F00,
CALL_GAME_SHUTDOWN_CRAFT = 0x004911D2,
// The window getters that answer for the player: GetTradeSkillLine and
// GetCraftDisplaySkillLine push (name, rank, maxRank) from the player's
// skill slot; GetCraftName pushes the name of the spell the player keeps
// for the open type. `int __fastcall(lua_State *)` each.
FUN_SCRIPT_GET_TRADE_SKILL_LINE = 0x004FDD40,
FUN_SCRIPT_GET_CRAFT_DISPLAY_SKILL_LINE = 0x004F6CB0,
FUN_SCRIPT_GET_CRAFT_NAME = 0x004F6C10,
// Trainer window (ClassTrainerFrame.cpp). Same slot-indirected shape as
// the trade-skill rows: `[VAR_TRAINER_SERVICES]` points at an array of
// 0x38-byte service records, the record's `+0x00` is the spell the
@@ -10888,6 +11115,14 @@ enum Offsets {
// Verified from Spell.dbc: 13262 "Disenchant" has Effect[0] = 99 (the
// only row carrying it). Effect 32 is TRIGGER_MISSILE, not disenchant.
SPELL_EFFECT_DISENCHANT = 99,
// A profession spell (Blacksmithing 2018, Enchanting 7411, Smelting
// 2656): casting it opens a profession window. TryCastSpell (0x006E4B60)
// tests Effect[0] for it (`cmp [edi+0xF4], 0x2F` at 0x006E4BCE) and
// routes on EffectMiscValue[0]: 0 opens the TradeSkill window for the
// spell's skill line, any other value is the Craft window's type (3
// Enchanting, 1 Beast Training). 3.3.5's GetTradeSkillListLink takes
// only spells whose Effect[0] is this.
SPELL_EFFECT_TRADE_SKILL = 47,
// EffectBasePoints[3] — the base magnitude of each effect, stored as
// (value - 1) for the fixed-die spells that back item stats (the
@@ -10954,6 +11189,15 @@ enum Offsets {
// SetHyperlink's `enchant:` on this bit, and its UnitCastingInfo reports
// it as isTradeskill.
SPELL_ATTR_TRADESPELL = 0x20,
// Column 3, castUI: the tab a learned spell without SPELL_ATTR_TRADESPELL
// is filed under. The learn function (0x005E9C20) puts a TRADESPELL
// spell in its skill line's list (CGPlayer_C::FindSkillLineSpells) and
// any other spell with castUI > 0 in tab array castUI
// (CGPlayer_C::GetTabSpells, `mov esi,[edi+0xC]` at 0x005E9D83), which
// the Craft window of that type lists. Extracted Spell.dbc: 187 of
// Enchanting's 192 SkillLineAbility spells hold 3, 138 of Beast
// Training's 140 hold 1.
OFF_SPELL_RECORD_CAST_UI = 0x0C,
// Attributes bits 2 (0x4) and 10 (0x400) — "on next melee swing" (the
// ability replaces the next white hit instead of sending its own attack,
// e.g. Heroic Strike / Maul / Raptor Strike). The CLIENT itself tests
@@ -12736,6 +12980,18 @@ enum Offsets {
CALL_TEXT_FIT_HEIGHT_STEP = 0x005C22B1,
CALL_TEXT_ENUMERATE_LINES_STEP = 0x005C24F9,
DRAW_BUILDER_FRAME_LINE_X = 0x48,
// The fit-to-height count's line test (0x005C21C0, after its stepper
// call): `fld [ebp+10h]` (the block height), then this `fadd dword ptr
// [VAR_TEXT_FIT_HEIGHT_EPSILON]` (D8 05 BC 26 80 00), then FCOMPP against
// the running height + font height; it stops at the first line that
// fails. [ebp-8] is the bytes fitted so far, 0 until a line fits. EAX is
// dead here (FNSTSW AX, then both paths reload it from [ebp-8]).
// Text::FitHeight replaces the fadd.
SITE_TEXT_FIT_HEIGHT_TOLERANCE = 0x005C22CB,
TEXT_FIT_HEIGHT_FRAME_FITTED_BYTES = 0x8,
// Exactly 2^-20 (0x35800000, .rdata): the fit's line margin above. A
// shared float, also read by collision, movement and matrix code.
VAR_TEXT_FIT_HEIGHT_EPSILON = 0x008026BC,
// The draw builder's right / centre x: `FUN_TEXT_SCREEN_TO_PIXEL_WIDTH(raw,
// -width)` (or -width / 2), `float __fastcall(int raw, float)`, `ret 4`,
// result in ST0, stored to the line x at 0x005CDE28.
@@ -12853,6 +13109,15 @@ enum Offsets {
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT = 0x33C,
OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES = 0x340,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS = 0x344, // the newest record; -1 when empty
// The record the lines are laid out from (4.3.4's m_currentScroll);
// FUN_SMF_DISPLAY_REFRESH stores its argument here.
OFF_SCROLLINGMESSAGEFRAME_SCROLL_RECORD = 0x348,
// A ring that is not full holds records 0..count-1: the writer fills
// them in order from an empty ring (Clear: position -1), and only a full
// ring wraps. ScrollOffset and ScrollToOffset (below) wrap by the count,
// not the limit, and ScrollToTop (0x00788690) lays out from record 0
// unless the ring is full, so each is right only for that layout. A
// ClassicAPI writer must leave the ring in it.
OFF_SCROLLINGMESSAGEFRAME_MESSAGES = 0x394, // the record array
SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE = 0x18,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT = 0x0,
@@ -12888,6 +13153,19 @@ enum Offsets {
// 0 when empty; the GetCurrentScroll method pushes it.
FUN_SCROLLINGMESSAGEFRAME_SCROLL_TO_OFFSET = 0x00788700,
FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET = 0x00788AC0,
// ScrollUp / ScrollDown (`int __thiscall(smf)`, plain `ret`; 4.3.4's
// 0x0051E620 / 0x0051E660 step for step): one record older / newer and a
// layout, answering 1. Each answers 0 without moving when empty or at its
// end, and re-arms the shown lines there: ScrollDown at
// OFF_SCROLLINGMESSAGEFRAME_AT_BOTTOM, ScrollUp at +0x350 (4.3.4's
// m_atTop), which the layout sets when every message from the scroll
// record back to the oldest fit the height (0x00788A37: laid-out count ==
// count remaining). The layout runs only on a shown frame ([smf+0xD4],
// m_visible), so a hidden frame's +0x350 is stale and its ScrollUp wraps
// around the ring. PageUp (0x007885B0) is a loop of ScrollUp that ends on
// the first 0.
FUN_SCROLLINGMESSAGEFRAME_SCROLL_UP = 0x00788610,
FUN_SCROLLINGMESSAGEFRAME_SCROLL_DOWN = 0x00788650,
// Clear (`void __thiscall(smf)`, 4.3.4's Clear): count 0, position 0, a
// layout from 0, +0x350 = 0, position -1. The Clear method's body.
FUN_SCROLLINGMESSAGEFRAME_CLEAR = 0x007882B0,
+13 -1
View File
@@ -162,8 +162,20 @@ uint32_t VertexColor(void *region) {
return bgra;
}
// An unchanged alpha writes nothing. The setter already does nothing when the
// one entry it would store is the region's colour, but the getter reads a
// gradient back as white, and the setter shrinks a gradient's four entries to
// that one (benilla-re ui/texture, "Setting a vertex colour"). So restoring
// the stored alpha (PreOnAnimUpdate, AnimDeactivated) on a gradient under an
// ancestor's alpha group flattened it to white. 3.3.5's and 4.3.4's
// SetVertexColor drop the gradient there too; here only a region that an
// animation itself fades loses it. The ancestor's fade still reaches the
// gradient: a frame's alpha change refolds every region's entries (0x77FAC0).
void SetVertexAlpha(void *region, uint8_t alpha) {
const uint32_t bgra = (VertexColor(region) & 0x00FFFFFF) | (static_cast<uint32_t>(alpha) << 24);
const uint32_t color = VertexColor(region);
if ((color >> 24) == alpha)
return;
const uint32_t bgra = (color & 0x00FFFFFF) | (static_cast<uint32_t>(alpha) << 24);
reinterpret_cast<SetColor_t>(Offsets::FUN_FONTSTRING_SET_COLOR)(region, nullptr, &bgra);
}
+56 -9
View File
@@ -36,8 +36,8 @@
// listed by the coverage function
// - a frame method -> Blizzard's widget system for its registry
// ("SimpleTextureAPI", Type "ScriptObject")
// - an enum / structure /
// event -> the System named in its declaration
// - an enum / constants /
// structure / event -> the System named in its declaration
// A system's `Environment` is "All" when its functions were recorded in both
// Lua states, else "Game" or "Glue"; a function whose own environment is
// narrower than its system's gets a Documentation line saying so.
@@ -79,13 +79,20 @@ enum Kind : uint8_t {
KIND_TABLE, // RegisterTableFunction
KIND_FRAME_METHOD, // RegisterFrameMethods, one per entry
KIND_ENUM, // RegisterIntegerEnum
KIND_CONSTANTS, // RegisterConstants
KIND_ORPHAN_METHOD_DOC // a Doc::Method naming no entry in its table
};
// The value-table kinds, which share the enum* fields of Entry.
bool IsValueTable(Kind kind) {
return kind == KIND_ENUM || kind == KIND_CONSTANTS;
}
struct Entry {
Kind kind;
unsigned env; // Env bits the registration was recorded in
const char *table; // KIND_TABLE: the namespace; KIND_ENUM: the parent ("Enum")
const char *table; // KIND_TABLE: the namespace; KIND_ENUM: the parent ("Enum");
// KIND_CONSTANTS: "Constants"
const char *name; // registered name / method name / enum sub-name
const void *registry; // frame methods only
const Function *doc; // nullptr = undocumented
@@ -195,6 +202,7 @@ const char *SystemOf(const Entry &e) {
case KIND_FRAME_METHOD:
return WidgetSystemFor(e.registry);
case KIND_ENUM:
case KIND_CONSTANTS:
return e.enumSystem;
default:
return nullptr;
@@ -240,6 +248,7 @@ std::vector<SystemInfo> CollectSystems() {
AddSystem(systems, SystemOf(e), nullptr, "ScriptObject", e.env);
break;
case KIND_ENUM:
case KIND_CONSTANTS:
AddSystem(systems, e.enumSystem, nullptr, nullptr, 0);
break;
default:
@@ -394,6 +403,25 @@ void PushEnumeration(void *L, const Entry &e) {
Lua::SetTable(L, -3);
}
// Pushes { Name, Type = "Constants",
// Values = { { Name, Type = "number", Value } } }.
void PushConstants(void *L, const Entry &e) {
Lua::NewTable(L);
Lua::SetFieldString(L, "Name", e.name);
Lua::SetFieldString(L, "Type", "Constants");
Lua::PushString(L, "Values");
Lua::NewTable(L);
for (int i = 0; i < e.enumCount; ++i) {
Lua::PushNumber(L, static_cast<double>(i + 1));
Lua::NewTable(L);
Lua::SetFieldString(L, "Name", e.enumEntries[i].key);
Lua::SetFieldString(L, "Type", "number");
Lua::SetFieldNumber(L, "Value", e.enumEntries[i].value);
Lua::SetTable(L, -3);
}
Lua::SetTable(L, -3);
}
// Pushes { Name, Type = "Structure" [, Documentation], Fields = { … } }.
void PushStructure(void *L, const Structure &s) {
Lua::NewTable(L);
@@ -479,10 +507,13 @@ void PushSystem(void *L, const SystemInfo &sys) {
Lua::SetTable(L, -3);
}
for (const Entry &e : g_entries) {
if (e.kind != KIND_ENUM || !SameTextNoCase(e.enumSystem, sys.name))
if (!IsValueTable(e.kind) || !SameTextNoCase(e.enumSystem, sys.name))
continue;
Lua::PushNumber(L, static_cast<double>(++n));
PushEnumeration(L, e);
if (e.kind == KIND_ENUM)
PushEnumeration(L, e);
else
PushConstants(L, e);
Lua::SetTable(L, -3);
}
Lua::SetTable(L, -3);
@@ -527,6 +558,7 @@ std::vector<std::string> Undocumented() {
break;
}
case KIND_ENUM:
case KIND_CONSTANTS:
if (e.enumSystem == nullptr)
out.push_back(std::string(e.table) + "." + e.name);
break;
@@ -662,18 +694,21 @@ void NameWidgetSystem(const void *registry, const char *system) {
g_ownWidgetSystems.push_back({reinterpret_cast<uintptr_t>(registry), system});
}
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
int count, const char *system) {
namespace {
void RecordValueTable(Kind kind, const char *parent, const char *sub,
const Game::Lua::EnumIntegerEntry *entries, int count,
const char *system) {
if (g_pass == ENV_NONE || sub == nullptr)
return;
if (Entry *e = Find(KIND_ENUM, parent, sub, nullptr)) {
if (Entry *e = Find(kind, parent, sub, nullptr)) {
e->env |= g_pass;
if (e->enumSystem == nullptr)
e->enumSystem = system;
return;
}
Entry e{};
e.kind = KIND_ENUM;
e.kind = kind;
e.env = g_pass;
e.table = parent;
e.name = sub;
@@ -683,6 +718,18 @@ void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntege
g_entries.push_back(e);
}
} // namespace
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
int count, const char *system) {
RecordValueTable(KIND_ENUM, parent, sub, entries, count, system);
}
void RecordConstants(const char *sub, const Game::Lua::EnumIntegerEntry *entries, int count,
const char *system) {
RecordValueTable(KIND_CONSTANTS, "Constants", sub, entries, count, system);
}
} // namespace Api::Documentation
// The Structure constructor lives here so the list head is private to this
+2
View File
@@ -39,5 +39,7 @@ void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry
void NameWidgetSystem(const void *registry, const char *system);
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
int count, const char *system);
void RecordConstants(const char *sub, const Game::Lua::EnumIntegerEntry *entries, int count,
const char *system);
} // namespace Api::Documentation
+14 -190
View File
@@ -702,216 +702,40 @@ const Game::Doc::Function kCancelUnitBuff{
"Cancels one of your own auras by index.", kCancelUnitBuffArgs, {},
"SpellGlobals"};
const Game::Doc::Field kAuraDataFields[] = {
Game::Doc::Req("name", "string"),
Game::Doc::Req("icon", "string", "The icon's texture path."),
Game::Doc::Req("applications", "number", "The stack count."),
Game::Doc::Req("spellId", "number"),
Game::Doc::Req("dispelName", "string",
"Magic, Curse, Disease, or Poison; empty when the aura has no dispel type."),
Game::Doc::Req("isHelpful", "bool"),
Game::Doc::Req("isHarmful", "bool"),
Game::Doc::Req("duration", "number", "In seconds; 0 when the duration is not known."),
Game::Doc::Req("expirationTime", "number",
"When the aura ends, in GetTime() seconds; 0 when not known."),
Game::Doc::Opt("sourceUnit", "UnitToken", nullptr,
"Nil when the cast was not seen or the caster has no unit token."),
Game::Doc::Opt("sourceGUID", "WOWGUID", nullptr,
"The caster's GUID, a ClassicAPI extension; nil when the cast was not seen."),
Game::Doc::Opt("auraInstanceID", "number", nullptr,
"Nil for an aura that is not in the unit's aura list."),
Game::Doc::Req("charges", "number", "Always 0."),
Game::Doc::Req("maxCharges", "number", "Always 0."),
Game::Doc::Req("timeMod", "number", "Always 1."),
Game::Doc::Req("isFromPlayerOrPlayerPet", "bool",
"True when any player or a player's pet applied the aura."),
Game::Doc::Req("canApplyAura", "bool",
"True when the player or the pet knows the spell that applies the aura."),
Game::Doc::Req("isRaid", "bool", "True when the aura passes the RAID filter."),
Game::Doc::Req("isStealable", "bool",
"True when the player can remove this buff from an enemy."),
Game::Doc::Req("isBossAura", "bool", "Always false."),
Game::Doc::Req("isNameplateOnly", "bool", "Always false."),
Game::Doc::Req("nameplateShowAll", "bool", "Always false."),
Game::Doc::Req("nameplateShowPersonal", "bool", "Always false."),
Game::Doc::Req("shouldConsolidate", "bool", "Always false."),
};
const Game::Doc::Structure kAuraData{"AuraData", "UnitAuras", kAuraDataFields,
"One aura on a unit."};
// The Classic Era `UnitAura` tuple, shared by the four positional getters.
// `Data::PushPositional` is the source of the order.
const Game::Doc::Field kPositionalRets[] = {
Game::Doc::Opt("name", "string", nullptr,
"Nil, as the only return, when there is no such aura."),
Game::Doc::Opt("icon", "string"),
Game::Doc::Opt("count", "number"),
Game::Doc::Opt("dispelType", "string", nullptr,
"Empty when the aura has no dispel type."),
Game::Doc::Opt("duration", "number"),
Game::Doc::Opt("expirationTime", "number"),
Game::Doc::Opt("source", "UnitToken", nullptr, "Nil when the caster is not known."),
Game::Doc::Opt("isStealable", "bool"),
Game::Doc::Opt("nameplateShowPersonal", "bool", nullptr, "Always false."),
Game::Doc::Opt("spellId", "number"),
Game::Doc::Opt("canApplyAura", "bool"),
Game::Doc::Opt("isBossDebuff", "bool", nullptr, "Always false."),
Game::Doc::Opt("castByPlayer", "bool", nullptr,
"True when a player applied the aura; false for a pet."),
Game::Doc::Opt("nameplateShowAll", "bool", nullptr, "Always false."),
Game::Doc::Opt("timeMod", "number", nullptr, "Always 1."),
};
const Game::Doc::Field kAuraRets[] = {
Game::Doc::Opt("aura", "AuraData", nullptr, "Nil when there is no such aura."),
};
const Game::Doc::Field kByIndexArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("index", "luaIndex"),
Game::Doc::Opt("filter", "AuraFilters", nullptr,
"Tokens such as HELPFUL, HARMFUL, PLAYER, RAID; HELPFUL when no range is given."),
};
const Game::Doc::Function kGetAuraDataByIndex{
"Returns the aura at an index among a unit's auras that match a filter.", kByIndexArgs,
kAuraRets};
const Game::Doc::Function kUnitAura{
"Returns the aura at an index as positional values, without making a table.",
kByIndexArgs, kPositionalRets};
const Game::Doc::Field kLockedIndexArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("index", "luaIndex"),
};
const Game::Doc::Function kGetBuffDataByIndex{
"Returns the buff at an index on a unit.", kLockedIndexArgs, kAuraRets};
const Game::Doc::Function kGetDebuffDataByIndex{
"Returns the debuff at an index on a unit.", kLockedIndexArgs, kAuraRets};
const Game::Doc::Field kLockedFilterArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("index", "luaIndex"),
Game::Doc::Opt("filter", "AuraFilters", nullptr,
"Further tokens such as PLAYER or RAID; the range is fixed."),
};
const Game::Doc::Function kUnitBuff{
"Returns the buff at an index as positional values, without making a table.",
kLockedFilterArgs, kPositionalRets};
const Game::Doc::Function kUnitDebuff{
"Returns the debuff at an index as positional values, without making a table.",
kLockedFilterArgs, kPositionalRets};
const Game::Doc::Field kSlotsArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Opt("filter", "AuraFilters"),
Game::Doc::Opt("maxSlots", "number", nullptr, "Nil or 0 returns all of them."),
Game::Doc::Opt("continuationToken", "number", nullptr,
"The first return of the previous call, to get the next batch."),
Game::Doc::Opt("buf", "table", nullptr,
"A table to fill with the slot IDs instead; the call then returns the "
"token and the count. A ClassicAPI extension."),
};
const Game::Doc::Field kSlotsRets[] = {
Game::Doc::Opt("outContinuationToken", "number", nullptr,
"Nil when this batch reaches the last aura."),
Game::Doc::Vararg("number", "The slot IDs, valid only in the frame they are returned."),
};
const Game::Doc::Function kGetAuraSlots{
"Returns the slot IDs of a unit's auras that match a filter, in batches.", kSlotsArgs,
kSlotsRets};
const Game::Doc::Field kBySlotArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("slot", "number", "A slot ID from C_UnitAuras.GetAuraSlots."),
};
const Game::Doc::Function kGetAuraDataBySlot{
"Returns the aura that a slot ID names.", kBySlotArgs, kAuraRets};
const Game::Doc::Function kUnitAuraBySlot{
"Returns the aura that a slot ID names as positional values, without making a table.",
kBySlotArgs, kPositionalRets};
const Game::Doc::Field kBySpellIDArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("spellID", "number"),
Game::Doc::Opt("filter", "AuraFilters", nullptr, "Nil searches buffs, then debuffs."),
};
const Game::Doc::Function kGetUnitAuraBySpellID{
"Returns the first aura on a unit with a spell ID; the player's own aura comes first.",
kBySpellIDArgs, kAuraRets};
const Game::Doc::Field kPlayerBySpellIDArgs[] = {
Game::Doc::Req("spellID", "number"),
};
const Game::Doc::Function kGetPlayerAuraBySpellID{
"Returns the first aura on the player with a spell ID.", kPlayerBySpellIDArgs, kAuraRets};
const Game::Doc::Field kBySpellNameArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("spellName", "cstring", "The localized name; case does not matter."),
Game::Doc::Opt("filter", "AuraFilters", nullptr, "Nil searches buffs, then debuffs."),
};
const Game::Doc::Function kGetAuraDataBySpellName{
"Returns the first aura on a unit with a spell name; the player's own aura comes first.",
kBySpellNameArgs, kAuraRets};
const Game::Doc::Field kUnitAurasArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Opt("filter", "AuraFilters", nullptr,
"With no HELPFUL or HARMFUL token, both are returned."),
};
const Game::Doc::Field kUnitAurasRets[] = {
Game::Doc::Req("auras", "table", "An array of AuraData; empty when the unit has none."),
};
const Game::Doc::Function kGetUnitAuras{
"Returns all of a unit's auras that match a filter, buffs first.", kUnitAurasArgs,
kUnitAurasRets};
const Game::Doc::Field kDispelColorArgs[] = {
Game::Doc::Opt("dispelName", "string", nullptr,
"Magic, Curse, Disease, Poison, or Enrage; any other value gives the "
"no-type color."),
};
const Game::Doc::Field kDispelColorRets[] = {
Game::Doc::Req("color", "colorRGBA"),
};
const Game::Doc::Function kGetAuraDispelTypeColor{
"Returns the border color for an aura's dispel type.", kDispelColorArgs,
kDispelColorRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataByIndex",
&Script_GetAuraDataByIndex, &kGetAuraDataByIndex);
&Script_GetAuraDataByIndex);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetBuffDataByIndex",
&Script_GetBuffDataByIndex, &kGetBuffDataByIndex);
&Script_GetBuffDataByIndex);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetDebuffDataByIndex",
&Script_GetDebuffDataByIndex, &kGetDebuffDataByIndex);
&Script_GetDebuffDataByIndex);
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAura",
&Script_UnitAura, &kUnitAura);
&Script_UnitAura);
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitBuff",
&Script_UnitBuff, &kUnitBuff);
&Script_UnitBuff);
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitDebuff",
&Script_UnitDebuff, &kUnitDebuff);
&Script_UnitDebuff);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraSlots",
&Script_GetAuraSlots, &kGetAuraSlots);
&Script_GetAuraSlots);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySlot",
&Script_GetAuraDataBySlot, &kGetAuraDataBySlot);
&Script_GetAuraDataBySlot);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataByAuraInstanceID",
&Script_GetAuraDataByAuraInstanceID,
&kGetAuraDataByAuraInstanceID);
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAuraBySlot",
&Script_UnitAuraBySlot, &kUnitAuraBySlot);
&Script_UnitAuraBySlot);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuraBySpellID",
&Script_GetUnitAuraBySpellID, &kGetUnitAuraBySpellID);
&Script_GetUnitAuraBySpellID);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetPlayerAuraBySpellID",
&Script_GetPlayerAuraBySpellID, &kGetPlayerAuraBySpellID);
&Script_GetPlayerAuraBySpellID);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySpellName",
&Script_GetAuraDataBySpellName, &kGetAuraDataBySpellName);
&Script_GetAuraDataBySpellName);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuras",
&Script_GetUnitAuras, &kGetUnitAuras);
&Script_GetUnitAuras);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDispelTypeColor",
&Script_GetAuraDispelTypeColor, &kGetAuraDispelTypeColor);
&Script_GetAuraDispelTypeColor);
Game::Lua::RegisterGlobalFunction("CancelUnitBuff", &Script_CancelUnitBuff,
&kCancelUnitBuff);
}
+1 -15
View File
@@ -193,23 +193,9 @@ int __fastcall Script_RegisterComboDuration(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("spellID", "number"),
Game::Doc::Req("baseSeconds", "number",
"The duration at zero combo points; each point adds a fifth of the "
"difference to maxSeconds."),
Game::Doc::Req("maxSeconds", "number", "The duration at five combo points."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("success", "bool"),
};
const Game::Doc::Function kRegisterComboDuration{
"Sets the combo point durations of a finisher whose client data is missing or wrong.",
kArgs, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_UnitAuras", "RegisterComboDuration",
&Script_RegisterComboDuration, &kRegisterComboDuration);
&Script_RegisterComboDuration);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -22
View File
@@ -946,31 +946,10 @@ int __fastcall Script_RegisterAuraDurationModifierByTrigger(void *L) {
return 1;
}
const Game::Doc::Field kDurationModArgs[] = {
Game::Doc::Req("triggerFamily", "number", "The SpellFamilyName of the triggering cast."),
Game::Doc::Req("triggerSchool", "number",
"The school index the trigger must have, or below 0 for any school."),
Game::Doc::Req("affectedFamily", "number", "The SpellFamilyName of the aura to change."),
Game::Doc::Req("affectedFamilyFlags", "number",
"A SpellFamilyFlags mask the aura must overlap, or 0 to match on the icon."),
Game::Doc::Req("affectedIcon", "number",
"The SpellIconID the aura must have, or 0 for any icon."),
Game::Doc::Req("op", "string", "refresh, reduce, set, or remove."),
Game::Doc::Opt("valueSeconds", "number", nullptr, "The amount for reduce and set."),
};
const Game::Doc::Field kDurationModRets[] = {
Game::Doc::Req("success", "bool"),
};
const Game::Doc::Function kRegisterAuraDurationModifierByTrigger{
"Changes the shown duration of the caster's aura on a unit when a matching cast by "
"that caster lands.",
kDurationModArgs, kDurationModRets, nullptr, true};
void RegisterDurationModLua() {
Game::Lua::RegisterTableFunction("C_UnitAuras",
"RegisterAuraDurationModifierByTrigger",
&Script_RegisterAuraDurationModifierByTrigger,
&kRegisterAuraDurationModifierByTrigger);
&Script_RegisterAuraDurationModifierByTrigger);
}
const Game::ModuleAutoRegister _autoregDurationMod{&RegisterDurationModLua};
+3 -2
View File
@@ -19,8 +19,9 @@
// the `BAG_UPDATE_DELAYED` event. Subscribe here to run C++ work exactly once
// per frame in which any bag actually changed, instead of polling every frame.
//
// Callbacks fire at the tail of the world tick, after every BAG_UPDATE for the
// frame has already fired (so bag contents are settled). No firing on frames
// Callbacks fire from the world tick's subscription, after the frame's UI draw
// and after every BAG_UPDATE for the frame has already fired (so bag contents
// are settled). No firing on frames
// where nothing changed. Order across subscribers is unspecified.
//
// Module pattern, matching `Tick::WorldTick::AutoSubscribe`: declare a
+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);
}
+17 -53
View File
@@ -25,7 +25,8 @@
// - a cast-time spell is cast -> SMSG_SPELL_GO, InterruptFlags & AUTOATTACK
// - autorepeat (Auto Shot / Shoot) fires -> SMSG_SPELL_GO, AttributesEx2 & AUTOREPEAT
// - starting a fresh melee attack -> SMSG_ATTACKSTART (off-hand only)
// - a weapon is swapped mid-combat -> no packet; polled off the invMgr GUIDs
// - a weapon is swapped mid-combat -> no packet; the engine's inventory-slot
// observer (via Player::Equipment)
// - the LOCAL PLAYER parries an attack -> SMSG_ATTACKERSTATEUPDATE, victim == us,
// targetState == PARRY (no separate packet —
// the server just clips OUR remaining time)
@@ -431,64 +432,29 @@ void OnPacket(uint32_t opcode, CDataStore *packet) {
const Net::PacketDispatch::AutoSubscribe _sub{&OnPacket};
// ---- Weapon swap mid-combat (polled -- no packet, no observer needed) ---
} // namespace
// ---- Weapon swap mid-combat (inventory-slot observer) -------------------
//
// A weapon equipped while `IsInCombat()` resets that hand's timer
// server-side (`Player::_ApplyWeaponDependentAuraMods` -> `SetAttackTime`
// with `resetTimer = IsInCombat()`). There's no dedicated packet for this;
// `Player::Equipment` already sees the same invMgr GUIDs change via the
// engine's inventory-slot observer for `PLAYER_EQUIPMENT_CHANGED`, but
// that's a one-way Lua event fire with nothing to subscribe to in C++. Only
// 3 slots matter here, so a plain 3-GUID compare on the existing WorldTick
// pass is simpler than a second hook on the same observer.
uint64_t g_lastMainGuid = 0;
uint64_t g_lastOffGuid = 0;
uint64_t g_lastRangedGuid = 0;
bool g_weaponSnapValid = false;
void PollWeaponSwap() {
const uint8_t *player = Unit::Identity::PlayerObject();
if (player == nullptr) {
g_weaponSnapValid = false;
return;
}
const uint8_t *invMgr = player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
const uint32_t count =
*reinterpret_cast<const uint32_t *>(invMgr + Offsets::OFF_INVMGR_SLOT_COUNT);
if (count < 18) {
g_weaponSnapValid = false;
return;
}
const uint64_t *guids = *reinterpret_cast<const uint64_t *const *>(
invMgr + Offsets::OFF_INVMGR_GUID_ARRAY);
if (guids == nullptr) {
g_weaponSnapValid = false;
return;
}
// `Player::Equipment`'s co-hook on the engine's inventory-slot observer calls
// in here when a slot's GUID changes. That runs inside SMSG_UPDATE_OBJECT
// after the whole values block is written, so `AttackTimeMs` already reads
// the new weapon's speed when it arrives in the same block.
void OnWeaponSlotChanged(int slot0) {
// 0-based invMgr slots: 15 = main hand (Lua 16), 16 = off hand (17),
// 17 = ranged (18) -- same `slot0 = luaSlot - 1` convention documented
// in Player::Equipment.
const uint64_t mainGuid = guids[15];
const uint64_t offGuid = guids[16];
const uint64_t rangedGuid = guids[17];
if (g_weaponSnapValid && PlayerInCombat()) {
if (mainGuid != g_lastMainGuid)
MarkReset(MAIN_HAND, AttackTimeMs(MAIN_HAND));
if (offGuid != g_lastOffGuid)
MarkReset(OFF_HAND, AttackTimeMs(OFF_HAND));
if (rangedGuid != g_lastRangedGuid)
MarkReset(RANGED, AttackTimeMs(RANGED));
}
g_lastMainGuid = mainGuid;
g_lastOffGuid = offGuid;
g_lastRangedGuid = rangedGuid;
g_weaponSnapValid = true;
// in Player::Equipment, and in SwingType order.
if (slot0 < 15 || slot0 > 17 || !PlayerInCombat())
return;
const auto type = static_cast<SwingType>(slot0 - 15);
MarkReset(type, AttackTimeMs(type));
}
namespace {
// ---- PLAYER_SWING fire (WorldTick, at most once per type per frame) -----
const Game::Doc::Field kPayload[] = {
@@ -504,8 +470,6 @@ const Game::Doc::Event kPlayerSwingDoc{
const Event::Custom::AutoReserve _evt{"PLAYER_SWING", &kPlayerSwingDoc};
void OnWorldTick() {
PollWeaponSwap();
const int slot = _evt.Slot();
const uint32_t now = NowMs();
for (int t = 0; t < 3; ++t) {
+6
View File
@@ -32,6 +32,12 @@ enum SwingType : int { MAIN_HAND = 0, OFF_HAND = 1, RANGED = 2 };
// callers treat 0 as "this swing type doesn't apply right now".
uint32_t AttackTimeMs(SwingType type);
// Called by `Player::Equipment` from the engine's inventory-slot observer
// when the item GUID in 0-based equipment slot `slot0` changes. A weapon
// slot (15 / 16 / 17) changed in combat resets that hand's swing timer; any
// other slot is ignored.
void OnWeaponSlotChanged(int slot0);
// True if `player` and `target` (raw CGUnit* object pointers, e.g. from
// `Unit::Position::ResolveToken` or `Object::ByGuid`) are within pure 2D
// (X/Y) melee reach of each other -- the server's own auto-attack distance
+1 -15
View File
@@ -62,23 +62,9 @@ int __fastcall Script_C_Container_AutoStoreItem(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("srcBag", "BagIndex"),
Game::Doc::Req("srcSlot", "luaIndex"),
Game::Doc::Opt("dstBag", "BagIndex", "0",
"0 for anywhere in the bags, 1 to 4 for one bag, -1 for the bank."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("sent", "bool", "False when the server cannot be asked for this move."),
};
const Game::Doc::Function kAutoStoreItem{
"Moves an item and lets the server choose where it goes, merging it into "
"existing stacks first.",
kArgs, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "AutoStoreItem",
&Script_C_Container_AutoStoreItem, &kAutoStoreItem);
&Script_C_Container_AutoStoreItem);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+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
+1 -13
View File
@@ -43,21 +43,9 @@ int __fastcall Script_C_Container_GetContainerItemCharges(void *L) {
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("uses", "number", nullptr, "Nil when the slot is empty."),
};
const Game::Doc::Function kGetContainerItemCharges{
"Returns the uses left on the item in a bag slot: its charges times its stack size.",
kArgs, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemCharges",
&Script_C_Container_GetContainerItemCharges,
&kGetContainerItemCharges);
&Script_C_Container_GetContainerItemCharges);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -14
View File
@@ -33,22 +33,9 @@ int __fastcall Script_C_Container_GetItemCooldown(void *L) {
return 3;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("itemID", "number"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("startTime", "number",
"When the cooldown started, in GetTime() seconds; 0 when none."),
Game::Doc::Req("duration", "number", "Length in seconds; 0 when there is no cooldown."),
Game::Doc::Req("enable", "number",
"0 while the cooldown waits to start, as for a potion used in combat; else 1."),
};
const Game::Doc::Function kGetItemCooldown{
"Returns the cooldown of an item's use effect.", kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetItemCooldown",
&Script_C_Container_GetItemCooldown, &kGetItemCooldown);
&Script_C_Container_GetItemCooldown);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -14
View File
@@ -40,22 +40,9 @@ int __fastcall Script_C_Container_GetContainerItemDurability(void *L) {
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("durability", "number", nullptr,
"Nil when the slot is empty or the item has no durability."),
Game::Doc::Opt("maxDurability", "number"),
};
const Game::Doc::Function kGetContainerItemDurability{
"Returns the current and maximum durability of the item in a bag slot.", kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemDurability",
&Script_C_Container_GetContainerItemDurability,
&kGetContainerItemDurability);
&Script_C_Container_GetContainerItemDurability);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -16
View File
@@ -80,25 +80,10 @@ int __fastcall Script_C_Container_GetContainerItemEquipmentSetInfo(void *L) {
return 2;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("inSet", "bool"),
Game::Doc::Opt("setList", "string", nullptr,
"The set names joined by PLAYER_LIST_DELIMITER; nil when inSet is false."),
};
const Game::Doc::Function kGetContainerItemEquipmentSetInfo{
"Returns whether the item in a bag slot is saved in an equipment set, and the "
"names of those sets.",
kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction(
"C_Container", "GetContainerItemEquipmentSetInfo",
&Script_C_Container_GetContainerItemEquipmentSetInfo,
&kGetContainerItemEquipmentSetInfo);
&Script_C_Container_GetContainerItemEquipmentSetInfo);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+3 -33
View File
@@ -182,45 +182,15 @@ int __fastcall Script_C_Container_CalculateTotalNumberOfFreeBagSlots(void *L) {
return 1;
}
const Game::Doc::Field kBagArgs[] = {
Game::Doc::Req("bagIndex", "BagIndex", "0 for the backpack, 1 to 4 for the equipped bags."),
};
const Game::Doc::Field kNumFreeSlotsRets[] = {
Game::Doc::Req("numFreeSlots", "number", "0 when the bag does not exist."),
Game::Doc::Req("bagFamily", "number",
"The bag's family bitmask; 0 for a bag that takes any item."),
};
const Game::Doc::Function kGetContainerNumFreeSlots{
"Returns the number of empty slots in a bag and the kind of items the bag takes.",
kBagArgs, kNumFreeSlotsRets};
const Game::Doc::Field kFreeSlotsRets[] = {
Game::Doc::Opt("freeSlots", "table", nullptr,
"The empty slot numbers in ascending order; nil when the bag does not exist."),
};
const Game::Doc::Function kGetContainerFreeSlots{
"Returns the numbers of the empty slots in a bag.", kBagArgs, kFreeSlotsRets};
const Game::Doc::Field kTotalRets[] = {
Game::Doc::Req("totalFreeSlots", "number"),
};
const Game::Doc::Function kCalculateTotalNumberOfFreeBagSlots{
"Returns the number of empty slots in the backpack and the equipped bags that take "
"any item.",
{}, kTotalRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerNumFreeSlots",
&Script_C_Container_GetContainerNumFreeSlots,
&kGetContainerNumFreeSlots);
&Script_C_Container_GetContainerNumFreeSlots);
Game::Lua::RegisterTableFunction("C_Container", "GetContainerFreeSlots",
&Script_C_Container_GetContainerFreeSlots,
&kGetContainerFreeSlots);
&Script_C_Container_GetContainerFreeSlots);
Game::Lua::RegisterTableFunction("C_Container", "CalculateTotalNumberOfFreeBagSlots",
&Script_C_Container_CalculateTotalNumberOfFreeBagSlots,
&kCalculateTotalNumberOfFreeBagSlots);
&Script_C_Container_CalculateTotalNumberOfFreeBagSlots);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -12
View File
@@ -40,20 +40,9 @@ int __fastcall Script_C_Container_HasContainerItem(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("hasItem", "bool"),
};
const Game::Doc::Function kHasContainerItem{
"Returns whether a bag slot holds an item, even one whose data is not cached yet.",
kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "HasContainerItem",
&Script_C_Container_HasContainerItem, &kHasContainerItem);
&Script_C_Container_HasContainerItem);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+2 -16
View File
@@ -117,27 +117,13 @@ int __fastcall Script_C_Container_UseHearthstone(void *L) {
return 1;
}
const Game::Doc::Field kHasRets[] = {
Game::Doc::Opt("itemID", "number", nullptr, "Nil when no hearthstone is in the bags."),
};
const Game::Doc::Function kPlayerHasHearthstone{
"Returns the ID of the hearthstone in the player's bags.", {}, kHasRets};
const Game::Doc::Field kUseRets[] = {
Game::Doc::Req("used", "bool",
"True when a hearthstone was found; the cast itself can still fail."),
};
const Game::Doc::Function kUseHearthstone{"Uses the hearthstone in the player's bags.", {},
kUseRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "PlayerHasHearthstone",
&Script_C_Container_PlayerHasHearthstone,
&kPlayerHasHearthstone);
&Script_C_Container_PlayerHasHearthstone);
Game::Lua::RegisterTableFunction("C_Container", "UseHearthstone",
&Script_C_Container_UseHearthstone, &kUseHearthstone);
&Script_C_Container_UseHearthstone);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -11
View File
@@ -41,19 +41,9 @@ int __fastcall Script_C_Container_GetContainerItemID(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("itemID", "number", nullptr, "Nil when the slot is empty."),
};
const Game::Doc::Function kGetContainerItemID{"Returns the ID of the item in a bag slot.",
kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemID",
&Script_C_Container_GetContainerItemID, &kGetContainerItemID);
&Script_C_Container_GetContainerItemID);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+2 -16
View File
@@ -57,23 +57,9 @@ int __fastcall Script_C_Container_MoveItem(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("srcBag", "BagIndex"),
Game::Doc::Req("srcSlot", "luaIndex"),
Game::Doc::Req("dstBag", "BagIndex"),
Game::Doc::Req("dstSlot", "luaIndex"),
Game::Doc::Req("count", "number", "From 1 to 255."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("sent", "bool", "False when an argument is not valid."),
};
const Game::Doc::Function kMoveItem{
"Moves part of a stack to another bag slot without the cursor.", kArgs, kRets, nullptr,
true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "MoveItem", &Script_C_Container_MoveItem,
&kMoveItem);
Game::Lua::RegisterTableFunction("C_Container", "MoveItem",
&Script_C_Container_MoveItem);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -16
View File
@@ -37,24 +37,9 @@ int __fastcall Script_C_Container_IsContainerItemOpenable(void *L) {
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("isOpenable", "bool", nullptr,
"Nil when the slot is empty or the item's data is not cached yet."),
Game::Doc::Opt("canOpen", "bool", nullptr, "False while the item is still locked."),
};
const Game::Doc::Function kIsContainerItemOpenable{
"Returns whether the item in a bag slot can be opened, and whether the player can "
"open it now.",
kArgs, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "IsContainerItemOpenable",
&Script_C_Container_IsContainerItemOpenable,
&kIsContainerItemOpenable);
&Script_C_Container_IsContainerItemOpenable);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -21
View File
@@ -97,29 +97,9 @@ int __fastcall Script_C_Container_GetContainerItemQuestInfo(void *L) {
return 1;
}
const Game::Doc::Field kItemQuestInfoFields[] = {
Game::Doc::Req("isQuestItem", "bool"),
Game::Doc::Opt("questID", "number", nullptr,
"The quest the item begins; nil when it begins none."),
Game::Doc::Req("isActive", "bool", "True when the quest in questID is in the quest log."),
};
const Game::Doc::Structure kItemQuestInfo{"ItemQuestInfo", "Container", kItemQuestInfoFields};
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("questInfo", "ItemQuestInfo",
"Always a table; an empty slot reports no quest."),
};
const Game::Doc::Function kGetContainerItemQuestInfo{
"Returns how the item in a bag slot relates to quests.", kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemQuestInfo",
&Script_C_Container_GetContainerItemQuestInfo,
&kGetContainerItemQuestInfo);
&Script_C_Container_GetContainerItemQuestInfo);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -13
View File
@@ -37,21 +37,9 @@ int __fastcall Script_C_Container_GetContainerItemRepairCost(void *L) {
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("repairCost", "number", "In copper; 0 when the item needs no repair."),
};
const Game::Doc::Function kGetContainerItemRepairCost{
"Returns what a merchant charges to repair the item in a bag slot.", kArgs, kRets,
nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemRepairCost",
&Script_C_Container_GetContainerItemRepairCost,
&kGetContainerItemRepairCost);
&Script_C_Container_GetContainerItemRepairCost);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+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;
}
+2 -14
View File
@@ -49,21 +49,9 @@ int __fastcall Script_C_Container_SwapItems(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("srcBag", "BagIndex"),
Game::Doc::Req("srcSlot", "luaIndex"),
Game::Doc::Req("dstBag", "BagIndex"),
Game::Doc::Req("dstSlot", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("sent", "bool", "False when an argument is not valid."),
};
const Game::Doc::Function kSwapItems{
"Swaps the contents of two bag slots without the cursor.", kArgs, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "SwapItems", &Script_C_Container_SwapItems,
&kSwapItems);
Game::Lua::RegisterTableFunction("C_Container", "SwapItems",
&Script_C_Container_SwapItems);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+40
View File
@@ -0,0 +1,40 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `Constants.MoneyFormattingConstants`, retail's MoneyConstants system
// (Blizzard_APIDocumentationGenerated/MoneyConstantsDocumentation.lua):
// GOLD_REWARD_THRESHOLD_TO_HIDE_COPPER = 10, the gold amount above which
// FormattingUtil's GetMoneyString(..., checkGoldThreshold) drops the copper.
#include "Game.h"
namespace Currency::MoneyConstants {
namespace {
const Game::Lua::EnumIntegerEntry kMoneyFormattingConstants[] = {
{"GOLD_REWARD_THRESHOLD_TO_HIDE_COPPER", 10},
};
void RegisterLuaFunctions() {
Game::Lua::RegisterConstants("MoneyFormattingConstants", kMoneyFormattingConstants,
sizeof(kMoneyFormattingConstants) /
sizeof(kMoneyFormattingConstants[0]),
"MoneyConstants");
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Currency::MoneyConstants
@@ -22,13 +22,18 @@ Util\SoundKitConstants.lua
Util\TableUtil.lua
Util\EnumUtil.lua
Util\StringUtil.lua
Util\TypeUtil.lua
Util\MathUtil.lua
Util\SortUtil.lua
Util\Spline.lua
Util\PixelUtil.lua
Util\FrameUtil.lua
Util\InputUtil.lua
Util\FormattingUtil.lua
Util\TimeUtil.lua
Util\Flags.lua
Util\TaggableObject.lua
Util\ExportUtil.lua
Util\ButtonStateBehavior.lua
Util\CallbackRegistry.lua
Util\GlobalCallbackRegistry.lua
@@ -36,22 +41,38 @@ Util\TemplateInfoCache.lua
Util\FrameFactory.lua
Util\DataProvider.lua
Util\TreeListDataProvider.lua
Util\IndexRangeDataProvider.lua
Util\CircularBuffer.lua
Util\ColorUtil.lua
Util\SharedColorConstants.lua
Util\ItemLocation.lua
Util\PlayerLocation.lua
Util\EventUtil.lua
Util\ContinuableContainer.lua
Util\AsyncCallbackSystem.lua
Util\Item.lua
Util\Spell.lua
Util\ObjectCache.lua
Util\SpellBookItemLocation.lua
Util\ItemUtil.lua
Util\ContainerFrame.lua
Util\AddOnCompat.lua
Util\FunctionUtil.lua
Util\MixinUtil.lua
Util\ObjectUpdater.lua
Util\CacheUtil.lua
Util\Pools.lua
Util\SecureTypes.lua
Util\ProxyUtil.lua
Util\Pools.lua
Util\TableBuilder.lua
Util\FrameWatcher.lua
Util\TimedCallback.lua
Util\EasingUtil.lua
Util\ScriptAnimationUtil.lua
Util\Interpolator.lua
Util\Sequencer.lua
Util\SmoothStatusBar.lua
Util\AnimationSystem.lua
Util\AnchorUtil.lua
Util\RegionUtil.lua
Util\GridLayoutUtil.lua
@@ -89,6 +110,8 @@ FrameXML\EventEditBox.lua
FrameXML\EventEditBox.xml
FrameXML\EventScrollFrame.lua
FrameXML\EventScrollFrame.xml
FrameXML\CallbackRegistrant.lua
FrameXML\CallbackRegistrant.xml
FrameXML\Scroll\ScrollUtil.lua
FrameXML\Scroll\ScrollController.lua
FrameXML\Scroll\ScrollBoxPadding.lua
+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,87 @@
-- Backport of retail's Blizzard_SharedXMLBase/CallbackRegistrant.lua.
CallbackRegistrantMixin = {};
function CallbackRegistrantMixin:OnShow()
for i, eventRegistrationInfo in ipairs(self:GetDynamicCallbackRegistrantHandlers()) do
self:RegisterFromRegistrationInfo(eventRegistrationInfo);
end
end
function CallbackRegistrantMixin:OnHide()
self:UnregisterAllInternal(self:GetDynamicCallbackRegistrantHandlers());
end
function CallbackRegistrantMixin:AddEventMethodInternal(handlersTable, callbackRegistry, event, handlerMethod)
local eventRegistrationInfo = self:CreateEventRegistrationInfo(callbackRegistry, event, handlerMethod)
table.insert(handlersTable, eventRegistrationInfo);
return eventRegistrationInfo;
end
function CallbackRegistrantMixin:AddDynamicEventMethod(callbackRegistry, event, handlerMethod)
local eventRegistrationInfo = self:AddEventMethodInternal(self:GetDynamicCallbackRegistrantHandlers(), callbackRegistry, event, handlerMethod)
if self:IsShown() then
self:RegisterFromRegistrationInfo(eventRegistrationInfo);
end
end
-- Routing calls here allows for future expansion in this template (i.e. checking duplicate registrations).
function CallbackRegistrantMixin:AddStaticEventMethod(callbackRegistry, event, handlerMethod)
local eventRegistrationInfo = self:AddEventMethodInternal(self:GetStaticCallbackRegistrantHandlers(), callbackRegistry, event, handlerMethod)
self:RegisterFromRegistrationInfo(eventRegistrationInfo);
end
function CallbackRegistrantMixin:RemoveStaticEventMethod(callbackRegistry, event, handlerMethod)
local handlers = self:GetStaticCallbackRegistrantHandlers();
for i, eventRegistrationInfo in ipairs(handlers) do
if (eventRegistrationInfo.callbackRegistry == callbackRegistry) and (eventRegistrationInfo.event == event) then
self:UnregisterFromRegistrationInfo(eventRegistrationInfo);
table.remove(handlers, i);
break;
end
end
end
function CallbackRegistrantMixin:UnregisterAllEventMethods()
self:UnregisterAllInternal(self:GetDynamicCallbackRegistrantHandlers());
self:UnregisterAllInternal(self:GetStaticCallbackRegistrantHandlers());
end
function CallbackRegistrantMixin:CreateEventRegistrationInfo(callbackRegistry, event, handlerMethod)
return { callbackRegistry = callbackRegistry, event = event, handlerMethod = handlerMethod, registered = false, };
end
function CallbackRegistrantMixin:RegisterFromRegistrationInfo(eventRegistrationInfo)
if eventRegistrationInfo.registered then
return;
end
eventRegistrationInfo.callbackRegistry:RegisterCallback(eventRegistrationInfo.event, eventRegistrationInfo.handlerMethod, self);
eventRegistrationInfo.registered = true;
end
function CallbackRegistrantMixin:UnregisterFromRegistrationInfo(eventRegistrationInfo)
if not eventRegistrationInfo.registered then
return;
end
eventRegistrationInfo.callbackRegistry:UnregisterCallback(eventRegistrationInfo.event, self);
eventRegistrationInfo.registered = false;
end
function CallbackRegistrantMixin:UnregisterAllInternal(handlersTable)
for i, eventRegistrationInfo in ipairs(handlersTable) do
self:UnregisterFromRegistrationInfo(eventRegistrationInfo);
end
end
function CallbackRegistrantMixin:GetDynamicCallbackRegistrantHandlers()
self.callbackRegistrantHandlers = self.callbackRegistrantHandlers or {};
return self.callbackRegistrantHandlers;
end
function CallbackRegistrantMixin:GetStaticCallbackRegistrantHandlers()
self.staticCallbackRegistrantHandlers = self.staticCallbackRegistrantHandlers or {};
return self.staticCallbackRegistrantHandlers;
end
@@ -0,0 +1,9 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\..\..\..\..\WoW\Data\Interface\AddOns\Blizzard_SharedXML\UI.xsd">
<Frame name="CallbackRegistrantTemplate" mixin="CallbackRegistrantMixin" virtual="true">
<Scripts>
<OnShow method="OnShow"/>
<OnHide method="OnHide"/>
</Scripts>
</Frame>
</Ui>
+245 -157
View File
@@ -1,53 +1,44 @@
-- ClassicAPI's own entry in the AddOn Options window
-- (FrameXML\InterfaceOptions.lua): a ClassicAPI page with Controls, Combat
-- and Text To Speech pages under it.
-- (FrameXML\InterfaceOptions.lua): a ClassicAPI page with one page under it
-- for each feature. Every cvar that ClassicAPI registers has a control here,
-- and so does the player cache.
--
-- Where retail's Settings shows a setting, the option copies its label,
-- tooltip and value mapping (Blizzard_SettingsDefinitions_Frame: Controls,
-- Combat, Accessibility; the TTS options use the Text To Speech window's
-- labels). Retail has no labels for the player cache, the cast settings or
-- friendly soft targeting, so those strings are ClassicAPI's own.
-- Each control reads and writes exactly one setting, and writes it only when
-- the player uses that control. So the pages have no okay, cancel or default:
-- Okay, Cancel and Escape only close the window, and Defaults does nothing
-- here. A page reads its settings again each time it is shown.
--
-- Changes apply at once. Cancel (and Escape) restores what each page held
-- when it was first shown since the last Okay or Cancel; Defaults restores
-- the shown page's defaults.
-- A page's controls scroll under its heading in a ScrollBox, so a page holds
-- as many as its feature has. Dropdowns and number boxes sit on the same row
-- as their label.
--
-- Every string is a global from locales\enUS.lua: retail's GlobalStrings key
-- where retail has the string (ENABLE_INTERACT_TEXT and the like), a
-- CLASSICAPI_ one where it does not.
--
-- Lua 5.0: no `...` expression or select().
local L = {
ABOUT = "Settings for the features that ClassicAPI adds to the game.",
PLAYER_CACHE = "Player Cache",
REMEMBER_PLAYERS = "Remember players",
REMEMBER_PLAYERS_TOOLTIP = "Saves the name, class, race and sex of the players you meet, so addons can show them in later sessions. All your characters on this realm share the data.",
FRIEND_TARGETING = "Enable Friendly Action Targeting",
FRIEND_TARGETING_TOOLTIP = "Picks the nearest friendly unit where you are looking. When you have no target, your helpful spells go to it.",
AUTO_STAND = "Auto Stand",
AUTO_STAND_TOOLTIP = "Stand up when you cast a spell while you sit.",
AUTO_DISMOUNT = "Auto Dismount",
AUTO_DISMOUNT_TOOLTIP = "Dismount when you cast a spell while you are mounted.",
AUTO_UNSHIFT = "Auto Cancel Form",
AUTO_UNSHIFT_TOOLTIP = "Leave your form when you cast a spell that you cannot cast in it.",
local ARC_CHOICES = {
{ value = 0, text = CLASSICAPI_DIRECTION_FRONT },
{ value = 1, text = CLASSICAPI_DIRECTION_CONE },
{ value = 2, text = CLASSICAPI_DIRECTION_ANY },
}
-- Enum.SoftTargetEnableFlags: retail's checkboxes switch between Any (on)
-- and Gamepad (off).
local SOFT_TARGET_GAMEPAD, SOFT_TARGET_ANY = 1, 3
local FORCE_CHOICES = {
{ value = 0, text = CLASSICAPI_LOOSE_TARGET_FOLLOWS_NONE },
{ value = 1, text = CLASSICAPI_LOOSE_TARGET_FOLLOWS_ENEMY },
{ value = 2, text = CLASSICAPI_LOOSE_TARGET_FOLLOWS_FRIEND },
}
-- ---------------------------------------------------------------------------
-- Settings: get / set / reset over cvars or the player cache
-- Settings: get / set over one cvar or the player cache
-- ---------------------------------------------------------------------------
local function ResetCVars(cvars)
for _, cvar in ipairs(cvars) do
SetCVar(cvar, GetCVarDefault(cvar))
end
end
-- On for any nonzero number.
local function CVarSetting(cvar)
return {
get = function() return GetCVar(cvar) == "1" end,
get = function() return (tonumber(GetCVar(cvar)) or 0) ~= 0 end,
set = function(value) SetCVar(cvar, value and "1" or "0") end,
reset = function() ResetCVars({ cvar }) end,
}
end
@@ -55,52 +46,33 @@ local function NumberCVarSetting(cvar)
return {
get = function() return tonumber(GetCVar(cvar)) end,
set = function(value) SetCVar(cvar, tostring(value)) end,
reset = function() ResetCVars({ cvar }) end,
}
end
-- Retail's "Enable Interact Key" / "Enable Action Targeting" proxies.
-- The soft-target enable cvars: 0 off, 1 gamepad, 2 keyboard and mouse,
-- 3 always. 1.12 has no gamepad, so 2 and 3 are on.
local function SoftTargetSetting(cvar)
return {
get = function() return tonumber(GetCVar(cvar)) == SOFT_TARGET_ANY end,
set = function(value) SetCVar(cvar, tostring(value and SOFT_TARGET_ANY or SOFT_TARGET_GAMEPAD)) end,
reset = function() ResetCVars({ cvar }) end,
get = function() return (tonumber(GetCVar(cvar)) or 0) >= 2 end,
set = function(value) SetCVar(cvar, value and "3" or "0") end,
}
end
-- Retail's "Interact Key Icons" proxy: 1 = NPCs only, 2 = all, 3 = none.
local ICON_CVARS = { "SoftTargetIconEnemy", "SoftTargetIconInteract", "SoftTargetIconGameObject", "SoftTargetLowPriorityIcons" }
local ICON_PRESETS = {
[1] = { "0", "1", "0", "0" },
[2] = { "1", "1", "1", "1" },
[3] = { "0", "0", "0", "0" },
}
local interactIconsSetting = {
get = function()
local on, off = true, true
for _, cvar in ipairs(ICON_CVARS) do
if GetCVar(cvar) == "1" then off = false else on = false end
end
if on then return 2 elseif off then return 3 else return 1 end
end,
set = function(value)
for i, cvar in ipairs(ICON_CVARS) do
SetCVar(cvar, ICON_PRESETS[value][i])
end
end,
reset = function() ResetCVars(ICON_CVARS) end,
}
local rememberSetting = {
get = function() return C_PlayerCache.IsEnabled() and true or false end,
set = function(value) C_PlayerCache.SetEnabled(value) end,
reset = function() C_PlayerCache.SetEnabled(false) end,
}
-- ---------------------------------------------------------------------------
-- Pages
-- ---------------------------------------------------------------------------
-- Row labels line up with the checkbox labels; row controls start at
-- CONTROL_X.
local LABEL_X = 48
local CONTROL_X = 200
-- pfUI's Interface Options skin finds the controls by these names.
local numControls = 0
local function NextName()
numControls = numControls + 1
@@ -115,6 +87,10 @@ local function RefreshControls(page)
page.updating = nil
end
-- The controls go on `page.content`, the one frame in the page's ScrollBox,
-- `page.y` down from its top. The clipping of the ScrollBox also keeps the
-- mouse off the controls that are scrolled out of view.
-- `title` is the heading, the category name when nil.
local function NewPage(name, parent, title, subtitle)
local page = CreateFrame("Frame")
page.name = name
@@ -123,66 +99,80 @@ local function NewPage(name, parent, title, subtitle)
local heading = page:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
heading:SetPoint("TOPLEFT", page, "TOPLEFT", 16, -16)
heading:SetText(title)
page.y = -40
heading:SetText(title or name)
local top = -40
if subtitle then
local text = page:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
text:SetPoint("TOPLEFT", heading, "BOTTOMLEFT", 0, -8)
text:SetPoint("RIGHT", page, "RIGHT", -32, 0)
text:SetJustifyH("LEFT")
text:SetText(subtitle)
page.y = -64
top = -64
end
page.refresh = function()
if not page.saved then
page.saved = {}
for i, control in ipairs(page.controls) do
page.saved[i] = control.setting.get()
end
end
RefreshControls(page)
end
page.okay = function()
page.saved = nil
end
page.cancel = function()
if not page.saved then return end
for i, control in ipairs(page.controls) do
if control.setting.get() ~= page.saved[i] then
control.setting.set(page.saved[i])
end
end
page.saved = nil
end
page.default = function()
for _, control in ipairs(page.controls) do
control.setting.reset()
end
RefreshControls(page)
end
local scrollBox = CreateFrame("Frame", nil, page, "WowScrollBox")
scrollBox:SetPoint("TOPLEFT", page, "TOPLEFT", 0, top)
scrollBox:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", -20, 8)
scrollBox:EnableMouseWheel(true)
local scrollBar = CreateFrame("EventFrame", nil, page, "MinimalScrollBar")
scrollBar:SetPoint("TOPLEFT", scrollBox, "TOPRIGHT", 4, 0)
scrollBar:SetPoint("BOTTOMLEFT", scrollBox, "BOTTOMRIGHT", 4, 0)
local content = CreateFrame("Frame", nil, scrollBox)
content:SetHeight(1)
content.scrollable = true
ScrollUtil.InitScrollBoxWithScrollBar(scrollBox, scrollBar, CreateScrollBoxLinearView())
ScrollUtil.AddManagedScrollBarVisibilityBehavior(scrollBox, scrollBar)
page.scrollBox = scrollBox
page.content = content
page.y = 0
page.refresh = RefreshControls
InterfaceOptions_AddCategory(page)
return page
end
-- Moves `page.y` down past a row and makes the content that tall. The
-- ScrollBox measures it again on its next update (a hidden page's waits for
-- the page to be shown).
local function Grow(page, height)
page.y = page.y - height
page.content:SetHeight(8 - page.y)
page.scrollBox:FullUpdate(ScrollBoxConstants.UpdateQueued)
end
local function AddControl(page, control, setting, height)
control.setting = setting
table.insert(page.controls, control)
page.y = page.y - height
Grow(page, height)
end
local function AddHeader(page, text)
local header = page:CreateFontString(nil, "ARTWORK", "GameFontNormal")
header:SetPoint("TOPLEFT", page, "TOPLEFT", 16, page.y - 8)
local header = page.content:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
header:SetPoint("TOPLEFT", page.content, "TOPLEFT", 16, page.y - 6)
header:SetText(text)
page.y = page.y - 28
Grow(page, 24)
end
local function AddRowLabel(page, label)
local text = page.content:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
text:SetPoint("TOPLEFT", page.content, "TOPLEFT", LABEL_X, page.y - 10)
text:SetText(label)
end
local function SetTooltip(frame, tooltip)
frame:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText(tooltip, nil, nil, nil, nil, 1)
end)
frame:SetScript("OnLeave", function() GameTooltip:Hide() end)
end
-- `enabledIf`, when given, greys the checkbox out while it returns false.
local function AddCheckbox(page, label, tooltip, setting, enabledIf)
local check = CreateFrame("CheckButton", NextName(), page, "OptionsCheckButtonTemplate")
check:SetPoint("TOPLEFT", page, "TOPLEFT", 16, page.y)
local check = CreateFrame("CheckButton", NextName(), page.content, "OptionsCheckButtonTemplate")
check:SetPoint("TOPLEFT", page.content, "TOPLEFT", 16, page.y)
local text = getglobal(check:GetName() .. "Text")
text:SetText(label)
check.tooltipText = tooltip
@@ -201,32 +191,32 @@ local function AddCheckbox(page, label, tooltip, setting, enabledIf)
end
end
AddControl(page, check, setting, 30)
return check
end
-- `options()` returns the entries as { value = ..., text = ... }.
local function AddDropDown(page, label, tooltip, setting, options)
local title = page:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
title:SetPoint("TOPLEFT", page, "TOPLEFT", 20, page.y - 6)
title:SetText(label)
-- `choices` is a list of { value = ..., text = ... }, or a function that
-- returns one. A value with no choice shows as the number itself.
local function AddDropDown(page, label, tooltip, setting, choices)
AddRowLabel(page, label)
local dropdown = CreateFrame("Frame", NextName(), page, "UIDropDownMenuTemplate")
dropdown:SetPoint("TOPLEFT", page, "TOPLEFT", 0, page.y - 20)
UIDropDownMenu_SetWidth(180, dropdown)
local function List()
if type(choices) == "function" then return choices() end
return choices
end
-- The template's art has a 16 pixel margin on the left.
local dropdown = CreateFrame("Frame", NextName(), page.content, "UIDropDownMenuTemplate")
dropdown:SetPoint("TOPLEFT", page.content, "TOPLEFT", CONTROL_X - 16, page.y)
UIDropDownMenu_SetWidth(150, dropdown)
dropdown:EnableMouse(true)
dropdown:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText(tooltip, nil, nil, nil, nil, 1)
end)
dropdown:SetScript("OnLeave", function() GameTooltip:Hide() end)
SetTooltip(dropdown, tooltip)
local function Initialize()
local selected = setting.get()
for _, option in ipairs(options()) do
for _, choice in ipairs(List()) do
local info = {}
info.text = option.text
info.value = option.value
info.checked = option.value == selected and 1 or nil
info.text = choice.text
info.value = choice.value
info.checked = choice.value == selected and 1 or nil
info.func = function()
UIDropDownMenu_SetSelectedValue(dropdown, this.value)
setting.set(this.value)
@@ -236,18 +226,55 @@ local function AddDropDown(page, label, tooltip, setting, options)
end
dropdown.Update = function(self)
UIDropDownMenu_Initialize(self, Initialize)
UIDropDownMenu_SetSelectedValue(self, setting.get())
local selected = setting.get()
UIDropDownMenu_SetSelectedValue(self, selected)
local text = tostring(selected or "")
for _, choice in ipairs(List()) do
if choice.value == selected then text = choice.text end
end
UIDropDownMenu_SetText(text, self)
end
AddControl(page, dropdown, setting, 56)
return dropdown
AddControl(page, dropdown, setting, 32)
end
local function AddSlider(page, label, tooltip, setting, minValue, maxValue)
local slider = CreateFrame("Slider", NextName(), page, "OptionsSliderTemplate")
slider:SetPoint("TOPLEFT", page, "TOPLEFT", 24, page.y - 20)
-- Writes when Enter is pressed or the box loses focus; Escape puts the
-- current value back. Text that is not a number is not written.
local function AddNumberBox(page, label, tooltip, setting)
AddRowLabel(page, label)
local box = CreateFrame("EditBox", NextName(), page.content, "InputBoxTemplate")
box:SetPoint("TOPLEFT", page.content, "TOPLEFT", CONTROL_X + 6, page.y - 6)
box:SetWidth(70)
box:SetHeight(20)
box:SetAutoFocus(false)
SetTooltip(box, tooltip)
local function ShowValue(self)
self:SetText(tostring(setting.get() or ""))
end
box:SetScript("OnEnterPressed", function() this:ClearFocus() end)
box:SetScript("OnEscapePressed", function()
ShowValue(this)
this:ClearFocus()
end)
box:SetScript("OnEditFocusLost", function()
this:HighlightText(0, 0)
local value = tonumber(this:GetText())
if value and value ~= setting.get() then
setting.set(value)
end
ShowValue(this)
end)
box.Update = ShowValue
AddControl(page, box, setting, 32)
end
local function AddSlider(page, label, tooltip, setting, minValue, maxValue, step)
local slider = CreateFrame("Slider", NextName(), page.content, "OptionsSliderTemplate")
slider:SetPoint("TOPLEFT", page.content, "TOPLEFT", 24, page.y - 20)
slider:SetWidth(200)
slider:SetMinMaxValues(minValue, maxValue)
slider:SetValueStep(1)
slider:SetValueStep(step)
slider.tooltipText = tooltip
getglobal(slider:GetName() .. "Text"):SetText(label)
getglobal(slider:GetName() .. "Low"):SetText(minValue)
@@ -255,21 +282,24 @@ local function AddSlider(page, label, tooltip, setting, minValue, maxValue)
local value = slider:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
value:SetPoint("LEFT", slider, "RIGHT", 8, 0)
local function Format(v)
if step < 1 then return string.format("%.2f", v) end
return v
end
slider:SetScript("OnValueChanged", function()
local v = math.floor(this:GetValue() + 0.5)
value:SetText(v)
local v = math.floor(this:GetValue() / step + 0.5) * step
value:SetText(Format(v))
if not page.updating and v ~= setting.get() then
setting.set(v)
end
end)
slider.Update = function(self)
local v = setting.get()
local v = setting.get() or minValue
self:SetValue(v)
value:SetText(v)
value:SetText(Format(v))
end
AddControl(page, slider, setting, 56)
return slider
end
-- ---------------------------------------------------------------------------
@@ -277,57 +307,115 @@ end
-- ---------------------------------------------------------------------------
local version = GetAddOnMetadata("!!!ClassicAPI", "Version")
local main = NewPage("ClassicAPI", nil, "ClassicAPI " .. version, L.ABOUT)
local main = NewPage("ClassicAPI", nil, "ClassicAPI " .. version, CLASSICAPI_OPTIONS_ABOUT)
-- ---------------------------------------------------------------------------
-- Controls
-- Interact Key
-- ---------------------------------------------------------------------------
local controls = NewPage(CONTROLS_LABEL, main.name, CONTROLS_LABEL)
local interact = NewPage(CLASSICAPI_INTERACT_KEY, main.name)
AddCheckbox(controls, ENABLE_INTERACT_TEXT, OPTION_TOOLTIP_ENABLE_INTERACT, SoftTargetSetting("SoftTargetInteract"))
AddDropDown(controls, INTERACT_ICONS_OPTION, OPTION_TOOLTIP_INTERACT_ICONS, interactIconsSetting, function()
return {
{ value = 1, text = INTERACT_ICONS_DEFAULT },
{ value = 2, text = INTERACT_ICONS_SHOW_ALL },
{ value = 3, text = INTERACT_ICONS_SHOW_NONE },
}
end)
AddCheckbox(controls, L.AUTO_STAND, L.AUTO_STAND_TOOLTIP, CVarSetting("autoStand"))
AddCheckbox(controls, L.AUTO_DISMOUNT, L.AUTO_DISMOUNT_TOOLTIP, CVarSetting("autoDismount"))
AddCheckbox(controls, L.AUTO_UNSHIFT, L.AUTO_UNSHIFT_TOOLTIP, CVarSetting("autoUnshift"))
AddCheckbox(interact, ENABLE_INTERACT_TEXT, OPTION_TOOLTIP_ENABLE_INTERACT, SoftTargetSetting("SoftTargetInteract"))
AddDropDown(interact, CLASSICAPI_INTERACT_DIRECTION, CLASSICAPI_INTERACT_DIRECTION_TOOLTIP, NumberCVarSetting("SoftTargetInteractArc"), ARC_CHOICES)
AddNumberBox(interact, CLASSICAPI_INTERACT_RANGE, CLASSICAPI_RANGE_TOOLTIP, NumberCVarSetting("SoftTargetInteractRange"))
AddCheckbox(interact, CLASSICAPI_STRICT_INTERACT_RANGE, CLASSICAPI_STRICT_INTERACT_RANGE_TOOLTIP, CVarSetting("SoftTargetInteractRangeIsHard"))
local interactIcon = CVarSetting("SoftTargetIconInteract")
AddCheckbox(interact, CLASSICAPI_INTERACT_ICON, CLASSICAPI_INTERACT_ICON_TOOLTIP, interactIcon)
-- The object icon is drawn only while the interact icon is on.
AddCheckbox(interact, CLASSICAPI_GAME_OBJECT_ICON, CLASSICAPI_GAME_OBJECT_ICON_TOOLTIP, CVarSetting("SoftTargetIconGameObject"), interactIcon.get)
AddHeader(interact, CLASSICAPI_ALL_SOFT_TARGET_ICONS)
AddCheckbox(interact, CLASSICAPI_LOW_PRIORITY_ICONS, CLASSICAPI_LOW_PRIORITY_ICONS_TOOLTIP, CVarSetting("SoftTargetLowPriorityIcons"))
AddNumberBox(interact, CLASSICAPI_ICON_SIZE, CLASSICAPI_ICON_SIZE_TOOLTIP, NumberCVarSetting("SoftTargetNameplateSize"))
-- ---------------------------------------------------------------------------
-- Action Targeting
-- ---------------------------------------------------------------------------
local targeting = NewPage(CLASSICAPI_ACTION_TARGETING, main.name)
AddHeader(targeting, CLASSICAPI_ENEMIES)
AddCheckbox(targeting, ACTION_TARGETING_OPTION, OPTION_TOOLTIP_ACTION_TARGETING, SoftTargetSetting("SoftTargetEnemy"))
AddDropDown(targeting, CLASSICAPI_ENEMY_DIRECTION, CLASSICAPI_UNIT_DIRECTION_TOOLTIP, NumberCVarSetting("SoftTargetEnemyArc"), ARC_CHOICES)
AddNumberBox(targeting, CLASSICAPI_ENEMY_RANGE, CLASSICAPI_RANGE_TOOLTIP, NumberCVarSetting("SoftTargetEnemyRange"))
AddCheckbox(targeting, CLASSICAPI_ENEMY_ICON, CLASSICAPI_ENEMY_ICON_TOOLTIP, CVarSetting("SoftTargetIconEnemy"))
AddHeader(targeting, CLASSICAPI_FRIENDS)
AddCheckbox(targeting, CLASSICAPI_FRIEND_TARGETING, CLASSICAPI_FRIEND_TARGETING_TOOLTIP, SoftTargetSetting("SoftTargetFriend"))
AddDropDown(targeting, CLASSICAPI_FRIEND_DIRECTION, CLASSICAPI_UNIT_DIRECTION_TOOLTIP, NumberCVarSetting("SoftTargetFriendArc"), ARC_CHOICES)
AddNumberBox(targeting, CLASSICAPI_FRIEND_RANGE, CLASSICAPI_RANGE_TOOLTIP, NumberCVarSetting("SoftTargetFriendRange"))
AddCheckbox(targeting, CLASSICAPI_FRIEND_ICON, CLASSICAPI_FRIEND_ICON_TOOLTIP, CVarSetting("SoftTargetIconFriend"))
AddHeader(targeting, CLASSICAPI_WHEN_YOU_HAVE_A_TARGET)
AddCheckbox(targeting, CLASSICAPI_KEEP_ACTION_TARGETING, CLASSICAPI_KEEP_ACTION_TARGETING_TOOLTIP, CVarSetting("SoftTargetWithLocked"))
AddCheckbox(targeting, CLASSICAPI_PREFER_YOUR_TARGET, CLASSICAPI_PREFER_YOUR_TARGET_TOOLTIP, CVarSetting("SoftTargetMatchLocked"))
AddDropDown(targeting, CLASSICAPI_LOOSE_TARGET_FOLLOWS, CLASSICAPI_LOOSE_TARGET_FOLLOWS_TOOLTIP, NumberCVarSetting("SoftTargetForce"), FORCE_CHOICES)
-- ---------------------------------------------------------------------------
-- Combat
-- ---------------------------------------------------------------------------
local combat = NewPage(COMBAT_LABEL, main.name, COMBAT_LABEL)
local combat = NewPage(COMBAT_LABEL, main.name)
AddCheckbox(combat, ACTION_TARGETING_OPTION, OPTION_TOOLTIP_ACTION_TARGETING, SoftTargetSetting("SoftTargetEnemy"))
-- Retail has no friend checkbox; same mapping as its enemy one.
AddCheckbox(combat, L.FRIEND_TARGETING, L.FRIEND_TARGETING_TOOLTIP, SoftTargetSetting("SoftTargetFriend"))
AddHeader(combat, CLASSICAPI_ACTION_BARS)
AddCheckbox(combat, ACTION_BUTTON_USE_KEY_DOWN, OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN, CVarSetting("ActionButtonUseKeyDown"))
AddCheckbox(combat, COUNTDOWN_FOR_COOLDOWNS_TEXT, OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS, CVarSetting("countdownForCooldowns"))
AddHeader(combat, CLASSICAPI_CASTING_AND_ATTACKING)
AddCheckbox(combat, CLASSICAPI_AUTO_STAND, CLASSICAPI_AUTO_STAND_TOOLTIP, CVarSetting("autoStand"))
AddCheckbox(combat, CLASSICAPI_AUTO_DISMOUNT, CLASSICAPI_AUTO_DISMOUNT_TOOLTIP, CVarSetting("autoDismount"))
AddCheckbox(combat, CLASSICAPI_AUTO_UNSHIFT, CLASSICAPI_AUTO_UNSHIFT_TOOLTIP, CVarSetting("autoUnshift"))
AddCheckbox(combat, STOP_AUTO_ATTACK, OPTION_TOOLTIP_STOP_AUTO_ATTACK, CVarSetting("stopAutoAttackOnTargetChange"))
-- ---------------------------------------------------------------------------
-- Bags and Items
-- ---------------------------------------------------------------------------
local bags = NewPage(CLASSICAPI_BAGS_AND_ITEMS, main.name)
AddCheckbox(bags, CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT, CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT_TOOLTIP, CVarSetting("sortBagsRightToLeft"))
AddCheckbox(bags, CLASSICAPI_IGNORE_BACKPACK, CLASSICAPI_IGNORE_BACKPACK_TOOLTIP, CVarSetting("backpackAutosortDisabled"))
AddCheckbox(bags, CLASSICAPI_IGNORE_BANK, CLASSICAPI_IGNORE_BANK_TOOLTIP, CVarSetting("bankAutosortDisabled"))
AddCheckbox(bags, CLASSICAPI_HIDE_EQUIPMENT_SETS, CLASSICAPI_HIDE_EQUIPMENT_SETS_TOOLTIP, CVarSetting("dontShowEquipmentSetsOnItems"))
-- ---------------------------------------------------------------------------
-- Interface
-- ---------------------------------------------------------------------------
local interface = NewPage(CLASSICAPI_INTERFACE, main.name)
AddHeader(interface, CLASSICAPI_NAMEPLATES)
AddSlider(interface, CLASSICAPI_TARGET_NAMEPLATE_ALPHA, CLASSICAPI_TARGET_NAMEPLATE_ALPHA_TOOLTIP, NumberCVarSetting("nameplateSelectedAlpha"), 0, 1, 0.05)
AddSlider(interface, CLASSICAPI_OTHER_NAMEPLATE_ALPHA, CLASSICAPI_OTHER_NAMEPLATE_ALPHA_TOOLTIP, NumberCVarSetting("nameplateNotSelectedAlpha"), 0, 1, 0.05)
AddHeader(interface, CLASSICAPI_NUMBERS)
AddCheckbox(interface, CLASSICAPI_SEPARATE_LARGE_NUMBERS, CLASSICAPI_SEPARATE_LARGE_NUMBERS_TOOLTIP, CVarSetting("breakUpLargeNumbers"))
AddHeader(interface, CLASSICAPI_MOTION)
AddNumberBox(interface, CLASSICAPI_UI_SHAKE_STRENGTH, CLASSICAPI_UI_SHAKE_STRENGTH_TOOLTIP, NumberCVarSetting("ShakeStrengthUI"))
-- ---------------------------------------------------------------------------
-- Text To Speech
-- ---------------------------------------------------------------------------
local tts = NewPage(TEXT_TO_SPEECH, main.name, TEXT_TO_SPEECH)
local tts = NewPage(TEXT_TO_SPEECH, main.name)
AddDropDown(tts, TEXT_TO_SPEECH_VOICE_OPTION, TEXT_TO_SPEECH_VOICE_OPTION, NumberCVarSetting("ttsVoice"), function()
local options = {}
local choices = {}
for _, voice in ipairs(C_VoiceChat.GetTtsVoices()) do
table.insert(options, { value = voice.voiceID, text = voice.name })
table.insert(choices, { value = voice.voiceID, text = voice.name })
end
return options
return choices
end)
AddSlider(tts, TEXT_TO_SPEECH_ADJUST_RATE, TEXT_TO_SPEECH_ADJUST_RATE, NumberCVarSetting("ttsSpeed"), -10, 10)
AddSlider(tts, TEXT_TO_SPEECH_ADJUST_VOLUME, TEXT_TO_SPEECH_ADJUST_VOLUME, NumberCVarSetting("ttsVolume"), 0, 100)
AddSlider(tts, TEXT_TO_SPEECH_ADJUST_RATE, TEXT_TO_SPEECH_ADJUST_RATE, NumberCVarSetting("ttsSpeed"), -10, 10, 1)
AddSlider(tts, TEXT_TO_SPEECH_ADJUST_VOLUME, TEXT_TO_SPEECH_ADJUST_VOLUME, NumberCVarSetting("ttsVolume"), 0, 100, 1)
-- ---------------------------------------------------------------------------
-- Player Cache
-- Advanced
-- ---------------------------------------------------------------------------
local playerCache = NewPage(L.PLAYER_CACHE, main.name, L.PLAYER_CACHE)
AddCheckbox(playerCache, L.REMEMBER_PLAYERS, L.REMEMBER_PLAYERS_TOOLTIP, rememberSetting)
local advanced = NewPage(CLASSICAPI_ADVANCED, main.name)
AddCheckbox(advanced, CLASSICAPI_REMEMBER_PLAYERS, CLASSICAPI_REMEMBER_PLAYERS_TOOLTIP, rememberSetting)
AddNumberBox(advanced, CLASSICAPI_COMBAT_LOG_RETENTION, CLASSICAPI_COMBAT_LOG_RETENTION_TOOLTIP, NumberCVarSetting("combatLogRetentionTime"))
AddCheckbox(advanced, CLASSICAPI_SCRIPT_PROFILE, CLASSICAPI_SCRIPT_PROFILE_TOOLTIP, CVarSetting("scriptProfile"))
@@ -0,0 +1,76 @@
-- Backport of retail's Blizzard_FrameXMLUtil/AnimationSystem.lua.
--[[ animTable = { --Note that only consistent data should be in here. These tables are meant to be shared across "sessions" of animations. Put changing data in the frame.
totalTime = number, --Time to complete the animation in seconds.
updateFunc = function, --The function called to do the actual change. Takes self, elapsed fraction. Usually frame.SetPoint, frame.SetAlpha, ect.
getPosFunc = function, --The function returning the data being passed into updateFunc. For example. might return .18 if updateFunc is frame.SetAlpha.
--]]
local AnimatingFrames = {};
local AnimUpdateFrame = CreateFrame("Frame");
local function Animation_UpdateFrame(self, animElapsed, animTable)
local totalTime = animTable.totalTime
if ( animElapsed and (animElapsed < totalTime)) then --Should be animating
local elapsedFraction = self.animReverse and (1-animElapsed/totalTime) or (animElapsed/totalTime);
animTable.updateFunc(self, animTable.getPosFunc(self, elapsedFraction));
else --Just finished animating
animTable.updateFunc(self, animTable.getPosFunc(self, self.animReverse and 0 or 1));
self.animating = false;
AnimatingFrames[self][animTable.updateFunc] = 0; --We use 0 instead of nil'ing out because we don't want to mess with 'next' (used in pairs)
if ( self.animPostFunc ) then
self.animPostFunc(self);
end
end
end
local totalElapsed = 0;
local function Animation_OnUpdate(self, elapsed)
totalElapsed = totalElapsed + elapsed;
local isAnyFrameAnimating = false;
for frame, frameTable in pairs(AnimatingFrames) do
for _, animTable in pairs(frameTable) do
if ( animTable ~= 0 ) then
Animation_UpdateFrame(frame, totalElapsed - frame.animStartTime, animTable);
isAnyFrameAnimating = true;
end
end
end
if ( not isAnyFrameAnimating ) then
table.wipe(AnimatingFrames);
AnimUpdateFrame:SetScript("OnUpdate", nil);
end
end
function SetUpAnimation(frame, animTable, postFunc, reverse)
if ( type(animTable.updateFunc) == "string" ) then
animTable.updateFunc = frame[animTable.updateFunc];
end
if ( not AnimatingFrames[frame] ) then
AnimatingFrames[frame] = {};
end
AnimatingFrames[frame][animTable.updateFunc] = animTable;
frame.animStartTime = totalElapsed;
frame.animReverse = reverse;
frame.animPostFunc = postFunc;
frame.animating = true;
animTable.updateFunc(frame, animTable.getPosFunc(frame, frame.animReverse and 1 or 0));
AnimUpdateFrame:SetScript("OnUpdate", Animation_OnUpdate);
end
function CancelAnimations(frame)
local anims = AnimatingFrames[frame];
if ( anims ) then
for k, v in pairs(anims) do
anims[k] = 0;
end
frame.animating = false;
end
end
@@ -0,0 +1,114 @@
-- Backport of retail's Blizzard_ObjectAPI/Mainline/AsyncCallbackSystem.lua.
--[[
Queries some data retrieval API (specifically where the data may not be currently available) and when it becomes available
calls a user-supplied function. The callback can be canceled if necessary (e.g. the frame that would use the data becomes
hidden before the data arrives).
The API is managed so that arbitrary query functions cannot be executed.
--]]
AsyncCallbackAPIType = {
ASYNC_QUEST = 1,
ASYNC_ITEM = 2,
ASYNC_SPELL = 3,
}
local permittedAPI =
{
[AsyncCallbackAPIType.ASYNC_QUEST] = { event = "QUEST_DATA_LOAD_RESULT", accessor = C_QuestLog.RequestLoadQuestByID },
[AsyncCallbackAPIType.ASYNC_ITEM] = { event = "ITEM_DATA_LOAD_RESULT", accessor = C_Item.RequestLoadItemDataByID },
[AsyncCallbackAPIType.ASYNC_SPELL] = { event = "SPELL_DATA_LOAD_RESULT", accessor = C_Spell.RequestLoadSpellData },
};
AsyncCallbackSystemMixin = {};
function AsyncCallbackSystemMixin:Init(apiType)
self.callbacks = {};
-- API Type should be set up from key value pairs before OnLoad.
self.api = permittedAPI[apiType];
self:SetScript("OnEvent",
function(self, event, ...)
if event == self.api.event then
local id, success = ...;
if success or apiType == AsyncCallbackAPIType.ASYNC_ITEM then
self:FireCallbacks(id, success);
else
self:ClearCallbacks(id);
end
end
end
);
self:RegisterEvent(self.api.event);
end
local CANCELED_SENTINEL = -1;
function AsyncCallbackSystemMixin:AddCallback(id, callbackFunction)
local callbacks = self:GetOrCreateCallbacks(id);
table.insert(callbacks, callbackFunction);
local needsAccessorCall = #callbacks == 1;
if needsAccessorCall then
self.api.accessor(id);
end
return #callbacks, callbacks;
end
function AsyncCallbackSystemMixin:AddCancelableCallback(id, callbackFunction)
-- NOTE: If the data is currently availble then the callback will be executed and callbacks cleared, so there will be nothing to cancel.
local index, callbacks = self:AddCallback(id, callbackFunction);
return function()
if #callbacks > 0 and callbacks[index] ~= CANCELED_SENTINEL then
callbacks[index] = CANCELED_SENTINEL;
return true;
end
return false;
end;
end
function AsyncCallbackSystemMixin:FireCallbacks(id, success)
local callbacks = self:GetCallbacks(id);
if callbacks then
self:ClearCallbacks(id);
for i, callback in ipairs(callbacks) do
if callback ~= CANCELED_SENTINEL then
xpcall(callback, CallErrorHandler, id, success);
end
end
-- The cancel functions have a reference to this table, so ensure that it's cleared out.
for i = #callbacks, 1, -1 do
callbacks[i] = nil;
end
end
end
function AsyncCallbackSystemMixin:ClearCallbacks(id)
self.callbacks[id] = nil;
end
function AsyncCallbackSystemMixin:GetCallbacks(id)
return self.callbacks[id];
end
function AsyncCallbackSystemMixin:GetOrCreateCallbacks(id)
local callbacks = self.callbacks[id];
if not callbacks then
callbacks = {};
self.callbacks[id] = callbacks;
end
return callbacks;
end
local function CreateListener(apiType)
local listener = Mixin(CreateFrame("Frame"), AsyncCallbackSystemMixin);
listener:Init(apiType);
return listener;
end
ItemEventListener = CreateListener(AsyncCallbackAPIType.ASYNC_ITEM);
SpellEventListener = CreateListener(AsyncCallbackAPIType.ASYNC_SPELL);
QuestEventListener = CreateListener(AsyncCallbackAPIType.ASYNC_QUEST);
@@ -1,214 +1,267 @@
-- Backport of 3.3.5 FrameXML/CallbackRegistryMixin.lua to vanilla 1.12 / Lua 5.0.
-- Backport of retail Blizzard_SharedXMLBase/CallbackRegistry.lua to vanilla 1.12 / Lua 5.0.
--
-- Implementation differences from the 3.3.5 source:
-- - 1.12 has no taint or Secure-Templates system, so the entire
-- AttributeDelegate/SetForbidden machinery for inserting event keys
-- into the callback tables is unnecessary. Sub-tables are allocated
-- lazily inside RegisterCallback instead.
-- - securecallfunction/secureexecuterange don't exist; they reduce to
-- direct calls / explicit for-loops here.
-- - GenerateClosure doesn't exist in 1.12; provide a local one that
-- captures registration-time varargs via SafePack-style storage.
-- - Lua 5.0 has no select() or `wipe()`; we use the implicit `arg`
-- table and assign-nil to clear tables.
-- Implementation differences from the retail source:
-- - No AttributeDelegate. Retail inserts event keys and creates deferred
-- callback tables through a forbidden frame's OnAttributeChanged, as a
-- taint barrier. 1.12 has no taint and no SetForbidden, so
-- SecureInsertEvent and GetMutableCallbacksByEvent do that work directly.
-- - TriggerEvent calls securecallfunction from its own loops, where retail
-- passes a vararg helper to secureexecuterange. The contract is the same
-- (an error goes to the error handler and the other callbacks still run),
-- but every call to a vararg function builds an `arg` table in Lua 5.0,
-- which would cost a table per callback per event.
-- - 1.12 has no global wipe, so UnregisterEvents uses table.wipe.
local generateOwnerID = CreateCounter()
local secureexecuterange = secureexecuterange;
local securecallfunction = securecallfunction;
local unpack = unpack;
local error = error;
local pairs = pairs;
local rawset = rawset;
local next = next;
local wipe = table.wipe;
local GetOrCreateTableEntry = GetOrCreateTableEntry;
local CallbackType = EnumUtil.MakeEnum("Closure", "Function")
local CallbackType = EnumUtil.MakeEnum("Closure", "Function");
-- Local GenerateClosure: binds `func` plus all of `...` so a later call
-- invokes func(boundArgs..., callArgs...). Mirrors 3.3.5's helper of the
-- same name. SafePack/SafeUnpack semantics so embedded nils survive.
local function GenerateClosure(func, ...)
local boundN = arg.n
local bound = {}
for i = 1, boundN do bound[i] = arg[i] end
return function(...)
local callN = arg.n
-- Forward bound + call args faithfully, embedded/leading nils and all.
-- The ONLY thing that makes unpack span holes on this Lua 5.0 build is
-- a `.n` field: unpack takes its count from getn (which returns t.n)
-- and ignores any explicit i/j, so a `.n`-less table stops dead at the
-- first hole (verified: unpack({1, 1, nil, 1}, 1, 4) yields just 1, 1).
local all = {}
local n = 0
for i = 1, boundN do n = n + 1; all[n] = bound[i] end
for i = 1, callN do n = n + 1; all[n] = arg[i] end
all.n = n
return func(unpack(all))
end
end
-- Callbacks can be registered without an owner as a matter of convenience. Generally this is fine when you never
-- intend to release the callback.
local generateOwnerID = CreateCounter();
CallbackRegistryMixin = CallbackRegistryMixin or {}
CallbackRegistryMixin = {};
function CallbackRegistryMixin:OnLoad()
local callbackTables = {}
for _, value in pairs(CallbackType) do
callbackTables[value] = {}
end
self.callbackTables = callbackTables
local callbackTables = {};
for callbackType, value in pairs(CallbackType) do
local callbacks = {};
callbackTables[value] = callbacks;
end
self.callbackTables = callbackTables;
self.executingEvents = {};
self.deferredCallbacks = {};
end
function CallbackRegistryMixin:SetUndefinedEventsAllowed(allowed)
self.isUndefinedEventAllowed = allowed
self.isUndefinedEventAllowed = allowed;
end
function CallbackRegistryMixin:GetCallbackTables()
return self.callbackTables
return self.callbackTables;
end
function CallbackRegistryMixin:GetCallbackTable(callbackType)
return self.callbackTables[callbackType]
return self.callbackTables[callbackType];
end
function CallbackRegistryMixin:GetCallbacksByEvent(callbackType, event)
local callbackTable = self:GetCallbackTable(callbackType)
return callbackTable[event]
local callbackTable = self:GetCallbackTable(callbackType);
return callbackTable[event];
end
-- Returns either the underlying callback table or a temporary table depending
-- on if the event is currently being dispatched. The callbacks stored in the
-- temporary table will be merged into the underlying callback table after the
-- dispatch is complete.
function CallbackRegistryMixin:GetMutableCallbacksByEvent(callbackType, event)
if securecallfunction(rawget, self.executingEvents, event) == nil then
return self:GetCallbacksByEvent(callbackType, event);
end
local deferrals = GetOrCreateTableEntry(self.deferredCallbacks, event);
return GetOrCreateTableEntry(deferrals, callbackType);
end
function CallbackRegistryMixin:HasRegistrantsForEvent(event)
for _, callbackTable in pairs(self:GetCallbackTables()) do
local callbacks = callbackTable[event]
if callbacks and next(callbacks) then
return true
end
end
return false
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
local callbacks = callbackTable[event];
if callbacks and securecallfunction(next, callbacks) then
return true;
end
end
return false;
end
-- Lazy allocation of per-event sub-tables. In 3.3.5 this went through a
-- forbidden attribute frame for taint-isolation reasons; that's a no-op
-- consideration on 1.12, so we just assign here.
function CallbackRegistryMixin:SecureInsertEvent(event)
for _, callbackTable in pairs(self:GetCallbackTables()) do
if not callbackTable[event] then
callbackTable[event] = {}
end
end
if not self:HasRegistrantsForEvent(event) then
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
if not callbackTable[event] then
rawset(callbackTable, event, {});
end
end
end
end
function CallbackRegistryMixin:RegisterCallback(event, func, owner, ...)
if type(event) ~= "string" then
error("CallbackRegistryMixin::RegisterCallback 'event' requires string type.")
elseif type(func) ~= "function" then
error("CallbackRegistryMixin::RegisterCallback 'func' requires function type.")
else
if owner == nil then
owner = generateOwnerID()
elseif type(owner) == "number" then
error("CallbackRegistryMixin:RegisterCallback 'owner' as number is reserved internally.")
end
end
if type(event) ~= "string" then
error("CallbackRegistryMixin::RegisterCallback 'event' requires string type.");
elseif type(func) ~= "function" then
error("CallbackRegistryMixin::RegisterCallback 'func' requires function type.");
else
if owner == nil then
owner = generateOwnerID();
elseif type(owner) == "number" then
error("CallbackRegistryMixin:RegisterCallback 'owner' as number is reserved internally.")
end
end
self:SecureInsertEvent(event)
-- Taint barrier for inserting event key into callback tables.
self:SecureInsertEvent(event);
for _, callbackTable in pairs(self:GetCallbackTables()) do
local callbacks = callbackTable[event]
callbacks[owner] = nil
end
-- An owner can have a single callback per event. The simpliest way to ensure
-- this is to remove all callbacks for the owner prior to new registration.
self:UnregisterCallback(event, owner);
if arg.n > 0 then
local callbacks = self:GetCallbacksByEvent(CallbackType.Closure, event)
callbacks[owner] = GenerateClosure(func, owner, unpack(arg))
else
local callbacks = self:GetCallbacksByEvent(CallbackType.Function, event)
callbacks[owner] = func
end
local count = select("#", ...);
if count > 0 then
local callbacks = self:GetMutableCallbacksByEvent(CallbackType.Closure, event);
callbacks[owner] = GenerateClosure(func, owner, ...);
else
local callbacks = self:GetMutableCallbacksByEvent(CallbackType.Function, event);
callbacks[owner] = func;
end
return owner
return owner;
end
local function CreateCallbackHandle(cbr, event, owner)
local handle =
{
Unregister = function()
cbr:UnregisterCallback(event, owner)
end,
}
return handle
-- Wrapped in a table for future flexibility.
local handle =
{
Unregister = function()
cbr:UnregisterCallback(event, owner);
end,
};
return handle;
end
function CallbackRegistryMixin:RegisterCallbackWithHandle(event, func, owner, ...)
owner = self:RegisterCallback(event, func, owner, unpack(arg))
return CreateCallbackHandle(self, event, owner)
owner = self:RegisterCallback(event, func, owner, ...);
return CreateCallbackHandle(self, event, owner);
end
function CallbackRegistryMixin:ReconcileDeferredCallbacks(event, closures, funcs)
self:CopyDeferredCallbacks(CallbackType.Closure, event, closures);
self:CopyDeferredCallbacks(CallbackType.Function, event, funcs);
self.deferredCallbacks[event] = nil;
end
function CallbackRegistryMixin:CopyDeferredCallbacks(callbackType, event, target)
local deferrals = self.deferredCallbacks[event];
if deferrals == nil then
return;
end
local callbacks = deferrals[callbackType];
if callbacks == nil then
return;
end
local function ExecuteAssignCallback(owner, callback)
target[owner] = callback;
end
secureexecuterange(callbacks, ExecuteAssignCallback);
end
function CallbackRegistryMixin:TriggerEvent(event, ...)
if type(event) ~= "string" then
error("CallbackRegistryMixin:TriggerEvent 'event' requires string type.")
elseif not self.isUndefinedEventAllowed and not (self.Event and self.Event[event]) then
error(string.format("CallbackRegistryMixin:TriggerEvent event '%s' doesn't exist.", event))
end
if type(event) ~= "string" then
error("CallbackRegistryMixin:TriggerEvent 'event' requires string type.");
elseif not self.isUndefinedEventAllowed and not self.Event[event] then
error(string.format("CallbackRegistryMixin:TriggerEvent event '%s' doesn't exist.", event));
end
local closures = self:GetCallbacksByEvent(CallbackType.Closure, event)
if closures then
for _, closure in pairs(closures) do
closure(unpack(arg))
end
end
-- TriggerEvent appears to need to support reentrant calls for now.
local count = (self.executingEvents[event] or 0) + 1;
local funcs = self:GetCallbacksByEvent(CallbackType.Function, event)
if funcs then
for owner, func in pairs(funcs) do
-- Stock 3.3.5 passes the owner as the first arg so callers can
-- distinguish multiple registrations of the same function. `arg`
-- carries its own `.n`, so unpack(arg) forwards the payload with
-- leading/embedded nils intact. Do NOT instead merge into a
-- `{ owner, ... }` table and unpack that WITHOUT setting `.n`: this
-- build's unpack counts via getn, so a `.n`-less table collapses at
-- the first hole (verified in-game: reload's PLAYER_ENTERING_WORLD,
-- arg1=nil arg2=1, dropped the payload).
func(owner, unpack(arg))
end
end
-- Set before invoking any callback so a reentrant call does not
-- attempt to call ReconcileDeferredCallbacks.
self.executingEvents[event] = count;
local closures = self:GetCallbacksByEvent(CallbackType.Closure, event);
if closures then
for owner, closure in pairs(closures) do
securecallfunction(closure, ...);
end
end
local funcs = self:GetCallbacksByEvent(CallbackType.Function, event);
if funcs then
for owner, func in pairs(funcs) do
securecallfunction(func, owner, ...);
end
end
-- The local count is the only value we care about for the purpose
-- of flushing the event key and reconciling the deferred callbacks.
if count == 1 then
self.executingEvents[event] = nil;
self:ReconcileDeferredCallbacks(event, closures, funcs);
end
end
function CallbackRegistryMixin:UnregisterCallback(event, owner)
if type(event) ~= "string" then
error("CallbackRegistryMixin:UnregisterCallback 'event' requires string type.")
elseif owner == nil then
error("CallbackRegistryMixin:UnregisterCallback 'owner' is required.")
end
if type(event) ~= "string" then
error("CallbackRegistryMixin:UnregisterCallback 'event' requires string type.");
elseif owner == nil then
error("CallbackRegistryMixin:UnregisterCallback 'owner' is required.");
end
for _, callbackTable in pairs(self:GetCallbackTables()) do
local callbacks = callbackTable[event]
if callbacks then
callbacks[owner] = nil
end
end
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
-- Only assign nil if the owner is present in the table, otherwise
-- its insertion could cause a rehash during iteration by secureexecuterange.
local callbacks = callbackTable[event];
if callbacks and callbacks[owner] then
callbacks[owner] = nil;
end
end
local deferrals = self.deferredCallbacks[event];
if deferrals then
for callbackType, callbacks in pairs(deferrals) do
-- Freely assign nil because deferredCallbacks are not subject
-- to secureexecuterange table mutilation stress disorder.
callbacks[owner] = nil;
end
end
end
function CallbackRegistryMixin:UnregisterEvents(eventTable)
if eventTable then
for _, callbackTable in pairs(self:GetCallbackTables()) do
for event in pairs(eventTable) do
if callbackTable[event] then
callbackTable[event] = nil
end
end
end
else
for _, callbackTable in pairs(self:GetCallbackTables()) do
for k in pairs(callbackTable) do
callbackTable[k] = nil
end
end
end
function CallbackRegistryMixin:UnregisterEvents()
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
wipe(callbackTable);
end
end
function CallbackRegistryMixin:GenerateCallbackEvents(events)
if not self.Event then
self.Event = {}
end
function CallbackRegistryMixin:UnregisterEventsByEventTable(eventTable)
if type(eventTable) ~= "table" then
error("CallbackRegistryMixin:UnregisterEventsByEventTable 'eventTable' requires table type.");
end
for eventIndex, eventName in ipairs(events) do
if self.Event[eventName] then
error(string.format("CallbackRegistryMixin:GenerateCallbackEvents: event '%s' already exists.", eventName))
end
self.Event[eventName] = eventName
end
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
for event in pairs(eventTable) do
callbackTable[event] = nil;
end
end
end
function CallbackRegistryMixin:GenerateCallbackEvents(eventTable)
if not self.Event then
self.Event = {};
end
if type(eventTable) ~= "table" then
error("CallbackRegistryMixin:GenerateCallbackEvents 'eventTable' requires table type.");
end
for eventIndex, eventName in ipairs(eventTable) do
if self.Event[eventName] then
error(string.format("CallbackRegistryMixin:GenerateCallbackEvents: event '%s' already exists.", eventName));
end
self.Event[eventName] = eventName;
end
end
function CallbackRegistryMixin.DoesFrameHaveEvent(frame, event)
return frame.Event and frame.Event[event]
return frame.Event and frame.Event[event];
end
@@ -132,7 +132,7 @@ local function ApplyItemQualityColors()
end
function CAPI_ApplyStandardColorGlobals()
-- `C_UIColor.GetColors()` makes a color object for each of its 187 rows,
-- `C_UIColor.GetColors()` makes a color object for each of its 331 rows,
-- so keep the values from the first call. Each later pass is then one
-- lookup per tag.
if not standardColors then
@@ -87,11 +87,7 @@ function GetClassColoredTextForUnit(unit, text)
end
function GetFactionColor(factionGroupTag)
-- Both PLAYER_FACTION_GROUP (tag -> index) and PLAYER_FACTION_COLORS
-- (index -> color) live in our Constants.lua. Guarded defensively in
-- case a client variant leaves one unset.
local group = PLAYER_FACTION_GROUP[factionGroupTag];
return PLAYER_FACTION_COLORS and PLAYER_FACTION_COLORS[group];
return PLAYER_FACTION_COLORS[PLAYER_FACTION_GROUP[factionGroupTag]];
end
function RGBToColorCode(r, g, b)
@@ -117,12 +117,6 @@ FACTION_LABELS_FROM_STRING = {
["Alliance"] = FACTION_ALLIANCE,
}
PLAYER_FACTION_COLORS =
{
[0] = PLAYER_FACTION_COLOR_HORDE,
[1] = PLAYER_FACTION_COLOR_ALLIANCE,
};
--
-- Glyph
--
@@ -0,0 +1,22 @@
-- Backport of the bag-count globals from retail's
-- Blizzard_UIPanels_Game/Mainline/ContainerFrame.lua.
--
-- Differences from the retail source:
-- - NUM_TOTAL_BAG_FRAMES is NUM_BAG_FRAMES: there is no reagent bag.
-- - ContainerFrame_GetContainerNumSlots gives the backpack no extra slots.
-- Retail counts 4 more for an account without an authenticator; here
-- the backpack's size does not depend on the account.
NUM_TOTAL_BAG_FRAMES = NUM_BAG_FRAMES;
function ContainerFrame_GetContainerNumSlots(bagId)
if bagId == KEYRING_CONTAINER then
local keyRingSize = GetKeyRingSize();
return keyRingSize, keyRingSize;
end
local currentNumSlots = C_Container.GetContainerNumSlots(bagId);
local maxNumSlots = currentNumSlots;
return maxNumSlots, currentNumSlots;
end
@@ -0,0 +1,114 @@
-- Backport of retail's Blizzard_ObjectAPI/Mainline/ContinuableContainer.lua.
ContinuableContainer = {};
--[[
Expected usage:
local continuableContainer = ContinuableContainer:Create();
local item = Item:CreateFromItemLocation(itemLoc);
continuableContainer:AddContinuable(item);
... add more continuables
continuableContainer:ContinueOnLoad(function()
-- code to execute once all continuables are ready, could be immediate
end);
]]
--[[static]] function ContinuableContainer:Create()
return CreateFromMixins(self);
end
function ContinuableContainer:AddContinuables(tbl)
for index, continuable in pairs(tbl) do
self:AddContinuable(continuable);
end
end
function ContinuableContainer:AddContinuable(continuable)
if not self.continuables or not self.evictableObjects then
self.continuables = {};
self.evictableObjects = {};
self.onContinuableLoadedCallback = function(id, success)
self.numOutstanding = self.numOutstanding - 1;
if not success then
for i, evictableObject in ipairs(self.evictableObjects) do
if evictableObject:GetItemID() == id then
table.remove(self.evictableObjects, i);
break;
end
end
end
self:CheckIfSatisfied();
end;
end
self.numOutstanding = self:GetNumOutstandingLoads() + 1;
if continuable:IsDataEvictable() then
table.insert(self.evictableObjects, continuable);
end
table.insert(self.continuables, continuable:ContinueWithCancelOnRecordLoad(self.onContinuableLoadedCallback));
end
function ContinuableContainer:ContinueOnLoad(callbackFunction)
if type(callbackFunction) ~= "function" then
error("Usage: ContinuableContainer:ContinueOnLoad(callbackFunction)", 2);
end
self.callbackFunction = callbackFunction;
return self:CheckIfSatisfied();
end
function ContinuableContainer:GetNumOutstandingLoads()
return self.numOutstanding or 0;
end
function ContinuableContainer:AreAnyLoadsOutstanding()
return self:GetNumOutstandingLoads() > 0;
end
function ContinuableContainer:Cancel()
local continuables = self.continuables;
if continuables then
self.continuables = nil;
self.evictableObjects = nil;
self.numOutstanding = nil;
for i, continuable in ipairs(continuables) do
continuable();
end
end
end
-- "private"
function ContinuableContainer:CheckIfSatisfied()
if not self:AreAnyLoadsOutstanding() and self.callbackFunction and self:RecheckEvictableContinuables() then
local callbackFunction = self.callbackFunction;
self.callbackFunction = nil;
self.continuables = nil;
self.evictableObjects = nil;
xpcall(callbackFunction, CallErrorHandler);
return true;
end
return false;
end
function ContinuableContainer:RecheckEvictableContinuables()
local areAllLoaded = true;
if self.evictableObjects then
for i, evictableObject in ipairs(self.evictableObjects) do
if not evictableObject:IsRecordDataCached() then
areAllLoaded = false;
self.numOutstanding = self.numOutstanding + 1;
table.insert(self.continuables, evictableObject:ContinueWithCancelOnRecordLoad(self.onContinuableLoadedCallback));
end
end
end
return areAllLoaded;
end
@@ -1,14 +1,12 @@
-- Backport of 3.3.5 FrameXML/EventUtil.lua to vanilla 1.12 / Lua 5.0.
-- Backport of retail Blizzard_SharedXML/EventUtil.lua to vanilla 1.12 / Lua 5.0.
--
-- Implementation differences from the 3.3.5 source:
-- - Lua 5.0 has no select(); we work off the implicit `arg` table
-- in vararg functions.
-- - 1.12 has no C_AddOns; the legacy global IsAddOnLoaded(name)
-- fills the same role.
-- - 1.12 has no secureexecuterange; queued callbacks are dispatched
-- by a plain ipairs loop.
-- - AreVariablesLoaded in 3.3.5 sniffs GlueParent / UIParent.firstTimeLoaded.
-- 1.12 has neither; we track a private flag flipped on VARIABLES_LOADED.
-- Implementation differences from the retail source:
-- - Retail's UIParent sets UIParent.variablesLoaded and calls
-- EventUtil.TriggerOnVariablesLoaded on VARIABLES_LOADED. 1.12's
-- FrameXML does neither, so the frame at the bottom of this file does
-- both.
-- - Varargs are read from the implicit `arg` table, which every Lua 5.0
-- vararg function already has, rather than walked with select().
EventUtil = {}
@@ -19,7 +17,7 @@ function ContinueAfterAllEventsMixin:Init(callback, ...)
for index = 1, arg.n do
local event = arg[index]
assert(C_EventUtils.IsEventValid(event), "Unknown event \"" .. event .. "\"")
assert(C_EventUtils.IsEventValid(event), ("Unknown event \"%s\""):format(event))
self.events[event] = false
local function OnEventReceived()
@@ -50,13 +48,8 @@ function EventUtil.ContinueAfterAllEvents(callback, ...)
obj:Init(callback, unpack(arg))
end
-- Tracked locally because 1.12 has no GlueParent / UIParent.firstTimeLoaded.
-- Bootstrap frame at the bottom of the file listens for VARIABLES_LOADED
-- and flips the flag, then drains the queued callbacks via the
-- TriggerOnVariablesLoaded path.
local variablesLoaded = false
function EventUtil.AreVariablesLoaded()
return variablesLoaded
return GlueParent or (UIParent and UIParent.variablesLoaded)
end
local eventUtilVariablesLoadedCallbacks = {}
@@ -69,25 +62,26 @@ function EventUtil.ContinueOnVariablesLoaded(callback)
table.insert(eventUtilVariablesLoadedCallbacks, callback)
end
local secureexecuterange = secureexecuterange
local eventUtilContinueOnVariablesLoadedTriggered = false
function EventUtil.TriggerOnVariablesLoaded()
if eventUtilContinueOnVariablesLoadedTriggered then
-- If TriggerOnVariablesLoaded has been called once already, don't do anything further.
return
end
eventUtilContinueOnVariablesLoadedTriggered = true
variablesLoaded = true
for _, callback in ipairs(eventUtilVariablesLoadedCallbacks) do
secureexecuterange(eventUtilVariablesLoadedCallbacks, function(_, callback)
callback()
end
end)
end
function EventUtil.ContinueOnAddOnLoaded(addOnName, callback)
local isLoadedOrLoading, isLoaded = C_AddOns.IsAddOnLoaded(addOnName)
if isLoaded then
callback();
return;
end
local isLoadedOrLoading, isLoaded = C_AddOns.IsAddOnLoaded(addOnName)
if isLoaded then
callback()
return
end
EventUtil.RegisterOnceFrameEventAndCallback("ADDON_LOADED", callback, addOnName)
end
@@ -125,6 +119,7 @@ function CallbackHandleContainerMixin:Init()
self.handles = {}
end
-- Accepts any CBR and stores a handle to the registered callback.
function CallbackHandleContainerMixin:RegisterCallback(cbr, event, callback, owner)
self:AddHandle(cbr:RegisterCallbackWithHandle(event, callback, owner))
end
@@ -141,7 +136,7 @@ function CallbackHandleContainerMixin:Unregister()
end
function CallbackHandleContainerMixin:IsEmpty()
return table.getn(self.handles) == 0
return #self.handles == 0
end
function EventUtil.CreateCallbackHandleContainer()
@@ -150,13 +145,13 @@ function EventUtil.CreateCallbackHandleContainer()
return cbrHandles
end
-- VARIABLES_LOADED bootstrap. Drains anything queued via
-- ContinueOnVariablesLoaded the first time the event fires. We use a
-- dedicated frame so we don't pollute EventRegistry's payload routing.
-- Retail UIParent's VARIABLES_LOADED work for EventUtil: mark the variables
-- loaded, then run what ContinueOnVariablesLoaded queued.
do
local f = CreateFrame("Frame")
f:RegisterEvent("VARIABLES_LOADED")
f:SetScript("OnEvent", function()
UIParent.variablesLoaded = true
EventUtil.TriggerOnVariablesLoaded()
f:UnregisterEvent("VARIABLES_LOADED")
end)
@@ -0,0 +1,149 @@
-- Backport of retail's Blizzard_SharedXMLBase/ExportUtil.lua.
-- Utility for compressing and uncompressing data into strings for export and import.
ExportUtil = {};
local BitsPerChar = 6;
local function MakeBase64ConversionTable()
local base64ConversionTable = {};
base64ConversionTable[0] = 'A';
for num = 1, 25 do
table.insert(base64ConversionTable, string.char(65 + num));
end
for num = 0, 25 do
table.insert(base64ConversionTable, string.char(97 + num));
end
for num = 0, 9 do
table.insert(base64ConversionTable, tostring(num));
end
table.insert(base64ConversionTable, '+');
table.insert(base64ConversionTable, '/');
return base64ConversionTable;
end
local NumberToBase64CharConversionTable = MakeBase64ConversionTable();
local Base64CharToNumberConversionTable = tInvert(MakeBase64ConversionTable());
function ExportUtil.ConvertToBase64(dataEntries)
local exportString = "";
local currentValue = 0;
local currentReservedBits = 0;
local totalBits = 0;
for i, dataEntry in ipairs(dataEntries) do
local remainingValue = dataEntry.value;
local remainingRequiredBits = dataEntry.bitWidth;
-- TODO: bit.lshift doesnt work with > 32 bits. Maybe use maxValue = 2^X instead?
local maxValue = bit.lshift(1, remainingRequiredBits);
if remainingValue >= maxValue then
error(("Data entry has higher value than storable in bitWidth. (%d in %d bits)"):format(remainingValue, remainingRequiredBits));
return "";
end
totalBits = totalBits + remainingRequiredBits;
while remainingRequiredBits > 0 do
local spaceInCurrentValue = (BitsPerChar - currentReservedBits);
local maxStorableValue = bit.lshift(1, spaceInCurrentValue);
local remainder = remainingValue % maxStorableValue;
remainingValue = bit.rshift(remainingValue, spaceInCurrentValue);
currentValue = currentValue + bit.lshift(remainder, currentReservedBits);
if spaceInCurrentValue > remainingRequiredBits then
currentReservedBits = (currentReservedBits + remainingRequiredBits) % BitsPerChar;
remainingRequiredBits = 0;
else
exportString = exportString..NumberToBase64CharConversionTable[currentValue];
currentValue = 0;
currentReservedBits = 0;
remainingRequiredBits = remainingRequiredBits - spaceInCurrentValue;
end
end
end
if currentReservedBits > 0 then
exportString = exportString..NumberToBase64CharConversionTable[currentValue];
end
return exportString;
end
function ExportUtil.ConvertFromBase64(exportString)
local dataValues = {};
for i = 1, #exportString do
table.insert(dataValues, Base64CharToNumberConversionTable[string.sub(exportString, i, i)]);
end
return dataValues;
end
function ExportUtil.MakeExportDataStream()
return CreateAndInitFromMixin(ExportDataStreamMixin);
end
function ExportUtil.MakeImportDataStream(exportString)
return CreateAndInitFromMixin(ImportDataStreamMixin, exportString);
end
ExportDataStreamMixin = {};
function ExportDataStreamMixin:Init()
self.dataEntries = {};
end
function ExportDataStreamMixin:AddValue(bitWidth, value)
table.insert(self.dataEntries, { bitWidth = bitWidth, value = value, });
end
function ExportDataStreamMixin:GetExportString()
return ExportUtil.ConvertToBase64(self.dataEntries);
end
ImportDataStreamMixin = {};
function ImportDataStreamMixin:Init(exportString)
self.dataValues = ExportUtil.ConvertFromBase64(exportString);
self.currentIndex = 1;
self.currentExtractedBits = 0;
self.currentRemainingValue = self.dataValues[1];
end
function ImportDataStreamMixin:ExtractValue(bitWidth)
if self.currentIndex > #self.dataValues then
return nil;
end
local value = 0;
local bitsNeeded = bitWidth;
local extractedBits = 0;
while bitsNeeded > 0 do
local remainingBits = BitsPerChar - self.currentExtractedBits;
local bitsToExtract = math.min(remainingBits, bitsNeeded);
self.currentExtractedBits = self.currentExtractedBits + bitsToExtract;
local maxStorableValue = bit.lshift(1, bitsToExtract);
local remainder = self.currentRemainingValue % maxStorableValue;
self.currentRemainingValue = bit.rshift(self.currentRemainingValue, bitsToExtract);
value = value + bit.lshift(remainder, extractedBits);
extractedBits = extractedBits + bitsToExtract;
bitsNeeded = bitsNeeded - bitsToExtract;
if bitsToExtract < remainingBits then
break;
elseif bitsToExtract >= remainingBits then
self.currentIndex = self.currentIndex + 1;
self.currentExtractedBits = 0;
self.currentRemainingValue = self.dataValues[self.currentIndex];
end
end
return value;
end
function ImportDataStreamMixin:GetNumberOfBits()
return BitsPerChar * getn(self.dataValues);
end
@@ -1,110 +1,244 @@
-- Backport of FrameXML/FormattingUtil.lua (feasible subset) to vanilla
-- 1.12 / Lua 5.0.
-- Backport of retail Blizzard_SharedXML/FormattingUtil.lua to vanilla 1.12 / Lua 5.0.
--
-- 5.0 notes: no `#`/`%`/string-method syntax — uses string.*, math.mod.
-- GetMoneyString gives the text-symbol form ("12g 34s 56c"). The coin-icon form
-- (GetCoinTextureString / C_CurrencyInfo.GetCoinTextureString) is provided by the
-- ClassicAPI DLL in C++ (src/currency/CoinText.cpp) — like retail, where it's a
-- C function — now that the inline-texture backport renders |T markup in 1.12.
-- Implementation differences from the retail source:
-- - GetMoneyString always gives the coin-icon form, retail's default. Retail
-- switches to the text-symbol form ("12g 34s 56c") under its colorblind
-- mode, which 1.12 does not have.
-- - Not ported: GetCurrencyString / GetCurrenciesString (1.12 has no
-- currencies).
--
-- The FrameXML format-string globals these functions consume
-- (LARGE_NUMBER_SEPERATOR, PERCENTAGE_STRING, GOLD_AMOUNT_SYMBOL,
-- GOLD_AMOUNT_TEXTURE, …) are defined in the locale layer (locales/enUS.lua),
-- which loads first.
-- The GlobalStrings these functions consume (LARGE_NUMBER_SEPERATOR,
-- PERCENTAGE_STRING, GOLD_AMOUNT_TEXTURE, SYMBOLIC_NEGATIVE_NUMBER, ...) are
-- defined in the locale layer (locales/*.lua), which loads first. The coin
-- formats there crop the one 1.12 sprite sheet, UI-MoneyIcons.
function SplitTextIntoLines(text, delimiter)
local lines = {};
local startIndex = 1;
local foundIndex = string.find(text, delimiter);
while foundIndex do
table.insert(lines, text:sub(startIndex, foundIndex - 1));
startIndex = foundIndex + 2;
foundIndex = string.find(text, delimiter, startIndex);
end
if startIndex <= #text then
table.insert(lines, text:sub(startIndex));
end
return lines;
end
function SplitTextIntoHeaderAndNonHeader(text)
local foundIndex = string.find(text, "|n");
if not foundIndex then
-- There was no newline...the whole thing is a header
return text;
elseif #text == 2 then
-- There was a newline, but that was all that was in the string.
return nil;
elseif foundIndex == 1 then
-- There was a newline at the very beginning...the whole rest of the string is a header
return text:sub(3);
elseif foundIndex == #text - 1 then
-- There was a newline at the very end...the whole rest of the string is a header
return text:sub(1, foundIndex - 1);
else
-- There was a newline somewhere in the middle...everything before it is the header and everything after it is the non-header
return text:sub(1, foundIndex - 1), text:sub(foundIndex + 2);
end
end
function FormatValueWithSign(value)
local formatString = value < 0 and SYMBOLIC_NEGATIVE_NUMBER or SYMBOLIC_POSITIVE_NUMBER;
return formatString:format(math.abs(value));
end
function FormatLargeNumber(amount)
amount = tostring(amount);
local newDisplay = "";
local strlen = amount:len();
--Add each thing behind a comma
for i=4, strlen, 3 do
newDisplay = LARGE_NUMBER_SEPERATOR..amount:sub(-(i - 1), -(i - 3))..newDisplay;
end
--Add everything before the first comma
newDisplay = amount:sub(1, (strlen % 3 == 0) and 3 or (strlen % 3))..newDisplay;
return newDisplay;
end
COPPER_PER_SILVER = 100;
SILVER_PER_GOLD = 100;
COPPER_PER_GOLD = COPPER_PER_SILVER * SILVER_PER_GOLD;
function FormatLargeNumber(amount)
amount = tostring(amount);
local newDisplay = "";
local strlen = string.len(amount);
-- Insert a separator before each group of three trailing digits.
for i = 4, strlen, 3 do
newDisplay = LARGE_NUMBER_SEPERATOR ..
string.sub(amount, -(i - 1), -(i - 3)) .. newDisplay;
end
-- Prepend everything before the first separator.
local lead = (math.mod(strlen, 3) == 0) and 3 or math.mod(strlen, 3);
newDisplay = string.sub(amount, 1, lead) .. newDisplay;
return newDisplay;
local COPPER_PER_SILVER = COPPER_PER_SILVER;
local SILVER_PER_GOLD = SILVER_PER_GOLD;
local COPPER_PER_GOLD = COPPER_PER_GOLD;
MoneyStringConstants =
{
SeparateThousands = true,
DontSeparateThousands = false,
CheckGoldThreshold = true,
DontCheckGoldThreshold = false,
ShowZeroAsGold = true,
ShowZeroAsLowerDenomination = false,
};
function GetMoneyString(money, separateThousands, checkGoldThreshold, showZeroAsGold, color)
local goldString, silverString, copperString;
local gold = floor(money / (COPPER_PER_SILVER * SILVER_PER_GOLD));
local silver = floor((money - (gold * COPPER_PER_SILVER * SILVER_PER_GOLD)) / COPPER_PER_SILVER);
local copper = mod(money, COPPER_PER_SILVER);
if (separateThousands) then
goldString = GOLD_AMOUNT_TEXTURE_STRING:format(FormatLargeNumber(gold), 0, 0);
else
goldString = GOLD_AMOUNT_TEXTURE:format(gold, 0, 0);
end
silverString = SILVER_AMOUNT_TEXTURE:format(silver, 0, 0);
copperString = COPPER_AMOUNT_TEXTURE:format(copper, 0, 0);
if showZeroAsGold and ( money == 0 ) then
return color and color:WrapTextInColorCode(goldString) or goldString;
end
copper = FormatDisplayCopper(checkGoldThreshold, gold, silver, copper);
local moneyString = "";
local separator = "";
if ( gold > 0 ) then
moneyString = goldString;
separator = " ";
end
if ( silver > 0 ) then
moneyString = moneyString..separator..silverString;
separator = " ";
end
if ( copper > 0 or moneyString == "" ) then
moneyString = moneyString..separator..copperString;
end
return color and color:WrapTextInColorCode(moneyString) or moneyString;
end
function FormatDisplayCopper(checkGoldThreshold, gold, silver, copper)
return (checkGoldThreshold and gold > Constants.MoneyFormattingConstants.GOLD_REWARD_THRESHOLD_TO_HIDE_COPPER ) and 0 or copper;
end
function FormatPercentage(percentage, roundToNearestInteger)
if roundToNearestInteger then
percentage = Round(percentage * 100);
else
percentage = percentage * 100;
end
return string.format(PERCENTAGE_STRING, percentage);
if roundToNearestInteger then
percentage = Round(percentage * 100);
else
percentage = percentage * 100;
end
return PERCENTAGE_STRING:format(percentage);
end
function FormatPercentageRounded(value)
local roundToNearestInteger = true;
return FormatPercentage(value, roundToNearestInteger);
end
function FormatFraction(numerator, denominator)
return string.format(GENERIC_FRACTION_STRING, numerator, denominator);
return GENERIC_FRACTION_STRING:format(numerator, denominator);
end
function SplitTextIntoLines(text, delimiter)
local lines = {};
local startIndex = 1;
local foundIndex = string.find(text, delimiter);
while foundIndex do
table.insert(lines, string.sub(text, startIndex, foundIndex - 1));
startIndex = foundIndex + 2;
foundIndex = string.find(text, delimiter, startIndex);
end
if startIndex <= string.len(text) then
table.insert(lines, string.sub(text, startIndex));
end
return lines;
function GetHighlightedNumberDifferenceString(baseString, newString)
local outputString = "";
-- output string is being built from the new string
local newStringIndex = 1;
-- find a stretch of digits (including . and , because of different locales) - but has to end in a digit
local PATTERN = "([,%.%d]*%d+)";
local start1, end1, baseNumberString = string.find(baseString, PATTERN);
local start2, end2, newNumberString = string.find(newString, PATTERN);
while start1 and start2 do
-- add from the new string until the matched spot
outputString = outputString .. string.sub(newString, newStringIndex, start2 - 1);
newStringIndex = end2 + 1;
if baseNumberString ~= newNumberString then
-- need to remove , and . before comparing numbers because of locales
local scrubbedBaseNumberString = gsub(baseNumberString, "[,%.]", "");
local scrubbedNewNumberString = gsub(newNumberString, "[,%.]", "");
local baseNumber = tonumber(scrubbedBaseNumberString);
local newNumber = tonumber(scrubbedNewNumberString);
if baseNumber and newNumber then
local delta = newNumber - baseNumber;
if delta > 0 then
newNumberString = GREEN_FONT_COLOR_CODE..string.format(newNumberString)..FONT_COLOR_CODE_CLOSE;
elseif delta < 0 then
newNumberString = RED_FONT_COLOR_CODE..string.format(newNumberString)..FONT_COLOR_CODE_CLOSE;
end
end
end
outputString = outputString..newNumberString;
start1, end1, baseNumberString = string.find(baseString, PATTERN, end1 + 1);
start2, end2, newNumberString = string.find(newString, PATTERN, end2 + 1);
end
outputString = outputString .. string.sub(newString, newStringIndex, string.len(newString));
return outputString;
end
function SplitTextIntoHeaderAndNonHeader(text)
local foundIndex = string.find(text, "|n");
local len = string.len(text);
if not foundIndex then
return text; -- no newline: whole thing is a header
elseif len == 2 then
return nil; -- the string was just a newline
elseif foundIndex == 1 then
return string.sub(text, 3); -- leading newline: rest is a header
elseif foundIndex == len - 1 then
return string.sub(text, 1, foundIndex - 1); -- trailing newline
else
return string.sub(text, 1, foundIndex - 1), string.sub(text, foundIndex + 2);
end
function FormatUnreadMailTooltip(tooltip, headerText, senders)
for i, sender in ipairs(senders) do
headerText = headerText.."\n"..sender;
end
tooltip:SetText(headerText);
end
-- (money, separateThousands [, checkGoldThreshold, showZeroAsGold])
-- Text-symbol form ("12g 34s 56c"); for the coin-icon form use
-- GetCoinTextureString below. The checkGoldThreshold arg is accepted for
-- signature compatibility but not applied (it relied on modern Constants tables).
function GetMoneyString(money, separateThousands, checkGoldThreshold, showZeroAsGold)
local gold = math.floor(money / COPPER_PER_GOLD);
local silver = math.floor(math.mod(money, COPPER_PER_GOLD) / COPPER_PER_SILVER);
local copper = math.mod(money, COPPER_PER_SILVER);
FormattingUtil = {};
local goldString = (separateThousands and FormatLargeNumber(gold) or tostring(gold)) ..
GOLD_AMOUNT_SYMBOL;
local silverString = silver .. SILVER_AMOUNT_SYMBOL;
local copperString = copper .. COPPER_AMOUNT_SYMBOL;
function FormattingUtil.GetCostString(icon, quantity, colorCode, abbreviate)
colorCode = colorCode or HIGHLIGHT_FONT_COLOR_CODE;
if showZeroAsGold and money == 0 then
return goldString;
end
local moneyString = "";
local separator = "";
if gold > 0 then
moneyString = goldString;
separator = " ";
end
if silver > 0 then
moneyString = moneyString .. separator .. silverString;
separator = " ";
end
if copper > 0 or moneyString == "" then
moneyString = moneyString .. separator .. copperString;
end
return moneyString;
local markup = CreateTextureMarkup(icon, 64, 64, 16, 16, 0, 1, 0, 1);
local amountString;
if abbreviate then
amountString = AbbreviateNumbers(quantity);
else
amountString = BreakUpLargeNumbers(quantity);
end
return ("%s%s %s|r"):format(colorCode, amountString, markup);
end
function FormattingUtil.GetItemCostString(itemID, quantity, colorCode, abbreviate)
local icon = C_Item.GetItemIconByID(itemID);
if icon then
return FormattingUtil.GetCostString(icon, quantity, colorCode, abbreviate);
end
return "";
end
function FormattingUtil.AddLeadingZeroes(digits, numZeroes, zeroesColor)
numZeroes = numZeroes or 7;
zeroesColor = zeroesColor or GRAY_FONT_COLOR;
local digitsString = tostring(digits);
local numDigits = #digitsString;
if numDigits < numZeroes then
local leadingZeroes = "";
for i = 1, (numZeroes - numDigits) do
leadingZeroes = leadingZeroes.."0";
end
leadingZeroes = zeroesColor:WrapTextInColorCode(leadingZeroes);
return leadingZeroes .. digits;
end
return digits;
end
function ReplaceGenderTokens(string, gender)
if not strfind(string, "%$") then
return string;
end
-- This is a very simple parser that will only handle $G/$g tokens
return gsub(string, "$[Gg]([^:]+):([^;]+);", "%"..gender);
end
+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,79 @@
-- Backport of retail's Blizzard_SharedXML/IndexRangeDataProvider.lua.
-- IndexRangeDataProviderMixin originally written for parity with ScrollBox API to
-- simulate extremely large ranges (i.e. 20,000 macro icons). As a result, this
-- data provider has a minimal API.
IndexRangeDataProviderMixin = CreateFromMixins(CallbackRegistryMixin);
IndexRangeDataProviderMixin:GenerateCallbackEvents(
{
"OnSizeChanged",
}
);
function IndexRangeDataProviderMixin:Init(size)
CallbackRegistryMixin.OnLoad(self);
self:SetSize(size);
end
function IndexRangeDataProviderMixin:IsVirtual()
return true;
end
function IndexRangeDataProviderMixin:Enumerate(indexBegin, indexEnd)
indexBegin = indexBegin and (indexBegin - 1) or 0;
indexEnd = indexEnd or self.size;
local function Enumerator(invariant, index)
index = index + 1;
if index <= indexEnd then
return index, index;
end
end
local invariant = nil;
return Enumerator, invariant, indexBegin;
end
function IndexRangeDataProviderMixin:EnumerateEntireRange()
local indexBegin, indexEnd = nil, nil;
return self:Enumerate(indexBegin, indexEnd);
end
function IndexRangeDataProviderMixin:GetSize()
return self.size;
end
function IndexRangeDataProviderMixin:SetSize(size)
self.size = math.max(0, size);
local pendingSort = false;
self:TriggerEvent(IndexRangeDataProviderMixin.Event.OnSizeChanged, pendingSort);
end
function IndexRangeDataProviderMixin:Flush()
self:SetSize(0);
end
function IndexRangeDataProviderMixin:Find(index)
if index <= self:GetSize() then
return index;
end
end
function IndexRangeDataProviderMixin:FindByPredicate(predicate)
for index = 1, self:GetSize() do
if predicate(index) then
return index;
end
end
end
function IndexRangeDataProviderMixin:ContainsByPredicate(predicate)
return self:FindByPredicate(predicate) ~= nil;
end
function CreateIndexRangeDataProvider(size)
return CreateAndInitFromMixin(IndexRangeDataProviderMixin, size or 0);
end
@@ -0,0 +1,354 @@
-- Backport of retail's Blizzard_ObjectAPI/Mainline/Item.lua.
--
-- Differences from the retail source:
-- - GetItemQualityColor reads ITEM_QUALITY_COLORS directly; retail's
-- ColorManager.GetColorDataForItemQuality returns the same entry.
Item = {};
ItemMixin = {};
--[[static]] function Item:CreateFromItemLocation(itemLocation)
if type(itemLocation) ~= "table" or type(itemLocation.HasAnyLocation) ~= "function" or not itemLocation:HasAnyLocation() then
error("Usage: Item:CreateFromItemLocation(notEmptyItemLocation)", 2);
end
local item = CreateFromMixins(ItemMixin);
item:SetItemLocation(itemLocation);
return item;
end
--[[static]] function Item:CreateFromBagAndSlot(bagID, slotIndex)
if type(bagID) ~= "number" or type(slotIndex) ~= "number" then
error("Usage: Item:CreateFromBagAndSlot(bagID, slotIndex)", 2);
end
local item = CreateFromMixins(ItemMixin);
item:SetItemLocation(ItemLocation:CreateFromBagAndSlot(bagID, slotIndex));
return item;
end
--[[static]] function Item:CreateFromEquipmentSlot(equipmentSlotIndex)
if type(equipmentSlotIndex) ~= "number" then
error("Usage: Item:CreateFromEquipmentSlot(equipmentSlotIndex)", 2);
end
local item = CreateFromMixins(ItemMixin);
item:SetItemLocation(ItemLocation:CreateFromEquipmentSlot(equipmentSlotIndex));
return item;
end
--[[static]] function Item:CreateFromItemLink(itemLink)
if type(itemLink) ~= "string" then
error("Usage: Item:CreateFromItemLink(itemLinkString)", 2);
end
local item = CreateFromMixins(ItemMixin);
item:SetItemLink(itemLink);
return item;
end
--[[static]] function Item:CreateFromItemID(itemID)
if type(itemID) ~= "number" then
error("Usage: Item:CreateFromItemID(itemID)", 2);
end
local item = CreateFromMixins(ItemMixin);
item:SetItemID(itemID);
return item;
end
--[[static]] function Item:CreateFromItemGUID(itemGUID)
if type(itemGUID) ~= "string" then
error("Usage: Item:CreateFromItemGUID(itemGUIDString)", 2);
end
local item = CreateFromMixins(ItemMixin);
item:SetItemGUID(itemGUID);
return item;
end
--[[static]] function Item:DoItemsMatch(item1, item2)
if not item1 or not item2 then
return false;
end
return item1:Matches(item2);
end
function ItemMixin:Matches(item)
if not item then
return false;
end
local itemID = item:GetItemID();
if (itemID ~= nil) and (self:GetItemID() == itemID) then
return true;
end
local itemLocation = item:GetItemLocation();
if (itemLocation ~= nil) and itemLocation:IsEqualTo(self:GetItemLocation()) then
return true;
end
return false;
end
function ItemMixin:SetItemLocation(itemLocation)
self:Clear();
self.itemLocation = itemLocation;
end
function ItemMixin:SetItemLink(itemLink)
self:Clear();
self.itemLink = itemLink;
end
function ItemMixin:SetItemID(itemID)
self:Clear();
self.itemID = itemID;
end
function ItemMixin:SetItemGUID(itemGUID)
self:Clear();
self.itemGUID = itemGUID;
end
function ItemMixin:GetItemLocation()
if self.itemLocation then
return self.itemLocation;
end
if self.itemGUID then
return C_Item.GetItemLocation(self.itemGUID);
end
return nil;
end
function ItemMixin:GetItemGUID()
if self.itemGUID then
return self.itemGUID;
end
if self.itemLocation then
return C_Item.GetItemGUID(self.itemLocation);
end
return nil;
end
function ItemMixin:HasItemLocation()
return self.itemLocation ~= nil;
end
function ItemMixin:Clear()
self.itemLocation = nil;
self.itemLink = nil;
self.itemID = nil;
self.itemGUID = nil;
end
function ItemMixin:IsItemEmpty()
if self:GetStaticBackingItem() then
return not C_Item.DoesItemExistByID(self:GetStaticBackingItem());
end
return not self:IsItemInPlayersControl();
end
function ItemMixin:GetStaticBackingItem()
return self.itemLink or self.itemID;
end
function ItemMixin:IsItemInPlayersControl()
local itemLocation = self:GetItemLocation();
return itemLocation and C_Item.DoesItemExist(itemLocation);
end
-- Item API
function ItemMixin:GetItemID()
local backingItem = self:GetStaticBackingItem();
if backingItem then
return C_Item.GetItemInfoInstant(backingItem) or C_Item.GetItemIDForItemInfo(backingItem);
end
if not self:IsItemEmpty() then
return C_Item.GetItemID(self:GetItemLocation());
end
return nil;
end
function ItemMixin:IsItemLocked()
return self:IsItemInPlayersControl() and C_Item.IsLocked(self:GetItemLocation());
end
function ItemMixin:LockItem()
if self:IsItemInPlayersControl() then
C_Item.LockItem(self:GetItemLocation());
end
end
function ItemMixin:UnlockItem()
if self:IsItemInPlayersControl() then
C_Item.UnlockItem(self:GetItemLocation());
end
end
function ItemMixin:GetItemIcon() -- requires item data to be loaded
if self:GetStaticBackingItem() then
return C_Item.GetItemIconByID(self:GetStaticBackingItem());
end
if not self:IsItemEmpty() then
return C_Item.GetItemIcon(self:GetItemLocation());
end
end
function ItemMixin:GetItemName() -- requires item data to be loaded
if self:GetStaticBackingItem() then
return C_Item.GetItemNameByID(self:GetStaticBackingItem());
end
if not self:IsItemEmpty() then
return C_Item.GetItemName(self:GetItemLocation());
end
return nil;
end
function ItemMixin:GetItemLink() -- requires item data to be loaded
if self.itemLink then
return self.itemLink;
end
if self.itemID then
return (select(2, C_Item.GetItemInfo(self.itemID)));
end
if not self:IsItemEmpty() then
return C_Item.GetItemLink(self:GetItemLocation());
end
return nil;
end
function ItemMixin:GetItemQuality() -- requires item data to be loaded
if self:GetStaticBackingItem() then
return C_Item.GetItemQualityByID(self:GetStaticBackingItem());
end
if not self:IsItemEmpty() then
return C_Item.GetItemQuality(self:GetItemLocation());
end
return nil;
end
function ItemMixin:GetStackCount() -- requires item data to be loaded
if not self:IsItemEmpty() then
return C_Item.GetStackCount(self:GetItemLocation());
end
return nil;
end
function ItemMixin:GetCurrentItemLevel() -- requires item data to be loaded
if self:GetStaticBackingItem() then
return (C_Item.GetDetailedItemLevelInfo(self:GetStaticBackingItem()));
end
if not self:IsItemEmpty() then
return C_Item.GetCurrentItemLevel(self:GetItemLocation());
end
return nil;
end
function ItemMixin:GetItemQualityColor() -- requires item data to be loaded
local itemQuality = self:GetItemQuality();
return ITEM_QUALITY_COLORS[itemQuality]; -- may be nil if item data isn't loaded
end
function ItemMixin:GetItemQualityColorRGB() -- requires item data to be loaded
local colorTbl = self:GetItemQualityColor();
return colorTbl.color:GetRGB();
end
function ItemMixin:GetItemMaxStackSize() -- requires item data to be loaded
if self:GetStaticBackingItem() then
return C_Item.GetItemMaxStackSizeByID(self:GetStaticBackingItem());
end
if not self:IsItemEmpty() then
return C_Item.GetItemMaxStackSize(self:GetItemLocation());
end
return nil;
end
function ItemMixin:IsStackable() -- requires item data to be loaded
local maxStackSize = self:GetItemMaxStackSize();
return maxStackSize and maxStackSize > 1;
end
function ItemMixin:GetInventoryType()
if self:GetStaticBackingItem() then
return C_Item.GetItemInventoryTypeByID(self:GetStaticBackingItem());
end
if not self:IsItemEmpty() then
return C_Item.GetItemInventoryType(self:GetItemLocation());
end
return nil;
end
function ItemMixin:GetInventoryTypeName()
if not self:IsItemEmpty() then
return select(4, C_Item.GetItemInfoInstant(self:GetItemID()));
end
end
function ItemMixin:IsItemDataCached()
if self:GetStaticBackingItem() then
return C_Item.IsItemDataCachedByID(self:GetStaticBackingItem());
end
if not self:IsItemEmpty() then
return C_Item.IsItemDataCached(self:GetItemLocation());
end
return true;
end
function ItemMixin:IsDataEvictable()
-- Item data could be evicted from the cache
return true;
end
function ItemMixin:ValidateForContinueOnItemLoad(methodName, callbackFunction)
if type(callbackFunction) ~= "function" then
error(("Usage: NonEmptyItem:%s(callbackFunction): invalid callbackFunction"):format(methodName), 3);
end
if self:IsItemEmpty() then
if self.itemLink then
error(("Usage: NonEmptyItem:%s(callbackFunction) invalid itemLink: <%s>"):format(methodName, self.itemLink), 3);
elseif self.itemID then
error(("Usage: NonEmptyItem:%s(callbackFunction) invalid itemID: <%d>"):format(methodName, self.itemID), 3);
end
error(("Usage: NonEmptyItem:%s(callbackFunction): invalid item"):format(methodName), 3);
else
if not self:GetItemID() then
error(("Usage: NonEmptyItem:%s(callbackFunction): item appeared non-empty, but had invalid itemID. itemID: <%s>, itemLink: <%s>"):format(methodName, tostring(self.itemID), tostring(self.itemLink)), 3);
end
end
end
-- Add a callback to be executed when item data is loaded, if the item data is already loaded then execute it immediately
function ItemMixin:ContinueOnItemLoad(callbackFunction)
self:ValidateForContinueOnItemLoad("ContinueOnItemLoad", callbackFunction);
ItemEventListener:AddCallback(self:GetItemID(), callbackFunction);
end
-- Same as ContinueOnItemLoad, except it returns a function that when called will cancel the continue
function ItemMixin:ContinueWithCancelOnItemLoad(callbackFunction)
self:ValidateForContinueOnItemLoad("ContinueWithCancelOnItemLoad", callbackFunction);
return ItemEventListener:AddCancelableCallback(self:GetItemID(), callbackFunction);
end
-- Generic aliases for use with ContinuableContainer
function ItemMixin:ContinueWithCancelOnRecordLoad(callbackFunction)
return self:ContinueWithCancelOnItemLoad(callbackFunction);
end
function ItemMixin:IsRecordDataCached()
return self:IsItemDataCached();
end
+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
@@ -78,8 +78,8 @@ function DeltaLerp(startValue, endValue, amount, timeSec)
return Lerp(startValue, endValue, Saturate(amount * timeSec * TARGET_FRAME_PER_SEC))
end
function FrameDeltaLerp(startValue, endValue, amount, elapsed)
return DeltaLerp(startValue, endValue, amount, elapsed)
function FrameDeltaLerp(startValue, endValue, amount)
return DeltaLerp(startValue, endValue, amount, GetTickTime())
end
function RandomFloatInRange(minValue, maxValue)
@@ -0,0 +1,465 @@
-- Backport of retail's Blizzard_SharedXML/MixinUtil.lua.
--
-- Implementation differences from the retail source:
-- - wipe is table.wipe here.
local wipe = table.wipe;
-- The following function is used with permission from Daniel Stephens
-- texture - Texture
-- canvasFrame - Canvas Frame (for anchoring)
-- startX,startY - Coordinate of start of line
-- endX,endY - Coordinate of end of line
-- lineWidth - Width of line
-- relPoint - Relative point on canvas to interpret coords (Default BOTTOMLEFT)
function DrawLine(texture, canvasFrame, startX, startY, endX, endY, lineWidth, lineFactor, relPoint)
if (not relPoint) then relPoint = "BOTTOMLEFT"; end
lineFactor = lineFactor * .5;
-- Determine dimensions and center point of line
local dx,dy = endX - startX, endY - startY;
local cx,cy = (startX + endX) / 2, (startY + endY) / 2;
-- Normalize direction if necessary
if (dx < 0) then
dx,dy = -dx,-dy;
end
-- Calculate actual length of line
local lineLength = sqrt((dx * dx) + (dy * dy));
-- Quick escape if it'sin zero length
if (lineLength == 0) then
texture:SetTexCoord(0,0,0,0,0,0,0,0);
texture:SetPoint("BOTTOMLEFT", canvasFrame, relPoint, cx,cy);
texture:SetPoint("TOPRIGHT", canvasFrame, relPoint, cx,cy);
return;
end
-- Sin and Cosine of rotation, and combination (for later)
local sin, cos = -dy / lineLength, dx / lineLength;
local sinCos = sin * cos;
-- Calculate bounding box size and texture coordinates
local boundingWidth, boundingHeight, bottomLeftX, bottomLeftY, topLeftX, topLeftY, topRightX, topRightY, bottomRightX, bottomRightY;
if (dy >= 0) then
boundingWidth = ((lineLength * cos) - (lineWidth * sin)) * lineFactor;
boundingHeight = ((lineWidth * cos) - (lineLength * sin)) * lineFactor;
bottomLeftX = (lineWidth / lineLength) * sinCos;
bottomLeftY = sin * sin;
bottomRightY = (lineLength / lineWidth) * sinCos;
bottomRightX = 1 - bottomLeftY;
topLeftX = bottomLeftY;
topLeftY = 1 - bottomRightY;
topRightX = 1 - bottomLeftX;
topRightY = bottomRightX;
else
boundingWidth = ((lineLength * cos) + (lineWidth * sin)) * lineFactor;
boundingHeight = ((lineWidth * cos) + (lineLength * sin)) * lineFactor;
bottomLeftX = sin * sin;
bottomLeftY = -(lineLength / lineWidth) * sinCos;
bottomRightX = 1 + (lineWidth / lineLength) * sinCos;
bottomRightY = bottomLeftX;
topLeftX = 1 - bottomRightX;
topLeftY = 1 - bottomLeftX;
topRightY = 1 - bottomLeftY;
topRightX = topLeftY;
end
-- Set texture coordinates and anchors
texture:ClearAllPoints();
texture:SetTexCoord(topLeftX, topLeftY, bottomLeftX, bottomLeftY, topRightX, topRightY, bottomRightX, bottomRightY);
texture:SetPoint("BOTTOMLEFT", canvasFrame, relPoint, cx - boundingWidth, cy - boundingHeight);
texture:SetPoint("TOPRIGHT", canvasFrame, relPoint, cx + boundingWidth, cy + boundingHeight);
end
-- Mix this into a Texture to be able to treat it like a line
LineMixin = {};
function LineMixin:SetStartPoint(x, y)
self.startX, self.startY = x, y;
end
function LineMixin:SetEndPoint(x, y)
self.endX, self.endY = x, y;
end
function LineMixin:SetThickness(thickness)
self.thickness = thickness;
end
function LineMixin:Draw()
local parent = self:GetParent();
local x, y = parent:GetLeft(), parent:GetBottom();
self:ClearAllPoints();
DrawLine(self, parent, self.startX - x, self.startY - y, self.endX - x, self.endY - y, self.thickness or 32, 1);
end
-- Mix this into a FontString to have it animate towards its value, call UpdateAnimatedValue every frame
AnimatedNumericFontStringMixin = {};
-- How long should it take to animate
function AnimatedNumericFontStringMixin:SetAnimatedDurationTimeSec(animatedDurationTimeSec)
self.animatedDurationTimeSec = animatedDurationTimeSec;
end
function AnimatedNumericFontStringMixin:GetAnimatedDurationTimeSec()
return self.animatedDurationTimeSec or 1.0;
end
function AnimatedNumericFontStringMixin:SetAnimatedValue(value)
self.targetAnimatedValue = value;
if not self.currentAnimatedValue then
self.currentAnimatedValue = self.targetAnimatedValue;
end
self.initialAnimatedValueDelta = math.abs(self.targetAnimatedValue - self.currentAnimatedValue);
end
-- Stops animating the value and just snaps to it
function AnimatedNumericFontStringMixin:SnapToTarget()
if self.targetAnimatedValue then
self:SetText(BreakUpLargeNumbers(Round(self.targetAnimatedValue)));
self.currentAnimatedValue = self.targetAnimatedValue;
self.targetAnimatedValue = nil;
end
end
-- Call this every frame
function AnimatedNumericFontStringMixin:UpdateAnimatedValue(elapsed)
if self.targetAnimatedValue then
local change = self.initialAnimatedValueDelta * (elapsed / self:GetAnimatedDurationTimeSec());
if math.abs(self.targetAnimatedValue - self.currentAnimatedValue) <= change then
self:SnapToTarget();
else
local direction = self.targetAnimatedValue > self.currentAnimatedValue and 1 or -1;
self.currentAnimatedValue = self.currentAnimatedValue + direction * change;
self:SetText(BreakUpLargeNumbers(Round(self.currentAnimatedValue)));
end
end
end
SparseGridMixin = {};
function SparseGridMixin:OnLoad(width, height)
self.width = width;
self.height = height;
self.cells = {};
end
function SparseGridMixin:SetCell(x, y, data)
if not self:IsInRange(x, y) then
error("index of out of range");
end
local linearIndex = self:CalculateLinearIndex(x, y);
self.cells[linearIndex] = data;
end
function SparseGridMixin:GetCell(x, y)
if not self:IsInRange(x, y) then
error("index of out of range");
end
local linearIndex = self:CalculateLinearIndex(x, y);
return self.cells[linearIndex];
end
function SparseGridMixin:IsInRange(x, y)
return x > 0 and x <= self.width and y > 0 and y <= self.height;
end
function SparseGridMixin:Clear()
wipe(self.cells);
end
function SparseGridMixin:CalculateLinearIndex(x, y)
return x + self.width * (y - 1);
end
-- Mix this in to make an object a doublyLinkedList
DoublyLinkedListMixin = {nodeCount = 0};
function DoublyLinkedListMixin:PushFront(nodeToInsert)
self:InsertBefore(nodeToInsert, self.front);
end
function DoublyLinkedListMixin:PushBack(nodeToInsert)
self:InsertAfter(nodeToInsert, self.back);
end
function DoublyLinkedListMixin:PopFront()
return self:Remove(self.front);
end
function DoublyLinkedListMixin:PopBack()
return self:Remove(self.back);
end
function DoublyLinkedListMixin:EnumerateNodes()
local index = 0;
local currentNode = self.front;
return function()
index = index + 1;
if currentNode then
local returnNode = currentNode;
currentNode = currentNode.next;
return index, returnNode;
else
return nil;
end
end;
end
function DoublyLinkedListMixin:EnumerateNodesReverse()
local index = 0;
local currentNode = self.back;
return function()
index = index + 1;
if currentNode then
local returnNode = currentNode;
currentNode = currentNode.prev;
return index, returnNode;
else
return nil;
end
end;
end
function DoublyLinkedListMixin:InsertBefore(nodeToInsert, listNode)
nodeToInsert.next = listNode;
if listNode then
if listNode.prev then
listNode.prev.next = nodeToInsert;
end
nodeToInsert.prev = listNode.prev;
listNode.prev = nodeToInsert;
end
if self.front == listNode then
self.front = nodeToInsert;
end
if not self.back then
self.back = nodeToInsert;
end
self.nodeCount = self.nodeCount + 1;
end
function DoublyLinkedListMixin:InsertAfter(nodeToInsert, listNode)
nodeToInsert.prev = listNode;
if listNode then
if listNode.next then
listNode.next.prev = nodeToInsert;
end
nodeToInsert.next = listNode.next;
listNode.next = nodeToInsert;
end
if self.back == listNode then
self.back = nodeToInsert;
end
if not self.front then
self.front = nodeToInsert;
end
self.nodeCount = self.nodeCount + 1;
end
function DoublyLinkedListMixin:Remove(node)
if node then
if self.front == node then
self.front = node.next;
end
if self.back == node then
self.back = node.prev;
end
if node.prev then
node.prev.next = node.next;
end
if node.next then
node.next.prev = node.prev;
end
node.prev = nil;
node.next = nil;
self.nodeCount = self.nodeCount - 1;
end
return node;
end
TextureLoadingGroupMixin = {};
function TextureLoadingGroupMixin:AddTexture(texture)
self.textures = self.textures or {};
self.textures[texture] = true;
end
function TextureLoadingGroupMixin:RemoveTexture(texture)
if self.textures then
self.textures[texture] = nil;
end
end
function TextureLoadingGroupMixin:Reset()
self.textures = nil;
end
function TextureLoadingGroupMixin:EnumerateTextures()
if self.textures then
return pairs(self.textures);
end
return nop;
end
function TextureLoadingGroupMixin:IsFullyLoaded()
if self.textures then
for texture in pairs(self.textures) do
if not texture:IsObjectLoaded() then
return false;
end
end
end
return true;
end
DirtiableMixin = {};
function DirtiableMixin:SetDirtyMethod(method)
self.dirtyCallback = function()
method(self);
self.dirty = nil;
end;
end
function DirtiableMixin:MarkDirty()
if not self.dirty then
RunNextFrame(self.dirtyCallback);
end
self.dirty = true;
end
-- This mixin implements a small ordered dirty-update runner. Consumers
-- define their own dirty flags and phase order. Phase methods return any
-- downstream flags that should be processed later in the same pass.
DirtyPhaseMixin = {};
function DirtyPhaseMixin:InitDirtyPhases()
self.dirty = false;
self.dirtyFlags = CreateFlags();
self.dirtyPhases = {};
end
function DirtyPhaseMixin:OnDirtyChanged(_dirty)
-- Override in the consumer to schedule or cancel dirty processing.
end
function DirtyPhaseMixin:SetDirtyPhases(phases)
assert(type(phases) == "table", "phases must be a table.");
for _index, phase in ipairs(phases) do
assert(phase.flag ~= nil, "phase.flag must be provided.");
assert(type(phase.handler) == "function", "phase.handler must be a function.");
end
self.dirtyPhases = phases;
end
function DirtyPhaseMixin:IsDirty(mask)
if mask == nil then
return self.dirty;
end
return self.dirtyFlags:IsSet(mask);
end
function DirtyPhaseMixin:UpdateDirtyState()
local dirty = self.dirtyFlags:IsAnySet();
if self.dirty ~= dirty then
self.dirty = dirty;
self:OnDirtyChanged(dirty);
end
end
function DirtyPhaseMixin:MarkDirty(mask)
self.dirtyFlags:Set(mask);
if not self.dirty then
self:UpdateDirtyState();
end
end
function DirtyPhaseMixin:MarkClean(mask)
self.dirtyFlags:Clear(mask);
if self.dirty then
self:UpdateDirtyState();
end
end
function DirtyPhaseMixin:ProcessDirtyFlags()
if not self:IsDirty() then
return;
end
-- Dirty processing is single-pass; phases should only return later phases.
for _index, phase in ipairs(self.dirtyPhases) do
if self.dirtyFlags:IsSet(phase.flag) then
self.dirtyFlags:Clear(phase.flag);
local downstreamMask = phase.handler();
if downstreamMask ~= nil then
self.dirtyFlags:Set(downstreamMask);
end
end
end
self:UpdateDirtyState();
if self:IsDirty() then
assertsafe(false, "Dirty flags were not fully cleared during update pass (remaining flags: %d)", self.dirtyFlags:GetFlags());
end
end
MixinUtil = {};
-- Calls an owned function on each entry of a table of mixin instances, checking that the instance does define that function
function MixinUtil.CallMethodOnAllSafe(instances, methodName, ...)
if not instances then
return;
end
for _, instance in ipairs(instances) do
if instance[methodName] then
instance[methodName](instance, ...);
end
end
end
-- Check existence of an element, then invoke the provided method by name with its arguments if it exists.
-- e.g. frame.Text:SetText("text") -> CallMethodSafe(frame.Text, "SetText", "text");
function MixinUtil.CallMethodSafe(element, methodName, ...)
if element then
element[methodName](element, ...);
end
end
@@ -0,0 +1,22 @@
-- Backport of retail's Blizzard_ObjectAPI/Mainline/ObjectCache.lua.
function ObjectCache_Create(mixin)
return {
objects = {},
Get = function(self, key)
local object = self.objects[key];
if object then
if object.CheckRefresh then
object:CheckRefresh();
end
return object;
end
object = CreateFromMixins(mixin);
object:Init(key);
self.objects[key] = object;
return object;
end,
}
end
@@ -0,0 +1,59 @@
-- Backport of retail's Blizzard_SharedXMLBase/ObjectUpdater.lua.
local States = EnumUtil.MakeEnum("Ready", "Begin", "Update", "End");
local ObjectUpdaterMixin = {};
function ObjectUpdaterMixin:Init(data, updateFunc, isCompleteFunc, finishFunc)
self.data = data;
self:SetUpdateFunction(updateFunc);
self:SetIsCompleteFunction(isCompleteFunc);
self:SetFinishFunction(finishFunc);
self.state = States.Begin;
self.ticker = C_Timer.NewTicker(0.01, GenerateClosure(self.Advance, self));
end
function ObjectUpdaterMixin:SetUpdateFunction(updateFunc)
self.updateFunc = updateFunc;
end
function ObjectUpdaterMixin:SetIsCompleteFunction(isCompleteFunc)
self.isCompleteFunc = isCompleteFunc;
end
function ObjectUpdaterMixin:SetFinishFunction(finishFunc)
self.finishFunc = finishFunc;
end
function ObjectUpdaterMixin:Cancel()
self.ticker:Cancel();
self.state = States.Ready;
end
function ObjectUpdaterMixin:Finished()
self:Cancel();
if self.finishFunc then
self.finishFunc(self.data);
end
end
function ObjectUpdaterMixin:Advance()
if self.state == States.Begin then
if self.isCompleteFunc(self.data) then
self.state = States.End;
else
self.state = States.Update;
end
elseif self.state == States.Update then
self.updateFunc(self.data);
if self.isCompleteFunc(self.data) then
self.state = States.End;
end
elseif self.state == States.End then
self:Finished();
return;
end
end
function CreateObjectUpdater(data, updateFunc, isCompleteFunc, finishFunc)
return CreateAndInitFromMixin(ObjectUpdaterMixin, data, updateFunc, isCompleteFunc, finishFunc);
end
+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
@@ -0,0 +1,50 @@
-- Backport of retail's Blizzard_SharedXML/Sequencer.lua.
SequencerMixin = {};
function SequencerMixin:Init()
self.original = {};
self.onFinish = GenerateClosure(self.ExecuteNext, self);
end
function SequencerMixin:AddInterpolated(v1, v2, time, interpType, func)
local interpolation = {v1 = v1, v2 = v2, time = time, interpType = interpType};
table.insert(self.original, 1, {interpolation = interpolation, func = func});
end
function SequencerMixin:Add(func)
table.insert(self.original, 1, {func = func});
end
function SequencerMixin:ExecuteNext()
local sequence = table.remove(self.sequences);
if sequence then
local interpolation = sequence.interpolation;
if interpolation then
self.interpolator = CreateInterpolator(sequence.interpType);
self.interpolator:Interpolate(interpolation.v1, interpolation.v2, interpolation.time, sequence.func, self.onFinish);
else
sequence.func();
self:ExecuteNext();
end
end
end
function SequencerMixin:Cancel()
if self.interpolator then
self.interpolator:Cancel();
end
self.sequences = {};
end
function SequencerMixin:Play()
local shallow = true;
self.sequences = CopyTable(self.original, shallow);
self:ExecuteNext();
end
function CreateSequencer()
local sequencer = CreateFromMixins(SequencerMixin);
sequencer:Init();
return sequencer;
end
@@ -0,0 +1,46 @@
-- Backport of retail's Blizzard_SharedXML/SharedColorConstants.lua.
FONT_COLOR_CODE_CLOSE = "|r";
FACTION_BAR_COLORS =
{
FACTION_RED_COLOR,
FACTION_RED_COLOR,
FACTION_ORANGE_COLOR,
FACTION_YELLOW_COLOR,
FACTION_GREEN_COLOR,
FACTION_GREEN_COLOR,
FACTION_GREEN_COLOR,
FACTION_GREEN_COLOR,
};
PLAYER_FACTION_COLORS =
{
[0] = PLAYER_FACTION_COLOR_HORDE,
[1] = PLAYER_FACTION_COLOR_ALLIANCE,
};
PLAYER_FACTION_COLORS_HEX =
{
[0] = PLAYER_FACTION_COLOR_HORDE:GenerateHexColor(),
[1] = PLAYER_FACTION_COLOR_ALLIANCE:GenerateHexColor(),
};
PLAYER_FACTION_BRIGHT_COLORS =
{
[0] = PVP_SCOREBOARD_HORDE_CELL_COLOR,
[1] = PVP_SCOREBOARD_ALLIANCE_CELL_COLOR,
};
COVENANT_COLORS =
{
[1] = KYRIAN_BLUE_COLOR,
[2] = VENTHYR_RED_COLOR,
[3] = NIGHT_FAE_BLUE_COLOR,
[4] = NECROLORD_GREEN_COLOR,
Kyrian = KYRIAN_BLUE_COLOR,
Venthyr = VENTHYR_RED_COLOR,
NightFae = NIGHT_FAE_BLUE_COLOR,
Necrolord = NECROLORD_GREEN_COLOR,
}
@@ -0,0 +1,62 @@
-- Backport of retail's Blizzard_SharedXML/SmoothStatusBar.lua.
local g_updatingBars = {};
local function IsCloseEnough(bar, newValue, targetValue)
local min, max = bar:GetMinMaxValues();
local range = max - min;
if range > 0.0 then
return math.abs((newValue - targetValue) / range) < .00001;
end
return true;
end
local function ProcessSmoothStatusBars()
for bar, targetValue in pairs(g_updatingBars) do
local effectiveTargetValue = Clamp(targetValue, bar:GetMinMaxValues());
local newValue = FrameDeltaLerp(bar:GetValue(), effectiveTargetValue, .25);
if IsCloseEnough(bar, newValue, effectiveTargetValue) then
g_updatingBars[bar] = nil;
bar:SetValue(effectiveTargetValue);
else
bar:SetValue(newValue);
end
end
end
C_Timer.NewTicker(0, ProcessSmoothStatusBars);
SmoothStatusBarMixin = {};
function SmoothStatusBarMixin:ResetSmoothedValue(value) --If nil, tries to set to the last target value
local targetValue = g_updatingBars[self];
if targetValue then
g_updatingBars[self] = nil;
self:SetValue(value or targetValue);
elseif value then
self:SetValue(value);
end
end
function SmoothStatusBarMixin:SetSmoothedValue(value)
g_updatingBars[self] = value;
end
function SmoothStatusBarMixin:SetMinMaxSmoothedValue(min, max)
self:SetMinMaxValues(min, max);
local targetValue = g_updatingBars[self];
if targetValue then
local ratio = 1;
if max ~= 0 and self.lastSmoothedMax and self.lastSmoothedMax ~= 0 then
ratio = max / self.lastSmoothedMax;
end
g_updatingBars[self] = targetValue * ratio;
end
self.lastSmoothedMin = min;
self.lastSmoothedMax = max;
end
@@ -0,0 +1,92 @@
-- Backport of retail's Blizzard_SharedXML/SortUtil.lua.
SortUtil = {};
function SortUtil.CompareNumeric(lhs, rhs)
return Sign(lhs - rhs);
end
function SortUtil.CompareUtf8i(lhs, rhs)
return Sign(strcmputf8i(lhs, rhs));
end
local SortManagerMixin = {};
function SortManagerMixin:Init()
self.comparatorsTbls = {};
end
function SortManagerMixin:InsertComparator(sortOrder, comparator)
assert(sortOrder, comparator);
table.insert(self.comparatorsTbls, {sortOrder = sortOrder, comparator = comparator, ascending = false});
end
function SortManagerMixin:SetDefaultComparator(defaultComparatorFunc)
self.defaultComparatorFunc = defaultComparatorFunc;
end
function SortManagerMixin:SetSortOrderFunc(sortOrderFunc)
self.sortOrderFunc = sortOrderFunc;
end
function SortManagerMixin:FindComparatorTbl(sortOrder)
return FindValueInTableIf(self.comparatorsTbls, function(comparatorsTbl)
return comparatorsTbl.sortOrder == sortOrder;
end);
end
function SortManagerMixin:ToggleSortAscending(sortOrder)
local comparatorTbl = self:FindComparatorTbl(sortOrder);
comparatorTbl.ascending = not comparatorTbl.ascending;
end
function SortManagerMixin:SetSortAscending(sortOrder, ascending)
local comparatorTbl = self:FindComparatorTbl(sortOrder);
comparatorTbl.ascending = ascending;
end
function SortManagerMixin:IsSortAscending(sortOrder)
local comparatorTbl = self:FindComparatorTbl(sortOrder);
return comparatorTbl.ascending;
end
function SortManagerMixin:CreateComparator()
return function(lhs, rhs)
local comparatorsTbls = self.comparatorsTbls;
local result = 0;
local sortOrder = self.sortOrderFunc();
if sortOrder then
local comparatorTbl = self:FindComparatorTbl(sortOrder);
if comparatorTbl then
result = NegateIf(comparatorTbl.comparator(lhs, rhs), comparatorTbl.ascending);
end
end
if result == 0 then
for _, comparatorTbl in ipairs(comparatorsTbls) do
if comparatorTbl.sortOrder ~= sortOrder then
result = NegateIf(comparatorTbl.comparator(lhs, rhs), comparatorTbl.ascending);
if result < 0 then
return true;
elseif result > 0 then
return false;
end
end
end
if not self.defaultComparatorFunc then
error("Provide a default comparator that guarantees strict ordering.");
end
return self.defaultComparatorFunc(lhs, rhs);
end
return result < 0;
end;
end
function SortUtil.CreateSortManager()
local sortManager = CreateFromMixins(SortManagerMixin);
sortManager:Init();
return sortManager;
end
@@ -0,0 +1,88 @@
-- Backport of retail's Blizzard_ObjectAPI/Mainline/Spell.lua.
Spell = {};
SpellMixin = {};
--[[static]] function Spell:CreateFromSpellID(spellID)
local spell = CreateFromMixins(SpellMixin);
spell:SetSpellID(spellID);
return spell;
end
function SpellMixin:SetSpellID(spellID)
self:Clear();
self.spellID = spellID;
end
function SpellMixin:GetSpellID()
return self.spellID;
end
function SpellMixin:Clear()
self.spellID = nil;
end
function SpellMixin:IsSpellEmpty()
local spellID = self:GetSpellID();
return not spellID or not C_Spell.DoesSpellExist(spellID);
end
-- Spell API
function SpellMixin:IsSpellDataCached()
if not self:IsSpellEmpty() then
return C_Spell.IsSpellDataCached(self:GetSpellID());
end
return true;
end
function SpellMixin:IsDataEvictable()
-- Spell data could be evicted from the cache
return true;
end
function SpellMixin:GetSpellName()
return (C_Spell.GetSpellName(self:GetSpellID()));
end
function SpellMixin:GetSpellTexture()
return (C_Spell.GetSpellTexture(self:GetSpellID()));
end
function SpellMixin:GetSpellSubtext()
return C_Spell.GetSpellSubtext(self:GetSpellID());
end
function SpellMixin:GetSpellDescription()
return C_Spell.GetSpellDescription(self:GetSpellID());
end
function SpellMixin:GetSpellDescriptionForItemLocation(itemLocation)
return C_Spell.GetSpellDescriptionForItemLocation(self:GetSpellID(), itemLocation);
end
-- Add a callback to be executed when spell data is loaded, if the spell data is already loaded then execute it immediately
function SpellMixin:ContinueOnSpellLoad(callbackFunction)
if type(callbackFunction) ~= "function" or self:IsSpellEmpty() then
error("Usage: NonEmptySpell:ContinueOnLoad(callbackFunction)", 2);
end
SpellEventListener:AddCallback(self:GetSpellID(), callbackFunction);
end
-- Same as ContinueOnSpellLoad, except it returns a function that when called will cancel the continue
function SpellMixin:ContinueWithCancelOnSpellLoad(callbackFunction)
if type(callbackFunction) ~= "function" or self:IsSpellEmpty() then
error("Usage: NonEmptySpell:ContinueWithCancelOnSpellLoad(callbackFunction)", 2);
end
return SpellEventListener:AddCancelableCallback(self:GetSpellID(), callbackFunction);
end
-- Generic aliases for use with ContinuableContainer
function SpellMixin:ContinueWithCancelOnRecordLoad(callbackFunction)
return self:ContinueWithCancelOnSpellLoad(callbackFunction);
end
function SpellMixin:IsRecordDataCached()
return self:IsSpellDataCached();
end
@@ -0,0 +1,46 @@
-- Backport of retail's Blizzard_ObjectAPI/Mainline/SpellBookItemLocation.lua.
SpellBookItemLocation = {};
SpellBookItemLocationMixin = {};
--[[static]] function SpellBookItemLocation:CreateEmpty()
local spelLBookItemLocation = CreateFromMixins(SpellBookItemLocationMixin);
return spelLBookItemLocation;
end
-- slotIndex: int
-- spellBank: Enum.SpellBookSpellBank (see SpellBookConstantsDocumentation.lua)
--[[static]] function SpellBookItemLocation:CreateFromIndexAndBank(slotIndex, spellBank)
local spellBookItemLocation = SpellBookItemLocation:CreateEmpty();
spellBookItemLocation:SetIndexAndBank(slotIndex, spellBank);
return spellBookItemLocation;
end
function SpellBookItemLocationMixin:Clear()
self.slotIndex = nil;
self.spellBank = nil;
end
function SpellBookItemLocationMixin:SetIndexAndBank(slotIndex, spellBank)
self:Clear();
self.slotIndex = slotIndex;
self.spellBank = spellBank;
end
function SpellBookItemLocationMixin:GetIndexAndBank()
return self.slotIndex, self.spellBank;
end
function SpellBookItemLocationMixin:IsEqualToIndexAndBank(otherSlotIndex, otherSpellBank)
local slotIndex, spellBank = self:GetIndexAndBank();
return slotIndex == otherSlotIndex and spellBank == otherSpellBank;
end
function SpellBookItemLocationMixin:IsEqualTo(otherSpellBookItemLocation)
if not otherSpellBookItemLocation then
return false;
end
local otherSlotIndex, otherSpellBank = otherSpellBookItemLocation:GetIndexAndBank();
return self:IsEqualToIndexAndBank(otherSlotIndex, otherSpellBank);
end
@@ -0,0 +1,133 @@
-- Backport of retail's Blizzard_SharedXML/Spline.lua.
local function CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1, p2, p3, p4)
local term1 = 2.0 * p2 + (p3 - p1) * t;
local term2 = (2.0 * p1 - 5.0 * p2 + 4.0 * p3 - p4) * tSquared;
local term3 = (3.0 * p2 - p1 - 3.0 * p3 + p4) * tCubed;
return 0.5 * (term1 + term2 + term3);
end
function CatmullRom_CalculatePointOnCurve(t, p1, p2, p3, p4)
local tSquared = t * t;
local tCubed = tSquared * t;
return CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1, p2, p3, p4);
end
function CatmullRom_Calculate2DPointOnCurve(t, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y)
local tSquared = t * t;
local tCubed = tSquared * t;
return
CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1x, p2x, p3x, p4x),
CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1y, p2y, p3y, p4y);
end
function CatmullRom_Calculate3DPointOnCurve(t, p1x, p1y, p1z, p2x, p2y, p2z, p3x, p3y, p3z, p4x, p4y, p4z)
local tSquared = t * t;
local tCubed = tSquared * t;
return
CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1x, p2x, p3x, p4x),
CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1y, p2y, p3y, p4y),
CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1z, p2z, p3z, p4z);
end
function CatmullRom_Calculate4DPointOnCurve(t, p1x, p1y, p1z, p1w, p2x, p2y, p2z, p2w, p3x, p3y, p3z, p3w, p4x, p4y, p4z, p4w)
local tSquared = t * t;
local tCubed = tSquared * t;
return
CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1x, p2x, p3x, p4x),
CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1y, p2y, p3y, p4y),
CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1z, p2z, p3z, p4z),
CatmullRom_CalculatePointOnCurveHelper(t, tSquared, tCubed, p1w, p2w, p3w, p4w);
end
CatmullRomSplineMixin = {};
function CreateCatmullRomSpline(numDimensions)
local spline = CreateFromMixins(CatmullRomSplineMixin);
spline:OnLoad(numDimensions)
return spline;
end
function CatmullRomSplineMixin:OnLoad(numDimensions)
if numDimensions == 1 then
self.calculateFunction = CatmullRom_CalculatePointOnCurve;
elseif numDimensions == 2 then
self.calculateFunction = CatmullRom_Calculate2DPointOnCurve;
elseif numDimensions == 3 then
self.calculateFunction = CatmullRom_Calculate3DPointOnCurve;
elseif numDimensions == 4 then
self.calculateFunction = CatmullRom_Calculate4DPointOnCurve;
else
-- In the interest of performance, limiting number of dimensions
error("Unsupported number of dimensions");
end
self.numDimensions = numDimensions;
self:ClearPoints();
end
function CatmullRomSplineMixin:GetNumDimensions()
return self.numDimensions;
end
local function AddPointHelper(pointData, startIndex, ...)
local pointIndex = 1;
for i = startIndex, startIndex + select("#", ...) do
pointData[i] = select(pointIndex, ...);
pointIndex = pointIndex + 1;
end
end
function CatmullRomSplineMixin:AddPoint(...)
assert(select("#", ...) == self.numDimensions);
if #self.pointData == 0 then
-- If first point, put it in the list three times times to simplify the curve calculation, at this time the curve represents a single point
AddPointHelper(self.pointData, 1, ...);
AddPointHelper(self.pointData, 1 + self.numDimensions, ...);
AddPointHelper(self.pointData, 1 + self.numDimensions * 2, ...);
else
-- For each other point, add it to the end twice but replacing the last one that was added twice
AddPointHelper(self.pointData, #self.pointData - self.numDimensions + 1, ...);
AddPointHelper(self.pointData, #self.pointData + 1, ...);
end
self.numPoints = self.numPoints + 1;
end
function CatmullRomSplineMixin:GetPoint(pointIndex)
if pointIndex > 0 and pointIndex <= self:GetNumPoints() then
return unpack(self.pointData, (pointIndex + 1) * self.numDimensions + 1, (pointIndex + 2) * self.numDimensions);
end
end
function CatmullRomSplineMixin:GetNumPoints()
return self.numPoints;
end
function CatmullRomSplineMixin:ClearPoints()
self.numPoints = 0;
self.pointData = {};
end
function CatmullRomSplineMixin:CalculatePointOnGlobalCurve(t)
local startSegmentIndex, localT = self:FindSegmentOnGlobalCurve(t);
if startSegmentIndex then
return self:CalculatePointOnLocalCurveSegment(startSegmentIndex, localT);
end
end
function CatmullRomSplineMixin:CalculatePointOnLocalCurveSegment(startSegmentIndex, t)
return self.calculateFunction(t, unpack(self.pointData, (startSegmentIndex - 2) * self.numDimensions + 1, (startSegmentIndex + 2) * self.numDimensions));
end
function CatmullRomSplineMixin:FindSegmentOnGlobalCurve(t)
local numPoints = self:GetNumPoints();
if numPoints > 1 then
local numSegments = numPoints - 1;
local segment = numSegments * t;
local segmentIndex = Clamp(math.floor(segment), 0, numSegments - 1) + 2;
return segmentIndex, PercentageBetween(segment + 2, segmentIndex, segmentIndex + 1);
end
end
@@ -0,0 +1,502 @@
-- Backport of retail's Blizzard_SharedXML/TableBuilder.lua.
local ColumnFramePoolCollection = CreateFramePoolCollection();
local ColumnWidthConstraints = {
Fill = 1, -- Width will be distributed by available space.
Fixed = 2, -- Width is specified when initializing the column.
};
-- Any row or cell is expected to initialize itself in terms of the row data. The dataIndex is provided
-- in case the derived mixin needs to make additional CAPI calls involving it's relative index. The row
-- data may also be needed for a tooltip, so it will be assigned to the row and cells on update.
TableBuilderElementMixin = {};
--Derive
function TableBuilderElementMixin:Init(...)
end
--Derive
function TableBuilderElementMixin:Populate(rowData, dataProviderKey)
end
TableBuilderCellMixin = CreateFromMixins(TableBuilderElementMixin);
--Derive
function TableBuilderCellMixin:OnLineEnter()
end
--Derive
function TableBuilderCellMixin:OnLineLeave()
end
TableBuilderRowMixin = CreateFromMixins(TableBuilderElementMixin);
--Derive
function TableBuilderRowMixin:OnLineEnter()
end
--Derive
function TableBuilderRowMixin:OnLineLeave()
end
function TableBuilderRowMixin:OnEnter()
self:OnLineEnter();
for i, cell in ipairs(self.cells) do
cell:OnLineEnter();
end
end
function TableBuilderRowMixin:OnLeave()
self:OnLineLeave();
for i, cell in ipairs(self.cells) do
cell:OnLineLeave();
end
end
-- Defines an entire column within the table builder, by default a column's sizing constraints are set to fill.
TableBuilderColumnMixin = {};
function TableBuilderColumnMixin:Init(table)
self.cells = {};
self.table = table;
local fillCoefficient = 1.0;
local padding = 0;
self:SetFillConstraints(fillCoefficient, padding);
end
-- Constructs the header frame with an optional initializer.
function TableBuilderColumnMixin:ConstructHeader(templateType, template, ...)
local frame = self.table:ConstructHeader(templateType, template);
self.headerFrame = frame;
if frame.Init then
frame:Init(...);
end
frame:Show();
end
function TableBuilderColumnMixin:Reset()
for row, cell in pairs(self.cells) do
self.pool:Release(cell);
tDeleteItem(row.cells, cell);
end
self.cells = {};
end
function TableBuilderColumnMixin:RemoveRow(row)
local cell = self.cells[row];
self.cells[row] = nil;
self.pool:Release(cell);
end
function TableBuilderColumnMixin:ConstructCell(row, rowData, dataProviderKey)
local cell = self.pool:Acquire();
self.cells[row] = cell;
if cell.Init then
cell:Init(unpack(self.args));
end
if cell.Populate then
cell.rowData = rowData;
cell:Populate(rowData, dataProviderKey);
end
cell:Show();
return cell;
end
-- Constructs cells corresponding to each row with an optional initializer.
function TableBuilderColumnMixin:ConstructCells(templateType, template, ...)
self.args = {...};
self.pool = ColumnFramePoolCollection:GetOrCreatePool(templateType, nil, template);
end
function TableBuilderColumnMixin:GetFillCoefficient()
return self.fillCoefficient;
end
function TableBuilderColumnMixin:SetFillCoefficient(fillCoefficient)
self.fillCoefficient = fillCoefficient;
end
function TableBuilderColumnMixin:GetPadding()
return self.padding;
end
function TableBuilderColumnMixin:SetPadding(padding)
self.padding = padding;
end
function TableBuilderColumnMixin:GetCellPadding()
return self.leftCellPadding or 0, self.rightCellPadding or 0;
end
function TableBuilderColumnMixin:SetCellPadding(leftCellPadding, rightCellPadding)
self.leftCellPadding = leftCellPadding;
self.rightCellPadding = rightCellPadding;
end
function TableBuilderColumnMixin:GetHeaderFrame()
return self.headerFrame;
end
function TableBuilderColumnMixin:SetHeaderFrame(headerFrame)
self.headerFrame = headerFrame;
end
function TableBuilderColumnMixin:GetWidthConstraints()
return self.widthConstraints;
end
function TableBuilderColumnMixin:GetFixedWidth()
return self.fixedWidth;
end
-- A header frame for the column is expected to be constructed or assigned prior to calling this.
-- See ConstructHeader() or SetHeaderFrame().
function TableBuilderColumnMixin:ConstrainToHeader(padding)
local header = self:GetHeaderFrame();
assert(header, "ConstrainToHeader() called with a nil header frame. Use ConstructHeader() or assign one with SetHeaderFrame(), or use SetFixedConstraints to have a headerless column.");
self:SetFixedConstraints(header:GetWidth(), padding or 0);
end
function TableBuilderColumnMixin:SetFixedConstraints(fixedWidth, padding)
self.widthConstraints = ColumnWidthConstraints.Fixed;
self.fixedWidth = fixedWidth;
self:SetFillCoefficient(0);
self:SetPadding(padding or 0);
end
function TableBuilderColumnMixin:SetFillConstraints(fillCoefficient, padding)
self.widthConstraints = ColumnWidthConstraints.Fill;
self.fixedWidth = 0;
self:SetFillCoefficient(fillCoefficient);
self:SetPadding(padding or 0);
end
function TableBuilderColumnMixin:SetCalculatedWidth(calculatedWidth)
self.calculatedWidth = calculatedWidth;
end
function TableBuilderColumnMixin:GetCalculatedWidth()
return self.calculatedWidth;
end
function TableBuilderColumnMixin:GetCellWidth()
local leftCellPadding, rightCellPadding = self:GetCellPadding();
return (self.calculatedWidth - leftCellPadding) - rightCellPadding;
end
function TableBuilderColumnMixin:GetFullWidth()
return self:GetCalculatedWidth() + self:GetPadding();
end
function TableBuilderColumnMixin:SetDisplayUnderPreviousHeader(displayUnderPreviousHeader)
self.displayUnderPreviousHeader = displayUnderPreviousHeader;
end
function TableBuilderColumnMixin:GetDisplayUnderPreviousHeader()
return self.displayUnderPreviousHeader;
end
-- Constructs a table of frames within an existing set of row frames. These row frames could originate from
-- a scroll box or statically fixed set. To populate the table, assign a data provider (CAPI or lua function)
-- that can retrieve an object by index (number).
TableBuilderMixin = {};
function TableBuilderMixin:Init()
self.rows = {};
self.columns = {};
self.leftMargin = 0;
self.rightMargin = 0;
self.columnHeaderOverlap = 0;
self.tableWidth = 0;
self.headerPoolCollection = CreateFramePoolCollection();
end
function TableBuilderMixin:GetDataProvider()
return self.dataProvider;
end
function TableBuilderMixin:SetDataProvider(dataProvider)
self.dataProvider = dataProvider;
end
function TableBuilderMixin:GetDataProviderData(dataProviderKey)
local dataProvider = self:GetDataProvider();
return dataProvider and dataProvider(dataProviderKey) or nil;
end
-- Controls the margins of the left-most and right-most columns within the table.
function TableBuilderMixin:SetTableMargins(leftMargin, rightMargin)
rightMargin = rightMargin or leftMargin; -- Use leftMargin as the default for both.
self.leftMargin = leftMargin;
self.rightMargin = rightMargin;
end
-- Column headers overlap to make a consistent display.
function TableBuilderMixin:SetColumnHeaderOverlap(columnHeaderOverlap)
self.columnHeaderOverlap = columnHeaderOverlap;
end
-- Can be used to set the table width, particularly if no header frames are involved.
function TableBuilderMixin:SetTableWidth(tableWidth)
self.tableWidth = tableWidth;
end
function TableBuilderMixin:GetTableWidth()
return self.tableWidth;
end
function TableBuilderMixin:GetTableMargins()
return self.leftMargin, self.rightMargin;
end
function TableBuilderMixin:GetColumnHeaderOverlap()
return self.columnHeaderOverlap;
end
function TableBuilderMixin:GetColumns()
return self.columns;
end
function TableBuilderMixin:GetHeaderContainer()
return self.headerContainer;
end
function TableBuilderMixin:SetHeaderContainer(headerContainer)
assert(headerContainer, "SetHeaderContainer() with a nil header container. Use ConstructHeader() or assign one with SetHeaderFrame(), or use SetFixedConstraints to have a headerless column.");
self.headerContainer = headerContainer;
self:SetTableWidth(headerContainer:GetWidth());
end
function TableBuilderMixin:GetHeaderPoolCollection()
return self.headerPoolCollection;
end
function TableBuilderMixin:EnumerateHeaders()
return self.headerPoolCollection:EnumerateActive();
end
function TableBuilderMixin:ConstructHeader(templateType, template)
local headerContainer = self:GetHeaderContainer();
assert(headerContainer ~= nil, "A header container must be set with TableBuilderMixin:SetHeaderContainer before adding column headers.")
local headerPoolCollection = self:GetHeaderPoolCollection();
local pool = headerPoolCollection:GetOrCreatePool(templateType, headerContainer, template);
return pool:Acquire(template);
end
function TableBuilderMixin:Arrange()
local columns = self:GetColumns();
if columns and #columns > 0 then
self:CalculateColumnSpacing();
self:ArrangeHeaders();
end
for columnIndex, column in ipairs(self.columns) do
column:Reset();
end
-- Repopulate cells for each row in case the column information changed.
for index, row in ipairs(self.rows) do
assert(TableIsEmpty(row.cells));
for columnIndex, column in ipairs(self.columns) do
assert(row.rowData);
local cell = column:ConstructCell(row, row.rowData, columnIndex);
table.insert(row.cells, cell);
end
self:ArrangeCells(row);
end
end
function TableBuilderMixin:Reset()
self:GetHeaderPoolCollection():ReleaseAll();
for columnIndex, column in ipairs(self.columns) do
column:Reset();
end
self.columns = {};
end
function TableBuilderMixin:AddRow(row, dataProviderKey)
local rowData = self:GetDataProviderData(dataProviderKey);
if not rowData then
return;
end
row.rowData = rowData;
table.insert(self.rows, row);
if row.Populate then
row:Populate(rowData, dataProviderKey);
end
row.cells = {};
for columnIndex, column in ipairs(self.columns) do
local cell = column:ConstructCell(row, rowData, columnIndex);
table.insert(row.cells, cell);
end
self:ArrangeCells(row);
end
function TableBuilderMixin:RemoveRow(row)
local deleted = tDeleteItem(self.rows, row) > 0;
if not deleted then
return;
end
for columnIndex, column in ipairs(self.columns) do
column:RemoveRow(row);
end
row.rowData = nil;
end
function TableBuilderMixin:ArrangeCells(row)
local columns = self:GetColumns();
if #columns == 0 then
return;
end
local height = row:GetHeight();
local leftMargin, rightMargin = self:GetTableMargins();
local column = columns[1];
local cell = row.cells[1];
cell:SetParent(row);
cell:SetHeight(height);
local leftCellPadding, rightCellPadding = column:GetCellPadding();
self:ArrangeHorizontally(cell, row, column:GetCellWidth(), "TOPLEFT", "TOPLEFT", "BOTTOMLEFT", "BOTTOMLEFT", leftMargin + leftCellPadding);
local previousCell = cell;
local previousRightCellPadding = rightCellPadding;
for columnIndex = 2, #columns do
column = columns[columnIndex];
cell = row.cells[columnIndex];
cell:SetParent(row);
cell:SetHeight(height);
leftCellPadding, rightCellPadding = column:GetCellPadding();
self:ArrangeHorizontally(cell, previousCell, column:GetCellWidth(), "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT", column:GetPadding() + leftCellPadding + previousRightCellPadding);
previousCell = cell;
previousRightCellPadding = rightCellPadding;
end
end
function TableBuilderMixin:AddColumn()
local column = CreateAndInitFromMixin(TableBuilderColumnMixin, self);
tinsert(self.columns, column);
return column;
end
function TableBuilderMixin:CalculateColumnSpacing()
-- The arrangement of frames is daisy-chained left to right. The margin on the left side
-- is created by adding the margin to it's anchor offset, and the margin on the right side
-- is created by subtracting space from the remaining fill space.
local columns = self:GetColumns();
local paddingTotal = 0;
local fillCoefficientTotal = 0;
local fixedWidthTotal = 0;
for columnIndex, column in ipairs(columns) do
if column:GetWidthConstraints() == ColumnWidthConstraints.Fill then
fillCoefficientTotal = fillCoefficientTotal + column:GetFillCoefficient();
else
fixedWidthTotal = fixedWidthTotal + column:GetFixedWidth();
end
paddingTotal = paddingTotal + column:GetPadding();
end
local tableWidth = self:GetTableWidth();
local leftMargin, rightMargin = self:GetTableMargins();
local fillWidthTotal = tableWidth - paddingTotal - (leftMargin + rightMargin) - fixedWidthTotal;
for k, column in pairs(columns) do
if fillCoefficientTotal > 0 and column:GetWidthConstraints() == ColumnWidthConstraints.Fill then
local fillRatio = column:GetFillCoefficient() / fillCoefficientTotal;
local width = fillRatio * fillWidthTotal;
column:SetCalculatedWidth(width);
else
local width = column:GetFixedWidth();
column:SetCalculatedWidth(width);
end
end
end
function TableBuilderMixin:ArrangeHorizontally(frame, relativeTo, width, pointTop, pointRelativeTop, pointBottom, pointRelativeBottom, xOffset)
frame:SetPoint(pointTop, relativeTo, pointRelativeTop, xOffset, 0);
frame:SetPoint(pointBottom, relativeTo, pointRelativeBottom, xOffset, 0);
frame:SetWidth(width);
end
function TableBuilderMixin:ArrangeHeaders()
local headerOverlap = self:GetColumnHeaderOverlap();
local leftMargin, rightMargin = self:GetTableMargins();
local columns = self:GetColumns();
-- Any trailing columns without headers should add to the width of the last column with a header.
local numColumns = #columns;
local lastActiveHeaderIndex = numColumns;
local trailingWidth = 0;
for i, column in ipairs(columns) do
if column:GetHeaderFrame() then
trailingWidth = 0;
lastActiveHeaderIndex = i;
else
trailingWidth = trailingWidth + column:GetFullWidth();
end
end
local previousHeader = nil;
local accumulatedWidth = 0;
local columnIndex = 1;
while columnIndex <= numColumns do
local column = columns[columnIndex];
accumulatedWidth = accumulatedWidth + column:GetCalculatedWidth();
local isLastIndex = columnIndex == lastActiveHeaderIndex;
local header = column:GetHeaderFrame();
if header then
if isLastIndex then
accumulatedWidth = accumulatedWidth + trailingWidth;
else
for j = columnIndex + 1, #columns do
local nextColumn = columns[j];
if nextColumn:GetDisplayUnderPreviousHeader() then
columnIndex = columnIndex + 1;
accumulatedWidth = accumulatedWidth + nextColumn:GetFullWidth();
else
break;
end
end
end
if previousHeader == nil then
self:ArrangeHorizontally(header, header:GetParent(), accumulatedWidth, "TOPLEFT", "TOPLEFT", "BOTTOMLEFT", "BOTTOMLEFT", leftMargin);
else
self:ArrangeHorizontally(header, previousHeader, accumulatedWidth + headerOverlap, "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT", column:GetPadding() - headerOverlap);
end
accumulatedWidth = 0;
previousHeader = header;
end
if isLastIndex then
break;
end
columnIndex = columnIndex + 1;
end
end
-- ... are additional mixins
function CreateTableBuilder(rows, ...)
local tableBuilder = CreateAndInitFromMixin(TableBuilderMixin, rows);
Mixin(tableBuilder, ...);
return tableBuilder;
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,87 @@
-- Backport of retail's Blizzard_SharedXMLBase/TaggableObject.lua.
local TAG_BANK_SIZE = 31;
local function GetTagValues(tag)
local index = math.floor(tag / TAG_BANK_SIZE);
local tagIndexInBank = tag - (index * TAG_BANK_SIZE);
local tagBit = bit.lshift(1, tagIndexInBank);
return index, tagBit;
end
local function AddTagToTable(tags, tag)
local index, tagBit = GetTagValues(tag);
local tagValue = tags[index] or 0;
tags[index] = bit.bor(tagValue, tagBit);
end
local function RemoveTagFromTable(tags, tag)
local index, tagBit = GetTagValues(tag);
local tagValue = tags[index];
if tagValue ~= nil and tagValue > 0 then
tags[index] = bit.band(tagValue, bit.bnot(tagBit));
end
end
local function TableHasTag(tags, tag)
local index, tagBit = GetTagValues(tag);
local tagValue = tags[index];
if tagValue ~= nil and tagValue > 0 then
return bit.band(tagValue, tagBit) == tagBit;
end
return false;
end
TaggableObjectMixin = {};
function TaggableObjectMixin:AddTag(tag)
AddTagToTable(GetOrCreateTableEntry(self, "tags"), tag);
end
function TaggableObjectMixin:RemoveTag(tag)
if self.tags then
RemoveTagFromTable(self.tags, tag);
end
end
function TaggableObjectMixin:RemoveAllTags()
self.tags = nil;
end
function TaggableObjectMixin:MatchesTag(tag)
if self.tags then
return TableHasTag(self.tags, tag);
end
return false;
end
function TaggableObjectMixin:MatchesAnyTag(...)
if self.tags then
for i = 1, select("#", ...) do
local tag = select(i, ...);
if TableHasTag(self.tags, tag) then
return true;
end
end
end
return false;
end
function TaggableObjectMixin:MatchesAllTags(...)
if self.tags then
for i = 1, select("#", ...) do
local tag = select(i, ...);
if not TableHasTag(self.tags, tag) then
return false;
end
end
return true;
end
return false;
end
@@ -1,792 +1,27 @@
-- Trade-skill *list* links (`|Htrade:...|h`) — backport of the 2.x+ shareable
-- profession link. The link is built entirely in the DLL
-- (`GetTradeSkillListLink` / `GetCraftListLink`, see src/tradeskill/Link.cpp);
-- this file only intercepts clicks on received links and renders the linker's
-- recipe list in a scroll frame (the UI half).
--
-- link: |cffffd000|Htrade:<skillLineID>:<curRank>:<maxRank>:<linkerName>:<base64 bits>|h[Name]|h|r
--
-- "Which recipes are known" is a bitfield over the skill line's canonical
-- recipe list (deterministic per DBC). The viewer lists the recipes the linker
-- knows (with an "X of Y" summary of the whole skill line), regardless of what
-- the reader can craft themselves.
-- Profession links (`|Htrade:...|h`), 3.3.5's UI half. The DLL builds the
-- link (GetTradeSkillListLink / GetCraftListLink, src/tradeskill/Link.cpp)
-- and opens a received one in the profession window itself
-- (src/tradeskill/Window.cpp), through ItemRefTooltip:SetHyperlink as
-- SetItemRef already calls it. This file ports the parts of 3.3.5's
-- Blizzard_TradeSkillUI that go with that:
-- - TradeSkillLinkButton: a button by the window title that puts the
-- window's link in the chat box, shown when the window has a link and
-- shows the player's own profession;
-- - TradeSkillFrame_SetSelection's linked branch: a window that shows
-- another player's profession hides what only works on the player's own
-- (Create, Create All and the count controls).
-- 3.3.5 never shows whose profession a linked window holds. 4.3.4 does, with
-- TradeSkillLinkNameButton and TradeSkillFrame_SetLinkName, ported here too.
-- The Craft window gets the same through GetCraftListLink and IsCraftLinked.
-- Frame geometry mirrors the real TradeSkillFrame. Turtle ships an HD
-- 768x512 two-column frame (TW-TradeSkill art); stock 1.12.1 only has the
-- vanilla 384x512 single-column art, so on that client we fall back to the
-- native Blizzard_TradeSkillUI geometry (list on top, detail below) rather
-- than stretch the wide art. LO holds every layout value that differs.
local TURTLE = (TURTLE_WOW_VERSION ~= nil);
local FRAME_W = TURTLE and 768 or 384;
local FRAME_H = 512;
local NUM_ROWS = TURTLE and 19 or 8;
local ROW_HEIGHT = 16;
local LO = TURTLE and {
hit = { 2, 82, 2, 73 },
quads = {
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-TopLeft", 512, 256, "TOPLEFT" },
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-TopRight", 256, 256, "TOPRIGHT" },
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-BotLeft2", 512, 256, "BOTTOMLEFT" },
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-BotRight2", 256, 256, "BOTTOMRIGHT" },
},
close = { -77, -8 },
bar = { 596, 15, 76, -44 },
barBdr = { 604, 22 },
summary = { "TOPRIGHT", -110, -70 },
drop = { 40, -60 },
scroll = { 296, 330, 37, -98 },
-- detail scroll frame: { x, y, width, height }. Tall enough that the
-- content fits (scrollbar auto-hides) — Turtle's right column is roomy.
detail = { 371, -110, 340, 290 },
exit = { "CENTER", "BOTTOMRIGHT", -128, 92 },
} or {
hit = { 0, 34, 0, 75 },
quads = {
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopLeft", 256, 256, "TOPLEFT" },
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopRight", 128, 256, "TOPRIGHT" },
{ "Interface\\TradeSkillFrame\\UI-TradeSkill-BotLeft", 256, 256, "BOTTOMLEFT" },
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-BotRight", 128, 256, "BOTTOMRIGHT" },
},
close = { -29, -8 },
bar = { 268, 15, 73, -37 },
barBdr = { 278, 22 },
summary = { "TOPRIGHT", -40, -58 },
drop = { 12, -62 },
scroll = { 296, 130, 21, -96 },
-- detail scroll frame { x, y, width, height } — native TradeSkillFrame's
-- TradeSkillDetailScrollFrame values. Small enough that many-reagent
-- recipes scroll (clipping partial rows), like native.
detail = { 20, -234, 297, 176 },
exit = { "CENTER", "TOPLEFT", 305, -422 },
sep = { 15, -221, 331 },
local TRADE_SKILL_CREATE_CONTROLS = {
"TradeSkillCreateButton",
"TradeSkillCreateAllButton",
"TradeSkillDecrementButton",
"TradeSkillInputBox",
"TradeSkillIncrementButton",
};
-- Reagents render in a 2-column grid inside the detail scroll child (see
-- the detail pane below). The detail is a real ScrollFrame like the
-- native TradeSkillFrame, so the whole pane pixel-scrolls and clips
-- partial reagent rows — `REAGENT_ROW_HEIGHT` is just the vertical stride
-- of one reagent row.
local REAGENT_ROW_HEIGHT = 43;
-- Scroll-frame templates. Stock mimics the native TradeSkillFrame, whose
-- list/detail scrolls use the ClassTrainer templates (they add the
-- decorative UI-ClassTrainer-ScrollBar track). Turtle's HD frame art
-- doesn't want that groove, so it keeps the plain templates. Both detail
-- templates are real ScrollFrames (ClassTrainerDetail... inherits
-- UIPanelScrollFrameTemplate), so the pixel-scroll logic is identical.
local LIST_SCROLL_TEMPLATE = TURTLE and "FauxScrollFrameTemplate"
or "ClassTrainerListScrollFrameTemplate";
local DETAIL_SCROLL_TEMPLATE = TURTLE and "UIPanelScrollFrameTemplate"
or "ClassTrainerDetailScrollFrameTemplate";
local PROFESSION_ICONS = {
[171] = "Trade_Alchemy",
[164] = "Trade_BlackSmithing",
[185] = "INV_Misc_Food_15", -- Cooking
[333] = "Trade_Engraving", -- Enchanting
[202] = "Trade_Engineering",
[129] = "Spell_Holy_SealOfSacrifice", -- First Aid
[165] = "INV_Misc_ArmorKit_17", -- Leatherworking
[197] = "Trade_Tailoring",
[186] = "Trade_Mining",
[182] = "Trade_Herbalism",
[393] = "INV_Misc_Pelt_Wolf_01", -- Skinning
[356] = "Trade_Fishing",
};
if TURTLE then
PROFESSION_ICONS[142] = "Trade_Survival"
PROFESSION_ICONS[755] = "INV_Misc_Gem_01" -- Jewelcrafting
end
local function ProfessionIcon(skillLineID)
local icon = PROFESSION_ICONS[skillLineID];
return icon and ("Interface\\Icons\\" .. icon)
or "Interface\\Icons\\INV_Misc_QuestionMark";
end
local function DifficultyBand(skill, hi, lo)
if not hi or hi <= 0 then
return 3;
end
if not lo or lo == 0 then
lo = (hi ~= 25) and (hi - 25) or 0;
end
local yellow = (hi + lo) / 2;
if skill < lo then
return 0; -- orange (optimal)
elseif skill < yellow then
return 1; -- yellow (medium)
elseif skill < hi then
return 2; -- green (easy)
end
return 3; -- grey (trivial)
end
local BAND_COLOR = {
[0] = DIFFICULT_DIFFICULTY_COLOR, -- orange
[1] = FAIR_DIFFICULTY_COLOR, -- yellow
[2] = EASY_DIFFICULTY_COLOR, -- green
[3] = TRIVIAL_DIFFICULTY_COLOR, -- grey
};
local function CraftableCount(entry)
local reagents = entry.reagents;
if not reagents or table.getn(reagents) == 0 then
return nil;
end
local best;
for i = 1, table.getn(reagents) do
local r = reagents[i];
local need = r.count or 1;
local n = math.floor((C_Item.GetItemCount(r.itemID) or 0) / need);
if not best or n < best then
best = n;
end
end
return best;
end
local function ParseTradeLink(link)
-- Current format, with the linker's name: trade:id:cur:max:Name:bits
local _, _, id, cur, max, who, bits =
string.find(link, "^trade:(%d+):(%d+):(%d+):([^:]+):([%w%+/]*)$");
if not id then
-- Legacy format (no name): trade:id:cur:max:bits
who = nil;
_, _, id, cur, max, bits =
string.find(link, "^trade:(%d+):(%d+):(%d+):([%w%+/]*)$");
end
if not id then
return nil;
end
return tonumber(id), tonumber(cur), tonumber(max), who, bits or "";
end
------------------------------------------------------------------------------
-- Display frame (built lazily on first use)
------------------------------------------------------------------------------
local frame;
local function ResolveSubclasses(list, onLoaded)
local present, subs, anyItem = {}, {}, false;
for i = 1, table.getn(list) do
local r = list[i];
if r.createdItem and r.createdItem > 0 then
anyItem = true;
if not r.subclass then
-- GetItemInfoInstant's 3rd return is the localized subclass
-- name (or nil when uncached).
local _, _, subType = C_Item.GetItemInfoInstant(r.createdItem);
if subType and subType ~= "" then
r.subclass = subType;
elseif onLoaded then
-- Not cached — re-derive when the item loads.
Item:CreateFromItemID(r.createdItem):ContinueOnItemLoad(onLoaded);
end
end
if r.subclass and not present[r.subclass] then
present[r.subclass] = true;
table.insert(subs, r.subclass);
end
end
end
table.sort(subs);
return subs, anyItem;
end
local SUBCLASS_ALL = {};
local function SubClassButton_OnClick()
local v = this.value;
if v == SUBCLASS_ALL then
v = nil;
end
frame.subclassFilter = v;
UIDropDownMenu_SetText(v or ALL_SUBCLASSES, frame.dropdown);
frame:ApplyFilter();
end
local function SubClassDropDown_Initialize()
local info = {};
info.text = ALL_SUBCLASSES;
info.value = SUBCLASS_ALL;
info.checked = (frame.subclassFilter == nil);
info.func = SubClassButton_OnClick;
UIDropDownMenu_AddButton(info);
local subs = frame.subclasses or {};
for i = 1, table.getn(subs) do
local b = {};
b.text = subs[i];
b.value = subs[i];
b.checked = (frame.subclassFilter == subs[i]);
b.func = SubClassButton_OnClick;
UIDropDownMenu_AddButton(b);
end
end
local function RefreshSubclasses()
if not (frame and frame:IsShown() and frame.recipes) then
return;
end
local subs, anyItem = ResolveSubclasses(frame.recipes, RefreshSubclasses);
frame.subclasses = subs;
if anyItem then
UIDropDownMenu_Initialize(frame.dropdown, SubClassDropDown_Initialize);
UIDropDownMenu_SetWidth(120, frame.dropdown);
UIDropDownMenu_SetText(frame.subclassFilter or ALL_SUBCLASSES, frame.dropdown);
frame.dropdown:Show();
else
frame.dropdown:Hide();
end
if frame.subclassFilter then
frame:ApplyFilter();
end
end
local function CreateFrame_TradeSkillLink()
local f = CreateFrame("Frame", "ClassicAPITradeSkillLinkFrame", UIParent);
f:SetSize(FRAME_W, FRAME_H);
f:SetPoint("CENTER");
f:SetMovable(true);
f:EnableMouse(true);
f:SetHitRectInsets(LO.hit[1], LO.hit[2], LO.hit[3], LO.hit[4]);
f:RegisterForDrag("LeftButton");
f:SetScript("OnDragStart", function() this:StartMoving(); end);
f:SetScript("OnDragStop", function() this:StopMovingOrSizing(); end);
f:SetFrameStrata("DIALOG");
f:Hide();
tinsert(UISpecialFrames, f:GetName())
f:SetScript("OnShow", function() PlaySound("igCharacterInfoOpen"); end);
f:SetScript("OnHide", function() PlaySound("igCharacterInfoClose"); end);
local portrait = f:CreateTexture(nil, "BACKGROUND");
portrait:SetSize(60, 60);
portrait:SetPoint("TOPLEFT", 7, -6);
f.portrait = portrait;
for i = 1, table.getn(LO.quads) do
local q = LO.quads[i];
local tx = f:CreateTexture(nil, "BORDER");
tx:SetTexture(q[1]);
tx:SetSize(q[2], q[3]);
tx:SetPoint(q[4]);
end
-- Horizontal divider between the recipe list and the detail pane.
-- Stock only (Turtle's HD frame art already has its own).
if LO.sep then
-- Decoded the BLP: it's a thin divider rod whose graphic lives in the
-- top quarter of the 256x64 texture (a 2nd rod fills the next quarter,
-- bottom half empty). So sample the top quarter, stretched to width —
-- the same region the native TradeSkillFrame uses for this divider.
local bar = f:CreateTexture(nil, "ARTWORK");
bar:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-HorizontalBar");
bar:SetTexCoord(0, 1, 0, 0.25);
bar:SetSize(LO.sep[3], 16);
bar:SetPoint("TOPLEFT", LO.sep[1], LO.sep[2]);
end
local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormal");
title:SetPoint("TOP", 0, -18);
f.title = title;
local close = CreateFrame("Button", nil, f, "UIPanelCloseButton");
close:SetPoint("TOPRIGHT", LO.close[1], LO.close[2]);
local exit = CreateFrame("Button", nil, f, "UIPanelButtonTemplate");
exit:SetSize(80, 22);
exit:SetPoint(LO.exit[1], f, LO.exit[2], LO.exit[3], LO.exit[4]);
exit:SetText(EXIT);
exit:SetScript("OnClick", function() f:Hide(); end);
local bar = CreateFrame("StatusBar", nil, f);
bar:SetSize(LO.bar[1], LO.bar[2]);
bar:SetPoint("TOPLEFT", LO.bar[3], LO.bar[4]);
bar:SetStatusBarTexture("Interface\\PaperDollInfoFrame\\UI-Character-Skills-Bar");
bar:SetStatusBarColor(0.0, 0.0, 1.0, 0.5);
bar:SetMinMaxValues(0, 1);
local barBg = bar:CreateTexture(nil, "BACKGROUND");
barBg:SetAllPoints(bar);
barBg:SetTexture(1, 1, 1, 0.2);
barBg:SetVertexColor(0.0, 0.0, 0.75, 0.5);
-- Border: tooltip edge around the bar (TradeSkillRankFrameBorder).
local border = CreateFrame("Frame", nil, bar);
border:SetSize(LO.barBdr[1], LO.barBdr[2]);
border:SetPoint("LEFT", -5, 1);
border:SetBackdrop({
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 16,
});
border:SetBackdropBorderColor(0.4, 0.4, 0.4);
local barName = bar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall");
barName:SetPoint("LEFT", 6, 1);
local barRank = bar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall");
barRank:SetJustifyH("LEFT");
barRank:SetWidth(128);
barRank:SetPoint("LEFT", barName, "RIGHT", 13, 0);
f.bar = bar;
f.barName = barName;
f.barRank = barRank;
local summary = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall");
summary:SetPoint(LO.summary[1], LO.summary[2], LO.summary[3]);
f.summary = summary;
local dropdown = CreateFrame("Frame", "ClassicAPITradeSkillLinkSubClass", f,
"UIDropDownMenuTemplate");
dropdown:SetPoint("TOPLEFT", LO.drop[1], LO.drop[2]);
f.dropdown = dropdown;
f.subclassFilter = nil; -- nil = all subclasses
local scroll = CreateFrame("ScrollFrame", "ClassicAPITradeSkillLinkScroll",
f, LIST_SCROLL_TEMPLATE);
scroll:SetSize(LO.scroll[1], LO.scroll[2]);
scroll:SetPoint("TOPLEFT", LO.scroll[3], LO.scroll[4]);
scroll:SetScript("OnVerticalScroll", function()
FauxScrollFrame_OnVerticalScroll(ROW_HEIGHT, function() f:Refresh(); end)
end)
f.scroll = scroll;
f.rows = {};
for i = 1, NUM_ROWS do
local rowName = "ClassicAPITradeSkillLinkRow" .. i;
local row = CreateFrame("Button", rowName, f,
"ClassTrainerSkillButtonTemplate");
row:SetWidth(290); -- height (16) comes from the template
if i == 1 then
row:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0);
else
row:SetPoint("TOPLEFT", f.rows[i - 1], "BOTTOMLEFT", 0, 0);
end
row:SetNormalTexture("");
row:SetDisabledTexture("");
row:SetHighlightTexture("");
local txt = getglobal(rowName .. "Text");
if txt then
txt:ClearAllPoints();
txt:SetPoint("LEFT", row, "LEFT", 4, 0);
end
local sel = row:CreateTexture(nil, "BACKGROUND");
sel:SetTexture("Interface\\Buttons\\UI-Listbox-Highlight2");
sel:SetAllPoints(row);
sel:Hide();
row.select = sel;
-- Override the template's trainer scripts with ours.
row:SetScript("OnClick", function()
if this.entry then
f:SelectRecipe(this.entry);
end
end);
f.rows[i] = row;
end
-- Detail pane: a real ScrollFrame (like the native TradeSkillFrame's
-- TradeSkillDetailScrollFrame) whose scroll child holds the product
-- header + reagent grid, so the whole pane pixel-scrolls — clipping
-- partial reagent rows — with the standard UIPanelScrollBar. The bar
-- auto-hides when the content fits (scrollBarHideable).
local detailScroll = CreateFrame("ScrollFrame",
"ClassicAPITradeSkillLinkDetailScroll", f, DETAIL_SCROLL_TEMPLATE);
detailScroll:SetSize(LO.detail[3], LO.detail[4]);
detailScroll:SetPoint("TOPLEFT", LO.detail[1], LO.detail[2]);
-- Bar visibility + wheel are driven per-render by SetDetailBarShown (from
-- the computed overflow). `scrollBarHideable` still lets the template's
-- ScrollFrame_OnScrollRangeChanged manage the slider on range changes.
-- Start with the wheel off; the first RenderDetail/ClearDetail sets it.
detailScroll.scrollBarHideable = 1;
detailScroll:EnableMouseWheel(false);
f.detailScroll = detailScroll;
local detailChild = CreateFrame("Frame",
"ClassicAPITradeSkillLinkDetailChild", detailScroll);
detailChild:SetSize(LO.detail[3], LO.detail[4]);
detailScroll:SetScrollChild(detailChild);
f.detailChild = detailChild;
local prodIcon = CreateFrame("Button", nil, detailChild);
prodIcon:SetSize(37, 37); -- native TradeSkillSkillIcon (37x37 @ 8,-3)
prodIcon:SetPoint("TOPLEFT", 8, -3);
prodIcon:SetNormalTexture("Interface\\Icons\\INV_Misc_QuestionMark");
local hdrLeft = detailChild:CreateTexture(nil, "BACKGROUND");
hdrLeft:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-DetailHeaderLeft");
hdrLeft:SetSize(256, 64); -- Blizzard's native fill width (+64 cap = 320 total)
hdrLeft:SetPoint("TOPLEFT", prodIcon, "TOPLEFT", -8, 6);
local hdrRight = detailChild:CreateTexture(nil, "BACKGROUND");
hdrRight:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-DetailHeaderRight");
hdrRight:SetSize(64, 64);
hdrRight:SetPoint("TOPLEFT", hdrLeft, "TOPRIGHT", 0, 0);
local prodCount = prodIcon:CreateFontString(nil, "ARTWORK", "NumberFontNormal");
prodCount:SetPoint("BOTTOMRIGHT", -5, 2);
prodIcon.count = prodCount;
f.prodIcon = prodIcon;
local prodName = detailChild:CreateFontString(nil, "ARTWORK", "GameFontNormal");
prodName:SetPoint("LEFT", prodIcon, "RIGHT", 5, 0);
prodName:SetWidth(LO.detail[3] - 60);
prodName:SetJustifyH("LEFT");
prodName:SetJustifyV("MIDDLE");
f.prodName = prodName;
local prodDesc = detailChild:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall");
prodDesc:SetPoint("TOPLEFT", prodIcon, "BOTTOMLEFT", 0, -6);
prodDesc:SetWidth(LO.detail[3] - 20);
prodDesc:SetJustifyH("LEFT");
prodDesc:SetJustifyV("TOP");
f.prodDesc = prodDesc;
local reagentLabel = detailChild:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall");
reagentLabel:SetPoint("TOPLEFT", prodDesc, "BOTTOMLEFT", 0, -8);
reagentLabel:SetText(SPELL_REAGENTS or "Reagents:");
f.reagentLabel = reagentLabel;
prodIcon:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT");
if this.itemID then
GameTooltip:SetHyperlink("item:"..this.itemID);
elseif this.spellID then
GameTooltip:SetSpellByID(this.spellID);
end
GameTooltip:Show();
end);
prodIcon:SetScript("OnLeave", GameTooltip_Hide);
-- Reagent buttons live in the scroll child (2 per row), built on
-- demand so any reagent count is handled and they scroll/clip with
-- the child.
f.reagents = {};
local function AcquireReagent(i)
if f.reagents[i] then
return f.reagents[i];
end
local rowN = math.floor((i - 1) / 2);
local col = (i - 1) - rowN * 2;
local btn = CreateFrame("Button", nil, detailChild);
btn:SetSize(39, 39);
btn:SetPoint("TOPLEFT", reagentLabel, "BOTTOMLEFT",
col * 134 - 5, -3 - rowN * REAGENT_ROW_HEIGHT);
-- Extend the hit area rightward over the name plate so the tooltip
-- fires anywhere on the element, not just the icon. Stop short of the
-- next column (pitch 134 − icon 39) so hovers don't bleed across.
btn:SetHitRectInsets(0, -(134 - 39), 0, 0);
btn:SetNormalTexture("Interface\\Icons\\INV_Misc_QuestionMark");
local plate = btn:CreateTexture(nil, "BACKGROUND");
plate:SetTexture("Interface\\QuestFrame\\UI-QuestItemNameFrame");
plate:SetSize(114, 64);
plate:SetPoint("LEFT", btn, "RIGHT", -10, 0);
local cnt = btn:CreateFontString(nil, "ARTWORK", "NumberFontNormal");
cnt:SetPoint("BOTTOMRIGHT", -4, 1);
btn.count = cnt;
local nm = btn:CreateFontString(nil, "ARTWORK", "GameFontHighlight");
nm:SetPoint("LEFT", plate, "LEFT", 15, 0);
nm:SetSize(80, 36);
nm:SetJustifyH("LEFT");
nm:SetJustifyV("MIDDLE");
btn.nameText = nm;
btn:SetScript("OnEnter", function()
if this.itemID then
GameTooltip:SetOwner(this, "ANCHOR_RIGHT");
GameTooltip:SetHyperlink("item:"..this.itemID);
GameTooltip:Show();
end
end);
btn:SetScript("OnClick", function()
if this.itemLink and IsShiftKeyDown() and ChatFrameEditBox:IsVisible() then
ChatFrameEditBox:Insert(this.itemLink);
end
end);
btn:SetScript("OnLeave", GameTooltip_Hide);
f.reagents[i] = btn;
return btn;
end
-- Show the detail bar/borders only when the content overflows the pane.
-- Driven by our own computed overflow, NOT the engine: scrollBarHideable's
-- auto-hide only fires on a range CHANGE (so content that fits from the
-- start never hides), and GetVerticalScrollRange isn't reliably readable
-- the same tick as UpdateScrollChildRect.
function f:SetDetailBarShown(show)
self.detailScroll:EnableMouseWheel(show);
local base = "ClassicAPITradeSkillLinkDetailScroll";
local sb = getglobal(base .. "ScrollBar");
if sb then sb:SetShown(show) end
local top = getglobal(base .. "Top");
if top then top:SetShown(show) end
local bottom = getglobal(base .. "Bottom");
if bottom then bottom:SetShown(show) end
end
function f:RenderDetail(entry)
local function itemInfo(id)
local item = Item:CreateFromItemID(id);
if not item:IsItemDataCached() then
item:ContinueOnItemLoad(function()
if self:IsShown() and self.selectedSpellID == entry.spellID then
self:RenderDetail(entry);
end
end);
end
return item:GetItemName(), item:GetItemIcon(), item:GetItemLink();
end
local prodTex, prodText, prodLink;
local created = entry.createdItem or 0;
if created > 0 then
local name, tex, link = itemInfo(created);
prodText = name or entry.name;
prodTex = tex;
prodLink = link;
else
prodText = entry.name;
prodTex = C_Spell.GetSpellTexture(entry.spellID);
end
local descText = (created > 0) and ""
or (C_Spell.GetSpellDescription(entry.spellID) or "");
self.prodDesc:SetText(descText);
self.prodIcon:SetNormalTexture(prodTex or
"Interface\\Icons\\INV_Misc_QuestionMark");
self.prodName:SetText(prodText or "");
self.prodName:SetTextColor(NORMAL_FONT_COLOR:GetRGB());
self.prodIcon.itemLink = prodLink; -- item tooltip when created > 0
self.prodIcon.spellID = entry.spellID; -- else spell tooltip
local made = entry.numMade or 0;
self.prodIcon.count:SetText(made > 1 and tostring(made) or "");
self.prodIcon:Show();
-- Fill every reagent slot; the scroll child clips/scrolls the
-- overflow. Buttons past the count are hidden.
local reagents = entry.reagents or {};
local total = table.getn(reagents);
for i = 1, total do
local btn = AcquireReagent(i);
local reagent = reagents[i];
local name, tex, link = itemInfo(reagent.itemID);
btn:SetNormalTexture(tex or "Interface\\Icons\\INV_Misc_QuestionMark");
btn.itemLink = link;
btn.itemID = reagent.itemID
btn.nameText:SetText(name or ("item:" .. reagent.itemID));
local need = reagent.count or 1;
local have = C_Item.GetItemCount(reagent.itemID) or 0;
if have >= need then
local r, g, b = HIGHLIGHT_FONT_COLOR:GetRGB()
btn:GetNormalTexture():SetVertexColor(r, g, b);
btn.nameText:SetTextColor(r, g, b);
else
local r, g, b = GRAY_FONT_COLOR:GetRGB()
btn:GetNormalTexture():SetVertexColor(r, g, b);
btn.nameText:SetTextColor(r, g, b);
end
local haveText = (have >= 100) and "*" or tostring(have);
btn.count:SetText(haveText .. " /" .. need);
btn:Show();
end
for i = total + 1, table.getn(self.reagents) do
self.reagents[i].itemLink = nil;
self.reagents[i].itemID = nil;
self.reagents[i]:Hide();
end
local function applyContentHeight()
local contentH = LO.detail[4];
if total > 0 then
local childTop = self.detailChild:GetTop();
local lastBtn = self.reagents[total];
local lastBottom = lastBtn and lastBtn:GetBottom();
if childTop and lastBottom then
-- +22: ~12px name-plate overhang below the icon + ~10px pad.
contentH = (childTop - lastBottom) + 22;
end
end
self.detailChild:SetHeight(math.max(contentH, LO.detail[4]));
-- Recompute the scroll range from the child's new height, then
-- show the bar only if the content actually overflows the pane.
self.detailScroll:UpdateScrollChildRect();
self:SetDetailBarShown(contentH > LO.detail[4] + 0.5);
end
applyContentHeight();
local sb = getglobal("ClassicAPITradeSkillLinkDetailScrollScrollBar");
if sb then
sb:SetValue(0);
end
self.detailScroll:SetVerticalScroll(0);
RunNextFrame(function()
if self:IsShown() and self.selectedSpellID == entry.spellID then
applyContentHeight();
end
end)
end
function f:SelectRecipe(entry)
self.selectedSpellID = entry.spellID;
self.selectedEntry = entry;
self:RenderDetail(entry); -- also resets the detail scroll to top
self:Refresh();
end
function f:ClearDetail()
self.selectedSpellID = nil;
self.selectedEntry = nil;
self.prodIcon:Hide();
self.prodName:SetText("");
self.prodDesc:SetText("");
for i = 1, table.getn(self.reagents) do
self.reagents[i].itemLink = nil;
self.reagents[i].itemID = nil;
self.reagents[i]:Hide();
end
self.detailChild:SetHeight(LO.detail[4]);
self.detailScroll:UpdateScrollChildRect();
self:SetDetailBarShown(false);
self.detailScroll:SetVerticalScroll(0);
end
function f:Refresh()
local recipes = self.displayed or {};
local total = table.getn(recipes);
FauxScrollFrame_Update(self.scroll, total, NUM_ROWS, ROW_HEIGHT);
local offset = FauxScrollFrame_GetOffset(self.scroll);
for i = 1, NUM_ROWS do
local row = self.rows[i];
local entry = recipes[offset + i];
if entry then
-- Append " [N]" when the reader can craft any (Blizzard's list).
local craftable = CraftableCount(entry);
local label = entry.name or "";
if craftable and craftable > 0 then
label = label .. " [" .. craftable .. "]";
end
row:SetText(label);
local r, g, b = BAND_COLOR[entry.band or 3]:GetRGB();
if self.selectedSpellID and entry.spellID == self.selectedSpellID then
row.select:SetVertexColor(r, g, b);
row.select:Show();
row:SetTextColor(1, 1, 1);
else
row.select:Hide();
row:SetTextColor(r, g, b);
end
row.spellID = entry.spellID;
row.entry = entry;
row:Show();
else
row.spellID = nil;
row.entry = nil;
row.select:Hide();
row:Hide();
end
end
end
function f:ApplyFilter()
local src = self.recipes or {};
if not self.subclassFilter then
self.displayed = src;
else
local out = {};
for i = 1, table.getn(src) do
if src[i].subclass == self.subclassFilter then
table.insert(out, src[i]);
end
end
self.displayed = out;
end
local sb = getglobal("ClassicAPITradeSkillLinkScrollScrollBar");
if sb then
sb:SetValue(0);
end
self:Refresh();
end
f:RegisterEvent("BAG_UPDATE_DELAYED");
f:SetScript("OnEvent", function()
if not f:IsShown() then
return;
end
f:Refresh();
if f.selectedEntry then
f:RenderDetail(f.selectedEntry);
end
end);
return f;
end
local function ShowTradeSkillLink(link, text)
local id, cur, max, who, bits = ParseTradeLink(link);
if not id then
return false;
end
local recipes = C_TradeSkillUI.GetTradeSkillListRecipes(id, bits);
if not recipes then
return false;
end
local prof = text and string.match(text, "|h%[(.+)%]|h") or "Trade Skills";
local total = table.getn(recipes);
local knownList = {};
for i = 1, total do
local r = recipes[i];
if r.isKnown then
r.band = DifficultyBand(cur or 0, r.trivialLevel, r.greenLevel);
r.name = C_Spell.GetSpellName(r.spellID) or ("spell:" .. r.spellID);
table.insert(knownList, r);
end
end
table.sort(knownList, function(a, b)
if a.band ~= b.band then
return a.band < b.band;
end
return a.name < b.name;
end);
local known = table.getn(knownList);
if not frame then
frame = CreateFrame_TradeSkillLink();
end
frame.title:SetText(who and who ~= "" and (who .. " - " .. prof) or prof);
SetPortraitToTexture(frame.portrait, ProfessionIcon(id));
frame.bar:SetMinMaxValues(0, (max and max > 0) and max or 1);
frame.bar:SetValue(cur or 0);
frame.barName:SetText(prof);
frame.barRank:SetFormattedText("%d/%d", cur or 0, max or 0);
frame.summary:SetFormattedText("%d of %d recipes known", known, total);
frame.recipes = knownList;
frame.subclassFilter = nil;
frame:ClearDetail();
frame:Show();
RefreshSubclasses();
frame:ApplyFilter();
local first = frame.displayed and frame.displayed[1];
if first then
frame:SelectRecipe(first);
end
return true;
end
------------------------------------------------------------------------------
-- Click interception: handle `trade:` before the engine's SetHyperlink (which
-- in 1.12 only knows item:/enchant: and would reject the link). Registered
-- on LinkUtil's SetItemRef dispatch (Util/LinkUtil.lua) instead of wrapping
-- SetItemRef a second time, so there is exactly one override to chain.
------------------------------------------------------------------------------
LinkUtil.RegisterLinkHandler("trade", function(link, text, linkData, contextData)
if IsShiftKeyDown() and ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
ChatFrameEditBox:Insert(text);
return LinkProcessorResponse.Handled;
end
if ShowTradeSkillLink(link, text) then
return LinkProcessorResponse.Handled;
end
return LinkProcessorResponse.Unhandled;
end);
local function CreateProfessionLinkButton(parentName, getLink)
local parent = _G[parentName];
local btnName = parentName .. "LinkButton";
@@ -832,24 +67,84 @@ local function CreateProfessionLinkButton(parentName, getLink)
GameTooltip:Show();
end);
btn:SetScript("OnLeave", GameTooltip_Hide);
return btn;
end
local TRADE_SKILL_LINKED_NAME_WIDTH = 120;
-- 4.3.4's TradeSkillLinkNameButton: "[name]" after the title of a linked
-- window, a whisper to that player on click. The returned function is
-- TradeSkillFrame_SetLinkName. It moves the title left by half the name, from
-- the title's own anchor, so the pair stays centered where the title was.
local function CreateLinkNameButton(parentName, isLinked)
local title = _G[parentName .. "TitleText"];
local point, relativeTo, relativePoint, x, y = title:GetPoint();
local btn = CreateFrame("Button", parentName .. "LinkNameButton", _G[parentName]);
btn:SetHeight(20);
btn:SetPoint("LEFT", title, "RIGHT", 5, 0);
local text = btn:CreateFontString(btn:GetName() .. "TitleText", "OVERLAY", "GameFontHighlight");
text:SetAllPoints(btn);
btn:SetScript("OnClick", function()
PlaySound("igMainMenuOptionCheckBoxOn");
if this.linkedName then
ChatFrame_OpenChat(SLASH_WHISPER1 .. " " .. this.linkedName .. " ", DEFAULT_CHAT_FRAME);
end
end);
btn:Hide();
return function()
local _, linkedName = isLinked();
title:ClearAllPoints();
if linkedName then
btn:Show();
text:SetText("[" .. linkedName .. "]");
local linkedNameWidth = math.min(text:GetStringWidth(), TRADE_SKILL_LINKED_NAME_WIDTH);
btn:SetWidth(linkedNameWidth);
btn.linkedName = linkedName;
title:SetPoint(point, relativeTo, relativePoint, x - linkedNameWidth / 2, y);
else
btn:Hide();
title:SetPoint(point, relativeTo, relativePoint, x, y);
end
end
end
EventUtil.ContinueOnAddOnLoaded("Blizzard_TradeSkillUI", function()
CreateProfessionLinkButton("TradeSkillFrame", C_TradeSkillUI.GetTradeSkillListLink);
local linkButton = CreateProfessionLinkButton("TradeSkillFrame", GetTradeSkillListLink);
local setLinkName = CreateLinkNameButton("TradeSkillFrame", IsTradeSkillLinked);
hooksecurefunc("TradeSkillFrame_SetSelection", function()
local linked = IsTradeSkillLinked();
for i = 1, table.getn(TRADE_SKILL_CREATE_CONTROLS) do
_G[TRADE_SKILL_CREATE_CONTROLS[i]]:SetShown(not linked);
end
if linkButton then
linkButton:SetShown(not linked and GetTradeSkillListLink() ~= nil);
end
setLinkName();
end);
end)
EventUtil.ContinueOnAddOnLoaded("Blizzard_CraftUI", function()
CreateProfessionLinkButton("CraftFrame", C_TradeSkillUI.GetCraftListLink)
local linkButton = CreateProfessionLinkButton("CraftFrame", GetCraftListLink);
local setLinkName = CreateLinkNameButton("CraftFrame", IsCraftLinked);
hooksecurefunc("CraftFrame_SetSelection", function()
local linked = IsCraftLinked();
CraftCreateButton:SetShown(not linked);
if linkButton then
linkButton:SetShown(not linked and GetCraftListLink() ~= nil);
end
setLinkName();
end);
end)
EventUtil.ContinueOnAddOnLoaded("AdvancedTradeSkillWindow", function()
CreateProfessionLinkButton("ATSWFrame", function()
return atsw_oldmode and C_TradeSkillUI.GetCraftListLink() or C_TradeSkillUI.GetTradeSkillListLink()
return atsw_oldmode and GetCraftListLink() or GetTradeSkillListLink()
end)
end)
EventUtil.ContinueOnAddOnLoaded("AdvancedTradeSkillWindow2", function()
CreateProfessionLinkButton("ATSWFrame", function()
return ATSW_TradeSkill() and C_TradeSkillUI.GetTradeSkillListLink() or C_TradeSkillUI.GetCraftListLink()
return ATSW_TradeSkill() and GetTradeSkillListLink() or GetCraftListLink()
end)
end)
@@ -0,0 +1,31 @@
-- Backport of retail's Blizzard_SharedXML/TypeUtil.lua.
--These functions are terrible, but they support legacy slash commands.
function ValueToBoolean(valueToCheck, defaultValue, defaultReturn)
if ( type(valueToCheck) == "nil" ) then
return false;
elseif ( type(valueToCheck) == "boolean" ) then
return valueToCheck;
elseif ( type(valueToCheck) == "number" ) then
return valueToCheck ~= 0;
elseif ( type(valueToCheck) == "string" ) then
return StringToBoolean(valueToCheck, defaultReturn);
else
return defaultReturn;
end
end
function StringToBoolean(stringToCheck, defaultReturn)
stringToCheck = string.lower(stringToCheck);
local firstChar = string.sub(stringToCheck, 1, 1);
if ( firstChar == "0" or firstChar == "n" or firstChar == "f" or stringToCheck == "off" or stringToCheck == "disabled" ) then
return false;
elseif ( firstChar == "1" or firstChar == "2" or firstChar == "3" or firstChar == "4" or firstChar == "5" or
firstChar == "6" or firstChar == "7" or firstChar == "8" or firstChar == "9" or firstChar == "y" or
firstChar == "t" or stringToCheck == "on" or stringToCheck == "enabled" ) then
return true;
end
return defaultReturn;
end
@@ -9,6 +9,7 @@ BINDING_NAME_INTERACTMOUSEOVER = "Mit Mouseover interagieren";
BINDING_NAME_INTERACTTARGET = "Mit Ziel interagieren";
BINDING_NAME_TARGETFOCUS = "Fokus anvisieren";
COMBAT_LABEL = "Kampf";
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Zahlen für Abklingzeiten anzeigen";
CONFIRM_DELETE_EQUIPMENT_SET = "Seid Ihr sicher, dass Ihr das Ausrüstungsset \"%s\" löschen wollt?";
CONFIRM_SAVE_EQUIPMENT_SET = "Möchtet Ihr das Ausrüstungsset \"%s\" speichern?";
DEBUFF_SYMBOL_CURSE = "Fl";
@@ -40,6 +41,7 @@ INTERACT_ICONS_SHOW_ALL = "Alles anzeigen";
INTERACT_ICONS_SHOW_NONE = "Nichts anzeigen";
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Vordefinierte Tastaturbelegungen für Aktionen reagieren bei gedrückter statt bei losgelassener Taste.";
OPTION_TOOLTIP_ACTION_TARGETING = "Ein Zielerfassungssystem, das Gegner auf Grundlage Eurer Blickrichtung dynamisch anvisiert. Funktioniert im Zusammenspiel mit dem Standardzielerfassungssystem.";
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Markieren, um für Abklingzeiten einen numerischen Countdown auf Aktionsleisten anzuzeigen.";
OPTION_TOOLTIP_ENABLE_INTERACT = "Erlaubt Interaktion mit NSCs und Objekten per Tastendruck";
OPTION_TOOLTIP_INTERACT_ICONS = "Legt fest, welche Symbole über NSCs und Gegenständen angezeigt werden, wenn die Interaktionstastenfunktion aktiviert ist.";
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Automatischen Angriff stoppen, sobald das Ziel gewechselt wird.";
@@ -100,6 +102,15 @@ ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "Rüstungsdurchschlag";
ITEM_MOD_HEALTH_REGEN_SHORT = "Gesundheitsregeneration";
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "Schaden pro Sekunde";
LARGE_NUMBER_SEPERATOR = ".";
DECIMAL_SEPERATOR = ",";
FIRST_NUMBER = "Tausend";
FIRST_NUMBER_CAP = " K";
FIRST_NUMBER_CAP_NO_SPACE = "K";
SECOND_NUMBER = "|7Million:Millionen;";
SECOND_NUMBER_CAP = " M";
SECOND_NUMBER_CAP_NO_SPACE = "M";
THIRD_NUMBER = "|7Milliarde:Milliarden;";
FOURTH_NUMBER = "|7Billion:Billionen;";
MINUTES_SECONDS = "%.2d:%.2d";
PAPERDOLL_EQUIPMENTMANAGER = "Ausrüstungsmanager";
PAPERDOLL_NEWEQUIPMENTSET = "Neues Set";
@@ -8,7 +8,88 @@ BINDING_NAME_FOCUSTARGET = "Focus Target";
BINDING_NAME_INTERACTMOUSEOVER = "Interact With Mouseover";
BINDING_NAME_INTERACTTARGET = "Interact With Target";
BINDING_NAME_TARGETFOCUS = "Target Focus";
CLASSICAPI_ACTION_BARS = "Action Bars";
CLASSICAPI_ACTION_TARGETING = "Action Targeting";
CLASSICAPI_ADVANCED = "Advanced";
CLASSICAPI_ALL_SOFT_TARGET_ICONS = "Icons for All Soft Targets";
CLASSICAPI_AUTO_DISMOUNT = "Auto Dismount";
CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "Dismount when you cast a spell while you are mounted.";
CLASSICAPI_AUTO_STAND = "Auto Stand";
CLASSICAPI_AUTO_STAND_TOOLTIP = "Stand up when you cast a spell while you sit.";
CLASSICAPI_AUTO_UNSHIFT = "Auto Cancel Form";
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Leave your form when you cast a spell that you cannot cast in it.";
CLASSICAPI_BAGS_AND_ITEMS = "Bags and Items";
CLASSICAPI_CASTING_AND_ATTACKING = "Casting and Attacking";
CLASSICAPI_COMBAT_LOG_RETENTION = "Combat Log Retention";
CLASSICAPI_COMBAT_LOG_RETENTION_TOOLTIP = "How many seconds the combat log keeps entries for addons.";
CLASSICAPI_DIRECTION_ANY = "Any direction";
CLASSICAPI_DIRECTION_CONE = "Within 30 degrees";
CLASSICAPI_DIRECTION_FRONT = "Directly in front";
CLASSICAPI_ENEMIES = "Enemies";
CLASSICAPI_ENEMY_DIRECTION = "Enemy Direction";
CLASSICAPI_ENEMY_ICON = "Show Enemy Icon";
CLASSICAPI_ENEMY_ICON_TOOLTIP = "Shows an icon over the enemy that action targeting picks.";
CLASSICAPI_ENEMY_RANGE = "Enemy Range";
CLASSICAPI_FRIEND_DIRECTION = "Friend Direction";
CLASSICAPI_FRIEND_ICON = "Show Friend Icon";
CLASSICAPI_FRIEND_ICON_TOOLTIP = "Shows an icon over the friendly unit that friendly action targeting picks.";
CLASSICAPI_FRIEND_RANGE = "Friend Range";
CLASSICAPI_FRIEND_TARGETING = "Enable Friendly Action Targeting";
CLASSICAPI_FRIEND_TARGETING_TOOLTIP = "Picks the nearest friendly unit where you are looking. When you have no target, your helpful spells go to it.";
CLASSICAPI_FRIENDS = "Friends";
CLASSICAPI_GAME_OBJECT_ICON = "Show Interact Icon on Objects";
CLASSICAPI_GAME_OBJECT_ICON_TOOLTIP = "Also shows the interact icon over objects that you cannot target, such as mailboxes and chests.";
CLASSICAPI_HIDE_EQUIPMENT_SETS = "Hide Equipment Sets on Items";
CLASSICAPI_HIDE_EQUIPMENT_SETS_TOOLTIP = "Removes the Equipment Sets line from item tooltips.";
CLASSICAPI_ICON_SIZE = "Icon Size";
CLASSICAPI_ICON_SIZE_TOOLTIP = "The size of the interact, enemy and friend icons.";
CLASSICAPI_IGNORE_BACKPACK = "Ignore Backpack When Sorting";
CLASSICAPI_IGNORE_BACKPACK_TOOLTIP = "Sorting your bags leaves the items in your backpack where they are.";
CLASSICAPI_IGNORE_BANK = "Ignore Main Bank When Sorting";
CLASSICAPI_IGNORE_BANK_TOOLTIP = "Sorting your bank leaves the items in the main bank slots where they are.";
CLASSICAPI_INTERACT_DIRECTION = "Interact Direction";
CLASSICAPI_INTERACT_DIRECTION_TOOLTIP = "Where an NPC or object must be for the Interact Key to pick it. The direction is the camera's, not your character's.";
CLASSICAPI_INTERACT_ICON = "Show Interact Icon";
CLASSICAPI_INTERACT_ICON_TOOLTIP = "Shows an icon over the NPC or object that the Interact Key uses.";
CLASSICAPI_INTERACT_KEY = "Interact Key";
CLASSICAPI_INTERACT_RANGE = "Interact Range";
CLASSICAPI_INTERFACE = "Interface";
CLASSICAPI_KEEP_ACTION_TARGETING = "Keep Action Targeting";
CLASSICAPI_KEEP_ACTION_TARGETING_TOOLTIP = "Action targeting still picks enemies and friends while you have a target that you chose.";
CLASSICAPI_LOOSE_TARGET_FOLLOWS = "Loose Target Follows";
CLASSICAPI_LOOSE_TARGET_FOLLOWS_ENEMY = "Soft enemy";
CLASSICAPI_LOOSE_TARGET_FOLLOWS_FRIEND = "Soft friend";
CLASSICAPI_LOOSE_TARGET_FOLLOWS_NONE = "Nothing";
CLASSICAPI_LOOSE_TARGET_FOLLOWS_TOOLTIP = "A target that action targeting set changes with the soft enemy or the soft friend. A target that you chose stays.";
CLASSICAPI_LOW_PRIORITY_ICONS = "Show Low Priority Icons";
CLASSICAPI_LOW_PRIORITY_ICONS_TOOLTIP = "Also shows an icon over a unit that you can do nothing with, such as a party member. The icon is the normal pointer.";
CLASSICAPI_MOTION = "Motion";
CLASSICAPI_NAMEPLATES = "Nameplates";
CLASSICAPI_NUMBERS = "Numbers";
CLASSICAPI_OPTIONS_ABOUT = "Settings for the features that ClassicAPI adds to the game. Each option changes one setting, and the change applies at once.";
CLASSICAPI_OTHER_NAMEPLATE_ALPHA = "Other Nameplate Opacity";
CLASSICAPI_OTHER_NAMEPLATE_ALPHA_TOOLTIP = "The opacity of the nameplates of units that are not your target.";
CLASSICAPI_PREFER_YOUR_TARGET = "Prefer Your Target";
CLASSICAPI_PREFER_YOUR_TARGET_TOOLTIP = "When the target that you chose can be picked, action targeting picks it. When this is off, it always picks the nearest unit.";
CLASSICAPI_RANGE_TOOLTIP = "The maximum distance in yards. It cannot be more than the targetNearestDistance setting.";
CLASSICAPI_REMEMBER_PLAYERS = "Remember Players";
CLASSICAPI_REMEMBER_PLAYERS_TOOLTIP = "Saves the name, class, race and sex of the players you meet, so addons can show them in later sessions. All your characters on this realm share the data.";
CLASSICAPI_SCRIPT_PROFILE = "Script Profiling";
CLASSICAPI_SCRIPT_PROFILE_TOOLTIP = "Measures the CPU time that addons use. The change starts after you reload the interface.";
CLASSICAPI_SEPARATE_LARGE_NUMBERS = "Separate Large Numbers";
CLASSICAPI_SEPARATE_LARGE_NUMBERS_TOOLTIP = "Puts a separator between each group of three digits, as in 1,234,567, where addons format numbers with BreakUpLargeNumbers.";
CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT = "Sort Bags Right to Left";
CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT_TOOLTIP = "Sorting puts your items at the end of your last bag, and junk at the start of your backpack.";
CLASSICAPI_STRICT_INTERACT_RANGE = "Strict Interact Range";
CLASSICAPI_STRICT_INTERACT_RANGE_TOOLTIP = "Never picks an NPC or object past the interact range. When this is off, one that you can use now is picked also when it is farther away.";
CLASSICAPI_TARGET_NAMEPLATE_ALPHA = "Target Nameplate Opacity";
CLASSICAPI_TARGET_NAMEPLATE_ALPHA_TOOLTIP = "The opacity of your target's nameplate.";
CLASSICAPI_UI_SHAKE_STRENGTH = "UI Shake Strength";
CLASSICAPI_UI_SHAKE_STRENGTH_TOOLTIP = "Scales how far shaking effects move frames. 0 turns the shake off.";
CLASSICAPI_UNIT_DIRECTION_TOOLTIP = "Where a unit must be for action targeting to pick it. The direction is the camera's, not your character's.";
CLASSICAPI_WHEN_YOU_HAVE_A_TARGET = "When You Have a Target";
COMBAT_LABEL = "Combat";
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Show Numbers for Cooldowns";
CONFIRM_DELETE_EQUIPMENT_SET = "Are you sure you want to delete the equipment set %s?";
CONFIRM_SAVE_EQUIPMENT_SET = "Would you like to save the equipment set \"%s\"?";
COPPER_AMOUNT = "%d Copper";
@@ -41,8 +122,9 @@ INTERACT_ICONS_DEFAULT = "NPCs Only (Default)";
INTERACT_ICONS_OPTION = "Interact Key Icons";
INTERACT_ICONS_SHOW_ALL = "Show All";
INTERACT_ICONS_SHOW_NONE = "Show None";
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Action button keybinds will respond on key down, rather than on key up.";
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Blizzard Action button keybinds will respond on key down, rather than on key up.";
OPTION_TOOLTIP_ACTION_TARGETING = "A targeting system that dynamically targets enemies based on where you're looking. Works alongside the standard targeting system.";
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Check this to show a number countdown for action button cooldowns.";
OPTION_TOOLTIP_ENABLE_INTERACT = "Allows you to interact with NPCs and objects using a keypress";
OPTION_TOOLTIP_INTERACT_ICONS = "Controls which icons appear above NPCs and objects when the Interact Key feature is enabled";
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Stop auto attacking when you switch targets.";
@@ -101,6 +183,15 @@ ITEMS_VARIABLE_QUANTITY = "%d |4Item:Items;";
ITEM_MISSING = "%s missing";
ITEM_SLOTS_IGNORED = "%d |4slot:slots; ignored";
LARGE_NUMBER_SEPERATOR = ",";
DECIMAL_SEPERATOR = ".";
FIRST_NUMBER = "thousand";
FIRST_NUMBER_CAP = " K";
FIRST_NUMBER_CAP_NO_SPACE = "K";
SECOND_NUMBER = "million";
SECOND_NUMBER_CAP = " M";
SECOND_NUMBER_CAP_NO_SPACE = "M";
THIRD_NUMBER = "billion";
FOURTH_NUMBER = "trillion";
MINUTES_SECONDS = "%.2d:%.2d";
PAPERDOLL_EQUIPMENTMANAGER = "Equipment Manager";
PAPERDOLL_NEWEQUIPMENTSET = "New Set";
@@ -180,6 +271,8 @@ SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
SLASH_USE1 = "/use";
SLASH_USE2 = "/use";
STOP_AUTO_ATTACK = "Stop Auto Attack";
SYMBOLIC_NEGATIVE_NUMBER = "-%s";
SYMBOLIC_POSITIVE_NUMBER = "+%s";
TARGETFOCUS = "Target Focus";
LINK_TRADESKILL_TOOLTIP = "Click here to create a link to your profession.";
TEXT_TO_SPEECH = "Text To Speech";
@@ -9,6 +9,7 @@ BINDING_NAME_INTERACTMOUSEOVER = "Interactuar al pasar el ratón";
BINDING_NAME_INTERACTTARGET = "Interactuar con objetivo";
BINDING_NAME_TARGETFOCUS = "Seleccionar enfocado";
COMBAT_LABEL = "Combate";
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Mostrar tiempos de reutilización en cifras";
CONFIRM_DELETE_EQUIPMENT_SET = "¿Seguro que quieres eliminar el equipamiento %s?";
CONFIRM_SAVE_EQUIPMENT_SET = "¿Quieres guardar el conjunto de equipamiento \"%s\"?";
DEBUFF_SYMBOL_CURSE = "Mal";
@@ -40,6 +41,7 @@ INTERACT_ICONS_SHOW_ALL = "Mostrar todos";
INTERACT_ICONS_SHOW_NONE = "No mostrar ninguno";
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Los atajos de los botones de acción responderán al pulsar una tecla y no al soltarla.";
OPTION_TOOLTIP_ACTION_TARGETING = "Un sistema de selección de objetivo que selecciona de forma dinámica a los enemigos en función de la dirección en la que mires. Funciona junto al sistema de selección de objetivos estándar.";
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Activa esta opción para mostrar una cuenta atrás numérica para el tiempo de reutilización de los botones de acción.";
OPTION_TOOLTIP_ENABLE_INTERACT = "Te permite interactuar con PNJ y objetos pulsando una tecla.";
OPTION_TOOLTIP_INTERACT_ICONS = "Controla los iconos que aparecen sobre los PNJ y objetos cuando se habilita la opción de tecla Interactuar";
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Detiene el ataque automático al cambiar de objetivo.";
@@ -100,6 +102,15 @@ ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "Penetración de armadura";
ITEM_MOD_HEALTH_REGEN_SHORT = "Regeneración de salud";
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "daño por segundo";
LARGE_NUMBER_SEPERATOR = "";
DECIMAL_SEPERATOR = ",";
FIRST_NUMBER = "mil";
FIRST_NUMBER_CAP = "mil";
FIRST_NUMBER_CAP_NO_SPACE = "mil";
SECOND_NUMBER = "|7millón:millones;";
SECOND_NUMBER_CAP = "mill.";
SECOND_NUMBER_CAP_NO_SPACE = "mill.";
THIRD_NUMBER = "mil millones";
FOURTH_NUMBER = "|7billón:billones;";
MINUTES_SECONDS = "%.2d:%.2d";
PAPERDOLL_EQUIPMENTMANAGER = "Gestor de equipamiento";
PAPERDOLL_NEWEQUIPMENTSET = "Nuevo conjunto";
@@ -9,6 +9,7 @@ BINDING_NAME_INTERACTMOUSEOVER = "Interagir par survol de la souris";
BINDING_NAME_INTERACTTARGET = "Interagir avec la cible";
BINDING_NAME_TARGETFOCUS = "Reprendre cible";
COMBAT_LABEL = "Combat";
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Temps de recharge en chiffres";
CONFIRM_DELETE_EQUIPMENT_SET = "Voulez-vous vraiment supprimer l'ensemble d'équipement %s ?";
CONFIRM_SAVE_EQUIPMENT_SET = "Voulez-vous sauvegarder l'ensemble d'équipement %s ?";
DEBUFF_SYMBOL_CURSE = "Mn";
@@ -40,6 +41,7 @@ INTERACT_ICONS_SHOW_ALL = "Tout afficher";
INTERACT_ICONS_SHOW_NONE = "Ne rien afficher";
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Les boutons d’action associés à un raccourci clavier se déclencheront au moment où la touche est enfoncée, et non relâchée.";
OPTION_TOOLTIP_ACTION_TARGETING = "Un système de ciblage qui vise en temps réel les adversaires en fonction de l’endroit où vous regardez. Fonctionne en parallèle du système de ciblage standard.";
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Cochez pour afficher les temps de recharge sous forme de chiffres sur vos boutons d'action.";
OPTION_TOOLTIP_ENABLE_INTERACT = "Vous permet d’interagir avec les PNJ et les objets en appuyant sur une touche";
OPTION_TOOLTIP_INTERACT_ICONS = "Contrôle les icônes qui s’affichent au-dessus des PNJ et des objets quand la fonction Commande d’interaction est activée";
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Interrompt l'attaque automatique lorsque vous changez de cible.";
@@ -100,6 +102,15 @@ ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "Pénétration d'armure";
ITEM_MOD_HEALTH_REGEN_SHORT = "Régénération des points de vie";
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "Dégâts par seconde";
LARGE_NUMBER_SEPERATOR = "";
DECIMAL_SEPERATOR = ",";
FIRST_NUMBER = "|7millier:milliers;";
FIRST_NUMBER_CAP = " K";
FIRST_NUMBER_CAP_NO_SPACE = " K";
SECOND_NUMBER = "|7million:millions;";
SECOND_NUMBER_CAP = " M";
SECOND_NUMBER_CAP_NO_SPACE = " M";
THIRD_NUMBER = "|7milliard:milliards;";
FOURTH_NUMBER = "|7millier:milliers; de milliards";
MINUTES_SECONDS = "%.2d:%.2d";
PAPERDOLL_EQUIPMENTMANAGER = "Gestionnaire d'équipement";
PAPERDOLL_NEWEQUIPMENTSET = "Nouvel ensemble";
@@ -0,0 +1,12 @@
if GetLocale() ~= "koKR" then return end
LARGE_NUMBER_SEPERATOR = ",";
DECIMAL_SEPERATOR = ".";
FIRST_NUMBER = "천";
FIRST_NUMBER_CAP = "만";
FIRST_NUMBER_CAP_NO_SPACE = "만";
SECOND_NUMBER = "만";
SECOND_NUMBER_CAP = "억";
SECOND_NUMBER_CAP_NO_SPACE = "억";
THIRD_NUMBER = "억";
FOURTH_NUMBER = "조";
@@ -3,6 +3,7 @@
<Script file="deDE.lua"/>
<Script file="esES.lua"/>
<Script file="frFR.lua"/>
<Script file="koKR.lua"/>
<Script file="ruRU.lua"/>
<Script file="zhCN.lua"/>
<Script file="zhTW.lua"/>
@@ -9,6 +9,7 @@ BINDING_NAME_INTERACTMOUSEOVER = "Взаимодействовать указа
BINDING_NAME_INTERACTTARGET = "Взаимодействовать с целью";
BINDING_NAME_TARGETFOCUS = "Восстановить цель";
COMBAT_LABEL = "Бой";
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Отсчет времени восстановления";
CONFIRM_DELETE_EQUIPMENT_SET = "Вы действительно хотите удалить комплект экипировки \"%s\"?";
CONFIRM_SAVE_EQUIPMENT_SET = "Хотите сохранить комплект экипировки \"%s\"?";
DEBUFF_SYMBOL_CURSE = "Пр";
@@ -40,6 +41,7 @@ INTERACT_ICONS_SHOW_ALL = "Показать все";
INTERACT_ICONS_SHOW_NONE = "Не показывать";
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Способности будут задействоваться, когда пользователь нажимает, а не отпускает клавишу.";
OPTION_TOOLTIP_ACTION_TARGETING = "Система захвата целей, динамически выбирающая цели в поле зрения. Работает параллельно со стандартной системой наведения.";
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Отображение обратного отсчета для времени восстановления на панели команд.";
OPTION_TOOLTIP_ENABLE_INTERACT = "Позволяет взаимодействовать с NPC и объектами по нажатию клавиши.";
OPTION_TOOLTIP_INTERACT_ICONS = "Определяет, какие значки появляются над NPC и объектами, если включен параметр клавиши взаимодействия.";
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Прекращение автоматической атаки при смене цели.";
@@ -100,6 +102,15 @@ ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "к пробиванию брони";
ITEM_MOD_HEALTH_REGEN_SHORT = "к восполнению здоровья";
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "к урону в секунду";
LARGE_NUMBER_SEPERATOR = "";
DECIMAL_SEPERATOR = ",";
FIRST_NUMBER = "тыс.";
FIRST_NUMBER_CAP = "Т";
FIRST_NUMBER_CAP_NO_SPACE = "Т";
SECOND_NUMBER = "млн";
SECOND_NUMBER_CAP = "М";
SECOND_NUMBER_CAP_NO_SPACE = "М";
THIRD_NUMBER = "млрд";
FOURTH_NUMBER = "трлн";
MINUTES_SECONDS = "%.2d:%.2d";
PAPERDOLL_EQUIPMENTMANAGER = "Комплекты экипировки";
PAPERDOLL_NEWEQUIPMENTSET = "Новый комплект";
@@ -9,6 +9,7 @@ BINDING_NAME_INTERACTMOUSEOVER = "与鼠标悬停处互动";
BINDING_NAME_INTERACTTARGET = "与目标互动";
BINDING_NAME_TARGETFOCUS = "目标焦点";
COMBAT_LABEL = "战斗";
COUNTDOWN_FOR_COOLDOWNS_TEXT = "显示冷却时间";
CONFIRM_DELETE_EQUIPMENT_SET = "你确认要删除装备方案%s吗?";
CONFIRM_SAVE_EQUIPMENT_SET = "你想要保存装备方案\"%s\"吗?";
DEBUFF_SYMBOL_CURSE = "诅";
@@ -40,6 +41,7 @@ INTERACT_ICONS_SHOW_ALL = "显示全部";
INTERACT_ICONS_SHOW_NONE = "全不显示";
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "在按下快捷键时施法,而不是在松开快捷键时施法。";
OPTION_TOOLTIP_ACTION_TARGETING = "此瞄准系统可以以你观看的方向动态地瞄准敌人。可以和标准的瞄准系统一起使用。";
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "勾选此项后,动作条上的按钮将以倒计时数字的方式显示冷却时间。";
OPTION_TOOLTIP_ENABLE_INTERACT = "使你可以通过按键与NPC和物体进行交互";
OPTION_TOOLTIP_INTERACT_ICONS = "控制交互按键启用后NPC和物体上方出现的图标";
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "当你切换目标时停止自动攻击。";
@@ -100,6 +102,15 @@ ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "护甲穿透";
ITEM_MOD_HEALTH_REGEN_SHORT = "生命值恢复";
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "每秒伤害";
LARGE_NUMBER_SEPERATOR = ",";
DECIMAL_SEPERATOR = ".";
FIRST_NUMBER = "千";
FIRST_NUMBER_CAP = "万";
FIRST_NUMBER_CAP_NO_SPACE = "万";
SECOND_NUMBER = "万";
SECOND_NUMBER_CAP = "亿";
SECOND_NUMBER_CAP_NO_SPACE = "亿";
THIRD_NUMBER = "亿";
FOURTH_NUMBER = "兆";
MINUTES_SECONDS = "%.2d:%.2d";
PAPERDOLL_EQUIPMENTMANAGER = "装备管理";
PAPERDOLL_NEWEQUIPMENTSET = "新的方案";
@@ -9,6 +9,7 @@ BINDING_NAME_INTERACTMOUSEOVER = "與滑鼠指定的目標互動";
BINDING_NAME_INTERACTTARGET = "與目標互動";
BINDING_NAME_TARGETFOCUS = "選定鎖定目標";
COMBAT_LABEL = "戰鬥";
COUNTDOWN_FOR_COOLDOWNS_TEXT = "顯示冷卻時間";
CONFIRM_DELETE_EQUIPMENT_SET = "你確定要刪除裝備設定:%s嗎?";
CONFIRM_SAVE_EQUIPMENT_SET = "你想要儲存裝備設定「%s」嗎?";
DEBUFF_SYMBOL_CURSE = "咒";
@@ -40,6 +41,7 @@ INTERACT_ICONS_SHOW_ALL = "顯示全部";
INTERACT_ICONS_SHOW_NONE = "不顯示圖示";
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "快捷鍵按鍵設定會在按下按鍵時生效,而不是在鬆開按鍵的時候。";
OPTION_TOOLTIP_ACTION_TARGETING = "根據你的視線方向,動態鎖定敵人的鎖定系統。與標準鎖定系統相輔相成。";
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "勾選此選項可在快捷鍵上面直接顯示冷卻時間的倒數計時。";
OPTION_TOOLTIP_ENABLE_INTERACT = "可透過按鍵與NPC和物件互動";
OPTION_TOOLTIP_INTERACT_ICONS = "在啟用互動鍵功能後,控制出現在NPC和物件上方的圖示";
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "當你切換目標時停止自動攻擊。";
@@ -100,6 +102,15 @@ ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "護甲穿透";
ITEM_MOD_HEALTH_REGEN_SHORT = "生命力恢復";
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "每秒傷害";
LARGE_NUMBER_SEPERATOR = ",";
DECIMAL_SEPERATOR = ".";
FIRST_NUMBER = "千";
FIRST_NUMBER_CAP = "萬";
FIRST_NUMBER_CAP_NO_SPACE = "萬";
SECOND_NUMBER = "萬";
SECOND_NUMBER_CAP = "億";
SECOND_NUMBER_CAP_NO_SPACE = "億";
THIRD_NUMBER = "億";
FOURTH_NUMBER = "兆";
MINUTES_SECONDS = "%.2d:%.2d";
PAPERDOLL_EQUIPMENTMANAGER = "裝備管理員";
PAPERDOLL_NEWEQUIPMENTSET = "新設定";
@@ -1027,20 +1027,6 @@ end
FRAMESTACK_UPDATE_TIME = .1;
-- Blizzard_SharedXML's StringToBoolean, for the /framestack arguments.
local function _StringToBoolean(stringToCheck, defaultReturn)
stringToCheck = string.lower(stringToCheck);
local firstChar = string.sub(stringToCheck, 1, 1);
if (firstChar == "0" or firstChar == "n" or firstChar == "f" or
stringToCheck == "off" or stringToCheck == "disabled") then
return false;
elseif (string.find(firstChar, "^[1-9yt]") or
stringToCheck == "on" or stringToCheck == "enabled") then
return true;
end
return defaultReturn;
end
local function _WrapColor(text, colorHex)
return "|c" .. colorHex .. text .. "|r";
end
@@ -1266,8 +1252,8 @@ function FrameStackTooltip_HandleSlashCmd(msg)
showHiddenArg, msg = string.match(msg or "", "^%s*(%S+)(.*)$");
showRegionsArg, msg = string.match(msg or "", "^%s*(%S+)(.*)$");
local showHidden = _StringToBoolean(showHiddenArg or "", false);
local showRegions = _StringToBoolean(showRegionsArg or "", true);
local showHidden = StringToBoolean(showHiddenArg or "", false);
local showRegions = StringToBoolean(showRegionsArg or "", true);
FrameStackTooltip_Toggle(showHidden, showRegions);
end
+18 -152
View File
@@ -543,180 +543,46 @@ int __fastcall Script_UseEquipmentSet(void *L) {
return 1;
}
const Game::Doc::Field kSetIDArgs[] = {
Game::Doc::Req("setID", "number"),
};
const Game::Doc::Field kSlotArgs[] = {
Game::Doc::Req("slot", "luaIndex", "An equipment slot, from 1 to 19."),
};
const Game::Doc::Field kCanUseRets[] = {
Game::Doc::Req("canUseEquipmentSets", "bool", "False while a set swap is in progress."),
};
const Game::Doc::Function kCanUseEquipmentSets{
"Returns whether an equipment set can be used now.", {}, kCanUseRets};
const Game::Doc::Field kNumRets[] = {
Game::Doc::Req("numEquipmentSets", "number"),
};
const Game::Doc::Function kGetNumEquipmentSets{
"Returns the number of equipment sets the character has.", {}, kNumRets};
const Game::Doc::Field kIDsRets[] = {
Game::Doc::Req("setIDs", "table", "The set IDs in the order the sets were created."),
};
const Game::Doc::Function kGetEquipmentSetIDs{
"Returns the IDs of all the character's equipment sets.", {}, kIDsRets};
const Game::Doc::Field kIDArgs[] = {
Game::Doc::Req("name", "cstring", "The exact name; case matters."),
};
const Game::Doc::Field kIDRets[] = {
Game::Doc::Opt("setID", "number", nullptr, "Nil when no set has that name."),
};
const Game::Doc::Function kGetEquipmentSetID{
"Returns the ID of the equipment set with a name.", kIDArgs, kIDRets};
const Game::Doc::Field kInfoRets[] = {
Game::Doc::Opt("name", "string", nullptr,
"Nil, with the rest, when the set does not exist."),
Game::Doc::Opt("icon", "string", nullptr, "The icon's texture name."),
Game::Doc::Opt("setID", "number"),
Game::Doc::Opt("isEquipped", "bool", nullptr,
"True when every slot that is not ignored holds what the set saved."),
Game::Doc::Opt("numItems", "number", nullptr,
"numEquipped plus numInInventory plus numMissing."),
Game::Doc::Opt("numEquipped", "number"),
Game::Doc::Opt("numInInventory", "number", nullptr,
"In the bags, or in the bank while the bank is open."),
Game::Doc::Opt("numMissing", "number"),
Game::Doc::Opt("numIgnored", "number"),
};
const Game::Doc::Function kGetEquipmentSetInfo{
"Returns an equipment set's name, icon, and where its items are.", kSetIDArgs,
kInfoRets};
const Game::Doc::Field kIgnoredRets[] = {
Game::Doc::Opt("ignoredSlots", "table", nullptr,
"The numbers of the ignored slots; nil when the set does not exist."),
};
const Game::Doc::Function kGetIgnoredSlots{
"Returns the slots an equipment set leaves alone.", kSetIDArgs, kIgnoredRets};
const Game::Doc::Field kItemIDsRets[] = {
Game::Doc::Opt("itemIDs", "table", nullptr,
"Keyed by slot: the item ID, 0 for an empty slot, 1 for an ignored slot, "
"or -1 when not known."),
};
const Game::Doc::Function kGetItemIDs{
"Returns the item ID an equipment set saved for each slot.", kSetIDArgs, kItemIDsRets};
const Game::Doc::Field kLocationsRets[] = {
Game::Doc::Opt("locations", "table", nullptr,
"Keyed by slot: a packed location, 0 for an empty slot, 1 for an ignored "
"slot, or -1 for a missing item."),
};
const Game::Doc::Function kGetItemLocations{
"Returns where each item of an equipment set is now.", kSetIDArgs, kLocationsRets};
const Game::Doc::Field kCreateArgs[] = {
Game::Doc::Req("name", "cstring", "Shorter than 32 characters, with no line break."),
Game::Doc::Opt("icon", "string", "\"INV_Misc_QuestionMark\"", "A texture name."),
};
const Game::Doc::Field kCreateRets[] = {
Game::Doc::Opt("setID", "number", nullptr, "Nil when the name is already in use."),
};
const Game::Doc::Function kCreateEquipmentSet{
"Saves the equipped items as a new equipment set.", kCreateArgs, kCreateRets};
const Game::Doc::Field kSaveArgs[] = {
Game::Doc::Req("setID", "number"),
Game::Doc::Opt("icon", "string", nullptr, "A new texture name; nil keeps the icon."),
};
const Game::Doc::Function kSaveEquipmentSet{
"Saves the equipped items into an existing equipment set.", kSaveArgs, {}};
const Game::Doc::Field kModifyArgs[] = {
Game::Doc::Req("setID", "number"),
Game::Doc::Req("newName", "cstring"),
};
const Game::Doc::Function kModifyEquipmentSet{
"Renames an equipment set; does nothing when another set has the name.", kModifyArgs,
{}};
const Game::Doc::Function kDeleteEquipmentSet{
"Deletes an equipment set and removes it from the action bars.", kSetIDArgs, {}};
const Game::Doc::Function kIgnoreSlotForSave{
"Makes the next save of an equipment set leave a slot alone.", kSlotArgs, {}};
const Game::Doc::Function kUnignoreSlotForSave{
"Makes the next save of an equipment set include a slot again.", kSlotArgs, {}};
const Game::Doc::Field kIsIgnoredRets[] = {
Game::Doc::Req("isSlotIgnored", "bool"),
};
const Game::Doc::Function kIsSlotIgnoredForSave{
"Returns whether the next save of an equipment set leaves a slot alone.", kSlotArgs,
kIsIgnoredRets};
const Game::Doc::Function kClearIgnoredSlotsForSave{
"Makes the next save of an equipment set include every slot.", {}, {}};
const Game::Doc::Field kLockedRets[] = {
Game::Doc::Opt("hasLockedItems", "bool", nullptr, "Nil when the set does not exist."),
};
const Game::Doc::Function kEquipmentSetContainsLockedItems{
"Returns whether an item of an equipment set is locked by a move in progress.",
kSetIDArgs, kLockedRets};
const Game::Doc::Field kUseRets[] = {
Game::Doc::Req("setWasEquipped", "bool",
"True when the swap starts; EQUIPMENT_SWAP_FINISHED gives the result."),
};
const Game::Doc::Function kUseEquipmentSet{
"Equips the items of an equipment set.", kSetIDArgs, kUseRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_EquipmentSet", "CanUseEquipmentSets",
&Script_CanUseEquipmentSets, &kCanUseEquipmentSets);
&Script_CanUseEquipmentSets);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetNumEquipmentSets",
&Script_GetNumEquipmentSets, &kGetNumEquipmentSets);
&Script_GetNumEquipmentSets);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetEquipmentSetIDs",
&Script_GetEquipmentSetIDs, &kGetEquipmentSetIDs);
&Script_GetEquipmentSetIDs);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetEquipmentSetID",
&Script_GetEquipmentSetID, &kGetEquipmentSetID);
&Script_GetEquipmentSetID);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetEquipmentSetInfo",
&Script_GetEquipmentSetInfo, &kGetEquipmentSetInfo);
&Script_GetEquipmentSetInfo);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetIgnoredSlots",
&Script_GetIgnoredSlots, &kGetIgnoredSlots);
&Script_GetIgnoredSlots);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetItemIDs",
&Script_GetItemIDs, &kGetItemIDs);
&Script_GetItemIDs);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetItemLocations",
&Script_GetItemLocations, &kGetItemLocations);
&Script_GetItemLocations);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "CreateEquipmentSet",
&Script_CreateEquipmentSet, &kCreateEquipmentSet);
&Script_CreateEquipmentSet);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "SaveEquipmentSet",
&Script_SaveEquipmentSet, &kSaveEquipmentSet);
&Script_SaveEquipmentSet);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "ModifyEquipmentSet",
&Script_ModifyEquipmentSet, &kModifyEquipmentSet);
&Script_ModifyEquipmentSet);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "DeleteEquipmentSet",
&Script_DeleteEquipmentSet, &kDeleteEquipmentSet);
&Script_DeleteEquipmentSet);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "IgnoreSlotForSave",
&Script_IgnoreSlotForSave, &kIgnoreSlotForSave);
&Script_IgnoreSlotForSave);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "UnignoreSlotForSave",
&Script_UnignoreSlotForSave, &kUnignoreSlotForSave);
&Script_UnignoreSlotForSave);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "IsSlotIgnoredForSave",
&Script_IsSlotIgnoredForSave, &kIsSlotIgnoredForSave);
&Script_IsSlotIgnoredForSave);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "ClearIgnoredSlotsForSave",
&Script_ClearIgnoredSlotsForSave,
&kClearIgnoredSlotsForSave);
&Script_ClearIgnoredSlotsForSave);
Game::Lua::RegisterTableFunction("C_EquipmentSet",
"EquipmentSetContainsLockedItems",
&Script_EquipmentSetContainsLockedItems,
&kEquipmentSetContainsLockedItems);
&Script_EquipmentSetContainsLockedItems);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "UseEquipmentSet",
&Script_UseEquipmentSet, &kUseEquipmentSet);
&Script_UseEquipmentSet);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+3 -25
View File
@@ -151,30 +151,8 @@ constexpr int LOC_IGNORED = 1;
// when the swap settles — see
// `EquipmentSet::Pending`. `success` is
// 1 or nil, never 0 (a Lua 0 is true).
inline const Game::Doc::Event kSetsChangedDoc{
"EquipmentSet",
"Fires when an equipment set is created, saved, renamed, or deleted, or when the slots "
"ignored for the next save change.",
{}};
inline const Game::Doc::Field kSwapPendingPayload[] = {
Game::Doc::Req("setID", "number"),
};
inline const Game::Doc::Event kSwapPendingDoc{
"EquipmentSet", "Fires when C_EquipmentSet.UseEquipmentSet has sent all its moves.",
kSwapPendingPayload};
inline const Game::Doc::Field kSwapFinishedPayload[] = {
Game::Doc::Opt("result", "number", nullptr, "1 when the swap succeeded, else nil."),
Game::Doc::Req("setID", "number"),
};
inline const Game::Doc::Event kSwapFinishedDoc{
"EquipmentSet", "Fires when the moves of an equipment set swap are done.",
kSwapFinishedPayload};
inline const Event::Custom::AutoReserve kEvtSetsChanged{"EQUIPMENT_SETS_CHANGED",
&kSetsChangedDoc};
inline const Event::Custom::AutoReserve kEvtSwapPending{"EQUIPMENT_SWAP_PENDING",
&kSwapPendingDoc};
inline const Event::Custom::AutoReserve kEvtSwapFinished{"EQUIPMENT_SWAP_FINISHED",
&kSwapFinishedDoc};
inline const Event::Custom::AutoReserve kEvtSetsChanged{"EQUIPMENT_SETS_CHANGED"};
inline const Event::Custom::AutoReserve kEvtSwapPending{"EQUIPMENT_SWAP_PENDING"};
inline const Event::Custom::AutoReserve kEvtSwapFinished{"EQUIPMENT_SWAP_FINISHED"};
} // namespace EquipmentSet
+1 -12
View File
@@ -218,16 +218,6 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
{"SetEquipmentSet", &Script_GameTooltipSetEquipmentSet},
};
const Game::Doc::Field kSetEquipmentSetArgs[] = {
Game::Doc::Req("name", "cstring", "The name of the equipment set."),
};
const Game::Doc::Function kSetEquipmentSet{
"Shows an equipment set in the tooltip: its name and where its items are.",
kSetEquipmentSetArgs, {}};
const Game::Doc::Method g_methodDocs[] = {
{"SetEquipmentSet", &kSetEquipmentSet},
};
} // namespace
static void RegisterLuaFunctions() {
@@ -236,8 +226,7 @@ static void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY),
g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+64 -12
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>
@@ -51,6 +52,7 @@ struct Reservation {
const char *name;
int slot; // -1 until claimed
const Game::Doc::Event *doc; // API documentation; nullptr = undocumented
int fixedId; // an `EngineName`'s engine id (its slot for good); -1 = claimed
};
Reservation g_reserved[MAX_RESERVED];
int g_reservedCount = 0;
@@ -120,6 +122,15 @@ bool Claimable(const uint8_t *base, int id) {
return true;
}
// Reservations that need a claimed slot (an `EngineName` has its own).
int ClaimDemand() {
int demand = 0;
for (int i = 0; i < g_reservedCount; ++i)
if (g_reserved[i].fixedId < 0)
++demand;
return demand;
}
// Walk the engine's event table from the LOW end looking for a claimable
// slot. The bulk-init at `0x0051AB30` names 419 of the 549 slots (verified
// against both benilla-re's tables/events.tsv and this exe's initializer);
@@ -196,7 +207,8 @@ void Grow() {
const int count = Game::Read<int>(Offsets::VAR_EVENT_TABLE_COUNT);
auto *base = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
if (base == nullptr || count <= 0 || g_reservedCount <= 0)
const int demand = ClaimDemand();
if (base == nullptr || count <= 0 || demand <= 0)
return;
int nulls = 0;
@@ -206,7 +218,7 @@ void Grow() {
const bool force = g_forceGrowNext;
g_forceGrowNext = false;
if (nulls >= g_reservedCount && !force)
if (nulls >= demand && !force)
return; // plenty of room — the common (only) real case
// Growing moves the buffer, invalidating every entry's self-referential
@@ -226,7 +238,7 @@ void Grow() {
}
constexpr int kMargin = 16;
const int deficit = (nulls < g_reservedCount) ? (g_reservedCount - nulls) : 0;
const int deficit = (nulls < demand) ? (demand - nulls) : 0;
const int newCount = count + deficit + kMargin;
auto *newBuf = static_cast<uint8_t *>(
SMemAlloc(static_cast<unsigned>(newCount) * Offsets::EVENT_ENTRY_STRIDE));
@@ -260,10 +272,29 @@ void Grow() {
SMemFree(base);
Debug::Log::Printf("[event] grew table %d -> %d (%d NULL, %d reserved%s)",
count, newCount, nulls, g_reservedCount,
count, newCount, nulls, demand,
force ? ", FORCED" : "");
}
// True when unit field `field` has a slot in the unit's watch copy, giving it
// the watched-field table's spare entry when it has none.
bool GiveWatchSlot(uint32_t field) {
using WatchedIndex_t = uint32_t(__fastcall *)(uint32_t field, uint32_t dwords,
uint32_t isActivePlayer);
const auto index = reinterpret_cast<WatchedIndex_t>(Offsets::FUN_UNIT_WATCHED_INDEX);
if (index(field, 1, 0) != Offsets::UNIT_WATCHED_NONE)
return true;
auto *table =
reinterpret_cast<uint32_t *>(static_cast<uintptr_t>(Offsets::VAR_UNIT_WATCHED_FIELDS));
for (int i = 0; i < Offsets::UNIT_WATCHED_FIELD_COUNT; ++i) {
if (table[i] == Offsets::UNIT_WATCHED_NONE) {
table[i] = field;
return true;
}
}
return false;
}
} // namespace
AutoReserve::AutoReserve(const char *name, const Game::Doc::Event *doc) {
@@ -286,10 +317,30 @@ AutoReserve::AutoReserve(const char *name, const Game::Doc::Event *doc) {
g_reserved[g_reservedCount].name = name;
g_reserved[g_reservedCount].slot = -1;
g_reserved[g_reservedCount].doc = doc;
g_reserved[g_reservedCount].fixedId = -1;
index_ = g_reservedCount;
++g_reservedCount;
}
EngineName::EngineName(int id, const char *name, const Game::Doc::Event *doc) {
if (name == nullptr || id < 0 || id >= Offsets::EVENT_NAME_TABLE_STATIC_COUNT ||
g_reservedCount >= MAX_RESERVED)
return;
// A named unit-field slot makes FUN_0051bbb0 watch field `id`, and a
// watcher on a field the unit's watch copy has no slot for writes past
// the copy, into the object (see FUN_UNIT_WATCHED_INDEX). Such a field
// takes the table's spare entry, or the name is refused.
if (id < Offsets::EVENT_NAME_UNIT_MAX && !GiveWatchSlot(static_cast<uint32_t>(id)))
return;
auto **slot = reinterpret_cast<const char **>(
static_cast<uintptr_t>(Offsets::VAR_EVENT_NAME_TABLE_STATIC)) + id;
if (*slot != nullptr)
return; // the engine names it; never take an engine event's slot
*slot = name;
g_reserved[g_reservedCount] = {name, id, doc, id};
++g_reservedCount;
}
int AutoReserve::Slot() const {
return index_ >= 0 ? g_reserved[index_].slot : -1;
}
@@ -357,16 +408,17 @@ void FireValues(int slot, void *L, int firstArgIdx, int nargs) {
PushString(L, "event");
PushString(L, name);
SetTable(L, GLOBALS_INDEX);
uintptr_t node = Game::Read<uintptr_t>(entry, Offsets::OFF_EVENT_ENTRY_HEAD);
while (node != 0 && (node & 1) == 0) {
const uintptr_t next = Game::Read<uintptr_t>(node + Offsets::OFF_EVENT_NODE_NEXT);
auto *frame = Game::Read<uint8_t *>(node + Offsets::OFF_EVENT_NODE_FRAME);
if (frame != nullptr)
// A string arg1 meets RegisterUnitEvent filters, which the engine's
// dispatches meet at the runner this walk never calls.
const char *unit = nargs > 0 && Type(L, firstArgIdx) == TYPE_STRING
? ToString(L, firstArgIdx)
: nullptr;
ForEachListener(slot, [&](uint8_t *frame) {
if (unit == nullptr || Frame::UnitEvent::Accepts(frame, name, unit))
Frame::ScriptArgs::RunSlotWithValues(
L, reinterpret_cast<const uint32_t *>(frame + Offsets::OFF_FRAME_ONEVENT_SLOT),
frame, firstArgIdx, nargs);
node = next;
}
});
PushString(L, "event");
PushValue(L, top + 1);
SetTable(L, GLOBALS_INDEX);
@@ -389,7 +441,7 @@ void EnableWrites() { g_writesEnabled = true; }
void PrepareForReload() {
for (int i = 0; i < g_reservedCount; ++i)
g_reserved[i].slot = -1;
g_reserved[i].slot = g_reserved[i].fixedId;
g_writesEnabled = false;
g_warnedTableFull = false; // fresh table after the rebuild — re-arm
g_growLatched = false; // re-arm the one-shot grow for the new table
+53 -3
View File
@@ -64,6 +64,36 @@ private:
int index_ = -1; // into the reservation table; -1 = dropped (overflow)
};
// Names engine event `id`, an id the engine already signals by number but
// 1.12 left unnamed. Place a static instance at file scope:
//
// static const Event::Custom::EngineName _n{id, "MY_EVENT", &kDoc};
//
// The unit-field events are the case this serves. `FUN_0051bbb0` watches unit
// field `i` only when slot `i` of the static name array
// (`VAR_EVENT_NAME_TABLE_STATIC`) is named, and its watcher fires event `i`
// once per token of the unit (the field index is the event id). So naming the
// slot is all the engine needs. Its table build
// (`SetScriptEventTable(0x00BE1198, 0x225)`, every world init) copies the
// name, and its own watchers fire the event. Nothing here fires anything.
//
// The name is written at construction, which is static init, before any world
// init or unit watch. Watch and unwatch then always see the same set. Nothing
// in the binary writes the array except the boot initializer `0x0051AB30`,
// and it stores only the slots it names. A slot the engine already names is
// left alone. A unit field the unit's watch copy has no slot for
// (FUN_UNIT_WATCHED_INDEX) would have its watchers write over the object, so
// it takes the watched-field table's one spare entry first, and the name is
// refused when that is gone. The caller proves that nothing else signals
// `id` by number while the game table is installed (the census in
// `GLUE_EVENT_COUNT`'s comment).
//
// The event is documented like a reservation: `ReservedName` / `ReservedDoc`
// list it, and its slot is `id` for good (never re-claimed on `/reload`).
struct EngineName {
EngineName(int id, const char *name, const Game::Doc::Event *doc);
};
// Read access to the reservation table for the API documentation: the
// number of reserved names, the literal name of the i-th (0-based), and its
// descriptor (nullptr when undocumented). Indices are stable for the
@@ -92,6 +122,25 @@ int LookupByName(const char *name);
// past the live table.
bool HasListeners(int slot);
// Calls `fn(frame)` for each frame registered for the event in `slot`, in
// the dispatcher's order. Each node's successor is read before `fn` runs,
// as the dispatcher reads it, so `fn` may run the frame's script. Nothing
// for a slot `HasListeners` is false for.
template <typename Fn> void ForEachListener(int slot, Fn fn) {
if (!HasListeners(slot))
return;
const uint8_t *entry = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR) +
slot * Offsets::EVENT_ENTRY_STRIDE;
uintptr_t node = Game::Read<uintptr_t>(entry, Offsets::OFF_EVENT_ENTRY_HEAD);
while (node != 0 && (node & 1) == 0) {
const uintptr_t next = Game::Read<uintptr_t>(node + Offsets::OFF_EVENT_NODE_NEXT);
auto *frame = Game::Read<uint8_t *>(node + Offsets::OFF_EVENT_NODE_FRAME);
if (frame != nullptr)
fn(frame);
node = next;
}
}
// Fires the event in `slot` with `nargs` Lua values at absolute stack index
// `firstArgIdx` as its payload — booleans and nils included, which the
// printf-style `Fire` cannot carry. Mirrors the engine dispatcher
@@ -99,9 +148,10 @@ bool HasListeners(int slot);
// restored after, and each listener's OnEvent runs through
// `Frame::ScriptArgs::RunSlotWithValues` (so `arg1..argN`, `this` and the
// modern positional arguments are set the way the engine's runner sets
// them). The values stay on the stack; the caller pops them. No-op for
// `slot < 0` or an event nobody listens to. 4.3.4's equivalent is
// `FrameScript_SignalEvent(id, L, count)`.
// them). A string first value meets RegisterUnitEvent filters, as a `%s`
// arg1 does on the engine's path. The values stay on the stack; the caller
// pops them. No-op for `slot < 0` or an event nobody listens to. 4.3.4's
// equivalent is `FrameScript_SignalEvent(id, L, count)`.
void FireValues(int slot, void *L, int firstArgIdx, int nargs);
// Dispatches a custom event via the engine's printf-style event
+19 -207
View File
@@ -669,242 +669,54 @@ static int __fastcall Script_C_Reputation_SetWatchedFactionByID(void *L) {
return 0;
}
const Game::Doc::Field kFactionDataFields[] = {
Game::Doc::Req("factionID", "number"),
Game::Doc::Req("name", "string"),
Game::Doc::Req("description", "string", "Empty when the faction has none."),
Game::Doc::Req("reaction", "luaIndex", "The rank, from 1 for Hated to 8 for Exalted."),
Game::Doc::Req("currentReactionThreshold", "number",
"The standing at the bottom of the current rank."),
Game::Doc::Req("nextReactionThreshold", "number",
"The standing at the top of the current rank."),
Game::Doc::Req("currentStanding", "number"),
Game::Doc::Req("atWarWith", "bool"),
Game::Doc::Req("canToggleAtWar", "bool",
"False below -3000 standing, when the war state is fixed, or with no "
"reputation slot."),
Game::Doc::Req("isChild", "bool", "Always false."),
Game::Doc::Req("isHeader", "bool", "True when the faction is a header in the reputation list."),
Game::Doc::Req("isHeaderWithRep", "bool", "Always false."),
Game::Doc::Req("isCollapsed", "bool", "True for a collapsed header."),
Game::Doc::Req("isWatched", "bool", "True when the faction is shown above the experience bar."),
Game::Doc::Req("hasBonusRepGain", "bool", "Always false."),
Game::Doc::Req("canSetInactive", "bool",
"True when the faction has a reputation slot and is not a header."),
Game::Doc::Req("isAccountWide", "bool", "Always false."),
};
const Game::Doc::Structure kFactionData{"FactionData", "Reputation", kFactionDataFields,
"A faction's reputation data."};
const Game::Doc::Field kFactionIDArgs[] = {
Game::Doc::Req("factionID", "number"),
};
const Game::Doc::Field kSortIndexArgs[] = {
Game::Doc::Req("factionSortIndex", "luaIndex", "A position in the reputation list."),
};
const Game::Doc::Field kFactionIDByIndexArgs[] = {
Game::Doc::Req("factionIndex", "luaIndex", "A position in the reputation list."),
};
const Game::Doc::Field kFactionIDByIndexRets[] = {
Game::Doc::Opt("factionID", "number", nullptr,
"0 for a header with no faction of its own, such as Other or Inactive; nil "
"when the index is out of range."),
};
const Game::Doc::Function kGetFactionIDByIndex{
"Returns the faction ID of the entry at a position in the reputation list.",
kFactionIDByIndexArgs, kFactionIDByIndexRets, "ReputationGlobals", true};
const Game::Doc::Field kFactionInfoByIDRets[] = {
Game::Doc::Opt("name", "string", nullptr, "Nil, with the rest, when no faction has the ID."),
Game::Doc::Opt("description", "string", nullptr, "Empty when the faction has none."),
Game::Doc::Opt("standingID", "number", nullptr, "The rank, from 1 for Hated to 8 for Exalted."),
Game::Doc::Opt("barMin", "number", nullptr, "The standing at the bottom of the current rank."),
Game::Doc::Opt("barMax", "number", nullptr, "The standing at the top of the current rank."),
Game::Doc::Opt("barValue", "number", nullptr, "The current standing."),
Game::Doc::Opt("atWarWith", "number", nullptr,
"1 when the player is at war with the faction, else nil."),
Game::Doc::Opt("canToggleAtWar", "number", nullptr,
"1 when the at-war state can be changed, else nil."),
Game::Doc::Opt("isHeader", "number", nullptr,
"1 when the faction is a header in the reputation list, else nil."),
Game::Doc::Opt("isCollapsed", "number", nullptr, "1 for a collapsed header, else nil."),
Game::Doc::Opt("isWatched", "number", nullptr,
"1 when the faction is shown above the experience bar, else nil."),
};
const Game::Doc::Function kGetFactionInfoByID{
"Returns the same values as GetFactionInfo for a faction ID, whether or not the faction "
"is in the reputation list.",
kFactionIDArgs, kFactionInfoByIDRets, "ReputationGlobals"};
const Game::Doc::Field kFactionParentIDRets[] = {
Game::Doc::Opt("parentFactionID", "number", nullptr,
"0 for a faction with no parent; nil when no faction has the ID."),
};
const Game::Doc::Function kGetFactionParentID{
"Returns the ID of a faction's parent faction, such as Alliance for Stormwind.",
kFactionIDArgs, kFactionParentIDRets, "ReputationGlobals", true};
const Game::Doc::Field kSetWatchedArgs[] = {
Game::Doc::Req("factionID", "number",
"0, or an ID with no reputation slot, clears the watched faction; a negative "
"ID does nothing."),
};
const Game::Doc::Function kSetWatchedFactionByID{
"Sets the faction shown above the experience bar.", kSetWatchedArgs, {}};
const Game::Doc::Field kSetSelectedByIDArgs[] = {
Game::Doc::Req("factionID", "number", "0 clears the selection; a negative ID does nothing."),
};
const Game::Doc::Function kSetSelectedFactionByID{
"Selects a faction in the reputation pane by ID, even when the faction is not listed.",
kSetSelectedByIDArgs, {}, nullptr, true};
const Game::Doc::Function kToggleFactionAtWarByID{
"Toggles whether the player is at war with a faction, by ID; the same limits apply as "
"for C_Reputation.ToggleFactionAtWar.",
kFactionIDArgs, {}, nullptr, true};
const Game::Doc::Field kIsActiveRets[] = {
Game::Doc::Req("isActive", "bool",
"False under the Inactive header, and for anything with no reputation slot."),
};
const Game::Doc::Function kIsFactionActive{
"Returns whether the faction at a position in the reputation list is active, that is, "
"not under the Inactive header.",
kSortIndexArgs, kIsActiveRets};
const Game::Doc::Function kIsFactionActiveByID{
"Returns whether a faction is active, that is, not under the Inactive header.",
kFactionIDArgs, kIsActiveRets, nullptr, true};
const Game::Doc::Field kIsInactiveRets[] = {
Game::Doc::Opt("isInactive", "number", nullptr,
"1 when the faction is under the Inactive header, else nil."),
};
const Game::Doc::Function kIsFactionInactive{
"Returns whether the faction at a position in the reputation list is under the Inactive "
"header, as 1 or nil.",
kSortIndexArgs, kIsInactiveRets, nullptr, true};
const Game::Doc::Function kSetFactionInactiveByID{
"Moves a faction under the Inactive header of the reputation list, by ID.",
kFactionIDArgs, {}, nullptr, true};
const Game::Doc::Function kSetFactionActiveByID{
"Moves a faction out from under the Inactive header of the reputation list, by ID.",
kFactionIDArgs, {}, nullptr, true};
const Game::Doc::Function kSetFactionInactive{
"Moves the faction at a position in the reputation list under the Inactive header.",
kSortIndexArgs, {}, nullptr, true};
const Game::Doc::Function kSetFactionActive{
"Moves the faction at a position in the reputation list out from under the Inactive "
"header.",
kSortIndexArgs, {}};
const Game::Doc::Function kToggleFactionAtWar{
"Toggles whether the player is at war with the faction at a position in the reputation "
"list.",
kSortIndexArgs, {}};
const Game::Doc::Function kSetSelectedFaction{
"Selects the faction at a position in the reputation list.", kSortIndexArgs, {}};
const Game::Doc::Field kWatchedFactionDataRets[] = {
Game::Doc::Opt("watchedFactionData", "FactionData", nullptr,
"Nil when no faction is watched."),
};
const Game::Doc::Function kGetWatchedFactionData{
"Returns the faction shown above the experience bar.", {}, kWatchedFactionDataRets};
const Game::Doc::Field kStandingsRets[] = {
Game::Doc::Req("standings", "table",
"Keyed by faction ID; each value is the faction's standing."),
};
const Game::Doc::Function kGetFactionStandings{
"Returns the standing of every faction the character has reputation with, keyed by "
"faction ID.",
{}, kStandingsRets, nullptr, true};
const Game::Doc::Field kFactionDataByIDRets[] = {
Game::Doc::Opt("factionData", "FactionData", nullptr, "Nil when no faction has the ID."),
};
const Game::Doc::Function kGetFactionDataByID{
"Returns a faction's reputation data by ID, whether or not the faction is in the "
"reputation list.",
kFactionIDArgs, kFactionDataByIDRets};
const Game::Doc::Field kFactionDataByIndexRets[] = {
Game::Doc::Opt("factionData", "FactionData", nullptr,
"Nil when the index is out of range or names the Other or Inactive header."),
};
const Game::Doc::Function kGetFactionDataByIndex{
"Returns the reputation data of the faction at a position in the reputation list.",
kSortIndexArgs, kFactionDataByIndexRets};
static void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("GetFactionIDByIndex", &Script_GetFactionIDByIndex,
&kGetFactionIDByIndex);
Game::Lua::RegisterGlobalFunction("GetFactionInfoByID", &Script_GetFactionInfoByID,
&kGetFactionInfoByID);
Game::Lua::RegisterGlobalFunction("GetFactionParentID", &Script_GetFactionParentID,
&kGetFactionParentID);
Game::Lua::RegisterGlobalFunction("GetFactionIDByIndex", &Script_GetFactionIDByIndex);
Game::Lua::RegisterGlobalFunction("GetFactionInfoByID", &Script_GetFactionInfoByID);
Game::Lua::RegisterGlobalFunction("GetFactionParentID", &Script_GetFactionParentID);
Game::Lua::RegisterTableFunction("C_Reputation", "SetWatchedFactionByID",
&Script_C_Reputation_SetWatchedFactionByID,
&kSetWatchedFactionByID);
&Script_C_Reputation_SetWatchedFactionByID);
Game::Lua::RegisterTableFunction("C_Reputation", "SetSelectedFactionByID",
&Script_C_Reputation_SetSelectedFactionByID,
&kSetSelectedFactionByID);
&Script_C_Reputation_SetSelectedFactionByID);
Game::Lua::RegisterTableFunction("C_Reputation", "ToggleFactionAtWarByID",
&Script_C_Reputation_ToggleFactionAtWarByID,
&kToggleFactionAtWarByID);
&Script_C_Reputation_ToggleFactionAtWarByID);
Game::Lua::RegisterTableFunction("C_Reputation", "IsFactionActive",
&Script_C_Reputation_IsFactionActive, &kIsFactionActive);
&Script_C_Reputation_IsFactionActive);
Game::Lua::RegisterTableFunction("C_Reputation", "IsFactionActiveByID",
&Script_C_Reputation_IsFactionActiveByID,
&kIsFactionActiveByID);
&Script_C_Reputation_IsFactionActiveByID);
Game::Lua::RegisterTableFunction(
"C_Reputation", "IsFactionInactive",
reinterpret_cast<Game::Lua::CFunction>(
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_IS_FACTION_INACTIVE)),
&kIsFactionInactive);
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_IS_FACTION_INACTIVE)));
Game::Lua::RegisterTableFunction("C_Reputation", "SetFactionInactiveByID",
&Script_C_Reputation_SetFactionInactiveByID,
&kSetFactionInactiveByID);
&Script_C_Reputation_SetFactionInactiveByID);
Game::Lua::RegisterTableFunction("C_Reputation", "SetFactionActiveByID",
&Script_C_Reputation_SetFactionActiveByID,
&kSetFactionActiveByID);
&Script_C_Reputation_SetFactionActiveByID);
Game::Lua::RegisterTableFunction(
"C_Reputation", "SetFactionInactive",
reinterpret_cast<Game::Lua::CFunction>(
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_INACTIVE)),
&kSetFactionInactive);
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_INACTIVE)));
Game::Lua::RegisterTableFunction(
"C_Reputation", "SetFactionActive",
reinterpret_cast<Game::Lua::CFunction>(
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_ACTIVE)),
&kSetFactionActive);
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_ACTIVE)));
Game::Lua::RegisterTableFunction(
"C_Reputation", "ToggleFactionAtWar",
reinterpret_cast<Game::Lua::CFunction>(
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_FACTION_TOGGLE_AT_WAR)),
&kToggleFactionAtWar);
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_FACTION_TOGGLE_AT_WAR)));
// The stock global, mirrored into the namespace — the engine's own
// handler already has the Lua C ABI, so it registers as-is.
Game::Lua::RegisterTableFunction(
"C_Reputation", "SetSelectedFaction",
reinterpret_cast<Game::Lua::CFunction>(
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_SELECTED_FACTION)),
&kSetSelectedFaction);
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_SELECTED_FACTION)));
Game::Lua::RegisterTableFunction("C_Reputation", "GetWatchedFactionData",
&Script_C_Reputation_GetWatchedFactionData,
&kGetWatchedFactionData);
&Script_C_Reputation_GetWatchedFactionData);
Game::Lua::RegisterTableFunction("C_Reputation", "GetFactionStandings",
&Script_C_Reputation_GetFactionStandings,
&kGetFactionStandings);
&Script_C_Reputation_GetFactionStandings);
Game::Lua::RegisterTableFunction("C_Reputation", "GetFactionDataByID",
&Script_C_Reputation_GetFactionDataByID,
&kGetFactionDataByID);
&Script_C_Reputation_GetFactionDataByID);
Game::Lua::RegisterTableFunction("C_Reputation", "GetFactionDataByIndex",
&Script_C_Reputation_GetFactionDataByIndex,
&kGetFactionDataByIndex);
&Script_C_Reputation_GetFactionDataByIndex);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+2 -23
View File
@@ -50,15 +50,7 @@ using GetStanding_t = int(__fastcall *)(int factionID);
constexpr const char *kEventName = "FACTION_STANDING_CHANGED";
const Game::Doc::Field kPayload[] = {
Game::Doc::Req("factionID", "number"),
Game::Doc::Req("newStanding", "number", "The faction's standing after the change."),
Game::Doc::Req("repGained", "number", "Negative for a loss."),
};
const Game::Doc::Event kEventDoc{"Reputation", "Fires when the player's reputation changes.",
kPayload};
const Event::Custom::AutoReserve _reserve{kEventName, &kEventDoc};
const Event::Custom::AutoReserve _reserve{kEventName};
// In-flight snapshot of the current rep change. Set on hook entry,
// cleared on hook exit. WoW's main thread is the only one that runs
@@ -75,22 +67,9 @@ int __fastcall Script_C_Reputation_GetLastStandingChange(void *L) {
return 3;
}
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("factionID", "number", nullptr,
"Nil, with the rest, outside a FACTION_STANDING_CHANGED or "
"CHAT_MSG_COMBAT_FACTION_CHANGE handler."),
Game::Doc::Opt("newStanding", "number", nullptr, "The faction's standing after the change."),
Game::Doc::Opt("repGained", "number", nullptr, "Negative for a loss."),
};
const Game::Doc::Function kGetLastStandingChange{
"Returns the reputation change that FACTION_STANDING_CHANGED and "
"CHAT_MSG_COMBAT_FACTION_CHANGE report, while their handlers run.",
{}, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Reputation", "GetLastStandingChange",
&Script_C_Reputation_GetLastStandingChange,
&kGetLastStandingChange);
&Script_C_Reputation_GetLastStandingChange);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+170 -236
View File
@@ -24,26 +24,19 @@
// the `this`/`arg1..argN` globals and then `lua_pcall(handler, /*nargs*/0)`.
// So `function(self, delta)` gets nothing today.
//
// We co-hook both runners and REPLACE their pcall tail: keep setting the same
// globals (so handlers reading `this`/`arg1` keep working), but push `self` +
// `arg1..argN` as real arguments and `pcall` with `nargs = 1 + N`. The
// reimplementation mirrors the engine's message-handler errfunc, but saves the
// clobbered globals on the Lua stack (not registry refs like the engine) so an
// out-of-memory longjmp mid-push unwinds them for free — no leaked refs, no
// per-call ref churn. See RunModern.
// We redirect each runner's own lua_pcall (RunnerPCall). By then the runner
// has done everything else — parsed its format, saved and set `this` and
// arg1..argN — so the values are read back from those globals and passed as
// `(self, arg1..argN)` with `nargs = 1 + N`. The format is never parsed here.
// The runner's switch is the only one, so a spec another DLL adds to it at
// runtime keeps working: SuperWoWhook's `%e` (a registry ref) crashed the copy
// of the switch this module used to carry.
//
// The reimplemented path is taken ONLY for a handler that can observe
// positional args: a Lua function that declares a parameter or is vararg
// (checked via its Proto), or a C function such as a frame method bound
// directly — see FunctionWantsArgs. A Lua handler that declares NO parameters
// can't observe positional args, so pushing
// them is a no-op — those route through the original engine runner unchanged:
// provably identical to vanilla, and OFF the engine's hottest Lua path (the vast
// majority of vanilla handlers, which read `this`/`arg1`, are param-less). So a
// param-less vanilla handler is untouched; only a param-declaring handler sees
// the `(self, [event,] arg1..argN)` shape. The one exception is a fire with
// more values than the engine runner's 19: 3.3.5 sets every argN, so that
// fire takes the reimplemented path whatever the handler.
// Positional args go only to a handler that can observe them: a Lua function
// that declares a parameter or is vararg (checked via its Proto), or a C
// function such as a frame method bound directly — see FunctionWantsArgs. Any
// other handler is called exactly as the engine calls it, so a param-less
// vanilla handler (the vast majority, reading `this`/`arg1`) is untouched.
//
// Each OnEvent handler also gets its own `event` global, saved before and
// restored after it (SaveEvent), as 3.3.5 does per handler.
@@ -66,13 +59,16 @@
// Runtime switch, default ON — modern handler signatures are a core 5.1
// feature, so ports that write `function(self, delta)` work out of the box.
// `SetModernScriptArgs(false)` disables it, `GetModernScriptArgs()` reads the
// state. While disabled both detours are a straight passthrough to the original
// runner (exact vanilla behavior, zero reimplementation risk) — the OFF escape
// valve, since this lands on the hottest Lua path in the engine
// (FUN_FRAME_RUN_SCRIPT_ARGS fires for every OnUpdate frame).
// state. While disabled the redirected pcall is the engine's call unchanged —
// exact vanilla behavior.
//
// Scripts the engine never runs (RunWithValues: custom events,
// OnAttributeChanged, …) carry arbitrary Lua values, which no runner format
// can, so RunCore runs those itself, as 3.3.5's FrameScript_Execute does.
#include "frame/ScriptArgs.h"
#include "Common.h"
#include "Game.h"
#include "HookSecureFunc.h"
#include "Offsets.h"
@@ -85,9 +81,9 @@ namespace Frame::ScriptArgs {
namespace {
// Runtime switch. Default ON (core 5.1 behavior). When false, both detours
// tail-call the original runner unchanged (exact vanilla behavior, no
// reimplementation risk) — the OFF escape valve for this hot path.
// Runtime switch. Default ON (core 5.1 behavior). When false, the redirected
// pcall makes the engine's own call and RunWithValues passes no positional
// args — exact vanilla behavior.
bool g_enabled = true;
// Chained at static-init time by each `AutoObserve` constructor.
@@ -101,109 +97,11 @@ void Notify(bool begin, int handlerRef, void *frame) {
// Chained at static-init time by each `AutoTrailingBool` constructor.
AutoTrailingBool *g_trailing = nullptr;
// The engine runner sets only arg1..arg19 (0x704F10 stops at the 19th spec,
// `cmp [ebp-4],0x13; jge` at 0x704F73). 3.3.5's FrameScript_Execute has no
// limit, so a fire with more specs than this takes the reimplemented path.
constexpr int kEngineArgLimit = 19;
using Game::Lua::RawGetI;
// Writes "arg" + k into buf.
void ArgName(char (&buf)[16], int k) { std::snprintf(buf, sizeof buf, "arg%d", k); }
// Counts the values in the runner's printf-style format: each %d/%u/%f/%s.
// Any other letter after `%` is skipped and consumes no vararg, as in the
// engine runner's switch.
int CountArgs(const char *fmt) {
if (fmt == nullptr)
return 0;
int n = 0;
for (const char *p = fmt; *p != '\0'; ++p) {
if (*p != '%')
continue;
++p;
if (*p == '\0')
break;
if (*p == 'd' || *p == 'u' || *p == 'f' || *p == 's')
++n;
}
return n;
}
// Where RunCore's arguments come from: the runner's printf-style format over
// its packed vararg buffer (%d/%u/%s 4 bytes, %f 8), or `n` Lua values already
// on the stack from absolute index `stackBase` (they sit below everything
// RunCore pushes, so the index holds). `hasTrailing` appends one boolean after
// them (see AutoTrailingBool).
struct ArgSource {
const char *fmt = nullptr;
const void *vaPtr = nullptr;
int stackBase = 0;
int n = 0;
bool hasTrailing = false;
bool trailing = false;
int Count() const { return n + (hasTrailing ? 1 : 0); }
};
// Pushes an ArgSource's values in order, one per PushNext; a new cursor
// starts again from the first.
class ArgCursor {
public:
explicit ArgCursor(const ArgSource &src)
: src_(src), p_(src.fmt), cur_(static_cast<const char *>(src.vaPtr)) {}
void PushNext(void *L) {
using namespace Game::Lua;
if (k_ >= src_.n) {
PushBool(L, src_.trailing);
++k_;
return;
}
if (p_ == nullptr) {
PushValue(L, src_.stackBase + k_++);
return;
}
for (; *p_ != '\0'; ++p_) {
if (*p_ != '%')
continue;
++p_;
const char spec = *p_;
if (spec == '\0')
return;
switch (spec) {
case 'd':
PushNumber(L, static_cast<double>(*reinterpret_cast<const int *>(cur_)));
cur_ += 4;
break;
case 'u':
PushNumber(L, static_cast<double>(
*reinterpret_cast<const unsigned int *>(cur_)));
cur_ += 4;
break;
case 'f':
PushNumber(L, *reinterpret_cast<const double *>(cur_));
cur_ += 8;
break;
case 's': // a null string pushes nil, as lua_pushstring does
PushString(L, *reinterpret_cast<const char *const *>(cur_));
cur_ += 4;
break;
default:
continue; // unknown spec — consume nothing
}
++p_;
++k_;
return;
}
}
private:
const ArgSource &src_;
const char *p_;
const char *cur_;
int k_ = 0;
};
// OnEvent dispatch? The event dispatchers invoke the frame's OnEvent slot
// (frame+0x0C); matching the handler ref against that slot identifies it
// exactly.
@@ -239,53 +137,42 @@ void RestoreEvent(void *L, int saved) {
SetTop(L, saved - 1);
}
// Reimplements the runner tail: set the `this`/`arg1..argN` globals, invoke the
// handler with modern positional args `(self, [event,] arg1..argN)` under the
// engine's own message-handler errfunc, then restore the globals. `fmt`/`vaPtr`
// are null for the no-arg runner (N == 0).
// Runs a handler with the `n` Lua values at absolute index `firstArgIdx` (they
// sit below everything this pushes, so the index holds): set the
// `this`/`arg1..argN` globals, invoke the handler with modern positional args
// `(self, [event,] arg1..argN)` under the engine's own message-handler
// errfunc, then restore the globals.
//
// Unlike the engine (and our first cut), the old global values are saved on the
// Lua STACK, not in the registry via luaL_ref. A registry save leaks its ref if
// Unlike the engine runners, the old global values are saved on the Lua
// STACK, not in the registry via luaL_ref. A registry save leaks its ref if
// any push before the protected call longjmps — and out-of-memory, the one
// failure that actually fires here, does exactly that. Stack slots need no
// cleanup: an unwinding error resets `L->top` past them for free, so nothing
// leaks and there's no per-call ref churn on the OnUpdate hot path. The cost is
// cleanup: an unwinding error resets `L->top` past them for free. The cost is
// a deeper stack (up to ~2*N slots live at once), so we CheckStack up front.
//
// Returns false WITHOUT side effects when it declines (no Lua state, or the
// stack can't grow); the caller then runs the original. Every false path is
// before the first push, so falling back never double-runs the handler.
//
// `positional` false sets the globals but calls the handler with no
// arguments — the vanilla shape, for RunWithValues while the switch is off.
bool RunCore(void *L, int handlerRef, void *frame, const ArgSource &src,
bool positional) {
// arguments — the vanilla shape, while the switch is off.
void RunCore(void *L, int handlerRef, void *frame, int firstArgIdx, int n, bool positional) {
using namespace Game::Lua;
// Headroom for the saved-old block plus the transient set/call pushes.
if (CheckStack(L, 2 * src.Count() + 16) == 0)
return false;
if (CheckStack(L, 2 * n + 16) == 0)
return;
const bool haveThis = frame != nullptr;
// When so, `event` (a global the dispatcher has set) is prepended as the
// first positional to mirror retail's (self, event, arg1..argN).
const bool isOnEvent = IsOnEvent(handlerRef, frame);
const int selfRef = haveThis ? Game::Lua::ObjectLuaRef(frame) : 0;
const int n = src.Count();
const int selfRef = Game::Lua::ObjectLuaRef(frame);
char argName[16];
// --- save the globals we're about to clobber, onto the stack ------------
// Absolute indices, live until the closing SetTop(base):
// base+1 = old _G.this (if haveThis)
// savedArgBase+1 .. +n = old _G.arg1..argN
// base+1 = old _G.this
// base+2 .. base+1+n = old _G.arg1..argN
// Raw reads, as 3.3.5's FrameScript_Execute saves them: a `_G` __index
// (a strict-globals guard) never runs for an unset argN.
const int base = GetTop(L);
if (haveThis) {
PushString(L, "this");
RawGet(L, GLOBALS_INDEX); // [.. oldThis]
}
const int savedArgBase = base + (haveThis ? 1 : 0);
PushString(L, "this");
RawGet(L, GLOBALS_INDEX); // [.. oldThis]
for (int k = 0; k < n; ++k) {
ArgName(argName, k + 1);
PushString(L, argName);
@@ -293,16 +180,13 @@ bool RunCore(void *L, int handlerRef, void *frame, const ArgSource &src,
}
// --- set the new globals (each op is stack-neutral) ---------------------
if (haveThis) {
PushString(L, "this");
RawGetI(L, REGISTRY_INDEX, selfRef); // frame object
SetTable(L, GLOBALS_INDEX);
}
ArgCursor globals(src);
PushString(L, "this");
RawGetI(L, REGISTRY_INDEX, selfRef); // frame object
SetTable(L, GLOBALS_INDEX);
for (int k = 0; k < n; ++k) {
ArgName(argName, k + 1);
PushString(L, argName);
globals.PushNext(L);
PushValue(L, firstArgIdx + k);
SetTable(L, GLOBALS_INDEX);
}
@@ -318,17 +202,13 @@ bool RunCore(void *L, int handlerRef, void *frame, const ArgSource &src,
RawGetI(L, REGISTRY_INDEX, handlerRef); // handler
int nargs = 0;
if (positional) {
if (haveThis)
RawGetI(L, REGISTRY_INDEX, selfRef); // self
else
PushNil(L);
RawGetI(L, REGISTRY_INDEX, selfRef); // self
if (isOnEvent) { // event goes before the args, retail-style
PushString(L, "event");
RawGet(L, GLOBALS_INDEX);
}
ArgCursor args(src);
for (int k = 0; k < n; ++k)
args.PushNext(L);
PushValue(L, firstArgIdx + k);
nargs = 1 + (isOnEvent ? 1 : 0) + n;
}
PCall(L, nargs, 0, errIdx);
@@ -338,41 +218,22 @@ bool RunCore(void *L, int handlerRef, void *frame, const ArgSource &src,
for (int k = 0; k < n; ++k) {
ArgName(argName, k + 1);
PushString(L, argName);
PushValue(L, savedArgBase + 1 + k);
SetTable(L, GLOBALS_INDEX);
}
if (haveThis) {
PushString(L, "this");
PushValue(L, base + 1);
PushValue(L, base + 2 + k);
SetTable(L, GLOBALS_INDEX);
}
PushString(L, "this");
PushValue(L, base + 1);
SetTable(L, GLOBALS_INDEX);
SetTop(L, base);
return true;
}
// Asks the providers for this run's trailing boolean.
void FindTrailing(int handlerRef, void *frame, ArgSource &src) {
for (AutoTrailingBool *t = g_trailing; t != nullptr; t = t->next) {
bool value = false;
if (t->fn(handlerRef, frame, &value)) {
src.hasTrailing = true;
src.trailing = value;
return;
}
}
}
// The runner path: arguments from the engine's printf-style format.
bool RunModern(int handlerRef, void *frame, const char *fmt, const void *vaPtr) {
void *L = Game::Lua::State();
if (L == nullptr)
return false;
ArgSource src;
src.fmt = fmt;
src.vaPtr = vaPtr;
src.n = CountArgs(fmt);
FindTrailing(handlerRef, frame, src);
return RunCore(L, handlerRef, frame, src, /*positional*/ true);
// Asks the providers for this run's trailing boolean; false when none takes
// one.
bool FindTrailing(int handlerRef, void *frame, bool *value) {
for (AutoTrailingBool *t = g_trailing; t != nullptr; t = t->next)
if (t->fn(handlerRef, frame, value))
return true;
return false;
}
// Lua 5.0 internal offsets for reading a handler's arity — verified against the
@@ -442,69 +303,144 @@ bool FunctionWantsArgs(const void *tv, int depth) {
return false;
}
// Does this handler actually observe positional args? A vanilla handler reads
// this/arg1 globals and declares NO parameters, so pushing positional args to
// it is observably identical to vanilla — those route through the original
// engine runner instead (exact vanilla, and off the hottest Lua path). Only a
// handler FunctionWantsArgs accepts takes the reimplemented positional path.
// --- The runners' lua_pcall, redirected -------------------------------------
// Each runner calls `lua_pcall(L, nargs, nresults, errfunc)` with [errfunc,
// handler] on top, `this` and arg1..arg`count` already set. A handler that
// observes positional args (FunctionWantsArgs, read straight off the top) gets
// `(self, [event,] arg1..argN [, trailing])`, each argN read back raw from the
// global the runner just set; any other handler, and every run while the
// switch is off, gets the engine's call unchanged.
//
// Reads the closures directly (the engine exposes no arity via the C API);
// every read is bounded — a non-function ref or a null Proto falls through to
// "no".
bool HandlerWantsArgs(void *L, int handlerRef) {
RawGetI(L, Game::Lua::REGISTRY_INDEX, handlerRef); // push registry[handlerRef]
const void *tv = reinterpret_cast<const void *>(
// A trailing boolean (AutoTrailingBool) also becomes global arg`count+1` for
// the call, as 3.3.5 sets it. Its old value waits on the stack below the
// errfunc, where the runner's own pops after the call never reach it.
int __fastcall RunnerPCall(void *L, int nargs, int nresults, int errfunc, int handlerRef,
void *frame, int count) {
using namespace Game::Lua;
const void *handler = reinterpret_cast<const void *>(
Game::Read<uintptr_t>(L, OFF_LUASTATE_TOP) - kTValueSize);
const bool wants = FunctionWantsArgs(tv, 0);
Game::Lua::SetTop(L, -2); // pop the handler
return wants;
// Headroom for self, event, the args, the trailing value and its save.
if (!g_enabled || !FunctionWantsArgs(handler, 0) || CheckStack(L, count + 8) == 0)
return PCall(L, nargs, nresults, errfunc);
bool trailing = false;
const bool hasTrailing = FindTrailing(handlerRef, frame, &trailing);
char trailingName[16];
int saved = 0;
if (hasTrailing) {
ArgName(trailingName, count + 1);
PushString(L, trailingName);
RawGet(L, GLOBALS_INDEX); // [.. errfunc handler old]
Insert(L, -3); // [.. old errfunc handler]
saved = GetTop(L) - 2;
PushString(L, trailingName);
PushBool(L, trailing);
SetTable(L, GLOBALS_INDEX);
}
int pushed = 1;
if (frame != nullptr)
RawGetI(L, REGISTRY_INDEX, Game::Lua::ObjectLuaRef(frame)); // self
else
PushNil(L);
if (IsOnEvent(handlerRef, frame)) { // event goes before the args, retail-style
PushString(L, "event");
RawGet(L, GLOBALS_INDEX);
++pushed;
}
char argName[16];
for (int k = 1; k <= count; ++k) {
ArgName(argName, k);
PushString(L, argName);
RawGet(L, GLOBALS_INDEX);
}
pushed += count;
if (hasTrailing) {
PushBool(L, trailing);
++pushed;
}
// Both runners pass errfunc = -2, relative to the top: it moves down by
// every argument pushed above it.
const int status = PCall(L, nargs + pushed, nresults, errfunc - pushed);
if (hasTrailing) {
PushString(L, trailingName);
PushValue(L, saved);
SetTable(L, GLOBALS_INDEX);
Remove(L, saved);
}
return status;
}
// --- FUN_FRAME_RUN_SCRIPT_ARGS co-hook (scripts with values) ----------------
// The runners reach RunnerPCall through these. ECX/EDX (L, nargs) and the
// pcall's two stack arguments pass through; each thunk adds the run's handler
// ref, frame and argument count from the runner's own frame (see
// CALL_FRAME_RUN_SCRIPT_ARGS_PCALL), then returns as lua_pcall does (`ret 8`).
//
// FUN_FRAME_RUN_SCRIPT_ARGS: [ebp+8] handlerRef, [ebp+0Ch] frame, [ebp-4] the
// number of argN its switch set — every spec it ran, one added at runtime
// included.
__declspec(naked) void RunArgsPCallThunk() {
__asm {
push dword ptr [ebp - 4] // count
push dword ptr [ebp + 0Ch] // frame
push dword ptr [ebp + 8] // handlerRef
push dword ptr [esp + 14h] // errfunc
push dword ptr [esp + 14h] // nresults
call RunnerPCall
ret 8
}
}
// FUN_FRAME_INVOKE_SCRIPT: [ebp-8] handlerRef, EDI frame; it sets no argN.
__declspec(naked) void InvokePCallThunk() {
__asm {
push 0 // count
push edi // frame
push dword ptr [ebp - 8] // handlerRef
push dword ptr [esp + 14h] // errfunc
push dword ptr [esp + 14h] // nresults
call RunnerPCall
ret 8
}
}
// Runs on both registration passes, so the glue screen's scripts get the
// positional args too. Idempotent.
void PatchCode() {
Common::RedirectCall(Offsets::CALL_FRAME_RUN_SCRIPT_ARGS_PCALL, Offsets::LUA_PCALL,
reinterpret_cast<const void *>(&RunArgsPCallThunk));
Common::RedirectCall(Offsets::CALL_FRAME_INVOKE_SCRIPT_PCALL, Offsets::LUA_PCALL,
reinterpret_cast<const void *>(&InvokePCallThunk));
}
const Game::GlueModuleAutoRegister _gluePatch{&PatchCode};
// --- Runner co-hooks: observers and the per-handler `event` ------------------
using RunArgs_t = void(__cdecl *)(int handlerRef, void *frame, const char *fmt,
const void *vaPtr);
RunArgs_t g_origRunArgs = nullptr;
void RunArgs(void *L, int handlerRef, void *frame, const char *fmt,
const void *vaPtr) {
// Past the engine's limit the original would drop arg20 onward, so even a
// handler that declares no parameters takes the uncapped path.
if (g_enabled && L != nullptr && handlerRef != 0 && fmt != nullptr &&
(HandlerWantsArgs(L, handlerRef) || CountArgs(fmt) > kEngineArgLimit) &&
RunModern(handlerRef, frame, fmt, vaPtr))
return;
g_origRunArgs(handlerRef, frame, fmt, vaPtr);
}
void __cdecl RunArgs_h(int handlerRef, void *frame, const char *fmt,
const void *vaPtr) {
void *L = Game::Lua::State();
Notify(true, handlerRef, frame);
const int savedEvent = SaveEvent(L, handlerRef, frame);
RunArgs(L, handlerRef, frame, fmt, vaPtr);
g_origRunArgs(handlerRef, frame, fmt, vaPtr);
RestoreEvent(L, savedEvent);
Notify(false, handlerRef, frame);
}
// --- FUN_FRAME_INVOKE_SCRIPT co-hook (no-arg scripts) -----------------------
// Same address Tooltip::SetEvents calls directly to fire OnTooltipSet*; with
// the switch enabled those fire with (self) too, which is the modern shape.
using Invoke_t = void(__fastcall *)(int handlerRef, void *frame);
Invoke_t g_origInvoke = nullptr;
void Invoke(void *L, int handlerRef, void *frame) {
if (g_enabled && L != nullptr && handlerRef != 0 && frame != nullptr &&
HandlerWantsArgs(L, handlerRef) &&
RunModern(handlerRef, frame, /*fmt*/ nullptr, /*vaPtr*/ nullptr))
return;
g_origInvoke(handlerRef, frame);
}
void __fastcall Invoke_h(int handlerRef, void *frame) {
void *L = Game::Lua::State();
Notify(true, handlerRef, frame);
const int savedEvent = SaveEvent(L, handlerRef, frame);
Invoke(L, handlerRef, frame);
g_origInvoke(handlerRef, frame);
RestoreEvent(L, savedEvent);
Notify(false, handlerRef, frame);
}
@@ -532,6 +468,7 @@ int __fastcall Script_GetModernScriptArgs(void *L) {
}
void RegisterLuaFunctions() {
PatchCode();
Game::Lua::RegisterGlobalFunction("SetModernScriptArgs",
&Script_SetModernScriptArgs);
Game::Lua::RegisterGlobalFunction("GetModernScriptArgs",
@@ -545,12 +482,9 @@ const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
void RunWithValues(void *L, int handlerRef, void *frame, int firstArgIdx, int nargs) {
if (handlerRef == 0 || frame == nullptr)
return;
ArgSource src;
src.stackBase = firstArgIdx;
src.n = nargs;
Notify(true, handlerRef, frame);
const int savedEvent = SaveEvent(L, handlerRef, frame);
RunCore(L, handlerRef, frame, src, /*positional*/ g_enabled);
RunCore(L, handlerRef, frame, firstArgIdx, nargs, /*positional*/ g_enabled);
RestoreEvent(L, savedEvent);
Notify(false, handlerRef, frame);
}
+2 -1
View File
@@ -18,7 +18,8 @@
namespace Frame::ScriptArgs {
// Runs a frame-script handler with arbitrary Lua values as its arguments —
// the engine's runners can only pass %d/%u/%f/%s. `nargs` values at absolute
// the engine's runners pass only what a printf-style format can carry (no
// booleans). `nargs` values at absolute
// stack index `firstArgIdx` become `arg1..argN` (globals, saved and restored
// around the call) and, while modern script args are on, the positional
// `(self, ...)`. Sets `this`, runs under the engine's script error handler,
+44 -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,24 @@ const Game::HookAutoRegister _hookUnregisterAll{
} // namespace
bool Accepts(const void *frame, const char *eventName, const char *arg1) {
if (g_filters.empty() || frame == nullptr || eventName == nullptr)
return true;
auto it = g_filters.find(frame);
if (it == g_filters.end())
return true;
const Filter *match = DispatchFilter(it->second, eventName);
return match == nullptr || MatchesUnit(*match, arg1);
}
const std::vector<std::string> *Units(const void *frame, const char *eventName) {
if (g_filters.empty() || frame == nullptr || eventName == nullptr)
return nullptr;
auto it = g_filters.find(frame);
if (it == g_filters.end())
return nullptr;
const Filter *match = DispatchFilter(it->second, eventName);
return match != nullptr ? &match->units : nullptr;
}
} // namespace Frame::UnitEvent
+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 <string>
#include <vector>
namespace Frame::UnitEvent {
// The RegisterUnitEvent filter for one fire whose first argument is the
// string `arg1`: false when `frame` registered `eventName` (the event-table
// entry's own name) with units and `arg1` matches none of them. True for a
// plain registration. The engine's dispatches meet this filter at the runner;
// a dispatcher of our own that walks the subscriber chain itself
// (Event::Custom::FireValues) asks here for each frame.
bool Accepts(const void *frame, const char *eventName, const char *arg1);
// The units `frame` gave RegisterUnitEvent for `eventName` (the event-table
// entry's own name), or nullptr for a plain registration. Valid until the
// frame's registrations change.
const std::vector<std::string> *Units(const void *frame, const char *eventName);
} // namespace Frame::UnitEvent
+240
View File
@@ -0,0 +1,240 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `UNIT_CONNECTION`, `UNIT_AREA_CHANGED` and the generic power events for a
// group member's party record.
//
// A group member's stats packet (ReadPartyMemberStats, 0x005E5110) and the
// player's own record check (ComparePartyRecord, 0x005F0B80) end in
// SignalPartyRecordChanges (FUN_PARTY_RECORD_SIGNAL), which raises the unit
// events for the fields that differ between the old and the new record. This
// is the 1.12 copy of 4.3.4's CGPlayer_C::DispatchPartyMemberStatsChanges
// (Player_C.cpp), which in the same places also signals UNIT_CONNECTION
// `(token, isConnected)` when the online bit of the status flags flips, and
// UNIT_POWER / UNIT_POWER_FREQUENT / UNIT_MAXPOWER with the power token
// beside each per-type power event. Retail adds UNIT_AREA_CHANGED `(token)`
// for the zone; DiffPartyRecords sets its bit (0x80, record +0x14) and the
// raiser reads nothing for it. The server sends the zone only to members
// that cannot see the player (tortoise-wow's Group::UpdatePlayerOutOfRange).
// A member in view is built with the player's own zone (BuildPartyRecord
// stores s_zoneId), which is also the zone GetRaidRosterInfo gives for a
// held object, so ComparePartyRecord sets the bit when a member comes into
// view from another zone. It never runs for the active player
// (CGPlayer_C::PostInit skips it), so "player" gets no event.
//
// Both calls point here instead (Common::RedirectCall): the function calls
// the raiser, then adds those events for the same change bits, after the
// engine's own events and with the new record already stored, so
// UnitIsConnected and UnitPower read the new values. A status change also
// raises the power event, and the raiser then drops the health and power
// bits, so the power events follow that order exactly.
//
// A member who logs off is reported only in the group list (SMSG 0x7D):
// tortoise-wow's Group::UpdatePlayerOnlineStatus sends SendUpdate alone for
// a logout. 4.3.4's CGPartyInfo::AddMember ends that path with the same
// diff and dispatch; 1.12's AddPartyMember stops after storing the record,
// so its call gets the missing tail. A raid member outside the player's
// party has no party record: RebuildRaidRoster sets its online bit in the
// raid slot, so the online bits before and after that call are compared,
// and a flip fires UNIT_CONNECTION, as retail's raid frames expect. Party
// members are left to AddPartyMember, which ran first.
#include "Common.h"
#include "Game.h"
#include "Offsets.h"
#include "event/Custom.h"
#include "unit/Identity.h"
#include "unit/PowerEvents.h"
#include <cstdint>
namespace Group::MemberEvents {
namespace {
// DiffPartyRecords (0x005E5510) change bits the raiser reads.
constexpr uint32_t CHANGE_STATUS = 0x1;
constexpr uint32_t CHANGE_HEALTH = 0x2;
constexpr uint32_t CHANGE_POWER = 0x10;
constexpr uint32_t CHANGE_MAX_POWER = 0x20;
constexpr uint32_t CHANGE_ZONE = 0x80;
constexpr uint32_t CHANGE_PET_POWER = 0x20000;
constexpr uint32_t CHANGE_PET_MAX_POWER = 0x40000;
const Game::Doc::Field kConnectionPayload[] = {
Game::Doc::Req("unitTarget", "UnitToken"),
Game::Doc::Req("isConnected", "bool"),
};
const Game::Doc::Event kConnectionDoc{
"Unit", "Fires when a group member goes offline or comes back online.", kConnectionPayload};
const Event::Custom::AutoReserve kConnection{"UNIT_CONNECTION", &kConnectionDoc};
const Game::Doc::Field kAreaPayload[] = {
Game::Doc::Req("unitTarget", "UnitToken"),
};
const Game::Doc::Event kAreaDoc{"Unit", "Fires when a group member moves to a different zone.",
kAreaPayload};
const Event::Custom::AutoReserve kArea{"UNIT_AREA_CHANGED", &kAreaDoc};
using Signal_t = void(__fastcall *)(const uint64_t *guid, uint32_t changes, const uint8_t *oldRec,
const uint8_t *newRec);
using Diff_t = uint32_t(__fastcall *)(const uint8_t *oldRec, const uint8_t *newRec);
using StatsLookup_t = const uint8_t *(__fastcall *)(const uint64_t *guid);
using AddMember_t = void(__fastcall *)(uint32_t online, const uint8_t *oldRecord, uint64_t guid);
using RebuildRaid_t = void(__fastcall *)(const uint64_t *guids, const uint32_t *flags,
const uint8_t *online, uint32_t count);
void SignalConnection(uint64_t guid, bool connected) {
const int slot = kConnection.Slot();
if (!Event::Custom::HasListeners(slot))
return;
void *L = Game::Lua::State();
if (L == nullptr)
return;
char tokens[16][32];
const int n = Unit::Identity::TokensForGUID(guid, tokens, 16);
const int top = Game::Lua::GetTop(L);
for (int i = 0; i < n; ++i) {
Game::Lua::PushString(L, tokens[i]);
Game::Lua::PushBool(L, connected);
Event::Custom::FireValues(slot, L, top + 1, 2);
Game::Lua::SetTop(L, top);
}
}
void SignalArea(uint64_t guid) {
const int slot = kArea.Slot();
if (!Event::Custom::HasListeners(slot))
return;
char tokens[16][32];
const int n = Unit::Identity::TokensForGUID(guid, tokens, 16);
for (int i = 0; i < n; ++i)
Event::Custom::Fire(slot, "%s", tokens[i]);
}
void __fastcall SignalSite(const uint64_t *guid, uint32_t changes, const uint8_t *oldRec,
const uint8_t *newRec) {
reinterpret_cast<Signal_t>(Offsets::FUN_PARTY_RECORD_SIGNAL)(guid, changes, oldRec, newRec);
const uint64_t member = *guid;
const int powerType = Game::Read<uint8_t>(newRec, Offsets::OFF_GROUP_MEMBER_STATS_POWER_TYPE);
if (changes & CHANGE_STATUS) {
Unit::PowerEvents::SignalPower(member, powerType);
const uint8_t status = Game::Read<uint8_t>(newRec, Offsets::OFF_GROUP_MEMBER_STATUS_FLAGS);
const uint8_t flips =
status ^ Game::Read<uint8_t>(oldRec, Offsets::OFF_GROUP_MEMBER_STATUS_FLAGS);
if (flips & Offsets::GROUP_MEMBER_STATUS_ONLINE)
SignalConnection(member, (status & Offsets::GROUP_MEMBER_STATUS_ONLINE) != 0);
changes &= ~(CHANGE_HEALTH | CHANGE_POWER);
}
if (changes & CHANGE_POWER)
Unit::PowerEvents::SignalPower(member, powerType);
if (changes & CHANGE_MAX_POWER)
Unit::PowerEvents::SignalMaxPower(member, powerType);
if (changes & CHANGE_ZONE)
SignalArea(member);
const uint64_t pet = Game::Read<uint64_t>(newRec, Offsets::OFF_GROUP_MEMBER_PET_GUID);
if (pet == 0)
return;
const int petPowerType = Game::Read<uint8_t>(newRec, Offsets::OFF_GROUP_MEMBER_PET_POWER_TYPE);
if (changes & CHANGE_PET_POWER)
Unit::PowerEvents::SignalPower(pet, petPowerType);
if (changes & CHANGE_PET_MAX_POWER)
Unit::PowerEvents::SignalMaxPower(pet, petPowerType);
}
void __fastcall AddMemberSite(uint32_t online, const uint8_t *oldRecord, uint64_t guid) {
reinterpret_cast<AddMember_t>(Offsets::FUN_ADD_PARTY_MEMBER)(online, oldRecord, guid);
if (oldRecord == nullptr)
return;
const uint8_t *record =
reinterpret_cast<StatsLookup_t>(Offsets::FUN_GROUP_MEMBER_STATS_LOOKUP)(&guid);
if (record == nullptr)
return;
const uint32_t changes =
reinterpret_cast<Diff_t>(Offsets::FUN_PARTY_RECORD_DIFF)(oldRecord, record);
if (changes != 0)
SignalSite(&guid, changes, oldRecord, record);
}
struct Presence {
uint64_t guid;
bool online;
};
// The raid slots' GUIDs and online bits.
int RaidPresence(Presence (&out)[Offsets::RAID_MAX_SLOTS]) {
int count = Game::Read<int>(Offsets::VAR_RAID_MEMBER_COUNT);
if (count > Offsets::RAID_MAX_SLOTS)
count = Offsets::RAID_MAX_SLOTS;
const auto *slots = reinterpret_cast<const uint8_t *const *>(
static_cast<uintptr_t>(Offsets::VAR_RAID_MEMBER_PTRS));
int n = 0;
for (int i = 0; i < count; ++i) {
if (slots[i] == nullptr)
continue;
const uint8_t flags = Game::Read<uint8_t>(slots[i], Offsets::OFF_RAID_SLOT_FLAGS);
out[n++] = {Game::Read<uint64_t>(slots[i], 0),
(flags & Offsets::RAID_SLOT_FLAG_ONLINE) != 0};
}
return n;
}
bool IsPartyMember(uint64_t guid) {
const auto *party =
reinterpret_cast<const uint64_t *>(static_cast<uintptr_t>(Offsets::VAR_PARTY_GUIDS));
for (int i = 0; i < Offsets::PARTY_MAX_SLOTS; ++i)
if (party[i] == guid)
return true;
return false;
}
void __fastcall RaidRosterSite(const uint64_t *guids, const uint32_t *flags,
const uint8_t *online, uint32_t count) {
Presence before[Offsets::RAID_MAX_SLOTS];
const int was = RaidPresence(before);
reinterpret_cast<RebuildRaid_t>(Offsets::FUN_REBUILD_RAID_ROSTER)(guids, flags, online, count);
if (!Event::Custom::HasListeners(kConnection.Slot()))
return;
Presence after[Offsets::RAID_MAX_SLOTS];
const int now = RaidPresence(after);
for (int i = 0; i < now; ++i) {
if (IsPartyMember(after[i].guid))
continue;
for (int j = 0; j < was; ++j) {
if (before[j].guid == after[i].guid) {
if (before[j].online != after[i].online)
SignalConnection(after[i].guid, after[i].online);
break;
}
}
}
}
void Register() {
const auto *site = reinterpret_cast<const void *>(&SignalSite);
Common::RedirectCall(Offsets::CALL_READ_PARTY_MEMBER_STATS_SIGNAL,
Offsets::FUN_PARTY_RECORD_SIGNAL, site);
Common::RedirectCall(Offsets::CALL_COMPARE_PARTY_RECORD_SIGNAL,
Offsets::FUN_PARTY_RECORD_SIGNAL, site);
Common::RedirectCall(Offsets::CALL_GROUP_LIST_ADD_PARTY_MEMBER, Offsets::FUN_ADD_PARTY_MEMBER,
reinterpret_cast<const void *>(&AddMemberSite));
Common::RedirectCall(Offsets::CALL_GROUP_LIST_REBUILD_RAID_ROSTER,
Offsets::FUN_REBUILD_RAID_ROSTER,
reinterpret_cast<const void *>(&RaidRosterSite));
}
const Game::ModuleAutoRegister _autoreg{&Register};
} // namespace
} // namespace Group::MemberEvents
+1 -1
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

Some files were not shown because too many files have changed in this diff Show More