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.
CHANGELOG
Changelog
1.0.7
- Keep every occupied, enabled equipment row visible: neutral
READYwhile idle, the live countdown during reload/charge/recharge, neutral--while a sample is canceled/failed or an overrun is untrustworthy, and--on an interrupted action's terminal frame. A real observed completion remains green and plays one cue; idle READY never does. - Match the native equipment-name height: mirror the native TMP localScale, measure both rendered glyph heights (
textBounds.size.y × lossyScale.y), and, when the timer glyph is shorter, scale it by the measured ratio plus a 10% readability margin. The fit never shrinks the label. - Treat the melee
Doneedge as terminal and stop the countdown at full charge; holding a charged swing no longer reopens a session or produces repeated suppressed completions. - Replace the generated placeholder icon with the user-provided pink-haired BioTracker/timer artwork, centered and resampled to a true 256×256 PNG. Preserve
icon-source.pngin the project; the package contains only the resizedicon.png. - Build reproducible archives with fixed ZIP entry timestamps and pinned icon source/output SHA-256 values.
1.0.6
- Fix the 1.0.5 all-hidden regression: the Test profile loaded 1.0.5, but active GearSpecial/GearClass rows repeatedly reported
chars=-1 vtx=0, so the glyph gate never opened. - Keep the label GameObject inactive before
AddComponent<TextMeshPro>to prevent the empty default quad. When non-empty text arrives, activate it withMeshRendererdisabled so TMP runsAwake/OnEnable, then callForceMeshUpdate; enable rendering only after characters and vertices are valid. - Keep a text-bearing TMP object active during bounded mesh retries while its renderer stays disabled. This avoids repeating the activation cycle and cannot expose an empty quad.
- Reviewed the locally cached
WeaponIconPlusassembly: its Harmony postfix edits the existingPUI_InventoryItem.m_selected_iconSpriteRenderer(size/enabled/color); it does not create a replacement UI label. No code or assets were copied.
1.0.5
- No label may reach the screen without text and real glyphs. 1.0.4's log showed four rows that only
ever sampled
active=0 text='', a mesh that wentchars=0 vtx=4→chars=4 vtx=32, eleven mesh changes, and material/shader identical to the native 3D TMP - plus a single short white horizontal line on screen. The 1.0.4 lifecycle could produce exactly such a frame: the label GameObject was created ACTIVE,AddComponent<TextMeshPro>built TMP's default (empty) quad, and the object was only deactivated at the very end of the create path;DestroyLabelused a bareObject.Destroy(deferred to the end of the frame) with the renderer still enabled; and mesh failures were swallowed by empty catches. This attribution is a hypothesis: the log cannot tie one sample line to the frame the line appeared in, so the fix is written to make any textless/invalid quad unreachable rather than to patch a specific frame. - Labels are now inert until proven drawable:
- the GameObject is created INACTIVE and the renderer is disabled immediately after
AddComponent<TextMeshPro>, so TMP's default quad is wiped (ClearMesh) before anything can see it; - every text update and every anchor rebind switches the renderer off and clears the mesh FIRST;
DestroyLabeldisables the renderer, thenSetActive(false), thenClearMesh(), and only then calls the deferredObject.Destroy- no residual quad can survive the frame a label is replaced;- empty/whitespace text keeps the label hidden with an empty mesh (
TMP_Text.ClearMesh(), verified public in the shipped interop, withTextMeshPro.ClearMesh(bool)as fallback); - non-empty text is shown only when
textInfo.characterCount > 0 && mesh.vertexCount > 0; otherwise it stays hidden with at most three bounded rebuild attempts (havePropertiesChanged+Graphic.SetAllDirty()+TextMeshPro.ForceMeshUpdate(true, true)). A vertex count alone is never treated as readable text, because the buffer capacity can exceed 4×characters.
- the GameObject is created INACTIVE and the renderer is disabled immediately after
- Diagnostics are delayed and honest: the old one-shot
drawnline fired while the row was still inactive and empty. It is replaced by ONEvisible sampleline per row, emitted only one frame after the row became active with a valid mesh, carryingtext,activeInHierarchy,chars,vtx,renderer.enabled/isVisible,mesh.bounds,shader,matSharedandmatInstance. At most threelabel kept hidden; <reason>lines document why a row refused to show. The per-state mesh line is gone, so four rows cost a handful of lines instead of spending the 40-line budget. - Tests: 24 new pure assertions (225 → 249) for the visibility gate (empty text never shows, characters AND vertices required, retry budget exactly 3, delayed sample requires active+valid+next frame) plus source guards (inactive-before-AddComponent, renderer-off-before-ClearMesh, renderer-off-before-Destroy).
1.0.4
- Three code-review blockers fixed on top of 1.0.3 (no gameplay change, no new dependency):
- First build applied the rect style too early.
CreateLabelcalledApplyRectStylebefore_labelRectwas assigned, and that helper returns early when the rect is null — so a freshly created label kept the default pivot/anchors/sizeDeltauntil some later rebind. The RectTransform is now resolved first (SurfaceSync.CanApplyLayout), then the style is applied, so first build and rebind go through the same path. A source-order guard inVerify.ps1fails the build if the order ever regresses. - Anchor adoption is transactional.
AnchorLatch.ShouldAdoptno longer commits by itself: the row first proves the candidate has a usable parent and thatRebindAnchorreally moved the label, and only then callsCommit. A missing parent, a throwingSetParentor a dead wrapper leaves the latch on the current anchor and the next frame retries, instead of locking the row onto an anchor it never reached. - Surface sync now follows swaps on the same native TMP. Previously only null font/material were
repaired, so a native font /
fontSharedMaterial/ shader replacement (skin, language, HUD rebuild) left a stale material and a stale mesh.SurfaceSynccompares pointers for font, material and shader, re-points the label, and forces a mesh rebuild — immediately after a create/rebind and then every 15th frame (~4 Hz at 60 fps), never per frame. - Tests: 23 new pure assertions (202 → 225) for the transactional latch (offer ≠ commit, failed rebind may retry, cancel/re-arm, reset) and the surface rules (equal/replaced/absent font, material, shader, layout readiness, mesh-rebuild decision).
1.0.3
- The label survives the native name swap and actually rasterises glyphs. 1.0.2 already resolved the
slots, bound the weapons and logged
drawn … visible=yes, but the game alternates each slot's native name between two sibling TMPs (Archetype Name/Archetype Name (1)), and the row destroyed and rebuilt the label on every pointer change (25 rebuilds for 4 rows). That churn also kept TextMeshPro from ever building a glyph mesh (textInfo.characterCount0,mesh.vertexCount0), which is why the bounding box came back assize=-4294967296and nothing appeared on screen. - The label is now created once per row and rebound instead of recreated: parent, font, material,
size and point-anchor are updated in place, and the transform is only re-parented when the native
parent really changed.
DestroyLabel()is no longer reachable from an anchor pointer change, and the one-shotdrawnline is not reset by a rebind, so 4 rows cost 4 lines instead of 25. - Anchor choice is sticky:
Diagnostics/AnchorLatch.csholds the current native TMP while it still renders and only adopts the other one after it stayed unusable for 0.35 s, so per-frame flicker cannot steal the label. - Mesh generation is now forced through verified interop members after every text/anchor change —
havePropertiesChanged = true,Graphic.SetAllDirty(),TMP_Text.ForceMeshUpdate(true, true)— and repaired per frame (bounded to 3 retries) together withfont,fontSharedMaterial,font.materialfallback,renderer.enabled, the nativegameObject.layerand the nativesortingLayerID/sortingOrder. - Diagnostics: the one-shot
drawnline now ends withchars=<n> vtx=<n> rend=<0/1> vis=<0/1> font=<name> mat=<name> shader=<name>, and a separate line is emitted only when that mesh state changes — still one line per row per state, never per frame. - 17 new pure assertions (185 → 202) pin the latch: 40 flips of a live pair cause zero re-adoptions, a dead anchor is replaced only after the hold window, null candidates and resets behave.
- No gameplay value is read or written differently, no new dependency, config layout unchanged.
1.0.2
- The timer labels are actually inside the HUD now. 1.0.1 resolved and bound all four slots and even
reached the drawing path (
[hud] slot Gear* drawn; … visible=yes), but the label was invisible on screen: it copied the native name's stretch anchor range (anchorMin/anchorMax) while setting a fixedsizeDelta. WithanchorMin != anchorMaxUnity adds the anchor span tosizeDelta(a parent-wide rect) and measuresanchoredPositionfromLerp(anchorMin, anchorMax, pivot)— with our pivot of(1, 0.5)that reference is the far edge of the span, so the label was placed up to half a parent width away from the native name. The label now collapses the native pair into a single-point anchoranchorMin = anchorMax = Lerp(native.anchorMin, native.anchorMax, native.pivot), which is exactly the reference point the nativeanchoredPositionis already expressed in, keeps a fixed size, and puts its right edge one gap left of the native rect's left edge, vertically centred. The point anchor is re-mirrored only when the native anchor actually moves. - The one-shot
drawnline now carries the geometry that tells "off screen" apart from "never placed": anchors, pivot, anchoredPosition, sizeDelta, rect, lossyScale, activeInHierarchy, TMP enabled, canvas render mode, parent CanvasGroup alpha, WorldToScreenPoint and the text bounds. Still exactly one line per row, never per frame. - 18 new pure layout assertions (167 → 185) for stretch anchors, non-centre pivots, gap/width clamping and the exact point coordinates.
- No gameplay value is read or written differently, no new dependency, config layout unchanged.
1.0.1
- The HUD now actually appears. The native slot table is read with
ContainsKey+ the indexer instead ofDictionary<InventorySlot, PUI_InventoryItem>.TryGetValue. In 1.0.0 every slot read threw a nativeMissingMethodExceptionout of that generic by-ref invoke ([hud] slot GearStandard unavailable: MissingMethodException, 2026-10-25 11:48 session), so no row was ever created and nothing was drawn.TryGetValueis kept only as a guarded fallback and the surviving lookup is remembered for the session. - Diagnostics, both bounded and never per frame: a failed slot read logs the interop member that refused plus the il2cpp message, once per slot, instead of only the exception type; the first time a row really draws it logs slot, kind, anchor, visibility and text once, so "no HUD" can be told apart from "no timer activity".
- No gameplay value is read differently, no new dependency, config layout unchanged.
1.0.0
- First release.
- Countdown to the full melee charge (
MWS_ChargeUp.m_maxDamageTime - m_elapsed), stopping at full charge: no hold timer is added. - Reload countdown for the primary and secondary weapons, built from the native
IsReloadingedge andReloadTime, restarted per inserted shell and reconciled withGetCurrentClip()growth orClipIsFull(). - BioTracker recharge countdown straight from
m_recharging,m_rechargeStartTimeandm_rechargeDuration. - One green
READYstate plus exactly one vanilla Wwise cue per real completion. Cancels, interrupts, weapon switches, empty reserves and sessions that started before the mod was watching stay silent. - Labels are siblings of the native TMP equipment names, following both the selected and the slim name, the HUD scale and native slot repositioning. At most four rows, one per slot.
- Config: enable per timer, ready event choice, ready hold time, overrun grace, gap/width ratios, ready colour, opacity and a bounded log budget.
- Client-side and read-only; no gameplay value is written and no network traffic is added.
Review hardening (same 1.0.0, pre-publication)
- Destroyed native objects can no longer throw into a Harmony postfix: every HUD/row native touch is guarded, references are cleared even when cleanup itself fails, one broken row never stops the others, and cache acquisition retries at a bounded rate instead of failing forever.
- Reload completion now requires a full magazine or provable reserve exhaustion; firing mid-reload no longer turns the row green or beeps.
- A new action inside the READY window retracts the green on the same frame and is timed from its own start.
- Melee completion also accepts native progress at
m_elapsed >= m_maxDamageTimefor the frame where the charge state flips, without ever fabricating a completion. - Label X is anchored to the native text's real glyph edge (
textBounds) with a rect fallback, and the size/pivot/anchors are written once instead of every frame. - Shell-by-shell segments are estimated from the observed insert interval and reported as approximate.
- A countdown that overshoots hides only its number and keeps watching for the real completion.
- Cue posting respects
None(no post at all), is no longer gated by a global 0.05 s window, and two rows completing on the same frame each post once. VerboseLognow really selects the 40/400 log budget, and messages are not even built past it.VISUAL-CONTRACT.md"row only next to a name the HUD shows" is now enforced by theEmptystate gate, the slot cache is dropped when a row is removed, and the install script falls back to a temp backup directory when none is supplied.
Runtime startup-crash fix (same 1.0.0, pre-publication)
- Traced the 2026-10-25 startup crash (coreclr +0x1D1FDD, 0xC0000005, 15-21 s, 4/4 runs) with isolated
probes: the Harmony hooks alone (358,600
PUI_Inventory.Setupcalls, ~3.5 kHz) and the injected MonoBehaviour alone both survive; the crash needed the per-frame HUD postfix path that adopted the instance and later dereferenced itsgameObject.__instance != nullcame back False in those probes, i.e. the menu hands out already-destroyed instances. - The postfix is now a pure invalidation hint: the instance is never cached, compared or dereferenced, and the HUD handle is re-acquired through a 0.5 s throttled probe with guarded reads.
- The injected MonoBehaviour is now a field-free pump; all state and logic live in the plain managed
TimerController, which also removes the four "unsupported parameter" interop warnings. ResetRowsis a no-op when there is nothing to reset and its log line is deduplicated (RepeatGate), so the per-frame native rebuild can no longer spend the log budget.- Estimated shell-reload windows are now prefixed with
~in the HUD (ReloadClock.Estimatedreally reaches the label), and the underestimated-window test is wired into the suite (157 assertions). - Evidence and scope:
reports/runtime-startup-failure.md(appendix A). In-level behaviour is still pending.