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) |
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, 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:
The client asks for a challenge, calls Mojang's session/minecraft/join with it, and the
server confirms with hasJoined, like a Minecraft server does. No password or token reaches
Arctic. That gives 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 mod/dist/arctic-client.pack: every target's jar, with each file
stored once and LZMA-compressed together (about 1 MB for all versions, against about 24 MB
of jars; most files are the same in every version). Before launch it rebuilds the jar the
game needs from it and copies it in (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 Vanilla 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.
Bedrock Edition (arctic-core::bedrock)#
Windows only. Since 1.21.120 the game is a GDK package (Microsoft.MinecraftUWP for the
release, Microsoft.MinecraftWindowsBeta for Preview) installed in C:\XboxGames; its data
is in %APPDATA%\Minecraft Bedrock\Users\: Shared\games\com.mojang for packs and one
folder per Xbox account for worlds, settings and the server list. An old UWP install's
LocalState\games\com.mojang is read too.
-
Finding and starting it goes through Windows'
PackageManager(WinRT): the package's version and folder, and its app list entry to launch (theminecraft:link is the fallback). "Running" is aMinecraft.Windows.exefrom the package's folder. -
Versions come from community archives of Microsoft's package links (LiteLDev's
minecraft-windows-gdk-version-db, then MinecraftBedrockArchiver'sGdkLinks), kept incache/bedrock-versions.jsonfor offline use; only*.xboxlive.com/.cn.msixvclinks are accepted. Installing hands the link toPackageManager.AddPackageByUriAsyncwithForceUpdateFromAnyVersion: Windows downloads the encrypted package and unlocks it with the Store license, so nothing from the archives but a link to Microsoft is trusted. Package version1.26.5203.0is game version 1.26.52.3. Builds before 1.21.120 are UWP.appxpackages known only by a Windows Update id (the raythnetwork list, merged into the same catalog; Beta is skipped, it needs an account).bedrock::fe3asks Microsoft's update service for the link, the package is downloaded intocache/bedrock-packages(the newest three are kept) and installed from the file; switching between the two kinds removes the installed package first with its data kept. A GDK install is told apart by itsMicrosoftGame.Config. -
Worlds (
minecraftWorlds\<id>, name inlevelname.txt) import from and export to.mcworld(the folder's contents zipped). Packs are read from theirmanifest.json(comments allowed, names looked up intexts/en_US.lang);.mcpack/.mcaddonfiles are sorted into resource, behavior or skin packs by their modules, replacing a pack with the same UUID. Removed worlds and packs move tocom.mojang\arctic-trash. -
Servers are
minecraftpe\external_servers.txt(id:name:host:port:timelines) and are pinged with RakNet's unconnected ping over UDP (arctic_ping::ping_bedrock). -
Clients (
bedrock::client): Bedrock has no mod loader, so a client (Latite or Flarial, chosen in the Bedrock tab orsettings.json'sbedrock_client) is a DLL loaded into the running game, as other launchers do it. The DLL comes from the client's own GitHub releases (SHA-256 checked against the digest GitHub lists, kept incache/bedrock-clients), and after Play the launcher waits forMinecraft.Windows.exe, a few seconds for its graphics, then runsLoadLibraryWon a thread inside it. GDK games only (1.21.120+). A client that can't be fetched or loaded never stops the game from starting. Arctic doesn't write or ship these clients; the tab warns that servers may ban modified clients.
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.