Skip to content

v1.6.0-alpha: summons and clones as BuffBot casters - #61

Merged
Chrizhermann merged 34 commits into
mainfrom
feat/summon-casters
Jul 18, 2026
Merged

v1.6.0-alpha: summons and clones as BuffBot casters#61
Chrizhermann merged 34 commits into
mainfrom
feat/summon-casters

Conversation

@Chrizhermann

Copy link
Copy Markdown
Owner

Closes #19.

What changed

  • Adds structural discovery for allied non-party spellcasters and stable per-identity summon presets in save schema v8.
  • Adds the Summons view, clone preset seeding, standalone Cast (this summon), automatic joint Cast All participation, and mid-run late join.
  • Refactors execution around fresh-resolved party/summon caster references, including gone-summon cleanup, allegiance revalidation, conservative multiplayer ownership, and Project Image owner-lock protection.
  • Bumps BuffBot and WeiDU versions to v1.6.0-alpha and documents usage, migration, limitations, and the [BuffBot] SummonsJoinCast kill switch.

Verification

  • Full in-game BfBot.Test.RunAll() on BG2:EE + EEex v1.0.0: 660 pass, 0 fail across every phase; 9 expected install/fixture warnings.
  • Live clone standalone cast, kill-mid-run cleanup, save-load recovery, and Project Image owner-lock skip passed.
  • Live Summons-view configuration and standalone UX passed.
  • Live late join passed: Imoen cast Project Image, the image attached with two entries, cast Stoneskin and Strength, and completed 3 casts / 0 skips without watchdog intervention.
  • git diff --check, schema JSON parse, secret/debug-artifact scan, and BfBot.VERSION ↔ WeiDU VERSION consistency passed.

Deferred follow-up

Copied F12 innates on clones are not part of this release; the failed live behavior and workaround are tracked in #60.

🤖 Generated with Claude Code

Christopher Hermann and others added 30 commits July 11, 2026 16:45
Live-session probe results (BG2:EE active playthrough, EEex v0.11.x):
- ITER_FORM: IterateCPtrList(m_lVertSort) yields plain-number object IDs
- Clone marker: clone-side stat 139 PUPPETMASTERTYPE (2=PI, 3=Simulacrum);
  owner linkage m_nCopyParent == stat 138; scriptName COPY for both types
- EA: clones + planetar = 4 (ALLY); accept band 2..30
- Planetar action queue works (cast started under plangood script)
- 402 handler sees the clone as m_sourceId with baked slot/preset intact
- Clones copy the owner's full innate list (PI and Sim, 14/14)
- PI puppet locks its owner: queued owner actions stall until expiry

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#19)

An active Project Image engine-locks its owner (queued actions fire as
delayed zombie casts at image expiry). Replace the watchdog-waits remedy
with skip-and-log: live PI clone => skip owner entries; owner entries
after a PI cast => dropped at build time (user priority order is never
reordered). Simulacrum never locks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ote for Task 11 (#19)

Infinity_DoFile serves a memory cache and never rereads disk (KB
eeex-filesystem.md, verified 2026-04-19, re-confirmed live 2026-07-11).
Point workflow docs at the forceLoad (io.open+loadstring) pattern and
warn about init/listener re-registration on hot reloads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ts (#19)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…es (#56)

Merge the sprite's real granted presets into the synthetic configs of
Cases 3-5 so the planner's correct orphan detection is not misread as
a failure on innate-carrying saves. Planner untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… presets (#56)

Follow-up to a34b7b2 per review: a warn-skip on a CLEAN actual set would
mask a planner over-report regression behind a green suite. Distinguish
dirty-save (warn-skip) from over-report (hard FAIL) using probe.actual.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ry step (#19)

Behavior-neutral for party casters; summon-kind refs resolve via object
ID with clean chain-completion when the sprite is gone. Input contract
of the Persist builders unchanged.

Advance callbacks embed caster keys as Lua long-bracket literals: the
BCS tokenizer strips single quotes inside double-quoted action args.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ock (#19)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ition I1) (#19)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tests, sweep log honesty (#19)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
)

Area sweep + per-sprite classifier (alive, not-party, EA 2..30, has
castable spells) with probe-verified clone detection (stat 139 +
m_nCopyParent) and identity fallback chain clone:owner -> scriptname
-> cre:resref -> name. TTL cache re-resolves sprites on hit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… 5 review) (#19)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-cache eviction, read-only note (#19)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…config, clone seeding (#19)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed checkpoint slot (#19)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hal-unsafe values (#19)

Quality-review conditions: summons subtree scrubbed on import (crash-on-
save class), warn-on-repair, qc clamped everywhere, keyword-safe
serializer keys, migration-contract + re-seed-guard tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ill-switch (#19)

BuildQueueForSummon builds a standalone exec queue for one allied
summon/clone off its per-identity preset (on/pri/tgt/var honored, own qc
via per-entry cheat override); BuildQueueFromPreset sweeps configured
allied summons into party casts behind the new SummonsJoinCast INI pref
(default 1). PeekSummonPreset makes queue building a strict read path —
detected-but-unconfigured summons can never pollute the save.

Three guards land with it:

- Puppet-lock: a live Project Image engine-locks its owner (probe-verified
  zombie-cast hazard) — a PI owner's party entries are skipped at build
  time, and entries trailing a "Project Image" cast in a chain are dropped;
  Simulacrum locks nothing. Pure decision core, applied regardless of the
  SummonsJoinCast pref.
- Gone-summon sweep: _SafetyTick finishes summon casters whose ref no
  longer resolves before the watchdog check — a summon destroyed mid-cast
  takes its queued advance with it, so the run now completes in ~2s instead
  of stalling into the 30s watchdog.
- Allegiance re-validation: the sweep re-classifies each detection entry
  off a fresh resolve, and every exec step re-reads the summon's EA band —
  a summon charmed/turned hostile mid-run ends its chain instead of
  receiving buffs.

SummonCasters phase: 72 → 128 assertions, full 22-phase suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ar puppet-lock, fresh-resolve builder (#19)

Review amendments on top of cfb2967 (Task 7, two-review conditions):

1. M1: Exec.Start derives each caster's cheatBoundary from the entries'
   cheat flags unconditionally (new _DeriveCheatBoundary helper, qcMode
   gate dropped) — a summon whose identity preset carries qc>0 now gets
   its BFBTCH toggle even in a qcMode=0 run (default UI path, Task 8's
   Exec.Start(q, 0)). Party-inert: party entries only carry cheat=true
   when qcMode>0, so a qcMode=0 party run still derives boundary 0.
   Fail-first test [7.8] (synthetic Start, stubbed _BuildQueue and
   _ProcessCasterEntry) captured the pre-fix boundary=0 FAIL. Hardening
   found while testing: the helper counts only cheat==1 or cheat==true,
   never bare truthiness — Lua 0 is truthy and builder-level entries
   carry cheat as 1/0.

2. M2: BuildQueueForCharacter now runs the puppet-lock policy
   (_CollectLiveCloneDescriptors + _ApplyPuppetLockPolicy +
   _LogBuildSkips), covering the Cast-Character button and the F12
   innate path in one place. Deliberate scope extension of the plan's
   puppet-lock policy to the per-character builder, adopted from the
   quality review. spellName ride-along added and stripped exactly like
   the preset builder; output shape unchanged. Test [7.9].

3. M3: BuildQueueForSummon no longer dereferences the caller-supplied
   summonEntry.sprite — always fresh-resolves via
   Exec._ResolveCaster(oid+name) (issue-#38 class: pcall does not catch
   the access violation from a freed CGameSprite; Task 10 will hand in
   stored detection entries). Resolver nil → warn + return nil.

4. N5: Persist Test 9 accepts the Task-7 summon entry shape — an entry
   is valid iff numeric caster 0..5 OR casterRef {kind="summon", numeric
   oid, non-empty string name}; anything else still fails.

5. N6: BuildQueueForSummon applies puppet-lock rule 2 to the summon's
   OWN chain (trailing entries after a "Project Image"-named cast are
   dropped) — a clone casting PI engine-locks itself into a zombie tail.
   Rule 1 intentionally not applied (a clone is not a party owner);
   _SeedCloneSpells untouched (the seed mirrors the owner by design).
   Test [7.10].

6. N4: _ApplyPuppetLockPolicy LIMITATION comment now spells out the
   English-only rule-2 name match (TLK-localized installs degrade
   benignly — delayed trailing casts, watchdog completes; rule 1 is
   locale-independent). Comment only.

7. N11: test [7.1]'s test:peek cleanup moved outside its pcall and
   asserted ([7.3]/[7.4] pattern) so a hard error can't strand the
   synthetic identity in the shared save.

Full suite green on the test install: 22 phases PASS, SummonCasters
145 pass / 0 fail / 0 warn.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Behavior-neutral: _view is fixed to "party" (nothing sets "summons" yet), so
every converted site is equivalent to the previous direct
EEex_Sprite_GetInPortrait(BfBot.UI._charSlot) call. 21 sites converted
(20 selected-sprite reads + the _OnOpen validity fallback, which now also
re-asserts party view — an idempotent no-op today). Deliberately unconverted
GetInPortrait sites (they read other slots): _Refresh char-name loop
(slots 0-5), legacy slot->name conversion (num - 1, two sites), and the
_CanCastAll other-slot loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iduals (#19)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BfBot.Mp.IsSummonLocallyControlled(summonEntry), the Task-13 predicate the
_SummonPassesMpRule seam (Task 7) feature-detects and now routes through:

- Single-player -> always true (connection-flag short-circuit via the new
  stubable BfBot.Mp._IsMpSession helper; fires before any entry inspection
  or sprite resolution).
- MP, clone summon (fresh-resolved sprite has an owner in m_nCopyParent:
  Project Image / Simulacrum / Mislead) -> the clone follows its OWNER's
  control: IsLocallyControlled(ownerSprite), owner required to be a party
  member.
- MP, ownerless summon (Planetar, elementals, ...) -> host heuristic: this
  machine is authoritative iff it controls portrait slot 0.
- FAIL-CLOSED in MP: any error or resolution failure (malformed entry,
  summon gone, owner gone or non-party, engine read error) -> false.
  Better a summon silently not joining on one machine than two machines
  double-queuing the same summon.

Avoids the verified hazard: IsLocallyControlled degrades to TRUE for
non-party sprites in MP auto mode (EEex_Sprite_GetCharacterIndex failure
path), so it is never called on the summon sprite itself - only on the
party-member owner or the slot-0 leader. Sprites are always fresh-resolved
by oid+name (issue-#38 discipline); caller-supplied sprite fields ignored.

Tests (SummonCasters [13.1-13.3], +16 asserts -> 161 phase / 572 suite,
fail-first observed then green, 22 phases): SP short-circuit order proof,
MP fail-closed guards, ownerless + clone branches via stubbed _IsMpSession
and a resolver stand-in pointing m_nCopyParent at the real leader, seam
routing/propagation. The live-clone owner branch under a real second
machine remains 2-machine-session-only - verification tracked on issue #19.

CHANGELOG deferred: the MP caveat lands with Task 14's v1.6.0-alpha entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… MP reflection latch (#19)

M1: docstring no longer presents Mislead as probe-verified (PI/Simulacrum
are; Mislead is per-IESDP, and a Mislead clone without m_nCopyParent falls
to the still-conservative host rule).
M2: _IsMpSession reflection failure now warns once (latch) instead of
degrading to single-player silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task 8 verified live (standalone cast, kill-mid-run sweep, reload-mid-run
gone-path recovery, puppet-lock rule-1 SKIP) — no fixes needed, no code
commit. Task 9 user panel smoke passed. Task 13 review-amended (c9ce065).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s, standalone cast (#19)

Task 10 coverage:
- A. View state + list: _summonList (deep-copied model of GetAlliedSummons —
  cache-owned tables never aliased, hand-off 5), _summonPage, identity-stable
  _summonSel (reselect by oid+name then identity, never row index); nameless
  entries refused from the model (resolver anti-oid-recycle guard, hand-off 3).
- B. _SelectedSummon() replaces the Task-9 stub (model lookup by descriptor).
- C. View toggle: bbView button flips party <-> summons ("Summons"/"Party"
  label); switch invalidates the summon sweep, rebuilds the list, resets
  paging, keeps _presetIdx; SetChar always lands in party view (hand-off 1).
- D. Summons tab row: bbS0-bbS5 share the character-tab row, labels via pure
  _SummonTabLabel (owner-possessive "asd's Image"/"...'s Simulacrum" for
  clones), <  1/2  > paging cluster visible only when >6 summons (pure
  _SummonPageSlice does the math + clamping), "No allied summons detected"
  empty-state label.
- E. Selection + seeding: first open of an identity+preset creates it via
  GetSummonPreset; clones seed from a FRESH m_nCopyParent owner resolve +
  fresh clone resolve (never cached sprites). Spell list, toggles, targets,
  priorities, variants, quick-cast all read/write the summon preset (v8
  schema on/tgt/pri/var + qc) — writes routed in the UI, never through the
  per-character Persist setters, so a summon sprite never grows a config.
  Lock and Unlock-Targeting are party-only (no lock/tgtUnlock in the v8
  summon schema) and are hidden/no-op in the summons view.
- F. Vanished summon: refresh prunes the dead entry, falls forward to the
  first live summon, else empty state; _OnOpen re-sweeps and falls back to
  party slot 0 (Task-9 pattern).
- G. Cast: summons-view cast-selected routes CastCharacter to
  BuildQueueForSummon + Exec.Start(q, 0) (entries carry their own cheat
  flag), label "Cast (this summon)"; _CastCharLabel/_IsCharSelected/
  _CanCastAll are view-aware (hand-off 2). Cast-All decision: Cast All stays
  party-preset-driven in BOTH views — BuildQueueFromPreset already sweeps
  configured summons into the run, so a summons-view variant would duplicate
  it (documented in BfBot.UI.Cast).
- H. Reasons + skip surfacing (hand-off 4): BuildQueueForCharacter returns a
  distinct "puppet-locked" reason when the puppet-lock policy empties a
  non-empty build (panel shows the Project-Image message instead of the
  generic no-spells text); _LogBuildSkips additionally queues its lines and
  the UI re-appends them into the fresh run log after Exec.Start's reset
  (DrainBuildSkips — minimal BfBotPer plumbing, no logging rewrite).
- I. Tests: SummonCasters phase grows page-slice math (empty/1/6/7/12/13 +
  clamps), label composition, list-model copy/filter semantics, view state
  machine + reselect, skip drain, puppet-locked reason (fail-first observed:
  7 new failures before implementation). Suite: 22 phases green, phase now
  196 pass / 0 fail.

Verified live on the test install (remote console, PI clone of the leader):
view switch, possessive tab label, seeding from owner preset, toggle/target
writes into the summon preset, standalone clone cast to DONE (Cast: 1,
Skipped: 0); synthetic summon config purged afterwards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rain, clone-type reselect, qc tick cache (#19)

- MINOR-1: _SurfaceBuildSkips inserts into the in-memory panel log only (mirrors _LogEntry's {type,msg} shape) — the file line is written once, at build time
- MINOR-2: new _StartRun gates surfacing on a successful Exec.Start; a refused Start discards pending skips instead of replaying them into the previous run's panel log
- MINOR-3: F12 innate path drains-and-discards Persist._pendingSkips after its build — no more leak into an unrelated panel cast's log
- MINOR-5: _ReselectSummon's identity fallback prefers the previously selected cloneType (PI + Sim of one owner share identity); _summonSel now carries cloneType
- MINOR-4: summons-view Quick Cast value cached in BfBot.UI._summonQc (written by _RefreshSummonsView exits + CycleQuickCast); per-frame bbQC text/color reads stop re-resolving the protagonist config every frame

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Christopher Hermann and others added 3 commits July 14, 2026 14:51
…19)

A) Exec.Start(queue, qcMode, presetIdx) records the run's preset as
   _runPresetIdx (nil for raw/console queues -> late-join inert); cleared
   in _HardReset. Threaded through from every preset-driven entry point:
   UI._StartRun (Cast / CastCharacter / _CastSelectedSummon pass
   UI._presetIdx) and BFBOTGO's direct Start call (one-line extension).

B) Exec._AttachCaster(summonEntry, queue): expands the builder queue via
   _BuildQueue, inserts a Start-shaped caster record (cheatBoundary
   derived from ITS entries at attach time), bumps _activeCasters +
   _totalEntries, logs "late-join: <name> (<n> entries)", notes watchdog
   progress, kicks _ProcessCasterEntry(key, 1).

C) Listener factored into Exec._OnSpriteLoaded, guards cheap-first
   (state -> _runPresetIdx -> checked pcall -> portrait -> attached),
   WARN on error (no silent pcall). TWO-PHASE by engine necessity, probe-
   verified live: the loaded listener fires BEFORE the engine sets a
   conjured clone's name ("?"), EA (owner's verbatim 2), script name
   (owner's, not COPY) and puppet linkage (m_nCopyParent -1, stat139 0) —
   only the spellbook is copied at fire time. Immediate classification
   would misidentify the clone and bind a nameless ref that the
   anti-recycle guard would kill mid-chain. So the listener records the
   oid into _pendingLateJoin; _ProcessLateJoins (driven by _SafetyTick's
   ~2s cadence, running-state only) classifies once the name resolves
   (retry budget 5) and runs the canonical sequence: classify ->
   already-attached -> ONE shared MP gate (_SummonPassesMpRule) ->
   BuildQueueForSummon(e, _runPresetIdx) -> _AttachCaster. Builder SKIPs
   surface into the live run's in-memory panel log (no second file write).

D) Registration in M_BfBot.lua via EEex_Sprite_AddLoadedListener: thin
   namespace-resolving wrapper; guard flag on the BfBot ROOT (module
   re-execution resets BfBot.Exec, never BfBot) so a re-run can never
   stack a second listener.

E) SummonCasters [11.1]-[11.5]: fail-first (17 red on pre-code), now
   239 pass / 0 fail (was 209); all 22 phases green on the test install.

F) Live verification (test install, headless, listener registered
   in-session with the same wrapper):
   - Run A (clone:Imoen UNCONFIGURED): PI conjured externally on Imoen
     mid-run -> processor saw the clone exactly once, no attach, no
     late-join line, run completed 6/6 cast.
   - Run B (clone:Imoen preset 1 = SPWI214): same spawn -> "late-join:
     Imoen (1 entries)" -> "CAST: Imoen -> Strength -> Imoen" ->
     "Imoen finished" in parallel with the party chain -> DONE
     Total: 7 | Cast: 7 | Skipped: 0 (clone entry included).
   - Cleanup asserted: config purged, wrapper restored, no save.
   Idle-load no-op remains on the user checkpoint (normal-load
   registration exercises after restart).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…guard, pending-clear tests (#19)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Chrizhermann
Chrizhermann merged commit 5d125c5 into main Jul 18, 2026
1 check passed
@Chrizhermann
Chrizhermann deleted the feat/summon-casters branch July 18, 2026 15:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support clones/summons as additional casters

1 participant