forfeiture, abandoning, entry_fee_collection at start
This commit is contained in:
@@ -46,7 +46,12 @@ public class AiBotController : NetworkBehaviour
|
||||
|
||||
void Awake()
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
enabled = false;
|
||||
return;
|
||||
#else
|
||||
EnsureTrajectoryLines();
|
||||
#endif
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
@@ -100,6 +105,10 @@ public class AiBotController : NetworkBehaviour
|
||||
return existingLine;
|
||||
}
|
||||
|
||||
Shader shader = Shader.Find("Sprites/Default");
|
||||
if (shader == null)
|
||||
return null;
|
||||
|
||||
GameObject go = new GameObject(name);
|
||||
go.transform.SetParent(transform, false);
|
||||
|
||||
@@ -109,7 +118,7 @@ public class AiBotController : NetworkBehaviour
|
||||
line.endWidth = trajectoryLineWidth;
|
||||
line.numCapVertices = 4;
|
||||
line.sortingOrder = trajectorySortingOrder;
|
||||
line.material = new Material(Shader.Find("Sprites/Default"));
|
||||
line.material = new Material(shader);
|
||||
line.startColor = color;
|
||||
line.endColor = color;
|
||||
line.positionCount = 0;
|
||||
@@ -1066,9 +1075,13 @@ public class AiBotController : NetworkBehaviour
|
||||
|
||||
public void Setup(Team team)
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
return;
|
||||
#else
|
||||
aiTeam = team;
|
||||
RefreshAiPucks();
|
||||
isEnabled = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void RefreshAiPucks()
|
||||
|
||||
+54
-3
@@ -15,22 +15,73 @@ public class Ball : NetworkBehaviour
|
||||
void Awake()
|
||||
{
|
||||
rb = GetComponent<Rigidbody2D>();
|
||||
col = GetComponent<Collider2D>();
|
||||
startPosition = transform.position;
|
||||
}
|
||||
|
||||
Collider2D col;
|
||||
Coroutine coroutineReset;
|
||||
bool holdingCollisionLock;
|
||||
|
||||
public void SetCollisionsEnabled(bool enabled)
|
||||
{
|
||||
if (col != null)
|
||||
col.enabled = enabled;
|
||||
if (rb != null && !enabled)
|
||||
{
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset(float duration){
|
||||
StartCoroutine(CoroutineReset(duration));
|
||||
if (coroutineReset != null)
|
||||
{
|
||||
StopCoroutine(coroutineReset);
|
||||
RestoreAfterReset();
|
||||
}
|
||||
coroutineReset = StartCoroutine(CoroutineReset(duration));
|
||||
}
|
||||
|
||||
IEnumerator CoroutineReset(float duration){
|
||||
if (GameManager.instance != null)
|
||||
{
|
||||
GameManager.instance.PushResetCollisionLock();
|
||||
holdingCollisionLock = true;
|
||||
}
|
||||
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
|
||||
float t=0;
|
||||
Vector3 pos1 = transform.position;
|
||||
Quaternion rot1 = transform.rotation;
|
||||
while (t < duration){
|
||||
t+=Time.deltaTime;
|
||||
transform.position = Vector3.Lerp(pos1, startPosition, t / duration);
|
||||
transform.rotation = Quaternion.Lerp(rot1, Quaternion.identity, t/duration);
|
||||
float u = Mathf.Clamp01(t / duration);
|
||||
transform.position = Vector3.Lerp(pos1, startPosition, u);
|
||||
transform.rotation = Quaternion.Lerp(rot1, Quaternion.identity, u);
|
||||
yield return null;
|
||||
}
|
||||
transform.position = startPosition;
|
||||
transform.rotation = Quaternion.identity;
|
||||
|
||||
RestoreAfterReset();
|
||||
}
|
||||
|
||||
void RestoreAfterReset()
|
||||
{
|
||||
if (rb != null)
|
||||
{
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
}
|
||||
if (holdingCollisionLock && GameManager.instance != null)
|
||||
{
|
||||
GameManager.instance.PopResetCollisionLock();
|
||||
holdingCollisionLock = false;
|
||||
}
|
||||
coroutineReset = null;
|
||||
}
|
||||
|
||||
public bool touchingB,touchingL,touchingR,touchingT = false;
|
||||
|
||||
@@ -58,6 +58,8 @@ public class GameCanvas : MonoBehaviour
|
||||
public float textEmoteDuration = 5f;
|
||||
public float emojiEmoteDuration = 5f;
|
||||
bool isShowingEmotes=> emotesPanel.gameObject.activeSelf;
|
||||
|
||||
public RectTransform kickWarning;
|
||||
|
||||
|
||||
|
||||
@@ -299,4 +301,17 @@ public class GameCanvas : MonoBehaviour
|
||||
timeoutForOpponentPanel.SetActive(false);
|
||||
}
|
||||
|
||||
|
||||
public void ShowKickWarning(){
|
||||
kickWarning.transform.localScale = new Vector3(0, 0, 0);
|
||||
kickWarning.transform.DOScale(1, 0.2f).SetEase(Ease.OutBack);
|
||||
kickWarning.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
public void HideKickWarning(){
|
||||
kickWarning.transform.DOScale(0, 0.2f).SetEase(Ease.InBack).OnComplete(() => {
|
||||
kickWarning.gameObject.SetActive(false);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+553
-34
@@ -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
|
||||
{
|
||||
|
||||
@@ -30,6 +30,15 @@ public class GameTutorialManager : MonoBehaviour
|
||||
|
||||
void Awake()
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
tutorialModeEnabled = false;
|
||||
tutorialModeTrigger = false;
|
||||
HideIntroImmediate(intro_shoot);
|
||||
HideIntroImmediate(intro_goal);
|
||||
enabled = false;
|
||||
return;
|
||||
#endif
|
||||
|
||||
tutorialModeEnabled = false;
|
||||
if (isTutorial)
|
||||
tutorialModeTrigger = true;
|
||||
|
||||
@@ -20,6 +20,8 @@ public class Goal : NetworkBehaviour
|
||||
void OnTriggerEnter2D(Collider2D collision)
|
||||
{
|
||||
if(!isServer){return;}
|
||||
if(GameManager.instance != null && GameManager.instance.AreResetCollisionsDisabled)
|
||||
return;
|
||||
|
||||
if(collision.gameObject.CompareTag("Player")){
|
||||
if(GameManager.instance == null || !GameManager.instance.CanProcessGoal){
|
||||
@@ -42,6 +44,8 @@ public class Goal : NetworkBehaviour
|
||||
if(!isServer){return;}
|
||||
|
||||
if(collision.gameObject.CompareTag("Puck")){
|
||||
if(GameManager.instance != null && GameManager.instance.AreResetCollisionsDisabled)
|
||||
return;
|
||||
Debug.Log("Puck exit goal", gameObject);
|
||||
Puck puck = collision.gameObject.GetComponent<Puck>();
|
||||
Debug.Log(puck.name);
|
||||
|
||||
@@ -53,7 +53,13 @@ public class LoginManager : MonoBehaviour
|
||||
[SerializeField] private TMP_Text serverWarningMessageTxt;
|
||||
[SerializeField] private Button btnWarningClose;
|
||||
|
||||
const float KeepaliveIntervalSeconds = 10f;
|
||||
|
||||
bool _authInProgress;
|
||||
Coroutine _keepaliveRoutine;
|
||||
bool _appPaused;
|
||||
bool _appFocused = true;
|
||||
bool _handlingKeepaliveUnauthorized;
|
||||
|
||||
public static string AuthToken { get; private set; }
|
||||
|
||||
@@ -117,6 +123,10 @@ public class LoginManager : MonoBehaviour
|
||||
|
||||
if (string.IsNullOrEmpty(AuthToken))
|
||||
ClearLocalUserProfile();
|
||||
#if !UNITY_EDITOR
|
||||
else
|
||||
StartKeepalive();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Start()
|
||||
@@ -139,13 +149,16 @@ public class LoginManager : MonoBehaviour
|
||||
|
||||
if (!mayContinue)
|
||||
{
|
||||
// Blocked by update or error panel — keep login UI disabled.
|
||||
// Blocked by update or error panel — keep login UI disabled, but hold the session.
|
||||
if (!string.IsNullOrEmpty(AuthToken))
|
||||
StartKeepalive();
|
||||
SetBusy(false);
|
||||
yield break;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// Dev: log in with clone-aware credentials instead of resuming a saved session.
|
||||
StopKeepalive();
|
||||
AuthToken = "";
|
||||
PlayerPrefs.DeleteKey(PlayerPrefsAuthKey);
|
||||
PlayerPrefs.Save();
|
||||
@@ -161,7 +174,10 @@ public class LoginManager : MonoBehaviour
|
||||
SetBusy(false);
|
||||
#else
|
||||
if (!string.IsNullOrEmpty(AuthToken))
|
||||
{
|
||||
StartKeepalive();
|
||||
yield return StartCoroutine(ResumeSessionCoroutine());
|
||||
}
|
||||
else
|
||||
SetBusy(false);
|
||||
#endif
|
||||
@@ -217,6 +233,8 @@ public class LoginManager : MonoBehaviour
|
||||
/// <summary>Removes auth token from memory and disk and clears cached profile (login screen recovery).</summary>
|
||||
static void InvalidateStoredSession()
|
||||
{
|
||||
if (instance != null)
|
||||
instance.StopKeepalive();
|
||||
AuthToken = "";
|
||||
ClearLocalUserProfile();
|
||||
ClearMatchYourTeam();
|
||||
@@ -224,6 +242,18 @@ public class LoginManager : MonoBehaviour
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
/// <summary>Clears session state and all PlayerPrefs (full logout).</summary>
|
||||
public static void Logout()
|
||||
{
|
||||
if (instance != null)
|
||||
instance.StopKeepalive();
|
||||
AuthToken = "";
|
||||
ClearLocalUserProfile();
|
||||
ClearMatchYourTeam();
|
||||
PlayerPrefs.DeleteAll();
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
static void ProceedToMainMenu()
|
||||
{
|
||||
if (LevelLoadManager.instance != null)
|
||||
@@ -242,6 +272,7 @@ public class LoginManager : MonoBehaviour
|
||||
void OnLogin()
|
||||
{
|
||||
if (_authInProgress) return;
|
||||
if (_keepaliveRoutine != null && !string.IsNullOrEmpty(AuthToken)) return;
|
||||
|
||||
string username = usernameInputLogin.text;
|
||||
string password = passwordInputLogin.text;
|
||||
@@ -270,6 +301,7 @@ public class LoginManager : MonoBehaviour
|
||||
void OnRegister()
|
||||
{
|
||||
if (_authInProgress) return;
|
||||
if (_keepaliveRoutine != null && !string.IsNullOrEmpty(AuthToken)) return;
|
||||
|
||||
string username = usernameInputRegister.text;
|
||||
string email = emailInputRegister != null ? emailInputRegister.text : "";
|
||||
@@ -374,6 +406,7 @@ public class LoginManager : MonoBehaviour
|
||||
AuthToken = resp.token;
|
||||
PlayerPrefs.SetString(PlayerPrefsAuthKey, resp.token);
|
||||
PlayerPrefs.Save();
|
||||
StartKeepalive();
|
||||
|
||||
bool profileOk = false;
|
||||
string profileErr = null;
|
||||
@@ -799,4 +832,127 @@ public class LoginManager : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
bool IsAppInForeground => !_appPaused && _appFocused;
|
||||
|
||||
void OnApplicationPause(bool paused)
|
||||
{
|
||||
_appPaused = paused;
|
||||
}
|
||||
|
||||
void OnApplicationFocus(bool hasFocus)
|
||||
{
|
||||
_appFocused = hasFocus;
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (instance == this)
|
||||
StopKeepalive();
|
||||
}
|
||||
|
||||
void StartKeepalive()
|
||||
{
|
||||
if (string.IsNullOrEmpty(AuthToken)) return;
|
||||
if (_keepaliveRoutine != null) return;
|
||||
_handlingKeepaliveUnauthorized = false;
|
||||
_keepaliveRoutine = StartCoroutine(KeepaliveLoopCoroutine());
|
||||
}
|
||||
|
||||
void StopKeepalive()
|
||||
{
|
||||
if (_keepaliveRoutine == null) return;
|
||||
StopCoroutine(_keepaliveRoutine);
|
||||
_keepaliveRoutine = null;
|
||||
}
|
||||
|
||||
IEnumerator KeepaliveLoopCoroutine()
|
||||
{
|
||||
while (!string.IsNullOrEmpty(AuthToken))
|
||||
{
|
||||
while (!IsAppInForeground && !string.IsNullOrEmpty(AuthToken))
|
||||
yield return null;
|
||||
|
||||
if (string.IsNullOrEmpty(AuthToken))
|
||||
break;
|
||||
|
||||
yield return KeepalivePingCoroutine();
|
||||
if (string.IsNullOrEmpty(AuthToken))
|
||||
break;
|
||||
|
||||
float elapsed = 0f;
|
||||
while (elapsed < KeepaliveIntervalSeconds && !string.IsNullOrEmpty(AuthToken))
|
||||
{
|
||||
if (!IsAppInForeground)
|
||||
{
|
||||
while (!IsAppInForeground && !string.IsNullOrEmpty(AuthToken))
|
||||
yield return null;
|
||||
break;
|
||||
}
|
||||
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
_keepaliveRoutine = null;
|
||||
}
|
||||
|
||||
IEnumerator KeepalivePingCoroutine()
|
||||
{
|
||||
if (string.IsNullOrEmpty(AuthToken))
|
||||
yield break;
|
||||
|
||||
string url = AuthBaseUrl + "/auth/keepalive";
|
||||
using (var request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST))
|
||||
{
|
||||
request.uploadHandler = new UploadHandlerRaw(Array.Empty<byte>());
|
||||
request.downloadHandler = new DownloadHandlerBuffer();
|
||||
request.SetRequestHeader("Content-Type", "application/json");
|
||||
request.SetRequestHeader("Authorization", "Bearer " + AuthToken);
|
||||
|
||||
yield return request.SendWebRequest();
|
||||
|
||||
if (request.responseCode == 401)
|
||||
{
|
||||
string text = request.downloadHandler != null ? request.downloadHandler.text : "";
|
||||
string err = "Session expired. Please sign in again.";
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
{
|
||||
AuthApiResponse resp = JsonUtility.FromJson<AuthApiResponse>(text);
|
||||
if (resp != null && !string.IsNullOrEmpty(resp.error))
|
||||
err = resp.error;
|
||||
}
|
||||
|
||||
HandleKeepaliveUnauthorized(err);
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (request.result != UnityWebRequest.Result.Success &&
|
||||
request.result != UnityWebRequest.Result.ProtocolError)
|
||||
{
|
||||
Logger.Log("auth/keepalive network error: " + (string.IsNullOrEmpty(request.error) ? "Network error" : request.error));
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (request.responseCode < 200 || request.responseCode >= 300)
|
||||
Logger.Log("auth/keepalive failed: HTTP " + request.responseCode);
|
||||
}
|
||||
}
|
||||
|
||||
void HandleKeepaliveUnauthorized(string message)
|
||||
{
|
||||
if (_handlingKeepaliveUnauthorized) return;
|
||||
_handlingKeepaliveUnauthorized = true;
|
||||
|
||||
InvalidateStoredSession();
|
||||
SetBusy(false);
|
||||
|
||||
if (error_txt != null)
|
||||
error_txt.text = string.IsNullOrEmpty(message) ? "Session expired. Please sign in again." : message;
|
||||
|
||||
Scene active = SceneManager.GetActiveScene();
|
||||
if (active.buildIndex != 0 && active.name != "Intro")
|
||||
SceneManager.LoadScene(0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -47,6 +47,14 @@ public class MainMenuManager : MonoBehaviour
|
||||
public TMP_Text txtWithdrawStatus;
|
||||
private bool isCoinBuyInProgress;
|
||||
|
||||
[Header("Settings")]
|
||||
public ProfileInfoFetcher profileInfoFetcher;
|
||||
public TMP_Text txtUsername;
|
||||
public TMP_Text txtEmail;
|
||||
public TMP_Text txtAge,txtMatches,txtWinRatio;
|
||||
public TMP_Text txtId;
|
||||
public Button btnLogout;
|
||||
|
||||
[Header("Audio")]
|
||||
public ToggleButton toggleSfx;
|
||||
public ToggleButton toggleMusic;
|
||||
@@ -77,6 +85,7 @@ public class MainMenuManager : MonoBehaviour
|
||||
if (success)
|
||||
{
|
||||
OnUserUpdated(user);
|
||||
RefreshSettingsProfile();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -87,6 +96,8 @@ public class MainMenuManager : MonoBehaviour
|
||||
}
|
||||
});
|
||||
|
||||
RefreshSettingsProfile();
|
||||
|
||||
btnBuyUsd5.onClick.AddListener(() => OnBuy(RcPack.Usd5));
|
||||
btnBuyUsd20.onClick.AddListener(() => OnBuy(RcPack.Usd20));
|
||||
btnBuyUsd50.onClick.AddListener(() => OnBuy(RcPack.Usd50));
|
||||
@@ -112,6 +123,9 @@ public class MainMenuManager : MonoBehaviour
|
||||
toggleSfx.SetIsOn(AudioManager.instance.IsSFXOn());
|
||||
toggleMusic.SetIsOn(AudioManager.instance.IsMusicOn());
|
||||
|
||||
if (btnLogout != null)
|
||||
btnLogout.onClick.AddListener(OnLogout);
|
||||
|
||||
if (!LevelLoadManager.BlocksMainMenuSettingsBootstrap)
|
||||
StartCoroutine(CoBootstrapSettingsWithoutLoader());
|
||||
}
|
||||
@@ -122,6 +136,14 @@ public class MainMenuManager : MonoBehaviour
|
||||
toggleMusic.onToggleValueChanged.RemoveListener(OnToggleMusic);
|
||||
if (btnTutorial != null && btnTutorial != btnPlay)
|
||||
btnTutorial.onClick.RemoveListener(OnPlayTutorial);
|
||||
if (btnLogout != null)
|
||||
btnLogout.onClick.RemoveListener(OnLogout);
|
||||
}
|
||||
|
||||
void OnLogout()
|
||||
{
|
||||
LoginManager.Logout();
|
||||
SceneManager.LoadScene(0);
|
||||
}
|
||||
|
||||
void OnToggleSfx(bool isOn)
|
||||
@@ -334,8 +356,18 @@ public class MainMenuManager : MonoBehaviour
|
||||
}
|
||||
|
||||
|
||||
void SetStatsPanelVisible(bool visible)
|
||||
{
|
||||
if (statsPanel == null)
|
||||
return;
|
||||
statsPanel.SetActive(visible);
|
||||
Transform topPanel = statsPanel.transform.parent;
|
||||
if (topPanel != null)
|
||||
topPanel.gameObject.SetActive(visible);
|
||||
}
|
||||
|
||||
public void ShowMainMenuScreen(){
|
||||
statsPanel.SetActive(true);
|
||||
SetStatsPanelVisible(true);
|
||||
mainMenuScreen.SetActive(true);
|
||||
gameModesScreen.SetActive(false);
|
||||
dummyBuyScreen.SetActive(false);
|
||||
@@ -344,7 +376,7 @@ public class MainMenuManager : MonoBehaviour
|
||||
}
|
||||
|
||||
public void ShowGameModesScreen(){
|
||||
statsPanel.SetActive(true);
|
||||
SetStatsPanelVisible(true);
|
||||
mainMenuScreen.SetActive(false);
|
||||
gameModesScreen.SetActive(true);
|
||||
dummyBuyScreen.SetActive(false);
|
||||
@@ -353,7 +385,7 @@ public class MainMenuManager : MonoBehaviour
|
||||
}
|
||||
|
||||
public void ShowDummyBuyScreen(){
|
||||
statsPanel.SetActive(false);
|
||||
SetStatsPanelVisible(false);
|
||||
mainMenuScreen.SetActive(false);
|
||||
gameModesScreen.SetActive(false);
|
||||
dummyBuyScreen.SetActive(true);
|
||||
@@ -362,7 +394,7 @@ public class MainMenuManager : MonoBehaviour
|
||||
}
|
||||
|
||||
public void ShowWithdrawalScreen(){
|
||||
statsPanel.SetActive(false);
|
||||
SetStatsPanelVisible(false);
|
||||
mainMenuScreen.SetActive(false);
|
||||
gameModesScreen.SetActive(false);
|
||||
dummyBuyScreen.SetActive(false);
|
||||
@@ -372,12 +404,49 @@ public class MainMenuManager : MonoBehaviour
|
||||
}
|
||||
|
||||
public void ShowSettingsScreen(){
|
||||
statsPanel.SetActive(false);
|
||||
SetStatsPanelVisible(false);
|
||||
mainMenuScreen.SetActive(false);
|
||||
gameModesScreen.SetActive(false);
|
||||
dummyBuyScreen.SetActive(false);
|
||||
withdrawalScreen.SetActive(false);
|
||||
settingsScreen.SetActive(true);
|
||||
RefreshSettingsProfile();
|
||||
}
|
||||
|
||||
void RefreshSettingsProfile()
|
||||
{
|
||||
if (profileInfoFetcher == null)
|
||||
return;
|
||||
|
||||
UserData user = LoginManager.CurrentUser;
|
||||
if (user == null)
|
||||
return;
|
||||
|
||||
profileInfoFetcher.GetPlayerProfile(user.id, (success, error, profile) =>
|
||||
{
|
||||
if (!success || profile == null)
|
||||
{
|
||||
Debug.LogWarning("Settings profile fetch failed: " + (error ?? "unknown"));
|
||||
return;
|
||||
}
|
||||
ApplySettingsProfile(profile);
|
||||
});
|
||||
}
|
||||
|
||||
void ApplySettingsProfile(PlayerProfileApiResponse profile)
|
||||
{
|
||||
if (txtUsername != null)
|
||||
txtUsername.text = profile.username ?? "";
|
||||
if (txtEmail != null)
|
||||
txtEmail.text = profile.email ?? "";
|
||||
if (txtId != null)
|
||||
txtId.text = profile.player_id.ToString();
|
||||
if (txtAge != null)
|
||||
txtAge.text = profile.days_since_registration.ToString();
|
||||
if (txtMatches != null)
|
||||
txtMatches.text = profile.matches_played.ToString();
|
||||
if (txtWinRatio != null)
|
||||
txtWinRatio.text = (profile.win_ratio * 100f).ToString("0.#") + "%";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -89,6 +89,13 @@ public class NetManager : NetworkManager
|
||||
|
||||
public override void OnServerDisconnect(NetworkConnectionToClient conn)
|
||||
{
|
||||
if (conn != null && conn.identity != null)
|
||||
{
|
||||
var netPlayer = conn.identity.GetComponent<NetPlayer>();
|
||||
if (netPlayer != null && GameManager.instance != null)
|
||||
GameManager.instance.OnDedicatedMatchPlayerDisconnected(netPlayer.myTeam);
|
||||
}
|
||||
|
||||
if (conn != null)
|
||||
Logger.Log($"Mirror server: client disconnected (connId={conn.connectionId})");
|
||||
base.OnServerDisconnect(conn);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
public class ProfileInfoFetcher : MonoBehaviour
|
||||
{
|
||||
/// <summary>GET /players/:playerId/profile. Callback (success, errorOrNull, profileOrNull).</summary>
|
||||
public void GetPlayerProfile(int playerId, Action<bool, string, PlayerProfileApiResponse> onComplete)
|
||||
{
|
||||
StartCoroutine(FetchPlayerProfileCoroutine(playerId, onComplete));
|
||||
}
|
||||
|
||||
IEnumerator FetchPlayerProfileCoroutine(int playerId, Action<bool, string, PlayerProfileApiResponse> onComplete)
|
||||
{
|
||||
string url = LoginManager.AuthBaseUrl + "/players/" + playerId + "/profile";
|
||||
using (var request = UnityWebRequest.Get(url))
|
||||
{
|
||||
request.downloadHandler = new DownloadHandlerBuffer();
|
||||
if (!string.IsNullOrEmpty(LoginManager.AuthToken))
|
||||
request.SetRequestHeader("Authorization", "Bearer " + LoginManager.AuthToken);
|
||||
|
||||
yield return request.SendWebRequest();
|
||||
|
||||
string text = request.downloadHandler != null ? request.downloadHandler.text : "";
|
||||
|
||||
if (request.result != UnityWebRequest.Result.Success &&
|
||||
request.result != UnityWebRequest.Result.ProtocolError)
|
||||
{
|
||||
onComplete?.Invoke(false, string.IsNullOrEmpty(request.error) ? "Network error" : request.error, null);
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (request.responseCode < 200 || request.responseCode >= 300)
|
||||
{
|
||||
onComplete?.Invoke(false, string.IsNullOrEmpty(text) ? "Request failed (" + request.responseCode + ")" : text, null);
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
onComplete?.Invoke(false, "Empty response from server", null);
|
||||
yield break;
|
||||
}
|
||||
|
||||
PlayerProfileApiResponse resp = JsonUtility.FromJson<PlayerProfileApiResponse>(text);
|
||||
if (resp != null && resp.ok)
|
||||
onComplete?.Invoke(true, null, resp);
|
||||
else
|
||||
{
|
||||
string err = resp != null && !string.IsNullOrEmpty(resp.error) ? resp.error : "Request failed";
|
||||
onComplete?.Invoke(false, err, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e6e403cd7bcf0149bad56dbad78c302
|
||||
+55
-4
@@ -6,6 +6,7 @@ using UnityEngine.EventSystems;
|
||||
public class Puck : NetworkBehaviour
|
||||
{
|
||||
[HideInInspector]public Rigidbody2D rb;
|
||||
Collider2D col;
|
||||
[SyncVar]
|
||||
public bool isSelected;
|
||||
public LineRenderer lineRenderer;
|
||||
@@ -30,6 +31,7 @@ public class Puck : NetworkBehaviour
|
||||
UpdateTeam();
|
||||
markerStartScale = selectedMarker.localScale;
|
||||
rb=GetComponent<Rigidbody2D>();
|
||||
col = GetComponent<Collider2D>();
|
||||
startPosition = transform.position;
|
||||
}
|
||||
|
||||
@@ -52,7 +54,9 @@ public class Puck : NetworkBehaviour
|
||||
GameEvents.OnSelectedPuckChanged -= OnSelectedPuckChanged;
|
||||
GameManager.OnTeamChanged -= OnTeamChanged;
|
||||
|
||||
GameManager.instance.DisposePuck(this);
|
||||
RestoreAfterReset();
|
||||
if (GameManager.instance != null)
|
||||
GameManager.instance.DisposePuck(this);
|
||||
}
|
||||
public Vector3 lineEnd;
|
||||
void Update()
|
||||
@@ -146,10 +150,28 @@ public class Puck : NetworkBehaviour
|
||||
AudioManager.instance.PlayPuckHit(vol);
|
||||
}
|
||||
|
||||
Coroutine coroutineReset;
|
||||
bool holdingCollisionLock;
|
||||
public void Reset(Vector3? position = null, float duration = 0.5f)
|
||||
{
|
||||
Vector3 pos = position ?? startPosition;
|
||||
StartCoroutine(CoroutineReset(pos, duration));
|
||||
if (coroutineReset != null)
|
||||
{
|
||||
StopCoroutine(coroutineReset);
|
||||
RestoreAfterReset();
|
||||
}
|
||||
coroutineReset = StartCoroutine(CoroutineReset(pos, duration));
|
||||
}
|
||||
|
||||
public void SetCollisionsEnabled(bool enabled)
|
||||
{
|
||||
if (col != null)
|
||||
col.enabled = enabled;
|
||||
if (rb != null && !enabled)
|
||||
{
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
}
|
||||
}
|
||||
Coroutine coroutineScheduleReset;
|
||||
public void ScheduleReset(Vector3 position, float duration = 0.5f){
|
||||
@@ -188,15 +210,44 @@ public class Puck : NetworkBehaviour
|
||||
}
|
||||
|
||||
IEnumerator CoroutineReset(Vector3 pos, float duration){
|
||||
if (GameManager.instance != null)
|
||||
{
|
||||
GameManager.instance.PushResetCollisionLock();
|
||||
holdingCollisionLock = true;
|
||||
}
|
||||
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
|
||||
float t=0;
|
||||
Vector3 pos1 = transform.position;
|
||||
Quaternion rot1 = transform.rotation;
|
||||
while(t<duration){
|
||||
t+=Time.deltaTime;
|
||||
transform.position = Vector3.Lerp(pos1, pos, t/duration);
|
||||
transform.rotation = Quaternion.Lerp(rot1, Quaternion.identity, t/duration);
|
||||
float u = Mathf.Clamp01(t/duration);
|
||||
transform.position = Vector3.Lerp(pos1, pos, u);
|
||||
transform.rotation = Quaternion.Lerp(rot1, Quaternion.identity, u);
|
||||
yield return null;
|
||||
}
|
||||
transform.position = pos;
|
||||
transform.rotation = Quaternion.identity;
|
||||
|
||||
RestoreAfterReset();
|
||||
}
|
||||
|
||||
void RestoreAfterReset()
|
||||
{
|
||||
if (rb != null)
|
||||
{
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
}
|
||||
if (holdingCollisionLock && GameManager.instance != null)
|
||||
{
|
||||
GameManager.instance.PopResetCollisionLock();
|
||||
holdingCollisionLock = false;
|
||||
}
|
||||
coroutineReset = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ public class ServerAutoClose : MonoBehaviour
|
||||
Logger.Log("All players left, exiting");
|
||||
else
|
||||
Logger.Log("No players joined, exiting");
|
||||
// ReportDedicatedMatchRoomClosed settles /winner first when escrow is open.
|
||||
GameManager.ReportDedicatedMatchRoomClosed();
|
||||
Application.Quit();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
[Serializable]
|
||||
public class PlayerProfileApiResponse
|
||||
{
|
||||
public bool ok;
|
||||
public string error;
|
||||
public int player_id;
|
||||
public string username;
|
||||
public string email;
|
||||
public int days_since_registration;
|
||||
public int matches_played;
|
||||
public float win_ratio;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: acfc3a6cd73c2234081774fcdc39082a
|
||||
Reference in New Issue
Block a user