282 lines
10 KiB
C#

// SynchronisationAgrandissement.cs
using UnityEngine;
using Mirror;
using System.Collections.Generic;
/// <summary>
/// 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".
/// </summary>
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<string> _agrandissementsEffectues = new SyncList<string>();
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()
// ============================================================
/// <summary>
/// Appele par un client qui veut agrandir. Envoie la demande au serveur.
/// En mode solo (pas de Mirror actif), execute directement localement.
/// </summary>
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
// ============================================================
/// <summary>
/// Appele cote serveur (soit par un host qui joue, soit par le bridge d'un client).
/// Valide et diffuse aux clients.
/// </summary>
[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<string>.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)
// ============================================================
/// <summary>
/// Execute l'agrandissement sur l'instance locale : trouve le bon mur et lance Agrandir().
/// Si instantane=true, skip les animations (pour rejoin).
/// </summary>
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<AgrandissementSalle>();
foreach (var a in tous)
{
if (a.direction != dirEnum) continue;
SalleDatacenter salle = FindObjectOfType<SalleDatacenter>();
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<PlayerAgrandissementBridge>();
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;
}
}
/// <summary>
/// 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).
/// </summary>
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);
}
}