Commit Graph

52 Commits

Author SHA1 Message Date
gamer147
ac78e809cd refactor(battle-node): clear residual scripted-bot prose from comments/docs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:52:41 -04:00
gamer147
05d8169012 refactor: type reward_type columns as UserGoodsType enum
Replace bare `int RewardType` on 12 catalog/reward entities and GrantedReward
with the existing UserGoodsType enum. Verified against the decompiled client:
every wire reward_type decodes through the single Wizard.UserGoods.Type enum, so
one enum is correct across all endpoint families (item_type is a separate
Item.Type axis, left untouched). EF stores the enum as the same int column, so
there is no migration.

- Importers cast seed int -> UserGoodsType at the ingest boundary.
- New GrantedReward.ToRewardList() extension replaces 8 copy-pasted
  GrantedReward -> RewardListEntry projections.
- Fix 3 .ToString() sites that would otherwise emit enum names ("Crystal")
  instead of the int wire value ("2").
- Wire DTOs keep int; the enum is widened to int at the wire boundary only.

Build green; 962/962 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:50:49 -04:00
gamer147
24f9b2240e feat(matching): move BotRoster from hardcoded fixture to DB-backed seed
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>
2026-06-02 11:58:19 -04:00
gamer147
b17c802581 feat(config): MatchingConfig section with AI-fallback threshold
Adds a [ConfigSection("Matching")] POCO carrying the
RankBattleAiFallbackThresholdSeconds tunable (default 15). Auto-picked
up by EnsureSeedDataAsync's reflection-based ConfigSection seeder.
Consumed by InProcessPairUp in a later task.

GameConfigurationJsonbTests.EnsureSeedData_writes_one_row_per_ConfigSection_with_ShippedDefaults_payload
updated to include the new Matching section in its expected keys.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 00:54:44 -04:00
gamer147
fc504af496 feat(tk2): weighted-group reward picking
Replaces the all-rows-granted reward model with per-group weighted
pick. Each ArenaTwoPickReward row now belongs to a RewardGroup with a
Weight; finish/retire groups the WinCount's rows by RewardGroup and
picks exactly one row per group, weighted by Weight (excluding
Weight==0). A RewardNum==0 outcome skips both the grant and the
rewards[] emission. Empty WinCount catalogs emit empty arrays.

Existing seed entries preserve deterministic behavior by living in
single-option groups (each with weight 1). Future seasons can expand
groups to multi-option for true randomized rewards (e.g. 200-280
rupies).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 13:44:33 -04:00
gamer147
6381e4da51 fix(tk2): match original SV (5-battle cap, no loss limit)
User clarified: the 0..7 win reward tiers came from Shadowverse Worlds
Beyond (sequel), not the original game we're emulating. Original SV's
Take Two caps at 5 total battles played and has no loss limit (verified
on prod: queueing continues with 2+ losses).

- arena-two-pick-rewards.json: drop 6w + 7w tiers (12 rows remain).
- ArenaTwoPickConfig: remove MaxLosses property.
- ArenaTwoPickService: termination is now battlesPlayed >= maxBattles
  (5 from MAX(reward.WinCount)). RecordBattleResult no longer flips
  IsSelectCompleted on the 2nd loss.
- ResolveMaxBattleCountAsync empty-catalog default 7 → 5.
- Tests updated for the new counts (16 → 12 rows, max 7 → 5).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 12:47:43 -04:00
gamer147
dc19289818 fix(tk2): honor consume_item_type (ticket/crystal/rupy/free) + correct entry ticket id
- ArenaTwoPickConfig: add TicketItemId=1, TicketCost=1, CrystalCost=150, RupyCost=150 scalars
- ArenaTwoPickService.EntryAsync: switch on eARENA_PAY (1/3/4/5); crystal/rupy go through
  ICurrencySpendService.TrySpendAsync; ticket uses item id 1 (challenge ticket, not 80001);
  free entry returns empty reward_list; invalid type throws
- Tests: fix ticket id 80001→1 in entry/e2e; add 4 new path tests; update ctor (10th arg)
  across all 4 service test files; fix e2e retire assertion (reward ticket 80001 post-state=1)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 12:26:57 -04:00
gamer147
f535642109 feat(config): add ArenaTwoPickConfig section 2026-05-31 10:25:40 -04:00
gamer147
6e7f0dc4c9 feat(config): extend ChallengeConfig with TK2 format_info + PoolCardSetIds
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 10:23:08 -04:00
gamer147
1dbc5fa831 feat(db): add ViewerArenaTwoPickRun entity + CandidatePair 2026-05-31 10:16:53 -04:00
gamer147
b32583ef48 feat(db): add ArenaTwoPickReward catalog entity 2026-05-31 10:12:16 -04:00
gamer147
50e4989b77 docs(importers): update data_dumps path references for reorg
Mirror of the outer-repo data_dumps/ reorganization (commit e1e595d in
the SVSim outer repo): updates all data_dumps/extract/ → data_dumps/scripts/,
data_dumps/client_master_csv → data_dumps/client-assets, data_dumps/traffic
→ data_dumps/captures/traffic in XML doc-comments and inline comments
across importers, controllers, middlewares, DTOs, and tests. Doc-only;
no logic changes; build green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 01:22:08 -04:00
gamer147
d66d1d8c6e test(packs): statistical sampler + mark PackRateConfig [Obsolete]
200k-slot statistical test asserts observed tier rates within +/- 0.5pp
of the seeded SV Classic shape (Bronze=76.5 / Silver=16 / Gold=6 /
Legendary=1.5 on general slots). Marked [Category("Slow")].

PackRateConfig is marked [Obsolete] — no longer consulted by
PackOpenService. Internal callers (GameConfigService / DbContext config
seeding / its own tests) still reference it; they'll go when v1
stabilizes and PackRateConfig is fully retired.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 22:46:12 -04:00
gamer147
b78d7d6cbe feat(packs): add PackDraw* tables and IsEnabled column
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>
2026-05-30 21:40:50 -04:00
gamer147
7b5edb7c65 feat(config): add Freeplay config section (default off)
Adds FreeplayConfig [ConfigSection("Freeplay")] with Enabled=false,
CurrencyAmount=99999, CardCopies=3, and ShippedDefaults(). Covered by
FreeplayConfigTests verifying the tier-chain resolves shipped defaults.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 13:25:47 -04:00
gamer147
75a2fca8bb feat(db): StoryDeckEntry presentation table + StoryDeckKind enum
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 10:25:03 -04:00
gamer147
96f1d73e35 docs: fix memory-reference name in gacha-point model docstrings 2026-05-28 22:54:46 -04:00
gamer147
21adc68e28 feat(db): add gacha-point balance + received tables 2026-05-28 22:50:22 -04:00
gamer147
f4f2ec380c feat(envelope): push required_res_ver from ResourceConfig on game_start
A wiped/fresh client (NukeIdentityOnStartup, new install, or any path
that clears PlayerPrefs) defaults its stored RES_VER to "00000000"
per Cute/SavedataManager.GetResourceVersion. The client builds the
Akamai manifest URL as dl/Manifest/<RES_VER>/<lang>/<Platform>/, and
Akamai 404s the "00000000" path -> Toolbox.AssetManager.InitializeManifest
fails -> the title screen shows "Connection Error / Reconnect"
before any tutorial UI loads.

Fix:

- New ResourceConfig [ConfigSection] in SVSim.Database — single
  field RequiredResVer defaulting to "4670rPsPMVlRTd2" (the value
  prod returned in data_dumps/traffic_prod_tutorial.ndjson and was
  still returning at 2026-05-28 21:00 UTC). Lives in GameConfigs so
  it can be tuned via DB / appsettings without code edits.

- ShadowverseTranslationMiddleware injects IGameConfigService and
  emits required_res_ver in data_headers ONLY on /check/game_start
  responses. NetworkTask.Parse opens a "new data is available" popup
  whenever required_res_ver is present and the URL is anything other
  than GameStartCheck (NetworkTask.cs:128-138); the suppression on
  game_start is what lets us silently bump PlayerPrefs["RES_VER"]
  before ResourceDownloader runs.

- DataHeaders gains a nullable RequiredResVer field. DataWrapper.DataHeaders
  is now Dictionary<string, object?> instead of the typed DataHeaders POCO
  directly — the construction site stays type-safe (the middleware builds
  the typed POCO, then projects through the same STJ +
  ConvertJsonTreeToPlainObject pipeline that DataWrapper.Data uses) so
  null-valued optional fields are absent from the wire instead of being
  written as "key":null. Without this, MessagePack's ContractlessStandardResolver
  walked the typed properties and wrote required_res_ver=null on every
  non-game_start response, tripping the popup on every boot.

- GameConfigurationJsonbTests updated to expect the 9th config section.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 18:03:15 -04:00
gamer147
2034034c1b feat(tutorial): add /tutorial/gift_top with hardcoded starter present list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 12:02:54 -04:00
gamer147
6e106d646b models(card): MaxCopies constant on OwnedCardEntry 2026-05-28 00:56:20 -04:00
gamer147
7ef5f03eb3 feat(spot-card-exchange): /spot_card_exchange/{top,exchange} + SpotPoints currency
Final shop family. Schema additions:
- ViewerCurrency.SpotPoints (ulong) — new currency column on Viewers.
- SpotCardExchangeEntry — catalog (distinct from the pre-existing
  SpotCardEntry, which is the /load/index rental-cost concept).
- ViewerSpotCardExchange — standalone composite-PK table tracking
  (viewer, card, exchanged_at, is_pre_release_snapshot). Standalone
  avoids cartesian-explode on viewer-graph reads.

RewardGrantService gains a SpotCardPoint=12 currency case mirroring
the RedEther/Crystal pattern. Doc comment refreshed; SpotCard=11 and
SpotCardOnlyLatestCardPack=13 remain unimplemented with explanatory
NotSupportedException — captures show emitters always use Card=5 with
the spot-card-specific id.

Controller:
- /top: emits exactly 9 clan buckets [{"1": [cards]}, ...] matching
  prod's arbitrary single-key shape. exchange_status per-card (0=
  available, 1=already-exchanged, 2=LimitOver after pre-release cap).
  pre_relase_info WIRE TYPO PRESERVED ("relase" not "release").
- /exchange: server-authoritative price (client-supplied
  exchange_point ignored); debits SpotPoints with post-state-total
  reward_list entry; grants card via RewardGrantService.ApplyAsync
  (cosmetic cascade included); persists ViewerSpotCardExchange row.
  Insufficient points / already-exchanged / pre-release-limit all
  return 400 without partial state.

LoadController now populates /load/index spot_point from
viewer.Currency.SpotPoints (was always 0).

PreReleaseLimit hardcoded to 2 matching capture; promote to GameConfig
when captures show variance.

504 tests pass (was 496; +8 spot-card-exchange tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 23:23:07 -04:00
gamer147
a5999a3e9c feat(leader-skin): shop catalog + 5 endpoints (/products, /buy, /buy_set, /buy_set_item, /ids)
Schema: LeaderSkinShopSeries -> Products (owned rewards) + owned
SetCompletionRewards on the series; ViewerLeaderSkinSetClaim composite
PK (ViewerId, SeriesId) backs the /buy_set_item idempotent-claim check.

Importer mirrors SleeveShopImporter: idempotent find-or-create, owned
collections rewritten wholesale on rerun. 16 series, 104 products.

Controller (extends existing /set with 5 new endpoints):
- /products: dict-keyed-by-series_id-string wire shape. is_completed
  per-viewer, rewards.status from ViewerLeaderSkinSetClaim (0=no set
  sale, 1=available, 2=claimed) matching client RewardStatus enum.
- /buy: single skin, sales_type 1/2 dispatch, 3=>501.
- /buy_set: whole series at SetPrice; requires set_sales_status != 0;
  grants every product's rewards (RewardGrantService idempotent on
  already-owned cosmetics, so partial-set buys don't double-add).
- /buy_set_item: requires viewer owns every skin in series; idempotent
  on re-claim (returns 200 + empty reward_list, not 400) so client
  retries don't error.
- /ids: flat owned-skin-id list for badge refresh.

496 tests pass (was 486; +10 leader-skin-shop tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:55:09 -04:00
gamer147
559a170957 feat(item-purchase): /item_purchase/{info,purchase} + catalog
Schema: ItemPurchaseCatalogEntry (single table). Per-viewer quota tracked
via existing ViewerEventCounter keyed by "item_purchase:<id>" with period
JstPeriod.MonthKey when IsMonthlyReset else AllTime.

Controller:
- /info returns catalog + per-period rest (server-computed
  max(0, PurchaseLimit - counter)) + user_card_pack_ticket_list (every
  Items.Type==2 row joined to viewer count, zeros included — client
  unconditionally UpdateItemNum's each entry).
- /purchase: sold_out check before currency check (no counter increment
  on currency failure), inline TryDebit covers RedEther/Crystal/Rupy/Item
  with post-state-total reward_list entry, grant via RewardGrantService.
  Request `rest` accepted but ignored (server counter is canonical).

Importer mirrors PaymentItemImporter shape — idempotent find-or-create,
seed-missing rows preserved. 3 entries from the prod capture.

486 tests pass (was 476; +10 item_purchase tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:41:02 -04:00
gamer147
f237851e42 feat(sleeve): shop catalog + /sleeve/{info,buy} endpoints
Schema: SleeveShopSeries -> SleeveShopProducts -> Rewards (owned).
Migration AddSleeveShop creates 3 tables with FK cascade.

Importer mirrors BuildDeck pattern: find-or-create per series/product,
rewards replaced wholesale on rerun (owned collection). 10 series,
270 products imported from seeds/sleeve-shop.json.

Controller:
- /sleeve/info returns wire-faithful dict-keyed shape
  ({sleeve_list: {<series_id>: {product_info: {<product_id>: ...}}}}).
  is_purchased_product derived from viewer.Sleeves.Contains(sleeve_id).
- /sleeve/buy: sales_type 0=free / 1=crystal / 2=rupy / 3=ticket(501).
  Validates series_product mismatch, currency, already-purchased.
  Currency debited with post-state-total reward_list entry; cosmetic
  grants dispatched through RewardGrantService.ApplyAsync (covers
  sleeve + emblem bundled grants per product).

476 tests pass (was 466; +10 sleeve tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:09:45 -04:00
gamer147
6a03ff1bf6 feat(items): catalog import with Type + ThumbnailPath columns
ItemEntry gains Type (client item_type enum, 1=challenge, 2=card-pack
ticket, 3=premium orb, 4=colosseum, 5=orb piece, 6=skin/event ticket,
7=other) and ThumbnailPath. ItemImporter mirrors PaymentItemImporter
shape: find-or-create per item_id, save once, idempotent. Wired into
Bootstrap.Program and SVSimTestFactory.SeedGlobalsAsync. Unblocks
/item_purchase/info (filters card-pack tickets by Type==2) and any
reward grant of UserGoodsType.Item, which previously threw because
the catalog was empty.

466 tests pass (was 461).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 21:44:24 -04:00
gamer147
dffd7a9746 db: add nullable Viewer.Udid with partial unique index
Backstop for /tool/signup idempotency: signup-created viewers carry
the client's UDID (the AES key for that client's wire traffic);
admin-imported viewers stay null. Partial unique index allows the
column to coexist with pre-existing null rows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 13:54:15 -04:00
gamer147
062adefb99 feat(missions): add 3 viewer entities + Viewer collections
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:08:11 -04:00
gamer147
9988ed85df feat(missions): add 3 catalog entities (mission, achievement, BP monthly) 2026-05-27 10:01:27 -04:00
gamer147
faa8c0e6dd feat(bp): add ViewerBattlePassProgressEntry + ViewerBattlePassClaimEntry 2026-05-26 22:07:05 -04:00
gamer147
34de3d53ad feat(bp): add BattlePassSeasonEntry + BattlePassRewardEntry + BattlePassTrack enum 2026-05-26 22:04:10 -04:00
gamer147
95b8f39ea5 refactor(bp): flatten BattlePassLevelEntry — drop misnamed RewardData jsonb
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:57:47 -04:00
gamer147
141f34f817 chore(bootstrap): refresh stale GlobalsImporter references in docs/test names 2026-05-26 16:44:54 -04:00
gamer147
d14a0be2c8 refactor(bootstrap): finalize load-index migration; GlobalsImporter is now a stub
Stage 9C of the bootstrap-seed-refactor:

- Add 6 seed DTOs for the card-id-keyed load-index tables (SpotCard,
  ReprintedCard, UnlimitedRestriction, LoadingExclusionCard, MaintenanceCard,
  FeatureMaintenance).
- Add CardListsImporter: idempotent upsert of the 6 tables, sharing one
  Cards FK set for orphan-warning. FeatureMaintenances clear-and-rewrites
  (synthetic ordinal Id; no natural key).
- Add RotationFlagUpdater: reads RotationConfig.RotationCardSetIds from the
  GameConfigs section (populated by RotationConfigImporter) and flips
  CardSet.IsInRotation to match.
- Add RotationConfig.RotationCardSetIds list property + wire it through
  RotationConfigImporter. No migration needed (sections are JSON blobs).
- RotationConfigImporter: use legacy local-kind DateTime parse for schedule
  windows so the JSON round-trip stays byte-equivalent to GlobalsImporter.
- Strip GlobalsImporter down to a no-op stub (Task 10 will delete it).
- Wire all 9 new importers into Program.cs and SVSimTestFactory.SeedGlobalsAsync,
  in the order RotationConfigImporter -> ... -> CardListsImporter -> RotationFlagUpdater.
- Delete prod-captures/load-index-2026-05-23.json.
- Add CardListsImporterTests covering each sub-table, idempotency,
  empty-seed handling, orphan-warning, and the clear-and-rewrite path.

Tests: 391 passing (382 baseline + 9 new).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 15:46:36 -04:00
gamer147
9090086a47 Class leader fixes 2026-05-26 10:01:37 -04:00
gamer147
b6966ece6e Prebuilt deck purchasing and fixes 2026-05-26 09:16:21 -04:00
gamer147
5e7a65fe5a Story 2026-05-25 14:36:12 -04:00
gamer147
558e8288eb Puzzles 2026-05-25 12:03:47 -04:00
gamer147
c14408ba06 Seeding reorg 2026-05-24 21:13:15 -04:00
gamer147
34bcc579a5 Additional card content 2026-05-24 17:07:05 -04:00
gamer147
d9ef9fe1fc Pack logic cleanup 2026-05-24 09:27:10 -04:00
gamer147
79209bd70b Pack opening 2026-05-24 02:03:13 -04:00
gamer147
21b97269ff Practice battles work 2026-05-23 22:46:11 -04:00
gamer147
d3b2970e11 Deck list work 2026-05-23 19:57:34 -04:00
gamer147
56d3cf0ec8 Seeding updated 2026-05-23 16:25:49 -04:00
gamer147
bf6ddf5428 Forgot unversioned xd 2026-05-23 14:18:18 -04:00
gamer147
6b70850b7b More features 2026-05-23 14:18:01 -04:00
gamer147
b2024af852 Lots of data and model setup 2025-05-18 02:27:17 -04:00
gamer147
79505e0c1a DTOs for index mostly done, doing DB models 2024-09-12 00:35:31 -04:00
gamer147
ac3b002d74 Stuff works 2024-09-08 10:27:12 -04:00