Skip to Content
ScriptingC# Tasks & NUI Callbacks

C# asynchronous work, exports and NUI callbacks

Use CitizenFX’s scheduler for game-facing work, and make callback completion explicit. Thread.Sleep blocks a thread; Task.Run does not establish the Citizen execution context required by natives. An async method that outlives the operation which started it also needs cancellation or an equivalent stale-work guard.

The runtime-lab source download contains separate client and server projects, a shared validation helper and a small browser UI. Both assemblies were compiled against CitizenFX.Core 1.0.26803, targeting net452 Legacy/Mono, with .NET SDK 10.0.401. This is not an Enhanced binary; use the migration guide for that separate target.

Compile the resource, not just the snippet

Extract as runtime-lab and run node build.mjs. It publishes FD.Runtime.Client.net.dll and FD.Runtime.Server.net.dll into separate dist/client and dist/server directories. The manifest points to those exact filenames. The project references exclude CitizenFX runtime assets from distribution; FiveM supplies its own core runtime rather than accepting a bundled, potentially conflicting CitizenFX.Core.dll from this example.

The JavaScript companion explains installation and the permission-controlled server request. The C# client commands /fd_timer, /fd_timer_cancel and /fd_form are local teaching actions and do not modify a database or grant permissions.

Await a game-scheduler delay and cancel obsolete work

The lab increments a generation counter when a new reminder starts, when it is cancelled and when the resource stops. Only the current generation may complete:

private async Task ReminderAsync(int ticket) { try { await Delay(1500); if (stopped || ticket != generation) return; Debug.WriteLine("[Lab] Reminder completed on the Citizen scheduler."); } catch (Exception error) { Debug.WriteLine("[Lab] Reminder failed: " + error.GetType().Name); } }

This is cancellation of the effect, not a claim that the underlying delay was physically aborted. The full class derives from BaseScript, registers the commands, stores the counter and catches failures. It avoids unobserved, fire-and-forget exceptions by handling them inside the launched task. Test starting twice quickly, cancelling before completion and restarting the resource during the wait. Only the last still-valid operation should print a completion.

Do not generalize this to arbitrary background database/network tasks without checking the runtime’s continuation behavior. Keep game-state access on the documented scheduler, and use session identity checks before delivering a delayed server result to a player.

Expose a deliberate synchronous export

In src/Server/Server.cs, the export is a typed delegate:

Exports.Add("FormatBanner", new Func<string, string>(Protocol.FormatBanner));

Protocol.FormatBanner accepts only a bounded printable string and returns a bounded string. The caller receives data, not a Task, exception object or mutable server object. Validation lives in the shared helper so the same input rules can be tested outside CitizenFX. The server JS wrapper separately controls access and catches export failures; the export itself is not a network authorization boundary.

For an asynchronous API, design the request, completion, error, timeout and restart contract explicitly. Do not present a Task-returning method as interoperable with Lua/JS just because C# compiles it. See exports and resource boundaries.

Complete a NUI callback exactly once

The Legacy fixture uses RegisterNuiCallbackType plus its matching __cfx_nui: event handler, which was checked by compiling against the pinned package. The current NUI callback documentation  also describes newer direct callback APIs; choose the API supported by the runtime and package you actually target.

The preview handler computes a reply inside a guarded block, then calls the callback outside that block:

object result; try { // Validate the dictionary and title; no server-side mutation occurs here. result = new { ok = true, message = "Local preview accepted; nothing was saved." }; } catch { result = new { ok = false, message = "Preview unavailable." }; } cb(result);

This excerpt illustrates completion placement; the download contains the real validation. If cb itself throws, placing it inside a try followed by another cb in catch can attempt a second completion. A missing completion can instead leave the browser request waiting indefinitely. Every accepted callback path must resolve once.

The browser submits JSON over https://${GetParentResourceName()}/preview, enforces a five-second fetch timeout and renders replies with textContent, not untrusted HTML. Its length checks are user experience, not security. Any real server operation would still require a separately validated server request.

Own focus only while the form is open

/fd_form opens the panel and captures focus. The close callback releases the focus the resource owns; Escape calls that path. onClientResourceStop invalidates reminders and closes the UI, so restarting a resource is also a tested recovery path. This simple lab is not a multi-resource focus coordinator; two unrelated menus must not continually steal focus from each other.

Verify valid/empty/long/control-character inputs, Escape, a missing callback resource, double submission and restart with the form open. Shared C# protocol tests and the Node harness pass in CI, and both CitizenFX projects compile. In-client callback dispatch, focus recovery and cross-runtime marshaling still require an actual FiveM acceptance run. The C# runtime manual  and NUI basics provide the surrounding contracts.