Project
Architecture
How the launcher is put together, for contributors.
Principles#
- Core vs UI.
arctic-coreholds all logic and is UI-agnostic.arctic-apponly renders state and starts background jobs. A future CLI or another frontend can reuse the core. - Never block the UI thread. Network and disk work runs on
std::threads (seearctic-app/src/tasks.rs), which report back through anmpscchannel ofEvents and callrequest_repaint(). - Small and fast. The app uses blocking
ureq, eframe'sglowbackend instead ofwgpu, and one static executable. The only async runtime is insidearctic-share, and it starts the first time you use Play together. Launch checks use file sizes rather than re-hashing thousands of assets, and downloads are SHA-1 verified as they stream. - No secrets in git. Tokens live only in
accounts.jsonin the local data dir. The Azure client ID comes from env or local config.
Launch pipeline (arctic-core::launch)#
versions::load_versiondownloads<id>.jsonand verifies it against the manifest's SHA-1.java::ensure_runtimepicksjavaVersion.component(orjre-legacyfor old versions), installs it from Mojang's runtime index intoruntimes/<component>/, and records the manifest SHA-1 in a marker file so later launches do nothing.launch::filesresolves libraries by evaluating the rules (versions::rules), plus the client jar, the log4j config, legacy natives (extracted with zip-slip protection) and assets. Pre-1.7virtualand pre-1.6map_to_resourcesasset layouts are handled too.launch::argsbuilds the command line. It supports the modernargumentsobject with rules/features and the legacyminecraftArgumentsstring, substituting${placeholders}.launch::spawnstartsjavaw.exewith the instance's game dir as the working directory. Output goes tologs/game-<instance>.log, and the access token is redacted from logs.
Downloader (net::download_all)#
- All missing files of a launch (Java runtime, libraries, client jar, assets, log config) go into one queue, deduplicated by destination.
- 64 keep-alive connections. ureq's default pool keeps only 3 idle connections per host, which forced a TLS handshake on most requests, so the pool is sized to the worker count.
- Two-ended queue: 8 workers take the largest files and the rest take the smallest, so bandwidth-bound and latency-bound work overlap. Total time is roughly max(bytes / bandwidth, files / request rate).
- Java runtime files use Mojang's LZMA streams, decompressed while downloading. Both the compressed and the final SHA-1 are verified.
- Timeouts are per phase (connect, response, then a body limit scaled to file size), so large files on slow connections are not cut off. Each file is retried 3× before the launch fails.
- Progress (files and bytes) is reported at most every 50 ms. The UI turns it into speed and ETA.
Measured on a ~50 MB/s line for a fresh 26.3 install (663 MB, 5.6k files): 39 s → 18 s. Relaunching an installed version prepares in about 0.2 s.
Game process (launch::process)#
The game's stdout/stderr are piped through two threads that write the log file and watch for
the "window is up" line (Backend library: / LWJGL Version:). A third thread waits for the
exit and handles Force close. The UI only receives GameEvents and never blocks on the
process. After a successful prepare, versions/<id>/.installed marks the version as
installed for the version picker.
Logs#
launch::logparse turns game output into LogLines. Modern versions print log4j XML
events because of the official logging config; these are collapsed into
[thread/LEVEL] message lines, with stack traces kept. Parsing runs on the tee threads,
which stream GameEvent::Output to the UI (capped at 20k lines). The launcher's own
log output goes to stderr, logs/launcher.log and an in-memory ring buffer
(arctic-app/src/logbook.rs). Both are shown on the Logs tab.
UI notes#
- Title bar: on Windows 11 the native caption, border and text colors are set via DWM
(
titlebar.rs) to match the theme. The native bar keeps snapping, resizing and shadows. - Icons:
icon_raster.rsis a dependency-free rasterizer shared by the app (window icon) andbuild.rs, which generates the multi-size.icoand embeds it withwinresource. No binary assets are checked in. - Instance icons:
InstanceIcon { style: FlakeStyle, color }is stored per instance. Loaders get a default style (Loader::default_icon), and users can override both.
Accounts (arctic-core::auth)#
AccountStore holds Vec<Account> plus the active id. Each Account holds a
MicrosoftSession: MSA refresh token, Minecraft access token, expiry, xuid.
The Microsoft chain is MSA OAuth → Xbox Live → XSTS → login_with_xbox → profile, and it
has two entry points:
- Browser: authorization code + PKCE, using a loopback redirect on an ephemeral port
(
http://localhost:<port>, listening on both 127.0.0.1 and ::1). - Device code: shows a code, polls the token endpoint and honours
slow_down.
Tokens are refreshed before launch when they are within 5 minutes of expiry.
TODO: encrypt accounts.json at rest with Windows DPAPI.
Crates#
| Crate | What it is |
|---|---|
arctic-core | Everything that isn't UI: versions, Java, launching, loaders, mods, skins, accounts |
arctic-app | The egui launcher |
arctic-cli | The arctic command |
arctic-share | Play together: peer-to-peer LAN tunnels (iroh) |
arctic-cosmetics | The Arctic cosmetics server (capes, sign-in via Mojang's session server) |
mod/ | The Arctic Client (Java, Gradle): a shared core plus per-version adapters, bundled into the launcher as jars |
Instances and mod loaders (instances, loaders)#
instances/<id>/instance.json + instances/<id>/minecraft/ (game dir). Libraries, assets,
versions and runtimes live in shared/ and runtimes/, so instances stay small.
For a loader instance, launch::prepare installs vanilla first, then asks
loaders::install_profile for the loader's version JSON and merges it onto vanilla
(versions::merge: the loader's libraries win over vanilla ones with the same
group:artifact, arguments are appended).
- Fabric / Quilt: one request to their meta servers for the profile.
- NeoForge / Forge (1.13+): the official installer jar is downloaded (and cached), the
libraries bundled in it are extracted, and its client processors run with the instance's
Java. A marker in
meta/loaders/records the result, so later launches need no network. - Forge 1.7.10–1.12.2: the legacy installer format; the universal jar is extracted and referenced directly.
Mods (mods)#
Modrinth search and install. Installing resolves the whole dependency tree before
downloading anything, so a missing dependency changes nothing on disk. Installed mods are
tracked in instances/<id>/mods.json (project, version, title, icon); jars added by hand are
listed too. Disabling a mod renames it to *.jar.disabled.
Play together (arctic-share)#
The host's invite code is its iroh endpoint id. The host listens for Minecraft's "Open to LAN" multicast announcements (224.0.2.60:4445) to find the local world's port. A guest connects over QUIC (hole punching, relay fallback), opens a local TCP port, and announces it to games on the same PC with multicast TTL 0, so the world appears in the Multiplayer LAN list. Each Minecraft connection is one bidirectional QUIC stream. Only connections from the guest's own machine are accepted.
Skins (skins)#
A per-profile library in profiles/<id>/skins/ (library.json + PNGs). Skin changes use
the Minecraft services API with the account's access token. Legacy 64×32 skins are expanded
and cleaned the same way the game does. The 3D preview (arctic-app/src/ui/skins/model.rs)
builds a textured mesh per body part, back-face culls and depth-sorts the faces.
Arctic looks and the mod#
A look is a skin (plus arm model) and a cape that the player picks locally. The launcher
publishes it to the looks server (arctic-cosmetics), which stores textures by SHA-1 and
relays each player's look to every Arctic client. Nothing is owned or unlocked: presets are
just textures everyone may use, and custom images are allowed (size-checked: 64×64 skins,
2:1 capes up to 512×256).
The server only checks who publishes, so nobody can change someone else's look:
- Microsoft accounts: the client asks for a challenge, calls Mojang's
session/minecraft/joinwith it, and the server confirms withhasJoined, like a Minecraft server does. No password or token reaches Arctic. - Offline accounts: the first launcher to use a name claims it with a random key, kept
in the profile's
cosmetics.json; later changes need the same key. The server derives the UUID from the name exactly like the game, so a key can only claim an offline UUID.
Both give an HMAC-signed token that expires after a week. Before launching with the
client, the launcher writes config/arctic-session.json (server, token, and the menu style
picked in the launcher) so cape changes made in game publish as that player.
The Arctic Client (mod/)#
Vanilla instances run the Arctic Client on supported versions: Fabric plus the Arctic mod,
installed quietly (launch::effective_loader); Fabric and Quilt instances get the mod too.
The launcher embeds the built jars (mod/dist/) and copies the right one in before launch
(arctic_mod::sync).
The client is split so it can cover many Minecraft versions:
mod/coreholds everything that doesn't touch Minecraft, compiled for Java 8 so the same code runs down to 1.8.9: a small UI toolkit drawn from rectangles and text (ui,gfx), the menu styles and the widget skins (style), the animated Arctic backdrop, the title menu, the Arctic menu (Right Shift) and the HUD editor (menu), the HUD widgets (hud: unmoved widgets stack in a column, moved ones pin to the nearest screen edge), Zoom, Freelook and Fullbright (feature, with keys stored by Minecraft key name), looks lookups and cape changes over plain HTTP (looks), andconfig/arctic.json.mod/versions/fabricis a thin adapter, one source tree built for each target inmod/targets.json(version differences are//#if MC …blocks, mostly inCompat): aPlatform(game state, screens, textures, sign-in), aGfxover that version's GUI renderer, a screen that hosts core pages, and mixins that replace the title screen, draw the HUD, count clicks, open the menu, restyle vanilla buttons, sliders, fields and checkboxes, and apply Arctic skins and capes.
The Performance switch (mods::performance) adds well-known optimization mods to Vanilla
instances from Modrinth, tracked in arctic-performance.json beside the game folder so they
never mix with user mods. It rechecks for new builds weekly, keeps what's installed when
offline, and runs Fabric even on versions the client doesn't support yet.
Vanilla screens keep their own logic; only their widgets and backdrop are restyled, so adapters stay small. The Classic style turns restyling off and keeps the HUD.
Platforms#
Windows and Linux (x86-64) are supported from the same code. The differences are small:
javaw.exevsbin/javain managed runtimes;- runtime symlinks, which exist only on Linux;
- the classpath separator;
- the updater's per-platform release asset;
- the Windows-only title bar tint and exe icon.
Rule evaluation maps the host to Mojang's windows/linux/osx names.
Profiles (arctic-core::profiles)#
profiles.json lists profiles and the active one. DataDirs::with_profile(id) scopes the
per-profile paths (settings, accounts, instances, game logs) to profiles/<id>/. Shared
downloads (shared/, runtimes/, meta/, cache/) stay launcher-wide. On first start,
data from the pre-profiles layout is moved into profiles/default/. Removing a profile
moves its folder to profiles/.trash/. The app switches profiles by reloading all
profile-scoped state (session.rs), and the CLI takes --profile.
Updates (arctic-core::update)#
See releasing.md. Briefly: GitHub Releases API → pick the newest release
for the channel → verify the asset's GitHub sha256: digest → self_replace → restart.