The five tutorial gifts every fresh viewer is given at signup used to live as a
static C# array in SVSim.Database/SeedData/TutorialPresents.cs — outside the
seed-JSON pipeline used by all 40+ other globals tables. Editing a gift required
a code change + rebuild instead of a JSON edit + bootstrap re-run.
Now authored in SVSim.Bootstrap/Data/seeds/tutorial-presents.json and loaded into
a new TutorialPresentEntries table via TutorialPresentsImporter (clear-and-rewrite,
mirroring HomeDialogs). ViewerRepository.RegisterAnonymousViewer reads the table
at signup and projects each row into a ViewerPresent with Source="tutorial".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[Timestamp] byte[] doesn't work under SQLite (the test backend) — EF
expects the DB to populate it on insert, but SQLite has no equivalent
of Postgres's xmin. The WHERE Status = Unclaimed filter plus
IInventoryService's viewer-level concurrency is the practical defense;
RowVersion was only a backstop. Regenerated the migration without the
RowVersion column.
Wire reward_type on the gift endpoint uses a gift-specific scheme that
diverges from UserGoodsType for currencies: wire 1 = Crystal (enum=2),
wire 9 = Rupy (enum=9), wire 4 = Item (enum=4). A naked cast resolves
wire 1 to UserGoodsType.RedEther and silently grants the wrong wallet
— restored the explicit WireRewardTypeToUserGoodsType map from the old
tutorial controller.
Retrofits existing GiftControllerTests to call SeedTutorialPresentsAsync
on the new helper (RegisterViewer doesn't auto-seed; only the prod
signup path does). All 7 existing tests pass.
Mirrors banners pattern: clear-and-rewrite from per-table JSON seed.
Ships one entry pointing at parent_gacha_id 80032 to match the
2026-06-03 prod capture.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Walk-down behavior: each call emits the highest-priority unfired
active dialog; subsequent calls walk to the next-priority entry.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Singleton keyed by ShortUdid; lock on per-viewer set to avoid
cross-viewer contention. Process lifetime — restart re-fires.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Window is [begin, end) — exclusive upper bound. Ordered priority-DESC
then Id-ASC so the controller can break on the first match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
DDL-only per migrations-are-ddl-only convention. Seeded by
SVSim.Bootstrap MyPageGlobalsImporter (T5) — no HasData.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reviewer noted the factory may be invoked more than once under contention.
Document the analysis inline so a future reader doesn't have to redo it:
the discarded instance's mutations land on private fields of a soon-unreachable
object, and the only shared sentinel (_noopViewMaterial) is read-only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Follow-up to the multi-instancing migration. Wraps the process-shared engine
statics that aren't ambient-fronted but race between concurrent battles:
- UnityEngine.Resources._loaded: Dictionary -> ConcurrentDictionary.GetOrAdd
(the shared prefab cache keyed by path; concurrent first-misses produced
duplicate GameObjects + Dictionary corruption)
- UnityEngine.GameObject._components: Dictionary -> ConcurrentDictionary with
Interlocked.CompareExchange init (Resources.Load returns SHARED prefab
GameObjects, so two engines' Setup() can race on the same _components map
— surfaced as "Operations that change non-concurrent collections" crashes
during BattleManagerBase ctor's GetComponent<T>() chain)
- Wizard.LocalLog: single static lock around all mutating entry points
(StringBuilder _lastTraceLogStringBuilder + ~12 mutable string/bool/int
scratch fields; serializing the trace-log surface is cheap since logging
is not the hot path)
Flips SVSim.BattleEngine.Tests assembly Parallelizable scope from Self to
Fixtures and restructures MultiInstanceEngineTests.StressN_BaselineMatches so
Setup runs INSIDE Task.Run (was previously serialized as a workaround for the
LocalLog races). The fixture is also lifted to ParallelScope.All so the
two-engines and stress tests can run alongside each other.
Suite fully green under fixture parallelism (59/0/2 across 3 consecutive runs);
SVSim.UnitTests still 1054/0/0 — true multi-instance correctness is now proved
end-to-end in tests rather than gated behind a serial workaround.
Manifest sha refresh + new patch artifact for the LocalLog edit (decomp-origin);
the two shim files are authored, so no metadata update is needed for them.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Step 9 of multi-instancing migration. PowerShell audit fails CI if anything
references the deleted BattleManagerBase.main field or introduces a new
Thread() outside the LeanThreadPool allowlist.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace trivially-true Pp>=0 with concrete post-Setup pins (LeaderLife=20,
Pp=0, HandCount=3). Drop the unused seed parameter from SampleDeck - every
call already returned the same vanilla deck, and the StressN test name 'Random
Decks' overpromised. The cross-contamination property the test pins (parallel
LeaderLife[] equals sequential LeaderLife[]) holds with identical decks +
distinct masterSeeds, which is what's actually being verified.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Step 7 of multi-instancing migration. Residual SVSim.UnitTests that touch
engine code directly are wrapped in TestBattleScope. EngineSessionGate is
deleted along with the _engineOwned bookkeeping in BattleSession; engine
setup is unconditional now that per-battle state is isolated on the ambient.
Gate-specific fallback branches in BattleSession.ShadowIngest are simplified.
Suite fully green (SVSim.UnitTests, SVSim.BattleEngine.Tests).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Step 6 of multi-instancing migration. HeadlessEngineEnv.EnsureInitialized
is split into EnsureProcessGlobals (idempotent, process-once) +
SeedCharaIdsOnCurrentAmbient (per-test). New TestBattleScope IDisposable
sets up a fresh BattleAmbientContext per test. NonParallelizable removed
from converted classes; assembly-level Parallelizable(Fixtures) enabled.
SVSim.BattleEngine.Tests fully green under parallel test execution.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Step 5 of multi-instancing migration. GameMgr.GetIns() now resolves through
BattleAmbient.Require() (throws when no scope active — fail-fast since engine
callers unconditionally dereference). SessionBattleEngine now owns a single
BattleAmbientContext, pushed via BattleAmbient.Enter at Setup/Receive/all
~30 read accessors and Debug* seams.
EngineGlobalInit.WirePerSessionGameMgr extracted out of the _done-gated block:
GameMgr is now per-session (ctx.GameMgr is a fresh `new()` per SessionBattleEngine),
so the DataMgr chara ids + NetworkUserInfoData seeding must run every Setup, not
process-once. The wiring itself is already idempotent. Without this, second-or-
later sessions in a process NRE in NetworkBattleManagerBase.CreateBackgroundId.
Expected state: SVSim.BattleEngine.Tests have known-failing tests that don't
go through SessionBattleEngine (Task 6 wraps HeadlessFixture). SVSim.UnitTests
mostly recover; residual failures (deal-frame Accepted:false in conductor
integration tests) are captured in
data_dumps/task5-test-output/failing-tests-after-task5-node-postwrap.txt for
Task 7.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Setter is the asymmetric one (write-through inside scope, unlike ViewerId's
no-op-in-scope) — adding parity with the SetRealTimeNetworkBattle ambient
setter test to catch future regressions if the routing branch is touched.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Hygiene fixup for the IsForecast/IsRandomDraw ambient conversion in 3b5f2e1.
The manifest sha was stale (pointed at the pre-ambient RNG-virtual-patched
contents) and the change had no companion .patch artifact alongside
BattleManagerBase.rng-virtual.patch. Follow established convention.
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>
Step 1 of the engine multi-instancing migration. Standalone infrastructure;
no engine static reads/writes through it yet. Scope is reentrant (restores
prior on dispose), AsyncLocal flows across awaits, and isolated between
concurrent Task.Run flows.
See docs/superpowers/specs/2026-06-07-engine-multi-instancing-design.md.
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>