new API and rematch WIP

This commit is contained in:
2026-05-04 17:48:09 +05:30
parent fb0d386ba1
commit 82287d74fb
17 changed files with 5534 additions and 1258 deletions
+4
View File
@@ -37,6 +37,7 @@ public class GameCanvas : MonoBehaviour
public TMP_Text blueTurnTimer;
[FormerlySerializedAs("txtTurnTimer")] public TMP_Text redTurnTimer;
public TMP_Text redName,blueName;
public TMP_Text txtRcPrize;
public TMP_Text txtWhosTurn;
public Image blueTurnTimerRound;
[FormerlySerializedAs("turnTimerRound")] public Image redTurnTimerRound;
@@ -87,6 +88,9 @@ public class GameCanvas : MonoBehaviour
}catch{
Logger.Log("Failed to fetch red and blue names, isServer?");
}
if (txtRcPrize != null)
txtRcPrize.text = CoinHelper.FormatString(GameManager.MatchRcPrizeCoins) + " RC";
}
void OnEmoteTextPressed(string txtName){
+57 -2
View File
@@ -14,6 +14,8 @@ public class GameManager : NetworkBehaviour
public static Team MyTeam { get; set; }
public static MatchmadePlayer RedPlayer { get; set; }
public static MatchmadePlayer BluePlayer { get; set; }
/// <summary>Coin units from matchmaker <c>rc_prize</c>; used for in-game prize UI.</summary>
public static int MatchRcPrizeCoins { get; set; }
/// <summary>Set by dedicated server startup (e.g. CupidConnector parsing <c>-matchId</c>). PATCH is skipped if id ≤ 0 or secret is empty.</summary>
public static int DedicatedMatchId { get; private set; }
/// <summary>Sent as <c>X-Dedicated-Server-Secret</c>. From <c>-dedicatedSecret</c> arg or <c>DEDICATED_SERVER_SECRET</c> env.</summary>
@@ -257,7 +259,7 @@ public class GameManager : NetworkBehaviour
using (var req = DedicatedMatschInternalApi.BuildWinnerRequest(baseUrl, matchId, secret, jsonBody))
{
yield return req.SendWebRequest();
DedicatedMatschInternalApi.LogIfFailed(req);
DedicatedMatschInternalApi.LogWinnerPatchResult(req);
}
}
@@ -688,7 +690,7 @@ public class GameManager : NetworkBehaviour
yield return new WaitForSeconds(2f);
yield return CoroutineFetchBothPlayersL10();
StopClient();
// StopClient();
LevelLoadManager.instance.SetupGameOver(team,redScore,blueScore);
GameOverCanvas.instance.Show(team, MyTeam, redScore, blueScore);
@@ -810,6 +812,9 @@ public class GameManager : NetworkBehaviour
freezeInput=false;
}
}
/// <summary>Headless dedicated server → matchmaker internal PATCH. Runs only where started (server-side coroutines).</summary>
@@ -850,4 +855,54 @@ static class DedicatedMatschInternalApi
Logger.Log("More details : " + req.url + " " + req.method + " " + req.uploadHandler.data + " " + req.downloadHandler.text);
}
}
/// <summary>Handles <c>PATCH /internal/match/:id/winner</c>: 200 parses economy; 409 idempotent (already paid).</summary>
public static void LogWinnerPatchResult(UnityWebRequest req)
{
long code = req.responseCode;
string body = req.downloadHandler != null ? req.downloadHandler.text : "";
if (code == 200 && req.result == UnityWebRequest.Result.Success)
{
try
{
var parsed = JsonUtility.FromJson<DedicatedWinnerPatchResponse>(body);
if (parsed != null && parsed.ok && parsed.economy != null)
GameManager.MatchRcPrizeCoins = parsed.economy.rc_prize;
}
catch (Exception e)
{
Logger.Log("Dedicated winner PATCH: could not parse economy: " + e.Message + " body=" + body);
}
Logger.Log("Dedicated match winner PATCH success: " + code + " " + body);
return;
}
if (code == 409)
{
Logger.Log("Dedicated match winner PATCH: winner already recorded (409 idempotent), no second payout. " + body);
return;
}
Logger.Log("Dedicated match winner PATCH failed: " + code + " " + req.error + " " + body);
Logger.Log("More details : " + req.url + " " + req.method + " " + body);
}
}
[Serializable]
class DedicatedWinnerPatchEconomy
{
public int entry_fee_rc;
public int rc_prize;
public int participant_cc;
}
[Serializable]
class DedicatedWinnerPatchResponse
{
public bool ok;
public int id;
public int winner_id;
public DedicatedWinnerPatchEconomy economy;
}
+153 -6
View File
@@ -1,8 +1,10 @@
using UnityEngine;
using DG.Tweening;
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine.Networking;
using UnityEngine.UI;
public class GameOverCanvas : MonoBehaviour
{
@@ -28,7 +30,7 @@ public class GameOverCanvas : MonoBehaviour
public Sprite redIcon, blueIcon;
public RectTransform p1_winner, p2_winner;
[Header("Rematch")]
[Header("Game Over Data")]
public Button btnRematch;
public Button btnLeave;
public GameObject rematchWarningLoser;
@@ -39,6 +41,17 @@ public class GameOverCanvas : MonoBehaviour
public float returnToMenuCountdown = 15f;
[Header("Rematch panel")]
public CanvasGroup rematchPanel;
public RectTransform rematchPopup;
public TMP_Text txtMyRCBalance, txtOpponentRCBalance;
public TMP_Text txtSelectedBet;
public Slider betSlider;
public Button btnConfirmBet;
public Button btnCancelBet;
public static GameOverCanvas instance;
void Awake()
@@ -64,11 +77,38 @@ public class GameOverCanvas : MonoBehaviour
btnRematch.onClick.AddListener(OnBtnRematchClicked);
btnLeave.onClick.AddListener(OnBtnLeaveGameClicked);
betSlider.onValueChanged.AddListener(OnBetSliderValueChanged);
}
void OnBetSliderValueChanged(float value){
txtSelectedBet.text = CoinHelper.FormatString((int)value);
}
void OnBtnRematchClicked(){
// LevelLoadManager.LoadLevel("Game");
NetPlayer.localPlayer.RequestRematch();
SetRematchPendingState(true);
}
public void OnRematchRequested(){
if(winningTeam == myTeam){
//Only i can initiate the rematch, so here we go
ShowRematchPanel();
}
}
void SetRematchPendingState(bool val){
btnRematch.interactable = false;
btnLeave.interactable = !val;
if(val){
StopCoroutine(countdownCoroutine);
}else{
countdownCoroutine = StartCoroutine(CoroutineCountdown());
}
}
void OnBtnLeaveGameClicked(){
LevelLoadManager.LoadLevel("MainMenu");
}
@@ -115,10 +155,17 @@ public class GameOverCanvas : MonoBehaviour
canvasGroup.alpha = 0;
canvasGroup.blocksRaycasts = false;
canvasGroup.interactable = false;
rematchPanel.alpha=0;
rematchPanel.blocksRaycasts = false;
rematchPanel.interactable = false;
}
Team winningTeam;
Team myTeam;
public void Setup(Team winningTeam, Team myTeam, int redScore, int blueScore){
this.winningTeam = winningTeam;
this.myTeam = myTeam;
p1_winner.gameObject.SetActive(false);
p2_winner.gameObject.SetActive(false);
if(myTeam == Team.Red){
@@ -184,10 +231,12 @@ public class GameOverCanvas : MonoBehaviour
rcPrize = 8.2f;
}
btnRematch.interactable = winningTeam == myTeam;
rematchWarningLoser.SetActive(winningTeam != myTeam);
btnRematch.interactable = winningTeam != myTeam;
rematchWarningLoser.SetActive(winningTeam == myTeam);
StartCoroutine(CoroutineShow());
StartCoroutine(CoroutineFetchAndUpdateBalances());
}
IEnumerator CoroutineShow(){
@@ -232,9 +281,9 @@ public class GameOverCanvas : MonoBehaviour
StartCoroutine(CoroutineCountdown());
countdownCoroutine = StartCoroutine(CoroutineCountdown());
}
Coroutine countdownCoroutine;
IEnumerator CoroutineSetTextNumber(float number, TMP_Text text, float speed = 100f, string formatting = "N0", string prefix = "", string suffix = ""){
float t=0;
@@ -260,4 +309,102 @@ public class GameOverCanvas : MonoBehaviour
LevelLoadManager.LoadLevel("MainMenu");
}
public void ShowRematchPanel(){
SetRematchPendingState(true);
StartCoroutine(CoroutineShowRematchPanel());
betSlider.maxValue = (LevelLoadManager.myPlayer.rc < LevelLoadManager.opponentPlayer.rc) ? LevelLoadManager.myPlayer.rc : LevelLoadManager.opponentPlayer.rc;
}
IEnumerator CoroutineShowRematchPanel(){
rematchPopup.localScale = new Vector3(0, 0, 0);
rematchPanel.blocksRaycasts = true;
rematchPanel.interactable = true;
rematchPanel.DOFade(1, 0.3f).SetEase(Ease.InOutBack);
yield return new WaitForSeconds(0.1f);
rematchPopup.DOScale(1, 0.2f).SetEase(Ease.OutBack);
yield return new WaitForSeconds(0.2f);
}
public void OnRematchCancelled(){
SetRematchPendingState(false);
StartCoroutine(CoroutineHideRematchPanel());
}
IEnumerator CoroutineHideRematchPanel(){
rematchPopup.DOScale(0, 0.2f).SetEase(Ease.InBack);
yield return new WaitForSeconds(0.2f);
rematchPanel.DOFade(0, 0.3f).SetEase(Ease.InOutBack);
yield return new WaitForSeconds(0.3f);
}
IEnumerator CoroutineFetchAndUpdateBalances()
{
var my = LevelLoadManager.myPlayer;
var opp = LevelLoadManager.opponentPlayer;
string baseUrl = (GameManager.DedicatedInternalApiBase ?? "").TrimEnd('/');
if (string.IsNullOrEmpty(baseUrl) || my == null || opp == null)
{
ApplyRcBalancesToUi(null);
yield break;
}
if (my.UserId > 0)
yield return FetchPlayerRcAndApply(baseUrl, my);
if (opp.UserId > 0 && opp.UserId != my.UserId)
yield return FetchPlayerRcAndApply(baseUrl, opp);
ApplyRcBalancesToUi(null);
}
void ApplyRcBalancesToUi(string loadingPlaceholder)
{
var my = LevelLoadManager.myPlayer;
var opp = LevelLoadManager.opponentPlayer;
string myText = loadingPlaceholder ?? (my != null ? CoinHelper.FormatString((int)my.rc) : "0.0 RC");
string oppText = loadingPlaceholder ?? (opp != null ? CoinHelper.FormatString((int)opp.rc) : "0.0 RC");
if (txtMyRCBalance != null)
txtMyRCBalance.text = myText;
if (txtOpponentRCBalance != null)
txtOpponentRCBalance.text = oppText;
}
IEnumerator FetchPlayerRcAndApply(string baseUrl, MatchmadePlayer player)
{
string url = $"{baseUrl}/players/{player.UserId}/rc";
using (var req = UnityWebRequest.Get(url))
{
yield return req.SendWebRequest();
if (req.responseCode != 200)
{
Logger.Log($"Player RC fetch HTTP {(int)req.responseCode}: {req.error} {url}");
yield break;
}
string text = req.downloadHandler != null ? req.downloadHandler.text : "";
if (string.IsNullOrEmpty(text))
yield break;
var resp = JsonUtility.FromJson<PlayerRcApiResponse>(text);
if (resp != null && resp.ok)
player.rc = resp.rc;
else if (resp != null && !string.IsNullOrEmpty(resp.error))
Logger.Log($"Player RC fetch: {resp.error} ({url})");
}
}
[Serializable]
class PlayerRcApiResponse
{
public bool ok;
public int player_id;
public float rc;
public string error;
}
}
+63
View File
@@ -216,4 +216,67 @@ public class NetPlayer : NetworkBehaviour
void RpcOnPointerUp(){
}
/// <summary>
/// Request a rematch with the opponent.
/// </summary>
public void RequestRematch(){
if(isServer){
GameOverCanvas.instance.OnRematchRequested();
RpcRequestRematch();
}else{
CmdRequestRematch();
}
}
[Command]
void CmdRequestRematch(){
Logger.Log("Rematch requested");
if(isServerOnly){
//server
}else{
GameOverCanvas.instance.OnRematchRequested();
}
RpcRequestRematch();
}
[ClientRpc]
void RpcRequestRematch(){
GameOverCanvas.instance.OnRematchRequested();
}
public void AcceptRematch(float betRc){
if(isServer){
OnRematchAccepted(betRc);
RpcAcceptRematch(betRc);
}else{
CmdAcceptRematch(betRc);
}
}
[Command]
void CmdAcceptRematch(float betRc){
OnRematchAccepted(betRc);
RpcAcceptRematch(betRc);
}
[ClientRpc]
void RpcAcceptRematch(float betRc){
OnRematchAccepted(betRc);
}
void OnRematchAccepted(float betRc){
//Implement here
if(betRc == 0){
//Cancel signal
GameOverCanvas.instance.OnRematchCancelled();
return;
}
}
}
@@ -22,6 +22,7 @@ public class LevelLoadManager : MonoBehaviour
public TMP_Text p1_l10, p2_l10;
public Sprite redIcon, blueIcon;
public TMP_Text txtMiddle;
public TMP_Text txtRcPrize;
[Header("Game Over UI")]
public GameObject p1_winner;
public GameObject p2_winner;
@@ -84,7 +85,7 @@ public class LevelLoadManager : MonoBehaviour
}
Team myTeam = Team.Red;
public void SetupMatchMade(Team myTeam_, string opponentName, string myName, string myL10, string opponentL10){
public void SetupMatchMade(Team myTeam_, string opponentName, string myName, string myL10, string opponentL10, string rcPrize){
myTeam = myTeam_;
matchmadeUI.SetActive(true);
p1_winner.SetActive(false);
@@ -113,23 +114,24 @@ public class LevelLoadManager : MonoBehaviour
p2_name.text = myName;
p1_l10.text = myL10;
p2_l10.text = opponentL10;
txtRcPrize.text = rcPrize;
}
public static MatchmadePlayer myPlayer;
public static MatchmadePlayer opponentPlayer;
public void SetupMatchMade(Team myTeam_, MatchmadePlayer myPlayer_, MatchmadePlayer opponentPlayer_){
public void SetupMatchMade(Team myTeam_, MatchmadePlayer myPlayer_, MatchmadePlayer opponentPlayer_, string rcPrize){
myTeam = myTeam_;
myPlayer = myPlayer_;
opponentPlayer = opponentPlayer_;
string opponentName = opponentPlayer_.Name;
string myName = myPlayer_.Name;
string myL10 = $"L10 {myPlayer.l10_wins} -{myPlayer.l10_losses}";
string opponentL10 = $"L10 {opponentPlayer.l10_wins} -{opponentPlayer.l10_losses}";
SetupMatchMade(myTeam, myName, opponentName, myL10, opponentL10);
SetupMatchMade(myTeam, myName, opponentName, myL10, opponentL10, rcPrize);
}
bool gameOver = false;
public void SetupGameOver(Team winningTeam, int redScore, int blueScore){
@@ -9,15 +9,246 @@ public class MatchmadePlayer
public int l10_wins;
public int l10_losses;
public int UserId;
/// <summary>Rocket Credits from server (e.g. GET /players/:id/rc). Cached client-side.</summary>
public float rc;
}
/// <summary>Per-team block from matchmaker GET / when a side is in queue or match is ready.</summary>
[Serializable]
public class MatchmadeResponse
public class MatchmadeTeamPayload
{
public MatchmadePlayer[] Players;
public string GameName;
public int Port;
public long InitTime;
public bool instance_started;
public int match_id;
public int entry_fee;
public int rc_prize;
public string your_team;
}
/// <summary>Envelope from matchmaker fill / rematch success (and in-progress queue state).</summary>
[Serializable]
public class MatchmadeResponse
{
public bool ok;
public int entry_fee;
public int rc_prize;
public MatchmadeTeamPayload for_red;
public MatchmadeTeamPayload for_blue;
/// <summary>Optional root field: which nested block is for this client when both <see cref="for_red"/> and <see cref="for_blue"/> list the same players.</summary>
public string your_team;
public static bool TeamPayloadHasPort(MatchmadeTeamPayload p) =>
p != null && p.Port > 0;
/// <summary>True when both sides are present on the same dedicated instance (2v2-style duplicate rosters).</summary>
public static bool IsMatchReadyDuplicateRosters(MatchmadeResponse e)
{
if (e == null || !e.ok)
return false;
if (!TeamPayloadHasPort(e.for_red) || !TeamPayloadHasPort(e.for_blue))
return false;
if (e.for_red.Players == null || e.for_blue.Players == null)
return false;
if (e.for_red.match_id != e.for_blue.match_id || e.for_red.Port != e.for_blue.Port)
return false;
return e.for_red.Players.Length >= 2 && e.for_blue.Players.Length >= 2;
}
/// <summary>True when each side lists at least one player and together they form a full 1v1 (disjoint user sets, same instance).</summary>
public static bool IsMatchReadySplitRosters(MatchmadeResponse e)
{
if (e == null || !e.ok)
return false;
if (!TeamPayloadHasPort(e.for_red) || !TeamPayloadHasPort(e.for_blue))
return false;
if (e.for_red.Players == null || e.for_blue.Players == null)
return false;
if (e.for_red.match_id != e.for_blue.match_id || e.for_red.Port != e.for_blue.Port)
return false;
if (e.for_red.Players.Length < 1 || e.for_blue.Players.Length < 1)
return false;
int a = e.for_red.Players[0].UserId;
int b = e.for_blue.Players[0].UserId;
if (a == b)
return false;
if (e.for_red.Players.Length == 1 && e.for_blue.Players.Length == 1)
return true;
// Wider payloads: require no user id appearing on both sides
foreach (var pr in e.for_red.Players)
{
foreach (var pb in e.for_blue.Players)
{
if (pr.UserId == pb.UserId)
return false;
}
}
return true;
}
public static bool IsMatchReady(MatchmadeResponse e) =>
IsMatchReadyDuplicateRosters(e) || IsMatchReadySplitRosters(e);
static bool PayloadContainsUserId(MatchmadeTeamPayload p, int userId)
{
if (p?.Players == null || userId == 0)
return false;
for (int i = 0; i < p.Players.Length; i++)
{
if (p.Players[i].UserId == userId)
return true;
}
return false;
}
static int IndexOfUserId(MatchmadePlayer[] players, int userId)
{
if (players == null || userId == 0)
return -1;
for (int i = 0; i < players.Length; i++)
{
if (players[i].UserId == userId)
return i;
}
return -1;
}
/// <summary>Duplicate roster payloads: same users in the same order on red and blue blocks.</summary>
static bool SamePlayerOrder(MatchmadePlayer[] a, MatchmadePlayer[] b)
{
if (a == null || b == null || a.Length != b.Length)
return false;
for (int i = 0; i < a.Length; i++)
{
if (a[i].UserId != b[i].UserId)
return false;
}
return true;
}
/// <summary>
/// When <see cref="for_red"/> and <see cref="for_blue"/> list the same roster in the same order (1v1),
/// matchmaker convention: index 0 = red team, index 1 = blue team.
/// </summary>
static MatchmadeTeamPayload SelectDuplicateRosterBySlot(MatchmadeResponse env, int userId)
{
MatchmadePlayer[] r = env.for_red?.Players;
MatchmadePlayer[] b = env.for_blue?.Players;
if (r == null || b == null || r.Length != 2 || b.Length != 2)
return null;
if (!SamePlayerOrder(r, b))
return null;
int idx = IndexOfUserId(r, userId);
if (idx == 0)
return env.for_red;
if (idx == 1)
return env.for_blue;
return null;
}
/// <summary>Pick the nested block for this client (port, your_team, players for UI).</summary>
public static MatchmadeTeamPayload SelectTeamPayload(MatchmadeResponse env, int userId, Team? teamDisambiguation = null)
{
if (env == null)
return null;
if (!string.IsNullOrEmpty(env.your_team))
{
string y = env.your_team.Trim().ToLowerInvariant();
if (y == "red")
return env.for_red;
if (y == "blue")
return env.for_blue;
}
bool onRed = PayloadContainsUserId(env.for_red, userId);
bool onBlue = PayloadContainsUserId(env.for_blue, userId);
if (onRed && !onBlue)
return env.for_red;
if (onBlue && !onRed)
return env.for_blue;
if (onRed && onBlue)
{
MatchmadeTeamPayload bySlot = SelectDuplicateRosterBySlot(env, userId);
if (bySlot != null)
return bySlot;
if (teamDisambiguation.HasValue)
{
if (teamDisambiguation.Value == Team.Red)
return env.for_red;
if (teamDisambiguation.Value == Team.Blue)
return env.for_blue;
}
Debug.LogWarning("MatchmadeResponse: both sides list this user but roster is not 1v1 duplicate order; set root your_team on the server or pass team disambiguation.");
}
return env.for_red;
}
/// <summary>Resolve self and opponent from the selected branch (duplicate full roster) or from both sides (split 1v1 rosters).</summary>
public static bool TryResolveMyAndOpponent(MatchmadeResponse env, MatchmadeTeamPayload branch, int userId, out MatchmadePlayer myPlayer, out MatchmadePlayer opponentPlayer)
{
myPlayer = null;
opponentPlayer = null;
if (env == null || branch == null || branch.Players == null || userId == 0)
return false;
MatchmadeTeamPayload other = ReferenceEquals(branch, env.for_blue) ? env.for_red : env.for_blue;
if (branch.Players.Length >= 2)
{
for (int i = 0; i < branch.Players.Length; i++)
{
if (branch.Players[i].UserId == userId)
{
myPlayer = branch.Players[i];
break;
}
}
if (myPlayer == null)
return false;
for (int i = 0; i < branch.Players.Length; i++)
{
if (branch.Players[i].UserId != myPlayer.UserId)
{
opponentPlayer = branch.Players[i];
break;
}
}
return opponentPlayer != null;
}
for (int i = 0; i < branch.Players.Length; i++)
{
if (branch.Players[i].UserId == userId)
{
myPlayer = branch.Players[i];
break;
}
}
if (myPlayer == null && branch.Players.Length == 1 && branch.Players[0].UserId == userId)
myPlayer = branch.Players[0];
if (other?.Players == null)
return false;
for (int i = 0; i < other.Players.Length; i++)
{
if (other.Players[i].UserId != userId)
{
opponentPlayer = other.Players[i];
break;
}
}
if (opponentPlayer == null && other.Players.Length == 1)
opponentPlayer = other.Players[0];
return myPlayer != null && opponentPlayer != null && myPlayer.UserId != opponentPlayer.UserId;
}
}