Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion IronNestFCS.Logic/ClickRaycaster.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using UnityEngine.InputSystem;
using MelonLoader;
using UnityEngine;
Expand Down
557 changes: 545 additions & 12 deletions IronNestFCS.Logic/Execution/FirePlanExecutor.cs

Large diffs are not rendered by default.

88 changes: 88 additions & 0 deletions IronNestFCS.Logic/FCS/ArtilleryTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,18 @@ public enum PendingHint {
AmmoMismatch,
}

/// <summary>
/// One fire mission. Every member below is a public field on purpose: the external bridge
/// (IronNestAgentBridge) reflects over this type with GetField and builds instances through
/// Activator.CreateInstance, so the public parameterless constructor, the member names, the
/// member kind (field, never property) and the field types are all a frozen contract.
/// A freshly constructed instance must already be safe to enqueue — in particular
/// failureReason is "" (never null) and serial stays 0 so the dispatcher can stamp it.
/// </summary>
public class ArtilleryTask {
// targetId is a recyclable map-marker id and may repeat; 0 is a legal value meaning
// "no marker, pure aim-point task" (the bridge always enqueues with targetId = 0).
// It is never a usable external handle — use serial for that.
public int targetId;
public float angel;
public float distance;
Expand All @@ -48,4 +59,81 @@ public class ArtilleryTask {
// solve the target, exclude that side and let the same task fall back to the other gun.
// Bit 0 = Left, bit 1 = Right. Reset when a brand-new target is enqueued.
public int dispatchExcludedGunMask;

// Globally unique mission number #N, stamped by the dispatcher with a zero-value sentinel
// (if (serial == 0) serial = ++counter). A non-zero value is always kept as-is, including a
// value pre-set by an external caller, and requeueing (urgent preemption, load recovery)
// never renumbers a task. Do not replace the sentinel with a private "already enqueued"
// flag: that would change the observable "external serial is honoured" behaviour.
public int serial;

// Scheduling priority 0-100, >= 90 is urgent. Set by the caller before enqueueing;
// enqueueing never resets it. Must stay a public int — the bridge's TrySetPriority uses
// default BindingFlags and therefore only sees public members.
public int priority = 50;

// Aim point in map-local units, frozen at enqueue time. Late-bound gunnery solutions
// (origin re-survey, motion lead) are only possible for a task that carries one.
public bool hasAimPoint;
public Vector3 aimLocal;

// Non-empty = FCS tracks this map entity itself and fits a motion model from the samples.
public string trackEntityId = "";

// The model has not been refreshed for over 90s and is still being extrapolated.
public bool trackingLost;

// A linear motion model exists. External callers (bridge/LLM) may set the model directly
// without asking FCS to track anything, hence this is independent of trackEntityId.
public bool hasMotion;

// p(t) = motionOriginLocal + motionVelLocalPerSec * (t - motionT0), t on the mission clock.
public Vector3 motionOriginLocal;
public Vector3 motionVelLocalPerSec;
public float motionT0;

// The agent re-aimed this task after it was enqueued. It opens the execution-time refresh
// gates for otherwise static tasks.
public bool aimAdjusted;

// Shared retry counter for both powder recovery paths (commit mismatch and dispenser stall).
public int loadRetryCount;

// Queue lifetime. firstEnqueuedAt uses the same zero-value sentinel as serial so the window
// is measured from the original command, not from the latest requeue.
public float validForSeconds;
public float firstEnqueuedAt;

/// <summary>
/// Motion status phrase shared by the HUD and external readers.
/// The branch predicate is the disjunction of hasMotion and trackEntityId, not "is there a
/// model": a tracked target that has never been sampled (always in the fog, or only one
/// sample so far) still reports as tracked, with a zero velocity vector.
/// </summary>
public string MotionSuffix(bool zh) {
if (!hasMotion && trackEntityId.Length == 0)
return aimAdjusted ? (zh ? " · 已改瞄" : " · re-aimed") : "";

// Speed deliberately uses the full Vector3 magnitude (z included): motionVelLocalPerSec is
// a frozen public field that the bridge/LLM may write with a non-zero z, and only FCS's own
// UpdateEntityMotion zeroes it. The horizontal Vector2 gauge used by ApplyMotionModel /
// ShortenedAim is NOT interchangeable here — it would change both the reported km/h and
// whether the task falls into the "static" branch below.
var speedKmh = motionVelLocalPerSec.magnitude * 3.8164f * 3600f;

// Course, on the other hand, is horizontal only.
var course = Mathf.Atan2(motionVelLocalPerSec.x, motionVelLocalPerSec.y) * Mathf.Rad2Deg;
if (course < 0f)
course += 360f;

var head = trackEntityId.Length > 0
? (zh ? $"跟踪 {trackEntityId}" : $"track {trackEntityId}")
: (zh ? "运动模型" : "motion");
var lost = trackingLost ? (zh ? "·失联外推" : "·extrapolating") : "";

if (speedKmh < 0.5f)
return zh ? $" · {head}(静止){lost}" : $" · {head}(static){lost}";

return $" · {head} {speedKmh:F0}km/h→{course:000}°{lost}";
}
}
2 changes: 1 addition & 1 deletion IronNestFCS.Logic/FCS/BallisticCalculator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections;
using System.Collections;
using Il2Cpp;
using MelonLoader;
using UnityEngine;
Expand Down
115 changes: 95 additions & 20 deletions IronNestFCS.Logic/FCS/CoroutineLock.cs
Original file line number Diff line number Diff line change
@@ -1,57 +1,132 @@
// Smart fork modifications Copyright (c) 2026 HisenWeb
// Based on IronNestFCS by svr2kos2
// SPDX-License-Identifier: MIT

using System;
using System.Collections;
using System.Collections.Generic;

namespace IronNestFCS.Logic.FCS;

/// <summary>
/// 协程级互斥锁。MelonCoroutines 全部在 Unity 主线程上协作式调度,没有真正并发,
/// 因此一个 bool 足以实现互斥——不需要任何并发原语(lock/Interlocked/SemaphoreSlim)。
/// 协程级优先级互斥锁。MelonCoroutines 全部在 Unity 主线程上协作式调度,没有真正并发,
/// 因此一个 bool 加一份等待票列表足以实现互斥——不需要任何并发原语
/// (lock/Interlocked/SemaphoreSlim)。
///
/// 等待者按 (priority 降序, 登记序号升序) 排队:高优先级先取锁,同级 FIFO。
/// 全局优先级约定:规划期台解 / 执行期征用·扳机 = 任务 priority(默认 50,紧急 ≥90);
/// 外部买卡请求 = 请求自带 priority;火药自动补货 = 20;人工击发等待期实时重调 = 10。
///
/// 用法(务必配 try/finally,保证协程被 Stop / yield break 时也能释放):
/// <code>
/// yield return deskLock.Acquire();
/// try { /* 临界区,可含 yield return */ }
/// finally { deskLock.Release(); }
/// </code>
/// 迭代器被 MelonCoroutines.Stop 停掉时会 Dispose,finally 块照常执行 → 锁不会泄漏。
/// 迭代器被 MelonCoroutines.Stop 停掉时会 Dispose,finally 块照常执行 → 锁与票都不会泄漏。
///
/// 注:四个 Acquire 重载全部保留。外部 mod 用无参 <c>GetMethod("Acquire")</c> 反射取方法时会因此
/// 抛 AmbiguousMatchException;这是已知且已裁决的契约事实,外部应改用
/// <c>GetMethod("Acquire", Type.EmptyTypes)</c> 精确取无参重载,FCS 侧不为此新增别名方法。
/// </summary>
public sealed class CoroutineLock {
/// <summary>无参 Acquire() 与无优先级的可取消重载使用的默认优先级。</summary>
private const int DefaultPriority = 50;

/// <summary>一张等待票。用引用类型是为了让 finally 里的 Remove 走引用相等,绝不误删同级同优先级的别人。</summary>
private sealed class Ticket {
public int Priority;
public int Seq;
}

private readonly List<Ticket> _waiters = new();
private bool _held;
private int _ticketSeq;

/// <summary>等待直到拿到锁。拿到后立即占用,调用方负责在 finally 里 Release。</summary>
/// <summary>当前票是否轮到自己:没有更高优先级的票,也没有同级更早登记的票。</summary>
private bool IsNext(int priority, int seq) {
for (var i = 0; i < _waiters.Count; i++) {
var waiter = _waiters[i];
if (waiter.Priority > priority)
return false;
if (waiter.Priority == priority && waiter.Seq < seq)
return false;
}

return true;
}

/// <summary>等待直到拿到锁(默认优先级 50)。拿到后立即占用,调用方负责在 finally 里 Release。</summary>
public IEnumerator Acquire() {
// 每帧重试一次。持锁方在主线程推进,释放后下一帧本协程即可抢到。
while (_held) {
yield return null;
return Acquire(DefaultPriority);
}

/// <summary>按给定优先级等待直到拿到锁。拿到后立即占用,调用方负责在 finally 里 Release。</summary>
public IEnumerator Acquire(int priority) {
var seq = _ticketSeq++;
var ticket = new Ticket { Priority = priority, Seq = seq };
_waiters.Add(ticket);
try {
// 每帧重试一次。持锁方在主线程推进,释放后下一帧轮到的协程即可抢到。
// 这里必须是 yield return null 而不是 WaitForSeconds / WaitUntilFocused:后者会给每次取锁
// 引入额外延迟,并让锁等待被暂停/失焦阻塞,破坏“人工等待期重调 P10 让路”的实时性前提。
while (_held || !IsNext(priority, seq)) {
yield return null;
}

_held = true;
}
finally {
// 协程被 Stop 时也要走到这里,否则残票会永久压住后来的同级/低优先级等待者。
_waiters.Remove(ticket);
}
_held = true;
}

/// <summary>
/// 可取消的锁等待。等待期间或真正占锁前如果 shouldCancel 返回 true,就直接退出且不会占锁;
/// 只有真正取得锁时才调用 onAcquired,调用方可据此区分“取得”与“取消”。
/// 可取消的锁等待(默认优先级 50)。等待期间或真正占锁前如果 shouldCancel 返回 true,
/// 就直接退出且不会占锁;只有真正取得锁时才调用 onAcquired,调用方可据此区分“取得”与“取消”。
/// </summary>
public IEnumerator Acquire(Func<bool> shouldCancel, Action onAcquired) {
while (_held) {
return Acquire(shouldCancel, onAcquired, DefaultPriority);
}

/// <summary>
/// 带优先级的可取消锁等待。与 <see cref="Acquire(int)"/> 完全同构:同样登记票、同样按
/// 优先级票队等待、同样在 finally 里移票。朴素的 <c>while (_held)</c> 写法会让可取消 acquire
/// 插队越过已排队的高优先级等待者,并使 priority 参数彻底失效。
/// </summary>
public IEnumerator Acquire(Func<bool> shouldCancel, Action onAcquired, int priority) {
var seq = _ticketSeq++;
var ticket = new Ticket { Priority = priority, Seq = seq };
_waiters.Add(ticket);
try {
// shouldCancel 在每次挂帧之前检查:入口即取消时不产生任何 yield。
while (_held || !IsNext(priority, seq)) {
if (shouldCancel())
yield break;
yield return null;
}

// 锁刚释放和本协程恢复之间也可能发生 F9 / 任务取消,因此占锁前再检查一次。
if (shouldCancel())
yield break;
yield return null;
}

// 锁刚释放和本协程恢复之间也可能发生 F9 / 任务取消,因此占锁前再检查一次。
if (shouldCancel())
yield break;

_held = true;
onAcquired();
_held = true;
onAcquired();
}
finally {
// 取消路径同样不得漏票。
_waiters.Remove(ticket);
}
}

public void Release() {
_held = false;
}

/// <summary>重绑定(热重载)时强制复位,防止上一轮异常残留导致死锁。</summary>
/// <summary>重绑定(热重载)时强制复位,防止上一轮异常残留的持锁标志或等待票导致死锁。</summary>
public void Reset() {
_held = false;
_waiters.Clear();
}
}
2 changes: 1 addition & 1 deletion IronNestFCS.Logic/FCS/GunPhysicalState.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Il2Cpp;
using Il2Cpp;
using UnityEngine;

namespace IronNestFCS.Logic.FCS;
Expand Down
2 changes: 1 addition & 1 deletion IronNestFCS.Logic/FCS/GunSystem.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections;
using System.Collections;
using Il2Cpp;
using Il2CppTMPro;
using MelonLoader;
Expand Down
Loading