336 lines
9.7 KiB
C#
336 lines
9.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
|
|
namespace StandardScene.MagCarSimulator
|
|
{
|
|
public sealed class MagCarSimHost : IDisposable
|
|
{
|
|
private readonly object _syncRoot = new object();
|
|
private readonly List<MagCarSimVehicle> _vehicles = new List<MagCarSimVehicle>();
|
|
private Timer _timer;
|
|
private SimpleLiteMap _map;
|
|
private int _tickBusy;
|
|
|
|
public MagCarSimHost(MagCarSimConfig config)
|
|
{
|
|
Config = config ?? throw new ArgumentNullException(nameof(config));
|
|
}
|
|
|
|
public MagCarSimConfig Config { get; }
|
|
public SimpleLiteMap Map => _map;
|
|
public bool IsListening { get; private set; }
|
|
public bool IsLooping { get; private set; }
|
|
|
|
public IReadOnlyList<MagCarSimVehicle> Vehicles
|
|
{
|
|
get
|
|
{
|
|
lock (_syncRoot)
|
|
{
|
|
return _vehicles.ToList();
|
|
}
|
|
}
|
|
}
|
|
|
|
public SimpleLiteMap LoadMap(string path)
|
|
{
|
|
var map = SimpleLiteMap.Load(path, Config.UseTagValueAsNode);
|
|
var controlPath = MagControlAreaFile.GuessPath(map.FilePath, Config.ControlAreasPath);
|
|
var controlSites = MagControlAreaFile.LoadSiteIds(controlPath);
|
|
map.AddSingleOccupancySites(controlSites);
|
|
lock (_syncRoot)
|
|
{
|
|
_map = map;
|
|
Config.MapPath = map.FilePath;
|
|
if (!string.IsNullOrWhiteSpace(controlPath))
|
|
{
|
|
Config.ControlAreasPath = controlPath;
|
|
}
|
|
|
|
foreach (var vehicle in _vehicles)
|
|
{
|
|
vehicle.AttachMap(map);
|
|
}
|
|
}
|
|
|
|
var exclusive = map.ListSingleOccupancySiteIds();
|
|
MagCarSimLog.WriteLine(
|
|
$"[{DateTime.Now:HH:mm:ss.fff}] 已加载地图 {map.FileName},站点 {map.Sites.Count},路径 {map.Tracks.Count},停止/交管点 {(exclusive.Count == 0 ? "无" : string.Join(",", exclusive))}");
|
|
return map;
|
|
}
|
|
|
|
public void RebuildVehicles(IEnumerable<MagCarSimVehicleConfig> configs)
|
|
{
|
|
if (IsListening)
|
|
{
|
|
throw new InvalidOperationException("请先停止模拟再改车辆列表");
|
|
}
|
|
|
|
lock (_syncRoot)
|
|
{
|
|
DisposeVehiclesUnlocked();
|
|
foreach (var config in configs ?? Array.Empty<MagCarSimVehicleConfig>())
|
|
{
|
|
if (config == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var vehicle = new MagCarSimVehicle(Clone(config), Config);
|
|
if (_map != null)
|
|
{
|
|
vehicle.AttachMap(_map);
|
|
}
|
|
|
|
_vehicles.Add(vehicle);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void StartListen()
|
|
{
|
|
List<MagCarSimVehicle> vehicles;
|
|
SimpleLiteMap map;
|
|
lock (_syncRoot)
|
|
{
|
|
if (IsListening)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_map == null)
|
|
{
|
|
throw new InvalidOperationException("请先加载 SimpleLite 地图");
|
|
}
|
|
|
|
if (_vehicles.Count == 0)
|
|
{
|
|
throw new InvalidOperationException("没有可启动的车辆");
|
|
}
|
|
|
|
EnsureUniquePortsUnlocked();
|
|
vehicles = _vehicles.ToList();
|
|
map = _map;
|
|
EnsureTimerUnlocked();
|
|
IsListening = true;
|
|
}
|
|
|
|
foreach (var vehicle in vehicles)
|
|
{
|
|
vehicle.AttachMap(map);
|
|
vehicle.StartListen();
|
|
}
|
|
}
|
|
|
|
public void StartLoops()
|
|
{
|
|
if (!IsListening)
|
|
{
|
|
StartListen();
|
|
}
|
|
|
|
List<MagCarSimVehicle> vehicles;
|
|
lock (_syncRoot)
|
|
{
|
|
vehicles = _vehicles.ToList();
|
|
IsLooping = true;
|
|
}
|
|
|
|
var now = DateTime.Now;
|
|
foreach (var group in vehicles.GroupBy(RingKey).OrderBy(g => g.Key))
|
|
{
|
|
var members = group.OrderBy(v => v.VehicleCode).ToList();
|
|
var dispatchAt = now;
|
|
foreach (var vehicle in members)
|
|
{
|
|
vehicle.StartLoop(dispatchAt);
|
|
dispatchAt = dispatchAt.AddMilliseconds(StaggerMs(vehicle));
|
|
}
|
|
|
|
MagCarSimLog.WriteLine(
|
|
$"[{DateTime.Now:HH:mm:ss.fff}] 同环依次发车 {members[0].Name} 等 {members.Count} 台,间隔 {StaggerMs(members[0])}ms");
|
|
}
|
|
}
|
|
|
|
public void StopAll()
|
|
{
|
|
Timer timer;
|
|
List<MagCarSimVehicle> vehicles;
|
|
lock (_syncRoot)
|
|
{
|
|
timer = _timer;
|
|
_timer = null;
|
|
IsListening = false;
|
|
IsLooping = false;
|
|
vehicles = _vehicles.ToList();
|
|
}
|
|
|
|
try
|
|
{
|
|
timer?.Dispose();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
foreach (var vehicle in vehicles)
|
|
{
|
|
vehicle.StopLoop();
|
|
vehicle.StopListen();
|
|
}
|
|
}
|
|
|
|
public bool TryRelease(ushort vehicleCode)
|
|
{
|
|
lock (_syncRoot)
|
|
{
|
|
var vehicle = _vehicles.FirstOrDefault(v => v.VehicleCode == vehicleCode);
|
|
if (vehicle == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
vehicle.Release();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public List<MagCarSimVehicleSnapshot> Snapshots()
|
|
{
|
|
lock (_syncRoot)
|
|
{
|
|
return _vehicles.Select(v => v.Snapshot()).ToList();
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
StopAll();
|
|
lock (_syncRoot)
|
|
{
|
|
DisposeVehiclesUnlocked();
|
|
}
|
|
}
|
|
|
|
private void EnsureTimerUnlocked()
|
|
{
|
|
_timer ??= new Timer(_ => OnTick(), null, 50, 50);
|
|
}
|
|
|
|
private void OnTick()
|
|
{
|
|
if (Interlocked.Exchange(ref _tickBusy, 1) != 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
List<MagCarSimVehicle> vehicles;
|
|
SimpleLiteMap map;
|
|
lock (_syncRoot)
|
|
{
|
|
vehicles = _vehicles.ToList();
|
|
map = _map;
|
|
}
|
|
|
|
var occupancy = SnapshotExclusiveOccupancy(map, vehicles);
|
|
var now = DateTime.Now;
|
|
foreach (var vehicle in vehicles)
|
|
{
|
|
vehicle.Tick(now, occupancy);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
Interlocked.Exchange(ref _tickBusy, 0);
|
|
}
|
|
}
|
|
|
|
private void EnsureUniquePortsUnlocked()
|
|
{
|
|
var seen = new HashSet<int>();
|
|
foreach (var vehicle in _vehicles)
|
|
{
|
|
if (!seen.Add(vehicle.ListenPort))
|
|
{
|
|
throw new InvalidOperationException($"端口 {vehicle.ListenPort} 被多辆车占用");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static Dictionary<int, ushort> SnapshotExclusiveOccupancy(
|
|
SimpleLiteMap map,
|
|
IEnumerable<MagCarSimVehicle> vehicles)
|
|
{
|
|
var occupancy = new Dictionary<int, ushort>();
|
|
if (map == null)
|
|
{
|
|
return occupancy;
|
|
}
|
|
|
|
foreach (var vehicle in vehicles)
|
|
{
|
|
if (vehicle.CurrentSiteId <= 0 ||
|
|
!map.SingleOccupancy(vehicle.CurrentSiteId) ||
|
|
occupancy.ContainsKey(vehicle.CurrentSiteId))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
occupancy[vehicle.CurrentSiteId] = vehicle.VehicleCode;
|
|
}
|
|
|
|
return occupancy;
|
|
}
|
|
|
|
private int StaggerMs(MagCarSimVehicle vehicle)
|
|
{
|
|
if (Config.LaunchStaggerMs > 0)
|
|
{
|
|
return Math.Max(200, Config.LaunchStaggerMs);
|
|
}
|
|
|
|
var interval = vehicle.Config.IntervalMs > 0 ? vehicle.Config.IntervalMs : Config.DefaultIntervalMs;
|
|
return Math.Max(200, interval);
|
|
}
|
|
|
|
private static string RingKey(MagCarSimVehicle vehicle)
|
|
{
|
|
var ids = vehicle.Config.LoopSiteIds;
|
|
if (ids != null && ids.Count > 0)
|
|
{
|
|
return string.Join("-", ids);
|
|
}
|
|
|
|
return $"s{vehicle.Config.StartSiteId}-e{vehicle.Config.EndSiteId}";
|
|
}
|
|
|
|
private void DisposeVehiclesUnlocked()
|
|
{
|
|
foreach (var vehicle in _vehicles)
|
|
{
|
|
vehicle.Dispose();
|
|
}
|
|
|
|
_vehicles.Clear();
|
|
}
|
|
|
|
private static MagCarSimVehicleConfig Clone(MagCarSimVehicleConfig source)
|
|
{
|
|
return new MagCarSimVehicleConfig
|
|
{
|
|
Name = source.Name,
|
|
VehicleCode = source.VehicleCode,
|
|
ListenPort = source.ListenPort,
|
|
StartSiteId = source.StartSiteId,
|
|
EndSiteId = source.EndSiteId,
|
|
IntervalMs = source.IntervalMs,
|
|
LoopSiteIds = source.LoopSiteIds == null ? null : new List<int>(source.LoopSiteIds)
|
|
};
|
|
}
|
|
}
|
|
}
|