Things were working, suddenly regressed

This commit is contained in:
gamer147
2026-05-23 18:14:42 -04:00
parent 56d3cf0ec8
commit 66184b3685
31 changed files with 1493 additions and 97 deletions

View File

@@ -4,14 +4,36 @@ using System.Text.Json.Serialization;
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Deck;
/// <summary>
/// Shape consumed by `DeckGroupListData(jsonData, format)` for a single-format call 窶・/// the format-scoped decks land under `user_deck_list` (vs. the per-format keys used
/// by /practice/deck_list with Format.All).
/// Shape consumed by <c>DeckGroupListData(jsonData, format)</c>. Spec at
/// <c>docs/api-spec/endpoints/post-login/deck-info.md</c> only enumerates <c>maintenance_card_list</c>
/// and <c>user_deck_list</c> explicitly (with <c>[k: string]: unknown</c> for the rest); the 2026-05-23
/// prod capture filled in the gap — <c>default_deck_list</c>, <c>user_leader_skin_setting_list</c>,
/// and <c>trial_deck_list</c> are all present and sourced from globals.
/// </summary>
[MessagePackObject]
public class DeckListResponse
{
[JsonPropertyName("maintenance_card_list")]
[Key("maintenance_card_list")] public List<long> MaintenanceCardList { get; set; } = new();
[JsonPropertyName("user_deck_list")]
[Key("user_deck_list")] public List<UserDeck>? UserDeckList { get; set; }
/// <summary>
/// Global starter decks, keyed by deck_no as string (prod ids 91-98 — one per class).
/// </summary>
[JsonPropertyName("default_deck_list")]
[Key("default_deck_list")] public Dictionary<string, DefaultDeck> DefaultDeckList { get; set; } = new();
/// <summary>
/// Default leader skin per class, keyed by class_id as string.
/// </summary>
[JsonPropertyName("user_leader_skin_setting_list")]
[Key("user_leader_skin_setting_list")] public Dictionary<string, DefaultLeaderSkinSetting> UserLeaderSkinSettingList { get; set; } = new();
/// <summary>
/// Trial / tutorial-specific decks. Empty in the 2026-05-23 prod capture; entry shape TBD.
/// </summary>
[JsonPropertyName("trial_deck_list")]
[Key("trial_deck_list")] public List<UserDeck> TrialDeckList { get; set; } = new();
}

View File

@@ -1,5 +1,6 @@
using MessagePack;
using SVSim.Database.Enums;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Responses;
@@ -24,15 +25,22 @@ public class IndexResponse
[JsonPropertyName("room_recovery_status")]
[Key("room_recovery_status")]
public int RoomRecoveryStatus { get; set; }
/// <summary>
/// Prod emits this as bool (per the 2026-05-23 capture); the spec leaves it as a TODO
/// (load-index.md line 296-297). We send bool to match prod; client's `.ToBoolean()`
/// path handles either shape, but matching prod avoids the int-vs-bool drift noted in
/// the seed-data-strategy crash audit.
/// </summary>
[JsonPropertyName("is_battle_pass_period")]
[Key("is_battle_pass_period")]
public int IsBattlePassPeriod { get; set; }
public bool IsBattlePassPeriod { get; set; }
[JsonPropertyName("card_set_id_for_resource_dl_view")]
[Key("card_set_id_for_resource_dl_view")]
public int CardSetIdForResourceDlView { get; set; }
// Serialized as wire deck_format via FormatJsonConverter (registered in Program.cs).
[JsonPropertyName("deck_format")]
[Key("deck_format")]
public int DeckFormat { get; set; } = 1;
public Format DeckFormat { get; set; } = Format.Rotation;
#endregion
@@ -123,9 +131,15 @@ public class IndexResponse
[Key("user_rank_match_list")]
public List<UserRankedMatches> UserRankedMatches { get; set; } = new();
/// <summary>
/// Spec: optional. Shape is {normal?, total?, campaign?[]} per common/types.ts.md DailyLoginBonus.
/// Until we have an active login-bonus campaign to surface in spec shape, omit. The skeleton
/// rows in DailyLoginBonuses table (prod sent {"1":[], "3":[], "4":[]}) preserve the capture
/// for archive but don't make it into the wire response.
/// </summary>
[JsonPropertyName("daily_login_bonus")]
[Key("daily_login_bonus")]
public DailyLoginBonus DailyLoginBonus { get; set; } = new();
public DailyLoginBonus? DailyLoginBonus { get; set; }
[JsonPropertyName("challenge_config")]
[Key("challenge_config")]
@@ -213,7 +227,7 @@ public class IndexResponse
[JsonPropertyName("avatar_info")]
[Key("avatar_info")]
public MyRotationInfo? AvatarRotationInfo { get; set; }
public AvatarInfo? AvatarRotationInfo { get; set; }
[JsonPropertyName("feature_maintenance_list")]
[Key("feature_maintenance_list")]
@@ -223,6 +237,10 @@ public class IndexResponse
[Key("special_crystal_info")]
public List<SpecialCrystalInfo> SpecialCrystalInfos { get; set; } = new();
/// <summary>
/// Spec: optional, Record&lt;string, BattlePassLevelInfo&gt; keyed by level-as-string
/// (load-index.md:228). Omit (null) when no Battle Pass is active.
/// </summary>
[JsonPropertyName("battle_pass_level_info")]
[Key("battle_pass_level_info")]
public Dictionary<string, BattlePassLevel>? BattlePassLevelInfo { get; set; }

View File

@@ -0,0 +1,223 @@
using MessagePack;
using SVSim.Database.Enums;
using System.Text.Json.Serialization;
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Responses;
/// <summary>
/// /mypage/index ("home screen refresh") response payload.
///
/// Required fields per the minimum-viable section of
/// docs/api-spec/endpoints/post-login/mypage-index.md and corroborated by
/// MyPageTask.cs direct-index accesses (jsonData["…"] without TryGetValue).
/// Optional fields are nullable and omitted by the global WhenWritingNull
/// policy — the client uses TryGetValue / GetValueOrDefault for those.
/// </summary>
[MessagePackObject]
public class MyPageIndexResponse
{
// ── User identity / counts ─────────────────────────────────────────────
/// <summary>
/// Full UserInfo block. Client only reads .name here (MyPageTask.cs:39) but
/// prod emits the full structure, so we do too.
/// </summary>
[JsonPropertyName("user_info")]
[Key("user_info")]
public UserInfo UserInfo { get; set; } = new();
[JsonPropertyName("unreceived_mission_reward_count")]
[Key("unreceived_mission_reward_count")]
public int UnreceivedMissionRewardCount { get; set; }
[JsonPropertyName("receive_friend_apply_count")]
[Key("receive_friend_apply_count")]
public int ReceiveFriendApplyCount { get; set; }
[JsonPropertyName("unread_present_count")]
[Key("unread_present_count")]
public int UnreadPresentCount { get; set; }
[JsonPropertyName("friend_battle_invite_count")]
[Key("friend_battle_invite_count")]
public int FriendBattleInviteCount { get; set; }
// ── Guild ──────────────────────────────────────────────────────────────
[JsonPropertyName("guild_notification")]
[Key("guild_notification")]
public GuildNotification GuildNotification { get; set; } = new();
// ── Announcements ──────────────────────────────────────────────────────
[JsonPropertyName("last_announce_id")]
[Key("last_announce_id")]
public int LastAnnounceId { get; set; }
/// <summary>ISO datetime. Parse is wrapped in try/catch on the client.</summary>
[JsonPropertyName("last_announce_update_time")]
[Key("last_announce_update_time")]
public string LastAnnounceUpdateTime { get; set; } = string.Empty;
// ── Maintenance ────────────────────────────────────────────────────────
/// <summary>Same shape as /load/index. Empty list in the 2026-05-23 capture.</summary>
[JsonPropertyName("feature_maintenance_list")]
[Key("feature_maintenance_list")]
public List<FeatureMaintenance> FeatureMaintenanceList { get; set; } = new();
// ── Arena / Colosseum ──────────────────────────────────────────────────
/// <summary>
/// Client unconditionally constructs ArenaData(arena_info) which reads [0],
/// so this MUST be a non-empty list. See LoadController BuildArenaInfosAsync
/// — we mirror that, returning null (omitted on wire) when no Take Two
/// season is seeded, in which case the client's Keys.Contains guard at
/// LoadDetail.cs:261 handles it. For mypage there is no equivalent guard;
/// the client always reads it. Until that's reconciled we send a minimal
/// stub on the controller side.
/// </summary>
[JsonPropertyName("arena_info")]
[Key("arena_info")]
public List<ArenaInfo> ArenaInfo { get; set; } = new();
[JsonPropertyName("is_arena_challenge_period")]
[Key("is_arena_challenge_period")]
public bool IsArenaChallengePeriod { get; set; }
[JsonPropertyName("is_available_colosseum_free_entry")]
[Key("is_available_colosseum_free_entry")]
public bool IsAvailableColosseumFreeEntry { get; set; }
/// <summary>
/// Required — ColosseumEntryInfoTask.SetColosseumInfo indexes this key
/// directly (Wizard/ColosseumEntryInfoTask.cs:102) and reads
/// is_colosseum_period without a guard.
/// </summary>
[JsonPropertyName("colosseum_info")]
[Key("colosseum_info")]
public ColosseumInfo ColosseumInfo { get; set; } = new();
// ── Convention / offline event ─────────────────────────────────────────
[JsonPropertyName("convention")]
[Key("convention")]
public Convention Convention { get; set; } = new();
// ── Battle / room recovery (optional) ─────────────────────────────────
[JsonPropertyName("unfinished_battle_exists")]
[Key("unfinished_battle_exists")]
public bool? UnfinishedBattleExists { get; set; }
[JsonPropertyName("battle_finish_wait_time")]
[Key("battle_finish_wait_time")]
public int? BattleFinishWaitTime { get; set; }
[JsonPropertyName("is_joined_room")]
[Key("is_joined_room")]
public bool? IsJoinedRoom { get; set; }
// ── Login bonus (optional) ─────────────────────────────────────────────
[JsonPropertyName("can_give_daily_login_bonus")]
[Key("can_give_daily_login_bonus")]
public bool? CanGiveDailyLoginBonus { get; set; }
// ── User config (settings echo) ────────────────────────────────────────
[JsonPropertyName("user_config")]
[Key("user_config")]
public UserConfig UserConfig { get; set; } = new();
// ── Quest progress ─────────────────────────────────────────────────────
[JsonPropertyName("quest")]
[Key("quest")]
public Quest Quest { get; set; } = new();
/// <summary>
/// Required — QuestOpenInfo.SetOpenInfo unconditionally calls .ToBoolean()
/// on this root-level field (Wizard/QuestOpenInfo.cs:32). Omitting it would
/// surface as a parse crash, not a defaulted value.
/// </summary>
[JsonPropertyName("is_hidden_boss_appeared")]
[Key("is_hidden_boss_appeared")]
public bool IsHiddenBossAppeared { get; set; }
// ── Master Points season window ────────────────────────────────────────
[JsonPropertyName("master_point_ranking_period")]
[Key("master_point_ranking_period")]
public MasterPointRankingPeriod MasterPointRankingPeriod { get; set; } = new();
// ── Pre-release card preview ───────────────────────────────────────────
/// <summary>Number cast to Prerelease.eStatus on the client.</summary>
[JsonPropertyName("pre_release_status")]
[Key("pre_release_status")]
public int PreReleaseStatus { get; set; }
// ── MyPage background ──────────────────────────────────────────────────
[JsonPropertyName("user_mypage_info")]
[Key("user_mypage_info")]
public UserMyPageInfo UserMyPageInfo { get; set; } = new();
// ── Basic puzzle badge ─────────────────────────────────────────────────
[JsonPropertyName("basic_puzzle")]
[Key("basic_puzzle")]
public BasicPuzzle BasicPuzzle { get; set; } = new();
// ── Battle Pass period flag ────────────────────────────────────────────
/// <summary>
/// Parsed by Data.ParseIsBattlePassPeriod. Same field as on /load/index
/// (prod emits bool there too).
/// </summary>
[JsonPropertyName("is_battle_pass_period")]
[Key("is_battle_pass_period")]
public bool IsBattlePassPeriod { get; set; }
// ── Special crystal info ───────────────────────────────────────────────
/// <summary>
/// Sibling under data, same shape as /load/index. Empty in the prod capture.
/// </summary>
[JsonPropertyName("special_crystal_info")]
[Key("special_crystal_info")]
public List<SpecialCrystalInfo> SpecialCrystalInfo { get; set; } = new();
// ── Notification setters that index root-of-data directly ──────────────
/// <summary>
/// Required — ShopNotification.SetShopNotification indexes the four nested
/// keys (card_pack, build_deck, sleeve, leader_skin) without TryGetValue
/// (Wizard/ShopNotification.cs:33-37). The inner ShopAppealInfo ctor early-
/// returns on empty, so default-constructed values are safe.
/// </summary>
[JsonPropertyName("shop_notification")]
[Key("shop_notification")]
public ShopNotification ShopNotification { get; set; } = new();
/// <summary>
/// Required — StoryNotification.SetStoryNotification indexes this key
/// directly (Wizard/StoryNotification.cs:22) before applying GetValueOrDefault
/// to its sub-fields.
/// </summary>
[JsonPropertyName("story_notification")]
[Key("story_notification")]
public StoryNotification StoryNotification { get; set; } = new();
// ── Optional UI surface area ───────────────────────────────────────────
/// <summary>Updated item counts. Refreshes Data.Load.data._userItemDict when present.</summary>
[JsonPropertyName("user_item_list")]
[Key("user_item_list")]
public List<UserItem>? UserItemList { get; set; }
[JsonPropertyName("gathering_info")]
[Key("gathering_info")]
public GatheringInfo? GatheringInfo { get; set; }
}