Files
Unlucky-Launcher/README.md
T
AskaEth 8ad9792abd docs(readme): 重排成面向第一次打开的人的次序,并补一张真截图
发布前把两份 README 的次序改成:一句话定位 → 截图 → 现在能用什么(含「还没做」)
→ 下载与运行 → 从源码构建 → 架构/模块地图 → 许可与致谢。原来的边界说明没删,
拉到对应功能旁边;「还没做」独立成节并写实。

同时按用户令清掉所有 v0.1.0 / Slint 时代的内容(版本考古段、历史 bin 行、Slint
Royalty-Free 义务段、crates/app 的模块地图行与构建说明、「翻牌还欠 bin 改名」)。
NOTICE 未动。

内容上纠正了几处「写得比实现强」:
- 启动器内的助手没有接真实模型(transport 是脚本化的),原句「chat 在启动器里」
  会让读者以为能用;现在明说占位,并把真的那部分(MCP server)单独讲清。
- 「界面文案统一走一份目录」改成本地化边界:守门只扫实例页,且外观面板老控件
  仍是硬编码英文。
- 实例页的 新建/导入/复制/打开文件夹/删除、日志页的 打开文件夹、设置帧的
  保存/重置/加覆盖 都是没有后端的占位按钮(pages/mod.rs 偏离 #8),写进「还没做」。
- Java 回退链按地区不同,补上 java.vendorPriority 与区间差异。
- 补 ULL_CF_API_KEY(CurseForge 检索的前置,此前全仓文档都没提)。

截图:docs/images/instances-en-light.png(DevDocs/artifacts/m120/03-instances-en-light.png,
1180×760、浅色、英文界面;实例名与档案名均为测试数据,无路径/令牌)。
2026-11-05 20:53:30 +08:00

332 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Unlucky Launcher (ULL)
**English** · [简体中文](README.zh-CN.md)
> **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](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. The CLI can plan the same install without downloading
(`ull install --dry-run`: file list, total size, source per file).
- **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. Installs are cancellable for real, not at the next
file boundary.
- **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.
```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_<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
`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 <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](#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_<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](LICENSE); third-party notices in
[NOTICE](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](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.