swiftlys2/docs/api/ai/

Thread Safety

Some framework APIs are thread-unsafe. Calling them from a non-main-thread context (e.g. inside Task.Run, or after await-ing a database call) can cause undefined behavior or crashes.

If an API is marked thread-unsafe, don't call it directly from a background thread. Use its Async counterpart, or schedule the call back onto the main thread first.

Core Rule

Thread-unsafe methods are annotated with APIThreadUnsafeAttribute in source and must run on the game thread. Prefer their Async variant when one exists - it marshals the call for you; otherwise hand it to Core.Scheduler yourself. Check Core.IsGameThread at runtime to branch explicitly:

if (!Core.IsGameThread)
{
    Core.Scheduler.NextTick(() => player.SendChat("Hello!"));
}
else
{
    player.SendChat("Hello!");
}

Safe Usage Patterns

Prefer Async counterparts in any async flow (e.g. after a database call):

public async Task NotifyPlayerAsync(IPlayer player)
{
    await player.SendChatAsync("Hello from an async flow.");
 
    using var sound = new SoundEvent("UI.CounterBeep", volume: 1.0f, pitch: 1.0f);
    sound.Recipients.AddAllPlayers();
    await sound.EmitAsync();
}

For a sync-only API with no Async variant, schedule it explicitly instead:

await Core.Scheduler.NextTickAsync(() => player.SendChat("Executed on the main thread via the scheduler."));

Common Thread-Unsafe APIs

Thread-Unsafe APIAsync Alternative
APIIPlayer.SendChatSendChatAsync
APIIPlayer.SendMessageSendMessageAsync
APIIPlayer.SendConsoleSendConsoleAsync
APIIPlayer.SendCenterSendCenterAsync
APIIPlayer.SendAlertSendAlertAsync
APIIPlayer.SendCenterHTMLSendCenterHTMLAsync
APIIPlayer.SendChatEOTSendChatEOTAsync
APIIPlayer.SendNotifySendNotifyAsync
APIIPlayer.KickKickAsync
APIIPlayer.ExecuteCommandExecuteCommandAsync
APIIPlayer.TeleportTeleportAsync
APIIPlayer.TakeDamageTakeDamageAsync
APIIPlayer.SwitchTeam / ChangeTeamSwitchTeamAsync / ChangeTeamAsync
APISoundEvent.EmitEmitAsync
APIIGameEventService.Fire and friendsFireAsync, FireToPlayerAsync, FireToServerAsync
APIIGameService.EmitHEGrenade and friendsEmitHEGrenadeAsync, EmitFlashbangAsync, EmitSmokeGrenadeAsync, EmitMolotovAsync, EmitDecoyAsync

APIIEngineService.ExecuteCommand, ExecuteCommandWithBuffer and DispatchParticleEffect also have Async counterparts - use those from a background flow even if you're unsure the sync overload is unsafe.

Checklist

Find sync calls to thread-unsafe APIs running after an await (outside Load/event handlers), replace with the Async variant, or wrap with Core.Scheduler.NextTick/NextWorldUpdate if no Async variant exists. Don't fire-and-forget gameplay-critical calls - await them or handle failures explicitly.

Reference

See Swiftly Core for Core.IsGameThread and Core.Scheduler.

See Scheduler for the full scheduling API.