| @@ -12,7 +12,7 @@ namespace NewMoon.Samples.NPC; | |||
| 12 | 12 | public static partial class NewMoonSampleNPCLogger | |
| 13 | 13 | { | |
| 14 | 14 | [LoggerMessage(1, LogLevel.Information, "Plugin started")] | |
| 15 | - | public static partial void LogStarted(this ILogger logger, int count); | |
| 15 | + | public static partial void LogStarted(this ILogger logger); | |
| 16 | 16 | ||
| 17 | 17 | [LoggerMessage(2, LogLevel.Information, "Plugin ended")] | |
| 18 | 18 | public static partial void LogEnded(this ILogger logger); | |
screwdriver / gist:f99d5bf9d8f14fe2a9a3bf56d4ac0c66
Last active 33 minutes ago
Luno revised this gist 33 minutes ago · ba3eacc
2 files changed, 8 insertions, 12 deletions
| @@ -7,18 +7,14 @@ namespace NewMoon.Samples.NPC; | |||
| 7 | 7 | /// <c>base_boss</c> that carries the new classname, and the registry calls the lifecycle overrides below. | |
| 8 | 8 | /// </summary> | |
| 9 | 9 | /// <remarks> | |
| 10 | - | /// The class only logs. Reading keyvalues, inputs and outputs from a custom entity is not part of the landed API yet, so the | |
| 11 | - | /// spawner supplies <c>health</c> and <c>model</c>, for example <c>ent_create npc_bot_newmoon health 500 model models/bots/headless_hatman.mdl</c>. The registry needs an accessible parameterless constructor. | |
| 10 | + | /// The class only logs, through the <see cref="FCustomEntity.PLogger"/> the registry supplies. The spawner supplies <c>health</c> | |
| 11 | + | /// and <c>model</c>, for example <c>ent_create npc_bot_newmoon health 500 model models/bots/headless_hatman.mdl</c>. The registry | |
| 12 | + | /// constructs the class through dependency injection, so it needs a public constructor; any constructor parameters are resolved | |
| 13 | + | /// from the plugin's services. The plugin registers it automatically at start-up. | |
| 12 | 14 | /// </remarks> | |
| 13 | 15 | [CustomEntity("npc_bot_newmoon", "base_boss", PrecacheModels = ["models/bots/headless_hatman.mdl"])] | |
| 14 | 16 | internal sealed class NPCBotNewMoon : FCustomEntity | |
| 15 | 17 | { | |
| 16 | - | /// <summary> | |
| 17 | - | /// The logger the callbacks write to; set by <see cref="CustomNpcSample"/> at start-up, because the registry creates this | |
| 18 | - | /// object through its parameterless constructor. | |
| 19 | - | /// </summary> | |
| 20 | - | internal static ILogger? Log { get; set; } | |
| 21 | - | ||
| 22 | 18 | // Captured at spawn so the remove log never depends on the view still resolving while the instance is torn down. | |
| 23 | 19 | private int _index = -1; | |
| 24 | 20 | ||
| @@ -26,12 +22,12 @@ internal sealed class NPCBotNewMoon : FCustomEntity | |||
| 26 | 22 | protected override void OnSpawn() | |
| 27 | 23 | { | |
| 28 | 24 | _index = Entity.Index; | |
| 29 | - | Log?.LogNPCSpawn(_index); | |
| 25 | + | PLogger.LogNPCSpawn(_index); | |
| 30 | 26 | } | |
| 31 | 27 | ||
| 32 | 28 | /// <inheritdoc/> | |
| 33 | - | protected override void OnActivate() => Log?.LogNPCActivate(_index); | |
| 29 | + | protected override void OnActivate() => PLogger.LogNPCActivate(_index); | |
| 34 | 30 | ||
| 35 | 31 | /// <inheritdoc/> | |
| 36 | - | protected override void OnRemoved() => Log?.LogNPCRemoved(_index); | |
| 32 | + | protected override void OnRemoved() => PLogger.LogNPCRemoved(_index); | |
| 37 | 33 | } | |
screwdriver
revised this gist 50 minutes ago
·
3ccb3c7
2 files changed, 6 insertions, 11 deletions
| @@ -4,15 +4,10 @@ namespace NewMoon.Samples.NPC; | |||
| 4 | 4 | /// An example gamemode that registers one custom NPC entity, <c>npc_bot_newmoon</c>, which fronts <c>base_boss</c>. | |
| 5 | 5 | /// </summary> | |
| 6 | 6 | [GameMode("Custom NPC Sample", "custom-npc-sample", Description = "Registers a custom NPC entity as an example.")] | |
| 7 | - | public sealed partial class CustomNpcSample : FGamePlugin | |
| 7 | + | public sealed partial class NewMoonSampleNPC : FGamePlugin | |
| 8 | 8 | { | |
| 9 | 9 | /// <inheritdoc/> | |
| 10 | - | protected override void OnPluginStart() | |
| 11 | - | { | |
| 12 | - | // The logger is set first: adoption inside the registration can raise OnSpawn for NPCs that already exist. | |
| 13 | - | NPCBotNewMoon.Log = PLogger; | |
| 14 | - | PLogger.LogRegistered(RegisterCustomEntities(NewMoon.Generated.CustomEntityTable.All)); | |
| 15 | - | } | |
| 10 | + | protected override void OnPluginStart() => PLogger.LogStarted(); | |
| 16 | 11 | ||
| 17 | 12 | /// <inheritdoc/> | |
| 18 | 13 | protected override void OnPluginEnd() => PLogger.LogEnded(); | |
| @@ -9,12 +9,12 @@ namespace NewMoon.Samples.NPC; | |||
| 9 | 9 | /// Source-generated log messages. | |
| 10 | 10 | /// </summary> | |
| 11 | 11 | [UsedImplicitly] | |
| 12 | - | public static partial class CustomNPCSampleLogger | |
| 12 | + | public static partial class NewMoonSampleNPCLogger | |
| 13 | 13 | { | |
| 14 | - | [LoggerMessage(1, LogLevel.Information, "Custom NPC sample registered {Count} custom entity classes")] | |
| 15 | - | public static partial void LogRegistered(this ILogger logger, int count); | |
| 14 | + | [LoggerMessage(1, LogLevel.Information, "Plugin started")] | |
| 15 | + | public static partial void LogStarted(this ILogger logger, int count); | |
| 16 | 16 | ||
| 17 | - | [LoggerMessage(2, LogLevel.Information, "Custom NPC sample ended: OnPluginEnd ran before the scope released")] | |
| 17 | + | [LoggerMessage(2, LogLevel.Information, "Plugin ended")] | |
| 18 | 18 | public static partial void LogEnded(this ILogger logger); | |
| 19 | 19 | ||
| 20 | 20 | [LoggerMessage(3, LogLevel.Information, "npc_bot_newmoon OnSpawn {Index}")] | |
screwdriver
revised this gist 1 hour ago
·
ec43675
7 files changed, 55 insertions, 311 deletions
| @@ -1,26 +0,0 @@ | |||
| 1 | - | using NewMoon.TF2Helpers; | |
| 2 | - | ||
| 3 | - | namespace NewMoon.Samples.CustomNpc; | |
| 4 | - | ||
| 5 | - | /// <summary> | |
| 6 | - | /// A light sample gamemode that registers one custom NPC classname, <c>nm_sample_npc</c>, which fronts <c>base_boss</c>. | |
| 7 | - | /// </summary> | |
| 8 | - | /// <remarks> | |
| 9 | - | /// The generator emits <c>NewMoon.Generated.CustomEntityTable.All</c> for this assembly, one descriptor per <c>[CustomEntity]</c> | |
| 10 | - | /// class. <see cref="FGamePlugin.RegisterCustomEntities"/> hands them to the registry, which installs the classname as a factory | |
| 11 | - | /// alias of <c>base_boss</c>. The plugin scope removes the alias, and despawns the live NPCs, when the plugin unloads. | |
| 12 | - | /// </remarks> | |
| 13 | - | [GameMode("Custom NPC Sample", "custom-npc-sample", Description = "Registers one custom NPC classname fronting base_boss.")] | |
| 14 | - | public sealed partial class CustomNpcSample : FGamePlugin | |
| 15 | - | { | |
| 16 | - | /// <inheritdoc/> | |
| 17 | - | protected override void OnPluginStart() | |
| 18 | - | { | |
| 19 | - | // The logger is set first: adoption inside the registration can raise OnSpawn for NPCs that already exist. | |
| 20 | - | SampleNpc.Log = PLogger; | |
| 21 | - | PLogger.LogRegistered(RegisterCustomEntities(NewMoon.Generated.CustomEntityTable.All)); | |
| 22 | - | } | |
| 23 | - | ||
| 24 | - | /// <inheritdoc/> | |
| 25 | - | protected override void OnPluginEnd() => PLogger.LogEnded(); | |
| 26 | - | } | |
| @@ -1,28 +0,0 @@ | |||
| 1 | - | using JetBrains.Annotations; | |
| 2 | - | using Microsoft.Extensions.Logging; | |
| 3 | - | ||
| 4 | - | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member | |
| 5 | - | ||
| 6 | - | namespace NewMoon.Samples.CustomNpc; | |
| 7 | - | ||
| 8 | - | /// <summary> | |
| 9 | - | /// Source-generated log messages for <see cref="CustomNpcSample"/> and <see cref="SampleNpc"/>. | |
| 10 | - | /// </summary> | |
| 11 | - | [UsedImplicitly] | |
| 12 | - | public static partial class CustomNpcSampleLogger | |
| 13 | - | { | |
| 14 | - | [LoggerMessage(1, LogLevel.Information, "Custom NPC sample registered {Count} custom entity classes.")] | |
| 15 | - | public static partial void LogRegistered(this ILogger logger, int count); | |
| 16 | - | ||
| 17 | - | [LoggerMessage(2, LogLevel.Information, "Custom NPC sample ended: OnPluginEnd ran before the scope released.")] | |
| 18 | - | public static partial void LogEnded(this ILogger logger); | |
| 19 | - | ||
| 20 | - | [LoggerMessage(3, LogLevel.Information, "nm_sample_npc OnSpawn {Index}")] | |
| 21 | - | public static partial void LogNpcSpawn(this ILogger logger, int index); | |
| 22 | - | ||
| 23 | - | [LoggerMessage(4, LogLevel.Information, "nm_sample_npc OnActivate {Index}")] | |
| 24 | - | public static partial void LogNpcActivate(this ILogger logger, int index); | |
| 25 | - | ||
| 26 | - | [LoggerMessage(5, LogLevel.Information, "nm_sample_npc OnRemoved {Index}")] | |
| 27 | - | public static partial void LogNpcRemoved(this ILogger logger, int index); | |
| 28 | - | } | |
| @@ -1,44 +0,0 @@ | |||
| 1 | - | <Project Sdk="Microsoft.NET.Sdk"> | |
| 2 | - | <PropertyGroup> | |
| 3 | - | <RootNamespace>NewMoon.Samples.CustomNpc</RootNamespace> | |
| 4 | - | <Version>1.0.0</Version> | |
| 5 | - | <Platforms>AnyCPU</Platforms> | |
| 6 | - | </PropertyGroup> | |
| 7 | - | ||
| 8 | - | <PropertyGroup> | |
| 9 | - | <Title>Custom NPC Sample Gamemode</Title> | |
| 10 | - | <Authors>Jon S. (Serafeline)</Authors> | |
| 11 | - | <PackageId>NewMoon.Gamemodes.Sample.CustomNpc</PackageId> | |
| 12 | - | <PackageVersion>$(Version)</PackageVersion> | |
| 13 | - | <IsPackable>false</IsPackable> | |
| 14 | - | <PackageDescription>A light sample gamemode that registers one custom NPC classname (nm_sample_npc) fronting base_boss through the TF2Helpers custom entity API.</PackageDescription> | |
| 15 | - | </PropertyGroup> | |
| 16 | - | ||
| 17 | - | <ItemGroup> | |
| 18 | - | <PackageReference Include="JetBrains.Annotations" /> | |
| 19 | - | <PackageReference Include="Microsoft.Extensions.Configuration.Json" /> | |
| 20 | - | </ItemGroup> | |
| 21 | - | ||
| 22 | - | <ItemGroup> | |
| 23 | - | <ProjectReference Include="..\ThirdParty\NewMoon.SourceNet\managed\SourceNet.Core\SourceNet.Core.csproj"> | |
| 24 | - | <Private>false</Private> | |
| 25 | - | </ProjectReference> | |
| 26 | - | <ProjectReference Include="..\ThirdParty\NewMoon.SourceNet\managed\SourceNet.Engine.Source\SourceNet.Engine.Source.csproj"> | |
| 27 | - | <Private>false</Private> | |
| 28 | - | </ProjectReference> | |
| 29 | - | <ProjectReference Include="..\ThirdParty\NewMoon.SourceNet\managed\SourceNet.Core.SourceGeneration\SourceNet.Core.SourceGeneration.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> | |
| 30 | - | <ProjectReference Include="..\ThirdParty\NewMoon.SourceNet\managed\SourceNet.Common\SourceNet.Common.csproj"> | |
| 31 | - | <Private>false</Private> | |
| 32 | - | </ProjectReference> | |
| 33 | - | <ProjectReference Include="..\NewMoon.Shared.Utility\NewMoon.Shared.Utility.csproj" /> | |
| 34 | - | </ItemGroup> | |
| 35 | - | ||
| 36 | - | <!-- Task 20's shared game mode props: the [GameMode] startup generator and the NMGM analyzers as one analyzer reference | |
| 37 | - | (analyzer references do not flow through project references), plus TeamFortress2.Api, SDKWrappers.Api, and | |
| 38 | - | TF2Helpers.Api as Private=false references (the host supplies them through PluginUseAPIAssembly). The sample uses no | |
| 39 | - | LoadoutSharp types, so that fourth API reference is left out. --> | |
| 40 | - | <PropertyGroup> | |
| 41 | - | <GameModeUsesLoadoutSharpApi>false</GameModeUsesLoadoutSharpApi> | |
| 42 | - | </PropertyGroup> | |
| 43 | - | <Import Project="..\NewMoon.Plugins.TF2Helpers.Api\build\NewMoon.GameMode.props" /> | |
| 44 | - | </Project> | |
| @@ -0,0 +1,19 @@ | |||
| 1 | + | namespace NewMoon.Samples.NPC; | |
| 2 | + | ||
| 3 | + | /// <summary> | |
| 4 | + | /// An example gamemode that registers one custom NPC entity, <c>npc_bot_newmoon</c>, which fronts <c>base_boss</c>. | |
| 5 | + | /// </summary> | |
| 6 | + | [GameMode("Custom NPC Sample", "custom-npc-sample", Description = "Registers a custom NPC entity as an example.")] | |
| 7 | + | public sealed partial class CustomNpcSample : FGamePlugin | |
| 8 | + | { | |
| 9 | + | /// <inheritdoc/> | |
| 10 | + | protected override void OnPluginStart() | |
| 11 | + | { | |
| 12 | + | // The logger is set first: adoption inside the registration can raise OnSpawn for NPCs that already exist. | |
| 13 | + | NPCBotNewMoon.Log = PLogger; | |
| 14 | + | PLogger.LogRegistered(RegisterCustomEntities(NewMoon.Generated.CustomEntityTable.All)); | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | /// <inheritdoc/> | |
| 18 | + | protected override void OnPluginEnd() => PLogger.LogEnded(); | |
| 19 | + | } | |
| @@ -0,0 +1,28 @@ | |||
| 1 | + | using JetBrains.Annotations; | |
| 2 | + | using Microsoft.Extensions.Logging; | |
| 3 | + | ||
| 4 | + | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member | |
| 5 | + | ||
| 6 | + | namespace NewMoon.Samples.NPC; | |
| 7 | + | ||
| 8 | + | /// <summary> | |
| 9 | + | /// Source-generated log messages. | |
| 10 | + | /// </summary> | |
| 11 | + | [UsedImplicitly] | |
| 12 | + | public static partial class CustomNPCSampleLogger | |
| 13 | + | { | |
| 14 | + | [LoggerMessage(1, LogLevel.Information, "Custom NPC sample registered {Count} custom entity classes")] | |
| 15 | + | public static partial void LogRegistered(this ILogger logger, int count); | |
| 16 | + | ||
| 17 | + | [LoggerMessage(2, LogLevel.Information, "Custom NPC sample ended: OnPluginEnd ran before the scope released")] | |
| 18 | + | public static partial void LogEnded(this ILogger logger); | |
| 19 | + | ||
| 20 | + | [LoggerMessage(3, LogLevel.Information, "npc_bot_newmoon OnSpawn {Index}")] | |
| 21 | + | public static partial void LogNPCSpawn(this ILogger logger, int index); | |
| 22 | + | ||
| 23 | + | [LoggerMessage(4, LogLevel.Information, "npc_bot_newmoon OnActivate {Index}")] | |
| 24 | + | public static partial void LogNPCActivate(this ILogger logger, int index); | |
| 25 | + | ||
| 26 | + | [LoggerMessage(5, LogLevel.Information, "npc_bot_newmoon OnRemoved {Index}")] | |
| 27 | + | public static partial void LogNPCRemoved(this ILogger logger, int index); | |
| 28 | + | } | |
| @@ -1,19 +1,17 @@ | |||
| 1 | 1 | using Microsoft.Extensions.Logging; | |
| 2 | - | using NewMoon.TF2Helpers; | |
| 3 | - | using NewMoon.TF2Helpers.Entities; | |
| 4 | 2 | ||
| 5 | - | namespace NewMoon.Samples.CustomNpc; | |
| 3 | + | namespace NewMoon.Samples.NPC; | |
| 6 | 4 | ||
| 7 | 5 | /// <summary> | |
| 8 | - | /// The custom NPC: a <c>nm_sample_npc</c> classname that is a factory alias of <c>base_boss</c>. Spawning it yields a real | |
| 6 | + | /// The custom NPC: a <c>npc_bot_newmoon</c> classname that is a factory alias of <c>base_boss</c>. Spawning it yields a real | |
| 9 | 7 | /// <c>base_boss</c> that carries the new classname, and the registry calls the lifecycle overrides below. | |
| 10 | 8 | /// </summary> | |
| 11 | 9 | /// <remarks> | |
| 12 | 10 | /// The class only logs. Reading keyvalues, inputs and outputs from a custom entity is not part of the landed API yet, so the | |
| 13 | - | /// spawner supplies <c>health</c> and <c>model</c>, for example <c>ent_create nm_sample_npc health 500 model models/bots/headless_hatman.mdl</c>. The registry needs an accessible parameterless constructor. | |
| 11 | + | /// spawner supplies <c>health</c> and <c>model</c>, for example <c>ent_create npc_bot_newmoon health 500 model models/bots/headless_hatman.mdl</c>. The registry needs an accessible parameterless constructor. | |
| 14 | 12 | /// </remarks> | |
| 15 | - | [CustomEntity("nm_sample_npc", "base_boss", PrecacheModels = ["models/bots/headless_hatman.mdl"])] | |
| 16 | - | internal sealed class SampleNpc : FCustomEntity | |
| 13 | + | [CustomEntity("npc_bot_newmoon", "base_boss", PrecacheModels = ["models/bots/headless_hatman.mdl"])] | |
| 14 | + | internal sealed class NPCBotNewMoon : FCustomEntity | |
| 17 | 15 | { | |
| 18 | 16 | /// <summary> | |
| 19 | 17 | /// The logger the callbacks write to; set by <see cref="CustomNpcSample"/> at start-up, because the registry creates this | |
| @@ -28,12 +26,12 @@ internal sealed class SampleNpc : FCustomEntity | |||
| 28 | 26 | protected override void OnSpawn() | |
| 29 | 27 | { | |
| 30 | 28 | _index = Entity.Index; | |
| 31 | - | Log?.LogNpcSpawn(_index); | |
| 29 | + | Log?.LogNPCSpawn(_index); | |
| 32 | 30 | } | |
| 33 | 31 | ||
| 34 | 32 | /// <inheritdoc/> | |
| 35 | - | protected override void OnActivate() => Log?.LogNpcActivate(_index); | |
| 33 | + | protected override void OnActivate() => Log?.LogNPCActivate(_index); | |
| 36 | 34 | ||
| 37 | 35 | /// <inheritdoc/> | |
| 38 | - | protected override void OnRemoved() => Log?.LogNpcRemoved(_index); | |
| 36 | + | protected override void OnRemoved() => Log?.LogNPCRemoved(_index); | |
| 39 | 37 | } | |
Diff is too large to be shown
luno revised this gist 2 hours ago · 12fc2d7
3 files changed, 204 insertions, 63 deletions
| @@ -1,61 +0,0 @@ | |||
| 1 | - | # Custom NPC sample | |
| 2 | - | ||
| 3 | - | A light gamemode that registers one custom NPC classname, `nm_sample_npc`, using the TF2Helpers friendly layer. | |
| 4 | - | ||
| 5 | - | ## What it shows | |
| 6 | - | ||
| 7 | - | - `SampleNpc` derives `FCustomEntity` and is declared `[CustomEntity("nm_sample_npc", "base_boss")]`. The classname is a factory | |
| 8 | - | alias of `base_boss`: spawning it gives a real `base_boss` (same ServerClass and datamap) that carries the new classname. It is | |
| 9 | - | not a new engine class. | |
| 10 | - | - The source generator emits `NewMoon.Generated.CustomEntityTable.All` for the assembly, and `CustomNpcSample.OnPluginStart` | |
| 11 | - | passes it to `RegisterCustomEntities`. | |
| 12 | - | - The NPC precaches `models/bots/headless_hatman.mdl` at registration and logs `OnSpawn`, `OnActivate` and `OnRemoved`. | |
| 13 | - | - Unloading the plugin despawns the live NPCs and removes the alias. | |
| 14 | - | ||
| 15 | - | ## Where it goes | |
| 16 | - | ||
| 17 | - | Drop the folder at the monorepo root, beside `NewMoon.Gamemodes.Sample.Bare`, and add it to `LoadoutSharp.slnx`. The csproj uses | |
| 18 | - | relative paths (`..\ThirdParty\...`, `..\NewMoon.Plugins.TF2Helpers.Api\build\NewMoon.GameMode.props`) that assume that location. | |
| 19 | - | ||
| 20 | - | ## Build | |
| 21 | - | ||
| 22 | - | ```bash | |
| 23 | - | .claude/scripts/build-gate.sh dotnet build NewMoon.Gamemodes.Sample.CustomNpc/NewMoon.Gamemodes.Sample.CustomNpc.csproj | |
| 24 | - | ``` | |
| 25 | - | ||
| 26 | - | The first build reports NMFG001: every game mode build checks its `[CustomEntity]` classes against the checked-in FGD | |
| 27 | - | (`NewMoon.Plugins.TF2Helpers.Api/Fgd/newmoon_custom_entities.fgd`), which has no block for `nm_sample_npc` yet. Regenerate the | |
| 28 | - | FGD, then build again: | |
| 29 | - | ||
| 30 | - | ```bash | |
| 31 | - | .claude/scripts/build-gate.sh ./build.sh GenerateCustomEntityFgd | |
| 32 | - | ``` | |
| 33 | - | ||
| 34 | - | To try the sample without touching the FGD, build with `-p:NewMoonCustomEntityFgdCheck=false`. | |
| 35 | - | ||
| 36 | - | Run the same command again whenever you change the class's keys, inputs or outputs. | |
| 37 | - | ||
| 38 | - | ## Try it on a dev server | |
| 39 | - | ||
| 40 | - | Load the gamemode, then from a console: | |
| 41 | - | ||
| 42 | - | ``` | |
| 43 | - | ent_create nm_sample_npc health 500 model models/bots/headless_hatman.mdl | |
| 44 | - | ``` | |
| 45 | - | ||
| 46 | - | `ent_create` writes the keyvalues before spawning, which matters: a `base_boss` spawns with 0 health unless `health` is set | |
| 47 | - | before spawn, and it needs a `model`. Expect the log to show `nm_sample_npc OnSpawn <index>`, then `OnActivate <index>` on the | |
| 48 | - | next tick, and `OnRemoved <index>` when the NPC is killed, removed or the plugin unloads. The NPC stands still. | |
| 49 | - | ||
| 50 | - | From code, the equivalent is `Map.CreateEntity("nm_sample_npc").KeyValue("health", 500).Model("...").At(origin).Spawn()`. | |
| 51 | - | ||
| 52 | - | ## Limits | |
| 53 | - | ||
| 54 | - | - 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 | |
| 55 | - | inherits the stock boss side effects (currency drop, crushing buildings, shoving players). | |
| 56 | - | - The class cannot set keyvalues or its model before spawn: `OnSpawn` runs after the native spawn. The spawner supplies them. | |
| 57 | - | - Keyvalue routing into `[CustomKey]` properties, `[CustomInput]` calls and `[CustomOutput]` firing are planned work | |
| 58 | - | (AB#29804 to AB#29807) and are not used here. | |
| 59 | - | - The engine sees only the `base_boss` datamap, so `ent_text` and `ent_dump` do not show managed state. | |
| 60 | - | - 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 | |
| 61 | - | created on that map. `ent_create` works at any time. | |
| @@ -1,7 +1,6 @@ | |||
| 1 | 1 | using Microsoft.Extensions.Logging; | |
| 2 | 2 | using NewMoon.TF2Helpers; | |
| 3 | 3 | using NewMoon.TF2Helpers.Entities; | |
| 4 | - | using NewMoon.Utility; | |
| 5 | 4 | ||
| 6 | 5 | namespace NewMoon.Samples.CustomNpc; | |
| 7 | 6 | ||
| @@ -11,7 +10,7 @@ namespace NewMoon.Samples.CustomNpc; | |||
| 11 | 10 | /// </summary> | |
| 12 | 11 | /// <remarks> | |
| 13 | 12 | /// The class only logs. Reading keyvalues, inputs and outputs from a custom entity is not part of the landed API yet, so the | |
| 14 | - | /// spawner supplies <c>health</c> and <c>model</c> (see the README). The registry needs an accessible parameterless constructor. | |
| 13 | + | /// spawner supplies <c>health</c> and <c>model</c>, for example <c>ent_create nm_sample_npc health 500 model models/bots/headless_hatman.mdl</c>. The registry needs an accessible parameterless constructor. | |
| 15 | 14 | /// </remarks> | |
| 16 | 15 | [CustomEntity("nm_sample_npc", "base_boss", PrecacheModels = ["models/bots/headless_hatman.mdl"])] | |
| 17 | 16 | internal sealed class SampleNpc : FCustomEntity | |
Diff is too large to be shown
screwdriver
revised this gist 2 hours ago
·
4052f86
3 files changed, 5 deletions
| @@ -1,5 +1,4 @@ | |||
| 1 | 1 | using NewMoon.TF2Helpers; | |
| 2 | - | using NewMoon.Utility; | |
| 3 | 2 | ||
| 4 | 3 | namespace NewMoon.Samples.CustomNpc; | |
| 5 | 4 | ||
| @@ -11,7 +10,6 @@ namespace NewMoon.Samples.CustomNpc; | |||
| 11 | 10 | /// class. <see cref="FGamePlugin.RegisterCustomEntities"/> hands them to the registry, which installs the classname as a factory | |
| 12 | 11 | /// alias of <c>base_boss</c>. The plugin scope removes the alias, and despawns the live NPCs, when the plugin unloads. | |
| 13 | 12 | /// </remarks> | |
| 14 | - | [AgentGenerated("claude-sonnet-5-5", "2026-11-01")] | |
| 15 | 13 | [GameMode("Custom NPC Sample", "custom-npc-sample", Description = "Registers one custom NPC classname fronting base_boss.")] | |
| 16 | 14 | public sealed partial class CustomNpcSample : FGamePlugin | |
| 17 | 15 | { | |
| @@ -1,6 +1,5 @@ | |||
| 1 | 1 | using JetBrains.Annotations; | |
| 2 | 2 | using Microsoft.Extensions.Logging; | |
| 3 | - | using NewMoon.Utility; | |
| 4 | 3 | ||
| 5 | 4 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member | |
| 6 | 5 | ||
| @@ -9,7 +8,6 @@ namespace NewMoon.Samples.CustomNpc; | |||
| 9 | 8 | /// <summary> | |
| 10 | 9 | /// Source-generated log messages for <see cref="CustomNpcSample"/> and <see cref="SampleNpc"/>. | |
| 11 | 10 | /// </summary> | |
| 12 | - | [AgentGenerated("claude-sonnet-5-5", "2026-11-01")] | |
| 13 | 11 | [UsedImplicitly] | |
| 14 | 12 | public static partial class CustomNpcSampleLogger | |
| 15 | 13 | { | |
| @@ -13,7 +13,6 @@ namespace NewMoon.Samples.CustomNpc; | |||
| 13 | 13 | /// The class only logs. Reading keyvalues, inputs and outputs from a custom entity is not part of the landed API yet, so the | |
| 14 | 14 | /// spawner supplies <c>health</c> and <c>model</c> (see the README). The registry needs an accessible parameterless constructor. | |
| 15 | 15 | /// </remarks> | |
| 16 | - | [AgentGenerated("claude-sonnet-5-5", "2026-11-01")] | |
| 17 | 16 | [CustomEntity("nm_sample_npc", "base_boss", PrecacheModels = ["models/bots/headless_hatman.mdl"])] | |
| 18 | 17 | internal sealed class SampleNpc : FCustomEntity | |
| 19 | 18 | { | |
luno revised this gist 2 hours ago · 527c893
5 files changed, 204 insertions
| @@ -0,0 +1,28 @@ | |||
| 1 | + | using NewMoon.TF2Helpers; | |
| 2 | + | using NewMoon.Utility; | |
| 3 | + | ||
| 4 | + | namespace NewMoon.Samples.CustomNpc; | |
| 5 | + | ||
| 6 | + | /// <summary> | |
| 7 | + | /// A light sample gamemode that registers one custom NPC classname, <c>nm_sample_npc</c>, which fronts <c>base_boss</c>. | |
| 8 | + | /// </summary> | |
| 9 | + | /// <remarks> | |
| 10 | + | /// The generator emits <c>NewMoon.Generated.CustomEntityTable.All</c> for this assembly, one descriptor per <c>[CustomEntity]</c> | |
| 11 | + | /// class. <see cref="FGamePlugin.RegisterCustomEntities"/> hands them to the registry, which installs the classname as a factory | |
| 12 | + | /// alias of <c>base_boss</c>. The plugin scope removes the alias, and despawns the live NPCs, when the plugin unloads. | |
| 13 | + | /// </remarks> | |
| 14 | + | [AgentGenerated("claude-sonnet-5-5", "2026-11-01")] | |
| 15 | + | [GameMode("Custom NPC Sample", "custom-npc-sample", Description = "Registers one custom NPC classname fronting base_boss.")] | |
| 16 | + | public sealed partial class CustomNpcSample : FGamePlugin | |
| 17 | + | { | |
| 18 | + | /// <inheritdoc/> | |
| 19 | + | protected override void OnPluginStart() | |
| 20 | + | { | |
| 21 | + | // The logger is set first: adoption inside the registration can raise OnSpawn for NPCs that already exist. | |
| 22 | + | SampleNpc.Log = PLogger; | |
| 23 | + | PLogger.LogRegistered(RegisterCustomEntities(NewMoon.Generated.CustomEntityTable.All)); | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | /// <inheritdoc/> | |
| 27 | + | protected override void OnPluginEnd() => PLogger.LogEnded(); | |
| 28 | + | } | |
| @@ -0,0 +1,30 @@ | |||
| 1 | + | using JetBrains.Annotations; | |
| 2 | + | using Microsoft.Extensions.Logging; | |
| 3 | + | using NewMoon.Utility; | |
| 4 | + | ||
| 5 | + | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member | |
| 6 | + | ||
| 7 | + | namespace NewMoon.Samples.CustomNpc; | |
| 8 | + | ||
| 9 | + | /// <summary> | |
| 10 | + | /// Source-generated log messages for <see cref="CustomNpcSample"/> and <see cref="SampleNpc"/>. | |
| 11 | + | /// </summary> | |
| 12 | + | [AgentGenerated("claude-sonnet-5-5", "2026-11-01")] | |
| 13 | + | [UsedImplicitly] | |
| 14 | + | public static partial class CustomNpcSampleLogger | |
| 15 | + | { | |
| 16 | + | [LoggerMessage(1, LogLevel.Information, "Custom NPC sample registered {Count} custom entity classes.")] | |
| 17 | + | public static partial void LogRegistered(this ILogger logger, int count); | |
| 18 | + | ||
| 19 | + | [LoggerMessage(2, LogLevel.Information, "Custom NPC sample ended: OnPluginEnd ran before the scope released.")] | |
| 20 | + | public static partial void LogEnded(this ILogger logger); | |
| 21 | + | ||
| 22 | + | [LoggerMessage(3, LogLevel.Information, "nm_sample_npc OnSpawn {Index}")] | |
| 23 | + | public static partial void LogNpcSpawn(this ILogger logger, int index); | |
| 24 | + | ||
| 25 | + | [LoggerMessage(4, LogLevel.Information, "nm_sample_npc OnActivate {Index}")] | |
| 26 | + | public static partial void LogNpcActivate(this ILogger logger, int index); | |
| 27 | + | ||
| 28 | + | [LoggerMessage(5, LogLevel.Information, "nm_sample_npc OnRemoved {Index}")] | |
| 29 | + | public static partial void LogNpcRemoved(this ILogger logger, int index); | |
| 30 | + | } | |
| @@ -0,0 +1,44 @@ | |||
| 1 | + | <Project Sdk="Microsoft.NET.Sdk"> | |
| 2 | + | <PropertyGroup> | |
| 3 | + | <RootNamespace>NewMoon.Samples.CustomNpc</RootNamespace> | |
| 4 | + | <Version>1.0.0</Version> | |
| 5 | + | <Platforms>AnyCPU</Platforms> | |
| 6 | + | </PropertyGroup> | |
| 7 | + | ||
| 8 | + | <PropertyGroup> | |
| 9 | + | <Title>Custom NPC Sample Gamemode</Title> | |
| 10 | + | <Authors>Jon S. (Serafeline)</Authors> | |
| 11 | + | <PackageId>NewMoon.Gamemodes.Sample.CustomNpc</PackageId> | |
| 12 | + | <PackageVersion>$(Version)</PackageVersion> | |
| 13 | + | <IsPackable>false</IsPackable> | |
| 14 | + | <PackageDescription>A light sample gamemode that registers one custom NPC classname (nm_sample_npc) fronting base_boss through the TF2Helpers custom entity API.</PackageDescription> | |
| 15 | + | </PropertyGroup> | |
| 16 | + | ||
| 17 | + | <ItemGroup> | |
| 18 | + | <PackageReference Include="JetBrains.Annotations" /> | |
| 19 | + | <PackageReference Include="Microsoft.Extensions.Configuration.Json" /> | |
| 20 | + | </ItemGroup> | |
| 21 | + | ||
| 22 | + | <ItemGroup> | |
| 23 | + | <ProjectReference Include="..\ThirdParty\NewMoon.SourceNet\managed\SourceNet.Core\SourceNet.Core.csproj"> | |
| 24 | + | <Private>false</Private> | |
| 25 | + | </ProjectReference> | |
| 26 | + | <ProjectReference Include="..\ThirdParty\NewMoon.SourceNet\managed\SourceNet.Engine.Source\SourceNet.Engine.Source.csproj"> | |
| 27 | + | <Private>false</Private> | |
| 28 | + | </ProjectReference> | |
| 29 | + | <ProjectReference Include="..\ThirdParty\NewMoon.SourceNet\managed\SourceNet.Core.SourceGeneration\SourceNet.Core.SourceGeneration.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> | |
| 30 | + | <ProjectReference Include="..\ThirdParty\NewMoon.SourceNet\managed\SourceNet.Common\SourceNet.Common.csproj"> | |
| 31 | + | <Private>false</Private> | |
| 32 | + | </ProjectReference> | |
| 33 | + | <ProjectReference Include="..\NewMoon.Shared.Utility\NewMoon.Shared.Utility.csproj" /> | |
| 34 | + | </ItemGroup> | |
| 35 | + | ||
| 36 | + | <!-- Task 20's shared game mode props: the [GameMode] startup generator and the NMGM analyzers as one analyzer reference | |
| 37 | + | (analyzer references do not flow through project references), plus TeamFortress2.Api, SDKWrappers.Api, and | |
| 38 | + | TF2Helpers.Api as Private=false references (the host supplies them through PluginUseAPIAssembly). The sample uses no | |
| 39 | + | LoadoutSharp types, so that fourth API reference is left out. --> | |
| 40 | + | <PropertyGroup> | |
| 41 | + | <GameModeUsesLoadoutSharpApi>false</GameModeUsesLoadoutSharpApi> | |
| 42 | + | </PropertyGroup> | |
| 43 | + | <Import Project="..\NewMoon.Plugins.TF2Helpers.Api\build\NewMoon.GameMode.props" /> | |
| 44 | + | </Project> | |
| @@ -0,0 +1,61 @@ | |||
| 1 | + | # Custom NPC sample | |
| 2 | + | ||
| 3 | + | A light gamemode that registers one custom NPC classname, `nm_sample_npc`, using the TF2Helpers friendly layer. | |
| 4 | + | ||
| 5 | + | ## What it shows | |
| 6 | + | ||
| 7 | + | - `SampleNpc` derives `FCustomEntity` and is declared `[CustomEntity("nm_sample_npc", "base_boss")]`. The classname is a factory | |
| 8 | + | alias of `base_boss`: spawning it gives a real `base_boss` (same ServerClass and datamap) that carries the new classname. It is | |
| 9 | + | not a new engine class. | |
| 10 | + | - The source generator emits `NewMoon.Generated.CustomEntityTable.All` for the assembly, and `CustomNpcSample.OnPluginStart` | |
| 11 | + | passes it to `RegisterCustomEntities`. | |
| 12 | + | - The NPC precaches `models/bots/headless_hatman.mdl` at registration and logs `OnSpawn`, `OnActivate` and `OnRemoved`. | |
| 13 | + | - Unloading the plugin despawns the live NPCs and removes the alias. | |
| 14 | + | ||
| 15 | + | ## Where it goes | |
| 16 | + | ||
| 17 | + | Drop the folder at the monorepo root, beside `NewMoon.Gamemodes.Sample.Bare`, and add it to `LoadoutSharp.slnx`. The csproj uses | |
| 18 | + | relative paths (`..\ThirdParty\...`, `..\NewMoon.Plugins.TF2Helpers.Api\build\NewMoon.GameMode.props`) that assume that location. | |
| 19 | + | ||
| 20 | + | ## Build | |
| 21 | + | ||
| 22 | + | ```bash | |
| 23 | + | .claude/scripts/build-gate.sh dotnet build NewMoon.Gamemodes.Sample.CustomNpc/NewMoon.Gamemodes.Sample.CustomNpc.csproj | |
| 24 | + | ``` | |
| 25 | + | ||
| 26 | + | The first build reports NMFG001: every game mode build checks its `[CustomEntity]` classes against the checked-in FGD | |
| 27 | + | (`NewMoon.Plugins.TF2Helpers.Api/Fgd/newmoon_custom_entities.fgd`), which has no block for `nm_sample_npc` yet. Regenerate the | |
| 28 | + | FGD, then build again: | |
| 29 | + | ||
| 30 | + | ```bash | |
| 31 | + | .claude/scripts/build-gate.sh ./build.sh GenerateCustomEntityFgd | |
| 32 | + | ``` | |
| 33 | + | ||
| 34 | + | To try the sample without touching the FGD, build with `-p:NewMoonCustomEntityFgdCheck=false`. | |
| 35 | + | ||
| 36 | + | Run the same command again whenever you change the class's keys, inputs or outputs. | |
| 37 | + | ||
| 38 | + | ## Try it on a dev server | |
| 39 | + | ||
| 40 | + | Load the gamemode, then from a console: | |
| 41 | + | ||
| 42 | + | ``` | |
| 43 | + | ent_create nm_sample_npc health 500 model models/bots/headless_hatman.mdl | |
| 44 | + | ``` | |
| 45 | + | ||
| 46 | + | `ent_create` writes the keyvalues before spawning, which matters: a `base_boss` spawns with 0 health unless `health` is set | |
| 47 | + | before spawn, and it needs a `model`. Expect the log to show `nm_sample_npc OnSpawn <index>`, then `OnActivate <index>` on the | |
| 48 | + | next tick, and `OnRemoved <index>` when the NPC is killed, removed or the plugin unloads. The NPC stands still. | |
| 49 | + | ||
| 50 | + | From code, the equivalent is `Map.CreateEntity("nm_sample_npc").KeyValue("health", 500).Model("...").At(origin).Spawn()`. | |
| 51 | + | ||
| 52 | + | ## Limits | |
| 53 | + | ||
| 54 | + | - 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 | |
| 55 | + | inherits the stock boss side effects (currency drop, crushing buildings, shoving players). | |
| 56 | + | - The class cannot set keyvalues or its model before spawn: `OnSpawn` runs after the native spawn. The spawner supplies them. | |
| 57 | + | - Keyvalue routing into `[CustomKey]` properties, `[CustomInput]` calls and `[CustomOutput]` firing are planned work | |
| 58 | + | (AB#29804 to AB#29807) and are not used here. | |
| 59 | + | - The engine sees only the `base_boss` datamap, so `ent_text` and `ent_dump` do not show managed state. | |
| 60 | + | - 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 | |
| 61 | + | created on that map. `ent_create` works at any time. | |
| @@ -0,0 +1,41 @@ | |||
| 1 | + | using Microsoft.Extensions.Logging; | |
| 2 | + | using NewMoon.TF2Helpers; | |
| 3 | + | using NewMoon.TF2Helpers.Entities; | |
| 4 | + | using NewMoon.Utility; | |
| 5 | + | ||
| 6 | + | namespace NewMoon.Samples.CustomNpc; | |
| 7 | + | ||
| 8 | + | /// <summary> | |
| 9 | + | /// The custom NPC: a <c>nm_sample_npc</c> classname that is a factory alias of <c>base_boss</c>. Spawning it yields a real | |
| 10 | + | /// <c>base_boss</c> that carries the new classname, and the registry calls the lifecycle overrides below. | |
| 11 | + | /// </summary> | |
| 12 | + | /// <remarks> | |
| 13 | + | /// The class only logs. Reading keyvalues, inputs and outputs from a custom entity is not part of the landed API yet, so the | |
| 14 | + | /// spawner supplies <c>health</c> and <c>model</c> (see the README). The registry needs an accessible parameterless constructor. | |
| 15 | + | /// </remarks> | |
| 16 | + | [AgentGenerated("claude-sonnet-5-5", "2026-11-01")] | |
| 17 | + | [CustomEntity("nm_sample_npc", "base_boss", PrecacheModels = ["models/bots/headless_hatman.mdl"])] | |
| 18 | + | internal sealed class SampleNpc : FCustomEntity | |
| 19 | + | { | |
| 20 | + | /// <summary> | |
| 21 | + | /// The logger the callbacks write to; set by <see cref="CustomNpcSample"/> at start-up, because the registry creates this | |
| 22 | + | /// object through its parameterless constructor. | |
| 23 | + | /// </summary> | |
| 24 | + | internal static ILogger? Log { get; set; } | |
| 25 | + | ||
| 26 | + | // Captured at spawn so the remove log never depends on the view still resolving while the instance is torn down. | |
| 27 | + | private int _index = -1; | |
| 28 | + | ||
| 29 | + | /// <inheritdoc/> | |
| 30 | + | protected override void OnSpawn() | |
| 31 | + | { | |
| 32 | + | _index = Entity.Index; | |
| 33 | + | Log?.LogNpcSpawn(_index); | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | /// <inheritdoc/> | |
| 37 | + | protected override void OnActivate() => Log?.LogNpcActivate(_index); | |
| 38 | + | ||
| 39 | + | /// <inheritdoc/> | |
| 40 | + | protected override void OnRemoved() => Log?.LogNpcRemoved(_index); | |
| 41 | + | } | |