Compare commits

...
65 Commits
Author SHA1 Message Date
Brues 58e7e1a2c6 frame: add Frame:IsEventRegistered
Vanilla has RegisterEvent / UnregisterEvent as frame methods but not the
query IsEventRegistered (added in a later expansion). There's no engine
Script_* to delegate to, so we replicate the membership check already
embedded in Frame::RegisterEvent (FUN_00702140): find the event by name
in the live event table (SStrCmpI), then walk that entry's subscriber
chain (head@+0xC, next@+4, frame@+8, low-bit/null sentinel) for this
frame. self is resolved via Game::Lua::ResolveObject, yielding the same
CFrameScriptObject* the chain stores.

Registered on the Frame method registry (VAR_FRAME_METHOD_REGISTRY,
alongside RegisterEvent) so it resolves on any frame. Pure read, no new
hooks. Returns a boolean, matching the 3.3.5-era contract; verified
in-game (true for a registered event, false otherwise). Documented in
docs/API.md and README.md.
2026-07-09 18:08:17 -05:00
Brues 5339c97b1e tradeskill: native single-column link viewer layout on stock 1.12.1
The link viewer mirrored Turtle's HD 768x512 two-column TradeSkillFrame,
whose TW-TradeSkill art doesn't exist in the base 1.12.1 client — an
earlier stretch fallback smeared the vanilla art's internal borders.

Split every layout value that differs by client into an LO table gated on
TURTLE_WOW_VERSION. On stock clients the viewer now mirrors the native
Blizzard_TradeSkillUI geometry: 384x512, single column (list on top,
detail below), built from the vanilla ClassTrainer + UI-TradeSkill
quadrant art at native size, 8 visible rows, and the Exit button placed
exactly where the base TradeSkillCancelButton sits. Turtle keeps its
768-wide two-column layout unchanged.
2026-07-09 17:26:10 -05:00
Brues 7406ac5db1 tradeskill: add link buttons to TradeSkillFrame and CraftFrame
Backport of the WotLK "link this profession to chat" button. A
petition-icon button sits right of the window title; clicking inserts
the window's |Htrade:...|h link (C_TradeSkillUI.GetTradeSkillListLink /
GetCraftListLink) into the chat edit box, opening it if hidden. Other
players click the link to open the recipe viewer.

Both windows are LoadOnDemand addons on this client, so creation is
deferred via EventUtil.ContinueOnAddOnLoaded (fires immediately if
already loaded). Uses the native GossipFrame\PetitionGossipIcon since
the 3.3.5 UI-TradeSkill-LinkButton art doesn't exist in 1.12. Adds the
LINK_TRADESKILL_TOOLTIP locale string for the hover text.
2026-07-09 16:57:49 -05:00
Brues 1c263152e3 spell: fix negative cast/channel startTimeMs/endTimeMs on long-lived tick
C_Spell.UnitCastingInfo/UnitChannelInfo returned negative startTimeMs
and endTimeMs for users whose engine ms tick has crossed 2^31. The tick
(FUN_OS_TICKCOUNT_MS) has two backends: GetTickCount, or a scaled rdtsc.
The rdtsc value survives warm reboots (only a full power-off resets the
CPU TSC), so it can sit well past 2^31 even right after a "restart" --
which is why rebooting didn't clear the negatives. We stored the tick in
a signed int and pushed it straight to a double, so the high bit read as
the sign.

Script_GetTime (0x00515EA0) computes (double)(tick & 0xFFFFFFFF) * 0.001,
i.e. it masks the tick to the low 32 bits UNSIGNED. New PushMs helper
reinterprets our stored tick as uint32 before the double conversion,
reproducing GetTime()'s exact value so startTimeMs/endTimeMs stay
consistent with GetTime()*1000 across the whole range (both wrap together
at the ~49.7-day 2^32 boundary). Carrying full 64-bit precision would be
wrong -- it would diverge from GetTime() past 2^32 and break addons'
(now-start)/(end-start) math.

Routes all four timestamp pushes (cast + channel, local and remote)
through PushMs. The aura expirationTime path already handled the tick as
unsigned, so no change there.
2026-07-09 13:10:57 -05:00
Brues ab211962e5 unit: fix IsStealthed via UNIT_FIELD_BYTES_1 sneak flag
The old IsStealthed read descriptor +0x17C & 0x02, which is actually
UNIT_FIELD_AURALEVELS (per-slot caster-level bytes) - it only worked by
coincidence on the original test char and read false at level 60
(0x3C & 0x02 == 0), so Prowl/Stealth reported not-stealthed.

_classicapi_DiffAll on a Prowl toggle from a Cat-Form baseline showed
the real signal: UNIT_FIELD_BYTES_1 (+0x210) byte 3 flips 0x00->0x02
(the sneak/anim-tier flag the client render path reads to draw a
stealthed unit semi-transparent); everything else that changed was
aura-array slot metadata. IsStealthed now reads that broadcast bit
directly - verified in-game for Rogue Stealth, Druid Prowl, and
Shadowmeld, with no mount false-positive.

Also refactored the shared buff-slot scan into FindPlayerBuffWithEffect
(Dismount still uses it for the mount aura) and dropped the bogus
OFF_PLAYER_FIELD_VIS_BYTES / PLAYER_VIS_BIT_STEALTH constants.
2026-07-09 11:53:58 -05:00
Brues 33a23c563c map: make GetMapOverlays overlay tiling HD-map-patch safe
A custom patch-*.mpq that upscales the WorldMap overlay art broke the tile
resolution: the resolver read the actual BLP dimensions but did its grid and
texcoord math against a hardcoded 256px cell, so HD tiles rendered sheared
and half-clipped.

- Grid resolution detects the full-cell file size S (256*scale) and uses it
  as the partial-tile threshold instead of a literal 256, so HD edge tiles
  aren't mistaken for full ones.
- Texcoords now use content/nextPOT(content) — the engine's own WorldMapFrame
  fraction, which is scale-invariant (an HD file is POT*scale, same fraction)
  and needs no file-size guessing. Fixes overlays whose stock file was a 128
  cell (content <=128): the patch doubled them to 256, which the earlier
  S>=256 floor read as un-upscaled and halved their texcoord (the clipped
  dark patches).

Draw sizes/positions stay in logical map pixels; stock data is unchanged
(its files already are next-POT). File reads now only drive grid resolution
and the rare DBC-rounded sliver case.
2026-07-09 11:18:27 -05:00
Brues 17304929b5 tradeskill: show craftable [N] on rows, refresh on BAG_UPDATE_DELAYED
Each recipe row appends " [N]" (min over reagents of floor(onHand/required),
via GetItemCount) like Blizzard's list. The frame registers
BAG_UPDATE_DELAYED and, while shown, re-runs Refresh (row counts) and
re-renders the selected recipe's detail pane (reagent have/need) so the
numbers track the reader's bags live.
2026-07-09 00:50:36 -05:00
Brues d88f02fc90 tradeskill: tighten link-viewer frame, add Exit button + craft bottom art
- SetHitRectInsets(2,82,2,73) so the mouse/drag region matches the visible
  parchment (decoded from the corner BLPs' alpha: opaque art spans frame
  x[2..686], y[2..439]) instead of the full 768x512 texture rect.
- Bottom quadrants use the CraftFrame's TW-TradeSkill-BotLeft2/BotRight2
  (the flat button-bar bottom) and add an Exit button positioned like
  CraftCancelButton.
- Quad helper uses the bare SetPoint(point) one-arg form (enabled by the
  Script_SetPoint co-hook in src/frame/Modern.cpp).
2026-07-09 00:44:39 -05:00
Brues 1c79742cc3 cleanup 2026-07-09 00:09:12 -05:00
Brues d696a16ac4 tradeskill: load link-viewer items via the Item mixin, drop the item-info poll
The detail pane and subclass dropdown now resolve crafted items/reagents
through Item:CreateFromItemID():ContinueOnItemLoad() — each uncached item
re-renders only its own pane/dropdown when its data arrives, guarded so a
load completing after a newer selection can't clobber it. Replaces the
global GET_ITEM_INFO_RECEIVED watcher that re-derived everything on a 0.3s
debounce whenever any item loaded anywhere.

The 'All' subclass view no longer reapplies the filter (and resets scroll)
as items trickle in; have/need counts still use GetItemCount and the
subclass name still uses the synchronous GetItemInfoInstant.
2026-07-08 23:57:53 -05:00
Brues 2c1962b207 tradeskill: add recipe detail pane to the trade-skill link viewer
Clicking a recipe row now shows a Blizzard-styled detail pane: the crafted
item (or enchant) with its icon on the Class-Trainer detail-header bar, the
spell description for enchants, and the reagent grid (icon + name plate +
have/need counts) matching the CraftFrame layout. The first recipe is
auto-selected on open; selecting a row tints the row highlight to the
recipe's difficulty colour and whitens its text.

Rows are now real ClassTrainerSkillButtonTemplate buttons (the template the
CraftFrame/TradeSkillFrame use) so name rendering and spacing match exactly.
Reagent + product data (createdItem, numMade, reagents) is folded into the
existing C_TradeSkillUI.GetTradeSkillListRecipes decoder rather than adding a
new function.

The real GetTradeSkillReagentInfo family is indexed by the player's own open
window, so it can't describe a linked recipe from someone else's profession;
the reagents are read straight from Spell.dbc instead.
2026-07-08 23:15:29 -05:00
Brues 23cdbe196d spell: CastAtCursor/CastAtUnit cast exact rank by spellID, accept spell name 2026-07-08 20:12:16 -05:00
Brues 8b6760cd77 tradeskill: crafting-window-style link viewer (difficulty colors, subclass filter, spell tooltips) 2026-07-08 14:24:25 -05:00
Brues 7c2317aca1 Suppress aura cache fallback when descriptor is authoritative
Stuck auras (pfUI issue #20 part 2): a no-duration debuff (Ghost/Spirit
in battlegrounds) stayed visible after the unit resurrected. It can only
come from the Aura::Source cache fallback — the descriptor path re-reads
the live aura array each call.

The aura is cached with expirationMs == 0 (infinite), so OnWorldTick
never expires it, and its real removal came via a bulk object drop
(death/leave-visibility, then resurrect) rather than a per-aura removal,
so OnAuraRemoved never fired to evict it. The infinite entry lingered and
the fallback resurrected it on the now-alive unit.

The fallback only exists for rogue stealth and party range fluctuation,
which both clear the unit's entire aura array at once. Gate both fallback
entry points on DescriptorHasVisibleAura: if the descriptor exposes any
live aura it's authoritative, so a cache-only aura is genuinely gone.
Stealth/range recovery is unaffected — those produce a wholesale-empty
aura array, so the fallback still fires there exactly as before.
2026-07-08 11:18:37 -05:00
Brues b764b14df1 Filter hidden auras in cache fallback path
Add visibility check to `FallbackEligible` to prevent hidden/internal auras (e.g. SPELL_ATTR_HIDDEN_CLIENTSIDE) from leaking through as phantom buffs. The descriptor path already drops these via `IsSlotPopulated`; the cache fallback must apply the same engine predicate to avoid surfacing invisible auras.
2026-07-08 10:55:33 -05:00
Brues fc1bd30bef tradeskill: add trade-skill list links (GetTradeSkillListLink/GetCraftListLink + viewer) 2026-07-08 00:18:07 -05:00
Brues fe29ec050c loc: fire LOSS_OF_CONTROL_UPDATE with unit payload ("player") 2026-07-07 17:46:51 -05:00
Brues 7fafb89d1d docs: move LOSS_OF_CONTROL events into the Events section 2026-07-07 17:40:53 -05:00
Brues bcefe3b87f loc: add C_LossOfControl (data query + ADDED/UPDATE events)
Synthesize the modern C_LossOfControl surface for the local player, which
vanilla lacks entirely (a Mists-era aggregation layer):

- SCHOOL_INTERRUPT: co-hook the SMSG_SPELL_COOLDOWN handler (FUN_006e9460,
  same __stdcall(opCode*, CDataStore*) shape/registrar as Spell::Cast's
  failure/channel hooks). Turtle's Player::ProhibitSpellSchool sends the
  lockout as ONE cooldown packet listing every spell of the interrupted
  school at one uniform duration -- a shape nothing else emits (verified
  against the server source) -- so a player-GUID batch of >=2 same-school
  spells with equal duration authoritatively yields the locked school
  (1<<Spell.dbc School) + duration. The interrupter isn't in the packet, so
  spellID=0 / displayText="Interrupted".

- CC types (STUN/FEAR/ROOT/CONFUSE/CHARM/POSSESS/SILENCE/PACIFY/
  PACIFYSILENCE/DISARM): scan the player's debuffs, classify each by its
  EffectApplyAuraName (values authoritative from the server's SPELL_AURA
  table). Timing is best-effort from the Aura::Source cache (nil when the
  applying cast wasn't observed -- the fields are nullable).

- LOSS_OF_CONTROL_ADDED / LOSS_OF_CONTROL_UPDATE: a WorldTick poll-and-diff
  of the active set fires ADDED (with the 1-based eventIndex) per new effect
  and one coalesced UPDATE on any change. Chosen over event-driven aura hooks
  because effect EXPIRY has no engine packet (a tick is needed regardless),
  and the WorldTick tail keeps Lua handlers out of the mid-packet contexts
  the state updates in. Scan is bounded (16 debuff slots) and idle-cheap.

Placeholders with no vanilla equivalent: priority=0, displayType=2,
auraInstanceID=nil; iconTexture is the icon path (no FileDataIDs in vanilla).

Verified in-game: Polymorph -> CONFUSE, correct name/icon/duration, nil once
worn off; a counterspelled heal -> SCHOOL_INTERRUPT (lockoutSchool + 10s) +
SILENCE (4s), with two ADDED at cast and UPDATE as each expires.
2026-07-07 17:28:45 -05:00
Brues 9e03f69e9b xml: add C_XMLUtil.GetTemplateInfo() and DoesTemplateExist()
Both resolve by name through the engine's own template lookup
(FUN_006ee6f0) -- the same path inherits= uses -- so they're
case-insensitive and cover Blizzard FrameXML plus addon templates.

GetTemplateInfo returns { type, width, height, keyValues, inherits } or
nil if the template doesn't exist. type is the element tag; width/height
are the RAW values from a <Size> element declared directly on the
template (0 if none / inherited) -- read straight from the attributes
rather than via the engine's Size parser (FUN_006f1eb0), which applies
UI-scale conversion we don't want here. inherits is the inherits=
attribute or nil. keyValues is always empty (vanilla's XML schema has no
<KeyValues> element) and sourceLocation is omitted (a 10.2 feature;
vanilla records no per-node file/line) -- both parity-safe.

DoesTemplateExist returns a bool.

Verified in-game: DoesTemplateExist true/false as expected; GetTemplate-
Info nil for a missing name; StatFrameTemplate -> 104x13; FriendsFrame-
TabTemplate -> inherits "CharacterFrameTabButtonTemplate".
2026-07-06 22:26:15 -05:00
Brues 694a7415ab xml: add C_XMLUtil.GetTemplates()
Backport the modern C_XMLUtil.GetTemplates() -- enumerate every
registered virtual XML template as { name, type }.

The engine keeps virtual templates (the <Frame virtual="true"> etc. that
inherits= targets) in a Storm hash table. The XML parser (FUN_006ede10)
registers each via FUN_006ee500 and the inherits resolver reads the same
table via FUN_006ee6f0, so it's the authoritative template store. The
new module walks it: each node carries the template name (+0x14) and its
parsed definition node (+0x18), whose element tag string (+0x8) is the
frame type ("Frame" / "Button" / ...).

Covers both Blizzard FrameXML and addon templates; rebuilt on /reload.
Fonts are a separate registry and correctly excluded (matching retail).
Returns an empty table before the first template registers (mask ==
0xFFFFFFFF). Pure read, no hooks. Verified in-game: 212 templates,
entry 1 = { name="AtlasLootDefaultFrameButtonTemplate", type="Button" }.
2026-07-06 21:39:55 -05:00
Brues 23b102c60c spell/Cast: clear player channel on early stop; unify player-object access
UnitChannelInfo showed a stale "Ritual of Summoning" countdown after
helping a warlock summon: g_channel only cleared at its computed endMs
or when a timed cast superseded it, so a ritual that ends early (when the
summon fills) lingered, and a following instant cast -- which supersedes
neither cast timing nor the channel -- couldn't dislodge it.

OnWorldTick now polls the player's UNIT_FIELD_CHANNEL_SPELL (+0x228) and
clears g_channel when it drops to 0, gated by a confirm latch so the
field's ~1-tick start lag can't wipe a freshly stamped channel. Both
channel-stamp sites route through a new StampChannel helper that re-arms
the latch. Also fixes early-ended channels generally (interrupt / cancel
/ LoS break), which previously self-expired only at endMs.

Cleanup: factor the player-object/descriptor read into shared
Unit::Identity::PlayerObject() / PlayerDescriptor(), now used by
State.cpp, Cast.cpp's poll, and ComboDuration. PlayerObject() uses the
object-manager GUID resolve (FUN_OBJECT_RESOLVE_BY_GUID) -- the fastest
player-object path, non-throwing (null pre-world) -- replacing the
duplicated resolves (State.cpp's token resolver, ComboDuration's
hand-rolled GUID plumbing).
2026-07-06 19:52:01 -05:00
Brues e165aa6cd4 README: add Frame backports, RegisterComboDuration, GetMapOverlays rows 2026-07-06 00:56:29 -05:00
Brues 8ed32e6c8e C_Map.GetMapOverlays: zone overlays from WorldMapOverlay.dbc
Every overlay of a zone regardless of exploration, replacing the
hand-measured tables map-reveal addons ship (pfUI pfMapOverlayData).
The engine's GetNumMapOverlays/GetMapOverlayInfo only expose
DISCOVERED overlays -- the exploration gate is a server-fed ID list at
0x00B6E630 -- so unexplored areas were previously undrawable without
per-server data. Reads WorldMapOverlay.dbc directly instead, scoped by
the zone's WorldMapArea row (never by folder contents).

No argument = the world map's currently viewed zone (same
continent/zone resolution GetMapInfo performs, globals 0x0084506C /
0x00845070 / 0x00B6E668); numeric argument = AreaTable areaID, the
same identity C_Map.GetBestMapForUnit returns.

Tile grids are resolved in the DLL rather than left to consumers,
because Octo's shipped data breaks the canonical ceil(w/256) layout
three ways (mirroring the InkLab map generator's empirically-derived
disambiguation):
- sliver columns the DBC rect rounds away (Icepoint's Kaneq'nuun has a
  real 8px third column; DBC w=512 implies 2 -- naive layout shears
  the overlay into misplaced strips, the one visibly broken overlay);
- foreign tiles appended to an overlay's number sequence (Kaneqnuun
  ships 12 tiles, the true grid is 3x2 = 6);
- same-rect higher-resolution re-exports (Stonetalon's WindshearCrag:
  4 real tiles for a 1-tile rect, kept and scaled).
The true grid comes from the actual BLP header dimensions (width @
+0x0C, height @ +0x10, BLP1 and BLP2 alike) read through the engine's
own VFS reader, since only a row's last tile may be narrower than 256.
A sweep of all 707 overlays found 108 art/DBC mismatches; Kaneq'nuun
is the only shear-class one, matching the single visible artifact.
Resolution is cached per overlay per session.

Each overlay returns the DBC fields plus a ready-to-draw `tiles`
array (file / width / height / texCoordX / texCoordY / offsetX /
offsetY) so consumers need no grid math at all. WorldMapArea.dbc and
WorldMapOverlay.dbc schemas verified against fresh extractions and
the engine's own Script_GetMapOverlayInfo (0x004A8A00).
2026-07-06 00:54:57 -05:00
Brues 26da489773 re-enable HookScript 2026-07-05 23:40:49 -05:00
Brues 9c73430ddd temporary disable HookScript 2026-07-05 23:40:04 -05:00
Brues e712af11a5 Drop stale WOW_PROJECT_ID rationale from Frame backport docs 2026-07-05 18:19:34 -05:00
Brues 45291cb16b Modern Region/Frame method backports (Frame::Modern)
ClassicAPI advertises WOW_PROJECT_ID = WOW_PROJECT_CLASSIC, which
steers modern addon ports onto their Classic Era code paths -- where
they also assume Era's C-level frame behaviors. Supply the missing
ones (surfaced by AI_VoiceOver's Era path, but generic to every port):

- region:SetPoint("RIGHT") -- the one-arg form. Vanilla's parser
  accepts every other modern shape but raises the usage error when
  arg 3 is fully omitted (LUA_TNONE fails its type validation); a
  co-hook on Script_SetPoint (0x007A2540, the ONE implementation in
  the Region base table) normalizes it to (point, 0, 0) -- vanilla's
  parent-relative form, exactly the modern semantic.
- region:SetSize / region:GetSize -- registered on the REGION base
  method registry (ctx 0x00CF54B4), so they resolve on frames,
  buttons, textures, fontstrings -- including engine-created textures
  (GetNormalTexture():SetSize(...)).
- frame:SetShown -- Show/Hide are per-branch script functions with
  their own object typechecks, so it's registered separately on
  Frame / Texture / FontString with matching delegates.
- frame:SetResizeBounds -- modern rename of SetMinResize/SetMaxResize.
- frame:HookScript -- absent in vanilla entirely. Chains the old
  handler (invoked vanilla-style, reading the this/event/argN
  globals), then invokes the hook handler with modern positional args
  ((frame, event, arg1..arg9) for OnEvent, (frame, arg1..arg9)
  otherwise) via a C closure over (handler, old, isOnEvent).

All methods delegate to the engine's own Script_* implementations by
reshaping the Lua stack -- no re-derived setters, so UI-scale
conversion, object resolution, and type checking stay engine code.
Method-registry contexts beyond GameTooltip's (Region / Frame /
Texture / FontString) are now in Offsets.h per the
docs/BlizzardScriptAPI.md catalogue.

Verified in-game: GameTooltip:GetSize() returns real dimensions,
CreateFrame + SetSize(50,50) reads back 50, and AI_VoiceOver's Era
path runs through its entire frame construction (SetPoint one-arg,
SetSize, SetShown, SetResizeBounds, 14 HookScript sites) where it
previously errored on the first call.
2026-07-05 18:16:47 -05:00
Brues 9bfbb2e6b0 We have other ways to detect 1.12.1 and this is causing conflict 2026-07-05 18:16:13 -05:00
Brues d6bf05c12c Combo-point finisher durations for C_UnitAuras
Rupture at 4 combo points reported duration=6 (the SpellDuration.dbc
base) instead of the real 14s: finisher durations are computed
server-side at cast time and 1.12 never transmits them for debuffs the
player puts on other units. Verified with a whole-protocol sniff --
SMSG_UPDATE_AURA_DURATION (0x137) is self-scoped (slot+duration for the
player's own aura array), 0x2DD has no client handler and never
arrives, and nothing else carries the value. (pfUI issue #17.)

Mirror the server's computation instead (new Aura::ComboDuration):

- Snapshot combo points ([player+0xE68]+0x1029, the field behind
  Script_GetComboPoints) the instant CMSG_CAST_SPELL leaves, via a
  co-hook on the NetClient send funnel (FUN_NET_SEND 0x005379A0). The
  server consumes CP before SMSG_SPELL_GO returns, so reading at
  SpellGo time is too late; the send funnel also covers
  nampower-queued casts, which bypass any Spell_C_CastSpell co-hook
  via trampoline re-entry.
- Compute base + (max - base) * CP / 5 from the spell's own
  SpellDuration.dbc row ((v)mangos CalculateSpellDuration), then apply
  the player's duration SpellMods, matching server order. DBC-driven
  and spell-agnostic: finishers are exactly the rows with base != max
  (Rupture 6/16s, Kidney Shot 1/6s, r1 0/5s, Slice and Dice 6/21s;
  Expose Armor 30/30 self-disables). Consumed by Aura::Source's
  SpellGo hook; 0 falls through to the existing base path.
- Turtle's reworked Rip (8s + 2s per CP) has NO client data: all six
  ranks' DurationIndex points at SpellDuration row 87, which Turtle
  added server-side only -- the client table jumps 86 -> 105. Built-in
  values cover it, gated on the dangling-row condition itself (the
  only dangling cross-reference in the entire client Spell.dbc, per a
  full DurationIndex/CastingTimeIndex/RangeIndex sweep) rather than on
  realm detection: stock clients resolve Rip's index normally, and if
  Turtle ships the missing row the DBC takes over automatically.
- C_UnitAuras.RegisterComboDuration(spellID, baseSec, maxSec) as an
  escape hatch for other servers with the same class of client-data
  gap; a registered override beats DBC and built-ins.

Verified in-game: Rupture at 4 CP now reads duration=14.
2026-07-05 17:12:10 -05:00
Brues 07a4cbde43 Evict remote casts via CGUnit_C::ClearCastingSpell choke point
Turtle's core doesn't broadcast SMSG_SPELL_FAILURE or
SMSG_SPELL_FAILED_OTHER on interrupt (verified by logging both
handlers through live tests), so the SPELL_FAILED_OTHER co-hook never
fired and interrupted enemy casts (Kick / Earth Shock / Counterspell)
lingered as ghost bars. What actually stops the cast visual on Turtle
is SuperWoW calling CGUnit_C::ClearCastingSpell (0x0060D040) directly.

Co-hook that choke point instead: every unit-stopped-casting path
funnels through it (failure packets, SPELL_GO completion, movement,
death, animation events, local cancel), gated on the unit's live
current-cast field at +0xC8C, so eviction is independent of which
signal the server sends. Both failure-packet handlers stay co-hooked
as backstops for servers that do broadcast them. Verified in-game:
all three interrupts evict; non-interrupting damage doesn't
false-evict.

Also confirmed via a whole-protocol sniff on the NetClient message
dispatch (FUN_NET_MESSAGE_DISPATCH = 0x00537AA0, documented for
future use): SMSG_SPELL_DELAYED goes only to the affected caster --
observers receive no packet of any kind for pushback, so remote bars
can't stretch. Documented as an inherent 1.12 protocol limitation.
2026-07-05 01:57:18 -05:00
Brues 017dc5b6f1 Add UPDATE_INVENTORY_DURABILITY (TODO #89)
Fires (no payload) when any equipped item's durability changes -
combat damage, death, repairs - via a co-hook on the engine's
inventory-alerts recompute (FUN_004C7EE0, the routine behind vanilla's
UPDATE_INVENTORY_ALERTS). The engine calls it whenever an owned item's
fields change (the item post-update handler FUN_005D9A90 routes every
item values-update through it) plus equip/enter-world paths, so it is
the engine's own durability-may-have-changed signal. Post-original we
diff a per-slot {itemGuid, durability} snapshot and fire once per
recompute with a real change - a death dinging all armor produces one
event, not nineteen.

The snapshot seeds silently on the first recompute of the process and
is deliberately NOT re-seeded per enter-world: death/release/graveyard
teleports re-run the setup, and a per-setup reset swallowed the
death's own durability loss (verified in-game; repairs fired, deaths
didn't until this was fixed).

Dead end, kept documented at OFF_DESC_ITEM_DURABILITY: bank-1 (ITEM
field) descriptor observers register without error but are never
invoked - item values-updates demonstrably arrive and player-bank
observers fire in the same session; registration arithmetic fully
verified (bank base FUN_00465690(1)=0x18, bank walk item 0->1->2).
Prime suspect is the registrar's internal wrapper lookup
(FUN_00464890, the observer registry, distinct from the object
manager) silently no-op'ing for item objects.

Also in Player::Equipment: resolve the player via the non-throwing
GUID resolver (FUN_00468460, what the engine's own bag observer uses)
instead of FUN_RESOLVE_UNIT_TOKEN, which raises a Lua error when the
token doesn't resolve - unsafe inside SMSG_UPDATE_OBJECT processing
where the player can be mid-create.
2026-07-05 00:23:01 -05:00
Brues ddcb6ba9f0 Add C_QuestLog.GetNumQuestObjectives(questID)
The quest's objective count — the length GetQuestDetails(questID)
.requirements would have — read straight from the cache block with no
Lua tables or strings materialized. nil when the quest isn't cached
(same contract as GetQuestDetails).

Exists because materializing full detail tables in a login-path loop
is expensive at warm cache: GetQuestDetails copies description /
objectives / completion text into Lua strings (~5 KB per quest).
TurtleGuide's guide-load OIDX validation did exactly that for every
tagged step — fine on the first (cold-cache) session, but once the
quest cache was warm from questcache.wdb every subsequent login
exhausted vanilla's fixed Lua memory pool and crashed the client in
lmemPool.cpp:53. With this accessor the same validation costs a few
dozen byte reads regardless of cache temperature.
2026-07-04 20:02:25 -05:00
Brues 17cf323a52 Add PLAYER_EQUIPMENT_CHANGED(equipmentSlot, hasCurrent) (TODO #88)
The earlier investigation tried to hook a firer of
UNIT_INVENTORY_CHANGED; none of the three known 0xBA fire sites
trigger on equip because equipment descriptor writes are silent — the
engine registers descriptor-field observers (the __AUCMirrorHandler__
system: registrar 0x00467E70, dispatcher 0x00465570 with per-node
mirror + memcmp change detection) for the bag/backpack/bank/keyring
GUID ranges but never for equipment 0x4A8..0x538.

So register our own: co-hook the engine's inventory observer setup
(FUN_004F8CC0, once per enter-world) and, post-original, register one
observer per equipment GUID field through the engine's registrar —
same bank/size/GUID shape as the engine's bag observers, engine-
managed lifetime, no polling, no hot-function hook. The dispatcher
invokes us only on a real change.

Callback ABI verified from the dispatcher call site at 0x004655DC and
the engine callback FUN_004F8DB0's RET 0x10:
  __fastcall(fieldOffset /*ecx*/, size /*edx*/, guidLo, guidHi,
             oldValue*, userArg) -> ret 0x10

equipmentSlot is the 1-based Lua inventory slot (1 head .. 19 tabard);
hasCurrent (1/nil) is read from the player invMgr GUID array — the
same post-change truth the engine's bag observer reads.
2026-07-04 17:01:01 -05:00
Brues a0b7d03c28 C_GossipInfo: session gating + quest-greeting support
Vanilla has two disjoint questgiver paths. Gossip NPCs send
SMSG_GOSSIP_MESSAGE into the gossip arrays this namespace reads; but
quest-only NPCs send SMSG_QUESTGIVER_QUEST_LIST (the QUEST_GREETING
event / QuestFrameGreetingPanel), whose data lands in entirely
separate storage C_GossipInfo never read — every getter returned
empty for those NPCs. GetText was also ungated: the gossip text
buffer persists after close, so it served the previous gossip NPC's
greeting.

- Gate all gossip reads on the live gossip NPC GUID (0x00BC3F58,
  cleared by the close worker at 0x004E1FA0 before GOSSIP_CLOSED
  fires). Stale text/options/quests are no longer visible, and
  selectors can't send a CMSG from stale slots.
- Serve the quest-greeting session when one is live instead: the
  questgiver GUID (0x00BE0810) must match the greeting-fill GUID
  (0x00C4D740) so leftovers can't leak when a quest session starts
  from a gossip-menu click. GetText returns the greeting text;
  Get{Available,Active}Quests / GetNum* read the greeting arrays
  (32 x 0x4C entries {questID, level, title}) with isComplete
  recovered from the packet handler's scratch status array
  (status 4 = turn-in-ready); Select{Available,Active}Quest route to
  the engine's greeting selectors (0x005012A0 / 0x00501320).
- Neither session live: empty tables, 0 counts, empty text.

GetOptions/GetNumOptions stay gossip-only — greetings have no
options. Retail keeps the two APIs disjoint; merging is deliberate so
modern-addon ports work on greeting-path NPCs (common on 1.12
servers).
2026-07-04 14:27:06 -05:00
Brues 15cae8a474 Add QUEST_REMOVED event; refactor quest log hook
Introduce QUEST_REMOVED (fires with questID) and document it in API.md and README. Rename Quest::Accepted → Quest::LogEvents and update the quest-log rebuild hook to compute pre/post deltas for both QUEST_ACCEPTED (questLogIndex, questID) and QUEST_REMOVED (questID). Add suppression rules to avoid firing on login/resync bulk changes. Update Offsets.h commentary to reflect the new LogEvents module and its responsibilities.
2026-07-04 12:04:27 -05:00
Brues 49865e9b9e Fix cast-then-channel spells showing the channel bar at cast start
Mind Control is a 3 s cast on a spell flagged CHANNELED with a 60 s
duration. HandleSpellStart branched on the channel attribute alone, so
at cast start it stamped g_channel with the 60 s duration and killed
the client hook's correct 3 s cast stamp — the bar showed a premature
60 s countdown (pfUI #9). Same story remotely: an observed MC stored a
60 s channel entry instead of the 3 s cast.

- Treat a channeled spell with castTime > 0 as its CAST phase in
  HandleSpellStart (player and remote paths); instant channels keep
  the immediate stamp.
- Co-hook the MSG_CHANNEL_START handler (0x006E7550, layout from
  nampower's SpellChannelStartHandlerHook: spellId u32, duration u32).
  The server sends it to the caster the moment the channel begins —
  after MC's cast completes — with the server-authoritative duration.
  Stamp g_channel there, before the original fires
  SPELLCAST_CHANNEL_START, so pollers see fresh data. For instant
  channels it re-stamps what the SpellStart path just wrote.

Verified: MC shows 3 s cast then 60 s channel; Mind Flay-style instant
channels unaffected.
2026-07-03 21:36:42 -05:00
Brues 98d4504b6f CBOR encode: keep encoding through OutOfMemory so the resize tally is complete
tinycbor's grow-and-retry contract: on buffer overflow the encoder
switches to counting mode and encoding must CONTINUE for
cbor_encoder_get_extra_bytes_needed() to be accurate — a nested
container's tally only reaches the parent via close_container. The old
code returned false on the first CborErrorOutOfMemory, so a mid-
container overflow could report extra == 0 and be misread as a genuine
"value not representable" failure (returning nil), or undercount and
churn through multiple resize passes.

Add EncOk() (NoError or OutOfMemory = keep going) around every encode
call; the retry loop now distinguishes real failures (return nil) from
overflow (single complete-tally resize, retry).
2026-07-03 20:48:39 -05:00
Brues 597530d558 Evict interrupted casts via SMSG_SPELL_FAILED_OTHER (TODO #98)
Remote casts cached from SMSG_SPELL_START were expired only by their
computed end time — an interrupted remote cast lingered as a ghost bar
for every UnitCastingInfo consumer (pfUI #11). The interrupt signal
does exist at the packet layer: the server broadcasts
SMSG_SPELL_FAILED_OTHER to observers whenever a started cast aborts
(interrupt, death, movement, fizzle).

Co-hook its handler (0x006E8E40, same __stdcall shape as
FUN_SPELL_DELAYED; address + layout mirrored from nampower's
SpellFailedOtherHandlerHook — body is plain casterGuid(u64),
spellId(u32)) and:

- evict the matching g_remoteCasts entry (guid + spellID), so remote
  cast/channel bars drop the same tick the cast dies;
- clear the player's own g_cast on a guid match — covers the
  server-stamped chained recast that the WorldTick VAR==0 poll can't
  clear (self-receipt is server-dependent; the endMs self-expiry
  backstop remains if it never fires).

Update the stale 'no per-unit interrupt signal in 1.12' notes in
Cast.cpp and docs/API.md.
2026-07-03 20:24:14 -05:00
Brues 2165a8e986 Restore TTS with VanillaTTS coexistence
Re-add the C_VoiceChat / C_TTSSettings SAPI module removed in 6f543ec,
gated so it coexists with the standalone VanillaTTS DLL: when
VanillaTTS.dll is loaded in the process, the module stands down
entirely — no Lua functions, no ttsVoice/ttsSpeed/ttsVolume cvars, no
VOICE_CHAT_TTS_* event reservations, no SAPI init. Without the gate,
each DLL would claim its own event-table slot for the same event names
(listeners bind to one slot while the other DLL fires the other) and
the cvar registrar would replace the first DLL's clamp callbacks.

The check (GetModuleHandleA at RegisterLuaFunctions time) is
load-order-independent — VanillaFixes loads every DLL at process
attach, long before FUN_LOAD_SCRIPT_FUNCTIONS fires. The event
AutoReserves move from file scope into the gated registration as
function-local statics so a stand-down leaves no reservations behind.

Restore the ole32 link, the README VoiceChat/API rows + event table,
and the docs/API.md VoiceChat section (with a coexistence note).
2026-07-03 19:00:17 -05:00
Brues cdb515e0c3 Remove third compare tooltip support
Drop legacy third shopping tooltip handling from GameTooltip_ShowCompareItem. Unpack only two shoppingTooltips, remove unused item3/shoppingTooltip3 variables and commented code, and simplify anchor/owner logic for the two-compare layout. GAP/backdrop spacing calculation is preserved.
2026-07-03 02:27:11 -05:00
Brues d55613e88c Clear the tooltip item GUID on every clear (fix stale GetItem)
GameTooltip:GetItem() returned the previous item after SetUnitAura /
SetEquipmentSet (and any tooltip that shows no item). The engine's per-tooltip
clear FUN_00530050 zeroes the unit / gameobject / itemID / spell identity fields
but leaves the item GUID (+0x380/+0x384) intact -- the field CGItem tooltips
(SetBagItem/SetInventoryItem) set and that Item::TooltipItem::CurrentID resolves
when the itemID field is 0. So a stale GUID lingered across a switch to non-item
content and GetItem/HasItem/IsEquippedItem resolved the old item.

Co-hook FUN_00530050 and zero +0x380/+0x384 after the original. All 14
Set*/builder paths route through this clear (verified via xrefs; the aura
builder FUN_0052f880 and the item builder FUN_0052b650 both call it), and the
item builder calls it before writing the GUID -- so every fresh tooltip resets
the item GUID and only a real item build re-sets it. The hook runs no Lua, just
zeroes 8 bytes; the CurrentID unit/GO/spell gating is now belt-and-suspenders.
Verified in-game: bag/equipped items still resolve; SetUnitAura / SetEquipmentSet
no longer leak the previous item.
2026-07-03 02:24:50 -05:00
Brues 3c0bf3abed OnTooltipSetItem: fire via the inner invoker + restore the Lua stack
The initial fire used FUN_00702690, whose exec-context mutation (DAT_00ceeac0)
is only valid from the engine's top-level frame-script dispatch; firing it from
inside the item builder (mid-Script_Set*, and in the wild under a stack of addon
Set* wrappers) raised through the outer wrappers. Two fixes make the fire safe
for normal handlers:

- Call the inner invoker FUN_00704d50(handler, tooltip) directly. It binds the
  global `this` = tooltip and runs the handler under its own protected pcall
  (0 args), self-contained and context-independent.
- Snapshot lua_gettop before the fire and lua_settop back after, so the still-
  running outer C code sees exactly the stack it left. Without this the invoker
  left the stack shifted and the outer code / other DLLs' hooks indexed a stale
  slot -> ACCESS_VIOLATION.

Verified in-game: SetScript / HookScript / GetScript all work, `this` is the
tooltip (a1 is nil -- vanilla convention, not modern `self`), and no-op /
print / AddLine handlers fire cleanly on bag / equipped / merchant / auction.

Docs: correct the handler example to use `this` (not a `self` arg), and add the
caveat that re-entrant tooltip rebuilds from the handler (ShowCompareItem, Set*
on another tooltip) can crash in a hook-heavy client and must be deferred
(C_Timer.After(0, ...)) -- the event fires mid-build.
2026-07-02 23:18:17 -05:00
Brues 3179d1741e Add OnTooltipSetItem frame script
Backports the modern OnTooltipSetItem tooltip script to 1.12 as a real
frame script, settable via the standard SetScript / GetScript / HookScript
and firing whenever a tooltip's item is set (with the tooltip as self). Lets
addons annotate item tooltips by hooking one script instead of wrapping every
Set* method -- the destructive wrap-and-rebuild pattern (AtlasLoot's
AddSourceLine) that clobbers the tooltip's item linkage.

Fully native, two Game::HookAutoRegister co-hooks in Tooltip::SetItemEvent:

- Resolver (FUN_005295d0, the CGGameTooltip vtable script-name -> slot
  vmethod): for "OnTooltipSetItem" (a name the engine doesn't know) return a
  pointer to a C-side per-tooltip 8-byte {handler, context} cell. The tooltip
  object (alloc size 0x460) has no free slot, and the resolver's only contract
  is "return where the handler lives", so external storage behaves exactly
  like the engine's own slots -- proven by OnTooltipCleared (+0x44c).
- Item builder (FUN_0052B650, the shared funnel for SetBagItem /
  SetInventoryItem / SetHyperlink / SetMerchantItem / SetAuctionItem / ...):
  after the build, fire the handler via FUN_00702690 (the no-arg runner the
  clear uses for OnTooltipCleared). Recursion-guarded, and suppressed via
  Tooltip::SetItemEvent::Suppressor during Tooltip::Compare's internal
  equipped-item build so a handler doesn't run mid line-shift.

Item only for now; OnTooltipSetSpell / OnTooltipSetUnit follow the same
pattern with their own builders.
2026-07-02 19:41:29 -05:00
Brues 63c003db26 Add GameTooltip:IsEquippedItem; share tooltip-item resolver; fix GetItem for CGItem/auction tooltips
Introduce Item::TooltipItem::CurrentID -- the single "what item is this
tooltip showing" resolver. It trusts the link-path itemID (+0x398), and
otherwise resolves the CGItem GUID (+0x380) but only when no unit /
gameobject / spell id is set, since the clear does not zero the item GUID
and it goes stale after the tooltip moves on.

- GameTooltip:GetItem / HasItem now route through CurrentID: fixes GetItem
  returning nil for CGItem tooltips (bag / equipped / merchant / loot) and
  HasItem false-positiving on a unit tooltip after an item hover.
- Add GameTooltip:IsEquippedItem() (3.3.5 backport, registry index 63),
  resolving the displayed item via CurrentID + the equipment-slot walk.
- SetHyperlinkCompareItem: the comparisonTooltip (4th arg) fallback uses
  CurrentID; stat deltas now show by default and hide only for an explicit
  false / 0 (was: show only for a truthy shift).
- GetItem round-trips the random-suffix id in the link for auction/compare
  items (no CGItem instance) via the compare descriptor.
- FrameXML GameTooltip_ShowCompareItem falls back to self.compareLink, so a
  caller can hand it an explicit link when the tooltip's item state was
  rebuilt out from under it (e.g. an AH addon appending source lines).
- Offsets: compare-descriptor + FontString offsets; drop the duplicate
  tooltip item-id/guid offsets in favor of the canonical OFF_TOOLTIP_*.
2026-07-02 19:09:03 -05:00
Brues d01e51d862 Add GameTooltip:SetHyperlinkCompareItem (item comparison tooltips)
Backports the 3.3.5+ shopping-tooltip method, fully native (no lua_pcall):
fills a tooltip with the item currently equipped in the given item's slot
— a grey "Currently Equipped" header, the equipped item's tooltip, then a
green/red per-stat delta breakdown gated on shiftButton.

- Extract the stat engine (base record + on-equip-spell auras + random
  suffix) out of item/Stats.cpp into the shared Item::StatAccum, reused by
  C_Item.GetItemStats/GetItemStatDelta and the compare deltas.
- Build via the engine's own item-tooltip builder; prepend the grey header
  by shifting the line FontStrings, replicating the show/hide protocol,
  keying on the visible flag (no stale text) and resetting cell color.
- Delta colors sourced from ColorData.h via a new constexpr ByTag helper.
- offset selects the slot for two-slot items (rings/trinkets/1H weapons);
  shiftButton gates the deltas (true/1 vs false/0/nil); a comparisonTooltip
  4th arg lets it run link-free, 1:1 with the retail call.
- FrameXML GameTooltip_ShowCompareItem consumer with a dynamic,
  backdrop-derived anchor gap.
2026-07-02 13:41:10 -05:00
Brues 92d868424c Lift GameTooltip's 30-line cap (pure C++)
Vanilla GameTooltip:AddLine silently drops any line past the 30
TextLeftN/TextRightN FontStrings the FrameXML template declares, so
stat-heavy tooltips (pfUI eqcompare, AtlasLoot) lose their tail.

Tooltip::LinePool co-hooks the tooltip's XML-OnLoad region scan
(FUN_00529650, CGameTooltip vtable slot 9) via HookAutoRegister. After
the engine sets up the template's 30 lines it creates TextLeft/Right
31..60 in C++ (alloc from the CSimpleFontString pool -> ctor(parent) ->
name -> font copied from the previous line -> anchor) and appends each
pair to the tooltip's three line arrays, bumping numLinesAllocated. Runs
once per GameTooltip-family frame at creation and per /reload, so it
auto-covers GameTooltip, ShoppingTooltip1/2, ItemRefTooltip, AtlasLoot,
WorldMapTooltip, etc. No addon Lua and no per-line AddLine hook.

SetPoint stores offsets in internal coordinates, not pixels, so the
anchor offsets are scaled the way Script_SetPoint does
(pixel * [0x00832A44] / ([0x00832A4C] * 1024)) — without that a -2px gap
rendered ~3950px off.

Adds the tooltip line-pool, FontString-creation, and UI-scale offsets to
Offsets.h; documents the behavior in the README and marks TODO #97 done.
2026-07-02 00:08:30 -05:00
Brues eb4c42b1e0 Update TODO.md 2026-07-01 22:28:37 -05:00
Brues 2968f51661 GetItemStats: cover Turtle equip-spell stats; exact effect value
Cross-checked against Turtle's server source (patch_1172): ItemModType
is unchanged vanilla (0-7) and ApplyItemEquipSpell applies only
ON_EQUIP, confirming special stats are all equip-spell auras.

- Value now uses the engine's CalculateSimpleValue = EffectBasePoints +
  EffectBaseDice (new OFF_SPELL_RECORD_EFFECT_BASE_DICE = 0x10C) instead
  of a hardcoded +1; equal for the fixed-die stat auras but exact.
- Decode the additional stat auras Turtle puts on custom gear: dodge %
  (49), parry % (47), block chance % (51), spell crit % (57/71), and
  flat health (34) / mana (35) folded into the existing keys. Adds shield
  block value from the record's m_block. New keys ITEM_MOD_DODGE_RATING,
  ITEM_MOD_PARRY_RATING, ITEM_MOD_BLOCK_RATING, ITEM_MOD_CRIT_SPELL_RATING,
  ITEM_MOD_BLOCK_VALUE (percent stats keep vanilla-native values).

Documents the expanded key set in docs/API.md.
2026-07-01 22:27:42 -05:00
Brues dbd3f7f6a5 GetItemStats: add weapon DPS and equip-spell stats
Weapon DPS (avg damage across the slots / swing time) is emitted under
ITEM_MOD_DAMAGE_PER_SECOND_SHORT, matching 3.3.5's GetItemStats.

Vanilla stores the "special" gear stats (crit, attack power, spell
power/healing, hit, mp5, defense) as on-equip spells rather than stat
slots, so decode each item's SpellTrigger == ON_EQUIP spells - and the
random-suffix equip enchants - through the SPELL_AURA_* effects and fold
them into the modern GetItemStats keys. Percent stats (crit/hit/defense)
are reported as the raw vanilla percentage under the *_RATING key, since
vanilla has no (level-dependent) rating conversion; e.g. Krol Blade's
+1% crit is CRIT_MELEE_RATING = 1, not the 13 rating TBC/Era would show.
Generic attack power subsumes its paired ranged-AP aura; rarer auras
with no clean key (percent AP, per-weapon-line skill, procs) are skipped.

Documents the full key set and its sources in docs/API.md.
2026-07-01 16:28:42 -05:00
Brues 2f5cec35ea Add C_Item.GetItemStats / GetItemStatDelta
Read an item's stats out of the cached ItemStats_C record and return
them keyed by the FrameXML global-string names WoW's GetItemStats uses
(ITEM_MOD_*_SHORT, RESISTANCE0..6_NAME). GetItemStatDelta returns the
per-stat difference (item2 - item1).

Random suffixes are resolved from the link's third
item:id:enchant:SUFFIX:unique field through ItemRandomProperties.dbc ->
SpellItemEnchantment.dbc: type-3 (equip-spell) enchants defer to the
spell's SPELL_AURA_MOD_STAT / MOD_RESISTANCE effect (value =
EffectBasePoints + 1), type-4 add a direct resistance. Reported set
stays consistent with the base record - base attributes, armor, and the
six resistance schools - so a plain item and its suffixed variant report
the same categories.

Adds OFF_SPELL_RECORD_EFFECT_BASE_POINTS, VAR_ITEMRANDOMPROP_RECORDS/
COUNT, and OFF_ITEMRANDOMPROP_ENCHANT to Offsets.h.
2026-07-01 15:29:14 -05:00
Brues 706b8597b6 small lint 2026-07-01 14:59:46 -05:00
Brues 644aa21459 Add C_Item.IsItemGUIDInInventory
Returns whether an item with the given "0x..." GUID is currently carried
by the player. Parses the GUID, then scans the 19 equipment slots plus
bags 0..4, comparing each item's instance GUID (+0x08 -> +0x00, the same
read GetItemGUID uses). Reuses the existing ResolveEquipmentSlot /
ResolveBag / GetBagSlotCount iteration.

Scope matches retail: equipment slots count (verified in-game —
IsItemGUIDInInventory(GetItemGUID({equipmentSlotIndex=1})) is true), bags
count, bank does not (GetItemCount(itemID, true) covers the bank). Returns
false for a malformed / zero GUID.

Factored the CGItem GUID read into a shared ReadItemGuid helper (also used
by GetItemGUID). Documented in docs/API.md and the README Item row.
2026-07-01 14:34:31 -05:00
Brues a77d8f3fac Add Enum.InventoryType
Registers Enum.InventoryType (equip-type enum) as a hardcoded mirror of
Blizzard's 0..34 spec, via the same RegisterIntegerEnum path as
Enum.PowerType / Enum.AddOnSecurityStatus. Values 0..28 are the ones
vanilla items report; 29..34 (profession / equippable-spell types) are
post-vanilla, included so backport code referencing those keys resolves.

The field names (IndexHeadType, Index2HweaponType, …) aren't present in
engine data and don't derive cleanly from the INVTYPE key strings, so a
static mirror is the correct approach — it's a fixed API contract, not
engine-driven. For the live set of slot keys the engine actually has, use
C_Item.GetItemInventorySlotKey.

Documented in docs/API.md and the README enum row.
2026-07-01 13:59:21 -05:00
Brues 766d128a5d Add C_Item.GetItemSubClassInfo / GetItemInventorySlot{Info,Key}
Three item-taxonomy lookups backported to the C_Item namespace, reusing
the ItemSubClass.dbc scan and INVTYPE key table already in the tree.

- GetItemSubClassInfo(classID, subClassID) -> subClassName,
  subClassUsesInvType. Name is the localized ItemSubClass.dbc verbose
  form (short-name fallback). subClassUsesInvType comes from the flags
  field at record +0x10, bit 0x200 — set on exactly the armor material
  types (Misc/Cloth/Leather/Mail/Plate), clear on weapons/shields/etc.
  Bit derived by dumping ItemSubClass.dbc and verified against the 1.15
  client (Cloth true, One-Handed Swords false, Shields false).
- GetItemInventorySlotKey(inventorySlot) -> the equipLoc key
  ("INVTYPE_HEAD"), straight from the engine's INVTYPE table.
- GetItemInventorySlotInfo(inventorySlot) -> the localized slot name
  ("Head"), resolved via the FrameXML global of that key so it's
  localized on non-English clients.

All three return nil for out-of-range / missing input. Documented in
docs/API.md and the README Item row.
2026-07-01 13:46:09 -05:00
Brues 6f543ec762 Remove TTS — extracted to the standalone VanillaTTS DLL
C_VoiceChat / C_TTSSettings (the SAPI text-to-speech surface) is moving to
its own injected DLL (C:\Git\VanillaTTS) so it can grow a cross-platform
software-synthesis backend (Flite under Wine) without bloating ClassicAPI.

- Delete src/voicechat/Tts.cpp.
- Drop the ole32 link from CMakeLists (it was only for SAPI/COM).
- Strip the VoiceChat section + TOC from docs/API.md and the VoiceChat
  row + VOICE_CHAT_TTS_* events from the README.

CVar::Factory stays — it's general-purpose infra now, not TTS-specific
(reworded its header comment to drop the TTS example).
2026-06-29 17:45:48 -05:00
Brues d9102bae3b Relicense source headers from LGPL to GPL v3
The repo was internally inconsistent: the LICENSE file is GPL v3 but every
src/ header carried the LGPL v3 notice and the README said "LGPL v3 or
later". LGPL was also the wrong fit — this is an injected mod plus a
bundled addon, not a library other code links against, so LGPL's linking
exception buys nothing.

It's also required: src/voicechat/Tts.cpp is ported from awesome_wotlk's
VoiceChat.cpp, which is GPL v3, and a derivative of GPL code cannot be
relicensed under more permissive terms. Dependencies stay compatible —
MinHook (BSD-2) and the nampower-mirrored packet shapes (BSD) are
GPL-compatible, and SAPI is covered by the system-library exception.

Swept "GNU Lesser General Public License" -> "GNU General Public License"
across all 257 src/ headers and changed the README license line to match
the GPL v3 LICENSE file. Comment-only; no build impact.
2026-06-29 16:38:16 -05:00
Brues ca6189cc1e Document TTS 2026-06-29 16:18:53 -05:00
Brues 7e86aa9dc5 Add SAPI text-to-speech (C_VoiceChat / C_TTSSettings)
Backports modern WoW's TTS Lua surface, ported from awesome_wotlk's
3.3.5 VoiceChat.cpp. The SAPI core is client-version-agnostic; only the
three integration seams are ClassicAPI-native:

  - namespaces via Game::Lua::RegisterTableFunction
  - the five VOICE_CHAT_TTS_* events via Event::Custom (AutoReserve + Fire)
  - ttsVoice / ttsSpeed / ttsVolume settings via CVar::Factory, persisted
    to Config.wtf and clamped through change callbacks

C_VoiceChat: GetTtsVoices, GetRemoteTtsVoices (stub == local; vanilla has
no voice chat), SpeakText(voiceID, text[, dest, rate, volume]),
StopSpeakingText. C_TTSSettings: Get/SetSpeechRate, Get/SetSpeechVolume,
GetSpeechVoiceID, GetVoiceOptionName, SetVoiceOption[ByName],
SetDefaultSettings, RefreshVoices.

SAPI is created lazily on first use under an STA; SetNotifyCallbackFunction
marshals playback events back to the main thread's message pump (WoW pumps
every frame), so OnSpeechNotify fires PLAYBACK_STARTED/FINISHED on the main
thread — verified in-game. Voice enumeration uses CLSID_SpObjectTokenCategory
directly so the build needs no sphelper.h; GUIDs are instantiated via
<initguid.h>, so only ole32 is linked (no sapi.lib). Degrades gracefully
without SAPI (e.g. Wine): CoCreateInstance fails, SpeakText fires
PLAYBACK_FAILED and returns, voice list comes back empty, no crash.
2026-06-29 16:04:54 -05:00
Brues 2ca5624aa0 Add CVar::Factory for registering custom CVars from C++
Vanilla 1.12 exposes CVar reading and Lua-side RegisterCVar(name, default)
but no way to register a cvar with a change-callback or react to value
changes from C. CVar::Factory wraps the engine internals so modules can
publish persisted, validated settings.

- FUN_REGISTER_CVAR (0x0063DB90), __fastcall(name, edx, flags, default,
  callback, categoryId, hiddenBool, userData). Dedups by name (idempotent
  across /reload) and forces the archive flag bit on, so registered cvars
  persist to Config.wtf.
- FUN_SET_CVAR_VALUE (0x0063DF50), __thiscall(cvar, value, a3..a6) — NOT
  __fastcall (that routes value through EDX and leaves the callee popping
  4 bytes too many off the stack, corrupting ESP). Fires the change
  callback.
- Change callback ABI verified at both the set path (0x0063DF7D) and the
  re-register path (0x0063DC51): __fastcall(cvar, prevValue, newValue,
  userData) returning char — 0 rejects the change, nonzero accepts. Clamp
  by setting the clamped value from inside the callback and returning 0.

Offsets + struct fields (value +0x20, flags +0x1C, callback +0xBC,
userData +0xC0) documented with their call-site derivations.
2026-06-29 16:04:42 -05:00
Brues 19397782f7 CacheUtil 2026-06-29 14:30:12 -05:00
Brues 284b951038 Resolve UnitClassBase/UnitRaceBase "player" at addon-load
UnitClassBase("player") and UnitRaceBase("player") returned nil until
~PLAYER_ENTERING_WORLD because they always read the class/race byte from
the in-world unit descriptor ([unit+0x110]+0x79/+0x78), which doesn't
exist until the player object spawns.

Vanilla's Script_UnitClass (0x00518350) avoids this: for the literal
"player" token it reads a login-time global (FUN_005abde0 ->
DAT_00c27e81) instead of the descriptor. That global mirrors the local
player's UNIT_FIELD_BYTES_0 race/class/sex and is populated at
character-enter, so UnitClass("player") resolves at addon-load.

Mirror that fast path: the "player" token now reads the login-time byte
globals (race 0x00C27E80, class 0x00C27E81, sex 0x00C27E82); all other
tokens keep the descriptor path, so remote-unit behavior is unchanged.

The "player" test is factored into Unit::Identity::IsPlayerToken, shared
by both modules (and GuidForToken). It routes through the engine's own
SStrCmpI (FUN_SSTR_CMP_I) to match Script_UnitClass's comparator exactly,
rather than the CRT _stricmp.
2026-06-29 13:53:48 -05:00
Brues 1df48772f4 Serve player GUID from char-select before the world loads
Vanilla doesn't populate the local player object (and its +0xC0 GUID) until
SMSG_UPDATE_OBJECT in the main loop, after FrameScript_Initialize and the
addon load pass — so UnitGUID("player") / PlayerGuid() returned nil/0 at
addon file-load on first login.

Co-hook the glue enter-world worker (FUN_0046B500, behind Script_EnterWorld)
to snapshot the selected character's GUID at the moment of entry: read from
the selected-character struct (VAR_GLUE_SELECTED_CHAR; GUID at offset 0,
verified against Script_GetCharacterInfo), gated on the "entering world"
state code so the rename/locked bail paths don't capture. PlayerGuid()
returns the live engine value when present and falls back to the captured
GUID otherwise — the same value, just available from addon-load time.
Captured fresh per entry, so a character switch can't serve a stale GUID.

This makes the GUID *value* available early (keying SavedVariables,
comparisons, the aura/cast caster checks); the player *object* still doesn't
exist until +0xC0 populates.

Also simplify GetPlayerGuid() to a passthrough now that UnitGUID("player")
is reliably available early and cheap — the memo saved only a trivial
C-call + snprintf and the GUID is session-stable.

Add FUN_GLUE_ENTER_WORLD / VAR_GLUE_SELECTED_CHAR offsets.
2026-06-29 01:21:24 -05:00
Brues 939b52ae69 Recover dropped auras in C_UnitAuras; evict on removal
The 1.12 engine drops a unit's UNIT_FIELD_AURA slots when it loses sync with
the unit (rogue stealth, party range fluctuation) even though the auras are
still active server-side, so C_UnitAuras.GetAuraDataByIndex /
GetBuffDataByIndex returned nil for auras that are still up.

Fall back to the Aura::Source cache (caster/timing captured from
SMSG_SPELL_GO at cast time, which survives descriptor clears) when the
descriptor has no slot:

- Aura::Source: entries gain a helpful/harmful kind, recorded from the
  descriptor slot at application time (OnAuraAdded/OnAuraStacksChanged);
  SpellGo-only entries stay unknown and are excluded. New Enumerate() returns
  non-expired entries of a given kind for a unit.
- Aura::Data: a shared BuildTable feeds both the descriptor Push and a new
  cache-fed path; PushNthCacheFallback / AppendCacheFallbacks append cache
  entries after the descriptor ones, deduped against populated slots.
- Aura::Api: the index path and GetUnitAuras consult the fallback.

To keep the fallback from resurrecting auras that are genuinely gone
(replaced by a higher rank, dispelled, fell off early), co-hook OnAuraRemoved
(FUN_00612320) and evict the cache entry. A rogue stealthing does NOT route
through OnAuraRemoved (object-visibility drop, not per-aura removal), so
eviction and stealth-recovery coexist: real removals evict, visibility drops
don't.

Add FUN_ON_AURA_REMOVED offset.
Fixes #5
2026-06-28 13:47:26 -05:00
Brues 2403ff7233 Detect player cast/channel from packets, not just the cast global
The 1.12 engine short-circuits a chained same-spell recast: Spell_C_CastSpell
bails at `spellID == VAR_CURRENT_CAST_SPELL` before setting the global or
firing SPELLCAST_START, so the client never runs its cast path for the 2nd of
a back-to-back same-spell cast (common with nampower spell queueing). And the
player channel field (+0x228) is server-pushed, landing ~1 tick after the
channel starts, so polling it shows the bar ~1s late.

Rework the player UnitCastingInfo / UnitChannelInfo tracking into a hybrid:

- Normal casts: co-hook FUN_CAST_START_SET, stamp g_cast from the spellID
  arg before the original (zero latency); cleared by the VAR==0 WorldTick
  poll.
- Chained same-spell recasts: stamp g_cast from the SMSG_SPELL_START co-hook
  (server castTime), deduped against a fresh client stamp, exempt from the
  VAR clear, self-expiring on endMs.
- Channels: stamp g_channel from the same packet (engine-computed duration)
  instead of the lagging +0x228 poll; self-expire on endMs.
- Cast and channel are mutually exclusive for the player.
- Co-hook SMSG_SPELL_DELAYED for cast pushback (extends endMs + delayMs).

Add FUN_SPELL_DELAYED and FUN_CAST_START_SET offsets.
2026-06-25 20:10:19 -05:00
288 changed files with 11084 additions and 1182 deletions
+2
View File
@@ -27,6 +27,7 @@ Util\EventUtil.lua
Util\ItemUtil.lua
Util\AddOnCompat.lua
Util\FunctionUtil.lua
Util\CacheUtil.lua
Util\Pools.lua
Util\FrameWatcher.lua
Util\TimedCallback.lua
@@ -34,5 +35,6 @@ Util\EquipmentManager.lua
Util\PlayerUtil.lua
Util\SlashCommandsRegistry.lua
Util\LinkUtil.lua
Util\TradeSkillLink.lua
Util\IconDataProvider.lua
Util\GameTooltip.lua
+88
View File
@@ -0,0 +1,88 @@
-- On-demand, throttled cache warmer (/warmcache). A brute-force scan of the
-- whole ID space floods the client's network buffers and panics in
-- WDataStoreSmallBuffer ("not enough memory") — the engine can't hold that
-- many pending queries. So this warms a *bounded* range, started/stopped
-- manually, at a low fixed rate. Tune WARM_RATE / WARM_INTERVAL if needed,
-- but keep it modest. Note: IDs the server doesn't know (gaps in the range)
-- get no response, so their pending entries linger — warm ranges you expect
-- to be mostly real, not arbitrary millions.
--
-- /warmcache item 1 40000 warm item IDs 1..40000
-- /warmcache <type> <a> <b> type = item | quest | creature | object | all
-- /warmcache stop abort an in-progress warm
local WARM_RATE = 20 -- IDs queued per tick
local WARM_INTERVAL = 0.2 -- seconds between ticks (-> ~20 IDs/sec)
local WARM_FN = {
item = function(id) C_Item.RequestLoadItemDataByID(id) end,
quest = function(id) C_QuestLog.RequestLoadQuestByID(id) end,
creature = function(id) C_CreatureInfo.RequestLoadCreatureByID(id) end,
object = function(id) C_GameObjectInfo.RequestLoadGameObjectByID(id) end,
all = function(id)
C_QuestLog.RequestLoadQuestByID(id)
C_Item.RequestLoadItemDataByID(id)
C_CreatureInfo.RequestLoadCreatureByID(id)
C_GameObjectInfo.RequestLoadGameObjectByID(id)
end,
}
local warm = { running = false, cancel = false }
local function warm_msg(s)
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccff[WarmCache]|r " .. s)
end
local function warm_step(fn, id, stop, lastMsg)
if warm.cancel then
warm.running = false
warm_msg("stopped at " .. (id - 1))
return
end
local n = 0
while n < WARM_RATE and id <= stop do
fn(id)
n = n + 1
id = id + 1
end
if id > stop then
warm.running = false
warm_msg("done (through " .. stop .. ")")
else
if (id - 1) - lastMsg >= 1000 then
warm_msg((id - 1) .. " / " .. stop .. " ...")
lastMsg = id - 1
end
C_Timer.After(WARM_INTERVAL, function() warm_step(fn, id, stop, lastMsg) end)
end
end
SLASH_WARMCACHE1 = "/warmcache"
SlashCmdList["WARMCACHE"] = function(msg)
local args = {}
for tok in string.gmatch(msg or "", "%S+") do
table.insert(args, string.lower(tok))
end
local cmd = args[1]
if cmd == "stop" then
if warm.running then warm.cancel = true else warm_msg("not running") end
return
end
if warm.running then
warm_msg("already running - /warmcache stop first")
return
end
local fn = WARM_FN[cmd]
local a, b = tonumber(args[2]), tonumber(args[3])
if not fn or not a or not b or a < 1 or b < a then
warm_msg("usage: /warmcache item|quest|creature|object|all <startID> <stopID>")
warm_msg(" /warmcache stop")
return
end
warm.running, warm.cancel = true, false
warm_msg(string.format("warming %s %d..%d (~%d/sec, /warmcache stop to abort)",
cmd, a, b, math.floor(WARM_RATE / WARM_INTERVAL)))
warm_step(fn, a, b, a - 1)
end
+4 -13
View File
@@ -27,13 +27,12 @@ INVSLOT_BACK = 15;
INVSLOT_MAINHAND = 16;
INVSLOT_OFFHAND = 17;
INVSLOT_RANGED = 18;
INVSLOT_TABARD = 19;
INVSLOT_LAST_EQUIPPED = INVSLOT_TABARD;
INVSLOT_TABARD = 19; INVSLOT_LAST_EQUIPPED = INVSLOT_TABARD;
INVSLOTS_EQUIPABLE_IN_COMBAT = {
[INVSLOT_MAINHAND] = true,
[INVSLOT_OFFHAND] = true,
[INVSLOT_RANGED] = true,
[INVSLOT_MAINHAND] = true,
[INVSLOT_OFFHAND] = true,
[INVSLOT_RANGED] = true,
}
-- Item IDs
@@ -124,14 +123,6 @@ PLAYER_FACTION_COLORS =
[1] = PLAYER_FACTION_COLOR_ALLIANCE,
};
WOW_PROJECT_MAINLINE = 1;
WOW_PROJECT_CLASSIC = 2;
WOW_PROJECT_BURNING_CRUSADE_CLASSIC = 5;
WOW_PROJECT_WRATH_CLASSIC = 11;
WOW_PROJECT_CATACLYSM_CLASSIC = 14;
WOW_PROJECT_MISTS_CLASSIC = 19;
WOW_PROJECT_ID = WOW_PROJECT_CLASSIC;
--
-- Glyph
--
+89
View File
@@ -1,3 +1,92 @@
function GameTooltip_Hide()
GameTooltip:Hide()
end
GameTooltip.shoppingTooltips = GameTooltip.shoppingTooltips or { ShoppingTooltip1, ShoppingTooltip2 }
function GameTooltip_ShowCompareItem(self, shift)
if ( not self ) then
self = GameTooltip;
end
local item, link = self:GetItem();
link = link or self.compareLink
if ( not link ) then
return;
end
local shoppingTooltip1, shoppingTooltip2 = unpack(self.shoppingTooltips or { ShoppingTooltip1, ShoppingTooltip2 });
local SEPARATION = 6;
local backdrop = shoppingTooltip1.GetBackdrop and shoppingTooltip1:GetBackdrop();
local GAP = SEPARATION + ((type(backdrop) == "table" and backdrop.edgeSize) or 0);
local item1 = nil;
local item2 = nil;
local side = "left";
if ( shoppingTooltip1:SetHyperlinkCompareItem(link, 1, shift, self) ) then
item1 = true;
end
if ( shoppingTooltip2:SetHyperlinkCompareItem(link, 2, shift, self) ) then
item2 = true;
end
-- find correct side
local rightDist = 0;
local leftPos = self:GetLeft();
local rightPos = self:GetRight();
if ( not rightPos ) then
rightPos = 0;
end
if ( not leftPos ) then
leftPos = 0;
end
rightDist = GetScreenWidth() - rightPos;
if (leftPos and (rightDist < leftPos)) then
side = "left";
else
side = "right";
end
-- see if we should slide the tooltip
if ( self:GetAnchorType() and self:GetAnchorType() ~= "ANCHOR_PRESERVE" ) then
local totalWidth = 0;
if ( item1 ) then
totalWidth = totalWidth + shoppingTooltip1:GetWidth();
end
if ( item2 ) then
totalWidth = totalWidth + shoppingTooltip2:GetWidth();
end
if ( (side == "left") and (totalWidth > leftPos) ) then
self:SetAnchorType(self:GetAnchorType(), (totalWidth - leftPos), 0);
elseif ( (side == "right") and (rightPos + totalWidth) > GetScreenWidth() ) then
self:SetAnchorType(self:GetAnchorType(), -((rightPos + totalWidth) - GetScreenWidth()), 0);
end
end
if ( item1 ) then
shoppingTooltip1:SetOwner(self, "ANCHOR_NONE");
shoppingTooltip1:ClearAllPoints();
if ( side and side == "left" ) then
shoppingTooltip1:SetPoint("TOPRIGHT", self, "TOPLEFT", -GAP, -10);
else
shoppingTooltip1:SetPoint("TOPLEFT", self, "TOPRIGHT", GAP, -10);
end
shoppingTooltip1:SetHyperlinkCompareItem(link, 1, shift, self);
shoppingTooltip1:Show();
if ( item2 ) then
shoppingTooltip2:SetOwner(shoppingTooltip1, "ANCHOR_NONE");
shoppingTooltip2:ClearAllPoints();
if ( side and side == "left" ) then
shoppingTooltip2:SetPoint("TOPRIGHT", shoppingTooltip1, "TOPLEFT", -GAP, 0);
else
shoppingTooltip2:SetPoint("TOPLEFT", shoppingTooltip1, "TOPRIGHT", GAP, 0);
end
shoppingTooltip2:SetHyperlinkCompareItem(link, 2, shift, self);
shoppingTooltip2:Show();
end
end
end
+2 -7
View File
@@ -1,5 +1,4 @@
local className = UnitClass('player')
local classFile, classID = UnitClassBase('player')
local className, classFile, classID = UnitClass('player'), UnitClassBase('player')
PlayerUtil = PlayerUtil or {}
@@ -32,12 +31,8 @@ function PlayerUtil.GetCurrentSpeed()
return GetUnitSpeed("player") / 7 * 100
end
local playerGuid
function GetPlayerGuid()
if not playerGuid then
playerGuid = UnitGUID("player")
end
return playerGuid
return UnitGUID("player")
end
function IsPlayerGuid(guid)
@@ -0,0 +1,698 @@
-- Trade-skill *list* links (`|Htrade:...|h`) — backport of the 2.x+ shareable
-- profession link. The link is built entirely in the DLL
-- (`GetTradeSkillListLink` / `GetCraftListLink`, see src/tradeskill/Link.cpp);
-- this file only intercepts clicks on received links and renders the linker's
-- recipe list in a scroll frame (the UI half).
--
-- link: |cffffd000|Htrade:<skillLineID>:<curRank>:<maxRank>:<linkerName>:<base64 bits>|h[Name]|h|r
--
-- "Which recipes are known" is a bitfield over the skill line's canonical
-- recipe list (deterministic per DBC). The viewer lists the recipes the linker
-- knows (with an "X of Y" summary of the whole skill line), regardless of what
-- the reader can craft themselves.
-- Frame geometry mirrors the real TradeSkillFrame. Turtle ships an HD
-- 768x512 two-column frame (TW-TradeSkill art); stock 1.12.1 only has the
-- vanilla 384x512 single-column art, so on that client we fall back to the
-- native Blizzard_TradeSkillUI geometry (list on top, detail below) rather
-- than stretch the wide art. LO holds every layout value that differs.
local TURTLE = (TURTLE_WOW_VERSION ~= nil);
local FRAME_W = TURTLE and 768 or 384;
local FRAME_H = 512;
local NUM_ROWS = TURTLE and 19 or 8;
local ROW_HEIGHT = 16;
local LO = TURTLE and {
hit = { 2, 82, 2, 73 },
quads = {
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-TopLeft", 512, 256, "TOPLEFT" },
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-TopRight", 256, 256, "TOPRIGHT" },
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-BotLeft2", 512, 256, "BOTTOMLEFT" },
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-BotRight2", 256, 256, "BOTTOMRIGHT" },
},
close = { -77, -8 },
bar = { 596, 15, 76, -44 },
barBdr = { 604, 22 },
summary = { "TOPRIGHT", -110, -70 },
drop = { 40, -60 },
scroll = { 296, 330, 37, -98 },
detail = { 379, -113 },
exit = { "CENTER", "BOTTOMRIGHT", -128, 92 },
} or {
hit = { 0, 34, 0, 75 },
quads = {
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopLeft", 256, 256, "TOPLEFT" },
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopRight", 128, 256, "TOPRIGHT" },
{ "Interface\\TradeSkillFrame\\UI-TradeSkill-BotLeft", 256, 256, "BOTTOMLEFT" },
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-BotRight", 128, 256, "BOTTOMRIGHT" },
},
close = { -29, -8 },
bar = { 268, 15, 73, -37 },
barBdr = { 278, 22 },
summary = { "TOPRIGHT", -40, -58 },
drop = { 12, -62 },
scroll = { 296, 130, 21, -96 },
detail = { 28, -237 },
exit = { "CENTER", "TOPLEFT", 305, -422 },
};
local PROFESSION_ICONS = {
[171] = "Trade_Alchemy",
[164] = "Trade_BlackSmithing",
[185] = "INV_Misc_Food_15", -- Cooking
[333] = "Trade_Engraving", -- Enchanting
[202] = "Trade_Engineering",
[129] = "Spell_Holy_SealOfSacrifice", -- First Aid
[165] = "INV_Misc_ArmorKit_17", -- Leatherworking
[197] = "Trade_Tailoring",
[186] = "Trade_Mining",
[182] = "Trade_Herbalism",
[393] = "INV_Misc_Pelt_Wolf_01", -- Skinning
[356] = "Trade_Fishing",
};
if TURTLE then
PROFESSION_ICONS[142] = "Trade_Survival"
PROFESSION_ICONS[755] = "INV_Misc_Gem_01" -- Jewelcrafting
end
local function ProfessionIcon(skillLineID)
local icon = PROFESSION_ICONS[skillLineID];
return icon and ("Interface\\Icons\\" .. icon)
or "Interface\\Icons\\INV_Misc_QuestionMark";
end
local function DifficultyBand(skill, hi, lo)
if not hi or hi <= 0 then
return 3;
end
if not lo or lo == 0 then
lo = (hi ~= 25) and (hi - 25) or 0;
end
local yellow = (hi + lo) / 2;
if skill < lo then
return 0; -- orange (optimal)
elseif skill < yellow then
return 1; -- yellow (medium)
elseif skill < hi then
return 2; -- green (easy)
end
return 3; -- grey (trivial)
end
local BAND_COLOR = {
[0] = DIFFICULT_DIFFICULTY_COLOR, -- orange
[1] = FAIR_DIFFICULTY_COLOR, -- yellow
[2] = EASY_DIFFICULTY_COLOR, -- green
[3] = TRIVIAL_DIFFICULTY_COLOR, -- grey
};
local function CraftableCount(entry)
local reagents = entry.reagents;
if not reagents or table.getn(reagents) == 0 then
return nil;
end
local best;
for i = 1, table.getn(reagents) do
local r = reagents[i];
local need = r.count or 1;
local n = math.floor((C_Item.GetItemCount(r.itemID) or 0) / need);
if not best or n < best then
best = n;
end
end
return best;
end
local function ParseTradeLink(link)
-- Current format, with the linker's name: trade:id:cur:max:Name:bits
local _, _, id, cur, max, who, bits =
string.find(link, "^trade:(%d+):(%d+):(%d+):([^:]+):([%w%+/]*)$");
if not id then
-- Legacy format (no name): trade:id:cur:max:bits
who = nil;
_, _, id, cur, max, bits =
string.find(link, "^trade:(%d+):(%d+):(%d+):([%w%+/]*)$");
end
if not id then
return nil;
end
return tonumber(id), tonumber(cur), tonumber(max), who, bits or "";
end
------------------------------------------------------------------------------
-- Display frame (built lazily on first use)
------------------------------------------------------------------------------
local frame;
local function ResolveSubclasses(list, onLoaded)
local present, subs, anyItem = {}, {}, false;
for i = 1, table.getn(list) do
local r = list[i];
if r.createdItem and r.createdItem > 0 then
anyItem = true;
if not r.subclass then
-- GetItemInfoInstant's 3rd return is the localized subclass
-- name (or nil when uncached).
local _, _, subType = C_Item.GetItemInfoInstant(r.createdItem);
if subType and subType ~= "" then
r.subclass = subType;
elseif onLoaded then
-- Not cached — re-derive when the item loads.
Item:CreateFromItemID(r.createdItem):ContinueOnItemLoad(onLoaded);
end
end
if r.subclass and not present[r.subclass] then
present[r.subclass] = true;
table.insert(subs, r.subclass);
end
end
end
table.sort(subs);
return subs, anyItem;
end
local SUBCLASS_ALL = {};
local function SubClassButton_OnClick()
local v = this.value;
if v == SUBCLASS_ALL then
v = nil;
end
frame.subclassFilter = v;
UIDropDownMenu_SetText(v or ALL_SUBCLASSES, frame.dropdown);
frame:ApplyFilter();
end
local function SubClassDropDown_Initialize()
local info = {};
info.text = ALL_SUBCLASSES;
info.value = SUBCLASS_ALL;
info.checked = (frame.subclassFilter == nil);
info.func = SubClassButton_OnClick;
UIDropDownMenu_AddButton(info);
local subs = frame.subclasses or {};
for i = 1, table.getn(subs) do
local b = {};
b.text = subs[i];
b.value = subs[i];
b.checked = (frame.subclassFilter == subs[i]);
b.func = SubClassButton_OnClick;
UIDropDownMenu_AddButton(b);
end
end
local function RefreshSubclasses()
if not (frame and frame:IsShown() and frame.recipes) then
return;
end
local subs, anyItem = ResolveSubclasses(frame.recipes, RefreshSubclasses);
frame.subclasses = subs;
if anyItem then
UIDropDownMenu_Initialize(frame.dropdown, SubClassDropDown_Initialize);
UIDropDownMenu_SetWidth(120, frame.dropdown);
UIDropDownMenu_SetText(frame.subclassFilter or ALL_SUBCLASSES, frame.dropdown);
frame.dropdown:Show();
else
frame.dropdown:Hide();
end
if frame.subclassFilter then
frame:ApplyFilter();
end
end
local function CreateFrame_TradeSkillLink()
local f = CreateFrame("Frame", "ClassicAPITradeSkillLinkFrame", UIParent);
f:SetSize(FRAME_W, FRAME_H);
f:SetPoint("CENTER", 0, 0);
f:SetMovable(true);
f:EnableMouse(true);
f:SetHitRectInsets(LO.hit[1], LO.hit[2], LO.hit[3], LO.hit[4]);
f:RegisterForDrag("LeftButton");
f:SetScript("OnDragStart", function() this:StartMoving(); end);
f:SetScript("OnDragStop", function() this:StopMovingOrSizing(); end);
f:SetFrameStrata("DIALOG");
f:Hide();
local portrait = f:CreateTexture(nil, "BACKGROUND");
portrait:SetSize(60, 60);
portrait:SetPoint("TOPLEFT", 7, -6);
f.portrait = portrait;
for i = 1, table.getn(LO.quads) do
local q = LO.quads[i];
local tx = f:CreateTexture(nil, "BORDER");
tx:SetTexture(q[1]);
tx:SetSize(q[2], q[3]);
tx:SetPoint(q[4]);
end
local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormal");
title:SetPoint("TOP", 0, -18);
f.title = title;
local close = CreateFrame("Button", nil, f, "UIPanelCloseButton");
close:SetPoint("TOPRIGHT", LO.close[1], LO.close[2]);
local exit = CreateFrame("Button", nil, f, "UIPanelButtonTemplate");
exit:SetSize(80, 22);
exit:SetPoint(LO.exit[1], f, LO.exit[2], LO.exit[3], LO.exit[4]);
exit:SetText(EXIT or "Exit");
exit:SetScript("OnClick", function() f:Hide(); end);
local bar = CreateFrame("StatusBar", nil, f);
bar:SetSize(LO.bar[1], LO.bar[2]);
bar:SetPoint("TOPLEFT", LO.bar[3], LO.bar[4]);
bar:SetStatusBarTexture("Interface\\PaperDollInfoFrame\\UI-Character-Skills-Bar");
bar:SetStatusBarColor(0.0, 0.0, 1.0, 0.5);
bar:SetMinMaxValues(0, 1);
local barBg = bar:CreateTexture(nil, "BACKGROUND");
barBg:SetAllPoints(bar);
barBg:SetTexture(1, 1, 1, 0.2);
barBg:SetVertexColor(0.0, 0.0, 0.75, 0.5);
-- Border: tooltip edge around the bar (TradeSkillRankFrameBorder).
local border = CreateFrame("Frame", nil, bar);
border:SetSize(LO.barBdr[1], LO.barBdr[2]);
border:SetPoint("LEFT", -5, 1);
border:SetBackdrop({
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 16,
});
border:SetBackdropBorderColor(0.4, 0.4, 0.4);
local barName = bar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall");
barName:SetPoint("LEFT", 6, 1);
local barRank = bar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall");
barRank:SetJustifyH("LEFT");
barRank:SetWidth(128);
barRank:SetPoint("LEFT", barName, "RIGHT", 13, 0);
f.bar = bar;
f.barName = barName;
f.barRank = barRank;
local summary = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall");
summary:SetPoint(LO.summary[1], LO.summary[2], LO.summary[3]);
f.summary = summary;
local dropdown = CreateFrame("Frame", "ClassicAPITradeSkillLinkSubClass", f,
"UIDropDownMenuTemplate");
dropdown:SetPoint("TOPLEFT", LO.drop[1], LO.drop[2]);
f.dropdown = dropdown;
f.subclassFilter = nil; -- nil = all subclasses
local scroll = CreateFrame("ScrollFrame", "ClassicAPITradeSkillLinkScroll",
f, "FauxScrollFrameTemplate");
scroll:SetSize(LO.scroll[1], LO.scroll[2]);
scroll:SetPoint("TOPLEFT", LO.scroll[3], LO.scroll[4]);
scroll:SetScript("OnVerticalScroll", function()
FauxScrollFrame_OnVerticalScroll(ROW_HEIGHT, function() f:Refresh(); end)
end)
f.scroll = scroll;
f.rows = {};
for i = 1, NUM_ROWS do
local rowName = "ClassicAPITradeSkillLinkRow" .. i;
local row = CreateFrame("Button", rowName, f,
"ClassTrainerSkillButtonTemplate");
row:SetWidth(290); -- height (16) comes from the template
if i == 1 then
row:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0);
else
row:SetPoint("TOPLEFT", f.rows[i - 1], "BOTTOMLEFT", 0, 0);
end
row:SetNormalTexture("");
row:SetDisabledTexture("");
row:SetHighlightTexture("");
local txt = getglobal(rowName .. "Text");
if txt then
txt:ClearAllPoints();
txt:SetPoint("LEFT", row, "LEFT", 4, 0);
end
local sel = row:CreateTexture(nil, "BACKGROUND");
sel:SetTexture("Interface\\Buttons\\UI-Listbox-Highlight2");
sel:SetAllPoints(row);
sel:Hide();
row.select = sel;
-- Override the template's trainer scripts with ours.
row:SetScript("OnClick", function()
if this.entry then
f:SelectRecipe(this.entry);
end
end);
f.rows[i] = row;
end
local NUM_REAGENTS = 8;
local prodIcon = CreateFrame("Button", nil, f);
prodIcon:SetSize(37, 37); -- TradeSkillSkillIcon is 37x37
prodIcon:SetPoint("TOPLEFT", LO.detail[1], LO.detail[2]);
prodIcon:SetNormalTexture("Interface\\Icons\\INV_Misc_QuestionMark");
local hdrLeft = prodIcon:CreateTexture(nil, "BACKGROUND");
hdrLeft:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-DetailHeaderLeft");
hdrLeft:SetSize(256, 64); -- Blizzard's native fill width (+64 cap = 320 total)
hdrLeft:SetPoint("TOPLEFT", prodIcon, "TOPLEFT", -8, 6);
local hdrRight = prodIcon:CreateTexture(nil, "BACKGROUND");
hdrRight:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-DetailHeaderRight");
hdrRight:SetSize(64, 64);
hdrRight:SetPoint("TOPLEFT", hdrLeft, "TOPRIGHT", 0, 0);
local prodCount = prodIcon:CreateFontString(nil, "ARTWORK", "NumberFontNormal");
prodCount:SetPoint("BOTTOMRIGHT", -5, 2);
prodIcon.count = prodCount;
f.prodIcon = prodIcon;
local prodName = prodIcon:CreateFontString(nil, "ARTWORK", "GameFontNormal");
prodName:SetPoint("LEFT", prodIcon, "RIGHT", 5, 0);
prodName:SetWidth(240);
prodName:SetJustifyH("LEFT");
prodName:SetJustifyV("MIDDLE");
f.prodName = prodName;
local prodDesc = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall");
prodDesc:SetPoint("TOPLEFT", prodIcon, "BOTTOMLEFT", 0, -6);
prodDesc:SetWidth(280);
prodDesc:SetJustifyH("LEFT");
prodDesc:SetJustifyV("TOP");
f.prodDesc = prodDesc;
local reagentLabel = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall");
reagentLabel:SetPoint("TOPLEFT", prodDesc, "BOTTOMLEFT", 0, -8);
reagentLabel:SetText(SPELL_REAGENTS or "Reagents:");
f.reagentLabel = reagentLabel;
f.reagents = {};
for i = 1, NUM_REAGENTS do
local rowN = math.floor((i - 1) / 2);
local col = (i - 1) - rowN * 2;
local btn = CreateFrame("Button", nil, f);
btn:SetSize(39, 39);
btn:SetPoint("TOPLEFT", reagentLabel, "BOTTOMLEFT",
col * 147 - 5, -3 - rowN * 43);
btn:SetNormalTexture("Interface\\Icons\\INV_Misc_QuestionMark");
local plate = btn:CreateTexture(nil, "BACKGROUND");
plate:SetTexture("Interface\\QuestFrame\\UI-QuestItemNameFrame");
plate:SetSize(128, 64);
plate:SetPoint("LEFT", btn, "RIGHT", -10, 0);
local cnt = btn:CreateFontString(nil, "ARTWORK", "NumberFontNormal");
cnt:SetPoint("BOTTOMRIGHT", -4, 1);
btn.count = cnt;
local nm = btn:CreateFontString(nil, "ARTWORK", "GameFontHighlight");
nm:SetPoint("LEFT", plate, "LEFT", 15, 0);
nm:SetSize(90, 36);
nm:SetJustifyH("LEFT");
nm:SetJustifyV("MIDDLE");
btn.nameText = nm;
btn:SetScript("OnEnter", function()
if this.itemLink then
GameTooltip:SetOwner(this, "ANCHOR_RIGHT");
GameTooltip:SetHyperlink(this.itemLink);
GameTooltip:Show();
end
end);
btn:SetScript("OnLeave", GameTooltip_Hide);
btn:Hide();
f.reagents[i] = btn;
end
function f:RenderDetail(entry)
local function itemInfo(id)
local item = Item:CreateFromItemID(id);
if not item:IsItemDataCached() then
item:ContinueOnItemLoad(function()
if self:IsShown() and self.selectedSpellID == entry.spellID then
self:RenderDetail(entry);
end
end);
end
return item:GetItemName(), item:GetItemIcon(), item:GetItemLink();
end
local prodTex, prodText, prodLink;
local created = entry.createdItem or 0;
if created > 0 then
local name, tex, link = itemInfo(created);
prodText = name or entry.name;
prodTex = tex;
prodLink = link;
else
prodText = entry.name;
prodTex = C_Spell.GetSpellTexture(entry.spellID);
end
if created > 0 then
self.prodDesc:SetText("");
else
self.prodDesc:SetText(C_Spell.GetSpellDescription(entry.spellID) or "");
end
self.prodIcon:SetNormalTexture(prodTex or
"Interface\\Icons\\INV_Misc_QuestionMark");
self.prodName:SetText(prodText or "");
self.prodName:SetTextColor(NORMAL_FONT_COLOR:GetRGB());
self.prodIcon.itemLink = prodLink; -- item tooltip when created > 0
self.prodIcon.spellID = entry.spellID; -- else spell tooltip
local made = entry.numMade or 0;
self.prodIcon.count:SetText(made > 1 and tostring(made) or "");
self.prodIcon:Show();
local reagents = entry.reagents or {};
for i = 1, NUM_REAGENTS do
local btn = self.reagents[i];
local r = reagents[i];
if r then
local name, tex, link = itemInfo(r.itemID);
btn:SetNormalTexture(tex or
"Interface\\Icons\\INV_Misc_QuestionMark");
btn.itemLink = link;
btn.nameText:SetText(name or ("item:" .. r.itemID));
local need = r.count or 1;
local have = C_Item.GetItemCount(r.itemID) or 0;
if have >= need then
btn:GetNormalTexture():SetVertexColor(1, 1, 1);
btn.nameText:SetTextColor(HIGHLIGHT_FONT_COLOR:GetRGB());
else
btn:GetNormalTexture():SetVertexColor(0.5, 0.5, 0.5);
btn.nameText:SetTextColor(GRAY_FONT_COLOR:GetRGB());
end
local haveText = (have >= 100) and "*" or tostring(have);
btn.count:SetText(haveText .. " /" .. need);
btn:Show();
else
btn.itemLink = nil;
btn:Hide();
end
end
end
prodIcon:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT");
if this.itemLink then
GameTooltip:SetHyperlink(this.itemLink);
elseif this.spellID then
GameTooltip:SetSpellByID(this.spellID);
end
GameTooltip:Show();
end);
prodIcon:SetScript("OnLeave", GameTooltip_Hide);
function f:SelectRecipe(entry)
self.selectedSpellID = entry.spellID;
self.selectedEntry = entry;
self:RenderDetail(entry);
self:Refresh();
end
function f:ClearDetail()
self.selectedSpellID = nil;
self.selectedEntry = nil;
self.prodIcon:Hide();
self.prodName:SetText("");
self.prodDesc:SetText("");
for i = 1, NUM_REAGENTS do
self.reagents[i]:Hide();
end
end
function f:Refresh()
local recipes = self.displayed or {};
local total = table.getn(recipes);
FauxScrollFrame_Update(self.scroll, total, NUM_ROWS, ROW_HEIGHT);
local offset = FauxScrollFrame_GetOffset(self.scroll);
for i = 1, NUM_ROWS do
local row = self.rows[i];
local entry = recipes[offset + i];
if entry then
-- Append " [N]" when the reader can craft any (Blizzard's list).
local craftable = CraftableCount(entry);
local label = entry.name or "";
if craftable and craftable > 0 then
label = label .. " [" .. craftable .. "]";
end
row:SetText(label);
local r, g, b = BAND_COLOR[entry.band or 3]:GetRGB();
if self.selectedSpellID and entry.spellID == self.selectedSpellID then
row.select:SetVertexColor(r, g, b);
row.select:Show();
row:SetTextColor(1, 1, 1);
else
row.select:Hide();
row:SetTextColor(r, g, b);
end
row.spellID = entry.spellID;
row.entry = entry;
row:Show();
else
row.spellID = nil;
row.entry = nil;
row.select:Hide();
row:Hide();
end
end
end
function f:ApplyFilter()
local src = self.recipes or {};
if not self.subclassFilter then
self.displayed = src;
else
local out = {};
for i = 1, table.getn(src) do
if src[i].subclass == self.subclassFilter then
table.insert(out, src[i]);
end
end
self.displayed = out;
end
local sb = getglobal("ClassicAPITradeSkillLinkScrollScrollBar");
if sb then
sb:SetValue(0);
end
self:Refresh();
end
f:RegisterEvent("BAG_UPDATE_DELAYED");
f:SetScript("OnEvent", function()
if not f:IsShown() then
return;
end
f:Refresh();
if f.selectedEntry then
f:RenderDetail(f.selectedEntry);
end
end);
return f;
end
local function ShowTradeSkillLink(link, text)
local id, cur, max, who, bits = ParseTradeLink(link);
if not id then
return false;
end
local recipes = C_TradeSkillUI.GetTradeSkillListRecipes(id, bits);
if not recipes then
return false;
end
local prof = text and string.match(text, "|h%[(.+)%]|h") or "Trade Skills";
local total = table.getn(recipes);
local knownList = {};
for i = 1, total do
local r = recipes[i];
if r.isKnown then
r.band = DifficultyBand(cur or 0, r.trivialLevel, r.greenLevel);
r.name = GetSpellInfo(r.spellID) or ("spell:" .. r.spellID);
table.insert(knownList, r);
end
end
table.sort(knownList, function(a, b)
if a.band ~= b.band then
return a.band < b.band;
end
return a.name < b.name;
end);
local known = table.getn(knownList);
if not frame then
frame = CreateFrame_TradeSkillLink();
end
frame.title:SetText(who and who ~= "" and (who .. " - " .. prof) or prof);
SetPortraitToTexture(frame.portrait, ProfessionIcon(id));
frame.bar:SetMinMaxValues(0, (max and max > 0) and max or 1);
frame.bar:SetValue(cur or 0);
frame.barName:SetText(prof);
frame.barRank:SetText(string.format("%d/%d", cur or 0, max or 0));
frame.summary:SetText(string.format("%d of %d recipes known", known, total));
frame.recipes = knownList;
frame.subclassFilter = nil;
frame:ClearDetail();
frame:Show();
RefreshSubclasses();
frame:ApplyFilter();
local first = frame.displayed and frame.displayed[1];
if first then
frame:SelectRecipe(first);
end
return true;
end
------------------------------------------------------------------------------
-- Click interception: handle `trade:` before the engine's SetHyperlink (which
-- in 1.12 only knows item:/enchant: and would reject the link).
------------------------------------------------------------------------------
local originalSetItemRef = SetItemRef;
function SetItemRef(link, text, button, chatFrame)
if type(link) == "string" and string.sub(link, 1, 6) == "trade:" then
if IsShiftKeyDown() and ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
ChatFrameEditBox:Insert(text);
return;
end
if ShowTradeSkillLink(link, text) then
return;
end
end
return originalSetItemRef(link, text, button, chatFrame);
end
local function CreateProfessionLinkButton(parentName, getLink)
local parent = _G[parentName];
local btnName = parentName .. "LinkButton";
if _G[btnName] or not parent then
return;
end
local btn = CreateFrame("Button", btnName, parent);
btn:SetSize(16, 16);
btn:SetPoint("LEFT", _G[parentName .. "TitleText"], "RIGHT", 4, 0);
local icon = "Interface\\GossipFrame\\PetitionGossipIcon";
btn:SetNormalTexture(icon);
btn:SetHighlightTexture(icon, "ADD");
btn:SetScript("OnClick", function()
local link = getLink();
if not link then
return;
end
if not ChatFrameEditBox:IsVisible() then
ChatFrameEditBox:Show();
end
ChatFrameEditBox:Insert(link);
end);
btn:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_TOPLEFT");
GameTooltip:SetText(LINK_TRADESKILL_TOOLTIP, nil, nil, nil, nil, 1);
GameTooltip:Show();
end);
btn:SetScript("OnLeave", GameTooltip_Hide);
end
EventUtil.ContinueOnAddOnLoaded("Blizzard_TradeSkillUI", function()
CreateProfessionLinkButton("TradeSkillFrame", C_TradeSkillUI.GetTradeSkillListLink);
end);
EventUtil.ContinueOnAddOnLoaded("Blizzard_CraftUI", function()
CreateProfessionLinkButton("CraftFrame", C_TradeSkillUI.GetCraftListLink);
end);
+2
View File
@@ -6,6 +6,7 @@ FOCUSTARGET = "Focus Target";
GENERIC_FRACTION_STRING = "%d/%d";
GOLD_AMOUNT_SYMBOL = "g";
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
ITEM_DELTA_DESCRIPTION = "If you replace this item, the following stat changes will occur:";
ITEMS_EQUIPPED = "%d |4item:items; equipped";
ITEMS_IN_INVENTORY = "%d |4item:items; in inventory";
ITEMS_NOT_IN_INVENTORY = "%d |4item:items; missing";
@@ -23,3 +24,4 @@ SLASH_EQUIP_SET2 = "/equipset";
SLASH_FOCUS1 = "/focus";
SLASH_FOCUS2 = "/focus";
TARGETFOCUS = "Target Focus";
LINK_TRADESKILL_TOOLTIP = "Click here to create a link to your profession.";
+4 -1
View File
@@ -99,7 +99,10 @@ target_compile_definitions(
${PROJECT_NAME}
PRIVATE CLASSICAPI_VERSION_VALUE=${CLASSICAPI_VERSION_VALUE})
target_link_libraries(${PROJECT_NAME} PRIVATE minhook advapi32)
# ole32: COM (CoInitializeEx / CoCreateInstance / CoTaskMemFree) for the
# SAPI text-to-speech backend in src/voicechat/Tts.cpp. The SAPI GUIDs are
# instantiated in that TU via <initguid.h>, so no sapi.lib is needed.
target_link_libraries(${PROJECT_NAME} PRIVATE minhook advapi32 ole32)
target_include_directories(
${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+29 -6
View File
@@ -33,22 +33,24 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetClassicExpansionLevel` |
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `frame:SetShown`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsEventRegistered` |
| [FriendList](docs/API.md#friendlist) | `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName` |
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID` |
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex` |
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetUnitAura` |
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:IsEquippedItem`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetHyperlinkCompareItem`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetUnitAura`, `OnTooltipSetItem` (script) |
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
| [Input](docs/API.md#input) | `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount` |
| [Macros](docs/API.md#macros) | `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
| [Map](docs/API.md#map) | `C_Map.GetBestMapForUnit` |
| [Map](docs/API.md#map) | `C_Map.GetBestMapForUnit`, `C_Map.GetMapOverlays` |
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems` |
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
| [NameCache](docs/API.md#namecache) | `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
| [Quest](docs/API.md#quest) | `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
| [Quest](docs/API.md#quest) | `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
| [Screen](docs/API.md#screen) | `GetPhysicalScreenSize` |
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
@@ -56,9 +58,12 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
| [Table](docs/API.md#table) | `table.wipe` |
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime` |
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes` |
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
| [Unit](docs/API.md#unit) | `GetUnitSpeed`, `UnitClassBase`, `UnitGUID`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsPossessed`, `UnitPower`, `UnitPowerMax`, `UnitPowerType`, `UnitRaceBase`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras` |
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterComboDuration` |
| [VoiceChat](docs/API.md#voicechat) | `C_VoiceChat.GetTtsVoices`, `C_VoiceChat.GetRemoteTtsVoices`, `C_VoiceChat.SpeakText`, `C_VoiceChat.StopSpeakingText`, `C_TTSSettings.GetSpeechRate`, `C_TTSSettings.GetSpeechVolume`, `C_TTSSettings.GetSpeechVoiceID`, `C_TTSSettings.GetVoiceOptionName`, `C_TTSSettings.SetSpeechRate`, `C_TTSSettings.SetSpeechVolume`, `C_TTSSettings.SetVoiceOption`, `C_TTSSettings.SetVoiceOptionByName`, `C_TTSSettings.SetDefaultSettings`, `C_TTSSettings.RefreshVoices` |
| [XMLUtil](docs/API.md#xmlutil) | `C_XMLUtil.DoesTemplateExist`, `C_XMLUtil.GetTemplateInfo`, `C_XMLUtil.GetTemplates` |
### GlueXML calls
@@ -117,10 +122,13 @@ when launching with `-console`), not as Lua functions. See the
| `HEARTHSTONE_BOUND` | *(none)* |
| `ITEM_DATA_LOAD_RESULT` | `itemID, success` |
| `LOOT_SCAN_COMPLETED` | *(none)* |
| `LOSS_OF_CONTROL_ADDED` | `eventIndex` |
| `LOSS_OF_CONTROL_UPDATE` | `unitToken` (always `"player"`) |
| `MODIFIER_STATE_CHANGED` | `keyName, down` |
| `NAME_PLATE_CREATED` | `nameplateFrame` |
| `NAME_PLATE_UNIT_ADDED` | `unitToken` ("nameplateN") |
| `NAME_PLATE_UNIT_REMOVED` | `unitToken` ("nameplateN") |
| `PLAYER_EQUIPMENT_CHANGED` | `equipmentSlot, hasCurrent` |
| `PLAYER_FOCUS_CHANGED` | *(none)* |
| `PLAYER_STARTED_LOOKING` | *(none)* |
| `PLAYER_STOPPED_LOOKING` | *(none)* |
@@ -130,8 +138,14 @@ when launching with `-console`), not as Lua functions. See the
| `PLAYER_STOPPED_TURNING` | *(none)* |
| `QUEST_ACCEPTED` | `questLogIndex, questID` |
| `QUEST_DATA_LOAD_RESULT` | `questID, success` |
| `QUEST_REMOVED` | `questID` |
| `QUEST_TURNED_IN` | `questID, xpReward, moneyReward` |
| `UPDATE_INVENTORY_DURABILITY` | *(none)* |
| `UPDATE_SHAPESHIFT_FORM` | *(none)* |
| `VOICE_CHAT_TTS_PLAYBACK_STARTED` | `numConsumers, utteranceID, durationMS, destination` |
| `VOICE_CHAT_TTS_PLAYBACK_FINISHED` | `numConsumers, utteranceID, destination` |
| `VOICE_CHAT_TTS_PLAYBACK_FAILED` | `status, utteranceID, destination` |
| `VOICE_CHAT_TTS_VOICES_UPDATE` | *(none)* |
### Globals
@@ -143,6 +157,7 @@ when launching with `-console`), not as Lua functions. See the
| Unit stat | `LE_UNIT_STAT_STRENGTH` … `LE_UNIT_STAT_SPIRIT` |
| Addon security | `Enum.AddOnSecurityStatus.{Secure,Insecure,Banned,NotAvailable}` |
| Power type | `Enum.PowerType.{HealthCost,None,Mana,Rage,Focus,Energy,Happiness}` |
| Inventory type | `Enum.InventoryType.Index*Type` (0–34, e.g. `IndexHeadType`=1 … `IndexRelicType`=28) |
### Unit tokens
@@ -164,6 +179,14 @@ native targeting bindings instead of orphaned at the bottom.
See [Focus / Bindings](docs/API.md#bindings-focustarget--targetfocus) for the implementation note.
### Behaviors
Transparent engine tweaks — no API to call, they just fix a vanilla limitation:
| Tweak | Effect |
|-------|--------|
| Tooltip line cap | Lifts `GameTooltip`'s hard 30-line limit to 60 for every `GameTooltipTemplate` frame (`GameTooltip`, `ShoppingTooltip1/2`, `ItemRefTooltip`, AtlasLoot, …). Stat-heavy tooltips and comparison blocks (e.g. pfUI's eqcompare) no longer have their extra lines silently dropped. Done in pure C++ by growing the engine's FontString pool at tooltip-creation time. |
## Installation
Download the prebuilt `ClassicAPI.dll` from the
@@ -265,4 +288,4 @@ at configure time; the value exposed to Lua will be `X*10000 + Y*100 + Z`.
## License
LGPL v3 or later. See the headers in `src/` for the full notice.
GPL v3 or later. See the headers in `src/` for the full notice.
+316 -2
View File
@@ -2725,7 +2725,26 @@ matching modern's 3-tuple. `CastSpellNoToggle("<name>")` macros are
also recognized because `Spell::CastNoToggle` already hooks the same
parser to register that pattern.
## 88. `PLAYER_EQUIPMENT_CHANGED(equipmentSlot, hasCurrent)` event
## ~~88. `PLAYER_EQUIPMENT_CHANGED(equipmentSlot, hasCurrent)` event~~ — DONE
**Resolution (2026-07-04, src/player/Equipment.cpp):** the 2026-05-22
investigation kept trying to hook a FIRER of UNIT_INVENTORY_CHANGED.
The winning move was the opposite: the engine has a generic
descriptor-field observer system (the "__AUCMirrorHandler__" nodes —
registrar `FUN_00467E70`, dispatcher `FUN_00465570` with per-node
mirror + memcmp change detection). `FUN_004F8CC0` registers observers
for the bag/backpack/bank/keyring GUID ranges but never for equipment
`0x4A8..0x538` — so we co-hook it and REGISTER OUR OWN observer per
equipment field, mirroring the engine's registration exactly (bank 4,
size 8, player GUID). Callback ABI verified from the dispatcher call
site at `0x004655DC` + `FUN_004F8DB0`'s `RET 0x10`:
`__fastcall(fieldOffset /*ecx*/, size /*edx*/, guidLo, guidHi,
oldValue*, userArg)`. hasCurrent read from the invMgr GUID array (the
same "current truth" the engine's bag callback reads). No polling, no
hot hook, engine-managed lifetime (setup re-runs each enter-world).
Offsets documented at `FUN_DESC_OBSERVER_REGISTER` in Offsets.h.
Original notes below kept for the investigation trail.
WotLK-era event that fires every time a paperdoll slot changes
(equip, unequip, swap). Heavily used by gear-tracking,
@@ -2828,7 +2847,33 @@ Same diff feeds `EQUIPMENT_SETS_CHANGED` /
module — currently those fire only on user-side mutations, not
on inventory changes that affect a set's "equipped count".
## 89. `UPDATE_INVENTORY_DURABILITY` event
## ~~89. `UPDATE_INVENTORY_DURABILITY` event~~ — DONE
**Resolution (2026-07-04, src/player/Equipment.cpp):** co-hook the
engine's inventory-alerts recompute `FUN_004C7EE0` — the routine that
walks equipment slots, reads each item's durability/broken state from
the descriptor, and unconditionally fires vanilla's
`UPDATE_INVENTORY_ALERTS` (event 501). The engine calls it whenever an
owned item's fields change (the item post-update handler `FUN_005D9A90`
routes every item values-update through it, alongside
UNIT_INVENTORY_CHANGED and the bag-update chain) plus equip and
enter-world paths — i.e., it IS the engine's own "durability may have
changed" signal. Post-original we diff a per-slot {guid, durability}
snapshot (seeded silently on the first recompute per enter-world) and
fire once per recompute with a real change. No polling, no hot hook.
**Dead end tried first:** bank-1 (ITEM field) descriptor observers per
equipped item, via the TODO #88 observer system. They register without
error but are NEVER invoked — item values-updates demonstrably arrive
(probed the values pre-pass `FUN_00465330`) and player-bank observers
fire in the same session; registration arithmetic fully verified (bank
base `FUN_00465690(1)=0x18`, bank walk item 0→1→2, anchors match
dispatch). Prime suspect: the registrar's internal wrapper lookup
(`FUN_00464890`, the observer registry — NOT the object manager)
silently no-ops for item objects. See the DEAD END note at
`OFF_DESC_ITEM_DURABILITY` in Offsets.h.
Original notes below kept for the investigation trail.
Fires when any equipped item's durability changes — picks up
combat damage, repairs, item destruction. Used by Repair-O-Matic
@@ -3301,3 +3346,272 @@ the matching read. Both should be straightforward once the field
offset is identified — 3.3.5's `Script_*` equivalents are a good
starting point for what the parameter shape and bounds-clamp
behavior should be.
## ~~97. Lift GameTooltip's 30-line cap~~ — DONE
Shipped as [src/tooltip/LinePool.cpp](src/tooltip/LinePool.cpp): pure C++,
no addon Lua. Co-hooks the XML-OnLoad region scan (FUN_00529650, vtable
slot 9) via `Game::HookAutoRegister`; after the engine sets up the
template's 30 lines it creates `TextLeft/Right 31..60` in C++ (alloc →
ctor → name → font-copied-from-prev → anchor, offsets scaled pixel →
internal like Script_SetPoint) and appends them to the tooltip's three
line arrays, bumping `numLinesAllocated`. Runs once per GameTooltip-family
frame at creation + per `/reload`, so it auto-covers GameTooltip,
ShoppingTooltip1/2, ItemRefTooltip, AtlasLootTooltip, WorldMapTooltip, …
Verified in-game: AtlasLoot tooltip rendered a 31st line correctly. Cap is
`kMaxLines = 60`. The design notes below record the RE trail; Approach A
(hook AddLine) / Approach B (addon Lua) were both explored and dropped in
favor of the scan-hook.
Vanilla 1.12's `GameTooltip:AddLine` silently drops any line past the
pool of `TextLeftN`/`TextRightN` FontStrings the XML template
pre-declares (30). Modern (3.3.5+) engines grow the pool on demand.
Addons that append comparison summaries, item-info footers, or aura
details to a full item tooltip get their tail chopped. Practical
example — pfUI's eqcompare block (base-stat deltas inline + extended
stats appended at the bottom) hits the cap on stat-heavy items where
the vendor tooltip already has ~22 lines and the compare block adds
another 10; extended-stat rows past line 30 disappear silently.
Also affects `ShoppingTooltip1`/`ShoppingTooltip2` (same
`GameTooltipTemplate`).
### Root cause (verified in Ghidra on the 1.12 Octo binary)
`AddLine` internal impl at `FUN_00530270` — the shared body called
from both `Script_GameTooltipAddLine` (`FUN_00531630`, method ptr
stored in the GameTooltip method table alongside the `"AddLine"`
string at `0x00854560`) and `Script_GameTooltipAddDoubleLine`:
```c
void FUN_00530270(int this, char *leftText, char *rightText,
colorLeft, colorRight, wrapFlag) {
if ((*(int *)(this + 0x31c) != *(int *)(this + 0x320)) && anyHasText) {
// set leftText on textLeft[numLines_used],
// set rightText on textRight[numLines_used],
// wrapFlag[numLines_used] = wrapFlag,
// ++numLines_used
}
// else: silently drop the line
}
```
`0x320 == 800` decimal, matches decompile. Pool exhausted ⇒ nothing
happens.
### CGameTooltip struct offsets (1.12)
Derived from `FUN_00530270` and the pool-init at `FUN_00529650`:
| Offset | Field |
|--------|----------------------------------------------------|
| `+0x31c` | `numLines` — current write index (0-based) |
| `+0x320` | `numLinesAllocated` — pool size (30 default) |
| `+0x328..+0x32c` | `textLeft[]` — array of `CSimpleFontString *`, count `+0x320` |
| `+0x334..+0x338` | `textRight[]` — same, symmetric right column |
| `+0x340..+0x344` | `wrapFlag[]` — per-line wrap bool |
Actual layout of the array slots: `+0x328` is the first ptr in an
external heap block whose base is `param_1[0xcb]` = `+0x32c`. The
init function reallocates all three arrays whenever `numLinesAllocated`
would change — see `FUN_00536c80` (the reallocator) and `FUN_004368c0`
(a matching alloc for the wrap-flag array of a different element size).
### The pool-init function
`FUN_00529650` — walks `%sTextLeftN` / `%sTextRightN` FontString names
from the FrameXML-declared pool, counts them, reallocates the three
arrays, stores the pointers. Called via vtable slot at `0x00808f84`
(only xref) — likely `SetOwner` or an equivalent virtual reset method
runs it. It's the natural growth path — if we can force it to see a
larger pool of Lua-visible FontStrings and trigger it, the arrays
resize themselves.
### The 3.3.5 reference (already REd)
3.3.5's `GameTooltip::AddLine` at `0x0061fec0` has the grow-on-demand
branch we want to backport. Shape:
```c
if (currentLine == numLinesAllocated - 1) {
format(buf, 256, "%sTextLeft%d", tooltipName, currentLine + 1);
auto *fs = SimpleFontString::create(this, /*layer*/2, /*subLayer*/1);
fs->setName(buf);
fs->setFontObject(prevLeft->fontObject);
fs->anchor(TOPLEFT, prevLeft, BOTTOMLEFT, 0.0, calculatedYOffset);
// ... mirror for TextRight ...
// ... append fs to textLeft[]/textRight[] at [numLinesAllocated],
// then ++numLinesAllocated.
}
// fall through to the existing "set text on line[currentLine]" path
```
3.3.5 helpers (offsets 3.3.5-specific — 1.12 equivalents need to be
re-derived):
- `FUN_00485240` — SimpleFontString constructor `(parent, layer, sublayer)`
- `FUN_0048b6c0` — set-name helper (registers under global `_G[name]`)
- `FUN_0048a260` — anchor helper `(this, anchorPoint, relativeTo, relativePoint, xoff, yoff)`
Struct offsets in 3.3.5 were `+0x2a4` (numLines) / `+0x2a8` (numAlloc) /
`+0x2b4` (textLeft) / `+0x2c0` (textRight) / `+0x2cc` (wrapFlag). All
shifted vs. 1.12 (see above), unsurprising given TBC/WotLK bloat.
### Implementation plan
**Approach A — pre-hook AddLine with a grow-if-needed prelude
(preferred)**:
1. Locate the 1.12 equivalents of the three 3.3.5 helpers. Trace them
from existing `CSimpleFontString`-creating sites in the engine —
the CreateFontString API dispatch is a good starting point (it's
Lua-visible and calls the same constructor).
2. Locate the array reallocators used by `FUN_00529650`
(`FUN_00536c80` for the two pointer arrays, `FUN_004368c0` for the
wrap-flag byte array).
3. MinHook `FUN_00530270`. Pre-hook body:
- If `numLines != numLinesAllocated`, call original — done.
- Otherwise: realloc the three arrays to `numLinesAllocated + 1`,
create two new `CSimpleFontString` (left + right), name them
`"%sTextLeft%d"` / `"%sTextRight%d"` with `tooltip->GetName()` and
the new index, anchor each below the prior-slot FontString on the
same side, use the prior FontString's `fontObject` (`+0xd8` in the
3.3.5 layout — confirm on 1.12) as the default, write the pointers
into `textLeft[numLinesAllocated]` / `textRight[numLinesAllocated]`,
zero-init `wrapFlag[numLinesAllocated]`, and bump
`numLinesAllocated`.
- Then tail-call the original — it now succeeds normally.
**Approach B — pre-declare a wider pool via Lua** (fallback if the
constructor helpers turn out to be too tangled):
1. In `!!!ClassicAPI` addon, at load time, call
`GameTooltip:CreateFontString("GameTooltipTextLeft" .. i, "ARTWORK",
"GameTooltipText")` for `i = 31..60`, plus `TextRightN`, plus set
anchors matching the pattern in the template.
2. Force the engine's pool-init to re-scan. If we can't reach the
vtable trigger cleanly, expose a small C entrypoint that walks the
pool the same way `FUN_00529650` does — takes `tooltip` as arg,
reallocates arrays, refills.
Approach B is less code but ties the cap to a fixed number (60 lines
is enough for pfUI's use case but arbitrary). Approach A grows on
demand like retail and has no ceiling.
### Testing
- Hover a random-suffix weapon with several equip-spell bonuses (Krol
Blade, Ironfoe, any ZG/AQ trinket) — tooltip should render >30 lines
without truncation.
- Hover an item that produces a 40+ line tooltip when pfUI's eqcompare
block is enabled (Priest Judgement Set chest against another
Judgement chest: item tooltip ~22 lines + compare block ~10 lines).
- Verify no crash / no visual glitches on:
- ShoppingTooltip1/2 (same class, same `GameTooltipTemplate`)
- Rapid re-hovers (create/destroy pressure on the added FontStrings)
- Item tooltips that stay under 30 lines (pre-existing behavior unchanged)
### Risks
- If the array reallocation logic in `FUN_00536c80` doesn't null-out
the tail on grow, `textLeft[N]`/`textRight[N]` might contain
garbage for a frame — anchor before write, or memset to zero.
- FontString lifetime: 3.3.5 leaks the extras for the tooltip
lifetime (they stay hidden when unused). GameTooltip is a
singleton, so leak is bounded. Fine.
- Interaction with `pfUI`'s existing tooltip skinning (frame borders,
status bar reposition, etc.): the layer we're modifying is beneath
the skinning; shouldn't touch anything visible unless anchor math
is wrong.
### Related
If we ever consider dropping the whole "GameTooltipTemplate declares
30 FontStrings in XML" scheme and instead having the engine start
with 0 and grow entirely on demand — that would be even cleaner, but
requires patching every consumer of the initial pool (there aren't
many, but they need audit). Filed for future if the incremental grow
turns out to be fragile.
### Investigation update (resolved unknowns)
Traced the pool machinery end-to-end. The open questions above are now
answered, and they point away from both the AddLine hook (Approach A)
and the "trigger the engine's scan" idea — a **hybrid** is cleaner than
either.
**The scan is virtual slot 9, run only at XML-template instantiation.**
`FUN_00529650` is slot 9 (`vtable+0x24`) of the CGameTooltip vtable
(base `0x00808f60`; the null at `0x00808f5c` is the boundary, and slot 2
`0x005368d0` is the `tooltip:method()` dispatcher — confirms the class).
The two vtable-base xrefs are the constructors `FUN_00529240` /
`FUN_00529480`. The scan's prologue calls
`FUN_0076a2f0(node) → FUN_0076a060(node)`, which reads the XML
`"inherits"` attribute and child `"Frames"` — i.e. the scan is the
frame's **XML OnLoad / region-setup** virtual. It runs once when
`GameTooltip` is built from `GameTooltipTemplate` (and again per
`/reload`), **not** on `SetOwner`/show. So we can't cheaply re-trigger
it — the `node` arg is a live XML-parse handle we don't have, and
calling it with a bogus node runs the inherits/Frames path on garbage.
**The scan finds lines by `_G[name]`, so Lua-created FontStrings count.**
`numLinesAllocated` (`+0x320`) is literally "how many contiguous
`%sTextLeftN` **and** `%sTextRightN` exist by name": pass 0 counts (stop
at the first missing index), pass 1 reallocs the three arrays to that
count and fills the pointers. The name lookup `FUN_0076c760` does
`lua_pushstring(name); gettable(_G); if type==TABLE: FrameScript_GetObject`
— a plain `_G[name]` resolve, so a FontString made with
`GameTooltip:CreateFontString("GameTooltipTextLeft31", …)` **is**
visible to it. (Names must be contiguous and exist in *both* columns.)
**The array reallocators are simple and callable from C.**
`FUN_00536c80` (pointer arrays) and `FUN_004368c0` (wrap-flag array) are
the same routine with different SMem tags:
`__thiscall(descriptor, newCount)` where the descriptor is
`{ count@+0x0, cap@+0x4, data@+0x8 }` — reallocs `data` to `newCount*4`
and copies the old contents. In the tooltip the three descriptors are:
| Descriptor `this` | count / cap / data | array |
|-------------------|--------------------|-------|
| `tooltip + 0x324` | `+0x324 / +0x328 / +0x32c` | `textLeft[]` (`CSimpleFontString*`) |
| `tooltip + 0x330` | `+0x330 / +0x334 / +0x338` | `textRight[]` |
| `tooltip + 0x33c` | `+0x33c / +0x340 / +0x344` | `wrapFlag[]` (int, 4-byte) |
`numLinesAllocated` (`+0x320`) is a separate field the scan sets equal
to the count; AddLine (`FUN_00530270`) gates on
`numLines(+0x31c) != numLinesAllocated(+0x320)`.
**Recommended approach — Lua creates FontStrings, C grows the arrays
(no hook, no XML re-run).** Replicate the scan's pass 1 directly:
1. **Lua (`!!!ClassicAPI`), at load / `PLAYER_LOGIN`:** for each frame on
`GameTooltipTemplate` (`GameTooltip`, `ShoppingTooltip1/2`,
`ItemRefTooltip`, …), create `<name>TextLeft31..N` and
`<name>TextRight31..N` via `frame:CreateFontString(nm, "ARTWORK",
"GameTooltipText")`, each anchored `TOPLEFT`/`TOPRIGHT` to the prior
line's `BOTTOMLEFT`/`BOTTOMRIGHT` (mirror the template's chain).
This avoids re-deriving the 1.12 FontString ctor / setName / anchor
helpers that Approach A needed — Lua already wraps them.
2. **C entrypoint** `GrowTooltipPool(tooltipObj, newCount)` (expose as a
Lua global, called once per frame right after step 1):
- `FUN_00536c80(tooltip+0x324, newCount)`; set `+0x328 = newCount`.
- `FUN_00536c80(tooltip+0x330, newCount)`; set `+0x334 = newCount`.
- `FUN_004368c0(tooltip+0x33c, newCount)`; set `+0x340 = newCount`.
- for `i` in `[oldAlloc, newCount)`: resolve `<name>TextLeftN`/
`RightN` via the `_G[name]` dance (same as `FUN_0076c760`), and if
either is missing stop at `i`; else write the two pointers into the
new `textLeft`/`textRight` data slots and zero `wrapFlag[i]`.
- set `numLinesAllocated (+0x320) = i`.
Resolve `tooltipObj` from the Lua frame the same way our other
tooltip methods do (`FrameScript_GetObject`).
Runs once per frame per session (re-runs on `/reload`, which recreates
the frames → scan resets to 30 → our addon re-creates + re-grows). No
per-line hook (dodges the MinHook-collision risk on a moderately hot
function), no XML-node fabrication.
Remaining risk to validate in-game: whether AddLine's downstream layout
/ `Show` sizing walks `numLines` FontStrings by their anchors (expected)
vs. any hidden fixed-30 assumption elsewhere; and that the Lua-created
lines inherit height/spacing identical to the template's 1..30 so the
tooltip grows seamlessly. Both are observable on the first >30-line
hover.
+1047 -24
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Common.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
namespace Common {
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Common.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
+818 -26
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `GetActionInfo(slot)` — modern-WoW-style action descriptor lookup.
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Embedded `!!!ClassicAPI` addon fallback. When the user doesn't have
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_AddOns.*` convenience wrappers. Modern WoW splits the legacy
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `CLASSIC_API_VERSION` global — a single integer addons can read for
+12 -5
View File
@@ -1,12 +1,12 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
// `C_UnitAuras.*` — modern aura-data namespace. Returns AuraData
// tables built by `Aura::Data::Push` (see `Data.h`). The fields the
@@ -92,11 +92,16 @@ int PushAuraByIndex(void *L, const char *unitToken, int index,
Data::Filter filter, bool playerOnly = false) {
const uint8_t *unit = ResolveUnit(unitToken);
const int slot = Data::FindNthSlot(unit, index, filter, playerOnly);
if (slot < 0) {
Game::Lua::PushNil(L);
if (slot >= 0) {
Data::Push(L, unit, slot);
return 1;
}
Data::Push(L, unit, slot);
// Descriptor exhausted — an aura the engine dropped from the slot array
// (rogue stealth, party range fluctuation) may still be live in the
// Aura::Source cache. Surface it after the descriptor entries.
if (Data::PushNthCacheFallback(L, unit, index, filter, playerOnly))
return 1;
Game::Lua::PushNil(L);
return 1;
}
@@ -218,6 +223,8 @@ void AppendRangeToArray(void *L, const uint8_t *unit, int outerIdx,
Data::Push(L, unit, slot);
Game::Lua::SetTable(L, outerIdx);
}
// Append auras the descriptor dropped but Aura::Source still has live.
Data::AppendCacheFallbacks(L, unit, filter, playerOnly, outerIdx, nextKey);
}
int __fastcall Script_GetUnitAuras(void *L) {
+259
View File
@@ -0,0 +1,259 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// See ComboDuration.h for the contract and the protocol findings that
// motivate the design (the server never transmits target-debuff
// durations in 1.12).
#include "ComboDuration.h"
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include "net/PacketReader.h"
#include "spell/Mod.h"
#include "unit/Identity.h"
#include <cstdint>
namespace Aura::ComboDuration {
namespace {
using Net::CDataStore;
uint32_t NowMs() {
using TickCount_t = uint32_t(__fastcall *)();
return reinterpret_cast<TickCount_t>(
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
}
// ---- Combo-point snapshot (NetClient send co-hook) ------------------------
// The player's live combo points, read the same way Script_GetComboPoints
// (0x0051A190) does: the CP byte on the CGPlayer +0xE68 sub-struct. The player
// comes from the shared Unit::Identity::PlayerObject() (the object-manager GUID
// resolve, our fastest player-object path — returns null pre-world rather than
// throwing), so this stays safe from any context the send hook can fire in.
uint8_t CurrentComboPoints() {
const uint8_t *player = Unit::Identity::PlayerObject();
if (player == nullptr)
return 0;
const uint8_t *info = *reinterpret_cast<const uint8_t *const *>(
player + Offsets::OFF_CGPLAYER_INFO);
if (info == nullptr)
return 0;
return *(info + Offsets::OFF_CGPLAYER_COMBO_POINTS);
}
// One cast in flight at a time is the practical case (a second finisher
// can't be cast until the server consumes the first one's combo points),
// so a single slot with a spellID match and a short TTL suffices.
struct Capture {
uint32_t spellId;
uint8_t comboPoints;
uint32_t capturedMs;
};
Capture g_capture{0, 0, 0};
// Worst-case send -> SMSG_SPELL_GO roundtrip. Past this the snapshot is
// stale (the cast failed / was rejected) and must not scale a later cast.
constexpr uint32_t kCaptureTtlMs = 2000;
int ConsumeCapturedCP(uint32_t spellId) {
const Capture c = g_capture;
g_capture = {0, 0, 0};
if (c.spellId != spellId || c.comboPoints == 0)
return 0;
if (NowMs() - c.capturedMs >= kCaptureTtlMs)
return 0;
return c.comboPoints;
}
// Co-hook on the NetClient send — the one funnel every outgoing CMSG
// passes through, including casts released from nampower's spell queue
// (which bypass any co-hook on Spell_C_CastSpell itself via trampoline
// re-entry, but still build and send CMSG_CAST_SPELL through the engine).
// Snapshot the combo points at the moment the cast request leaves: the
// server consumes them before SMSG_SPELL_GO arrives, so reading at
// SpellGo time is too late.
using NetSend_t = void(__fastcall *)(void *conn, void *edx,
CDataStore *packet);
NetSend_t g_origNetSend = nullptr;
void __fastcall NetSend_h(void *conn, void *edx, CDataStore *packet) {
if (packet != nullptr) {
const uint32_t saved = packet->m_read;
const uint32_t opcode = Net::Read<uint32_t>(packet);
if (opcode == Offsets::OP_CMSG_CAST_SPELL) {
const uint32_t spellId = Net::Read<uint32_t>(packet);
if (spellId != 0)
g_capture = {spellId, CurrentComboPoints(), NowMs()};
}
packet->m_read = saved;
}
g_origNetSend(conn, edx, packet);
}
const Game::HookAutoRegister _sendHook{
Offsets::FUN_NET_SEND, reinterpret_cast<void *>(&NetSend_h),
reinterpret_cast<void **>(&g_origNetSend)};
// ---- Duration data (SpellDuration.dbc + Lua overrides) --------------------
// Lua-registered overrides for spells whose client DBC row is missing or
// wrong. Turtle's reworked Rip is the motivating case: its DurationIndex
// dangles (records[87] == NULL in Turtle's SpellDuration.dbc) because the
// rework lives entirely server-side, so the values must come from config.
struct Override {
uint32_t spellId;
int32_t baseMs;
int32_t maxMs;
};
constexpr int kMaxOverrides = 64;
Override g_overrides[kMaxOverrides];
int g_overrideCount = 0;
const Override *FindOverride(uint32_t spellId) {
for (int i = 0; i < g_overrideCount; ++i) {
if (g_overrides[i].spellId == spellId)
return &g_overrides[i];
}
return nullptr;
}
// Known values for spells whose duration row DANGLES — a non-zero
// DurationIndex pointing at a SpellDuration row the client doesn't have.
// Consulted ONLY when that exact condition holds, so the gate is the bug
// itself, not a server fingerprint: on a stock client every one of these
// resolves to a valid row and this table is never reached, and if the
// broken client data is ever fixed the DBC row takes over automatically.
//
// Turtle's reworked Rip is the known case: all six ranks point at
// SpellDuration row 87, which Turtle added on the SERVER only
// ({8000, 0, 18000} — live server data shows 10/12/14/16/18s at 1..5 CP,
// i.e. 8s + 2s per combo point); their client patch never shipped the
// row (the client table jumps 86 -> 105). Verified the only dangling
// duration index in the entire client Spell.dbc.
constexpr Override kDanglingRowKnown[] = {
{1079, 8000, 18000}, {9492, 8000, 18000}, {9493, 8000, 18000},
{9752, 8000, 18000}, {9894, 8000, 18000}, {9896, 8000, 18000},
};
const Override *FindDanglingRowKnown(uint32_t spellId) {
for (const auto &k : kDanglingRowKnown) {
if (k.spellId == spellId)
return &k;
}
return nullptr;
}
// `C_UnitAuras.RegisterComboDuration(spellID, baseSeconds, maxSeconds)`
// -> true on success. Re-registering a spellID replaces its entry.
// Values are in seconds to match the C_UnitAuras duration convention;
// the formula applied is the server's own:
// `base + (max - base) * comboPoints / 5`.
int __fastcall Script_RegisterComboDuration(void *L) {
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2) ||
!Game::Lua::IsNumber(L, 3)) {
Game::Lua::Error(
L, "Usage: C_UnitAuras.RegisterComboDuration(spellID, "
"baseSeconds, maxSeconds)");
return 0;
}
const auto spellId = static_cast<uint32_t>(Game::Lua::ToNumber(L, 1));
const auto baseMs = static_cast<int32_t>(Game::Lua::ToNumber(L, 2) * 1000.0);
const auto maxMs = static_cast<int32_t>(Game::Lua::ToNumber(L, 3) * 1000.0);
if (spellId == 0 || baseMs < 0 || maxMs <= 0) {
Game::Lua::PushBool(L, false);
return 1;
}
for (int i = 0; i < g_overrideCount; ++i) {
if (g_overrides[i].spellId == spellId) {
g_overrides[i] = {spellId, baseMs, maxMs};
Game::Lua::PushBool(L, true);
return 1;
}
}
if (g_overrideCount >= kMaxOverrides) {
Game::Lua::PushBool(L, false);
return 1;
}
g_overrides[g_overrideCount++] = {spellId, baseMs, maxMs};
Game::Lua::PushBool(L, true);
return 1;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_UnitAuras", "RegisterComboDuration",
&Script_RegisterComboDuration);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
uint32_t TryComboScaledMs(const uint8_t *spellRecord, uint32_t spellId) {
if (spellRecord == nullptr || spellId == 0)
return 0;
const int cp = ConsumeCapturedCP(spellId);
if (cp <= 0 || cp > 5)
return 0;
int32_t baseMs = 0;
int32_t maxMs = 0;
if (const Override *o = FindOverride(spellId)) {
baseMs = o->baseMs;
maxMs = o->maxMs;
} else {
const int idx = *reinterpret_cast<const int *>(
spellRecord + Offsets::OFF_SPELL_RECORD_DURATION_INDEX);
const uint8_t *row =
DBC::Record(Offsets::VAR_SPELL_DURATION_RECORDS,
Offsets::VAR_SPELL_DURATION_COUNT,
static_cast<uint32_t>(idx));
if (row != nullptr) {
baseMs = *reinterpret_cast<const int32_t *>(row + 0x4);
maxMs = *reinterpret_cast<const int32_t *>(row + 0xC);
} else if (idx != 0) {
// Dangling duration index — the client data is broken for this
// spell. Fall back to the built-in known values (Turtle Rip).
const Override *k = FindDanglingRowKnown(spellId);
if (k == nullptr)
return 0;
baseMs = k->baseMs;
maxMs = k->maxMs;
} else {
return 0; // no duration row at all
}
}
// The server scales only when the row has a combo range: base != max
// ((v)mangos CalculateSpellDuration). base < 0 is the "infinite"
// sentinel; fixed-duration rows (base == max) take the regular path.
if (baseMs < 0 || maxMs <= 0 || baseMs == maxMs)
return 0;
int32_t durationMs = baseMs + (maxMs - baseMs) * cp / 5;
if (durationMs <= 0)
return 0;
// Player duration SpellMods apply on top of the combo-scaled value,
// matching the server's order (scale, then ApplySpellMod).
durationMs = static_cast<int32_t>(Spell::Mod::Apply(
spellRecord, Offsets::SPELLMOD_OP_DURATION,
static_cast<float>(durationMs)));
return durationMs > 0 ? static_cast<uint32_t>(durationMs) : 0;
}
} // namespace Aura::ComboDuration
+50
View File
@@ -0,0 +1,50 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
// `Aura::ComboDuration` — combo-point-scaled durations for finisher
// auras (Rupture, Kidney Shot, Slice and Dice, Rip, …).
//
// The 1.12 server computes a finisher's real duration at cast time as
// `base + (max - base) * comboPoints / 5` from the spell's
// SpellDuration.dbc row and never transmits the result for auras the
// player puts on OTHER units (verified with a whole-protocol sniff:
// SMSG_UPDATE_AURA_DURATION is self-scoped, and nothing else carries
// it). So this module mirrors the computation client-side:
//
// - a co-hook on the NetClient send (`FUN_NET_SEND`) snapshots the
// player's combo points the instant CMSG_CAST_SPELL leaves — the
// one moment the count is still intact (the server clears it
// before SMSG_SPELL_GO comes back);
// - `TryComboScaledMs` consumes that snapshot when `Aura::Source`'s
// SpellGo hook stores the aura, computing the scaled duration from
// SpellDuration.dbc and applying the player's duration SpellMods on
// top, matching the server's order of operations. Spells whose
// duration index DANGLES (client data bug — Turtle's reworked Rip
// is the known case) fall back to built-in values gated on that
// exact condition; a Lua-registered override
// (`C_UnitAuras.RegisterComboDuration`) beats both, as an escape
// hatch for other servers whose values differ from the client DBC.
namespace Aura::ComboDuration {
// Combo-scaled duration (ms) for a player-cast spell, or 0 when it
// doesn't apply: no combo points captured for this cast, the spell's
// duration row isn't combo-scaled (base == max) and has no override,
// or the row is missing. A 0 return means "use the regular base
// duration path".
uint32_t TryComboScaledMs(const uint8_t *spellRecord, uint32_t spellId);
} // namespace Aura::ComboDuration
+218 -85
View File
@@ -1,12 +1,12 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
// Aura-table primitives. See `Data.h` for the contract; see `Api.cpp`
// for the Lua-binding surface that consumes these.
@@ -350,10 +350,13 @@ const char *DispelName(uint32_t dispelTypeID) {
return (name == nullptr || *name == '\0') ? nullptr : name;
}
void Push(void *L, const uint8_t *unit, int slot) {
const uint32_t spellID = ReadSpellID(unit, slot);
// Writes the full modern AuraData table on top of the Lua stack from
// already-resolved values. Shared by the descriptor path (`Push`) and the
// cache-fallback path (`PushFromCache`). Net stack effect: +1 (the table).
static void BuildTable(void *L, uint32_t spellID, int applications,
bool isHelpful, double duration, double expirationTime,
uint64_t casterGuid) {
const uint8_t *spellRecord = SpellRecord(spellID);
const bool isHelpful = slot < Offsets::UNIT_AURA_BUFF_COUNT;
uint32_t dispelTypeID = 0;
if (spellRecord != nullptr) {
@@ -370,87 +373,32 @@ void Push(void *L, const uint8_t *unit, int slot) {
Game::Lua::SetFieldString(L, "name", name);
Game::Lua::SetFieldString(L, "icon", icon);
Game::Lua::SetFieldNumber(L, "applications",
static_cast<double>(ReadStacks(unit, slot)));
static_cast<double>(applications));
Game::Lua::SetFieldNumber(L, "spellId", static_cast<double>(spellID));
Game::Lua::SetFieldString(L, "dispelName", dispel);
Game::Lua::SetFieldBool(L, "isHelpful", isHelpful);
Game::Lua::SetFieldBool(L, "isHarmful", !isHelpful);
// Timing fields. Vanilla doesn't broadcast cast time / duration
// for ANY unit's auras except the local player's — for everyone
// else (target, party, raid, mouseover) `expirationTime` stays 0
// and addons have to track expiry themselves via aura-add events.
// For the player we read the engine's `VAR_PLAYER_BUFF_TABLE`
// (same data `GetPlayerBuffTimeLeft` returns), which gives us a
// real expiration timestamp.
//
// `duration` comes from Spell.dbc → SpellDuration.dbc with
// level scaling. That's the *base* value; talent / glyph
// duration-extension modifiers (Improved PW:F, etc.) are
// already baked into the expiration timestamp on the
// caster's side, so `expirationTime - GetTime()` reflects the
// true remaining time even when `duration` doesn't include
// the talent boost. We populate `duration` for any unit (the
// base value is identical regardless of who's affected); only
// `expirationTime` is player-gated.
double duration = 0.0;
double expirationTime = 0.0;
const uint8_t *player = LocalPlayer();
if (spellID != 0) {
const int unitLevel = (unit != nullptr) ? PlayerLevel(unit) : 0;
duration = SpellBaseDurationSeconds(spellID, unitLevel);
}
if (unit != nullptr && unit == player) {
const uint8_t *entry = FindPlayerBuffEntry(spellID);
if (entry != nullptr)
expirationTime = PlayerBuffExpirationSeconds(entry);
}
// Caster + non-player expiration from the SMSG_SPELL_GO cache
// (`Aura::Source`), captured at cast time. The vanilla descriptor has
// neither. `sourceUnit` applies to any unit; `expirationTime` only
// fills in here when the player-buff table above didn't — that table is
// authoritative for the player's own auras, the cache covers everyone
// else. Both are best-effort: a miss (aura predates login, or the cast
// wasn't observed) leaves the modern-truthful defaults untouched.
const char *sourceUnit = nullptr;
const char *sourceGUID = nullptr;
char tokenBuf[32];
char guidBuf[Guid::STRING_SIZE];
if (spellID != 0) {
uint64_t casterGuid = 0;
uint32_t expMs = 0;
uint32_t durMs = 0;
if (Aura::Source::Get(UnitGuid(unit), spellID, &casterGuid, &expMs,
&durMs)) {
if (expirationTime == 0.0 && expMs != 0)
expirationTime = static_cast<double>(expMs) * 0.001;
// Prefer the applied (caster-modified) duration so `duration`
// stays consistent with `expirationTime` — otherwise a talented
// DoT (e.g. Improved Shadow Word: Pain) shows remaining time
// exceeding the base `duration`.
if (durMs != 0)
duration = static_cast<double>(durMs) * 0.001;
if (casterGuid != 0) {
// `sourceUnit` is a token (nil when the caster maps to no
// current token); `sourceGUID` is the raw "0x..." GUID and
// is set whenever we have a caster — a ClassicAPI extension
// (not in retail AuraData) that survives the caster leaving
// token range and doubles as a unit token under SuperWoW.
sourceUnit = Unit::Identity::TokenFromGUID(
casterGuid, tokenBuf, sizeof tokenBuf);
sourceGUID = Guid::FormatAsString(casterGuid, guidBuf,
sizeof guidBuf);
}
}
}
Game::Lua::SetFieldNumber(L, "duration", duration);
Game::Lua::SetFieldNumber(L, "expirationTime", expirationTime);
if (sourceUnit != nullptr)
Game::Lua::SetFieldString(L, "sourceUnit", sourceUnit);
if (sourceGUID != nullptr)
Game::Lua::SetFieldString(L, "sourceGUID", sourceGUID);
if (casterGuid != 0) {
// `sourceUnit` is a token (nil when the caster maps to no current
// token); `sourceGUID` is the raw "0x..." GUID and is set whenever we
// have a caster — a ClassicAPI extension (not in retail AuraData) that
// survives the caster leaving token range and doubles as a unit token
// under SuperWoW.
char tokenBuf[32];
char guidBuf[Guid::STRING_SIZE];
const char *sourceUnit = Unit::Identity::TokenFromGUID(
casterGuid, tokenBuf, sizeof tokenBuf);
const char *sourceGUID =
Guid::FormatAsString(casterGuid, guidBuf, sizeof guidBuf);
if (sourceUnit != nullptr)
Game::Lua::SetFieldString(L, "sourceUnit", sourceUnit);
if (sourceGUID != nullptr)
Game::Lua::SetFieldString(L, "sourceGUID", sourceGUID);
}
Game::Lua::SetFieldNumber(L, "charges", 0);
Game::Lua::SetFieldNumber(L, "maxCharges", 0);
Game::Lua::SetFieldNumber(L, "timeMod", 1);
@@ -466,11 +414,196 @@ void Push(void *L, const uint8_t *unit, int slot) {
Game::Lua::SetFieldBool(L, "shouldConsolidate", false);
Game::Lua::SetFieldBool(L, "isRaid", false);
// `sourceUnit` / `sourceGUID` are set above when the `Aura::Source`
// cache has a caster for this aura; left unset (→ nil) on a miss.
// `auraInstanceID` and `points` are deliberately omitted — modern
// returns nil for those when they don't apply, and Lua reading a
// missing key yields nil, so the table doesn't need an explicit entry.
// `auraInstanceID` and `points` are deliberately omitted — modern returns
// nil for those when they don't apply, and Lua reading a missing key
// yields nil, so the table doesn't need an explicit entry.
}
void Push(void *L, const uint8_t *unit, int slot) {
const uint32_t spellID = ReadSpellID(unit, slot);
const bool isHelpful = slot < Offsets::UNIT_AURA_BUFF_COUNT;
// Timing fields. Vanilla doesn't broadcast cast time / duration for ANY
// unit's auras except the local player's — for everyone else (target,
// party, raid, mouseover) `expirationTime` stays 0 unless the
// `Aura::Source` cache observed the cast. For the player we read the
// engine's `VAR_PLAYER_BUFF_TABLE` (same data `GetPlayerBuffTimeLeft`
// returns), which gives a real expiration timestamp.
//
// `duration` comes from Spell.dbc → SpellDuration.dbc with level scaling
// (the *base* value); talent / glyph duration-extension modifiers are
// already baked into the expiration timestamp on the caster's side, so
// `expirationTime - GetTime()` reflects true remaining time even when the
// base `duration` doesn't include the talent boost.
double duration = 0.0;
double expirationTime = 0.0;
uint64_t casterGuid = 0;
const uint8_t *player = LocalPlayer();
if (spellID != 0) {
const int unitLevel = (unit != nullptr) ? PlayerLevel(unit) : 0;
duration = SpellBaseDurationSeconds(spellID, unitLevel);
}
if (unit != nullptr && unit == player) {
const uint8_t *entry = FindPlayerBuffEntry(spellID);
if (entry != nullptr)
expirationTime = PlayerBuffExpirationSeconds(entry);
}
// Caster + non-player expiration from the SMSG_SPELL_GO cache, captured at
// cast time (the descriptor has neither). The player-buff table above is
// authoritative for the player's own expiration; the cache fills it for
// everyone else. Prefer the applied (caster-modified) duration so it stays
// consistent with `expirationTime` — otherwise a talented DoT (Improved
// Shadow Word: Pain) would show remaining time exceeding the base.
if (spellID != 0) {
uint64_t c = 0;
uint32_t expMs = 0;
uint32_t durMs = 0;
if (Aura::Source::Get(UnitGuid(unit), spellID, &c, &expMs, &durMs)) {
if (expirationTime == 0.0 && expMs != 0)
expirationTime = static_cast<double>(expMs) * 0.001;
if (durMs != 0)
duration = static_cast<double>(durMs) * 0.001;
casterGuid = c;
}
}
BuildTable(L, spellID, ReadStacks(unit, slot), isHelpful, duration,
expirationTime, casterGuid);
}
namespace {
// Builds an AuraData table from an `Aura::Source` cache entry — the fallback
// when the descriptor has dropped the aura's slot. Stack count isn't broadcast
// in SMSG_SPELL_GO, so `applications` defaults to 1. `duration` uses the
// applied (caster-modified) ms when known, else the Spell.dbc base.
void PushFromCache(void *L, const uint8_t *unit,
const Aura::Source::CachedAura &c, bool isHelpful) {
double duration = 0.0;
if (c.durationMs != 0) {
duration = static_cast<double>(c.durationMs) * 0.001;
} else {
const int unitLevel = (unit != nullptr) ? PlayerLevel(unit) : 0;
duration = SpellBaseDurationSeconds(c.spellId, unitLevel);
}
const double expirationTime =
(c.expirationMs != 0) ? static_cast<double>(c.expirationMs) * 0.001 : 0.0;
BuildTable(L, c.spellId, 1, isHelpful, duration, expirationTime,
c.casterGuid);
}
// True if `unit`'s descriptor currently exposes any visible aura in either
// range. The cache fallback is only trustworthy when the descriptor has been
// wholesale-cleared: the two states it exists for — rogue stealth and party
// range fluctuation — clear the *entire* aura array at once. If the unit shows
// even one live aura, the descriptor is authoritative, so a cache entry the
// descriptor lacks is a genuinely-gone aura we never saw removed (e.g. a
// no-duration debuff — Ghost/Spirit — on a unit that died and resurrected out
// of our visibility, so no OnAuraRemoved fired and its infinite entry never
// expired). Surfacing it then produces a stuck phantom.
bool DescriptorHasVisibleAura(const uint8_t *unit) {
if (unit == nullptr)
return false;
for (int slot = 0; slot < Offsets::UNIT_AURA_TOTAL; ++slot) {
if (IsSlotPopulated(unit, slot))
return true;
}
return false;
}
// Counts populated descriptor slots matching `filter` (and `playerOnly`).
int CountSlots(const uint8_t *unit, Filter filter, bool playerOnly) {
if (unit == nullptr)
return 0;
const int start = (filter == Filter::Harmful)
? Offsets::UNIT_AURA_BUFF_COUNT
: 0;
const int end = (filter == Filter::Harmful) ? Offsets::UNIT_AURA_TOTAL
: Offsets::UNIT_AURA_BUFF_COUNT;
int n = 0;
for (int slot = start; slot < end; ++slot) {
if (!IsSlotPopulated(unit, slot))
continue;
if (playerOnly && !IsPlayerCast(unit, slot))
continue;
++n;
}
return n;
}
constexpr int kFallbackMax = Offsets::UNIT_AURA_TOTAL;
// Whether cache entry `c` should be surfaced as a fallback for `unit` under
// `filter`/`playerOnly`: the spell must be a user-visible aura, it must not
// already be in a populated descriptor slot (no double-listing the same live
// aura) and, when player-filtered, must have been cast by us. Superseded /
// dispelled auras are kept out of the cache by `Aura::Source`'s removal
// eviction, not filtered here.
bool FallbackEligible(const uint8_t *unit, const Aura::Source::CachedAura &c,
Filter filter, bool playerOnly) {
if (!IsVisible(SpellRecord(c.spellId)))
return false; // hidden/internal aura (e.g. SPELL_ATTR_HIDDEN_CLIENTSIDE):
// the descriptor path drops these via IsSlotPopulated, so
// the cache path must apply the same engine predicate or
// they leak through as tooltip-less phantom buffs.
if (FindSlotBySpellID(unit, c.spellId, &filter, false) >= 0)
return false; // still in the descriptor — returned via the slot path
if (playerOnly && c.casterGuid != Unit::Identity::PlayerGuid())
return false;
return true;
}
} // namespace
bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
Filter filter, bool playerOnly) {
if (unit == nullptr)
return false;
if (DescriptorHasVisibleAura(unit))
return false; // descriptor authoritative — see DescriptorHasVisibleAura
// The descriptor held `descCount` matches; the caller already found fewer
// than `oneBasedIndex` there, so the cache supplies index
// `oneBasedIndex - descCount` (1-based) of the entries it didn't.
const int target = oneBasedIndex - CountSlots(unit, filter, playerOnly);
if (target < 1)
return false;
const uint64_t guid = UnitGuid(unit);
if (guid == 0)
return false;
Aura::Source::CachedAura buf[kFallbackMax];
const int n = Aura::Source::Enumerate(
guid, filter == Filter::Harmful, buf, kFallbackMax);
int seen = 0;
for (int i = 0; i < n; ++i) {
if (!FallbackEligible(unit, buf[i], filter, playerOnly))
continue;
if (++seen == target) {
PushFromCache(L, unit, buf[i], filter == Filter::Helpful);
return true;
}
}
return false;
}
void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
bool playerOnly, int outerIdx, int &nextKey) {
if (unit == nullptr)
return;
if (DescriptorHasVisibleAura(unit))
return; // descriptor authoritative — see DescriptorHasVisibleAura
const uint64_t guid = UnitGuid(unit);
if (guid == 0)
return;
Aura::Source::CachedAura buf[kFallbackMax];
const int n = Aura::Source::Enumerate(
guid, filter == Filter::Harmful, buf, kFallbackMax);
for (int i = 0; i < n; ++i) {
if (!FallbackEligible(unit, buf[i], filter, playerOnly))
continue;
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
PushFromCache(L, unit, buf[i], filter == Filter::Helpful);
Game::Lua::SetTable(L, outerIdx);
}
}
} // namespace Aura::Data
+22 -2
View File
@@ -1,12 +1,12 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
#pragma once
@@ -112,4 +112,24 @@ const char *DispelName(uint32_t dispelTypeID);
// Net stack effect: +1 (the table).
void Push(void *L, const uint8_t *unit, int slot);
// Fallback for the index path when the descriptor has dropped an aura's slot
// (rogue stealth, party range fluctuation) but it's still active server-side.
// `oneBasedIndex` is the same index the caller passed to `FindNthSlot`; this
// resolves it against the `Aura::Source` cache *after* the descriptor matches
// (so cache entries append after descriptor ones, preserving existing index
// order). On a hit it pushes the AuraData table and returns true; on a miss it
// pushes nothing and returns false (caller pushes nil). Only entries whose
// helpful/harmful classification matches `filter` and that aren't already in a
// populated descriptor slot are considered.
bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
Filter filter, bool playerOnly = false);
// Appends every eligible `Aura::Source` cache fallback for `unit` matching
// `filter` into the array table at `outerIdx`, continuing from `nextKey`
// (updated in place). The bulk-enumeration analog of `PushNthCacheFallback`,
// for `GetUnitAuras`. Skips entries already present in a populated descriptor
// slot so it never double-lists.
void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
bool playerOnly, int outerIdx, int &nextKey);
} // namespace Aura::Data
+102 -13
View File
@@ -1,12 +1,12 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
// See `Source.h` for the contract. This is the backend: a co-hook on
// `FUN_SPELL_GO` that mirrors nampower's `SpellGoHook` parse to capture the
@@ -15,6 +15,7 @@
#include "Source.h"
#include "ComboDuration.h"
#include "Data.h"
#include "Game.h"
#include "Offsets.h"
@@ -69,12 +70,19 @@ uint32_t SpellDurationMs(const uint8_t *spellRecord, bool casterIsPlayer) {
// ---- Cache ---------------------------------------------------------------
// Helpful/harmful classification, recorded from the aura's descriptor slot
// when it's applied (the only place the buff/debuff split is known). SpellGo
// gives no slot, so casts we only saw there stay Unknown until/unless an
// application hook fires for the same aura.
enum Kind : int8_t { KIND_UNKNOWN = -1, KIND_HELPFUL = 0, KIND_HARMFUL = 1 };
struct Entry {
uint64_t targetGuid;
uint64_t casterGuid;
uint32_t spellId;
uint32_t expirationMs; // 0 = infinite / unknown duration
uint32_t durationMs; // applied duration (incl. caster mods); 0 = none
int8_t kind; // Kind; descriptor-slot-derived, KIND_UNKNOWN if only seen via SpellGo
bool used;
};
@@ -88,13 +96,19 @@ int g_writeCursor = 0;
// would replace talented timing with the unmodified base), so it skips
// entries that already carry a caster.
void Store(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
uint32_t expirationMs, uint32_t durationMs, bool fromCast) {
uint32_t expirationMs, uint32_t durationMs, bool fromCast,
int8_t kind) {
if (targetGuid == 0 || spellId == 0)
return;
// Update an existing entry for this exact aura instance.
for (auto &e : g_cache) {
if (e.used && e.targetGuid == targetGuid && e.spellId == spellId) {
// Learn the classification whenever a slot-derived kind arrives —
// independent of caster/timing ownership, and never downgrade a
// known kind back to unknown.
if (kind != KIND_UNKNOWN)
e.kind = kind;
if (!fromCast && e.casterGuid != 0)
return; // SpellGo owns this entry; keep its caster + timing
if (casterGuid != 0)
@@ -108,13 +122,32 @@ void Store(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
const uint32_t now = NowMs();
for (auto &e : g_cache) {
if (!e.used || (e.expirationMs != 0 && now >= e.expirationMs)) {
e = {targetGuid, casterGuid, spellId, expirationMs, durationMs, true};
e = {targetGuid, casterGuid, spellId, expirationMs, durationMs,
kind, true};
return;
}
}
Entry &slot = g_cache[g_writeCursor];
g_writeCursor = (g_writeCursor + 1) % kCacheSize;
slot = {targetGuid, casterGuid, spellId, expirationMs, durationMs, true};
slot = {targetGuid, casterGuid, spellId, expirationMs, durationMs, kind,
true};
}
// Evict the entry for an aura the engine reports gone. Keyed by (target,
// spell) like the rest of the cache. Without this, the GetAuraDataByIndex
// fallback would keep surfacing a dropped aura until its computed expiry —
// e.g. a Rank 1 buff replaced by Rank 2 (engine drops Rank 1 from the
// descriptor) would show as a phantom second aura, or a dispelled buff would
// linger.
void Evict(uint64_t targetGuid, uint32_t spellId) {
if (targetGuid == 0 || spellId == 0)
return;
for (auto &e : g_cache) {
if (e.used && e.targetGuid == targetGuid && e.spellId == spellId) {
e.used = false;
return;
}
}
}
// Drop entries whose timed aura has elapsed so the table doesn't fill with
@@ -171,16 +204,26 @@ void __fastcall SpellGo_h(uint64_t *itemGUID, uint64_t *casterGUID,
return;
const bool casterIsPlayer = (caster == Unit::Identity::PlayerGuid());
const uint32_t durationMs = SpellDurationMs(rec, casterIsPlayer);
// Combo-point finishers (Rupture, Kidney Shot, …) have their real
// duration computed at cast time from the combo points spent — the
// base-duration helper can't know it. ComboDuration mirrors the
// server's computation from the CP snapshot its send hook captured;
// 0 means "not combo-scaled", fall through to the base path.
uint32_t durationMs =
casterIsPlayer ? ComboDuration::TryComboScaledMs(rec, spellId) : 0;
if (durationMs == 0)
durationMs = SpellDurationMs(rec, casterIsPlayer);
const uint32_t expirationMs = durationMs > 0 ? NowMs() + durationMs : 0;
if (numTargets == 0) {
// No explicit hit list (self-cast with caster-implicit target).
Store(caster, spellId, caster, expirationMs, durationMs, true);
Store(caster, spellId, caster, expirationMs, durationMs, true,
KIND_UNKNOWN);
return;
}
for (int i = 0; i < numTargets; ++i)
Store(targets[i], spellId, caster, expirationMs, durationMs, true);
Store(targets[i], spellId, caster, expirationMs, durationMs, true,
KIND_UNKNOWN);
}
const Game::HookAutoRegister _hook{Offsets::FUN_SPELL_GO,
@@ -194,7 +237,7 @@ const Game::HookAutoRegister _hook{Offsets::FUN_SPELL_GO,
// paths, so it stamps timing only with `fromCast=false` — Store skips any
// entry SpellGo already owns, so a directly-cast aura keeps its talented
// timing. Base (unmodified) duration is the best estimate without a caster.
void StampApplication(void *unit, uint32_t spellId) {
void StampApplication(void *unit, uint32_t spellId, int8_t kind) {
if (spellId == 0)
return;
const uint8_t *rec = Spell::Lookup::RecordForID(static_cast<int>(spellId));
@@ -206,7 +249,13 @@ void StampApplication(void *unit, uint32_t spellId) {
const uint32_t durationMs = SpellDurationMs(rec, /*casterIsPlayer*/ false);
const uint32_t expirationMs = durationMs > 0 ? NowMs() + durationMs : 0;
Store(unitGuid, spellId, /*casterGuid*/ 0, expirationMs, durationMs,
/*fromCast*/ false);
/*fromCast*/ false, kind);
}
// Classify by the absolute aura slot: 0..BUFF_COUNT-1 = buff (helpful),
// BUFF_COUNT..TOTAL-1 = debuff (harmful).
int8_t KindForSlot(int slot) {
return slot >= Offsets::UNIT_AURA_BUFF_COUNT ? KIND_HARMFUL : KIND_HELPFUL;
}
// OnAuraAdded — a new aura occupies a slot (gives the spellId directly).
@@ -217,7 +266,7 @@ OnAuraAdded_t g_origOnAuraAdded = nullptr;
void __fastcall OnAuraAdded_h(void *unit, void *edx, uint32_t slot,
uint32_t spellId) {
g_origOnAuraAdded(unit, edx, slot, spellId);
StampApplication(unit, spellId);
StampApplication(unit, spellId, KindForSlot(static_cast<int>(slot)));
}
const Game::HookAutoRegister _hookAuraAdded{
@@ -234,8 +283,10 @@ OnAuraStacksChanged_t g_origOnAuraStacksChanged = nullptr;
void __fastcall OnAuraStacksChanged_h(void *unit, void *edx, int slot,
uint8_t stackCount) {
g_origOnAuraStacksChanged(unit, edx, slot, stackCount);
StampApplication(unit, Aura::Data::ReadSpellID(
static_cast<const uint8_t *>(unit), slot));
StampApplication(
unit,
Aura::Data::ReadSpellID(static_cast<const uint8_t *>(unit), slot),
KindForSlot(slot));
}
const Game::HookAutoRegister _hookAuraStacks{
@@ -243,6 +294,26 @@ const Game::HookAutoRegister _hookAuraStacks{
reinterpret_cast<void *>(&OnAuraStacksChanged_h),
reinterpret_cast<void **>(&g_origOnAuraStacksChanged)};
// OnAuraRemoved — a descriptor aura slot went empty: the aura fell off, was
// dispelled, or was replaced by a higher rank (the diff dispatcher fires
// Removed(old) + Added(new)). Evict the cache entry so the fallback stops
// surfacing it. Same ABI as OnAuraAdded (unit in ecx, slot + spellId on the
// stack); `slot` is unused here — we evict by spellId.
using OnAuraRemoved_t = void(__fastcall *)(void *unit, void *edx, uint32_t slot,
uint32_t spellId);
OnAuraRemoved_t g_origOnAuraRemoved = nullptr;
void __fastcall OnAuraRemoved_h(void *unit, void *edx, uint32_t slot,
uint32_t spellId) {
g_origOnAuraRemoved(unit, edx, slot, spellId);
(void)slot;
Evict(Unit::Identity::GuidForObject(unit), spellId);
}
const Game::HookAutoRegister _hookAuraRemoved{
Offsets::FUN_ON_AURA_REMOVED, reinterpret_cast<void *>(&OnAuraRemoved_h),
reinterpret_cast<void **>(&g_origOnAuraRemoved)};
} // namespace
bool Get(uint64_t unitGuid, uint32_t spellId, uint64_t *outCaster,
@@ -260,4 +331,22 @@ bool Get(uint64_t unitGuid, uint32_t spellId, uint64_t *outCaster,
return false;
}
int Enumerate(uint64_t unitGuid, bool harmful, CachedAura *out, int maxOut) {
if (unitGuid == 0 || out == nullptr || maxOut <= 0)
return 0;
const int8_t want = harmful ? KIND_HARMFUL : KIND_HELPFUL;
const uint32_t now = NowMs();
int n = 0;
for (const auto &e : g_cache) {
if (n >= maxOut)
break;
if (!e.used || e.targetGuid != unitGuid || e.kind != want)
continue;
if (e.expirationMs != 0 && now >= e.expirationMs)
continue; // expired (infinite-duration entries pass)
out[n++] = {e.spellId, e.casterGuid, e.expirationMs, e.durationMs};
}
return n;
}
} // namespace Aura::Source
+22 -2
View File
@@ -1,12 +1,12 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
#pragma once
@@ -42,4 +42,24 @@ namespace Aura::Source {
bool Get(uint64_t unitGuid, uint32_t spellId, uint64_t *outCaster,
uint32_t *outExpirationMs, uint32_t *outDurationMs);
// One cached aura, as returned by `Enumerate`.
struct CachedAura {
uint32_t spellId;
uint64_t casterGuid; // 0 = caster unknown (application hook, no SpellGo)
uint32_t expirationMs; // 0 = infinite / unknown
uint32_t durationMs; // applied duration (incl. caster mods); 0 = none
};
// Fills `out` with up to `maxOut` cached, non-expired auras on `unitGuid`
// whose helpful/harmful classification matches `harmful`. Returns the count
// written. The classification is recorded from the aura's descriptor slot at
// application time (`OnAuraAdded`/`OnAuraStacksChanged`); entries we only ever
// saw via `SMSG_SPELL_GO` have no slot and so an unknown classification —
// those are excluded (we can't tell which range they belong to).
//
// This is the fallback source for `Aura::Data` when the unit descriptor has
// dropped an aura's slot (rogue stealth, party range fluctuation) even though
// the aura is still active server-side: the cache survives descriptor clears.
int Enumerate(uint64_t unitGuid, bool harmful, CachedAura *out, int maxOut);
} // namespace Aura::Source
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `BAG_UPDATE_DELAYED` event — fires once per frame in which any
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Inject ClassicAPI-owned bindings into a chosen engine section.
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "cache/QueryLoad.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `GetSavedCharacterOrder(realm)` / `SetSavedCharacterOrder(realm, order)`
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `GetCurrentChatGUID()` — call from inside an addon's OnEvent for
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Class metadata accessors backed by `ChrClasses.dbc`.
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Container.GetContainerItemCharges(containerIndex, slotIndex)` —
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Container.GetItemCooldown(itemID)` — modern signature only
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Container.GetContainerItemDurability(containerIndex, slotIndex)` —
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Container.GetContainerItemID(bagIndex, slotIndex)` — modern
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Container.MoveItem(srcBag, srcSlot, dstBag, dstSlot, count)` —
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Container.IsContainerItemOpenable(bag, slot)` — positional-arg
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Container.GetContainerItemRepairCost(containerIndex, slotIndex)`
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Container.SwapItems(srcBag, srcSlot, dstBag, dstSlot)` — atomic
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
//
// Restores Lua 5.0's stripped `coroutine.*` library to the in-game Lua
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_CreatureInfo.GetCreatureInfoByID(creatureID)` -> table | nil
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `GetCursorInfo()` — backport of the modern WoW global. Surfaces
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
+101
View File
@@ -0,0 +1,101 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "cvar/Factory.h"
#include "Offsets.h"
#include <cstdint>
#include <cstdio>
#include <cstdlib>
namespace CVar::Factory {
namespace {
// `FUN_REGISTER_CVAR` — verified ABI (see Offsets.h and the calibration
// against the engine's own call site at 0x005E2490 / the Lua
// Script_RegisterCVar path). MSVC `__fastcall` places the first two args in
// ECX/EDX and pushes the rest right-to-left, which reproduces the engine's
// exact push order: flags, defaultValue, callback, categoryId, hiddenBool,
// userData (with flags as the first stack arg / last push).
using RegisterCVar_t = void *(__fastcall *)(const char *name, int edxZero,
uint32_t flags,
const char *defaultValue,
ChangeCallback callback,
int categoryId, int hiddenBool,
void *userData);
using FindCVar_t = void *(__fastcall *)(const char *name);
// `FUN_SET_CVAR_VALUE` — `__thiscall(cvar /*ecx*/, value, a3, a4, a5, a6)`,
// i.e. ECX=cvar with `value` AND a3..a6 all passed on the stack (5 stack
// args; the function cleans 20 bytes / `RET 0x14`). NOT __fastcall — that
// would route `value` through EDX, push only 4 args, and leave the callee
// popping 4 bytes too many → ESP corruption → silent crash. Mirror the
// Script_SetCVar invocation (0x00488CA8): a3..a6 = 1, 0, 0, 1.
using SetCVarValue_t = char(__thiscall *)(void *cvar, const char *value,
int a3, int a4, int a5, int a6);
const char *ReadValue(void *cvar) {
if (cvar == nullptr)
return nullptr;
return *reinterpret_cast<const char *const *>(
static_cast<const uint8_t *>(cvar) + Offsets::OFF_CVAR_VALUE_STR);
}
} // namespace
Handle Register(const char *name, const char *defaultValue, uint32_t flags,
ChangeCallback callback, void *userData) {
if (name == nullptr)
return nullptr;
auto reg = reinterpret_cast<RegisterCVar_t>(Offsets::FUN_REGISTER_CVAR);
return reg(name, /*edxZero*/ 0, flags, defaultValue, callback,
Offsets::CVAR_SCRIPT_CATEGORY, /*hiddenBool*/ 0, userData);
}
Handle Find(const char *name) {
if (name == nullptr)
return nullptr;
auto find = reinterpret_cast<FindCVar_t>(Offsets::FUN_FIND_CVAR);
return find(name);
}
const char *GetString(Handle cvar) {
return ReadValue(cvar);
}
int GetInt(Handle cvar, int fallback) {
const char *value = ReadValue(cvar);
if (value == nullptr || value[0] == '\0')
return fallback;
return std::atoi(value);
}
void SetString(Handle cvar, const char *value) {
if (cvar == nullptr || value == nullptr)
return;
auto set = reinterpret_cast<SetCVarValue_t>(Offsets::FUN_SET_CVAR_VALUE);
set(cvar, value, 1, 0, 0, 1);
}
void SetInt(Handle cvar, int value) {
char buf[16];
// Hand-format to avoid pulling in a locale-aware path; value range for
// our cvars is small, 16 bytes is ample for a 32-bit int + sign + NUL.
std::snprintf(buf, sizeof buf, "%d", value);
SetString(cvar, buf);
}
} // namespace CVar::Factory
+80
View File
@@ -0,0 +1,80 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
// CVar::Factory — create and drive engine CVars from C++.
//
// Vanilla 1.12 only exposes CVar *reading* and *registration without a
// callback* through Lua (`RegisterCVar(name, default)`); there's no way for
// an addon to register a cvar with a change-callback or to react to value
// changes. This wraps the engine's internal registrar (`FUN_REGISTER_CVAR`)
// and value-setter (`FUN_SET_CVAR_VALUE`) so other modules can publish
// persisted, validated settings (clamped via the change callback and saved
// to Config.wtf).
//
// CVars registered here persist to `WTF\Config.wtf` (the engine forces the
// archive flag bit on at registration) and are visible to the in-game Lua
// `GetCVar`/`SetCVar` globals, since cvar storage is process-global.
namespace CVar::Factory {
// Opaque handle to an engine CVar. Stable for the cvar's lifetime (vanilla
// never reallocates a registered cvar).
using Handle = void *;
// Change callback. Fires whenever the cvar's value changes (and once at
// registration). Mirrors the engine ABI verified at FUN_SET_CVAR_VALUE:
// - `cvar` — this cvar's Handle
// - `prevValue` — the value before the change (may be null/empty)
// - `newValue` — the value being applied
// - `userData` — whatever was passed to Register
// Return a nonzero value to ACCEPT the change, 0 to REJECT it (the engine
// keeps the previous value). To clamp rather than reject, set the clamped
// value with SetString from inside the callback and return 0 for the
// out-of-range input. Must tolerate being invoked with partial context.
using ChangeCallback = int(__fastcall *)(Handle cvar, const char *prevValue,
const char *newValue, void *userData);
// Register a cvar (or fetch the existing one — the engine dedups by name,
// and re-registering OR-merges `flags` and updates the callback). `name`
// and `defaultValue` must outlive the process (the engine stores `name` by
// pointer; pass string literals). `defaultValue` may be null for "". The
// returned Handle is never null on success; null only if the engine
// registrar itself failed.
Handle Register(const char *name, const char *defaultValue,
uint32_t flags = 0, ChangeCallback callback = nullptr,
void *userData = nullptr);
// Look up an already-registered cvar by name, or null if none.
Handle Find(const char *name);
// Read the cvar's current value string (the raw engine pointer at
// `+OFF_CVAR_VALUE_STR`). Returns null if `cvar` is null. The pointer is
// valid for the cvar's lifetime.
const char *GetString(Handle cvar);
// Read the value parsed as a base-10 integer, or `fallback` if `cvar` is
// null or the value isn't numeric.
int GetInt(Handle cvar, int fallback = 0);
// Set the cvar's value (fires its change callback). No-op if `cvar` is
// null. `value` is copied by the engine into the cvar's inline buffer.
void SetString(Handle cvar, const char *value);
// Convenience: set from an integer.
void SetInt(Handle cvar, int value);
} // namespace CVar::Factory
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Glue-side CVar bindings. Vanilla 1.12 registers `GetCVar`,
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Lookup.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Log.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Temporary probe helpers used to derive byte offsets in the player /
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_EncodingUtil.EncodeBase64` / `DecodeBase64`, with `Base64Variant`
+32 -29
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
@@ -30,6 +30,16 @@ constexpr int kMaxDepth = 64;
// ---------------- encode ----------------
// tinycbor's grow-and-retry contract: on buffer overflow the encoder
// switches to counting mode (end = NULL, accumulates bytes_needed) and
// encoding must CONTINUE so the final tally is complete — a nested
// container's tally only reaches the parent via close_container. So
// CborErrorOutOfMemory is not a failure here; the caller reads
// cbor_encoder_get_extra_bytes_needed() afterwards, resizes, retries.
bool EncOk(CborError err) {
return err == CborNoError || err == CborErrorOutOfMemory;
}
bool EncodeValue(void *L, int idx, CborEncoder *enc, int depth);
bool IsLuaArray(void *L, int idx, int &outLen) {
@@ -73,8 +83,8 @@ bool EncodeTable(void *L, int idx, CborEncoder *enc, int depth) {
int arrayLen = 0;
if (IsLuaArray(L, idx, arrayLen)) {
CborEncoder arr;
if (cbor_encoder_create_array(enc, &arr,
static_cast<size_t>(arrayLen)) != CborNoError)
if (!EncOk(cbor_encoder_create_array(enc, &arr,
static_cast<size_t>(arrayLen))))
return false;
for (int i = 1; i <= arrayLen; ++i) {
Game::Lua::PushNumber(L, static_cast<double>(i));
@@ -85,12 +95,12 @@ bool EncodeTable(void *L, int idx, CborEncoder *enc, int depth) {
}
Game::Lua::SetTop(L, -2);
}
return cbor_encoder_close_container(enc, &arr) == CborNoError;
return EncOk(cbor_encoder_close_container(enc, &arr));
}
const int pairs = CountTablePairs(L, idx);
CborEncoder map;
if (cbor_encoder_create_map(enc, &map, static_cast<size_t>(pairs)) != CborNoError)
if (!EncOk(cbor_encoder_create_map(enc, &map, static_cast<size_t>(pairs))))
return false;
Game::Lua::PushNil(L);
while (Game::Lua::Next(L, idx) != 0) {
@@ -101,16 +111,16 @@ bool EncodeTable(void *L, int idx, CborEncoder *enc, int depth) {
}
Game::Lua::SetTop(L, -2);
}
return cbor_encoder_close_container(enc, &map) == CborNoError;
return EncOk(cbor_encoder_close_container(enc, &map));
}
bool EncodeValue(void *L, int idx, CborEncoder *enc, int depth) {
switch (Game::Lua::Type(L, idx)) {
case Game::Lua::TYPE_NIL:
return cbor_encode_null(enc) == CborNoError;
return EncOk(cbor_encode_null(enc));
case Game::Lua::TYPE_BOOLEAN:
return cbor_encode_boolean(enc,
Game::Lua::ToBoolean(L, idx) != 0) == CborNoError;
return EncOk(cbor_encode_boolean(enc,
Game::Lua::ToBoolean(L, idx) != 0));
case Game::Lua::TYPE_NUMBER: {
const double n = Game::Lua::ToNumber(L, idx);
// Whole numbers in [-2^53, 2^53] go out as integers so
@@ -118,17 +128,17 @@ bool EncodeValue(void *L, int idx, CborEncoder *enc, int depth) {
if (std::isfinite(n) && n == std::floor(n) &&
n >= -9007199254740992.0 && n <= 9007199254740992.0) {
if (n >= 0.0)
return cbor_encode_uint(enc,
static_cast<uint64_t>(n)) == CborNoError;
return cbor_encode_negative_int(enc,
static_cast<uint64_t>(-n)) == CborNoError;
return EncOk(cbor_encode_uint(enc,
static_cast<uint64_t>(n)));
return EncOk(cbor_encode_negative_int(enc,
static_cast<uint64_t>(-n)));
}
return cbor_encode_double(enc, n) == CborNoError;
return EncOk(cbor_encode_double(enc, n));
}
case Game::Lua::TYPE_STRING: {
const char *s = Game::Lua::ToString(L, idx);
const unsigned int n = Game::Lua::StrLen(L, idx);
return cbor_encode_text_string(enc, s, n) == CborNoError;
return EncOk(cbor_encode_text_string(enc, s, n));
}
case Game::Lua::TYPE_TABLE:
return EncodeTable(L, idx, enc, depth);
@@ -143,22 +153,15 @@ int __fastcall Script_SerializeCBOR(void *L) {
return 0;
}
// tinycbor encodes into a fixed-size buffer; if it overflows, it
// tracks the extra bytes needed so we can resize and retry.
// tinycbor encodes into a fixed-size buffer; on overflow it keeps
// encoding in counting mode (EncOk treats OutOfMemory as success) so
// the extra-bytes tally is complete, then we resize and retry once.
std::vector<uint8_t> buf(256);
for (;;) {
CborEncoder enc;
cbor_encoder_init(&enc, buf.data(), buf.size(), 0);
if (!EncodeValue(L, 1, &enc, 0)) {
// If it was a buffer-size issue we'll catch it via the
// extra-bytes-needed check below; otherwise this is a
// genuine "value not representable" failure.
const size_t extra = cbor_encoder_get_extra_bytes_needed(&enc);
if (extra == 0)
return 0;
buf.resize(buf.size() + extra);
continue;
}
if (!EncodeValue(L, 1, &enc, 0))
return 0; // genuine "value not representable" failure
const size_t extra = cbor_encoder_get_extra_bytes_needed(&enc);
if (extra > 0) {
buf.resize(buf.size() + extra);
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_EncodingUtil.CompressString` / `DecompressString`, full
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_EquipmentSet.*` — backports the modern equipment-set API on top
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Data.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Locations.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Storage.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `GameTooltip:SetEquipmentSet(name)` — modern method that fills the
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Custom.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// FACTION_STANDING_CHANGED — fires once per reputation change with
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Restores `UNIT_FACTION("player")` event semantics that vanilla 1.12.1
+321
View File
@@ -0,0 +1,321 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Modern Region/Frame method backports.
//
// Modern addon ports routinely call these methods unconditionally —
// they're the C-level frame surface later clients take for granted.
// This module supplies the ones vanilla lacks (surfaced via
// AI_VoiceOver's modern code path, but generic to every port):
//
// - `region:SetPoint("RIGHT")` — the one-arg form. Vanilla's parser
// accepts every other modern form but errors when arg 3 is fully
// omitted; a co-hook on Script_SetPoint normalizes it to
// `(point, 0, 0)` (parent-relative, zero offsets — the exact modern
// semantic).
// - `region:SetSize(w, h)` / `region:GetSize()` — registered on the
// REGION base method registry, so they resolve on frames, buttons,
// textures, fontstrings — everything.
// - `frame:SetShown(shown)` — Show/Hide are per-branch script functions
// (each with its own object typecheck), so SetShown is registered
// separately on Frame / Texture / FontString with matching delegates.
// - `frame:SetResizeBounds(minW, minH [, maxW, maxH])` — the modern
// rename of vanilla's SetMinResize/SetMaxResize pair.
// - `frame:HookScript(scriptType, handler)` — absent in vanilla
// entirely. Chains after any existing handler and invokes `handler`
// with modern positional args (see the implementation comment).
//
// Implementation style: each new method delegates to the ENGINE's own
// Script_* implementations by reshaping the Lua stack and calling them
// (`int __fastcall(void *L)`, self at index 1) — no re-derived setters,
// so pixel/UI-scale conversion, object resolution, and type checking are
// all the engine's own code. Values are read out BEFORE any stack
// reshaping.
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
#include <cstring>
namespace Frame::Modern {
namespace {
using ScriptFn_t = int(__fastcall *)(void *L);
int CallScript(uintptr_t fn, void *L) {
return reinterpret_cast<ScriptFn_t>(fn)(L);
}
// ---- SetSize / GetSize (Region base — all region types) -------------------
// `region:SetSize(width, height)` → SetWidth + SetHeight.
int __fastcall Script_SetSize(void *L) {
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3)) {
Game::Lua::Error(L, "Usage: region:SetSize(width, height)");
return 0;
}
const double h = Game::Lua::ToNumber(L, 3);
Game::Lua::SetTop(L, 2); // (self, width)
CallScript(Offsets::FUN_SCRIPT_REGION_SETWIDTH, L);
Game::Lua::SetTop(L, 1); // (self)
Game::Lua::PushNumber(L, h);
CallScript(Offsets::FUN_SCRIPT_REGION_SETHEIGHT, L);
return 0;
}
// `region:GetSize()` → width, height.
int __fastcall Script_GetSize(void *L) {
Game::Lua::SetTop(L, 1); // (self)
CallScript(Offsets::FUN_SCRIPT_REGION_GETWIDTH, L); // (self, w)
CallScript(Offsets::FUN_SCRIPT_REGION_GETHEIGHT, L); // (self, w, h)
return 2;
}
// ---- SetShown (per-branch Show/Hide delegates) -----------------------------
// `object:SetShown(shown)` — any falsy value hides, truthy shows,
// matching modern semantics.
template <uintptr_t ShowFn, uintptr_t HideFn>
int __fastcall Script_SetShown(void *L) {
const bool shown = Game::Lua::ToBoolean(L, 2) != 0;
Game::Lua::SetTop(L, 1); // (self) — Show/Hide take no args
CallScript(shown ? ShowFn : HideFn, L);
return 0;
}
// ---- SetResizeBounds (Frame) -----------------------------------------------
// `frame:SetResizeBounds(minWidth, minHeight [, maxWidth, maxHeight])` —
// modern rename of the vanilla SetMinResize/SetMaxResize pair.
int __fastcall Script_SetResizeBounds(void *L) {
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3)) {
Game::Lua::Error(L, "Usage: frame:SetResizeBounds(minWidth, "
"minHeight [, maxWidth, maxHeight])");
return 0;
}
const bool haveMax = Game::Lua::IsNumber(L, 4) && Game::Lua::IsNumber(L, 5);
const double maxW = haveMax ? Game::Lua::ToNumber(L, 4) : 0.0;
const double maxH = haveMax ? Game::Lua::ToNumber(L, 5) : 0.0;
Game::Lua::SetTop(L, 3); // (self, minW, minH)
CallScript(Offsets::FUN_SCRIPT_FRAME_SETMINRESIZE, L);
if (haveMax) {
Game::Lua::SetTop(L, 1); // (self)
Game::Lua::PushNumber(L, maxW);
Game::Lua::PushNumber(L, maxH);
CallScript(Offsets::FUN_SCRIPT_FRAME_SETMAXRESIZE, L);
}
return 0;
}
// ---- HookScript (Frame) ----------------------------------------------------
// `frame:HookScript(scriptType, handler)` — vanilla has no HookScript at
// all. Chains onto any existing handler: the old handler runs first,
// vanilla-style (no args — it reads the `this`/`event`/`argN` globals like
// every 1.12 handler), then `handler` is invoked MODERN-style with
// positional arguments `(frame, event, arg1..arg9)` for OnEvent and
// `(frame, arg1..arg9)` otherwise — mirroring the compat convention
// modern addon ports expect (and ship themselves when they detect a
// legacy client). The chain is a C closure over (handler, old, isOnEvent).
// Pushes _G[name].
void PushGlobal(void *L, const char *name) {
Game::Lua::PushString(L, name);
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
}
// The chained handler the engine actually invokes (with no args, like any
// vanilla script handler). Upvalues: 1 = hook handler, 2 = old handler
// (or nil), 3 = isOnEvent (number). Errors inside lua_call propagate to
// the engine's own protected script dispatch.
int __fastcall HookThunk(void *L) {
if (Game::Lua::Type(L, Game::Lua::UpvalueIndex(2)) ==
Game::Lua::TYPE_FUNCTION) {
Game::Lua::PushValue(L, Game::Lua::UpvalueIndex(2));
Game::Lua::Call(L, 0, 0);
}
const bool isOnEvent =
Game::Lua::ToNumber(L, Game::Lua::UpvalueIndex(3)) != 0;
Game::Lua::PushValue(L, Game::Lua::UpvalueIndex(1));
PushGlobal(L, "this");
if (isOnEvent)
PushGlobal(L, "event");
static const char *const kArgs[] = {"arg1", "arg2", "arg3", "arg4",
"arg5", "arg6", "arg7", "arg8",
"arg9"};
for (const char *a : kArgs)
PushGlobal(L, a);
Game::Lua::Call(L, (isOnEvent ? 2 : 1) + 9, 0);
return 0;
}
int __fastcall Script_HookScript(void *L) {
if (!Game::Lua::IsString(L, 2) ||
Game::Lua::Type(L, 3) != Game::Lua::TYPE_FUNCTION) {
Game::Lua::Error(L, "Usage: frame:HookScript(\"type\", function)");
return 0;
}
const char *scriptName = Game::Lua::ToString(L, 2);
const bool isOnEvent =
scriptName != nullptr && std::strcmp(scriptName, "OnEvent") == 0;
Game::Lua::SetTop(L, 3); // (self, name, handler)
// GetScript reads self/name at 1/2 and pushes the current handler
// (or nil) — extra stack entries above are ignored by its reads.
CallScript(Offsets::FUN_SCRIPT_FRAME_GETSCRIPT, L); // (.., old)
// Closure upvalues, in upvalue order: handler, old, isOnEvent.
Game::Lua::PushValue(L, 3); // handler
Game::Lua::PushValue(L, 4); // old
Game::Lua::PushNumber(L, isOnEvent ? 1.0 : 0.0); // flag
Game::Lua::PushCClosure(L, &HookThunk, 3); // (self,name,handler,old,closure)
Game::Lua::Remove(L, 4); // drop old
Game::Lua::Remove(L, 3); // drop handler → (self,name,closure)
CallScript(Offsets::FUN_SCRIPT_FRAME_SETSCRIPT, L);
return 0;
}
// ---- IsEventRegistered (Frame) ---------------------------------------------
// `frame:IsEventRegistered("event")` — vanilla has RegisterEvent /
// UnregisterEvent but not the query. There's no engine Script_* to
// delegate to, so we replicate the membership check that lives inside
// Frame::RegisterEvent (FUN_00702140): find the event by name in the live
// event table, then walk that entry's subscriber chain for this frame.
// The frame pointer we compare against is the same CFrameScriptObject*
// the engine stores (Game::Lua::ResolveObject resolves self exactly as
// Script_RegisterEvent does).
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
bool FrameHasEvent(void *frame, const char *eventName) {
const uint32_t count = *reinterpret_cast<const uint32_t *>(
static_cast<uintptr_t>(Offsets::VAR_EVENT_TABLE_COUNT));
auto *base = *reinterpret_cast<const uint8_t *const *>(
static_cast<uintptr_t>(Offsets::VAR_EVENT_TABLE_BASE_PTR));
if (count == 0 || base == nullptr)
return false;
auto sstrcmpi = reinterpret_cast<SStrCmpI_t>(
static_cast<uintptr_t>(Offsets::FUN_SSTR_CMP_I));
for (uint32_t i = 0; i < count; ++i) {
const uint8_t *entry = base + i * Offsets::EVENT_ENTRY_STRIDE;
const char *name = *reinterpret_cast<const char *const *>(
entry + Offsets::OFF_EVENT_ENTRY_NAME);
if (name == nullptr)
continue;
if (sstrcmpi(name, eventName, 0x7FFFFFFF) != 0)
continue;
// Names are unique, so this is the one entry — walk its chain.
uintptr_t node = *reinterpret_cast<const uintptr_t *>(
entry + Offsets::OFF_EVENT_ENTRY_HEAD);
while ((node & 1) == 0 && node != 0) {
if (*reinterpret_cast<void *const *>(
node + Offsets::OFF_EVENT_NODE_FRAME) == frame)
return true;
node = *reinterpret_cast<const uintptr_t *>(
node + Offsets::OFF_EVENT_NODE_NEXT);
}
return false;
}
return false;
}
int __fastcall Script_IsEventRegistered(void *L) {
if (!Game::Lua::IsString(L, 2)) {
Game::Lua::Error(L, "Usage: frame:IsEventRegistered(\"event\")");
return 0;
}
void *frame = Game::Lua::ResolveObject(L, 1);
const char *eventName = Game::Lua::ToString(L, 2);
const bool registered =
frame != nullptr && eventName != nullptr && FrameHasEvent(frame, eventName);
Game::Lua::PushBool(L, registered);
return 1;
}
// ---- SetPoint one-arg form (Script_SetPoint co-hook) -----------------------
// Vanilla's Script_SetPoint accepts every modern form except the bare
// one-arg call (`region:SetPoint("RIGHT")`) — a fully-omitted arg 3 is
// LUA_TNONE, which fails its string/table/number/nil validation and
// raises the usage error. Normalize to the vanilla (point, x, y) form
// with zero offsets, which anchors relative to the parent at the same
// point — exactly the modern one-arg semantic. All other call shapes
// pass through untouched.
ScriptFn_t g_origSetPoint = nullptr;
int __fastcall SetPoint_h(void *L) {
if (Game::Lua::GetTop(L) == 2 && Game::Lua::IsString(L, 2)) {
Game::Lua::PushNumber(L, 0.0);
Game::Lua::PushNumber(L, 0.0);
}
return g_origSetPoint(L);
}
const Game::HookAutoRegister _setPointHook{
Offsets::FUN_SCRIPT_REGION_SETPOINT,
reinterpret_cast<void *>(&SetPoint_h),
reinterpret_cast<void **>(&g_origSetPoint)};
// ---- Registration ----------------------------------------------------------
const Game::Lua::FrameMethodEntry g_regionMethods[] = {
{"SetSize", &Script_SetSize},
{"GetSize", &Script_GetSize},
};
const Game::Lua::FrameMethodEntry g_frameMethods[] = {
{"SetShown", &Script_SetShown<Offsets::FUN_SCRIPT_FRAME_SHOW,
Offsets::FUN_SCRIPT_FRAME_HIDE>},
{"SetResizeBounds", &Script_SetResizeBounds},
{"HookScript", &Script_HookScript},
{"IsEventRegistered", &Script_IsEventRegistered},
};
const Game::Lua::FrameMethodEntry g_textureMethods[] = {
{"SetShown", &Script_SetShown<Offsets::FUN_SCRIPT_TEXTURE_SHOW,
Offsets::FUN_SCRIPT_TEXTURE_HIDE>},
};
const Game::Lua::FrameMethodEntry g_fontStringMethods[] = {
{"SetShown", &Script_SetShown<Offsets::FUN_SCRIPT_FONTSTRING_SHOW,
Offsets::FUN_SCRIPT_FONTSTRING_HIDE>},
};
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY),
g_regionMethods,
static_cast<int>(sizeof(g_regionMethods) / sizeof(g_regionMethods[0])));
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY),
g_frameMethods,
static_cast<int>(sizeof(g_frameMethods) / sizeof(g_frameMethods[0])));
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_TEXTURE_METHOD_REGISTRY),
g_textureMethods,
static_cast<int>(sizeof(g_textureMethods) / sizeof(g_textureMethods[0])));
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY),
g_fontStringMethods,
static_cast<int>(sizeof(g_fontStringMethods) /
sizeof(g_fontStringMethods[0])));
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Frame::Modern
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_FriendList.SendWhoQueryByName` — sends a /who name-filter query
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_GameObjectInfo.GetGameObjectInfoByID(gameObjectID)` -> table | nil
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
+162 -14
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_GossipInfo.*` — retail-shaped wrappers around vanilla 1.12's
@@ -22,6 +22,24 @@
// SMSG_GOSSIP_MESSAGE handler at `0x004E26E0` and cleared each open.
// We read the same shape the engine's own `Script_GetGossip*` reads.
//
// Vanilla has a SECOND questgiver path this namespace also covers:
// NPCs without a gossip menu send SMSG_QUESTGIVER_QUEST_LIST instead —
// the QUEST_GREETING event and the QuestFrameGreetingPanel ("Current
// Quests" / "Available Quests") — whose data lands in a storage
// entirely separate from the gossip arrays (see the VAR_GREETING_*
// block in Offsets.h). Retail keeps the two APIs disjoint, but for
// backporting it's far more useful for `C_GossipInfo` to serve
// whichever questgiver session is actually live:
// - gossip session live (VAR_GOSSIP_NPC_GUID != 0) → gossip arrays
// - greeting session live (VAR_QUESTGIVER_GUID != 0
// and == VAR_GREETING_NPC_GUID) → greeting arrays
// - neither → empty / ""
// The GUID gates double as staleness protection: both storages persist
// after their frames close (the engine never clears them until the
// next open), so ungated reads would report the previous NPC.
// `GetOptions`/`GetNumOptions` stay gossip-only — greetings have no
// options.
//
// Missing modern fields — vanilla 1.12's server simply never sends
// them, so there's nothing to surface:
// - `rewards`, `spellID` (post-vanilla; merged in with the
@@ -46,6 +64,57 @@ namespace {
using EngineScriptFn = int(__fastcall *)(void *L);
uint64_t ReadU64(uintptr_t va) {
return *reinterpret_cast<const uint64_t *>(va);
}
// A gossip session is live from SMSG_GOSSIP_MESSAGE until the close
// worker (0x004E1FA0) zeroes the GUID and fires GOSSIP_CLOSED.
bool GossipSessionActive() {
return ReadU64(Offsets::VAR_GOSSIP_NPC_GUID) != 0;
}
// A quest-greeting session is live while the quest-session GUID is set
// AND the greeting arrays were filled for that same NPC. The second
// check matters because a gossip-menu quest click opens a quest session
// at panel 1 without ever touching the greeting arrays — without it,
// leftovers from an older greeting NPC would leak through.
bool GreetingSessionActive() {
const uint64_t giver = ReadU64(Offsets::VAR_QUESTGIVER_GUID);
return giver != 0 && giver == ReadU64(Offsets::VAR_GREETING_NPC_GUID);
}
const uint8_t *GreetingEntry(bool active, int slot) {
const uintptr_t base = active ? Offsets::VAR_GREETING_ACTIVE_ENTRIES
: Offsets::VAR_GREETING_AVAILABLE_ENTRIES;
return reinterpret_cast<const uint8_t *>(
base + static_cast<uintptr_t>(slot) * Offsets::GREETING_QUESTS_STRIDE);
}
int GreetingCount(bool active) {
const int count = *reinterpret_cast<const int *>(
active ? Offsets::VAR_GREETING_ACTIVE_COUNT
: Offsets::VAR_GREETING_AVAILABLE_COUNT);
return (count < 0) ? 0
: (count > Offsets::GREETING_QUESTS_MAX) ? Offsets::GREETING_QUESTS_MAX
: count;
}
// The greeting arrays drop the per-quest dialog status, but the packet
// handler's scratch arrays (packet order, zeroed per packet) keep it —
// status 4 = complete/turn-in-ready, same sentinel as the gossip rows.
bool GreetingQuestIsComplete(uint32_t questID) {
const auto *ids = reinterpret_cast<const uint32_t *>(
Offsets::VAR_GREETING_SCRATCH_IDS);
const auto *status = reinterpret_cast<const uint32_t *>(
Offsets::VAR_GREETING_SCRATCH_STATUS);
for (int i = 0; i < Offsets::GREETING_QUESTS_MAX; ++i) {
if (ids[i] == questID)
return status[i] == 4;
}
return false;
}
const uint8_t *OptionEntry(int slot) {
return reinterpret_cast<const uint8_t *>(
static_cast<uintptr_t>(Offsets::VAR_GOSSIP_OPTIONS) +
@@ -79,14 +148,21 @@ bool IsActiveQuest(uint32_t status) {
return status == 3 || status == 4;
}
// `C_GossipInfo.GetText()` — pushes the greeting string the engine
// resolved for the gossip-giver's NPC_TEXT.dbc entry. Mirrors what
// `Script_GetGossipText` does (one inline `lua_pushstring` of the
// fixed buffer at `VAR_GOSSIP_GREETING_TEXT`).
// `C_GossipInfo.GetText()` — the greeting string of whichever
// questgiver session is live: the gossip buffer (what
// `Script_GetGossipText` pushes) for a gossip session, the quest
// greeting buffer (what `Script_GetGreetingText` pushes) for a
// greeting session, "" when neither is open. Both buffers persist
// after close, so the gates keep stale text from the previous NPC
// out.
int __fastcall Script_C_GossipInfo_GetText(void *L) {
const char *text = reinterpret_cast<const char *>(
static_cast<uintptr_t>(Offsets::VAR_GOSSIP_GREETING_TEXT));
Game::Lua::PushString(L, text);
uintptr_t buf = 0;
if (GossipSessionActive())
buf = Offsets::VAR_GOSSIP_GREETING_TEXT;
else if (GreetingSessionActive())
buf = Offsets::VAR_QUEST_GREETING_TEXT;
Game::Lua::PushString(
L, (buf != 0) ? reinterpret_cast<const char *>(buf) : "");
return 1;
}
@@ -108,6 +184,8 @@ int __fastcall Script_C_GossipInfo_GetText(void *L) {
int __fastcall Script_C_GossipInfo_GetOptions(void *L) {
Game::Lua::SetTop(L, 0);
Game::Lua::NewTable(L);
if (!GossipSessionActive())
return 1; // greeting sessions have no options; stale data stays hidden
int outIdx = 0;
for (int slot = 0; slot < Offsets::GOSSIP_OPTIONS_MAX; ++slot) {
@@ -138,6 +216,36 @@ int __fastcall Script_C_GossipInfo_GetOptions(void *L) {
return 1;
}
// Greeting-session variant: walks the pre-split greeting array for the
// wanted side. Same table shape as the gossip rows (questID / title /
// questLevel / isComplete) so consumers can't tell the sources apart.
int PushGreetingQuestList(void *L, bool wantActive) {
const int count = GreetingCount(wantActive);
for (int slot = 0; slot < count; ++slot) {
const uint8_t *entry = GreetingEntry(wantActive, slot);
const uint32_t questID = *reinterpret_cast<const uint32_t *>(
entry + Offsets::OFF_GREETING_QUEST_ID);
Game::Lua::PushNumber(L, static_cast<double>(slot + 1));
Game::Lua::NewTable(L);
Game::Lua::SetFieldNumber(L, "questID",
static_cast<double>(questID));
Game::Lua::SetFieldString(L, "title", reinterpret_cast<const char *>(
entry + Offsets::OFF_GREETING_QUEST_TITLE));
const int32_t level = *reinterpret_cast<const int32_t *>(
entry + Offsets::OFF_GREETING_QUEST_LEVEL);
Game::Lua::SetFieldNumber(L, "questLevel",
static_cast<double>(level));
if (wantActive)
Game::Lua::SetFieldBool(L, "isComplete",
GreetingQuestIsComplete(questID));
Game::Lua::SetTable(L, -3);
}
return 1;
}
// Shared body for `GetAvailableQuests` / `GetActiveQuests`. Both walk
// the same backing array and split by the `status` field — `wantActive`
// inverts the filter.
@@ -145,6 +253,12 @@ int PushQuestList(void *L, bool wantActive) {
Game::Lua::SetTop(L, 0);
Game::Lua::NewTable(L);
if (!GossipSessionActive()) {
if (GreetingSessionActive())
return PushGreetingQuestList(L, wantActive);
return 1; // no questgiver session — empty table, never stale data
}
int outIdx = 0;
for (int slot = 0; slot < Offsets::GOSSIP_QUESTS_MAX; ++slot) {
const uint8_t *entry = QuestEntry(slot);
@@ -189,15 +303,19 @@ int __fastcall Script_C_GossipInfo_GetActiveQuests(void *L) {
int __fastcall Script_C_GossipInfo_GetNumOptions(void *L) {
int count = 0;
for (int slot = 0; slot < Offsets::GOSSIP_OPTIONS_MAX; ++slot) {
if (OptionIndex(OptionEntry(slot)) >= 0)
count += 1;
if (GossipSessionActive()) {
for (int slot = 0; slot < Offsets::GOSSIP_OPTIONS_MAX; ++slot) {
if (OptionIndex(OptionEntry(slot)) >= 0)
count += 1;
}
}
Game::Lua::PushNumber(L, static_cast<double>(count));
return 1;
}
int CountQuests(bool wantActive) {
if (!GossipSessionActive())
return GreetingSessionActive() ? GreetingCount(wantActive) : 0;
int count = 0;
for (int slot = 0; slot < Offsets::GOSSIP_QUESTS_MAX; ++slot) {
const uint8_t *entry = QuestEntry(slot);
@@ -247,6 +365,17 @@ void EngineSelectActiveQuest(int idx0Based) {
fn(idx0Based);
}
// Greeting-session selectors — the workers vanilla's
// Script_Select{Available,Active}Quest call. Index straight into the
// corresponding greeting array (they validate the questgiver GUID
// internally before sending the CMSG).
void EngineSelectGreetingQuest(bool wantActive, int idx0Based) {
auto fn = reinterpret_cast<SelectQuest_t>(
wantActive ? Offsets::FUN_GREETING_SELECT_ACTIVE_QUEST
: Offsets::FUN_GREETING_SELECT_AVAILABLE_QUEST);
fn(idx0Based);
}
// `C_GossipInfo.SelectOption(gossipOptionID[, text])` — wraps vanilla's
// `SelectGossipOption(slotIndex)`. `text` is the boxCoded password,
// passed through to the engine helper which gates on the
@@ -260,6 +389,8 @@ int __fastcall Script_C_GossipInfo_SelectOption(void *L) {
const int target = static_cast<int>(Game::Lua::ToNumber(L, 1));
const char *password =
Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : nullptr;
if (!GossipSessionActive())
return 0; // stale option slots must not send a CMSG
for (int slot = 0; slot < Offsets::GOSSIP_OPTIONS_MAX; ++slot) {
const uint8_t *entry = OptionEntry(slot);
@@ -284,7 +415,8 @@ int __fastcall Script_C_GossipInfo_SelectOptionByIndex(void *L) {
return 0;
}
const int slot0Based = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
if (slot0Based < 0 || slot0Based >= Offsets::GOSSIP_OPTIONS_MAX)
if (slot0Based < 0 || slot0Based >= Offsets::GOSSIP_OPTIONS_MAX ||
!GossipSessionActive())
return 0;
EngineSelectOption(slot0Based, nullptr);
return 0;
@@ -301,6 +433,22 @@ int SelectQuestByID(void *L, bool wantActive, const char *errUsage) {
}
const uint32_t target = static_cast<uint32_t>(Game::Lua::ToNumber(L, 1));
if (!GossipSessionActive()) {
if (!GreetingSessionActive())
return 0;
const int count = GreetingCount(wantActive);
for (int slot = 0; slot < count; ++slot) {
const uint32_t questID = *reinterpret_cast<const uint32_t *>(
GreetingEntry(wantActive, slot) +
Offsets::OFF_GREETING_QUEST_ID);
if (questID == target) {
EngineSelectGreetingQuest(wantActive, slot);
return 0;
}
}
return 0;
}
int idx = 0;
for (int slot = 0; slot < Offsets::GOSSIP_QUESTS_MAX; ++slot) {
const uint8_t *entry = QuestEntry(slot);
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
+2 -2
View File
@@ -1,12 +1,12 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
#include "Game.h"
#include "guid/Guid.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Guid.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `GLOBAL_MOUSE_DOWN` / `GLOBAL_MOUSE_UP` — modern WoW fires these
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `ExportInterfaceFiles art|code` — backport of the 4.3.4 developer-
+3 -3
View File
@@ -1,14 +1,14 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "item/Arg.h"

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