diff --git a/Assets/_Project/Prefabs/Conveyor.prefab b/Assets/_Project/Prefabs/Conveyor.prefab deleted file mode 100644 index c5a9149c0..000000000 --- a/Assets/_Project/Prefabs/Conveyor.prefab +++ /dev/null @@ -1,147 +0,0 @@ -%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: 2432281660239002494} - m_Layer: 0 - m_Name: Conveyor - 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: 2.5, y: 2.5, z: 2.5} - 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: 4300000, guid: abc00000000010690097314383055197, type: 3} ---- !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: c01dbe8a4e818c5469eceae8b286bf4d, 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 &2432281660239002494 -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: 23131e6166dce204582bbedc8511658e, type: 3} - m_Name: - m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.ConveyorAuthoring - Direction: 0 - PeriodTicks: 20 diff --git a/Assets/_Project/Prefabs/Conveyor.prefab.meta b/Assets/_Project/Prefabs/Conveyor.prefab.meta deleted file mode 100644 index c0eb1ab59..000000000 --- a/Assets/_Project/Prefabs/Conveyor.prefab.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 91f9551ef855b3b4f98bcfcb7bf747e2 -PrefabImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/_Project/Prefabs/Fabricator.prefab b/Assets/_Project/Prefabs/Fabricator.prefab deleted file mode 100644 index f01aeb50c..000000000 --- a/Assets/_Project/Prefabs/Fabricator.prefab +++ /dev/null @@ -1,151 +0,0 @@ -%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: 320054987393778360} - m_Layer: 0 - m_Name: Fabricator - 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: 1, y: 1, z: 1} - 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: -1810898345513765494, guid: c8c42eb35b8e2e649b185de6fd33e2e7, type: 3} ---- !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: 4cee7a5983b187943adc589b9cb1f084, 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 &320054987393778360 -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: 014833c467fb1d6499f437b7bf76db80, type: 3} - m_Name: - m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.FabricatorAuthoring - InResourceId: 2 - InAmount: 1 - OutResourceId: 4 - OutAmount: 3 - PeriodTicks: 30 - InputFromLedger: 1 diff --git a/Assets/_Project/Prefabs/Fabricator.prefab.meta b/Assets/_Project/Prefabs/Fabricator.prefab.meta deleted file mode 100644 index 2087e87bd..000000000 --- a/Assets/_Project/Prefabs/Fabricator.prefab.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 8dd9baab4cbf6c04f9320ed5ed764c65 -PrefabImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/_Project/Prefabs/Harvester.prefab b/Assets/_Project/Prefabs/Harvester.prefab deleted file mode 100644 index 76d9785f4..000000000 --- a/Assets/_Project/Prefabs/Harvester.prefab +++ /dev/null @@ -1,148 +0,0 @@ -%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: 1647940186412478499} - m_Layer: 0 - m_Name: Harvester - 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: 2.5, y: 2.5, z: 2.5} - 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: 4300000, guid: abc00000000010690097314383055197, type: 3} ---- !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: c01dbe8a4e818c5469eceae8b286bf4d, 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 &1647940186412478499 -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: d69d7296747332b4fbe7901ec5210149, type: 3} - m_Name: - m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.HarvesterAuthoring - OutputResourceId: 2 - Yield: 1 - PeriodTicks: 60 diff --git a/Assets/_Project/Prefabs/Harvester.prefab.meta b/Assets/_Project/Prefabs/Harvester.prefab.meta deleted file mode 100644 index 0245ba1a0..000000000 --- a/Assets/_Project/Prefabs/Harvester.prefab.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 9e227d9b387139340933dcf55d1c3a87 -PrefabImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/_Project/Scripts/Authoring/Automation.meta b/Assets/_Project/Scripts/Authoring/Automation.meta deleted file mode 100644 index 3ecd05c4a..000000000 --- a/Assets/_Project/Scripts/Authoring/Automation.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: c51a770922f68b046b12dc55a7f054c2 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/_Project/Scripts/Authoring/Automation/ConveyorAuthoring.cs b/Assets/_Project/Scripts/Authoring/Automation/ConveyorAuthoring.cs deleted file mode 100644 index fe86ced87..000000000 --- a/Assets/_Project/Scripts/Authoring/Automation/ConveyorAuthoring.cs +++ /dev/null @@ -1,35 +0,0 @@ -using ProjectM.Simulation; -using Unity.Entities; -using UnityEngine; - -namespace ProjectM.Authoring -{ - /// - /// Authoring for a Conveyor belt ghost prefab. Bakes {Type=Conveyor} + - /// (default facing; BuildPlaceSystem overrides Direction per placement from the RPC) + - /// a DISABLED (an empty belt). BuildPlaceSystem stamps the Cell; the transport - /// system initializes the period gate on first encounter. - /// - public class ConveyorAuthoring : MonoBehaviour - { - [Tooltip("Default belt facing (0=+X, 1=-X, 2=+Z, 3=-Z); the build RPC overrides this per placement.")] - public byte Direction = 0; - [Min(1)] public int PeriodTicks = 20; - - private class ConveyorBaker : Baker - { - public override void Bake(ConveyorAuthoring authoring) - { - var entity = GetEntity(authoring, TransformUsageFlags.Dynamic); - this.AddPlacedStructure(entity, StructureType.Conveyor); - AddComponent(entity, new Conveyor - { - Direction = authoring.Direction, - PeriodTicks = authoring.PeriodTicks, - }); - AddComponent(entity, new ConveyorItem { ResourceId = 0, Count = 0 }); - SetComponentEnabled(entity, false); // baked empty (disabled) - } - } - } -} diff --git a/Assets/_Project/Scripts/Authoring/Automation/ConveyorAuthoring.cs.meta b/Assets/_Project/Scripts/Authoring/Automation/ConveyorAuthoring.cs.meta deleted file mode 100644 index eda769d31..000000000 --- a/Assets/_Project/Scripts/Authoring/Automation/ConveyorAuthoring.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 23131e6166dce204582bbedc8511658e \ No newline at end of file diff --git a/Assets/_Project/Scripts/Authoring/Automation/FabricatorAuthoring.cs b/Assets/_Project/Scripts/Authoring/Automation/FabricatorAuthoring.cs deleted file mode 100644 index 1f3a772cd..000000000 --- a/Assets/_Project/Scripts/Authoring/Automation/FabricatorAuthoring.cs +++ /dev/null @@ -1,53 +0,0 @@ -using ProjectM.Simulation; -using Unity.Entities; -using UnityEngine; - -namespace ProjectM.Authoring -{ - /// - /// Authoring for a Fabricator machine ghost prefab. Bakes {Type=Fabricator} + - /// recipe + a (kept) empty buffer — the production query - /// needs the buffer as a column, but a ledger-fed Fabricator ignores it. It deposits its output directly into - /// the GLOBAL ledger, so it needs no output buffer. Default recipe (EB-2): 1 Ore -> 3 Charge, ledger-fed - /// ( != 0) — mints the turret-ammo Charge that TurretFireSystem - /// spends per shot. - /// - public class FabricatorAuthoring : MonoBehaviour - { - [Tooltip("Input resource id consumed per run (1=Aether, 2=Ore, 3=Biomass).")] - public byte InResourceId = ResourceId.Ore; - [Min(1)] public int InAmount = 1; - [Tooltip("Output resource id deposited to the global ledger (4 = EB-2 Charge / turret ammo).")] - public byte OutResourceId = ResourceId.Charge; - [Min(1)] public int OutAmount = 3; - [Min(1)] public int PeriodTicks = 30; - [Tooltip("EB-2: 1 = ledger-fed (consume the input from the shared ledger; no conveyor). 0 = M7 MachineInput chain.")] - public byte InputFromLedger = 1; - [Min(1f)] public float MaxHp = 120f; - - private class FabricatorBaker : Baker - { - public override void Bake(FabricatorAuthoring authoring) - { - var entity = GetEntity(authoring, TransformUsageFlags.Dynamic); - this.AddPlacedStructure(entity, StructureType.Fabricator); - AddComponent(entity, new Fabricator - { - InResourceId = authoring.InResourceId, - InAmount = authoring.InAmount, - OutResourceId = authoring.OutResourceId, - OutAmount = authoring.OutAmount, - PeriodTicks = authoring.PeriodTicks, - InputFromLedger = authoring.InputFromLedger, - }); - AddBuffer(entity); - // EB-1 parity (DR-032): machines can die. Without Health this baker left the Fabricator OUT of - // the EnemyAISystem fortress-aggro snapshot entirely (its query keys on Health) — an invulnerable - // machine, diverging from DR-032. Now that it IS targetable, the DamageEvent buffer must exist - // (an AI strike appends into it; absent = ECB-playback throw) + Destructible lets - // HealthApplyDamageSystem destroy it at 0 (occupancy auto-frees). - this.AddDamageable(entity, authoring.MaxHp); - } - } - } -} diff --git a/Assets/_Project/Scripts/Authoring/Automation/FabricatorAuthoring.cs.meta b/Assets/_Project/Scripts/Authoring/Automation/FabricatorAuthoring.cs.meta deleted file mode 100644 index 0391c7660..000000000 --- a/Assets/_Project/Scripts/Authoring/Automation/FabricatorAuthoring.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 014833c467fb1d6499f437b7bf76db80 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Authoring/Automation/HarvesterAuthoring.cs b/Assets/_Project/Scripts/Authoring/Automation/HarvesterAuthoring.cs deleted file mode 100644 index 99d4dd316..000000000 --- a/Assets/_Project/Scripts/Authoring/Automation/HarvesterAuthoring.cs +++ /dev/null @@ -1,36 +0,0 @@ -using ProjectM.Simulation; -using Unity.Entities; -using UnityEngine; - -namespace ProjectM.Authoring -{ - /// - /// Authoring for a Harvester machine ghost prefab (duplicate a structure ghost so the ownerless interpolated - /// GhostAuthoringComponent comes free). Bakes {Type=Harvester} + - /// stats + an empty buffer. BuildPlaceSystem stamps the Cell at placement; the - /// production system initializes the tick baseline on first encounter (NextTick/LastProcessedTick baked 0). - /// - public class HarvesterAuthoring : MonoBehaviour - { - [Tooltip("Resource id this generator produces (1=Aether, 2=Ore, 3=Biomass).")] - public byte OutputResourceId = ResourceId.Ore; - [Min(1)] public int Yield = 1; - [Min(1)] public int PeriodTicks = 60; - - private class HarvesterBaker : Baker - { - public override void Bake(HarvesterAuthoring authoring) - { - var entity = GetEntity(authoring, TransformUsageFlags.Dynamic); - this.AddPlacedStructure(entity, StructureType.Harvester); - AddComponent(entity, new Harvester - { - ResourceId = authoring.OutputResourceId, - Yield = authoring.Yield, - PeriodTicks = authoring.PeriodTicks, - }); - AddBuffer(entity); - } - } - } -} diff --git a/Assets/_Project/Scripts/Authoring/Automation/HarvesterAuthoring.cs.meta b/Assets/_Project/Scripts/Authoring/Automation/HarvesterAuthoring.cs.meta deleted file mode 100644 index d01c64a0e..000000000 --- a/Assets/_Project/Scripts/Authoring/Automation/HarvesterAuthoring.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: d69d7296747332b4fbe7901ec5210149 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Authoring/Building/StructureCatalogAuthoring.cs b/Assets/_Project/Scripts/Authoring/Building/StructureCatalogAuthoring.cs index ca38787d0..924ba0652 100644 --- a/Assets/_Project/Scripts/Authoring/Building/StructureCatalogAuthoring.cs +++ b/Assets/_Project/Scripts/Authoring/Building/StructureCatalogAuthoring.cs @@ -6,12 +6,10 @@ using UnityEngine.Serialization; namespace ProjectM.Authoring { /// - /// Authoring for the baked singleton (the build cost/prefab table). For the - /// M6 foundation there is one buildable type (Turret), so the entry is flat fields — only the prefab - /// object-ref + cost amount need inspector wiring; the type + cost-resource are byte consts in the baker - /// (enum-via-MCP is unreliable, and bytes dodge the cross-assembly enum-in-Burst hazard). M7 generalizes to - /// an array; the runtime buffer is already the data-driven shape. Place - /// once in the gameplay subscene. + /// Authoring for the baked singleton (the build cost/prefab table). Flat + /// prefab + cost fields per buildable type (the type + cost-resource are byte consts in the baker — + /// enum-via-MCP is unreliable, and bytes dodge the cross-assembly enum-in-Burst hazard); the runtime + /// buffer is the data-driven shape. Place once in the gameplay subscene. /// public class StructureCatalogAuthoring : MonoBehaviour { @@ -33,18 +31,6 @@ namespace ProjectM.Authoring [Tooltip("Ore cost to build a pylon.")] [Min(0)] public int PylonCostOre = 2; - [Tooltip("Harvester machine ghost prefab (HarvesterAuthoring + GhostAuthoring).")] - public GameObject HarvesterPrefab; - [Min(0)] public int HarvesterCostOre = 20; - - [Tooltip("Fabricator machine ghost prefab (FabricatorAuthoring + GhostAuthoring).")] - public GameObject FabricatorPrefab; - [Min(0)] public int FabricatorCostOre = 30; - - [Tooltip("Conveyor belt ghost prefab (ConveyorAuthoring + GhostAuthoring).")] - public GameObject ConveyorPrefab; - [Min(0)] public int ConveyorCostOre = 2; - private class StructureCatalogBaker : Baker { public override void Bake(StructureCatalogAuthoring authoring) @@ -84,40 +70,6 @@ namespace ProjectM.Authoring CostAmount = authoring.PylonCostOre, }); } - if (authoring.HarvesterPrefab != null) - { - buf.Add(new StructureCatalogEntry - { - Type = StructureType.Harvester, - Prefab = GetEntity(authoring.HarvesterPrefab, TransformUsageFlags.Dynamic), - CostResourceId = ResourceId.Ore, - CostAmount = authoring.HarvesterCostOre, - }); - } - - if (authoring.FabricatorPrefab != null) - { - buf.Add(new StructureCatalogEntry - { - Type = StructureType.Fabricator, - Prefab = GetEntity(authoring.FabricatorPrefab, TransformUsageFlags.Dynamic), - CostResourceId = ResourceId.Ore, - CostAmount = authoring.FabricatorCostOre, - }); - } - - if (authoring.ConveyorPrefab != null) - { - buf.Add(new StructureCatalogEntry - { - Type = StructureType.Conveyor, - Prefab = GetEntity(authoring.ConveyorPrefab, TransformUsageFlags.Dynamic), - CostResourceId = ResourceId.Ore, - CostAmount = authoring.ConveyorCostOre, - }); - } - - } } } diff --git a/Assets/_Project/Scripts/Client/Building/BuildSendSystem.cs b/Assets/_Project/Scripts/Client/Building/BuildSendSystem.cs index c4406a3fe..624f2cb0c 100644 --- a/Assets/_Project/Scripts/Client/Building/BuildSendSystem.cs +++ b/Assets/_Project/Scripts/Client/Building/BuildSendSystem.cs @@ -29,9 +29,8 @@ namespace ProjectM.Client { (UnityEngine.InputSystem.Key.B, StructureType.Turret), (UnityEngine.InputSystem.Key.V, StructureType.Wall), - (UnityEngine.InputSystem.Key.F, StructureType.Fabricator), - // DR-042 C6d: Pylon/Harvester/Conveyor are dead (unwired automation) — dropped from the hotkey fallback - // to match the hidden build palette; their PlaceStructure execute_code statics remain for dev. + // LANTERN purge: the automation buildables (Fabricator/Harvester/Conveyor) are deleted; Pylon stays + // hidden from the hotkey fallback to match the build palette. PlaceStructure execute_code statics remain. }; UnityEngine.Camera _camera; // cursor -> ground re-raycast for click-to-place (resolved lazily) @@ -61,9 +60,6 @@ namespace ProjectM.Client /// EDITOR / execute_code hook: queue a wall placement at a specific cell. public static void PlaceWall(int cellX, int cellZ) => PlaceStructure(StructureType.Wall, cellX, cellZ); - /// EDITOR / execute_code hook: queue a fabricator placement at a specific cell. - public static void PlaceFabricator(int cellX, int cellZ) => PlaceStructure(StructureType.Fabricator, cellX, cellZ); - #endif protected override void OnCreate() diff --git a/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs b/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs index d39dcfe52..5940660d3 100644 --- a/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs +++ b/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs @@ -96,8 +96,8 @@ namespace ProjectM.Client Label _aetherNum, _oreNum, _bioNum, _chargeNum; // build palette + hints - VisualElement _paletteRow, _hintBar, _facingArrow, _buildDiscoveryChip; - bool _paletteBuilt, _hintBuilt, _hintConveyor; + VisualElement _paletteRow, _hintBar, _buildDiscoveryChip; + bool _paletteBuilt, _hintBuilt; byte _hintScheme = 255; readonly Dictionary _palette = new(); @@ -520,10 +520,7 @@ namespace ProjectM.Client if (buildActive) { byte scheme = AimPresentation.Scheme; - bool conv = BuildPaletteState.Selected == StructureType.Conveyor; - if (!_hintBuilt || _hintScheme != scheme || _hintConveyor != conv) RebuildHints(scheme, conv); - if (conv && _facingArrow != null) - _facingArrow.style.rotate = new StyleRotate(new Rotate(new Angle(FacingDegrees(BuildPaletteState.Direction)))); + if (!_hintBuilt || _hintScheme != scheme) RebuildHints(scheme); _hintBar.style.display = DisplayStyle.Flex; } else @@ -678,10 +675,8 @@ namespace ProjectM.Client } } - // DR-042 C6d: Harvester/Conveyor/Pylon are dead (unwired automation) -> hidden from the build palette - // (catalog + prefabs stay baked, code-intact per DR-020). Only Turret/Wall/Fabricator are buildable in the UI. - static bool IsPaletteType(byte type) => - type != StructureType.Pylon && type != StructureType.Harvester && type != StructureType.Conveyor; + // LANTERN purge: the automation buildables are deleted; Pylon stays hidden from the build palette (cosmetic-only). + static bool IsPaletteType(byte type) => type != StructureType.Pylon; void UpdatePalette(int aether, int ore, int bio, bool onExpedition) { @@ -769,30 +764,15 @@ namespace ProjectM.Client _palette[type] = new PaletteItem { Root = root, Cost = costLabel, CostAmount = cost, CostRes = costRes, Glow = glow, Icon = iconEl }; } - void RebuildHints(byte scheme, bool conveyor) + void RebuildHints(byte scheme) { _hintBar.Clear(); - _facingArrow = null; var theme = HudTheme.Get(); bool pad = scheme == InputSchemeId.Gamepad; AddHint(pad ? theme?.PadPlace : theme?.KbmPlace, pad ? "A" : "LMB", "PLACE"); AddHint(pad ? theme?.PadCancel : theme?.KbmCancel, pad ? "B" : "RMB", "CANCEL"); - if (conveyor) - { - // Rotate hint + a LIVE facing arrow (resolves the DR-021 conveyor-facing indicator). Only conveyors - // rotate, so this chip is gated to them — the other buildables don't show a meaningless ROTATE. - var chip = MakeChip(); - chip.Add(HudUi.Glyph(pad ? theme?.PadRotate : null, pad ? "LB" : "R", 26)); - var lbl = HudUi.Text("FACING", 12, MenuUi.SubCol, TextAnchor.MiddleLeft); - lbl.style.marginLeft = 5; lbl.style.marginRight = 6; - chip.Add(lbl); - _facingArrow = HudUi.Icon(theme != null ? theme.ConveyorIcon : null, 24, AetherCyan); - chip.Add(_facingArrow); - _hintBar.Add(chip); - } AddHint(pad ? theme?.PadExit : null, pad ? "MENU" : "ESC", "EXIT"); _hintScheme = scheme; - _hintConveyor = conveyor; _hintBuilt = true; } @@ -1313,17 +1293,6 @@ namespace ProjectM.Client return resId == ResourceId.Aether ? t.AetherIcon : resId == ResourceId.Biomass ? t.BioIcon : t.OreIcon; } - // Conveyor facing (BuildPaletteState.Direction 0=+X,1=-X,2=+Z,3=-Z) → arrow rotation; the arrow art points up (+Z). - static float FacingDegrees(byte dir) - { - switch (dir) - { - case 0: return 90f; // +X - case 1: return 270f; // -X - case 3: return 180f; // -Z - default: return 0f; // +Z - } - } static Color PhaseColor(byte phase) { @@ -1352,9 +1321,6 @@ namespace ProjectM.Client case StructureType.Turret: return "Turret"; case StructureType.Wall: return "Wall"; case StructureType.Pylon: return "Pylon"; - case StructureType.Harvester: return "Harvester"; - case StructureType.Fabricator: return "Fabricator"; - case StructureType.Conveyor: return "Conveyor"; default: return "?"; } } diff --git a/Assets/_Project/Scripts/Client/UI/HudTheme.cs b/Assets/_Project/Scripts/Client/UI/HudTheme.cs index 12b54c93e..097e357ec 100644 --- a/Assets/_Project/Scripts/Client/UI/HudTheme.cs +++ b/Assets/_Project/Scripts/Client/UI/HudTheme.cs @@ -51,14 +51,10 @@ namespace ProjectM.Client public Sprite TurretIcon; public Sprite WallIcon; public Sprite PylonIcon; - public Sprite HarvesterIcon; - public Sprite FabricatorIcon; - public Sprite ConveyorIcon; [Header("Build-ghost preview meshes (authored real-size, ground pivot — BuildSendSystem)")] public Mesh TurretGhostMesh; public Mesh WallGhostMesh; - public Mesh FabricatorGhostMesh; [Header("Build-mode control glyphs")] public Sprite KbmPlace; // LMB @@ -89,9 +85,6 @@ namespace ProjectM.Client case StructureType.Turret: return TurretIcon; case StructureType.Wall: return WallIcon; case StructureType.Pylon: return PylonIcon; - case StructureType.Harvester: return HarvesterIcon; - case StructureType.Fabricator: return FabricatorIcon; - case StructureType.Conveyor: return ConveyorIcon; default: return null; } } @@ -103,7 +96,6 @@ namespace ProjectM.Client { case StructureType.Turret: return TurretGhostMesh; case StructureType.Wall: return WallGhostMesh; - case StructureType.Fabricator: return FabricatorGhostMesh; default: return null; } } diff --git a/Assets/_Project/Scripts/Server/Automation/BaseRestoreSystem.cs b/Assets/_Project/Scripts/Server/Automation/BaseRestoreSystem.cs index 0f8c2eb7d..dbfc3e672 100644 --- a/Assets/_Project/Scripts/Server/Automation/BaseRestoreSystem.cs +++ b/Assets/_Project/Scripts/Server/Automation/BaseRestoreSystem.cs @@ -10,13 +10,11 @@ namespace ProjectM.Server { /// /// One-shot server restore of player-built structures for a "Continue" session. The menu (WorldLauncher) stages a - /// / carrier in the fresh ServerWorld BEFORE the - /// gameplay subscene streams; this system waits (RequireForUpdate) for the streamed - /// + + a valid NetworkTime, then replays each saved structure CHARGE-FREE: Instantiate the - /// catalog prefab at the saved cell (preserving the baked Scale), re-stamp the rebased tick fields - /// (; LastProcessed = now so within-session catch-up resumes from now, - /// never a wall-clock mint), re-tag RegionTag{Base} + RuntimePlacedTag, refill the in-flight conveyor item + the - /// machine I/O buffers, then DESTROY the carrier so it never runs again. The ledger/goal restore separately + + /// carrier in the fresh ServerWorld BEFORE the gameplay subscene streams; this + /// system waits (RequireForUpdate) for the streamed + + + /// a valid NetworkTime, then replays each saved structure CHARGE-FREE: Instantiate the catalog prefab at the + /// saved cell (preserving the baked Scale), restore the wounded HP born-correct, re-tag RegionTag{Base} + + /// RuntimePlacedTag, then DESTROY the carrier so it never runs again. The ledger restores separately + /// absolutely via CycleDirectorSpawnSystem's born-correct load (no double-spend, no Withdraw here). /// [BurstCompile] @@ -24,14 +22,12 @@ namespace ProjectM.Server public partial struct BaseRestoreSystem : ISystem { ComponentLookup m_TransformLookup; - ComponentLookup m_ConveyorLookup; ComponentLookup m_HealthLookup; [BurstCompile] public void OnCreate(ref SystemState state) { m_TransformLookup = state.GetComponentLookup(isReadOnly: true); - m_ConveyorLookup = state.GetComponentLookup(isReadOnly: true); m_HealthLookup = state.GetComponentLookup(isReadOnly: true); state.RequireForUpdate(); state.RequireForUpdate(); @@ -48,7 +44,6 @@ namespace ProjectM.Server uint now = serverTick.TickIndexForValidTick; m_TransformLookup.Update(ref state); - m_ConveyorLookup.Update(ref state); m_HealthLookup.Update(ref state); var anchor = SystemAPI.GetSingleton(); @@ -56,8 +51,8 @@ namespace ProjectM.Server var ecb = new EntityCommandBuffer(Allocator.Temp); - foreach (var (pending, ioBuf, carrier) in - SystemAPI.Query, DynamicBuffer>().WithEntityAccess()) + foreach (var (pending, carrier) in + SystemAPI.Query>().WithEntityAccess()) { for (int s = 0; s < pending.Length; s++) { @@ -81,12 +76,12 @@ namespace ProjectM.Server { Type = p.Type, Cell = cell, - NextTick = ProductionMath.RestoreNextTick(now, p.RemainingTicks), + NextTick = 0u, // cooldown restore retired with the automation chain (LANTERN purge) LastProcessedTick = TickUtil.NonZero(now), }); // EB-1: restore the wounded HP born-correct in the SAME ecb as Instantiate (Health.Current is a // [GhostField]; a deferred set would leak baked Max to clients for one snapshot). Max + the - // 0->full fallback come from the BAKED prefab, never the save. Automation machines lack Health. + // 0->full fallback come from the BAKED prefab, never the save. if (m_HealthLookup.HasComponent(prefab)) { var hm = m_HealthLookup[prefab]; @@ -94,35 +89,6 @@ namespace ProjectM.Server } ecb.AddComponent(structure, new RegionTag { Region = RegionId.Base }); ecb.AddComponent(structure); - - if (p.Type == StructureType.Conveyor && m_ConveyorLookup.HasComponent(prefab)) - { - var conv = m_ConveyorLookup[prefab]; - conv.Direction = p.Direction; - ecb.SetComponent(structure, conv); - ecb.SetComponent(structure, new ConveyorItem { ResourceId = p.ConveyorResId, Count = p.ConveyorCount }); - ecb.SetComponentEnabled(structure, p.ConveyorCount > 0); - } - - // Refill machine I/O buffers from the flat io table (only slots with saved rows -> the prefab has them). - bool inInit = false, outInit = false; - DynamicBuffer inBuf = default; - DynamicBuffer outBuf = default; - for (int r = 0; r < ioBuf.Length; r++) - { - if (ioBuf[r].StructureIndex != s) - continue; - if (ioBuf[r].Slot == 0) - { - if (!inInit) { inBuf = ecb.SetBuffer(structure); inInit = true; } - inBuf.Add(new MachineInput { ResourceId = ioBuf[r].ResourceId, Count = ioBuf[r].Count }); - } - else - { - if (!outInit) { outBuf = ecb.SetBuffer(structure); outInit = true; } - outBuf.Add(new MachineOutput { ResourceId = ioBuf[r].ResourceId, Count = ioBuf[r].Count }); - } - } } ecb.DestroyEntity(carrier); diff --git a/Assets/_Project/Scripts/Server/Automation/ConveyorTransportSystem.cs b/Assets/_Project/Scripts/Server/Automation/ConveyorTransportSystem.cs deleted file mode 100644 index 090caa33c..000000000 --- a/Assets/_Project/Scripts/Server/Automation/ConveyorTransportSystem.cs +++ /dev/null @@ -1,276 +0,0 @@ -using ProjectM.Simulation; -using Unity.Burst; -using Unity.Collections; -using Unity.Entities; -using Unity.Mathematics; -using Unity.NetCode; - -namespace ProjectM.Server -{ - /// - /// Server-only, deterministic conveyor transport — the MIDDLE of the M7 auto-gather chain - /// (Harvester → Conveyor → Fabricator). Unlike the per-machine catch-up production systems, a conveyor is a - /// single TRANSPORT STEP: each period-due, empty first PULLS one item off an adjacent - /// upstream (the cell at myCell − DirOffset(dir) — i.e. the machine feeding - /// INTO this belt) onto its own ; then every loaded conveyor advances its item - /// EXACTLY one cell toward myCell + DirOffset(dir). The move resolution is delegated to the pure, - /// unit-tested so determinism is provable WITHOUT a world: sources are - /// processed sorted by (NOT hashmap order), occupancy is read from a - /// pre-move double-buffer snapshot, a destination conveyor cell accepts at most one item (only if it was empty - /// in the snapshot; ties → lowest CellKey wins, losers stall with no silent loss), and machine-input sink - /// cells always accept (deposit). Sinks are a separate set so an item leaving the belt into a fabricator's - /// never collides with belt occupancy. - /// - /// Mirrors TurretFireSystem's now-extraction (NetworkTime.ServerTick.TickIndexForValidTick) + - /// cooldown idiom (each conveyor is period-gated the same way), and - /// ResourceHarvestSystem's Temp-collection foreach idiom. Runs in the plain server - /// SimulationSystemGroup [UpdateAfter(HarvesterProductionSystem)] (after harvesters deposit, so - /// fresh output is pull-eligible this tick; before the fabricator consumes). All buffer/enableable mutation is - /// in place (toggling an enableable bit is NOT a structural change) → no ECB. - /// - /// - [BurstCompile] - [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] - [UpdateInGroup(typeof(SimulationSystemGroup))] - [UpdateAfter(typeof(HarvesterProductionSystem))] - public partial struct ConveyorTransportSystem : ISystem - { - [BurstCompile] - public void OnCreate(ref SystemState state) - { - state.RequireForUpdate(); - state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly())); - } - - [BurstCompile] - public void OnUpdate(ref SystemState state) - { - var serverTick = SystemAPI.GetSingleton().ServerTick; - if (!serverTick.IsValid) - return; - uint now = serverTick.TickIndexForValidTick; - - // ---- Snapshot every conveyor once (entity, cell, direction, item, period-due) ---- - var convEntity = new NativeList(Allocator.Temp); - var convCell = new NativeList(Allocator.Temp); - var convDir = new NativeList(Allocator.Temp); - var convItemRes = new NativeList(Allocator.Temp); // 0 = empty - var convItemCnt = new NativeList(Allocator.Temp); // 0 = empty - var convDue = new NativeList(Allocator.Temp); // period-gate satisfied this tick - - foreach (var (ps, conveyor, e) in - SystemAPI.Query, RefRO>().WithEntityAccess()) - { - int period = math.max(1, conveyor.ValueRO.PeriodTicks); - - // Period-gate each conveyor through NextTick exactly like the production systems. A never-processed - // belt initialises its baseline this tick and is NOT due (mirrors NeedsInit on the machines). - bool due; - if (ProductionMath.NeedsInit(ps.ValueRO.LastProcessedTick)) - { - ps.ValueRW.LastProcessedTick = TickUtil.NonZero(now); - ps.ValueRW.NextTick = TickUtil.NonZero(now + (uint)period); - due = false; - } - else - { - int cycles = ProductionMath.CyclesDue( - serverTick, ps.ValueRO.NextTick, ps.ValueRO.LastProcessedTick, period, Tuning.MaxProductionCatchup); - due = cycles > 0; - if (due) - { - // A belt moves at most one cell per period; collapse any catch-up to a single step but keep - // the baseline advancing so it re-evaluates next period. - ps.ValueRW.LastProcessedTick = TickUtil.NonZero(now); - ps.ValueRW.NextTick = TickUtil.NonZero(now + (uint)period); - } - } - - int res = 0, cnt = 0; - if (SystemAPI.IsComponentEnabled(e)) - { - var item = SystemAPI.GetComponent(e); - if (item.Count > 0) - { - res = item.ResourceId; - cnt = item.Count; - } - } - - convEntity.Add(e); - convCell.Add(ps.ValueRO.Cell); - convDir.Add(conveyor.ValueRO.Direction); - convItemRes.Add(res); - convItemCnt.Add(cnt); - convDue.Add(due); - } - - int n = convEntity.Length; - - // Cell → conveyor snapshot index (belt occupancy map for ResolveMoves + the pull lookup). - var cellToIndex = new NativeHashMap(n, Allocator.Temp); - for (int i = 0; i < n; i++) - cellToIndex.TryAdd(convCell[i], i); // duplicate cells can't occur (one structure per cell) - - // Sink cells = cells hosting a machine-input buffer (fabricators); these always accept a deposit. Map - // each sink cell to its owning entity so an arriving item can be deposited into its MachineInput. - var sinkCells = new NativeHashSet(8, Allocator.Temp); - var sinkCellToEntity = new NativeHashMap(8, Allocator.Temp); - foreach (var (ps, _, e) in - SystemAPI.Query, DynamicBuffer>().WithEntityAccess()) - { - sinkCells.Add(ps.ValueRO.Cell); - sinkCellToEntity.TryAdd(ps.ValueRO.Cell, e); - } - - // Source cells = cells hosting a machine-OUTPUT buffer (harvesters/fabricators) a belt can pull from. - // Built once so the pull phase is a single hash lookup per belt (no nested per-belt query). - var outputCellToEntity = new NativeHashMap(8, Allocator.Temp); - foreach (var (ps, _, e) in - SystemAPI.Query, DynamicBuffer>().WithEntityAccess()) - { - outputCellToEntity.TryAdd(ps.ValueRO.Cell, e); - } - - // ---- PULL: each empty, due belt draws one item off an adjacent UPSTREAM MachineOutput ---- - // Upstream cell = myCell − DirOffset(dir): the machine feeding INTO this belt sits there. We pull a - // single unit so a harvester's buffered output flows onto the belt one item per period. - for (int i = 0; i < n; i++) - { - if (!convDue[i] || convItemCnt[i] > 0) - continue; - - int2 srcCell = convCell[i] - ConveyorMath.DirOffset(convDir[i]); - - // The feeder must be a machine with a MachineOutput buffer (harvester or fabricator), NOT another - // conveyor (belts hand off in the move phase, not via pull). Single hash lookup on the prebuilt map. - if (!outputCellToEntity.TryGetValue(srcCell, out var feeder)) - continue; - - var output = SystemAPI.GetBuffer(feeder); - // Pull the first available resource row off the feeder (deterministic: first non-empty row order). - byte pulledId = 0; - for (int r = 0; r < output.Length; r++) - { - if (output[r].ResourceId != 0 && output[r].Count > 0) - { - pulledId = output[r].ResourceId; - break; - } - } - if (pulledId == 0) - continue; - - int taken = MachineSlotMath.Withdraw(output, pulledId, 1); - if (taken <= 0) - continue; - - // Load the belt in the snapshot so it participates in THIS tick's move resolution. - convItemRes[i] = pulledId; - convItemCnt[i] = taken; - } - - // ---- MOVE: resolve all belt advances from the pre-move (post-pull) snapshot, then apply ---- - var srcCells = new NativeArray(n, Allocator.Temp); - var dirs = new NativeArray(n, Allocator.Temp); - var itemRes = new NativeArray(n, Allocator.Temp); - var itemCnt = new NativeArray(n, Allocator.Temp); - for (int i = 0; i < n; i++) - { - srcCells[i] = convCell[i]; - dirs[i] = convDir[i]; - // Pass the FULL post-pull occupancy (due AND non-due) so the resolver blocks a due belt from moving - // INTO an occupied non-due cell. Non-due belts must still not ADVANCE themselves — that is enforced - // after resolution by skipping any returned move whose source belt is not due this tick. - itemRes[i] = convItemRes[i]; - itemCnt[i] = convItemCnt[i]; - } - - var outMoveDst = new NativeArray(n, Allocator.Temp); - var outMoveSrcIdx = new NativeArray(n, Allocator.Temp); - ConveyorMath.ResolveMoves( - srcCells, dirs, itemRes, itemCnt, - cellToIndex, sinkCells, - outMoveDst, outMoveSrcIdx, out int moveCount); - - // Track which belts END this tick holding an item so we can settle enableable bits exactly once. - var endRes = new NativeArray(n, Allocator.Temp); - var endCnt = new NativeArray(n, Allocator.Temp); - for (int i = 0; i < n; i++) - { - // Default: every snapshot item stays put (stalls / non-due / no valid move). Moves below override. - endRes[i] = convItemRes[i]; - endCnt[i] = convItemCnt[i]; - } - - for (int m = 0; m < moveCount; m++) - { - int srcIdx = outMoveSrcIdx[m]; - - // A non-due belt contributes its occupancy to the snapshot (so due belts can't overrun it) but must - // NOT advance its own item — skip its move and leave its item parked (the endRes/endCnt defaults). - if (!convDue[srcIdx]) - continue; - - int2 dst = outMoveDst[m]; - int movRes = convItemRes[srcIdx]; - int movCnt = convItemCnt[srcIdx]; - - // The source belt empties (its item left this cell). - endRes[srcIdx] = 0; - endCnt[srcIdx] = 0; - - if (sinkCells.Contains(dst)) - { - // Item leaves the belt network into a machine input slot. - if (sinkCellToEntity.TryGetValue(dst, out var sinkEntity)) - { - var input = SystemAPI.GetBuffer(sinkEntity); - MachineSlotMath.Deposit(input, (byte)movRes, movCnt); - } - } - else if (cellToIndex.TryGetValue(dst, out int dstIdx)) - { - // Item advances onto the next belt cell (resolver guaranteed it was empty in the snapshot). - endRes[dstIdx] = movRes; - endCnt[dstIdx] = movCnt; - } - } - - // ---- Settle each conveyor's ConveyorItem to its end-of-tick state (single write per belt) ---- - for (int i = 0; i < n; i++) - { - var e = convEntity[i]; - if (endCnt[i] > 0) - { - SystemAPI.SetComponent(e, new ConveyorItem { ResourceId = (byte)endRes[i], Count = endCnt[i] }); - SystemAPI.SetComponentEnabled(e, true); - } - else - { - SystemAPI.SetComponent(e, new ConveyorItem { ResourceId = 0, Count = 0 }); - SystemAPI.SetComponentEnabled(e, false); - } - } - - convEntity.Dispose(); - convCell.Dispose(); - convDir.Dispose(); - convItemRes.Dispose(); - convItemCnt.Dispose(); - convDue.Dispose(); - cellToIndex.Dispose(); - sinkCells.Dispose(); - sinkCellToEntity.Dispose(); - outputCellToEntity.Dispose(); - srcCells.Dispose(); - dirs.Dispose(); - itemRes.Dispose(); - itemCnt.Dispose(); - outMoveDst.Dispose(); - outMoveSrcIdx.Dispose(); - endRes.Dispose(); - endCnt.Dispose(); - } - } -} diff --git a/Assets/_Project/Scripts/Server/Automation/ConveyorTransportSystem.cs.meta b/Assets/_Project/Scripts/Server/Automation/ConveyorTransportSystem.cs.meta deleted file mode 100644 index 9e22417cb..000000000 --- a/Assets/_Project/Scripts/Server/Automation/ConveyorTransportSystem.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 002c28988137cb945b9ffaccbb6d645f \ No newline at end of file diff --git a/Assets/_Project/Scripts/Server/Automation/FabricatorProductionSystem.cs b/Assets/_Project/Scripts/Server/Automation/FabricatorProductionSystem.cs deleted file mode 100644 index 631bae9f4..000000000 --- a/Assets/_Project/Scripts/Server/Automation/FabricatorProductionSystem.cs +++ /dev/null @@ -1,104 +0,0 @@ -using ProjectM.Simulation; -using Unity.Burst; -using Unity.Entities; -using Unity.Mathematics; -using Unity.NetCode; - -namespace ProjectM.Server -{ - /// - /// Server-only, deterministic fabricator production — the BACK of the M7 auto-gather chain - /// (Harvester → Conveyor → Fabricator). Each consumes - /// of its (byte) input resource from its OWN server-only - /// buffer (filled by an upstream conveyor) and, on a - /// cadence, deposits of its output - /// resource into the GLOBAL ledger — so a self-running base compounds its stockpile. Resolves the ledger via - /// GetSingletonEntity<ResourceLedger>()GetBuffer<StorageEntry>() (NEVER - /// GetSingleton<StorageEntry> — a second StorageEntry buffer exists on the base container). - /// Mirrors TurretFireSystem's now-extraction + cooldown idiom and ResourceHarvestSystem's ledger - /// resolve; runs in the plain server SimulationSystemGroup - /// [UpdateAfter(ConveyorTransportSystem)] (which itself is after the harvester + the predicted group), - /// so a single tick can harvest → transport → fabricate in chain order. In-place buffer/ledger mutation - /// (not structural) → no ECB. - /// - /// SINGLE GATED CATCH-UP PATH, INPUT-LIMITED (no mint-from-nothing): when due, the awarded - /// cycles are further clamped to what the buffered input can afford - /// (floor(TotalOf(input,InResourceId)/InAmount)). The tick fields are re-stamped EVERY due period - /// regardless of runs (even a starved fabricator advances its baseline so it re-evaluates next period, - /// not on the next tick — preventing a busy retry storm). Offline catch-up is within-session tick math; never - /// wall-clock. - /// - /// - [BurstCompile] - [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] - [UpdateInGroup(typeof(SimulationSystemGroup))] - [UpdateAfter(typeof(ConveyorTransportSystem))] - public partial struct FabricatorProductionSystem : ISystem - { - [BurstCompile] - public void OnCreate(ref SystemState state) - { - state.RequireForUpdate(); - state.RequireForUpdate(); - state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly())); - } - - [BurstCompile] - public void OnUpdate(ref SystemState state) - { - var serverTick = SystemAPI.GetSingleton().ServerTick; - if (!serverTick.IsValid) - return; - uint now = serverTick.TickIndexForValidTick; - - var ledgerEntity = SystemAPI.GetSingletonEntity(); - var ledger = SystemAPI.GetBuffer(ledgerEntity); - - foreach (var (ps, fab, input) in - SystemAPI.Query, RefRO, DynamicBuffer>()) - { - int period = fab.ValueRO.PeriodTicks; // CyclesDue clamps to max(1, period) - - // Never-processed (baked/just-placed) machine: initialise the catch-up baseline, produce nothing. - if (ProductionMath.NeedsInit(ps.ValueRO.LastProcessedTick)) - { - ps.ValueRW.LastProcessedTick = TickUtil.NonZero(now); - ps.ValueRW.NextTick = TickUtil.NonZero(now + (uint)System.Math.Max(1, period)); - continue; - } - - int cycles = ProductionMath.CyclesDue( - serverTick, ps.ValueRO.NextTick, ps.ValueRO.LastProcessedTick, period, Tuning.MaxProductionCatchup); - if (cycles <= 0) - continue; // still cooling down / nothing due - - byte inId = fab.ValueRO.InResourceId; - int inAmount = fab.ValueRO.InAmount; - - // Input-limited: never produce more than the available input affords (no mint-from-nothing). EB-2: - // a ledger-fed Fabricator (InputFromLedger != 0) sources its input from the SHARED ledger (read LIVE - // here so a 2nd ledger-fed Fabricator sees the 1st's same-tick withdrawal) instead of MachineInput; - // both modes deposit the output to the ledger. A zero/negative input amount is unsatisfiable. - bool fromLedger = fab.ValueRO.InputFromLedger != 0; - int available = fromLedger ? StorageMath.TotalOf(ledger, inId) : MachineSlotMath.TotalOf(input, inId); - int affordable = inAmount > 0 ? available / inAmount : 0; - int runs = math.min(cycles, affordable); - - if (runs > 0) - { - if (fromLedger) - StorageMath.Withdraw(ledger, inId, inAmount * runs); - else - MachineSlotMath.Withdraw(input, inId, inAmount * runs); - StorageMath.Deposit(ledger, (ushort)fab.ValueRO.OutResourceId, fab.ValueRO.OutAmount * runs); - } - - // Re-stamp every due period regardless of runs (starved fabricators re-evaluate next period, not - // every tick) so the catch-up baseline never silently rewinds. - uint p = (uint)System.Math.Max(1, period); - ps.ValueRW.LastProcessedTick = TickUtil.NonZero(now); - ps.ValueRW.NextTick = TickUtil.NonZero(now + p); - } - } - } -} diff --git a/Assets/_Project/Scripts/Server/Automation/FabricatorProductionSystem.cs.meta b/Assets/_Project/Scripts/Server/Automation/FabricatorProductionSystem.cs.meta deleted file mode 100644 index da75b3c33..000000000 --- a/Assets/_Project/Scripts/Server/Automation/FabricatorProductionSystem.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 1d8dbf02b41c9a94ea57fd7ca00f266d \ No newline at end of file diff --git a/Assets/_Project/Scripts/Server/Automation/HarvesterProductionSystem.cs b/Assets/_Project/Scripts/Server/Automation/HarvesterProductionSystem.cs deleted file mode 100644 index b83fc1194..000000000 --- a/Assets/_Project/Scripts/Server/Automation/HarvesterProductionSystem.cs +++ /dev/null @@ -1,76 +0,0 @@ -using ProjectM.Simulation; -using Unity.Burst; -using Unity.Entities; -using Unity.NetCode; - -namespace ProjectM.Server -{ - /// - /// Server-only, deterministic harvester production — the FRONT of the M7 auto-gather chain - /// (Harvester → Conveyor → Fabricator). Each machine is a fixed-yield generator: - /// every server ticks it deposits of its - /// configured (byte) resource into its OWN server-only buffer (NOT the global - /// ledger — a conveyor pulls it onward, or it sits buffered). Mirrors TurretFireSystem's exact - /// now-extraction (NetworkTime.ServerTick.TickIndexForValidTick) + - /// cooldown idiom, and runs in the plain server SimulationSystemGroup - /// [UpdateAfter(PredictedSimulationSystemGroup)] (the predicted group is OrderFirst → UpdateBefore is - /// ignored). Production mutates a DynamicBuffer in place (not a structural change) → no ECB needed. - /// - /// SINGLE GATED CATCH-UP PATH (offline-quit safe, NO wall-clock minting): a never-processed machine - /// (LastProcessedTick==0) is initialised this tick and produces nothing; otherwise - /// awards floor((now-LastProcessedTick)/period) cycles, clamped - /// to , and the tick fields are re-stamped. All catch-up is - /// WITHIN-SESSION tick math; the stockpile is preserved across quit by the persistence layer, never re-minted - /// from a saved wall-clock. - /// - /// - [BurstCompile] - [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] - [UpdateInGroup(typeof(SimulationSystemGroup))] - [UpdateAfter(typeof(PredictedSimulationSystemGroup))] - public partial struct HarvesterProductionSystem : ISystem - { - [BurstCompile] - public void OnCreate(ref SystemState state) - { - state.RequireForUpdate(); - state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly())); - } - - [BurstCompile] - public void OnUpdate(ref SystemState state) - { - var serverTick = SystemAPI.GetSingleton().ServerTick; - if (!serverTick.IsValid) - return; - uint now = serverTick.TickIndexForValidTick; - - foreach (var (ps, harvester, output) in - SystemAPI.Query, RefRO, DynamicBuffer>()) - { - int period = harvester.ValueRO.PeriodTicks; // CyclesDue clamps to max(1, period) - - // Never-processed (baked/just-placed) machine: initialise the catch-up baseline, produce nothing. - if (ProductionMath.NeedsInit(ps.ValueRO.LastProcessedTick)) - { - ps.ValueRW.LastProcessedTick = TickUtil.NonZero(now); - ps.ValueRW.NextTick = TickUtil.NonZero(now + (uint)System.Math.Max(1, period)); - continue; - } - - int cycles = ProductionMath.CyclesDue( - serverTick, ps.ValueRO.NextTick, ps.ValueRO.LastProcessedTick, period, Tuning.MaxProductionCatchup); - if (cycles <= 0) - continue; // still cooling down / nothing due - - // Fixed-yield generation into the machine's own output slot (byte id; ledger conversion happens - // only at the global-ledger boundary, which this machine never crosses directly). - MachineSlotMath.Deposit(output, harvester.ValueRO.ResourceId, harvester.ValueRO.Yield * cycles); - - uint p = (uint)System.Math.Max(1, period); - ps.ValueRW.LastProcessedTick = TickUtil.NonZero(now); - ps.ValueRW.NextTick = TickUtil.NonZero(now + p); - } - } - } -} diff --git a/Assets/_Project/Scripts/Server/Automation/HarvesterProductionSystem.cs.meta b/Assets/_Project/Scripts/Server/Automation/HarvesterProductionSystem.cs.meta deleted file mode 100644 index 31bd916ef..000000000 --- a/Assets/_Project/Scripts/Server/Automation/HarvesterProductionSystem.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: c484cb2331137134888f10eed7689140 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Server/Building/BuildPlaceSystem.cs b/Assets/_Project/Scripts/Server/Building/BuildPlaceSystem.cs index 4f6cfe55d..5d1b5f037 100644 --- a/Assets/_Project/Scripts/Server/Building/BuildPlaceSystem.cs +++ b/Assets/_Project/Scripts/Server/Building/BuildPlaceSystem.cs @@ -23,13 +23,11 @@ namespace ProjectM.Server public partial struct BuildPlaceSystem : ISystem { ComponentLookup m_TransformLookup; - ComponentLookup m_ConveyorLookup; [BurstCompile] public void OnCreate(ref SystemState state) { m_TransformLookup = state.GetComponentLookup(isReadOnly: true); - m_ConveyorLookup = state.GetComponentLookup(isReadOnly: true); state.RequireForUpdate(); state.RequireForUpdate(); state.RequireForUpdate(); @@ -43,7 +41,6 @@ namespace ProjectM.Server public void OnUpdate(ref SystemState state) { m_TransformLookup.Update(ref state); - m_ConveyorLookup.Update(ref state); uint now = SystemAPI.GetSingleton().ServerTick.TickIndexForValidTick; var anchor = SystemAPI.GetSingleton(); @@ -102,12 +99,6 @@ namespace ProjectM.Server }); ecb.AddComponent(structure, new RegionTag { Region = RegionId.Base }); ecb.AddComponent(structure); // player-built -> persisted by SaveStructureScan - if (req.StructureType == StructureType.Conveyor && m_ConveyorLookup.HasComponent(entry.Prefab)) - { - var conv = m_ConveyorLookup[entry.Prefab]; - conv.Direction = req.Direction; - ecb.SetComponent(structure, conv); - } } } diff --git a/Assets/_Project/Scripts/Simulation/Automation.meta b/Assets/_Project/Scripts/Simulation/Automation.meta deleted file mode 100644 index ef17aa019..000000000 --- a/Assets/_Project/Scripts/Simulation/Automation.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 4eba886d11c07eb4d97ca0d821a1560f -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/_Project/Scripts/Simulation/Automation/AutomationComponents.cs b/Assets/_Project/Scripts/Simulation/Automation/AutomationComponents.cs deleted file mode 100644 index 85c7ef87a..000000000 --- a/Assets/_Project/Scripts/Simulation/Automation/AutomationComponents.cs +++ /dev/null @@ -1,91 +0,0 @@ -using Unity.Entities; - -namespace ProjectM.Simulation -{ - /// - /// A fixed-yield resource generator — the FRONT of the M7 auto-gather chain (Harvester -> Conveyor -> - /// Fabricator). Each period it deposits of into its OWN - /// server-only buffer (a conveyor pulls it onward). Server-only data (NO - /// [GhostField]); the client only ever sees PlacedStructure.Type. Reuses - /// PlacedStructure.NextTick/LastProcessedTick for the deterministic, within-session catch-up - /// cadence (see HarvesterProductionSystem). - /// - public struct Harvester : IComponentData - { - /// Resource id produced (a byte; see ). - public byte ResourceId; - /// Units produced per elapsed period. - public int Yield; - /// Server ticks between productions. - public int PeriodTicks; - } - - /// - /// A recipe machine — the BACK of the M7 chain. Consumes of - /// per run from its own buffer (fed by a conveyor) and deposits - /// of into the GLOBAL ledger. Strictly input-limited (never mints from an empty - /// slot). Server-only data. - /// - public struct Fabricator : IComponentData - { - public byte InResourceId; - public int InAmount; - public byte OutResourceId; - public int OutAmount; - public int PeriodTicks; - - /// EB-2: 0 = consume the input from the MachineInput buffer (the M7 conveyor chain); !=0 = consume - /// the input from the SHARED ledger (a base-loop ledger-fed Fabricator, e.g. Ore -> Charge). Server-only, NO [GhostField]. - public byte InputFromLedger; - } - - /// - /// A directional transport belt — the MIDDLE of the M7 chain. Each period it pulls one item off an adjacent - /// upstream (when empty) and advances a held exactly one - /// cell toward . is a byte (0=+X,1=-X,2=+Z,3=-Z) — never an enum - /// (the cross-assembly enum-in-Burst hazard). Server-only data. - /// - public struct Conveyor : IComponentData - { - /// Belt facing: 0=+X, 1=-X, 2=+Z, 3=-Z (see ConveyorMath.DirOffset). - public byte Direction; - public int PeriodTicks; - } - - /// - /// A machine's INPUT staging buffer (server-only, NO [GhostField] -> never replicated). A DISTINCT element type - /// from the global ledger's (so GetSingleton<StorageEntry> stays - /// unambiguous) and from (so a machine can carry both without a buffer-type clash). - /// - public struct MachineInput : IBufferElementData - { - public byte ResourceId; - public int Count; - } - - /// A machine's OUTPUT staging buffer (server-only, NO [GhostField]). See . - public struct MachineOutput : IBufferElementData - { - public byte ResourceId; - public int Count; - } - - /// - /// The single in-flight item a conveyor carries. An ENABLEABLE component (enabled = the belt is occupied) so a - /// transport step is a bit-flip + field copy, never a structural change. Baked DISABLED (an empty belt). - /// Server-only. - /// - public struct ConveyorItem : IComponentData, IEnableableComponent - { - public byte ResourceId; - public int Count; - } - - /// - /// Marks a structure PLACED by a player at runtime (BuildPlaceSystem) or restored from a save — i.e. the - /// persistable set, as opposed to anything baked into the subscene. SaveWriteSystem scans only these and - /// BaseRestoreSystem re-adds the tag, so save/restore is the single source of truth for player builds. - /// Server-only (not replicated). - /// - public struct RuntimePlacedTag : IComponentData { } -} diff --git a/Assets/_Project/Scripts/Simulation/Automation/AutomationComponents.cs.meta b/Assets/_Project/Scripts/Simulation/Automation/AutomationComponents.cs.meta deleted file mode 100644 index 2d881c927..000000000 --- a/Assets/_Project/Scripts/Simulation/Automation/AutomationComponents.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 6eeef378186b39d41a2db7adcc620dd9 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Simulation/Automation/ConveyorMath.cs b/Assets/_Project/Scripts/Simulation/Automation/ConveyorMath.cs deleted file mode 100644 index 0617cc1fe..000000000 --- a/Assets/_Project/Scripts/Simulation/Automation/ConveyorMath.cs +++ /dev/null @@ -1,97 +0,0 @@ -using Unity.Collections; -using Unity.Mathematics; - -namespace ProjectM.Simulation -{ - /// - /// Pure, deterministic, ORDER-INDEPENDENT conveyor move resolver (the server ConveyorTransportSystem - /// applies the result). Determinism: sources are processed sorted by (NEVER hashmap - /// order); a destination belt cell accepts AT MOST ONE item and only if it was EMPTY in the pre-move snapshot - /// (double-buffering -> exactly one cell/tick); ties break to the lowest-CellKey source and losers STALL with no - /// loss; machine-input SINK cells always accept (a merge). World-free so it is exhaustively unit-tested. - /// - public static class ConveyorMath - { - /// Cardinal grid step for a belt direction byte (0=+X,1=-X,2=+Z,3=-Z). - public static int2 DirOffset(byte dir) - { - switch (dir) - { - case 1: return new int2(-1, 0); - case 2: return new int2(0, 1); - case 3: return new int2(0, -1); - default: return new int2(1, 0); // 0 = +X - } - } - - /// A stable, collision-free total order over grid cells (the deterministic tie-break key). - public static long CellKey(int2 cell) => ((long)cell.x << 32) | (uint)cell.y; - - /// - /// Resolve, for each belt holding an item, whether it advances one cell toward its direction this tick. - /// Inputs are read-only snapshots; outputs are the accepted moves ( -> - /// ), length . A move is accepted when the - /// destination is a SINK cell (always, a merge) or an EMPTY, unclaimed belt cell. Sources are iterated in - /// CellKey order so the result is identical regardless of input array order. Scratch is Temp + disposed. - /// - public static void ResolveMoves( - NativeArray srcCells, NativeArray dirs, - NativeArray itemRes, NativeArray itemCnt, - NativeHashMap cellToIndex, NativeHashSet sinkCells, - NativeArray outMoveDst, NativeArray outMoveSrcIdx, out int moveCount) - { - int n = srcCells.Length; - moveCount = 0; - - // Stable iteration order = sources sorted by CellKey (insertion sort; n is small). - var order = new NativeArray(n, Allocator.Temp); - for (int i = 0; i < n; i++) order[i] = i; - for (int i = 1; i < n; i++) - { - int cur = order[i]; - long curKey = CellKey(srcCells[cur]); - int j = i - 1; - while (j >= 0 && CellKey(srcCells[order[j]]) > curKey) - { - order[j + 1] = order[j]; - j--; - } - order[j + 1] = cur; - } - - var claimed = new NativeHashSet(n, Allocator.Temp); - for (int oi = 0; oi < n; oi++) - { - int i = order[oi]; - if (itemCnt[i] <= 0) continue; // nothing to move - - int2 dst = srcCells[i] + DirOffset(dirs[i]); - - bool accept = false; - bool isSink = sinkCells.Contains(dst); - if (isSink) - { - accept = true; // sinks merge -> unlimited acceptors, never claimed - } - else if (cellToIndex.TryGetValue(dst, out int dstIdx)) - { - // dst is a belt cell: accept only if EMPTY in the snapshot AND not already claimed this tick. - if (itemCnt[dstIdx] == 0 && !claimed.Contains(dst)) - accept = true; - } - // else: dst is neither a belt nor a sink -> dead end -> stall. - - if (accept) - { - if (!isSink) claimed.Add(dst); - outMoveDst[moveCount] = dst; - outMoveSrcIdx[moveCount] = i; - moveCount++; - } - } - - order.Dispose(); - claimed.Dispose(); - } - } -} diff --git a/Assets/_Project/Scripts/Simulation/Automation/ConveyorMath.cs.meta b/Assets/_Project/Scripts/Simulation/Automation/ConveyorMath.cs.meta deleted file mode 100644 index 8612aad89..000000000 --- a/Assets/_Project/Scripts/Simulation/Automation/ConveyorMath.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 251361cf456e888459d473b5fedf7c4a \ No newline at end of file diff --git a/Assets/_Project/Scripts/Simulation/Automation/MachineSlotMath.cs b/Assets/_Project/Scripts/Simulation/Automation/MachineSlotMath.cs deleted file mode 100644 index 332a5cfbd..000000000 --- a/Assets/_Project/Scripts/Simulation/Automation/MachineSlotMath.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Unity.Entities; - -namespace ProjectM.Simulation -{ - /// - /// Pure, deterministic deposit/withdraw/total helpers for a machine's server-only / - /// staging buffers — the byte-id, non-replicated twin of - /// (which serves the [GhostField] global ledger). No RNG/wall-clock. DynamicBuffer is - /// a handle, so mutations apply to the underlying entity buffer. Overloaded per buffer type because the two - /// element types are deliberately distinct (a machine can carry both without a singleton-buffer clash). Deposit - /// is a no-op for count <= 0 or resource id 0; Withdraw clamps to available and drops a row at zero. - /// - public static class MachineSlotMath - { - // ---- MachineOutput ---- - public static void Deposit(DynamicBuffer buffer, byte resourceId, int count) - { - if (count <= 0 || resourceId == 0) return; - for (int i = 0; i < buffer.Length; i++) - if (buffer[i].ResourceId == resourceId) - { - var e = buffer[i]; e.Count += count; buffer[i] = e; return; - } - buffer.Add(new MachineOutput { ResourceId = resourceId, Count = count }); - } - - public static int Withdraw(DynamicBuffer buffer, byte resourceId, int count) - { - if (count <= 0 || resourceId == 0) return 0; - for (int i = 0; i < buffer.Length; i++) - if (buffer[i].ResourceId == resourceId) - { - var e = buffer[i]; - int taken = e.Count < count ? e.Count : count; - e.Count -= taken; - if (e.Count <= 0) buffer.RemoveAt(i); else buffer[i] = e; - return taken; - } - return 0; - } - - public static int TotalOf(DynamicBuffer buffer, byte resourceId) - { - int total = 0; - for (int i = 0; i < buffer.Length; i++) - if (buffer[i].ResourceId == resourceId) total += buffer[i].Count; - return total; - } - - // ---- MachineInput ---- - public static void Deposit(DynamicBuffer buffer, byte resourceId, int count) - { - if (count <= 0 || resourceId == 0) return; - for (int i = 0; i < buffer.Length; i++) - if (buffer[i].ResourceId == resourceId) - { - var e = buffer[i]; e.Count += count; buffer[i] = e; return; - } - buffer.Add(new MachineInput { ResourceId = resourceId, Count = count }); - } - - public static int Withdraw(DynamicBuffer buffer, byte resourceId, int count) - { - if (count <= 0 || resourceId == 0) return 0; - for (int i = 0; i < buffer.Length; i++) - if (buffer[i].ResourceId == resourceId) - { - var e = buffer[i]; - int taken = e.Count < count ? e.Count : count; - e.Count -= taken; - if (e.Count <= 0) buffer.RemoveAt(i); else buffer[i] = e; - return taken; - } - return 0; - } - - public static int TotalOf(DynamicBuffer buffer, byte resourceId) - { - int total = 0; - for (int i = 0; i < buffer.Length; i++) - if (buffer[i].ResourceId == resourceId) total += buffer[i].Count; - return total; - } - } -} diff --git a/Assets/_Project/Scripts/Simulation/Automation/MachineSlotMath.cs.meta b/Assets/_Project/Scripts/Simulation/Automation/MachineSlotMath.cs.meta deleted file mode 100644 index d1926ae5a..000000000 --- a/Assets/_Project/Scripts/Simulation/Automation/MachineSlotMath.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 16b6aef96031f54469f05044a2c18e66 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Simulation/Automation/ProductionMath.cs b/Assets/_Project/Scripts/Simulation/Automation/ProductionMath.cs deleted file mode 100644 index d956055be..000000000 --- a/Assets/_Project/Scripts/Simulation/Automation/ProductionMath.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Unity.Mathematics; -using Unity.NetCode; - -namespace ProjectM.Simulation -{ - /// - /// Pure, deterministic catch-up + cooldown math shared by the M7 production systems (Harvester/Conveyor/ - /// Fabricator). No RNG/wall-clock -> server-authoritative. The single GATED catch-up path: a never-processed - /// machine () initializes first; a cooling machine yields 0; a due machine yields - /// floor(elapsed/period) clamped to [0, maxCatchup]; period is guarded by max(1,...). Cooldown is persisted as - /// REMAINING ticks (epoch-independent) so a save survives the server-tick origin reset on a fresh session. - /// - public static class ProductionMath - { - /// True for a never-processed machine (baked/just-placed) — initialize the baseline before producing. - public static bool NeedsInit(uint lastProcessedTick) => lastProcessedTick == 0u; - - /// - /// Cycles to award THIS process. 0 if cooling ( newer than ) - /// or nothing elapsed; otherwise floor(elapsed/period) clamped to [0, ]. - /// ==0 is the inactive sentinel (never read as a future cooling tick). The lower - /// bound is 0 (not 1): when genuinely due the NextTick gate guarantees elapsed>=period, so a sub-period - /// edge (e.g. a freshly restored remaining==0 machine) floors to 0 rather than minting prematurely. - /// is guarded by max(1,...) so a 0 never divides. - /// - public static int CyclesDue(NetworkTick now, uint nextTick, uint lastProcessedTick, int period, int maxCatchup) - { - int p = math.max(1, period); - - if (nextTick != 0u) - { - var next = new NetworkTick(nextTick); - if (next.IsValid && next.IsNewerThan(now)) - return 0; // still cooling down - } - - int since = now.TicksSince(new NetworkTick(TickUtil.NonZero(lastProcessedTick))); - if (since <= 0) - return 0; - - return math.clamp(since / p, 0, maxCatchup); - } - - /// Remaining cooldown ticks to PERSIST (epoch-independent): 0 if inactive or already due, else nextTick-now. - public static uint RemainingTicks(uint nextTick, uint nowTick) => - nextTick == 0u ? 0u : (nextTick > nowTick ? nextTick - nowTick : 0u); - - /// Re-anchor a persisted remaining cooldown to the current tick origin on restore (NonZero-guarded). - public static uint RestoreNextTick(uint nowTick, uint remaining) => TickUtil.NonZero(nowTick + remaining); - } -} diff --git a/Assets/_Project/Scripts/Simulation/Automation/ProductionMath.cs.meta b/Assets/_Project/Scripts/Simulation/Automation/ProductionMath.cs.meta deleted file mode 100644 index b90f0117e..000000000 --- a/Assets/_Project/Scripts/Simulation/Automation/ProductionMath.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 6d461ab50604ea642b26586bffeed41e \ No newline at end of file diff --git a/Assets/_Project/Scripts/Simulation/Building/StructureComponents.cs b/Assets/_Project/Scripts/Simulation/Building/StructureComponents.cs index 851373867..d095c1ae1 100644 --- a/Assets/_Project/Scripts/Simulation/Building/StructureComponents.cs +++ b/Assets/_Project/Scripts/Simulation/Building/StructureComponents.cs @@ -76,4 +76,12 @@ namespace ProjectM.Simulation /// Tag on the baked singleton carrying the buffer (the build cost/prefab table). public struct StructureCatalog : IComponentData { } + + /// + /// Marks a structure PLACED by a player at runtime (BuildPlaceSystem) or restored from a save — i.e. the + /// persistable set, as opposed to anything baked into the subscene. SaveWriteSystem scans only these and + /// BaseRestoreSystem re-adds the tag, so save/restore is the single source of truth for player builds. + /// Server-only (not replicated). (Re-homed here from the retired automation components — LANTERN purge.) + /// + public struct RuntimePlacedTag : IComponentData { } } diff --git a/Assets/_Project/Scripts/Simulation/Persistence/SaveStructureScan.cs b/Assets/_Project/Scripts/Simulation/Persistence/SaveStructureScan.cs index a0edcdccf..9e227c940 100644 --- a/Assets/_Project/Scripts/Simulation/Persistence/SaveStructureScan.cs +++ b/Assets/_Project/Scripts/Simulation/Persistence/SaveStructureScan.cs @@ -6,17 +6,16 @@ namespace ProjectM.Simulation { /// /// Scans a server world for PLAYER-built structures ( + ) - /// into the flat SaveData v2 arrays — the SINGLE shared scan used by BOTH the autosave (SaveWriteSystem) and the + /// into the flat SaveData arrays — the SINGLE shared scan used by BOTH the autosave (SaveWriteSystem) and the /// quit-to-menu save (WorldLauncher), so the two paths can never drift (only RuntimePlacedTag structures are saved; - /// anything baked into the subscene is the subscene's source of truth, not the save's). Cooldowns are stored as - /// REMAINING ticks (epoch-independent). Managed (List/array) — runs only on a save, never in the hot loop. + /// anything baked into the subscene is the subscene's source of truth, not the save's). Managed (List/array) — + /// runs only on a save, never in the hot loop. /// public static class SaveStructureScan { public static void Collect(EntityManager em, uint nowTick, out StructureSave[] structures, out StructureIoRow[] io) { var structs = new List(); - var ioRows = new List(); using var q = em.CreateEntityQuery( ComponentType.ReadOnly(), @@ -27,46 +26,19 @@ namespace ProjectM.Simulation { var e = entities[k]; var ps = em.GetComponentData(e); - int idx = structs.Count; - var row = new StructureSave + structs.Add(new StructureSave { Type = ps.Type, CellX = ps.Cell.x, CellZ = ps.Cell.y, - RemainingTicks = ProductionMath.RemainingTicks(ps.NextTick, nowTick), - // EB-1: guarded so automation machines (no Health) don't crash the autosave path (no try/catch). + // EB-1: guarded so structures without Health don't crash the autosave path (no try/catch). HP = em.HasComponent(e) ? em.GetComponentData(e).Current : 0f, - }; - - if (em.HasComponent(e)) - row.Direction = em.GetComponentData(e).Direction; - - if (em.HasComponent(e) && em.IsComponentEnabled(e)) - { - var item = em.GetComponentData(e); - row.ConveyorResId = item.ResourceId; - row.ConveyorCount = item.Count; - } - - structs.Add(row); - - if (em.HasBuffer(e)) - { - var buf = em.GetBuffer(e, true); - for (int i = 0; i < buf.Length; i++) - ioRows.Add(new StructureIoRow { StructureIndex = idx, Slot = 0, ResourceId = buf[i].ResourceId, Count = buf[i].Count }); - } - if (em.HasBuffer(e)) - { - var buf = em.GetBuffer(e, true); - for (int i = 0; i < buf.Length; i++) - ioRows.Add(new StructureIoRow { StructureIndex = idx, Slot = 1, ResourceId = buf[i].ResourceId, Count = buf[i].Count }); - } + }); } structures = structs.ToArray(); - io = ioRows.ToArray(); + io = System.Array.Empty(); // machine I/O retired with the automation chain (row type dies at save v7) } } } diff --git a/Assets/_Project/Scripts/Simulation/Tuning.cs b/Assets/_Project/Scripts/Simulation/Tuning.cs index 64c2dc53c..3c403f7cf 100644 --- a/Assets/_Project/Scripts/Simulation/Tuning.cs +++ b/Assets/_Project/Scripts/Simulation/Tuning.cs @@ -56,11 +56,8 @@ namespace ProjectM.Simulation /// baked telegraph can't drift from the server windup. public const int ChargerWindupTicks = 30; - // ---- Production / automation (M7: Harvester/Conveyor/Fabricator) ---- + // ---- Base defense (EB-2) ---- - /// Max production cycles a single machine awards in one process (bounds within-session - /// catch-up after any skipped ticks; restore re-seats the baseline so this never reflects wall-clock). - public const int MaxProductionCatchup = 600; /// EB-2: Charge (turret munition) consumed per turret shot, withdrawn from the global ledger. A /// turret with 0 Charge SOFT-FAILS (no shot, no cooldown advance). A ledger-fed Fabricator mints Charge from @@ -73,9 +70,8 @@ namespace ProjectM.Simulation // ---- Cold start (CycleDirectorSpawnSystem seeds the shared ledger on a NEW game) ---- - /// DR-042 C6c: Ore deposited into the shared ledger at spawn on a NEW game ONLY (a restored save keeps - /// its persisted ledger). Bootstraps the Fabricator(30)->Charge->Turret(10) chain so a turret placed before any - /// mining isn't a silent cold deadlock. Ore-only so the 'build a Fabricator to arm turrets' lesson survives. + /// DR-042 C6c: Ore deposited into the shared ledger at spawn on a NEW game ONLY (a restored save + /// keeps its persisted ledger) — seed capital for the build loop. public const int StartingOre = 90; // ---- Expedition run economy (RoomFieldSystem / RunDirectorSystem) ---- diff --git a/Assets/_Project/Tests/EditMode/AutomationSaveRoundTripTests.cs b/Assets/_Project/Tests/EditMode/AutomationSaveRoundTripTests.cs deleted file mode 100644 index d8c929522..000000000 --- a/Assets/_Project/Tests/EditMode/AutomationSaveRoundTripTests.cs +++ /dev/null @@ -1,71 +0,0 @@ -using NUnit.Framework; -using ProjectM.Simulation; -using UnityEngine; - -namespace ProjectM.Tests -{ - /// - /// Tests the SaveData v2 structure-persistence schema (M7): a player-built structure set + the flat machine-I/O - /// table round-trip through JsonUtility with fields intact; the REMAINING-ticks cooldown survives an epoch reset - /// (RemainingTicks/RestoreNextTick); and a save JSON lacking the v2 arrays deserializes without throwing (degrades - /// to no structures). The deeper instantiate-from-catalog restore is covered by the Play-mode validation pass. - /// - public class AutomationSaveRoundTripTests - { - [Test] - public void StructuresAndIo_RoundTrip_PreservesFields() - { - var data = new SaveData - { - GoalCharge = 3, - GoalTarget = 10, - Structures = new[] - { - new StructureSave { Type = StructureType.Harvester, CellX = 4, CellZ = -2, RemainingTicks = 12 }, - new StructureSave { Type = StructureType.Conveyor, CellX = 5, CellZ = -2, Direction = 2, RemainingTicks = 3, ConveyorResId = ResourceId.Ore, ConveyorCount = 1 }, - new StructureSave { Type = StructureType.Fabricator, CellX = 6, CellZ = -2, RemainingTicks = 40 }, - }, - StructureIo = new[] - { - new StructureIoRow { StructureIndex = 0, Slot = 1, ResourceId = ResourceId.Ore, Count = 7 }, - new StructureIoRow { StructureIndex = 2, Slot = 0, ResourceId = ResourceId.Ore, Count = 5 }, - }, - }; - - var back = JsonUtility.FromJson(JsonUtility.ToJson(data)); - - Assert.AreEqual(SaveData.CurrentVersion, back.Version); - Assert.AreEqual(3, back.Structures.Length); - Assert.AreEqual(StructureType.Conveyor, back.Structures[1].Type); - Assert.AreEqual(2, back.Structures[1].Direction); - Assert.AreEqual(ResourceId.Ore, back.Structures[1].ConveyorResId); - Assert.AreEqual(1, back.Structures[1].ConveyorCount); - Assert.AreEqual(12u, back.Structures[0].RemainingTicks); - Assert.AreEqual(2, back.StructureIo.Length); - Assert.AreEqual(2, back.StructureIo[1].StructureIndex); - Assert.AreEqual(0, back.StructureIo[1].Slot); - Assert.AreEqual(5, back.StructureIo[1].Count); - } - - [Test] - public void RemainingTicks_RestoreNextTick_PreservesCooldownGap_AcrossEpochs() - { - uint saveNow = 5000u, savedNext = 5037u; - uint remaining = ProductionMath.RemainingTicks(savedNext, saveNow); - Assert.AreEqual(37u, remaining); - - uint restoreNow = 11u; - uint restoredNext = ProductionMath.RestoreNextTick(restoreNow, remaining); - Assert.AreEqual(48u, restoredNext); - } - - [Test] - public void Save_Lacking_V2_Arrays_DeserializesWithoutThrowing() - { - SaveData back = null; - Assert.DoesNotThrow(() => back = JsonUtility.FromJson("{\"Version\":2,\"GoalCharge\":1,\"GoalTarget\":10}")); - Assert.IsNotNull(back); - Assert.IsTrue(back.Structures == null || back.Structures.Length == 0); - } - } -} diff --git a/Assets/_Project/Tests/EditMode/AutomationSaveRoundTripTests.cs.meta b/Assets/_Project/Tests/EditMode/AutomationSaveRoundTripTests.cs.meta deleted file mode 100644 index 264b506c9..000000000 --- a/Assets/_Project/Tests/EditMode/AutomationSaveRoundTripTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: ad495310b2721f147925ae48deb41bb4 \ No newline at end of file diff --git a/Assets/_Project/Tests/EditMode/ConveyorMathTests.cs b/Assets/_Project/Tests/EditMode/ConveyorMathTests.cs deleted file mode 100644 index dbc61781a..000000000 --- a/Assets/_Project/Tests/EditMode/ConveyorMathTests.cs +++ /dev/null @@ -1,256 +0,0 @@ -using System.Collections.Generic; -using NUnit.Framework; -using ProjectM.Simulation; -using Unity.Collections; -using Unity.Mathematics; - -namespace ProjectM.Tests -{ - /// - /// Pure, world-free tests for — the DETERMINISTIC, ORDER-INDEPENDENT belt resolver - /// that the server ConveyorTransportSystem applies. Drives the array-based ResolveMoves directly - /// (no ECS world) so determinism is provable: a 4-cell line advances exactly one cell/tick, a Y-junction lets a - /// single deterministic winner (lowest CellKey source) into a shared destination while the loser STALLS with no - /// loss, a blocked (already-occupied) destination stalls its source, and the END-STATE is IDENTICAL under two - /// shuffled input orders. Sink (machine-input) cells always accept. DirOffset/CellKey are pinned too. - /// - public class ConveyorMathTests - { - const byte DirPosX = 0; - const byte DirNegX = 1; - const byte DirPosZ = 2; - const byte DirNegZ = 3; - - [Test] - public void DirOffset_Maps_All_Four_Directions() - { - Assert.AreEqual(new int2(1, 0), ConveyorMath.DirOffset(DirPosX), "0 = +X"); - Assert.AreEqual(new int2(-1, 0), ConveyorMath.DirOffset(DirNegX), "1 = -X"); - Assert.AreEqual(new int2(0, 1), ConveyorMath.DirOffset(DirPosZ), "2 = +Z"); - Assert.AreEqual(new int2(0, -1), ConveyorMath.DirOffset(DirNegZ), "3 = -Z"); - } - - [Test] - public void CellKey_Is_Stable_And_Order_Defining() - { - // The key must be a stable total order used to break move ties deterministically. - long a = ConveyorMath.CellKey(new int2(0, 0)); - long b = ConveyorMath.CellKey(new int2(1, 0)); - long c = ConveyorMath.CellKey(new int2(0, 1)); - Assert.AreEqual(a, ConveyorMath.CellKey(new int2(0, 0)), "CellKey is deterministic for a given cell."); - Assert.AreNotEqual(a, b); - Assert.AreNotEqual(a, c); - Assert.AreNotEqual(b, c, "Distinct cells map to distinct keys (no collision in-range)."); - } - - // ---- ResolveMoves harness ------------------------------------------------------------------------------ - - /// One conveyor cell in a scenario: its grid cell, belt direction, and the item it currently holds. - struct Belt - { - public int2 Cell; - public byte Dir; - public int Res; // 0 = empty - public int Cnt; // 0 = empty - } - - /// - /// Drives over a set of belts (+ optional sink cells), returning the - /// resolved (srcIndex -> destCell) move list as a dictionary keyed by source index for easy assertion. - /// All native containers are Temp + disposed before returning. - /// - static Dictionary Resolve(Belt[] belts, int2[] sinks) - { - int n = belts.Length; - var srcCells = new NativeArray(n, Allocator.Temp); - var dirs = new NativeArray(n, Allocator.Temp); - var itemRes = new NativeArray(n, Allocator.Temp); - var itemCnt = new NativeArray(n, Allocator.Temp); - var cellToIndex = new NativeHashMap(n, Allocator.Temp); - var sinkCells = new NativeHashSet(math.max(1, sinks?.Length ?? 0), Allocator.Temp); - var outMoveDst = new NativeArray(n, Allocator.Temp); - var outMoveSrcIdx = new NativeArray(n, Allocator.Temp); - - for (int i = 0; i < n; i++) - { - srcCells[i] = belts[i].Cell; - dirs[i] = belts[i].Dir; - itemRes[i] = belts[i].Res; - itemCnt[i] = belts[i].Cnt; - cellToIndex[belts[i].Cell] = i; - } - if (sinks != null) - foreach (var s in sinks) - sinkCells.Add(s); - - ConveyorMath.ResolveMoves(srcCells, dirs, itemRes, itemCnt, cellToIndex, sinkCells, - outMoveDst, outMoveSrcIdx, out int moveCount); - - var result = new Dictionary(); - for (int m = 0; m < moveCount; m++) - result[outMoveSrcIdx[m]] = outMoveDst[m]; - - srcCells.Dispose(); dirs.Dispose(); itemRes.Dispose(); itemCnt.Dispose(); - cellToIndex.Dispose(); sinkCells.Dispose(); outMoveDst.Dispose(); outMoveSrcIdx.Dispose(); - return result; - } - - [Test] - public void Line_Item_Advances_Exactly_One_Cell() - { - // 4-cell +X line at (0,0),(1,0),(2,0),(3,0). Only the head (index 0) holds an item; the cell ahead (1,0) - // is EMPTY in the snapshot, so the item moves exactly one cell. No other belt holds an item -> no other move. - var belts = new[] - { - new Belt { Cell = new int2(0, 0), Dir = DirPosX, Res = ResourceId.Ore, Cnt = 1 }, - new Belt { Cell = new int2(1, 0), Dir = DirPosX, Res = 0, Cnt = 0 }, - new Belt { Cell = new int2(2, 0), Dir = DirPosX, Res = 0, Cnt = 0 }, - new Belt { Cell = new int2(3, 0), Dir = DirPosX, Res = 0, Cnt = 0 }, - }; - - var moves = Resolve(belts, sinks: null); - - Assert.AreEqual(1, moves.Count, "Exactly one item moves this tick."); - Assert.IsTrue(moves.ContainsKey(0), "The head belt's item is the one that moves."); - Assert.AreEqual(new int2(1, 0), moves[0], "The item advances exactly one cell along +X."); - } - - [Test] - public void Full_Line_All_Advance_One_Cell_From_PreMove_Snapshot() - { - // Every belt in a 4-cell line holds an item, EXCEPT the head cell ahead of the line is open. Because - // occupancy is read from the PRE-MOVE snapshot (double-buffered), each item shifts forward one cell — - // the cell ahead was occupied in the snapshot for the tail belts, so ONLY the lead item (whose target is - // empty in the snapshot) may advance. This pins the snapshot (not live) occupancy rule. - var belts = new[] - { - new Belt { Cell = new int2(0, 0), Dir = DirPosX, Res = ResourceId.Ore, Cnt = 1 }, // -> (1,0) occupied in snapshot -> stall - new Belt { Cell = new int2(1, 0), Dir = DirPosX, Res = ResourceId.Ore, Cnt = 1 }, // -> (2,0) occupied in snapshot -> stall - new Belt { Cell = new int2(2, 0), Dir = DirPosX, Res = ResourceId.Ore, Cnt = 1 }, // -> (3,0) occupied in snapshot -> stall - new Belt { Cell = new int2(3, 0), Dir = DirPosX, Res = ResourceId.Ore, Cnt = 1 }, // -> (4,0) not a belt, not a sink -> no move - }; - - var moves = Resolve(belts, sinks: null); - - Assert.AreEqual(0, moves.Count, - "A fully-packed belt line with a dead end produces no moves (snapshot occupancy blocks every step)."); - } - - [Test] - public void Sink_Cell_Always_Accepts_The_Head_Item() - { - // A 2-cell +X line whose head's target cell is a SINK (machine input). Sinks always accept (deposit), - // even though they are not conveyor cells in cellToIndex. - var belts = new[] - { - new Belt { Cell = new int2(0, 0), Dir = DirPosX, Res = ResourceId.Biomass, Cnt = 2 }, - }; - var sinks = new[] { new int2(1, 0) }; - - var moves = Resolve(belts, sinks); - - Assert.AreEqual(1, moves.Count, "The item moves into the adjacent sink."); - Assert.AreEqual(new int2(1, 0), moves[0], "The destination is the sink cell."); - } - - [Test] - public void Y_Junction_Deterministic_Winner_By_Lowest_CellKey_Loser_Stalls() - { - // Two source belts both feed the SAME destination (1,1): - // A at (0,1) facing +X -> (1,1) - // B at (1,0) facing +Z -> (1,1) - // The destination is an EMPTY conveyor cell -> it accepts AT MOST ONE; the tie breaks to the lowest - // CellKey source. The loser STALLS (no move) with no item loss. - var dst = new int2(1, 1); - var a = new int2(0, 1); - var b = new int2(1, 0); - - var belts = new[] - { - new Belt { Cell = a, Dir = DirPosX, Res = ResourceId.Ore, Cnt = 1 }, // index 0 - new Belt { Cell = b, Dir = DirPosZ, Res = ResourceId.Ore, Cnt = 1 }, // index 1 - new Belt { Cell = dst, Dir = DirPosX, Res = 0, Cnt = 0 }, // index 2 (empty target) - }; - - var moves = Resolve(belts, sinks: null); - - // Exactly one of the two contenders wins; it targets dst. The winner is the lower-CellKey source. - int winnerIdx = ConveyorMath.CellKey(a) < ConveyorMath.CellKey(b) ? 0 : 1; - int loserIdx = winnerIdx == 0 ? 1 : 0; - - Assert.AreEqual(1, moves.Count, "Only one item may enter the shared (empty) destination this tick."); - Assert.IsTrue(moves.ContainsKey(winnerIdx), "The lowest-CellKey source wins the contended cell."); - Assert.AreEqual(dst, moves[winnerIdx], "The winner moves into the shared destination."); - Assert.IsFalse(moves.ContainsKey(loserIdx), "The loser stalls in place (no item loss)."); - } - - [Test] - public void Blocked_Cell_Stalls_The_Source() - { - // Head item at (0,0) facing +X; (1,0) is OCCUPIED in the snapshot (and its own item can't move because - // (2,0) is not a belt/sink). The head item must STALL, not overwrite or destroy the blocker. - var belts = new[] - { - new Belt { Cell = new int2(0, 0), Dir = DirPosX, Res = ResourceId.Ore, Cnt = 1 }, - new Belt { Cell = new int2(1, 0), Dir = DirPosX, Res = ResourceId.Biomass, Cnt = 1 }, // blocker; -> (2,0) dead end - }; - - var moves = Resolve(belts, sinks: null); - - Assert.IsFalse(moves.ContainsKey(0), "A source whose destination is occupied in the snapshot stalls (no loss)."); - Assert.IsFalse(moves.ContainsKey(1), "The blocker itself has a dead-end target and also stalls."); - Assert.AreEqual(0, moves.Count, "Nothing moves when the only path is blocked."); - } - - [Test] - public void EndState_Is_Identical_Under_Two_Shuffled_Input_Orders() - { - // A Y-junction plus a 4-cell line, fed in two DIFFERENT array orders. Because ResolveMoves iterates - // sources SORTED by CellKey (not hashmap/array order), the resolved set of (srcCell -> destCell) moves - // must be byte-for-byte identical regardless of input ordering. We compare keyed by CELL (order-stable), - // not by array index (which differs between the two orderings). - var dst = new int2(1, 1); - var aCell = new int2(0, 1); - var bCell = new int2(1, 0); - - var orderOne = new[] - { - new Belt { Cell = aCell, Dir = DirPosX, Res = ResourceId.Ore, Cnt = 1 }, - new Belt { Cell = bCell, Dir = DirPosZ, Res = ResourceId.Ore, Cnt = 1 }, - new Belt { Cell = dst, Dir = DirPosX, Res = 0, Cnt = 0 }, - new Belt { Cell = new int2(5, 5), Dir = DirPosX, Res = ResourceId.Ore, Cnt = 1 }, // lone item -> (6,5) sink - }; - // Same scenario, shuffled: reverse + interleave the array order. - var orderTwo = new[] - { - new Belt { Cell = new int2(5, 5), Dir = DirPosX, Res = ResourceId.Ore, Cnt = 1 }, - new Belt { Cell = dst, Dir = DirPosX, Res = 0, Cnt = 0 }, - new Belt { Cell = bCell, Dir = DirPosZ, Res = ResourceId.Ore, Cnt = 1 }, - new Belt { Cell = aCell, Dir = DirPosX, Res = ResourceId.Ore, Cnt = 1 }, - }; - var sinks = new[] { new int2(6, 5) }; - - var movesOne = Resolve(orderOne, sinks); - var movesTwo = Resolve(orderTwo, sinks); - - // Re-key both result sets by the SOURCE CELL (stable across orderings) -> destination cell. - var byCellOne = ReKeyBySourceCell(orderOne, movesOne); - var byCellTwo = ReKeyBySourceCell(orderTwo, movesTwo); - - Assert.AreEqual(byCellOne.Count, byCellTwo.Count, "Both orderings resolve the same number of moves."); - foreach (var kv in byCellOne) - { - Assert.IsTrue(byCellTwo.ContainsKey(kv.Key), $"Source cell {kv.Key} moved in order-1 but not order-2."); - Assert.AreEqual(kv.Value, byCellTwo[kv.Key], $"Source cell {kv.Key} resolved to a different destination across orderings (NON-deterministic)."); - } - } - - static Dictionary ReKeyBySourceCell(Belt[] belts, Dictionary movesBySrcIdx) - { - var byCell = new Dictionary(); - foreach (var kv in movesBySrcIdx) - byCell[belts[kv.Key].Cell] = kv.Value; - return byCell; - } - } -} diff --git a/Assets/_Project/Tests/EditMode/ConveyorMathTests.cs.meta b/Assets/_Project/Tests/EditMode/ConveyorMathTests.cs.meta deleted file mode 100644 index ce478dcdb..000000000 --- a/Assets/_Project/Tests/EditMode/ConveyorMathTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: e7846ef025344bf428c68924d389d1b8 \ No newline at end of file diff --git a/Assets/_Project/Tests/EditMode/ConveyorTransportSystemTests.cs b/Assets/_Project/Tests/EditMode/ConveyorTransportSystemTests.cs deleted file mode 100644 index af4baaed2..000000000 --- a/Assets/_Project/Tests/EditMode/ConveyorTransportSystemTests.cs +++ /dev/null @@ -1,187 +0,0 @@ -using NUnit.Framework; -using ProjectM.Server; -using ProjectM.Simulation; -using Unity.Core; -using Unity.Entities; -using Unity.Mathematics; -using Unity.NetCode; - -namespace ProjectM.Tests -{ - /// - /// Plain-Entities EditMode tests for the server-only — the transport step - /// that PULLS resources off an adjacent upstream onto an empty belt, advances a held - /// exactly one cell per tick along its , and DEPOSITS - /// into a downstream machine's sink. Determinism (Y-junction tie-break, stall-no-loss, - /// shuffle-invariance) is exhaustively proven world-free in ; these tests pin the - /// SYSTEM wiring: snapshot -> ResolveMoves -> apply (ConveyorItem enable/disable bit + sink deposit + machine pull), - /// plus the per-conveyor period gate (init on first touch, then move on the production cadence). - /// - public class ConveyorTransportSystemTests - { - static (World world, SimulationSystemGroup group) MakeWorld(string name, uint serverTick) - { - var world = new World(name); - var group = world.GetOrCreateSystemManaged(); - group.AddSystemToUpdateList(world.GetOrCreateSystem()); - group.SortSystems(); - world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f)); - SetServerTick(world, serverTick); - return (world, group); - } - - static void SetServerTick(World world, uint tick) - { - var em = world.EntityManager; - using var q = em.CreateEntityQuery(typeof(NetworkTime)); - Entity e = q.IsEmpty ? em.CreateEntity(typeof(NetworkTime)) : q.GetSingletonEntity(); - em.SetComponentData(e, new NetworkTime { ServerTick = new NetworkTick(tick) }); - } - - static Entity MakeConveyor(EntityManager em, int2 cell, byte dir, int periodTicks, byte itemRes, int itemCnt) - { - var e = em.CreateEntity(); - em.AddComponentData(e, new RegionTag { Region = RegionId.Base }); - em.AddComponentData(e, new PlacedStructure - { - Type = StructureType.Conveyor, - Cell = cell, - NextTick = 0u, - LastProcessedTick = 0u, - }); - em.AddComponentData(e, new Conveyor { Direction = dir, PeriodTicks = periodTicks }); - em.AddComponentData(e, new ConveyorItem { ResourceId = itemRes, Count = itemCnt }); - em.SetComponentEnabled(e, itemCnt > 0); // baked DISABLED unless carrying an item - return e; - } - - static Entity MakeHarvesterOutput(EntityManager em, int2 cell, byte resourceId, int count) - { - // A minimal upstream producer: a PlacedStructure at a cell with a populated MachineOutput buffer. - var e = em.CreateEntity(); - em.AddComponentData(e, new RegionTag { Region = RegionId.Base }); - em.AddComponentData(e, new PlacedStructure { Type = StructureType.Harvester, Cell = cell, NextTick = 0u, LastProcessedTick = 1u }); - var output = em.AddBuffer(e); - if (count > 0) - output.Add(new MachineOutput { ResourceId = resourceId, Count = count }); - return e; - } - - static Entity MakeFabricatorInput(EntityManager em, int2 cell) - { - // A minimal downstream sink: a PlacedStructure at a cell with an (empty) MachineInput buffer. - var e = em.CreateEntity(); - em.AddComponentData(e, new RegionTag { Region = RegionId.Base }); - em.AddComponentData(e, new PlacedStructure { Type = StructureType.Fabricator, Cell = cell, NextTick = 0u, LastProcessedTick = 1u }); - em.AddBuffer(e); - return e; - } - - static int OutputOf(EntityManager em, Entity machine, byte resourceId) - { - var buf = em.GetBuffer(machine); - int total = 0; - for (int i = 0; i < buf.Length; i++) - if (buf[i].ResourceId == resourceId) total += buf[i].Count; - return total; - } - - static int InputOf(EntityManager em, Entity machine, byte resourceId) - { - var buf = em.GetBuffer(machine); - int total = 0; - for (int i = 0; i < buf.Length; i++) - if (buf[i].ResourceId == resourceId) total += buf[i].Count; - return total; - } - - /// Ticks the system past its per-conveyor init gate so subsequent updates actually transport. - static void InitThenAdvance(World world, SimulationSystemGroup group, uint initTick, uint moveTick) - { - SetServerTick(world, initTick); - group.Update(); // first touch: init the conveyor period gate (no move) - SetServerTick(world, moveTick); - group.Update(); // a period later: the transport step runs - } - - [Test] - public void Conveyor_Pulls_From_Adjacent_Upstream_MachineOutput_When_Empty() - { - var (world, group) = MakeWorld("ConveyorPull", serverTick: 100); - using (world) - { - var em = world.EntityManager; - // Harvester output at (0,0); an EMPTY belt at (1,0) facing +X. The belt should pull one unit from the - // upstream output (the harvester's cell (0,0) is belt.cell - DirOffset = (1,0)-(1,0) = (0,0)). - var harvester = MakeHarvesterOutput(em, new int2(0, 0), ResourceId.Ore, count: 4); - var belt = MakeConveyor(em, new int2(1, 0), dir: 0 /*+X*/, periodTicks: 30, itemRes: 0, itemCnt: 0); - - InitThenAdvance(world, group, initTick: 100, moveTick: 130); - - Assert.IsTrue(em.IsComponentEnabled(belt), "An empty belt adjacent to an upstream output pulls an item (ConveyorItem enabled)."); - var item = em.GetComponentData(belt); - Assert.AreEqual(ResourceId.Ore, item.ResourceId, "The pulled item carries the upstream resource id."); - Assert.AreEqual(3, OutputOf(em, harvester, ResourceId.Ore), "Exactly one unit is pulled off the upstream output (4 -> 3)."); - } - } - - [Test] - public void Item_Advances_Exactly_One_Cell_Along_Direction() - { - var (world, group) = MakeWorld("ConveyorAdvance", serverTick: 100); - using (world) - { - var em = world.EntityManager; - // Two +X belts (0,0)->(1,0). The source carries an item; the destination belt is empty. After a tick - // the item should be on the destination belt and the source belt empty. - var src = MakeConveyor(em, new int2(0, 0), dir: 0, periodTicks: 30, itemRes: ResourceId.Biomass, itemCnt: 1); - var dst = MakeConveyor(em, new int2(1, 0), dir: 0, periodTicks: 30, itemRes: 0, itemCnt: 0); - - InitThenAdvance(world, group, initTick: 100, moveTick: 130); - - Assert.IsFalse(em.IsComponentEnabled(src), "The item leaves the source belt (now empty)."); - Assert.IsTrue(em.IsComponentEnabled(dst), "The item arrives on the next belt (exactly one cell along +X)."); - Assert.AreEqual(ResourceId.Biomass, em.GetComponentData(dst).ResourceId, "The carried resource id is preserved across the move."); - } - } - - [Test] - public void Item_Deposits_Into_Downstream_Machine_Input_Sink() - { - var (world, group) = MakeWorld("ConveyorSink", serverTick: 100); - using (world) - { - var em = world.EntityManager; - // A +X belt at (0,0) carrying an item; a fabricator input at (1,0) is the sink. The item should be - // deposited into the machine's MachineInput and removed from the belt. - var belt = MakeConveyor(em, new int2(0, 0), dir: 0, periodTicks: 30, itemRes: ResourceId.Ore, itemCnt: 1); - var fab = MakeFabricatorInput(em, new int2(1, 0)); - - InitThenAdvance(world, group, initTick: 100, moveTick: 130); - - Assert.AreEqual(1, InputOf(em, fab, ResourceId.Ore), "The item is deposited into the downstream machine's input."); - Assert.IsFalse(em.IsComponentEnabled(belt), "The belt is empty after handing its item to the sink."); - } - } - - [Test] - public void Does_Not_Transport_While_Cooling_Down() - { - var (world, group) = MakeWorld("ConveyorCooling", serverTick: 100); - using (world) - { - var em = world.EntityManager; - var src = MakeConveyor(em, new int2(0, 0), dir: 0, periodTicks: 30, itemRes: ResourceId.Ore, itemCnt: 1); - var dst = MakeConveyor(em, new int2(1, 0), dir: 0, periodTicks: 30, itemRes: 0, itemCnt: 0); - - SetServerTick(world, 100); - group.Update(); // init (NextTick -> 130) - SetServerTick(world, 115); - group.Update(); // 115 < 130 -> belt is still cooling, no move - - Assert.IsTrue(em.IsComponentEnabled(src), "The item stays put while the belt is cooling down."); - Assert.IsFalse(em.IsComponentEnabled(dst), "Nothing arrives before the belt's period elapses."); - } - } - } -} diff --git a/Assets/_Project/Tests/EditMode/ConveyorTransportSystemTests.cs.meta b/Assets/_Project/Tests/EditMode/ConveyorTransportSystemTests.cs.meta deleted file mode 100644 index 241ef0c3d..000000000 --- a/Assets/_Project/Tests/EditMode/ConveyorTransportSystemTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 5342d4ffd0bafeb418f5c2f9b3e3ee4e \ No newline at end of file diff --git a/Assets/_Project/Tests/EditMode/FabricatorProductionSystemTests.cs b/Assets/_Project/Tests/EditMode/FabricatorProductionSystemTests.cs deleted file mode 100644 index 81b17ce99..000000000 --- a/Assets/_Project/Tests/EditMode/FabricatorProductionSystemTests.cs +++ /dev/null @@ -1,260 +0,0 @@ -using NUnit.Framework; -using ProjectM.Server; -using ProjectM.Simulation; -using Unity.Core; -using Unity.Entities; -using Unity.NetCode; - -namespace ProjectM.Tests -{ - /// - /// Plain-Entities EditMode tests for the server-only — the recipe machine - /// that consumes InAmount of its input resource per run and deposits OutAmount * runs into the GLOBAL - /// resource ledger (resolved via , never GetSingleton<StorageEntry>). Pins: it - /// INITIALIZES on first touch without producing; it is strictly INPUT-LIMITED (runs = min(cycles, affordable) — no - /// mint-from-nothing); it consumes exactly InAmount*runs; and catch-up after skipped ticks awards the exact capped - /// amount. EB-2 adds the LEDGER-FED mode (InputFromLedger != 0): input is withdrawn from the shared ledger - /// (read LIVE inside the loop so two ledger-fed machines split a finite pool correctly) instead of MachineInput — - /// this is how mined Ore becomes turret-ammo Charge. - /// - public class FabricatorProductionSystemTests - { - static (World world, SimulationSystemGroup group, Entity ledger) MakeWorld(string name, uint serverTick) - { - var world = new World(name); - var group = world.GetOrCreateSystemManaged(); - group.AddSystemToUpdateList(world.GetOrCreateSystem()); - group.SortSystems(); - world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f)); - SetServerTick(world, serverTick); - var em = world.EntityManager; - var ledger = em.CreateEntity(typeof(ResourceLedger)); - em.AddBuffer(ledger); - return (world, group, ledger); - } - - static void SetServerTick(World world, uint tick) - { - var em = world.EntityManager; - using var q = em.CreateEntityQuery(typeof(NetworkTime)); - Entity e = q.IsEmpty ? em.CreateEntity(typeof(NetworkTime)) : q.GetSingletonEntity(); - em.SetComponentData(e, new NetworkTime { ServerTick = new NetworkTick(tick) }); - } - - static Entity MakeFabricator(EntityManager em, byte inId, int inAmt, byte outId, int outAmt, - int periodTicks, int seedInput, byte fromLedger = 0) - { - var e = em.CreateEntity(); - em.AddComponentData(e, new RegionTag { Region = RegionId.Base }); - em.AddComponentData(e, new PlacedStructure - { - Type = StructureType.Fabricator, - NextTick = 0u, - LastProcessedTick = 0u, - }); - em.AddComponentData(e, new Fabricator - { - InResourceId = inId, - InAmount = inAmt, - OutResourceId = outId, - OutAmount = outAmt, - PeriodTicks = periodTicks, - InputFromLedger = fromLedger, - }); - var input = em.AddBuffer(e); - if (seedInput > 0) - input.Add(new MachineInput { ResourceId = inId, Count = seedInput }); - em.AddBuffer(e); - return e; - } - - static void SeedLedger(EntityManager em, Entity ledger, ushort itemId, int count) - { - var buf = em.GetBuffer(ledger); - buf.Add(new StorageEntry { ItemId = itemId, Count = count }); - } - - static int LedgerCount(EntityManager em, Entity ledger, ushort itemId) - { - var buf = em.GetBuffer(ledger); - for (int i = 0; i < buf.Length; i++) - if (buf[i].ItemId == itemId) return buf[i].Count; - return 0; - } - - static int InputOf(EntityManager em, Entity machine, byte resourceId) - { - var buf = em.GetBuffer(machine); - int total = 0; - for (int i = 0; i < buf.Length; i++) - if (buf[i].ResourceId == resourceId) - total += buf[i].Count; - return total; - } - - [Test] - public void First_Update_Initializes_Without_Producing() - { - var (world, group, ledger) = MakeWorld("FabInit", serverTick: 100); - using (world) - { - var em = world.EntityManager; - var f = MakeFabricator(em, ResourceId.Ore, inAmt: 2, outId: ResourceId.Aether, outAmt: 1, periodTicks: 30, seedInput: 10); - - group.Update(); - - Assert.AreEqual(0, LedgerCount(em, ledger, ResourceId.Aether), "First touch only initializes (no production)."); - Assert.AreEqual(10, InputOf(em, f, ResourceId.Ore), "No input is consumed during init."); - var ps = em.GetComponentData(f); - Assert.AreNotEqual(0u, ps.LastProcessedTick); - Assert.AreNotEqual(0u, ps.NextTick); - } - } - - [Test] - public void Produces_One_Run_Per_Period_When_Input_Is_Available() - { - var (world, group, ledger) = MakeWorld("FabRun", serverTick: 100); - using (world) - { - var em = world.EntityManager; - var f = MakeFabricator(em, ResourceId.Ore, inAmt: 2, outId: ResourceId.Aether, outAmt: 3, periodTicks: 30, seedInput: 10); - - group.Update(); // init (NextTick -> 130) - SetServerTick(world, 130); - group.Update(); // one period elapsed, input affords it -> 1 run - - Assert.AreEqual(3, LedgerCount(em, ledger, ResourceId.Aether), "One run deposits OutAmount into the ledger."); - Assert.AreEqual(8, InputOf(em, f, ResourceId.Ore), "One run consumes InAmount from the input buffer (10 - 2)."); - } - } - - [Test] - public void Is_Input_Limited_No_Mint_From_Empty_Slot() - { - var (world, group, ledger) = MakeWorld("FabStarved", serverTick: 100); - using (world) - { - var em = world.EntityManager; - // Empty input slot: even with periods elapsed, affordable == 0 -> runs == 0 -> nothing minted. - var f = MakeFabricator(em, ResourceId.Ore, inAmt: 2, outId: ResourceId.Aether, outAmt: 3, periodTicks: 30, seedInput: 0); - - group.Update(); // init - SetServerTick(world, 250); // plenty of periods elapsed - group.Update(); - - Assert.AreEqual(0, LedgerCount(em, ledger, ResourceId.Aether), - "A starved fabricator mints nothing — production is strictly input-limited."); - Assert.AreEqual(0, InputOf(em, f, ResourceId.Ore), "No phantom input appears."); - } - } - - [Test] - public void Runs_Are_Clamped_To_Affordable_Input() - { - var (world, group, ledger) = MakeWorld("FabAfford", serverTick: 100); - using (world) - { - var em = world.EntityManager; - // 5 periods become due (150 ticks / 30), but only 3 runs are affordable (7 input / 2 per run = 3). - var f = MakeFabricator(em, ResourceId.Ore, inAmt: 2, outId: ResourceId.Aether, outAmt: 1, periodTicks: 30, seedInput: 7); - - group.Update(); // init at 100 - SetServerTick(world, 250); // floor(150/30) = 5 cycles due - group.Update(); - - Assert.AreEqual(3, LedgerCount(em, ledger, ResourceId.Aether), - "runs = min(cyclesDue=5, affordable=3) = 3 — output is clamped to available input."); - Assert.AreEqual(1, InputOf(em, f, ResourceId.Ore), "3 runs consume 6 of 7 input, leaving 1."); - } - } - - [Test] - public void CatchUp_Awards_Exact_Multiple_When_Input_Allows() - { - var (world, group, ledger) = MakeWorld("FabCatchUp", serverTick: 100); - using (world) - { - var em = world.EntityManager; - var f = MakeFabricator(em, ResourceId.Ore, inAmt: 1, outId: ResourceId.Biomass, outAmt: 2, periodTicks: 30, seedInput: 1000); - - group.Update(); // init at 100 - SetServerTick(world, 250); // floor(150/30) = 5 cycles, all affordable - group.Update(); - - Assert.AreEqual(10, LedgerCount(em, ledger, ResourceId.Biomass), "5 runs * OutAmount(2) = 10 deposited."); - Assert.AreEqual(995, InputOf(em, f, ResourceId.Ore), "5 runs * InAmount(1) = 5 consumed (1000 - 5)."); - } - } - - // ---- EB-2: ledger-fed Fabricator (mined Ore -> turret-ammo Charge) ---- - - [Test] - public void LedgerFed_Withdraws_Input_From_Ledger_And_Deposits_Output() - { - var (world, group, ledger) = MakeWorld("FabLedgerFed", serverTick: 100); - using (world) - { - var em = world.EntityManager; - SeedLedger(em, ledger, ResourceId.Ore, 10); - // InputFromLedger=1, MachineInput intentionally EMPTY (seedInput:0) — input must come from the ledger. - var f = MakeFabricator(em, ResourceId.Ore, inAmt: 1, outId: ResourceId.Charge, outAmt: 3, periodTicks: 30, seedInput: 0, fromLedger: 1); - - group.Update(); // init (NextTick -> 130) - SetServerTick(world, 130); - group.Update(); // one period: withdraw 1 Ore FROM the ledger, deposit 3 Charge - - Assert.AreEqual(3, LedgerCount(em, ledger, ResourceId.Charge), "One run deposits OutAmount Charge into the ledger."); - Assert.AreEqual(9, LedgerCount(em, ledger, ResourceId.Ore), "One run withdraws InAmount Ore FROM the ledger (10 - 1)."); - Assert.AreEqual(0, InputOf(em, f, ResourceId.Ore), "A ledger-fed Fabricator never touches its MachineInput buffer."); - } - } - - [Test] - public void Two_LedgerFed_Fabricators_Split_A_Finite_Ledger_Via_The_Live_Read() - { - // 3 Ore in the ledger, two ledger-fed Fabricators each needing 2 Ore/run. The system reads the ledger - // LIVE inside the loop, so the first withdraws 2 (Ore 3->1) and the second sees only 1 (floor(1/2)=0) and - // starves. A HOISTED/stale read would let both see 3, both run, and drive the ledger negative. - var (world, group, ledger) = MakeWorld("FabLedgerSplit", serverTick: 100); - using (world) - { - var em = world.EntityManager; - SeedLedger(em, ledger, ResourceId.Ore, 3); - MakeFabricator(em, ResourceId.Ore, inAmt: 2, outId: ResourceId.Charge, outAmt: 1, periodTicks: 30, seedInput: 0, fromLedger: 1); - MakeFabricator(em, ResourceId.Ore, inAmt: 2, outId: ResourceId.Charge, outAmt: 1, periodTicks: 30, seedInput: 0, fromLedger: 1); - - group.Update(); // init both (NextTick -> 130) - SetServerTick(world, 130); - group.Update(); // one period: only ONE fab can afford 2 Ore from the shared pool - - Assert.AreEqual(1, LedgerCount(em, ledger, ResourceId.Charge), - "Only one of the two ledger-fed Fabricators affords a run from the shared 3 Ore — the live read prevents a double-spend."); - Assert.AreEqual(1, LedgerCount(em, ledger, ResourceId.Ore), "2 of 3 Ore withdrawn; the ledger never goes negative."); - } - } - - [Test] - public void LedgerFed_Runs_Are_Clamped_To_Affordable_Ledger_Under_CatchUp() - { - // 5 periods become due (150 ticks / 30), but the shared ledger only affords 3 runs (7 Ore / 2 per run). - // Pins the LEDGER branch of runs = min(cyclesDue, affordable) as the strict binding minimum under catch-up. - var (world, group, ledger) = MakeWorld("FabLedgerCatchUp", serverTick: 100); - using (world) - { - var em = world.EntityManager; - SeedLedger(em, ledger, ResourceId.Ore, 7); - var f = MakeFabricator(em, ResourceId.Ore, inAmt: 2, outId: ResourceId.Charge, outAmt: 1, periodTicks: 30, seedInput: 0, fromLedger: 1); - - group.Update(); // init at 100 - SetServerTick(world, 250); // floor(150/30) = 5 cycles due - group.Update(); - - Assert.AreEqual(3, LedgerCount(em, ledger, ResourceId.Charge), - "runs = min(cyclesDue=5, ledgerAffordable=floor(7/2)=3) = 3 — clamped by the LEDGER, not the cycle count."); - Assert.AreEqual(1, LedgerCount(em, ledger, ResourceId.Ore), "3 runs withdraw 6 of 7 Ore from the ledger, leaving 1."); - Assert.AreEqual(0, InputOf(em, f, ResourceId.Ore), "Ledger-fed mode never touches MachineInput."); - } - } - } -} diff --git a/Assets/_Project/Tests/EditMode/FabricatorProductionSystemTests.cs.meta b/Assets/_Project/Tests/EditMode/FabricatorProductionSystemTests.cs.meta deleted file mode 100644 index 1c4c58592..000000000 --- a/Assets/_Project/Tests/EditMode/FabricatorProductionSystemTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: aeaf3925e6ddb6e4faeac39f32f98e5d \ No newline at end of file diff --git a/Assets/_Project/Tests/EditMode/HarvesterProductionSystemTests.cs b/Assets/_Project/Tests/EditMode/HarvesterProductionSystemTests.cs deleted file mode 100644 index 849a08a86..000000000 --- a/Assets/_Project/Tests/EditMode/HarvesterProductionSystemTests.cs +++ /dev/null @@ -1,153 +0,0 @@ -using NUnit.Framework; -using ProjectM.Server; -using ProjectM.Simulation; -using Unity.Core; -using Unity.Entities; -using Unity.NetCode; - -namespace ProjectM.Tests -{ - /// - /// Plain-Entities EditMode tests for the server-only — the fixed-yield - /// generator that deposits Yield * cycles of its resource into its OWN buffer - /// on a deterministic period. Pins the SINGLE GATED catch-up path from the M7 contract: a never-processed machine - /// (LastProcessedTick==0) only INITIALIZES on its first touch (no production), then produces exactly one yield per - /// elapsed period, catch-up after skipped ticks awards the exact (capped) amount, and a cooling machine produces - /// nothing. Output stays in the machine's local buffer (server-only, no GhostField) — the conveyor pulls it later. - /// - public class HarvesterProductionSystemTests - { - static (World world, SimulationSystemGroup group) MakeWorld(string name, uint serverTick) - { - var world = new World(name); - var group = world.GetOrCreateSystemManaged(); - group.AddSystemToUpdateList(world.GetOrCreateSystem()); - group.SortSystems(); - world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f)); - SetServerTick(world, serverTick); - return (world, group); - } - - static void SetServerTick(World world, uint tick) - { - var em = world.EntityManager; - using var q = em.CreateEntityQuery(typeof(NetworkTime)); - Entity e = q.IsEmpty ? em.CreateEntity(typeof(NetworkTime)) : q.GetSingletonEntity(); - em.SetComponentData(e, new NetworkTime { ServerTick = new NetworkTick(tick) }); - } - - static Entity MakeHarvester(EntityManager em, byte resourceId, int yield, int periodTicks) - { - var e = em.CreateEntity(); - em.AddComponentData(e, new RegionTag { Region = RegionId.Base }); - em.AddComponentData(e, new PlacedStructure - { - Type = StructureType.Harvester, - NextTick = 0u, - LastProcessedTick = 0u, // never processed -> first update only initializes - }); - em.AddComponentData(e, new Harvester { ResourceId = resourceId, Yield = yield, PeriodTicks = periodTicks }); - em.AddBuffer(e); - return e; - } - - static int OutputOf(EntityManager em, Entity machine, byte resourceId) - { - var buf = em.GetBuffer(machine); - int total = 0; - for (int i = 0; i < buf.Length; i++) - if (buf[i].ResourceId == resourceId) - total += buf[i].Count; - return total; - } - - [Test] - public void First_Update_Initializes_Without_Producing() - { - var (world, group) = MakeWorld("HarvesterInit", serverTick: 100); - using (world) - { - var em = world.EntityManager; - var h = MakeHarvester(em, ResourceId.Aether, yield: 5, periodTicks: 30); - - group.Update(); - - Assert.AreEqual(0, OutputOf(em, h, ResourceId.Aether), - "A never-processed machine only initializes on its first touch (no production)."); - var ps = em.GetComponentData(h); - Assert.AreNotEqual(0u, ps.LastProcessedTick, "Init stamps LastProcessedTick to 'now'."); - Assert.AreNotEqual(0u, ps.NextTick, "Init stamps the next production tick (now + period)."); - } - } - - [Test] - public void Produces_One_Yield_After_Exactly_One_Period() - { - var (world, group) = MakeWorld("HarvesterOnePeriod", serverTick: 100); - using (world) - { - var em = world.EntityManager; - var h = MakeHarvester(em, ResourceId.Aether, yield: 5, periodTicks: 30); - - group.Update(); // tick 100: init (NextTick -> 130) - SetServerTick(world, 130); - group.Update(); // tick 130: one period elapsed -> +5 - - Assert.AreEqual(5, OutputOf(em, h, ResourceId.Aether), "One elapsed period yields exactly Yield."); - } - } - - [Test] - public void Does_Not_Produce_While_Cooling_Down() - { - var (world, group) = MakeWorld("HarvesterCooling", serverTick: 100); - using (world) - { - var em = world.EntityManager; - var h = MakeHarvester(em, ResourceId.Ore, yield: 5, periodTicks: 30); - - group.Update(); // init (NextTick -> 130) - SetServerTick(world, 115); - group.Update(); // 115 < 130 -> still cooling - - Assert.AreEqual(0, OutputOf(em, h, ResourceId.Ore), "No production before the period elapses."); - } - } - - [Test] - public void CatchUp_Awards_Exact_Multiple_For_Skipped_Periods() - { - var (world, group) = MakeWorld("HarvesterCatchUp", serverTick: 100); - using (world) - { - var em = world.EntityManager; - var h = MakeHarvester(em, ResourceId.Biomass, yield: 5, periodTicks: 30); - - group.Update(); // init at 100 (LastProcessedTick -> 100) - SetServerTick(world, 250); // 150 ticks elapsed -> floor(150/30) = 5 cycles - group.Update(); - - Assert.AreEqual(25, OutputOf(em, h, ResourceId.Biomass), - "Catch-up awards Yield * floor(elapsed/period) = 5 * 5 = 25 (skipped ticks are not lost)."); - } - } - - [Test] - public void CatchUp_Is_Capped_At_MaxProductionCatchup() - { - var (world, group) = MakeWorld("HarvesterCap", serverTick: 100); - using (world) - { - var em = world.EntityManager; - var h = MakeHarvester(em, ResourceId.Aether, yield: 1, periodTicks: 1); - - group.Update(); // init at 100 - SetServerTick(world, 100u + 5_000_000u); // an absurd gap - group.Update(); - - Assert.AreEqual(Tuning.MaxProductionCatchup, OutputOf(em, h, ResourceId.Aether), - "A long gap is capped at MaxProductionCatchup cycles (no unbounded mint)."); - } - } - } -} diff --git a/Assets/_Project/Tests/EditMode/HarvesterProductionSystemTests.cs.meta b/Assets/_Project/Tests/EditMode/HarvesterProductionSystemTests.cs.meta deleted file mode 100644 index 2063052ac..000000000 --- a/Assets/_Project/Tests/EditMode/HarvesterProductionSystemTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: a5de181e5c7cfe54a803d176411f8944 \ No newline at end of file diff --git a/Assets/_Project/Tests/EditMode/ProductionMathTests.cs b/Assets/_Project/Tests/EditMode/ProductionMathTests.cs deleted file mode 100644 index 1c8d59d80..000000000 --- a/Assets/_Project/Tests/EditMode/ProductionMathTests.cs +++ /dev/null @@ -1,134 +0,0 @@ -using NUnit.Framework; -using ProjectM.Simulation; -using Unity.NetCode; - -namespace ProjectM.Tests -{ - /// - /// Pure tests for — the deterministic, world-free catch-up math the M7 automation - /// systems (Harvester/Conveyor/Fabricator) share. Pins the SINGLE GATED catch-up path: a never-processed machine - /// needs init (no production), a cooling machine yields 0 cycles, a due machine yields at least 1, a long-skipped - /// machine is CAPPED at maxCatchup (no wall-clock mint), period=0 is guarded by max(1,...), and the - /// RemainingTicks/RestoreNextTick pair round-trips epoch-independently for save/restore. - /// - public class ProductionMathTests - { - [Test] - public void NeedsInit_True_Only_For_Zero_LastProcessedTick() - { - Assert.IsTrue(ProductionMath.NeedsInit(0u), "A 0 LastProcessedTick is a never-processed (baked/uninit) machine."); - Assert.IsFalse(ProductionMath.NeedsInit(1u), "Any non-zero tick has been initialized."); - Assert.IsFalse(ProductionMath.NeedsInit(12345u)); - } - - [Test] - public void CyclesDue_Cooling_Returns_Zero() - { - // NextTick is in the future relative to now -> still cooling, no production. - var now = new NetworkTick(100u); - int cycles = ProductionMath.CyclesDue(now, nextTick: 130u, lastProcessedTick: 100u, period: 30, maxCatchup: 600); - Assert.AreEqual(0, cycles, "A machine whose NextTick is newer than now is cooling down (0 cycles)."); - } - - [Test] - public void CyclesDue_Exactly_One_Period_Elapsed_Returns_One() - { - // now == nextTick (not newer than) and one full period has elapsed since lastProcessed. - var now = new NetworkTick(130u); - int cycles = ProductionMath.CyclesDue(now, nextTick: 130u, lastProcessedTick: 100u, period: 30, maxCatchup: 600); - Assert.AreEqual(1, cycles, "One elapsed period at the ready tick produces exactly one cycle."); - } - - [Test] - public void CyclesDue_Multiple_Periods_Awards_Floor_Division() - { - // 100 ticks elapsed at period 30 -> floor(100/30) = 3. - var now = new NetworkTick(200u); - int cycles = ProductionMath.CyclesDue(now, nextTick: 130u, lastProcessedTick: 100u, period: 30, maxCatchup: 600); - Assert.AreEqual(3, cycles, "Catch-up awards floor(elapsed/period) cycles."); - } - - [Test] - public void CyclesDue_FarPast_Is_Capped_At_MaxCatchup() - { - // Huge elapsed gap must not mint unbounded production — clamp to maxCatchup. - var now = new NetworkTick(1_000_000u); - int cycles = ProductionMath.CyclesDue(now, nextTick: 31u, lastProcessedTick: 1u, period: 1, maxCatchup: 600); - Assert.AreEqual(600, cycles, "A long-skipped machine is capped at maxCatchup (no wall-clock mint)."); - } - - [Test] - public void CyclesDue_Period_Zero_Is_Guarded_By_Max_One() - { - // period 0 must not divide-by-zero; max(1,period) means every elapsed tick is one cycle (then capped). - var now = new NetworkTick(110u); - int cycles = ProductionMath.CyclesDue(now, nextTick: 0u, lastProcessedTick: 100u, period: 0, maxCatchup: 600); - Assert.AreEqual(10, cycles, "period=0 is treated as 1 (floor(10/1) = 10), never a divide-by-zero."); - } - - [Test] - public void CyclesDue_NonPositive_Elapsed_Returns_Zero() - { - // now == lastProcessed -> since == 0 -> 0 cycles (nothing due yet). NextTick=0 means "ready/inactive". - var now = new NetworkTick(100u); - int cycles = ProductionMath.CyclesDue(now, nextTick: 0u, lastProcessedTick: 100u, period: 30, maxCatchup: 600); - Assert.AreEqual(0, cycles, "Zero elapsed ticks since last process yields no cycles."); - } - - [Test] - public void CyclesDue_Inactive_NextTick_Zero_Does_Not_Cool_Block() - { - // NextTick==0 is the "inactive/uninitialized" sentinel — it must NOT be read as a future cooling tick. - // With a full period elapsed, the machine is due despite NextTick==0. - var now = new NetworkTick(140u); - int cycles = ProductionMath.CyclesDue(now, nextTick: 0u, lastProcessedTick: 100u, period: 30, maxCatchup: 600); - Assert.AreEqual(1, cycles, "NextTick==0 is the inactive sentinel, never a cooling gate."); - } - - [Test] - public void RemainingTicks_Zero_NextTick_Is_Inactive() - { - Assert.AreEqual(0u, ProductionMath.RemainingTicks(nextTick: 0u, nowTick: 100u), - "An inactive (NextTick==0) machine has no remaining cooldown to persist."); - } - - [Test] - public void RemainingTicks_Future_NextTick_Returns_Gap() - { - Assert.AreEqual(25u, ProductionMath.RemainingTicks(nextTick: 125u, nowTick: 100u), - "Remaining = nextTick - now when the next action is still in the future."); - } - - [Test] - public void RemainingTicks_Past_NextTick_Returns_Zero() - { - Assert.AreEqual(0u, ProductionMath.RemainingTicks(nextTick: 90u, nowTick: 100u), - "A machine already past its NextTick has 0 remaining (it is due, not cooling)."); - } - - [Test] - public void RemainingTicks_RestoreNextTick_RoundTrip_Is_EpochIndependent() - { - // Save at one epoch (saveNow), restore at an unrelated epoch (restoreNow): the COOLDOWN GAP is preserved - // even though the absolute tick differs. This is why we persist remaining-ticks, not an absolute tick. - uint saveNow = 1000u; - uint savedNext = 1040u; // 40 ticks of cooldown remaining at save time - uint remaining = ProductionMath.RemainingTicks(savedNext, saveNow); - Assert.AreEqual(40u, remaining); - - uint restoreNow = 7u; // a brand-new session, ticks start near 0 - uint restoredNext = ProductionMath.RestoreNextTick(restoreNow, remaining); - Assert.AreEqual(47u, restoredNext, "Restore re-stamps now + remaining so the cooldown gap survives across sessions."); - // And the gap measured from the restore epoch matches the original remaining. - Assert.AreEqual(40u, ProductionMath.RemainingTicks(restoredNext, restoreNow)); - } - - [Test] - public void RestoreNextTick_Coerces_Zero_Sum_Away_From_The_Inactive_Sentinel() - { - // now+remaining == 0 (both zero) must not collapse to the "inactive" sentinel; TickUtil.NonZero coerces to 1. - uint restoredNext = ProductionMath.RestoreNextTick(nowTick: 0u, remaining: 0u); - Assert.AreEqual(1u, restoredNext, "A 0 sum is coerced to 1 (the 0 = inactive sentinel is reserved)."); - } - } -} diff --git a/Assets/_Project/Tests/EditMode/ProductionMathTests.cs.meta b/Assets/_Project/Tests/EditMode/ProductionMathTests.cs.meta deleted file mode 100644 index 3d44f20a2..000000000 --- a/Assets/_Project/Tests/EditMode/ProductionMathTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 1c51d5b61b932d04c9273a3d674752a3 \ No newline at end of file