More features

This commit is contained in:
gamer147
2026-05-23 14:18:01 -04:00
parent b2024af852
commit 6b70850b7b
59 changed files with 862 additions and 42033 deletions

View File

@@ -6,5 +6,5 @@ namespace SVSim.EmulatedEntrypoint.Models.Dtos;
public class EmblemIdentifier
{
[Key("emblem_id")]
public int EmblemId { get; set; }
public long EmblemId { get; set; }
}

View File

@@ -1,7 +1,13 @@
using MessagePack;
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
[MessagePackObject]
public class IndexRequest : BaseRequest
{
[Key("carrier")]
public string Carrier { get; set; }
[Key("card_master_hash")]
public string CardMasterHash { get; set; }
}
}

View File

@@ -2,31 +2,83 @@ using MessagePack;
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Responses;
/// <summary>
/// Wire-shape mirrors production's <c>/check/game_start</c> response. Several fields here are
/// NOT read by <c>Cute/GameStartCheckTask.Parse</c> (<c>now_viewer_id</c>, <c>now_name</c>,
/// <c>now_rank</c> — those are consumed by sibling tasks); they're included because prod sends
/// them and the boot worked when we matched prod exactly. Removing them is a regression risk
/// even though the parse-time decompile says they're unused.
/// </summary>
[MessagePackObject]
public class GameStartResponse
{
/// <summary>The signed-in viewer's internal id. Prod always sends.</summary>
[Key("now_viewer_id")]
public long NowViewerId { get; set; }
/// <summary>
/// Whether the user has set a data-transfer password. Prod sends a non-null bool;
/// <c>GameStartCheckTask.Parse</c> gates the read with <c>Keys.Contains</c>.
/// </summary>
[Key("is_set_transition_password")]
public bool IsSetTransitionPassword { get; set; }
/// <summary>Viewer display name. Not read by GameStartCheckTask but sent by prod.</summary>
[Key("now_name")]
public string NowName { get; set; }
public string NowName { get; set; } = string.Empty;
/// <summary>
/// Per-format rank-name map keyed by deck-format id ("1", "2", "4" observed in prod).
/// Stub for now until rank state is persisted; pinned to RankName_010 / RankName_017
/// (matches prod's shape).
/// </summary>
[Key("now_rank")]
public Dictionary<string, string> NowRank { get; set; }
public Dictionary<string, string> NowRank { get; set; } = new();
/// <summary>
/// Tutorial progress — **sent as a string on the wire** ("100" = tutorial complete).
/// <c>GameStartCheckTask.Parse</c> calls <c>.ToInt()</c> so LitJson coerces.
/// </summary>
[Key("now_tutorial_step")]
public string NowTutorialStep { get; set; }
public string NowTutorialStep { get; set; } = "100";
/// <summary>
/// Linked social accounts. Per-entry shape in <see cref="TransitionAccountData"/>.
/// </summary>
[Key("transition_account_data")]
public List<TransitionAccountData> TransitionAccountData { get; set; }
public List<TransitionAccountData> TransitionAccountData { get; set; } = new();
// INTENTIONALLY OMITTED: `rewrite_viewer_id` and `account_delete_reservation_status`.
// Both are presence-checked by the client via `Keys.Contains(...)` + `.ToInt()` with no
// null guard. MessagePack-CSharp writes [Key] properties unconditionally (null → Nil),
// and the System.Text.Json `WhenWritingNull` ignore only affects the plain-JSON path.
// So including these as nullable properties is a guaranteed NRE on the encrypted client
// path. We don't need them — the client tolerates their absence — so don't declare them.
// Re-add only if we have a real value to send.
// --- Agreement / consent state (all required) ---
/// <summary><c>PlayerStaticData.AgreementState</c> enum.</summary>
[Key("tos_state")]
public int TosState { get; set; }
[Key("tos_id")]
public int TosId { get; set; }
/// <summary><c>PlayerStaticData.AgreementState</c> enum.</summary>
[Key("policy_state")]
public int PolicyState { get; set; }
[Key("policy_id")]
public int PolicyId { get; set; }
[Key("kor_authority_id")]
public int KorAuthorityId { get; set; }
/// <summary><c>PlayerStaticData.AgreementState</c> enum.</summary>
[Key("kor_authority_state")]
public int KorAuthorityState { get; set; }
}
/// <summary>Current Terms of Service document id.</summary>
[Key("tos_id")]
public int TosId { get; set; }
/// <summary>Current Privacy Policy document id.</summary>
[Key("policy_id")]
public int PolicyId { get; set; }
/// <summary>Current Korean authority consent document id.</summary>
[Key("kor_authority_id")]
public int KorAuthorityId { get; set; }
}

View File

@@ -8,10 +8,6 @@ public class IndexResponse
{
#region Primitive Returns
[Key("ts_card_rotation")]
public string TsCardRotation { get; set; } = string.Empty;
[Key("is_beginner_mission")]
public int IsBeginnerMission { get; set; }
[Key("spot_point")]
public int SpotPoint { get; set; }
[Key("is_available_colosseum_free_entry")]
@@ -23,29 +19,22 @@ public class IndexResponse
[Key("room_recovery_status")]
public int RoomRecoveryStatus { get; set; }
[Key("is_battle_pass_period")]
public bool IsBattlePassPeriod { get; set; }
public int IsBattlePassPeriod { get; set; }
[Key("card_set_id_for_resource_dl_view")]
public int CardSetIdForResourceDlView { get; set; }
[Key("deck_format")]
public int DeckFormat { get; set; } = 1;
#endregion
#region Basic User Data
/// <summary>
/// The user's tutorial progress state.
/// </summary>
[Key("user_tutorial")]
public UserTutorial UserTutorial { get; set; } = new UserTutorial();
/// <summary>
/// Basic information about the user.
/// </summary>
[Key("user_info")]
public UserInfo UserInfo { get; set; } = new UserInfo();
/// <summary>
/// The in-game currency information for this user.
/// </summary>
[Key("user_crystal_count")]
public UserCurrency UserCurrency { get; set; } = new UserCurrency();
@@ -53,234 +42,163 @@ public class IndexResponse
#region Inventory Data
/// <summary>
/// Items that the user has and how many of each.
/// </summary>
[Key("user_item_list")]
public List<UserItem> UserItems { get; set; } = new List<UserItem>();
/// <summary>
/// Decks for the rotation format.
/// </summary>
[Key("user_item_list")]
public List<UserItem> UserItems { get; set; } = new();
[Key("user_deck_rotation")]
public UserFormatDeckInfo UserRotationDecks { get; set; } = new UserFormatDeckInfo();
public UserFormatDeckInfo UserRotationDecks { get; set; } = new();
/// <summary>
/// Decks for the unlimited format.
/// </summary>
[Key("user_deck_unlimited")]
public UserFormatDeckInfo UserUnlimitedDecks { get; set; } = new UserFormatDeckInfo();
/// <summary>
/// Decks for the unlimited format.
/// </summary>
public UserFormatDeckInfo UserUnlimitedDecks { get; set; } = new();
[Key("user_deck_my_rotation")]
public UserFormatDeckInfo UserMyRotationDecks { get; set; } = new UserFormatDeckInfo();
public UserFormatDeckInfo UserMyRotationDecks { get; set; } = new();
/// <summary>
/// The list of cards and how many of each this user has.
/// </summary>
[Key("user_card_list")]
public List<UserCard> UserCards { get; set; } = new List<UserCard>();
public List<UserCard> UserCards { get; set; } = new();
/// <summary>
/// The classes a user has and their stats.
/// </summary>
[Key("user_class_list")]
public List<UserClass> UserClasses { get; set; } = new List<UserClass>();
public List<UserClass> UserClasses { get; set; } = new();
/// <summary>
/// Mapping of SleeveId to a <see cref="SleeveIdentifier"/> object.
/// Wire is an array; parser iterates by index (LoadDetail.cs:358-360).
/// </summary>
[Key("user_sleeve_list")]
public Dictionary<string, SleeveIdentifier> Sleeves { get; set; } = new Dictionary<string, SleeveIdentifier>();
public List<SleeveIdentifier> Sleeves { get; set; } = new();
/// <summary>
/// The emblems available to this user.
/// </summary>
[Key("user_emblem_list")]
public List<EmblemIdentifier> UserEmblems { get; set; } = new List<EmblemIdentifier>();
public List<EmblemIdentifier> UserEmblems { get; set; } = new();
[Key("user_degree_list")]
public List<DegreeIdentifier> UserDegrees { get; set; } = new();
/// <summary>
/// The degrees available to this user.
/// </summary>
[Key("degree_id")]
public List<DegreeIdentifier> UserDegrees { get; set; } = new List<DegreeIdentifier>();
/// <summary>
/// Leader skins available to the leader.
/// Wire is an array; parser iterates by index (LoadDetail.cs:348-356).
/// </summary>
[Key("user_leader_skin_list")]
public Dictionary<string, UserLeaderSkin> LeaderSkins { get; set; } = new Dictionary<string, UserLeaderSkin>();
public List<UserLeaderSkin> LeaderSkins { get; set; } = new();
/// <summary>
/// Backgrounds for 'My Page' the user has collected.
/// Wire is string[]; parser calls .ToString() on each element (LoadDetail.cs:387-392).
/// </summary>
[Key("user_mypage_list")]
public List<int> MyPageBackgrounds { get; set; } = new List<int>();
public List<string> MyPageBackgrounds { get; set; } = new();
#endregion
#region Advanced Player Data
/// <summary>
/// Maps a deck format (as a string) to info about ranked for that format.
/// Wire is an array of 5 entries; parser uses deck_format as discriminator
/// (LoadDetail.cs:527-538).
/// </summary>
[Key("user_rank")]
public Dictionary<string, UserRankInfo> UserRankInfo { get; set; } = new Dictionary<string, UserRankInfo>();
public List<UserRankInfo> UserRankInfo { get; set; } = new();
/// <summary>
/// The number of ranked matches for each class the user has played.
/// </summary>
[Key("user_rank_match_list")]
public List<UserRankedMatches> UserRankedMatches { get; set; } = new List<UserRankedMatches>();
public List<UserRankedMatches> UserRankedMatches { get; set; } = new();
/// <summary>
/// The daily login bonuses currently going on, including if the player should receive the next reward for any.
/// </summary>
[Key("daily_login_bonus")]
public DailyLoginBonus DailyLoginBonus { get; set; } = new DailyLoginBonus();
public DailyLoginBonus DailyLoginBonus { get; set; } = new();
/// <summary>
/// User configuration for the arena.
/// </summary>
[Key("challenge_config")]
public ArenaConfig ArenaConfig { get; set; } = new ArenaConfig();
public ArenaConfig ArenaConfig { get; set; } = new();
#endregion
#region Global Data
/// <summary>
/// Cards that have had their red ether values overriden.
/// </summary>
[Key("red_ether_overwrite_list")]
public List<RedEtherOverride> RedEtherOverrides { get; set; } = new List<RedEtherOverride>();
public List<RedEtherOverride> RedEtherOverrides { get; set; } = new();
/// <summary>
/// Cards that are currently undergoing maintenance.
/// Wire is a flat number[]; parser passes it straight to SetMaintenanceCardIds
/// (LoadDetail.cs:165).
/// </summary>
[Key("maintenance_card_list")]
public List<CardIdentifier> MaintenanceCards { get; set; } = new List<CardIdentifier>();
/// <summary>
/// The arena formats currently available.
/// </summary>
public List<long> MaintenanceCards { get; set; } = new();
[Key("arena_info")]
public List<ArenaInfo> ArenaInfos { get; set; } = new List<ArenaInfo>();
public List<ArenaInfo> ArenaInfos { get; set; } = new();
/// <summary>
/// Dictionary of rank id to information about that rank.
/// Wire is an array; client uses POSITIONAL logic (index >= 24 = master ranks,
/// LoadDetail.cs:417-422). Order must match repository's ordering.
/// </summary>
[Key("rank_info")]
public Dictionary<string, RankInfo> RankInfo { get; set; } = new Dictionary<string, RankInfo>();
public List<RankInfo> RankInfo { get; set; } = new();
/// <summary>
/// Dictionary mapping a class level to information about that level.
/// Wire is an array; parser iterates by index (LoadDetail.cs:425-434).
/// </summary>
[Key("class_exp")]
public Dictionary<string, ClassExp> ClassExp { get; set; } = new Dictionary<string, ClassExp>();
public List<ClassExp> ClassExp { get; set; } = new();
/// <summary>
/// Card ids that should not show up on loading screen tips.
/// </summary>
[Key("loading_exclusion_card_list")]
public List<long> LoadingTipCardExclusions { get; set; } = new List<long>();
public List<long> LoadingTipCardExclusions { get; set; } = new();
/// <summary>
/// The default settings for every user.
/// </summary>
[Key("default_setting")]
public DefaultSettings DefaultSettings { get; set; } = new DefaultSettings();
public DefaultSettings DefaultSettings { get; set; } = new();
/// <summary>
/// Any cards that are restricted in unlimited, and the maximum count that can be run of the card.
/// </summary>
[Key("unlimited_restricted_base_card_id_list")]
public Dictionary<string, int> UnlimitedBanList { get; set; } = new Dictionary<string, int>();
public Dictionary<string, int> UnlimitedBanList { get; set; } = new();
/// <summary>
/// Sets currently available in rotation.
/// Client unconditionally accesses [1] and [Count-1] (LoadDetail.cs:184) — list MUST
/// have at least 2 entries or the client crashes.
/// </summary>
[Key("rotation_card_set_id_list")]
public List<CardSetIdentifier> RotationSets { get; set; } = new List<CardSetIdentifier>();
public List<CardSetIdentifier> RotationSets { get; set; } = new();
/// <summary>
/// Allows cards out of your 'My Rotation' to still be used. TODO investigate
/// Wire is a flat number[]; parser iterates and reads .ToInt() (LoadDetail.cs:463-468).
/// </summary>
[Key("reprinted_base_card_ids")]
public Dictionary<string, long> ReprintedCards { get; set; } = new Dictionary<string, long>();
public List<long> ReprintedCards { get; set; } = new();
/// <summary>
/// Something to do with destroying cards. TODO investigate
/// </summary>
[Key("spot_cards")]
public Dictionary<string, int> SpotCards { get; set; } = new Dictionary<string, int>();
public Dictionary<string, int> SpotCards { get; set; } = new();
/// <summary>
/// Info about the next set to be released? TODO investigate
/// </summary>
[Key("pre_release_info")]
public PreReleaseInfo? PreReleaseInfo { get; set; }
/// <summary>
/// Information for the 'My Rotation' mode.
/// </summary>
[Key("my_rotation_info")]
public MyRotationInfo? MyRotationInfo { get; set; }
/// <summary>
/// Information about some avatar mode? TODO investigate
/// </summary>
[Key("avatar_info")]
public MyRotationInfo? AvatarRotationInfo { get; set; }
/// <summary>
/// List of features that are undergoing maintenance.
/// </summary>
[Key("feature_maintenance_list")]
public List<FeatureMaintenance> FeatureMaintenances { get; set; } = new List<FeatureMaintenance>();
/// <summary>
/// Special deals on crystal purchases.
/// </summary>
public List<FeatureMaintenance> FeatureMaintenances { get; set; } = new();
[Key("special_crystal_info")]
public List<SpecialCrystalInfo> SpecialCrystalInfos { get; set; } = new List<SpecialCrystalInfo>();
/// <summary>
/// Current battle pass levels and required points for each.
/// </summary>
public List<SpecialCrystalInfo> SpecialCrystalInfos { get; set; } = new();
[Key("battle_pass_level_info")]
public Dictionary<string, BattlePassLevel>? BattlePassLevelInfo { get; set; }
/// <summary>
/// Battlefields that can currently be picked.
/// Wire is string[]; parser calls .ToString() on each element (LoadDetail.cs:493-499).
/// </summary>
[Key("open_battle_field_id_list")]
public Dictionary<string, int> OpenBattlefieldIds { get; set; } = new Dictionary<string, int>();
public List<string> OpenBattlefieldIds { get; set; } = new();
#endregion
#region Misc Data
/// <summary>
/// What loot box features are disabled for this user.
/// </summary>
[Key("loot_box_regulation")]
public LootBoxRegulations LootBoxRegulations { get; set; } = new LootBoxRegulations();
public LootBoxRegulations LootBoxRegulations { get; set; } = new();
/// <summary>
/// Something about whether the user has an invite notification.
/// </summary>
[Key("gathering_info")]
public GatheringInfo GatheringInfo { get; set; } = new GatheringInfo();
public GatheringInfo GatheringInfo { get; set; } = new();
/// <summary>
/// User configuration.
/// Spec is unclear whether this is returned at /load/index or only at /config/* endpoints
/// (load-index.md line 390). Pending live-capture confirmation; harmless extra.
/// </summary>
[Key("user_config")]
public UserConfig UserConfig { get; set; } = new UserConfig();
public UserConfig UserConfig { get; set; } = new();
#endregion
}
}

View File

@@ -5,8 +5,10 @@ namespace SVSim.EmulatedEntrypoint.Models.Dtos.Responses;
[MessagePackObject]
public class SpecialTitleCheckResponse
{
/// <summary>
/// Numeric string. "0"/"1" are the built-in default title screens; any other value
/// is treated as an asset-bundle id. When omitted, the client defaults to "0".
/// </summary>
[Key("title_image_id")]
public int TitleImageId { get; set; }
[Key("title_sound_id")]
public int TitleSoundId { get; set; }
}
public string? TitleImageId { get; set; }
}

View File

@@ -12,5 +12,5 @@ public class SleeveIdentifier
/// The id of the sleeve.
/// </summary>
[Key("sleeve_id")]
public int SleeveId { get; set; }
public long SleeveId { get; set; }
}

View File

@@ -2,15 +2,35 @@ using MessagePack;
namespace SVSim.EmulatedEntrypoint.Models.Dtos;
/// <summary>
/// Per-link entry in <c>transition_account_data</c>. Production sends three string fields per
/// entry even though <c>GameStartCheckTask.Parse</c> only reads <c>social_account_type</c>.
/// The extra two are read by adjacent tasks (<c>GetGameDataByTransitionCode</c>,
/// <c>GetGameDataBySocialAccountTask</c>) — kept here so the wire matches prod regardless of
/// which task ends up consuming the payload.
/// </summary>
[MessagePackObject]
public class TransitionAccountData
{
/// <summary>
/// The social provider's account id (e.g. SteamID as a string). Sent as string on the wire.
/// </summary>
[Key("social_account_id")]
public string SocialAccountId { get; set; }
public string? SocialAccountId { get; set; }
/// <summary>
/// <c>Cute/CuteNetworkDefine.ACCOUNT_TYPE</c> enum, **sent as string** on the wire even
/// though it's numeric. <c>GameStartCheckTask.Parse</c> calls <c>.ToInt()</c> on it so
/// LitJson coerces transparently — but matching prod's string form makes us safer against
/// future client paths that might compare it as a literal.
/// 1=GooglePlay, 2=GameCenter, 3=Facebook, 4=DMM, 5=Steam, 6=AppleID.
/// </summary>
[Key("social_account_type")]
public string SocialAccountType { get; set; }
public string? SocialAccountType { get; set; }
/// <summary>
/// The viewer id this social connection is linked to. Sent as string.
/// </summary>
[Key("connected_viewer_id")]
public string ConnectedViewerId { get; set; }
}
public string? ConnectedViewerId { get; set; }
}

View File

@@ -18,8 +18,12 @@ public class UserInfo
public DateTime LastPlayTime { get; set; }
[Key("is_received_two_pick_mission")]
public int HasReceivedPickTwoMission { get; set; }
/// <summary>
/// Birth date as yyyy-MM-dd. Parser does .ToString() on this field (LoadDetail.cs:203).
/// Format verified against live capture pending.
/// </summary>
[Key("birth")]
public long Birthday { get; set; }
public string Birthday { get; set; } = string.Empty;
[Key("selected_emblem_id")]
public long SelectedEmblemId { get; set; }
[Key("selected_degree_id")]
@@ -45,7 +49,7 @@ public class UserInfo
this.MaxFriend = viewer.Info.MaxFriends;
this.LastPlayTime = viewer.LastLogin;
this.HasReceivedPickTwoMission = viewer.MissionData.HasReceivedPickTwoMission ? 1 : 0;
this.Birthday = viewer.Info.BirthDate.Ticks;
this.Birthday = viewer.Info.BirthDate.ToString("yyyy-MM-dd");
this.SelectedEmblemId = viewer.Info.SelectedEmblem.Id;
this.SelectedDegreeId = viewer.Info.SelectedDegree.Id;
this.MissionChangeTime = viewer.MissionData.MissionChangeTime;
@@ -53,4 +57,4 @@ public class UserInfo
this.IsOfficial = viewer.Info.IsOfficial ? 1 : 0;
this.IsOfficialMarkDisplayed = viewer.Info.IsOfficialMarkDisplayed ? 1 : 0;
}
}
}

View File

@@ -21,7 +21,9 @@ public class UserLeaderSkin
{
this.Id = leaderSkin.Id;
this.Name = leaderSkin.Name;
this.ClassId = leaderSkin.Class.Id;
// Class is nullable — BaseDataSeeder maps CSV class_chara_id=0 to null. Fall back to
// the FK column (also nullable) and finally 0 for class-agnostic skins.
this.ClassId = leaderSkin.Class?.Id ?? leaderSkin.ClassId ?? 0;
this.EmoteId = leaderSkin.EmoteId;
this.IsOwned = isOwned;
}

View File

@@ -20,7 +20,7 @@ public class UserRankInfo
[Key("is_master_rank")]
public int IsMasterRank { get; set; }
[Key("is_grand_master_rank")]
public bool IsGrandMasterRank { get; set; }
public int IsGrandMasterRank { get; set; }
[Key("master_point")]
public int MasterPoints { get; set; }
[Key("period_grand_master_point")]