# Custom NPC sample A light gamemode that registers one custom NPC classname, `nm_sample_npc`, using the TF2Helpers friendly layer. ## What it shows - `SampleNpc` derives `FCustomEntity` and is declared `[CustomEntity("nm_sample_npc", "base_boss")]`. The classname is a factory alias of `base_boss`: spawning it gives a real `base_boss` (same ServerClass and datamap) that carries the new classname. It is not a new engine class. - The source generator emits `NewMoon.Generated.CustomEntityTable.All` for the assembly, and `CustomNpcSample.OnPluginStart` passes it to `RegisterCustomEntities`. - The NPC precaches `models/bots/headless_hatman.mdl` at registration and logs `OnSpawn`, `OnActivate` and `OnRemoved`. - Unloading the plugin despawns the live NPCs and removes the alias. ## Where it goes Drop the folder at the monorepo root, beside `NewMoon.Gamemodes.Sample.Bare`, and add it to `LoadoutSharp.slnx`. The csproj uses relative paths (`..\ThirdParty\...`, `..\NewMoon.Plugins.TF2Helpers.Api\build\NewMoon.GameMode.props`) that assume that location. ## Build ```bash .claude/scripts/build-gate.sh dotnet build NewMoon.Gamemodes.Sample.CustomNpc/NewMoon.Gamemodes.Sample.CustomNpc.csproj ``` The first build reports NMFG001: every game mode build checks its `[CustomEntity]` classes against the checked-in FGD (`NewMoon.Plugins.TF2Helpers.Api/Fgd/newmoon_custom_entities.fgd`), which has no block for `nm_sample_npc` yet. Regenerate the FGD, then build again: ```bash .claude/scripts/build-gate.sh ./build.sh GenerateCustomEntityFgd ``` To try the sample without touching the FGD, build with `-p:NewMoonCustomEntityFgdCheck=false`. Run the same command again whenever you change the class's keys, inputs or outputs. ## Try it on a dev server Load the gamemode, then from a console: ``` ent_create nm_sample_npc health 500 model models/bots/headless_hatman.mdl ``` `ent_create` writes the keyvalues before spawning, which matters: a `base_boss` spawns with 0 health unless `health` is set before spawn, and it needs a `model`. Expect the log to show `nm_sample_npc OnSpawn `, then `OnActivate ` on the next tick, and `OnRemoved ` when the NPC is killed, removed or the plugin unloads. The NPC stands still. From code, the equivalent is `Map.CreateEntity("nm_sample_npc").KeyValue("health", 500).Model("...").At(origin).Spawn()`. ## Limits - The NPC has no locomotion or AI. A bare `base_boss` stands still and has no animation driver, so it holds a pose. It also inherits the stock boss side effects (currency drop, crushing buildings, shoving players). - The class cannot set keyvalues or its model before spawn: `OnSpawn` runs after the native spawn. The spawner supplies them. - Keyvalue routing into `[CustomKey]` properties, `[CustomInput]` calls and `[CustomOutput]` firing are planned work (AB#29804 to AB#29807) and are not used here. - The engine sees only the `base_boss` datamap, so `ent_text` and `ent_dump` do not show managed state. - On a server's first map after start, plugins load after the map's entity lump is parsed, so a map-placed `nm_sample_npc` is not created on that map. `ent_create` works at any time.