CounterDroneBackend/src/CounterDrone.Core/Simulation/SimulationEngine.cs
tian ad0eaf47da 完善模型属性:加 Model/Description 字段,TaskNumber→ScenarioNumber,Task→Info,Task→Scenario 方法名
- DroneProfile/DroneSpec: 加 Model、Description
- Scenario: 加 Description,TaskNumber→ScenarioNumber
- EquipmentDeployment/FireUnitSpec: 加 Description
- ScenarioConfig: Task→Info
- ScenarioService: CreateTask→CreateScenario 等方法名统一
- 238 单元测试通过
2026-06-18 15:15:41 +08:00

549 lines
27 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Linq;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
using CounterDrone.Core.Services;
using SQLite;
namespace CounterDrone.Core.Simulation
{
/// <summary>仿真引擎 — 纯执行器,接收发射计划按时间表执行</summary>
public class SimulationEngine
{
private readonly IScenarioService _scenarioService;
private readonly FrameDataStore _frameStore;
private readonly IDamageModel _damageModel;
private readonly IPathProvider _paths;
public SimulationState State { get; private set; } = SimulationState.Idle;
public int FrameIndex { get; private set; }
public float SimulationTime { get; private set; }
public float TimeScale { get; set; } = 1f;
/// <summary>是否录制逐帧数据(回放用)。默认开启。</summary>
public bool RecordFrames { get; set; } = true;
private float _tickInterval;
public IReadOnlyList<DroneEntity> Drones => _drones;
public IReadOnlyList<CloudEntity> Clouds => _clouds;
public IReadOnlyList<PlatformEntity> Platforms => _platforms;
public IReadOnlyList<MunitionEntity> Munitions => _munitions;
public IReadOnlyList<DetectionEntity> DetectionEntities => _detectionEntities;
public IReadOnlyList<SimEvent> Events => _allEvents;
public event Action<MunitionEntity>? OnMunitionLaunched;
public event Action<CloudEntity>? OnCloudGenerated;
public event Action<DroneEntity>? OnDroneDestroyed;
public event Action<DroneEntity>? OnDroneReachedTarget;
public event Action<DroneEntity, ControlZoneEntity>? OnZoneIntruded;
public event Action<DroneEntity, DetectionEntity>? OnTargetDetected;
public event Action? OnSimulationEnded;
private List<DroneEntity> _drones = new();
private List<PlatformEntity> _platforms = new();
private List<DetectionEntity> _detectionEntities = new();
private List<MunitionEntity> _munitions = new();
private List<CloudEntity> _clouds = new();
private List<ControlZoneEntity> _zones = new();
private CombatScene _scene = new();
private float _disperseHeight;
private AmmunitionSpec _ammoSpec = new();
private int _tickRate = 20;
private readonly List<FireEvent> _fireSchedule = new();
private int _nextFireIndex;
private bool _anyThreatDetected;
private float _detectionTime;
private readonly List<SimEvent> _allEvents = new();
private string _scenarioId = string.Empty;
private readonly IDefensePlanner _planner;
private int _entityCounter;
// ── GC 优化:预分配复用池 ──
private readonly List<EntitySnapshot> _snapshotPool = new();
private readonly List<SimEvent> _frameEventPool = new();
private readonly SimulationFrameResult _frameResultPool = new();
private readonly List<int> _removalIndices = new();
private readonly List<DroneEntity> _flyingDrones = new();
public SimulationEngine(IScenarioService scenarioService, FrameDataStore frameStore,
IDamageModel damageModel, IPathProvider paths, IDefensePlanner planner)
{
_scenarioService = scenarioService;
_frameStore = frameStore;
_damageModel = damageModel;
_paths = paths;
_planner = planner;
}
public void SetFireSchedule(List<FireEvent> schedule)
{
_fireSchedule.Clear();
_fireSchedule.AddRange(schedule.OrderBy(f => f.FireTime));
_nextFireIndex = 0;
}
public void Initialize(string scenarioId)
{
_scenarioId = scenarioId;
var config = _scenarioService.GetScenarioDetail(scenarioId);
if (config == null) throw new InvalidOperationException("scenario not found");
_scene = config.Scene;
_disperseHeight = (float)config.Cloud.DisperseHeight;
_tickRate = config.Info.TickRate;
_tickInterval = 1.0f / _tickRate;
using (var ammoDb = new SQLiteConnection(_paths.GetMainDbPath()))
{
_ammoSpec = ammoDb.Table<AmmunitionSpec>()
.FirstOrDefault(a => a.AerosolType == config.Cloud.AerosolType);
}
if (_ammoSpec == null || _ammoSpec.Id == null)
_ammoSpec = DefaultData.Load(_paths).Ammunition
.First(a => a.AerosolType == config.Cloud.AerosolType);
_drones.Clear();
foreach (var target in config.Targets)
{
var route = config.Routes.FirstOrDefault(r => r.WaveId == target.WaveId);
var wps = config.WaypointGroups.GetValueOrDefault(target.WaveId, new List<Waypoint>());
if (route == null || wps.Count == 0) continue;
var mode = (FormationMode)route.FormationMode;
var lateralSpacing = (float)route.LateralSpacing;
var longSpacing = (float)route.LongitudinalSpacing;
int latCount = route.LateralCount ?? target.Quantity;
int longCount = route.LongitudinalCount ?? 1;
for (int i = 0; i < target.Quantity; i++)
{
int latIdx = i % latCount;
int longIdx = i / latCount;
_drones.Add(new DroneEntity($"drone_{++_entityCounter}", target.WaveId,
target, wps, latIdx, lateralSpacing, longIdx, longSpacing, mode,
route.LateralAxis, route.LongitudinalAxis));
}
}
// 平台 ID 从 1 开始,确保与报告映射一致
_entityCounter = 0;
_platforms.Clear();
foreach (var equip in config.Equipment)
if (equip.EquipmentRole != (int)EquipmentRole.Detection)
for (int i = 0; i < equip.Quantity; i++)
{
int gunCount = equip.GunCount ?? 1;
int chPerGun = equip.ChannelsPerGun ?? 1;
for (int g = 0; g < gunCount; g++)
for (int ch = 0; ch < chPerGun; ch++)
_platforms.Add(new PlatformEntity($"plat_{++_entityCounter}", equip));
}
_zones.Clear();
foreach (var z in config.ControlZones)
_zones.Add(new ControlZoneEntity(z.Id, z.Name, z.VerticesJson,
(float)z.MinAltitude, (float)z.MaxAltitude));
// 探测设备运行时实体
_detectionEntities.Clear();
var detectionSrcs = BuildDetectionSources(config);
int detIdx = 0;
foreach (var src in detectionSrcs)
_detectionEntities.Add(new DetectionEntity($"det_{++detIdx}", src));
_munitions.Clear();
_clouds.Clear();
_allEvents.Clear();
FrameIndex = 0;
SimulationTime = 0;
_entityCounter = 0;
_nextFireIndex = 0;
// 自动从 Planner 生成发射计划
if (_fireSchedule.Count == 0)
{
var fireUnits = BuildFireUnits(config);
var threats = BuildDroneWaves(config);
if (fireUnits.Count > 0 && threats.Count > 0)
{
var detectionSources = BuildDetectionSources(config);
var result = _planner.Plan(fireUnits, threats, _scene, detectionSources);
SetFireSchedule(result.Best.MergedSchedule);
if (_fireSchedule.Count == 0)
_allEvents.Add(new SimEvent
{
Type = SimEventType.PlanningFailed,
OccurredAt = 0,
Description = result.Best.Summary,
});
}
}
if (RecordFrames)
_frameStore.BeginRecording(_scenarioId);
_anyThreatDetected = false;
State = SimulationState.Running;
}
public SimulationFrameResult Tick(float deltaTime)
{
if (State != SimulationState.Running)
{
_frameResultPool.State = State;
_frameResultPool.FrameIndex = FrameIndex;
_frameResultPool.SimulationTime = SimulationTime;
_frameResultPool.EntitySnapshots = null!;
_frameResultPool.NewEvents = null!;
return _frameResultPool;
}
var scaledDt = deltaTime * TimeScale;
SimulationTime += scaledDt;
FrameIndex++;
_frameEventPool.Clear();
// 1. 按发射计划执行(以探测时刻为时间起点)
if (_anyThreatDetected)
{
while (_nextFireIndex < _fireSchedule.Count && _fireSchedule[_nextFireIndex].FireTime + _detectionTime <= SimulationTime)
{
var fe = _fireSchedule[_nextFireIndex++];
float launchTime = fe.FireTime + _detectionTime;
var platform = _platforms.Count > fe.PlatformIndex ? _platforms[fe.PlatformIndex] : null;
if (platform == null) continue;
if (platform.PlatformType == Models.PlatformType.AirBased && platform.Ready)
{
// 空基从当前位置直接发射(与地基相同)
platform.Release();
var m = new MunitionEntity($"mun_{++_entityCounter}",
Models.PlatformType.GroundBased,
platform.AerosolType ?? Models.AerosolType.InertGas,
platform.PosX, platform.PosY, platform.PosZ,
fe.TargetX, fe.TargetY, fe.TargetZ, fe.MuzzleVelocity, _disperseHeight,
launchTime, fe.LaunchAngle, fe.FlightDuration);
_munitions.Add(m);
OnMunitionLaunched?.Invoke(m);
_frameEventPool.Add(new SimEvent { Type = SimEventType.MunitionLaunched, OccurredAt = launchTime, SourceId = platform.Id, Description = $"空基发射 {m.Id} @({platform.PosX:F0},{platform.PosY:F0},{platform.PosZ:F0})→({fe.TargetX:F0},{fe.TargetY:F0},{fe.TargetZ:F0})" });
}
else if (platform.Ready)
{
platform.Release();
var m = new MunitionEntity($"mun_{++_entityCounter}",
platform.PlatformType ?? Models.PlatformType.GroundBased,
platform.AerosolType ?? Models.AerosolType.InertGas,
platform.PosX, platform.PosY, platform.PosZ,
fe.TargetX, fe.TargetY, fe.TargetZ, fe.MuzzleVelocity, _disperseHeight,
launchTime, fe.LaunchAngle, fe.FlightDuration);
_munitions.Add(m);
OnMunitionLaunched?.Invoke(m);
_frameEventPool.Add(new SimEvent { Type = SimEventType.MunitionLaunched, OccurredAt = launchTime, SourceId = platform.Id, Description = $"计划发射 {m.Id} @({platform.PosX:F0},{platform.PosY:F0},{platform.PosZ:F0})→({fe.TargetX:F0},{fe.TargetY:F0},{fe.TargetZ:F0})" });
}
}
}
// 2. 弹药飞行 → 云团生成(手动遍历+移除索引,避免 ToList()
_removalIndices.Clear();
for (int i = 0; i < _munitions.Count; i++)
{
var m = _munitions[i];
m.Update(scaledDt);
if (m.HasArrived)
{
var disp = AlgorithmFactory.Create<ICloudDispersionModel>();
disp.Initialize(_ammoSpec, _scene, new Algorithms.Vector3(m.PosX, m.PosY, m.PosZ), m.ArrivalTime);
var cloud = new CloudEntity($"cloud_{++_entityCounter}", m.AerosolType, disp, m.ArrivalTime);
_clouds.Add(cloud);
OnCloudGenerated?.Invoke(cloud);
_frameEventPool.Add(new SimEvent { Type = SimEventType.CloudGenerated, OccurredAt = m.ArrivalTime, Description = $"云团生成 @({m.PosX:F0},{m.PosY:F0},{m.PosZ:F0})" });
_removalIndices.Add(i);
}
}
for (int i = _removalIndices.Count - 1; i >= 0; i--)
_munitions.RemoveAt(_removalIndices[i]);
// 3. 云团演化(手动遍历+移除索引)
_removalIndices.Clear();
for (int i = 0; i < _clouds.Count; i++)
{
var c = _clouds[i];
c.Tick(scaledDt, (float)_scene.WindSpeed, (WindDirection)_scene.WindDirection);
if (c.IsDissipated) _removalIndices.Add(i);
}
for (int i = _removalIndices.Count - 1; i >= 0; i--)
_clouds.RemoveAt(_removalIndices[i]);
// 4. 无人机:缓存飞行状态列表,避免多次 Where
_flyingDrones.Clear();
for (int i = 0; i < _drones.Count; i++)
if (_drones[i].Status == DroneStatus.Flying)
_flyingDrones.Add(_drones[i]);
for (int i = 0; i < _flyingDrones.Count; i++)
_flyingDrones[i].SavePreviousPosition();
for (int i = 0; i < _flyingDrones.Count; i++)
{
_flyingDrones[i].Update(scaledDt);
if (_flyingDrones[i].Status == DroneStatus.ReachedTarget)
{
OnDroneReachedTarget?.Invoke(_flyingDrones[i]);
_frameEventPool.Add(new SimEvent { Type = SimEventType.DroneReachedTarget, OccurredAt = SimulationTime, SourceId = _flyingDrones[i].Id, Description = "到达攻击目标" });
}
}
// 5. 实时探测扫描(决策 1/2离开回退、融合取最早/同刻取精度高)
if (_detectionEntities.Count > 0)
{
float visibility = (float)_scene.Visibility;
for (int di = 0; di < _flyingDrones.Count; di++)
{
var drone = _flyingDrones[di];
DetectionEntity? bestDetector = null;
float bestAccuracy = float.MaxValue;
for (int ei = 0; ei < _detectionEntities.Count; ei++)
{
var det = _detectionEntities[ei];
bool inRange = det.IsInRange(drone.PosX, drone.PosY, drone.PosZ, visibility, out _);
bool? stateChange = det.UpdateState(drone.Id, inRange);
if (stateChange == true)
{
// 决策 2首次发现同 tick 取精度最高Accuracy 最小)
if (bestDetector == null || det.Source.Accuracy < bestAccuracy)
{
bestDetector = det;
bestAccuracy = det.Source.Accuracy;
}
}
}
if (bestDetector != null)
{
if (!_anyThreatDetected)
{
_anyThreatDetected = true;
_detectionTime = SimulationTime;
}
OnTargetDetected?.Invoke(drone, bestDetector);
_frameEventPool.Add(new SimEvent
{
Type = SimEventType.TargetDetected,
OccurredAt = SimulationTime,
SourceId = bestDetector.Id,
TargetId = drone.Id,
Description = $"探测设备 {bestDetector.Id} 发现 drone {drone.Id} @({drone.PosX:F0},{drone.PosY:F0},{drone.PosZ:F0})",
});
}
}
}
// 6. 毁伤判定:积分路径段在云内的时间(快速排斥:先检查包围盒)
var (wvx, wvy, wvz) = Kinematics.WindToVector((WindDirection)_scene.WindDirection, (float)_scene.WindSpeed);
float cloudDx = wvx * scaledDt, cloudDy = wvy * scaledDt, cloudDz = wvz * scaledDt;
float effectiveConcentration = (float)_ammoSpec.EffectiveConcentration;
for (int di = 0; di < _flyingDrones.Count; di++)
{
var drone = _flyingDrones[di];
for (int ci = 0; ci < _clouds.Count; ci++)
{
var cloud = _clouds[ci];
if (cloud.Dispersion.CoreDensity < effectiveConcentration) continue;
// 快速排斥:检查无人机与云团中心的距离是否在 2×有效半径内
float effR = cloud.Dispersion.EffectiveRadius;
float ccx = cloud.Dispersion.Center.X, ccy = cloud.Dispersion.Center.Y, ccz = cloud.Dispersion.Center.Z;
float dx = drone.PosX - ccx, dy = drone.PosY - ccy, dz = drone.PosZ - ccz;
float distSq = dx * dx + dy * dy + dz * dz;
if (distSq > effR * effR * 4) continue; // 距离 > 2R不可能穿云
float inCloudDistance = DamageAssessment.PathInSphere(
drone.PrevX - (ccx - cloudDx), drone.PrevY - (ccy - cloudDy), drone.PrevZ - (ccz - cloudDz),
drone.PosX - ccx, drone.PosY - ccy, drone.PosZ - ccz,
0f, 0f, 0f, effR);
if (inCloudDistance > 0)
{
float exposureIncrement = inCloudDistance / (drone.CruiseSpeed / 3.6f);
drone.ExposureTime += exposureIncrement;
var dmg = _damageModel.CalculateDamage(drone.DroneType, drone.PowerType, cloud.AerosolType, cloud.Dispersion.CoreDensity, drone.ExposureTime, exposureIncrement);
drone.ApplyDamage(dmg);
if (drone.Status == DroneStatus.Destroyed)
{
OnDroneDestroyed?.Invoke(drone);
_frameEventPool.Add(new SimEvent { Type = SimEventType.DroneDestroyed, OccurredAt = SimulationTime, TargetId = drone.Id, Description = $"无人机 {drone.Id} 被摧毁 @({drone.PosX:F0},{drone.PosY:F0},{drone.PosZ:F0})" });
break;
}
}
}
}
// 7. 平台冷却
for (int i = 0; i < _platforms.Count; i++) _platforms[i].Update(scaledDt);
// 8. 管控区域
for (int di = 0; di < _flyingDrones.Count; di++)
{
var drone = _flyingDrones[di];
for (int zi = 0; zi < _zones.Count; zi++)
{
if (_zones[zi].ContainsPoint(drone.PosX, drone.PosY, drone.PosZ))
{
drone.MarkZoneIntruded();
OnZoneIntruded?.Invoke(drone, _zones[zi]);
_frameEventPool.Add(new SimEvent { Type = SimEventType.ZoneIntruded, OccurredAt = SimulationTime, SourceId = drone.Id, TargetId = _zones[zi].Id, Description = $"侵入 {_zones[zi].Name}" });
}
}
}
// 9. 结束判定
bool allDone = true;
for (int i = 0; i < _drones.Count; i++)
if (_drones[i].Status == DroneStatus.Flying) { allDone = false; break; }
if (allDone)
{
State = SimulationState.Completed;
OnSimulationEnded?.Invoke();
_frameEventPool.Add(new SimEvent { Type = SimEventType.SimulationEnd, OccurredAt = SimulationTime });
}
_allEvents.AddRange(_frameEventPool);
if (RecordFrames)
{
var snapshots = CollectSnapshots();
_frameStore.RecordFrame(FrameIndex, SimulationTime, snapshots);
_frameResultPool.EntitySnapshots = snapshots;
}
else
{
_frameResultPool.EntitySnapshots = null!;
}
_frameResultPool.NewEvents = _frameEventPool;
_frameResultPool.State = State;
_frameResultPool.FrameIndex = FrameIndex;
_frameResultPool.SimulationTime = SimulationTime;
return _frameResultPool;
}
public void Pause() { if (State == SimulationState.Running) State = SimulationState.Paused; }
public void Resume() { if (State == SimulationState.Paused) State = SimulationState.Running; }
public void Stop() { State = SimulationState.Stopped; _frameStore.Flush(); }
private static List<FireUnit> BuildFireUnits(ScenarioConfig config)
{
var units = new List<FireUnit>();
foreach (var eq in config.Equipment.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform))
{
int qty = Math.Max(1, eq.Quantity);
int gunCount = eq.GunCount ?? 1;
int chPerGun = eq.ChannelsPerGun ?? 1;
int channels = gunCount * chPerGun;
for (int i = 0; i < qty; i++)
{
units.Add(new FireUnit
{
Id = $"fu{units.Count}",
Type = (PlatformType)(eq.PlatformType ?? (int)PlatformType.GroundBased),
Position = new Algorithms.Vector3((float)eq.PositionX + i * 50, (float)eq.PositionY, (float)eq.PositionZ),
GunCount = gunCount,
ChannelsPerGun = chPerGun,
ChannelInterval = (float)(eq.ChannelInterval ?? 1),
CruiseSpeed = (float)(eq.CruiseSpeed ?? 0f),
ReleaseAltitude = (float)(eq.ReleaseAltitude ?? 0f),
MuzzleVelocity = (float)(eq.MuzzleVelocity ?? 0f),
TotalMunitions = eq.MunitionCount ?? channels,
AmmoTypes = new List<AerosolType> { (AerosolType)(eq.AerosolType ?? 0) },
Cooldown = (float)eq.Cooldown,
// 探测能力(火力单元自带探测,激活原死字段)
RadarRange = (float)(eq.RadarRange ?? 0f),
EORange = (float)(eq.EORange ?? 0f),
IRRange = (float)(eq.IRRange ?? 0f),
});
}
}
return units;
}
/// <summary>构建统一信息网络的探测源列表。
/// 独立探测设备EquipmentRole.Detection+ 火力单元自带探测能力。</summary>
private static List<DetectionSource> BuildDetectionSources(ScenarioConfig config)
{
var sources = new List<DetectionSource>();
foreach (var eq in config.Equipment)
{
// 只要有任何探测字段非 null 且 > 0就是有效探测源无论是否独立探测设备
bool hasDetection = (eq.RadarRange ?? 0) > 0 || (eq.EORange ?? 0) > 0 || (eq.IRRange ?? 0) > 0;
if (!hasDetection) continue;
int qty = Math.Max(1, eq.Quantity);
for (int i = 0; i < qty; i++)
{
sources.Add(new DetectionSource
{
Position = new Algorithms.Vector3((float)eq.PositionX + i * 50, (float)eq.PositionY, (float)eq.PositionZ),
RadarRange = (float)(eq.RadarRange ?? 0f),
EORange = (float)(eq.EORange ?? 0f),
IRRange = (float)(eq.IRRange ?? 0f),
Accuracy = (float)(eq.DetectionAccuracy ?? 0f),
// 3D 球冠几何:缺失时退化为无限制(等价 2D保证存量数据平滑过渡
MinElevation = (float)(eq.MinElevation ?? float.MaxValue),
MaxElevation = (float)(eq.MaxElevation ?? float.MaxValue),
MinDetectAlt = (float)(eq.MinDetectAlt ?? float.MaxValue),
MaxDetectAlt = (float)(eq.MaxDetectAlt ?? float.MaxValue),
});
}
}
return sources;
}
private static List<DroneWave> BuildDroneWaves(ScenarioConfig config)
{
var groups = new List<DroneWave>();
foreach (var t in config.Targets)
{
var route = config.Routes.FirstOrDefault(r => r.WaveId == t.WaveId);
var wps = config.WaypointGroups.GetValueOrDefault(t.WaveId, new List<Waypoint>());
groups.Add(new DroneWave
{
WaveId = t.WaveId,
Target = t,
Route = route ?? new RoutePlan(),
Waypoints = wps,
});
}
return groups;
}
private List<EntitySnapshot> CollectSnapshots()
{
_snapshotPool.Clear();
for (int i = 0; i < _drones.Count; i++)
{
var d = _drones[i];
float vx = d.PosX - d.PrevX, vy = d.PosY - d.PrevY, vz = d.PosZ - d.PrevZ;
_snapshotPool.Add(new EntitySnapshot { EntityId = d.Id, EntityType = Models.EntityType.Drone, PosX = d.PosX, PosY = d.PosY, PosZ = d.PosZ, VelX = vx, VelY = vy, VelZ = vz, DamageStage = (int)_damageModel.GetDamageStage(1 - d.Hp), Hp = d.Hp });
}
for (int i = 0; i < _munitions.Count; i++)
{
var m = _munitions[i];
var (vx, vy, vz) = m.Velocity;
_snapshotPool.Add(new EntitySnapshot { EntityId = m.Id, EntityType = Models.EntityType.Munition, PosX = m.PosX, PosY = m.PosY, PosZ = m.PosZ, VelX = vx, VelY = vy, VelZ = vz });
}
for (int i = 0; i < _platforms.Count; i++)
{
var p = _platforms[i];
var (vx, vy, vz) = p.CurrentVelocity;
_snapshotPool.Add(new EntitySnapshot { EntityId = p.Id, EntityType = Models.EntityType.Platform, PosX = p.PosX, PosY = p.PosY, PosZ = p.PosZ, VelX = vx, VelY = vy, VelZ = vz, PlatformStateStr = p.State.ToString() });
}
for (int i = 0; i < _detectionEntities.Count; i++)
{
var d = _detectionEntities[i];
_snapshotPool.Add(new EntitySnapshot { EntityId = d.Id, EntityType = Models.EntityType.DetectionEquip, PosX = d.PosX, PosY = d.PosY, PosZ = d.PosZ });
}
for (int i = 0; i < _clouds.Count; i++)
{
var c = _clouds[i];
_snapshotPool.Add(new EntitySnapshot { EntityId = c.Id, EntityType = Models.EntityType.Cloud, PosX = c.PosX, PosY = c.PosY, PosZ = c.PosZ, CloudRadius = c.Radius, CloudOpacity = c.Dispersion.Particles.Opacity, CloudPhase = c.Phase, CloudElapsed = c.Elapsed });
}
return _snapshotPool;
}
}
}