Skip to content

Smart FDC 战术层增强:任务优先级、移动目标提前量、炮击顺序规划、装填恢复、唯一任务序号 - #31

Open
StevenLi-phoenix wants to merge 4 commits into
HisenWeb:masterfrom
StevenLi-phoenix:cleanhouse
Open

Smart FDC 战术层增强:任务优先级、移动目标提前量、炮击顺序规划、装填恢复、唯一任务序号#31
StevenLi-phoenix wants to merge 4 commits into
HisenWeb:masterfrom
StevenLi-phoenix:cleanhouse

Conversation

@StevenLi-phoenix

Copy link
Copy Markdown
Contributor

为 Smart FDC 补齐一组"战术层"增强,全部在 IronNestFCS.Logic 内、不改既有公开 API。这套功能已随一个 LLM 火控指挥官项目实战通关完整战役(演示:YouTube / Bilibili),随后按规格做了一次 clean-room 重写,即本 PR。

功能

  • 任务优先级体系:ArtilleryTask.priority(0–100,默认 50)。高优先级任务赢下配位、同批开火顺序、跨批次执行顺序;≥90 视为紧急——跳过凑对窗口,且可抢占一门"装载已匹配"的忙炮(被抢占任务无损退回队列)。
  • 炮击顺序规划:每个规划轮对等待队列做序列优化——优先级带为硬外层顺序,带内以 max(Δ方位/4°s, Δ俯仰/2°s) 的换位耗时做 Held-Karp 精确求解(≤10 任务,超出退化贪心),队列本体重排,HUD 直接显示计划炮击顺序。
  • 移动目标:任务可携带线性运动模型(实体跟踪自动拟合,或外部转录);规划期与执行期(装填后 pre-aim、扳机前 pre-fire、人工击发等待期每 3s)按"装药感知飞行时间"(实测 TTI 表,C1–C6 1.43–4.76 s/km)重解提前量。修掉了平均弹速假设造成的"炮弹落在移动目标身后"系统误差。
  • 晚绑定射击诸元:瞄点在入队时固化为地图局部坐标,每轮从玩家炮塔棋子("Player Turret Piece")的实时位置重推方位/距离——排队期间重校炮位自动纠正所有待办任务。
  • 解析仰角解:实测证明游戏弹道是线性的(仰角 = 距离km × 12 / 装药,60° 封顶,52 组台解验证残差 ±0.01°);重解仰角走解析式,零耗时不占弹道台,物理台只做兜底。
  • 装填失败恢复:装药 commit 不符时不再直接判败——射程仍够就按实际装药打;不够则自动排一发同方位缩程清膛弹再重装(膛内弹只能打出去是游戏机制);供药机瞬断做有界重试。
  • 任务唯一序号 #N:取代可回收的地图标记号,取消/改瞄/日志/HUD 全部以 serial 寻址;左右炮位在 HUD 以固定槽位 T9/T10 标示。
  • 排队时效:validForSeconds>0 的任务超窗自动撤销(仅限仍在等待队列的)。
  • 外部接入点(供其它 mod 反射调用,不依赖则零影响):AdjustTaskAim 非阻塞改瞄、CancelPendingTaskRequestConsoleCard 打孔卡代购队列(方位/距离/起始网格拨盘自动化)。
  • 优先级协程锁:三台共享操作台的等待队列按优先级放行(同级 FIFO),人工等待期的实时重调永远让路给新任务规划。
  • 若干内务:购买流程去重、事件驱动买卡排空取代 1s 轮询、规划轮左右侧规则表驱动。

质量与兼容

  • 与 baseline 行为兼容:未改任何既有公开成员;新增字段/方法均为增量。UI 文本走既有 FcsLocalization 双语通道。
  • 开发方法:先把全部行为提炼成规格(仓内 REQUIREMENTS.md,约 1800 行,经两轮共 135 条对抗性审计),再由未接触旧代码的实现者按规格重写,最后逐子系统对照实战版本做行为回归验证。构建 0 error / 0 warning(nullable 全开)。
  • 全部源文件 UTF-8 with BOM(规避中文 locale 下 csc 按 ANSI 误读字面量的坑)。

如希望拆分成更小的 PR(比如只要优先级/移动目标/恢复中的某几项),我可以按子系统拆——各功能在提交上是可分的。

🤖 Generated with Claude Code

StevenLi-phoenix and others added 3 commits August 26, 2026 02:28
Seven adversarial auditors compared v1 against the legacy diff; all
findings folded in. Three declared deviations from legacy behavior:
UTF-8 BOM discipline (no mojibake), cancellations now recorded in
RecentTasks, and the reflection-contract note on CoroutineLock.Acquire
overload ambiguity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sequencing/log-order/threshold precision from the second adversarial
pass, the ResolveElevation liveness exception, the FcsModule TaskContext
carve-out, and the DisplayName()/ProgressText HUD contracts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Seven Opus implementers rebuilt the enhancements from REQUIREMENTS.md
alone (no access to the legacy diff); UTF-8 BOM discipline applied
repo-wide; build clean at 0 errors / 0 warnings in Logic.

Adversarial verification (7 comparators + reflection-contract check
against the legacy behavior) found 4 low regressions - all fixed - and
12 accepted deviations, archived in REQUIREMENTS.md Appendix D.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 469c11a997

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +274 to +277
// Take the gun slot first and leave the queue only after that succeeded: an external reader must
// never observe a frame in which the task is neither pending nor on a gun.
if (!RemovePendingTask(task))
MelonLogger.Warning($"[FCS Dispatch] admitted T{task.targetId} was no longer present in pending queue");
MelonLogger.Warning($"[FCS Dispatch] admitted #{task.serial} was no longer present in pending queue");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Revalidate tasks before admitting materialized plans

When CancelPendingBySerial or SweepExpiredTasks removes a task while this planning coroutine is suspended in MaterializeCandidate, the stale assignment still reaches AddPlan; this failed RemovePendingTask is only logged after the gun slot has already been assigned. Consequently, a task that was reported as cancelled/expired and recorded in RecentTasks can become active and eventually fire. Confirm the task is still pending and not failed before creating/admitting its plan, and discard or restart stale materialization otherwise.

Useful? React with 👍 / 👎.

Comment on lines +722 to +725
if (!ReferenceEquals(_current, plan) || !IsActive(plan) || plan.Failed)
yield break;
if (_fcs.Turret.LastRotationSucceeded)
appliedAzimuth = plan.Task.angel;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reconcile the armed follower after an azimuth re-lay

When a same-bearing follower has already been armed during a manual fire wait and the current moving target then triggers this re-lay, the shared turret rotates but the follower remains armed. Pulling the shared trigger can therefore fire the follower at the current task's new bearing rather than its own target. After a successful re-lay, re-evaluate the follower against the effective bearing and clear its ready/armed state if it is now outside SameAzimuthToleranceDegrees.

Useful? React with 👍 / 👎.

Comment on lines +113 to +115
result => {
LastCardRequestResult = $"{request.CardId}: {result} @{FcsRuntimeClock.Now:F0}";
MelonLogger.Msg($"[FCS] console card request {request.CardId} -> {result}");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make every card result token unique

If two identical card requests fail on an immediate path such as card not found or card has no DraggableItem, both callbacks can run within the same rounded second and produce exactly the same LastCardRequestResult. The external poller detects completions only by string inequality, so the later request then has no observable result and its caller can wait indefinitely. Include a monotonically increasing request/completion identifier rather than relying only on F0 time.

Useful? React with 👍 / 👎.

Comment on lines +139 to +151
var requiredCharge = BallisticCalculator.MinimumCharge(urgent.distance);
FirePlan? victim = null;

// Left before right, replacing only on a strictly lower priority, so equal priorities pick the left gun.
foreach (var plan in new[] { _leftPlan, _rightPlan })
{
if (plan == null
|| plan.Task.priority >= urgent.priority
|| ReferenceEquals(_current, plan)
|| ReferenceEquals(_fireWaitOwner, plan)
|| plan.ShotObserved
|| plan.Shell != urgent.bulletType
|| plan.Charge < requiredCharge)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh an urgent target before choosing a transferable charge

EnqueueTask attempts urgent preemption before the planning round calls RefreshPendingSolutions, so urgent.distance can still describe the unled target or an aim computed from an old firing origin. If motion lead or origin re-survey moves the target across a 5 km charge boundary, this check can preempt a plan whose committed charge becomes insufficient as soon as planning refreshes the urgent task; the victim is disrupted while the urgent task remains pending instead of taking the freed gun. Evaluate the urgent task's late-bound solution before calculating requiredCharge, or otherwise use a charge requirement that accounts for that pending refresh.

Useful? React with 👍 / 👎.

…ult uniqueness, stale preemption solution

- Discard materialized plans whose task was cancelled/expired mid-round
  (and skip them in the pending-reset sweep - a second resurrection hole
  found while fixing the first).
- Disarm a same-bearing armed follower when a manual-wait re-lay moves
  the shared turret outside SameAzimuthToleranceDegrees; new
  TriggerConsole.Disarm(side) primitive, follower task unaffected.
- LastCardRequestResult gains a monotonic completion counter so
  identical same-second results stay distinguishable to pollers.
- TryPreemptForUrgent refreshes the urgent task's late-bound solution
  before computing the transferable-charge requirement.

Spec sections (16, 8.5, 15.2/17.13, 5.5, appendix C) updated in step.
Verified via an equivalent out-of-repo compile (0 errors) - the local
game install was removed after archival, taking the Il2Cpp reference
assemblies with it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@StevenLi-phoenix

Copy link
Copy Markdown
Contributor Author

Codex 的四条意见全部采纳,已在 e0f1fd2 修复:

  • P1 物化竞态:任务在规划协程物化期间被取消/过期(progress=Failed)时,该 assignment 直接丢弃、不再占炮位;顺带发现并修掉了第二个洞——规划轮末尾的"未选中任务复位 Pending"扫描会把刚取消的任务复活成无队列归属的幽灵任务,同样加了 Failed 跳过。
  • P1 已武装 follower:人工等待重调成功转炮后,若已武装的同方位 follower 与新方位差超出 SameAzimuthToleranceDegrees,自动解除其武装(新增 TriggerConsole.Disarm(side) 单侧原语,pose-verified);follower 任务不判失败、不释放炮位,轮到它时按正常流程重新武装。
  • P2 结果串唯一性:LastCardRequestResult 追加单调完成序号({CardId}: {result} @{t:F0} #{n}),计数器跨重绑不复位,同秒同结果对轮询方保持可分辨。
  • P2 抢占陈旧解算:TryPreemptForUrgent 在计算可转移装药需求前,先对紧急任务做一次与规划轮相同的晚绑定刷新(运动模型 + 诸元重推),消除跨 5km 装药档的误抢占。

REQUIREMENTS.md 相应章节已同步。

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0f1fd2f03

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@@ -540,6 +888,79 @@ private bool CanFollowerArm(FirePlan current, FirePlan follower, out float azimu
return azimuthDelta <= SameAzimuthToleranceDegrees;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Compare followers against the effective bearing

When either task's bearing changes during the new execution-stage tracking refresh, these immutable FirePlan.Azimuth values remain at their planning-time bearings. In particular, the current plan can complete its pre-fire azimuth correction before opening the wait, after which this check still approves a follower whose original bearing matched even though the turret now points elsewhere; pulling the shared trigger then fires that follower off-target. Use the current turret/applied bearing and the follower's refreshed task bearing for this authorization.

Useful? React with 👍 / 👎.

task.motionVelLocalPerSec = task.hasMotion
? Vector3.Lerp(task.motionVelLocalPerSec, sampled, VelocityLowPassFactor)
: sampled;
_entitySamples[task.trackEntityId] = (local, now);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Share the sampled velocity between tasks

When multiple pending tasks track the same entity ID, the first task processed updates this shared sample timestamp, so every following task in the same refresh sees dt < 0.5s and leaves its own motionVelLocalPerSec unchanged. With the stable queue iteration order, a later task can remain at zero velocity through every refresh and be planned or armed without lead while the first task tracks correctly. Cache the fitted velocity with the entity sample or sample each entity once and propagate the model to all matching tasks.

Useful? React with 👍 / 👎.


if (preFireSolve.Ok)
{
appliedDistance = plan.Task.distance;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Advance the range baseline only after elevation succeeds

If the pre-fire elevation command times out or otherwise leaves LastElevationSucceeded false, this assignment still records the refreshed range as applied. For an unchanged tracked range or a static agent re-aim, the manual-wait condition subsequently sees no range delta and never retries, while the trigger path proceeds with the old elevation (including immediate AutoFire). Move the baseline update after successful SetElevation; the manual-relay path has the same premature update pattern.

Useful? React with 👍 / 👎.

if (plan == null
|| plan.Task.priority >= urgent.priority
|| ReferenceEquals(_current, plan)
|| ReferenceEquals(_fireWaitOwner, plan)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude the armed follower from urgent preemption

When a same-bearing follower has armed itself during the current manual fire wait, it is neither _current nor _fireWaitOwner, so an arriving higher-priority task can select it as the victim here. ReleaseGunSlot clears only the logical ready state and never disarms the physical safety; the commander's subsequent shared-trigger pull can therefore fire the preempted round after its FirePlan has been discarded, leaving an uncontrolled shot with no task to settle. Treat _armedFollower as non-preemptable, or synchronously disarm it before releasing the slot.

Useful? React with 👍 / 👎.

_fcs.MapTable.ApplyMotionModel(urgent);
_fcs.MapTable.RefreshSolution(urgent);

var requiredCharge = BallisticCalculator.MinimumCharge(urgent.distance);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject unreachable urgent tasks before preempting

For a refreshed urgent range above the maximum C6 reach of 30 km, MinimumCharge saturates at 6 rather than indicating failure, so this method can evict a lower-priority C6 plan. The next planning round then rejects the urgent task at the explicit task.distance > charge * 5 eligibility check, meaning the victim was disrupted without creating any usable firing opportunity. Check the maximum supported range before selecting or tearing down a victim.

Useful? React with 👍 / 👎.

continue;
}

var plan = _fcs.Planner.CreatePlan(assignment.Planning, item.Candidate, commitAt);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Revalidate re-aimed tasks before admitting their plans

A pending task remains adjustable while MaterializeCandidate yields on the physical calculator, but this admission path verifies only cancellation/expiry. If the commander moves the aim across a charge boundary during that window, the candidate still carries the old charge and elevation while CreatePlan reads the task's new bearing and range; the now-insufficient charge can be physically committed, and both execution-stage elevation solves then fail without preventing the trigger path from firing the stale solution. Detect solution changes after materialization and rematch or rematerialize before admission.

Useful? React with 👍 / 👎.

Comment on lines +84 to +85
if (task.serial == 0)
task.serial = ++_serialCounter;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reserve externally supplied serials in the counter

When a supported external caller pre-sets a positive serial, this branch preserves it but does not advance or otherwise reserve _serialCounter. For example, enqueueing external serial = 1 followed by an ordinary zero-serial task assigns #1 to both, after which cancellation and re-aim operations select whichever duplicate they encounter first and recent outcomes cannot distinguish the missions. Preserve the supplied value while also advancing the counter or rejecting an already-used serial.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant