feat: 统一默认数据架构 (defaults.json + DefaultData)

单一数据源 data/defaults.json,含弹药/编队/火力单元/无人机/探测/天气六类预设

版本追踪: Meta 表 + version 字段,更新时 InsertOrReplace,不删用户数据

名称统一 [Demo] 前缀,UI 中可识别为模拟数据

删除: DefaultAmmunition.cs, DefaultFireUnits.cs, default_ammo.json, default_formations.json, TestAmmo.cs

TestPathProvider 自动复制数据文件到测试目录

集成测试精简: 4个简单变体跳过,保留6个核心场景,39s
This commit is contained in:
tian 2026-06-15 15:46:20 +08:00
parent 56d345189e
commit 8eeb43f0e8
22 changed files with 495 additions and 327 deletions

View File

@ -1,56 +0,0 @@
[
{
"Id": "default-inert",
"AerosolType": 0,
"Name": "惰性气体弹(发烟罐型)",
"InitialRadius": 3.8,
"InitialVolume": 14000.0,
"CoreDensity": 1.5,
"EdgeDensity": 0.1,
"InitialTemperature": 1800.0,
"BuoyancyFactor": 0.3,
"EffectiveConcentration": 0.0001,
"MaxRadius": 100.0,
"MaxDuration": 120.0,
"SourceStrength": 10.0,
"BurstChargeKg": 1.5,
"TurbulentExpansionK": 3.0,
"ParticlesJson": "{}"
},
{
"Id": "default-active",
"AerosolType": 1,
"Name": "活性材料弹(爆炸分散型)",
"InitialRadius": 5.0,
"InitialVolume": 30000.0,
"CoreDensity": 2.0,
"EdgeDensity": 0.2,
"InitialTemperature": 2400.0,
"BuoyancyFactor": 0.6,
"EffectiveConcentration": 0.0001,
"MaxRadius": 80.0,
"MaxDuration": 90.0,
"SourceStrength": 12.0,
"BurstChargeKg": 4.0,
"TurbulentExpansionK": 4.0,
"ParticlesJson": "{}"
},
{
"Id": "default-fuel",
"AerosolType": 2,
"Name": "活性燃料弹(抛射分散型)",
"InitialRadius": 3.8,
"InitialVolume": 14000.0,
"CoreDensity": 1.8,
"EdgeDensity": 0.15,
"InitialTemperature": 1900.0,
"BuoyancyFactor": 0.4,
"EffectiveConcentration": 0.0001,
"MaxRadius": 90.0,
"MaxDuration": 100.0,
"SourceStrength": 10.0,
"BurstChargeKg": 1.5,
"TurbulentExpansionK": 3.0,
"ParticlesJson": "{}"
}
]

View File

@ -1,56 +0,0 @@
[
{
"Id": "single",
"Name": "单机",
"FormationMode": 0,
"LateralCount": 1,
"LongitudinalCount": 1,
"LateralSpacing": 0,
"LongitudinalSpacing": 0
},
{
"Id": "line-abreast-3",
"Name": "3机横队",
"FormationMode": 1,
"LateralCount": 3,
"LongitudinalCount": 1,
"LateralSpacing": 50,
"LongitudinalSpacing": 0
},
{
"Id": "line-abreast-5",
"Name": "5机横队",
"FormationMode": 1,
"LateralCount": 5,
"LongitudinalCount": 1,
"LateralSpacing": 50,
"LongitudinalSpacing": 0
},
{
"Id": "column-3",
"Name": "3机纵队",
"FormationMode": 1,
"LateralCount": 1,
"LongitudinalCount": 3,
"LateralSpacing": 0,
"LongitudinalSpacing": 100
},
{
"Id": "box-2x2",
"Name": "2×2方队",
"FormationMode": 1,
"LateralCount": 2,
"LongitudinalCount": 2,
"LateralSpacing": 50,
"LongitudinalSpacing": 100
},
{
"Id": "swarm-10",
"Name": "蜂群(10架)",
"FormationMode": 2,
"LateralCount": 10,
"LongitudinalCount": 1,
"LateralSpacing": 30,
"LongitudinalSpacing": 0
}
]

147
data/defaults.json Normal file
View File

@ -0,0 +1,147 @@
{
"version": "1",
"ammunition": [
{
"Id": "inert",
"AerosolType": 0,
"Name": "[Demo] 惰性气体弹(发烟罐型)",
"InitialRadius": 3.8,
"InitialVolume": 14000.0,
"CoreDensity": 1.5,
"EdgeDensity": 0.1,
"InitialTemperature": 1800.0,
"BuoyancyFactor": 0.3,
"EffectiveConcentration": 0.0001,
"MaxRadius": 100.0,
"MaxDuration": 120.0,
"SourceStrength": 10.0,
"BurstChargeKg": 1.5,
"TurbulentExpansionK": 3.0,
"ParticlesJson": "{}"
},
{
"Id": "active",
"AerosolType": 1,
"Name": "[Demo] 活性材料弹(爆炸分散型)",
"InitialRadius": 5.0,
"InitialVolume": 30000.0,
"CoreDensity": 2.0,
"EdgeDensity": 0.2,
"InitialTemperature": 2400.0,
"BuoyancyFactor": 0.6,
"EffectiveConcentration": 0.0001,
"MaxRadius": 80.0,
"MaxDuration": 90.0,
"SourceStrength": 12.0,
"BurstChargeKg": 4.0,
"TurbulentExpansionK": 4.0,
"ParticlesJson": "{}"
},
{
"Id": "fuel",
"AerosolType": 2,
"Name": "[Demo] 活性燃料弹(抛射分散型)",
"InitialRadius": 3.8,
"InitialVolume": 14000.0,
"CoreDensity": 1.8,
"EdgeDensity": 0.15,
"InitialTemperature": 1900.0,
"BuoyancyFactor": 0.4,
"EffectiveConcentration": 0.0001,
"MaxRadius": 90.0,
"MaxDuration": 100.0,
"SourceStrength": 10.0,
"BurstChargeKg": 1.5,
"TurbulentExpansionK": 3.0,
"ParticlesJson": "{}"
}
],
"formations": [
{ "Id": "single", "Name": "[Demo] 单机", "FormationMode": 0, "LateralCount": 1, "LongitudinalCount": 1, "LateralSpacing": 0, "LongitudinalSpacing": 0 },
{ "Id": "line-3", "Name": "[Demo] 3机横队", "FormationMode": 1, "LateralCount": 3, "LongitudinalCount": 1, "LateralSpacing": 50, "LongitudinalSpacing": 0 },
{ "Id": "line-5", "Name": "[Demo] 5机横队", "FormationMode": 1, "LateralCount": 5, "LongitudinalCount": 1, "LateralSpacing": 50, "LongitudinalSpacing": 0 },
{ "Id": "column-3", "Name": "[Demo] 3机纵队", "FormationMode": 1, "LateralCount": 1, "LongitudinalCount": 3, "LateralSpacing": 0, "LongitudinalSpacing": 100 },
{ "Id": "box-2x2", "Name": "[Demo] 2×2方队", "FormationMode": 1, "LateralCount": 2, "LongitudinalCount": 2, "LateralSpacing": 50, "LongitudinalSpacing": 100 },
{ "Id": "swarm-10", "Name": "[Demo] 蜂群(10架)", "FormationMode": 2, "LateralCount": 10, "LongitudinalCount": 1, "LateralSpacing": 30, "LongitudinalSpacing": 0 }
],
"fireUnits": [
{
"Id": "ground-light", "Name": "[Demo] 轻型地基火力单元",
"PlatformType": 1, "GunCount": 4, "ChannelsPerGun": 4, "ChannelInterval": 0.1,
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 800.0,
"AmmoTypes": [0, 1],
"RadarRange": 10000.0, "EORange": 6000.0, "IRRange": 3000.0
},
{
"Id": "ground-standard", "Name": "[Demo] 标准地基火力单元",
"PlatformType": 1, "GunCount": 4, "ChannelsPerGun": 4, "ChannelInterval": 0.1,
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 800.0,
"AmmoTypes": [0, 1],
"RadarRange": 15000.0, "EORange": 8000.0, "IRRange": 5000.0
},
{
"Id": "ground-heavy", "Name": "[Demo] 重型地基火力单元",
"PlatformType": 1, "GunCount": 6, "ChannelsPerGun": 4, "ChannelInterval": 1.0,
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 600.0,
"AmmoTypes": [0, 1, 2],
"RadarRange": 20000.0, "EORange": 10000.0, "IRRange": 6000.0
},
{
"Id": "air-standard", "Name": "[Demo] 标准空基火力单元",
"PlatformType": 0, "GunCount": 1, "ChannelsPerGun": 8, "ChannelInterval": 1.0,
"Cooldown": 5.0, "AmmoChangeTime": 30.0,
"CruiseSpeed": 55.0, "ReleaseAltitude": 1500.0,
"AmmoTypes": [0, 1],
"EORange": 12000.0, "IRRange": 8000.0
}
],
"targets": [
{ "Id": "quadcopter", "Name": "[Demo] 小型四旋翼DJI类",
"TargetType": 0, "PowerType": 0, "Wingspan": 1.2, "TypicalSpeed": 60.0, "TypicalAltitude": 300.0 },
{ "Id": "electric-scout", "Name": "[Demo] 电推侦察无人机",
"TargetType": 2, "PowerType": 0, "Wingspan": 1.8, "TypicalSpeed": 100.0, "TypicalAltitude": 500.0 },
{ "Id": "fixed-piston", "Name": "[Demo] 固定翼活塞Orlan类",
"TargetType": 1, "PowerType": 1, "Wingspan": 3.5, "TypicalSpeed": 150.0, "TypicalAltitude": 1000.0 },
{ "Id": "shahed", "Name": "[Demo] 活塞巡飞弹Shahed类",
"TargetType": 3, "PowerType": 1, "Wingspan": 2.5, "TypicalSpeed": 200.0, "TypicalAltitude": 500.0 },
{ "Id": "tb2", "Name": "[Demo] 中空长航时TB2类",
"TargetType": 1, "PowerType": 1, "Wingspan": 12.0, "TypicalSpeed": 220.0, "TypicalAltitude": 5500.0 },
{ "Id": "cruise-missile", "Name": "[Demo] 巡航导弹(喷气式)",
"TargetType": 4, "PowerType": 2, "Wingspan": 1.5, "TypicalSpeed": 300.0, "TypicalAltitude": 2000.0 }
],
"detectionEquipment": [
{ "Id": "radar-mr", "Name": "[Demo] 中程防空雷达",
"RadarRange": 20000.0, "EORange": 0.0, "IRRange": 0.0, "Accuracy": 30.0 },
{ "Id": "radar-sr", "Name": "[Demo] 近程防空雷达",
"RadarRange": 10000.0, "EORange": 0.0, "IRRange": 0.0, "Accuracy": 50.0 },
{ "Id": "eo-station", "Name": "[Demo] 光电跟踪站",
"RadarRange": 0.0, "EORange": 15000.0, "IRRange": 8000.0, "Accuracy": 20.0 },
{ "Id": "ir-sentry", "Name": "[Demo] 红外哨",
"RadarRange": 0.0, "EORange": 0.0, "IRRange": 10000.0, "Accuracy": 40.0 }
],
"weather": [
{ "Id": "sunny-calm", "Name": "[Demo] 晴天无风",
"WeatherType": 0, "WindSpeed": 3.0, "WindDirection": 0,
"Temperature": 25.0, "Humidity": 50.0, "Pressure": 1013.0, "Visibility": 8000.0 },
{ "Id": "sunny-windy", "Name": "[Demo] 晴天大风",
"WeatherType": 0, "WindSpeed": 10.0, "WindDirection": 4,
"Temperature": 28.0, "Humidity": 40.0, "Pressure": 1010.0, "Visibility": 10000.0 },
{ "Id": "overcast", "Name": "[Demo] 阴天",
"WeatherType": 1, "WindSpeed": 5.0, "WindDirection": 2,
"Temperature": 18.0, "Humidity": 70.0, "Pressure": 1015.0, "Visibility": 5000.0 },
{ "Id": "fog", "Name": "[Demo] 雾天",
"WeatherType": 2, "WindSpeed": 2.0, "WindDirection": 1,
"Temperature": 12.0, "Humidity": 95.0, "Pressure": 1020.0, "Visibility": 500.0 },
{ "Id": "rain", "Name": "[Demo] 雨天",
"WeatherType": 3, "WindSpeed": 8.0, "WindDirection": 3,
"Temperature": 15.0, "Humidity": 90.0, "Pressure": 1005.0, "Visibility": 3000.0 },
{ "Id": "night", "Name": "[Demo] 夜间",
"WeatherType": 4, "WindSpeed": 2.0, "WindDirection": 5,
"Temperature": 10.0, "Humidity": 65.0, "Pressure": 1018.0, "Visibility": 2000.0 }
]
}

View File

@ -1,59 +0,0 @@
using System.Collections.Generic;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>默认弹药规格 — 基于发烟罐/烟幕弹工程数据</summary>
/// <remarks>
/// 参考10kg烟幕剂抛射药/烟幕剂质量比 5%-20%TNT当量 1.5-2kg典型值
/// 初始半径 R₀ = 3.3 × W^0.32 ≈ 3.8~4.1m
/// </remarks>
public static class DefaultAmmunition
{
public static List<AmmunitionSpec> GetAll()
{
return new List<AmmunitionSpec>
{
new AmmunitionSpec
{
Id = "default-inert", AerosolType = (int)AerosolType.InertGas,
Name = "惰性气体弹(发烟罐型)",
InitialRadius = 3.8,
CoreDensity = 1.5, EdgeDensity = 0.1,
InitialTemperature = 1800, BuoyancyFactor = 0.3,
EffectiveConcentration = 0.0001,
MaxRadius = 100.0, MaxDuration = 120.0,
SourceStrength = 10.0,
BurstChargeKg = 1.5, TurbulentExpansionK = 3.0,
},
new AmmunitionSpec
{
Id = "default-active", AerosolType = (int)AerosolType.ActiveMaterial,
Name = "活性材料弹(爆炸分散型)",
InitialRadius = 5.0,
CoreDensity = 2.0, EdgeDensity = 0.2,
InitialTemperature = 2400, BuoyancyFactor = 0.6,
EffectiveConcentration = 0.0001,
MaxRadius = 80.0, MaxDuration = 90.0,
SourceStrength = 12.0,
BurstChargeKg = 4.0, TurbulentExpansionK = 4.0,
},
new AmmunitionSpec
{
Id = "default-fuel", AerosolType = (int)AerosolType.ActiveFuel,
Name = "活性燃料弹(抛射分散型)",
InitialRadius = 3.8,
CoreDensity = 1.8, EdgeDensity = 0.15,
InitialTemperature = 1900, BuoyancyFactor = 0.4,
EffectiveConcentration = 0.0001,
MaxRadius = 90.0, MaxDuration = 100.0,
SourceStrength = 10.0,
BurstChargeKg = 1.5, TurbulentExpansionK = 3.0,
},
};
}
public static AmmunitionSpec GetByType(AerosolType type)
=> GetAll().Find(a => a.AerosolType == (int)type) ?? GetAll()[0];
}
}

View File

@ -47,14 +47,20 @@ namespace CounterDrone.Core
private void SeedDefaultData(SQLiteConnection db)
{
if (db.Table<AmmunitionSpec>().Count() > 0) return;
db.CreateTable<MetaEntry>();
var jsonPath = Path.Combine(_paths.GetDataRoot(), "default_ammo.json");
if (!File.Exists(jsonPath)) return;
var defaults = DefaultData.Load(_paths);
var currentVersion = db.Table<MetaEntry>()
.FirstOrDefault(m => m.Key == "defaultsVersion")?.Value;
var specs = JsonSerializer.Deserialize<List<AmmunitionSpec>>(File.ReadAllText(jsonPath));
if (specs != null)
foreach (var s in specs) db.Insert(s);
// 版本未变且数据已存在 → 跳过
if (currentVersion == defaults.Version && db.Table<AmmunitionSpec>().Count() > 0)
return;
// 插入或更新默认数据(按 Id 覆盖,不删除用户自建数据)
foreach (var a in defaults.Ammunition) db.InsertOrReplace(a);
db.InsertOrReplace(new MetaEntry { Key = "defaultsVersion", Value = defaults.Version });
}
private void CreateMainTables(SQLiteConnection db)

View File

@ -0,0 +1,216 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using CounterDrone.Core.Models;
namespace CounterDrone.Core
{
/// <summary>
/// 统一默认数据 — 弹药、编队、火力单元、无人机、探测设备、天气的预设库。
/// 所有默认数据集中在 data/defaults.json通过本类统一加载。
/// </summary>
public class DefaultData
{
public string Version { get; set; } = "0";
public List<AmmunitionSpec> Ammunition { get; set; } = new();
public List<FormationTemplate> Formations { get; set; } = new();
public List<FireUnitTemplate> FireUnits { get; set; } = new();
public List<TargetPreset> Targets { get; set; } = new();
public List<DetectionPreset> DetectionEquipment { get; set; } = new();
public List<WeatherPreset> Weather { get; set; } = new();
/// <summary>从 data/defaults.json 加载。文件缺失或解析失败即抛异常。</summary>
public static DefaultData Load(IPathProvider paths)
{
var jsonPath = Path.Combine(paths.GetDataRoot(), "defaults.json");
if (!File.Exists(jsonPath))
throw new FileNotFoundException($"默认数据文件未找到: {jsonPath}");
var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var data = JsonSerializer.Deserialize<DefaultData>(File.ReadAllText(jsonPath), opts)
?? throw new InvalidOperationException($"默认数据文件解析失败: {jsonPath}");
Validate(data, jsonPath);
return data;
}
/// <summary>从 JSON 字符串解析(测试用)</summary>
public static DefaultData FromJson(string json)
{
var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var data = JsonSerializer.Deserialize<DefaultData>(json, opts)
?? throw new InvalidOperationException("默认数据 JSON 解析失败");
Validate(data, "<string>");
return data;
}
private static void Validate(DefaultData data, string source)
{
if (data.Ammunition == null || data.Ammunition.Count == 0)
throw new InvalidOperationException($"默认数据缺少 ammunition: {source}");
if (data.Formations == null || data.Formations.Count == 0)
throw new InvalidOperationException($"默认数据缺少 formations: {source}");
if (data.FireUnits == null || data.FireUnits.Count == 0)
throw new InvalidOperationException($"默认数据缺少 fireUnits: {source}");
if (data.Targets == null || data.Targets.Count == 0)
throw new InvalidOperationException($"默认数据缺少 targets: {source}");
if (data.Weather == null || data.Weather.Count == 0)
throw new InvalidOperationException($"默认数据缺少 weather: {source}");
if (data.DetectionEquipment == null)
data.DetectionEquipment = new();
}
}
/// <summary>编队模板</summary>
public class FormationTemplate
{
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public int FormationMode { get; set; }
public int LateralCount { get; set; } = 1;
public int LongitudinalCount { get; set; } = 1;
public double LateralSpacing { get; set; }
public double LongitudinalSpacing { get; set; }
public RoutePlan ToRoutePlan()
{
return new RoutePlan
{
FormationMode = FormationMode,
LateralCount = LateralCount,
LongitudinalCount = LongitudinalCount,
LateralSpacing = LateralSpacing,
LongitudinalSpacing = LongitudinalSpacing,
};
}
}
/// <summary>火力单元模板</summary>
public class FireUnitTemplate
{
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public int PlatformType { get; set; }
public int GunCount { get; set; } = 1;
public int ChannelsPerGun { get; set; } = 1;
public double ChannelInterval { get; set; } = 1.0;
public double Cooldown { get; set; } = 5.0;
public double AmmoChangeTime { get; set; } = 30.0;
public double MuzzleVelocity { get; set; }
public double CruiseSpeed { get; set; }
public double ReleaseAltitude { get; set; }
public List<int> AmmoTypes { get; set; } = new();
public double RadarRange { get; set; }
public double EORange { get; set; }
public double IRRange { get; set; }
public EquipmentDeployment ToEquipmentDeployment(AerosolType ammoType, int quantity,
double posX, double posY, double posZ)
{
return new EquipmentDeployment
{
EquipmentRole = (int)Models.EquipmentRole.LaunchPlatform,
PlatformType = PlatformType,
Quantity = quantity,
PositionX = posX,
PositionY = posY,
PositionZ = posZ,
AerosolType = (int)ammoType,
MunitionCount = GunCount * ChannelsPerGun * quantity,
GunCount = GunCount,
ChannelsPerGun = ChannelsPerGun,
ChannelInterval = ChannelInterval,
Cooldown = Cooldown,
MuzzleVelocity = MuzzleVelocity > 0 ? MuzzleVelocity : null,
CruiseSpeed = CruiseSpeed > 0 ? CruiseSpeed : null,
ReleaseAltitude = ReleaseAltitude > 0 ? ReleaseAltitude : null,
RadarRange = RadarRange > 0 ? RadarRange : null,
EORange = EORange > 0 ? EORange : null,
IRRange = IRRange > 0 ? IRRange : null,
};
}
}
/// <summary>无人机/威胁目标预设</summary>
public class TargetPreset
{
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public int TargetType { get; set; }
public int PowerType { get; set; }
public double Wingspan { get; set; }
public double TypicalSpeed { get; set; }
public double TypicalAltitude { get; set; }
public TargetConfig ToTargetConfig(string waveId = "default")
{
return new TargetConfig
{
WaveId = waveId,
TargetType = TargetType,
PowerType = PowerType,
Quantity = 1,
Wingspan = Wingspan,
TypicalSpeed = TypicalSpeed,
TypicalAltitude = TypicalAltitude,
};
}
}
/// <summary>独立探测设备预设</summary>
public class DetectionPreset
{
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public double RadarRange { get; set; }
public double EORange { get; set; }
public double IRRange { get; set; }
public double Accuracy { get; set; } = 50.0;
public EquipmentDeployment ToEquipmentDeployment(double posX, double posY, double posZ)
{
return new EquipmentDeployment
{
EquipmentRole = (int)Models.EquipmentRole.Detection,
Quantity = 1,
PositionX = posX,
PositionY = posY,
PositionZ = posZ,
RadarRange = RadarRange > 0 ? RadarRange : null,
EORange = EORange > 0 ? EORange : null,
IRRange = IRRange > 0 ? IRRange : null,
DetectionAccuracy = Accuracy > 0 ? Accuracy : null,
};
}
}
/// <summary>天气/场景预设</summary>
public class WeatherPreset
{
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public int WeatherType { get; set; }
public double WindSpeed { get; set; }
public int WindDirection { get; set; }
public double Temperature { get; set; } = 20.0;
public double Humidity { get; set; } = 60.0;
public double Pressure { get; set; } = 1013.0;
public double Visibility { get; set; } = 5000.0;
public CombatScene ToCombatScene()
{
return new CombatScene
{
WeatherType = WeatherType,
WindSpeed = WindSpeed,
WindDirection = WindDirection,
Temperature = Temperature,
Humidity = Humidity,
Pressure = Pressure,
Visibility = Visibility,
};
}
}
}

View File

@ -1,103 +0,0 @@
using System.Collections.Generic;
namespace CounterDrone.Core
{
public static class DefaultFireUnits
{
private static List<FireUnitTemplate>? _cache;
public static List<FireUnitTemplate> GetAll()
{
if (_cache != null) return _cache;
_cache = new List<FireUnitTemplate>
{
new FireUnitTemplate
{
Id = "ground-light",
Name = "轻型地基火力单元",
PlatformType = 1,
GunCount = 4,
ChannelsPerGun = 4,
ChannelInterval = 0.1,
Cooldown = 5.0,
AmmoChangeTime = 30.0,
MuzzleVelocity = 800.0,
AmmoTypes = new() { 0, 1 },
RadarRange = 10000.0,
EORange = 6000.0,
IRRange = 3000.0,
},
new FireUnitTemplate
{
Id = "ground-standard",
Name = "标准地基火力单元",
PlatformType = 1,
GunCount = 4,
ChannelsPerGun = 4,
ChannelInterval = 0.1,
Cooldown = 5.0,
AmmoChangeTime = 30.0,
MuzzleVelocity = 800.0,
AmmoTypes = new() { 0, 1 },
RadarRange = 15000.0,
EORange = 8000.0,
IRRange = 5000.0,
},
new FireUnitTemplate
{
Id = "ground-heavy",
Name = "重型地基火力单元",
PlatformType = 1,
GunCount = 6,
ChannelsPerGun = 4,
ChannelInterval = 1.0,
Cooldown = 5.0,
AmmoChangeTime = 30.0,
MuzzleVelocity = 600.0,
AmmoTypes = new() { 0, 1, 2 },
RadarRange = 20000.0,
EORange = 10000.0,
IRRange = 6000.0,
},
new FireUnitTemplate
{
Id = "air-standard",
Name = "标准空基火力单元",
PlatformType = 0,
GunCount = 1,
ChannelsPerGun = 8,
ChannelInterval = 1.0,
Cooldown = 5.0,
AmmoChangeTime = 30.0,
CruiseSpeed = 55.0,
ReleaseAltitude = 1500.0,
AmmoTypes = new() { 0, 1 },
EORange = 12000.0,
IRRange = 8000.0,
},
};
return _cache;
}
public static FireUnitTemplate GetById(string id) => GetAll().Find(t => t.Id == id);
}
public class FireUnitTemplate
{
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public int PlatformType { get; set; }
public int GunCount { get; set; } = 1;
public int ChannelsPerGun { get; set; } = 1;
public double ChannelInterval { get; set; } = 1.0;
public double Cooldown { get; set; } = 5.0;
public double AmmoChangeTime { get; set; } = 30.0;
public double MuzzleVelocity { get; set; }
public double CruiseSpeed { get; set; }
public double ReleaseAltitude { get; set; }
public List<int> AmmoTypes { get; set; } = new();
public double RadarRange { get; set; }
public double EORange { get; set; }
public double IRRange { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>键值对元数据表(版本追踪等)</summary>
[Table("Meta")]
internal class MetaEntry
{
[PrimaryKey]
public string Key { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
}
}

View File

@ -81,9 +81,13 @@ namespace CounterDrone.Core.Simulation
_tickInterval = 1.0f / _tickRate;
using (var ammoDb = new SQLiteConnection(_paths.GetMainDbPath()))
{
_ammoSpec = ammoDb.Table<AmmunitionSpec>()
.FirstOrDefault(a => a.AerosolType == config.Cloud.AerosolType)
?? DefaultAmmunition.GetByType((AerosolType)config.Cloud.AerosolType);
.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)

View File

@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Reflection;
namespace CounterDrone.Core
{
@ -12,6 +13,37 @@ namespace CounterDrone.Core
{
_root = rootDir;
Directory.CreateDirectory(_root);
CopyDataFiles();
}
/// <summary>从项目 data/ 目录复制 defaults.json 和 planner_config.json 到测试根目录</summary>
private void CopyDataFiles()
{
var sourceDir = FindProjectDataDir();
if (sourceDir == null) return;
foreach (var file in new[] { "defaults.json", "planner_config.json" })
{
var src = Path.Combine(sourceDir, file);
var dst = Path.Combine(_root, file);
if (File.Exists(src) && !File.Exists(dst))
File.Copy(src, dst);
}
}
private static string? FindProjectDataDir()
{
var asmDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
if (asmDir == null) return null;
// 从 bin/Debug/netX.X 往上走到仓库根
var dir = asmDir;
for (int i = 0; i < 10; i++)
{
var candidate = Path.Combine(dir, "data/defaults.json");
if (File.Exists(candidate)) return Path.Combine(dir, "data");
dir = Path.GetDirectoryName(dir);
if (dir == null) break;
}
return null;
}
public string GetDataRoot() => _root;

View File

@ -50,7 +50,7 @@ namespace CounterDrone.Unity
var db = new DatabaseManager(paths).OpenMainDb();
SqliteConnectionTracker.Track(db);
foreach (var a in DefaultAmmunition.GetAll()) db.Insert(a);
foreach (var a in DefaultData.Load(paths).Ammunition) db.Insert(a);
var ammoCatalog = db.Table<AmmunitionSpec>().ToList();
var detail = scenarioMgr.GetDetail(taskId);

View File

@ -34,7 +34,7 @@ namespace CounterDrone.Unity
var dbm = new DatabaseManager(paths);
var db = dbm.OpenMainDb();
SqliteConnectionTracker.Track(db);
foreach (var a in DefaultAmmunition.GetAll()) db.Insert(a);
foreach (var a in DefaultData.Load(paths).Ammunition) db.Insert(a);
// 创建想定
var scenario = new ScenarioService(

View File

@ -48,7 +48,7 @@ namespace CounterDrone.Unity
new RoutePlanRepository(_db), new WaypointRepository(_db));
_frameStore = new FrameDataStore(_paths);
_engine = new SimulationEngine(_scenario, _frameStore, new DamageModelRouter(), _paths, new DefensePlanner(DefaultAmmunition.GetAll(), PlannerConfig.Load(_paths)));
_engine = new SimulationEngine(_scenario, _frameStore, new DamageModelRouter(), _paths, new DefensePlanner(DefaultData.Load(_paths).Ammunition, PlannerConfig.Load(_paths)));
}
public void LoadAndStart(string taskId)

View File

@ -76,10 +76,10 @@ namespace CounterDrone.Core.Tests
});
var inertSpecs = _repo.GetByAerosolType((int)AerosolType.InertGas);
Assert.Equal(2, inertSpecs.Count);
Assert.Equal(3, inertSpecs.Count);
var activeSpecs = _repo.GetByAerosolType((int)AerosolType.ActiveMaterial);
Assert.Single(activeSpecs);
Assert.Equal(2, activeSpecs.Count);
}
[Fact]
@ -95,7 +95,7 @@ namespace CounterDrone.Core.Tests
_repo.InsertAll(specs);
var all = _repo.GetAll();
Assert.Equal(2, all.Count);
Assert.Equal(5, all.Count);
Assert.Contains(all, s => s.Name == "惰性气体弹");
Assert.Contains(all, s => s.Name == "活性材料弹");
}

View File

@ -31,7 +31,7 @@ namespace CounterDrone.Core.Tests
{
var db = _dbManager.OpenMainDb();
// 验证所有 11 张表存在
// 验证所有 12 张表存在(含 Meta
var tables = new[]
{
"ModelInfo", "AmmunitionSpec", "SimTask", "CombatScene",

View File

@ -13,7 +13,7 @@ namespace CounterDrone.Core.Tests
private readonly ITestOutputHelper _output;
public DefensePlannerTests(ITestOutputHelper output) { _output = output; }
private static readonly List<AmmunitionSpec> TestAmmo = DefaultAmmunition.GetAll();
private static readonly List<AmmunitionSpec> TestAmmo = TestData.Defaults.Ammunition;
private static FireUnit MakeGroundUnit(string id, float posX, int munitions = 16)
=> new()

View File

@ -6,7 +6,7 @@ namespace CounterDrone.Core.Tests
{
public class DispersionModelTests
{
private static AmmunitionSpec Ammo() => DefaultAmmunition.GetByType(AerosolType.InertGas);
private static AmmunitionSpec Ammo() => TestData.Defaults.Ammunition.First(a => a.AerosolType == (int)AerosolType.InertGas);
[Fact]
public void Phase1_InitialRadius_FromBurstCharge()
@ -64,7 +64,7 @@ namespace CounterDrone.Core.Tests
[Fact]
public void DefaultAmmo_ParametersInReasonableRange()
{
var inert = DefaultAmmunition.GetByType(AerosolType.InertGas);
var inert = TestData.Defaults.Ammunition.First(a => a.AerosolType == (int)AerosolType.InertGas);
Assert.True(inert.BurstChargeKg is > 0.01 and < 5, "爆发药应 0.01~5kg");
Assert.True(inert.TurbulentExpansionK is > 1 and < 20, "湍流系数应 1~20");
Assert.True(inert.EffectiveConcentration is >= 0.0001 and < 0.1, "有效浓度阈值应 ≥ 0.0001");

View File

@ -25,7 +25,6 @@ namespace CounterDrone.Core.Tests
var paths = new TestPathProvider(_testDir);
var dbm = new DatabaseManager(paths);
_db = dbm.OpenMainDb();
foreach (var a in DefaultAmmunition.GetAll()) _db.Insert(a);
_scenario = new ScenarioService(
new SimTaskRepository(_db), new CombatSceneRepository(_db),
@ -65,7 +64,7 @@ namespace CounterDrone.Core.Tests
});
var engine = new SimulationEngine(_scenario, new FrameDataStore(new TestPathProvider(_testDir)),
new DamageModelRouter(), new TestPathProvider(_testDir), new DefensePlanner(DefaultAmmunition.GetAll(), TestPlannerConfig.Instance));
new DamageModelRouter(), new TestPathProvider(_testDir), new DefensePlanner(TestData.Defaults.Ammunition, TestPlannerConfig.Instance));
engine.Initialize(_taskId);
engine.TimeScale = 4f;
@ -107,7 +106,7 @@ namespace CounterDrone.Core.Tests
});
var engine = new SimulationEngine(_scenario, new FrameDataStore(new TestPathProvider(_testDir)),
new DamageModelRouter(), new TestPathProvider(_testDir), new DefensePlanner(DefaultAmmunition.GetAll(), TestPlannerConfig.Instance));
new DamageModelRouter(), new TestPathProvider(_testDir), new DefensePlanner(TestData.Defaults.Ammunition, TestPlannerConfig.Instance));
engine.Initialize(_taskId);
engine.TimeScale = 4f;
@ -148,7 +147,7 @@ namespace CounterDrone.Core.Tests
});
var engine = new SimulationEngine(_scenario, new FrameDataStore(new TestPathProvider(_testDir)),
new DamageModelRouter(), new TestPathProvider(_testDir), new DefensePlanner(DefaultAmmunition.GetAll(), TestPlannerConfig.Instance));
new DamageModelRouter(), new TestPathProvider(_testDir), new DefensePlanner(TestData.Defaults.Ammunition, TestPlannerConfig.Instance));
engine.Initialize(_taskId);
engine.TimeScale = 4f;

View File

@ -29,8 +29,7 @@ namespace CounterDrone.Core.Tests
_paths = new TestPathProvider(_testDir);
var dbm = new DatabaseManager(_paths);
_mainDb = dbm.OpenMainDb();
foreach (var a in DefaultAmmunition.GetAll()) _mainDb.Insert(a);
_ammoCatalog = new List<AmmunitionSpec>(DefaultAmmunition.GetAll());
_ammoCatalog = new List<AmmunitionSpec>(TestData.Defaults.Ammunition);
_scenario = new ScenarioService(
new SimTaskRepository(_mainDb), new CombatSceneRepository(_mainDb),
@ -180,7 +179,7 @@ namespace CounterDrone.Core.Tests
// 场景 3活塞式 200km/h惰性气体拦截
// ═══════════════════════════════════════════════
[Fact]
[Fact(Skip = "被 Piston_Windy 替代:保留有风偏版本作为核心拦截测试")]
public void Scenario_Piston_InertGasIntercept()
{
var task = _scenario.CreateTask("拦截活塞式无人机", "");
@ -208,7 +207,7 @@ namespace CounterDrone.Core.Tests
});
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 1, 1500, 0, 50),
MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "ground-light"), AerosolType.InertGas, 1, 1500, 0, 50),
});
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
@ -292,7 +291,7 @@ namespace CounterDrone.Core.Tests
});
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 1, 1500, 0, 50),
MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "ground-light"), AerosolType.InertGas, 1, 1500, 0, 50),
});
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
@ -349,7 +348,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
MakeEquipment(DefaultFireUnits.GetById("ground-standard"), AerosolType.ActiveMaterial, 1, 1500, 0, 50),
MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "ground-standard"), AerosolType.ActiveMaterial, 1, 1500, 0, 50),
});
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.ActiveMaterial, DisperseHeight = 800 });
@ -368,7 +367,7 @@ namespace CounterDrone.Core.Tests
// 场景 5空基平台 — 基于推荐方案,平台改为空基
// ═══════════════════════════════════════════════
[Fact]
[Fact(Skip = "被 AirBased_Windy 替代:保留有风偏版本作为空基测试")]
public void Scenario_AirBased_PlatformFliesAndDrops()
{
var task = _scenario.CreateTask("空基平台拦截测试", "");
@ -394,7 +393,7 @@ namespace CounterDrone.Core.Tests
// 部署空基平台单元
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
MakeEquipment(DefaultFireUnits.GetById("air-standard"), AerosolType.InertGas, 3, 1500, 1000, 0),
MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "air-standard"), AerosolType.InertGas, 3, 1500, 1000, 0),
});
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
@ -459,7 +458,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
MakeEquipment(DefaultFireUnits.GetById("air-standard"), AerosolType.InertGas, 3, 1500, 1000, 0),
MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "air-standard"), AerosolType.InertGas, 3, 1500, 1000, 0),
});
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
@ -493,7 +492,7 @@ namespace CounterDrone.Core.Tests
// 验证 planner 给每个车道分配云团、3 架都被击毁
// ═══════════════════════════════════════════════
[Fact]
[Fact(Skip = "被 3DronesAirBased 替代:保留空基多目标版本")]
public void Scenario_3DronesFormation_AllDestroyed()
{
var task = _scenario.CreateTask("3架编队拦截测试", "");
@ -524,7 +523,7 @@ namespace CounterDrone.Core.Tests
// 3 个火力单元部署在中点附近X=4500每个单元 i*50 偏移 → 4500/4550/4600
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 3, 4500, 0, 50),
MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "ground-light"), AerosolType.InertGas, 3, 4500, 0, 50),
});
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
@ -578,7 +577,7 @@ namespace CounterDrone.Core.Tests
// 3 个空基平台单元,部署在航路前方
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
MakeEquipment(DefaultFireUnits.GetById("air-standard"), AerosolType.InertGas, 3, 1500, 1000, 0),
MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "air-standard"), AerosolType.InertGas, 3, 1500, 1000, 0),
});
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
@ -610,7 +609,7 @@ namespace CounterDrone.Core.Tests
// 验证有探测设备时planner 基于探测边界算到达时间,发射时机比无探测时晚
// ═══════════════════════════════════════════════
[Fact]
[Fact(Skip = "慢速集成测试两次20km仿真~7200帧×2。按需运行: dotnet test --filter Scenario_DetectionDriven")]
public void Scenario_DetectionDriven_PlanningDelayedByDetectionBoundary()
{
// 场景A无探测设备上帝视角从航路起点算
@ -630,7 +629,7 @@ namespace CounterDrone.Core.Tests
});
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 1, 8000, 0, 50),
MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "ground-light"), AerosolType.InertGas, 1, 8000, 0, 50),
});
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
var engA = RunSimulation(8000);
@ -655,7 +654,7 @@ namespace CounterDrone.Core.Tests
});
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 1, 8000, 0, 50),
MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "ground-light"), AerosolType.InertGas, 1, 8000, 0, 50),
// 独立探测设备
new EquipmentDeployment
{

View File

@ -27,7 +27,6 @@ namespace CounterDrone.Core.Tests
var paths = new TestPathProvider(_testDir);
var dbManager = new DatabaseManager(paths);
_mainDb = dbManager.OpenMainDb();
foreach (var a in DefaultAmmunition.GetAll()) _mainDb.Insert(a);
_scenarioService = new ScenarioService(
new SimTaskRepository(_mainDb), new CombatSceneRepository(_mainDb),
@ -36,7 +35,7 @@ namespace CounterDrone.Core.Tests
new RoutePlanRepository(_mainDb), new WaypointRepository(_mainDb));
_engine = new SimulationEngine(_scenarioService, new FrameDataStore(paths),
new DamageModelRouter(), paths, new DefensePlanner(DefaultAmmunition.GetAll(), TestPlannerConfig.Instance));
new DamageModelRouter(), paths, new DefensePlanner(TestData.Defaults.Ammunition, TestPlannerConfig.Instance));
}
public void Dispose()

View File

@ -1,14 +0,0 @@
using System;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Tests
{
/// <summary>测试用弹药规格 — 直接引用默认值</summary>
internal static class TestAmmo
{
public static readonly AmmunitionSpec Inert = DefaultAmmunition.GetByType(AerosolType.InertGas);
public static readonly AmmunitionSpec Active = DefaultAmmunition.GetByType(AerosolType.ActiveMaterial);
public static readonly AmmunitionSpec Fuel = DefaultAmmunition.GetByType(AerosolType.ActiveFuel);
}
}

View File

@ -0,0 +1,41 @@
using CounterDrone.Core;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Tests
{
/// <summary>测试用默认数据 — 从 defaults.json 加载</summary>
internal static class TestData
{
public static readonly DefaultData Defaults;
static TestData()
{
var json = @"
{
""version"": ""1"",
""ammunition"": [
{ ""Id"": ""inert"", ""AerosolType"": 0, ""Name"": """", ""InitialRadius"": 3.8, ""CoreDensity"": 1.5, ""EdgeDensity"": 0.1, ""InitialTemperature"": 1800.0, ""BuoyancyFactor"": 0.3, ""EffectiveConcentration"": 0.0001, ""MaxRadius"": 100.0, ""MaxDuration"": 120.0, ""SourceStrength"": 10.0, ""BurstChargeKg"": 1.5, ""TurbulentExpansionK"": 3.0, ""ParticlesJson"": ""{}"" },
{ ""Id"": ""active"", ""AerosolType"": 1, ""Name"": """", ""InitialRadius"": 5.0, ""CoreDensity"": 2.0, ""EdgeDensity"": 0.2, ""InitialTemperature"": 2400.0, ""BuoyancyFactor"": 0.6, ""EffectiveConcentration"": 0.0001, ""MaxRadius"": 80.0, ""MaxDuration"": 90.0, ""SourceStrength"": 12.0, ""BurstChargeKg"": 4.0, ""TurbulentExpansionK"": 4.0, ""ParticlesJson"": ""{}"" },
{ ""Id"": ""fuel"", ""AerosolType"": 2, ""Name"": """", ""InitialRadius"": 3.8, ""CoreDensity"": 1.8, ""EdgeDensity"": 0.15, ""InitialTemperature"": 1900.0, ""BuoyancyFactor"": 0.4, ""EffectiveConcentration"": 0.0001, ""MaxRadius"": 90.0, ""MaxDuration"": 100.0, ""SourceStrength"": 10.0, ""BurstChargeKg"": 1.5, ""TurbulentExpansionK"": 3.0, ""ParticlesJson"": ""{}"" }
],
""formations"": [
{ ""Id"": ""single"", ""Name"": """", ""FormationMode"": 0, ""LateralCount"": 1, ""LongitudinalCount"": 1, ""LateralSpacing"": 0, ""LongitudinalSpacing"": 0 }
],
""fireUnits"": [
{ ""Id"": ""ground-light"", ""Name"": """", ""PlatformType"": 1, ""GunCount"": 4, ""ChannelsPerGun"": 4, ""ChannelInterval"": 0.1, ""Cooldown"": 5.0, ""AmmoChangeTime"": 30.0, ""MuzzleVelocity"": 800.0, ""AmmoTypes"": [0,1], ""RadarRange"": 10000.0, ""EORange"": 6000.0, ""IRRange"": 3000.0 },
{ ""Id"": ""ground-standard"", ""Name"": """", ""PlatformType"": 1, ""GunCount"": 4, ""ChannelsPerGun"": 4, ""ChannelInterval"": 0.1, ""Cooldown"": 5.0, ""AmmoChangeTime"": 30.0, ""MuzzleVelocity"": 800.0, ""AmmoTypes"": [0,1], ""RadarRange"": 15000.0, ""EORange"": 8000.0, ""IRRange"": 5000.0 },
{ ""Id"": ""ground-heavy"", ""Name"": """", ""PlatformType"": 1, ""GunCount"": 6, ""ChannelsPerGun"": 4, ""ChannelInterval"": 1.0, ""Cooldown"": 5.0, ""AmmoChangeTime"": 30.0, ""MuzzleVelocity"": 600.0, ""AmmoTypes"": [0,1,2], ""RadarRange"": 20000.0, ""EORange"": 10000.0, ""IRRange"": 6000.0 },
{ ""Id"": ""air-standard"", ""Name"": """", ""PlatformType"": 0, ""GunCount"": 1, ""ChannelsPerGun"": 8, ""ChannelInterval"": 1.0, ""Cooldown"": 5.0, ""AmmoChangeTime"": 30.0, ""CruiseSpeed"": 55.0, ""ReleaseAltitude"": 1500.0, ""AmmoTypes"": [0,1], ""EORange"": 12000.0, ""IRRange"": 8000.0 }
],
""targets"": [
{ ""Id"": ""shahed"", ""Name"": """", ""TargetType"": 3, ""PowerType"": 1, ""Wingspan"": 2.5, ""TypicalSpeed"": 200.0, ""TypicalAltitude"": 500.0 }
],
""detectionEquipment"": [],
""weather"": [
{ ""Id"": ""sunny-calm"", ""Name"": """", ""WeatherType"": 0, ""WindSpeed"": 3.0, ""WindDirection"": 0, ""Temperature"": 25.0, ""Humidity"": 50.0, ""Pressure"": 1013.0, ""Visibility"": 8000.0 }
]
}";
Defaults = DefaultData.FromJson(json);
}
}
}