How Sunrise is built
The parts, threads and startup order of the Sunrise DLL.
Sunrise is one DLL. The game loads it in place of its Steam API library. Inside the game process it is two things at once: a client mod that hooks the game, and a local server that answers the game’s network traffic. Nothing leaves the machine unless you point it at an external server.
How the game loads it
The build output is named steam_api64.dll (the TargetName in Sunrise.vcxproj). It sits in bin/x64 beside the game executable. The game loads it like the real Steam library and calls its exports.
Sunrise implements those exports itself in src/dllmain.cpp. The game calls three of them at the points that matter:
export
what Sunrise does there
SteamAPI_Init
starts Sunrise: settings, logging, state, server and client setup
SteamAPI_RunCallbacks
runs every frame: attaches hooks, delivers Steam callbacks, runs client work
SteamAPI_Shutdown
stops everything in reverse order
The other SteamAPI_* and SteamInternal_* exports answer the game’s Steam questions locally: user, language, callbacks, interfaces. The code is in src/steam/.
The DLL also exports three entry points for a host that loads it another way:
export
what it runs
SunriseInitialize
the egress guard, then core::initialize
SunriseActivateClient
the game image scan and the main hook install
SunriseShutdown
the same shutdown as SteamAPI_Shutdown
The layers
Sunrise has six layers. Each one is a top-level folder under src/. Third-party code is in vendor/: Detours for hooks, Dear ImGui for the menu, and Lua for mission scripts.
Everything in the graph runs in the game process. The game reaches Sunrise three ways: through the client’s hooks, through Steam API calls, and over a real TCP connection to the server’s BAP listener on loopback. The client hands its requests to the server through consumer.h.
layer
what it holds
src/core
the base every layer uses, and the startup order
src/steam
the local Steam API the game loads
src/client
everything that touches the game’s own code
src/middleware
the game’s wire and data formats, as encoders and decoders
src/state
what the server knows: the account, the activities, the game data
src/server
the local server that answers the game
Core
Core holds the parts with no game knowledge.
folder
what it owns
settings/
reads and checks settings.json, one part per layer
logging/
the log sinks, the line format and the in-memory log view
filesystem/
the Sunrise folder and the paths under it
threading/
lock helpers
json/
the JSON reader that settings.json, mod.json and mod_config.json are parsed with
ui/
the menu and HUD framework: layout, theme, fonts, overlays, notices, world markers; and the Core pages HUD, Logs and Mods
runtime/
the startup and shutdown order across all layers
core/runtime calls each layer’s start and stop in order. See Startup order.
Steam
The game loads Sunrise as its Steam library, so Sunrise must answer every Steam call itself.
folder
what it owns
interfaces/
the Steam interfaces the game asks for: apps, DLC, app ticket, friends, matchmaking, rich presence, user stats, networking
runtime/
the exports, the callback queue, and the per-frame pump
The per-frame pump is where the client layer attaches and the server thread starts.
Client
The client layer is the only layer that reads or changes the game’s code.
folder
what it owns
patterns/, targets/
find game functions by byte pattern and keep the resolved addresses
executable/, memory/, process/
read the game image’s sections, access game memory, and stop the other threads for a code patch
hooking/
the Detours wrapper: one transaction per hook group, and a detach that waits for running calls
hooks/
every hook; see Client hooks
network/
consumer.h, the request types the hooks hand to the server
content/
workers that read the installed packages and build caches and the activity SDK
diagnostics/
the game side of the Sunrise Studio link: camera, world, object, animation and CUI screen data
activity/
the mission launcher, which starts an activity through the game’s own entry points
movement/, player/
the saved values of the Movement and Player pages, and the player position
graphics/, input/
display setup under Wine, and the game window’s focus
ui/
the Movement, Player and Activity Launcher menu pages
runtime/
installs the hooks in groups and removes them at shutdown
A hook does not answer the game itself. It turns the game’s call into a request, and the server answers it. The rule for hooks that change game behavior is on Client hooks.
Middleware
Middleware is the game’s formats, written as code. It encodes and decodes. It does not decide what to send.
folder
what it owns
signon/
the SignOn request and response
bap/
BAP frames, the service catalog and every service body, including the activity messages
secure_channel/, crypto/
the encrypted BAP frame and the ciphers it needs; SHA-1 for the package header digests
web_service/
the ws RPC envelope and one codec per opcode
queuez/
the service 123 update body
datagen/
builds the game’s account, character and roster records from Sunrise’s state
protobuf/, encoding/, compression/
protobuf, the bit-packed codec, and Oodle
content/
the package reader and its mod overlay, named tags, the package tables, the package merge and config bake writers, and the CUI checks for mod config bindings
gameplay/
the peer transport: association, DTLS records, NAT, peer and group messages
runtime/
loads the package name catalog on the first boot
The formats are described in the Rainbow Six Seige section.
State
State is what the server knows about the world. It holds facts, not protocol.
folder
what it owns
account/, investment/
the account, its characters and items, and the SQLite store
equipment/, progression/, unlocks/, entitlements/
equipped gear and light, progress, unlock flags, owned content
activity/
activity sessions, bubbles, membership, entity slots and destinations
activity_sdk/
the generated activity SDK that mission scripts read
build_data/
game data read from the installed packages and mod packages: items, activities, vendors, records and more
content_manifest/
the scan of the installed packages, its cache, and the rows mods add
content/
the name-to-definition catalog read from the installed content
mods/
the mod loader: discovery, manifests, load order, package checks, merges, mod config
matchmaking/, gameplay/, steam/
matchmaking config, replicated entity state, local Steam data such as rich presence and achievements
runtime/
starts the state, and holds the per-run values such as session tokens
How the account is stored is on Accounts, characters and saves.
Server
The server answers the game. It reads requests, asks state, and uses middleware to write answers.
folder
what it owns
http/
the SignOn answer
transport/
the BAP listener: a nonblocking TCP socket on loopback, 8 slots
bap/
one session per connection, the plaintext and encrypted routes, and every push
web_service/
the ws RPC handlers: equip, dismantle, vendors and the rest
activity/
the Activity Host and the mission script runtime
gameplay/
the gameplay UDP endpoint and its peer protocol
runtime/
the server thread and its service slice
studio/
the command side of the Sunrise Studio link: mod script reloads
ui/
the Activity Host menu page and its World and Packets windows
See The Activity Host and Mission scripting.
How the layers depend on each other
The main direction is one way: the server uses middleware and state, and everyone uses core.
layer
uses
core
the other layers only in three places: core/runtime starts and stops them, settings/ parses into state types, and the HUD overlays and the Mods page read their status
steam
core; it starts the server thread and the client activation
client
core, middleware and state; the server only as noted below
middleware
core and state
state
core, and a few middleware helpers
server
core, middleware, state, and the client’s request types
Three links cross the main direction:
- Middleware and state use each other.
datagenreads the account from state to build the game’s records. State uses a few middleware parts, such as the loadout resolver and SHA-256. The mod loader in state hands the mod package files to the middleware package reader. - The client reaches the server in two places. It registers an investment consumer with the BAP server at start, and one world-marker renderer reads the current activity link.
- The server uses client UI code. The Activity Host windows draw world markers with the client’s marker code.
Everything else between client and server goes through the request types in src/client/network/consumer.h.
The files Sunrise writes
Sunrise keeps its files in one folder, Sunrise, beside the DLL. For a normal install that is bin/x64/Sunrise/. The path code is in src/core/filesystem/path.cpp.
path
what it holds
settings.json
the settings; see Settings, logs and in-game tools
logs/sunrise.log
the log, when the file log is on
cache/
caches built from the installed packages and mod packages
cache/mods/
merged and configured mod package files
data/investment.sqlite3
the persistent SQLite database
sdk/
the generated activity SDK, with sdk/catalog.bin and sdk/lua/
scripts/
your mission scripts
mods/
installed mods, one folder each; Sunrise creates the folder
mod_config.json
the mod config values changed on the Mods page
exports/package_keys.bin
the package keys for Sunrise Studio, when client.dump_package_keys is on
movement.json, player.json, hud.json, world_markers.json
values saved by the menu pages; see Values the menu saves
Startup order
Startup is split in two. SteamAPI_Init builds all process state, but installs no game hooks. The first calls to SteamAPI_RunCallbacks then attach to the game.
This is the order in src/steam/runtime/steam_lifecycle.cpp, src/core/runtime/core_runtime.cpp, src/server/runtime/server_runtime.cpp and src/steam/runtime/callbacks/callback_dispatch.cpp, with logging and error paths left out.
/* SteamAPI_Init -> steam::initialize */
if (running_under_wine) initialize_wine_display();
egress::install(); /* socket and name-lookup redirect, before anything connects */
core::initialize(module): /* each step must succeed, or every earlier one is undone */
settings::initialize(); /* reads Sunrise/settings.json */
log::initialize(); /* opens the sinks */
ui::runtime::initialize(); /* menu visibility and toggle key */
hud::initialize(); logs::initialize(); mods_page::initialize(); /* the three Core pages */
initialize_content_manifest(): /* outside the game process only the SDK step runs */
content_manifest::initialize(); /* reads the installed packages */
mods::initialize(); /* loads and serves the mods; a failure only logs */
build_data::bind_content(); /* its cache keys on the content, mods included */
initialize_activity_sdk(); /* keys on the installed packages only */
investment::store::initialize(); /* opens or creates the save database, then checks it */
initialize_cheat_session(); /* in-memory save copy for a cheat mod or client.cheats */
state::initialize(); /* account and build data */
middleware::initialize(); /* package name catalog, first boot only */
server::initialize():
activity::mission::initialize(); /* mission script runtime */
register_http_consumer(http::consume);
register_bap_consumer(bap::consume);
transport::initialize(); /* BAP listener on 127.0.0.1:bap_port; a failure only warns */
gameplay::initialize(); /* gameplay UDP endpoint; a failure only warns */
server_ui::initialize(); /* Activity Host page */
client::initialize(); /* SDK worker setup, saved page values, client pages */
package_trust::install(); /* must attach before the base packages register */
bootflow_texture_override::install();
/* SteamAPI_RunCallbacks -> run_slice, every frame */
activate_graphics_once(); /* presentation, cursor and input hooks */
if (main_activation_pending()) { show_busy_overlay(); return; } /* one frame to draw it */
main_ok = activate_main_once(); /* scan the game image, install the network and feature hooks */
deliver_queued_steam_callbacks();
if (main_ok) {
apply_feature_flags_once();
server::start_service(); /* from here the server has its own thread */
service_client_workers(); /* investment, SDK generation, scriptables */
}
If a step of core::initialize fails, the log gets ev=initialize stage=<step> result=fail. A failed mod load is the exception: it logs ev=initialize stage=mods result=fail, and the game starts with no mods. If the image scan fails, the screen shows “Sunrise could not attach to the game. The boot will not finish.”
Threads
The server never runs on the game’s frame. The game can block on a loopback send until the server answers, so the game must not be the server’s clock.
thread
what runs on it
the game thread that calls SteamAPI_RunCallbacks
hook activation, Steam callback delivery, and the client content workers’ service calls
the server thread
one service slice every 10 ms: BAP transport, Activity Host, mission scripts, gameplay endpoint
the game thread that sends an HTTP request
the SignOn answer, built in place by server::http::consume
The server owns its thread (src/server/runtime/server_runtime.cpp). The frame pump calls server::start_service on each frame after the image scan succeeds. The first call starts the thread. If that fails, the next frame tries again. Server shutdown signals the thread and waits for the running slice to end. The loop is:
while (WaitForSingleObject(stop_event, 10) == WAIT_TIMEOUT) {
server::service(GetTickCount64());
}
void server::service(uint64_t now) {
transport::service(now); /* accept, read, route frames, write answers */
activity::host::service(now);
studio::service(now); /* the Studio command link; timed with the mission stage */
activity::mission::service(now); /* mission scripts and script reloads */
gameplay::service(now);
}
Each stage is timed. A stage over 40 ms logs ev=core stage=service result=slow step=<stage>. Every 2 seconds a debug line reports the worst times of the window.
SteamAPI_RunCallbacks wraps its work in a structured exception handler. A fault inside it is logged on the next call as ev=core stage=pump result=fault code=0x..., so one fault does not stop every later frame.
How a request reaches the server
The game expects remote servers. Sunrise catches that traffic at two points and answers it in the same process.
Sockets and name lookups
The egress hooks in src/client/hooks/egress/ replace the Winsock connect and send calls and the name resolvers. Every IPv4 destination keeps its port but gets its address replaced with the redirect target. The target is 127.0.0.1, or client.external_server.host when the external server is on. A connect the policy cannot redirect fails with WSAEACCES. The game’s Demonware discovery query is answered by the hooks themselves. The guard attaches before Core starts and never detaches.
HTTP: SignOn
The game sends HTTP through one shared executor. Sunrise replaces it (src/client/hooks/network/http/). For each request:
if (external_server.enabled) {
rewrite_url_host(descriptor); /* send it to the external server */
return original_executor(descriptor);
}
if (descriptor.operation != POST) return refuse_unmapped(url);
HttpRequest request = { url, content_type, body, response_buffer };
if (!http_consumer(&request, &response)) return refuse_unmapped(url);
publish_completion(response.size, response.status);
return SKIP_NATIVE_OPERATION; /* the game's own transport never runs */
The server’s HTTP consumer (src/server/http/server_http.cpp) answers one route: a URL that contains /SignOn. The hook refuses any other URL and logs ev=http stage=route result=unmapped. The SignOn answer carries the BAP port from server.bap_port.
BAP
The game then opens a TCP connection for BAP. The egress hook sends it to 127.0.0.1, where the server’s listener (src/server/transport/bap_listener.cpp) waits on the BAP port.
On each server slice the listener:
- Accepts a waiting connection into a free slot. There are 8 slots.
- Reads what the socket has.
- Splits the stream into frames and passes each one to
bap::consume. - Writes the answer back.
- On a poll tick, asks the session whether it owes a push.
bap::consume (src/server/bap/bap_route.cpp) takes four events: open, frame, poll and close. A frame is parsed, then routed to the plaintext or the encrypted handler of that connection’s session. Its scratch buffer is wiped after every call.
struct BapRequest {
BapEvent event; /* open, frame, poll or close */
uint32_t connectionId; /* one per listener slot */
span<const byte> frame; /* inbound bytes, for a frame */
span<byte> response; /* caller-owned output buffer */
};
The listener calls bap::consume itself; no hook is involved. The type sits in src/client/network/consumer.h beside the HTTP request types, which the HTTP hook uses to reach the server. The two other client-to-server links are listed in How the layers depend on each other.
Gameplay traffic
In-activity gameplay uses UDP. With server.gameplay.topology set to embedded, the server binds its own UDP endpoint (src/server/gameplay/endpoint/). The port settings are on Settings, logs and in-game tools.
The Sunrise Studio link
Sunrise Studio talks to a running game through named shared memory, never a socket. Studio creates each mapping. Sunrise only opens one that already exists, at most once a second. It drops the mapping when Studio’s heartbeat goes stale. The link is off unless core.studio_attach is true.
part
code
runs on
does
camera, world, object and animation links
src/client/diagnostics/studio/
the game’s camera callback
publishes snapshots; reads only
CUI link
src/client/diagnostics/studio/
the game’s UI frame, after the game’s own UI update
copies the open CUI screens and one widget tree; reads only
command link
src/server/studio/
the server thread, right before the mission scripts run
takes a reload_mod request and answers it once
A reload_mod request names one mod. The mod must have loaded this session. The command link hands it to the mission runtime, which reloads the scripts of that mod and of the mods that depend on it. The answer comes after every affected mission reaches a safe point. The Reload script button on the Script page of the World window uses the same path for every running script. The reload rules are in Debugging a script.
The mapping names, layouts and timings are on The live link.
