128 lines
4.2 KiB
C#
128 lines
4.2 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace StandardScene.Magnetic.Tasking
|
|
{
|
|
/// <summary>
|
|
/// 断线重连后,用当前占点匹配 tasklist 链路的结果。
|
|
/// </summary>
|
|
public sealed class Fass2ChainMatch
|
|
{
|
|
public int TaskId { get; set; }
|
|
public int TaskPriority { get; set; }
|
|
public int CurrentSiteId { get; set; }
|
|
public int TargetSiteId { get; set; }
|
|
public int IndexInPath { get; set; }
|
|
public int DistanceToTarget { get; set; }
|
|
public bool IsAtStartPoint { get; set; }
|
|
public bool IsAtEndPoint { get; set; }
|
|
public IReadOnlyList<int> FullPath { get; set; } = new List<int>();
|
|
public IReadOnlyList<int> RemainingPath { get; set; } = new List<int>();
|
|
|
|
public override string ToString()
|
|
{
|
|
var position = IsAtStartPoint ? "start" : (IsAtEndPoint ? "end" : "mid");
|
|
return $"task={TaskId} {CurrentSiteId}->{TargetSiteId} pos={position} remain={DistanceToTarget} prio={TaskPriority}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 一站多链路时的固定裁决:原 goal 仍在剩余路径 → 起点 → 优先级 → 距目标更近。
|
|
/// </summary>
|
|
public static class Fass2ReconnectChainSelector
|
|
{
|
|
public static bool TrySelect(
|
|
IReadOnlyList<Fass2ChainMatch> matches,
|
|
int? preferGoalSiteId,
|
|
out Fass2ChainMatch selected,
|
|
out string reason)
|
|
{
|
|
selected = null;
|
|
reason = "no chain";
|
|
if (matches == null || matches.Count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var candidates = matches.Where(m => m != null && m.TargetSiteId > 0).ToList();
|
|
if (candidates.Count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (preferGoalSiteId != null && preferGoalSiteId.Value > 0)
|
|
{
|
|
var keepGoal = candidates
|
|
.Where(m => RemainingContains(m, preferGoalSiteId.Value))
|
|
.ToList();
|
|
if (keepGoal.Count == 1)
|
|
{
|
|
selected = keepGoal[0];
|
|
reason = $"preferGoal={preferGoalSiteId.Value}";
|
|
return true;
|
|
}
|
|
|
|
if (keepGoal.Count > 1)
|
|
{
|
|
candidates = keepGoal;
|
|
reason = $"preferGoal={preferGoalSiteId.Value} narrowed={keepGoal.Count}";
|
|
}
|
|
}
|
|
|
|
var ranked = candidates
|
|
.OrderByDescending(m => m.IsAtStartPoint ? 1 : 0)
|
|
.ThenByDescending(m => m.TaskPriority)
|
|
.ThenBy(m => m.DistanceToTarget)
|
|
.ThenBy(m => m.TaskId)
|
|
.ToList();
|
|
|
|
if (ranked.Count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var best = ranked[0];
|
|
if (ranked.Count > 1)
|
|
{
|
|
var second = ranked[1];
|
|
var tied = best.IsAtStartPoint == second.IsAtStartPoint &&
|
|
best.TaskPriority == second.TaskPriority &&
|
|
best.DistanceToTarget == second.DistanceToTarget;
|
|
if (tied && best.TargetSiteId != second.TargetSiteId)
|
|
{
|
|
reason = $"ambiguous {best} vs {second}";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
selected = best;
|
|
if (string.IsNullOrEmpty(reason) || reason == "no chain")
|
|
{
|
|
reason = $"selected {best}";
|
|
}
|
|
else
|
|
{
|
|
reason = $"{reason}; selected {best}";
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool RemainingContains(Fass2ChainMatch match, int siteId)
|
|
{
|
|
if (match.RemainingPath != null)
|
|
{
|
|
for (var i = 0; i < match.RemainingPath.Count; i++)
|
|
{
|
|
if (match.RemainingPath[i] == siteId)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return match.TargetSiteId == siteId;
|
|
}
|
|
}
|
|
}
|