From c718d75774be64fe6c7d6e82c2568d07fc9bca24 Mon Sep 17 00:00:00 2001 From: Ap_Tx <3045929398@qq.com> Date: Sat, 10 Oct 2026 21:02:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=20Minecraft=20=E7=AA=97=E5=8F=A3=E6=A0=87=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 全局开关 + 默认标题,实例可覆盖;Win32 重命名窗口。前端设置页同步。 --- .../ui/instance_settings/WindowSettings.vue | 39 ++++++++- .../ui/settings/DefaultInstanceSettings.vue | 50 ++++++++++++ apps/app-frontend/src/helpers/types.d.ts | 3 + .../app-frontend/src/locales/en-US/index.json | 24 ++++++ .../app-frontend/src/locales/zh-CN/index.json | 10 ++- apps/app/src/lightweight_mode.rs | 80 ++++++++++++++++++- .../20261004000000_custom-window-title.sql | 2 + packages/app-lib/src/api/instance/run.rs | 12 +++ packages/app-lib/src/event/emit.rs | 2 + packages/app-lib/src/event/mod.rs | 2 + packages/app-lib/src/launcher/mod.rs | 2 + .../state/instances/commands/edit_instance.rs | 9 +++ .../src/state/instances/model/launch.rs | 7 ++ packages/app-lib/src/state/process.rs | 5 ++ packages/app-lib/src/state/settings.rs | 30 +++++++ 15 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 packages/app-lib/migrations/20261004000000_custom-window-title.sql diff --git a/apps/app-frontend/src/components/ui/instance_settings/WindowSettings.vue b/apps/app-frontend/src/components/ui/instance_settings/WindowSettings.vue index a7d8968..c28b576 100644 --- a/apps/app-frontend/src/components/ui/instance_settings/WindowSettings.vue +++ b/apps/app-frontend/src/components/ui/instance_settings/WindowSettings.vue @@ -36,24 +36,29 @@ const fullscreenSetting: Ref = ref( instance.value.force_fullscreen ?? globalSettings.force_fullscreen, ) const maximizeWindowSetting = ref(instance.value.maximize_window ?? globalSettings.maximize_window) +const windowTitle = ref(instance.value.window_title ?? '') const editInstanceObject = computed(() => { + const title = windowTitle.value.trim() ? windowTitle.value.trim() : null if (!overrideWindowSettings.value) { return { force_fullscreen: null, maximize_window: null, game_resolution: null, + // The title is independent of the window-size override checkbox. + window_title: title, } } return { force_fullscreen: fullscreenSetting.value, maximize_window: maximizeWindowSetting.value, game_resolution: fullscreenSetting.value ? null : resolution.value, + window_title: title, } }) watch( - [overrideWindowSettings, resolution, fullscreenSetting, maximizeWindowSetting], + [overrideWindowSettings, resolution, fullscreenSetting, maximizeWindowSetting, windowTitle], async () => { await edit(instance.value.id, editInstanceObject.value) }, @@ -109,6 +114,18 @@ const messages = defineMessages({ id: 'instance.settings.tabs.window.height.enter', defaultMessage: 'Enter height...', }, + windowTitle: { + id: 'instance.settings.tabs.window.window-title', + defaultMessage: 'Window title', + }, + windowTitleDescription: { + id: 'instance.settings.tabs.window.window-title.description', + defaultMessage: 'Customize the Minecraft window title. Leave empty to keep the default.', + }, + enterWindowTitle: { + id: 'instance.settings.tabs.window.window-title.enter', + defaultMessage: 'Enter window title...', + }, }) @@ -200,5 +217,25 @@ const messages = defineMessages({ :placeholder="formatMessage(messages.enterHeight)" /> +
+
+

+ {{ formatMessage(messages.windowTitle) }} +

+

+ {{ formatMessage(messages.windowTitleDescription) }} +

+
+ +
diff --git a/apps/app-frontend/src/components/ui/settings/DefaultInstanceSettings.vue b/apps/app-frontend/src/components/ui/settings/DefaultInstanceSettings.vue index fc73d9c..68c5089 100644 --- a/apps/app-frontend/src/components/ui/settings/DefaultInstanceSettings.vue +++ b/apps/app-frontend/src/components/ui/settings/DefaultInstanceSettings.vue @@ -38,6 +38,26 @@ const messages = defineMessages({ id: 'app.settings.defaults.maximize-window.unsupported', defaultMessage: 'Not supported on this operating system.', }, + windowTitleEnabled: { + id: 'app.settings.defaults.window-title.enabled', + defaultMessage: 'Custom Minecraft window title', + }, + windowTitleEnabledDescription: { + id: 'app.settings.defaults.window-title.enabled-description', + defaultMessage: 'Allow instances to customize the Minecraft window title.', + }, + defaultWindowTitle: { + id: 'app.settings.defaults.window-title.default', + defaultMessage: 'Default window title', + }, + defaultWindowTitleDescription: { + id: 'app.settings.defaults.window-title.default-description', + defaultMessage: 'Used when an instance leaves its window title empty.', + }, + defaultWindowTitlePlaceholder: { + id: 'app.settings.defaults.window-title.default-placeholder', + defaultMessage: 'Enter default window title...', + }, width: { id: 'app.settings.defaults.width', defaultMessage: 'Width' }, widthDescription: { id: 'app.settings.defaults.width-description', @@ -212,6 +232,36 @@ watch( /> + + + + + + + + + + diff --git a/apps/app-frontend/src/helpers/types.d.ts b/apps/app-frontend/src/helpers/types.d.ts index 22a28bb..f05cc8f 100644 --- a/apps/app-frontend/src/helpers/types.d.ts +++ b/apps/app-frontend/src/helpers/types.d.ts @@ -55,6 +55,7 @@ export type GameInstance = { force_fullscreen?: boolean maximize_window?: boolean game_resolution?: [number, number] + window_title?: string | null launch_preparation_timeout?: number | null hooks: Hooks } @@ -239,6 +240,8 @@ type AppSettings = { force_fullscreen: boolean maximize_window: boolean game_resolution: [number, number] + custom_window_title_enabled: boolean + default_window_title: string hide_on_process_start: boolean enter_lightweight_mode_on_game_launch: boolean auto_set_java_high_performance_mode: boolean diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index ec78130..399d63b 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -11431,5 +11431,29 @@ }, "app.anniversary.banner": { "message": "Happy {ordinal} Anniversary, Starlight!" + }, + "instance.settings.tabs.window.window-title": { + "message": "Window title" + }, + "instance.settings.tabs.window.window-title.description": { + "message": "Customize the Minecraft window title. Leave empty to keep the default." + }, + "instance.settings.tabs.window.window-title.enter": { + "message": "Enter window title..." + }, + "app.settings.defaults.window-title.enabled": { + "message": "Custom Minecraft window title" + }, + "app.settings.defaults.window-title.enabled-description": { + "message": "Allow instances to customize the Minecraft window title." + }, + "app.settings.defaults.window-title.default": { + "message": "Default window title" + }, + "app.settings.defaults.window-title.default-description": { + "message": "Used when an instance does not provide its own title." + }, + "app.settings.defaults.window-title.default-placeholder": { + "message": "Enter the default window title..." } } diff --git a/apps/app-frontend/src/locales/zh-CN/index.json b/apps/app-frontend/src/locales/zh-CN/index.json index 8760bfe..edb61ed 100644 --- a/apps/app-frontend/src/locales/zh-CN/index.json +++ b/apps/app-frontend/src/locales/zh-CN/index.json @@ -11351,5 +11351,13 @@ "app.settings.synced-options.game-settings.value-off": { "message": "关闭" }, "app.settings.synced-options.game-settings.value-on": { "message": "开启" }, "app.settings.synced-options.game-settings.value-unset": { "message": "选择一个值" }, - "app.anniversary.banner": { "message": "Starlight {ordinal} 周年快乐!" } + "app.anniversary.banner": { "message": "Starlight {ordinal} 周年快乐!" }, + "instance.settings.tabs.window.window-title": { "message": "窗口标题" }, + "instance.settings.tabs.window.window-title.description": { "message": "自定义 Minecraft 窗口标题。留空则使用默认标题。" }, + "instance.settings.tabs.window.window-title.enter": { "message": "输入窗口标题..." }, + "app.settings.defaults.window-title.enabled": { "message": "自定义 Minecraft 窗口标题" }, + "app.settings.defaults.window-title.enabled-description": { "message": "允许实例自定义 Minecraft 窗口标题。" }, + "app.settings.defaults.window-title.default": { "message": "默认窗口标题" }, + "app.settings.defaults.window-title.default-description": { "message": "当实例未提供自己的标题时使用。" }, + "app.settings.defaults.window-title.default-placeholder": { "message": "输入默认窗口标题..." } } diff --git a/apps/app/src/lightweight_mode.rs b/apps/app/src/lightweight_mode.rs index e5772b4..7887e16 100644 --- a/apps/app/src/lightweight_mode.rs +++ b/apps/app/src/lightweight_mode.rs @@ -253,6 +253,14 @@ impl LightweightMode { if payload.maximize_window { maximize_minecraft_window(payload.pid).await; } + if let Some(title) = payload + .window_title + .as_ref() + .map(|value| value.trim()) + .filter(|value| !value.is_empty()) + { + rename_minecraft_window(payload.pid, title).await; + } let settings = match theseus::settings::get().await { Ok(settings) => settings, Err(error) => { @@ -332,6 +340,8 @@ struct ProcessEventPayload { pid: u32, #[serde(default)] maximize_window: bool, + #[serde(default)] + window_title: Option, event: String, crashed: Option, #[serde(default)] @@ -339,7 +349,7 @@ struct ProcessEventPayload { } #[cfg(target_os = "windows")] -use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicIsize, AtomicU32, Ordering}; #[cfg(target_os = "windows")] static MAXIMIZE_PROCESS_ID: AtomicU32 = AtomicU32::new(0); @@ -400,6 +410,74 @@ async fn maximize_minecraft_window(pid: u32) { #[cfg(not(target_os = "windows"))] async fn maximize_minecraft_window(_pid: u32) {} +#[cfg(target_os = "windows")] +static RENAME_WINDOW_HANDLE: AtomicIsize = AtomicIsize::new(0); + +#[cfg(target_os = "windows")] +unsafe extern "system" fn rename_if_owned_by_process( + hwnd: windows::Win32::Foundation::HWND, + _: windows::Win32::Foundation::LPARAM, +) -> windows::core::BOOL { + use windows::Win32::UI::WindowsAndMessaging::{ + GetWindowThreadProcessId, IsWindowVisible, + }; + use windows::core::BOOL; + + let mut window_pid = 0; + unsafe { GetWindowThreadProcessId(hwnd, Some(&mut window_pid)) }; + if window_pid == MAXIMIZE_PROCESS_ID.load(Ordering::Relaxed) + && unsafe { IsWindowVisible(hwnd).as_bool() } + { + RENAME_WINDOW_HANDLE.store(hwnd.0 as isize, Ordering::Relaxed); + return BOOL(0); + } + BOOL(1) +} + +#[cfg(target_os = "windows")] +async fn rename_minecraft_window(pid: u32, title: &str) { + if pid == 0 || title.is_empty() { + return; + } + + // PCL keeps re-applying the title: FML/Quilt and some mods reset it + // while the game boots, so a one-shot rename is not enough. + let deadline = + tokio::time::Instant::now() + std::time::Duration::from_secs(45); + let mut applied = false; + loop { + let found = { + let _guard = MAXIMIZE_WINDOW_ENUMERATION.lock(); + MAXIMIZE_PROCESS_ID.store(pid, Ordering::Relaxed); + RENAME_WINDOW_HANDLE.store(0, Ordering::Relaxed); + unsafe { + use windows::Win32::Foundation::LPARAM; + use windows::Win32::UI::WindowsAndMessaging::EnumWindows; + let _ = EnumWindows(Some(rename_if_owned_by_process), LPARAM(0)); + } + RENAME_WINDOW_HANDLE.load(Ordering::Relaxed) + }; + if found != 0 { + let hwnd = windows::Win32::Foundation::HWND(found as *mut _); + let wide: Vec = title.encode_utf16().chain(Some(0)).collect(); + unsafe { + use windows::Win32::UI::WindowsAndMessaging::SetWindowTextW; + let _ = + SetWindowTextW(hwnd, windows::core::PCWSTR(wide.as_ptr())); + } + applied = true; + } + if tokio::time::Instant::now() >= deadline { + return; + } + let _ = applied; + tokio::time::sleep(std::time::Duration::from_millis(400)).await; + } +} + +#[cfg(not(target_os = "windows"))] +async fn rename_minecraft_window(_pid: u32, _title: &str) {} + #[tauri::command] pub fn lightweight_mode_frontend_ready( diff --git a/packages/app-lib/migrations/20261004000000_custom-window-title.sql b/packages/app-lib/migrations/20261004000000_custom-window-title.sql new file mode 100644 index 0000000..5e361a7 --- /dev/null +++ b/packages/app-lib/migrations/20261004000000_custom-window-title.sql @@ -0,0 +1,2 @@ +ALTER TABLE settings ADD COLUMN custom_window_title_enabled INTEGER NOT NULL DEFAULT FALSE; +ALTER TABLE settings ADD COLUMN default_window_title TEXT NOT NULL DEFAULT 'Minecraft'; \ No newline at end of file diff --git a/packages/app-lib/src/api/instance/run.rs b/packages/app-lib/src/api/instance/run.rs index b32fcb8..d5ebea2 100644 --- a/packages/app-lib/src/api/instance/run.rs +++ b/packages/app-lib/src/api/instance/run.rs @@ -250,6 +250,17 @@ async fn run_credentials( .launch_overrides .maximize_window .unwrap_or(settings.maximize_window); + let window_title = settings + .custom_window_title_enabled + .then(|| { + context + .launch_overrides + .window_title + .clone() + .filter(|value| !value.trim().is_empty()) + .unwrap_or_else(|| settings.default_window_title.clone()) + }) + .filter(|value| !value.trim().is_empty()); let env_args = context .launch_overrides .custom_env_vars @@ -319,6 +330,7 @@ async fn run_credentials( &memory, &resolution, maximize_window, + window_title, credentials, post_exit_hook, &context, diff --git a/packages/app-lib/src/event/emit.rs b/packages/app-lib/src/event/emit.rs index 9b0f15a..96e21c5 100644 --- a/packages/app-lib/src/event/emit.rs +++ b/packages/app-lib/src/event/emit.rs @@ -400,6 +400,7 @@ pub async fn emit_process( uuid: Uuid, pid: u32, maximize_window: bool, + window_title: Option, event: ProcessPayloadType, message: &str, crashed: Option, @@ -416,6 +417,7 @@ pub async fn emit_process( uuid, pid, maximize_window, + window_title, event, message: message.to_string(), crashed, diff --git a/packages/app-lib/src/event/mod.rs b/packages/app-lib/src/event/mod.rs index a12e48f..fc4b130 100644 --- a/packages/app-lib/src/event/mod.rs +++ b/packages/app-lib/src/event/mod.rs @@ -336,6 +336,8 @@ pub struct ProcessPayload { pub uuid: Uuid, pub pid: u32, pub maximize_window: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub window_title: Option, pub event: ProcessPayloadType, pub message: String, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/packages/app-lib/src/launcher/mod.rs b/packages/app-lib/src/launcher/mod.rs index dd2f3be..4637aa1 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -1520,6 +1520,7 @@ pub async fn launch_minecraft( memory: &MemorySettings, resolution: &WindowSize, maximize_window: bool, + window_title: Option, credentials: &Credentials, post_exit_hook: Option, context: &InstanceLaunchContext, @@ -2391,6 +2392,7 @@ pub async fn launch_minecraft( command, post_exit_hook, maximize_window, + window_title, instance_path.clone(), logs_folder, version_info.logging.is_some(), diff --git a/packages/app-lib/src/state/instances/commands/edit_instance.rs b/packages/app-lib/src/state/instances/commands/edit_instance.rs index e2af145..d6de374 100644 --- a/packages/app-lib/src/state/instances/commands/edit_instance.rs +++ b/packages/app-lib/src/state/instances/commands/edit_instance.rs @@ -103,6 +103,12 @@ pub struct InstanceLaunchOverridesPatch { skip_serializing_if = "Option::is_none", with = "serde_with::rust::double_option" )] + pub window_title: Option>, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "serde_with::rust::double_option" + )] pub launch_preparation_timeout: Option>, pub hooks: Option, } @@ -371,6 +377,9 @@ fn apply_launch_overrides_patch( if let Some(game_resolution) = patch.game_resolution { overrides.game_resolution = game_resolution; } + if let Some(window_title) = patch.window_title { + overrides.window_title = window_title.filter(|value| !value.trim().is_empty()); + } if let Some(mode) = patch.instance_mode { overrides.instance_mode = Some(mode); } diff --git a/packages/app-lib/src/state/instances/model/launch.rs b/packages/app-lib/src/state/instances/model/launch.rs index 848866b..8d01a81 100644 --- a/packages/app-lib/src/state/instances/model/launch.rs +++ b/packages/app-lib/src/state/instances/model/launch.rs @@ -38,6 +38,8 @@ pub struct InstanceLaunchOverrides { pub maximize_window: Option, pub game_resolution: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub window_title: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub launch_preparation_timeout: Option, pub hooks: Hooks, } @@ -55,6 +57,7 @@ impl InstanceLaunchOverrides { force_fullscreen: None, maximize_window: None, game_resolution: None, + window_title: None, launch_preparation_timeout: None, hooks: Hooks { pre_launch: None, @@ -86,6 +89,8 @@ pub(crate) struct InstanceLaunchOverridesData { #[serde(default, skip_serializing_if = "Option::is_none")] pub game_resolution: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub window_title: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub launch_preparation_timeout: Option, #[serde(default)] pub hooks: Hooks, @@ -107,6 +112,7 @@ impl InstanceLaunchOverridesData { force_fullscreen: self.force_fullscreen, maximize_window: self.maximize_window, game_resolution: self.game_resolution, + window_title: self.window_title, launch_preparation_timeout: self.launch_preparation_timeout, hooks: self.hooks, } @@ -125,6 +131,7 @@ impl From<&InstanceLaunchOverrides> for InstanceLaunchOverridesData { force_fullscreen: overrides.force_fullscreen, maximize_window: overrides.maximize_window, game_resolution: overrides.game_resolution, + window_title: overrides.window_title.clone(), launch_preparation_timeout: overrides.launch_preparation_timeout, hooks: overrides.hooks.clone(), } diff --git a/packages/app-lib/src/state/process.rs b/packages/app-lib/src/state/process.rs index adf5a7d..bd34aa6 100644 --- a/packages/app-lib/src/state/process.rs +++ b/packages/app-lib/src/state/process.rs @@ -312,6 +312,7 @@ impl ProcessManager { mut mc_command: Command, post_exit_command: Option, maximize_window: bool, + window_title: Option, game_dir: PathBuf, logs_folder: PathBuf, xml_logging: bool, @@ -336,6 +337,7 @@ impl ProcessManager { uuid: Uuid::new_v4(), pid: mc_proc.id().unwrap_or_default(), maximize_window, + window_title: window_title.clone(), start_time: Utc::now(), instance_id: instance_id.to_string(), instance_path: instance_path.to_string(), @@ -509,6 +511,7 @@ impl ProcessManager { metadata.uuid, metadata.pid, metadata.maximize_window, + metadata.window_title.clone(), ProcessPayloadType::Launched, "Launched Minecraft", None, @@ -586,6 +589,7 @@ pub struct ProcessMetadata { pub uuid: Uuid, pub pid: u32, pub maximize_window: bool, + pub window_title: Option, pub instance_id: String, pub instance_path: String, pub instance_name: String, @@ -1311,6 +1315,7 @@ impl Process { uuid, pid, false, + None, ProcessPayloadType::Finished, "Exited process", Some(!mc_exit_status.success() && !manually_killed), diff --git a/packages/app-lib/src/state/settings.rs b/packages/app-lib/src/state/settings.rs index 8ded758..5e75252 100644 --- a/packages/app-lib/src/state/settings.rs +++ b/packages/app-lib/src/state/settings.rs @@ -176,6 +176,10 @@ pub struct Settings { pub force_fullscreen: bool, pub maximize_window: bool, #[serde(default)] + pub custom_window_title_enabled: bool, + #[serde(default = "default_window_title")] + pub default_window_title: String, + #[serde(default)] pub force_unicode_font: bool, pub game_resolution: WindowSize, pub hide_on_process_start: bool, @@ -198,6 +202,10 @@ pub struct Settings { pub version: usize, } +fn default_window_title() -> String { + "Minecraft".to_string() +} + fn default_true() -> bool { true } @@ -328,6 +336,16 @@ impl Settings { ) .fetch_one(exec) .await?; + let custom_window_title_enabled: bool = sqlx::query_scalar( + "SELECT custom_window_title_enabled FROM settings WHERE id = 0", + ) + .fetch_one(exec) + .await?; + let default_window_title: String = sqlx::query_scalar( + "SELECT default_window_title FROM settings WHERE id = 0", + ) + .fetch_one(exec) + .await?; let settings = Self { max_concurrent_downloads: res.max_concurrent_downloads as usize, max_concurrent_writes: res.max_concurrent_writes as usize, @@ -415,6 +433,8 @@ impl Settings { }, force_fullscreen: res.mc_force_fullscreen == 1, maximize_window: res.mc_maximize_window == 1, + custom_window_title_enabled, + default_window_title, force_unicode_font: sqlx::query_scalar( "SELECT mc_force_unicode_font FROM settings WHERE id = 0", ) @@ -663,6 +683,16 @@ impl Settings { .bind(self.ignore_ssl_errors) .execute(exec) .await?; + sqlx::query( + "UPDATE settings SET custom_window_title_enabled = ? WHERE id = 0", + ) + .bind(self.custom_window_title_enabled) + .execute(exec) + .await?; + sqlx::query("UPDATE settings SET default_window_title = ? WHERE id = 0") + .bind(self.default_window_title.trim()) + .execute(exec) + .await?; sqlx::query("UPDATE settings SET mc_memory_optimize = ? WHERE id = 0") .bind(self.memory.optimize_before_launch) .execute(exec)