Deck list work
This commit is contained in:
46
SVSim.EmulatedEntrypoint/Models/Dtos/BannerInfo.cs
Normal file
46
SVSim.EmulatedEntrypoint/Models/Dtos/BannerInfo.cs
Normal 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();
|
||||
}
|
||||
@@ -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 (Dragonblade–Rivenbrandt)",
|
||||
/// "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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
23
SVSim.EmulatedEntrypoint/Models/Dtos/CompetitionInfo.cs
Normal file
23
SVSim.EmulatedEntrypoint/Models/Dtos/CompetitionInfo.cs
Normal 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; }
|
||||
}
|
||||
@@ -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")]
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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")]
|
||||
|
||||
@@ -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")]
|
||||
|
||||
102
SVSim.EmulatedEntrypoint/Models/Dtos/PaymentItemInfo.cs
Normal file
102
SVSim.EmulatedEntrypoint/Models/Dtos/PaymentItemInfo.cs
Normal 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; }
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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<...>(). 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; }
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
20
SVSim.EmulatedEntrypoint/Models/Dtos/RoomTypeInSession.cs
Normal file
20
SVSim.EmulatedEntrypoint/Models/Dtos/RoomTypeInSession.cs
Normal 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();
|
||||
}
|
||||
62
SVSim.EmulatedEntrypoint/Models/Dtos/SealedInfo.cs
Normal file
62
SVSim.EmulatedEntrypoint/Models/Dtos/SealedInfo.cs
Normal 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();
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
25
SVSim.EmulatedEntrypoint/Models/Dtos/SpecialDeckFormat.cs
Normal file
25
SVSim.EmulatedEntrypoint/Models/Dtos/SpecialDeckFormat.cs
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user