add-component
Add a component to Lodestone - the data struct, its reflection registration (which drives serialization, the inspector, MCP, script bindings and replication), any custom handling, and its tests. Use w
SKILL.md.claude/skills/add-component/SKILL.md
GitHubAdd a Component
Components are the data entities carry. The reflection registry is how every engine system learns about them, so a component is only finished when it's registered and tested (see AGENTS.md, "Adding a Component", and docs/Decisions/0010-component-reflection.md).
1. Define the data
- A struct named
<Name>Componentin the module that owns it (core components are inSource/Core/Lodestone/Scene/Components.h), with public PascalCase fields and default member initializers. Components must be default-constructible and copyable - Use the reflected field types for everything that's saved, edited or replicated:
bool,int32_t,uint32_t,float,glm::vec2/3/4,glm::quat,std::string,UUID. Refer to other entities and to assets byUUID, never by handle or path - Data the simulation needs but files mustn't contain goes in a separate component registered as
ComponentFlags::Internal - A component that drives a system with state of its own, outside the registry (as the physics components drive Jolt), needs that system to be a
StatefulSystemthat saves and restores its state - seePhysicsWorldand Decision 0021 - Keep logic out of components: systems (
Simulation::AddSystem) and engine code act on them
2. Register it
In the module's registration function (core: RegisterCoreComponents() in Components.cpp):
registry.Register<LightComponent>("Light", "A point light")
.Field("Color", &LightComponent::Color, {.Description = "Linear RGB", .Min = 0.0, .Max = 1.0})
.Field("Intensity", &LightComponent::Intensity, {.Description = "In candela", .Min = 0.0, .Flags = FieldFlags::Replicated});
- The component and field names are permanent: files, MCP and scripts use them. Renaming one later is a file format change with a migration (
docs/Architecture.md#file-formats) - Give every field a description, and limits where values have them. Defaults must be within the limits
FieldFlags::Replicatedfor what clients need to see;FieldFlags::ReadOnlyfor what only the engine may change;ComponentFlags::Requiredonly for components every entity has
3. Custom handling, only if needed
If the generic path can't express it - like Hierarchy, whose parent is applied through Scene::SetParent when a scene loads - add the handling next to the generic code, and say why in a comment.
4. Test it
In Tests/Core (or the module's test executable):
- The registration: the type is found by name, with the expected fields, types, limits and flags
- A scene with the component saves and loads unchanged (
SceneSerializer::SerializeToTextbefore and after), and invalid values (wrong type, out of range) fail to load with a clear message - If systems change it during the simulation: restoring a snapshot and re-simulating the same input commands gives the same result
- Any custom handling
Then follow build-and-test and ship-change.