mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-15 07:30:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e793f80f6b | ||
|
|
ded767bf19 | ||
|
|
70a69b64fd | ||
|
|
0f5b50ea14 | ||
|
|
be6dc200fe | ||
|
|
7f52afeef4 |
@@ -1,3 +1,5 @@
|
||||
function MouseIsOver(region, topOffset, bottomOffset, leftOffset, rightOffset)
|
||||
return region:IsMouseOver(topOffset, bottomOffset, leftOffset, rightOffset);
|
||||
if region:IsMouseOver(topOffset, bottomOffset, leftOffset, rightOffset) then
|
||||
return 1;
|
||||
end
|
||||
end
|
||||
|
||||
+61
@@ -584,6 +584,11 @@ build instructions.
|
||||
- [`GetTotemDuration(slot)`](#gettotemdurationslot)
|
||||
- [`TargetTotem(slot)`](#targettotemslot)
|
||||
|
||||
- [Tracking](#tracking)
|
||||
- [`GetNumTrackingTypes()`](#getnumtrackingtypes)
|
||||
- [`GetTrackingInfo(index)`](#gettrackinginfoindex)
|
||||
- [`SetTracking(index)`](#settrackingindex)
|
||||
|
||||
- [TradeSkillUI](#tradeskillui)
|
||||
- [`C_TradeSkillUI.GetTradeSkillListLink()`](#c_tradeskilluigettradeskilllistlink)
|
||||
- [`C_TradeSkillUI.GetCraftListLink()`](#c_tradeskilluigetcraftlistlink)
|
||||
@@ -14141,6 +14146,62 @@ Looks up the totem creature's live GUID (object-manager scan for the
|
||||
player-owned creature of the slot's tracked entry) and sets the target
|
||||
through the same engine path `TargetUnit` uses (`CMSG_SET_SELECTION`).
|
||||
|
||||
## Tracking
|
||||
|
||||
Vanilla 1.12 stores only the tracking spell that is active now. It does
|
||||
not keep a list of the tracking spells you know. These globals add that
|
||||
list. A minimap addon can then build a tracking menu without a hardcoded
|
||||
spell table.
|
||||
|
||||
A tracking spell is a spell in your spellbook that finds creatures,
|
||||
resources, or hidden units. This is the same test the engine uses to find
|
||||
the active tracker. The list covers Find Herbs, Find Minerals, Find
|
||||
Treasure, the Hunter Track spells, Sense Undead, Sense Demons, and Track
|
||||
Humanoids. It also covers server-added trackers, such as Turtle's Find
|
||||
Trees. You do not have to maintain a spell list.
|
||||
|
||||
### `GetNumTrackingTypes()`
|
||||
|
||||
Returns the number of tracking spells in your spellbook.
|
||||
|
||||
```lua
|
||||
GetNumTrackingTypes() -- for example, 8 for a Hunter
|
||||
```
|
||||
|
||||
### `GetTrackingInfo(index)`
|
||||
|
||||
`index` starts at 1 and follows spellbook order. The function returns
|
||||
these five values, or `nil` if `index` is out of range:
|
||||
|
||||
| # | Value | Type | Notes |
|
||||
|---|-------|------|-------|
|
||||
| 1 | `name` | string | Localized spell name. |
|
||||
| 2 | `texture` | string | Icon path. Give it to `texture:SetTexture(...)`. `nil` if the icon is missing. |
|
||||
| 3 | `active` | boolean | `true` when this tracker is the one now in effect. |
|
||||
| 4 | `category` | string | Always `"spell"`. Vanilla has no non-spell trackers. |
|
||||
| 5 | `spellID` | number | The tracking spell's ID. This is a ClassicAPI extra. The retail 5th value is `nested`, which has no meaning here. |
|
||||
|
||||
```lua
|
||||
local name, texture, active, category, spellID = GetTrackingInfo(1)
|
||||
```
|
||||
|
||||
### `SetTracking(index)`
|
||||
|
||||
Turns on the tracking spell at `index`. In vanilla, you select a tracker
|
||||
when you cast its spell. This function casts it. For an out-of-range
|
||||
index, it does nothing. To turn tracking off, use the built-in
|
||||
`CancelTrackingBuff()`.
|
||||
|
||||
```lua
|
||||
for i = 1, GetNumTrackingTypes() do
|
||||
local name = GetTrackingInfo(i)
|
||||
if name == "Find Herbs" then
|
||||
SetTracking(i)
|
||||
break
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## TradeSkillUI
|
||||
|
||||
Backports the 2.x+ **trade-skill list link** — the shareable
|
||||
|
||||
@@ -1785,6 +1785,13 @@ enum Offsets {
|
||||
SMSG_SPELL_CHANNEL_UPDATE = 0x13A,
|
||||
SMSG_SPELL_DELAYED = 0x1E2,
|
||||
SMSG_SPELL_FAILED_OTHER = 0x2A6,
|
||||
// Melee swing result broadcast. Body (verified against the server's own
|
||||
// writer, tortoise-wow Unit::SendAttackStateUpdate): u32 hitInfo,
|
||||
// packedGuid attacker, packedGuid victim, u32 totalDamage, u8 subCount,
|
||||
// per-sub {u32 school, f32 coeff, u32 damage, u32 absorb, i32 resist},
|
||||
// u32 targetState, u32, u32 spellId, u32 blocked. `Aura::JudgementRefresh`
|
||||
// reads through totalDamage.
|
||||
SMSG_ATTACKERSTATEUPDATE = 0x14A,
|
||||
|
||||
// NetClient send — `__thiscall void(void *conn, CDataStore *packet)`.
|
||||
// The outgoing counterpart of FUN_NET_MESSAGE_DISPATCH: every CMSG the
|
||||
@@ -4102,6 +4109,15 @@ enum Offsets {
|
||||
VAR_PET_SPELLBOOK = 0x00B6F098,
|
||||
SPELLBOOK_MAX_SLOTS = 0x400,
|
||||
|
||||
// Active minimap-tracking spell ID (0 = none). Vanilla tracks exactly
|
||||
// one active tracker as a plain int here, derived from the player's
|
||||
// active Track Creatures/Resources/Stealthed aura by `FUN_004E4170`
|
||||
// (an aura-descriptor observer registered by `FUN_004E4100`). Read by
|
||||
// `Script_GetTrackingTexture` (`0x004E4A20`), `Script_CancelTrackingBuff`
|
||||
// (`0x004E4A80`), and `GameTooltip:SetTrackingSpell` (`0x00532C50`);
|
||||
// `Spell::Tracking::GetTrackingInfo` reads it to report `active`.
|
||||
VAR_ACTIVE_TRACKING_SPELL = 0x00BC6378,
|
||||
|
||||
// Player-spell-knowledge bitmap — `[VAR_PLAYER_SPELL_BITMAP]` is a
|
||||
// pointer to a dword bitmap covering all spellIDs the player has
|
||||
// learned, including talent passives, racials, profession recipes,
|
||||
@@ -4372,6 +4388,15 @@ enum Offsets {
|
||||
// results). Callable directly from C++ — push args on stack, call.
|
||||
FUN_SCRIPT_CAST_SPELL_BY_NAME = 0x004B4AB0,
|
||||
|
||||
// `Script_CastSpell` — engine's Lua wrapper for the slot-based
|
||||
// `CastSpell(spellbookSlot, bookType)` global. `int __fastcall(void *L)`.
|
||||
// Parses (slot, bookType) from stack[1]/[2] via `FUN_004B3EC0`, then
|
||||
// dispatches through `FUN_SPELL_CAST_DISPATCH` with the current target
|
||||
// (or self). Raises "Invalid spell slot in CastSpell" for a bad slot.
|
||||
// Callable from C++ by preparing the stack and tail-calling it (the
|
||||
// `Spell::Tracking::SetTracking` "select = cast" path does this).
|
||||
FUN_SCRIPT_CAST_SPELL = 0x004B42F0,
|
||||
|
||||
// Inner spell-cast dispatcher — `__fastcall(slot, bookType, targetGuidLo,
|
||||
// targetGuidHi)`. `Script_CastSpellByName` calls this after resolving
|
||||
// the spell name to a spellbook slot. For ground-target spells, the
|
||||
@@ -6499,6 +6524,12 @@ enum Offsets {
|
||||
// SPELL_ATTR_EX3_IGNORE_CASTER_MODIFIERS (verified via disassembly; the
|
||||
// server enum agrees). NOT AttributesEx2's bit 29 (that's CANT_CRIT).
|
||||
SPELL_ATTR_EX3_IGNORE_CASTER_MODIFIERS = 0x20000000,
|
||||
// AttributesEx3 bit 18 — the paladin judgement-debuff marker. The server's
|
||||
// melee-swing judgement refresh selects auras by SPELLFAMILY_PALADIN +
|
||||
// this bit (tortoise-wow SpellDefines.h SPELL_ATTR_EX3_ALWAYS_HIT,
|
||||
// Unit::DealMeleeDamage). Verified in the client Spell.dbc: all 14
|
||||
// Judgement of Light/Wisdom/Justice/Crusader debuff ranks carry it.
|
||||
SPELL_ATTR_EX3_ALWAYS_HIT = 0x00040000,
|
||||
// AttributesEx channel bits (CHANNELED_1 0x4 | CHANNELED_2 0x40) and the
|
||||
// AttributesEx2 autorepeat flag (bit 5, Auto Shot / Shoot) — each shared
|
||||
// by more than one module, so kept here rather than redefined locally.
|
||||
|
||||
@@ -68,12 +68,9 @@ namespace AuctionHouse::PostItem {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *kEvtStart = "AUCTION_MULTISELL_START";
|
||||
constexpr const char *kEvtUpdate = "AUCTION_MULTISELL_UPDATE";
|
||||
constexpr const char *kEvtFailure = "AUCTION_MULTISELL_FAILURE";
|
||||
const Event::Custom::AutoReserve _r1{kEvtStart};
|
||||
const Event::Custom::AutoReserve _r2{kEvtUpdate};
|
||||
const Event::Custom::AutoReserve _r3{kEvtFailure};
|
||||
const Event::Custom::AutoReserve _evtStart{"AUCTION_MULTISELL_START"};
|
||||
const Event::Custom::AutoReserve _evtUpdate{"AUCTION_MULTISELL_UPDATE"};
|
||||
const Event::Custom::AutoReserve _evtFailure{"AUCTION_MULTISELL_FAILURE"};
|
||||
|
||||
// Generous per-step abort bound. A split or a post is one server round-trip
|
||||
// (~tens of frames); 600 frames (~10s at 60fps) means "something's wrong"
|
||||
@@ -206,7 +203,7 @@ bool FindFreeGeneralSlot(void *L, int *outBag, int *outSlot) {
|
||||
void Finish(bool success) {
|
||||
g_job.active = false;
|
||||
if (!success)
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEvtFailure), "");
|
||||
Event::Custom::Fire(_evtFailure.Slot(), "");
|
||||
// success needs no event: the last PostWait already fired
|
||||
// AUCTION_MULTISELL_UPDATE(total, total).
|
||||
}
|
||||
@@ -279,7 +276,7 @@ void Tick() {
|
||||
// Post confirmed once the server pulls the item out of the slot.
|
||||
if (w.item == nullptr || w.guid != g_job.postGuid) {
|
||||
++g_job.postedStacks;
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEvtUpdate), "%d%d",
|
||||
Event::Custom::Fire(_evtUpdate.Slot(), "%d%d",
|
||||
g_job.postedStacks, g_job.totalStacks);
|
||||
g_job.phase = Phase::Idle;
|
||||
g_job.waitTicks = 0;
|
||||
@@ -400,7 +397,7 @@ int __fastcall Script_C_AuctionHouse_PostItem(void *L) {
|
||||
g_job.runTime = runTime;
|
||||
g_job.phase = Phase::Idle;
|
||||
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEvtStart), "");
|
||||
Event::Custom::Fire(_evtStart.Slot(), "");
|
||||
Game::Lua::PushBool(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// 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/>.
|
||||
|
||||
// Paladin judgement melee refresh — client mirror of the server's white-swing
|
||||
// rule (pfUI#45: the judgement debuff's timer ran to zero while the debuff
|
||||
// stayed up through the paladin's auto-attacks).
|
||||
//
|
||||
// The server rule (tortoise-wow Unit::DealMeleeDamage, vmangos lineage): a
|
||||
// melee swing that dealt ANY damage (`totalDamage > 0` — misses/dodges/parries
|
||||
// deal none) refreshes every aura on the victim that the attacker cast whose
|
||||
// spell is SPELLFAMILY_PALADIN with SPELL_ATTR_EX3_ALWAYS_HIT — the judgement
|
||||
// marker (all 14 Judgement of Light/Wisdom/Justice/Crusader debuff ranks carry
|
||||
// it, verified in the client Spell.dbc). RefreshHolder resets the holder to
|
||||
// its own full duration.
|
||||
//
|
||||
// The edit itself is packet-silent (UpdateAuraDuration is self-scoped — the
|
||||
// same blindness every Turtle::DurationMods rule works around), but its
|
||||
// TRIGGER is not: DealMeleeDamage's only caller is Unit::AttackerStateUpdate
|
||||
// (white swings), which then broadcasts SMSG_ATTACKERSTATEUPDATE to everyone
|
||||
// in range — attacker, victim, and totalDamage are its first fields (wire
|
||||
// format verified against the server's own SendAttackStateUpdate writer; see
|
||||
// Offsets.h at SMSG_ATTACKERSTATEUPDATE). So the swing packet is an exact
|
||||
// client-side stand-in for the refresh, for ANY paladin's judgements — ours
|
||||
// or a raid member's.
|
||||
//
|
||||
// Ability damage (Crusader/Holy Strike) does NOT reach that server block —
|
||||
// melee-class spell damage never routes through DealMeleeDamage on this core
|
||||
// — so no rule is registered for it. Re-judging is covered separately: the
|
||||
// judgement debuff is cast as a real triggered spell, its SMSG_SPELL_GO
|
||||
// refreshes the cache entry via StoreFromCast.
|
||||
//
|
||||
// Not gated on Turtle::Detected(): the block is vmangos-lineage core (the
|
||||
// classic "judgements are refreshed by the paladin's melee attacks"
|
||||
// mechanic), and the refresh only ever touches existing judgement entries
|
||||
// with a matching known caster — on a server without the mechanic the debuff
|
||||
// would genuinely expire, removing the entry via OnAuraRemoved before drift
|
||||
// could show.
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "aura/Source.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "net/PacketReader.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Aura::JudgementRefresh {
|
||||
|
||||
namespace {
|
||||
|
||||
void OnPacket(uint32_t opcode, Net::CDataStore *packet) {
|
||||
if (opcode != Offsets::SMSG_ATTACKERSTATEUPDATE || packet == nullptr)
|
||||
return;
|
||||
Net::Read<uint32_t>(packet); // hitInfo — the damage gate below subsumes it
|
||||
const uint64_t attacker = Net::ReadPackedGuid(packet);
|
||||
const uint64_t victim = Net::ReadPackedGuid(packet);
|
||||
const uint32_t totalDamage = Net::Read<uint32_t>(packet);
|
||||
// The server's exact gate: any nonzero melee damage refreshes; a fully
|
||||
// avoided/absorbed swing (totalDamage 0) does not.
|
||||
if (totalDamage == 0 || attacker == 0 || victim == 0)
|
||||
return;
|
||||
Aura::Source::RefreshJudgements(victim, attacker);
|
||||
}
|
||||
|
||||
const Net::PacketDispatch::AutoSubscribe _sub{&OnPacket};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Aura::JudgementRefresh
|
||||
+392
-3
@@ -340,6 +340,10 @@ Entry *Claim(uint32_t now) {
|
||||
|
||||
// ---- Writes --------------------------------------------------------------
|
||||
|
||||
// Defined in the duration-modifier section below; used by StoreFromCast's
|
||||
// refresh path too.
|
||||
void SignalAuraChanged(uint64_t guid);
|
||||
|
||||
// The SpellGo hook: authoritative caster + caster-modified (talented) timing.
|
||||
// Identity is `(target, spell, caster)`, so a second caster of the same spell
|
||||
// opens its own entry instead of overwriting the first's, and a recast by the
|
||||
@@ -373,6 +377,12 @@ void StoreFromCast(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
|
||||
e->casterGuid = casterGuid;
|
||||
e->expirationMs = expirationMs;
|
||||
e->durationMs = durationMs;
|
||||
// An in-place recast refresh changes no descriptor field, so the engine
|
||||
// fires no UNIT_AURA — event-driven consumers (aura-bar addons counting
|
||||
// down a once-read duration) would never re-read the new timing. Signal it
|
||||
// ourselves (deferred to the world tick, coalesced). A NEW application
|
||||
// doesn't need this: its descriptor write fires the engine's own event.
|
||||
SignalAuraChanged(targetGuid);
|
||||
}
|
||||
|
||||
// The OnAuraAdded / OnAuraStacksChanged application hooks: a descriptor slot,
|
||||
@@ -810,6 +820,279 @@ void OnWorldTick() {
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
|
||||
// ---- Paladin judgement identity + refresh (pfUI#45) ----------------------
|
||||
//
|
||||
// The judgement debuff (Judgement of Light/Wisdom/Justice/Crusader) is cast
|
||||
// SERVER-side as a triggered spell whose SMSG_SPELL_GO never reaches the
|
||||
// client, so its cache entry is seated caster-less by OnAuraAdded — and the
|
||||
// server's refresh mechanics are keyed by caster. The one attributable
|
||||
// signal is the Judgement CAST itself (SPELL_PALADIN_JUDGEMENT, the single
|
||||
// spell every seal judges through): its SPELL_GO names caster and victim.
|
||||
// HandleSpellGo uses it to (a) refresh existing judgement entries WITH
|
||||
// adoption — a re-judge is an in-place RefreshHolder server-side, no
|
||||
// descriptor change, no OnAuraAdded — and (b) arm a short window so the
|
||||
// fresh debuff's application gets attributed to the judging caster. From
|
||||
// then on the caster-STRICT swing refresh (Aura::JudgementRefresh's
|
||||
// SMSG_ATTACKERSTATEUPDATE subscriber) can do its job.
|
||||
|
||||
constexpr uint32_t kSpellJudgement = 20271; // server SPELL_PALADIN_JUDGEMENT
|
||||
|
||||
// The server's judgement-debuff selector, verbatim from its melee-refresh
|
||||
// block (tortoise-wow Unit::DealMeleeDamage): SPELLFAMILY_PALADIN +
|
||||
// SPELL_ATTR_EX3_ALWAYS_HIT. All 14 debuff ranks carry it (client DBC
|
||||
// verified); no shared family-flag mask exists (each seal's debuff has its
|
||||
// own bit), which is why this isn't an AddDurationMod rule.
|
||||
bool IsJudgementSpell(const uint8_t *rec) {
|
||||
constexpr uint32_t kPaladinFamily = 10; // SPELLFAMILY_PALADIN
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
if (*reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_FAMILY_NAME) != kPaladinFamily)
|
||||
return false;
|
||||
return (*reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX3) &
|
||||
Offsets::SPELL_ATTR_EX3_ALWAYS_HIT) != 0;
|
||||
}
|
||||
|
||||
// Armed attribution: a Judgement cast went off from `caster` at `victim`;
|
||||
// the triggered debuff's OnAuraAdded lands ~a server tick later (SPELL_GO
|
||||
// precedes the aura's SMSG_UPDATE_OBJECT — the same ordering kEvictGraceMs
|
||||
// exists for). One-shot, short TTL so a judge whose debuff never lands
|
||||
// can't mis-attribute a later application.
|
||||
struct JudgeArm {
|
||||
uint64_t victim;
|
||||
uint64_t caster;
|
||||
uint32_t untilMs;
|
||||
};
|
||||
constexpr int kJudgeArmMax = 4; // simultaneous judging paladins in view
|
||||
constexpr uint32_t kJudgeArmTtlMs = 2000;
|
||||
JudgeArm g_judgeArms[kJudgeArmMax];
|
||||
|
||||
void ArmJudgementAttribution(uint64_t victim, uint64_t caster) {
|
||||
const uint32_t now = NowMs();
|
||||
JudgeArm *slot = nullptr;
|
||||
for (auto &a : g_judgeArms) {
|
||||
if (a.victim == victim && a.caster == caster) {
|
||||
slot = &a; // re-judge inside the TTL — refresh in place
|
||||
break;
|
||||
}
|
||||
if (slot == nullptr &&
|
||||
(a.victim == 0 || Time::Clock::Reached(now, a.untilMs)))
|
||||
slot = &a;
|
||||
}
|
||||
if (slot == nullptr)
|
||||
slot = &g_judgeArms[0]; // all live (>4 paladins?) — steal the first
|
||||
*slot = {victim, caster, now + kJudgeArmTtlMs};
|
||||
}
|
||||
|
||||
// The caster armed for `victim`, or 0. One-shot: consumed on hit.
|
||||
uint64_t ConsumeJudgementAttribution(uint64_t victim, uint32_t now) {
|
||||
for (auto &a : g_judgeArms) {
|
||||
if (a.victim != victim || a.victim == 0 ||
|
||||
Time::Clock::Reached(now, a.untilMs))
|
||||
continue;
|
||||
const uint64_t caster = a.caster;
|
||||
a = {};
|
||||
return caster;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Shared body of the two refresh triggers. `adopt` lets the Judgement-cast
|
||||
// path claim a caster-less entry (the cast is proof of ownership); the swing
|
||||
// path stays caster-strict — a swing gives no proof, and adopting there
|
||||
// would let any tank's white swings pin another paladin's judgement timer.
|
||||
int RefreshJudgementsImpl(uint64_t unitGuid, uint64_t attackerGuid, bool adopt) {
|
||||
if (unitGuid == 0 || attackerGuid == 0)
|
||||
return 0;
|
||||
const uint32_t now = NowMs();
|
||||
int refreshed = 0;
|
||||
for (int i = 0; i < g_usedHigh; ++i) {
|
||||
Entry &e = g_cache[i];
|
||||
if (!e.used || e.targetGuid != unitGuid)
|
||||
continue;
|
||||
if (e.casterGuid != attackerGuid && !(adopt && e.casterGuid == 0))
|
||||
continue;
|
||||
if (e.durationMs == 0)
|
||||
continue; // never invent a duration
|
||||
if (!IsJudgementSpell(
|
||||
Spell::Lookup::RecordForID(static_cast<int>(e.spellId))))
|
||||
continue;
|
||||
if (e.casterGuid == 0)
|
||||
e.casterGuid = attackerGuid; // adoption claim
|
||||
e.expirationMs = now + e.durationMs; // RefreshHolder → own duration
|
||||
e.stampMs = now;
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
++refreshed;
|
||||
}
|
||||
return refreshed;
|
||||
}
|
||||
|
||||
// ---- Triggered applications (server AddAura inference) --------------------
|
||||
//
|
||||
// Some server mechanics APPLY an aura as a side effect of another spell via a
|
||||
// direct AddAura — no cast, no SMSG_SPELL_GO for the applied aura, and a
|
||||
// duration the packet stream never carries. The application itself reaches
|
||||
// the client only as a descriptor change (OnAuraAdded → caster-less, base
|
||||
// duration). But the TRIGGER's SMSG_SPELL_GO is visible, so — same shape as
|
||||
// the judgement attribution above — a registered trigger cast by the local
|
||||
// player arms its hit targets for a short window, and the caster-less
|
||||
// application that follows consumes the arm: player attribution + the rule's
|
||||
// percentage of the aura's own (player-modified) base duration, mirroring
|
||||
// the server's `max(1, CalculateDuration(caster) * pct / 100)`.
|
||||
//
|
||||
// Registered from src/turtle (Stinging Nettle: Mongoose Bite / fire-trap
|
||||
// effects apply the highest known Serpent Sting rank at 20/40%). Local
|
||||
// player only — a rule may be gated on a talent-granted passive
|
||||
// (`gateSpellId`), and other players' talents aren't client-knowable.
|
||||
|
||||
struct TriggeredApplication {
|
||||
uint32_t triggerSpellId; // exact trigger spell; 0 = match by family+mask below
|
||||
uint32_t triggerFamily; // (triggerSpellId==0) trigger's SpellFamilyName
|
||||
uint64_t triggerMask; // (triggerSpellId==0) family-flag overlap — rank-proof
|
||||
uint32_t gateSpellId; // rule live only while the player knows this; 0 = always
|
||||
uint32_t affectedFamily; // applied aura's SpellFamilyName
|
||||
uint64_t affectedMask; // + family-flag overlap
|
||||
int32_t durationPct; // applied duration = pct% of its own base
|
||||
};
|
||||
constexpr int kTrigAppMax = 32;
|
||||
TriggeredApplication g_trigApps[kTrigAppMax];
|
||||
int g_trigAppCount = 0;
|
||||
|
||||
// Talent gate: does the player currently know `spellID`? The engine's
|
||||
// spell-knowledge bitmap (same store IsPlayerSpell reads — covers talent
|
||||
// passives), checked live at trigger time so respecs are honored.
|
||||
bool PlayerKnowsSpell(uint32_t spellID) {
|
||||
if (spellID == 0)
|
||||
return false;
|
||||
auto *bitmap = Game::Read<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_SPELL_BITMAP));
|
||||
if (bitmap == nullptr)
|
||||
return false;
|
||||
const uint32_t spellCount = Game::Read<uint32_t>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_SPELL_RECORD_COUNT));
|
||||
if (spellID > spellCount)
|
||||
return false;
|
||||
return (bitmap[spellID >> 5] & (1u << (spellID & 31))) != 0;
|
||||
}
|
||||
|
||||
// Armed rule instance awaiting its application on `victim`. Caster is
|
||||
// implicitly the local player (only player triggers arm).
|
||||
struct TrigAppArm {
|
||||
uint64_t victim; // 0 = empty
|
||||
uint32_t affectedFamily;
|
||||
uint64_t affectedMask;
|
||||
int32_t durationPct;
|
||||
uint32_t untilMs;
|
||||
};
|
||||
constexpr int kTrigAppArmMax = 8;
|
||||
constexpr uint32_t kTrigAppArmTtlMs = 2000;
|
||||
TrigAppArm g_trigAppArms[kTrigAppArmMax];
|
||||
|
||||
// Mirror the server's re-apply on an EXISTING matching aura at trigger time
|
||||
// (tortoise-wow ApplyStingingNettle): an existing holder from this caster
|
||||
// whose remaining time exceeds the scaled duration is kept; otherwise the
|
||||
// server re-AddAura's at the scaled duration — a remove+add of the same
|
||||
// spellID that reuses the descriptor slot, so NO OnAuraAdded fires and the
|
||||
// armed window is never consumed (verified in-game: chained Mongoose Bites
|
||||
// left the first application's timer running out). The trigger is the only
|
||||
// audible signal, so the refresh happens here. A caster-less matching entry
|
||||
// is adopted and stamped unconditionally: it's a nettle application whose
|
||||
// arm was missed, seated by the application hook at the WRONG base duration,
|
||||
// so its remaining time can't gate the mirror — and the player's trigger is
|
||||
// proof of ownership, same as the judgement-cast adoption.
|
||||
void RefreshTriggeredExisting(uint64_t victim, const TriggeredApplication &r,
|
||||
uint32_t now) {
|
||||
for (int i = 0; i < g_usedHigh; ++i) {
|
||||
Entry &e = g_cache[i];
|
||||
if (!e.used || e.targetGuid != victim)
|
||||
continue;
|
||||
const bool mine = e.casterGuid == Unit::Identity::PlayerGuid();
|
||||
if (!mine && e.casterGuid != 0)
|
||||
continue; // another caster's aura — not ours to touch
|
||||
const uint8_t *rec =
|
||||
Spell::Lookup::RecordForID(static_cast<int>(e.spellId));
|
||||
if (!Spell::Lookup::IsFitToFamily(rec, r.affectedFamily,
|
||||
r.affectedMask))
|
||||
continue;
|
||||
const uint32_t base = SpellDurationMs(rec, /*casterIsPlayer*/ true);
|
||||
if (base == 0)
|
||||
continue;
|
||||
uint32_t newMs = base * static_cast<uint32_t>(r.durationPct) / 100;
|
||||
if (newMs == 0)
|
||||
newMs = 1;
|
||||
// The server's keep-the-longer guard, applied only to trusted
|
||||
// (player-stamped) timing. An elapsed timer yields Remaining 0 and
|
||||
// refreshes — the invisible server re-adds are exactly what let it
|
||||
// elapse while the aura stayed up.
|
||||
if (mine && e.expirationMs != 0 &&
|
||||
Time::Clock::Remaining(now, e.expirationMs) > newMs)
|
||||
continue;
|
||||
e.casterGuid = Unit::Identity::PlayerGuid();
|
||||
e.durationMs = newMs;
|
||||
e.expirationMs = now + newMs;
|
||||
e.stampMs = now;
|
||||
SignalAuraChanged(victim);
|
||||
break; // one aura instance per (target, spell, caster) — server touches one holder
|
||||
}
|
||||
}
|
||||
|
||||
// A player trigger cast landed on `victim`: arm the first live rule for this
|
||||
// trigger (rules sharing a trigger are registration-ordered — the higher
|
||||
// talent rank registers first, so its gate wins while known). Also refreshes
|
||||
// an existing matching aura in place (see RefreshTriggeredExisting) — the arm
|
||||
// only covers applications that reach the descriptor as a change.
|
||||
void ArmTriggeredApplications(uint64_t victim, uint32_t triggerSpellId,
|
||||
const uint8_t *triggerRec) {
|
||||
for (int i = 0; i < g_trigAppCount; ++i) {
|
||||
const TriggeredApplication &r = g_trigApps[i];
|
||||
if (r.triggerSpellId != 0
|
||||
? r.triggerSpellId != triggerSpellId
|
||||
: !Spell::Lookup::IsFitToFamily(triggerRec, r.triggerFamily,
|
||||
r.triggerMask))
|
||||
continue;
|
||||
if (r.gateSpellId != 0 && !PlayerKnowsSpell(r.gateSpellId))
|
||||
continue;
|
||||
const uint32_t now = NowMs();
|
||||
RefreshTriggeredExisting(victim, r, now);
|
||||
TrigAppArm *slot = nullptr;
|
||||
for (auto &a : g_trigAppArms) {
|
||||
if (a.victim == victim && a.affectedFamily == r.affectedFamily &&
|
||||
a.affectedMask == r.affectedMask) {
|
||||
slot = &a; // re-trigger inside the TTL — refresh in place
|
||||
break;
|
||||
}
|
||||
if (slot == nullptr &&
|
||||
(a.victim == 0 || Time::Clock::Reached(now, a.untilMs)))
|
||||
slot = &a;
|
||||
}
|
||||
if (slot == nullptr)
|
||||
slot = &g_trigAppArms[0]; // all live — steal the first
|
||||
*slot = {victim, r.affectedFamily, r.affectedMask, r.durationPct,
|
||||
now + kTrigAppArmTtlMs};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The armed duration percent for a caster-less application of `rec` on
|
||||
// `victim`, or 0. One-shot: consumed on hit.
|
||||
int32_t ConsumeTriggeredApplication(uint64_t victim, const uint8_t *rec,
|
||||
uint32_t now) {
|
||||
if (victim == 0 || rec == nullptr)
|
||||
return 0;
|
||||
for (auto &a : g_trigAppArms) {
|
||||
if (a.victim != victim || Time::Clock::Reached(now, a.untilMs))
|
||||
continue;
|
||||
if (!Spell::Lookup::IsFitToFamily(rec, a.affectedFamily,
|
||||
a.affectedMask))
|
||||
continue;
|
||||
const int32_t pct = a.durationPct;
|
||||
a = {};
|
||||
return pct;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- SMSG_SPELL_GO co-hook ----------------------------------------------
|
||||
|
||||
// The hit-target list is all we need: each entry is a unit the cast landed
|
||||
@@ -847,7 +1130,27 @@ void HandleSpellGo(uint64_t caster, uint32_t spellId, const uint64_t *targets,
|
||||
// before the aura gate below: the trigger spell applies no aura itself.
|
||||
ApplyDurationModifiers(spellId, caster, targets, numTargets);
|
||||
|
||||
// Judgement: refresh-with-adoption for a re-judge (in-place server
|
||||
// refresh, no aura event) and arm attribution for the fresh debuff that
|
||||
// follows. Before the aura gate — Judgement itself applies no aura.
|
||||
if (spellId == kSpellJudgement) {
|
||||
for (int i = 0; i < numTargets; ++i) {
|
||||
RefreshJudgementsImpl(targets[i], caster, /*adopt*/ true);
|
||||
ArmJudgementAttribution(targets[i], caster);
|
||||
}
|
||||
}
|
||||
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(static_cast<int>(spellId));
|
||||
|
||||
// Triggered-application rules (Stinging Nettle): a registered trigger
|
||||
// cast by the local player arms its hit targets so the server-added aura
|
||||
// that follows (no SPELL_GO of its own) gets player attribution + the
|
||||
// rule's scaled duration. Before the aura gate — Mongoose Bite applies
|
||||
// no aura of its own.
|
||||
if (g_trigAppCount != 0 && caster == Unit::Identity::PlayerGuid())
|
||||
for (int i = 0; i < numTargets; ++i)
|
||||
ArmTriggeredApplications(targets[i], spellId, rec);
|
||||
|
||||
if (!SpellAppliesAura(rec))
|
||||
return;
|
||||
|
||||
@@ -933,9 +1236,38 @@ void StampApplication(void *unit, uint32_t spellId, int slot) {
|
||||
// (and attribute the caster) — SpellGo couldn't always land the talented
|
||||
// value on this target's entry. Otherwise base (we lack other casters'
|
||||
// mods). See WasRecentPlayerCast.
|
||||
const bool byPlayer = WasRecentPlayerCast(spellId);
|
||||
const uint32_t durationMs = SpellDurationMs(rec, byPlayer);
|
||||
const uint64_t caster = byPlayer ? Unit::Identity::PlayerGuid() : 0;
|
||||
uint64_t caster = 0;
|
||||
uint32_t durationMs = 0;
|
||||
// A triggered-application arm (Stinging Nettle): the server AddAura'd
|
||||
// this aura off the player's trigger cast at a scaled duration —
|
||||
// attribute the player and mirror the server's max(1, base × pct / 100),
|
||||
// player-modified base since the player is the caster. Checked BEFORE
|
||||
// the recent-cast window below: the arm names this exact TARGET, while
|
||||
// WasRecentPlayerCast is spell-scoped only — a real Serpent Sting cast
|
||||
// at unit A must not stamp its full duration onto a nettle application
|
||||
// landing on unit B moments later. The order is safe for a real cast's
|
||||
// own application: its entry is already cast-owned (StoreFromCast), and
|
||||
// StoreFromApplication never overwrites an owned entry's timing.
|
||||
if (const int32_t pct = ConsumeTriggeredApplication(unitGuid, rec, NowMs())) {
|
||||
const uint32_t playerBase = SpellDurationMs(rec, /*player*/ true);
|
||||
if (playerBase > 0) {
|
||||
caster = Unit::Identity::PlayerGuid();
|
||||
durationMs = playerBase * static_cast<uint32_t>(pct) / 100;
|
||||
if (durationMs == 0)
|
||||
durationMs = 1;
|
||||
}
|
||||
}
|
||||
if (caster == 0) {
|
||||
const bool byPlayer = WasRecentPlayerCast(spellId);
|
||||
durationMs = SpellDurationMs(rec, byPlayer);
|
||||
caster = byPlayer ? Unit::Identity::PlayerGuid() : 0;
|
||||
// A judgement debuff is cast server-side (no SPELL_GO, so neither
|
||||
// path above can name its caster) — attribute it to the paladin
|
||||
// whose Judgement cast just armed this victim. See the judgement
|
||||
// section.
|
||||
if (caster == 0 && IsJudgementSpell(rec))
|
||||
caster = ConsumeJudgementAttribution(unitGuid, NowMs());
|
||||
}
|
||||
const uint32_t expirationMs = durationMs > 0 ? NowMs() + durationMs : 0;
|
||||
StoreFromApplication(unitGuid, spellId, caster, expirationMs, durationMs,
|
||||
slot, KindForSlot(slot));
|
||||
@@ -1043,6 +1375,63 @@ bool AddDurationMod(uint32_t triggerSpellId, uint32_t affectedFamily,
|
||||
affectedIcon, op, valueMs);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
bool RegisterTriggeredApplication(uint32_t triggerSpellId,
|
||||
uint32_t triggerFamily, uint64_t triggerMask,
|
||||
uint32_t gateSpellId, uint32_t affectedFamily,
|
||||
uint64_t affectedMask, int32_t durationPct) {
|
||||
// Trigger must be identified one way or the other.
|
||||
if ((triggerSpellId == 0 && (triggerFamily == 0 || triggerMask == 0)) ||
|
||||
affectedMask == 0 || durationPct <= 0)
|
||||
return false;
|
||||
for (int i = 0; i < g_trigAppCount; ++i) { // replace an identical rule
|
||||
TriggeredApplication &r = g_trigApps[i];
|
||||
if (r.triggerSpellId == triggerSpellId &&
|
||||
r.triggerFamily == triggerFamily && r.triggerMask == triggerMask &&
|
||||
r.gateSpellId == gateSpellId && r.affectedFamily == affectedFamily &&
|
||||
r.affectedMask == affectedMask) {
|
||||
r.durationPct = durationPct;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (g_trigAppCount >= kTrigAppMax)
|
||||
return false;
|
||||
g_trigApps[g_trigAppCount++] = {triggerSpellId, triggerFamily, triggerMask,
|
||||
gateSpellId, affectedFamily,
|
||||
affectedMask, durationPct};
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool AddTriggeredApplication(uint32_t triggerSpellId, uint32_t gateSpellId,
|
||||
uint32_t affectedFamily, uint64_t affectedMask,
|
||||
int32_t durationPct) {
|
||||
return RegisterTriggeredApplication(triggerSpellId, /*triggerFamily*/ 0,
|
||||
/*triggerMask*/ 0, gateSpellId,
|
||||
affectedFamily, affectedMask,
|
||||
durationPct);
|
||||
}
|
||||
|
||||
bool AddTriggeredApplicationByFamily(uint32_t triggerFamily,
|
||||
uint64_t triggerMask, uint32_t gateSpellId,
|
||||
uint32_t affectedFamily,
|
||||
uint64_t affectedMask,
|
||||
int32_t durationPct) {
|
||||
return RegisterTriggeredApplication(/*triggerSpellId*/ 0, triggerFamily,
|
||||
triggerMask, gateSpellId,
|
||||
affectedFamily, affectedMask,
|
||||
durationPct);
|
||||
}
|
||||
|
||||
int RefreshJudgements(uint64_t unitGuid, uint64_t attackerGuid) {
|
||||
// Caster-strict (no adoption) — the Judgement-cast path inside
|
||||
// HandleSpellGo is the one with proof of ownership; see
|
||||
// RefreshJudgementsImpl.
|
||||
return RefreshJudgementsImpl(unitGuid, attackerGuid, /*adopt*/ false);
|
||||
}
|
||||
|
||||
uint32_t RefreshDurationByFamily(uint64_t unitGuid, uint32_t family,
|
||||
uint64_t mask, uint32_t icon,
|
||||
uint64_t casterGuid) {
|
||||
|
||||
@@ -69,6 +69,21 @@ uint32_t RefreshDurationByFamily(uint64_t unitGuid, uint32_t family,
|
||||
uint64_t mask, uint32_t icon,
|
||||
uint64_t casterGuid);
|
||||
|
||||
// Mirrors the server's melee-swing judgement refresh (tortoise-wow
|
||||
// Unit::DealMeleeDamage): a white swing from `attackerGuid` that dealt any
|
||||
// damage refreshes every aura it cast on `unitGuid` whose spell is
|
||||
// SPELLFAMILY_PALADIN with SPELL_ATTR_EX3_ALWAYS_HIT (the judgement-debuff
|
||||
// marker — all Judgement of Light/Wisdom/Justice/Crusader ranks) back to its
|
||||
// own applied duration. Caster-strict: entries with an unknown caster are
|
||||
// left alone (a swing gives no proof of ownership). The caster comes from
|
||||
// the Judgement-cast attribution inside the SpellGo path — the debuff itself
|
||||
// is cast server-side with no client-visible SPELL_GO, so the Judgement cast
|
||||
// (20271) is what names the owner: it refresh-with-adoption's existing
|
||||
// entries (re-judge = in-place server refresh, no aura event) and arms the
|
||||
// fresh application's attribution. Returns the number refreshed. Fed by
|
||||
// Aura::JudgementRefresh's SMSG_ATTACKERSTATEUPDATE subscriber.
|
||||
int RefreshJudgements(uint64_t unitGuid, uint64_t attackerGuid);
|
||||
|
||||
// Op codes for AddDurationMod (mirror the Lua op strings).
|
||||
enum DurationModOp {
|
||||
DURATION_MOD_REFRESH = 0,
|
||||
@@ -88,6 +103,39 @@ bool AddDurationMod(uint32_t triggerSpellId, uint32_t affectedFamily,
|
||||
uint64_t affectedMask, uint32_t affectedIcon, int op,
|
||||
int32_t valueMs);
|
||||
|
||||
// Register a server "triggered application" rule from C++: `triggerSpellId`'s
|
||||
// SMSG_SPELL_GO — local player casts only — arms its hit targets for a short
|
||||
// window, and a caster-less aura application on an armed target matching
|
||||
// `affectedFamily` + a family-flag overlap with `affectedMask` is attributed
|
||||
// to the player at `durationPct` percent of the aura's own player-modified
|
||||
// base duration (the server's `max(1, CalculateDuration(caster) * pct / 100)`).
|
||||
// For server mechanics that AddAura an aura off another spell's hit with no
|
||||
// cast packet of its own (Turtle's Stinging Nettle: Mongoose Bite / fire-trap
|
||||
// effects apply the highest known Serpent Sting rank at 20/40% duration).
|
||||
// A re-trigger over an EXISTING matching aura is also handled: the server's
|
||||
// re-add reuses the descriptor slot (no application event), so the trigger
|
||||
// additionally refreshes the player's existing cached entry in place,
|
||||
// honoring the server's keep-the-longer-remaining guard.
|
||||
// `gateSpellId` (0 = none) keeps the rule live only while the player knows
|
||||
// that spell (a talent-granted passive), read from the spell-knowledge bitmap
|
||||
// at trigger time so respecs are honored; rules matching the same trigger are
|
||||
// tried in registration order and the first live one arms (register the
|
||||
// higher talent rank first). Returns false on bad input or a full table.
|
||||
bool AddTriggeredApplication(uint32_t triggerSpellId, uint32_t gateSpellId,
|
||||
uint32_t affectedFamily, uint64_t affectedMask,
|
||||
int32_t durationPct);
|
||||
|
||||
// Family-mask trigger form of the above: the trigger matches any cast whose
|
||||
// spell is SpellFamilyName `triggerFamily` with a family-flag overlap of
|
||||
// `triggerMask` — rank-proof when the bit selects exactly the trigger set
|
||||
// (verify against the DBC that no unrelated spell shares it first: hunter
|
||||
// 0x4 is exactly the fire-trap effects, but 0x2 is Mongoose Bite AND Raptor
|
||||
// Strike, so that trigger needs the exact-ID form).
|
||||
bool AddTriggeredApplicationByFamily(uint32_t triggerFamily,
|
||||
uint64_t triggerMask, uint32_t gateSpellId,
|
||||
uint32_t affectedFamily,
|
||||
uint64_t affectedMask, int32_t durationPct);
|
||||
|
||||
// One cached aura, as returned by `Enumerate`.
|
||||
struct CachedAura {
|
||||
uint32_t spellId;
|
||||
|
||||
@@ -143,7 +143,7 @@ void OnWorldTick() {
|
||||
static_cast<uintptr_t>(Offsets::VAR_IN_WORLD)) == 0)
|
||||
return;
|
||||
g_pending = false;
|
||||
const int slot = Event::Custom::Lookup(kEventName);
|
||||
const int slot = _reserve.Slot();
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "");
|
||||
// Notify C++ subscribers of the bag-change frame (see UpdateDelayed.h).
|
||||
|
||||
Vendored
+7
-7
@@ -61,8 +61,8 @@ constexpr int kMaxWaitTicks = 1200; // give up and fire failure after this many
|
||||
|
||||
struct Registered {
|
||||
void *cache;
|
||||
const char *eventName;
|
||||
uintptr_t getRecord; // this cache class's _GetRecord
|
||||
const Event::Custom::AutoReserve *reserve; // completion event's reservation
|
||||
uintptr_t getRecord; // this cache class's _GetRecord
|
||||
Pending pending[kMaxPending];
|
||||
int pendingCount;
|
||||
};
|
||||
@@ -79,8 +79,7 @@ Registered *Find(void *cache) {
|
||||
}
|
||||
|
||||
void Fire(const Registered *r, uint32_t id, bool success) {
|
||||
Event::Custom::FireIdSuccess(Event::Custom::Lookup(r->eventName),
|
||||
static_cast<int>(id), success);
|
||||
Event::Custom::FireIdSuccess(r->reserve->Slot(), static_cast<int>(id), success);
|
||||
}
|
||||
|
||||
int FindPending(const Registered *r, uint32_t id) {
|
||||
@@ -176,15 +175,16 @@ const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
|
||||
} // namespace
|
||||
|
||||
void Register(void *cacheInstance, const char *eventName, uintptr_t getRecordAddr) {
|
||||
if (cacheInstance == nullptr || eventName == nullptr || getRecordAddr == 0)
|
||||
void Register(void *cacheInstance, const Event::Custom::AutoReserve *reserve,
|
||||
uintptr_t getRecordAddr) {
|
||||
if (cacheInstance == nullptr || reserve == nullptr || getRecordAddr == 0)
|
||||
return;
|
||||
if (Find(cacheInstance) != nullptr) // idempotent (safe across /reload)
|
||||
return;
|
||||
if (g_cacheCount >= kMaxCaches)
|
||||
return;
|
||||
g_caches[g_cacheCount].cache = cacheInstance;
|
||||
g_caches[g_cacheCount].eventName = eventName;
|
||||
g_caches[g_cacheCount].reserve = reserve;
|
||||
g_caches[g_cacheCount].getRecord = getRecordAddr;
|
||||
g_caches[g_cacheCount].pendingCount = 0;
|
||||
++g_cacheCount;
|
||||
|
||||
Vendored
+14
-10
@@ -15,6 +15,10 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Event::Custom {
|
||||
struct AutoReserve;
|
||||
}
|
||||
|
||||
// Shared async-load machinery for the client query caches that aren't
|
||||
// item/quest — creature, gameobject, and (potentially) the other
|
||||
// SMSG_*_QUERY caches. Modeled on `Item::Data`'s pending/timeout/event
|
||||
@@ -30,10 +34,9 @@
|
||||
// functions, so hooking creature's and gameobject's separately is fine);
|
||||
// item/quest have their own parsers and aren't touched.
|
||||
//
|
||||
// static const char *kEvent = "CREATURE_DATA_LOAD_RESULT";
|
||||
// static const Event::Custom::AutoReserve _r{kEvent}; // reserve the event
|
||||
// static const Event::Custom::AutoReserve _r{"CREATURE_DATA_LOAD_RESULT"};
|
||||
// ...
|
||||
// Cache::QueryLoad::Register(cacheInstance, kEvent,
|
||||
// Cache::QueryLoad::Register(cacheInstance, &_r,
|
||||
// Offsets::FUN_CREATURE_GET_RECORD); // at init
|
||||
// Cache::QueryLoad::RequestLoad(cacheInstance, id); // fire a load
|
||||
//
|
||||
@@ -44,13 +47,14 @@ namespace Cache::QueryLoad {
|
||||
|
||||
// Register a query cache for async-load tracking. `cacheInstance` is the
|
||||
// cache global (e.g. `Offsets::VAR_CREATURE_CACHE` cast to void*);
|
||||
// `eventName` is a Custom event the caller has reserved via
|
||||
// `Event::Custom::AutoReserve`, fired `(id, success)` on completion;
|
||||
// `getRecordAddr` is the cache class's `_GetRecord` (e.g.
|
||||
// `Offsets::FUN_CREATURE_GET_RECORD`). `cacheInstance`/`eventName` must
|
||||
// outlive the process (pass static values). Idempotent — calling again
|
||||
// for the same cache is a no-op (safe across /reload).
|
||||
void Register(void *cacheInstance, const char *eventName, uintptr_t getRecordAddr);
|
||||
// `reserve` is the caller's `Event::Custom::AutoReserve` for the
|
||||
// completion event, fired `(id, success)`; `getRecordAddr` is the cache
|
||||
// class's `_GetRecord` (e.g. `Offsets::FUN_CREATURE_GET_RECORD`).
|
||||
// `cacheInstance`/`reserve` must outlive the process (pass statics).
|
||||
// Idempotent — calling again for the same cache is a no-op (safe across
|
||||
// /reload).
|
||||
void Register(void *cacheInstance, const Event::Custom::AutoReserve *reserve,
|
||||
uintptr_t getRecordAddr);
|
||||
|
||||
// True if `id` is already in the cache (pure peek, no network query).
|
||||
bool IsCached(void *cacheInstance, uint32_t id);
|
||||
|
||||
@@ -361,7 +361,7 @@ void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetCreatureTypeIDs",
|
||||
&Script_GetCreatureTypeIDs);
|
||||
Cache::QueryLoad::Register(reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE),
|
||||
kCreatureDataLoadResult, Offsets::FUN_CREATURE_GET_RECORD);
|
||||
&_reserveLoadResult, Offsets::FUN_CREATURE_GET_RECORD);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -386,7 +386,7 @@ int __fastcall Script_UseEquipmentSet(void *L) {
|
||||
const uint32_t setID = ArgSetID(L, 1);
|
||||
const Set *s = Data::FindByID(setID);
|
||||
if (s == nullptr) {
|
||||
const int evt = Event::Custom::Lookup(kSwapFinishedEvent);
|
||||
const int evt = kEvtSwapFinished.Slot();
|
||||
if (evt >= 0)
|
||||
Event::Custom::Fire(evt, "%d%d", 0, static_cast<int>(setID));
|
||||
Game::Lua::PushBoolean(L, 0);
|
||||
@@ -396,7 +396,7 @@ int __fastcall Script_UseEquipmentSet(void *L) {
|
||||
// Fire PENDING right after the set-exists check, before any of
|
||||
// the swap work. Addon UI can use this to gate swap-in-progress
|
||||
// visuals.
|
||||
const int pendingEvt = Event::Custom::Lookup(kSwapPendingEvent);
|
||||
const int pendingEvt = kEvtSwapPending.Slot();
|
||||
if (pendingEvt >= 0)
|
||||
Event::Custom::Fire(pendingEvt, "%d", static_cast<int>(setID));
|
||||
|
||||
@@ -569,7 +569,7 @@ int __fastcall Script_UseEquipmentSet(void *L) {
|
||||
virtualPaperdoll[targetSlot - 1] = 0;
|
||||
}
|
||||
|
||||
const int evt = Event::Custom::Lookup(kSwapFinishedEvent);
|
||||
const int evt = kEvtSwapFinished.Slot();
|
||||
if (evt >= 0)
|
||||
Event::Custom::Fire(evt, "%d%d", 1, static_cast<int>(setID));
|
||||
|
||||
@@ -620,8 +620,5 @@ static void RegisterLuaFunctions() {
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
static const Event::Custom::AutoReserve _reserve{kEventName};
|
||||
static const Event::Custom::AutoReserve _reservePending{kSwapPendingEvent};
|
||||
static const Event::Custom::AutoReserve _reserveFinished{kSwapFinishedEvent};
|
||||
|
||||
} // namespace EquipmentSet::Api
|
||||
|
||||
@@ -75,7 +75,7 @@ uint32_t NextAvailableID() {
|
||||
}
|
||||
|
||||
void FireChanged() {
|
||||
const int slot = Event::Custom::Lookup(kEventName);
|
||||
const int slot = kEvtSetsChanged.Slot();
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "");
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "event/Custom.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
@@ -109,8 +111,9 @@ constexpr int LOC_MISSING = -1;
|
||||
// coincidence, not load-bearing).
|
||||
constexpr int LOC_IGNORED = 1;
|
||||
|
||||
// Custom event names. All three are reserved at module-init time
|
||||
// via `Event::Custom::AutoReserve` in `Api.cpp`.
|
||||
// Custom event reservations, shared by the Data and Api TUs (C++17 inline
|
||||
// variables — one instance each; the AutoReserve ctor dedups by name anyway).
|
||||
// Fire via `kEvt*.Slot()`.
|
||||
//
|
||||
// EQUIPMENT_SETS_CHANGED — no payload; fires on any mutation
|
||||
// (create / save / modify / delete /
|
||||
@@ -130,8 +133,8 @@ constexpr int LOC_IGNORED = 1;
|
||||
// pickup→equip pair actually completed,
|
||||
// matching modern's "we dispatched"
|
||||
// definition of success).
|
||||
constexpr const char *kEventName = "EQUIPMENT_SETS_CHANGED";
|
||||
constexpr const char *kSwapPendingEvent = "EQUIPMENT_SWAP_PENDING";
|
||||
constexpr const char *kSwapFinishedEvent = "EQUIPMENT_SWAP_FINISHED";
|
||||
inline const Event::Custom::AutoReserve kEvtSetsChanged{"EQUIPMENT_SETS_CHANGED"};
|
||||
inline const Event::Custom::AutoReserve kEvtSwapPending{"EQUIPMENT_SWAP_PENDING"};
|
||||
inline const Event::Custom::AutoReserve kEvtSwapFinished{"EQUIPMENT_SWAP_FINISHED"};
|
||||
|
||||
} // namespace EquipmentSet
|
||||
|
||||
+10
-10
@@ -244,25 +244,25 @@ void Grow() {
|
||||
} // namespace
|
||||
|
||||
AutoReserve::AutoReserve(const char *name) {
|
||||
if (name == nullptr || g_reservedCount >= MAX_RESERVED)
|
||||
if (name == nullptr)
|
||||
return;
|
||||
// Dedup: a name reserved twice resolves both instances to the one entry.
|
||||
for (int i = 0; i < g_reservedCount; ++i) {
|
||||
if (std::strcmp(g_reserved[i].name, name) == 0)
|
||||
if (std::strcmp(g_reserved[i].name, name) == 0) {
|
||||
index_ = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (g_reservedCount >= MAX_RESERVED)
|
||||
return; // overflow — index_ stays -1, Slot() reports unclaimed
|
||||
g_reserved[g_reservedCount].name = name;
|
||||
g_reserved[g_reservedCount].slot = -1;
|
||||
index_ = g_reservedCount;
|
||||
++g_reservedCount;
|
||||
}
|
||||
|
||||
int Lookup(const char *name) {
|
||||
if (name == nullptr)
|
||||
return -1;
|
||||
for (int i = 0; i < g_reservedCount; ++i) {
|
||||
if (std::strcmp(g_reserved[i].name, name) == 0)
|
||||
return g_reserved[i].slot;
|
||||
}
|
||||
return -1;
|
||||
int AutoReserve::Slot() const {
|
||||
return index_ >= 0 ? g_reserved[index_].slot : -1;
|
||||
}
|
||||
|
||||
int LookupByName(const char *name) {
|
||||
|
||||
+22
-17
@@ -37,31 +37,36 @@ namespace Event::Custom {
|
||||
// DLL operate on the table independently of the others.
|
||||
//
|
||||
// The name pointer must outlive the engine (a string literal does).
|
||||
// Same name reserved twice is deduped; reserving more than `MAX_RESERVED`
|
||||
// names total (see Custom.cpp) silently drops the overflow — keep that cap
|
||||
// above the codebase-wide `AutoReserve` count.
|
||||
// Same name reserved twice is deduped (both instances resolve to the same
|
||||
// reservation); reserving more than `MAX_RESERVED` names total (see
|
||||
// Custom.cpp) silently drops the overflow — keep that cap above the
|
||||
// codebase-wide `AutoReserve` count.
|
||||
//
|
||||
// Fire through the instance: `Fire(_r.Slot(), …)`. `Slot()` is the O(1)
|
||||
// read of the reservation's current slot id — it resolves through the
|
||||
// shared reservation table, so reload invalidation (`PrepareForReload`)
|
||||
// and re-claiming (`RetryClaims`) stay centralized and the value is never
|
||||
// stale. Returns -1 while unclaimed (before the first Lua-side
|
||||
// `RegisterEvent` triggers `RetryClaims`), which `Fire` treats as a no-op.
|
||||
// Do NOT cache the return across frames — read it at fire time (the read
|
||||
// is two loads; there is nothing to save).
|
||||
struct AutoReserve {
|
||||
explicit AutoReserve(const char *name);
|
||||
};
|
||||
int Slot() const;
|
||||
|
||||
// Returns the slot id currently assigned to `name`, or -1 if not yet
|
||||
// claimed (e.g. before the first Lua-side `RegisterEvent` has
|
||||
// triggered `RetryClaims`). Slot indices may change across `/reload`,
|
||||
// so call this at fire time rather than caching the value.
|
||||
//
|
||||
// Only consults our reservation cache — for engine-defined events
|
||||
// (UNIT_FACTION, BAG_UPDATE, etc.) use `LookupByName`.
|
||||
int Lookup(const char *name);
|
||||
private:
|
||||
int index_ = -1; // into the reservation table; -1 = dropped (overflow)
|
||||
};
|
||||
|
||||
// Walks the live engine event table at `[VAR_EVENT_TABLE_BASE_PTR]`
|
||||
// looking for an entry whose name strcmps equal to `name`. Returns
|
||||
// the slot index, or -1 if no entry matches.
|
||||
//
|
||||
// Works for both engine-defined events AND our `AutoReserve`-claimed
|
||||
// custom events (both populate the same table). Slightly slower than
|
||||
// `Lookup` (O(table size) vs O(reservations)) — prefer `Lookup` for
|
||||
// names we've reserved. Use this for engine-defined events we want to
|
||||
// fire ourselves (e.g., polyfilling missing event dispatches).
|
||||
// custom events (both populate the same table). O(table size) — for
|
||||
// names we've reserved, prefer the O(1) `AutoReserve::Slot()`. Use this
|
||||
// for engine-defined events we want to fire ourselves (e.g., polyfilling
|
||||
// missing event dispatches).
|
||||
int LookupByName(const char *name);
|
||||
|
||||
// True iff at least one frame is currently registered for the event in
|
||||
@@ -88,7 +93,7 @@ bool HasListeners(int slot);
|
||||
// Fire(slot, "%s%d", keyName, down); // MODIFIER_STATE_CHANGED(key, down)
|
||||
//
|
||||
// No-op for `eventID < 0`, which lets callers cheaply guard on the
|
||||
// `Lookup()` result without an explicit if.
|
||||
// `Slot()` result without an explicit if.
|
||||
template <typename... Args>
|
||||
inline void Fire(int eventID, const char *format, Args... args) {
|
||||
if (eventID < 0)
|
||||
|
||||
@@ -98,7 +98,7 @@ void __fastcall FireNotify_h(int factionID, int delta) {
|
||||
// any addon relying on the chat event firing first would expect.
|
||||
FireNotify_o(factionID, delta);
|
||||
|
||||
const int slot = Event::Custom::Lookup(kEventName);
|
||||
const int slot = _reserve.Slot();
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "%d%d%d", factionID, newStanding, delta);
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_GameObjectInfo", "RequestLoadGameObjectByID",
|
||||
&Script_RequestLoadGameObjectByID);
|
||||
Cache::QueryLoad::Register(reinterpret_cast<void *>(Offsets::VAR_GAMEOBJECT_CACHE),
|
||||
kGameObjectDataLoadResult, Offsets::FUN_GAMEOBJECT_GET_RECORD);
|
||||
&_reserveLoadResult, Offsets::FUN_GAMEOBJECT_GET_RECORD);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -64,8 +64,8 @@ namespace Input::GlobalMouse {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *kDownEvent = "GLOBAL_MOUSE_DOWN";
|
||||
constexpr const char *kUpEvent = "GLOBAL_MOUSE_UP";
|
||||
const Event::Custom::AutoReserve _reserveDown{"GLOBAL_MOUSE_DOWN"};
|
||||
const Event::Custom::AutoReserve _reserveUp{"GLOBAL_MOUSE_UP"};
|
||||
|
||||
// Held-state bitmap — bit `i` set means the button with 1-based
|
||||
// `IsMouseButtonDown` ID `i+1` is currently held. Modern API IDs:
|
||||
@@ -154,7 +154,7 @@ void ProcessMouseMessage(UINT msg, WPARAM wParam) {
|
||||
else g_mouseButtonMask &= ~bit;
|
||||
}
|
||||
|
||||
const int slot = Event::Custom::Lookup(down ? kDownEvent : kUpEvent);
|
||||
const int slot = (down ? _reserveDown : _reserveUp).Slot();
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "%s", name);
|
||||
}
|
||||
@@ -241,8 +241,6 @@ void RegisterLuaFunctions() {
|
||||
InstallHook();
|
||||
}
|
||||
|
||||
const Event::Custom::AutoReserve _reserveDown{kDownEvent};
|
||||
const Event::Custom::AutoReserve _reserveUp{kUpEvent};
|
||||
const Tick::WorldTick::AutoSubscribe _evict{&EvictTick};
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
|
||||
@@ -102,11 +102,9 @@ void SetBit(int bitIdx, bool down) {
|
||||
if (down) g_modifierMask |= bit;
|
||||
else g_modifierMask &= ~bit;
|
||||
|
||||
// Slot was assigned when the engine populated its event table; our
|
||||
// hook on `RebuildEventTable` recorded the index. Lookup at fire
|
||||
// time rather than caching because `/reload` re-runs the rebuild
|
||||
// and the index may shift.
|
||||
const int slot = Event::Custom::Lookup(kModifierStateChanged);
|
||||
// Read the slot at fire time (never cache it) — `/reload` rebuilds the
|
||||
// event table and the reservation re-claims, which may shift the index.
|
||||
const int slot = _reserveModifierStateChanged.Slot();
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "%s%d", kKeyName[bitIdx], static_cast<int>(down));
|
||||
}
|
||||
|
||||
+4
-4
@@ -58,13 +58,13 @@ static const Event::Custom::AutoReserve _reserveItemDataLoadResult{kItemDataLoad
|
||||
static const Event::Custom::AutoReserve _reserveGetItemInfoReceived{kGetItemInfoReceived};
|
||||
|
||||
static void FireGetItemInfoReceived(int itemID, bool success) {
|
||||
Event::Custom::FireIdSuccess(Event::Custom::Lookup(kGetItemInfoReceived),
|
||||
itemID, success);
|
||||
Event::Custom::FireIdSuccess(_reserveGetItemInfoReceived.Slot(), itemID,
|
||||
success);
|
||||
}
|
||||
|
||||
static void FireItemDataLoadResult(int itemID, bool success) {
|
||||
Event::Custom::FireIdSuccess(Event::Custom::Lookup(kItemDataLoadResult),
|
||||
itemID, success);
|
||||
Event::Custom::FireIdSuccess(_reserveItemDataLoadResult.Slot(), itemID,
|
||||
success);
|
||||
}
|
||||
|
||||
// Item-load callbacks are __stdcall, 2 args / 8 bytes — matching the
|
||||
|
||||
@@ -318,7 +318,7 @@ void AppendNonBagOwned(uint64_t *out, int *count) {
|
||||
}
|
||||
|
||||
void FireChanged() {
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEvtNewItems), "");
|
||||
Event::Custom::Fire(_r.Slot(), "");
|
||||
}
|
||||
|
||||
// Login-baseline handler. Runs every frame but does no work once the
|
||||
|
||||
@@ -344,7 +344,7 @@ StartFn_t g_origStart = nullptr;
|
||||
// structurally: a new item appears or the ring evicts an old one (shifting
|
||||
// every index).
|
||||
void FireFullUpdate() {
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kFullUpdate), "");
|
||||
Event::Custom::Fire(_evFullUpdate.Slot(), "");
|
||||
}
|
||||
|
||||
// START_ROLL: let the original build the store node, then read the item's
|
||||
@@ -373,8 +373,7 @@ void __fastcall Roll_h(void *msg, void *ctx, int a3) {
|
||||
if (structural)
|
||||
FireFullUpdate();
|
||||
if (itemIdx > 0)
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kRollChanged), "%d%d", itemIdx,
|
||||
playerIdx);
|
||||
Event::Custom::Fire(_evRollChanged.Slot(), "%d%d", itemIdx, playerIdx);
|
||||
}
|
||||
|
||||
void __fastcall Won_h(void *ctx, void *msg, int a3) {
|
||||
@@ -388,7 +387,7 @@ void __fastcall Won_h(void *ctx, void *msg, int a3) {
|
||||
if (structural)
|
||||
FireFullUpdate();
|
||||
if (itemIdx > 0)
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kRollComplete), "%d", itemIdx);
|
||||
Event::Custom::Fire(_evRollComplete.Slot(), "%d", itemIdx);
|
||||
}
|
||||
|
||||
void __fastcall Passed_h(void *ctx, void *msg) {
|
||||
@@ -402,7 +401,7 @@ void __fastcall Passed_h(void *ctx, void *msg) {
|
||||
if (structural)
|
||||
FireFullUpdate();
|
||||
if (itemIdx > 0)
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kRollComplete), "%d", itemIdx);
|
||||
Event::Custom::Fire(_evRollComplete.Slot(), "%d", itemIdx);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookRoll{
|
||||
|
||||
+1
-1
@@ -267,7 +267,7 @@ void Complete() {
|
||||
s_scan.currentGuid = 0;
|
||||
s_scan.suppressEvents = false;
|
||||
s_scan.lootMode = false;
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEventCompleted), "");
|
||||
Event::Custom::Fire(_reserve.Slot(), "");
|
||||
}
|
||||
|
||||
void TryStartNext() {
|
||||
|
||||
@@ -223,8 +223,8 @@ int BuildList(LocEntry *out, int maxOut) {
|
||||
// lesson). The scan is bounded (16 debuff slots) and early-outs when idle, so
|
||||
// the per-frame cost is negligible.
|
||||
|
||||
constexpr const char *kEventAdded = "LOSS_OF_CONTROL_ADDED";
|
||||
constexpr const char *kEventUpdate = "LOSS_OF_CONTROL_UPDATE";
|
||||
const Event::Custom::AutoReserve _reserveAdded{"LOSS_OF_CONTROL_ADDED"};
|
||||
const Event::Custom::AutoReserve _reserveUpdate{"LOSS_OF_CONTROL_UPDATE"};
|
||||
|
||||
// Identity of an active effect for frame-to-frame diffing: a CC is keyed by
|
||||
// spellID, a school interrupt by its school mask (spellID 0).
|
||||
@@ -255,7 +255,7 @@ void OnWorldTick() {
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!ContainsKey(g_prev, g_prevCount, cur[i])) {
|
||||
// A newly-applied effect -> LOSS_OF_CONTROL_ADDED(eventIndex).
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEventAdded), "%d", i + 1);
|
||||
Event::Custom::Fire(_reserveAdded.Slot(), "%d", i + 1);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
@@ -269,15 +269,13 @@ void OnWorldTick() {
|
||||
// forward-compatible with per-unit tracking and matches modern WoW, whose
|
||||
// LOSS_OF_CONTROL_UPDATE carries the unit token.
|
||||
if (changed)
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEventUpdate), "%s", "player");
|
||||
Event::Custom::Fire(_reserveUpdate.Slot(), "%s", "player");
|
||||
|
||||
for (int i = 0; i < n; ++i)
|
||||
g_prev[i] = cur[i];
|
||||
g_prevCount = n;
|
||||
}
|
||||
|
||||
static const Event::Custom::AutoReserve _reserveAdded{kEventAdded};
|
||||
static const Event::Custom::AutoReserve _reserveUpdate{kEventUpdate};
|
||||
static const Tick::WorldTick::AutoSubscribe _tick{&OnWorldTick};
|
||||
|
||||
// `C_LossOfControl.GetActiveLossOfControlDataCount()` -> number.
|
||||
|
||||
+11
-15
@@ -59,13 +59,9 @@ namespace NamePlate::Events {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *kEventCreated = "NAME_PLATE_CREATED";
|
||||
constexpr const char *kEventUnitAdded = "NAME_PLATE_UNIT_ADDED";
|
||||
constexpr const char *kEventUnitRemoved = "NAME_PLATE_UNIT_REMOVED";
|
||||
|
||||
const Event::Custom::AutoReserve _r1{kEventCreated};
|
||||
const Event::Custom::AutoReserve _r2{kEventUnitAdded};
|
||||
const Event::Custom::AutoReserve _r3{kEventUnitRemoved};
|
||||
const Event::Custom::AutoReserve _evtCreated{"NAME_PLATE_CREATED"};
|
||||
const Event::Custom::AutoReserve _evtUnitAdded{"NAME_PLATE_UNIT_ADDED"};
|
||||
const Event::Custom::AutoReserve _evtUnitRemoved{"NAME_PLATE_UNIT_REMOVED"};
|
||||
|
||||
// Previous tick's snapshot — GUID → nameplate-frame pointer for each
|
||||
// nameplated unit. Compared against the next tick's walk to compute
|
||||
@@ -112,14 +108,14 @@ int AssignSlot(uint64_t guid) {
|
||||
return static_cast<int>(g_slots.size() - 1);
|
||||
}
|
||||
|
||||
// Fire `eventName` with a pre-formatted string as `arg1`. The engine
|
||||
// Fire `event` with a pre-formatted string as `arg1`. The engine
|
||||
// dispatcher's `%s` format code pushes the C string into `_G.arg1`
|
||||
// as a Lua string — no escaping concerns for our own input
|
||||
// (`"nameplateN"`).
|
||||
void FireWithString(const char *eventName, const char *value) {
|
||||
void FireWithString(const Event::Custom::AutoReserve &event, const char *value) {
|
||||
if (value == nullptr)
|
||||
return;
|
||||
const int slot = Event::Custom::Lookup(eventName);
|
||||
const int slot = event.Slot();
|
||||
if (slot < 0)
|
||||
return;
|
||||
Event::Custom::Fire(slot, "%s", value);
|
||||
@@ -172,10 +168,10 @@ using LuaRefRef_t = int(__fastcall *)(void *L, int t);
|
||||
using LuaRefUnref_t = void(__fastcall *)(void *L, int t, int ref);
|
||||
using LuaRawGetI_t = void(__fastcall *)(void *L, int t, int n);
|
||||
|
||||
void FireWithFrame(const char *eventName, void *frame) {
|
||||
void FireWithFrame(const Event::Custom::AutoReserve &event, void *frame) {
|
||||
if (frame == nullptr)
|
||||
return;
|
||||
const int slot = Event::Custom::Lookup(eventName);
|
||||
const int slot = event.Slot();
|
||||
if (slot < 0)
|
||||
return;
|
||||
|
||||
@@ -229,7 +225,7 @@ void OnWorldTick() {
|
||||
// event handler.
|
||||
for (const auto &kv : g_currentTickPlates) {
|
||||
if (g_seenPlates.insert(kv.second).second)
|
||||
FireWithFrame(kEventCreated, const_cast<void *>(kv.second));
|
||||
FireWithFrame(_evtCreated, const_cast<void *>(kv.second));
|
||||
if (g_lastTickPlates.find(kv.first) == g_lastTickPlates.end()) {
|
||||
const int slot = AssignSlot(kv.first);
|
||||
// Watch this unit's fields so UNIT_HEALTH/UNIT_AURA/… fire with
|
||||
@@ -237,7 +233,7 @@ void OnWorldTick() {
|
||||
// target/party/raid tokens, not nameplates).
|
||||
Unit::TokenObserver::Register(kv.first, &NamePlateFieldCb);
|
||||
char tokenBuf[24];
|
||||
FireWithString(kEventUnitAdded,
|
||||
FireWithString(_evtUnitAdded,
|
||||
FormatNamePlateToken(tokenBuf, sizeof tokenBuf, slot + 1));
|
||||
}
|
||||
}
|
||||
@@ -254,7 +250,7 @@ void OnWorldTick() {
|
||||
continue;
|
||||
const int oneBased = static_cast<int>(it - g_slots.begin()) + 1;
|
||||
char tokenBuf[24];
|
||||
FireWithString(kEventUnitRemoved,
|
||||
FireWithString(_evtUnitRemoved,
|
||||
FormatNamePlateToken(tokenBuf, sizeof tokenBuf, oneBased));
|
||||
Unit::TokenObserver::Unregister(kv.first, &NamePlateFieldCb);
|
||||
*it = 0; // free the slot (no shift of survivors)
|
||||
|
||||
@@ -109,8 +109,7 @@ int __fastcall EquipSlotChanged_cb(uint32_t fieldOffset, uint32_t /*size*/,
|
||||
const int slot0 =
|
||||
static_cast<int>(fieldOffset - Offsets::OFF_DESC_PLAYER_EQUIP_FIRST) >> 3;
|
||||
if (slot0 >= 0 && slot0 < Offsets::DESC_PLAYER_EQUIP_SLOTS) {
|
||||
Event::Custom::FireIdSuccess(Event::Custom::Lookup(kEvtEquipmentChanged),
|
||||
slot0 + 1,
|
||||
Event::Custom::FireIdSuccess(_r.Slot(), slot0 + 1,
|
||||
SlotHasItem(guidLo, guidHi, slot0));
|
||||
// Gear swap changes item +healing and Spirit/Armor — invalidate the
|
||||
// GetSpellBonusHealing cache.
|
||||
@@ -225,7 +224,7 @@ void __fastcall AlertsRecompute_h() {
|
||||
return;
|
||||
}
|
||||
if (changed)
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEvtDurability), "");
|
||||
Event::Custom::Fire(_r2.Slot(), "");
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _alertsHook{
|
||||
|
||||
@@ -68,8 +68,7 @@ void __fastcall BindPointUpdateHandler_h(void *packetBuffer) {
|
||||
static_cast<uintptr_t>(Offsets::VAR_IN_WORLD)) == 0)
|
||||
return;
|
||||
|
||||
const int slot = Event::Custom::Lookup(kEventName);
|
||||
Event::Custom::Fire(slot, "");
|
||||
Event::Custom::Fire(_reserve.Slot(), "");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
+12
-18
@@ -43,12 +43,12 @@ namespace Player::InputEvents {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *kStartedMovingEvent = "PLAYER_STARTED_MOVING";
|
||||
constexpr const char *kStoppedMovingEvent = "PLAYER_STOPPED_MOVING";
|
||||
constexpr const char *kStartedLookingEvent = "PLAYER_STARTED_LOOKING";
|
||||
constexpr const char *kStoppedLookingEvent = "PLAYER_STOPPED_LOOKING";
|
||||
constexpr const char *kStartedTurningEvent = "PLAYER_STARTED_TURNING";
|
||||
constexpr const char *kStoppedTurningEvent = "PLAYER_STOPPED_TURNING";
|
||||
const Event::Custom::AutoReserve _reserveStartedMoving{"PLAYER_STARTED_MOVING"};
|
||||
const Event::Custom::AutoReserve _reserveStoppedMoving{"PLAYER_STOPPED_MOVING"};
|
||||
const Event::Custom::AutoReserve _reserveStartedLooking{"PLAYER_STARTED_LOOKING"};
|
||||
const Event::Custom::AutoReserve _reserveStoppedLooking{"PLAYER_STOPPED_LOOKING"};
|
||||
const Event::Custom::AutoReserve _reserveStartedTurning{"PLAYER_STARTED_TURNING"};
|
||||
const Event::Custom::AutoReserve _reserveStoppedTurning{"PLAYER_STOPPED_TURNING"};
|
||||
|
||||
// Below this per-frame delta, treat the yaw as stable. Picks up
|
||||
// real rotation (smallest user-perceptible RMB-drag is ~0.5°/frame
|
||||
@@ -85,12 +85,12 @@ const uint8_t *Camera() {
|
||||
gameState, Offsets::OFF_GAME_STATE_CAMERA_PTR);
|
||||
}
|
||||
|
||||
void FireTransition(bool now, bool &prev, const char *startedName,
|
||||
const char *stoppedName) {
|
||||
void FireTransition(bool now, bool &prev, const Event::Custom::AutoReserve &started,
|
||||
const Event::Custom::AutoReserve &stopped) {
|
||||
if (now == prev)
|
||||
return;
|
||||
prev = now;
|
||||
const int slot = Event::Custom::Lookup(now ? startedName : stoppedName);
|
||||
const int slot = (now ? started : stopped).Slot();
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "");
|
||||
}
|
||||
@@ -172,19 +172,13 @@ void OnWorldTick() {
|
||||
g_havePrevCameraYaw = true;
|
||||
}
|
||||
|
||||
FireTransition(moving, g_wasMoving, kStartedMovingEvent, kStoppedMovingEvent);
|
||||
FireTransition(turning, g_wasTurning, kStartedTurningEvent, kStoppedTurningEvent);
|
||||
FireTransition(looking, g_wasLooking, kStartedLookingEvent, kStoppedLookingEvent);
|
||||
FireTransition(moving, g_wasMoving, _reserveStartedMoving, _reserveStoppedMoving);
|
||||
FireTransition(turning, g_wasTurning, _reserveStartedTurning, _reserveStoppedTurning);
|
||||
FireTransition(looking, g_wasLooking, _reserveStartedLooking, _reserveStoppedLooking);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static const Event::Custom::AutoReserve _reserveStartedMoving{kStartedMovingEvent};
|
||||
static const Event::Custom::AutoReserve _reserveStoppedMoving{kStoppedMovingEvent};
|
||||
static const Event::Custom::AutoReserve _reserveStartedLooking{kStartedLookingEvent};
|
||||
static const Event::Custom::AutoReserve _reserveStoppedLooking{kStoppedLookingEvent};
|
||||
static const Event::Custom::AutoReserve _reserveStartedTurning{kStartedTurningEvent};
|
||||
static const Event::Custom::AutoReserve _reserveStoppedTurning{kStoppedTurningEvent};
|
||||
static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
|
||||
} // namespace Player::InputEvents
|
||||
|
||||
+3
-3
@@ -33,8 +33,8 @@ static const Event::Custom::AutoReserve _reserveQuestDataLoadResult{kQuestDataLo
|
||||
static void __stdcall QuestLoadCallback(void *userData, int success) {
|
||||
const auto questID =
|
||||
static_cast<int>(reinterpret_cast<uintptr_t>(userData));
|
||||
Event::Custom::FireIdSuccess(Event::Custom::Lookup(kQuestDataLoadResult),
|
||||
questID, success != 0);
|
||||
Event::Custom::FireIdSuccess(_reserveQuestDataLoadResult.Slot(), questID,
|
||||
success != 0);
|
||||
}
|
||||
|
||||
static void RequestAndMaybeNotify(uint32_t questID) {
|
||||
@@ -45,7 +45,7 @@ static void RequestAndMaybeNotify(uint32_t questID) {
|
||||
// invoking our callback. Fire the event ourselves so addons get the
|
||||
// notification regardless of cache state, matching modern semantics.
|
||||
if (wasCached) {
|
||||
Event::Custom::FireIdSuccess(Event::Custom::Lookup(kQuestDataLoadResult),
|
||||
Event::Custom::FireIdSuccess(_reserveQuestDataLoadResult.Slot(),
|
||||
static_cast<int>(questID), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ void __fastcall QuestLogRebuild_h(int param_1) {
|
||||
// (login or post-reload). Modern WoW's QUEST_ACCEPTED doesn't
|
||||
// fire on initial sync either; we match that.
|
||||
if (newCount == 1) {
|
||||
const int slot = Event::Custom::Lookup(kAcceptedEventName);
|
||||
const int slot = _reserveAccepted.Slot();
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "%d%d", firstNewIdx, firstNewID);
|
||||
}
|
||||
@@ -168,7 +168,7 @@ void __fastcall QuestLogRebuild_h(int param_1) {
|
||||
// all but one of the previous character's quests (1 removal, N
|
||||
// additions in the same rebuild).
|
||||
if (removedCount == 1 && newCount <= 1) {
|
||||
const int slot = Event::Custom::Lookup(kRemovedEventName);
|
||||
const int slot = _reserveRemoved.Slot();
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "%d", firstRemovedID);
|
||||
}
|
||||
|
||||
@@ -77,8 +77,7 @@ int __fastcall Handler_h(void *thisPlayer, void *edx, void *packetStream) {
|
||||
const int result = Handler_o(thisPlayer, edx, packetStream);
|
||||
|
||||
if (questID > 0) {
|
||||
const int slot = Event::Custom::Lookup(kEventName);
|
||||
Event::Custom::Fire(slot, "%u%u%u",
|
||||
Event::Custom::Fire(_reserve.Slot(), "%u%u%u",
|
||||
questID, xpReward, moneyReward);
|
||||
}
|
||||
|
||||
|
||||
+13
-9
@@ -514,14 +514,18 @@ void HandleSpellStart(uint64_t caster, int spellID, uint32_t castTime,
|
||||
now - g_cast.startMs < kCastStartDedupMs) {
|
||||
// Confirming packet. Keep startMs (no visual restart), but SNAP the
|
||||
// end time to the server's authoritative castTime. The client stamp
|
||||
// came from FUN_006e3340, which folds in the caster's cast-speed /
|
||||
// ranged-haste multiplier (descriptor +0x22c) — right for spells the
|
||||
// server also hastes (Aimed/Multi-Shot), WRONG for ones it doesn't.
|
||||
// Volley is the case that surfaced this: not channeled, not sped by
|
||||
// Quick Shots server-side, so the hasted prediction ended the bar
|
||||
// early and the player was "still casting after the bar finished."
|
||||
// The server's castTime is reality; reconcile to it (preserving any
|
||||
// pushback already accumulated). A no-op when client and server agree.
|
||||
// came from FUN_006e3340, whose only caster factor is the cast-speed
|
||||
// multiplier at descriptor +0x22c (UNIT_MOD_CAST_SPEED — spell
|
||||
// haste); the server additionally scales ranged ABILITIES by ranged
|
||||
// attack speed (SpellEntry::GetCastTime's IsRangedSpell branch,
|
||||
// m_modAttackSpeedPct[RANGED_ATTACK] — quiver/Quick Shots), which
|
||||
// the prediction can't see. Two real divergences so far: Volley
|
||||
// (client +0x22c hastes it, server doesn't → bar ended early) and
|
||||
// Steady/Aimed Shot (server ranged-hastes it, client doesn't → bar
|
||||
// ran long, pfUI#43). The server's castTime is reality; reconcile
|
||||
// to it (preserving accumulated pushback). A no-op when they agree;
|
||||
// when they differ, PollPlayer sees the end move and fires
|
||||
// UNIT_SPELLCAST_DELAYED so cast bars re-read the corrected times.
|
||||
g_cast.endMs =
|
||||
g_cast.startMs + static_cast<int>(castTime) + g_cast.delayMs;
|
||||
g_castTargetGuid = targetGuid; // confirming packet carries the target
|
||||
@@ -837,7 +841,7 @@ void OnWorldTick() {
|
||||
// based so it needs no changes to the many stamp/clear sites; ~1 frame
|
||||
// latency is imperceptible for a cast bar, and every fire is
|
||||
// listener-gated (near-free when no addon uses these).
|
||||
Spell::CastEvents::PollPlayer(g_cast.spellID, g_cast.startMs, g_cast.delayMs,
|
||||
Spell::CastEvents::PollPlayer(g_cast.spellID, g_cast.startMs, g_cast.endMs,
|
||||
g_channel.spellID, g_channel.startMs);
|
||||
Spell::CastEvents::PollRemote();
|
||||
Spell::CastEvents::PollReticle();
|
||||
|
||||
+35
-41
@@ -31,33 +31,20 @@ namespace Spell::CastEvents {
|
||||
namespace {
|
||||
|
||||
|
||||
constexpr const char *kStart = "UNIT_SPELLCAST_START";
|
||||
constexpr const char *kStop = "UNIT_SPELLCAST_STOP";
|
||||
constexpr const char *kDelayed = "UNIT_SPELLCAST_DELAYED";
|
||||
constexpr const char *kChannelStart = "UNIT_SPELLCAST_CHANNEL_START";
|
||||
constexpr const char *kChannelStop = "UNIT_SPELLCAST_CHANNEL_STOP";
|
||||
constexpr const char *kChannelUpdate = "UNIT_SPELLCAST_CHANNEL_UPDATE";
|
||||
constexpr const char *kSucceeded = "UNIT_SPELLCAST_SUCCEEDED";
|
||||
constexpr const char *kInterrupted = "UNIT_SPELLCAST_INTERRUPTED";
|
||||
constexpr const char *kFailed = "UNIT_SPELLCAST_FAILED";
|
||||
constexpr const char *kFailedQuiet = "UNIT_SPELLCAST_FAILED_QUIET";
|
||||
constexpr const char *kSent = "UNIT_SPELLCAST_SENT";
|
||||
constexpr const char *kReticleTarget = "UNIT_SPELLCAST_RETICLE_TARGET";
|
||||
constexpr const char *kReticleClear = "UNIT_SPELLCAST_RETICLE_CLEAR";
|
||||
|
||||
const Event::Custom::AutoReserve _rStart{kStart};
|
||||
const Event::Custom::AutoReserve _rStop{kStop};
|
||||
const Event::Custom::AutoReserve _rDelayed{kDelayed};
|
||||
const Event::Custom::AutoReserve _rChannelStart{kChannelStart};
|
||||
const Event::Custom::AutoReserve _rChannelStop{kChannelStop};
|
||||
const Event::Custom::AutoReserve _rChannelUpdate{kChannelUpdate};
|
||||
const Event::Custom::AutoReserve _rSucceeded{kSucceeded};
|
||||
const Event::Custom::AutoReserve _rInterrupted{kInterrupted};
|
||||
const Event::Custom::AutoReserve _rFailed{kFailed};
|
||||
const Event::Custom::AutoReserve _rFailedQuiet{kFailedQuiet};
|
||||
const Event::Custom::AutoReserve _rSent{kSent};
|
||||
const Event::Custom::AutoReserve _rReticleTarget{kReticleTarget};
|
||||
const Event::Custom::AutoReserve _rReticleClear{kReticleClear};
|
||||
// The reservations double as the fire handles (`k*.Slot()` — O(1)).
|
||||
const Event::Custom::AutoReserve kStart{"UNIT_SPELLCAST_START"};
|
||||
const Event::Custom::AutoReserve kStop{"UNIT_SPELLCAST_STOP"};
|
||||
const Event::Custom::AutoReserve kDelayed{"UNIT_SPELLCAST_DELAYED"};
|
||||
const Event::Custom::AutoReserve kChannelStart{"UNIT_SPELLCAST_CHANNEL_START"};
|
||||
const Event::Custom::AutoReserve kChannelStop{"UNIT_SPELLCAST_CHANNEL_STOP"};
|
||||
const Event::Custom::AutoReserve kChannelUpdate{"UNIT_SPELLCAST_CHANNEL_UPDATE"};
|
||||
const Event::Custom::AutoReserve kSucceeded{"UNIT_SPELLCAST_SUCCEEDED"};
|
||||
const Event::Custom::AutoReserve kInterrupted{"UNIT_SPELLCAST_INTERRUPTED"};
|
||||
const Event::Custom::AutoReserve kFailed{"UNIT_SPELLCAST_FAILED"};
|
||||
const Event::Custom::AutoReserve kFailedQuiet{"UNIT_SPELLCAST_FAILED_QUIET"};
|
||||
const Event::Custom::AutoReserve kSent{"UNIT_SPELLCAST_SENT"};
|
||||
const Event::Custom::AutoReserve kReticleTarget{"UNIT_SPELLCAST_RETICLE_TARGET"};
|
||||
const Event::Custom::AutoReserve kReticleClear{"UNIT_SPELLCAST_RETICLE_CLEAR"};
|
||||
|
||||
// Autoshot spams client-side failures while ramping — nampower filters it
|
||||
// out of its failure event, so do we.
|
||||
@@ -108,8 +95,9 @@ void BuildCastGuid(char *out, size_t n, int type, int spellID, int castUID) {
|
||||
// rank)`. Gated on a listener so an unwatched event does no DBC lookups or
|
||||
// string formatting. A null name/rank pushes `nil` through the dispatcher's
|
||||
// `lua_pushstring(NULL) → pushnil` tail-jump, which is fine.
|
||||
void Fire(const char *eventName, int spellID, int guidNum, int type = 3) {
|
||||
const int slot = Event::Custom::Lookup(eventName);
|
||||
void Fire(const Event::Custom::AutoReserve &event, int spellID, int guidNum,
|
||||
int type = 3) {
|
||||
const int slot = event.Slot();
|
||||
if (!Event::Custom::HasListeners(slot))
|
||||
return;
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
@@ -127,7 +115,7 @@ void Fire(const char *eventName, int spellID, int guidNum, int type = 3) {
|
||||
// inserted at arg2. `target` is a unit token (e.g. "target") resolved from
|
||||
// the cast packet, or "" for self / no-target casts.
|
||||
void FireSent(int spellID, int guidNum, const char *target) {
|
||||
const int slot = Event::Custom::Lookup(kSent);
|
||||
const int slot = kSent.Slot();
|
||||
if (!Event::Custom::HasListeners(slot))
|
||||
return;
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
@@ -148,8 +136,8 @@ void FireSent(int spellID, int guidNum, const char *target) {
|
||||
// the load-bearing fields — are exact; only arg2 differs (`""` vs `nil`),
|
||||
// which is inconsequential for a reticle (no cast to identify). spellName /
|
||||
// rank are our usual extension tail.
|
||||
void FireReticle(const char *eventName, int spellID) {
|
||||
const int slot = Event::Custom::Lookup(eventName);
|
||||
void FireReticle(const Event::Custom::AutoReserve &event, int spellID) {
|
||||
const int slot = event.Slot();
|
||||
if (!Event::Custom::HasListeners(slot))
|
||||
return;
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
@@ -176,7 +164,7 @@ int g_guidCounter = 0;
|
||||
int g_castSpell = 0; // 0 = not casting
|
||||
int g_castStart = 0; // startMs of the tracked cast (detects same-spell recast)
|
||||
int g_castGuid = 0; // castGUID counter value for the tracked cast
|
||||
int g_castDelay = 0; // last-seen accumulated pushback (ms)
|
||||
int g_castEnd = 0; // last-seen endMs (any movement fires DELAYED)
|
||||
bool g_castSucceeded = false; // SPELL_GO seen for the tracked cast this cast
|
||||
|
||||
// UNIT_SPELLCAST_INTERRUPTED de-dup. SpellFailed_h and PollPlayer can both
|
||||
@@ -382,9 +370,9 @@ const Net::SendObserver::AutoSubscribe _sendSub{&OnSend};
|
||||
// the payload is fanned out per token (`target` / `focus` / `nameplateN` /
|
||||
// `party` / `raid` / `mouseover`). Same `(unit, castGUID, spellID, spellName,
|
||||
// rank)` shape as the player events, just with a non-"player" unit.
|
||||
void FireRemote(const char *eventName, uint64_t casterGuid, int spellID,
|
||||
int guidNum) {
|
||||
const int slot = Event::Custom::Lookup(eventName);
|
||||
void FireRemote(const Event::Custom::AutoReserve &event, uint64_t casterGuid,
|
||||
int spellID, int guidNum) {
|
||||
const int slot = event.Slot();
|
||||
if (!Event::Custom::HasListeners(slot))
|
||||
return;
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
@@ -441,7 +429,7 @@ RemoteEvt *AllocRemoteEvt(uint64_t guid) {
|
||||
|
||||
bool PlayerCastSucceeded() { return g_castSucceeded; }
|
||||
|
||||
void PollPlayer(int castSpellID, int castStartMs, int castDelayMs,
|
||||
void PollPlayer(int castSpellID, int castStartMs, int castEndMs,
|
||||
int channelSpellID, int channelStartMs) {
|
||||
// ---- Regular cast -------------------------------------------------
|
||||
// A "new cast" is a different spell OR the same spell re-stamped at a
|
||||
@@ -474,12 +462,18 @@ void PollPlayer(int castSpellID, int castStartMs, int castDelayMs,
|
||||
g_castGuid = NextCastGuid(castSpellID);
|
||||
g_castSpell = castSpellID;
|
||||
g_castStart = castStartMs;
|
||||
g_castDelay = castDelayMs;
|
||||
g_castEnd = castEndMs;
|
||||
g_castSucceeded = false;
|
||||
Fire(kStart, castSpellID, g_castGuid);
|
||||
} else if (g_castSpell != 0 && castDelayMs > g_castDelay) {
|
||||
// Pushback grew (SMSG_SPELL_DELAYED extended the cast).
|
||||
g_castDelay = castDelayMs;
|
||||
} else if (g_castSpell != 0 && castEndMs != g_castEnd) {
|
||||
// The cast's end moved: pushback (SMSG_SPELL_DELAYED extends it) or
|
||||
// the confirming SMSG_SPELL_START snapping the client-predicted cast
|
||||
// time to the server's authoritative one — the server scales ranged
|
||||
// abilities by ranged attack speed (Steady/Aimed Shot), which the
|
||||
// client's prediction helper doesn't model, so the confirming packet
|
||||
// shortens the bar ~RTT after START (pfUI#43). Either direction, the
|
||||
// addon must re-read the times; DELAYED is the re-read trigger.
|
||||
g_castEnd = castEndMs;
|
||||
Fire(kDelayed, g_castSpell, g_castGuid);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,11 +45,15 @@ namespace Spell::CastEvents {
|
||||
// after its own upkeep, passing the live `g_cast` / `g_channel` fields.
|
||||
// Detects, against the previous snapshot:
|
||||
// - regular cast start / stop / same-spell recast → UNIT_SPELLCAST_START / _STOP
|
||||
// - accumulated pushback growth → UNIT_SPELLCAST_DELAYED
|
||||
// - end-time movement (either direction) → UNIT_SPELLCAST_DELAYED
|
||||
// - channel start / stop → UNIT_SPELLCAST_CHANNEL_START / _STOP
|
||||
// End-time movement covers both pushback (SMSG_SPELL_DELAYED) and the
|
||||
// confirming SMSG_SPELL_START reconciling the client-predicted cast time to
|
||||
// the server's (ranged-haste-scaled Steady/Aimed Shot, pfUI#43) — DELAYED is
|
||||
// the "re-read the times" trigger, whichever way the end moved.
|
||||
// Channels never fire INTERRUPTED — retail emits only CHANNEL_STOP for a
|
||||
// channel whether it ends naturally or is cut short.
|
||||
void PollPlayer(int castSpellID, int castStartMs, int castDelayMs,
|
||||
void PollPlayer(int castSpellID, int castStartMs, int castEndMs,
|
||||
int channelSpellID, int channelStartMs);
|
||||
|
||||
// Derive the ground-target reticle events from the engine's targeting flags.
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
// 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 "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "spell/Lookup.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// `GetNumTrackingTypes()` / `GetTrackingInfo(index)` / `SetTracking(index)` —
|
||||
// backport of the minimap-tracking enumeration the modern client exposes.
|
||||
//
|
||||
// Vanilla 1.12 never enumerates the tracking abilities the player KNOWS — it
|
||||
// only stores the single currently-active tracker as a plain spellID in
|
||||
// `VAR_ACTIVE_TRACKING_SPELL`, surfaced through `GetTrackingTexture()` /
|
||||
// `CancelTrackingBuff()` / `GameTooltip:SetTrackingSpell()`. 3.3.5's
|
||||
// `GetTrackingInfo` iterates a spellbook-derived list of tracking spells and
|
||||
// reports name/texture/active/category per entry; we rebuild that list here.
|
||||
//
|
||||
// A tracking spell is identified exactly the way the engine's own active-
|
||||
// tracker updater `FUN_004E4170` identifies one: a Spell.dbc record with an
|
||||
// `EffectApplyAuraName` of Track Creatures (44), Track Resources (45), or
|
||||
// Track Stealthed (151). That effect-based test reproduces pfUI's hardcoded
|
||||
// spellID/icon table (Find Herbs/Minerals/Treasure/Trees, every Hunter
|
||||
// Track*, Sense Undead/Demons, Track Humanoids) with no table to maintain,
|
||||
// and auto-covers server-custom trackers such as Turtle's Find Trees (52917).
|
||||
namespace Spell::Tracking {
|
||||
|
||||
// Tracking aura effect types — SPELL_AURA_TRACK_* ApplyAura names, the same
|
||||
// values `FUN_004E4170` scans a spell's three effects for.
|
||||
static constexpr int32_t AURA_TRACK_CREATURES = 44;
|
||||
static constexpr int32_t AURA_TRACK_RESOURCES = 45;
|
||||
static constexpr int32_t AURA_TRACK_STEALTHED = 151;
|
||||
|
||||
// True iff `spellID`'s record has any of the three Track* effects.
|
||||
static bool IsTrackingSpell(int spellID) {
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
auto *aura = Game::Ptr<const int32_t>(
|
||||
record, Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
if (aura[i] == AURA_TRACK_CREATURES || aura[i] == AURA_TRACK_RESOURCES ||
|
||||
aura[i] == AURA_TRACK_STEALTHED)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Walks the player spellbook in slot order counting tracking spells. If
|
||||
// `wantNth >= 1` and an Nth tracking spell exists, writes its spellID and
|
||||
// 1-based spellbook slot to `*outSpellID` / `*outSlot`. Returns the total
|
||||
// number of tracking spells found (so callers detect out-of-range by
|
||||
// comparing the return against `wantNth`).
|
||||
//
|
||||
// The spellbook array is zero-padded past the populated count (see
|
||||
// `Spell::Lookup::FindSpellbookSlot`), so a 0 entry is just an empty slot —
|
||||
// skip it and keep scanning the full range rather than break, matching the
|
||||
// existing spellbook walkers.
|
||||
static int EnumerateTracking(int wantNth, int *outSpellID, int *outSlot) {
|
||||
auto *book = reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_SPELLBOOK));
|
||||
int found = 0;
|
||||
for (int i = 0; i < Offsets::SPELLBOOK_MAX_SLOTS; ++i) {
|
||||
const int spellID = book[i];
|
||||
if (spellID <= 0 || !IsTrackingSpell(spellID))
|
||||
continue;
|
||||
++found;
|
||||
if (found == wantNth) {
|
||||
if (outSpellID != nullptr)
|
||||
*outSpellID = spellID;
|
||||
if (outSlot != nullptr)
|
||||
*outSlot = i + 1;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
// `GetNumTrackingTypes()` -> number of tracking spells in the player's book.
|
||||
static int __fastcall Script_GetNumTrackingTypes(void *L) {
|
||||
Game::Lua::PushNumber(L,
|
||||
static_cast<double>(EnumerateTracking(0, nullptr, nullptr)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `GetTrackingInfo(index)` -> name, texture, active, category, spellID.
|
||||
// - index is 1-based into the tracking list (spellbook order).
|
||||
// - texture is the SpellIcon.dbc path (nil if the icon is missing).
|
||||
// - active is a boolean: the tracker currently in effect.
|
||||
// - category is always "spell" — vanilla has no non-spell ("other")
|
||||
// trackers, unlike 3.3.5's class-masked table.
|
||||
// - spellID is a ClassicAPI extension (the modern 5th return is `nested`,
|
||||
// which has no meaning here); handy since selecting/among trackers is
|
||||
// spellID-driven.
|
||||
// Returns nil for an out-of-range index.
|
||||
static int __fastcall Script_GetTrackingInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetTrackingInfo(index)");
|
||||
return 0;
|
||||
}
|
||||
const int index = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (index < 1)
|
||||
return 0;
|
||||
|
||||
int spellID = 0;
|
||||
int slot = 0;
|
||||
if (EnumerateTracking(index, &spellID, &slot) < index)
|
||||
return 0; // index past the number of tracking types -> nil
|
||||
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
const int locale = Game::Read<int>(static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
const char *name =
|
||||
Game::Read<const char *>(record, Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
|
||||
const char *texture = nullptr;
|
||||
const int iconID = Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_ICON_ID);
|
||||
if (auto *iconRec = DBC::Record(Offsets::VAR_SPELL_ICON_RECORDS,
|
||||
Offsets::VAR_SPELL_ICON_COUNT,
|
||||
static_cast<uint32_t>(iconID))) {
|
||||
texture = Game::Read<const char *>(iconRec, Offsets::OFF_SPELLICON_PATH);
|
||||
}
|
||||
|
||||
const bool active =
|
||||
Game::Read<int>(static_cast<uintptr_t>(Offsets::VAR_ACTIVE_TRACKING_SPELL)) ==
|
||||
spellID;
|
||||
|
||||
Game::Lua::PushString(L, name); // 1. name (PushString handles NULL -> nil)
|
||||
Game::Lua::PushString(L, texture); // 2. texture (icon path) or nil
|
||||
Game::Lua::PushBool(L, active); // 3. active
|
||||
Game::Lua::PushString(L, "spell"); // 4. category
|
||||
Game::Lua::PushNumber(L, static_cast<double>(spellID)); // 5. spellID (extension)
|
||||
return 5;
|
||||
}
|
||||
|
||||
// `SetTracking(index)` — selects (activates) the tracking spell at `index`.
|
||||
// Vanilla has no "set tracking" primitive: choosing a tracker IS casting its
|
||||
// spell (the tracking dropdown does exactly this). So we resolve the entry's
|
||||
// spellbook slot and tail-call the engine's own `Script_CastSpell(slot,
|
||||
// "spell")` — reusing its slot bounds-check, target resolution, and error
|
||||
// handling rather than reconstructing the cast path. Returns nil for an
|
||||
// out-of-range index.
|
||||
static int __fastcall Script_SetTracking(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: SetTracking(index)");
|
||||
return 0;
|
||||
}
|
||||
const int index = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (index < 1)
|
||||
return 0;
|
||||
|
||||
int spellID = 0;
|
||||
int slot = 0;
|
||||
if (EnumerateTracking(index, &spellID, &slot) < index)
|
||||
return 0;
|
||||
|
||||
// Rebuild the stack as CastSpell(slot, "spell") and hand off to the
|
||||
// engine handler. `slot` is 1-based, matching what a Lua `CastSpell`
|
||||
// caller passes.
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slot));
|
||||
Game::Lua::PushString(L, "spell");
|
||||
using CastSpell_t = int(__fastcall *)(void *);
|
||||
auto castSpell = reinterpret_cast<CastSpell_t>(Offsets::FUN_SCRIPT_CAST_SPELL);
|
||||
return castSpell(L);
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetNumTrackingTypes",
|
||||
&Script_GetNumTrackingTypes);
|
||||
Game::Lua::RegisterGlobalFunction("GetTrackingInfo", &Script_GetTrackingInfo);
|
||||
Game::Lua::RegisterGlobalFunction("SetTracking", &Script_SetTracking);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Spell::Tracking
|
||||
@@ -0,0 +1,133 @@
|
||||
// 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/>.
|
||||
|
||||
// Turtle WoW "Stinging Nettle" hunter talent (talent 403; rank passives
|
||||
// 51579 / 51580): Mongoose Bite and triggered fire traps apply the hunter's
|
||||
// highest known Serpent Sting rank at 20% (R1) / 40% (R2) of its duration.
|
||||
//
|
||||
// Server-side (tortoise-wow spell_hunter.cpp ApplyStingingNettle) the sting
|
||||
// is a direct `target->AddAura(...)` — no cast, no SMSG_SPELL_GO for the
|
||||
// sting itself — so the client sees only the descriptor application and
|
||||
// OnAuraAdded would seat it caster-less at the full 15s base. The TRIGGERS
|
||||
// are visible, though: Mongoose Bite is a real player cast, and the
|
||||
// fire-trap effect spells are cast by the trap's OWNER (GameObject::Use
|
||||
// TYPE_TRAP → pOwner->CastSpell) with non-zero SpellVisual, so they pass
|
||||
// IsNeedSendToClient and their SMSG_SPELL_GO broadcasts with the hunter as
|
||||
// caster and the victim in the hit list. Registered as Aura::Source
|
||||
// triggered-application rules: the trigger arms its hit targets, and the
|
||||
// caster-less Serpent Sting application that follows consumes the arm
|
||||
// (player attribution + the server's max(1, base × pct / 100)).
|
||||
//
|
||||
// The rank gates are the talent-granted passives, read live from the
|
||||
// spell-knowledge bitmap at trigger time (respec-safe); R2 registers first
|
||||
// so its 40% wins while known. The percents come from the rank spells' own
|
||||
// EffectBasePoints[0] + 1 — the same DBC value the server's
|
||||
// CalculateSimpleValue reads (19 → 20%, 39 → 40%), with the server's own
|
||||
// 20/40 fallback — so a Turtle rebalance that patches the client DBC tracks
|
||||
// automatically.
|
||||
//
|
||||
// Server guards that mirror for free: no talent → no arm (gate fails, and
|
||||
// the server applies nothing); an existing LONGER sting → the server skips
|
||||
// AddAura entirely → no application event → the arm just expires. A nettle
|
||||
// re-apply over a SHORTER existing sting reuses the descriptor slot (no
|
||||
// OnAuraAdded, verified in-game with chained Mongoose Bites), so the arm
|
||||
// mechanism additionally refreshes the existing cached entry at trigger
|
||||
// time, mirroring the server's keep-the-longer rule — see
|
||||
// RefreshTriggeredExisting in aura/Source.cpp. Other hunters' nettle stings
|
||||
// stay at base duration (their talents aren't client-knowable).
|
||||
//
|
||||
// Gated on Turtle::Detected() via a WorldTick latch, like DurationMods —
|
||||
// the trigger spells exist on stock 1.12, but the mechanic is Turtle's.
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "aura/Source.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "turtle/Detect.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Turtle::StingingNettle {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kHunter = 9; // SPELLFAMILY_HUNTER
|
||||
// Serpent Sting's SpellFamilyFlags bit — all player ranks carry it (1978,
|
||||
// 13549..13555, 25295, and Turtle's added Rank 9, 33459; client DBC
|
||||
// verified).
|
||||
constexpr uint64_t kSerpentStingFlag = 0x4000;
|
||||
|
||||
constexpr uint32_t kNettleR1 = 51579; // 20%
|
||||
constexpr uint32_t kNettleR2 = 51580; // 40%
|
||||
|
||||
// The rank's duration percent: its record's EffectBasePoints[0] + 1 (the
|
||||
// server's CalculateSimpleValue), else the server's hardcoded fallback.
|
||||
int32_t NettlePct(uint32_t rankSpellId, int32_t fallbackPct) {
|
||||
const uint8_t *rec =
|
||||
Spell::Lookup::RecordForID(static_cast<int>(rankSpellId));
|
||||
if (rec == nullptr)
|
||||
return fallbackPct;
|
||||
const int32_t pct =
|
||||
*reinterpret_cast<const int32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_BASE_POINTS) +
|
||||
1;
|
||||
return pct > 0 ? pct : fallbackPct;
|
||||
}
|
||||
|
||||
// Fire-trap effects' SpellFamilyFlags bit. Verified exhaustively against the
|
||||
// client Spell.dbc: bit 0x4 in the hunter family selects EXACTLY the eight
|
||||
// spells Turtle's script binding lists (Immolation Trap Effect r1-5,
|
||||
// Explosive Trap Effect r1-3) and nothing else — so the mask form is
|
||||
// rank-proof, covering any future trap rank for free. (Freezing Trap is the
|
||||
// distinct bit 0x8.)
|
||||
constexpr uint64_t kFireTrapEffectFlag = 0x4;
|
||||
|
||||
// Mongoose Bite ranks must stay EXACT IDs: their family bit (0x2) is shared
|
||||
// with all eight Raptor Strike ranks — verified against the DBC — and Raptor
|
||||
// Strike does NOT trigger nettle server-side. Mask-matching it would keep a
|
||||
// melee-weaving hunter's target permanently armed.
|
||||
constexpr uint32_t kMongooseBite[] = {1495, 14269, 14270, 14271};
|
||||
|
||||
bool g_registered = false;
|
||||
|
||||
void RegisterAll() {
|
||||
const int32_t pctR2 = NettlePct(kNettleR2, 40);
|
||||
const int32_t pctR1 = NettlePct(kNettleR1, 20);
|
||||
// Higher rank first — rules matching the same trigger arm in
|
||||
// registration order, first live gate wins.
|
||||
Aura::Source::AddTriggeredApplicationByFamily(
|
||||
kHunter, kFireTrapEffectFlag, kNettleR2, kHunter, kSerpentStingFlag,
|
||||
pctR2);
|
||||
Aura::Source::AddTriggeredApplicationByFamily(
|
||||
kHunter, kFireTrapEffectFlag, kNettleR1, kHunter, kSerpentStingFlag,
|
||||
pctR1);
|
||||
for (uint32_t id : kMongooseBite) {
|
||||
Aura::Source::AddTriggeredApplication(id, kNettleR2, kHunter,
|
||||
kSerpentStingFlag, pctR2);
|
||||
Aura::Source::AddTriggeredApplication(id, kNettleR1, kHunter,
|
||||
kSerpentStingFlag, pctR1);
|
||||
}
|
||||
}
|
||||
|
||||
void OnTick() {
|
||||
if (g_registered || !Turtle::Detected())
|
||||
return;
|
||||
RegisterAll();
|
||||
g_registered = true;
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tickSub{&OnTick};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Turtle::StingingNettle
|
||||
+1
-1
@@ -284,7 +284,7 @@ void Set(uint64_t guid) {
|
||||
TokenObserver::Register(guid, &FocusFieldCb);
|
||||
g_focusObserved = true;
|
||||
}
|
||||
const int slot = Event::Custom::Lookup(kEventName);
|
||||
const int slot = _reserve.Slot();
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "");
|
||||
}
|
||||
|
||||
@@ -173,8 +173,7 @@ void __fastcall BonusActionBarUpdate_h() {
|
||||
if (current < 0 || current == s_lastForm)
|
||||
return;
|
||||
s_lastForm = current;
|
||||
const int slot = Event::Custom::Lookup(kEventName);
|
||||
Event::Custom::Fire(slot, "");
|
||||
Event::Custom::Fire(_reserve.Slot(), "");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
+21
-4
@@ -108,6 +108,19 @@ constexpr const char *kEvtPlaybackStarted = "VOICE_CHAT_TTS_PLAYBACK_STARTED";
|
||||
constexpr const char *kEvtSpeakTextUpdate = "VOICE_CHAT_TTS_SPEAK_TEXT_UPDATE";
|
||||
constexpr const char *kEvtVoicesUpdate = "VOICE_CHAT_TTS_VOICES_UPDATE";
|
||||
|
||||
// The lazily-created reservations, set by RegisterLuaFunctions. Null while
|
||||
// standing down for VanillaTTS — but the fire helpers only run from our own
|
||||
// registered surface, so they never see null in that mode; SlotOf's guard is
|
||||
// belt-and-suspenders (`Fire(-1)` no-ops).
|
||||
const Event::Custom::AutoReserve *g_evtPlaybackFailed = nullptr;
|
||||
const Event::Custom::AutoReserve *g_evtPlaybackFinished = nullptr;
|
||||
const Event::Custom::AutoReserve *g_evtPlaybackStarted = nullptr;
|
||||
const Event::Custom::AutoReserve *g_evtVoicesUpdate = nullptr;
|
||||
|
||||
int SlotOf(const Event::Custom::AutoReserve *r) {
|
||||
return r != nullptr ? r->Slot() : -1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CVar names + handles (registered in RegisterLuaFunctions)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -186,18 +199,18 @@ std::wstring Utf8ToWide(const char *s) {
|
||||
// Event fire helpers (engine dispatcher via Event::Custom)
|
||||
// ---------------------------------------------------------------------------
|
||||
void FireVoicesUpdate() {
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEvtVoicesUpdate), "");
|
||||
Event::Custom::Fire(SlotOf(g_evtVoicesUpdate), "");
|
||||
}
|
||||
void FirePlaybackStarted(int numConsumers, int utteranceID, int durationMS, int dest) {
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEvtPlaybackStarted), "%d%d%d%d",
|
||||
Event::Custom::Fire(SlotOf(g_evtPlaybackStarted), "%d%d%d%d",
|
||||
numConsumers, utteranceID, durationMS, dest);
|
||||
}
|
||||
void FirePlaybackFinished(int numConsumers, int utteranceID, int dest) {
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEvtPlaybackFinished), "%d%d%d",
|
||||
Event::Custom::Fire(SlotOf(g_evtPlaybackFinished), "%d%d%d",
|
||||
numConsumers, utteranceID, dest);
|
||||
}
|
||||
void FirePlaybackFailed(const char *status, int utteranceID, int dest) {
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEvtPlaybackFailed), "%s%d%d",
|
||||
Event::Custom::Fire(SlotOf(g_evtPlaybackFailed), "%s%d%d",
|
||||
status, utteranceID, dest);
|
||||
}
|
||||
|
||||
@@ -565,6 +578,10 @@ void RegisterLuaFunctions() {
|
||||
static const Event::Custom::AutoReserve _r3{kEvtPlaybackStarted};
|
||||
static const Event::Custom::AutoReserve _r4{kEvtSpeakTextUpdate};
|
||||
static const Event::Custom::AutoReserve _r5{kEvtVoicesUpdate};
|
||||
g_evtPlaybackFailed = &_r1;
|
||||
g_evtPlaybackFinished = &_r2;
|
||||
g_evtPlaybackStarted = &_r3;
|
||||
g_evtVoicesUpdate = &_r5;
|
||||
|
||||
g_cvarVoice = CVar::Factory::Register(kCVarVoice, "0", 0, &OnVoiceChanged);
|
||||
g_cvarSpeed = CVar::Factory::Register(kCVarSpeed, "0", 0, &OnSpeedChanged);
|
||||
|
||||
Reference in New Issue
Block a user