Init Homebase
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &3885353946372160549
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 3572766465862231365}
|
||||
- component: {fileID: 3909651526955663392}
|
||||
- component: {fileID: 3320445911748035220}
|
||||
- component: {fileID: 9053853372340598254}
|
||||
- component: {fileID: 6834786618115927220}
|
||||
- component: {fileID: 1499754650316338861}
|
||||
m_Layer: 0
|
||||
m_Name: Storage
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &3572766465862231365
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3885353946372160549}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 0.8, y: 0.8, z: 0.8}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!33 &3909651526955663392
|
||||
MeshFilter:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3885353946372160549}
|
||||
m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0}
|
||||
--- !u!23 &3320445911748035220
|
||||
MeshRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3885353946372160549}
|
||||
m_Enabled: 1
|
||||
m_CastShadows: 1
|
||||
m_ReceiveShadows: 1
|
||||
m_DynamicOccludee: 1
|
||||
m_StaticShadowCaster: 0
|
||||
m_MotionVectors: 1
|
||||
m_LightProbeUsage: 1
|
||||
m_ReflectionProbeUsage: 1
|
||||
m_RayTracingMode: 2
|
||||
m_RayTraceProcedural: 0
|
||||
m_RayTracingAccelStructBuildFlagsOverride: 0
|
||||
m_RayTracingAccelStructBuildFlags: 1
|
||||
m_SmallMeshCulling: 1
|
||||
m_ForceMeshLod: -1
|
||||
m_MeshLodSelectionBias: 0
|
||||
m_RenderingLayerMask: 1
|
||||
m_RendererPriority: 0
|
||||
m_Materials:
|
||||
- {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
|
||||
m_StaticBatchInfo:
|
||||
firstSubMesh: 0
|
||||
subMeshCount: 0
|
||||
m_StaticBatchRoot: {fileID: 0}
|
||||
m_ProbeAnchor: {fileID: 0}
|
||||
m_LightProbeVolumeOverride: {fileID: 0}
|
||||
m_ScaleInLightmap: 1
|
||||
m_ReceiveGI: 1
|
||||
m_PreserveUVs: 1
|
||||
m_IgnoreNormalsForChartDetection: 0
|
||||
m_ImportantGI: 0
|
||||
m_StitchLightmapSeams: 1
|
||||
m_SelectedEditorRenderState: 3
|
||||
m_MinimumChartSize: 4
|
||||
m_AutoUVMaxDistance: 0.5
|
||||
m_AutoUVMaxAngle: 89
|
||||
m_LightmapParameters: {fileID: 0}
|
||||
m_GlobalIlluminationMeshLod: 0
|
||||
m_SortingLayerID: 0
|
||||
m_SortingLayer: 0
|
||||
m_SortingOrder: 0
|
||||
m_MaskInteraction: 0
|
||||
m_AdditionalVertexStreams: {fileID: 0}
|
||||
--- !u!114 &9053853372340598254
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3885353946372160549}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: c16549610bfe4458aa9389201d072bb6, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.Entities.Hybrid::Unity.Entities.Hybrid.Baking.LinkedEntityGroupAuthoring
|
||||
--- !u!114 &6834786618115927220
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3885353946372160549}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 7c79d771cedb4794bf100ce60df5f764, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.NetCode.Authoring.Hybrid::Unity.NetCode.GhostAuthoringComponent
|
||||
HasOwner: 0
|
||||
SupportAutoCommandTarget: 1
|
||||
TrackInterpolationDelay: 0
|
||||
GhostGroup: 0
|
||||
UsePreSerialization: 0
|
||||
UseSingleBaseline: 0
|
||||
RollbackPredictedSpawnedGhostState: 0
|
||||
RollbackPredictionOnStructuralChanges: 1
|
||||
DefaultGhostMode: 0
|
||||
SupportedGhostModes: 3
|
||||
OptimizationMode: 0
|
||||
Importance: 1
|
||||
MaxSendRate: 0
|
||||
prefabId:
|
||||
--- !u!114 &1499754650316338861
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3885353946372160549}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 8cc6285a9b8958a47a61a07550b7f792, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.SharedStorageContainerAuthoring
|
||||
InteractRadius: 2
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6636e906a2ac46345889f5517658be67
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -281,6 +281,52 @@ Transform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &874705786
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 874705788}
|
||||
- component: {fileID: 874705787}
|
||||
m_Layer: 0
|
||||
m_Name: BaseAnchor
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &874705787
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 874705786}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4b53184727e358b4eb27f68ae25504d8, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.BaseAnchorAuthoring
|
||||
CellSize: 1
|
||||
PlotSize: 32
|
||||
--- !u!4 &874705788
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 874705786}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 1, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1379903944
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -585,6 +631,52 @@ Transform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1930969065
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1930969067}
|
||||
- component: {fileID: 1930969066}
|
||||
m_Layer: 0
|
||||
m_Name: StorageSpawner
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &1930969066
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1930969065}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 968b8c85b6f69ae438e56cb1f19a2450, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.StorageSpawnerAuthoring
|
||||
ContainerPrefab: {fileID: 3885353946372160549, guid: 6636e906a2ac46345889f5517658be67, type: 3}
|
||||
Cell: {x: 19, y: 19}
|
||||
--- !u!4 &1930969067
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1930969065}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 1, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1936735558
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -758,3 +850,5 @@ SceneRoots:
|
||||
- {fileID: 1694504175}
|
||||
- {fileID: 1936735562}
|
||||
- {fileID: 691660676}
|
||||
- {fileID: 874705788}
|
||||
- {fileID: 1930969067}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
using NUnit.Framework;
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace ProjectM.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Pure-function tests for <see cref="BaseGridMath"/> (no ECS world), mirroring PlayerSpawnRingTests.
|
||||
/// Pins the locked M5 home-base grid coordinate space: planar 32x32, CellSize 1.0, corner-origin,
|
||||
/// center-returning, half-open cell bounds, floor-based world->cell. M6 placement builds on this.
|
||||
/// </summary>
|
||||
public class BaseGridMathTests
|
||||
{
|
||||
const float Eps = 1e-4f;
|
||||
|
||||
// Locked config: center at (0,1,0); 32x32 cells of 1.0u => origin corner at (-16,1,-16).
|
||||
static BaseAnchor MakeAnchor()
|
||||
{
|
||||
var dims = new int2(32, 32);
|
||||
float cell = 1f;
|
||||
var anchorPos = new float3(0f, 1f, 0f);
|
||||
var gridOrigin = new float3(
|
||||
anchorPos.x - dims.x * cell * 0.5f,
|
||||
anchorPos.y,
|
||||
anchorPos.z - dims.y * cell * 0.5f);
|
||||
return new BaseAnchor { AnchorPos = anchorPos, GridOrigin = gridOrigin, CellSize = cell, GridDims = dims };
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CellToWorld_Returns_Cell_Centers()
|
||||
{
|
||||
var a = MakeAnchor();
|
||||
AssertXz(BaseGridMath.CellToWorld(a, new int2(0, 0)), -15.5f, -15.5f);
|
||||
AssertXz(BaseGridMath.CellToWorld(a, new int2(31, 31)), 15.5f, 15.5f);
|
||||
AssertXz(BaseGridMath.CellToWorld(a, new int2(16, 16)), 0.5f, 0.5f);
|
||||
// Centers stay on the base plane.
|
||||
Assert.AreEqual(1f, BaseGridMath.CellToWorld(a, new int2(5, 9)).y, Eps);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RoundTrip_CellToWorld_Then_WorldToCell()
|
||||
{
|
||||
var a = MakeAnchor();
|
||||
AssertCell(BaseGridMath.WorldToCell(a, BaseGridMath.CellToWorld(a, new int2(0, 0))), 0, 0);
|
||||
AssertCell(BaseGridMath.WorldToCell(a, BaseGridMath.CellToWorld(a, new int2(31, 31))), 31, 31);
|
||||
AssertCell(BaseGridMath.WorldToCell(a, BaseGridMath.CellToWorld(a, new int2(16, 16))), 16, 16);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WorldToCell_Uses_Floor_Not_Truncation()
|
||||
{
|
||||
var a = MakeAnchor();
|
||||
// local x = worldX + 16, then floor. z = 0 -> cell.y = 16.
|
||||
Assert.AreEqual(0, BaseGridMath.WorldToCell(a, new float3(-15.6f, 1f, 0f)).x);
|
||||
Assert.AreEqual(-1, BaseGridMath.WorldToCell(a, new float3(-16.4f, 1f, 0f)).x);
|
||||
// A point exactly on a cell's lower edge belongs to the higher cell (half-open).
|
||||
Assert.AreEqual(1, BaseGridMath.WorldToCell(a, new float3(-15.0f, 1f, 0f)).x);
|
||||
Assert.AreEqual(16, BaseGridMath.WorldToCell(a, new float3(-15.6f, 1f, 0f)).y);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsCellInPlot_Is_HalfOpen()
|
||||
{
|
||||
var a = MakeAnchor();
|
||||
Assert.IsTrue(BaseGridMath.IsCellInPlot(a, new int2(0, 0)));
|
||||
Assert.IsTrue(BaseGridMath.IsCellInPlot(a, new int2(31, 31)));
|
||||
Assert.IsFalse(BaseGridMath.IsCellInPlot(a, new int2(-1, 0)));
|
||||
Assert.IsFalse(BaseGridMath.IsCellInPlot(a, new int2(0, 32)));
|
||||
Assert.IsFalse(BaseGridMath.IsCellInPlot(a, new int2(32, 32)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsPointInPlot_Bounds()
|
||||
{
|
||||
var a = MakeAnchor();
|
||||
Assert.IsTrue(BaseGridMath.IsPointInPlot(a, a.AnchorPos)); // center
|
||||
Assert.IsTrue(BaseGridMath.IsPointInPlot(a, new float3(-16f, 1f, 0f))); // lower corner edge -> cell 0
|
||||
Assert.IsTrue(BaseGridMath.IsPointInPlot(a, new float3(15.9f, 1f, 15.9f))); // cell (31,31)
|
||||
Assert.IsFalse(BaseGridMath.IsPointInPlot(a, new float3(-16.1f, 1f, 0f))); // cell (-1,*) out
|
||||
Assert.IsFalse(BaseGridMath.IsPointInPlot(a, new float3(16f, 1f, 0f))); // far edge -> cell 32 out
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClampCell_Bounds_To_Plot()
|
||||
{
|
||||
var a = MakeAnchor();
|
||||
AssertCell(BaseGridMath.ClampCell(a, new int2(-5, 99)), 0, 31);
|
||||
AssertCell(BaseGridMath.ClampCell(a, new int2(10, 10)), 10, 10);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PlotCenter_Equals_AnchorPos()
|
||||
{
|
||||
var a = MakeAnchor();
|
||||
var c = BaseGridMath.PlotCenter(a);
|
||||
Assert.AreEqual(a.AnchorPos.x, c.x, Eps);
|
||||
Assert.AreEqual(a.AnchorPos.y, c.y, Eps);
|
||||
Assert.AreEqual(a.AnchorPos.z, c.z, Eps);
|
||||
}
|
||||
|
||||
static void AssertXz(float3 p, float x, float z)
|
||||
{
|
||||
Assert.AreEqual(x, p.x, Eps);
|
||||
Assert.AreEqual(z, p.z, Eps);
|
||||
}
|
||||
|
||||
static void AssertCell(int2 c, int x, int y)
|
||||
{
|
||||
Assert.AreEqual(x, c.x);
|
||||
Assert.AreEqual(y, c.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 001a3491cf2ef57468f3e4c61e98d30c
|
||||
@@ -0,0 +1,141 @@
|
||||
using NUnit.Framework;
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
|
||||
namespace ProjectM.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Plain-Entities tests for <see cref="StorageMath"/> deposit/withdraw merge logic, using a temp World
|
||||
/// and a real DynamicBuffer<StorageEntry> (no netcode world). Pins shared-storage semantics:
|
||||
/// deposits merge by item id, withdraws clamp to available and drop empty rows.
|
||||
/// </summary>
|
||||
public class StorageMathTests
|
||||
{
|
||||
static (World world, Entity e) MakeWorld()
|
||||
{
|
||||
var world = new World("StorageMathTestWorld");
|
||||
var e = world.EntityManager.CreateEntity(typeof(StorageEntry));
|
||||
return (world, e);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Deposit_New_Item_Appends_Row()
|
||||
{
|
||||
var (world, e) = MakeWorld();
|
||||
try
|
||||
{
|
||||
var buf = world.EntityManager.GetBuffer<StorageEntry>(e);
|
||||
StorageMath.Deposit(buf, 1, 5);
|
||||
Assert.AreEqual(1, buf.Length);
|
||||
Assert.AreEqual((ushort)1, buf[0].ItemId);
|
||||
Assert.AreEqual(5, buf[0].Count);
|
||||
}
|
||||
finally { world.Dispose(); }
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Deposit_Same_Item_Merges()
|
||||
{
|
||||
var (world, e) = MakeWorld();
|
||||
try
|
||||
{
|
||||
var buf = world.EntityManager.GetBuffer<StorageEntry>(e);
|
||||
StorageMath.Deposit(buf, 1, 5);
|
||||
StorageMath.Deposit(buf, 1, 3);
|
||||
Assert.AreEqual(1, buf.Length);
|
||||
Assert.AreEqual(8, buf[0].Count);
|
||||
}
|
||||
finally { world.Dispose(); }
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Deposit_Different_Items_Separate_Rows()
|
||||
{
|
||||
var (world, e) = MakeWorld();
|
||||
try
|
||||
{
|
||||
var buf = world.EntityManager.GetBuffer<StorageEntry>(e);
|
||||
StorageMath.Deposit(buf, 1, 5);
|
||||
StorageMath.Deposit(buf, 2, 7);
|
||||
Assert.AreEqual(2, buf.Length);
|
||||
}
|
||||
finally { world.Dispose(); }
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Withdraw_Partial_Decrements()
|
||||
{
|
||||
var (world, e) = MakeWorld();
|
||||
try
|
||||
{
|
||||
var buf = world.EntityManager.GetBuffer<StorageEntry>(e);
|
||||
StorageMath.Deposit(buf, 1, 5);
|
||||
int taken = StorageMath.Withdraw(buf, 1, 2);
|
||||
Assert.AreEqual(2, taken);
|
||||
Assert.AreEqual(1, buf.Length);
|
||||
Assert.AreEqual(3, buf[0].Count);
|
||||
}
|
||||
finally { world.Dispose(); }
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Withdraw_To_Zero_Drops_Row()
|
||||
{
|
||||
var (world, e) = MakeWorld();
|
||||
try
|
||||
{
|
||||
var buf = world.EntityManager.GetBuffer<StorageEntry>(e);
|
||||
StorageMath.Deposit(buf, 1, 5);
|
||||
int taken = StorageMath.Withdraw(buf, 1, 5);
|
||||
Assert.AreEqual(5, taken);
|
||||
Assert.AreEqual(0, buf.Length);
|
||||
}
|
||||
finally { world.Dispose(); }
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Withdraw_More_Than_Available_Clamps()
|
||||
{
|
||||
var (world, e) = MakeWorld();
|
||||
try
|
||||
{
|
||||
var buf = world.EntityManager.GetBuffer<StorageEntry>(e);
|
||||
StorageMath.Deposit(buf, 1, 3);
|
||||
int taken = StorageMath.Withdraw(buf, 1, 10);
|
||||
Assert.AreEqual(3, taken);
|
||||
Assert.AreEqual(0, buf.Length);
|
||||
}
|
||||
finally { world.Dispose(); }
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Withdraw_Missing_Item_Is_NoOp()
|
||||
{
|
||||
var (world, e) = MakeWorld();
|
||||
try
|
||||
{
|
||||
var buf = world.EntityManager.GetBuffer<StorageEntry>(e);
|
||||
StorageMath.Deposit(buf, 1, 3);
|
||||
int taken = StorageMath.Withdraw(buf, 99, 1);
|
||||
Assert.AreEqual(0, taken);
|
||||
Assert.AreEqual(1, buf.Length);
|
||||
}
|
||||
finally { world.Dispose(); }
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Deposit_Ignores_NonPositive_And_ZeroItem()
|
||||
{
|
||||
var (world, e) = MakeWorld();
|
||||
try
|
||||
{
|
||||
var buf = world.EntityManager.GetBuffer<StorageEntry>(e);
|
||||
StorageMath.Deposit(buf, 1, 0);
|
||||
StorageMath.Deposit(buf, 1, -5);
|
||||
StorageMath.Deposit(buf, 0, 5);
|
||||
Assert.AreEqual(0, buf.Length);
|
||||
}
|
||||
finally { world.Dispose(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c3f5de6f166b6da45bd20e2884e2f4c0
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f08a78b99fcf9948987cc3202bfc0b0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,677 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!29 &1
|
||||
OcclusionCullingSettings:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 2
|
||||
m_OcclusionBakeSettings:
|
||||
smallestOccluder: 5
|
||||
smallestHole: 0.25
|
||||
backfaceThreshold: 100
|
||||
m_SceneGUID: 00000000000000000000000000000000
|
||||
m_OcclusionCullingData: {fileID: 0}
|
||||
--- !u!104 &2
|
||||
RenderSettings:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 10
|
||||
m_Fog: 0
|
||||
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
|
||||
m_FogMode: 3
|
||||
m_FogDensity: 0.01
|
||||
m_LinearFogStart: 0
|
||||
m_LinearFogEnd: 300
|
||||
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
|
||||
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
|
||||
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
|
||||
m_AmbientIntensity: 1
|
||||
m_AmbientMode: 0
|
||||
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
|
||||
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_HaloStrength: 0.5
|
||||
m_FlareStrength: 1
|
||||
m_FlareFadeSpeed: 3
|
||||
m_HaloTexture: {fileID: 0}
|
||||
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_DefaultReflectionMode: 0
|
||||
m_DefaultReflectionResolution: 128
|
||||
m_ReflectionBounces: 1
|
||||
m_ReflectionIntensity: 1
|
||||
m_CustomReflection: {fileID: 0}
|
||||
m_Sun: {fileID: 0}
|
||||
m_UseRadianceAmbientProbe: 0
|
||||
--- !u!157 &3
|
||||
LightmapSettings:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 13
|
||||
m_BakeOnSceneLoad: 0
|
||||
m_GISettings:
|
||||
serializedVersion: 2
|
||||
m_BounceScale: 1
|
||||
m_IndirectOutputScale: 1
|
||||
m_AlbedoBoost: 1
|
||||
m_EnvironmentLightingMode: 0
|
||||
m_EnableBakedLightmaps: 1
|
||||
m_EnableRealtimeLightmaps: 0
|
||||
m_LightmapEditorSettings:
|
||||
serializedVersion: 12
|
||||
m_Resolution: 2
|
||||
m_BakeResolution: 40
|
||||
m_AtlasSize: 1024
|
||||
m_AO: 0
|
||||
m_AOMaxDistance: 1
|
||||
m_CompAOExponent: 1
|
||||
m_CompAOExponentDirect: 0
|
||||
m_ExtractAmbientOcclusion: 0
|
||||
m_Padding: 2
|
||||
m_LightmapParameters: {fileID: 0}
|
||||
m_LightmapsBakeMode: 1
|
||||
m_TextureCompression: 1
|
||||
m_ReflectionCompression: 2
|
||||
m_MixedBakeMode: 2
|
||||
m_BakeBackend: 1
|
||||
m_PVRSampling: 1
|
||||
m_PVRDirectSampleCount: 32
|
||||
m_PVRSampleCount: 512
|
||||
m_PVRBounces: 2
|
||||
m_PVREnvironmentSampleCount: 256
|
||||
m_PVREnvironmentReferencePointCount: 2048
|
||||
m_PVRFilteringMode: 1
|
||||
m_PVRDenoiserTypeDirect: 1
|
||||
m_PVRDenoiserTypeIndirect: 1
|
||||
m_PVRDenoiserTypeAO: 1
|
||||
m_PVRFilterTypeDirect: 0
|
||||
m_PVRFilterTypeIndirect: 0
|
||||
m_PVRFilterTypeAO: 0
|
||||
m_PVREnvironmentMIS: 1
|
||||
m_PVRCulling: 1
|
||||
m_PVRFilteringGaussRadiusDirect: 1
|
||||
m_PVRFilteringGaussRadiusIndirect: 5
|
||||
m_PVRFilteringGaussRadiusAO: 2
|
||||
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
|
||||
m_PVRFilteringAtrousPositionSigmaIndirect: 2
|
||||
m_PVRFilteringAtrousPositionSigmaAO: 1
|
||||
m_ExportTrainingData: 0
|
||||
m_TrainingDataDestination: TrainingData
|
||||
m_LightProbeSampleCountMultiplier: 4
|
||||
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_LightingSettings: {fileID: 0}
|
||||
--- !u!196 &4
|
||||
NavMeshSettings:
|
||||
serializedVersion: 2
|
||||
m_ObjectHideFlags: 0
|
||||
m_BuildSettings:
|
||||
serializedVersion: 3
|
||||
agentTypeID: 0
|
||||
agentRadius: 0.5
|
||||
agentHeight: 2
|
||||
agentSlope: 45
|
||||
agentClimb: 0.4
|
||||
ledgeDropHeight: 0
|
||||
maxJumpAcrossDistance: 0
|
||||
minRegionArea: 2
|
||||
manualCellSize: 0
|
||||
cellSize: 0.16666667
|
||||
manualTileSize: 0
|
||||
tileSize: 256
|
||||
buildHeightMesh: 0
|
||||
maxJobWorkers: 0
|
||||
preserveTilesOutsideBounds: 0
|
||||
debug:
|
||||
m_Flags: 0
|
||||
m_NavMeshData: {fileID: 0}
|
||||
--- !u!1 &121227139
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 121227141}
|
||||
- component: {fileID: 121227140}
|
||||
m_Layer: 0
|
||||
m_Name: NetConnectionUI
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &121227140
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 121227139}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 977a7574cf6ff4898b7e9db6c21368f9, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: ProjectM.Client::ProjectM.Client.ConnectionUI
|
||||
_port: 7979
|
||||
--- !u!4 &121227141
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 121227139}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &330585543
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 330585546}
|
||||
- component: {fileID: 330585545}
|
||||
- component: {fileID: 330585544}
|
||||
- component: {fileID: 330585547}
|
||||
- component: {fileID: 330585548}
|
||||
m_Layer: 0
|
||||
m_Name: Main Camera
|
||||
m_TagString: MainCamera
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!81 &330585544
|
||||
AudioListener:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 330585543}
|
||||
m_Enabled: 1
|
||||
--- !u!20 &330585545
|
||||
Camera:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 330585543}
|
||||
m_Enabled: 1
|
||||
serializedVersion: 2
|
||||
m_ClearFlags: 1
|
||||
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
|
||||
m_projectionMatrixMode: 1
|
||||
m_GateFitMode: 2
|
||||
m_FOVAxisMode: 0
|
||||
m_Iso: 200
|
||||
m_ShutterSpeed: 0.005
|
||||
m_Aperture: 16
|
||||
m_FocusDistance: 10
|
||||
m_FocalLength: 50
|
||||
m_BladeCount: 5
|
||||
m_Curvature: {x: 2, y: 11}
|
||||
m_BarrelClipping: 0.25
|
||||
m_Anamorphism: 0
|
||||
m_SensorSize: {x: 36, y: 24}
|
||||
m_LensShift: {x: 0, y: 0}
|
||||
m_NormalizedViewPortRect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 0
|
||||
width: 1
|
||||
height: 1
|
||||
near clip plane: 0.3
|
||||
far clip plane: 1000
|
||||
field of view: 60
|
||||
orthographic: 0
|
||||
orthographic size: 5
|
||||
m_Depth: -1
|
||||
m_CullingMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_RenderingPath: -1
|
||||
m_TargetTexture: {fileID: 0}
|
||||
m_TargetDisplay: 0
|
||||
m_TargetEye: 3
|
||||
m_HDR: 1
|
||||
m_AllowMSAA: 1
|
||||
m_AllowDynamicResolution: 0
|
||||
m_ForceIntoRT: 0
|
||||
m_OcclusionCulling: 1
|
||||
m_StereoConvergence: 10
|
||||
m_StereoSeparation: 0.022
|
||||
--- !u!4 &330585546
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 330585543}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 1, z: -10}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &330585547
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 330585543}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_RenderShadows: 1
|
||||
m_RequiresDepthTextureOption: 2
|
||||
m_RequiresOpaqueTextureOption: 2
|
||||
m_CameraType: 0
|
||||
m_Cameras: []
|
||||
m_RendererIndex: -1
|
||||
m_VolumeLayerMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 1
|
||||
m_VolumeTrigger: {fileID: 0}
|
||||
m_VolumeFrameworkUpdateModeOption: 2
|
||||
m_RenderPostProcessing: 1
|
||||
m_Antialiasing: 0
|
||||
m_AntialiasingQuality: 2
|
||||
m_StopNaN: 0
|
||||
m_Dithering: 0
|
||||
m_ClearDepth: 1
|
||||
m_AllowXRRendering: 1
|
||||
m_AllowHDROutput: 1
|
||||
m_UseScreenCoordOverride: 0
|
||||
m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_RequiresDepthTexture: 0
|
||||
m_RequiresColorTexture: 0
|
||||
m_TaaSettings:
|
||||
m_Quality: 3
|
||||
m_FrameInfluence: 0.1
|
||||
m_JitterScale: 1
|
||||
m_MipBias: 0
|
||||
m_VarianceClampScale: 0.9
|
||||
m_ContrastAdaptiveSharpening: 0
|
||||
m_Version: 2
|
||||
--- !u!114 &330585548
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 330585543}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 3e5890693b64a429789bf3edfae0a6ff, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: ProjectM.Client::ProjectM.Client.PrototypeCameraRig
|
||||
Pitch: 45
|
||||
Yaw: 0
|
||||
Distance: 16
|
||||
TargetHeight: 1
|
||||
Orthographic: 0
|
||||
FieldOfView: 55
|
||||
OrthoSize: 10
|
||||
FollowSharpness: 8
|
||||
FallbackTarget: {x: 3, y: 0, z: 4}
|
||||
--- !u!1 &410087039
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 410087041}
|
||||
- component: {fileID: 410087040}
|
||||
- component: {fileID: 410087042}
|
||||
m_Layer: 0
|
||||
m_Name: Directional Light
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!108 &410087040
|
||||
Light:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 410087039}
|
||||
m_Enabled: 1
|
||||
serializedVersion: 13
|
||||
m_Type: 1
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_Intensity: 2
|
||||
m_Range: 10
|
||||
m_SpotAngle: 30
|
||||
m_InnerSpotAngle: 21.80208
|
||||
m_CookieSize2D: {x: 10, y: 10}
|
||||
m_Shadows:
|
||||
m_Type: 2
|
||||
m_Resolution: -1
|
||||
m_CustomResolution: -1
|
||||
m_Strength: 1
|
||||
m_Bias: 0.05
|
||||
m_NormalBias: 0.4
|
||||
m_NearPlane: 0.2
|
||||
m_CullingMatrixOverride:
|
||||
e00: 1
|
||||
e01: 0
|
||||
e02: 0
|
||||
e03: 0
|
||||
e10: 0
|
||||
e11: 1
|
||||
e12: 0
|
||||
e13: 0
|
||||
e20: 0
|
||||
e21: 0
|
||||
e22: 1
|
||||
e23: 0
|
||||
e30: 0
|
||||
e31: 0
|
||||
e32: 0
|
||||
e33: 1
|
||||
m_UseCullingMatrixOverride: 0
|
||||
m_Cookie: {fileID: 0}
|
||||
m_DrawHalo: 0
|
||||
m_Flare: {fileID: 0}
|
||||
m_RenderMode: 0
|
||||
m_CullingMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_RenderingLayerMask: 1
|
||||
m_Lightmapping: 4
|
||||
m_LightShadowCasterMode: 0
|
||||
m_AreaSize: {x: 1, y: 1}
|
||||
m_BounceIntensity: 1
|
||||
m_ColorTemperature: 5000
|
||||
m_UseColorTemperature: 1
|
||||
m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_UseBoundingSphereOverride: 0
|
||||
m_UseViewFrustumForShadowCasterCull: 1
|
||||
m_ForceVisible: 0
|
||||
m_ShapeRadius: 0
|
||||
m_ShadowAngle: 0
|
||||
m_LightUnit: 1
|
||||
m_LuxAtDistance: 1
|
||||
m_EnableSpotReflector: 1
|
||||
--- !u!4 &410087041
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 410087039}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
|
||||
m_LocalPosition: {x: 0, y: 3, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
|
||||
--- !u!114 &410087042
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 410087039}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_UsePipelineSettings: 1
|
||||
m_AdditionalLightsShadowResolutionTier: 2
|
||||
m_CustomShadowLayers: 0
|
||||
m_LightCookieSize: {x: 1, y: 1}
|
||||
m_LightCookieOffset: {x: 0, y: 0}
|
||||
m_SoftShadowQuality: 1
|
||||
m_RenderingLayersMask:
|
||||
serializedVersion: 0
|
||||
m_Bits: 1
|
||||
m_ShadowRenderingLayersMask:
|
||||
serializedVersion: 0
|
||||
m_Bits: 1
|
||||
m_Version: 4
|
||||
m_LightLayerMask: 1
|
||||
m_ShadowLayerMask: 1
|
||||
m_RenderingLayers: 1
|
||||
m_ShadowRenderingLayers: 1
|
||||
--- !u!1 &832575517
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 832575519}
|
||||
- component: {fileID: 832575518}
|
||||
m_Layer: 0
|
||||
m_Name: Global Volume
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &832575518
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 832575517}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 172515602e62fb746b5d573b38a5fe58, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IsGlobal: 1
|
||||
priority: 0
|
||||
blendDistance: 0
|
||||
weight: 1
|
||||
sharedProfile: {fileID: 11400000, guid: 10fc4df2da32a41aaa32d77bc913491c, type: 2}
|
||||
--- !u!4 &832575519
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 832575517}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &833091043
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 833091047}
|
||||
- component: {fileID: 833091046}
|
||||
- component: {fileID: 833091045}
|
||||
- component: {fileID: 833091044}
|
||||
m_Layer: 0
|
||||
m_Name: Ground
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!23 &833091044
|
||||
MeshRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 833091043}
|
||||
m_Enabled: 1
|
||||
m_CastShadows: 1
|
||||
m_ReceiveShadows: 1
|
||||
m_DynamicOccludee: 1
|
||||
m_StaticShadowCaster: 0
|
||||
m_MotionVectors: 1
|
||||
m_LightProbeUsage: 1
|
||||
m_ReflectionProbeUsage: 1
|
||||
m_RayTracingMode: 2
|
||||
m_RayTraceProcedural: 0
|
||||
m_RayTracingAccelStructBuildFlagsOverride: 0
|
||||
m_RayTracingAccelStructBuildFlags: 1
|
||||
m_SmallMeshCulling: 1
|
||||
m_ForceMeshLod: -1
|
||||
m_MeshLodSelectionBias: 0
|
||||
m_RenderingLayerMask: 1
|
||||
m_RendererPriority: 0
|
||||
m_Materials:
|
||||
- {fileID: 2100000, guid: f5ef5fb55f211414595517e5ed7857b9, type: 2}
|
||||
m_StaticBatchInfo:
|
||||
firstSubMesh: 0
|
||||
subMeshCount: 0
|
||||
m_StaticBatchRoot: {fileID: 0}
|
||||
m_ProbeAnchor: {fileID: 0}
|
||||
m_LightProbeVolumeOverride: {fileID: 0}
|
||||
m_ScaleInLightmap: 1
|
||||
m_ReceiveGI: 1
|
||||
m_PreserveUVs: 1
|
||||
m_IgnoreNormalsForChartDetection: 0
|
||||
m_ImportantGI: 0
|
||||
m_StitchLightmapSeams: 1
|
||||
m_SelectedEditorRenderState: 3
|
||||
m_MinimumChartSize: 4
|
||||
m_AutoUVMaxDistance: 0.5
|
||||
m_AutoUVMaxAngle: 89
|
||||
m_LightmapParameters: {fileID: 0}
|
||||
m_GlobalIlluminationMeshLod: 0
|
||||
m_SortingLayerID: 0
|
||||
m_SortingLayer: 0
|
||||
m_SortingOrder: 0
|
||||
m_MaskInteraction: 0
|
||||
m_AdditionalVertexStreams: {fileID: 0}
|
||||
--- !u!64 &833091045
|
||||
MeshCollider:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 833091043}
|
||||
m_Material: {fileID: 0}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_LayerOverridePriority: 0
|
||||
m_IsTrigger: 0
|
||||
m_ProvidesContacts: 0
|
||||
m_Enabled: 1
|
||||
serializedVersion: 5
|
||||
m_Convex: 0
|
||||
m_CookingOptions: 30
|
||||
m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0}
|
||||
--- !u!33 &833091046
|
||||
MeshFilter:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 833091043}
|
||||
m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0}
|
||||
--- !u!4 &833091047
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 833091043}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 3, y: 0, z: 4}
|
||||
m_LocalScale: {x: 3, y: 1, z: 3}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1314640898
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1314640900}
|
||||
- component: {fileID: 1314640899}
|
||||
m_Layer: 0
|
||||
m_Name: GameplaySubScene
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &1314640899
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1314640898}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 45a335734b1572644a6a5d09d87adc65, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.Scenes::Unity.Scenes.SubScene
|
||||
_SceneAsset: {fileID: 102900000, guid: 9dc8ce2e486074792932d618c976e312, type: 3}
|
||||
_HierarchyColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
|
||||
AutoLoadScene: 1
|
||||
_SceneGUID:
|
||||
Value:
|
||||
x: 3807153369
|
||||
y: 2538014340
|
||||
z: 2171413394
|
||||
w: 557737884
|
||||
--- !u!4 &1314640900
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1314640898}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1660057539 &9223372036854775807
|
||||
SceneRoots:
|
||||
m_ObjectHideFlags: 0
|
||||
m_Roots:
|
||||
- {fileID: 330585546}
|
||||
- {fileID: 410087041}
|
||||
- {fileID: 832575519}
|
||||
- {fileID: 1314640900}
|
||||
- {fileID: 833091047}
|
||||
- {fileID: 121227141}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 071c9bf7daa2eb64fb15fde7c69bbe4b
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -16,7 +16,7 @@ Multiplayer game on **Unity DOTS (Entities) + Netcode for Entities** — server-
|
||||
| `com.unity.burst` | 1.8.29 | (transitive) |
|
||||
| `com.unity.mathematics` | 1.3.3 | (transitive) |
|
||||
|
||||
> ⚠️ The values above are the **Unity 6.4.7** set being reverted to — **verify against `packages-lock.json` after the editor downgrade re-resolves**, and reconcile `manifest.json` if the brief 6.6 upgrade left explicit version pins.
|
||||
> ✅ Reconciled 2026-06-02: `manifest.json` pins aligned to the resolved **Unity 6.4.7** lock (entities/entities.graphics 6.4.0, URP 17.4.0, test-framework 1.6.0, ugui 2.0.0, multiplayer.center 1.0.1) — a no-op re-resolve (lock unchanged, console clean). The values above now match `packages-lock.json`. See [[DR-008_M5_HomeBase_BaseLayer_Storage]].
|
||||
|
||||
> **Version history & status (2026-05-30):** built on **6.4.7** (`6000.4.7f1`; Netcode 1.13.2 / Physics 1.4.6 / Entities 6.4.0). Briefly upgraded to **6.6.0a6**, where Netcode→6.6.0, Physics→6.5.0, Entities→6.5.0 all **renumbered** into the editor line — BUT the alpha's **Netcode/Transport runtime is broken** (all in-editor connections fail with "invalid wrapped network interface"; **confirmed engine bug** via a zero-gameplay repro — see `Docs/Vault` DR-002 and `Docs/UnityBugReport-Netcode-Transport-6.6.0a6.md`). **→ Reverting to Unity 6.4.7 for stable netcode runtime.** If returning to 6.6 later, expect the renumber and re-test the runtime. The M1 player slice should port to 6.4 / Netcode 1.13.2 with no or minimal changes — recompile and `read_console` after the downgrade.
|
||||
|
||||
@@ -71,6 +71,17 @@ Root namespace: **`ProjectM`**. Code lives under `Assets/_Project/Scripts/` in f
|
||||
- **Do NOT copy the CC sample's global `LocalTransform → DontSerializeVariant`.** It is project-wide and would break the non-character ghosts here (projectiles/dummies/pickups) that rely on stock `LocalTransform` replication. Our CC character replicates position via the normal owner-predicted `LocalTransform` path; only the `CharacterInterpolation` variant is registered.
|
||||
- **Top-down CC config (planar, no gravity):** `AuthoringKinematicCharacterProperties` with `SnapToGround=false`, `InterpolateRotation=false` (rotation owned by `PlayerAimSystem`), `SimulateDynamicBody=false` (players don't physically push each other); gravity is handled in the processor by feeding `float3.zero` to `Update_GroundPushing` and never adding a gravity term. Result: stays on the spawn plane (y≈1) with no planar-pin system.
|
||||
|
||||
### Build gotchas (learned — M5 home base + shared storage, 2026-06-02)
|
||||
|
||||
- **Ownerless interpolated ghost ≠ owner-predicted for buffer replication.** A server-spawned **ownerless** ghost (e.g. the shared storage chest) replicates a `[GhostField]` `IBufferElementData` to all clients with **no `OwnerSendType` and no `GhostOwner`** — server mutations just propagate. `[GhostComponent(OwnerSendType = SendToOwnerType.All)]` (per `StatModifier`) is **only** for the predicting *owner* to recompute its own state; adding it (or a `GhostOwner`) to an ownerless ghost is wrong.
|
||||
- **One-off shared-state actions belong on an `IRpcCommand`, not a predicted `InputEvent`.** RPCs are reliable, so deposit/withdraw landed even while the server tick-batched (the M2 one-shot `Fire` InputEvent drops under batching). RPC payloads are **plain blittable fields — no `[GhostField]`**; store an op as a **byte**, not an enum (cross-assembly enum-Burst hazard, same one that de-Bursted `ProjectileClassificationSystem`). For a **single** shared target, resolve it as a **server singleton** — never put an `Entity` (not stable cross-world) in the command; only reach for a ghost-id+spawn-tick (`SpawnedGhostEntityMap`) when there are many targets (and that lookup may force the handler off Burst).
|
||||
- **Apply server-only RPC effects in the server `SimulationSystemGroup`, NOT the predicted loop** — the predicted loop re-runs on rollback and would double-apply. (Mutating a `DynamicBuffer` is not a structural change, so it's safe to do while iterating a *different* entity query, e.g. the RPC requests.)
|
||||
- **Build-grid math must be deterministic + integer-stable.** Corner-origin + center-returning + **half-open** cell bounds + `math.floor` (not truncation — negatives). Lock `CellSize`/`PlotSize` as a coordinate space once: M6 placement builds on it; changing them later invalidates placed structures. (`BaseGridMath`, unit-tested in EditMode like `PlayerSpawnMath`.)
|
||||
- **Runtime-spawn shared ghosts; don't bake them into the subscene.** A one-shot server spawner (mirrors `UpgradePickup`/`TrainingDummy`) keeps the subscene ghost-free and dodges the prespawn section-ack/CRC handshake. Do **not** add such a ghost to a connection's `LinkedEntityGroup` if it must survive that player's disconnect (the shared base is world-owned).
|
||||
- **Build a correctly-configured ghost prefab by duplicating an existing one** (`UpgradePickup.prefab` → `Storage.prefab`, then swap the authoring MonoBehaviour via `manage_prefabs modify_contents`) rather than hand-adding `GhostAuthoringComponent` — its ownerless/interpolated settings (`HasOwner=0`, `DefaultGhostMode=Interpolated`) + `LinkedEntityGroupAuthoring` come along for free.
|
||||
- **`execute_code` runs as a method body** — **no `using` directives** (they parse as statements → "Identifier expected"); fully-qualify every type (`Unity.Entities.World`, `ProjectM.Simulation.BaseAnchor`, …). Also: world flags overlap a shared `Game` bit, so identify worlds by `world.Name == "ServerWorld"/"ClientWorld"` rather than `(Flags & GameServer)`.
|
||||
- **An unfocused editor throttles Edit mode to near-idle** → MCP pings time out and the bridge looks hung (it still *queues* commands — `telemetry_ping` succeeds). `Application.runInBackground` only helps in **Play** mode. If it wedges, focus or restart the editor; don't pile `refresh_unity` calls onto a blocked main thread. Prefer `refresh_unity scope=scripts` for code-only changes (`scope=all force` is heavy and contributed to a mid-session hang).
|
||||
|
||||
## Bootstrap & worlds
|
||||
|
||||
- `ProjectM.Simulation.GameBootstrap : ClientServerBootstrap` → overrides `Initialize`, sets `AutoConnectPort = 7979` (in-editor auto-connect over IPC; set in M1 — was 0), calls `CreateDefaultClientServerWorlds()`. Entering Play Mode creates separate `ServerWorld` (`WorldFlags.GameServer`) and `ClientWorld` (`WorldFlags.GameClient`) — verified.
|
||||
|
||||
@@ -30,6 +30,12 @@ One design doc per gameplay system, linked here. Each should state: purpose, com
|
||||
- **Systems:** `StatRecomputeSystem` (predicted, `[UpdateBefore]` Aim/Move; folds blob base + modifier buffer → `Effective*` **every tick** — rollback-correct); `AbilityFireSystem` rerouted (effective stats + prefab-by-id + snapshot-at-fire); `PlayerMoveSystem` → effective move; `UpgradePickupSpawnSystem` / `UpgradePickupSystem` (server; overlap-grant via `AppendToBuffer`); `DebugModifierInjectionSystem` (editor-only, server world); `HealthApplyDamageSystem` clamps to effective MaxHealth. Authoring: `AbilityDefinition`/`CharacterStatsDefinition` SOs + `AbilityDatabaseAuthoring` blob baker.
|
||||
- **Netcode shape:** definitions = baked config (not replicated, identical both worlds); modifiers = **replicated ghost buffer** on the player → both worlds recompute identical effective stats (prediction-correct, validated under tick-batching); pickup = **interpolated** server-authoritative ghost. Status: **built + runtime-validated** (EditMode 38/38). Decisions: [[DR-004_M3_DataDriven_Abilities_Modifiers]].
|
||||
|
||||
### M5 — Home base: base-layer + shared storage · [[2026-06-02_M5_HomeBase_BaseLayer]]
|
||||
|
||||
- **Components** (`ProjectM.Simulation/HomeBase`): `BaseAnchor` (baked singleton — `AnchorPos`, `GridOrigin`, `CellSize`, `int2 GridDims`; flat/blittable, no entity refs); `BaseGridMath` (pure static — WorldToCell/CellToWorld/IsCellInPlot/IsPointInPlot/ClampCell/PlotCenter; corner-origin, center-returning, half-open, floor); `StorageEntry` (`[GhostField]` buffer — `ushort ItemId`, `int Count`); `SharedStorageContainer` (tag); `StorageSpawner` (baked singleton — prefab + `int2 Cell`); `StorageOpRequest` (`IRpcCommand` — byte Op/ItemId/Count) + `StorageOp` consts; `StorageMath` (deposit-merge / withdraw-clamp-drop, unit-tested).
|
||||
- **Systems:** `SharedStorageSpawnSystem` (server one-shot — instantiate the container ghost at `CellToWorld(cell)`, destroy spawner); `StorageOpReceiveSystem` (server `SimulationSystemGroup`, NOT predicted — apply the RPC to the singleton container's buffer via `StorageMath`); `StorageOpSendSystem` (client managed `SystemBase` — E/Q keyboard + editor-only `Deposit`/`Withdraw` statics → `StorageOpRequest` RPC). `GoInGameServerSystem` re-rooted onto `BaseGridMath.PlotCenter(BaseAnchor)` (with a `TryGetSingleton` fallback).
|
||||
- **Netcode shape:** base config = **baked, ghost-free, identical both worlds** (not replicated). Storage container = **ownerless interpolated** server-spawned ghost; its `StorageEntry` buffer is a `[GhostField]` (no `OwnerSendType`/`GhostOwner`) so server mutations replicate to all clients. Deposit/withdraw = **server-authoritative `IRpcCommand`** resolved against the single container singleton, applied outside the predicted loop (no rollback double-apply). Status: **built + runtime-validated** (server == client buffer; EditMode 62/62). Decisions: [[DR-008_M5_HomeBase_BaseLayer_Storage]]. M6 (grid placement) + M7 (production) build on `BaseGridMath` + the runtime-ghost-into-cell spawn path.
|
||||
|
||||
## Conventions
|
||||
|
||||
DOTS/ECS conventions live in repo `CLAUDE.md` and the `dots-dev` skill's `dots-conventions.md`. Don't duplicate volatile API details here — link to context7-derived notes instead.
|
||||
@@ -16,7 +16,7 @@ Unordered pool of candidate work. Promote to a [[Milestones|milestone]] when com
|
||||
- [ ] Replace template `SampleScene` with a dedicated bootstrap scene + gameplay subscene.
|
||||
- [ ] Optional template cleanup: remove `com.unity.visualscripting`, `Assets/TutorialInfo/`, `Assets/Readme.asset` (delete each asset **with** its `.meta`).
|
||||
- [x] Decide **relay provider** before M4 — resolved: **Direct IP/LAN now, Unity Relay later** ([[DR-005_M4_Connection_Model_Direct_IP]], [[2026-06-01_M4_LAN_CoOp_And_Classification_Fix]]).
|
||||
- [ ] Decide home-base **grid 2D vs 3D** before M6 (build/placement).
|
||||
- [x] Decide home-base **grid 2D vs 3D** before M6 — resolved 2026-06-02: **planar single-level `int2` grid**, CellSize 1.0, 32×32 plot (full 3D/stacked deferred). Locked in `BaseGridMath` — [[DR-008_M5_HomeBase_BaseLayer_Storage]].
|
||||
- [ ] Decide **production replication** (predicted vs server-only) before M7 (automation).
|
||||
- [ ] **M2 follow-up — restart the editor to clear the corrupted Burst cache**, then confirm the console is clean on a warm play (no "not a known Burst entry point"). See [[2026-05-31_M2_Combat]] / [[DR-003_M2_Combat_Netcode_Architecture]].
|
||||
- [ ] **M2 follow-up — live interactive fire test** (focused Play Mode: press Space / LMB / RT → predicted projectile + dummy HP drop). The server combat loop + replication are validated; the input→`AbilityFireSystem`→predicted-spawn→classification path is only validated structurally.
|
||||
@@ -29,7 +29,11 @@ Unordered pool of candidate work. Promote to a [[Milestones|milestone]] when com
|
||||
- [ ] **M3 follow-up — standalone-server debug modifier path** via `IRpcCommand` (current `DebugModifierInjectionSystem` is in-editor single-process only).
|
||||
- [ ] **M3 follow-up — rate-limited turning** (`PlayerAimSystem` still snaps rotation; `EffectiveCharacterStats.TurnRate` is wired but unused).
|
||||
- [ ] **M3 polish — pickup visuals** (primitive sphere/default material currently); pickup auto-grant feel (continuous overlap).
|
||||
- [ ] **M5 — base subscene streaming** (the other half of M5): persistent home-base subscene that streams in/out around players. Physics-in-prediction slice is done ([[DR-006_M5_Physics_In_Prediction]]).
|
||||
- [ ] **M5 follow-up — base/expedition subscene split + streaming (Option C)**: the persistent-space split the locked world design ultimately needs (`SceneSystem.LoadSceneAsync`/`UnloadScene`, per-world load on the listen-server, enter-expedition/return-to-base transition). Deferred to its own world-architecture milestone — M6/M7 only need the anchor + grid, now done ([[DR-008_M5_HomeBase_BaseLayer_Storage]]). The physics-in-prediction + base-layer slices of M5 are done ([[DR-006_M5_Physics_In_Prediction]], [[DR-008_M5_HomeBase_BaseLayer_Storage]]).
|
||||
- [ ] **M5 follow-up — shared-storage disk persistence** (host-only): runtime structures don't exist until M6, so nothing to save yet; add a thin per-record serialization slice (replayed through M6's placement path) after M6. `BaseAnchor`/`StorageEntry` are already flat/serialization-friendly.
|
||||
- [ ] **M5 follow-up — storage interaction polish**: proximity gate the deposit/withdraw (the container carries `HitRadius`); real item/UI model beyond the fixed test item; multi-writer ordering beyond first-come server apply.
|
||||
- [ ] **M5 follow-up — multi-client shared storage**: validate two clients see identical shared-storage buffer state (pairs with the deferred M5b multi-client interpolation + M4 two-build tests).
|
||||
- [ ] **M5 follow-up — home-base visuals**: only an editor plot-bounds gizmo + a placeholder primitive container today; real ground/walls/structures arrive with M6.
|
||||
- [ ] ~~**M5 follow-up — same-tick movement**~~ — moot after M5b (the CC character runs in the predicted fixed-step group; M5's `PlayerMoveSystem` is deleted). See [[DR-007_M5b_Character_Controller_Package]].
|
||||
- [ ] **M5b follow-up — multi-client CC interpolation validation**: live two-build / thin-client run to confirm remote-peer interpolation smoothness (predicted-only `CharacterInterpolation` variant is in; only single-client validated). Pairs with the deferred M4 real-LAN two-build test.
|
||||
- [ ] **M5b follow-up — player-vs-player collision**: currently non-physical (`SimulateDynamicBody=false`); enable + handle masses if mutual push is wanted.
|
||||
|
||||
@@ -14,8 +14,8 @@ permalink: gamevault/06-roadmap/milestones
|
||||
| **M1 — Player slice** | Server-spawned owner-predicted player; twin-stick WASD + directional aim | ✅ Done 2026-05-31 — runtime-validated on Unity 6.4.7 (connect→spawn→owner-predicted ghost→replication; EditMode 3/3). The 6.6 failure was environment-specific, see [[DR-002_Unity66_Alpha_Netcode_Transport]] — [[2026-05-30_M1_Player_Slice]] |
|
||||
| **M2 — Combat** | Directional ability fire + deterministic soft auto-target; server-authoritative damage/health | ✅ Done 2026-05-31 — runtime-validated on 6.4.7: input→fire→**predicted projectile**→**swept hit**→server damage→`Health` `[GhostField]` replicated server→client; movement + fire confirmed live; EditMode 22/22. Predicted-projectile + server auto-target + non-Burst classifier — [[DR-003_M2_Combat_Netcode_Architecture]], [[2026-05-31_M2_Combat]]. (Projectile ghost-map errors were later root-caused to a `[ReadOnly]`-write in `ProjectileClassificationSystem` — fixed 2026-06-01, see [[2026-06-01_M4_LAN_CoOp_And_Classification_Fix]] — NOT two-editor tick-batching as first thought.) |
|
||||
| **M3 — Data-driven abilities & modifiers** | Ability **and** character stats authored in ScriptableObjects, baked to DOTS **blob assets**; runtime **flat + % modifier** stacks (upgrades/buffs) → effective stats, server-authoritative + prediction-correct. Pattern slice: refactor the current projectile ability + 1–2 sample abilities onto the data model. | ✅ Done 2026-05-31 — runtime-validated on 6.4.7: blob DB baked into both worlds; data-driven base + replicated `StatModifier` ghost buffer → **identical effective stats on server & owner-predicted client** (held under tick-batching); data-only ability swap; real pickup grant; EditMode 38/38. Blob DB + replicated modifier buffer + every-tick effective recompute — [[DR-004_M3_DataDriven_Abilities_Modifiers]], [[2026-05-31_M3_Data_Driven_Abilities]]. |
|
||||
| **M4 — Co-op** | 2–4 players; client-hosted listen-server (Direct IP/LAN now, Unity Relay later) | 🚧 In progress 2026-06-01 — **LAN slice done + runtime-validated**: no-auto-connect `ConnectionConfig` + request-component host/join, editor auto-host + thin clients, deterministic ring spawn; 3 clients (1 real + 2 thin) connect→spawn (distinct slots)→replicate→clean disconnect; `ConnectionUI` for builds; EditMode 45/45. **Unity Relay + real two-build LAN join deferred** — [[DR-005_M4_Connection_Model_Direct_IP]], [[2026-06-01_M4_LAN_CoOp_And_Classification_Fix]]. |
|
||||
| **M5 — Home base + physics** | Persistent base subscene streaming + Unity Physics in the predicted loop | 🚧 In progress 2026-06-01 — **physics-in-prediction slice done + runtime-validated** on 6.4.7: player is a velocity-driven dynamic Unity Physics body in the predicted loop (built-in `CapsuleCollider`+`Rigidbody` bake; `PhysicsVelocity` auto-replicated), collides with baked static walls (stops at the surface, no tunnel/climb-over), planar-pinned, **server == client** with no desync; EditMode 51/51. **Base subscene streaming deferred** to a later pass — [[DR-006_M5_Physics_In_Prediction]], [[2026-06-01_M5_Physics_In_Prediction]]. **M5b (same day): player movement re-founded on the Unity Character Controller package** (`com.unity.charactercontroller` 1.4.2) — kinematic collide-and-slide, owner-predicted, data-driven speed; replaces the dynamic-Rigidbody mover (keeps the DR-006 predicted-physics infra). Runtime-validated (collide-and-slide, planar, server==client, CharacterInterpolation predicted-only); EditMode 47/47 — [[DR-007_M5b_Character_Controller_Package]], [[2026-06-01_M5b_Character_Controller]]. |
|
||||
| **M4 — Co-op** | 2–4 players; client-hosted listen-server (Direct IP/LAN now, Unity Relay later) | ✅ Done 2026-06-02 — **LAN slice + multi-client validated**: no-auto-connect `ConnectionConfig` + request-component host/join, editor auto-host + thin clients, deterministic ring spawn; 3 clients (1 real + 2 thin) connect→spawn (distinct slots)→replicate→clean disconnect; `ConnectionUI` for builds; EditMode 45/45. **Two controllable characters in-game confirmed 2026-06-02 via Unity Multiplayer Play Mode** (extra virtual player; full connection handshake not exercised end-to-end, but in-scene co-op looks good). **Unity Relay + real two-build LAN join deferred** — [[DR-005_M4_Connection_Model_Direct_IP]], [[2026-06-01_M4_LAN_CoOp_And_Classification_Fix]]. |
|
||||
| **M5 — Home base + physics** | Persistent base subscene streaming + Unity Physics in the predicted loop | 🚧 In progress 2026-06-01 — **physics-in-prediction slice done + runtime-validated** on 6.4.7: player is a velocity-driven dynamic Unity Physics body in the predicted loop (built-in `CapsuleCollider`+`Rigidbody` bake; `PhysicsVelocity` auto-replicated), collides with baked static walls (stops at the surface, no tunnel/climb-over), planar-pinned, **server == client** with no desync; EditMode 51/51. **Base subscene streaming deferred** to a later pass — [[DR-006_M5_Physics_In_Prediction]], [[2026-06-01_M5_Physics_In_Prediction]]. **M5b (same day): player movement re-founded on the Unity Character Controller package** (`com.unity.charactercontroller` 1.4.2) — kinematic collide-and-slide, owner-predicted, data-driven speed; replaces the dynamic-Rigidbody mover (keeps the DR-006 predicted-physics infra). Runtime-validated (collide-and-slide, planar, server==client, CharacterInterpolation predicted-only); EditMode 47/47 — [[DR-007_M5b_Character_Controller_Package]], [[2026-06-01_M5b_Character_Controller]]. **Base-layer done 2026-06-02 + runtime-validated:** home base = baked ghost-free `BaseAnchor` + locked deterministic planar build-grid (`BaseGridMath`, 1.0u × 32²; M6 builds on it) + player spawn re-rooted onto the anchor + one **shared-storage ghost** (ownerless interpolated; deposit/withdraw via server-authoritative `IRpcCommand`; **server == client** buffer). EditMode 62/62. **Subscene split (base/expedition) + disk persistence still deferred** — [[DR-008_M5_HomeBase_BaseLayer_Storage]], [[2026-06-02_M5_HomeBase_BaseLayer]]. |
|
||||
| **M6 — Build/placement** | Server-authoritative grid build placement via RPC | ⬜ |
|
||||
| **M7 — Automation** | Self-running tick-based production chains (deterministic offline catch-up) | ⬜ |
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
date: 2026-06-02
|
||||
type: session
|
||||
tags: [session, dots, netcode, home-base, rpc, ghost, m5, m4]
|
||||
permalink: gamevault/07-sessions/2026/2026-06-02-m5-homebase-baselayer
|
||||
---
|
||||
|
||||
# Session 2026-06-02 — M5: home-base base-layer + shared storage
|
||||
|
||||
## Goal
|
||||
|
||||
Close out M4 and take the next milestone via `/dots-dev`. Operator closed **M4** (multi-client co-op validated via Unity **Multiplayer Play Mode** — two controllable characters in-scene; full handshake not exercised end-to-end, Relay still deferred). For **M5**, clarified what "home base" means for this game and built the operator-chosen **Option B**: a fixed Base Anchor + planar build-grid coordinate space + spawn re-root, **plus** one shared storage container ghost (deposit/withdraw via RPC). Locked in [[DR-008_M5_HomeBase_BaseLayer_Storage]].
|
||||
|
||||
## Process
|
||||
|
||||
- **Scoping research workflow** (4 parallel readers → synthesis): design/genre meaning, DOTS/Netcode subscene+streaming feasibility, codebase scene state, persistence/world-arch → a grounded "home base" definition + 3 scoped options (A/B/C). Operator picked **B**; grid **1.0u × 32²** planar; storage = **(itemId, count)**; persistence = in-session only (no disk); one shared base; manifest reconcile folded in.
|
||||
- **Design/verify workflow** (3 parallel → I synthesized the plan): extracted exact in-repo templates (RPC `GoInGameRequest`, `StatModifier` ghost buffer, `UpgradePickup` server-spawned interpolated ghost, ring spawn, baked-singleton bakers); **context7 was unreachable** so Netcode 1.13.2 shapes were verified by reflection on the installed assemblies + the project's own proven usages; locked the deterministic grid math.
|
||||
- **Implementation** (sequential via MCP `create_script`/`apply_text_edits` — single editor, domain-reload-ordered; NOT a parallel swarm): components+math → tests → server systems+spawn re-root → client send → authoring → prefab/subscene wiring → runtime validation.
|
||||
|
||||
## Done
|
||||
|
||||
- **New (`ProjectM.Simulation/HomeBase`):** `BaseAnchor`; `BaseGridMath` (WorldToCell/CellToWorld/IsCellInPlot/IsPointInPlot/ClampCell/PlotCenter — corner-origin, center-returning, half-open, floor); `StorageEntry` (`[GhostField]` buffer, ownerless); `SharedStorageContainer` tag; `StorageSpawner`; `StorageOpRequest` + `StorageOp` byte consts; `StorageMath` (deposit-merge / withdraw-clamp-drop).
|
||||
- **New (`ProjectM.Server/HomeBase`):** `SharedStorageSpawnSystem` (one-shot, spawns the container ghost at `CellToWorld(cell)`); `StorageOpReceiveSystem` (server RPC apply via `StorageMath`, singleton container, outside the predicted loop).
|
||||
- **New (`ProjectM.Client/HomeBase`):** `StorageOpSendSystem` (managed `SystemBase`; E/Q keyboard edge + editor-only `Deposit`/`Withdraw` statics → `StorageOpRequest` RPC to the server connection).
|
||||
- **New (`ProjectM.Authoring/HomeBase`):** `BaseAnchorAuthoring` (bakes `BaseAnchor` from the GameObject position + plot gizmo); `SharedStorageContainerAuthoring`; `StorageSpawnerAuthoring`.
|
||||
- **Modified:** `GoInGameServerSystem` — spawn ring re-rooted on `BaseGridMath.PlotCenter(BaseAnchor)` with a `TryGetSingleton` fallback. `Packages/manifest.json` — stale 6.6-era pins reconciled to the resolved 6.4.7 lock.
|
||||
- **Assets:** `Storage.prefab` (duplicated from `UpgradePickup.prefab` → swapped to `SharedStorageContainerAuthoring`, kept the ownerless-interpolated `GhostAuthoringComponent`). `Gameplay.unity` subscene — added `BaseAnchor` (at (0,1,0)) + `StorageSpawner` (→ Storage.prefab, cell (19,19)) authoring GameObjects.
|
||||
- **Tests:** `BaseGridMathTests` (7) + `StorageMathTests` (8). **EditMode 62/62.**
|
||||
|
||||
## Validation
|
||||
|
||||
- **EditMode 62/62 green** (+15 vs M5b's 47).
|
||||
- **Runtime (single in-editor client, 6.4.7):** `BaseAnchor` baked **identically into both worlds**; player **spawn re-rooted** onto the anchor (spawned at (2.5,1,0)); storage ghost **server-spawned + replicated** to the client at (3.5,1,3.5) = `CellToWorld(19,19)`; **deposit (1×5, 2×3)** then **withdraw (decrement + clamp + drop-empty)** → **server == client buffer** every time, driven through the real RPC→server→replication path; RPCs survived the tick-batching artifact. Console clean of code/Burst/ghost/RPC errors — only the known unfocused-editor tick-batching warning.
|
||||
|
||||
## Decisions
|
||||
- [[DR-008_M5_HomeBase_BaseLayer_Storage]] — home base = baked ghost-free `BaseAnchor` + locked deterministic `BaseGridMath` grid (M6 builds on it) + spawn re-root + one ownerless-interpolated shared-storage ghost mutated by a server-authoritative RPC (singleton-resolved, byte op, outside the predicted loop). Streaming + disk persistence deferred.
|
||||
|
||||
## Diagnosis notes (for future me)
|
||||
- **`execute_code` runs as a method body** — no `using` directives allowed (they parse as statements → "Identifier expected"); **fully-qualify** all types (`Unity.Entities.World`, `ProjectM.Simulation.BaseAnchor`, …).
|
||||
- **Ownerless interpolated ghost ≠ owner-predicted ghost for buffer replication:** a server-spawned ownerless chest needs **no `OwnerSendType`/`GhostOwner`** — `[GhostField]` alone replicates server mutations to all. `OwnerSendType.All` (per `StatModifier`) is only for the predicting *owner* to recompute.
|
||||
- **RPC > predicted InputEvent for one-off shared-state actions:** reliable delivery meant deposit/withdraw landed even while the editor tick-batched (the M2 one-shot `Fire` InputEvent drops under batching).
|
||||
- **The editor hung mid-session** (unresponsive bridge: queued commands accepted, pings unanswered) while unfocused — Edit-mode throttles to near-idle when the window lacks OS focus (`Application.runInBackground` only helps in Play mode). Operator **restarted the editor**; it recovered clean. Avoid piling `refresh_unity` calls onto a blocked main thread; wait or ask to focus/restart.
|
||||
- **`scope=all force` refresh is heavy** — fine on a fresh editor, but it (plus an unfocused throttle) likely contributed to the hang. Prefer `scope=scripts` for code-only changes.
|
||||
|
||||
## Open / deferred
|
||||
- **Option C** (base/expedition subscene split + async `SceneSystem` streaming) — own world-architecture milestone; M6/M7 don't need it.
|
||||
- **Disk persistence** — nothing to save until M6 produces structures; thin host-only slice afterward.
|
||||
- **Storage polish** — proximity gate (container has `HitRadius`), real item/UI model, multi-writer ordering beyond first-come.
|
||||
- **Multi-client storage** — validate two clients see identical shared state (pairs with the deferred M5b multi-client interpolation + M4 two-build tests).
|
||||
|
||||
## Next
|
||||
Begin **M6 — server-authoritative grid build placement via RPC**, reusing `BaseGridMath` (legality + snap) and the runtime-ghost-into-base-cell spawn path from this slice.
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
id: DR-008
|
||||
title: M5 — Home-base base-layer (BaseAnchor + build grid) + shared storage container
|
||||
status: accepted
|
||||
date: 2026-06-02
|
||||
tags:
|
||||
- decision
|
||||
- netcode
|
||||
- home-base
|
||||
- rpc
|
||||
- ghost
|
||||
- m5
|
||||
permalink: gamevault/07-sessions/decisions/dr-008-m5-homebase-baselayer-storage
|
||||
---
|
||||
|
||||
# DR-008 — M5 Home-base base-layer + shared storage (Option B)
|
||||
|
||||
## Context
|
||||
|
||||
M5's physics half (predicted physics [[DR-006_M5_Physics_In_Prediction]] + Character Controller [[DR-007_M5b_Character_Controller_Package]]) was done; the remaining half — sketched loosely as "persistent home-base subscene that streams in/out" — needed its meaning pinned down. A read-only research workflow grounded "home base" in the [[Pillars]] (co-op ARPG + V Rising-style shared persistent buildable base + Factorio automation; base vs instanced expeditions) and produced three scoped options: **A** anchor + claimed plot + build-grid config + spawn re-root; **B** = A + one shared-storage ghost; **C** = A + base/expedition subscene split + async streaming. The operator chose **Option B** with: planar `int2` build grid, **CellSize 1.0**, **32×32** plot; storage items as **(itemId, count)** pairs; in-session ("survives logout") persistence only — **no disk save/load**; one **shared** world-owned base. Subscene split + streaming (**C**) explicitly deferred — M6/M7 need only the anchor + grid, not streaming.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **Home base = a baked, ghost-free config singleton + one runtime shared-storage ghost — NOT a building/save/streaming system.** `BaseAnchor` (`IComponentData`, flat/blittable, no entity refs) carries `AnchorPos`, `GridOrigin` (min-XZ corner of cell (0,0)), `CellSize`, `GridDims`. Baked into the gameplay subscene via `BaseAnchorAuthoring` (`TransformUsageFlags.None`; reads the GameObject position as `AnchorPos`, derives `GridOrigin` centered on it). Present identically in both worlds (baked, not replicated) — the same pattern as `PlayerSpawner`/`AbilityDatabase`/`NetCodePhysicsConfig`.
|
||||
2. **`BaseGridMath` is the locked, deterministic, unit-tested coordinate space M6 builds on.** Corner-origin, center-returning, **half-open** cell bounds, `math.floor` world→cell (negative-correct). `WorldToCell`/`CellToWorld`/`IsCellInPlot`/`IsPointInPlot`/`ClampCell`/`PlotCenter`. M6's server placement handler will call `IsPointInPlot` (legality) + `CellToWorld` (snap) directly. **CellSize/PlotSize are treated as a locked coordinate space** — changing them after M6 builds invalidates placement.
|
||||
3. **Spawn is re-rooted onto the anchor.** `GoInGameServerSystem` now uses `BaseGridMath.PlotCenter(BaseAnchor)` as the ring center when the anchor singleton is present, falling back to `PlayerSpawner.SpawnPoint` if the base subscene hasn't streamed in yet (`SystemAPI.TryGetSingleton`). Ring radius/slots stay on `PlayerSpawner`. (Anchor placed at the existing spawn plane (0,1,0) so the re-root is behaviour-preserving.)
|
||||
4. **Shared storage = an ownerless INTERPOLATED ghost with a replicated `[GhostField]` buffer.** `StorageEntry` (`IBufferElementData`, `[GhostField] ushort ItemId` + `int Count`, `[InternalBufferCapacity(16)]`) on a `SharedStorageContainer`-tagged ghost. The container is **ownerless** → **no `OwnerSendType`, no `GhostOwner`** (those are only for owner-predicted ghosts like the player's `StatModifier` buffer); server mutations auto-replicate to all clients. The container is **server-spawned at runtime** (one-shot `SharedStorageSpawnSystem` reads a baked `StorageSpawner` + `BaseAnchor`, instantiates the ghost prefab at `CellToWorld(cell)`, destroys the spawner) — NOT baked into the subscene (keeps the subscene ghost-free, dodges the prespawn section-ack handshake). It is **not** added to any connection's `LinkedEntityGroup`, so it survives player disconnects.
|
||||
5. **Deposit/withdraw = an `IRpcCommand`, applied server-authoritatively outside the predicted loop.** `StorageOpRequest { byte Op; ushort ItemId; int Count }` — a one-off action, so an RPC, not a per-tick predicted input. **Op is a byte** (not an enum) to keep the generated serializer trivial / dodge the cross-assembly enum-Burst hazard; **plain blittable fields, no `[GhostField]`** (RPC payloads auto-serialize). **No target entity is carried** — M5 has a single shared container the server resolves as a **singleton** (entity refs aren't stable cross-world; this avoids the ghost-id+spawn-tick `SpawnedGhostEntityMap` lookup, which also keeps the handler Burst-clean). `StorageOpReceiveSystem` runs in the server `SimulationSystemGroup` (NOT the prediction loop → no rollback double-apply), applies via the pure `StorageMath.Deposit/Withdraw`, and destroys the request. `StorageOpSendSystem` (client, managed `SystemBase`) sends on a keyboard edge (E/Q, fully-qualified Input System to dodge the `PlayerInput` name collision) or an editor-only static (`Deposit`/`Withdraw`) for headless validation.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Validated at runtime on 6.4.7 (single in-editor client).** `BaseAnchor` baked identically into **both** worlds (AnchorPos (0,1,0), GridOrigin (-16,1,-16), 1.0u × 32²). Player **spawn re-rooted** onto the anchor → spawned at (2.5,1,0) (PlotCenter + ring slot 0). Storage ghost **server-spawned and replicated to the client** at (3.5,1,3.5) = `CellToWorld(cell 19,19)`. **Deposit** (1×5, 2×3) and **Withdraw** (decrement + clamp-to-available + drop-empty-row) applied server-side and replicated — **server == client buffer** in every case. The RPCs propagated correctly **despite the in-editor tick-batching artifact** (RPCs are reliable, unlike the one-shot `Fire` InputEvent that can drop under batching). EditMode **62/62** (+15: 7 `BaseGridMath`, 8 `StorageMath`). Console clean of code/Burst/ghost/RPC errors — only the known unfocused-editor tick-batching warning.
|
||||
- **Foundation for M6/M7.** M6 grid placement reuses `BaseGridMath` (legality + snap) and the runtime-ghost-into-base-cell spawn path verbatim; M7 production machines generalise `StorageEntry` into input/output buffers + a server tick. The flat, entity-ref-free `BaseAnchor`/`StorageEntry` shapes are serialization-ready for the deferred disk-persistence slice.
|
||||
- **No new asmdefs.** Everything fits the existing Simulation/Server/Client/Authoring split; all needed references (`Unity.NetCode`, `Unity.Physics`, `Unity.Transforms`) were already declared. New code lives under `…/HomeBase/` in each assembly.
|
||||
- **Housekeeping:** stale `manifest.json` pins from the brief 6.6 upgrade (entities/entities.graphics 6.5.0, URP 17.6.0, test-framework 1.8.0, ugui 2.6.0, multiplayer.center 2.0.0) **reconciled to the resolved 6.4.7 lock** (a no-op re-resolve; lock unchanged, console clean) — closes the [[CLAUDE]] pending-reconcile TODO.
|
||||
|
||||
## Open / deferred
|
||||
|
||||
- **Base/expedition subscene split + async streaming (Option C)** — the persistent-space split the locked world design ultimately needs (`SceneSystem.LoadSceneAsync`/`UnloadScene`, per-world load on the listen-server). Deferred to its own world-architecture milestone; M6/M7 don't depend on it.
|
||||
- **Disk persistence** — runtime structures don't exist until M6, so there's nothing to save yet. A thin host-only per-structure serialization slice (replayed through M6's placement path) comes after M6.
|
||||
- **Storage interaction polish** — deposit/withdraw is non-proximity-gated and uses a fixed test item; add an interact-range check (the container carries `HitRadius`) and a real item/UI model later. Multi-writer ordering is currently first-come server apply (fine for the prototype).
|
||||
- **Plot footprint / base visuals** — only an editor gizmo (plot bounds) + a placeholder primitive container; real ground/walls/structures arrive with M6.
|
||||
- **Multi-client storage** — validated single-client; a live two-build / Multiplayer-Play-Mode check that two clients see identical shared-storage state pairs with the deferred M5b multi-client interpolation test.
|
||||
|
||||
Mirrors the server-authoritative + deterministic + co-op pillars from [[Pillars]]. Builds on [[DR-006_M5_Physics_In_Prediction]] / [[DR-007_M5b_Character_Controller_Package]] (kept infra), [[DR-005_M4_Connection_Model_Direct_IP]] (spawn/co-op), [[DR-004_M3_DataDriven_Abilities_Modifiers]] (the replicated-buffer + byte-enum patterns reused here).
|
||||
@@ -5,22 +5,22 @@
|
||||
"com.unity.charactercontroller": "1.4.2",
|
||||
"com.unity.cinemachine": "3.1.6",
|
||||
"com.unity.collab-proxy": "2.12.4",
|
||||
"com.unity.entities": "6.5.0",
|
||||
"com.unity.entities.graphics": "6.5.0",
|
||||
"com.unity.entities": "6.4.0",
|
||||
"com.unity.entities.graphics": "6.4.0",
|
||||
"com.unity.ide.rider": "3.0.40",
|
||||
"com.unity.ide.visualstudio": "2.0.27",
|
||||
"com.unity.inputsystem": "1.19.0",
|
||||
"com.unity.multiplayer.center": "2.0.0",
|
||||
"com.unity.multiplayer.center": "1.0.1",
|
||||
"com.unity.multiplayer.playmode": "2.0.2",
|
||||
"com.unity.multiplayer.tools": "2.2.8",
|
||||
"com.unity.netcode": "1.13.2",
|
||||
"com.unity.physics": "1.4.6",
|
||||
"com.unity.probuilder": "6.0.9",
|
||||
"com.unity.render-pipelines.universal": "17.6.0",
|
||||
"com.unity.render-pipelines.universal": "17.4.0",
|
||||
"com.unity.services.multiplayer": "2.1.3",
|
||||
"com.unity.test-framework": "1.8.0",
|
||||
"com.unity.test-framework": "1.6.0",
|
||||
"com.unity.timeline": "1.8.12",
|
||||
"com.unity.ugui": "2.6.0",
|
||||
"com.unity.ugui": "2.0.0",
|
||||
"com.unity.visualeffectgraph": "17.4.0",
|
||||
"com.unity.visualscripting": "1.9.11",
|
||||
"com.unity.modules.accessibility": "1.0.0",
|
||||
|
||||
Reference in New Issue
Block a user