73cfe2943d
Structures (Turret/Wall/Pylon) reuse the combat spine: authoring bakes Health(GhostField)+DamageEvent buffer+a Destructible tag (no HitRadius -> no friendly projectile fire; no EffectiveCharacterStats -> clamp-to-0). HealthApplyDamageSystem destroys a Destructible at 0 (occupancy auto-frees). EnemyAISystem fortress-targets the weighted-nearest of players+structures via the shared EnemyAIMath.PickWeightedNearest (StructureAggroWeight TuningConfig knob, <1 prefers structures, squared factor; snapshot above the early-return so an undefended base is razed). Persistence v3: per-structure HP threaded through 5 sites (SaveData/PendingStructure/scan-guarded/BaseRestore same-ECB born-correct/WorldLauncher via SaveApply.ToPending); SaveService floor-gate [2,3] loads old saves. Loss feedback: proximity-gated StructureFeedbackSystem; CombatFeedbackSystem suppressed for structures. Pre-code review caught the DamageEvent-buffer crash blocker + 8 majors; post-code review clean. See DR-032. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
73 lines
3.2 KiB
C#
73 lines
3.2 KiB
C#
using System.Collections.Generic;
|
|
using Unity.Collections;
|
|
using Unity.Entities;
|
|
|
|
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
|
|
/// 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.
|
|
/// </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>(),
|
|
ComponentType.ReadOnly<RuntimePlacedTag>());
|
|
using var entities = q.ToEntityArray(Allocator.Temp);
|
|
|
|
for (int k = 0; k < entities.Length; k++)
|
|
{
|
|
var e = entities[k];
|
|
var ps = em.GetComponentData<PlacedStructure>(e);
|
|
int idx = structs.Count;
|
|
|
|
var row = 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).
|
|
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();
|
|
}
|
|
}
|
|
}
|