using UnityEngine;
using UnityEngine.Networking;
using System;
using System.Collections;
using System.Collections.Generic;
using Mirror;
///
/// MasterServerClient.cs - v6.6
///
/// v6.6 : Robustesse du heartbeat pour dedicated server
/// - Heartbeat passe de 30s a 10s par defaut (reduit la fenetre
/// ou le master server peut purger un serveur inactif)
/// - Sur reponse 404 (serveur inconnu), re-enregistrement AUTOMATIQUE
/// immediat avec les memes parametres que le register initial (au lieu
/// de juste reset les variables et attendre le prochain heartbeat)
/// - Memoire des parametres de registration pour pouvoir re-register
/// sans intervention exterieure
/// - Apres 3 echecs consecutifs, abandon et log d'erreur
///
/// v6.5 : Version originale
///
/// Usage :
/// Placer sur le meme GameObject que le NetworkManager (ou un singleton).
/// Configurer masterServerUrl dans l'Inspector.
///
public class MasterServerClient : MonoBehaviour
{
// ══════════════════════════════════════════════════
// CONFIGURATION
// ══════════════════════════════════════════════════
[Header("Master Server")]
[Tooltip("URL du master server (ex: http://192.168.1.50:8080)")]
public string masterServerUrl = "http://localhost:8080";
[Tooltip("Token d'authentification (doit correspondre au serveur)")]
public string token = "dcsim-2026-secret";
[Header("Heartbeat")]
[Tooltip("Intervalle du heartbeat en secondes (10s recommande pour dedicated)")]
public float heartbeatInterval = 10f;
[Tooltip("Nombre max d'echecs consecutifs avant abandon (reset si succes)")]
public int maxEchecsConsecutifs = 3;
// ══════════════════════════════════════════════════
// ETAT
// ══════════════════════════════════════════════════
[HideInInspector] public string serverId = "";
[HideInInspector] public bool estEnregistre = false;
[HideInInspector] public List derniereListeServeurs = new List();
[HideInInspector] public bool requeteEnCours = false;
[HideInInspector] public string dernierStatut = "";
// Singleton
public static MasterServerClient Instance { get; private set; }
private Coroutine _heartbeatCoroutine;
private int _echecsConsecutifs = 0;
// v6.6 : memoire des parametres d'enregistrement pour re-register automatique
private string _dernierNom = "";
private int _dernierPort = 7777;
private int _dernierMaxJoueurs = 4;
private int _derniereLangue = 1;
private string _dernierMode = "Sandbox";
private string _derniereVersion = "v6.5g";
private bool _dernierMotDePasse = false;
// ══════════════════════════════════════════════════
// MODELES DE DONNEES
// ══════════════════════════════════════════════════
[System.Serializable]
public class ServerData
{
public string id;
public string nom;
public string ip;
public int port;
public int joueurs;
public int max_joueurs;
public int ping;
public int langue;
public string mode;
public string version;
public bool mot_de_passe;
}
[System.Serializable]
private class ServerListWrapper
{
public List servers;
}
[System.Serializable]
private class RegisterRequest
{
public string nom;
public int port;
public int max_joueurs;
public int langue;
public string mode;
public string version;
public bool mot_de_passe;
public string token;
}
[System.Serializable]
private class RegisterResponse
{
public string server_id;
public string status;
}
[System.Serializable]
private class HeartbeatRequest
{
public int joueurs;
public string token;
}
// ══════════════════════════════════════════════════
// LIFECYCLE
// ══════════════════════════════════════════════════
void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
Debug.Log($"[MasterServer] Awake → Instance initialisee, URL={masterServerUrl}");
}
void OnDestroy()
{
if (Instance == this)
Instance = null;
}
void OnApplicationQuit()
{
if (estEnregistre && !string.IsNullOrEmpty(serverId))
{
var request = new UnityWebRequest(
$"{masterServerUrl}/unregister/{serverId}", "DELETE");
request.timeout = 2;
request.SendWebRequest();
Debug.Log($"[MasterServer] Desenregistrement envoye ({serverId})");
}
}
// ══════════════════════════════════════════════════
// API PUBLIQUE
// ══════════════════════════════════════════════════
public void EnregistrerServeur(string nom, int port, int maxJoueurs,
int langue, string mode, string version, bool motDePasse)
{
// v6.6 : memorise les parametres pour pouvoir re-register en cas de 404
_dernierNom = nom;
_dernierPort = port;
_dernierMaxJoueurs = maxJoueurs;
_derniereLangue = langue;
_dernierMode = mode;
_derniereVersion = version;
_dernierMotDePasse = motDePasse;
StartCoroutine(Register(nom, port, maxJoueurs, langue, mode, version, motDePasse));
}
public void DesenregistrerServeur()
{
if (estEnregistre && !string.IsNullOrEmpty(serverId))
StartCoroutine(Unregister());
}
public void RecupererServeurs(System.Action, string> callback)
{
StartCoroutine(FetchServers(callback));
}
public void VerifierConnexion(System.Action callback)
{
StartCoroutine(CheckHealth(callback));
}
// ══════════════════════════════════════════════════
// COROUTINES HTTP
// ══════════════════════════════════════════════════
private IEnumerator Register(string nom, int port, int maxJoueurs,
int langue, string mode, string version, bool motDePasse)
{
requeteEnCours = true;
dernierStatut = "Enregistrement...";
var body = new RegisterRequest
{
nom = nom,
port = port,
max_joueurs = maxJoueurs,
langue = langue,
mode = mode,
version = version,
mot_de_passe = motDePasse,
token = token
};
string json = JsonUtility.ToJson(body);
byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(json);
var request = new UnityWebRequest($"{masterServerUrl}/register", "POST");
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
request.timeout = 10;
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
var response = JsonUtility.FromJson(request.downloadHandler.text);
serverId = response.server_id;
estEnregistre = true;
_echecsConsecutifs = 0;
dernierStatut = $"Enregistre ({serverId})";
Debug.Log($"[MasterServer] Serveur enregistre : {nom} → ID={serverId}");
// Demarrer le heartbeat (une seule coroutine active a la fois)
if (_heartbeatCoroutine != null)
StopCoroutine(_heartbeatCoroutine);
_heartbeatCoroutine = StartCoroutine(HeartbeatLoop());
}
else
{
dernierStatut = $"Erreur enregistrement : {request.error}";
Debug.LogWarning($"[MasterServer] Erreur register : {request.error} (code {request.responseCode})");
}
requeteEnCours = false;
request.Dispose();
}
private IEnumerator Unregister()
{
requeteEnCours = true;
dernierStatut = "Desenregistrement...";
if (_heartbeatCoroutine != null)
{
StopCoroutine(_heartbeatCoroutine);
_heartbeatCoroutine = null;
}
var request = UnityWebRequest.Delete($"{masterServerUrl}/unregister/{serverId}");
request.timeout = 5;
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
Debug.Log($"[MasterServer] Serveur desenregistre : {serverId}");
dernierStatut = "Serveur retire de la liste";
}
else
{
Debug.LogWarning($"[MasterServer] Erreur unregister : {request.error}");
dernierStatut = "Erreur desenregistrement (le cleanup nettoiera)";
}
estEnregistre = false;
serverId = "";
requeteEnCours = false;
request.Dispose();
}
private IEnumerator HeartbeatLoop()
{
while (estEnregistre && !string.IsNullOrEmpty(serverId))
{
yield return new WaitForSecondsRealtime(heartbeatInterval);
if (!estEnregistre || string.IsNullOrEmpty(serverId))
yield break;
int nbJoueurs = NetworkServer.active ? NetworkServer.connections.Count : 0;
var body = new HeartbeatRequest
{
joueurs = nbJoueurs,
token = token
};
string json = JsonUtility.ToJson(body);
byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(json);
var request = new UnityWebRequest(
$"{masterServerUrl}/heartbeat/{serverId}", "PUT");
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
request.timeout = 10;
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
// Reset du compteur d'echecs
_echecsConsecutifs = 0;
}
else
{
_echecsConsecutifs++;
long code = request.responseCode;
string err = request.error;
Debug.LogWarning($"[MasterServer] Heartbeat echoue ({_echecsConsecutifs}/{maxEchecsConsecutifs}) : HTTP {code} - {err}");
// v6.6 : si 404, le master server nous a purges → on re-register AUTOMATIQUEMENT
// et IMMEDIATEMENT, sans attendre le prochain cycle de heartbeat
if (code == 404)
{
Debug.Log("[MasterServer] Serveur inconnu (404), re-enregistrement automatique...");
request.Dispose();
// Reset de l'etat
estEnregistre = false;
string ancienId = serverId;
serverId = "";
// Relance Register() avec les parametres memorises
yield return StartCoroutine(Register(
_dernierNom, _dernierPort, _dernierMaxJoueurs,
_derniereLangue, _dernierMode, _derniereVersion, _dernierMotDePasse
));
if (estEnregistre)
{
Debug.Log($"[MasterServer] Re-enregistrement OK : {ancienId} → {serverId}");
// Le HeartbeatLoop continue, mais on vient de demarrer un nouveau via Register.
// On sort de cette boucle pour eviter d'en avoir deux en parallele.
yield break;
}
else
{
Debug.LogError($"[MasterServer] Re-enregistrement echoue, abandon du heartbeat");
yield break;
}
}
// Apres trop d'echecs consecutifs (pas 404 mais reseau/timeout/etc), abandon
if (_echecsConsecutifs >= maxEchecsConsecutifs)
{
Debug.LogError($"[MasterServer] Abandon du heartbeat apres {maxEchecsConsecutifs} echecs consecutifs");
estEnregistre = false;
yield break;
}
}
request.Dispose();
}
}
private IEnumerator FetchServers(System.Action, string> callback)
{
requeteEnCours = true;
dernierStatut = "Recherche de serveurs...";
float startTime = Time.realtimeSinceStartup;
var request = UnityWebRequest.Get($"{masterServerUrl}/servers");
request.timeout = 10;
yield return request.SendWebRequest();
int pingMs = Mathf.RoundToInt((Time.realtimeSinceStartup - startTime) * 1000f);
if (request.result == UnityWebRequest.Result.Success)
{
string responseText = request.downloadHandler.text;
string wrapped = "{\"servers\":" + responseText + "}";
var wrapper = JsonUtility.FromJson(wrapped);
derniereListeServeurs = wrapper.servers ?? new List();
foreach (var srv in derniereListeServeurs)
srv.ping = pingMs;
dernierStatut = $"{derniereListeServeurs.Count} serveur(s) trouve(s)";
Debug.Log($"[MasterServer] {derniereListeServeurs.Count} serveurs recuperes (ping={pingMs}ms)");
callback?.Invoke(derniereListeServeurs, null);
}
else
{
string erreur = $"Erreur : {request.error}";
dernierStatut = erreur;
Debug.LogWarning($"[MasterServer] Erreur fetch : {request.error}");
callback?.Invoke(new List(), erreur);
}
requeteEnCours = false;
request.Dispose();
}
private IEnumerator CheckHealth(System.Action callback)
{
var request = UnityWebRequest.Get($"{masterServerUrl}/health");
request.timeout = 5;
yield return request.SendWebRequest();
bool ok = request.result == UnityWebRequest.Result.Success;
callback?.Invoke(ok);
request.Dispose();
}
}