- Replace GuildNotification TODO stub in MyPageController.Index with
BuildGuildNotificationAsync: populates guild_id, guild_room_message_id,
is_join_request, is_invited from real DB state.
- Inject IGuildService, IGuildInviteRepository, IGuildJoinRequestRepository,
IGuildChatMessageRepository into MyPageController.
- Add GetMaxMessageIdSafelyAsync to IGuildChatMessageRepository (returns null
when no messages exist, vs GetMaxMessageIdAsync which returns 0).
- LoadDetail.cs audit: grep returned no guild fields — no LoadController changes.
- Add GuildCrossSurfaceTests (4 tests): guild member, pending invite, pending
join request, no-guild viewer all verified green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Injects ILoginBonusService into MyPageController and sets
CanGiveDailyLoginBonus from IsDue(viewer) in the Index response.
Adds two integration tests: fresh viewer → true, post-load-index → false.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three fixups the Phase 1-3 work missed (would bite the first time an admin
flipped on a real event):
* IsDisplayTips + TipsId added to ColosseumSeasonConfig. Drives
Wizard/ColosseumEntryInfoTask.cs:129 (NeedsFirstTips = is_display_tips == 1).
The wire DTO already had the properties; the season config didn't carry
them, so admins had no way to gate the first-time tutorial tip popup.
* colosseum_id is now emitted from SeasonId. Wire field was always-absent
before (DTO had the property but builder never set it); client fell back
to 0 and couldn't disambiguate cups.
* ColosseumLobbyInfoBuilder.Build now takes ColosseumRoundsConfig + nowUtc
and derives the wire is_round_period / now_round / next_round / start_time
/ end_time from the rounds schedule against the current clock. Previous
hardcode (is_round_period: true, now_round: 1, event-window dates) would
have shown "Round 1 ends 2026-07-31" instead of the actual round's end.
Per the client parser at ColosseumEntryInfoTask.cs:111-126:
- inside a round → emit that round's window + is_round_period=true
- between rounds → emit next round's start as start_time + next_round
- pre-event → next_round=1, start_time=event start
- post-event → anchor to last round so the client doesn't NRE
- no rounds configured → fall back to event-level window
Why these were missed: I implemented ColosseumSeasonConfig from the plan's
explicit field list rather than cross-checking against the captured
colosseum.json wire shape; I added ColosseumId to the wire DTO without
wiring the builder side; and no test exercised a mid-event scenario with
realistic dates — Phase 2's E2E hand-edits run.RoundId via
PromoteRunToRoundAsync instead of letting the controller derive it.
* 10 new builder unit tests cover off-season / mid-round / between-rounds /
pre-event / post-event / empty-rounds-fallback + the new field surfacing.
Full suite: 1355/1355 (was 1345 + 10).
/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).
is_skip_gacha_effect, use_challenge_two_pick_premium_card, and
challenge_two_pick_sleeve_id all persist on ViewerInfo and round-trip
through /load/index.
The two challenge fields move from the global ChallengeConfig section to
ViewerInfo — they're viewer preferences, not server-wide policy. Premium
card defaults to 0; sleeve falls back to DefaultLoadoutConfig.SleeveId
(3000011) when unset. MatchContextBuilder's TK2 sleeve read switches to
the same viewer-scoped path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
Bug 1 ("pay to enter again after restart"):
arena_info[0].is_join shipped from the static ArenaSeasonConfig seed,
so /load/index and /mypage/index always emitted false regardless of
viewer state. The client uses is_join to choose between the "Pay to
enter" and "Resume run" dialogs (Wizard/ChallengeEntry.cs:165 + the
ArenaEntryBase._isJoinFunc pivot). Without a per-viewer override every
cold start after a partial run looked like "no run" and the player got
charged again.
LoadController + MyPageController now compute is_join from
ViewerArenaTwoPickRuns presence. MyPageController grew an
IArenaTwoPickRunRepository dep (LoadController already had _db).
Bug 2: /arena/get_challenge_info 404. Stubbed via a new
ArenaController + DTO pair. Returns the season seed's begin/end_time
+ name where available; placeholder zeros for win history. All 6 keys
required by ChallangeHistoryInfoTask.Parse are present (unconditional
JsonData lookups).
Routing smoke added for /arena/get_challenge_info.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
MyPageTask.Parse (Wizard/MyPageTask.cs:155-163) does
`_userItemDict.Clear();` the moment `user_item_list` is present in
the response body — not when it's non-empty — then re-populates
from the wire. Our /mypage/index was emitting [] by default (the
field initializer on the DTO), which wiped the inventory that
/load/index had just populated.
Downstream consequence: the client's PackChildGachaInfo.CostGoodsCount
reads from _userItemDict, so a wiped dict makes every ticket-cost
pack report CostGoodsCount=0, PackConfig.EnableBuyPack returns false,
and is_hide=1 packs (including the tutorial legendary starter 99047)
disappear from the rotation pack list — even though the gift bundle
just granted the ticket and the DB row exists. The tutorial then
auto-selects whatever non-tutorial pack happens to be at index 0 of
the filtered list, the user can't afford it, and the flow is stuck.
Fix:
- MyPageController.Index now sets UserItemList from viewer.Items
(already loaded by GetViewerByShortUdid's home-screen graph).
- DTO docstring rewritten to call out the presence-sensitive semantics
and the load-bearing path through PackConfig.EnableBuyPack, so the
next developer doesn't get the "empty is fine" hint the old comment
implied.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>