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 SQLite; using Xunit; namespace CounterDrone.Core.Tests { public class FullPipelineTests : IDisposable { private readonly string _testDir; private readonly IPathProvider _paths; private readonly IScenarioService _scenario; private readonly FrameDataStore _frameStore; private readonly SQLiteConnection _mainDb; private List _ammoCatalog; private string _taskId = string.Empty; public FullPipelineTests() { _testDir = Path.Combine(Path.GetTempPath(), $"cd_full_{Guid.NewGuid():N}"); _paths = new TestPathProvider(_testDir); var dbm = new DatabaseManager(_paths); _mainDb = dbm.OpenMainDb(); // 使用默认弹药规格 foreach (var a in DefaultAmmunition.GetAll()) _mainDb.Insert(a); _ammoCatalog = new List(DefaultAmmunition.GetAll()); _scenario = 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)); _frameStore = new FrameDataStore(_paths); } public void Dispose() { _mainDb?.Close(); if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true); } private SimulationEngine RunSimulation(int maxTicks, float tickDt = 1f / 20f) { var engine = new SimulationEngine(_scenario, _frameStore, new DamageModelRouter(), _paths); // 如果有推荐方案,生成发射计划 var detail = _scenario.GetTaskDetail(_taskId)!; if (detail.Equipment.Any(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform)) { var schedule = BuildFireSchedule(detail); engine.SetFireSchedule(schedule); } engine.Initialize(_taskId); engine.TimeScale = 4f; // 4倍速加速 for (int i = 0; i < maxTicks; i++) { var r = engine.Tick(tickDt); if (r.State == SimulationState.Completed) break; } engine.Stop(); return engine; } /// 从推荐方案的时机参数生成发射计划 private List BuildFireSchedule(TaskFullConfig detail) { var schedule = new List(); var cloud = detail.Cloud; if (!cloud.RecommendedTiming.HasValue || cloud.RecommendedTiming.Value <= 0) return schedule; var platforms = detail.Equipment .Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform) .ToList(); if (platforms.Count == 0) return schedule; // 每个部署平台打一发(平台数量由推荐算法确定) var releaseAlt = (float)cloud.DisperseHeight; if (releaseAlt < 10) releaseAlt = 300f; for (int i = 0; i < platforms.Count; i++) { var p = platforms[i]; var mv = (float)(p.MuzzleVelocity ?? 800); var tx = (float)cloud.PositionX; var ty = (float)cloud.PositionY; var tz = (float)cloud.PositionZ; var dx = tx - (float)p.PositionX; var dz = tz - (float)p.PositionZ; var dist = (float)Math.Sqrt(dx * dx + dz * dz); var angle = Kinematics.CalculateLaunchAngle(dist, mv, releaseAlt); var sinA = (float)Math.Sin(angle); var tUp = mv * sinA / 9.81f; var peakH = mv * mv * sinA * sinA / (2f * 9.81f); var dropH = Math.Max(0, peakH - releaseAlt); var ft = tUp + (float)Math.Sqrt(2f * dropH / 9.81f); var fireTime = (float)cloud.RecommendedTiming!.Value - ft; if (fireTime < 0) fireTime = 0.1f; schedule.Add(new FireEvent { FireTime = fireTime, PlatformIndex = i, TargetX = tx, TargetY = ty, TargetZ = tz, MuzzleVelocity = mv }); } return schedule; } /// 将 DefenseAdvisor 推荐方案写入想定 private DefenseRecommendation GetAndApplyRecommendation() { var detail = _scenario.GetTaskDetail(_taskId)!; var threat = new ThreatProfile { Environment = detail.Scene, Targets = detail.Targets, Route = detail.Route, Waypoints = detail.Waypoints, }; var rec = new DefaultDefenseAdvisor(_ammoCatalog).Recommend(threat); _scenario.SaveCloudDispersal(_taskId, rec.Best.RecommendedCloud); var equips = new List(); foreach (var d in rec.Best.Detections) equips.Add(new EquipmentDeployment { EquipmentRole = (int)EquipmentRole.Detection, Quantity = d.Quantity, PositionX = d.Position.X, PositionY = d.Position.Y, PositionZ = d.Position.Z, DetectionRadius = d.DetectionRadius, }); foreach (var p in rec.Best.Platforms) equips.Add(new EquipmentDeployment { EquipmentRole = (int)EquipmentRole.LaunchPlatform, PlatformType = (int)p.Type, Quantity = p.Quantity, PositionX = p.Position.X, PositionY = p.Position.Y, PositionZ = p.Position.Z, AerosolType = (int)rec.Best.RecommendedAerosolType, MunitionCount = p.MunitionCount, Cooldown = p.Cooldown, MuzzleVelocity = p.MuzzleVelocity, }); _scenario.SaveDeployment(_taskId, equips); return rec; } private static readonly string ReportOutputDir = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..", "..", "reports"); private void VerifyAndExportReport(SimulationEngine eng, DroneEntity drone) { var detail = _scenario.GetTaskDetail(_taskId)!; var report = new ReportGenerator().Generate(detail, eng.Events.ToList(), drone.Status, eng.SimulationTime); Assert.Contains("仿真评估报告", report); Assert.Contains(detail.Task.Name, report); var svc = new ReportService(_mainDb, _paths); var r = svc.Generate(_taskId, detail, eng.Events.ToList(), drone.Status.ToString(), eng.SimulationTime); svc.ExportToFile(r.Id, ReportOutputDir); // 持久化目录 } // ═══════════════════════════════════════════════ // 场景 1:无防御 — 无人机到达目标 // ═══════════════════════════════════════════════ [Fact] public void Scenario_NoDefense_DroneReachesTarget() { var task = _scenario.CreateTask("无防御", ""); _taskId = task.Id; _scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 }); _scenario.SaveTarget(_taskId, new TargetConfig { TargetType = (int)TargetType.FixedWing, PowerType = (int)PowerType.Jet, Quantity = 1, TypicalSpeed = 600, TypicalAltitude = 400, }); _scenario.SaveRoute(_taskId, new RoutePlan { FormationMode = (int)FormationMode.Single }, new List { new Waypoint { PosX = 0, PosY = 400, PosZ = 0, Speed = 600 }, new Waypoint { PosX = 3000, PosY = 400, PosZ = 0, Speed = 600 }, }); _scenario.SaveCloudDispersal(_taskId, new CloudDispersal()); _scenario.SaveDeployment(_taskId, new List()); var eng = RunSimulation(400); Assert.Equal(DroneStatus.ReachedTarget, eng.Drones[0].Status); } // ═══════════════════════════════════════════════ // 场景 2:管控区域侵入 // ═══════════════════════════════════════════════ [Fact] public void Scenario_ZoneIntrusion_DroneMarked() { var task = _scenario.CreateTask("管控区", ""); _taskId = task.Id; _scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 }); _scenario.SaveTarget(_taskId, new TargetConfig { TargetType = (int)TargetType.Electric, PowerType = (int)PowerType.Electric, Quantity = 1, TypicalSpeed = 300, TypicalAltitude = 300, }); _scenario.SaveRoute(_taskId, new RoutePlan { FormationMode = (int)FormationMode.Single }, new List { new Waypoint { PosX = 0, PosY = 300, PosZ = 0, Speed = 300 }, new Waypoint { PosX = 3000, PosY = 300, PosZ = 0, Speed = 300 }, }); _scenario.SaveCloudDispersal(_taskId, new CloudDispersal()); _scenario.SaveDeployment(_taskId, new List()); _scenario.SaveControlZones(_taskId, new List { new Models.ControlZone { Name = "禁飞区", VerticesJson = "[{\"X\":1200,\"Y\":0,\"Z\":-200},{\"X\":1800,\"Y\":0,\"Z\":-200},{\"X\":1800,\"Y\":0,\"Z\":200},{\"X\":1200,\"Y\":0,\"Z\":200}]", MinAltitude = 0, MaxAltitude = 1000, }, }); var eng = RunSimulation(400); Assert.Equal(DroneStatus.ZoneIntruded, eng.Drones[0].Status); } // ═══════════════════════════════════════════════ // 场景 3:活塞发动机 → 算法推荐惰性气体 → 拦截成功 // ═══════════════════════════════════════════════ [Fact] public void Scenario_Piston_InertGasIntercept() { var task = _scenario.CreateTask("活塞拦截", ""); _taskId = task.Id; _scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 3, WindDirection = (int)WindDirection.W, }); _scenario.SaveTarget(_taskId, new TargetConfig { TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston, Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500, }); _scenario.SaveRoute(_taskId, new RoutePlan { FormationMode = (int)FormationMode.Single }, new List { new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 }, new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 }, }); var rec = GetAndApplyRecommendation(); // 验证推荐参数和装备被正确持久化 var saved = _scenario.GetTaskDetail(_taskId)!; Assert.True(saved.Cloud.RecommendedTiming.HasValue, $"Piston: RecommendedTiming={saved.Cloud.RecommendedTiming}"); Assert.True(saved.Cloud.RecommendedTiming > 0); var platforms = saved.Equipment.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform).ToList(); Assert.True(platforms.Count > 0, "应有发射平台"); // 直接验证 BuildFireSchedule 逻辑 var c = saved.Cloud; var p = platforms[0]; var mv = (float)(p.MuzzleVelocity ?? 800); var dx = (float)c.PositionX - (float)p.PositionX; var dz = (float)c.PositionZ - (float)p.PositionZ; var dist = (float)System.Math.Sqrt(dx * dx + dz * dz); var ft = dist / mv; var fireTime = (float)c.RecommendedTiming!.Value - ft; Assert.True(fireTime > 0, $"发射时间={fireTime}, 距离={dist}, 飞行时间={ft}, 推荐时机={c.RecommendedTiming}"); Assert.Equal(AerosolType.InertGas, rec.Best.RecommendedAerosolType); Assert.True(rec.Best.InterceptProbability > 0); Assert.True(rec.Best.RecommendedCloud.RecommendedTiming > 0); var eng = RunSimulation(8000); var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched); var cloudsGenerated = eng.Events.Count(e => e.Type == SimEventType.CloudGenerated); var drone = eng.Drones[0]; var msg = $"Piston: guns={platforms.Count}, launched={launched}, clouds={cloudsGenerated}, " + $"droneStatus={drone.Status}, hp={drone.Hp:F3}, posX={drone.PosX:F0}, " + $"cloudCount={eng.Clouds.Count}"; Assert.True(launched > 0, msg); Assert.True(cloudsGenerated > 0, msg); VerifyAndExportReport(eng, drone); } // ═══════════════════════════════════════════════ // 场景 4:喷气发动机 → 算法推荐活性材料 // ═══════════════════════════════════════════════ [Fact] public void Scenario_Jet_ActiveMaterialIntercept() { var task = _scenario.CreateTask("喷气拦截", ""); _taskId = task.Id; _scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 }); _scenario.SaveTarget(_taskId, new TargetConfig { TargetType = (int)TargetType.HighSpeed, PowerType = (int)PowerType.Jet, Quantity = 1, TypicalSpeed = 500, TypicalAltitude = 800, }); _scenario.SaveRoute(_taskId, new RoutePlan { FormationMode = (int)FormationMode.Single }, new List { new Waypoint { PosX = 0, PosY = 800, PosZ = 0, Speed = 500 }, new Waypoint { PosX = 30000, PosY = 800, PosZ = 0, Speed = 500 }, }); var rec = GetAndApplyRecommendation(); var saved = _scenario.GetTaskDetail(_taskId)!; Assert.True(saved.Cloud.RecommendedTiming.HasValue, $"Jet: RecommendedTiming={saved.Cloud.RecommendedTiming}"); Assert.Equal(AerosolType.ActiveMaterial, rec.Best.RecommendedAerosolType); Assert.True(rec.Best.InterceptProbability > 0); Assert.True(rec.Best.RecommendedCloud.RecommendedTiming > 0); var jetPlatforms = saved.Equipment.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform).ToList(); var eng = RunSimulation(8000); var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched); var cloudsGenerated = eng.Events.Count(e => e.Type == SimEventType.CloudGenerated); var drone = eng.Drones[0]; var msg = $"Jet: guns={jetPlatforms.Count}, launched={launched}, clouds={cloudsGenerated}, " + $"droneStatus={drone.Status}, hp={drone.Hp:F3}, posX={drone.PosX:F0}, " + $"cloudCount={eng.Clouds.Count}"; Assert.True(launched > 0, msg); Assert.True(cloudsGenerated > 0, msg); VerifyAndExportReport(eng, drone); } } }