swiftlys2/docs/api/ai/

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-thread

Querying 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.