Inventory: per-player items backbone (DR-026 Phase 0)
Data-driven ItemDatabase catalog + per-player replicated InventorySlot ([GhostField] OwnerSendType.All, a StatModifier twin). Harvest reroutes to the firing player's personal inventory (optional ComponentLookup<GhostOwner> in ResourceHarvestSystem; remainder/un-owned -> ledger); the G-key InventoryDepositRequest RPC moves the bag into the shared ledger the build economy spends. Catalog asset (Aether/Ore/Biomass + Stone Pickaxe) wired into the Gameplay subscene; read-only HUD inventory panel. ushort ItemId subsumes ResourceId; byte Category/Tier baked for gear-tier progression. Session-only (no SaveData bump). Play-validated host+client: catalog baked into both worlds, the re-baked player ghost carries InventorySlot with a clean handshake, a server write replicates to the client owner, the deposit RPC round-trips, and the HUD renders catalog names. See DR-026. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-authoritative handler for <see cref="InventoryDepositRequest"/> RPCs: moves items from the
|
||||
/// sender's PERSONAL <see cref="InventorySlot"/> inventory into the shared base stockpile (the global
|
||||
/// <see cref="ResourceLedger"/> the build/upgrade/automation economy spends from). Resolves the sender's
|
||||
/// player (SourceConnection -> NetworkId -> GhostOwner) via the AbilityUpgradeSystem owner-map idiom,
|
||||
/// then withdraws from the player's inventory and deposits into the ledger IN-PLACE (buffer mutation is not
|
||||
/// a structural change). <c>ItemId == 0</c> ("deposit all") is handled BEFORE any per-item withdraw and
|
||||
/// never writes a 0-id row. Resolves the ledger via <c>GetSingletonEntity<ResourceLedger>()</c> then
|
||||
/// <c>GetBuffer<StorageEntry>()</c> — NEVER <c>GetSingleton<StorageEntry></c> (the base
|
||||
/// container owns a second StorageEntry buffer). Plain server SimulationSystemGroup (not predicted, so the
|
||||
/// effect applies exactly once — no rollback double-apply).
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct InventoryDepositSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<ResourceLedger>();
|
||||
var builder = new EntityQueryBuilder(Allocator.Temp)
|
||||
.WithAll<InventoryDepositRequest, ReceiveRpcCommandRequest>();
|
||||
state.RequireForUpdate(state.GetEntityQuery(builder));
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var ledger = SystemAPI.GetBuffer<StorageEntry>(SystemAPI.GetSingletonEntity<ResourceLedger>());
|
||||
|
||||
var playerByConn = new NativeHashMap<int, Entity>(8, Allocator.Temp);
|
||||
foreach (var (owner, entity) in
|
||||
SystemAPI.Query<RefRO<GhostOwner>>().WithAll<PlayerTag, InventorySlot>().WithEntityAccess())
|
||||
playerByConn[owner.ValueRO.NetworkId] = entity;
|
||||
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (request, receive, requestEntity) in
|
||||
SystemAPI.Query<RefRO<InventoryDepositRequest>, RefRO<ReceiveRpcCommandRequest>>().WithEntityAccess())
|
||||
{
|
||||
var conn = receive.ValueRO.SourceConnection;
|
||||
if (SystemAPI.HasComponent<NetworkId>(conn)
|
||||
&& playerByConn.TryGetValue(SystemAPI.GetComponent<NetworkId>(conn).Value, out var player))
|
||||
{
|
||||
var inv = SystemAPI.GetBuffer<InventorySlot>(player);
|
||||
var req = request.ValueRO;
|
||||
|
||||
if (req.ItemId == 0)
|
||||
{
|
||||
// Deposit EVERYTHING: drain each non-empty stack into the ledger, then clear the bag.
|
||||
for (int i = 0; i < inv.Length; i++)
|
||||
{
|
||||
var slot = inv[i];
|
||||
if (slot.ItemId != 0 && slot.Count > 0)
|
||||
StorageMath.Deposit(ledger, slot.ItemId, slot.Count);
|
||||
}
|
||||
inv.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Count <= 0 means "all of that item"; Withdraw clamps to what is available.
|
||||
int want = req.Count <= 0 ? int.MaxValue : req.Count;
|
||||
int moved = InventoryMath.Withdraw(inv, req.ItemId, want);
|
||||
if (moved > 0)
|
||||
StorageMath.Deposit(ledger, req.ItemId, moved);
|
||||
}
|
||||
}
|
||||
|
||||
ecb.DestroyEntity(requestEntity);
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
playerByConn.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user