diff --git a/README.md b/README.md index ae9079d..e327847 100644 --- a/README.md +++ b/README.md @@ -2,117 +2,144 @@ **English** · [简体中文](README.zh-CN.md) -> **Official project home**: https://github.com/AskaEth/Unlucky-Launcher | downloads, issues and +> **Official project home**: | downloads, issues and > release notes all live there: -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=` 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=` (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___` — 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___.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 ` 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__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 ` fetches +one. The game's JDK is the launcher's data, not its own dependency. -`unlucky-launcher ` 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=` before launching, or build with +it (`ULL_MSA_CLIENT_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=` (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___`; the single source of truth for that shape is +`packaging/make_artifacts.py::asset_stem()`: -## Building +| Slot | Asset | Shape | +|------|-------|-------| +| `windows-x64` | `unlucky-launcher__windows_x64.exe` | bare `.exe`, double-click to run | +| `windows-arm64` | `unlucky-launcher__windows_arm64.exe` | bare `.exe` | +| `linux-x64` | `unlucky-launcher__linux_x64.tar.gz` | one archive, one top-level directory | +| `linux-arm64` | `unlucky-launcher__linux_arm64.tar.gz` | same | +| `macos-arm64` | `unlucky-launcher__macos_arm64.tar.gz` | same | +| `macos-x64` | `unlucky-launcher__macos_x64.tar.gz` | same | +| every slot | `ull___[.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 ` 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___` — 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___` 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 ` writes one frame and exits, which is the acceptance channel CI's headless + `--screenshot ` 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** (), 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** (), 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 diff --git a/README.zh-CN.md b/README.zh-CN.md index 55d2194..8b1d78a 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -2,114 +2,237 @@ [English](README.md) · **简体中文** -> **官方项目主页**:https://github.com/AskaEth/Unlucky-Launcher | 下载、议题、发布说明都在这里: +> **官方项目主页**: | 下载、议题、发布说明都在这里: > -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=`(或构建时注入)才会有。没配时点登录**不会联网**,弹窗直接说清是这一项 - 没配:标题「微软登录未配置」,正文点名 `ULL_MSA_CLIENT_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=`(或构建时注入:`ULL_MSA_CLIENT_ID= cargo build`,它通过 +`option_env!` 烧进二进制)。没有它时点登录**不会联网**,弹窗直接说清是这一项没配:标题「微软登录未配置」, +正文点名 `ULL_MSA_CLIENT_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 ` 出一帧图后退出——这是 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 ` 出一帧图后退出,这是 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**(),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**(),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 / diff --git a/docs/images/instances-en-light.png b/docs/images/instances-en-light.png new file mode 100644 index 0000000..ef98929 Binary files /dev/null and b/docs/images/instances-en-light.png differ