Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions GenOnlineService/Database/Database.MatchHistory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,76 @@ public static async Task<ulong> CreatePlaceholderMatchHistory(
}
}

/// <summary>
/// 1v1 tie-break used when no slot reported a win.
///
/// A player who stayed connected and reported won=false has explicitly conceded, and must not be
/// handed the win merely because they left the lobby last. If exactly one of the two players has an
/// in-game disconnect record, that player is the only one who *could not* report, and their opponent
/// has already declared the loss — so the disconnected player won, and there is nothing to guess.
///
/// Deliberately 1v1 only: with teams or FFA a single disconnect says nothing about who won, so those
/// shapes fall through to the timestamp fallback unchanged.
/// </summary>
/// <returns>TRUE when the rule decided the match; FALSE otherwise, with strReason saying why not.</returns>
public static bool TryResolveOneVsOneByDisconnect(
IReadOnlyDictionary<int, MatchdataMemberModel> members,
IEnumerable<Int64> disconnectedUserIDs,
out int winningSlotIndex,
out Int64 winningUserID,
out Int64 concedingUserID,
out string strReason)
{
winningSlotIndex = -1;
winningUserID = -1;
concedingUserID = -1;

// Observers and AI/placeholder slots (user_id <= 0) never quit the game, so they are not
// participants for this purpose - the same filter the timestamp fallback applies.
List<int> lstActiveSlots = new();
foreach (var member in members)
{
if (member.Value.side != Constants.OBSERVER_SIDE_VALUE && member.Value.user_id > 0)
{
lstActiveSlots.Add(member.Key);
}
}

if (lstActiveSlots.Count != 2)
{
strReason = $"not a 1v1 ({lstActiveSlots.Count} active participants)";
return false;
}

// Materialize once - the caller passes ConcurrentDictionary.Keys, tests pass arrays
List<Int64> lstDisconnectedUserIDs = disconnectedUserIDs.ToList();

List<int> lstDisconnectedSlots = new();
foreach (int slotIndex in lstActiveSlots)
{
if (lstDisconnectedUserIDs.Contains(members[slotIndex].user_id))
{
lstDisconnectedSlots.Add(slotIndex);
}
}

if (lstDisconnectedSlots.Count != 1)
{
strReason = $"{lstDisconnectedSlots.Count} of 2 players have an in-game disconnect record (needs exactly 1)";
return false;
}

// Exactly two active slots, so the one that is not the winner is the conceding player
winningSlotIndex = lstDisconnectedSlots[0];
int concedingSlotIndex = (lstActiveSlots[0] == winningSlotIndex) ? lstActiveSlots[1] : lstActiveSlots[0];

winningUserID = members[winningSlotIndex].user_id;
concedingUserID = members[concedingSlotIndex].user_id;
strReason = $"user={winningUserID} disconnected in-game, user={concedingUserID} stayed connected and reported won=false";

return true;
}

public static async Task DetermineLobbyWinnerIfNotPresent(
AppDbContext db,
GenOnlineService.Lobby lobby)
Expand Down Expand Up @@ -613,6 +683,27 @@ public static async Task DetermineLobbyWinnerIfNotPresent(
Console.WriteLine($"[WinnerDet] IngameAbandon: user={_kv.Key} at={_kv.Value:O}");
foreach (var _kv in lobby.TimeMemberLeft)
Console.WriteLine($"[WinnerDet] MemberLeft: user={_kv.Key} at={_kv.Value:O}");
// 6a. 1v1 special case - see TryResolveOneVsOneByDisconnect for the reasoning
if (TryResolveOneVsOneByDisconnect(members, lobby.TimePlayerAbandonedIngame.Keys,
out int oneVsOneWinningSlot, out Int64 oneVsOneWinningUserID, out _, out string strOneVsOneReason))
{
Console.WriteLine($"[WinnerDet] Match={lobby.MatchID}: 1v1 disconnect rule — {strOneVsOneReason} → awarding to user={oneVsOneWinningUserID} slot={oneVsOneWinningSlot} (skipping last-to-leave fallback).");

foreach (var kv in members)
{
if (kv.Value.side == Constants.OBSERVER_SIDE_VALUE)
continue;

bool bIsWinner = kv.Key == oneVsOneWinningSlot;
Console.WriteLine($"[WinnerDet] Match={lobby.MatchID}: marking slot={kv.Key} user={kv.Value.user_id} as {(bIsWinner ? "WINNER" : "loser")}.");
await UpdateMatchHistorySetWinFlag(db, lobby.MatchID, kv.Key, bIsWinner);
}

return;
}

Console.WriteLine($"[WinnerDet] Match={lobby.MatchID}: 1v1 disconnect rule declined — {strOneVsOneReason}; using last-to-leave fallback.");

DateTime latestLeave = DateTime.MinValue;
MatchdataMemberModel? lastPlayerNullable = null;
int lastSlot = -1;
Expand Down