diff --git a/client-patch/90CN_gm_window.cpp b/client-patch/90CN_gm_window.cpp index 175fde2..ff1702b 100644 --- a/client-patch/90CN_gm_window.cpp +++ b/client-patch/90CN_gm_window.cpp @@ -92,6 +92,125 @@ void LayoutCenterOnOwner() SetWindowPos(g_hwnd, nullptr, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); } +// ---------------------------------------------------------------- 跟随游戏窗口 +// +// 需求:控制台要“长”在游戏画面里。全屏(无边框最大化)时用配置里的目标尺寸, +// 窗口模式下按游戏窗口的实际客户区等比收小并居中;玩家改分辨率、切全屏/窗口, +// 都跟着重新摆一次。 + +int g_desiredW = 1040; +int g_desiredH = 720; +RECT g_lastGameRect = { 0, 0, 0, 0 }; // 上次布局时游戏客户区的位置 +bool g_fitValid = false; + +struct GameSearch { + DWORD pid; + HWND best; + LONG area; +}; + +BOOL CALLBACK OnGameCandidate(HWND hwnd, LPARAM param) +{ + GameSearch* s = reinterpret_cast(param); + DWORD pid = 0; + GetWindowThreadProcessId(hwnd, &pid); + if (pid != s->pid) return TRUE; + if (!IsWindowVisible(hwnd)) return TRUE; + if (GetWindow(hwnd, GW_OWNER)) return TRUE; + + wchar_t cls[128] = {}; + if (GetClassNameW(hwnd, cls, 128)) { + if (_wcsicmp(cls, kWindowClass) == 0) return TRUE; // 我们自己 + if (_wcsicmp(cls, L"Browser Example") == 0) return TRUE; // 游戏自带面板 + if (_wcsicmp(cls, L"Internet Explorer_Server") == 0) return TRUE; + } + + RECT rc = {}; + if (!GetWindowRect(hwnd, &rc)) return TRUE; + const LONG area = (rc.right - rc.left) * (rc.bottom - rc.top); + if (area > s->area) { + s->area = area; + s->best = hwnd; + } + return TRUE; +} + +// 游戏主窗口 —— 本进程里可见、没有 owner、面积最大的那个顶层窗口。 +HWND FindGameWindow() +{ + GameSearch s = { GetCurrentProcessId(), nullptr, 0 }; + EnumWindows(OnGameCandidate, reinterpret_cast(&s)); + return s.best; +} + +// 算一个「装得进游戏窗口」的矩形。gameOut 回传游戏客户区,供变化检测用。 +bool FitInsideGame(int desiredW, int desiredH, RECT* out, RECT* gameOut) +{ + const HWND game = FindGameWindow(); + if (!game) return false; + + RECT client = {}; + if (!GetClientRect(game, &client)) return false; + POINT tl = { 0, 0 }; + if (!ClientToScreen(game, &tl)) return false; + RECT g = { client.left + tl.x, client.top + tl.y, client.right + tl.x, client.bottom + tl.y }; + + const int gw = g.right - g.left; + const int gh = g.bottom - g.top; + if (gw < 320 || gh < 240) return false; // 还没进游戏/窗口还没建好 + + int margin = gw / 60; + if (margin < 12) margin = 12; + if (margin > 28) margin = 28; + + int w = desiredW > 0 ? desiredW : gw; + int h = desiredH > 0 ? desiredH : gh; + if (w > gw - 2 * margin) w = gw - 2 * margin; + if (h > gh - 2 * margin) h = gh - 2 * margin; + // 窗口小到一定程度就别再缩了,否则控件挤得没法用;宁可稍微溢出一点。 + if (w < 720) w = 720; + if (h < 500) h = 500; + if (w > gw) w = gw; + if (h > gh) h = gh; + + out->left = g.left + (gw - w) / 2; + out->top = g.top + (gh - h) / 2; + out->right = out->left + w; + out->bottom = out->top + h; + if (gameOut) *gameOut = g; + return true; +} + +// 游戏窗口变了(切全屏/改分辨率/挪窗口)就重新摆一次。玩家自己拖过窗口、 +// 游戏没变的情况下不动它,避免跟玩家较劲。 +void FollowGameWindow() +{ + if (!g_hwnd || !IsWindow(g_hwnd) || !IsWindowVisible(g_hwnd) || !g_fitValid) return; + + RECT fit = {}; + RECT game = {}; + if (!FitInsideGame(g_desiredW, g_desiredH, &fit, &game)) return; + + RECT cur = {}; + GetWindowRect(g_hwnd, &cur); + + const bool gameChanged = !EqualRect(&game, &g_lastGameRect); + const bool outside = cur.left < game.left - 4 || cur.top < game.top - 4 || + cur.right > game.right + 4 || cur.bottom > game.bottom + 4; + if (!gameChanged && !outside) return; + + SetWindowPos(g_hwnd, HWND_TOPMOST, fit.left, fit.top, + fit.right - fit.left, fit.bottom - fit.top, + SWP_NOACTIVATE | SWP_SHOWWINDOW); + ResizeWebView(); + InvalidateRect(g_hwnd, nullptr, FALSE); + g_lastGameRect = game; + LogLine("gm window: refit to game %ldx%ld -> %dx%d at %ld,%ld", + game.right - game.left, game.bottom - game.top, + static_cast(fit.right - fit.left), static_cast(fit.bottom - fit.top), + static_cast(fit.left), static_cast(fit.top)); +} + void CloseButtonRect(RECT* out) { RECT rc = {}; @@ -450,20 +569,45 @@ extern "C" bool GmWindowShow(const wchar_t* url, int width, int height) if (!EnsureWindow()) return false; if (url && *url) g_url = url; + if (width > 0) g_desiredW = width; + if (height > 0) g_desiredH = height; + + // 优先让窗口落在游戏画面里:全屏时就是配置的目标尺寸,窗口模式下自动收小。 + RECT fit = {}; + RECT game = {}; + const bool fitted = FitInsideGame(g_desiredW, g_desiredH, &fit, &game); - RECT rc = {}; - GetWindowRect(g_hwnd, &rc); - const int w = width > 0 ? width : (rc.right - rc.left); - const int h = height > 0 ? height : (rc.bottom - rc.top); if (!IsWindowVisible(g_hwnd)) { - SetWindowPos(g_hwnd, HWND_TOPMOST, 0, 0, w, h, SWP_NOMOVE | SWP_SHOWWINDOW); + if (fitted) { + SetWindowPos(g_hwnd, HWND_TOPMOST, fit.left, fit.top, + fit.right - fit.left, fit.bottom - fit.top, + SWP_SHOWWINDOW | SWP_NOACTIVATE); + g_lastGameRect = game; + g_fitValid = true; + LogLine("gm window: show inside game %ldx%ld -> %dx%d at %ld,%ld", + game.right - game.left, game.bottom - game.top, + static_cast(fit.right - fit.left), static_cast(fit.bottom - fit.top), + static_cast(fit.left), static_cast(fit.top)); + } else { + SetWindowPos(g_hwnd, HWND_TOPMOST, 0, 0, + g_desiredW, g_desiredH, SWP_NOMOVE | SWP_SHOWWINDOW | SWP_NOACTIVATE); + g_fitValid = false; + LogLine("gm window: game window not found, show at %dx%d", g_desiredW, g_desiredH); + } } else { SetWindowPos(g_hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + if (fitted) { + SetWindowPos(g_hwnd, HWND_TOPMOST, fit.left, fit.top, + fit.right - fit.left, fit.bottom - fit.top, SWP_NOACTIVATE); + g_lastGameRect = game; + g_fitValid = true; + ResizeWebView(); + } } if (!g_envStarted) { - LayoutCenterOnOwner(); + if (!fitted) LayoutCenterOnOwner(); StartWebView(); } else if (g_web && !g_url.empty()) { // 页面服务可能重启过,重新打开时刷一次,避免停在错误页 @@ -504,6 +648,13 @@ extern "C" void GmWindowAssertTop() } } +// 游戏窗口换了尺寸/位置(切全屏、改分辨率、拖窗口)时把控制台重新摆回游戏画面里。 +// 主循环每轮调一次,内部只在游戏矩形变化或窗口超出游戏范围时才真正动窗口。 +extern "C" void GmWindowFollowGame() +{ + FollowGameWindow(); +} + extern "C" bool GmWindowFailed() { return g_failed; diff --git a/client-patch/90CN_web_redirect.cpp b/client-patch/90CN_web_redirect.cpp index 7a278df..b72738d 100644 --- a/client-patch/90CN_web_redirect.cpp +++ b/client-patch/90CN_web_redirect.cpp @@ -39,6 +39,7 @@ extern "C" void GmWindowHide(); extern "C" bool GmWindowIsVisible(); extern "C" bool GmWindowFailed(); extern "C" void GmPumpMessages(unsigned timeoutMs); +extern "C" void GmWindowFollowGame(); extern "C" void GmWindowAssertTop(); namespace { @@ -699,7 +700,8 @@ DWORD WINAPI WorkerThread(void*) ShowWindow(panel, SW_HIDE); LogLine("web window: panel suppressed hwnd=%p", (void*)panel); } - // 全屏游戏会抢窗口栈,每轮确认一次置顶 + // 全屏游戏会抢窗口栈,每轮确认一次置顶;同时跟着游戏窗口尺寸走 + GmWindowFollowGame(); GmWindowAssertTop(); continue; } diff --git a/server/internal/services/dnfbridge/client_build_id.go b/server/internal/services/dnfbridge/client_build_id.go index 585eb5b..d0a6973 100644 --- a/server/internal/services/dnfbridge/client_build_id.go +++ b/server/internal/services/dnfbridge/client_build_id.go @@ -10,8 +10,8 @@ const ( boundMovieSHA256 = "D62E3623C39CD9FCED5F6C4C7F5E6E18BEB69EB269ABF5D713EDE08EE8C926A1" boundMovieBytes = int64(22846532) - boundDllSHA256 = "CE86D4DF60BC3F8CB33F31D8D2FFE94512FA0E3B7D2EF24B13E142507787CC42" - boundDllBytes = int64(1258496) + boundDllSHA256 = "186A2C95D04B5E0D574D1A66EE097D2B83E41995E50A7C60F7F8620D93C89F23" + boundDllBytes = int64(1260032) )