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(); } }