Files
90CNServer/client-patch/90CN_gm_window.cpp
T
hanfuit b22579658e 小井盖改为自绘 GM 窗口(WebView2)+ 背包可见 + 启动器互斥
- 90CN.dll:新增 90CN_gm_window.cpp,自绘标题栏/边框的置顶窗口,内嵌 WebView2 承载 gmweb 页面
- 点小井盖时隐藏游戏内嵌 IE 面板、弹出控制台;关掉后还原,独占全屏压不住时可选 minimize_game
- 窗口每轮确认置顶,避免游戏抢窗口栈后消失
- gmweb:新增背包页,发放后自动跳转并高亮新入包的行(GET /local/dnf-gm/inventory/list)
- 启动器:新增 /console-status,控制台运行时置灰 GM 工具入口并阻止进入
- 服务端指纹回绑新 DLL(1258496 / 7A179B1B…)
2026-11-05 13:16:52 +08:00

521 lines
17 KiB
C++

// ============================================================================
// 90CN_gm_window.cpp -- 我们自己绘制的 GM 窗口(自绘外壳 + Edge 内核内容)
//
// 为什么要自己画:游戏内嵌的 IE 控件在这个进程里输入法候选框出不来,
// 弹一个浏览器窗口又太“外部”。这里自己建一个无系统边框的窗口,标题栏、
// 边框、按钮全部由 GDI 绘制成和游戏一致的深色风格;内容区放一个 WebView2
// 控件(Edge 内核),中文输入由内核处理,候选框正常。
//
// 窗口生命周期:
// GmWindowShow() 第一次调用时创建窗口并初始化内嵌浏览器,之后复用
// GmWindowHide() 只是隐藏,不销毁,下次打开秒开
// GmPumpMessages() 由重定向线程每轮调用,负责把消息泵喂给窗口和 WebView2
//
// 失败不影响游戏:WebView2 初始化失败会把 g_failed 置位,由调用方回退到
// Edge 应用窗口或内嵌 IE 方案。
// ============================================================================
#include <windows.h>
#include <windowsx.h>
#include <wrl.h>
#include <string>
#include "WebView2.h"
extern "C" void __cdecl CnmLogLine(const char* format, ...);
#define LogLine CnmLogLine
using Microsoft::WRL::Callback;
using Microsoft::WRL::ComPtr;
namespace {
const wchar_t kWindowClass[] = L"90CN_GMHostWindow";
const wchar_t kTitleText[] = L"GM 控制台";
const int kTitleH = 38; // 自绘标题栏高度
const int kBtnW = 44; // 标题栏按钮宽度
const int kEdge = 5; // 拉伸手柄厚度
const int kMinW = 760;
const int kMinH = 520;
HWND g_hwnd = nullptr;
HFONT g_fontTitle = nullptr;
HFONT g_fontSmall = nullptr;
HBRUSH g_brBg = nullptr;
ComPtr<ICoreWebView2Environment> g_env;
ComPtr<ICoreWebView2Controller> g_ctrl;
ComPtr<ICoreWebView2> g_web;
std::wstring g_url;
std::wstring g_userDataDir;
bool g_envStarted = false;
bool g_failed = false;
bool g_trackingLeave = false;
int g_hover = 0; // 1 = 关闭按钮, 2 = 刷新按钮
// 颜色:和 gmweb 的深色主题对齐
const COLORREF kColBg = RGB(0x12, 0x15, 0x1c);
const COLORREF kColBar = RGB(0x1a, 0x1e, 0x27);
const COLORREF kColLine = RGB(0x2f, 0x35, 0x47);
const COLORREF kColTitle = RGB(0xe8, 0xc9, 0x79);
const COLORREF kColMuted = RGB(0x8d, 0x94, 0xa6);
std::wstring EnvValue(const wchar_t* name)
{
wchar_t buf[MAX_PATH] = {};
const DWORD got = GetEnvironmentVariableW(name, buf, MAX_PATH);
return got ? std::wstring(buf, got) : std::wstring();
}
void ResizeWebView()
{
if (!g_ctrl || !g_hwnd) return;
RECT rc = {};
GetClientRect(g_hwnd, &rc);
RECT bounds = { 0, kTitleH, rc.right, rc.bottom };
if (bounds.bottom < bounds.top) bounds.bottom = bounds.top;
g_ctrl->put_Bounds(bounds);
}
void LayoutCenterOnOwner()
{
RECT work = {};
SystemParametersInfoW(SPI_GETWORKAREA, 0, &work, 0);
RECT rc = {};
GetWindowRect(g_hwnd, &rc);
const int w = rc.right - rc.left;
const int h = rc.bottom - rc.top;
const int x = work.left + (work.right - work.left - w) / 2;
const int y = work.top + (work.bottom - work.top - h) / 2;
SetWindowPos(g_hwnd, nullptr, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
}
void CloseButtonRect(RECT* out)
{
RECT rc = {};
GetClientRect(g_hwnd, &rc);
out->right = rc.right;
out->left = rc.right - kBtnW;
out->top = 0;
out->bottom = kTitleH;
}
void RefreshButtonRect(RECT* out)
{
RECT close = {};
CloseButtonRect(&close);
out->right = close.left;
out->left = close.left - kBtnW;
out->top = 0;
out->bottom = kTitleH;
}
void PaintWindow(HWND hwnd)
{
PAINTSTRUCT ps = {};
HDC dc = BeginPaint(hwnd, &ps);
if (!dc) return;
RECT rc = {};
GetClientRect(hwnd, &rc);
// 整窗底色
HBRUSH bg = g_brBg ? g_brBg : static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH));
FillRect(dc, &rc, bg);
// 标题栏
RECT bar = { 0, 0, rc.right, kTitleH };
HBRUSH barBrush = CreateSolidBrush(kColBar);
FillRect(dc, &bar, barBrush);
DeleteObject(barBrush);
// 金色小竖条 + 标题
RECT accent = { 14, 11, 17, kTitleH - 11 };
HBRUSH accentBrush = CreateSolidBrush(kColTitle);
FillRect(dc, &accent, accentBrush);
DeleteObject(accentBrush);
SetBkMode(dc, TRANSPARENT);
SetTextColor(dc, kColTitle);
HFONT old = static_cast<HFONT>(SelectObject(dc, g_fontTitle ? g_fontTitle : GetStockObject(DEFAULT_GUI_FONT)));
RECT textRect = { 24, 0, rc.right - 2 * kBtnW - 8, kTitleH };
DrawTextW(dc, kTitleText, -1, &textRect, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
// 刷新按钮
RECT refresh = {};
RefreshButtonRect(&refresh);
if (g_hover == 2) {
HBRUSH hb = CreateSolidBrush(RGB(0x26, 0x2c, 0x3a));
FillRect(dc, &refresh, hb);
DeleteObject(hb);
}
SetTextColor(dc, kColMuted);
SelectObject(dc, g_fontSmall ? g_fontSmall : GetStockObject(DEFAULT_GUI_FONT));
DrawTextW(dc, L"刷新", -1, &refresh, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
// 关闭按钮(自己画 ×,不依赖字体里有没有这个字形)
RECT close = {};
CloseButtonRect(&close);
if (g_hover == 1) {
HBRUSH hb = CreateSolidBrush(RGB(0xc0, 0x39, 0x2b));
FillRect(dc, &close, hb);
DeleteObject(hb);
}
const int cx = (close.left + close.right) / 2;
const int cy = (close.top + close.bottom) / 2;
const int r = 6;
HPEN pen = CreatePen(PS_SOLID, 2, g_hover == 1 ? RGB(0xff, 0xff, 0xff) : kColMuted);
HPEN oldPen = static_cast<HPEN>(SelectObject(dc, pen));
MoveToEx(dc, cx - r, cy - r, nullptr);
LineTo(dc, cx + r + 1, cy + r + 1);
MoveToEx(dc, cx + r, cy - r, nullptr);
LineTo(dc, cx - r - 1, cy + r + 1);
SelectObject(dc, oldPen);
DeleteObject(pen);
// 分隔线与外框
HPEN linePen = CreatePen(PS_SOLID, 1, kColLine);
HPEN prevPen = static_cast<HPEN>(SelectObject(dc, linePen));
MoveToEx(dc, 0, kTitleH - 1, nullptr);
LineTo(dc, rc.right, kTitleH - 1);
HBRUSH hollow = static_cast<HBRUSH>(GetStockObject(NULL_BRUSH));
HBRUSH prevBrush = static_cast<HBRUSH>(SelectObject(dc, hollow));
Rectangle(dc, 0, 0, rc.right, rc.bottom);
SelectObject(dc, prevBrush);
SelectObject(dc, prevPen);
DeleteObject(linePen);
if (g_failed) {
SetTextColor(dc, kColMuted);
RECT tip = { 0, kTitleH, rc.right, rc.bottom };
DrawTextW(dc, L"内嵌浏览器初始化失败,请检查 WebView2 运行时是否安装。",
-1, &tip, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
} else if (!g_web) {
SetTextColor(dc, kColMuted);
RECT tip = { 0, kTitleH, rc.right, rc.bottom };
DrawTextW(dc, L"正在加载 GM 页面…", -1, &tip, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}
SelectObject(dc, old);
EndPaint(hwnd, &ps);
}
// ---------------------------------------------------------------- WebView2
std::wstring UserDataDir()
{
const std::wstring base = EnvValue(L"LOCALAPPDATA");
if (base.empty()) return std::wstring();
const std::wstring root = base + L"\\90CN";
CreateDirectoryW(root.c_str(), nullptr);
const std::wstring dir = root + L"\\wv2";
CreateDirectoryW(dir.c_str(), nullptr);
return dir;
}
void StartWebView()
{
if (g_envStarted || g_failed || !g_hwnd) return;
g_envStarted = true;
g_userDataDir = UserDataDir();
if (g_userDataDir.empty()) {
g_failed = true;
LogLine("gm window: LOCALAPPDATA missing, webview cannot start");
InvalidateRect(g_hwnd, nullptr, FALSE);
return;
}
const HRESULT hr = CreateCoreWebView2EnvironmentWithOptions(nullptr, g_userDataDir.c_str(), nullptr,
Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
[](HRESULT result, ICoreWebView2Environment* env) -> HRESULT {
if (FAILED(result) || !env) {
g_failed = true;
LogLine("gm window: environment failed hr=0x%08X", (unsigned)result);
if (g_hwnd) InvalidateRect(g_hwnd, nullptr, FALSE);
return S_OK;
}
g_env = env;
g_env->CreateCoreWebView2Controller(g_hwnd,
Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
[](HRESULT result2, ICoreWebView2Controller* controller) -> HRESULT {
if (FAILED(result2) || !controller) {
g_failed = true;
LogLine("gm window: controller failed hr=0x%08X", (unsigned)result2);
if (g_hwnd) InvalidateRect(g_hwnd, nullptr, FALSE);
return S_OK;
}
g_ctrl = controller;
g_ctrl->get_CoreWebView2(&g_web);
if (g_web) {
// 关掉右键菜单和开发者工具入口,看起来更像应用而不是网页
ComPtr<ICoreWebView2Settings> settings;
if (SUCCEEDED(g_web->get_Settings(&settings)) && settings) {
settings->put_AreDefaultContextMenusEnabled(FALSE);
settings->put_IsStatusBarEnabled(FALSE);
settings->put_AreDevToolsEnabled(FALSE);
}
if (!g_url.empty()) g_web->Navigate(g_url.c_str());
}
ResizeWebView();
if (g_hwnd) InvalidateRect(g_hwnd, nullptr, FALSE);
LogLine("gm window: webview ready");
return S_OK;
}).Get());
return S_OK;
}).Get());
if (FAILED(hr)) {
g_failed = true;
LogLine("gm window: CreateCoreWebView2EnvironmentWithOptions hr=0x%08X (runtime missing?)", (unsigned)hr);
if (g_hwnd) InvalidateRect(g_hwnd, nullptr, FALSE);
}
}
// ---------------------------------------------------------------- 窗口过程
LRESULT HitTest(HWND hwnd)
{
POINT pt = {};
GetCursorPos(&pt);
ScreenToClient(hwnd, &pt);
RECT rc = {};
GetClientRect(hwnd, &rc);
RECT close = {};
CloseButtonRect(&close);
RECT refresh = {};
RefreshButtonRect(&refresh);
if (PtInRect(&close, pt) || PtInRect(&refresh, pt)) return HTCLIENT;
// 先判边缘,方便拉伸
const bool left = pt.x < kEdge;
const bool right = pt.x >= rc.right - kEdge;
const bool top = pt.y < kEdge;
const bool bottom = pt.y >= rc.bottom - kEdge;
if (top && left) return HTTOPLEFT;
if (top && right) return HTTOPRIGHT;
if (bottom && left) return HTBOTTOMLEFT;
if (bottom && right) return HTBOTTOMRIGHT;
if (left) return HTLEFT;
if (right) return HTRIGHT;
if (top) return HTTOP;
if (bottom) return HTBOTTOM;
if (pt.y < kTitleH) return HTCAPTION;
return HTCLIENT;
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
switch (msg) {
case WM_NCCALCSIZE:
if (wparam) return 0; // 去掉系统非客户区,边框和标题栏由我们自己画
break;
case WM_NCHITTEST:
return HitTest(hwnd);
case WM_ERASEBKGND:
return 1;
case WM_PAINT:
PaintWindow(hwnd);
return 0;
case WM_SIZE:
ResizeWebView();
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
case WM_MOUSEMOVE: {
if (!g_trackingLeave) {
TRACKMOUSEEVENT tme = { sizeof(tme), TME_LEAVE, hwnd, 0 };
TrackMouseEvent(&tme);
g_trackingLeave = true;
}
POINT pt = { GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam) };
RECT close = {};
CloseButtonRect(&close);
RECT refresh = {};
RefreshButtonRect(&refresh);
int hover = 0;
if (PtInRect(&close, pt)) hover = 1;
else if (PtInRect(&refresh, pt)) hover = 2;
if (hover != g_hover) {
g_hover = hover;
InvalidateRect(hwnd, nullptr, FALSE);
}
return 0;
}
case WM_MOUSELEAVE:
g_trackingLeave = false;
if (g_hover != 0) {
g_hover = 0;
InvalidateRect(hwnd, nullptr, FALSE);
}
return 0;
case WM_LBUTTONDOWN: {
POINT pt = { GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam) };
RECT close = {};
CloseButtonRect(&close);
RECT refresh = {};
RefreshButtonRect(&refresh);
if (PtInRect(&close, pt)) {
PostMessageW(hwnd, WM_CLOSE, 0, 0);
return 0;
}
if (PtInRect(&refresh, pt)) {
if (g_web) g_web->Reload();
return 0;
}
break;
}
case WM_CLOSE:
ShowWindow(hwnd, SW_HIDE);
SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
LogLine("gm window: hidden by user");
return 0;
case WM_GETMINMAXINFO: {
MINMAXINFO* info = reinterpret_cast<MINMAXINFO*>(lparam);
info->ptMinTrackSize.x = kMinW;
info->ptMinTrackSize.y = kMinH;
return 0;
}
case WM_DESTROY:
g_hwnd = nullptr;
return 0;
default:
break;
}
return DefWindowProcW(hwnd, msg, wparam, lparam);
}
bool EnsureWindow()
{
if (g_hwnd && IsWindow(g_hwnd)) return true;
WNDCLASSEXW wc = {};
wc.cbSize = sizeof(wc);
wc.style = CS_DROPSHADOW;
wc.lpfnWndProc = WindowProc;
wc.hInstance = GetModuleHandleW(nullptr);
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
wc.hbrBackground = nullptr;
wc.lpszClassName = kWindowClass;
if (!RegisterClassExW(&wc) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) {
LogLine("gm window: RegisterClassEx failed err=%u", GetLastError());
return false;
}
if (!g_brBg) g_brBg = CreateSolidBrush(kColBg);
if (!g_fontTitle) {
g_fontTitle = CreateFontW(-16, 0, 0, 0, FW_SEMIBOLD, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
DEFAULT_PITCH | FF_DONTCARE, L"Microsoft YaHei UI");
}
if (!g_fontSmall) {
g_fontSmall = CreateFontW(-13, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
DEFAULT_PITCH | FF_DONTCARE, L"Microsoft YaHei UI");
}
g_hwnd = CreateWindowExW(WS_EX_TOOLWINDOW, kWindowClass, kTitleText,
WS_POPUP | WS_THICKFRAME | WS_CLIPCHILDREN,
CW_USEDEFAULT, CW_USEDEFAULT, 1040, 720,
nullptr, nullptr, wc.hInstance, nullptr);
if (!g_hwnd) {
LogLine("gm window: CreateWindowEx failed err=%u", GetLastError());
return false;
}
return true;
}
} // namespace
// ---------------------------------------------------------------- 对外接口
extern "C" bool GmWindowShow(const wchar_t* url, int width, int height)
{
if (!EnsureWindow()) return false;
if (url && *url) g_url = url;
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);
} else {
SetWindowPos(g_hwnd, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
}
if (!g_envStarted) {
LayoutCenterOnOwner();
StartWebView();
} else if (g_web && !g_url.empty()) {
// 页面服务可能重启过,重新打开时刷一次,避免停在错误页
g_web->Reload();
}
ShowWindow(g_hwnd, SW_SHOW);
SetForegroundWindow(g_hwnd);
ResizeWebView();
InvalidateRect(g_hwnd, nullptr, FALSE);
return true;
}
extern "C" void GmWindowHide()
{
if (g_hwnd && IsWindow(g_hwnd) && IsWindowVisible(g_hwnd)) {
ShowWindow(g_hwnd, SW_HIDE);
SetWindowPos(g_hwnd, HWND_NOTOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
}
}
extern "C" bool GmWindowIsVisible()
{
return g_hwnd && IsWindow(g_hwnd) && IsWindowVisible(g_hwnd);
}
// 游戏有时会在切图、恢复全屏时把窗口栈重排一遍,把我们的窗口压回普通层。
// 主循环每轮调一次,发现置顶丢失就重新提上去(不抢焦点)。
extern "C" void GmWindowAssertTop()
{
if (!g_hwnd || !IsWindow(g_hwnd)) return;
const LONG_PTR ex = GetWindowLongPtrW(g_hwnd, GWL_EXSTYLE);
if ((ex & WS_EX_TOPMOST) == 0) {
SetWindowPos(g_hwnd, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
LogLine("gm window: re-asserted topmost");
}
}
extern "C" bool GmWindowFailed()
{
return g_failed;
}
extern "C" void GmPumpMessages(unsigned timeoutMs)
{
MsgWaitForMultipleObjects(0, nullptr, FALSE, timeoutMs, QS_ALLINPUT);
MSG msg = {};
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}