P2-1:`--dry-run` 原来写成「file list, total size, source per file」,比实现强。人类 可读输出是按角色的文件数与字节数 + 合计 + 每类一个源链样本 (crates/cli/src/commands/install/report.rs:161-201),逐文件列源是被刻意否决的设计 (crates/core/src/game/install/plan_view.rs:46-49:「只取每类的首个文件做样本……逐文件 列源选择既没用也没法读」)。整份逐文件清单只在 --json 的 data["files"] 里。 P2-2:「安装是真取消」原来挂在整合包条目下,读者会读成「整合包导入也能真停」。实际 导入取消是界面级的:面板收回 + 结果作废,后台导入跑到底(crates/presenter/src/host/ addon/pack.rs:28-42 模块文档原话「取消是界面级的……core 的 ImportRequest 没有取消 令牌」)。真取消是版本安装那条路(crates/presenter/src/host/dispatch.rs:61-88:在飞 文件就地掐断、段边界收手、不登记实例),已移到「版本管理」条目。 中英两版同步改,未动结构。
Unlucky Launcher (ULL)
English · 简体中文
Official project home: https://github.com/AskaEth/Unlucky-Launcher | downloads, issues and release notes all live there: https://github.com/AskaEth/Unlucky-Launcher/releases
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 — 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).
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/diagnosecollects five kinds of signals (exit code,latest.log,crash-reports,hs_err_pidfiles, 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
processorsreally 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.mrpackor 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, becausecore'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_KEYbefore 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 <major> 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=<id> before launching, or build with
it (ULL_MSA_CLIENT_ID=<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=<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-rendererkeeps 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 serveexposes 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 unlessagent.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 configprints 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.
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-dirtake 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 Xalone 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-arm64andmacos-x64archives 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 manualworkflow_dispatchprobe withcontinue-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. Names follow
unlucky-launcher_<version>_<platform>_<arch>; the single source of truth for that shape is
packaging/make_artifacts.py::asset_stem():
| Slot | Asset | Shape |
|---|---|---|
windows-x64 |
unlucky-launcher_<version>_windows_x64.exe |
bare .exe, double-click to run |
windows-arm64 |
unlucky-launcher_<version>_windows_arm64.exe |
bare .exe |
linux-x64 |
unlucky-launcher_<version>_linux_x64.tar.gz |
one archive, one top-level directory |
linux-arm64 |
unlucky-launcher_<version>_linux_arm64.tar.gz |
same |
macos-arm64 |
unlucky-launcher_<version>_macos_arm64.tar.gz |
same |
macos-x64 |
unlucky-launcher_<version>_macos_x64.tar.gz |
same |
| every slot | ull_<version>_<platform>_<arch>[.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
dlopens 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 <command> 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. Nothing else needs configuring.
Build from source
Prerequisites:
- Rust stable (
rust-toolchain.tomlselects the toolchain automatically; the gates also need therustfmtandclippycomponents) - 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
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.icedis cargo's bin name, not a download name —packaging/make_artifacts.pystages that same build asunlucky-launcher_<version>_<platform>_<arch>and names the binaryunlucky-launcherinside 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-rendererforces tiny-skia (ICED_BACKEND=tiny-skia);--screenshot <png>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; third-party notices in NOTICE.
- iced (https://github.com/iced-rs/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/designare a Rust port of its HCT / ColorSpec2021 algorithms; details and the exact ported functions are listed in NOTICE. - Other major dependencies:
rmcpandaxum(Apache-2.0 / MIT) for the MCP server,tokio(MIT),reqwestwith 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 indeny.toml; GPL-family dependencies may not enter the main line.
Thanks to HMCL and PCL2 (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.
