复审 r2(p2-3items / fix-then-merge)逐条整改。**P2-6 与 P3-3/4/5 按塔台指示不动**,只登记。 ## 必修 1:`PACK_RUNNING` 在早退路径不清零(跨功能泄漏) 改前:`fresh_cancel()` 置 bool,`super::finished()` 在 `block_on(import_pack(..))` **之后**才清; 探测失败 / 目标目录不可用两条 `return` 都在它之前 ⇒ bool 永久留在「在跑」。 后果:之后**任何**一次 `CancelInstall`(包括下载页的「取消安装」)①不再发 `InstallCancelled`、 ②还给内容库发 `PackEvent::Cancelled`(一句「导入还在后台跑」的假话)。 改后:`InFlight` **RAII 句柄**(`Drop` 减一)取代手工 `finished()`;计数用「每个 host 一格」的 `in_flight: usize` 而不是 bool(连点两次 Import 各自进出)。 `InFlight::begin(&env)` 在**派发点**创建、随线程移动到出口 ⇒ 正常收尾、两条早退、`?`、panic 展开**任何**退出路径都会减一,以后再加早退分支也不用记得补一行。 ## 必修 2:`dir_name` 注释与实测不一致 改前:注释与报告写「`Better MC` → `Better-MC`(不加哈希)」,而 `faithful` 只认 `[A-Za-z0-9_-]`, 空格不算 ⇒ 实际 `Better-MC-c7c9c149`;测试只钉了无空格形态。 改后:**选「空格算 faithful」这边**,注释、代码、测试三处对齐 —— 名字只含 ASCII 字母数字、 空格、`-`、`_` 时保持原样(空格折成 `-`);折过**别的**字符(中文、`.`、括号…)才补 `-<hash8>`。 测试钉住四例:`Better-MC` → 原样、`Better MC` → `Better-MC`(不加哈希)、`pack` → `pack`、 `Survival 1.20.1` → `Survival-1-20-1-<hash>`(折过 `.` 必须补)。 ## 必修 3:两处注释与代码相反 - `host/job.rs` 的 `HostTask::Pack`:原写「代次与 `HostTask::Install` 共用同一计数器」,改为 「pack 自己的一条(`PACK_EPOCH`),不与 `install_epoch` 共用」并写明为什么(P2-7 的教训)。 - `host/addon/pack/import.rs` 的 `target_of`:`<slug>` → `<dir_name>`,并注明中文名不会被折成同一个 `pack`。 ## 必修 4:两条 P3 - `04` 图里「Back」出现两次:有预检结果时字段区那行只留「预检」,`Back` 只由预览块给一个 (没有预检时才在字段区给 `Back`)。 - 导入侧 `rejected` 列表补 `bounded`(与 missing / conflicts 同一口径)。 ## 必修 5:app 级用例(P2-1 的护栏) 新增 `a_failed_import_releases_the_in_flight_flag_and_keeps_install_cancel_intact`: `CoreHost::headless` + 真派 `PackCommand::Import`(包不存在 ⇒ 探测早退)→ 泵到 `Probe` → 断言 `in_flight == 0` → 再派 `CancelInstall` → 断言**仍会**发 `InstallCancelled` 且**不发** `PackEvent::Cancelled`。 ## P3-6(裸静态量):按塔台要求正面回答并已改掉 不再用裸静态量:`PACK_EPOCH` / `PACK_RUNNING` / `PACK_CANCEL` 换成**按 host 键控**的一张表 (键 = `LauncherLayout::home()`),理由是实测撞到过串扰 —— `host/cancel_tests` 派一次 `CancelInstall` 就 bump 了全局代次,我这条在飞的导入事件被按代次丢掉(新用例因此红过)。 为什么不挪进 `CoreHost`:那要给 `host/mod.rs` 加字段(不在 scope)。这张表等价,`host/mod.rs` 一旦进 scope,搬过去只是把表删掉 —— 报告里也这么写了。 新增用例 `pack_epoch_is_per_host_and_is_its_own_counter` 钉住「另一个 host 的代次不受影响」。 ## 顺手的两条 - `pack/tests.rs` 加完用例后 429 行超线 ⇒ 夹具(临时目录 / 写文件 / 泵 host)拆到 `pack/tests/harness.rs`,`tests.rs` 回到 3xx 行。 - clippy `-D warnings` 抓到 `== false`、两处多余的 `&`、一个未用的再导出,都改掉了。 门禁:`cargo fmt --all --check` 通过;clippy 三 crate `--all-targets -- -D warnings` EXIT=0; `cargo test -p unlucky-app-iced --lib` **507 passed**;`cargo test -p unlucky-presenter --lib` **185 passed**(新增的 app 级用例在内,且是在**全量并行**下跑的);改动文件全部 ≤400 行。
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 (ULL) 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. The launcher embeds an AI agent client (chat / configuration / crash diagnosis) and can be attached to other agent tooling as an MCP server.
Highlights
- Single-file native binary: the launcher itself needs no JVM, no WebView and no sidecar runtime. The JDK a game needs is downloaded and managed per instance, not as a launcher dependency.
- One UI stack in the build: the UI is iced 0.14 + Material 3 Expressive. The earlier Slint
implementation (
crates/app) is kept as source-only history: it is excluded from the cargo workspace, so the workspace never builds, tests or ships it. v0.1.0 — the only tagged release so far — still carries a Slint build (see Status). - CPU software-rendering fallback:
--software-rendererkeeps the interface rendering normally on machines with no usable GPU or with broken drivers. - AI agent + MCP: chat, setting edits and crash diagnosis inside the launcher;
ull agent serveexposes the same core as an MCP server (stdio or Streamable HTTP) to external agent tools. - Mod loaders: Fabric, Quilt, NeoForge, Forge, with modpack and CurseForge / Modrinth clients in the core.
- Java management: a Temurin → Oracle → Microsoft → Zulu fallback chain, region-aware.
- Accounts: sign in with a Microsoft account over OAuth 2.0 device code flow → Xbox Live →
XSTS →
api.minecraftservices.com; account tokens are kept on this machine only. - Engine: multi-source download (health scoring, mirrors, chunked transfers, hash while downloading) and a YAML rule-based crash diagnosis engine.
- License: Apache-2.0 © AskaEth (YeiJ).
Status
Two tracks. They are not the same product yet, so read the headings before downloading anything.
Track A — historical: v0.1.0, Slint UI (the only tagged release so far)
Historical implementation — no longer in the default build or the release matrix. The Slint tree (
crates/app) is kept in the repository as source-only reference: it is excluded from the cargo workspace, socargo build/cargo test/cargo clippynever touch it and no artifact is built from it. Read the source if you want to see how that UI was put together; do not expect to build it. The list below is kept because it records what the v0.1.0 assets actually contain — since v0.1.0 the pipeline builds the iced binary and ships it under the same public asset names.
- GitHub Releases
v0.1.0(2026-11-01, flagged pre-release): single-file assets for Windows x64 (a bare.exe), Linux x64 and macOS (arm64 and x64).tar.gz. Every release also carriesSHA256SUMS.txtand theLICENSE/NOTICE/README.mdtexts as separate assets; on Windows those texts sit next to the.exebecause a bare exe cannot hold them, while the unix archives already contain them. - UI: Slint + Metro. This is the shipped GUI — the instance, download, account, Java, log and settings pages all run on the core engine (the in-app AI chat panel is a scripted placeholder).
- CLI in the same binary:
unlucky-launcher instances list,unlucky-launcher launch <instance>,unlucky-launcher logs <sid>and the rest of the command set run before any UI is built — no window, no renderer probe, no font scan. Commands:instances,install,launch,accounts,sessions,logs,diag,config,java,agent, plus--help/--version. - A second, lighter artifact
ull(cargo build --release --bin ull, zero UI dependencies): the same command set from a binary that links no graphics stack at all — not even fontconfig. Servers, CI containers and Termux getull; desktops getunlucky-launcherand one binary covers both GUI and CLI. Both are release assets from the next tagged release (thev0.1.0assets predate this). - Windows ARM64 and Linux ARM64 slots are wired into the release pipeline; the next tagged release
builds them. The
v0.1.0assets predate those two slots.
Track B — in progress on dev: next-generation UI (iced + Material 3 Expressive)
This track is not released yet — you can see it only by building from source.
- Locked direction: the look is Material 3 Expressive and the framework is iced 0.14; the UI layer is written from scratch. Neither the style nor the framework will change again.
- Done: the token crate
crates/design(no UI framework, no third-party crates — real HCT color, type scale, shape scale, spacing, elevation, motion, icon geometry, per-component metrics) and the skeleton cratecrates/app-iced(opens a window, self-drawn title bar and window buttons, navigation rail, colors and type taken from tokens only, GPU→CPU backend probe with automatic fallback, and the screenshot/diff channel the visual reviews use). - Not done: the component library (buttons, dialogs, lists, menus, search, and the rest of the M3 set), page migration, and the i18n pass. Every page in the iced binary is a placeholder.
- Slint is retired from the build, kept as reference:
crates/appis no longer a workspace member (M118) — the source stays for historical reference only: not compiled, not tested, not shipped, and no longer buildable as a workspace member. What is left of the flip is the rename: until the iced build passes equivalence acceptance its binary is still calledunlucky-launcher-iced, while the release pipeline already ships it under the publicunlucky-launcherasset name.
Binaries you may run into, so you can tell them apart:
| Binary | Crate | State |
|---|---|---|
unlucky-launcher-iced |
crates/app-iced (iced 0.14) |
the only GUI binary the workspace builds; the release pipeline stages it under the public name unlucky-launcher; same subcommand dispatch |
unlucky-launcher |
crates/app (Slint) |
historical: that crate is excluded from the workspace, so this binary is no longer built or shipped; the v0.1.0 assets are the last Slint builds |
ull |
crates/cli |
the zero-UI build of the same command set, shipped as its own asset |
unlucky-launcher and ull 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, a CI container or Termux where no window can exist.
Pick ull when there is no desktop, unlucky-launcher <command> when there is. Both print the same
CLI text and name themselves after the binary you invoked: unlucky-launcher --help starts with
unlucky-launcher ·, ull --help starts with ull ·.
Subcommand dispatch
unlucky-launcher <args> first asks "is this a CLI call?" before any UI code runs:
unlucky-launcher # GUI
unlucky-launcher --software-renderer # GUI, CPU renderer (unchanged)
unlucky-launcher instances list # CLI, no window
unlucky-launcher --json launch 1.20.1 # CLI, global options before the command
unlucky-launcher --version # CLI, prints version + OS summary
The 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 itself lives in exactly one place —
crates/cli/src/cli/parse.rs — so both GUI binaries and ull accept the same set by construction.
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/design # design tokens: real HCT color / type / shape / spacing / elevation / motion / icons
│ # / component metrics — zero UI dependencies, no third-party crates
├── crates/app # HISTORICAL Slint UI (bin `unlucky-launcher`): excluded from the workspace,
│ # source-only reference — never built, tested or shipped any more
├── 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
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) |
addon |
mod loader installation, modpacks, CurseForge / Modrinth |
diagnose |
five signal collectors, YAML rule engine, hs_err parsing |
platform |
OS abstraction: paths, processes, file locks, capability probing, runtime OS detection |
Building
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 (the Slint build also needed femtovg)
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/
--workspace covers the eight members in the module map. crates/app is excluded at the workspace
level (M118), so none of these commands compiles the Slint tree — and there is no build command for
it: that crate is kept as source-only reference and can no longer be built as a workspace member.
What the build produces:
unlucky-launcher-iced— the GUI (iced). It is also the CLI: the same binary answers every subcommand listed above. The name is temporary — it becomesunlucky-launcherat the flip. The release artifacts already carry the public name: the pipeline builds this bin andpackaging/make_artifacts.pystages it asunlucky-launcher[.exe], so download links never change.unlucky-launcher— historical: no workspace build produces this binary any more; it belonged tocrates/app, which is excluded. The v0.1.0 assets are the last builds under this name.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, which 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
- Historical (v0.1.0's Slint build still takes it):
unlucky-launcher --software-renderer, i.e.SLINT_BACKEND=winit-software, forced the CPU renderer of that build through its own fallback chain
Dependency license check (optional locally, required in CI):
cargo deny check licenses bans sources.
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: a red result blocks
the merge. The macOS runtime probe is not there yet — it only runs on a manual workflow_dispatch
and its result is recorded, not enforced (continue-on-error), until the slot passes a first run on
a real runner.
| Workflow | Content |
|---|---|
.github/workflows/ci.yml |
fmt + clippy + test + release build (Windows / Linux matrix); Linux runtime smoke (Xvfb: renders one real frame, then asserts the PNG is not blank); macOS runtime probe (manual dispatch only) |
.github/workflows/guards.yml |
zero-UI guard for crates/core (manifest plus cargo tree), 400-lines-per-file guard, cargo-deny license and source guard |
.github/workflows/release.yml |
tag v* → per-slot builds → single-file artifacts + SHA256SUMS.txt → GitHub Release |
Platform baseline
Minimum baseline: Windows 10; Windows 8.1 / 8 / 7 are not supported.
- Windows 10/11 builds use the standard toolchain (MSVC / GNU), built the same way as upstream cargo
- macOS 12+ (Apple Silicon and Intel) and Linux x64 / arm64 are supported build slots; on Linux,
X11 is the preferred path today and Wayland is the long-term direction. macOS builds are unsigned,
so the first launch needs the Gatekeeper quarantine attribute cleared
(
xattr -d com.apple.quarantine) - Android Termux is experimental and not promised: CLI only, GUI unverified
- Each platform runs its own test matrix; capability differences use feature gates with runtime probing.
License and acknowledgements
- This project: Apache License 2.0, see LICENSE; third-party notices in NOTICE.
- Slint (https://slint.dev), Royalty-Free 2.0: obligation for the Slint build that
v0.1.0ships — the condition attaches to each distribution of an Application, not to a version number. Upstream §2 offers two forms: (a) the officialAboutSlintwidget on an "About" screen or dialog reachable from the top level menu (or the splash screen when there is none); (b) the official Slint attribution badge on a public page where the binaries can be downloaded.v0.1.0's settings page does embed theAboutSlintwidget (crates/app/ui/pages/settings.slint, reachable from the sidebar); whether that satisfies §2(a), and the fact that thev0.1.0release page carries a text link rather than the §2(b) badge, are not judged here — Slint's own licence text is the authority. Thev0.1.0NOTICE, that page's text and this section are records, not a statement of discharge. Later builds link no Slint, so they add nothing to this obligation. The obligation follows a Slint-linked binary: it applies to thev0.1.0assets (and to any later build that links Slint again), not to new iced builds —crates/appis excluded from the workspace, so the pipeline no longer produces one. - 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.