diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d50aea..426e899 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,10 @@ add_library(version SHARED deps/minhook/src/hde/hde64.c ) +add_executable(cgss-borderless-helper WIN32 + src/helper_main.cpp +) + target_include_directories(version PRIVATE src deps/minhook/include @@ -29,6 +33,10 @@ target_include_directories(version PRIVATE deps/rapidjson/include ) +target_include_directories(cgss-borderless-helper PRIVATE + src +) + target_compile_definitions(version PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN @@ -39,18 +47,36 @@ target_link_libraries(version PRIVATE kernel32 ) +target_link_libraries(cgss-borderless-helper PRIVATE + user32 + kernel32 + shell32 +) + set_target_properties(version PROPERTIES OUTPUT_NAME "version" PREFIX "" ) +set_target_properties(cgss-borderless-helper PROPERTIES + OUTPUT_NAME "cgss-borderless-helper" +) + if(MINGW) target_compile_options(version PRIVATE "$<$:-fno-exceptions>" "$<$:-fno-rtti>" ) + target_compile_options(cgss-borderless-helper PRIVATE + -fno-exceptions + -fno-rtti + ) target_link_options(version PRIVATE "-static-libgcc" "-static-libstdc++" ) + target_link_options(cgss-borderless-helper PRIVATE + "-static-libgcc" + "-static-libstdc++" + ) endif() diff --git a/Makefile b/Makefile index 8ab94a1..4c068e1 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ TARGET := build/version.dll +HELPER_TARGET := build/cgss-borderless-helper.exe BUILD_DIR := build OBJ_DIR := $(BUILD_DIR)/obj BUILD ?= release @@ -14,6 +15,8 @@ CFLAGS := -std=gnu11 $(DEFINES) $(INCLUDES) CXXFLAGS := -std=gnu++20 -fno-exceptions -fno-rtti $(DEFINES) $(INCLUDES) LDFLAGS := -shared -static-libgcc -static-libstdc++ LDLIBS := -luser32 -lkernel32 +HELPER_LDFLAGS := -static-libgcc -static-libstdc++ -mwindows +HELPER_LDLIBS := -luser32 -lkernel32 -lshell32 ifeq ($(BUILD),debug) DEFINES += -D_DEBUG @@ -38,18 +41,22 @@ CXX_SRCS := \ src/config.cpp \ src/il2cpp_symbols.cpp +HELPER_CXX_SRCS := \ + src/helper_main.cpp + RC_SRCS := src/version.rc DEF_FILE := src/version.def C_OBJS := $(patsubst %.c,$(OBJ_DIR)/%.o,$(C_SRCS)) CXX_OBJS := $(patsubst %.cpp,$(OBJ_DIR)/%.o,$(CXX_SRCS)) +HELPER_CXX_OBJS := $(patsubst %.cpp,$(OBJ_DIR)/%.o,$(HELPER_CXX_SRCS)) RC_OBJS := $(patsubst %.rc,$(OBJ_DIR)/%.res,$(RC_SRCS)) OBJS := $(C_OBJS) $(CXX_OBJS) $(RC_OBJS) .PHONY: all release debug clean -all: $(TARGET) -release: $(TARGET) +all: $(TARGET) $(HELPER_TARGET) +release: $(TARGET) $(HELPER_TARGET) debug: $(MAKE) BUILD=debug @@ -57,6 +64,10 @@ $(TARGET): $(OBJS) $(DEF_FILE) @mkdir -p $(dir $@) $(CXX) $(LDFLAGS) -o $@ $(OBJS) $(DEF_FILE) $(LDLIBS) +$(HELPER_TARGET): $(HELPER_CXX_OBJS) + @mkdir -p $(dir $@) + $(CXX) $(HELPER_LDFLAGS) -o $@ $(HELPER_CXX_OBJS) $(HELPER_LDLIBS) + $(OBJ_DIR)/%.o: %.c @mkdir -p $(dir $@) $(CC) $(CFLAGS) -c $< -o $@ diff --git a/README.md b/README.md index d994c77..c843ca8 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ - 等待 IL2CPP 运行时初始化稳定 - 可选地 hook `Stage.NetworkUtil.GetSchemeType()`,在需要时强制切到 `http` - 支持通过 `config.json` 覆盖 `api_url` 和 `asset_url` +- 自动拉起同目录下的 `cgss-borderless-helper.exe`,用外部 helper 处理 `F11` 无边框全屏 **目录结构** - `src/`:代理、日志、IL2CPP 符号解析、hook 逻辑 @@ -28,15 +29,22 @@ cmake -S . -B build -G Ninja -DCMAKE_SYSTEM_NAME=Windows \ cmake --build build ``` +或直接使用: +```sh +make +``` + **输出** - `build/version.dll` +- `build/cgss-borderless-helper.exe` **config.json** ```json { "force_http": true, "api_url": "apis.game.starlight-stage.jp/", - "asset_url": "asset-starlight-stage.akamaized.net/" + "asset_url": "asset-starlight-stage.akamaized.net/", + "launch_borderless_helper": true } ``` @@ -44,12 +52,20 @@ cmake --build build - `force_http`:是否强制将 scheme 改成 `http`。默认 `true` - `api_url`:API 主机名,按原始字段格式填写,不带 scheme - `asset_url`:资源主机名,按原始字段格式填写,不带 scheme +- `launch_borderless_helper`:是否自动启动 `cgss-borderless-helper.exe`。默认 `true` **使用方法** -1. 将 `build/version.dll` 复制到 `imascgstage.exe` 同目录 +1. 将 `build/version.dll` 和 `build/cgss-borderless-helper.exe` 一起复制到 `imascgstage.exe` 同目录 2. 如需覆盖地址,在同目录放置 `config.json` 3. 启动游戏 -4. 如果 hook 未生效,查看 `cgss-dmm-hook.log` +4. 进入游戏后按 `F11` 切换无边框全屏 +5. 如果 hook 未生效,查看 `cgss-dmm-hook.log` 和 `cgss-borderless-helper.log` + +说明: +- `cgss-borderless-helper.exe` 现在只负责外部窗口 borderless 行为,不再修改 Unity 内部 `Screen.SetResolution`。 +- 这样更接近 Borderless Gaming 的工作方式,也能减少退出时的干扰。 +- `version.dll` 只在 `imascgstage.exe` 主进程中继续启动 hook 和 helper;被其他进程间接加载时会跳过。 **致谢** - 本项目的 `version.dll` 代理注入思路与运行时 IL2CPP hook 方向,受 `gkms-localify-dmm` 项目启发。 +- `cgss-borderless-helper.exe` 的外部无边框全屏处理思路,参考了 Borderless Gaming 项目;本项目同样按 GPL 开源。 diff --git a/config.json.example b/config.json.example index 17845f5..0e05996 100644 --- a/config.json.example +++ b/config.json.example @@ -1,5 +1,6 @@ { "force_http": true, "api_url": "apis.game.starlight-stage.jp/", - "asset_url": "asset-starlight-stage.akamaized.net/" + "asset_url": "asset-starlight-stage.akamaized.net/", + "launch_borderless_helper": true } diff --git a/src/config.cpp b/src/config.cpp index 16533cf..d630005 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -59,7 +59,8 @@ namespace config { "{\r\n" " \"force_http\": false,\r\n" " \"api_url\": \"apis.game.starlight-stage.jp/\",\r\n" - " \"asset_url\": \"asset-starlight-stage.akamaized.net/\"\r\n" + " \"asset_url\": \"asset-starlight-stage.akamaized.net/\",\r\n" + " \"launch_borderless_helper\": true\r\n" "}\r\n"; } @@ -114,8 +115,13 @@ namespace config { read_bool(document, "force_http", g_urls.force_http); read_string(document, "api_url", g_urls.api_url); read_string(document, "asset_url", g_urls.asset_url); + read_bool(document, "launch_borderless_helper", g_urls.launch_borderless_helper); hook_logf("[cgss-dmm-hook] force_http=%s", g_urls.force_http ? "true" : "false"); + hook_logf( + "[cgss-dmm-hook] launch_borderless_helper=%s", + g_urls.launch_borderless_helper ? "true" : "false" + ); if (!g_urls.api_url.empty()) { hook_logf("[cgss-dmm-hook] normalized api_url=%s", g_urls.api_url.c_str()); diff --git a/src/config.hpp b/src/config.hpp index cedaf61..0b057da 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -5,6 +5,7 @@ namespace config { bool force_http = true; std::string api_url; std::string asset_url; + bool launch_borderless_helper = true; }; void load(); diff --git a/src/helper_main.cpp b/src/helper_main.cpp new file mode 100644 index 0000000..3995c78 --- /dev/null +++ b/src/helper_main.cpp @@ -0,0 +1,385 @@ +#include +#include + +#include +#include +#include +#include + +namespace { + constexpr DWORD kWindowPollIntervalMs = 500; + constexpr int kWindowPollMaxAttempts = 240; + constexpr DWORD kHotkeyPollIntervalMs = 50; + constexpr DWORD kMaintainIntervalMs = 500; + constexpr DWORD kDelayedStyleApplyMs = 4; + constexpr int kToggleKey = VK_F11; + + HWND g_main_window = nullptr; + bool g_borderless_active = false; + bool g_saved_state_valid = false; + RECT g_saved_window_rect = {}; + LONG_PTR g_saved_style = 0; + LONG_PTR g_saved_exstyle = 0; + DWORD g_target_process_id = 0; + + std::wstring get_window_class_name(HWND hwnd) { + wchar_t buffer[256] = {}; + int len = GetClassNameW(hwnd, buffer, static_cast(std::size(buffer))); + if (len <= 0) { + return {}; + } + return std::wstring(buffer, buffer + len); + } + + void helper_logf(const char* format, ...) { + char exe_path[MAX_PATH] = {}; + DWORD len = GetModuleFileNameA(nullptr, exe_path, MAX_PATH); + if (len == 0 || len >= MAX_PATH) { + return; + } + + char* last_sep = exe_path; + for (char* p = exe_path; *p != '\0'; ++p) { + if (*p == '\\' || *p == '/') { + last_sep = p; + } + } + *last_sep = '\0'; + lstrcatA(exe_path, "\\cgss-borderless-helper.log"); + + char message[1024] = {}; + va_list args; + va_start(args, format); + _vsnprintf_s(message, sizeof(message), _TRUNCATE, format, args); + va_end(args); + + std::ofstream stream(exe_path, std::ios::app | std::ios::binary); + if (!stream.is_open()) { + return; + } + + SYSTEMTIME st = {}; + GetLocalTime(&st); + char line[1200] = {}; + _snprintf_s( + line, + sizeof(line), + _TRUNCATE, + "[%04u-%02u-%02u %02u:%02u:%02u] %s\r\n", + static_cast(st.wYear), + static_cast(st.wMonth), + static_cast(st.wDay), + static_cast(st.wHour), + static_cast(st.wMinute), + static_cast(st.wSecond), + message + ); + stream << line; + } + + void set_dpi_awareness() { + using SetProcessDpiAwarenessContextFn = BOOL(WINAPI*)(HANDLE); + auto user32 = GetModuleHandleW(L"user32.dll"); + if (!user32) { + user32 = LoadLibraryW(L"user32.dll"); + } + + if (user32) { + auto set_context = reinterpret_cast( + GetProcAddress(user32, "SetProcessDpiAwarenessContext") + ); + if (set_context && set_context(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) { + helper_logf("[cgss-borderless-helper] enabled per-monitor-v2 DPI awareness"); + return; + } + } + + SetProcessDPIAware(); + helper_logf("[cgss-borderless-helper] enabled legacy DPI awareness"); + } + + bool parse_arguments() { + LPWSTR* argv = nullptr; + int argc = 0; + argv = CommandLineToArgvW(GetCommandLineW(), &argc); + if (!argv) { + return false; + } + + bool ok = false; + for (int i = 1; i + 1 < argc; ++i) { + if (lstrcmpiW(argv[i], L"--pid") == 0) { + g_target_process_id = wcstoul(argv[i + 1], nullptr, 10); + ok = g_target_process_id != 0; + break; + } + } + + LocalFree(argv); + return ok; + } + + struct WindowSearchContext { + DWORD process_id = 0; + HWND hwnd = nullptr; + }; + + BOOL CALLBACK enum_windows_proc(HWND hwnd, LPARAM lparam) { + auto* context = reinterpret_cast(lparam); + if (!context) { + return TRUE; + } + + DWORD process_id = 0; + GetWindowThreadProcessId(hwnd, &process_id); + if (process_id != context->process_id) { + return TRUE; + } + if (GetWindow(hwnd, GW_OWNER) != nullptr) { + return TRUE; + } + + LONG_PTR style = GetWindowLongPtrW(hwnd, GWL_STYLE); + if ((style & WS_CHILD) != 0) { + return TRUE; + } + + context->hwnd = hwnd; + return FALSE; + } + + HWND find_main_window(DWORD process_id) { + WindowSearchContext context; + context.process_id = process_id; + EnumWindows(&enum_windows_proc, reinterpret_cast(&context)); + return context.hwnd; + } + + bool get_monitor_rect(HWND hwnd, RECT& rect, MONITORINFOEXW* out_monitor_info = nullptr) { + MONITORINFOEXW monitor_info = {}; + monitor_info.cbSize = sizeof(monitor_info); + HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + if (!monitor || !GetMonitorInfoW(monitor, &monitor_info)) { + return false; + } + rect = monitor_info.rcMonitor; + if (out_monitor_info) { + *out_monitor_info = monitor_info; + } + return true; + } + + void save_window_state(HWND hwnd) { + if (g_saved_state_valid) { + return; + } + + GetWindowRect(hwnd, &g_saved_window_rect); + g_saved_style = GetWindowLongPtrW(hwnd, GWL_STYLE); + g_saved_exstyle = GetWindowLongPtrW(hwnd, GWL_EXSTYLE); + g_saved_state_valid = true; + helper_logf( + "[cgss-borderless-helper] saved window rect=(%ld,%ld)-(%ld,%ld)", + static_cast(g_saved_window_rect.left), + static_cast(g_saved_window_rect.top), + static_cast(g_saved_window_rect.right), + static_cast(g_saved_window_rect.bottom) + ); + } + + void apply_borderless_once(HWND hwnd) { + RECT monitor_rect = {}; + MONITORINFOEXW monitor_info = {}; + if (!get_monitor_rect(hwnd, monitor_rect, &monitor_info)) { + helper_logf("[cgss-borderless-helper] failed to query monitor rect"); + return; + } + + save_window_state(hwnd); + + LONG_PTR style = GetWindowLongPtrW(hwnd, GWL_STYLE); + LONG_PTR exstyle = GetWindowLongPtrW(hwnd, GWL_EXSTYLE); + LONG_PTR new_style = style & ~(WS_CAPTION | WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX); + LONG_PTR new_exstyle = exstyle & + ~(WS_EX_DLGMODALFRAME | WS_EX_COMPOSITED | WS_EX_WINDOWEDGE | + WS_EX_CLIENTEDGE | WS_EX_LAYERED | WS_EX_STATICEDGE | + WS_EX_TOOLWINDOW | WS_EX_APPWINDOW); + + SetWindowPos( + hwnd, + nullptr, + monitor_rect.left, + monitor_rect.top, + monitor_rect.right - monitor_rect.left, + monitor_rect.bottom - monitor_rect.top, + SWP_SHOWWINDOW | SWP_NOOWNERZORDER | SWP_NOSENDCHANGING + ); + + Sleep(kDelayedStyleApplyMs); + SetWindowLongPtrW(hwnd, GWL_STYLE, new_style); + SetWindowLongPtrW(hwnd, GWL_EXSTYLE, new_exstyle); + SetWindowPos( + hwnd, + nullptr, + monitor_rect.left, + monitor_rect.top, + monitor_rect.right - monitor_rect.left, + monitor_rect.bottom - monitor_rect.top, + SWP_FRAMECHANGED | SWP_SHOWWINDOW | SWP_NOOWNERZORDER | SWP_NOSENDCHANGING + ); + + RECT actual_rect = {}; + GetWindowRect(hwnd, &actual_rect); + helper_logf( + "[cgss-borderless-helper] applied borderless class=%ls monitor=%ls target=(%ld,%ld)-(%ld,%ld) actual=(%ld,%ld)-(%ld,%ld)", + get_window_class_name(hwnd).c_str(), + monitor_info.szDevice, + static_cast(monitor_rect.left), + static_cast(monitor_rect.top), + static_cast(monitor_rect.right), + static_cast(monitor_rect.bottom), + static_cast(actual_rect.left), + static_cast(actual_rect.top), + static_cast(actual_rect.right), + static_cast(actual_rect.bottom) + ); + } + + bool needs_borderless_reapply(HWND hwnd) { + RECT expected_rect = {}; + if (!get_monitor_rect(hwnd, expected_rect)) { + return false; + } + + RECT current_rect = {}; + if (!GetWindowRect(hwnd, ¤t_rect)) { + return false; + } + + LONG_PTR style = GetWindowLongPtrW(hwnd, GWL_STYLE); + const bool has_border = (style & (WS_CAPTION | WS_THICKFRAME | WS_SYSMENU)) != 0; + const bool rect_mismatch = current_rect.left != expected_rect.left || + current_rect.top != expected_rect.top || + current_rect.right != expected_rect.right || + current_rect.bottom != expected_rect.bottom; + return has_border || rect_mismatch; + } + + void restore_window(HWND hwnd) { + if (!g_saved_state_valid) { + return; + } + + SetWindowLongPtrW(hwnd, GWL_STYLE, g_saved_style); + SetWindowLongPtrW(hwnd, GWL_EXSTYLE, g_saved_exstyle); + SetWindowPos( + hwnd, + nullptr, + g_saved_window_rect.left, + g_saved_window_rect.top, + g_saved_window_rect.right - g_saved_window_rect.left, + g_saved_window_rect.bottom - g_saved_window_rect.top, + SWP_FRAMECHANGED | SWP_SHOWWINDOW | SWP_NOOWNERZORDER | SWP_NOSENDCHANGING + ); + helper_logf("[cgss-borderless-helper] restored original window style and rect"); + } + + void toggle_borderless(HWND hwnd) { + if (!hwnd) { + return; + } + + RECT monitor_rect = {}; + MONITORINFOEXW monitor_info = {}; + if (!get_monitor_rect(hwnd, monitor_rect, &monitor_info)) { + helper_logf("[cgss-borderless-helper] failed to resolve monitor rect for toggle"); + return; + } + + if (g_borderless_active) { + g_borderless_active = false; + restore_window(hwnd); + return; + } + + g_borderless_active = true; + helper_logf( + "[cgss-borderless-helper] borderless enabled monitor=%ls rect=(%ld,%ld)-(%ld,%ld)", + monitor_info.szDevice, + static_cast(monitor_rect.left), + static_cast(monitor_rect.top), + static_cast(monitor_rect.right), + static_cast(monitor_rect.bottom) + ); + apply_borderless_once(hwnd); + } +} + +int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { + set_dpi_awareness(); + + if (!parse_arguments()) { + helper_logf("[cgss-borderless-helper] missing or invalid --pid argument"); + return 1; + } + + HANDLE target_process = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, FALSE, g_target_process_id); + if (!target_process) { + helper_logf("[cgss-borderless-helper] failed to open target process"); + return 1; + } + + for (int i = 0; i < kWindowPollMaxAttempts; ++i) { + g_main_window = find_main_window(g_target_process_id); + if (g_main_window) { + helper_logf("[cgss-borderless-helper] found main window hwnd=0x%p", g_main_window); + break; + } + if (WaitForSingleObject(target_process, 0) == WAIT_OBJECT_0) { + CloseHandle(target_process); + helper_logf("[cgss-borderless-helper] target process exited before main window appeared"); + return 0; + } + Sleep(kWindowPollIntervalMs); + } + + if (!g_main_window) { + CloseHandle(target_process); + helper_logf("[cgss-borderless-helper] timed out waiting for main window"); + return 1; + } + + SHORT previous_hotkey_state = 0; + DWORD last_maintain_tick = GetTickCount(); + + while (WaitForSingleObject(target_process, 0) != WAIT_OBJECT_0) { + if (!IsWindow(g_main_window)) { + helper_logf("[cgss-borderless-helper] target window no longer valid"); + break; + } + + SHORT current_hotkey_state = GetAsyncKeyState(kToggleKey); + bool is_pressed = (current_hotkey_state & 0x8000) != 0; + bool was_pressed = (previous_hotkey_state & 0x8000) != 0; + if (is_pressed && !was_pressed) { + helper_logf("[cgss-borderless-helper] F11 pressed"); + toggle_borderless(g_main_window); + } + previous_hotkey_state = current_hotkey_state; + + DWORD now = GetTickCount(); + if (g_borderless_active && now - last_maintain_tick >= kMaintainIntervalMs) { + if (needs_borderless_reapply(g_main_window)) { + helper_logf("[cgss-borderless-helper] detected borderless drift, reapplying"); + apply_borderless_once(g_main_window); + } + last_maintain_tick = now; + } + + Sleep(kHotkeyPollIntervalMs); + } + + CloseHandle(target_process); + helper_logf("[cgss-borderless-helper] target process exited, helper shutting down"); + return 0; +} diff --git a/src/hook.cpp b/src/hook.cpp index 3878b72..5b733da 100644 --- a/src/hook.cpp +++ b/src/hook.cpp @@ -1,6 +1,7 @@ #include "stdinclude.hpp" #include "config.hpp" +#include "hook.hpp" namespace { using GetSchemeTypeFn = int (*)(const MethodInfo*); @@ -400,187 +401,98 @@ namespace { hook_log("[cgss-dmm-hook] force_http disabled, keeping original scheme"); } - if (get_application_server_url_addr) { - auto create_status = MH_CreateHook( - reinterpret_cast(get_application_server_url_addr), - reinterpret_cast(&get_application_server_url_hook), - reinterpret_cast(&g_orig_get_application_server_url) - ); - if (create_status == MH_OK || create_status == MH_ERROR_ALREADY_CREATED) { - auto enable_status = MH_EnableHook(reinterpret_cast(get_application_server_url_addr)); - if (enable_status == MH_OK || enable_status == MH_ERROR_ENABLED) { - hook_log("[cgss-dmm-hook] hooked Stage.NetworkUtil.GetApplicationServerUrl"); - } + auto hook_method = [](uintptr_t address, void* detour, void** original, const char* name) { + if (!address) { + return; } - } + auto create_status = MH_CreateHook(reinterpret_cast(address), detour, original); + if (create_status != MH_OK && create_status != MH_ERROR_ALREADY_CREATED) { + return; + } + auto enable_status = MH_EnableHook(reinterpret_cast(address)); + if (enable_status == MH_OK || enable_status == MH_ERROR_ENABLED) { + hook_logf("[cgss-dmm-hook] hooked %s", name); + } + }; - if (get_resource_server_url_addr) { - auto create_status = MH_CreateHook( - reinterpret_cast(get_resource_server_url_addr), - reinterpret_cast(&get_resource_server_url_hook), - reinterpret_cast(&g_orig_get_resource_server_url) - ); - if (create_status == MH_OK || create_status == MH_ERROR_ALREADY_CREATED) { - auto enable_status = MH_EnableHook(reinterpret_cast(get_resource_server_url_addr)); - if (enable_status == MH_OK || enable_status == MH_ERROR_ENABLED) { - hook_log("[cgss-dmm-hook] hooked Stage.NetworkUtil.GetResourceServerUrl"); - } - } - } - - if (get_tele_scope_server_url_addr) { - auto create_status = MH_CreateHook( - reinterpret_cast(get_tele_scope_server_url_addr), - reinterpret_cast(&get_tele_scope_server_url_hook), - reinterpret_cast(&g_orig_get_tele_scope_server_url) - ); - if (create_status == MH_OK || create_status == MH_ERROR_ALREADY_CREATED) { - auto enable_status = MH_EnableHook(reinterpret_cast(get_tele_scope_server_url_addr)); - if (enable_status == MH_OK || enable_status == MH_ERROR_ENABLED) { - hook_log("[cgss-dmm-hook] hooked Stage.NetworkUtil.GetTeleScopeServerUrl"); - } - } - } - - if (get_concert_server_url_addr) { - auto create_status = MH_CreateHook( - reinterpret_cast(get_concert_server_url_addr), - reinterpret_cast(&get_concert_server_url_hook), - reinterpret_cast(&g_orig_get_concert_server_url) - ); - if (create_status == MH_OK || create_status == MH_ERROR_ALREADY_CREATED) { - auto enable_status = MH_EnableHook(reinterpret_cast(get_concert_server_url_addr)); - if (enable_status == MH_OK || enable_status == MH_ERROR_ENABLED) { - hook_log("[cgss-dmm-hook] hooked Stage.NetworkUtil.GetConcertServerUrl"); - } - } - } - - if (cp_get_application_server_url_addr) { - auto create_status = MH_CreateHook( - reinterpret_cast(cp_get_application_server_url_addr), - reinterpret_cast(&custom_preference_get_application_server_url_hook), - reinterpret_cast(&g_orig_custom_preference_get_application_server_url) - ); - if (create_status == MH_OK || create_status == MH_ERROR_ALREADY_CREATED) { - auto enable_status = MH_EnableHook(reinterpret_cast(cp_get_application_server_url_addr)); - if (enable_status == MH_OK || enable_status == MH_ERROR_ENABLED) { - hook_log("[cgss-dmm-hook] hooked Cute.CustomPreference.GetApplicationServerURL"); - } - } - } - - if (cp_get_tele_scope_server_url_addr) { - auto create_status = MH_CreateHook( - reinterpret_cast(cp_get_tele_scope_server_url_addr), - reinterpret_cast(&custom_preference_get_tele_scope_server_url_hook), - reinterpret_cast(&g_orig_custom_preference_get_tele_scope_server_url) - ); - if (create_status == MH_OK || create_status == MH_ERROR_ALREADY_CREATED) { - auto enable_status = MH_EnableHook(reinterpret_cast(cp_get_tele_scope_server_url_addr)); - if (enable_status == MH_OK || enable_status == MH_ERROR_ENABLED) { - hook_log("[cgss-dmm-hook] hooked Cute.CustomPreference.GetTeleScopeServerURL"); - } - } - } - - if (cp_get_concert_server_url_addr) { - auto create_status = MH_CreateHook( - reinterpret_cast(cp_get_concert_server_url_addr), - reinterpret_cast(&custom_preference_get_concert_server_url_hook), - reinterpret_cast(&g_orig_custom_preference_get_concert_server_url) - ); - if (create_status == MH_OK || create_status == MH_ERROR_ALREADY_CREATED) { - auto enable_status = MH_EnableHook(reinterpret_cast(cp_get_concert_server_url_addr)); - if (enable_status == MH_OK || enable_status == MH_ERROR_ENABLED) { - hook_log("[cgss-dmm-hook] hooked Cute.CustomPreference.GetConcertServerURL"); - } - } - } - - if (cp_get_resource_server_url_addr) { - auto create_status = MH_CreateHook( - reinterpret_cast(cp_get_resource_server_url_addr), - reinterpret_cast(&custom_preference_get_resource_server_url_hook), - reinterpret_cast(&g_orig_custom_preference_get_resource_server_url) - ); - if (create_status == MH_OK || create_status == MH_ERROR_ALREADY_CREATED) { - auto enable_status = MH_EnableHook(reinterpret_cast(cp_get_resource_server_url_addr)); - if (enable_status == MH_OK || enable_status == MH_ERROR_ENABLED) { - hook_log("[cgss-dmm-hook] hooked Cute.CustomPreference.GetResourceServerURL"); - } - } - } - - if (cp_set_application_server_url_addr) { - auto create_status = MH_CreateHook( - reinterpret_cast(cp_set_application_server_url_addr), - reinterpret_cast(&custom_preference_set_application_server_url_hook), - reinterpret_cast(&g_orig_custom_preference_set_application_server_url) - ); - if (create_status == MH_OK || create_status == MH_ERROR_ALREADY_CREATED) { - auto enable_status = MH_EnableHook(reinterpret_cast(cp_set_application_server_url_addr)); - if (enable_status == MH_OK || enable_status == MH_ERROR_ENABLED) { - hook_log("[cgss-dmm-hook] hooked Cute.CustomPreference.SetApplicationServerURL"); - } - } - } - - if (cp_set_tele_scope_server_url_addr) { - auto create_status = MH_CreateHook( - reinterpret_cast(cp_set_tele_scope_server_url_addr), - reinterpret_cast(&custom_preference_set_tele_scope_server_url_hook), - reinterpret_cast(&g_orig_custom_preference_set_tele_scope_server_url) - ); - if (create_status == MH_OK || create_status == MH_ERROR_ALREADY_CREATED) { - auto enable_status = MH_EnableHook(reinterpret_cast(cp_set_tele_scope_server_url_addr)); - if (enable_status == MH_OK || enable_status == MH_ERROR_ENABLED) { - hook_log("[cgss-dmm-hook] hooked Cute.CustomPreference.SetTeleScopeServerURL"); - } - } - } - - if (cp_set_concert_server_url_addr) { - auto create_status = MH_CreateHook( - reinterpret_cast(cp_set_concert_server_url_addr), - reinterpret_cast(&custom_preference_set_concert_server_url_hook), - reinterpret_cast(&g_orig_custom_preference_set_concert_server_url) - ); - if (create_status == MH_OK || create_status == MH_ERROR_ALREADY_CREATED) { - auto enable_status = MH_EnableHook(reinterpret_cast(cp_set_concert_server_url_addr)); - if (enable_status == MH_OK || enable_status == MH_ERROR_ENABLED) { - hook_log("[cgss-dmm-hook] hooked Cute.CustomPreference.SetConcertServerURL"); - } - } - } - - if (cp_set_resource_server_url_addr) { - auto create_status = MH_CreateHook( - reinterpret_cast(cp_set_resource_server_url_addr), - reinterpret_cast(&custom_preference_set_resource_server_url_hook), - reinterpret_cast(&g_orig_custom_preference_set_resource_server_url) - ); - if (create_status == MH_OK || create_status == MH_ERROR_ALREADY_CREATED) { - auto enable_status = MH_EnableHook(reinterpret_cast(cp_set_resource_server_url_addr)); - if (enable_status == MH_OK || enable_status == MH_ERROR_ENABLED) { - hook_log("[cgss-dmm-hook] hooked Cute.CustomPreference.SetResourceServerURL"); - } - } - } - - if (cp_set_scheme_mode_addr) { - auto create_status = MH_CreateHook( - reinterpret_cast(cp_set_scheme_mode_addr), - reinterpret_cast(&custom_preference_set_scheme_mode_hook), - reinterpret_cast(&g_orig_custom_preference_set_scheme_mode) - ); - if (create_status == MH_OK || create_status == MH_ERROR_ALREADY_CREATED) { - auto enable_status = MH_EnableHook(reinterpret_cast(cp_set_scheme_mode_addr)); - if (enable_status == MH_OK || enable_status == MH_ERROR_ENABLED) { - hook_log("[cgss-dmm-hook] hooked Cute.CustomPreference.SetScemeMode"); - } - } - } + hook_method( + get_application_server_url_addr, + reinterpret_cast(&get_application_server_url_hook), + reinterpret_cast(&g_orig_get_application_server_url), + "Stage.NetworkUtil.GetApplicationServerUrl" + ); + hook_method( + get_resource_server_url_addr, + reinterpret_cast(&get_resource_server_url_hook), + reinterpret_cast(&g_orig_get_resource_server_url), + "Stage.NetworkUtil.GetResourceServerUrl" + ); + hook_method( + get_tele_scope_server_url_addr, + reinterpret_cast(&get_tele_scope_server_url_hook), + reinterpret_cast(&g_orig_get_tele_scope_server_url), + "Stage.NetworkUtil.GetTeleScopeServerUrl" + ); + hook_method( + get_concert_server_url_addr, + reinterpret_cast(&get_concert_server_url_hook), + reinterpret_cast(&g_orig_get_concert_server_url), + "Stage.NetworkUtil.GetConcertServerUrl" + ); + hook_method( + cp_get_application_server_url_addr, + reinterpret_cast(&custom_preference_get_application_server_url_hook), + reinterpret_cast(&g_orig_custom_preference_get_application_server_url), + "Cute.CustomPreference.GetApplicationServerURL" + ); + hook_method( + cp_get_tele_scope_server_url_addr, + reinterpret_cast(&custom_preference_get_tele_scope_server_url_hook), + reinterpret_cast(&g_orig_custom_preference_get_tele_scope_server_url), + "Cute.CustomPreference.GetTeleScopeServerURL" + ); + hook_method( + cp_get_concert_server_url_addr, + reinterpret_cast(&custom_preference_get_concert_server_url_hook), + reinterpret_cast(&g_orig_custom_preference_get_concert_server_url), + "Cute.CustomPreference.GetConcertServerURL" + ); + hook_method( + cp_get_resource_server_url_addr, + reinterpret_cast(&custom_preference_get_resource_server_url_hook), + reinterpret_cast(&g_orig_custom_preference_get_resource_server_url), + "Cute.CustomPreference.GetResourceServerURL" + ); + hook_method( + cp_set_application_server_url_addr, + reinterpret_cast(&custom_preference_set_application_server_url_hook), + reinterpret_cast(&g_orig_custom_preference_set_application_server_url), + "Cute.CustomPreference.SetApplicationServerURL" + ); + hook_method( + cp_set_tele_scope_server_url_addr, + reinterpret_cast(&custom_preference_set_tele_scope_server_url_hook), + reinterpret_cast(&g_orig_custom_preference_set_tele_scope_server_url), + "Cute.CustomPreference.SetTeleScopeServerURL" + ); + hook_method( + cp_set_concert_server_url_addr, + reinterpret_cast(&custom_preference_set_concert_server_url_hook), + reinterpret_cast(&g_orig_custom_preference_set_concert_server_url), + "Cute.CustomPreference.SetConcertServerURL" + ); + hook_method( + cp_set_resource_server_url_addr, + reinterpret_cast(&custom_preference_set_resource_server_url_hook), + reinterpret_cast(&g_orig_custom_preference_set_resource_server_url), + "Cute.CustomPreference.SetResourceServerURL" + ); + hook_method( + cp_set_scheme_mode_addr, + reinterpret_cast(&custom_preference_set_scheme_mode_hook), + reinterpret_cast(&g_orig_custom_preference_set_scheme_mode), + "Cute.CustomPreference.SetScemeMode" + ); apply_custom_preference_url_overrides(); diff --git a/src/hook.hpp b/src/hook.hpp new file mode 100644 index 0000000..4c9add5 --- /dev/null +++ b/src/hook.hpp @@ -0,0 +1,3 @@ +#pragma once + +void start_hook_thread(); diff --git a/src/proxy.cpp b/src/proxy.cpp index c1a0e02..172eb8b 100644 --- a/src/proxy.cpp +++ b/src/proxy.cpp @@ -1,5 +1,8 @@ #include "stdinclude.hpp" +#include "config.hpp" +#include "hook.hpp" + extern "C" { void* GetFileVersionInfoA_Original = nullptr; void* GetFileVersionInfoSizeA_Original = nullptr; @@ -33,11 +36,45 @@ extern "C" { } } -void start_hook_thread(); - namespace { constexpr DWORD kProxyInitDelayMs = 1000; + constexpr const char* kHelperExeName = "cgss-borderless-helper.exe"; + constexpr const char* kGameExeName = "imascgstage.exe"; HMODULE g_original = nullptr; + volatile LONG g_helper_started = 0; + + bool is_game_process() { + char path[MAX_PATH] = {}; + DWORD len = GetModuleFileNameA(nullptr, path, MAX_PATH); + if (len == 0 || len >= MAX_PATH) { + return false; + } + + const char* base_name = path; + for (const char* p = path; *p != '\0'; ++p) { + if (*p == '\\' || *p == '/') { + base_name = p + 1; + } + } + return lstrcmpiA(base_name, kGameExeName) == 0; + } + + std::string get_game_directory() { + char path[MAX_PATH] = {}; + DWORD len = GetModuleFileNameA(nullptr, path, MAX_PATH); + if (len == 0 || len >= MAX_PATH) { + return {}; + } + + char* last_sep = path; + for (char* p = path; *p != '\0'; ++p) { + if (*p == '\\' || *p == '/') { + last_sep = p; + } + } + *last_sep = '\0'; + return path; + } void load_original_version_dll() { if (g_original) { @@ -68,9 +105,72 @@ namespace { hook_log("[cgss-dmm-hook] version.dll initialized"); } + void start_borderless_helper() { + if (!config::get().launch_borderless_helper) { + hook_log("[cgss-dmm-hook] launch_borderless_helper=false, skipping helper launch"); + return; + } + if (InterlockedCompareExchange(&g_helper_started, 1, 0) != 0) { + return; + } + + const auto game_dir = get_game_directory(); + if (game_dir.empty()) { + hook_log("[cgss-dmm-hook] failed to resolve game directory for helper launch"); + return; + } + + std::string helper_path = game_dir; + helper_path += "\\"; + helper_path += kHelperExeName; + + DWORD attributes = GetFileAttributesA(helper_path.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { + hook_log("[cgss-dmm-hook] borderless helper exe not found, skipping launch"); + return; + } + + char command_line[512] = {}; + wsprintfA(command_line, "\"%s\" --pid %lu", helper_path.c_str(), GetCurrentProcessId()); + + STARTUPINFOA startup_info = {}; + startup_info.cb = sizeof(startup_info); + PROCESS_INFORMATION process_info = {}; + BOOL result = CreateProcessA( + helper_path.c_str(), + command_line, + nullptr, + nullptr, + FALSE, + CREATE_NO_WINDOW, + nullptr, + game_dir.c_str(), + &startup_info, + &process_info + ); + if (!result) { + hook_logf( + "[cgss-dmm-hook] failed to launch borderless helper, error=%lu", + static_cast(GetLastError()) + ); + return; + } + + CloseHandle(process_info.hThread); + CloseHandle(process_info.hProcess); + hook_log("[cgss-dmm-hook] launched borderless helper"); + } + DWORD WINAPI init_thread(void*) { Sleep(kProxyInitDelayMs); + if (!is_game_process()) { + load_original_version_dll(); + hook_log("[cgss-dmm-hook] non-game process detected, skipping hook/helper startup"); + return 0; + } + config::load(); load_original_version_dll(); + start_borderless_helper(); start_hook_thread(); return 0; }