Init Homebase
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6928b42621b979b478f0ae236c575df8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,61 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Authoring for the baked <see cref="BaseAnchor"/> singleton — the shared home base's fixed anchor
|
||||
/// and planar build-grid coordinate space. Place once in the gameplay subscene; the GameObject's
|
||||
/// position is the plot center (and the player spawn-ring center). The baker derives GridOrigin (the
|
||||
/// min-XZ corner of cell (0,0)) so the plot is centered on the anchor. The entity carries no transform
|
||||
/// (TransformUsageFlags.None) — the position is baked as flat data. Present identically on client and
|
||||
/// server (baked, not replicated). Draws the plot footprint as an editor gizmo when selected.
|
||||
/// </summary>
|
||||
public class BaseAnchorAuthoring : MonoBehaviour
|
||||
{
|
||||
[Min(0.01f)]
|
||||
[Tooltip("World-unit size of one square build-grid cell.")]
|
||||
public float CellSize = 1f;
|
||||
|
||||
[Min(1)]
|
||||
[Tooltip("Build-grid extent in cells along X and Z (square plot).")]
|
||||
public int PlotSize = 32;
|
||||
|
||||
private class BaseAnchorBaker : Baker<BaseAnchorAuthoring>
|
||||
{
|
||||
public override void Bake(BaseAnchorAuthoring authoring)
|
||||
{
|
||||
// Data singleton: no transform on the entity, but we read the GameObject position as data.
|
||||
var entity = GetEntity(authoring, TransformUsageFlags.None);
|
||||
|
||||
float3 anchorPos = authoring.transform.position;
|
||||
var dims = new int2(authoring.PlotSize, authoring.PlotSize);
|
||||
float cell = authoring.CellSize;
|
||||
var gridOrigin = new float3(
|
||||
anchorPos.x - dims.x * cell * 0.5f,
|
||||
anchorPos.y,
|
||||
anchorPos.z - dims.y * cell * 0.5f);
|
||||
|
||||
AddComponent(entity, new BaseAnchor
|
||||
{
|
||||
AnchorPos = anchorPos,
|
||||
GridOrigin = gridOrigin,
|
||||
CellSize = cell,
|
||||
GridDims = dims,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// Editor-only footprint gizmo: draws the claimed plot bounds on the base plane when selected.
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
float extent = CellSize * PlotSize;
|
||||
Gizmos.color = new Color(0.2f, 0.8f, 1f, 0.9f);
|
||||
Gizmos.DrawWireCube(transform.position, new Vector3(extent, 0.05f, extent));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b53184727e358b4eb27f68ae25504d8
|
||||
@@ -0,0 +1,32 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Authoring for the shared storage-container ghost prefab: an ownerless INTERPOLATED ghost whose
|
||||
/// replicated <see cref="StorageEntry"/> buffer is the shared inventory any player deposits into /
|
||||
/// withdraws from (server-authoritative, applied by StorageOpReceiveSystem). Add a
|
||||
/// GhostAuthoringComponent (Interpolated) to the prefab so clients see its contents replicate.
|
||||
/// <c>GetEntity(TransformUsageFlags.Dynamic)</c> gives it a runtime world transform, set at spawn to
|
||||
/// the base cell center.
|
||||
/// </summary>
|
||||
public class SharedStorageContainerAuthoring : MonoBehaviour
|
||||
{
|
||||
[Min(0f)]
|
||||
[Tooltip("Interaction radius (world units) for the deposit/withdraw test; reserved for proximity gating.")]
|
||||
public float InteractRadius = 2f;
|
||||
|
||||
private class SharedStorageContainerBaker : Baker<SharedStorageContainerAuthoring>
|
||||
{
|
||||
public override void Bake(SharedStorageContainerAuthoring authoring)
|
||||
{
|
||||
var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
|
||||
AddComponent<SharedStorageContainer>(entity);
|
||||
AddComponent(entity, new HitRadius { Value = authoring.InteractRadius });
|
||||
AddBuffer<StorageEntry>(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8cc6285a9b8958a47a61a07550b7f792
|
||||
@@ -0,0 +1,38 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Authoring for the baked <see cref="StorageSpawner"/> singleton (mirrors UpgradePickupSpawnerAuthoring).
|
||||
/// Place once in the gameplay subscene; the server-only SharedStorageSpawnSystem reads it, instantiates
|
||||
/// the storage-container ghost at the base-grid cell center (BaseGridMath.CellToWorld), then destroys
|
||||
/// the singleton so it fires exactly once. The entity carries no transform; only the prefab needs one.
|
||||
/// </summary>
|
||||
public class StorageSpawnerAuthoring : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Storage-container ghost prefab to instantiate. Must carry SharedStorageContainerAuthoring + a GhostAuthoringComponent.")]
|
||||
public GameObject ContainerPrefab;
|
||||
|
||||
[Tooltip("Build-grid cell at which to place the container (cell center, on the base plane).")]
|
||||
public Vector2Int Cell = new Vector2Int(16, 22);
|
||||
|
||||
private class StorageSpawnerBaker : Baker<StorageSpawnerAuthoring>
|
||||
{
|
||||
public override void Bake(StorageSpawnerAuthoring authoring)
|
||||
{
|
||||
var entity = GetEntity(authoring, TransformUsageFlags.None);
|
||||
|
||||
AddComponent(entity, new StorageSpawner
|
||||
{
|
||||
Prefab = authoring.ContainerPrefab != null
|
||||
? GetEntity(authoring.ContainerPrefab, TransformUsageFlags.Dynamic)
|
||||
: Entity.Null,
|
||||
Cell = new int2(authoring.Cell.x, authoring.Cell.y),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 968b8c85b6f69ae438e56cb1f19a2450
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1befa5e760ee51b4b8ad625ae55c5024
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,76 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Client-only sender for shared-storage deposit/withdraw <see cref="StorageOpRequest"/> RPCs. A
|
||||
/// one-off action (not per-tick predicted input), so it is an RPC: on an interact key edge (E =
|
||||
/// deposit, Q = withdraw a default test item) it creates the request entity targeted at the server
|
||||
/// connection, and the server applies it authoritatively in StorageOpReceiveSystem. Managed
|
||||
/// SystemBase because it reads the managed Input System. Input System types are fully qualified and
|
||||
/// <c>using UnityEngine.InputSystem;</c> is intentionally omitted (that namespace defines a
|
||||
/// PlayerInput type that collides with <see cref="ProjectM.Simulation.PlayerInput"/>). An editor-only
|
||||
/// static hook (Deposit/Withdraw) drives the same path from execute_code for headless validation
|
||||
/// without a focused Game view.
|
||||
/// </summary>
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
|
||||
public partial class StorageOpSendSystem : SystemBase
|
||||
{
|
||||
// Default test item used by the keyboard interact and the parameterless debug hooks.
|
||||
const ushort DefaultItemId = 1;
|
||||
const int DefaultCount = 1;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
struct PendingStorageOp { public byte Op; public ushort ItemId; public int Count; }
|
||||
|
||||
static readonly System.Collections.Generic.Queue<PendingStorageOp> s_Pending =
|
||||
new System.Collections.Generic.Queue<PendingStorageOp>();
|
||||
|
||||
/// <summary>EDITOR / execute_code hook: queue a deposit of <paramref name="count"/> of <paramref name="itemId"/>.</summary>
|
||||
public static void Deposit(ushort itemId = DefaultItemId, int count = DefaultCount) =>
|
||||
s_Pending.Enqueue(new PendingStorageOp { Op = StorageOp.Deposit, ItemId = itemId, Count = count });
|
||||
|
||||
/// <summary>EDITOR / execute_code hook: queue a withdraw of <paramref name="count"/> of <paramref name="itemId"/>.</summary>
|
||||
public static void Withdraw(ushort itemId = DefaultItemId, int count = DefaultCount) =>
|
||||
s_Pending.Enqueue(new PendingStorageOp { Op = StorageOp.Withdraw, ItemId = itemId, Count = count });
|
||||
#endif
|
||||
|
||||
protected override void OnCreate()
|
||||
{
|
||||
RequireForUpdate<NetworkId>();
|
||||
}
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
// Need the server connection to target the RPC; bail (keeping any queued ops) until connected.
|
||||
if (!SystemAPI.TryGetSingletonEntity<NetworkId>(out var connection))
|
||||
return;
|
||||
|
||||
var keyboard = UnityEngine.InputSystem.Keyboard.current;
|
||||
if (keyboard != null)
|
||||
{
|
||||
if (keyboard.eKey.wasPressedThisFrame)
|
||||
Send(connection, StorageOp.Deposit, DefaultItemId, DefaultCount);
|
||||
if (keyboard.qKey.wasPressedThisFrame)
|
||||
Send(connection, StorageOp.Withdraw, DefaultItemId, DefaultCount);
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
while (s_Pending.Count > 0)
|
||||
{
|
||||
var op = s_Pending.Dequeue();
|
||||
Send(connection, op.Op, op.ItemId, op.Count);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void Send(Entity connection, byte op, ushort itemId, int count)
|
||||
{
|
||||
var request = EntityManager.CreateEntity();
|
||||
EntityManager.AddComponentData(request, new StorageOpRequest { Op = op, ItemId = itemId, Count = count });
|
||||
EntityManager.AddComponentData(request, new SendRpcCommandRequest { TargetConnection = connection });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d59f540925fe24a439bad6f7a77907fe
|
||||
@@ -34,6 +34,12 @@ namespace ProjectM.Server
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var spawner = SystemAPI.GetSingleton<PlayerSpawner>();
|
||||
|
||||
// M5 home base: re-root the spawn ring on the baked BaseAnchor when present; fall back
|
||||
// to the spawner's SpawnPoint if the base subscene hasn't streamed in yet.
|
||||
var center = spawner.SpawnPoint;
|
||||
if (SystemAPI.TryGetSingleton<BaseAnchor>(out var baseAnchor))
|
||||
center = BaseGridMath.PlotCenter(baseAnchor);
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (receive, requestEntity) in
|
||||
@@ -45,7 +51,7 @@ namespace ProjectM.Server
|
||||
var networkId = SystemAPI.GetComponent<NetworkId>(connection);
|
||||
|
||||
var player = ecb.Instantiate(spawner.PlayerPrefab);
|
||||
ecb.SetComponent(player, LocalTransform.FromPosition(spawner.SpawnPoint + PlayerSpawnMath.SpawnOffset(networkId.Value, spawner.SpawnRingRadius, spawner.RingSlots)));
|
||||
ecb.SetComponent(player, LocalTransform.FromPosition(center + PlayerSpawnMath.SpawnOffset(networkId.Value, spawner.SpawnRingRadius, spawner.RingSlots)));
|
||||
ecb.SetComponent(player, new GhostOwner { NetworkId = networkId.Value });
|
||||
|
||||
// Auto-despawn the player when its owning connection is removed.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b28010c047f72f4b8d242ee1f4351cd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,51 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.Transforms;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-only, one-shot spawner for the shared home-base storage container (mirrors
|
||||
/// UpgradePickupSpawnSystem). On its first update it reads the baked <see cref="StorageSpawner"/>
|
||||
/// singleton and the <see cref="BaseAnchor"/>, instantiates the container ghost at the cell center
|
||||
/// (<see cref="BaseGridMath.CellToWorld"/>), then destroys the spawner singleton so the system idles
|
||||
/// (spawned exactly once). Runs in the default SimulationSystemGroup (NOT the prediction loop); the
|
||||
/// container replicates to clients as an ownerless interpolated ghost. The container is intentionally
|
||||
/// NOT linked to any connection's LinkedEntityGroup, so it persists across player disconnects.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct SharedStorageSpawnSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<StorageSpawner>();
|
||||
state.RequireForUpdate<BaseAnchor>();
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var spawnerEntity = SystemAPI.GetSingletonEntity<StorageSpawner>();
|
||||
var spawner = SystemAPI.GetComponent<StorageSpawner>(spawnerEntity);
|
||||
var anchor = SystemAPI.GetSingleton<BaseAnchor>();
|
||||
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
if (spawner.Prefab != Entity.Null)
|
||||
{
|
||||
var container = ecb.Instantiate(spawner.Prefab);
|
||||
var position = BaseGridMath.CellToWorld(anchor, spawner.Cell);
|
||||
ecb.SetComponent(container, LocalTransform.FromPosition(position));
|
||||
}
|
||||
|
||||
// One-shot: remove the spawner so RequireForUpdate fails and the system idles.
|
||||
ecb.DestroyEntity(spawnerEntity);
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c60c2c14e48ea0c45858cf0054c1663f
|
||||
@@ -0,0 +1,54 @@
|
||||
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="StorageOpRequest"/> RPCs (deposit/withdraw on the
|
||||
/// shared storage container). Resolves the single <see cref="SharedStorageContainer"/> as a singleton,
|
||||
/// applies the op to its replicated <see cref="StorageEntry"/> buffer via <see cref="StorageMath"/>,
|
||||
/// and destroys the request entity. Runs in the default SimulationSystemGroup (NOT the prediction
|
||||
/// loop), so a server event is applied exactly once (no rollback double-apply). Op is read as a byte
|
||||
/// (see <see cref="StorageOp"/>); the buffer mutation auto-replicates to all clients via GhostField.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct StorageOpReceiveSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<SharedStorageContainer>();
|
||||
|
||||
var builder = new EntityQueryBuilder(Allocator.Temp)
|
||||
.WithAll<StorageOpRequest, ReceiveRpcCommandRequest>();
|
||||
state.RequireForUpdate(state.GetEntityQuery(builder));
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var containerEntity = SystemAPI.GetSingletonEntity<SharedStorageContainer>();
|
||||
var contents = SystemAPI.GetBuffer<StorageEntry>(containerEntity);
|
||||
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (request, requestEntity) in
|
||||
SystemAPI.Query<RefRO<StorageOpRequest>>().WithAll<ReceiveRpcCommandRequest>().WithEntityAccess())
|
||||
{
|
||||
var op = request.ValueRO;
|
||||
if (op.Op == StorageOp.Withdraw)
|
||||
StorageMath.Withdraw(contents, op.ItemId, op.Count);
|
||||
else
|
||||
StorageMath.Deposit(contents, op.ItemId, op.Count);
|
||||
|
||||
ecb.DestroyEntity(requestEntity);
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6739144c8fa1bd040ad766919f9535f3
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d26b9ac48ea390e4fa1310800701eb52
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton baked into the gameplay subscene describing the shared home base: a fixed anchor
|
||||
/// position and the planar build-grid coordinate space (origin + cell size + extent) that M6 build
|
||||
/// placement snaps structures into and M7 production chains tick inside. Flat and blittable (no
|
||||
/// entity refs) so it stays deterministic across both worlds and serialization-friendly for later
|
||||
/// persistence. Present identically on client and server (baked, not replicated).
|
||||
/// </summary>
|
||||
public struct BaseAnchor : IComponentData
|
||||
{
|
||||
/// <summary>World-space center of the plot; also the player spawn-ring center. Equals BaseGridMath.PlotCenter(this).</summary>
|
||||
public float3 AnchorPos;
|
||||
|
||||
/// <summary>World-space min-XZ CORNER of cell (0,0). Y is the base plane. Baked = AnchorPos - (GridDims*CellSize)/2 on XZ.</summary>
|
||||
public float3 GridOrigin;
|
||||
|
||||
/// <summary>Size of one square grid cell in world units.</summary>
|
||||
public float CellSize;
|
||||
|
||||
/// <summary>Grid extent in cells along X and Z; valid cell indices are [0, GridDims).</summary>
|
||||
public int2 GridDims;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9434d000149a08e438c5a7b876f7e10c
|
||||
@@ -0,0 +1,54 @@
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Deterministic, side-effect-free math for the home-base build grid. Pure (no RNG, no wall-clock)
|
||||
/// so server and predicting client agree on cell occupancy. Convention: <see cref="BaseAnchor.GridOrigin"/>
|
||||
/// is the min-XZ CORNER of cell (0,0); <see cref="CellToWorld"/> returns cell CENTERS; cell bounds are
|
||||
/// half-open so a point on a cell's lower edge belongs to that cell and the far plot edge is out of plot.
|
||||
/// World->cell uses math.floor (not truncation) so negative coordinates snap correctly. Unit-tested in
|
||||
/// EditMode (no netcode world required); M6's server placement handler calls these directly.
|
||||
/// </summary>
|
||||
public static class BaseGridMath
|
||||
{
|
||||
/// <summary>Planar (XZ) world position -> integer grid cell. Uses floor, so values below GridOrigin go negative (out of plot).</summary>
|
||||
public static int2 WorldToCell(in BaseAnchor a, float3 worldPos)
|
||||
{
|
||||
float2 local = (worldPos.xz - a.GridOrigin.xz) / a.CellSize;
|
||||
return (int2)math.floor(local);
|
||||
}
|
||||
|
||||
/// <summary>Grid cell -> world position of the cell CENTER, on the base plane (Y = GridOrigin.y).</summary>
|
||||
public static float3 CellToWorld(in BaseAnchor a, int2 cell)
|
||||
{
|
||||
float2 centerXZ = a.GridOrigin.xz + ((float2)cell + 0.5f) * a.CellSize;
|
||||
return new float3(centerXZ.x, a.GridOrigin.y, centerXZ.y);
|
||||
}
|
||||
|
||||
/// <summary>True when the cell is inside the plot. Half-open: [0, GridDims), so the far edge is out.</summary>
|
||||
public static bool IsCellInPlot(in BaseAnchor a, int2 cell)
|
||||
{
|
||||
return math.all(cell >= 0) && math.all(cell < a.GridDims);
|
||||
}
|
||||
|
||||
/// <summary>True when the world point falls inside the plot (its cell is in-plot).</summary>
|
||||
public static bool IsPointInPlot(in BaseAnchor a, float3 worldPos)
|
||||
{
|
||||
return IsCellInPlot(a, WorldToCell(a, worldPos));
|
||||
}
|
||||
|
||||
/// <summary>Clamp a cell into the valid [0, GridDims-1] range.</summary>
|
||||
public static int2 ClampCell(in BaseAnchor a, int2 cell)
|
||||
{
|
||||
return math.clamp(cell, new int2(0, 0), a.GridDims - 1);
|
||||
}
|
||||
|
||||
/// <summary>World-space center of the whole plot (== AnchorPos when baked correctly).</summary>
|
||||
public static float3 PlotCenter(in BaseAnchor a)
|
||||
{
|
||||
float2 centerXZ = a.GridOrigin.xz + (float2)a.GridDims * a.CellSize * 0.5f;
|
||||
return new float3(centerXZ.x, a.GridOrigin.y, centerXZ.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 30b3681e3ce98b241a7ceb7af7e62962
|
||||
@@ -0,0 +1,12 @@
|
||||
using Unity.Entities;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Tag marking the shared home-base storage container. All state lives in the entity's
|
||||
/// <see cref="StorageEntry"/> buffer. In M5 there is exactly one (server-spawned at a fixed base
|
||||
/// cell), so server systems resolve it as a singleton. Server-authoritative and world-resident, so
|
||||
/// its contents survive a player disconnect (no disk persistence yet).
|
||||
/// </summary>
|
||||
public struct SharedStorageContainer : IComponentData { }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8de8d91f5d8f0a64c87b0847ae85c564
|
||||
@@ -0,0 +1,23 @@
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// One (item, count) row in a shared storage container's contents. The container's DynamicBuffer of
|
||||
/// these is the server-authoritative shared inventory; it is a GhostField buffer so the server's
|
||||
/// mutations replicate to every client. The container is an ownerless INTERPOLATED ghost, so no
|
||||
/// OwnerSendType / GhostOwner applies (those are for owner-predicted ghosts like the player).
|
||||
/// ItemId is an opaque id; a richer item model (and the M7 machine input/output generalisation)
|
||||
/// layers on top without changing replication.
|
||||
/// </summary>
|
||||
[InternalBufferCapacity(16)]
|
||||
public struct StorageEntry : IBufferElementData
|
||||
{
|
||||
/// <summary>Opaque item identifier (0 = empty/unused).</summary>
|
||||
[GhostField] public ushort ItemId;
|
||||
|
||||
/// <summary>Quantity of this item in the container.</summary>
|
||||
[GhostField] public int Count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f017cb14ca58bc54191ad3f356541ca6
|
||||
@@ -0,0 +1,62 @@
|
||||
using Unity.Entities;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Pure, deterministic merge logic for a shared storage container's <see cref="StorageEntry"/> buffer.
|
||||
/// No RNG / wall-clock, so server and (future) prediction agree. Deposit merges into an existing row
|
||||
/// for the same item or appends a new row; Withdraw decrements and drops a row that hits zero, clamping
|
||||
/// to available. DynamicBuffer is a handle, so mutations apply to the underlying entity buffer.
|
||||
/// Unit-tested in EditMode via a plain Entities world.
|
||||
/// </summary>
|
||||
public static class StorageMath
|
||||
{
|
||||
/// <summary>Add <paramref name="count"/> of <paramref name="itemId"/>, merging into an existing row if present. No-op for count <= 0 or itemId 0.</summary>
|
||||
public static void Deposit(DynamicBuffer<StorageEntry> buffer, ushort itemId, int count)
|
||||
{
|
||||
if (count <= 0 || itemId == 0)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
if (buffer[i].ItemId == itemId)
|
||||
{
|
||||
var entry = buffer[i];
|
||||
entry.Count += count;
|
||||
buffer[i] = entry;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
buffer.Add(new StorageEntry { ItemId = itemId, Count = count });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove up to <paramref name="count"/> of <paramref name="itemId"/>, clamped to what is available.
|
||||
/// Drops the row when it reaches zero. Returns the amount actually withdrawn (0 if none). No-op for
|
||||
/// count <= 0 or itemId 0.
|
||||
/// </summary>
|
||||
public static int Withdraw(DynamicBuffer<StorageEntry> buffer, ushort itemId, int count)
|
||||
{
|
||||
if (count <= 0 || itemId == 0)
|
||||
return 0;
|
||||
|
||||
for (int i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
if (buffer[i].ItemId == itemId)
|
||||
{
|
||||
var entry = buffer[i];
|
||||
int taken = entry.Count < count ? entry.Count : count;
|
||||
entry.Count -= taken;
|
||||
if (entry.Count <= 0)
|
||||
buffer.RemoveAt(i);
|
||||
else
|
||||
buffer[i] = entry;
|
||||
return taken;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c20f98fb70e66c94193c16e4bfd7f1b4
|
||||
@@ -0,0 +1,33 @@
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Client -> server request to deposit into or withdraw from the shared storage container. A one-off
|
||||
/// action, so it is an RPC (not a per-tick predicted input). Op is stored as a byte (see
|
||||
/// <see cref="StorageOp"/>) rather than an enum to keep the generated serializer trivial and avoid the
|
||||
/// cross-assembly enum-codegen hazard. No target entity is carried: M5 has a single shared container,
|
||||
/// which the server resolves as a singleton (entity refs are not stable across worlds).
|
||||
/// </summary>
|
||||
public struct StorageOpRequest : IRpcCommand
|
||||
{
|
||||
/// <summary>Operation code (see <see cref="StorageOp"/>): 0 = deposit, 1 = withdraw.</summary>
|
||||
public byte Op;
|
||||
|
||||
/// <summary>Item to deposit/withdraw.</summary>
|
||||
public ushort ItemId;
|
||||
|
||||
/// <summary>Quantity to deposit/withdraw (server clamps withdraw to available).</summary>
|
||||
public int Count;
|
||||
}
|
||||
|
||||
/// <summary>Operation codes for <see cref="StorageOpRequest.Op"/> (byte to keep RPC serialization trivial).</summary>
|
||||
public static class StorageOp
|
||||
{
|
||||
/// <summary>Add items to the shared container.</summary>
|
||||
public const byte Deposit = 0;
|
||||
|
||||
/// <summary>Remove items from the shared container.</summary>
|
||||
public const byte Withdraw = 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc9ec88867d746e45b9204331b5bab51
|
||||
@@ -0,0 +1,20 @@
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton baked into the gameplay subscene, holding the baked storage-container ghost prefab and
|
||||
/// the base-grid cell to spawn it at. A one-shot server system instantiates the prefab at
|
||||
/// BaseGridMath.CellToWorld(anchor, Cell) and then destroys this singleton. Mirrors the
|
||||
/// UpgradePickupSpawner / PlayerSpawner pattern.
|
||||
/// </summary>
|
||||
public struct StorageSpawner : IComponentData
|
||||
{
|
||||
/// <summary>Baked storage-container ghost prefab to instantiate.</summary>
|
||||
public Entity Prefab;
|
||||
|
||||
/// <summary>Base-grid cell at which to place the container (cell center, on the base plane).</summary>
|
||||
public int2 Cell;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a2e4fad83fa03b4890d736b388a9917
|
||||
Reference in New Issue
Block a user