Step 4 of multi-instancing migration. Three additional per-battle statics
front-fronted by BattleAmbient.Current, each with a static fallback for
unwrapped callers. ViewerId's SavedataManager-persisting setter is preserved
on the fallback path; inside a scope, the setter is a no-op (the per-battle
perspective is fixed at scope entry).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Step 3 of multi-instancing migration. The dominant per-battle singleton now
resolves through BattleAmbient.Current.Mgr when a scope is active. The legacy
'main' field is renamed _mainFallback and retained for unwrapped callers
(tests, anything not yet scope-wrapped). GetIns() still returns null when
neither is set, preserving the '?.Foo ?? default' patterns in engine code.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Step 2 of multi-instancing migration. Both flags now resolve through
BattleAmbient.Current when a scope is active, otherwise hit a static fallback
that preserves today's behavior unchanged for unwrapped callers.
Suite green: SVSim.BattleEngine.Tests pass; SVSim.UnitTests baseline holds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Six distinct fixes accumulated over live-test iterations against four bids
(654473755566, 806245601092, 283192092460, 131549100204, 799755786270) — together
they take the shadow engine from "throws on the first non-mulligan play" to
"survives a full PvP battle, only weird-edge-case Unity touches still left to whack".
1. Engine StableRandom seed aligned with clients' Matched.seed
(BattleSession.EnsureEngineSetup, NodeNativeBattleHarness.Create). Clients seed
_stableRandom with BattleSeeds.Stable(masterSeed) (the value the node ships in
Matched.seed); we were passing the RAW masterSeed to engine.Setup, so every
StableRandom call diverged from call #1 onward — every turn-1+ draw picked a
different deck position than the clients. Verified Stable(1184631275)=1543475792
matches the wire on bid 654473755566.
2. SeedDeck advances cardTotalNum to deck.Count+1 + pins BattleStartDeckCardList.
Mirrors SBattleLoad.InitPlayer's tail (SBattleLoad.cs:1292). Without it,
skill-generated tokens auto-assigned Index 0,1,... and COLLIDED with deck-loaded
indices 1..40 — silent until something addressed the deck card with the
colliding Index (Hoverboarder at deck idx 1 + a token at engine Index 1 made
GetBattleCardIdx's SingleOrDefault throw on bid 806245601092).
3. BattleCardView.GameObject lazily non-null in the shim (ViewUiTouchStubs.cs).
The IsRecovery card-create delegate (NetworkBattleManagerBase.cs:379) passes
null cardGameObject; Skill_metamorphose.cs:147 in the in-play branch then NRE'd
on `metamorphosedCard.BattleCardView.GameObject.transform.rotation = identity`,
a purely cosmetic touch with no game-state implication. Bid 283192092460:
Petrification on a board follower.
4. TranslateChoiceKeyAction unwraps wrapped selectCard on shadow ingest
(SessionBattleEngine.cs, sibling to TranslateTargetOwners). Live sender-send
wires Choice plays as selectCard:{cardId:[...], open:0}; engine's
ConvertToListInt does `value as List<object>` — a Dict casts to null and
foreach NREs. The receiver's swallow-all catch (NetworkBattleReceiver.cs:1255)
logs to Debug.LogError + LocalLog — both shimmed/no-op'd headlessly — and
returns false, but Receive calls ReceivedMessage with checkBreakData:false so
the false isn't propagated. The play continues with choiceIdList=[], the chosen
branch never resolves, the played card stays in hand; a later targeted play
(A's bounce on B's "board" idx 20) then can't find the target → NRE on null in
ActionProcessor.PlayCard:407. Bid 131549100204: B's Resonance + A's bounce.
Opponent-relay path is unaffected — node strips selectCard from broadcasts.
5. HeadlessHandViewStub overrides HandUnfocus/HandFocus/FocusRearrangeHandHand
to return NullVfx. CreateHandControl returns null in headless; the base
methods unconditionally deref `_handControl.SetHandState(...)`. A follower
with a when_spell_play Heal trigger fired on its leader for amount 0 — even
a 0-heal drives ApplyHealing → CreatePullHandInVfx → HandUnfocus → NRE.
Bid 799755786270: two consecutive spell plays both crashed this stack.
Added InternalsVisibleTo("SVSim.BattleEngine.Tests") so the shim-level
regression tests can pin the no-op contracts directly.
Plus the previous-session fixes carried in this same uncommitted state
(see docs/superpowers/plans/2026-06-07-shadow-engine-desync-handoff.md):
- doesPlayerGoFirst:true + mgr.IsFirst:true (turn-1 draw count correct
per seat)
- RecoveryOperationCollection.PlayHandCardOperation routes all type:30
through PlaySkillSelectHandCardOperation (skips the two-phase user-select
guard that aborts targeted spells in recovery)
- ShadowFeed + ToRawBody: server-generated typed bodies (DealBody, etc.)
converted to RawBody before engine.Receive (`env.Body as RawBody`
returned null for typed bodies)
- Ready idxChangeSeed seeds A's XorShift via the receiver; B's seed is
injected via SeedOppoIdxChange (BattleSeeds.IdxChange + viewerId)
- ReadySpin defaulted to 0 (was 243) — non-zero double-cranks the shadow
which ingests BOTH sides' Ready frames on one stream
Test counts: SVSim.UnitTests 1054/1054, SVSim.BattleEngine.Tests 34/34.
Open: known-residual Unity touches are individual whack-a-mole now (per-card
skill edge cases), not the structural divergences fixed here.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Card 900124030 (ELF cost-3, when_play damage=3 to enemy leader) resolves to
correct authoritative state headless via the IsForecast/IsRecovery +
ActionProcessor.PlayCard path. New oracle dimension (opponent leader-life delta)
passes; 3/3 tests green; engine still 0 errors; check_drift clean.
Four headless gaps, each mechanical (no logic/Unity wall):
- Data seam: InitLeaderLife (SetupInitialGameState->InitializeClassLife subset);
leader BaseMaxLife was 0 => game-over => play silently rejected. M2 missed it
(only asserted leader life unchanged: 0==0).
- Runtime cast: re-attach IClassBattleCardView on the generated
NullClassBattleCardView stub (members already present; base-clause recovery
stripped the decl). Compiled fine -> M1 loop never surfaced it.
- M1 mis-cut: copy NullVfxWithLoading verbatim (its GetInstance() lazy singleton
was stubbed to default!/null). Same pattern as M2 NullCardVfxCreator.
- Card events: CreateHeadlessHandCard now calls SetupCardEvent so a spell's
OnPlay->RemoveSpellCardFromHand / OnFinishWhenPlaySkill->AddSpellCardToCemetery
fire (the bare CreateCardWithoutResources seam skips them).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First green: a zero-skill vanilla follower (100011010, neutral 1/2) resolves to
correct authoritative state HEADLESS via IsForecast/IsRecovery + ActionProcessor.
PlayCard (DP4), no Unity runtime. §5 oracle passes (PP-cost; hand->in-play;
atk/health == CardCSVData base; opponent unchanged; no exception). VERDICT: the
port approach is validated through the resolution path, not just M1's compile path.
VanillaFollowerOracleTests.Vanilla_follower_resolves_to_correct_state — GREEN.
HeadlessCardMaster now loads the follower's real id from cards.json.
Resolution-path shim/engine gaps closed (all mechanical no-op fills or data seams,
never a Unity/logic wall):
- M1 mis-cut copies (DP1/DP3 — pure no-op logic wrongly stubbed to null):
Engine/Wizard.Battle.View.Vfx/NullCardVfxCreator.cs (its GetInstance() singleton
was nulled) + its dep NotEmptyNullVfx.cs. Deleted the generated NullCardVfxCreator
stub + its _IfaceImpl block; both manifested, check_drift clean.
- _IfaceImpl explicit-impl shadow: interface-typed view/mgr calls dispatch to the
explicit impls (which returned default!), shadowing public stubs. Fixed
IBattlePlayerView.GetSideLogControl (SkillProcessor side-log tail) to return a
non-null no-op. KEY M3+ learning: fix _IfaceImpl.g.cs for interface-typed NREs.
(GameMgr/component-model/Resources/IClassBattleCardView shim fills + CardIconControl
copy + the SVSim.BattleEngine.Tests project landed in the prior commit 2b50657.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First green of the M2 go/no-go probe: `new SingleBattleMgr(StandardBattleMgr-
ContentsCreator)` now builds the two-player pair fully headless against the shim,
no Unity runtime. Verdict: headless construction is feasible; every blocker was a
mechanical no-op shim fill or data seam, not a Unity/logic wall.
Shim fills (authored):
- GameMgr: lazy non-null DataMgr/PrefabMgr/InputMgr/SoundMgr/BattleControl.
- GameObject: lazy cached component model so GetComponent<T>/AddComponent<T> return
non-null no-op instances for Component-derived T (F1: unguarded view touches).
- Resources.Load(string): cached non-null GameObject so the prefab->Instantiate->
GetComponent chain (UnityEventAgent) yields a real object.
- ClassBattleCardViewBase: re-attach dropped IClassBattleCardView (no-op members);
ClassBattleCardBase.Setup casts the created view to it.
Engine copy (DP1/DP3 mis-cut fix):
- CardIconControl.cs copied verbatim (manifested) + generated null-stub deleted.
SplitAndCompleteIconStr is pure string logic on the resolution path that M1 had
wrongly stubbed as "View" -> null deref in SkillCreator.CreateBuildInfo.
Test harness (SVSim.BattleEngine.Tests, authored fixture):
- HeadlessContentsCreator/HeadlessPhaseCreator: deterministic replica of the solo
practice init (StandardBattleMgrContentsCreator + SingleBattlePhaseCreator) with
no-op recovery/replay managers.
- HeadlessCardMaster: reflects the loader cards.json dump into CardMaster.
- HeadlessMasterData: minimal Data.Master (class-character list, empty collections)
+ Data.Load + player/enemy chara ids.
- ConstructionProbeTests.SingleBattleMgr_constructs_headless — GREEN.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Final three clusters:
- RoomParamKey: copy Wizard.RoomMatch/RoomParamKey.cs verbatim (UriNames/WatchUriNames
static dicts keyed by PlayerController.ROOM_URI + PlayerControllerForWatching.
SEND_PARAMETER — both now real enums).
- CardChooseTask: copy the TwoPick/CardChooseTask.cs (TaskManager `using`s .Arena.TwoPick,
not .Competition — copy_loop had only landed the Competition twin).
- SetCardNumLabel CS1739: decompiler param-name artifact — the local fn's 3rd param was
recovered as `flag` but call sites pass it named `isRed:`. First DP5 tracked patch:
Engine/UICardList.cs edited (flag->isRed, zero logic change), recorded in
Patches/ + manifest patched=1 (drift-clean).
M1 exit criteria met: `dotnet build SVSim.BattleEngine` = 0 errors, no Unity ref in csproj,
check_drift clean. Session 7: 198 -> 0 across waves 7a-7k.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- VFX: SequentialVfxPlayer.GetAllVfxAsList, ParallelVfxPlayer.GetVfxList,
VfxMgr.CheckAndAddEffectVfxList; point our own ShowBattleUIImmediatelyVfx stub at
NullVfx.GetInstance() (it called a non-existent NullVfx.Create).
- Static factories: SkillTargetSelectTouchProcessor.Create (10-arg),
DialogReportToManagement.Create(long).
- Re-add StartSkillSelectVfx.OnStart event (m1_stub_gen drops `event` decls — the
recurring session-6 gap; generator event-capture fix still pending).
- Stop the BattleCardView/GameObjMgr ctor cascade: parameterless ctors on the no-op
BattleCardView and GameObjMgr hand shims so non-chaining subclass/field stubs satisfy
their implicit base() call.
- Copy Cute/ListExtensions.cs (FisherYatesShuffle extension) verbatim into Engine.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Regenerate 31 VFX/View/UI/Touch stubs to keep their decomp ': base(...)' /
': this(...)' ctor initializers (m1_stub_gen was dropping them -> CS7036/CS1729
when the copied base has no parameterless ctor). Whole base-ctor cluster cleared.
- UnityEngine.Event: add rawType/keyCode/modifiers/Use() + EventType enum (NGUI
UIInput/UIInputOnGUI legacy IMGUI path).
- Reward: copy the real Wizard.Scripts.Network.Data.TaskData.Arena.Reward verbatim
(was an empty ambiguous-name shim in LooseEnds); deps (UserGoods/JsonData) present.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the minimal hand shims partial + generate full member surface for the manager/
task/controller god-objects (LoadingViewManager/DeckUpdateTask/MyPageTask/ReplayController/
PlayerControllerForWatching/WatchDataHandler/EvolutionTouchProcessor/StoryChapterSelection
Utility/NonDialogPopup). NonDialogPopup given MonoBehaviour base + hand Close() removed
(superseded by full surface). LoadTask dup deleted (already copied verbatim). RoomMatch
watch/replay closure types stubbed. Copied 8 more closure files.
CS0246-in-generated-signature masking note: 4 such errors were hiding ~1582 — generated
CS0246 masks as hard as header CS0246; the real frontier is 1586 (CS7036 base-ctor +
member-level), 0 structural.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The stub generator emits net-new types as base-LESS partials, so generated Vfx/View
types weren't actually VfxBase/etc. -> hundreds of CS1503/CS0029 'cannot convert to
VfxBase' at every polymorphic call site. m1_baseclauses.py recovers each generated
type's decomp base CLASS (interfaces dropped to avoid CS0535) into _BaseClauses.g.cs,
cross-namespace bases fully qualified. Generated the intermediate Vfx/processing base
types (SpreadOutVfx/OpenCardVfx/ProcessingBase/DamageVfxBase/ForecastIconVfxBase/...).
DefaultOpeningVfx regenerated WITH override (its base OpeningVfx is copied+abstract).
Clearing the polymorphism cascade + the masking base-type CS0246s unmasked the true
member-level frontier: 2202 (CS1501/CS1061/CS1503), 0 structural errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Generate no-op shells for the entire stop-listed View/Vfx/UI/Touch/Story missing-
type closure (~180 types) + 5 copyable engine files. Net-new shells emitted base-less,
so override members are stripped via the new --no-override generator flag. SDK/BCL
over-reach (Adjust/GZipStream/Socket*) and non-battle Story-world clusters reduced to
minimal/empty stubs instead of full-surface. Nested-type closure (BuildInfo/BattleDialog/
ROOM_URI/FuncGetCantAttackText) placed top-level in their decomp namespaces.
Clearing the last View CS0115 unmasked the true member-level frontier: 3916 errors,
0 generated/structural errors, now dominated by Unity-type + god-object members.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copied the 89 uncopied AI*SimulationUtility/extension files defining the
AIVirtualCard/AIVirtualField extension methods; the compile loop then auto-closed
the revealed type deps (~3049 files total, drift-clean). 10.0k -> 62 errors.
Grows Vector2/3, Mathf, Color, Quaternion, GameObject, Transform, Camera, Material,
ParticleSystem, Rect, Bounds, Time to the surface the engine references; adds Input/
Random/Resources statics + full KeyCode enum. Copies the verbatim extension files that
collapse thousands of CS1061s at once (ContentKeywordExt, JsonData/LitJson extensions,
EventExtension.Call, GameObjectExtension(s)). 26.5k -> 15.9k errors; residual now
dominated by god-object member surface (GameMgr/UIManager/EffectMgr/Vfx*).
Resolves the 268-error header frontier: settings Item base, ErrorDialog.Data,
RoomConnectController nested types, Unity asset/light/collider types, CriWare/
CodeStage/Spine SDK surface, and copies INetworkLogger + SingletonMonoBehaviour
verbatim. Per F3 this unmasks the type bodies (~26.5k member-level errors now
visible) -- the real M1 bulk, attacked in following waves.