diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b1aec7cb..50aa85d8e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.16...3.28 FATAL_ERROR) project(EasyRPG_Player VERSION 0.8 DESCRIPTION "Interpreter for RPG Maker 2000/2003 games" HOMEPAGE_URL "https://easyrpg.org" - LANGUAGES CXX) + LANGUAGES C CXX) # Extra CMake Module files list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/builds/cmake/Modules") @@ -13,6 +13,7 @@ include(PlayerFindPackage) include(PlayerBuildType) include(PlayerMisc) include(GetGitRevisionDescription) +include(CheckCSourceCompiles) # Dependencies provided by CMake Presets list(APPEND CMAKE_PREFIX_PATH "${PLAYER_PREFIX_PATH_APPEND}") @@ -922,6 +923,30 @@ else() TARGET nlohmann_json::nlohmann_json ONLY_CONFIG ) + + find_package(Libwebsockets CONFIG REQUIRED) + require_lws_config(LWS_ROLE_H1 1 requirements) + require_lws_config(LWS_WITHOUT_CLIENT 0 requirements) + require_lws_config(LWS_WITH_SECURE_STREAMS 1 requirements) + # require_lws_config(LWS_WITH_SECURE_STREAMS_CPP 1 requirements) + require_lws_config(LWS_WITH_SECURE_STREAMS_STATIC_POLICY_ONLY 0 requirements) + require_lws_config(LWS_WITH_TLS 1 requirements) + require_lws_config(LWS_WITH_SECURE_STREAMS_AUTH_SIGV4 0 requirements) + + # uses system trust store + require_lws_config(LWS_WITH_MBEDTLS 0 requirements) + require_lws_config(LWS_WITH_WOLFSSL 0 requirements) + require_lws_config(LWS_WITH_CYASSL 0 requirements) + + # require_lws_config(LWS_WITH_SYS_FAULT_INJECTION 1 has_fault_injection) + # require_lws_config(LWS_WITH_SECURE_STREAMS_PROXY_API 1 has_ss_proxy) + # require_lws_config(LWS_WITH_SYS_STATE 1 has_sys_state) + if(websockets_shared) + target_link_libraries(${PROJECT_NAME} websockets_shared ${LIBWEBSOCKETS_DEP_LIBS}) + add_dependencies(${PROJECT_NAME} websockets_shared) + else() + target_link_libraries(${PROJECT_NAME} websockets ${LIBWEBSOCKETS_DEP_LIBS}) + endif() endif() # Sound system to use diff --git a/builds/android/app/build.gradle b/builds/android/app/build.gradle index b00b9396a..da7b496de 100644 --- a/builds/android/app/build.gradle +++ b/builds/android/app/build.gradle @@ -51,7 +51,8 @@ android { cmake { arguments "-DPLAYER_GRADLE_BUILD=ON", "-DBUILD_SHARED_LIBS=ON", - "-DPLAYER_ENABLE_TESTS=OFF" + "-DPLAYER_ENABLE_TESTS=OFF", + "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON" if (project.hasProperty("toolchainDirs")) { arguments.add('-DPLAYER_ANDROID_TOOLCHAIN_PATH=' + project.properties['toolchainDirs']) diff --git a/src/game_map.cpp b/src/game_map.cpp index 7c369810a..f2bd27605 100644 --- a/src/game_map.cpp +++ b/src/game_map.cpp @@ -56,7 +56,7 @@ #include #include "scene_gameover.h" #include "multiplayer/game_multiplayer.h" -#include +#include "web_api.h" #include "feature.h" namespace { @@ -344,9 +344,7 @@ std::unique_ptr Game_Map::LoadMapFile(int map_id) { Output::Debug("Loaded Map {}", map_name); - EM_ASM({ - onLoadMap(UTF8ToString($0)); - }, map_name.c_str()); + Web_API::OnLoadMap(map_name); if (map.get() == NULL) { Output::ErrorStr(lcf::LcfReader::GetError()); @@ -1252,7 +1250,7 @@ void Game_Map::Update(MapUpdateAsyncContext& actx, bool is_preupdate) { //If not resuming from async op ... Main_Data::game_player->Update(); GMI().Update(); - + for (auto& vehicle: vehicles) { if (vehicle.GetMapId() == GetMapId()) { vehicle.Update(); diff --git a/src/multiplayer/yno_connection.cpp b/src/multiplayer/yno_connection.cpp index 2fb7ebbd2..0bb1d5c93 100644 --- a/src/multiplayer/yno_connection.cpp +++ b/src/multiplayer/yno_connection.cpp @@ -1,19 +1,32 @@ #include "yno_connection.h" -#include +#include "output.h" +#ifdef __EMSCRIPTEN__ +# include +#else +# include +# include +#endif #include "../external/TinySHA1.hpp" struct YNOConnection::IMPL { +#ifdef __EMSCRIPTEN__ EMSCRIPTEN_WEBSOCKET_T socket; +#else + lws_ss_handle* handle; + lws_context* cx; +#endif + uint32_t msg_count; bool closed; - static EM_BOOL onopen(int eventType, const EmscriptenWebSocketOpenEvent *event, void *userData) { +#ifdef __EMSCRIPTEN__ + static bool onopen(int eventType, const EmscriptenWebSocketOpenEvent *event, void *userData) { auto _this = static_cast(userData); _this->SetConnected(true); _this->DispatchSystem(SystemMessage::OPEN); - return EM_TRUE; + return true; } - static EM_BOOL onclose(int eventType, const EmscriptenWebSocketCloseEvent *event, void *userData) { + static bool onclose(int eventType, const EmscriptenWebSocketCloseEvent *event, void *userData) { auto _this = static_cast(userData); _this->SetConnected(false); _this->DispatchSystem( @@ -21,16 +34,16 @@ struct YNOConnection::IMPL { SystemMessage::EXIT : SystemMessage::CLOSE ); - return EM_TRUE; + return true; } - static EM_BOOL onmessage(int eventType, const EmscriptenWebSocketMessageEvent *event, void *userData) { + static bool onmessage(int eventType, const EmscriptenWebSocketMessageEvent *event, void *userData) { auto _this = static_cast(userData); // IMPORTANT!! numBytes is always one byte larger than the actual length // so the actual length is numBytes - 1 // NOTE: that extra byte is just in text mode, and it does not exist in binary mode if (event->isText) { - return EM_FALSE; + return false; } std::string_view cstr(reinterpret_cast(event->data), event->numBytes); std::vector mstrs = Split(cstr, Multiplayer::Packet::MSG_DELIM); @@ -50,14 +63,146 @@ struct YNOConnection::IMPL { _this->Dispatch(namestr, Split(argstr)); } } - return EM_TRUE; + return true; } +#else + typedef struct { + struct lws_ss_handle* ss; + void* opaque_data; + + // custom logic fields begin + YNOConnection* conn; + // custom logic fields end + + lws_sorted_usec_list_t sul; + int count; + bool due; + } yno_socket_t; + + static lws_ss_state_return_t yno_socket_rx(void* userData, const uint8_t* in, size_t len, int flags) + { + auto* self = static_cast(userData); + auto* h = self->ss; + + std::string_view cstr(reinterpret_cast(in), len); + std::vector mstrs = Split(cstr, Multiplayer::Packet::MSG_DELIM); + for (auto& mstr : mstrs) { + auto p = mstr.find(Multiplayer::Packet::PARAM_DELIM); + if (p == mstr.npos) { + /* + Usually npos is the maximum value of size_t. + Adding to it is undefined behavior. + If it returns end iterator instead of npos, the if statement is + duplicated code because the statement in else clause will handle it. + */ + self->conn->Dispatch(mstr); + } else { + auto namestr = mstr.substr(0, p); + auto argstr = mstr.substr(p + Multiplayer::Packet::PARAM_DELIM.size()); + self->conn->Dispatch(namestr, Split(argstr)); + } + } + return LWSSSSRET_OK; + } + + static constexpr uint RATE_US = 50000; + static constexpr uint PKT_SIZE = 80; + + static void yno_socket_txcb(struct lws_sorted_usec_list* sul) + { + auto* self = lws_container_of(sul, yno_socket_t, sul); + self->due = true; + if (lws_ss_request_tx(self->ss) != LWSSSSRET_OK) { + // TODO: The fuck you expect me to do? + } + + lws_sul_schedule(lws_ss_get_context(self->ss), 0, &self->sul, yno_socket_txcb, RATE_US); + } + + static lws_ss_state_return_t yno_socket_tx(void *userobj, lws_ss_tx_ordinal_t ord, uint8_t *buf, size_t *len, int *flags) + { + auto* self = static_cast(userobj); + if (!self->due) + return LWSSSSRET_TX_DONT_SEND; + + self->due = false; + + if (lws_get_random(lws_ss_get_context(self->ss), buf, PKT_SIZE) != PKT_SIZE) + return LWSSSSRET_TX_DONT_SEND; + + *len = PKT_SIZE; + *flags = LWSSS_FLAG_SOM | LWSSS_FLAG_EOM; + + self->count++; + + lws_sul_schedule(lws_ss_get_context(self->ss), 0, &self->sul, yno_socket_txcb, RATE_US); + + return LWSSSSRET_OK; + } + + + static lws_ss_state_return_t yno_socket_state(void* userData, void* h_src, lws_ss_constate_t state, lws_ss_tx_ordinal_t sck) + { + auto* self = static_cast(userData); + switch (state) { + case LWSSSCS_CREATING: + return lws_ss_request_tx(self->ss); + case LWSSSCS_CONNECTED: + self->conn->SetConnected(true); + self->conn->DispatchSystem(SystemMessage::OPEN); + lws_sul_schedule(lws_ss_get_context(self->ss), 0, &self->sul, yno_socket_txcb, RATE_US); + break; + case LWSSSCS_DISCONNECTED: + self->conn->SetConnected(false); + self->conn->DispatchSystem(SystemMessage::CLOSE); + lws_sul_cancel(&self->sul); + break; + default: + break; + } + return LWSSSSRET_OK; + } + + static constexpr lws_ss_info_t ssi { + .streamtype = "yno", + .user_alloc = sizeof(yno_socket_t), + .handle_offset = offsetof(yno_socket_t, ss), + .opaque_user_data_offset = offsetof(yno_socket_t, opaque_data), + .rx = yno_socket_rx, + .tx = yno_socket_tx, + .state = yno_socket_state, + }; +#endif + +#ifdef __EMSCRIPTEN__ static void set_callbacks(int socket, void* userData) { emscripten_websocket_set_onopen_callback(socket, userData, onopen); emscripten_websocket_set_onclose_callback(socket, userData, onclose); emscripten_websocket_set_onmessage_callback(socket, userData, onmessage); } +#endif + void initWs(lws_ss_policy* policy) { +#ifndef __EMSCRIPTEN__ + lws_set_log_level(LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE, nullptr); + lws_context_creation_info info { + .protocols = lws_sspc_protocols, + .options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT, + .fd_limit_per_thread = 1 + 6 + 1, + .port = CONTEXT_PORT_NO_LISTEN, + .pss_policies = policy, + }; + cx = lws_create_context(&info); + if (!cx) Output::ErrorStr("lws_create_context failed"); + + if (lws_ss_create(cx, 0, &ssi, nullptr, &handle, nullptr, nullptr)) + Output::ErrorStr("lws_ss_create failed"); + + std::thread event_thread([](lws_context* cx) { + while (!lws_service(cx, 0)); + }, cx); + } +#endif }; const size_t YNOConnection::MAX_QUEUE_SIZE{ 4088 }; @@ -70,14 +215,18 @@ YNOConnection::YNOConnection() : impl(new IMPL) { YNOConnection::YNOConnection(YNOConnection&& o) : Connection(std::move(o)), impl(std::move(o.impl)) { +#ifdef __EMSCRIPTEN__ IMPL::set_callbacks(impl->socket, this); +#endif } YNOConnection& YNOConnection::operator=(YNOConnection&& o) { Connection::operator=(std::move(o)); if (this != &o) { Close(); impl = std::move(o.impl); +#ifdef __EMSCRIPTEN__ IMPL::set_callbacks(impl->socket, this); +#endif } return *this; } @@ -92,6 +241,7 @@ void YNOConnection::Open(std::string_view uri) { Close(); } +#ifdef __EMSCRIPTEN__ std::string s {uri}; EmscriptenWebSocketCreateAttributes ws_attrs = { s.data(), @@ -101,6 +251,15 @@ void YNOConnection::Open(std::string_view uri) { impl->socket = emscripten_websocket_new(&ws_attrs); impl->closed = false; IMPL::set_callbacks(impl->socket, this); +#else + const lws_ss_policy_t yno_policy { + .streamtype = "yno", + .endpoint = uri.data(), + .port = 443, + .protocol = (uint8_t)(uri.find("wss") == 0 ? 1 : 0), + }; +#endif + impl->initWs(); } void YNOConnection::Close() { @@ -108,11 +267,15 @@ void YNOConnection::Close() { if (impl->closed) return; impl->closed = true; +#ifdef __EMSCRIPTEN__ // strange bug: // calling with (impl->socket, 1005, "any reason") raises exceptions // might be an emscripten bug emscripten_websocket_close(impl->socket, 0, nullptr); emscripten_websocket_delete(impl->socket); +#else + if (impl->handle) lws_ss_destroy(&impl->handle); +#endif } template @@ -182,7 +345,11 @@ void YNOConnection::Send(std::string_view data) { if (!IsConnected()) return; unsigned short ready; +#ifdef __EMSCRIPTEN__ emscripten_websocket_get_ready_state(impl->socket, &ready); +#else + ready = impl->ready; +#endif if (ready == 1) { // OPEN ++impl->msg_count; auto sendmsg = calculate_header(GetKey(), impl->msg_count, data); diff --git a/src/web_api.cpp b/src/web_api.cpp index b116f8c1e..7b0d38dab 100644 --- a/src/web_api.cpp +++ b/src/web_api.cpp @@ -1,6 +1,10 @@ #include "web_api.h" -#include "emscripten/emscripten.h" -#include "output.h" +#ifdef __EMSCRIPTEN__ +#include +#else +#define EM_ASM_INT(x, ...) 0 +#define EM_ASM(x, ...) +#endif using namespace Web_API;