//! The spellbook (decision 0216 §8, slice 5) — the spell **source** for the cursor payload //! system: a read-only book model the app builds from `PlayerActions.spells` //! (`SMSG_INITIAL_SPELLS`), the same two-way seam shape as [`super::merchant`]/[`super::action`]: //! the app pushes a **book snapshot** ([`UiScript::set_spellbook`] — tabs + the flat slot list, //! already resolved to name/rank/icon/passive by the app's `benilla_formats::SpellCatalog` × //! skill-line join), and `CastSpell`/`PickupSpell` queue outbound intents the app drains //! ([`UiScript::take_spell_casts`] / the cursor seam's own `CursorPayload::Spell` arm — decision //! 0216 §1). The engine holds no spell KNOWLEDGE (icons/ranks/skill lines are the app's job) — a //! slot is "a spell id, a name, a rank, a texture, and a passive bit". //! //! ## The book-id seam (decision 0218 §4's byte-verified 0-based book slot) //! //! The ref's own FrameXML computes a **1-based, per-tab-cumulative "book id"** //! (`SpellBookFrame.lua`'s `SpellBook_GetSpellID`: `buttonId + tabOffset + 12*(page-1)`, where //! `buttonId` is a spell button's own 1..12 `id=` attribute and `tabOffset` is //! `GetSpellTabInfo`'s own `offset` return) and passes that SAME id, unmodified, to every one of //! `GetSpellName`/`GetSpellTexture`/`IsSpellPassive`/`CastSpell`/`PickupSpell`. 0218 §4 byte-read //! `PickupSpell`'s own argument as a **0-based book slot** — so the real client's Lua↔C++ glue //! does the `-1` itself, invisibly to FrameXML. This engine keeps the ref's exact Lua-facing //! convention (every binding below takes the SAME 1-based-cumulative `id` a transcribed //! `SpellBookFrame.xml` computes and passes verbatim, so the transcription needs no special //! casing) and does the byte-verified `-1` at THIS one seam ([`slot_index`]) before indexing //! [`SpellBookState::slots`] (0-based, flat, tab order). `GetSpellTabInfo`'s pushed `offset` is //! therefore exactly each tab's 0-based START index into `slots` — the app computes it as the //! running sum of every earlier tab's `num_spells` (tab 1's is `0`, so its first spell's book id //! is `1`, matching the ref's own "first tab's first spell is id 1"). //! //! ## The pet book (decision 1032 — live; 0216 §8's deferral is retired) //! //! `BOOKTYPE_PET` selects a **second slot list** ([`PetBookState`]), fed from `SMSG_PET_SPELLS`' //! own spell tail. Every `bookType`-taking binding is a two-way fork ([`book_slot`]) exactly as the //! reference's are — `isPet ? [0xb6f098 + 4*i] : [0xb700f0 + 4*i]`, written out once per binding — //! and the three pet-only bindings (`HasPetSpells`, `GetSpellAutocast`, `ToggleSpellAutocast`) live //! here with them. //! //! Two asymmetries are the API and not tidiable away: //! //! - `GetNumSpellTabs`/`GetSpellTabInfo` take **no** `bookType` and only ever answer the player's //! skill lines. That is the reference's own signature: the pet book has no tabs, and //! `SpellBookFrame_Update` hides the whole skill-line strip while it is up. //! - `PickupSpell` and `CastSpell` produce a **different kind of thing** on the pet side — a pet //! action word on the cursor (`0x494e20`, cursor modes 1-7) and a `CMSG_PET_ACTION` on the wire //! (`0x4b34ce`) — rather than a spell payload and a player cast. See each binding. //! //! `BOOKTYPE_SPELL`/`BOOKTYPE_PET` are installed as plain Lua globals here rather than left to the //! transcribed XML's own `