forfeiture, abandoning, entry_fee_collection at start

This commit is contained in:
2026-08-14 11:13:01 +05:30
parent 9970488938
commit 74d9fae6e6
54 changed files with 6214 additions and 2576 deletions
+553 -34
View File
@@ -26,7 +26,11 @@ public class GameManager : NetworkBehaviour
public static void ConfigureDedicatedMatchReporting(int matchId, string secret=null, string internalApiBaseUrl = null)
{
DedicatedMatchId = matchId;
DedicatedMatchSecret = secret ?? "38516e39d3406225b7c3015a66e5d25c54e9b2024c0755212122bdcea3e3a328";
if (string.IsNullOrEmpty(secret))
secret = Environment.GetEnvironmentVariable("DEDICATED_SERVER_SECRET");
DedicatedMatchSecret = string.IsNullOrEmpty(secret)
? "38516e39d3406225b7c3015a66e5d25c54e9b2024c0755212122bdcea3e3a328"
: secret;
if (!string.IsNullOrWhiteSpace(internalApiBaseUrl))
DedicatedInternalApiBase = internalApiBaseUrl.TrimEnd('/');
@@ -136,16 +140,26 @@ public class GameManager : NetworkBehaviour
return "";
}
/// <summary>Sets match <c>status</c> to <c>-1</c> (room closed). Safe to call from shutdown paths; runs synchronously so it completes before <c>Application.Quit</c>.</summary>
/// <summary>
/// If entry fees were collected and no winner was reported yet, settle via blocking <c>/winner</c>
/// (remaining player or last-disconnect-wins) before room close / quit.
/// </summary>
public static void SettleWinnerIfNeededBeforeShutdown()
{
if (instance == null)
return;
instance.SettleWinnerIfNeededBeforeShutdownInstance();
}
/// <summary>Sets match <c>status</c> to <c>-1</c> (room closed). Settles winner first when escrow is open. Synchronous for <c>Application.Quit</c>.</summary>
public static void ReportDedicatedMatchRoomClosed()
{
SettleWinnerIfNeededBeforeShutdown();
if (_dedicatedRoomClosedReported || DedicatedMatchId <= 0 || string.IsNullOrEmpty(DedicatedMatchSecret))
return;
_dedicatedRoomClosedReported = true;
if (DedicatedMatchId <= 0 || string.IsNullOrEmpty(DedicatedMatchSecret))
return;
using (var req = DedicatedMatschInternalApi.BuildRequest(DedicatedInternalApiBase, DedicatedMatchId, DedicatedMatchSecret, "{\"status\":-1}"))
{
var op = req.SendWebRequest();
@@ -158,11 +172,24 @@ public class GameManager : NetworkBehaviour
static bool _dedicatedRoomClosedReported;
static string Iso8601UtcNow() => DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'", System.Globalization.CultureInfo.InvariantCulture);
const float DisconnectForfeitGraceSeconds = 15f;
const int ConsecutiveSkipsToForfeit = 2;
bool _dedicatedReportedRedJoin;
bool _dedicatedReportedBlueJoin;
bool _dedicatedReportedGameOver;
bool _entriesCollected;
bool _collectFailed;
bool _winnerSettled;
bool _replayRecordingStarted;
bool _redConnected;
bool _blueConnected;
float _redForfeitDeadline = -1f;
float _blueForfeitDeadline = -1f;
readonly List<Team> _disconnectOrder = new List<Team>();
int _consecutiveSkipsRed;
int _consecutiveSkipsBlue;
public static Action<Team> OnTeamChanged;
public static bool isMoving{
get{
@@ -211,6 +238,7 @@ public class GameManager : NetworkBehaviour
public float ballMoveTime = 3f;
public float puckDragClampMax = 5f;
public float puckDragMinClamp = 1f;
public float puckSelectMaxDistance = 4f;
[Header("Field bounds")]
[Tooltip("Pitch width (X). Length (Y) is fieldSize × 2. Origin-centered; used for boundary-based camera zoom while dragging.")]
@@ -320,9 +348,28 @@ public class GameManager : NetworkBehaviour
/// <summary>Server-only: called from <see cref="NetPlayer.CmdSetTeam"/> after <c>myTeam</c> is set.</summary>
public void OnMatchPlayerTeamAssigned(Team team)
{
if (!isServer || DedicatedMatchId <= 0 || string.IsNullOrEmpty(DedicatedMatchSecret))
Logger.Log($"Dedicated join: team assigned {team} isServer={isServer} matchId={DedicatedMatchId} secretEmpty={string.IsNullOrEmpty(DedicatedMatchSecret)} redJoin={_dedicatedReportedRedJoin} blueJoin={_dedicatedReportedBlueJoin} collected={_entriesCollected}");
if (!isServer)
return;
if (team == Team.Red)
{
_redConnected = true;
_redForfeitDeadline = -1f;
}
else if (team == Team.Blue)
{
_blueConnected = true;
_blueForfeitDeadline = -1f;
}
if (DedicatedMatchId <= 0 || string.IsNullOrEmpty(DedicatedMatchSecret))
{
Logger.Log($"Dedicated join: skipping PATCH (matchId={DedicatedMatchId}, secretEmpty={string.IsNullOrEmpty(DedicatedMatchSecret)})");
return;
}
int n = FindObjectsByType<NetPlayer>(FindObjectsSortMode.None).Length;
string ts = Iso8601UtcNow();
@@ -330,20 +377,141 @@ public class GameManager : NetworkBehaviour
{
_dedicatedReportedRedJoin = true;
int status = n >= 2 ? 2 : 1;
bool collectAttempt = _dedicatedReportedRedJoin && _dedicatedReportedBlueJoin;
string json = "{\"red_joined_at\":\"" + ts + "\",\"status\":" + status + "}";
StartCoroutine(DedicatedMatchPatch(DedicatedInternalApiBase, DedicatedMatchId, DedicatedMatchSecret, json));
// StartCoroutine(DedicatedMatchInternalApi.Patch(DedicatedInternalApiBase, DedicatedMatchId, DedicatedMatchSecret, json));
Logger.Log($"Dedicated join: starting red PATCH collectAttempt={collectAttempt} players={n} body={json}");
StartCoroutine(CoDedicatedMatchJoinPatch(DedicatedInternalApiBase, DedicatedMatchId, DedicatedMatchSecret, json, collectAttempt));
}
else if (team == Team.Blue && !_dedicatedReportedBlueJoin)
{
_dedicatedReportedBlueJoin = true;
int status = n >= 2 ? 2 : 1;
bool collectAttempt = _dedicatedReportedRedJoin && _dedicatedReportedBlueJoin;
string json = "{\"blue_joined_at\":\"" + ts + "\",\"status\":" + status + "}";
StartCoroutine(DedicatedMatchPatch(DedicatedInternalApiBase, DedicatedMatchId, DedicatedMatchSecret, json));
Logger.Log($"Dedicated join: starting blue PATCH collectAttempt={collectAttempt} players={n} body={json}");
StartCoroutine(CoDedicatedMatchJoinPatch(DedicatedInternalApiBase, DedicatedMatchId, DedicatedMatchSecret, json, collectAttempt));
}
else
{
Logger.Log($"Dedicated join: no new PATCH for {team} (already reported or unknown team)");
}
}
IEnumerator DedicatedMatchPatch(string baseUrl, int matchId, string secret, string json){
/// <summary>Server-only: Mirror disconnect — track order and start forfeit grace after collect + kickoff.</summary>
public void OnDedicatedMatchPlayerDisconnected(Team team)
{
if (!isServer)
return;
if (team != Team.Red && team != Team.Blue)
return;
if (team == Team.Red)
{
if (!_redConnected)
return;
_redConnected = false;
_redForfeitDeadline = Time.realtimeSinceStartup + DisconnectForfeitGraceSeconds;
}
else
{
if (!_blueConnected)
return;
_blueConnected = false;
_blueForfeitDeadline = Time.realtimeSinceStartup + DisconnectForfeitGraceSeconds;
}
_disconnectOrder.Add(team);
Logger.Log($"Dedicated match: {team} disconnected (order count={_disconnectOrder.Count})");
if (!_entriesCollected || _winnerSettled || !gameStarted)
return;
// Both gone → last player who disconnected wins immediately.
if (!_redConnected && !_blueConnected && _disconnectOrder.Count > 0)
EndMatch(_disconnectOrder[_disconnectOrder.Count - 1]);
}
IEnumerator CoDedicatedMatchJoinPatch(string baseUrl, int matchId, string secret, string json, bool collectAttempt)
{
if (matchId <= 0 || string.IsNullOrEmpty(secret))
{
Logger.Log($"Dedicated join PATCH: abort before send (matchId={matchId}, secretEmpty={string.IsNullOrEmpty(secret)})");
yield break;
}
using (var req = DedicatedMatschInternalApi.BuildRequest(baseUrl, matchId, secret, json))
{
Logger.Log("Dedicated join PATCH BEFORE: url=" + req.url
+ " method=" + req.method
+ " collectAttempt=" + collectAttempt
+ " body=" + json
+ " base=" + baseUrl);
yield return req.SendWebRequest();
long code = req.responseCode;
string body = req.downloadHandler != null ? req.downloadHandler.text : "";
Logger.Log("Dedicated join PATCH AFTER: result=" + req.result
+ " http=" + code
+ " error=" + (req.error ?? "")
+ " collectAttempt=" + collectAttempt
+ " body=" + body);
if (code >= 200 && code < 300)
{
DedicatedMatschInternalApi.LogIfFailed(req);
try
{
var parsed = JsonUtility.FromJson<DedicatedJoinPatchResponse>(body);
bool collected = parsed != null && (parsed.entries_collected || parsed.already_collected);
Logger.Log("Dedicated join PATCH parsed: ok=" + (parsed != null && parsed.ok)
+ " id=" + (parsed != null ? parsed.id : 0)
+ " entries_collected=" + (parsed != null && parsed.entries_collected)
+ " already_collected=" + (parsed != null && parsed.already_collected)
+ " escrow_user_id=" + (parsed != null ? parsed.escrow_user_id : 0)
+ " willSetCollected=" + collected);
if (collected)
{
_entriesCollected = true;
Logger.Log("Dedicated match: entry fees collected into escrow"
+ (parsed.already_collected ? " (already_collected)" : ""));
}
else
{
Logger.Log("Dedicated join PATCH: 2xx but entries not collected yet (waiting for other join or unexpected body)");
}
}
catch (Exception e)
{
Logger.Log("Dedicated join PATCH: could not parse body: " + e.Message + " body=" + body);
}
yield break;
}
DedicatedMatschInternalApi.LogIfFailed(req);
// Collect runs on the join that sets both timestamps; non-2xx then is fatal.
if (collectAttempt)
{
_collectFailed = true;
Logger.Log("Dedicated match: collect failed (HTTP " + code + ") — aborting kickoff, no winner call");
AbortMatchCollectFailed();
}
else
{
Logger.Log("Dedicated join PATCH: non-2xx on first join (http=" + code + ") — not aborting collect yet");
}
}
}
void AbortMatchCollectFailed()
{
if (NetworkServer.active)
NetworkServer.DisconnectAll();
}
IEnumerator DedicatedMatchPatch(string baseUrl, int matchId, string secret, string json)
{
if (matchId <= 0 || string.IsNullOrEmpty(secret))
yield break;
@@ -369,9 +537,14 @@ public class GameManager : NetworkBehaviour
void ReportDedicatedMatchGameOver(Team winningTeam)
{
if (!isServer || DedicatedMatchId <= 0 || string.IsNullOrEmpty(DedicatedMatchSecret) || _dedicatedReportedGameOver)
if (!isServer || DedicatedMatchId <= 0 || string.IsNullOrEmpty(DedicatedMatchSecret) || _winnerSettled)
return;
_dedicatedReportedGameOver = true;
if (!_entriesCollected)
{
Logger.Log("Dedicated match: skipping winner PATCH (entries not collected)");
return;
}
_winnerSettled = true;
StartCoroutine(CoReportDedicatedMatchGameOver(winningTeam));
}
@@ -382,8 +555,95 @@ public class GameManager : NetworkBehaviour
yield return DedicatedMatchPatchWinner(DedicatedInternalApiBase, DedicatedMatchId, DedicatedMatchSecret, winner);
}
/// <summary>Score or forfeit end: latch game over, report winner if escrowed, notify clients.</summary>
void EndMatch(Team winningTeam)
{
if (!isServer || isGameEnded)
return;
isGameEnded = true;
ReplayRecorder.StopAndFlush();
ReportDedicatedMatchGameOver(winningTeam);
RpcGameOver(winningTeam);
gameOver(winningTeam);
}
void HandleDisconnectForfeitTimers()
{
if (!_entriesCollected || _winnerSettled || !gameStarted || isGameEnded)
return;
float now = Time.realtimeSinceStartup;
if (!_redConnected && _blueConnected && _redForfeitDeadline > 0f && now >= _redForfeitDeadline)
{
Logger.Log("Dedicated match: red forfeit grace expired — blue wins");
EndMatch(Team.Blue);
return;
}
if (!_blueConnected && _redConnected && _blueForfeitDeadline > 0f && now >= _blueForfeitDeadline)
{
Logger.Log("Dedicated match: blue forfeit grace expired — red wins");
EndMatch(Team.Red);
return;
}
if (!_redConnected && !_blueConnected
&& _redForfeitDeadline > 0f && now >= _redForfeitDeadline
&& _blueForfeitDeadline > 0f && now >= _blueForfeitDeadline
&& _disconnectOrder.Count > 0)
{
Team winner = _disconnectOrder[_disconnectOrder.Count - 1];
Logger.Log("Dedicated match: both forfeit graces expired — last disconnect wins: " + winner);
EndMatch(winner);
}
}
Team ResolveForfeitWinnerForShutdown()
{
if (_redConnected && !_blueConnected)
return Team.Red;
if (_blueConnected && !_redConnected)
return Team.Blue;
if (_disconnectOrder.Count > 0)
return _disconnectOrder[_disconnectOrder.Count - 1];
Logger.Log("Dedicated match: shutdown settle with no disconnect order — defaulting to red");
return Team.Red;
}
void SettleWinnerIfNeededBeforeShutdownInstance()
{
if (!_entriesCollected || _winnerSettled)
return;
if (DedicatedMatchId <= 0 || string.IsNullOrEmpty(DedicatedMatchSecret))
return;
Team winningTeam = ResolveForfeitWinnerForShutdown();
_winnerSettled = true;
isGameEnded = true;
Logger.Log("Dedicated match: blocking winner settle before shutdown → " + winningTeam);
using (var statusReq = DedicatedMatschInternalApi.BuildRequest(
DedicatedInternalApiBase, DedicatedMatchId, DedicatedMatchSecret, "{\"status\":3}"))
{
var op = statusReq.SendWebRequest();
while (!op.isDone)
System.Threading.Thread.Sleep(16);
DedicatedMatschInternalApi.LogIfFailed(statusReq);
}
string winner = winningTeam == Team.Red ? "red" : "blue";
string jsonBody = "{\"winner\":\"" + winner + "\"}";
using (var winReq = DedicatedMatschInternalApi.BuildWinnerRequest(
DedicatedInternalApiBase, DedicatedMatchId, DedicatedMatchSecret, jsonBody))
{
var op = winReq.SendWebRequest();
while (!op.isDone)
System.Threading.Thread.Sleep(16);
DedicatedMatschInternalApi.LogWinnerPatchResult(winReq);
}
}
void Start()
{
@@ -405,6 +665,38 @@ public class GameManager : NetworkBehaviour
pucks.Remove(puck);
}
int resetCollisionLockCount;
public bool AreResetCollisionsDisabled => resetCollisionLockCount > 0;
public void PushResetCollisionLock()
{
resetCollisionLockCount++;
if (resetCollisionLockCount == 1)
SetPlayCollisionsEnabled(false);
}
public void PopResetCollisionLock()
{
resetCollisionLockCount = Mathf.Max(0, resetCollisionLockCount - 1);
if (resetCollisionLockCount == 0)
SetPlayCollisionsEnabled(true);
}
void SetPlayCollisionsEnabled(bool enabled)
{
foreach (Puck puck in pucks)
{
if (puck != null)
puck.SetCollisionsEnabled(enabled);
}
if (ball != null)
{
Ball ballComp = ball.GetComponent<Ball>();
if (ballComp != null)
ballComp.SetCollisionsEnabled(enabled);
}
}
[SyncVar(hook = nameof(OnSelectedPuckChanged))]
@@ -451,7 +743,7 @@ public class GameManager : NetworkBehaviour
continue;
if(puck.team != SelectedTeam){continue;}
float dist = Vector2.Distance(position, puck.transform.position);
if (dist < minDistance)
if (dist < minDistance && dist <= puckSelectMaxDistance)
{
minDistance = dist;
closestPuck = puck;
@@ -486,6 +778,14 @@ public class GameManager : NetworkBehaviour
selectedPuck = null;
GameEvents.OnSelectedPuckChanged?.Invoke(null);
if (isServer)
{
bool wasWarned = GetConsecutiveSkips(SelectedTeam) > 0;
ClearConsecutiveSkips(SelectedTeam);
if (wasWarned)
RpcHideSkipForfeitWarning(SelectedTeam);
}
SwitchTeams();
}
@@ -518,6 +818,7 @@ public class GameManager : NetworkBehaviour
SelectedTeam = SelectedTeam == Team.Red ? Team.Blue : Team.Red;
OnTeamChanged?.Invoke(SelectedTeam);
MaybeWarnIfAtRiskOfSkipForfeit(SelectedTeam);
switchingTeams=false;
}
@@ -571,12 +872,35 @@ public class GameManager : NetworkBehaviour
#endif
if (players.Length == 2)
{
gameStarted = true;
GameCanvas.instance.HideWaitingForOpponentPanel();
Logger.Log("Game started On Server");
// Dedicated matches: wait until API collected entry fees into escrow.
if (DedicatedMatchId > 0)
{
if (_entriesCollected && !_collectFailed)
{
gameStarted = true;
GameCanvas.instance.HideWaitingForOpponentPanel();
Logger.Log("Game started On Server (entries collected)");
}
else if (Time.frameCount % 300 == 0)
{
Logger.Log("Dedicated kickoff waiting: players=2 collected=" + _entriesCollected
+ " collectFailed=" + _collectFailed
+ " redJoin=" + _dedicatedReportedRedJoin
+ " blueJoin=" + _dedicatedReportedBlueJoin
+ " matchId=" + DedicatedMatchId);
}
}
else
{
gameStarted = true;
GameCanvas.instance.HideWaitingForOpponentPanel();
Logger.Log("Game started On Server");
}
}
}
HandleDisconnectForfeitTimers();
// Host practice (tutorial) and dedicated matches both record once NetworkServer is ready.
// Retry until StartRecording succeeds — StartHost can lag one frame behind gameStarted.
if (gameStarted && !_replayRecordingStarted && NetworkServer.active)
@@ -636,7 +960,7 @@ public class GameManager : NetworkBehaviour
}
void HandleTurnTimer(){
if (turnTimerPaused)
if (turnTimerPaused || isGameEnded)
return;
if(turnTimerCounter < turnTimer){
@@ -644,11 +968,103 @@ public class GameManager : NetworkBehaviour
turnTimerCounter += Time.deltaTime;
}
}else{
SwitchTeams();
OnTurnTimerExpired();
turnTimerCounter = 0;
}
}
void OnTurnTimerExpired()
{
if (!isServer || !gameStarted || isGameEnded)
return;
if (GameTutorialManager.tutorialModeEnabled)
{
SwitchTeams();
return;
}
Team skippingTeam = SelectedTeam;
int skips = IncrementConsecutiveSkips(skippingTeam);
Logger.Log($"Match: {skippingTeam} skipped turn ({skips}/{ConsecutiveSkipsToForfeit})");
if (skips >= ConsecutiveSkipsToForfeit)
{
Team winner = GetOpposingTeam(skippingTeam);
Logger.Log($"Match: {skippingTeam} forfeited for skipping two consecutive turns — {winner} wins");
RpcHideSkipForfeitWarning(skippingTeam);
EndMatch(winner);
return;
}
SwitchTeams();
}
int GetConsecutiveSkips(Team team)
{
return team == Team.Red ? _consecutiveSkipsRed : _consecutiveSkipsBlue;
}
int IncrementConsecutiveSkips(Team team)
{
if (team == Team.Red)
return ++_consecutiveSkipsRed;
return ++_consecutiveSkipsBlue;
}
void ClearConsecutiveSkips(Team team)
{
if (team == Team.Red)
_consecutiveSkipsRed = 0;
else
_consecutiveSkipsBlue = 0;
}
void MaybeWarnIfAtRiskOfSkipForfeit(Team team)
{
if (!isServer || !gameStarted || isGameEnded)
return;
if (GameTutorialManager.tutorialModeEnabled)
return;
if (GetConsecutiveSkips(team) <= 0)
return;
RpcShowSkipForfeitWarning(team);
}
[ClientRpc]
void RpcShowSkipForfeitWarning(Team team)
{
ShowSkipForfeitWarning(team);
}
[ClientRpc]
void RpcHideSkipForfeitWarning(Team team)
{
HideSkipForfeitWarning(team);
}
/// <summary>Shown when this team starts a turn after already skipping once.</summary>
public void ShowSkipForfeitWarning(Team team)
{
if (NetPlayer.localPlayer != null && NetPlayer.localPlayer.myTeam != team)
return;
if (GameCanvas.instance == null)
return;
GameCanvas.instance.ShowKickWarning();
}
public void HideSkipForfeitWarning(Team team)
{
if (NetPlayer.localPlayer != null && NetPlayer.localPlayer.myTeam != team)
return;
if (GameCanvas.instance == null)
return;
GameCanvas.instance.HideKickWarning();
}
float flashPhase = 0f;
float lastUpdateTime = 0f;
float lastRemainingTime = 10f;
@@ -727,6 +1143,7 @@ public class GameManager : NetworkBehaviour
turnTimerCounter = 0;
SelectedTeam = kickoffTeam;
OnTeamChanged?.Invoke(SelectedTeam);
MaybeWarnIfAtRiskOfSkipForfeit(SelectedTeam);
}
public void OnGoal(Team team){
@@ -769,17 +1186,9 @@ public class GameManager : NetworkBehaviour
if(blueScore >= 3){
isGameEnded = true;
ReplayRecorder.StopAndFlush();
ReportDedicatedMatchGameOver(Team.Blue);
RpcGameOver(Team.Blue);
gameOver(Team.Blue);
EndMatch(Team.Blue);
}else if(redScore >= 3){
isGameEnded = true;
ReplayRecorder.StopAndFlush();
ReportDedicatedMatchGameOver(Team.Red);
RpcGameOver(Team.Red);
gameOver(Team.Red);
EndMatch(Team.Red);
}else{
StartCoroutine(CoroutineOnGoal(team));
}
@@ -847,6 +1256,7 @@ public class GameManager : NetworkBehaviour
}
bool _thirdPlayerLeaveTriggered;
bool _leaveRequested;
/// <summary>
/// Called when the local client spawns into a match. If three <see cref="NetPlayer"/> instances exist
@@ -859,8 +1269,108 @@ public class GameManager : NetworkBehaviour
StartCoroutine(CoLeaveIfThirdPlayer());
}
public void Leave(){
LevelLoadManager.instance.Leave();
/// <summary>
/// Voluntary leave: forfeit this player immediately so the opponent wins, then return to the menu.
/// </summary>
public void Leave()
{
if (_leaveRequested)
return;
_leaveRequested = true;
bool shouldForfeit = gameStarted && !isGameEnded;
if (shouldForfeit)
{
if (isServer)
ForfeitLeavingTeam(ResolveLocalLeavingTeam());
else
CmdForfeit();
StartCoroutine(CoLeaveAfterForfeitSent());
return;
}
DoLeaveToMenu();
}
IEnumerator CoLeaveAfterForfeitSent()
{
// Let the forfeit Command / game-over RPC flush before tearing down the connection.
yield return null;
yield return new WaitForSecondsRealtime(0.1f);
DoLeaveToMenu();
}
void DoLeaveToMenu()
{
if (LevelLoadManager.instance != null)
LevelLoadManager.instance.Leave();
}
[Command(requiresAuthority = false)]
void CmdForfeit(NetworkConnectionToClient sender = null)
{
Team leavingTeam;
if (!TryResolveLeavingTeam(sender, out leavingTeam))
{
Logger.Log("Forfeit: could not resolve leaving team from sender");
return;
}
ForfeitLeavingTeam(leavingTeam);
}
bool TryResolveLeavingTeam(NetworkConnectionToClient sender, out Team leavingTeam)
{
leavingTeam = Team.Red;
if (sender != null)
{
if (sender.identity != null)
{
var fromIdentity = sender.identity.GetComponent<NetPlayer>();
if (fromIdentity != null)
{
leavingTeam = fromIdentity.myTeam;
return true;
}
}
foreach (var player in FindObjectsByType<NetPlayer>(FindObjectsSortMode.None))
{
if (player != null && player.connectionToClient == sender)
{
leavingTeam = player.myTeam;
return true;
}
}
}
if (isServer && isClient)
{
leavingTeam = ResolveLocalLeavingTeam();
return leavingTeam == Team.Red || leavingTeam == Team.Blue;
}
return false;
}
Team ResolveLocalLeavingTeam()
{
if (NetPlayer.localPlayer != null)
return NetPlayer.localPlayer.myTeam;
return MyTeam;
}
void ForfeitLeavingTeam(Team leavingTeam)
{
if (!isServer || isGameEnded)
return;
if (!gameStarted)
return;
if (leavingTeam != Team.Red && leavingTeam != Team.Blue)
return;
Team winner = GetOpposingTeam(leavingTeam);
Logger.Log($"Match: {leavingTeam} forfeited by leave — {winner} wins");
EndMatch(winner);
}
IEnumerator CoLeaveIfThirdPlayer()
@@ -978,14 +1488,13 @@ public class GameManager : NetworkBehaviour
IEnumerator CoroutineReset(bool kickoff = false){
ReplayRecorder.RecordReset(kickoff);
float resetDuration = 0.5f;
ball.GetComponent<Ball>().Reset(resetDuration);
if(!kickoff){
foreach(Puck puck in pucks){
puck.Reset(null, resetDuration);
}
}
ball.GetComponent<Ball>().Reset(resetDuration);
yield return new WaitForSeconds(resetDuration);
freezeInput=false;
@@ -1083,6 +1592,16 @@ static class DedicatedMatschInternalApi
}
}
[Serializable]
class DedicatedJoinPatchResponse
{
public bool ok;
public int id;
public bool entries_collected;
public bool already_collected;
public int escrow_user_id;
}
[Serializable]
class DedicatedWinnerPatchEconomy
{