app+ui: the queue, the disenchant sequencer — 62/62

QueueSpellByName, QueueScript, ChannelStopCastingNextTick,
DisenchantAll. Every one of nampower's 62 registered Lua functions is
now defined; the `comm -23` in the doc prints nothing.

**Upstream's queue has two halves and one is deliberately not built.**
The Lua surface — remember this spell, send it when the client will
accept it — is `spell/queue.rs`. The other half intercepts *every
ordinary press*, so a keypress during a cast is held rather than
refused, and it is governed by NP_QueueCastTimeSpells,
NP_QueueInstantSpells, the buffer rows and the quickcast pair. That half
is not a binding: it changes how every key in the game feels, its bugs
are double-casts and dropped casts, and it is not something to switch on
as a side effect of porting three function names. Those twenty CVars
stay stored-only, each row saying so; the machinery they would sit on is
now here, and turning it on is its own piece of work with its own live
test.

Two of the twenty-two went live, because they are the explicit queue's
own knobs: NP_SpellQueueWindowMs and NP_QueueSpellsOnCooldown.

Three slots, because upstream has three and its own event says so:
SPELL_QUEUE_EVENT's code is 0/1 on-swing, 2/3 normal, 4/5 non-GCD, so
the queue is keyed by what kind of cast a spell is — and the kind comes
off its own Spell.dbc row through the same three tests the send path
makes. Each pops on its own condition: non-GCD and on-swing as soon as
no cast guards (neither uses the global cooldown), normal when the GCD
has run out too.

Three decisions at the queue, each pinned:

- A replacement raises no popped event. A second queue of one kind
  replaces the first — upstream's own log line is `Replacing queued
  nonGcd spell X with Y` — and the replaced entry was never sent, so a
  consumer counting pops against queues would see one too many.
- An expiry raises nothing either. `*_POPPED` means "this was sent".
- The expiry is measured from the end of the cast in flight, not from
  the moment of queueing, so queueing early during a long cast is not
  punished. A lapsed deadline cannot shorten the window.

**`DisenchantAll` is specified completely by the DLL's own strings**,
which is why it is built this way rather than designed. `0x93087`
onward carries the argument note, the precondition, the three quality
keywords, every stop reason and the three chat lines — one of them as a
Lua chunk that builds the item link itself.

"move during cast to cancel" is the abort, and it needed nothing new:
moving already cancels a cast here, the cast then fails, and a failed
cast stops the run. The one piece of machinery a sequencer usually
needs is the client's own, and the player's way out is the one upstream
documents.

How a step's outcome is read: the item is gone, or it is not. There is
no "disenchant succeeded" packet — the item leaves the bag. Each step
remembers the guid it cast at and waits for it to vanish with a
ten-second deadline. An observable test with no new wire plumbing,
right for the same reason the cast bar's own edges are.

The queue's drain and the sequencer both live inside
`drain_spell_casts`, which that file's own note explains: only one
system may hold `CastLadder`, because a second would share its
resources with every other cast-send system in an order the ambiguity
gate cannot know is safe.

`spell/queue.rs` names no VM — the first draft mentioned `UiScript` in
one comment and `ui_script_wall.rs` counted it as gameplay code naming
the VM, which is the gate working as intended: that module really is
gameplay state and not a feed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-10-24 13:25:27 -06:00
co-authored by Claude Opus 5
parent b652c90a4b
commit 702eb475a4
13 changed files with 1366 additions and 30 deletions
+66 -20
View File
@@ -476,7 +476,7 @@ real when something else agrees — an addon calling it, or the mod's own docs.
What it found that this file did not already have: **38 names in `nampower.dll` that grom's addons
call**, on top of the families the nampower section above already tracks. Unported, and listed
rather than guessed at — `CancelPlayerAuraSlot`, `CancelPlayerAuraSpellId`, `CombatLogFlush`, `CustomFileExists`, `EncryptPassword`, `EncryptedServerLogin`, `ExecuteCustomLuaFile`, `ExportFile`,
rather than guessed at — `CancelPlayerAuraSlot`, `CancelPlayerAuraSpellId`, `ChannelStopCastingNextTick`, `CombatLogFlush`, `CustomFileExists`, `EncryptPassword`, `EncryptedServerLogin`, `ExecuteCustomLuaFile`, `ExportFile`,
`CastSpellByNameNoQueue`, `CastSpellNoQueue`, `ChannelStopCastingNextTick`, `CombatLogFlush`,
`CustomFileExists`, `EncryptPassword`, `ExecuteCustomLuaFile`, `FindPlayerItemSlot`, `GetBagItem`,
`GetCastInfo`, `GetCurrentCastingInfo`, `GetItemIconTexture`, `GetItemIdCooldown`,
@@ -620,18 +620,11 @@ migrates by itself. The machinery to make the Remember Password tick-box write a
now all here — one line in `login/mod.rs` — and that is his call to make, not a side effect of
porting two verbs.
**Needs a feed this client does not have, with what it would take (2).**
| Name | What is missing |
|---|---|
| `GetUnitData(unit)` | The whole descriptor as a table, where `GetUnitField` answers one field. Needs a decision about which fields: the real one returns GUID-typed columns (charm, summon, charmedBy) this client's field reader does not type |
**Not ours to add (3).** `QueueSpellByName`, `QueueScript` and `ChannelStopCastingNextTick` are the
spell queue itself, which this client deliberately does not have (see the nampower rows above:
namreeb's always-on cast rule is ported, pepo's queue machinery is not). `LearnTalentRank(page,
index, rank)` is left out for a narrower reason: `LearnTalent` here learns the *next* rank, so
reaching a named rank means queueing the pair once per missing rank, and whether the server accepts
that burst is not something this client can check before sending.
**Everything below this line is the reasoning of earlier rounds, kept rather than deleted.** Each
section records what was missing *then*, what it would have taken, and — where a later round built
it — which of those reasons turned out to be wrong. That is the point of keeping them: three of the
"needs a feed" rows were answerable with data the tree already had, and a list of only the final
answers would hide that the first reading was the expensive mistake.
### The cast feed — `GetCurrentCastingInfo`, `GetCastInfo` and `SPELL_CAST_EVENT`
@@ -874,6 +867,62 @@ Three decisions at that mapping:
the player. The other five (`guid`, `posX`, `posY`, `power`, `maxPower`) are filled by the
caller, which knows them.
### The queue, and the one verb that needs it — 62/62
**Built**, on grom's "62/62": `QueueSpellByName`, `QueueScript`, `ChannelStopCastingNextTick` and
`DisenchantAll`.
**Upstream's queue has two halves, and one of them is deliberately not built.** The Lua surface —
"remember this spell and send it when the client will accept it" — is `spell/queue.rs`. The other
half intercepts **every ordinary press**, so a keypress during a cast is held rather than refused;
it is governed by `NP_QueueCastTimeSpells`, `NP_QueueInstantSpells`, `NP_QueueTargetingSpells`,
the buffer rows and the quickcast pair. That half is not a binding: it changes how every key in the
game feels, it is the one piece whose bugs are double-casts and dropped casts, and it is not
something to switch on as a side effect of porting three function names. **Those twenty CVars stay
stored-only**, each row saying so. If grom wants press-interception, it is its own piece of work
with its own live test — and the machinery it would sit on is now all here.
Two of the twenty-two went **live**, because they are the explicit queue's own knobs:
`NP_SpellQueueWindowMs` (how long past the in-flight cast's end an entry is held before it gives
up) and `NP_QueueSpellsOnCooldown` (whether an entry may wait out the spell's own cooldown).
**Three slots, because upstream has three and its own event says so.** `SPELL_QUEUE_EVENT`'s first
argument is `0`/`1` on-swing queued/popped, `2`/`3` normal, `4`/`5` non-GCD — so the queue is keyed
by what *kind* of cast a spell is, and the kind comes off its own `Spell.dbc` row through the same
three tests the send path makes. Each slot pops on its own condition: non-GCD and on-swing as soon
as no cast guards (neither uses the global cooldown), normal when the GCD has run out too.
Three decisions at the queue:
- **A replacement raises no popped event.** A second queue of one kind replaces the first — which
is upstream's own behaviour, its log line being `Replacing queued nonGcd spell X with Y` — and the
replaced entry was never sent. A consumer counting pops against queues would otherwise see one
too many.
- **An expiry raises nothing either.** `*_POPPED` means "this spell was sent"; a timed-out entry
was not, and upstream has no code for one.
- **The expiry is measured from the end of the cast in flight**, not from the moment of queueing,
so a player who queues early during a three-second cast is not punished for it. A lapsed
deadline cannot shorten the window below a fresh queue's.
**`DisenchantAll` is specified completely by the DLL's own strings**, which is why it is built the
way it is rather than designed here. `0x93087`–`0x932d0` carries the argument note
(`includeSoulbound: optional number - pass 1 to include soulbound items (defaults to 0)`), the
precondition (`You do not have the Disenchant spell`), the three quality keywords (`greens`,
`blues`, `purples`), every stop reason, and the three chat lines — one of them as a Lua chunk that
builds the item link with `GetContainerItemLink`.
**"move during cast to cancel" is the abort, and it needed nothing new.** Moving already cancels a
cast here (`inflight.rs`'s `local_self_cancel`), the cast then fails, and a failed cast stops the
run — so the one piece of machinery a sequencer usually needs is the client's own, and the player's
way out is the one upstream documents.
**How a step's outcome is read: the item is gone, or it is not.** There is no "disenchant
succeeded" packet; what happens is the item leaves the bag. Each step remembers the guid it cast at
and waits for it to vanish, with a ten-second deadline. Gone is success and the run continues;
still there past the deadline is a failure and the run stops with upstream's own line. An
observable test with no new wire plumbing — right for the same reason the cast bar's own edges are,
that the thing the player sees is the thing the client keys on.
### The whole registered list, accounted for
With the registration table read (the section above), the surface is no longer a count of what a
@@ -890,15 +939,12 @@ comm -23 /tmp/np /tmp/ours
| Bucket | Count | Names |
|---|---|---|
| **Built** | 58 | `CancelPlayerAuraSlot`, `CancelPlayerAuraSpellId`, `CastSpellByName`, `CastSpellByNameNoQueue`, `CastSpellNoQueue`, `FindPlayerItemSlot`, `GetBagItem`, `GetBagItems`, `GetEquippedItem`, `GetEquippedItems`, `GetItemIconTexture`, `GetItemIdCooldown`, `GetItemLevel`, `GetItemStats`, `GetItemStatsField`, `GetNampowerVersion`, `GetPlayerAuraDuration`, `GetQuestDialogQuestId`, `GetQuestLogQuestIds`, `GetSpellDuration`, `GetSpellIconTexture`, `GetSpellIdCooldown`, `GetSpellIdForName`, `GetSpellModifiers`, `GetSpellNameAndRankForId`, `GetRaidTargets`, `GetSpellPower`, `GetSpellRangeData`, `GetSpellRec`, `GetSpellRecField`, `GetUnitData`, `ImportFile`, `ReadCustomFile`, `StartItemExport`, `WriteCustomFile`, `GetSpellSlotTypeIdForName`, `GetTrinketCooldown`, `GetTrinkets`, `GetUnitField`, `GetUnitGUID`, `IsAuraHidden`, `IsSpellInRange`, `IsSpellUsable`, `PlayerIsMoving`, `PlayerIsRooted`, `PlayerIsSwimming`, `LearnTalentRank`, `SetLocalRaidTargetIndex`, `SetMouseoverUnit`, `UseItemIdOrName`, `UseTrinket` |
| **Built** | **62** | `CancelPlayerAuraSlot`, `CancelPlayerAuraSpellId`, `CastSpellByNameNoQueue`, `CastSpellNoQueue`, `ChannelStopCastingNextTick`, `CombatLogFlush`, `CustomFileExists`, `DisenchantAll`, `EncryptPassword`, `EncryptedServerLogin`, `ExecuteCustomLuaFile`, `ExportFile`, `FindPlayerItemSlot`, `GetBagItem`, `GetBagItems`, `GetCastInfo`, `GetCurrentCastingInfo`, `GetEquippedItem`, `GetEquippedItems`, `GetItemIconTexture`, `GetItemIdCooldown`, `GetItemLevel`, `GetItemStats`, `GetItemStatsField`, `GetNampowerVersion`, `GetPlayerAuraDuration`, `GetQuestDialogQuestId`, `GetQuestLogQuestIds`, `GetRaidTargets`, `GetSpellDuration`, `GetSpellIconTexture`, `GetSpellIdCooldown`, `GetSpellIdForName`, `GetSpellModifiers`, `GetSpellNameAndRankForId`, `GetSpellPower`, `GetSpellRangeData`, `GetSpellRec`, `GetSpellRecField`, `GetSpellSlotTypeIdForName`, `GetTrinketCooldown`, `GetTrinkets`, `GetUnitData`, `GetUnitField`, `GetUnitGUID`, `ImportFile`, `IsAuraHidden`, `IsSpellInRange`, `IsSpellUsable`, `LearnTalentRank`, `PlayerIsMoving`, `PlayerIsRooted`, `PlayerIsSwimming`, `QueueScript`, `QueueSpellByName`, `ReadCustomFile`, `SetLocalRaidTargetIndex`, `SetMouseoverUnit`, `StartItemExport`, `UseItemIdOrName`, `UseTrinket`, `WriteCustomFile` |
| **The queue, and the one verb that needs it** | 4 | `QueueSpellByName`, `QueueScript`, `ChannelStopCastingNextTick`, `DisenchantAll` |
58 + 4 = 62, and the `comm -23` above is the check: run it and the four it prints are exactly the
last row — `DisenchantAll` and the queue trio.
The two that were at the head of that last bucket are now built — see "The cast feed" above for
why they came first and what each decision cost.
**62 of 62.** The `comm -23` above is the check, and it now prints nothing. What remains unbuilt is
not a *name* — it is upstream's press-interception half of the queue, which has no Lua surface of
its own and whose twenty CVars say at each row that they are stored-only and why.
### The event table, accounted for