# Unlucky Launcher (ULL) **English** · [简体中文](README.zh-CN.md) > **Official project home**: | downloads, issues and > release notes all live there: Unlucky Launcher is a free, open-source **Minecraft: Java Edition** launcher written in Rust (Apache-2.0). It ships as **one native binary per platform** — no installer, no bundled runtime, no WebView — and covers the launcher job from installing a version to reading a crash report: instances and versions, mod loaders and modpacks, mods / resource packs / shaders, per-instance Java, Microsoft / third-party / offline accounts, and a crash diagnosis that runs locally on a YAML rule engine. Nothing of yours goes through a service of ours: account tokens stay on the machine. ![The instances page: the local instance list on the left; on the right the selected instance's version, loader, isolation mode and last-played time](docs/images/instances-en-light.png) *The instances page — 1180×760, English UI, light theme. The instance names are test data, not anybody's real setup. That `Launch failed · exit0 静默失败` line is also an honest sight of a known gap: the reason text is still raw Chinese straight out of the launch layer (see [Not done yet](#not-done-yet)).* ## What works today ### Instances, versions and launching - **Instances** — a list-detail page: the local instance list on the left; on the right the selected instance's version, loader, isolation mode (an isolated game directory or the shared one), files-on-disk state and last-played time, with Play / Stop. Narrow windows switch panes instead of stacking them. - **Where an instance comes from** — installing a version (with a name, loader and loader version) or importing a modpack. The instances page's own **New instance / Import / Duplicate / Open folder / Delete** buttons are **not wired yet**: they are placeholder actions that only write a line to stderr (`crates/app-iced/src/app/update/page_messages.rs`), and the instance wizard is the pending piece (`crates/app-iced/src/pages/mod.rs`, deviation #8). - **Version management** — a three-column rail (version type / major version / search) over the Mojang version list: release, snapshot and ancient versions, plus loader selection and a loader version when you install. Cancelling an installation really stops it — the in-flight file is cut, the job ends at the next stage boundary, and no instance is registered (`crates/presenter/src/host/dispatch.rs:61-88`). The CLI can plan the same install without downloading (`ull install --dry-run`: per-role file counts, total size, and one source-chain sample per file category; the full file list is in `--json`). - **Launching** — a 10-stage command-line assembly (`crates/core/src/launch/`), per-instance JVM and game arguments, a native-library pass, and a JSONL session record for every run. Process trees are killed as trees, not as one PID. - **Crash diagnosis is real and local** — `crates/core/src/diagnose` collects five kinds of signals (exit code, `latest.log`, `crash-reports`, `hs_err_pid` files, environment snapshot including the mod list), matches them against a YAML rule set, and shows the findings with fix actions on the logs page. No model, no network: it works offline and on a machine with no GPU. ### Mod loaders, packs and the content library - **Loaders** — Fabric, Quilt, NeoForge and Forge. Forge/NeoForge versions that need the official installer `processors` really run them, with a managed JDK if the machine has none. - **Modpacks** — import a CurseForge manifest, a Modrinth `.mrpack`, or a MultiMC/Prism instance; export as `.mrpack` or a CurseForge manifest. Cancelling an **import** is interface-level only: the panel closes and the result is discarded, but the import thread runs to the end, because `core`'s import request has no cancel token yet (`crates/presenter/src/host/addon/pack.rs:28-42`). - **Content library** — modpacks / mods / resource packs / shaders with keyword, source, version, loader and sort filters, paged results, and one-click *install into this instance*. - **What that page will refuse, and says so on the row** — a modpack cannot be installed *from* the library (packs go through the import pipeline above); CurseForge's *install* is disabled because its file endpoint is not wired; and an item whose game version or loader does not match the target instance is disabled with the mismatch spelled out. Nothing fails silently. - **CurseForge search needs a key** — set `ULL_CF_API_KEY` before launching; without it that source is greyed out in the source filter. The key stays in the environment: it is not written to the settings file and not logged (`crates/presenter/src/host/addon.rs`). ### Java A managed JDK per instance, with a four-vendor fallback chain ordered by region — Temurin first, then Oracle before Microsoft inside mainland China (measured 28× faster there) and Microsoft before Oracle elsewhere, Zulu last — overridable through the `java.vendorPriority` setting (`crates/core/src/game/java/vendor.rs`). Mirrors are region-aware too. `java list` shows every JDK it can actually see (including the ones the official launcher downloaded), `java install ` fetches one. The game's JDK is the launcher's data, not its own dependency. ### Accounts Three tracks on one page: - **Microsoft** — OAuth 2.0 device code flow → Xbox Live → XSTS → `api.minecraftservices.com`. - **Third-party community auth servers** (authlib-injector style). - **Local offline profiles**, for isolated testing. **Microsoft sign-in needs an Azure public client id, and no release binary carries one.** The id is not in this repo and CI injects none, so set `ULL_MSA_CLIENT_ID=` before launching, or build with it (`ULL_MSA_CLIENT_ID= cargo build`, which bakes it in via `option_env!`). Without one, sign-in stops before any network call and the alert says exactly what is missing: the title "Microsoft sign-in is not configured", a body naming `ULL_MSA_CLIENT_ID=` (set it, or build with it, then restart the launcher — a running process does not pick up a new variable), and `Code: msa_not_configured`. It offers no Retry button, because retrying in that same process cannot help. Every other sign-in failure keeps the older shape: one category line plus its own `Code:` line, with the full reason only in the launcher log. Community auth servers and local profiles need no id. ### Look and language - **One UI stack** — iced 0.14 + Material 3 Expressive, a self-drawn title bar, an animated navigation rail, light / dark / follow-system. - **Appearance** — window background image with a colour pipeline (panel colours can be derived from the wallpaper), per-panel transparency, and whole-window transparency on Windows. On Windows 11 and Server 2025 the system is asked for rounded corners; macOS and Linux leave corners to the desktop's own rules; nothing is drawn by hand. - **CPU fallback** — `--software-renderer` keeps the interface rendering on machines with no usable GPU or broken drivers, and the backend probe falls back on its own when no adapter is usable. On that path panel-level translucency and blur are switched off (window-level transparency still works) and the settings panel says so. - **Localisation** — system-language detection, an explicit override in settings, and persistence across restarts. Most copy comes from one message catalogue (`crates/i18n`). **Boundary, stated because it is easy to over-read that sentence**: the anti-hard-coding guard (`crates/app-iced/src/pages/instances/tests/i18n.rs`) scans the instances page sources only, not every screen; a hard-coded *Chinese* literal has no ASCII word for it to match, so nothing catches that class; and the appearance panel's older controls are still hard-coded English (`crates/app-iced/src/pages/appearance/section.rs`), so a Chinese UI still shows them in English. ### The assistant, and the MCP server These are two different things and the README should not blur them: - **The in-app assistant is a placeholder shell today.** Its transport is scripted (`crates/app-iced/src/presenter/transport.rs`), no real model is connected, and the UI says so in four places on every screen it appears — subtitle, input placeholder, empty-state body, and a "Simulated" chip. The session state, unread count, draft, confirm flow and named failure codes are real presenter state; only the model behind them is not there yet. - **The MCP server is real.** `ull agent serve` exposes the launcher core to external agent tools as an MCP server over stdio or Streamable HTTP, loopback only (`127.0.0.1`, default port 47823), read-only unless `agent.mcp.allow_write=true`. The same ten tool specs (`crates/agent/src/tools/`) feed both shapes, and both go through the same executor — schema validation, sensitivity tiers, audit trail. `ull agent config` prints ready-made Claude Code / Cursor snippets. ### Command line The GUI binary also answers every CLI subcommand **before any UI code runs** — no window, no renderer probe, no font scan. A second, lighter `ull` artifact serves the same command set from a binary that links no graphics stack at all. ```bash unlucky-launcher # GUI unlucky-launcher --software-renderer # GUI, CPU renderer unlucky-launcher instances list # CLI, no window unlucky-launcher --json launch 1.20.1 # CLI, global options before the command unlucky-launcher --version # CLI, version + OS summary ``` The dispatch rule is deliberately narrow, so nothing that used to work changed meaning: - The first argument is a bare word (not `-…`) → it sits in the subcommand position, so the CLI gets it. GUI flags never appear there: `--screenshot` / `--diff` / `--data-dir` take their values *after* the flag. - The first argument is a flag → the line is a CLI call only if `crates/cli`'s parser also produces a real command (`--help`, `--version`, `--json instances list`). `--data-dir X` alone parses to "no subcommand", so it still opens the GUI; every GUI-only flag (`--software-renderer`, `--screenshot*`, `--renderer`, `--theme`, `--diff`, `--force-state`, …) fails CLI parsing, so it still opens the GUI. **Unknown subcommands are an error, not a GUI launch**: `unlucky-launcher lst` prints "未知命令 `lst`", hints `unlucky-launcher --help`, and exits 1. Silently opening a window would tell a script that its typo took effect. The subcommand table lives in exactly one place (`crates/cli/src/cli/parse.rs`), so both binaries accept the same set by construction. `ull --help` lists them all: `instances`, `install`, `launch`, `pack import`, `accounts`, `sessions`, `logs`, `diag`, `config`, `java`, `agent`, `version`, `help`. ### Not done yet This list is the reason to believe the rest. None of it is hidden behind marketing copy. - **No installer and no auto-update channel.** You download a file and run it. Nothing checks for a newer version for you, and there is no update source decided yet. - **Several buttons are placeholders.** On the instances page: *New instance*, *Import*, *Duplicate*, *Open folder*, *Delete*. On the logs page: *Open folder*. In the settings frame: *Save*, *Reset section*, *Add override*. Each writes `[page] 占位动作 …` to stderr and changes nothing — instance management from the GUI is the instance-wizard work still outstanding, and settings change as you edit them rather than on a Save. - **Linux and macOS artifacts are unverified.** The reference machine is Windows; the `linux-x64`, `linux-arm64`, `macos-arm64` and `macos-x64` archives are produced by the CI matrix and have not been downloaded and run end to end. Treat them as untested output, not as "supported because the matrix lists them". - **macOS is not finalised.** It has two rounds of real-device evidence (Apple Silicon), which is why the build exists — but the menu-bar name, the placement of the three window buttons (macOS puts them top-left; ours are drawn top-right), and 300s+ steady state still need a real machine. macOS builds are unsigned, so the first launch needs the Gatekeeper quarantine attribute cleared (`xattr -d com.apple.quarantine`). The macOS runtime smoke job is a manual `workflow_dispatch` probe with `continue-on-error`, not a gate. - **The content library is cramped at low window heights**, and the window height is not wired into the page model yet — the page lays itself out for a nominal height rather than the one it gets. - **Launch-side failure text is not structured.** The tile takes a Chinese sentence straight from the launch layer (`crates/core/src/launch/exit_analysis.rs`) instead of a reason code the UI could translate — which is exactly the `静默失败` you can see in the English screenshot above. - **The in-app assistant has no model behind it** (see above). The MCP server does. - **Pack import has two global policies**, keep-user or overwrite, not a per-file choice; the import report lists every conflict so nothing is overwritten silently. - **Install progress is stage-level**, not byte-level, on the content-library install path. - **No native file chooser.** Import takes drag-and-drop or a pasted path. - **Android/Termux is experimental and not promised**: no released asset targets it, and the GUI is unverified there. ## Download and run Grab the file for your platform from [Releases](https://github.com/AskaEth/Unlucky-Launcher/releases). Names follow `unlucky-launcher___`; the single source of truth for that shape is `packaging/make_artifacts.py::asset_stem()`: | Slot | Asset | Shape | |------|-------|-------| | `windows-x64` | `unlucky-launcher__windows_x64.exe` | bare `.exe`, double-click to run | | `windows-arm64` | `unlucky-launcher__windows_arm64.exe` | bare `.exe` | | `linux-x64` | `unlucky-launcher__linux_x64.tar.gz` | one archive, one top-level directory | | `linux-arm64` | `unlucky-launcher__linux_arm64.tar.gz` | same | | `macos-arm64` | `unlucky-launcher__macos_arm64.tar.gz` | same | | `macos-x64` | `unlucky-launcher__macos_x64.tar.gz` | same | | every slot | `ull___[.exe]` | bare CLI binary, its own asset | | all | `SHA256SUMS.txt` | `sha256sum -c` eats it directly | Each UNIX archive contains the executable plus `LICENSE`, `NOTICE` and `README.md`, in one top-level directory. Windows ships a bare `.exe` (a single file cannot carry the texts), so those three documents are attached to the release next to it — downloading the Windows binary does not strip the licence. Download links never carry implementation detail in the name, and the binary inside an archive is always called `unlucky-launcher`. The two shapes are not duplicates. The GUI binary needs the graphics stack when it opens a window (it `dlopen`s X11 / Wayland / GL and links `libfontconfig` at load); `ull` needs nothing beyond glibc, so it runs on a server or a CI container where no window can exist. On Linux that floor is glibc ≥ 2.35 (the slots build on Ubuntu 22.04) — an older distribution needs a source build (`packaging/README.md` §4). Pick `ull` when there is no desktop, `unlucky-launcher ` when there is. Both print the same CLI text and each names itself after the file you ran — `argv[0]` with its directory and a trailing `.exe` removed, nothing else (`crates/cli/src/cli/program.rs`), so the `ull` asset prints `ull ·`, the binary inside an archive prints `unlucky-launcher ·`, and a local `cargo build` prints `unlucky-launcher-iced ·`. Minimum platform baseline: **Windows 10**; Windows 8.1 / 8 / 7 are not supported. macOS 12+ on Apple Silicon and Intel, Linux x64 / arm64 (`crates/core/src/platform/os_info.rs` holds the baseline check). On Windows 10/11 the builds use the standard toolchain (MSVC / GNU), built the same way as upstream cargo. On Linux, X11 is the preferred path today and Wayland is the long-term direction. Capability differences are feature gates with runtime probing, never a compile-time "this platform gets less". First run with a Microsoft account? Set `ULL_MSA_CLIENT_ID` first — see [Accounts](#accounts). Nothing else needs configuring. ## Build from source Prerequisites: - Rust stable (`rust-toolchain.toml` selects the toolchain automatically; the gates also need the `rustfmt` and `clippy` components) - Windows: MSVC toolchain (what CI uses) or MinGW-w64 (`x86_64-pc-windows-gnu`) - Linux: `libxkbcommon-dev libfontconfig1-dev libgl1-mesa-dev libegl1-mesa-dev libwayland-dev libxcb1-dev` — winit + wgpu/tiny-skia ask for these at build time ```bash cargo fmt --all --check # formatting cargo clippy --workspace --all-targets -- -D warnings # lints (zero-warning gate) cargo test --workspace # tests cargo build --release # binaries land in target/release/ cargo deny check licenses bans sources # dependency licences (required in CI) ``` What the build produces: - `unlucky-launcher-iced` — the GUI, and the CLI in the same file: it answers every subcommand listed above. `iced` is cargo's bin name, not a download name — `packaging/make_artifacts.py` stages that same build as `unlucky-launcher___` and names the binary `unlucky-launcher` inside the archive. - `ull` — the CLI as a standalone, UI-free binary. Same command set, no graphics stack, smaller, and the right artifact for a machine that has no display at all. Rendering switches: - `unlucky-launcher-iced --software-renderer` forces tiny-skia (`ICED_BACKEND=tiny-skia`); `--screenshot ` writes one frame and exits — that is the acceptance channel CI's headless smoke job uses - With no usable adapter the iced build's explicit probe falls back to the CPU path on its own **CI gates.** Every push to `main` and every pull request runs fmt / clippy (warnings are errors) / the full test suite, and the Linux headless render smoke is a hard gate on the same footing: it renders one real frame under Xvfb, then asserts the PNG is not blank, and a red result blocks the merge. The macOS probe is not there yet — manual dispatch only, result recorded but not enforced. | Workflow | Content | |----------|---------| | `.github/workflows/ci.yml` | fmt + clippy + test + release build (Windows / Linux matrix); Linux runtime smoke (Xvfb, one real frame, PNG asserted non-blank); macOS runtime probe (manual dispatch only) | | `.github/workflows/guards.yml` | zero-UI guards (`crates/core`, `crates/presenter`, `crates/design`), 400-lines-per-file guard, cargo-deny licence and source guard | | `.github/workflows/release.yml` | tag `v*` → six-slot build → single-file artifacts + `SHA256SUMS.txt` → GitHub Release | ## Architecture and module map ``` Unlucky-Launcher (cargo workspace) ├── crates/core # domain library: zero UI dependencies, eight modules ├── crates/agent # AI agent: ToolSpec registry + built-in client + MCP server (one core, two shapes) ├── crates/presenter # business host layer: framework-free state / host / dto / conversion ├── crates/design # design tokens: real HCT color / type / shape / spacing / elevation / motion / icons │ # / component metrics — zero UI dependencies, no third-party crates ├── crates/i18n # language detection, message catalogue, CJK font policy ├── crates/palette-extract # the only crate that decodes images, for the wallpaper colour pipeline ├── crates/app-iced # the iced UI (bin `unlucky-launcher-iced`): the only GUI the workspace builds ├── crates/cli # CLI (bin `ull`): headless, zero UI └── packaging # single-file artifact matrix and release scripts ``` Layering: `app-iced` → `presenter` → `core` / `agent`. `design` and `i18n` are consumed by the shell and the presenter; none of them may depend on a rendering framework, and CI guards enforce it. The eight modules of `crates/core`: | Module | Responsibility | |--------|----------------| | `setting` | three data-directory modes, schema-based config, atomic writes, migrations | | `download` | multi-source health scoring, mirrors, chunked downloads, hash-while-downloading | | `game` | version / instance repository, snapshots and inheritance resolution | | `launch` | 10-stage launch pipeline, processes, JSONL event stream | | `auth` | Microsoft sign-in (OAuth 2.0 device code → Xbox Live → XSTS → Minecraft Services), third-party community servers, local profiles | | `addon` | mod loader installation, modpack import, CurseForge / Modrinth, pack export | | `diagnose` | five signal collectors, YAML rule engine, hs_err parsing | | `platform` | OS abstraction: paths, processes, file locks, capability probing, runtime OS detection | ## License and acknowledgements - **This project**: Apache License 2.0, see [LICENSE](LICENSE); third-party notices in [NOTICE](NOTICE). - **iced** (), MIT: the framework of the UI, together with `winit` (Apache-2.0) and, through it, `wgpu` (MIT / Apache-2.0) / `tiny-skia` (BSD-3-Clause). - **Google Material Color Utilities** (Apache-2.0): the color tokens in `crates/design` are a Rust port of its HCT / ColorSpec2021 algorithms; details and the exact ported functions are listed in [NOTICE](NOTICE). - **Other major dependencies**: `rmcp` and `axum` (Apache-2.0 / MIT) for the MCP server, `tokio` (MIT), `reqwest` with rustls (MIT / Apache-2.0), `serde` / `serde_json` (MIT / Apache-2.0), `zip` / `flate2` / `tar` (MIT / Apache-2.0). The allow-list cargo-deny enforces is in `deny.toml`; GPL-family dependencies may not enter the main line. Thanks to [HMCL](https://github.com/HMCL-dev/HMCL) and [PCL2](https://github.com/Meloong-Git/PCL) (both GPL-3.0). They have been with the vast majority of Minecraft players for years, and this project has benefited a great deal from their design choices and implementation details.