复审 r1(reviewer-accscope-code,p2-2items / fix-then-merge)的两条阻塞 + 四条顺手的 p3。 ## p2-1 `[window]` 的三个窗口属性曾是格式串常量(U1:常量冒充观测结果) 第一版把 `resizable=true decorations=false transparent=false` 写在格式化串里,与交给 iced 的 `window::Settings` 没有任何绑定;定向变异把 `settings.decorations` 改成 `true`,**371 个测试全绿**, 而验收包里 `[caps]` 与 `[window]` 会互相矛盾。 - 新增 `acceptance::window::WindowFacts` + `from_settings()`:字段只能从**真实 settings** 抽出来, 没有手抄入口;`window_text()` 逐字段打印。 - 窗口设置收成单一产地 `app/boot/diagnostics::window_settings()`,`launch()` 与 `[window]`/`[caps]` 两行日志共用同一个 `&window::Settings`;`pages/harness.rs` 同样从自己那份 settings 取。 - 守门测试 `window_settings_match_what_the_acceptance_logs_claim`,含「from_settings 必须逐字段 搬运」的反向断言。变异实证:`window_settings` 的 `decorations → true` ⇒ 红并点名; `from_settings` 里写死 `decorations: false` ⇒ 红(第一次写测试时这条漏抓,补强后实测红)。 ## p2-2 语言维度空转、且没有任何结论断言 `lang::resolve` 是 setting → 平台源 → env,macOS 的 AppleLanguages 几乎恒有值 ⇒ `LANG=zh_CN` / `LANG=en_US` 两档同值,而原脚本只断言「`[i18n]` 这行存在」——整条链退化成 `source=default lang=en` 时核心集一条都不红。 - 脚本加两条结论断言:`^\[i18n\].*platform=applelanguages:[A-Za-z-]+` 与 ` source=applelanguages` (退化日志上实测红、健康日志上实测绿)。 - `REPORT.md` 语言行下注明:env 优先级低于平台源,正常 Mac 上两档同值,这一维验的是「平台源 真读到了 + 中文字体回退」,不是中英对照。 ## p3(复审 + 第二路自跑复核同源的三条) - `[perf] boot probe=0ms` → `probe=skipped`(没计时就不报数;降级重开那一轮改为**真计时**)。 变异实证:把 skipped 写成 0ms ⇒ `perf_lines_carry_measured_millis` 红。 - `[env] gpu=` 排序改成 `DiscreteGpu > IntegratedGpu > 其它`:本机原来报核显,而 iced 实际选的是 独显(现在报 `AMD Radeon RX 6600 XT [DiscreteGpu]`,与 `Selected:` 一致)。 - `[i18n] setting=<未设置>` → `<未接线>`(配置侧根本没读取点,写「未设置」等于把「我们没读」 说成「用户没设」)。 - `[backend] final=` 失败行:`refuse` / `none` 两条路是 `final=none`(什么都没跑起来),只有 `respawn` 才是兜底名;`pages/harness.rs` 不再写死 `via="refuse"`,改用共用的 `failure_via()`。 实跑负例:`[backend] final=none result=failed primary=wgpu fallback=tiny-skia via=refuse`。 - 脚本 `[tick]` 那处唯一未锚定的 `field()`:加 `^` 锚定、正则改 `clock=[a-z0-9-]+`(真值是 `idle-1hz`,旧 `[a-z]+` 把它截成 `idle`,靠前缀巧合通过);断言也改成 `clock=idle-1hz`。 - 脚本 `[backend] final` 取值改 `field_backend()`:优先 `result=started`(respawn 场景一条日志里 有父 failed + 子 started 两条,旧 `head -1` 会把「这次没跑起来」写进报告)。 - 脚本 `exec 3>&1` 全程留痕(终端提示也进 `logs/00-run.log`)、跑前清空 `$OUT/shots/`、 python3 缺失时在 REPORT 里写明「几何列未采」而不是留空。 - `renderer/mod.rs` 补一段依据:为什么「单项 ICED_BACKEND ⇒ 没有静默回退」成立(`iced_tiny_skia` 的 `with_backend` 只接受 `tiny-skia`),以及它依赖 iced 实现细节的两层缓解。 ## 结构 `app/boot.rs` 400 行零余量 → 拆成 `boot.rs`(248) + `boot/fallback.rs`(117,兜底/重开/退出码) + `boot/diagnostics.rs`(110,窗口设置与四段报告);`run()` 回到「探测 → 字体 → 打印 → 起 iced」。 `acceptance` 再把 `[window]` 拆成 `window.rs`(142)。 门禁:`cargo fmt --check` / `clippy --all-targets -- -D warnings` / `test`(373 lib + 6 integration)全绿。
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.
- Two UI stacks during the rewrite: the released UI is Slint (Royalty-Free 2.0); the next-generation UI is iced 0.14 + Material 3 Expressive and is still being written (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 — released: v0.1.0, Slint UI (this is what you can download today)
- 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 stays until the flip:
crates/appremains the reference implementation until the iced build passes equivalence acceptance; only then isunlucky-launcher-icedrenamed tounlucky-launcherand the Slint crate retired.
Binaries in the workspace, so you can tell them apart:
| Binary | Crate | State |
|---|---|---|
unlucky-launcher |
crates/app (Slint) |
released in v0.1.0; still the usable GUI; also runs every CLI subcommand |
unlucky-launcher-iced |
crates/app-iced (iced 0.14) |
under construction; skeleton only, not shipped; same subcommand dispatch |
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 # Slint UI (bin `unlucky-launcher`): the released GUI, kept until equivalence acceptance
├── crates/app-iced # next-generation iced UI (bin `unlucky-launcher-iced`): under construction
├── 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— needed by both UI stacks (Slint's winit + femtovg, iced's winit + wgpu/tiny-skia)
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/
What the build produces:
unlucky-launcher— the Slint UI (the released one). It is also the CLI: the same binary answers every subcommand listed above.unlucky-launcher-iced— the iced UI skeleton. The name is temporary: it can only becomeunlucky-launcherat the flip, because two crates in one workspace cannot emit the same binary name into the same target directory. Same subcommand dispatch.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 --software-renderer(equivalent toSLINT_BACKEND=winit-software) forces the CPU path on the Slint buildunlucky-launcher-iced --software-rendererforces tiny-skia (ICED_BACKEND=tiny-skia);--screenshot <png>writes one frame and exits, which is the acceptance channel- Both stacks fall back to the CPU path when no usable adapter is there: the Slint build through its own renderer fallback chain, the iced build through its explicit probe
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.0
ships — an
AboutSlintwidget on the about page (still outstanding) and, until it lands, the NOTICE file, this section and a "Made with Slint" note on the release page. The obligation is kept for as long as a Slint-linked binary ships. - iced (https://github.com/iced-rs/iced), MIT: the framework of the next-generation UI,
together with
winitand, through it,wgpu/tiny-skia(Apache-2.0 / MIT). - 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.