Files
Unlucky-Launcher/crates/core/src/launch/game_version.rs
T
AskaEth bc41dfcf7d
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
Unlucky Launcher v0.0.1-alpha
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent),
MCP server 接入外部 Agent 工具,GUI 开发中。
2026-10-31 21:54:10 +08:00

179 lines
5.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 游戏版本号解析与比较(供命令行组装的分支门限使用)。
//!
//! 依据 `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()
}