Files
Unlucky-Launcher/README.md
T
AskaEth a3f26d424b docs(readme): r1 复审 P2 两条措辞修正(dry-run 口径、导入取消的真实含义)
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:在飞
文件就地掐断、段边界收手、不登记实例),已移到「版本管理」条目。

中英两版同步改,未动结构。
2026-11-05 20:59:16 +08:00

22 KiB
Raw Blame History

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: the local instance list on the left; on the right the selected instance's
version, loader, isolation mode and last-played time

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/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 <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-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.

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. 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.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
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_<version>_<platform>_<arch> 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 <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/design are a Rust port of its HCT / ColorSpec2021 algorithms; details and the exact ported functions are listed in 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 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.