You are viewing a potentially older version of this package. View all versions.
HawChow-EquipmentTimers-1.0.0 icon

EquipmentTimers

Countdowns left of the native GTFO equipment names: melee charge, primary/secondary reload and BioTracker recharge, with one green READY cue. Client-side, read-only, no gameplay changes.

Date uploaded 2 days ago
Version 1.0.0
Download link HawChow-EquipmentTimers-1.0.0.zip
Downloads 28
Dependency string HawChow-EquipmentTimers-1.0.0

This mod requires the following mods to function

BepInEx-BepInExPack_GTFO-3.2.3 icon
BepInEx-BepInExPack_GTFO

BepInEx pack for GTFO. Preconfigured and includes Unity Base DLLs.

Preferred version: 3.2.3

README

EquipmentTimers

Small countdowns drawn inside the native GTFO equipment list (top right), immediately to the left of each equipment name. Nothing is added to the crosshair, and there is no separate panel.

Timed item What the number means
Melee weapon Seconds until the charge-up reaches full damage (m_chargeUpDone)
Primary weapon Seconds left in the current reload
Secondary weapon Seconds left in the current reload
BioTracker / class tool Seconds until the scanner can be used again

When a timer really completes, the row turns green and shows READY for a moment, and one vanilla UI sound is played. A countdown that merely reaches zero never counts as a completion.

Feedback: qingju.me | 小黑盒语音

Features

  • Native look: same TMP font asset, same font size, same font style, same renderer sorting layer as the equipment name it sits next to.
  • Anchored to both native name states, so it follows the selected full name and the compact slim name, and re-anchors when you switch gear.
  • Follows HUD scaling, resolution changes and native slot repositioning, because the label is a sibling of the native text inside the same canvas.
  • At most four rows (primary, secondary, tool, melee), one label per slot, no duplicates.
  • Neutral colour while counting (inherits the native name colour), green only for the completion cue, so green always means "ready now".
  • Client-side and read-only: no gameplay value is written, no packet is sent, reload speed, charge speed, damage and scan cooldown are untouched.
  • Bounded logging: at most 40 lines per session by default (400 with VerboseLog), never per frame.

Installation

  1. Install BepInExPack_GTFO (this is the only dependency).
  2. Drop EquipmentTimers.dll into BepInEx/plugins/ (the package already uses BepInEx/plugins/EquipmentTimers/EquipmentTimers.dll).
  3. Start GTFO. The timer appears the first time you wield a timed item.

Configuration

BepInEx/config/HawChow.EquipmentTimers.cfg (or a config editor):

Section Key Default Meaning
General Enabled true Master switch; off hides every label immediately
General ShowMeleeCharge true Melee charge-up countdown
General ShowReload true Reload countdown
General ShowScanner true BioTracker recharge countdown
General PlayReadySound true Play the one-shot ready cue
General ReadySoundEvent IMPLANTREADY Vanilla Wwise event: IMPLANTREADY, IMPLANTREADYSMALL, HUD_SCAN_COMPLETE_INDICATION, BUTTONGENERICBLIPACTIVATED, BUTTONGENERICBLIPONE, None
General ReadyHoldSeconds 0.6 How long READY stays on screen (0.2–3 s)
General OverrunGraceSeconds 2.5 Drop a countdown silently if the native action never ends (0.5–10 s)
Layout GapRatio 0.22 Gap to the native name, as a fraction of the native font size
Layout MinGap / MaxGap 2 / 40 Clamps for that gap, in canvas units
Layout LabelWidthRatio 4 Label box width as a multiple of the font size
Layout MinLabelWidth 24 Lower clamp for that width
Layout ReadyColor #6BF25A #RRGGBB or r,g,b (0-1 or 0-255)
Layout NeutralAlpha 0.92 Opacity of the neutral countdown
Debug VerboseLog false Raise the log budget to 400 lines

Known limits and accuracy

  • GTFO exposes no reload timestamp: only IsReloading and ReloadTime. The first reload segment counts down ReloadTime from the IsReloading rising edge; each further segment is estimated from the observed interval between two inserts and is prefixed with ~ (for example ~0.5s), so an estimate can never be mistaken for the native window. The row only turns green once, on the native end of the session.
  • A reload counts as completed only when the magazine is full or the clip grew during the session and the reserve that was non-empty at the start is now empty (provable natural exhaustion). Firing mid-reload therefore never turns the row green, and an unreadable reserve is never assumed empty.
  • A countdown that reaches zero while the native action is still running hides its number but keeps watching; the real completion that lands later still turns green and beeps exactly once.
  • The native HUD rebuilds itself thousands of times per second in the main menu (measured ~3.5 kHz) and hands out HUD instances that are frequently already destroyed. The mod treats those calls as an invalidation hint only: it never caches or dereferences them, and re-acquires the HUD through a throttled probe. This was the 2026-10-25 startup-crash path; with the fix the menu stays stable for 211-217 s in a row (see the project's reports/runtime-startup-failure.md).
  • A charge or reload that started before the mod could observe it (attaching mid-level, reloading while a level loads) still shows a number but ends silently, by design: the ready cue is never played for a completion the mod did not watch from the start.
  • The tool countdown follows the scanner's own m_recharging / m_rechargeStartTime / m_rechargeDuration; modded tools that do not use those fields will not show a countdown.
  • Melee "max charge hold" time is intentionally not shown: the timer stops at full charge.

Verification status (v1.0.0)

  • Build, head-less unit tests (timer completion, cancel, repeated frames, first observation, weapon switch, shell-by-shell, empty reserve, level reset, layout maths, colour parsing, bounded log) and static source guards all pass locally against the real GTFO interop assembly set.
  • Main-menu stability is verified on the release build: two consecutive runs of this exact DLL in a 26-mod r1modman profile stayed alive for 211 s and 217 s with zero Application crash events, after the 2026-10-25 startup crash was traced to the per-frame HUD postfix path and fixed.
  • In-level behaviour is not verified yet. Pixel alignment on the native name, per-row behaviour with real weapons and the audible cue still need the acceptance run; treat the layout numbers above as the contract and report any misalignment.
  • No in-game screenshot is bundled with v1.0.0 for that reason; the icon is drawn procedurally and is not evidence of in-game rendering.

Credits and licence

  • Behaviour reference: ReloadTimerPlus by Zose and MeleeTimer by long_walter. No code or asset from ReloadTimerPlus is used (it ships without an explicit licence). MeleeTimer is MIT licensed and is credited as the source of the "remaining = AutoAttackTime − elapsed, font taken from the native HUD" idea; this mod implements its own tracker, HUD anchoring and completion policy and reads MWS_ChargeUp.m_maxDamageTime from the live charge state instead of the animation data block.
  • All names, fields and events used here are the game's own (PUI_InventoryItem, Gear.MWS_ChargeUp, ItemEquippable, Gear.EnemyScanner, AK.EVENTS); no game assembly is redistributed with this package.

CHANGELOG

更新日志

1.0.9

  • 哨戒炮工具行(TimerKind.SentryAmmo,独立行类型,因此永远不会被当成换弹):两项信息同时显示,绝不互相替代。只要可读,玩家自己的职业弹药池始终以 当前/总 显示(PlayerBackpackManager.GetBulletsInPack/GetAmmoMaxCap(AmmoType.Class, SNet_Player)),无论手持还是已部署。当该玩家的背包也报告了已放置的哨戒炮(PlayerBackpack.IsLocal、IsDeployed(GearClass)、TryGetDeployedItem(GearClass, out Item)、SentryGunInstance.m_isDeployed)时,第二个标签再以 NN% 显示该物件自身的弹药,数值来自它自己的 Ammo/AmmoMaxCap(实例值不可读时,只有数值改用同步快照 m_sync.LastSyncData.ammo),例如 40/80 与 72% 并存。容量为零或更低、负数/NaN、背包缺失或任何 getter 失败,都只隐藏百分比——只要玩家比例可读,它就继续显示。不回写任何内容:不写弹药字段、不写背包字段、不发 SNet 包。
  • 哨戒炮百分比是一条真实列,而不是猜出来的坐标:每次槽位扫描都会测量主/副武器行原生弹药总数 TMP,并把百分比左边缘经该行自己的父级投影到那个世界 X 上。测不到原生弹药文本时,百分比就不绘制。
  • 完成提示音现在可按 cue 在 BepInEx/plugins/EquipmentTimers/Assets/Audio/ready-sounds.json 中编辑:native(原有 ReadySoundEvent)、playerVoice(经 PlayerVoiceManager.WantToSay(playerID, eventID) 播放角色自己的原生台词)、wwise(白名单内的原版事件)、wav(JSON 旁的本地文件,PCM 8/16/24/32-bit 或 float32,单/双声道,≤ 30 秒且 ≤ 32 MB)或 none。随包默认:BioTracker 完成时播放当前角色的原生 PLAY_CL_SCAN 命令列表台词;武器与近战 cue 保持 1.0.8 的原生事件,因此除非玩家编辑该文件,其他行为不变。PlayReadySound 仍是总开关,ReadySoundEvent 仍是回退值;CustomReadySounds=false 会完全忽略 JSON。
  • 自定义 WAV 从磁盘解码,并在单一隔离适配层(Audio/ReadySoundPlayer.cs)中作为本地 2D 一次性音效播放;不使用 UnityWebRequest、不下载、不发自定义网络包、不修改游戏字段。绝对路径、盘符、.. 与流名一律拒绝;文件缺失或损坏时每个文件只记一条警告,并回退到原生事件。
  • 移除切装闪烁:被替换的原生槽位物件不再销毁该行的 TextMeshPro(RebindUiItem);重绑保持同一实例、同一文本,并在字体未变时保留其字形 mesh,而不是隐藏/清空;当持有的锚点不再渲染时,锚点 latch 可以立即切换(重绑不再需要重建)。背包条目或原生名称短暂缺失一两帧时,用有界宽限(0.25 秒 / 0.18 秒)桥接,之后该行照旧隐藏,因此旧物品文本绝不会残留。
  • 测试:新增 132 条纯逻辑断言(428 -> 560),覆盖哨戒炮双态策略(手持比例与部署百分比并存、容量与弹药为零/负数/NaN/Inf、钳制、显示映射)、按类型的 tracker/cue 分派、cue 映射(默认值、逐 cue 覆盖、未知类型、未知 cue、目录穿越/绝对路径/流名/非 wav 拒绝、音量钳制、截断与非对象文档)、WAV 解码器(16-bit 单/双声道、>2 声道、不支持的位深、截断的 data 块、缺失 RIFF、空数据)、音频退役策略以及有界宽限。源码守卫现在会校验音频适配层隔离、随包 cue 映射、哨戒炮双态读取、真实挂载点,以及重绑绝不清空或销毁标签。
  • 审查修复(P0):1.0.9 同时修复了挂载本身。Plugin.Load 注册了注入的 IL2CPP 类型却从未添加它,导致 Awake/Update 从不运行,也就没有标签、哨戒炮行或提示音。现在插件在 ClassInjector.RegisterTypeInIl2Cpp<...>() 之后立即调用 AddComponent<EquipmentTimerOverlay>(),通过单实例 MountOverlay() 辅助方法挂在插件自己的 DontDestroyOnLoad 对象上,并在 Unload() 中销毁它(其 OnDestroy 运行 TimerController.Shutdown,正是释放标签、哨戒炮百分比与原生缓存句柄的地方)。Verify.ps1 现在校验真实挂载点及其顺序,而不再只看注册。
  • 审查修复:已部署哨戒炮的查询使用该行自己的库存槽位(IsDeployed(slot)、TryGetDeployedItem(slot, ...)),而不是假定 GearClass,因为 KindOf 接受任意装备槽位中的 SentryGunFirstPerson;槽位无法识别的行不显示百分比。
  • 审查修复:哨戒炮百分比标签现在会在其行锚点的父级发生变化(HUD 重建 / 兄弟节点切换)时,先重挂到该锚点当前父级,再做任何 anchoredPosition 运算;并在字号/字型不同时随时复制锚点的实时值——不逐帧销毁,除非字体真的变了也不逐帧重建。
  • 审查修复:被淘汰的 AudioClip 以截止时间「退役」(ClipRetentionPolicy:音频长度 + 1 秒,带夹取),只有在截止时间已过且音源未在播放时才销毁,因此一次性音效绝不会被截断;Shutdown() 统一释放缓存与退役的音频。
  • 审查修复:如果无法确定插件的绝对目录,音效层拒绝解析任何路径(ReadySoundPaths.IsUsableRoot),因此 WAV 绝不会相对进程工作目录被拾取;cue 映射保持内置默认,播放回退到原生事件。
  • 版本载体(PluginInfo.cs、AssemblyInfo.cs、csproj、manifest)、包成员清单(现在还包含 Assets/Audio/ready-sounds.json 与 Assets/Audio/README.txt)、Verify.ps1、Build-Package.ps1、Install-TestProfile.ps1(资源放在 DLL 旁,已存在的用户 cue 映射 / WAV 永不覆盖)以及两个 README 都已更新到 1.0.9。
  • 未在游戏内验证:游戏为每个角色实际选择的台词、原生语音与用户 WAV 的听感响度/混音、以及百分比在实机 HUD 上的屏幕位置,均属静态结论,仍需一次实机验证。

1.0.8

  • 武器行现在读取原生弹匣,而不再只做倒数:ClipIsFull() 为真的满弹匣 -> 常驻绿色 READY;部分弹匣 -> 黄色 RELOAD;空弹匣 -> 红色 RELOAD;原生换弹期间仍显示倒计时。GetCurrentClip()/GetMaxClip() 任一读取失败时保持中性 --,绝不宣称任何状态。
  • 只要 m_recharging 为假,BioTracker 就是常驻绿色 READY,因为那正是原生「扫描可用」状态。它和满弹匣 READY 都不能播放提示音:cue 仍然只跟随 TimerTracker.Completions。
  • 近战行只在 MWS_ChargeUp 内存在。蓄力过程中显示蓄力倒计时,蓄力完成显示绿色 READY,m_releaseWarning 为真显示红色 !。离开蓄力状态(挥出、取消、换武器)会在同一帧隐藏该行,因此不会有 --、idle READY 或过期绿色残留在挥击之后。
  • GlueGun(Gear.GlueGun,已对照已安装 interop 验证:GetShotCount(float) 对应原生 RVA 0x139A060,Updatepressure 对应 0x139B300):仅在原生蓄力状态真正运行时(m_fireButtonDown && !m_reCharging && !m_firing && m_pressure > 0)显示一行 Nx T.Ts,其中 N 是原生 GetShotCount((m_pressure - m_pressureProgressToFire) / (1 - m_pressureProgressToFire)) 的返回值,T 是 m_pressure * TimeToMaxpressure——即已经过去的蓄力时间,绝不是剩余时间。低于 m_pressureProgressToFire 时计数为 0(武器还不能开火);读不到计数(Owner 为空 / 分母退化)时保留中性占位符。不回写任何内容,也从不调用 FireBurst。KindOf 现在会在 GetMaxClip 换弹兜底之前识别 GlueGun。
  • 所有标签使用 TMP Left 对齐与左 pivot,并且每帧共用同一条左列;该列由四个装备槽位的原生字形边缘推导,再投影回各自父级,因此不同父级缩放或偏移不会跨父级比较 anchoredPosition。
  • 同一 1.0.8 候选上的审查修复:共享列现在还会预留每个标签的实测文本宽度,使整列(sharedLeft + 最宽标签)结束于最左侧原生名称左边一个间距处,而不是从那里开始——长标签再也不会压到名称。世界坐标到父级的转换会折出每行自己的 anchor 参考点(rect.xMin + anchor.x * rect.width),因此非居中 pivot 或拉伸 anchor 不再让某行偏移,逐行兜底也保持同样的不重叠保证。每个装备行现在都通过原生背包读取自己的槽位:PlayerBackpackManager.TryGetItem(_player.Owner, slot, out BackpackItem bp),并以 bp.IsLoaded、存活的 bp.Instance 与 item.ItemDataBlock.inventorySlot == slot 作为身份证明,因此未手持的武器/工具行也能显示真实状态;任一步失败就脱离该行,而不是绘制缓存猜测;近战行保留已验证的 FirstPersonItemHolder.m_meleeWeapon 兜底,用于背包实例缺失或不是装备的情况。Install-TestProfile.ps1 的措辞/断言现在明确写出包布局副本(BepInEx/plugins/EquipmentTimers/EquipmentTimers.dll)与唯一的 r2modman 管理副本之间的关系,因此不可能出现两个可加载 DLL。
  • 测试:1.0.8 候选新增 135 条纯逻辑断言(293 -> 428),覆盖弹匣状态、BioTracker 就绪状态、近战蓄力状态门控(含 m_releaseWarning)、GlueGun 门控 / 归一化压力 / 开火阈值 / 已过蓄力时钟 / 含 NaN、Inf 与零分母的一行格式、共享列不等式(sharedLeft + textWidth <= nameLeft - gap)以及不同父级缩放、偏移与非居中 anchor 下的 anchor 参考点放置几何,另有逐槽位背包绑定矩阵(绑定 / 过期 / 已销毁 / 非装备 / 身份不匹配 / 近战 holder 兜底)。源码守卫已更新到新 API(TimerDisplayPolicy、SlotItemRules、TimerLayout.TryColumnLeftEdge/ColumnLimit/TryPlaceLeftEdge/AnchorReferenceLocalX、TextAlignmentOptions.Left)。
  • TimerRowTone 贯通 TimerRowView、TimerDisplayPolicy(新增、纯逻辑)、TimerStyle.Tone 与 TimerSlotRow,因此颜色只在一处由原生状态决定。

1.0.7

  • 让每个已占用且启用的装备行保持可见:空闲时中性 READY,换弹/蓄力/冷却时显示实时倒计时;采样被取消/失败或过冲不可信时显示中性 --;被中断动作在其终止帧显示 --。真正观察到的完成仍为绿色并播放一次 cue;idle READY 绝不播放。
  • 匹配原生装备名称高度:镜像原生 TMP 的 localScale,测量两侧实际渲染的字形高度(textBounds.size.y × lossyScale.y);当计时字形偏矮时,按实测比例放大并额外加 10% 可读性余量。适配只会放大,绝不缩小标签。
  • 将近战的 Done 沿视为终止状态,并在满蓄力时停止倒计时;继续按住满蓄力挥击不再重开会话,也不再产生重复的抑制完成。
  • 用用户提供的粉发 BioTracker/计时器美术替换生成的占位图标,居中并重采样为真正的 256×256 PNG。项目中保留 icon-source.png;包内只含缩放后的 icon.png。
  • 使用固定 ZIP 条目时间戳与固定的图标源/输出 SHA-256 值构建可复现归档。

1.0.6

  • 修复 1.0.5 全部隐藏的回归:Test 配置加载了 1.0.5,但活跃的 GearSpecial/GearClass 行反复报告 chars=-1 vtx=0,导致字形门控从未打开。
  • 在 AddComponent<TextMeshPro> 之前保持标签 GameObject 非激活,以防空的默认 quad。非空文本到达时,在禁用 MeshRenderer 的状态下激活它,让 TMP 执行 Awake/OnEnable,然后调用 ForceMeshUpdate;只有字符与顶点都有效后才启用渲染。
  • 在有界 mesh 重试期间保持带文本的 TMP 对象激活,同时其 renderer 保持禁用。这避免了重复的激活循环,也不可能暴露空 quad。
  • 审阅了本地缓存的 WeaponIconPlus 程序集:它的 Harmony postfix 修改已有的 PUI_InventoryItem.m_selected_icon SpriteRenderer(尺寸/启用/颜色),并不创建替代的 UI 标签。未复制任何代码或素材。

1.0.5

  • 没有文本与真实字形,任何标签都不允许上屏。 1.0.4 的日志显示四行始终只采样到 active=0 text='',mesh 从 chars=0 vtx=4 走到 chars=4 vtx=32,发生十一次 mesh 变更,material/shader 与原生 3D TMP 完全一致——同时屏幕上出现了一条短白色横线。
  • 1.0.4 的生命周期确实能产生这样一帧:标签 GameObject 以 ACTIVE 状态创建,AddComponent<TextMeshPro> 构建了 TMP 的默认(空)quad,而该对象直到创建路径最后才被停用;DestroyLabel 使用裸 Object.Destroy(延迟到帧末),且 renderer 仍处于启用状态;mesh 失败又被空 catch 吞掉。这一归因是假设:日志无法把某条采样行对应到白线出现的那一帧,因此修复的目标是让任何无文本/非法 quad 都不可达,而不是去修补某一特定帧。
  • 标签现在在证明可绘制之前保持惰性:
    • GameObject 以 INACTIVE 创建,并在 AddComponent<TextMeshPro> 之后立即禁用 renderer,因此 TMP 的默认 quad 会在任何东西看到它之前被 ClearMesh 抹掉;
    • 每次文本更新与每次锚点重绑都先关闭 renderer、再清 mesh;
    • DestroyLabel 先禁用 renderer,再 SetActive(false),再 ClearMesh(),最后才调用延迟的 Object.Destroy——标签被替换的那一帧不可能残留 quad;
    • 空/纯空白文本保持隐藏并清空 mesh(TMP_Text.ClearMesh(),已确认在随包 interop 中为 public,回退到 TextMeshPro.ClearMesh(bool));
    • 非空文本只有在 textInfo.characterCount > 0 && mesh.vertexCount > 0 时才显示;否则保持隐藏,最多做三次有界重建(havePropertiesChanged + Graphic.SetAllDirty() + TextMeshPro.ForceMeshUpdate(true, true))。单独的顶点数从不被当作可读文本,因为缓冲容量可能超过 4×字符数。
  • 诊断改为延迟且诚实:旧的一次性 drawn 行会在该行仍处于非激活且空的状态下触发。现在每行改为一条 visible sample 行,只在该行变为激活且 mesh 有效之后一帧发出,携带 text、activeInHierarchy、chars、vtx、renderer.enabled/isVisible、mesh.bounds、shader、matShared 与 matInstance。最多三条 label kept hidden; <reason> 行说明某行为何拒绝显示。逐状态的 mesh 行已移除,因此四行只花几条日志,而不是耗尽 40 行预算。
  • 测试:新增 24 条纯逻辑断言(225 → 249),覆盖可见性门控(空文本永不显示、必须同时有字符与顶点、重试预算恰为 3、延迟采样要求激活+有效+下一帧)以及源码守卫(先非激活再 AddComponent、先关 renderer 再 ClearMesh、先关 renderer 再 Destroy)。

1.0.4

  • 在 1.0.3 之上修复三个 code review 阻塞项(不改玩法、不加新依赖):
  • 首次构建过早应用 rect 样式。 CreateLabel 在 _labelRect 赋值之前就调用了 ApplyRectStyle,而该辅助方法在 rect 为空时提前返回——于是新建标签会一直保留默认 pivot/anchor/sizeDelta,直到之后的某次重绑。现在先解析 RectTransform(SurfaceSync.CanApplyLayout),再应用样式,使首次构建与重绑走同一条路径。Verify.ps1 中的源码顺序守卫会在该顺序回退时让构建失败。
  • 锚点采用是事务式的。 AnchorLatch.ShouldAdopt 不再自行提交:该行先证明候选锚点有可用父级、且 RebindAnchor 真的移动了标签,然后才调用 Commit。父级缺失、SetParent 抛异常或 wrapper 已销毁时,latch 保持当前锚点并在下一帧重试,而不是把该行锁在一个它从未到达的锚点上。
  • 表面同步现在会跟随同一个原生 TMP 上的替换。 以前只修复为空的 font/material,因此原生 font / fontSharedMaterial / shader 被替换(皮肤、语言、HUD 重建)时会留下过期材质与过期 mesh。SurfaceSync 比较 font、material 与 shader 的指针,重新为标签指定,并强制重建 mesh——创建/重绑之后立即执行,此后每第 15 帧(60 fps 下约 4 Hz),从不逐帧。
  • 测试:新增 23 条纯逻辑断言(202 → 225),覆盖事务式 latch(提供 ≠ 提交、失败的重绑可重试、取消/重新武装、重置)与表面规则(font、material、shader 的相同/替换/缺失、布局就绪、mesh 重建决策)。

1.0.3

  • 标签在原生名称切换后仍然存活,并且真正栅格化出字形。 1.0.2 已能解析槽位、绑定武器并打印 drawn … visible=yes,但游戏会在两个兄弟 TMP 之间交替每个槽位的原生名称(Archetype Name / Archetype Name (1)),而该行在每次指针变化时销毁并重建标签(4 行共 25 次重建)。这种反复还让 TextMeshPro 始终无法生成字形 mesh(textInfo.characterCount 为 0、mesh.vertexCount 为 0),这正是包围盒返回 size=-4294967296、屏幕上什么都没有的原因。
  • 标签现在每行只创建一次,之后重绑而不是重建:parent、font、material、尺寸与点 anchor 就地更新,只有原生父级真的变化时才重新挂载。DestroyLabel() 不再能从锚点指针变化到达,一次性 drawn 行也不再因重绑而重置,因此 4 行只花 4 条日志而不是 25 条。
  • 锚点选择是粘性的:Diagnostics/AnchorLatch.cs 在原生 TMP 仍渲染时保持当前锚点,只有当另一个锚点连续 0.35 秒不可用后才采用它,因此逐帧闪烁无法把标签抢走。
  • mesh 生成现在在每次文本/锚点变化后通过已验证的 interop 成员强制触发——havePropertiesChanged = true、Graphic.SetAllDirty()、TMP_Text.ForceMeshUpdate(true, true)——并逐帧修复(最多 3 次重试),同时维护 font、fontSharedMaterial、font.material 回退、renderer.enabled、原生 gameObject.layer 与原生 sortingLayerID/sortingOrder。
  • 诊断:一次性 drawn 行现在以 chars=<n> vtx=<n> rend=<0/1> vis=<0/1> font=<name> mat=<name> shader=<name> 结尾,并且只有该 mesh 状态变化时才另发一行——仍是每行每状态一条,从不逐帧。
  • 新增 17 条纯逻辑断言(185 → 202)固定 latch 行为:一对存活锚点翻转 40 次产生零次重新采用,失效锚点只有在保持窗口之后才被替换,空候选与重置行为符合预期。
  • 不读取或写入任何不同的玩法数值,无新依赖,配置结构不变。

1.0.2

  • 计时标签现在真正位于 HUD 内部。 1.0.1 已能解析并绑定全部四个槽位,甚至进入绘制路径([hud] slot Gear* drawn; … visible=yes),但标签在屏幕上不可见:它复制了原生名称的拉伸 anchor 范围(anchorMin/anchorMax),同时又设置固定 sizeDelta。当 anchorMin != anchorMax 时,Unity 会把 anchor 跨度加到 sizeDelta 上(得到父级宽的矩形),并从 Lerp(anchorMin, anchorMax, pivot) 量取 anchoredPosition——当 pivot 为 (1, 0.5) 时,该参考点是跨度的远端边缘,于是标签被放到最远达半个父级宽的位置。现在标签把原生锚点对折叠成单点 anchor anchorMin = anchorMax = Lerp(native.anchorMin, native.anchorMax, native.pivot),这正是原生 anchoredPosition 已经使用的参考点;标签保持固定尺寸,其右边缘位于原生 rect 左边缘左侧一个间距处、垂直居中。只有当原生 anchor 真的移动时才重新镜像该点 anchor。
  • 一次性 drawn 行现在携带能区分「跑到屏幕外」与「从未放置」的几何:anchor、pivot、anchoredPosition、sizeDelta、rect、lossyScale、activeInHierarchy、TMP enabled、canvas render mode、父级 CanvasGroup alpha、WorldToScreenPoint 与文本 bounds。仍然严格每行一条,从不逐帧。
  • 新增 18 条纯布局断言(167 → 185),覆盖拉伸 anchor、非居中 pivot、间距/宽度钳制与精确的点坐标。
  • 不读取或写入任何不同的玩法数值,无新依赖,配置结构不变。

1.0.1

  • HUD 现在真正出现了。 原生槽位表改用 ContainsKey + 索引器读取,而不是 Dictionary<InventorySlot, PUI_InventoryItem>.TryGetValue。在 1.0.0 中,每次槽位读取都会从该泛型 by-ref 调用中抛出原生 MissingMethodException([hud] slot GearStandard unavailable: MissingMethodException,2026-10-25 11:48 会话),因此从未创建任何行,也就什么都没绘制。TryGetValue 仅作为受保护的回退保留,并在本会话内记住可用的查找方式。
  • 诊断同样有界且从不逐帧:槽位读取失败时记录拒绝的 interop 成员及其 il2cpp 消息(每个槽位一次),而不只是异常类型;某行真正绘制时第一次记录 slot、kind、anchor、可见性与文本,从而把「没有 HUD」与「没有计时活动」区分开。
  • 不读取任何不同的玩法数值,无新依赖,配置结构不变。

1.0.0

  • 首个发布版本。
  • 到近战满蓄力的倒计时(MWS_ChargeUp.m_maxDamageTime - m_elapsed),在满蓄力时停止:不额外增加保持计时。
  • 主武器与副武器的换弹倒计时,由原生 IsReloading 沿与 ReloadTime 构建,每插入一发就重启,并与 GetCurrentClip() 增长或 ClipIsFull() 对齐。
  • BioTracker 冷却倒计时直接来自 m_recharging、m_rechargeStartTime 与 m_rechargeDuration。
  • 每次真正完成时给出一行绿色 READY 状态加恰好一次原版 Wwise cue。取消、中断、换武器、备弹耗尽,以及在 MOD 开始观察之前就已开始的会话,都保持静默。
  • 标签是原生 TMP 装备名称的兄弟节点,同时跟随选中名称与 slim 名称、HUD 缩放与原生槽位重排。最多四行,每槽一行。
  • 配置:按计时器启用、完成事件选择、READY 保持时长、过冲宽限、间距/宽度比例、READY 颜色、透明度以及有界日志预算。
  • 纯客户端且只读;不写任何玩法数值,也不增加任何网络流量。

审查加固(同一 1.0.0,发布前)

  • 已销毁的原生对象不再能把异常抛进 Harmony postfix:所有 HUD/行的原生访问都有保护,即使清理本身失败也会清空引用,一行损坏绝不阻断其他行,缓存获取按有界频率重试而不是永久失败。
  • 换弹完成现在要求弹匣已满或可证明的备弹耗尽;换弹中途开火不再让该行变绿或发声。
  • 在 READY 窗口内开始的新动作会在同一帧收回绿色,并从它自己的起点开始计时。
  • 近战完成也接受 m_elapsed >= m_maxDamageTime 的原生进度,用于蓄力状态翻转的那一帧,同时绝不凭空制造完成。
  • 标签 X 锚定到原生文本真实字形边缘(textBounds),并以 rect 兜底;尺寸/pivot/anchor 只写一次,而不是逐帧写。
  • 逐发换弹的分段由两次插入之间的实测间隔估算,并标记为近似值。
  • 过冲的倒计时只隐藏其数字,并继续观察真正的完成。
  • cue 投递尊重 None(完全不投递),不再被全局 0.05 秒窗口门控,同一帧完成的两行各自投递一次。
  • VerboseLog 现在真正选择 40/400 行日志预算,超出预算时连消息都不构造。
  • VISUAL-CONTRACT.md 中「行只画在 HUD 显示的某个名称旁」现在由 Empty 状态门控强制,行被移除时丢弃槽位缓存,安装脚本在未提供备份目录时回退到临时备份目录。

运行时启动崩溃修复(同一 1.0.0,发布前)

  • 用隔离探针定位了 2026-10-25 的启动崩溃(coreclr +0x1D1FDD、0xC0000005、15-21 秒、4/4 次复现):单独启用 Harmony 钩子(358,600 次 PUI_Inventory.Setup 调用,约 3.5 kHz)与单独启用注入 MonoBehavior 都能存活;崩溃需要那条逐帧 HUD postfix 路径——它采用了实例、随后又解引用其 gameObject。在这些探针中 __instance != null 返回 False,即菜单交出的实例往往已经销毁。
  • postfix 现在只是纯粹的失效提示:实例从不被缓存、比较或解引用,HUD 句柄通过 0.5 秒限频探测与受保护读取重新获取。
  • 注入的 MonoBehavior 现在是无字段泵;所有状态与逻辑都放在普通托管的 TimerController 中,这同时消除了四条「unsupported parameter」interop 警告。
  • ResetRows 在无内容可重置时是空操作,其日志行经 RepeatGate 去重,因此逐帧原生重建无法再耗掉日志预算。
  • 估算的逐发换弹窗口现在在 HUD 中以 ~ 前缀显示(ReloadClock.Estimated 真正到达标签),低估窗口测试已接入套件(157 断言)。
  • 证据与范围:reports/runtime-startup-failure.md(附录 A)。关卡内行为仍待实机验证。