C1: replay_detail — flatten stored payload to data level. ChatReplayDetailTask.Parse() calls
new ReplayDetailInfo(data) which unguardedly accesses data[battleId], data[seed], data[vid1],
etc. Wrapping under replay_info key crashes the client. Controller now returns Ok(JsonElement)
directly so stored battle fields are at data root. Wire-shape test added.
C2: Guild.LeaderViewerId long to long?; add HasOne<Viewer> FK with OnDelete=SetNull; migration
AddGuildLeaderViewerIdFk; all consumers null-guarded with ?? 0L.
C3: BreakupAsync — wrap 6 destructive ops in IDbContextTransaction with InMemory fallback.
C4: CommitJoinAsync — wrap member-add + viewer-guildId-set + invite/request cleanup in
IDbContextTransaction with InMemory fallback. Chat event emitted after commit.
I1: GuildController — remove SVSimDbContext field; inject IViewerRepository + IGuildMemberRepository.
Add IGuildMemberRepository.GetViewerIdsInAGuildAsync (batch guild-membership check).
All _db.Viewers / _db.GuildMembers queries replaced with repo calls.
I2: GuildService — extract 3 _db.Viewers queries: GetEquippedEmblemIdAsync (CreateAsync),
LoadGuildProfileBatchAsync (ListOutgoingInvitesAsync + ListPendingJoinRequestsForMyGuildAsync).
Add GuildMemberProfile record with IsOfficialMarkDisplayed. GetEmblemListAsync for EmblemList.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Partial index (filter: "Status" = 0) replaces the unconditional unique
index on (GuildId, InviteeViewerId). Old Rejected/Canceled rows no
longer conflict when a new Pending invite is inserted for the same pair.
Adds regression tests: Reinvite_after_reject_succeeds and
Reinvite_after_cancel_succeeds.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The captured shape ({"1":[], "3":[], "4":[]}) is the empty-period
wire echo; the client parser (LoadDetail.cs:553) only reads normal/total/
campaign. Table, entity, seed, and importer were never wired to a code path.
Replaced by config-section catalog + service in the follow-up commits.
/mypage/index data.colosseum_info and /arena_colosseum/{top,get_fee_info}
now both project through ColosseumLobbyInfoBuilder.Build(ColosseumSeasonConfig).
One source of truth for the home-tab gating and the lobby reads — admins
flip the season on by writing to GameConfigs (per the runbook), no second
table to keep in sync.
* New ColosseumLobbyInfoBuilder — static projection from ColosseumSeasonConfig
to the wire ColosseumLobbyInfo. Replaces ArenaColosseumController's private
BuildColosseumInfo and MyPageController's BuildColosseumInfo(ColosseumConfig).
* MyPageController reads ColosseumSeasonConfig via IGameConfigService; the
IGlobalsRepository.GetCurrentColosseum() call goes away.
* MyPageIndexResponse.ColosseumInfo retyped to the new ColosseumLobbyInfo.
* ColosseumSalesPeriodInfo relocated into the ArenaColosseum namespace
alongside the rest of the family DTOs.
* Drops: Colosseums DB table (migration DropColosseumsTable), ColosseumConfig
entity, the captured-shape Models.Dtos.ColosseumInfo DTO, ColosseumSeed,
MyPageGlobalsImporter.ImportColosseumAsync + seed file colosseum.json,
IGlobalsRepository.GetCurrentColosseum + impl, the two related tests.
Net: home-screen Grand Prix tab now hidden by default because the season
ships with IsColosseumPeriod=false. Full suite: 1345/1345 (was 1347 — the
2 dropped tests covered the now-dead surface).
Closes the family. arena-colosseum 10/16 → 15/16 zero stubs (the 16th —
finish_load — is dead per project_dead_battle_endpoints).
* 2-Pick draft lift onto ArenaColosseumController:
- get_candidate_classes samples from ArenaTwoPickConfig.AllowedClassIds
and persists the slate onto the run.
- class_choose accepts class_id XOR chaos_id; both populate run.ClassId,
chaos branch stores ChaosId for replay.
- get_candidate_cards is the idempotent draft-resume snapshot.
- card_choose appends both cards from the picked pair, advances turn 1..15.
- Pool override: ArenaTwoPickCardPoolService gets a non-breaking
GeneratePickSetsForTurn(..., poolCardSetIds) overload; Colosseum routes
pass ColosseumSeasonConfig.PoolCardSetIds (falls back to challenge →
rotation when empty).
* Curated-deck schema: ColosseumHofDeck / ColosseumWindFallDeck /
ColosseumAvatarDeck — three identical tables (separate per per-pool
operational lifecycle), unique on DeckNo. Migration AddColosseumCuratedDecks.
* IColosseumCuratedDeck interface + a generic ColosseumCuratedDeckImporterBase<T>
with three concrete subclasses (HOF / WindFall / Avatar), registered in
Bootstrap. Seed files ship empty.
* 6 curated endpoints on ArenaColosseumController: get_{hof|windfall|avatar}_deck_list
return BARE-ARRAY data per spec; register_{hof|windfall|avatar}_deck share
one generic RegisterCuratedAsync<T> dispatcher. Cross-pool register
rejected via per-pool lookup. Curated register has no is_published flag
(constructed-only) — clears the run's flag for state consistency.
* Tests: 5 draft HTTP tests + 11 curated-deck HTTP tests (4 parameterized
3 ways across HOF/WindFall/Avatar + a cross-pool isolation test + an
is_published clear test). Existing TwoPick service tests updated for the
new pool overload. Full suite: 1347/1347.
Phase 3 ship gate met. Branch ready for merge.
New table backs /replay/info; composite PK (ViewerId, BattleId), index on
(ViewerId, CreateTime) for the newest-first list query. 50-row per-viewer
retention enforced by BattleHistoryWriter (next commit).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Lays the persistence foundation for the /friend/* API surface. Three new
model classes with composite PKs / unique constraints / FK cascades registered
on SVSimDbContext; 4/4 persistence tests pass on SQLite in-memory.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three new EF entities for /campaign/regist_serial_code: SerialCodeEntry (code, message,
window, enabled flag), SerialCodeRewardEntry (FK child, per-slot reward), and
ViewerSerialCodeRedemption (composite-PK redemption record). Registered in SVSimDbContext
with unique index on Code and cascade FK constraints. 3/3 persistence tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds AddViewerMyPageBgSelection migration: two int scalars on Viewers
(MyPageBgId, MyPageBgSelectType default 0) and ViewerMyPageBgRotation
owned table with composite PK (ViewerId, Slot), FK cascade to Viewers.
Also adds ToTable(ViewerMyPageBgRotation) to OwnsMany config so EF
uses the correct table name instead of defaulting to the entity class.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds BGType persistence (0=Deck/1=CustomBG/2=RandomBG) to Viewer via two scalar
columns and an owned collection keyed (ViewerId, Slot). Two persistence tests
confirm round-trip and zero-defaults on fresh viewers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the ViewerAcquireHistoryEntry model (8 fields: Id, ViewerId,
RewardType, RewardDetailId, RewardCount, AcquireType, Message,
AcquireTime), registers DbSet<ViewerAcquireHistoryEntry> on
SVSimDbContext, configures model (PK, FK cascade to Viewer, MaxLength
64 on Message, composite index on ViewerId/AcquireTime/Id), and adds a
DbSet round-trip integration test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
Phase 3 shipped the AI rank battle bot pool as a hardcoded 8-entry list
inlined in SVSim.EmulatedEntrypoint/Matching/BotRoster.cs — editing meant
recompiling. Per PLAN.md 2026-06-02 item (d), move it to a Bootstrap
importer so the roster lives in seeds/bot-roster.json and the DB.
Shape mirrors PracticeOpponent end-to-end:
- BotRosterEntry (SVSim.Database/Models) — PK = AiId via the Id passthrough
pattern. DbSet<BotRosterEntry> BotRoster on SVSimDbContext.
- AddBotRoster migration (DDL only, per migrations-are-DDL-only rule).
- seeds/bot-roster.json — 8 rows preserving the current prod-verified
cosmetic ids (sleeve 704141010 / emblem 400001100 / degree 120027 /
field 5) and series-1 ai_ids from rm_ai_setting.csv (1111..1181).
- BotRosterSeed POCO + BotRosterImporter (idempotent upsert keyed by AiId,
leaves seed-missing rows intact). Wired into SVSim.Bootstrap/Program.cs
next to PracticeOpponentImporter.
- IGlobalsRepository.GetBotRoster() + impl.
IBotRoster.Pick → PickAsync because BotRoster now depends on the transient
IGlobalsRepository. RankBattleController awaits the new signature. The
deterministic hash-on-ctx invariant (same ctx → same bot, so /ai_<fmt>/start
retries pick the same opponent) is preserved.
DI: AddSingleton<IBotRoster> → AddTransient (matches IGlobalsRepository's
lifetime). Test fixture's SeedGlobalsAsync also runs the importer so
RankBattleControllerTests + the rewritten BotRosterTests both see seeded
rows.
Tests: 931 → 936 passing. Existing 3 BotRosterTests reshaped for the DB
backing + 1 new "throws on empty roster" guard; 4 new
BotRosterImporterTests mirror PracticeOpponentImporterTests
(round-trip / idempotent / seed-missing-row-intact / ai_id=0 skip).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Register ArenaTwoPickRewards and ViewerArenaTwoPickRuns DbSets in SVSimDbContext and generate the AddArenaTwoPick migration with both CreateTable calls, all four jsonb columns on the run table, and the correct indexes (WinCount scalar + unique WinCount/RewardType/RewardId on rewards; unique ViewerId on runs).
Three new EF entities and a migration:
- PackDrawConfigEntry (per-pack: animation rate, has-bonus flag, special-kind label)
- PackDrawSlotRateEntry (pack/slot/tier -> rate, unique index)
- PackDrawCardWeightEntry (per-card-rate facts incl rate-less rows)
DrawSlot {General, Eighth, Bonus} and DrawTier {Bronze, Silver, Gold, Legendary, Special}.
Special collapses leader_card and limited_time_leader (verified mutually exclusive per pack).
IsEnabled column on PackConfigEntry — admin gate for synthesized stubs, distinct from
the wire-mirror IsHide.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
(1) RegisterAnonymousViewer now catches the unique-violation
race (SQLSTATE 23505 on Postgres / code 19 on SQLite) and
re-reads by UDID, returning the existing row instead of
surfacing 500 to the second concurrent /tool/signup caller.
New repo test exercises the back-to-back register path.
(2) Add unique index on SocialAccountConnection (AccountType,
AccountId). The auth handler's find-or-link path claimed
this index existed as the dedup backstop; the claim was
accurate as design intent but the schema was missing. Now
matched. Comment in handler updated.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add ValueGeneratedOnAdd to ViewerBattlePassProgress.Id and
ViewerBattlePassClaims.Id so Postgres generates IDENTITY values at
runtime. Regenerate AddBattlePass migration in-place to include the
IdentityByDefaultColumn annotations. Add IBattlePassRepository /
BattlePassRepository (season lookup + level-curve cache) and
IViewerBattlePassRepository / ViewerBattlePassRepository
(get-or-create progress, claim reads/writes).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds DbSets and OnModelCreating config for BattlePassSeasonEntry,
BattlePassRewardEntry, ViewerBattlePassProgressEntry, and
ViewerBattlePassClaimEntry; generates migration 20260527021011_AddBattlePass
with DDL-only CreateTable + CreateIndex calls and no InsertData.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>