Server API for C# packages (NextMP.Server)
C# packages run on CoreCLR inside the server process. The server hosts .NET itself, through hostfxr, from its own pinned runtime in modules/dotnet, so no system-wide .NET is needed. The packages bind to the same core as JavaScript packages, with the same rules for identity, dimensions and values. JavaScript is not involved: a server with only C# packages never loads Node.
Status: server-side only, implemented and tested on macOS and Windows.
- Tested: the runtime test and a laid-out server running a package through start and SIGTERM.
- CI jobs are defined for Linux, macOS and Windows.
- Executed on Windows (2026-09-28): an MSVC x64 build, the runtime test on real CoreCLR, and a laid-out server with a C# package stopping on Ctrl+C.
- Not yet executed: a real Linux host.
The new player-voice operations are compiled/tested in the current macOS C# build; a Windows build of these additions is still pending. See voice (docs/voice.md) for Player.SetVoiceMuted, SetVoiceRange, JoinVoiceChannel, LeaveVoiceChannel, BlockVoice and the shared policies.
A package
packages/mode/
nextmp.json {"runtime": "csharp", "main": "bin/Mode.dll", "dependencies": ["accounts"]}
bin/Mode.dll your assembly
bin/*.dll its NuGet dependencies (copied by `dotnet build`/`publish`)
dependencies is optional and follows the same rules as JavaScript. A C# package may depend on a JavaScript package and the other way round.
The assembly must contain exactly one public class implementing NextMP.IPackage:
using NextMP;
public sealed class Mode : IPackage
{
public void Start()
{
Events.PlayerJoin += player => player.OutputChatBox($"welcome {player.Name}");
Events.AddCommand("car", (player, text, args) =>
player.PutIntoVehicle(Mp.Vehicles.New(Mp.Hash("adder"), player.Position), -1));
}
public void Stop() => Mp.Players.Broadcast("restarting");
}
Build it against NextMP.Server.dll from the server's modules/dotnet, and do not copy that file into the package. It must bind to the server's copy:
<Reference Include="NextMP.Server">
<HintPath>path/to/server/modules/dotnet/NextMP.Server.dll</HintPath>
<Private>false</Private>
</Reference>
Target net10.0. The server never downloads NuGet packages itself; ship what your package needs in its directory.
Threading, async and timers
- Server thread only. All API calls must come from the server thread, that is, from
Start, an event handler, a timer or a continuation. Calling from elsewhere throwsInvalidOperationException. awaitworks. Continuations resume on the server tick, not on the thread pool.- Timers.
Timers.SetTimeout(action, ms),Timers.SetInterval(action, ms)andTimers.Clear(id)run on the tick. Missed intervals are skipped, not replayed. Mp.StopTokenis cancelled when the package stops.- Handler exceptions are logged with the package name. They never reach the server or other packages.
API
| Area | Members |
|---|---|
Mp | Players, Vehicles, Objects, Peds, Log, Warn, Hash(name) (the game's joaat, equal to the server's), StopToken |
Player | Id, Valid, Name, SocialClub, Serial, Ip, Ping, Position (set teleports), Heading, Velocity, Dimension (int?, null is global), Health, Armour, Model (settable), Weapon, WeaponAmmo, Weapons, Frozen, Invincible, Animation, Vehicle, Seat; OutputChatBox, Kick, Call(event, args), CallUnreliable(event, args) (no delivery promise; see the server API), CallProcAsync(name, args), GiveWeapon, RemoveWeapon, RemoveAllWeapons, SetWeaponAmmo, GetWeaponAmmo, SetClothes, SetProp, PlayAnimation, StopAnimation, ClearBlood, PutIntoVehicle |
Vehicle | Id, Valid, Model, Position (set teleports), Rotation, Heading (settable), Velocity, Dimension, Controller, body/engine/petrol-tank health, Engine, Siren, Lights, Locked, WindowsOpen, Occupants, NumberPlate, NumberPlateType, Livery, WheelType, WindowTint, PearlescentColor, WheelColor, DirtLevel, NeonEnabled, NeonColor; Repair, SetColor/GetColor, SetColorRGB/GetColorRGB, SetMod/GetMod, SetExtra(int, bool)/GetExtra(int), SetPaint(primaryType, primaryColor, secondaryType, secondaryColor)/GetPaint(int) (−1: never set), SetDoor, GetOccupant, Destroy |
WorldObject, Ped | Id, Valid, Model, Position, Rotation and Dimension (all settable), Move(position, rotation?), Destroy; Ped also has Heading, Health, MaxHealth, Armour and Weapon (all settable) |
| Pools | Mp.Players.All/At/Count/Broadcast/InRange, Mp.Vehicles.New/All/At/Count/InRange, Mp.Objects.New/At/All/Count, Mp.Peds.New/At/All/Count |
| Variables | SetVariable, SetVariables (all or none), GetVariable<T>, HasVariable, DeleteVariable, GetVariableNames on every entity (same contract as JS) |
Mp.Blips | New(sprite, position, BlipOptions?); Blip: Position, Sprite, Color, Alpha, Scale, ShortRange, Name, Dimension |
Mp.Markers | New(type 0..43, position, scale, MarkerOptions?); Marker: Type, Position, Rotation, Direction, Scale, Color (Rgba), Visible, BobUpAndDown, FaceCamera, Rotate, DrawDistance, Dimension, HideFor(player)/ShowFor(player) |
Mp.Checkpoints | New(type 0..69, position, radius, CheckpointOptions?); Checkpoint: Type, Position, Direction, Radius, Height, Color, Visible, DrawDistance, Dimension, IsPlayerWithin, HideFor(player)/ShowFor(player) |
Mp.Colshapes | NewCircle, NewSphere, NewTube, NewCuboid, NewRectangle, NewPolygon (same arguments as JS); Colshape: ShapeType, Position (null for a polygon, which cannot move), Dimension, IsPointWithin, IsPlayerWithin |
| Shared operations | Player: SetHeadBlend, UpdateHeadBlend (mixes only), DisableVoiceTo/EnableVoiceTo (the target listener's voice block), SetHeadOverlay, SetHeadOverlayColor, SetFaceFeature, SetHairColor, SetEyeColor, SetDecoration/RemoveDecoration/ClearDecorations, GetAppearance, GiveWeaponComponent/RemoveWeaponComponent, SetWeaponTint, PlaySpeech, SetMaxArmour; every entity: AttachTo, Detach, GetAttachment; Mp.World: Weather, Time, SetTime, Explode, SetWeatherTransition, RequestIpl, RemoveIpl; Player.Notify; Blip/Marker/Checkpoint/Colshape: SetVariable, SetVariables, GetVariable<T>, HasVariable, DeleteVariable, GetVariableNames (server-side only); Mp.Server: Config, Start/Stop/RestartPackage, GetVehicleModel/GetPedModel/GetWeaponModel; Vehicle.GetModCount |
| Zone events | PlayerEnterColshape, PlayerExitColshape, PlayerEnterCheckpoint, PlayerExitCheckpoint |
| Streaming events | PlayerStreamIn(player, forPlayer), PlayerStreamOut(player, forPlayer) — produced while any C# package runs (the JS engine produces them only for packages with a handler) |
Events | PackagesLoaded, ServerShutdown, Tick, PlayerJoin, PlayerQuit, PlayerSpawn, PlayerDeath, PlayerChat (return false to cancel), PlayerCommand, PlayerDamage, PlayerStartEnterVehicle, PlayerStartExitVehicle, PlayerEnterVehicle, PlayerVehicleSeatChange, PlayerExitVehicle, PlayerWeaponChange, PlayerDimensionChange, VehicleCreate, VehicleDamage, VehicleDeath (vehicle only; cause and killer unknown), VehicleDestroy, ConsoleCommand, On(clientEvent, handler), AddCommand(name, handler), AddProc(name, handler) (sync or async) |
Appearance reads (GetClothes, GetProp, GetHeadBlend, GetFaceFeature, HairColor, HairHighlightColor, EyeColor) return announced Core data or nullable absence, not invented game facts. SetEyeColor remains the checked write path. IsAiming, IsClimbing, IsEnteringVehicle, IsInCover, IsJumping, IsLeavingVehicle, IsReloading, and IsInMelee read existing reported flags, not anticheat assertions. Vehicle Horn, HighBeams and Dead are likewise read-only reported state. Genuine generation checks apply to every read.
DistanceTo(Vector3) and DistanceSquaredTo(Vector3) use direct Core positions on physical entities and positioned world entities. Non-finite targets and destroyed wrappers are refused; centre-less polygon colshapes reject distance rather than assume a location. They do not route through JS or scan pools. World.SetTime(hour, minute, second) preserves seconds in the existing clock.
ServerShutdown is a synchronous no-argument event once before final package teardown; it cannot cancel or defer shutdown. PlayerStartExitVehicle has one current Player argument, after an observed same-vehicle leaving-state edge; unobserved intermediate states are not fabricated. Both subscriptions belong to their package and are removed on stop, with no remote event impersonation. Events.EntityModelChange has (Player player, uint oldModel) arguments, after a changed model is committed/published by Core. It currently covers Player model writes, not immutable entity classes; zero/unchanged writes do not fire. The captured generation rejects stale reconnect facts, and the subscription is removed on package stop. There is no JS bridge or ABI bump.
Events.EntityCreated has one VariableHolder argument, specifically a current Vehicle, WorldObject or Ped, after Core insertion and publication. Existing VehicleCreate runs first. Destruction by earlier handlers prevents stale delivery; subscriptions are package-owned and non-cancellable. Players and virtual/world entities are not covered. It uses tagged scalars in the current Core C ABI, not JS.
Every JavaScript server API has a C# counterpart. Where the shape differs, it is because .NET already has the idiom:
| JavaScript | C# |
|---|---|
mp.events.add / once / remove for engine events | Events.X += handler / -= (once: remove inside the handler) |
mp.events.call(name, ...) (local, same package) | Call your own method: a package's local events never leave it |
packageStop | IPackage.Stop(), plus Mp.StopToken |
pool.exists(entity), pool.atRemoteId(id), pool.forEach | entity.Valid, pool.At(id), foreach (var x in pool.All) |
pool.forEachInRange(position, range, fn) | pool.InRange(position, range) (same 0..10000 limit, any dimension) |
vehicle.teleport(position), setNeonColor/getNeonColor | vehicle.Position = …, NeonColor |
seat null (not yet assigned) | int? null |
Identity and lifetime:
- One object per entity. There is one
Playerobject per connection, so==and dictionary keys work across events. A reconnect is a new object. - Stale objects throw. A player who left or an entity that was destroyed throws
EntityGoneExceptionon use.
Players and vehicles:
Player.AllWeaponsis a detachedIReadOnlyDictionary<uint, int>of server-held weapon hashes and ammunition, read directly from Core; it has no write authority.- Same limits and answers as JS. Members beyond the basics use the same names, limits and answers as the JS API.
- Bad values throw. An out-of-range argument throws
InvalidOperationExceptionwith the server's reason. - Methods report the core's answer. Methods that JS answers with a boolean return that same boolean.
Blips, markers, checkpoints and colshapes:
- Same rules as JS. Field names, ranges and defaults are those of the JS API. They cross the boundary as JSON, and the server checks them in one place for both languages.
- Reads ask the server. Every property read asks the server, and every write changes that one field.
- Marker color.
Marker.Color = new Rgba(r, g, b, a)uses the same existing Core state as JavaScript'smarker.setColor(r, g, b, a); no JavaScript bridge or new wire format. - Refusals say why. Out-of-range values and a reached limit throw
InvalidOperationExceptionwith the server's reason, for examplethe server refused: 'alpha' must be an integer in 0..255. - Dimensions. Options and colshape methods default to dimension 0. Pass
nullfor global. - Ids are never reused. A destroyed one throws
EntityGoneExceptionon use.
Procedures (same contract as JS):
- Registering.
Events.AddProc(name, (player, arguments) => value)or anasynchandler. The value, or the task's result, is the answer, and a thrown exception answers with its message. One handler per name across all C# packages: a second one throwsInvalidOperationException. - Calling.
await player.CallProcAsync(name, args)returns the client's answer asJsonElement. With no answer it throwsProcedureException, whoseCodeisfailed,noHandler,busy,timeoutordisconnected, with the same messages as JavaScript. Bad names or arguments throwArgumentExceptionat once. - Threading. Answers and continuations run on the package's server-thread context, like every
other
await.
Values:
- Values sent to clients and stored as variables follow the network value rule:
null, bool, numbers, strings, arrays or lists, and objects with string keys (including anonymous objects). - Client event arguments arrive as
JsonElement[]. - Entities. A
Player,Vehicle,WorldObjectorPedpassed toCall,CallProcAsyncor returned from a procedure reaches the client as that entity (same rule as JS).- One that a client sent is a
JsonElementthatAsPlayer(),AsVehicle(),AsObject()orAsPed()resolves. Each givesnullfor another kind or for an entity that is gone. - Variables never hold entities:
SetVariablewith one throwsInvalidOperationException.
- One that a client sent is a
Stopping and unloading
A stopped package loses every handler, timer and pending continuation. Then its assemblies are unloaded from a collectible AssemblyLoadContext:
- Library caches. After unloading starts, the server asks libraries to drop cached type
information, through their declared
[MetadataUpdateHandler](the mechanism .NET hot reload uses). Without that, System.Text.Json alone kept every package that stored an anonymous object as a variable loaded forever. - Late unloads. If something still holds the package, for example a thread or a static field in another assembly, unloading continues in the background. The log warns if the package is still loaded 30 seconds later.
Deployment
- Your own builds.
tools/fetch-dotnet.sh(or.ps1) installs the pinned SDK into a directory of your choice. Configure with-DNEXTMP_DOTNET_ROOT=<that directory>. - What the server carries. The build copies the runtime into
modules/dotnet:host/fxr/<version>,shared/Microsoft.NETCore.App/<version>,NextMP.Server.dlland its runtimeconfig. - The layout (
nextmp_dist) carriesmodules/as is.
Platform additions
player.Transfer("eu-2", new { spawn = "airport" }); // throws with the reason when refused
var from = player.TransferredFrom; // null when joined directly
var discord = player.DiscordId; // verified by the server, never client-declared
player.SetDiscordPresence(new DiscordPresence(Details: "Racing", LargeImageKey: "track", PartySize: 1, PartyMax: 8));
player.SetDiscordPresence(null); // clear
Events.PlatformNotice += (name, player, data) => { /* playerTransfer, playerTransferFailed, playerDevice, playerIntegrity, queueJoin, queueLeave, serverDrain */ };
Discord sign-in, mp.state and the join-queue controls are JavaScript-only in this release; outgoing HTTP and timers use .NET's own HttpClient and timers. See platform.md.
Event handler presence
The server counts client event names that no package listens for (abuse diagnostics, security/network-abuse.md (docs/security/network-abuse.md)). C# packages take part through the optional managed entry Host.HandlesClientEvent: Events.On(name, …) makes a name known while the handler is registered, and stopping or failing to start a package removes its handlers. Procedures registered with Events.AddProc already claim their calls. There are no per-handler rate limits for C# handlers; limit inside the handler if needed.
Pool distance and dimension queries
Mp.Players and Mp.Vehicles provide GetClosest(position) (one entity or null) and GetClosest(position, limit) (a snapshot, limit 0–4096). GetClosestInDimension(position, dimension[, limit]) filters an exact signed 32-bit dimension; null selects only global entities. These match the server JS getClosest / getClosestInDimension contract: 3D distance, ties by id, finite coordinates required. ForEachInDimension(dimension, callback) visits a snapshot and skips entities destroyed by an earlier callback. Entities created during a callback wait until the next traversal. No GTA natives or ABI changes are involved.