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);
}