Client internals
This page covers what the shipped Dauntless executable can and cannot do by itself, and the few ways we found to see inside it. Most of it comes from disassembling the 2.1.1 client. Where it matters for how we run the game today, we checked the same thing in 1.4.4. Every section says which build it is about.
Contents
The two builds
| 2.1.1 “Awakening” | 1.4.4 | |
|---|---|---|
| Released | December 2024, the final release | October 2020 |
| Engine | Unreal Engine 5 | Unreal Engine 4.25.3, changelist 239827 |
| Content | IoStore (.utoc/.ucas). Most .pak files are small stubs. Config sits in an Oodle-compressed pak. | pak v9 with an unencrypted index, zlib |
Dauntless-Win64-Shipping.exe | 151,448,856 bytes | 103,673,520 bytes |
| Online identity | Epic Online Services (EOS SDK 1.16) plus the Phoenix backend | Phoenix backend plus Epic’s older MCP subsystem. No EOS. |
| Our use | Research only | What we run |
Addresses. For 2.1.1 we give virtual addresses, with image base 0x140000000. For 1.4.4 we give RVAs, as Undaunted’s source does. Add 0x140000000 to an RVA to get the VA.
A client-only build
Both executables are Unreal TargetType.Client builds, compiled with WITH_SERVER_CODE=0. Unreal compiles the server entry points out of that kind of build. The evidence:
| Evidence | 2.1.1 | 1.4.4 |
|---|---|---|
Body of UWorld::Listen | 0x140bfde50: xor al, al / ret | RVA 0x789370: xor al, al / ret |
Its call site in UEngine::LoadMap | 0x14537140e | RVA 0x372E746 |
"Failed to listen: %s" (inside #if WITH_SERVER_CODE) | absent | absent |
"LoadMap: failed to Listen(%s)" (outside it) | present | present |
UWorld::GetNetMode | 0x1453e4ef0: returns NM_Client (3) whenever a net driver exists | InternalGetNetMode, RVA 0x378BDA0: the same |
"WindowsClient" / "WindowsServer" in the exe | 1 / 0 | 1 / 0 |
GIsClient forced to 1 in FEngineLoop::PreInit | write at 0x140c0f683 | writes at RVA 0x79A67A and 0x79A81B |
What each row means:
UWorld::Listenis a stub. In stock Unreal this function creates the game net driver and starts listening, all inside#if WITH_SERVER_CODE. Here the whole body is gone. The linker folded what was left into a shared two-instruction “return false” function.LoadMapcalls it whenever the URL has?listen, sees false, and gives up.GetNetModealways answers “client”. The Unreal source isIsRunningClientOnly() ? NM_Client : NetDriver->GetNetMode(). In both builds the compiler kept only theNM_Clientside. That only happens when “client only” is a compile-time constant.- The platform name.
FPlatformProperties::PlatformName()returnsWindowsServer,WindowsEditor,WindowsClientorWindows, chosen by template argument when the engine is compiled. Each exe contains"WindowsClient"once and never contains"WindowsServer". In 2.1.1 that one string has 21 code references, which isPlatformName()inlined at each call site. The same name is why the user config folder is...\Saved\Config\WindowsClient. GIsClientis forced on. In 2.1.1 the normal start-up path writesGIsClient = 1(0x140c0f683) andGIsServer = 0(0x140c0f68a). The other writes toGIsClientare on the commandlet path (-run=), which this build rejects with"Tried to run commandlet in non-editor build". Our reviews disagreed on whether that path could leaveGIsClientat 0. It makes no difference to hosting: withGIsClientat 0,LoadMapcalls theListenstub on every map load and fails.
What that means for the stock client
With no injected code:
-serverdoes nothing. The exe starts a normal windowed client (2.1.1, tested).- Adding
?listento the boot URL makesBrowse()fail. The client then says the default map “could not be found” and exits. That dialog appears wheneverBrowse()fails, not only when a map is missing (2.1.1, tested). -UseStandaloneDedicatedServer,-GAMESERVER_STATUS_FILE=,-GAMESERVER_INSTRUCTION_FILE=and-PLAYFAB_GAMEMODE=have parsers in the exe, but none of them makes the process host.-UseStandaloneDedicatedServeronly changes the build version the client asks the matchmaker for (LOCAL_<computer name>), which was Phoenix’s local-server development mode (2.1.1, from disassembly). These switches may not even reach their parsers: see the command-line allow list below.GlobalDefaultServerGameModeis never read. The function that returns the default game mode has no dedicated-server branch (2.1.1, from disassembly).
What is still there
Only the entry points were removed. The layer beneath them is intact:
UIpNetDriver::InitListenis real, working code (2.1.10x140c6d200, 1.4.4 RVA0x806F80). It lives in a plugin module thatWITH_SERVER_CODEdoes not touch. In 2.1.1 its only remaining caller is the online beacon host, which is not a game server.- The server half of
UWorld::NotifyControlMessageis compiled in, with a full jump table over theNMT_*control messages (2.1.10x1453e5d40). So are"PreLogin failure: %s","Join succeeded: %s"andAArchonGameMode::PostLogin. - The client’s outbound connect path is complete.
At first we read this as “compiled in but unreachable, so hosting is impossible”. That was wrong. The stock code path never reaches these functions, but injected code can. How multiplayer works explains how Undaunted does it on 1.4.4.
Starting the client: LauncherCheck and -EpicPortal
- Both builds include Unreal’s
LauncherCheckmodule. In 2.1.1 it reads no configuration, only the command line. Its test (WasRanFromLauncher,0x14543f100) passes if-EpicPortal,-NoEpicPortalor-qis present. The other two are not on the command-line allow list below, so if that list works as disassembled, only-EpicPortalcan pass the check. That fits what we saw: launched with plain-NoEpicPortalinstead, 2.1.1 still exited atLauncherCheckwithin six log lines. Without-EpicPortal, 2.1.1 exits almost at once. We always pass-EpicPortalto 1.4.4 as well and have not tested it without the switch. - 2.1.1 also has EOS’s own
bShouldEnforceBeingLaunchedByEGSsetting, which relaunches the game through the Epic store. It is alreadyFalsein the shipped config, andFOnlineSubsystemEOS::Initonly consults it when-EpicPortalis absent. It is not whatLauncherChecktests. - Start
Dauntless-Win64-Shipping.exedirectly.start_protected_game.exeis the EasyAntiCheat bootstrapper. When you start the shipping exe directly, EasyAntiCheat never runs. On 2.1.1 the install then stays unmodified and signature-valid. On 1.4.4, Undaunted adds two DLLs toArchon\Binaries\Win64but leaves the exe itself byte-identical (its launcher checks the hash). - On 1.4.4, Undaunted’s launcher passes
-EpicPortaltogether with placeholder values for the other Epic switches. The game runs with the placeholders, so nothing on this path checks them:
Dauntless-Win64-Shipping.exe <metagame host:port> -AUTH_PASSWORD=<account key> -AUTH_LOGIN=unused
-AUTH_TYPE=exchangecode -epicapp=<any> -epicenv=Prod -EpicPortal -epicusername=<any>
-epicuserid=<any> -epiclocale=en-US -epicsandboxid=<any> -epicdeploymentid=<any>
The command-line allow list (2.1.1)
Disassembly shows that 2.1.1 passes its command line through Unreal’s command-line allow list. FCommandLine::Set (0x142f582e0) always calls the filter (0x142f42c10), which deletes every switch that does not start with one of these entries (UTF-16 string at 0x146f22170):
-fullscreen /windowed -noautosettings -AUTH_LOGIN= -AUTH_PASSWORD= -AUTH_TYPE= -epicapp= -epicenv=
-EpicPortal -epicusername= -epicuserid= -epiclocale= -networkversionoverride= -environment=
The same string is in the 1.4.4 exe. We have not confirmed the filter directly at run time on either build. One 2.1.1 observation fits it: plain -NoEpicPortal, which LauncherCheck would accept, did not get the client past LauncherCheck. If the filter works as disassembled, switches such as -server, -nothreadtimeout, -noheartbeatthread, -UseStandaloneDedicatedServer, -NoEpicPortal, -q and -EngineINI= are removed before any code reads them. So treat any 2.1.1 finding that depends on such a switch as unverified, and prefer ini settings where one exists. Undaunted’s server DLL on 1.4.4 reads its own arguments with the Win32 GetCommandLineW and gives the engine a fixed command line through a hook on FCommandLine::Get (see How multiplayer works).
Login switches per build
2.1.1: Epic Online Services
The command-line login passes two gates:
- The game’s own gate (
0x142924b3f) reads only-AUTH_PASSWORD=and-AUTH_TYPE=. If either is empty, it fails at once with"Failed to login to Epic with no given Auth info"and shows the login-failed dialog. EOS is never contacted. - EOS auto-login (
FUserManagerEOS::AutoLogin,0x141c89680) then needs all three switches to be non-empty, including-AUTH_LOGIN=. If one is missing it logs, for example,"AutoLogin missing AUTH_LOGIN=<login id>.".
Pass all three. With only some of them, we saw the client quietly reuse an expired cached token and fail. -AUTH_TYPE= accepts exactly five values, compared in FUserManagerEOS::Login:
-AUTH_TYPE= | EOS credential type | Uses |
|---|---|---|
password | 0 | AUTH_LOGIN as id, AUTH_PASSWORD as token |
exchangecode | 1 | AUTH_PASSWORD as the exchange code |
persistentauth | 2 | nothing. Reached Epic and returned EOS_InvalidAuth for us. |
developer | 4 | AUTH_LOGIN and AUTH_PASSWORD |
accountportal | 6 | nothing. EOS opens Epic’s account portal in a browser. |
Any other value fails with "Unable to Login() user (%d) due to missing auth parameters".
- What works is
accountportal. The player signs in to Epic in a browser. The login and password values are ignored but must not be empty:-AUTH_TYPE=accountportal -AUTH_LOGIN=unused -AUTH_PASSWORD=unused. In September 2026 Epic’s EOS service still accepted this for Dauntless. - No anonymous device login. The exe looks up EOS SDK functions by name, so every API it uses appears as a string.
EOS_Connect_CreateDeviceIdis not among them. - Every failure shows the dialog, with one exception. The async failure handler (
0x142925e80) checks for the Epic error codeerrors.com.epicgames.account.no_account_found_for_external_auth. On that code it takes a different branch and shows no dialog. - After EOS, the client gets its Phoenix session with
PUT gamesession-prod.steelyard.ca/gamesession/epiceos, sending the EOS token as the bearer. It then uses the session token from the reply as the bearer for later calls. The email-and-password routePOST auth-prod.steelyard.ca/game/loginis only used when the credential type is not"epic".
1.4.4: no EOS at all
- 1.4.4 predates EOS. The exe has no reference to
EOSSDK-Win64-Shipping.dlland noOnlineSubsystemEOSstrings. The cooked config setsDefaultPlatformService=Phoenix. Epic account traffic goes through the olderOnlineSubsystemMcp, whose[OnlineSubsystemMcp.*]sections name hosts onepicgames.com. accountportalandpersistentauthdo not exist in this build. It knowsexchangecode,passwordanddeveloper.- We use Undaunted’s approach:
-AUTH_TYPE=exchangecode -AUTH_LOGIN=unused -AUTH_PASSWORD=<account key>. Undaunted’s client DLL points every MCP section’sDomainandProtocolat the metagame. The metagame’sPOST /account/api/oauth/tokentreats the exchange code as the player’s account key and returns a signed token. The client then sends that token as the bearer on every request. No Epic account is involved. Treat the account key like a password. -AUTH_TYPE=passwordshould send an email and password to Phoenix’s/game/login. We have not tested this, because we use the exchange-code path. Undaunted’s metagame has no/game/loginhandler, so this route would need backend work first.
No console
- 2.1.1:
ALLOW_CONSOLEis compiled out. We tried Tilde, and extra+ConsoleKeys=F8andF9lines in the userInput.ini. Nothing opens. The cookedDefaultInput.inistill containsConsoleKeysand 898ManualAutoCompleteListcheat entries. That is leftover data. Nothing reads it. - Neither build contains
-ExecCmds, so console commands cannot be passed on the command line either. - 1.4.4: Undaunted’s client DLL creates a
UConsoleobject itself and binds it to F2. That suggests the stock build creates none. We have not checked which commands it accepts.
Logging
File logging is stripped (2.1.1)
- The shipping build writes no log file.
-abslog=produces nothing. TheUE_LOGformat strings are still in the binary, which is how we find functions. - Standard output is the exception. In our early 2.1.1 runs we started the exe with
-logand redirected its standard output to a file, and it printed between one and four thousand log lines per boot there (we sawDisplay,WarningandErrorlines). We did not work out which switch or setting that depends on. [Core.Log]in the userEngine.iniis read (0x142f22e80).-LogCmds=is not parsed. It appears only in help text.- On 1.4.4 we have not checked the binary for a file sink. Undaunted’s DLL prints its own messages to a console window.
HTTPEventLog ships only Warning and above (2.1.1)
Both builds’ DefaultGame.ini has an [HTTPEventLog] section with bEnabled=True, EventLogEndPoint="https://telemetry.steelyard.ca/log" and Sample1InX=1 (no sampling). It looks like a way to get the log stream back. In 2.1.1 it is not:
FHttpEventLogis anFOutputDevice. It is constructed unconditionally (0x14265dd5d).bEnabledis never read.- Its
Serialize(0x142947830) starts withcmp r8b, 3/ja→ return. Any line more verbose than Warning is dropped before the payload is built. That cut-off is a hard-coded constant, not a setting. - The format is a Splunk HTTP Event Collector envelope:
host,source(WindowsClient),sourcetype(game-log),index(dauntless),time,event,game-idandseverity. It is sent with anAuthorization: Splunk <token>header. The token is hard-coded in the exe. We do not reproduce it. The expected reply is{"text":"Success","code":0}. - We pointed
telemetry.steelyard.caat our server and answered/log. The client never shipped a single line. We also never found where the device attaches itself to the global log. It may never be attached at all (unverified).
The practical upshot: raising log verbosity cannot help this channel, because it never ships a line below Warning. In principle the lever works the other way round. The cooked [Core.Log] pins LogHttp at Error in both builds, so HTTP warnings are currently discarded. Raising that category to Warning would let them through. We have not tested this.
The telemetry heartbeat as an instrument
With no log file and no console, the client’s own telemetry was our most reliable view of what it was doing.
- Heartbeat. The client POSTs a heartbeat to
TrackingEndpoint+/heartbeatabout once a second. The cooked endpoint in both builds ishttps://tracking-{environment}.steelyard.ca(2.1.1, observed live). The JSON body we recorded hasbuild,platform,state,region,server,session,map,pingandplaytime. In our standalone runsserverheld the local computer name, so treat recorded heartbeats as personal data.mapis the most useful field. It shows which map the client is actually on. That is how we confirmed a boot had really reached Ramsgate.state: we recordedmenuon the login map andcityin Ramsgate. The stringsmenu,city,islandandlobbysit right after the field names in the string table. We read these as the four possible values (likely, not proven).- On 1.4.4, Undaunted’s metagame (
POST /heartbeat) usesmapto record player activity.
- Analytics events (2.1.1) are batched to
telemetry-ingest-prod.steelyard.ca/event?id=prod, up to 100 events per request and up to 30 seconds late (MaximumSecondsBeforeTelemetrySent=30). Two are worth watching:playerdata_load_failed(UArchonLoadManager::LoadFailed,0x1429ad0f0). Itsloadersarray names the player-data loaders that never finished. That is the quickest way to find a missing backend endpoint.client_login_failednames the login step that failed, for exampleLoginToEpicProxy.
- Crash reports.
%LOCALAPPDATA%\Archon\Saved\Crashes\UECC-*\CrashContext.runtime-xmlholds<ErrorMessage>and<PCallStack>. Take the module base from the same report. The static address is then RVA +0x140000000(2.1.1).
The hang detector
When the game thread stops sending heartbeats, Unreal’s FThreadHeartBeat shows “Application Hang Detected” (“The application has hung and will now close.”) and exits. During investigation that kills the evidence.
In 2.1.1, FThreadHeartBeat::InitSettings (0x142ec08c0) reads four keys from [Core.System] in the engine ini: StuckDuration, HangDuration, PresentHangDuration and HangsAreFatal. They are read again at run time, so the user Engine.ini overrides them. The built-in HangDuration is 25 seconds, and the cooked DefaultEngine.ini raises it to 60 in both builds. HangsAreFatal defaults to True.
| Switch | Where | Effect (2.1.1) |
|---|---|---|
HangsAreFatal=False | [Core.System] in the user Engine.ini | No dialog and no exit (0x142ec4c8c jumps past both). The detector still logs "Hang detected on %s (thread hasn't sent a heartbeat for %.2f seconds):" at Error, with that thread’s call stack. This is the one we recommend. |
HangDuration=<seconds> | same section | The threshold. Leave it at 60 when HangsAreFatal=False. Raising it only delays the report. |
-nothreadtimeout | command line | Returns before any hang processing (0x142ec2f99). The process is not killed, but you also get no report. -debughangdetection cancels it. Not on the command-line allow list, so it may never reach this code (unverified). |
-noheartbeatthread | command line | Present in both exes. We passed it together with -nothreadtimeout in our 2.1.1 standalone launches. We did not disassemble its effect, and it is not on the allow list either (unverified). |
Two more notes:
- The hang dialog is a blocking modal. The process stays alive, with every thread intact, until someone dismisses it. That is the moment to take a full memory dump, for example with Task Manager → Create dump file.
- Not every “loading timeout” is a hang.
PlayerStartEventTimeout=120.0under[/Script/Archon.ArchonPlayerController]inGame.iniis a gameplay failsafe in both builds. It sends the player back to the menu with “Loading timeout while joining the server”.
Configuration: the user ini layer, and quoting URLs
- Unreal lays a writable user config over the cooked defaults:
%LOCALAPPDATA%\Archon\Saved\Config\WindowsClient\{Engine,Game,Input,...}.ini. This works in both builds. On 2.1.1, aGameDefaultMapoverride booted the client straight into Ramsgate. On 1.4.4, our game servers read their endpoint overrides fromGame.ini. - Put each key in the right file. The Phoenix endpoints (
[OnlineSubsystemPhoenix]) live inDefaultGame.ini, so overrides for them go inGame.ini. The same section inEngine.iniis ignored. - Correction. We once concluded that loose config files were ignored. That test was flawed in three ways. It put Game keys in
Engine.ini. It “changed” a setting that was already set. And it measuredLauncherCheck, which reads no config at all. Its-EngineINI=variant was probably also deleted by the command-line allow list before the engine saw it. - Quote every URL. On 2.1.1 we wrote 163 endpoint URLs, unquoted, into a user
Engine.ini. The game read the file and wrote it back with every value cut tohttps:. The engine’s ini parser drops everything from//onward in an unquoted value. The cooked config quotes all of its URLs, and so do we:
; in the user Game.ini
[OnlineSubsystemPhoenix]
; wrong: comes back as AuthEndpoint=https:
AuthEndpoint=https://auth-prod.steelyard.ca/game/login
; right
AuthEndpoint="http://127.0.0.1:61000/game/login"
- The game rewrites the user ini files itself. Back them up before you edit them. A bad
GameDefaultMapstops the game from booting at all.