Entity
SwiftlyS2 provides a full entity system for creating entities, querying existing entities, safely tracking entities with handles, and hooking entity inputs/outputs.
Accessing Entity System Service
The entity system service is available through Core.EntitySystem.
public override void Load(bool hotReload)
{
var entitySystem = Core.EntitySystem;
}Creating Entities
You can create entities by schema class or by designer name.
Create by Class
CPointWorldText worldText = Core.EntitySystem.CreateEntity<CPointWorldText>();Signature:
T CreateEntity<T>() where T : class, ISchemaClass<T>Create by Designer Name
CBaseEntity relay = Core.EntitySystem.CreateEntityByDesignerName<CBaseEntity>("logic_relay");Signature:
T CreateEntityByDesignerName<T>(string designerName) where T : class, ISchemaClass<T>Spawning Entities
Creating an entity does not automatically spawn it into the game world.
After creating, call DispatchSpawn (or DispatchSpawnAsync) on the entity instance.
Basic Spawn
CPointWorldText worldText = Core.EntitySystem.CreateEntity<CPointWorldText>();
worldText.DispatchSpawn();Spawn with Key Values
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);Async Spawn
await relay.DispatchSpawnAsync(keyValues);Use the async variant when spawning from non-main-thread contexts.
Querying Existing Entities
Most query methods return IEnumerable<T>, so filter as early as possible.
Get All Entities
IEnumerable<CEntityInstance> allEntities = Core.EntitySystem.GetAllEntities();Get by Class
IEnumerable<CPointWorldText> worldTexts = Core.EntitySystem.GetAllEntitiesByClass<CPointWorldText>();Get by Designer Name
IEnumerable<CBaseEntity> relays = Core.EntitySystem.GetAllEntitiesByDesignerName<CBaseEntity>("logic_relay");Get by Index or Address
CEntityInstance? byIndex = Core.EntitySystem.GetEntityByIndex(100);
CBaseEntity? typedByIndex = Core.EntitySystem.GetEntityByIndex<CBaseEntity>(100);
CEntityInstance? byAddress = Core.EntitySystem.GetEntityByAddress((nint)0x12345678);
CBaseEntity? typedByAddress = Core.EntitySystem.GetEntityByAddress<CBaseEntity>((nint)0x12345678);Getting Game Rules
CCSGameRules? gameRules = Core.EntitySystem.GetGameRules();Entity Handles and Safety
For long-term tracking, store CHandle<T> instead of storing direct entity references.
Check Entity Validity with IsValid
When you are working with a direct entity reference, check IsValid before using it.
CBaseEntity? entity = Core.EntitySystem.GetEntityByIndex<CBaseEntity>(100);
if (entity == null || !entity.IsValid)
{
return;
}
Console.WriteLine("Entity reference is valid.");IsValid is a point-in-time check. For long-lived tracking, prefer storing CHandle<T> and checking handle.IsValid before accessing handle.Value.
Convert Entity to Handle
CHandle<CBaseEntity> handle = Core.EntitySystem.GetRefEHandle(entity);Resolve Handle Back to Entity
if (handle.IsValid)
{
CBaseEntity resolved = handle.Value;
}Safe Tracking Pattern
List<CHandle<CBaseEntity>> trackedEntities = new();
CBaseEntity entity = Core.EntitySystem.CreateEntity<CBaseEntity>();
trackedEntities.Add(Core.EntitySystem.GetRefEHandle(entity));
entity.Despawn();
Core.Scheduler.DelayBySeconds(10f, () =>
{
foreach (var tracked in trackedEntities)
{
if (!tracked.IsValid)
{
continue;
}
CBaseEntity current = tracked.Value;
Console.WriteLine("Tracked entity is still valid.");
}
});Hooking Entity Outputs
Entity output hooks can be registered using methods or attributes.
private Guid _outputHookGuid;
public override void Load(bool hotReload)
{
_outputHookGuid = Core.EntitySystem.HookEntityOutput("func_door", "OnOpen", OnDoorOpened);
}
public override void Unload(bool hotReload)
{
if (_outputHookGuid != Guid.Empty)
{
Core.EntitySystem.UnhookEntityOutput(_outputHookGuid);
}
}
private void OnDoorOpened(IOnEntityFireOutputHookEvent @event)
{
Console.WriteLine($"Output '{@event.OutputName}' fired by '{@event.DesignerName}'");
}[EntityOutputHandler("func_door", "OnOpen")]
public void OnDoorOpened(IOnEntityFireOutputHookEvent @event)
{
Console.WriteLine($"Output '{@event.OutputName}' fired by '{@event.DesignerName}'");
}Hooking Entity Inputs
Entity input hooks can also be registered with methods or attributes.
private Guid _inputHookGuid;
public override void Load(bool hotReload)
{
_inputHookGuid = Core.EntitySystem.HookEntityInput("func_button", "Press", OnButtonPress);
}
public override void Unload(bool hotReload)
{
if (_inputHookGuid != Guid.Empty)
{
Core.EntitySystem.UnhookEntityInput(_inputHookGuid);
}
}
private void OnButtonPress(IOnEntityIdentityAcceptInputHookEvent @event)
{
Console.WriteLine($"Input '{@event.InputName}' accepted by '{@event.DesignerName}'");
}[EntityInputHandler("func_button", "Press")]
public void OnButtonPress(IOnEntityIdentityAcceptInputHookEvent @event)
{
Console.WriteLine($"Input '{@event.InputName}' accepted by '{@event.DesignerName}'");
}If you need to block the input/output flow, you can set @event.Result = HookResult.Stop inside the callback.
Callback Signatures
The hook callbacks use these delegate signatures:
public delegate void IEntitySystemService.EntityOutputEventHandler(IOnEntityFireOutputHookEvent @event)
public delegate void IEntitySystemService.EntityInputEventHandler(IOnEntityIdentityAcceptInputHookEvent @event)Many entity-system methods throw InvalidOperationException if called too early when the entity system is not available yet.
Reference
See IEntitySystemService for all entity-system methods.
See IEntitySystemService.EntityInputEventHandler and IEntitySystemService.EntityOutputEventHandler for callback signatures.
See EntityInputHandlerAttribute and EntityOutputHandlerAttribute for attribute-based hooks.