- P2-1:`[.exe]` 只对两个 Windows 槽成立,四个 Linux/macOS 槽是 .tar.gz (make_artifacts.py:76-82 SLOTS、:243 stage_archive;packaging/README.md §1)。 三处产物名统一写成 `unlucky-launcher_<version>_<platform>_<arch>` + 按槽位说明形态。 - P2-2:删掉「`unlucky-launcher` once extracted or downloaded」与段尾「Windows 裸资产文件名带 版本段」的互相拆台;改成归档解出叫 `unlucky-launcher`、Windows 裸资产叫 `unlucky-launcher_<version>_<platform>_<arch>.exe`。中英同步。
267 lines
18 KiB
Markdown
267 lines
18 KiB
Markdown
# 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 (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 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` before launching, or build with it. Without one, pressing sign-in
|
||
only raises a generic "Sign-in failed" alert with `Code: other`; nothing on that screen tells you
|
||
the id is what is missing. 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).
|
||
|
||
## Status
|
||
|
||
`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.
|
||
|
||
- **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).
|
||
|
||
Binaries you may run into, so you can tell them apart:
|
||
|
||
| 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 |
|
||
|
||
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 ·`.
|
||
|
||
### Subcommand dispatch
|
||
|
||
`unlucky-launcher <args>` first asks "is this a CLI call?" before any UI code runs:
|
||
|
||
```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
|
||
```
|
||
|
||
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-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 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.
|
||
|
||
## 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 # 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
|
||
```
|
||
|
||
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.
|
||
|
||
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 |
|
||
|
||
## Building
|
||
|
||
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/
|
||
```
|
||
|
||
`--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.
|
||
- `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
|
||
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 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 |
|
||
|
||
## 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: 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.
|
||
|
||
## 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
|
||
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.
|