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 API | Async Alternative |
|---|---|
| APIIPlayer.SendChat | SendChatAsync |
| APIIPlayer.SendMessage | SendMessageAsync |
| APIIPlayer.SendConsole | SendConsoleAsync |
| APIIPlayer.SendCenter | SendCenterAsync |
| APIIPlayer.SendAlert | SendAlertAsync |
| APIIPlayer.SendCenterHTML | SendCenterHTMLAsync |
| APIIPlayer.SendChatEOT | SendChatEOTAsync |
| APIIPlayer.SendNotify | SendNotifyAsync |
| APIIPlayer.Kick | KickAsync |
| APIIPlayer.ExecuteCommand | ExecuteCommandAsync |
| APIIPlayer.Teleport | TeleportAsync |
| APIIPlayer.TakeDamage | TakeDamageAsync |
APIIPlayer.SwitchTeam / ChangeTeam | SwitchTeamAsync / ChangeTeamAsync |
| APISoundEvent.Emit | EmitAsync |
| APIIGameEventService.Fire and friends | FireAsync, FireToPlayerAsync, FireToServerAsync |
| APIIGameService.EmitHEGrenade and friends | EmitHEGrenadeAsync, 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.