Thread Safety
Some framework APIs are thread-unsafe. Calling them from non-main-thread contexts can cause undefined behavior or crashes.
If an API is marked thread-unsafe, do not call it directly from background tasks. Use its async counterpart or schedule execution on the main loop.
Core Rule
- Thread-unsafe APIs must run on the main thread.
- Prefer
Asyncvariants when they exist. - Async variants return
Task(orTask<T>) so they can be awaited in async flows.
In practice, async variants typically execute immediately when already on the main thread, and otherwise are scheduled safely for main-thread execution.
How to Identify Thread-Unsafe APIs
In API reference pages, thread-unsafe methods are annotated with [ThreadUnsafe] and typically include a note like:
Thread unsafe, use async variant instead for non-main thread context.
See ThreadUnsafeAttribute for the marker used in API docs.
Safe Usage Patterns
Prefer Async Counterparts
public async Task NotifyPlayerAsync(IPlayer player)
{
await player.SendChatAsync("Hello from async flow.");
await player.ExecuteCommandAsync("play buttons/blip1");
}Emit Sound Safely in Async Flows
using var sound = new SoundEvent("UI.CounterBeep", volume: 1.0f, pitch: 1.0f);
sound.Recipients.AddAllPlayers();
await sound.EmitAsync();Schedule Explicitly on the Main Loop
If you are in a background flow and need to invoke a sync-only path, schedule it with the scheduler:
public async Task NotifyOnMainThreadAsync(IPlayer player)
{
await Core.Scheduler.NextTickAsync(() =>
{
player.SendChat("Executed on main thread via scheduler.");
});
}Do not use scheduler overloads that accept Func<Task...> for NextTick/NextWorldUpdate; those overloads are obsolete and documented as unsafe.
Common Thread-Unsafe APIs
| Thread-Unsafe API | Async / Safe Alternative |
|---|---|
SoundEvent.Emit() | SoundEvent.EmitAsync() |
ICommandContext.Reply(string) | ICommandContext.ReplyAsync(string) |
IPlayer.SendChat(string) | IPlayer.SendChatAsync(string) |
IPlayer.SendMessage(...) | IPlayer.SendMessageAsync(...) |
IPlayer.Kick(...) | IPlayer.KickAsync(...) |
IPlayer.ExecuteCommand(string) | IPlayer.ExecuteCommandAsync(string) |
IPlayer.Teleport(...) | IPlayer.TeleportAsync(...) |
IGameEventService.Fire* | IGameEventService.FireAsync* |
IEngineService.ExecuteCommand(string) | IEngineService.ExecuteCommandAsync(string) |
IEngineService.ExecuteCommandWithBuffer(...) | IEngineService.ExecuteCommandWithBufferAsync(...) |
IEngineService.DispatchParticleEffect(...) | IEngineService.DispatchParticleEffectAsync(...) |
CEntityInstance.AcceptInput<T>(...) | CEntityInstance.AcceptInputAsync<T>(...) |
CEntityInstance.AddEntityIOEvent<T>(...) | CEntityInstance.AddEntityIOEventAsync<T>(...) |
CEntityInstance.DispatchSpawn(...) | CEntityInstance.DispatchSpawnAsync(...) |
CEntityInstance.Despawn() | CEntityInstance.DespawnAsync() |
CBaseModelEntity.SetModel(string) | CBaseModelEntity.SetModelAsync(string) |
CBaseModelEntity.SetBodygroupByName(string, int) | CBaseModelEntity.SetBodygroupByNameAsync(string, int) |
CCSPlayerController.Respawn() | CCSPlayerController.RespawnAsync() |
*Projectile.EmitGrenade(...) | *Projectile.EmitGrenadeAsync(...) |
Migration Checklist
- Find sync calls to APIs marked thread-unsafe.
- Replace them with async variants and
awaitthe returned task. - If no async variant exists, schedule the call with
Core.Scheduler.NextTickorCore.Scheduler.NextWorldUpdate. - Avoid fire-and-forget calls for gameplay-critical operations.
Reference
See ThreadUnsafeAttribute for the thread-unsafe marker.
See ISchedulerService for safe main-loop scheduling.
See IPlayer, IGameEventService, IEngineService, CEntityInstance, and SoundEvent for sync/async method pairs.