Skip to content

Commit b165735

Browse files
committed
feat: 增加控制台输出开关
在默认配置和 manifest 中加入 EnableConsole,默认保持创建控制台的现有行为。 关闭开关时跳过 AllocConsole、标题和代码页设置,直接复用原进程的标准输出句柄。根据句柄类型分别使用 WriteConsoleW 和 WriteFile,兼容控制台、文件及管道重定向,并统一 AppleChu 控制台标识。
1 parent 691e81e commit b165735

6 files changed

Lines changed: 119 additions & 47 deletions

File tree

manifest.toml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ game_versions = ["2.45"]
1111

1212
[ui]
1313
groups = [
14-
{ id = "common", label = { zh = "常用", en = "Common" }, sections = ["General", "SkipStartup", "FreePlay", "DisableTimer", "SkipMapAnimation"] },
14+
{ id = "common", label = { zh = "常用", en = "Common" }, sections = ["System", "General", "SkipStartup", "FreePlay", "DisableTimer", "SkipMapAnimation"] },
1515
{ id = "gameplay", label = { zh = "游戏", en = "Gameplay" }, sections = ["UnlockTracks", "CustomTimers", "AllTimers999", "Autoplay"] },
1616
{ id = "display", label = { zh = "显示", en = "Display" }, sections = ["Unlock120fps", "Bypass1080p", "Bypass120hz", "DpiAware", "FpsDisplay", "FrameLock"] },
1717
{ id = "audio", label = { zh = "音频", en = "Audio" }, sections = ["ForceSharedAudio", "Force2chAudio"] },
@@ -21,6 +21,17 @@ groups = [
2121
{ id = "ux", label = { zh = "体验", en = "UX" }, sections = ["ExitConfirm", "DeviceLostFix"] },
2222
]
2323

24+
[[config.sections]]
25+
id = "System"
26+
always_enabled = true
27+
label = { zh = "系统设置", en = "System" }
28+
[[config.sections.entries]]
29+
key = "EnableConsole"
30+
type = "bool"
31+
default = true
32+
label = { zh = "开启控制台", en = "Enable console" }
33+
description = { zh = "关闭后沿用启动进程的标准输出流", en = "Reuse the launcher's standard output stream when disabled" }
34+
2435
[[config.sections]]
2536
id = "General"
2637
always_enabled = true

src/config.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ Version = "1"
2929
## =============================================================================
3030
3131
[System]
32+
## 是否创建新的控制台窗口
33+
## true = 创建控制台窗口(默认)
34+
## false = 不创建窗口,沿用启动进程的标准输出流
35+
EnableConsole = true
36+
3237
## 店内联机基准机/从机
3338
## false = 基准机(单机,或局域网中的标准机,默认)
3439
## true = 从机(局域网中有 2-4 台主机时,非标准机的那些设为 true)

src/proxy/loader/console.rs

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use windows_sys_loader::Win32::System::Console::{
66
};
77

88
use super::log::{write_banner_line, ANSI_CYAN};
9-
use super::state::LoaderState;
9+
use super::state::{LoaderState, OutputSink};
1010
use super::{hash, pe};
1111

1212
const CP_UTF8: u32 = 65001;
@@ -15,30 +15,50 @@ const ANSI_GRAY: &str = "\x1b[37m";
1515
// TODO: 控制台图标(SetConsoleIcon / 嵌入 RT_ICON 资源)
1616
// TODO: 控制台字体大小/窗口尺寸调整
1717

18-
pub unsafe fn init(state: &mut LoaderState) {
19-
AllocConsole();
18+
pub unsafe fn init(state: &mut LoaderState, console_enabled: bool) {
19+
if console_enabled {
20+
AllocConsole();
21+
}
2022

2123
let handle = GetStdHandle(STD_OUTPUT_HANDLE);
2224
if handle.is_null() || handle == INVALID_HANDLE_VALUE {
2325
return;
2426
}
2527

26-
SetConsoleOutputCP(CP_UTF8);
27-
SetConsoleTitleA(b"ChuModLoader\0".as_ptr());
28+
if console_enabled {
29+
SetConsoleOutputCP(CP_UTF8);
30+
SetConsoleTitleA(b"AppleChu\0".as_ptr());
31+
}
2832

29-
enable_ansi(handle);
30-
state.console = handle;
33+
state.output = match console_ansi_enabled(handle, console_enabled) {
34+
Some(ansi_enabled) => OutputSink::Console {
35+
handle,
36+
ansi_enabled,
37+
},
38+
None => OutputSink::Stream(handle),
39+
};
3140

3241
print_banner(state);
3342
}
3443

35-
unsafe fn enable_ansi(handle: windows_sys_loader::Win32::Foundation::HANDLE) {
44+
unsafe fn console_ansi_enabled(
45+
handle: windows_sys_loader::Win32::Foundation::HANDLE,
46+
configure: bool,
47+
) -> Option<bool> {
3648
let mut mode: u32 = 0;
37-
if GetConsoleMode(handle, &mut mode) != 0 {
38-
SetConsoleMode(
39-
handle,
40-
mode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING,
41-
);
49+
if GetConsoleMode(handle, &mut mode) == 0 {
50+
return None;
51+
}
52+
53+
if configure {
54+
Some(
55+
SetConsoleMode(
56+
handle,
57+
mode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING,
58+
) != 0,
59+
)
60+
} else {
61+
Some(mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0)
4262
}
4363
}
4464

@@ -57,7 +77,7 @@ fn print_banner(state: &mut LoaderState) {
5777
};
5878

5979
write_banner_line(state, ANSI_CYAN, sep);
60-
write_banner_line(state, ANSI_CYAN, &format!("ChuModLoader v{version} Nya~ "));
80+
write_banner_line(state, ANSI_CYAN, &format!("AppleChu v{version} Nya~ "));
6181
write_banner_line(state, ANSI_GRAY, &format!("OS: {}", os_version()));
6282
write_banner_line(state, ANSI_GRAY, &format!("Hash Code: {hash_code}"));
6383
write_banner_line(state, ANSI_CYAN, sep);

src/proxy/loader/log.rs

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use std::ffi::{c_char, c_void};
22
use std::io::Write;
33

4-
use windows_sys_loader::Win32::Foundation::INVALID_HANDLE_VALUE;
5-
use windows_sys_loader::Win32::System::Console::WriteConsoleA;
4+
use windows_sys_loader::Win32::Storage::FileSystem::WriteFile;
5+
use windows_sys_loader::Win32::System::Console::WriteConsoleW;
66

7-
use super::state::{LoaderState, STATE};
7+
use super::state::{LoaderState, OutputSink, STATE};
88

99
extern "system" {
1010
fn GetLocalTime(st: *mut SYSTEMTIME);
@@ -76,17 +76,8 @@ pub fn write_banner_line(state: &mut LoaderState, ansi: &str, msg: &str) {
7676
let _ = f.flush();
7777
}
7878

79-
if state.console != INVALID_HANDLE_VALUE && !state.console.is_null() {
80-
let colored = format!("{ANSI_TIME}[{time}]{ANSI_RESET} {ansi}{msg}{ANSI_RESET}\n");
81-
let mut written = 0u32;
82-
WriteConsoleA(
83-
state.console,
84-
colored.as_ptr(),
85-
colored.len() as u32,
86-
&mut written,
87-
std::ptr::null(),
88-
);
89-
}
79+
let colored = format!("{ANSI_TIME}[{time}]{ANSI_RESET} {ansi}{msg}{ANSI_RESET}\n");
80+
write_output(state.output, &plain, &colored);
9081
}
9182
}
9283

@@ -115,20 +106,50 @@ pub fn write_log_inner_level(state: &mut LoaderState, level: LogLevel, msg: &str
115106
let _ = f.flush();
116107
}
117108

118-
if state.console != INVALID_HANDLE_VALUE && !state.console.is_null() {
119-
let colored = format!(
120-
"{ANSI_TIME}[{time}]{ANSI_RESET} {ANSI_TAG}[loader]{ANSI_RESET} {body}[{label}] {msg}{ANSI_RESET}\n",
121-
body = level.body_ansi(),
122-
label = level.label(),
123-
);
109+
let colored = format!(
110+
"{ANSI_TIME}[{time}]{ANSI_RESET} {ANSI_TAG}[loader]{ANSI_RESET} {body}[{label}] {msg}{ANSI_RESET}\n",
111+
body = level.body_ansi(),
112+
label = level.label(),
113+
);
114+
write_output(state.output, &plain, &colored);
115+
}
116+
}
117+
118+
fn write_output(output: OutputSink, plain: &str, colored: &str) {
119+
match output {
120+
OutputSink::None => {}
121+
OutputSink::Console {
122+
handle,
123+
ansi_enabled,
124+
} => {
125+
let text = if ansi_enabled { colored } else { plain };
126+
let utf16: Vec<u16> = text.encode_utf16().collect();
127+
let length = u32::try_from(utf16.len()).unwrap_or(u32::MAX);
128+
let mut written = 0u32;
129+
// SAFETY: 初始化输出时已排除无效句柄,UTF-16 缓冲区在同步调用期间保持有效。
130+
unsafe {
131+
WriteConsoleW(
132+
handle,
133+
utf16.as_ptr(),
134+
length,
135+
&mut written,
136+
std::ptr::null(),
137+
);
138+
}
139+
}
140+
OutputSink::Stream(handle) => {
141+
let length = u32::try_from(plain.len()).unwrap_or(u32::MAX);
124142
let mut written = 0u32;
125-
WriteConsoleA(
126-
state.console,
127-
colored.as_ptr(),
128-
colored.len() as u32,
129-
&mut written,
130-
std::ptr::null(),
131-
);
143+
// SAFETY: 初始化输出时已排除无效句柄,字符串缓冲区在同步调用期间保持有效。
144+
unsafe {
145+
WriteFile(
146+
handle,
147+
plain.as_ptr(),
148+
length,
149+
&mut written,
150+
std::ptr::null_mut(),
151+
);
152+
}
132153
}
133154
}
134155
}

src/proxy/loader/mod.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use windows_sys_loader::Win32::System::LibraryLoader::GetModuleHandleA;
2121

2222
use chu_abi::{ChuModInfo, CHUMOD_API_VERSION};
2323

24+
use crate::config::Config;
2425
use crate::proxy::api_impl;
2526

2627
use self::log::{log_info, write_log_inner};
@@ -39,12 +40,16 @@ pub unsafe fn load_mods() {
3940
}
4041
state.loaded = true;
4142

42-
console::init(&mut state);
43-
4443
let base_dir = match get_self_base_dir() {
4544
Some(d) => d,
4645
None => return,
4746
};
47+
let config = Config::load(&base_dir);
48+
console::init(
49+
&mut state,
50+
config.get_bool("System", "EnableConsole", true),
51+
);
52+
4853
state.base_dir = base_dir.clone();
4954
state.log_file = File::create(format!("{}\\chumod_loader.log", base_dir)).ok();
5055
write_log_inner(&mut state, &format!("loader start: base={}", base_dir));

src/proxy/loader/state.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,22 @@ use std::ffi::c_void;
22
use std::fs::File;
33
use std::sync::Mutex;
44

5-
use windows_sys_loader::Win32::Foundation::{HANDLE, INVALID_HANDLE_VALUE};
5+
use windows_sys_loader::Win32::Foundation::HANDLE;
66

77
use chu_abi::{ChuModFrameFunc, ChuModReadyFunc, ChuModShutdownFunc};
88

99
pub type HMODULE = *mut c_void;
1010

11+
#[derive(Clone, Copy)]
12+
pub enum OutputSink {
13+
None,
14+
Console {
15+
handle: HANDLE,
16+
ansi_enabled: bool,
17+
},
18+
Stream(HANDLE),
19+
}
20+
1121
pub struct LoadedMod {
1222
pub handle: HMODULE,
1323
pub on_ready: Option<ChuModReadyFunc>,
@@ -28,7 +38,7 @@ pub struct LoaderState {
2838
pub manifest_paths: Vec<String>,
2939
pub log_file: Option<File>,
3040
pub current_mod_log_file: Option<File>,
31-
pub console: HANDLE,
41+
pub output: OutputSink,
3242
}
3343

3444
unsafe impl Send for LoaderState {}
@@ -43,7 +53,7 @@ impl Default for LoaderState {
4353
manifest_paths: Vec::new(),
4454
log_file: None,
4555
current_mod_log_file: None,
46-
console: INVALID_HANDLE_VALUE,
56+
output: OutputSink::None,
4757
}
4858
}
4959
}

0 commit comments

Comments
 (0)