Files
ue-toolchain/plugins/ue-design-skills/skills/ue-game-settings-architecture/references/patterns.md
T
ue-toolchain dab3f35079 feat(skills): ship ue-design-skills bundle, licensing and delivery gate
Phase 0 of the handoff plan, as a marketplace rather than a flat skills/
directory. Content moved out of the LyraResearch archive and depersonalised:
addresses stay in the archive, recipes ship.

- plugins/ue-design-skills: 17 skills, 232 failure-mode entries, each with the
  six required fields; catalog.json as the harness-neutral source of truth and
  .claude-plugin/ as one adapter over it.
- _gate: 16 rules, one poisoned fixture per rule, plus surface coverage so a
  declared file cannot silently miss the line rules.
- ADR-0002 (harness-neutral bundle behind a marketplace) and ADR-0003 (split
  licensing: CC BY-ND 4.0 prose, Apache-2.0 code and metadata).
- LICENSE files at both levels, CONTRIBUTING.md, docs/licensing-options.md as
  the material the licence decision grew from.

Verified: gate.py 0 violations; test_gate.py 16/16 rules redden on their
fixtures with a clean baseline and 2 root files reaching the line rules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:48:55 +07:00

11 KiB

Patterns: a measured settings and performance layer

A worked example of SKILL.md against one reference product. The conclusion is unusual enough to state at the top, because it changes what you should do with the rest of the file:

The framework is the best thing in the audited project. The wiring around it is where every defect is.

Roughly 5700 lines of settings framework contain no reference to the game that ships it, and no finding in this file is about its design. The nine failure modes are all in the 2100 lines of project-specific backend and registry that bind it — paths that the compiler cannot check, a pipeline with no caller, a composition part with no writer. That distinction is the adoption advice: take the framework, audit the wiring.

Markers: [measured] — read in source; [derived] — conclusion from measured facts; [open] — not answerable from source.


1. What the framework gives you, concretely

Capability [measured] Why it is hard to rebuild
Setting model fully separated from widgets The separation is what makes search, grouping and testing possible at all
Widget resolved by walking the model's superclass chain Map one base class, cover every derived setting, keep specific overrides
Apply / cancel / reset as a real transaction over a dirty set Correct restore of runtime state, not only stored values
Edit conditions with five outcomes, not a boolean disabled-with-player-reason, hidden-with-developer-reason, single option disabled, killed
Cascading dependencies between settings Changing one setting re-evaluates the others without hand-written wiring
Full-text search over rich text reduced to plain text Falls out of the model separation
Asynchronous readiness before the screen renders Audio device lists and cloud values arrive late; the screen waits
A debug mode that shows developer names and classes in the UI Makes every other finding in this file findable

[derived] The five-outcome edit condition is the piece most worth internalising even if you write your own framework. Hand-built settings screens almost universally have a boolean, and the two missing things — tell the player why it is disabled and tell the developer why it is hidden — are exactly the two questions asked later at cost.


2. The trade the framework makes, and where it lands

Bindings are string paths resolved by reflection — a getter and setter path rooted at the local player [measured]. That removes a class per setting and removes the compiler from the relationship.

The failure mode is precise: the stringification macro checks that a function name exists, not its type, arity or constness; mismatches surface as an assertion at registry initialization; assertions are compiled out of shipping [measured].

[derived] So the worst case is a shipping build where a setting reads a default and writes nowhere, with no diagnostic anywhere. That is not an argument against the design — it is an argument for one automated test that walks the registry and resolves every path. Recipes: GS-01, GS-02.


3. The ownership split, and the leak inside it

The design is a clean two-store split [measured]:

Machine-local Player-shared
Base engine user settings, ini per-player save game, cloud-syncable
Holds resolution, quality, frame limits, device profile, benchmark result colour-blind mode, sensitivities, subtitles, language, haptics

The authors state the rationale in a comment: shared settings are safe to put in the cloud and are stored per player, so controller preferences belong there rather than in local settings where every user would receive them [measured].

Then the accessor breaks it. The per-player getter for local settings returns a process-global singleton [measured].

[derived] "This player's local settings" is a fiction; there is one object per process. The corroborating evidence is structural rather than a second defect: the registry is dense with "only while playing as the primary player" conditions, which is the architecture apologising for the accessor. Recipe: GS-03.

A second, milder leak in the same area: audio volumes live in machine-local storage [measured] — a pure player preference placed by where the applying subsystem reads, not by what the value means. The audio registry reaches into both stores from adjacent lines. Recipe: GS-04.

[derived] Both are worth seeing together, because they are the same mistake at two scales: storage placed by implementation convenience rather than by semantics, then papered over at the call sites.


4. Three findings that are the same shape

This is the most transferable observation in the file. Three unrelated features, one structure:

Feature Present [measured] Missing [measured]
Automatic quality detection at first launch capability check, benchmark, threshold mapping, apply, save any caller of the decision function
Per-game-mode device profiles name composition, fallback chain, four candidate forms, logging any writer of the mode suffix
Input sensitivity application storage, dirty tracking, save, load, an apply function a body in the apply function

[derived] Each is complete except for one link, and in each case the missing link is invisible to every check that the rest of the feature passes. The decision function has two occurrences — its declaration and its definition. The suffix has six occurrences, of which one is a log line printing it and none is an assignment. The apply function is called correctly from the right place and contains nothing.

The suffix case deserves emphasis because it is the canonical shape from ue-reference-project-adoption appearing in a subsystem where the consequence is architectural: a variable read four times and written zero times, with the authors' own comment noting that nothing sets it. Any search asking "is this used?" answers yes, loudly. Recipes: GS-06, GS-07, GS-08.


5. Where the transaction leaks

Video settings apply immediately from inside a change notification, not on apply, and the code says so with a comment reading "for now" [measured].

[derived] Cancel still restores the value, so the transaction is not broken — it is leaky in a way the player sees: a preview they did not ask for, on a global apply path, potentially once per slider tick.

The comment is what turns this from a judgement call into a finding. A "for now" on a global apply path is a scheduled item. Recipe: GS-05.


6. The upgrade guard, and the number it does not tell you

Manual enumeration of an engine struct's members — copying, clamping and maximising quality levels field by field — is protected by a compile-time assertion on the struct's size, at five separate sites for one struct [measured].

[derived] This is good practice and it works: after an engine upgrade adds a quality channel, the build breaks rather than silently ignoring it. The gap is procedural. The person who hits the assertion during an upgrade needs to know how many functions must be reviewed, and that number is nowhere near the message. Recipe: GS-09.

Put the count in the assertion text. A guard is only as useful as the instruction it gives the person it stops.


7. The reapplication point, and why it is where it is

Global scalability is reapplied at the last line of the mode-load completion, after the load state is set and after all three waves of load notification [measured]. The same function serves the other trigger that changes the same state — a hotfixed device profile [measured].

Four reasons, all measured, and together they are a small lesson in ordering:

  1. feature plugins activated by the mode can change console variables and frontend policy, so effective values are unknown until they have all run;
  2. the device profile itself may change, and the reapplication reselects it;
  3. scalability is process-global, not per world — the authors note this in a comment explaining why they do not track it per world in multi-instance editor play;
  4. it is guarded off for servers, where render settings are meaningless.

[derived] Two different causes — mode loaded, profile hotfixed — reduced to one idempotent reaction at one named point. That arrangement is worth copying directly, and it is the opposite of letting each feature reapply what it thinks it changed.


8. Performance statistics: one collector, and the honest limits

Statistics come from the engine's own frame-data consumer interface rather than from hand-rolled timers [measured], feeding a ring buffer for graphing. Eighteen statistics, with a compile-time count assertion in three places [measured].

[derived] The design point worth copying is that presentation and collection are separate: widgets read a cache, and no widget collects. The design point worth questioning before copying is that a buffer size is meaningless without a documented sample cadence — a hundred-odd samples describes a window only once you know the interval.

Two hard couplings to be aware of before lifting the subsystem: one statistic reads a game-specific state class, and latency statistics are gated on a specific GPU vendor because the project integrates one vendor's plugin [measured]. Both are small and both are visible; neither is a reason to rewrite the rest.


9. What to copy, in order

  1. The framework itself, if the UI dependency is acceptable. Five-outcome edit conditions, superclass-chain widget resolution, the change tracker, async readiness.
  2. The reapplication point (§7) — one idempotent function, two causes.
  3. The two-store split (§3) — but decide placement by semantics and check your accessor's body before trusting the word "local".
  4. The size-guard discipline (§6), improved by writing the count into the message.
  5. Collector/presenter separation for statistics (§8).

Audit before shipping: every property path resolves (GS-01, GS-02); the accessor is not a singleton (GS-03); every apply function has a body (GS-06); every composition part has a writer (GS-07); every pipeline has a caller (GS-08).

[derived] That list is short, mechanical, and would have caught every defect in this file. None of it requires understanding the framework — which is the point of shipping recipes rather than conclusions.


Provenance

Measured against Epic's Lyra Starter Game on Unreal Engine 5.6 and the settings plugin shipped with it: roughly 5700 lines of framework, 2100 of project settings backends, 2000 of registry, and 800 of performance code, read as source in a single workspace. Source addresses stay in the research archive that produced this skill; each GS- identifier resolves back to the audited location there.

Evidence boundary

Widget assets are binary and were not read, so nothing here describes the presentation layer beyond its base classes. Configuration values quoted describe that project on that day. Re-run the recipes against your own tree.