Skip to content

Commit 23cdbe1

Browse files
committed
spell: CastAtCursor/CastAtUnit cast exact rank by spellID, accept spell name
1 parent 8b6760c commit 23cdbe1

4 files changed

Lines changed: 97 additions & 49 deletions

File tree

docs/API.md

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,8 @@ build instructions.
343343
- [`C_Spell.IsSpellHarmful(spellID)` / `C_Spell.IsSpellHelpful(spellID)`](#c_spellisspellharmfulspellid--c_spellisspellhelpfulspellid)
344344
- [`GetSpellSchool(spellID)`](#getspellschoolspellid)
345345
- [`CastSpellNoToggle(name | spellID)`](#castspellnotogglename--spellid)
346-
- [`C_Spell.CastAtCursor(spellID)`](#c_spellcastatcursorspellid)
347-
- [`C_Spell.CastAtUnit(spellID, unit)`](#c_spellcastatunitspellid-unit)
346+
- [`C_Spell.CastAtCursor(spellIDOrName)`](#c_spellcastatcursorspellidorname)
347+
- [`C_Spell.CastAtUnit(spellIDOrName, unit)`](#c_spellcastatunitspellidorname-unit)
348348
- [`C_Spell.CancelSpellByID(spellID)` / `CancelSpellByName(name)`](#c_spellcancelspellbyidspellid--cancelspellbynamename)
349349
- [`C_Spell.UnitCastingInfo(unit)` / `C_Spell.CastingInfo()`](#c_spellunitcastinginfounit--c_spellcastinginfo)
350350
- [`C_Spell.UnitChannelInfo(unit)` / `C_Spell.ChannelInfo()`](#c_spellunitchannelinfounit--c_spellchannelinfo)
@@ -5507,7 +5507,7 @@ C_Item.UseAtCursor("Iron Grenade")
55075507

55085508
Implementation chains the existing item-use path
55095509
(`Item::Location::FindByArgInBags` + `FUN_ITEM_USE`) with
5510-
[`Spell::AtCursor::Resolve`](#c_spellcastatcursorspellid) — same
5510+
[`Spell::AtCursor::Resolve`](#c_spellcastatcursorspellidorname) — same
55115511
cursor-resolution helper `C_Spell.CastAtCursor` uses. When the item
55125512
fires a non-ground-target spell, the cursor leg no-ops and returns
55135513
`false`; the item still uses normally (any implicit target — current
@@ -5535,7 +5535,7 @@ C_Item.UseAtUnit("Iron Grenade", "player")
55355535
Same chain as `UseAtCursor` (`Item::Location::FindByArgInBags` +
55365536
`FUN_ITEM_USE`) but committing the placement at the unit's world
55375537
position via
5538-
[`Spell::AtCursor::CommitAtCoords`](#c_spellcastatunitspellid-unit)
5538+
[`Spell::AtCursor::CommitAtCoords`](#c_spellcastatunitspellidorname-unit)
55395539
instead of the cursor raycast. Returns `true` when the placement
55405540
landed at the unit; `false` for non-ground-target items (the item
55415541
still fires with any implicit target), unparseable input,
@@ -8474,28 +8474,37 @@ Using this from inside a macro action slot? See
84748474
[`CastSpellNoToggle` as a macro cast line](#castspellnotoggle-as-a-macro-cast-line) below for the additional
84758475
parser support that makes the slot tag correctly for action-bar UIs.
84768476

8477-
### `C_Spell.CastAtCursor(spellID)`
8477+
### `C_Spell.CastAtCursor(spellIDOrName)`
84788478

84798479
Casts a ground-target spell at the player's current cursor world
84808480
position, bypassing the manual click on the AoE reticle the engine
84818481
would otherwise require. ClassicAPI's analog of modern's
84828482
`/cast [@cursor] Blizzard`. Returns `true` when the cursor-placement
84838483
leg landed; `false` for non-ground-target spells (cast still fires
8484-
normally on the current target), unknown spellIDs, cursor over UI /
8484+
normally on the current target), unknown spells, cursor over UI /
84858485
off-screen, etc.
84868486

8487+
Accepts a **spellID** or a **spell name**:
8488+
84878489
```lua
8488-
C_Spell.CastAtCursor(10) -- Blizzard rank 1 at cursor
8489-
C_Spell.CastAtCursor(2120) -- Flamestrike rank 1 at cursor
8490+
C_Spell.CastAtCursor(10) -- Blizzard Rank 1 (exact spellID)
8491+
C_Spell.CastAtCursor(2120) -- Flamestrike Rank 1
8492+
C_Spell.CastAtCursor("Blizzard") -- highest known rank
8493+
C_Spell.CastAtCursor("Blizzard(Rank 6)") -- that specific rank
84908494
```
84918495

8496+
A **numeric** spellID casts that *exact* spell — every rank is its own
8497+
spellID, so `10` casts Blizzard Rank 1 (not the highest known rank). A
8498+
**name** goes through the engine's own resolver, which parses a trailing
8499+
`(Rank N)`; a bare name casts the highest rank you know.
8500+
84928501
Two-stage internally:
84938502

8494-
1. Initiates the cast through the same path
8495-
[`CastSpellByName`](docs/API.md#numeric-spellids-in-cast-and-castspellbyname)
8496-
uses (`FUN_RESOLVE_SPELL_NAME_TO_SLOT` + the engine's cast
8497-
dispatcher). If the spell needs a ground target, the engine
8498-
enters placement mode and arms the AoE reticle.
8503+
1. Initiates the cast: a numeric spellID resolves to its exact spellbook
8504+
slot (`Spell::Lookup::FindSpellbookSlot`, so the requested rank is the one
8505+
that fires), a name goes through `FUN_RESOLVE_SPELL_NAME_TO_SLOT`; either
8506+
way it dispatches through the engine's cast dispatcher. If the spell needs
8507+
a ground target, the engine enters placement mode and arms the AoE reticle.
84998508

85008509
2. Refreshes the cursor's screen→world raycast via
85018510
`FUN_REFRESH_CURSOR_RAYCAST` (the same internal the engine fires
@@ -8514,24 +8523,28 @@ The companion item version is
85148523
[`C_Item.UseAtCursor`](#c_itemuseatcursoriteminfo) — same chain via
85158524
the item-use path for grenades / on-use ground-target items.
85168525

8517-
### `C_Spell.CastAtUnit(spellID, unit)`
8526+
### `C_Spell.CastAtUnit(spellIDOrName, unit)`
85188527

85198528
Casts a ground-target spell at `unit`'s feet, bypassing the AoE
85208529
reticle click. ClassicAPI's analog of modern's
8521-
`/cast [@unit] Flamestrike``CastAtUnit(spellID, "player")` drops it
8530+
`/cast [@unit] Flamestrike``CastAtUnit(spell, "player")` drops it
85228531
at the player's position, `"target"` at the target's, and so on for
85238532
any unit token (`"mouseover"`, `"party1"`, `"raid7"`, …). Returns
85248533
`true` when the placement landed at the unit; `false` for
85258534
non-ground-target spells (cast still fires normally), unknown
8526-
spellIDs, or a unit with no resolvable position.
8535+
spells, or a unit with no resolvable position.
8536+
8537+
The first argument takes a spellID or a spell name, with the same
8538+
exact-rank / `(Rank N)` semantics as `CastAtCursor`.
85278539

85288540
```lua
8529-
C_Spell.CastAtUnit(2120, "target") -- Flamestrike at the target's feet
8530-
C_Spell.CastAtUnit(10, "player") -- Blizzard centered on yourself
8541+
C_Spell.CastAtUnit(2120, "target") -- Flamestrike at the target's feet
8542+
C_Spell.CastAtUnit(10, "player") -- Blizzard Rank 1 on yourself
8543+
C_Spell.CastAtUnit("Blizzard(Rank 6)", "target")
85318544
```
85328545

85338546
Same two-stage shape as
8534-
[`C_Spell.CastAtCursor`](#c_spellcastatcursorspellid), but instead of
8547+
[`C_Spell.CastAtCursor`](#c_spellcastatcursorspellidorname), but instead of
85358548
the cursor raycast, step 2 reads the unit's world position from its
85368549
`GetPosition` virtual (the same one `CheckInteractDistance` /
85378550
`UnitInRange` use) and commits placement there via

src/spell/AtCursor.cpp

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@
1717
//
1818
// Implementation chains the engine's existing primitives:
1919
//
20-
// 1. Initiate the cast the same way `Script_CastSpellByName` does:
21-
// resolve `spellID` (via our `Spell::CastByID` numeric-name hook)
22-
// to a spellbook slot through `FUN_RESOLVE_SPELL_NAME_TO_SLOT`,
23-
// then dispatch through `FUN_SPELL_CAST_DISPATCH`. For a ground-
24-
// target spell, the engine enters placement mode at this point
25-
// (sets `VAR_SPELL_PLACEMENT_STATE`); for a normal cast, it
26-
// fires immediately.
20+
// 1. Initiate the cast: resolve the *exact* `spellID` to its spellbook
21+
// slot (`Spell::Lookup::FindSpellbookSlot` — NOT name resolution, which
22+
// would cast the highest rank) and dispatch through
23+
// `FUN_SPELL_CAST_DISPATCH`. For a ground-target spell, the engine
24+
// enters placement mode at this point (sets `VAR_SPELL_PLACEMENT_STATE`);
25+
// for a normal cast, it fires immediately.
2726
//
2827
// 2. `Spell::AtCursor::Resolve()` then refreshes the cursor's world
2928
// raycast via `FUN_REFRESH_CURSOR_RAYCAST` and commits the
@@ -41,10 +40,10 @@
4140

4241
#include "Game.h"
4342
#include "Offsets.h"
43+
#include "spell/Lookup.h"
4444
#include "spell/MacroPrimarySpell.h"
4545

4646
#include <cstdint>
47-
#include <cstdio>
4847
#include <cstring>
4948

5049
namespace Spell::AtCursor {
@@ -141,49 +140,71 @@ bool Resolve() {
141140

142141
namespace {
143142

144-
using NameToSlot_t = int(__fastcall *)(const char *name, void *out);
145143
using CastDispatch_t = void(__fastcall *)(unsigned slot, int bookType,
146144
unsigned targetGuidLo, float targetGuidHi);
145+
using NameToSlot_t = int(__fastcall *)(const char *name, void *out);
146+
147+
void DispatchSlot(int slot0Based, int bookType) {
148+
auto dispatch = reinterpret_cast<CastDispatch_t>(
149+
Offsets::FUN_SPELL_CAST_DISPATCH);
150+
// (0, 0) GUID = no implicit target; the engine handles placement.
151+
dispatch(static_cast<unsigned>(slot0Based), bookType, 0, 0.0f);
152+
}
147153

148154
} // namespace
149155

150156
bool DispatchSpellCast(int spellID) {
151157
if (spellID <= 0)
152158
return false;
153159

154-
// Hand the spellID off as a numeric string — our `Spell::CastByID`
155-
// hook on `FUN_RESOLVE_SPELL_NAME_TO_SLOT` translates pure-numeric
156-
// input into the locale-resolved spell name internally, so the
157-
// engine treats it just like `CastSpellByName("Blizzard")`.
158-
char numBuf[16];
159-
std::snprintf(numBuf, sizeof(numBuf), "%d", spellID);
160+
// Resolve the EXACT spellID to its spellbook slot. Every rank is its own
161+
// slot, so we must NOT go through name resolution — that walks to the
162+
// highest rank and would cast Blizzard(Rank 6) for a Rank 1 (spellID 10)
163+
// request. `FindSpellbookSlot` returns a 1-based slot; the dispatcher
164+
// wants the 0-based index into `VAR_PLAYER_SPELLBOOK`.
165+
int bookType = 0;
166+
const int slot1 = Spell::Lookup::FindSpellbookSlot(spellID, &bookType);
167+
if (slot1 <= 0)
168+
return false; // not in the player's spellbook
160169

170+
DispatchSlot(slot1 - 1, bookType);
171+
return true;
172+
}
173+
174+
bool DispatchSpellCastByName(const char *name) {
175+
if (name == nullptr || *name == '\0')
176+
return false;
177+
178+
// The engine's resolver strips a trailing `(Rank N)` and returns the
179+
// 0-based spellbook slot (highest rank when no rank is given). This is the
180+
// by-name path — for an exact rank prefer the numeric spellID overload.
161181
auto nameToSlot = reinterpret_cast<NameToSlot_t>(
162182
Offsets::FUN_RESOLVE_SPELL_NAME_TO_SLOT);
163183
int bookType = 0;
164-
const int slot = nameToSlot(numBuf, &bookType);
184+
const int slot = nameToSlot(name, &bookType);
165185
if (slot < 0)
166186
return false; // not in the player's spellbook
167187

168-
auto dispatch = reinterpret_cast<CastDispatch_t>(
169-
Offsets::FUN_SPELL_CAST_DISPATCH);
170-
dispatch(static_cast<unsigned>(slot), bookType,
171-
0, 0.0f); // no implicit target unit; engine handles placement
188+
DispatchSlot(slot, bookType);
172189
return true;
173190
}
174191

175192
namespace {
176193

177194
int __fastcall Script_C_Spell_CastAtCursor(void *L) {
178-
if (!Game::Lua::IsNumber(L, 1)) {
179-
Game::Lua::PushBool(L, false);
180-
return 1;
195+
// Accept a numeric spellID (exact rank) or a spell name (the engine's
196+
// resolver parses a trailing "(Rank N)"). IsNumber is checked first so a
197+
// numeric string like "10" still takes the exact-spellID path.
198+
bool dispatched = false;
199+
if (Game::Lua::IsNumber(L, 1)) {
200+
dispatched = DispatchSpellCast(static_cast<int>(Game::Lua::ToNumber(L, 1)));
201+
} else if (Game::Lua::IsString(L, 1)) {
202+
dispatched = DispatchSpellCastByName(Game::Lua::ToString(L, 1));
181203
}
182-
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 1));
183204

184-
// Spell not in the player's spellbook → match `CastSpellByName`'s
185-
// silent failure.
186-
if (!DispatchSpellCast(spellID)) {
205+
// Bad input / spell not in the player's spellbook → match
206+
// `CastSpellByName`'s silent failure.
207+
if (!dispatched) {
187208
Game::Lua::PushBool(L, false);
188209
return 1;
189210
}

src/spell/AtCursor.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ bool CommitAtCoords(const float coords[3]);
4646
// `CommitAtCoords`); for a normal spell it fires immediately. Returns
4747
// true if the cast was dispatched (spell is in the player's
4848
// spellbook), false otherwise.
49+
//
50+
// The numeric form casts the *exact* spellID (a specific rank). The
51+
// name form defers to the engine's own resolver, which parses a
52+
// trailing `(Rank N)` — `"Blizzard"` casts the highest known rank,
53+
// `"Blizzard(Rank 6)"` casts that rank.
4954
bool DispatchSpellCast(int spellID);
55+
bool DispatchSpellCastByName(const char *name);
5056

5157
} // namespace Spell::AtCursor

src/spell/AtUnit.cpp

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,14 @@ namespace Spell::AtUnit {
4343
namespace {
4444

4545
int __fastcall Script_C_Spell_CastAtUnit(void *L) {
46-
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsString(L, 2)) {
46+
// arg1: numeric spellID (exact rank) or spell name ("(Rank N)"-aware).
47+
// arg2: unit token.
48+
const bool byNumber = Game::Lua::IsNumber(L, 1);
49+
if ((!byNumber && !Game::Lua::IsString(L, 1)) ||
50+
!Game::Lua::IsString(L, 2)) {
4751
Game::Lua::PushBool(L, false);
4852
return 1;
4953
}
50-
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 1));
5154
const char *token = Game::Lua::ToString(L, 2);
5255

5356
// Resolve the unit's position before dispatching — fail fast so a
@@ -61,7 +64,12 @@ int __fastcall Script_C_Spell_CastAtUnit(void *L) {
6164
return 1;
6265
}
6366

64-
if (!Spell::AtCursor::DispatchSpellCast(spellID)) {
67+
const bool dispatched =
68+
byNumber
69+
? Spell::AtCursor::DispatchSpellCast(
70+
static_cast<int>(Game::Lua::ToNumber(L, 1)))
71+
: Spell::AtCursor::DispatchSpellCastByName(Game::Lua::ToString(L, 1));
72+
if (!dispatched) {
6573
Game::Lua::PushBool(L, false);
6674
return 1;
6775
}

0 commit comments

Comments
 (0)