pack multi v7.1

This commit is contained in:
Stephane MAURO 2026-04-18 11:31:50 +00:00
parent ae8b5d3d2d
commit 95300d4815
27 changed files with 3030 additions and 10 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@ -1 +0,0 @@
{"root":[{"assemblyName":"Assembly-CSharp","nameSpace":"Mirror","className":"GeneratedNetworkCode","methodName":"InitReadWriters","loadTypes":1,"isUnityClass":false},{"assemblyName":"Cinemachine","nameSpace":"Cinemachine","className":"CinemachineStoryboard","methodName":"InitializeModule","loadTypes":0,"isUnityClass":false},{"assemblyName":"Cinemachine","nameSpace":"Cinemachine","className":"CinemachineCore","methodName":"InitializeModule","loadTypes":0,"isUnityClass":false},{"assemblyName":"Cinemachine","nameSpace":"Cinemachine","className":"UpdateTracker","methodName":"InitializeModule","loadTypes":0,"isUnityClass":false},{"assemblyName":"Cinemachine","nameSpace":"Cinemachine","className":"CinemachineImpulseManager","methodName":"InitializeModule","loadTypes":0,"isUnityClass":false},{"assemblyName":"Cinemachine","nameSpace":"Cinemachine.PostFX","className":"CinemachineVolumeSettings","methodName":"InitializeModule","loadTypes":0,"isUnityClass":false},{"assemblyName":"glTFast","nameSpace":"","className":"$BurstDirectCallInitializer","methodName":"Initialize","loadTypes":2,"isUnityClass":false},{"assemblyName":"Mirror.Authenticators","nameSpace":"Mirror.Authenticators","className":"UniqueNameAuthenticator","methodName":"ResetStatics","loadTypes":0,"isUnityClass":false},{"assemblyName":"Mirror.Authenticators","nameSpace":"Mirror","className":"GeneratedNetworkCode","methodName":"InitReadWriters","loadTypes":1,"isUnityClass":false},{"assemblyName":"Mirror.Components","nameSpace":"Mirror","className":"GeneratedNetworkCode","methodName":"InitReadWriters","loadTypes":1,"isUnityClass":false},{"assemblyName":"Mirror","nameSpace":"Mirror","className":"NetworkClient","methodName":"Shutdown","loadTypes":1,"isUnityClass":false},{"assemblyName":"Mirror","nameSpace":"Mirror","className":"NetworkDiagnostics","methodName":"ResetStatics","loadTypes":0,"isUnityClass":false},{"assemblyName":"Mirror","nameSpace":"Mirror","className":"NetworkIdentity","methodName":"ResetStatics","loadTypes":1,"isUnityClass":false},{"assemblyName":"Mirror","nameSpace":"Mirror","className":"NetworkLoop","methodName":"ResetStatics","loadTypes":1,"isUnityClass":false},{"assemblyName":"Mirror","nameSpace":"Mirror","className":"NetworkLoop","methodName":"RuntimeInitializeOnLoad","loadTypes":1,"isUnityClass":false},{"assemblyName":"Mirror","nameSpace":"Mirror","className":"NetworkManager","methodName":"ResetStatics","loadTypes":1,"isUnityClass":false},{"assemblyName":"Mirror","nameSpace":"Mirror","className":"NetworkServer","methodName":"Shutdown","loadTypes":1,"isUnityClass":false},{"assemblyName":"Mirror","nameSpace":"Mirror","className":"NetworkTime","methodName":"ResetStatics","loadTypes":0,"isUnityClass":false},{"assemblyName":"Mirror","nameSpace":"Mirror","className":"ThreadLog","methodName":"Initialize","loadTypes":1,"isUnityClass":false},{"assemblyName":"Mirror","nameSpace":"Mirror","className":"GeneratedNetworkCode","methodName":"InitReadWriters","loadTypes":1,"isUnityClass":false},{"assemblyName":"Mirror.Examples","nameSpace":"Mirror.Examples.MultipleMatch","className":"CanvasController","methodName":"ResetStatics","loadTypes":1,"isUnityClass":false},{"assemblyName":"Mirror.Examples","nameSpace":"Mirror","className":"GeneratedNetworkCode","methodName":"InitReadWriters","loadTypes":1,"isUnityClass":false},{"assemblyName":"Unity.Burst","nameSpace":"","className":"$BurstDirectCallInitializer","methodName":"Initialize","loadTypes":2,"isUnityClass":true},{"assemblyName":"Unity.Collections","nameSpace":"","className":"$BurstDirectCallInitializer","methodName":"Initialize","loadTypes":2,"isUnityClass":true},{"assemblyName":"Unity.InputSystem","nameSpace":"UnityEngine.InputSystem","className":"InputSystem","methodName":"RunInitializeInPlayer","loadTypes":4,"isUnityClass":true},{"assemblyName":"Unity.InputSystem","nameSpace":"UnityEngine.InputSystem","className":"InputSystem","methodName":"RunInitialUpdate","loadTypes":1,"isUnityClass":true},{"assemblyName":"Unity.InputSystem","nameSpace":"UnityEngine.InputSystem.UI","className":"InputSystemUIInputModule","methodName":"ResetDefaultActions","loadTypes":4,"isUnityClass":true},{"assemblyName":"Unity.RenderPipelines.Core.Runtime","nameSpace":"UnityEngine.Experimental.Rendering","className":"XRSystem","methodName":"XRSystemInit","loadTypes":3,"isUnityClass":true},{"assemblyName":"Unity.RenderPipelines.Core.Runtime","nameSpace":"UnityEngine.Rendering","className":"DebugUpdater","methodName":"RuntimeInit","loadTypes":0,"isUnityClass":true},{"assemblyName":"Unity.VisualScripting.Core","nameSpace":"Unity.VisualScripting","className":"RuntimeVSUsageUtility","methodName":"RuntimeInitializeOnLoadBeforeSceneLoad","loadTypes":1,"isUnityClass":true}]}

View File

@ -1 +0,0 @@
{"names":["UnityEngine.dll","UnityEngine.AIModule.dll","UnityEngine.AccessibilityModule.dll","UnityEngine.AndroidJNIModule.dll","UnityEngine.AnimationModule.dll","UnityEngine.AssetBundleModule.dll","UnityEngine.AudioModule.dll","UnityEngine.ClothModule.dll","UnityEngine.ClusterInputModule.dll","UnityEngine.ClusterRendererModule.dll","UnityEngine.ContentLoadModule.dll","UnityEngine.CoreModule.dll","UnityEngine.CrashReportingModule.dll","UnityEngine.DSPGraphModule.dll","UnityEngine.DirectorModule.dll","UnityEngine.GIModule.dll","UnityEngine.GameCenterModule.dll","UnityEngine.GridModule.dll","UnityEngine.HotReloadModule.dll","UnityEngine.IMGUIModule.dll","UnityEngine.ImageConversionModule.dll","UnityEngine.InputModule.dll","UnityEngine.InputLegacyModule.dll","UnityEngine.JSONSerializeModule.dll","UnityEngine.LocalizationModule.dll","UnityEngine.ParticleSystemModule.dll","UnityEngine.PerformanceReportingModule.dll","UnityEngine.PhysicsModule.dll","UnityEngine.Physics2DModule.dll","UnityEngine.ProfilerModule.dll","UnityEngine.PropertiesModule.dll","UnityEngine.RuntimeInitializeOnLoadManagerInitializerModule.dll","UnityEngine.ScreenCaptureModule.dll","UnityEngine.SharedInternalsModule.dll","UnityEngine.SpriteMaskModule.dll","UnityEngine.SpriteShapeModule.dll","UnityEngine.StreamingModule.dll","UnityEngine.SubstanceModule.dll","UnityEngine.SubsystemsModule.dll","UnityEngine.TLSModule.dll","UnityEngine.TerrainModule.dll","UnityEngine.TerrainPhysicsModule.dll","UnityEngine.TextCoreFontEngineModule.dll","UnityEngine.TextCoreTextEngineModule.dll","UnityEngine.TextRenderingModule.dll","UnityEngine.TilemapModule.dll","UnityEngine.UIModule.dll","UnityEngine.UIElementsModule.dll","UnityEngine.UmbraModule.dll","UnityEngine.UnityAnalyticsModule.dll","UnityEngine.UnityAnalyticsCommonModule.dll","UnityEngine.UnityConnectModule.dll","UnityEngine.UnityCurlModule.dll","UnityEngine.UnityTestProtocolModule.dll","UnityEngine.UnityWebRequestModule.dll","UnityEngine.UnityWebRequestAssetBundleModule.dll","UnityEngine.UnityWebRequestAudioModule.dll","UnityEngine.UnityWebRequestTextureModule.dll","UnityEngine.UnityWebRequestWWWModule.dll","UnityEngine.VFXModule.dll","UnityEngine.VRModule.dll","UnityEngine.VehiclesModule.dll","UnityEngine.VideoModule.dll","UnityEngine.VirtualTexturingModule.dll","UnityEngine.WindModule.dll","UnityEngine.XRModule.dll","Assembly-CSharp.dll","glTFast.dots.dll","Unity.RenderPipelines.Core.Runtime.dll","Unity.ProBuilder.Stl.dll","Unity.ProBuilder.Csg.dll","Unity.RenderPipelines.Universal.Config.Runtime.dll","Unity.VisualScripting.Flow.dll","Telepathy.dll","glTFast.Newtonsoft.dll","Unity.RenderPipelines.Core.ShaderLibrary.dll","Mirror.Examples.dll","Unity.Collections.dll","Unity.ProBuilder.Poly2Tri.dll","glTFast.Documentation.Examples.dll","Unity.TextMeshPro.dll","kcp2k.dll","Unity.RenderPipelines.ShaderGraph.ShaderGraphLibrary.dll","Unity.RenderPipelines.Universal.Runtime.dll","Unity.Burst.dll","Unity.ProBuilder.dll","Mirror.dll","Unity.VisualScripting.Core.dll","Mirror.Transports.dll","UnityEngine.UI.dll","Mirror.Components.dll","glTFast.Export.dll","Unity.RenderPipelines.Universal.2D.Internal.dll","Unity.RenderPipeline.Universal.ShaderLibrary.dll","Unity.Timeline.dll","Unity.InputSystem.dll","SimpleWebTransport.dll","Unity.InputSystem.ForUI.dll","Mirror.Authenticators.dll","glTFast.dll","Unity.ProBuilder.KdTree.dll","Unity.Mathematics.dll","Unity.RenderPipelines.Universal.Shaders.dll","Unity.VisualScripting.State.dll","Cinemachine.dll","Unity.Collections.LowLevel.ILSupport.dll","Mirror.BouncyCastle.Cryptography.dll","Unity.VisualScripting.Antlr3.Runtime.dll","Unity.Burst.Unsafe.dll","Newtonsoft.Json.dll"],"types":[2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16]}

View File

@ -1,2 +0,0 @@
MAURO Stéphane
DatacenterSim_Alpha

View File

@ -1,6 +0,0 @@
gfx-enable-gfx-jobs=1
gfx-enable-native-gfx-jobs=1
wait-for-native-debugger=0
hdr-display-enabled=0
gc-max-time-slice=3
build-guid=e778e91ac99847908610029fa6e532a7

View File

@ -0,0 +1,179 @@
# DatacenterSim v7.1 — Pack multi complet
Pack de 3 patches qui corrigent les 3 bugs identifiés en multijoueur après reco :
1. **Portage d'équipements** invisible pour les autres joueurs
2. **Câbles alim + RJ45** pendants après reco
3. **Serveurs éteints** après reco
---
## Ce qu'il y a dans le ZIP
### Scripts_Modifies/ (remplacer dans Assets/Scripts/)
- **`PlayerInteraction.cs`** — 3 changements
- Revert de mon patch authority (plus de AssignClientAuthority/RemoveClientAuthority sur équipements)
- `CmdUpdateTransportPosition` bouge maintenant l'objet côté serveur
- Helper `SetNetworkTransformActif()` + désactivation NT local pendant portage + réactivation à la pose
- `CmdAppuyerBoutonPower` appelle `EtatEquipementNetworkSync.ServerSyncEtat` pour le rejoin
- **`CablageReseau.cs`** — 4 ajouts
- `CmdCreerCableRJ45` / `CmdCreerCableAlim` enregistrent dans le gestionnaire
- `CmdSupprimerCableRJ45` / `CmdSupprimerCableAlim` désenregistrent
- **`BaieNetworkSetup.cs`** — 1 ajout
- `OnStartClient` notifie le gestionnaire que la baie est prête
- **`EtatEquipement.cs`** — 1 ajout
- `VerifierAlimentation` appelle `EtatEquipementNetworkSync.ServerSyncEtat` quand l'état change
### Scripts_Nouveaux/ (AJOUTER dans Assets/Scripts/)
- **`GestionnaireCablesReseau.cs`** — NetworkBehaviour singleton qui tient 2 SyncLists (câbles alim + RJ45) et rejoue les câbles au rejoin des clients
- **`EtatEquipementNetworkSync.cs`** — Composant à ajouter sur les prefabs d'équipements rackmount pour synchroniser l'état bouton/sous-tension au rejoin
---
## Configuration Unity requise
### 1. Prefabs d'équipements : NetworkTransform en `Server To Client`
**IMPORTANT** : tu avais configuré `Client To Server` pour mon dernier patch raté. **Remets-les en `Server To Client`** :
Pour chaque prefab dans `NetworkManager.spawnPrefabs` (sauf Chariot et prefab joueur) :
- Ouvrir le prefab
- Le `NetworkTransformReliable` / `NetworkTransformUnreliable` :
- **Sync Direction : `Server To Client`** ⚠️
- Sync Position : ✅
- Sync Rotation : ✅
- Sync Scale : ❌
- Compress Rotation : ✅
- Only Sync On Change : ✅
- Interpolate Position : ✅
- Interpolate Rotation : ✅
- Send Interval Multiplier : 3 ou 5
Prefabs concernés : `Serveur_1U`, `Switch_24P`, `Parefeu`, `PDU_APC_*`, `Baie_Procedurale`, `Toron_Base`.
**Ne PAS toucher** : le chariot (Client To Server) et le prefab joueur (Client To Server).
### 2. Ajouter `GestionnaireCablesReseau` à la scène `Datacenter_01`
1. Dans la scène, `Hierarchy` → clic droit → `Create Empty`
2. Renommer : `GestionnaireCables_Holder`
3. `Add Component``NetworkIdentity` (laisser toutes les checkboxes décochées)
4. `Add Component``GestionnaireCablesReseau`
5. **Sauver la scène** (Ctrl+S)
### 3. Ajouter `EtatEquipementNetworkSync` sur les prefabs d'équipements rackmount
Pour chaque prefab qui a un `BoutonPower` + `EtatEquipement` :
- Ouvrir le prefab
- `Add Component``EtatEquipementNetworkSync`
- Sauver
Prefabs concernés : `Serveur_1U`, `Switch_24P`, `Parefeu`.
(Pas besoin sur PDU, Toron, Baie — ces prefabs n'ont pas de bouton power.)
---
## Tests à faire
### Test 1 — Non-régression solo
Lancer en éditeur Play. Tout doit fonctionner comme avant (pas de multi).
### Test 2 — Portage visible en multi
1. Host + 1 client connecté
2. Client prend un serveur 1U depuis un carton → **tu dois voir le serveur suivre le client**
3. Client le pose dans une baie → **tu dois voir le snap**
4. Tu prends le même serveur pour le déracker → **le client doit te voir le porter**
### Test 3 — Câbles alim au rejoin
1. Client pose baie + 2 serveurs + 2 PDUs + 2 câbles alim
2. Client allume les serveurs (bouton Power)
3. Client déco + reco
4. **Attendu** : câbles alim toujours connectés (non pendants), serveurs toujours allumés (ventilos tournent, LED verte)
### Test 4 — Câbles RJ45 au rejoin
1. Client pose baie + 2 serveurs + 1 switch + torons + câbles RJ45
2. Client déco + reco
3. **Attendu** : câbles RJ45 toujours branchés correctement
### Test 5 — 2e joueur qui rejoint en cours
1. Client 1 a tout câblé et allumé
2. Client 2 se connecte
3. **Attendu** : client 2 voit TOUT l'état (câbles + serveurs allumés)
---
## Logs à surveiller
### Côté serveur, quand un câble est créé :
```
[Serveur] Câble alim créé : PDU 10 prise 11 → equip 7 port 0
[GestionnaireCablesReseau] Cable alim enregistre (total : 1)
```
### Côté client qui rejoint :
```
[GestionnaireCablesReseau] Client rejoint : 2 cables alim, 3 cables RJ45 a recreer
[BaieNetworkSetup] Baie regeneree cote client : Baie_Procedurale(Clone)
[GestionnaireCablesReseau] Baie prete : Baie_Procedurale(Clone), retry cables en attente
[GestionnaireCablesReseau] Cable alim recree : PDU 10 prise #11 -> PSU0
[GestionnaireCablesReseau] Tous les cables ont ete recrees
```
### Si timeout (normalement jamais) :
```
[GestionnaireCablesReseau] Timeout : 1 cables alim + 0 cables RJ45 jamais recrees
```
→ ça veut dire qu'une baie ou un équipement ne s'est pas spawné correctement côté client.
---
## Architecture du mécanisme "baie prête"
Le problème classique au rejoin : on tente de recréer un câble entre PDU X et serveur Y, mais X ou Y n'existent pas encore localement parce que leur parent (la baie) n'a pas fini de se régénérer.
**Solution** :
1. Au `OnStartClient` du gestionnaire, on met tous les câbles à recréer en "liste d'attente"
2. On tente une première passe après 0.5s (laisse le temps aux premiers spawn)
3. Chaque `BaieNetworkSetup.OnStartClient` se termine par `NotifierBaiePrete()` qui déclenche un **retry** sur tous les câbles en attente
4. En fallback, un retry périodique toutes les 0.5s pendant 15s max
5. Timeout à 15s avec un warning dans les logs
C'est robuste aux ordres de spawn aléatoires et évite tout callback hell.
---
## Remarques techniques
### Pourquoi retirer l'authority pour le portage ?
`NetworkTransform Client-to-Server` exige l'authority. Le patch précédent transférait l'authority au ramassage, mais ça créait plein de race conditions (logs `EntityStateMessage without authority`, objets figés/dédoublés...).
La nouvelle approche : le **serveur est TOUJOURS autoritaire**. Le client porteur envoie des `CmdUpdateTransportPosition` qui bougent l'objet côté serveur, puis `NetworkTransform Server-to-Client` propage aux autres clients. Beaucoup plus simple, plus robuste.
### Pourquoi désactiver NT local pour le porteur ?
Sans ça, le client porteur reçoit sa propre position serveur avec ~50ms de latence → son objet tremble. En désactivant le NT localement, son objet suit parfaitement ses mouvements (il est pilote par le Rigidbody local).
Quand il pose, on réactive NT → il reçoit alors la position "figée" du serveur (qui a fait `rb.isKinematic=true` + position snap).
### Pourquoi un gestionnaire séparé pour les câbles ?
`CablageReseau` est sur le prefab joueur : chaque joueur a sa propre instance. Impossible d'y mettre une SyncList globale. On crée donc un singleton à part qui porte l'état global.
### Pourquoi un composant séparé pour l'état power ?
Pour éviter de transformer `EtatEquipement` en `NetworkBehaviour` (ce qui casserait les équipements instanciés localement, par ex. pour les tests en éditeur). Composant séparé = découplage propre.
---
## Si ça ne marche pas
1. **Serveur crash au lancement** → probablement `GestionnaireCablesReseau` sans `NetworkIdentity` dans la scène. Vérifier.
2. **Câbles toujours pendants au reco** → regarder les logs `[GestionnaireCablesReseau]`. Si "Timeout", c'est que les refs PDU/port sont toujours null. Ouvrir un ticket.
3. **Portage local trembant**`SetNetworkTransformActif` pas appelé au ramassage. Vérifier logs console.
4. **Erreur `EntityStateMessage without authority`** → il reste un NT en Client-to-Server quelque part. Vérifier tous les prefabs.

View File

@ -0,0 +1,61 @@
using UnityEngine;
using Mirror;
/// <summary>
/// BaieNetworkSetup.cs - v6.3 Multijoueur
///
/// Ajouté automatiquement par BoutiqueReseau lors du spawn d'une baie.
/// Quand le client reçoit l'objet via NetworkServer.Spawn(), les materials
/// créés en runtime par BaieProcedurale.Generer() ne sont PAS synchronisés
/// (Mirror ne sync pas les materials). Ce script régénère la baie localement
/// sur chaque client après le spawn.
///
/// SETUP : Ne pas ajouter manuellement — BoutiqueReseau l'ajoute automatiquement.
/// </summary>
public class BaieNetworkSetup : NetworkBehaviour
{
public override void OnStartClient()
{
base.OnStartClient();
// Sur le serveur/host, la baie est déjà générée dans TraiterPlacementBaieServeur
// Sur les clients distants, il faut la régénérer pour avoir les materials
if (isServer) return;
BaieProcedurale bp = GetComponent<BaieProcedurale>();
if (bp != null)
{
bp.Generer();
bp.InstallerPDUAutomatiquement();
Debug.Log($"[BaieNetworkSetup] Baie régénérée côté client : {gameObject.name}");
}
BaieRack br = GetComponent<BaieRack>() ?? GetComponentInChildren<BaieRack>();
if (br != null) br.GenererSlots();
// Appliquer les layers
int layerBaie = LayerMask.NameToLayer("Baie");
int layerEquip = LayerMask.NameToLayer("Equipement");
if (layerBaie >= 0) SetLayerRecursif(gameObject, layerBaie);
foreach (Transform t in GetComponentsInChildren<Transform>(true))
{
if (t.GetComponent<PDU>() != null && layerEquip >= 0)
SetLayerRecursif(t.gameObject, layerEquip);
}
// v7.1 : notifier le gestionnaire de cables que cette baie est prete.
// Le gestionnaire va retenter de recreer les cables en attente qui
// referencent cette baie / ses PDUs / ses equipements.
if (GestionnaireCablesReseau.Instance != null)
GestionnaireCablesReseau.Instance.NotifierBaiePrete(gameObject);
}
private void SetLayerRecursif(GameObject obj, int layer)
{
obj.layer = layer;
foreach (Transform c in obj.transform)
SetLayerRecursif(c.gameObject, layer);
}
}

View File

@ -0,0 +1,427 @@
using UnityEngine;
using System.Collections.Generic;
using Mirror;
/// <summary>
/// CablageReseau.cs - v6.7 Multijoueur
///
/// v6.7 : sync longueur toron
/// - CmdSyncLongueurToron : met à jour la SyncVar longueur sur le serveur
/// après consommation ou restitution de câble. Propage automatiquement
/// à tous les clients via le hook de ToronNetworkSync.
///
/// v6.4 : sync câbles RJ45 + alimentation + interrupteur prise
///
/// SETUP : Ajouter sur le prefab PlayerCapsule.
/// </summary>
public class CablageReseau : NetworkBehaviour
{
// ════════════════════════════════════════════════════════════
// CÂBLES RJ45
// ════════════════════════════════════════════════════════════
[Command]
public void CmdCreerCableRJ45(uint equipSourceNetId, int portSourceIndex,
uint equipDestNetId, int portDestIndex,
Color couleur, float longueur)
{
Debug.Log($"[Serveur] Câble RJ45 créé : equip {equipSourceNetId} port {portSourceIndex} → equip {equipDestNetId} port {portDestIndex}");
// v7.1 : enregistrer le cable dans le gestionnaire pour le rejoin des clients
if (GestionnaireCablesReseau.Instance != null)
{
GestionnaireCablesReseau.Instance.EnregistrerCableRJ45(
equipSourceNetId, portSourceIndex, equipDestNetId, portDestIndex, couleur, longueur);
}
RpcCreerCableRJ45(equipSourceNetId, portSourceIndex, equipDestNetId, portDestIndex, couleur, longueur);
}
[ClientRpc]
void RpcCreerCableRJ45(uint equipSourceNetId, int portSourceIndex,
uint equipDestNetId, int portDestIndex,
Color couleur, float longueur)
{
if (isLocalPlayer) return;
PortRJ45 portSource = TrouverPortRJ45(equipSourceNetId, portSourceIndex);
PortRJ45 portDest = TrouverPortRJ45(equipDestNetId, portDestIndex);
if (portSource == null || portDest == null)
{
Debug.LogWarning($"[CablageReseau] Ports introuvables pour câble RJ45 distant");
return;
}
List<PointAccroche> chemin = ConstruireCheminAutoLocal(
portSource.GetPointConnexion(), portDest.GetPointConnexion());
string nom = "Cable_" + portSource.nomPort + "_vers_" + portDest.nomPort + "_distant";
GameObject cableObj = new GameObject(nom);
CableRJ45 cable = cableObj.AddComponent<CableRJ45>();
cable.Initialiser(portSource, portDest, chemin, couleur, longueur, null);
portSource.Connecter(portDest, cable);
portDest.Connecter(portSource, cable);
Debug.Log($"[CablageReseau] Câble RJ45 distant créé : {portSource.nomPort} → {portDest.nomPort}");
}
[Command]
public void CmdSupprimerCableRJ45(uint equipSourceNetId, int portSourceIndex,
uint equipDestNetId, int portDestIndex)
{
Debug.Log($"[Serveur] Câble RJ45 supprimé");
// v7.1 : desinscrire du gestionnaire
if (GestionnaireCablesReseau.Instance != null)
{
GestionnaireCablesReseau.Instance.OublierCableRJ45(
equipSourceNetId, portSourceIndex, equipDestNetId, portDestIndex);
}
RpcSupprimerCableRJ45(equipSourceNetId, portSourceIndex, equipDestNetId, portDestIndex);
}
[ClientRpc]
void RpcSupprimerCableRJ45(uint equipSourceNetId, int portSourceIndex,
uint equipDestNetId, int portDestIndex)
{
if (isLocalPlayer) return;
PortRJ45 portSource = TrouverPortRJ45(equipSourceNetId, portSourceIndex);
PortRJ45 portDest = TrouverPortRJ45(equipDestNetId, portDestIndex);
if (portSource != null && portSource.cable != null)
{
portSource.cable.SupprimerComplet();
}
else if (portDest != null && portDest.cable != null)
{
portDest.cable.SupprimerComplet();
}
Debug.Log("[CablageReseau] Câble RJ45 distant supprimé");
}
// ════════════════════════════════════════════════════════════
// SYNC LONGUEUR TORON (v6.7)
// ════════════════════════════════════════════════════════════
/// <summary>
/// v6.7 : Met à jour la SyncVar longueur du toron côté serveur.
/// Appelé par CableManager après consommation ou restitution de câble.
/// La SyncVar de ToronNetworkSync propage automatiquement la nouvelle
/// longueur à TOUS les clients via le hook.
/// </summary>
[Command]
public void CmdSyncLongueurToron(uint toronNetId, float longueurRestante, float longueurTotale)
{
if (!NetworkServer.spawned.ContainsKey(toronNetId)) return;
var obj = NetworkServer.spawned[toronNetId];
if (obj == null) return;
ToronNetworkSync sync = obj.GetComponent<ToronNetworkSync>();
if (sync != null)
{
sync.ServerSetLongueur(longueurTotale, longueurRestante);
Debug.Log($"[Serveur] Longueur toron sync : {obj.name} → {longueurRestante:F1}m / {longueurTotale:F1}m");
}
}
// ════════════════════════════════════════════════════════════
// CÂBLES ALIMENTATION
// ════════════════════════════════════════════════════════════
[Command]
public void CmdCreerCableAlim(uint pduNetId, int indexPrise,
uint equipNetId, int indexPortAlim,
Color couleur)
{
Debug.Log($"[Serveur] Câble alim créé : PDU {pduNetId} prise {indexPrise} → equip {equipNetId} port {indexPortAlim}");
PriseC13 prise = TrouverPriseC13(pduNetId, indexPrise);
PortAlimentation port = TrouverPortAlimentation(equipNetId, indexPortAlim);
if (prise != null && port != null && !prise.estConnectee)
{
GameObject cableObj = new GameObject("CableAlim_serveur");
CableAlimentation cable = cableObj.AddComponent<CableAlimentation>();
cable.couleurCable = couleur;
cable.Initialiser(prise, port);
prise.Connecter(cable, port);
port.Connecter(cable, prise);
}
// v7.1 : enregistrer le cable dans le gestionnaire pour le rejoin des clients
if (GestionnaireCablesReseau.Instance != null)
{
GestionnaireCablesReseau.Instance.EnregistrerCableAlim(
pduNetId, indexPrise, equipNetId, indexPortAlim, couleur);
}
RpcCreerCableAlim(pduNetId, indexPrise, equipNetId, indexPortAlim, couleur);
}
[ClientRpc]
void RpcCreerCableAlim(uint pduNetId, int indexPrise,
uint equipNetId, int indexPortAlim,
Color couleur)
{
if (isLocalPlayer) return;
PriseC13 prise = TrouverPriseC13(pduNetId, indexPrise);
PortAlimentation port = TrouverPortAlimentation(equipNetId, indexPortAlim);
if (prise == null || port == null)
{
Debug.LogWarning("[CablageReseau] Prise/Port introuvables pour câble alim distant");
return;
}
GameObject cableObj = new GameObject("CableAlim_distant");
CableAlimentation cable = cableObj.AddComponent<CableAlimentation>();
cable.couleurCable = couleur;
cable.Initialiser(prise, port);
prise.Connecter(cable, port);
port.Connecter(cable, prise);
Debug.Log($"[CablageReseau] Câble alim distant créé : PDU prise #{indexPrise} → {port.nomPort}");
}
[Command]
public void CmdSupprimerCableAlim(uint pduNetId, int indexPrise,
uint equipNetId, int indexPortAlim)
{
Debug.Log($"[Serveur] Câble alim supprimé");
// v7.1 : desinscrire du gestionnaire
if (GestionnaireCablesReseau.Instance != null)
{
GestionnaireCablesReseau.Instance.OublierCableAlim(
pduNetId, indexPrise, equipNetId, indexPortAlim);
}
PriseC13 prise = TrouverPriseC13(pduNetId, indexPrise);
if (prise != null && prise.cableBranche != null)
prise.cableBranche.Supprimer();
RpcSupprimerCableAlim(pduNetId, indexPrise, equipNetId, indexPortAlim);
}
[ClientRpc]
void RpcSupprimerCableAlim(uint pduNetId, int indexPrise,
uint equipNetId, int indexPortAlim)
{
if (isLocalPlayer) return;
PriseC13 prise = TrouverPriseC13(pduNetId, indexPrise);
if (prise != null && prise.cableBranche != null)
{
prise.cableBranche.Supprimer();
}
else
{
PortAlimentation port = TrouverPortAlimentation(equipNetId, indexPortAlim);
if (port != null && port.cableBranche != null)
port.cableBranche.Supprimer();
}
Debug.Log("[CablageReseau] Câble alim distant supprimé");
}
// ════════════════════════════════════════════════════════════
// INTERRUPTEUR PRISE
// ════════════════════════════════════════════════════════════
[Command]
public void CmdBasculerPrise(uint pduNetId, int indexPrise)
{
PriseC13 prise = TrouverPriseC13(pduNetId, indexPrise);
if (prise != null)
prise.BasculerInterrupteur();
RpcBasculerPrise(pduNetId, indexPrise);
}
[ClientRpc]
void RpcBasculerPrise(uint pduNetId, int indexPrise)
{
if (isLocalPlayer) return;
PriseC13 prise = TrouverPriseC13(pduNetId, indexPrise);
if (prise != null)
prise.BasculerInterrupteur();
}
// ════════════════════════════════════════════════════════════
// HELPERS — Recherche de composants par netId + index
// ════════════════════════════════════════════════════════════
private PortRJ45 TrouverPortRJ45(uint equipNetId, int portIndex)
{
if (!NetworkClient.spawned.ContainsKey(equipNetId)) return null;
GameObject equipGO = NetworkClient.spawned[equipNetId].gameObject;
PortRJ45[] ports = equipGO.GetComponentsInChildren<PortRJ45>();
foreach (var port in ports)
{
if (port.numeroPort == portIndex)
return port;
}
if (portIndex >= 0 && portIndex < ports.Length)
return ports[portIndex];
return null;
}
private PriseC13 TrouverPriseC13(uint pduNetId, int indexPrise)
{
if (!NetworkClient.spawned.ContainsKey(pduNetId)) return null;
GameObject pduGO = NetworkClient.spawned[pduNetId].gameObject;
PDU pdu = pduGO.GetComponent<PDU>();
if (pdu != null && indexPrise >= 0 && indexPrise < pdu.prises.Count)
return pdu.prises[indexPrise];
PriseC13[] prises = pduGO.GetComponentsInChildren<PriseC13>();
foreach (var p in prises)
if (p.indexPrise == indexPrise) return p;
return null;
}
private PortAlimentation TrouverPortAlimentation(uint equipNetId, int indexPort)
{
if (!NetworkClient.spawned.ContainsKey(equipNetId)) return null;
GameObject equipGO = NetworkClient.spawned[equipNetId].gameObject;
PortAlimentation[] ports = equipGO.GetComponentsInChildren<PortAlimentation>();
if (indexPort >= 0 && indexPort < ports.Length)
return ports[indexPort];
return null;
}
private List<PointAccroche> ConstruireCheminAutoLocal(Vector3 posSource, Vector3 posDest)
{
List<PointAccroche> chemin = new List<PointAccroche>();
PointAccroche[] tousPoints = FindObjectsOfType<PointAccroche>();
if (tousPoints.Length == 0) return chemin;
PointAccroche ptDepart = null;
float bestDistDepart = 3f;
foreach (var p in tousPoints)
{
float d = Vector3.Distance(posSource, p.GetPosition());
if (d < bestDistDepart) { bestDistDepart = d; ptDepart = p; }
}
PointAccroche ptArrivee = null;
float bestDistArrivee = 3f;
foreach (var p in tousPoints)
{
float d = Vector3.Distance(posDest, p.GetPosition());
if (d < bestDistArrivee) { bestDistArrivee = d; ptArrivee = p; }
}
if (ptDepart == null || ptArrivee == null) return chemin;
if (ptDepart == ptArrivee) { chemin.Add(ptDepart); return chemin; }
float distanceVoisinage = 1.5f;
Dictionary<PointAccroche, float> gScore = new Dictionary<PointAccroche, float>();
Dictionary<PointAccroche, PointAccroche> cameFrom = new Dictionary<PointAccroche, PointAccroche>();
HashSet<PointAccroche> closedSet = new HashSet<PointAccroche>();
List<PointAccroche> openSet = new List<PointAccroche>();
gScore[ptDepart] = 0;
openSet.Add(ptDepart);
int maxIter = 500;
int iter = 0;
while (openSet.Count > 0 && iter < maxIter)
{
iter++;
PointAccroche current = null;
float bestF = float.MaxValue;
foreach (var n in openSet)
{
float g = gScore.ContainsKey(n) ? gScore[n] : float.MaxValue;
float f = g + Vector3.Distance(n.GetPosition(), ptArrivee.GetPosition());
if (f < bestF) { bestF = f; current = n; }
}
if (current == null) break;
if (current == ptArrivee)
{
List<PointAccroche> result = new List<PointAccroche> { current };
while (cameFrom.ContainsKey(current))
{
current = cameFrom[current];
result.Insert(0, current);
}
return result;
}
openSet.Remove(current);
closedSet.Add(current);
foreach (var autre in tousPoints)
{
if (autre == current || closedSet.Contains(autre)) continue;
if (Vector3.Distance(current.GetPosition(), autre.GetPosition()) > distanceVoisinage) continue;
float tentG = gScore[current] + Vector3.Distance(current.GetPosition(), autre.GetPosition());
if (!openSet.Contains(autre)) openSet.Add(autre);
else if (gScore.ContainsKey(autre) && tentG >= gScore[autre]) continue;
cameFrom[autre] = current;
gScore[autre] = tentG;
}
}
chemin.Add(ptDepart);
chemin.Add(ptArrivee);
return chemin;
}
// ════════════════════════════════════════════════════════════
// HELPERS PUBLICS
// ════════════════════════════════════════════════════════════
public static uint GetEquipNetId(PortRJ45 port)
{
if (port == null) return 0;
NetworkIdentity netId = port.GetComponentInParent<NetworkIdentity>();
return netId != null ? netId.netId : 0;
}
public static uint GetPDUNetId(PriseC13 prise)
{
if (prise == null || prise.pduParent == null) return 0;
NetworkIdentity netId = prise.pduParent.GetComponent<NetworkIdentity>();
return netId != null ? netId.netId : 0;
}
public static uint GetEquipNetIdFromPortAlim(PortAlimentation port)
{
if (port == null || port.equipement == null) return 0;
NetworkIdentity netId = port.equipement.GetComponent<NetworkIdentity>();
return netId != null ? netId.netId : 0;
}
public static int GetPortAlimIndex(PortAlimentation port)
{
if (port == null || port.equipement == null) return 0;
PortAlimentation[] ports = port.equipement.GetComponentsInChildren<PortAlimentation>();
for (int i = 0; i < ports.Length; i++)
if (ports[i] == port) return i;
return 0;
}
}

View File

@ -0,0 +1,306 @@
using UnityEngine;
using System.Collections.Generic;
/// <summary>
/// Gère l'état sous tension d'un équipement rackmount.
///
/// NE GÉNÈRE PAS les ports alimentation → utiliser EquipementAlim pour ça.
/// NE GÉNÈRE PAS de ventilateur → utiliser un child "Ventilateur" existant
/// ou laisser vide (pas de ventilo visuel).
///
/// Quand alimenté :
/// - LEDs d'activité s'allument (emission verte) sur la face avant
/// - Ventilateur tourne (si un child "Ventilateur" est assigné)
/// - Signale à AmbianceSonore qu'une machine est active
///
/// Setup Unity :
/// 1. Ajouter EquipementAlim sur l'équipement → générer les ports alim
/// 2. Ajouter EtatEquipement sur l'équipement
/// 3. Les ports alim sont trouvés automatiquement (GetComponentsInChildren)
/// 4. Optionnel : créer un child "Ventilateur" et l'assigner
/// 5. Configurer le nombre de LEDs, consommation, etc.
/// </summary>
public class EtatEquipement : MonoBehaviour
{
[Header("Configuration")]
[Tooltip("Mode alimentation redondante : l'équipement reste allumé si au moins 1 port est alimenté")]
public bool alimentationRedondante = false;
[Tooltip("Consommation électrique en watts")]
public float consommationWatts = 350f;
[Header("LEDs")]
[Tooltip("Nombre de LEDs d'activité sur la face avant")]
[Range(0, 8)]
public int nombreLEDs = 3;
public Color couleurLEDAllumee = new Color(0f, 1f, 0f, 1f); // Vert
public Color couleurLEDEteinte = new Color(0.05f, 0.05f, 0.05f, 1f); // Quasi noir
public Color couleurLEDStandby = new Color(1f, 0.5f, 0f, 1f); // Orange
[Tooltip("Intensité de l'émission des LEDs")]
public float intensiteLED = 3f;
[Tooltip("Décalage vertical des LEDs depuis le centre")]
public float ledsDecalageY = 0f;
[Tooltip("Décalage horizontal de départ des LEDs")]
public float ledsDecalageX = 0f;
[Tooltip("Espacement entre les LEDs")]
public float ledsEspacement = 0.015f;
[Tooltip("Taille d'une LED")]
public Vector3 ledsTaille = new Vector3(0.006f, 0.006f, 0.003f);
[Header("Ventilateur")]
[Tooltip("Assigner un child 'Ventilateur' existant. Si vide, pas d'animation ventilo.")]
public Transform ventilateur;
[Tooltip("Vitesse de rotation du ventilateur en degrés/seconde")]
public float vitesseVentilateur = 720f;
[Tooltip("Axe de rotation du ventilateur (local)")]
public Vector3 axeRotation = Vector3.forward;
[Header("État (lecture seule)")]
public bool estSousTension = false;
public bool estEnStandby = false;
[Header("Références (auto-remplies au Start)")]
public List<Renderer> ledsRenderers = new List<Renderer>();
// Privé
private bool _ledsGenerees = false;
void Start()
{
// Génère les LEDs visuelles si configurées
if (nombreLEDs > 0 && !_ledsGenerees)
{
GenererLEDs();
}
// Cherche un ventilateur existant si pas assigné
if (ventilateur == null)
{
Transform ventiloChild = transform.Find("Ventilateur");
if (ventiloChild != null)
ventilateur = ventiloChild;
// PAS de création automatique — on ne pollue plus le mesh
}
// État initial : éteint
MettreAJourVisuel();
}
void Update()
{
// Animation ventilateur
if (estSousTension && ventilateur != null)
{
ventilateur.Rotate(axeRotation, vitesseVentilateur * Time.deltaTime, Space.Self);
}
}
/// <summary>
/// Génère les LEDs d'activité sur la face avant de l'équipement.
/// </summary>
private void GenererLEDs()
{
// Supprime les anciennes LEDs
for (int i = transform.childCount - 1; i >= 0; i--)
{
Transform enfant = transform.GetChild(i);
if (enfant.name.StartsWith("LED_Activity_"))
{
if (Application.isPlaying) Destroy(enfant.gameObject);
else DestroyImmediate(enfant.gameObject);
}
}
ledsRenderers.Clear();
Bounds bounds = GetBounds();
float startX = ledsDecalageX - ((nombreLEDs - 1) * ledsEspacement) / 2f;
for (int i = 0; i < nombreLEDs; i++)
{
GameObject ledObj = GameObject.CreatePrimitive(PrimitiveType.Sphere);
ledObj.name = $"LED_Activity_{i}";
ledObj.transform.SetParent(transform);
ledObj.transform.localPosition = new Vector3(
startX + i * ledsEspacement,
ledsDecalageY,
bounds.extents.z + 0.002f // Juste devant la face avant
);
ledObj.transform.localScale = ledsTaille;
// Désactive le collider (pas interactif)
Collider ledCol = ledObj.GetComponent<Collider>();
if (ledCol != null)
{
if (Application.isPlaying) Destroy(ledCol);
else DestroyImmediate(ledCol);
}
// Material noir par défaut
Renderer rend = ledObj.GetComponent<Renderer>();
if (rend != null)
{
rend.material = new Material(Shader.Find("Standard"));
rend.material.color = couleurLEDEteinte;
}
ledsRenderers.Add(rend);
}
_ledsGenerees = true;
}
/// <summary>
/// Vérifie l'état d'alimentation en fonction des ports connectés.
/// Appelé par PortAlimentation quand l'état change.
/// Cherche les ports dynamiquement via GetComponentsInChildren.
/// </summary>
public void VerifierAlimentation()
{
bool ancienEtat = estSousTension;
PortAlimentation[] ports = GetComponentsInChildren<PortAlimentation>();
if (ports.Length == 0)
{
estSousTension = false;
}
else if (alimentationRedondante)
{
// Au moins 1 port alimenté suffit
estSousTension = false;
foreach (var port in ports)
{
if (port.estAlimente)
{
estSousTension = true;
break;
}
}
}
else
{
// Tous les ports doivent être alimentés
estSousTension = true;
foreach (var port in ports)
{
if (!port.estAlimente)
{
estSousTension = false;
break;
}
}
}
if (ancienEtat != estSousTension)
{
MettreAJourVisuel();
string etat = estSousTension ? "SOUS TENSION" : "HORS TENSION";
Debug.Log($"{gameObject.name}: {etat}");
// Notifie l'ambiance sonore
AmbianceSonore ambiance = FindObjectOfType<AmbianceSonore>();
if (ambiance != null)
ambiance.RecalculerAmbiance();
// v7.1 : propager l'etat sous-tension au EtatEquipementNetworkSync (pour rejoin)
if (Mirror.NetworkServer.active)
{
EtatEquipementNetworkSync sync = GetComponent<EtatEquipementNetworkSync>();
if (sync == null) sync = GetComponentInParent<EtatEquipementNetworkSync>();
if (sync != null) sync.ServerSyncEtat();
}
}
}
/// <summary>
/// Met à jour l'apparence visuelle selon l'état d'alimentation.
/// </summary>
public void MettreAJourVisuel()
{
// LEDs
foreach (var ledRenderer in ledsRenderers)
{
if (ledRenderer == null) continue;
Color couleur;
if (estSousTension)
couleur = couleurLEDAllumee;
else if (estEnStandby)
couleur = couleurLEDStandby;
else
couleur = couleurLEDEteinte;
ledRenderer.material.color = couleur;
ledRenderer.material.EnableKeyword("_EMISSION");
if (estSousTension || estEnStandby)
ledRenderer.material.SetColor("_EmissionColor", couleur * intensiteLED);
else
ledRenderer.material.SetColor("_EmissionColor", Color.black);
}
// Clignotement
if (estSousTension)
InvokeRepeating(nameof(ClignoterLEDs), 0.5f, 0.3f);
else
CancelInvoke(nameof(ClignoterLEDs));
}
/// <summary>
/// Fait clignoter aléatoirement les LEDs pour simuler l'activité.
/// </summary>
private void ClignoterLEDs()
{
if (!estSousTension) return;
foreach (var ledRenderer in ledsRenderers)
{
if (ledRenderer == null) continue;
if (Random.value < 0.3f)
{
bool allumee = Random.value > 0.5f;
Color couleur = allumee ? couleurLEDAllumee : couleurLEDEteinte;
ledRenderer.material.color = couleur;
ledRenderer.material.SetColor("_EmissionColor",
allumee ? couleurLEDAllumee * intensiteLED : Color.black);
}
}
}
/// <summary>
/// Retourne la consommation actuelle en watts (0 si éteint).
/// </summary>
public float GetConsommationActuelle()
{
return estSousTension ? consommationWatts : 0f;
}
private Bounds GetBounds()
{
BoxCollider boxCol = GetComponent<BoxCollider>();
if (boxCol != null)
return new Bounds(boxCol.center, boxCol.size);
Renderer rend = GetComponent<Renderer>();
if (rend != null)
{
Bounds worldBounds = rend.bounds;
Vector3 localCenter = transform.InverseTransformPoint(worldBounds.center);
Vector3 localSize = new Vector3(
worldBounds.size.x / transform.lossyScale.x,
worldBounds.size.y / transform.lossyScale.y,
worldBounds.size.z / transform.lossyScale.z
);
return new Bounds(localCenter, localSize);
}
return new Bounds(Vector3.zero, new Vector3(0.48f, 0.044f, 0.7f));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,107 @@
// EtatEquipementNetworkSync.cs - v1.0
//
// Composant a ajouter sur chaque prefab d'equipement rackmount qui possede un
// EtatEquipement + BoutonPower (Serveur_1U, Switch_24P, Parefeu, etc.).
//
// Synchronise 2 etats critiques au rejoin :
// 1. L'etat "bouton Power allume" (BoutonPower.estAllume)
// 2. L'etat "sous tension" de l'equipement (EtatEquipement.estSousTension)
//
// Le serveur est autoritaire : quand le serveur change l'etat (via
// CmdAppuyerBoutonPower), il appelle ServerSetEstAllume() qui met a jour la
// SyncVar. Les clients (actuellement connectes ET futurs rejoigneurs)
// recoivent l'etat via le hook SyncVar.
//
// Note : les animations visuelles (ventilos, LEDs) sont gerees par l'Update()
// de EtatEquipement et BoutonPower qui lisent simplement les bool locaux.
using UnityEngine;
using Mirror;
[RequireComponent(typeof(NetworkIdentity))]
public class EtatEquipementNetworkSync : NetworkBehaviour
{
[SyncVar(hook = nameof(OnBoutonAllumeChanged))]
private bool _syncBoutonAllume = false;
[SyncVar(hook = nameof(OnSousTensionChanged))]
private bool _syncSousTension = false;
private BoutonPower _bouton;
private EtatEquipement _etat;
void Awake()
{
_bouton = GetComponentInChildren<BoutonPower>();
_etat = GetComponentInChildren<EtatEquipement>();
}
public override void OnStartClient()
{
base.OnStartClient();
// Sur le host, rien a faire (le serveur est deja a jour)
if (NetworkServer.active) return;
// Au rejoin : appliquer les valeurs actuelles des SyncVars.
// Les hooks ne sont PAS appeles automatiquement pour les valeurs initiales,
// il faut forcer l'application ici.
ApplyBoutonAllume(_syncBoutonAllume);
ApplySousTension(_syncSousTension);
}
// ============================================================
// API SERVEUR
// ============================================================
/// <summary>
/// Appele cote serveur quand BoutonPower.Appuyer() a ete execute et qu'il
/// faut synchroniser l'etat aux clients (actuels et futurs).
/// </summary>
[Server]
public void ServerSyncEtat()
{
if (_bouton != null) _syncBoutonAllume = _bouton.estAllume;
if (_etat != null) _syncSousTension = _etat.estSousTension;
}
// ============================================================
// HOOKS
// ============================================================
void OnBoutonAllumeChanged(bool ancien, bool nouveau)
{
ApplyBoutonAllume(nouveau);
}
void OnSousTensionChanged(bool ancien, bool nouveau)
{
ApplySousTension(nouveau);
}
// ============================================================
// APPLICATION LOCALE
// ============================================================
private void ApplyBoutonAllume(bool valeur)
{
if (_bouton == null) _bouton = GetComponentInChildren<BoutonPower>();
if (_bouton == null) return;
if (_bouton.estAllume != valeur)
{
// On assigne directement le champ sans appeler Appuyer() (qui ferait
// des effets de bord comme verifier le cablage, rejouer les sons, etc.)
_bouton.estAllume = valeur;
_bouton.MettreAJourVisuel();
}
}
private void ApplySousTension(bool valeur)
{
if (_etat == null) _etat = GetComponentInChildren<EtatEquipement>();
if (_etat == null) return;
_etat.estSousTension = valeur;
}
}

View File

@ -0,0 +1,474 @@
// GestionnaireCablesReseau.cs - v1.0
//
// Singleton NetworkBehaviour qui synchronise les cables (alimentation + RJ45) entre
// le serveur et les clients, y compris au moment du rejoin d'un client.
//
// Architecture :
// - Cote serveur : chaque creation de cable est enregistree dans une SyncList.
// La suppression retire l'entree correspondante.
// - Cote client au rejoin (OnStartClient) : parcours des SyncLists et recreation
// locale de chaque cable. On attend d'abord que toutes les baies et PDUs soient
// "pretes" (via un systeme d'event) pour eviter les refs null sur les ports.
//
// Setup Unity :
// 1. Dans la scene Datacenter_01, creer un GameObject "GestionnaireCables_Holder"
// 2. Add Component : NetworkIdentity (rien coche)
// 3. Add Component : GestionnaireCablesReseau
// 4. Sauver la scene
//
// Le gestionnaire est appele depuis CablageReseau.CmdCreerCable* / CmdSupprimerCable*
// via GestionnaireCablesReseau.Instance.
using UnityEngine;
using Mirror;
using System.Collections;
using System.Collections.Generic;
public class GestionnaireCablesReseau : NetworkBehaviour
{
public static GestionnaireCablesReseau Instance { get; private set; }
// ============================================================
// STRUCTURES DE DONNEES
// ============================================================
[System.Serializable]
public struct CableAlimData
{
public uint pduNetId;
public int indexPrise;
public uint equipNetId;
public int indexPortAlim;
public Color couleur;
public string Id => $"A_{pduNetId}_{indexPrise}_{equipNetId}_{indexPortAlim}";
}
[System.Serializable]
public struct CableRJ45Data
{
public uint equipSourceNetId;
public int portSourceIndex;
public uint equipDestNetId;
public int portDestIndex;
public Color couleur;
public float longueur;
public string Id => $"R_{equipSourceNetId}_{portSourceIndex}_{equipDestNetId}_{portDestIndex}";
}
// SyncLists — synchronisees automatiquement aux clients
private readonly SyncList<CableAlimData> _cablesAlim = new SyncList<CableAlimData>();
private readonly SyncList<CableRJ45Data> _cablesRJ45 = new SyncList<CableRJ45Data>();
// ============================================================
// EVENT : "Baies et equipements pretes pour recevoir les cables"
// ============================================================
//
// Probleme : au rejoin, les baies sont spawnees dans un ordre imprevisible.
// Si on tente de recreer un cable alors que la baie+PDU cibles ne sont pas
// encore pretes (BaieNetworkSetup.OnStartClient pas encore termine), on aura
// des refs null.
//
// Solution : chaque BaieNetworkSetup notifie le gestionnaire quand sa baie
// est prete. Quand on recoit une notification, on re-tente les cables en
// attente (ceux qui referencaient cette baie/PDU).
// Structures de "cables en attente" cote client
private readonly HashSet<string> _cablesAlimRecrees = new HashSet<string>();
private readonly HashSet<string> _cablesRJ45Recrees = new HashSet<string>();
// Cables en attente : ceux qu'on a tente de recreer mais dont les refs etaient null
private readonly List<CableAlimData> _attenteAlim = new List<CableAlimData>();
private readonly List<CableRJ45Data> _attenteRJ45 = new List<CableRJ45Data>();
// ============================================================
// LIFECYCLE
// ============================================================
void Awake()
{
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
Instance = this;
}
void OnDestroy()
{
if (Instance == this) Instance = null;
}
public override void OnStartClient()
{
base.OnStartClient();
// Sur le host, rien a faire : le serveur a deja cree les cables en live
if (NetworkServer.active) return;
Debug.Log($"[GestionnaireCablesReseau] Client rejoint : {_cablesAlim.Count} cables alim, {_cablesRJ45.Count} cables RJ45 a recreer");
// Lancer la coroutine qui tentera de recreer les cables regulierement
// jusqu'a ce que tous soient crees (ou abandon apres timeout)
StartCoroutine(RejouerCablesProgressivement());
}
// ============================================================
// API SERVEUR (appelee par CablageReseau)
// ============================================================
[Server]
public void EnregistrerCableAlim(uint pduNetId, int indexPrise, uint equipNetId, int indexPortAlim, Color couleur)
{
CableAlimData data = new CableAlimData
{
pduNetId = pduNetId,
indexPrise = indexPrise,
equipNetId = equipNetId,
indexPortAlim = indexPortAlim,
couleur = couleur
};
// Eviter les doublons (meme prise + meme port = meme cable)
for (int i = 0; i < _cablesAlim.Count; i++)
{
if (_cablesAlim[i].pduNetId == pduNetId && _cablesAlim[i].indexPrise == indexPrise &&
_cablesAlim[i].equipNetId == equipNetId && _cablesAlim[i].indexPortAlim == indexPortAlim)
return;
}
_cablesAlim.Add(data);
Debug.Log($"[GestionnaireCablesReseau] Cable alim enregistre (total : {_cablesAlim.Count})");
}
[Server]
public void OublierCableAlim(uint pduNetId, int indexPrise, uint equipNetId, int indexPortAlim)
{
for (int i = _cablesAlim.Count - 1; i >= 0; i--)
{
if (_cablesAlim[i].pduNetId == pduNetId && _cablesAlim[i].indexPrise == indexPrise &&
_cablesAlim[i].equipNetId == equipNetId && _cablesAlim[i].indexPortAlim == indexPortAlim)
{
_cablesAlim.RemoveAt(i);
Debug.Log($"[GestionnaireCablesReseau] Cable alim supprime (reste : {_cablesAlim.Count})");
return;
}
}
}
[Server]
public void EnregistrerCableRJ45(uint equipSourceNetId, int portSourceIndex,
uint equipDestNetId, int portDestIndex,
Color couleur, float longueur)
{
CableRJ45Data data = new CableRJ45Data
{
equipSourceNetId = equipSourceNetId,
portSourceIndex = portSourceIndex,
equipDestNetId = equipDestNetId,
portDestIndex = portDestIndex,
couleur = couleur,
longueur = longueur
};
for (int i = 0; i < _cablesRJ45.Count; i++)
{
if (_cablesRJ45[i].equipSourceNetId == equipSourceNetId &&
_cablesRJ45[i].portSourceIndex == portSourceIndex &&
_cablesRJ45[i].equipDestNetId == equipDestNetId &&
_cablesRJ45[i].portDestIndex == portDestIndex)
return;
}
_cablesRJ45.Add(data);
Debug.Log($"[GestionnaireCablesReseau] Cable RJ45 enregistre (total : {_cablesRJ45.Count})");
}
[Server]
public void OublierCableRJ45(uint equipSourceNetId, int portSourceIndex,
uint equipDestNetId, int portDestIndex)
{
for (int i = _cablesRJ45.Count - 1; i >= 0; i--)
{
var c = _cablesRJ45[i];
// Match dans les deux sens car un cable RJ45 est bidirectionnel
bool match1 = c.equipSourceNetId == equipSourceNetId && c.portSourceIndex == portSourceIndex &&
c.equipDestNetId == equipDestNetId && c.portDestIndex == portDestIndex;
bool match2 = c.equipSourceNetId == equipDestNetId && c.portSourceIndex == portDestIndex &&
c.equipDestNetId == equipSourceNetId && c.portDestIndex == portSourceIndex;
if (match1 || match2)
{
_cablesRJ45.RemoveAt(i);
Debug.Log($"[GestionnaireCablesReseau] Cable RJ45 supprime (reste : {_cablesRJ45.Count})");
return;
}
}
}
// ============================================================
// API CLIENT : notification de baie prete
// ============================================================
/// <summary>
/// Appele par BaieNetworkSetup.OnStartClient une fois que la baie + ses PDUs
/// sont genere localement. Le gestionnaire tente de recreer les cables en
/// attente qui referencaient cette baie ou ses enfants.
/// </summary>
public void NotifierBaiePrete(GameObject baie)
{
if (!NetworkClient.active || NetworkServer.active) return;
Debug.Log($"[GestionnaireCablesReseau] Baie prete : {baie.name}, retry cables en attente");
RetenterCablesEnAttente();
}
// ============================================================
// COROUTINE DE REJEU (client qui rejoint)
// ============================================================
private IEnumerator RejouerCablesProgressivement()
{
// Attendre 0.5s pour laisser les premieres baies + PDUs se generer via BaieNetworkSetup
yield return new WaitForSeconds(0.5f);
// Initialiser la liste d'attente avec tous les cables connus
_attenteAlim.Clear();
foreach (var data in _cablesAlim) _attenteAlim.Add(data);
_attenteRJ45.Clear();
foreach (var data in _cablesRJ45) _attenteRJ45.Add(data);
// Tenter une premiere passe
RetenterCablesEnAttente();
// Si il reste des cables en attente, re-tenter periodiquement
// (les notifications de baie prete vont aussi declencher des retries)
float tempsEcoule = 0f;
const float timeoutMax = 15f;
const float intervalRetry = 0.5f;
while ((_attenteAlim.Count > 0 || _attenteRJ45.Count > 0) && tempsEcoule < timeoutMax)
{
yield return new WaitForSeconds(intervalRetry);
tempsEcoule += intervalRetry;
RetenterCablesEnAttente();
}
if (_attenteAlim.Count > 0 || _attenteRJ45.Count > 0)
{
Debug.LogWarning($"[GestionnaireCablesReseau] Timeout : {_attenteAlim.Count} cables alim + {_attenteRJ45.Count} cables RJ45 jamais recrees");
}
else
{
Debug.Log($"[GestionnaireCablesReseau] Tous les cables ont ete recrees");
}
}
private void RetenterCablesEnAttente()
{
// Cables alim
for (int i = _attenteAlim.Count - 1; i >= 0; i--)
{
CableAlimData data = _attenteAlim[i];
if (_cablesAlimRecrees.Contains(data.Id)) { _attenteAlim.RemoveAt(i); continue; }
if (TenterRecreerCableAlim(data))
{
_cablesAlimRecrees.Add(data.Id);
_attenteAlim.RemoveAt(i);
}
}
// Cables RJ45
for (int i = _attenteRJ45.Count - 1; i >= 0; i--)
{
CableRJ45Data data = _attenteRJ45[i];
if (_cablesRJ45Recrees.Contains(data.Id)) { _attenteRJ45.RemoveAt(i); continue; }
if (TenterRecreerCableRJ45(data))
{
_cablesRJ45Recrees.Add(data.Id);
_attenteRJ45.RemoveAt(i);
}
}
}
// ============================================================
// RECREATION LOCALE DES CABLES
// ============================================================
private bool TenterRecreerCableAlim(CableAlimData data)
{
PriseC13 prise = TrouverPriseC13(data.pduNetId, data.indexPrise);
PortAlimentation port = TrouverPortAlimentation(data.equipNetId, data.indexPortAlim);
if (prise == null || port == null) return false;
// Deja connecte ? (si BaieNetworkSetup l'a fait automatiquement via InstallerPDUAutomatiquement)
if (prise.estConnectee || port.estConnecte) return true;
GameObject cableObj = new GameObject($"CableAlim_rejoin_{data.pduNetId}_{data.indexPrise}");
CableAlimentation cable = cableObj.AddComponent<CableAlimentation>();
cable.couleurCable = data.couleur;
cable.Initialiser(prise, port);
prise.Connecter(cable, port);
port.Connecter(cable, prise);
Debug.Log($"[GestionnaireCablesReseau] Cable alim recree : PDU {data.pduNetId} prise #{data.indexPrise} -> {port.nomPort}");
return true;
}
private bool TenterRecreerCableRJ45(CableRJ45Data data)
{
PortRJ45 portSource = TrouverPortRJ45(data.equipSourceNetId, data.portSourceIndex);
PortRJ45 portDest = TrouverPortRJ45(data.equipDestNetId, data.portDestIndex);
if (portSource == null || portDest == null) return false;
// Deja connecte ?
if (portSource.cable != null || portDest.cable != null) return true;
List<PointAccroche> chemin = ConstruireCheminAuto(
portSource.GetPointConnexion(), portDest.GetPointConnexion());
GameObject cableObj = new GameObject($"CableRJ45_rejoin_{data.equipSourceNetId}_{data.portSourceIndex}");
CableRJ45 cable = cableObj.AddComponent<CableRJ45>();
cable.Initialiser(portSource, portDest, chemin, data.couleur, data.longueur, null);
portSource.Connecter(portDest, cable);
portDest.Connecter(portSource, cable);
Debug.Log($"[GestionnaireCablesReseau] Cable RJ45 recree : {portSource.nomPort} -> {portDest.nomPort}");
return true;
}
// ============================================================
// HELPERS DE RECHERCHE (copies depuis CablageReseau)
// ============================================================
private PortRJ45 TrouverPortRJ45(uint equipNetId, int portIndex)
{
if (!NetworkClient.spawned.ContainsKey(equipNetId)) return null;
GameObject equipGO = NetworkClient.spawned[equipNetId].gameObject;
if (equipGO == null) return null;
PortRJ45[] ports = equipGO.GetComponentsInChildren<PortRJ45>();
foreach (var port in ports)
if (port.numeroPort == portIndex) return port;
if (portIndex >= 0 && portIndex < ports.Length)
return ports[portIndex];
return null;
}
private PriseC13 TrouverPriseC13(uint pduNetId, int indexPrise)
{
if (!NetworkClient.spawned.ContainsKey(pduNetId)) return null;
GameObject pduGO = NetworkClient.spawned[pduNetId].gameObject;
if (pduGO == null) return null;
PDU pdu = pduGO.GetComponent<PDU>();
if (pdu != null && indexPrise >= 0 && indexPrise < pdu.prises.Count)
return pdu.prises[indexPrise];
PriseC13[] prises = pduGO.GetComponentsInChildren<PriseC13>();
foreach (var p in prises)
if (p.indexPrise == indexPrise) return p;
return null;
}
private PortAlimentation TrouverPortAlimentation(uint equipNetId, int indexPort)
{
if (!NetworkClient.spawned.ContainsKey(equipNetId)) return null;
GameObject equipGO = NetworkClient.spawned[equipNetId].gameObject;
if (equipGO == null) return null;
PortAlimentation[] ports = equipGO.GetComponentsInChildren<PortAlimentation>();
if (indexPort >= 0 && indexPort < ports.Length) return ports[indexPort];
return null;
}
private List<PointAccroche> ConstruireCheminAuto(Vector3 posSource, Vector3 posDest)
{
// Algorithme A* simplifie (copie depuis CablageReseau)
List<PointAccroche> chemin = new List<PointAccroche>();
PointAccroche[] tousPoints = FindObjectsOfType<PointAccroche>();
if (tousPoints.Length == 0) return chemin;
PointAccroche ptDepart = null;
float bestDistDepart = 3f;
foreach (var p in tousPoints)
{
float d = Vector3.Distance(posSource, p.GetPosition());
if (d < bestDistDepart) { bestDistDepart = d; ptDepart = p; }
}
PointAccroche ptArrivee = null;
float bestDistArrivee = 3f;
foreach (var p in tousPoints)
{
float d = Vector3.Distance(posDest, p.GetPosition());
if (d < bestDistArrivee) { bestDistArrivee = d; ptArrivee = p; }
}
if (ptDepart == null || ptArrivee == null) return chemin;
if (ptDepart == ptArrivee) { chemin.Add(ptDepart); return chemin; }
float distanceVoisinage = 1.5f;
Dictionary<PointAccroche, float> gScore = new Dictionary<PointAccroche, float>();
Dictionary<PointAccroche, PointAccroche> cameFrom = new Dictionary<PointAccroche, PointAccroche>();
HashSet<PointAccroche> closedSet = new HashSet<PointAccroche>();
List<PointAccroche> openSet = new List<PointAccroche>();
gScore[ptDepart] = 0;
openSet.Add(ptDepart);
int maxIter = 500;
int iter = 0;
while (openSet.Count > 0 && iter < maxIter)
{
iter++;
PointAccroche current = null;
float bestF = float.MaxValue;
foreach (var n in openSet)
{
float g = gScore.ContainsKey(n) ? gScore[n] : float.MaxValue;
float f = g + Vector3.Distance(n.GetPosition(), ptArrivee.GetPosition());
if (f < bestF) { bestF = f; current = n; }
}
if (current == null) break;
if (current == ptArrivee)
{
List<PointAccroche> result = new List<PointAccroche> { current };
while (cameFrom.ContainsKey(current))
{
current = cameFrom[current];
result.Insert(0, current);
}
return result;
}
openSet.Remove(current);
closedSet.Add(current);
foreach (var autre in tousPoints)
{
if (autre == current || closedSet.Contains(autre)) continue;
if (Vector3.Distance(current.GetPosition(), autre.GetPosition()) > distanceVoisinage) continue;
float tentG = gScore[current] + Vector3.Distance(current.GetPosition(), autre.GetPosition());
if (!openSet.Contains(autre)) openSet.Add(autre);
else if (gScore.ContainsKey(autre) && tentG >= gScore[autre]) continue;
cameFrom[autre] = current;
gScore[autre] = tentG;
}
}
chemin.Add(ptDepart);
chemin.Add(ptArrivee);
return chemin;
}
}