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.
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.
| 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.
- Install WebView2 from the offline standalone installer, not the bootstrapper.
- Install DXVK + VKD3D-Proton. Wine's builtin
wined3dis what renders the launcher as a black window here — not WebView2. - Skip
FiveM_Installer.exe. It crashes under Wine on every runner tested. The Cfx depot serves public, hash-verified manifests; fetch from those instead. - Write the registry entries and
config.tomlthe installer would have written. - Kick the updater. The launcher's "ready" handshake is a no-op, so it hangs at
INITIALIZING UPDATER… 0%forever.invokeNative('retryUpdates')starts it. - 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. - The invisible cursor traces to a missing Xcursor fallback theme rather than a Wine bug, though see Chapter 6 — still unsolved here.
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 /installExit 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.
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.
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.
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.
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 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:
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. StartingFiveM.exedirectly reproduces the hang.
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.
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 — noindex.theme, nocursors/~/.config/kcminputrcis 0 bytesXCURSOR_THEMEis 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.themeNone 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 winemineReaching 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 unlesslocalStorage.legalAcceptanceDataholds a validtos.acceptedAtTimestampmatching the current version hash. Once accepted it resolves immediately, and it was already accepted here. - Account linking. The visible
401islambda.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 viaexecuteCommand('connect …')both do nothing — no log, no error, no process.connectTois a known message type (unknown ones logReceived unknown message type), so native accepts it and silently drops it.
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/cfxVerified 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 inresolveServer()entirely, which is where the public path dies.
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.cfgA 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.
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| # | 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 |
- ✅ 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
MIT. Nothing here redistributes Rockstar or Cfx.re material; the scripts download from official endpoints and verify hashes.


