diff --git a/src/client/launcher/html/doc_host_ui_handler.cpp b/src/client/launcher/html/doc_host_ui_handler.cpp
new file mode 100644
index 00000000..2ec43a22
--- /dev/null
+++ b/src/client/launcher/html/doc_host_ui_handler.cpp
@@ -0,0 +1,115 @@
+#include
+#include "html_frame.hpp"
+
+doc_host_ui_handler::doc_host_ui_handler(html_frame* frame): frame_(frame)
+{
+}
+
+HRESULT doc_host_ui_handler::QueryInterface(REFIID riid, LPVOID* ppvObj)
+{
+ auto client_site = this->frame_->get_client_site();
+ if (client_site)
+ {
+ return client_site->QueryInterface(riid, ppvObj);
+ }
+
+ return E_NOINTERFACE;
+}
+
+ULONG doc_host_ui_handler::AddRef()
+{
+ return 1;
+}
+
+ULONG doc_host_ui_handler::Release()
+{
+ return 1;
+}
+
+HRESULT doc_host_ui_handler::ShowContextMenu(DWORD /*dwID*/, POINT* /*ppt*/, IUnknown* /*pcmdtReserved*/,
+ IDispatch* /*pdispReserved*/)
+{
+ return S_OK;
+}
+
+HRESULT doc_host_ui_handler::ShowUI(DWORD /*dwID*/, IOleInPlaceActiveObject* /*pActiveObject*/,
+ IOleCommandTarget* /*pCommandTarget*/,
+ IOleInPlaceFrame* /*pFrame*/, IOleInPlaceUIWindow* /*pDoc*/)
+{
+ return S_OK;
+}
+
+HRESULT doc_host_ui_handler::HideUI()
+{
+ return S_OK;
+}
+
+HRESULT doc_host_ui_handler::UpdateUI()
+{
+ return S_OK;
+}
+
+HRESULT doc_host_ui_handler::EnableModeless(BOOL /*fEnable*/)
+{
+ return S_OK;
+}
+
+HRESULT doc_host_ui_handler::OnDocWindowActivate(BOOL /*fActivate*/)
+{
+ return S_OK;
+}
+
+HRESULT doc_host_ui_handler::OnFrameWindowActivate(BOOL /*fActivate*/)
+{
+ return S_OK;
+}
+
+HRESULT doc_host_ui_handler::ResizeBorder(LPCRECT /*prcBorder*/, IOleInPlaceUIWindow* /*pUIWindow*/,
+ BOOL /*fRameWindow*/)
+{
+ return S_OK;
+}
+
+HRESULT doc_host_ui_handler::TranslateAcceleratorA(LPMSG /*lpMsg*/, const GUID* pguidCmdGroup, DWORD /*nCmdID*/)
+{
+ pguidCmdGroup = nullptr;
+ return S_FALSE;
+}
+
+HRESULT doc_host_ui_handler::GetOptionKeyPath(LPOLESTR* /*pchKey*/, DWORD /*dw*/)
+{
+ return S_FALSE;
+}
+
+HRESULT doc_host_ui_handler::GetDropTarget(IDropTarget* /*pDropTarget*/, IDropTarget** /*ppDropTarget*/)
+{
+ return S_FALSE;
+}
+
+HRESULT doc_host_ui_handler::GetExternal(IDispatch** ppDispatch)
+{
+ *ppDispatch = this->frame_->get_html_dispatch();
+ return (*ppDispatch) ? S_OK : S_FALSE;
+}
+
+HRESULT doc_host_ui_handler::FilterDataObject(IDataObject* /*pDO*/, IDataObject** ppDORet)
+{
+ *ppDORet = nullptr;
+ return S_FALSE;
+}
+
+HRESULT STDMETHODCALLTYPE doc_host_ui_handler::TranslateUrl(DWORD /*dwTranslate*/, OLECHAR __RPC_FAR* /*pchURLIn*/,
+ OLECHAR __RPC_FAR* __RPC_FAR* ppchURLOut)
+{
+ *ppchURLOut = nullptr;
+ return S_FALSE;
+}
+
+HRESULT doc_host_ui_handler::GetHostInfo(DOCHOSTUIINFO __RPC_FAR * pInfo)
+{
+ pInfo->cbSize = sizeof(DOCHOSTUIINFO);
+ pInfo->dwFlags = DOCHOSTUIFLAG_NO3DBORDER | DOCHOSTUIFLAG_DPI_AWARE /*| DOCHOSTUIFLAG_SCROLL_NO*/;
+ pInfo->dwDoubleClick = DOCHOSTUIDBLCLK_DEFAULT;
+
+ return S_OK;
+}
diff --git a/src/client/launcher/html/doc_host_ui_handler.hpp b/src/client/launcher/html/doc_host_ui_handler.hpp
new file mode 100644
index 00000000..0b538b41
--- /dev/null
+++ b/src/client/launcher/html/doc_host_ui_handler.hpp
@@ -0,0 +1,47 @@
+#pragma once
+
+class html_frame;
+
+class doc_host_ui_handler final : public IDocHostUIHandler
+{
+public:
+ doc_host_ui_handler(html_frame* frame);
+ virtual ~doc_host_ui_handler() = default;
+
+private:
+ html_frame* frame_;
+
+public: // IDocHostUIHandler interface
+ HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID* ppvObj) override;
+ ULONG STDMETHODCALLTYPE AddRef() override;
+ ULONG STDMETHODCALLTYPE Release() override;
+ HRESULT STDMETHODCALLTYPE ShowContextMenu(
+ DWORD dwID,
+ POINT __RPC_FAR * ppt,
+ IUnknown __RPC_FAR * pcmdtReserved,
+ IDispatch __RPC_FAR * pdispReserved) override;
+ HRESULT STDMETHODCALLTYPE ShowUI(
+ DWORD dwID,
+ IOleInPlaceActiveObject __RPC_FAR * pActiveObject,
+ IOleCommandTarget __RPC_FAR * pCommandTarget,
+ IOleInPlaceFrame __RPC_FAR * pFrame,
+ IOleInPlaceUIWindow __RPC_FAR * pDoc) override;
+ HRESULT STDMETHODCALLTYPE GetHostInfo(DOCHOSTUIINFO __RPC_FAR * pInfo) override;
+ HRESULT STDMETHODCALLTYPE HideUI() override;
+ HRESULT STDMETHODCALLTYPE UpdateUI() override;
+ HRESULT STDMETHODCALLTYPE EnableModeless(BOOL fEnable) override;
+ HRESULT STDMETHODCALLTYPE OnDocWindowActivate(BOOL fActivate) override;
+ HRESULT STDMETHODCALLTYPE OnFrameWindowActivate(BOOL fActivate) override;
+ HRESULT STDMETHODCALLTYPE ResizeBorder(LPCRECT prcBorder, IOleInPlaceUIWindow __RPC_FAR * pUIWindow,
+ BOOL fRameWindow) override;
+ HRESULT STDMETHODCALLTYPE TranslateAccelerator(LPMSG lpMsg, const GUID __RPC_FAR * pguidCmdGroup, DWORD nCmdID)
+ override;
+ HRESULT STDMETHODCALLTYPE GetOptionKeyPath(LPOLESTR __RPC_FAR * pchKey, DWORD dw) override;
+ HRESULT STDMETHODCALLTYPE GetDropTarget(IDropTarget __RPC_FAR * pDropTarget,
+ IDropTarget __RPC_FAR *__RPC_FAR * ppDropTarget) override;
+ HRESULT STDMETHODCALLTYPE GetExternal(IDispatch __RPC_FAR *__RPC_FAR * ppDispatch) override;
+ HRESULT STDMETHODCALLTYPE TranslateUrl(DWORD dwTranslate, OLECHAR __RPC_FAR * pchURLIn,
+ OLECHAR __RPC_FAR *__RPC_FAR * ppchURLOut) override;
+ HRESULT STDMETHODCALLTYPE FilterDataObject(IDataObject __RPC_FAR * pDO, IDataObject __RPC_FAR *__RPC_FAR * ppDORet)
+ override;
+};
diff --git a/src/client/launcher/html/html_argument.cpp b/src/client/launcher/html/html_argument.cpp
new file mode 100644
index 00000000..181dc6e5
--- /dev/null
+++ b/src/client/launcher/html/html_argument.cpp
@@ -0,0 +1,48 @@
+#include
+#include "html_argument.hpp"
+
+html_argument::html_argument(VARIANT* val) : value_(val)
+{
+}
+
+bool html_argument::is_empty() const
+{
+ return this->value_ == nullptr || this->value_->vt == VT_EMPTY;
+}
+
+bool html_argument::is_string() const
+{
+ if (this->is_empty()) return false;
+ return this->value_->vt == VT_BSTR;
+}
+
+bool html_argument::is_number() const
+{
+ if (this->is_empty()) return false;
+ return this->value_->vt == VT_I4;
+}
+
+bool html_argument::is_bool() const
+{
+ if (this->is_empty()) return false;
+ return this->value_->vt == VT_BOOL;
+}
+
+std::string html_argument::get_string() const
+{
+ if (!this->is_string()) return {};
+ std::wstring wide_string(this->value_->bstrVal);
+ return std::string(wide_string.begin(), wide_string.end());
+}
+
+int html_argument::get_number() const
+{
+ if (!this->is_number()) return 0;
+ return this->value_->intVal;
+}
+
+bool html_argument::get_bool() const
+{
+ if (!this->is_bool()) return false;
+ return this->value_->boolVal != FALSE;
+}
diff --git a/src/client/launcher/html/html_argument.hpp b/src/client/launcher/html/html_argument.hpp
new file mode 100644
index 00000000..6e460e29
--- /dev/null
+++ b/src/client/launcher/html/html_argument.hpp
@@ -0,0 +1,20 @@
+#pragma once
+
+class html_argument final
+{
+public:
+ html_argument(VARIANT* val);
+
+ bool is_empty() const;
+
+ bool is_string() const;
+ bool is_number() const;
+ bool is_bool() const;
+
+ std::string get_string() const;
+ int get_number() const;
+ bool get_bool() const;
+
+private:
+ VARIANT* value_;
+};
diff --git a/src/client/launcher/html/html_dispatch.cpp b/src/client/launcher/html/html_dispatch.cpp
new file mode 100644
index 00000000..0cbea58d
--- /dev/null
+++ b/src/client/launcher/html/html_dispatch.cpp
@@ -0,0 +1,61 @@
+#include
+#include "html_frame.hpp"
+
+html_dispatch::html_dispatch(html_frame* frame) : frame_(frame)
+{
+}
+
+HRESULT html_dispatch::QueryInterface(const IID& riid, LPVOID* ppvObj)
+{
+ if (!memcmp(&riid, &IID_IUnknown, sizeof(GUID)) ||
+ !memcmp(&riid, &IID_IDispatch, sizeof(GUID)))
+ {
+ *ppvObj = this;
+ this->AddRef();
+ return S_OK;
+ }
+
+ *ppvObj = nullptr;
+ return E_NOINTERFACE;
+}
+
+ULONG html_dispatch::AddRef()
+{
+ return 1;
+}
+
+ULONG html_dispatch::Release()
+{
+ return 1;
+}
+
+HRESULT html_dispatch::GetTypeInfoCount(UINT* pctinfo)
+{
+ return S_FALSE;
+}
+
+HRESULT html_dispatch::GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo** ppTInfo)
+{
+ return S_FALSE;
+}
+
+HRESULT html_dispatch::GetIDsOfNames(const IID& riid, LPOLESTR* rgszNames, UINT cNames, LCID lcid, DISPID* rgDispId)
+{
+ for (unsigned int i = 0; i < cNames; ++i)
+ {
+ std::wstring wide_name(rgszNames[i]);
+ std::string name(wide_name.begin(), wide_name.end());
+
+ rgDispId[i] = this->frame_->get_callback_id(name);
+ }
+
+ return S_OK;
+}
+
+HRESULT html_dispatch::Invoke(DISPID dispIdMember, const IID& riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams,
+ VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr)
+{
+ html_frame::callback_params params(pDispParams, pVarResult);
+ this->frame_->invoke_callback(dispIdMember, ¶ms);
+ return S_OK;
+}
diff --git a/src/client/launcher/html/html_dispatch.hpp b/src/client/launcher/html/html_dispatch.hpp
new file mode 100644
index 00000000..869af7a8
--- /dev/null
+++ b/src/client/launcher/html/html_dispatch.hpp
@@ -0,0 +1,24 @@
+#pragma once
+
+class html_frame;
+
+class html_dispatch final : public IDispatch
+{
+public:
+ html_dispatch(html_frame* frame);
+ virtual ~html_dispatch() = default;
+
+private:
+ html_frame* frame_;
+
+public: // IDispatch interface
+ HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID FAR* ppvObj) override;
+ ULONG STDMETHODCALLTYPE AddRef() override;
+ ULONG STDMETHODCALLTYPE Release() override;
+ HRESULT STDMETHODCALLTYPE GetTypeInfoCount(UINT* pctinfo) override;
+ HRESULT STDMETHODCALLTYPE GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo** ppTInfo) override;
+ HRESULT STDMETHODCALLTYPE GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames, UINT cNames, LCID lcid, DISPID* rgDispId)
+ override;
+ HRESULT STDMETHODCALLTYPE Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams,
+ VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr) override;
+};
diff --git a/src/client/launcher/html/html_frame.cpp b/src/client/launcher/html/html_frame.cpp
new file mode 100644
index 00000000..0c5c992e
--- /dev/null
+++ b/src/client/launcher/html/html_frame.cpp
@@ -0,0 +1,372 @@
+#include
+#include "html_frame.hpp"
+#include "utils/nt.hpp"
+#include "utils/io.hpp"
+#include "utils/hook.hpp"
+
+std::atomic html_frame::frame_count_ = 0;
+
+namespace
+{
+ void* original_func{};
+ GUID browser_emulation_guid{ 0xac969931, 0x3566, 0x4b50, {0xae, 0x48, 0x71, 0xb9, 0x6a, 0x75, 0xc8, 0x79} };
+
+ int WINAPI co_internet_feature_value_internal_stub(const GUID* guid, uint32_t* result)
+ {
+ const auto res = static_cast(original_func)(guid, result);
+
+ if (IsEqualGUID(*guid, browser_emulation_guid))
+ {
+ *result = 11000;
+ return 0;
+ }
+
+ return res;
+ }
+
+ void patch_cached_browser_emulator(const utils::nt::library& urlmon)
+ {
+ std::string data{};
+ if (!utils::io::read_file(urlmon.get_path().generic_string(), &data))
+ {
+ return;
+ }
+
+ const utils::nt::library file_lib(reinterpret_cast(data.data()));
+
+ auto translate_file_offset_to_rva = [&](const size_t file_offset) -> size_t
+ {
+ const auto sections = file_lib.get_section_headers();
+ for (const auto* section : sections)
+ {
+ if (section->PointerToRawData <= file_offset && section->PointerToRawData + section->SizeOfRawData > file_offset)
+ {
+ const auto section_va = file_offset - section->PointerToRawData;
+ return section_va + section->VirtualAddress;
+ }
+ }
+
+ return 0;
+ };
+
+ const auto guid_pos = data.find(std::string(reinterpret_cast(&browser_emulation_guid), sizeof(browser_emulation_guid)));
+ if (guid_pos == std::string::npos)
+ {
+ return;
+ }
+
+ const auto guid_rva = translate_file_offset_to_rva(guid_pos);
+ const auto guid_va = reinterpret_cast(urlmon.get_ptr() + guid_rva);
+
+ if (!IsEqualGUID(*guid_va, browser_emulation_guid))
+ {
+ return;
+ }
+
+ const size_t unrelocated_guid_va = file_lib.get_optional_header()->ImageBase + guid_rva;
+ const auto guid_ptr_pos = data.find(std::string(reinterpret_cast(&unrelocated_guid_va), sizeof(unrelocated_guid_va)));
+ if (guid_ptr_pos == std::string::npos)
+ {
+ return;
+ }
+
+ const auto guid_ptr_rva = translate_file_offset_to_rva(guid_ptr_pos);
+ *reinterpret_cast(urlmon.get_ptr() + guid_ptr_rva) = guid_va;
+ }
+
+ void setup_ie_hooks()
+ {
+ static const auto _ = []
+ {
+ const auto urlmon = utils::nt::library::load("urlmon.dll"s);
+ const auto target = urlmon.get_iat_entry("iertutil.dll", MAKEINTRESOURCEA(700));
+
+ original_func = *target;
+ utils::hook::set(target, co_internet_feature_value_internal_stub);
+
+ patch_cached_browser_emulator(urlmon);
+
+ return 0;
+ }();
+ (void)_;
+ }
+}
+
+html_frame::callback_params::callback_params(DISPPARAMS* params, VARIANT* res) : result(res)
+{
+ for (auto i = params->cArgs; i > 0; --i)
+ {
+ auto param = ¶ms->rgvarg[i - 1];
+ this->arguments.emplace_back(param);
+ }
+}
+
+html_frame::html_frame() : in_place_frame_(this), in_place_site_(this), ui_handler_(this), client_site_(this),
+html_dispatch_(this)
+{
+ setup_ie_hooks();
+ if (frame_count_++ == 0 && OleInitialize(nullptr) != S_OK)
+ {
+ throw std::runtime_error("Unable to initialize the OLE library");
+ }
+
+ set_browser_feature("FEATURE_BROWSER_EMULATION", 11000);
+ set_browser_feature("FEATURE_GPU_RENDERING", 1);
+}
+
+html_frame::~html_frame()
+{
+ if (--frame_count_ <= 0)
+ {
+ frame_count_ = 0;
+ OleUninitialize();
+ }
+}
+
+void html_frame::object_deleter(IUnknown* object)
+{
+ if (object)
+ {
+ object->Release();
+ }
+}
+
+HWND html_frame::get_window() const
+{
+ return this->window_;
+}
+
+std::shared_ptr html_frame::get_browser_object() const
+{
+ return this->browser_object_;
+}
+
+ole_in_place_frame* html_frame::get_in_place_frame()
+{
+ return &this->in_place_frame_;
+}
+
+ole_in_place_site* html_frame::get_in_place_site()
+{
+ return &this->in_place_site_;
+}
+
+doc_host_ui_handler* html_frame::get_ui_handler()
+{
+ return &this->ui_handler_;
+}
+
+ole_client_site* html_frame::get_client_site()
+{
+ return &this->client_site_;
+}
+
+html_dispatch* html_frame::get_html_dispatch()
+{
+ return &this->html_dispatch_;
+}
+
+std::shared_ptr html_frame::get_web_browser() const
+{
+ if (!this->browser_object_) return {};
+
+ IWebBrowser2* web_browser = nullptr;
+ if (FAILED(this->browser_object_->QueryInterface(IID_IWebBrowser2, reinterpret_cast(&web_browser)))
+ || !web_browser)
+ return {};
+
+ return std::shared_ptr(web_browser, object_deleter);
+}
+
+std::shared_ptr html_frame::get_dispatch() const
+{
+ const auto web_browser = this->get_web_browser();
+ if (!web_browser) return {};
+
+ IDispatch* dispatch = nullptr;
+ if (FAILED(web_browser->get_Document(&dispatch)) || !dispatch) return {};
+
+ return std::shared_ptr(dispatch, object_deleter);
+}
+
+std::shared_ptr html_frame::get_document() const
+{
+ const auto dispatch = this->get_dispatch();
+ if (!dispatch) return {};
+
+ IHTMLDocument2* document = nullptr;
+ if (FAILED(dispatch->QueryInterface(IID_IHTMLDocument2, reinterpret_cast(&document)))
+ || !document)
+ return {};
+
+ return std::shared_ptr(document, object_deleter);
+}
+
+void html_frame::initialize(const HWND window)
+{
+ if (this->window_) return;
+ this->window_ = window;
+
+ this->create_browser();
+ this->initialize_browser();
+}
+
+void html_frame::create_browser()
+{
+ LPCLASSFACTORY class_factory = nullptr;
+ if (FAILED(
+ CoGetClassObject(CLSID_WebBrowser, CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER, nullptr, IID_IClassFactory,
+ reinterpret_cast(&class_factory))) || !class_factory)
+ {
+ throw std::runtime_error("Unable to get the class factory");
+ }
+
+ IOleObject* browser_object = nullptr;
+ class_factory->CreateInstance(nullptr, IID_IOleObject, reinterpret_cast(&browser_object));
+ class_factory->Release();
+
+ if (!browser_object)
+ {
+ throw std::runtime_error("Unable to create browser object");
+ }
+
+ this->browser_object_ = std::shared_ptr(browser_object, [](IOleObject* browser_object)
+ {
+ if (browser_object)
+ {
+ browser_object->Close(OLECLOSE_NOSAVE);
+ object_deleter(browser_object);
+ }
+ });
+}
+
+void html_frame::initialize_browser()
+{
+ this->browser_object_->SetClientSite(this->get_client_site());
+ this->browser_object_->SetHostNames(L"Hostname", nullptr);
+
+ RECT rect;
+ GetClientRect(this->get_window(), &rect);
+ OleSetContainedObject(this->browser_object_.get(), TRUE);
+
+ this->browser_object_->DoVerb(OLEIVERB_SHOW, nullptr, this->get_client_site(), -1, this->get_window(), &rect);
+ this->resize(rect.right, rect.bottom);
+}
+
+bool html_frame::set_browser_feature(const std::string& feature, DWORD value)
+{
+ const auto registry_path = R"(SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\)" + feature;
+
+ HKEY key = nullptr;
+ if (RegCreateKeyA(HKEY_CURRENT_USER, registry_path.data(), &key) == ERROR_SUCCESS)
+ {
+ RegCloseKey(key);
+ }
+
+ key = nullptr;
+ if (RegOpenKeyExA(
+ HKEY_CURRENT_USER, registry_path.data(), 0,
+ KEY_ALL_ACCESS, &key) != ERROR_SUCCESS)
+ {
+ return false; // Error :(
+ }
+
+ const utils::nt::library self;
+ const auto name = self.get_name();
+
+ DWORD type{};
+ auto is_new = true;
+ if (RegQueryValueExA(key, name.data(), nullptr, &type, nullptr, nullptr) == ERROR_SUCCESS)
+ {
+ is_new = false;
+ }
+
+ RegSetValueExA(key, name.data(), 0, REG_DWORD, reinterpret_cast(&value), sizeof(value));
+ RegCloseKey(key);
+
+ return is_new;
+}
+
+void html_frame::resize(const DWORD width, const DWORD height) const
+{
+ auto web_browser = this->get_web_browser();
+ if (web_browser)
+ {
+ web_browser->put_Left(0);
+ web_browser->put_Top(0);
+ web_browser->put_Width(width);
+ web_browser->put_Height(height);
+ }
+}
+
+bool html_frame::load_url(const std::string& url) const
+{
+ auto web_browser = this->get_web_browser();
+ if (!web_browser) return false;
+
+ std::wstring wide_url(url.begin(), url.end());
+
+ VARIANT my_url;
+ VariantInit(&my_url);
+ my_url.vt = VT_BSTR;
+ my_url.bstrVal = SysAllocString(wide_url.data());
+
+ const auto _ = gsl::finally([&my_url]() { VariantClear(&my_url); });
+ if (!my_url.bstrVal) return false;
+
+ return SUCCEEDED(web_browser->Navigate2(&my_url, nullptr, nullptr, nullptr, nullptr));
+}
+
+bool html_frame::load_html(const std::string& html) const
+{
+ if (!this->load_url("about:blank")) return false;
+
+ const auto document = this->get_document();
+ if (!document) return false;
+
+ SAFEARRAYBOUND safe_array_bound = {1, 0};
+ auto safe_array = SafeArrayCreate(VT_VARIANT, 1, &safe_array_bound);
+ if (!safe_array) return false;
+
+ const auto _ = gsl::finally([safe_array]() { SafeArrayDestroy(safe_array); });
+
+ VARIANT* variant = nullptr;
+ if (FAILED(SafeArrayAccessData(safe_array, reinterpret_cast(&variant))) || !variant) return false;
+
+ std::wstring wide_html(html.begin(), html.end());
+
+ variant->vt = VT_BSTR;
+ variant->bstrVal = SysAllocString(wide_html.data());
+ if (!variant->bstrVal) return false;
+
+ document->write(safe_array);
+ document->close();
+
+ return true;
+}
+
+int html_frame::get_callback_id(const std::string& name)
+{
+ for (auto i = 0u; i < this->callbacks_.size(); ++i)
+ {
+ if (this->callbacks_[i].first == name)
+ {
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+void html_frame::invoke_callback(const int id, callback_params* params)
+{
+ if (id >= 0 && static_cast(id) < this->callbacks_.size())
+ {
+ this->callbacks_[id].second(params);
+ }
+}
+
+void html_frame::register_callback(const std::string& name, const std::function& callback)
+{
+ this->callbacks_.emplace_back(name, callback);
+}
diff --git a/src/client/launcher/html/html_frame.hpp b/src/client/launcher/html/html_frame.hpp
new file mode 100644
index 00000000..04d8108c
--- /dev/null
+++ b/src/client/launcher/html/html_frame.hpp
@@ -0,0 +1,67 @@
+#pragma once
+#include "ole_in_place_frame.hpp"
+#include "ole_in_place_site.hpp"
+#include "doc_host_ui_handler.hpp"
+#include "ole_client_site.hpp"
+#include "html_dispatch.hpp"
+#include "html_argument.hpp"
+
+class html_frame
+{
+public:
+ class callback_params final
+ {
+ public:
+ callback_params(DISPPARAMS* params, VARIANT* res);
+
+ std::vector arguments;
+ html_argument result;
+ };
+
+ html_frame();
+ virtual ~html_frame();
+
+ void initialize(HWND window);
+
+ void resize(DWORD width, DWORD height) const;
+ bool load_url(const std::string& url) const;
+ bool load_html(const std::string& html) const;
+
+ HWND get_window() const;
+
+ std::shared_ptr get_browser_object() const;
+ std::shared_ptr get_web_browser() const;
+ std::shared_ptr get_dispatch() const;
+ std::shared_ptr get_document() const;
+
+ ole_in_place_frame* get_in_place_frame();
+ ole_in_place_site* get_in_place_site();
+ doc_host_ui_handler* get_ui_handler();
+ ole_client_site* get_client_site();
+ html_dispatch* get_html_dispatch();
+
+ int get_callback_id(const std::string& name);
+ void invoke_callback(int id, callback_params* params);
+
+ void register_callback(const std::string& name, const std::function& callback);
+
+private:
+ HWND window_ = nullptr;
+ std::shared_ptr browser_object_;
+
+ ole_in_place_frame in_place_frame_;
+ ole_in_place_site in_place_site_;
+ doc_host_ui_handler ui_handler_;
+ ole_client_site client_site_;
+ html_dispatch html_dispatch_;
+
+ std::vector>> callbacks_;
+
+ void create_browser();
+ void initialize_browser();
+
+ static bool set_browser_feature(const std::string& feature, DWORD value);
+ static void object_deleter(IUnknown* object);
+
+ static std::atomic frame_count_;
+};
diff --git a/src/client/launcher/html/html_window.cpp b/src/client/launcher/html/html_window.cpp
new file mode 100644
index 00000000..bf01098c
--- /dev/null
+++ b/src/client/launcher/html/html_window.cpp
@@ -0,0 +1,29 @@
+#include
+#include "html_window.hpp"
+
+window* html_window::get_window()
+{
+ return this;
+}
+
+html_frame* html_window::get_html_frame()
+{
+ return this;
+}
+
+LRESULT html_window::processor(const UINT message, const WPARAM w_param, const LPARAM l_param)
+{
+ if (message == WM_SIZE)
+ {
+ this->resize(LOWORD(l_param), HIWORD(l_param));
+ return 0;
+ }
+
+ if (message == WM_CREATE)
+ {
+ this->initialize(*this);
+ return 0;
+ }
+
+ return window::processor(message, w_param, l_param);
+}
diff --git a/src/client/launcher/html/html_window.hpp b/src/client/launcher/html/html_window.hpp
new file mode 100644
index 00000000..5b9f25be
--- /dev/null
+++ b/src/client/launcher/html/html_window.hpp
@@ -0,0 +1,15 @@
+#pragma once
+#include "../window.hpp"
+#include "html_frame.hpp"
+
+class html_window final : public window, public html_frame
+{
+public:
+ ~html_window() = default;
+
+ window* get_window();
+ html_frame* get_html_frame();
+
+private:
+ LRESULT processor(UINT message, WPARAM w_param, LPARAM l_param) override;
+};
diff --git a/src/client/launcher/html/ole_client_site.cpp b/src/client/launcher/html/ole_client_site.cpp
new file mode 100644
index 00000000..6e3914ea
--- /dev/null
+++ b/src/client/launcher/html/ole_client_site.cpp
@@ -0,0 +1,77 @@
+#include
+#include "html_frame.hpp"
+
+ole_client_site::ole_client_site(html_frame* frame): frame_(frame)
+{
+}
+
+HRESULT ole_client_site::QueryInterface(REFIID riid, LPVOID* ppvObject)
+{
+ if (!memcmp(&riid, &IID_IUnknown, sizeof(GUID)) ||
+ !memcmp(&riid, &IID_IOleClientSite, sizeof(GUID)))
+ {
+ *ppvObject = this;
+ this->AddRef();
+ return S_OK;
+ }
+
+ if (!memcmp(&riid, &IID_IOleInPlaceSite, sizeof(GUID)))
+ {
+ auto in_place_site = this->frame_->get_in_place_site();
+ in_place_site->AddRef();
+ *ppvObject = in_place_site;
+ return S_OK;
+ }
+
+ if (!memcmp(&riid, &IID_IDocHostUIHandler, sizeof(GUID)))
+ {
+ auto ui_handler = this->frame_->get_ui_handler();
+ ui_handler->AddRef();
+ *ppvObject = ui_handler;
+ return S_OK;
+ }
+
+ *ppvObject = nullptr;
+ return E_NOINTERFACE;
+}
+
+ULONG ole_client_site::AddRef()
+{
+ return 1;
+}
+
+ULONG ole_client_site::Release()
+{
+ return 1;
+}
+
+HRESULT ole_client_site::SaveObject()
+{
+ return E_NOTIMPL;
+}
+
+HRESULT ole_client_site::GetMoniker(DWORD /*dwAssign*/, DWORD /*dwWhichMoniker*/, IMoniker** /*ppmk*/)
+{
+ return E_NOTIMPL;
+}
+
+HRESULT ole_client_site::GetContainer(LPOLECONTAINER* ppContainer)
+{
+ *ppContainer = nullptr;
+ return E_NOINTERFACE;
+}
+
+HRESULT ole_client_site::ShowObject()
+{
+ return NOERROR;
+}
+
+HRESULT ole_client_site::OnShowWindow(BOOL /*fShow*/)
+{
+ return E_NOTIMPL;
+}
+
+HRESULT ole_client_site::RequestNewObjectLayout()
+{
+ return E_NOTIMPL;
+}
diff --git a/src/client/launcher/html/ole_client_site.hpp b/src/client/launcher/html/ole_client_site.hpp
new file mode 100644
index 00000000..d0adc80d
--- /dev/null
+++ b/src/client/launcher/html/ole_client_site.hpp
@@ -0,0 +1,24 @@
+#pragma once
+
+class html_frame;
+
+class ole_client_site final : public IOleClientSite
+{
+public:
+ ole_client_site(html_frame* frame);
+ virtual ~ole_client_site() = default;
+
+private:
+ html_frame* frame_;
+
+public:
+ HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID* ppvObject) override;
+ ULONG STDMETHODCALLTYPE AddRef() override;
+ ULONG STDMETHODCALLTYPE Release() override;
+ HRESULT STDMETHODCALLTYPE SaveObject() override;
+ HRESULT STDMETHODCALLTYPE GetMoniker(DWORD dwAssign, DWORD dwWhichMoniker, IMoniker** ppmk) override;
+ HRESULT STDMETHODCALLTYPE GetContainer(LPOLECONTAINER FAR* ppContainer) override;
+ HRESULT STDMETHODCALLTYPE ShowObject() override;
+ HRESULT STDMETHODCALLTYPE OnShowWindow(BOOL fShow) override;
+ HRESULT STDMETHODCALLTYPE RequestNewObjectLayout() override;
+};
diff --git a/src/client/launcher/html/ole_in_place_frame.cpp b/src/client/launcher/html/ole_in_place_frame.cpp
new file mode 100644
index 00000000..172e5db0
--- /dev/null
+++ b/src/client/launcher/html/ole_in_place_frame.cpp
@@ -0,0 +1,82 @@
+#include
+#include "html_frame.hpp"
+
+ole_in_place_frame::ole_in_place_frame(html_frame* frame): frame_(frame)
+{
+}
+
+HRESULT ole_in_place_frame::QueryInterface(REFIID /*riid*/, LPVOID* /*ppvObj*/)
+{
+ return E_NOTIMPL;
+}
+
+ULONG ole_in_place_frame::AddRef()
+{
+ return 1;
+}
+
+ULONG ole_in_place_frame::Release()
+{
+ return 1;
+}
+
+HRESULT ole_in_place_frame::GetWindow(HWND* lphwnd)
+{
+ *lphwnd = this->frame_->get_window();
+ return S_OK;
+}
+
+HRESULT ole_in_place_frame::ContextSensitiveHelp(BOOL /*fEnterMode*/)
+{
+ return E_NOTIMPL;
+}
+
+HRESULT ole_in_place_frame::GetBorder(LPRECT /*lprectBorder*/)
+{
+ return E_NOTIMPL;
+}
+
+HRESULT ole_in_place_frame::RequestBorderSpace(LPCBORDERWIDTHS /*pborderwidths*/)
+{
+ return E_NOTIMPL;
+}
+
+HRESULT ole_in_place_frame::SetBorderSpace(LPCBORDERWIDTHS /*pborderwidths*/)
+{
+ return E_NOTIMPL;
+}
+
+HRESULT ole_in_place_frame::SetActiveObject(IOleInPlaceActiveObject* /*pActiveObject*/, LPCOLESTR /*pszObjName*/)
+{
+ return S_OK;
+}
+
+HRESULT ole_in_place_frame::InsertMenus(HMENU /*hmenuShared*/, LPOLEMENUGROUPWIDTHS /*lpMenuWidths*/)
+{
+ return E_NOTIMPL;
+}
+
+HRESULT ole_in_place_frame::SetMenu(HMENU /*hmenuShared*/, HOLEMENU /*holemenu*/, HWND /*hwndActiveObject*/)
+{
+ return S_OK;
+}
+
+HRESULT ole_in_place_frame::RemoveMenus(HMENU /*hmenuShared*/)
+{
+ return E_NOTIMPL;
+}
+
+HRESULT ole_in_place_frame::SetStatusText(LPCOLESTR /*pszStatusText*/)
+{
+ return S_OK;
+}
+
+HRESULT ole_in_place_frame::EnableModeless(BOOL /*fEnable*/)
+{
+ return S_OK;
+}
+
+HRESULT ole_in_place_frame::TranslateAcceleratorA(LPMSG /*lpmsg*/, WORD /*wID*/)
+{
+ return E_NOTIMPL;
+}
diff --git a/src/client/launcher/html/ole_in_place_frame.hpp b/src/client/launcher/html/ole_in_place_frame.hpp
new file mode 100644
index 00000000..4a39d7f3
--- /dev/null
+++ b/src/client/launcher/html/ole_in_place_frame.hpp
@@ -0,0 +1,30 @@
+#pragma once
+
+class html_frame;
+
+class ole_in_place_frame final : public IOleInPlaceFrame
+{
+public:
+ ole_in_place_frame(html_frame* frame);
+ virtual ~ole_in_place_frame() = default;
+
+private:
+ html_frame* frame_;
+
+public: // IOleInPlaceFrame interface
+ HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID FAR* ppvObj) override;
+ ULONG STDMETHODCALLTYPE AddRef() override;
+ ULONG STDMETHODCALLTYPE Release() override;
+ HRESULT STDMETHODCALLTYPE GetWindow(HWND FAR* lphwnd) override;
+ HRESULT STDMETHODCALLTYPE ContextSensitiveHelp(BOOL fEnterMode) override;
+ HRESULT STDMETHODCALLTYPE GetBorder(LPRECT lprectBorder) override;
+ HRESULT STDMETHODCALLTYPE RequestBorderSpace(LPCBORDERWIDTHS pborderwidths) override;
+ HRESULT STDMETHODCALLTYPE SetBorderSpace(LPCBORDERWIDTHS pborderwidths) override;
+ HRESULT STDMETHODCALLTYPE SetActiveObject(IOleInPlaceActiveObject* pActiveObject, LPCOLESTR pszObjName) override;
+ HRESULT STDMETHODCALLTYPE InsertMenus(HMENU hmenuShared, LPOLEMENUGROUPWIDTHS lpMenuWidths) override;
+ HRESULT STDMETHODCALLTYPE SetMenu(HMENU hmenuShared, HOLEMENU holemenu, HWND hwndActiveObject) override;
+ HRESULT STDMETHODCALLTYPE RemoveMenus(HMENU hmenuShared) override;
+ HRESULT STDMETHODCALLTYPE SetStatusText(LPCOLESTR pszStatusText) override;
+ HRESULT STDMETHODCALLTYPE EnableModeless(BOOL fEnable) override;
+ HRESULT STDMETHODCALLTYPE TranslateAccelerator(LPMSG lpmsg, WORD wID) override;
+};
diff --git a/src/client/launcher/html/ole_in_place_site.cpp b/src/client/launcher/html/ole_in_place_site.cpp
new file mode 100644
index 00000000..d6cd3d82
--- /dev/null
+++ b/src/client/launcher/html/ole_in_place_site.cpp
@@ -0,0 +1,105 @@
+#include
+#include "html_frame.hpp"
+
+ole_in_place_site::ole_in_place_site(html_frame* frame) : frame_(frame)
+{
+}
+
+HRESULT ole_in_place_site::QueryInterface(REFIID riid, LPVOID FAR* ppvObj)
+{
+ auto client_site = this->frame_->get_client_site();
+ if (client_site)
+ {
+ return client_site->QueryInterface(riid, ppvObj);
+ }
+
+ return E_NOINTERFACE;
+}
+
+ULONG ole_in_place_site::AddRef()
+{
+ return 1;
+}
+
+ULONG ole_in_place_site::Release()
+{
+ return 1;
+}
+
+HRESULT ole_in_place_site::GetWindow(HWND* lphwnd)
+{
+ *lphwnd = this->frame_->get_window();
+ return S_OK;
+}
+
+HRESULT ole_in_place_site::ContextSensitiveHelp(BOOL /*fEnterMode*/)
+{
+ return E_NOTIMPL;
+}
+
+HRESULT ole_in_place_site::CanInPlaceActivate()
+{
+ return S_OK;
+}
+
+HRESULT ole_in_place_site::OnInPlaceActivate()
+{
+ return S_OK;
+}
+
+HRESULT ole_in_place_site::OnUIActivate()
+{
+ return S_OK;
+}
+
+HRESULT ole_in_place_site::GetWindowContext(LPOLEINPLACEFRAME* lplpFrame, LPOLEINPLACEUIWINDOW* lplpDoc,
+ LPRECT /*lprcPosRect*/, LPRECT /*lprcClipRect*/,
+ LPOLEINPLACEFRAMEINFO lpFrameInfo)
+{
+ *lplpFrame = this->frame_->get_in_place_frame();
+ *lplpDoc = nullptr;
+
+ lpFrameInfo->fMDIApp = FALSE;
+ lpFrameInfo->hwndFrame = this->frame_->get_window();
+ lpFrameInfo->haccel = nullptr;
+ lpFrameInfo->cAccelEntries = 0;
+
+ return S_OK;
+}
+
+HRESULT ole_in_place_site::Scroll(SIZE /*scrollExtent*/)
+{
+ return E_NOTIMPL;
+}
+
+HRESULT ole_in_place_site::OnUIDeactivate(BOOL /*fUndoable*/)
+{
+ return S_OK;
+}
+
+HRESULT ole_in_place_site::OnInPlaceDeactivate()
+{
+ return S_OK;
+}
+
+HRESULT ole_in_place_site::DiscardUndoState()
+{
+ return E_NOTIMPL;
+}
+
+HRESULT ole_in_place_site::DeactivateAndUndo()
+{
+ return E_NOTIMPL;
+}
+
+HRESULT ole_in_place_site::OnPosRectChange(LPCRECT lprcPosRect)
+{
+ IOleInPlaceObject* in_place = nullptr;
+ if (!this->frame_->get_browser_object()->QueryInterface(IID_IOleInPlaceObject, reinterpret_cast(&in_place)))
+ {
+ in_place->SetObjectRects(lprcPosRect, lprcPosRect);
+ in_place->Release();
+ }
+
+ return S_OK;
+}
diff --git a/src/client/launcher/html/ole_in_place_site.hpp b/src/client/launcher/html/ole_in_place_site.hpp
new file mode 100644
index 00000000..3dad18cf
--- /dev/null
+++ b/src/client/launcher/html/ole_in_place_site.hpp
@@ -0,0 +1,32 @@
+#pragma once
+
+class html_frame;
+
+class ole_in_place_site final : public IOleInPlaceSite
+{
+public:
+ ole_in_place_site(html_frame* frame);
+ virtual ~ole_in_place_site() = default;
+
+private:
+ html_frame* frame_;
+
+public: // IOleInPlaceSite interface
+ HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID FAR* ppvObj) override;
+ ULONG STDMETHODCALLTYPE AddRef() override;
+ ULONG STDMETHODCALLTYPE Release() override;
+ HRESULT STDMETHODCALLTYPE GetWindow(HWND FAR* lphwnd) override;
+ HRESULT STDMETHODCALLTYPE ContextSensitiveHelp(BOOL fEnterMode) override;
+ HRESULT STDMETHODCALLTYPE CanInPlaceActivate() override;
+ HRESULT STDMETHODCALLTYPE OnInPlaceActivate() override;
+ HRESULT STDMETHODCALLTYPE OnUIActivate() override;
+ HRESULT STDMETHODCALLTYPE GetWindowContext(LPOLEINPLACEFRAME FAR* lplpFrame, LPOLEINPLACEUIWINDOW FAR* lplpDoc,
+ LPRECT lprcPosRect, LPRECT lprcClipRect,
+ LPOLEINPLACEFRAMEINFO lpFrameInfo) override;
+ HRESULT STDMETHODCALLTYPE Scroll(SIZE scrollExtent) override;
+ HRESULT STDMETHODCALLTYPE OnUIDeactivate(BOOL fUndoable) override;
+ HRESULT STDMETHODCALLTYPE OnInPlaceDeactivate() override;
+ HRESULT STDMETHODCALLTYPE DiscardUndoState() override;
+ HRESULT STDMETHODCALLTYPE DeactivateAndUndo() override;
+ HRESULT STDMETHODCALLTYPE OnPosRectChange(LPCRECT lprcPosRect) override;
+};
diff --git a/src/client/launcher/launcher.cpp b/src/client/launcher/launcher.cpp
new file mode 100644
index 00000000..ccd1baca
--- /dev/null
+++ b/src/client/launcher/launcher.cpp
@@ -0,0 +1,66 @@
+#include
+#include "launcher.hpp"
+
+#include
+
+launcher::launcher()
+{
+ this->create_main_menu();
+}
+
+void launcher::create_main_menu()
+{
+ this->main_window_.register_callback("openUrl", [](html_frame::callback_params* params)
+ {
+ if (params->arguments.empty()) return;
+
+ const auto param = params->arguments[0];
+ if (!param.is_string()) return;
+
+ const auto url = param.get_string();
+ ShellExecuteA(nullptr, "open", url.data(), nullptr, nullptr, SW_SHOWNORMAL);
+ });
+
+ this->main_window_.register_callback("selectMode", [this](html_frame::callback_params* params)
+ {
+ if (params->arguments.empty()) return;
+
+ const auto param = params->arguments[0];
+ if (!param.is_number()) return;
+
+ const auto number = static_cast(param.get_number());
+ this->select_mode(number);
+ });
+
+ this->main_window_.set_callback(
+ [](window* window, const UINT message, const WPARAM w_param, const LPARAM l_param) -> LRESULT
+ {
+ if (message == WM_CLOSE)
+ {
+ window::close_all();
+ }
+
+ return DefWindowProcA(*window, message, w_param, l_param);
+ });
+
+ this->main_window_.create("S2-Mod", 750, 420);
+ this->main_window_.load_html(load_content(MENU_MAIN));
+ this->main_window_.show();
+}
+
+launcher::mode launcher::run() const
+{
+ window::run();
+ return this->mode_;
+}
+
+void launcher::select_mode(const mode mode)
+{
+ this->mode_ = mode;
+ this->main_window_.close();
+}
+
+std::string launcher::load_content(const int res)
+{
+ return utils::nt::load_resource(res);
+}
diff --git a/src/client/launcher/launcher.hpp b/src/client/launcher/launcher.hpp
new file mode 100644
index 00000000..1808376c
--- /dev/null
+++ b/src/client/launcher/launcher.hpp
@@ -0,0 +1,32 @@
+#pragma once
+#include "html/html_window.hpp"
+
+class launcher final
+{
+public:
+ enum class mode
+ {
+ none,
+ singleplayer,
+ multiplayer,
+ server,
+ // Surrogates
+ survival,
+ zombies,
+ };
+
+ launcher();
+
+ mode run() const;
+
+private:
+ mode mode_ = mode::none;
+
+ html_window main_window_;
+
+ void select_mode(mode mode);
+
+ void create_main_menu();
+
+ static std::string load_content(int res);
+};
diff --git a/src/client/launcher/window.cpp b/src/client/launcher/window.cpp
new file mode 100644
index 00000000..eceaf84b
--- /dev/null
+++ b/src/client/launcher/window.cpp
@@ -0,0 +1,216 @@
+#include
+#include "window.hpp"
+
+#include
+
+#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
+#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
+#endif
+
+std::mutex window::mutex_;
+std::vector window::windows_;
+
+window::window()
+{
+ ZeroMemory(&this->wc_, sizeof(this->wc_));
+
+ this->classname_ = "window-base-" + std::to_string(time(nullptr));
+
+ this->wc_.cbSize = sizeof(this->wc_);
+ this->wc_.style = CS_HREDRAW | CS_VREDRAW;
+ this->wc_.lpfnWndProc = static_processor;
+ this->wc_.hInstance = GetModuleHandle(nullptr);
+ this->wc_.hCursor = LoadCursor(nullptr, IDC_ARROW);
+ this->wc_.hIcon = LoadIcon(this->wc_.hInstance, MAKEINTRESOURCE(102));
+ this->wc_.hIconSm = this->wc_.hIcon;
+ this->wc_.hbrBackground = HBRUSH(COLOR_WINDOW);
+ this->wc_.lpszClassName = this->classname_.data();
+ RegisterClassEx(&this->wc_);
+}
+
+void window::create(const std::string& title, const int width, const int height, const long flags)
+{
+ {
+ std::lock_guard _(mutex_);
+ windows_.push_back(this);
+ }
+
+ const auto x = (GetSystemMetrics(SM_CXSCREEN) - width) / 2;
+ const auto y = (GetSystemMetrics(SM_CYSCREEN) - height) / 2;
+
+ this->handle_ = CreateWindowExA(NULL, this->wc_.lpszClassName, title.data(), flags, x, y, width, height, nullptr,
+ nullptr, this->wc_.hInstance, this);
+
+ BOOL value = TRUE;
+ DwmSetWindowAttribute(this->handle_,
+ DWMWA_USE_IMMERSIVE_DARK_MODE, &value, sizeof(value));
+
+ SendMessageA(this->handle_, WM_DPICHANGED, 0, 0);
+}
+
+window::~window()
+{
+ this->close();
+ UnregisterClass(this->wc_.lpszClassName, this->wc_.hInstance);
+}
+
+void window::close()
+{
+ if (!this->handle_) return;
+
+ DestroyWindow(this->handle_);
+ this->handle_ = nullptr;
+}
+
+void window::run()
+{
+ MSG msg;
+ while (GetMessage(&msg, nullptr, 0, 0))
+ {
+ TranslateMessage(&msg);
+ DispatchMessage(&msg);
+ }
+}
+
+void window::close_all()
+{
+ std::unique_lock lock(mutex_);
+ auto window_list = windows_;
+ lock.unlock();
+
+ const auto current_thread_id = GetCurrentThreadId();
+ for (auto& window : window_list)
+ {
+ const auto thread_id = GetWindowThreadProcessId(*window, nullptr);
+
+ if (thread_id == current_thread_id)
+ {
+ window->close();
+ }
+ }
+}
+
+void window::remove_window(const window* window)
+{
+ std::lock_guard _(mutex_);
+
+ for (auto i = windows_.begin(); i != windows_.end(); ++i)
+ {
+ if (*i == window)
+ {
+ windows_.erase(i);
+ break;
+ }
+ }
+}
+
+int window::get_window_count()
+{
+ std::lock_guard _(mutex_);
+
+ auto count = 0;
+ const auto current_thread_id = GetCurrentThreadId();
+
+ for (const auto& window : windows_)
+ {
+ const auto thread_id = GetWindowThreadProcessId(*window, nullptr);
+
+ if (thread_id == current_thread_id)
+ {
+ ++count;
+ }
+ }
+
+ return count;
+}
+
+void window::show() const
+{
+ ShowWindow(this->handle_, SW_SHOW);
+ UpdateWindow(this->handle_);
+}
+
+void window::hide() const
+{
+ ShowWindow(this->handle_, SW_HIDE);
+ UpdateWindow(this->handle_);
+}
+
+void window::set_callback(const std::function& callback)
+{
+ this->callback_ = callback;
+}
+
+LRESULT window::processor(const UINT message, const WPARAM w_param, const LPARAM l_param)
+{
+ if (message == WM_DPICHANGED)
+ {
+ const utils::nt::library user32{"user32.dll"};
+ const auto get_dpi = user32 ? user32.get_proc("GetDpiForWindow") : nullptr;
+
+ if (get_dpi)
+ {
+ const auto dpi = get_dpi(*this);
+ if (dpi != this->last_dpi_)
+ {
+ RECT rect;
+ GetWindowRect(*this, &rect);
+
+ const auto scale = dpi * 1.0 / this->last_dpi_;
+ this->last_dpi_ = dpi;
+
+ const auto width = rect.right - rect.left;
+ const auto height = rect.bottom - rect.top;
+
+ MoveWindow(*this, rect.left, rect.top, int(width * scale), int(height * scale), TRUE);
+ }
+ }
+ }
+
+ if (message == WM_DESTROY)
+ {
+ remove_window(this);
+
+ if (get_window_count() == 0)
+ {
+ PostQuitMessage(0);
+ }
+
+ return TRUE;
+ }
+
+ if (message == WM_KILL_WINDOW)
+ {
+ DestroyWindow(*this);
+ return 0;
+ }
+
+ if (this->callback_)
+ {
+ return this->callback_(this, message, w_param, l_param);
+ }
+
+ return DefWindowProc(*this, message, w_param, l_param);
+}
+
+LRESULT CALLBACK window::static_processor(HWND hwnd, UINT message, WPARAM w_param, LPARAM l_param)
+{
+ if (message == WM_CREATE)
+ {
+ auto data = reinterpret_cast(l_param);
+ SetWindowLongPtrA(hwnd, GWLP_USERDATA, LONG_PTR(data->lpCreateParams));
+
+ static_cast(data->lpCreateParams)->handle_ = hwnd;
+ }
+
+ const auto self = reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA));
+ if (self) return self->processor(message, w_param, l_param);
+
+ return DefWindowProc(hwnd, message, w_param, l_param);
+}
+
+
+window::operator HWND() const
+{
+ return this->handle_;
+}
diff --git a/src/client/launcher/window.hpp b/src/client/launcher/window.hpp
new file mode 100644
index 00000000..3bf83b15
--- /dev/null
+++ b/src/client/launcher/window.hpp
@@ -0,0 +1,46 @@
+#pragma once
+#pragma comment (lib, "dwmapi.lib")
+#include
+
+#define WM_KILL_WINDOW (WM_USER+0)
+
+class window
+{
+public:
+ window();
+ virtual ~window();
+
+ void create(const std::string& title, int width, int height,
+ long flags = (WS_OVERLAPPEDWINDOW & ~(WS_THICKFRAME | WS_MAXIMIZEBOX)));
+
+ void close();
+
+ void show() const;
+ void hide() const;
+
+ void set_callback(const std::function& callback);
+
+ operator HWND() const;
+
+ static void run();
+ static void close_all();
+
+protected:
+ virtual LRESULT processor(UINT message, WPARAM w_param, LPARAM l_param);
+
+private:
+ uint32_t last_dpi_ = 96;
+
+ WNDCLASSEX wc_{};
+ HWND handle_ = nullptr;
+ std::string classname_;
+ std::function callback_;
+
+ static LRESULT CALLBACK static_processor(HWND hwnd, UINT message, WPARAM w_param, LPARAM l_param);
+
+ static std::mutex mutex_;
+ static std::vector windows_;
+
+ static void remove_window(const window* window);
+ static int get_window_count();
+};