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,225 @@
|
||||
# Patterns: tag-addressed input in a measured reference product
|
||||
|
||||
A worked example of the three-layer architecture from `SKILL.md`, measured in one
|
||||
reference project. It exists to answer the question the norms cannot: **what does
|
||||
this architecture actually cost, and what does it actually buy?**
|
||||
|
||||
The short version, and it is the reason this file is worth reading: the
|
||||
architecture works, the composition benefits are real and measurable, and the
|
||||
teardown half was never finished. Those three facts are not in tension. They are
|
||||
what a reference project looks like.
|
||||
|
||||
Markers: **[measured]** — read in source or config; **[derived]** — conclusion
|
||||
from measured facts; **[open]** — not answerable from source alone.
|
||||
|
||||
---
|
||||
|
||||
## 1. The shape, in one chain
|
||||
|
||||
```text
|
||||
key
|
||||
→ mapping context
|
||||
→ input action
|
||||
→ [payload: input tag] handler on the hero component
|
||||
→ ability system component buffers the spec handle
|
||||
~~~ end of the input-processing step ~~~
|
||||
→ controller post-process input
|
||||
→ activation policy decides
|
||||
→ activate
|
||||
```
|
||||
|
||||
Seven hops, five of them across assets **[measured]**. That count is the whole
|
||||
trade: it is why a feature plugin can add controls without touching the base
|
||||
module, and it is why a broken binding takes an afternoon to diagnose.
|
||||
|
||||
---
|
||||
|
||||
## 2. What the split actually buys, with numbers
|
||||
|
||||
| Benefit | Measured evidence |
|
||||
|---|---|
|
||||
| Code cost per new ability control | **zero lines** — one loop binds every row of the config **[measured]** |
|
||||
| Code cost per new native control | one explicit line; the project has exactly five and they have not changed **[measured]** |
|
||||
| Feature-plugin controls without base-module edits | one plugin ships 11 input actions and its own config, and the base module contains no reference to them **[measured]** |
|
||||
| One switch to suppress all ability input | a single tag on the ability system component clears the buffers and returns early **[measured]** |
|
||||
| Same action, different ability per character | the join is a string in two assets; no code path is per-character **[derived]** |
|
||||
|
||||
The fourth row is the one most often underestimated. Without a semantic layer,
|
||||
suppressing input during a menu means either a flag consulted in every handler or
|
||||
unbinding and rebinding at runtime. Here it is one tag, checked once per frame,
|
||||
in one place.
|
||||
|
||||
---
|
||||
|
||||
## 3. What it costs, also with numbers
|
||||
|
||||
**Compile-time errors become runtime silence.** The native path logs an error
|
||||
when a tag cannot be resolved at bind time. The ability path has **no error case
|
||||
at all** — the pressed tag is compared against granted specs, and no match is
|
||||
indistinguishable from not having the ability **[measured]**. This asymmetry is
|
||||
worth internalising as a triage rule: a silent native control is a config
|
||||
problem; a silent ability control could be anywhere along the chain in §1.
|
||||
|
||||
**The lookup is exact, and everything around it looks hierarchical.** Both lookup
|
||||
sites use exact tag matching **[measured]**, while the editor picker filters by
|
||||
category and the vocabulary is a tree. A spec tagged with a parent is never found
|
||||
by a press of its child. Nothing at either end of the join states this. Recipe:
|
||||
IN-01.
|
||||
|
||||
**Two declaration sources for one namespace.** Native tags are C++ symbols;
|
||||
ability tags are config strings; the two sets do not overlap **[measured]**. Both
|
||||
are defensible individually. Together they mean no single artefact lists the input
|
||||
vocabulary. Recipe: IN-02.
|
||||
|
||||
**Linear scans on every press.** The lookup iterates all activatable specs
|
||||
**[measured]**. At the scale of this project that is free; it is proportional to
|
||||
ability count times presses per frame, and neither number is bounded by the
|
||||
design.
|
||||
|
||||
---
|
||||
|
||||
## 4. Teardown: the half that was not finished
|
||||
|
||||
This is the cluster worth studying, because the four findings compound into one
|
||||
outcome and each looks minor alone.
|
||||
|
||||
| Finding **[measured]** | Alone | In combination |
|
||||
|---|---|---|
|
||||
| Bind handles declared as locals at two call sites and discarded | a missed `TArray` member | removal has no data to work with |
|
||||
| The removal function's body is a single TODO comment | an unimplemented method | the feature action's teardown calls into it and returns successfully |
|
||||
| A working `RemoveBinds` helper exists with **zero callers** | dead code | the mechanism was built and could never be reached |
|
||||
| The context action's tracking list is read, iterated and removed from — and never written | an empty loop | contexts come off only reactively, so behaviour depends on teardown order |
|
||||
|
||||
**[derived]** Deactivating a feature leaves its ability bindings on a live pawn.
|
||||
The practical damage is limited because pawns are recreated between matches and
|
||||
because an orphan binding sends a tag that matches no spec — but the leak is real,
|
||||
and a later re-grant of the same ability resurrects phantom input.
|
||||
|
||||
The instructive part is the third row. Someone wrote the removal helper. It is
|
||||
correct. It has never been called, because the handles it needs were thrown away
|
||||
one function earlier. **A correct utility that nothing can call is
|
||||
indistinguishable from a utility that does not exist** — and it is worse, because
|
||||
its presence answers "is removal supported?" with a yes.
|
||||
|
||||
The original authors had also left a comment on the broken tracking line noting
|
||||
that the container is never modified **[measured]**. That is a known defect that
|
||||
shipped, which is the most honest possible evidence for the general rule: *finding
|
||||
a defect and fixing a defect are different projects, and reference code shows you
|
||||
the first.*
|
||||
|
||||
Recipes: IN-03, IN-04, IN-05.
|
||||
|
||||
---
|
||||
|
||||
## 5. One flag, two operations — the sharpest single finding
|
||||
|
||||
In the base initialization path, the call that makes a mapping context **active**
|
||||
sits *inside* the branch that checks whether the context should be registered with
|
||||
the player's settings **[measured]**.
|
||||
|
||||
In the feature-plugin path for the same data structure, the two are separate: the
|
||||
settings registration is skipped by the flag, and the activation call runs
|
||||
unconditionally **[measured]**.
|
||||
|
||||
**[derived]** So the same checkbox means "expose this for rebinding" in one place
|
||||
and "expose this for rebinding **and also make it work at all**" in another. Clear
|
||||
the flag on a base mapping and the controls go silent, with no error and no
|
||||
obvious connection between cause and effect.
|
||||
|
||||
Two properties make this the best example in the file:
|
||||
|
||||
1. **It is verifiable in ten seconds** by reading the two files side by side, and
|
||||
invisible in any amount of reading of either file alone.
|
||||
2. **The correct version exists in the same codebase**, which removes any argument
|
||||
about intent. This is not a design decision applied inconsistently; one of the
|
||||
two is wrong.
|
||||
|
||||
Recipe: IN-06. The same initialization path carries a second nesting defect: the
|
||||
loop over default mapping contexts is nested inside a check for the pawn's input
|
||||
config, so a pawn with no config gets no contexts either and is completely mute
|
||||
**[measured]** — two unrelated features disabled by one condition. Recipe: IN-07.
|
||||
|
||||
---
|
||||
|
||||
## 6. Initialization order as a broadcast dependency
|
||||
|
||||
Initialization clears **every** mapping on the subsystem before adding its own
|
||||
**[measured]**. That includes contexts added by features that activated earlier.
|
||||
|
||||
Recovery works, and it works by broadcast: the component sets its readiness flag
|
||||
and then sends a semantic "bind inputs now" event, which both feature actions
|
||||
listen for alongside the generic extension event **[measured]**. Features re-add
|
||||
what was just removed.
|
||||
|
||||
**[derived]** This is correct and it is fragile in a specific way: correctness is a
|
||||
property of *event ordering* rather than of stored state. Nothing reconciles what
|
||||
was cleared against what was restored, and a feature that is active but not
|
||||
listening for the ready event loses its contexts permanently.
|
||||
|
||||
Worth copying from this: the readiness flag is set **before** the broadcast, not
|
||||
after **[measured]**. A handler that checks readiness during the broadcast sees
|
||||
true. That ordering is one line and it is the difference between the pattern
|
||||
working and not. Recipe: IN-08.
|
||||
|
||||
---
|
||||
|
||||
## 7. Sediment: things that exist and do nothing
|
||||
|
||||
A reference project accumulates these, and each one costs a reader time:
|
||||
|
||||
- two mapping-management functions whose bodies contain only a check and a comment,
|
||||
called from the initialization path, doing nothing **[measured]** — the remains of
|
||||
a superseded settings API;
|
||||
- a user-settings class and a key-profile class registered in config, both of which
|
||||
are empty overrides **[measured]** — extension points presented as features;
|
||||
- a legacy axis-configuration block sitting beside the modern input system, whose
|
||||
deadzone and sensitivity values are overridden by modifiers that read player
|
||||
settings **[measured]**. A reader tuning the legacy values changes nothing.
|
||||
Recipe: IN-09;
|
||||
- a settings-driven modifier that resolves a property **by name through
|
||||
reflection** **[measured]**, so renaming the underlying setting breaks it
|
||||
silently with no compiler involvement. Recipe: IN-10.
|
||||
|
||||
**[derived]** None of these is a bug. Collectively they are the reason "read the
|
||||
reference and copy what it does" is a bad adoption strategy: a meaningful share of
|
||||
what it does is nothing.
|
||||
|
||||
---
|
||||
|
||||
## 8. What the reference got right
|
||||
|
||||
1. **The payload trick.** Passing the semantic tag as a bound-delegate payload is
|
||||
what makes one function pair serve unlimited abilities **[measured]**. It is
|
||||
the single most portable idea in this file.
|
||||
2. **Buffer then activate in one batch.** Collecting handles and activating after
|
||||
all input is processed removes event-order dependence, and the authors left a
|
||||
comment explaining precisely why — held input would otherwise activate an
|
||||
ability and then deliver a press event to it **[measured]**.
|
||||
3. **Setting readiness before broadcasting it** (§6).
|
||||
4. **Separate actions for mouse look and stick look**, with different tags and
|
||||
different handlers, because the stick needs frame-time scaling and the mouse
|
||||
does not **[measured]**. This is semantic distinction done correctly — not
|
||||
device sniffing in gameplay code, but two genuinely different commands.
|
||||
5. **Device switching kept entirely out of the input layer.** The input module
|
||||
contains no reference to input-device types at all **[measured]**; device
|
||||
changes affect icons, haptics and UI style, and never gameplay bindings.
|
||||
|
||||
---
|
||||
|
||||
## Provenance
|
||||
|
||||
Measured against Epic's Lyra Starter Game on Unreal Engine 5.6, read as source and
|
||||
config in a single workspace: roughly 870 lines across the input module, plus the
|
||||
hero component, two feature actions and the input configuration file. Blueprint
|
||||
assets — the mapping contexts and input actions themselves — are binary and were
|
||||
not read, so every statement here about which keys map to what is **[open]**.
|
||||
|
||||
Source addresses stay in the research archive that produced this skill; each
|
||||
`IN-` identifier resolves back to the audited location there.
|
||||
|
||||
## Evidence boundary
|
||||
|
||||
These are examples and failure evidence from one project on one engine version,
|
||||
not guarantees about others. The counts are properties of that project on that
|
||||
day and are quoted to show the shape of a contrast, not to be imported. Re-run the
|
||||
recipes against your own tree.
|
||||
Reference in New Issue
Block a user