diff --git a/DebugFalling.cs b/DebugFalling.cs
new file mode 100644
index 0000000..67fb40e
--- /dev/null
+++ b/DebugFalling.cs
@@ -0,0 +1,94 @@
+using UnityEngine;
+
+///
+/// TEMPORAIRE - Debug pour diagnostiquer le problème de chute sous la map.
+/// Ajouter sur le prefab PlayerCapsule, supprimer après diagnostic.
+///
+public class DebugFalling : MonoBehaviour, IClientOnly
+{
+ private CharacterController _cc;
+ private StarterAssets.FirstPersonController _fpc;
+ private float _lastLogTime = 0f;
+ private float _lastY;
+ private bool _wasFalling = false;
+
+ void Start()
+ {
+ _cc = GetComponent();
+ _fpc = GetComponent();
+ _lastY = transform.position.y;
+
+ if (_cc != null)
+ {
+ Debug.Log($"[DebugFall] CharacterController: center={_cc.center} height={_cc.height} radius={_cc.radius} skinWidth={_cc.skinWidth}");
+ Debug.Log($"[DebugFall] Layer joueur: {gameObject.layer} ({LayerMask.LayerToName(gameObject.layer)})");
+ }
+
+ if (_fpc != null)
+ {
+ Debug.Log($"[DebugFall] FPC enabled={_fpc.enabled} GroundLayers={_fpc.GroundLayers.value} GroundedOffset={_fpc.GroundedOffset} GroundedRadius={_fpc.GroundedRadius}");
+ }
+
+ // Test : y a-t-il un sol sous nous ?
+ RaycastHit hit;
+ if (Physics.Raycast(transform.position + Vector3.up * 0.5f, Vector3.down, out hit, 50f))
+ Debug.Log($"[DebugFall] Sol détecté: {hit.collider.gameObject.name} layer={LayerMask.LayerToName(hit.collider.gameObject.layer)} distance={hit.distance:F2} point={hit.point}");
+ else
+ Debug.Log("[DebugFall] AUCUN SOL DÉTECTÉ sous le joueur !");
+
+ // Test GroundLayers
+ if (_fpc != null)
+ {
+ int solLayer = LayerMask.NameToLayer("Sol");
+ bool solInclus = (_fpc.GroundLayers.value & (1 << solLayer)) != 0;
+ Debug.Log($"[DebugFall] Layer Sol ({solLayer}) inclus dans GroundLayers: {solInclus}");
+
+ // Test CheckSphere comme le fait le FPC
+ Vector3 spherePos = new Vector3(transform.position.x, transform.position.y - _fpc.GroundedOffset, transform.position.z);
+ bool grounded = Physics.CheckSphere(spherePos, _fpc.GroundedRadius, _fpc.GroundLayers, QueryTriggerInteraction.Ignore);
+ Debug.Log($"[DebugFall] CheckSphere grounded={grounded} spherePos={spherePos} radius={_fpc.GroundedRadius}");
+ }
+ }
+
+ void Update()
+ {
+ if (_fpc == null || _cc == null) return;
+
+ float y = transform.position.y;
+ bool falling = y < _lastY - 0.01f;
+
+ // Log quand on commence à tomber
+ if (falling && !_wasFalling)
+ {
+ Debug.LogWarning($"[DebugFall] DÉBUT CHUTE à Y={y:F2} pos={transform.position} Grounded={_fpc.Grounded} FPC.enabled={_fpc.enabled} CC.enabled={_cc.enabled} CC.isGrounded={_cc.isGrounded}");
+
+ // Vérifier ce qu'il y a sous nous
+ RaycastHit hit;
+ if (Physics.Raycast(transform.position, Vector3.down, out hit, 50f))
+ Debug.LogWarning($"[DebugFall] Sous nous: {hit.collider.gameObject.name} layer={LayerMask.LayerToName(hit.collider.gameObject.layer)} dist={hit.distance:F2}");
+ else
+ Debug.LogWarning("[DebugFall] RIEN sous le joueur !");
+ }
+
+ // Log continu pendant la chute (1x par seconde)
+ if (falling && Time.time - _lastLogTime > 1f)
+ {
+ _lastLogTime = Time.time;
+ Debug.LogWarning($"[DebugFall] EN CHUTE Y={y:F2} velocity.y={_cc.velocity.y:F2} Grounded={_fpc.Grounded} CC.isGrounded={_cc.isGrounded}");
+ }
+
+ // Log quand on tombe trop bas
+ if (y < -5f && Time.time - _lastLogTime > 2f)
+ {
+ _lastLogTime = Time.time;
+ Debug.LogError($"[DebugFall] SOUS LA MAP Y={y:F2} téléportation de secours !");
+ // Téléporter au spawn de secours
+ _cc.enabled = false;
+ transform.position = new Vector3(0, 2, 0);
+ _cc.enabled = true;
+ }
+
+ _wasFalling = falling;
+ _lastY = y;
+ }
+}
\ No newline at end of file
diff --git a/MasterServerClient.cs b/MasterServerClient.cs
new file mode 100644
index 0000000..7f0082c
--- /dev/null
+++ b/MasterServerClient.cs
@@ -0,0 +1,422 @@
+using UnityEngine;
+using UnityEngine.Networking;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Mirror;
+
+///
+/// MasterServerClient.cs - v6.6
+///
+/// v6.6 : Robustesse du heartbeat pour dedicated server
+/// - Heartbeat passe de 30s a 10s par defaut (reduit la fenetre
+/// ou le master server peut purger un serveur inactif)
+/// - Sur reponse 404 (serveur inconnu), re-enregistrement AUTOMATIQUE
+/// immediat avec les memes parametres que le register initial (au lieu
+/// de juste reset les variables et attendre le prochain heartbeat)
+/// - Memoire des parametres de registration pour pouvoir re-register
+/// sans intervention exterieure
+/// - Apres 3 echecs consecutifs, abandon et log d'erreur
+///
+/// v6.5 : Version originale
+///
+/// Usage :
+/// Placer sur le meme GameObject que le NetworkManager (ou un singleton).
+/// Configurer masterServerUrl dans l'Inspector.
+///
+public class MasterServerClient : MonoBehaviour
+{
+ // ══════════════════════════════════════════════════
+ // CONFIGURATION
+ // ══════════════════════════════════════════════════
+
+ [Header("Master Server")]
+ [Tooltip("URL du master server (ex: http://192.168.1.50:8080)")]
+ public string masterServerUrl = "http://localhost:8080";
+
+ [Tooltip("Token d'authentification (doit correspondre au serveur)")]
+ public string token = "dcsim-2026-secret";
+
+ [Header("Heartbeat")]
+ [Tooltip("Intervalle du heartbeat en secondes (10s recommande pour dedicated)")]
+ public float heartbeatInterval = 10f;
+
+ [Tooltip("Nombre max d'echecs consecutifs avant abandon (reset si succes)")]
+ public int maxEchecsConsecutifs = 3;
+
+ // ══════════════════════════════════════════════════
+ // ETAT
+ // ══════════════════════════════════════════════════
+
+ [HideInInspector] public string serverId = "";
+ [HideInInspector] public bool estEnregistre = false;
+ [HideInInspector] public List derniereListeServeurs = new List();
+ [HideInInspector] public bool requeteEnCours = false;
+ [HideInInspector] public string dernierStatut = "";
+
+ // Singleton
+ public static MasterServerClient Instance { get; private set; }
+
+ private Coroutine _heartbeatCoroutine;
+ private int _echecsConsecutifs = 0;
+
+ // v6.6 : memoire des parametres d'enregistrement pour re-register automatique
+ private string _dernierNom = "";
+ private int _dernierPort = 7777;
+ private int _dernierMaxJoueurs = 4;
+ private int _derniereLangue = 1;
+ private string _dernierMode = "Sandbox";
+ private string _derniereVersion = "v6.5g";
+ private bool _dernierMotDePasse = false;
+
+ // ══════════════════════════════════════════════════
+ // MODELES DE DONNEES
+ // ══════════════════════════════════════════════════
+
+ [System.Serializable]
+ public class ServerData
+ {
+ public string id;
+ public string nom;
+ public string ip;
+ public int port;
+ public int joueurs;
+ public int max_joueurs;
+ public int ping;
+ public int langue;
+ public string mode;
+ public string version;
+ public bool mot_de_passe;
+ }
+
+ [System.Serializable]
+ private class ServerListWrapper
+ {
+ public List servers;
+ }
+
+ [System.Serializable]
+ private class RegisterRequest
+ {
+ public string nom;
+ public int port;
+ public int max_joueurs;
+ public int langue;
+ public string mode;
+ public string version;
+ public bool mot_de_passe;
+ public string token;
+ }
+
+ [System.Serializable]
+ private class RegisterResponse
+ {
+ public string server_id;
+ public string status;
+ }
+
+ [System.Serializable]
+ private class HeartbeatRequest
+ {
+ public int joueurs;
+ public string token;
+ }
+
+ // ══════════════════════════════════════════════════
+ // LIFECYCLE
+ // ══════════════════════════════════════════════════
+
+ void Awake()
+ {
+ if (Instance != null && Instance != this)
+ {
+ Destroy(gameObject);
+ return;
+ }
+ Instance = this;
+ Debug.Log($"[MasterServer] Awake → Instance initialisee, URL={masterServerUrl}");
+ }
+
+ void OnDestroy()
+ {
+ if (Instance == this)
+ Instance = null;
+ }
+
+ void OnApplicationQuit()
+ {
+ if (estEnregistre && !string.IsNullOrEmpty(serverId))
+ {
+ var request = new UnityWebRequest(
+ $"{masterServerUrl}/unregister/{serverId}", "DELETE");
+ request.timeout = 2;
+ request.SendWebRequest();
+ Debug.Log($"[MasterServer] Desenregistrement envoye ({serverId})");
+ }
+ }
+
+ // ══════════════════════════════════════════════════
+ // API PUBLIQUE
+ // ══════════════════════════════════════════════════
+
+ public void EnregistrerServeur(string nom, int port, int maxJoueurs,
+ int langue, string mode, string version, bool motDePasse)
+ {
+ // v6.6 : memorise les parametres pour pouvoir re-register en cas de 404
+ _dernierNom = nom;
+ _dernierPort = port;
+ _dernierMaxJoueurs = maxJoueurs;
+ _derniereLangue = langue;
+ _dernierMode = mode;
+ _derniereVersion = version;
+ _dernierMotDePasse = motDePasse;
+
+ StartCoroutine(Register(nom, port, maxJoueurs, langue, mode, version, motDePasse));
+ }
+
+ public void DesenregistrerServeur()
+ {
+ if (estEnregistre && !string.IsNullOrEmpty(serverId))
+ StartCoroutine(Unregister());
+ }
+
+ public void RecupererServeurs(System.Action, string> callback)
+ {
+ StartCoroutine(FetchServers(callback));
+ }
+
+ public void VerifierConnexion(System.Action callback)
+ {
+ StartCoroutine(CheckHealth(callback));
+ }
+
+ // ══════════════════════════════════════════════════
+ // COROUTINES HTTP
+ // ══════════════════════════════════════════════════
+
+ private IEnumerator Register(string nom, int port, int maxJoueurs,
+ int langue, string mode, string version, bool motDePasse)
+ {
+ requeteEnCours = true;
+ dernierStatut = "Enregistrement...";
+
+ var body = new RegisterRequest
+ {
+ nom = nom,
+ port = port,
+ max_joueurs = maxJoueurs,
+ langue = langue,
+ mode = mode,
+ version = version,
+ mot_de_passe = motDePasse,
+ token = token
+ };
+
+ string json = JsonUtility.ToJson(body);
+ byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(json);
+
+ var request = new UnityWebRequest($"{masterServerUrl}/register", "POST");
+ request.uploadHandler = new UploadHandlerRaw(bodyRaw);
+ request.downloadHandler = new DownloadHandlerBuffer();
+ request.SetRequestHeader("Content-Type", "application/json");
+ request.timeout = 10;
+
+ yield return request.SendWebRequest();
+
+ if (request.result == UnityWebRequest.Result.Success)
+ {
+ var response = JsonUtility.FromJson(request.downloadHandler.text);
+ serverId = response.server_id;
+ estEnregistre = true;
+ _echecsConsecutifs = 0;
+ dernierStatut = $"Enregistre ({serverId})";
+ Debug.Log($"[MasterServer] Serveur enregistre : {nom} → ID={serverId}");
+
+ // Demarrer le heartbeat (une seule coroutine active a la fois)
+ if (_heartbeatCoroutine != null)
+ StopCoroutine(_heartbeatCoroutine);
+ _heartbeatCoroutine = StartCoroutine(HeartbeatLoop());
+ }
+ else
+ {
+ dernierStatut = $"Erreur enregistrement : {request.error}";
+ Debug.LogWarning($"[MasterServer] Erreur register : {request.error} (code {request.responseCode})");
+ }
+
+ requeteEnCours = false;
+ request.Dispose();
+ }
+
+ private IEnumerator Unregister()
+ {
+ requeteEnCours = true;
+ dernierStatut = "Desenregistrement...";
+
+ if (_heartbeatCoroutine != null)
+ {
+ StopCoroutine(_heartbeatCoroutine);
+ _heartbeatCoroutine = null;
+ }
+
+ var request = UnityWebRequest.Delete($"{masterServerUrl}/unregister/{serverId}");
+ request.timeout = 5;
+
+ yield return request.SendWebRequest();
+
+ if (request.result == UnityWebRequest.Result.Success)
+ {
+ Debug.Log($"[MasterServer] Serveur desenregistre : {serverId}");
+ dernierStatut = "Serveur retire de la liste";
+ }
+ else
+ {
+ Debug.LogWarning($"[MasterServer] Erreur unregister : {request.error}");
+ dernierStatut = "Erreur desenregistrement (le cleanup nettoiera)";
+ }
+
+ estEnregistre = false;
+ serverId = "";
+ requeteEnCours = false;
+ request.Dispose();
+ }
+
+ private IEnumerator HeartbeatLoop()
+ {
+ while (estEnregistre && !string.IsNullOrEmpty(serverId))
+ {
+ yield return new WaitForSecondsRealtime(heartbeatInterval);
+
+ if (!estEnregistre || string.IsNullOrEmpty(serverId))
+ yield break;
+
+ int nbJoueurs = NetworkServer.active ? NetworkServer.connections.Count : 0;
+
+ var body = new HeartbeatRequest
+ {
+ joueurs = nbJoueurs,
+ token = token
+ };
+
+ string json = JsonUtility.ToJson(body);
+ byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(json);
+
+ var request = new UnityWebRequest(
+ $"{masterServerUrl}/heartbeat/{serverId}", "PUT");
+ request.uploadHandler = new UploadHandlerRaw(bodyRaw);
+ request.downloadHandler = new DownloadHandlerBuffer();
+ request.SetRequestHeader("Content-Type", "application/json");
+ request.timeout = 10;
+
+ yield return request.SendWebRequest();
+
+ if (request.result == UnityWebRequest.Result.Success)
+ {
+ // Reset du compteur d'echecs
+ _echecsConsecutifs = 0;
+ }
+ else
+ {
+ _echecsConsecutifs++;
+ long code = request.responseCode;
+ string err = request.error;
+ Debug.LogWarning($"[MasterServer] Heartbeat echoue ({_echecsConsecutifs}/{maxEchecsConsecutifs}) : HTTP {code} - {err}");
+
+ // v6.6 : si 404, le master server nous a purges → on re-register AUTOMATIQUEMENT
+ // et IMMEDIATEMENT, sans attendre le prochain cycle de heartbeat
+ if (code == 404)
+ {
+ Debug.Log("[MasterServer] Serveur inconnu (404), re-enregistrement automatique...");
+ request.Dispose();
+
+ // Reset de l'etat
+ estEnregistre = false;
+ string ancienId = serverId;
+ serverId = "";
+
+ // Relance Register() avec les parametres memorises
+ yield return StartCoroutine(Register(
+ _dernierNom, _dernierPort, _dernierMaxJoueurs,
+ _derniereLangue, _dernierMode, _derniereVersion, _dernierMotDePasse
+ ));
+
+ if (estEnregistre)
+ {
+ Debug.Log($"[MasterServer] Re-enregistrement OK : {ancienId} → {serverId}");
+ // Le HeartbeatLoop continue, mais on vient de demarrer un nouveau via Register.
+ // On sort de cette boucle pour eviter d'en avoir deux en parallele.
+ yield break;
+ }
+ else
+ {
+ Debug.LogError($"[MasterServer] Re-enregistrement echoue, abandon du heartbeat");
+ yield break;
+ }
+ }
+
+ // Apres trop d'echecs consecutifs (pas 404 mais reseau/timeout/etc), abandon
+ if (_echecsConsecutifs >= maxEchecsConsecutifs)
+ {
+ Debug.LogError($"[MasterServer] Abandon du heartbeat apres {maxEchecsConsecutifs} echecs consecutifs");
+ estEnregistre = false;
+ yield break;
+ }
+ }
+
+ request.Dispose();
+ }
+ }
+
+ private IEnumerator FetchServers(System.Action, string> callback)
+ {
+ requeteEnCours = true;
+ dernierStatut = "Recherche de serveurs...";
+
+ float startTime = Time.realtimeSinceStartup;
+
+ var request = UnityWebRequest.Get($"{masterServerUrl}/servers");
+ request.timeout = 10;
+
+ yield return request.SendWebRequest();
+
+ int pingMs = Mathf.RoundToInt((Time.realtimeSinceStartup - startTime) * 1000f);
+
+ if (request.result == UnityWebRequest.Result.Success)
+ {
+ string responseText = request.downloadHandler.text;
+ string wrapped = "{\"servers\":" + responseText + "}";
+ var wrapper = JsonUtility.FromJson(wrapped);
+
+ derniereListeServeurs = wrapper.servers ?? new List();
+
+ foreach (var srv in derniereListeServeurs)
+ srv.ping = pingMs;
+
+ dernierStatut = $"{derniereListeServeurs.Count} serveur(s) trouve(s)";
+ Debug.Log($"[MasterServer] {derniereListeServeurs.Count} serveurs recuperes (ping={pingMs}ms)");
+
+ callback?.Invoke(derniereListeServeurs, null);
+ }
+ else
+ {
+ string erreur = $"Erreur : {request.error}";
+ dernierStatut = erreur;
+ Debug.LogWarning($"[MasterServer] Erreur fetch : {request.error}");
+ callback?.Invoke(new List(), erreur);
+ }
+
+ requeteEnCours = false;
+ request.Dispose();
+ }
+
+ private IEnumerator CheckHealth(System.Action callback)
+ {
+ var request = UnityWebRequest.Get($"{masterServerUrl}/health");
+ request.timeout = 5;
+
+ yield return request.SendWebRequest();
+
+ bool ok = request.result == UnityWebRequest.Result.Success;
+ callback?.Invoke(ok);
+ request.Dispose();
+ }
+}
diff --git a/README_PATCHES_V2.md b/README_PATCHES_V2.md
new file mode 100644
index 0000000..fad1790
--- /dev/null
+++ b/README_PATCHES_V2.md
@@ -0,0 +1,47 @@
+# Patches v2 - Correction des problèmes du premier déploiement
+
+## Ce qui est dans ce ZIP
+
+### Scripts modifiés (2)
+
+- **`MasterServerClient.cs` v6.6**
+ - Heartbeat passe de 30s → 10s par défaut
+ - Sur 404, re-registration automatique IMMÉDIATE (mémoise les paramètres)
+ - Compteur d'échecs consécutifs (abandon après 3)
+
+- **`DebugFalling.cs`**
+ - Marqué `IClientOnly` → détruit au boot serveur, zéro log parasite
+
+## Actions manuelles dans Unity (à faire AVANT de rebuild)
+
+### 1. Remplacer les 2 scripts
+Copie les 2 `.cs` dans `Assets/Scripts/`, écrase les fichiers existants.
+
+### 2. Assigner le catalogue à BoutiqueReseau (Inspector)
+
+Dans la scène `Datacenter_01` :
+1. Sélectionne le GameObject qui porte le composant `BoutiqueReseau` (probablement sur le même GameObject que le `NetworkManager` ou sur un "Managers")
+2. Dans l'Inspector, déplie le composant `BoutiqueReseau`
+3. Champ **Catalogue** → clique sur le petit triangle pour déplier, mets la taille à X
+4. Glisse-dépose tes `ArticleCatalogue` ScriptableObjects depuis le Project Window vers les slots
+5. Sauvegarde la scène (Ctrl+S)
+
+💡 **Pour retrouver tes articles plus rapidement** : sélectionne l'objet qui porte `UIBoutique` actuellement et regarde son champ `catalogue`, tu dois y voir tous les ScriptableObjects. Note-les ou fais un drag multi-sélection.
+
+### 3. Rebuild
+
+Build settings → Linux Dedicated Server → Build → déploie sur la Debian.
+
+## Effet attendu
+
+Après ces patches, plus de :
+- ❌ `[MasterServer] Heartbeat échoué : HTTP/1.1 404 Not Found`
+ → Résolu par re-registration automatique + intervalle réduit à 10s
+
+- ❌ `[BoutiqueReseau] Article introuvable : Baie 42U`
+ → Résolu par l'assignation directe du catalogue dans l'Inspector
+
+- ❌ Spam de logs `[DebugFall] ...`
+ → Résolu par le marqueur IClientOnly
+
+Logs serveur propres = monitoring plus efficace.