2 Commits
Author SHA1 Message Date
valknarandClaude Sonnet 5 6ff116ba02 Bump version to 0.3.2 for the MsgBox fix
CI / Build wasm engine (push) Successful in 10m55s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 19:31:05 +02:00
valknarandClaude Sonnet 5 1d7c4e7b7d 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 <noreply@anthropic.com>
2026-08-23 19:30:25 +02:00
4 changed files with 127 additions and 3 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "@valknar/vpinball-wasm", "name": "@valknar/vpinball-wasm",
"version": "0.3.1", "version": "0.3.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@valknar/vpinball-wasm", "name": "@valknar/vpinball-wasm",
"version": "0.3.1", "version": "0.3.2",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"devDependencies": { "devDependencies": {
"typescript": "^5.6.0" "typescript": "^5.6.0"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@valknar/vpinball-wasm", "name": "@valknar/vpinball-wasm",
"version": "0.3.1", "version": "0.3.2",
"description": "Visual Pinball's engine compiled to WebAssembly - real .vpx tables, real VBScript, WebGL2 rendering, in the browser", "description": "Visual Pinball's engine compiled to WebAssembly - real .vpx tables, real VBScript, WebGL2 rendering, in the browser",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"type": "module", "type": "module",
@@ -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 <locale.h>
#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
@@ -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 <libwinevbs/libwinevbs.h>
#endif
+#ifdef __EMSCRIPTEN__
+#include <emscripten.h>
+#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