Deck list work

This commit is contained in:
gamer147
2026-05-23 19:57:34 -04:00
parent 66184b3685
commit d3b2970e11
41 changed files with 70683 additions and 81 deletions

View File

@@ -0,0 +1,46 @@
using MessagePack;
using System.Text.Json.Serialization;
namespace SVSim.EmulatedEntrypoint.Models.Dtos;
/// <summary>
/// One entry from /mypage/index data.banner — the home-screen promo carousel. Consumed by
/// MyPageBannerBase.BannerInfo.Parse(jsonData[i]) when the client iterates banner[i] (banner
/// access is TryGetValue-guarded but the per-entry parse is unconditional).
///
/// Prod-captured shape:
/// <code>
/// {"image_name":"banner_000788","click":"account_transition_with_two","status":"10",
/// "change_time":"10","remaining_time":"0","image_paths":[]}
/// </code>
///
/// Note: change_time, remaining_time, and status are strings on the wire (PHP convention) even
/// though they look numeric. The DB stores them in matching column types but the wire shape rules.
/// </summary>
[MessagePackObject]
public class BannerInfo
{
[JsonPropertyName("image_name")]
[Key("image_name")]
public string ImageName { get; set; } = string.Empty;
[JsonPropertyName("click")]
[Key("click")]
public string Click { get; set; } = string.Empty;
[JsonPropertyName("status")]
[Key("status")]
public string Status { get; set; } = string.Empty;
[JsonPropertyName("change_time")]
[Key("change_time")]
public string ChangeTime { get; set; } = string.Empty;
[JsonPropertyName("remaining_time")]
[Key("remaining_time")]
public string RemainingTime { get; set; } = string.Empty;
[JsonPropertyName("image_paths")]
[Key("image_paths")]
public List<string> ImagePaths { get; set; } = new();
}

View File

@@ -4,17 +4,44 @@ using System.Text.Json.Serialization;
namespace SVSim.EmulatedEntrypoint.Models.Dtos;
/// <summary>
/// colosseum_info on /mypage/index, consumed by
/// ColosseumEntryInfoTask.SetColosseumInfo (Wizard/ColosseumEntryInfoTask.cs:99).
/// colosseum_info on /mypage/index, consumed by ColosseumEntryInfoTask.SetColosseumInfo
/// (Wizard/ColosseumEntryInfoTask.cs:99). The outer object is read unconditionally, and
/// is_colosseum_period gates everything else. When a cup IS active, the client reads
/// many more sub-fields inside the gate (deck_format, now_round, start_time, end_time,
/// sales_period_info, etc.) — we now mirror the full prod shape so the gate-true branch
/// works once we have colosseum data seeded.
///
/// The block is indexed unconditionally — it MUST be present, and
/// `is_colosseum_period` MUST be set. All other fields are only read inside the
/// `if (IsColosseumPeriod)` branch, so when no Take Two cup is active we emit
/// the minimum payload (is_colosseum_period=false) and leave the rest defaulted.
/// Prod-captured shape (15 fields):
/// <code>
/// {"colosseum_id":"165","is_display_tips":"0","tips_id":"0",
/// "card_pool_name":"Take Two (DragonbladeRivenbrandt)",
/// "is_colosseum_period":true,"is_round_period":true,"deck_format":"3",
/// "is_normal_two_pick":"1","is_special_mode":"10","is_all_card_enabled":0,
/// "start_time":"2026-05-21 06:00:00","colosseum_name":"Rivenbrandt Take Two Cup",
/// "now_round":"1","end_time":"2026-05-25 19:59:59",
/// "sales_period_info":{"sales_period_time":"2026-05-25 19:59:59"}}
/// </code>
/// </summary>
[MessagePackObject]
public class ColosseumInfo
{
[JsonPropertyName("colosseum_id")]
[Key("colosseum_id")]
public string ColosseumId { get; set; } = string.Empty;
/// <summary>Wire is "0"/"1" string. Client compares with == "1" (GetValueOrDefault-guarded).</summary>
[JsonPropertyName("is_display_tips")]
[Key("is_display_tips")]
public string IsDisplayTips { get; set; } = "0";
[JsonPropertyName("tips_id")]
[Key("tips_id")]
public string TipsId { get; set; } = "0";
[JsonPropertyName("card_pool_name")]
[Key("card_pool_name")]
public string CardPoolName { get; set; } = string.Empty;
[JsonPropertyName("is_colosseum_period")]
[Key("is_colosseum_period")]
public bool IsColosseumPeriod { get; set; }
@@ -23,11 +50,15 @@ public class ColosseumInfo
[Key("is_round_period")]
public bool IsRoundPeriod { get; set; }
/// <summary>
/// Wire is a stringified int in prod (e.g. "3"). DB stores as string. Client calls
/// <c>jsonData["deck_format"].ToInt()</c> inside the IsColosseumPeriod gate.
/// </summary>
[JsonPropertyName("deck_format")]
[Key("deck_format")]
public int DeckFormat { get; set; }
public string DeckFormat { get; set; } = "0";
/// <summary>Wire is "1"/"0" string in prod. Client compares with == "1".</summary>
/// <summary>Wire is "1"/"0" string. Client compares with == "1".</summary>
[JsonPropertyName("is_normal_two_pick")]
[Key("is_normal_two_pick")]
public string IsNormalTwoPick { get; set; } = "0";
@@ -37,7 +68,30 @@ public class ColosseumInfo
[Key("is_special_mode")]
public string IsSpecialMode { get; set; } = "0";
[JsonPropertyName("is_all_card_enabled")]
[Key("is_all_card_enabled")]
public int IsAllCardEnabled { get; set; }
/// <summary>"yyyy-MM-dd HH:mm:ss" wire format.</summary>
[JsonPropertyName("start_time")]
[Key("start_time")]
public string StartTime { get; set; } = string.Empty;
[JsonPropertyName("colosseum_name")]
[Key("colosseum_name")]
public string ColosseumName { get; set; } = string.Empty;
/// <summary>Round number as string (e.g. "1"). Client casts to int.</summary>
[JsonPropertyName("now_round")]
[Key("now_round")]
public string NowRound { get; set; } = "0";
/// <summary>"yyyy-MM-dd HH:mm:ss" wire format.</summary>
[JsonPropertyName("end_time")]
[Key("end_time")]
public string EndTime { get; set; } = string.Empty;
[JsonPropertyName("sales_period_info")]
[Key("sales_period_info")]
public ColosseumSalesPeriodInfo SalesPeriodInfo { get; set; } = new();
}

View File

@@ -0,0 +1,18 @@
using MessagePack;
using System.Text.Json.Serialization;
namespace SVSim.EmulatedEntrypoint.Models.Dtos;
/// <summary>
/// Nested under /mypage/index data.colosseum_info.sales_period_info. Carries the wall-clock end of
/// the current cup's sales window. Captured from prod:
/// <c>"sales_period_info": { "sales_period_time": "2026-05-25 19:59:59" }</c>.
/// </summary>
[MessagePackObject]
public class ColosseumSalesPeriodInfo
{
/// <summary>Wire format is "yyyy-MM-dd HH:mm:ss" (prod's PHP convention, not ISO).</summary>
[JsonPropertyName("sales_period_time")]
[Key("sales_period_time")]
public string SalesPeriodTime { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,23 @@
using MessagePack;
using System.Text.Json.Serialization;
namespace SVSim.EmulatedEntrypoint.Models.Dtos;
/// <summary>
/// Tournament-window block returned by /mypage/index. Client constructs
/// ArenaCompetition(base.ResponseData) at MyPageTask.cs:110, which then reads
/// `responseData["data"]["competition_info"]["is_competition_period"]`
/// unconditionally (ArenaCompetition.cs:232-233). The remaining fields
/// (deck_format, entry_start_time, freebie_status, featured_entry_reward_list,
/// etc.) are only read when IsCompetitionPeriod is true, so the minimum-viable
/// payload while we have no tournament implementation is just the bool=false.
/// Prod emits the same `{"is_competition_period":false}` shape when no
/// tournament is active.
/// </summary>
[MessagePackObject]
public class CompetitionInfo
{
[JsonPropertyName("is_competition_period")]
[Key("is_competition_period")]
public bool IsCompetitionPeriod { get; set; }
}

View File

@@ -16,11 +16,16 @@ public class Convention
public bool IsJoinTournament { get; set; }
/// <summary>
/// ISO datetime. Optional — omitted via WhenWritingNull when not set.
/// Client null-checks before parsing (MyPageTask.cs:59).
/// ISO datetime, or null when no recent tournament. Client does
/// `if (jsonData["convention"]["recent_start_date"] != null)` (MyPageTask.cs:59)
/// the key must be PRESENT (LitJson throws KeyNotFoundException on missing key);
/// the null check exists to detect "no recent tournament", not "field absent".
/// Override the global WhenWritingNull so the explicit null reaches the wire,
/// matching prod's `"recent_start_date":null` in the convention block.
/// </summary>
[JsonPropertyName("recent_start_date")]
[Key("recent_start_date")]
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public string? RecentStartDate { get; set; }
[JsonPropertyName("is_admin_watch_user")]

View File

@@ -35,4 +35,19 @@ public class DefaultDeck
[JsonPropertyName("card_id_array")]
[Key("card_id_array")]
public List<long> CardIdArray { get; set; } = new();
/// <summary>0/1. Client reads via GetJsonBool(default true) in DeckData.Initialize. Prod always sends 1 for the 8 starter decks.</summary>
[JsonPropertyName("is_complete_deck")]
[Key("is_complete_deck")]
public int IsCompleteDeck { get; set; } = 1;
/// <summary>0/1. Read by downstream deck-edit UI (not by DeckData.Initialize itself). Prod always sends 1.</summary>
[JsonPropertyName("is_available_deck")]
[Key("is_available_deck")]
public int IsAvailableDeck { get; set; } = 1;
/// <summary>Card ids currently under maintenance (disabled). Empty for the 8 starter decks in prod.</summary>
[JsonPropertyName("maintenance_card_ids")]
[Key("maintenance_card_ids")]
public List<long> MaintenanceCardIds { get; set; } = new();
}

View File

@@ -3,10 +3,20 @@ using System.Text.Json.Serialization;
namespace SVSim.EmulatedEntrypoint.Models.Dtos;
/// <summary>
/// gathering_info on /mypage/index — multiplayer-event participation state. Consumed by
/// GatheringMyPageInfo ctor (TryGetValue-guarded) but emitted unconditionally to match prod and
/// to keep post-parse UI consumers from reading nulls.
/// </summary>
[MessagePackObject]
public class GatheringInfo
{
[JsonPropertyName("has_invite")]
[Key("has_invite")]
public int HasInvite { get; set; }
}
/// <summary>Whether this viewer has entered the current gathering event. Per-viewer state — currently always 0.</summary>
[JsonPropertyName("is_entry")]
[Key("is_entry")]
public int IsEntry { get; set; }
}

View File

@@ -0,0 +1,26 @@
using MessagePack;
using System.Text.Json.Serialization;
namespace SVSim.EmulatedEntrypoint.Models.Dtos;
/// <summary>
/// gathering_notification on /mypage/refresh — slim "matching established?" notification flag for
/// gathering events. Single field carrying either an empty string (no match) or the localized
/// "matching established" message (active match).
///
/// **Distinct from <see cref="GatheringInfo"/>**, which is what /mypage/index emits under the
/// <c>gathering_info</c> key — that DTO carries the viewer's full event participation state
/// (has_invite / is_entry). They share a topic ("gathering events") but solve different problems
/// and live at different wire keys; don't conflate them.
///
/// Consumed unconditionally at <c>MyPageRefreshTask.cs:31</c>:
/// <c>jsonData["data"]["gathering_notification"]["matching_established_message"].ToString()</c>.
/// </summary>
[MessagePackObject]
public class GatheringNotification
{
/// <summary>Empty string when no match — correct for fresh viewers and idle states. Prod sends "".</summary>
[JsonPropertyName("matching_established_message")]
[Key("matching_established_message")]
public string MatchingEstablishedMessage { get; set; } = string.Empty;
}

View File

@@ -5,20 +5,25 @@ namespace SVSim.EmulatedEntrypoint.Models.Dtos;
/// <summary>
/// guild_notification on /mypage/index. Consumed by
/// MyPageNotifications.GuildNotification.SetGuildNotification. Prod sends nulls
/// for guild_id / guild_room_message_id when the viewer isn't in a guild; with
/// WhenWritingNull those keys are omitted on our wire, which is equivalent
/// since the parser is null-tolerant.
/// MyPageNotifications.GuildNotification.SetGuildNotification (GuildNotification.cs:30-38),
/// which reads guild_id / guild_room_message_id via `var x = json["guild_id"]; if (x != null) ...`
/// — the LitJson indexer throws KeyNotFoundException on a missing key, so these
/// must reach the client as explicit nulls when there's no guild. Override the
/// global WhenWritingNull so they survive serialization. Prod's wire matches:
/// `"guild_notification":{"guild_id":null,"guild_room_message_id":null,...}`.
/// See [[project-wire-null-policy]] for the broader pattern.
/// </summary>
[MessagePackObject]
public class GuildNotification
{
[JsonPropertyName("guild_id")]
[Key("guild_id")]
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public long? GuildId { get; set; }
[JsonPropertyName("guild_room_message_id")]
[Key("guild_room_message_id")]
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public long? GuildRoomMessageId { get; set; }
[JsonPropertyName("is_join_request")]

View File

@@ -20,9 +20,10 @@ public class MasterPointRankingPeriod
[Key("period_num")]
public int PeriodNum { get; set; }
/// <summary>Stored as long to mirror MasterPointRankingPeriodEntry.NecessaryScore (rank-point thresholds can grow large).</summary>
[JsonPropertyName("necessary_score")]
[Key("necessary_score")]
public int NecessaryScore { get; set; }
public long NecessaryScore { get; set; }
/// <summary>ISO datetime.</summary>
[JsonPropertyName("begin_time")]

View File

@@ -0,0 +1,102 @@
using MessagePack;
using System.Text.Json.Serialization;
namespace SVSim.EmulatedEntrypoint.Models.Dtos;
/// <summary>
/// One entry under /payment_pc/item_list data, parsed by PaymentItemListTask
/// (Cute/PaymentItemListTask.cs:43-70). The client iterates via int index and reads
/// 8 fields unconditionally (store_product_id, name, text, purchase_limit, id, image_name,
/// end_time, special_shop_flag), with number_of_product_purchased TryGetValue-guarded.
///
/// All wire fields are PHP-stringified EXCEPT <c>purchase_num_current</c>, which is a true int.
/// String-typed properties avoid JsonConverter machinery — the controller stringifies typed DB
/// columns via ToString(InvariantCulture) on the way out, same approach as MyPageController.BuildBannerInfo.
///
/// Prod-captured shape (one entry):
/// <code>
/// {"record_id":"21","id":"8","store_product_id":"10011",
/// "name":"60-crystal set","text":"Purchase 60 Crystals","price":"0.99",
/// "charge_crystal_num":"60","free_crystal_num":"0","purchase_limit":"999999999",
/// "special_shop_flag":"0","image_name":"thumbnail_crystal",
/// "start_time":"2022-10-05 15:00:00","end_time":"2030-03-01 14:59:59",
/// "remaining_time":"0","is_resale_product":"0","resale_start_date":"","purchase_num_current":0}
/// </code>
/// </summary>
[MessagePackObject]
public class PaymentItemInfo
{
[JsonPropertyName("record_id")]
[Key("record_id")]
public string RecordId { get; set; } = "0";
[JsonPropertyName("id")]
[Key("id")]
public string Id { get; set; } = "0";
[JsonPropertyName("store_product_id")]
[Key("store_product_id")]
public string StoreProductId { get; set; } = "0";
[JsonPropertyName("name")]
[Key("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("text")]
[Key("text")]
public string Text { get; set; } = string.Empty;
/// <summary>Decimal as PHP-stringified value (e.g. "0.99"). Preserves prod's wire convention.</summary>
[JsonPropertyName("price")]
[Key("price")]
public string Price { get; set; } = "0";
[JsonPropertyName("charge_crystal_num")]
[Key("charge_crystal_num")]
public string ChargeCrystalNum { get; set; } = "0";
[JsonPropertyName("free_crystal_num")]
[Key("free_crystal_num")]
public string FreeCrystalNum { get; set; } = "0";
[JsonPropertyName("purchase_limit")]
[Key("purchase_limit")]
public string PurchaseLimit { get; set; } = "0";
[JsonPropertyName("special_shop_flag")]
[Key("special_shop_flag")]
public string SpecialShopFlag { get; set; } = "0";
[JsonPropertyName("image_name")]
[Key("image_name")]
public string ImageName { get; set; } = string.Empty;
/// <summary>"yyyy-MM-dd HH:mm:ss" wire format.</summary>
[JsonPropertyName("start_time")]
[Key("start_time")]
public string StartTime { get; set; } = string.Empty;
/// <summary>"yyyy-MM-dd HH:mm:ss" wire format.</summary>
[JsonPropertyName("end_time")]
[Key("end_time")]
public string EndTime { get; set; } = string.Empty;
[JsonPropertyName("remaining_time")]
[Key("remaining_time")]
public string RemainingTime { get; set; } = "0";
[JsonPropertyName("is_resale_product")]
[Key("is_resale_product")]
public string IsResaleProduct { get; set; } = "0";
/// <summary>Empty string ("") when unset; otherwise "yyyy-MM-dd HH:mm:ss". Matches prod.</summary>
[JsonPropertyName("resale_start_date")]
[Key("resale_start_date")]
public string ResaleStartDate { get; set; } = string.Empty;
/// <summary>True int on the wire (not string) — count of this viewer's purchases of this product.
/// Per-viewer state; currently hardcoded to 0 server-side until purchase tracking lands.</summary>
[JsonPropertyName("purchase_num_current")]
[Key("purchase_num_current")]
public int PurchaseNumCurrent { get; set; }
}

View File

@@ -0,0 +1,14 @@
using MessagePack;
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
/// <summary>
/// Request body for /mypage/refresh. Carries only the standard auth envelope —
/// no <c>carrier</c> field, unlike MyPageIndexRequest. Confirmed against prod traffic
/// in data_dumps/traffic_prod.ndjson: both refresh request bodies have exactly
/// <c>viewer_id / steam_id / steam_session_ticket</c>.
/// </summary>
[MessagePackObject]
public class MyPageRefreshRequest : BaseRequest
{
}

View File

@@ -0,0 +1,12 @@
using MessagePack;
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
/// <summary>
/// Request body for /payment_pc/item_list. Prod sends only the standard auth envelope
/// (viewer_id / steam_id / steam_session_ticket) — no additional fields.
/// </summary>
[MessagePackObject]
public class PaymentItemListRequest : BaseRequest
{
}

View File

@@ -16,9 +16,29 @@ public class DeckListResponse
[JsonPropertyName("maintenance_card_list")]
[Key("maintenance_card_list")] public List<long> MaintenanceCardList { get; set; } = new();
/// <summary>
/// Single-format viewer decks. Emitted when the request specified a specific format
/// (e.g. Rotation, Unlimited) — mutually exclusive with the per-format keys below.
/// </summary>
[JsonPropertyName("user_deck_list")]
[Key("user_deck_list")] public List<UserDeck>? UserDeckList { get; set; }
/// <summary>
/// Per-format viewer decks. Emitted when the request specified All format (deck_format=0).
/// Prod's <c>DeckListUtility.ParseDeckInfoResponceData</c> All-format branch only walks these
/// per-format keys (not user_deck_list), so the controller swaps shape based on the request.
/// The PreRotation / Crossover / Avatar siblings exist in client code but prod omits them
/// for fresh viewers; we mirror that omission.
/// </summary>
[JsonPropertyName("user_deck_rotation")]
[Key("user_deck_rotation")] public List<UserDeck>? UserDeckRotation { get; set; }
[JsonPropertyName("user_deck_unlimited")]
[Key("user_deck_unlimited")] public List<UserDeck>? UserDeckUnlimited { get; set; }
[JsonPropertyName("user_deck_my_rotation")]
[Key("user_deck_my_rotation")] public List<UserDeck>? UserDeckMyRotation { get; set; }
/// <summary>
/// Global starter decks, keyed by deck_no as string (prod ids 91-98 — one per class).
/// </summary>

View File

@@ -89,6 +89,48 @@ public class MyPageIndexResponse
[Key("is_available_colosseum_free_entry")]
public bool IsAvailableColosseumFreeEntry { get; set; }
// ── Sealed Arena season ────────────────────────────────────────────────
/// <summary>
/// sealed_info is consumed by ArenaData.SetSealedMyPageResponseData (Keys.Contains-guarded),
/// but post-parse-consumer policy says we emit anyway. Defaults to a zeroed-out SealedInfo
/// when no current season is seeded — Enable=0 means the UI treats Sealed as inactive.
/// </summary>
[JsonPropertyName("sealed_info")]
[Key("sealed_info")]
public SealedInfo SealedInfo { get; set; } = new();
// ── Mypage banner carousel ─────────────────────────────────────────────
/// <summary>
/// banner is consumed by per-entry parsing inside a TryGetValue guard
/// (Wizard/MyPageBannerBase.BannerInfo.Parse iterates the array if present). We always emit
/// the list — empty when no rows have been imported. See SVSim.Bootstrap.GlobalsImporter.ImportBanners.
/// </summary>
[JsonPropertyName("banner")]
[Key("banner")]
public List<BannerInfo> Banner { get; set; } = new();
/// <summary>Prod sends explicit null. Override WhenWritingNull so the key survives serialization.</summary>
[JsonPropertyName("sub_banner")]
[Key("sub_banner")]
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public object? SubBanner { get; set; }
[JsonPropertyName("sub_banner_list")]
[Key("sub_banner_list")]
public List<object> SubBannerList { get; set; } = new();
[JsonPropertyName("home_dialog_list")]
[Key("home_dialog_list")]
public List<object> HomeDialogList { get; set; } = new();
// ── Room type in session (Special-format windows) ──────────────────────
[JsonPropertyName("room_type_in_session")]
[Key("room_type_in_session")]
public RoomTypeInSession RoomTypeInSession { get; set; } = new();
/// <summary>
/// Required — ColosseumEntryInfoTask.SetColosseumInfo indexes this key
/// directly (Wizard/ColosseumEntryInfoTask.cs:102) and reads
@@ -104,25 +146,37 @@ public class MyPageIndexResponse
[Key("convention")]
public Convention Convention { get; set; } = new();
// ── Battle / room recovery (optional) ─────────────────────────────────
/// <summary>
/// Required — MyPageTask.cs:110 constructs ArenaCompetition(responseData)
/// which indexes data.competition_info.is_competition_period unconditionally
/// (ArenaCompetition.cs:232-233). When false, the rest of the block is
/// skipped, so a default-constructed CompetitionInfo is sufficient.
/// </summary>
[JsonPropertyName("competition_info")]
[Key("competition_info")]
public CompetitionInfo CompetitionInfo { get; set; } = new();
// ── Battle / room recovery ─────────────────────────────────────────────
/// <summary>Prod always sends concrete bool here even for fresh viewers — emit always.</summary>
[JsonPropertyName("unfinished_battle_exists")]
[Key("unfinished_battle_exists")]
public bool? UnfinishedBattleExists { get; set; }
public bool UnfinishedBattleExists { get; set; }
/// <summary>Only meaningful when UnfinishedBattleExists is true. Keep nullable + omitted otherwise — prod also omits it for fresh viewers.</summary>
[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; }
public bool IsJoinedRoom { get; set; }
// ── Login bonus (optional) ─────────────────────────────────────────────
// ── Login bonus ────────────────────────────────────────────────────────
[JsonPropertyName("can_give_daily_login_bonus")]
[Key("can_give_daily_login_bonus")]
public bool? CanGiveDailyLoginBonus { get; set; }
public bool CanGiveDailyLoginBonus { get; set; }
// ── User config (settings echo) ────────────────────────────────────────
@@ -210,14 +264,45 @@ public class MyPageIndexResponse
[Key("story_notification")]
public StoryNotification StoryNotification { get; set; } = new();
// ── Optional UI surface area ───────────────────────────────────────────
// ── Per-viewer / event state ───────────────────────────────────────────
/// <summary>Updated item counts. Refreshes Data.Load.data._userItemDict when present.</summary>
/// <summary>
/// Updated item counts. Empty list = "no items to update" (client iterates 0 times, no UI change).
/// Per-viewer state — populate from viewer.Items when that wiring lands.
/// </summary>
[JsonPropertyName("user_item_list")]
[Key("user_item_list")]
public List<UserItem>? UserItemList { get; set; }
public List<UserItem> UserItemList { get; set; } = new();
[JsonPropertyName("gathering_info")]
[Key("gathering_info")]
public GatheringInfo? GatheringInfo { get; set; }
public GatheringInfo GatheringInfo { get; set; } = new();
/// <summary>Per-viewer offline-event participation. Empty for fresh viewers; prod also sends [].</summary>
[JsonPropertyName("user_offline_event")]
[Key("user_offline_event")]
public List<object> UserOfflineEvent { get; set; } = new();
// ── Fields prod sends as explicit null ─────────────────────────────────
/// <summary>
/// CRITICAL — emitting this field (even as null) routes MyPageTask.Parse through
/// CampaignBattleWin.Clear() which initializes RewardList = new List&lt;...&gt;(). Without it,
/// RewardList stays null and MyPageMenu.GetMyPageInfo NREs on its foreach iteration.
/// See [[project-wire-null-policy]] for the broader "post-parse-consumer" rationale.
/// </summary>
[JsonPropertyName("treasure_info")]
[Key("treasure_info")]
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public object? TreasureInfo { get; set; }
[JsonPropertyName("lottery_period_info")]
[Key("lottery_period_info")]
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public object? LotteryPeriodInfo { get; set; }
[JsonPropertyName("all_card_enabled_period")]
[Key("all_card_enabled_period")]
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public object? AllCardEnabledPeriod { get; set; }
}

View File

@@ -0,0 +1,39 @@
using MessagePack;
using System.Text.Json.Serialization;
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Responses;
/// <summary>
/// /mypage/refresh response — a slim notification-delta payload, NOT a full state refresh.
/// Prod sends exactly 3 top-level keys, all of which the client reads unconditionally:
///
/// <list type="bullet">
/// <item><c>friend_battle_invite_count</c> — int, viewer's room-invite count
/// (consumed at <c>MyPageRefreshTask.cs:29</c>).</item>
/// <item><c>shop_notification</c> — same nested shape as /mypage/index's shop_notification.
/// The side-effect call <c>ShopNotification.SetShopNotification</c> unconditionally indexes
/// all four sub-keys (card_pack / build_deck / sleeve / leader_skin), already handled by
/// our <see cref="ShopNotification"/> DTO's field initializers.</item>
/// <item><c>gathering_notification</c> — new shape distinct from /mypage/index's gathering_info.
/// Carries only the matching-established message string.</item>
/// </list>
///
/// All three fields are required-present per the new "anything prod emits, we emit" methodology
/// — even though the third call site looks tolerant, omitting the key would throw
/// KeyNotFoundException at LitJson's indexer.
/// </summary>
[MessagePackObject]
public class MyPageRefreshResponse
{
[JsonPropertyName("friend_battle_invite_count")]
[Key("friend_battle_invite_count")]
public int FriendBattleInviteCount { get; set; }
[JsonPropertyName("shop_notification")]
[Key("shop_notification")]
public ShopNotification ShopNotification { get; set; } = new();
[JsonPropertyName("gathering_notification")]
[Key("gathering_notification")]
public GatheringNotification GatheringNotification { get; set; } = new();
}

View File

@@ -0,0 +1,20 @@
using MessagePack;
using System.Text.Json.Serialization;
namespace SVSim.EmulatedEntrypoint.Models.Dtos;
/// <summary>
/// room_type_in_session on /mypage/index — list of "special" deck-format windows currently active.
/// Consumed by RoomRuleInfo (Wizard/RoomRuleInfo.cs:61) via TryGetValue, but emitted unconditionally
/// per the post-parse-consumer-safe policy.
///
/// Prod-captured shape:
/// <code>{"special_deck_format_list": [{"deck_format":"5","end_time":"2030-06-26 19:59:59"}]}</code>
/// </summary>
[MessagePackObject]
public class RoomTypeInSession
{
[JsonPropertyName("special_deck_format_list")]
[Key("special_deck_format_list")]
public List<SpecialDeckFormat> SpecialDeckFormatList { get; set; } = new();
}

View File

@@ -0,0 +1,62 @@
using MessagePack;
using System.Text.Json.Serialization;
namespace SVSim.EmulatedEntrypoint.Models.Dtos;
/// <summary>
/// sealed_info on /mypage/index — current Sealed Arena season configuration. Consumed by
/// ArenaData.SetSealedMyPageResponseData (ArenaData.cs:59-65), which is Keys.Contains-guarded,
/// but post-parse UI almost certainly dereferences fields from the SealedMyPageResponseData
/// it builds. Since the user reclassified "Safe to omit" as a non-policy, we now always emit.
///
/// Prod-captured shape:
/// <code>
/// {"enable":1,"crystal_cost":600,"rupy_cost":600,"ticket_cost":4,"is_join":false,
/// "pack_info":[10032,10032,10031,10030,10029],"deck_using_num_min":30,"schedule_id":21,
/// "is_deck_code_maintenance":false,"sales_period_info":{"sales_period_series":33}}
/// </code>
/// </summary>
[MessagePackObject]
public class SealedInfo
{
[JsonPropertyName("enable")]
[Key("enable")]
public int Enable { get; set; }
[JsonPropertyName("crystal_cost")]
[Key("crystal_cost")]
public int CrystalCost { get; set; }
[JsonPropertyName("rupy_cost")]
[Key("rupy_cost")]
public int RupyCost { get; set; }
[JsonPropertyName("ticket_cost")]
[Key("ticket_cost")]
public int TicketCost { get; set; }
[JsonPropertyName("is_join")]
[Key("is_join")]
public bool IsJoin { get; set; }
/// <summary>Pack set ids used in this Sealed pool. Prod sends 5 entries (one per draft pack).</summary>
[JsonPropertyName("pack_info")]
[Key("pack_info")]
public List<int> PackInfo { get; set; } = new();
[JsonPropertyName("deck_using_num_min")]
[Key("deck_using_num_min")]
public int DeckUsingNumMin { get; set; }
[JsonPropertyName("schedule_id")]
[Key("schedule_id")]
public int ScheduleId { get; set; }
[JsonPropertyName("is_deck_code_maintenance")]
[Key("is_deck_code_maintenance")]
public bool IsDeckCodeMaintenance { get; set; }
[JsonPropertyName("sales_period_info")]
[Key("sales_period_info")]
public SealedSalesPeriodInfo SalesPeriodInfo { get; set; } = new();
}

View File

@@ -0,0 +1,17 @@
using MessagePack;
using System.Text.Json.Serialization;
namespace SVSim.EmulatedEntrypoint.Models.Dtos;
/// <summary>
/// Nested under /mypage/index data.sealed_info.sales_period_info. Distinct from Arena/Colosseum's
/// sales_period_info shapes — this inner value is an int (the active schedule series number),
/// not a date string. Captured from prod: <c>"sales_period_info": { "sales_period_series": 33 }</c>.
/// </summary>
[MessagePackObject]
public class SealedSalesPeriodInfo
{
[JsonPropertyName("sales_period_series")]
[Key("sales_period_series")]
public int SalesPeriodSeries { get; set; }
}

View File

@@ -0,0 +1,25 @@
using MessagePack;
using System.Text.Json.Serialization;
namespace SVSim.EmulatedEntrypoint.Models.Dtos;
/// <summary>
/// One entry under /mypage/index data.room_type_in_session.special_deck_format_list. Consumed by
/// RoomRuleInfo ctor (Wizard/RoomRuleInfo.cs:61-70) which is TryGetValue-guarded but the per-entry
/// fields are accessed unconditionally inside the guard.
///
/// Prod-captured shape: <c>{"deck_format":"5","end_time":"2030-06-26 19:59:59"}</c>.
/// </summary>
[MessagePackObject]
public class SpecialDeckFormat
{
/// <summary>Wire is string per prod's PHP convention (despite looking numeric like "5").</summary>
[JsonPropertyName("deck_format")]
[Key("deck_format")]
public string DeckFormat { get; set; } = string.Empty;
/// <summary>"yyyy-MM-dd HH:mm:ss" wire format.</summary>
[JsonPropertyName("end_time")]
[Key("end_time")]
public string EndTime { get; set; } = string.Empty;
}