fix(load/index): UserInfo dates as nullable yyyy-MM-dd HH:mm:ss strings
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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Globalization;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
using SVSim.Database.Models;
|
using SVSim.Database.Models;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
@@ -7,6 +8,9 @@ namespace SVSim.EmulatedEntrypoint.Models.Dtos;
|
|||||||
[MessagePackObject]
|
[MessagePackObject]
|
||||||
public class UserInfo
|
public class UserInfo
|
||||||
{
|
{
|
||||||
|
/// <summary>Wire format prod uses for the two datetime fields here. No 'T', no fractions, no zone.</summary>
|
||||||
|
private const string ProdDateTimeFormat = "yyyy-MM-dd HH:mm:ss";
|
||||||
|
|
||||||
[JsonPropertyName("device_type")]
|
[JsonPropertyName("device_type")]
|
||||||
[Key("device_type")]
|
[Key("device_type")]
|
||||||
public int DeviceType { get; set; }
|
public int DeviceType { get; set; }
|
||||||
@@ -19,9 +23,15 @@ public class UserInfo
|
|||||||
[JsonPropertyName("max_friend")]
|
[JsonPropertyName("max_friend")]
|
||||||
[Key("max_friend")]
|
[Key("max_friend")]
|
||||||
public int MaxFriend { get; set; }
|
public int MaxFriend { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Wire format <c>"yyyy-MM-dd HH:mm:ss"</c> (space-separated, no 'T', no Z, no fractions).
|
||||||
|
/// Null for fresh accounts that have never played — prod omits/nulls this rather than
|
||||||
|
/// emitting <c>DateTime.MinValue</c> with .NET's default ISO-8601-with-Z serialization,
|
||||||
|
/// which can crash the client's DateTime parser.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("last_play_time")]
|
[JsonPropertyName("last_play_time")]
|
||||||
[Key("last_play_time")]
|
[Key("last_play_time")]
|
||||||
public DateTime LastPlayTime { get; set; }
|
public string? LastPlayTime { get; set; }
|
||||||
[JsonPropertyName("is_received_two_pick_mission")]
|
[JsonPropertyName("is_received_two_pick_mission")]
|
||||||
[Key("is_received_two_pick_mission")]
|
[Key("is_received_two_pick_mission")]
|
||||||
public int HasReceivedPickTwoMission { get; set; }
|
public int HasReceivedPickTwoMission { get; set; }
|
||||||
@@ -38,9 +48,10 @@ public class UserInfo
|
|||||||
[JsonPropertyName("selected_degree_id")]
|
[JsonPropertyName("selected_degree_id")]
|
||||||
[Key("selected_degree_id")]
|
[Key("selected_degree_id")]
|
||||||
public int SelectedDegreeId { get; set; }
|
public int SelectedDegreeId { get; set; }
|
||||||
|
/// <summary>Same format/null rules as <see cref="LastPlayTime"/>.</summary>
|
||||||
[JsonPropertyName("mission_change_time")]
|
[JsonPropertyName("mission_change_time")]
|
||||||
[Key("mission_change_time")]
|
[Key("mission_change_time")]
|
||||||
public DateTime MissionChangeTime { get; set; }
|
public string? MissionChangeTime { get; set; }
|
||||||
[JsonPropertyName("mission_receive_type")]
|
[JsonPropertyName("mission_receive_type")]
|
||||||
[Key("mission_receive_type")]
|
[Key("mission_receive_type")]
|
||||||
public int MissionReceiveType { get; set; }
|
public int MissionReceiveType { get; set; }
|
||||||
@@ -61,14 +72,17 @@ public class UserInfo
|
|||||||
this.Name = viewer.DisplayName;
|
this.Name = viewer.DisplayName;
|
||||||
this.CountryCode = viewer.Info.CountryCode;
|
this.CountryCode = viewer.Info.CountryCode;
|
||||||
this.MaxFriend = viewer.Info.MaxFriends;
|
this.MaxFriend = viewer.Info.MaxFriends;
|
||||||
this.LastPlayTime = viewer.LastLogin;
|
this.LastPlayTime = FormatProdDateTime(viewer.LastLogin);
|
||||||
this.HasReceivedPickTwoMission = viewer.MissionData.HasReceivedPickTwoMission ? 1 : 0;
|
this.HasReceivedPickTwoMission = viewer.MissionData.HasReceivedPickTwoMission ? 1 : 0;
|
||||||
this.Birthday = viewer.Info.BirthDate.ToString("yyyy-MM-dd");
|
this.Birthday = viewer.Info.BirthDate.ToString("yyyy-MM-dd");
|
||||||
this.SelectedEmblemId = viewer.Info.SelectedEmblem.Id;
|
this.SelectedEmblemId = viewer.Info.SelectedEmblem.Id;
|
||||||
this.SelectedDegreeId = viewer.Info.SelectedDegree.Id;
|
this.SelectedDegreeId = viewer.Info.SelectedDegree.Id;
|
||||||
this.MissionChangeTime = viewer.MissionData.MissionChangeTime;
|
this.MissionChangeTime = FormatProdDateTime(viewer.MissionData.MissionChangeTime);
|
||||||
this.MissionReceiveType = viewer.MissionData.MissionReceiveType;
|
this.MissionReceiveType = viewer.MissionData.MissionReceiveType;
|
||||||
this.IsOfficial = viewer.Info.IsOfficial ? 1 : 0;
|
this.IsOfficial = viewer.Info.IsOfficial ? 1 : 0;
|
||||||
this.IsOfficialMarkDisplayed = viewer.Info.IsOfficialMarkDisplayed ? 1 : 0;
|
this.IsOfficialMarkDisplayed = viewer.Info.IsOfficialMarkDisplayed ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? FormatProdDateTime(DateTime dt)
|
||||||
|
=> dt == default ? null : dt.ToString(ProdDateTimeFormat, CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user