From ef58ba789f66d72dbe494def9cc14cd8f2ac6492 Mon Sep 17 00:00:00 2001 From: JoKeRZH429 <83122870+JoKeRZH429@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:29:17 +0500 Subject: [PATCH 1/2] feat: accept desynced field in match outcome payload --- GenOnlineService/Controllers/Lobby/LobbyController.cs | 4 +++- GenOnlineService/Database/Database.MatchHistory.cs | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/GenOnlineService/Controllers/Lobby/LobbyController.cs b/GenOnlineService/Controllers/Lobby/LobbyController.cs index 00e3404..6051a97 100644 --- a/GenOnlineService/Controllers/Lobby/LobbyController.cs +++ b/GenOnlineService/Controllers/Lobby/LobbyController.cs @@ -303,6 +303,7 @@ public async Task Delete(Int64 lobbyID) && data.ContainsKey("total_money") && data.ContainsKey("won") && data.ContainsKey("side") + && data.ContainsKey("desynced") ) { Int64 user_id = TokenHelper.GetUserID(this); @@ -321,6 +322,7 @@ public async Task Delete(Int64 lobbyID) int total_money = data["total_money"].GetInt32(); int side = data["side"].GetInt32(); bool won = data["won"].GetBoolean(); + bool desynced = data["desynced"].GetBoolean(); UInt64 match_id = data["match_id"].GetUInt64(); // were they really in the match they claim to be in? @@ -340,7 +342,7 @@ public async Task Delete(Int64 lobbyID) // store in DB await using var db = await _dbFactory.CreateDbContextAsync(); await Database.MatchHistory.CommitPlayerOutcome(db, slotIndexInLobby, match_id, side, - buildings_built, buildings_killed, buildings_lost, units_built, units_killed, units_lost, total_money, won); + buildings_built, buildings_killed, buildings_lost, units_built, units_killed, units_lost, total_money, won, desynced); } } } diff --git a/GenOnlineService/Database/Database.MatchHistory.cs b/GenOnlineService/Database/Database.MatchHistory.cs index b76b8ef..d20b721 100644 --- a/GenOnlineService/Database/Database.MatchHistory.cs +++ b/GenOnlineService/Database/Database.MatchHistory.cs @@ -211,6 +211,10 @@ public struct MatchdataMemberModel [JsonConverter(typeof(BoolFromIntConverter))] public bool won { get; set; } = false; // tinyint(4) DEFAULT NULL + + [JsonConverter(typeof(BoolFromIntConverter))] + public bool desynced {get; set; } = false; + public List metadata { get; set; } = new List(); public MatchdataMemberModel() @@ -333,7 +337,8 @@ public static async Task CommitPlayerOutcome( int unitsKilled, int unitsLost, int totalMoney, - bool won) + bool won, + bool desynced) { if (slotIndex < 0 || slotIndex > 7) return; @@ -361,6 +366,7 @@ public static async Task CommitPlayerOutcome( model.units_lost = unitsLost; model.total_money = totalMoney; model.won = won; + model.desynced = desynced; // 4. Serialize back string updatedJson = JsonSerializer.Serialize(model); From eba2f5eac2ff4be8f2348998aea538d6d2d4b33f Mon Sep 17 00:00:00 2001 From: JoKeRZH429 <83122870+JoKeRZH429@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:29:55 +0500 Subject: [PATCH 2/2] feat: award No Result (loss to all) when a desync is reported --- .../Database/Database.MatchHistory.cs | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/GenOnlineService/Database/Database.MatchHistory.cs b/GenOnlineService/Database/Database.MatchHistory.cs index d20b721..c441ea8 100644 --- a/GenOnlineService/Database/Database.MatchHistory.cs +++ b/GenOnlineService/Database/Database.MatchHistory.cs @@ -551,7 +551,22 @@ public static async Task DetermineLobbyWinnerIfNotPresent( } } - // 3. Build winner groups from active, non-observer members. + // 3. Check if any member reported a desync. If so, mark all non-observer members as losers which the + // ExternalLeaderboardClient interprets as a No Result + bool anyDesync = members.Values.Any(m => m.desynced); + if (anyDesync) + { + foreach (var kv in members) + { + if (kv.Value.side == Constants.OBSERVER_SIDE_VALUE) + continue; + + await UpdateMatchHistorySetWinFlag(db, lobby.MatchID, kv.Key, false); + } + return; + } + + // 4. Build winner groups from active, non-observer members. // Teamless players are treated individually using synthetic keys. Dictionary> winGroups = new(); foreach (var kv in members) @@ -569,7 +584,7 @@ public static async Task DetermineLobbyWinnerIfNotPresent( slotList.Add(kv.Key); } - // 4. Compare winner groups by report count. + // 5. Compare winner groups by report count. // A unique maximum wins; ties or no winner groups fall back to // the abandoned timestamp-based resolution. int? conclusiveWinningTeam = null; @@ -593,7 +608,7 @@ public static async Task DetermineLobbyWinnerIfNotPresent( } } - // 5. If conclusively determined, propagate the win to the team + // 6. If conclusively determined, propagate the win to the team // and explicitly award a loss to everyone else. if (conclusiveWinningTeam != null || conclusiveWinningSlot != -1) { @@ -609,7 +624,7 @@ public static async Task DetermineLobbyWinnerIfNotPresent( return; } - // 6. No winner — determine who quit last (= winner) using the most accurate timing available. + // 7. No winner — determine who quit last (= winner) using the most accurate timing available. // Prefer TimePlayerAbandonedIngame (recorded the instant each player's WS dropped while // in-game) over TimeMemberLeft (recorded when the player was structurally removed from the // lobby, which can happen much later, or earlier due to a fresh-session reconnect, skewing