EquipmentTimers
Status left of the native GTFO equipment names: green READY, yellow/red RELOAD, melee charge, BioTracker recharge, and GlueGun shots + charge time. Client-side, read-only.
| Date uploaded | a day ago |
| Version | 1.0.8 |
| Download link | HawChow-EquipmentTimers-1.0.8.zip |
| Downloads | 21 |
| Dependency string | HawChow-EquipmentTimers-1.0.8 |
This mod requires the following mods to function
BepInEx-BepInExPack_GTFO
BepInEx pack for GTFO. Preconfigured and includes Unity Base DLLs.
Preferred version: 3.2.3README
EquipmentTimers
Persistent status and countdowns drawn inside the native GTFO equipment list (top right), immediately to the
left of each equipment name, all of them left-aligned in one shared column. A weapon shows a green READY while its
magazine is full, a yellow RELOAD while it is not, a red RELOAD when it is empty, and the remaining seconds while
it reloads. The BioTracker shows a green READY whenever the scan is available. Nothing is added to the crosshair,
and there is no separate panel.
| Timed item | What the label means |
|---|---|
| Primary / secondary weapon | Green READY (full magazine), yellow RELOAD (not full), red RELOAD (empty); the remaining seconds while reloading |
| BioTracker / class tool | Green READY when ready; seconds until the scan can be used again while recharging |
| Melee weapon | Only inside MWS_ChargeUp: seconds until full damage, then green READY, and a red ! when m_releaseWarning is set |
| GlueGun | Only while the charge is held: 3x 1.2s - the native burst shot count and the charge time already elapsed |
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, and a
green magazine-full / BioTracker-ready state never plays the sound either.
Features
- Native look: same TMP font asset/style/sorting layer, mirrored native transform scale, and measured glyph-height fit. A shorter timer glyph is scaled to at least native height with a 10% readability margin.
- 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 for countdown/unknown status (inherits the native name colour), green for a real completion and for the native "full magazine / BioTracker ready" state, yellow for a partial magazine, red for an empty one or a charge that wants the thrust released.
- One shared left column: every label is left-aligned (TMP
Left, left pivot) on the same left edge, and that edge also reserves the widest label plus the configured gap, so the whole column ends left of the leftmost native name's glyph left edge instead of starting there. Rows are compared in world/screen space and each one is projected back into its own parent (including that parent's anchor reference point), so different parent scales or offsets cannot break the alignment. - 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
- Install BepInExPack_GTFO (this is the only dependency).
- Drop
EquipmentTimers.dllintoBepInEx/plugins/(the package already usesBepInEx/plugins/EquipmentTimers/EquipmentTimers.dll). - Start GTFO. Every occupied, enabled gear row shows its native state; weapons and the BioTracker read READY/RELOAD when idle, and the numbers count down during reload/charge/recharge.
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 | ShowGlueGun |
true |
GlueGun shot count and elapsed charge time |
| 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
IsReloadingandReloadTime. The first reload segment counts downReloadTimefrom theIsReloadingrising 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 shows
--instead of a guessed value but keeps watching; canceled/failed samples remain--while reads fail, and a valid interrupted action shows--on its terminal frame before returning to neutral idle READY. 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. - The GlueGun read-out is only drawn while the native charge state runs (
m_fireButtonDown && !m_reCharging && !m_firing && m_pressure > 0). The burst size is the nativeGetShotCount()answer for the same normalized pressure the game uses; belowm_pressureProgressToFirethe weapon cannot fire yet, so the count is0. If the count cannot be read (no owner, degenerate denominator, any interop refusal) the label shows a neutral placeholder instead of a fabricated number. - The GlueGun clock is the charge time already elapsed (
m_pressure x TimeToMaxpressure), never a remaining time, and the row disappears the moment the charge ends - no idleREADY, no countdown. - The melee row only exists inside
MWS_ChargeUp; leaving the state (release, cancel, weapon swap) hides it on that same frame, so no--, idleREADYor stale green window survives. - Every gear row reads its own slot:
PlayerBackpackManager.TryGetItem(_player.Owner, slot, out BackpackItem bp)withbp.IsLoaded, a livebp.Instanceanditem.ItemDataBlock.inventorySlot == slotas the identity proof, so the non-wielded weapon/tool rows show their native state too. If any of that fails the row detaches and shows nothing rather than a cached guess; the melee row keeps the verifiedFPItemHolder.m_meleeWeaponfallback for a missing / non-gear backpack instance. - Melee "max charge hold" time is intentionally not shown: the timer stops at full charge.
Verification status (v1.0.8)
- The fixed-SDK Release build, head-less timer/state-mapping tests (386 assertions), icon, manifest, assembly metadata and source guards pass against the installed GTFO interop set (Test-profile BepInEx).
- 1.0.6 was loaded in Test and its labels appeared, but the in-game check found them smaller than equipment names and hidden while idle. 1.0.7 mirrored native TMP scale and fitted the measured glyph height; 1.0.8 adds the weapon READY/RELOAD states, the persistent BioTracker READY, the melee charge-state gate and the GlueGun shot/charge line, all left-aligned in one shared column.
- No in-game visual pass is claimed for 1.0.8: the exact build still needs its post-install check in GTFO. The 1.0.0 main-menu run and its 211/217-second stability result are historical and do not verify this build.
- No gameplay screenshot is bundled. The package icon is artwork, 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_maxDamageTimefrom 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为真显示红色!。离开蓄力状态(挥出、取消、换武器)会在同一帧隐藏该行,因此不会有--、idleREADY或过期绿色残留在挥击之后。 - GlueGun(
Gear.GlueGun,已对照已安装 interop 验证:GetShotCount(float)对应原生 RVA0x139A060,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_iconSpriteRenderer(尺寸/启用/颜色),并不创建替代的 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×字符数。
- GameObject 以 INACTIVE 创建,并在
- 诊断改为延迟且诚实:旧的一次性
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)时,该参考点是跨度的远端边缘,于是标签被放到最远达半个父级宽的位置。现在标签把原生锚点对折叠成单点 anchoranchorMin = 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)。关卡内行为仍待实机验证。