Files
ue-toolchain/plugins/ue-design-skills/skills/ue-cosmetics-and-teams/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

289 lines
13 KiB
Markdown

# Patterns: cosmetics and teams in a measured reference product
A worked example of both halves of `SKILL.md` against one reference project read
as source. The two systems are presented together because the audit found what
the skill claims: they touch at exactly one point, and both are well built on
either side of it.
This file is unusually positive. That is the finding, not a tone: the
architecture here is the strongest in the audited project, and the defects are
narrow and specific. Separating "this design is worth copying" from "this
implementation is finished" is the entire job.
Markers: **[measured]** — read in source; **[derived]** — conclusion from
measured facts; **[open]** — not answerable from source, because the call site is
in a binary asset.
---
## 1. The two-level split, and what it buys
```text
controller component owns the desired part list, survives respawn
│ on possession change: remove from old, apply to new, keep handles
▼
pawn component replicated list, spawns presentation, computes tags
```
The controller component subscribes to possession changes **only on the
authority**, and if a pawn already exists at subscription time it invokes its own
handler with a null previous pawn **[measured]**. That second detail is the
pattern's whole robustness: the same code path handles "pawn already here" and
"pawn arrives later", so ordering stops mattering.
Transfer removes by handle from the old pawn and re-applies to the new, guarding
against double-add by checking whether the handle is already valid **[measured]**.
**[derived]** This generalises well beyond appearance. Any state that belongs to
the *player* and is realized on the *body* — loadouts, perks, temporary buffs —
has the same shape and the same failure mode if you skip it: the state dies with
the pawn.
The link from controller to pawn is found dynamically each time, with no cache
**[measured]**. If the pawn has no cosmetics component the lookup returns null and
nothing happens, silently — a tolerance that is correct for optional cosmetics and
would be wrong for anything required.
---
## 2. Replicating intent, with the split declared in the type
One structure carries both server-only and client-only fields, distinguished by
annotation rather than by having two types **[measured]**:
| Field | Replicated | Lives on |
|---|---|---|
| the part itself: class plus socket | yes | server → clients |
| the handle returned to the caller | **no** | server only |
| the spawned presentation component | **no** | client only |
**[derived]** This is the cleanest available demonstration of "replicate intent,
realize locally". The wire carries a class reference and a socket name; the
receiving machine reconstructs the appearance. Late join is deterministic because
the state is compact and complete.
Change handling is deliberately coarse: the change callback destroys and respawns
rather than propagating a diff, and the authors say so in a comment
**[measured]**. Recipe: CT-02.
Part identity is defined as the pair (class, socket) and deliberately **excludes**
the collision mode from the comparison **[measured]** — so two requests differing
only in collision are the same part. **[derived]** That is a real decision about
what identity means, made once, in the comparison function where it belongs.
---
## 3. The isolation guarantee, stated precisely
One line excludes presentation from the dedicated server, and it is the only such
line in the module **[measured]**.
**[derived]** The consequence is strong and worth stating exactly: on a dedicated
server the cosmetic actors do not exist, so they cannot collide, block a trace,
tick, or influence the authoritative simulation in any way. That is a real
architectural guarantee obtained for one `if`.
It is also narrower than the sentence people repeat. On a listen server or in
standalone the parts exist on the authority, with whatever their class contains,
and nothing in the type system prevents a part class from carrying an ability
system or enabled collision. The other protections — collision defaulting to off,
authority-only mutators, no ability-system includes anywhere in the module
**[measured]** — are conventions and content discipline. Recipe: CT-01.
**The honest form:** *not present on a dedicated server*, not *cannot affect
gameplay*.
---
## 4. The ten-line pattern worth stealing
An array of rules, each an asset plus a set of required tags, plus a default.
Gather tags, walk the rules in order, first rule whose tags are **all** present
wins, otherwise the default **[measured]**.
It appears at least three times in the audited project — body mesh, forced
physics asset, and weapon animation layer **[measured]** — and costs roughly ten
lines each time.
**[derived]** Its sharp edge is that priority is array order, with no weights and
no specificity ranking. A broad rule above a narrow one silently shadows it, and
the failure produces a plausible wrong asset rather than an error. Recipe: CT-03.
The tag namespace is declared in config and is deliberately small — a body style
and two animation styles **[measured]**. **[derived]** Small is the right size:
every tag in this namespace multiplies the rule matrix that has to be reasoned
about.
---
## 5. Where the two systems touch
Exactly one place, and the code says so. The cosmetics component broadcasts a
change with a comment noting that observers may need to apply team colouring
**[measured]**; the team display asset applies material parameters to an actor
**and its child actors by default** **[measured]** — and cosmetic parts are child
actors.
**[derived]** That default is what carries team colour onto modular parts. It is
one boolean default, and it is the seam.
**[open]** The actual call is made from a Blueprint: the applying function has no
C++ caller. Which leads to the most important methodological point in this file.
---
## 6. What could not be answered, and why that matters
Four things in this subsystem have **no C++ call site** **[measured]**:
- who applies team colours to an actor;
- who selects the weapon animation layer from cosmetic tags;
- where the cosmetics component is added to a pawn;
- where the controller component is added to a controller.
All four are wired in Blueprint or data assets, which are binary and were not
read.
**[derived]** The correct conclusion is *"the wiring is in a place this audit
cannot see"*, and the incorrect one — available, tempting, and wrong — is *"this
function is unused"*. Both components are marked as spawnable from Blueprint
**[measured]**, which is positive evidence for the first reading.
This is the general rule from `ue-reference-project-adoption`, met in its natural
habitat: **absence of a C++ caller is not absence of a caller.** Anyone deleting
"dead" code in a project with a large Blueprint surface needs the editor's
reference viewer, not a text search.
---
## 7. Teams: one source, mirrors that refuse loudly
Membership lives on the player state, replicated push-based, authority-only, with
a non-authority write logged as an error **[measured]**.
Every mirror — controller, bot controller, local player, pawn — reads through to
the source, and their setters **log an error naming the real owner** rather than
silently doing nothing **[measured]**.
**[derived]** This is the single most copyable line in the team system. A silent
no-op setter on a mirror is indistinguishable from a working one until something
depends on it; an error line converts an invisible failure into a searchable one,
at a cost of one statement. Recipe: CT-06.
The pawn mirror is more subtle and also correct: it accepts a direct write **only
while unpossessed**, and refuses with an explanatory error once a controller owns
it **[measured]**. On possession it takes the controller's value and subscribes to
its change delegate; on unpossession it unsubscribes and calls an overridable hook
whose default is "no team" **[measured]**.
**[derived]** That hook is an extension point placed exactly where a neutral
faction would need it, which is the difference between a design and an
implementation that happens to work.
---
## 8. Resolution, comparison, and the one permissive branch
Resolution is a four-step cascade — interface, instigator, team-info actor,
associated player state **[measured]**. **[derived]** The instigator step is what
lets projectiles and area effects carry the shooter's team without owning one; a
design that omits it grows a team field on every projectile.
Comparison returns three states rather than a boolean **[measured]**, which is
correct and is what makes the next finding visible at all.
The damage rule permits four ways **[measured]**, and the third is the finding:
when the relationship is indeterminate, damage is allowed if the target has an
ability system component — with an author's comment marking it temporary until a
training dummy gets a team assignment.
**[derived]** It exists to make one specific actor damageable and it generalises
to every unassigned actor with an ability system. Harmless while the only such
actor is a dummy; a rule violation the moment a neutral faction or a destructible
objective exists. **Solve it in data — give the dummy a team — rather than in the
rule.** Recipe: CT-08.
Friendly fire is promised by a header comment and implemented nowhere: a
project-wide search for any friendly-fire symbol returns **zero** **[measured]**.
Allies are unconditionally safe. Recipe: CT-07.
---
## 9. One policy function, two consumers — the pattern to copy
The same permission function is called by the authoritative damage calculation
and by the client-side decision to show a hit marker **[measured]**.
**[derived]** This removes an entire bug class by construction. "The client showed
a hit and the server dealt no damage" requires two implementations of one rule to
disagree, and here there is only one implementation. Any project with predicted
feedback should copy this arrangement before copying anything else in the team
system.
The damage calculation consumes the result as a **0-or-1 multiplier** in the
formula rather than as an early return **[measured]**. **[derived]** The team rule
becomes an ordinary participant in the arithmetic, alongside distance and surface
attenuation, which keeps the execution pipeline uniform and makes the rule easy to
soften later — a scalar between zero and one is already expressible.
---
## 10. Presentation as named parameters
The display asset is a bag of named material parameters — scalars, colours,
textures, a short name — not a set of assets **[measured]**. Materials are
authored to expect the names, so adding a team costs one asset rather than a
duplicated material tree.
Two defects in the applying code, both narrow:
- **Alpha is dropped** by the material paths, which convert a four-channel colour
to a three-element vector, while the effects path preserves all four
**[measured]**. The same asset therefore behaves differently in two consumers.
Recipe: CT-09.
- **A viewer identity parameter is accepted and ignored**, with a comment saying
so **[measured]**. The signature and header describe viewer-relative colouring;
the implementation returns the absolute asset. Recipe: CT-10.
Reactive binding is done well: an observation action broadcasts **immediately on
activation** so a late subscriber gets current state rather than waiting for the
next change, and a second action re-subscribes correctly when the observed team
changes **[measured]**. **[derived]** Both are small patterns that eliminate a
race, and both are reusable anywhere state is observed through a key that can
itself change.
---
## 11. What to copy, in order
1. **One policy function for authority and prediction** (§9). Highest value,
lowest cost, removes a whole bug class.
2. **Loud mirrors** (§7). One log line per setter.
3. **Controller-owned intent, pawn-owned realization** (§1), including the
already-possessed case.
4. **Non-replicated fields inside a replicated entry** (§2) — one type instead of
two.
5. **Rules-plus-default selection** (§4), with a shadowing check added.
6. **Immediate first broadcast on subscription** (§10).
What to fix while copying: the indeterminate branch (CT-08), the friendly-fire
gap (CT-07), the alpha conversion (CT-09), the ignored viewer parameter (CT-10),
and the privacy that is a name rather than a mechanism (CT-11).
---
## Provenance
Measured against Epic's Lyra Starter Game on Unreal Engine 5.6: roughly 1300
lines of cosmetics across 11 files and 1800 lines of teams across 22, read as
source in a single workspace. Source addresses stay in the research archive that
produced this skill; each `CT-` identifier resolves back to the audited location
there.
## Evidence boundary
Four wiring call sites are in Blueprint or data assets and were not read; they are
marked open above and nothing here depends on them. Everything else describes one
project on one engine version. Re-run the recipes against your own tree before
trusting any specific claim.