diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/DailyLoginBonus.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/DailyLoginBonus.cs index b72e0eb4..5346c40b 100644 --- a/SVSim.EmulatedEntrypoint/Models/Dtos/DailyLoginBonus.cs +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/DailyLoginBonus.cs @@ -3,16 +3,24 @@ using System.Text.Json.Serialization; namespace SVSim.EmulatedEntrypoint.Models.Dtos; +/// +/// Wire shape: { normal?, total?, campaign? (array) }. Client parser at +/// LoadDetail.cs:553 reads exactly these three keys. normal presence drives +/// IsExistLoginBonusData() / popup eligibility. +/// [MessagePackObject] public class DailyLoginBonus { - [JsonPropertyName("total")] - [Key("total")] - public LoginBonusCampaign? Total { get; set; } - [JsonPropertyName("normal")] - [Key("normal")] + [JsonPropertyName("normal")] [Key("normal")] public LoginBonusCampaign? Normal { get; set; } - [JsonPropertyName("campaign")] - [Key("campaign")] - public LoginBonusCampaign? Campaign { get; set; } -} \ No newline at end of file + + [JsonPropertyName("total")] [Key("total")] + public LoginBonusCampaign? Total { get; set; } + + /// + /// Array of campaign panels (LoadDetail.cs:583 iterates jsonData3[i]). Empty list + /// when no specials are active — prod sends [] not null in that case. + /// + [JsonPropertyName("campaign")] [Key("campaign")] + public List Campaign { get; set; } = new(); +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusCampaign.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusCampaign.cs index 7a4aedeb..65eab5b7 100644 --- a/SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusCampaign.cs +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusCampaign.cs @@ -3,25 +3,38 @@ using System.Text.Json.Serialization; namespace SVSim.EmulatedEntrypoint.Models.Dtos; +/// +/// One login-bonus campaign panel. Used for normal, each campaign array entry, and (when +/// populated) total. campaign_id and img ship as strings on the wire. Client +/// consumers: NormalData (reads name, now_count, is_next_reward, reward[]), +/// SpecialData (also reads start_date, end_date, img, is_one_day_multi_rewards), +/// ContinuousData (reads only reward[]). +/// [MessagePackObject] public class LoginBonusCampaign { - [JsonPropertyName("name")] - [Key("name")] + [JsonPropertyName("name")] [Key("name")] public string Name { get; set; } = string.Empty; - [JsonPropertyName("campaign_id")] - [Key("campaign_id")] - public int CampaignId { get; set; } - [JsonPropertyName("img")] - [Key("img")] - public int Image { get; set; } - [JsonPropertyName("now_count")] - [Key("now_count")] + + [JsonPropertyName("campaign_id")] [Key("campaign_id")] + public string CampaignId { get; set; } = "0"; + + [JsonPropertyName("img")] [Key("img")] + public string Img { get; set; } = "0"; + + [JsonPropertyName("now_count")] [Key("now_count")] public int NowCount { get; set; } - [JsonPropertyName("is_next_reward")] - [Key("is_next_reward")] + + [JsonPropertyName("is_next_reward")] [Key("is_next_reward")] public bool IsNextReward { get; set; } - [JsonPropertyName("reward")] - [Key("reward")] - public List Rewards { get; set; } = new List(); -} \ No newline at end of file + + [JsonPropertyName("reward")] [Key("reward")] + public List Rewards { get; set; } = new(); + + /// + /// SpecialData-only on the client (SpecialData.cs:50). Server emits it on every panel + /// for byte-faithful replay; client ignores it on Normal/Total paths. + /// + [JsonPropertyName("is_one_day_multi_rewards")] [Key("is_one_day_multi_rewards")] + public bool IsOneDayMultiRewards { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusReward.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusReward.cs index 5e3c04da..28cd4788 100644 --- a/SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusReward.cs +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/LoginBonusReward.cs @@ -3,34 +3,23 @@ using System.Text.Json.Serialization; namespace SVSim.EmulatedEntrypoint.Models.Dtos; +/// +/// One day's reward in a login-bonus cycle. All numeric fields ship as quoted strings on +/// the wire — prod-confirmed shape from traffic_prod_tutorial.ndjson. Client uses +/// JsonData.ToInt() so it tolerates either form, but we match prod exactly. +/// [MessagePackObject] public class LoginBonusReward { - /// - /// The effect shown ingame. - /// - [JsonPropertyName("effect_id")] - [Key("effect_id")] - public int EffectId { get; set; } - - /// - /// The type of reward. - /// - [JsonPropertyName("reward_type")] - [Key("reward_type")] - public int RewardType { get; set; } - - /// - /// A more specified reward type (ie if a pack, which pack). - /// - [JsonPropertyName("reward_detail_id")] - [Key("reward_detail_id")] - public int RewardDetailId { get; set; } - - /// - /// The count of the reward. - /// - [JsonPropertyName("reward_number")] - [Key("reward_number")] - public int RewardNumber { get; set; } -} \ No newline at end of file + [JsonPropertyName("effect_id")] [Key("effect_id")] + public string EffectId { get; set; } = "0"; + + [JsonPropertyName("reward_type")] [Key("reward_type")] + public string RewardType { get; set; } = "0"; + + [JsonPropertyName("reward_detail_id")] [Key("reward_detail_id")] + public string RewardDetailId { get; set; } = "0"; + + [JsonPropertyName("reward_number")] [Key("reward_number")] + public string RewardNumber { get; set; } = "0"; +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/IndexResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/IndexResponse.cs index 29eb205b..85aee91f 100644 --- a/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/IndexResponse.cs +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/IndexResponse.cs @@ -149,8 +149,10 @@ public class IndexResponse public List UserRankedMatches { get; set; } = new(); /// - /// Spec: optional. Shape is {normal?, total?, campaign?[]} per common/types.ts.md DailyLoginBonus. - /// Populated by ILoginBonusService when the viewer has an active bonus period; null otherwise. + /// Spec: optional. Shape {normal?, total?, campaign?[]} — client parses three keys + /// in LoadDetail.cs:553. normal presence triggers the login-bonus popup + /// (NextSceneSwitcher.cs:103). Populated by ILoginBonusService at /load/index time; + /// null when the viewer has already claimed today's bonus. /// [JsonPropertyName("daily_login_bonus")] [Key("daily_login_bonus")] diff --git a/SVSim.UnitTests/Dtos/DailyLoginBonusWireShapeTests.cs b/SVSim.UnitTests/Dtos/DailyLoginBonusWireShapeTests.cs new file mode 100644 index 00000000..8513a6e4 --- /dev/null +++ b/SVSim.UnitTests/Dtos/DailyLoginBonusWireShapeTests.cs @@ -0,0 +1,59 @@ +using System.Text.Json; +using NUnit.Framework; +using SVSim.EmulatedEntrypoint.Models.Dtos; + +namespace SVSim.UnitTests.Dtos; + +public class DailyLoginBonusWireShapeTests +{ + private static readonly JsonSerializerOptions Opts = new() + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, + }; + + [Test] + public void Reward_numeric_fields_serialize_as_strings() + { + var r = new LoginBonusReward + { + EffectId = "1", RewardType = "9", RewardDetailId = "0", RewardNumber = "20" + }; + + var json = JsonSerializer.Serialize(r, Opts); + using var doc = JsonDocument.Parse(json); + Assert.That(doc.RootElement.GetProperty("effect_id").ValueKind, Is.EqualTo(JsonValueKind.String)); + Assert.That(doc.RootElement.GetProperty("reward_type").ValueKind, Is.EqualTo(JsonValueKind.String)); + Assert.That(doc.RootElement.GetProperty("reward_detail_id").ValueKind, Is.EqualTo(JsonValueKind.String)); + Assert.That(doc.RootElement.GetProperty("reward_number").ValueKind, Is.EqualTo(JsonValueKind.String)); + } + + [Test] + public void Campaign_id_and_img_serialize_as_strings() + { + var c = new LoginBonusCampaign { CampaignId = "3", Img = "0", Name = "Daily Bonus" }; + var json = JsonSerializer.Serialize(c, Opts); + using var doc = JsonDocument.Parse(json); + Assert.That(doc.RootElement.GetProperty("campaign_id").ValueKind, Is.EqualTo(JsonValueKind.String)); + Assert.That(doc.RootElement.GetProperty("img").ValueKind, Is.EqualTo(JsonValueKind.String)); + } + + [Test] + public void Campaign_supports_is_one_day_multi_rewards_flag() + { + var c = new LoginBonusCampaign { IsOneDayMultiRewards = false }; + var json = JsonSerializer.Serialize(c, Opts); + using var doc = JsonDocument.Parse(json); + Assert.That(doc.RootElement.TryGetProperty("is_one_day_multi_rewards", out var v), Is.True); + Assert.That(v.ValueKind, Is.EqualTo(JsonValueKind.False)); + } + + [Test] + public void DailyLoginBonus_campaign_is_an_array() + { + var d = new DailyLoginBonus { Normal = new LoginBonusCampaign(), Campaign = new List() }; + var json = JsonSerializer.Serialize(d, Opts); + using var doc = JsonDocument.Parse(json); + Assert.That(doc.RootElement.GetProperty("campaign").ValueKind, Is.EqualTo(JsonValueKind.Array)); + } +}