Like 0

Revision 12fc2d76232e7ce0dd506f5104eaf0921eb0a1ad

CustomNpcSample.cs Raw
1using NewMoon.TF2Helpers;
2
3namespace 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.")]
14public 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}
27
CustomNpcSample.logger.cs Raw
1using JetBrains.Annotations;
2using Microsoft.Extensions.Logging;
3
4#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
5
6namespace NewMoon.Samples.CustomNpc;
7
8/// <summary>
9/// Source-generated log messages for <see cref="CustomNpcSample"/> and <see cref="SampleNpc"/>.
10/// </summary>
11[UsedImplicitly]
12public 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}
29
NewMoon.Gamemodes.Sample.CustomNpc.csproj Raw
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>
45
SampleNpc.cs Raw
1using Microsoft.Extensions.Logging;
2using NewMoon.TF2Helpers;
3using NewMoon.TF2Helpers.Entities;
4
5namespace NewMoon.Samples.CustomNpc;
6
7/// <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
9/// <c>base_boss</c> that carries the new classname, and the registry calls the lifecycle overrides below.
10/// </summary>
11/// <remarks>
12/// 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.
14/// </remarks>
15[CustomEntity("nm_sample_npc", "base_boss", PrecacheModels = ["models/bots/headless_hatman.mdl"])]
16internal sealed class SampleNpc : FCustomEntity
17{
18 /// <summary>
19 /// The logger the callbacks write to; set by <see cref="CustomNpcSample"/> at start-up, because the registry creates this
20 /// object through its parameterless constructor.
21 /// </summary>
22 internal static ILogger? Log { get; set; }
23
24 // Captured at spawn so the remove log never depends on the view still resolving while the instance is torn down.
25 private int _index = -1;
26
27 /// <inheritdoc/>
28 protected override void OnSpawn()
29 {
30 _index = Entity.Index;
31 Log?.LogNpcSpawn(_index);
32 }
33
34 /// <inheritdoc/>
35 protected override void OnActivate() => Log?.LogNpcActivate(_index);
36
37 /// <inheritdoc/>
38 protected override void OnRemoved() => Log?.LogNpcRemoved(_index);
39}
40
nm_sample_npc.fgd Raw
1// nm_sample_npc.fgd - the FGD GenerateCustomEntityFgd produces for this sample on its own.
2// In the monorepo, do not hand-edit: add the class, then run
3// .claude/scripts/build-gate.sh ./build.sh GenerateCustomEntityFgd
4// and the block lands in NewMoon.Plugins.TF2Helpers.Api/Fgd/newmoon_custom_entities.fgd.
5// Load it in Hammer under Tools > Options > Game Configurations > Game Data files, after TF2's own FGDs.
6
7// Common base classes for the NewMoon custom entity FGD (AB#29812).
8// Hand-maintained. Derived from the CBaseEntity data description (BEGIN_DATADESC_NO_BASE( CBaseEntity ) in
9// src/game/server/baseentity.cpp) and the key handling in CBaseEntity::KeyValue (src/game/shared/baseentity_shared.cpp)
10// of the Source SDK 2013. Refresh it from a GetDataDescMap dump of a live entity when the engine surface changes.
11// The nm_ prefix keeps these names from colliding with the host FGD names; this file is self-contained and inherits nothing.
12
13@BaseClass = nm_Targetname
14[
15 targetname(target_source) : "Name" : : "The name other entities use to refer to this entity."
16
17 input Kill(void) : "Remove this entity from the world."
18 input KillHierarchy(void) : "Remove this entity and all of its children from the world."
19 input AddOutput(string) : "Add an output connection at run time. Format: <output> <target>:<input>:<parameter>:<delay>:<max fires, -1 for no limit>."
20 input FireUser1(string) : "Fire this entity's OnUser1 output."
21 input FireUser2(string) : "Fire this entity's OnUser2 output."
22 input FireUser3(string) : "Fire this entity's OnUser3 output."
23 input FireUser4(string) : "Fire this entity's OnUser4 output."
24
25 output OnUser1(void) : "Fired by the FireUser1 input."
26 output OnUser2(void) : "Fired by the FireUser2 input."
27 output OnUser3(void) : "Fired by the FireUser3 input."
28 output OnUser4(void) : "Fired by the FireUser4 input."
29]
30
31@BaseClass = nm_Origin
32[
33 origin(origin) : "Origin (X Y Z)" : : "Where the entity is placed in the world."
34]
35
36@BaseClass = nm_Angles
37[
38 angles(angle) : "Pitch Yaw Roll (Y Z X)" : "0 0 0" : "The orientation of the entity in the world."
39]
40
41@BaseClass = nm_Parentname
42[
43 parentname(target_destination) : "Parent" : : "The name of the entity this one is parented to. A child moves with its parent."
44
45 input SetParent(string) : "Parent this entity to the named entity."
46 input SetParentAttachment(string) : "Attach this entity to the named attachment point of its parent. The entity must already be parented."
47 input SetParentAttachmentMaintainOffset(string) : "Attach this entity to the named attachment point of its parent and keep its current offset."
48 input ClearParent(void) : "Detach this entity from its parent."
49]
50
51@BaseClass = nm_RenderFields
52[
53 rendermode(choices) : "Render Mode" : 0 : "Overrides how the entity is drawn. Used together with FX Amount and FX Color." =
54 [
55 0 : "Normal"
56 1 : "Color"
57 2 : "Texture"
58 3 : "Glow"
59 4 : "Solid"
60 5 : "Additive"
61 7 : "Additive Fractional Frame"
62 9 : "World Space Glow"
63 10 : "Do Not Render"
64 ]
65 renderamt(integer) : "FX Amount (0 - 255)" : 255 : "How opaque the entity is, as read by the Render Mode."
66 rendercolor(color255) : "FX Color (R G B)" : "255 255 255" : "The tint applied to the entity."
67 renderfx(choices) : "Render FX" : 0 : "A render effect applied to the entity." =
68 [
69 0 : "Normal"
70 1 : "Slow Pulse"
71 2 : "Fast Pulse"
72 3 : "Slow Wide Pulse"
73 4 : "Fast Wide Pulse"
74 5 : "Slow Fade Away"
75 6 : "Fast Fade Away"
76 7 : "Slow Become Solid"
77 8 : "Fast Become Solid"
78 9 : "Slow Strobe"
79 10 : "Fast Strobe"
80 11 : "Faster Strobe"
81 12 : "Slow Flicker"
82 13 : "Fast Flicker"
83 14 : "Constant Glow"
84 15 : "Distort"
85 16 : "Hologram (Distort + Fade)"
86 17 : "Explode"
87 18 : "Glow Shell"
88 19 : "Clamp Minimum Scale"
89 24 : "Fast Wider Pulse"
90 ]
91 disableshadows(choices) : "Disable Shadows" : 0 : "Turns off the dynamic shadow of this entity." =
92 [
93 0 : "No"
94 1 : "Yes"
95 ]
96 disablereceiveshadows(choices) : "Disable Receiving Shadows" : 0 : "Stops shadows from falling on this entity." =
97 [
98 0 : "No"
99 1 : "Yes"
100 ]
101
102 input Alpha(integer) : "Set the opacity of the entity, 0 to 255."
103 input Color(color255) : "Set the tint of the entity. Format: <red> <green> <blue>."
104 input AlternativeSorting(bool) : "Try to fix draw-order problems. True turns the alternative sorting on and false turns it off."
105 input EnableShadow(void) : "Turn the shadow of this entity on."
106 input DisableShadow(void) : "Turn the shadow of this entity off."
107]
108
109@BaseClass base(nm_Targetname, nm_Origin, nm_Angles, nm_Parentname, nm_RenderFields) = nm_entity_base
110[
111 teamnumber(integer) : "Team Number" : 0 : "The team this entity starts on. 0 is unassigned, 2 is RED and 3 is BLU."
112 vscripts(string) : "Entity Scripts" : : "Space-delimited list of VScript files run on this entity at spawn."
113 thinkfunction(string) : "Script think function" : : "The VScript function called on every think of this entity."
114
115 input Use(void) : "Act as if a player pressed the use key on this entity."
116 input SetTeam(integer) : "Move this entity to the given team number."
117 input SetDamageFilter(string) : "Set the filter entity that decides which damage this entity takes. An empty parameter clears it."
118 input EnableDamageForces(void) : "Let damage push this entity around."
119 input DisableDamageForces(void) : "Stop damage from pushing this entity around."
120 input DispatchEffect(string) : "Dispatch the named particle effect from this entity."
121 input DispatchResponse(string) : "Run the named response concept on this entity."
122 input AddContext(string) : "Add a response context to this entity. Format: <name>:<value>."
123 input RemoveContext(string) : "Remove the named response context from this entity."
124 input ClearContext(void) : "Remove every response context from this entity."
125 input RunScriptFile(string) : "Run a VScript file in this entity's script scope."
126 input RunScriptCode(string) : "Run a line of VScript in this entity's script scope."
127 input CallScriptFunction(string) : "Call the named VScript function in this entity's script scope."
128 input TerminateScriptScope(void) : "Destroy this entity's VScript scope."
129]
130
131// nm-fgd-appearance: studio()
132// Curated base class for custom entities that front base_boss (AB#29812).
133// Hand-maintained. Derived from the data description of the Source SDK 2013 class CTFBaseBoss
134// (src/game/server/tf/player_vs_environment/tf_base_boss.cpp) plus the inherited CBaseCombatCharacter
135// (src/game/server/basecombatcharacter.cpp), CBaseAnimating (src/game/server/baseanimating.cpp) and CBaseEntity
136// (src/game/server/baseentity.cpp) rows. One row per key name, the most-derived class wins.
137// Refresh it from a GetDataDescMap dump of a live base_boss when the engine surface changes.
138
139@BaseClass base(nm_entity_base) = nm_base_boss_base
140[
141 health(integer) : "Health" : 0 : "Starting and maximum health of the boss."
142 model(studio) : "World Model" : : "The model the boss uses."
143 speed(float) : "Speed" : 75 : "Movement speed of the boss, in units per second."
144 start_disabled(choices) : "Start Disabled" : 0 : "Spawn the boss disabled until it gets the Enable input." =
145 [
146 0 : "No"
147 1 : "Yes"
148 ]
149 skin(integer) : "Skin" : 0 : "Which skin of the model to use."
150 body(integer) : "Body Group" : 0 : "Which body group value of the model to show."
151 hitboxset(integer) : "Hitbox Set" : 0 : "Which hitbox set of the model to use."
152 modelscale(float) : "Model Scale" : "1.0" : "A multiplier for the size of the model."
153 playbackrate(float) : "Playback Rate" : "1.0" : "How fast animations play. 1 is normal speed."
154 fademindist(float) : "Start Fade Distance" : -1 : "Distance at which the boss starts to fade. Below 0 uses the end distance."
155 fademaxdist(float) : "End Fade Distance" : 0 : "Distance at which the boss is fully faded. 0 turns fading off."
156 fadescale(float) : "Fade Scale" : 1 : "Scales the forced engine fade. 0 turns the forced fade off."
157 LightingOrigin(target_destination) : "Lighting Origin" : : "Name of an entity to sample lighting from instead of the boss origin."
158 Relationship(string) : "Relationship" : : "Relationship rules for the boss. Format: <class or name> <D_HT|D_LI|D_FR|D_NU> <priority>."
159 physdamagescale(float) : "Physics Impact Damage Scale" : "1.0" : "Scales damage taken from physics impacts. 0 turns the feature off."
160
161 input Enable(void) : "Enable the boss."
162 input Disable(void) : "Disable the boss."
163 input SetSpeed(float) : "Set the movement speed of the boss."
164 input SetStepHeight(float) : "Set the highest step the boss walks up."
165 input SetMaxJumpHeight(float) : "Set the highest jump the boss makes."
166 input SetHealth(integer) : "Set the health of the boss."
167 input SetMaxHealth(integer) : "Set the maximum health of the boss."
168 input AddHealth(integer) : "Add to the health of the boss."
169 input RemoveHealth(integer) : "Remove health from the boss."
170 input Skin(integer) : "Change the model skin to this number."
171 input SetBodyGroup(integer) : "Show the body group with this index."
172 input SetModel(string) : "Change the model of the boss to the named one."
173 input SetModelScale(string) : "Change the model scale. Format: <scale> <seconds the change takes>."
174 input SetCycle(float) : "Set the position in the current animation, from 0 to 1."
175 input SetPlaybackRate(float) : "Set the animation playback rate."
176 input physdamagescale(float) : "Set the physics impact damage scale."
177 input fademindist(float) : "Set the start fade distance."
178 input fademaxdist(float) : "Set the end fade distance."
179 input Ignite(void) : "Set the boss on fire."
180 input IgniteLifetime(float) : "Set the boss on fire for this many seconds."
181 input IgniteNumHitboxFires(integer) : "Set the boss on fire with this many hitbox fires."
182 input IgniteHitboxFireScale(float) : "Set the boss on fire with this hitbox fire scale."
183 input BecomeRagdoll(void) : "Turn the boss into a ragdoll."
184 input SetLightingOrigin(string) : "Sample lighting from the named entity."
185 input SetLightingOriginHack(string) : "Sample lighting from the named entity, relative to the boss."
186 input KilledNPC(void) : "Tell the boss that an NPC was killed."
187
188 output OnHealthBelow90Percent(void) : "Fired when the health drops below 90 percent."
189 output OnHealthBelow80Percent(void) : "Fired when the health drops below 80 percent."
190 output OnHealthBelow70Percent(void) : "Fired when the health drops below 70 percent."
191 output OnHealthBelow60Percent(void) : "Fired when the health drops below 60 percent."
192 output OnHealthBelow50Percent(void) : "Fired when the health drops below 50 percent."
193 output OnHealthBelow40Percent(void) : "Fired when the health drops below 40 percent."
194 output OnHealthBelow30Percent(void) : "Fired when the health drops below 30 percent."
195 output OnHealthBelow20Percent(void) : "Fired when the health drops below 20 percent."
196 output OnHealthBelow10Percent(void) : "Fired when the health drops below 10 percent."
197 output OnKilled(void) : "Fired when the boss is killed."
198 output OnIgnite(void) : "Fired when the boss catches fire."
199]
200
201@PointClass base(nm_base_boss_base) studio() = nm_sample_npc : "Custom entity (fronts base_boss), declared by NewMoon.Samples.CustomNpc.SampleNpc"
202[
203]
204