feat: integrate StarLight updates and improve font settings and skin editor loading
Some checks failed
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled

This commit is contained in:
2026-09-19 18:14:41 +08:00
parent a47d309103
commit a477b1fb9a
24 changed files with 665 additions and 74 deletions

View File

@ -0,0 +1 @@
ALTER TABLE settings ADD COLUMN mc_force_unicode_font INTEGER NOT NULL DEFAULT FALSE;

View File

@ -24,11 +24,14 @@ enum LanguageCodeStyle {
/// directory (e.g. modpacks that ship a preconfigured `options.txt`). For
/// instances the player already uses, their in-game choice is kept and only
/// its casing is normalized for the game version to avoid resets or crashes.
/// The font preference is initialized independently of the language, only
/// when a fresh instance has no explicit font choice in its options file.
pub fn game_language_options(
launcher_locale: &str,
game_release_time: DateTime<Utc>,
options_txt: &str,
has_saves: bool,
force_unicode_font: bool,
) -> Vec<(String, String)> {
let style = match language_code_style(game_release_time) {
LanguageCodeStyle::Unsupported => return Vec::new(),
@ -47,16 +50,21 @@ pub fn game_language_options(
.as_deref()
.and_then(|code| normalize_language_code(code, legacy_region_case))
};
let Some(desired) = desired else {
return Vec::new();
};
let mut options = Vec::new();
if current.as_deref() != Some(desired.as_str()) {
if let Some(desired) = desired
&& current.as_deref() != Some(desired.as_str())
{
options.push(("lang".to_string(), desired));
}
if fresh && needs_unicode_font(launcher_locale) {
options.push(("forceUnicodeFont".to_string(), "true".to_string()));
if fresh
&& !options_txt
.lines()
.any(|line| line.starts_with("forceUnicodeFont:"))
{
options.push((
"forceUnicodeFont".to_string(),
force_unicode_font.to_string(),
));
}
options
}
@ -105,20 +113,6 @@ fn normalize_language_code(
}
}
/// CJK glyphs are not covered by the game's default bitmap font in older
/// versions, so first-time setups for these languages also force the
/// unicode font.
fn needs_unicode_font(launcher_locale: &str) -> bool {
launcher_locale
.split(['-', '_'])
.next()
.is_some_and(|language| {
language.eq_ignore_ascii_case("zh")
|| language.eq_ignore_ascii_case("ja")
|| language.eq_ignore_ascii_case("ko")
})
}
#[cfg(test)]
mod tests {
use super::*;
@ -142,10 +136,10 @@ mod tests {
#[test]
fn fresh_instance_follows_launcher_language() {
assert_eq!(
game_language_options("zh-CN", modern(), "", false),
game_language_options("zh-CN", modern(), "", false, false),
vec![
("lang".to_string(), "zh_cn".to_string()),
("forceUnicodeFont".to_string(), "true".to_string()),
("forceUnicodeFont".to_string(), "false".to_string()),
]
);
}
@ -153,26 +147,50 @@ mod tests {
#[test]
fn legacy_versions_use_uppercase_region() {
assert_eq!(
game_language_options("zh-CN", legacy(), "", false),
game_language_options("zh-CN", legacy(), "", false, false),
vec![
("lang".to_string(), "zh_CN".to_string()),
("forceUnicodeFont".to_string(), "true".to_string()),
("forceUnicodeFont".to_string(), "false".to_string()),
]
);
}
#[test]
fn non_cjk_languages_skip_unicode_font() {
fn unicode_font_can_be_enabled_for_any_language() {
for locale in ["zh-CN", "zh-TW", "ja-JP", "ko-KR", "en-US", ""] {
let options =
game_language_options(locale, modern(), "", false, true);
assert!(
options.contains(&(
"forceUnicodeFont".to_string(),
"true".to_string()
)),
"{locale}"
);
}
}
#[test]
fn non_cjk_languages_also_use_the_font_default() {
assert_eq!(
game_language_options("en-US", modern(), "", false),
vec![("lang".to_string(), "en_us".to_string())]
game_language_options("en-US", modern(), "", false, false),
vec![
("lang".to_string(), "en_us".to_string()),
("forceUnicodeFont".to_string(), "false".to_string()),
]
);
}
#[test]
fn versions_before_1_1_are_left_alone() {
assert_eq!(
game_language_options("zh-CN", release(2011, 11, 17), "", false),
game_language_options(
"zh-CN",
release(2011, 11, 17),
"",
false,
true
),
Vec::new()
);
}
@ -184,6 +202,7 @@ mod tests {
"zh-CN",
modern(),
"fullscreen:false\nlang:ja_jp\n",
true,
true
),
Vec::new()
@ -193,7 +212,13 @@ mod tests {
#[test]
fn played_instances_get_their_casing_normalized() {
assert_eq!(
game_language_options("en-US", modern(), "lang:zh_CN\n", true),
game_language_options(
"en-US",
modern(),
"lang:zh_CN\n",
true,
true
),
vec![("lang".to_string(), "zh_cn".to_string())]
);
}
@ -201,10 +226,16 @@ mod tests {
#[test]
fn preconfigured_language_without_saves_is_overridden() {
assert_eq!(
game_language_options("zh-TW", modern(), "lang:en_us\n", false),
game_language_options(
"zh-TW",
modern(),
"lang:en_us\n",
false,
false
),
vec![
("lang".to_string(), "zh_tw".to_string()),
("forceUnicodeFont".to_string(), "true".to_string()),
("forceUnicodeFont".to_string(), "false".to_string()),
]
);
}
@ -212,16 +243,25 @@ mod tests {
#[test]
fn matching_language_needs_no_update() {
assert_eq!(
game_language_options("ja-JP", modern(), "lang:ja_jp\n", true),
game_language_options(
"ja-JP",
modern(),
"lang:ja_jp\n",
true,
false
),
Vec::new()
);
}
#[test]
fn empty_locale_makes_no_changes() {
assert_eq!(game_language_options("", modern(), "", false), Vec::new());
fn empty_locale_only_initializes_the_font_default() {
assert_eq!(
game_language_options("", modern(), "lang:zh_cn\n", true),
game_language_options("", modern(), "", false, false),
vec![("forceUnicodeFont".to_string(), "false".to_string())]
);
assert_eq!(
game_language_options("", modern(), "lang:zh_cn\n", true, false),
Vec::new()
);
}
@ -229,8 +269,39 @@ mod tests {
#[test]
fn crlf_options_files_are_parsed() {
assert_eq!(
game_language_options("ko-KR", modern(), "lang:ko_kr\r\n", true),
game_language_options(
"ko-KR",
modern(),
"lang:ko_kr\r\n",
true,
false
),
Vec::new()
);
}
#[test]
fn existing_font_choices_are_preserved_even_without_saves_or_language() {
for has_saves in [false, true] {
for enabled in [false, true] {
for language in ["", "lang:zh_cn\r\n"] {
let options_txt = format!(
"{language}forceUnicodeFont:{enabled}\r\nfullscreen:false\r\n"
);
let options = game_language_options(
"zh-CN",
modern(),
&options_txt,
has_saves,
!enabled,
);
assert!(
options
.iter()
.all(|(key, _)| key != "forceUnicodeFont")
);
}
}
}
}
}

View File

@ -2326,6 +2326,7 @@ pub async fn launch_minecraft(
&& (!mc_set_options.is_empty()
|| offline_skin_pack.enabled_pack_id.is_some()
|| options_existed
|| settings.force_unicode_font
|| !settings.locale.is_empty())
{
let (mut options_string, input_encoding) = if options_existed {
@ -2353,6 +2354,7 @@ pub async fn launch_minecraft(
launch_release_time,
&options_string,
instance_path.join("saves").exists(),
settings.force_unicode_font,
);
if !mc_set_options.is_empty()

View File

@ -166,6 +166,8 @@ pub struct Settings {
pub memory: MemorySettings,
pub force_fullscreen: bool,
pub maximize_window: bool,
#[serde(default)]
pub force_unicode_font: bool,
pub game_resolution: WindowSize,
pub hide_on_process_start: bool,
pub enter_lightweight_mode_on_game_launch: bool,
@ -347,6 +349,11 @@ impl Settings {
},
force_fullscreen: res.mc_force_fullscreen == 1,
maximize_window: res.mc_maximize_window == 1,
force_unicode_font: sqlx::query_scalar(
"SELECT mc_force_unicode_font FROM settings WHERE id = 0",
)
.fetch_one(exec)
.await?,
game_resolution: WindowSize(
res.mc_game_resolution_x as u16,
res.mc_game_resolution_y as u16,
@ -590,6 +597,12 @@ impl Settings {
.bind(self.memory.optimize_before_launch)
.execute(exec)
.await?;
sqlx::query(
"UPDATE settings SET mc_force_unicode_font = ? WHERE id = 0",
)
.bind(self.force_unicode_font)
.execute(exec)
.await?;
Ok(())
}
@ -1092,6 +1105,58 @@ mod tests {
assert!(!reloaded.bypass_curseforge_download_restrictions);
}
#[tokio::test]
async fn unicode_font_defaults_off_after_upgrade_and_round_trips() {
let migrator = sqlx::migrate!();
for previous_schema in [false, true] {
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await
.unwrap();
if previous_schema {
let previous_migrator = sqlx::migrate::Migrator {
migrations: std::borrow::Cow::Owned(
migrator
.iter()
.filter(|migration| {
migration.version < 20260919000000
})
.cloned()
.collect(),
),
..sqlx::migrate::Migrator::DEFAULT
};
previous_migrator.run(&pool).await.unwrap();
sqlx::query(
"UPDATE settings SET locale = 'zh-TW' WHERE id = 0",
)
.execute(&pool)
.await
.unwrap();
}
migrator.run(&pool).await.unwrap();
let mut settings = Settings::get(&pool).await.unwrap();
assert!(!settings.force_unicode_font);
if previous_schema {
assert_eq!(settings.locale, "zh-TW");
}
for enabled in [true, false] {
settings.force_unicode_font = enabled;
settings.update(&pool).await.unwrap();
let reloaded = Settings::get(&pool).await.unwrap();
assert_eq!(reloaded.force_unicode_font, enabled);
}
// Older clients and serialized settings omit the newly added field.
let mut legacy = serde_json::to_value(&settings).unwrap();
legacy.as_object_mut().unwrap().remove("force_unicode_font");
let restored: Settings = serde_json::from_value(legacy).unwrap();
assert!(!restored.force_unicode_font);
}
}
#[tokio::test]
async fn memory_optimization_round_trips_in_a_fresh_database() {
let pool = sqlx::sqlite::SqlitePoolOptions::new()