mirror of
https://github.com/whoahq/whoa.git
synced 2026-03-18 13:41:06 +03:00
Merge 0573235a70 into b69a992141
This commit is contained in:
commit
af9acfd4b8
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,7 +3,7 @@
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
/build
|
||||
/build*
|
||||
/cmake-build-*
|
||||
/out
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
55
src/app/web/Whoa.cpp
Normal 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
144
src/app/web/shell.html
Normal 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>
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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}
|
||||
)
|
||||
|
||||
@ -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,
|
||||
|
||||
36
src/client/gui/web/OsGui.cpp
Normal file
36
src/client/gui/web/OsGui.cpp
Normal 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
|
||||
}
|
||||
@ -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}
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
288
src/event/web/Input.cpp
Normal 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;
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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*);
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -16,6 +16,10 @@
|
||||
#include <winsock2.h>
|
||||
#endif
|
||||
|
||||
#if defined(WHOA_SYSTEM_WEB)
|
||||
struct sockaddr_in;
|
||||
#endif
|
||||
|
||||
class CDataStore;
|
||||
class WowConnectionNet;
|
||||
class WowConnectionResponse;
|
||||
|
||||
@ -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;
|
||||
|
||||
740
src/net/connection/web/WowConnection.cpp
Normal file
740
src/net/connection/web/WowConnection.cpp
Normal 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();
|
||||
}
|
||||
174
src/net/connection/web/WowConnectionNet.cpp
Normal file
174
src/net/connection/web/WowConnectionNet.cpp
Normal 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);
|
||||
}
|
||||
54
src/net/connection/web/WsState.hpp
Normal file
54
src/net/connection/web/WsState.hpp
Normal 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
|
||||
@ -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
|
||||
)
|
||||
|
||||
if(NOT WHOA_SYSTEM_WEB)
|
||||
target_link_libraries(sound
|
||||
PUBLIC
|
||||
fmod
|
||||
)
|
||||
)
|
||||
endif()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
77
src/sound/web/SESound.cpp
Normal 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) {
|
||||
}
|
||||
@ -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()
|
||||
|
||||
150
src/util/web/library_fetchfs.js
Normal file
150
src/util/web/library_fetchfs.js
Normal 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));
|
||||
},
|
||||
});
|
||||
@ -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
|
||||
|
||||
2
vendor/CMakeLists.txt
vendored
2
vendor/CMakeLists.txt
vendored
@ -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")
|
||||
|
||||
2
vendor/freetype-2.0.9/src/base/ftobjs.c
vendored
2
vendor/freetype-2.0.9/src/base/ftobjs.c
vendored
@ -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 )
|
||||
|
||||
4
vendor/freetype-2.0.9/src/pcf/pcfdriver.c
vendored
4
vendor/freetype-2.0.9/src/pcf/pcfdriver.c
vendored
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
2
vendor/freetype-2.0.9/src/sfnt/sfobjs.c
vendored
2
vendor/freetype-2.0.9/src/sfnt/sfobjs.c
vendored
@ -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;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user