1.12's SpellShapeshiftForm.dbc gives SHAPESHIFT_FORM_FLAG_CAN_INTERACT
(0x8) only to Shadowform. 3.3.5 and 4.4.2 also give it to Cat, Travel,
Aquatic, Bear, Dire Bear and Ghost Wolf, and Turtle's id 11 (Swift
Travel Form) has it there too. So in the later clients, these forms pass
the NPC right-click gate (CGPlayer_C::HandleOnRightClick, 0x005F0130)
and the game-object use check (0x005F3130) as they are. The form cancel
that comes before the range check almost never runs.
In 1.12, autoUnshift cancelled every druid form at that gate, even when
the NPC was out of interact range. With autoUnshift on, the redirected
IsShapeShifted calls at both gates and at CanInteractNow (0x00606880)
now read a form in that set as able to interact. They report "not
shifted", so the engine's own 0x8 test that follows does not refuse it,
and the cursor shows the NPC's service icon. The cancel stays for a form
that cannot interact in either client.
Neither vmangos nor tortoise-wow tests the form on an NPC or object
interaction. CanInteractWithNPC and the use handlers have no form check;
the only one is item use. So the client gates are the only refusal.
The three settings only covered casts. Each 1.12.1 gate that refuses an
action for the player's stand state, mount or form now passes when the
matching setting allows it, and the engine's own action (the stand setter
FUN_005ED430, or CMSG_CANCEL_AURA for the mount / form aura) runs just
before the action is sent. With the settings at 0 nothing changes.
- NPC right-click (0x005F0130): the IsShapeShifted call at 0x005F0150
cancels a form without SpellShapeshiftForm flag 0x8 instead of showing
ERR_CANT_INTERACT_SHAPESHIFTED. CanInteractNow (0x00606880), whose only
caller is the hover cursor, passes the same form, so the NPC's service
cursor shows.
- Game objects: the use check (vtable slot 6, 0x005F3130), also the hover
cursor's query, passes the mount (0x005F31A8) and the form (0x005F31E1)
for an object with no Lock row. The default use's no-spell branch
(0x005F35DF, before CMSG_GAMEOBJ_USE) and the mailbox and text use slots
dismount (except at a mailbox, FUN_0047CFF0) and cancel the form. A
locked object opens by a spell, which the cast path already handles.
- Loot: the GetStandState calls of the unit (0x0060BFB7) and corpse
(0x005D6C3B) right-clicks stand the player up instead of showing
ERR_LOOT_NOTSTANDING. The unit loot step's mount read (0x0060BF98)
passes, and LootUnit dismounts before CMSG_LOOT (0x005DF3B3).
- Item use (0x005D8D00): the ERR_NO_ITEMS_WHILE_SHAPESHIFTED refusal at
0x005D914D cancels the form and continues. Turtle's exe already skips
that test; the stock exe reaches it.
- Attack: the mount reads in the attack cursor (0x00482659), the
right-click attack (0x0060C1B4), StartAttack (0x005ECC06) and both tests
of the attack resolver (0x00613039, ERR_ATTACK_MOUNTED; 0x00613142)
pass. StartAttack, after all its tests (0x005ECC56), dismounts when the
target is in melee reach (Combat::Swing::InMeleeRange). Otherwise it
returns without attacking and shows ERR_OUT_OF_RANGE, except while
click-to-move (FUN_0060F900) walks the player there.
- Taxi: after the taxi map opener's TAXIMAP_OPENED tutorial call
(0x004DBAA2), a map with more than one node stands the player up,
dismounts and cancels the form, each when its setting is on.
Every site is patched only when it holds the expected stock bytes, which
both the Octo and stock 1.12.1 exes do.
PlayerModel:ApplySpellVisualKit(spellVisualKitID [, oneShot]) shows a
SpellVisualKit's effects on the pane's model.
In the world, 1.12.1 plays a kit on a unit (0x0060EDF0), through effect
objects, spell durations and the unit's own animation, and a pane has
none of these. The engine's own kit on a bare model is the
char-select ghost (0x004727F0), and this is that routine for any kit on
the pane's model:
- Each of the nine effect columns (+0x0C) is created as a model from its
SpellVisualEffectName file, in the pane's scene. It is attached at
the column's attachment (20, 34, 19, 21, 22, 17, 23, 24, 25) when the
model has that attachment (0x00712CB0, 0x00712F70), as in 0x00472780.
The effects get the frame's light through LightChildren.
- Procedure kind 1 sets the tint (an 0xRRGGBB colour stored as a float,
scaled by 1/255) and kind 14 the alpha, clamped to 0..1. Both are
plain stores (0x00710CF0, 0x00710CB0), as the ghost kit makes them.
The kit's animation and sound do not play, which matches retail.
An effect model's sequence plays one time. Without oneShot, each effect
gets the world's loop callback (KitEffectLoopCallback, 0x0060ED00): a
clean end (reason 0) plays the animation again. With oneShot, each
effect gets EffectDoneCallback's test (0x005FBF50) and CEffect::Stop's
model steps (0x006203E0): clear the callback and hide the model, so
its emitters stop and the particles already out finish. A FrameTick
drain, which runs after the strata walk, then mirrors
DrainDyingEffects (0x0061F680). It detaches a stopped effect (0x00713020)
once HasLiveParticles (0x007135C0) is false, or at once when its
model has already dropped it. A model's destructor detaches its
children first (0x0070E3F5), so the drain never reaches a freed parent.
The effects are children of the pane's model, so SetUnit, SetItem or
a hide and show clears them.
PlayerModel:SetItem(itemID [, appearanceModID, itemVisualID]) shows one
item's own model in the pane: a weapon, shield, helm, shoulder, quiver or
ammo, with its skin texture and its glow.
A 1.12.1 pane shows whatever its display record names (+0x3F0).
SetDisplay (0x00505CB0) stores the record and builds it when the pane is
visible, the show path (0x00505D49) builds it again, and the SetUnit
helper (0x00505B50) clears it. SetItem gives the pane a record of its
own and passes it to SetDisplay. The AttachDisplayModel (0x00505A70)
co-hook in Model::DisplayInfo then builds the item instead of a
creature. So hide, show, SetUnit, SetDisplayInfo and SetKeepModelOnHide
handle an item with no new hooks.
The model is built from 1.12.1's own steps:
- the item folder by inventory type, from the engine's own
Item\ObjectComponents\ path strings (0x00838CD8 on);
- model name 0, else name 1, with the texture of the same index;
- for a head item, the helm builder's (0x004799A0) path, with the
player's race prefix (ChrRaces +0x18) and sex letter (0x00838AB4);
- AttachItemModelFile's (0x004798C0) steps: create the model in the
pane's scene, load the skin with the item-model status (0x00B424F0)
and filter 3, bind it as texture type 2 (0x00710EC0), and attach the
item visuals (0x00479700);
- AttachDisplayModel's install: SetModelInstance (vtable +0x90), the
pane's done callback (0x005062A0 through 0x00711BB0), Stand, and the
release of the creation reference.
The display's own ItemVisuals row wins over itemVisualID, the rule
GetItemDescVisual (0x0062EC70) applies to enchants. The visual's child
models get the frame's light and fog callback, now a shared
LightChildren helper, so they are not drawn black. An uncached item is
queued through Item::Data::WarmCache, and each visible pane that still
shows it is built again when the item loads.
FreezeAnimation(animation, variation, frame) holds the model in one
pose of an animation until the pane loads a new model, as SetAnimation
does. frame is the time into the animation in milliseconds, the unit
of M2 sequence times.
The engine's sequence time is start + (ftol(elapsed * speed) + offset)
mod length. So the pane's arm plays the animation through
PlayAnimation (0x007121A0) at speed 0 and writes frame to bone 0's
slot offset (+0xB8, bone states 0x118 bytes each at [model+0x90]).
Speed 0 also puts the end stamp on the start stamp. Update
(0x00719370) would then queue the done callback on the next frame, and
the pane's done callback (0x005062A0) would arm Stand. The arm
therefore sets the slot's done latch (+0xC0), the flag Update itself
sets once it has reported done. A held animation never ends.
SetAnimation and FreezeAnimation now also mark the pane for one re-arm.
A call with the animation that already plays then still starts or ends
a freeze, where the per-paint compare alone would skip it.
Adds SetAnimation, SetDoBlend/GetDoBlend, HasAnimation,
SetKeepModelOnHide/GetKeepModelOnHide, CanSetUnit and GetDisplayInfo
to PlayerModel (and so to DressUpModel and TabardModel). SetUnit now
returns whether the unit exists, and SetCreature takes an optional
display ID.
Animation. A 1.12.1 pane rests on Stand: its per-paint UpdateModel
(0x00505C50) arms Stand (PlayAnimation 0x007121A0 with id 0) whenever
key bone -1 plays anything else and no turn is pending. A deadline of 0
counts as passed, so any other animation lasted one frame. SetAnimation
gives the pane a resting animation and variation of its own. With one
set, UpdateModel is hooked to arm that animation in Stand's place, using
the pane's blend setting. Every other pane runs the stock function. The
pane's done callback (0x005062A0) arms Stand when the animation ends,
so the next paint starts it again and it loops. CSimpleModelFFX's
SetModelInstance (0x0076CD30), which every pane model build and hide
goes through, clears it, so a new model rests on Stand again.
SetRotation turns without the shuffle while a resting animation other
than Stand is set. HasAnimation is PlayAnimation's own test on the raw
id: the lookup at shared data +0x24 must give an index below the
sequence count at +0x1C. It is false until the model has loaded.
Keep on hide. The pane's Hide (0x00505CE0) drops its model through
slot 36, and its Show (0x00505D00) builds a new one from the unit or
display record. With keep on, Hide runs only the base hide (0x0076AD50)
and Show runs only the base show (0x0076AE10) while the pane still
holds its model. The model then comes back with its try-ons and
animation. The SetUnit helper (0x00505B50, behind SetUnit, RefreshUnit
and Dress) and SetDisplay (0x00505CB0) only store the new subject while
the pane is hidden, so they drop a kept model first. The next Show then
builds the new one. Off is the default and matches the stock client.
Units and displays. SetUnit runs the engine's method (0x00505D70,
which Turtle patches) unchanged. It then reports whether the GUID it
stored resolves to a unit (type mask 8), the same test the helper uses
before it builds. CanSetUnit is that resolve without the set.
GetDisplayInfo reads the display record's ID, or the unit's
UNIT_FIELD_DISPLAYID (+0x1F4, field 0x7D). SetCreature with a display
ID shows that display through the SetDisplayInfo record.
TryOn now takes retail's (item [, handSlotName, spellEnchantID]) and
returns an Enum.ItemTryOnReason. The pane gains IsGeoReady, SetSheathed /
GetSheathed and SetAutoDress / GetAutoDress.
Hand: TryOnItem (0x00504540) picks a lane at one site, 0x0050492D
(mov edx,[edi+0x2C]; cmp edx,0x0D). A 6-byte call there jumps a named hand
to the worker's own main-hand (0x0050495F) or off-hand (0x005049B1) path,
so its CanHoldBoth (0x00504BC0) eviction, lane store and attach all still
run; with no hand it replays the two instructions and the jnz reads their
flags. The name is matched like GetInventorySlotInfo matches it: SStrCmpI
over the PaperDollItemFrame rows, slot 16 or 17.
Enchant glow: the player's AttachEnchantVisual step (0x005EED50) on the
attachment AttachItemModel (0x0047A0C0) returns: FindChild (0x00712F00),
then AttachItemVisuals (0x00479700) with SpellItemEnchantment +0x58,
skipped when the display's own ItemVisuals row exists (GetItemDescVisual's
rule, 0x0062EC70). The enchant is the argument, else the link's enchant
field, which Item::Arg now parses.
Sheathing re-runs the unit's AttachHandItem step on the pane's lanes with
the sheathed flag. The lanes keep no display row, so the pane records one:
the unit's vtable +0x9C row when SetModelInstance (slot 0x00808438) clones
it, then whatever the worker's attach (0x00504AE6) installs; its two
evictions (0x00504A0C, 0x00504A55) drop theirs. Each lane keeps its glow
too, a player's read through GetVisibleItem + GetItemDescVisual, so a
re-hung weapon keeps it. Auto-dress off runs ClearModel (0x00504490) after
SetModelInstance unless Dress() is the one rebuilding.
TryOn's result reads the gates the worker passes: an uncached record is
DataPending, a held type installs, armor needs the component and the
ready byte (+0x40C).
DressUpItemLink now passes the whole link to DressUpItem, so a
Control-click shows the link's enchant, and skips a cached item that
IsDressableItem rejects. DressUpItem stays, since Blizzard_AuctionUI and
Turtle both replace it.
Adds the expandedCursors cvar (default 0; AddOn Options > ClassicAPI >
Interface > Cursor). At 1 the hover cursor shows what an NPC offers: an
anvil for a repairer, a hearthstone for an innkeeper, a horseshoe for a
stable master, and a quest giver's ! or ? (a blue ? for a repeatable
reward). The player's fishing bobber shows a fish hook in place of the
Interact gear. Each has a gray form beyond reach or when not usable.
NPCs: ClassifyHoveredUnit (0x00482200) reaches its NPC flag chain at
0x00482336 for a unit the player can interact with, the unit in ESI and
the beyond-reach flag in EBX. The chain's `mov ecx, [esi+0x110]` becomes
a call to a thunk. At 0 it replays the instruction and the stock chain
runs. At 1 it picks from the quest giver status first (CGUnit_C +0xCB8,
read by the meaning of the client's status-to-marker table at
0x0080C454), then the NPC flags in service order: repair, innkeeper,
flight master, trainer, spirit healer, banker, registrar/tabard/
battlemaster, auctioneer, stable master, vendor, then gossip or a quest
giver IsQuestGiverToTalk (0x005DF490) accepts. It leaves through the
chain's own epilogue (0x00482356).
Bobber: the fishing node handler's cursor slot (vtable 0x0080BC80, slot
4; GetCursor 0x005F8760 is its one reader) points at a wrapper that runs
BaseHandlerGetCursor (0x005F3070) and maps Interact/UnableInteract to
the hook. The engine still decides whether the bobber is interactable
and usable.
Showing a mode past 41: SetCursorMode indexes a 42-slot image table, so
its existing hook now sends the new modes to Cursor::Extended::Show. It
calls SetCustomCursor (0x00523C40) with the mode's path, so a real file
wins, and otherwise does what that function does after its load: copy
the 32x32 image into slot 41 (0x00C0BD80), store mode 41, send it
(SendCursorImage 0x00523790) and keep the name. The hook also records
the new modes while Hover::Mode computes, so the soft-interact pick, its
icon and SetUnitCursorTexture follow; a quest cursor has no icon image.
Images: the 14 BLPs ship inside the DLL (src/embed/Textures, a new
TEXTURES_ROOT pack). Texture::Builtin decodes each once with the
engine's BLP reader from memory: BlpParse (0x005A83B0) is BlpOpen
without the file read, and BlpReset never frees the file buffer. The
reader moves to Texture::BlpReader, shared with the map overlays.
Texture loads of the same paths, such as the option label's inline
icons, get the same images.
The reader refuses a DXT5 file. BlpDecodeLevel (0x005A7F00) converts a
DXT level through the matrix InitConvertMatrix (0x005A4FC0) fills, and
that matrix has DXT1 and DXT3 converters to BGRA8888 but none for DXT5:
BlpDecodeLevel would return 1 with its SMemAlloc buffer unwritten. No
shipped cursor is DXT5; the check keeps a later embedded DXT5 file, or a
DXT5 map overlay, from being read as heap memory.
Game paths now compare through one rule, Common::SamePath (any case,
either slash), and the embedded manifests through Embed::FindFile. The
embedded addons, the bindings splice and sound muting each had their own
copy of the rule; they and Texture::Builtin now share it.
The option's header, label and tooltip are translated for every client
locale, in Blizzard's own terms for each (NSC, PNJ, 物体, 物件, ...).
Release Win32 build is clean. In game, only the soft-interact quest
icon was checked so far.
* feat(texture): add static creature portraits by display ID
* docs(api): document static creature portrait functions
* Update API.md
* refactor(model): bake display-ID portraits on the engine's own path
SetPortraitTextureFromCreatureDisplayID now replays 1.12.1's unit
portrait bake, BakePortrait (0x00524F60), call for call on a display
model of its own, instead of driving a hidden PlayerModel through Lua.
The display model is built the way AttachDisplayModel (0x00505A70)
builds one, without a frame: M2CreateScene (0x00706E10), then
CreateModelFromPath (0x00707350) on the CreatureModelData path, then
ApplyCreatureDisplay (0x004797B0). A character display is dressed
through the engine compositor with the same code that dresses
PlayerModel:SetDisplayInfo, now shared as Model::DisplayInfo::
CreateDresser / PumpDresser / DestroyDresser.
The bake's own gate, IsReady(model, 0, 1) (0x00710450), only passes for
a model that something has drawn: a BLP texture gets its device texture
only when GetTextureDeviceObject (0x0044ACF0) is asked with wait set.
The shared M2 still loads on its own, so the gate here is
IsLoaded(model, 0, 1) (0x007103D0) plus the dress, and the bake's
IsReady(copy, 1, 1) makes the device textures, as it does for the copy
of a unit's model.
The bake itself is the engine's sequence: copy into a fresh scene,
DetachAllItemModels, camera 0, PlayAnimation, save projection, view,
viewport and fog (GxMasterEnable 0x0058A210 / 0x0058A1E0), the 64x64
DDC box in the bake's own float order, CameraSetDeviceTransforms, clear
colour 0, ClearDevice(3), PortraitLighting, both scene passes, read back
through 0x00589CF0, clear, cut the alpha mask in (0x005258A0), restore.
It runs at the head of the UI render callback, the same point as an
OnUpdate script that calls SetPortraitTexture, behind the bake's device
gate (0x0058A260 window active, 0x0058A280 device alive).
Each display ID owns one callback texture (TextureCreateWithCallback,
which makes its device texture at creation). It is bound with SetImage
(0x00770300) at call time and filled in place with GxTexMarkRectDirty
(0x0058ACD0), the bake's own path for re-baking a cached portrait. A
region the caller re-points before the bake finishes keeps what it was
given, so no region is tracked: the request map, the region-destruction
subscription and ClassicAPI_CancelPortraitTextureRequest are gone. The
function returns nothing, as retail's does.
Portraits are kept until logout, the lifetime of the engine's display-ID
portrait cache (cleared in ShutdownGame). A display with no portrait
camera is done once and stays transparent.
Offsets.h: the portrait block is renamed to engine names with calling
conventions and sources; the duplicates of FUN_M2SCENE_ANIMATE,
FUN_M2MODEL_SET_ANIMATED and GetCameraById are removed, as are the
made-up draw-flag and scene-count fields and the raw device-vtable
slots. Model::Camera::CameraById is exposed instead of being copied.
---------
Co-authored-by: z1xel <ramirezthiago@71gmail.com>
Co-authored-by: Brues <5278969+brues-code@users.noreply.github.com>
The Lua row gains debuglocals, debugstack's coroutine argument,
difftime, gmatch, math.cosh/sinh/tanh, newproxy, scrub, string.byte and
strbyte ranges, string.format's two-digit %n$, %0 in string.gsub and
table.removemulti. The Lua errors section says the window shows each
error's local variables.
The Interact Key page gets rows for Interact With Target and Interact
With Mouseover, and Bags and Items gets AoE Loot under a Loot header.
A row is retail's settings-page binding row: the binding's name, then a
button for each of its two keys. Left click a button and press a key,
mouse button 3-5 or the wheel to bind it; Escape cancels; right click
unbinds that key. The tooltip on a bound key and the page's output line
use retail's strings, added to every locale with Blizzard's translations.
The binding is 1.12's Key Bindings window's (Blizzard_BindingUI): the
same ALT-CTRL-SHIFT chord order, the same skipped modifier keys, the same
KEY_UNBOUND_ERROR check for an action that loses its last key, and the
same put-back when SetBinding refuses a key. A change saves at once with
SaveBindings(GetCurrentBindingSet()), so it lands in the account or
character set in use, as every other control on these pages writes at
once.
While a button listens, a full-screen button on FULLSCREEN_DIALOG takes
keys, clicks and the wheel, as retail's KeybindListener and its input
blocker do. A left or right click on it cancels, and so does hiding the
page.
C_ActionBar.FindSpellActionButtons, HasSpellActionButtons and
IsOnBarOrSpecialBar find a spell on the bars. A slot matches on the
spell ID stored in VAR_ACTION_TABLE under the spell tag, the same test
4.3.4's PutActionInSlot makes, so a macro or item that casts the spell
does not count and a rank matches only itself. A name resolves through
the engine's spellbook resolver. IsOnBarOrSpecialBar also reads the
shapeshift bar's spell array (pointer at 0x00B71108, count at
0x00B71104, as Script_GetShapeshiftFormInfo 0x004B45C0 reads them) and
the pet bar's spell entries (types 1-5, spell ID in the low 16 bits, as
Script_GetPetActionInfo 0x004BDC50 reads them, gated on the pet GUID).
Enum.ActionBarSet is registered; all 120 slots are its Mkb set.
IsStackableAction (global, 1/nil) and C_ActionBar.IsStackableAction
(bool) answer 4.3.4's IsStackableAucItem: the slot's item has a max stack
above 1. C_ActionBar.GetItemActionOnEquipSpellID returns the item's
first spell with trigger 1 (on equip). The item is the item-by-ID
action's, or the one a macro's #showtooltip / #show resolved to, as
4.3.4's CGActionBar::GetItem returns it. Both peek the item cache and
never query it.
C_ActionBar.IsMacroActionWithShowTooltip parses the macro's body with
the parse Macro::ShowTooltip hands the tooltip over on, so it answers
while that module yields or a publisher owns the macro.
ACTION_RANGE_CHECK_UPDATE(slot, isInRange, checksRange) and
C_ActionBar.EnableActionRangeCheck(slot, enable). Enabled slots are
checked on each world tick while a frame is registered for the event,
with IsActionInRange's own answer (Action::Range::Answer, the macro
unit redirect included), and fire on change. Enabling is counted per
slot, and each enable makes the slot's next check fire, because retail
buttons take their first state from the event. A new Lua state starts
with no slot enabled.
ACTION_USABLE_CHANGED(changes) carries { slot, usable, noMana } for
each slot that changed. The engine's bulk refresh 0x004E5C00 already
compares each slot's pair against the caches IsUsableAction reads
(0x00BC6B60 / 0x00BC67A0). Its per-slot call at 0x004E5C15 is
redirected to record the slots whose pair differs, and its
ACTIONBAR_UPDATE_USABLE signal at 0x004E5C53 to send that event
unchanged and then the new one. No hook.
bar:SetValue(value, interpolation) and SetMinMaxValues(min, max,
interpolation) take Enum.StatusBarInterpolation. With
ExponentialEaseOut the fill eases from what it showed to the new value;
GetInterpolatedValue, IsInterpolating and SetToTargetValue read and end
the ease, and SetTimerDuration's interpolation argument now chases the
timer the same way. The curve was measured in a retail 12.1.5 client
at 77 and 30 frames per second and fits to under 1%: each frame closes
the gap in the fill FRACTION by exp(-18 * elapsed), whatever the range,
and the frame whose gap before its step is under 0.001 snaps to the
target. The same probe on 1.12 matches: k = 18.0, a full-bar ease done
in 0.38 s against retail's 0.39.
SetValue stores the target at once (GetValue returns it, OnValueChanged
runs once, with it); a new eased value goes on from the shown fill, and
an Immediate one, or SetToTargetValue, snaps. SetMinMaxValues with the
ease keeps the fraction shown and eases to the new one. A hidden bar
does not ease. Only a target that held still since the last frame may
snap: an eased timer starts at a near-zero gap, and retail chases it
until the timer ends.
The ease lives where the timer does, so StatusBar::Timer becomes
StatusBar::Display: one state per bar holding the timer, the ease and
the fraction drawn. The co-hook on the bar's GetFillFraction virtual
(0x783380) answers that fraction, so the ease keeps 1.12's texture crop
and StatusBar::Fill's styles. Advance runs at the head of
CSimpleStatusBar::OnFrameUpdate (0x7833C0) for shown bars and now takes
the pass's elapsed, which is the step retail takes.
SetValue and SetMinMaxValues stay the engine's Lua methods (0x78F690,
0x78F4B0, read from the method table at 0x87B010), registered over by
wrappers that read the interpolation argument and the fraction shown,
call the engine method for its checks, errors and setter, then start or
stop the ease. Both engine methods read no argument past their own.
/etrace and /eventtrace now open a backport of retail's
Blizzard_EventTrace, the load-on-demand addon ClassicAPI_EventTrace
embedded in the DLL. It replaces the old event trace frame in
ClassicAPI_DebugTools, and DebugToolsSavedVars with it; the panel saves
EventTraceSavedVars.
What differs on 1.12.1:
- RegisterAllEvents adds the frame to every event slot, so it opens the
listener gate of each ClassicAPI event that builds its arguments only
for a listener (UNIT_AURA_DELTA, the combat log and cast events). The
panel registers all events only while it is shown or logging while
hidden; otherwise it keeps PLAYER_LOGOUT, which saves its variables,
since this client has no ADDONS_UNLOADING.
- An engine event signalled by a number with no name arrives with a nil
event and is not logged.
- An event's table arguments are copied when logged, and the tooltip
lists their contents. UNIT_AURA_DELTA refills the same tables on every
fire, so a kept reference shows only the latest delta.
- No tool windows, ImGui debug tools or secret values.
The window's templates are new shared templates in !!!ClassicAPI:
ButtonFrameTemplate and PortraitFrameBaseTemplate (with
PortraitFrameMixin / TitledPanelMixin), UIPanelCloseButtonNoScripts,
PanelResizeButtonTemplate, InputBoxInstructionsTemplate,
SharedTooltipTemplate, VisibleWhilePlayingAnimGroupTemplate, and
GameTooltip_AddHighlightLine / GameTooltip_AddColoredDoubleLine. Art
this client lacks is drawn with its own textures: tooltip border
chrome, the world map magnifier, the cancel button, a turned chat
expand arrow for the resize grip, and the panel button in three slices.
The scroll bars are the 1.12 bar, whose art is wider than its frame,
so the lists leave room for it.
Retail's search box is ClassicAPISearchBoxTemplate. Turtle's FrameXML
registers its own SearchBoxTemplate first, and 1.12 keeps the first
template of a name, so the retail one takes that name only through a
TOC line marked [ExcludeLoadGameType turtle].
The EVENTTRACE_* strings and SLASH_EVENTTRACE1-4 come from Classic
Era's GlobalStrings for each client locale but koKR.
For a region with no set size, 1.12's Script_GetWidth (0x007A1E00) and
Script_GetHeight (0x007A2030) return the rect span without dividing by
the region's layout scale. The value is the size times the effective
scale: a 160-high track at a UI scale of 0.91 answers 145.6.
dceec8b replaced both getters with the divided form. That broke vanilla
addons tuned to the scaled value. BetterCharacterPanel sets its paperdoll
column from PaperDollFrame:GetHeight() with padding tuned at one UI
scale, so the resist icons and the stat list left the frame.
- GetWidth / GetHeight / GetSize call the engine getters again. With
ignoreRect, a region with no set size still answers 0.
- New region methods GetUnscaledWidth / GetUnscaledHeight /
GetUnscaledSize give the size in GetTop / GetBottom units: the explicit
size, else the rect span over OFF_REGION_EFFECTIVE_SCALE after a
pending resize. GetRect and GetScaledRect keep using these.
- The ported Lua in !!!ClassicAPI (ScrollBox, Menu, LayoutFrame, Util)
calls the GetUnscaled* names wherever it measures a region.
Fixes#118
The Texture row lists the SetTexture wrap modes and the tile methods. The
XML attributes row now names the element attributes that ClassicAPI reads,
for frames, buttons, textures, font strings, status bars and edit boxes,
and intrinsic frame types.
Adds the three call stack heights an error handler uses to start its
stack at the code that failed:
debugstack(GetCallstackHeight() - (GetErrorCallstackHeight() - 1))
A height counts levels as lua_getstack (0x006FBAA0) does, and so as
debugstack does: C functions and lost tail calls count, and level 1 is
the caller.
1.12.1 records no error height, but every error report runs one C
function, FrameScriptCallErrorHandler (0x00703B40, __fastcall(L)). It
pushes the seterrorhandler function (registry ref 0x008722CC), moves it
under the message and lua_calls it. Nothing calls it directly: state
open pushes it as a C closure and refs it (0x008722C8). Every script
pcall passes that closure as the errfunc, so Lua calls it at the point
of the error before the stack unwinds; a chunk that fails to compile is
reported by a pcall of it (0x00704AE0). ClassicAPI's securecall,
hooksecurefunc and script runners pass the same ref.
So while any handler runs, that closure's level sits directly above the
code that failed. GetErrorCallstackHeight walks the levels with
lua_getstack and lua_getinfo "f", finds the innermost one whose function
is 0x00703B40 (lua_tocfunction, 0x006F3720) and answers the height below
it, or nil when no error is being reported. There is no hook, and
nothing is recorded, so a handler that fails part way leaves nothing
behind.
SetErrorCallstackHeight sets the height for a handler called with no
error raised. It applies only while no 0x00703B40 level is on the stack,
so a value left set by a failed handler cannot move a later error's
stack. A new Lua state starts with no value set.
Users:
- The Lua error window takes its stack this way, so it starts at the
failing code even when another addon wraps the handler.
- CallErrorHandler and assertsafe set the height around their direct
handler call, so they report from their caller.
- Game::Lua gains GetStack, GetInfo and ToCFunction; SourceLocation uses
them in place of its own copies.
Docs: the xpcall and coroutine notes said error() returns without
raising. Stock error (0x00702970) ends in lua_error (0x006F4440) and
raises. pfUI replaces the global error with one that prints and
returns, which is what those notes described.
Tested in game: with seterrorhandler wrapping the old handler, a nil
index in a local function f showed a stack that starts at f.
The Lua error window (/luaerrors) moves from the load-on-demand
ClassicAPI_DebugTools into !!!ClassicAPI and becomes the client's error
display. Each error opens it with the message, time, count and stack, and
the arrows page through every error so far.
1.12.1's error handler is FrameXML's _ERRORMESSAGE (BasicControls.xml). It
reports an error as ScriptErrors_Message:SetText(message) and then
ScriptErrors:Show(), and only while the stock box is hidden. The window's
OnLoad takes those two slots, the same two !BugGrabber 2.1.1 takes: SetText
records the error and Show does nothing, so the stock box stays hidden and
every error arrives. Errors still pass through the handler first, so:
- !BugGrabber loads after !!!ClassicAPI and takes the slots back. BugSack
gets the errors and the window does not open.
- An addon that calls seterrorhandler (pfUI) replaces _ERRORMESSAGE, so
neither shows. One that wraps the old handler still ends here.
The engine's error function (0x00703B40) is C. It runs at the point of the
error, before the stack unwinds, and calls the registered handler. From the
SetText slot, level 1 is the slot, 2 is _ERRORMESSAGE, 3 is that C function
and 4 is the code that failed, so the stack starts at debugstack(4), the
level !BugGrabber's GrabError reads from the same slot.
The text is ClassicAPI's ScrollingEditBoxTemplate with a MinimalScrollBar.
The bottom row has Reload UI, arrows that wrap around the index label, and
Close. /luaerrors opens or closes the window and /luaerrors clear empties
it; the old on/off handler install is gone.
Not tested in game yet.
Fires (no payload) each time the game applies the interface scale to
UIParent.
In 1.12 every scale change goes through one function, SetUiParentScale
(0x00494550, void __stdcall(float), ret 4). It raises the value to 0.64,
calls SetScale with force (0x0076AC10) and drains the layout queue
(0x00768ED0), and does nothing while UIParent (0x00B4B44C) is null. It
has six callers:
- the uiScale cvar callback (0x00490770, two sites)
- the useUiScale cvar callback (0x004907F0, two sites)
- OnDisplaySizeChanged (0x00492E90) when useUiScale is on
- the automatic scale ApplyAutoUiScale (0x00492F70)
One MinHook on it fires the custom event after the original returns,
only while UIParent is set. The game UI's Initialize stores UIParent
(0x00490077) and its Shutdown clears it (0x00490CA4), so the event never
fires on the glue screen. Each path calls the setter once, so one change
fires one event; a resize fires it before DISPLAY_SIZE_CHANGED.
Tested in game: useUiScale on, uiScale 0.8 and useUiScale off each fired
one event with the new scale (0.91, 0.8, the automatic 0.9).
Adds C_AutoComplete.GetAutoCompleteResults, C_AutoComplete.IsRecognizedName,
Enum.AutoCompleteEntryFlag and Enum.AutoCompletePriority, and the cvars
autoCompleteResortNamesOnRecency, autoCompleteWhenEditingFromCenter and
autoCompleteUseContext. ClassicAPI only answers the queries; an addon draws
the list of names.
1.12.1 keeps no autocomplete list, but it keeps every list the names come
from, so each query reads them where the client keeps them:
- party: the four GUIDs at VAR_PARTY_GUIDS (0x00BC6F48), names from the
name cache, online from the member stats block
- raid: the VAR_RAID_MEMBER_PTRS slots (0x00B712A8) and their online bit
- guild: the GuildMemberInfo roster at 0x00B72704, GUID at +0x00 and the
online word at +0x48 (stored at 0x004D0C22, tested by GetGuildRosterInfo
at 0x004D13C5). Read only while the player's guild key is set, the test
IsInGuild (0x00516DE0) makes, because the client keeps the old roster
after a /gquit
- friends: the 50 slots of the social singleton
Whisper partners are the one list the client does not keep. The chat
dispatch hook (FUN_0049A870) reports each whisper and whisper inform
(types 6 and 7, not the addon language -1) with the sender GUID it already
resolves. Logging out clears the list.
Names match case-insensitively by UTF-8 code point; Text::Utf8::CompareI
now takes a character limit. Results come most recent whisper first, then
by name. A result's priority is its closest tie: friend, guild, group,
whisper.
GetAutoCompletePresenceID and GetAutoCompleteRealms are constant answers in
Compat.lua. The three cvars are on a new Social page of the options panel.
Adds C_SpellActivationOverlay.IsSpellOverlayed (and the global
IsSpellOverlayed), Enum.ScreenLocationType, and the four
SPELL_ACTIVATION_OVERLAY_* events. Nothing is drawn: an action-bar or
UI addon draws the alerts and the glows from these.
Glows come from the engine's own reactive-spell list. AddSpellToBook
puts every learned spell whose record has AttributesEx4 0x200 on the
list at 0x00B71150 (count 0x00B7114C), one 8-byte {spellID, usable}
entry per name. FireUsableEvents (0x004B31C0) retests the list with
the engine's usable check, stores each answer, then signals
SPELL_UPDATE_USABLE (0x107). The module subscribes to that signal
through Event::SignalHook and reads the stored answers, so the verdict
is the engine's (rage, target health, aura states, and the hidden
combo point behind Overpower) and no new hook is needed. A usable entry
glows every spellbook spell with the same name, which is the list's own
identity rule, so a bar slot with any rank glows. On stock 1.12.1 this
is Execute, Overpower, Revenge, Riposte, Counterattack, Mongoose Bite
and Hammer of Wrath; Turtle's Spell.dbc changes the set by itself.
Alerts come from two Blizzard rows whose aura exists unchanged in 1.12:
Shadow Trance (Nightfall art, glows Shadow Bolt through its class mask
via Spell::Lookup::IsFitToFamily) and druid Clearcasting
(Natures_Grace art). An Aura::Source observer reconciles on the
player's aura add/remove, rescanning the descriptor so counts can't
drift. The texture is a path; 1.12 has no file data IDs.
On world entry, HIDE fires with no spell ID and every active alert
fires SHOW again, so a /reload redraws them. Glows already lit before a
/reload do not re-fire; a button reads IsSpellOverlayed when it draws.
Logging out clears the state without events.
Event::EnteringWorld gains an observer list, because it claims
PLAYER_ENTERING_WORLD in the signal chain and later subscribers never
see it. Event::Custom's reservation cap goes from 96 to 128: the four
new events filled the old cap exactly, and a name past the cap is
dropped silently.
pfDebug was a separate addon that profiled through ClassicAPI's own
APIs: GetAddOnMemoryUsage and GetAddOnCPUUsage, GetFrameCPUUsage,
GetFunctionCPUUsage, GetEventCPUUsage, and the intrinsic precall and
postcall script hooks. Its code now lives in ClassicAPI_DebugTools
under Profiler/, with its MIT notice (Copyright (c) 2018 Shagu). The
analyzer tabs (Addons, Frames, Events, Globals) and the widget are
pfDebug's, with the frame and template names renamed to
ClassicAPIProfiler*.
/profiler and /prof load DebugTools on demand through the same stub as
/dump, then toggle the analyzer. The subcommands widget, reset and
profile on|off are pfDebug's.
What changed in the move:
- The sampler starts only when the widget or the analyzer first opens,
so a load for /dump costs nothing. It first takes every counter as it
is, so the first CPU/s and KB/s cover one second, not the session.
- Whether profiling runs is probed, not read from the cvar: a function
is called once and GetFunctionCPUUsage must count that call. The
scriptProfile cvar can already hold the next session's value.
- A frame record starts at the frame's current CPU time and call count,
and opening the analyzer takes every frame and event counter again,
because the frame totals and the deep hooks keep counting while it is
closed. pfDebug started records at 0, so the first scan and each
reopen showed a frame's whole session as one second.
- The /fstack lines (owner, OnUpdate and OnEvent owner and CPU, all
scripts, deep allocations) are added at the end of
FrameStackTooltip_OnTooltipSetFrameStack, not by hooking
OnTooltipSetFrameStack.
- No pfUI skin and no SavedVariables. The plain backdrop is pfDebug's
own fallback. The widget no longer opens at login, because that would
load DebugTools at login.
README's Debug tools table gains /profiler and the new /fstack lines.
On a stock 1.12.1 client, ScrollBoxListView, ScrollUtil, Menu and the
DebugTools table inspector failed with "attempt to call global `wipe'":
those ports call bare wipe(), and only Turtle's client defines that
global. Stock 1.12.1 has only the table.wipe that ClassicAPI registers.
3.3.5 makes the alias in FrameScript's compat preamble, next to tinsert
and tremove (wipe = tab.wipe). 1.12's preamble is older than table.wipe,
so Wipe.cpp now registers the same C function as the global wipe as well
as table.wipe, with a LuaGlobals descriptor.
CallbackRegistry, MixinUtil and SecureTypes lose their "1.12 has no
global wipe" notes and their table.wipe locals, and now match retail.
UnitFacing(unit) returns the unit's facing in radians (0 = north,
counter-clockwise, 0..2pi), or nothing when the token has no unit.
The facing comes from the object's GetFacing virtual, vtable slot 6.
The unit and player vtables (0x80C4F8 / 0x80AF78) both hold
CGUnit_C::GetFacing (0x5F1F30) there, read from the binary: it loads the
mover at +0x118, pushes its facing +0x1C and returns
CMovement::TransportFacingToWorld (0x7C4AE0), which adds the transport's
facing and wraps once into [0, 2pi]. Unit::Position::Facing wraps the
call next to the shared position read.
GetPlayerFacing moves to the same read. It used to load the float at
+0x9C4 (the mover's +0x1C) directly, which is the facing relative to the
transport, so on a boat or zeppelin it returned the wrong angle. The
OFF_UNIT_FACING offset goes with it.
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.
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+.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
ScrollingMessageFrame:RemoveMessagesByPredicate(predicate), retail's: the
predicate is called with each message's GetMessageInfo values, oldest
first, and the messages it returns true for are removed; the rest keep
their order. It covers 4.3.4's RemoveMessagesByAccessID / ByExtraData,
whose int fields the retail-shaped records here do not have.
1.12's ring has no compaction of its own; the removal is 4.3.4's
RemoveLines loop over it: every kept record moves forward in ring order to
the first free slot (freeing the text it lands on, nulling the one it
left), the position moves back by the removed count, and the
extra-argument tables move with their records. Then the engine's own
layout (0x00788750) from the record at the kept scroll offset, clamped to
the oldest, and its re-arm (0x00788B80) - a layout at the bottom keeps the
fade state, so 4.3.4 re-arms after - or its Clear (0x007882B0) when
nothing is kept.
The predicate runs for every message before anything moves, through the
engine's error handler; a predicate that errors keeps that message, as
retail's safesecurecall does. GetMessageInfo shares its record lookup and
value push with it.
ScrollingMessageFrame:BackFillMessage(text, r, g, b, ...), retail's: the
message becomes the oldest, for rebuilding a frame from history newest to
oldest (Blizzard_CombatLog's refilter adds each entry with it).
The ring's writer 0x00788150 has one slot rule: the one after the newest.
BackFillMessage writes the slot 4.3.4's AddMessage(..., addToTop) picks —
the free one before the oldest, (pos - count + 1) mod max once the count
has grown — and fills the record as the writer does: a Storm copy of the
text, AddMessage's colour (clamped, x255 + 0.5 truncated, alpha 0xFF; the
font's without r, g, b), shown, the frame's two times, id 0, and the extra
arguments kept for GetMessageInfo. Nothing already in the ring moves, so a
scrolled-up frame keeps its view; at the bottom the lines are laid out
again from the newest, as the writer does.
A frame at its line limit drops the message, as retail's CircularBuffer
PushBack does ("Won't overwrite front"); 4.3.4 overwrites the oldest slot
there, which Blizzard_CombatLog never reaches.
SetInsertMode(SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP) now shows the
newest message at the top, older ones below it, as retail's mixin does.
1.12's line layout anchors each line once, when it makes it, through one
SetPoint call (0x00788975). That call is redirected: lines of a TOP frame
take the TOP anchors, every other line keeps the engine's own arguments.
The layout remakes the lines when the frame resizes, so the redirect
covers that too. A mode change re-anchors the existing lines with the
layout's own steps and refreshes the display.
Insert mode moves out of Scroll.cpp into InsertMode.cpp.
Retail's ScrollingMessageFrame SetScrollOffset, GetScrollOffset,
GetMaxScrollRange, ScrollByAmount, SetInsertMode / GetInsertMode with the
SCROLLING_MESSAGE_FRAME_INSERT_MODE_* globals, and
AddOnDisplayRefreshedCallback, which ScrollUtil's
InitScrollingMessageFrameWithScrollBar calls.
The offset is the engine's own: SetScrollFromBottom's body (0x00788700)
is 4.3.4's SetScrollOffset step for step, and GetCurrentScroll is
GetScrollOffset under retail's name. The setters clamp to
0..GetNumMessages()-1 first, as retail does; the engine wraps. 1.12's
layout stacks lines bottom-up only, so the insert mode is BOTTOM and
SetInsertMode raises an error for anything else.
The callbacks run after the frame's own update (0x00788460, OnUpdate then
the fader), where retail's OnPostUpdate runs them, for a frame the line
layout, AddMessage or ScrollByAmount marked. The layout hook moves out of
Text::InlineTexture into ScrollingMessageFrame::Layout, which owns it and
fans out to both.
C_DeathInfo.GetSelfResurrectOptions, C_DeathInfo.UseSelfResurrectOption
and Enum.SelfResurrectOptionType over what 1.12 already serves through
HasSoulstone and UseSoulstone. Both pick one option the same way:
PLAYER_SELF_RES_SPELL when the server set it, else the first carried
item whose on-use spell is a self-resurrect. The list holds that option
under HasSoulstone's alive gate, and using it calls the engine's
UseSoulstone. Nothing in 1.12 limits a self-resurrection, so canUse is
true, isLimited false and priority 0.
SELF_RES_SPELL_CHANGED is 4.3.4's watcher on PLAYER_SELF_RES_SPELL from
SetActiveMirrorHandlers. 1.12 watches no such field, so the watcher joins
CGPlayer_C::AddActiveWatchers and leaves before its removal, the lifetime
of the engine's own player watchers.
4.3.4's CGIncomingResurrection. SMSG_SPELL_START adds a resurrect cast
under its object target, a unit or a corpse. SMSG_SPELL_GO, the failure
packets and ClearCastingSpell (through Spell::Cast, Turtle's only
interrupt signal) end it. A cast whose caster leaves view never reports
its end, so it ends on the next world tick. Each change fires the event
for every token naming the target, or the corpse's owner. 1.12's class
resurrections carry effect 113, not 4.3.4's 18, so both count.
Object::Corpse::ByOwner, lifted from spell/Range.cpp, now skips bones as
4.3.4's ClntObjMgrFindCorpseByOwner does. Turtle's bones keep their
owner, so old bones hid a ghost's live corpse, from this API and from
the range check's ghost redirect.
HasEffect moves from CombatLog to Spell::Lookup, and the SpellCastTargets
object mask 0x8A02 becomes one Offsets.h constant.
QueryQuestsCompleted, GetQuestsCompleted, C_QuestLog.IsQuestFlaggedCompleted,
C_QuestLog.GetAllCompletedQuestIDs and QUEST_QUERY_COMPLETE, on Turtle clients
only. The server answers `.queststatuses` with TWQUEST guild addon messages
from the player, split at 2000 characters; the shorter last message is the
end of the list, so no timers are needed.
The DLL sends the request itself on SMSG_LOGIN_VERIFY_WORLD (the earliest
request the server answers), after each turn-in, and from
QueryQuestsCompleted. It reads every answer at the packet funnel, checks that
the sender is the player, replaces the list and fires QUEST_QUERY_COMPLETE.
A turned-in quest goes into the list at once; the server's next answer takes
repeatable quests out again. The list lives through /reload.
Net::OutPacket replaces the two hand-built CDataStore senders (item cache
resend, SetPVP). Replaces the closed Lua collector of PR #86.
Port of 4.3.4's bag search (CGContainerInfo): C_Container.SetItemSearch,
C_Container.IsContainerFiltered and INVENTORY_SEARCH_UPDATE. An item
matches on its equipment sets' names, its suffixed name, its subclass
short name or its equip slot name, case- and accent-folded per locale.
The keyring is searched; non-ASCII search text is folded whole.
C_Container.GetContainerItemInfo now reports isFiltered and takes an
optional table to fill and return, so bag addons can read every slot
without a new table per call. Item::Location::ResolveBagArgs resolves
the caller's own bag/slot args without wiping the stack.
Both mirror 3.3.5 (0x005DBE10, 0x00596290).
GetTradeSkillDescription: nil unless the row's recipe has a description
in the client's locale, then 1.12's spell-text parser with the arguments
GetCraftDescription passes. That call moves to Spell::Description::Expand,
shared with C_Spell.GetSpellDescription.
GetTrainerServiceItemLink: a 1.12 server sends the trainer spell that
teaches the recipe, not the recipe. Follow its first LEARN_SPELL or
LEARN_PET_SPELL effect as 1.12's GetTrainerServiceIcon (0x004D8F50) does,
then 3.3.5's gate: tradeskill trainer, effect 0 CREATE_ITEM, item cached.
3.3.5's other create-item effects are dropped: 1.12's 59 is
OPEN_LOCK_ITEM and 157 does not exist.
3.3.5 pushes m_gossipOptionsCount; 1.12 keeps no count. Count the slots
1.12's Script_GetGossipOptions (0x4E28D0) pushes: every slot with a
non-negative index, no session check, so the count matches the list.
3.3.5 and 4.3.4 call CGReputationInfo::CollapseHeader(m_numFactions,
collapse); an index at the entry count sets every header. 1.12's
CollapseHeader (0x4D6400) has the same branch, so both make that call
with 1.12's entry count.
IsChatAFK reads 1.12's chat AFK mirror (0xB6E5CC), 4.3.4's CGChat::m_afk,
with the same writers: SetAfk, ClearAfk, the PLAYER_FLAGS change handler
and world entry. 1.12 keeps no DND mirror; its /dnd branch tests the
PLAYER_FLAGS DND bit, so IsChatDND reads that bit.