Frostdev

Kiln · For AI agents

Teach your agent
to build Kiln mods.

A skill for Claude Code and other Agent Skills tools: setup, the mod API and its thread rules, data packs, talents, quests, sounds, the dev kit, and the mistakes to avoid.

13 files · 108 KB of Markdown · updated November 12, 2026

Claude Code

Unzip it into ~/.claude/skills/ for all your projects, or .claude/skills/ in your mod’s repository.

Claude apps

Upload the zip where you add skills.

Other agents

Put the kiln-modding folder where your tool looks for skills. It’s plain Markdown.

What’s inside

kiln-modding/SKILL.mdWhen to use it, the workflow, the rules that matter, the checks before done.Raw
---
name: kiln-modding
description: Build, test and debug Enshrouded mods with Kiln.Sdk, C# APIs, JSON blocks, data packs and assets. Use for Kiln mod projects, the kiln CLI, local test servers, hot reload, lint and mod-load failures; not for Kiln launcher or framework internals.
---

# Kiln modding

Kiln is a mod framework and launcher for Enshrouded. Mods are C# assemblies (.NET 10) that Kiln loads into the dedicated
server and the game client, plus optional JSON data and asset files. This skill covers mods, not Kiln's own services or the
launcher's code.

Enshrouded is a trademark of Keen Games GmbH. Kiln is unofficial.

## Before you start

1. Read the page for what you're using: https://frostdev.io/kiln/wiki. [references/docs-map.md](references/docs-map.md) says
   which one.
2. The current build is **0.2.0-dev.26**, with mod API 0.2.1 and the matching dev.26 MDK pack. References marked **dev.20+**,
   **dev.21+** or **dev.22+** require that runtime and MDK or newer; dev.19 lacks the dev.20 additions. Check the installed build.
   Items marked **dev.26+** are in development, not released: they need a dev.26 build, and none has been seen by a player
   (contract and capabilities, typed ECS reads, `Kiln.TestKit`, `[ClientCheck]`, `SteamGuard`, `InstanceData`, player
   animation states, weapon moves, the movable data folder).
   Server/offline verification does not establish the pending client rendering, input or multiplayer checks. Many dev.21 to dev.24
   paths were verified only under Wine (tests, dedicated-server labs), not on Windows, a Steam Deck or in a real client; of dev.24
   only Kiln's main-menu entries with ShroudForge mods on, W and S steering with ShroudForge's Unlimited Flight and SET's
   tools were seen in a real game (Linux, Proton, one client). The official dev.22 launcher has Nexus integration disabled:
   import downloaded archives manually; no Catalog, account sign-in or `nxm://` handler.
3. Decide the side and what players need, before writing code:
   - `side`: `server` (default), `client`, or `both`;
   - `clients`: `none` (vanilla players can join), `kiln` (players need Kiln; the server sends definitions), or `mod`
     (players need this mod, from a server modpack). A stated `kiln` or `mod` makes servers refuse vanilla clients.

## Workflow

1. **Scaffold**: `kiln new <template> <Name> --server <server>` (`kiln new --list`; or `dotnet new kiln-mod`). Never by hand.
   [references/setup.md](references/setup.md).
2. **Write**: to make something new, building blocks first (`kiln new spell`, `Blocks.Define`, `blocks/*.json`; [references/docs-map.md](references/docs-map.md)): no GUIDs, no base items. `kiln blocks show <name>` says what a block became. Data over code where you can.
   [references/blocks.md](references/blocks.md) for the block families, then [references/data-packs.md](references/data-packs.md),
   [references/quests-talents-sounds.md](references/quests-talents-sounds.md),
   [references/vfx.md](references/vfx.md),
   [references/api.md](references/api.md).
3. **Check names**: `kiln catalog search`. Never guess game ids or field paths.
4. **Build and deploy** to a local test server: `kiln build && kiln deploy`, then `kiln run`.
5. **Check the log**: `kiln logs --mod <id>` for `mod <id> <version> loaded in … ms`, or the reason it didn't load.
6. **Hot reload** after each change: `kiln deploy --reload`.
7. **Test** in the server with `[KilnTest]` methods: `kiln test`. dev.26+: the same methods offline under `dotnet test`
   (`Kiln.TestKit`), and `[ClientCheck]` for what a player must see. [references/testing.md](references/testing.md).
8. **Package**: `kiln pack`.

`--json` on any `kiln` command gives one JSON document; exit 0 ok, 1 failure, 2 usage.
[references/devkit.md](references/devkit.md).

Never deploy to a server players are using. Use a separate local test server.

## Rules that matter

- **Tick thread.** `OnLoad`, `OnUnload` and ordinary server event handlers run on the tick thread; their `await`
  continuations return there. Game state must be touched there. ECS/system-hook callbacks instead run on game workers;
  see [references/world-programs.md](references/world-programs.md). From other threads, use `Context.Scheduler.Post(...)`
  or `await Context.Scheduler.NextTick()`. Never block the tick thread (`.Result`, `.Wait()`, `Thread.Sleep`).
- **Client threads.** Client-world work runs on the main thread (`Client.Tick`, `Client.Post`). Draw functions run every frame:
  keep them short.
- **Register in `OnLoad`, work later.** On a client `OnLoad` can run in the main menu. Do world work from
  `ServerStartedEvent` or `Client.Tick`.
- **Registrations belong to one mod lifetime.** Do not retain callbacks or handles from an old load. Release threads, timers,
  sockets, files and static references held by non-mod code. ECS bindings drain callbacks at unload but keep an inert scheduler
  entry until restart; see [references/world-programs.md](references/world-programs.md).
- **Item state (dev.20+).** Use `Items.Move` for gear transfers, not take + give. Item-entity `EntityData` is session-only.
  dev.26+: `InstanceData` keeps a value on one item (a player's inventory not yet seen by a player); no affix API.
  [references/api.md](references/api.md#persistent-data).
- **Changes are layers.** They come off when the mod unloads. Do content changes in `Content.OnReady(...)`.
- **Fixed ids.** GUIDs of items, recipes, clones, perks, buffs, tree nodes and NPCs, and ids of quests, steps and talents, never
  change after release. Saves keep them. For native quests and dialogue, names and option order feed the ids too.
- **Fail soft.** Check `X.Available` and degrade: services can be missing on a build Kiln doesn't know. dev.26+:
  `Capabilities.Is(name)` / `Why(name)`; an off capability throws `KilnUnavailableException`.
- **Never ship Kiln assemblies.** Kiln.Sdk references them with `ExcludeAssets=runtime`.
- **Never ship game data.** Models, sounds, Impact programs and catalogs are made on the user's machine. Ship your sources and a
  `cook.json`. `assets/cooked/` stays out.
- **Faults disable your mod, not the server.** 5 exceptions in 60 s by default. Fix the cause, then `reload <id>`.
- **Say what you haven't seen.** A dedicated server draws nothing and has no sound. Report what only a player can see or hear as
  "needs a client check" until you've seen it. What Kiln itself has confirmed: https://frostdev.io/kiln/wiki/status.

Mistakes seen in practice: [references/pitfalls.md](references/pitfalls.md). What has no API yet, hard caps and the
planned release: `docs/kiln/modding/limits.md`.

## Checks before you call it done

- `kiln build` passes with no `KILN0xx` errors and no `KILN1xx` warnings you didn't mean.
- `kiln lint` is clean (with a catalog: `kiln catalog build`).
- The log shows the mod loaded, no `[E] [mod:<id>]` lines and no data-pack problems.
- `kiln test` passes, also after a hot reload.
- `echo "reload-test <id> 10" > kiln/control/rt.cmd` reports no leaked load contexts.
- An advanced mod: `kiln/state/hooks.json` shows your link on the right function (and `linkSlots.used` does not grow on each
  command), and kiln.log has no `hooks:` warning you didn't expect.

## When something isn't documented

Don't guess at game internals. Read the wiki and the XML docs that ship with Kiln.Api. If a service doesn't offer what you need,
say so instead of patching game memory. Low-level hooks are for mods that opt into the advanced tier (`"advanced": true`);
the short rules (kiln.hooks v2: link order, `caller()`, link slots, `hooks.json`; closed DLL mods and `[late]`; EML and EMF
notes) are at the end of [api.md](references/api.md).
kiln-modding/references/setup.mdKiln.Sdk, templates, project properties, build targets.Raw
# Project setup: Kiln.Sdk, templates, build targets

Wiki: [dev kit](https://frostdev.io/kiln/wiki/dev-kit), [your first mod](https://frostdev.io/kiln/wiki/first-mod).

`Kiln.Api`, `Kiln.Sdk` and `Kiln.Templates` share the full MDK pack version: `0.2.0-dev.24` for this release, or a unique
`0.2.0-dev.local.<date>.<time>` for a local pack. `0.2.0` is only the source's base version. These packages use a local feed,
not nuget.org: pass the MDK feed as `--feed` or `$KILN_FEED`. Use the actual pack version when editing a project by hand.

## New project
```sh
kiln new mod MyMod --feed <feed> --server <server folder> --set tests=true
# same thing:
dotnet new kiln-mod -n MyMod --feed <feed> --server <server folder> --tests --services "Players;Chat"
```

| Template | Gives you | Side, clients |
|---|---|---|
| `mod` | a code mod | server, none |
| `datapack` | `content/*.json` | server, none |
| `items` | new items and recipes | both, mod |
| `creature` | a creature clone | both, kiln |
| `ability` | an ability; `impact=true` adds a program | both, none |
| `ui` | a game UI overlay | client, mod |
| `model` | a glTF model and recipe | both, mod |
| `sound` | sounds, cues and recipe | both, mod |
| `talents` | a talent tree | both, mod |
| `native` | a C++ mod | both |
| `test` | a test mod; `target=<mod>` | server, none |

**dev.20+ block templates:** `item-blocks`, `world-blocks`, `creature-blocks`, `npc`, `cosmetic`,
`building-material`, `ability-blocks`, `quest`; `ui --set blocks=true` selects UI as data. Use `kiln new --list` from the
installed MDK to confirm availability. These add typed blocks and/or `blocks/*.json`; keep the form you need. `items` and
`creature` remain the clone-based path when a block has no field for the change. [blocks.md](blocks.md).

**dev.26+ (in development):** `dotnet new kiln-unit-test -n MyMod.Tests --mod ../MyMod/MyMod.csproj` makes an xunit project on
the `Kiln.TestKit` package ([testing.md](testing.md)). `Game` in `KilnServices` references `Kiln.Game` (`Kiln.Ecs` typed reads);
the build then writes `kiln.gametypes.json` beside the mod, which Kiln checks at load. Release packs carry their full version
(`0.2.0-dev.26`) and an SDK feed folder (`sdk/feed/`). The launcher can move its data folder (Settings, Library, Data
folder); after a move, delete a native mod project's `build` folder once (CMake keeps the old SDK path). Not run in a real
launcher window yet.

The project file:
```xml
<Project Sdk="Kiln.Sdk/0.2.0-dev.24">
  <PropertyGroup>
    <KilnModId>my-mod</KilnModId>
    <Version>1.0.0</Version>
    <KilnServices>Players;Chat</KilnServices>
  </PropertyGroup>
  <ItemGroup>
    <KilnDependency Include="sample-lib" Version="^1.2" />
  </ItemGroup>
</Project>
```

## Properties (each maps to a manifest field)
| Property | Default | Manifest |
|---|---|---|
| `KilnModId` | lower-case `AssemblyName` | `id` |
| `KilnModName`, `Version` | `AssemblyName`, 1.0.0 | `name`, `version` |
| `Authors`, `Description`, `PackageLicenseExpression`, `PackageProjectUrl` | | `authors`, `description`, `license`, `url` |
| `KilnApiVersion` | the SDK's (0.2.1) | `apiVersion` |
| `KilnSide` | server | `side` |
| `KilnClients` | from the side: server none, else mod | `clients` |
| `KilnEntryType` | the only public `Mod` subclass | `entryType` |
| `KilnAdvanced` | false | `advanced` |
| `KilnFeatures`, `KilnExports` | | `features`, `exports` |
| `KilnTestsAutoRun`, `KilnTestTimeoutSeconds` | | `tests` |

Not in the manifest:
| Property | Does |
|---|---|
| `KilnServices` | services to reference: Abilities, Assets, Audio, Build, Chat, Client, Combat, Content, Creatures, Gameplay, Items, Network, Permissions, Players, Quests, Saves, Settings, Skills, Spawn, Time, Ui, World; and `Testing` |
| `KilnServer`, `KilnModsDir` | deploy target |
| `KilnReload` | KilnDeploy also reloads the mod |
| `KilnModFolders` | data folders copied into the mod; includes `blocks`, content, assets, skills, quests, icons, Impact and audio sources; inspect the installed SDK for the full list |
| `KilnPackExclude` | left out of the zip; default `assets/cooked/**` |
| `KilnAnalyzers` | false: no KILN1xx |
| `KilnGameIds`, `KilnCatalog` | game-id constants |

Items: `KilnDependency` (`Version`, `Optional`), `KilnLoadBefore`, `KilnLoadAfter`, `KilnIncompatible` (`Version`).

## kiln.mod.json
None in the project folder: the build generates one. One there: the build validates and copies it. The loader's own parser checks it.
`KILN001` invalid, `KILN002` wrong `entryAssembly`, `KILN004` unknown service, warning `KILN003`. Fields:
[manifest.md](manifest.md).

## Targets
`kiln` drives these: `dotnet build` (the output folder is the mod folder), `-t:KilnDeploy -p:KilnServer=<server>`, `-p:KilnReload=true`,
`-t:KilnPack`.

NuGet caches each package id/version. Give each local pack a new full version and update the project to it; re-packing
the same version can leave the project using its old cached packages.

## A finished mod
```
kiln/mods/my-mod/
  kiln.mod.json
  MyMod.dll  MyMod.pdb       (+ NuGet dependencies)
  content/*.json             data packs
  skills/*.json              talents
  blocks/*.json              building blocks (including quests/UI on dev.20+)
  quests/*.json              quests, NPCs, dialogue
  kiln.programs.json          optional dev.20+ boot ECS declarations
  assets/assets.json         icons, textures, resources
  assets/cook.json           recipe; assets/cooked/ is made on the player's machine
  audio/  impact/  icons/    sources
```
A `.disabled` file in the folder switches the mod off at the next start.
kiln-modding/references/api.mdThe mod API by area, with the thread each part runs on.Raw
# The mod API, by area

Namespace `Kiln` for everything. Each service is its own assembly: list it in `KilnServices` (setup.md). Signatures here are the
common ones; the XML docs and the wiki have the rest. Talents, quests, NPCs and sounds:
[quests-talents-sounds.md](quests-talents-sounds.md).

## Threads, in one table
| Code | Thread |
|---|---|
| `OnLoad`, `OnUnload`, `Context.Tick.Pre/Post`, `[Subscribe]` handlers, IMC handlers, command handlers, `await` continuations in mod code | server tick thread (dedicated server, or the hosted world on a client) |
| `Client.Tick`, `Client.Post`, network handlers on a client, key binding handlers | client main thread |
| `Ui.Menus` clicks | game UI thread (main thread), after the frame |
| screen draw functions, crafting panels, `Ui.Frame`, `Hud.Show` text functions | render thread, every frame: draw only |
| `EcsPrograms.Bind` callbacks (dev.20+) | game worker thread; declared component/event access only |
| `Abilities.Natives.Wrap`, `Abilities.OnDamage` | job thread inside the sim step: touch only the call |

Back to the tick thread: `Context.Scheduler.Post(action)` or `await Context.Scheduler.NextTick()`. Check with
`Game.IsTickThread` / `Game.AssertTickThread()`. On a client in the main menu there is no world: `Scheduler.Post`
work waits for the first world step.

## The API contract (dev.26+, in development; not seen by a player)
dev.26 is not released: these need a dev.26 runtime and MDK. Page: `docs/kiln/modding/contract.md`.
- **One error rule.** The game refused: a result with `Ok = false` and `Reason` (`IKilnResult`). A bad argument:
  `ArgumentException`. A capability that is off: `KilnUnavailableException` (`Capability`, `Reason`; a
  `NotSupportedException`). Few results implement `IKilnResult` yet (`PlayerAnimDefineResult`), and older services still
  throw their own exception types: catch `NotSupportedException`.
- **Capabilities.** `Capabilities.Is(name)`, `Get(name)` (`Capability`: `State`, `Reason`, `ReasonText`, `Source`,
  `SessionDependent`, `IsAvailable`), `Why(name)` (one sentence to log), `All`; any thread. `CapabilityChangedEvent`
  on the tick thread. `Game.HasFeature(name)` asks the same table. Names: every sigdb feature, `kiln.<service>`,
  `identity.entities`, `identity.players`, `items.*`, `saves.instance_data`, `anim.player_states`, `blocks.weapon_moves`.
  A row a managed service registers exists only after its first use (Kiln.Items: the first tick; item blocks: the
  first compile), so name only sigdb features, `kiln.<service>` and `anim.player_states` in a manifest's `features`;
  check the rest with `Capabilities.Is` at run time. `[RequiresCapability(name)]` marks members of Kiln.Items, item data,
  Kiln.PlayerAnim and the move blocks; it is documentation: the member's own code throws, nothing checks the attribute.
- **Identity.** `EntityRef(EntityId Id, ulong Serial)`: `EntityRef.Of(id)`, `IsAlive`, `HasSerial`, `IsNone`;
  converts implicitly to `EntityId`. `PlayerRef(ulong SteamId, Guid CharacterId, EntityRef Entity)`: `PlayerRef.Of(entity)`,
  `Resolve()`, `IsOnline`, `IsSamePerson(other)`; explicit conversions from `Player`, `TalentHolder`, `QuestHolder`,
  `CharacterKey`.
  `ItemInstanceRef(EntityRef Entity, ItemId Item)` (not Kiln.Blocks' `ItemRef`). `ItemId` moved to Kiln.Api (old builds
  load unchanged). Tick thread, from the first tick, not in `OnLoad`. Sources: players, item entities, every entity
  kiln.spawn can inspect. An entity loaded from the save with no `SpawnTime` has serial 0 (`HasSerial` false): its
  `IsAlive` only says the id exists. Test `IsNone`, not `== None`. Refs ran on a test server; player refs with
  simulated players only.
- **Side and thread markers:** `[ServerOnly]`, `[ClientOnly]`, `[AnyThread]`, `[RenderThread]`, `[WorkerThread]`
  beside `[TickThreadOnly]`. Markers only; few members carry them yet.
- **API version:** `Mods.RuntimeApiVersion` is the running Kiln's mod API. `Mods.ApiVersion` is obsolete: a const,
  copied into your mod at build time. Release builds carry their full version (`0.2.0-dev.26`).
- **`[Experimental]` ids:** `KILNX001` lab and test members (hidden; a build error unless the project adds it to
  `NoWarn`), `KILNX002` API not yet proven in a real client. Neither is covered by the additive-API promise.

## The mod
```csharp
public sealed class MyMod : Mod
{
    public override void OnLoad()
    {
        Log.Info($"{Context.Info.Name} {Context.Info.Version} (Kiln {Game.KilnVersion}, phase {Lifecycle.Phase})");
        Context.Tick.Pre += OnPre;                     // (in TickInfo t): t.Step, t.ClockDelta, t.WallDeltaMs
        Context.GameLog.Line += (level, text) => { };  // the game's log, on the tick thread
        _ = LaterAsync();
    }
    private void OnPre(in TickInfo t) { }
    private async Task LaterAsync() { await Context.Scheduler.NextTick(); /* still the tick thread */ }
    public override void OnUnload() { }                // release threads, files, sockets you created
}
```
- `Context`: `Info` (the whole manifest as `ModInfo`), `Log`, `Tick`, `GameLog`, `Scheduler`, `Messages`,
  `DataDirectory` (`<dataDir>/<id>`: your files).
- `Game`: `KilnVersion`, `IsTickThread`, `HasFeature(name)`, `AssertTickThread()`. dev.26+: `HasFeature` is
  `Capabilities.Is`.
- The dedicated server steps about 24.5 times a second (~41 ms). 1470 steps ≈ 1 minute.

## Lifecycle events (Kiln.Api; tick thread; not cancelable)
`ModsLoadedEvent` (once, after the last `OnLoad`), `ServerStartingEvent`, `ServerStartedEvent` (`Step`, `Source`:
the world is live, players can join), `ServerStoppingEvent` (`Reason`: save your state here, the world is intact),
`ModReloadedEvent`, `ModDisabledEvent`. `Lifecycle.Phase` (`Booting`, `Starting`, `Running`, `Stopping`, client
`Idle`), `Lifecycle.IsServerRunning`, `Lifecycle.StartedAtStep` tell a hot-loaded mod what it missed.

## Other mods
`Mods.All`, `Mods.IsLoaded(id)`, `Mods.Get(id)` (`ModEntry`: `State`, `Error`, `Version`, `Stats`), `Mods.Current`,
`Mods.Side`, `await Mods.ReloadAsync(id)` / `UnloadAsync(id)` / `LoadAsync(folder)`.
Messages: receiver `Context.Messages.On("method", m => ... m.PayloadAs<T>() ... m.Sender)`; sender (any thread)
`Mods.Send("target-id", "method", payload)`. Delivered on the receiver's next step. Payloads: BCL types, Kiln types or
types from an assembly the receiver exports. Shared APIs: put them in `MyLib.Api.dll`, list it in `exports`,
dependents reference it with `Private="false"`. Reloading a provider reloads its dependents.

## Events
```csharp
[Subscribe] private void OnJoin(PlayerJoinedEvent e) => Log.Info($"{e.Player.Name} joined");
[Subscribe(EventPriority.High, ReceiveCanceled = true)] private void OnChat(ChatMessageEvent e) { e.Cancel(); }
var sub = Events.On<CommandExecutedEvent>(e => { });   // sub.Dispose() to stop
Events.Post(new MyEvent());        // tick thread; Events.Enqueue(ev) from any thread
```
Priorities `Highest, High, Normal, Low, Lowest, Monitor`. `CancelableEvent.Cancel()`, `ResultEvent` (`Allow`,
`Deny`). Build your own events only when `Events.HasSubscribers<T>()`. Each service page lists its events with
thread, cancel meaning and writable fields: read it before relying on one. Some player-facing events are built but not yet
confirmed with a real player.

## Configs
```csharp
public sealed class MyConfig
{
    [Comment("How many homes a player may set."), Range(1, 50)] public int MaxHomes { get; set; } = 3;
    [RequiresRestart] public int Port { get; set; } = 8080;
    [Synced] public double DamageFactor { get; set; } = 1.0;   // the server's value on connected Kiln clients
}
_cfg = Config.Register<MyConfig>();            // ConfigKind.Common -> <configDir>/<modid>.json (Server / Client kinds too)
_cfg.Changed += e => { };                      // or [Subscribe] ConfigChangedEvent
var n = _cfg.Value.MaxHomes;                   // always current
```
Commented JSONC, validated, clamped, hot-reloaded on edit. Never build the config path yourself: `Config.Directory`.
**dev.20+:** `Config.RegisterWorld<T>()` keeps one config per world in its sidecar; values switch before `WorldLoadedEvent`
and use defaults with no world. `[RequiresRestart]` edits apply when that world next opens. `Context.Locale.Get(key, args)`
reads this mod's `locales/<language>.json`, falling back to English/key; use it for mod text, not game-tag lookup.

## Commands and permissions
```csharp
[Command("heal", Description = "heals a player", Permission = CommandPermission.Admin)]
private string Heal(CommandContext c, Player target, [Range(1, 1000)] int amount = 100) => $"healed {target.Name}";

Commands.Register(Commands.Literal("give").Then(Commands.Argument("player", Arg.Player())
    .Then(Commands.Argument("item", Arg.Item()).Executes(c => Give(c.Get<Player>("player"), c.Get<ItemInfo>("item"))))));

Permissions.Register("mymod.fly", PermissionLevel.Everyone, "fly around");
if (Permissions.Has(player, "mymod.fly")) { }
```
Arg types: `Int`, `Double`, `Bool`, `Word`, `String`, `Greedy`, `Enum<T>`, `Choice`, `Player(s)`, `Entity(ies)`
(`@s @a @p @r @e[...]`), `Position` (`~` relative), `Item`, `Duration`, `TimeOfDay`. `/help` and completion are
built in. Console: `kiln/control/console/*.cmd`.

## Chat
`Chat.Received += m => { m.Cancel = true; }`, `Chat.Broadcast(text)`, `Chat.Tell(player, text)`,
`Chat.Say(player, text, to)`, `Chat.Players`, `Chat.FindPlayer(name)`. Tick thread for sends.

## Players and progression
`Players.Available`, `Players.All`, `Players.Find(name | EntityId)`, `FindBySteamId`, `PositionOf`,
`Players.Joined/Left` (`Action<Player>`). Progression: `player.Progression.Read()`, `AddExperience(n, source)`,
`SetLevel`, `GiveSkillPoints(n)` (up to the game's givable flags), `Unlock(node)`, `Lock`, `Respec()`;
`PlayerLevelChangedEvent`, `PlayerSkillsChangedEvent`. Changes return `Task<ProgressionResult>`; game refusals come
back as `Ok = false`, not exceptions. Level cap 50. New nodes and trees: [talents](quests-talents-sounds.md).

Player state (any of 350 states from the game's own enums, `docs/kiln/modding/player-states.md`): `player.Is(PlayerStateId.Soaked)`, `player.IsGliding`, `IsSwimming`, `IsAttacking`..., `player.Snapshot()` (`Health`, `Mana`, `Stamina`, `ShroudTimeLeft`, `Action`, `Active`), `player.Equipment`, `player.Buffs`. Live on the tick thread. Events: `PlayerStateChangedEvent` (any state), `PlayerActionEvent` (any action sequence, `Cancel` stops it), `PlayerConsumeEvent` (`e.Item == GameItems.Food_...`, `Cancel`: item stays), `PlayerConsumedEvent`, `PlayerClientActionEvent` (any client command), `PlayerJumpEvent`, `PlayerGlideEvent`, `PlayerEquipmentChangedEvent`, `PlayerHotbarChangedEvent`. Check the dated wiki status for player-facing verification.

## Items, spawn, world, time, settings, combat
- Items: `Items.Lookup(name)`, `Items.Search`, `await Items.Give(target, id, n, gear, options)`,
  `Items.Read(inventory)`, `await Items.Take(...)`, `Items.FindInventories(kinds)`.
- Spawn: `Spawn.Lookup(template)`, `await Spawn.Entity(template, pos, yaw, options)`, `Spawn.Item`,
  `Spawn.Group`, `Spawn.Inspect`, `Spawn.Destroy`, `Spawn.Tracked`, `Spawn.CleanupTracked(tag)`. Check
  `Spawn.WorldRunning` first. dev.20+: `SpawnOptions.Components` takes up to 64 `SpawnComponent.Of(name, bytes/value)`
  entries applied with the spawn, before its first tick; check `Spawn.SupportsComponents`. Exact reflected sizes are
  required, and `SpawnTime` needs `SpawnFlags.NoTrack`. This is distinct from queued component changes later.
- World: positions are `WorldPosition` in metres; teleport and entity reads.
- Time: `Time.State`, `Time.Set("19:45")`, `Time.ResetWeather()`. Set the time only after the world runs
  (`ServerStartedEvent`), or the save load overwrites it.
- Settings: `Settings.Get(name)`, `Settings.Set(name, value)`, `TrySet`, `Settings.All`.
- Combat (dev.20+): `Attributes.Get(entity, GameAttribute.Stamina)` and `Set` / `Add` (also Mana, BodyHeat, Oxygen);
  check `Available`/read results. These use the game attribute path, on the tick thread.
- Combat: `Combat.GetHealth`, `SetHealth`, `Heal`, `await Combat.Damage(target, n, attacker)`, `Kill`,
  `SetInvulnerable`; `EntityHurtEvent` (cancel, change `Amount`), `EntityDeathEvent`, ...
- Gameplay (dev.20+): `Offers.Add/Remove/IsActive/Status` puts an E prompt on a suitable world entity, owned by this mod;
  it needs collision and cannot already have a game/other-mod offer. Check availability/status; pending is not visible proof.
- Gameplay: `InteractEvent`, `CraftEvent`, `LootEvent`, `TeleportEvent`, `NaturalSpawnEvent`,
  protection zones (`Gameplay.Protect`).

## Inventory transfers (dev.20+)
`await Items.Move(from, slot, to, count: 0)`, `MoveAll(from, to)`, `QuickStack(from, to)` keep the same gear entity and its
level, upgrades, gem slot and durability. A take followed by a give makes new gear. `MoveResult` reports refusals and a
bounded `Changes` ledger. Check `Items.MovesAvailable`; run on the tick thread and await completion there.

`MoveOptions.DryRun` rolls back and emits no transfer event; `Equipped` and `ActionBar` opt into those slots; `Silent` skips
transfer rules/events. `Items.ContainerName(chest)` reads a player's name, never renames; check `ContainerNamesAvailable`.
`Items.Transfers.Keep(slot)`, `KeepItem(item, owner)`, `Deny(container, TransferKinds.QuickStack)` return disposable rules.
`InventoryTransferEvent` is a report after quick stack/move all, not cancelable. To cancel an incoming player request,
first require `e.Name == "inventoryTransferAction"` and `e.CanCancel` on `PlayerClientActionEvent`; then parse `e.Payload`
with `Items.Transfers.ParseAction` and cancel the held action. The parser does not identify an action from its bytes.
Joined-client events cannot cancel the server's action, and a Quick Stack Station has no such request. Rules and
subscriptions end with the original mod lifetime.
dev.21+: a native `transfer_listen` handler (kiln.items) also runs on a dry-run move (not a `Silent` one) with
`KILN_TRANSFER_DRY_RUN` in `ev->flags`. Keep and cancel as usual; count, log and change nothing outside the transfer.
Wiki: [inventory](https://frostdev.io/kiln/wiki/inventory).

## Persistent data
`WorldData.Get<T>(key)` (live object: change it in place; saved with the world), `PlayerData.Get<T>(steamId, key)`,
`EntityData.Get<T>(entity, key)` / `Set`. Tick thread. JSON via System.Text.Json. Keys are per mod. Kept across hot
reloads. `WorldSavingEvent` runs before the game's snapshot. Backups: `Saves.Request()`, `Saves.Backup(...)`.
In a hosted world, `OnLoad` sees empty world data until `WorldLoadedEvent`: read per-world data from that event.
**dev.20+:** `EntityData.IsPersistent(gearEntity)` is false. Gear has no stable world-position identity: its data follows a
move within the session, not a restart. Kiln skips these attachments on save and refuses old positional gear records.
World entities such as saved chests retain their normal persistence. dev.24 and older supply no persistent per-item data.

**Data on one item (dev.26+, in development; a player's inventory not seen by a player):** Kiln.Saves.
```csharp
_kills = InstanceData.Key<Kills>("kills");          // OnLoad; options: new InstanceKeyOptions { Persist = Persist.Session }
var sword = Items.Held(player);                     // ItemInstanceRef; also Items.Equipped(player, EquipmentSlot.ArmourHead),
sword.Get(_kills).Count++;                          //   InventorySlot.Ref, give/take/move results' Ref
```
- `Get` (the live object, saved with the world), `TryGet`, `Has`, `Set`, `Remove`; `InstanceData.All(key)`,
  `InstanceData.Parked(key)` (waiting or set aside, with the reason). Tick thread.
- Events: `InstanceDataChangedEvent` (a `Set`/`Remove`), `InstanceDataRestoredEvent` (`Exact`, `Moved`),
  `InstanceDataParkedEvent`, `InstanceDataLostEvent` (`Destroyed`, `Unmatched`, `Ambiguous`, `Capped`). Kiln.Items:
  `ItemMovedEvent` (`Transaction`, `Transfer`, `Observed`), `item.Location()` (`ItemLocation`). Admins: `/instances`.
- When Kiln is not sure which of two identical items a value belongs to, it sets the value aside, never on another item.
  Identical items swapped where Kiln did not see it (another world, an offline edit) trade their data.
- `Persist.Carried` throws `KilnUnavailableException("saves.instance_data.carried")`: not in this release. Plain stacks
  have no item entity. 256 records per holder per mod, 64 KB per value, waiting records dropped after 3 sessions.
- Chests proven on a test server; backpack, action bar and worn items wait for the guided client pass. Affixes: none (perks
  come from the item type). Page: `docs/kiln/modding/inventory.md` "Data on one item"; sample `mods/samples/ItemData`.

## Content and game data
```csharp
Content.OnReady(_ =>                              // after the packs, at every world load and pack reload
{
    Content.Items.Get("Material_T1_Wood_Logs")!.MaxStack = 2000;
    Content.Recipes.Get("Tool_basic_FishingRod_T0_PK")!.CraftingDuration = TimeSpan.FromSeconds(5);
    var r = Content.Apply("""{ "items": { "Material_*": { "maxStackSize": { "mul": 2 } } } }""");
});
Content.Items.Register(new ItemDefinition { Guid = ..., Name = "MyMod_FrostEdge", BaseOn = "Weapon_T1_1H_SwordEpic_01",
    DisplayName = "Frost Edge", Fields = new() { ["damageSetup.dmgMod"] = 1.5, ["rarity"] = "Rare" } });   // in OnLoad
Content.Recipes.Register(new RecipeDefinition { Guid = ..., Name = "MyMod_FrostEdge_Recipe", Station = "hand",
    Inputs = [("Material_T1_Twigs", 2)], Outputs = [("MyMod_FrostEdge", 1)] });
```
Registries: `Content.Items`, `Recipes`, `Loot`, `Drops`; game data: `Content.Templates`, `Arsenals`, `Sequences`,
`Buffs`, `Perks`, `Skills`. `entry.Get(path)`, `Original(path)`, `Set(path, value)`, `Reset(path)`.
`Content.Dump()` writes every registry with its fields to `kiln/data/content/dump.json`: look names and paths up
there instead of guessing. `Content.Status()`, `Content.PackReport()`.
dev.21+: `Overrides.Claim`/`Release`/`RestoreAll` from a thread where no mod's code runs (`Task.Run`, a timer) throw
`InvalidOperationException`; post to the tick thread with `Context.Scheduler.Post`. A `ReflectedResources` handle got in
`OnLoad` stays your claim from any thread. `Assets.Reflect(key)` returns `null` on a build without kiln.types.

## Typed ECS reads (dev.26+, in development; not seen by a player)
`<KilnServices>Game</KilnServices>`, then `Kiln.Ecs` (Kiln.Game): a typed struct for every component and event of
the game, generated per verified build (`Ecs.Health`, `Ecs.LastTransform`, `Ecs.Enemy01_State`, `Ecs.FactionType`).
```csharp
if (!Ecs.Available) return;                                   // kiln.world off: the reads would throw
if (!Ecs.Supports<Ecs.LastTransform>()) { Log.Warn(Ecs.WhyNot<Ecs.LastTransform>()!); return; }
if (Ecs.TryGet<Ecs.LastTransform>(entity, out var t)) Log.Info($"x {t.transform.position.x}");
foreach (var e in Ecs.All<Ecs.Health>()) { }                  // also Ecs.Get<T>(e), Ecs.Has<T>(e)
```
- `TryGet`, `Get`, `Has`, `All`: tick thread; `Available`, `Supports`, `WhyNot`: any thread.
- Each type is checked against the running game at first use. A mismatch reads as nothing (false, null, empty) with
  one warning: never a partial read. Kiln also stamps the mod (`kiln.gametypes.json`) and refuses it by name at load
  when a type it uses changed; an enum constant the compiler folds into a number is not stamped.
- Reads only: a struct is a copy. Writes stay `World.TryWrite` (raw, not saved) until dev.27.
- `Kiln.GameTypes.Components` and the nine old structs are obsolete (`Use Kiln.Ecs.`) and still work.
- Server lab: typed reads equal raw bytes for 118 components; the rest and the client side are unproven.
  Page: `docs/kiln/typegen.md` §4.

## Creatures
`Creatures.Template(name)`, `Creatures.Of(entity)` → `CreatureRef` (`Level`, `Faction`, `Scale`, `Resistance[kind]`,
`SpeedFactor`, `DamageFactor`, `XpFactor`, `Loot`, `Features`, `Ai.Set(path, v)`, `Reset()`), `Command.Guard /
Attack / Stay / MoveTo / Heal`. Variants: `CreatureVariants.Register(new CreatureVariant { Id, Base, Level, ... })`,
`await Creatures.Spawn(variantOrTemplate, pos)`, `CreatureVariants.AddSpawnRule(...)`. Setters are layers of your mod.
dev.26+: `Arsenals.RulesState` (`ArsenalRulesStatus`) replaces the obsolete `Arsenals.Status` (`RulesStatus`).

## Abilities
`Abilities.Register(new AbilityDefinition { Id = "mymod:name", DefaultKey = "G", ChatCommand, Cost, Cooldown,
CastTime, Targeting, OnCast = async cast => { await cast.Impact(template, pos, configs); } })`,
`Abilities.Cast(actor, id)`, `ImpactStartingEvent` (change configs or cancel before a game impact runs; narrow with
`Abilities.HoldPrograms(ids)`), `await Abilities.RunImpact(...)`, `Abilities.PlaySequence(actor, guid)`,
`await Abilities.ApplyBuff(target, buffGuid, duration, caster)`, `Abilities.Attributes.Get/Add`,
`Abilities.Natives.Register(name, argc, call => ...)` for Impact programs.
dev.26+ (not seen by a player): a mod's own **player animation states** come from `assets/player-states.json`
(`{"format": 1, "states": {"my-slash": {"clip": "my_slash", "like": "Attack_Sword_Shield_01"}}}`), merged into the
player's graph at boot, never hot-reloaded; the mod needs `"side": "both"`, `"clients": "mod"` and its cook on the server.
Sequences enter them: a weapon's `Moves` or a cast's `Animation` names them ([blocks.md](blocks.md)). `Kiln.PlayerAnim`'s
`PlayerAnimStates` (any thread) only reads: `Available`, `Graph`, `GraphHash`, `Find(mod, name)`, `FindGameStates(name)`,
`All`, `GuidOf(mod, name)`, `IdOf(guid)`; `Define(state)` is refused in this release. The player graph's key cannot be
overridden (`Assets.Override`/`Register` throw). Page: `docs/kiln/modding/animations.md` "Player states". A Kiln impact has no used item: give it
its own `Damage1` config or it hits for 0.

## World edits
`await Build.Dig / Add / Flatten / Smooth / Paint / Replace(BuildShape.Sphere|Box|Cylinder(...))`,
`Build.Read/Write(box, cells)`, `Build.PlaceProp(template, pos, rotation)`, `Schematic.CaptureAsync`, `.Paste`,
`await Build.PlaceOnce(key, schematic, at)`, `await Build.Undo()`. Results carry `Refusal` (zone, base, not loaded).
Vanilla clients see the edits and the game saves them.

**dev.20+ flame writes:** `await WorldProgress.SetFlameLevel(level)` (Quests) returns `FlameChange`: inspect `Ok`,
`Reason`, `Marked` and `Mirrored`, not just queue acceptance. `LimitsAt(level)` reads derived limits. Client UI/fog/placement
checks remain pending; a failed rollback is explicitly reported and may leave changed state.

## ECS world programs (dev.20+)
For `kiln.programs.json` boot declarations and `EcsPrograms.Bind` in `OnLoad`, read
[world-programs.md](world-programs.md). Ordinary `OnLoad` is too late to add a scheduler declaration. A callback is a worker
context, never a place to call tick-thread game services or retain component pointers.

## Game UI, HUD, keys
`Ui.Menus.Add(MenuLocation.MainMenu|PauseMenu, text, onClick)`, `Ui.Screen(title, ui => { ... }, options)` →
`Open/Close/Toggle`, `Ui.Crafting.AddTab / AddSubcategory / AddPanel`, `Ui.Screens.Current`, `Hud.Show(id, text)`,
`Hud.Toast(text)`, `KeyBindings.Register(id, "F8", action, description)`, `UiText.Of(("en", ...), ("de", ...))`.
`Ui.Available` is false on a dedicated server. dev.20+: `WindowBlock`, `HudLineBlock`, `KeyBindingBlock` and the equivalent
JSON sections bind config and safe `game.*` values without draw code: [blocks.md](blocks.md#ui-as-data).
dev.20+: `StatusLines.Add(() => cachedText)` / `Set("state", text)` report mod status when asked, including on servers.
A source may run on a worker: use a tick-produced snapshot, never game state or tick-held locks. dev.21+: the menu and the
dashboard never wait for a source. They show the lines read last and refresh on a worker, so a new line appears a moment later.
dev.22+: `new UiScreenOptions { FullScreen = true }` is a whole-display screen (no title bar, not movable); draw your own
layout with `ui.FontSize(36)`, `ui.BeginChild(id, w, h, UiChildFlags.NavFlattened)`, `ui.Cursor` / `ui.SetCursor`, `ui.LastItem`,
`ui.Selectable(label, selected, width, height)`, `ui.DrawRect(UiLayer.Window, ...)`, `ui.PushColor` / `PushVar` and
`ui.KeyPressed(UiKey.Tab, claim: true)` (docs/kiln/ui.md "Layout for full screens"); `ui.HasLayout` is false on an older kiln.ui.
The Kiln menu (Insert, the MODS entries) lists every engine's mods; `Ui.AddConfigPage` pages show inline in its details.
A UI `command` runs in this game's registry; it does not send chat to a joined server. Use `castAbility` / `applyEffect`
with the same mod on the server for server-side actions. Old callbacks, queued edits and commands cannot outlive their mod load.

**dev.20+ game text:** `Texts.Get/GetPlain(tag)` in `Kiln.Content` resolves English loca. A server needs a client catalog
from the same game build (`kiln catalog build --side client`, local `kiln/catalog/client.json`). `Texts.Status` explains a
missing catalog; `Preload` loads ahead of time. dev.21+: while another thread loads the catalog (`Preload`, or a first call),
`Texts.Get` returns `null` at once instead of waiting; `Texts.Status` says "still loading". Catalogs contain game data and
must not ship in a mod.

## Assets
`assets/assets.json` entries `icon`, `texture`, `resource`, `blob`; loaded at boot. Icons and textures are client
only. Models and sounds: sources + `assets/cook.json`, cooked on the player's machine with `kiln cook`.

## Network
```csharp
var ch = Network.Channel("mymod:main", version: "1")
    .Register<Ping>((m, ctx) => ctx.Reply(new Pong(...)), NetDirection.ToServer)
    .Register<Pong>((m, ctx) => Hud.Toast("pong"), NetDirection.ToClient);
ch.SendToServer(new Ping(...)); ch.SendTo(player, msg); ch.SendToAll(msg);
```
Register messages in the same order on both sides. Server handlers on the tick thread, client handlers on the main
thread. Content changes reach Kiln clients without any code (content sync); configs need `[Synced]`.

## Client
`Client.State` (`Menu`, `DedicatedServer`, `Hosting`, `Remote`), `Client.IsClient`, `IsHosting`, `LocalPlayer`,
`Client.Tick += (in TickInfo t) => ...`, `Client.Post(action)`, `await Client.NextTick()`.
dev.26+, experimental (`KILNX002`), never run in a real client: `SteamGuard` (Kiln.Client, client only, any thread) stops
the game client reporting achievements to Steam. `SteamGuard.Hold(reason)` (an `IDisposable`; throws
`KilnUnavailableException` when it cannot protect), `TryHold(reason, out hold)`, `Latch(reason)` (until the game exits),
`Status` (`SteamGuardStatus`), `Protecting`. Call `Hold`/`TryHold`/`Latch` on the game's tick thread. Not covered: the game's
own stat reset, other mods' Steam calls, a player without Kiln.

## Hooks (advanced tier only) and other DLL mods
Only when no service does the job, and only with `"advanced": true`. dev.26+: the gate reads the manifest's `advanced`
(`ModInfo.Advanced`) and also covers `OriginalCode.Read`/`FindUnique`/`TryFindUnique`, `Keep.Block`/`Keep.Span`,
`Arsenals.JobHandler`, `Is.Job`, `Act.Job`, `Abilities.Programs.Register` and `NativeCall.Bytes`: without it they throw
`InvalidOperationException`. An unloaded mod can no longer install hooks. Since dev.21 every hook Kiln places on the game exe goes
through one registry, `kiln.hooks` v2: one detour per game function, shared by Kiln's services, EMF and Shroudtopia mod hooks
and your mod. Your hook is a **link** in that function's chain. An address outside the game image is refused
(`KILN_ERR_INVALID_ARGUMENT`); hooks on system DLL exports and EMF/Shroudtopia hooks outside the exe stay plain safetyhook.
- **Order on one function:** `before`/`after` constraints, then priority (higher first), then registration order. EMF mods'
  `hook`/`hook_pattern`/`hook_address` and Shroudtopia's `CreateDetour` are links owned by `emf:<id>` and `shroudtopia:<mod>` at
  priority 1 with `KILN_HOOK_NEWEST_FIRST`: they run newest first, before Kiln's priority-0 links. To run ahead of them, take a
  higher priority or `before` their owner.
- C#: `Hooks.Detour(symbolOrAddress, call => call.CallOriginal(), priority)` and
  `Hooks.Mid(symbolOrAddress, (ref Registers r) => { ... }, priority)` return a `HookHandle` (`Remove()`; unload removes it).
  A detour reads and writes `call[0]`..`call[3]` (rcx, rdx, r8, r9); returning a value without `CallOriginal()` replaces the
  function. Since dev.21 the original gets **every** argument, not only the first four. Floats: a mid hook (`r.XmmF(n)`) or a
  native mod.
- Mid hooks follow safetyhook's stack rules: `Rsp` is read-only, `TrampolineRsp` starts at `Rsp - 8`, and the game resumes with
  `rsp = TrampolineRsp; ret`. Return early from a function's first instruction with `r.Rax = value; r.TrampolineRsp = r.Rsp;`
  (C: `r->trampoline_rsp = r->rsp`).
- C++ (a native mod's `KilnNativeContext::api` and `owner`): `#include "kiln_hook.hpp"`, a global `kiln::RawHook`, any signature:
  ```cpp
  static kiln::RawHook g;
  static void hk(void* chat, int type, const char* text) {
      if (g.caller() == g_player_line_ret) { /* the game's call site: read it before calling on, never _ReturnAddress() */ }
      g.call<void>(chat, type, text);          // the rest of the chain, then the game (the game's exact argument types)
  }
  // install(api, owner, target, fn, label, priority = 0, before = nullptr, after = nullptr): here after "kiln.chat"
  if (!g.install(api, owner, target, (void*)&hk, "mymod/Thing", 0, nullptr, "kiln.chat")) { /* g.error() says why */ }
  ```
  `before`/`after` are comma-separated owner names (`kiln.chat`, `emf:coolmod`; kind prefix and `.dll` optional); a constraint
  that contradicts an earlier one is dropped with a kiln.log warning. Other kinds via `kiln::hooks_v2(api)`: `detour2` (up to 12
  integer/pointer arguments, `KilnHookCall2` with `c->caller`) and `kiln::MidHook2` (`void(*)(Ctx&)`, `Ctx` = `KilnRegisters` or
  `safetyhook::Context`).
- **`KilnHookOrder.flags`** (set `size = sizeof(KilnHookOrder)`; a smaller size reads 0): `KILN_HOOK_NEWEST_FIRST` runs the
  flagged links of one priority first, newest first. `RawHook::install` has no `flags` argument: call `hooks->raw_detour` with
  your own `KilnHookOrder`.
- **Same handler again:** installing the same raw handler on a function replaces the earlier link in one step (its handle stops
  working; the handler never runs twice for one call). A replaced `MidHook2` can run its handler twice for one call at that moment.
- **Link slots:** 65,536 per process, 512 hooked addresses (`KilnHooksStats.targets_cap`). A revoked link keeps its slot; an
  identical re-install (kind, function, user pointer, owner, label) takes it back. Managed `Hooks.Detour`/`Hooks.Mid` take a new
  slot on every install up to dev.24: install once, switch a flag. dev.26+: a managed re-install by the same mod, kind and
  address reuses its slot, also after a hot reload (test server); each new address still takes one. kiln.log warns at 75%
  and when full.
- Dropping a hook object does not remove the link: `revoke()` or `revoke_owner(owner)`. Reordering links already installed is not
  atomic: a call in flight at that moment can skip a link or run one twice (no crash).
- `kiln/state/hooks.json`: every function with its links in chain order, `foreignBefore`/`foreignOver` (other modules' hooks
  found before or over Kiln), `overlaps`, `rules`, `linkSlots` `{used, cap}`. kiln.log: `hooks: <label> chained over <module>+0x...`.

**Closed-source DLL mods** (XHL-Core, Auto Loot Plus, trainers) hook in their `DllMain`, before Kiln, and Kiln chains over them.
If a hosted engine needs Kiln's hook first, load the DLL late:
- `kiln/chain.ini` `[late]` repeats the path of an entry that `[chain]`, `[profile]` or `dinput8-chain.ini` already holds. The
  launcher's profile native page has the switch "Load after Kiln and ShroudForge" per chain entry and "Load late" on an overlap.
  kiln-core then loads the DLL after Kiln's services, plugins and ShroudForge: Kiln hooks first, the DLL hooks over it. An older
  proxy ignores `[late]` and loads the DLL early.
- Limits: EMF's own loader is never deferred (the launcher says "Always loads first"). A DLL another loader already started is
  `already-loaded` (`state/native.json` `chain.late`). A late DLL of a disabled or policy-refused native mod is skipped. An
  `early` or `boot` native mod cannot `requires` a mod whose DLL is late; a `world` mod can.
- A late DLL (or a native mod) may patch the game's entry point: the proxy puts the entry's own bytes back before the core
  starts, and the patch runs before the game's own entry.
- Play-once with a profile writes `kiln/chain/once.ini` with that profile's `[chain]` and `[late]`, so that start follows its
  own compat rules; the next normal start is back to the active profile's.
- Overlaps: kiln.log `compat: overlap:` lines and `hooks.json` `overlaps[]` (`foreign-before`, `foreign-over`, `refused`) name both
  sides and a fix (`late` or `rule`).
- `<configDir>/compat-rules.json` (after Kiln's `kiln/compat-rules.json`; the same `id` replaces it) rules can `warn`, load a
  chain DLL `late`, or `disable` a chain DLL or a profile-listed mod. The launcher applies `late` and `disable` of chain entries
  when it writes chain.ini; kiln-core logs `warn` rules, skips a disabled profile-listed mod and reports the rest as not applied,
  also `disable` of a native mod whose DLL the proxy already loaded (activate the profile again in the launcher).

## Other engines' mods next to yours (EML, EMF)
- EML: each game file is read at most once per start, and not at all while the OS shows it unchanged (change stamps in
  `kiln/eml/cache/files.json`). The first start after the update still reads all game files once (about 48 GiB on a full client
  install). `KILN_EML_FULL_HASH=1` turns reuse off; set it for one start after a power loss or hard crash. `state/eml.json`
  `fingerprint` and the kiln.log line `[eml] Fingerprints: hashed ... reused ...` show the cost. An EML mod whose `src/mod.lua`
  cannot be read is switched off before the game-data step, with the reason in `state/eml.json` and kiln.log; the others run.
- EMF portraits: `image_load("portrait:<key>")` from a tick or render thread returns a handle at once while the picture is still
  being drawn. It shows nothing until the drawing finishes, then the picture; no retry is needed. Called while the mod's DLL loads, it
  waits (at most 120 s for the whole start). kiln-eml's portrait cache codes: 5 busy and 7 cache I/O error (both tried again at
  the next start), 6 full (delete unused folders in `emf/cache/portraits`). Use kiln-emf and kiln-eml from the same release.
- Most of these paths were verified under Wine and on lab servers, not on a real client: report them as "needs a client check".
kiln-modding/references/manifest.mdkiln.mod.json: sides, what players need, version ranges, load order.Raw
# kiln.mod.json (manifest v2)

Wiki: [manifest](https://frostdev.io/kiln/wiki/manifest). Usually Kiln.Sdk writes it from project properties; a hand-written file in
the project folder is validated and copied. JSON with `//` comments and trailing commas.

```jsonc
{
  "id": "my-mod",            // required: [a-z0-9][a-z0-9._-]{0,63}; not kiln, kiln.api, game, enshrouded, native, permissions
  "name": "My Mod",
  "version": "1.4.0",        // required, semver
  "description": "…", "authors": ["…"], "license": "MIT", "url": "https://…",
  "apiVersion": "0.2",       // required
  "side": "server",          // server (default) | client | both
  "clients": "none",         // none | kiln | mod (default: server -> none, client/both -> mod)
  "dependencies": {
    "sample-lib": "^1.2",
    "kiln": ">=0.2",
    "extras": { "version": "*", "optional": true }
  },
  "loadAfter": ["some-mod"], "loadBefore": ["other-mod"],   // ordering only
  "incompatible": { "rival": "<3" },
  "features": ["tick"],      // Kiln features this game build must have
  "entryAssembly": "MyMod.dll",
  "entryType": "MyMod.MyModClass",   // default: the only public Mod subclass
  "exports": ["MyMod.Api.dll"],      // assemblies dependents compile against
  "advanced": false,
  "tests": { "autoRun": true, "timeoutSeconds": 20 }
}
```

## Clients
| `clients` | A joining player needs | Use for |
|---|---|---|
| `none` | nothing | server logic, chat commands, world edits, creature and loot changes, `"journal": "chat"` quests |
| `kiln` | Kiln: the server sends definitions | new items, recipes, template clones from existing resources |
| `mod` | this mod, from the server's modpack | icons, models, sounds with your audio, talents, quests, NPC clones, client code, key-bound abilities |

A stated `kiln` or `mod` makes the server refuse vanilla clients, unless `network.json` has `modSync: off`. A value from the `side`
default doesn't, so state it. A Kiln client without a stated `mod` mod is refused by default and told which; `modSync: warn` or `off` lets it in (passes on test servers, not yet in a real game).

`side: both` mods load on server and client. A `client` mod is skipped on a dedicated server (not an error).

## Version ranges
`*` any · `^1.2.3` / `1.2` compatible (for 0.x the minor is breaking: `^0.2` = `>=0.2.0 <0.3.0`) · `~1.2` patch updates · `=1.2.3` exact ·
`1.x` · `>=1.0 <2` · `1.0 - 2.0` · `[1.0,2.0)` · `^1 || ^3`.

## Load order and failures
Dependencies first, then `loadAfter`/`loadBefore`, ties by id. A failed mod never stops the others. Reasons are in kiln.log, `Mods.All`,
the control command `mods` and `kiln/logs/mods.txt`:
```
[E] [host] mod x 1.0.0 not loaded: missing dependency y ^1.0
[E] [host] mod x 1.0.0 not loaded: needs y [2.0,3.0), found y 1.2.0
[I] [host] mod x 1.0.0 skipped: side 'client': not loaded on a server
```
Unknown keys are kept and warned about. `kiln lint` catches them first.
kiln-modding/references/blocks.mddev.20+ block templates, world fields, UI bindings and actions.Raw
# Block families (dev.20+ additions)

Start with typed `Blocks.Define` in `OnLoad` or `blocks/*.json` and `<KilnServices>Blocks</KilnServices>`. Keep block names
stable after release. Fields take inline blocks, handles, names in this mod, or supported `other-mod:name` references.
`kiln lint` and `kiln blocks show <name>` expose errors and the lowered result. These additions require dev.20+; check the
installed runtime and MDK. dev.20 and the current dev.21 include them; dev.19 installations do not.

## Items, loot and templates

`LootTableBlock` / `lootTables` define weighted entries for rewards, container `DefaultLoot` and creature drops.
When updating dev.19 C# code, an item's `Loot.Tables` entries are now `DropPlace`, not `LootTableBlock`:
`new DropPlace { DropsLike = GameItems.Material_T1_Twigs }`. Their JSON `loot.tables` shape is unchanged.
`RecipeBlock` / `recipes` can name a game station. `kiln new item-blocks`, `world-blocks`, `creature-blocks`, `npc`,
`cosmetic`, `building-material`, `ability-blocks`, `quest` scaffold block projects. `kiln new ui MyUi --set blocks=true`
selects UI blocks. Consult `kiln new --list` for the installed template's options. Clone templates remain useful only when
blocks do not express the required field.

**Weapon moves (dev.26+, in development; no player has seen a move):** `WeaponBlock.Moves` (`WeaponMoves`): `Primary`
and `Heavy` take one `MoveBlock` or a chain (`[first, second]`); `HeavyAir`, `DodgeAttack`, `JumpAttack`, `PlungeAttack`,
`Block`, `Parry`, `Special`, `Execution`, `SneakAttack` one each. A `MoveBlock` has `Animation` (a mod state or a game
state by name, a GUID or a `0x` id; a list for several state events), `Like` (the sequence it starts from), `HitAt` and
`Length` in seconds. JSON: `"moves": { "primary": [ { "animation": "my-slash", "hitAt": 0.28 } ], "heavy": {...} }`.
Wands: `WandMoves.Heavy with { Animation = ..., HitAt = 1.1 }` (a `like` other than the premades is refused); spells:
`CastBlock.Animation` (`CastAnimation.Lob with { ... }`); an ability's cast refuses both. `VisualBlock.Animated`
(`AnimatedModel`: `Model`, `Graph`, `LeftModel`, `LeftGraph`) is a held model with moving parts; a game that did not cook
it shows the rigid model. Only the primary and heavy chains link; other follow-ups stay the game's. A name that cannot be
looked up is reported by block and slot and the move keeps its `like` state. Clips can be reworked at cook time:
`"from"` with `trim`, `reverse`, `speed`, `mirror`, `blend`, `offset` in `cook.json`. Lab-checked on a dedicated server
only. Page: `docs/kiln/modding/item-blocks.md` "Moves"; sample `mods/samples/WeaponMoves`; `kiln new item-blocks`.

## World blocks

```csharp
var table = Blocks.Define("rune-table", new PropBlock
{
    Name = "Rune Table", Provides = true,
    Visual = new VisualBlock { Like = GameItems.Prop_Decoration_Comfort1_T0_Table_Medium },
});
Blocks.Define("rune-bench", new StationBlock
{
    Name = "Rune Workbench", Kind = StationKind.Workbench,
    Recipes = [new RecipeBlock { Needs = { [GameItems.Material_T1_Twigs] = 2 },
        Output = GameItems.Material_T1_Wood_Logs, NeedsProps = [table] }],
});
```

`Provides` occupies one of 13 shared crafting-prop slots; `NeedsProps` takes at most 8. This is a manual-station check:
automated factory recipes reject it. Use `clients: mod`. JSON crafting props enter the boot table at first start; a new C#
prop enters on the next start. Restart each game after table changes.

`ContainerBlock.DefaultLoot` names a loot table and fills each placed-container identity once. Save progress prevents a
restart/refill; replacing a container at the same spot does not reset it. `Lid = new LidBlock { Moves = false }` keeps a
barrel shut. A custom lid's graph/open/shut states must exist. `DoorBlock.Opens` controls swing; `Sides` controls allowed
use, with `DoorSide.Front`, `Back`, `Both`.

A prop with its own model can use `Colliders = [new BoxCollider { Size = new(...), Center = new(...) }, ...]` (1..16 boxes).
Do not combine `Collider` and `Colliders`, use spheres/capsules, or replace a borrowed game look's collision. Ceiling
starters do not support several boxes. The placement check uses the enclosing box unless `Placement.Size` overrides it.
Wiki: [world blocks](https://frostdev.io/kiln/wiki/world-blocks).

## UI as data

```jsonc
{ "format": 1,
  "windows": { "info": { "title": "My mod", "rows": [
    { "type": "value", "label": "FPS", "bind": "game.fps", "format": "0" },
    { "type": "button", "label": "Close", "does": { "closeWindow": "info" } } ] } },
  "hudLines": { "fps": { "text": "{game.fps:0} fps", "when": "game.inGame", "refresh": 1 } },
  "keyBindings": { "open-info": { "key": "F8", "does": { "openWindow": "info" } } } }
```

`WindowBlock`, `HudLineBlock`, `KeyBindingBlock`: `clients: mod`. Dedicated servers validate and retain them but draw/bind
nothing. Register this mod's config before defining blocks that bind it. `config.<path>` edits only that mod's config;
server and synced values are read-only on a joined client. Safe `game.*` values include fps, screen, state, language and
computer clock; no health, mana, position, inventory or world time. Conditions support `!`, comparisons, `&&`, not `||`.
Refresh is 0.1..3600 seconds; closed windows and hidden HUD lines do not draw.

Rows: text, value, toggle, slider, choice, separator, button. Each button/key does exactly one action: local command,
toggle/set config, cast ability, apply effect, open/close window, toast. `command` never sends to a joined server;
`castAbility` / `applyEffect` need the mod on that server and keep normal costs/cooldowns. No free drawing, text input,
tables or tabs: use `Ui.Window` / `Ui.Screen` for those.

Unloading removes windows, menus, HUD and keys and drops pending commands/config writes. Do not keep old handles across
reload. Rebinding persists under `<mod>:<name>`; use `was` when renaming a key. Real rendering, clicks, gamepad input and
joined-server actions need a client check. Wiki: [UI blocks](https://frostdev.io/kiln/wiki/ui-blocks).
kiln-modding/references/world-programs.mddev.20+ boot ECS declarations, worker access and callback lifetime.Raw
# ECS world programs (dev.20+)

Use this only when ordinary tick events and service APIs cannot express the work. `EcsPrograms` is in `Kiln.Api`, backed
by `kiln.world` v2. `Available` checks boot declaration/binding support; `SupportsOtherWrites` checks tracked writes to
another entity. These APIs require 0.2.0-dev.20 or newer. Dedicated-server and reload checks do not establish
real-client replication; client-world ECS programs remain unsupported.

Ship strict JSON at the mod root, `kiln.programs.json`; Kiln.Sdk copies it for build, deploy and pack:

```json
{ "format": 1, "programs": { "count": {} } }
```

The native service reserves this scheduler declaration before worlds finalize. Bind that key in `OnLoad`:

```csharp
private IDisposable? _program;
private int _updates;
public override void OnLoad()
{
    if (!EcsPrograms.Available) return;
    _program = EcsPrograms.Bind("count", _ => Interlocked.Increment(ref _updates));
}
public override void OnUnload() => _program?.Dispose();
```

Here the default is a host-only system program (one worker callback per step, no entity query). A tick handler can read
`Volatile.Read(ref _updates)` to report it. Do not log or call tick-thread game services from every worker callback.

Declarations support `tpl`, `tplOptional`, `read`, `readOptional`, `write`, `writeOptional`, `singletonsRead`,
`singletonsWrite`, `otherTpl`, `otherRead`, `otherWrite`, `with`, `without`, `anyOf`, `eventsIn`, `eventsOut`, and `flags`.
For an entity query, specify `flags: ["hostOnly"]` and a required query list; do not retain the default `system` flag with
an entity query. Match real component/event names and exact unmanaged layouts from the installed API/catalog; never guess
sizes or copy private engine offsets.

Inside the callback, `TryRead<T>` / `TryWrite<T>` use declared current-entity access; `TryReadOther<T>` /
`TryWriteOther<T>` use declared other-entity access. They take the component size from kiln.types: without it they return
false and the mod log says so once per program (dev.21+; calling them per entity allocates and locks nothing). Writes mark
the game's replication/save tracking. `ReadEvents(name)`
returns payload copies for each declared input; multiple event inputs work. `EmitEvent(name, bytes)` uses a declared
output with the exact reflected payload size (less than 256 bytes), stamps GameEvent timestamps, and returns false when full. `QueueAdd` / `QueueRemove` apply at the end of the step. A context/component span
cannot escape its callback. Send ordinary service work back with `Context.Scheduler.Post` and recheck lifetime/world state.

The file is strict JSON (no comments/trailing commas/duplicate keys), at most 64 KiB, 1..8 declarations per mod. Names,
access counts and system/query combinations are validated; an unknown declaration key or undeclared access fails.
Programs share a 32-slot world-service limit and the game's remaining scheduler capacity. Changing declarations needs a server restart. Dispose on the tick thread. Disposing/unloading drains bound callbacks and makes the reserved entry
inert; hot reload may bind the same declaration again, not add a new scheduler slot. The old load's callback must never
be reused.

Native program hooks and spawn-time components are separate APIs; this does not make ordinary service calls safe on
worker threads or add client-world EMF programs. Wiki: [world programs](https://frostdev.io/kiln/wiki/world-programs).

## Hooks around game systems

`GameSystems.Hook(name, before: callback, after: callback)` observes a real system update once per slice, on game worker
threads. Dispose on the tick thread; unload removes and drains the hook. A second hook for the same update function fails,
including differently named systems sharing that function. A hook has only the original system's access rights: it cannot
add scheduler permissions. `GameSystems.Profile` measures the existing runner separately. Missing symbols or a conflicting
runtime can make hooks unavailable. A callback exception disables the callback; check the log.
kiln-modding/references/data-packs.mdData packs, new items, game data, creatures and assets.json.Raw
# Data packs and data files

Wiki: [data packs](https://frostdev.io/kiln/wiki/data-packs), [new items](https://frostdev.io/kiln/wiki/new-items),
[game data](https://frostdev.io/kiln/wiki/game-data), [creatures](https://frostdev.io/kiln/wiki/creatures),
[assets](https://frostdev.io/kiln/wiki/assets). Prefer a pack over code whenever the change is data. `kiln lint` checks them.

## Where and when
`<mod>/content/*.json`. Applied when the world's registries have loaded (before the save is read), in mod load order,
then file-name order. A pack follows its mod: unload the mod and its pack comes off; edit a file and every pack is
re-applied (checked every ~2 s). Packs apply even when no mod references Kiln.Content.

## Format
```json
{
  "format": 1,
  "items": {
    "Material_T1_Wood_Logs": { "maxStackSize": 1000 },
    "*": { "@where": { "maxStackSize": ">1", "category": "Materials" },
           "maxStackSize": { "mul": 4, "max": 9999, "raiseOnly": true } },
    "Weapon_T1_1H_SwordEpic_01": { "damageSetup.dmgMod": 1.25, "rarity": "Epic" }
  },
  "recipes": {
    "Tool_basic_FishingRod_T0_PK": { "inputs": [ { "item": "Material_T1_Twigs", "count": 1 } ] },
    "Ammo_Arrow_T1_Flint": { "outputs": { "mul": 2 } },
    "*": { "@where": { "output": "Material_T1_Charcoal" }, "craftingDuration": 30 }
  },
  "loot":  { "Material_T1_Wood_Logs": { "stackSizeMin": 5, "stackSizeMax": 20 } },
  "drops": { "*": { "count": { "mul": 2 }, "chance": { "mul": 1.5, "max": 1 } } },
  "options": { "stackGuard": true }
}
```
- **Selectors**: a debug name, an id (`0x2b0cf1d5`), or a case-insensitive glob (`Material_*`, `*`).
- **`@where`**: conditions on the game's original values: equality, `">1"`, `">="`, `"<"`, `"<="`, `"!="`, a list
  = any of; `name` (glob); recipes also `output` / `input`.
- **Fields**: paths from the game's reflection. Get the real names from `kiln catalog search` or `Content.Dump()`
  (`kiln/data/content/dump.json`), never guess. Identity fields (ids, GUIDs, flags) are read-only.
- **Values**: numbers, bools, enum names, times in seconds, or ops on the original:
  `{"mul", "add", "min", "max", "raiseOnly", "lowerOnly"}`, `{"set": v}`, `{"reset": true}`.
- **Recipe lists**: `inputs` / `outputs` = the whole new list (1–32 `{"item", "count"}` or `{"category", "count"}`),
  or `{"mul": x}` to scale.
- **Conflicts**: later entries in a pack win; across packs the later mod wins; code changes win over packs. Conflicts
  and problems (unknown field, out of range, selector matching nothing) are logged per file and in
  `Content.PackReport()`; they never stop other entries.

## New items and recipes: `"add"`
```json
{ "format": 1,
  "items": { "add": [ {
      "guid": "7d3e9b52-1c84-4f0a-a6d7-5e2b8c9f1a03", "name": "MyMod_Glacier", "base": "Weapon_T1_1H_SwordEpic_01",
      "model": "item:Weapon_T4_1H_SwordEpic_02", "icon": "icons/glacier.png",
      "texts": { "en": { "name": "Glacier" }, "de": { "name": "Gletscher" } },
      "fields": { "damageSetup.dmgMod": 1.6, "rarity": "Epic" },
      "loot": { "like": "Material_T1_Metal_Scraps" },
      "drops": [ { "table": "like:Material_T1_Metal_Scraps", "countMin": 1, "countMax": 2, "chance": 0.25 } ] } ] },
  "recipes": { "add": [ {
      "guid": "84d38550-b3df-426d-a42d-5b1259156447", "name": "MyMod_Glacier_Recipe", "station": "hand",
      "inputs": [ { "item": "Material_T1_Twigs", "count": 2 } ], "outputs": [ { "item": "MyMod_Glacier", "count": 1 } ] } ] } }
```
- `guid` fixed forever (generate once). `name` unique debug name. `base` required: the clone's behaviour, stats, model
  and icon unless you change them.
- Kitbash references (`model`, `icon`, `perks`, `weaponType`, `visual`, `impact`, `refs`) point at things the game
  already loaded (`item:<name>` or a GUID) or that kiln.assets serves. Changing a game item's references needs
  `"@refs": true` on the rule.
- `station`: `"hand"`, a recipe whose station to use, or `"0x<workshop id>"` (traders are workshops too). `unlock`:
  `Always` (default) or `LikeBase`.
- Clients: new items need a stated `clients: "kiln"` (definitions are synced; PNG icons are not) or `"mod"`.
- Removing a mod: its items stay registered until restart; after a restart, saved stacks become placeholders named
  `[Missing: <mod>:<item>]` and come back when the mod does.

## Game data sections (same files)
`templates` (creatures, projectiles, explosions; paths start with a component type name), `arsenals` (enemy
attacks), `sequences` (cast timing, cost events), `buffs`, `perks`, `skills` (skill-tree nodes), and `items` paths
into config values (`"impactValues.ManaCost": 30`).
```json
{ "format": 1,
  "items": { "Ammo_T1_Spell_Fire_Ball_lvl10": { "impactValues.ManaCost": 30 } },
  "templates": {
    "Enemy_Vukah_*": { "@where": { "Faction.faction": "Vukah" },
                       "Enemy01BehaviorState.combatCommitmentSetup.view.range": { "mul": 1.5 } },
    "add": [ { "guid": "…", "name": "MyMod_Elite_Vukah", "base": "Enemy_Vukah_Melee",
               "fields": { "Level.dataStorage[0]": 20, "ScaleComponent.uniformScale": 1.4 } } ] },
  "perks": { "Perk_Damage_Ice": { "damageModifier.distribution.magicalIceDamage": { "mul": 2 } } },
  "skills": { "item:Ability_DoubleJump": { "costs": 1 } } }
```
- Template edits are global (every entity of the template). Per-creature changes belong to kiln.creatures.
- An enemy's sight is `Enemy01BehaviorState.combatCommitmentSetup.view.range` (by day; times `nightRangeFactor` at night, the Vukah's 0.6),
  its cone `.view.coneHalfAngle`, the distance it engages a seen target at once `.gain.inRange.range`. Not `aggroSetup`: its `viewRange`
  only sizes the candidate search (never below 64 m), `aggroRange` and `viewConeHalfAngle` are not read.
- When a change shows: template-only data at once; spawned values (level, scale) at the next spawn; configs at the
  next cast or buff; costs at the next payment. Vanilla clients may show vanilla tooltips; Kiln clients get synced
  values.
- Perk and buff clones (`"add"` in `perks` / `buffs`) get their own names per language.

## Creatures section
```json
{ "creatures": {
    "variants": [ { "id": "elite_vukah", "base": "Enemy_Vukah_Melee", "name": "Elite Vukah", "level": 20,
                    "scale": 1.4, "resistances": { "Fire": 0.5 }, "features": ["-FleeHome"],
                    "ai": { "combatCommitmentSetup.view.range": 30 } } ],
    "spawnRules": [ { "replace": "Enemy_Vukah_Melee", "with": "elite_vukah", "chance": 0.2, "night": true } ] } }
```

## Assets: `assets/assets.json`
```json
{ "format": 1, "entries": [
  { "kind": "icon", "guid": "<item GUID>", "file": "icons/my_item.png" },
  { "kind": "texture", "guid": "<texture resource GUID>", "type": "keen::UiTextureResource", "part": 0, "file": "retex/x.png" } ] }
```
Kinds: `icon`, `texture`, `resource`, `blob`. Paths relative to `assets/`, no `..`. Icons and textures are client
only (skipped on a server). Models: write `assets/cook.json` and ship your glTF; `assets/cooked/` is made on the
player's machine and never shipped.
kiln-modding/references/quests-talents-sounds.mdTalents, progression, quests, friendly NPCs, sounds and voice.Raw
# Talents, progression, quests, NPCs, sounds

Wiki: [talents](https://frostdev.io/kiln/wiki/talents), [progression](https://frostdev.io/kiln/wiki/progression),
[quests](https://frostdev.io/kiln/wiki/quests), [NPCs](https://frostdev.io/kiln/wiki/npcs),
[sounds](https://frostdev.io/kiln/wiki/sounds). `kiln lint` checks every format here.

## Client evidence

The dated [status page](https://frostdev.io/kiln/wiki/status) is the source for what a player has seen. Do not infer a new
block form, party scope, joined-player behaviour, UI or sound check from a server pass. Report the specific unobserved
result as "needs a client check".

## Progression (`Player.Progression`, service Players; works, seen with vanilla clients)
`Read()`, `AddExperience(n, source)`, `SetLevel(n)` (cap 50), `GiveSkillPoints(n)` (up to 60), `Unlock(node, force)`,
`Lock(node, force)`, `Respec()`. They return `Task<ProgressionResult>`: refusals are `Ok = false`, only bad arguments throw.

## Talents (`skills/*.json`, service Skills)
Both ways need `"side": "both"`, `"clients": "mod"`. Comments allowed, trailing commas not. Template: `kiln new talents`.
```json
{ "format": 1,
  "gameTree": { "nodes": [
    { "guid": "<fixed>", "name": "mymod_ward", "class": "Mage", "like": "0xcfa5985b", "from": ["0xcfa5985b"],
      "offset": [170, -40], "cost": 1, "configs": { "0x0a0c87e7": 2 },
      "texts": { "en": { "name": "Ward", "description": "+2 Intelligence." } } } ] },
  "trees": [ {
    "id": "mymod:ember", "name": { "en": "Ember Arts" }, "points": { "start": 1, "perLevel": 1 },
    "menu": { "en": "Ember Arts" }, "key": "K", "respec": true,
    "nodes": [ { "id": "kindle", "name": { "en": "Kindle" }, "col": 1, "row": 0, "cost": 1, "maxLevel": 3,
                 "icon": "icons/kindle.png", "effects": [ { "attribute": "Intelligence", "amount": 1 } ] } ] } ] }
```
- `gameTree`: drawn by the game, paid with its points. Put nodes in the file: on a dedicated server `GameTree.Define` counts from the
  next start. `maxLevel` 0 to 7. Nodes past the game's caps are kept by Kiln (up to 4096), outside the character save.
- `trees`: Kiln's own screen. Points: `{"start", "perLevel"}`, `{"share": 0.5}`, `TalentPoints.Custom`. `col`/`row` 0 to 31, levels to
  99. `requires`, `requireAll`, `pointsSpent`, `level`.
- Effects: `{"attribute", "amount"}`, `{"gameNode": "0x…"}`, `{"program", "configs"}`, `{"buff", "refresh"}`, `{"ability"}`,
  `TalentEffect.Callback(apply, remove)`.
- C#: `Skills.Register(new TalentTree {...})`, `Skills.HolderOf(player)`, `Skills.Unlock/Lock/Respec/Grant/Points/State/Has`,
  `TalentUnlockingEvent` (cancel with a `Reason`), `TalentChangedEvent`. Chat: `/talents`.

## Quests (`quests/*.json`, service Quests)
Comments and trailing commas allowed.
```jsonc
{ "format": 1,
  "npcs": { "ilse": { "template": "NPC_Workshop_Blacksmith01", "dialogue": "ilse", "names": { "en": "Ilse" } } },
  "quests": { "crates": {
    "name": { "en": "The Lost Crates" }, "giver": "ilse", "requires": [ { "level": 8 } ],
    "steps": [ { "id": "hunt", "text": { "en": "Drive off the scavengers" },
                 "objective": { "kill": { "tag": "crates", "count": 6 } },
                 "spawn": { "template": "Enemy_Scavenger_Melee01", "count": 6, "tag": "crates", "near": "ilse", "distance": [60, 120] },
                 "marker": { "name": { "en": "The Old Road" } } } ],
    "rewards": [ { "xp": 1500 } ] } },
  "dialogues": { "ilse": { "start": "hello", "nodes": { "hello": {
    "text": { "en": "Scavengers took our crates." },
    "options": [ { "text": { "en": "I'll find them." }, "if": { "quest": "crates", "is": "available" },
                   "do": [ { "start": "crates" } ], "goto": "end" } ] } } } } }
```
- Scope: `player` (default), `party` (`partyRadius`), `world`.
- Objectives: `kill`, `collect`, `deliver`, `reach`, `talk`, `craft`, `build`, `knowledge`, `custom`.
- Conditions: `level`, `quest` + `is`, `step` + `is`, `item`, `knowledge`, `predicate`, `all`, `any`, `not`.
- Actions: `xp`, `item`, `take`, `start`, `complete`, `fail`, `reset`, `advance`, `spawn`, `despawn`, `teleport`, `chat`, `toast`, `waypoint`,
  `talk`, `knowledge`, `revoke`, `signal`, `call`.
- Clients: quests and dialogue are in the game's UI by default and need `mod` + `both`. For a mod vanilla clients join: `"journal": "chat"` and `"native": false`, with `none`.
- Places: step `site`, `spawn`, `reach.site`, `npcs.*.place` with `near` and `distance`: real ground, outside bases. Don't guess positions.
- C#: `Quests.Define`, `Quests.Start`, `Quests.Signal`, `Quests.Advance`, `Quests.BindNpc`, `Dialogues.Open`, `QuestHolder.Of(player)`.
  Events: `QuestStartedEvent`, `QuestStepEvent`, `QuestCompletedEvent`, `QuestFailedEvent` (cancelable), `QuestStateChangedEvent`.
- Test without a player: `QuestHolder.Synthetic("name")`; `/questadmin reload | state | start | step | advance`. `QuestInput` is a
  lab helper: dev.26+ marks it `[Experimental("KILNX001")]` and hides it (a lab-only project adds `KILNX001` to `NoWarn`).

## Quest and dialogue blocks (dev.20+)
Use `QuestBlock` / `DialogueBlock` or `quests` / `dialogues` inside `blocks/*.json`; `kiln new quest MyQuest` scaffolds one.
`NpcBlock.Talk` names a dialogue block. Item, NPC, status and loot-table fields can name blocks of this mod. Typed pieces:
`StepBlock`, `Goal.Kill(...)`, `Req.Level(8)`, `Do.Start("errand")`, `RewardBlock`.

```jsonc
{ "format": 1,
  "quests": { "errand": { "name": "Wood for Ilse", "giver": "ilse",
    "steps": [ { "id": "wood", "text": "Bring three logs to Ilse",
      "objective": { "deliver": { "item": "Material_T1_Wood_Logs", "count": 3, "to": "ilse" } } } ],
    "rewards": { "xp": 100 } } },
  "dialogues": { "ilse-talk": { "start": "hello", "nodes": { "hello": { "options": [
    { "text": "Do you need wood?", "if": { "quest": "errand", "is": "available" },
      "do": [ { "start": "errand" } ], "reply": "Three logs, please." } ] } } } },
  "npcs": { "ilse": { "name": "Ilse", "body": "villager", "talk": "ilse-talk" } } }
```

- `Blocks.Define` returns `QuestHandle`; use `.FullId` with `Quests.Start` / `StatusOf`. Keep block names and step ids fixed.
- Rewards are one bundle: `xp`, `items`, `status`, `knowledge`, `lootTable`; step rewards run before `onDone`. Inventory
  overflow lands at the player's feet. A world quest pays every player present from one loot roll.
- Blocks use the game's UI: `side: both`, `clients: mod`. For vanilla clients, keep `quests/*.json` with chat/non-native flags.
- JSON blocks enter native journal data at the first start. C# definitions are written to the native sidecar for the next
  start; ship the generated `quests/generated/<mod>.json` for first-start UI. Restart server and clients after changes.
- The block form has no top-level `knowledge`, option `set`, `toast` or `waypoint` action. Unknown keys fail. Native appearance,
  party scope and dialogue interaction still need the client checks listed on the wiki; server lowering is not visual proof.

## Friendly NPCs and native quests (same files)
`clone`, `roster`, and quests and dialogue in the game's UI need `"clients": "mod"`, `"side": "both"` (not `kiln`: the client
builds them from the mod's files at boot, so changes need a restart).
- `npcs.<id>.place`: `{near, distance}` or `{at}`, with `yaw`, `once`, `unlock`. Real ground outside bases (`inBase`, `exact` opt out). A game NPC's unlock is granted for that world and stays.
- `clone`: `{guid, name}`, fixed. `roster`: `{unlock, roamingRange, summonItem}`. Limit 64 roster NPCs, 35 are the game's.
- `vendor.recipes`: shared with the base's workshop.
- Dialogue: any `if` on any node (one knowledge check, the rest kept by the engine). `reply`, `once`, `close`, `hint`,
  `greetings`, `farewell`. Add options at the end of a node: positions feed the ids.
- `knowledge`: `{"name": {"scope": "world"}}`, resolved as `<mod>:<name>`.
- Villagers: `Villagers.Find/Of/List`, `.Teleport`, `.GoTo`, `.Stay`, `.Follow`, `VillagerStateEvent`.

## Sounds (`assets/cook.json`, service Audio)
`"side": "both"` or `"client"`, `"clients": "mod"`. Ship your audio and the recipe. Plain JSON: no comments. Template: `kiln new sound`.
```json
{ "format": 1, "tool": "soundcook", "out": "cooked", "guidNamespace": "<a uuid4, once>",
  "steps": [
    { "op": "sound", "name": "click", "src": "../audio/click.wav", "channels": "mono", "cue": { "scene": "Ui" } },
    { "op": "sound", "name": "boom_1", "src": "../audio/boom_1.ogg", "channels": "mono", "normalize": -23 },
    { "op": "cue", "name": "boom", "sounds": ["boom_1"], "positional": { "min": 3, "max": 60 } },
    { "op": "voiceover", "name": "hello", "fallback": "en", "languages": { "en": ["../audio/en/hello.wav"] },
      "subtitles": { "en": "Hello." } } ] }
```
- Sources: WAV, FLAC, Ogg, MP3, any rate. 3D sounds mono. 1 s or less starts at once.
- Overrides: `inputs` + `"guid": "@input"`, `"mode": "override"`. Voice slots: en, de, fr, es, zh; others use English; empty is silent.
- C#: `SoundId.Named(Context, "boom")`, `Sound.Play2D/PlayAt/PlayOn/PlayMusic/Stop/SetVolume`,
  `Voiceover.Play(VoiceoverId.Named(Context, "hello"), speaker)`, `Voiceover.Narrate`. Any thread. Server calls relay to Kiln clients.
- `Sound.Replace` isn't any-thread: call it from `Client.Post` or mod code.
kiln-modding/references/devkit.mdThe kiln command, lint, analyzers, catalogs, VS Code, the debugger.Raw
# The dev kit

Wiki: [dev kit](https://frostdev.io/kiln/wiki/dev-kit), [kiln](https://frostdev.io/kiln/wiki/cli),
[lint](https://frostdev.io/kiln/wiki/lint), [VS Code](https://frostdev.io/kiln/wiki/vscode),
[catalogs](https://frostdev.io/kiln/wiki/catalogs), [debugging](https://frostdev.io/kiln/wiki/debugging).

## The `kiln` command
| Command | Does |
|---|---|
| `kiln new --list`, `kiln new <template> <Name> [--server S] [--feed F] [--set opt=value]` | a project; `tests=true`, `impact=true`, `target=<mod>` |
| `kiln build [-c CONFIG] [--no-cook] [-- args]` | cooks if needed, builds; C# or native |
| `kiln cook [--check] [--game DIR]` | every `cook.json`; `--check` exits 1 when one is needed |
| `kiln deploy [--server S] [--reload]` | copies into the server; hot reloads |
| `kiln pack` | `<id>-<version>.zip`, no cooked files |
| `kiln run [--server S] [--stop\|--restart\|--status] [--wait N]` | starts a launcher or kilnctl server; follows the log until up |
| `kiln reload [ID]`, `kiln test [ID] [FILTER] [--exact NAME]` | through the control folder |
| `kiln logs [-f] [--mod ID] [--level D\|I\|W\|E] [-n N]` | kiln.log, filtered |
| `kiln lint [PATH…]`, `kiln debug [--dap]`, `kiln catalog status\|build\|path\|search` | below |

`--server`: a folder or server name, else `$KILN_SERVER`, else the project's `KilnServer`. Native mods need `kiln run --restart`.

`--json`: one document on stdout, tool output on stderr, errors as `{"ok": false, "error": {"code", "message"}}`.

## kiln lint
`kiln lint [PATH…] [--json] [--catalog F] [--profile DIR] [--min-severity L] [--off KLnnn] [--stdin --stdin-path P] [--list-rules]`.
Exit 1 means errors. Build a Kiln.Sdk project once first: its manifest is read from the build output.
KL0xx reading, KL1xx schema, KL2xx manifest vs content, KL3xx ids, KL4xx game references (needs a catalog), KL5xx files and cook recipes,
KL6xx licences. Findings can carry `fixes`.

## Analyzers (in every Kiln.Sdk build)
| Id | Catches | Fix |
|---|---|---|
| KILN101 | tick-thread API off the tick thread | `Context.Scheduler.Post` |
| KILN102 | `async void` | `async Task`; keep `OnLoad` synchronous |
| KILN103 | process-wide event handler never removed | `-=` in `OnUnload` |
| KILN104 | mod object in process-wide state | undo it in `OnUnload` |
| KILN105 | blocking call on the game thread | `await` |
| KILN106 | thread, timer, watcher, socket, subscription never released | dispose in `OnUnload` |
| KILN107 (error) | advanced API without `KilnAdvanced` | opt in, or don't use it |
| KILN108 | service used but not in `KilnServices` | add it |
| KILN110 (error) | bad or duplicate block name | rename it |
| KILN111 | a name that is no block, or the wrong kind; only info for a game-style name (`Weapon_T1_...`) your local catalog lacks (dev.21+). Names from `content/*.json` and `ItemDefinition` count | define it, or use the handle `Blocks.Define` returned |
| KILN112 (error) | `clients` too low for the mod's blocks | raise `clients` (`kiln` or `mod`) |

KILN001-004 are manifest checks. KILN190: no catalog, so no game-id constants. Configure in `.editorconfig`
(`dotnet_diagnostic.KILN105.severity`). Don't suppress one without saying why.

## Catalogs and game ids
`kiln catalog build` makes the catalog from the user's own game, read-only. Never commit or ship it. `kiln catalog search "bronze sword"
[--section items] [--json]`.

Kiln.Sdk generates constants from it: `using Kiln.GameIds;`, `GameItems.X`, `GameItems.Ids.X`, `GameItems.Guids.X`, `GameBuffs`, `GamePerks`,
`GameSkills`, `GameWorkshops`; with `<KilnGameIds>` also `GameRecipes`, `GameTemplates`, `GameSequences`, `GameSounds`.

## VS Code
The `.vsix`, installed by hand, adds schemas, lint on save, game-id hover and completion, Mods, Test Servers, Testing and Game Data views, a
kiln.log follower, a server console, Impact assembly and a `kiln` debug type.

## Debugger
`kiln debug --server <name> --dap` is a Debug Adapter Protocol proxy for netcoredbg (win64 build, bundled with the VS Code extension;
otherwise `KILN_NETCOREDBG`). In VS Code: `{ "type": "kiln", "request": "attach", "server": "<name>" }`.
- Deploy a Debug build: Release breakpoints never bind.
- A breakpoint pauses the whole server. Test servers only; Steam-library servers are refused.
- Never `kill -9` netcoredbg while attached: the server dies, with no save. Disconnect instead.
- After `kiln reload`, reattach. Watch expressions run in the server.
- Dedicated servers on Linux and Wine only. Windows servers are untested, and the client isn't covered.
kiln-modding/references/testing.mdA local test server, hot reload, in-server tests, logs.Raw
# Test server, hot reload, in-server tests, logs

Wiki: [testing](https://frostdev.io/kiln/wiki/testing), [hot reload](https://frostdev.io/kiln/wiki/hot-reload),
[debugging](https://frostdev.io/kiln/wiki/debugging), [servers](https://frostdev.io/kiln/wiki/servers).

## A local test server
Use a server nobody plays on. Ways to get one:
- **Kiln Launcher**: Servers → New server (Linux: SteamCMD install, own Wine prefix), then install Kiln on it.
- **kilnctl** (Linux):
  ```sh
  kilnctl init mytest --dir <dir> --port 15637 --policy any
  kilnctl install mytest && kilnctl prefix mytest && kilnctl deploy mytest
  kilnctl start mytest       # returns when the world runs
  kilnctl logs mytest --kiln # kiln.log
  kilnctl stop mytest        # the game saves
  ```
  `kiln run --server mytest` starts it too and follows the log until it's up.

`policy any` lets Kiln run on a game build without a verified record. On `verified-only` (the default) an unknown
build runs vanilla and your mod never loads: kiln.log says so at boot.

No player is needed for most checks. Design player-free checks: entities you spawn, containers, NPCs in the world,
log lines. Write down what only a joined player can confirm (UI, client visuals, real hits) instead of claiming it.

## Launcher notes (dev.21+)
- Library Remove and Clean up are refused while Enshrouded runs ("Close Enshrouded first: the game may be reading this mod"),
  also with force. Clean up never lists a version the game's active profile uses. A running server blocks Remove only when its
  profile points into the Library.
- A profile's native page lists each chain.ini entry with "Load after Kiln and ShroudForge", last session's hook overlaps with
  "Load late", and the compat rules that apply.
- Play-once with a profile follows that profile's chain and compat rules for that start only.
- These pages and messages are covered by tests and mock data, not by a hand-run against a real install.

## Deploy and hot reload
```sh
kiln deploy                   # copy into <server>/kiln/mods/<id>
kiln deploy --reload          # and reload it in the running server
```
The control folder does the rest: write a command into `kiln/control/<name>.cmd`, read `<name>.result.json`.
| Command | Does |
|---|---|
| `mods` | every mod: id, version, state, error, side, load index, dependencies, faults, ms/step |
| `load <folder>` | load `kiln/mods/<folder>` |
| `unload <id>` | unload it and its dependents |
| `reload <id>` | dependents unload, the mod reloads from disk, everything loads again |
| `reload-test <id> <n>` | n reloads, one per step, then a leak check |
| `test <id> [filter]` | run its `[KilnTest]` methods |
| `gc` | full collection; how many unloaded mod contexts are still alive |

```sh
echo "reload my-mod" > <server>/kiln/control/r.cmd && sleep 2 && cat <server>/kiln/control/r.result.json
```
What survives a reload: `WorldData` / `PlayerData` / `EntityData` (serialized and read into the new types), config
files. What does not: anything in static fields, your own threads and timers (stop them in `OnUnload`), content and
creature layers (they come off and your new `OnReady` / `OnLoad` puts them back).

## In-server tests (Kiln.Testing)
`<KilnServices>Testing</KilnServices>`, or `dotnet new kiln-mod --tests`.
```csharp
using Kiln.Testing;
using Assert = Kiln.Testing.Assert;

public sealed class MyTests
{
    [KilnTest] public void Mod_is_loaded() => Assert.True(Mods.IsLoaded("my-mod"));

    [KilnTest(TimeoutSeconds = 10)]
    public async Task Happens_within_a_few_steps(TestContext t)
    {
        StartIt();
        await t.Until(() => Done, TimeSpan.FromSeconds(5), "it to finish");   // checked once per step
        await t.Ticks(3);
        Assert.True(Game.IsTickThread);
    }

    [KilnTest(Skip = "needs a player")] public void Later() { }
}
```
- Tests run one at a time on the tick thread, so they may touch game state and call services. Instance tests on the
  `Mod` subclass run on the live mod instance.
- `TestContext`: `Ticks(n)`, `Until(cond, timeout)`, `Delay(time)`, `Log`, `Mod`, `Cancellation`.
- `Assert`: `True`, `False`, `Equal`, `NotEqual`, `Null`, `NotNull`, `Same`, `Contains`, `InRange`, `Throws`,
  `ThrowsAsync`, `Fail`, `Skip`.
- Only awaits are timed: a test that blocks the tick thread cannot be stopped. Never block.
- Run: `kiln test [id] [filter] [--exact name] [--json]`, `"tests": {"autoRun": true}` (at server start and after each hot reload),
  `echo "test my-mod" > kiln/control/t.cmd`, or `await Kiln.Testing.Tests.RunAsync("my-mod", filter)`.
- Results: kiln.log lines `PASS` / `FAIL` / `SKIP` tagged `[mod:<id>]`, and `kiln/logs/tests/<id>.json`
  (`passed`, `failed`, `skipped`, `results[]` with `outcome`, `message`, `stack`).

## Offline unit tests (Kiln.TestKit, dev.26+, in development)
Runs a mod under `dotnet test` with no game: Kiln.Host's real loader, a tick thread of its own, console commands, fake
players and saved data. Needs the dev.26 MDK (package `Kiln.TestKit`, same version as `Kiln.Sdk`). Page:
`docs/kiln/modding/unit-tests.md`.
```sh
dotnet new kiln-unit-test -n MyMod.Tests --mod ../MyMod/MyMod.csproj && dotnet test MyMod.Tests
```
```csharp
using var kiln = new KilnHarness();
var mod = kiln.Load<MyMod.MyModMod>();          // a second, isolated copy, as the game loads it
kiln.Step();                                    // first step: mods load, the server starts
Assert.True(mod.IsLoaded, mod.Error ?? kiln.DumpLogs());
var ada = kiln.Players.Join("Ada"); kiln.Step();
var r = kiln.RunCommand(ada, "mymod status");   // or RunCommand(line) as the console
var n = kiln.Run(() => Players.Count);          // Kiln API only through Run/As: it runs on the harness's tick thread
```
- `public sealed class ModKilnTests : KilnTestSuite<MyMod.MyModMod>;` (namespace `Kiln.TestKit.Xunit`) runs every
  `[KilnTest]` as an xunit test, in the in-game runner's order.
- Real offline: loading, lifecycle, events, scheduler, configs, commands, permissions, messages, locales, the fault policy.
  Faked: players, `WorldData`/`PlayerData`, `EntityData` on player entities only. Everything else (spawn, items, combat,
  content, chat's native side, network, rendering, hooks) reports itself unavailable, and a test that asked for one is
  skipped with its name; `AllowUnavailable` lets a suite run anyway. `kiln.Capabilities.Override(name, available)`.
- Gaps: a service that caches "no table" (`Kiln.Ui` render, `kiln.net` at the first channel) is asked once per process, so
  a later test can pass where it should be skipped: read the service's `Available` in the test. No client/server pair.
  One harness per process. A blocked test is stopped by the watchdog (Linux tested, Windows not).
- Offline green is not a server or client check: run `kiln test` on a test server as well.

## Guided client checks (`[ClientCheck]`, dev.26+, in development)
For what only a player can judge. Nothing of the runner has run in a real client yet. Page: `docs/kiln/modding/testing.md`.
```csharp
public override void OnLoad() => ClientChecks.Register(this);      // Kiln.Testing
[ClientCheck("blocks.frost-flash", "Frost flash on hit", Tags = [ClientCheckTags.Visual], Needs = ClientCheckNeeds.Hosted)]
public async Task Frost_flash(CheckContext c)
{
    c.Instruct("Hit the dummy three times. Does a blue flash appear?");
    await c.Until(() => Hits >= 3, TimeSpan.FromMinutes(2), "three hits");
    c.Expect(Hits == 3, "three hits counted");
    await c.Verdict();                                             // the player's pass, fail or skip
}
```
- Tick thread, like `[KilnTest]`. `CheckContext`: `Instruct`, `Expect` (recorded, never changes the verdict), `Verdict`,
  `OnEnd`, `Ticks`, `Until`, `Delay`, `Player` (`EntityId`), `Session`, `Cancellation`.
- The player opens the list with Ctrl+Shift+F5 (Ctrl+Shift+F6/F7/F8: pass, fail, skip); verdicts go to `kiln/state/client-checks.json`.
- A `cheats` check (level, xp, skill points, knowledge) starts only while the player's achievement guard protects and a
  `guard-proof` check passed on that build; it asks for a throwaway character every time.
- Write checks for what you could not see; don't report them as passed until a player ran them.
- Check sets to copy (dev.26+, none run by a player yet): item data (`mods/tests/instances-test/ClientChecks.cs`, area
  "Item data"), player states (`mods/samples/PlayerStates`, area "Player animations"), weapon moves and held models
  (`mods/tests/weapon-moves-test`, `blocks.moves-*`, `blocks.held-animated*`).
- Offline (`Kiln.TestKit`) item data, player states and weapon moves are unavailable: `kiln.items`, `kiln.playeranim` and
  `kiln.gamedata` have no fakes. Test them on a test server.

## Reading the logs
`kiln logs [-f] [--mod ID] [--level E] [-n N]`, or:

| File | What |
|---|---|
| `kiln/logs/kiln.log` | Kiln and every mod (`[mod:<id>]`), load order, failures with reasons, data-pack problems |
| `kiln/logs/mods.txt` | the current mod list with states |
| `kiln/logs/tests/<id>.json` | test results |
| `kiln/crash-reports/crash-*.txt` | a disabled mod, an unhandled exception, a native fault: description, system, mod list, stack, last log lines |
| the game's own log | `logs/enshrouded_server.log` in the server folder; `Context.GameLog.Line` mirrors it |

Success looks like `mod my-mod 0.1.0 loaded in … ms`. A fault looks like
`[E] [host] mod x threw in Pre (4/5): …`; after the limit, `disabled after 5 faults within 60 s; reload x to try
again; crash report …`. Fault settings live in `kiln/kiln.json` `mods` (`fault_limit`, `fault_window_s`,
`test_timeout_s`).

## Linting
`kiln lint`, the analyzers and game-id checks: [devkit.md](devkit.md). Impact programs: `impact.py lint my.bin`.
kiln-modding/references/vfx.mdParticles, lights and the game's VFX: files, budget, checks.Raw
# Particles, lights, the game's VFX

Wiki: [vfx](https://frostdev.io/kiln/wiki/vfx). `kiln lint` checks all of it.

## What's been seen in a real game
Nothing built by Kiln, so far: files, budget and checks are proven offline, and effects have run on real GPUs in a test harness. Say "needs a client check" about how anything looks, or whether it shows. A dedicated server draws nothing. Kept current at
https://frostdev.io/kiln/wiki/status.

## Writing one
- Scaffold: `kiln new vfx MyFx`. Bigger example: `mods/samples/VfxSample`.
- A `vfx` section in `blocks/*.json`, or `Blocks.Define("name", new VfxBlock { ... })` with `KilnServices` `Blocks`. Prefer data: a block in a file is there from the first start, and one from code from the next, because the game lists VFX once.
- A particle block needs `burst` or `rate`. Textures are PNGs in `vfx/`, power of two, 4 to 2048.
- Attach with a `Vfx` field on an effect, area, on-hit, cast, projectile or status: a block's name, `"other-mod:name"`, `"game:<name>"` or a GUID. From code: `Vfx.PlayAt`, `Vfx.PlayOn`, `Vfx.Stop`.

## The game's VFX
- Never guess a name: `kiln catalog search fireball --section vfx`, then `kiln vfx info <name>` for its length and parameters.
- VFX have no names of their own. A name is a template or sequence that uses it. Catalog entries without `byName` only have a label: write `"game:<guid>"`. `kiln lint` says when (KL323).
- `GameVfx.*` constants hold GUIDs and need `<KilnGameIds>Vfx</KilnGameIds>` and a catalog. Don't use them in a project others build without one.
- **Parameters** only work on an effect, area or on-hit, and only on a game VFX: `{ "name": "game:X", "parameters": { "radius": 3, "color": "#ff0000", "count": { "int": 7 } } }`. Kiln refuses them on a cast, projectile, status or a VFX block. The game ignores a wrong name or type silently, so run `kiln lint` with a catalog (KL321).

## Rules that matter
- A new VFX needs `side` both and `clients` `mod` (KL211). Only naming the game's VFX doesn't.
- All mods share room for 473 new VFX, 386 meshes, about 240 KB of mesh memory and 10 sounds in VFX (KL320). Past a limit Kiln refuses the whole mod.
- A VFX block's `sound` is counted but not played yet. No decals, fog, custom meshes or custom shaders yet.
- From code, a play reaches only Kiln clients near it, and joiners don't see it. For what every player must see, attach it to data.
kiln-modding/references/pitfalls.mdCommon mistakes and what to do instead.Raw
# Common mistakes

| Mistake | What happens | Do instead |
|---|---|---|
| Calling a service from `Task.Run`, a timer or a socket callback | `InvalidOperationException` ("wrong thread") or a race with the game's jobs | `Context.Scheduler.Post(...)` or `await Context.Scheduler.NextTick()` first |
| `.Result` / `.Wait()` / `Thread.Sleep` on the tick thread | the server's step stalls; a blocked test cannot time out | `await` (continuations come back to the tick thread) |
| Changing content in `OnLoad` directly | the registries may not be ready, and the change is not applied again at the next world load or pack reload | `Content.OnReady(_ => ...)`; register new items in `OnLoad` |
| Setting the time of day or touching world state in `OnLoad` | the save load overwrites it; on a client there may be no world yet | do it in `ServerStartedEvent` (or `Client.Tick` on a client) |
| A new GUID on every build, or changing a released GUID | saved items disappear or become `[Missing: …]` placeholders | generate GUIDs once and hard-code them |
| Shipping `Kiln.Api.dll` or `Kiln.<Service>.dll` in the mod folder | type identity breaks; the mod may not load | let Kiln.Sdk reference them (`ExcludeAssets=runtime`) |
| Guessing field paths or item names | the pack logs "unknown field" / "selector matched nothing" and does nothing | `kiln catalog search`, or `Content.Dump()` (`kiln/data/content/dump.json`); `kiln lint` |
| Static caches, threads, timers or event handlers on non-Kiln objects | the old load context never unloads; `reload-test` reports leaks | keep state on the mod instance; stop what you start in `OnUnload` |
| `RegisterJson<T>()` with reflection for network messages | keeps an unloaded mod in memory after a hot reload for a while | source-generated `JsonTypeInfo<T>` or `INetMessage<T>` |
| `PlaySequence` of a cast to make an NPC or player cast a spell | the animation plays, the spell does not fire | `Abilities.Cast` / `RunImpact` for the effect |
| Expecting vanilla clients to see new items, icons or models | refused; with `modSync: off`, let in without new entities, and a Kiln item can crash them | state `clients` as `kiln` or `mod`; a server modpack delivers `mod` mods |
| Leaving `clients` unstated on a `both` mod that vanilla clients can't use | vanilla clients aren't refused: the default doesn't count | state `kiln` or `mod` |
| `"clients": "kiln"` for quests, clones or roster NPCs | the game's journal stays empty: the client builds them from the mod's files at boot | `"clients": "mod"`, `"side": "both"` (KL210) |
| Spawning a named NPC the world hasn't unlocked | the game destroys it within seconds | `place`, grant its unlock, or a `test_Townsfolk_*` villager |
| Reordering options, or renaming quests, steps or nodes, in native dialogue | ids change; saves drop what they held | keep names; add options at the end |
| A journal quest with no `name`, a step with no `text` or `title`, `roster` with no `clone` | KL312; that mod gets none of the game's UI, and its quests run with chat lines | fix what lint reports. Any `if` is fine: what one knowledge check can't say is about a second behind |
| Code-defined game-tree nodes on a dedicated server | not in the tree until the next start | `skills/*.json` |
| Comments or trailing commas in `assets/cook.json` | the cook fails (KL002, KL003) | plain JSON |
| A voice-over with an empty language slot | silence for players with that voice language | `"fallback": "en"`, if that's what you want |
| A Release build under the debugger | breakpoints never bind | deploy a Debug build |
| `kill -9` on netcoredbg while attached | the server dies with no save | disconnect from the editor |
| Raising max stacks, then removing the mod | after a restart without it, stacks above the vanilla size are cut on their next move | tell users; this is the game's own rule |
| Shipping cooked models, Impact listings or other game-derived files | redistributes game data | ship sources and a `cook.json` recipe; `assets/cooked/` is gitignored |
| Replacing a registered Impact program in place | refused: running instances read program code live | clone it to a new GUID |
| A Kiln impact without damage configs | it hits for 0 (no used item) | pass `Damage1` (e.g. `DamageSetup.FireOnly(10f)`) in the configs |
| `Abilities.PlaySequence` on a sequence that consumes the used item (every potion and food) | it has no used item, and the game reads one with no check: the server dies | Kiln refuses it and logs why: play a copy with the event's `consume` off, or let the player use the item |
| An Impact native's argument left unconnected | a native that reads it with no check kills the server (`hasAttackCategory`'s `matchAll`) | the linter says `E_UNCONNECTED`, and kiln.abilities refuses the program |
| An Impact loop without an exit | hangs the server's simulation step | the linter warns (`W_LOOP`): add an exit |
| Many hits on one target in one step | the engine applies at most 4 impacts per target per step | Kiln defers the rest; don't rely on them landing in the same step |
| Two mods detouring the same very hot function (advanced tier) | under Wine a hot reload of one can stall | one mod per hot function |
| `_ReturnAddress()` in a native hook handler (advanced tier) | another link in front of yours hides the game's call site | `RawHook::caller()`, read before calling the original |
| `Hooks.Detour`/`Hooks.Mid` per command, or a native hook whose label or user pointer changes each time | each such install takes a new link slot; links are never freed, and after 65,536 hooking fails | install once, switch a flag; an identical native re-install reuses its slot |
| Testing against the live server | players see your half-done mod; a fault disables it in front of them | a separate local test server |
| Claiming a client-side effect works after a server-only test | a dedicated server draws nothing, has no UI and no sound | list it as "needs a client check" |

## dev.20+ additions

| Mistake | Result | Use instead |
|---|---|---|
| Take then give to move gear | makes a new item entity, losing instance state | `Items.Move` |
| Treating gear `EntityData` as saved item payload | no stable positional identity; data is session-only | check `IsPersistent`; persistent item data/affixes are not supplied |
| Registering an ECS scheduler declaration in ordinary `OnLoad` | world registration has already closed | ship `kiln.programs.json`, then `EcsPrograms.Bind` in `OnLoad` |
| Calling game services from an ECS worker callback | wrong thread and undeclared access | declared context access, or post tick work |
| A UI block `command` intended for a joined server | runs only in this game's command registry | `castAbility` / `applyEffect` with the mod on the server |
| A factory recipe with `NeedsProps` | refused: no nearby-prop context on that path | use a manual station |

## dev.21+ additions

| Mistake | Result | Use instead |
|---|---|---|
| A mid hook that sets `TrampolineRsp` to anything but `Rsp` to return early | the stub resumes with `rsp = TrampolineRsp; ret`, so any other value returns to the wrong address | at a function's first instruction: `r.Rax = value; r.TrampolineRsp = r.Rsp;` |
| Installing the same raw handler twice on one function for two links | the second install replaces the first; one handler has one live link per function | one handler function per link |
| Expecting a priority-0 link to run before an EMF or Shroudtopia mod's hook | their links run first (priority 1, newest first) | a higher priority, or `before` their owner (`emf:<id>`) |
| An `early` or `boot` native mod that `requires` a mod whose DLL is in `chain.ini [late]` | it fails at startup (the late DLL loads after all plugins) | a `world`-phase mod, or load the DLL early |
| `Overrides.Claim` (or a write through a resource got there) from `Task.Run` or a timer | `InvalidOperationException`; before dev.21 the change counted as Kiln's and was never given back | `Context.Scheduler.Post`; get handles in `OnLoad` |
| Reading a `null` from `Texts.Get` as "no such text" | it also means another thread is loading the catalog | check `Texts.Status`; `await Texts.Preload()` first |
| A native transfer handler that counts or logs every call | dry-run moves call it too (`KILN_TRANSFER_DRY_RUN`) | skip side effects when the flag is set |
| File or folder names that end in a dot or a space in a mod zip | the launcher refuses the zip: Windows would turn them into other names | rename them |

## dev.26+ additions (in development; not seen by a player)

| Mistake | Result | Use instead |
|---|---|---|
| A managed service's capability (`items.moves`) in the manifest's `features` | refused: the service registers its row only after manifests are checked | sigdb features and `kiln.<service>` only; `Capabilities.Is(name)` at run time for the rest |
| Trusting `[RequiresCapability]` to guard a call | it is documentation; the member's own code throws `KilnUnavailableException` | `Capabilities.Is` / `Why` before the call |
| Taking `EntityRef.IsAlive` as "the same entity" for a world entity loaded from the save | no `SpawnTime`, serial 0: `IsAlive` only says the id exists | check `HasSerial` |
| `EntityRef.Of` / `PlayerRef.Of` in `OnLoad` | the identity sources start at the first tick: unavailable | `ServerStartedEvent` or later |
| `x == EntityRef.None` | false for a none id with a serial (record equality) | `x.IsNone` |
| Catching only `KilnUnavailableException` | older services still throw their own exception for an off capability | catch `NotSupportedException` too (it derives from it) |
| `Mods.ApiVersion` to learn the running Kiln's API | it is the value your mod was built with | `Mods.RuntimeApiVersion` |
| Calling `Rules.Simulate`, `QuestInput`, `GameTree.Lab*` and other lab helpers | `KILNX001` build error; they may change without notice | test kit or a lab mod with `KILNX001` in `NoWarn` |
| `OriginalCode`, `Keep`, `Arsenals.JobHandler`, `Is.Job`/`Act.Job`, `Abilities.Programs.Register`, `NativeCall.Bytes` without `"advanced": true` | `InvalidOperationException` | `"advanced": true`, or a typed service call |
| Writing a `Kiln.Ecs` struct you read and expecting the game to change | it is a copy | `World.TryWrite` (raw, not saved) until typed writes (dev.27) |
| An enum value of `Kiln.Ecs` used only as a constant | folded into a number: a renumbering after a game update is not caught at load | check `Ecs.Supports<T>()` for the type that holds it |
| A test that passes under `dotnet test` taken as proof for the server | most native services are unavailable offline; cached lookups can hide that | `kiln test` on a test server too |
| Relying on `SteamGuard` for `/questadmin` or rewards paid at a later join | not covered | a throwaway character; experimental, never run in a real client |
| `Persist.Carried` for item data that travels with a character | `KilnUnavailableException("saves.instance_data.carried")` | `Persist.World`; not in this release |
| Treating a set-aside item data record as lost | it waits in `InstanceData.Parked(key)`; Kiln never moves it to another item by itself | read `Parked`, `/instances`; decide in the mod |
| `InstanceData` on a plain stack (wood, arrows) | no item entity: nothing to keep data on | data on the holder (`EntityData`, `PlayerData`) |
| Changing `assets/player-states.json` and hot-reloading | states stay as they were at boot | restart the game and the server |
| Player states on a server that did not cook the mod, or a mod without `"side": "both"`, `"clients": "mod"` | states refused; clients that cooked are refused at join (graph hash differs) | cook on the server too; state both |
| `Assets.Override` / `Register` on the player graph's key, or an `assets.json` entry for it | `ArgumentException`; the entry is refused at boot | `assets/player-states.json` |
| A second cast of a mod's wand state before the first ended | the clip continues, it does not restart | design the clip for that, or a longer cooldown |
| A move's `animation` by name on a build without `anim.player_states` | the move keeps its `like` state; the name is reported by block and slot | check the report; a GUID or `0x` id needs no lookup |
| `blocks.weapon_moves` or `items.*` in a manifest's `features` | refused: the rows appear after manifests are checked | `Capabilities.Is` at run time |
| Moving Kiln's data folder, then building a native mod | CMake keeps the old SDK path | delete the project's `build` folder once |
kiln-modding/references/docs-map.mdWhich wiki page answers which question.Raw
# Where the answers are

The wiki: https://frostdev.io/kiln/wiki/<page>.

| Question | Page |
|---|---|
| Start a project | `first-mod`, `dev-kit` |
| Make a spell, a status, an effect, an item or a weapon | `blocks`, `new-items` |
| Make a prop, a station that works on its own (a forge, a drying rack), comfort | `world-blocks` |
| A cosmetic, a hat, a cape: a model worn on the body | `blocks`, `models` |
| The `kiln` command | `cli` |
| Manifest, sides, `clients`, version ranges | `manifest` |
| Events, configs, commands, saved data, other mods | `code-mods` |
| Hot reload, the control folder | `hot-reload` |
| In-server tests | `testing` |
| Why a mod didn't load, faults, breakpoints | `debugging` |
| Linter and analyzers | `lint` |
| VS Code | `vscode` |
| Game ids | `catalogs` |
| Items, recipes, loot, drops | `data-packs` |
| New items | `new-items` |
| Move gear with its state; quick-stack rules; chest names | `inventory` |
| Boot ECS declarations, worker callbacks, other-entity writes | `world-programs` |
| Spells, creatures, attacks as data; clones | `game-data` |
| One creature, variants, spawn rules, guard | `creatures` |
| Scripted abilities, per-cast tweaks, buffs | `abilities` |
| Impact programs | `impact` |
| Levels, XP, skill points | `progression` |
| New skill nodes, talent trees | `talents` |
| Quests, conversations | `quests` |
| Placed and new NPCs, the game's journal and menus | `npcs` |
| Sounds, music, voice | `sounds` |
| Particles, lights, the game's VFX: [vfx.md](vfx.md) | `blocks`, `shaders` |
| Icons, retextures, resources | `assets` |
| 3D models | `models` |
| New surfaces for models from PNGs and numbers | `blocks`, `shaders` |
| Terrain, props, schematics, zones | `world-edits` |
| Menus, screens, crafting tabs, HUD, keys | `game-ui`; data blocks: `ui-blocks` |
| Network channels, synced configs, client code | `multiplayer` |
| Mods of other engines (EML, EMF, closed DLL mods), chain loading | `other-mods` |
| Forge concepts | `forge` |
| What works today | `status` |

dev.26+ (in development; pages in the repository's `docs/kiln/`, not on the wiki yet):

| Question | Page |
|---|---|
| What a mod cannot do yet, hard caps, the planned release | `modding/limits.md` |
| Error rule, `EntityRef`/`PlayerRef`, side and thread markers | `modding/contract.md` |
| What works on this build and why not (`Capabilities`, `capabilities.json`) | `services/caps.md` |
| Typed ECS reads (`Kiln.Ecs`), layout stamps | `typegen.md` §4, §6 |
| Unit tests with no game (`Kiln.TestKit`) | `modding/unit-tests.md` |
| Guided client checks (`[ClientCheck]`) | `modding/testing.md`; the pack: `client-pass.md` |
| The achievement guard (`Kiln.SteamGuard`) | `services/steamguard.md` |
| Data on one item (`InstanceData`) | `modding/inventory.md` "Data on one item"; `services/saves.md`, `services/items.md` |
| A mod's own player animation states (`assets/player-states.json`) | `modding/animations.md` "Player states"; `services/playeranim.md` |
| Weapon moves, animated held models, clips reworked at cook time | `modding/item-blocks.md` "Moves"; `modding/animations.md` "Clips reworked" |

dev.26+ samples: ItemData, PlayerStates, WeaponMoves.

Sample mods: Hello, SampleLib, SampleConsumer, SampleTests, BigStacks, NewItems, SpellTweaks, CreatureTweaks, EliteVukah, FireNova,
ImpactSample, AssetSample, ModelSample, SoundSample, VfxSample, SkillTreeSample, quests-sample, NpcSample, Schematics, MenuButtons, UiSample,
NetSample, Claims. dev.20+: UiBlocks, WorldPrograms and the block templates in [setup.md](setup.md).

Kiln's reverse-engineering notes are for its own developers. Mods never need them. Don't copy addresses or offsets into mod code.

Enshrouded is a trademark of Keen Games GmbH. Kiln is unofficial.