diff --git a/DorkNet.Server/Services/PlayerService.cs b/DorkNet.Server/Services/PlayerService.cs index 0d0e396..faa37e5 100644 --- a/DorkNet.Server/Services/PlayerService.cs +++ b/DorkNet.Server/Services/PlayerService.cs @@ -174,13 +174,21 @@ public class PlayerService(DorkNetDbContext db, RoomService rooms, ILogger WireDates.FarFuture; + if (nameDrift || banDrift) { await db.Players .Where(p => p.Id == SystemAccountId) .ExecuteUpdateAsync(s => s .SetProperty(p => p.Username, SystemAccountUsername) - .SetProperty(p => p.DisplayName, SystemAccountUsername)); + .SetProperty(p => p.DisplayName, SystemAccountUsername) + .SetProperty(p => p.BannedUntil, WireDates.FarFuture)); } return; } @@ -204,7 +212,10 @@ public class PlayerService(DorkNetDbContext db, RoomService rooms, ILogger now. - BannedUntil = new DateTime(9999, 12, 31, 0, 0, 0, DateTimeKind.Utc), + // WireDates.FarFuture (2099), NOT year 9999 — a 9999 BannedUntil + // serialised to the watch overflows its local-timezone date parse + // in some DST timezones and corrupts the response. See WireDates. + BannedUntil = WireDates.FarFuture, PasswordHash = null, CreatedAt = new DateTime(2020, 3, 6, 0, 0, 0, DateTimeKind.Utc), LastSeenAt = new DateTime(2020, 3, 6, 0, 0, 0, DateTimeKind.Utc), diff --git a/DorkNet.Server/Services/StoreService.cs b/DorkNet.Server/Services/StoreService.cs index 6893373..b2de155 100644 --- a/DorkNet.Server/Services/StoreService.cs +++ b/DorkNet.Server/Services/StoreService.cs @@ -867,8 +867,11 @@ public class StoreService(DorkNetDbContext db, LevelService level, IConfiguratio { StorefrontType = storefrontType, StorefrontName = name, - NextUpdate = "9999-12-31T00:00:00Z", - NewUntil = "9999-12-31T00:00:00Z", + // NOT year 9999: that overflows the watch's local-timezone date + // parse in some DST timezones → "malformed RecNet response" and a + // broken store for that player only. See WireDates. + NextUpdate = WireDates.FarFutureIso, + NewUntil = WireDates.FarFutureIso, StoreItems = storeItems, }; } diff --git a/DorkNet.Server/WireDates.cs b/DorkNet.Server/WireDates.cs new file mode 100644 index 0000000..8043d0e --- /dev/null +++ b/DorkNet.Server/WireDates.cs @@ -0,0 +1,32 @@ +namespace DorkNet.Server; + +/// +/// Canonical "far future / never expires" sentinel dates for anything we +/// serialise back to the watch. +/// +/// DO NOT use DateTime.MaxValue or year 9999 in client-facing wire +/// data. The 2020.12 watch parses ISO-8601 timestamps and adjusts them to the +/// player's LOCAL timezone (DateTimeParse.ParseISO8601 → AdjustTimeZoneToLocal +/// → TimeZoneInfo DST resolution). For a player whose timezone has DST +/// adjustment rules, resolving the DST transition for a year-9999 instant +/// pushes the computed transition point past DateTime.MaxValue (year +/// 10000) and throws ArgumentOutOfRangeException: Year, Month, and Day +/// parameters describe an un-representable DateTime. That blows up the +/// WHOLE response deserialization → "Received malformed RecNet response" → the +/// store / gift drops / promo board silently break, but ONLY for players in +/// the affected timezones (everyone else parses the same payload fine, which +/// makes it look like a per-player gremlin). +/// +/// 2099 is effectively "forever" for a 2020-era client and is nowhere near the +/// overflow boundary in any timezone. +/// +public static class WireDates +{ + /// Far-future sentinel as a (UTC). + public static readonly DateTime FarFuture = + new(2099, 12, 31, 0, 0, 0, DateTimeKind.Utc); + + /// Far-future sentinel pre-rendered as the ISO-8601 Z string the + /// watch expects (used where the DTO field is a raw string). + public const string FarFutureIso = "2099-12-31T00:00:00Z"; +}