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、浅色、英文界面;实例名与档案名均为测试数据,无路径/令牌)。
This commit is contained in:
AskaEth committed 2026-11-05 20:53:30 +08:00
1 parent 9cc3260644
commit 8ad9792abd
3 files changed
+458 -345

No files matched your search

+244 -183
View File
@@ -2,117 +2,144 @@
**English** · [简体中文](README.zh-CN.md)
> **Official project home**: https://github.com/AskaEth/Unlucky-Launcher | downloads, issues and
> **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.
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.
## Highlights
![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)
- **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 binary, two shapes**: 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.
- **One UI stack**: iced 0.14 + Material 3 Expressive, with self-drawn title bar, animated
navigation rail, light / dark / follow-system, and a background-image colour pipeline. The earlier
Slint implementation (`crates/app`) is kept as source-only history: excluded from the cargo
workspace, never built, tested or shipped.
- **CPU software-rendering fallback**: `--software-renderer` keeps the interface rendering normally
on machines with no usable GPU or with broken drivers; the backend probe falls back on its own when
no adapter is usable.
- **AI agent + MCP**: chat, setting edits and crash diagnosis inside the launcher; `ull agent serve`
exposes the same core as an MCP server (stdio or Streamable HTTP) to external agent tools. The
agent rail runs on real presenter state, and says so plainly when no model endpoint is wired.
- **Mod loaders and modpacks**: Fabric, Quilt, NeoForge, Forge; CurseForge manifest, Modrinth
`.mrpack` and MultiMC/Prism packs import, plus mrpack / CurseForge export.
- **Java management**: a Temurin → Oracle → Microsoft → Zulu fallback chain, region-aware.
- **Accounts**: Microsoft sign-in over OAuth 2.0 device code flow → Xbox Live → XSTS →
`api.minecraftservices.com`; third-party community auth servers; local offline profiles for
isolated testing. Account tokens are kept on this machine only. **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. Without one, sign-in
stops before any network call and the alert says 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, the full reason only
in the launcher log. Community auth servers and local profiles need no id.
- **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).
*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)).*
## Status
## What works today
`v0.1.0` (2026-11-01) predates the UI rewrite: its assets are Slint builds, and it stays the last
Slint tag. `v0.2.0` is the first tag after the rewrite — the iced line, which is what the release
pipeline builds now. Everything described below lives on that line.
### Instances, versions and launching
- **What the iced UI has**: instances (list-detail), version management (three-column rail layout
with search), content library (modpacks / mods / resource packs / shaders with source and version
filters), logs, settings (general / appearance / Java / downloads), accounts (Microsoft device
code, third-party community servers, local profiles), the AI rail and the installer flows — all on
one component library and one token set. The page list is the one in `presenter::router::Route` —
instances, version management, content library, logs, settings, accounts, instance detail, the AI
page; `pages::Screen::ALL`, the smaller set that has a slug (`--screen=` and the page test matrix,
`crates/app-iced/src/pages/tests.rs`), leaves out accounts, instance detail and the AI page, which
are routed but opened from the nav rail or an instance tile.
- **Localisation follows the system language**: detection, an explicit override in settings, and
persistence across restarts. UI copy goes through one message catalogue (`crates/i18n`).
- **What the copy guard catches, and what it does not**: a test reads the instances page sources and
fails on any string literal that contains an ASCII word unless it is on a short whitelist, so a
newly hard-coded English string turns red as it lands
(`crates/app-iced/src/pages/instances/tests/i18n.rs`). Known boundary: that scan covers the
instances page rather than every screen, and a hard-coded Chinese literal has no ASCII word to
match, so nothing catches it — that class relies on review and the screenshot run.
- **Appearance**: window background image, per-panel transparency and whole-window transparency
(Windows), light / dark / follow-system, and system window corners where the platform grants them.
On CPU rendering the translucent layers are switched off and the UI says so.
- **Packaging and the installer paths** (curseforge / modrinth / multimc import, mrpack and
CurseForge export, install options with loader selection and real cancellation) are in the build.
- **What is not done yet**: installers / auto-update channel; macOS re-verification on a real
machine after the Windows pass; per-file conflict policy on pack import (the import pipeline has one
global policy today, and the UI states that rather than pretending otherwise); a native file
chooser (import accepts drag-and-drop and a pasted path).
- **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.
Binaries you may run into, so you can tell them apart:
### Mod loaders, packs and the content library
| Binary | Crate | State |
|--------|-------|-------|
| `unlucky-launcher-iced` | `crates/app-iced` (iced 0.14) | the GUI binary the workspace builds; the release pipeline stages the file as `unlucky-launcher_<version>_<platform>_<arch>` — a bare `.exe` on the two Windows slots, a `.tar.gz` on the four Linux / macOS slots — and the binary inside each archive is named `unlucky-launcher`; the same binary answers every CLI subcommand |
| `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 |
- **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`).
The GUI binary and `ull` are not duplicates. An archive extracts the first as `unlucky-launcher`; on
Windows it is a bare `unlucky-launcher_<version>_<platform>_<arch>.exe`. 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. That floor is glibc ≥ 2.35 (the Linux slots build on Ubuntu 22.04), so an older distribution —
and Android/Termux, which is not a released slot at all — needs a source build
(`packaging/README.md` §4, §9). 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, and nothing else (`crates/cli/src/cli/program.rs`). So
`ull` prints `ull ·`, the `unlucky-launcher` inside an archive prints `unlucky-launcher ·`, a local
`cargo build` prints `unlucky-launcher-iced ·`, and the bare Windows asset, whose file name keeps the
version segment, prints `unlucky-launcher_<version>_windows_x64 ·`.
### Java
### Subcommand dispatch
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.
`unlucky-launcher <args>` first asks "is this a CLI call?" before any UI code runs:
### 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 (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
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 rule is deliberately narrow, so nothing that used to work changed meaning:
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
@@ -123,46 +150,92 @@ The rule is deliberately narrow, so nothing that used to work changed meaning:
`--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 the GUI binary and `ull` accept the same set by construction.
**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`.
## Module map
### Not done yet
```
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 # 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
```
This list is the reason to believe the rest. None of it is hidden behind marketing copy.
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 the guards enforce it.
- **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.
The eight modules of `crates/core`:
## Download and run
| 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 |
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()`:
## Building
| 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:
@@ -177,84 +250,72 @@ 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)
```
`--workspace` covers the 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 `iced` in this name is cargo's, not the download's: renaming the bin
is what the "flip" still owes. The release file name carries no implementation detail — the
pipeline builds this bin and `packaging/make_artifacts.py` stages it as
`unlucky-launcher_<version>_<platform>_<arch>` — a bare `.exe` on the two Windows slots, a
`.tar.gz` on the four Linux / macOS slots — with the binary inside each archive named
`unlucky-launcher`, so download links never carry `iced`.
- `unlucky-launcher` — **historical**: no workspace build produces this binary any more; it belonged
to `crates/app`, which is excluded. The v0.1.0 assets are the last builds under this name.
- `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, which is the acceptance channel CI's headless
`--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
- **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.
**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: renders one real frame, then asserts the PNG is not 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 license and source guard |
| `.github/workflows/release.yml` | tag `v*` → per-slot builds → single-file artifacts + `SHA256SUMS.txt` → GitHub Release |
| `.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 |
## Platform baseline
## Architecture and module map
Minimum baseline: **Windows 10**; Windows 8.1 / 8 / 7 are not supported.
```
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
```
- 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: no released asset targets it (build from source
there), and the GUI is unverified
- Each platform runs its own test matrix; capability differences use feature gates with runtime
probing.
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).
- **Slint** (<https://slint.dev>), Royalty-Free 2.0: obligation for the Slint build that `v0.1.0`
ships — the condition attaches to each distribution of an Application, not to a version number.
Upstream §2 offers two forms: (a) the official `AboutSlint` widget 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 the `AboutSlint` widget** (`crates/app/ui/pages/settings.slint`, reachable from the
sidebar); whether that satisfies §2(a), and the fact that the `v0.1.0` release page carries a text
link rather than the §2(b) badge, are **not judged here** — Slint's own licence text is the
authority. The `v0.1.0` NOTICE, 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 the `v0.1.0` assets (and to any later build that
links Slint again), not to new iced builds — `crates/app` is 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/design` are a Rust
+214 -162
View File
@@ -2,114 +2,237 @@
[English](README.md) · **简体中文**
> **官方项目主页**:https://github.com/AskaEth/Unlucky-Launcher | 下载、议题、发布说明都在这里:
> **官方项目主页**:<https://github.com/AskaEth/Unlucky-Launcher> | 下载、议题、发布说明都在这里:
> <https://github.com/AskaEth/Unlucky-Launcher/releases>
Unlucky Launcher(ULL)是一个用 Rust 写的免费开源 Minecraft: Java 版启动器(Apache-2.0)。
每个平台就一个原生二进制——没有安装包、没有捆绑运行时、没有 WebView。启动器内置 AI Agent
客户端(对话 / 改配置 / 崩溃诊断),也能作为 MCP server 接进别的 Agent 工具。
Unlucky Launcher 是一个用 Rust 写的免费开源 **Minecraft: Java 版**启动器(Apache-2.0)。每个平台就是
**一个原生二进制**——没有安装包、没有捆绑运行时、没有 WebView——从装一个版本到读懂一次崩溃,它把启动器
该做的事都覆盖了:实例与版本、加载器与整合包、Mod / 资源包 / 光影、逐实例 Java、微软 / 第三方社区服 /
本地离线账户,以及一套在本地按 YAML 规则跑的崩溃诊断。你的数据不经过我们自己的任何服务:账户令牌只
留在本机。
## 特性
![实例页:左侧是本地实例列表,右侧是所选实例的版本、加载器、隔离方式与上次游玩时间](docs/images/instances-en-light.png)
- **单文件原生二进制**:启动器本体不需要 JVM、不需要 WebView、不带旁挂运行时。游戏要的 JDK 由
启动器按实例下载管理,不是启动器的依赖。
- **一壳两形**:GUI 二进制在跑任何 UI 代码之前就认得全部 CLI 子命令——不开窗、不探渲染器、不扫字体。
另有一份更轻的 `ull` 产物,命令集相同,却一个图形栈都不链接。
- **默认构建里只有一套 UI**:UI 是 iced 0.14 + Material 3 Expressive。早先的 Slint 实现
(`crates/app`)作为**历史参考**保留源码:已被 workspace `exclude`,默认构建不编、测试不跑、
发布不发。`v0.1.0` 那个 tag 发的仍是 Slint 版;iced 重写后的第一个 tag 是 `v0.2.0`(见「当前状态」)。
- **CPU 软渲染兜底**:`--software-renderer` 让没有可用 GPU 或驱动有问题的老机也能正常显示界面。
- **AI Agent + MCP**:启动器内可对话、改设置、读崩溃诊断;`ull agent serve` 把同一个内核以 MCP
server 形态(stdio 或 Streamable HTTP)开放给外部 Agent 工具。
- **加载器**:Fabric / Quilt / NeoForge / Forge;CurseForge 清单、Modrinth `.mrpack`、
MultiMC/Prism 整合包导入,mrpack / CurseForge 导出。
- **Java 管理**:Temurin → Oracle → Microsoft → Zulu 回退链,带地区判定。
- **账户**:三轨——Microsoft 账户登录(OAuth 2.0 device code flow → Xbox Live → XSTS →
`api.minecraftservices.com`)、第三方社区服、本地离线档案;账户令牌只保存在本机。**微软登录需要
一个 Azure 公共客户端 id,而发布版二进制里没有**——这个 id 不在仓库里,CI 也不注入,所以启动前设
`ULL_MSA_CLIENT_ID=<id>`(或构建时注入)才会有。没配时点登录**不会联网**,弹窗直接说清是这一项
没配:标题「微软登录未配置」,正文点名 `ULL_MSA_CLIENT_ID=<id>`(设好或构建时注入后要
**重启启动器**——已在运行的进程读不到新设的环境变量),并带 `代码:msa_not_configured`;它
**不给重试按钮**,因为在同一个进程里重试必然是同一个错。其它登录失败仍是老样子:一句分类文案 +
`代码:<码>`,完整原因只在启动器日志里。第三方社区服与本地档案不需要 id。
- **引擎**:多源下载(健康度评分、镜像、分块传输、边下边算)与 YAML 规则引擎的崩溃诊断。
- **许可**:Apache-2.0 © AskaEth (YeiJ)。
*实例页——1180×760,英文界面,浅色档。实例名是测试数据,不是谁的真实存档。图里那句
`Launch failed · exit0 静默失败` 也正好是一处已知缺口的诚实样子:那行原因仍是启动层直接抛出来的中文
原文(见[还没做](#还没做))。*
## 当前状态
## 现在能用什么
`v0.1.0`(2026-11-01)早于 UI 重写:那批资产是 Slint 版,它也是最后一个 Slint 版 tag。重写之后的
第一个 tag 是 `v0.2.0`——也就是 iced 这条线,下面说的这套东西都在线上,也正是发布流水线现在会打
出来的东西。
### 实例、版本与启动
- **iced 版界面上有什么**:实例、版本管理(三栏 + 搜索)、内容库(整合包 / Mod / 资源包 / 光影,
带来源与版本筛选)、日志、设置(通用 / 外观 / Java / 下载)、账户(微软设备码、第三方社区服、本地
档案)、AI 右栏,以及安装流程——共用一套组件库与一套令牌。页面清单就是 `presenter::router::Route`
里的那些——实例、版本管理、内容库、日志、设置、账户、实例详情、AI 页;`pages::Screen::ALL` 是其中
带 slug 的较小一组(`--screen=` 与页面测试矩阵用它,`crates/app-iced/src/pages/tests.rs`),账户、
实例详情与 AI 页不在这一组里——它们能路由到,但入口是导航轨与实例磁贴。
- **文案跟随系统语言**:自动探测、设置里可显式切换、重启后保持。界面文案统一走一份消息目录
(`crates/i18n`)。
- **文案守门抓得住什么、抓不住什么**:有一条测试扫实例页的源码,凡是**带 ASCII 单词**的字符串字面量
不在短白名单里就红——新写一句英文界面文案,落地即红
(`crates/app-iced/src/pages/instances/tests/i18n.rs`)。已知边界:这条扫描只覆盖实例页,不是每一屏;
而且中文写死的字面量里没有 ASCII 单词,扫不到——这一类只能靠 review 与出图。
- **外观**:窗口背景图、面板透明度、整窗透明度(Windows)、浅色 / 深色 / 跟随系统,以及在系统愿意给
的时候使用系统窗口圆角。CPU 渲染档会关掉半透明层,并在界面上说清楚。
- **整合包与安装链路都在构建里**:CurseForge 清单 / Modrinth `.mrpack` / MultiMC·Prism 导入,mrpack
与 CurseForge 导出,安装选项(选加载器)与**真取消**。
- **还没做**:安装器与自动更新通道;Windows 定型后的 macOS 真机复核;整合包导入的**逐文件**冲突策略
(当前只有一份全局策略,界面如实写明而不是假装支持);原生文件选择框(导入支持拖拽与粘贴路径)。
- **Slint 版已退出构建,只留源码作参考**:`crates/app` 不再是 workspace 成员(M118)——源码留作历史
参考,不编译、不测试、不发布,也不再有可用的构建方式。还欠「翻牌」的只有**仓库内的 bin 名**(现在叫
`unlucky-launcher-iced`);**产物名不欠**:`packaging/make_artifacts.py` 落盘时用的是
`unlucky-launcher_<版本>_<平台>_<架构>`——两个 Windows 槽是裸 `.exe`,其余四个 Linux / macOS 槽
是 `.tar.gz`——归档里的二进制名是 `unlucky-launcher`。
- **实例**——list-detail 版式:左侧是本地实例列表;右侧是所选实例的版本、加载器、隔离方式(独立游戏
目录或共享目录)、文件是否在盘上、上次游玩时间,以及 Play / Stop。窄窗是把两个面板整页切换,不是上下堆。
- **实例从哪来**——装一个版本(带实例名、加载器与加载器版本),或者导入一个整合包。实例页自己的
**新建实例 / 导入 / 复制 / 打开文件夹 / 删除** 五个按钮**还没接后端**:它们是占位动作,点了只往
stderr 写一行(`crates/app-iced/src/app/update/page_messages.rs`),欠的是实例向导
(`crates/app-iced/src/pages/mod.rs` 偏离 #8)。
- **版本管理**——三栏(版本类型 / 主版本 / 搜索)读 Mojang 版本清单:正式版、快照、远古版;安装时可选
加载器与加载器版本。命令行还能只出计划不下载(`ull install --dry-run`:文件清单、总大小、每个文件的
选用源)。
- **启动**——10 阶段命令行组装(`crates/core/src/launch/`)、逐实例 JVM 与游戏参数、原生库解压落位,
每次运行留一份 JSONL 会话记录。进程按**进程树**杀,不是只杀一个 PID。
- **崩溃诊断是真的、也是本地的**——`crates/core/src/diagnose` 采五类信号(退出码、`latest.log`、
`crash-reports`、`hs_err_pid`、含 mod 清单的环境快照),按 YAML 规则集匹配,把结论与修复动作显示在
日志页上。不接模型、不联网:断网能用,没有 GPU 的机器也能用。
可能会遇到的 bin,先认清楚:
### 加载器、整合包与内容库
| 二进制 | crate | 状态 |
|--------|-------|------|
| `unlucky-launcher-iced` | `crates/app-iced`(iced 0.14) | workspace 里**唯一**的 GUI bin;发布流水线把文件落成 `unlucky-launcher_<版本>_<平台>_<架构>`(两个 Windows 槽是裸 `.exe`,其余四个 Linux / macOS 槽是 `.tar.gz`),归档里的二进制名是 `unlucky-launcher`;子命令分发同款 |
| `unlucky-launcher` | `crates/app`(Slint) | **历史**:该 crate 已被 workspace `exclude`,这个 bin 不再被构建或分发;`v0.1.0` 的资产是最后一批 Slint 版 |
| `ull` | `crates/cli` | 同一套命令的零 UI 构建,作为独立资产随发布分发 |
- **加载器**——Fabric、Quilt、NeoForge、Forge。需要跑官方 installer `processors` 的 Forge/NeoForge 版本
会真跑一遍;机器上没有 Java 时先装一个托管 JDK。
- **整合包**——导入 CurseForge 清单、Modrinth `.mrpack`、MultiMC/Prism 实例;导出为 `.mrpack` 或
CurseForge 清单。安装是真取消,不是「等这一批文件下完再说」。
- **内容库**——整合包 / Mod / 资源包 / 光影,带关键词、来源、版本、加载器与排序筛选,分页结果,
以及一键**装入这个实例**。
- **这一页会拒绝什么,就写在那一行上**——整合包**不能**从内容库直接装(整合包走上面的导入管线);
CurseForge 的「装入」是禁用态,因为它的文件端点还没接通;游戏版本或加载器对不上目标实例的条目也是
禁用态,并把不匹配写清楚。没有一条是静默失败的。
- **CurseForge 检索要 key**——启动前设 `ULL_CF_API_KEY`,没有它这个来源在来源筛选里是灰的。key 只活在
环境变量里:不写进配置文件、不进日志(`crates/presenter/src/host/addon.rs`)。
GUI 二进制与 `ull` 不是重复品。归档解压出来那个二进制叫 `unlucky-launcher`;Windows 上是裸的
`unlucky-launcher_<版本>_<平台>_<架构>.exe`。开窗时 GUI 二进制要用图形栈(X11 / Wayland / GL 走
`dlopen`,启动时直接链接 `libfontconfig`);`ull` 除了 glibc 什么都不需要,所以在没有窗口的服务器、
CI 容器上跑得起来。这个地板是 glibc ≥ 2.35(Linux 槽位在 Ubuntu 22.04 上构建),更老的发行版——以及
根本不在发布矩阵里的 Android/Termux——得源码自建(`packaging/README.md` §4、§9)。没有桌面就用 `ull`,
有桌面就用 `unlucky-launcher <子命令>`。两者打出来的 CLI 文案是同一套,自称的名字跟着**你敲的那个
文件**走——`argv[0]` 去掉目录与结尾的 `.exe`,别的都不剥(`crates/cli/src/cli/program.rs`)。所以
`ull` 打印 `ull ·`,归档里那个 `unlucky-launcher` 打印 `unlucky-launcher ·`,本地 `cargo build` 出来
的打印 `unlucky-launcher-iced ·`,而 Windows 的裸 exe 文件名带版本段,打印的是
`unlucky-launcher_<版本>_windows_x64 ·`。
### Java
### 子命令分发
逐实例的托管 JDK,四家发行版的回退链**按地区排**——Temurin 打头,国内是 Oracle 先于 Microsoft
(那边实测快 28×)、国外反过来,Zulu 收尾——可用 `java.vendorPriority` 设置整体覆盖
(`crates/core/src/game/java/vendor.rs`)。镜像也按地区选。`java list` 把它真能找到的每个 JDK 都列出来
(包括官方启动器自己下的那些),`java install <主版本>` 拉一个。游戏要的 JDK 是启动器的数据,不是启动器
本身的依赖。
`unlucky-launcher <参数>` 在跑任何 UI 代码之前先问一句「这是不是一次 CLI 调用」:
### 账户
三条轨道在同一页:
- **微软**——OAuth 2.0 设备码流程 → Xbox Live → XSTS → `api.minecraftservices.com`。
- **第三方社区认证服**(authlib-injector 一类)。
- **本地离线档案**,用于隔离测试。
**微软登录需要一个 Azure 公共客户端 id,而发布版二进制里没有。** 这个 id 不在仓库里,CI 也不注入,所以
启动前设 `ULL_MSA_CLIENT_ID=<id>`(或构建时注入:`ULL_MSA_CLIENT_ID=<id> cargo build`,它通过
`option_env!` 烧进二进制)。没有它时点登录**不会联网**,弹窗直接说清是这一项没配:标题「微软登录未配置」,
正文点名 `ULL_MSA_CLIENT_ID=<id>`(设好或构建时注入后要**重启启动器**——已在运行的进程读不到新设的环境
变量),并带 `代码:msa_not_configured`;它**不给重试按钮**,因为在同一个进程里重试必然是同一个错。其它
登录失败仍是老样子:一句分类文案 + `代码:<码>`,完整原因只在启动器日志里。第三方社区服与本地档案不需要 id。
### 外观与语言
- **只有一套 UI 栈**——iced 0.14 + Material 3 Expressive,自绘标题栏、带动效的导航轨,浅色 / 深色 /
跟随系统。
- **外观**——窗口背景图与取色链路(面板配色可从壁纸推出来)、面板透明度、整窗透明度(Windows)。Windows 11
与 Server 2025 上向系统请求外框圆角;macOS 与 Linux 交给桌面合成器自己的规则;我们不自己画圆角。
- **CPU 兜底**——`--software-renderer` 让没有可用 GPU 或驱动有问题的机器照样正常显示;没有可用适配器时,
后端探测自己退到 CPU。这条路上**面板级**半透明与模糊会关掉(**窗口级**透明度照常可用),设置面板会写明。
- **本地化**——跟随系统语言探测、设置里可显式切换、重启后保持。绝大多数文案来自同一份消息目录
(`crates/i18n`)。**边界,因为上面这句很容易被读大**:防硬编码的守门
(`crates/app-iced/src/pages/instances/tests/i18n.rs`)只扫实例页源码,不是每一屏;写死的中文字面量里
没有 ASCII 单词可以匹配,这一类它抓不到;外观面板的老控件还是硬编码英文
(`crates/app-iced/src/pages/appearance/section.rs`),中文界面下它们仍显示英文。
### 助手,以及 MCP server
这是两件不同的事,README 不该含糊:
- **启动器内的助手今天还是占位壳。** 它的 transport 是脚本化的
(`crates/app-iced/src/presenter/transport.rs`),没有接真实模型,而且界面上它出现的每一屏都用四处明说
这一点——副标题、输入框占位符、空态正文,以及一枚「模拟」胶囊。会话状态、未读数、草稿、确认流程与具名
失败码都是真的 presenter 状态;缺的只是它们后面的模型。
- **MCP server 是真的。** `ull agent serve` 把启动器内核以 MCP server 形态(stdio 或 Streamable HTTP)
开放给外部 Agent 工具,只监听回环(`127.0.0.1`,默认端口 47823),除非
`agent.mcp.allow_write=true` 否则只读。十个工具规格(`crates/agent/src/tools/`)同时喂给两种形态,两条
路都过同一个执行器——schema 校验、敏感分级、审计留痕。`ull agent config` 能打印现成的 Claude Code /
Cursor 配置片段。
### 命令行
GUI 二进制在跑任何 UI 代码之前就先认全部 CLI 子命令——不开窗、不探渲染器、不扫字体。另有一份更轻的 `ull`
产物,命令集相同,却一个图形栈都不链接。
```bash
unlucky-launcher # GUI
unlucky-launcher --software-renderer # GUI,CPU 渲染(行为不变)
unlucky-launcher instances list # CLI,不开窗
unlucky-launcher --json launch 1.20.1 # CLI,全局选项排在子命令前面
unlucky-launcher --version # CLI,输出版本 + 系统摘要
unlucky-launcher # GUI
unlucky-launcher --software-renderer # GUI,CPU 渲染
unlucky-launcher instances list # CLI,不开窗
unlucky-launcher --json launch 1.20.1 # CLI,全局选项排在子命令前面
unlucky-launcher --version # CLI,版本 + 系统摘要
```
判据刻意收得很窄,所以原本能用的用法一条都没变:
- 第一个参数是裸词(不以 `-` 开头)→ 它就在子命令的位置上,交给 CLI。GUI 的 flag 永远不会出现在
那个位置:`--screenshot` / `--diff` / `--data-dir` 的取值都排在 flag 之后。
- 第一个参数是 flag → 只有当 `crates/cli` 的解析器也解析出一条真命令时才算 CLI 调用
(`--help`、`--version`、`--json instances list`)。单独 `--data-dir X` 解析成「没有子命令」,
仍然开 GUI;GUI 专属参数(`--software-renderer`、`--screenshot*`、`--renderer`、`--theme`、
`--diff`、`--force-state` 等)在 CLI 里一律解析失败,也仍然开 GUI。
- 第一个参数是裸词(不以 `-` 开头)→ 它就在子命令的位置上,交给 CLI。GUI 的 flag 永远不会出现在那个位置:
`--screenshot` / `--diff` / `--data-dir` 的取值都排在 flag 之后。
- 第一个参数是 flag → 只有当 `crates/cli` 的解析器也解析出一条真命令时才算 CLI 调用(`--help`、
`--version`、`--json instances list`)。单独 `--data-dir X` 解析成「没有子命令」,仍然开 GUI;GUI 专属
参数(`--software-renderer`、`--screenshot*`、`--renderer`、`--theme`、`--diff`、`--force-state` 等)
在 CLI 里一律解析失败,也仍然开 GUI。
**未知子命令是错误,不是开窗**:`unlucky-launcher lst` 打印「未知命令 `lst`」、提示
`unlucky-launcher --help`,以 1 退出。静默开窗会让脚本以为自己那个拼错的参数生效了。子命令表只有
一处定义(`crates/cli/src/cli/parse.rs`),所以 GUI 二进制和 `ull` 认的命令集天然一致。
`unlucky-launcher --help`,以 1 退出。静默开窗会让脚本以为自己那个拼错的参数生效了。子命令表只有一处定义
(`crates/cli/src/cli/parse.rs`),所以两个二进制认的命令集天然一致。`ull --help` 会全列出来:`instances`、
`install`、`launch`、`pack import`、`accounts`、`sessions`、`logs`、`diag`、`config`、`java`、`agent`、
`version`、`help`。
## 模块地图
### 还没做
这份清单是让人相信上面那些话的理由。没有一条藏在宣传话术后面。
- **没有安装包,也没有自动更新通道。** 下个文件直接跑,没有任何东西替你检查新版本,更新源也还没定。
- **有一批按钮是占位。** 实例页:*新建实例*、*导入*、*复制*、*打开文件夹*、*删除*。日志页:*打开文件夹*。
设置页框架:*保存*、*重置本组*、*添加覆盖*。点下去只往 stderr 写一行 `[page] 占位动作 …`,什么都不改
——图形界面里的实例管理就是还欠着的那块实例向导;设置项是边改边生效,而不是靠「保存」。
- **Linux 与 macOS 产物未验证。** 参考机是 Windows;`linux-x64`、`linux-arm64`、`macos-arm64`、
`macos-x64` 四份归档由 CI 矩阵产出,还没被下载下来端到端跑过。把它们当**未经测试的产出**,而不是
「矩阵里有就算支持」。
- **macOS 未定型。** 有两轮真机证据(Apple Silicon),构建因此存在——但菜单栏显示名、三个窗口按钮的位置
(macOS 习惯在左上,我们自绘在右上)、300s+ 稳态仍需真机复核。macOS 产物未签名,首次启动要自己清掉
Gatekeeper 隔离标记(`xattr -d com.apple.quarantine`)。macOS 运行期冒烟是手动 `workflow_dispatch`
的探针、带 `continue-on-error`,不是门禁。
- **内容库页在低窗口高度下会挤**,而且窗口高度还没接进页面模型——这一页按一个名义高度排版,而不是按它
真正拿到的高度。
- **启动侧失败提示未结构化。** 磁贴直接采用启动层抛出的中文句子
(`crates/core/src/launch/exit_analysis.rs`),而不是一个界面能翻译的原因码——上面那张英文截图里的
`静默失败` 就是它。
- **启动器内的助手背后没有模型**(见上)。MCP server 有。
- **整合包导入只有两份全局策略**:保留用户侧或覆盖,不是逐文件选择;导入报告会逐条列出冲突,所以没有
文件是被静默覆盖的。
- **内容库的安装进度是阶段级**,不是字节级。
- **没有原生文件选择框。** 导入靠拖拽或粘贴路径。
- **Android/Termux 是试验级、不承诺**:发布矩阵里没有面向它的资产,那一侧的 GUI 也未验证。
## 下载与运行
从 [Releases](https://github.com/AskaEth/Unlucky-Launcher/releases) 拿你那个平台的文件。文件名形如
`unlucky-launcher_<版本>_<平台>_<架构>`;这个形状的唯一真相源是
`packaging/make_artifacts.py::asset_stem()`:
| 槽位 | 产物 | 形态 |
|------|------|------|
| `windows-x64` | `unlucky-launcher_<版本>_windows_x64.exe` | 裸 `.exe`,双击即用 |
| `windows-arm64` | `unlucky-launcher_<版本>_windows_arm64.exe` | 裸 `.exe` |
| `linux-x64` | `unlucky-launcher_<版本>_linux_x64.tar.gz` | 单个归档,解压出一个顶层目录 |
| `linux-arm64` | `unlucky-launcher_<版本>_linux_arm64.tar.gz` | 同上 |
| `macos-arm64` | `unlucky-launcher_<版本>_macos_arm64.tar.gz` | 同上 |
| `macos-x64` | `unlucky-launcher_<版本>_macos_x64.tar.gz` | 同上 |
| 每个槽位 | `ull_<版本>_<平台>_<架构>[.exe]` | 裸 CLI 文件,独立资产 |
| 全平台 | `SHA256SUMS.txt` | `sha256sum -c` 可直吃 |
UNIX 归档里是二进制加 `LICENSE`、`NOTICE`、`README.md`,都在一个顶层目录下。Windows 发的是裸 `.exe`
(一个文件装不下这三份文本),所以这三份作为并列资产挂在 Release 上——下 Windows 二进制不会把许可条款
弄丢。下载链接里不带实现细节字样,归档里的二进制名永远是 `unlucky-launcher`。
这两种形态不是重复品。GUI 二进制开窗时要用图形栈(X11 / Wayland / GL 走 `dlopen`,启动时直接链接
`libfontconfig`);`ull` 除了 glibc 什么都不需要,所以在没有窗口的服务器、CI 容器上跑得起来。Linux 上这个
地板是 glibc ≥ 2.35(槽位在 Ubuntu 22.04 上构建)——更老的发行版得源码自建(`packaging/README.md` §4)。
没有桌面就用 `ull`,有桌面就用 `unlucky-launcher <子命令>`。两者打出来的 CLI 文案是同一套,自称的名字跟着
**你敲的那个文件**走——`argv[0]` 去掉目录与结尾的 `.exe`,别的都不剥
(`crates/cli/src/cli/program.rs`),所以 `ull` 打印 `ull ·`,归档里那个二进制打印
`unlucky-launcher ·`,本地 `cargo build` 出来的打印 `unlucky-launcher-iced ·`。
最低平台基线:**Windows 10**;Windows 8.1 / 8 / 7 不在支持范围内。macOS 12+(Apple Silicon 与 Intel),
Linux x64 / arm64(基线判定在 `crates/core/src/platform/os_info.rs`)。Windows 10/11 的产物由标准工具链
(MSVC / GNU)构建,构建方式与上游 cargo 一致。Linux 上 X11 是当前首选路径,Wayland 是大方向。能力差异
一律用 feature gate + 运行期探测表达,绝不做成「这个平台天生少一块」的编译期裁剪。
第一次用微软账户?先设 `ULL_MSA_CLIENT_ID`——见[账户](#账户)。其它都不需要配置。
## 从源码构建
前置:
- Rust stable(`rust-toolchain.toml` 会自动选工具链;门禁还需要 `rustfmt` / `clippy` 组件)
- Windows:MSVC 工具链(CI 用这套)或 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 编译期要用
```bash
cargo fmt --all --check # 格式
cargo clippy --workspace --all-targets -- -D warnings # 静态检查(零 warning 门禁)
cargo test --workspace # 测试
cargo build --release # bin 落在 target/release/
cargo deny check licenses bans sources # 依赖许可(CI 必跑)
```
编出来的 bin:
- `unlucky-launcher-iced`——GUI,同时也是 CLI:同一个文件认上面列的全部子命令。名字里的 `iced` 是 cargo
的 bin 名,不是下载名——`packaging/make_artifacts.py` 把同一次构建落成
`unlucky-launcher_<版本>_<平台>_<架构>`,归档里的二进制名写成 `unlucky-launcher`。
- `ull`——CLI 的独立无 UI 构建:同一套命令、不链接图形栈、体积更小,给完全没有显示设备的机器用。
渲染开关:
- `unlucky-launcher-iced --software-renderer` 强制 tiny-skia(`ICED_BACKEND=tiny-skia`);
`--screenshot <png>` 出一帧图后退出——这是 CI 无头冒烟 job 用的验收通道
- 没有可用适配器时,iced 版靠显式后端探测自己退到 CPU
**CI 门禁。** 每次推送到 `main` 与每个 PR 都跑 fmt / clippy(warnings 视为错误)/ 全量测试,Linux 无头
渲染冒烟与它们同级、是硬门禁:在 Xvfb 里真渲染一帧,再断言 PNG 非空白,红了就不许合。macOS 探针还不算
——只手动 dispatch,结果留证据但不作为流水线结论。
| Workflow | 内容 |
|----------|------|
| `.github/workflows/ci.yml` | fmt + clippy + test + release build(Windows / Linux 矩阵);Linux runtime smoke(Xvfb 真渲染一帧、断言 PNG 非空白);macOS 运行期探针(仅手动 dispatch) |
| `.github/workflows/guards.yml` | `crates/core` / `crates/presenter` / `crates/design` 零 UI 守门、单文件 ≤400 行守门、cargo-deny 许可与来源守门 |
| `.github/workflows/release.yml` | tag `v*` → 六槽位构建 → 单文件产物 + `SHA256SUMS.txt` → GitHub Release |
## 架构与模块地图
```
Unlucky-Launcher (cargo workspace)
@@ -120,13 +243,14 @@ Unlucky-Launcher (cargo workspace)
│ # —— 零 UI 依赖、零第三方 crate
├── crates/i18n # 语言探测、消息目录、中文字体策略
├── crates/palette-extract # 唯一解码图片的 crate,供壁纸取色链路使用
├── crates/app # 历史 Slint UI(bin `unlucky-launcher`):已被 workspace `exclude`,
│ # 仅作源码参考——不再被构建 / 测试 / 发布
├── crates/app-iced # iced UI(bin `unlucky-launcher-iced`):workspace 里唯一的 GUI bin
├── crates/cli # CLI(bin `ull`):headless,零 UI
└── packaging # 单文件产物矩阵与发布脚本
```
层间纪律:`app-iced` → `presenter` → `core` / `agent`。`design` 与 `i18n` 只被壳层与 presenter 消费;
它们都不得反过来依赖渲染框架,CI 守门会拦。
`crates/core` 的八个模块:
| 模块 | 职责 |
@@ -136,85 +260,13 @@ Unlucky-Launcher (cargo workspace)
| `game` | 版本/实例仓库、快照与继承解析 |
| `launch` | 10 阶段启动管线、进程、JSONL 事件流 |
| `auth` | Microsoft 账户登录(OAuth 2.0 device code → Xbox Live → XSTS → Minecraft Services)、第三方社区服、本地档案 |
| `addon` | 加载器安装、整合包、CurseForge / Modrinth |
| `addon` | 加载器安装、整合包、CurseForge / Modrinth、整合包导出 |
| `diagnose` | 五类信号采集、YAML 规则引擎、hs_err 解析 |
| `platform` | OS 抽象:路径、进程、文件锁、能力探测、运行期 OS 检测 |
## 构建
前置:
- Rust stable(`rust-toolchain.toml` 会自动选工具链;门禁还需要 `rustfmt` / `clippy` 组件)
- Windows:MSVC 工具链(CI 用这套)或 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 编译期要用(Slint 版当年还要 femtovg)
```bash
cargo fmt --all --check # 格式
cargo clippy --workspace --all-targets -- -D warnings # 静态检查(零 warning 门禁)
cargo test --workspace # 测试
cargo build --release # bin 落在 target/release/
```
`--workspace` 就是模块地图里那八个成员:`crates/app` 已在 workspace 层被排除(M118),上面这些命令
都不会编 Slint 那棵树——而且**它没有构建方式**:该 crate 只作源码参考,作为 workspace 成员已不可编。
编出来的 bin:
- `unlucky-launcher-iced`——GUI(iced)。它同时也是 CLI:同一个二进制认上面列的全部子命令。名字里的
`iced` 是 cargo 的,不是下载链接的:还欠「翻牌」的只有这个 bin 名;发布文件里不带实现细节——流水线编
这个 bin,`packaging/make_artifacts.py` 落盘时改成
`unlucky-launcher_<版本>_<平台>_<架构>`——两个 Windows 槽是裸 `.exe`,其余四个 Linux / macOS 槽
是 `.tar.gz`——归档里的二进制名是 `unlucky-launcher`。
- `unlucky-launcher`——**历史**:workspace 构建已不产出它(它属于被排除的 `crates/app`)。
`v0.1.0` 的资产是最后一批用这个名字发出去的构建。
- `ull`——CLI 的独立无 UI 构建:同一套命令、不链接图形栈、体积更小,给完全没有显示设备的机器用。
渲染开关:
- `unlucky-launcher-iced --software-renderer` 强制 tiny-skia(`ICED_BACKEND=tiny-skia`);
`--screenshot <png>` 出一帧图后退出,这是 CI 无头冒烟 job 用的验收通道
- 没有可用适配器时,iced 版靠显式后端探测自己退到 CPU
- **历史**(`v0.1.0` 的 Slint 版仍认这条):`unlucky-launcher --software-renderer`,即
`SLINT_BACKEND=winit-software`,让那一版走它自己的渲染器回退链
依赖许可检查(本地可选,CI 必跑):`cargo deny check licenses bans sources`。
## CI 门禁
每次推送到 `main` 与每个 PR 都跑 fmt / clippy(warnings 视为错误)/ 全量测试,Linux 无头渲染冒烟
与它们同级、是硬门禁:红了就不许合。macOS 运行期探针还不算——它只在手动 `workflow_dispatch` 时跑,
结果留证据但不作为流水线结论(`continue-on-error`),直到该槽位在真实 runner 上首跑通过。
| Workflow | 内容 |
|----------|------|
| `.github/workflows/ci.yml` | fmt + clippy + test + release build(Windows / Linux 矩阵);Linux runtime smoke(Xvfb 里真渲染一帧,再断言 PNG 非空白);macOS 运行期探针(仅手动 dispatch) |
| `.github/workflows/guards.yml` | `crates/core` 零 UI 守门(依赖表 + `cargo tree`)、单文件 ≤400 行守门、cargo-deny 许可与来源守门 |
| `.github/workflows/release.yml` | tag `v*` → 分槽位构建 → 单文件产物 + `SHA256SUMS.txt` → GitHub Release |
## 平台基线
最低基线 **Windows 10**;Windows 8.1 / 8 / 7 不在支持范围内。
- Windows 10/11 的产物由**标准工具链**(MSVC / GNU)构建,构建方式与上游 cargo 一致
- macOS 12+(Apple Silicon 与 Intel)与 Linux x64 / arm64 都是支持槽位;Linux 上 X11 是当前首选路径,
Wayland 是大方向。macOS 产物未签名,首次启动要自己清掉 Gatekeeper 隔离标记
(`xattr -d com.apple.quarantine`)
- Android Termux 试验级、不承诺:发布矩阵里没有面向它的资产(要在 Termux 上跑得源码自建),GUI 未验证
- 每个平台跑各自的测试矩阵;能力差异用 feature gate + 运行期探测表达。
## 许可证与致谢
- **本项目**:Apache License 2.0,见 [LICENSE](LICENSE);第三方声明见 [NOTICE](NOTICE)。
- **Slint**(<https://slint.dev>),Royalty-Free 2.0:义务针对 `v0.1.0` 发布的那批构建——条件挂在每一次
「随应用分发」上,而不是挂在某个版本号上。上游 §2 给出两种形式:(a) 在可从顶层菜单进入的「关于」页/
对话框里显示官方 `AboutSlint` 部件(没有该页时改为启动画面);(b) 在可下载该二进制的公开网页上显示
官方归属徽章。**`v0.1.0` 的设置页「关于」分组里内置了 `AboutSlint` 部件**(`crates/app/ui/pages/
settings.slint`,从侧栏进入);该部件是否满足 §2(a) 的全部条件、以及 `v0.1.0` 发布页上那是文字链接
而不是 §2(b) 点名的官方徽章图这两点,**本节不作法律判断**,具体以 Slint 官方许可原文为准。`v0.1.0`
的 NOTICE、发布页说明与本节**都只是记录,不构成履行声明**。后续版本不再链接 Slint,该义务不随新产物
新增。义务跟着**链接了 Slint 的产物**走:已发布的 `v0.1.0` 资产(以及将来任何重新链接 Slint 的构建)
适用;`crates/app` 已被 workspace `exclude`,流水线不再产出链接 Slint 的二进制,所以新的 iced 版不适用。
- **iced**(<https://github.com/iced-rs/iced>),MIT:现役 UI 的框架;与它同栈的 `winit`(Apache-2.0)、
以及经它引入的 `wgpu`(MIT / Apache-2.0)/ `tiny-skia`(BSD-3-Clause)。
- **Google Material Color Utilities**(Apache-2.0):`crates/design` 的色彩令牌是它 HCT /
Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB