From d3ef76324fdb777147c96466bd7807c5e9fcc7eb Mon Sep 17 00:00:00 2001 From: gamer147 Date: Thu, 28 May 2026 18:03:32 -0400 Subject: [PATCH] fix(load/index): UserInfo dates as nullable yyyy-MM-dd HH:mm:ss strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LastPlayTime and MissionChangeTime were typed as DateTime, which STJ serialised as "0001-01-01T00:00:00.0000000Z" for a fresh viewer (DateTime.MinValue). Prod's wire shape is "yyyy-MM-dd HH:mm:ss" (no T, no Z, no fractional seconds) when present and null when absent — verified against data_dumps/traffic_prod_tutorial.ndjson. The .NET default format has a real chance of crashing the client's DateTime.Parse path on any code that reads either field, and the fields are presence-sensitive (NetworkTask-family Keys.Contains followed by ToDateTime), so emitting the .NET default reaches the client as a stale-but-present value. Switching the properties to string? + FormatProdDateTime helper: - non-default DateTime -> "yyyy-MM-dd HH:mm:ss" - DateTime.MinValue -> null (omitted from wire via global WhenWritingNull policy in Program.cs) Co-Authored-By: Claude Opus 4.7 --- .../Models/Dtos/UserInfo.cs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/UserInfo.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/UserInfo.cs index 994a0ce..469975e 100644 --- a/SVSim.EmulatedEntrypoint/Models/Dtos/UserInfo.cs +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/UserInfo.cs @@ -1,3 +1,4 @@ +using System.Globalization; using MessagePack; using SVSim.Database.Models; using System.Text.Json.Serialization; @@ -7,6 +8,9 @@ namespace SVSim.EmulatedEntrypoint.Models.Dtos; [MessagePackObject] public class UserInfo { + /// Wire format prod uses for the two datetime fields here. No 'T', no fractions, no zone. + private const string ProdDateTimeFormat = "yyyy-MM-dd HH:mm:ss"; + [JsonPropertyName("device_type")] [Key("device_type")] public int DeviceType { get; set; } @@ -19,9 +23,15 @@ public class UserInfo [JsonPropertyName("max_friend")] [Key("max_friend")] public int MaxFriend { get; set; } + /// + /// Wire format "yyyy-MM-dd HH:mm:ss" (space-separated, no 'T', no Z, no fractions). + /// Null for fresh accounts that have never played — prod omits/nulls this rather than + /// emitting DateTime.MinValue with .NET's default ISO-8601-with-Z serialization, + /// which can crash the client's DateTime parser. + /// [JsonPropertyName("last_play_time")] [Key("last_play_time")] - public DateTime LastPlayTime { get; set; } + public string? LastPlayTime { get; set; } [JsonPropertyName("is_received_two_pick_mission")] [Key("is_received_two_pick_mission")] public int HasReceivedPickTwoMission { get; set; } @@ -38,9 +48,10 @@ public class UserInfo [JsonPropertyName("selected_degree_id")] [Key("selected_degree_id")] public int SelectedDegreeId { get; set; } + /// Same format/null rules as . [JsonPropertyName("mission_change_time")] [Key("mission_change_time")] - public DateTime MissionChangeTime { get; set; } + public string? MissionChangeTime { get; set; } [JsonPropertyName("mission_receive_type")] [Key("mission_receive_type")] public int MissionReceiveType { get; set; } @@ -61,14 +72,17 @@ public class UserInfo this.Name = viewer.DisplayName; this.CountryCode = viewer.Info.CountryCode; this.MaxFriend = viewer.Info.MaxFriends; - this.LastPlayTime = viewer.LastLogin; + this.LastPlayTime = FormatProdDateTime(viewer.LastLogin); this.HasReceivedPickTwoMission = viewer.MissionData.HasReceivedPickTwoMission ? 1 : 0; 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; + this.MissionChangeTime = FormatProdDateTime(viewer.MissionData.MissionChangeTime); this.MissionReceiveType = viewer.MissionData.MissionReceiveType; this.IsOfficial = viewer.Info.IsOfficial ? 1 : 0; this.IsOfficialMarkDisplayed = viewer.Info.IsOfficialMarkDisplayed ? 1 : 0; } + + private static string? FormatProdDateTime(DateTime dt) + => dt == default ? null : dt.ToString(ProdDateTimeFormat, CultureInfo.InvariantCulture); }