(Init) Setting
This commit is contained in:
@@ -0,0 +1,371 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Guidance for working in this Unity project. **These rules are mandatory** — follow them
|
||||
exactly when reading, writing, or refactoring any C# script.
|
||||
|
||||
---
|
||||
|
||||
## 1. Project overview
|
||||
|
||||
First-person survival/exploration game built in **Unity 6000.3.8f1** with **URP**.
|
||||
The game is **online co-op multiplayer** (a single-player build is just a host with no
|
||||
clients — there is no separate offline mode). Core systems already in place: first-person
|
||||
locomotion, camera, stats (health/hunger/thirst), inventory + hotbar, crafting, cooking,
|
||||
harvesting/tools, procedural scatter placement, audio (footsteps by surface + music),
|
||||
seasons, and a full UI/menu/settings stack.
|
||||
|
||||
**Networking stack** (see §3):
|
||||
- **FishNet 4.7.2** (in `Assets/FishNet/`) — the netcode.
|
||||
- **Steamworks.NET** + **FishySteamworks** transport — Steam relay (SteamNetworkingSockets,
|
||||
no port forwarding). Sessions are joined by a shareable code (host SteamID64) or Steam invite.
|
||||
|
||||
Context for current work:
|
||||
- **This is final, production-quality code — not a prototype.** Aim for clean, well-named,
|
||||
well-architected code that respects the patterns in this document. No quick hacks,
|
||||
no "temporary" shortcuts; if something is worth doing, do it properly.
|
||||
- **Multiplayer co-op is a hard constraint** — when touching any player, world or shared
|
||||
system, assume two+ players exist (see §3).
|
||||
- **No player arms / viewmodel** — feedback comes from camera, audio and UI only.
|
||||
- Animations use **DOTween** (tweens, not Animator) wherever possible.
|
||||
- Shaders are **hand-written HLSL/ShaderLab** — do not propose Shader Graph.
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture — the golden rule
|
||||
|
||||
**Everything talks through the static event bus `PlayerEvents`**
|
||||
(`Assets/GAME/Script/Player/Events/PlayerEvents.cs`).
|
||||
|
||||
- `PlayerEvents` is the source of truth for the **local** player's cross-system state
|
||||
(`IsDead`, `IsGrounded`, `IsInventoryOpen`, `IsPaused`, `InputLocked`, …). Here "shared"
|
||||
means shared *between systems on one client* — never *between players* (see §3).
|
||||
- Producers call a `Raise*` method; the raiser updates the matching `IsX` state, logs
|
||||
(under the `PLAYER_EVENT_LOG` define) and invokes the event.
|
||||
- Consumers subscribe to the `event` in `OnEnable` and **unsubscribe in `OnDisable`**.
|
||||
- **Never** reach across systems with `FindObjectOfType` or hard references to read state
|
||||
that the bus already exposes. Add a new event/state to `PlayerEvents` instead — **except**
|
||||
per-player networked state, which belongs on a `NetworkBehaviour` (SyncVar/RPC), not the
|
||||
bus (see §3).
|
||||
|
||||
**Each script owns one concern.** A script reads its own inputs/state from the bus,
|
||||
does its job, and raises events for others. Do not let a script mutate another system's
|
||||
internal data directly. If two systems need to coordinate, route it through `PlayerEvents`.
|
||||
|
||||
Data that is authored in the editor lives in **ScriptableObjects** (`ItemData`,
|
||||
`CraftingRecipe`, `SurfaceSoundProfile`, `ScatterProfile`, `MusicPlaylist`,
|
||||
`ToolDropTable`, …). Logic lives in `MonoBehaviour`s. Keep them separate.
|
||||
|
||||
Singletons exist where a single instance is genuinely global (`PlayerInventory.Instance`,
|
||||
`PlayerStats.Instance`, …); guard every access for `null` — they may not exist yet or in
|
||||
every scene.
|
||||
|
||||
---
|
||||
|
||||
## 3. Multiplayer — the second golden rule
|
||||
|
||||
The game runs on **FishNet** (co-op, **client-authoritative** — friendly play, *not*
|
||||
anti-cheat). Networking code lives in `Assets/GAME/Script/Network/`. When you write or
|
||||
change anything that involves a player, the world, or shared state, obey these rules.
|
||||
|
||||
### `PlayerEvents` is the LOCAL player's bus only
|
||||
|
||||
The static `PlayerEvents` (state + events) represents **the local owned player**, never
|
||||
remote players. Only owner-side code may call `Raise*`. Remote player copies must **never**
|
||||
drive the static bus — they only update their own instance state (e.g. via SyncVar hooks).
|
||||
Per-player networked state (health, position, …) belongs on the player's `NetworkBehaviour`
|
||||
(SyncVar / RPC), not in `PlayerEvents`.
|
||||
|
||||
### Owner gating
|
||||
|
||||
`PlayerNetworkController` (on the player root) disables every owner-only component
|
||||
(input, locomotion, camera, tools, **stats**, inventory, hotbar, interactor, audio,
|
||||
lifecycle) and owner-only object (cameras, audio listener, tool holder) on non-owners, then
|
||||
raises `LocalPlayerSpawned`. **Any new player component that simulates or reads local input
|
||||
must be added to that `ownerOnlyBehaviours` list.** Scene/UI code must bind to the *local*
|
||||
player — get it from `PlayerInventory.Instance` / `PlayerStats.Instance` (owner-only
|
||||
singletons), **never** `FindObjectOfType<PlayerX>()`, which also matches disabled remote
|
||||
puppets (their root GameObject stays active) and targets the wrong player.
|
||||
|
||||
### Networking a player system — the canonical pattern (mirror `PlayerInventory`/`PlayerStats`)
|
||||
|
||||
- Make it a `NetworkBehaviour`. **No destructive `Awake` singleton guard** (it would destroy
|
||||
spawned remote players). Set the `Instance` only for the owner, in `OnStartNetwork`
|
||||
(`if (base.Owner.IsLocalClient) Instance = this;`), clear it in `OnStopNetwork`.
|
||||
- Owner simulates locally; replicate a lightweight snapshot via SyncVar (server writes,
|
||||
pushed from the owner through a `[ServerRpc]`, throttled + immediate on key events).
|
||||
- SyncVar `OnChange` updates remote copies' local state only — guard `if (base.IsOwner) return;`
|
||||
and never touch `PlayerEvents` there.
|
||||
|
||||
### World state
|
||||
|
||||
World objects (pickables, harvestables) are **not** NetworkObjects. They use the
|
||||
`WorldObject` + `WorldObjectRegistry` pattern: one networked registry per type per scene
|
||||
owns a synced set of inactive ids + server-side authority (health, loot, drops) via RPC.
|
||||
Add new harvestable/pickable-like world state here, not as per-object NetworkObjects.
|
||||
`ItemData` cannot cross the wire — map items to ids through `ItemDatabase`
|
||||
(**Tools ▸ Ashwild ▸ Rebuild Item Database** after adding items).
|
||||
|
||||
### Shared interactables (cooking stations, future chests/benches)
|
||||
|
||||
Anything two players can use at once needs server-authoritative state + RPCs — local-only
|
||||
MonoBehaviour state will let players clobber each other. `CookingStation` is the reference:
|
||||
the server owns the simulation (fuel burn + cooking timers), replicates a compact view
|
||||
(`SyncList<SlotView>` + a `SyncVar<int>` log count), and mutates only through
|
||||
`[ServerRpc(RequireOwnership = false)]` requests that refund the item on failure via
|
||||
`PlayerInventory.GrantItemFromServer`. Mirror it for chests/benches.
|
||||
|
||||
### Not yet networked — do not rely on these in co-op
|
||||
|
||||
These still run locally and will desync between clients until migrated:
|
||||
|
||||
- **`GrassClearer`** — modifies the terrain detail layer locally, no RPC → others keep the grass.
|
||||
- **`SeasonManager`** — the global `_Season` shader value is not synced → visual desync.
|
||||
|
||||
### Reference implementations
|
||||
|
||||
Copy these patterns rather than inventing new ones: `PlayerInventory.cs` and `PlayerStats.cs`
|
||||
(networked player system), `HarvestableRegistry.cs` / `WorldObjectRegistry.cs` (world state),
|
||||
`CookingStation.cs` (server-authoritative shared interactable), `PlayerNetworkController.cs`
|
||||
(owner gating).
|
||||
|
||||
---
|
||||
|
||||
## 4. Folder map (`Assets/GAME/Script/`)
|
||||
|
||||
| Folder | Namespace | Responsibility |
|
||||
| --- | --- | --- |
|
||||
| `Player/Events/` | `Ashwild.Player` | `PlayerEvents` — the central bus. Start here to understand the game. |
|
||||
| `Player/Input/` | `Ashwild.Player` | `PlayerInputRouter` — the **only** Input System receiver. |
|
||||
| `Player/Locomotion/` | `Ashwild.Player` | Movement, ground/slope checks, jump, slide, crouch. |
|
||||
| `Player/Camera/` | `Ashwild.Player` | First-person camera, look, head height. |
|
||||
| `Player/Tools/` | `Ashwild.Player` | Tool equip/swing logic and tool holder. |
|
||||
| `Player/` (root) | `Ashwild.Player` | `PlayerStats`, `PlayerInteractor`, `PlayerLifecycle`, `PlayerAudio`. |
|
||||
| `Network/` | `Ashwild.Network` | FishNet layer — session host/join, player spawn, owner gating, `WorldObject`/registry world-state sync. |
|
||||
| `Inventory/` | `Ashwild.Inventory` | Inventory model, slots, hotbar, pickables, inventory UI. |
|
||||
| `Crafting/` | `Ashwild.Crafting` | Recipes, ingredients, crafting manager and its UI. |
|
||||
| `Cooking/` | `Ashwild.Cooking` | Cooking stations (food + fuel → cooked results). |
|
||||
| `Harvesting/` | `Ashwild.Harvesting` | Harvestables, tools, resource drops, scatter placement. |
|
||||
| `Audio/` | `Ashwild.Audio` | Music manager/playlist, surface sound profiles. |
|
||||
| `Environment/` | `Ashwild.Environment` | Season manager. |
|
||||
| `GrassClearer/` | `Ashwild.GrassClearer` | Detail/grass removal around placed objects. |
|
||||
| `Interaction/` | `Ashwild.Interaction` | `IInteractable` and interaction contracts. |
|
||||
| `Settings/` | `Ashwild.Settings` | Settings manager, panels and sub-panels, brightness. |
|
||||
| `UI/` | `Ashwild.UI` | HUD, panels, notifications, crosshair, death/splash/pause screens. |
|
||||
| `Integrations/` | `Ashwild.Integrations` | Third-party integrations (e.g. Discord Rich Presence). |
|
||||
| `Editor/` | `Ashwild.EditorTools` | Custom inspectors, property drawers, tooling (editor-only). |
|
||||
|
||||
### Namespaces — one per top-level system folder
|
||||
|
||||
Every script lives in a namespace **`Ashwild.<TopLevelFolder>`** — flat, one per
|
||||
top-level system folder. Sub-folders do **not** get their own namespace: everything under
|
||||
`Player/` (incl. `Player/Camera/`, `Player/Input/`, `Player/Tools/`, `Player/HeldItems/`)
|
||||
is `Ashwild.Player`. This flatness is deliberate — it keeps cross-references short and
|
||||
avoids a sub-namespace shadowing a Unity type (a namespace `Ashwild.Player.Camera` would
|
||||
mask `UnityEngine.Camera` inside it).
|
||||
|
||||
Rules when adding or moving a script:
|
||||
|
||||
- New file → wrap it in the namespace of its top-level folder. Add `using Ashwild.X;` for
|
||||
every other Ashwild namespace whose types it references.
|
||||
- **`Editor/` is the one exception to the `Ashwild.<Folder>` rule** — its namespace is
|
||||
**`Ashwild.EditorTools`**, *not* `Ashwild.Editor`, because a namespace segment named
|
||||
`Editor` shadows the `UnityEditor.Editor` base type that every custom inspector derives
|
||||
from (`CS0118`).
|
||||
- **Never** put `using Ashwild.EditorTools;` in a runtime (non-`Editor/`) script. Editor
|
||||
code compiles into the separate `Assembly-CSharp-Editor` assembly, which runtime code
|
||||
cannot reference (`CS0234`). Editor scripts may freely `using` runtime namespaces, not
|
||||
the reverse.
|
||||
|
||||
---
|
||||
|
||||
## 5. Code conventions — non-negotiable
|
||||
|
||||
### 5.1 XML summaries
|
||||
Every **class, method, and non-obvious public member** gets a `/// <summary>` written in
|
||||
**English**, describing intent (the *why*), not restating the signature.
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Adds an item to the inventory, stacking into existing slots first.
|
||||
/// Returns false (and logs) when the item is null or no room remains.
|
||||
/// </summary>
|
||||
public bool AddItem(ItemData item, int quantity = 1) { ... }
|
||||
```
|
||||
|
||||
**Use the 3-line form** — opening `/// <summary>`, the text, and the closing
|
||||
`/// </summary>` each on their own line; never collapse a summary onto one line.
|
||||
|
||||
Scope (this matches the real codebase — see `PlayerInventory`/`PlayerStats`):
|
||||
- **Always:** every class, every method (incl. Unity lifecycle and event handlers), and any
|
||||
public member whose intent isn't obvious from its name.
|
||||
- **Not required:** trivial getters / `=>` passthroughs and `[SerializeField]` fields — group
|
||||
the latter under `[Header("...")]` and clarify individual ones with `[Tooltip("...")]`
|
||||
(or a summary) only when the name alone doesn't convey intent.
|
||||
|
||||
### 5.1b No in-body comments — explain in the summary above the method
|
||||
**Never write explanatory comments *inside* a method body.** All rationale — the *why*, the
|
||||
gotchas, the edge cases — goes in the `/// <summary>` above the method (extend it with as much
|
||||
prose as needed). The body stays pure code. If a block feels like it needs an inline comment to
|
||||
be understood, that's a signal to either name things better or extract it into its own
|
||||
well-summarized method. The only inline text allowed in a body is a `// TODO:` / `// HACK:`
|
||||
marker for tracked future work — not an explanation of what the current code does.
|
||||
|
||||
```csharp
|
||||
// ❌ wrong — explanation lives inside the body
|
||||
private void Step()
|
||||
{
|
||||
// Subtract instead of zeroing so the leftover carries into the next step.
|
||||
distanceTravelled -= stepDistance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires a footstep and carries the leftover distance into the next step. Subtracting
|
||||
/// (instead of zeroing) keeps the cadence even at sprint speed, where the per-frame
|
||||
/// overshoot would otherwise randomly stretch steps and drift out of sync.
|
||||
/// </summary>
|
||||
private void Step()
|
||||
{
|
||||
distanceTravelled -= stepDistance;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Specific `#region`s — never one generic blob
|
||||
Group members into **named, specific** regions reflecting their role. Do **not** dump
|
||||
everything under a single `#region Methods`. Typical regions for a MonoBehaviour:
|
||||
|
||||
```csharp
|
||||
#region Serialized Fields
|
||||
#region State
|
||||
#region Unity Lifecycle // Awake / OnEnable / OnDisable / Start / Update
|
||||
#region Event Handlers // bus callbacks
|
||||
#region Public API
|
||||
#region Internal Helpers
|
||||
#endregion
|
||||
```
|
||||
|
||||
Pick regions that match what the script actually does (e.g. `#region Ground Check`,
|
||||
`#region Surface Detection`, `#region Playback`). The goal: a reader scans the region
|
||||
names and knows the script's shape instantly.
|
||||
|
||||
### 5.3 Event subscription lifecycle
|
||||
Subscribe in `OnEnable`, unsubscribe in `OnDisable` — always paired, same order.
|
||||
Never subscribe without a matching unsubscribe (leaks + double-fire after reload).
|
||||
|
||||
### 5.4 DOTween hygiene
|
||||
Cache tweeners in fields and `Kill()` them in `OnDestroy` (and before restarting them).
|
||||
A tween targeting a destroyed object throws — never leave one running on teardown.
|
||||
|
||||
### 5.5 Defensive logging instead of crashes
|
||||
Guard nullable inputs and log a clear, prefixed error with the **context object** so
|
||||
clicking the console message selects the culprit in the hierarchy:
|
||||
|
||||
```csharp
|
||||
if (itemData == null)
|
||||
{
|
||||
Debug.LogError($"[Pickable] '{name}' has no ItemData assigned — cannot pick up.", this);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
Prefix logs with `[ClassName]`. Pass `this` as the second arg whenever possible.
|
||||
|
||||
### 5.6 Input naming — avoid the `On<Action>` trap
|
||||
The `PlayerInput` component uses **Send Messages**, so Unity invokes `On<ActionName>`
|
||||
methods by reflection on the player's GameObject and its components.
|
||||
**Only `PlayerInputRouter` may declare `On<Action>` methods.** Any other component with a
|
||||
method named like an input action (e.g. `OnHotbarScroll`) will be invoked by the Input
|
||||
System and crash on a signature mismatch. Name bus handlers `Handle<Thing>` instead.
|
||||
|
||||
### 5.7 General practices
|
||||
- `private` fields exposed to the inspector use `[SerializeField] private`, grouped under
|
||||
`[Header("...")]`. No public fields just to show them in the inspector.
|
||||
- One class per file; file name matches the type.
|
||||
- Prefer `OnEnable/OnDisable` over `Start/OnDestroy` for subscription symmetry.
|
||||
- No magic numbers in logic that designers tune — promote them to serialized fields.
|
||||
- Don't add `FindObjectOfType` in hot paths (`Update`/`FixedUpdate`); cache references.
|
||||
- Keep `Update`/`FixedUpdate` cheap; cache expensive lookups (see `PlayerAudio`'s surface
|
||||
cache for the pattern).
|
||||
|
||||
---
|
||||
|
||||
## 6. Canonical script template
|
||||
|
||||
```csharp
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// One-sentence description of this component's single responsibility.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class ExampleSystem : MonoBehaviour
|
||||
{
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("Tuning")]
|
||||
[SerializeField] private float duration = 1f;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
private bool isActive;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the bus events this system reacts to.
|
||||
/// </summary>
|
||||
private void OnEnable()
|
||||
{
|
||||
PlayerEvents.Died += HandleDied;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes — must mirror OnEnable exactly.
|
||||
/// </summary>
|
||||
private void OnDisable()
|
||||
{
|
||||
PlayerEvents.Died -= HandleDied;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
/// <summary>
|
||||
/// Resets the system when the player dies.
|
||||
/// </summary>
|
||||
private void HandleDied() => isActive = false;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Starts the effect; ignored if already running.
|
||||
/// </summary>
|
||||
public void Activate()
|
||||
{
|
||||
if (isActive) return;
|
||||
isActive = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Before you finish
|
||||
|
||||
- New cross-system communication → add an event to `PlayerEvents`, don't bypass the bus.
|
||||
- Touched a `MonoBehaviour` that subscribes? Verify `OnEnable`/`OnDisable` stay paired.
|
||||
- Touched a player system? Confirm it's owner-gated (in `ownerOnlyBehaviours`) and that a
|
||||
remote copy never drives `PlayerEvents` or simulates itself (see §3).
|
||||
- New networked prefab? Add a `NetworkObject` and register it in `DefaultPrefabObjects`.
|
||||
- Added a tween? Verify it's killed on teardown.
|
||||
- The project must compile with **zero errors**; treat new warnings as something to fix
|
||||
or justify.
|
||||
- Editor-only code stays under `Editor/`.
|
||||
@@ -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).
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(cd \"d:/Projet/Perso/Unity/FpsPlayer\" && echo \"=== fileIDs in m_AddedComponents ===\" && grep \"addedObject:\" Assets/GAME/Prefabs/Player/Player.prefab | grep -oE \"[0-9]+\" | sort -u > /tmp/added.txt && cat /tmp/added.txt && echo && echo \"=== fileIDs of MonoBehaviour/Component blocks ===\" && grep -E \"^--- !u!\\(114|54|136|81|82|20\\) &\" Assets/GAME/Prefabs/Player/Player.prefab | grep -oE \"&[0-9]+\" | tr -d '&' | sort -u > /tmp/blocks.txt && cat /tmp/blocks.txt && echo && echo \"=== blocks present but not in m_AddedComponents \\(need parent component check\\) ===\" && comm -23 /tmp/blocks.txt /tmp/added.txt)",
|
||||
"Read(//tmp/**)",
|
||||
"PowerShell(Rename-Item \"d:\\\\Projet\\\\Perso\\\\Unity\\\\FpsPlayer\\\\Assets\\\\GAME\\\\Script\\\\Inventory\\\\ItemPickup.cs\" \"Pickable.cs\")",
|
||||
"PowerShell(Rename-Item \"d:\\\\Projet\\\\Perso\\\\Unity\\\\FpsPlayer\\\\Assets\\\\GAME\\\\Script\\\\Inventory\\\\ItemPickup.cs.meta\" \"Pickable.cs.meta\")",
|
||||
"PowerShell(\"renamed OK\")",
|
||||
"WebFetch(domain:gitlab.com)",
|
||||
"Bash(grep -rl \"5741570150184284c9dd1dd33c459e72\" Assets --include=*.unity --include=*.prefab --include=*.asset)",
|
||||
"Bash(grep -rl \"83c5938a71ea3b34e93b9ede7be0ccbf\" Assets --include=*.unity --include=*.prefab --include=*.asset)"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user