mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
master
817
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d7103de564 |
feat(spell): count set bonuses in GetSpellBonusHealing
Set-bonus spells are applied server-side as auras the client never sees as buffs or known spells, so the derived healing sum missed them — Lawbringer (3) is "Increase Healing 31" (9317), Judgement (3) is 57 (18037). Add a pass that counts the equipped members of each set from the item record's set id (OFF_ITEMSTATS_ITEM_SET), reads ItemSet.dbc's bonus spells and member thresholds, and runs every met bonus through the same aura decoder — the rule the item tooltip's set-bonus lines use at 0x52DFB9. The Ironclad (aura 199) armor conversion stays on UNIT_FIELD_RESISTANCES[0]: the server's SpellBaseHealingBonusDone uses GetArmor(), the full total, even though the talent text says "Armor from items". Refs #135 |
||
|
|
b65c9e0319 |
fix(spell): never auto-unshift out of Spirit of Redemption
3.3.5's and 4.4.2's SpellShapeshiftForm.dbc give Spirit of Redemption (form 32) flags 0x4100, which include SHAPESHIFT_FORM_FLAG_NO_AUTO_UNSHIFT (0x100). So the later clients' CGUnit_C::CanAutoUnshift refuses it, and no cast, interaction or item use cancels it. 1.12's row has flags 0, so with autoUnshift on, our CanAutoUnshift allowed it. A priest in Spirit of Redemption who cast a NOT_SHAPESHIFT spell, right-clicked an NPC or object, used an item or opened the flight map sent CMSG_CANCEL_AURA for it. The later-client flags that 1.12's rows lack now sit in one table: CAN_INTERACT on the druid forms and Ghost Wolf (read only while autoUnshift is on, as before), and NO_AUTO_UNSHIFT on form 32, which CanAutoUnshift reads unconditionally, as 4.3.4 does. With it, every autoUnshift site leaves Spirit of Redemption on and keeps 1.12's own refusal. The comment on FormCanInteract also records why a locked object still unshifts. It opens by a spell (Opening 6478 for chests and barrels), and the server's CheckCast form test refuses that spell on 1.12's spell data, which keeps SPELL_ATTR_NOT_SHAPESHIFT (3.3.5 dropped it). |
||
|
|
de033896b1 |
feat(spell): let autoUnshift talk and use objects in the druid forms
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. |
||
|
|
dc8d4caf12 |
fix(loot): skip corpses that stay lootable after LootAllCorpses (#133)
* fix(loot): skip corpses that stay lootable after LootAllCorpses A corpse whose remaining loot this player can never take keeps UNIT_DYNFLAG_LOOTABLE set: quest items for quests they don't have, condition-gated loot, and the like. The per-corpse re-check still passes, so a caller that runs C_Loot.LootAllCorpses from a timer re-opens the same corpse on every walk and re-sends the refused CMSG_LOOT_ITEM, which never clears. Loot mode now remembers every corpse it opened and released. Later walks skip a remembered corpse while it is still visible and lootable; the entry is dropped once the corpse stops being lootable or leaves the object manager. A corpse that merely left render range is dropped too, so it gets one retry when the player comes back. ScanNearbyLoot stays read-only and keeps reporting the corpse. 1.12 has no per-slot "cannot loot" flag (GetLootSlotInfo has no `locked` return), so one refused request per corpse is unavoidable; this only stops it from repeating on every walk. * docs(loot): describe the LootAllCorpses corpse skip Explain why a corpse that stays lootable after the walk is skipped by later walks, and when the skip entry is dropped. * fix(loot): skip only corpses that open empty in LootAllCorpses The previous change remembered every corpse the loot walk opened. The most common reason a corpse stays UNIT_DYNFLAG_LOOTABLE is full bags, so those corpses were never looted again by the walk, even after the player freed bag space. Rolls and master-loot assignments in progress had the same problem. The server never sends a player a loot slot that player cannot take: another member's quest item and condition-gated loot are left out of SMSG_LOOT_RESPONSE, while the corpse stays lootable for as long as any loot remains. So the stuck corpse opens to an EMPTY window, with no coin and no populated VAR_LOOT_SLOTS entry. Loot mode now remembers a corpse only when the scraped window is empty. A window with anything in it (full bags, a roll, a master-loot slot) is retried. An entry is forgotten: - on the first world tick where its unit is gone or no longer lootable. This replaces the prune at walk start, which missed a corpse that left view and came back, and a creature that respawned with the same GUID and died again, between two walks; - all at once on QUEST_LOG_UPDATE (event 0x134, observed through the shared FUN_FIRE_EVENT_NO_ARGS interceptor and never swallowed), because quest items are per-player loot and a quest change can put an item into a window that was empty. The memory moves out of the per-walk ScanCtx into its own vector. The API reference now describes the skip as a contract only. --------- Co-authored-by: Brues <5278969+brues-code@users.noreply.github.com> |
||
|
|
20069f3815 |
docs(api): sort the reference's sections alphabetically
Seven sections were out of place in the body: ChatBubbles (after Mount), Bindings (after Focus), APIDocumentation, CVar, MerchantFrame, NamePlate and PvP. The contents list shared the last four. Both are now in the same case-insensitive order, with one blank line between contents groups (DeathInfo and Sound had none). The ClassicAPI namespace section had no contents entry; it has one now. The PVP_TIMER_UPDATE entry moves from the PvP section to Events, between PLAYER_SWING_RANGE_UPDATE and QUEST_ACCEPTED. It now names the PvP timer and links the two timer functions, and the PvP section links to it. |
||
|
|
f2fa112f0a |
feat(mount): add C_MountJournal.GetMountFromSpell and GetMountInfoExtraByID
1.12.1 has no mount table, so a mount's ID is the ID of the spell that summons it: GetMountFromSpell(458) returns 458. A spell is a mount when one of its effects applies SPELL_AURA_MOUNTED (78); Spell.dbc is read directly, so mounts the player has not learned resolve too. On Turtle every mount item carries spell 46499 "Add Mount to Collection" instead of its mount spell (the server maps item to spell from its own table), so only the mount spell gives a mount ID. GetMountInfoExtraByID returns the nine retail values. The display ID is the one the server mounts the player on: the mount aura's misc value is a creature entry, and the client's creature cache holds that creature's display (SMSG_CREATURE_QUERY_RESPONSE carries the template's first display, and every mount creature has exactly one). An uncached creature returns nil and starts the cache load through the new Creature::Info::RequestLoad, which C_CreatureInfo.RequestLoadCreatureByID now uses too; CREATURE_DATA_LOAD_RESULT fires when the data lands. Verified in game: 23219 (Swift Mistsaber) returns 14332. Mount type and model scene default to 230 and 4. Seven mounts take retail's values instead (the six Qiraji battle tanks use scene 44, the Riding Turtle type 231), each checked by name against Turtle's Spell.dbc. The seat animation is 91 (Mount), the animation 1.12.1's world mount plays on the rider. |
||
|
|
a106d86969 |
fix(unit): consult the name cache only when no unit object is held
UnitClassBase and UnitRaceBase now follow stock Script_UnitClass (0x00518350) and Script_UnitRace (0x00518200) branch for branch. "player" reads the selected-character record; any other token goes through GetGUIDFromName + ClntObjMgrObjectPtr(TYPEMASK_UNIT) and reads the held object's descriptor byte (+0x79 class, +0x78 race). The NameCache record (+0x140 / +0x138, null callback) is peeked only when no object is held. Before, the NameCache was also peeked when a held object's byte was 0. That never changed a result (creatures are not in the NameCache) but resolved the token twice and did not match the engine's branch. The comments and API.md also claimed creatures return nil, that class IDs run 1-9, and that UnitClass has a third return. None of that is true: creatures go through the same ChrClasses/ChrRaces lookup as in UnitClass/UnitRace, Druid is 11, and stock returns exactly two values. |
||
|
|
d825bcbdda |
fix(spell): stand, dismount and unshift only when the cast is sent
autoStand / autoDismount / autoUnshift ran their actions as soon as the caster-state check (CheckCasterStateForSpell, 0x006094F0) passed. In the 1.12.1 cast request (TryCastSpell, 0x006E4B60) that check comes before the target arm (0x006E5250), so a cast that then found no target still dismounted the player and showed "You have no target" (Charge with nothing targeted). A targeting cursor the player cancelled did the same. The caster check now only keeps what its rungs noted, and the actions run at the send. SendCast (0x006E54F0) builds the cast packet; its five callers all pass the global pending cast: the arm's two immediate sends and the object / location / trade-slot binders, which the targeting cursor's click also runs. The arm's only caller is the cast request, after the caster check, so every send follows a check of the same cast. Each action runs only if its state still holds, since the player can stand, dismount or leave the form while the cursor is up. The send redirects must all apply before the rungs are patched. Item use had the same ordering gap. Its form refusal (0x005D914D) comes before the toggle test, which turns off a use aura that is already up instead of casting (0x005D9157..0x005D9237). Re-using such an item while shapeshifted dropped the form as well. The refusal now only notes the unshift; it runs at the bind-on-use test (0x005D91D3), which every path past the toggle reaches, and the toggle's cancel call clears it. Fixes #132 |
||
|
|
55d94c80d6 |
feat(spell): apply autoStand/autoDismount/autoUnshift to interaction, loot, items, attacks and taxis
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. |
||
|
|
c0b403ba05 |
feat(model): add PlayerModel:ApplySpellVisualKit
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. |
||
|
|
8f4f574174 |
feat(model): add PlayerModel:SetItem
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. |
||
|
|
43a989dd56 |
feat(model): add PlayerModel:FreezeAnimation
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. |
||
|
|
0caddb8fe6 |
feat(model): add PlayerModel animation, keep-on-hide and unit methods
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. |
||
|
|
4726fd7c85 |
feat(model): add DressUpModel try-on hand, enchant glow, sheathing and auto-dress
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. |
||
|
|
3e0c40184b |
feat(cursor): add expanded cursors for NPCs and the fishing bobber
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. |
||
|
|
6f3a8ffc1d |
feat(texture): add static creature portraits by display ID (#123)
* 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> |
||
|
|
12a81f7f89 |
feat(baselib): debugstack takes a coroutine
debugstack([thread,] [start [, count1 [, count2]]]) returns the stack of a coroutine when one is passed first, with start defaulting to 0 (the coroutine's innermost level), as 4.3.4's Script_debugstack does. 1.12.1's debugstack (0x703760) has no thread argument. A thread fails its lua_isnumber and lua_isstring checks, so it returns the thread itself, and msg .. debugstack(co) raises "attempt to concatenate a thread value" in the coroutine error paths of WeakAuras, Questie's ThreadLib, MRT and Zygor. The engine function still builds the text, so its line format and its 12/10-level head and tail windows are unchanged, which keeps addons that parse debugstack() for their folder working. It reads levels through six calls on the state it runs on: lua_getstack (0x6FBAA0) at 0x70383F, 0x70386E, 0x703896, 0x7038AD and 0x703998, and lua_getinfo (0x6FBC70) at 0x7038CD. Those calls are redirected to read from the state the new global names for the call: the thread, or the calling state. The engine function has no callers of its own, so the global is its only way in and sets that state for every call; an error part way cannot leave a thread behind for the next call. The global removes the thread argument, inserts 0 as start when none is given, and calls the engine function directly in its own frame, so levels without a thread count exactly as before. |
||
|
|
05fa5845b7 |
feat(baselib): debuglocals, and the error window shows locals
debuglocals([level [, skipFunctionsAndUserdata]]) returns the locals and
then the upvalues of a call level as text, one name=value line each.
It runs only while an error handler runs, as in 4.3.4. 1.12.1's error
function sets no flag for that, but while a handler runs that function
(FUN_FRAMESCRIPT_CALL_ERROR_HANDLER) is a C closure level on the stack;
ErrorCallstack's search for it moves into a header so both modules use
it. level counts as lua_getstack and debugstack do.
Each local comes from lua_getlocal (0x6FBB20, fastcall L/ar + n), then
each upvalue of the level's function (lua_getinfo "f") from
lua_getupvalue (0x6F4660, fastcall L/funcindex + n). Values are
formatted as 4.3.4's FrameScript_PackVariable does, with retail's
spacing (probed: n=7, s="hi", t=<table>{ / k=1 / }): a frame table shows
its name, read from t[0] as FrameScript_GetObject does; a table's fields
show one space in at the top level only; a function shows where it is
defined (lua_getinfo ">nS"). The text stops at 4095 bytes. The second
argument, which retail's error handler passes as true, leaves out
function and userdata values.
Where 4.3.4 would fail inside the handler, this does not: a number key
outside int range is formatted from a copy, not converted in place
(which breaks lua_next), and a key that is not a number or string prints
as its type.
Retail also lists (*temporary) lines, 5.1's names for a level's stack
slots past its locals, including a C level's arguments. 1.12.1's
lua_getlocal is 5.0's and names declared locals only; those slots are
compiler registers that 5.0 and 5.1 allocate differently, so they are
not imitated.
The error window takes the locals as retail's GetErrorData does,
debuglocals(level, true) at the stack's level, and shows them under
Locals ("<none>" when there are none).
LuaCAPI.md had lua_pushupvalues, lua_getupvalue and aux_upvalue one row
off; corrected.
|
||
|
|
6ed0edc947 |
feat(baselib): gmatch, table.removemulti, math hyperbolics, difftime, newproxy, scrub
The base-library names that 1.12.1's Lua 5.0 and its compat.lua do not have: - gmatch: the global alias of string.gmatch (5.0's str_gfind, 0x7FCFA0), set by value like strmatch and strrev. - table.removemulti(t [, pos [, count]]): removes count elements from pos, shifts the rest down with lua_rawgeti/lua_rawseti and returns the removed elements. 5.0 keeps the length apart from the keys, so like the engine's table.remove (0x7FB770) it records the new length with luaL_setn (0x6F4EA0). - math.cosh, math.sinh, math.tanh: luaL_checknumber (0x6F4C80) and the C function. - difftime(t2 [, t1]): C difftime over the two times, next to the engine's date and time globals. - newproxy([false | true | proxy]): an empty userdata from lua_newuserdata. The closure's upvalue is a weak table (its own metatable, __mode "kv") that marks the metatables newproxy(true) makes, so only those can be shared. - scrub(...): replaces each argument that is not a boolean, number or string with nil, in place with lua_replace (0x6F32B0). All are registered on both Lua states. Game::Lua gains RawSetI (lua_rawseti, 0x6F3F60), Replace, CheckLString (luaL_checklstring, 0x6F4BE0), CheckNumber and OptNumber (luaL_optnumber, 0x6F4CD0); string.byte now uses them instead of local typedefs. issecure, issecurevariable and forceinsecure are left out. 1.12.1 has no taint, so they could only return constants, and a constant "secure" from issecurevariable makes AceHook-3.0 refuse every plain hook. |
||
|
|
1645e6b43c |
feat(baselib): string.byte returns a range of byte codes
1.12.1's str_byte (0x7FC210) is Lua 5.0's: it reads only the start position and pushes one code, so string.byte(s, 1, -1) silently returns the first byte alone. string.byte and the strbyte global now take (s [, i [, j]]) and push the codes of bytes i..j, with j defaulting to i. Negative positions count from the end and the range is clamped to the string. The arguments are read with the engine's luaL_checklstring (0x6F4BE0) and luaL_optnumber (0x6F4CD0), and positions truncate toward zero as the engine's str_byte and str_sub do, so a call without j returns exactly what it did before. Each code takes a stack slot, reserved with lua_checkstack (0x6F2F30). The engine refuses more than 2048 slots, so a range longer than about 2000 bytes raises "stack overflow (string slice too long)". compat.lua set strbyte from the old string.byte when the state opened, so strbyte is re-aliased to the new table entry. |
||
|
|
efcdc54a50 |
feat(baselib): string.gsub takes 5.1 string and table replacements
1.12.1's str_gsub (0x7FD0E0) is Lua 5.0's. Its replacement argument
differs from 5.1 in four ways: a table is rejected ("string or function
expected"); in a replacement string, %0, and %1 when the pattern has no
captures, raise "invalid capture index" (add_s checks every %n with
check_capture, 0x7FCE20); a function that returns nil or false deletes
the match instead of keeping it; and one that returns a table deletes
it too instead of raising an error.
The first two only ever raised errors, so the 5.1 behavior cannot break
an existing addon. The function rules can: SuperAPI's
CombatText_AddMessage returns nil to delete the player's own " [0x...]"
GUID from heal text. So function replacements keep 1.12.1's behavior,
and only string and table replacements change. A scan of every gsub
call in the installed addons and the stock FrameXML found no other code
that depends on the old string or function results.
str_gsub's match loop, anchor, count and buffer stay the engine's. Two
parts are replaced:
- The 60-byte replacement-type gate at 0x7FD15E (lua_gettop / isstring /
lua_type == function, then luaL_argerror) becomes a call to a
__fastcall check with ECX = L that accepts a number, string, function
or table and raises "string/function/table expected" otherwise.
- The one call to add_s (0x7FD203 -> 0x7FD2A0, __fastcall ms/b + s/e on
the stack, ret 8) goes to a function with the same signature. A
function replacement calls the engine's add_s unchanged. A string or
number gets 5.1's add_s, and a table gets 5.1's lookup: the first
capture or whole match as the key, a nil or false value keeps the
match, and any other non-string value raises "invalid replacement
value". Both use the engine's push_onecapture (0x7FCF40, plus 5.1's
whole-match case for index 0 with no captures), luaL_addlstring
(0x6F51F0) and luaL_addvalue (0x6F5270).
Both patches check the original bytes or call target and are skipped
when they differ.
The table-replacement shim in StringLib is removed. It wrapped the
pattern in an outer capture so a closure could return the whole match,
and deleted the match for a missing key when the pattern had a
back-reference. string.gsub and gsub are the engine's function again.
|
||
|
|
a7ce7555f0 |
fix(softinteract): anyinteract yields when the target is out of reach (#121)
* fix(softinteract): anyinteract yields when the target is out of reach
GetAny treated Reach::Out — the cursor's Unable form — as a usable
target, so with a target too far away the Interact Key pressed the inert
target instead of the nearest object it could interact with.
Prefer the target only while the cursor says it can be used now
(Reach::Now), and fall back to softinteract otherwise. A target behind
the camera still wins while it is in reach: the cursor verdict does not
look at facing.
* docs(api): anyinteract needs the target to be in reach now
The token section said a target that is too far away still counts, which
the implementation no longer does.
* fix(softinteract): anyinteract keeps an out-of-reach target when nothing is picked
Retail 12.1.5, targeting an NPC that is out of reach: facing away from
everything (softinteract nil), anyinteract is the target; facing a nearer
NPC, it is that NPC. The previous change returned 0 in the first case,
so the Interact Key lost the engine's own out-of-reach handling on the
target (the "need to be closer" error, or the click-to-move walk).
GetAny now reads the target's cursor verdict once: in reach wins, then
softinteract, then the target while it is interactable but out of reach.
* feat(interact): honor looseTargeting in C_PlayerInteractionManager.InteractUnit
On retail the Interact Key on a nearer NPC opens its dialog while a
farther unit stays targeted; with no target it targets the NPC. The key
calls InteractUnit("anyinteract") with looseTargeting at its default,
true, which ClassicAPI ignored, so the right-click's select replaced the
target.
FUN_ON_SPRITE_RIGHT_CLICK (0x00492820) is the active-player gate, the
select (FUN_SET_SELECTION_BY_GUID, 0x00493540), then the object's
right-click virtual (vtable +0x60, call at 0x00492881) with
FUN_GAMEUI_IS_KEY_DOWN(0). The select stores only units, so only a unit
other than the current target would move it. With looseTargeting and a
target, that one case runs the gate and the virtual without the select;
every other case still calls the engine function, which nampower also
detours for its right-click guards. The global InteractUnit keeps
targeting.
---------
Co-authored-by: Brues <5278969+brues-code@users.noreply.github.com>
|
||
|
|
65c6b0b271 |
feat(action): spell search, slot item and #showtooltip action queries
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. |
||
|
|
5f94fb98bd |
feat(action): C_ActionBar.GetSpell
C_ActionBar.GetSpell(actionID) returns the spell an action-bar slot casts, or 0. It is the engine's own slot resolver (0x004E5A50), which the cooldown, usable and range readers already go through: a spell action is its spell, a macro is its cached primary spell (+0x564, the one #showtooltip sets), and an item-by-ID action is the first on-use spell in its item cache record. An empty slot, a slot outside 1..120, an equipment set or a macro whose macro is gone gives 0. The spellbook flag the resolver also hands back is not part of the answer. |
||
|
|
426f6b6ec9 |
fix(container): free slots count the bank and use the engine's counter
GetContainerNumFreeSlots and GetContainerFreeSlots now take the main bank (-1) and the bank bags (5..10) while a banker is open. The main bank is gated on the banker GUID (0x00BDD038); bank bags get the gate from the engine's container-GUID getter (0x004F93E0), which answers 0 for them while the bank is closed. A bag's empty slots are counted by the engine's own CountFreeSlots (0x00622910, __thiscall on the bag's inventory list), and its slot count comes from that live list instead of the item record. The backpack and main bank are ranges of the player's list, walked slot by slot. Nothing walks PackBagSlot per slot any more, so the Lua stack is left alone. A container that is not there (keyring, empty bag slot, closed bank) now returns only 0, leaving bagFamily nil. The bag view SortBags kept for itself moves to Container::Bag so both modules share it, and Item::Location::EquippedBagInventory becomes BagInventory (1..10) beside a new BagContainer. C_Item.GetItemData's bagFamily now goes through BitmaskForRecord, so a bag whose family field is empty reports the same family there as it does from GetItemFamily (a Soul Pouch was 0 there, 0x4 here). |
||
|
|
72f0e5d98f | docs(action): range and usable events move to the Events section | ||
|
|
ceb6aaf46b |
feat(action): action range and usable events
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.
|
||
|
|
3849b7513b |
feat(statusbar): the standard fill moves the bar texture's rect
1.12's CSimpleStatusBar::OnFrameUpdate (0x007833C0) draws the fill by cropping. InstallBarTexture (0x007831F0) sets the texture's SetTexCoordModifiesRect flag (+0x124), and the update writes the filled part into the coordinates. The crop shrinks the drawn quad but leaves the texture's rect at the full bar, so a region anchored to GetStatusBarTexture() stays on the bar's far edge. Reverse, center, rotated and tiled bars already moved the texture's points. The OnFrameUpdate hook now replaces the engine's update for every bar. It runs the base frame update (0x0076B2C0), clears the crop flag, and moves the texture's four points in from the bar's edges with CLayoutFrame SetPoint and doResize, as 4.3.4's OnLayerUpdate does. The fraction comes from the bar's fill-fraction virtual (+0x98), so SetValue, the ease and SetTimerDuration all move the rect. doResize queues the texture for the layout pass before the draw, so anchored regions follow in the same frame. An empty fill keeps a zero-width rect on the start edge (CalculateRect 0x767A20 has no size test), so its anchors still resolve. The crop path and the code that handed a bar back to it are removed. A standard vertical bar now shows the bottom part of its image. |
||
|
|
5911298acd |
fix(text): inline icons are drawn by their line, as 4.3.4 does
Inline icons were CSimpleTextures registered on the fontstring's parent frame, with their visibility copied from the line once per tick, after the draw. Any line that hid kept its icons for one more frame; once chat lines moved with their text, a line scrolled off the top flashed its icons above the chat frame. 4.3.4's CSimpleEmbeddedTexture has no frame, and the fontstring draws it: CSimpleFontString::Draw queues the string, then QueueTextures. 1.12's equivalent is CSimpleFontString::Emit (0x00772E50), the only caller of AppendToBatch (0x00773080). The icon textures are now made with no frame (SimpleRegionConstruct skips SetRegionParent for a null parent; the frame-alpha fold 0x0077FAC0 tests the parent), anchored to the fontstring and given its scale through layout slot 0x10, as AddRegionToLayer does. A hook on Emit adds a line's icons to the layer batch right after its text, and only when the text went in (the batch count at +0x4 grew), so an icon draws exactly when its line does, in the same frame. The batch copies each texture's handle, blend and colour count when built (0x00772FD0) and reads vertices, texcoords and colours through pointers, so a change to a line's icon set marks the fontstring's layer (NotifyLayerChanged 0x0077FDF0), and moves and fades need no rebuild. 4.3.4 sets the textures up in UpdateString. RebuildString (0x007724A0) now does the same: when its gate passes (layout flags +0x60 bit 1, `test byte [esi+0x60],1`) it replaces the built string, so the line's icons are dropped, the new node is laid out (FUN_TEXT_ENSURE_BUILT) and its placements applied at once. The paint flush stays the backstop. Removed: the per-tick visibility mirror, the freshness expiry, region Show/Hide and re-parenting, and the SMF early apply with its _classicapi_InlineTexEarly toggle; the rebuild refresh covers what it did, and it rebuilt every icon line on every layout. The textures are deleted through their vtable slot 0 (as the frame destructor deletes regions) one tick after their fontstring dies or a reload, once the batches holding them were rebuilt. |
||
|
|
48822978c6 |
feat(statusbar): ExponentialEaseOut interpolation
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. |
||
|
|
f8a796ae04 |
feat(duration): duration text bindings, Format methods, GetTotemDuration
A font string can now count down by itself. A binding from
C_DurationUtil.CreateDurationTextBinding() takes a font string
(SetFontString), a duration object (SetDuration) and a format: either
SetFormatter(formatter), which writes a NumericRuleFormatter's text for
the remaining seconds, or SetTextFormat(formatString, components), where
each "{}" takes one component's property (Enum.DurationTextBindingProperty)
through its own formatter. SetExpiredText and SetZeroDurationText give
fixed texts, SetUpdateInterval spaces the updates out, and SetEnabled /
Enable / Disable start and stop it. The getters, Copy, Assign,
SetToDefaults, UpdateFontString, GetFormattedText and the Can* checks
are there too; the color curves are not. Duration objects gain
FormatElapsedDuration, FormatRemainingDuration and FormatTotalDuration
on the same formatter. Each answer was probed in a retail 12.1.5 client,
and the same probe matches on 1.12.
A binding is a userdata over a reference-counted native object, like the
duration object and the formatter, and it keeps both of those by
reference, so a change to the duration shows at the next frame. An
enabled binding takes a reference of its own, because it must go on
updating with no Lua object left; a Game::ReloadAutoRegister disables
every binding before the Lua state goes. The text is set through
CSimpleFontString::SetText (0x771D80), only when it differs from the
font string's own text buffer (+0xF0).
The updates run at the head of the UI render root (0x764330), before
the frame pass runs any OnUpdate or draws: Tick::FrameTick gains
AutoSubscribeBefore for this. At the tail, where FrameTick and
WorldTick subscribers run, the text would draw a frame after a timer
bar's fill, which the same pass computes; the probe caught that as a
"3.0" where retail showed "2.9". Nothing is mid-draw at the head, so the
formatter's call into str_format is safe there.
A binding keeps a font string pointer, and 1.12 hands a destroyed font
string's block back to the region pool, where it can come back as
another font string. The new FontString::Destruction co-hooks
CSimpleFontString::~CSimpleFontString (0x770E50, decompiled; its
deleting destructor 0x770E20 calls it, then frees the block) and runs
its listeners first, like Frame::Destruction does for frames. The
binding drops the pointer there and writes nothing after.
GetTotemDuration(slot) now returns a duration object, as retail's does:
the totem's span from its spawn stamp to its expiry, in exact
GetTime() seconds rather than GetTotemInfo's whole seconds, or a zero
span for an empty slot, since retail's return is never nil. Each call
returns a new object.
|
||
|
|
aff52e0ef6 |
feat(statusbar): timer bars, duration objects and Unit*Duration
A status bar can now fill by itself from a duration, with no Lua on each frame. bar:SetTimerDuration(duration [, interpolation [, direction]]) takes a duration object from C_DurationUtil.CreateDuration(), and Enum.StatusBarTimerDirection picks ElapsedTime (fills) or RemainingTime (empties). GetTimerDuration() returns it. UnitCastingDuration(unit) and UnitChannelDuration(unit) return the time span of a unit's cast or channel as a duration object. The duration object is a userdata like the NumericRuleFormatter: a pointer to a reference-counted span (start, total, mod rate) that every Lua object and every bar holding it own a reference to. Times are GetTime() seconds read through Time::Clock::NowSeconds, so they agree with GetTime() exactly. Its methods set the span from a start, an end or both, and read the start, end, total, elapsed and remaining time, the two percents and the started / expired / active state, in real or base time (Enum.DurationTimeModifier). The answers were probed in a retail 12.1.5 client and the same probe matches on 1.12. 1.12 already draws a bar's fill from a virtual, not from its stored value: CSimpleStatusBar::OnFrameUpdate (0x7833C0) crops the bar texture to the fraction its vtable slot +0x98 returns (0x783380, (value - min) / (max - min), or 0 for an empty range; disassembled). The timer co-hooks that function, so a timed bar keeps 1.12's texture crop. StatusBar::Fill's reversed, centered, turned and tiled layouts now take their fraction from the same virtual, so they follow a timer too. Fill's OnFrameUpdate co-hook calls Timer::Advance first, which marks the fill changed when the share moved and caches the share it drew. Shown bars update on every frame; hidden bars do nothing and are right on their first frame shown. As in retail, the timer stores 0 for the value and the range (written directly, so no OnValueChanged runs), SetValue leaves it running, and SetMinMaxValues ends it: Frame::MinMaxChanged's co-hook on the range setter (0x783270) clears the timer first. The bar keeps the duration object, not a copy, so a change to the object shows on the next frame. ExponentialEaseOut is accepted and draws as Immediate for now. UnitCastingDuration and UnitChannelDuration share one per-unit lookup with C_Spell.UnitCastingInfo / UnitChannelInfo, so they take the same unit tokens and have the same limits. Each call returns a new snapshot; it does not follow pushback or the end of the cast. |
||
|
|
2396d44f10 |
fix(frame): GetWidth / GetHeight / GetSize keep 1.12's scaled size
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.
|
||
|
|
932e6ed82f |
feat(texture): wrap modes and tiling
texture:SetTexture(file, wrapModeHorizontal, wrapModeVertical), the XML hWrapMode / vWrapMode attributes, texture:SetHorizTile / GetHorizTile / SetVertTile / GetVertTile and the XML horizTile / vertTile attributes. Wrap. 1.12 fixes a texture's wrap when its file loads. CSimpleTexture::SetTexture (0x00770200) builds the flags word with CGxTexFlags (0x0058A980) from one wrap argument for both axes, and TextureCreate (0x00449D90) caches the handle under the name and that word, so a repeating copy of a file is its own shared texture. The device reads one bit per axis (3 for U, 4 for V), repeat or clamp, so the mirror and border modes clamp. A texture with a mode or a tile flag loads through a co-hook on that SetTexture: the same body, with a repeat per axis. The engine's same-file early-out (0x0044AC70) ignores flags, so the co-hook also compares the held image's wrap (its device texture's flags at +0x3C, or a loading BLP handle's at +0x150), and for the same file it swaps in the other copy and keeps the pixel shader. Changing a mode or a tile flag reloads the held file the same way. The Lua method is registered again so it stores the wrap arguments before the engine handler (0x0079BB40) runs: true or "REPEAT" repeats, anything else clamps. Tiling. A tiled axis repeats, and each non-zero coordinate on it becomes the texture's length in script units over the image's size in texels. The script units are the engine's own coordinate push: DivideByScreenScaleX (0x0041AE40) of aspect (0x0041AD70) x 1024 x length, over the effective scale. The texels are the device texture's width and height (+0x24 / +0x28): the file's size, halved only past the GPU limit and untouched by BaseMip. The layer batch keeps a pointer to the eight coordinates (0x00772FD0), so the count is written whenever an input changes: after each corner store, after both native SetTexCoord forms (0x00770410, 0x007704C0), on a tile change, and after CSimpleTexture::Emit (0x007706E0), whose device lookup loads the image. A tile change counts against the held image before the repeating copy of the same file replaces it, since the copy has no size until it is drawn. A texture showing an atlas does not tile (the atlases are parts of their files), and neither does one with SetTexCoordModifiesRect on. SetAtlas clears modes and tiling. A tiled status bar texture takes StatusBar::Fill's anchored layout instead of 1.12's crop, which would size it by the count, and its fill sets no coordinates. An XML <TexCoords> that moves a tiled axis off 0..1 logs "Invalid TexCoords value (horizTile is on)" at error level (new Xml::Error) and falls back to the whole image. Also corrects the AtlasData.h note on the atlas tile flags: 4 is horizontal and 2 is vertical. |
||
|
|
eb0b70bc5e |
feat(xml): rotation, degrees, registerForDrag and group parentArray
Four more later-client XML attributes, each applied only when present and non-empty, after the engine has loaded the node. - Texture `rotation` (degrees) and FontString `degrees` / `radians` (`radians` wins when both are given), parsed with SStrToFloat (FUN_SSTR_TO_FLOAT): Texture::Transform's rotation about the default pivot. The bodies of texture:SetRotation and fontstring:SetRotation move into SetTextureRotation / SetFontStringRotation, which the Lua methods and the Xml::TypeLoad subscribers both call. - Frame `registerForDrag`: the names, split on spaces and commas with Storm's tokenizer (FUN_STORM_STR_TOKENIZE) as registerForClicks is, passed to Frame:RegisterForDrag itself (FUN_SCRIPT_FRAME_REGISTERFORDRAG 0x00776D60) as its own Lua call. That function ORs LeftButton 1, MiddleButton 2 and RightButton 4 and stores the mask at frame+0xFC; there is no setter to call instead. - AnimationGroup `parentArray`: the group is appended to region[key]. The append moves out of Xml::ParentKey into Xml::AppendToArray, which frames, regions and groups now share. Tested in game with an XML test addon: rotation="90" and degrees="90" read back as pi/2, two groups land in parentArray in load order, and a frame with registerForDrag="LeftButton" drags. |
||
|
|
238e35ae53 |
feat(animation): from/to alpha, setToFinalAlpha, target keys, scale from/to
Later clients' animation attributes and the methods behind them, on the animation port's own objects. Alpha fromAlpha / toAlpha (SetFromAlpha / SetToAlpha / GetFromAlpha / GetToAlpha): an absolute alpha. Each update the target's alpha becomes from + (to - from) x progress, through Region::SetAnimAlpha, which takes AddAnimAlpha's target rule but sets instead of adding: a frame's own alpha through its vtable setter, a texture's or font string's vertex alpha through FUN_REGION_GET_COLOR / FUN_FONTSTRING_SET_COLOR. Earlier orders apply at full progress before the current order updates, so the last write wins. Setting either end turns the mode on, an unset end is 0, and SetChange returns to the relative change. Absolute values and the 0 default follow Blizzard's own code: it starts fades with SetFromAlpha(region:GetAlpha()) and fades in with toAlpha="1" alone. AnimationGroup setToFinalAlpha (SetToFinalAlpha / IsSetToFinalAlpha): when the group finishes (Finish included), Region::KeepAnimAlpha makes the alpha each alpha animation's target ended on the stored alpha, so the end restores that. Stop restores the stored alpha as before. targetKey / childKey (SetTargetKey / SetChildKey), with SetTarget, SetTargetName, SetTargetParent and GetTarget: a key path from the animation, where $parent steps to the group, then its region, then that region's parent; childKey "X" is "$parent.$parent.X". The steps among the animation objects are taken by Region::FindRegionByKey, and the rest goes to the relativeKey resolver in Xml::Anchors, now public with a switch for its leading-global rule. A name or key is looked up again on Play, as 4.3.4 looks up `target`; the setters also look it up at once and return whether they found a region. GetTarget returns the group's region when there is no target. Scale fromScaleX / fromScaleY / toScaleX / toScaleY (SetScaleFrom / SetScaleTo / GetScaleFrom / GetScaleTo): the scale goes from `from` to `to`, passed to AddAnimScale as 1 - scale. SetScale sets the end, so 4.3.4's scale is the case from = 1 and its results do not change. Tested in game with an XML test addon: childKey and targetKey resolve, the absolute alpha and the last-write-wins rule give each order's end value, setToFinalAlpha keeps 0.5 after the group finishes, a group without it restores alpha 1, and the from/to scale values load. |
||
|
|
5b7fe94d4d |
feat(xml): alpha, scale, desaturated, indented and other XML attributes
Later clients read these attributes in their XML loaders; 1.12's do not. Each is now read after the engine has loaded the node (Xml::FrameLoad for frames, Xml::TypeLoad for the rest) and goes through the setter the matching Lua method uses. Each applies only when present and non-empty, so an element's own value overrides its template's. Frames: - scale: Frame:SetScale's path (0x00775140). A negative scale is refused, anything else goes to SetFrameScale (FUN_FRAME_SET_SCALE 0x0076AC10, __thiscall(frame, float, force), ret 8) without force. A Model keeps `scale` as its model scale, which its own loader reads. - dontSavePosition, propagateKeyboardInput: the existing frame flags, through FUN_FRAME_SET_FRAME_FLAG. Textures and font strings: - alpha: parsed as the frame loader parses its own `alpha` (SStrToFloat, clamped to 0..1, truncated to a byte), then set as Texture:SetAlpha and FontString:SetAlpha set it (0x0079B580, 0x0079CB70): the region color from FUN_REGION_GET_COLOR with its alpha byte replaced, through FUN_FONTSTRING_SET_COLOR. It applies after the node's <Color>. - desaturated (textures): Texture::Desaturation's full amount, the same as SetDesaturated. It applies whenever present; an empty value is false. - indented (font strings): the style bit SetIndentedWordWrap sets. Status bars: - reverseFill, rotatesTexture, fillStyle: StatusBar::Fill's modes, read after the status bar loader has set the BarTexture. An unknown fillStyle name changes nothing. Tested in game with an XML test addon: every attribute set its value, and parentKey, wordwrap and countInvisibleLetters still load. |
||
|
|
2eef939b5a |
feat(xml): registerForClicks and wordwrap attributes
1.12's loaders read neither attribute: the Button loader (0x007788C0) has no registerForClicks, and the font string loader (0x00770F40) reads nonspacewrap but not wordwrap. Both were silently ignored, so ported templates got the default click mask and wrapping text. Both are read in the loader hooks Xml::ParentKey already owns, after the engine's loader and before OnLoad, and only when present and non-empty, so a node's value beats its template's and an OnLoad call still overrides it. No new hooks. registerForClicks follows 3.3.5's CSimpleButton::LoadXML. Storm's tokenizer (0x0064AE50, the one 3.3.5 calls) splits the value on spaces and commas. Each name ORs in its bits through Frame::MouseRegistration::MaskForName, the RegisterForMouse name table (3.3.5's MouseButtonStringToBitmask, AnyUp / AnyDown included). The result replaces the mask through the engine's own setter 0x00779730 (__thiscall(button, mask), ret 4, writes only +0x330). wordwrap goes through FontString::WordWrap::Set, the body of fontstring:SetWordWrap: it clears the font's override of the no-wrap style bit and lays the string out again when the value changes. Tested in game: WowStyle1DropdownTemplate's Text (wordwrap="false") now reports CanWordWrap() = nil. |
||
|
|
36f3661b98 |
feat(lua): GetCallstackHeight, GetErrorCallstackHeight, SetErrorCallstackHeight
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.
|
||
|
|
99f72aa631 |
feat(ui): UI_SCALE_CHANGED event
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). |
||
|
|
9e5c0c558a |
feat(autocomplete): C_AutoComplete name completion (API only)
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. |
||
|
|
b3b950f2d8 |
feat(spell): spell alerts and proc glows (API only)
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.
|
||
|
|
5dcf10a9fd |
fix(player): name cache keeps one copy of each player and honors renames
The persistent name cache copied every name reply into its own map, so each player the engine named this session was held twice. The copy also went stale. On a rename the server sends SMSG_INVALIDATE_PLAYER (0x31C) and HandleNameInvalidate 0x00555600 frees the engine's record, but our copy kept the old name, and GetPlayerInfoByGUID / UnitNameFromGUID fell through to it. The store now holds only the players the engine's name cache (0x00C0E228) does not: - A Net::PacketDispatch subscriber reads SMSG_NAME_QUERY_RESPONSE (0x51) in the same order as HandleNameQueryResponse 0x005551A0 and drops our entry just before the engine stores its record. This replaces the hook on NameCacheStore 0x0055F310. The backstop save runs before the drop, so no save misses the player. - The same subscriber drops our entry on SMSG_INVALIDATE_PLAYER. - The engine empties its cache only at world entry: NameCacheClear 0x00560400, called from CGGameUI::InitializeGame through 0x00555740. A hook there copies the engine's records into the store first, while the store still holds the realm they came from. - At exit the engine frees the cache from its atexit handler (NameCacheSave 0x0055EC70). Storm's exit handler 0x0064CC30 runs every atexit handler before ExitProcess, so DLL_PROCESS_DETACH was too late to read it. At the first world entry the clear hook registers our save with the exe's own _atexit (0x00409AEF). Handlers run newest first, so ours runs before the engine's. Detach now calls Shutdown, which never reads the engine's cache. - A save writes our store, then the engine's records, so a load applies the engine's data over ours. - C_PlayerCache.RememberPlayer leaves alone a player the engine holds. One hook removed (0x0055F310), one added (0x00560400). |
||
|
|
3ef6c4ffed |
fix(player): name cache keeps each player's realm and whole UTF-8 names
The persistent name cache cut names at 16 bytes. A Chinese character is 3 bytes in UTF-8, so a 6-character name lost part of its last character. The limit is now the engine's own: the name cache store (0x0055F310) copies the name with SStrCopy(dst, src, 0x30) and the realm with SStrCopy(dst, src, 0x100), now PLAYER_INFO_NAME_SIZE and PLAYER_INFO_REALM_SIZE in Offsets.h. A longer name from Lua is cut on a UTF-8 lead byte, never inside a character. Entries are now keyed by GUID, the engine name cache's own key, and keep the record's server name (+0x30, empty for a player on this realm). The by-name index uses the form the engine itself shows for a player from another realm: ClientChatHandler (0x0049D560) builds the sender as name, "-", and the realm with spaces and tabs removed. A bare name stands for this realm. Player::FullName builds and parses that form. Two players with one name from two realms no longer replace each other, C_PlayerCache.GetPlayerInfoByName takes "Name-Realm" (a chat sender name as it comes), and the persistent fallbacks of GetPlayerInfoByGUID and UnitNameFromGUID return the stored realm. A "Name-Realm" skips the by-name search and the friends list, which keep no realm. The engine name cache walk moved from Unit::Identity into Player::Info, its one caller. The cache is one flat file per realm, WTF\ClassicAPI\NameCache-<realm>@<realmList>.txt, in a v5 format split on tabs (a realm can hold spaces): guid, name, realm, class, race, sex. The v4 file under WTF\ClassicAPI\<realmList>\<realm>\ is migrated on first load and deleted, with its emptied folders, once v5 is saved. The old import from the per-account path is gone. The store loaded once per process and saved to whatever realm was current, so logging out into another realm wrote the first realm's players into the second realm's file. It now records the file it holds: every access compares that with the current realm's file, and on a change saves the old realm's entries to their own file before loading the new one. A failed rename leaves the cache dirty for the next flush. Verified on Octo: all 63,178 N'Zoth v4 entries migrated unchanged, 13 of 13 C'Thun entries migrated, the v4 folders were removed, and a N'Zoth to C'Thun switch in one session kept the two files apart. Fixes #119 |
||
|
|
1ef8c5597a |
fix(player): UnitNameFromGUID returns the name cache's realm
CGUnit_C::GetUnitName (0x00609210) takes a realm out-pointer as its stack argument, not the flags word Offsets.h called it. For a player whose name-cache record has a non-empty server name (record +0x30) it stores a pointer to that string, and otherwise leaves the pointer alone (0x0060926B-0x00609283). Vanilla's Script_UnitName (0x00517020) passes &realm there and returns it as UnitName's second value. The record's server name is the realm string SMSG_NAME_QUERY_RESPONSE carries: Tortoise sends it empty, a cross-realm server fills it. UnitNameFromGUID pushed a hard-coded "" instead. NameFromGuid now passes the out-pointer through, as Script_UnitName does, and gains a step between the friends list and the persistent cache: the engine name cache record itself (FUN_PLAYER_INFO_LOOKUP with no callback, a passive peek), which also gives the realm for players out of range. Before, a player who whispered from far away resolved only with the persistent cache on. UnitSpellTargetName shares NameFromGuid and finds those players too. The realm is "" when the record has none, or when the name came from the friends list or the persistent cache, which stores no realm. Retail documents both returns as non-nil strings. GetNameAndServerNameFromGUID in the addon's PlayerUtil returned GetRealmName(), the local realm, for every GUID. It now returns GetPlayerInfoByGUID's realm, as retail's UnitUtil does, so a player on the local realm gets "" and code that appends "-" .. realm only when it is non-empty works. Seven files declared their own function-pointer type for 0x00609210, all with the wrong int *outFlags argument. Unit::Identity::NameForUnit is now the one wrapper. The other six callers pass no realm and behave as before. Offsets.h gives the getter's real signature (RET 4) and what it stores. API.md's GetPlayerInfoByGUID and UnitNameFromGUID entries describe the realm and the four sources. |
||
|
|
84171bef2d |
fix(unit): mirror timers clear on leaving the world, empty slots say UNKNOWN
The engine's handler for the three mirror-timer SMSGs (0x005E7990) only reads each packet and signals MIRROR_TIMER_START / _PAUSE / _STOP, so GetMirrorTimerInfo and GetMirrorTimerProgress read a table of our own. A MinHook on that handler fed it by reading the packet and putting the cursor back. The table now reads START (0x1D9) and STOP (0x1DB) as a Net::PacketDispatch subscriber, and the hook is gone. CGPlayer_C::Release (0x005E9AA0) runs StopMirrorTimers (0x005E9B90) for the active player: MIRROR_TIMER_STOP for all three types, with no packet. The table never saw it, so a breath timer stayed after a logout or a loading screen. Release then calls LeaveWorldUI (0x00490A80), which fires PLAYER_LEAVING_WORLD through the no-arg dispatcher. An Event::SignalHook subscriber clears the table on that event. What the functions return: - GetMirrorTimerInfo returns an empty slot as "UNKNOWN" with zeros, the name helper 0x005E7AE0 gives for no type, instead of nothing. Code that tests for "UNKNOWN" on PLAYER_ENTERING_WORLD called its setup with nil before. paused is a number, as the event's %d is, and spellID is a seventh value. - A PAUSE packet no longer changes the table. Tortoise never sends one: it resends START with the paused byte, which the table keeps. - GetMirrorTimerProgress is value + scale * elapsed with no clamp. A timer started paused holds its value. Names match through SStrCmpI, and a name that is not a timer type returns nil. - The time comes from Time::Clock. 0x0042B790 truncates the tick itself and returns it in EAX, so the GetTickCount workaround for an FPU return had no cause. Offsets.h drops the CDataStore readers, the handler and the PAUSE opcode, which nothing reads now, and its comment gives the verified signatures and packet layout. Both functions get API descriptors under the MirrorTimer system. API.md's section is rewritten to match. |
||
|
|
38d5c6c7b6 |
fix(unit): ClassicAPI's unit events reach markN and the soft tokens
ClassicAPI fires its own unit events (UNIT_AURA_DELTA, the remote UNIT_SPELLCAST_*, the power, group-member, distance, incoming-resurrection and targetable events) once per token through Unit::Identity::TokensForGUID. That took the engine's tokens from its reverse map FUN_UNIT_TOKENS_FROM_GUID (0x00515C50) and then added its own short list: focus and nameplateN. The engine's unit events get their ClassicAPI tokens from a second list in Unit::TokenEvents, which also has markN, softenemy and softfriend. So a soft enemy got UNIT_HEALTH but never UNIT_SPELLCAST_START. That list is now the public Unit::TokenEvents::TokensOf, capped at the caller's room and empty before the player object exists, as before. TokensForGUID appends it to the engine's tokens, so both kinds of event reach the same tokens, and a new token goes in one place. nameplateN now comes before focus, the order TokensOf already used. API.md's soft enemy and soft friend section now says that their tokens fire unit events, as the focus, nameplateN and markN sections do. |
||
|
|
f9ec92014c |
feat(target): show softenemy and softfriend names as the target's
Retail shows a soft unit's overhead name the way it shows the target's: a soft enemy past nameplate range keeps its name, whatever the UnitName cvars say. 1.12 decides an overhead name in CGUnit_C::ShouldShowName (0x6070A0), which its only caller, the name draw PLAYERNAMEDESC::UpdatePlate (0x6C6EE2), asks every frame. In order: no name when CanSelect (vtable +0x58) refuses the unit, or when it has a nameplate (+0xE60) or a chat bubble (+0xE64); the active player by UnitNameOwn; the target always; then the UnitName mask. A hook gives a soft unit the target's branch: the first three tests, then 1. The player is never a soft unit, so the own-unit test does not apply. In nameplate range the plate shows and the name does not, as for the target. The name draw runs inside the scene (the model draw callback 0x6C7780), so the hook reads the soft GUIDs cached on the UI frame tick, as the selection circle does, and never picks there. Nothing invalidates: the verdict is asked fresh each draw. |
||
|
|
264af04ee3 |
feat(cooldown): countdown formatter and C_StringUtil.CreateNumericRuleFormatter
Addons can now choose the countdown text and its color for each range of time left. Cooldown:SetCountdownFormatter(formatter) and GetCountdownFormatter() take a NumericRuleFormatter from C_StringUtil.CreateNumericRuleFormatter(). Its breakpoints each give a threshold, an optional step, rounding (Enum.NumericRuleFormatRounding), min and max, a format string, and optional div/mod/step components, one for each format argument. Color codes in the format string pass through. The formatter is a full Lua userdata, the first in the DLL. Its block holds a pointer to a reference-counted native object. Every Lua object and every frame that uses it hold one reference, so the GC finalizer (__gc) and the frame's deleting destructor can run in any order. lua_newuserdata (0x6F4560, __fastcall, block at Udata + 0x10, read from the disassembly) is new in Offsets.h. lua_touserdata reuses FUN_FRAMESCRIPT_GET_OBJECT, which is that function. One metatable per Lua state lives in the registry. Its __metatable is false, and the identity check compares a value's metatable with it. 1.12 has no newproxy, so the only finalizer an allocation can run is ours. Thus no addon code can change a formatter while it formats. The text goes through 1.12's own str_format (0x7FD3D0). Its closure is made once per state and kept in the registry, because FormatNumber runs from addon timers and lua_pushcclosure allocates on each push. The rounding is x - fmod(x, step), plus a step away from zero when the rounding needs it. Only that arithmetic matches retail to the last bit: Up of 0.7 by 0.1 gives 0.7000000000000001 and of 0.6 gives 0.6, but ceil(x / step) * step gives 0.6000000000000001. The cooldown shares the formatter and gives it the unrounded seconds left. The formatter replaces the built-in text and both thresholds, and every gate stays. The cached countdown text is now a heap std::string, because a formatter's text has no length limit. The frame frees it, and its reference, in DeletingDestructor. A probe addon recorded 229 results in retail 12.1.5. The same probe on 1.18.1 matched 197 of them. The other results come from 1.12's own str_format (%e exponent digits, %q, %d past the int range, its error wording), from field storage on the object (not ported), and from addresses and times. |
||
|
|
090cfc3bfd |
feat(lua): global wipe, the table.wipe alias
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. |