(Init) Setting

This commit is contained in:
2026-06-22 16:11:12 +02:00
parent 8a89a91fe2
commit 91df8353ec
35 changed files with 3476 additions and 0 deletions
@@ -0,0 +1,3 @@
# Memory Index
- [Migration multijoueur](multiplayer-migration.md) — passage en multi via FishNet + Steamworks.NET + FishySteamworks, lobby Steam (code + invitations)
@@ -0,0 +1,78 @@
---
name: multiplayer-migration
description: Migration du jeu solo vers le multijoueur via FishNet + Steam (en cours, démarré juin 2026)
metadata:
type: project
---
Le jeu de survie Ashwild passe en multijoueur coop. Stack réseau choisie :
- **FishNet 4.7.2** (déjà installé dans `Assets/FishNet/`)
- **Steamworks.NET** (wrapper API Steam) — choisi plutôt que Facepunch.Steamworks
- **FishySteamworks** (transport FishNet, utilise SteamNetworkingSockets → relais Steam, pas de port forwarding)
Objectif matchmaking : **lobby Steam** offrant à la fois
1. un **code de session** partageable, et
2. les **invitations Steam natives** (overlay « Inviter », clic droit → « Rejoindre » dans la liste d'amis, lancement via invitation).
Contraintes projet : bêta-testeurs dans ~2 semaines → priorité stabilité. Tout passe par le bus `PlayerEvents` (voir CLAUDE.md).
**App IDs Steam** (dans `steam_appid.txt` à la racine) :
- **App ID actif = `4282850`** = vrai jeu « Aetheris » (app privée/incomplète sur Steamworks). Choisi pour les **tests internes** : le user préfère tester sur son propre jeu Steam. Chaque testeur doit avoir accès à l'app (collaborateur Steamworks ou app en bibliothèque), sinon `SteamAPI.Init()` échoue chez lui.
- `480` (Spacewar) = alternative de dev (accès gratuit pour tous) si besoin de tester avec des comptes sans accès à l'app.
**Phase 1 (fondation) — code écrit, scripts dans `Assets/GAME/Script/Network/`** :
- `GameSession.cs` : holder statique (slot choisi, Host/Join, code) qui survit au chargement de scène.
- `NetworkSessionManager.cs` : singleton (sur le GameObject NetworkManager). `HostSession()` / `JoinSession(code)` / `StopSession()`. Démarre server+client, charge la GameScene en réseau via `SceneManager.LoadGlobalScenes` (ReplaceOption.All), génère le code = SteamID64, logs `[NetworkSession]`, branché sur le bus.
- `PlayerSpawner.cs` : sur le NetworkManager. S'abonne à `OnClientLoadedStartScenes`, spawn le prefab joueur (NetworkObject) par connexion à un `PlayerSpawnPoint`.
- `PlayerSpawnPoint.cs` : marqueur de point de spawn (avec gizmo).
- `PlayerNetworkController.cs` : NetworkBehaviour sur le joueur. En `OnStartClient`, n'active les composants « owner-only » (input, caméra, audio, stats, inventaire…) que si `IsOwner`. Liste à câbler dans l'inspector.
- `SavePanel.LoadGame` modifié : appelle `NetworkSessionManager.Instance.HostSession()` (repli `SceneManager.LoadScene` si pas câblé).
- Bus `PlayerEvents` étendu : états `IsOnline/IsHost/SessionCode` + events `SessionStarting/Started/Joining/Joined/Stopped/Error`, `RemotePlayerCountChanged`, `LocalPlayerSpawned`.
**Correctif Phase 1 (NRE au spawn)** : les UI de scène (`HotbarUI`, `InventoryUI`, `CraftingUI`, `DeathScreen`, `PlayerStatsUI`) lisaient `PlayerInventory.Instance`/`PlayerStats.Instance` dans `Start()` → NRE car le joueur est spawné par le réseau APRÈS le chargement de scène. Corrigé : init dépendante du joueur différée via `PlayerEvents.LocalPlayerSpawned` (+ flag `bound`, + repli si l'Instance existe déjà au Start).
**Dette technique Phase 2 (validée avec le user)** : le pattern « chaque UI attend LocalPlayerSpawned puis se bind aux singletons » marche mais répète du boilerplate. Version propre à faire plus tard : router les UI **uniquement via le bus statique `PlayerEvents`** (events `HealthChanged`/`HungerChanged`/`ThirstChanged`/`InventorySlotChanged`/`SelectedHotbarSlotChanged` existent déjà) au lieu des singletons `.Instance` + leurs `UnityEvent` d'instance → supprime tout le problème de timing. Option : classe de base `PlayerBoundUI` si on garde le pattern actuel.
**Refacto Crafting (couplage UI/manager, validé user)** : `CraftingUI` est devenue une vue 100% bête (reçoit des délégués `canCraft`/`countItem`/`craft` du manager, ne connaît plus ni `PlayerInventory` ni `CraftingManager`). `CraftingManager` est le contrôleur : possède `recipes` + une réf `craftingUI`, `Build()` la vue, gère l'attente joueur + refresh sur changement d'inventaire. Manip Unity : assigner `Crafting UI` sur le CraftingManager.
**Pickables en réseau (Phase 1)** : `Pickable` est passé en `NetworkBehaviour`. Ramassage **autoritaire serveur** : `Interact()``Pickup()``RequestPickupServerRpc(NetworkConnection conn)` (RequireOwnership=false) → le serveur résout l'inventaire du joueur via `conn.FirstObject.GetComponent<PlayerInventory>()` (PAS le singleton), `AddItem`, puis `Despawn()` (garde `taken` anti-double). `PlayerInventory.DropItem` spawne désormais le pickable via `InstanceFinder.ServerManager.Spawn` quand il est serveur (sinon warning + abort, Phase 2). Manips Unity : ajouter `NetworkObject` aux prefabs/instances Pickable + drop prefabs (`item.WorldPrefab`), et Refresh Default Prefabs pour les drops.
- Limite Phase 2 : pour un client distant, l'`AddItem` se fait côté serveur sur sa copie de `PlayerInventory` → ne remonte pas tant que l'inventaire n'est pas réseau ; le **type d'item** d'un drop runtime ne se réplique pas aux autres clients sans item-id SyncVar.
**Phase 2 — Inventaire réseau (approche CLIENT-AUTHORITATIVE, validée user)** : pas de SyncList serveur-auth (trop lourd + faudrait réseauter le crafting). À la place :
- `ItemDatabase` (Resources/ItemDatabase.asset) = registre `id ↔ ItemData`, rempli par **Tools ▸ Ashwild ▸ Rebuild Item Database** (`ItemDatabaseBuilder`). Nécessaire car on ne peut pas envoyer un ScriptableObject sur le réseau.
- `PlayerInventory` est passé `MonoBehaviour``NetworkBehaviour`. Slots restent **locaux** (tout le code crafting/use/hotbar inchangé). Singleton posé **uniquement pour l'owner** en `OnStartNetwork` (plus de clobber Awake destructeur). `GrantItemFromServer(item,qty)` (serveur) → `TargetGrantItem` (owner) → `AddItem` local. `DropItem``SpawnDropServerRpc` (serveur spawn le pickable). Ajout `CanFit()`.
- `Pickable` : `RequestPickupServerRpc` → serveur résout l'inventaire via `conn.FirstObject`, `GrantItemFromServer`, `Despawn`. SyncVar `syncItemId` pour que les drops s'affichent correctement chez les autres clients. Pré-check `CanFit` côté client avant de demander.
- Compromis assumé : inventaire pas anti-triche (ok coop entre amis).
**Pickups UNIFIÉS (validé user : un seul script pour tout)** : on ne peut PAS faire de chaque objet un NetworkObject (limite FishNet 254 imbriqués + trop lourd pour le scatter de masse). `Pickable` ET `ScatterPickup` SUPPRIMÉS, remplacés par UN seul `WorldPickup` + le registre :
- `WorldPickup` (MonoBehaviour, PAS réseau, IInteractable) : pour TOUT (scatter, posés main, drops). `id >= 0` = pickup de scène (baké) ; `id < 0` = drop runtime. `Interact``registry.RequestPickupServerRpc(id)`. `HideAsClaimed()`/`InitializeAsDrop()`.
- `WorldPickupRegistry` (NetworkBehaviour, 1 par scène, dans `Assets/GAME/Script/Network/`) : `SyncHashSet<int> pickedSceneIds` + `SyncDictionary<int,DropRecord> activeDrops`. `RequestPickupServerRpc(id)` (serveur résout l'item depuis sa copie de scène ou le record drop, `GrantItemFromServer`). `RequestDropServerRpc(itemId,qty,pos,rot)` (serveur ajoute à activeDrops). OnChange/OnStartClient → cache scène / instancie ou détruit les visuels de drop localement.
- `WorldPickupIdAssigner` (éditeur) : **Tools ▸ Ashwild ▸ Assign World Pickup IDs**.
- `PlayerInventory.DropItem``WorldPickupRegistry.Instance.RequestDropServerRpc(...)` (plus de SpawnDropServerRpc/Pickable).
- Conséquence : UN seul prefab par item suffit (le même `WorldPickup` sert au scatter ET de `ItemData.WorldPrefab` pour les drops).
- Renommé ensuite `WorldPickup``Pickable`, `WorldPickupRegistry``PickableRegistry`.
**Base partagée + Harvestables réseau (validé user : code clean, polymorphisme, pas de god-object)** :
- `WorldObject` (abstract MonoBehaviour) : base de tous les objets de monde locaux (id baké, `HideAsInactive(bool fresh)`, `ShowActive()`, `GetRegistry()` abstrait, `ShouldAutoRegister`, `SetId`). Dossier `Network/`.
- `WorldObjectRegistry` (abstract NetworkBehaviour, RequireComponent NetworkObject) : plomberie commune = `SyncHashSet<int> inactiveIds`, dict `registered`, catch-up OnStartClient, OnChange→hide/show, `MarkInactive/MarkActive`, `ResolveInventory`. `Awake/OnDestroy` virtuels (le singleton typé est posé par la sous-classe).
- `Pickable : WorldObject` + `PickableRegistry : WorldObjectRegistry` (ajoute `activeDrops` SyncDictionary + RPC pickup/drop).
- `Harvestable : WorldObject` : devient data+feedback (MaxHealth/CanRespawn/RespawnDelay/RollDrops/PlayHitEffect/PlayBlockedFeedback) ; vie/depletion/respawn RETIRÉS (→ registre). `HideAsInactive(fresh)` joue `onDepleted` seulement si `fresh`.
- `HarvestableRegistry : WorldObjectRegistry` : vie serveur (`Dictionary<int,float>`), `RequestHitServerRpc(id,damage,toolId,hitDir)` (dégâts client-auth, loot+état serveur-auth), `PlayHitObserversRpc` (feedback chez tous SAUF le frappeur via `LocalConnection.ClientId`), respawn coroutine.
- `ToolBehaviour.TryHarvest` : blocked→local ; sinon `PlayHitEffect` local immédiat + `HarvestableRegistry.Instance.RequestHitServerRpc(...)`.
- Outil unique **Tools ▸ Ashwild ▸ Assign World Object IDs** (`WorldObjectIdAssigner`, remplace PickableIdAssigner) : ids sur TOUS les `WorldObject` (Pickable+Harvestable).
- Risque à valider au 1er compile : SyncTypes répartis base/sous-classe NetworkBehaviour (héritage non générique — normalement OK ; repli = remettre les SyncTypes dans les sous-classes).
- Ennemis = hors registre (NetworkObjects mobiles, futur).
- Manips Unity : poser 1 `PickableRegistry` + 1 `HarvestableRegistry` dans TestScene ; `Pickable`/`Harvestable` sur les prefabs (id -1) ; (re)placer → Assign World Object IDs → save scene.
**✅ PlayerStats networké (FAIT, juin 2026)** : `PlayerStats` est passé `MonoBehaviour``NetworkBehaviour`, même pattern que PlayerInventory (client-authoritative). `Awake` destructeur SUPPRIMÉ ; `Instance` posé owner-only en `OnStartNetwork`, nettoyé en `OnStopNetwork`. Simulation (drain/dégâts/regen) inchangée mais gardée par `if (IsSpawned && !IsOwner) return;` (remotes ne simulent pas ; hors-ligne IsSpawned=false → tourne quand même pour les test scenes). Ajout SyncVars `netHealth/netHunger/netThirst/netDead` : l'owner pousse un snapshot via `PushStatsServerRpc` (throttle `replicationInterval`=0.2s + push immédiat sur dégâts/mort/revive) → les autres clients mirrorent via `OnChange` (met à jour `currentHealth` local + invoke les `UnityEvent` d'instance, JAMAIS le bus statique `PlayerEvents`). `OnStartServer` seed les SyncVars à max pour les late joiners. → barres de vie d'équipiers possibles plus tard en lisant `playerStats.HealthNormalized` sur le NetworkObject distant. Manip Unity : vérifier que `PlayerStats` est bien dans la liste `ownerOnlyBehaviours` du `PlayerNetworkController`, rouvrir Unity pour le codegen FishNet.
**Fix PlayerEvents (FAIT)** : ajout `ResetPlayerState()` appelé dans `RaiseSessionStopped` → reset des flags joueur-local statiques (IsDead/IsInventoryOpen/IsPaused/IsGrounded…) entre deux sessions (sinon mourir puis quitter laissait l'état collé).
**✅ Audit gating IsOwner (FAIT)** : le prefab `Player.prefab` confirme que `PlayerNetworkController.ownerOnlyBehaviours` contient TOUS les systèmes joueur (PlayerInputRouter, PlayerInput, PlayerLocomotion, PlayerCamera, PlayerToolHolder, **PlayerStats**, PlayerInventory, HotbarController, PlayerInteractor, PlayerAudio, **PlayerLifecycle**) et `ownerOnlyObjects` = Main Camera + AudioListener + Tools + Tools Camera. Donc les puppets distants sont passifs (désactivés). Gating solide. Held items (ToolBehaviour/ConsumableBehaviour) sous le holder Tools (désactivé sur remotes) → OK.
- **Seul trou trouvé + corrigé** : `DeathScreen.BindToPlayer` faisait `FindFirstObjectByType<PlayerLifecycle>()` → trouve aussi les Behaviours désactivés tant que leur GameObject est actif (les puppets distants le restent) → pouvait respawn le mauvais joueur. Remplacé par `PlayerStats.Instance.GetComponent<PlayerLifecycle>()` (joueur local garanti).
- Latent mineur restant : `PlayerInventory.Start` fait `FindAnyObjectByType<InventoryUI>()` (UI de scène unique, pas un objet joueur → OK mais fragile). Limite rejoin : `DeathScreen`/UI gardent `bound=true` après 1er spawn → pas de rebind si le joueur local despawn/respawn dans la même session.
**Coquille corrigée** : en réécrivant `PlayerStats`, j'avais redéclaré `public enum DamageType` en bas du fichier → CS0101 (il vit déjà en bas de `PlayerEvents.cs`). Retiré.
**Manips Unity restantes** : prefab→NetworkObject+NetworkTransform (déjà sur le prefab : NetworkObject + NetworkTransform clientAuthoritative présents ✓), NetworkManager dans MenuScene, retirer le joueur en dur de GameScene, PlayerSpawnPoint, enregistrer le prefab dans DefaultPrefabObjects. Rouvrir Unity pour le codegen FishNet (PlayerStats devenu NetworkBehaviour).
@@ -0,0 +1,17 @@
---
name: refacto-namespaces-asmdef
description: Refacto archi différé — introduire des namespaces + asmdef (à faire dans une session dédiée)
metadata:
type: project
---
**À FAIRE dans une session dédiée, PAS au milieu de la migration réseau.** Décidé juin 2026 avec le user.
Aujourd'hui tout le code C# du jeu est en **global namespace** (un seul `Assembly-CSharp`). Conséquences : risque de collisions de types (ex. le `CS0101` sur `DamageType` rencontré en migrant `PlayerStats`), dépendances pas explicites, compile plus lente.
Plan visé quand le **coop sera fonctionnel de bout en bout** (donc APRÈS la migration réseau — sinon double churn, chaque fichier networké serait re-touché) :
1. **Namespaces** par domaine, alignés sur le folder map : `Ashwild.Player`, `Ashwild.Network`, `Ashwild.Inventory`, `Ashwild.Crafting`, `Ashwild.Cooking`, `Ashwild.Harvesting`, `Ashwild.Audio`, `Ashwild.Environment`, `Ashwild.UI`, `Ashwild.Settings`, etc.
2. **asmdef par dossier** (compile incrémentale + dépendances forcées propres). Attention aux refs FishNet / InputSystem dans chaque asmdef, et `Editor/` en asmdef éditeur séparé.
Pourquoi différé : refacto transverse (touche tous les fichiers) qui ne fait pas avancer le jeu jouable ; à faire en UNE passe propre une fois l'archi réseau stabilisée. Voir [[multiplayer-migration]] pour l'ordre des migrations restantes (CookingStation → GrassClearer → SeasonManager).