mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 12:27:35 +00:00
6000df8009857e8e137a1f73621412929218db14
521
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6000df8009 |
feat(cvar): add C_CVar.SetTempCVar and RemoveTempCVar
SetTempCVar changes a cvar for the session without saving it to Config.wtf; RemoveTempCVar restores the value from before the first temp set. The temp set passes a6 = 0 to the engine setter, the only argument that marks the config dirty, so it applies the value and runs the change callback without asking for a save. When something else dirties the config, a co-hook on the config writer (0x0063D980) points each temp cvar's value at its saved string for the write and restores it after. A plain SetCVar over a temp value wins: the entry drops itself, so the user's set is saved and never rolled back. Errors mirror Script_SetCVar. Registered in-game and on glue. |
||
|
|
fde3beca9d |
fix(table): keep the stored length on weak-valued tables
The getn heal read a nil at the stored last slot as proof that nobody maintained the length. For a weak-VALUED table that is false: the collector clears those slots behind the writer, so `luaL_setn` is still exactly what the last append left and the holes below it come and go between two reads. Compost-2.0 is the casualty. `secondarycache` is `__mode = "v"`, filled with `table.insert`; `GetTable` walks it with `pairs` and calls `table.remove(cache, i)` on a live index. Once the GC clears slot 1 and the stored last slot, the heal bisects to border 0, `luaB_tremove` takes its `e == 0` empty-table early-out and returns NOTHING, and the next line indexes with the nil it got back -- "table index is nil" at Compost-2.0.lua:81, once per recycled table, in every addon that pulls Compost in through AceLibrary. The border search is what exposed it (5aa382b's table.insert switch is not involved -- Compost never appends a nil, so no mark ever applied to its tables). The walk-down it replaced returned the HIGHEST populated slot, which is always at or above the live index the pairs loop holds, so remove still found something; a bisect returns some border and probes its way down to 0 through the collected slots. That tradeoff was known and written down -- a weak cache read through table.remove is where it bites. So gate the heal: `__mode` mentioning "v" returns the stored length untouched, as rule 3, ahead of the mark. It costs one getmetatable and a raw `__mode` fetch on the already-rare unhealthy path, so the bisect keeps its win everywhere else, and the mode is read raw -- the same fetch the collector's own check uses. New plumbing: Game::Lua::GetMetatable, from the offset already catalogued in LuaCAPI.md. LuaEnv::Fenv had rolled its own copy; it now shares this one. Verified in game: a three-slot weak table with slots 1 and 3 collected reports getn 3 and hands back its live slot (0 and nothing before), and the Compost errors are gone. |
||
|
|
bbefdb847d |
fix(aura): include the player's pet in the PLAYER filter
Blizzard defines the token as "Include only auras that were cast by the
player, or by the player's pet or vehicle"
(AuraUtil.AuraFilters.Player). There are no vehicles here, so the caster
set is player + pet -- but all three comparisons tested
`casterGuid == PlayerGuid()` and nothing else.
A pet's cast carries the PET's guid in the Aura::Source cache, since
that is who SMSG_SPELL_GO names, so "HARMFUL|PLAYER" silently dropped
every aura the pet applied. `!PLAYER` had the mirror bug: it returned
the pet's auras as though a third party cast them.
Centralize on one IsPlayerOrPetCaster predicate rather than fixing each
site -- IsPlayerCast, GroupIsPlayerCast and CachedMatches had already
grown three separate copies of the comparison, which is how they would
drift again. The pet guid comes from VAR_PET_GUID, a live engine global,
so the predicate stays two compares with no object resolve; it runs per
aura slot. A caster of 0 (cache miss) still matches nothing.
Unrelated and untouched: the `isFromPlayerOrPlayerPet` aura field, which
means "applied by ANY player or a player's pet" and is answered from the
caster's HIGHGUID prefix.
Verified in-game: a Hunter pet's Scorpid Poison now returns from
C_UnitAuras.GetAuraDataByIndex("target", 1, "HARMFUL|PLAYER").
|
||
|
|
0296e0e5a4 |
feat(player): add WEAPON_SLOT_CHANGED
Fires with no payload when the item in inventory slot 16, 17 or 18 changes -- equip, unequip and swap alike. The narrow sibling of PLAYER_EQUIPMENT_CHANGED that swing-timer consumers want, so they no longer filter three slots out of a 19-slot event in Lua on every gear change. Blizzard's own Blizzard_SwingTimer registers this event. Rides the descriptor-field observers Player::Equipment already installs for PLAYER_EQUIPMENT_CHANGED -- the weapon slots are three of the 19 -- so there is no new hook, no second registration and no polling. The dispatcher invokes the callback only when a slot's item GUID actually changed, and seeds each node's mirror from live at registration, so login and /reload produce no fire. Coalesced to at most one fire per frame via a dirty flag drained on WorldTick: one action can change two weapon slots (a two-hander replacing a one-hander plus an off-hand) and the per-slot observer would otherwise fire twice for it. The drain checks the reservation slot before clearing the flag, so a change marked while the reservation is still unclaimed is held rather than swallowed. Slot 18 is exempted for the classes whose slot 18 is a relic slot -- a Libram/Idol/Totem is a passive stat item, not something you swing. Gated on the same data Script_UnitHasRelicSlot (0x00519E50) reads rather than a hardcoded class list: ChrClasses.dbc[class] + 0x40, verified in the disassembly at 0x00519EAE-0x00519EC0 and against the client's own ChrClasses.dbc, where that column is 1 for PALADIN/SHAMAN/DRUID and 0 for the other six. Read live, since the class-byte global is repopulated per character-enter. Not verified in-game yet: built and deployed only. |
||
|
|
4d835aa674 |
feat(combat): add PLAYER_SWING / PLAYER_SWING_RANGE_UPDATE / C_SwingTimer
Backports the Forever beta swing-timer API. PLAYER_SWING and PLAYER_SWING_RANGE_UPDATE fire off the packets the engine already sends for melee/ranged auto-attacks, mirrored as state (parry haste, extra attacks, on-next-swing abilities, cast-time resets) rather than relayed 1:1, so N packets for one logical reset collapse into one event. Two bugs surfaced only by in-game testing, not by reading the disassembly: - FUN_UNIT_CAN_ATTACK (0x00606980) is __thiscall with the second unit pushed on the stack, not __fastcall(ecx,edx) -- Ghidra's decompile of the one caller renders it as a plain two-arg call either way, so the wrong convention crashed on a bogus ecx read off [ebp+8] garbage. Same trap as FUN_SET_CVAR_VALUE. - The generic engine range core (FUN_006e3480, what C_Spell.IsSpellInRange already uses) computes full 3D distance, but the server's actual melee-attack gate (Unit::CanReachWithMeleeAutoAttackAtPosition, WorldObject::CanReachWithMeleeSpellAttack) is explicitly 2D -- verified by landing hits at 5.08yd 3D distance that the 3D check rejected. Melee now gets its own 2D check (Combat::Swing::InMeleeRange), reading the same live engine reach constants; ranged keeps the 3D core, which matches the server's own (also 3D) generic spell-range gate. Also: attack-start no longer resets the off-hand timer unless the target is actually in range. Unit::Attack's off-hand reset fires the instant an attack is declared, even out of range, and the server just silently re-arms it every 100ms until you're close enough -- firing PLAYER_SWING there promised a swing time that was not real. Slam is special-cased for Turtle: every rank in Turtle's own client Spell.dbc carries AttributesEx2's swing-reset exemption, but packet timestamps show Turtle's server does not honor it for Slam -- both melee timers delay after casting it, contradicting both the client data and the public tortoise-wow reference source. Combat::Swing::AutoSwingResetOverride is a small extension point (mirroring Turtle::ComboDuration's Rip override) for this kind of server/client divergence; src/turtle/SwingReset.cpp registers the known Slam ranks. Verified in-game: swing detection (dual-wield, extra attacks coalescing to one event, cast-time resets), the melee range fix, and the attack-start range gate. The Slam override has not been rebuilt/retested yet. |
||
|
|
dfbba225d2 |
fix(macro): correct the loose/mpq split in the icon enumeration
The three macro-icon scan callbacks were named and tagged for the wrong walker. FUN_LOAD_MACRO_ICONS hands 0x004F0220 to FUN_MPQ_ENUM_FILES (the archive listfile walk) and 0x004F0350 / 0x004F04F0 to the disk walker, but Icons.cpp had it exactly inverted: the archive pass was tagged SRC_LOOSE and both disk passes SRC_MPQ. So GetMacroIcons / GetMacroItemIcons returned the loose on-disk icons -- nothing at all on a stock install -- while GetLooseMacroIcons / GetLooseMacroItemIcons returned the archive set. It went unnoticed because the only consumer, IconDataProviderMixin, calls all four and concatenates, so only the ordering within each list changed and never the content. Tags corrected, and the offsets renamed to what the loader actually hands them to, with each one's filter recorded since that is the load-bearing difference between the two disk passes: FUN_MACRO_ICON_CB_DISK -> FUN_MACRO_ICON_CB_MPQ FUN_MACRO_ICON_CB_USER_MPQ -> FUN_MACRO_ICON_CB_DISK_PREFIXED FUN_MACRO_ICON_CB_INSTALL_MPQ -> FUN_MACRO_ICON_CB_DISK_ANY Also replaces a wrong guess in the Offsets.h block. It claimed a filter inside the SStrDup/array-append helpers downstream of all three callbacks was what kept INV_* out of the engine's own list. Decompiling the loader and all three callbacks shows there is no shared downstream helper at all: the grow-and-append code is inlined identically into each callback, so the only filtering is what each one does itself. The list is Ability_*/Spell_* only because passes 1-2 prefix-filter and pass 3 -- the one that does not -- is a disk walk over a folder that ships empty. A loose INV_*.blp dropped into Interface\Icons\ does enter the list, through pass 3 alone. The block now also records the qsort + adjacent SStrCmpI dedup, that GetMacroIconInfo pushes the empty string rather than nil for a bad index, and that only GetNumMacroIcons triggers the lazy build. Verified in-game. |
||
|
|
930b0c6492 |
Add C_LossOfControl.GetSchoolLockout
Serving the school-interrupt lockout through the generic list costs a caller two full BuildList passes -- GetActiveLossOfControlDataCount runs one and GetActiveLossOfControlData runs another -- plus a Lua table per entry, and BuildList classifies up to 16 debuff slots with RecordForID on each. All of that aura work is waste for a caller that only wants the lockout, and a macro conditional reading it every frame pays it every frame. The state behind it is g_schoolLock, seven pairs of ticks. Read it directly: no table, no debuff scan, one pass. Return a mask rather than a school. Each SMSG_SPELL_COOLDOWN batch locks one school, so being kicked on fire and then frost leaves two entries active, and a caller walking the list and stopping at the first SCHOOL_INTERRUPT drops the rest. filterMask narrows the scan, which is how a caller gets one school's own remaining time instead of the aggregate. Also document that the ADDED/UPDATE events diff which effects are active, so re-locking an already-locked school extends endMs without firing either one -- that expiry cannot be cached on the events. |
||
|
|
a33146241e |
feat(macro): make [button:N] read the click it is running inside
The condition existed but was a stub: it matched only "1" / "leftbutton" and always passed, because nothing on this client could say which mouse button had driven the current click. GetMouseButtonClicked can now answer that, so the condition asks it. Numbers use the modern API order -- 1 left, 2 RIGHT, 3 middle, 4, 5 -- which is deliberately NOT the order of the engine's internal click bitmask (where 2 is the middle button); that mask never reaches Lua. A name is matched as written, so [button:rightbutton] and [button:2] are the same test, case-insensitively. Outside a click the answer stays "left button". That is not a cosmetic default: Macro::ShowTooltip re-evaluates conditional directives off the world tick with no click in progress, so a nil-means-no-match rule would change which icon a `/cast [button:2] X; Y` macro displays at rest. The engine's own no-argument Button:Click() means the left button, so it is also the honest resting answer for a state-driver poll. Verified in-game with all five buttons, both directly from OnClick and through a real macro body -- click -> attribute macro -> EXECUTE_CHAT_LINE -> SlashCmdList -> SecureCmdOptionParse, which confirms the click context survives the whole chain -- plus the name form, the btn alias, and the no-click resting answer. |
||
|
|
c7af8d396e |
feat(frame): accept AnyUp / AnyDown in Button:RegisterForClicks
The engine's RegisterForClicks (0x00782490) SStrCmpI's each argument against exactly ten literals -- LeftButtonDown 0x1 / LeftButtonUp 0x100, Middle 0x2 / 0x200, Right 0x4 / 0x400, Button4 0x8 / 0x800, Button5 0x10 / 0x1000 -- treats anything else as contributing nothing, and ASSIGNS the OR to the click mask. So the collective names later clients take, "AnyUp" and "AnyDown", did not fall back to the default here: they zeroed the mask and the button silently stopped answering every real click, while Button:Click (which ignores the mask) kept working and hid it. That is what broke the first click-bracket harness yesterday. Frame::ClickMask re-registers RegisterForClicks on the Button method registry (0x00CF4E14, new VAR_BUTTON_METHOD_REGISTRY; most recent registration wins, CheckButton inherits through the type walk). It does not reimplement the parser: for each AnyUp / AnyDown argument it appends the five names that collective stands for onto the Lua stack and tail-calls the engine's own function, which then does the self typecheck, the parse, the setter and any error text itself. The collective string is left in place -- the engine's unknown-name -> 0 rule already makes it inert -- so the reshape is purely additive and the engine's loop terminates exactly as before. Verified in-game with left, middle and right: AnyUp fires OnClick on release; AnyDown on press; "anyup" + "LeftButtonDown" (case check) fires left on both press and release and the others on release only; a LeftButtonUp control is unchanged; "Fnord" alone still yields no clicks. |
||
|
|
a2a2ccb804 |
feat(frame): bracket the click at the engine's click vmethod
PreClick/PostClick now fire on every click, with or without an OnClick
handler, and GetMouseButtonClicked is live inside OnMouseDown and
OnMouseUp on any frame -- the two things 3.3.5 does that the previous
runner-only bracket could not.
3.3.5 (CSimpleButton::Click FUN_0096fd70 -> FUN_0096f090) fires PreClick,
OnClick and PostClick as three independent `if (slot)` blocks, so
Pre/Post never depended on OnClick. And the frame-manager field behind
GetMouseButtonClicked is written by exactly four dispatches -- that
click, the double-click, and CSimpleFrame::OnMouseDown FUN_0048fc30 /
OnMouseUp FUN_0048fce0 on every frame type -- not by OnDragStart.
Confirmed live on 3.3.5: `down LeftButton LeftButton`, `up LeftButton
LeftButton`, `drag LeftButton nil`.
|
||
|
|
7a4ab29cb0 |
fix(input): scope GetMouseButtonClicked to the click dispatch
It read a button captured from the WH_GETMESSAGE mouse-message hook and evicted two world ticks later, so it reported a button for as long as one was held, and for a couple of frames after every click -- useless as the "am I inside a click handler" test it exists to be. 3.3.5 does none of that. The getter (FUN_0050f950) pushes a frame-manager field that only the four click dispatches ever write -- button OnClick FUN_0048fc30, OnDoubleClick FUN_0048fce0, and the two secure click paths FUN_0096fd70/FUN_0096fdd0 that wrap PreClick -> OnClick -> PostClick -- each saving the old value on the C stack, setting its own button argument, running the handler, then restoring. That C-stack save is what makes a nested click shadow the outer one and restore on unwind. 1.12 has no such field but the same bracket point: FUN_00779540 (OnClick) and FUN_00779650 (OnDoubleClick) fire their handler through the runner Frame::ClickEvents already intercepts, passing the button name as the fire's only "%s" argument. Mirror the engine's bracket there and read the name out of the fire, which also inherits the engine's own naming: Button:Click (FUN_007826C0) folds its optional string down to a numeric mask, so a bare Click() arrives as "LeftButton" and an unrecognized name as "UNKNOWN". GetMouseButtonClicked moves to Frame::ClickEvents with the dispatch it describes. IsMouseButtonDown and GLOBAL_MOUSE_* keep the message hook, which is the right source for a device query. The recursion guard now covers only the PreClick/PostClick pair, so a nested click still gets its own button scope. It now returns nil in OnMouseDown / OnMouseUp / OnDragStart, matching 3.3.5, where those paths never touch the field. Verified in-game across all four dispatches: real and programmatic clicks, OnDoubleClick, PreClick/PostClick, the nested inner/outer restore, Click() argument folding, and an OnUpdate watcher that never saw a value outside a handler. |
||
|
|
a2d7d4e9e6 |
feat(classes): add C_ClassColor.GetClassColor
Returns a class's color as a ColorMixin, nil for anything that is not a class name. There is no engine source for the colors on this client: ChrClasses.dbc has 17 columns and none of them is a color, and the binary carries no class-color table (searched it for the class float triples and for the hex strings, no hits). RAID_CLASS_COLORS is what the client actually has -- FrameXML builds it in Fonts.xml, Util/Color*.lua gives it the mixin, and every UI colors classes from it -- so the function reads that table instead of carrying a second copy of the nine colors. Same call C_UnitAuras.GetAuraDispelTypeColor makes with the DEBUFF_TYPE_*_COLOR globals: one source of truth, one shared object per class, and a class another addon recolors stays recolored here. The lookup runs metamethods, so the pfUI shim in Util/AddOnCompat.lua keeps answering grey for unknown keys. Verified in-game: "WARRIOR" returns 0.78/0.61/0.43 with hex, colorStr and the mixin methods; an unknown name returns nil; the result is identical to GetClassColorObj for the same class. |
||
|
|
89c705b922 |
docs: describe the four macro commands added today
Covers /castsequence, /castrandom, /userandom and /stopmacro in the Lua reference, plus StopMacro beside GetMacroSpell, and updates the README groups and counts. Two caveats are worth having written down rather than found: a list names no single action, so the random commands give the action button nothing to show, and being static lines they end the #showtooltip collection -- a /castrandom above a /cast takes the icon from it. |
||
|
|
298eff528c |
fix(quest): fire QUEST_ACCEPTED from the engine's own accept gate
The client announces "Quest accepted" from a per-quest-slot descriptor observer, while the quest log is rebuilt from a separate one. Slot 0's announcer shares a descriptor anchor with the rebuild node and is registered first, so accepting into an empty quest log announced a quest no quest-log API could see yet. Rebuild the log at the top of the observer, as 3.3.5 does, and source QUEST_ACCEPTED / QUEST_REMOVED from the engine's own per-slot gates instead of a log diff. That drops the set-diff login heuristic, catches the re-announce when a quest goes complete to incomplete, and makes the message precede the event on every path. A cold quest cache rides the engine's own callback queue so the deferred announce still comes first. |
||
|
|
40abcabe9d |
fix(unit): read the creature id from the unit's live entry, not the GUID
UnitCreatureID and C_CreatureInfo.GetCreatureID extracted the template from
GUID bits 24-47. On the (v)mangos family that is the spawn row's FIRST
creature id: Creature::CreateFromProto builds the GUID from creature_id[0]
and applies the rolled template through UpdateEntry -> SetEntry only, and a
respawn re-rolls. Turtle's creature table has up to four ids per row (6,935
of 79,773 rows use more than one), so mobs with different names reported the
same id — every Gordunni ogre from a {5236, 5238, 5239} row read 5236.
New Unit::CreatureID reads the entry the engine keys the creature NAME query
on: the instance block at [[unit+0x08]+0x0C] (FUN_00604600's own read), with
the GUID bits only as the out-of-view fallback for creature GUIDs. The
descriptor's OBJECT_FIELD_ENTRY slot reads 0 for creatures on this client,
as it does for items, so it is documented as unusable. Pet GUIDs pack the pet
number, not a template, so Guid::CreatureEntry now returns 0 for them; pets
in view report their real template from the block.
Verified in-game on Turtle: Gordunni Shaman / Battlemaster / Mage-Lord read
5236 / 5238 / 5239 while all three carry 5236 in the GUID.
|
||
|
|
108a91a843 |
feat(api): describe the API in the code, browse it with /classicapi
docs/API.md is 18.7k lines and its anchors are signature slugs, so editing a signature breaks the 281 internal and 88 README links into it. Blizzard solved the same problem by shipping the API as data plus a browser addon, and warcraft.wiki.gg renders those same tables. One set of descriptors can feed all three. Each registration now carries one next to it: typed arguments and returns, nilability, defaults, and a sentence. Events, enumerations and returned table shapes are described the same way. No Script_* body changed. Game.h gains namespace Game::Doc (Field with Req/Opt/Vararg, FieldList, Function, Method, Structure, Event) and a trailing descriptor argument on all six Game::Lua::Register* plus Event::Custom::AutoReserve. api/Documentation.cpp records every registration on the first pass of each Lua state and seals it, so /reload re-registers but records nothing and the environment comes from which pass saw the name. Export is lazy: nothing is materialized until C_APIDocumentation.GetSystem asks for one system, because eager export would build thousands of tables per reload for a slash command. _classicapi_UndocumentedAPI lists what still has no descriptor. AddOns/!!!ClassicAPI/APIDocumentation/ ports Blizzard_APIDocumentation's eight files. Named /classicapi, not /api: the browser holds only what ClassicAPI adds, so /api would have implied the whole 1.12 surface. Two deliberate differences from the retail source, both noted in the file headers: a system search also matches its namespace, since that is the name people type; and an undocumented function says so, because an empty signature otherwise reads as a real one. LinkUtil gains the SetItemRef to LinkUtil.ProcessLink dispatch. That handler registry was dead code; a new link type now registers a handler instead of stacking another SetItemRef override. Also defines assertsafe there, which was undefined and only mattered once the function went live. Documented: the C_Spell and C_SpellBook surface, the spell globals, both spellbook enumerations, the 13 UNIT_SPELLCAST_* events, GameTooltip as the widget-method example and C_Glue as the glue and mixed-environment example. 66 of 692 function registrations. tools/New-ApiDocSkeletons.ps1 emits paste-ready skeletons to make the rest of the sweep tractable. Verified in game: the browser lists, searches, and prints detail; links render and click through; right-click copies and shift-click dumps. |
||
|
|
b4e5f4f2bb |
feat(spell): fill notInterruptible in UnitCastingInfo / UnitChannelInfo
Mirror the 3.3.5 client's own predicate on 1.12 data instead of pushing a placeholder false. The value is relative to the local player: the spellbook is folded into school masks of INTERRUPT_CAST spells and MOD_SILENCE spells (cached on Player::StatSignal), a cast is not interruptible when its PreventionType is not SILENCE or when the caster's immunity auras (effect / state / school / mechanic immunity) strip every mask, and the flag is never reported while the player knows no interrupt or silence at all. 1.12 differences, each documented in Spell::Interruptible: the vanilla server's interrupt effect also needs InterruptFlags 0x02 (channels: ChannelInterruptFlags 0x04), which silence ignores; SMSG_SPELL_START carries no immunity block, so creature_template interrupt immunity stays invisible; Feral Charge's interrupt lives on its triggered spell, so the capability walk follows one TRIGGER_SPELL hop. Verified in-game: a paladin's Holy Light reads false, flips to true while Divine Shield is up, and back to false when it drops. Also centralize SPELL_EFFECT_APPLY_AURA in Offsets.h (was local to Level.cpp). |
||
|
|
390eec4749 |
docs(nameplate): correct stale frame-identity and token-shift prose
The API.md nameplate prose contradicted the code in two places, and both were resolved in the code's favor after verifying it. Frame identity: the prose claimed default engine plates get a fresh wrapper table per call with unstable identity. UI::FrameObject::Push has one path for both plate kinds — a registry ref at +0x08 of <= 0 delegates to the engine's ScriptRegister, which builds the wrapper and writes the ref back, so every later call rawgeti's that same cached slot. Identity is stable for both kinds and addon-set fields survive. Dropped the registry/metatable mechanics that only existed to justify the wrong claim; kept the pooled-frame caveat, restated with its real reason. Token stability: the NAME_PLATE_UNIT_ADDED note said "nameplate3" can resolve elsewhere after a slot "vacates and shifts". AssignSlot never shifts an entry and removal frees the slot in place. Kept the advice to store the GUID, fixed the reason: slots are reused, never shifted. Also fixed the iteration example one code block above the slot rules, which broke at the first vacant slot and so missed every plate past a gap in the sparse slot array. |
||
|
|
f86893c8ef |
fix(spell): make IsUsableSpell the engine's own usability verdict
IsUsableSpell hand-rolled five checks (known, alive, off cooldown, power, reagents) and diverged from IsUsableAction exactly where the engine does more or less: it never tested stance, so Whirlwind in Battle Stance reported usable, and it tested cooldown first, so a 0-rage Whirlwind on cooldown reported (nil, nil) instead of (nil, 1). IsUsableAction reads a per-slot cache that FUN_004E5050 fills by calling FUN_SPELL_IS_USABLE (0x006E3D60) on a player spell's record, or by comparing the pet's power against the spell cost for a pet spell. Run those same two paths live, plus the knowledge gate the helper lacks. Cooldown is not part of usability: the helper has no cooldown branch and FrameXML greys and swipes as separate states. Item::Usable had been calling the helper all along. A pet-book spell given by ID now takes the pet path instead of always reporting not usable. Offsets.h: add VAR_PET_GUID and FUN_PET_ACTIONS_USABLE (verified via Script_HasPetUI, Script_GetPetActionsUsable and the recompute's pet branch); replace the FUN_SPELL_IS_USABLE comment with the decompiled check list. Fixes #51 |
||
|
|
b0585f5628 |
feat(sound): add C_Sound.PlayVocalErrorSound and Enum.Vocalerrorsounds
Plays the player character's spoken complaint for an error ("my bags are
full") in their race's and sex's voice.
This one maps exactly rather than approximately: VocalUISounds.dbc's
second column IS the modern Enum.Vocalerrorsounds value, and this build
carries every one of the 68 values (0..67) across 11 races. Verified by
resolving a row's sound ids to their SoundEntries names — row
{9, 0, 1, 1875, 1999, ...} is error 0 for race 1, whose normal sounds are
HumanMale_InventoryFull and HumanFemale_InventoryFull, which also pins
the column layout {id, enum, race, normal[male,female],
pissed[male,female]}.
The row lookup is ours because the engine has none: it loads the table
and never reads it, the only xrefs to its records/count globals being the
loader's own writes. Only the indexing is reimplemented — the same
records[id] walk every other DBC reader here performs — while the play
goes through the engine's by-id player like every other sound.
Race and sex come from the login-time bytes rather than the unit
descriptor, so this resolves from addon-load onward, before the in-world
player object exists.
The "pissed" column is deliberately unused: many of the ids it names
match no SoundEntries row. Combinations with no recording play nothing
and raise nothing, which is data rather than an error.
Verified in game on a Night Elf female: Inventoryfull, Outofammo and
Nomana each played, and GetRecentSoundFiles showed
Sound\Character\NightElf\NightElfFemaleErrorMessages\... — right race and
sex — with varying file suffixes, so the engine's weighted variant pick
is running too.
|
||
|
|
6013d8b937 |
feat(sound): add C_Sound.PlaySoundWithOptions and GetSoundScaledVolume
The table form of PlaySound, and the volume reader that falls out of it. Four of the params' six fields map onto something real here. volumeOverride is the one that needed finding. FUN_00458DA0(row, stream, scale) computes the row's own volume times `scale` and hands it to FUN_007A5DC0, which stores it on the stream and pushes it to FMOD. The by-id player already calls that with a scale of 1.0 immediately after starting a sound, and the setter writes rather than accumulates, so calling it again with another scale simply replaces the volume — no touching FMOD directly, and no reimplementing the row's volume rules. Resolving the row needs FUN_0045CDA0, since the applicator is a method on it. GetSoundScaledVolume then comes free: that same setter writes the result to stream + 0x74, so the field IS the scaled volume. Read only off a stream still in the engine's live list, because a finished stream's memory returns to a pool. soundKitID and runFinishCallback were already supported. uiSoundSubType, forceNoDuplicates and overridePriority are accepted and ignored, and the docs say so rather than implying they work. Params are read with GetTable rather than a raw read, so a table built from a mixin still resolves. Verified in game: volumeOverride 0.1 is audibly quiet against 1.0, runFinishCallback still fires SOUNDKIT_FINISHED through this path, and GetSoundScaledVolume returned 0.276 for a volumeOverride of 0.4 — the row's own 0.69 scaled, which is what confirms the multiplier semantics the docs describe. |
||
|
|
27277b11aa |
feat(sound): fire SOUNDKIT_FINISHED when an opted-in sound ends
Reports the handle of a finished sound, and only for sounds played with `runFinishCallback` — the documented contract, and what makes it affordable: without the opt-in every footstep and UI click would dispatch a Lua event. Detected on the world tick, NOT from FMOD's end callback. That callback (FUN_007A53C0) runs on the STREAMER thread — it opens with EnterCriticalSection and only moves the stream onto a deferred list — so firing an event there would call into the Lua VM off the main thread. The engine reclaims those streams in FUN_007A4B60, from the per-frame sound update that ends in FSOUND_Update, so noticing completion on the main thread is what the engine itself does. A tracked entry keeps the stream's path hash (+0x80, stamped at creation by FUN_007A51D0) beside the handle. Stream objects come from a pool and their addresses are reused, so "still in the live list" is not enough on its own — a later sound can land on the same address and the event would be deferred to the wrong one. Comparing the hash narrows that to the same file replaying at the same address, for one extra dword read of a node already found alive. C_Sound.PlaySound takes the remaining modern arguments: channel, forceNoDuplicates and runFinishCallback. forceNoDuplicates is accepted and ignored — the engine's by-path dedup is bit 0 of a flag word the row itself supplies, and the by-id player ORs in only bits 1, 2 and 4 from its arguments, so asking for it would mean bypassing that player and re-deriving its weighted file pick. The global PlaySound deliberately does NOT take those: it gains the SoundKitID form and nothing else, staying as close to the global it replaces as the extension allows. |
||
|
|
ccfdb8309c |
feat(sound): add C_Sound.PlayItemSound and Enum.ItemSoundType
Plays the noise an item makes when handled, without performing the action. Almost all of it is the engine's: FUN_00457FF0 takes a sound type and an ItemDisplayInfo id, walks that row's ItemGroupSounds id at +0x2C, and plays the SoundEntries id at row + 0x04 + soundType * 4. Those four columns are in the modern Enum.ItemSoundType order (Pickup, Drop, Use, Close), which makes this a backport rather than a reconstruction — the module only turns an item argument into a display id and calls it. The item argument takes an ItemLocation table and the other forms this project's item APIs accept (GUID string, itemID, link, carried item name), through the shared ResolveItemArgOrLocation. Sound type is read off the stack before resolving, since that resolver drives PackBagSlot and stomps the stack. Only Pickup and Drop play on a stock client, and that is the data rather than a limit of the code: of ItemGroupSounds' 24 rows, the 18 any display row points at (~29,000 displays) populate columns 1 and 2 only. The six rows naming a Use sound are unreachable or broken — groups 1 and 2 have no displays, and the ids they name (273/274/275) match no SoundEntries row — and the Close column is zero in all 24. Use and Close fall through the engine's own bounds checks silently, which is why no item can test them; documented so nobody hunts for one. The type stays a pass-through that indexes the row rather than selecting a branch here, so a server filling those columns in gets Use and Close working with no change on this side. Sound keys off the item's display, not its itemID, so items that look alike sound alike — the engine's grouping, and why every item has a sound without one being recorded per item. Verified in game: Pickup and Drop play, and differ by material. |
||
|
|
be7a8a0dd7 |
feat(sound): mute sound files, play by SoundKitID, and report what played
Five additions, one hook between them.
MuteSoundFile / UnmuteSoundFile stop a given audio file from playing.
They co-hook FUN_007A5450, the single funnel every sound passes through:
this client links only FMOD's FSOUND_Stream_* API, so even a one-shot UI
click is a stream opened by path, and the six callers of that function
are the whole surface (PlaySoundFile, the SoundEntries player behind
PlaySound and every DBC-driven spell/creature/UI sound, and four
music/ambience paths). A muted path returns NULL, which is the engine's
own "did not play" answer — its pre-play gate FUN_007A66A0 already
returns that at a category's concurrency cap, so every caller handles it
and PlaySoundFile already reports it to Lua as nil. Suppressing the open
rather than muting an opened stream means nothing is decoded for a sound
nobody hears; the only visible difference is PlaySoundFile returning nil
instead of 1 for a muted file.
C_Sound.GetRecentSoundFiles reports the last 64 DISTINCT files played,
newest first, with a timestamp and a muted flag — the other half of
muting, since hearing a sound tells you nothing about which of ~9,800
files it was. Always on: the hook exists for muting anyway, recording is
one bounded copy into a preallocated slot on a call that is about to open
and decompress an archive file, and a switch would have to be armed
before hearing the sound you wanted to identify.
C_Sound.PlaySound plays a SoundKitID and returns willPlay, soundHandle. A
SoundKitID is a SoundEntries row id — the same ids the modern client's
SOUNDKIT constants carry, verified against this build's table (850
igMainMenuOpen, 8959 RaidWarning, 8960 ReadyCheck). The engine has always
played by id internally; the only Lua door was the row's NAME string, so
addons written against SOUNDKIT ids could not play a sound here. The
global PlaySound is therefore widened to a strict superset: a number
plays by id, anything else tail-calls the engine with the stack untouched
so the name form is unchanged. It discriminates on lua_type rather than
lua_isnumber, which would also accept a numeric string and reroute
PlaySound("123") away from the name lookup.
C_Sound.IsPlaying validates a handle by walking the engine's live stream
list rather than dereferencing it, since a stream object is freed when
the sound ends, with a step guard so a corrupt list cannot spin inside a
Lua call. Handles are pooled and reused, which the docs call out.
FUN_0045CE60's ABI came from the disassembly, not the decompile — Ghidra
maps its arguments inconsistently against the call site. Re-registering
the PlaySound global was contention-checked first: no sibling DLL
references Script_PlaySound (UnitXP_SP3's "PlaySound" string is the winmm
PlaySoundW import), and the one addon that reassigns the global does a
temporary swap and restore around its own calls.
Verified in game: play by id and by name, mute and unmute across both
entry points, IsPlaying true while a sound runs, and the recent list
filling from sounds the player never triggered (zone music, quest log,
enter world) — which is what shows the funnel sees everything.
|
||
|
|
f9167bac06 |
feat(export): add ExportSoundFiles, and sweep every archive when enumerating
ExportSoundFiles is a sibling of ExportDBCFiles that dumps the MPQ `Sound\` tree to `BlizzardSound\`, with an optional subpath prefix (the full tree is ~3.5 GB and the command runs synchronously). All 8803 SoundEntries rows have a `Sound\...` DirectoryBase, so that one root is the whole corpus. Writing it exposed a bug in the existing enumeration. FUN_00401470's `archiveSelector` is not an archive SET, as the old comment assumed — it is an INDEX into a fixed 10-entry table built by the mount function FUN_00403740 (model, texture, terrain, wmo, sound, misc, interface, fonts, speech, dbc). Each call walks the primary patch archives plus exactly ONE of those ten. We passed 6, copied from the macro-icon loader, so ExportInterfaceFiles was complete only by coincidence — interface.MPQ IS selector 6 — while the sound export opened neither sound.MPQ nor speech.MPQ and silently lost 6,561 of 9,535 indexed files. EnumAllArchives now sweeps all ten, deduped; ExportInterfaceFiles and ExportDBCFiles go through it too. The sound export also unions the archive index with SoundEntries.dbc, because each source holds files the other lacks: an MPQ resolves a read by hashing the path, so the index is optional and 299 shipped files are named by no index at all, while ~300 more (character combat grunts, doodad audio referenced from model data) appear in no SoundEntries row. Same shape ExportDBCFiles already uses to union the index with its .text path-getter scan. Failed reads and writes are now counted and logged rather than silently skipped. A path only reaches that point because an index or the sound table named it, so a failure means the two disagree — which is worth seeing, and was invisible before: the console line gains "(N skipped)" and each path goes to Logs\classicapi_debug.log. Also: no extension filter (the tree is all audio, and the table carries typo'd names like Kill3.wac a whitelist would drop), and the destination is BlizzardSound\ rather than Sound\ — the loose-file VFS index resolves loose files ahead of archives, so exporting into Sound\ would shadow the MPQs with gigabytes of duplicates re-indexed at every launch. Verified on the 1.18.1 build across both fixes: 3,184 files with the original selector, 9,526 once SoundEntries was unioned in, 9,818 with the full archive sweep, and no zero-byte writes. ExportInterfaceFiles holds at 340/340 and reports nothing skipped, so neither it nor ExportDBCFiles was ever affected. Two residuals on this Turtle build, both logged rather than hidden. The 12 `Sound\Creature\Elekk\*` + 4 `Sound\Creature\ElekkPet\*` reads fail because SoundEntries names a folder the archive does not use (the files sit flat in `Sound\Creature\`) — a data inconsistency, not a code fault. Separately, patch-7's 14 flat `Sound\Creature\Elekk_*.wav` index entries do not reach disk, and that one is NOT explained: patch-7's listfile is demonstrably walked (its 4 exclusive interface-code files exported, and interface has no DBC fallback), yet those entries produced neither a file nor a logged failure. |
||
|
|
7d82ae8e92 |
docs: record where hooksecurefunc's unhookable list comes from
`kUnhookableNames` is patch 11.0.0's own refusal list, verbatim — the 23 taint-relevant names, not "the Lua base library". Nothing said so, and with the base-library luaL_reg table (0x00811E28, 36 entries) freshly catalogued the two looked like they should be one thing. They must not be: 11.0 leaves tostring / error / loadstring / assert / collectgarbage hookable and includes secure-family names that are no base function, so deriving the list from the engine table would move it away from the client. No earlier client on this machine (3.3.5, 4.3.4, 5.4.8) has the restriction at all. Say so in the source comment and the TODO section, and list the names in the API reference, which documented every other hooksecurefunc error but this one. Comment-only in the source; no behavior change. |
||
|
|
d98d3a9fd5 |
perf(luasyntax): decide with one allocation-free lex before tokenizing a chunk
The load hook gated on raw bytes — memchr for `#`/`%` and three scalar scans for `...`, `0x` and `[=` — then tokenized the whole chunk, and RewriteAll threw the token vector away when no operator token existed. A `%` in a format string or a `...` in prose was enough, so nearly every addon file and every SavedVariables file paid for a token stream it did not need: on the dev install, 723 of 1391 addon files (101.6 of 160 MB on disk) and 39 of 301 SV files (16.7 of 16.9 MB). The lexer is now `Lex<Sink>`, one body with two sinks. RunPasses runs it first with `FlagSink`, which records only whether `#`, `%`, `...`, a hex number or a leveled long bracket occurred outside a string or comment, and tokenizes only when one did. The byte gates are gone (the flag lex replaces all five) and RewriteAll takes the flags instead of rescanning the tokens. A chunk with no real 5.1 syntax costs one pass and no allocation, and the string/comment skipping stays single-sourced. Measured in game after the change: 40,921 chunks / 19.1 MB per login, 112 chunks / 2.8 MB tokenized, 39 ms inside the rewrite in total. `_classicapi_TranspileStats()` exposes those counters. |
||
|
|
3d91ae0f91 |
feat(macro): show a spell named by ID even when it is not learned
`#showtooltip 1539` resolved its value through the engine's name resolver, which maps a name -- or an ID through its name -- to a spellbook SLOT. An unlearned spell has no slot, so the directive matched nothing and the button greyed with no icon and no tooltip. That constraint is right for `/cast`, which needs the slot, but an icon and a tooltip need only the spell record. A value the directive names itself is the author saying what to display, so it now falls back to `Spell.dbc` when the spellbook has no slot for it. The button still greys, because the spell still cannot be cast, which is the honest result: the macro shows what it is about and says you cannot use it. `spell:N` is accepted as the display counterpart of `item:N`, and skips the spellbook unconditionally -- nobody writes that form except to name a spell for display. A bare number skips it only for an explicitly named value; one lifted off a `/cast` line resolves exactly as far as that cast would, which keeps the bare form honest about what the macro will actually do. A spell NAME cannot be rescued either way, since 1.12 has no name-keyed spell index. The name path still runs first, so a learned spell keeps its real spellbook resolution including the pet-book flag; the record fallback only sees what the spellbook rejected, and forces `isPet = 0` there, a pet slot being exactly what just failed to resolve. The 1..19 inventory-slot rule stays ahead of it, so `#show 13` is still slot 13 rather than spell 13. A published value counts as explicit for the same reason a directive's does. |
||
|
|
0ba73b8457 |
fix(macro): leave GetMacroInfo alone and expose the shown icon instead
`Macro::Info` re-registered `GetMacroInfo` to answer with the icon a `?` macro's `#showtooltip` resolved to, so the Macro UI's list would match the action button. But the Macro UI seeds its icon selector from that same return -- `MacroFrame_Update` sets `MacroPopupFrame.selectedIconTexture` from it, and `MacroPopupFrame_Update` checks the grid button whose name matches -- so opening the selector on such a macro pre-selected an icon the player never picked, in the one place that edits the stored icon. Saving was never at risk (an unclicked selector leaves `selectedIcon` nil and `EditMacro` then leaves the icon alone), but the display was wrong. One return cannot serve both readers, and the one that edits state has the stronger claim, so `GetMacroInfo` goes back to the engine's: it is the read side of the icon `C_Macro.EditMacro` writes, and every addon that captures it now gets the stored icon. The icon a macro SHOWS becomes something a caller asks for, through `C_Macro.GetMacroIcon`. It reads the macro icon getter this DLL already hooks, so the `?` rule and the active-icon choice stay in one place and the answer matches what the action button and the drag cursor get. Util/MacroIconSelection.lua paints the Macro UI's list buttons from it after the stock update, which is additive -- an empty slot or a macro with a chosen icon answers with what stock already drew. Dropping the re-registration also takes a commonly captured global out of the override chain. |
||
|
|
d9565fbe1f |
fix(macro): correct macro display on a client that owns the macro tooltip
SuperWoW detours the engine's macro tooltip builder and reimplements 3.3.5's version of it: build the cached spell's tooltip when the macro's primary-spell field is non-zero, else add the macro's name. We write that field on purpose -- it is what makes a macro button's icon, cooldown, range, usable and auto-repeat state correct -- so every display fault here traced back to what we put in it. Four things were wrong: - `MaintainExternal` re-derived "the publisher had a value" from `kind != None`, but `Publish` marks a claimed macro `ShowTooltip` either way. One tick after `SetMacroDisplay(i, false)` it turned the publisher's "nothing matched" (cache 0, button usable) into "named an unknown spell" (the grey-out sentinel), and then stuck, because the next pass saw the sentinel already in place. The button greyed and the tooltip came up empty -- a spell tooltip for -1 builds nothing and skips the name. `matched` is stored on the entry now. - A publisher's claim outlived the publisher. `PrepareForReload` never cleared the entries and `Rescan` deliberately never re-reads a claimed body, so an addon disabled between reloads left every macro it had ever claimed frozen on its last answer. Claims are dropped on reload, each one handed back to the engine's own parse first. - `LookupEntries` scanned all 36 entries for a matching macroID. That is not sound: `FUN_MACRO_CREATE` bumps one of two per-scope counters, so a general and a per-character macro can share an id, and an entry whose slot-map index shifted keeps its old id until the next rescan. Either way another macro's resolution could answer. It resolves through the slot map now and requires the entry to still agree. - `#show` is icon-only, but the tooltip is not ours to delegate on this client: the field we fill for the icon is the same one the detoured builder reads, and it cannot carry the distinction. `#show` builds the macro's name through `Script_SetText`, which runs the identical sequence. `Publish` takes the kind from the body for the same reason, so a claim can no longer promote `#show` to `#showtooltip`. Also: a resolved spellID with no Spell.dbc row goes back to the engine rather than rendering an empty frame, matching the fallback 3.3.5's own macro tooltip ends with; and a conditional directive keeps its tooltip refreshing while hovered, which is 3.3.5's `dynamic | built` return. |
||
|
|
f895d38f76 |
refactor(time): drop C_DateAndTime.GetServerTime for the ClassicAPI mirror
The alias existed because FrameXML takes the GetServerTime global on some clients, and an addon needs some reachable name for the epoch. ClassicAPI.GetServerTime now is that name, so the hand-made one is redundant surface. Docs now point the GetServerTime caveat at the mirror. |
||
|
|
804ed61c76 |
feat(lua): mirror every registration under a ClassicAPI namespace
Registering a Lua name is not the same as owning it. The engine's
registrars write _G, FrameXML and every addon load after that, and
whoever writes last wins with no error and no way for the loser to
notice. GetServerTime is the case on record: a client whose GameTime.lua
declares its own replaced ours on every login, and no addon could reach
the C function again.
Every name RegisterGlobalFunction, RegisterGlueFunction and
RegisterTableFunction bind is now also bound under _G.ClassicAPI, which
nothing else writes -- ClassicAPI.GetServerTime,
ClassicAPI.C_Item.IsBound, namespaces kept as subtables. All three
registrars are single chokepoints, so this is one helper covering 637
in-game registrations plus 26 glue ones, and anything added later needs
no extra call.
Two properties, both of which depend on doing this in the registrar
rather than from addon Lua:
- It runs at registration, so the mirror captures OUR function. A
Lua-side `ClassicAPI.X = X` in the embedded addon would run after
FrameXML and faithfully copy the clobbered version.
- It mirrors by VALUE, reading the name back rather than making a
second closure over the same C function. So `ClassicAPI.X == X`
holds until something replaces the global, and the pair doubles as a
clobber check:
if GetServerTime ~= ClassicAPI.GetServerTime then ... end
EnsureSubTable uses raw get/set so a namespace carrying a metatable
cannot divert either side, and MirrorRegistration restores the stack via
an absolute gettop rather than counting pops.
Not covered, and documented as such: frame methods, which live in the
per-frame-type registry rather than _G, and RegisterIntegerEnum values,
where a mirror would be a stale copy rather than a reference.
Verified in-game: ClassicAPI.GetServerTime == GetServerTime is false on
a client whose FrameXML takes that global, while
ClassicAPI.GetTimeCached == GetTimeCached is true.
|
||
|
|
aacace1645 |
fix(time): take the server clock from the engine's own time sync
`GetServerTime` had two separate problems. The global is not ours to keep. Registration happens inside FUN_LOAD_SCRIPT_FUNCTIONS and FrameXML loads after that, so a client whose GameTime.lua declares its own `function GetServerTime()` replaces it on every login and every /reload, and no addon can reach the C function again. Turtle's version returns (serverHour, serverMinute) -- which is itself just the player's own machine clock, since it only diverges from date() if the realm answers a TW_TIME broadcast, and nothing in the public core does. Add C_DateAndTime.GetServerTime: the same C function under a name FrameXML never touches. The value was also the wrong clock. It was rebuilt from the broadcast gametime struct, which carries minute granularity, no timezone, and whatever per-map time offset the realm folded in before sending. The engine already has a better source: FUN_004DE430 (quest-log enter-world setup) sends CMSG_QUERY_TIME unconditionally, and the handler caches localEpoch - serverEpoch at VAR_SERVER_TIME_DELTA, refreshed hourly. The reply is a raw time(nullptr), so the skew yields a true UTC epoch with real seconds, free of both the timezone and the map offset. The sign is confirmed by the engine's own use of it -- the timed-quest expiry check in FUN_004DE510 converts a server timestamp into local-clock space by ADDING the delta. The two clocks answer different questions, so Time::Server exposes both and each function takes the one its contract names: CurrentEpoch() the instant GetServerTime, GetSecondsUntilDailyReset RealmClockEpoch() the display GetCurrentCalendarTime, GetServerTimeLocal GetServerTimeLocal is "the epoch offset by the server's timezone", which RealmClockEpoch already is by construction, so its round-trip through the *player's* timezone is gone. GetSecondsUntilDailyReset moves the other way: a realm buckets its day over a raw time(nullptr), so the boundary is UTC midnight rather than the displayed midnight. Verified in-game across an Azeroth -> Kalimdor port on two realms, one on UTC and one on UTC-5. The instant stays continuous (+51 s across the port, matching real elapsed time) while GetGameTime jumps 12 hours; GetServerTimeLocal tracks the realm's displayed clock and rolls the date with it; GetCurrentCalendarTime's hour matches GetGameTime in both zones. Fixes #50 |
||
|
|
4bf2c17e0f |
docs: repair the broken links in the API reference
Fourteen anchors in docs/API.md pointed at headings that do not exist, so those links went nowhere. Most are typos in the slug (`doesitemexit`, `gettaxiroutep`, `afters`) or a slug that was never updated after its heading gained an argument (`C_Spell.CastAtUnit` picked up `placeGroundSpell`, `GetItemSubClassInfo` picked up its `C_Item` twin). `C_QuestLog.GetQuestIDForLogIndex` was written in camel case, which a heading anchor never is. `GetMacroInfo` has no entry of its own in this document, so that reference is now plain text instead of a link to nothing. Also correct the table-of-contents text for `C_Spell.CastAtUnit`, which still named the two-argument signature. Found while checking the anchors this branch touched. A pass over both documents now reports no broken link, in either direction. |
||
|
|
3191c4dcfc |
fix(macro): leave a macro alone when its conditions are not ours
A macro written for another macro addon names conditions this parser does not have. SuperCleveRoidMacros' `[alive]` is one. The `#showtooltip` reader scans every macro body, so it met that condition and printed "SecureCmdOptionParse: unknown condition 'alive'" at login, about a macro that works. Worse than the noise: a condition we cannot evaluate cannot pass, so its group fails. A macro with no unconditional fallback clause then matched nothing, and the reader wrote 0 over the spellID the engine's own parse had put in the per-macro cache - costing that button its cooldown, range and usable state. `SecureCmdOptionParse` now records the keywords it does not own on its cached parse and returns the first one as a third value. It still names them, once each, for a caller the player drove. The reader passes the new `quiet` flag and reads that value instead: on a foreign dialect it resolves nothing, writes nothing, and stops re-evaluating until the body changes. The engine's parse stays in the cache, and the addon that owns those conditions can still claim the macro through `C_Macro.SetMacroDisplay`. Such an entry also stays out of `dirty`, because a dirty entry nothing evaluates would keep `HasPendingWork` true and re-run the catch-up on every reader. Moving the warn-once bookkeeping from parse time to call time also stops a quiet scan from consuming the one warning a keyword gets, so a condition the player later types is still named. This is not a load-order fault. SuperCleveRoidMacros creates `_G.CleveRoids` at the top of Init.lua, and that same file sets `ClassicAPIMacroDisplay`, which lifts our wholesale yield on purpose so it can publish per macro. The window is between our first tick and its first publish, which it cannot make until the player object exists. |
||
|
|
4c7f1c36d6 |
docs: cover the macro conditions and #showtooltip in the README
The README said nothing about either, and its Macros section still claimed the macro work added no Lua functions. Two rows in Modern client behaviors: the `[conditions]` / `@unit` syntax across 38 slash commands, and what `#showtooltip` drives on a macro button. The Macros section now lists the body forms a macro accepts - the directive, the conditions, the `!Name` prefix, and `#` comment lines, which no longer reach chat. A new Slash commands section names all 38 commands by group, and the bundled-addon section mentions that it holds them and `SecureCmdOptionParse`. `C_Macro.SetMacroDisplay` and `GetMacroItem` join the Macros row of the call table, and `IsUnitToken` / `UnitTokenFromName` join the Unit row. Also add the `## MerchantFrame` heading the README already linked to. The six `C_MerchantFrame.*` entries sat inside `## Map` with no heading of their own, so that link went nowhere. |
||
|
|
36c367a6d6 |
feat(macro): native #showtooltip, conditional slash commands, and a display API for addons (#48)
* feat(macro): native #showtooltip/#show, conditional /cast and /use
Backport the macro display system to the engine:
- Macro::ShowTooltip resolves `#showtooltip [cond] value` / `#show` (and
the bare form over the macro's /cast and /use lines, first match wins)
through the Lua SecureCmdOptionParse on the world tick and writes the
result into the engine's per-macro spell cache (+0x564/+0x568), which
every action reader already consumes. Repaints go through the engine's
own slot-changed notifier. Item targets are cached alongside.
Re-evaluates on modifier / mouseover / target / combat changes and every
200 ms otherwise. Yields entirely when SuperCleveRoidMacros is loaded.
- The `?` icon rule is a co-hook on the macro icon getter, so action
buttons and the drag cursor both show the resolved icon; GetMacroInfo,
GameTooltip:SetAction, GetActionCooldown, GetActionCount and
IsConsumableAction are re-registered in front of the engine for macro
slots with a resolved directive and tail-call it otherwise.
- Macro::RunBody re-implements the engine's body runner so `#` lines
never reach chat; Macro::Execute skips them too.
- /cast and /use in the embedded addon (3.3.5 handler shape): items
first, [@unit] via C_Spell.CastAtUnit, 1..19 = inventory slot, other
numbers stay spellIDs. SecureCmdOptionParse: a group whose only piece
is @unit passes only while the unit exists (3.3.5 passes it
unconditionally). C_Item.GetItemCount accepts names. GetMacroItem
added; GetMacroSpell now reflects the directive.
- Offsets.h: 0x004F0F40 was the macroID -> MacroEntry resolver,
mislabeled as a spell-state cache; renamed FUN_MACRO_ID_TO_ENTRY, with
the action-bar / macro-runner constants this needs added and verified.
* feat(unit): add IsUnitToken for token-vs-name dispatch
Vanilla ships each "act on a unit" verb as two Lua entries, one taking a
unit token and one taking a character name, and each errors on the other
kind of input. Conditional slash commands need to accept either, because
SecureCmdOptionParse hands back a token for `/target [@focus]` and a name
for `/target Bob` through the same variable.
IsUnitToken(value) answers which one a string is by calling the engine's
own token resolver inside lua_pcall and reporting whether it returned or
threw. Consulting the resolver rather than matching a token list here
keeps the answer correct for every family it knows, including this
client's native GUID literals and our own focus / nameplateN / markN,
which unit/TokenExtensions.cpp hooks onto the same function. It reports
whether the text names a token, not whether a unit is there, so "target"
with nothing targeted is still true.
if IsUnitToken(x) then TargetUnit(x) else TargetByName(x) end
Additive on purpose. Merging the pairs into a modern TargetUnit /
AssistUnit / FollowUnit was written and reverted: those names are common
enough for other DLLs and addons to replace that re-registration is
silently last-one-wins, with no error and no trampoline to chain. That
mechanism is only safe for names nobody else contends.
Offsets.h records two findings from decompiling the resolver. The throw
is raised by its callee, not by FUN_RESOLVE_UNIT_TOKEN, which is a thin
wrapper returning 0 on a miss. And this client reaches the error through
a patched fallthrough that first parses a 0x<hex> GUID literal, so GUID
unit tokens resolve natively with no SuperWoW loaded.
* feat(addon): backport the conditional slash commands
Adds 27 commands that take [conditions] through SecureCmdOptionParse,
following the 3.3.5 SecureCmdList handler shapes: targeting
(/targetexact, /cleartarget, /targetlasttarget, /targetlastenemy,
/targetenemy, /targetfriend, /targetenemyplayer, /targetfriendplayer,
/targetparty, /targetraid), casting and state (/stopcasting, /cancelaura,
/cancelform, /dismount), equipment (/equip, /equipslot), action bars
(/changeactionbar, /swapactionbar) and the pet family.
/target, /assist and /follow gain conditions by replacing the FrameXML
handlers, which is the only way to add them. Plain input keeps working:
each routes through IsUnitToken to the engine's token entry or its
by-name entry, where the old handler always went by name.
Three natives have a different shape here than the 3.3.5 handlers assume,
so those bodies are not straight ports. PetAttack takes no target and
always sends the pet at the current target. ChangeActionBarPage takes no
page and only fires ACTIONBAR_PAGE_CHANGED, so CURRENT_ACTIONBAR_PAGE is
set first. Pet autocast is a spellbook-slot toggle with no enable and
disable entries, so a name is resolved against the pet book and on/off
read the state and toggle only on a mismatch. /cancelaura needs no buff
walk, since CancelSpellByName already matches a player buff by name.
Not ported for want of a native: /targetlastfriend, and the /mt and /ma
family, which needs party assignments the client does not have.
Command names come from the locale files, with the localized name taken
from the matching Blizzard GlobalStrings and the English one alongside.
zhCN ships English for all 27. The short aliases retail also carries
(/eq, /tar) are left out, since those collide with addon shorthands.
* fix(macro): resolve item link values in #showtooltip
`#showtooltip` over a `/use item:6948` line showed no icon. The directive
evaluator built its item argument as `{0, 0, value}`, hardcoding the whole
value as a NAME, so the `item:N` and pasted-link forms could never match
and the value fell through to the spell lookup. `/use` itself was fine,
which is why the macro cast the item but the button stayed blank.
The link parsing already existed for the Lua entries. Split it out as
`Item::Arg::ResolveString` and call that instead of hand-building the
argument, so both paths read the forms the same way.
A bare number keeps skipping the item lookup: `ResolveString` reads "6948"
as an itemID, and a bare number that is not an equipment slot has to stay
a spellID per the documented `/cast 5019` rule.
An explicit itemID now also resolves when none is carried, falling back to
the ID after the bag and equipment walk misses. The icon and tooltip come
from the item record, and the tooltip path already handled an unheld item
through a hyperlink, so a macro naming an item in the bank or one written
before looting it now displays. Names are still limited to what you hold,
since 1.12 has no name-keyed item cache.
* fix(macro): make [@cursor] work in /cast, /use and #showtooltip
`/use [@cursor] item:4390` did nothing and its `#showtooltip` showed the
question mark. Two causes, both in the addon.
The parser rejected the clause. A group whose only piece is `@unit` passes
only while that unit exists, and the existence test ran UnitExists on
`cursor`, which is not a unit token. The engine raises there, the pcall
caught it, and the group failed. `none` was already exempt; `cursor` joins
it, since both name something other than a unit and neither can be tested
that way.
Nothing routed the target once it did pass. `SecureCmdUseItem` sent every
target through UseItemByName, which takes a unit GUID, and the spell branch
had only the on-unit and default cases. A `cursor` target now goes to
C_Item.UseAtCursor and C_Spell.CastAtCursor, which were written as the
`[@cursor]` analogs but had no caller. SecureCmdItemParse has already
turned a bag or inventory slot into a link, so one item call covers every
value form.
`#showtooltip` needed no change: the value resolves once the clause passes.
* feat(macro): accept a character name as a macro target
`/cast [target=Feral] Rejuvenation` did nothing. A `@unit` or `target=unit`
piece takes a character name as well as a unit token, but nothing here
turned a name into something the unit functions accept, and the existence
gate ran UnitExists on it, which raises for a string that names no token.
`UnitTokenFromName(name [, exactMatch])` runs the engine's own by-name
search — `FUN_UNIT_FIND_BY_NAME`, the one behind TargetByName, AssistByName
and FollowByName — and reports the winner as a token: a standard one when
it fits, else the GUID literal, which this client's resolver takes too. It
walks party, then raid, then every loaded unit, so it reaches units no
token names. Prefix match by default, nearest first, like TargetByName.
`GroupPasses` now resolves a named target once and uses it for both jobs:
the bare-group existence test (`[@Feral][] Spell` falls through when they
are not around) and the conditions, which previously raised on a name.
The pcall that already guarded the existence test is what tells a token
from a name, so a token target costs no more than before.
The slash handlers resolve the target the same way before handing it to
CastAtUnit, UseItemByName, FocusUnit or StartAttack, and drop the command
when a named unit is not around.
* feat(macro): drop a ground-target item at your feet with [@player]
`/use [@player] item:4390` sent the grenade through UseItemByName, which
passes a target GUID but never commits the placement, so the reticle came
up instead of the grenade landing. Route a `player` target to
`C_Item.UseAtUnit` instead, which commits at the unit's position.
Only your own position is offered. Aiming a ground-target item at another
unit is not something the game lets you do by hand, so the slash handler
does not either; every other unit target keeps going to UseItemByName.
`C_Item.UseAtUnit` now dispatches with the unit's GUID rather than a zero
target, matching `C_Spell.CastAtUnit`. Without that, routing `@player`
through it would have cost a non-ground item its target: a bandage or a
scroll would fire at the current selection instead of at you. It now
covers both kinds of on-use item, the way CastAtUnit covers both kinds of
spell.
* fix(macro): stop /cast [@unit] aiming ground spells at that unit
The same aim the item side was just denied: `/cast [@target] Flamestrike`
dropped the reticle on the target with no click, for any unit. Placing a
ground spell somewhere you could not put it by hand is aim the player has
not earned, and it should not differ between a spell and a grenade.
`/cast [@player]` and `/cast [@cursor]` still place, since both name a
position you can already aim at. Every other unit still has the spell cast
on it when it takes a unit target, which is what `@unit` is for; only the
ground-placement leg is dropped, leaving the reticle up exactly as casting
by hand does.
`C_Spell.CastAtUnit` keeps the whole primitive and gains a third argument
for the choice, defaulting to placing so the API and its callers are
unchanged. The restriction lives in the slash handler, the same split the
item side uses, where `C_Item.UseAtUnit` stays unrestricted and only
`/use` limits itself to `@player`.
`Spell::AtCursor::GroundPlacementPending` reports whether the in-flight
placement is a ground one, so the handler can leave that reticle up while
still cancelling a placement for a unit the engine did not accept. Leaving
that armed would strand the cursor.
* fix(item): stop a bad unit raising out of C_Item.UseItemByName
The optional `unit` went straight to the engine's token resolver, which
raises for a string that names no token. Both the code comment and the
API docs promised the opposite, that an unrecognized string is no target,
matching the no-op contract the item argument already has.
It is reachable without an addon calling the function directly: secure
button attributes and key bindings both feed it a configured string, so a
`unit="foo"` attribute raised inside a click handler.
Probe with Unit::TokenResolve::IsUnitToken first and treat anything else
as no target, which makes the documented contract true.
C_Item.UseAtUnit keeps raising, which is deliberate and now stated in its
docs: it is the companion of C_Spell.CastAtUnit, which documents the same
behavior as matching UnitHealth("garbage").
* feat(macro): let an addon publish its own macro display
`C_Macro.SetMacroDisplay(macroSlot, value)` hands a macro's resolution to
the display path, for an addon that parses macros itself. It is aimed at
SuperCleveRoidMacros, which today replaces ten action globals plus
GameTooltip.SetAction to put its answers on the bar, and borrows an
unrelated action slot to get a range check for a macro.
None of that is needed. Writing the spell into the engine's own per-macro
cache is what makes cooldown, range, usable, current and auto-repeat
correct for a macro button, with nothing overridden, and the icon then
reaches two places a Lua override cannot: the cursor while a macro is
dragged, and the macro window grid, both of which come from an engine
getter we hook.
The value takes the forms a `#showtooltip` value takes, so a publisher
hands over what it would have written in the macro. Three states: a value
to show, false for claimed-but-nothing-matched, nil to release.
Ownership is per macro. A published one is not parsed or re-evaluated by
us and is honored even while yielding; the rest stay ours. Nothing is
re-evaluated for the publisher, but a published resolution is re-applied
whenever the engine's re-parse overwrites the cache, so a macro edit
cannot silently replace it.
The wholesale stand-down when SuperCleveRoidMacros is loaded is lifted by
`CleveRoids.ClassicAPIMacroDisplay`, which a build sets to say it drives
us. Forks without it keep the all-or-nothing yield, so an older one cannot
end up fighting for the same buttons.
* fix(macro): accept the !Name prefix in /cast and #showtooltip
`#showtooltip` over `/cast !Shoot` showed nothing and the line cast
nothing: the `!` prefix, which asks for a toggle ability to be started
but never turned off, was never part of vanilla and no name resolver
knew it.
Split it in two.
The NAME is normalized in the one co-hook on the engine's single
name -> spellbook-slot resolver, which already owned the numeric
spellID form (so `spell/CastByID.cpp` becomes `spell/NameResolve.cpp`).
`FUN_RESOLVE_SPELL_NAME_TO_BOOK_ID` calls that resolver, so a single
strip covers the runtime cast, `Spell::Arg`, the `#showtooltip`
resolution and the engine's own macro parser -- which is what lets
`IsAutoRepeatAction` light a `/cast !Shoot` button even while macro
display is yielded to SuperCleveRoidMacros.
The BEHAVIOR stays in Lua: `SecureCmdCast` strips the prefix and casts
through the existing `CastSpellNoToggle`, which asks the engine whether
the ability is already up first. `CastSpellByName` cannot carry it --
re-registering that global is off limits and nampower already co-hooks
it.
`CastSpellNoToggle` gains the `placeGroundSpell` argument that
`C_Spell.CastAtUnit` has, so `/cast [@target] !X` keeps the rule that
only your own feet and the cursor place a ground-target spell. It also
tolerates a `!` on its own input now, since its auto-repeat name
compare is ours rather than the engine's.
* feat(item): take an item location wherever an item action takes an item
`C_Item.UseAtCursor`, `UseAtUnit`, `UseItemByName` and `EquipItemByName`
took an item reference only - an itemID, `item:N`, a link, or a name - and
resolved it by searching the bags for the first match. Holding two stacks of
the same item, or two copies enchanted differently, there was no way to say
which one you meant.
They now also take an item location: `{bagID=B, slotIndex=S}`,
`{equipmentSlotIndex=N}`, or an item GUID string - the shapes
`C_Item.GetItemID` already took. A location names one exact item.
`Item::Location::FindItemArgOrLocation` owns the overload; the old `Resolve`
(locations, pointer) and the new `ResolveItemArgOrLocation` (either, pointer)
are wrappers over it, so the classification and the walk live in one place.
None of the four call sites resolves anything itself any more.
A table is only ever a location, and so is a string that parses as a strict
`0x` + 16 hex digit GUID, which no item link or item name can look like. The
form is decided from the argument alone and committed to: a location resolve
STOMPS the Lua stack (`PackBagSlot` overwrites the first slots with its own
arguments), so falling back to the reference forms after one had run would
re-read the argument as whatever it left behind - a bag index read as an
itemID, which names an unrelated item.
`EquipItemByName` needs the source coordinates too, since the swap packet
carries them, which is what the detail form is for. An
`{equipmentSlotIndex=N}` location names a worn item, so with a `dstSlot` it
routes to `Item::Swap::FromPaperdoll` - that is how a ring 11<->12, trinket
13<->14 or weapon 16<->17 move is expressed. Without a `dstSlot` there is no
slot for the engine to pick, so that combination does nothing.
Its `lua_settop(L, 0)` ahead of `ClearCursor` is gone: `Script_ClearCursor`
(`0x004895B0`) is `FUN_00495190(1, 1)` and a return - no arguments, no stack
reads. Dropping it lets the item resolve run after the cursor clear, which a
location needs: while an item is held its slot reads empty, so the location
it came from would otherwise resolve to nothing.
* refactor: centralize the item constants and the icon-path build
Four copies of the ItemStats displayInfoID -> `Interface\Icons\<name>`
chain had accumulated - `item/GetData`, `item/Info`, `container/ItemInfo`
and the new `item/Icon` - and they had already drifted: two checked for an
empty icon name and two did not. `Item::Icon::PathForDisplayInfoID` is now
the one builder and the other three call it, so a change to that chain has
a single site. `PathForItemID` is it plus the cache lookup.
`action/ItemState` had local constants shadowing values already in
`Offsets.h`: `kItemSpellSlots` for `ITEMSTATS_SPELL_SLOT_COUNT`, and a bare
`trigger == 0` for `ITEM_SPELLTRIGGER_ON_USE`. `INVTYPE_AMMO` / `THROWN`
existed in two files, and the engine runner's `0x400` macro line buffer in
two more, so those move to `Offsets.h` as well - `INVTYPE_*` next to the
string table they index, `MACRO_LINE_BUFFER_SIZE` next to the delimiters.
CLAUDE.md's rule, and duplicated offsets drifting is exactly what it was
written for.
* fix(macro): indented directives, release, and icon-getter re-entry
Three defects found reviewing the `#showtooltip` work, plus dead code.
A directive typed with a leading space was lost AND leaked to chat.
`ParseDirective` and the two `#`-line filters all tested byte 0, but the
tokenizer splits on `\r\n` only, so ` #showtooltip` arrives with its space:
the parse missed it, and then `Macro::RunBody` / `Macro::Execute` failed to
recognize the comment and dispatched the line through `EXECUTE_CHAT_LINE` -
the /say this feature exists to stop. All three skip blanks now, as does
`CastCommandArgs` so an indented `/cast` line is still collected, and the
runner dispatches the trimmed line so an indented command works at all.
`C_Macro.SetMacroDisplay(slot, nil)` left the publisher's spell in place.
`Release` cleared our entry and asked for a rescan, but `Rescan` only marks
an entry dirty when it has a directive with options - so for a macro with
no directive of its own, nothing ever rewrote `+0x564`, and the button, its
tooltip, cooldown and auto-repeat state stayed on the published spell until
the engine happened to re-parse. It now hands the field back to the engine
by running the engine's own parser, then repaints.
The macro icon getter could run Lua from under a click. `FUN_MACRO_ICON_PATH`
has two callers and one is `FUN_MACRO_PICKUP`, inside the UseAction core, so
`Lookup`'s catch-up could evaluate conditions through `SecureCmdOptionParse`
and fire `ACTIONBAR_SLOT_CHANGED` into addon handlers while the drag was on
the stack - the re-entrancy hazard CLAUDE.md records for `Tooltip::SetEvents`.
`LookupPassive` reads only what the entries already hold; the world tick
does the work and repaints, so a resolution can be one tick behind an edit.
Also drop `Active()` (no callers) and `RunBody`'s unused trampoline, and
correct the header's reference to a module that does not exist: the icon
comes from `Macro::IconPath`, not `Action::Texture`.
* fix(slash): five conditional-command defects, and cheaper option passes
`/cast 5019` could consume an item. A bare number that is not an
equipment slot is a spellID, but it reached the item lookup, which reads a
number as an itemID - so carrying item 5019 used it instead of casting the
spell. `Macro::ShowTooltip::ResolveValue` already guarded this exact case,
so the button showed the spell while the click used an item.
Every bare `/targetenemy` stepped backwards. The cycling selectors pass
the parsed value as their reverse flag, and a clause that matches with no
value yields the empty string - which is a true boolean in Lua. Six
commands were affected in their most common form.
`/cast [@none] Spell` did nothing. `@none` names no unit, but only
`@cursor` was exempted from the token resolve, so `none` went to the
by-name search and the cast was dropped. It now means no unit, which is
the untargeted form of the verb; `/target [@none]` clears the target, as
documented. The other unit verbs no longer feed `none` to that search
either - it matches on the START of a name, so it would find a Nonek.
`/assist [@target] Bob` ignored Bob. `/target` and `/follow` both let a
trailing name win over an explicit `target=target`; assist did not.
`/use [@player] 0 1` used the wrong item. The position branches ran before
the bag branch, so a named slot was thrown away and the item re-found by
link - the first match anywhere, which with two stacks is the wrong one.
Every form now resolves to one item and goes through one call, which also
means a slot is aimed by `@unit` like a name is, and that `/use` always
uses: clicking a bag slot sells the item at a merchant and repairs it
under the repair cursor (`Script_UseContainerItem` branches on the
merchant GUID and on cursor mode 0x11), which a typed command must not do.
`GroupPasses` also stopped paying for an answer it discards. A group with
conditions only needs a token, so it asks `IsUnitToken` rather than
`pcall(UnitExists)` - the conditions test existence themselves. The
existence path keeps its single pcall, and a name, which costs an
object-manager search, is remembered for the rest of the pass.
|
||
|
|
8b06caf708 |
docs(api): rewrite the EquipmentSet intro for the caller
The paragraph explained the storage choice rather than the API: which opcode carries set data, that "this server" does not speak it, and that the file format matches a sibling project. None of that changes what a caller writes, and "this server" names nothing. Say what the namespace does, in the section-opener style the rest of the file uses, and keep the one storage fact a caller acts on -- sets belong to one character. |
||
|
|
4684088675 |
docs(api): give GetWeaponEnchantInfo real numbers for both enchant kinds
The example claimed charges = 5 for Brilliant Mana Oil and hid the enchant ID behind a placeholder. Live dumps of both kinds correct it: the oil returns charges = 0 with enchant 2629, and Instant Poison returns charges = 40 with enchant 323. Show each case, so the zero reads as a property of time-based enchants rather than as the only answer the field ever gives. |
||
|
|
9a26207df2 |
feat(item): read a bagged weapon's temporary enchant
C_Item.GetWeaponEnchantInfo covers only the three equipped weapon slots, so a poisoned or oiled weapon sitting in a bag reported nothing. Add C_Item.GetItemTempEnchantInfo(itemLocation), returning the same (hasEnchant, expirationMs, charges, enchantID) tuple for any location the rest of the C_Item location family accepts. The temporary-enchant reader already needed nothing but a CGItem, so it now takes one directly, with a thin wrapper for the paperdoll path. Its local copy of the slot offset (0x4C) gives way to the block layout Offsets.h already carried for spell/BonusDamage.cpp, extended with the within-slot duration and charges deltas and a named temporary-slot index. Verified in game: a staff carrying Brilliant Mana Oil reports enchant 2629 both equipped and in a bag, and the remaining duration keeps counting down while the weapon sits there. Fixes #47 |
||
|
|
2adfaff020 |
docs(api): move the enchant functions into the Item section
C_Item.GetWeaponEnchantInfo and C_Item.GetEnchantInfo were documented inside the Spell section, sandwiched between C_Spell entries, and neither appeared in the Contents at all. Both now sit in Item after C_Item.GetStackCount, kept adjacent since the second resolves the IDs the first returns, and listed in that section's Contents block so body and Contents order agree. The headings are unchanged, so existing links to them still resolve. |
||
|
|
8a7ab490f4 |
docs(api): move CURSOR_CHANGED in with the other events
It was documented under Cursor while every other standalone event lives in the Events section, placed there to follow that section's alphabetical run. The heading text is unchanged, so existing links to #cursor_changed-event still resolve, and Cursor keeps a pointer to it so a reader of GetCursorInfo can still find it. |
||
|
|
5070c892e2 |
feat(texture): add Texture:SetDesaturation and GetDesaturation
Backports the parametric desaturation methods. The engine desaturates a region by storing one pixel-shader object in the region's shader slot and binding it per draw, so an amount is a matter of having more shader objects: one CGxShader per level (256 levels, framebuffer precision), created lazily through the engine's own loader under a path that exists nowhere and filled with hand-encoded ps_2_0 / ps_1_1 bytecode, then compiled the way the loader does. No draw hooks, no per-region state. SetDesaturated is re-registered on the Texture registry (the registrar front-inserts and the dispatcher walks from the front) so all three methods share one state and `false` clears a partial amount. Every desaturation path, including SetDesaturated(true), now multiplies the vertex-colour tint in like retail; the stock shader dropped it. The first ps_2_0 program was rejected by the D3D9 runtime validator: an lrp may not write a register it reads as src0 or src2. The shipped program is the instruction shape fxc emits, and both programs were validated on a NULLREF device. A build attempted while the device is lost is not latched, and a level that cannot be built falls back to the engine's stock shader. |
||
|
|
8295ec9442 |
docs(api): clarify the multi-flavor TOC section
The intro claimed this client is the Vanilla game type, which read as a contradiction against the paragraph below it saying `_Vanilla` files are not used. Both are true: `_Vanilla` as a file suffix names the Classic Era client, while `vanilla` as a directive value names this client's game type. Drop the intro sentence, since the [Family] / [Game] table already states it where a reader can use it, and add a note about the shared word where the collision is first hit. Explain why `_Vanilla` and `_Classic` are skipped instead of saying only that they target an engine this build does not match, and state the consequence: an addon shipping just `_Vanilla` and `_Mainline` TOCs does not load at all, so it needs a `_ClassicAPI.toc`. List both flavor sets in probe order, and give the Bindings half the same shape as the TOC half. The `## Interface:` subsection showed a multi-version line and then said nothing about it. Say that the addon loads when 11200 appears anywhere in the list. |
||
|
|
c5b1431073 |
feat(cvar): default scriptMemory to 0
Sets the cvar most people already set by hand, so a fresh install does not meet the interface memory popup. Only the default is overridden: the write happens when the live value still equals the cvar's registered value (read from +0x30, so no hardcoded 49152), which leaves a deliberate Config.wtf choice alone. The archive flag makes the 0 persist, so the comparison fails on every later boot and the write happens once per install. What the cvar gates, since none of it is obvious: FUN_00495590 polls Lua's live allocation (KB) against it, forces a full GC when over, and if still over fires MEMORY_EXHAUSTED and starts a 15-second timer, after which FUN_00401560(0) exits the process. FrameXML shows the event as the "disable some interface add-ons and restart" popup. The poll is gated on `limit > 0`, so 0 disables it, and nothing else caps Lua memory (lmemPool's allocator is six size classes falling through to the Storm heap, with no ceiling of its own). Timing is load-bearing: scriptMemory is writable on the glue state and read-only in the world, where the innermost setter's `if (flags & 4) return;` makes any write a silent no-op. The glue registration is what makes this land; the in-game one stays as a second chance. Verified by deleting the Config.wtf line, launching, and finding 0 in the AddOns dialog and back in Config.wtf on exit. Adds CVar::Factory::GetDefaultString for the comparison. |
||
|
|
b39c16578b |
feat(addon): add the /startattack and /stopattack slash commands
Blizzard ships both in FrameXML's ChatFrame.lua; the handlers here are 3.3.5a's verbatim, routed to ClassicAPI's StartAttack / StopAttack. Every locale file gets the client's own command names, taken from the per-locale GlobalStrings dumps: /angriffstart, /empezarataque, /lanceattaque, /начатьатаку, /開始攻擊 and their stop counterparts, next to the English names. Gives users a clean /startattack without the macro-addon trick of placing the Attack spell on an action slot to read its state. Refs #46 |
||
|
|
7d7e392924 |
fix(spellbook): flag talent ranks by name when the next-rank link is missing
SkillLineAbility's next-rank column is empty for whole chains — every Mind Flay rank (15407, 17311..17314, 18807) holds 0 — and the engine's own rank-chain walk (FUN_SPELL_RANK_CHAIN_KNOWN) reads that same field, so IsClassTalentSpellBookItem flagged only Mind Flay rank 1. Fall back to the engine's other notion of a rank set: a spellbook spell that shares a talent rank spell's localized name, the same rule a name-cast uses to pick the highest rank. |
||
|
|
14fa610bc8 |
feat(spellbook): add the skill-line, cast-count, and loss-of-control surface
Nine C_SpellBook functions, two C_Spell functions, and one event, all
backed by engine data the stock Lua surface never exposed:
- LEARNED_SPELL_IN_SKILL_LINE(spellID, skillLineIndex, isGuildPerkSpell),
fired from the learn-spell hook under the engine's own
LEARNED_SPELL_IN_TAB gate (notified AND appended to the book). The
learn/unlearn co-hooks move from spell/Info.cpp to spell/Learn.cpp.
- GetNumSpellBookSkillLines / GetSpellBookSkillLineInfo /
GetSpellBookItemSkillLineIndex / GetSkillLineIndexByID, reading the
engine's tab array ({skillLineID, numSpells} entries at 0x00B71160).
The info table calls the engine's own Script_GetSpellTabInfo for the
name and icon.
- C_Spell.GetSpellCastCount / GetSpellBookItemCastCount: the minimum
over reagent slots of floor(carried / required), where carried is
equipped + bags 0..4. GetActionCount / IsConsumableAction are
item-only in this client, so the count is computed, not mirrored.
- C_Spell.GetSpellLossOfControlCooldown /
GetSpellBookItemLossOfControlCooldownInfo / ...Duration: the
C_LossOfControl effect that blocks a given spell, shaped as a
cooldown. Silence and pacify gate on Spell.dbc PreventionType
(+0x294, verified against six known spells).
- IsClassTalentSpellBookItem: Talent.dbc rank spells plus trained
higher ranks via SkillLineAbility's next-rank link (+0x20, verified
on Mortal Strike). Talent::SpellSet now owns the talent spell set
that GetCurrentLevelSpells already used.
- ContainsAnyDisenchantSpell: any known spell with effect 99.
Shared helpers added along the way: Spell::Lookup::SpellBankArgToBookType
and SpellbookItemArgsToID, Spell::Cooldown::Query, and
LossOfControl::LockoutForSpell.
|
||
|
|
8ca415e9a0 |
feat(spellbook): add C_SpellBook.GetPlayerSpellsByAura
Every spell the player currently knows with an effect applying the given
aura, as a 1-based ascending array of spell IDs. This is IsPlayerSpell
turned around: it walks the same known-spell bitmap and reports the set
bits whose Spell.dbc record carries the aura, so only a set bit ever
touches a record -- a few hundred lookups, not the ~28,000-row table.
It replaces a C_Spell.GetSpellsByAura that scanned the whole table and
was never committed. That answered "which spells anywhere apply this
aura", when the caller summing aura amounts is asking "which of MY
spells do" -- and the bitmap is that set. It also holds only a talent's
current rank, so a sum over the result never double-counts ranks.
Known is not active. A passive is in effect while known and never shows
in the buff list; a castable buff in the result is merely learned; a
buff another player put on us is not in the bitmap at all. Callers split
on C_Spell.IsSpellPassive and check the buff list for the rest. auraName
0 (no aura) and negatives return the empty array, as does pre-login.
Lives beside PlayerKnowsSpell in spell/Info.cpp rather than in a new
file, so the bitmap read is not copied an eighth time.
Verified in-game. Rogue: aura 217 -> {52508}, rank 2 only. Priest: aura
134 -> {14777, 45553} -- Meditation rank 3 only, plus Improved
Shadowform, a source no one had listed and the data surfaced on its
own. Each class returns empty for the other's aura.
Motivated by pfUI's energytick module: its energy-tick interval needed
the SPELL_AURA_MOD_ENERGY_REGEN_TIME amount, which it had as constants
derived from two hardcoded spell IDs. With this and GetSpellEffectInfo it
names no spell at all.
|
||
|
|
becf2f4866 |
feat(spell): add C_Spell.GetSpellEffectInfo
Returns one table per spell effect holding that effect's raw Spell.dbc fields: effect, auraName, basePoints, baseDice, dieSides, pointsPerLevel, dicePerLevel, miscValue. Three entries always, an unused slot all zeros. Deliberately raw rather than one computed magnitude per effect. The engine's figure also folds in a die roll the client cannot reproduce, combo points against the combo target, the caster's spell mods, and a level rescale for flagged spells -- a number carrying none of those while looking authoritative is worse than the inputs. The docs carry the formula and say that dieSides > 1 means the magnitude is rolled. Three new offsets. DieSides was already established by the EFFECT_BASE_DICE note, which records the verified column run as Effect@61, DieSides@64, BaseDice@67, BasePoints@76 -- it just had no named constant. The two per-level fields extend the same run at columns 70 and 73, and both are FLOAT, not int: field 73 on spells 10/17/43/91/122 decodes as 0.1/0.8/1.0/-1.0/0.5 as float and as garbage as int32. Verified in-game against answers known independently of the code: Power Word: Fortitude rank 1 reports auraName 29, basePoints 2, baseDice 1 and miscValue 2 (STAT_STAMINA), i.e. the +3 Stamina on its tooltip; Blizzard rank 1 reports effect 27 / auraName 3 with pointsPerLevel 0.1 (float32 widened), confirming the float read from Lua; Blade Rush rank 2 reports auraName 217 with basePoints 11 and baseDice 1, the 12 the server sums for SPELL_AURA_MOD_ENERGY_REGEN_TIME. Motivated by that last one: computing a rogue's energy tick interval needs the aura's amount, and the only alternative from Lua was hardcoding it or parsing a localized spell description. |