// SynchronisationAgrandissement.cs using UnityEngine; using Mirror; using System.Collections.Generic; /// /// Singleton NetworkBehaviour qui synchronise les agrandissements de salle entre /// le serveur dedicated et tous les clients. /// /// Pattern : /// 1. Un client clique [E] sur un panneau d'agrandissement /// -> AgrandissementSalle.DemanderAgrandir() trouve le singleton et appelle CmdAgrandir(id) /// 2. Cote serveur, CmdAgrandir valide (pas deja agrandi) et diffuse RpcLancerAgrandissement(id) /// 3. Chaque client (+ serveur) recoit le Rpc et execute l'animation locale d'Agrandir() /// en parallele. Tous voient la meme animation car les parametres sont identiques. /// 4. Un client qui rejoint en cours de partie recoit la SyncList des agrandissements deja /// faits et les rejoue instantanement (sans animation). /// /// A placer : en composant sur le GameObject du NetworkManager (ou tout objet avec /// NetworkIdentity configure pour exister des le demarrage reseau). /// /// L'identifiant d'un agrandissement est "salleId_direction", ex: "1_Nord". /// public class SynchronisationAgrandissement : NetworkBehaviour { public static SynchronisationAgrandissement Instance { get; private set; } // Liste synchronisee des agrandissements deja effectues // Format des entrees : "salleId_Direction" (ex: "1_Nord", "2_Est") private readonly SyncList _agrandissementsEffectues = new SyncList(); private void Awake() { if (Instance != null && Instance != this) { Debug.LogWarning("[SyncAgrandissement] Instance deja existante, destruction de ce duplicata"); Destroy(this); return; } Instance = this; } private void OnDestroy() { if (Instance == this) Instance = null; } // ============================================================ // CLIENT : appel par AgrandissementSalle.DemanderAgrandir() // ============================================================ /// /// Appele par un client qui veut agrandir. Envoie la demande au serveur. /// En mode solo (pas de Mirror actif), execute directement localement. /// public void DemanderAgrandissement(int salleId, string direction) { string id = ConstruireId(salleId, direction); // Mode solo (pas de serveur Mirror demarre) : execution directe locale if (!NetworkServer.active && !NetworkClient.active) { Debug.Log($"[SyncAgrandissement] Mode solo, execution locale directe : {id}"); ExecuterAgrandissementLocal(salleId, direction, false); return; } // Mode multi : Cmd vers serveur // Il faut que le client ait authority. On passe par le player local qui // porte ce role, ou directement si on est serveur nous-meme. if (NetworkServer.active) { // Host ou dedicated : on est serveur, on peut traiter directement TraiterDemandeServeur(salleId, direction); } else if (NetworkClient.active && NetworkClient.connection != null) { // Client pur : on doit passer par un PlayerBridge qui porte isLocalPlayer PlayerAgrandissementBridge bridge = TrouverBridgeLocal(); if (bridge != null) { bridge.CmdDemanderAgrandissement(salleId, direction); } else { Debug.LogError("[SyncAgrandissement] Aucun PlayerAgrandissementBridge trouve sur le joueur local !"); } } } // ============================================================ // SERVEUR : validation + broadcast // ============================================================ /// /// Appele cote serveur (soit par un host qui joue, soit par le bridge d'un client). /// Valide et diffuse aux clients. /// [Server] public void TraiterDemandeServeur(int salleId, string direction) { string id = ConstruireId(salleId, direction); // Validation if (_agrandissementsEffectues.Contains(id)) { Debug.LogWarning($"[SyncAgrandissement] Demande refusee, deja agrandi : {id}"); return; } // Enregistrer dans la liste synchronisee (pour rejoin) _agrandissementsEffectues.Add(id); Debug.Log($"[SyncAgrandissement] Agrandissement valide : {id} (total : {_agrandissementsEffectues.Count})"); // Diffuser a tous les clients (le host le recoit aussi) RpcLancerAgrandissement(salleId, direction); } // ============================================================ // CLIENTS : reception du Rpc // ============================================================ [ClientRpc] private void RpcLancerAgrandissement(int salleId, string direction) { Debug.Log($"[SyncAgrandissement] Rpc recu, execution agrandissement : salle {salleId} direction {direction}"); ExecuterAgrandissementLocal(salleId, direction, false); } // ============================================================ // REJOIN : un nouveau client rejoue l'etat actuel // ============================================================ public override void OnStartClient() { base.OnStartClient(); // Ne pas rejouer sur le host (il est deja a jour, c'est lui le serveur) if (NetworkServer.active) return; // Le SyncList n'est pas forcement deja rempli a ce moment-la. // On se branche sur ses callbacks et on rejoue les entrees existantes. _agrandissementsEffectues.Callback += OnAgrandissementsListChanged; // Rejouer immediatement les entrees deja presentes if (_agrandissementsEffectues.Count > 0) { Debug.Log($"[SyncAgrandissement] Client rejoint : rejeu de {_agrandissementsEffectues.Count} agrandissement(s) passe(s)"); foreach (string id in _agrandissementsEffectues) { RejouerInstantane(id); } } } public override void OnStopClient() { base.OnStopClient(); _agrandissementsEffectues.Callback -= OnAgrandissementsListChanged; } private void OnAgrandissementsListChanged(SyncList.Operation op, int index, string oldItem, string newItem) { // On ignore les changements qui arrivent par le flux normal (deja gere par Rpc) // Ce callback ne sert qu'au cas ou la SyncList est modifiee avant que le client recoive // le Rpc correspondant (race condition rare). Dans ce cas on rejoue aussi. // En pratique on s'en remet surtout au rejeu initial dans OnStartClient(). } private void RejouerInstantane(string id) { int salleId; string direction; if (!DecomposerId(id, out salleId, out direction)) { Debug.LogWarning($"[SyncAgrandissement] Id mal forme : {id}"); return; } ExecuterAgrandissementLocal(salleId, direction, true); } // ============================================================ // EXECUTION LOCALE (cote serveur ET cote client) // ============================================================ /// /// Execute l'agrandissement sur l'instance locale : trouve le bon mur et lance Agrandir(). /// Si instantane=true, skip les animations (pour rejoin). /// private void ExecuterAgrandissementLocal(int salleId, string direction, bool instantane) { AgrandissementSalle mur = TrouverMurAgrandissement(salleId, direction); if (mur == null) { Debug.LogWarning($"[SyncAgrandissement] Mur introuvable : salle {salleId} direction {direction}"); return; } if (mur.estAgrandi || mur.enAnimation) { Debug.Log($"[SyncAgrandissement] Agrandissement deja lance ou termine localement, skip"); return; } mur.AgrandirLocal(instantane); } private AgrandissementSalle TrouverMurAgrandissement(int salleId, string direction) { AgrandissementSalle.DirectionAgrandissement dirEnum; if (!System.Enum.TryParse(direction, out dirEnum)) return null; AgrandissementSalle[] tous = FindObjectsOfType(); foreach (var a in tous) { if (a.direction != dirEnum) continue; SalleDatacenter salle = FindObjectOfType(); if (salle == null) continue; if (salle.salleId == salleId) return a; } // Fallback : si aucun salleId ne matche (salle mono, pas encore enregistree // par le GestionnaireMultiSalles au moment de l'appel), on renvoie n'importe // quel mur dans la bonne direction. foreach (var a in tous) { if (a.direction == dirEnum) return a; } return null; } private PlayerAgrandissementBridge TrouverBridgeLocal() { PlayerAgrandissementBridge[] tous = FindObjectsOfType(); foreach (var b in tous) { if (b.isLocalPlayer) return b; } return null; } // ============================================================ // HELPERS ID // ============================================================ private static string ConstruireId(int salleId, string direction) => $"{salleId}_{direction}"; private static bool DecomposerId(string id, out int salleId, out string direction) { salleId = -1; direction = ""; if (string.IsNullOrEmpty(id)) return false; int sep = id.IndexOf('_'); if (sep <= 0 || sep >= id.Length - 1) return false; if (!int.TryParse(id.Substring(0, sep), out salleId)) return false; direction = id.Substring(sep + 1); return true; } } /// /// Bridge Mirror : composant a ajouter au prefab du joueur pour qu'il puisse /// envoyer une Cmd d'agrandissement vers le serveur (car les [Command] ne peuvent /// etre appelees que depuis un NetworkBehaviour porte par le joueur local). /// public class PlayerAgrandissementBridge : NetworkBehaviour { [Command] public void CmdDemanderAgrandissement(int salleId, string direction) { if (SynchronisationAgrandissement.Instance == null) { Debug.LogError("[PlayerAgrandissementBridge] Instance SynchronisationAgrandissement introuvable cote serveur !"); return; } SynchronisationAgrandissement.Instance.TraiterDemandeServeur(salleId, direction); } }