From 1d7c4e7b7db4186d6f33a702ac873d603b9d9087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Sun, 23 Aug 2026 19:30:25 +0200 Subject: [PATCH] Implement real MsgBox() for VBScript table scripts (browser confirm/alert) libwinevbs's __LIBWINEVBS__ build of Global_MsgBox() only logged the prompt text and never set a return value, since it has no host UI to show a dialog through. Any table script that branches on MsgBox(...) = vbYes (a common pattern - e.g. core.vbs's own trough ball-count dialog) always took the "no" branch, since the result stayed at its zero-initialized default. Adds a msgbox callback to libwinevbs_callbacks_t, wired on the vpinball-wasm side to a real window.confirm()/alert() call. Both are synchronous browser APIs that block JS execution and return a value immediately, matching what VBScript's MsgBox() needs (its result is used by the calling statement right away) - the only way to answer it asynchronously would require Asyncify or a busy-wait poll loop, which this project has deliberately avoided elsewhere. Confirmed fixed by hands-on testing: a table's ball-count confirm dialog now correctly returns Yes/No based on the user's click, instead of always silently taking the "no" branch. Co-Authored-By: Claude Sonnet 5 --- patches/libwinevbs/0002-msgbox-callback.patch | 64 +++++++++++++++++++ .../0007-emscripten-msgbox-dialog.patch | 60 +++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 patches/libwinevbs/0002-msgbox-callback.patch create mode 100644 patches/vpinball/0007-emscripten-msgbox-dialog.patch diff --git a/patches/libwinevbs/0002-msgbox-callback.patch b/patches/libwinevbs/0002-msgbox-callback.patch new file mode 100644 index 0000000..56ceb94 --- /dev/null +++ b/patches/libwinevbs/0002-msgbox-callback.patch @@ -0,0 +1,64 @@ +diff --git a/include/libwinevbs.h b/include/libwinevbs.h +index 3ba4df9..6cac7df 100644 +--- a/include/libwinevbs.h ++++ b/include/libwinevbs.h +@@ -52,6 +52,14 @@ LIBWINEVBS_API const char* libwinevbs_hresult_name(HRESULT hr); + typedef struct { + void (*log)(libwinevbs_log_level_t level, const char* format, va_list args); + HRESULT (*create_object)(const WCHAR* progid, IClassFactory* cf, IUnknown** obj); ++ /* Answers a VBScript MsgBox() call. Must return synchronously (its result ++ is used by the calling script statement immediately) - e.g. a real, ++ blocking confirm()/alert() call under Emscripten. type is the raw ++ VBScript "buttons" argument (MB_OK=0, MB_OKCANCEL=1, MB_YESNO=4, ...); ++ title may be NULL. Return the matching button id (IDOK=1, IDCANCEL=2, ++ IDABORT=3, IDRETRY=4, IDIGNORE=5, IDYES=6, IDNO=7), matching vbOK..vbNo. ++ If unset, MsgBox always answers IDOK/vbOK without asking anything. */ ++ int (*msgbox)(const char* prompt, int type, const char* title); + } libwinevbs_callbacks_t; + + LIBWINEVBS_API void libwinevbs_init(const libwinevbs_callbacks_t* callbacks); +diff --git a/src/libwinevbs.c b/src/libwinevbs.c +index 7b3d6f0..91091e4 100644 +--- a/src/libwinevbs.c ++++ b/src/libwinevbs.c +@@ -32,6 +32,14 @@ HRESULT libwinevbs_create_object(const WCHAR* progid, IClassFactory* cf, IUnknow + return CLASS_E_CLASSNOTAVAILABLE; + } + ++int libwinevbs_msgbox(const char* prompt, int type, const char* title) ++{ ++ if (g_callbacks.msgbox) ++ return g_callbacks.msgbox(prompt, type, title); ++ ++ return 1; /* IDOK/vbOK - no host callback registered, so just proceed */ ++} ++ + const char* libwinevbs_hresult_name(HRESULT hr) + { + switch (hr) { +diff --git a/wine/dlls/vbscript/global.c b/wine/dlls/vbscript/global.c +index 0e0e144..351f967 100644 +--- a/wine/dlls/vbscript/global.c ++++ b/wine/dlls/vbscript/global.c +@@ -33,6 +33,7 @@ + #include + #include "scrrun_private.h" + HRESULT libwinevbs_create_object(const WCHAR *progid, IClassFactory* cf, IUnknown** obj); ++int libwinevbs_msgbox(const char *prompt, int type, const char *title); + extern HRESULT WINAPI WshShellFactory_CreateInstance(IClassFactory*,IUnknown*,REFIID,void**); + #endif + +@@ -2834,9 +2835,12 @@ static HRESULT Global_MsgBox(BuiltinDisp *This, VARIANT *args, unsigned args_cnt + hres = show_msgbox(This->ctx, prompt, type, title, res); + #else + if(SUCCEEDED(hres)) { +- char buf[2048]; ++ char buf[2048], title_buf[256] = {0}; + WideCharToMultiByte(CP_ACP, 0, prompt, -1, buf, sizeof(buf) - 1, NULL, NULL); ++ if (title) ++ WideCharToMultiByte(CP_ACP, 0, title, -1, title_buf, sizeof(title_buf) - 1, NULL, NULL); + libwinevbs_log(LIBWINEVBS_LOG_INFO, "vbscript: MsgBox prompt=%s", buf); ++ hres = return_short(res, libwinevbs_msgbox(buf, type, title ? title_buf : NULL)); + } + #endif + diff --git a/patches/vpinball/0007-emscripten-msgbox-dialog.patch b/patches/vpinball/0007-emscripten-msgbox-dialog.patch new file mode 100644 index 0000000..c7dcdec --- /dev/null +++ b/patches/vpinball/0007-emscripten-msgbox-dialog.patch @@ -0,0 +1,60 @@ +diff --git a/src/core/VPApp.cpp b/src/core/VPApp.cpp +index 7175dd2..f05a324 100644 +--- a/src/core/VPApp.cpp ++++ b/src/core/VPApp.cpp +@@ -36,6 +36,10 @@ + #include + #endif + ++#ifdef __EMSCRIPTEN__ ++#include ++#endif ++ + #include "parts/ball.h" + #include "parts/timer.h" + #include "parts/flipper.h" +@@ -273,6 +277,34 @@ int VPApp::GetLogicalNumberOfProcessors() const + return m_logicalNumberOfProcessors; + } + ++#ifdef __EMSCRIPTEN__ ++// VBScript's MsgBox() must return synchronously (its VARIANT result is used ++// right after the call, in the same script statement) - window.confirm()/ ++// alert() are the only browser APIs that block JS execution and return a ++// value synchronously, so they're used here directly rather than routing ++// through any async/JS-callback mechanism the host page might otherwise want ++// to customize the look of (which would require Asyncify to suspend the ++// call, since single-threaded builds have no other way to block for it). ++// type is the raw VBScript "buttons" argument (MB_OK=0, MB_OKCANCEL=1, ++// MB_ABORTRETRYIGNORE=2, MB_YESNOCANCEL=3, MB_YESNO=4, MB_RETRYCANCEL=5). ++// The returned id matches vbOK(1)/vbCancel(2)/vbYes(6)/vbNo(7). ++static int EmscriptenMsgBox(const char* prompt, int type, const char* title) ++{ ++ const string message = (title && title[0]) ? (string(title) + "\n\n" + prompt) : string(prompt); ++ switch (type & 0x0F) ++ { ++ case 1: // MB_OKCANCEL ++ return EM_ASM_INT({ return confirm(UTF8ToString($0)) ? 1 : 0; }, message.c_str()) ? 1 /* IDOK */ : 2 /* IDCANCEL */; ++ case 3: // MB_YESNOCANCEL - Cancel isn't distinguishable from No via confirm(), best effort ++ case 4: // MB_YESNO ++ return EM_ASM_INT({ return confirm(UTF8ToString($0)) ? 1 : 0; }, message.c_str()) ? 6 /* IDYES */ : 7 /* IDNO */; ++ default: ++ EM_ASM_({ alert(UTF8ToString($0)); }, message.c_str()); ++ return 1; // IDOK ++ } ++} ++#endif ++ + void VPApp::InitInstance() + { + std::filesystem::path iniFileName = m_commandLineCustomSettingsFileName; +@@ -325,6 +357,9 @@ void VPApp::InitInstance() + delete[] buffer; + } + }; ++#ifdef __EMSCRIPTEN__ ++ callbacks.msgbox = &EmscriptenMsgBox; ++#endif + libwinevbs_init(&callbacks); + #endif +