using System; using System.Collections.Generic; using System.IO; using System.Linq; using CounterDrone.Core; using CounterDrone.Core.Algorithms; using CounterDrone.Core.Models; using CounterDrone.Core.Repository; using CounterDrone.Core.Services; using CounterDrone.Core.Simulation; using Xunit; namespace CounterDrone.Core.Tests { /// 仿真引擎基础单元测试(不含集成场景,集成场景见 FullPipelineTests) public class SimulationEngineTests : IDisposable { private readonly string _testDir; private readonly SimulationEngine _engine; private readonly IScenarioService _scenarioService; private readonly SQLite.SQLiteConnection _mainDb; private string _taskId = string.Empty; public SimulationEngineTests() { _testDir = Path.Combine(Path.GetTempPath(), $"cd_test_{Guid.NewGuid():N}"); var paths = new TestPathProvider(_testDir); var dbManager = new DatabaseManager(paths); _mainDb = dbManager.OpenMainDb(); _scenarioService = new ScenarioService( new SimTaskRepository(_mainDb), new CombatSceneRepository(_mainDb), new ControlZoneRepository(_mainDb), new TargetConfigRepository(_mainDb), new EquipmentDeploymentRepository(_mainDb), new CloudDispersalRepository(_mainDb), new RoutePlanRepository(_mainDb), new WaypointRepository(_mainDb)); _engine = new SimulationEngine(_scenarioService, new FrameDataStore(paths), new DamageModelRouter(), paths, new DefensePlanner(TestData.Defaults.Ammunition, TestPlannerConfig.Instance)); } public void Dispose() { _engine.Stop(); _mainDb?.Close(); if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true); } private void SetupScenario() { var task = _scenarioService.CreateTask("Test", ""); _taskId = task.Id; _scenarioService.SaveScene(_taskId, new CombatScene { WindSpeed = 0 }); _scenarioService.SaveTarget(_taskId, new TargetConfig { WaveId = "default", TargetType = (int)TargetType.Piston, Quantity = 1, PowerType = (int)PowerType.Piston, TypicalSpeed = 60, }); _scenarioService.SaveDeployment(_taskId, new List { new EquipmentDeployment { EquipmentRole = (int)EquipmentRole.LaunchPlatform, PlatformType = (int)PlatformType.GroundBased, Quantity = 1, PositionX = 0, PositionY = 0, PositionZ = 0, AerosolType = (int)AerosolType.InertGas, MunitionCount = 1, Cooldown = 5, }, }); _scenarioService.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 300, Duration = 60, RecommendedTiming = 10, PositionX = 1000, PositionY = 300, PositionZ = 0, PositionMode = (int)PositionMode.AlgorithmRecommended, }); _scenarioService.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single }, new List { new Waypoint { PosX = 0, PosY = 300, PosZ = 0, Altitude = 300, Speed = 60 }, new Waypoint { PosX = 10000, PosY = 300, PosZ = 0, Altitude = 300, Speed = 60 }, }); } [Fact] public void Initialize_LoadsEntities() { SetupScenario(); _engine.Initialize(_taskId); Assert.Single(_engine.Drones); Assert.Equal(SimulationState.Running, _engine.State); } [Fact] public void Tick_DroneMoves() { SetupScenario(); _engine.Initialize(_taskId); _engine.Tick(1f / 20f); Assert.True(_engine.Drones[0].PosX > 0); } [Fact] public void PauseAndResume() { SetupScenario(); _engine.Initialize(_taskId); _engine.Pause(); Assert.Equal(SimulationState.Paused, _engine.State); var result = _engine.Tick(1f); Assert.Equal(SimulationState.Paused, result.State); _engine.Resume(); Assert.Equal(SimulationState.Running, _engine.State); } [Fact] public void Stop_ClosesGracefully() { SetupScenario(); _engine.Initialize(_taskId); _engine.Tick(1f / 20f); _engine.Stop(); Assert.Equal(SimulationState.Stopped, _engine.State); } [Fact] public void FrameData_IsWritten() { SetupScenario(); _engine.Initialize(_taskId); for (int i = 0; i < 10; i++) _engine.Tick(1f / 20f); var framePath = Path.Combine(_testDir, "frames", $"{_taskId}.db"); Assert.True(File.Exists(framePath)); } [Fact] public void AirBased_LaunchesWithCorrectDescription() { var task = _scenarioService.CreateTask("t", ""); _scenarioService.SaveScene(task.Id, new CombatScene()); _scenarioService.SaveTarget(task.Id, new TargetConfig { WaveId = "d", Quantity = 1, TypicalSpeed = 60, TypicalAltitude = 300 }); _scenarioService.SaveDeployment(task.Id, new List { new() { EquipmentRole = (int)EquipmentRole.LaunchPlatform, PlatformType = (int)PlatformType.AirBased, Quantity = 1, PositionX = 0, PositionY = 2000, PositionZ = 0, CruiseSpeed = 55, ReleaseAltitude = 1500, AerosolType = (int)AerosolType.InertGas, MunitionCount = 1 }, }); _scenarioService.SaveCloudDispersal(task.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 300 }); _scenarioService.SaveRoute(task.Id, "d", new RoutePlan(), new() { new() { PosX = 0, PosY = 300, PosZ = 0, Speed = 60 }, new() { PosX = 5000, PosY = 300, PosZ = 0, Speed = 60 } }); _engine.SetFireSchedule(new List { new() { FireTime = 0.1f, PlatformIndex = 0, TargetX = 2500, TargetY = 300, TargetZ = 0 } }); _engine.Initialize(task.Id); _engine.TimeScale = 10f; SimEvent launch = null; for (int i = 0; i < 5000; i++) { _engine.Tick(1f / 20f); launch = _engine.Events.FirstOrDefault(e => e.Type == SimEventType.MunitionLaunched); if (launch != null) break; if (_engine.State == SimulationState.Completed) break; } Assert.NotNull(launch); Assert.Contains("空基", launch.Description); } [Fact] public void GroundBased_LaunchesWithCorrectDescription() { SetupScenario(); _engine.SetFireSchedule(new List { new() { FireTime = 0.1f, PlatformIndex = 0, TargetX = 1000, TargetY = 300, TargetZ = 0, MuzzleVelocity = 800 } }); _engine.Initialize(_taskId); SimEvent launch = null; for (int i = 0; i < 50; i++) { _engine.Tick(1f / 20f); launch = _engine.Events.FirstOrDefault(e => e.Type == SimEventType.MunitionLaunched); if (launch != null) break; } Assert.NotNull(launch); Assert.Contains("计划发射", launch.Description); } } }