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>
This commit is contained in:
+348
@@ -0,0 +1,348 @@
|
||||
# Failure modes: game settings and performance
|
||||
|
||||
Nine ways a settings layer stores a value the player chose and does nothing with
|
||||
it.
|
||||
|
||||
The shared property is unusual and worth stating carefully: **in this area the
|
||||
framework is usually right and the wiring is usually missing.** Every finding
|
||||
below sits at a seam — between a model and a property path, between a stored
|
||||
value and the code that applies it, between a complete pipeline and the caller it
|
||||
never got. None of them is a design flaw in the mechanism they belong to.
|
||||
|
||||
That matters for adoption, which is what this skill is for: the correct response
|
||||
to this list is *copy the framework, audit the wiring*, not *write your own*.
|
||||
|
||||
Recipes use `rg` from a project source root and were executed against the audited
|
||||
project while this file was written.
|
||||
|
||||
---
|
||||
|
||||
## Binding
|
||||
|
||||
### GS-01 - A property path the compiler does not check
|
||||
|
||||
**Mechanism.** Settings bind to storage through a stringified path to a getter
|
||||
and setter, resolved by reflection at runtime. A macro checks that the function
|
||||
*name* exists; nothing checks the return type, the parameter count or constness.
|
||||
|
||||
**Why it is silent.** A mismatched path fails at initialization with an
|
||||
assertion — and assertions are compiled out of shipping builds. In shipping, a
|
||||
broken binding is a setting that reads a default and writes nowhere.
|
||||
|
||||
**Why the obvious check misses it.** The macro's name contains "checked", and it
|
||||
genuinely does check something. Review sees a compile-time-looking construct and
|
||||
stops. The remaining gap is invisible until the types diverge, which happens
|
||||
during a refactor rather than at authoring time.
|
||||
|
||||
**Symptom.** In development, an assertion when the settings screen opens — often
|
||||
dismissed as unrelated. In shipping, a setting that silently does nothing, which
|
||||
is indistinguishable from a setting that is not implemented.
|
||||
|
||||
**Detect.** Find the path-building macros and confirm the failure mode of the
|
||||
resolver, then check whether anything validates paths outside the editor:
|
||||
|
||||
```bash
|
||||
rg -n "GET_FUNCTION_NAME_STRING_CHECKED|FCachedPropertyPath|PropertyPathHelpers" \
|
||||
--glob "*.h" --glob "*.cpp" .
|
||||
rg -n -B4 "ensure|checkf" --glob "*.cpp" . | rg -i "getter|setter|propertypath"
|
||||
rg -n "UE_BUILD_SHIPPING" --glob "*.cpp" . | rg -i "ensure"
|
||||
```
|
||||
|
||||
**Guardrail.** Validate every path during registry initialization, report the
|
||||
**setting's developer name** rather than a reflection error, and run that
|
||||
validation in an automated test so it fails a build rather than a play session.
|
||||
|
||||
---
|
||||
|
||||
### GS-02 - Uniqueness enforced on the identifier, nothing enforced on the pair
|
||||
|
||||
**Mechanism.** Settings carry a stable developer name, and the registry checks it
|
||||
is unique. Nothing checks that the name still matches the property path it binds.
|
||||
|
||||
**Why it is silent.** Both halves are individually valid: a unique name and a
|
||||
resolvable path. A setting named for one property and bound to another works
|
||||
perfectly and is wrong.
|
||||
|
||||
**Why the obvious check misses it.** The uniqueness check exists and passes,
|
||||
which answers "are the identifiers sane?". Copy-paste between two settings
|
||||
changes the display text and leaves the path — and the resulting setting looks
|
||||
correct in every view except behaviour.
|
||||
|
||||
**Symptom.** Two settings that move together, or one that changes something the
|
||||
player did not select. Reported as "the wrong option is wired up", investigated
|
||||
in the widget layer.
|
||||
|
||||
**Detect.** Extract name/path pairs and eyeball the mismatches:
|
||||
|
||||
```bash
|
||||
rg -n -B2 -A6 "SetDevName\(" --glob "*.cpp" . \
|
||||
| rg "SetDevName|SetDynamicGetter|SetDynamicSetter"
|
||||
```
|
||||
|
||||
Read the triples. A developer name and a getter that share no token is the
|
||||
finding — crude, and it catches the copy-paste error that produces this.
|
||||
|
||||
**Guardrail.** Assert the correspondence in a test that walks the registry, or
|
||||
generate the name from the path. A convention nothing enforces is a convention
|
||||
that has already been broken somewhere.
|
||||
|
||||
---
|
||||
|
||||
## Ownership
|
||||
|
||||
### GS-03 - A per-player accessor that returns a global singleton
|
||||
|
||||
**Mechanism.** The local player exposes an accessor for its settings; the
|
||||
implementation returns a process-wide singleton.
|
||||
|
||||
**Why it is silent.** With one local player it is correct in every observable
|
||||
way. The abstraction is only wrong when a second player exists, which is a
|
||||
configuration most testing never enters.
|
||||
|
||||
**Why the obvious check misses it.** Every call site reads correctly — a player
|
||||
asking for its settings. The defect is one line inside an accessor that nobody
|
||||
reads twice, because accessors are the least interesting code in a file.
|
||||
|
||||
**Symptom.** In split-screen, the second player edits the first player's
|
||||
settings. Usually discovered not as a bug but as a growing population of "only
|
||||
the primary player may change this" conditions, added one at a time by people
|
||||
working around a cause nobody named.
|
||||
|
||||
**Detect.** Read the body of every per-player settings accessor:
|
||||
|
||||
```bash
|
||||
rg -n -A4 "::Get\w*Settings\(\) const|::Get\w*Settings\(\)" --glob "*.cpp" . \
|
||||
| rg -i "::Get\(\)|GEngine->|StaticClass"
|
||||
rg -n "PlayingAsPrimaryPlayer|IsPrimaryPlayer" --glob "*.cpp" . | wc -l
|
||||
```
|
||||
|
||||
The second count is the corroborating signal: a large number of primary-player
|
||||
conditions in a project that claims per-player settings means the claim is not
|
||||
true. In the audited project the accessor returns a global object, and the
|
||||
conditions are numerous.
|
||||
|
||||
**Guardrail.** Decide per value whether it is machine-scoped or player-scoped,
|
||||
and let the accessor's return type say which. If a value must be global, name the
|
||||
accessor globally — the honest name prevents the workarounds.
|
||||
|
||||
---
|
||||
|
||||
### GS-04 - Player preferences stored in machine-scoped storage
|
||||
|
||||
**Mechanism.** A value that is clearly a personal preference — volume, for
|
||||
instance — lives in the machine-local store because that is where the subsystem
|
||||
applying it happens to read from.
|
||||
|
||||
**Why it is silent.** It works for one player on one machine, which is the
|
||||
overwhelmingly common case. The value persists, applies, and survives restart.
|
||||
|
||||
**Why the obvious check misses it.** The placement follows the *implementation*
|
||||
rather than the *semantics*, and the implementation reason is real: the applying
|
||||
code is machine-global. A reviewer asking "does this work?" gets yes.
|
||||
|
||||
**Symptom.** Preferences that do not follow the player to another machine, and
|
||||
two local players who cannot have different values. Discovered when cloud saves
|
||||
or split-screen are added, long after the placement was set.
|
||||
|
||||
**Detect.** Classify the fields of both stores against the ownership test:
|
||||
|
||||
```bash
|
||||
rg -n "UPROPERTY\(Config\)" -A2 --glob "*.h" <local-settings>.h | rg "\w+ \w+;"
|
||||
rg -n "UPROPERTY\(\)" -A2 --glob "*.h" <shared-settings>.h | rg "\w+ \w+;"
|
||||
```
|
||||
|
||||
For each field ask the four ownership questions from the skill. In the audited
|
||||
project the audio volumes sit in machine-local storage, and the registry that
|
||||
builds the audio screen reaches into both stores from adjacent lines.
|
||||
|
||||
**Guardrail.** Place by semantics, then solve the application problem. If the
|
||||
applying subsystem is global, the setting can still be player-scoped with the
|
||||
primary player's value applied — that is a deliberate policy rather than an
|
||||
accident of storage.
|
||||
|
||||
---
|
||||
|
||||
## Application
|
||||
|
||||
### GS-05 - A global apply invoked from a change notification
|
||||
|
||||
**Mechanism.** Changing a setting immediately calls the heavyweight global apply
|
||||
path from inside the change handler, rather than staging the value until the
|
||||
player presses apply.
|
||||
|
||||
**Why it is silent.** The result is correct and even feels responsive — the
|
||||
player sees the change instantly. Cancel still restores the value, so the
|
||||
transaction is not broken, only leaky.
|
||||
|
||||
**Why the obvious check misses it.** It is three lines in an edit condition,
|
||||
placed where a reviewer expects a condition rather than an action. The cost only
|
||||
appears with a setting a player can scrub — a slider makes it one global reapply
|
||||
per tick.
|
||||
|
||||
**Symptom.** Stutter while adjusting a quality setting; a visible flash on values
|
||||
the player then cancels; and, for expensive channels, a hitch per change.
|
||||
|
||||
**Detect.** Find application calls inside notification paths, and check for the
|
||||
tell:
|
||||
|
||||
```bash
|
||||
rg -n -B6 "ApplyScalabilitySettings|ApplySettings|SetQualityLevels" --glob "*.cpp" . \
|
||||
| rg "SettingChanged|OnSettingChanged|NotifySettingChanged"
|
||||
rg -n -i "//\s*TODO.*(for now|immediately)" --glob "*.cpp" .
|
||||
```
|
||||
|
||||
The second command is the high-yield one: in the audited project the immediate
|
||||
apply carries a comment saying "for now", which converts a judgement call into a
|
||||
confirmed finding.
|
||||
|
||||
**Guardrail.** Stage by default; preview deliberately and cheaply; apply on
|
||||
apply. Treat "for now" on a global apply path as a scheduled item, not a note.
|
||||
|
||||
---
|
||||
|
||||
### GS-06 - A stored preference with no application code
|
||||
|
||||
**Mechanism.** A value is exposed, edited, persisted and loaded. The function
|
||||
that would apply it has an empty body.
|
||||
|
||||
**Why it is silent.** Every visible part of the round trip works: the slider
|
||||
moves, the value saves, it comes back after restart. Only the effect is missing,
|
||||
and the effect is subjective — sensitivity, in the audited case.
|
||||
|
||||
**Why the obvious check misses it.** The apply function exists and is called. A
|
||||
review of the settings layer finds a complete implementation; the emptiness is in
|
||||
a body that the caller has no reason to open. And because the value *is* readable,
|
||||
game code may consume it directly elsewhere, making the empty function neither
|
||||
wrong nor sufficient.
|
||||
|
||||
**Symptom.** A player changes a preference, the UI confirms it, and nothing feels
|
||||
different. Support cannot reproduce it because the value really is stored.
|
||||
|
||||
**Detect.** Find apply functions with empty bodies:
|
||||
|
||||
```bash
|
||||
rg -n -A3 "void \w+::Apply\w+\(\)" --glob "*.cpp" . | rg -B2 "^\s*\}"
|
||||
```
|
||||
|
||||
Then, for each stored preference, find its consumer:
|
||||
|
||||
```bash
|
||||
V='MouseSensitivityX'
|
||||
rg -n "\b$V\b" --glob "*.cpp" --glob "*.h" . | rg -v "UPROPERTY|Set$V|Get$V"
|
||||
```
|
||||
|
||||
No consumer outside the accessors means the value goes nowhere.
|
||||
|
||||
**Guardrail.** A setting ships with its consumer, in the same change. If
|
||||
application is intentionally the game's responsibility rather than the
|
||||
framework's, delete the empty function — an empty function is a claim.
|
||||
|
||||
---
|
||||
|
||||
## Wiring
|
||||
|
||||
### GS-07 - A composition part that is read six times and never assigned
|
||||
|
||||
**Mechanism.** A device-profile name is composed from parts — base platform, a
|
||||
mode suffix, a user choice — with fallback through progressively shorter
|
||||
combinations. One part is declared as a local, read repeatedly, and never
|
||||
written.
|
||||
|
||||
**Why it is silent.** The fallback is designed to handle a missing part, and it
|
||||
does. Every path produces a valid profile name; the composition simply never
|
||||
takes the branches that use the absent part.
|
||||
|
||||
**Why the obvious check misses it.** The variable appears in six places,
|
||||
including a log line that prints it. Any "is this used?" search answers yes
|
||||
loudly. The absent half is the **writer**, and no default tool asks that
|
||||
question — this is the canonical shape from `ue-reference-project-adoption`,
|
||||
found here in a subsystem where the consequence is a whole feature that does not
|
||||
exist.
|
||||
|
||||
**Symptom.** Documentation and code structure both indicate that a game mode can
|
||||
select its own device profile. It cannot. A team planning per-mode performance
|
||||
profiles discovers this after designing around it.
|
||||
|
||||
**Detect.** For any composed name, count reads against writes:
|
||||
|
||||
```bash
|
||||
V='ExperienceSuffix'
|
||||
rg -n "\b$V\b" --glob "*.cpp" .
|
||||
rg -n "\b$V\b\s*=[^=]" --glob "*.cpp" .
|
||||
```
|
||||
|
||||
Reads with no assignment is the finding. In the audited project the first command
|
||||
returns six lines — a declaration, four reads and a log — and the second returns
|
||||
nothing but the declaration itself, which the authors annotated with a comment
|
||||
saying nothing sets it.
|
||||
|
||||
**Guardrail.** Do not ship a composition part with no producer. If it is
|
||||
scaffolding for a planned feature, say so where a reader of the *composition*
|
||||
will see it, not only at the declaration.
|
||||
|
||||
---
|
||||
|
||||
### GS-08 - A complete pipeline with no caller
|
||||
|
||||
**Mechanism.** Automatic quality detection is fully implemented: a capability
|
||||
check, a benchmark, threshold mapping, application and save. The function that
|
||||
decides whether to run it at startup has no caller anywhere.
|
||||
|
||||
**Why it is silent.** The manual path works. A player who presses the button gets
|
||||
correct auto-detection, so the feature demonstrably functions — just never on its
|
||||
own.
|
||||
|
||||
**Why the obvious check misses it.** Every component is present and testable, and
|
||||
unit tests of the threshold mapping pass. "Do we support automatic quality?"
|
||||
answers yes from any angle except the one that matters: who starts it.
|
||||
|
||||
**Symptom.** First-run experience uses default quality on every machine. Nobody
|
||||
notices, because developers' machines default acceptably and the button exists.
|
||||
|
||||
**Detect.** For the decision function, distinguish declaration from call:
|
||||
|
||||
```bash
|
||||
F='ShouldRunAutoBenchmarkAtStartup'
|
||||
rg -n "\b$F\b" --glob "*.h" --glob "*.cpp" .
|
||||
```
|
||||
|
||||
Exactly two hits — the declaration and the definition — is the finding. Extend to
|
||||
content if the project can call functions from assets, and record the result as
|
||||
open if you cannot inspect them.
|
||||
|
||||
**Guardrail.** Every entry-point function gets a **call-site test**, not only a
|
||||
unit test of what it computes. The mapping was never the part at risk.
|
||||
|
||||
---
|
||||
|
||||
### GS-09 - A size guard whose count nobody knows
|
||||
|
||||
**Mechanism.** Code enumerates every member of an engine struct — copying,
|
||||
clamping, maximising quality levels field by field — and protects itself with a
|
||||
compile-time assertion on the struct's size.
|
||||
|
||||
**Why it is silent.** It is correct, and it is good practice: after an engine
|
||||
upgrade adds a channel, the build breaks instead of silently ignoring it.
|
||||
|
||||
**Why the obvious check misses it.** Nothing is wrong here. The hazard is
|
||||
procedural: the guard fires during an upgrade, under time pressure, and the
|
||||
person who bumps the number needs to know **how many functions** must be reviewed.
|
||||
That number is not written anywhere near the assertion.
|
||||
|
||||
**Symptom.** An engine upgrade where the size constant is updated and one of the
|
||||
five enumerating functions is not, silently dropping a quality channel — exactly
|
||||
the failure the guard was written to prevent.
|
||||
|
||||
**Detect.** Count the guards and the functions they protect before upgrading:
|
||||
|
||||
```bash
|
||||
rg -n "static_assert\(sizeof\(" --glob "*.cpp" --glob "*.h" .
|
||||
rg -c "static_assert\(sizeof\(Scalability::FQualityLevels\)" --glob "*.cpp" .
|
||||
```
|
||||
|
||||
In the audited project this returns **five** sites for one struct. Five is the
|
||||
number an upgrader needs and would otherwise have to discover by grepping mid-fix.
|
||||
|
||||
**Guardrail.** Write the count and the list of functions into the assertion
|
||||
message. The guard is only as good as the instruction it gives the person it
|
||||
stops.
|
||||
@@ -0,0 +1,229 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user