Merge remote-tracking branch 'origin/feat/inport-props' into feat/craft-discovery

# Conflicts:
#	Assets/External/Animated PBR Chest Demo/Materials/WoodChest.mat
#	Packages/com.distantlands.cozy.core/Content/Integration/Import for BiRP.unitypackage.meta
#	Packages/com.distantlands.cozy.core/Content/Integration/Import for HDRP.unitypackage.meta
#	Packages/com.distantlands.cozy.core/Content/Integration/Import for URP.unitypackage.meta
This commit is contained in:
2026-07-25 19:42:26 +02:00
954 changed files with 999772 additions and 26193 deletions
@@ -4,6 +4,7 @@ using FishNet.Object;
using FishNet.Object.Synchronizing;
using UnityEngine;
using Ashwild.Inventory;
using Ashwild.Player;
namespace Ashwild.Network
{
@@ -74,15 +75,44 @@ namespace Ashwild.Network
/// <summary>
/// Registers a world object, hiding it immediately if it is already inactive.
///
/// Rejects — loudly — an object whose id is unusable, because both failure modes are otherwise
/// invisible at runtime and produce the exact same symptom: the interaction plays its local
/// feedback and grants nothing. A scene object left at id -1 (the id tool was never run on it, or
/// its prefab carries a baked id) would be treated as a runtime drop; two objects sharing an id
/// would overwrite each other here, so claiming one silently kills the other for good. The object
/// stays unregistered, which is what makes the interaction refuse itself instead of failing later.
/// </summary>
public void RegisterObject(WorldObject obj)
{
if (obj == null) return;
if (obj.Id < 0)
{
Debug.LogError($"[{GetType().Name}] '{obj.name}' has no baked id (id = {obj.Id}) and cannot be " +
"tracked — it will not be interactable. Run Tools ▸ Ashwild ▸ Setup World Object IDs and save the scene.", obj);
return;
}
if (registered.TryGetValue(obj.Id, out WorldObject existing) && existing != null && existing != obj)
{
Debug.LogError($"[{GetType().Name}] Duplicate world object id {obj.Id}: '{obj.name}' collides with " +
$"'{existing.name}'. Claiming one would silently disable the other. Run Tools ▸ Ashwild ▸ Setup World Object IDs.", obj);
return;
}
registered[obj.Id] = obj;
if (inactiveIds.Contains(obj.Id))
obj.HideAsInactive(false);
}
/// <summary>
/// Returns whether this id is tracked by the registry on this client. Interactions check it before
/// asking the server, so an object the registry never accepted (bad or duplicate id) refuses up
/// front rather than playing its feedback and losing the request server-side.
/// </summary>
public bool IsRegistered(int id) => registered.ContainsKey(id);
/// <summary>
/// Returns whether the object with this id has been removed from the world.
/// </summary>
@@ -114,12 +144,35 @@ namespace Ashwild.Network
}
/// <summary>
/// Returns the PlayerInventory on the player object owned by the given connection.
/// Returns the PlayerInventory owned by the given connection. Delegates to the shared lookup,
/// which scans the connection's objects instead of trusting FirstObject (see NetworkPlayerLookup).
/// </summary>
protected PlayerInventory ResolveInventory(NetworkConnection conn)
protected PlayerInventory ResolveInventory(NetworkConnection conn) => NetworkPlayerLookup.ResolveInventory(conn);
/// <summary>
/// Server-side: reports an interaction the server could not carry out for a reason the client had
/// no way to foresee (an id it does not track, a player object it cannot resolve). It logs on the
/// server and tells the requesting player, because the alternative — the historic behaviour — is a
/// bare `return` that leaves the player watching a hit that gave nothing, with the only trace of it
/// in the host's console. Normal races (someone else took it first) are not reported here: the
/// object visibly disappears, which is explanation enough.
/// </summary>
protected void ReportFailure(NetworkConnection conn, int id, string reason)
{
NetworkObject playerObject = conn != null ? conn.FirstObject : null;
return playerObject != null ? playerObject.GetComponent<PlayerInventory>() : null;
Debug.LogWarning($"[{GetType().Name}] Interaction on id {id} refused for client " +
$"{(conn != null ? conn.ClientId : -1)}: {reason}", this);
TargetReportFailure(conn, id, reason);
}
/// <summary>
/// Runs on the requesting client: surfaces the refusal in the HUD and logs it locally, so the
/// player who actually experienced it sees the diagnosis in their own console.
/// </summary>
[TargetRpc]
private void TargetReportFailure(NetworkConnection conn, int id, string reason)
{
Debug.LogError($"[{GetType().Name}] The server refused the interaction on id {id}: {reason}", this);
PlayerEvents.RaiseInteractionRefused("Interaction failed");
}
#endregion