From d4d359aceade22028d170a9418484600ad128321 Mon Sep 17 00:00:00 2001 From: Alex Tiernan-Berry Date: Fri, 6 Feb 2026 02:21:20 +0000 Subject: [PATCH] feat(web): add Emscripten/WASM build infrastructure Adds the platform layer for building whoa as a WebAssembly application: Working: - CMake configuration for WHOA_SYSTEM_WEB with pthreads and ASYNCIFY - Web entry point and HTML shell template - Event loop adapted for emscripten_set_main_loop callback model - WebSocket-based networking (WowConnection over JS WebSocket API) - Sound system stubs (audio not yet implemented) - FetchFS for async file loading from web server - Freetype fixes for WASM compatibility (type mismatches) - Input handling for web canvas Missing (in separate commits): - WebGPU graphics backend (CGxDeviceWebGPU) - WGSL shaders - API selection in Device.cpp --- .gitignore | 2 +- CMakeLists.txt | 8 + src/app/CMakeLists.txt | 25 + src/app/web/Whoa.cpp | 55 ++ src/app/web/shell.html | 144 ++++ src/async/AsyncFileRead.cpp | 3 + src/client/CMakeLists.txt | 7 + src/client/ClientServices.cpp | 2 +- src/client/gui/web/OsGui.cpp | 36 + src/event/CMakeLists.txt | 7 + src/event/Event.cpp | 8 + src/event/Event.hpp | 4 + src/event/Input.cpp | 2 + src/event/Scheduler.cpp | 16 + src/event/web/Input.cpp | 288 ++++++++ src/gx/CGxDevice.cpp | 13 +- src/gx/Texture.cpp | 13 +- src/gx/Texture.hpp | 2 +- src/gx/Window.hpp | 4 +- src/gx/buffer/CGxPool.hpp | 1 + src/gx/texture/CGxTex.cpp | 8 + src/gx/texture/CGxTex.hpp | 2 + src/net/CMakeLists.txt | 18 + src/net/connection/WowConnection.hpp | 4 + src/net/connection/WowConnectionResponse.hpp | 3 + src/net/connection/web/WowConnection.cpp | 740 +++++++++++++++++++ src/net/connection/web/WowConnectionNet.cpp | 174 +++++ src/net/connection/web/WsState.hpp | 54 ++ src/sound/CMakeLists.txt | 20 +- src/sound/SESound.hpp | 40 +- src/sound/SESoundInternal.hpp | 9 +- src/sound/SOUNDKITDEF.hpp | 7 + src/sound/web/SESound.cpp | 77 ++ src/util/CMakeLists.txt | 10 + src/util/web/library_fetchfs.js | 150 ++++ test/CMakeLists.txt | 19 + vendor/CMakeLists.txt | 2 +- vendor/freetype-2.0.9/src/base/ftobjs.c | 2 +- vendor/freetype-2.0.9/src/pcf/pcfdriver.c | 4 +- vendor/freetype-2.0.9/src/sfnt/sfobjs.c | 2 +- 40 files changed, 1964 insertions(+), 21 deletions(-) create mode 100644 src/app/web/Whoa.cpp create mode 100644 src/app/web/shell.html create mode 100644 src/client/gui/web/OsGui.cpp create mode 100644 src/event/web/Input.cpp create mode 100644 src/net/connection/web/WowConnection.cpp create mode 100644 src/net/connection/web/WowConnectionNet.cpp create mode 100644 src/net/connection/web/WsState.hpp create mode 100644 src/sound/web/SESound.cpp create mode 100644 src/util/web/library_fetchfs.js diff --git a/.gitignore b/.gitignore index 8bf97cd..632c1c6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ .vscode .idea -/build +/build* /cmake-build-* /out diff --git a/CMakeLists.txt b/CMakeLists.txt index a10416b..63c1ad6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,7 @@ set(CMAKE_CXX_STANDARD 11) include(lib/system/cmake/system.cmake) + # Some templates abuse offsetof if(WHOA_SYSTEM_LINUX OR WHOA_SYSTEM_MAC) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-invalid-offsetof") @@ -63,6 +64,13 @@ if(WHOA_SYSTEM_LINUX OR WHOA_SYSTEM_MAC) find_package(Threads REQUIRED) endif() +# Emscripten pthreads support +if(WHOA_SYSTEM_WEB) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pthread") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pthread -s PTHREAD_POOL_SIZE=4") +endif() + # Library search paths if(WHOA_SYSTEM_MAC) set(CMAKE_SKIP_BUILD_RPATH FALSE) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index a829594..bb6a9ee 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -52,6 +52,31 @@ if(WHOA_SYSTEM_LINUX) ) endif() +if(WHOA_SYSTEM_WEB) + file(GLOB PRIVATE_SOURCES "web/*.cpp") + + add_executable(Whoa ${PRIVATE_SOURCES}) + + target_link_libraries(Whoa + PRIVATE + client + event + gx + net + util + ) + + set_target_properties(Whoa PROPERTIES SUFFIX ".html") + + target_link_options(Whoa PRIVATE + "SHELL:--shell-file ${CMAKE_CURRENT_SOURCE_DIR}/web/shell.html" + -sASYNCIFY + -sASYNCIFY_STACK_SIZE=524288 + -sALLOW_MEMORY_GROWTH + "-sASYNCIFY_IMPORTS=['__syscall_openat','__syscall_stat64','__syscall_lstat64','__syscall_fstat64']" + ) +endif() + target_include_directories(Whoa PRIVATE ${CMAKE_SOURCE_DIR}/src diff --git a/src/app/web/Whoa.cpp b/src/app/web/Whoa.cpp new file mode 100644 index 0000000..b6712e6 --- /dev/null +++ b/src/app/web/Whoa.cpp @@ -0,0 +1,55 @@ +#include "client/Client.hpp" +#include "event/Event.hpp" +#include "gx/Device.hpp" +#include +#include + +// Forward declaration - defined in Client.cpp +int32_t InitializeGlobal(); + +// FETCHFS: Set the base URL for lazy file loading from server +// Defined in library_fetchfs.js, linked via util library +extern "C" void fetchfs_set_base_url(const char* url); + +static bool s_initialized = false; + +// Main loop callback for Emscripten +static void MainLoop() { + // Process one frame of events + EventProcessFrame(); +} + +// Resize callback +static EM_BOOL OnResize(int eventType, const EmscriptenUiEvent* uiEvent, void* userData) { + double cssWidth, cssHeight; + emscripten_get_element_css_size("#canvas", &cssWidth, &cssHeight); + + if (g_theGxDevicePtr) { + g_theGxDevicePtr->DeviceWM(GxWM_Size, + static_cast(cssWidth), + static_cast(cssHeight)); + } + + return EM_TRUE; +} + +int main(int argc, char* argv[]) { + // Configure FETCHFS to load files from ./data/ on the server + // FETCHFS patches MEMFS to fetch files on-demand when accessed + fetchfs_set_base_url("./data/"); + + StormInitialize(); + + // Set up resize callback + emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_TRUE, OnResize); + + if (InitializeGlobal()) { + s_initialized = true; + + // Use Emscripten's main loop instead of blocking EventDoMessageLoop + // 0 = use requestAnimationFrame, 0 = don't simulate infinite loop + emscripten_set_main_loop(MainLoop, 0, 0); + } + + return 0; +} diff --git a/src/app/web/shell.html b/src/app/web/shell.html new file mode 100644 index 0000000..2fee9ba --- /dev/null +++ b/src/app/web/shell.html @@ -0,0 +1,144 @@ + + + + + + Whoa + + + + +
+
Loading...
+
+
+
+
+
+
+ + + {{{ SCRIPT }}} + + diff --git a/src/async/AsyncFileRead.cpp b/src/async/AsyncFileRead.cpp index a83b9ca..5100c8b 100644 --- a/src/async/AsyncFileRead.cpp +++ b/src/async/AsyncFileRead.cpp @@ -39,6 +39,7 @@ void AsyncFileReadCreateThread(CAsyncQueue* queue, const char* queueName) { thread->queue = queue; thread->currentObject = nullptr; + printf("AsyncFileReadCreateThread: Creating thread '%s' for queue=%p\n", queueName, static_cast(queue)); SThread::Create(AsyncFileReadThread, thread, thread->thread, const_cast(queueName), 0); } @@ -147,6 +148,7 @@ uint32_t AsyncFileReadThread(void* param) { AsyncFileRead::s_queueLock.Leave(); int32_t tries = 10; + int32_t readSuccess = 0; while (1) { if (SFile::IsStreamingMode() && object->file) { // TODO @@ -154,6 +156,7 @@ uint32_t AsyncFileReadThread(void* param) { } if (SFile::Read(object->file, object->buffer, object->size, nullptr, nullptr, nullptr)) { + readSuccess = 1; break; } diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 70b686a..7b9f96c 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -24,6 +24,13 @@ if(WHOA_SYSTEM_LINUX) list(APPEND PRIVATE_SOURCES ${LINUX_SOURCES}) endif() +if(WHOA_SYSTEM_WEB) + file(GLOB WEB_SOURCES + "gui/web/*.cpp" + ) + list(APPEND PRIVATE_SOURCES ${WEB_SOURCES}) +endif() + add_library(client STATIC ${PRIVATE_SOURCES} ) diff --git a/src/client/ClientServices.cpp b/src/client/ClientServices.cpp index f3fbeab..18a4ebf 100644 --- a/src/client/ClientServices.cpp +++ b/src/client/ClientServices.cpp @@ -317,7 +317,7 @@ void ClientServices::InitLoginServerCVars(int32_t force, const char* locale) { "realmList", "Address of realm list server", 0x0, - "us.logon.worldofwarcraft.com:3724", + "logon.chromiecraft.com:3724", nullptr, NET, false, diff --git a/src/client/gui/web/OsGui.cpp b/src/client/gui/web/OsGui.cpp new file mode 100644 index 0000000..5f4f523 --- /dev/null +++ b/src/client/gui/web/OsGui.cpp @@ -0,0 +1,36 @@ +#include "client/gui/OsGui.hpp" +#include + +void* OsGuiGetWindow(int32_t type) { + // No native window handle on web + return nullptr; +} + +bool OsGuiIsModifierKeyDown(int32_t key) { + EmscriptenKeyboardEvent state; + + // Get current keyboard state from Emscripten + // Note: This only works if a key event was recently processed + switch (key) { + case 0: // KEY_LSHIFT + case 1: // KEY_RSHIFT + return false; // Would need to track state manually + case 2: // KEY_LCONTROL + case 3: // KEY_RCONTROL + return false; + case 4: // KEY_LALT + case 5: // KEY_RALT + return false; + default: + return false; + } +} + +int32_t OsGuiProcessMessage(void* message) { + // Not used on web - events come through Emscripten callbacks + return 0; +} + +void OsGuiSetGxWindow(void* window) { + // No-op on web +} diff --git a/src/event/CMakeLists.txt b/src/event/CMakeLists.txt index 0670307..316f37d 100644 --- a/src/event/CMakeLists.txt +++ b/src/event/CMakeLists.txt @@ -22,6 +22,13 @@ if(WHOA_SYSTEM_LINUX) list(APPEND PRIVATE_SOURCES ${LINUX_SOURCES}) endif() +if(WHOA_SYSTEM_WEB) + file(GLOB WEB_SOURCES + "web/*.cpp" + ) + list(APPEND PRIVATE_SOURCES ${WEB_SOURCES}) +endif() + add_library(event STATIC ${PRIVATE_SOURCES} ) diff --git a/src/event/Event.cpp b/src/event/Event.cpp index 42f82aa..370882a 100644 --- a/src/event/Event.cpp +++ b/src/event/Event.cpp @@ -193,3 +193,11 @@ void EventUnregisterEx(EVENTID id, EVENTHANDLERFUNC handler, void* param, uint32 } } } + +#if defined(WHOA_SYSTEM_WEB) +void EventProcessFrame() { + // Process one frame of the scheduler + // This is the non-blocking version of EventDoMessageLoop for web + SchedulerMainProcess(); +} +#endif diff --git a/src/event/Event.hpp b/src/event/Event.hpp index ffee971..1d21bef 100644 --- a/src/event/Event.hpp +++ b/src/event/Event.hpp @@ -53,6 +53,10 @@ void EventPostClose(); void EventPostCloseEx(HEVENTCONTEXT contextHandle); +#if defined(WHOA_SYSTEM_WEB) +void EventProcessFrame(); +#endif + void EventRegister(EVENTID id, int32_t (*handler)(const void*, void*)); void EventRegisterEx(EVENTID id, int32_t (*handler)(const void*, void*), void* param, float priority); diff --git a/src/event/Input.cpp b/src/event/Input.cpp index 0ed01b9..68d600e 100644 --- a/src/event/Input.cpp +++ b/src/event/Input.cpp @@ -632,6 +632,7 @@ const char* KeyCodeToString(KEY key) { return "UNKNOWN"; } +#if !defined(WHOA_SYSTEM_WEB) void OsInputInitialize() { #if defined(WHOA_SYSTEM_WIN) Input::s_numlockState = GetAsyncKeyState(144); @@ -656,6 +657,7 @@ bool OsInputIsUsingCocoaEventLoop() { return true; } +#endif void OsInputPostEvent(OSINPUT id, int32_t param0, int32_t param1, int32_t param2, int32_t param3) { // TODO diff --git a/src/event/Scheduler.cpp b/src/event/Scheduler.cpp index 808b34c..9d3f556 100644 --- a/src/event/Scheduler.cpp +++ b/src/event/Scheduler.cpp @@ -146,6 +146,22 @@ void IEvtSchedulerProcess() { // dword_141B3C8 = 0; } #endif + + #if defined(WHOA_SYSTEM_WEB) + // Web builds use a callback-based main loop (emscripten_set_main_loop) + // This function should not be called - use EventProcessFrame() instead + Event::s_startEvent.Set(); + + PropSelectContext(0); + + uintptr_t v0 = SGetCurrentThreadId(); + char v2[64]; + SStrPrintf(v2, 64, "Engine %x", v0); + + OsCallInitialize(v2); + + // Don't block - the main loop callback will call SchedulerMainProcess + #endif } void IEvtSchedulerShutdown() { diff --git a/src/event/web/Input.cpp b/src/event/web/Input.cpp new file mode 100644 index 0000000..432baf8 --- /dev/null +++ b/src/event/web/Input.cpp @@ -0,0 +1,288 @@ +#include "event/Input.hpp" +#include +#include + +// Key code translation from DOM key codes to engine KEY values +static KEY TranslateKeyCode(const char* code) { + // Letters + if (strcmp(code, "KeyA") == 0) return KEY_A; + if (strcmp(code, "KeyB") == 0) return KEY_B; + if (strcmp(code, "KeyC") == 0) return KEY_C; + if (strcmp(code, "KeyD") == 0) return KEY_D; + if (strcmp(code, "KeyE") == 0) return KEY_E; + if (strcmp(code, "KeyF") == 0) return KEY_F; + if (strcmp(code, "KeyG") == 0) return KEY_G; + if (strcmp(code, "KeyH") == 0) return KEY_H; + if (strcmp(code, "KeyI") == 0) return KEY_I; + if (strcmp(code, "KeyJ") == 0) return KEY_J; + if (strcmp(code, "KeyK") == 0) return KEY_K; + if (strcmp(code, "KeyL") == 0) return KEY_L; + if (strcmp(code, "KeyM") == 0) return KEY_M; + if (strcmp(code, "KeyN") == 0) return KEY_N; + if (strcmp(code, "KeyO") == 0) return KEY_O; + if (strcmp(code, "KeyP") == 0) return KEY_P; + if (strcmp(code, "KeyQ") == 0) return KEY_Q; + if (strcmp(code, "KeyR") == 0) return KEY_R; + if (strcmp(code, "KeyS") == 0) return KEY_S; + if (strcmp(code, "KeyT") == 0) return KEY_T; + if (strcmp(code, "KeyU") == 0) return KEY_U; + if (strcmp(code, "KeyV") == 0) return KEY_V; + if (strcmp(code, "KeyW") == 0) return KEY_W; + if (strcmp(code, "KeyX") == 0) return KEY_X; + if (strcmp(code, "KeyY") == 0) return KEY_Y; + if (strcmp(code, "KeyZ") == 0) return KEY_Z; + + // Numbers + if (strcmp(code, "Digit0") == 0) return KEY_0; + if (strcmp(code, "Digit1") == 0) return KEY_1; + if (strcmp(code, "Digit2") == 0) return KEY_2; + if (strcmp(code, "Digit3") == 0) return KEY_3; + if (strcmp(code, "Digit4") == 0) return KEY_4; + if (strcmp(code, "Digit5") == 0) return KEY_5; + if (strcmp(code, "Digit6") == 0) return KEY_6; + if (strcmp(code, "Digit7") == 0) return KEY_7; + if (strcmp(code, "Digit8") == 0) return KEY_8; + if (strcmp(code, "Digit9") == 0) return KEY_9; + + // Function keys + if (strcmp(code, "F1") == 0) return KEY_F1; + if (strcmp(code, "F2") == 0) return KEY_F2; + if (strcmp(code, "F3") == 0) return KEY_F3; + if (strcmp(code, "F4") == 0) return KEY_F4; + if (strcmp(code, "F5") == 0) return KEY_F5; + if (strcmp(code, "F6") == 0) return KEY_F6; + if (strcmp(code, "F7") == 0) return KEY_F7; + if (strcmp(code, "F8") == 0) return KEY_F8; + if (strcmp(code, "F9") == 0) return KEY_F9; + if (strcmp(code, "F10") == 0) return KEY_F10; + if (strcmp(code, "F11") == 0) return KEY_F11; + if (strcmp(code, "F12") == 0) return KEY_F12; + + // Special keys + if (strcmp(code, "Escape") == 0) return KEY_ESCAPE; + if (strcmp(code, "Enter") == 0) return KEY_ENTER; + if (strcmp(code, "NumpadEnter") == 0) return KEY_ENTER; + if (strcmp(code, "Backspace") == 0) return KEY_BACKSPACE; + if (strcmp(code, "Tab") == 0) return KEY_TAB; + if (strcmp(code, "Space") == 0) return KEY_SPACE; + + // Arrow keys + if (strcmp(code, "ArrowLeft") == 0) return KEY_LEFT; + if (strcmp(code, "ArrowUp") == 0) return KEY_UP; + if (strcmp(code, "ArrowRight") == 0) return KEY_RIGHT; + if (strcmp(code, "ArrowDown") == 0) return KEY_DOWN; + + // Navigation keys + if (strcmp(code, "Insert") == 0) return KEY_INSERT; + if (strcmp(code, "Delete") == 0) return KEY_DELETE; + if (strcmp(code, "Home") == 0) return KEY_HOME; + if (strcmp(code, "End") == 0) return KEY_END; + if (strcmp(code, "PageUp") == 0) return KEY_PAGEUP; + if (strcmp(code, "PageDown") == 0) return KEY_PAGEDOWN; + + // Modifier keys + if (strcmp(code, "ShiftLeft") == 0) return KEY_LSHIFT; + if (strcmp(code, "ShiftRight") == 0) return KEY_RSHIFT; + if (strcmp(code, "ControlLeft") == 0) return KEY_LCONTROL; + if (strcmp(code, "ControlRight") == 0) return KEY_RCONTROL; + if (strcmp(code, "AltLeft") == 0) return KEY_LALT; + if (strcmp(code, "AltRight") == 0) return KEY_RALT; + + // Lock keys + if (strcmp(code, "CapsLock") == 0) return KEY_CAPSLOCK; + if (strcmp(code, "NumLock") == 0) return KEY_NUMLOCK; + if (strcmp(code, "ScrollLock") == 0) return KEY_SCROLLLOCK; + + // Numpad + if (strcmp(code, "Numpad0") == 0) return KEY_NUMPAD0; + if (strcmp(code, "Numpad1") == 0) return KEY_NUMPAD1; + if (strcmp(code, "Numpad2") == 0) return KEY_NUMPAD2; + if (strcmp(code, "Numpad3") == 0) return KEY_NUMPAD3; + if (strcmp(code, "Numpad4") == 0) return KEY_NUMPAD4; + if (strcmp(code, "Numpad5") == 0) return KEY_NUMPAD5; + if (strcmp(code, "Numpad6") == 0) return KEY_NUMPAD6; + if (strcmp(code, "Numpad7") == 0) return KEY_NUMPAD7; + if (strcmp(code, "Numpad8") == 0) return KEY_NUMPAD8; + if (strcmp(code, "Numpad9") == 0) return KEY_NUMPAD9; + if (strcmp(code, "NumpadAdd") == 0) return KEY_NUMPAD_PLUS; + if (strcmp(code, "NumpadSubtract") == 0) return KEY_NUMPAD_MINUS; + if (strcmp(code, "NumpadMultiply") == 0) return KEY_NUMPAD_MULTIPLY; + if (strcmp(code, "NumpadDivide") == 0) return KEY_NUMPAD_DIVIDE; + if (strcmp(code, "NumpadDecimal") == 0) return KEY_NUMPAD_DECIMAL; + + // Punctuation + if (strcmp(code, "Minus") == 0) return KEY_MINUS; + if (strcmp(code, "Equal") == 0) return KEY_PLUS; + if (strcmp(code, "BracketLeft") == 0) return KEY_BRACKET_OPEN; + if (strcmp(code, "BracketRight") == 0) return KEY_BRACKET_CLOSE; + if (strcmp(code, "Semicolon") == 0) return KEY_SEMICOLON; + if (strcmp(code, "Quote") == 0) return KEY_APOSTROPHE; + if (strcmp(code, "Backquote") == 0) return KEY_TILDE; + if (strcmp(code, "Backslash") == 0) return KEY_BACKSLASH; + if (strcmp(code, "Comma") == 0) return KEY_COMMA; + if (strcmp(code, "Period") == 0) return KEY_PERIOD; + if (strcmp(code, "Slash") == 0) return KEY_SLASH; + + return KEY_NONE; +} + +static uint32_t GetMetaKeyState(const EmscriptenKeyboardEvent* e) { + uint32_t state = 0; + if (e->shiftKey) { + state |= (1 << KEY_LSHIFT); + } + if (e->ctrlKey) { + state |= (1 << KEY_LCONTROL); + } + if (e->altKey) { + state |= (1 << KEY_LALT); + } + return state; +} + +static MOUSEBUTTON TranslateMouseButton(int button) { + switch (button) { + case 0: return MOUSE_BUTTON_LEFT; + case 1: return MOUSE_BUTTON_MIDDLE; + case 2: return MOUSE_BUTTON_RIGHT; + case 3: return MOUSE_BUTTON_XBUTTON1; + case 4: return MOUSE_BUTTON_XBUTTON2; + default: return MOUSE_BUTTON_NONE; + } +} + +// Keyboard event callback +static EM_BOOL OnKeyDown(int eventType, const EmscriptenKeyboardEvent* e, void* userData) { + KEY key = TranslateKeyCode(e->code); + if (key != KEY_NONE) { + uint32_t metaState = GetMetaKeyState(e); + OsQueuePut(OS_INPUT_KEY_DOWN, static_cast(key), metaState, e->repeat ? 1 : 0, 0); + } + + // Also send char event for printable characters + if (e->key[0] != '\0' && e->key[1] == '\0') { + uint32_t metaState = GetMetaKeyState(e); + OsQueuePut(OS_INPUT_CHAR, static_cast(e->key[0]), metaState, e->repeat ? 1 : 0, 0); + } + + return EM_TRUE; +} + +static EM_BOOL OnKeyUp(int eventType, const EmscriptenKeyboardEvent* e, void* userData) { + KEY key = TranslateKeyCode(e->code); + if (key != KEY_NONE) { + uint32_t metaState = GetMetaKeyState(e); + OsQueuePut(OS_INPUT_KEY_UP, static_cast(key), metaState, 0, 0); + } + return EM_TRUE; +} + +// Mouse event callbacks +static EM_BOOL OnMouseDown(int eventType, const EmscriptenMouseEvent* e, void* userData) { + MOUSEBUTTON button = TranslateMouseButton(e->button); + int32_t x = e->targetX; + int32_t y = e->targetY; + OsQueuePut(OS_INPUT_MOUSE_DOWN, static_cast(button), x, y, 0); + return EM_TRUE; +} + +static EM_BOOL OnMouseUp(int eventType, const EmscriptenMouseEvent* e, void* userData) { + MOUSEBUTTON button = TranslateMouseButton(e->button); + int32_t x = e->targetX; + int32_t y = e->targetY; + OsQueuePut(OS_INPUT_MOUSE_UP, static_cast(button), x, y, 0); + return EM_TRUE; +} + +static EM_BOOL OnMouseMove(int eventType, const EmscriptenMouseEvent* e, void* userData) { + if (Input::s_osMouseMode == OS_MOUSE_MODE_RELATIVE) { + // Relative mouse movement (pointer locked) + int32_t dx = e->movementX; + int32_t dy = e->movementY; + OsQueuePut(OS_INPUT_MOUSE_MOVE_RELATIVE, dx, dy, 0, 0); + } else { + // Absolute mouse position + int32_t x = e->targetX; + int32_t y = e->targetY; + OsQueuePut(OS_INPUT_MOUSE_MOVE, 0, x, y, 0); + } + return EM_TRUE; +} + +static EM_BOOL OnWheel(int eventType, const EmscriptenWheelEvent* e, void* userData) { + // Normalize wheel delta + int32_t delta = static_cast(-e->deltaY); + if (e->deltaMode == DOM_DELTA_LINE) { + delta *= 40; // Approximate line height + } else if (e->deltaMode == DOM_DELTA_PAGE) { + delta *= 400; // Approximate page height + } + OsQueuePut(OS_INPUT_MOUSE_WHEEL, delta, 0, 0, 0); + return EM_TRUE; +} + +static EM_BOOL OnFocus(int eventType, const EmscriptenFocusEvent* e, void* userData) { + OsQueuePut(OS_INPUT_FOCUS, 1, 0, 0, 0); + return EM_TRUE; +} + +static EM_BOOL OnBlur(int eventType, const EmscriptenFocusEvent* e, void* userData) { + OsQueuePut(OS_INPUT_FOCUS, 0, 0, 0, 0); + return EM_TRUE; +} + +static EM_BOOL OnPointerLockChange(int eventType, const EmscriptenPointerlockChangeEvent* e, void* userData) { + if (!e->isActive && Input::s_osMouseMode == OS_MOUSE_MODE_RELATIVE) { + // Pointer lock was released externally + Input::s_osMouseMode = OS_MOUSE_MODE_NORMAL; + } + return EM_TRUE; +} + +void OsInputInitialize() { + // Register keyboard events on document + emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, OnKeyDown); + emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, OnKeyUp); + + // Register mouse events on canvas + emscripten_set_mousedown_callback("#canvas", nullptr, EM_TRUE, OnMouseDown); + emscripten_set_mouseup_callback("#canvas", nullptr, EM_TRUE, OnMouseUp); + emscripten_set_mousemove_callback("#canvas", nullptr, EM_TRUE, OnMouseMove); + emscripten_set_wheel_callback("#canvas", nullptr, EM_TRUE, OnWheel); + + // Register focus events + emscripten_set_focus_callback("#canvas", nullptr, EM_TRUE, OnFocus); + emscripten_set_blur_callback("#canvas", nullptr, EM_TRUE, OnBlur); + + // Register pointer lock change event + emscripten_set_pointerlockchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, OnPointerLockChange); +} + +int32_t OsInputGet(OSINPUT* id, int32_t* param0, int32_t* param1, int32_t* param2, int32_t* param3) { + return OsQueueGet(id, param0, param1, param2, param3); +} + +void OsInputSetMouseMode(OS_MOUSE_MODE mode) { + if (mode == Input::s_osMouseMode) { + return; + } + + Input::s_osMouseMode = mode; + + if (mode == OS_MOUSE_MODE_RELATIVE) { + // Request pointer lock + emscripten_request_pointerlock("#canvas", EM_TRUE); + } else { + // Exit pointer lock + emscripten_exit_pointerlock(); + } +} + +int32_t OsWindowProc(void* window, uint32_t message, uintptr_t wparam, intptr_t lparam) { + // Not used on web - events come through Emscripten callbacks + return 0; +} + +bool OsInputIsUsingCocoaEventLoop() { + return false; +} diff --git a/src/gx/CGxDevice.cpp b/src/gx/CGxDevice.cpp index 880a107..b78064a 100644 --- a/src/gx/CGxDevice.cpp +++ b/src/gx/CGxDevice.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include #include @@ -179,7 +181,16 @@ int32_t CGxDevice::GLLAdapterMonitorModes(TSGrowableArray& monit #endif void CGxDevice::Log(const char* format, ...) { - // TODO + va_list args; + va_start(args, format); + char buffer[1024]; + vsnprintf(buffer, sizeof(buffer), format, args); + va_end(args); +#if defined(__EMSCRIPTEN__) + printf("[GxDevice] %s\n", buffer); +#else + fprintf(stderr, "[GxDevice] %s\n", buffer); +#endif } void CGxDevice::Log(const CGxFormat& format) { diff --git a/src/gx/Texture.cpp b/src/gx/Texture.cpp index 929b79d..b34a26d 100644 --- a/src/gx/Texture.cpp +++ b/src/gx/Texture.cpp @@ -156,7 +156,8 @@ void FillInSolidTexture(const CImVector& color, CTexture* texture) { gxTexFlags, userArg, GxuUpdateSingleColorTexture, - GxTex_Argb8888 + GxTex_Argb8888, + "SolidColor" ); if (color.a < 0xFE) { @@ -252,7 +253,7 @@ int32_t GxTexCreate(const CGxTexParms& parms, CGxTex*& texId) { parms.flags, parms.userArg, parms.userFunc, - "", + parms.name, texId ); } @@ -329,7 +330,7 @@ void TextureFreeGxTex(CGxTex* texId) { GxTexDestroy(texId); } -CGxTex* TextureAllocGxTex(EGxTexTarget target, uint32_t width, uint32_t height, uint32_t depth, EGxTexFormat format, CGxTexFlags flags, void* userArg, TEXTURE_CALLBACK* userFunc, EGxTexFormat dataFormat) { +CGxTex* TextureAllocGxTex(EGxTexTarget target, uint32_t width, uint32_t height, uint32_t depth, EGxTexFormat format, CGxTexFlags flags, void* userArg, TEXTURE_CALLBACK* userFunc, EGxTexFormat dataFormat, const char* name) { CGxTexParms gxTexParms; gxTexParms.height = height; @@ -342,6 +343,7 @@ CGxTex* TextureAllocGxTex(EGxTexTarget target, uint32_t width, uint32_t height, gxTexParms.userFunc = userFunc; gxTexParms.flags = flags; gxTexParms.flags.m_generateMipMaps = 0; + gxTexParms.name = name ? name : ""; CGxTexParms gxTexParms2; @@ -718,7 +720,8 @@ int32_t PumpBlpTextureAsync(CTexture* texture, void* buf) { texture->gxTexFlags, texture, &UpdateBlpTextureAsync, - texture->dataFormat + texture->dataFormat, + texture->filename ); texture->gxTex = gxTex; @@ -1005,7 +1008,7 @@ HTEXTURE TextureCreate(EGxTexTarget target, uint32_t width, uint32_t height, uin texFlags.m_maxAnisotropy = texFlags.m_filter == GxTex_Anisotropic ? CTexture::s_maxAnisotropy : 1; - texture->gxTex = TextureAllocGxTex(target, width, height, depth, format, texFlags, userArg, userFunc, dataFormat); + texture->gxTex = TextureAllocGxTex(target, width, height, depth, format, texFlags, userArg, userFunc, dataFormat, a10 ? a10 : "UniqueTexture"); texture->dataFormat = dataFormat; texture->gxWidth = width; texture->gxHeight = height; diff --git a/src/gx/Texture.hpp b/src/gx/Texture.hpp index a518a17..25a00e5 100644 --- a/src/gx/Texture.hpp +++ b/src/gx/Texture.hpp @@ -45,7 +45,7 @@ MipBits* MippedImgAllocA(uint32_t, uint32_t, uint32_t, const char*, int32_t); uint32_t MippedImgCalcSize(uint32_t, uint32_t, uint32_t); -CGxTex* TextureAllocGxTex(EGxTexTarget, uint32_t, uint32_t, uint32_t, EGxTexFormat, CGxTexFlags, void*, void (*userFunc)(EGxTexCommand, uint32_t, uint32_t, uint32_t, uint32_t, void*, uint32_t&, const void*&), EGxTexFormat); +CGxTex* TextureAllocGxTex(EGxTexTarget, uint32_t, uint32_t, uint32_t, EGxTexFormat, CGxTexFlags, void*, void (*userFunc)(EGxTexCommand, uint32_t, uint32_t, uint32_t, uint32_t, void*, uint32_t&, const void*&), EGxTexFormat, const char* name = ""); MipBits* TextureAllocMippedImg(PIXEL_FORMAT pixelFormat, uint32_t width, uint32_t height); diff --git a/src/gx/Window.hpp b/src/gx/Window.hpp index 19a14e1..8fa0754 100644 --- a/src/gx/Window.hpp +++ b/src/gx/Window.hpp @@ -11,7 +11,7 @@ #include #endif -#if defined(WHOA_SYSTEM_LINUX) || defined(WHOA_SYSTEM_WIN) +#if defined(WHOA_SYSTEM_LINUX) || defined(WHOA_SYSTEM_WIN) || defined(WHOA_SYSTEM_WEB) struct Rect { int16_t top; int16_t left; @@ -20,7 +20,7 @@ struct Rect { }; #endif -#if defined(WHOA_SYSTEM_MAC) || defined(WHOA_SYSTEM_LINUX) +#if defined(WHOA_SYSTEM_MAC) || defined(WHOA_SYSTEM_LINUX) || defined(WHOA_SYSTEM_WEB) typedef struct tagRECT { int32_t left; int32_t top; diff --git a/src/gx/buffer/CGxPool.hpp b/src/gx/buffer/CGxPool.hpp index 500a6c5..b9d3502 100644 --- a/src/gx/buffer/CGxPool.hpp +++ b/src/gx/buffer/CGxPool.hpp @@ -26,6 +26,7 @@ class CGxPool : public TSLinkedNode { , m_usage(usage) , m_size(size) , m_apiSpecific(nullptr) + , m_mem(nullptr) , unk1C(0) , m_hint(hint) , m_name(name) diff --git a/src/gx/texture/CGxTex.cpp b/src/gx/texture/CGxTex.cpp index 740ed80..245aad4 100644 --- a/src/gx/texture/CGxTex.cpp +++ b/src/gx/texture/CGxTex.cpp @@ -1,6 +1,7 @@ #include "gx/texture/CGxTex.hpp" #include "gx/Gx.hpp" #include +#include CGxTexFlags::CGxTexFlags(EGxTexFilter filter, uint32_t wrapU, uint32_t wrapV, uint32_t force, uint32_t generateMipMaps, uint32_t renderTarget, uint32_t maxAnisotropy) { this->m_filter = filter; @@ -47,6 +48,13 @@ CGxTex::CGxTex(EGxTexTarget target, uint32_t width, uint32_t height, uint32_t de this->m_needsFlagUpdate = 1; this->m_needsCreation = 1; + if (name && name[0]) { + std::strncpy(this->m_name, name, sizeof(this->m_name) - 1); + this->m_name[sizeof(this->m_name) - 1] = '\0'; + } else { + this->m_name[0] = '\0'; + } + // TODO remaining constructor logic } diff --git a/src/gx/texture/CGxTex.hpp b/src/gx/texture/CGxTex.hpp index b85aa96..f95e258 100644 --- a/src/gx/texture/CGxTex.hpp +++ b/src/gx/texture/CGxTex.hpp @@ -39,6 +39,7 @@ class CGxTexParms { CGxTexFlags flags = CGxTexFlags(GxTex_Linear, 0, 0, 0, 0, 0, 1); void* userArg; void (*userFunc)(EGxTexCommand, uint32_t, uint32_t, uint32_t, uint32_t, void*, uint32_t&, const void*&); + const char* name = ""; }; class CGxTex { @@ -61,6 +62,7 @@ class CGxTex { uint8_t m_needsUpdate; uint8_t m_needsCreation; uint8_t m_needsFlagUpdate; + char m_name[260]; // Member functions CGxTex(EGxTexTarget, uint32_t, uint32_t, uint32_t, EGxTexFormat, EGxTexFormat, CGxTexFlags, void*, void (*)(EGxTexCommand, uint32_t, uint32_t, uint32_t, uint32_t, void*, uint32_t&, const void*&), const char*); diff --git a/src/net/CMakeLists.txt b/src/net/CMakeLists.txt index 72a9587..54afb20 100644 --- a/src/net/CMakeLists.txt +++ b/src/net/CMakeLists.txt @@ -20,6 +20,17 @@ if(WHOA_SYSTEM_MAC OR WHOA_SYSTEM_LINUX) list(APPEND PRIVATE_SOURCES ${BSD_SOURCES}) endif() +if(WHOA_SYSTEM_WEB) + list(REMOVE_ITEM PRIVATE_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/connection/WowConnection.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/connection/WowConnectionNet.cpp + ) + file(GLOB WEB_SOURCES + "connection/web/*.cpp" + ) + list(APPEND PRIVATE_SOURCES ${WEB_SOURCES}) +endif() + add_library(net STATIC ${PRIVATE_SOURCES} ) @@ -46,3 +57,10 @@ if(WHOA_SYSTEM_WIN) wsock32 ) endif() + +if(WHOA_SYSTEM_WEB) + target_link_options(net + PUBLIC + -lwebsocket.js + ) +endif() diff --git a/src/net/connection/WowConnection.hpp b/src/net/connection/WowConnection.hpp index 0433dfb..d787cf1 100644 --- a/src/net/connection/WowConnection.hpp +++ b/src/net/connection/WowConnection.hpp @@ -16,6 +16,10 @@ #include #endif +#if defined(WHOA_SYSTEM_WEB) +struct sockaddr_in; +#endif + class CDataStore; class WowConnectionNet; class WowConnectionResponse; diff --git a/src/net/connection/WowConnectionResponse.hpp b/src/net/connection/WowConnectionResponse.hpp index f23c600..4e704cd 100644 --- a/src/net/connection/WowConnectionResponse.hpp +++ b/src/net/connection/WowConnectionResponse.hpp @@ -8,6 +8,9 @@ class WowConnection; class WowConnectionResponse { public: + // Virtual destructor + virtual ~WowConnectionResponse() = default; + // Virtual member functions virtual void WCMessageReady(WowConnection* conn, uint32_t timeStamp, CDataStore* msg) = 0; virtual void WCConnected(WowConnection* conn, WowConnection* inbound, uint32_t timeStamp, const NETCONNADDR* addr) = 0; diff --git a/src/net/connection/web/WowConnection.cpp b/src/net/connection/web/WowConnection.cpp new file mode 100644 index 0000000..cf3cd65 --- /dev/null +++ b/src/net/connection/web/WowConnection.cpp @@ -0,0 +1,740 @@ +#include "net/connection/WowConnection.hpp" +#include "net/connection/WowConnectionNet.hpp" +#include "net/connection/WowConnectionResponse.hpp" +#include "net/connection/web/WsState.hpp" +#include "util/HMAC.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Static variables +uint64_t WowConnection::s_countTotalBytes; +int32_t WowConnection::s_destroyed; +int32_t WowConnection::s_lagTestDelayMin; +WowConnectionNet* WowConnection::s_network; +ATOMIC32 WowConnection::s_numWowConnections; +bool (*WowConnection::s_verifyAddr)(const NETADDR*); + +static uint8_t s_arc4drop1024[1024] = { 0x00 }; +static uint8_t s_arc4seed[] = { + // Receive key + 0xCC, 0x98, 0xAE, 0x04, 0xE8, 0x97, 0xEA, 0xCA, 0x12, 0xDD, 0xC0, 0x93, 0x42, 0x91, 0x53, 0x57, + // Send key + 0xC2, 0xB3, 0x72, 0x3C, 0xC6, 0xAE, 0xD9, 0xB5, 0x34, 0x3C, 0x53, 0xEE, 0x2F, 0x43, 0x67, 0xCE, +}; + +// WebSocket callbacks + +static EM_BOOL ws_onopen(int eventType, const EmscriptenWebSocketOpenEvent* event, void* userData) { + auto conn = static_cast(userData); + auto wsState = static_cast(conn->m_event); + if (wsState) { + wsState->connectPending = true; + } + return EM_TRUE; +} + +static EM_BOOL ws_onmessage(int eventType, const EmscriptenWebSocketMessageEvent* event, void* userData) { + auto conn = static_cast(userData); + auto wsState = static_cast(conn->m_event); + if (wsState && event->numBytes > 0) { + wsState->recvBuf.append(event->data, event->numBytes); + } + return EM_TRUE; +} + +static EM_BOOL ws_onclose(int eventType, const EmscriptenWebSocketCloseEvent* event, void* userData) { + auto conn = static_cast(userData); + auto wsState = static_cast(conn->m_event); + if (wsState) { + wsState->closePending = true; + } + return EM_TRUE; +} + +static EM_BOOL ws_onerror(int eventType, const EmscriptenWebSocketErrorEvent* event, void* userData) { + auto conn = static_cast(userData); + auto wsState = static_cast(conn->m_event); + if (wsState) { + wsState->errorPending = true; + } + return EM_TRUE; +} + +// SENDNODE - same framing logic as native + +WowConnection::SENDNODE::SENDNODE(void* data, int32_t size, uint8_t* buf, bool raw) : TSLinkedNode() { + if (data) { + this->data = buf; + } + + if (raw) { + memcpy(this->data, data, size); + this->size = size; + } else { + uint32_t headerSize = size > 0x7FFF ? 3 : 2; + + if (!data) { + this->data = &buf[-headerSize]; + } + + auto headerBuf = static_cast(this->data); + + if (size > 0x7FFF) { + headerBuf[0] = ((size >> (8 * 2)) & 0xff) | 0x80; + headerBuf[1] = (size >> (8 * 1)) & 0xff; + headerBuf[2] = (size >> (8 * 0)) & 0xff; + } else { + headerBuf[0] = (size >> (8 * 1)) & 0xff; + headerBuf[1] = (size >> (8 * 0)) & 0xff; + } + + if (data) { + memcpy(static_cast(&this->data[headerSize]), data, size); + } + + this->size = size + headerSize; + } + + this->datasize = size; + this->offset = 0; + this->allocsize = 0; + + memcpy(this->header, this->data, std::min(this->size, 8u)); +} + +int32_t WowConnection::CreateSocket() { + // Not used on web - WebSocket handles are managed via WsState + return 0; +} + +int32_t WowConnection::InitOsNet(bool (*fcn)(const NETADDR*), void (*threadinit)(), int32_t numThreads, bool useEngine) { + if (!WowConnection::s_network) { + WowConnection::s_verifyAddr = fcn; + WowConnection::s_destroyed = 0; + + numThreads = std::min(numThreads, 32); + + auto networkMem = SMemAlloc(sizeof(WowConnectionNet), __FILE__, __LINE__, 0x0); + auto network = new (networkMem) WowConnectionNet(numThreads, threadinit); + + WowConnection::s_network = network; + WowConnection::s_network->PlatformInit(useEngine); + WowConnection::s_network->Start(); + } + + return 1; +} + +WowConnection::WowConnection(WowConnectionResponse* response, void (*func)(void)) { + this->Init(response, func); + this->m_sock = -1; +} + +WowConnection::WowConnection(int32_t sock, sockaddr_in* addr, WowConnectionResponse* response) { + this->Init(response, nullptr); + this->m_sock = -1; + this->m_connState = WOWC_DISCONNECTED; +} + +void WowConnection::AcquireResponseRef() { + // Simplified for single-threaded web - no locking or thread ID needed + this->m_responseRef++; +} + +void WowConnection::AddRef() { + SInterlockedIncrement(&this->m_refCount); +} + +void WowConnection::CheckAccept() { + // Not applicable on web - no listening sockets +} + +void WowConnection::CheckConnect() { + auto wsState = static_cast(this->m_event); + if (!wsState) { + return; + } + + if (wsState->errorPending) { + wsState->errorPending = false; + + WowConnection::s_network->Remove(this); + + if (wsState->ws > 0) { + emscripten_websocket_close(wsState->ws, 1000, "error"); + emscripten_websocket_delete(wsState->ws); + wsState->ws = 0; + } + this->m_sock = -1; + + this->SetState(WOWC_DISCONNECTED); + this->AddRef(); + this->AcquireResponseRef(); + + this->m_lock.Leave(); + + if (this->m_response) { + this->m_response->WCCantConnect(this, OsGetAsyncTimeMsPrecise(), &this->m_peer); + } + } else if (wsState->connectPending) { + wsState->connectPending = false; + + this->SetState(WOWC_CONNECTED); + this->AddRef(); + this->AcquireResponseRef(); + + this->m_lock.Leave(); + + if (this->m_response) { + this->m_response->WCConnected(this, nullptr, OsGetAsyncTimeMsPrecise(), &this->m_peer); + } + } else { + return; + } + + this->m_lock.Enter(); + this->ReleaseResponseRef(); + this->Release(); +} + +void WowConnection::CloseSocket(int32_t sock) { + auto wsState = static_cast(this->m_event); + if (wsState && wsState->ws > 0) { + emscripten_websocket_close(wsState->ws, 1000, nullptr); + emscripten_websocket_delete(wsState->ws); + wsState->ws = 0; + wsState->recvBuf.clear(); + wsState->connectPending = false; + wsState->closePending = false; + wsState->errorPending = false; + } +} + +bool WowConnection::Connect(char const* address, int32_t retryMs) { + char host[256]; + auto port = SStrChr(address, ':'); + + if (port) { + this->m_connectPort = SStrToInt(port + 1); + + size_t portIndex = port - address + 1; + portIndex = std::min(portIndex, sizeof(host)); + SStrCopy(host, address, portIndex); + } else { + this->m_connectPort = 0; + SStrCopy(host, address, sizeof(host)); + } + + this->Connect(host, this->m_connectPort, retryMs); + + return true; +} + +bool WowConnection::Connect(char const* address, uint16_t port, int32_t retryMs) { + // Store hostname for WebSocket URL construction + auto wsState = static_cast(this->m_event); + if (!wsState) { + wsState = new WsState(); + this->m_event = wsState; + } + + SStrCopy(wsState->connectHost, address, sizeof(wsState->connectHost)); + this->m_connectAddress = 1; // Non-zero to indicate valid target + this->m_connectPort = port; + + this->StartConnect(); + + return true; +} + +void WowConnection::Disconnect() { + this->m_lock.Enter(); + + if (this->m_sock >= 0 && this->GetState() == WOWC_CONNECTED) { + this->m_connState = WOWC_DISCONNECTING; + + if (WowConnection::s_network) { + WowConnection::s_network->PlatformChangeState(this, WOWC_CONNECTED); + } + } + + this->m_lock.Leave(); +} + +void WowConnection::DoDisconnect() { + this->m_lock.Enter(); + + if (this->m_sock >= 0) { + WowConnection::s_network->Remove(this); + this->CloseSocket(this->m_sock); + } + + this->SetState(WOWC_DISCONNECTED); + + this->AddRef(); + this->AcquireResponseRef(); + + this->m_lock.Leave(); + + if (this->m_response && this->m_sock >= 0) { + this->m_response->WCDisconnected(this, OsGetAsyncTimeMsPrecise(), &this->m_peer); + } + + this->m_lock.Enter(); + + this->m_sock = -1; + this->ReleaseResponseRef(); + + this->m_lock.Leave(); + + this->Release(); +} + +void WowConnection::DoExceptions() { + this->AddRef(); + this->m_lock.Enter(); + + if (this->GetState() == WOWC_CONNECTING) { + this->CheckConnect(); + } + + this->m_lock.Leave(); + this->Release(); +} + +void WowConnection::DoMessageReads() { + if (!this->m_readBuffer) { + this->m_readBuffer = static_cast(SMemAlloc(1024, __FILE__, __LINE__, 0x0)); + this->m_readBufferSize = 1024; + this->m_readBytes = 0; + } + + uint32_t timeStamp = OsGetAsyncTimeMsPrecise(); + + this->AcquireResponseRef(); + this->m_response->NotifyAboutToDoReads(); + this->ReleaseResponseRef(); + + auto wsState = static_cast(this->m_event); + if (!wsState) { + return; + } + + while (true) { + auto headerSize = 2; + auto size = -1; + + if (this->m_readBytes >= headerSize) { + if ((this->m_readBuffer[0] & 0x80) == 0) { + size = (this->m_readBuffer[1] | ((this->m_readBuffer[0] & 0x7F) << 8)) + headerSize; + } else { + headerSize = 3; + + if (this->m_readBytes >= headerSize) { + size = (this->m_readBuffer[2] | ((this->m_readBuffer[1] | ((this->m_readBuffer[0] & 0x7F) << 8)) << 8)) + headerSize; + } + } + } + + if (this->m_readBytes >= this->m_readBufferSize) { + auto readBuffer = SMemReAlloc( + this->m_readBuffer, + this->m_readBufferSize * 2, + __FILE__, + __LINE__, + 0x0 + ); + this->m_readBuffer = static_cast(readBuffer); + this->m_readBufferSize = this->m_readBufferSize * 2; + + if (this->m_readBufferSize > 32000000) { + this->Disconnect(); + return; + } + } + + int32_t bytesToRead; + + if (size >= 0) { + bytesToRead = size - this->m_readBytes; + if (this->m_readBufferSize - this->m_readBytes < size - this->m_readBytes) { + bytesToRead = this->m_readBufferSize - this->m_readBytes; + } + } else { + bytesToRead = headerSize - this->m_readBytes; + } + + int32_t bytesRead; + + if (bytesToRead <= 0) { + bytesRead = 0; + } else { + // Read from WebSocket receive buffer instead of recv() + bytesRead = wsState->recvBuf.read( + &this->m_readBuffer[this->m_readBytes], + bytesToRead + ); + + if (bytesRead <= 0) { + break; + } + } + + if (this->m_encrypt) { + auto v22 = headerSize + this->uint376 - this->m_readBytes; + auto v23 = v22 <= 0 ? 0 : v22; + if (v23 >= bytesRead) { + v23 = bytesRead; + } + + SARC4ProcessBuffer( + &this->m_readBuffer[this->m_readBytes], + v23, + &this->m_receiveKey, + &this->m_receiveKey + ); + } + + this->m_readBytes += bytesRead; + + if (size >= 0 && this->m_readBytes >= size) { + CDataStore msg = CDataStore(&this->m_readBuffer[headerSize], size - headerSize); + + this->AcquireResponseRef(); + + if (this->m_response) { + this->m_lock.Leave(); + this->m_response->WCMessageReady(this, timeStamp, &msg); + this->m_lock.Enter(); + } + + this->m_readBytes = 0; + + this->ReleaseResponseRef(); + } + + if (bytesRead <= 0) { + return; + } + } +} + +void WowConnection::DoReads() { + this->AddRef(); + + this->m_lock.Enter(); + + if (this->m_connState == WOWC_CONNECTED) { + if (this->m_type == WOWC_TYPE_STREAM) { + this->DoStreamReads(); + } else { + this->DoMessageReads(); + } + } + + this->m_lock.Leave(); + + this->Release(); +} + +void WowConnection::DoStreamReads() { + uint32_t startTime = OsGetAsyncTimeMsPrecise(); + uint8_t buf[4096]; + + auto wsState = static_cast(this->m_event); + if (!wsState) { + return; + } + + while (1) { + int32_t bytesRead = wsState->recvBuf.read(buf, sizeof(buf)); + + if (bytesRead <= 0) { + break; + } + + this->AcquireResponseRef(); + this->m_lock.Leave(); + + if (this->m_response) { + this->m_response->WCDataReady(this, OsGetAsyncTimeMs(), buf, bytesRead); + } + + this->m_lock.Enter(); + this->ReleaseResponseRef(); + + if (this->GetState() == WOWC_DISCONNECTING || (OsGetAsyncTimeMsPrecise() - startTime) >= 5) { + return; + } + } +} + +void WowConnection::DoWrites() { + this->AddRef(); + + this->m_lock.Enter(); + + if (this->m_connState == WOWC_CONNECTING) { + this->CheckConnect(); + } + + this->m_lock.Leave(); + + this->Release(); +} + +void WowConnection::FreeSendNode(SENDNODE* sn) { + SMemFree(sn, __FILE__, __LINE__, 0x0); +} + +WOW_CONN_STATE WowConnection::GetState() { + return this->m_connState; +} + +void WowConnection::Init(WowConnectionResponse* response, void (*func)(void)) { + SInterlockedIncrement(&WowConnection::s_numWowConnections); + + this->m_refCount = 1; + this->m_responseRef = 0; + this->m_sendDepth = 0; + this->m_sendDepthBytes = 0; + this->m_maxSendDepth = 100000; + this->m_connState = WOWC_UNINITIALIZED; + this->m_response = response; + this->m_connectAddress = 0; + this->m_connectPort = 0; + this->m_serviceFlags = 0x0; + this->m_serviceCount = 0; + this->m_readBuffer = nullptr; + this->m_readBytes = 0; + this->m_readBufferSize = 0; + this->m_event = nullptr; + this->m_encrypt = false; + + this->SetState(WOWC_INITIALIZED); + this->m_type = WOWC_TYPE_MESSAGES; +} + +WowConnection::SENDNODE* WowConnection::NewSendNode(void* data, int32_t size, bool raw) { + uint32_t allocsize = size + sizeof(SENDNODE) + 3; + + auto m = SMemAlloc(allocsize, __FILE__, __LINE__, 0x0); + auto buf = &static_cast(m)[sizeof(SENDNODE)]; + auto sn = new (m) SENDNODE(data, size, buf, raw); + + sn->allocsize = allocsize; + + return sn; +} + +void WowConnection::Release() { + if (SInterlockedDecrement(&this->m_refCount) <= 0) { + // Clean up WebSocket state + auto wsState = static_cast(this->m_event); + if (wsState) { + if (wsState->ws > 0) { + emscripten_websocket_close(wsState->ws, 1000, nullptr); + emscripten_websocket_delete(wsState->ws); + } + wsState->recvBuf.clear(); + delete wsState; + this->m_event = nullptr; + } + + if (WowConnection::s_network) { + WowConnection::s_network->Delete(this); + } else { + delete this; + } + } +} + +void WowConnection::ReleaseResponseRef() { + // Simplified for single-threaded web - no locking needed + this->m_responseRef--; +} + +WC_SEND_RESULT WowConnection::Send(CDataStore* msg, int32_t a3) { + uint8_t* data; + msg->GetDataInSitu(reinterpret_cast(data), msg->Size()); + + WowConnection::s_countTotalBytes += msg->Size(); + + this->m_lock.Enter(); + + if (msg->Size() == 0 || this->m_connState != WOWC_CONNECTED) { + this->m_lock.Leave(); + return WC_SEND_ERROR; + } + + auto wsState = static_cast(this->m_event); + if (!wsState || wsState->ws <= 0) { + this->m_lock.Leave(); + return WC_SEND_ERROR; + } + + // Build framed packet with header + auto sn = this->NewSendNode(data, msg->Size(), false); + + if (this->m_encrypt) { + auto bufSize = std::min(sn->size, sn->size + this->uint375 - sn->datasize); + SARC4ProcessBuffer(sn->data, bufSize, &this->m_sendKey, &this->m_sendKey); + } + + // Send via WebSocket binary + auto result = emscripten_websocket_send_binary(wsState->ws, sn->data, sn->size); + + this->FreeSendNode(sn); + + this->m_lock.Leave(); + + if (result == EMSCRIPTEN_RESULT_SUCCESS) { + return WC_SEND_SENT; + } + + return WC_SEND_ERROR; +} + +WC_SEND_RESULT WowConnection::SendRaw(uint8_t* data, int32_t len, bool a4) { + WowConnection::s_countTotalBytes += len; + + this->m_lock.Enter(); + + if (len > 0 && this->m_connState == WOWC_CONNECTED) { + auto wsState = static_cast(this->m_event); + if (wsState && wsState->ws > 0) { + auto result = emscripten_websocket_send_binary(wsState->ws, data, len); + this->m_lock.Leave(); + + if (result == EMSCRIPTEN_RESULT_SUCCESS) { + return WC_SEND_SENT; + } + + return WC_SEND_ERROR; + } + } + + this->m_lock.Leave(); + + return WC_SEND_ERROR; +} + +void WowConnection::SetEncryption(bool enabled) { + this->m_lock.Enter(); + + this->m_encrypt = enabled; + + SARC4PrepareKey(this->m_sendKeyInit, sizeof(this->m_sendKeyInit), &this->m_sendKey); + SARC4PrepareKey(this->m_receiveKeyInit, sizeof(this->m_receiveKeyInit), &this->m_receiveKey); + + SARC4ProcessBuffer(s_arc4drop1024, sizeof(s_arc4drop1024), &this->m_sendKey, &this->m_sendKey); + SARC4ProcessBuffer(s_arc4drop1024, sizeof(s_arc4drop1024), &this->m_receiveKey, &this->m_receiveKey); + + this->m_lock.Leave(); +} + +void WowConnection::SetEncryptionKey(const uint8_t* key, uint8_t keyLen, uint8_t a4, const uint8_t* seedData, uint8_t seedLen) { + if (!seedData) { + seedData = s_arc4seed; + seedLen = sizeof(s_arc4seed); + } + + const uint8_t* seeds[] = { + seedData, + &seedData[seedLen / 2] + }; + + HMAC_SHA1(seeds[a4], seedLen / 2, key, keyLen, this->m_sendKeyInit); + HMAC_SHA1(seeds[a4 ^ 1], seedLen / 2, key, keyLen, this->m_receiveKeyInit); + + SARC4PrepareKey(this->m_sendKeyInit, sizeof(this->m_sendKeyInit), &this->m_sendKey); + SARC4PrepareKey(this->m_receiveKeyInit, sizeof(this->m_receiveKeyInit), &this->m_receiveKey); + + SARC4ProcessBuffer(s_arc4drop1024, sizeof(s_arc4drop1024), &this->m_sendKey, &this->m_sendKey); + SARC4ProcessBuffer(s_arc4drop1024, sizeof(s_arc4drop1024), &this->m_receiveKey, &this->m_receiveKey); +} + +void WowConnection::SetState(WOW_CONN_STATE state) { + WOW_CONN_STATE oldState = this->m_connState; + this->m_connState = state; + + if (WowConnection::s_network) { + WowConnection::s_network->PlatformChangeState(this, oldState); + } +} + +void WowConnection::SetType(WOWC_TYPE type) { + this->m_lock.Enter(); + this->m_type = type; + this->m_lock.Leave(); +} + +void WowConnection::StartConnect() { + auto wsState = static_cast(this->m_event); + if (!wsState) { + wsState = new WsState(); + this->m_event = wsState; + } + + // Close existing WebSocket if reconnecting + if (wsState->ws > 0) { + if (this->m_netlink.IsLinked()) { + WowConnection::s_network->Remove(this); + } + + emscripten_websocket_close(wsState->ws, 1000, "reconnecting"); + emscripten_websocket_delete(wsState->ws); + wsState->ws = 0; + wsState->recvBuf.clear(); + wsState->connectPending = false; + wsState->closePending = false; + wsState->errorPending = false; + this->m_sock = -1; + } + + this->m_lock.Enter(); + + // Build proxy WebSocket URL from the page origin + char url[512]; + EM_ASM({ + var proto = (location.protocol === 'https:') ? 'wss:' : 'ws:'; + var url = proto + '//' + location.host + '/proxy/tcp/' + UTF8ToString($0) + '/' + $1; + stringToUTF8(url, $2, $3); + }, wsState->connectHost, this->m_connectPort, url, sizeof(url)); + + EmscriptenWebSocketCreateAttributes attr; + emscripten_websocket_init_create_attributes(&attr); + attr.url = url; + + wsState->ws = emscripten_websocket_new(&attr); + + if (wsState->ws <= 0) { + this->SetState(WOWC_ERROR); + this->m_lock.Leave(); + return; + } + + this->m_sock = wsState->ws; + + // Register WebSocket callbacks + emscripten_websocket_set_onopen_callback(wsState->ws, this, ws_onopen); + emscripten_websocket_set_onmessage_callback(wsState->ws, this, ws_onmessage); + emscripten_websocket_set_onclose_callback(wsState->ws, this, ws_onclose); + emscripten_websocket_set_onerror_callback(wsState->ws, this, ws_onerror); + + if (!this->m_netlink.IsLinked()) { + WowConnection::s_network->Add(this); + } + + this->SetState(WOWC_CONNECTING); + + this->m_lock.Leave(); +} diff --git a/src/net/connection/web/WowConnectionNet.cpp b/src/net/connection/web/WowConnectionNet.cpp new file mode 100644 index 0000000..d5ebcb3 --- /dev/null +++ b/src/net/connection/web/WowConnectionNet.cpp @@ -0,0 +1,174 @@ +#include "net/connection/WowConnectionNet.hpp" +#include "net/connection/WowConnection.hpp" +#include "net/connection/web/WsState.hpp" +#include +#include + +// Periodic callback that polls all connections for WebSocket events. +// Replaces the native select()/thread model with an interval-based poll. +static void webNetworkPoll(void* userData) { + auto net = static_cast(userData); + + // Copy connection pointers to a local array so we can safely iterate + // even if Service() modifies the connection list + WowConnection* conns[64]; + int32_t count = 0; + + net->m_connectionsLock.Enter(); + + for (auto conn = net->m_connections.Head(); conn && count < 64; conn = net->m_connections.Link(conn)->Next()) { + auto wsState = static_cast(conn->m_event); + if (!wsState) { + continue; + } + + uint32_t flags = 0; + + switch (conn->m_connState) { + case WOWC_CONNECTING: + if (wsState->connectPending || wsState->errorPending) { + flags |= 0x1; // DoWrites -> CheckConnect + } + break; + + case WOWC_CONNECTED: + if (wsState->recvBuf.size > 0) { + flags |= 0x2; // DoReads + } + if (wsState->closePending || wsState->errorPending) { + flags |= 0x8; // DoDisconnect + } + break; + + case WOWC_DISCONNECTING: + flags |= 0x8; // DoDisconnect + break; + + default: + break; + } + + if (flags) { + conn->AddRef(); + conn->m_serviceFlags = flags; + conns[count++] = conn; + } + } + + net->m_connectionsLock.Leave(); + + // Process connections outside the lock + for (int32_t i = 0; i < count; i++) { + auto conn = conns[i]; + uint32_t flags = conn->m_serviceFlags; + conn->m_serviceFlags = 0; + + net->Service(conn, flags); + + conn->Release(); + } +} + +void WowConnectionNet::Add(WowConnection* connection) { + this->m_connectionsLock.Enter(); + + if (!this->m_connections.IsLinked(connection)) { + this->m_connections.LinkToTail(connection); + this->PlatformAdd(connection); + } + + this->m_connectionsLock.Leave(); +} + +void WowConnectionNet::Delete(WowConnection* connection) { + this->m_connectionsLock.Enter(); + + if (connection->m_refCount == 0) { + delete connection; + } + + this->m_connectionsLock.Leave(); +} + +void WowConnectionNet::PlatformAdd(WowConnection* connection) { + // No TCP_NODELAY or fd_set management needed on web +} + +void WowConnectionNet::PlatformChangeState(WowConnection* connection, WOW_CONN_STATE state) { + // No notification needed - polling handles state changes +} + +void WowConnectionNet::PlatformInit(bool useEngine) { +} + +void WowConnectionNet::PlatformRemove(WowConnection* connection) { +} + +void WowConnectionNet::PlatformRun() { + // Not used on web - polling is driven by emscripten_set_interval +} + +void WowConnectionNet::PlatformWorkerReady() { +} + +void WowConnectionNet::Remove(WowConnection* connection) { + this->m_connectionsLock.Enter(); + + if (this->m_connections.IsLinked(connection)) { + this->m_connections.UnlinkNode(connection); + } + + this->PlatformRemove(connection); + + this->m_connectionsLock.Leave(); +} + +void WowConnectionNet::Run() { + // Not used on web +} + +void WowConnectionNet::RunWorker(int32_t id) { + // Not used on web - no worker threads +} + +void WowConnectionNet::Service(WowConnection* connection, uint32_t serviceFlags) { + while (serviceFlags) { + if (serviceFlags & 0x1) { + connection->DoWrites(); + } + + if (serviceFlags & 0x2) { + connection->DoReads(); + } + + if (serviceFlags & 0x4) { + connection->DoExceptions(); + } + + if (serviceFlags & 0x8) { + connection->DoDisconnect(); + } + + this->m_connectionsLock.Enter(); + + serviceFlags = connection->m_serviceFlags; + connection->m_serviceFlags = 0; + + this->m_connectionsLock.Leave(); + } +} + +void WowConnectionNet::SignalWorker(WowConnection* connection, uint32_t flags) { + // On web, service directly instead of dispatching to worker threads + connection->AddRef(); + connection->m_serviceFlags = flags; + + this->Service(connection, flags); + + connection->Release(); +} + +void WowConnectionNet::Start() { + // Use a periodic interval instead of threads to poll connections + emscripten_set_interval(webNetworkPoll, 16.0, this); +} diff --git a/src/net/connection/web/WsState.hpp b/src/net/connection/web/WsState.hpp new file mode 100644 index 0000000..ec43f01 --- /dev/null +++ b/src/net/connection/web/WsState.hpp @@ -0,0 +1,54 @@ +#ifndef NET_CONNECTION_WEB_WS_STATE_HPP +#define NET_CONNECTION_WEB_WS_STATE_HPP + +#include +#include +#include +#include +#include + +struct WsRecvBuffer { + uint8_t* data = nullptr; + int32_t size = 0; + int32_t capacity = 0; + + void append(const uint8_t* src, int32_t len) { + if (size + len > capacity) { + int32_t newCap = std::max(capacity ? capacity * 2 : 4096, size + len); + data = static_cast(realloc(data, newCap)); + capacity = newCap; + } + memcpy(data + size, src, len); + size += len; + } + + int32_t read(uint8_t* dst, int32_t maxLen) { + int32_t toRead = std::min(maxLen, size); + if (toRead > 0) { + memcpy(dst, data, toRead); + size -= toRead; + if (size > 0) { + memmove(data, data + toRead, size); + } + } + return toRead; + } + + void clear() { + free(data); + data = nullptr; + size = 0; + capacity = 0; + } +}; + +struct WsState { + EMSCRIPTEN_WEBSOCKET_T ws = 0; + WsRecvBuffer recvBuf; + bool connectPending = false; + bool closePending = false; + bool errorPending = false; + char connectHost[256] = {0}; +}; + +#endif diff --git a/src/sound/CMakeLists.txt b/src/sound/CMakeLists.txt index 44b9ad7..cc57112 100644 --- a/src/sound/CMakeLists.txt +++ b/src/sound/CMakeLists.txt @@ -1,5 +1,16 @@ file(GLOB PRIVATE_SOURCES "*.cpp") +if(WHOA_SYSTEM_WEB) + # For web builds, exclude FMOD-dependent files and use stubs + list(REMOVE_ITEM PRIVATE_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/SESound.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/SESoundInternal.cpp + ) + + file(GLOB WEB_SOURCES "web/*.cpp") + list(APPEND PRIVATE_SOURCES ${WEB_SOURCES}) +endif() + add_library(sound STATIC ${PRIVATE_SOURCES} ) @@ -13,6 +24,11 @@ target_link_libraries(sound PRIVATE ui util - PUBLIC - fmod ) + +if(NOT WHOA_SYSTEM_WEB) + target_link_libraries(sound + PUBLIC + fmod + ) +endif() diff --git a/src/sound/SESound.hpp b/src/sound/SESound.hpp index 06b3725..35b514d 100644 --- a/src/sound/SESound.hpp +++ b/src/sound/SESound.hpp @@ -5,10 +5,12 @@ #include "sound/SESoundInternal.hpp" #include "sound/SEUserData.hpp" #include -#include #include #include +#if !defined(WHOA_SYSTEM_WEB) +#include + struct SOUND_INTERNAL_LOOKUP : TSHashObject { SESoundInternal* m_internal; }; @@ -65,4 +67,40 @@ class SESound { SESoundInternal* m_internal = nullptr; }; +#else // WHOA_SYSTEM_WEB + +// Stub SESound class for web builds - sound is not supported +class SESound { + public: + // Public static variables + static TSGrowableArray s_ChannelGroups; + static int32_t s_Initialized; + + // Public static functions + static void* CreateSoundGroup(const char* name, int32_t maxAudible); + static SEChannelGroup* GetChannelGroup(const char* name, bool create, bool createInMaster); + static float GetChannelGroupVolume(const char* name); + static int32_t Heartbeat(const void* data, void* param); + static void Init(int32_t maxChannels, int32_t (*a2), int32_t enableReverb, int32_t enableSoftwareHRTF, int32_t* numChannels, int32_t* outputDriverIndex, const char* outputDriverName, void (*a8), int32_t a9); + static int32_t IsInitialized(); + static void MuteChannelGroup(const char* name, bool mute); + static void SetChannelGroupVolume(const char* name, float volume); + static void SetMasterVolume(float volume); + + // Public member functions + void CompleteLoad(); + SEUserData* GetUserData(); + bool IsPlaying(); + int32_t Load(const char* filename, int32_t a3, void* soundGroup1, void* soundGroup2, bool a6, bool a7, uint32_t a8, int32_t a9, uint32_t a10); + void Play(); + void SetChannelGroup(const char* name, bool inMaster); + void SetFadeInTime(float fadeInTime); + void SetFadeOutTime(float fadeOutTime); + void SetUserData(SEUserData* userData); + void SetVolume(float volume); + void StopOrFadeOut(int32_t stop, float fadeOutTime); +}; + +#endif // WHOA_SYSTEM_WEB + #endif diff --git a/src/sound/SESoundInternal.hpp b/src/sound/SESoundInternal.hpp index 1f5d16f..6bab9fb 100644 --- a/src/sound/SESoundInternal.hpp +++ b/src/sound/SESoundInternal.hpp @@ -2,13 +2,18 @@ #define SOUND_SE_SOUND_INTERNAL_HPP #include -#include #include +#if !defined(WHOA_SYSTEM_WEB) +#include +#endif + class SESound; class SEUserData; class SFile; +#if !defined(WHOA_SYSTEM_WEB) + class SoundCacheNode : public TSLinkedNode { public: // Member variables @@ -84,4 +89,6 @@ class SEStreamedSound : public SESoundInternal { // TODO }; +#endif // !defined(WHOA_SYSTEM_WEB) + #endif diff --git a/src/sound/SOUNDKITDEF.hpp b/src/sound/SOUNDKITDEF.hpp index dc3f82b..f6434a8 100644 --- a/src/sound/SOUNDKITDEF.hpp +++ b/src/sound/SOUNDKITDEF.hpp @@ -4,9 +4,11 @@ #include "db/Db.hpp" #include +#if !defined(WHOA_SYSTEM_WEB) namespace FMOD { class SoundGroup; } +#endif class SOUNDKITDEF { public: @@ -20,8 +22,13 @@ class SOUNDKITDEF { int32_t fileCount = 0; int32_t selectedIndex = -1; // TODO: 0x48 - 0x94 +#if !defined(WHOA_SYSTEM_WEB) FMOD::SoundGroup* soundGroup1; FMOD::SoundGroup* soundGroup2; +#else + void* soundGroup1; + void* soundGroup2; +#endif // TODO: 0xA0 - 0xA4 int32_t advancedID = 0; SoundEntriesAdvancedRec* advanced; diff --git a/src/sound/web/SESound.cpp b/src/sound/web/SESound.cpp new file mode 100644 index 0000000..2f7f439 --- /dev/null +++ b/src/sound/web/SESound.cpp @@ -0,0 +1,77 @@ +#include "sound/SESound.hpp" +#include "sound/SEChannelGroup.hpp" + +// Web stub implementation - sound is not supported on web platform + +TSGrowableArray SESound::s_ChannelGroups; +int32_t SESound::s_Initialized = 0; + +void* SESound::CreateSoundGroup(const char* name, int32_t maxAudible) { + return nullptr; +} + +SEChannelGroup* SESound::GetChannelGroup(const char* name, bool create, bool createInMaster) { + return nullptr; +} + +float SESound::GetChannelGroupVolume(const char* name) { + return 0.0f; +} + +int32_t SESound::Heartbeat(const void* data, void* param) { + return 1; +} + +void SESound::Init(int32_t maxChannels, int32_t (*a2), int32_t enableReverb, int32_t enableSoftwareHRTF, int32_t* numChannels, int32_t* outputDriverIndex, const char* outputDriverName, void (*a8), int32_t a9) { + // Sound not supported on web - leave as not initialized + SESound::s_Initialized = 0; +} + +int32_t SESound::IsInitialized() { + return 0; +} + +void SESound::MuteChannelGroup(const char* name, bool mute) { +} + +void SESound::SetChannelGroupVolume(const char* name, float volume) { +} + +void SESound::SetMasterVolume(float volume) { +} + +void SESound::CompleteLoad() { +} + +SEUserData* SESound::GetUserData() { + return nullptr; +} + +bool SESound::IsPlaying() { + return false; +} + +int32_t SESound::Load(const char* filename, int32_t a3, void* soundGroup1, void* soundGroup2, bool a6, bool a7, uint32_t a8, int32_t a9, uint32_t a10) { + return 0; +} + +void SESound::Play() { +} + +void SESound::SetChannelGroup(const char* name, bool inMaster) { +} + +void SESound::SetFadeInTime(float fadeInTime) { +} + +void SESound::SetFadeOutTime(float fadeOutTime) { +} + +void SESound::SetUserData(SEUserData* userData) { +} + +void SESound::SetVolume(float volume) { +} + +void SESound::StopOrFadeOut(int32_t stop, float fadeOutTime) { +} diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index a0696b6..c1fbb9c 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -39,3 +39,13 @@ if(WHOA_SYSTEM_LINUX OR WHOA_SYSTEM_MAC) Threads::Threads ) endif() + +if(WHOA_SYSTEM_WEB) + set(FETCHFS_LIBRARY ${CMAKE_CURRENT_SOURCE_DIR}/web/library_fetchfs.js) + target_link_options(util + PUBLIC + --js-library ${FETCHFS_LIBRARY} + ) + # Ensure rebuild when JS library changes + set_property(TARGET util APPEND PROPERTY LINK_DEPENDS ${FETCHFS_LIBRARY}) +endif() diff --git a/src/util/web/library_fetchfs.js b/src/util/web/library_fetchfs.js new file mode 100644 index 0000000..b50d888 --- /dev/null +++ b/src/util/web/library_fetchfs.js @@ -0,0 +1,150 @@ +/** + * FETCHFS - Lazy file loading for Emscripten via the Fetch API. + * Patches FS.open to fetch files on-demand when they don't exist locally. + */ + +addToLibrary({ + $FETCHFS__deps: ['$FS'], + $FETCHFS__postset: 'FETCHFS.staticInit();', + $FETCHFS: { + baseUrl: './data/', + initialized: false, + originalOpen: null, + + staticInit: function() { + if (typeof FS !== 'undefined' && FS.open) { + FETCHFS.originalOpen = FS.open; + FS.open = FETCHFS.patchedOpen; + FETCHFS.initialized = true; + } + }, + + setBaseUrl: function(url) { + FETCHFS.baseUrl = url; + }, + + // Build the fetch URL for a given path + buildUrl: function(path) { + // Remove leading slash and normalize backslashes + var cleanPath = path.replace(/^\/+/, '').replace(/\\/g, '/'); + return FETCHFS.baseUrl + cleanPath; + }, + + // Ensure parent directories exist in MEMFS + ensureParentDirs: function(path) { + var parts = path.split('/').filter(function(p) { return p.length > 0; }); + var current = ''; + + for (var i = 0; i < parts.length - 1; i++) { + current += '/' + parts[i]; + try { + FS.mkdir(current); + } catch (e) { + // Directory might already exist (EEXIST) - that's fine + if (e.errno !== 20) { + // Some other error, but we can try to continue + } + } + } + }, + + // Synchronously fetch file contents using XMLHttpRequest + fetchFileSync: function(path) { + var url = FETCHFS.buildUrl(path); + + try { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, false); // false = synchronous + // Use overrideMimeType to get binary data (can't use responseType with sync XHR) + xhr.overrideMimeType('text/plain; charset=x-user-defined'); + xhr.send(null); + + if (xhr.status === 200 || xhr.status === 0) { // 0 for file:// protocol + // Convert the response string to Uint8Array + var text = xhr.responseText; + var data = new Uint8Array(text.length); + for (var i = 0; i < text.length; i++) { + data[i] = text.charCodeAt(i) & 0xff; + } + return data; + } else { + return null; + } + } catch (err) { + return null; + } + }, + + // Normalize a path (resolve . and .., make absolute) + normalizePath: function(path) { + // Convert backslashes to forward slashes (Windows-style paths) + path = path.replace(/\\/g, '/'); + // Make absolute if relative + if (path[0] !== '/') { + path = FS.cwd() + '/' + path; + } + // Split and resolve . and .. + var parts = path.split('/'); + var result = []; + for (var i = 0; i < parts.length; i++) { + var part = parts[i]; + if (part === '' || part === '.') continue; + if (part === '..') { + if (result.length > 0) result.pop(); + } else { + result.push(part); + } + } + return '/' + result.join('/'); + }, + + // Patched open that fetches files on demand + patchedOpen: function(path, flags, mode) { + // If path is not a string (e.g., internal FS node), pass through to original + if (typeof path !== 'string') { + return FETCHFS.originalOpen(path, flags, mode); + } + + // Normalize the path upfront so all operations use consistent paths + var normalizedPath = FETCHFS.normalizePath(path); + + // Try original open first with normalized path + try { + var result = FETCHFS.originalOpen(normalizedPath, flags, mode); + return result; + } catch (e) { + // Only handle ENOENT (file/directory not found) + if (e.errno !== 44) { + throw e; + } + } + + // File not found locally - try to fetch it from server + var data = FETCHFS.fetchFileSync(normalizedPath); + + if (data) { + // Ensure parent directories exist + FETCHFS.ensureParentDirs(normalizedPath); + + // Write the fetched data using FS.writeFile which properly initializes MEMFS nodes + try { + FS.writeFile(normalizedPath, data); + } catch (e) { + throw new FS.ErrnoError(44); // ENOENT + } + + // Now the original open should succeed + return FETCHFS.originalOpen(normalizedPath, flags, mode); + } + + // Fetch failed - throw original error + throw new FS.ErrnoError(44); // ENOENT + }, + }, + + // C-callable function to set base URL + fetchfs_set_base_url__deps: ['$FETCHFS'], + fetchfs_set_base_url: function(url) { + FETCHFS.setBaseUrl(UTF8ToString(url)); + }, +}); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a2292d6..9eff2ba 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -44,6 +44,25 @@ if(WHOA_SYSTEM_WIN OR WHOA_SYSTEM_LINUX) ) endif() +if(WHOA_SYSTEM_WEB) + file(GLOB PRIVATE_SOURCES + "Test.cpp" + "gx/*.cpp" + "gx/font/*.cpp" + "util/*.cpp" + ) + + add_executable(WhoaTest ${PRIVATE_SOURCES}) + + target_link_libraries(WhoaTest + PRIVATE + client + event + gx + util + ) +endif() + target_include_directories(WhoaTest PRIVATE ${CMAKE_SOURCE_DIR}/src diff --git a/vendor/CMakeLists.txt b/vendor/CMakeLists.txt index 1920c65..0bfa781 100644 --- a/vendor/CMakeLists.txt +++ b/vendor/CMakeLists.txt @@ -58,7 +58,7 @@ if(WHOA_SYSTEM_WIN) else() set_target_properties(fmod PROPERTIES IMPORTED_IMPLIB ${FMOD_DIR}/win-x86_64/lib/fmod_vc.lib - IMPORTED_LOCATION FMOD_DIR}/win-x86_64/lib/fmod.dll + IMPORTED_LOCATION ${FMOD_DIR}/win-x86_64/lib/fmod.dll INTERFACE_INCLUDE_DIRECTORIES ${FMOD_DIR}/win-x86_64/inc ) install(FILES ${FMOD_DIR}/win-x86_64/lib/fmod.dll DESTINATION "bin") diff --git a/vendor/freetype-2.0.9/src/base/ftobjs.c b/vendor/freetype-2.0.9/src/base/ftobjs.c index e0ffe24..a03b411 100644 --- a/vendor/freetype-2.0.9/src/base/ftobjs.c +++ b/vendor/freetype-2.0.9/src/base/ftobjs.c @@ -1100,7 +1100,7 @@ static FT_Error open_face( FT_Driver driver, FT_Stream stream, - FT_Long face_index, + FT_Int face_index, FT_Int num_params, FT_Parameter* params, FT_Face* aface ) diff --git a/vendor/freetype-2.0.9/src/pcf/pcfdriver.c b/vendor/freetype-2.0.9/src/pcf/pcfdriver.c index 864036b..4ff6559 100644 --- a/vendor/freetype-2.0.9/src/pcf/pcfdriver.c +++ b/vendor/freetype-2.0.9/src/pcf/pcfdriver.c @@ -48,7 +48,7 @@ THE SOFTWARE. #define FT_COMPONENT trace_pcfdriver - static FT_Error + static void PCF_Face_Done( PCF_Face face ) { FT_Memory memory = FT_FACE_MEMORY( face ); @@ -81,8 +81,6 @@ THE SOFTWARE. FREE( face->charset_registry ); FT_TRACE4(( "DONE_FACE!!!\n" )); - - return PCF_Err_Ok; } diff --git a/vendor/freetype-2.0.9/src/sfnt/sfobjs.c b/vendor/freetype-2.0.9/src/sfnt/sfobjs.c index 9d54b76..edbdf79 100644 --- a/vendor/freetype-2.0.9/src/sfnt/sfobjs.c +++ b/vendor/freetype-2.0.9/src/sfnt/sfobjs.c @@ -242,7 +242,7 @@ } /* check that we have a valid TrueType file */ - error = sfnt->load_sfnt_header( face, stream, face_index, &sfnt_header ); + error = sfnt->load_sfnt_header( face, stream, (FT_Long)face_index, &sfnt_header ); if ( error ) goto Exit;