Commit Graph

761 Commits

Author SHA1 Message Date
gamer147
b18b24b085 test(ranking): literal-prod-JSON wire-shape parity
Per feedback_wire_shape_tests: controller round-trip tests using the
same DTO can't catch wire-key/wire-type drift. Asserts parsing of and
snake_case emission for the period list + monthly ranking shapes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 10:48:51 -04:00
gamer147
f743b27696 feat(ranking): stub /ranking/* (6 endpoints)
Rankings menu opens. Period picker shows deterministic monthly schedule.
Every leaderboard returns { period, ranking: [] }.

Endpoints:
- /ranking/get_viewable_ranking_period_list
- /ranking/master_point_{rotation,unlimited}_info
- /ranking/rank_match_class_win_{rotation,unlimited}_info
- /ranking/two_pick_win_info

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 10:46:00 -04:00
gamer147
80f249f8a2 feat(ranking): add RankingPeriodSchedule helper
Pure deterministic monthly period generator for the four ranking
families. Anchor dates derived from prod capture (2026-06-09): id=1 is
each family's launch month in JST; id=N is anchor + N-1 months. Used
by /ranking/get_viewable_ranking_period_list to render the period
picker and by per-family leaderboard endpoints to echo the requested
period back.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 10:36:51 -04:00
gamer147
b4aa07577f fix(friend): add BaseRequest body param to 6 body-less actions
ShadowverseTranslationMiddleware throws InvalidOperationException when
a Unity client posts an encrypted msgpack body to an action with zero
[FromBody] parameters — it has no target type for the deserializer.
Tests pass because they post JSON directly with no UnityPlayer UA and
the middleware short-circuits. Same defect already fixed on /replay/info
in 216dcab; this catches up the friend system shipped 2026-06-09.

Fixed actions: info, receive_apply_info, send_apply_info,
played_together_info, reject_apply_all, cancel_apply_all.

Tests updated to post the BaseRequest auth fields so [ApiController]
model validation passes (BaseRequest.ViewerId is non-nullable string).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 08:54:50 -04:00
gamer147
b54a47f333 test(replay): literal-prod-JSON wire-shape parity for /replay/info
Per feedback_wire_shape_tests (2026-05-28): controller round-trip tests
using the same DTO can't catch wire-key/wire-type drift. Asserts
parsing of, snake_case emission for, and MessagePack round-trip
through the captured prod frame.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 08:24:24 -04:00
gamer147
0996074287 feat(replay): wire arena two-pick finish hook
Same pattern as rank-battle: DoMatching stashes context; Finish takes
it and records history + played-together. Opponent identity is left
as placeholder fields until the resolver carries it through.

Test seeds an active ViewerArenaTwoPickRun so RecordBattleResultAsync
does not throw no_active_run during the e2e flow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 08:22:19 -04:00
gamer147
81aac701f4 feat(replay): wire finish hook for rank-battle family
Finish now consumes the stashed BattleContext, records a
ViewerBattleHistory row (idempotent + retention-capped), and
calls IPlayedTogetherWriter for human PvP (skipped for AI).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 08:15:12 -04:00
gamer147
a81289311f feat(replay): stash battle context at /ai_*/start time
AiStartInternal now writes a BattleContext keyed by viewer id; the next
commit consumes it in /finish to write a ViewerBattleHistory row.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 08:10:01 -04:00
gamer147
b44354315a fix(replay-tests): supply BaseRequest auth fields in PostAsJsonAsync bodies
Following the 216dcab fix that added [FromBody] BaseRequest _ to the
Info action, the existing tests' empty new {} payloads no longer satisfy
[ApiController] model validation (BaseRequest.ViewerId is non-nullable
string). Use the same EmptyBody() shape as RankBattleControllerTests
to mirror the production wire.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 08:06:36 -04:00
gamer147
216dcab316 fix(replay): add BaseRequest body param to /replay/info
The translation middleware throws InvalidOperationException when an
action has no body parameters and a Unity client sends an encrypted
msgpack body. Tests pass without it because they post JSON directly
and bypass the middleware (no UnityPlayer User-Agent).

See RankBattleController.ForceFinish for the documented convention.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 08:03:37 -04:00
gamer147
c6259e5a14 feat(replay): add ReplayController for /replay/{info,detail}
/replay/info reads from ReplayHistoryReader, newest-first, capped at 50.
/replay/detail returns 400 with result_code=99 - local cache is the
canonical playback source so this endpoint is cache-miss fallback only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 07:59:51 -04:00
gamer147
2f7a2305da feat(replay): add /replay/info and /replay/detail DTOs
All numeric fields on ReplayInfoItemDto ship as string — matches prod
capture (frame 96 of traffic_prod_misc_clicking.ndjson). api-spec doc
will be updated separately.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 07:51:00 -04:00
gamer147
86d86f6ead feat(replay): add ReplayHistoryReader for newest-first list query
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 07:48:18 -04:00
gamer147
2b6c7bd6a4 feat(replay): add BattleHistoryWriter with 50-row per-viewer retention
Idempotent on (ViewerId, BattleId); evicts oldest CreateTime row when
at cap. No-op when ctx is null (server-restart safety).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 07:43:50 -04:00
gamer147
869f9ce13d feat(replay): add BattleContextStore for start->finish handoff
Bridges the start-time -> finish-time gap. /finish carries neither
battle_id nor opponent identity; this store holds both for the finish
handler to compose a ViewerBattleHistory row.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 07:38:37 -04:00
gamer147
0bb0f46abc feat(replay): add ViewerBattleHistory entity + migration
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>
2026-06-10 07:32:05 -04:00
gamer147
2d65fcd91c test(friend): end-to-end multi-viewer apply→approve→friendship flow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 22:14:45 -04:00
gamer147
d848f4a03f test(friend): literal wire-shape round-trip
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 22:13:16 -04:00
gamer147
2d13f0b72d feat(friend): FriendController with 12 endpoints + integration tests 2026-06-09 22:12:00 -04:00
gamer147
a6e5c9f0bc feat(friend): wire DTOs for /friend/* endpoints
12 files: 3 entry types (FriendEntryDto, FriendApplyEntryDto,
PlayedTogetherEntryDto), 5 response wrappers, 4 request DTOs.
All carry [MessagePackObject] + [Key] + [JsonPropertyName] per convention.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 22:06:36 -04:00
gamer147
b5b4781693 feat(friend): bulk apply ops + IPlayedTogetherWriter with retention cap
Implements RejectAllAppliesAsync, CancelAllAppliesAsync (ExecuteDelete bulk
deletes on incoming/outgoing applies respectively) and RecordAsync (upsert
played-together row with 50-row per-viewer retention eviction). 4 new tests
added; all 1186 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 22:04:04 -04:00
gamer147
17591a6ebd feat(friend): implement single-target write methods on FriendService
SendApplyAsync, ApproveApplyAsync, RejectApplyAsync, CancelApplyAsync,
RejectFriendAsync all implemented. 11 new tests appended; all 23 friend
tests pass, full suite 1182/1182 green.
2026-06-09 21:59:32 -04:00
gamer147
d078f275f8 feat(friend): implement 5 read methods on FriendService + register DI + read test suite
GetFriendsAsync, GetReceiveAppliesAsync, GetSendAppliesAsync, GetPlayedTogetherAsync,
SearchAsync all implemented. LoadViewerProjectionAsync materialises the full Viewer
entity (with Include/ThenInclude for SelectedEmblem/Degree) then projects in-memory —
avoids the EF Core limitation where Include is silently ignored under Select.
FriendService + IPlayedTogetherWriter registered as Scoped in Program.cs.
12 read tests, all green; full suite 1171/1171 still passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 21:53:10 -04:00
gamer147
300eee36e9 feat(friend): IFriendService + IPlayedTogetherWriter + DTO records
Task 3: service contract (interface + DTOs) and FriendService skeleton.
All methods throw NotImplementedException; Tasks 4-6 fill in the logic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 21:44:51 -04:00
gamer147
f40ecb8ca7 feat(friend): migration for friend system tables
Generates AddFriendSystemTables migration: ViewerFriends (composite PK,
2 FK cascades), ViewerFriendApplies (int identity PK, unique on
From/To, 2 FK cascades), ViewerPlayedTogethers (composite PK, Owner FK
only). Includes composite/unique indexes. 4/4 FriendPersistenceTests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 21:42:41 -04:00
gamer147
1813217c16 feat(friend): add ViewerFriend + ViewerFriendApply + ViewerPlayedTogether entities
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>
2026-06-09 21:40:08 -04:00
gamer147
c1eec9057a test(gift): claim Card/Sleeve presents; reject unsupported types
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 20:58:53 -04:00
gamer147
742058403c refactor(campaign): delegate gift-reward-type check to GiftRewardTypes
Delete local IsSupportedGiftRewardType and replace its single call site with
GiftRewardTypes.IsSupported — Card (5) and Sleeve (6) are now accepted.
Update unsupported-type test sentinel from 5 (Card) to 11 (SpotCard).
Add Card and Sleeve success-path tests; full suite 1152/1152.
2026-06-09 20:52:47 -04:00
gamer147
b2a5b69423 fix(gift): wire reward_type is UserGoodsType integer, not legacy 1/4/9 encoding
Replace WireRewardTypeToUserGoodsType switch with a validating identity cast backed
by GiftRewardTypes.IsSupported. Wire type 1 is RedEther (UserGoodsType.RedEther),
not Crystal (UserGoodsType.Crystal=2); the old switch silently granted the wrong
wallet for every tutorial-completion claim. Update all 5 GiftControllerTests assertions
and 1 TutorialFlowEndToEndTests assertion to expect RedEther instead of Crystals.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 20:45:49 -04:00
gamer147
366a71688d feat(gift): add GiftRewardTypes supported-set helper
Centralizes which UserGoodsType values IInventoryTransaction.GrantAsync
can handle in the gift-inbox flow; both GiftController (post-fix) and
CampaignController will consult this instead of duplicating the set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 20:39:10 -04:00
gamer147
0d32d2167b feat(campaign): CampaignController.RegisterSerialCode + DTOs + tests
Adds POST /campaign/regist_serial_code with 9 integration tests covering
success path, all disqualifier conditions (unknown/disabled/expired/future/
already-redeemed/unsupported-reward-type), 401 on missing auth, and case-
sensitivity. IsSupportedGiftRewardType uses gift-wire literals (1/4/9) not
UserGoodsType enum values, matching GiftController.WireRewardTypeToUserGoodsType.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 18:51:03 -04:00
gamer147
9f7e78691b feat(serial-code): migration for SerialCode tables
Adds AddSerialCodeTables migration: SerialCodes, SerialCodeRewards,
ViewerSerialCodeRedemptions with FKs, cascade deletes, unique index
on Code, and composite index on (SerialCodeId, Slot).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 18:45:18 -04:00
gamer147
206be77a86 feat(serial-code): add SerialCode + SerialCodeReward + ViewerSerialCodeRedemption entities
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>
2026-06-09 18:42:10 -04:00
gamer147
b117fe825c test(profile): literal wire-shape round-trip for UserClass
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 17:41:11 -04:00
gamer147
11215bd69f feat(profile): ProfileController + DTOs + integration tests
Add /profile/index endpoint that returns user_rank_match_total_win (stubbed 0)
and user_class_list built from viewer Classes + owned LeaderSkins. Six NUnit
integration tests cover zero wins, all classes present, level/exp/default skin,
leader_skin_id_list population, is_random_leader_skin round-trip, and 401 on
unauthenticated access.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 17:37:35 -04:00
gamer147
f204656f4d refactor(user-class): require owned skin list, read IsRandomLeaderSkin from data 2026-06-09 17:31:06 -04:00
gamer147
da6b448598 feat(viewer): migration for ViewerClassData.IsRandomLeaderSkin
Adds AddViewerClassDataIsRandomLeaderSkin migration: boolean column on
the ViewerClassData shadow table, nullable: false, defaultValue: false.
Also updates the model snapshot to reflect the new bool property.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 17:27:16 -04:00
gamer147
2f4420bf15 feat(viewer): add ViewerClassData.IsRandomLeaderSkin column
Adds the bool column (defaults false) to the [Owned] ViewerClassData
entity and a two-test persistence suite that verifies round-trip and
default-value behaviour via SQLite/EnsureCreated.
2026-06-09 17:24:32 -04:00
gamer147
b50d69d3a5 test(user-mypage): assert /user_mypage/update returns 401 without auth
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 17:04:12 -04:00
gamer147
483cc1c1e0 feat(mypage): /mypage/index reflects persisted bg selection
Wires MyPageController.Index to read Viewer.MyPageBgId/SelectType/BgRotation
instead of emitting an empty MyPageBgSetting placeholder; adds Include for
MyPageBgRotation in ViewerRepository.GetViewerByShortUdid; adds fresh-viewer
zero-defaults test and write→read round-trip test (9/9 controller tests pass).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 16:53:34 -04:00
gamer147
6123a64b37 test(user-mypage): explain Clear() + cover unparseable rotation entries
Add a comment above MyPageBgRotation.Clear() explaining the EF OwnsMany
delete-on-clear semantics. Add a 7th test covering garbage/"" entries in
mypage_id_list falling back to BgId=0 while adjacent valid entries are
unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 16:50:38 -04:00
gamer147
9ff6c70faf feat(user-mypage): UserMyPageController + DTOs + persistence tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 16:46:49 -04:00
gamer147
b447f5032d fix(mypage): wire mypage_id/select_type/mypage_id_list as strings
Prod capture (traffic_prod_misc_clicking.ndjson) shows all three
MyPageBgSetting fields arrive as decimal strings, not ints.  Update the
DTO from int/int/List<int> to string/string/List<string> with "0"/empty
defaults, and add a literal wire-shape round-trip test pinning the
exact JSON against the capture.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 16:40:38 -04:00
gamer147
51ef460d39 feat(viewer): migration for mypage bg selection
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>
2026-06-09 16:33:12 -04:00
gamer147
ee808a60a2 feat(viewer): add MyPageBgSelectType + MyPageBgId scalars + MyPageBgRotation owned collection
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>
2026-06-09 16:26:48 -04:00
gamer147
8de78ba7ed test(inventory): lock Unknown-source fallthrough behaviour
Adds Commit_writes_history_row_with_Unknown_source_when_caller_omits_Source
to InventoryHistoryTests — asserts that BeginAsync without a configure callback
writes AcquireType=0 / Message="Unknown", per spec §10.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:33:04 -04:00
gamer147
5334263793 feat(inventory): tag remaining BeginAsync call sites for acquire history
Add GrantSource.CardCraft (16) for card crafting via red ether, and tag
CardInventoryRepository.Create accordingly. LoadController backfill and
ArenaTwoPickService.Entry are debit/infrastructure-only — left as Unknown.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:09:43 -04:00
gamer147
c2c3abc6f0 feat(gift): tag receive-gift tx as AdminGrant for acquire history
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:08:27 -04:00
gamer147
6ec6a9c3fc feat(spot-card): tag exchange tx as GachaPointExchange for acquire history
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:08:03 -04:00
gamer147
d759465cf2 feat(achievement): tag receive-reward tx as AchievementReward for acquire history
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:07:42 -04:00