LANTERN purge B1: delete the automation chain (Harvester/Conveyor/Fabricator)
Deletes the M7 production systems, automation components/math, authoring, 3 machine prefabs, and 6 test files (-43 tests, 459 green). Trims the automation paths out of BaseRestoreSystem/SaveStructureScan/BuildPlaceSystem/BuildSendSystem/ HudSystem/HudTheme/StructureCatalogAuthoring/Tuning. RuntimePlacedTag (save marker, a keeper) re-homed into StructureComponents.cs. StructureType byte codes stay reserved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c51a770922f68b046b12dc55a7f054c2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,35 +0,0 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Authoring for a Conveyor belt ghost prefab. Bakes <see cref="PlacedStructure"/>{Type=Conveyor} +
|
||||
/// <see cref="Conveyor"/> (default facing; BuildPlaceSystem overrides Direction per placement from the RPC) +
|
||||
/// a DISABLED <see cref="ConveyorItem"/> (an empty belt). BuildPlaceSystem stamps the Cell; the transport
|
||||
/// system initializes the period gate on first encounter.
|
||||
/// </summary>
|
||||
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<ConveyorAuthoring>
|
||||
{
|
||||
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<ConveyorItem>(entity, false); // baked empty (disabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 23131e6166dce204582bbedc8511658e
|
||||
@@ -1,53 +0,0 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Authoring for a Fabricator machine ghost prefab. Bakes <see cref="PlacedStructure"/>{Type=Fabricator} +
|
||||
/// <see cref="Fabricator"/> recipe + a (kept) empty <see cref="MachineInput"/> 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
|
||||
/// (<see cref="Fabricator.InputFromLedger"/> != 0) — mints the turret-ammo Charge that <c>TurretFireSystem</c>
|
||||
/// spends per shot.
|
||||
/// </summary>
|
||||
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<FabricatorAuthoring>
|
||||
{
|
||||
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<MachineInput>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 014833c467fb1d6499f437b7bf76db80
|
||||
@@ -1,36 +0,0 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Authoring for a Harvester machine ghost prefab (duplicate a structure ghost so the ownerless interpolated
|
||||
/// GhostAuthoringComponent comes free). Bakes <see cref="PlacedStructure"/>{Type=Harvester} + <see cref="Harvester"/>
|
||||
/// stats + an empty <see cref="MachineOutput"/> buffer. BuildPlaceSystem stamps the Cell at placement; the
|
||||
/// production system initializes the tick baseline on first encounter (NextTick/LastProcessedTick baked 0).
|
||||
/// </summary>
|
||||
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<HarvesterAuthoring>
|
||||
{
|
||||
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<MachineOutput>(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d69d7296747332b4fbe7901ec5210149
|
||||
@@ -6,12 +6,10 @@ using UnityEngine.Serialization;
|
||||
namespace ProjectM.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Authoring for the baked <see cref="StructureCatalog"/> 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 <see cref="StructureCatalogEntry"/> buffer is already the data-driven shape. Place
|
||||
/// once in the gameplay subscene.
|
||||
/// Authoring for the baked <see cref="StructureCatalog"/> 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
|
||||
/// <see cref="StructureCatalogEntry"/> buffer is the data-driven shape. Place once in the gameplay subscene.
|
||||
/// </summary>
|
||||
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<StructureCatalogAuthoring>
|
||||
{
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// <summary>EDITOR / execute_code hook: queue a wall placement at a specific cell.</summary>
|
||||
public static void PlaceWall(int cellX, int cellZ) => PlaceStructure(StructureType.Wall, cellX, cellZ);
|
||||
|
||||
/// <summary>EDITOR / execute_code hook: queue a fabricator placement at a specific cell.</summary>
|
||||
public static void PlaceFabricator(int cellX, int cellZ) => PlaceStructure(StructureType.Fabricator, cellX, cellZ);
|
||||
|
||||
#endif
|
||||
|
||||
protected override void OnCreate()
|
||||
|
||||
@@ -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<byte, PaletteItem> _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 "?";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,11 @@ namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// One-shot server restore of player-built structures for a "Continue" session. The menu (WorldLauncher) stages a
|
||||
/// <see cref="PendingStructure"/>/<see cref="PendingStructureIo"/> carrier in the fresh ServerWorld BEFORE the
|
||||
/// gameplay subscene streams; this system waits (RequireForUpdate) for the streamed <see cref="StructureCatalog"/>
|
||||
/// + <see cref="BaseAnchor"/> + 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
|
||||
/// (<see cref="ProductionMath.RestoreNextTick"/>; 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 +
|
||||
/// <see cref="PendingStructure"/> carrier in the fresh ServerWorld BEFORE the gameplay subscene streams; this
|
||||
/// system waits (RequireForUpdate) for the streamed <see cref="StructureCatalog"/> + <see cref="BaseAnchor"/> +
|
||||
/// 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).
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
@@ -24,14 +22,12 @@ namespace ProjectM.Server
|
||||
public partial struct BaseRestoreSystem : ISystem
|
||||
{
|
||||
ComponentLookup<LocalTransform> m_TransformLookup;
|
||||
ComponentLookup<Conveyor> m_ConveyorLookup;
|
||||
ComponentLookup<Health> m_HealthLookup;
|
||||
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
m_TransformLookup = state.GetComponentLookup<LocalTransform>(isReadOnly: true);
|
||||
m_ConveyorLookup = state.GetComponentLookup<Conveyor>(isReadOnly: true);
|
||||
m_HealthLookup = state.GetComponentLookup<Health>(isReadOnly: true);
|
||||
state.RequireForUpdate<StructureCatalog>();
|
||||
state.RequireForUpdate<BaseAnchor>();
|
||||
@@ -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<BaseAnchor>();
|
||||
@@ -56,8 +51,8 @@ namespace ProjectM.Server
|
||||
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (pending, ioBuf, carrier) in
|
||||
SystemAPI.Query<DynamicBuffer<PendingStructure>, DynamicBuffer<PendingStructureIo>>().WithEntityAccess())
|
||||
foreach (var (pending, carrier) in
|
||||
SystemAPI.Query<DynamicBuffer<PendingStructure>>().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<RuntimePlacedTag>(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<ConveyorItem>(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<MachineInput> inBuf = default;
|
||||
DynamicBuffer<MachineOutput> 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<MachineInput>(structure); inInit = true; }
|
||||
inBuf.Add(new MachineInput { ResourceId = ioBuf[r].ResourceId, Count = ioBuf[r].Count });
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!outInit) { outBuf = ecb.SetBuffer<MachineOutput>(structure); outInit = true; }
|
||||
outBuf.Add(new MachineOutput { ResourceId = ioBuf[r].ResourceId, Count = ioBuf[r].Count });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ecb.DestroyEntity(carrier);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <see cref="Conveyor"/> first PULLS one item off an adjacent
|
||||
/// upstream <see cref="MachineOutput"/> (the cell at <c>myCell − DirOffset(dir)</c> — i.e. the machine feeding
|
||||
/// INTO this belt) onto its own <see cref="ConveyorItem"/>; then every loaded conveyor advances its item
|
||||
/// EXACTLY one cell toward <c>myCell + DirOffset(dir)</c>. The move resolution is delegated to the pure,
|
||||
/// unit-tested <see cref="ConveyorMath.ResolveMoves"/> so determinism is provable WITHOUT a world: sources are
|
||||
/// processed sorted by <see cref="ConveyorMath.CellKey"/> (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
|
||||
/// <see cref="MachineInput"/> never collides with belt occupancy.
|
||||
/// <para>
|
||||
/// Mirrors <c>TurretFireSystem</c>'s now-extraction (<c>NetworkTime.ServerTick.TickIndexForValidTick</c>) +
|
||||
/// <see cref="PlacedStructure.NextTick"/> cooldown idiom (each conveyor is period-gated the same way), and
|
||||
/// <c>ResourceHarvestSystem</c>'s Temp-collection foreach idiom. Runs in the plain server
|
||||
/// <c>SimulationSystemGroup</c> <c>[UpdateAfter(HarvesterProductionSystem)]</c> (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.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
||||
[UpdateAfter(typeof(HarvesterProductionSystem))]
|
||||
public partial struct ConveyorTransportSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<NetworkTime>();
|
||||
state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly<Conveyor>()));
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
|
||||
if (!serverTick.IsValid)
|
||||
return;
|
||||
uint now = serverTick.TickIndexForValidTick;
|
||||
|
||||
// ---- Snapshot every conveyor once (entity, cell, direction, item, period-due) ----
|
||||
var convEntity = new NativeList<Entity>(Allocator.Temp);
|
||||
var convCell = new NativeList<int2>(Allocator.Temp);
|
||||
var convDir = new NativeList<byte>(Allocator.Temp);
|
||||
var convItemRes = new NativeList<int>(Allocator.Temp); // 0 = empty
|
||||
var convItemCnt = new NativeList<int>(Allocator.Temp); // 0 = empty
|
||||
var convDue = new NativeList<bool>(Allocator.Temp); // period-gate satisfied this tick
|
||||
|
||||
foreach (var (ps, conveyor, e) in
|
||||
SystemAPI.Query<RefRW<PlacedStructure>, RefRO<Conveyor>>().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<ConveyorItem>(e))
|
||||
{
|
||||
var item = SystemAPI.GetComponent<ConveyorItem>(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<int2, int>(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<int2>(8, Allocator.Temp);
|
||||
var sinkCellToEntity = new NativeHashMap<int2, Entity>(8, Allocator.Temp);
|
||||
foreach (var (ps, _, e) in
|
||||
SystemAPI.Query<RefRO<PlacedStructure>, DynamicBuffer<MachineInput>>().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<int2, Entity>(8, Allocator.Temp);
|
||||
foreach (var (ps, _, e) in
|
||||
SystemAPI.Query<RefRO<PlacedStructure>, DynamicBuffer<MachineOutput>>().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<MachineOutput>(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<int2>(n, Allocator.Temp);
|
||||
var dirs = new NativeArray<byte>(n, Allocator.Temp);
|
||||
var itemRes = new NativeArray<int>(n, Allocator.Temp);
|
||||
var itemCnt = new NativeArray<int>(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<int2>(n, Allocator.Temp);
|
||||
var outMoveSrcIdx = new NativeArray<int>(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<int>(n, Allocator.Temp);
|
||||
var endCnt = new NativeArray<int>(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<MachineInput>(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<ConveyorItem>(e, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
SystemAPI.SetComponent(e, new ConveyorItem { ResourceId = 0, Count = 0 });
|
||||
SystemAPI.SetComponentEnabled<ConveyorItem>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 002c28988137cb945b9ffaccbb6d645f
|
||||
@@ -1,104 +0,0 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-only, deterministic fabricator production — the BACK of the M7 auto-gather chain
|
||||
/// (Harvester → Conveyor → Fabricator). Each <see cref="Fabricator"/> consumes
|
||||
/// <see cref="Fabricator.InAmount"/> of its (byte) input resource from its OWN server-only
|
||||
/// <see cref="MachineInput"/> buffer (filled by an upstream conveyor) and, on a
|
||||
/// <see cref="Fabricator.PeriodTicks"/> cadence, deposits <see cref="Fabricator.OutAmount"/> of its output
|
||||
/// resource into the GLOBAL ledger — so a self-running base compounds its stockpile. Resolves the ledger via
|
||||
/// <c>GetSingletonEntity<ResourceLedger>()</c> → <c>GetBuffer<StorageEntry>()</c> (NEVER
|
||||
/// <c>GetSingleton<StorageEntry></c> — a second StorageEntry buffer exists on the base container).
|
||||
/// Mirrors <c>TurretFireSystem</c>'s now-extraction + cooldown idiom and <c>ResourceHarvestSystem</c>'s ledger
|
||||
/// resolve; runs in the plain server <c>SimulationSystemGroup</c>
|
||||
/// <c>[UpdateAfter(ConveyorTransportSystem)]</c> (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.
|
||||
/// <para>
|
||||
/// SINGLE GATED CATCH-UP PATH, INPUT-LIMITED (no mint-from-nothing): when due, the awarded
|
||||
/// <see cref="ProductionMath.CyclesDue"/> cycles are further clamped to what the buffered input can afford
|
||||
/// (<c>floor(TotalOf(input,InResourceId)/InAmount)</c>). The tick fields are re-stamped EVERY due period
|
||||
/// regardless of <c>runs</c> (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.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
||||
[UpdateAfter(typeof(ConveyorTransportSystem))]
|
||||
public partial struct FabricatorProductionSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<NetworkTime>();
|
||||
state.RequireForUpdate<ResourceLedger>();
|
||||
state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly<Fabricator>()));
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
|
||||
if (!serverTick.IsValid)
|
||||
return;
|
||||
uint now = serverTick.TickIndexForValidTick;
|
||||
|
||||
var ledgerEntity = SystemAPI.GetSingletonEntity<ResourceLedger>();
|
||||
var ledger = SystemAPI.GetBuffer<StorageEntry>(ledgerEntity);
|
||||
|
||||
foreach (var (ps, fab, input) in
|
||||
SystemAPI.Query<RefRW<PlacedStructure>, RefRO<Fabricator>, DynamicBuffer<MachineInput>>())
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d8dbf02b41c9a94ea57fd7ca00f266d
|
||||
@@ -1,76 +0,0 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-only, deterministic harvester production — the FRONT of the M7 auto-gather chain
|
||||
/// (Harvester → Conveyor → Fabricator). Each <see cref="Harvester"/> machine is a fixed-yield generator:
|
||||
/// every <see cref="Harvester.PeriodTicks"/> server ticks it deposits <see cref="Harvester.Yield"/> of its
|
||||
/// configured (byte) resource into its OWN server-only <see cref="MachineOutput"/> buffer (NOT the global
|
||||
/// ledger — a conveyor pulls it onward, or it sits buffered). Mirrors <c>TurretFireSystem</c>'s exact
|
||||
/// now-extraction (<c>NetworkTime.ServerTick.TickIndexForValidTick</c>) + <see cref="PlacedStructure.NextTick"/>
|
||||
/// cooldown idiom, and runs in the plain server <c>SimulationSystemGroup</c>
|
||||
/// <c>[UpdateAfter(PredictedSimulationSystemGroup)]</c> (the predicted group is OrderFirst → UpdateBefore is
|
||||
/// ignored). Production mutates a DynamicBuffer in place (not a structural change) → no ECB needed.
|
||||
/// <para>
|
||||
/// 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
|
||||
/// <see cref="ProductionMath.CyclesDue"/> awards <c>floor((now-LastProcessedTick)/period)</c> cycles, clamped
|
||||
/// to <see cref="Tuning.MaxProductionCatchup"/>, 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.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
||||
[UpdateAfter(typeof(PredictedSimulationSystemGroup))]
|
||||
public partial struct HarvesterProductionSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<NetworkTime>();
|
||||
state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly<Harvester>()));
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
|
||||
if (!serverTick.IsValid)
|
||||
return;
|
||||
uint now = serverTick.TickIndexForValidTick;
|
||||
|
||||
foreach (var (ps, harvester, output) in
|
||||
SystemAPI.Query<RefRW<PlacedStructure>, RefRO<Harvester>, DynamicBuffer<MachineOutput>>())
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c484cb2331137134888f10eed7689140
|
||||
@@ -23,13 +23,11 @@ namespace ProjectM.Server
|
||||
public partial struct BuildPlaceSystem : ISystem
|
||||
{
|
||||
ComponentLookup<LocalTransform> m_TransformLookup;
|
||||
ComponentLookup<Conveyor> m_ConveyorLookup;
|
||||
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
m_TransformLookup = state.GetComponentLookup<LocalTransform>(isReadOnly: true);
|
||||
m_ConveyorLookup = state.GetComponentLookup<Conveyor>(isReadOnly: true);
|
||||
state.RequireForUpdate<StructureCatalog>();
|
||||
state.RequireForUpdate<BaseAnchor>();
|
||||
state.RequireForUpdate<ResourceLedger>();
|
||||
@@ -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<NetworkTime>().ServerTick.TickIndexForValidTick;
|
||||
var anchor = SystemAPI.GetSingleton<BaseAnchor>();
|
||||
|
||||
@@ -102,12 +99,6 @@ namespace ProjectM.Server
|
||||
});
|
||||
ecb.AddComponent(structure, new RegionTag { Region = RegionId.Base });
|
||||
ecb.AddComponent<RuntimePlacedTag>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4eba886d11c07eb4d97ca0d821a1560f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,91 +0,0 @@
|
||||
using Unity.Entities;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// A fixed-yield resource generator — the FRONT of the M7 auto-gather chain (Harvester -> Conveyor ->
|
||||
/// Fabricator). Each period it deposits <see cref="Yield"/> of <see cref="ResourceId"/> into its OWN
|
||||
/// server-only <see cref="MachineOutput"/> buffer (a conveyor pulls it onward). Server-only data (NO
|
||||
/// [GhostField]); the client only ever sees <c>PlacedStructure.Type</c>. Reuses
|
||||
/// <c>PlacedStructure.NextTick</c>/<c>LastProcessedTick</c> for the deterministic, within-session catch-up
|
||||
/// cadence (see <c>HarvesterProductionSystem</c>).
|
||||
/// </summary>
|
||||
public struct Harvester : IComponentData
|
||||
{
|
||||
/// <summary>Resource id produced (a byte; see <see cref="ResourceId"/>).</summary>
|
||||
public byte ResourceId;
|
||||
/// <summary>Units produced per elapsed period.</summary>
|
||||
public int Yield;
|
||||
/// <summary>Server ticks between productions.</summary>
|
||||
public int PeriodTicks;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A recipe machine — the BACK of the M7 chain. Consumes <see cref="InAmount"/> of <see cref="InResourceId"/>
|
||||
/// per run from its own <see cref="MachineInput"/> buffer (fed by a conveyor) and deposits <see cref="OutAmount"/>
|
||||
/// of <see cref="OutResourceId"/> into the GLOBAL ledger. Strictly input-limited (never mints from an empty
|
||||
/// slot). Server-only data.
|
||||
/// </summary>
|
||||
public struct Fabricator : IComponentData
|
||||
{
|
||||
public byte InResourceId;
|
||||
public int InAmount;
|
||||
public byte OutResourceId;
|
||||
public int OutAmount;
|
||||
public int PeriodTicks;
|
||||
|
||||
/// <summary>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].</summary>
|
||||
public byte InputFromLedger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A directional transport belt — the MIDDLE of the M7 chain. Each period it pulls one item off an adjacent
|
||||
/// upstream <see cref="MachineOutput"/> (when empty) and advances a held <see cref="ConveyorItem"/> exactly one
|
||||
/// cell toward <see cref="Direction"/>. <see cref="Direction"/> is a byte (0=+X,1=-X,2=+Z,3=-Z) — never an enum
|
||||
/// (the cross-assembly enum-in-Burst hazard). Server-only data.
|
||||
/// </summary>
|
||||
public struct Conveyor : IComponentData
|
||||
{
|
||||
/// <summary>Belt facing: 0=+X, 1=-X, 2=+Z, 3=-Z (see <c>ConveyorMath.DirOffset</c>).</summary>
|
||||
public byte Direction;
|
||||
public int PeriodTicks;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A machine's INPUT staging buffer (server-only, NO [GhostField] -> never replicated). A DISTINCT element type
|
||||
/// from the global ledger's <see cref="StorageEntry"/> (so <c>GetSingleton<StorageEntry></c> stays
|
||||
/// unambiguous) and from <see cref="MachineOutput"/> (so a machine can carry both without a buffer-type clash).
|
||||
/// </summary>
|
||||
public struct MachineInput : IBufferElementData
|
||||
{
|
||||
public byte ResourceId;
|
||||
public int Count;
|
||||
}
|
||||
|
||||
/// <summary>A machine's OUTPUT staging buffer (server-only, NO [GhostField]). See <see cref="MachineInput"/>.</summary>
|
||||
public struct MachineOutput : IBufferElementData
|
||||
{
|
||||
public byte ResourceId;
|
||||
public int Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public struct ConveyorItem : IComponentData, IEnableableComponent
|
||||
{
|
||||
public byte ResourceId;
|
||||
public int Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public struct RuntimePlacedTag : IComponentData { }
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6eeef378186b39d41a2db7adcc620dd9
|
||||
@@ -1,97 +0,0 @@
|
||||
using Unity.Collections;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Pure, deterministic, ORDER-INDEPENDENT conveyor move resolver (the server <c>ConveyorTransportSystem</c>
|
||||
/// applies the result). Determinism: sources are processed sorted by <see cref="CellKey"/> (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.
|
||||
/// </summary>
|
||||
public static class ConveyorMath
|
||||
{
|
||||
/// <summary>Cardinal grid step for a belt direction byte (0=+X,1=-X,2=+Z,3=-Z).</summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A stable, collision-free total order over grid cells (the deterministic tie-break key).</summary>
|
||||
public static long CellKey(int2 cell) => ((long)cell.x << 32) | (uint)cell.y;
|
||||
|
||||
/// <summary>
|
||||
/// 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 (<paramref name="outMoveSrcIdx"/> ->
|
||||
/// <paramref name="outMoveDst"/>), length <paramref name="moveCount"/>. 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.
|
||||
/// </summary>
|
||||
public static void ResolveMoves(
|
||||
NativeArray<int2> srcCells, NativeArray<byte> dirs,
|
||||
NativeArray<int> itemRes, NativeArray<int> itemCnt,
|
||||
NativeHashMap<int2, int> cellToIndex, NativeHashSet<int2> sinkCells,
|
||||
NativeArray<int2> outMoveDst, NativeArray<int> 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<int>(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<int2>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 251361cf456e888459d473b5fedf7c4a
|
||||
@@ -1,85 +0,0 @@
|
||||
using Unity.Entities;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Pure, deterministic deposit/withdraw/total helpers for a machine's server-only <see cref="MachineInput"/> /
|
||||
/// <see cref="MachineOutput"/> staging buffers — the byte-id, non-replicated twin of <see cref="StorageMath"/>
|
||||
/// (which serves the [GhostField] global <see cref="StorageEntry"/> 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.
|
||||
/// </summary>
|
||||
public static class MachineSlotMath
|
||||
{
|
||||
// ---- MachineOutput ----
|
||||
public static void Deposit(DynamicBuffer<MachineOutput> 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<MachineOutput> 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<MachineOutput> 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<MachineInput> 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<MachineInput> 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<MachineInput> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 16b6aef96031f54469f05044a2c18e66
|
||||
@@ -1,51 +0,0 @@
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 (<see cref="NeedsInit"/>) 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.
|
||||
/// </summary>
|
||||
public static class ProductionMath
|
||||
{
|
||||
/// <summary>True for a never-processed machine (baked/just-placed) — initialize the baseline before producing.</summary>
|
||||
public static bool NeedsInit(uint lastProcessedTick) => lastProcessedTick == 0u;
|
||||
|
||||
/// <summary>
|
||||
/// Cycles to award THIS process. 0 if cooling (<paramref name="nextTick"/> newer than <paramref name="now"/>)
|
||||
/// or nothing elapsed; otherwise floor(elapsed/period) clamped to [0, <paramref name="maxCatchup"/>].
|
||||
/// <paramref name="nextTick"/>==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.
|
||||
/// <paramref name="period"/> is guarded by max(1,...) so a 0 never divides.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Remaining cooldown ticks to PERSIST (epoch-independent): 0 if inactive or already due, else nextTick-now.</summary>
|
||||
public static uint RemainingTicks(uint nextTick, uint nowTick) =>
|
||||
nextTick == 0u ? 0u : (nextTick > nowTick ? nextTick - nowTick : 0u);
|
||||
|
||||
/// <summary>Re-anchor a persisted remaining cooldown to the current tick origin on restore (NonZero-guarded).</summary>
|
||||
public static uint RestoreNextTick(uint nowTick, uint remaining) => TickUtil.NonZero(nowTick + remaining);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d461ab50604ea642b26586bffeed41e
|
||||
@@ -76,4 +76,12 @@ namespace ProjectM.Simulation
|
||||
|
||||
/// <summary>Tag on the baked singleton carrying the <see cref="StructureCatalogEntry"/> buffer (the build cost/prefab table).</summary>
|
||||
public struct StructureCatalog : IComponentData { }
|
||||
|
||||
/// <summary>
|
||||
/// 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.)
|
||||
/// </summary>
|
||||
public struct RuntimePlacedTag : IComponentData { }
|
||||
}
|
||||
|
||||
@@ -6,17 +6,16 @@ namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Scans a server world for PLAYER-built structures (<see cref="PlacedStructure"/> + <see cref="RuntimePlacedTag"/>)
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static class SaveStructureScan
|
||||
{
|
||||
public static void Collect(EntityManager em, uint nowTick, out StructureSave[] structures, out StructureIoRow[] io)
|
||||
{
|
||||
var structs = new List<StructureSave>();
|
||||
var ioRows = new List<StructureIoRow>();
|
||||
|
||||
using var q = em.CreateEntityQuery(
|
||||
ComponentType.ReadOnly<PlacedStructure>(),
|
||||
@@ -27,46 +26,19 @@ namespace ProjectM.Simulation
|
||||
{
|
||||
var e = entities[k];
|
||||
var ps = em.GetComponentData<PlacedStructure>(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<Health>(e) ? em.GetComponentData<Health>(e).Current : 0f,
|
||||
};
|
||||
|
||||
if (em.HasComponent<Conveyor>(e))
|
||||
row.Direction = em.GetComponentData<Conveyor>(e).Direction;
|
||||
|
||||
if (em.HasComponent<ConveyorItem>(e) && em.IsComponentEnabled<ConveyorItem>(e))
|
||||
{
|
||||
var item = em.GetComponentData<ConveyorItem>(e);
|
||||
row.ConveyorResId = item.ResourceId;
|
||||
row.ConveyorCount = item.Count;
|
||||
}
|
||||
|
||||
structs.Add(row);
|
||||
|
||||
if (em.HasBuffer<MachineInput>(e))
|
||||
{
|
||||
var buf = em.GetBuffer<MachineInput>(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<MachineOutput>(e))
|
||||
{
|
||||
var buf = em.GetBuffer<MachineOutput>(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<StructureIoRow>(); // machine I/O retired with the automation chain (row type dies at save v7)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,11 +56,8 @@ namespace ProjectM.Simulation
|
||||
/// baked telegraph can't drift from the server windup.</summary>
|
||||
public const int ChargerWindupTicks = 30;
|
||||
|
||||
// ---- Production / automation (M7: Harvester/Conveyor/Fabricator) ----
|
||||
// ---- Base defense (EB-2) ----
|
||||
|
||||
/// <summary>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).</summary>
|
||||
public const int MaxProductionCatchup = 600;
|
||||
|
||||
/// <summary>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) ----
|
||||
|
||||
/// <summary>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.</summary>
|
||||
/// <summary>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.</summary>
|
||||
public const int StartingOre = 90;
|
||||
|
||||
// ---- Expedition run economy (RoomFieldSystem / RunDirectorSystem) ----
|
||||
|
||||
Reference in New Issue
Block a user