Unlucky Launcher v0.0.1-alpha
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent), MCP server 接入外部 Agent 工具,GUI 开发中。
This commit is contained in:
commit
bc41dfcf7d
517 files changed
+92844
No files matched your search
@@ -0,0 +1,178 @@
|
||||
//! 游戏版本号解析与比较(供命令行组装的分支门限使用)。
|
||||
//!
|
||||
//! 依据 `analyse/02` §5.2(版本号驱动的分支:legacy/modern 参数模板、log4j 注入、
|
||||
//! `--quickPlay*` 门限、`--graphicsBackend` 门限)。
|
||||
//!
|
||||
//! 说明:这是 launch 侧的最小比较器。若 M14(game)后续提供权威 `GameVersion` 比较器,
|
||||
//! 只需替换本文件内部实现,`is_at_least` / `is_before` 的调用点不用动
|
||||
//! (见 `DevDocs/SubAgentsTalk/launch-接口约定.md` §7-3)。
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
|
||||
/// 版本的发布阶段。派生 `Ord` 的变体顺序即排序顺序:Snapshot < PreRelease < ReleaseCandidate < Release。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Stage {
|
||||
/// 快照(`26.2-snapshot-2`),数字为快照序号。
|
||||
Snapshot(u32),
|
||||
/// 预发布(`1.20-pre1`)。
|
||||
PreRelease(u32),
|
||||
/// 候选发布(`1.20-rc1`)。
|
||||
ReleaseCandidate(u32),
|
||||
/// 正式版。
|
||||
Release,
|
||||
}
|
||||
|
||||
/// 游戏版本号。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GameVersion {
|
||||
raw: String,
|
||||
parts: Vec<u32>,
|
||||
stage: Stage,
|
||||
}
|
||||
|
||||
impl GameVersion {
|
||||
/// 解析版本串;无法识别(如 `20w14a`)时返回 [`GameVersion::unknown`]。
|
||||
pub fn parse(raw: &str) -> Self {
|
||||
let trimmed = raw.trim();
|
||||
let (base, stage) = split_stage(trimmed);
|
||||
let parts = base
|
||||
.split('.')
|
||||
.map_while(|piece| piece.parse::<u32>().ok())
|
||||
.collect::<Vec<_>>();
|
||||
if parts.is_empty() {
|
||||
return Self::unknown(trimmed);
|
||||
}
|
||||
Self {
|
||||
raw: trimmed.to_string(),
|
||||
parts,
|
||||
stage,
|
||||
}
|
||||
}
|
||||
|
||||
/// 未知/无法解析的版本(比较结果为“不满足任何门限”,由调用方给默认分支)。
|
||||
pub fn unknown(raw: &str) -> Self {
|
||||
Self {
|
||||
raw: raw.to_string(),
|
||||
parts: Vec::new(),
|
||||
stage: Stage::Release,
|
||||
}
|
||||
}
|
||||
|
||||
/// 原始版本串。
|
||||
pub fn raw(&self) -> &str {
|
||||
&self.raw
|
||||
}
|
||||
|
||||
/// 是否解析出了数字版本(`false` 表示未知形状,例如周快照)。
|
||||
pub fn is_known(&self) -> bool {
|
||||
!self.parts.is_empty()
|
||||
}
|
||||
|
||||
/// 是否是**周快照**形态(`20w14a` / `26w14a`)。
|
||||
///
|
||||
/// 周快照是「年(2 位) + `w` + 周(2 位) + 字母」的独立命名族,与 `26.2-snapshot-N`
|
||||
/// 分属两套编号,**无法**可靠排序;所以 [`Self::is_known`] 对它恒为 `false`
|
||||
/// (不猜,免得误触发「老版本」分支去套 legacy 参数模板)。要判能力请改看版本 JSON 的
|
||||
/// 库表([`crate::game::capability`]),别在这里编一套排序规则。
|
||||
pub fn is_weekly_snapshot(&self) -> bool {
|
||||
is_weekly_snapshot_str(&self.raw)
|
||||
}
|
||||
|
||||
/// 主版本号(`1.20.1` → 1,`26.2` → 26)。
|
||||
pub fn major(&self) -> u32 {
|
||||
self.parts.first().copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
/// 是否 `>= other`。自身或 `other` 不可解析时返回 `false`。
|
||||
pub fn is_at_least(&self, other: &str) -> bool {
|
||||
let other = Self::parse(other);
|
||||
if !self.is_known() || !other.is_known() {
|
||||
return false;
|
||||
}
|
||||
self.cmp(&other) != Ordering::Less
|
||||
}
|
||||
|
||||
/// 是否 `< other`。自身不可解析时返回 `false`(未知版本不触发“老版本”分支)。
|
||||
pub fn is_before(&self, other: &str) -> bool {
|
||||
let other = Self::parse(other);
|
||||
if !self.is_known() || !other.is_known() {
|
||||
return false;
|
||||
}
|
||||
self.cmp(&other) == Ordering::Less
|
||||
}
|
||||
|
||||
/// 去掉尾部 0 后的分量序列(`1.20.0` 与 `1.20` 等价)。
|
||||
fn trimmed_parts(&self) -> &[u32] {
|
||||
let mut end = self.parts.len();
|
||||
while end > 0 && self.parts[end - 1] == 0 {
|
||||
end -= 1;
|
||||
}
|
||||
&self.parts[..end]
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for GameVersion {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.trimmed_parts()
|
||||
.cmp(other.trimmed_parts())
|
||||
.then_with(|| self.stage.cmp(&other.stage))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for GameVersion {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for GameVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.raw)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GameVersion {
|
||||
fn default() -> Self {
|
||||
Self::unknown("")
|
||||
}
|
||||
}
|
||||
|
||||
fn split_stage(raw: &str) -> (&str, Stage) {
|
||||
for marker in ["-snapshot", " snapshot"] {
|
||||
if let Some(pos) = raw.find(marker) {
|
||||
let number = trailing_number(&raw[pos + marker.len()..]);
|
||||
return (&raw[..pos], Stage::Snapshot(number));
|
||||
}
|
||||
}
|
||||
for marker in ["-pre", "-preview"] {
|
||||
if let Some(pos) = raw.find(marker) {
|
||||
let number = trailing_number(&raw[pos + marker.len()..]);
|
||||
return (&raw[..pos], Stage::PreRelease(number));
|
||||
}
|
||||
}
|
||||
if let Some(pos) = raw.find("-rc") {
|
||||
let number = trailing_number(&raw[pos + 3..]);
|
||||
return (&raw[..pos], Stage::ReleaseCandidate(number));
|
||||
}
|
||||
(raw, Stage::Release)
|
||||
}
|
||||
|
||||
fn trailing_number(text: &str) -> u32 {
|
||||
let digits = text.trim_start_matches(['-', '.', '_']);
|
||||
digits
|
||||
.split(|ch: char| !ch.is_ascii_digit())
|
||||
.next()
|
||||
.and_then(|piece| piece.parse::<u32>().ok())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// `20w14a` / `26w14a` 形态判定(纯函数,便于单测)。
|
||||
fn is_weekly_snapshot_str(raw: &str) -> bool {
|
||||
let bytes = raw.trim().as_bytes();
|
||||
bytes.len() == 6
|
||||
&& bytes[..2].iter().all(u8::is_ascii_digit)
|
||||
&& bytes[2].eq_ignore_ascii_case(&b'w')
|
||||
&& bytes[3..5].iter().all(u8::is_ascii_digit)
|
||||
&& bytes[5].is_ascii_alphabetic()
|
||||
}
|
||||
Reference in new issue
Block a user