using SimpleCore; using SimpleCore.Compiler; using SimpleCore.PropType; using SimpleCore.Traffic; using System; using System.Collections.Generic; using System.Linq; namespace StandardScene.Magnetic.Tasking { /// /// 方案 B′:用任务下发的 siteIds 经 SegmentPlan.Forecast 建立 seqScope/pendingLocks,再段级 TryLock/Leave。 /// internal sealed class Fass2TrafficLocker { private readonly AbstractCar _car; private readonly Action _log; public Fass2TrafficLocker(AbstractCar car, Action log) { _car = car ?? throw new ArgumentNullException(nameof(car)); _log = log ?? (_ => { }); } public void PrepareSequence(IReadOnlyList siteIds, int fromIndex) { if (siteIds == null || siteIds.Count == 0) { throw new InvalidOperationException("traffic plan siteIds is empty"); } fromIndex = Math.Max(0, Math.Min(fromIndex, siteIds.Count - 1)); var startSiteId = siteIds[fromIndex]; var startSite = SimpleLib.GetSite(startSiteId); if (startSite == null) { throw new InvalidOperationException($"traffic start site invalid: {startSiteId}"); } // TrafficReset 会 programs.Clear() 把 now 置空;TryLock 在真正占点前要求 programs.now != null, // 否则抛 Program obsoleted。环线拦截路径仍处在 move 脚本的 actualSendScript 中,需保留/重建锚点。 var keepProgram = _car.status.programs.now; _log($"prepare reset start={startSiteId}, goal={siteIds[siteIds.Count - 1]}, fromIndex={fromIndex}, route=[{string.Join(",", siteIds)}]"); try { _car.TrafficReset(startSite, makeAvailable: true, strict: false); } catch (Exception ex) when (IsStartOccupiedByOtherCar(ex)) { // 起点被他车占用时不能 Reset;若本车已持有该站则跳过 Reset 继续建序,否则交给上层等待。 if (Array.IndexOf(_car.status.holdingLocks, startSiteId) < 0) { throw new InvalidOperationException( $"traffic start site {startSiteId} occupied by another car", ex); } _log($"prepare reset skipped, already holding start={startSiteId}, ex={ex.Message}"); } EnsureProgramAnchor(keepProgram); if (fromIndex >= siteIds.Count - 1) { LogTrafficState("prepare single-site"); return; } Fass2RouteHelper.ForecastTrafficSequence(_car, siteIds, fromIndex); LogTrafficState("prepare forecast ok"); } public void RebaseFrom(IReadOnlyList siteIds, int fromIndex, int windowSize) { PrepareSequence(siteIds, fromIndex); EnsureWindowLocked(siteIds, fromIndex, Math.Max(1, windowSize)); } private static bool IsStartOccupiedByOtherCar(Exception ex) { for (var cur = ex; cur != null; cur = cur.InnerException) { var msg = cur.Message ?? string.Empty; if (msg.IndexOf("already locks it", StringComparison.OrdinalIgnoreCase) >= 0 || msg.IndexOf("unavailable site", StringComparison.OrdinalIgnoreCase) >= 0) { return true; } } return false; } public bool TryLockNext(int siteId) { if (!_car.status.usage.Get().scheduling) { throw new InvalidOperationException("abandoned"); } if (Array.IndexOf(_car.status.holdingLocks, siteId) >= 0) { return true; } if (_car.status.pendingLocks.Length > 0 && _car.status.pendingLocks[0] != siteId) { // 窗口重入时可能再次请求已锁过的更早站点;交给 EnsureWindowLocked 跳过,避免 UDP 线程抛异常。 _log($"lock skip site={siteId}, expect pending0={_car.status.pendingLocks[0]}, holding=[{FormatInts(_car.status.holdingLocks)}]"); return false; } EnsureProgramAnchor(keepProgram: null); if (TrafficControl.TryLock(_car, siteId)) { _log($"lock ok site={siteId}, holding=[{FormatInts(_car.status.holdingLocks)}]"); return true; } _log($"lock wait site={siteId}, blockedBy={FormatBlocked()}, TCStat={FormatTcStat()}"); return false; } public void LeavePassed(int siteId) { if (_car.status.holdingLocks.Length <= 1) { _log($"leave skip site={siteId}, holding count={_car.status.holdingLocks.Length}"); return; } if (Array.IndexOf(_car.status.holdingLocks, siteId) < 0) { _log($"leave skip site={siteId}, not in holding"); return; } EnsureProgramAnchor(keepProgram: null); TrafficControl.Leave(_car, siteId); _log($"leave site={siteId}, holding=[{FormatInts(_car.status.holdingLocks)}]"); } /// /// 释放路径上严格位于 keepFromIndex 之前的持锁站点(当前站及前方窗口保留)。 /// public void ReleaseLocksBehind(IReadOnlyList siteIds, int keepFromIndex) { if (siteIds == null || siteIds.Count == 0 || _car.status.holdingLocks.Length <= 1) { return; } keepFromIndex = Math.Max(0, Math.Min(keepFromIndex, siteIds.Count - 1)); var holdingSnapshot = _car.status.holdingLocks.ToArray(); foreach (var siteId in holdingSnapshot) { if (_car.status.holdingLocks.Length <= 1) { break; } var index = IndexOfSite(siteIds, siteId); if (index < 0 || index >= keepFromIndex) { continue; } LeavePassed(siteId); } } private static int IndexOfSite(IReadOnlyList siteIds, int siteId) { for (var i = 0; i < siteIds.Count; i++) { if (siteIds[i] == siteId) { return i; } } return -1; } public int EnsureWindowLocked(IReadOnlyList siteIds, int fromIndex, int windowSize) { if (siteIds == null || siteIds.Count == 0) { return 0; } fromIndex = Math.Max(0, Math.Min(fromIndex, siteIds.Count - 1)); windowSize = Math.Max(1, Math.Min(windowSize, 10)); var maxIndex = Math.Min(siteIds.Count - 1, fromIndex + windowSize - 1); // 返回值必须是从 fromIndex 起的连续已占/新锁站数,供 0xB1 窗口切片使用。 // UDP Tick 会反复进入:已在 holding 的站直接计数,禁止再次对已消费的 pending 站 TryLock。 var lockedCount = 0; for (var i = fromIndex; i <= maxIndex; i++) { var siteId = siteIds[i]; if (Array.IndexOf(_car.status.holdingLocks, siteId) >= 0) { lockedCount++; continue; } if (_car.status.pendingLocks.Length == 0 || _car.status.pendingLocks[0] != siteId) { _log( $"window gap site={siteId}, pending0={(_car.status.pendingLocks.Length > 0 ? _car.status.pendingLocks[0].ToString() : "-")}, holding=[{FormatInts(_car.status.holdingLocks)}]"); break; } if (!TryLockNext(siteId)) { break; } lockedCount++; } if (lockedCount == 0) { lockedCount = 1; _log($"window warn: no holding overlap index={fromIndex}, site={siteIds[fromIndex]}, holding=[{FormatInts(_car.status.holdingLocks)}]"); } _log($"window locked={lockedCount}/{windowSize}, fromIndex={fromIndex}, holding=[{FormatInts(_car.status.holdingLocks)}], pending=[{FormatInts(_car.status.pendingLocks)}]"); return lockedCount; } public void FinalizeAtSite(int siteId) { _log($"finalize site={siteId}, holding=[{FormatInts(_car.status.holdingLocks)}], pending=[{FormatInts(_car.status.pendingLocks)}]"); } /// /// SimpleCore.TryLock 在写锁前检查 programs.now;为空则抛 Program obsoleted。 /// private void EnsureProgramAnchor(CarProgram keepProgram) { if (_car.status.programs.now != null) { return; } if (keepProgram != null) { _car.status.programs.now = keepProgram; _log($"restore programs.now after TrafficReset, name={keepProgram.name}, state={keepProgram.status.state}"); return; } var dummy = new CarProgram { name = $"fass2-traffic-anchor:{_car.id}", plans = new[] { new SegmentPlan { usingCar = _car } } }; dummy.status.state = CarProgram.StatusEnum.Dummy; _car.status.programs.now = dummy; _log($"install dummy programs.now for TryLock, name={dummy.name}"); } private void LogTrafficState(string prefix) { _log( $"{prefix}, holding=[{FormatInts(_car.status.holdingLocks)}], pending=[{FormatInts(_car.status.pendingLocks)}], seqScope=[{FormatInts(_car.status.seqScope)}], seqPtr={_car.status.seqPtr}"); } private string FormatBlocked() { if (_car.status.blockedBy == null || _car.status.blockedBy.Length == 0) { return "-"; } return string.Join(",", _car.status.blockedBy.Select(p => $"{p.Item1}:{p.Item2}")); } private string FormatTcStat() { try { var tc = _car.status.TCStat; return string.IsNullOrWhiteSpace(tc) ? "-" : tc; } catch { return "-"; } } private static string FormatInts(int[] values) { return values == null || values.Length == 0 ? string.Empty : string.Join(",", values); } } }