Compare commits

...

15 Commits

Author SHA1 Message Date
Alex Tiernan-Berry
1ad0d797ba
Merge 0573235a70 into d210df2f43 2026-02-09 14:01:46 +03:00
fallenoak
d210df2f43
feat(ui): add CGPartyInfo::NumMembers 2026-02-09 04:56:46 -06:00
fallenoak
66df4c55da
feat(ui): implement CScriptRegion_SetSize
Some checks are pending
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) Waiting to run
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) Waiting to run
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) Waiting to run
2026-02-08 21:45:58 -06:00
fallenoak
91da4e9680
feat(ui): add CLayoutFrame::SetSize 2026-02-08 21:45:31 -06:00
fallenoak
fabd5888a9
feat(ui): add CGGameUI::RegisterGameCVars 2026-02-08 20:37:45 -06:00
fallenoak
1fd5c5c944
feat(ui): stub Script_BNFeaturesEnabledAndConnected
Some checks are pending
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) Waiting to run
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) Waiting to run
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) Waiting to run
2026-02-08 13:22:15 -06:00
fallenoak
4f26eeb05c
feat(ui): stub Script_BNConnected 2026-02-08 13:18:40 -06:00
fallenoak
14d14dacb0
feat(ui): stub Script_BNFeaturesEnabled 2026-02-08 13:11:45 -06:00
fallenoak
c6ddfc0d87
feat(ui): implement Script_IsBNLogin 2026-02-08 13:07:40 -06:00
fallenoak
99a95e9db4
feat(ui): add BattlenetUI_RegisterScriptFunctions 2026-02-08 13:01:55 -06:00
fallenoak
7ec9a35b4b
feat(ui): implement Script_GetBonusBarOffset
Some checks are pending
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) Waiting to run
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) Waiting to run
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) Waiting to run
2026-02-08 07:16:58 -06:00
fallenoak
43895197af
feat(ui): implement Script_SetCVar 2026-02-08 06:58:15 -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
55 changed files with 2454 additions and 28 deletions

2
.gitignore vendored
View File

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

View File

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

View File

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

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

View File

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

View File

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

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})
endif()
if(WHOA_SYSTEM_WEB)
file(GLOB WEB_SOURCES
"web/*.cpp"
)
list(APPEND PRIVATE_SOURCES ${WEB_SOURCES})
endif()
add_library(event STATIC
${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);
#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);

View File

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

View File

@ -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() {

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/Memory.hpp>
#include <algorithm>
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <limits>
#include <new>
@ -179,7 +181,16 @@ int32_t CGxDevice::GLLAdapterMonitorModes(TSGrowableArray<CGxMonitorMode>& 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) {

View File

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

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);
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);

View File

@ -11,7 +11,7 @@
#include <ApplicationServices/ApplicationServices.h>
#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;

View File

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

View File

@ -1,6 +1,7 @@
#include "gx/texture/CGxTex.hpp"
#include "gx/Gx.hpp"
#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) {
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
}

View File

@ -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*);

View File

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

View File

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

View File

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

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")
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()

View File

@ -5,10 +5,12 @@
#include "sound/SESoundInternal.hpp"
#include "sound/SEUserData.hpp"
#include <cstdint>
#include <fmod.hpp>
#include <storm/Hash.hpp>
#include <storm/Thread.hpp>
#if !defined(WHOA_SYSTEM_WEB)
#include <fmod.hpp>
struct SOUND_INTERNAL_LOOKUP : TSHashObject<SOUND_INTERNAL_LOOKUP, HASHKEY_NONE> {
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<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

View File

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

View File

@ -4,9 +4,11 @@
#include "db/Db.hpp"
#include <cstdint>
#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;

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

@ -862,6 +862,13 @@ void CLayoutFrame::SetProtectFlag(uint32_t flag) {
this->m_flags &= ~0x800;
}
void CLayoutFrame::SetSize(float width, float height) {
this->m_flags &= ~0x8;
this->m_width = width;
this->m_height = height;
this->Resize(0);
}
void CLayoutFrame::SetWidth(float width) {
this->m_flags &= ~0x8;
this->m_width = width;

View File

@ -49,6 +49,7 @@ class CLayoutFrame {
virtual bool SetLayoutScale(float scale, bool force);
virtual void SetWidth(float width);
virtual void SetHeight(float height);
virtual void SetSize(float width, float height);
virtual float GetWidth();
virtual float GetHeight();
virtual void GetSize(float& width, float& height, int32_t a4);

View File

@ -219,7 +219,28 @@ int32_t CScriptRegion_SetHeight(lua_State* L) {
}
int32_t CScriptRegion_SetSize(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
auto type = CScriptRegion::GetObjectType();
auto region = static_cast<CScriptRegion*>(FrameScript_GetObjectThis(L, type));
if (!region->ProtectedFunctionsAllowed()) {
// TODO disallowed logic
return 0;
}
if (!lua_isnumber(L, 2) || !lua_isnumber(L, 3)) {
luaL_error(L, "Usage: %s:SetSize(width, height)", region->GetDisplayName());
return 0;
}
auto ndcWidth = static_cast<float>(lua_tonumber(L, 2)) / (CoordinateGetAspectCompensation() * 1024.0f);
auto ddcWidth = NDCToDDCWidth(ndcWidth);
auto ndcHeight = static_cast<float>(lua_tonumber(L, 3)) / (CoordinateGetAspectCompensation() * 1024.0f);
auto ddcHeight = NDCToDDCWidth(ndcHeight);
region->SetSize(ddcWidth, ddcHeight);
return 0;
}
int32_t CScriptRegion_GetSize(lua_State* L) {

View File

@ -83,7 +83,9 @@ int32_t Script_IsActionInRange(lua_State* L) {
}
int32_t Script_GetBonusBarOffset(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
lua_pushnumber(L, CGActionBar::GetBonusBarOffset());
return 1;
}
int32_t Script_GetMultiCastBarOffset(lua_State* L) {

319
src/ui/game/BattlenetUI.cpp Normal file
View File

@ -0,0 +1,319 @@
#include "ui/game/BattlenetUI.hpp"
#include "client/ClientServices.hpp"
#include "net/Login.hpp"
#include "ui/FrameScript.hpp"
#include "util/Lua.hpp"
#include "util/Unimplemented.hpp"
namespace BattlenetUI {
int32_t Script_BNGetInfo(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetNumFriends(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetFriendInfo(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetFriendInfoByID(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetNumFriendToons(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetFriendToonInfo(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetToonInfo(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNRemoveFriend(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNSetFriendNote(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNSetSelectedFriend(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetSelectedFriend(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetNumFriendInvites(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetFriendInviteInfo(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNSendFriendInvite(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNSendFriendInviteByID(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNAcceptFriendInvite(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNDeclineFriendInvite(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNReportFriendInvite(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNSetAFK(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNSetDND(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNSetCustomMessage(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetCustomMessageTable(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNSetFocus(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNSendWhisper(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNCreateConversation(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNInviteToConversation(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNLeaveConversation(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNSendConversationMessage(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetNumConversationMembers(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetConversationMemberInfo(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetConversationInfo(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNListConversation(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetNumBlocked(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetBlockedInfo(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNIsBlocked(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNSetBlocked(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNSetSelectedBlock(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetSelectedBlock(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetNumBlockedToons(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetBlockedToonInfo(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNIsToonBlocked(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNSetToonBlocked(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNSetSelectedToonBlock(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetSelectedToonBlock(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNReportPlayer(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNConnected(lua_State* L) {
// TODO real implementation
lua_pushboolean(L, false);
return 1;
}
int32_t Script_BNFeaturesEnabledAndConnected(lua_State* L) {
// TODO real implementation
lua_pushboolean(L, false);
return 1;
}
int32_t Script_IsBNLogin(lua_State* L) {
if (ClientServices::LoginConnection() && ClientServices::LoginConnection()->GetLoginServerType() == 1) {
lua_pushboolean(L, true);
} else {
lua_pushboolean(L, false);
}
return 1;
}
int32_t Script_BNFeaturesEnabled(lua_State* L) {
// TODO real implementation
lua_pushboolean(L, false);
return 1;
}
int32_t Script_BNRequestFOFInfo(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetNumFOF(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetFOFInfo(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNSetMatureLanguageFilter(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetMatureLanguageFilter(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNIsSelf(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNIsFriend(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
int32_t Script_BNGetMaxPlayersInConversation(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
}
static FrameScript_Method s_ScriptFunctions[] = {
{ "BNGetInfo", &Script_BNGetInfo },
{ "BNGetNumFriends", &Script_BNGetNumFriends },
{ "BNGetFriendInfo", &Script_BNGetFriendInfo },
{ "BNGetFriendInfoByID", &Script_BNGetFriendInfoByID },
{ "BNGetNumFriendToons", &Script_BNGetNumFriendToons },
{ "BNGetFriendToonInfo", &Script_BNGetFriendToonInfo },
{ "BNGetToonInfo", &Script_BNGetToonInfo },
{ "BNRemoveFriend", &Script_BNRemoveFriend },
{ "BNSetFriendNote", &Script_BNSetFriendNote },
{ "BNSetSelectedFriend", &Script_BNSetSelectedFriend },
{ "BNGetSelectedFriend", &Script_BNGetSelectedFriend },
{ "BNGetNumFriendInvites", &Script_BNGetNumFriendInvites },
{ "BNGetFriendInviteInfo", &Script_BNGetFriendInviteInfo },
{ "BNSendFriendInvite", &Script_BNSendFriendInvite },
{ "BNSendFriendInviteByID", &Script_BNSendFriendInviteByID },
{ "BNAcceptFriendInvite", &Script_BNAcceptFriendInvite },
{ "BNDeclineFriendInvite", &Script_BNDeclineFriendInvite },
{ "BNReportFriendInvite", &Script_BNReportFriendInvite },
{ "BNSetAFK", &Script_BNSetAFK },
{ "BNSetDND", &Script_BNSetDND },
{ "BNSetCustomMessage", &Script_BNSetCustomMessage },
{ "BNGetCustomMessageTable", &Script_BNGetCustomMessageTable },
{ "BNSetFocus", &Script_BNSetFocus },
{ "BNSendWhisper", &Script_BNSendWhisper },
{ "BNCreateConversation", &Script_BNCreateConversation },
{ "BNInviteToConversation", &Script_BNInviteToConversation },
{ "BNLeaveConversation", &Script_BNLeaveConversation },
{ "BNSendConversationMessage", &Script_BNSendConversationMessage },
{ "BNGetNumConversationMembers", &Script_BNGetNumConversationMembers },
{ "BNGetConversationMemberInfo", &Script_BNGetConversationMemberInfo },
{ "BNGetConversationInfo", &Script_BNGetConversationInfo },
{ "BNListConversation", &Script_BNListConversation },
{ "BNGetNumBlocked", &Script_BNGetNumBlocked },
{ "BNGetBlockedInfo", &Script_BNGetBlockedInfo },
{ "BNIsBlocked", &Script_BNIsBlocked },
{ "BNSetBlocked", &Script_BNSetBlocked },
{ "BNSetSelectedBlock", &Script_BNSetSelectedBlock },
{ "BNGetSelectedBlock", &Script_BNGetSelectedBlock },
{ "BNGetNumBlockedToons", &Script_BNGetNumBlockedToons },
{ "BNGetBlockedToonInfo", &Script_BNGetBlockedToonInfo },
{ "BNIsToonBlocked", &Script_BNIsToonBlocked },
{ "BNSetToonBlocked", &Script_BNSetToonBlocked },
{ "BNSetSelectedToonBlock", &Script_BNSetSelectedToonBlock },
{ "BNGetSelectedToonBlock", &Script_BNGetSelectedToonBlock },
{ "BNReportPlayer", &Script_BNReportPlayer },
{ "BNConnected", &Script_BNConnected },
{ "BNFeaturesEnabledAndConnected", &Script_BNFeaturesEnabledAndConnected },
{ "IsBNLogin", &Script_IsBNLogin },
{ "BNFeaturesEnabled", &Script_BNFeaturesEnabled },
{ "BNRequestFOFInfo", &Script_BNRequestFOFInfo },
{ "BNGetNumFOF", &Script_BNGetNumFOF },
{ "BNGetFOFInfo", &Script_BNGetFOFInfo },
{ "BNSetMatureLanguageFilter", &Script_BNSetMatureLanguageFilter },
{ "BNGetMatureLanguageFilter", &Script_BNGetMatureLanguageFilter },
{ "BNIsSelf", &Script_BNIsSelf },
{ "BNIsFriend", &Script_BNIsFriend },
{ "BNGetMaxPlayersInConversation", &Script_BNGetMaxPlayersInConversation },
};
}
void BattlenetUI_RegisterScriptFunctions() {
for (auto& func : BattlenetUI::s_ScriptFunctions) {
FrameScript_RegisterFunction(func.name, func.method);
}
}

View File

@ -0,0 +1,6 @@
#ifndef UI_GAME_BATTLENET_UI_HPP
#define UI_GAME_BATTLENET_UI_HPP
void BattlenetUI_RegisterScriptFunctions();
#endif

View File

@ -1,4 +1,9 @@
#include "ui/game/CGActionBar.hpp"
uint32_t CGActionBar::s_bonusBarOffset;
uint32_t CGActionBar::s_currentPage;
uint32_t CGActionBar::s_tempPageActiveFlags;
uint32_t CGActionBar::GetBonusBarOffset() {
return CGActionBar::s_bonusBarOffset;
}

View File

@ -5,9 +5,16 @@
class CGActionBar {
public:
// Static variables
// Public static variables
static uint32_t s_currentPage;
static uint32_t s_tempPageActiveFlags;
// Public static functions
static uint32_t GetBonusBarOffset();
private:
// Private static variables
static uint32_t s_bonusBarOffset;
};
#endif

View File

@ -1,11 +1,13 @@
#include "ui/game/CGGameUI.hpp"
#include "client/Client.hpp"
#include "console/CVar.hpp"
#include "object/Client.hpp"
#include "ui/CScriptObject.hpp"
#include "ui/FrameXML.hpp"
#include "ui/Key.hpp"
#include "ui/game/ActionBarScript.hpp"
#include "ui/game/BattlefieldInfoScript.hpp"
#include "ui/game/BattlenetUI.hpp"
#include "ui/game/CGCharacterModelBase.hpp"
#include "ui/game/CGCooldown.hpp"
#include "ui/game/CGDressUpModelFrame.hpp"
@ -70,8 +72,7 @@ void LoadScriptFunctions() {
// TODO
GMTicketInfoRegisterScriptFunctions();
// TODO
BattlenetUI_RegisterScriptFunctions();
}
void CGGameUI::EnterWorld() {
@ -121,6 +122,7 @@ void CGGameUI::Initialize() {
LoadScriptFunctions();
ScriptEventsRegisterEvents();
CGGameUI::RegisterGameCVars();
// TODO
@ -232,3 +234,47 @@ void CGGameUI::RegisterFrameFactories() {
FrameXML_RegisterFactory("TabardModel", &CGTabardModelFrame::Create, false);
FrameXML_RegisterFactory("QuestPOIFrame", &CGQuestPOIFrame::Create, false);
}
void CGGameUI::RegisterGameCVars() {
// TODO
CVar::Register("enableCombatText", "Whether to show floating combat text", 0x10, "1", nullptr, GAME);
CVar::Register("combatTextFloatMode", "The combat text float mode", 0x10, "1", nullptr, GAME);
CVar::Register("fctCombatState", nullptr, 0x10, "0", nullptr, GAME);
CVar::Register("fctDodgeParryMiss", nullptr, 0x10, "0", nullptr, GAME);
CVar::Register("fctDamageReduction", nullptr, 0x10, "0", nullptr, GAME);
CVar::Register("fctRepChanges", nullptr, 0x10, "0", nullptr, GAME);
CVar::Register("fctReactives", nullptr, 0x10, "0", nullptr, GAME);
CVar::Register("fctFriendlyHealers", nullptr, 0x10, "0", nullptr, GAME);
CVar::Register("fctComboPoints", nullptr, 0x10, "0", nullptr, GAME);
CVar::Register("fctLowManaHealth", nullptr, 0x10, "1", nullptr, GAME);
CVar::Register("fctEnergyGains", nullptr, 0x10, "0", nullptr, GAME);
CVar::Register("fctPeriodicEnergyGains", nullptr, 0x10, "0", nullptr, GAME);
CVar::Register("fctHonorGains", nullptr, 0x10, "0", nullptr, GAME);
CVar::Register("fctAuras", nullptr, 0x10, "0", nullptr, GAME);
CVar::Register("fctAllSpellMechanics", nullptr, 0x10, "0", nullptr, GAME);
CVar::Register("fctSpellMechanics", nullptr, 0x10, "0", nullptr, GAME);
CVar::Register("fctSpellMechanicsOther", nullptr, 0x10, "0", nullptr, GAME);
CVar::Register("xpBarText", "Whether the XP bar shows the numeric experience value", 0x10, "0", nullptr, GAME);
CVar::Register("playerStatusText", "Whether the player portrait shows numeric health/mana values", 0x10, "0", nullptr, GAME);
CVar::Register("petStatusText", "Whether the pet portrait shows numeric health/mana values", 0x10, "0", nullptr, GAME);
CVar::Register("partyStatusText", "Whether the party portraits shows numeric health/mana values", 0x10, "0", nullptr, GAME);
CVar::Register("targetStatusText", "Whether the target portrait shows numeric health/mana values", 0x10, "0", nullptr, GAME);
CVar::Register("statusTextPercentage", "Whether numeric health/mana values are shown as raw values or percentages", 0x10, "0", nullptr, GAME);
CVar::Register("showPartyBackground", "Show a background behind party members", 0x10, "0", nullptr, GAME);
CVar::Register("partyBackgroundOpacity", "The opacity of the party background", 0x10, "0.5", nullptr, GAME);
CVar::Register("hidePartyInRaid", "Whether to hide the party UI while in a raid", 0x10, "0", nullptr, GAME);
CVar::Register("showPartyPets", "Whether to show pets in the party UI", 0x20, "1", nullptr, GAME);
CVar::Register("showRaidRange", "Show range indicator in raid UI", 0x20, "0", nullptr, GAME);
CVar::Register("showArenaEnemyFrames", "Show arena enemy frames while in an Arena", 0x20, "1", nullptr, GAME);
CVar::Register("showArenaEnemyCastbar", "Show the spell enemies are casting on the Arena Enemy frames", 0x20, "1", nullptr, GAME);
CVar::Register("showArenaEnemyPets", "Show the enemy team's pets on the ArenaEnemy frames", 0x20, "1", nullptr, GAME);
CVar::Register("fullSizeFocusFrame", "Increases the size of the focus frame to that of the target frame", 0x20, "0", nullptr, GAME);
// TODO
}

View File

@ -23,6 +23,7 @@ class CGGameUI {
static int32_t IsRaidMember(const WOWGUID& guid);
static int32_t IsRaidMemberOrPet(const WOWGUID& guid);
static void RegisterFrameFactories();
static void RegisterGameCVars();
private:
static WOWGUID s_currentObjectTrack;

View File

@ -0,0 +1,15 @@
#include "ui/game/CGPartyInfo.hpp"
WOWGUID CGPartyInfo::m_members[4];
uint32_t CGPartyInfo::NumMembers() {
uint32_t count = 0;
for (auto& member : CGPartyInfo::m_members) {
if (member != 0) {
count++;
}
}
return count;
}

View File

@ -0,0 +1,16 @@
#ifndef UI_GAME_C_G_PARTY_INFO_HPP
#define UI_GAME_C_G_PARTY_INFO_HPP
#include "util/GUID.hpp"
class CGPartyInfo {
public:
// Public static functions
static uint32_t NumMembers();
private:
// Private static variables
static WOWGUID m_members[];
};
#endif

View File

@ -4,6 +4,7 @@
#include "ui/FrameScript.hpp"
#include "ui/ScriptFunctionsShared.hpp"
#include "ui/game/CGGameUI.hpp"
#include "ui/game/Types.hpp"
#include "ui/simple/CSimpleTop.hpp"
#include "util/StringTo.hpp"
#include "util/Unimplemented.hpp"
@ -131,7 +132,41 @@ int32_t Script_GetCVarInfo(lua_State* L) {
}
int32_t Script_SetCVar(lua_State* L) {
WHOA_UNIMPLEMENTED(0);
if (!lua_isstring(L, 1)) {
luaL_error(L, "Usage: SetCVar(\"cvar\", value [, \"scriptCvar\")");
return 0;
}
auto varName = lua_tostring(L, 1);
auto var = CVar::LookupRegistered(varName);
if (!var || (var->m_flags & 0x40)) {
luaL_error(L, "Couldn't find CVar named '%s'", varName);
return 0;
}
if (var->m_flags & 0x4 || var->m_flags & 0x100) {
luaL_error(L, "\"%s\" is read-only", varName);
return 0;
}
if (!(var->m_flags & 0x8)/* TODO || CSimpleTop::GetInstance()->dword124C */) {
auto value = lua_tostring(L, 2);
if (!value) {
value = "0";
}
var->Set(value, true, false, false, true);
if (lua_isstring(L, 3)) {
auto scriptVarName = lua_tostring(L, 3);
FrameScript_SignalEvent(SCRIPT_CVAR_UPDATE, "%s%s", scriptVarName, value);
}
} else {
// TODO CGGameUI::ShowBlockedActionFeedback(nullptr, 2);
}
return 0;
}
int32_t Script_GetCVar(lua_State* L) {

View File

@ -1187,7 +1187,7 @@ void ScriptEventsInitialize() {
g_scriptEvents[295] = "TRAINER_UPDATE";
g_scriptEvents[296] = "TRAINER_DESCRIPTION_UPDATE";
g_scriptEvents[297] = "TRAINER_CLOSED";
g_scriptEvents[298] = "CVAR_UPDATE";
g_scriptEvents[SCRIPT_CVAR_UPDATE] = "CVAR_UPDATE";
g_scriptEvents[299] = "TRADE_SKILL_SHOW";
g_scriptEvents[300] = "TRADE_SKILL_UPDATE";
g_scriptEvents[301] = "TRADE_SKILL_CLOSE";

View File

@ -9,6 +9,8 @@ enum SCRIPTEVENT {
SCRIPT_PLAYER_LOGOUT = 254,
SCRIPT_PLAYER_ENTERING_WORLD = 255,
// TODO
SCRIPT_CVAR_UPDATE = 298,
// TODO
};
#endif

View File

@ -40,3 +40,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()

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()
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

View File

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

View File

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

View File

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

View File

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