v1.6.0-alpha: summons and clones as BuffBot casters - #61
Merged
Conversation
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>
…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>
…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>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #19.
What changed
v1.6.0-alphaand documents usage, migration, limitations, and the[BuffBot] SummonsJoinCastkill switch.Verification
BfBot.Test.RunAll()on BG2:EE + EEex v1.0.0: 660 pass, 0 fail across every phase; 9 expected install/fixture warnings.git diff --check, schema JSON parse, secret/debug-artifact scan, andBfBot.VERSION↔ WeiDUVERSIONconsistency 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