Skip to content

About

FiveM for GTA V Enhanced on Linux under native Wine: findings, install scripts, and a minimal WebView2 repro

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

Repository files navigation

FiveM for GTA V Enhanced on Linux — native Wine, no Bottles

Getting the FiveM for GTA V Enhanced client running on Linux under plain Wine, and the FXServer dev loop alongside it. This is a findings write-up: what broke, what the cause actually was, and where I was wrong on the way.

Result: the launcher runs and reaches the live server browser. Everything below is reproducible with install.sh plus bin/launch.sh.

FiveM for GTA V Enhanced running on Linux, showing the live server browser

Not working: connecting to a server. It fails early — inside the UI's own resolveServer() lookup — so the anti-cheat / ticket integrity wall that stops Enhanced multiplayer on Linux has not been reached, let alone tested. No anti-cheat evasion was attempted, and none is described here — nothing in this repo hides Wine from anything.


Test machine

OS CachyOS (Arch), KDE on Wayland
GPU RTX 3070, NVIDIA 610.57.04
Wine wine-cachyos 10.0 (also tested GE-Proton 11-3 = wine 11.0-staging, and upstream wine 11.16)
Game GTA V Enhanced, Steam appid 3240220

No Flatpak, no Bottles. That removes a whole class of problems — commands touch the prefix directly, and winedbg attaches to a normal process instead of fighting a sandbox PID namespace.


The short version

  1. Install WebView2 from the offline standalone installer, not the bootstrapper.
  2. Install DXVK + VKD3D-Proton. Wine's builtin wined3d is what renders the launcher as a black window here — not WebView2.
  3. Skip FiveM_Installer.exe. It crashes under Wine on every runner tested. The Cfx depot serves public, hash-verified manifests; fetch from those instead.
  4. Write the registry entries and config.toml the installer would have written.
  5. Kick the updater. The launcher's "ready" handshake is a no-op, so it hangs at INITIALIZING UPDATER… 0% forever. invokeNative('retryUpdates') starts it.
  6. Register a desktop-side cfx:// handler — the client registers the scheme inside the prefix, but nothing on Linux claims it, so auth callbacks and join links have nowhere to go.
  7. The invisible cursor traces to a missing Xcursor fallback theme rather than a Wine bug, though see Chapter 6 — still unsolved here.

Chapter 1 — WebView2, the easy way

The Evergreen bootstrapper (fwlink 2124703) is the one everyone reaches for, and it half-installs under Wine: exits non-zero, unpacks into EdgeCore\, then skips both the copy into EdgeWebView\Application\ and the registry write.

The offline standalone installer does not have that problem:

https://go.microsoft.com/fwlink/p/?LinkId=2124701
wine MicrosoftEdgeWebView2RuntimeInstallerX64.exe /silent /install

Exit code 0. It populates EdgeWebView\Application\<version>\ itself and writes Software\Wow6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-…} with pv, name and location. No manual copy, no registry surgery.

install.sh still writes the keys into the 64-bit view as well, since FiveM's loader is 64-bit and the installer only registers the WOW6432Node view. Registry writes happen with no wineserver running — a live one will flush its in-memory copy over your changes and they silently vanish.

Chromium also runs fine on Wine 10/11. A headless smoke test of msedgewebview2.exe produced no SIGSYS and no RPC failure. The old "Wine can't run Chromium" wall is a Wine 9 problem.

Chapter 2 — The black window is wined3d, not WebView2

The launcher came up black, and the wine log said why, once per frame:

err:d3d:wined3d_debug_callback "GL_INVALID_FRAMEBUFFER_OPERATION error generated.
                                Framebuffer bindings are not framebuffer complete."
fixme:dxgi:d3d11_swapchain_Present1 Ignored present parameters

wined3d means D3D11 was being translated to OpenGL. Dropping DXVK and VKD3D-Proton into the prefix and forcing them native fixed the rendering immediately:

cp GE-Proton*/files/lib/wine/dxvk/x86_64-windows/{d3d11,d3d10core,dxgi,d3d9}.dll  system32/
cp GE-Proton*/files/lib/wine/vkd3d-proton/x86_64-windows/{d3d12,d3d12core}.dll     system32/
export WINEDLLOVERRIDES="d3d9,d3d10core,d3d11,d3d12,d3d12core,dxgi=n"

After that: DXVK: v3.0.2, D3D_FEATURE_LEVEL_12_1, vkd3d-proton reporting DXR 1.1.

Worth separating these two, because "black launcher window" gets blamed on WebView2 a lot. Here WebView2 was healthy and the renderer was the problem.

Chapter 3 — The installer crashes, so don't use it

FiveM_Installer.exe dies immediately after printing its own debug line:

OutputDebugStringA "Loaded OpenGL 3.3."
err:seh:NtRaiseException Unhandled exception code c0000409

0xC0000409 is a fail-fast. Under winedbg the real cause is an unhandled C++ exception whose what() is:

bad array new length

…i.e. new T[n] with a bogus n. It's a GLFW + RmlUi UI (both libraries' strings are all over the binary), and it dies at the same instruction on wine-cachyos 10.0, GE-Proton 11-3 and upstream wine 11.16. Not a runner problem. The download is byte-identical to the CDN, so not corruption either.

Rather than debug someone else's installer, note what it actually does — and do that:

The Cfx depot is public and self-describing.

https://depot.cfx.re/public/p/<product>/u/<branch>

returns JSON with a build number and, per file, a download URL, byte count, MD5 and SHA256:

product branch build files size
cfxlauncher main 138 3 34.9 MB
cfxui_gen9 main 58 1 5.0 MB
fivem_gen9 main 131 177 145.8 MB

bin/fetch-launcher.sh and bin/fetch-product.sh pull these and verify every file's SHA256. Same files, same source, hash-checked — just without the crashing GUI.

modify.exe, which the launcher looks for as its updater, is simply the installer copied under another name. install.sh places it, though note it will hit the same crash if the launcher ever actually invokes it.

Chapter 4 — What the installer writes

With the files in place the launcher still said "Launcher is not installed." It reads that from the registry, so those entries have to exist. The names come straight out of the installer's UTF-16 strings:

Software\FiveM-for-GTAV-Enhanced                 InstallLocation, DisplayVersion
Software\FiveM-for-GTAV-Enhanced\Settings        GameCacheLocation, ServerCacheLocation
Software\Microsoft\Windows\CurrentVersion\Uninstall\FiveM-for-GTAV-Enhanced
SOFTWARE\WOW6432Node\Rockstar Games\GTAV Enhanced  InstallFolder, InstallFolderSteam

The game itself is mapped into the prefix as a drive letter rather than reached through Z: — dosdevices/r: → the GTA V Enhanced folder. Short paths, fewer surprises.

There is also a client config at %APPDATA%\FiveM for GTAV Enhanced\config.toml. Absent, the launcher logs Failed to load client config. It is a flat TOML map of ConVars; gamepath, gamePlatform and ui_updateChannel are enough to silence it.

See bin/register.sh.

Chapter 5 — The real blocker, and the wrong turn I took first

Now the launcher rendered properly, logged no errors at all, and sat at INITIALIZING UPDATER… 0% forever. No network activity. winedbg showed the main thread parked in its message loop, waiting.

The launcher stalled at INITIALIZING UPDATER, 0%

The launcher UI is a WebView2 page. Its only channel to native is:

function safePost(msg) {
  try { window.chrome && window.chrome.webview && window.chrome.webview.postMessage(msg); } catch (_) {}
}
…
safePost({ type: 'ready' });

Attaching a DevTools client showed window.chrome.webview === null, at document start, before any page script ran, with nothing ever assigning it.

So I concluded Wine had failed to bind the bridge, and I was wrong. I spent a while on that theory:

  • Tried WebView2 148.0.3967.96 (the oldest fixed version Microsoft still publishes) against Evergreen 152.0.4191.53 — identical behaviour.
  • Disabled site isolation (--disable-site-isolation-trials, --disable-features=IsolateOrigins,site-per-process, --renderer-process-limit=1, --process-per-site), verified applied — no change.
  • --single-process — no change.
  • Confirmed the renderer really did carry --embedded-browser-webview=1.

What settled it was writing a minimal WebView2 host — repro/wv2repro.cpp, ~150 lines, cross-compiled with mingw-w64 against WebView2 SDK 1.0.4191.47 — and running it in the same prefix against the same runtime:

runtime override: C:\Program Files (x86)\Microsoft\EdgeWebView\Application\152.0.4191.53
  [0] initial document (no navigation yet)   "about:blank  ->  chrome.webview = object"
  [1] after navigate #1 (data: URL)          "data:…one  ->  chrome.webview = object"
  [2] after navigate #2 (data: URL)          "data:…two  ->  chrome.webview = object"
  [3] after navigate #3 (about:blank)        "about:blank  ->  chrome.webview = object"

Wine's WebView2 bridge is fine. The null is FiveM's own doing. Its injected document-created script captures the bridge into a closure, defines invokeNative over it, then nulls the global so page content cannot reach it — both strings are in FiveM.exe:

const webview = chrome.webview;
// … window.invokeNative = (native, arg) => webview.postMessage({ type: native, arg });
chrome.webview = null;

Which leaves the update page's own safePost() reading a global that is already null, so safePost({type:'ready'}) dies silently inside the && chain and native never hears that the UI is ready.

invokeNative still holds the real bridge. Calling what the Retry button calls:

window.invokeNative('retryUpdates')

started the update run instantly:

DOWNLOADING UPDATE FOR LIBCEF...          257.5 MB
DOWNLOADING UPDATE FOR GAMECACHE_GEN9...  3.67 GB   → VALIDATING → ok
DOWNLOADING UPDATE FOR FIVEM_GEN9...      139.1 MB
→ cfx-ui/index.html — the real UI, live server browser

…ending at the account-link prompt, which is the first thing the working client asks:

The Link your account prompt over the loaded FiveM UI

bin/kick-updater.py does this over CDP and bin/launch.sh calls it on every start. It's a no-op if the launcher is already moving. Subsequent launches go straight to the server browser, since the downloads are cached.

Always launch through bin/launch.sh. Starting FiveM.exe directly reproduces the hang.

Getting DevTools onto the launcher

WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS is ignored, because FiveM supplies its own AdditionalBrowserArguments and the app's value wins. The Edge policy key is appended instead and does work:

HKCU\Software\Policies\Microsoft\Edge\WebView2\AdditionalBrowserArguments
  "FiveM.exe" = "--remote-debugging-port=9222 --remote-allow-origins=*"

That's bin/enable-devtools.sh. It does not apply to fixed-version runtimes, so keep Evergreen selected while debugging. bin/cdp.py is a dependency-free CDP client if you want to poke at the page yourself.

Chapter 6 — The invisible cursor (unsolved)

Commonly written off as an unavoidable Wine + embedded-Chromium quirk. The system state here is genuinely broken in a way worth fixing regardless:

  • /usr/share/icons/default/ exists but is an empty directory — no index.theme, no cursors/
  • ~/.config/kcminputrc is 0 bytes
  • XCURSOR_THEME is unset

libXcursor falls back to the default theme, finds nothing, and Wine under XWayland has no cursor images to draw. KDE's own windows are unaffected because the compositor draws their cursor. So:

mkdir -p ~/.icons/default
printf '[Icon Theme]\nInherits=breeze_cursors\n' > ~/.icons/default/index.theme

None of this has actually restored the cursor in the client yet. Setting XCURSOR_THEME=breeze_cursors + XCURSOR_SIZE=24 for the process made no difference, and the usual fallback — running inside a Wine virtual desktop, where Wine draws its own software cursor — is unusable, because wine explorer /desktop=… dies immediately under XWayland:

X Error of failed request:  BadWindow (invalid Window parameter)
  Major opcode of failed request:  10 (X_UnmapWindow)

Worth narrowing before going further — is the cursor missing in a plain Win32 Wine app too, or only in the CEF window?

WINEPREFIX=<prefix> wine winemine

Chapter 7 — Connecting, cfx://, and why account linking is a red herring

Reaching the server browser is not the same as joining a server. Clicking Connect on the official FiveM Test Server does nothing at all — no game process, no error, no log line.

The UI's connect path is:

async connectTo(t) {
  if (this.currentConnectNonce) { console.warn(`Already connecting somewhere`); return }
  await this.legalService.clearance(),
  this.server = await this.resolveServer(t),
  …
  mpMenu.invokeNative(`connectTo`, JSON.stringify([this.pickServerConnectEndPoint(this.server, …), this.currentConnectNonce]))
}

Hooking invokeNative and then clicking Connect records zero calls, so it dies inside resolveServer() — the Cfx server-list lookup — before the native is ever reached. Things that turned out not to be the cause:

  • Terms of Service. clearance() waits on a deferred promise unless localStorage.legalAcceptanceData holds a valid tos.acceptedAtTimestamp matching the current version hash. Once accepted it resolves immediately, and it was already accepted here.
  • Account linking. The visible 401 is lambda.fivem.net/api/identities, i.e. the optional linked identities feature (Discord/Steam links for allow-lists and upvoting). Not the join path.
  • Calling the native directly with a correctly shaped payload (JSON.stringify(['cfx.re/join/<id>', nonce])) and via executeCommand('connect …') both do nothing — no log, no error, no process. connectTo is a known message type (unknown ones log Received unknown message type), so native accepts it and silently drops it.

The cfx:// scheme is missing on the Linux side

Signing in ends with the desktop asking which application should open the callback and finding none. The scheme is cfx://, and the client registers it inside the prefix:

[Software\Classes\cfx\shell\open\command]
@="\"C:\FiveM Enhanced\FiveM.exe\" \"%1\""

Nothing registers it with the desktop, so the browser's redirect has nowhere to go. bin/cfx-url-handler.sh bridges it:

cat > ~/.local/share/applications/fivem-cfx-handler.desktop <<EOF
[Desktop Entry]
Type=Application
Name=FiveM for GTAV Enhanced (cfx:// handler)
Exec=$PWD/bin/cfx-url-handler.sh %u
Terminal=false
NoDisplay=true
MimeType=x-scheme-handler/cfx;
EOF
update-desktop-database ~/.local/share/applications
xdg-mime default fivem-cfx-handler.desktop x-scheme-handler/cfx

Verified end to end — xdg-open "cfx://…" now reaches FiveM.exe. It still does not complete the round trip, because handing a URL to the client fails:

[error] Failed to check properly if pipe exists. Error code: 50.

Error 50 is ERROR_NOT_SUPPORTED: Wine does not support enumerating \\.\pipe\, which is how the client looks for an already-running instance to hand the URL to. A cold start with the URL launches the client but ignores it. The CLI11 parser also rejects some shapes outright — cfx://auth/... errors while cfx://connect/... does not — and the only options the binary defines are --retry, --inject, --updater and --wd.

Next thing worth trying: a local server on 127.0.0.1:30120. That skips the Cfx server-list lookup in resolveServer() entirely, which is where the public path dies.

Chapter 8 — The server side needs none of this

FXServer has an official native Linux build, so server development doesn't involve Wine at all:

curl -O https://runtime.fivem.net/artifacts/fivem/build_proot_linux/master/<build>/fx.tar.xz
tar xf fx.tar.xz
git clone https://github.com/citizenfx/cfx-server-data.git server-data
cd server-data && ../server/run.sh +exec server.cfg

A license key is required even for a purely local server — sv_lan 1 does not bypass it on current builds, contrary to a lot of advice. Free keys come from portal.cfx.re.


Usage

git clone https://github.com/akacross/fivem-enhanced-linux
cd fivem-enhanced-linux
./install.sh          # prefix, WebView2, DXVK, depot fetch, registry, config
bin/launch.sh         # start the client (and kick the updater)

install.sh is idempotent. Defaults live in bin/env.sh — FIVEM_ROOT, WINE, and GTA_ENHANCED_DIR are all overridable.

script what it does
bin/launch.sh start the client, then kick the updater
bin/kick-updater.py the invokeNative('retryUpdates') workaround
bin/fetch-launcher.sh / bin/fetch-product.sh SHA256-verified depot fetch
bin/register.sh registry + config.toml the installer would write
bin/use-webview2.sh switch Evergreen ⇄ a fixed-version runtime
bin/enable-devtools.sh WebView2 remote debugging on 127.0.0.1:9222
bin/debug-ui.sh / bin/cdp.py inspect the launcher page
bin/cfx-url-handler.sh desktop-side cfx:// handler (auth callbacks, join links)
bin/checkpoint.sh snapshot/restore prefix registry
repro/wv2repro.cpp minimal WebView2 host used to disprove the Wine-bug theory

Handy while debugging — screenshot just the launcher, since the pointer and window stacking make full-screen grabs awkward:

import -window "FiveM for GTAV Enhanced" out.png

Findings, condensed

# Finding
1 Offline WebView2 installer (fwlink 2124701) installs cleanly; the bootstrapper half-installs
2 Register WebView2 in the 64-bit view too, with no wineserver running
3 Black launcher window = builtin wined3d, fixed by DXVK + VKD3D-Proton
4 FiveM_Installer.exe throws bad array new length after GL init on every Wine tested
5 The Cfx depot serves public per-file manifests with SHA256 — no installer needed
6 modify.exe is the installer copied under another name
7 "Launcher is not installed." is a registry check, not a file check
8 FiveM nulls chrome.webview after capturing it, so the page's ready post is a silent no-op
9 invokeNative('retryUpdates') starts the stalled update run
10 Wine's WebView2 bridge is fine — proven with a minimal host, after I wrongly blamed it
11 WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS is ignored; use the Edge policy key
12 Empty /usr/share/icons/default starves libXcursor — real, but not enough to restore the cursor
13 sv_lan 1 does not waive the FXServer license key
14 Connect dies inside resolveServer() — invokeNative is never reached
15 The cfx:// scheme is registered in the prefix but not on the desktop
16 Handing a URL to the client fails: Wine can't enumerate \\.\pipe\ (error 50)
17 The 401 on /api/identities is optional account linking, not the join path

Status

  • ✅ Launcher runs, updates, reaches the live server browser
  • ✅ FXServer runs natively on Linux
  • ✅ cfx:// URLs now reach the client from the desktop
  • ❌ Connecting to a public server — fails in resolveServer(), before any native call
  • ❌ Handing a URL to a running client — blocked by Wine's \\.\pipe\ enumeration gap
  • ❓ Connecting to a local server — untried, and the most promising route
  • ❌ Anti-cheat-protected multiplayer — not attempted, and not something this repo tries to work around

Licence

MIT. Nothing here redistributes Rockstar or Cfx.re material; the scripts download from official endpoints and verify hashes.

About

FiveM for GTA V Enhanced on Linux under native Wine: findings, install scripts, and a minimal WebView2 repro

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages