diff --git a/Dedicated_Server_Linux/DatacenterSim.x86_64 b/Dedicated_Server_Linux/DatacenterSim.x86_64
deleted file mode 100755
index 9021e73..0000000
Binary files a/Dedicated_Server_Linux/DatacenterSim.x86_64 and /dev/null differ
diff --git a/Dedicated_Server_Linux/DatacenterSim_Data/Plugins/lib_burst_generated.so b/Dedicated_Server_Linux/DatacenterSim_Data/Plugins/lib_burst_generated.so
deleted file mode 100644
index 9c188d5..0000000
Binary files a/Dedicated_Server_Linux/DatacenterSim_Data/Plugins/lib_burst_generated.so and /dev/null differ
diff --git a/Dedicated_Server_Linux/DatacenterSim_Data/Resources/UnityPlayer.png b/Dedicated_Server_Linux/DatacenterSim_Data/Resources/UnityPlayer.png
deleted file mode 100644
index 43897cf..0000000
Binary files a/Dedicated_Server_Linux/DatacenterSim_Data/Resources/UnityPlayer.png and /dev/null differ
diff --git a/Dedicated_Server_Linux/DatacenterSim_Data/Resources/unity default resources b/Dedicated_Server_Linux/DatacenterSim_Data/Resources/unity default resources
deleted file mode 100644
index 7cf4261..0000000
Binary files a/Dedicated_Server_Linux/DatacenterSim_Data/Resources/unity default resources and /dev/null differ
diff --git a/Dedicated_Server_Linux/DatacenterSim_Data/Resources/unity_builtin_extra b/Dedicated_Server_Linux/DatacenterSim_Data/Resources/unity_builtin_extra
deleted file mode 100644
index 5b39231..0000000
Binary files a/Dedicated_Server_Linux/DatacenterSim_Data/Resources/unity_builtin_extra and /dev/null differ
diff --git a/Dedicated_Server_Linux/DatacenterSim_Data/RuntimeInitializeOnLoads.json b/Dedicated_Server_Linux/DatacenterSim_Data/RuntimeInitializeOnLoads.json
deleted file mode 100644
index e648b56..0000000
--- a/Dedicated_Server_Linux/DatacenterSim_Data/RuntimeInitializeOnLoads.json
+++ /dev/null
@@ -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}]}
diff --git a/Dedicated_Server_Linux/DatacenterSim_Data/ScriptingAssemblies.json b/Dedicated_Server_Linux/DatacenterSim_Data/ScriptingAssemblies.json
deleted file mode 100644
index c32c357..0000000
--- a/Dedicated_Server_Linux/DatacenterSim_Data/ScriptingAssemblies.json
+++ /dev/null
@@ -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]}
\ No newline at end of file
diff --git a/Dedicated_Server_Linux/DatacenterSim_Data/app.info b/Dedicated_Server_Linux/DatacenterSim_Data/app.info
deleted file mode 100644
index 7a89685..0000000
--- a/Dedicated_Server_Linux/DatacenterSim_Data/app.info
+++ /dev/null
@@ -1,2 +0,0 @@
-MAURO Stéphane
-DatacenterSim_Alpha
\ No newline at end of file
diff --git a/Dedicated_Server_Linux/DatacenterSim_Data/boot.config b/Dedicated_Server_Linux/DatacenterSim_Data/boot.config
deleted file mode 100644
index a6da18b..0000000
--- a/Dedicated_Server_Linux/DatacenterSim_Data/boot.config
+++ /dev/null
@@ -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
diff --git a/Dedicated_Server_Linux/DatacenterSim_Data/globalgamemanagers b/Dedicated_Server_Linux/DatacenterSim_Data/globalgamemanagers
deleted file mode 100644
index 235e219..0000000
Binary files a/Dedicated_Server_Linux/DatacenterSim_Data/globalgamemanagers and /dev/null differ
diff --git a/Dedicated_Server_Linux/DatacenterSim_Data/globalgamemanagers.assets b/Dedicated_Server_Linux/DatacenterSim_Data/globalgamemanagers.assets
deleted file mode 100644
index 89ea865..0000000
Binary files a/Dedicated_Server_Linux/DatacenterSim_Data/globalgamemanagers.assets and /dev/null differ
diff --git a/Dedicated_Server_Linux/DatacenterSim_Data/il2cpp_data/Metadata/global-metadata.dat b/Dedicated_Server_Linux/DatacenterSim_Data/il2cpp_data/Metadata/global-metadata.dat
deleted file mode 100644
index f4d6d3e..0000000
Binary files a/Dedicated_Server_Linux/DatacenterSim_Data/il2cpp_data/Metadata/global-metadata.dat and /dev/null differ
diff --git a/Dedicated_Server_Linux/DatacenterSim_Data/il2cpp_data/Resources/mscorlib.dll-resources.dat b/Dedicated_Server_Linux/DatacenterSim_Data/il2cpp_data/Resources/mscorlib.dll-resources.dat
deleted file mode 100644
index 6d144fc..0000000
Binary files a/Dedicated_Server_Linux/DatacenterSim_Data/il2cpp_data/Resources/mscorlib.dll-resources.dat and /dev/null differ
diff --git a/Dedicated_Server_Linux/DatacenterSim_Data/level0 b/Dedicated_Server_Linux/DatacenterSim_Data/level0
deleted file mode 100644
index 99dc680..0000000
Binary files a/Dedicated_Server_Linux/DatacenterSim_Data/level0 and /dev/null differ
diff --git a/Dedicated_Server_Linux/DatacenterSim_Data/resources.assets b/Dedicated_Server_Linux/DatacenterSim_Data/resources.assets
deleted file mode 100644
index 4528481..0000000
Binary files a/Dedicated_Server_Linux/DatacenterSim_Data/resources.assets and /dev/null differ
diff --git a/Dedicated_Server_Linux/DatacenterSim_Data/sharedassets0.assets b/Dedicated_Server_Linux/DatacenterSim_Data/sharedassets0.assets
deleted file mode 100644
index 116992e..0000000
Binary files a/Dedicated_Server_Linux/DatacenterSim_Data/sharedassets0.assets and /dev/null differ
diff --git a/Dedicated_Server_Linux/DatacenterSim_Data/sharedassets0.resource b/Dedicated_Server_Linux/DatacenterSim_Data/sharedassets0.resource
deleted file mode 100644
index 465cf10..0000000
Binary files a/Dedicated_Server_Linux/DatacenterSim_Data/sharedassets0.resource and /dev/null differ
diff --git a/Dedicated_Server_Linux/GameAssembly.so b/Dedicated_Server_Linux/GameAssembly.so
deleted file mode 100644
index f386cda..0000000
Binary files a/Dedicated_Server_Linux/GameAssembly.so and /dev/null differ
diff --git a/Dedicated_Server_Linux/UnityPlayer.so b/Dedicated_Server_Linux/UnityPlayer.so
deleted file mode 100644
index 50b14d6..0000000
Binary files a/Dedicated_Server_Linux/UnityPlayer.so and /dev/null differ
diff --git a/Patchs/pack_multi/README_PACK_v7.1.md b/Patchs/pack_multi/README_PACK_v7.1.md
new file mode 100644
index 0000000..73ef7f5
--- /dev/null
+++ b/Patchs/pack_multi/README_PACK_v7.1.md
@@ -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.
diff --git a/Patchs/pack_multi/Scripts_Modifies/BaieNetworkSetup.cs b/Patchs/pack_multi/Scripts_Modifies/BaieNetworkSetup.cs
new file mode 100644
index 0000000..47316aa
--- /dev/null
+++ b/Patchs/pack_multi/Scripts_Modifies/BaieNetworkSetup.cs
@@ -0,0 +1,61 @@
+using UnityEngine;
+using Mirror;
+
+///
+/// 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.
+///
+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();
+ if (bp != null)
+ {
+ bp.Generer();
+ bp.InstallerPDUAutomatiquement();
+ Debug.Log($"[BaieNetworkSetup] Baie régénérée côté client : {gameObject.name}");
+ }
+
+ BaieRack br = GetComponent() ?? GetComponentInChildren();
+ 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(true))
+ {
+ if (t.GetComponent() != 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);
+ }
+}
\ No newline at end of file
diff --git a/Patchs/pack_multi/Scripts_Modifies/CablageReseau.cs b/Patchs/pack_multi/Scripts_Modifies/CablageReseau.cs
new file mode 100644
index 0000000..59b256f
--- /dev/null
+++ b/Patchs/pack_multi/Scripts_Modifies/CablageReseau.cs
@@ -0,0 +1,427 @@
+using UnityEngine;
+using System.Collections.Generic;
+using Mirror;
+
+///
+/// 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.
+///
+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 chemin = ConstruireCheminAutoLocal(
+ portSource.GetPointConnexion(), portDest.GetPointConnexion());
+
+ string nom = "Cable_" + portSource.nomPort + "_vers_" + portDest.nomPort + "_distant";
+ GameObject cableObj = new GameObject(nom);
+ CableRJ45 cable = cableObj.AddComponent();
+ 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)
+ // ════════════════════════════════════════════════════════════
+
+ ///
+ /// 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.
+ ///
+ [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();
+ 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();
+ 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();
+ 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();
+
+ 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();
+
+ if (pdu != null && indexPrise >= 0 && indexPrise < pdu.prises.Count)
+ return pdu.prises[indexPrise];
+
+ PriseC13[] prises = pduGO.GetComponentsInChildren();
+ 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();
+
+ if (indexPort >= 0 && indexPort < ports.Length)
+ return ports[indexPort];
+
+ return null;
+ }
+
+ private List ConstruireCheminAutoLocal(Vector3 posSource, Vector3 posDest)
+ {
+ List chemin = new List();
+ PointAccroche[] tousPoints = FindObjectsOfType();
+
+ 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 gScore = new Dictionary();
+ Dictionary cameFrom = new Dictionary();
+ HashSet closedSet = new HashSet();
+ List openSet = new List();
+
+ 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 result = new List { 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();
+ 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();
+ 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();
+ 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();
+ for (int i = 0; i < ports.Length; i++)
+ if (ports[i] == port) return i;
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/Patchs/pack_multi/Scripts_Modifies/EtatEquipement.cs b/Patchs/pack_multi/Scripts_Modifies/EtatEquipement.cs
new file mode 100644
index 0000000..e9196bd
--- /dev/null
+++ b/Patchs/pack_multi/Scripts_Modifies/EtatEquipement.cs
@@ -0,0 +1,306 @@
+using UnityEngine;
+using System.Collections.Generic;
+
+///
+/// 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.
+///
+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 ledsRenderers = new List();
+
+ // 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);
+ }
+ }
+
+ ///
+ /// Génère les LEDs d'activité sur la face avant de l'équipement.
+ ///
+ 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();
+ if (ledCol != null)
+ {
+ if (Application.isPlaying) Destroy(ledCol);
+ else DestroyImmediate(ledCol);
+ }
+
+ // Material noir par défaut
+ Renderer rend = ledObj.GetComponent();
+ if (rend != null)
+ {
+ rend.material = new Material(Shader.Find("Standard"));
+ rend.material.color = couleurLEDEteinte;
+ }
+
+ ledsRenderers.Add(rend);
+ }
+
+ _ledsGenerees = true;
+ }
+
+ ///
+ /// 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.
+ ///
+ public void VerifierAlimentation()
+ {
+ bool ancienEtat = estSousTension;
+
+ PortAlimentation[] ports = GetComponentsInChildren();
+
+ 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();
+ if (ambiance != null)
+ ambiance.RecalculerAmbiance();
+
+ // v7.1 : propager l'etat sous-tension au EtatEquipementNetworkSync (pour rejoin)
+ if (Mirror.NetworkServer.active)
+ {
+ EtatEquipementNetworkSync sync = GetComponent();
+ if (sync == null) sync = GetComponentInParent();
+ if (sync != null) sync.ServerSyncEtat();
+ }
+ }
+ }
+
+ ///
+ /// Met à jour l'apparence visuelle selon l'état d'alimentation.
+ ///
+ 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));
+ }
+
+ ///
+ /// Fait clignoter aléatoirement les LEDs pour simuler l'activité.
+ ///
+ 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);
+ }
+ }
+ }
+
+ ///
+ /// Retourne la consommation actuelle en watts (0 si éteint).
+ ///
+ public float GetConsommationActuelle()
+ {
+ return estSousTension ? consommationWatts : 0f;
+ }
+
+ private Bounds GetBounds()
+ {
+ BoxCollider boxCol = GetComponent();
+ if (boxCol != null)
+ return new Bounds(boxCol.center, boxCol.size);
+
+ Renderer rend = GetComponent();
+ 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));
+ }
+}
\ No newline at end of file
diff --git a/Patchs/pack_multi/Scripts_Modifies/PlayerInteraction.cs b/Patchs/pack_multi/Scripts_Modifies/PlayerInteraction.cs
new file mode 100644
index 0000000..9c0a737
--- /dev/null
+++ b/Patchs/pack_multi/Scripts_Modifies/PlayerInteraction.cs
@@ -0,0 +1,1476 @@
+using UnityEngine;
+using System.Collections.Generic;
+using Mirror;
+
+///
+/// PlayerInteraction.cs - v6.13 Multijoueur
+///
+/// v6.13 : Fix des syncs manquantes en multijoueur
+/// BUG 1 : La pile de ZoneLivraison ne se reorganise pas cote autres clients
+/// quand un joueur ramasse un article (le client qui ramasse reorganise
+/// en local, les autres ne voient rien bouger).
+/// Fix : CmdReorganiserZoneLivraison envoie la reorganisation au serveur,
+/// qui modifie les positions des articles -> Mirror sync via NetworkTransform.
+///
+/// BUG 2 : Les equipements poses sur le chariot "volent" quand l'autre
+/// joueur pilote. Mirror ne sync pas le SetParent, donc seul le client qui
+/// a pose voit les equipements parentes au chariot. Les autres les voient
+/// a leur position monde figee au moment de la pose.
+/// Fix : CmdParenterAuChariot + RpcParenterAuChariot pour broadcaster le
+/// SetParent a tous les clients (chacun fait SetParent local). Idem pour
+/// le detachement au ramassage (CmdDetacherDuChariot + RpcDetacherDuChariot).
+///
+/// v6.12 : Transfert d'autorite pour le pilotage du chariot en multijoueur
+/// v6.11 : Pose/retrait des torons sur les axes lateraux du chariot
+/// v6.10 : Fix critique - objets qui flottent apres avoir ete poses sur le chariot
+/// v6.9 : Integration du systeme de pose plateau / etagere basse du Chariot v7.x
+/// - Detection des zones de pose via RaycastAll + tri par distance
+/// - Distinction automatique zonePosePlateau vs zonePoseEtagere
+/// - Affichage du fantome a la bonne position via AfficherFantomePose(tailleU, surEtagereBasse)
+/// - Cachage des fantomes quand on quitte le chariot du regard ou qu'on pose
+/// - Pose sur etagere basse via PoserEquipementSurEtagere
+/// - Ramassage depuis etagere basse (via equipementsSurEtagere.Contains)
+///
+/// v6.8 : Systeme d'assise sur les chaises
+/// v6.7 : Placement local client pour supports muraux
+/// v6.5 : Remise de PDU sur SupportPDU
+/// v6.4 : Portage toron "dans les mains"
+/// v6.3 : Placement precis toron sur axe cible
+/// v6.2 : Remise de toron sur SupportTorons
+///
+public class PlayerInteraction : NetworkBehaviour
+{
+ [Header("Interaction")]
+ public float porteeInteraction = 4f;
+ public KeyCode toucheInteraction = KeyCode.E;
+ public KeyCode toucheConfiguration = KeyCode.F;
+
+ [Header("Performance")]
+ [Range(1, 5)]
+ public int detectInterval = 2;
+ public bool detecterTriggers = false;
+
+ [Header("Transport")]
+ public float distanceTransport = 1.5f;
+ public float hauteurTransport = 0.6f;
+ public float decalageHorizontal = 0f;
+ public float vitesseSuivi = 15f;
+
+ [Header("Transport — Toron (v6.4)")]
+ public float distanceTransportToron = 0.85f;
+ public float hauteurTransportToron = 0.15f;
+ public float decalageHorizontalToron = 0.35f;
+ public Vector3 rotationEulerToron = new Vector3(0f, 90f, 0f);
+
+ [Header("Remise sur supports muraux (v6.2/6.5)")]
+ public float distanceDetectionSupport = 3.5f;
+ public float angleDetectionSupport = 35f;
+
+ private Camera _camera;
+ private Interactable _objetSurvole;
+ private Interactable _objetEnMain;
+ private Rigidbody _rbEnMain;
+ private Vector3 _cibleTransport;
+ private Quaternion _rotationOffsetTransport = Quaternion.identity;
+
+ private RackSlot _slotSurvole;
+ private Chariot _chariotPilote;
+ private Chariot _chariotVise;
+ private bool _visePoignee = false;
+ // v6.9 : distinction zone plateau/etagere du chariot vise
+ private bool _chariotZoneEtagereVisee = false;
+ // v6.11 : index de l'axe toron cible sur le chariot (0=Gauche, 1=Droit, -1=aucun)
+ private int _chariotAxeToronCibleIndex = -1;
+
+ private EmplacementBaie _emplacementVise;
+ private EmplacementPDU _emplacementPDUVise;
+ private PorteBaie _porteVisee;
+
+ private PriseC13 _priseSurvolee;
+ private BoutonPower _boutonPowerVise;
+
+ // v6.8 : système d'assise
+ private ChaiseSiege _chaiseSiegeVisee;
+ private ChaiseSiege _chaiseSiegeActuelle;
+
+ private List _collisionsIgnorees = new List();
+ private EmplacementBaie[] _tousEmplacements;
+ private bool _emplacementsVisibles = false;
+ private bool _emplacementsPDUVisibles = false;
+
+ private int _detectFrame = 0;
+
+ private MenuPause _cachedPause;
+ private MenuPrincipal _cachedMenu;
+ private bool _cacheInitialized = false;
+
+ // v6.2 : axe cible courant sur SupportTorons
+ private int _axeSupportCibleLigne = -1;
+ private int _axeSupportCibleColonne = -1;
+
+ // v6.5 : emplacement cible courant sur SupportPDU
+ private int _emplacementSupportPDUCibleLigne = -1;
+ private int _emplacementSupportPDUCibleColonne = -1;
+
+ [SyncVar(hook = nameof(OnObjetPorteChange))]
+ private uint _objetPorteNetId = 0;
+
+ private GameObject _objetPorteDistant;
+
+ public Interactable ObjetEnMain => _objetEnMain;
+
+ void Start()
+ {
+ _camera = GetComponentInChildren();
+ if (_camera == null) _camera = Camera.main;
+ }
+
+ private bool _popupEtaitOuvert = false;
+
+ void Update()
+ {
+ if (!isLocalPlayer) return;
+
+ // v6.8 : mode assis — seul [E] pour se lever est actif
+ if (_chaiseSiegeActuelle != null)
+ {
+ if (Input.GetKeyDown(toucheInteraction))
+ {
+ _chaiseSiegeActuelle.SeLever();
+ _chaiseSiegeActuelle = null;
+ HUDManager hud = GetComponent();
+ if (hud != null) hud.SetInfoEquipement("");
+ }
+ return;
+ }
+
+ if (_camera == null) _camera = Camera.main;
+
+ if (UIConfigurationEquipement.Instance != null && UIConfigurationEquipement.Instance.EstOuvert())
+ {
+ _popupEtaitOuvert = true;
+ return;
+ }
+
+ if (_popupEtaitOuvert)
+ {
+ _popupEtaitOuvert = false;
+ Cursor.lockState = CursorLockMode.Locked;
+ Cursor.visible = false;
+ var playerInput = GetComponent();
+ if (playerInput != null) playerInput.enabled = true;
+ var fpc = GetComponent();
+ if (fpc != null) fpc.enabled = true;
+ }
+
+ if (Cursor.visible && _objetEnMain == null)
+ {
+ if (!_cacheInitialized || _cachedPause == null) { _cachedPause = FindObjectOfType(); _cachedMenu = FindObjectOfType(); if (_cachedPause != null) _cacheInitialized = true; }
+ bool pauseOuverte = (_cachedPause != null && _cachedPause.estEnPause);
+ bool menuOuvert = (_cachedMenu != null && _cachedMenu.EstAffiche());
+ MonitoringDatacenter monitoring = FindObjectOfType();
+ bool monitoringOuvert = (monitoring != null && monitoring.EstAffiche());
+ PlanSalle plan = FindObjectOfType();
+ bool planOuvert = (plan != null && plan.EstOuvert());
+ UIBoutique boutique = FindObjectOfType();
+ bool boutiqueOuverte = (boutique != null && boutique.panneauPrincipal != null && boutique.panneauPrincipal.activeSelf);
+ UIConfigurationEquipement configPopup = UIConfigurationEquipement.Instance;
+ bool configOuverte = (configPopup != null && configPopup.EstOuvert());
+ UITickets tickets = UITickets.Instance;
+ bool ticketsOuverts = (tickets != null && tickets.EstAffiche());
+
+ if (!pauseOuverte && !menuOuvert && !monitoringOuvert && !planOuvert && !boutiqueOuverte && !configOuverte && !ticketsOuverts)
+ {
+ Cursor.lockState = CursorLockMode.Locked;
+ Cursor.visible = false;
+ var fpc2 = GetComponent();
+ if (fpc2 != null) fpc2.enabled = true;
+ var pi2 = GetComponent();
+ if (pi2 != null) pi2.enabled = true;
+ }
+ }
+
+ if (_objetEnMain == null)
+ {
+ if (Time.frameCount % detectInterval == 0)
+ DetecterObjet();
+ if (_emplacementsVisibles) { SetEmplacementsVisibles(false); _emplacementsVisibles = false; }
+ if (_emplacementsPDUVisibles) { SetEmplacementsPDUVisibles(false); _emplacementsPDUVisibles = false; }
+ if (_axeSupportCibleLigne >= 0) QuitterSurvolSupport();
+ if (_emplacementSupportPDUCibleLigne >= 0) QuitterSurvolSupportPDU();
+ // v6.9 : cacher les fantomes du chariot quand on n'a plus rien en main
+ CacherFantomesChariotTous();
+ }
+ else
+ {
+ DetecterCiblePose();
+ bool portePDU = _objetEnMain.GetComponent() != null;
+ if (portePDU && !_emplacementsPDUVisibles) { SetEmplacementsPDUVisibles(true); _emplacementsPDUVisibles = true; }
+ if (!portePDU && _emplacementsPDUVisibles) { SetEmplacementsPDUVisibles(false); _emplacementsPDUVisibles = false; }
+ if (_emplacementsVisibles) { SetEmplacementsVisibles(false); _emplacementsVisibles = false; }
+ }
+
+ if (Input.GetKeyDown(toucheInteraction))
+ {
+ // v6.8 : s'asseoir si on vise une chaise et pas d'objet en main
+ if (_chaiseSiegeVisee != null && _objetEnMain == null)
+ {
+ if (_chaiseSiegeVisee.Asseoir(gameObject))
+ {
+ _chaiseSiegeActuelle = _chaiseSiegeVisee;
+ _chaiseSiegeVisee = null;
+ HUDManager hud = GetComponent();
+ if (hud != null) hud.SetInfoEquipement("[E] Se lever");
+ }
+ return;
+ }
+
+ if (_boutonPowerVise != null && _objetEnMain == null) { CmdAppuyerBoutonPower(_boutonPowerVise.GetComponentInParent().netId); return; }
+ if (_priseSurvolee != null && _priseSurvolee.estConnectee && _objetEnMain == null) { CmdBasculerInterrupteur(_priseSurvolee.GetComponentInParent().netId); return; }
+ if (_porteVisee != null && _objetEnMain == null)
+ {
+ NetworkIdentity porteNetId = _porteVisee.GetComponentInParent();
+ if (porteNetId != null) CmdTogglePorte(porteNetId.netId, _porteVisee.gameObject.name);
+ return;
+ }
+
+ if (_chariotPilote != null && _objetEnMain == null && _visePoignee)
+ {
+ LacherChariot();
+ QuitterSurvol();
+ return;
+ }
+
+ if (_objetEnMain == null && _objetSurvole != null) Ramasser();
+ else if (_objetEnMain != null) Poser();
+ }
+
+ if (Input.GetKeyDown(toucheConfiguration) && _objetEnMain == null && _objetSurvole != null)
+ {
+ RackSlot slotConfig = TrouverSlotDeEquipement(_objetSurvole);
+ if (slotConfig != null)
+ {
+ ConfigurationEquipement config = _objetSurvole.GetComponent();
+ if (config == null) config = _objetSurvole.gameObject.AddComponent();
+ if (UIConfigurationEquipement.Instance != null)
+ { UIConfigurationEquipement.Instance.Ouvrir(config, premiereOuverture: false); QuitterSurvol(); }
+ }
+ }
+
+ if (Input.GetKeyDown(KeyCode.F5) && isServer && SaveManager.Instance != null)
+ {
+ if (SaveManager.Instance.SauvegardeRapide())
+ Debug.Log("[QuickSave] Sauvegarde rapide effectuée !");
+ }
+ }
+
+ void FixedUpdate()
+ {
+ if (!isLocalPlayer) return;
+
+ if (_objetEnMain != null && _rbEnMain != null)
+ {
+ Vector3 dir = new Vector3(_camera.transform.forward.x, 0, _camera.transform.forward.z).normalized;
+
+ bool estToron = _objetEnMain.GetComponent() != null;
+ float dist = estToron ? distanceTransportToron : distanceTransport;
+ float hauteur = estToron ? hauteurTransportToron : hauteurTransport;
+ float decalage = estToron ? decalageHorizontalToron : decalageHorizontal;
+
+ _cibleTransport = transform.position + dir * dist
+ + Vector3.up * hauteur + _camera.transform.right * decalage;
+ Vector3 direction = _cibleTransport - _rbEnMain.position;
+ Vector3 desiredVelocity = direction * vitesseSuivi;
+ if (direction.magnitude > 0.05f)
+ {
+ RaycastHit hit;
+ int excludeMask = ~LayerMask.GetMask("Joueur", "Equipement", "Sol", "Baie", "Default", "Emplacement");
+ if (Physics.SphereCast(_rbEnMain.position, 0.15f, direction.normalized, out hit, direction.magnitude, excludeMask))
+ if (!EstColliderIgnore(hit.collider) && hit.distance < 0.15f)
+ desiredVelocity = Vector3.ProjectOnPlane(desiredVelocity, hit.normal);
+ }
+ _rbEnMain.velocity = desiredVelocity;
+
+ if (estToron)
+ _rbEnMain.rotation = Quaternion.LookRotation(dir) * Quaternion.Euler(rotationEulerToron);
+ else
+ _rbEnMain.rotation = Quaternion.LookRotation(dir) * _rotationOffsetTransport;
+
+ CmdUpdateTransportPosition(_rbEnMain.position, _rbEnMain.rotation);
+ }
+ }
+
+ // ══════════════════════════════════════════════════════════
+ // COMMANDS
+ // ══════════════════════════════════════════════════════════
+
+ [Command]
+ void CmdRamasser(uint objetNetId)
+ {
+ if (!NetworkServer.spawned.ContainsKey(objetNetId)) return;
+ _objetPorteNetId = objetNetId;
+ var obj = NetworkServer.spawned[objetNetId];
+ Rigidbody rb = obj.GetComponent();
+ if (rb != null) { rb.isKinematic = false; rb.useGravity = false; rb.freezeRotation = true; }
+ RpcOnRamasser(objetNetId);
+ }
+
+ [Command]
+ void CmdPoser(uint objetNetId, Vector3 position, Quaternion rotation, bool avecGravite, bool estRacke)
+ {
+ if (!NetworkServer.spawned.ContainsKey(objetNetId)) return;
+ _objetPorteNetId = 0;
+ var obj = NetworkServer.spawned[objetNetId];
+ obj.transform.position = position;
+ obj.transform.rotation = rotation;
+ Rigidbody rb = obj.GetComponent();
+ if (rb != null)
+ {
+ if (estRacke) { if (!rb.isKinematic) { rb.velocity = Vector3.zero; rb.angularVelocity = Vector3.zero; } rb.isKinematic = true; }
+ else { rb.isKinematic = false; rb.useGravity = avecGravite; rb.velocity = Vector3.zero; rb.freezeRotation = false; rb.interpolation = RigidbodyInterpolation.None; }
+ }
+ RpcOnPoser(objetNetId, position, rotation, avecGravite, estRacke);
+ }
+
+ [Command]
+ void CmdPoserToronSurSupport(uint toronNetId, int ligne, int colonne)
+ {
+ if (!NetworkServer.spawned.ContainsKey(toronNetId)) return;
+ _objetPorteNetId = 0;
+ var obj = NetworkServer.spawned[toronNetId];
+ if (obj == null) return;
+ bool place = false;
+ if (SupportTorons.Instance != null)
+ {
+ if (ligne >= 0 && colonne >= 0) place = SupportTorons.Instance.PoserToronSurAxeSpecifique(obj.gameObject, ligne, colonne);
+ if (!place) place = SupportTorons.Instance.PoserToron(obj.gameObject);
+ }
+ if (place) { RpcOnPoser(toronNetId, obj.transform.position, obj.transform.rotation, false, true); }
+ else { Rigidbody rb = obj.GetComponent(); if (rb != null) { rb.isKinematic = false; rb.useGravity = true; rb.velocity = Vector3.zero; } RpcOnPoser(toronNetId, obj.transform.position, obj.transform.rotation, true, false); }
+ }
+
+ [Command]
+ void CmdPoserPDUSurSupport(uint pduNetId, int ligne, int colonne)
+ {
+ if (!NetworkServer.spawned.ContainsKey(pduNetId)) return;
+ _objetPorteNetId = 0;
+ var obj = NetworkServer.spawned[pduNetId];
+ if (obj == null) return;
+ bool place = false;
+ if (SupportPDU.Instance != null)
+ {
+ if (ligne >= 0 && colonne >= 0) place = SupportPDU.Instance.PoserPDUSurEmplacementSpecifique(obj.gameObject, ligne, colonne);
+ if (!place) place = SupportPDU.Instance.PoserPDU(obj.gameObject);
+ }
+ if (place) { RpcOnPoser(pduNetId, obj.transform.position, obj.transform.rotation, false, true); }
+ else { Rigidbody rb = obj.GetComponent(); if (rb != null) { rb.isKinematic = false; rb.useGravity = true; rb.velocity = Vector3.zero; } RpcOnPoser(pduNetId, obj.transform.position, obj.transform.rotation, true, false); }
+ }
+
+ [Command(channel = Channels.Unreliable)]
+ void CmdUpdateTransportPosition(Vector3 position, Quaternion rotation)
+ {
+ // v7.1 : bouger l'objet COTE SERVEUR pour que NetworkTransform Server-to-Client
+ // propage aux autres clients. On garde le Rpc en complement pour un rendu
+ // smooth Lerp cote clients observateurs (latence masquee).
+ if (_objetPorteNetId != 0 && NetworkServer.spawned.ContainsKey(_objetPorteNetId))
+ {
+ var obj = NetworkServer.spawned[_objetPorteNetId];
+ if (obj != null)
+ {
+ obj.transform.position = position;
+ obj.transform.rotation = rotation;
+ }
+ }
+ RpcUpdateTransportPosition(position, rotation);
+ }
+
+ [Command]
+ void CmdAppuyerBoutonPower(uint equipNetId)
+ {
+ if (!NetworkServer.spawned.ContainsKey(equipNetId)) return;
+ var obj = NetworkServer.spawned[equipNetId];
+ BoutonPower bouton = obj.GetComponentInChildren();
+ if (bouton != null)
+ {
+ bouton.Appuyer();
+ RpcSyncBoutonPower(equipNetId, bouton.estAllume);
+
+ // v7.1 : propager l'etat au EtatEquipementNetworkSync pour le rejoin
+ EtatEquipementNetworkSync sync = obj.GetComponent();
+ if (sync != null) sync.ServerSyncEtat();
+ }
+ }
+
+ [ClientRpc]
+ void RpcSyncBoutonPower(uint equipNetId, bool estAllume)
+ {
+ if (!NetworkClient.spawned.ContainsKey(equipNetId)) return;
+ var obj = NetworkClient.spawned[equipNetId];
+ BoutonPower bouton = obj.GetComponentInChildren();
+ if (bouton != null && bouton.estAllume != estAllume) bouton.Appuyer();
+ }
+
+ [Command]
+ void CmdBasculerInterrupteur(uint pduNetId)
+ {
+ if (!NetworkServer.spawned.ContainsKey(pduNetId)) return;
+ var obj = NetworkServer.spawned[pduNetId];
+ PriseC13 prise = obj.GetComponentInChildren();
+ if (prise != null) prise.BasculerInterrupteur();
+ }
+
+ [Command]
+ void CmdInstallerPDU(uint pduNetId, Vector3 position, Quaternion rotation)
+ {
+ if (!NetworkServer.spawned.ContainsKey(pduNetId)) return;
+ var obj = NetworkServer.spawned[pduNetId];
+ PDU pdu = obj.GetComponent();
+ if (pdu == null) return;
+ PDUNetworkState netState = obj.GetComponent();
+ if (netState != null) netState.ServerSetInstalle(true, (int)pdu.coteInstallation);
+ else pdu.estInstalle = true;
+ RpcInstallerPDU(pduNetId, position, rotation);
+ }
+
+ [ClientRpc]
+ void RpcInstallerPDU(uint pduNetId, Vector3 position, Quaternion rotation)
+ {
+ if (!NetworkClient.spawned.ContainsKey(pduNetId)) return;
+ var obj = NetworkClient.spawned[pduNetId];
+ PDU pdu = obj.GetComponent();
+ if (pdu == null) return;
+ pdu.estInstalle = true;
+ if (!isLocalPlayer) { obj.transform.position = position; obj.transform.rotation = rotation; Rigidbody rb = obj.GetComponent(); if (rb != null) { rb.isKinematic = true; rb.useGravity = false; } }
+ }
+
+ [Command]
+ void CmdDesinstallerPDU(uint pduNetId)
+ {
+ if (!NetworkServer.spawned.ContainsKey(pduNetId)) return;
+ var obj = NetworkServer.spawned[pduNetId];
+ PDU pdu = obj.GetComponent();
+ if (pdu == null) return;
+ PDUNetworkState netState = obj.GetComponent();
+ if (netState != null) netState.ServerSetInstalle(false);
+ else pdu.estInstalle = false;
+ RpcDesinstallerPDU(pduNetId);
+ }
+
+ [ClientRpc]
+ void RpcDesinstallerPDU(uint pduNetId)
+ {
+ if (!NetworkClient.spawned.ContainsKey(pduNetId)) return;
+ var obj = NetworkClient.spawned[pduNetId];
+ PDU pdu = obj.GetComponent();
+ if (pdu != null) { pdu.estInstalle = false; Rigidbody rb = obj.GetComponent(); if (rb != null) { rb.isKinematic = false; rb.useGravity = true; } }
+ }
+
+ [Command]
+ void CmdInstallerDansSlot(uint equipNetId, uint baieNetId, int slotIndex)
+ {
+ if (!NetworkServer.spawned.ContainsKey(equipNetId) || !NetworkServer.spawned.ContainsKey(baieNetId)) return;
+ var equipObj = NetworkServer.spawned[equipNetId]; var baieObj = NetworkServer.spawned[baieNetId];
+ RackSlot[] slots = baieObj.GetComponentsInChildren();
+ if (slotIndex >= 0 && slotIndex < slots.Length) { RackSlot slot = slots[slotIndex]; if (!slot.estOccupe) { slot.estOccupe = true; slot.equipementInstalle = equipObj.GetComponent(); } }
+ RpcInstallerDansSlot(equipNetId, baieNetId, slotIndex);
+ }
+
+ [ClientRpc]
+ void RpcInstallerDansSlot(uint equipNetId, uint baieNetId, int slotIndex)
+ {
+ if (isLocalPlayer) return;
+ if (!NetworkClient.spawned.ContainsKey(equipNetId) || !NetworkClient.spawned.ContainsKey(baieNetId)) return;
+ var equipObj = NetworkClient.spawned[equipNetId]; var baieObj = NetworkClient.spawned[baieNetId];
+ RackSlot[] slots = baieObj.GetComponentsInChildren();
+ if (slotIndex >= 0 && slotIndex < slots.Length) { RackSlot slot = slots[slotIndex]; if (!slot.estOccupe) { slot.estOccupe = true; slot.equipementInstalle = equipObj.GetComponent(); } }
+ }
+
+ [Command]
+ void CmdTogglePorte(uint baieNetId, string nomPorte)
+ {
+ if (!NetworkServer.spawned.ContainsKey(baieNetId)) return;
+ var obj = NetworkServer.spawned[baieNetId];
+ foreach (PorteBaie porte in obj.GetComponentsInChildren())
+ if (porte.gameObject.name == nomPorte) { porte.TogglePorte(); RpcTogglePorte(baieNetId, nomPorte); break; }
+ }
+
+ // ══════════════════════════════════════════════════════════
+ // CLIENT RPCs
+ // ══════════════════════════════════════════════════════════
+
+ [ClientRpc]
+ void RpcOnRamasser(uint objetNetId)
+ {
+ if (isLocalPlayer) return;
+ if (!NetworkClient.spawned.ContainsKey(objetNetId)) return;
+ var obj = NetworkClient.spawned[objetNetId];
+ _objetPorteDistant = obj.gameObject;
+ Rigidbody rb = obj.GetComponent();
+ if (rb != null) { rb.isKinematic = true; rb.velocity = Vector3.zero; }
+ Interactable inter = obj.GetComponent();
+ if (inter != null) inter.estRamasse = true;
+ }
+
+ [ClientRpc]
+ void RpcOnPoser(uint objetNetId, Vector3 position, Quaternion rotation, bool avecGravite, bool estRacke)
+ {
+ if (isLocalPlayer) return;
+ if (!NetworkClient.spawned.ContainsKey(objetNetId)) return;
+ var obj = NetworkClient.spawned[objetNetId];
+ obj.transform.position = position; obj.transform.rotation = rotation;
+ Rigidbody rb = obj.GetComponent();
+ if (rb != null)
+ {
+ if (estRacke) { if (!rb.isKinematic) { rb.velocity = Vector3.zero; rb.angularVelocity = Vector3.zero; } rb.isKinematic = true; }
+ else { rb.isKinematic = false; rb.useGravity = avecGravite; rb.velocity = Vector3.zero; }
+ }
+ Interactable inter = obj.GetComponent();
+ if (inter != null) inter.estRamasse = false;
+ _objetPorteDistant = null;
+ }
+
+ [ClientRpc(channel = Channels.Unreliable)]
+ void RpcUpdateTransportPosition(Vector3 position, Quaternion rotation)
+ {
+ if (isLocalPlayer) return;
+ if (_objetPorteDistant != null)
+ {
+ _objetPorteDistant.transform.position = Vector3.Lerp(_objetPorteDistant.transform.position, position, Time.deltaTime * 20f);
+ _objetPorteDistant.transform.rotation = Quaternion.Slerp(_objetPorteDistant.transform.rotation, rotation, Time.deltaTime * 20f);
+ }
+ }
+
+ [ClientRpc]
+ void RpcTogglePorte(uint baieNetId, string nomPorte)
+ {
+ if (isServer) return;
+ if (!NetworkClient.spawned.ContainsKey(baieNetId)) return;
+ var obj = NetworkClient.spawned[baieNetId];
+ foreach (PorteBaie porte in obj.GetComponentsInChildren())
+ if (porte.gameObject.name == nomPorte) { porte.TogglePorte(); break; }
+ }
+
+ // ══════════════════════════════════════════════════════════
+ // HOOK SyncVar
+ // ══════════════════════════════════════════════════════════
+
+ void OnObjetPorteChange(uint ancienId, uint nouveauId)
+ {
+ if (isLocalPlayer) return;
+ if (nouveauId == 0) _objetPorteDistant = null;
+ else if (NetworkClient.spawned.ContainsKey(nouveauId)) _objetPorteDistant = NetworkClient.spawned[nouveauId].gameObject;
+ }
+
+ // ══════════════════════════════════════════════════════════
+ // HELPERS LOCAUX
+ // ══════════════════════════════════════════════════════════
+
+ private bool EstColliderIgnore(Collider col) { for (int i = 0; i < _collisionsIgnorees.Count; i++) if (_collisionsIgnorees[i] == col) return true; return false; }
+
+ void SetEmplacementsVisibles(bool visible)
+ {
+ if (_tousEmplacements == null || _tousEmplacements.Length == 0) _tousEmplacements = FindObjectsOfType();
+ foreach (EmplacementBaie empl in _tousEmplacements) if (empl != null && !empl.estOccupe) empl.SetModeVisible(visible);
+ }
+
+ void SetEmplacementsPDUVisibles(bool visible)
+ {
+ foreach (EmplacementPDU empl in FindObjectsOfType()) if (empl != null && !empl.estOccupe) empl.SetModeVisible(visible);
+ }
+
+ // v6.9 : helper pour cacher les fantomes du chariot
+ void CacherFantomesChariotTous()
+ {
+ if (_chariotVise != null) { _chariotVise.CacherFantomes(); }
+ if (_chariotPilote != null) { _chariotPilote.CacherFantomes(); }
+ // v6.11 : CacherFantomes() inclut deja le fantome toron axe
+ // (cf. Chariot.CacherFantomes v7.3) mais on le reappelle explicitement ici
+ // au cas ou la version du Chariot n'aurait pas encore ete mise a jour
+ if (_chariotVise != null) _chariotVise.CacherFantomeToronAxe();
+ if (_chariotPilote != null) _chariotPilote.CacherFantomeToronAxe();
+ }
+
+ // ==================== DETECTION OBJET ====================
+
+ void DetecterObjet()
+ {
+ QuitterSurvolSlot(); QuitterSurvolEmplacement(); QuitterSurvolEmplacementPDU();
+ QuitterSurvolPrise(); QuitterSurvolBoutonPower();
+ _chariotVise = null; _visePoignee = false; _porteVisee = null;
+
+ if (_camera == null) return;
+
+ Ray rayon = new Ray(_camera.transform.position, _camera.transform.forward);
+ QueryTriggerInteraction triggerMode = detecterTriggers ? QueryTriggerInteraction.Collide : QueryTriggerInteraction.Ignore;
+
+ int maskObjets = ~LayerMask.GetMask("Joueur", "Baie");
+ RaycastHit hitSimple;
+ if (Physics.Raycast(rayon, out hitSimple, porteeInteraction, maskObjets, triggerMode))
+ {
+ if (!EstColliderIgnore(hitSimple.collider))
+ if (TraiterHitDetection(hitSimple)) return;
+ }
+
+ int maskBaie = LayerMask.GetMask("Baie");
+ if (Physics.Raycast(rayon, out hitSimple, porteeInteraction, maskBaie, triggerMode))
+ {
+ if (!EstColliderIgnore(hitSimple.collider))
+ if (TraiterHitBaie(hitSimple)) return;
+ }
+
+ QuitterSurvol();
+ }
+
+ bool TraiterHitDetection(RaycastHit impact)
+ {
+ // v6.8 : chaise pour s'asseoir
+ ChaiseSiege chaise = impact.collider.GetComponentInParent();
+ if (chaise != null && !chaise.estOccupee && _objetEnMain == null)
+ {
+ _chaiseSiegeVisee = chaise;
+ HUDManager hud = GetComponent();
+ if (hud != null) hud.SetInfoEquipement("[E] S'asseoir");
+ QuitterSurvol();
+ return true;
+ }
+ if (_chaiseSiegeVisee != null && chaise == null)
+ _chaiseSiegeVisee = null;
+
+ BoutonPower bouton = impact.collider.GetComponent();
+ if (bouton != null) { _boutonPowerVise = bouton; bouton.Survoler(true); return true; }
+
+ PriseC13 prise = impact.collider.GetComponent();
+ if (prise != null) { _priseSurvolee = prise; prise.Survoler(true); return true; }
+
+ PorteBaie porte = impact.collider.GetComponentInParent();
+ if (porte != null) { _porteVisee = porte; QuitterSurvol(); return true; }
+
+ Chariot chariotHit = impact.collider.GetComponentInParent();
+ if (chariotHit != null && impact.collider.gameObject.name == "Ch_PoigneeZone")
+ {
+ _visePoignee = true;
+ Interactable ci = chariotHit.GetComponent();
+ if (ci != null) SetSurvol(ci);
+ return true;
+ }
+
+ MonitoringDatacenter monitoring = impact.collider.GetComponent();
+ if (monitoring != null) { Interactable interMon = impact.collider.GetComponent(); if (interMon != null) SetSurvol(interMon); return true; }
+
+ PDU pduVise = impact.collider.GetComponentInParent();
+ if (pduVise != null && pduVise.estInstalle) { Interactable interPDU = pduVise.GetComponent(); if (interPDU != null) { SetSurvol(interPDU); return true; } }
+
+ Interactable obj = impact.collider.GetComponentInParent();
+ if (obj != null)
+ {
+ if (obj.GetComponent() != null && !_visePoignee) { QuitterSurvol(); return true; }
+ SetSurvol(obj); return true;
+ }
+
+ return false;
+ }
+
+ bool TraiterHitBaie(RaycastHit impact)
+ {
+ if (impact.collider.gameObject.name == "BaieZoneTrigger") return false;
+ PorteBaie porte = impact.collider.GetComponent();
+ if (porte != null) { _porteVisee = porte; QuitterSurvol(); return true; }
+ RackSlot slot = impact.collider.GetComponent();
+ if (slot != null && slot.estOccupe && slot.equipementInstalle != null)
+ {
+ if (!PorteAvantOuverte(slot)) return false;
+ BoutonPower boutonRack = slot.equipementInstalle.GetComponentInChildren();
+ if (boutonRack != null && ViseBouton(boutonRack.transform.position)) { _boutonPowerVise = boutonRack; boutonRack.Survoler(true); return true; }
+ SetSurvol(slot.equipementInstalle); return true;
+ }
+ EmplacementPDU emplPDU = impact.collider.GetComponent();
+ if (emplPDU != null) return false;
+ return false;
+ }
+
+ // ==================== DETECTION CIBLE POSE ====================
+
+ void DetecterCiblePose()
+ {
+ _porteVisee = null;
+ QuitterSurvolSlot(); QuitterSurvolEmplacement(); QuitterSurvolEmplacementPDU();
+
+ if (_camera == null) return;
+
+ if (DetecterCibleSupportTorons()) return;
+ QuitterSurvolSupport();
+
+ if (DetecterCibleSupportPDU()) return;
+ QuitterSurvolSupportPDU();
+
+ Ray rayon = new Ray(_camera.transform.position, _camera.transform.forward);
+ bool portePDU = _objetEnMain != null && _objetEnMain.GetComponent() != null;
+
+ List colsPortes = new List();
+ if (_objetEnMain != null)
+ foreach (Collider c in _objetEnMain.GetComponentsInChildren())
+ if (c.enabled) { c.enabled = false; colsPortes.Add(c); }
+
+ int slotMask = LayerMask.GetMask("Baie", "Sol", "Emplacement");
+ RaycastHit[] hits = Physics.RaycastAll(rayon, porteeInteraction, slotMask, QueryTriggerInteraction.Collide);
+ System.Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance));
+ foreach (Collider c in colsPortes) c.enabled = true;
+
+ RackSlot slotTouche = null; EmplacementBaie emplTouche = null; EmplacementPDU emplPDUTouche = null;
+
+ foreach (var hit in hits)
+ {
+ if (hit.collider.gameObject.name == "BaieZoneTrigger") continue;
+ if (portePDU) { EmplacementPDU ep = hit.collider.GetComponent(); if (ep != null && !ep.estOccupe) { emplPDUTouche = ep; break; } continue; }
+ RackSlot s = hit.collider.GetComponent(); if (s != null) { slotTouche = s; break; }
+ EmplacementBaie e = hit.collider.GetComponent(); if (e != null) { emplTouche = e; break; }
+ }
+
+ if (emplPDUTouche != null)
+ {
+ if (_emplacementPDUVise != emplPDUTouche) { QuitterSurvolEmplacementPDU(); _emplacementPDUVise = emplPDUTouche; _emplacementPDUVise.SurvolDebut(); }
+ HUDManager hud = GetComponent();
+ if (hud != null) hud.SetInfoEquipement("[E] Installer PDU (" + (_emplacementPDUVise.cote == EmplacementPDU.CotePDU.Gauche ? "Gauche" : "Droit") + ")");
+ // v6.9 : on a trouve une cible non-chariot -> cacher les fantomes
+ CacherFantomesChariotTous();
+ _chariotVise = null; _chariotZoneEtagereVisee = false;
+ return;
+ }
+
+ if (slotTouche != null)
+ {
+ bool porteOuverte = PorteAvantOuverte(slotTouche);
+ if (_slotSurvole != slotTouche) { QuitterSurvolSlot(); _slotSurvole = slotTouche; if (!slotTouche.estOccupe && porteOuverte) _slotSurvole.SurvolDebut(); }
+ if (!slotTouche.estOccupe) { HUDManager hud = GetComponent(); if (hud != null) hud.SetInfoEquipement(porteOuverte ? "U" + slotTouche.numeroSlot.ToString("D2") + " - [E] Installer ici" : "Ouvrir la porte avant d'abord"); }
+ CacherFantomesChariotTous();
+ _chariotVise = null; _chariotZoneEtagereVisee = false;
+ return;
+ }
+
+ if (emplTouche != null && !emplTouche.estOccupe)
+ { if (_emplacementVise != emplTouche) { QuitterSurvolEmplacement(); _emplacementVise = emplTouche; _emplacementVise.SurvolDebut(); } CacherFantomesChariotTous(); _chariotVise = null; _chariotZoneEtagereVisee = false; return; }
+
+ HUDManager hudClear = GetComponent(); if (hudClear != null && _slotSurvole == null) hudClear.SetInfoEquipement("");
+
+ // ───── v6.11 : Detection zones de pose du chariot (plateau, etagere, axes toron) ─────
+ Chariot ancienChariot = _chariotVise;
+ _chariotVise = null;
+ _chariotZoneEtagereVisee = false;
+ _chariotAxeToronCibleIndex = -1;
+
+ bool porteToron = (_objetEnMain != null && _objetEnMain.GetComponent() != null);
+
+ int maskChariot = LayerMask.GetMask("Chariot");
+ RaycastHit[] hitsChariot;
+ if (maskChariot != 0)
+ hitsChariot = Physics.RaycastAll(rayon, porteeInteraction, maskChariot, QueryTriggerInteraction.Collide);
+ else
+ hitsChariot = Physics.RaycastAll(rayon, porteeInteraction, ~LayerMask.GetMask("Joueur", "Equipement", "Sol", "Baie", "Emplacement"), QueryTriggerInteraction.Collide);
+
+ System.Array.Sort(hitsChariot, (a, b) => a.distance.CompareTo(b.distance));
+
+ Chariot chariotHit = null;
+ int axeToronIdx = -1;
+ bool zoneTrouvee = false;
+
+ foreach (var h in hitsChariot)
+ {
+ Chariot c = h.collider.GetComponentInParent();
+ if (c == null) continue;
+
+ if (porteToron)
+ {
+ // Porte un toron : priorite aux axes toron, on ignore plateau/etagere
+ if (c.zonesToronAxes != null)
+ {
+ int iFound = -1;
+ for (int i = 0; i < c.zonesToronAxes.Length; i++)
+ {
+ if (c.zonesToronAxes[i] != null && h.collider.gameObject == c.zonesToronAxes[i])
+ {
+ // Verifier que l'axe est libre
+ bool libre = (c.equipementsToronAxes == null
+ || i >= c.equipementsToronAxes.Count
+ || c.equipementsToronAxes[i] == null);
+ if (libre) { iFound = i; break; }
+ }
+ }
+ if (iFound >= 0)
+ {
+ chariotHit = c;
+ axeToronIdx = iFound;
+ zoneTrouvee = true;
+ break;
+ }
+ }
+ }
+ else
+ {
+ // Equipement normal : zones plateau/etagere
+ bool estZonePlateau = (c.zonePosePlateau != null && h.collider.gameObject == c.zonePosePlateau);
+ bool estZoneEtagere = (c.zonePoseEtagere != null && h.collider.gameObject == c.zonePoseEtagere);
+
+ if (estZonePlateau || estZoneEtagere)
+ {
+ chariotHit = c;
+ _chariotZoneEtagereVisee = estZoneEtagere;
+ zoneTrouvee = true;
+ break;
+ }
+ }
+
+ // Fallback : garder le chariot comme candidat si pas encore trouve de zone
+ if (chariotHit == null) chariotHit = c;
+ }
+
+ if (chariotHit != null)
+ {
+ _chariotVise = chariotHit;
+
+ if (ancienChariot != null && ancienChariot != chariotHit)
+ {
+ ancienChariot.CacherFantomes();
+ ancienChariot.CacherFantomeToronAxe();
+ }
+
+ if (porteToron && zoneTrouvee && axeToronIdx >= 0)
+ {
+ _chariotAxeToronCibleIndex = axeToronIdx;
+ chariotHit.AfficherFantomeToronAxe(axeToronIdx);
+ HUDManager hud = GetComponent();
+ if (hud != null)
+ hud.SetInfoEquipement(axeToronIdx == 0
+ ? "[E] Accrocher le toron sur l'axe gauche"
+ : "[E] Accrocher le toron sur l'axe droit");
+ }
+ else if (!porteToron && zoneTrouvee)
+ {
+ int tailleU = (_objetEnMain != null && _objetEnMain.tailleU > 0) ? _objetEnMain.tailleU : 1;
+ chariotHit.AfficherFantomePose(tailleU, _chariotZoneEtagereVisee);
+ HUDManager hud = GetComponent();
+ if (hud != null)
+ hud.SetInfoEquipement(_chariotZoneEtagereVisee ? "[E] Poser sur l'etagere basse" : "[E] Poser sur le plateau");
+ }
+ else
+ {
+ // Chariot vise mais pas de zone specifique : cacher les fantomes
+ chariotHit.CacherFantomes();
+ chariotHit.CacherFantomeToronAxe();
+ }
+ }
+ else
+ {
+ if (ancienChariot != null)
+ {
+ ancienChariot.CacherFantomes();
+ ancienChariot.CacherFantomeToronAxe();
+ }
+ }
+ }
+
+ // ==================== DÉTECTION SUPPORTS MURAUX ====================
+
+ bool DetecterCibleSupportTorons()
+ {
+ if (_objetEnMain == null) return false;
+ if (_objetEnMain.GetComponent() == null) return false;
+ SupportTorons support = SupportTorons.Instance;
+ if (support == null) return false;
+ float dist = Vector3.Distance(transform.position, support.transform.position);
+ if (dist > distanceDetectionSupport) return false;
+ Vector3 dirSupport = (support.transform.position - _camera.transform.position);
+ float distCam = dirSupport.magnitude;
+ if (distCam < 0.01f) return false;
+ dirSupport /= distCam;
+ float angle = Vector3.Angle(_camera.transform.forward, dirSupport);
+ if (angle > angleDetectionSupport) return false;
+ int r, c; Vector3 posAxe;
+ if (!support.TrouverAxeLibreLePlusProche(transform.position, out r, out c, out posAxe)) return false;
+ support.AfficherSurvolAxe(r, c);
+ _axeSupportCibleLigne = r; _axeSupportCibleColonne = c;
+ HUDManager hud = GetComponent();
+ if (hud != null) hud.SetInfoEquipement("[E] Remettre le toron sur le support");
+ return true;
+ }
+
+ void QuitterSurvolSupport()
+ {
+ if (_axeSupportCibleLigne < 0) return;
+ if (SupportTorons.Instance != null) SupportTorons.Instance.CacherSurvolAxe();
+ _axeSupportCibleLigne = -1; _axeSupportCibleColonne = -1;
+ }
+
+ bool DetecterCibleSupportPDU()
+ {
+ if (_objetEnMain == null) return false;
+ PDU pdu = _objetEnMain.GetComponent();
+ if (pdu == null) return false;
+ if (pdu.estInstalle) return false;
+ SupportPDU support = SupportPDU.Instance;
+ if (support == null) return false;
+ float dist = Vector3.Distance(transform.position, support.transform.position);
+ if (dist > distanceDetectionSupport) return false;
+ Vector3 dirSupport = (support.transform.position - _camera.transform.position);
+ float distCam = dirSupport.magnitude;
+ if (distCam < 0.01f) return false;
+ dirSupport /= distCam;
+ float angle = Vector3.Angle(_camera.transform.forward, dirSupport);
+ if (angle > angleDetectionSupport) return false;
+ int r, c; Vector3 posEmpl;
+ if (!support.TrouverEmplacementLibreLePlusProche(transform.position, out r, out c, out posEmpl)) return false;
+ support.AfficherSurvolEmplacement(r, c);
+ _emplacementSupportPDUCibleLigne = r; _emplacementSupportPDUCibleColonne = c;
+ HUDManager hud = GetComponent();
+ if (hud != null) hud.SetInfoEquipement("[E] Remettre le PDU sur le support");
+ return true;
+ }
+
+ void QuitterSurvolSupportPDU()
+ {
+ if (_emplacementSupportPDUCibleLigne < 0) return;
+ if (SupportPDU.Instance != null) SupportPDU.Instance.CacherSurvolEmplacement();
+ _emplacementSupportPDUCibleLigne = -1; _emplacementSupportPDUCibleColonne = -1;
+ }
+
+ // ==================== SURVOL HELPERS ====================
+
+ void SetSurvol(Interactable obj) { if (_objetSurvole != obj) { if (_objetSurvole != null) _objetSurvole.SurvoleeFin(); _objetSurvole = obj; _objetSurvole.SurvoleDebut(); } }
+ void QuitterSurvol() { if (_objetSurvole != null) { _objetSurvole.SurvoleeFin(); _objetSurvole = null; } }
+ void QuitterSurvolSlot() { if (_slotSurvole != null) { _slotSurvole.SurvolFin(); _slotSurvole = null; } }
+ void QuitterSurvolEmplacement() { if (_emplacementVise != null) { _emplacementVise.SurvolFin(); _emplacementVise = null; } }
+ void QuitterSurvolEmplacementPDU() { if (_emplacementPDUVise != null) { _emplacementPDUVise.SurvolFin(); _emplacementPDUVise = null; } }
+ void QuitterSurvolPrise() { if (_priseSurvolee != null) { _priseSurvolee.Survoler(false); _priseSurvolee = null; } }
+ void QuitterSurvolBoutonPower() { if (_boutonPowerVise != null) { _boutonPowerVise.Survoler(false); _boutonPowerVise = null; } }
+
+ // ==================== RAMASSER ====================
+
+ void Ramasser()
+ {
+ TerminalCommande terminal = _objetSurvole.GetComponent();
+ if (terminal != null) { terminal.Interagir(); QuitterSurvol(); return; }
+ MonitoringDatacenter monitoring = _objetSurvole.GetComponent();
+ if (monitoring != null) { monitoring.Interagir(); QuitterSurvol(); return; }
+ TableauTickets tableau = _objetSurvole.GetComponent();
+ if (tableau != null) { tableau.Interagir(); QuitterSurvol(); return; }
+ AgrandissementSalle agr = _objetSurvole.GetComponentInParent();
+ if (agr != null && !agr.estAgrandi && !agr.enAnimation) { agr.Agrandir(); QuitterSurvol(); return; }
+
+ Chariot chariot = _objetSurvole.GetComponent();
+ if (chariot != null && _visePoignee)
+ {
+ if (_chariotPilote == chariot)
+ {
+ LacherChariot();
+ }
+ else
+ {
+ if (_chariotPilote != null) LacherChariot();
+ PrendreChariot(chariot);
+ QuitterSurvol();
+ }
+ return;
+ }
+
+ Chariot chariotParent = _objetSurvole.GetComponentInParent();
+ if (chariotParent != null && chariot == null)
+ {
+ if (chariotParent.estPilote) { QuitterSurvol(); return; }
+
+ // v6.11 : verifier en PREMIER si le toron survole est accroche sur un axe lateral
+ if (chariotParent.equipementsToronAxes != null)
+ {
+ for (int i = 0; i < chariotParent.equipementsToronAxes.Count; i++)
+ {
+ if (chariotParent.equipementsToronAxes[i] == _objetSurvole.gameObject)
+ {
+ GameObject t = chariotParent.RetirerToronDeAxeChariot(i);
+ if (t != null)
+ {
+ PrendreEnMain(t.GetComponent());
+ return;
+ }
+ }
+ }
+ }
+
+ // v6.9 : detecter si l'objet est sur le plateau ou sur l'etagere basse
+ bool surEtagere = chariotParent.equipementsSurEtagere != null
+ && chariotParent.equipementsSurEtagere.Contains(_objetSurvole.gameObject);
+
+ GameObject top = surEtagere
+ ? chariotParent.RetirerEquipementSurEtagere()
+ : chariotParent.RetirerEquipementDuDessus();
+
+ if (top != null)
+ {
+ // v6.13 Bug 2 : broadcaster le detachement a tous les clients
+ // (sinon sur les autres clients l'objet reste parente au chariot et
+ // vole avec lui quand on le lache).
+ NetworkIdentity topNetId = top.GetComponent();
+ if (topNetId != null) CmdDetacherDuChariot(topNetId.netId);
+
+ PrendreEnMain(top.GetComponent());
+ return;
+ }
+ }
+
+ PDU pdu = _objetSurvole.GetComponent();
+ if (pdu != null && pdu.estInstalle)
+ {
+ bool cables = false;
+ foreach (var p in pdu.prises) if (p != null && p.estConnectee) { cables = true; break; }
+ if (cables) { Debug.LogWarning("Impossible : des cables sont branches !"); QuitterSurvol(); return; }
+ pdu.Desinstaller();
+ NetworkIdentity pduNetIdentity = pdu.GetComponent();
+ if (pduNetIdentity != null) CmdDesinstallerPDU(pduNetIdentity.netId);
+ PrendreEnMain(_objetSurvole);
+ return;
+ }
+
+ BaieRack baieRack = _objetSurvole.GetComponent() ?? _objetSurvole.GetComponentInChildren();
+ if (baieRack != null) { EmplacementBaie empl = TrouverEmplacementDeBaie(_objetSurvole.gameObject); if (empl != null) empl.RetirerBaie(); }
+
+ RackSlot slotSrc = TrouverSlotDeEquipement(_objetSurvole);
+ if (slotSrc != null)
+ {
+ if (!PorteAvantOuverte(slotSrc)) { HUDManager hud = GetComponent(); if (hud != null) hud.SetInfoEquipement("Ouvrir la porte avant d'abord"); QuitterSurvol(); return; }
+ RackSlot[] slotsArr = ObtenirSlotsParent(slotSrc);
+ if (slotsArr != null) slotSrc.Liberer(slotsArr);
+ }
+
+ ZoneLivraison zone = _objetSurvole.GetComponentInParent();
+ if (zone == null) zone = ZoneLivraison.Instance;
+ if (zone != null)
+ {
+ Interactable plusHaut = zone.TrouverPlusHautDansPile(_objetSurvole);
+ if (plusHaut == null) plusHaut = _objetSurvole;
+ Vector3 pos = plusHaut.transform.position;
+ if (plusHaut != _objetSurvole) _objetSurvole.SurvoleeFin();
+ plusHaut.transform.SetParent(null);
+ IgnorerCollisionsAvecStructure(plusHaut.gameObject, zone.transform, true);
+ PrendreEnMain(plusHaut);
+ // v6.13 Bug 1 : reorganisation cote SERVEUR pour que les positions
+ // soient sync via NetworkTransform a tous les clients.
+ CmdReorganiserZoneLivraison(pos);
+ return;
+ }
+
+ PrendreEnMain(_objetSurvole);
+ }
+
+ void PrendreEnMain(Interactable obj)
+ {
+ if (obj == null) return;
+ CartonLivraison carton = obj.GetComponent();
+ if (carton != null && carton.ContientBaie()) { QuitterSurvol(); carton.OuvrirPlanPlacement(); return; }
+ _objetEnMain = obj; _objetSurvole = null;
+ _objetEnMain.Ramasser();
+ _rbEnMain = _objetEnMain.GetComponent();
+ if (_rbEnMain != null)
+ {
+ _rbEnMain.isKinematic = false; _rbEnMain.useGravity = false;
+ _rbEnMain.freezeRotation = true;
+ _rbEnMain.collisionDetectionMode = CollisionDetectionMode.Continuous;
+ _rbEnMain.interpolation = RigidbodyInterpolation.Interpolate;
+ Vector3 dirJoueur = new Vector3(_camera.transform.forward.x, 0, _camera.transform.forward.z).normalized;
+ Quaternion rotJoueur = Quaternion.LookRotation(dirJoueur);
+ _rotationOffsetTransport = Quaternion.Inverse(rotJoueur) * _objetEnMain.transform.rotation;
+ }
+ NetworkIdentity objNetId = obj.GetComponent();
+ if (objNetId != null) CmdRamasser(objNetId.netId);
+
+ // v7.1 : desactiver le NetworkTransform local pour que le client porteur ne
+ // recoive pas la position serveur (qui a une latence). Ainsi il voit son objet
+ // suivre fluidement ses mouvements. NT est reactive au moment de la pose.
+ SetNetworkTransformActif(_objetEnMain.gameObject, false);
+ }
+
+ // v7.1 : helper pour activer/desactiver le NetworkTransform local pendant le portage
+ // Evite que le client porteur recoive la position serveur (avec latence) et voie
+ // son objet trembler. Les autres clients continuent de recevoir normalement.
+ void SetNetworkTransformActif(GameObject obj, bool actif)
+ {
+ if (obj == null) return;
+ foreach (var nt in obj.GetComponents())
+ nt.enabled = actif;
+ }
+
+ // ==================== POSER ====================
+
+ void Poser()
+ {
+ RestaurerCollisionsIgnorees();
+
+ if (_axeSupportCibleLigne >= 0 && _objetEnMain != null && _objetEnMain.GetComponent() != null)
+ { PoserSurSupportTorons(); return; }
+
+ if (_emplacementSupportPDUCibleLigne >= 0 && _objetEnMain != null && _objetEnMain.GetComponent() != null)
+ { PoserSurSupportPDU(); return; }
+
+ // v6.11 : pose d'un toron sur un axe lateral du chariot (prioritaire sur plateau/etagere)
+ if (_chariotVise != null && _chariotAxeToronCibleIndex >= 0
+ && _objetEnMain != null && _objetEnMain.GetComponent() != null)
+ {
+ int indexAxe = _chariotAxeToronCibleIndex;
+ if (_chariotVise.PoserToronSurAxeChariot(_objetEnMain.gameObject, indexAxe))
+ {
+ _chariotVise.CacherFantomeToronAxe();
+ Vector3 posT = _objetEnMain.transform.position;
+ Quaternion rotT = _objetEnMain.transform.rotation;
+ NotifierPoserReseau(false, posT, rotT, estRacke: true);
+
+ // v6.13 Bug 2 : broadcast parenting a tous les clients
+ NetworkIdentity eqNetId = _objetEnMain.GetComponent();
+ NetworkIdentity chNetId = _chariotVise.GetComponent();
+ if (eqNetId != null && chNetId != null)
+ CmdParenterAuChariot(eqNetId.netId, chNetId.netId);
+
+ // v7.1 : reactiver NetworkTransform local avant de lacher la reference
+ if (_objetEnMain != null) SetNetworkTransformActif(_objetEnMain.gameObject, true);
+ _objetEnMain.Poser(); _objetEnMain = null; _rbEnMain = null;
+ _chariotAxeToronCibleIndex = -1;
+ HUDManager hud = GetComponent();
+ if (hud != null) hud.SetInfoEquipement("");
+ return;
+ }
+ }
+
+ bool estNonRackable = (_objetEnMain.tailleU <= 0 || _objetEnMain.GetComponent() != null);
+ bool estCarton = _objetEnMain.GetComponent() != null;
+ bool estBaie = _objetEnMain.GetComponent() != null;
+ bool estPDU = _objetEnMain.GetComponent() != null;
+
+ Vector3 posFinal = _objetEnMain.transform.position;
+ Quaternion rotFinal = _objetEnMain.transform.rotation;
+ bool gravite = true;
+
+ if (estPDU && _emplacementPDUVise != null && !_emplacementPDUVise.estOccupe)
+ {
+ PDU pduObj = _objetEnMain.GetComponent();
+ pduObj.InstallerSurEmplacement(_emplacementPDUVise);
+ gravite = false; posFinal = _objetEnMain.transform.position; rotFinal = _objetEnMain.transform.rotation;
+ NotifierPoserReseau(gravite, posFinal, rotFinal, estRacke: true);
+ NetworkIdentity pduNetId = _objetEnMain.GetComponent();
+ if (pduNetId != null) CmdInstallerPDU(pduNetId.netId, posFinal, rotFinal);
+ // v7.1 : reactiver NetworkTransform local avant de lacher la reference
+ if (_objetEnMain != null) SetNetworkTransformActif(_objetEnMain.gameObject, true);
+ _objetEnMain.Poser(); _objetEnMain = null; _rbEnMain = null;
+ QuitterSurvolEmplacementPDU();
+ HUDManager hud = GetComponent(); if (hud != null) hud.SetInfoEquipement("");
+ CacherFantomesChariotTous();
+ return;
+ }
+ if (estPDU)
+ {
+ PDU pduObj = _objetEnMain.GetComponent();
+ GestionnaireAlimentation ga = GetComponent();
+ if (ga != null && !pduObj.estInstalle && ga.TenterSnapPDU(pduObj))
+ {
+ gravite = false; posFinal = _objetEnMain.transform.position; rotFinal = _objetEnMain.transform.rotation;
+ NotifierPoserReseau(gravite, posFinal, rotFinal);
+ NetworkIdentity pduNetId = _objetEnMain.GetComponent();
+ if (pduNetId != null) CmdInstallerPDU(pduNetId.netId, posFinal, rotFinal);
+ // v7.1 : reactiver NetworkTransform local avant de lacher la reference
+ if (_objetEnMain != null) SetNetworkTransformActif(_objetEnMain.gameObject, true);
+ _objetEnMain.Poser(); _objetEnMain = null; _rbEnMain = null;
+ CacherFantomesChariotTous();
+ return;
+ }
+ }
+ if (estCarton)
+ {
+ CartonLivraison carton = _objetEnMain.GetComponent();
+ if (carton != null && carton.ContientEquipement())
+ {
+ if (_rbEnMain != null) { _rbEnMain.isKinematic = true; _rbEnMain.velocity = Vector3.zero; }
+ gravite = false; posFinal = _objetEnMain.transform.position; rotFinal = _objetEnMain.transform.rotation;
+ NotifierPoserReseau(gravite, posFinal, rotFinal);
+ // v7.1 : reactiver NetworkTransform local avant de lacher la reference
+ if (_objetEnMain != null) SetNetworkTransformActif(_objetEnMain.gameObject, true);
+ _objetEnMain.Poser(); _objetEnMain = null; _rbEnMain = null;
+ carton.TransformerAuSol();
+ CacherFantomesChariotTous();
+ return;
+ }
+ }
+ if (!estNonRackable && !estBaie && !estCarton && !estPDU && _slotSurvole != null && !_slotSurvole.estOccupe)
+ {
+ if (!PorteAvantOuverte(_slotSurvole)) { HUDManager hud = GetComponent(); if (hud != null) hud.SetInfoEquipement("Ouvrir la porte avant d'abord"); return; }
+ RackSlot[] slotsArr = ObtenirSlotsParent(_slotSurvole);
+ if (slotsArr != null && _slotSurvole.TenterInstallation(_objetEnMain, slotsArr))
+ {
+ gravite = false; posFinal = _objetEnMain.transform.position; rotFinal = _objetEnMain.transform.rotation;
+ NotifierPoserReseau(gravite, posFinal, rotFinal, estRacke: true);
+ NetworkIdentity equipNetId = _objetEnMain.GetComponent();
+ NetworkIdentity baieNetId = _slotSurvole.GetComponentInParent();
+ if (equipNetId != null && baieNetId != null) { RackSlot[] allSlots = baieNetId.GetComponentsInChildren(); int slotIdx = System.Array.IndexOf(allSlots, _slotSurvole); CmdInstallerDansSlot(equipNetId.netId, baieNetId.netId, slotIdx); }
+ // v7.1 : reactiver NetworkTransform local avant de lacher la reference
+ if (_objetEnMain != null) SetNetworkTransformActif(_objetEnMain.gameObject, true);
+ _objetEnMain.Poser(); _objetEnMain = null; _rbEnMain = null;
+ QuitterSurvolSlot();
+ HUDManager hud = GetComponent(); if (hud != null) hud.SetInfoEquipement("");
+ CacherFantomesChariotTous();
+ return;
+ }
+ }
+
+ // ───── v6.10 : Pose sur chariot (plateau ou etagere basse) ─────
+ // IMPORTANT : estRacke=true pour que le serveur mette isKinematic=true.
+ // Sinon les objets poses deviennent non-kinematic sans gravite cote serveur
+ // et flottent en apesanteur des qu'on en retire un du dessus.
+ if (_chariotVise != null && !estNonRackable && !estBaie && !estCarton && !estPDU)
+ {
+ bool pose = _chariotZoneEtagereVisee
+ ? _chariotVise.PoserEquipementSurEtagere(_objetEnMain.gameObject)
+ : _chariotVise.PoserEquipement(_objetEnMain.gameObject);
+ if (pose)
+ {
+ _chariotVise.CacherFantomes();
+ gravite = false; posFinal = _objetEnMain.transform.position; rotFinal = _objetEnMain.transform.rotation;
+ NotifierPoserReseau(gravite, posFinal, rotFinal, estRacke: true);
+
+ // v6.13 Bug 2 : broadcast parenting a tous les clients
+ NetworkIdentity eqNetId = _objetEnMain.GetComponent();
+ NetworkIdentity chNetId = _chariotVise.GetComponent();
+ if (eqNetId != null && chNetId != null)
+ CmdParenterAuChariot(eqNetId.netId, chNetId.netId);
+
+ // v7.1 : reactiver NetworkTransform local avant de lacher la reference
+ if (_objetEnMain != null) SetNetworkTransformActif(_objetEnMain.gameObject, true);
+ _objetEnMain.Poser(); _objetEnMain = null; _rbEnMain = null;
+ return;
+ }
+ }
+ if (_chariotPilote != null && _chariotVise == null && !estNonRackable && !estBaie && !estCarton && !estPDU)
+ if (_chariotPilote.PoserEquipement(_objetEnMain.gameObject))
+ {
+ _chariotPilote.CacherFantomes();
+ gravite = false; posFinal = _objetEnMain.transform.position; rotFinal = _objetEnMain.transform.rotation;
+ NotifierPoserReseau(gravite, posFinal, rotFinal, estRacke: true);
+
+ // v6.13 Bug 2 : broadcast parenting a tous les clients
+ NetworkIdentity eqNetId = _objetEnMain.GetComponent();
+ NetworkIdentity chNetId = _chariotPilote.GetComponent();
+ if (eqNetId != null && chNetId != null)
+ CmdParenterAuChariot(eqNetId.netId, chNetId.netId);
+
+ // v7.1 : reactiver NetworkTransform local avant de lacher la reference
+ if (_objetEnMain != null) SetNetworkTransformActif(_objetEnMain.gameObject, true);
+ _objetEnMain.Poser(); _objetEnMain = null; _rbEnMain = null;
+ return;
+ }
+
+ // Drop au sol
+ if (_rbEnMain != null) { _rbEnMain.useGravity = true; _rbEnMain.velocity = Vector3.zero; _rbEnMain.interpolation = RigidbodyInterpolation.None; }
+ posFinal = _objetEnMain.transform.position; rotFinal = _objetEnMain.transform.rotation;
+ NotifierPoserReseau(true, posFinal, rotFinal);
+ _objetEnMain.Poser(); _objetEnMain = null; _rbEnMain = null;
+ QuitterSurvolSlot(); QuitterSurvolEmplacement(); QuitterSurvolEmplacementPDU();
+ QuitterSurvolSupport();
+ QuitterSurvolSupportPDU();
+ CacherFantomesChariotTous();
+ }
+
+ void PoserSurSupportTorons()
+ {
+ if (_objetEnMain == null) return;
+ int ligneCible = _axeSupportCibleLigne; int colonneCible = _axeSupportCibleColonne;
+ NetworkIdentity toronNetId = _objetEnMain.GetComponent();
+ if (toronNetId != null) CmdPoserToronSurSupport(toronNetId.netId, ligneCible, colonneCible);
+ if (!isServer && SupportTorons.Instance != null) { bool place = SupportTorons.Instance.PoserToronSurAxeSpecifique(_objetEnMain.gameObject, ligneCible, colonneCible); if (!place) SupportTorons.Instance.PoserToron(_objetEnMain.gameObject); }
+ else if (toronNetId == null && SupportTorons.Instance != null) { bool place = SupportTorons.Instance.PoserToronSurAxeSpecifique(_objetEnMain.gameObject, ligneCible, colonneCible); if (!place) SupportTorons.Instance.PoserToron(_objetEnMain.gameObject); }
+ _objetEnMain.Poser(); _objetEnMain = null; _rbEnMain = null;
+ QuitterSurvolSupport();
+ HUDManager hud = GetComponent(); if (hud != null) hud.SetInfoEquipement("");
+ }
+
+ void PoserSurSupportPDU()
+ {
+ if (_objetEnMain == null) return;
+ int ligneCible = _emplacementSupportPDUCibleLigne; int colonneCible = _emplacementSupportPDUCibleColonne;
+ NetworkIdentity pduNetId = _objetEnMain.GetComponent();
+ if (pduNetId != null) CmdPoserPDUSurSupport(pduNetId.netId, ligneCible, colonneCible);
+ if (!isServer && SupportPDU.Instance != null) { bool place = SupportPDU.Instance.PoserPDUSurEmplacementSpecifique(_objetEnMain.gameObject, ligneCible, colonneCible); if (!place) SupportPDU.Instance.PoserPDU(_objetEnMain.gameObject); }
+ else if (pduNetId == null && SupportPDU.Instance != null) { bool place = SupportPDU.Instance.PoserPDUSurEmplacementSpecifique(_objetEnMain.gameObject, ligneCible, colonneCible); if (!place) SupportPDU.Instance.PoserPDU(_objetEnMain.gameObject); }
+ _objetEnMain.Poser(); _objetEnMain = null; _rbEnMain = null;
+ QuitterSurvolSupportPDU();
+ HUDManager hud = GetComponent(); if (hud != null) hud.SetInfoEquipement("");
+ }
+
+ void NotifierPoserReseau(bool avecGravite, Vector3 position, Quaternion rotation, bool estRacke = false)
+ {
+ if (_objetEnMain == null) return;
+ NetworkIdentity objNetId = _objetEnMain.GetComponent();
+ if (objNetId != null) CmdPoser(objNetId.netId, position, rotation, avecGravite, estRacke);
+ }
+
+ // ==================== HELPERS ====================
+
+ bool ViseBouton(Vector3 positionMonde, float seuilPixels = 60f)
+ {
+ if (_camera == null) return false;
+ Vector3 screenPos = _camera.WorldToScreenPoint(positionMonde);
+ if (screenPos.z <= 0f) return false;
+ return Vector2.Distance(new Vector2(Screen.width / 2f, Screen.height / 2f), new Vector2(screenPos.x, screenPos.y)) < seuilPixels;
+ }
+
+ bool PorteAvantOuverte(RackSlot slot)
+ {
+ if (slot == null) return true;
+ Transform baie = slot.transform.parent; if (baie == null) return true;
+ foreach (Transform enfant in baie)
+ if (enfant.name == "Pivot_PorteAvant") { PorteBaie porte = enfant.GetComponent(); if (porte != null) return porte.estOuverte; }
+ return true;
+ }
+
+ RackSlot[] ObtenirSlotsParent(RackSlot slot)
+ {
+ if (slot == null) return null;
+ BaieRack baieRack = slot.GetComponentInParent(); if (baieRack != null && baieRack.slots != null) return baieRack.slots;
+ BaieProcedurale baieProc = slot.GetComponentInParent(); if (baieProc != null && baieProc.slots != null) return baieProc.slots;
+ Transform parent = slot.transform.parent; if (parent != null) return parent.GetComponentsInChildren();
+ return null;
+ }
+
+ private void IgnorerCollisionsAvecStructure(GameObject objet, Transform parent, bool ignorer)
+ {
+ Collider objCollider = objet.GetComponent(); if (objCollider == null) return;
+ _collisionsIgnorees.Clear();
+ for (int i = 0; i < parent.childCount; i++)
+ {
+ Transform enfant = parent.GetChild(i); string nom = enfant.name;
+ if (nom.StartsWith("Planche_") || nom.StartsWith("Montant_") || nom.StartsWith("Etiquette_"))
+ { Collider sc = enfant.GetComponent(); if (sc != null) { Physics.IgnoreCollision(objCollider, sc, ignorer); if (ignorer) _collisionsIgnorees.Add(sc); } }
+ Interactable inter = enfant.GetComponent();
+ if (inter != null && enfant.gameObject != objet)
+ { Collider ec = enfant.GetComponent(); if (ec != null) { Physics.IgnoreCollision(objCollider, ec, ignorer); if (ignorer) _collisionsIgnorees.Add(ec); } }
+ }
+ }
+
+ private void RestaurerCollisionsIgnorees()
+ {
+ if (_objetEnMain == null || _collisionsIgnorees.Count == 0) return;
+ Collider objCollider = _objetEnMain.GetComponent();
+ if (objCollider == null) { _collisionsIgnorees.Clear(); return; }
+ foreach (Collider col in _collisionsIgnorees) if (col != null) Physics.IgnoreCollision(objCollider, col, false);
+ _collisionsIgnorees.Clear();
+ }
+
+ // v6.12 : gestion pilotage chariot + transfert d'autorite multijoueur
+ void PrendreChariot(Chariot chariot)
+ {
+ if (chariot == null) return;
+ _chariotPilote = chariot;
+
+ // Demander l'autorite au serveur pour pouvoir modifier transform.position
+ // via NetworkTransform ClientToServer. Sans ca, Mirror ignore les changements
+ // cote client et les autres joueurs ne voient pas le chariot bouger.
+ NetworkIdentity ni = chariot.GetComponent();
+ if (ni != null) CmdDemanderAutoriteChariot(ni.netId);
+
+ chariot.TogglePilotage(transform);
+ }
+
+ void LacherChariot()
+ {
+ if (_chariotPilote == null) return;
+
+ NetworkIdentity ni = _chariotPilote.GetComponent();
+
+ _chariotPilote.CacherFantomes();
+ _chariotPilote.TogglePilotage(transform);
+
+ // Relacher l'autorite APRES TogglePilotage pour que la position finale
+ // soit bien envoyee au serveur avant que le client perde son droit d'ecrire.
+ if (ni != null) CmdRelacherAutoriteChariot(ni.netId);
+
+ _chariotPilote = null;
+ }
+
+ [Command]
+ void CmdDemanderAutoriteChariot(uint chariotNetId)
+ {
+ if (!NetworkServer.spawned.ContainsKey(chariotNetId)) return;
+ NetworkIdentity ni = NetworkServer.spawned[chariotNetId];
+ if (ni == null) return;
+
+ // Si un autre client a deja l'autorite, la retirer d'abord
+ if (ni.connectionToClient != null && ni.connectionToClient != connectionToClient)
+ ni.RemoveClientAuthority();
+
+ ni.AssignClientAuthority(connectionToClient);
+ }
+
+ [Command]
+ void CmdRelacherAutoriteChariot(uint chariotNetId)
+ {
+ if (!NetworkServer.spawned.ContainsKey(chariotNetId)) return;
+ NetworkIdentity ni = NetworkServer.spawned[chariotNetId];
+ if (ni == null) return;
+
+ // Ne retirer l'autorite que si c'est bien ce joueur qui l'a actuellement,
+ // pour eviter de voler l'autorite a quelqu'un d'autre par accident.
+ if (ni.connectionToClient == connectionToClient)
+ ni.RemoveClientAuthority();
+ }
+
+ // ===== v6.13 Bug 1 : reorganisation ZoneLivraison cote serveur =====
+ [Command]
+ void CmdReorganiserZoneLivraison(Vector3 posRetireeWorld)
+ {
+ if (ZoneLivraison.Instance != null)
+ ZoneLivraison.Instance.ReorganiserNiveauComplet(posRetireeWorld);
+ }
+
+ // ===== v6.13 Bug 2 : sync du parenting equipement <-> chariot =====
+ [Command]
+ void CmdParenterAuChariot(uint equipNetId, uint chariotNetId)
+ {
+ if (!NetworkServer.spawned.ContainsKey(equipNetId)) return;
+ if (!NetworkServer.spawned.ContainsKey(chariotNetId)) return;
+ RpcParenterAuChariot(equipNetId, chariotNetId);
+ }
+
+ [ClientRpc]
+ void RpcParenterAuChariot(uint equipNetId, uint chariotNetId)
+ {
+ if (!NetworkClient.spawned.TryGetValue(equipNetId, out NetworkIdentity eqNi)) return;
+ if (!NetworkClient.spawned.TryGetValue(chariotNetId, out NetworkIdentity chNi)) return;
+ if (eqNi == null || chNi == null) return;
+
+ // worldPositionStays=true : conserver la position monde actuelle. L'equipement
+ // est deja a la bonne position (via NetworkTransform), on veut juste l'attacher
+ // au chariot pour qu'il suive les futurs mouvements.
+ eqNi.transform.SetParent(chNi.transform, true);
+ }
+
+ [Command]
+ void CmdDetacherDuChariot(uint equipNetId)
+ {
+ if (!NetworkServer.spawned.ContainsKey(equipNetId)) return;
+ RpcDetacherDuChariot(equipNetId);
+ }
+
+ [ClientRpc]
+ void RpcDetacherDuChariot(uint equipNetId)
+ {
+ if (!NetworkClient.spawned.TryGetValue(equipNetId, out NetworkIdentity eqNi)) return;
+ if (eqNi == null) return;
+ eqNi.transform.SetParent(null, true);
+ }
+
+ EmplacementBaie TrouverEmplacementDeBaie(GameObject baie)
+ {
+ if (_tousEmplacements == null) _tousEmplacements = FindObjectsOfType();
+ foreach (EmplacementBaie empl in _tousEmplacements) if (empl.estOccupe && empl.baieInstallee == baie) return empl;
+ return null;
+ }
+
+ RackSlot TrouverSlotDeEquipement(Interactable equipement)
+ {
+ if (equipement == null) return null;
+ foreach (BaieRack baie in FindObjectsOfType()) { if (baie.slots == null) continue; foreach (RackSlot s in baie.slots) if (s != null && s.equipementInstalle == equipement) return s; }
+ foreach (BaieProcedurale baie in FindObjectsOfType()) { if (baie.slots == null) continue; foreach (RackSlot s in baie.slots) if (s != null && s.equipementInstalle == equipement) return s; }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/Patchs/pack_multi/Scripts_Nouveaux/EtatEquipementNetworkSync.cs b/Patchs/pack_multi/Scripts_Nouveaux/EtatEquipementNetworkSync.cs
new file mode 100644
index 0000000..bbd81d2
--- /dev/null
+++ b/Patchs/pack_multi/Scripts_Nouveaux/EtatEquipementNetworkSync.cs
@@ -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();
+ _etat = GetComponentInChildren();
+ }
+
+ 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
+ // ============================================================
+
+ ///
+ /// Appele cote serveur quand BoutonPower.Appuyer() a ete execute et qu'il
+ /// faut synchroniser l'etat aux clients (actuels et futurs).
+ ///
+ [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();
+ 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();
+ if (_etat == null) return;
+
+ _etat.estSousTension = valeur;
+ }
+}
diff --git a/Patchs/pack_multi/Scripts_Nouveaux/GestionnaireCablesReseau.cs b/Patchs/pack_multi/Scripts_Nouveaux/GestionnaireCablesReseau.cs
new file mode 100644
index 0000000..4c76a6e
--- /dev/null
+++ b/Patchs/pack_multi/Scripts_Nouveaux/GestionnaireCablesReseau.cs
@@ -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 _cablesAlim = new SyncList();
+ private readonly SyncList _cablesRJ45 = new SyncList();
+
+ // ============================================================
+ // 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 _cablesAlimRecrees = new HashSet();
+ private readonly HashSet _cablesRJ45Recrees = new HashSet();
+
+ // Cables en attente : ceux qu'on a tente de recreer mais dont les refs etaient null
+ private readonly List _attenteAlim = new List();
+ private readonly List _attenteRJ45 = new List();
+
+ // ============================================================
+ // 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
+ // ============================================================
+
+ ///
+ /// 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.
+ ///
+ 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();
+ 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 chemin = ConstruireCheminAuto(
+ portSource.GetPointConnexion(), portDest.GetPointConnexion());
+
+ GameObject cableObj = new GameObject($"CableRJ45_rejoin_{data.equipSourceNetId}_{data.portSourceIndex}");
+ CableRJ45 cable = cableObj.AddComponent();
+ 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();
+ 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();
+ if (pdu != null && indexPrise >= 0 && indexPrise < pdu.prises.Count)
+ return pdu.prises[indexPrise];
+
+ PriseC13[] prises = pduGO.GetComponentsInChildren();
+ 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();
+ if (indexPort >= 0 && indexPort < ports.Length) return ports[indexPort];
+ return null;
+ }
+
+ private List ConstruireCheminAuto(Vector3 posSource, Vector3 posDest)
+ {
+ // Algorithme A* simplifie (copie depuis CablageReseau)
+ List chemin = new List();
+ PointAccroche[] tousPoints = FindObjectsOfType();
+ 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 gScore = new Dictionary();
+ Dictionary cameFrom = new Dictionary();
+ HashSet closedSet = new HashSet();
+ List openSet = new List();
+
+ 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 result = new List { 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;
+ }
+}
diff --git a/dcsim_pack_multi_v7.1.zip b/ZIP/dcsim_pack_multi_v7.1.zip
similarity index 100%
rename from dcsim_pack_multi_v7.1.zip
rename to ZIP/dcsim_pack_multi_v7.1.zip