mirror of
https://github.com/whoahq/whoa.git
synced 2026-03-18 13:41:06 +03:00
Merge 0573235a70 into e5150d7d21
This commit is contained in:
commit
9ce41b2da0
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,7 +3,7 @@
|
|||||||
.vscode
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
/build
|
/build*
|
||||||
/cmake-build-*
|
/cmake-build-*
|
||||||
/out
|
/out
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,7 @@ set(CMAKE_CXX_STANDARD 11)
|
|||||||
|
|
||||||
include(lib/system/cmake/system.cmake)
|
include(lib/system/cmake/system.cmake)
|
||||||
|
|
||||||
|
|
||||||
# Some templates abuse offsetof
|
# Some templates abuse offsetof
|
||||||
if(WHOA_SYSTEM_LINUX OR WHOA_SYSTEM_MAC)
|
if(WHOA_SYSTEM_LINUX OR WHOA_SYSTEM_MAC)
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-invalid-offsetof")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-invalid-offsetof")
|
||||||
@ -63,6 +64,13 @@ if(WHOA_SYSTEM_LINUX OR WHOA_SYSTEM_MAC)
|
|||||||
find_package(Threads REQUIRED)
|
find_package(Threads REQUIRED)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Emscripten pthreads support
|
||||||
|
if(WHOA_SYSTEM_WEB)
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread")
|
||||||
|
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pthread")
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pthread -s PTHREAD_POOL_SIZE=4")
|
||||||
|
endif()
|
||||||
|
|
||||||
# Library search paths
|
# Library search paths
|
||||||
if(WHOA_SYSTEM_MAC)
|
if(WHOA_SYSTEM_MAC)
|
||||||
set(CMAKE_SKIP_BUILD_RPATH FALSE)
|
set(CMAKE_SKIP_BUILD_RPATH FALSE)
|
||||||
|
|||||||
@ -52,6 +52,31 @@ if(WHOA_SYSTEM_LINUX)
|
|||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(WHOA_SYSTEM_WEB)
|
||||||
|
file(GLOB PRIVATE_SOURCES "web/*.cpp")
|
||||||
|
|
||||||
|
add_executable(Whoa ${PRIVATE_SOURCES})
|
||||||
|
|
||||||
|
target_link_libraries(Whoa
|
||||||
|
PRIVATE
|
||||||
|
client
|
||||||
|
event
|
||||||
|
gx
|
||||||
|
net
|
||||||
|
util
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(Whoa PROPERTIES SUFFIX ".html")
|
||||||
|
|
||||||
|
target_link_options(Whoa PRIVATE
|
||||||
|
"SHELL:--shell-file ${CMAKE_CURRENT_SOURCE_DIR}/web/shell.html"
|
||||||
|
-sASYNCIFY
|
||||||
|
-sASYNCIFY_STACK_SIZE=524288
|
||||||
|
-sALLOW_MEMORY_GROWTH
|
||||||
|
"-sASYNCIFY_IMPORTS=['__syscall_openat','__syscall_stat64','__syscall_lstat64','__syscall_fstat64']"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
target_include_directories(Whoa
|
target_include_directories(Whoa
|
||||||
PRIVATE
|
PRIVATE
|
||||||
${CMAKE_SOURCE_DIR}/src
|
${CMAKE_SOURCE_DIR}/src
|
||||||
|
|||||||
55
src/app/web/Whoa.cpp
Normal file
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->queue = queue;
|
||||||
thread->currentObject = nullptr;
|
thread->currentObject = nullptr;
|
||||||
|
|
||||||
|
printf("AsyncFileReadCreateThread: Creating thread '%s' for queue=%p\n", queueName, static_cast<void*>(queue));
|
||||||
SThread::Create(AsyncFileReadThread, thread, thread->thread, const_cast<char*>(queueName), 0);
|
SThread::Create(AsyncFileReadThread, thread, thread->thread, const_cast<char*>(queueName), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,6 +148,7 @@ uint32_t AsyncFileReadThread(void* param) {
|
|||||||
AsyncFileRead::s_queueLock.Leave();
|
AsyncFileRead::s_queueLock.Leave();
|
||||||
|
|
||||||
int32_t tries = 10;
|
int32_t tries = 10;
|
||||||
|
int32_t readSuccess = 0;
|
||||||
while (1) {
|
while (1) {
|
||||||
if (SFile::IsStreamingMode() && object->file) {
|
if (SFile::IsStreamingMode() && object->file) {
|
||||||
// TODO
|
// TODO
|
||||||
@ -154,6 +156,7 @@ uint32_t AsyncFileReadThread(void* param) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (SFile::Read(object->file, object->buffer, object->size, nullptr, nullptr, nullptr)) {
|
if (SFile::Read(object->file, object->buffer, object->size, nullptr, nullptr, nullptr)) {
|
||||||
|
readSuccess = 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,13 @@ if(WHOA_SYSTEM_LINUX)
|
|||||||
list(APPEND PRIVATE_SOURCES ${LINUX_SOURCES})
|
list(APPEND PRIVATE_SOURCES ${LINUX_SOURCES})
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(WHOA_SYSTEM_WEB)
|
||||||
|
file(GLOB WEB_SOURCES
|
||||||
|
"gui/web/*.cpp"
|
||||||
|
)
|
||||||
|
list(APPEND PRIVATE_SOURCES ${WEB_SOURCES})
|
||||||
|
endif()
|
||||||
|
|
||||||
add_library(client STATIC
|
add_library(client STATIC
|
||||||
${PRIVATE_SOURCES}
|
${PRIVATE_SOURCES}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -317,7 +317,7 @@ void ClientServices::InitLoginServerCVars(int32_t force, const char* locale) {
|
|||||||
"realmList",
|
"realmList",
|
||||||
"Address of realm list server",
|
"Address of realm list server",
|
||||||
0x0,
|
0x0,
|
||||||
"us.logon.worldofwarcraft.com:3724",
|
"logon.chromiecraft.com:3724",
|
||||||
nullptr,
|
nullptr,
|
||||||
NET,
|
NET,
|
||||||
false,
|
false,
|
||||||
|
|||||||
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})
|
list(APPEND PRIVATE_SOURCES ${LINUX_SOURCES})
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(WHOA_SYSTEM_WEB)
|
||||||
|
file(GLOB WEB_SOURCES
|
||||||
|
"web/*.cpp"
|
||||||
|
)
|
||||||
|
list(APPEND PRIVATE_SOURCES ${WEB_SOURCES})
|
||||||
|
endif()
|
||||||
|
|
||||||
add_library(event STATIC
|
add_library(event STATIC
|
||||||
${PRIVATE_SOURCES}
|
${PRIVATE_SOURCES}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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);
|
void EventPostCloseEx(HEVENTCONTEXT contextHandle);
|
||||||
|
|
||||||
|
#if defined(WHOA_SYSTEM_WEB)
|
||||||
|
void EventProcessFrame();
|
||||||
|
#endif
|
||||||
|
|
||||||
void EventRegister(EVENTID id, int32_t (*handler)(const void*, void*));
|
void EventRegister(EVENTID id, int32_t (*handler)(const void*, void*));
|
||||||
|
|
||||||
void EventRegisterEx(EVENTID id, int32_t (*handler)(const void*, void*), void* param, float priority);
|
void EventRegisterEx(EVENTID id, int32_t (*handler)(const void*, void*), void* param, float priority);
|
||||||
|
|||||||
@ -632,6 +632,7 @@ const char* KeyCodeToString(KEY key) {
|
|||||||
return "UNKNOWN";
|
return "UNKNOWN";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !defined(WHOA_SYSTEM_WEB)
|
||||||
void OsInputInitialize() {
|
void OsInputInitialize() {
|
||||||
#if defined(WHOA_SYSTEM_WIN)
|
#if defined(WHOA_SYSTEM_WIN)
|
||||||
Input::s_numlockState = GetAsyncKeyState(144);
|
Input::s_numlockState = GetAsyncKeyState(144);
|
||||||
@ -656,6 +657,7 @@ bool OsInputIsUsingCocoaEventLoop() {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void OsInputPostEvent(OSINPUT id, int32_t param0, int32_t param1, int32_t param2, int32_t param3) {
|
void OsInputPostEvent(OSINPUT id, int32_t param0, int32_t param1, int32_t param2, int32_t param3) {
|
||||||
// TODO
|
// TODO
|
||||||
|
|||||||
@ -146,6 +146,22 @@ void IEvtSchedulerProcess() {
|
|||||||
// dword_141B3C8 = 0;
|
// dword_141B3C8 = 0;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(WHOA_SYSTEM_WEB)
|
||||||
|
// Web builds use a callback-based main loop (emscripten_set_main_loop)
|
||||||
|
// This function should not be called - use EventProcessFrame() instead
|
||||||
|
Event::s_startEvent.Set();
|
||||||
|
|
||||||
|
PropSelectContext(0);
|
||||||
|
|
||||||
|
uintptr_t v0 = SGetCurrentThreadId();
|
||||||
|
char v2[64];
|
||||||
|
SStrPrintf(v2, 64, "Engine %x", v0);
|
||||||
|
|
||||||
|
OsCallInitialize(v2);
|
||||||
|
|
||||||
|
// Don't block - the main loop callback will call SchedulerMainProcess
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void IEvtSchedulerShutdown() {
|
void IEvtSchedulerShutdown() {
|
||||||
|
|||||||
288
src/event/web/Input.cpp
Normal file
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/Error.hpp>
|
||||||
#include <storm/Memory.hpp>
|
#include <storm/Memory.hpp>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstdarg>
|
||||||
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <new>
|
#include <new>
|
||||||
@ -179,7 +181,16 @@ int32_t CGxDevice::GLLAdapterMonitorModes(TSGrowableArray<CGxMonitorMode>& monit
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
void CGxDevice::Log(const char* format, ...) {
|
void CGxDevice::Log(const char* format, ...) {
|
||||||
// TODO
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
char buffer[1024];
|
||||||
|
vsnprintf(buffer, sizeof(buffer), format, args);
|
||||||
|
va_end(args);
|
||||||
|
#if defined(__EMSCRIPTEN__)
|
||||||
|
printf("[GxDevice] %s\n", buffer);
|
||||||
|
#else
|
||||||
|
fprintf(stderr, "[GxDevice] %s\n", buffer);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void CGxDevice::Log(const CGxFormat& format) {
|
void CGxDevice::Log(const CGxFormat& format) {
|
||||||
|
|||||||
@ -156,7 +156,8 @@ void FillInSolidTexture(const CImVector& color, CTexture* texture) {
|
|||||||
gxTexFlags,
|
gxTexFlags,
|
||||||
userArg,
|
userArg,
|
||||||
GxuUpdateSingleColorTexture,
|
GxuUpdateSingleColorTexture,
|
||||||
GxTex_Argb8888
|
GxTex_Argb8888,
|
||||||
|
"SolidColor"
|
||||||
);
|
);
|
||||||
|
|
||||||
if (color.a < 0xFE) {
|
if (color.a < 0xFE) {
|
||||||
@ -252,7 +253,7 @@ int32_t GxTexCreate(const CGxTexParms& parms, CGxTex*& texId) {
|
|||||||
parms.flags,
|
parms.flags,
|
||||||
parms.userArg,
|
parms.userArg,
|
||||||
parms.userFunc,
|
parms.userFunc,
|
||||||
"",
|
parms.name,
|
||||||
texId
|
texId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -329,7 +330,7 @@ void TextureFreeGxTex(CGxTex* texId) {
|
|||||||
GxTexDestroy(texId);
|
GxTexDestroy(texId);
|
||||||
}
|
}
|
||||||
|
|
||||||
CGxTex* TextureAllocGxTex(EGxTexTarget target, uint32_t width, uint32_t height, uint32_t depth, EGxTexFormat format, CGxTexFlags flags, void* userArg, TEXTURE_CALLBACK* userFunc, EGxTexFormat dataFormat) {
|
CGxTex* TextureAllocGxTex(EGxTexTarget target, uint32_t width, uint32_t height, uint32_t depth, EGxTexFormat format, CGxTexFlags flags, void* userArg, TEXTURE_CALLBACK* userFunc, EGxTexFormat dataFormat, const char* name) {
|
||||||
CGxTexParms gxTexParms;
|
CGxTexParms gxTexParms;
|
||||||
|
|
||||||
gxTexParms.height = height;
|
gxTexParms.height = height;
|
||||||
@ -342,6 +343,7 @@ CGxTex* TextureAllocGxTex(EGxTexTarget target, uint32_t width, uint32_t height,
|
|||||||
gxTexParms.userFunc = userFunc;
|
gxTexParms.userFunc = userFunc;
|
||||||
gxTexParms.flags = flags;
|
gxTexParms.flags = flags;
|
||||||
gxTexParms.flags.m_generateMipMaps = 0;
|
gxTexParms.flags.m_generateMipMaps = 0;
|
||||||
|
gxTexParms.name = name ? name : "";
|
||||||
|
|
||||||
CGxTexParms gxTexParms2;
|
CGxTexParms gxTexParms2;
|
||||||
|
|
||||||
@ -718,7 +720,8 @@ int32_t PumpBlpTextureAsync(CTexture* texture, void* buf) {
|
|||||||
texture->gxTexFlags,
|
texture->gxTexFlags,
|
||||||
texture,
|
texture,
|
||||||
&UpdateBlpTextureAsync,
|
&UpdateBlpTextureAsync,
|
||||||
texture->dataFormat
|
texture->dataFormat,
|
||||||
|
texture->filename
|
||||||
);
|
);
|
||||||
|
|
||||||
texture->gxTex = gxTex;
|
texture->gxTex = gxTex;
|
||||||
@ -1005,7 +1008,7 @@ HTEXTURE TextureCreate(EGxTexTarget target, uint32_t width, uint32_t height, uin
|
|||||||
|
|
||||||
texFlags.m_maxAnisotropy = texFlags.m_filter == GxTex_Anisotropic ? CTexture::s_maxAnisotropy : 1;
|
texFlags.m_maxAnisotropy = texFlags.m_filter == GxTex_Anisotropic ? CTexture::s_maxAnisotropy : 1;
|
||||||
|
|
||||||
texture->gxTex = TextureAllocGxTex(target, width, height, depth, format, texFlags, userArg, userFunc, dataFormat);
|
texture->gxTex = TextureAllocGxTex(target, width, height, depth, format, texFlags, userArg, userFunc, dataFormat, a10 ? a10 : "UniqueTexture");
|
||||||
texture->dataFormat = dataFormat;
|
texture->dataFormat = dataFormat;
|
||||||
texture->gxWidth = width;
|
texture->gxWidth = width;
|
||||||
texture->gxHeight = height;
|
texture->gxHeight = height;
|
||||||
|
|||||||
@ -45,7 +45,7 @@ MipBits* MippedImgAllocA(uint32_t, uint32_t, uint32_t, const char*, int32_t);
|
|||||||
|
|
||||||
uint32_t MippedImgCalcSize(uint32_t, uint32_t, uint32_t);
|
uint32_t MippedImgCalcSize(uint32_t, uint32_t, uint32_t);
|
||||||
|
|
||||||
CGxTex* TextureAllocGxTex(EGxTexTarget, uint32_t, uint32_t, uint32_t, EGxTexFormat, CGxTexFlags, void*, void (*userFunc)(EGxTexCommand, uint32_t, uint32_t, uint32_t, uint32_t, void*, uint32_t&, const void*&), EGxTexFormat);
|
CGxTex* TextureAllocGxTex(EGxTexTarget, uint32_t, uint32_t, uint32_t, EGxTexFormat, CGxTexFlags, void*, void (*userFunc)(EGxTexCommand, uint32_t, uint32_t, uint32_t, uint32_t, void*, uint32_t&, const void*&), EGxTexFormat, const char* name = "");
|
||||||
|
|
||||||
MipBits* TextureAllocMippedImg(PIXEL_FORMAT pixelFormat, uint32_t width, uint32_t height);
|
MipBits* TextureAllocMippedImg(PIXEL_FORMAT pixelFormat, uint32_t width, uint32_t height);
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
#include <ApplicationServices/ApplicationServices.h>
|
#include <ApplicationServices/ApplicationServices.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(WHOA_SYSTEM_LINUX) || defined(WHOA_SYSTEM_WIN)
|
#if defined(WHOA_SYSTEM_LINUX) || defined(WHOA_SYSTEM_WIN) || defined(WHOA_SYSTEM_WEB)
|
||||||
struct Rect {
|
struct Rect {
|
||||||
int16_t top;
|
int16_t top;
|
||||||
int16_t left;
|
int16_t left;
|
||||||
@ -20,7 +20,7 @@ struct Rect {
|
|||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(WHOA_SYSTEM_MAC) || defined(WHOA_SYSTEM_LINUX)
|
#if defined(WHOA_SYSTEM_MAC) || defined(WHOA_SYSTEM_LINUX) || defined(WHOA_SYSTEM_WEB)
|
||||||
typedef struct tagRECT {
|
typedef struct tagRECT {
|
||||||
int32_t left;
|
int32_t left;
|
||||||
int32_t top;
|
int32_t top;
|
||||||
|
|||||||
@ -26,6 +26,7 @@ class CGxPool : public TSLinkedNode<CGxPool> {
|
|||||||
, m_usage(usage)
|
, m_usage(usage)
|
||||||
, m_size(size)
|
, m_size(size)
|
||||||
, m_apiSpecific(nullptr)
|
, m_apiSpecific(nullptr)
|
||||||
|
, m_mem(nullptr)
|
||||||
, unk1C(0)
|
, unk1C(0)
|
||||||
, m_hint(hint)
|
, m_hint(hint)
|
||||||
, m_name(name)
|
, m_name(name)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#include "gx/texture/CGxTex.hpp"
|
#include "gx/texture/CGxTex.hpp"
|
||||||
#include "gx/Gx.hpp"
|
#include "gx/Gx.hpp"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
CGxTexFlags::CGxTexFlags(EGxTexFilter filter, uint32_t wrapU, uint32_t wrapV, uint32_t force, uint32_t generateMipMaps, uint32_t renderTarget, uint32_t maxAnisotropy) {
|
CGxTexFlags::CGxTexFlags(EGxTexFilter filter, uint32_t wrapU, uint32_t wrapV, uint32_t force, uint32_t generateMipMaps, uint32_t renderTarget, uint32_t maxAnisotropy) {
|
||||||
this->m_filter = filter;
|
this->m_filter = filter;
|
||||||
@ -47,6 +48,13 @@ CGxTex::CGxTex(EGxTexTarget target, uint32_t width, uint32_t height, uint32_t de
|
|||||||
this->m_needsFlagUpdate = 1;
|
this->m_needsFlagUpdate = 1;
|
||||||
this->m_needsCreation = 1;
|
this->m_needsCreation = 1;
|
||||||
|
|
||||||
|
if (name && name[0]) {
|
||||||
|
std::strncpy(this->m_name, name, sizeof(this->m_name) - 1);
|
||||||
|
this->m_name[sizeof(this->m_name) - 1] = '\0';
|
||||||
|
} else {
|
||||||
|
this->m_name[0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
// TODO remaining constructor logic
|
// TODO remaining constructor logic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -39,6 +39,7 @@ class CGxTexParms {
|
|||||||
CGxTexFlags flags = CGxTexFlags(GxTex_Linear, 0, 0, 0, 0, 0, 1);
|
CGxTexFlags flags = CGxTexFlags(GxTex_Linear, 0, 0, 0, 0, 0, 1);
|
||||||
void* userArg;
|
void* userArg;
|
||||||
void (*userFunc)(EGxTexCommand, uint32_t, uint32_t, uint32_t, uint32_t, void*, uint32_t&, const void*&);
|
void (*userFunc)(EGxTexCommand, uint32_t, uint32_t, uint32_t, uint32_t, void*, uint32_t&, const void*&);
|
||||||
|
const char* name = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
class CGxTex {
|
class CGxTex {
|
||||||
@ -61,6 +62,7 @@ class CGxTex {
|
|||||||
uint8_t m_needsUpdate;
|
uint8_t m_needsUpdate;
|
||||||
uint8_t m_needsCreation;
|
uint8_t m_needsCreation;
|
||||||
uint8_t m_needsFlagUpdate;
|
uint8_t m_needsFlagUpdate;
|
||||||
|
char m_name[260];
|
||||||
|
|
||||||
// Member functions
|
// Member functions
|
||||||
CGxTex(EGxTexTarget, uint32_t, uint32_t, uint32_t, EGxTexFormat, EGxTexFormat, CGxTexFlags, void*, void (*)(EGxTexCommand, uint32_t, uint32_t, uint32_t, uint32_t, void*, uint32_t&, const void*&), const char*);
|
CGxTex(EGxTexTarget, uint32_t, uint32_t, uint32_t, EGxTexFormat, EGxTexFormat, CGxTexFlags, void*, void (*)(EGxTexCommand, uint32_t, uint32_t, uint32_t, uint32_t, void*, uint32_t&, const void*&), const char*);
|
||||||
|
|||||||
@ -20,6 +20,17 @@ if(WHOA_SYSTEM_MAC OR WHOA_SYSTEM_LINUX)
|
|||||||
list(APPEND PRIVATE_SOURCES ${BSD_SOURCES})
|
list(APPEND PRIVATE_SOURCES ${BSD_SOURCES})
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(WHOA_SYSTEM_WEB)
|
||||||
|
list(REMOVE_ITEM PRIVATE_SOURCES
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/connection/WowConnection.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/connection/WowConnectionNet.cpp
|
||||||
|
)
|
||||||
|
file(GLOB WEB_SOURCES
|
||||||
|
"connection/web/*.cpp"
|
||||||
|
)
|
||||||
|
list(APPEND PRIVATE_SOURCES ${WEB_SOURCES})
|
||||||
|
endif()
|
||||||
|
|
||||||
add_library(net STATIC
|
add_library(net STATIC
|
||||||
${PRIVATE_SOURCES}
|
${PRIVATE_SOURCES}
|
||||||
)
|
)
|
||||||
@ -46,3 +57,10 @@ if(WHOA_SYSTEM_WIN)
|
|||||||
wsock32
|
wsock32
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(WHOA_SYSTEM_WEB)
|
||||||
|
target_link_options(net
|
||||||
|
PUBLIC
|
||||||
|
-lwebsocket.js
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|||||||
@ -16,6 +16,10 @@
|
|||||||
#include <winsock2.h>
|
#include <winsock2.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(WHOA_SYSTEM_WEB)
|
||||||
|
struct sockaddr_in;
|
||||||
|
#endif
|
||||||
|
|
||||||
class CDataStore;
|
class CDataStore;
|
||||||
class WowConnectionNet;
|
class WowConnectionNet;
|
||||||
class WowConnectionResponse;
|
class WowConnectionResponse;
|
||||||
|
|||||||
@ -8,6 +8,9 @@ class WowConnection;
|
|||||||
|
|
||||||
class WowConnectionResponse {
|
class WowConnectionResponse {
|
||||||
public:
|
public:
|
||||||
|
// Virtual destructor
|
||||||
|
virtual ~WowConnectionResponse() = default;
|
||||||
|
|
||||||
// Virtual member functions
|
// Virtual member functions
|
||||||
virtual void WCMessageReady(WowConnection* conn, uint32_t timeStamp, CDataStore* msg) = 0;
|
virtual void WCMessageReady(WowConnection* conn, uint32_t timeStamp, CDataStore* msg) = 0;
|
||||||
virtual void WCConnected(WowConnection* conn, WowConnection* inbound, uint32_t timeStamp, const NETCONNADDR* addr) = 0;
|
virtual void WCConnected(WowConnection* conn, WowConnection* inbound, uint32_t timeStamp, const NETCONNADDR* addr) = 0;
|
||||||
|
|||||||
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")
|
file(GLOB PRIVATE_SOURCES "*.cpp")
|
||||||
|
|
||||||
|
if(WHOA_SYSTEM_WEB)
|
||||||
|
# For web builds, exclude FMOD-dependent files and use stubs
|
||||||
|
list(REMOVE_ITEM PRIVATE_SOURCES
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SESound.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SESoundInternal.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
file(GLOB WEB_SOURCES "web/*.cpp")
|
||||||
|
list(APPEND PRIVATE_SOURCES ${WEB_SOURCES})
|
||||||
|
endif()
|
||||||
|
|
||||||
add_library(sound STATIC
|
add_library(sound STATIC
|
||||||
${PRIVATE_SOURCES}
|
${PRIVATE_SOURCES}
|
||||||
)
|
)
|
||||||
@ -13,6 +24,11 @@ target_link_libraries(sound
|
|||||||
PRIVATE
|
PRIVATE
|
||||||
ui
|
ui
|
||||||
util
|
util
|
||||||
|
)
|
||||||
|
|
||||||
|
if(NOT WHOA_SYSTEM_WEB)
|
||||||
|
target_link_libraries(sound
|
||||||
PUBLIC
|
PUBLIC
|
||||||
fmod
|
fmod
|
||||||
)
|
)
|
||||||
|
endif()
|
||||||
|
|||||||
@ -5,10 +5,12 @@
|
|||||||
#include "sound/SESoundInternal.hpp"
|
#include "sound/SESoundInternal.hpp"
|
||||||
#include "sound/SEUserData.hpp"
|
#include "sound/SEUserData.hpp"
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <fmod.hpp>
|
|
||||||
#include <storm/Hash.hpp>
|
#include <storm/Hash.hpp>
|
||||||
#include <storm/Thread.hpp>
|
#include <storm/Thread.hpp>
|
||||||
|
|
||||||
|
#if !defined(WHOA_SYSTEM_WEB)
|
||||||
|
#include <fmod.hpp>
|
||||||
|
|
||||||
struct SOUND_INTERNAL_LOOKUP : TSHashObject<SOUND_INTERNAL_LOOKUP, HASHKEY_NONE> {
|
struct SOUND_INTERNAL_LOOKUP : TSHashObject<SOUND_INTERNAL_LOOKUP, HASHKEY_NONE> {
|
||||||
SESoundInternal* m_internal;
|
SESoundInternal* m_internal;
|
||||||
};
|
};
|
||||||
@ -65,4 +67,40 @@ class SESound {
|
|||||||
SESoundInternal* m_internal = nullptr;
|
SESoundInternal* m_internal = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#else // WHOA_SYSTEM_WEB
|
||||||
|
|
||||||
|
// Stub SESound class for web builds - sound is not supported
|
||||||
|
class SESound {
|
||||||
|
public:
|
||||||
|
// Public static variables
|
||||||
|
static TSGrowableArray<SEChannelGroup> s_ChannelGroups;
|
||||||
|
static int32_t s_Initialized;
|
||||||
|
|
||||||
|
// Public static functions
|
||||||
|
static void* CreateSoundGroup(const char* name, int32_t maxAudible);
|
||||||
|
static SEChannelGroup* GetChannelGroup(const char* name, bool create, bool createInMaster);
|
||||||
|
static float GetChannelGroupVolume(const char* name);
|
||||||
|
static int32_t Heartbeat(const void* data, void* param);
|
||||||
|
static void Init(int32_t maxChannels, int32_t (*a2), int32_t enableReverb, int32_t enableSoftwareHRTF, int32_t* numChannels, int32_t* outputDriverIndex, const char* outputDriverName, void (*a8), int32_t a9);
|
||||||
|
static int32_t IsInitialized();
|
||||||
|
static void MuteChannelGroup(const char* name, bool mute);
|
||||||
|
static void SetChannelGroupVolume(const char* name, float volume);
|
||||||
|
static void SetMasterVolume(float volume);
|
||||||
|
|
||||||
|
// Public member functions
|
||||||
|
void CompleteLoad();
|
||||||
|
SEUserData* GetUserData();
|
||||||
|
bool IsPlaying();
|
||||||
|
int32_t Load(const char* filename, int32_t a3, void* soundGroup1, void* soundGroup2, bool a6, bool a7, uint32_t a8, int32_t a9, uint32_t a10);
|
||||||
|
void Play();
|
||||||
|
void SetChannelGroup(const char* name, bool inMaster);
|
||||||
|
void SetFadeInTime(float fadeInTime);
|
||||||
|
void SetFadeOutTime(float fadeOutTime);
|
||||||
|
void SetUserData(SEUserData* userData);
|
||||||
|
void SetVolume(float volume);
|
||||||
|
void StopOrFadeOut(int32_t stop, float fadeOutTime);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // WHOA_SYSTEM_WEB
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -2,13 +2,18 @@
|
|||||||
#define SOUND_SE_SOUND_INTERNAL_HPP
|
#define SOUND_SE_SOUND_INTERNAL_HPP
|
||||||
|
|
||||||
#include <storm/List.hpp>
|
#include <storm/List.hpp>
|
||||||
#include <fmod.hpp>
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
|
#if !defined(WHOA_SYSTEM_WEB)
|
||||||
|
#include <fmod.hpp>
|
||||||
|
#endif
|
||||||
|
|
||||||
class SESound;
|
class SESound;
|
||||||
class SEUserData;
|
class SEUserData;
|
||||||
class SFile;
|
class SFile;
|
||||||
|
|
||||||
|
#if !defined(WHOA_SYSTEM_WEB)
|
||||||
|
|
||||||
class SoundCacheNode : public TSLinkedNode<SoundCacheNode> {
|
class SoundCacheNode : public TSLinkedNode<SoundCacheNode> {
|
||||||
public:
|
public:
|
||||||
// Member variables
|
// Member variables
|
||||||
@ -84,4 +89,6 @@ class SEStreamedSound : public SESoundInternal {
|
|||||||
// TODO
|
// TODO
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#endif // !defined(WHOA_SYSTEM_WEB)
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -4,9 +4,11 @@
|
|||||||
#include "db/Db.hpp"
|
#include "db/Db.hpp"
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
|
#if !defined(WHOA_SYSTEM_WEB)
|
||||||
namespace FMOD {
|
namespace FMOD {
|
||||||
class SoundGroup;
|
class SoundGroup;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
class SOUNDKITDEF {
|
class SOUNDKITDEF {
|
||||||
public:
|
public:
|
||||||
@ -20,8 +22,13 @@ class SOUNDKITDEF {
|
|||||||
int32_t fileCount = 0;
|
int32_t fileCount = 0;
|
||||||
int32_t selectedIndex = -1;
|
int32_t selectedIndex = -1;
|
||||||
// TODO: 0x48 - 0x94
|
// TODO: 0x48 - 0x94
|
||||||
|
#if !defined(WHOA_SYSTEM_WEB)
|
||||||
FMOD::SoundGroup* soundGroup1;
|
FMOD::SoundGroup* soundGroup1;
|
||||||
FMOD::SoundGroup* soundGroup2;
|
FMOD::SoundGroup* soundGroup2;
|
||||||
|
#else
|
||||||
|
void* soundGroup1;
|
||||||
|
void* soundGroup2;
|
||||||
|
#endif
|
||||||
// TODO: 0xA0 - 0xA4
|
// TODO: 0xA0 - 0xA4
|
||||||
int32_t advancedID = 0;
|
int32_t advancedID = 0;
|
||||||
SoundEntriesAdvancedRec* advanced;
|
SoundEntriesAdvancedRec* advanced;
|
||||||
|
|||||||
77
src/sound/web/SESound.cpp
Normal file
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
|
Threads::Threads
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(WHOA_SYSTEM_WEB)
|
||||||
|
set(FETCHFS_LIBRARY ${CMAKE_CURRENT_SOURCE_DIR}/web/library_fetchfs.js)
|
||||||
|
target_link_options(util
|
||||||
|
PUBLIC
|
||||||
|
--js-library ${FETCHFS_LIBRARY}
|
||||||
|
)
|
||||||
|
# Ensure rebuild when JS library changes
|
||||||
|
set_property(TARGET util APPEND PROPERTY LINK_DEPENDS ${FETCHFS_LIBRARY})
|
||||||
|
endif()
|
||||||
|
|||||||
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()
|
endif()
|
||||||
|
|
||||||
|
if(WHOA_SYSTEM_WEB)
|
||||||
|
file(GLOB PRIVATE_SOURCES
|
||||||
|
"Test.cpp"
|
||||||
|
"gx/*.cpp"
|
||||||
|
"gx/font/*.cpp"
|
||||||
|
"util/*.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(WhoaTest ${PRIVATE_SOURCES})
|
||||||
|
|
||||||
|
target_link_libraries(WhoaTest
|
||||||
|
PRIVATE
|
||||||
|
client
|
||||||
|
event
|
||||||
|
gx
|
||||||
|
util
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
target_include_directories(WhoaTest
|
target_include_directories(WhoaTest
|
||||||
PRIVATE
|
PRIVATE
|
||||||
${CMAKE_SOURCE_DIR}/src
|
${CMAKE_SOURCE_DIR}/src
|
||||||
|
|||||||
2
vendor/CMakeLists.txt
vendored
2
vendor/CMakeLists.txt
vendored
@ -58,7 +58,7 @@ if(WHOA_SYSTEM_WIN)
|
|||||||
else()
|
else()
|
||||||
set_target_properties(fmod PROPERTIES
|
set_target_properties(fmod PROPERTIES
|
||||||
IMPORTED_IMPLIB ${FMOD_DIR}/win-x86_64/lib/fmod_vc.lib
|
IMPORTED_IMPLIB ${FMOD_DIR}/win-x86_64/lib/fmod_vc.lib
|
||||||
IMPORTED_LOCATION FMOD_DIR}/win-x86_64/lib/fmod.dll
|
IMPORTED_LOCATION ${FMOD_DIR}/win-x86_64/lib/fmod.dll
|
||||||
INTERFACE_INCLUDE_DIRECTORIES ${FMOD_DIR}/win-x86_64/inc
|
INTERFACE_INCLUDE_DIRECTORIES ${FMOD_DIR}/win-x86_64/inc
|
||||||
)
|
)
|
||||||
install(FILES ${FMOD_DIR}/win-x86_64/lib/fmod.dll DESTINATION "bin")
|
install(FILES ${FMOD_DIR}/win-x86_64/lib/fmod.dll DESTINATION "bin")
|
||||||
|
|||||||
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
|
static FT_Error
|
||||||
open_face( FT_Driver driver,
|
open_face( FT_Driver driver,
|
||||||
FT_Stream stream,
|
FT_Stream stream,
|
||||||
FT_Long face_index,
|
FT_Int face_index,
|
||||||
FT_Int num_params,
|
FT_Int num_params,
|
||||||
FT_Parameter* params,
|
FT_Parameter* params,
|
||||||
FT_Face* aface )
|
FT_Face* aface )
|
||||||
|
|||||||
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
|
#define FT_COMPONENT trace_pcfdriver
|
||||||
|
|
||||||
|
|
||||||
static FT_Error
|
static void
|
||||||
PCF_Face_Done( PCF_Face face )
|
PCF_Face_Done( PCF_Face face )
|
||||||
{
|
{
|
||||||
FT_Memory memory = FT_FACE_MEMORY( face );
|
FT_Memory memory = FT_FACE_MEMORY( face );
|
||||||
@ -81,8 +81,6 @@ THE SOFTWARE.
|
|||||||
FREE( face->charset_registry );
|
FREE( face->charset_registry );
|
||||||
|
|
||||||
FT_TRACE4(( "DONE_FACE!!!\n" ));
|
FT_TRACE4(( "DONE_FACE!!!\n" ));
|
||||||
|
|
||||||
return PCF_Err_Ok;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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 */
|
/* check that we have a valid TrueType file */
|
||||||
error = sfnt->load_sfnt_header( face, stream, face_index, &sfnt_header );
|
error = sfnt->load_sfnt_header( face, stream, (FT_Long)face_index, &sfnt_header );
|
||||||
if ( error )
|
if ( error )
|
||||||
goto Exit;
|
goto Exit;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user