feat(gameplay): 更新单位属性和材质配置

- 调整多个单位的基础生命值和攻击力数值平衡
- 修改CoreBattleGround材质的纹理缩放比例
- 更新地砖材质颜色为白色
- 优化纹理资源设置,调整最大纹理尺寸并添加PlayableAds平台支持
- 修复弓箭手技能目标查找逻辑,改用统一的敌人检测方法
- 为水晶矿添加中立阵营设置和光球视觉效果同步
- 调整预制体中的全局缩放参数
- 为狼人和石巨人添加NavMeshAgent组件支持
- 优化水晶矿血条显示位置计算方式
```
This commit is contained in:
huadexuan committed 2026-09-20 21:52:31 +08:00
1 parent 0e7de5f885
commit 53d95802e5
448 files changed
+133333 -472

No files matched your search

@@ -0,0 +1,400 @@
#include "BrowserInstance.h"
#include <wrl/event.h> // Microsoft::WRL::Callback
#include <shlwapi.h>
// ── 静态成员定义 ──────────────────────────────────────────────────────────────
std::mutex BrowserInstance::s_mapMutex;
std::unordered_map<HWND, BrowserInstance*> BrowserInstance::s_hwndMap;
std::unordered_set<BrowserInstance*> BrowserInstance::s_liveInstances;
std::mutex BrowserInstance::s_liveSetMutex;
// ── WinEvent 回调(WINEVENT_INCONTEXT → 在 Unity 主线程同步调用)────────────
void CALLBACK BrowserInstance::WinEventProc(
HWINEVENTHOOK /*hook*/, DWORD /*event*/,
HWND hwnd, LONG objId, LONG childId,
DWORD /*idEventThread*/, DWORD /*dwmsEventTime*/)
{
if (objId != 0 /*OBJID_WINDOW*/ || childId != 0 /*CHILDID_SELF*/) return;
BrowserInstance* inst = nullptr;
{
std::lock_guard<std::mutex> lk(s_mapMutex);
auto it = s_hwndMap.find(hwnd);
if (it == s_hwndMap.end()) return;
inst = it->second;
}
if (!inst || inst->m_shutdown.load()) return;
RECT rc = {};
if (!GetClientRect(hwnd, &rc)) return;
inst->PostResizeBounds(rc.right, rc.bottom);
}
// ── Constructor / Destructor ──────────────────────────────────────────────────
BrowserInstance::BrowserInstance(HWND parentHwnd,
std::wstring userDataFolder,
std::wstring initialUrl,
NB_Callbacks callbacks)
: m_parentHwnd(parentHwnd)
, m_userDataFolder(std::move(userDataFolder))
, m_initialUrl(std::move(initialUrl))
, m_callbacks(callbacks)
{
{
std::lock_guard<std::mutex> lk(s_liveSetMutex);
s_liveInstances.insert(this);
}
m_staThread = std::make_unique<STAThread>(L"NativeBrowser-STA");
m_wndProcHook = std::make_unique<WndProcHook>(
parentHwnd,
[this](int w, int h) { PostResizeBounds(w, h); },
[this] { FireParentDestroyed(); }
);
m_staThread->Post([this] { InitOnSTAThread(); });
}
BrowserInstance::~BrowserInstance()
{
{
std::lock_guard<std::mutex> lk(s_liveSetMutex);
s_liveInstances.erase(this);
}
// 析构时确保两阶段都完成(正常情况下 NB_Destroy 已经完成了 Phase 1;
// 若析构由 NB_Destroy 的后台线程 delete 触发,Phase 2 也已完成,均幂等)。
PrepareShutdown();
FinalizeShutdown();
}
// ── 两阶段关闭 ────────────────────────────────────────────────────────────────
bool BrowserInstance::PrepareShutdown()
{
// m_shutdown 保证幂等:只有第一次调用执行实际逻辑
if (m_shutdown.exchange(true)) return false;
// 1. 从全局映射删除 → WinEvent/WM_SIZE 回调立即找不到此实例(线程安全)
{
std::lock_guard<std::mutex> lk(s_mapMutex);
s_hwndMap.erase(m_parentHwnd);
}
// 2. 卸载 WndProcHook(调用 RemoveWindowSubclass,必须在 Unity 主线程)
m_wndProcHook.reset();
// WinEvent 的 UnhookWinEvent 需在 STA 线程(与 SetWinEventHook 同线程),
// 留给 FinalizeShutdown 通过 STA Post 完成。
return true;
}
void BrowserInstance::PostFinalizeShutdown()
{
// m_finalized 保证幂等
if (m_finalized.exchange(true)) return;
// 确保 PrepareShutdown 已完成(若直接调用 FinalizeShutdown 而未先 Prepare)
PrepareShutdown();
// 捕获 WinEvent hook 供 STA 线程卸载(原子交换,避免与 RegisterWinEventHook 竞争)
HWINEVENTHOOK hook = m_winEventHook.exchange(nullptr);
// 向 STA 线程投递:卸载 WinEvent hook + 关闭 WebView2 COM 对象
m_staThread->Post([this, hook] {
if (hook) UnhookWinEvent(hook); // 与 SetWinEventHook 同线程(STA)
if (m_webview)
{
m_webview->remove_NavigationStarting(m_navigationToken);
m_webview->remove_DocumentTitleChanged(m_titleToken);
m_webview->remove_WebMessageReceived(m_messageToken);
m_webview->remove_NavigationCompleted(m_loadToken);
}
if (m_controller) m_controller->Close();
m_webview.Reset();
m_controller.Reset();
m_env.Reset();
});
// 投递 WM_QUIT,让 STA 线程在处理完上述 close 工作后退出。
// 不在此函数内等待——由调用方负责(NB_ShutdownAll 用 pumped wait 避免死锁)。
m_staThread->RequestQuit();
}
HANDLE BrowserInstance::GetStaThreadHandle() const
{
return m_staThread ? m_staThread->ThreadHandle() : nullptr;
}
void BrowserInstance::FinalizeShutdown(DWORD timeoutMs)
{
PostFinalizeShutdown(); // 幂等
if (m_staThread)
m_staThread->Shutdown(timeoutMs); // RequestQuit 已发;这里只是 WaitForSingleObject
}
// ── 跨 Domain Reload 恢复 ─────────────────────────────────────────────────────
bool BrowserInstance::IsLive(void* handle)
{
if (!handle) return false;
std::lock_guard<std::mutex> lk(s_liveSetMutex);
return s_liveInstances.count(static_cast<BrowserInstance*>(handle)) > 0;
}
void BrowserInstance::SetCallbacks(const NB_Callbacks& callbacks)
{
std::lock_guard<std::mutex> lk(m_callbacksMutex);
m_callbacks = callbacks;
}
// ── Public API ────────────────────────────────────────────────────────────────
void BrowserInstance::Navigate(const wchar_t* url)
{
std::wstring u(url ? url : L"");
m_staThread->Post([this, u = std::move(u)] {
if (m_webview) m_webview->Navigate(u.c_str());
});
}
void BrowserInstance::GoBack() { m_staThread->Post([this] { if (m_webview) m_webview->GoBack(); }); }
void BrowserInstance::GoForward() { m_staThread->Post([this] { if (m_webview) m_webview->GoForward(); }); }
void BrowserInstance::Reload() { m_staThread->Post([this] { if (m_webview) m_webview->Reload(); }); }
void BrowserInstance::Stop() { m_staThread->Post([this] { if (m_webview) m_webview->Stop(); }); }
void BrowserInstance::Resize(int x, int y, int w, int h)
{
m_staThread->Post([this, x, y, w, h] {
if (m_controller) m_controller->put_Bounds({ x, y, x + w, y + h });
});
}
void BrowserInstance::SetVisible(bool v)
{
m_staThread->Post([this, v] {
if (m_controller) m_controller->put_IsVisible(v ? TRUE : FALSE);
});
}
void BrowserInstance::ExecuteScript(const wchar_t* js)
{
std::wstring s(js ? js : L"");
m_staThread->Post([this, s = std::move(s)] {
if (m_webview) m_webview->ExecuteScript(s.c_str(), nullptr);
});
}
void BrowserInstance::PostWebMessage(const wchar_t* json)
{
std::wstring s(json ? json : L"");
m_staThread->Post([this, s = std::move(s)] {
if (m_webview) m_webview->PostWebMessageAsJson(s.c_str());
});
}
void BrowserInstance::OpenDevTools()
{
m_staThread->Post([this] {
if (m_webview) m_webview->OpenDevToolsWindow();
});
}
void BrowserInstance::MoveFocus()
{
// 必须在拥有 controller 的 STA 线程上调用。PROGRAMMATIC 让 WebView2 取得 Win32 焦点,
// 内容文档随之获得系统焦点,挂起的 autofocus / 已聚焦元素的光标得以显示并接收键盘输入。
m_staThread->Post([this] {
if (m_controller && !m_shutdown.load())
m_controller->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC);
});
}
void BrowserInstance::SetTopInset(int px)
{
m_topInset.store(px < 0 ? 0 : px);
m_staThread->Post([this] { SyncBounds(); });
}
// ── Resize 合并投递(主线程调用)─────────────────────────────────────────────
void BrowserInstance::PostResizeBounds(int w, int h)
{
m_pendingW.store((LONG)w);
m_pendingH.store((LONG)h);
bool expected = false;
if (m_resizePending.compare_exchange_strong(expected, true))
{
m_staThread->Post([this] {
m_resizePending.store(false);
if (!m_controller || m_shutdown.load()) return;
LONG w = m_pendingW.load();
LONG h = m_pendingH.load();
int inset = m_topInset.load();
m_controller->put_Bounds({ 0, inset, (int)w, (int)h });
});
}
}
// ── WinEvent Hook ─────────────────────────────────────────────────────────────
void BrowserInstance::RegisterWinEventHook()
{
HMODULE hMod = nullptr;
GetModuleHandleExW(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(&BrowserInstance::WinEventProc),
&hMod);
DWORD parentThreadId = GetWindowThreadProcessId(m_parentHwnd, nullptr);
m_winEventHook = SetWinEventHook(
EVENT_OBJECT_LOCATIONCHANGE, EVENT_OBJECT_LOCATIONCHANGE,
hMod,
WinEventProc,
0,
parentThreadId,
WINEVENT_INCONTEXT
);
}
// ── WebView2 Initialization (runs on STA thread) ──────────────────────────────
void BrowserInstance::InitOnSTAThread()
{
using namespace Microsoft::WRL;
HRESULT hr = CreateCoreWebView2EnvironmentWithOptions(
nullptr,
m_userDataFolder.c_str(),
nullptr,
Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
[this](HRESULT result, ICoreWebView2Environment* env) -> HRESULT
{
if (FAILED(result) || !env) { FireLoadCompleted(false); return result; }
m_env = env;
return env->CreateCoreWebView2Controller(
m_parentHwnd,
Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
[this](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT
{
if (FAILED(result) || !controller) { FireLoadCompleted(false); return result; }
m_controller = controller;
m_controller->get_CoreWebView2(&m_webview);
SyncBounds();
ComPtr<ICoreWebView2Settings> settings;
if (SUCCEEDED(m_webview->get_Settings(&settings)))
{
settings->put_IsStatusBarEnabled(FALSE);
settings->put_AreDevToolsEnabled(TRUE);
}
m_webview->add_NavigationStarting(
Callback<ICoreWebView2NavigationStartingEventHandler>(
[this](ICoreWebView2*, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT
{
LPWSTR uri = nullptr;
args->get_Uri(&uri);
if (uri) { FireNavigationStarting(uri); CoTaskMemFree(uri); }
return S_OK;
}).Get(), &m_navigationToken);
m_webview->add_DocumentTitleChanged(
Callback<ICoreWebView2DocumentTitleChangedEventHandler>(
[this](ICoreWebView2* wv, IUnknown*) -> HRESULT
{
LPWSTR title = nullptr;
wv->get_DocumentTitle(&title);
if (title) { FireTitleChanged(title); CoTaskMemFree(title); }
return S_OK;
}).Get(), &m_titleToken);
m_webview->add_WebMessageReceived(
Callback<ICoreWebView2WebMessageReceivedEventHandler>(
[this](ICoreWebView2*, ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT
{
LPWSTR json = nullptr;
args->get_WebMessageAsJson(&json);
if (json) { FireWebMessageReceived(json); CoTaskMemFree(json); }
return S_OK;
}).Get(), &m_messageToken);
m_webview->add_NavigationCompleted(
Callback<ICoreWebView2NavigationCompletedEventHandler>(
[this](ICoreWebView2*, ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT
{
BOOL success = FALSE;
args->get_IsSuccess(&success);
FireLoadCompleted(success == TRUE);
return S_OK;
}).Get(), &m_loadToken);
m_ready = true;
{
std::lock_guard<std::mutex> lk(s_mapMutex);
s_hwndMap[m_parentHwnd] = this;
}
RegisterWinEventHook();
m_webview->Navigate(m_initialUrl.c_str());
return S_OK;
}).Get());
}).Get());
if (FAILED(hr))
FireLoadCompleted(false);
}
void BrowserInstance::SyncBounds()
{
if (!m_controller || !m_parentHwnd) return;
RECT rc = {};
GetClientRect(m_parentHwnd, &rc);
int inset = m_topInset.load();
m_controller->put_Bounds({ 0, inset, rc.right, rc.bottom });
}
// ── Callbacks ─────────────────────────────────────────────────────────────────
void BrowserInstance::FireNavigationStarting(const std::wstring& url)
{
NB_NavigationCallback cb; void* ctx;
{ std::lock_guard<std::mutex> lk(m_callbacksMutex); cb = m_callbacks.onNavigationStarting; ctx = m_callbacks.ctx; }
if (cb) cb(url.c_str(), ctx);
}
void BrowserInstance::FireLoadCompleted(bool success)
{
NB_LoadCompletedCallback cb; void* ctx;
{ std::lock_guard<std::mutex> lk(m_callbacksMutex); cb = m_callbacks.onLoadCompleted; ctx = m_callbacks.ctx; }
if (cb) cb(success, ctx);
}
void BrowserInstance::FireTitleChanged(const std::wstring& title)
{
NB_TitleChangedCallback cb; void* ctx;
{ std::lock_guard<std::mutex> lk(m_callbacksMutex); cb = m_callbacks.onTitleChanged; ctx = m_callbacks.ctx; }
if (cb) cb(title.c_str(), ctx);
}
void BrowserInstance::FireWebMessageReceived(const std::wstring& json)
{
NB_MessageCallback cb; void* ctx;
{ std::lock_guard<std::mutex> lk(m_callbacksMutex); cb = m_callbacks.onWebMessageReceived; ctx = m_callbacks.ctx; }
if (cb) cb(json.c_str(), ctx);
}
void BrowserInstance::FireParentDestroyed()
{
NB_DestroyedCallback cb; void* ctx;
{ std::lock_guard<std::mutex> lk(m_callbacksMutex); cb = m_callbacks.onParentDestroyed; ctx = m_callbacks.ctx; }
if (cb) cb(ctx);
}
@@ -0,0 +1,43 @@
fileFormatVersion: 2
guid: 2fc66ef2416d9064f9d3a0e22f94fc97
PluginImporter:
externalObjects: {}
serializedVersion: 3
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
Any:
enabled: 0
settings:
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Editor:
enabled: 1
settings:
CPU: x86_64
DefaultValueInitialized: true
OS: Windows
Linux64:
enabled: 0
settings:
CPU: None
Win:
enabled: 0
settings:
CPU: None
Win64:
enabled: 0
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,110 @@
#pragma once
#include <windows.h>
#include <wrl/client.h>
#include <WebView2.h>
#include <string>
#include <memory>
#include <atomic>
#include <mutex>
#include <unordered_map>
#include <unordered_set>
#include "../include/NativeBrowser.h"
#include "STAThread.h"
#include "WndProcHook.h"
class BrowserInstance {
public:
BrowserInstance(HWND parentHwnd,
std::wstring userDataFolder,
std::wstring initialUrl,
NB_Callbacks callbacks);
~BrowserInstance();
// ── 跨 Domain Reload 恢复支持 ─────────────────────────────────────────
// 用于验证一个 void* 是否仍指向活跃的 BrowserInstance,避免野指针崩溃
static bool IsLive(void* handle);
// 替换托管回调函数指针(Domain Reload 后旧指针已失效,必须在 OnEnable 中调用)
void SetCallbacks(const NB_Callbacks& callbacks);
// 活跃实例注册表(供 NativeBrowser.cpp 的 NB_IsHandleValid 使用)
static std::unordered_set<BrowserInstance*> s_liveInstances;
static std::mutex s_liveSetMutex;
BrowserInstance(const BrowserInstance&) = delete;
BrowserInstance& operator=(const BrowserInstance&) = delete;
void Navigate (const wchar_t* url);
void GoBack ();
void GoForward ();
void Reload ();
void Stop ();
void Resize (int x, int y, int w, int h);
void SetVisible (bool v);
void SetTopInset (int px);
void ExecuteScript (const wchar_t* js);
void PostWebMessage (const wchar_t* json);
void OpenDevTools ();
void MoveFocus ();
bool PrepareShutdown();
void FinalizeShutdown(DWORD timeoutMs = 5000);
// 拆分版 FinalizeShutdown:仅投递 close 工作和 WM_QUIT 到 STA 线程,不等待。
// 调用方需通过 GetStaThreadHandle() 自行等待——通常用 MsgWaitForMultipleObjects
// pump 消息,避免 WebView2 的 cross-apartment marshaling 在父 HWND 所属线程上死锁。
void PostFinalizeShutdown();
HANDLE GetStaThreadHandle() const;
void PostResizeBounds(int w, int h);
private:
void InitOnSTAThread();
void SyncBounds();
void RegisterWinEventHook();
static void CALLBACK WinEventProc(HWINEVENTHOOK hook, DWORD event,
HWND hwnd, LONG objId, LONG childId,
DWORD idEventThread, DWORD dwmsEventTime);
static std::mutex s_mapMutex;
static std::unordered_map<HWND, BrowserInstance*> s_hwndMap;
void FireNavigationStarting (const std::wstring& url);
void FireLoadCompleted (bool success);
void FireTitleChanged (const std::wstring& title);
void FireWebMessageReceived (const std::wstring& json);
void FireParentDestroyed ();
HWND m_parentHwnd;
std::wstring m_userDataFolder;
std::wstring m_initialUrl;
NB_Callbacks m_callbacks;
std::unique_ptr<STAThread> m_staThread;
std::unique_ptr<WndProcHook> m_wndProcHook;
Microsoft::WRL::ComPtr<ICoreWebView2Environment> m_env;
Microsoft::WRL::ComPtr<ICoreWebView2Controller> m_controller;
Microsoft::WRL::ComPtr<ICoreWebView2> m_webview;
EventRegistrationToken m_navigationToken = {};
EventRegistrationToken m_titleToken = {};
EventRegistrationToken m_messageToken = {};
EventRegistrationToken m_loadToken = {};
std::atomic<HWINEVENTHOOK> m_winEventHook { nullptr };
std::atomic<LONG> m_pendingW { 0 };
std::atomic<LONG> m_pendingH { 0 };
std::atomic<bool> m_resizePending { false };
std::atomic<bool> m_ready { false };
std::atomic<bool> m_shutdown { false };
std::atomic<bool> m_finalized { false };
std::atomic<int> m_topInset { 0 };
// 保护 m_callbacks 的读写,SetCallbacks 在 Unity 主线程调用,
// Fire* 系列在 STA 线程调用,需要互斥
mutable std::mutex m_callbacksMutex;
};
@@ -0,0 +1,43 @@
fileFormatVersion: 2
guid: 4b99dbf7103b9ae43a458a59e5b176b9
PluginImporter:
externalObjects: {}
serializedVersion: 3
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
Any:
enabled: 0
settings:
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Editor:
enabled: 1
settings:
CPU: x86_64
DefaultValueInitialized: true
OS: Windows
Linux64:
enabled: 0
settings:
CPU: None
Win:
enabled: 0
settings:
CPU: None
Win64:
enabled: 0
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,168 @@
#include "../include/NativeBrowser.h"
#include "BrowserInstance.h"
#include <WebView2.h>
#include <string>
#include <thread>
#include <vector>
// ── 生命周期 ──────────────────────────────────────────────────────────────
NB_API void* NB_Create(HWND parentHwnd, const wchar_t* userDataFolder,
const wchar_t* initialUrl, const NB_Callbacks* callbacks)
{
if (!parentHwnd || !IsWindow(parentHwnd)) return nullptr;
try
{
auto* inst = new BrowserInstance(
parentHwnd,
userDataFolder ? userDataFolder : L"",
initialUrl ? initialUrl : L"about:blank",
callbacks ? *callbacks : NB_Callbacks{}
);
return inst;
}
catch (...) { return nullptr; }
}
NB_API void NB_Destroy(void* handle)
{
if (!handle) return;
auto* inst = static_cast<BrowserInstance*>(handle);
inst->PrepareShutdown();
std::thread([inst] {
inst->FinalizeShutdown();
delete inst;
}).detach();
}
NB_API void NB_ShutdownAll(uint32_t timeoutMs)
{
// 复制一份活跃实例列表,避免在等待期间与 NB_Destroy 的后台 delete 线程竞争。
std::vector<BrowserInstance*> insts;
{
std::lock_guard<std::mutex> lk(BrowserInstance::s_liveSetMutex);
insts.assign(BrowserInstance::s_liveInstances.begin(),
BrowserInstance::s_liveInstances.end());
}
if (insts.empty()) return;
// Phase 1: PrepareShutdown 必须在调用线程(父 HWND 所属线程)上执行,
// 因为 ~WndProcHook 调用 RemoveWindowSubclass 要求窗口所属线程。
for (auto* inst : insts)
try { inst->PrepareShutdown(); } catch (...) {}
// Phase 2: 并行投递 close + WM_QUIT 到所有 STA 线程,收集线程句柄等待。
std::vector<HANDLE> threads;
threads.reserve(insts.size());
for (auto* inst : insts)
{
try
{
inst->PostFinalizeShutdown();
if (HANDLE h = inst->GetStaThreadHandle()) threads.push_back(h);
}
catch (...) {}
}
// Phase 3: 在调用线程 pump 消息等待 STA 线程退出。
// 不能 WaitForSingleObject:ICoreWebView2Controller::Close 会跨 apartment 把
// 工作 marshal 回父 HWND 所属线程;若调用线程阻塞不 pump 消息,STA 线程将
// 永久卡在 Close 内部,msedgewebview2.exe 不会被通知退出,并且后续父 HWND
// 销毁时无法整理归 STA 所有的子窗口 → 主线程挂起。
const DWORD start = GetTickCount();
while (!threads.empty())
{
const DWORD elapsed = GetTickCount() - start;
if (elapsed >= timeoutMs) break;
DWORD result = MsgWaitForMultipleObjectsEx(
(DWORD)threads.size(), threads.data(),
timeoutMs - elapsed,
QS_ALLINPUT, MWMO_INPUTAVAILABLE);
if (result == WAIT_TIMEOUT || result == WAIT_FAILED) break;
if (result < WAIT_OBJECT_0 + threads.size())
{
threads.erase(threads.begin() + (result - WAIT_OBJECT_0));
continue;
}
// 输入队列有消息:pump 以让 cross-apartment marshaling 完成。
MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT)
{
// 调用方的主消息循环需要这条;重新投递并立即返回。
PostQuitMessage((int)msg.wParam);
return;
}
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
}
NB_API void NB_SetCallbacks(void* handle, const NB_Callbacks* callbacks)
{
if (!handle || !callbacks) return;
if (!BrowserInstance::IsLive(handle)) return;
static_cast<BrowserInstance*>(handle)->SetCallbacks(*callbacks);
}
NB_API bool NB_IsHandleValid(void* handle)
{
return BrowserInstance::IsLive(handle);
}
// ── 导航 ──────────────────────────────────────────────────────────────────
NB_API void NB_Navigate (void* h, const wchar_t* url) { if (h) static_cast<BrowserInstance*>(h)->Navigate(url); }
NB_API void NB_GoBack (void* h) { if (h) static_cast<BrowserInstance*>(h)->GoBack(); }
NB_API void NB_GoForward (void* h) { if (h) static_cast<BrowserInstance*>(h)->GoForward(); }
NB_API void NB_Reload (void* h) { if (h) static_cast<BrowserInstance*>(h)->Reload(); }
NB_API void NB_Stop (void* h) { if (h) static_cast<BrowserInstance*>(h)->Stop(); }
// ── 布局 ──────────────────────────────────────────────────────────────────
NB_API void NB_Resize (void* h, int x, int y, int w, int h2) { if (h) static_cast<BrowserInstance*>(h)->Resize(x, y, w, h2); }
NB_API void NB_SetVisible (void* h, bool v) { if (h) static_cast<BrowserInstance*>(h)->SetVisible(v); }
NB_API void NB_SetTopInset(void* h, int px) { if (h) static_cast<BrowserInstance*>(h)->SetTopInset(px); }
// ── 脚本 ──────────────────────────────────────────────────────────────────
NB_API void NB_ExecuteScript (void* h, const wchar_t* js) { if (h) static_cast<BrowserInstance*>(h)->ExecuteScript(js); }
NB_API void NB_PostWebMessage (void* h, const wchar_t* json) { if (h) static_cast<BrowserInstance*>(h)->PostWebMessage(json); }
// ── 焦点 ──────────────────────────────────────────────────────────────────
NB_API void NB_MoveFocus(void* h) { if (h) static_cast<BrowserInstance*>(h)->MoveFocus(); }
// ── 调试 ──────────────────────────────────────────────────────────────────
NB_API void NB_OpenDevTools(void* h) { if (h) static_cast<BrowserInstance*>(h)->OpenDevTools(); }
// ── Runtime 检测 ──────────────────────────────────────────────────────────
NB_API bool NB_IsRuntimeAvailable()
{
LPWSTR version = nullptr;
HRESULT hr = GetAvailableCoreWebView2BrowserVersionString(nullptr, &version);
if (version) CoTaskMemFree(version);
return SUCCEEDED(hr);
}
NB_API const wchar_t* NB_GetRuntimeVersion()
{
static wchar_t s_buf[128] = {};
LPWSTR version = nullptr;
if (SUCCEEDED(GetAvailableCoreWebView2BrowserVersionString(nullptr, &version)) && version)
{
wcsncpy_s(s_buf, version, _TRUNCATE);
CoTaskMemFree(version);
}
return s_buf;
}
@@ -0,0 +1,43 @@
fileFormatVersion: 2
guid: ffaeb9254924fd540b5d715b37113e41
PluginImporter:
externalObjects: {}
serializedVersion: 3
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
Any:
enabled: 0
settings:
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Editor:
enabled: 1
settings:
CPU: x86_64
DefaultValueInitialized: true
OS: Windows
Linux64:
enabled: 0
settings:
CPU: None
Win:
enabled: 0
settings:
CPU: None
Win64:
enabled: 0
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,154 @@
#include "STAThread.h"
#include <objbase.h>
#include <stdexcept>
static constexpr wchar_t kWndClass[] = L"NativeBrowserSTAPump";
static LRESULT CALLBACK PumpWndProc(HWND hwnd, UINT msg, WPARAM w, LPARAM l)
{
return DefWindowProcW(hwnd, msg, w, l);
}
// ── 构造/析构 ──────────────────────────────────────────────────────────────
STAThread::STAThread(std::wstring name)
: m_name(std::move(name))
{
m_readyEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
if (!m_readyEvent)
throw std::runtime_error("STAThread: CreateEvent failed");
m_thread = CreateThread(nullptr, 0, ThreadProc, this, 0, nullptr);
if (!m_thread)
{
CloseHandle(m_readyEvent);
throw std::runtime_error("STAThread: CreateThread failed");
}
// 等待线程就绪(m_pumpHwnd 已创建,CoInitialize 已完成)
WaitForSingleObject(m_readyEvent, INFINITE);
CloseHandle(m_readyEvent);
m_readyEvent = nullptr;
}
STAThread::~STAThread()
{
Shutdown(5000);
if (m_thread)
{
CloseHandle(m_thread);
m_thread = nullptr;
}
}
// ── 公开 API ──────────────────────────────────────────────────────────────
void STAThread::Post(std::function<void()> work)
{
{
std::lock_guard<std::mutex> lk(m_queueMutex);
m_queue.push(std::move(work));
}
if (m_pumpHwnd)
PostMessageW(m_pumpHwnd, WM_EXECUTE, 0, 0);
}
void STAThread::RequestQuit()
{
// 幂等:仅第一次调用真正发送 WM_QUIT
if (m_shutdown.exchange(true)) return;
if (m_pumpHwnd)
PostMessageW(m_pumpHwnd, WM_QUIT, 0, 0);
}
void STAThread::Shutdown(DWORD timeoutMs)
{
RequestQuit();
if (m_thread)
WaitForSingleObject(m_thread, timeoutMs);
}
// ── 线程实现 ──────────────────────────────────────────────────────────────
DWORD WINAPI STAThread::ThreadProc(LPVOID param)
{
reinterpret_cast<STAThread*>(param)->Run();
return 0;
}
void STAThread::Run()
{
// STA COM 初始化(必须在 GetMessage 循环前完成)
HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
if (FAILED(hr) && hr != S_FALSE)
{
SetEvent(m_readyEvent);
return;
}
// 注册 message-only 窗口类(忽略"已注册"错误,重用已有注册即可)
HINSTANCE inst = GetModuleHandleW(nullptr);
WNDCLASSEXW wc = {};
wc.cbSize = sizeof(wc);
wc.lpfnWndProc = PumpWndProc;
wc.hInstance = inst;
wc.lpszClassName = kWndClass;
RegisterClassExW(&wc); // 忽略返回值;若已注册直接复用
// 创建 message-only 窗口(HWND_MESSAGE = (HWND)-3)
m_pumpHwnd = CreateWindowExW(
0, kWndClass, L"", 0,
0, 0, 0, 0,
HWND_MESSAGE, nullptr, inst, nullptr);
// 通知构造函数:线程已就绪
SetEvent(m_readyEvent);
if (!m_pumpHwnd)
{
CoUninitialize();
return;
}
// 处理构造函数返回前就入队的工作项
DrainQueue();
// Win32 消息泵——同时服务用户工作队列和 WebView2 内部 COM 回调
MSG msg;
while (GetMessageW(&msg, nullptr, 0, 0) > 0)
{
if (msg.message == WM_EXECUTE)
{
DrainQueue();
continue; // 线程消息无需 Translate/Dispatch
}
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
// 最后清理一次队列(处理 Shutdown 前投递的工作)
DrainQueue();
DestroyWindow(m_pumpHwnd);
m_pumpHwnd = nullptr;
UnregisterClassW(kWndClass, inst);
CoUninitialize();
}
void STAThread::DrainQueue()
{
for (;;)
{
std::function<void()> work;
{
std::lock_guard<std::mutex> lk(m_queueMutex);
if (m_queue.empty()) break;
work = std::move(m_queue.front());
m_queue.pop();
}
// 在 STA 线程上执行,异常不向上传播(避免崩溃消息泵)
try { work(); }
catch (...) {}
}
}
@@ -0,0 +1,43 @@
fileFormatVersion: 2
guid: 2ac276fea4b65e041940e59b8ee03a84
PluginImporter:
externalObjects: {}
serializedVersion: 3
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
Any:
enabled: 0
settings:
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Editor:
enabled: 1
settings:
CPU: x86_64
DefaultValueInitialized: true
OS: Windows
Linux64:
enabled: 0
settings:
CPU: None
Win:
enabled: 0
settings:
CPU: None
Win64:
enabled: 0
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,48 @@
#pragma once
#include <windows.h>
#include <atomic>
#include <functional>
#include <queue>
#include <mutex>
#include <string>
/// STA 线程 + Win32 消息泵 + 工作队列。
/// 所有 WebView2 COM 对象必须在同一 STA 线程上创建和访问。
class STAThread {
public:
static constexpr UINT WM_EXECUTE = WM_APP + 1;
/// 启动 STA 线程,阻塞直到线程就绪(message-only 窗口已创建)。
explicit STAThread(std::wstring name = L"WebView2-STA");
~STAThread();
STAThread(const STAThread&) = delete;
STAThread& operator=(const STAThread&) = delete;
/// 在 STA 线程异步执行 work(线程安全,可从任意线程调用)。
void Post(std::function<void()> work);
/// 仅向 STA 线程投递 WM_QUIT,不等待退出(线程安全,幂等)。
/// 调用方负责后续等待 ThreadHandle()。
void RequestQuit();
/// 停止 STA 线程(发送 WM_QUIT)并等待退出(最多 timeoutMs ms)。
/// 等待使用 WaitForSingleObject——若调用方需要在等待期间 pump 消息,请使用 RequestQuit + ThreadHandle 自行 MsgWaitForMultipleObjects。
void Shutdown(DWORD timeoutMs = 5000);
HWND PumpHWND() const { return m_pumpHwnd; }
HANDLE ThreadHandle() const { return m_thread; }
private:
static DWORD WINAPI ThreadProc(LPVOID param);
void Run();
void DrainQueue();
std::wstring m_name;
HANDLE m_thread = nullptr;
HANDLE m_readyEvent = nullptr; // 线程就绪信号
HWND m_pumpHwnd = nullptr; // message-only 窗口
std::mutex m_queueMutex;
std::queue<std::function<void()>> m_queue;
std::atomic<bool> m_shutdown { false };
};
@@ -0,0 +1,43 @@
fileFormatVersion: 2
guid: e265ceebc7185524f80432008c1508e8
PluginImporter:
externalObjects: {}
serializedVersion: 3
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
Any:
enabled: 0
settings:
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Editor:
enabled: 1
settings:
CPU: x86_64
DefaultValueInitialized: true
OS: Windows
Linux64:
enabled: 0
settings:
CPU: None
Win:
enabled: 0
settings:
CPU: None
Win64:
enabled: 0
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,52 @@
#include "WndProcHook.h"
WndProcHook::WndProcHook(HWND hwnd,
std::function<void(int w, int h)> onResize,
std::function<void()> onDestroy)
: m_hwnd(hwnd)
, m_onResize(std::move(onResize))
, m_onDestroy(std::move(onDestroy))
{
if (!hwnd || !IsWindow(hwnd)) return;
// data 指向 this,SubclassProc 通过 data 拿回实例指针
m_installed = SetWindowSubclass(hwnd, SubclassProc, kSubclassId,
reinterpret_cast<DWORD_PTR>(this));
}
WndProcHook::~WndProcHook()
{
if (m_installed && m_hwnd && IsWindow(m_hwnd))
RemoveWindowSubclass(m_hwnd, SubclassProc, kSubclassId);
}
LRESULT CALLBACK WndProcHook::SubclassProc(
HWND hwnd, UINT msg, WPARAM w, LPARAM l,
UINT_PTR subclassId, DWORD_PTR data)
{
auto* self = reinterpret_cast<WndProcHook*>(data);
switch (msg)
{
case WM_SIZE:
{
int width = LOWORD(l);
int height = HIWORD(l);
if (self && self->m_onResize && (width > 0 || height > 0))
self->m_onResize(width, height);
break;
}
case WM_DESTROY:
// 先通知上层,再自动卸载 hook(窗口即将销毁,不需要再保留)
if (self)
{
self->m_installed = false; // 防止析构函数重复 Remove
if (self->m_onDestroy)
self->m_onDestroy();
}
RemoveWindowSubclass(hwnd, SubclassProc, subclassId);
break;
}
return DefSubclassProc(hwnd, msg, w, l);
}
@@ -0,0 +1,43 @@
fileFormatVersion: 2
guid: e747acbe12284b945801da61a4cf8a02
PluginImporter:
externalObjects: {}
serializedVersion: 3
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
Any:
enabled: 0
settings:
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Editor:
enabled: 1
settings:
CPU: x86_64
DefaultValueInitialized: true
OS: Windows
Linux64:
enabled: 0
settings:
CPU: None
Win:
enabled: 0
settings:
CPU: None
Win64:
enabled: 0
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,30 @@
#pragma once
#include <windows.h>
#include <commctrl.h>
#include <functional>
/// 使用 SetWindowSubclass 对父 HWND 进行子类化,
/// 拦截 WM_SIZE(同步 WebView2 大小)和 WM_DESTROY(通知 C# 重建)。
/// SetWindowSubclass 比 SetWindowLongPtr 更安全,支持多层 hook 共存。
class WndProcHook {
public:
WndProcHook(HWND hwnd,
std::function<void(int w, int h)> onResize,
std::function<void()> onDestroy);
~WndProcHook(); // 自动 Unhook
WndProcHook(const WndProcHook&) = delete;
WndProcHook& operator=(const WndProcHook&) = delete;
private:
static LRESULT CALLBACK SubclassProc(
HWND hwnd, UINT msg, WPARAM w, LPARAM l,
UINT_PTR subclassId, DWORD_PTR data);
HWND m_hwnd;
bool m_installed = false;
std::function<void(int w, int h)> m_onResize;
std::function<void()> m_onDestroy;
static constexpr UINT_PTR kSubclassId = 0xC0DE1234;
};
@@ -0,0 +1,43 @@
fileFormatVersion: 2
guid: ebcae0884dd4a174ba4583a1f6fb25bd
PluginImporter:
externalObjects: {}
serializedVersion: 3
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
Any:
enabled: 0
settings:
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Editor:
enabled: 1
settings:
CPU: x86_64
DefaultValueInitialized: true
OS: Windows
Linux64:
enabled: 0
settings:
CPU: None
Win:
enabled: 0
settings:
CPU: None
Win64:
enabled: 0
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant: