diff --git a/CMakeLists.txt b/CMakeLists.txt index 50aa85d8e..690e1d3b2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -927,11 +927,11 @@ else() 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 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_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) + # require_lws_config(LWS_WITH_SECURE_STREAMS_AUTH_SIGV4 0 requirements) # uses system trust store require_lws_config(LWS_WITH_MBEDTLS 0 requirements) @@ -947,6 +947,13 @@ else() else() target_link_libraries(${PROJECT_NAME} websockets ${LIBWEBSOCKETS_DEP_LIBS}) endif() + + #find_package(websocketpp REQUIRED) + #add_dependencies(${PROJECT_NAME} websocketpp::websocketpp) + #find_package(httplib CONFIG REQUIRED) + #target_link_libraries(${PROJECT_NAME} httplib::httplib) + find_package(cpr CONFIG REQUIRED) + target_link_libraries(${PROJECT_NAME} cpr::cpr) endif() # Sound system to use @@ -1162,7 +1169,8 @@ if(${PLAYER_BUILD_EXECUTABLE} AND ${PLAYER_TARGET_PLATFORM} MATCHES "^SDL[12]$" if(WIN32) # Open console for Debug builds - set_target_properties(${EXE_NAME} PROPERTIES WIN32_EXECUTABLE $<$>:TRUE>) + #set_target_properties(${EXE_NAME} PROPERTIES WIN32_EXECUTABLE $<$>:TRUE>) + set_target_properties(${EXE_NAME} PROPERTIES WIN32_EXECUTABLE FALSE) # Add resources string(REPLACE "." "," RC_VERSION ${PROJECT_VERSION}) @@ -1177,6 +1185,7 @@ if(${PLAYER_BUILD_EXECUTABLE} AND ${PLAYER_TARGET_PLATFORM} MATCHES "^SDL[12]$" # Change executable name set_target_properties(${EXE_NAME} PROPERTIES OUTPUT_NAME "Player") + target_link_libraries(${EXE_NAME} dbghelp) endif() target_link_libraries(${EXE_NAME} ${PROJECT_NAME}) diff --git a/src/game_actor.cpp b/src/game_actor.cpp index 423a23037..91bd0bffc 100644 --- a/src/game_actor.cpp +++ b/src/game_actor.cpp @@ -1147,7 +1147,7 @@ void Game_Actor::ChangeClass(int new_class_id, } } -StringView Game_Actor::GetClassName() const { +StringView Game_Actor::ClassName() const { if (!GetClass()) { return {}; } diff --git a/src/game_actor.h b/src/game_actor.h index 5627a8731..e6a9ec033 100644 --- a/src/game_actor.h +++ b/src/game_actor.h @@ -794,7 +794,7 @@ class Game_Actor final : public Game_Battler { * * @return Rpg2k3 hero class name */ - StringView GetClassName() const; + StringView ClassName() const; /** * Gets battle commands. diff --git a/src/game_pictures.cpp b/src/game_pictures.cpp index baf93829b..2b69fd507 100644 --- a/src/game_pictures.cpp +++ b/src/game_pictures.cpp @@ -101,7 +101,7 @@ std::vector Game_Pictures::GetSaveData() const { auto data_size = GetDefaultNumberOfPictures(); save.reserve(data_size); - int idx; + int idx{ 0 }; for (auto& pic: pictures) { if (++idx > data_size) break; save.push_back(pic.data); diff --git a/src/multiplayer/game_multiplayer.cpp b/src/multiplayer/game_multiplayer.cpp index db4a18468..5ffea8980 100644 --- a/src/multiplayer/game_multiplayer.cpp +++ b/src/multiplayer/game_multiplayer.cpp @@ -39,6 +39,10 @@ #include "yno_connection.h" #include "messages.h" +#ifndef EMSCRIPTEN +# include +#endif + static Game_Multiplayer _instance; Game_Multiplayer& Game_Multiplayer::Instance() { @@ -125,10 +129,7 @@ static std::string get_room_url(int room_id, std::string_view session_token) { return room_url; } -void Game_Multiplayer::InitConnection() { - /* - conn.RegisterSystemHandler(YNOConnection::SystemMessage::OPEN, [] (Multiplayer::Connection& c) { - });*/ +void Game_Multiplayer::InitConnection() { using YSM = YNOConnection::SystemMessage; using MCo = Multiplayer::Connection; connection.RegisterSystemHandler(YSM::CLOSE, [this] (MCo& c) { @@ -150,6 +151,7 @@ void Game_Multiplayer::InitConnection() { connection.RegisterHandler("s", [this] (SyncPlayerDataPacket& p) { host_id = p.host_id; auto key_num = std::stoul(p.key); + Output::Debug("[{}] syncplayerdata key={}", room_id, key_num); //if (key_num > std::numeric_limits::max()) { // std::terminate(); //} @@ -473,9 +475,20 @@ void Game_Multiplayer::InitConnection() { cu_temperature = p.temperature; cu_precipitation = p.precipitation; }); +#ifndef EMSCRIPTEN + sessionConn.RegisterSystemHandler(YSM::OPEN, [this](MCo&) { + session_active = true; + }); + sessionConn.RegisterSystemHandler(YSM::CLOSE, [this](MCo&) { + Output::Error("[session] exited"); + }); + sessionConn.RegisterHandler("gsay", [](SessionGSay& p) { + Output::Debug("{}: {}", p.uuid, p.msg); + }); +#endif } -using namespace Messages::C2S; +namespace C2S = Messages::C2S; void Game_Multiplayer::Connect(int map_id, bool room_switch) { Output::Debug("MP: connecting to id={}", map_id); @@ -515,6 +528,28 @@ void Game_Multiplayer::Initialize() { } } +void Game_Multiplayer::InitSession() { +#ifndef EMSCRIPTEN + std::string formdata = "user=&password="; + std::string sessionEndpoint = Web_API::GetSocketURL() + "session"; + cpr::Header header; + header["Content-Type"] = "application/x-www-form-urlencoded"; + + auto resp = cpr::Post( + cpr::Url{ "https://connect.ynoproject.net/yume/api/login" }, + cpr::Body{ formdata }, + header); + if (resp.status_code >= 200 && resp.status_code < 300) { + session_token = resp.text; + sessionEndpoint += "?token=" + session_token; + } + else { + Output::Debug("login: {} {}", resp.status_code, resp.text); + } + sessionConn.Open(sessionEndpoint); +#endif +} + void Game_Multiplayer::Quit() { Web_API::UpdateConnectionStatus(0); // disconnected connection.Close(); @@ -539,25 +574,25 @@ void Game_Multiplayer::SendBasicData() { void Game_Multiplayer::MainPlayerMoved(int dir) { auto& p = Main_Data::game_player; - connection.SendPacketAsync(p->GetX(), p->GetY()); + connection.SendPacketAsync(p->GetX(), p->GetY()); } void Game_Multiplayer::MainPlayerFacingChanged(int dir) { - connection.SendPacketAsync(dir); + connection.SendPacketAsync(dir); } void Game_Multiplayer::MainPlayerChangedMoveSpeed(int spd) { - connection.SendPacketAsync(spd); + connection.SendPacketAsync(spd); } void Game_Multiplayer::MainPlayerChangedSpriteGraphic(std::string name, int index) { - connection.SendPacketAsync(name, index); + connection.SendPacketAsync(name, index); Web_API::OnPlayerSpriteUpdated(name, index); } void Game_Multiplayer::MainPlayerJumped(int x, int y) { auto& p = Main_Data::game_player; - connection.SendPacketAsync(x, y); + connection.SendPacketAsync(x, y); } void Game_Multiplayer::MainPlayerFlashed(int r, int g, int b, int p, int f) { @@ -565,26 +600,26 @@ void Game_Multiplayer::MainPlayerFlashed(int r, int g, int b, int p, int f) { if (last_flash_frame_index == frame_index - 1 && (last_frame_flash.get() == nullptr || *last_frame_flash == flash_array)) { if (last_frame_flash.get() == nullptr) { last_frame_flash = std::make_unique>(flash_array); - connection.SendPacketAsync(r, g, b, p, f); + connection.SendPacketAsync(r, g, b, p, f); } } else { - connection.SendPacketAsync(r, g, b, p, f); + connection.SendPacketAsync(r, g, b, p, f); last_frame_flash.reset(); } last_flash_frame_index = frame_index; } void Game_Multiplayer::MainPlayerChangedTransparency(int transparency) { - connection.SendPacketAsync(transparency); + connection.SendPacketAsync(transparency); } void Game_Multiplayer::MainPlayerChangedSpriteHidden(bool hidden) { int hidden_bin = hidden ? 1 : 0; - connection.SendPacketAsync(hidden_bin); + connection.SendPacketAsync(hidden_bin); } void Game_Multiplayer::MainPlayerTeleported(int map_id, int x, int y) { - connection.SendPacketAsync(x, y); + connection.SendPacketAsync(x, y); Web_API::OnPlayerTeleported(map_id, x, y); } @@ -604,13 +639,13 @@ void Game_Multiplayer::MainPlayerTriggeredEvent(int event_id, bool action) { } void Game_Multiplayer::SystemGraphicChanged(StringView sys) { - connection.SendPacketAsync(ToString(sys)); + connection.SendPacketAsync(ToString(sys)); Web_API::OnUpdateSystemGraphic(ToString(sys)); } void Game_Multiplayer::SePlayed(const lcf::rpg::Sound& sound) { if (!Main_Data::game_player->IsMenuCalling()) { - connection.SendPacketAsync(sound); + connection.SendPacketAsync(sound); } } @@ -652,7 +687,7 @@ bool Game_Multiplayer::IsPictureSynced(int pic_id, Game_Pictures::ShowParams& pa void Game_Multiplayer::PictureShown(int pic_id, Game_Pictures::ShowParams& params) { if (IsPictureSynced(pic_id, params)) { auto& p = Main_Data::game_player; - connection.SendPacketAsync(pic_id, params, + connection.SendPacketAsync(pic_id, params, Game_Map::GetPositionX(), Game_Map::GetPositionY(), p->GetPanX(), p->GetPanY()); } @@ -661,7 +696,7 @@ void Game_Multiplayer::PictureShown(int pic_id, Game_Pictures::ShowParams& param void Game_Multiplayer::PictureMoved(int pic_id, Game_Pictures::MoveParams& params) { if (sync_picture_cache.count(pic_id) && sync_picture_cache[pic_id]) { auto& p = Main_Data::game_player; - connection.SendPacketAsync(pic_id, params, + connection.SendPacketAsync(pic_id, params, Game_Map::GetPositionX(), Game_Map::GetPositionY(), p->GetPanX(), p->GetPanY()); } @@ -670,7 +705,7 @@ void Game_Multiplayer::PictureMoved(int pic_id, Game_Pictures::MoveParams& param void Game_Multiplayer::PictureErased(int pic_id) { if (sync_picture_cache.count(pic_id) && sync_picture_cache[pic_id]) { sync_picture_cache.erase(pic_id); - connection.SendPacketAsync(pic_id); + connection.SendPacketAsync(pic_id); } } @@ -689,7 +724,7 @@ bool Game_Multiplayer::IsBattleAnimSynced(int anim_id) { void Game_Multiplayer::PlayerBattleAnimShown(int anim_id) { if (IsBattleAnimSynced(anim_id)) { - connection.SendPacketAsync(anim_id); + connection.SendPacketAsync(anim_id); } } @@ -774,7 +809,7 @@ void Game_Multiplayer::UpdateGlobalVariables() { void Game_Multiplayer::Update() { if (session_active) { if (last_flash_frame_index > -1 && frame_index > last_flash_frame_index) { - connection.SendPacketAsync(); + connection.SendPacketAsync(); last_flash_frame_index = -1; last_frame_flash.reset(); } @@ -892,6 +927,10 @@ void Game_Multiplayer::Update() { DrawableMgr::SetLocalList(old_list); } - if (session_connected) + if (session_connected) { connection.FlushQueue(); +#ifndef EMSCRIPTEN + sessionConn.FlushQueue(); +#endif + } } diff --git a/src/multiplayer/game_multiplayer.h b/src/multiplayer/game_multiplayer.h index afd65b643..e6a8b35d3 100644 --- a/src/multiplayer/game_multiplayer.h +++ b/src/multiplayer/game_multiplayer.h @@ -20,6 +20,7 @@ class Game_Multiplayer { void Connect(int map_id, bool room_switch = false); void Initialize(); + void InitSession(); void Quit(); void Update(); void SendBasicData(); @@ -60,6 +61,9 @@ class Game_Multiplayer { } settings; YNOConnection connection; +#ifndef EMSCRIPTEN + YNOConnection sessionConn; +#endif bool session_active{ false }; // if true, it will automatically reconnect when disconnected bool session_connected{ false }; bool switching_room{ true }; // when client enters new room, but not synced to server diff --git a/src/multiplayer/messages.h b/src/multiplayer/messages.h index 6d08998b7..6df45211e 100644 --- a/src/multiplayer/messages.h +++ b/src/multiplayer/messages.h @@ -375,6 +375,27 @@ namespace S2C { int temperature; int precipitation; }; +#ifndef EMSCRIPTEN + class SessionGSay : public S2CPacket { + public: + SessionGSay(const PL& v) + : uuid(v.at(0)), + mapid(v.at(1)), + prevmapid(v.at(2)), + prevlocsstr(v.at(3)), + x(stoi((std::string)v.at(4))), + y(stoi((std::string)v.at(5))), + msg(v.at(6)), + msgid(v.at(7)) {} + int x, y; + std::string uuid; + std::string mapid; + std::string prevmapid; + std::string prevlocsstr; + std::string msg; + std::string msgid; + }; +#endif } namespace C2S { using C2SPacket = Multiplayer::C2SPacket; @@ -606,7 +627,16 @@ namespace C2S { int event_id; int action_bin; }; - +#ifndef EMSCRIPTEN + class SessionPlayerName : public C2SPacket { + public: + SessionPlayerName(std::string name_) : C2SPacket("name"), + name(name_) {} + std::string ToBytes() const override { return Build(name); } + protected: + std::string name; + }; +#endif } } diff --git a/src/multiplayer/yno_connection.cpp b/src/multiplayer/yno_connection.cpp index 5b97c2a50..7cbf76ea1 100644 --- a/src/multiplayer/yno_connection.cpp +++ b/src/multiplayer/yno_connection.cpp @@ -1,52 +1,62 @@ #include "yno_connection.h" #include "multiplayer/packet.h" #include "output.h" +#include +#include +#include +#include #ifdef __EMSCRIPTEN__ # include #else # include # include +# include +# include #endif #include "../external/TinySHA1.hpp" +namespace { + std::shared_ptr _sessionclient; + std::string session_token; +} + struct YNOConnection::IMPL { #ifdef __EMSCRIPTEN__ EMSCRIPTEN_WEBSOCKET_T socket; #else - lws_ss_handle* handle; - lws_context* cx; + std::shared_ptr _wsclient; + WebSocketClient* GetWsClient() { return _wsclient.get(); }; #endif - uint32_t msg_count; - bool closed; + uint32_t msg_count = 0; + bool closed = true; -#ifdef __EMSCRIPTEN__ - static bool onopen(int eventType, const EmscriptenWebSocketOpenEvent *event, void *userData) { + static bool onopen_common(void* userData) { auto _this = static_cast(userData); _this->SetConnected(true); _this->DispatchSystem(SystemMessage::OPEN); return true; } - static bool onclose(int eventType, const EmscriptenWebSocketCloseEvent *event, void *userData) { + static bool onclose_common(bool exit, void* userData) { auto _this = static_cast(userData); _this->SetConnected(false); _this->DispatchSystem( - event->code == 1028 ? + exit ? SystemMessage::EXIT : SystemMessage::CLOSE ); return true; } - static bool onmessage(int eventType, const EmscriptenWebSocketMessageEvent *event, void *userData) { + static bool onmessage_common(const std::string& cstr, void* userData, bool isText) { 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) { + if (isText) { return false; } - std::string_view cstr(reinterpret_cast(event->data), event->numBytes); + //std::string_view cstr(reinterpret_cast(data), dlen); std::vector mstrs = Split(cstr, Multiplayer::Packet::MSG_DELIM); for (auto& mstr : mstrs) { auto p = mstr.find(Multiplayer::Packet::PARAM_DELIM); @@ -58,7 +68,8 @@ struct YNOConnection::IMPL { duplicated code because the statement in else clause will handle it. */ _this->Dispatch(mstr); - } else { + } + else { auto namestr = mstr.substr(0, p); auto argstr = mstr.substr(p + Multiplayer::Packet::PARAM_DELIM.size()); _this->Dispatch(namestr, Split(argstr)); @@ -66,149 +77,204 @@ struct YNOConnection::IMPL { } return true; } -#else - struct yno_socket_t { - struct lws_ss_handle* ss; - void* opaque_data; - - // custom logic fields begin - YNOConnection* conn; - std::deque queue; - // custom logic fields end - - lws_sorted_usec_list_t sul; - int count; - bool due; - }; - 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; +#ifdef __EMSCRIPTEN__ + static bool onopen(int eventType, const EmscriptenWebSocketOpenEvent *event, void *userData) { + return onopen_common(userData); + } + static bool onclose(int eventType, const EmscriptenWebSocketCloseEvent *event, void *userData) { + return onclose_common(event->code == 1028, event->data, event->numBytes, userData); + } + static bool onmessage(int eventType, const EmscriptenWebSocketMessageEvent *event, void *userData) { + return onmessage_common(std::string_view(reinterpret_cast(event->data, event->numBytes)), userData, event->isText); + } + 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); + } +#else + void set_callbacks(void* userData) { + assert(_wsclient && "wsclient not initialized"); + _wsclient->SetUserData(userData); + _wsclient->RegisterOnConnect(onopen_common); + _wsclient->RegisterOnDisconnect(onclose_common); + _wsclient->RegisterOnMessage([](const std::string& str, void* userdata) { return onmessage_common(str, userdata, false); }); + } + void create_client(const std::string& url) { + //if (_wsclient) _wsclient->Stop(); + _wsclient.reset(new WebSocketClient(url)); + //_wsclient->Start(); + } + void start() { + _wsclient->Start(); } +#endif +}; - static constexpr uint RATE_US = 50000; - static constexpr size_t PKT_SIZE = 80; +#ifndef EMSCRIPTEN +WebSocketClient::WebSocketClient(const std::string& url) : url_(url), context_(nullptr), wsi_(nullptr), running_(false), userdata_(nullptr) { +} - static void yno_socket_txcb(struct lws_sorted_usec_list* sul) - { - auto* self = lws_container_of(sul, yno_socket_t, sul); +WebSocketClient::~WebSocketClient() { + Stop(); +} - self->due = true; - if (lws_ss_request_tx(self->ss) != LWSSSSRET_OK) { - // TODO: The fuck you expect me to do? - } +void WebSocketClient::Start() { + if (running_) return; + + struct lws_context_creation_info info = {}; + info.options = 0 + | LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT + | LWS_SERVER_OPTION_LIBUV + ; + uv_loop_t* loop = uv_default_loop(); + info.protocols = protocols_; + info.foreign_loops = (void **)&loop; + info.port = CONTEXT_PORT_NO_LISTEN; + info.fd_limit_per_thread = 1 + 1 + 1; + info.user = this; + + context_ = lws_create_context(&info); + if (!context_) { + Output::ErrorStr("Failed to create LWS context"); + } - lws_sul_schedule(lws_ss_get_context(self->ss), 0, &self->sul, yno_socket_txcb, RATE_US); + const char *prot, *addr, *path; + int port; + if (lws_parse_uri(url_.data(), &prot, &addr, &port, &path)) { + Output::Error("Invalid wsurl: {}", url_); } - 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; + lws_client_connect_info ccinfo{}; + ccinfo.context = context_; + ccinfo.address = addr; + ccinfo.port = port; + ccinfo.path = path; + ccinfo.host = ccinfo.address; + ccinfo.origin = lws_canonical_hostname(context_); + ccinfo.ssl_connection = LCCSCF_USE_SSL; + ccinfo.protocol = protocols_[0].name; + + wsi_ = lws_client_connect_via_info(&ccinfo); + if (!wsi_) { + lws_context_destroy(context_); + Output::ErrorStr("Failed to connect to the WebSocket server"); + } - self->due = false; + running_.store(true, std::memory_order_acquire); +} - auto& queue = self->queue; - if (queue.empty()) - return LWSSSSRET_TX_DONT_SEND; +void WebSocketClient::Stop() { + Output::Debug("Stopping {}", url_); + if (!running_) return; + running_.store(false, std::memory_order_release); + lws_set_timeout(wsi_, PENDING_TIMEOUT_AWAITING_PROXY_RESPONSE, LWS_TO_KILL_ASYNC); +} - size_t _len = *len = std::min(queue.size(), PKT_SIZE); - std::copy(queue.begin(), queue.begin() + _len, buf); - queue.erase(queue.begin(), queue.begin() + _len); +void WebSocketClient::Send(std::string_view data) { + std::lock_guard _guard(bmutex_); + buffer_.insert(buffer_.end(), data.begin(), data.end()); +} - *flags = LWSSS_FLAG_SOM | LWSSS_FLAG_EOM; +int WebSocketClient::CallbackFunction(struct lws* wsi, enum lws_callback_reasons reason, + void* user, void* in, size_t len) { - self->count++; + WebSocketClient* client = reinterpret_cast(lws_context_user(lws_get_context(wsi))); - lws_sul_schedule(lws_ss_get_context(self->ss), 0, &self->sul, yno_socket_txcb, RATE_US); + switch (reason) { + case LWS_CALLBACK_CLIENT_ESTABLISHED: + if (!client->running_ && client->ready_) return -1; + client->ready_ = true; + if (client->on_connect_) client->on_connect_(client->userdata_); + break; - return LWSSSSRET_OK; - } + case LWS_CALLBACK_CLIENT_WRITEABLE: { + if (!client->running_ && client->ready_) return -1; + if (!client->buffer_.empty()) { + size_t current_size = client->buffer_.size(); + std::lock_guard _guard(client->bmutex_); + std::vector buffer; + buffer.resize(LWS_PRE + current_size); + buffer.insert(buffer.begin() + LWS_PRE, client->buffer_.begin(), client->buffer_.end()); + client->buffer_.clear(); - 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; + if (lws_write(wsi, &buffer[LWS_PRE], current_size, LWS_WRITE_BINARY) < current_size) { + return -1; + } } - return LWSSSSRET_OK; - } + } break; + + case LWS_CALLBACK_CLIENT_RECEIVE: + if (!client->running_ && client->ready_) return -1; + if (client->on_message_) { + //Output::Debug("Receiving {} bytes from {}", len, client->url_); + std::string message(reinterpret_cast(in), len); + client->on_message_(message, client->userdata_); + } + else + Output::Warning("{}: no on_message defined", client->url_); + break; + + case LWS_CALLBACK_WS_PEER_INITIATED_CLOSE: + if (client->on_disconnect_) { + lcf::Span close_span( + lws_get_close_payload(wsi), + lws_get_close_length(wsi) + ); + bool exit = false; + if (close_span.size() >= 2) { + int16_t close_reason = static_cast((close_span[0] << 8) | close_span[1]); + exit = close_reason == 1028; + } + client->on_disconnect_(exit, client->userdata_); + } + client->running_.store(false, std::memory_order_release); + break; - 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 + case LWS_CALLBACK_CLIENT_CLOSED: + client->running_.store(false, std::memory_order_release); + if (client->on_disconnect_) { + client->on_disconnect_(false, client->userdata_); + } + break; -#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); - } -#else - void initWs(const lws_ss_policy* policy) { - 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); + case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: + Output::Warning("LWS error: {} ({})", std::string((const char*)in, len), client->url_); + client->running_.store(false, std::memory_order_release); + if (client->on_disconnect_) { + client->on_disconnect_(false, client->userdata_); + } + break; + + case LWS_CALLBACK_EVENT_WAIT_CANCELLED: + if (client->ready_) { + Output::Warning("{}: event wait cancelled", client->url_); + client->running_.store(false, std::memory_order_release); + } + break; + + //case LWS_CALLBACK_OPENSSL_PERFORM_SERVER_CERT_VERIFICATION: + // X509_STORE_CTX_set_error((X509_STORE_CTX*)user, X509_V_OK); + // break; + + //case LWS_CALLBACK_LOCK_POLL: + //case LWS_CALLBACK_UNLOCK_POLL: + //case LWS_CALLBACK_CHANGE_MODE_POLL_FD: + //case LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER: + //case LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP: + //case LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH: + // break; + + //default: + // Output::Debug("Unhandled {}", (int)reason); + // break; } + + return 0; +} #endif -}; const size_t YNOConnection::MAX_QUEUE_SIZE{ 4088 }; @@ -220,8 +286,10 @@ YNOConnection::YNOConnection() : impl(new IMPL) { YNOConnection::YNOConnection(YNOConnection&& o) : Connection(std::move(o)), impl(std::move(o.impl)) { -#ifdef __EMSCRIPTEN__ +#ifdef EMSCRIPTEN IMPL::set_callbacks(impl->socket, this); +#else + impl->set_callbacks(this); #endif } YNOConnection& YNOConnection::operator=(YNOConnection&& o) { @@ -229,8 +297,10 @@ YNOConnection& YNOConnection::operator=(YNOConnection&& o) { if (this != &o) { Close(); impl = std::move(o.impl); -#ifdef __EMSCRIPTEN__ +#ifdef EMSCRIPTEN IMPL::set_callbacks(impl->socket, this); +#else + impl->set_callbacks(this); #endif } return *this; @@ -246,8 +316,8 @@ void YNOConnection::Open(std::string_view uri) { Close(); } -#ifdef __EMSCRIPTEN__ std::string s {uri}; +#ifdef __EMSCRIPTEN__ EmscriptenWebSocketCreateAttributes ws_attrs = { s.data(), "binary", @@ -257,13 +327,10 @@ void YNOConnection::Open(std::string_view uri) { 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), - }; - impl->initWs(&yno_policy); + impl->create_client(s); + impl->closed = false; + impl->set_callbacks(this); + impl->start(); #endif } @@ -279,7 +346,7 @@ void YNOConnection::Close() { emscripten_websocket_close(impl->socket, 0, nullptr); emscripten_websocket_delete(impl->socket); #else - if (impl->handle) lws_ss_destroy(&impl->handle); + // handled by create_client #endif } @@ -292,17 +359,9 @@ std::string_view as_bytes(const T& v) { ); } -// poor method to test current endian -// need improvement -bool is_big_endian() { - union endian_tester { - uint16_t num; - char layout[2]; - }; - - endian_tester t; - t.num = 1; - return t.layout[0] == 0; +static bool is_big_endian() { + const uint16_t n = 0x1; + return reinterpret_cast(&n)[0] == 0x00; } std::string reverse_endian(std::string src) { @@ -331,6 +390,7 @@ std::string as_big_endian_bytes(T v) { const unsigned char psk[] = {}; std::string calculate_header(uint32_t key, uint32_t count, std::string_view msg) { + Output::Debug("header key={} count={}", key, count); std::string hashmsg{as_bytes(psk)}; hashmsg += as_big_endian_bytes(key); hashmsg += as_big_endian_bytes(count); @@ -353,21 +413,22 @@ void YNOConnection::Send(std::string_view data) { #ifdef __EMSCRIPTEN__ emscripten_websocket_get_ready_state(impl->socket, &ready); #else - ready = true; // TODO + auto wsclient = impl->GetWsClient(); + ready = wsclient->Ready(); #endif - if (ready == 1) { // OPEN + if (ready) { // OPEN ++impl->msg_count; auto sendmsg = calculate_header(GetKey(), impl->msg_count, data); sendmsg += data; #ifdef __EMSCRIPTEN__ emscripten_websocket_send_binary(impl->socket, sendmsg.data(), sendmsg.size()); #else - auto* socket = lws_container_of(impl->handle, IMPL::yno_socket_t, ss); - if (!socket->queue.empty()) { + if (!wsclient->Empty()) { auto delim = Multiplayer::Packet::MSG_DELIM; - socket->queue.insert(socket->queue.end(), delim.begin(), delim.end()); + wsclient->Send(delim); } - socket->queue.insert(socket->queue.end(), sendmsg.begin(), sendmsg.end()); + wsclient->Send(sendmsg); + wsclient->RequestFlush(); #endif } } diff --git a/src/multiplayer/yno_connection.h b/src/multiplayer/yno_connection.h index 7b8048e36..07a64fdce 100644 --- a/src/multiplayer/yno_connection.h +++ b/src/multiplayer/yno_connection.h @@ -1,6 +1,13 @@ #ifndef EP_YNO_CONNECTION_H #define EP_YNO_CONNECTION_H +#ifndef EMSCRIPTEN +# include +# include +# include +# include +#endif + #include "connection.h" class YNOConnection : public Multiplayer::Connection { @@ -21,4 +28,51 @@ class YNOConnection : public Multiplayer::Connection { std::unique_ptr impl; }; +#ifndef EMSCRIPTEN +class WebSocketClient : public std::enable_shared_from_this { +public: + using Callback = std::function; + using DisconnectCallback = std::function; + using MessageCallback = std::function; + + WebSocketClient(const std::string& url); + ~WebSocketClient(); + + inline void RegisterOnConnect(Callback callback) { on_connect_ = callback; } + inline void RegisterOnMessage(MessageCallback callback) { on_message_ = callback; } + inline void RegisterOnDisconnect(DisconnectCallback callback) { on_disconnect_ = callback; } + + void Start(); + void Stop(); + inline void SetUserData(void* userdata) noexcept { userdata_ = userdata; } + void Send(std::string_view data); + inline bool Empty() const noexcept { return buffer_.empty(); } + inline bool Ready() const noexcept { return running_ && ready_; } + inline void RequestFlush() { + lws_callback_on_writable(wsi_); + } +private: + static int CallbackFunction(struct lws* wsi, enum lws_callback_reasons reason, + void* user, void* in, size_t len); + + static inline struct lws_protocols protocols_[] = { + {"binary", CallbackFunction, 0, 65536, 0, nullptr, 0}, + {nullptr, nullptr} // terminator + }; + + std::string url_; + struct lws_context* context_; + void* userdata_; + struct lws* wsi_; + std::atomic running_; + std::atomic ready_; + std::mutex bmutex_; + std::vector buffer_{}; + + Callback on_connect_; + MessageCallback on_message_; + DisconnectCallback on_disconnect_; +}; +#endif + #endif diff --git a/src/player.cpp b/src/player.cpp index bacc047ac..986b5fc26 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -32,6 +32,10 @@ # include #endif +#ifndef EMSCRIPTEN +# include +#endif + #include "async_handler.h" #include "audio.h" #include "cache.h" @@ -203,6 +207,8 @@ void Player::Init(std::vector args) { void Player::Run() { Instrumentation::Init("EasyRPG-Player"); + GMI().InitSession(); + Scene::Push(std::make_shared()); Graphics::UpdateSceneCallback(); @@ -263,6 +269,8 @@ void Player::MainLoop() { Scene::old_instances.clear(); + uv_run(uv_default_loop(), UV_RUN_NOWAIT); + if (!Transition::instance().IsActive() && Scene::instance->type == Scene::Null) { Exit(); return; @@ -280,6 +288,7 @@ void Player::MainLoop() { iframe.End(); Game_Clock::SleepFor(next - now); } + } void Player::Pause() { diff --git a/src/web_api.cpp b/src/web_api.cpp index 7b0d38dab..5f0fa386d 100644 --- a/src/web_api.cpp +++ b/src/web_api.cpp @@ -9,6 +9,8 @@ using namespace Web_API; std::string Web_API::GetSocketURL() { + return "wss://connect.ynoproject.net/2kki/"; + //return "wss://localhost:8028/backend/2kki/"; return reinterpret_cast(EM_ASM_INT({ var ws = Module.wsUrl; var len = lengthBytesUTF8(ws)+1; @@ -103,6 +105,7 @@ void Web_API::ShowToastMessage(std::string_view msg, std::string_view icon) { } bool Web_API::ShouldConnectPlayer(std::string_view uuid) { + return true; int result = EM_ASM_INT({ return shouldConnectPlayer(UTF8ToString($0, $1)) ? 1 : 0; }, uuid.data(), uuid.size()); diff --git a/src/window_base.cpp b/src/window_base.cpp index 63c45bb98..66ad81d0a 100644 --- a/src/window_base.cpp +++ b/src/window_base.cpp @@ -124,7 +124,7 @@ void Window_Base::DrawActorTitle(const Game_Actor& actor, int cx, int cy) const } void Window_Base::DrawActorClass(const Game_Actor& actor, int cx, int cy) const { - contents->TextDraw(cx, cy, Font::ColorDefault, actor.GetClassName()); + contents->TextDraw(cx, cy, Font::ColorDefault, actor.ClassName()); } void Window_Base::DrawActorLevel(const Game_Actor& actor, int cx, int cy) const {