Try the new CitizenFX C# templates for FiveM

We’ve recently published the CitizenFX.Templates package on NuGet, which includes a nice template for making a fully C#-based resource using the latest recommendations. The docs will be updated whenever more people confirm this works and report some feedback as to this, but for now here’s the steps to set it up:

dotnet new -i CitizenFX.Templates
mkdir MyResource
cd MyResource
dotnet new cfx-resource

You’ll find helpful instructions afterwards in the generated README.md.

Uploading photos from in-game to the Cfx.re Gallery

Recent builds of FiveM have added backing functionality to the photo gallery feature in the pause menu. This guide shows how to upload your photos to the Cfx.re Snapmatic section on the forums.

  1. Make sure you have a photo camera resource that uses photo commands to save a gallery photo. An example resource would e.g. be Xinerki’s ‘Cellphone Camera‘ resource, instructions for which can be found in the topic.
  2. Sign in and link your Cfx.re forum account in the main menu of FiveM.
  3. Go to the pause menu in a server (or a local game), and go to Gallery, and press Enter/A.
  4. Select a photo to upload, assuming you have a few. You can also press Enter/A to view/confirm and change the title.
  5. Press the ‘Upload to Social Club’ button as marked in the bottom of the screen.
  6. Say ‘Yes’.
  7. Wait a while.
  8. You’ll see your image uploaded on the forums!

Using the new console key bindings

In future versions of FiveM (currently a partial implementation is on canary), you’ll be able to define keyboard bindings in a new way, as originally detailed in the 2018 design document “Configuration & Input – Redesigned“.

Here’s a sample snippet of a simple ‘hands up’ binding:

local handsUp = false

CreateThread(function()
    while true do
        Wait(0)

        if handsUp then
            TaskHandsUp(PlayerPedId(), 250, PlayerPedId(), -1, true)
        end
    end
end)

RegisterCommand('+handsup', function()
    handsUp = true
end, false)

RegisterCommand('-handsup', function()
    handsUp = false
end, false)

RegisterKeyMapping('+handsup', 'Hands Up', 'keyboard', 'i')
RegisterKeyMapping('say hi', 'Say hi', 'keyboard', 'o')
User-editable bindings.

These bindings will be editable by the user in the ‘key bindings’ option (only 1 binding per command yet, no secondary binding), and depending on user demand we’ll add a helper for computing the right hashes to use with ~INPUT_~-style display as well for help hints and similar.

In addition to that, a plethora of new console commands are added to the client F8 console, an example is below:

// bind a key to toggle the safe zone to an offscreen size and back
bind KEYBOARD F3 "+vstr hideHud showHud"
seta "hideHud" "profile_safezoneSize 500"
seta "showHud" "profile_safezoneSize 7"

// list all bindings
bind

// bind for a specific resource (similar to RegisterKeyMapping)
rbind runcode keyboard o "say hi"

// unbind
unbind keyboard f3

// set the volume way above the usual max
profile_sfxVolume 50

// ear rape?
profile_sfxVolume 9000

// toggle a variable
toggle strmem

// toggle a variable between two values
toggle con_miniConChannels * minicon:*

// see cmdlist to view all new and existing variables and commands

… and the fxd:/fivem.cfg (%appdata%\citizenfx) file contains a lot of additional user settings now in a readable format.

Optimizing resource downloads using a caching proxy

Server builds starting at pipeline ID 1679 added the ability to configure the fileServer field used for resource downloading using a set of commands:

# Set the file server for the specified resource regex.
# The URL should *not* end with a slash.
fileserver_add ".*" "http://10.10.0.1/files"

# Remove the file server associated with a resource regex.
fileserver_remove ".*"

# List all registered file server patterns.
fileserver_list

# Old command, but **required** to not get corrupted cache entries.
adhesive_cdnKey "someSecurePassphrase"

There’s currently no cache invalidation logic based on hashes (this’ll need yet another server update due to the file server not ignoring query strings + a client update), so make sure to clear your proxy’s cache before you modify/restart a resource.

Here’s an example NGINX configuration for setting this up (but you should, for example, run this behind yet another proxy which offers HTTP/2 over TLS, or otherwise set this up properly – we’re expecting the community to provide fleshed-out examples on the forums):

default.template

proxy_cache_path /srv/cache levels=1:2 keys_zone=assets:48m max_size=10g ;
log_format asset '$remote_addr - [$time_local] "$request" $status $body_bytes_sent $upstream_cache_status';

server {
        listen 80;

        location /files/ {
                access_log /dev/stdout asset;
                add_header X-Cache-Status $upstream_cache_status;
                proxy_cache_lock on;
                proxy_pass $REMOTE$request_uri;
                proxy_cache assets;
                proxy_cache_valid 1y;
                proxy_cache_key $request_uri$is_args$args;
        }
}

Dockerfile

FROM nginx:alpine
COPY default.template /etc/nginx/conf.d/default.template
CMD sh -c "envsubst \"`env | awk -F = '{printf \" \\\\$%s\", $1}'`\" < /etc/nginx/conf.d/default.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"

Start command

docker build -t fxproxy .
docker run -d --name=fxproxy -e REMOTE=http://10.10.0.2:30120 -p 80:80 -v $PWD/cache:/srv/cache fxproxy

OneSync: intercepting game events (such as explosions)

The latest version of the server (1543+) added support for parsing and preventing routing of game events. This currently only supports parsing CExplosionEvent, but this might be a fairly useful one to prevent routing of ‘excessive’ explosions, or explosions that are ‘too close’ to a player and not of the correct weapon type.

Here’s an example:

-- SERVER script, requires OneSync!
AddEventHandler('explosionEvent', function(sender, ev)
    print(GetPlayerName(sender), json.encode(ev))
end)

This’ll show JSON data similar to the following:

{"explosionType":0,"isAudible":true,"posX":742.84313964844,"cameraShake":1.0,"isInvisible":true,"ownerNetId":0,"posY":-1808.2889404297,"damageScale":1.0,"posZ":33.105224609375}

If you want to, say, make an explosion-free zone:

AddEventHandler('explosionEvent', function(sender, ev)
    if ev.posX > 2000.0 and ev.posY > 2000.0 and ev.posX < 3000.0 and ev.posY < 3000.0 then
        CancelEvent()
    end
end)

This can be used in a variety of ways – rate limiting, automatic warning/kicking, disabling routing of bad explosions, etc.

Two new experimental OneSync variables

In the latest Windows server release (1504 or higher), two experimental variables have been added to help troubleshooting problematic behavior and offer a ‘workaround’ for common problems.

onesync_distanceCullVehicles true

This console variable (set it in your config, or in the console at runtime) will lead to player-occupied vehicles also being subject to distance culling – that is, they won’t exist if a player is more than n units away from the entity.

Enabling the variable might help with game performance (FPS), and should help with bandwidth concerns as well as ‘texture loss’/’city bug’ streaming issues caused by loading larger amounts of custom addon player vehicles than usual.

Cons are that player-occupied vehicles will not be known to client scripts unless another player is nearby enough. This might affect ‘teleport into player’s vehicle’ scripts, for instance.

onesync_forceMigration true

This one forces any entity which has not received any clone sync updates for over x seconds to be migrated to any other nearby player. While it doesn’t fix the underlying cause for ‘ghost vehicle/ped’ migration failures, it should make them less painful and not require kicking the alleged owner anymore.

There should not be too many cons, but it’s disabled by default lest it crash players when it’s triggered, or worse.

Useful snippet: getting the top left of the minimap in screen coordinates

SetScriptGfxAlign(string.byte('L'), string.byte('B'))
local minimapTopX, minimapTopY = GetScriptGfxPosition(-0.0045, 0.002 + (-0.188888))
ResetScriptGfxAlign()

local w, h = GetActiveScreenResolution()

return { w * minimapTopX, h * minimapTopY }

This could be useful if you want to align things to NUI or other screen-space draws, and works on pretty much any aspect ratio. The magic numbers here are the minimap position from common:/data/ui/frontend.xml, as seen below:

<data name="minimap"           alignX="L"	alignY="B"	posX="-0.0045"		posY="0.002"		sizeX="0.150"		sizeY="0.188888" /> <!-- WARNING! Feed MUST match posX and PosY -->

This will likely also work for aligning to other positions, assuming you set the right sizes.

Improving script stack traces

In the past, the Citizen framework scripting runtime would print really messy stack traces such as.. this:

Stack traces across runtimes.

This stack trace goes on for around 3-4 times the displayed length, and is generally.. a big mess. You can tell that some resource code is running, like @stest3/sv.lua:20, f.js:8 and @stest1/sv.lua, but everything else that is shown is just noise.

Starting at today’s Windows server (and client, on canary) releases, we’re doing something new and exciting: stack boundary stitching. This is the third attempt at implementing cross-runtime stack tracing, and the first one that actually had the possibility to make it out the door.

To compare, look at the new stack trace output:

A stitched stack.

You’ll see that we’re filtering out only user code, and we’re also showing each error only once: every error down the stack due to failed reference calls simply gets hidden. In addition to that, we now format C# functions using a ‘friendly’ library with support for providing nice method names.

An API and more functionality for stack traces (showing a call boundary, JS source map support) might be offered in the future – for now this is a feature which is generally runtime-internal, and is supported across runtimes: Mono/C#, V8/JS and Lua.

It’d also be really nice if you’d provide some more suggestions regarding developer experience improvements you’d love to see. We think this one will help a lot diagnosing script issues at a glance, however!

GET_ACTIVE_PLAYERS: the replacement for player loops

Just a quick hint: when writing new client-side scripts in Lua/JS (C# already has the Players list doing exactly this), you can loop through players by using the GET_ACTIVE_PLAYERS native. See below for an example of before/after:

Before…

for i = 0, 255 do
    if NetworkIsPlayerActive(i) then
        local ped = GetPlayerPed(i)
        -- do stuff
    end
end

After…

for _, player in ipairs(GetActivePlayers()) do
    local ped = GetPlayerPed(player)
    -- do stuff
end

This should be a bit more performant than 256 native invocations, even if no player slot exists, and it also doesn’t differ based on server-side sync technology slot count – all players with an active ped should be returned by this native.

Adaptive Cards in deferrals

We’ll be writing some more documentation on this soon, but the latest server version (combined with the canary client at this time – no prod yet!) supports presenting Adaptive Cards to clients during deferrals.

Here’s a quick code sample:

AddEventHandler('playerConnecting', function(name, skr, d)
    d.defer()

    Wait(50)

    -- badly serialized JSON in a string, from the Adaptive Cards designer
    d.presentCard([==[{"type":"AdaptiveCard","body":[{"type":"TextBlock","size":"ExtraLarge","weight":"Bolder","text":"Server password?!"},{"type":"TextBlock","text":"That's right, motherfucker! You have to enter a goddamn PASSWORD to connect to this server...","wrap":true},{"type":"Input.Text","id":"password","title":"","placeholder":"better enter one now"},{"type":"Image","url":"http://images.amcnetworks.com/ifccenter.com/wp-content/uploads/2019/04/pulpfic_1280.jpg","altText":""},{"type":"ActionSet","actions":[{"type":"Action.Submit","title":"Sure..."},{"type":"Action.ShowCard","title":"YOU WISH!!!!!!","card":{"type":"AdaptiveCard","body":[{"type":"Image","url":"https://i.imgur.com/YjMR0E6.jpg","altText":""}],"$schema":"http://adaptivecards.io/schemas/adaptive-card.json"}}]}],"$schema":"http://adaptivecards.io/schemas/adaptive-card.json","version":"1.0"}]==],
        function(data, rawData)
            -- you can chain cards, this is just the example adaptive card in the designer
            d.presentCard([==[{"type":"AdaptiveCard","body":[{"type":"Container","items":[{"type":"TextBlock","size":"Medium","weight":"Bolder","text":"Publish Adaptive Card schema"},{"type":"ColumnSet","columns":[{"type":"Column","items":[{"type":"Image","style":"Person","url":"https://pbs.twimg.com/profile_images/3647943215/d7f12830b3c17a5a9e4afcc370e3a37e_400x400.jpeg","size":"Small"}],"width":"auto"},{"type":"Column","items":[{"type":"TextBlock","weight":"Bolder","text":"Matt Hidinger","wrap":true},{"type":"TextBlock","spacing":"None","text":"Created {{DATE(2017-02-14T06:08:39Z,SHORT)}}","isSubtle":true,"wrap":true}],"width":"stretch"}]}]},{"type":"Container","items":[{"type":"TextBlock","text":"Now that we have defined the main rules and features of the format, we need to produce a schema and publish it to GitHub. The schema will be the starting point of our reference documentation.","wrap":true},{"type":"FactSet","facts":[{"title":"Board:","value":"Adaptive Card"},{"title":"List:","value":"Backlog"},{"title":"Assigned to:","value":"Matt Hidinger"},{"title":"Due date:","value":"Not set"}]}]}],"actions":[{"type":"Action.ShowCard","title":"Set due date","card":{"type":"AdaptiveCard","body":[{"type":"Input.Date","id":"dueDate"},{"type":"Input.Text","id":"comment","placeholder":"Add a comment","isMultiline":true}],"actions":[{"type":"Action.Submit","title":"OK","url":"http://adaptivecards.io"}],"$schema":"http://adaptivecards.io/schemas/adaptive-card.json"}},{"type":"Action.Submit","title":"View","url":"http://adaptivecards.io"}],"$schema":"http://adaptivecards.io/schemas/adaptive-card.json","version":"1.0"}]==],
                function(_, rawData2)
                    -- normally you'd check the password, also submit button IDs are sent as submitId
                    -- we're lazy so just reject everyone and tell them the password
                    d.done('you suck actually for entering the password: ' .. data.password .. ' and data like ' .. rawData2)
                end)
        end)
end)