mirror of
https://gitea.tendokyu.moe/moegrid/sega-mount.git
synced 2026-09-22 22:37:57 +03:00
添加了图标,内置了驱动,修改了界面
This commit is contained in:
+14
-20
@@ -38,7 +38,7 @@ if (WIN32)
|
||||
target_link_libraries(imgui PUBLIC d3d11 d3dcompiler dxgi)
|
||||
endif ()
|
||||
|
||||
# ===== 公共源文件(cmd 和 gui 共享) =====
|
||||
# ===== 公共源文件 =====
|
||||
set(SEGA_CORE_SOURCES
|
||||
src/config.cpp
|
||||
src/driver_service.cpp
|
||||
@@ -52,37 +52,30 @@ set(SEGA_CORE_SOURCES
|
||||
src/utils/files.cpp
|
||||
src/utils/log.cpp
|
||||
src/utils/strings.cpp
|
||||
src/utils/resource.cpp
|
||||
)
|
||||
|
||||
set(SEGA_WIN_LIBS bcrypt virtdisk)
|
||||
|
||||
# ===== 目标1: sega_mount_cmd (命令行版) =====
|
||||
add_executable(sega_mount_cmd
|
||||
src/cmd/main.cpp
|
||||
${SEGA_CORE_SOURCES}
|
||||
)
|
||||
target_include_directories(sega_mount_cmd PRIVATE src)
|
||||
if (WIN32)
|
||||
target_link_libraries(sega_mount_cmd PRIVATE ${SEGA_WIN_LIBS})
|
||||
endif ()
|
||||
set_target_properties(sega_mount_cmd PROPERTIES OUTPUT_NAME sega_mount_cmd)
|
||||
|
||||
# ===== 目标2: sega_mount_gui (GUI版, ImGui + Win32 + DirectX11) =====
|
||||
add_executable(sega_mount_gui
|
||||
src/gui/main.cpp
|
||||
# ===== 统一目标: sega_mount (GUI + CLI 合一) =====
|
||||
# 无参数启动 GUI;带参数(-m/-u)走 CLI 模式
|
||||
add_executable(sega_mount
|
||||
src/main.cpp
|
||||
src/gui/theme.cpp
|
||||
src/gui/imgui_spectrum.cpp
|
||||
src/gui/dx11_backend.cpp
|
||||
src/gui/gui_utils.cpp
|
||||
src/gui/ui.cpp
|
||||
res/resource.rc
|
||||
${SEGA_CORE_SOURCES}
|
||||
)
|
||||
target_include_directories(sega_mount_gui PRIVATE src)
|
||||
target_include_directories(sega_mount PRIVATE src res)
|
||||
if (WIN32)
|
||||
target_link_libraries(sega_mount_gui PRIVATE ${SEGA_WIN_LIBS} imgui comdlg32)
|
||||
# WinMain 入口需要 Windows 子系统(不弹控制台窗口)
|
||||
set_target_properties(sega_mount_gui PROPERTIES LINK_FLAGS "/SUBSYSTEM:WINDOWS")
|
||||
target_link_libraries(sega_mount PRIVATE ${SEGA_WIN_LIBS} imgui comdlg32)
|
||||
# WinMain 入口需要 Windows 子系统(无参数时不弹控制台窗口)
|
||||
set_target_properties(sega_mount PROPERTIES LINK_FLAGS "/SUBSYSTEM:WINDOWS")
|
||||
endif ()
|
||||
set_target_properties(sega_mount_gui PROPERTIES OUTPUT_NAME sega_mount_gui)
|
||||
set_target_properties(sega_mount PROPERTIES OUTPUT_NAME sega_mount)
|
||||
|
||||
# ===== 单元测试: 挂载/卸载 OPT 容器 =====
|
||||
add_executable(test_mount_opt
|
||||
@@ -94,6 +87,7 @@ add_executable(test_mount_opt
|
||||
src/utils/files.cpp
|
||||
src/utils/log.cpp
|
||||
src/utils/strings.cpp
|
||||
src/utils/resource.cpp
|
||||
)
|
||||
target_include_directories(test_mount_opt PRIVATE src)
|
||||
if (WIN32)
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
[CONFIG]
|
||||
ICF_PATH = D:\SDGB_1.55.01\amfs\ICF1
|
||||
IMAGE_DIR = D:\SDGB_1.55.01
|
||||
OVERLAY_DIR = D:\SDGB_1.55.01\overlay
|
||||
MOUNT_LETTER = X
|
||||
APP_LINK = C:\Mount\App
|
||||
OPT_LINK = C:\Mount\Option
|
||||
|
||||
[KEYS]
|
||||
APP_KEY = 7CA4E6B6F3D6E8B26472973887D7FA3A
|
||||
APP_IV = 53FE7135762DE3F97E7FE76B0FEF3F27
|
||||
OPT_KEY = 5C84A9E726EAA5DD351F2B0750C23697
|
||||
OPT_IV = C063BF6F562D084D7963C987F5281761
|
||||
@@ -0,0 +1,86 @@
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
|
||||
# 获取当前脚本文件所在的文件夹路径
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# 拼接出绝对路径,确保一定能找到同目录下的 icons.json
|
||||
JSON_FILE = os.path.join(SCRIPT_DIR, 'info.json')
|
||||
HEADER_FILE = os.path.join(SCRIPT_DIR, 'lucide.h')
|
||||
|
||||
|
||||
def hex_to_utf8_escape(hex_str):
|
||||
"""
|
||||
将 JSON 中的 '\\e585' 或 'e585' 转化为 C++ 可用的 UTF-8 转义序列,如 "\\xEE\\x96\\x85"
|
||||
"""
|
||||
# 提取纯十六进制字符串
|
||||
clean_hex = hex_str.replace('\\', '').replace('e', 'E', 1) if hex_str.startswith('\\') else hex_str
|
||||
code_point = int(clean_hex, 16)
|
||||
|
||||
# 编码为 UTF-8 字节
|
||||
utf8_bytes = chr(code_point).encode('utf-8')
|
||||
|
||||
# 转为 C++ 转义字符串格式 "\\xEE\\x96\\x85"
|
||||
return ''.join([f'\\x{b:02X}' for b in utf8_bytes])
|
||||
|
||||
|
||||
def sanitize_macro_name(key):
|
||||
"""
|
||||
将图标 key (如 'a-arrow-down') 转换为合法的 C++ 宏名称 (如 'ICON_A_ARROW_DOWN')
|
||||
"""
|
||||
# 将连字符/空格等替换为下划线,转大写
|
||||
clean_key = re.sub(r'[^a-zA-Z0-9_]', '_', key)
|
||||
return f"ICON_{clean_key.upper()}"
|
||||
|
||||
|
||||
def generate_header():
|
||||
with open(JSON_FILE, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
macros = []
|
||||
min_code = 0xFFFF
|
||||
max_code = 0x0000
|
||||
|
||||
for key, info in data.items():
|
||||
encoded_code = info.get('encodedCode', '')
|
||||
if not encoded_code:
|
||||
continue
|
||||
|
||||
# 解析 Unicode 码位,用于计算范围
|
||||
clean_hex = encoded_code.replace('\\', '')
|
||||
code_point = int(clean_hex, 16)
|
||||
|
||||
if code_point < min_code:
|
||||
min_code = code_point
|
||||
if code_point > max_code:
|
||||
max_code = code_point
|
||||
|
||||
macro_name = sanitize_macro_name(key)
|
||||
utf8_str = hex_to_utf8_escape(encoded_code)
|
||||
|
||||
macros.append((macro_name, utf8_str, key, encoded_code))
|
||||
|
||||
# 生成 C++ 头文件内容
|
||||
header_content = []
|
||||
header_content.append("// Auto-generated by generate_header.py")
|
||||
header_content.append("#pragma once\n")
|
||||
|
||||
# 记录码位范围(ImGui 加载字体时需要用到)
|
||||
header_content.append(f"#define ICON_MIN 0x{min_code:04X}")
|
||||
header_content.append(f"#define ICON_MAX 0x{max_code:04X}\n")
|
||||
|
||||
# 生成宏定义
|
||||
for macro_name, utf8_str, original_key, raw_hex in macros:
|
||||
# 对齐排版
|
||||
header_content.append(f'#define {macro_name:<30} "{utf8_str}" // {raw_hex} ({original_key})')
|
||||
|
||||
with open(HEADER_FILE, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(header_content))
|
||||
|
||||
print(f"✅ 成功生成头文件 {HEADER_FILE}!共处理了 {len(macros)} 个图标。")
|
||||
print(f"📊 码位范围: 0x{min_code:04X} - 0x{max_code:04X}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
generate_header()
|
||||
+12044
File diff suppressed because it is too large
Load Diff
+2013
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -7,11 +7,13 @@ KEY/IV 直接生成为字节数组,C++ 端无需 hex_to_bytes 转换
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
JSON_PATH = Path("./res/keys.json")
|
||||
OUTPUT_PATH = Path("./src/utils/game_keys.h")
|
||||
# 获取当前脚本文件所在的文件夹路径
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
JSON_PATH = os.path.join(SCRIPT_DIR, "./keys.json")
|
||||
OUTPUT_PATH = os.path.join(SCRIPT_DIR, "./game_keys.h")
|
||||
|
||||
|
||||
def hex_to_c_array(hex_str):
|
||||
@@ -22,10 +24,6 @@ def hex_to_c_array(hex_str):
|
||||
|
||||
|
||||
def main():
|
||||
if not JSON_PATH.exists():
|
||||
print("Error: " + str(JSON_PATH) + " not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with open(JSON_PATH, "r", encoding="utf-8") as f:
|
||||
entries = json.load(f)
|
||||
|
||||
@@ -61,7 +59,6 @@ def main():
|
||||
out.append("")
|
||||
|
||||
content = "\n".join(out)
|
||||
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// Windows 资源文件 — 将字体和驱动文件嵌入 exe
|
||||
#include "resource.h"
|
||||
|
||||
IDR_LUCIDE_FONT RCDATA "..\\res\\icons\\lucide.ttf"
|
||||
IDR_DRIVER_SYS RCDATA "..\\res\\driver\\sgfscrypt.sys"
|
||||
IDR_DRIVER_INF RCDATA "..\\res\\driver\\sgfscrypt.inf"
|
||||
@@ -1,100 +0,0 @@
|
||||
#define LOG_MODULE "MAIN"
|
||||
#include "config.h"
|
||||
#include "driver_service.h"
|
||||
#include "icf_mount.h"
|
||||
#include "utils/strings.h"
|
||||
#include "utils/files.h"
|
||||
#include "utils/log.h"
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
|
||||
static void print_usage() {
|
||||
std::fputs(
|
||||
"SEGA MOUNT\n"
|
||||
"Usage:\n"
|
||||
" sega_mount -m [config.ini] Mount\n"
|
||||
" sega_mount -u [config.ini] Unmount\n",
|
||||
stdout
|
||||
);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
// 设置控制台为UTF-8代码页,配合/utf-8编译选项避免中文乱码
|
||||
SetConsoleOutputCP(CP_UTF8);
|
||||
SetConsoleCP(CP_UTF8);
|
||||
|
||||
if (argc < 2) {
|
||||
print_usage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 解析命令:-m 挂载,-u 卸载
|
||||
std::string cmd = argv[1];
|
||||
bool is_mount = cmd == "-m";
|
||||
bool is_unmount = cmd == "-u";
|
||||
if (!is_mount && !is_unmount) {
|
||||
LOG_ERROR("Unknown command: {}", cmd);
|
||||
print_usage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 确定config.ini路径:优先命令行参数,否则使用当前运行目录
|
||||
if (argc >= 3) {
|
||||
g_config_path_w = utils::to_wstring(argv[2]);
|
||||
} else {
|
||||
g_config_path_w = std::format(L"{}\\config.ini", utils::get_cwd());
|
||||
}
|
||||
|
||||
// 解析config.ini 到全局 g_cfg
|
||||
std::string cfg_err;
|
||||
if (!parse_config(g_config_path_w, g_cfg, &cfg_err)) {
|
||||
LOG_ERROR("{}", cfg_err);
|
||||
return 1;
|
||||
}
|
||||
g_config_path = utils::wstr_to_str(g_config_path_w);
|
||||
|
||||
if (g_cfg.icf_path.empty()) {
|
||||
LOG_ERROR("ICF_PATH is empty in config");
|
||||
return 1;
|
||||
}
|
||||
|
||||
LOG_INFO("Config loaded from {}", g_config_path);
|
||||
LOG_INFO(" ICF_PATH: {}", utils::wstr_to_str(g_cfg.icf_path));
|
||||
LOG_INFO(" IMAGE_DIR: {}", utils::wstr_to_str(g_cfg.image_dir));
|
||||
LOG_INFO(" OVERLAY_DIR: {}", utils::wstr_to_str(g_cfg.overlay_dir));
|
||||
LOG_INFO(" MOUNT_LETTER: {}", static_cast<char>(g_cfg.mount_letter));
|
||||
LOG_INFO(" APP_LINK: {}", utils::wstr_to_str(g_cfg.app_link));
|
||||
LOG_INFO(" OPT_LINK: {}", utils::wstr_to_str(g_cfg.opt_link));
|
||||
LOG_INFO(" APP_KEY: {}", g_cfg.app_key.empty() ? "(use built-in table)" : "(override)");
|
||||
LOG_INFO(" OPT_KEY: {}", g_cfg.opt_key.empty() ? "(use built-in table)" : "(override)");
|
||||
|
||||
// 确保sgfscrypt服务已安装并运行
|
||||
std::string svc_err;
|
||||
if (!driver::ensure_running(&svc_err)) {
|
||||
LOG_ERROR("Failed to start driver service: {}", svc_err);
|
||||
LOG_ERROR("Hint: run this program as Administrator");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (is_mount) {
|
||||
auto result = icf_mount::mount_icf_apps(g_cfg);
|
||||
if (!result.success) {
|
||||
LOG_ERROR("Mount failed: {}", result.error);
|
||||
return 1;
|
||||
}
|
||||
LOG_INFO("Mount Success: {} APPs, {} OPTs mounted",
|
||||
result.apps.size(), result.opts.size());
|
||||
return 0;
|
||||
}
|
||||
|
||||
// is_unmount
|
||||
auto result = icf_mount::unmount_icf_apps(g_cfg);
|
||||
if (!result.success) {
|
||||
LOG_ERROR("Unmount failed: {}", result.error);
|
||||
return 1;
|
||||
}
|
||||
LOG_INFO("Unmount Success: {} APPs, {} OPTs unmounted",
|
||||
result.apps.size(), result.opts.size());
|
||||
return 0;
|
||||
}
|
||||
+1
-1
@@ -9,7 +9,7 @@ std::wstring g_config_path_w;
|
||||
|
||||
bool parse_config(const std::wstring &path, Config &cfg, std::string *error) {
|
||||
if (GetFileAttributesW(path.c_str()) == INVALID_FILE_ATTRIBUTES) {
|
||||
if (error) *error = "Config file not found: " + utils::wstr_to_str(path);
|
||||
if (error) *error = "配置文件未找到:" + utils::wstr_to_str(path);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+17
-17
@@ -1,17 +1,20 @@
|
||||
#define LOG_MODULE "DRIVER"
|
||||
#include "driver_service.h"
|
||||
#include "resource.h"
|
||||
#include "utils/files.h"
|
||||
#include "utils/strings.h"
|
||||
#include "utils/resource.h"
|
||||
#include "utils/log.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
|
||||
namespace driver {
|
||||
|
||||
bool ensure_running(std::string *error) {
|
||||
SC_HANDLE hScm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS);
|
||||
if (!hScm) {
|
||||
if (error) *error = "OpenSCManager failed: " + utils::last_error_str();
|
||||
if (error) *error = "OpenSCManager 失败:" + utils::last_error_str();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -21,31 +24,28 @@ namespace driver {
|
||||
if (!hSvc) {
|
||||
DWORD last = GetLastError();
|
||||
if (last != ERROR_SERVICE_DOES_NOT_EXIST) {
|
||||
if (error) *error = "OpenService failed: " + utils::last_error_str(last);
|
||||
if (error) *error = "OpenService 失败:" + utils::last_error_str(last);
|
||||
CloseServiceHandle(hScm);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Service does not exist - copy driver to system directory and create service
|
||||
std::wstring src_path = utils::get_exe_dir() + L"\\" + DRIVER_FILENAME;
|
||||
|
||||
// Service does not exist - extract embedded driver to system directory and create service
|
||||
// Get system drivers directory (typically C:\Windows\System32\drivers)
|
||||
wchar_t sys_dir[MAX_PATH] = {};
|
||||
UINT sys_len = GetSystemDirectoryW(sys_dir, MAX_PATH);
|
||||
if (sys_len == 0 || sys_len > MAX_PATH) {
|
||||
if (error) *error = "GetSystemDirectory failed: " + utils::last_error_str();
|
||||
if (error) *error = "GetSystemDirectory 失败:" + utils::last_error_str();
|
||||
CloseServiceHandle(hScm);
|
||||
return false;
|
||||
}
|
||||
std::wstring dst_path = std::wstring(sys_dir) + L"\\drivers\\" + DRIVER_FILENAME;
|
||||
|
||||
// Copy driver file to system directory (overwrite if exists)
|
||||
if (!CopyFileW(src_path.c_str(), dst_path.c_str(), FALSE)) {
|
||||
if (error) *error = "Copy driver to system directory failed: " + utils::last_error_str();
|
||||
// 从嵌入资源提取驱动文件到系统目录(覆盖已有文件)
|
||||
if (!utils::extract_resource_to_file(IDR_DRIVER_SYS, dst_path, error)) {
|
||||
CloseServiceHandle(hScm);
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("Driver copied to {}", utils::wstr_to_str(dst_path));
|
||||
LOG_INFO("驱动已提取到 {}", utils::wstr_to_str(dst_path));
|
||||
|
||||
hSvc = CreateServiceW(
|
||||
hScm, SERVICE_NAME, SERVICE_NAME,
|
||||
@@ -58,17 +58,17 @@ namespace driver {
|
||||
);
|
||||
|
||||
if (!hSvc) {
|
||||
if (error) *error = "CreateService failed: " + utils::last_error_str();
|
||||
if (error) *error = "CreateService 失败:" + utils::last_error_str();
|
||||
CloseServiceHandle(hScm);
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("Service installed: {}", utils::wstr_to_str(dst_path));
|
||||
LOG_INFO("服务已安装:{}", utils::wstr_to_str(dst_path));
|
||||
}
|
||||
|
||||
// Query current state
|
||||
SERVICE_STATUS status{};
|
||||
if (!QueryServiceStatus(hSvc, &status)) {
|
||||
if (error) *error = "QueryServiceStatus failed: " + utils::last_error_str();
|
||||
if (error) *error = "QueryServiceStatus 失败:" + utils::last_error_str();
|
||||
CloseServiceHandle(hSvc);
|
||||
CloseServiceHandle(hScm);
|
||||
return false;
|
||||
@@ -84,7 +84,7 @@ namespace driver {
|
||||
if (!StartServiceW(hSvc, 0, nullptr)) {
|
||||
DWORD last = GetLastError();
|
||||
if (last != ERROR_SERVICE_ALREADY_RUNNING) {
|
||||
if (error) *error = "StartService failed: " + utils::last_error_str(last);
|
||||
if (error) *error = "StartService 失败:" + utils::last_error_str(last);
|
||||
CloseServiceHandle(hSvc);
|
||||
CloseServiceHandle(hScm);
|
||||
return false;
|
||||
@@ -96,7 +96,7 @@ namespace driver {
|
||||
while (tries-- > 0) {
|
||||
if (!QueryServiceStatus(hSvc, &status)) break;
|
||||
if (status.dwCurrentState == SERVICE_RUNNING) {
|
||||
LOG_INFO("Service started");
|
||||
LOG_INFO("服务已启动");
|
||||
CloseServiceHandle(hSvc);
|
||||
CloseServiceHandle(hScm);
|
||||
return true;
|
||||
@@ -106,13 +106,13 @@ namespace driver {
|
||||
}
|
||||
|
||||
if (status.dwCurrentState != SERVICE_RUNNING) {
|
||||
if (error) *error = "Service failed to reach RUNNING state";
|
||||
if (error) *error = "服务未能进入运行状态";
|
||||
CloseServiceHandle(hSvc);
|
||||
CloseServiceHandle(hScm);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("Service started");
|
||||
LOG_INFO("服务已启动");
|
||||
CloseServiceHandle(hSvc);
|
||||
CloseServiceHandle(hScm);
|
||||
return true;
|
||||
|
||||
+27
-27
@@ -2,8 +2,8 @@
|
||||
#include "fscrypt.h"
|
||||
#include "utils/strings.h"
|
||||
#include "utils/aes.h"
|
||||
#include "utils/crypto_constants.h"
|
||||
#include "utils/game_keys.h"
|
||||
#include "crypto_constants.h"
|
||||
#include "keys/game_keys.h"
|
||||
#include "utils/log.h"
|
||||
#include <cstdio>
|
||||
#include <format>
|
||||
@@ -29,7 +29,7 @@ namespace fscrypt {
|
||||
// 解析解密后的BootId,提取容器类型、数据偏移、扇区大小等信息
|
||||
static bool parse_boot_id(const uint8_t *decrypted_bootid, size_t size, BootIdInfo *info, std::string *error) {
|
||||
if (size < 0x38) {
|
||||
if (error) *error = "Decrypted BootId is too small";
|
||||
if (error) *error = "解密后的 BootId 太小";
|
||||
return false;
|
||||
}
|
||||
info->type = static_cast<ContainerType>(decrypted_bootid[13]);
|
||||
@@ -65,7 +65,7 @@ namespace fscrypt {
|
||||
*image_type = "OPTION";
|
||||
return true;
|
||||
default:
|
||||
if (error) *error = std::format("Unsupported BootId container type: {}", static_cast<int>(type));
|
||||
if (error) *error = std::format("不支持的 BootId 容器类型:{}", static_cast<int>(type));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@ namespace fscrypt {
|
||||
|
||||
// GameID = 文件名前4个字符
|
||||
if (filename.size() < 4) {
|
||||
if (error) *error = "Filename too short for GameID";
|
||||
if (error) *error = "文件名太短,无法提取 GameID";
|
||||
return false;
|
||||
}
|
||||
std::string game_id;
|
||||
@@ -94,7 +94,7 @@ namespace fscrypt {
|
||||
bool is_app = filename.size() >= 4 && _wcsicmp(filename.substr(filename.size() - 4).c_str(), L".app") == 0;
|
||||
bool is_opt = filename.size() >= 4 && _wcsicmp(filename.substr(filename.size() - 4).c_str(), L".opt") == 0;
|
||||
if (!is_app && !is_opt) {
|
||||
if (error) *error = "Filename must end with .app or .opt";
|
||||
if (error) *error = "文件名必须以 .app 或 .opt 结尾";
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ namespace fscrypt {
|
||||
if (!key_hex.empty() && !iv_hex.empty()) {
|
||||
if (!utils::hex_to_bytes(key_hex, out_key, 16) ||
|
||||
!utils::hex_to_bytes(iv_hex, out_iv, 16)) {
|
||||
if (error) *error = std::format("Invalid hex in set_key: {}/{}", key_hex, iv_hex);
|
||||
if (error) *error = std::format("set_key 中十六进制无效:{}/{}", key_hex, iv_hex);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -121,7 +121,7 @@ namespace fscrypt {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (error) *error = std::format("GameID {} not found in keys table", game_id);
|
||||
if (error) *error = std::format("GameID {} 在密钥表中未找到", game_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ namespace fscrypt {
|
||||
nullptr
|
||||
);
|
||||
if (hDriver == INVALID_HANDLE_VALUE) {
|
||||
result.error = std::format("Open Fscrypt Driver Failed: {}", utils::last_error_str());
|
||||
result.error = std::format("打开 Fscrypt 驱动失败:{}", utils::last_error_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ namespace fscrypt {
|
||||
uint8_t app_iv[16];
|
||||
std::string err;
|
||||
if (!lookup_key_iv(container_path, key, app_iv, &err)) {
|
||||
result.error = std::format("Lookup key/IV failed: {}", err);
|
||||
result.error = std::format("查找密钥/IV 失败:{}", err);
|
||||
CloseHandle(hDriver);
|
||||
return result;
|
||||
}
|
||||
@@ -159,14 +159,14 @@ namespace fscrypt {
|
||||
// Read container BootId (0x60 bytes)
|
||||
FILE *container_file = nullptr;
|
||||
if (_wfopen_s(&container_file, container_path.c_str(), L"rb") != 0 || !container_file) {
|
||||
result.error = "Open container file failed";
|
||||
result.error = "打开容器文件失败";
|
||||
CloseHandle(hDriver);
|
||||
return result;
|
||||
}
|
||||
uint8_t bootid_encrypted[BOOTID_SIZE] = {};
|
||||
size_t bootid_read = fread(bootid_encrypted, 1, BOOTID_SIZE, container_file);
|
||||
if (bootid_read != BOOTID_SIZE) {
|
||||
result.error = "Read BootId failed";
|
||||
result.error = "读取 BootId 失败";
|
||||
fclose(container_file);
|
||||
CloseHandle(hDriver);
|
||||
return result;
|
||||
@@ -177,7 +177,7 @@ namespace fscrypt {
|
||||
auto bootid_decrypted = aes::cbc_decrypt(crypto::BOOT_KEY, crypto::BOOT_IV,
|
||||
bootid_encrypted, BOOTID_SIZE, &err);
|
||||
if (bootid_decrypted.empty()) {
|
||||
result.error = std::format("Decrypt BootId failed: {}", err);
|
||||
result.error = std::format("解密 BootId 失败:{}", err);
|
||||
fclose(container_file);
|
||||
CloseHandle(hDriver);
|
||||
return result;
|
||||
@@ -186,7 +186,7 @@ namespace fscrypt {
|
||||
// Parse BootId
|
||||
BootIdInfo info{};
|
||||
if (!parse_boot_id(bootid_decrypted.data(), bootid_decrypted.size(), &info, &err)) {
|
||||
result.error = std::format("Parse BootId failed: {}", err);
|
||||
result.error = std::format("解析 BootId 失败:{}", err);
|
||||
fclose(container_file);
|
||||
CloseHandle(hDriver);
|
||||
return result;
|
||||
@@ -196,7 +196,7 @@ namespace fscrypt {
|
||||
const char *container_name = nullptr;
|
||||
const char *image_type = nullptr;
|
||||
if (!recognize_container(info.type, &container_name, &image_type, &err)) {
|
||||
result.error = std::format("Recognize BootId container failed: {}", err);
|
||||
result.error = std::format("识别 BootId 容器失败:{}", err);
|
||||
fclose(container_file);
|
||||
CloseHandle(hDriver);
|
||||
return result;
|
||||
@@ -261,7 +261,7 @@ namespace fscrypt {
|
||||
&in_buf, sizeof(in_buf),
|
||||
out_buf, sizeof(out_buf),
|
||||
&bytes_returned, nullptr)) {
|
||||
result.error = std::format("DeviceIoControl failed: {}", utils::last_error_str());
|
||||
result.error = std::format("DeviceIoControl 失败:{}", utils::last_error_str());
|
||||
CloseHandle(hDriver);
|
||||
return result;
|
||||
}
|
||||
@@ -270,9 +270,9 @@ namespace fscrypt {
|
||||
if (bytes_returned >= sizeof(wchar_t) && out_buf[0] != 0) {
|
||||
result.nt_target = out_buf; // 含 \??\FscryptDisk_<tag>\ .
|
||||
result.success = true;
|
||||
LOG_INFO("Mounted: {}", utils::wstr_to_str(result.nt_target));
|
||||
LOG_INFO("已挂载:{}", utils::wstr_to_str(result.nt_target));
|
||||
} else {
|
||||
result.error = "Mount IOCTL succeeded but output buffer is empty";
|
||||
result.error = "Mount IOCTL 成功但输出缓冲区为空";
|
||||
result.success = false;
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ namespace fscrypt {
|
||||
nullptr
|
||||
);
|
||||
if (hDriver == INVALID_HANDLE_VALUE) {
|
||||
LOG_ERROR("Open Fscrypt Driver Failed: {}", utils::last_error_str());
|
||||
LOG_ERROR("打开 Fscrypt 驱动失败:{}", utils::last_error_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -327,7 +327,7 @@ namespace fscrypt {
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_ERROR("UNMOUNT failed: {}", utils::last_error_str());
|
||||
LOG_ERROR("UNMOUNT 失败:{}", utils::last_error_str());
|
||||
CloseHandle(hDriver);
|
||||
return false;
|
||||
}
|
||||
@@ -346,7 +346,7 @@ namespace fscrypt {
|
||||
nullptr
|
||||
);
|
||||
if (hCheck == INVALID_HANDLE_VALUE) {
|
||||
LOG_ERROR("LINK failed: Non-existent device");
|
||||
LOG_ERROR("LINK 失败:设备不存在");
|
||||
return false;
|
||||
}
|
||||
CloseHandle(hCheck);
|
||||
@@ -406,7 +406,7 @@ namespace fscrypt {
|
||||
std::error_code mk_ec;
|
||||
fs::create_directories(link_path, mk_ec);
|
||||
if (mk_ec && !fs::exists(link_path)) {
|
||||
LOG_ERROR("CreateDirectory failed for {}: {} {}", utils::wstr_to_str(link_path), mk_ec.value(),
|
||||
LOG_ERROR("创建目录失败 {}:{} {}", utils::wstr_to_str(link_path), mk_ec.value(),
|
||||
mk_ec.message());
|
||||
return false;
|
||||
}
|
||||
@@ -422,7 +422,7 @@ namespace fscrypt {
|
||||
nullptr
|
||||
);
|
||||
if (hDir == INVALID_HANDLE_VALUE) {
|
||||
LOG_ERROR("CreateFileW failed for {}: {}", utils::wstr_to_str(link_path), utils::last_error_str());
|
||||
LOG_ERROR("CreateFileW 失败 {}:{}", utils::wstr_to_str(link_path), utils::last_error_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -431,12 +431,12 @@ namespace fscrypt {
|
||||
reparse_buf.data(), static_cast<DWORD>(buf_size),
|
||||
nullptr, 0, &bytes_returned, nullptr);
|
||||
if (!ok) {
|
||||
LOG_ERROR("FSCTL_SET_REPARSE_POINT failed: {}", utils::last_error_str());
|
||||
LOG_ERROR("FSCTL_SET_REPARSE_POINT 失败:{}", utils::last_error_str());
|
||||
}
|
||||
CloseHandle(hDir);
|
||||
|
||||
if (ok) {
|
||||
LOG_INFO("Linked {} -> {}", utils::wstr_to_str(target), utils::wstr_to_str(source));
|
||||
LOG_INFO("已链接 {} -> {}", utils::wstr_to_str(target), utils::wstr_to_str(source));
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
@@ -455,10 +455,10 @@ namespace fscrypt {
|
||||
|
||||
// mount point重解析点可直接用RemoveDirectoryW移除(会删除junction而非目标内容)
|
||||
if (!RemoveDirectoryW(path.c_str())) {
|
||||
LOG_ERROR("RemoveDirectory failed for {}: {}", utils::wstr_to_str(path), utils::last_error_str());
|
||||
LOG_ERROR("RemoveDirectory 失败 {}:{}", utils::wstr_to_str(path), utils::last_error_str());
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("Removed link {}", utils::wstr_to_str(path));
|
||||
LOG_INFO("已移除链接 {}", utils::wstr_to_str(path));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+15
-11
@@ -23,16 +23,20 @@ bool CreateDeviceD3D(HWND hWnd) {
|
||||
|
||||
UINT createDeviceFlags = 0;
|
||||
D3D_FEATURE_LEVEL featureLevel;
|
||||
const D3D_FEATURE_LEVEL featureLevelArray[2] = {D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_0};
|
||||
HRESULT res = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr,
|
||||
createDeviceFlags, featureLevelArray, 2,
|
||||
D3D11_SDK_VERSION, &sd, &g_pSwapChain,
|
||||
&g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext);
|
||||
constexpr D3D_FEATURE_LEVEL featureLevelArray[2] = {D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_0};
|
||||
HRESULT res = D3D11CreateDeviceAndSwapChain(
|
||||
nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr,
|
||||
createDeviceFlags, featureLevelArray, 2,
|
||||
D3D11_SDK_VERSION, &sd, &g_pSwapChain,
|
||||
&g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext
|
||||
);
|
||||
if (res == DXGI_ERROR_UNSUPPORTED)
|
||||
res = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_WARP, nullptr,
|
||||
createDeviceFlags, featureLevelArray, 2,
|
||||
D3D11_SDK_VERSION, &sd, &g_pSwapChain,
|
||||
&g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext);
|
||||
res = D3D11CreateDeviceAndSwapChain(
|
||||
nullptr, D3D_DRIVER_TYPE_WARP, nullptr,
|
||||
createDeviceFlags, featureLevelArray, 2,
|
||||
D3D11_SDK_VERSION, &sd, &g_pSwapChain,
|
||||
&g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext
|
||||
);
|
||||
if (res != S_OK)
|
||||
return false;
|
||||
|
||||
@@ -78,8 +82,8 @@ LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
||||
case WM_SIZE:
|
||||
if (wParam == SIZE_MINIMIZED)
|
||||
return 0;
|
||||
g_ResizeWidth = (UINT) LOWORD(lParam);
|
||||
g_ResizeHeight = (UINT) HIWORD(lParam);
|
||||
g_ResizeWidth = static_cast<UINT>(LOWORD(lParam));
|
||||
g_ResizeHeight = static_cast<UINT>(HIWORD(lParam));
|
||||
return 0;
|
||||
case WM_SYSCOMMAND:
|
||||
if ((wParam & 0xfff0) == SC_KEYMENU)
|
||||
|
||||
@@ -24,3 +24,6 @@ inline ID3D11RenderTargetView *g_mainRenderTargetView = nullptr;
|
||||
|
||||
// ===== 驱动状态 =====
|
||||
inline bool g_driver_ready = false;
|
||||
|
||||
// ===== 主题状态 =====
|
||||
inline bool g_dark_theme = true; // true=暗色, false=亮色
|
||||
|
||||
@@ -12,7 +12,7 @@ void load_config_file(const std::wstring &path) {
|
||||
g_config_path = utils::wstr_to_str(path);
|
||||
std::string err;
|
||||
if (parse_config(path, g_cfg, &err)) {
|
||||
g_log_lines.push_back(std::format("[INFO] Config loaded: {}", g_config_path));
|
||||
g_log_lines.push_back(std::format("[INFO] 配置已加载:{}", g_config_path));
|
||||
} else {
|
||||
g_log_lines.push_back(std::format("[ERROR] {}", err));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// Adobe Spectrum 主题 for ImGui(精简版,不含内嵌字体数据)
|
||||
// 来源:https://github.com/adizorde/imgui-spectrum
|
||||
// 字体加载与压缩字体数据已移除,项目使用自带的中文字体(msyh.ttc)
|
||||
#include "imgui_spectrum.h"
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
namespace ImGui::Spectrum {
|
||||
// 浅色调色板
|
||||
static const SpectrumPalette LightPalette = {
|
||||
/* GRAY50..900 */ Color(0xFFFFFF), Color(0xFAFAFA), Color(0xF5F5F5), Color(0xEAEAEA), Color(0xE1E1E1),
|
||||
Color(0xCACACA), Color(0xB3B3B3), Color(0x8E8E8E), Color(0x707070), Color(0x4B4B4B), Color(0x2C2C2C),
|
||||
/* BLUE */ Color(0x2680EB), Color(0x1473E6), Color(0x0D66D0), Color(0x095ABA),
|
||||
/* RED */ Color(0xE34850), Color(0xD7373F), Color(0xC9252D), Color(0xBB121A),
|
||||
/* ORANGE */ Color(0xE68619), Color(0xDA7B11), Color(0xCB6F10), Color(0xBD640D),
|
||||
/* GREEN */ Color(0x2D9D78), Color(0x268E6C), Color(0x12805C), Color(0x107154),
|
||||
/* INDIGO */ Color(0x6767EC), Color(0x5C5CE0), Color(0x5151D3), Color(0x4646C6),
|
||||
/* CELERY */ Color(0x44B556), Color(0x3DA74E), Color(0x379947), Color(0x318B40),
|
||||
/* MAGENTA */ Color(0xD83790), Color(0xCE2783), Color(0xBC1C74), Color(0xAE0E66),
|
||||
/* YELLOW */ Color(0xDFBF00), Color(0xD2B200), Color(0xC4A600), Color(0xB79900),
|
||||
/* FUCHSIA */ Color(0xC038CC), Color(0xB130BD), Color(0xA228AD), Color(0x93219E),
|
||||
/* SEAFOAM */ Color(0x1B959A), Color(0x16878C), Color(0x0F797D), Color(0x096C6F),
|
||||
/* CHARTREUSE */ Color(0x85D044), Color(0x7CC33F), Color(0x73B53A), Color(0x6AA834),
|
||||
/* PURPLE */ Color(0x9256D9), Color(0x864CCC), Color(0x7A42BF), Color(0x6F38B1),
|
||||
};
|
||||
|
||||
// 暗色调色板
|
||||
static const SpectrumPalette DarkPalette = {
|
||||
/* GRAY50..900 */ Color(0x252525), Color(0x2F2F2F), Color(0x323232), Color(0x393939), Color(0x3E3E3E),
|
||||
Color(0x4D4D4D), Color(0x5C5C5C), Color(0x7B7B7B), Color(0x999999), Color(0xCDCDCD), Color(0xFFFFFF),
|
||||
/* BLUE */ Color(0x2680EB), Color(0x378EF0), Color(0x4B9CF5), Color(0x5AA9FA),
|
||||
/* RED */ Color(0xE34850), Color(0xEC5B62), Color(0xF76D74), Color(0xFF7B82),
|
||||
/* ORANGE */ Color(0xE68619), Color(0xF29423), Color(0xF9A43F), Color(0xFFB55B),
|
||||
/* GREEN */ Color(0x2D9D78), Color(0x33AB84), Color(0x39B990), Color(0x3FC89C),
|
||||
/* INDIGO */ Color(0x6767EC), Color(0x7575F1), Color(0x8282F6), Color(0x9090FA),
|
||||
/* CELERY */ Color(0x44B556), Color(0x4BC35F), Color(0x51D267), Color(0x58E06F),
|
||||
/* MAGENTA */ Color(0xD83790), Color(0xE2499D), Color(0xEC5AAA), Color(0xF56BB7),
|
||||
/* YELLOW */ Color(0xDFBF00), Color(0xEDCC00), Color(0xFAD900), Color(0xFFE22E),
|
||||
/* FUCHSIA */ Color(0xC038CC), Color(0xCF3EDC), Color(0xD951E5), Color(0xE366EF),
|
||||
/* SEAFOAM */ Color(0x1B959A), Color(0x20A3A8), Color(0x23B2B8), Color(0x26C0C7),
|
||||
/* CHARTREUSE */ Color(0x85D044), Color(0x8EDE49), Color(0x9BEC54), Color(0xA3F858),
|
||||
/* PURPLE */ Color(0x9256D9), Color(0x9D64E1), Color(0xA873E9), Color(0xB483F0),
|
||||
};
|
||||
|
||||
const SpectrumPalette *Colors = &LightPalette;
|
||||
|
||||
// 应用 Spectrum 主题样式。dark=true 使用暗色主题。
|
||||
void StyleColorsSpectrum(bool dark) {
|
||||
Colors = dark ? &DarkPalette : &LightPalette;
|
||||
|
||||
ImGuiStyle *style = &ImGui::GetStyle();
|
||||
style->WindowRounding = 4.0f;
|
||||
style->FrameRounding = 4.0f;
|
||||
style->FrameBorderSize = 1.0f;
|
||||
style->GrabRounding = 4.0f;
|
||||
|
||||
ImVec4 *col = style->Colors;
|
||||
col[ImGuiCol_Text] = ColorConvertU32ToFloat4(Colors->GRAY800); // text on hovered controls is gray900
|
||||
col[ImGuiCol_TextDisabled] = ColorConvertU32ToFloat4(Colors->GRAY500);
|
||||
col[ImGuiCol_WindowBg] = ColorConvertU32ToFloat4(Colors->GRAY100);
|
||||
col[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f);
|
||||
col[ImGuiCol_PopupBg] = ColorConvertU32ToFloat4(Colors->GRAY50);
|
||||
// not sure about this. Note: applies to tooltips too.
|
||||
col[ImGuiCol_Border] = ColorConvertU32ToFloat4(Colors->GRAY300);
|
||||
col[ImGuiCol_BorderShadow] = ColorConvertU32ToFloat4(Spectrum::Static::NONE); // We don't want shadows. Ever.
|
||||
col[ImGuiCol_FrameBg] = ColorConvertU32ToFloat4(Colors->GRAY75);
|
||||
// this isnt right, spectrum does not do this, but it's a good fallback
|
||||
col[ImGuiCol_FrameBgHovered] = ColorConvertU32ToFloat4(Colors->GRAY50);
|
||||
col[ImGuiCol_FrameBgActive] = ColorConvertU32ToFloat4(Colors->GRAY200);
|
||||
col[ImGuiCol_TitleBg] = ColorConvertU32ToFloat4(Colors->GRAY300);
|
||||
// those titlebar values are totally made up, spectrum does not have this.
|
||||
col[ImGuiCol_TitleBgActive] = ColorConvertU32ToFloat4(Colors->GRAY200);
|
||||
col[ImGuiCol_TitleBgCollapsed] = ColorConvertU32ToFloat4(Colors->GRAY400);
|
||||
col[ImGuiCol_MenuBarBg] = ColorConvertU32ToFloat4(Colors->GRAY100);
|
||||
col[ImGuiCol_ScrollbarBg] = ColorConvertU32ToFloat4(Colors->GRAY100); // same as regular background
|
||||
col[ImGuiCol_ScrollbarGrab] = ColorConvertU32ToFloat4(Colors->GRAY400);
|
||||
col[ImGuiCol_ScrollbarGrabHovered] = ColorConvertU32ToFloat4(Colors->GRAY600);
|
||||
col[ImGuiCol_ScrollbarGrabActive] = ColorConvertU32ToFloat4(Colors->GRAY700);
|
||||
col[ImGuiCol_CheckMark] = ColorConvertU32ToFloat4(Colors->BLUE500);
|
||||
col[ImGuiCol_SliderGrab] = ColorConvertU32ToFloat4(Colors->GRAY700);
|
||||
col[ImGuiCol_SliderGrabActive] = ColorConvertU32ToFloat4(Colors->GRAY800);
|
||||
col[ImGuiCol_Button] = ColorConvertU32ToFloat4(Colors->GRAY75);
|
||||
// match default button to Spectrum's 'Action Button'.
|
||||
col[ImGuiCol_ButtonHovered] = ColorConvertU32ToFloat4(Colors->GRAY50);
|
||||
col[ImGuiCol_ButtonActive] = ColorConvertU32ToFloat4(Colors->GRAY200);
|
||||
col[ImGuiCol_Header] = ColorConvertU32ToFloat4(Colors->BLUE400);
|
||||
col[ImGuiCol_HeaderHovered] = ColorConvertU32ToFloat4(Colors->BLUE500);
|
||||
col[ImGuiCol_HeaderActive] = ColorConvertU32ToFloat4(Colors->BLUE600);
|
||||
col[ImGuiCol_Separator] = ColorConvertU32ToFloat4(Colors->GRAY400);
|
||||
col[ImGuiCol_SeparatorHovered] = ColorConvertU32ToFloat4(Colors->GRAY600);
|
||||
col[ImGuiCol_SeparatorActive] = ColorConvertU32ToFloat4(Colors->GRAY700);
|
||||
col[ImGuiCol_ResizeGrip] = ColorConvertU32ToFloat4(Colors->GRAY400);
|
||||
col[ImGuiCol_ResizeGripHovered] = ColorConvertU32ToFloat4(Colors->GRAY600);
|
||||
col[ImGuiCol_ResizeGripActive] = ColorConvertU32ToFloat4(Colors->GRAY700);
|
||||
col[ImGuiCol_PlotLines] = ColorConvertU32ToFloat4(Colors->BLUE400);
|
||||
col[ImGuiCol_PlotLinesHovered] = ColorConvertU32ToFloat4(Colors->BLUE600);
|
||||
col[ImGuiCol_PlotHistogram] = ColorConvertU32ToFloat4(Colors->BLUE400);
|
||||
col[ImGuiCol_PlotHistogramHovered] = ColorConvertU32ToFloat4(Colors->BLUE600);
|
||||
col[ImGuiCol_TextSelectedBg] = ColorConvertU32ToFloat4((Colors->BLUE400 & 0x00FFFFFF) | 0x33000000);
|
||||
col[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f);
|
||||
col[ImGuiCol_NavCursor] = ColorConvertU32ToFloat4((Colors->GRAY900 & 0x00FFFFFF) | 0x0A000000);
|
||||
col[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f);
|
||||
col[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f);
|
||||
col[ImGuiCol_ModalWindowDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.35f);
|
||||
col[ImGuiCol_CheckMark] = ColorConvertU32ToFloat4(Colors->GRAY50);
|
||||
col[ImGuiCol_Tab] = ColorConvertU32ToFloat4(Colors->GRAY300);
|
||||
col[ImGuiCol_TabSelected] = ColorConvertU32ToFloat4(Colors->BLUE500);
|
||||
col[ImGuiCol_TabHovered] = ColorConvertU32ToFloat4(Colors->BLUE700);
|
||||
col[ImGuiCol_TabDimmed] = ColorConvertU32ToFloat4(Colors->GRAY400);
|
||||
col[ImGuiCol_TabDimmedSelected] = ColorConvertU32ToFloat4(Colors->BLUE700);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
/*
|
||||
Color definitions in ImGui are a good starting point,
|
||||
but do not cover all the intricacies of Spectrum's possible colors
|
||||
in controls and widgets.
|
||||
|
||||
One big difference is that ImGui communicates widget activity
|
||||
(hover, pressed) with their background, while spectrum uses a mix
|
||||
of background and border, with border being the most common choice.
|
||||
|
||||
Because of this, we reference extra colors in spectrum from
|
||||
imgui_widgets.cpp directly. Theme-dependent colors are accessed at
|
||||
runtime via Spectrum::Colors (a pointer swapped by SetTheme()).
|
||||
*/
|
||||
|
||||
namespace ImGui::Spectrum {
|
||||
// Widget metric constants
|
||||
constexpr float CHECKBOX_BORDER_SIZE = 2.0f;
|
||||
constexpr float CHECKBOX_ROUNDING = 2.0f;
|
||||
|
||||
// Sets the ImGui style to Spectrum. Pass dark=true for dark theme.
|
||||
// To switch themes at runtime, call again with the new value.
|
||||
void StyleColorsSpectrum(bool dark = false);
|
||||
|
||||
namespace {
|
||||
// Unnamed namespace, since we only use this here.
|
||||
unsigned int Color(unsigned int c) {
|
||||
// ImGui ImU32 format is 0xAABBGGRR; standard RGB literals are 0xRRGGBB.
|
||||
// Swap R and B channels and add full alpha.
|
||||
const short a = 0xFF;
|
||||
const short r = (c >> 16) & 0xFF;
|
||||
const short g = (c >> 8) & 0xFF;
|
||||
const short b = (c >> 0) & 0xFF;
|
||||
return (a << 24) | (r << 0) | (g << 8) | (b << 16);
|
||||
}
|
||||
}
|
||||
|
||||
inline unsigned int color_alpha(unsigned int alpha, unsigned int c) {
|
||||
return ((alpha & 0xFF) << 24) | (c & 0x00FFFFFF);
|
||||
}
|
||||
|
||||
namespace Static {
|
||||
// static colors (same in both light and dark themes)
|
||||
const unsigned int NONE = 0x00000000; // transparent
|
||||
const unsigned int WHITE = Color(0xFFFFFF);
|
||||
const unsigned int BLACK = Color(0x000000);
|
||||
const unsigned int GRAY200 = Color(0xF4F4F4);
|
||||
const unsigned int GRAY300 = Color(0xEAEAEA);
|
||||
const unsigned int GRAY400 = Color(0xD3D3D3);
|
||||
const unsigned int GRAY500 = Color(0xBCBCBC);
|
||||
const unsigned int GRAY600 = Color(0x959595);
|
||||
const unsigned int GRAY700 = Color(0x767676);
|
||||
const unsigned int GRAY800 = Color(0x505050);
|
||||
const unsigned int GRAY900 = Color(0x323232);
|
||||
const unsigned int BLUE400 = Color(0x378EF0);
|
||||
const unsigned int BLUE500 = Color(0x2680EB);
|
||||
const unsigned int BLUE600 = Color(0x1473E6);
|
||||
const unsigned int BLUE700 = Color(0x0D66D0);
|
||||
const unsigned int RED400 = Color(0xEC5B62);
|
||||
const unsigned int RED500 = Color(0xE34850);
|
||||
const unsigned int RED600 = Color(0xD7373F);
|
||||
const unsigned int RED700 = Color(0xC9252D);
|
||||
const unsigned int ORANGE400 = Color(0xF29423);
|
||||
const unsigned int ORANGE500 = Color(0xE68619);
|
||||
const unsigned int ORANGE600 = Color(0xDA7B11);
|
||||
const unsigned int ORANGE700 = Color(0xCB6F10);
|
||||
const unsigned int GREEN400 = Color(0x33AB84);
|
||||
const unsigned int GREEN500 = Color(0x2D9D78);
|
||||
const unsigned int GREEN600 = Color(0x268E6C);
|
||||
const unsigned int GREEN700 = Color(0x12805C);
|
||||
}
|
||||
|
||||
// Theme-dependent color palette. Access via Spectrum::Colors->.
|
||||
struct SpectrumPalette {
|
||||
unsigned int GRAY50, GRAY75, GRAY100, GRAY200, GRAY300, GRAY400, GRAY500, GRAY600, GRAY700, GRAY800,
|
||||
GRAY900;
|
||||
unsigned int BLUE400, BLUE500, BLUE600, BLUE700;
|
||||
unsigned int RED400, RED500, RED600, RED700;
|
||||
unsigned int ORANGE400, ORANGE500, ORANGE600, ORANGE700;
|
||||
unsigned int GREEN400, GREEN500, GREEN600, GREEN700;
|
||||
unsigned int INDIGO400, INDIGO500, INDIGO600, INDIGO700;
|
||||
unsigned int CELERY400, CELERY500, CELERY600, CELERY700;
|
||||
unsigned int MAGENTA400, MAGENTA500, MAGENTA600, MAGENTA700;
|
||||
unsigned int YELLOW400, YELLOW500, YELLOW600, YELLOW700;
|
||||
unsigned int FUCHSIA400, FUCHSIA500, FUCHSIA600, FUCHSIA700;
|
||||
unsigned int SEAFOAM400, SEAFOAM500, SEAFOAM600, SEAFOAM700;
|
||||
unsigned int CHARTREUSE400, CHARTREUSE500, CHARTREUSE600, CHARTREUSE700;
|
||||
unsigned int PURPLE400, PURPLE500, PURPLE600, PURPLE700;
|
||||
};
|
||||
|
||||
extern const SpectrumPalette *Colors;
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
// SEGA Mount GUI — ImGui + Win32 + DirectX11
|
||||
#define LOG_MODULE "GUI"
|
||||
#include "gui_common.h"
|
||||
#include "dx11_backend.h"
|
||||
#include "gui_utils.h"
|
||||
#include "ui.h"
|
||||
#include "theme.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_impl_win32.h"
|
||||
#include "imgui_impl_dx11.h"
|
||||
#include "driver_service.h"
|
||||
#include "utils/files.h"
|
||||
#include <tchar.h>
|
||||
#include <windows.h>
|
||||
#include <format>
|
||||
|
||||
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int) {
|
||||
SetConsoleOutputCP(CP_UTF8);
|
||||
SetConsoleCP(CP_UTF8);
|
||||
|
||||
// 默认加载当前目录的 config.ini
|
||||
std::wstring default_config = std::format(L"{}\\config.ini", utils::get_cwd());
|
||||
load_config_file(default_config);
|
||||
|
||||
// 启动时自动检测驱动
|
||||
{
|
||||
std::string svc_err;
|
||||
g_driver_ready = driver::ensure_running(&svc_err);
|
||||
if (g_driver_ready)
|
||||
g_log_lines.emplace_back("[INFO] Driver service ready");
|
||||
else
|
||||
g_log_lines.push_back(std::format("[ERROR] Driver: {}", svc_err));
|
||||
}
|
||||
|
||||
// 初始化 ImGui DPI 感知
|
||||
ImGui_ImplWin32_EnableDpiAwareness();
|
||||
float main_scale = ImGui_ImplWin32_GetDpiScaleForMonitor(
|
||||
::MonitorFromPoint(POINT{0, 0}, MONITOR_DEFAULTTOPRIMARY));
|
||||
|
||||
// 创建 Win32 窗口
|
||||
WNDCLASSEXW wc = {
|
||||
sizeof(wc), CS_CLASSDC, WndProc, 0L, 0L,
|
||||
hInstance, nullptr, nullptr, nullptr, nullptr,
|
||||
L"SEGA Mount", nullptr
|
||||
};
|
||||
::RegisterClassExW(&wc);
|
||||
HWND hwnd = ::CreateWindowW(wc.lpszClassName, L"SEGA Mount GUI",
|
||||
WS_OVERLAPPEDWINDOW, 100, 100,
|
||||
(int)(900 * main_scale), (int)(600 * main_scale),
|
||||
nullptr, nullptr, wc.hInstance, nullptr);
|
||||
|
||||
// 初始化 D3D11
|
||||
if (!CreateDeviceD3D(hwnd)) {
|
||||
CleanupDeviceD3D();
|
||||
::UnregisterClassW(wc.lpszClassName, wc.hInstance);
|
||||
return 1;
|
||||
}
|
||||
|
||||
::ShowWindow(hwnd, SW_SHOWDEFAULT);
|
||||
::UpdateWindow(hwnd);
|
||||
|
||||
// 初始化 ImGui
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
(void) io;
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
|
||||
// 应用暗色主题(CinderImGui 风格)
|
||||
apply_dark_theme();
|
||||
|
||||
// DPI 缩放
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.ScaleAllSizes(main_scale);
|
||||
style.FontScaleDpi = main_scale;
|
||||
|
||||
// 加载中文字体
|
||||
io.Fonts->AddFontFromFileTTF(R"(c:\Windows\Fonts\msyh.ttc)", 0, nullptr,
|
||||
io.Fonts->GetGlyphRangesChineseFull());
|
||||
|
||||
ImGui_ImplWin32_Init(hwnd);
|
||||
ImGui_ImplDX11_Init(g_pd3dDevice, g_pd3dDeviceContext);
|
||||
|
||||
// 启用日志捕获
|
||||
g_log_capture_enabled = true;
|
||||
|
||||
// 主循环
|
||||
bool done = false;
|
||||
while (!done) {
|
||||
MSG msg;
|
||||
while (::PeekMessage(&msg, nullptr, 0U, 0U, PM_REMOVE)) {
|
||||
::TranslateMessage(&msg);
|
||||
::DispatchMessage(&msg);
|
||||
if (msg.message == WM_QUIT)
|
||||
done = true;
|
||||
}
|
||||
if (done)
|
||||
break;
|
||||
|
||||
if (g_SwapChainOccluded && g_pSwapChain->Present(0, DXGI_PRESENT_TEST) == DXGI_STATUS_OCCLUDED) {
|
||||
::Sleep(10);
|
||||
continue;
|
||||
}
|
||||
g_SwapChainOccluded = false;
|
||||
|
||||
if (g_ResizeWidth != 0 && g_ResizeHeight != 0) {
|
||||
CleanupRenderTarget();
|
||||
g_pSwapChain->ResizeBuffers(0, g_ResizeWidth, g_ResizeHeight, DXGI_FORMAT_UNKNOWN, 0);
|
||||
g_ResizeWidth = g_ResizeHeight = 0;
|
||||
CreateRenderTarget();
|
||||
}
|
||||
|
||||
ImGui_ImplDX11_NewFrame();
|
||||
ImGui_ImplWin32_NewFrame();
|
||||
|
||||
// 窗口最小化时 DisplaySize 为 0×0,跳过渲染避免 ClipRect 断言失败
|
||||
ImGuiIO &frame_io = ImGui::GetIO();
|
||||
if (frame_io.DisplaySize.x <= 0.0f || frame_io.DisplaySize.y <= 0.0f) {
|
||||
::Sleep(10);
|
||||
continue;
|
||||
}
|
||||
|
||||
ImGui::NewFrame();
|
||||
|
||||
DrawUI();
|
||||
|
||||
ImGui::Render();
|
||||
constexpr float clear_color[4] = {0.1f, 0.1f, 0.1f, 1.0f};
|
||||
g_pd3dDeviceContext->OMSetRenderTargets(1, &g_mainRenderTargetView, nullptr);
|
||||
g_pd3dDeviceContext->ClearRenderTargetView(g_mainRenderTargetView, clear_color);
|
||||
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
|
||||
|
||||
HRESULT hr = g_pSwapChain->Present(1, 0);
|
||||
g_SwapChainOccluded = (hr == DXGI_STATUS_OCCLUDED);
|
||||
}
|
||||
|
||||
// 清理
|
||||
g_log_capture_enabled = false;
|
||||
ImGui_ImplDX11_Shutdown();
|
||||
ImGui_ImplWin32_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
|
||||
CleanupDeviceD3D();
|
||||
::DestroyWindow(hwnd);
|
||||
::UnregisterClassW(wc.lpszClassName, wc.hInstance);
|
||||
|
||||
return 0;
|
||||
}
|
||||
+15
-56
@@ -1,62 +1,21 @@
|
||||
#include "theme.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_spectrum.h"
|
||||
|
||||
// 从 CinderImGui::Options::darkTheme() 提取,适配原生 ImGui
|
||||
// ModalWindowDarkening → ModalWindowDimBg (ImGui 1.73+ 重命名)
|
||||
void apply_dark_theme() {
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
static bool s_dark = true; // 当前主题状态
|
||||
|
||||
style.WindowMinSize = ImVec2(160, 20);
|
||||
style.FramePadding = ImVec2(4, 2);
|
||||
style.ItemSpacing = ImVec2(6, 2);
|
||||
style.ItemInnerSpacing = ImVec2(2, 4);
|
||||
style.Alpha = 0.95f;
|
||||
style.WindowRounding = 4.0f;
|
||||
style.FrameRounding = 2.0f;
|
||||
style.IndentSpacing = 6.0f;
|
||||
style.ColumnsMinSpacing = 50.0f;
|
||||
style.GrabMinSize = 14.0f;
|
||||
style.GrabRounding = 16.0f;
|
||||
style.ScrollbarSize = 12.0f;
|
||||
style.ScrollbarRounding = 16.0f;
|
||||
// 应用 Adobe Spectrum 主题。dark=true 暗色, dark=false 亮色
|
||||
void apply_theme(bool dark) {
|
||||
s_dark = dark;
|
||||
ImGui::Spectrum::StyleColorsSpectrum(dark);
|
||||
}
|
||||
|
||||
ImVec4 *c = style.Colors;
|
||||
c[ImGuiCol_Text] = ImVec4(0.86f, 0.93f, 0.89f, 0.78f);
|
||||
c[ImGuiCol_TextDisabled] = ImVec4(0.86f, 0.93f, 0.89f, 0.28f);
|
||||
c[ImGuiCol_WindowBg] = ImVec4(0.13f, 0.14f, 0.17f, 1.00f);
|
||||
c[ImGuiCol_Border] = ImVec4(0.31f, 0.31f, 1.00f, 0.00f);
|
||||
c[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f);
|
||||
c[ImGuiCol_FrameBg] = ImVec4(0.20f, 0.22f, 0.27f, 1.00f);
|
||||
c[ImGuiCol_FrameBgHovered] = ImVec4(0.92f, 0.18f, 0.29f, 0.78f);
|
||||
c[ImGuiCol_FrameBgActive] = ImVec4(0.92f, 0.18f, 0.29f, 1.00f);
|
||||
c[ImGuiCol_TitleBg] = ImVec4(0.20f, 0.22f, 0.27f, 1.00f);
|
||||
c[ImGuiCol_TitleBgCollapsed] = ImVec4(0.20f, 0.22f, 0.27f, 0.75f);
|
||||
c[ImGuiCol_TitleBgActive] = ImVec4(0.92f, 0.18f, 0.29f, 1.00f);
|
||||
c[ImGuiCol_MenuBarBg] = ImVec4(0.20f, 0.22f, 0.27f, 0.47f);
|
||||
c[ImGuiCol_ScrollbarBg] = ImVec4(0.20f, 0.22f, 0.27f, 1.00f);
|
||||
c[ImGuiCol_ScrollbarGrab] = ImVec4(0.09f, 0.15f, 0.16f, 1.00f);
|
||||
c[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.92f, 0.18f, 0.29f, 0.78f);
|
||||
c[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.92f, 0.18f, 0.29f, 1.00f);
|
||||
c[ImGuiCol_CheckMark] = ImVec4(0.71f, 0.22f, 0.27f, 1.00f);
|
||||
c[ImGuiCol_SliderGrab] = ImVec4(0.47f, 0.77f, 0.83f, 0.14f);
|
||||
c[ImGuiCol_SliderGrabActive] = ImVec4(0.92f, 0.18f, 0.29f, 1.00f);
|
||||
c[ImGuiCol_Button] = ImVec4(0.47f, 0.77f, 0.83f, 0.14f);
|
||||
c[ImGuiCol_ButtonHovered] = ImVec4(0.92f, 0.18f, 0.29f, 0.86f);
|
||||
c[ImGuiCol_ButtonActive] = ImVec4(0.92f, 0.18f, 0.29f, 1.00f);
|
||||
c[ImGuiCol_Header] = ImVec4(0.92f, 0.18f, 0.29f, 0.76f);
|
||||
c[ImGuiCol_HeaderHovered] = ImVec4(0.92f, 0.18f, 0.29f, 0.86f);
|
||||
c[ImGuiCol_HeaderActive] = ImVec4(0.92f, 0.18f, 0.29f, 1.00f);
|
||||
c[ImGuiCol_Separator] = ImVec4(0.14f, 0.16f, 0.19f, 1.00f);
|
||||
c[ImGuiCol_SeparatorHovered] = ImVec4(0.92f, 0.18f, 0.29f, 0.78f);
|
||||
c[ImGuiCol_SeparatorActive] = ImVec4(0.92f, 0.18f, 0.29f, 1.00f);
|
||||
c[ImGuiCol_ResizeGrip] = ImVec4(0.47f, 0.77f, 0.83f, 0.04f);
|
||||
c[ImGuiCol_ResizeGripHovered] = ImVec4(0.92f, 0.18f, 0.29f, 0.78f);
|
||||
c[ImGuiCol_ResizeGripActive] = ImVec4(0.92f, 0.18f, 0.29f, 1.00f);
|
||||
c[ImGuiCol_PlotLines] = ImVec4(0.86f, 0.93f, 0.89f, 0.63f);
|
||||
c[ImGuiCol_PlotLinesHovered] = ImVec4(0.92f, 0.18f, 0.29f, 1.00f);
|
||||
c[ImGuiCol_PlotHistogram] = ImVec4(0.86f, 0.93f, 0.89f, 0.63f);
|
||||
c[ImGuiCol_PlotHistogramHovered] = ImVec4(0.92f, 0.18f, 0.29f, 1.00f);
|
||||
c[ImGuiCol_TextSelectedBg] = ImVec4(0.92f, 0.18f, 0.29f, 0.43f);
|
||||
c[ImGuiCol_PopupBg] = ImVec4(0.20f, 0.22f, 0.27f, 0.9f);
|
||||
c[ImGuiCol_ModalWindowDimBg] = ImVec4(0.20f, 0.22f, 0.27f, 0.73f);
|
||||
// 切换主题(Dark ↔ Light)
|
||||
void toggle_theme() {
|
||||
apply_theme(!s_dark);
|
||||
}
|
||||
|
||||
// 获取当前是否为暗色主题
|
||||
bool is_dark_theme() {
|
||||
return s_dark;
|
||||
}
|
||||
|
||||
+8
-2
@@ -1,4 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
// 应用 CinderImGui 风格的暗色主题(红色强调)
|
||||
void apply_dark_theme();
|
||||
// 应用 Adobe Spectrum 主题。dark=true 暗色, dark=false 亮色
|
||||
void apply_theme(bool dark);
|
||||
|
||||
// 切换主题(Dark ↔ Light)
|
||||
void toggle_theme();
|
||||
|
||||
// 获取当前是否为暗色主题
|
||||
bool is_dark_theme();
|
||||
|
||||
+78
-32
@@ -2,9 +2,48 @@
|
||||
#include "gui_common.h"
|
||||
#include "gui_utils.h"
|
||||
#include "icf_mount.h"
|
||||
#include "theme.h"
|
||||
#include "resource.h"
|
||||
#include "utils/resource.h"
|
||||
#include "utils/strings.h"
|
||||
#include "imgui.h"
|
||||
#include "icons/lucide.h"
|
||||
#include <format>
|
||||
#include <windows.h>
|
||||
|
||||
// 初始化 ImGui:主题、DPI 缩放、字体加载
|
||||
void InitUI(float main_scale) {
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
|
||||
// 应用 Spectrum 主题(默认暗色)
|
||||
apply_theme(g_dark_theme);
|
||||
|
||||
// DPI 缩放
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.ScaleAllSizes(main_scale);
|
||||
style.FontScaleDpi = main_scale;
|
||||
|
||||
// 加载中文字体(主字体)
|
||||
constexpr float font_size = 18.0f;
|
||||
io.Fonts->AddFontFromFileTTF(R"(c:\Windows\Fonts\msyh.ttc)", font_size, nullptr,
|
||||
io.Fonts->GetGlyphRangesChineseFull());
|
||||
|
||||
// 加载 Lucide 图标字体(合并模式回退),从 exe 资源中加载
|
||||
{
|
||||
const void *font_data = nullptr;
|
||||
DWORD font_size_res = 0;
|
||||
if (utils::load_resource(IDR_LUCIDE_FONT, font_data, font_size_res)) {
|
||||
static constexpr ImWchar icon_ranges[] = {ICON_MIN, ICON_MAX, 0};
|
||||
ImFontConfig merge_cfg;
|
||||
merge_cfg.MergeMode = true;
|
||||
merge_cfg.FontDataOwnedByAtlas = false;
|
||||
merge_cfg.GlyphOffset.y = font_size * 0.18f;
|
||||
io.Fonts->AddFontFromMemoryTTF(const_cast<void *>(font_data), static_cast<int>(font_size_res),
|
||||
font_size, &merge_cfg, icon_ranges);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawUI() {
|
||||
ImGuiViewport *viewport = ImGui::GetMainViewport();
|
||||
@@ -18,7 +57,7 @@ void DrawUI() {
|
||||
ImGui::Begin("Main", nullptr, flags);
|
||||
|
||||
// ===== 顶栏:配置文件路径 =====
|
||||
ImGui::Text("Config:");
|
||||
ImGui::Text(ICON_SETTINGS " 配置文件:");
|
||||
ImGui::SameLine();
|
||||
ImGui::InputText("##config_path", g_config_path.data(), g_config_path.capacity() + 1,
|
||||
ImGuiInputTextFlags_CallbackResize,
|
||||
@@ -34,25 +73,32 @@ void DrawUI() {
|
||||
},
|
||||
&g_config_path);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Load")) {
|
||||
if (ImGui::Button(ICON_FOLDER_OPEN " 加载")) {
|
||||
g_config_path_w = utils::to_wstring(g_config_path);
|
||||
load_config_file(g_config_path_w);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Save")) {
|
||||
if (ImGui::Button(ICON_SAVE " 保存")) {
|
||||
g_config_path_w = utils::to_wstring(g_config_path);
|
||||
std::string err;
|
||||
save_config(g_config_path_w, g_cfg, &err);
|
||||
g_log_lines.emplace_back("[INFO] Config saved");
|
||||
g_log_lines.emplace_back("[INFO] 配置已保存");
|
||||
}
|
||||
|
||||
// 主题切换按钮
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(is_dark_theme() ? (ICON_SUN " 亮色模式") : (ICON_MOON " 暗色模式"))) {
|
||||
toggle_theme();
|
||||
g_dark_theme = is_dark_theme();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// ===== 配置编辑区 =====
|
||||
if (ImGui::CollapsingHeader("Configuration", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
// 3列布局: label(110) | input(自适应) | button(30)
|
||||
if (ImGui::CollapsingHeader(ICON_SETTINGS " 配置", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
// 3列布局: label(130) | input(自适应) | button(30)
|
||||
ImGui::Columns(3, nullptr, false);
|
||||
float col_label = 110.0f;
|
||||
float col_label = 130.0f;
|
||||
float col_button = 30.0f;
|
||||
float col_input = ImGui::GetWindowWidth() - col_label - col_button - 40.0f;
|
||||
ImGui::SetColumnWidth(0, col_label);
|
||||
@@ -62,15 +108,15 @@ void DrawUI() {
|
||||
// ICF_PATH (file)
|
||||
static char icf_path_buf[512];
|
||||
strncpy_s(icf_path_buf, utils::wstr_to_str(g_cfg.icf_path).c_str(), sizeof(icf_path_buf) - 1);
|
||||
ImGui::TextUnformatted("ICF Path");
|
||||
ImGui::TextUnformatted(ICON_FILE " ICF 路径");
|
||||
ImGui::NextColumn();
|
||||
ImGui::PushItemWidth(-1);
|
||||
if (ImGui::InputText("##icf_path", icf_path_buf, sizeof(icf_path_buf)))
|
||||
g_cfg.icf_path = utils::to_wstring(icf_path_buf);
|
||||
ImGui::PopItemWidth();
|
||||
ImGui::NextColumn();
|
||||
if (ImGui::Button("...##icf_browse")) {
|
||||
auto sel = open_file_dialog("ICF Files", "*.icf");
|
||||
if (ImGui::Button(ICON_FOLDER_OPEN "##icf_browse")) {
|
||||
auto sel = open_file_dialog("ICF 文件", "*.icf");
|
||||
if (!sel.empty()) {
|
||||
strncpy_s(icf_path_buf, sel.c_str(), sizeof(icf_path_buf) - 1);
|
||||
g_cfg.icf_path = utils::to_wstring(sel);
|
||||
@@ -81,14 +127,14 @@ void DrawUI() {
|
||||
// IMAGE_DIR (folder)
|
||||
static char image_dir_buf[512];
|
||||
strncpy_s(image_dir_buf, utils::wstr_to_str(g_cfg.image_dir).c_str(), sizeof(image_dir_buf) - 1);
|
||||
ImGui::TextUnformatted("Image Dir");
|
||||
ImGui::TextUnformatted(ICON_FOLDER_OPEN " 镜像目录");
|
||||
ImGui::NextColumn();
|
||||
ImGui::PushItemWidth(-1);
|
||||
if (ImGui::InputText("##image_dir", image_dir_buf, sizeof(image_dir_buf)))
|
||||
g_cfg.image_dir = utils::to_wstring(image_dir_buf);
|
||||
ImGui::PopItemWidth();
|
||||
ImGui::NextColumn();
|
||||
if (ImGui::Button("...##img_browse")) {
|
||||
if (ImGui::Button(ICON_FOLDER_OPEN "##img_browse")) {
|
||||
auto sel = open_folder_dialog();
|
||||
if (!sel.empty()) {
|
||||
strncpy_s(image_dir_buf, sel.c_str(), sizeof(image_dir_buf) - 1);
|
||||
@@ -100,14 +146,14 @@ void DrawUI() {
|
||||
// OVERLAY_DIR (folder)
|
||||
static char overlay_dir_buf[512];
|
||||
strncpy_s(overlay_dir_buf, utils::wstr_to_str(g_cfg.overlay_dir).c_str(), sizeof(overlay_dir_buf) - 1);
|
||||
ImGui::TextUnformatted("Overlay Dir");
|
||||
ImGui::TextUnformatted(ICON_LAYERS " Overlay 目录");
|
||||
ImGui::NextColumn();
|
||||
ImGui::PushItemWidth(-1);
|
||||
if (ImGui::InputText("##overlay_dir", overlay_dir_buf, sizeof(overlay_dir_buf)))
|
||||
g_cfg.overlay_dir = utils::to_wstring(overlay_dir_buf);
|
||||
ImGui::PopItemWidth();
|
||||
ImGui::NextColumn();
|
||||
if (ImGui::Button("...##ovr_browse")) {
|
||||
if (ImGui::Button(ICON_FOLDER_OPEN "##ovr_browse")) {
|
||||
auto sel = open_folder_dialog();
|
||||
if (!sel.empty()) {
|
||||
strncpy_s(overlay_dir_buf, sel.c_str(), sizeof(overlay_dir_buf) - 1);
|
||||
@@ -117,7 +163,7 @@ void DrawUI() {
|
||||
ImGui::NextColumn();
|
||||
|
||||
// MOUNT_LETTER
|
||||
ImGui::TextUnformatted("Mount Letter");
|
||||
ImGui::TextUnformatted(ICON_HARD_DRIVE " 挂载盘符");
|
||||
ImGui::NextColumn();
|
||||
char letter_str[2] = {static_cast<char>(g_cfg.mount_letter), 0};
|
||||
ImGui::PushItemWidth(40);
|
||||
@@ -130,14 +176,14 @@ void DrawUI() {
|
||||
// APP_LINK (folder)
|
||||
static char app_link_buf[512];
|
||||
strncpy_s(app_link_buf, utils::wstr_to_str(g_cfg.app_link).c_str(), sizeof(app_link_buf) - 1);
|
||||
ImGui::TextUnformatted("App Link");
|
||||
ImGui::TextUnformatted(ICON_LINK " App 链接");
|
||||
ImGui::NextColumn();
|
||||
ImGui::PushItemWidth(-1);
|
||||
if (ImGui::InputText("##app_link", app_link_buf, sizeof(app_link_buf)))
|
||||
g_cfg.app_link = utils::to_wstring(app_link_buf);
|
||||
ImGui::PopItemWidth();
|
||||
ImGui::NextColumn();
|
||||
if (ImGui::Button("...##app_browse")) {
|
||||
if (ImGui::Button(ICON_FOLDER_OPEN "##app_browse")) {
|
||||
auto sel = open_folder_dialog();
|
||||
if (!sel.empty()) {
|
||||
strncpy_s(app_link_buf, sel.c_str(), sizeof(app_link_buf) - 1);
|
||||
@@ -149,14 +195,14 @@ void DrawUI() {
|
||||
// OPT_LINK (folder)
|
||||
static char opt_link_buf[512];
|
||||
strncpy_s(opt_link_buf, utils::wstr_to_str(g_cfg.opt_link).c_str(), sizeof(opt_link_buf) - 1);
|
||||
ImGui::TextUnformatted("Opt Link");
|
||||
ImGui::TextUnformatted(ICON_LINK " Opt 链接");
|
||||
ImGui::NextColumn();
|
||||
ImGui::PushItemWidth(-1);
|
||||
if (ImGui::InputText("##opt_link", opt_link_buf, sizeof(opt_link_buf)))
|
||||
g_cfg.opt_link = utils::to_wstring(opt_link_buf);
|
||||
ImGui::PopItemWidth();
|
||||
ImGui::NextColumn();
|
||||
if (ImGui::Button("...##opt_browse")) {
|
||||
if (ImGui::Button(ICON_FOLDER_OPEN "##opt_browse")) {
|
||||
auto sel = open_folder_dialog();
|
||||
if (!sel.empty()) {
|
||||
strncpy_s(opt_link_buf, sel.c_str(), sizeof(opt_link_buf) - 1);
|
||||
@@ -171,45 +217,45 @@ void DrawUI() {
|
||||
ImGui::Separator();
|
||||
|
||||
// ===== 操作区 =====
|
||||
ImGui::Text("Driver: ");
|
||||
ImGui::Text(ICON_HARD_DRIVE " 驱动:");
|
||||
ImGui::SameLine();
|
||||
if (g_driver_ready) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.2f, 0.8f, 0.2f, 1.0f));
|
||||
ImGui::Text("Ready");
|
||||
ImGui::Text(ICON_CIRCLE_CHECK " 就绪");
|
||||
ImGui::PopStyleColor();
|
||||
} else {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
|
||||
ImGui::Text("Not Running");
|
||||
ImGui::Text(ICON_CIRCLE_X " 未运行");
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(g_driver_ready ? "Mount" : "Mount (need driver)")) {
|
||||
if (ImGui::Button(g_driver_ready ? (ICON_PLAY " 挂载") : (ICON_CIRCLE_ALERT " 挂载(需要驱动)"))) {
|
||||
if (g_driver_ready && !g_mounting) {
|
||||
g_mounting = true;
|
||||
g_log_lines.emplace_back("[INFO] Starting mount...");
|
||||
g_log_lines.emplace_back("[INFO] 开始挂载...");
|
||||
auto result = icf_mount::mount_icf_apps(g_cfg);
|
||||
if (result.success) {
|
||||
g_log_lines.push_back(std::format("[INFO] Mount OK: {} APPs, {} OPTs",
|
||||
g_log_lines.push_back(std::format("[INFO] 挂载成功:{} 个 APP,{} 个 OPT",
|
||||
result.apps.size(), result.opts.size()));
|
||||
} else {
|
||||
g_log_lines.push_back(std::format("[ERROR] Mount failed: {}", result.error));
|
||||
g_log_lines.push_back(std::format("[ERROR] 挂载失败:{}", result.error));
|
||||
}
|
||||
g_mounting = false;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Unmount")) {
|
||||
if (ImGui::Button(ICON_SQUARE " 卸载")) {
|
||||
if (g_driver_ready && !g_unmounting) {
|
||||
g_unmounting = true;
|
||||
g_log_lines.emplace_back("[INFO] Starting unmount...");
|
||||
g_log_lines.emplace_back("[INFO] 开始卸载...");
|
||||
auto result = icf_mount::unmount_icf_apps(g_cfg);
|
||||
if (result.success) {
|
||||
g_log_lines.push_back(std::format("[INFO] Unmount OK: {} APPs, {} OPTs",
|
||||
g_log_lines.push_back(std::format("[INFO] 卸载成功:{} 个 APP,{} 个 OPT",
|
||||
result.apps.size(), result.opts.size()));
|
||||
} else {
|
||||
g_log_lines.push_back(std::format("[ERROR] Unmount failed: {}", result.error));
|
||||
g_log_lines.push_back(std::format("[ERROR] 卸载失败:{}", result.error));
|
||||
}
|
||||
g_unmounting = false;
|
||||
}
|
||||
@@ -218,9 +264,9 @@ void DrawUI() {
|
||||
ImGui::Separator();
|
||||
|
||||
// ===== 日志面板 =====
|
||||
ImGui::Checkbox("Auto-scroll", &g_auto_scroll);
|
||||
ImGui::Checkbox(" 自动滚动", &g_auto_scroll);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Clear Log"))
|
||||
if (ImGui::Button(ICON_TRASH " 清空日志"))
|
||||
g_log_lines.clear();
|
||||
|
||||
ImGui::BeginChild("LogPanel", ImVec2(-1, -1), ImGuiChildFlags_Borders);
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
// 初始化 ImGui:主题、DPI 缩放、字体加载
|
||||
// main_scale 为当前显示器 DPI 缩放比例
|
||||
void InitUI(float main_scale);
|
||||
|
||||
// 主界面绘制
|
||||
void DrawUI();
|
||||
|
||||
+36
-36
@@ -26,7 +26,7 @@ namespace icf_mount {
|
||||
// 读取ICF文件
|
||||
auto icf_data = utils::read_file(cfg.icf_path);
|
||||
if (icf_data.empty()) {
|
||||
result.error = "Failed to read ICF file";
|
||||
result.error = "读取 ICF 文件失败";
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -34,14 +34,14 @@ namespace icf_mount {
|
||||
std::string parse_err;
|
||||
auto icf_file = icf::parse_file(icf_data, &parse_err);
|
||||
if (!icf_file) {
|
||||
result.error = std::format("Parse ICF failed: {}", parse_err);
|
||||
result.error = std::format("解析 ICF 失败:{}", parse_err);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 筛选Active状态的APP条目,按part_index排序
|
||||
auto app_entries = get_active_apps(*icf_file);
|
||||
if (app_entries.empty()) {
|
||||
result.error = "No active APP entries found in ICF";
|
||||
result.error = "ICF 中未找到活跃的 APP 条目";
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -52,11 +52,11 @@ namespace icf_mount {
|
||||
auto *cur = app_entries[i];
|
||||
auto *prev = app_entries[i - 1];
|
||||
if (cur->part_index != prev->part_index + 1) {
|
||||
result.error = std::format("Part index not consecutive: {} -> {}", prev->part_index, cur->part_index);
|
||||
result.error = std::format("分区序号不连续:{} -> {}", prev->part_index, cur->part_index);
|
||||
return result;
|
||||
}
|
||||
if (cur->base_version != prev->version) {
|
||||
result.error = std::format("Base version mismatch for part {}: {} != {}", cur->part_index,
|
||||
result.error = std::format("分区 {} 基础版本不匹配:{} != {}", cur->part_index,
|
||||
cur->base_version, prev->version);
|
||||
return result;
|
||||
}
|
||||
@@ -81,14 +81,14 @@ namespace icf_mount {
|
||||
std::wstring app_path = std::format(L"{}\\{}", dir, utils::to_wstring(entry->filename));
|
||||
std::wstring tag = std::format(L"APP_{}", entry->part_index);
|
||||
|
||||
LOG_INFO("Mount part {}: {}", entry->part_index, entry->filename);
|
||||
LOG_INFO("挂载分区 {}:{}", entry->part_index, entry->filename);
|
||||
|
||||
auto mount_res = fscrypt::mount_container(app_path, tag);
|
||||
mount_entry.success = mount_res.success;
|
||||
mount_entry.error = mount_res.error;
|
||||
|
||||
if (!mount_res.success) {
|
||||
LOG_ERROR("Mount part {} failed: {}", entry->part_index, mount_res.error);
|
||||
LOG_ERROR("挂载分区 {} 失败:{}", entry->part_index, mount_res.error);
|
||||
all_ok = false;
|
||||
}
|
||||
|
||||
@@ -109,14 +109,14 @@ namespace icf_mount {
|
||||
|
||||
if (!vhd_exists) {
|
||||
// 创建overlay.vhd差分盘
|
||||
LOG_INFO("Creating overlay.vhd (parent: internal_{}.vhd)", last->part_index);
|
||||
LOG_INFO("创建 overlay.vhd(父盘:internal_{}.vhd)", last->part_index);
|
||||
std::string ov_err;
|
||||
if (!vhd_mount::create_diff_vhd(overlay_vhd, parent_for_create, &ov_err)) {
|
||||
LOG_ERROR("Overlay create failed: {}", ov_err);
|
||||
LOG_ERROR("创建 overlay 失败:{}", ov_err);
|
||||
all_ok = false;
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("Reusing existing overlay.vhd");
|
||||
LOG_INFO("复用已有 overlay.vhd");
|
||||
// 先尝试detach,处理上次未正常卸载的情况
|
||||
vhd_mount::detach_vhd(overlay_vhd, nullptr);
|
||||
}
|
||||
@@ -126,7 +126,7 @@ namespace icf_mount {
|
||||
vhd_mount::MountedVhdInfo vhd_info{};
|
||||
std::string attach_err;
|
||||
if (!vhd_mount::attach_vhd(overlay_vhd, vhd_info, &attach_err)) {
|
||||
LOG_ERROR("Overlay attach failed: {}", attach_err);
|
||||
LOG_ERROR("附加 overlay 失败:{}", attach_err);
|
||||
all_ok = false;
|
||||
} else {
|
||||
result.apps.back().vhd_success = true;
|
||||
@@ -136,10 +136,10 @@ namespace icf_mount {
|
||||
std::wstring mount_point = std::format(L"{}:\\", cfg.mount_letter);
|
||||
DeleteVolumeMountPointW(mount_point.c_str()); // 清理可能残留的旧挂载点
|
||||
if (!SetVolumeMountPointW(mount_point.c_str(), vhd_info.volume_guid.c_str())) {
|
||||
LOG_ERROR("Assign drive {} failed: {}", drive, utils::last_error_str());
|
||||
LOG_ERROR("分配盘符 {} 失败:{}", drive, utils::last_error_str());
|
||||
all_ok = false;
|
||||
} else {
|
||||
LOG_INFO("Overlay mounted to {}:", drive);
|
||||
LOG_INFO("overlay 已挂载到 {}:", drive);
|
||||
|
||||
// 仅新建overlay.vhd时才拷贝overlay目录文件
|
||||
// OVERLAY_DIR为空或不存在时跳过拷贝
|
||||
@@ -153,16 +153,16 @@ namespace icf_mount {
|
||||
size_t copied = 0;
|
||||
if (!utils::copy_dir_overwrite(cfg.overlay_dir, mount_point,
|
||||
&cp_err, &copied)) {
|
||||
LOG_ERROR("Overlay copy failed: {}", cp_err);
|
||||
LOG_ERROR("overlay 文件拷贝失败:{}", cp_err);
|
||||
all_ok = false;
|
||||
} else {
|
||||
LOG_INFO("Copied {} files", copied);
|
||||
LOG_INFO("已拷贝 {} 个文件", copied);
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("OVERLAY_DIR not found, skip copy");
|
||||
LOG_INFO("OVERLAY_DIR 目录不存在,跳过拷贝");
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("OVERLAY_DIR empty, skip copy");
|
||||
LOG_INFO("OVERLAY_DIR 为空,跳过拷贝");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,14 +183,14 @@ namespace icf_mount {
|
||||
std::wstring opt_path = std::format(L"{}\\{}", dir, utils::to_wstring(entry->filename));
|
||||
std::wstring opt_tag = std::format(L"OPT_{}", utils::to_wstring(entry->version));
|
||||
|
||||
LOG_INFO("Mount OPT {}: {}", entry->version, entry->filename);
|
||||
LOG_INFO("挂载 OPT {}:{}", entry->version, entry->filename);
|
||||
|
||||
auto mount_res = fscrypt::mount_container(opt_path, opt_tag);
|
||||
mount_entry.success = mount_res.success;
|
||||
mount_entry.error = mount_res.error;
|
||||
|
||||
if (!mount_res.success) {
|
||||
LOG_ERROR("Mount OPT {} failed: {}", entry->version, mount_res.error);
|
||||
LOG_ERROR("挂载 OPT {} 失败:{}", entry->version, mount_res.error);
|
||||
}
|
||||
|
||||
result.opts.push_back(mount_entry);
|
||||
@@ -203,7 +203,7 @@ namespace icf_mount {
|
||||
std::wstring tag = std::format(L"APP_{}", app.part_index);
|
||||
std::wstring link_path = std::format(L"{}\\{}", cfg.app_link, app.part_index);
|
||||
if (!fscrypt::link_container(tag, link_path)) {
|
||||
LOG_ERROR("Link APP {} failed", app.part_index);
|
||||
LOG_ERROR("链接 APP {} 失败", app.part_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,14 +215,14 @@ namespace icf_mount {
|
||||
std::wstring tag = std::format(L"OPT_{}", utils::to_wstring(opt.version));
|
||||
std::wstring link_path = std::format(L"{}\\{}", cfg.opt_link, utils::to_wstring(opt.version));
|
||||
if (!fscrypt::link_container(tag, link_path)) {
|
||||
LOG_ERROR("Link OPT {} failed", opt.version);
|
||||
LOG_ERROR("链接 OPT {} 失败", opt.version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.success = all_ok;
|
||||
if (!all_ok) {
|
||||
result.error = "Some APPs failed to mount";
|
||||
result.error = "部分 APP 挂载失败";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -279,14 +279,14 @@ namespace icf_mount {
|
||||
// 读取并解析ICF
|
||||
auto icf_data = utils::read_file(cfg.icf_path);
|
||||
if (icf_data.empty()) {
|
||||
result.error = "Failed to read ICF file";
|
||||
result.error = "读取 ICF 文件失败";
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string parse_err;
|
||||
auto icf_file = icf::parse_file(icf_data, &parse_err);
|
||||
if (!icf_file) {
|
||||
result.error = std::format("Parse ICF failed: {}", parse_err);
|
||||
result.error = std::format("解析 ICF 失败:{}", parse_err);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -318,21 +318,21 @@ namespace icf_mount {
|
||||
char drive = static_cast<char>(cfg.mount_letter);
|
||||
std::wstring mount_point = std::format(L"{}:\\", cfg.mount_letter);
|
||||
if (DeleteVolumeMountPointW(mount_point.c_str())) {
|
||||
LOG_INFO("Removed drive letter {}", drive);
|
||||
LOG_INFO("已移除盘符 {}", drive);
|
||||
}
|
||||
|
||||
WIN32_FILE_ATTRIBUTE_DATA fa;
|
||||
if (GetFileAttributesExW(overlay_vhd.c_str(), GetFileExInfoStandard, &fa)) {
|
||||
LOG_INFO("Detaching overlay.vhd...");
|
||||
LOG_INFO("正在分离 overlay.vhd...");
|
||||
std::string vhd_err;
|
||||
if (!vhd_mount::detach_vhd(overlay_vhd, &vhd_err)) {
|
||||
LOG_ERROR("Overlay detach failed: {}", vhd_err);
|
||||
LOG_ERROR("分离 overlay 失败:{}", vhd_err);
|
||||
all_ok = false;
|
||||
} else {
|
||||
LOG_INFO("Detached overlay.vhd");
|
||||
LOG_INFO("已分离 overlay.vhd");
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("overlay.vhd not found, skip detach");
|
||||
LOG_INFO("overlay.vhd 不存在,跳过分离");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,13 +347,13 @@ namespace icf_mount {
|
||||
|
||||
std::wstring tag = std::format(L"APP_{}", entry->part_index);
|
||||
|
||||
LOG_INFO("Unmount part {}: {}", entry->part_index, entry->filename);
|
||||
LOG_INFO("卸载分区 {}:{}", entry->part_index, entry->filename);
|
||||
|
||||
bool ok = fscrypt::unmount_container(tag);
|
||||
mount_entry.success = ok;
|
||||
if (!ok) {
|
||||
mount_entry.error = "Unmount failed";
|
||||
LOG_ERROR("Unmount part {} failed", entry->part_index);
|
||||
mount_entry.error = "卸载失败";
|
||||
LOG_ERROR("卸载分区 {} 失败", entry->part_index);
|
||||
all_ok = false;
|
||||
}
|
||||
|
||||
@@ -371,13 +371,13 @@ namespace icf_mount {
|
||||
|
||||
std::wstring opt_tag = std::format(L"OPT_{}", utils::to_wstring(entry->version));
|
||||
|
||||
LOG_INFO("Unmount OPT {}: {}", entry->version, entry->filename);
|
||||
LOG_INFO("卸载 OPT {}:{}", entry->version, entry->filename);
|
||||
|
||||
bool ok = fscrypt::unmount_container(opt_tag);
|
||||
mount_entry.success = ok;
|
||||
if (!ok) {
|
||||
mount_entry.error = "Unmount failed";
|
||||
LOG_ERROR("Unmount OPT {} failed", entry->version);
|
||||
mount_entry.error = "卸载失败";
|
||||
LOG_ERROR("卸载 OPT {} 失败", entry->version);
|
||||
all_ok = false;
|
||||
}
|
||||
|
||||
@@ -386,7 +386,7 @@ namespace icf_mount {
|
||||
|
||||
result.success = all_ok;
|
||||
if (!all_ok) {
|
||||
result.error = "Some containers failed to unmount";
|
||||
result.error = "部分容器卸载失败";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+8
-8
@@ -1,7 +1,7 @@
|
||||
#include "icf_parser.h"
|
||||
#include "utils/aes.h"
|
||||
#include "utils/crc32.h"
|
||||
#include "utils/crypto_constants.h"
|
||||
#include "crypto_constants.h"
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
|
||||
@@ -69,13 +69,13 @@ namespace icf {
|
||||
std::optional<std::vector<uint8_t> > decrypt(const std::vector<uint8_t> &encrypted,
|
||||
std::string *error) {
|
||||
if (encrypted.size() & 0xF) {
|
||||
if (error) *error = "Data length not aligned to 16 bytes";
|
||||
if (error) *error = "数据长度未按 16 字节对齐";
|
||||
return std::nullopt;
|
||||
}
|
||||
std::string err;
|
||||
auto result = aes::cbc_decrypt(crypto::BOOT_KEY, crypto::BOOT_IV, encrypted.data(), encrypted.size(), &err);
|
||||
if (result.empty()) {
|
||||
if (error) *error = std::format("Decryption failed: {}", err);
|
||||
if (error) *error = std::format("解密失败:{}", err);
|
||||
return std::nullopt;
|
||||
}
|
||||
return result;
|
||||
@@ -135,7 +135,7 @@ namespace icf {
|
||||
dataname = decode_version(sec->version, true);
|
||||
entry.version = dataname;
|
||||
if (sysver != 0) {
|
||||
entry.error = "Redundant SYSTEM image";
|
||||
entry.error = "重复的 SYSTEM 镜像";
|
||||
}
|
||||
sysver = *reinterpret_cast<const uint32_t *>(sec->platform_version);
|
||||
break;
|
||||
@@ -155,14 +155,14 @@ namespace icf {
|
||||
for (size_t k = 0; k < app_datanames.size(); k++) {
|
||||
if (app_datanames[k] == basename) {
|
||||
if (app_timestamps[k] != entry.base_timestamp) {
|
||||
entry.error = "Base APP timestamp mismatch";
|
||||
entry.error = "基础 APP 时间戳不匹配";
|
||||
}
|
||||
found_base = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found_base) {
|
||||
entry.error = std::format("Unable to locate base APP {}", basename);
|
||||
entry.error = std::format("无法找到基础 APP {}", basename);
|
||||
}
|
||||
sfx = std::format("{}_{}.app", sec->part_index, basename);
|
||||
} else {
|
||||
@@ -170,7 +170,7 @@ namespace icf {
|
||||
}
|
||||
|
||||
if (*reinterpret_cast<const uint32_t *>(sec->platform_version) != sysver) {
|
||||
if (entry.error.empty()) entry.error = "SYSTEM version mismatch";
|
||||
if (entry.error.empty()) entry.error = "SYSTEM 版本不匹配";
|
||||
}
|
||||
|
||||
pfx = std::string(file.header.game_id, 4);
|
||||
@@ -184,7 +184,7 @@ namespace icf {
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
entry.error = std::format("Unknown Type: {}", sec->type);
|
||||
entry.error = std::format("未知类型:{}", sec->type);
|
||||
file.entries.push_back(entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
// SEGA Mount — 统一入口(GUI + CLI)
|
||||
// 无参数启动 GUI;带参数(-m/-u)走 CLI 模式
|
||||
#define LOG_MODULE "MAIN"
|
||||
#include "gui/gui_common.h"
|
||||
#include "gui/dx11_backend.h"
|
||||
#include "gui/gui_utils.h"
|
||||
#include "gui/ui.h"
|
||||
#include "gui/theme.h"
|
||||
#include "config.h"
|
||||
#include "driver_service.h"
|
||||
#include "icf_mount.h"
|
||||
#include "utils/strings.h"
|
||||
#include "utils/files.h"
|
||||
#include "utils/log.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_impl_win32.h"
|
||||
#include "imgui_impl_dx11.h"
|
||||
#include <tchar.h>
|
||||
#include <windows.h>
|
||||
#include <format>
|
||||
#include <cstdio>
|
||||
#include <ios>
|
||||
|
||||
// ===== CLI 模式 =====
|
||||
|
||||
// 附着到父进程的控制台,使 stdout/stderr/stdin 可用
|
||||
static void attach_to_parent_console() {
|
||||
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
|
||||
FILE *fp = nullptr;
|
||||
freopen_s(&fp, "CONOUT$", "w", stdout);
|
||||
freopen_s(&fp, "CONOUT$", "w", stderr);
|
||||
freopen_s(&fp, "CONIN$", "r", stdin);
|
||||
std::ios::sync_with_stdio();
|
||||
}
|
||||
}
|
||||
|
||||
static void print_usage() {
|
||||
std::fputs(
|
||||
"SEGA MOUNT\n"
|
||||
"用法:\n"
|
||||
" sega_mount -h 显示帮助\n"
|
||||
" sega_mount -m [config.ini] 挂载\n"
|
||||
" sega_mount -u [config.ini] 卸载\n"
|
||||
" sega_mount 启动 GUI(无参数)\n",
|
||||
stdout
|
||||
);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
|
||||
// CLI 主逻辑:解析参数并执行挂载/卸载
|
||||
static int run_cli(int argc, wchar_t *argv[]) {
|
||||
SetConsoleOutputCP(CP_UTF8);
|
||||
SetConsoleCP(CP_UTF8);
|
||||
|
||||
if (argc < 2) {
|
||||
print_usage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 解析命令:-h 帮助,-m 挂载,-u 卸载
|
||||
std::string cmd = utils::wstr_to_str(argv[1]);
|
||||
bool is_mount = cmd == "-m";
|
||||
bool is_unmount = cmd == "-u";
|
||||
if (cmd == "-h" || cmd == "--help") {
|
||||
print_usage();
|
||||
return 0;
|
||||
}
|
||||
if (!is_mount && !is_unmount) {
|
||||
LOG_ERROR("未知命令:{}", cmd);
|
||||
print_usage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 确定config.ini路径:优先命令行参数,否则使用当前运行目录
|
||||
if (argc >= 3) {
|
||||
g_config_path_w = argv[2];
|
||||
} else {
|
||||
g_config_path_w = std::format(L"{}\\config.ini", utils::get_cwd());
|
||||
}
|
||||
|
||||
// 解析config.ini 到全局 g_cfg
|
||||
std::string cfg_err;
|
||||
if (!parse_config(g_config_path_w, g_cfg, &cfg_err)) {
|
||||
LOG_ERROR("{}", cfg_err);
|
||||
return 1;
|
||||
}
|
||||
g_config_path = utils::wstr_to_str(g_config_path_w);
|
||||
|
||||
if (g_cfg.icf_path.empty()) {
|
||||
LOG_ERROR("配置中 ICF_PATH 为空");
|
||||
return 1;
|
||||
}
|
||||
|
||||
LOG_INFO("配置已加载:{}", g_config_path);
|
||||
LOG_INFO(" ICF_PATH: {}", utils::wstr_to_str(g_cfg.icf_path));
|
||||
LOG_INFO(" IMAGE_DIR: {}", utils::wstr_to_str(g_cfg.image_dir));
|
||||
LOG_INFO(" OVERLAY_DIR: {}", utils::wstr_to_str(g_cfg.overlay_dir));
|
||||
LOG_INFO(" MOUNT_LETTER:{}", static_cast<char>(g_cfg.mount_letter));
|
||||
LOG_INFO(" APP_LINK: {}", utils::wstr_to_str(g_cfg.app_link));
|
||||
LOG_INFO(" OPT_LINK: {}", utils::wstr_to_str(g_cfg.opt_link));
|
||||
LOG_INFO(" APP_KEY: {}", g_cfg.app_key.empty() ? "(使用内置密钥表)" : "(自定义覆盖)");
|
||||
LOG_INFO(" OPT_KEY: {}", g_cfg.opt_key.empty() ? "(使用内置密钥表)" : "(自定义覆盖)");
|
||||
|
||||
// 确保sgfscrypt服务已安装并运行
|
||||
std::string svc_err;
|
||||
if (!driver::ensure_running(&svc_err)) {
|
||||
LOG_ERROR("启动驱动服务失败:{}", svc_err);
|
||||
LOG_ERROR("提示:请以管理员身份运行此程序");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (is_mount) {
|
||||
auto result = icf_mount::mount_icf_apps(g_cfg);
|
||||
if (!result.success) {
|
||||
LOG_ERROR("挂载失败:{}", result.error);
|
||||
return 1;
|
||||
}
|
||||
LOG_INFO("挂载成功:{} 个 APP,{} 个 OPT",
|
||||
result.apps.size(), result.opts.size());
|
||||
return 0;
|
||||
}
|
||||
|
||||
// is_unmount
|
||||
auto result = icf_mount::unmount_icf_apps(g_cfg);
|
||||
if (!result.success) {
|
||||
LOG_ERROR("卸载失败:{}", result.error);
|
||||
return 1;
|
||||
}
|
||||
LOG_INFO("卸载成功:{} 个 APP,{} 个 OPT",
|
||||
result.apps.size(), result.opts.size());
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ===== GUI 模式 =====
|
||||
|
||||
static int run_gui(HINSTANCE hInstance) {
|
||||
SetConsoleOutputCP(CP_UTF8);
|
||||
SetConsoleCP(CP_UTF8);
|
||||
|
||||
// 默认加载当前目录的 config.ini
|
||||
std::wstring default_config = std::format(L"{}\\config.ini", utils::get_cwd());
|
||||
load_config_file(default_config);
|
||||
|
||||
// 启动时自动检测驱动
|
||||
{
|
||||
std::string svc_err;
|
||||
g_driver_ready = driver::ensure_running(&svc_err);
|
||||
if (g_driver_ready)
|
||||
g_log_lines.emplace_back("[INFO] 驱动服务已就绪");
|
||||
else
|
||||
g_log_lines.push_back(std::format("[ERROR] 驱动:{}", svc_err));
|
||||
}
|
||||
|
||||
// 初始化 ImGui DPI 感知
|
||||
ImGui_ImplWin32_EnableDpiAwareness();
|
||||
float main_scale = ImGui_ImplWin32_GetDpiScaleForMonitor(
|
||||
::MonitorFromPoint(POINT{0, 0}, MONITOR_DEFAULTTOPRIMARY));
|
||||
|
||||
// 创建 Win32 窗口
|
||||
WNDCLASSEXW wc = {
|
||||
sizeof(wc), CS_CLASSDC, WndProc, 0L, 0L,
|
||||
hInstance, nullptr, nullptr, nullptr, nullptr,
|
||||
L"SEGA Mount", nullptr
|
||||
};
|
||||
::RegisterClassExW(&wc);
|
||||
HWND hwnd = ::CreateWindowW(wc.lpszClassName, L"SEGA Mount GUI",
|
||||
WS_OVERLAPPEDWINDOW, 100, 100,
|
||||
(int)(900 * main_scale), (int)(600 * main_scale),
|
||||
nullptr, nullptr, wc.hInstance, nullptr);
|
||||
|
||||
// 初始化 D3D11
|
||||
if (!CreateDeviceD3D(hwnd)) {
|
||||
CleanupDeviceD3D();
|
||||
::UnregisterClassW(wc.lpszClassName, wc.hInstance);
|
||||
return 1;
|
||||
}
|
||||
|
||||
::ShowWindow(hwnd, SW_SHOWDEFAULT);
|
||||
::UpdateWindow(hwnd);
|
||||
|
||||
// 初始化 ImGui
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
InitUI(main_scale);
|
||||
|
||||
ImGui_ImplWin32_Init(hwnd);
|
||||
ImGui_ImplDX11_Init(g_pd3dDevice, g_pd3dDeviceContext);
|
||||
|
||||
// 启用日志捕获
|
||||
g_log_capture_enabled = true;
|
||||
|
||||
// 主循环
|
||||
bool done = false;
|
||||
while (!done) {
|
||||
MSG msg;
|
||||
while (::PeekMessage(&msg, nullptr, 0U, 0U, PM_REMOVE)) {
|
||||
::TranslateMessage(&msg);
|
||||
::DispatchMessage(&msg);
|
||||
if (msg.message == WM_QUIT)
|
||||
done = true;
|
||||
}
|
||||
if (done)
|
||||
break;
|
||||
|
||||
if (g_SwapChainOccluded && g_pSwapChain->Present(0, DXGI_PRESENT_TEST) == DXGI_STATUS_OCCLUDED) {
|
||||
::Sleep(10);
|
||||
continue;
|
||||
}
|
||||
g_SwapChainOccluded = false;
|
||||
|
||||
if (g_ResizeWidth != 0 && g_ResizeHeight != 0) {
|
||||
CleanupRenderTarget();
|
||||
g_pSwapChain->ResizeBuffers(0, g_ResizeWidth, g_ResizeHeight, DXGI_FORMAT_UNKNOWN, 0);
|
||||
g_ResizeWidth = g_ResizeHeight = 0;
|
||||
CreateRenderTarget();
|
||||
}
|
||||
|
||||
ImGui_ImplDX11_NewFrame();
|
||||
ImGui_ImplWin32_NewFrame();
|
||||
|
||||
// 窗口最小化时 DisplaySize 为 0×0,跳过渲染避免 ClipRect 断言失败
|
||||
ImGuiIO &frame_io = ImGui::GetIO();
|
||||
if (frame_io.DisplaySize.x <= 0.0f || frame_io.DisplaySize.y <= 0.0f) {
|
||||
::Sleep(10);
|
||||
continue;
|
||||
}
|
||||
|
||||
ImGui::NewFrame();
|
||||
|
||||
DrawUI();
|
||||
|
||||
ImGui::Render();
|
||||
// 根据当前主题设置背景色:Dark GRAY100=#323232, Light GRAY100=#F5F5F5
|
||||
float clear_color[4];
|
||||
if (is_dark_theme()) {
|
||||
clear_color[0] = clear_color[1] = clear_color[2] = 0.196f; // #323232
|
||||
} else {
|
||||
clear_color[0] = clear_color[1] = clear_color[2] = 0.961f; // #F5F5F5
|
||||
}
|
||||
clear_color[3] = 1.0f;
|
||||
g_pd3dDeviceContext->OMSetRenderTargets(1, &g_mainRenderTargetView, nullptr);
|
||||
g_pd3dDeviceContext->ClearRenderTargetView(g_mainRenderTargetView, clear_color);
|
||||
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
|
||||
|
||||
HRESULT hr = g_pSwapChain->Present(1, 0);
|
||||
g_SwapChainOccluded = (hr == DXGI_STATUS_OCCLUDED);
|
||||
}
|
||||
|
||||
// 清理
|
||||
g_log_capture_enabled = false;
|
||||
ImGui_ImplDX11_Shutdown();
|
||||
ImGui_ImplWin32_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
|
||||
CleanupDeviceD3D();
|
||||
::DestroyWindow(hwnd);
|
||||
::UnregisterClassW(wc.lpszClassName, wc.hInstance);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ===== 统一入口 =====
|
||||
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int) {
|
||||
// 解析命令行参数
|
||||
int argc;
|
||||
LPWSTR *argv = CommandLineToArgvW(GetCommandLineW(), &argc);
|
||||
|
||||
// 有命令行参数时进入 CLI 模式
|
||||
if (argc > 1) {
|
||||
attach_to_parent_console();
|
||||
int ret = run_cli(argc, argv);
|
||||
LocalFree(argv);
|
||||
return ret;
|
||||
}
|
||||
|
||||
LocalFree(argv);
|
||||
|
||||
// 无参数时启动 GUI
|
||||
return run_gui(hInstance);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
// 资源头文件 — 定义嵌入资源的 ID
|
||||
#define IDR_LUCIDE_FONT 101
|
||||
#define IDR_DRIVER_SYS 102
|
||||
#define IDR_DRIVER_INF 103
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "resource.h"
|
||||
#include "strings.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace utils {
|
||||
// 加载嵌入资源(RT_RCDATA),返回数据指针和大小
|
||||
bool load_resource(int resource_id, const void *&data, DWORD &size,
|
||||
std::string *error) {
|
||||
HRSRC hRes = FindResourceW(nullptr, MAKEINTRESOURCEW(resource_id), MAKEINTRESOURCEW(10));
|
||||
if (!hRes) {
|
||||
if (error) *error = "查找资源失败:" + last_error_str();
|
||||
return false;
|
||||
}
|
||||
HGLOBAL hMem = LoadResource(nullptr, hRes);
|
||||
if (!hMem) {
|
||||
if (error) *error = "加载资源失败:" + last_error_str();
|
||||
return false;
|
||||
}
|
||||
void *ptr = LockResource(hMem);
|
||||
DWORD sz = SizeofResource(nullptr, hRes);
|
||||
if (!ptr || sz == 0) {
|
||||
if (error) *error = "资源数据为空";
|
||||
return false;
|
||||
}
|
||||
data = ptr;
|
||||
size = sz;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 将嵌入资源提取到文件(覆盖已有文件)
|
||||
bool extract_resource_to_file(int resource_id, const std::wstring &dst_path,
|
||||
std::string *error) {
|
||||
const void *data = nullptr;
|
||||
DWORD size = 0;
|
||||
if (!load_resource(resource_id, data, size, error))
|
||||
return false;
|
||||
|
||||
HANDLE hFile = CreateFileW(dst_path.c_str(), GENERIC_WRITE, 0, nullptr,
|
||||
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (hFile == INVALID_HANDLE_VALUE) {
|
||||
if (error) *error = "创建目标文件失败:" + last_error_str();
|
||||
return false;
|
||||
}
|
||||
DWORD written = 0;
|
||||
if (!WriteFile(hFile, data, size, &written, nullptr) || written != size) {
|
||||
if (error) *error = "写入文件失败:" + last_error_str();
|
||||
CloseHandle(hFile);
|
||||
return false;
|
||||
}
|
||||
CloseHandle(hFile);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <windows.h>
|
||||
|
||||
namespace utils {
|
||||
// 加载嵌入资源(RT_RCDATA),返回数据指针和大小
|
||||
// 资源数据由 Windows 管理,无需手动释放
|
||||
// 成功返回 true,失败时填充 error
|
||||
bool load_resource(int resource_id, const void *&data, DWORD &size,
|
||||
std::string *error = nullptr);
|
||||
|
||||
// 将嵌入资源提取到文件(覆盖已有文件)
|
||||
// 成功返回 true,失败时填充 error
|
||||
bool extract_resource_to_file(int resource_id, const std::wstring &dst_path,
|
||||
std::string *error = nullptr);
|
||||
}
|
||||
@@ -68,12 +68,16 @@ namespace utils {
|
||||
|
||||
// 将 Windows 错误码转为可读字符串: "5: Access is denied."
|
||||
std::string last_error_str(DWORD code) {
|
||||
// 用 FormatMessage 取系统英文描述文本
|
||||
// 用 FormatMessage 取系统描述文本(使用系统默认语言)
|
||||
LPWSTR msg_buf = nullptr;
|
||||
DWORD msg_len = FormatMessageW(
|
||||
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr, code, MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
|
||||
reinterpret_cast<LPWSTR>(&msg_buf), 0, nullptr);
|
||||
nullptr, code,
|
||||
0,
|
||||
reinterpret_cast<LPWSTR>(&msg_buf),
|
||||
0,
|
||||
nullptr
|
||||
);
|
||||
|
||||
std::string desc;
|
||||
if (msg_len > 0 && msg_buf) {
|
||||
|
||||
+7
-7
@@ -27,7 +27,7 @@ namespace vhd_mount {
|
||||
&open_params, &hVhd);
|
||||
if (err != ERROR_SUCCESS) {
|
||||
if (error)
|
||||
*error = std::format("OpenVirtualDisk failed: {} path={}", err, utils::wstr_to_str(vhd_path));
|
||||
*error = std::format("OpenVirtualDisk 失败:{} path={}", err, utils::wstr_to_str(vhd_path));
|
||||
return INVALID_HANDLE_VALUE;
|
||||
}
|
||||
return hVhd;
|
||||
@@ -109,7 +109,7 @@ namespace vhd_mount {
|
||||
ATTACH_VIRTUAL_DISK_FLAG_NO_DRIVE_LETTER,
|
||||
0, &attach_params, nullptr);
|
||||
if (err != ERROR_SUCCESS) {
|
||||
if (error) *error = std::format("AttachVirtualDisk (RW) failed: {}", err);
|
||||
if (error) *error = std::format("AttachVirtualDisk(RW)失败:{}", err);
|
||||
CloseHandle(hVhd);
|
||||
return false;
|
||||
}
|
||||
@@ -128,7 +128,7 @@ namespace vhd_mount {
|
||||
CloseHandle(hVhd);
|
||||
|
||||
if (err != ERROR_SUCCESS) {
|
||||
if (error) *error = std::format("GetVirtualDiskPhysicalPath failed: {}", err);
|
||||
if (error) *error = std::format("GetVirtualDiskPhysicalPath 失败:{}", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ namespace vhd_mount {
|
||||
DWORD disk_num = 0;
|
||||
if (swscanf_s(phys_path, L"\\\\.\\PhysicalDrive%u", &disk_num) != 1) {
|
||||
if (error)
|
||||
*error = std::format("Failed to parse physical disk path: {}", utils::wstr_to_str(phys_path));
|
||||
*error = std::format("解析物理磁盘路径失败:{}", utils::wstr_to_str(phys_path));
|
||||
return false;
|
||||
}
|
||||
info.disk_number = disk_num;
|
||||
@@ -155,7 +155,7 @@ namespace vhd_mount {
|
||||
Sleep(100);
|
||||
}
|
||||
|
||||
if (error) *error = std::format("no volume found on disk {}", disk_num);
|
||||
if (error) *error = std::format("磁盘 {} 上未找到卷", disk_num);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ namespace vhd_mount {
|
||||
CloseHandle(hVhd);
|
||||
|
||||
if (err != ERROR_SUCCESS) {
|
||||
if (error) *error = std::format("DetachVirtualDisk failed: {}", err);
|
||||
if (error) *error = std::format("DetachVirtualDisk 失败:{}", err);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -207,7 +207,7 @@ namespace vhd_mount {
|
||||
&hVhd);
|
||||
if (err != ERROR_SUCCESS) {
|
||||
if (error)
|
||||
*error = std::format("CreateVirtualDisk failed: {} path={} parent={}", err,
|
||||
*error = std::format("CreateVirtualDisk 失败:{} path={} parent={}", err,
|
||||
utils::wstr_to_str(vhd_path), utils::wstr_to_str(parent_vhd_path));
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user