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:
@@ -0,0 +1,315 @@
|
||||
---
|
||||
name: ue-cosmetics-and-teams
|
||||
description: >-
|
||||
Design or review modular character cosmetics and team systems in Unreal
|
||||
Engine: replicated cosmetic intent with local realization, controller-owned
|
||||
selection versus pawn-owned realization, fast-array entries, dedicated-server
|
||||
exclusion, tag-driven mesh and animation selection, one authoritative team
|
||||
identifier with subscribed mirrors, team relationship and damage policy, and
|
||||
data-driven material presentation. Use when building skins, modular
|
||||
characters, team assignment, friendly fire, hit-marker filtering or
|
||||
team-coloured UI and effects.
|
||||
---
|
||||
|
||||
# UE cosmetics and teams
|
||||
|
||||
Two invariants, one per half:
|
||||
|
||||
> Replicate cosmetic **intent**, never the spawned presentation.
|
||||
|
||||
> Store team membership **once**, authoritatively; every other copy is a mirror
|
||||
> with a subscription.
|
||||
|
||||
Measured patterns from a reference product: [patterns](references/patterns.md).
|
||||
Detection recipes: [failure modes](references/failure-modes.md).
|
||||
|
||||
These two systems are in one skill because they meet at exactly one point —
|
||||
presentation — and the most common design error is letting them meet anywhere
|
||||
else.
|
||||
|
||||
Related skills: `ue-data-driven-architecture`, `ue-gas-architecture`,
|
||||
`ue-gameplay-messaging`, `ue-modular-gameplay`, `ue-multiplayer-authority`.
|
||||
|
||||
Method, not architecture — how to check any claim in this bundle before repeating it: `ue-evidence-discipline`.
|
||||
|
||||
---
|
||||
|
||||
# Part I — Modular cosmetics
|
||||
|
||||
## 1. Split persistent intent from pawn realization
|
||||
|
||||
A controller-level component owns the selection, so it survives pawn death and
|
||||
respawn. A pawn-level component realizes it on the current body.
|
||||
|
||||
```text
|
||||
controller component pawn component
|
||||
├─ desired part list ├─ replicated part list
|
||||
├─ listens for pawn changes ├─ spawns local presentation components
|
||||
└─ applies and removes on the ├─ computes cosmetic tags
|
||||
current pawn, keeping └─ notifies body and animation selection
|
||||
handles
|
||||
```
|
||||
|
||||
If the only record of a player's appearance lives on the pawn, it dies with the
|
||||
pawn. That is the whole reason for the split.
|
||||
|
||||
### Possession transfer
|
||||
|
||||
On a possession change, in this order: remove owned parts from the old pawn;
|
||||
find the component on the new pawn; add owned parts; retain the returned handles.
|
||||
**Each owner removes only its own parts** — cheats, developer settings and real
|
||||
content coexist in one list, distinguished by a source field rather than by
|
||||
separate lists.
|
||||
|
||||
---
|
||||
|
||||
## 2. Define a part as data, and make it incapable of gameplay
|
||||
|
||||
A minimal part: a class to spawn, a socket to attach to, and a collision mode
|
||||
that defaults to none.
|
||||
|
||||
Default presentation safety: collision off unless explicitly requested; no
|
||||
ability system, attributes or authority inside the cosmetic actor; no replicated
|
||||
gameplay state; validated attach target; locally tracked handle.
|
||||
|
||||
### The guarantee is the net-mode guard, not the convention
|
||||
|
||||
Nothing in the type system prevents putting a gameplay-capable actor into a part
|
||||
class. The real isolation comes from one line: **presentation is not spawned at
|
||||
all on a dedicated server.** The server therefore cannot trace against, collide
|
||||
with, or be influenced by cosmetics, because they do not exist there.
|
||||
|
||||
Be precise about the limit of that guarantee: on a listen server or in
|
||||
standalone, the parts *do* exist on the authority. Recipe: CT-01.
|
||||
|
||||
---
|
||||
|
||||
## 3. Replicate intent with a fast array
|
||||
|
||||
The replicated entry holds the intent — class and socket. It must not hold the
|
||||
local handle or the spawned component. Put both of those in the same struct as
|
||||
**non-replicated** fields and let the annotation express the split, rather than
|
||||
maintaining two parallel types.
|
||||
|
||||
Callbacks realize changes locally: post-add spawns, pre-remove destroys,
|
||||
post-change rebuilds.
|
||||
|
||||
### Decide what "change" means before you need it
|
||||
|
||||
A common and defensible implementation makes post-change destroy and respawn the
|
||||
actor, because propagating a diff into an already-spawned actor is hard. That is
|
||||
fine and it is a decision with a cost: every edit is a full rebuild, so a system
|
||||
that edits parts frequently will hitch. Recipe: CT-02.
|
||||
|
||||
---
|
||||
|
||||
## 4. Tag-driven selection: rules plus a default
|
||||
|
||||
Parts contribute tags — body style, animation style, armour set. A selection set
|
||||
maps required tags to a mesh or an animation layer, with a fallback:
|
||||
|
||||
1. gather tags from all active parts;
|
||||
2. walk the rules in order;
|
||||
3. the first rule whose required tags are **all** present wins;
|
||||
4. otherwise use the default.
|
||||
|
||||
This microscopic pattern — an array of (asset, required tags) plus a default,
|
||||
first match wins — is about ten lines and is worth copying wholesale. It is the
|
||||
same shape whether it selects a mesh, a physics asset or an animation layer.
|
||||
|
||||
### Its one sharp edge
|
||||
|
||||
**Priority is array order.** There are no weights and no specificity ranking, so
|
||||
a broad rule placed above a narrow one silently shadows it, and the symptom is a
|
||||
wrong mesh rather than an error. Document the ordering requirement next to the
|
||||
array, and validate that no rule's tag set is a subset of an earlier rule's.
|
||||
Recipe: CT-03.
|
||||
|
||||
### Do not let cosmetic tags become gameplay tags
|
||||
|
||||
They may share a type. They must not share authority. A tag contributed by a
|
||||
cosmetic part is client-realizable data; nothing that decides gameplay may read
|
||||
it without an explicit, reviewed promotion.
|
||||
|
||||
---
|
||||
|
||||
## 5. Cosmetic lifecycle tests
|
||||
|
||||
Add and remove one part; several owners contributing; controller changes pawn;
|
||||
pawn destroyed before controller cleanup; replicated add, remove and change; late
|
||||
join reconstructs everything; **dedicated server spawns zero presentation
|
||||
actors**; listen server does not realize twice; invalid part class logs rather
|
||||
than crashes; incomplete tag set falls back.
|
||||
|
||||
---
|
||||
|
||||
# Part II — Teams
|
||||
|
||||
## 6. One authoritative source, and loud mirrors
|
||||
|
||||
Put the team identifier on the player state: it survives respawn, already
|
||||
replicates, and belongs to player identity rather than to a body.
|
||||
|
||||
Controllers, pawns and local players may mirror it. The rules that make mirroring
|
||||
safe:
|
||||
|
||||
- mirrors **subscribe** to the source and re-subscribe when the source object
|
||||
changes;
|
||||
- a write to a mirror is **rejected loudly**, not silently ignored;
|
||||
- a pawn may hold its own value only while unpossessed, and must say so in the
|
||||
error when it refuses.
|
||||
|
||||
The second rule is the one teams get wrong. A silent no-op setter on a mirror is
|
||||
indistinguishable from a working setter until something depends on it. Log an
|
||||
error naming the real owner. Recipe: CT-06.
|
||||
|
||||
---
|
||||
|
||||
## 7. Resolve team from an arbitrary object, in one place
|
||||
|
||||
Centralize the cascade so damage, UI, AI and targeting cannot each grow their own
|
||||
version:
|
||||
|
||||
1. the object implements the team interface;
|
||||
2. a pawn resolves through its controller or player state;
|
||||
3. an actor resolves through its **instigator** — this is what lets projectiles
|
||||
and effects carry the shooter's team without owning one;
|
||||
4. otherwise unassigned.
|
||||
|
||||
### Return a relationship, not a boolean
|
||||
|
||||
```text
|
||||
same team / different teams / indeterminate
|
||||
```
|
||||
|
||||
A boolean conflates "enemy" with "unknown", and the caller then has to guess.
|
||||
Every caller must handle the third case explicitly — and "indeterminate means
|
||||
allowed" is the failure that ships. See `ue-multiplayer-authority` for why an
|
||||
error path that permits is worse than one that denies.
|
||||
|
||||
---
|
||||
|
||||
## 8. One damage policy, used by both authority and prediction
|
||||
|
||||
One function answers "may this instigator damage this target". The damage
|
||||
calculation multiplies by its result as a scalar rather than branching, so the
|
||||
team rule participates in the formula like any other modifier.
|
||||
|
||||
**The same function decides whether to show a hit marker.** That single choice
|
||||
removes the entire class of "the client showed a hit and the server dealt no
|
||||
damage" bugs, because there is no second implementation to disagree with.
|
||||
|
||||
### Make the policy configurable before you need it
|
||||
|
||||
Friendly fire, self-damage, damage to unassigned actors and per-mode overrides
|
||||
are policy inputs. If they are hard-coded inside the damage calculation, a mode
|
||||
that wants different rules requires editing the calculation.
|
||||
|
||||
Watch for the shape where a header comment promises settings that the body does
|
||||
not implement — the comment is a design that was not built, and it reads as a
|
||||
feature. Recipe: CT-07.
|
||||
|
||||
---
|
||||
|
||||
## 9. Presentation as a named parameter bag
|
||||
|
||||
A team display asset should hold **named material parameters** — scalars,
|
||||
colours, textures, a short name — rather than a set of assets. Consumers ask for
|
||||
a parameter; materials are authored to expect the names. Adding a team then costs
|
||||
one asset instead of a duplicated material tree.
|
||||
|
||||
Apply it to meshes, effects and UI. Applying to an actor should include child
|
||||
actors by default — that is precisely what carries team colour onto cosmetic
|
||||
parts, and it is the only place the two halves of this skill touch.
|
||||
|
||||
### Two traps in the applying code
|
||||
|
||||
**Colour conversion drops alpha.** Converting a linear colour to a three-element
|
||||
vector to feed a material parameter silently discards the fourth channel. If any
|
||||
team parameter encodes opacity, it is gone. Recipe: CT-09.
|
||||
|
||||
**A viewer-relative parameter that is ignored.** If the API accepts a viewer
|
||||
identity — "always show my own team as blue" — then the implementation must use
|
||||
it. An accepted-and-ignored parameter is worse than an absent one: it makes the
|
||||
feature look supported and unimplementable at the same time. Recipe: CT-10.
|
||||
|
||||
---
|
||||
|
||||
## 10. Replicate intent, realize locally — the shared principle
|
||||
|
||||
| Domain | Replicated intent | Local realization |
|
||||
|---|---|---|
|
||||
| cosmetics | part definitions | spawned components, meshes, materials |
|
||||
| teams | team identifier | colours, UI, effects, nameplates |
|
||||
| composition | selected definition | activated plugins and actions |
|
||||
|
||||
Realization must be **idempotent**: applying the same intent twice produces one
|
||||
result. This is what makes late join deterministic from compact state.
|
||||
|
||||
---
|
||||
|
||||
## 11. Validation checklist
|
||||
|
||||
### Cosmetics
|
||||
|
||||
- [ ] part class is presentation-only and valid for the body;
|
||||
- [ ] socket exists, or a fallback is defined;
|
||||
- [ ] collision disabled by default;
|
||||
- [ ] handle and spawned component are non-replicated;
|
||||
- [ ] dedicated server realizes nothing;
|
||||
- [ ] every owner retains removal handles;
|
||||
- [ ] possession transfer removes then applies, exactly once;
|
||||
- [ ] rule ordering documented, fallback present, no shadowed rules;
|
||||
- [ ] no gameplay decision reads a cosmetic tag.
|
||||
|
||||
### Teams
|
||||
|
||||
- [ ] one authoritative source, authority-only writes;
|
||||
- [ ] mirrors subscribe, re-subscribe, and refuse writes loudly;
|
||||
- [ ] resolution is centralized and includes the instigator step;
|
||||
- [ ] the indeterminate relationship is handled explicitly at every caller;
|
||||
- [ ] damage and hit feedback call the same policy function;
|
||||
- [ ] friendly fire and self-damage are configurable and tested;
|
||||
- [ ] "private" team data is actually filtered, or is renamed;
|
||||
- [ ] display parameters exist in the target materials;
|
||||
- [ ] a viewer-relative API uses the viewer.
|
||||
|
||||
---
|
||||
|
||||
## 12. Keep them separate
|
||||
|
||||
They meet at one seam:
|
||||
|
||||
```text
|
||||
team identifier → display asset → material parameters on cosmetic parts
|
||||
```
|
||||
|
||||
Do not let cosmetics own the team identifier, and do not let the team system own
|
||||
part selection. Separation is what allows skins independent of teams, players
|
||||
with cosmetics and no team, team recolour over any skin, and viewer-relative
|
||||
presentation without touching authoritative state.
|
||||
|
||||
Combine them only inside a small presentation adapter — never in authority or
|
||||
replication ownership.
|
||||
|
||||
---
|
||||
|
||||
## Provenance
|
||||
|
||||
The findings behind the failure modes come from a source audit of Epic's Lyra
|
||||
Starter Game on Unreal Engine 5.6 — roughly 1300 lines of cosmetics and 1800 of
|
||||
teams, read rather than run. Several of the wiring call sites live in Blueprint
|
||||
graphs, which are binary and were not read; those are marked open rather than
|
||||
concluded, and the skill is written so that none of its claims depend on them.
|
||||
|
||||
Source addresses stay in the research archive. Each entry carries a stable
|
||||
identifier (`CT-01`, `CT-02`, …) resolving back to the audited location there.
|
||||
|
||||
## Evidence boundary
|
||||
|
||||
One project, one engine version, one workspace. The limits are specific and worth
|
||||
repeating: the components are added to actors from data assets that were not
|
||||
read, the team colour application has no C++ caller, and the animation-layer
|
||||
selection is invoked from a Blueprint. Absence of a C++ caller is **not** absence
|
||||
of a caller — see the failure modes for how to check properly before deleting
|
||||
anything.
|
||||
Reference in New Issue
Block a user