Entity
SwiftlyS2's entity system creates entities, queries existing ones, tracks them safely with handles, and hooks entity inputs/outputs.
Accessing Entity System Service
Available through Core.EntitySystem.
public override void Load(bool hotReload)
{
var entitySystem = Core.EntitySystem;
}Creating Entities
By schema class via APIIEntitySystemService.CreateEntity, or by designer name via APIIEntitySystemService.CreateEntityByDesignerName - each also has an overload to force a specific entity index:
CPointWorldText worldText = Core.EntitySystem.CreateEntity<CPointWorldText>();
CBaseEntity relay = Core.EntitySystem.CreateEntityByDesignerName<CBaseEntity>("logic_relay");Every entity-system method here throws InvalidOperationException if called
too early, before the entity system is available.
Spawning Entities
Creating an entity doesn't spawn it - call DispatchSpawn/DispatchSpawnAsync (the latter for non-main-thread contexts) after creating, optionally with a CEntityKeyValues:
CPointWorldText worldText = Core.EntitySystem.CreateEntity<CPointWorldText>();
worldText.DispatchSpawn();
CBaseEntity relay = Core.EntitySystem.CreateEntityByDesignerName<CBaseEntity>("logic_relay");
using var keyValues = new CEntityKeyValues();
keyValues.SetString("targetname", "sw_relay_01");
keyValues.SetBool("StartDisabled", false);
relay.DispatchSpawn(keyValues);
await relay.DispatchSpawnAsync(keyValues); // non-main-threadQuerying Existing Entities
Every query method returns IEnumerable<T> - filter as early as possible. See APIIEntitySystemService.GetAllEntities, APIIEntitySystemService.GetAllEntitiesByClass, APIIEntitySystemService.GetAllEntitiesByDesignerName.
IEnumerable<CEntityInstance> allEntities = Core.EntitySystem.GetAllEntities();
IEnumerable<CPointWorldText> worldTexts = Core.EntitySystem.GetAllEntitiesByClass<CPointWorldText>();
IEnumerable<CBaseEntity> relays = Core.EntitySystem.GetAllEntitiesByDesignerName<CBaseEntity>("logic_relay");Get by Index or Address
APIIEntitySystemService.GetEntityByIndex takes a uint
index; APIIEntitySystemService.GetEntityByAddress takes
a pointer:
CEntityInstance? byIndex = Core.EntitySystem.GetEntityByIndex(100u);
CBaseEntity? typedByIndex = Core.EntitySystem.GetEntityByIndex<CBaseEntity>(100u);
CEntityInstance? byAddress = Core.EntitySystem.GetEntityByAddress((nint)0x12345678);
CBaseEntity? typedByAddress = Core.EntitySystem.GetEntityByAddress<CBaseEntity>((nint)0x12345678);The generic GetEntityByIndex<T>/GetEntityByAddress<T> overloads throw InvalidOperationException if the resolved entity isn't type T. Use the non-generic overload to check the type yourself.
Getting Game Rules
APIIEntitySystemService.GetGameRules:
CCSGameRules? gameRules = Core.EntitySystem.GetGameRules();Entity Handles and Safety
Check IsValid before using a direct entity reference - it's only a point-in-time check, so for tracking across frames/ticks store a APICHandle`1 (via APIIEntitySystemService.GetRefEHandle) instead, and check handle.IsValid before reading handle.Value:
List<CHandle<CBaseEntity>> trackedEntities = new();
CBaseEntity entity = Core.EntitySystem.CreateEntity<CBaseEntity>();
trackedEntities.Add(Core.EntitySystem.GetRefEHandle(entity));
entity.Despawn(); // also has a DespawnAsync() counterpart
Core.Scheduler.DelayBySeconds(10, () =>
{
foreach (var tracked in trackedEntities)
{
if (tracked.IsValid)
{
CBaseEntity current = tracked.Value!;
Console.WriteLine("Tracked entity is still valid.");
}
}
});Hooking Entity Outputs and Inputs
Register by designer name via APIIEntitySystemService.HookEntityOutput, or with APIEntityOutputHandlerAttribute (also available as APIEntityOutputHandlerAttribute`1 - [EntityOutputHandler<T>("OutputName")] resolves the designer name from the schema class). Inputs work identically via APIIEntitySystemService.HookEntityInput and APIEntityInputHandlerAttribute/APIEntityInputHandlerAttribute`1, just swap Output→Input, OutputName→InputName, and the event type.
private Guid _outputHookGuid;
public override void Load(bool hotReload)
{
_outputHookGuid = Core.EntitySystem.HookEntityOutput("func_door", "OnOpen", OnDoorOpened);
}
public override void Unload()
{
Core.EntitySystem.UnhookEntityOutput(_outputHookGuid);
}
private void OnDoorOpened(IOnEntityFireOutputHookEvent @event)
{
Console.WriteLine($"Output '{@event.OutputName}' fired by '{@event.DesignerName}'");
}Input hooking mirrors this exactly, callback taking an APIIOnEntityIdentityAcceptInputHookEvent with @event.InputName (output callbacks take APIIOnEntityFireOutputHookEvent with @event.OutputName). Set @event.Result = HookResult.Stop in either callback to block the flow. Unhook with APIIEntitySystemService.UnhookEntityOutput/APIIEntitySystemService.UnhookEntityInput.