Skip to content

4.3.31: FFXIV 7.55, Black layers, game-loop recovery, idle refresh setting, copy layers from unavailable devices - #206

Merged
logicallysynced merged 11 commits into
masterfrom
chromatics-4.x
Jul 31, 2026
Merged

4.3.31: FFXIV 7.55, Black layers, game-loop recovery, idle refresh setting, copy layers from unavailable devices#206
logicallysynced merged 11 commits into
masterfrom
chromatics-4.x

Conversation

@logicallysynced

@logicallysynced logicallysynced commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Covers v4.3.25 through v4.3.31 on top of the 4.3.24 release.

FFXIV patch 7.55

Sharlayan 9.1.3 carries the memory signatures for patch 7.55. The release has no C# API changes, so the upgrade is the package reference in both projects plus the NLog 6.1.4 pin the new version requires.

Game-loop fault recovery

A production report (CHROMATICS-1D) showed a single fault killing lighting for the rest of the session, with no stack in the log to diagnose it. The game loop now runs its whole iteration under one fault handler: a failed tick logs the full exception and retries, ten consecutive failures drop the game connection so the reconnect path recovers, and the first and final failures forward to Sentry. The internal catch that silently swallowed processing faults in Release builds now rethrows to that single policy point, and the connection-loop fault logs carry full stacks too.

Black layer type

"Black" is available as both a base layer and a dynamic layer. The base variant holds the whole device at black, a dark foundation where only your dynamic layers light up. The dynamic variant holds the selected keys at black, a mask over anything a lower layer would paint. Neither reads the palette. layers.chromatics4 schemaVersion bumps 7 -> 8; older builds reset Black layers to None on load through the existing guard.

Lower the RGB refresh rate while FFXIV is not running

New checkbox under Settings → General, on by default. The disconnect throttle already existed; the setting lets users turn it off and keep their configured rate at all times. A follow-up derives the rate from the live game-connection state instead of caller-passed flags, which fixes reports of the rate staying low with the setting off.

Copy layers from disabled and disconnected devices

The copy dialog lists devices that only exist in layers.chromatics4 (provider off, or hardware gone) as copy sources, alongside devices disabled on the Mappings tab. The picker labels their state, a note under the source dropdown says the copy runs from saved layers, and a checkbox hides unavailable devices from both pickers. Disconnected devices are source-only; their default LED mapping comes from a device-free LayerCopier core with per-row overrides available. The dialog now reads the shared dynamic-layer order list, so new layer types can no longer drift out of its display names.

Smaller fixes

  • The update prompt showed the newest version's heading twice and repeated older sections. publish.py now embeds only each version's own bullets, and the dialog trims old-format notes already on the feed - including mislabelled assets, version-prefix collisions, and leading separators.
  • The "Pair" button in the Nanoleaf dialog read as the noun in Spanish, French, and Japanese; corrected to each language's pairing verb.
  • Replaced the obsolete TextBox.Watermark with PlaceholderText (AVLN5001).
  • actions/setup-dotnet bumped v5 -> v6 in CI, superseding Dependabot PR Bump actions/setup-dotnet from 5 to 6 #207.

Notes

  • New locale keys shipped in all seven languages; orphaned draft keys pruned.
  • chromatics-docs updated separately (Settings, Mappings, and layer reference pages).
  • 176/176 xUnit tests pass, with new coverage for LED mapping rules and update-notes trimming.

🤖 Generated with Claude Code

…3.24 -> v4.3.25)

Idle refresh: the disconnect throttle (surface drops to the idle tick
rate while FFXIV is not attached, resumes the configured rate on
attach) is now gated by a new General setting, on by default. The
Settings toggle re-applies the rate immediately through
RGBController.ReapplyIdleUpdateRate instead of waiting for the next
connect or disconnect.

Copy layers: the copy dialog now lists devices that survive only in
layers.chromatics4 - provider off or hardware gone - as copy sources,
alongside Mapping-tab-disabled devices. Both carry a state label in
the picker ("not connected" / "disabled") and a notice under the
source row so the origin is obvious, and a checkbox hides them for a
shorter list. Absent devices have no IRGBDevice, so a new
LayerCopier.ComputeDefaultMappingForLayer overload derives the
default LedId mapping from the layer's own LedId set (identity for
keyboard pairs, ordinal fallback otherwise); they stay source-only
since a destination needs live LEDs. Device names aren't persisted
with layers, so absent entries are labelled by device type plus an
id slice.

New locale keys in all seven languages. 160/160 tests pass.
@logicallysynced logicallysynced self-assigned this Jul 18, 2026
…ng rules under test (v4.3.25 -> v4.3.26)

The hide checkbox only rebuilt the source list: when the selected
source survived the filter, the selection setter no-oped, the
source-changed handler never ran, and the destination dropdown kept
(or kept missing) disabled devices until the source changed. The
filter toggle now rebuilds destinations, revalidates the selected
destination, and rebuilds the layer rows whenever the source
selection carries over.

The absent-source default-mapping rules moved onto a device-free
core (source LedIds + destination LedId list) with the IRGBDevice
overload delegating to it, since constructing an IRGBDevice needs a
full device stack that kept the rules out of unit-test reach. Five
tests pin the behaviour: keyboard identity with no positional
fallback, exact-id match, ordinal fallback, tail dropping, and
empty-input handling.

165/165 tests pass.
Machine translation rendered the single-word key as the noun (a pair
of things) instead of the verb the Nanoleaf pairing button needs.
Corrected to match each language's own pairing strings: Emparejar,
Appairer, and the katakana pairing form. German, Korean, and Chinese
were already verbs. Locale-only change, no version bump.
…> v4.3.27)

The update dialog prepends its own "## {version}" heading per release,
but the notes publish.py embedded started with the version's changelog
heading themselves and ran to the end of CHANGELOG.md - so the prompt
showed the version line twice and stacked older sections into every
entry when listing target plus deltas.

Fixed at both ends. publish.py's extract_release_notes now captures
only the version's own bullets (heading excluded, stopping at the next
section), since the dialog assembles the cumulative changelog itself
from per-version asset notes. The dialog trims each release's notes
through a new UpdateService.TrimNotesToOwnSection before rendering,
which drops a leading heading naming that version (all changelog
heading shapes, 4-part tolerated) and cuts at the next section - a
no-op for new-format notes, and the compatibility path for releases
already published with the old format. Entries whose notes trim to
nothing are skipped instead of rendering an empty section.

Eight tests pin the trimming: old format reduces to own bullets, new
format passes through, every heading variant strips, a mismatched
heading is kept while trailing sections still cut, and empty input
returns empty. Also corrected a small grammar slip in the changelog
bullet wording. 173/173 tests pass.
Avalonia 12 deprecated Watermark (AVLN5001); the Nanoleaf adoption
dialog's manual-IP field was the only use. Same rendered behaviour and
the same locale key, so no version bump.
Both hold their keys at #000000: the base variant covers the whole
device (a dark foundation only higher layers light up), the dynamic
variant covers the selected keys (a mask over anything a lower layer
would paint). Neither reads the palette - black is the point, so the
colour never varies and there is nothing to configure beyond the key
assignment.

Wiring follows the layer-type invariants: DynamicLayerType.Black = 18
matches its appended position in _dynamicLayerOrder, BaseLayerType
.Black = 6 appends to the enum, both register in LayerProcessorFactory
(whose switches throw on missing entries), and layers.chromatics4
schemaVersion bumps 7 -> 8 in all three write sites with the version
history noting the new value space. SanitizeLayerTypeIndexes already
guards by Enum.IsDefined, so older builds reset Black layers to None
on load instead of dispatching a cast they cannot handle.

The ComboBox tooltip comes from the LayerDisplay description; both
descriptions and the shared "Black" name ship in all seven languages.
173/173 tests pass.
…lack tooltip (v4.3.28 -> v4.3.29)

The surface tick rate was set from a flag each caller passed:
RunStartupEffects passed idle:true, StopEffects(gameFirstConnected)
passed false. But RunStartupEffects also runs on device hot-plug and
re-enable while the game is attached, so the effective rate depended
on which path fired last rather than on whether FFXIV was connected -
which could leave the rate lowered at the wrong time. ApplyUpdateRate
now derives the idle state from GameController.IsGameConnected and the
setting, recomputed on every call and on the Settings toggle, so the
configured rate always runs while connected and whenever the setting
is off. Removes the caller-passed flag and the last-requested-state
field.

Also removed the "(#000000)" from the two Black layer tooltips per
request; the descriptions still say the keys are set to black. Locale
keys regenerated for all seven languages.

173/173 tests pass.
@logicallysynced logicallysynced changed the title 4.3.25: idle refresh-rate setting and copying layers from unavailable devices 4.3.29: Black layers, idle refresh-rate setting, copy layers from unavailable devices Jul 20, 2026
…v6 (v4.3.29 -> v4.3.30)

Sharlayan 9.1.3 carries the FFXIVClientStructs signatures for patch
7.55; the release has no C# API changes, so the bump is the package
reference in both projects plus the NLog 6.1.4 companion pin the new
version requires.

GameLoop hardening from CHROMATICS-1D: a user's session died to
"Insufficient memory" with 25GB of RAM free - the shape GDI+ uses for
many non-memory failures - and the loop's fault handling made it
worse twice over: exceptions escaped the loop entirely (one bad tick
killed lighting until an app restart) and the fault continuation
logged only the message, so the report arrived without a stack.
GameProcessLayers now runs under a per-tick catch that logs the full
exception and keeps ticking; ten consecutive failures drop the game
connection so the reconnect loop takes over cleanly. The continuation
backstop logs the full exception too.

CI: actions/setup-dotnet bumped v5 -> v6, superseding Dependabot PR
#207 without its whole-file line-ending rewrite.

173/173 tests pass.
… edge cases, shared layer order (v4.3.30 -> v4.3.31)

The review of the latest PR commits confirmed six findings:

- The CHROMATICS-1D resilience was a no-op: GameProcessLayers wraps
  its whole body in a catch that swallows every fault (Debug-only
  logging, compiled out in Release), so the new per-tick handler
  never fired - and the original 1D fault must have escaped from
  OUTSIDE that method. The loop's catch now covers the entire
  iteration (process polling and the disconnect branch included), the
  internal swallow rethrows so the loop owns the single fault policy,
  the counter resets at loop start (a static count carried across
  sessions), and only the first and final failures forward to Sentry
  so a 10-strike storm is not 10 events. Both GameConnectionLoop
  continuations now log the full exception like GameLoop's does.

- TrimNotesToOwnSection had three edge bugs: a version could match
  its own numeric prefix in a longer version's heading (fixed with a
  word boundary), a mislabelled leading heading wiped the whole entry
  to empty via a cut at position zero (now keeps that heading's
  section and cuts at the next), and a leading horizontal rule
  doubled the dialog's separator (now stripped). Three tests pin
  each.

- CopyLayersDialogViewModel kept a private copy of the dynamic-layer
  order array that missed the Black addition, showing Black layers as
  "None" in the copy dialog. The duplicate is gone; both consumers
  read LayerItemViewModel.DynamicLayerOrder.

- BlackBaseProcessor allocated a brush every tick for a colour that
  never changes; one static brush now serves all ticks.

- Six locale files carried orphaned keys for the old hex-suffixed
  Black tooltips with no en.json master; pruned.

- The changelog bullet for the refresh-rate setting now mentions the
  off-state fix that landed with it.

176/176 tests pass.
@logicallysynced logicallysynced changed the title 4.3.29: Black layers, idle refresh-rate setting, copy layers from unavailable devices 4.3.31: FFXIV 7.55, Black layers, game-loop recovery, idle refresh setting, copy layers from unavailable devices Jul 31, 2026
@logicallysynced
logicallysynced merged commit 95e99f8 into master Jul 31, 2026
5 of 7 checks passed
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