Compare commits

...

5 Commits

Author SHA1 Message Date
Alex Tiernan-Berry
aa575b11fd
Merge 0573235a70 into 06186d1251 2026-02-19 22:24:26 +01:00
fallenoak
06186d1251
feat(ui): call CGCamera::SetupWorldProjection from CGWorldFrame::OnWorldUpdate
Some checks failed
Push / ${{ matrix.build.system_name }} / ${{ matrix.build.build_type }} / ${{ matrix.build.compiler_name }} (map[build_type:Release cc:cl compiler_name:MSVC cxx:cl os:windows-latest system_name:Windows test_path:WhoaTest]) (push) Has been cancelled
Push / ${{ matrix.build.system_name }} / ${{ matrix.build.build_type }} / ${{ matrix.build.compiler_name }} (map[build_type:Release cc:clang compiler_name:Clang cxx:clang++ os:macos-latest system_name:macOS test_path:WhoaTest]) (push) Has been cancelled
Push / ${{ matrix.build.system_name }} / ${{ matrix.build.build_type }} / ${{ matrix.build.compiler_name }} (map[build_type:Release cc:gcc compiler_name:GCC cxx:g++ os:ubuntu-latest system_name:Linux test_path:WhoaTest]) (push) Has been cancelled
2026-02-19 06:56:43 -06:00
Alex Tiernan-Berry
0573235a70
Merge branch 'master' into master 2026-02-06 02:39:00 +00:00
Alex Tiernan-Berry
47a9d80584 Merge branch 'master' of https://github.com/atiernan/whoa 2026-02-06 02:23:57 +00:00
Alex Tiernan-Berry
d4d359acea 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
2026-02-06 02:21:20 +00:00
41 changed files with 1968 additions and 21 deletions

2
.gitignore vendored
View File

@ -3,7 +3,7 @@
.vscode .vscode
.idea .idea
/build /build*
/cmake-build-* /cmake-build-*
/out /out

View File

@ -26,6 +26,7 @@ set(CMAKE_CXX_STANDARD 11)
include(lib/system/cmake/system.cmake) include(lib/system/cmake/system.cmake)
# Some templates abuse offsetof # Some templates abuse offsetof
if(WHOA_SYSTEM_LINUX OR WHOA_SYSTEM_MAC) if(WHOA_SYSTEM_LINUX OR WHOA_SYSTEM_MAC)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-invalid-offsetof") 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) find_package(Threads REQUIRED)
endif() 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 # Library search paths
if(WHOA_SYSTEM_MAC) if(WHOA_SYSTEM_MAC)
set(CMAKE_SKIP_BUILD_RPATH FALSE) set(CMAKE_SKIP_BUILD_RPATH FALSE)

View File

@ -52,6 +52,31 @@ if(WHOA_SYSTEM_LINUX)
) )
endif() 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 target_include_directories(Whoa
PRIVATE PRIVATE
${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/src

55
src/app/web/Whoa.cpp Normal file
View File

@ -0,0 +1,55 @@
#include "client/Client.hpp"
#include "event/Event.hpp"
#include "gx/Device.hpp"
#include <emscripten.h>
#include <emscripten/html5.h>
// 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<uintptr_t>(cssWidth),
static_cast<uintptr_t>(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;
}

144
src/app/web/shell.html Normal file
View File

@ -0,0 +1,144 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Whoa</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #000;
}
#canvas {
width: 100%;
height: 100%;
display: block;
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
font-family: Arial, sans-serif;
font-size: 18px;
text-align: center;
}
#loading.hidden {
display: none;
}
#progress {
width: 300px;
height: 20px;
background: #333;
border-radius: 10px;
margin-top: 20px;
overflow: hidden;
}
#progress-bar {
width: 0%;
height: 100%;
background: linear-gradient(90deg, #4a90d9, #67b26f);
transition: width 0.3s;
}
#error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #f66;
font-family: Arial, sans-serif;
font-size: 16px;
text-align: center;
max-width: 80%;
display: none;
}
</style>
</head>
<body>
<canvas id="canvas" oncontextmenu="event.preventDefault()"></canvas>
<div id="loading">
<div>Loading...</div>
<div id="progress">
<div id="progress-bar"></div>
</div>
<div id="status"></div>
</div>
<div id="error"></div>
<script>
// Check for WebGPU support
if (!navigator.gpu) {
document.getElementById('loading').style.display = 'none';
document.getElementById('error').style.display = 'block';
document.getElementById('error').innerHTML =
'<h2>WebGPU Not Supported</h2>' +
'<p>Your browser does not support WebGPU.</p>' +
'<p>Please use a recent version of Chrome, Edge, or Firefox with WebGPU enabled.</p>';
}
var Module = {
canvas: (function() {
var canvas = document.getElementById('canvas');
// Set canvas size to match window
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
return canvas;
})(),
onRuntimeInitialized: function() {
document.getElementById('loading').classList.add('hidden');
},
setStatus: function(text) {
var statusElement = document.getElementById('status');
if (statusElement) {
statusElement.innerHTML = text;
}
// Parse progress if available
var match = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
if (match) {
var progress = parseInt(match[2]) / parseInt(match[4]) * 100;
document.getElementById('progress-bar').style.width = progress + '%';
}
},
print: function(text) {
console.log(text);
},
printErr: function(text) {
console.error(text);
},
totalDependencies: 0,
monitorRunDependencies: function(left) {
this.totalDependencies = Math.max(this.totalDependencies, left);
if (left) {
Module.setStatus('Loading... (' + (this.totalDependencies - left) + '/' + this.totalDependencies + ')');
} else {
Module.setStatus('');
}
}
};
// Handle window resize
window.addEventListener('resize', function() {
var canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
Module.setStatus('Downloading...');
</script>
{{{ SCRIPT }}}
</body>
</html>

View File

@ -39,6 +39,7 @@ void AsyncFileReadCreateThread(CAsyncQueue* queue, const char* queueName) {
thread->queue = queue; thread->queue = queue;
thread->currentObject = nullptr; thread->currentObject = nullptr;
printf("AsyncFileReadCreateThread: Creating thread '%s' for queue=%p\n", queueName, static_cast<void*>(queue));
SThread::Create(AsyncFileReadThread, thread, thread->thread, const_cast<char*>(queueName), 0); SThread::Create(AsyncFileReadThread, thread, thread->thread, const_cast<char*>(queueName), 0);
} }
@ -147,6 +148,7 @@ uint32_t AsyncFileReadThread(void* param) {
AsyncFileRead::s_queueLock.Leave(); AsyncFileRead::s_queueLock.Leave();
int32_t tries = 10; int32_t tries = 10;
int32_t readSuccess = 0;
while (1) { while (1) {
if (SFile::IsStreamingMode() && object->file) { if (SFile::IsStreamingMode() && object->file) {
// TODO // TODO
@ -154,6 +156,7 @@ uint32_t AsyncFileReadThread(void* param) {
} }
if (SFile::Read(object->file, object->buffer, object->size, nullptr, nullptr, nullptr)) { if (SFile::Read(object->file, object->buffer, object->size, nullptr, nullptr, nullptr)) {
readSuccess = 1;
break; break;
} }

View File

@ -24,6 +24,13 @@ if(WHOA_SYSTEM_LINUX)
list(APPEND PRIVATE_SOURCES ${LINUX_SOURCES}) list(APPEND PRIVATE_SOURCES ${LINUX_SOURCES})
endif() endif()
if(WHOA_SYSTEM_WEB)
file(GLOB WEB_SOURCES
"gui/web/*.cpp"
)
list(APPEND PRIVATE_SOURCES ${WEB_SOURCES})
endif()
add_library(client STATIC add_library(client STATIC
${PRIVATE_SOURCES} ${PRIVATE_SOURCES}
) )

View File

@ -317,7 +317,7 @@ void ClientServices::InitLoginServerCVars(int32_t force, const char* locale) {
"realmList", "realmList",
"Address of realm list server", "Address of realm list server",
0x0, 0x0,
"us.logon.worldofwarcraft.com:3724", "logon.chromiecraft.com:3724",
nullptr, nullptr,
NET, NET,
false, false,

View File

@ -0,0 +1,36 @@
#include "client/gui/OsGui.hpp"
#include <emscripten/html5.h>
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
}

View File

@ -22,6 +22,13 @@ if(WHOA_SYSTEM_LINUX)
list(APPEND PRIVATE_SOURCES ${LINUX_SOURCES}) list(APPEND PRIVATE_SOURCES ${LINUX_SOURCES})
endif() endif()
if(WHOA_SYSTEM_WEB)
file(GLOB WEB_SOURCES
"web/*.cpp"
)
list(APPEND PRIVATE_SOURCES ${WEB_SOURCES})
endif()
add_library(event STATIC add_library(event STATIC
${PRIVATE_SOURCES} ${PRIVATE_SOURCES}
) )

View File

@ -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

View File

@ -53,6 +53,10 @@ void EventPostClose();
void EventPostCloseEx(HEVENTCONTEXT contextHandle); void EventPostCloseEx(HEVENTCONTEXT contextHandle);
#if defined(WHOA_SYSTEM_WEB)
void EventProcessFrame();
#endif
void EventRegister(EVENTID id, int32_t (*handler)(const void*, void*)); void EventRegister(EVENTID id, int32_t (*handler)(const void*, void*));
void EventRegisterEx(EVENTID id, int32_t (*handler)(const void*, void*), void* param, float priority); void EventRegisterEx(EVENTID id, int32_t (*handler)(const void*, void*), void* param, float priority);

View File

@ -632,6 +632,7 @@ const char* KeyCodeToString(KEY key) {
return "UNKNOWN"; return "UNKNOWN";
} }
#if !defined(WHOA_SYSTEM_WEB)
void OsInputInitialize() { void OsInputInitialize() {
#if defined(WHOA_SYSTEM_WIN) #if defined(WHOA_SYSTEM_WIN)
Input::s_numlockState = GetAsyncKeyState(144); Input::s_numlockState = GetAsyncKeyState(144);
@ -656,6 +657,7 @@ bool OsInputIsUsingCocoaEventLoop() {
return true; return true;
} }
#endif
void OsInputPostEvent(OSINPUT id, int32_t param0, int32_t param1, int32_t param2, int32_t param3) { void OsInputPostEvent(OSINPUT id, int32_t param0, int32_t param1, int32_t param2, int32_t param3) {
// TODO // TODO

View File

@ -146,6 +146,22 @@ void IEvtSchedulerProcess() {
// dword_141B3C8 = 0; // dword_141B3C8 = 0;
} }
#endif #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() { void IEvtSchedulerShutdown() {

288
src/event/web/Input.cpp Normal file
View File

@ -0,0 +1,288 @@
#include "event/Input.hpp"
#include <emscripten/html5.h>
#include <cstring>
// 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<int32_t>(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<int32_t>(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<int32_t>(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<int32_t>(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<int32_t>(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<int32_t>(-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;
}

View File

@ -7,6 +7,8 @@
#include <storm/Error.hpp> #include <storm/Error.hpp>
#include <storm/Memory.hpp> #include <storm/Memory.hpp>
#include <algorithm> #include <algorithm>
#include <cstdarg>
#include <cstdio>
#include <cstring> #include <cstring>
#include <limits> #include <limits>
#include <new> #include <new>
@ -179,7 +181,16 @@ int32_t CGxDevice::GLLAdapterMonitorModes(TSGrowableArray<CGxMonitorMode>& monit
#endif #endif
void CGxDevice::Log(const char* format, ...) { 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) { void CGxDevice::Log(const CGxFormat& format) {

View File

@ -156,7 +156,8 @@ void FillInSolidTexture(const CImVector& color, CTexture* texture) {
gxTexFlags, gxTexFlags,
userArg, userArg,
GxuUpdateSingleColorTexture, GxuUpdateSingleColorTexture,
GxTex_Argb8888 GxTex_Argb8888,
"SolidColor"
); );
if (color.a < 0xFE) { if (color.a < 0xFE) {
@ -252,7 +253,7 @@ int32_t GxTexCreate(const CGxTexParms& parms, CGxTex*& texId) {
parms.flags, parms.flags,
parms.userArg, parms.userArg,
parms.userFunc, parms.userFunc,
"", parms.name,
texId texId
); );
} }
@ -329,7 +330,7 @@ void TextureFreeGxTex(CGxTex* texId) {
GxTexDestroy(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; CGxTexParms gxTexParms;
gxTexParms.height = height; gxTexParms.height = height;
@ -342,6 +343,7 @@ CGxTex* TextureAllocGxTex(EGxTexTarget target, uint32_t width, uint32_t height,
gxTexParms.userFunc = userFunc; gxTexParms.userFunc = userFunc;
gxTexParms.flags = flags; gxTexParms.flags = flags;
gxTexParms.flags.m_generateMipMaps = 0; gxTexParms.flags.m_generateMipMaps = 0;
gxTexParms.name = name ? name : "";
CGxTexParms gxTexParms2; CGxTexParms gxTexParms2;
@ -718,7 +720,8 @@ int32_t PumpBlpTextureAsync(CTexture* texture, void* buf) {
texture->gxTexFlags, texture->gxTexFlags,
texture, texture,
&UpdateBlpTextureAsync, &UpdateBlpTextureAsync,
texture->dataFormat texture->dataFormat,
texture->filename
); );
texture->gxTex = gxTex; 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; 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->dataFormat = dataFormat;
texture->gxWidth = width; texture->gxWidth = width;
texture->gxHeight = height; texture->gxHeight = height;

View File

@ -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); 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); MipBits* TextureAllocMippedImg(PIXEL_FORMAT pixelFormat, uint32_t width, uint32_t height);

View File

@ -11,7 +11,7 @@
#include <ApplicationServices/ApplicationServices.h> #include <ApplicationServices/ApplicationServices.h>
#endif #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 { struct Rect {
int16_t top; int16_t top;
int16_t left; int16_t left;
@ -20,7 +20,7 @@ struct Rect {
}; };
#endif #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 { typedef struct tagRECT {
int32_t left; int32_t left;
int32_t top; int32_t top;

View File

@ -26,6 +26,7 @@ class CGxPool : public TSLinkedNode<CGxPool> {
, m_usage(usage) , m_usage(usage)
, m_size(size) , m_size(size)
, m_apiSpecific(nullptr) , m_apiSpecific(nullptr)
, m_mem(nullptr)
, unk1C(0) , unk1C(0)
, m_hint(hint) , m_hint(hint)
, m_name(name) , m_name(name)

View File

@ -1,6 +1,7 @@
#include "gx/texture/CGxTex.hpp" #include "gx/texture/CGxTex.hpp"
#include "gx/Gx.hpp" #include "gx/Gx.hpp"
#include <algorithm> #include <algorithm>
#include <cstring>
CGxTexFlags::CGxTexFlags(EGxTexFilter filter, uint32_t wrapU, uint32_t wrapV, uint32_t force, uint32_t generateMipMaps, uint32_t renderTarget, uint32_t maxAnisotropy) { 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; 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_needsFlagUpdate = 1;
this->m_needsCreation = 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 // TODO remaining constructor logic
} }

View File

@ -39,6 +39,7 @@ class CGxTexParms {
CGxTexFlags flags = CGxTexFlags(GxTex_Linear, 0, 0, 0, 0, 0, 1); CGxTexFlags flags = CGxTexFlags(GxTex_Linear, 0, 0, 0, 0, 0, 1);
void* userArg; void* userArg;
void (*userFunc)(EGxTexCommand, uint32_t, uint32_t, uint32_t, uint32_t, void*, uint32_t&, const void*&); void (*userFunc)(EGxTexCommand, uint32_t, uint32_t, uint32_t, uint32_t, void*, uint32_t&, const void*&);
const char* name = "";
}; };
class CGxTex { class CGxTex {
@ -61,6 +62,7 @@ class CGxTex {
uint8_t m_needsUpdate; uint8_t m_needsUpdate;
uint8_t m_needsCreation; uint8_t m_needsCreation;
uint8_t m_needsFlagUpdate; uint8_t m_needsFlagUpdate;
char m_name[260];
// Member functions // 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*); 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*);

View File

@ -20,6 +20,17 @@ if(WHOA_SYSTEM_MAC OR WHOA_SYSTEM_LINUX)
list(APPEND PRIVATE_SOURCES ${BSD_SOURCES}) list(APPEND PRIVATE_SOURCES ${BSD_SOURCES})
endif() 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 add_library(net STATIC
${PRIVATE_SOURCES} ${PRIVATE_SOURCES}
) )
@ -46,3 +57,10 @@ if(WHOA_SYSTEM_WIN)
wsock32 wsock32
) )
endif() endif()
if(WHOA_SYSTEM_WEB)
target_link_options(net
PUBLIC
-lwebsocket.js
)
endif()

View File

@ -16,6 +16,10 @@
#include <winsock2.h> #include <winsock2.h>
#endif #endif
#if defined(WHOA_SYSTEM_WEB)
struct sockaddr_in;
#endif
class CDataStore; class CDataStore;
class WowConnectionNet; class WowConnectionNet;
class WowConnectionResponse; class WowConnectionResponse;

View File

@ -8,6 +8,9 @@ class WowConnection;
class WowConnectionResponse { class WowConnectionResponse {
public: public:
// Virtual destructor
virtual ~WowConnectionResponse() = default;
// Virtual member functions // Virtual member functions
virtual void WCMessageReady(WowConnection* conn, uint32_t timeStamp, CDataStore* msg) = 0; 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; virtual void WCConnected(WowConnection* conn, WowConnection* inbound, uint32_t timeStamp, const NETCONNADDR* addr) = 0;

View File

@ -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 <common/DataStore.hpp>
#include <common/Time.hpp>
#include <storm/Error.hpp>
#include <storm/Memory.hpp>
#include <storm/String.hpp>
#include <algorithm>
#include <cstring>
#include <new>
#include <emscripten.h>
#include <emscripten/websocket.h>
// 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<WowConnection*>(userData);
auto wsState = static_cast<WsState*>(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<WowConnection*>(userData);
auto wsState = static_cast<WsState*>(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<WowConnection*>(userData);
auto wsState = static_cast<WsState*>(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<WowConnection*>(userData);
auto wsState = static_cast<WsState*>(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<WowConnection::SENDNODE>() {
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<uint8_t*>(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<uint8_t*>(&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<WsState*>(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<WsState*>(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<WsState*>(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<uint8_t*>(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<WsState*>(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<uint8_t*>(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<WsState*>(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<uint8_t*>(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<WsState*>(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<void*&>(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<WsState*>(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<WsState*>(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<WsState*>(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();
}

View File

@ -0,0 +1,174 @@
#include "net/connection/WowConnectionNet.hpp"
#include "net/connection/WowConnection.hpp"
#include "net/connection/web/WsState.hpp"
#include <storm/Atomic.hpp>
#include <emscripten.h>
// 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<WowConnectionNet*>(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<WsState*>(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);
}

View File

@ -0,0 +1,54 @@
#ifndef NET_CONNECTION_WEB_WS_STATE_HPP
#define NET_CONNECTION_WEB_WS_STATE_HPP
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <emscripten/websocket.h>
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<uint8_t*>(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

View File

@ -1,5 +1,16 @@
file(GLOB PRIVATE_SOURCES "*.cpp") 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 add_library(sound STATIC
${PRIVATE_SOURCES} ${PRIVATE_SOURCES}
) )
@ -13,6 +24,11 @@ target_link_libraries(sound
PRIVATE PRIVATE
ui ui
util util
)
if(NOT WHOA_SYSTEM_WEB)
target_link_libraries(sound
PUBLIC PUBLIC
fmod fmod
) )
endif()

View File

@ -5,10 +5,12 @@
#include "sound/SESoundInternal.hpp" #include "sound/SESoundInternal.hpp"
#include "sound/SEUserData.hpp" #include "sound/SEUserData.hpp"
#include <cstdint> #include <cstdint>
#include <fmod.hpp>
#include <storm/Hash.hpp> #include <storm/Hash.hpp>
#include <storm/Thread.hpp> #include <storm/Thread.hpp>
#if !defined(WHOA_SYSTEM_WEB)
#include <fmod.hpp>
struct SOUND_INTERNAL_LOOKUP : TSHashObject<SOUND_INTERNAL_LOOKUP, HASHKEY_NONE> { struct SOUND_INTERNAL_LOOKUP : TSHashObject<SOUND_INTERNAL_LOOKUP, HASHKEY_NONE> {
SESoundInternal* m_internal; SESoundInternal* m_internal;
}; };
@ -65,4 +67,40 @@ class SESound {
SESoundInternal* m_internal = nullptr; 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<SEChannelGroup> 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 #endif

View File

@ -2,13 +2,18 @@
#define SOUND_SE_SOUND_INTERNAL_HPP #define SOUND_SE_SOUND_INTERNAL_HPP
#include <storm/List.hpp> #include <storm/List.hpp>
#include <fmod.hpp>
#include <cstdint> #include <cstdint>
#if !defined(WHOA_SYSTEM_WEB)
#include <fmod.hpp>
#endif
class SESound; class SESound;
class SEUserData; class SEUserData;
class SFile; class SFile;
#if !defined(WHOA_SYSTEM_WEB)
class SoundCacheNode : public TSLinkedNode<SoundCacheNode> { class SoundCacheNode : public TSLinkedNode<SoundCacheNode> {
public: public:
// Member variables // Member variables
@ -84,4 +89,6 @@ class SEStreamedSound : public SESoundInternal {
// TODO // TODO
}; };
#endif // !defined(WHOA_SYSTEM_WEB)
#endif #endif

View File

@ -4,9 +4,11 @@
#include "db/Db.hpp" #include "db/Db.hpp"
#include <cstdint> #include <cstdint>
#if !defined(WHOA_SYSTEM_WEB)
namespace FMOD { namespace FMOD {
class SoundGroup; class SoundGroup;
} }
#endif
class SOUNDKITDEF { class SOUNDKITDEF {
public: public:
@ -20,8 +22,13 @@ class SOUNDKITDEF {
int32_t fileCount = 0; int32_t fileCount = 0;
int32_t selectedIndex = -1; int32_t selectedIndex = -1;
// TODO: 0x48 - 0x94 // TODO: 0x48 - 0x94
#if !defined(WHOA_SYSTEM_WEB)
FMOD::SoundGroup* soundGroup1; FMOD::SoundGroup* soundGroup1;
FMOD::SoundGroup* soundGroup2; FMOD::SoundGroup* soundGroup2;
#else
void* soundGroup1;
void* soundGroup2;
#endif
// TODO: 0xA0 - 0xA4 // TODO: 0xA0 - 0xA4
int32_t advancedID = 0; int32_t advancedID = 0;
SoundEntriesAdvancedRec* advanced; SoundEntriesAdvancedRec* advanced;

77
src/sound/web/SESound.cpp Normal file
View File

@ -0,0 +1,77 @@
#include "sound/SESound.hpp"
#include "sound/SEChannelGroup.hpp"
// Web stub implementation - sound is not supported on web platform
TSGrowableArray<SEChannelGroup> 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) {
}

View File

@ -91,4 +91,8 @@ void CGWorldFrame::OnWorldRender() {
void CGWorldFrame::OnWorldUpdate() { void CGWorldFrame::OnWorldUpdate() {
// TODO // TODO
this->m_camera->SetupWorldProjection(this->m_screenRect);
// TODO
} }

View File

@ -40,3 +40,13 @@ if(WHOA_SYSTEM_LINUX OR WHOA_SYSTEM_MAC)
Threads::Threads Threads::Threads
) )
endif() 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()

View File

@ -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));
},
});

View File

@ -45,6 +45,25 @@ if(WHOA_SYSTEM_WIN OR WHOA_SYSTEM_LINUX)
) )
endif() 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 target_include_directories(WhoaTest
PRIVATE PRIVATE
${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/src

View File

@ -58,7 +58,7 @@ if(WHOA_SYSTEM_WIN)
else() else()
set_target_properties(fmod PROPERTIES set_target_properties(fmod PROPERTIES
IMPORTED_IMPLIB ${FMOD_DIR}/win-x86_64/lib/fmod_vc.lib 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 INTERFACE_INCLUDE_DIRECTORIES ${FMOD_DIR}/win-x86_64/inc
) )
install(FILES ${FMOD_DIR}/win-x86_64/lib/fmod.dll DESTINATION "bin") install(FILES ${FMOD_DIR}/win-x86_64/lib/fmod.dll DESTINATION "bin")

View File

@ -1100,7 +1100,7 @@
static FT_Error static FT_Error
open_face( FT_Driver driver, open_face( FT_Driver driver,
FT_Stream stream, FT_Stream stream,
FT_Long face_index, FT_Int face_index,
FT_Int num_params, FT_Int num_params,
FT_Parameter* params, FT_Parameter* params,
FT_Face* aface ) FT_Face* aface )

View File

@ -48,7 +48,7 @@ THE SOFTWARE.
#define FT_COMPONENT trace_pcfdriver #define FT_COMPONENT trace_pcfdriver
static FT_Error static void
PCF_Face_Done( PCF_Face face ) PCF_Face_Done( PCF_Face face )
{ {
FT_Memory memory = FT_FACE_MEMORY( face ); FT_Memory memory = FT_FACE_MEMORY( face );
@ -81,8 +81,6 @@ THE SOFTWARE.
FREE( face->charset_registry ); FREE( face->charset_registry );
FT_TRACE4(( "DONE_FACE!!!\n" )); FT_TRACE4(( "DONE_FACE!!!\n" ));
return PCF_Err_Ok;
} }

View File

@ -242,7 +242,7 @@
} }
/* check that we have a valid TrueType file */ /* 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 ) if ( error )
goto Exit; goto Exit;