From e6000b7e3fc9d4f46370b9c79a3f593a1062dea6 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Sun, 8 Sep 2024 23:14:25 -0400 Subject: [PATCH 01/18] Add custom code for Collective Unconscious --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index b6f3443d0..ff178c65c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1273,7 +1273,9 @@ if(PLAYER_BUILD_EXECUTABLE AND ${PLAYER_TARGET_PLATFORM} MATCHES "^SDL.*$" AND N "-sALLOW_MEMORY_GROWTH -sMINIFY_HTML=0 -sMODULARIZE -sEXPORT_NAME=createEasyRpgPlayer \ -sEXIT_RUNTIME=0 --bind --pre-js ${PLAYER_JS_PREJS} --post-js ${PLAYER_JS_POSTJS} \ -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=['$autoResumeAudioContext','$dynCall'] \ + -sEXPORTED_FUNCTIONS=_main,_malloc,_free \ -sEXPORTED_RUNTIME_METHODS=['FS','HEAPU8'] \ + -sGL_ENABLE_GET_PROC_ADDRESS") -sEXPORTED_FUNCTIONS=_main,_malloc,_free") set_source_files_properties("src/platform/sdl/main.cpp" PROPERTIES OBJECT_DEPENDS "${PLAYER_JS_PREJS};${PLAYER_JS_POSTJS};${PLAYER_JS_SHELL}") From 352f71b23e8cddfb06707568b752030f03fb8e18 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Tue, 3 Dec 2024 18:51:15 -0500 Subject: [PATCH 02/18] stash: web audio api --- CMakeLists.txt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index ff178c65c..735eae18f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1805,3 +1805,19 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/fix_compile_commands.py WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) endif() + +if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + execute_process(COMMAND ${CMAKE_CXX_COMPILER} --cflags + OUTPUT_VARIABLE EM_CFLAGS + COMMAND_ERROR_IS_FATAL ANY + ) + string(STRIP "${EM_CFLAGS}" EM_CFLAGS) + find_package(Python3 REQUIRED) + message("Python: ${Python3_EXECUTABLE}") + set(ENV{EXTRA_FLAGS} "${EM_CFLAGS}") + execute_process( + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/fix_compile_commands.py + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) +endif() From df60583d00a34b778d58721ddf1da5ca8cf844ae Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Tue, 3 Dec 2024 18:53:43 -0500 Subject: [PATCH 03/18] stash: non-WASM build with LWS --- CMakeLists.txt | 31 ++++- builds/android/app/build.gradle | 3 +- src/game_map.cpp | 8 +- src/multiplayer/yno_connection.cpp | 197 +++++++++++++++++++++++++++-- src/web_api.cpp | 8 +- 5 files changed, 228 insertions(+), 19 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 735eae18f..c868dc50f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.18...3.31 FATAL_ERROR) project(EasyRPG_Player VERSION 0.8.1 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") @@ -12,6 +12,8 @@ include(PlayerConfigureWindows) include(PlayerFindPackage) include(PlayerBuildType) include(PlayerMisc) +include(GetGitRevisionDescription) +include(CheckCSourceCompiles) # Dependencies provided by CMake Presets option(PLAYER_FIND_ROOT_PATH_APPEND @@ -1008,7 +1010,32 @@ else() CONDITION PLAYER_WITH_NLOHMANN_JSON DEFINITION HAVE_NLOHMANN_JSON TARGET nlohmann_json::nlohmann_json - ONLY_CONFIG) + 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() # Configure Audio backends diff --git a/builds/android/app/build.gradle b/builds/android/app/build.gradle index 6ddfcd1b6..8bd845f56 100644 --- a/builds/android/app/build.gradle +++ b/builds/android/app/build.gradle @@ -55,7 +55,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") && project.toolchainDirs) { arguments.add('-DPLAYER_ANDROID_TOOLCHAIN_PATH=' + project.toolchainDirs) diff --git a/src/game_map.cpp b/src/game_map.cpp index dfd688b4e..16cef202e 100644 --- a/src/game_map.cpp +++ b/src/game_map.cpp @@ -57,7 +57,7 @@ #include #include "scene_gameover.h" #include "multiplayer/game_multiplayer.h" -#include +#include "web_api.h" #include "feature.h" namespace { @@ -374,11 +374,7 @@ std::unique_ptr Game_Map::LoadMapFile(int map_id, bool map_change Output::Debug("Loaded Map {}", map_name); - if (map_changed) { - EM_ASM({ - onLoadMap(UTF8ToString($0)); - }, map_name.c_str()); - } + Web_API::OnLoadMap(map_name); if (map.get() == NULL) { Output::ErrorStr(lcf::LcfReader::GetError()); diff --git a/src/multiplayer/yno_connection.cpp b/src/multiplayer/yno_connection.cpp index 17284da5a..f40cdc64a 100644 --- a/src/multiplayer/yno_connection.cpp +++ b/src/multiplayer/yno_connection.cpp @@ -1,5 +1,12 @@ #include "yno_connection.h" -#include +#include "multiplayer/packet.h" +#include "output.h" +#ifdef __EMSCRIPTEN__ +# include +#else +# include +# include +#endif #include "../external/TinySHA1.hpp" #ifndef PLAYER_PSK @@ -7,17 +14,24 @@ #endif 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( @@ -25,16 +39,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); @@ -54,14 +68,150 @@ struct YNOConnection::IMPL { _this->Dispatch(namestr, Split(argstr)); } } - return EM_TRUE; + 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; + } + + static constexpr uint RATE_US = 50000; + static constexpr size_t 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; + + auto& queue = self->queue; + if (queue.empty()) + return LWSSSSRET_TX_DONT_SEND; + + 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); + + *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); } +#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); + } +#endif }; const size_t YNOConnection::MAX_QUEUE_SIZE{ 4088 }; @@ -74,14 +224,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; } @@ -96,6 +250,7 @@ void YNOConnection::Open(std::string_view uri) { Close(); } +#ifdef __EMSCRIPTEN__ std::string s {uri}; EmscriptenWebSocketCreateAttributes ws_attrs = { s.data(), @@ -105,6 +260,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), + }; + impl->initWs(&yno_policy); +#endif } void YNOConnection::Close() { @@ -112,11 +276,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 @@ -186,12 +354,25 @@ 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 = true; // TODO +#endif if (ready == 1) { // 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()) { + auto delim = Multiplayer::Packet::MSG_DELIM; + socket->queue.insert(socket->queue.end(), delim.begin(), delim.end()); + } + socket->queue.insert(socket->queue.end(), sendmsg.begin(), sendmsg.end()); +#endif } } 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; From f148446bde5254822277e755f2dccb466fbcdd55 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Mon, 16 Dec 2024 01:03:44 -0500 Subject: [PATCH 04/18] Implement session websocket Most packets are now correctly sent, except for the case when changing rooms which leaves the user in singleplayer mode still. --- CMakeLists.txt | 17 +- src/game_actor.cpp | 2 +- src/game_actor.h | 2 +- src/multiplayer/game_multiplayer.cpp | 87 ++++-- src/multiplayer/game_multiplayer.h | 4 + src/multiplayer/messages.h | 32 ++- src/multiplayer/yno_connection.cpp | 383 ++++++++++++++++----------- src/multiplayer/yno_connection.h | 54 ++++ src/player.cpp | 9 + src/web_api.cpp | 3 + src/window_base.cpp | 2 +- 11 files changed, 402 insertions(+), 193 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c868dc50f..111f24a20 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1016,11 +1016,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) @@ -1036,6 +1036,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() # Configure Audio backends @@ -1265,7 +1272,8 @@ if(PLAYER_BUILD_EXECUTABLE AND ${PLAYER_TARGET_PLATFORM} MATCHES "^SDL.*$" AND N 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}) @@ -1280,6 +1288,7 @@ if(PLAYER_BUILD_EXECUTABLE AND ${PLAYER_TARGET_PLATFORM} MATCHES "^SDL.*$" AND N # 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 91d7668f6..ef2188003 100644 --- a/src/game_actor.cpp +++ b/src/game_actor.cpp @@ -1079,7 +1079,7 @@ void Game_Actor::ChangeClass(int new_class_id, } } -std::string_view 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 cf2631573..de4dee8ee 100644 --- a/src/game_actor.h +++ b/src/game_actor.h @@ -841,7 +841,7 @@ class Game_Actor final : public Game_Battler { * * @return Rpg2k3 hero class name */ - std::string_view GetClassName() const; + StringView ClassName() const; /** * Gets battle commands. diff --git a/src/multiplayer/game_multiplayer.cpp b/src/multiplayer/game_multiplayer.cpp index e5e1a6ac6..f969cef17 100644 --- a/src/multiplayer/game_multiplayer.cpp +++ b/src/multiplayer/game_multiplayer.cpp @@ -43,6 +43,10 @@ #include "yno_connection.h" #include "messages.h" +#ifndef EMSCRIPTEN +# include +#endif + static Game_Multiplayer _instance; Game_Multiplayer& Game_Multiplayer::Instance() { @@ -131,9 +135,6 @@ static std::string get_room_url(int room_id, std::string_view session_token) { } void Game_Multiplayer::InitConnection() { - /* - conn.RegisterSystemHandler(YNOConnection::SystemMessage::OPEN, [] (Multiplayer::Connection& c) { - });*/ using YSM = YNOConnection::SystemMessage; using MCo = Multiplayer::Connection; connection.RegisterSystemHandler(YSM::CLOSE, [this] (MCo& c) { @@ -155,6 +156,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(); //} @@ -464,9 +466,20 @@ void Game_Multiplayer::InitConnection() { Web_API::OnPlayerNameUpdated(p.name, p.id); }); +#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); @@ -525,6 +538,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(); @@ -548,25 +583,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) { @@ -576,26 +611,26 @@ void Game_Multiplayer::MainPlayerFlashed(int r, int g, int b, int p, int f) { && last_flash_frame_flash == flash_array) { if (!repeating_flash_active) { repeating_flash_active = true; - 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_flash_frame_index = frame_index; last_flash_frame_flash = flash_array; } 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); } @@ -620,13 +655,13 @@ void Game_Multiplayer::MainPlayerChangedAnimState(bool stopping) { } void Game_Multiplayer::SystemGraphicChanged(std::string_view 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); } } @@ -670,19 +705,19 @@ void Game_Multiplayer::PictureShown(int pic_id, Game_Pictures::ShowParams& param bool was_synced = sync_picture_cache.count(pic_id) && sync_picture_cache[pic_id]; if (IsPictureSynced(pic_id, params.name)) { 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()); } else if (was_synced) { sync_picture_cache.erase(pic_id); - connection.SendPacketAsync(pic_id); + connection.SendPacketAsync(pic_id); } } 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()); } @@ -691,7 +726,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); } } @@ -707,7 +742,7 @@ void Game_Multiplayer::SendShownPictures() { if (!IsPictureSynced(id, params.name)) continue; auto& p = Main_Data::game_player; - connection.SendPacketAsync(id, params, + connection.SendPacketAsync(id, params, Game_Map::GetPositionX(), Game_Map::GetPositionY(), p->GetPanX(), p->GetPanY()); } @@ -728,7 +763,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); } } @@ -782,7 +817,7 @@ void Game_Multiplayer::ApplyScreenTone() { void Game_Multiplayer::Update() { if (session_active) { if (repeating_flash_active && frame_index > last_flash_frame_index) { - connection.SendPacketAsync(); + connection.SendPacketAsync(); repeating_flash_active = false; last_flash_frame_index = -1; last_flash_frame_flash.fill(0); @@ -900,6 +935,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 597759fb7..e2c3b4a4e 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(); @@ -56,6 +57,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 040d68f65..8c88b86d1 100644 --- a/src/multiplayer/messages.h +++ b/src/multiplayer/messages.h @@ -380,6 +380,27 @@ namespace S2C { public: BadgeUpdatePacket(const PL& v) {} }; +#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; @@ -619,7 +640,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 f40cdc64a..8ef02e5f0 100644 --- a/src/multiplayer/yno_connection.cpp +++ b/src/multiplayer/yno_connection.cpp @@ -1,11 +1,17 @@ #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" @@ -13,44 +19,48 @@ #define PLAYER_PSK #endif +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); @@ -62,7 +72,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)); @@ -70,149 +81,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 }; @@ -224,8 +290,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) { @@ -233,8 +301,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; @@ -250,8 +320,8 @@ void YNOConnection::Open(std::string_view uri) { Close(); } -#ifdef __EMSCRIPTEN__ std::string s {uri}; +#ifdef __EMSCRIPTEN__ EmscriptenWebSocketCreateAttributes ws_attrs = { s.data(), "binary", @@ -261,13 +331,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 } @@ -283,7 +350,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 } @@ -296,17 +363,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) { @@ -335,6 +394,7 @@ std::string as_big_endian_bytes(T v) { const unsigned char psk[] = PLAYER_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); @@ -357,21 +417,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 6c63ec87d..d29daa255 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -31,6 +31,10 @@ # include #endif +#ifndef EMSCRIPTEN +# include +#endif + #include "async_handler.h" #include "audio.h" #include "cache.h" @@ -209,6 +213,8 @@ void Player::Init(std::vector args) { void Player::Run() { Instrumentation::Init("EasyRPG-Player"); + GMI().InitSession(); + Scene::Push(std::make_shared()); Graphics::UpdateSceneCallback(); @@ -269,6 +275,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; @@ -294,6 +302,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 5c0c43682..577a7c3d8 100644 --- a/src/window_base.cpp +++ b/src/window_base.cpp @@ -128,7 +128,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 { From 563f60283217ad0104caa6010c2b878523c9b43a Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Tue, 17 Dec 2024 00:51:58 -0500 Subject: [PATCH 05/18] Fix websockets issue with ynoserver When changing rooms, the SR command needs some time for changes to register otherwise the user will be denied access to the server. --- src/multiplayer/game_multiplayer.cpp | 15 +++++--- src/multiplayer/yno_connection.cpp | 56 ++++++++++++++-------------- src/multiplayer/yno_connection.h | 1 + src/platform/sdl/main.cpp | 1 + 4 files changed, 41 insertions(+), 32 deletions(-) diff --git a/src/multiplayer/game_multiplayer.cpp b/src/multiplayer/game_multiplayer.cpp index f969cef17..ba7b461ba 100644 --- a/src/multiplayer/game_multiplayer.cpp +++ b/src/multiplayer/game_multiplayer.cpp @@ -156,7 +156,6 @@ 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(); //} @@ -468,10 +467,13 @@ void Game_Multiplayer::InitConnection() { }); #ifndef EMSCRIPTEN sessionConn.RegisterSystemHandler(YSM::OPEN, [this](MCo&) { + if (connection.IsConnected()) + connection.Close(); session_active = true; + if (room_id != -1) + Connect(room_id); }); sessionConn.RegisterSystemHandler(YSM::CLOSE, [this](MCo&) { - Output::Error("[session] exited"); }); sessionConn.RegisterHandler("gsay", [](SessionGSay& p) { Output::Debug("{}: {}", p.uuid, p.msg); @@ -546,7 +548,7 @@ void Game_Multiplayer::InitSession() { header["Content-Type"] = "application/x-www-form-urlencoded"; auto resp = cpr::Post( - cpr::Url{ "https://connect.ynoproject.net/yume/api/login" }, + cpr::Url{ "https://connect.ynoproject.net/2kki/api/login" }, cpr::Body{ formdata }, header); if (resp.status_code >= 200 && resp.status_code < 300) { @@ -556,6 +558,7 @@ void Game_Multiplayer::InitSession() { else { Output::Debug("login: {} {}", resp.status_code, resp.text); } + sessionConn.need_header = false; sessionConn.Open(sessionEndpoint); #endif } @@ -794,7 +797,8 @@ void Game_Multiplayer::ApplyRepeatingFlashes() { void Game_Multiplayer::ApplyTone(Tone tone) { for (auto& p : players) { p.second.sprite->SetTone(tone); - p.second.chat_name->SetEffectsDirty(); + if (p.second.chat_name) + p.second.chat_name->SetEffectsDirty(); } } @@ -901,7 +905,8 @@ void Game_Multiplayer::Update() { } } } - p.second.chat_name->SetTransparent(overlap); + if (p.second.chat_name) + p.second.chat_name->SetTransparent(overlap); } } diff --git a/src/multiplayer/yno_connection.cpp b/src/multiplayer/yno_connection.cpp index 8ef02e5f0..420cc2e66 100644 --- a/src/multiplayer/yno_connection.cpp +++ b/src/multiplayer/yno_connection.cpp @@ -60,7 +60,6 @@ struct YNOConnection::IMPL { if (isText) { return false; } - //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); @@ -197,12 +196,13 @@ int WebSocketClient::CallbackFunction(struct lws* wsi, enum lws_callback_reasons 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(); + { + std::lock_guard _guard(client->bmutex_); + buffer.resize(LWS_PRE + current_size); + buffer.insert(buffer.begin() + LWS_PRE, client->buffer_.begin(), client->buffer_.end()); + client->buffer_.clear(); + } if (lws_write(wsi, &buffer[LWS_PRE], current_size, LWS_WRITE_BINARY) < current_size) { return -1; @@ -213,12 +213,9 @@ int WebSocketClient::CallbackFunction(struct lws* wsi, enum lws_callback_reasons 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: @@ -263,17 +260,17 @@ int WebSocketClient::CallbackFunction(struct lws* wsi, enum lws_callback_reasons // 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; + 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; + default: + Output::Debug("Unhandled LWS event {}", (int)reason); + break; } return 0; @@ -394,7 +391,6 @@ std::string as_big_endian_bytes(T v) { const unsigned char psk[] = PLAYER_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); @@ -422,15 +418,15 @@ void YNOConnection::Send(std::string_view data) { #endif if (ready) { // OPEN ++impl->msg_count; - auto sendmsg = calculate_header(GetKey(), impl->msg_count, data); - sendmsg += data; + std::string sendmsg; + if (need_header) { + sendmsg = calculate_header(GetKey(), impl->msg_count, data); + sendmsg += data; + } + else sendmsg = data; #ifdef __EMSCRIPTEN__ emscripten_websocket_send_binary(impl->socket, sendmsg.data(), sendmsg.size()); #else - if (!wsclient->Empty()) { - auto delim = Multiplayer::Packet::MSG_DELIM; - wsclient->Send(delim); - } wsclient->Send(sendmsg); wsclient->RequestFlush(); #endif @@ -460,8 +456,14 @@ void YNOConnection::FlushQueue() { bulk += data; m_queue.pop(); } - if (!bulk.empty()) + if (!bulk.empty()) { Send(bulk); +#ifndef EMSCRIPTEN + // yield early for this batch, since otherwise the switch room command + // has no time to register + return; +#endif + } include = !include; } } diff --git a/src/multiplayer/yno_connection.h b/src/multiplayer/yno_connection.h index 07a64fdce..af7664ba1 100644 --- a/src/multiplayer/yno_connection.h +++ b/src/multiplayer/yno_connection.h @@ -23,6 +23,7 @@ class YNOConnection : public Multiplayer::Connection { void Close() override; void Send(std::string_view data) override; void FlushQueue() override; + bool need_header = true; protected: struct IMPL; std::unique_ptr impl; diff --git a/src/platform/sdl/main.cpp b/src/platform/sdl/main.cpp index cb289d367..cf876f535 100644 --- a/src/platform/sdl/main.cpp +++ b/src/platform/sdl/main.cpp @@ -83,6 +83,7 @@ int main(int argc, char* argv[]) { EpAndroid::env = (JNIEnv*)SDL_GetAndroidJNIEnv(); #endif + //Output::SetLogLevel(LogLevel::Debug); Player::Init(std::move(args)); Player::Run(); From a3ed91ff5760ae1f93d18edd4b776354fb21e2bd Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Wed, 18 Dec 2024 03:22:51 -0500 Subject: [PATCH 06/18] In-game chat log proof of concept --- CMakeLists.txt | 29 +++- CMakePresets.json | 16 ++ src/game_config.h | 2 + src/graphics.cpp | 15 +- src/graphics.h | 5 + src/input_buttons.h | 8 +- src/input_buttons_desktop.cpp | 1 + src/multiplayer/chat_overlay.cpp | 159 +++++++++++++++++++ src/multiplayer/chat_overlay.h | 63 ++++++++ src/multiplayer/game_multiplayer.cpp | 25 ++- src/multiplayer/game_multiplayer.h | 3 + src/multiplayer/yno_connection.cpp | 2 +- src/platform/sdl/sdl2_ui.cpp | 27 ++-- src/platform/ynoshell/main.cpp | 218 +++++++++++++++++++++++++++ src/player.cpp | 14 +- src/player.h | 2 + src/scene_settings.cpp | 1 + src/window_settings.cpp | 7 +- src/window_settings.h | 1 + 19 files changed, 577 insertions(+), 21 deletions(-) create mode 100644 src/multiplayer/chat_overlay.cpp create mode 100644 src/multiplayer/chat_overlay.h create mode 100644 src/platform/ynoshell/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 111f24a20..22ac1e1ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -517,6 +517,13 @@ target_sources(${PROJECT_NAME} PRIVATE src/external/TinySHA1.hpp ) +if(NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + target_sources(${PROJECT_NAME} PRIVATE + src/multiplayer/chat_overlay.h + src/multiplayer/chat_overlay.cpp + ) +endif() + include(CMakeDependentOption) # Include directories @@ -582,6 +589,21 @@ endif() set(PLAYER_BUILD_EXECUTABLE ON) set(PLAYER_TEST_LIBRARIES ${PROJECT_NAME}) +set(PLAYER_SHELL "none" CACHE STRING "Optional UI shell, options: none yno") +set_property(CACHE PLAYER_SHELL PROPERTY STRINGS none yno) + +if(${PLAYER_SHELL} STREQUAL "yno") + set(PLAYER_YNO TRUE) + target_compile_definitions(${PROJECT_NAME} PUBLIC PLAYER_YNO) + + player_find_package(NAME wxWidgets REQUIRED) + target_link_libraries(${PROJECT_NAME} wx::core wx::base) +elseif(${PLAYER_SHELL} STREQUAL "none") + # do nothing +else() + message(FATAL_ERROR "Invalid shell ${PLAYER_SHELL}") +endif() + if(ANDROID AND PLAYER_GRADLE_BUILD) # Build invoked by Gradle # Ugly: Gradle has no way to branch based on the ABI @@ -626,7 +648,7 @@ elseif(PLAYER_TARGET_PLATFORM STREQUAL "SDL2") TARGET SDL2::SDL2 REQUIRED) - if(TARGET SDL2::SDL2main) + if(TARGET SDL2::SDL2main AND ${PLAYER_SHELL} STREQUAL "none") target_link_libraries(${PROJECT_NAME} SDL2::SDL2main) endif() @@ -1264,6 +1286,8 @@ if(PLAYER_BUILD_EXECUTABLE AND ${PLAYER_TARGET_PLATFORM} MATCHES "^SDL.*$" AND N set(EXE_NAME ${PROJECT_NAME}_exe) if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") add_executable(${EXE_NAME} "src/platform/emscripten/main.cpp") + elseif(PLAYER_YNO) + add_executable(${EXE_NAME} "src/platform/ynoshell/main.cpp") else() add_executable(${EXE_NAME} "src/platform/sdl/main.cpp") endif() @@ -1272,8 +1296,7 @@ if(PLAYER_BUILD_EXECUTABLE AND ${PLAYER_TARGET_PLATFORM} MATCHES "^SDL.*$" AND N if(WIN32) # Open console for Debug builds - #set_target_properties(${EXE_NAME} PROPERTIES WIN32_EXECUTABLE $<$>:TRUE>) - set_target_properties(${EXE_NAME} PROPERTIES WIN32_EXECUTABLE FALSE) + set_target_properties(${EXE_NAME} PROPERTIES WIN32_EXECUTABLE $<$>:TRUE>) # Add resources string(REPLACE "." "," RC_VERSION ${PROJECT_VERSION}) diff --git a/CMakePresets.json b/CMakePresets.json index e49252ce0..178682412 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -637,6 +637,22 @@ "type-release" ] }, + { + "name": "windows-x64-vs2022-ynoshell-release", + "displayName": "Windows (x64) using Visual Studio 2022 (ynoshell) (Release)", + "inherits": "windows-x64-vs2022-release", + "cacheVariables": { + "PLAYER_SHELL": "yno" + } + }, + { + "name": "windows-x64-vs2022-ynoshell-relwithdebinfo", + "displayName": "Windows (x64) using Visual Studio 2022 (ynoshell) (RelWithDebInfo)", + "inherits": "windows-x64-vs2022-relwithdebinfo", + "cacheVariables": { + "PLAYER_SHELL": "yno" + } + }, { "name": "macos-parent", "cacheVariables": { diff --git a/src/game_config.h b/src/game_config.h index b64dd805f..0adc656f6 100644 --- a/src/game_config.h +++ b/src/game_config.h @@ -143,6 +143,8 @@ struct Game_ConfigVideo { ConfigParam window_width{ "", "", "Video", "WindowWidth", -1 }; ConfigParam window_height{ "", "", "Video", "WindowHeight", -1 }; + void* foreign_window_handle = nullptr; + void Hide(); }; diff --git a/src/graphics.cpp b/src/graphics.cpp index f19c4b031..ba0bb16c5 100644 --- a/src/graphics.cpp +++ b/src/graphics.cpp @@ -30,6 +30,9 @@ #include "drawable_mgr.h" #include "baseui.h" #include "game_clock.h" +#ifdef PLAYER_YNO +# include "multiplayer/chat_overlay.h" +#endif using namespace std::chrono_literals; @@ -40,6 +43,9 @@ namespace Graphics { std::unique_ptr message_overlay; std::unique_ptr fps_overlay; +#ifdef PLAYER_YNO + std::unique_ptr chat_overlay; +#endif std::string window_title_key; } @@ -50,11 +56,13 @@ void Graphics::Init() { message_overlay = std::make_unique(); fps_overlay = std::make_unique(); + chat_overlay = std::make_unique(); } void Graphics::Quit() { fps_overlay.reset(); message_overlay.reset(); + chat_overlay.reset(); Cache::ClearAll(); @@ -108,7 +116,7 @@ void Graphics::Draw(Bitmap& dst) { auto& transition = Transition::instance(); auto min_z = std::numeric_limits::min(); - auto max_z = std::numeric_limits::max(); + auto constexpr max_z = std::numeric_limits::max(); if (transition.IsActive()) { min_z = transition.GetZ(); } else if (transition.IsErasedNotActive()) { @@ -149,3 +157,8 @@ MessageOverlay& Graphics::GetMessageOverlay() { return *message_overlay; } +#ifdef PLAYER_YNO +ChatOverlay& Graphics::GetChatOverlay() { + return *chat_overlay; +} +#endif diff --git a/src/graphics.h b/src/graphics.h index 6bf5ec988..2e74577fb 100644 --- a/src/graphics.h +++ b/src/graphics.h @@ -27,6 +27,9 @@ class MessageOverlay; class Scene; +#ifdef PLAYER_YNO +class ChatOverlay; +#endif /** * Graphics namespace. @@ -61,6 +64,8 @@ namespace Graphics { * @return message overlay */ MessageOverlay& GetMessageOverlay(); + + ChatOverlay& GetChatOverlay(); } #endif diff --git a/src/input_buttons.h b/src/input_buttons.h index 652406292..a7290d7b3 100644 --- a/src/input_buttons.h +++ b/src/input_buttons.h @@ -85,6 +85,7 @@ namespace Input { FAST_FORWARD_B, TOGGLE_FULLSCREEN, TOGGLE_ZOOM, + SHOW_CHAT, BUTTON_COUNT }; @@ -130,7 +131,8 @@ namespace Input { "FAST_FORWARD_A", "FAST_FORWARD_B", "TOGGLE_FULLSCREEN", - "TOGGLE_ZOOM"); + "TOGGLE_ZOOM", + "SHOW_CHAT"); static_assert(kInputButtonNames.size() == static_cast(BUTTON_COUNT)); constexpr auto kInputButtonHelp = lcf::makeEnumTags( @@ -175,7 +177,8 @@ namespace Input { "Run the game at x{} speed", "Run the game at x{} speed", "Toggle Fullscreen mode", - "Toggle Window Zoom level"); + "Toggle Window Zoom level", + "Toggle chat display"); static_assert(kInputButtonHelp.size() == static_cast(BUTTON_COUNT)); /** @@ -191,6 +194,7 @@ namespace Input { case TOGGLE_ZOOM: case FAST_FORWARD_A: case FAST_FORWARD_B: + case SHOW_CHAT: return true; default: return false; diff --git a/src/input_buttons_desktop.cpp b/src/input_buttons_desktop.cpp index 9fb6bc772..7a61f07eb 100644 --- a/src/input_buttons_desktop.cpp +++ b/src/input_buttons_desktop.cpp @@ -101,6 +101,7 @@ Input::ButtonMappingArray Input::GetDefaultButtonMappings() { {RESET, Keys::F12}, {FAST_FORWARD_A, Keys::F}, {FAST_FORWARD_B, Keys::G}, + {SHOW_CHAT, Keys::T}, #if defined(USE_MOUSE) && defined(SUPPORT_MOUSE) {MOUSE_LEFT, Keys::MOUSE_LEFT}, diff --git a/src/multiplayer/chat_overlay.cpp b/src/multiplayer/chat_overlay.cpp new file mode 100644 index 000000000..b6eff3cb1 --- /dev/null +++ b/src/multiplayer/chat_overlay.cpp @@ -0,0 +1,159 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ +#include + +#include "chat_overlay.h" +#include "drawable_mgr.h" +#include "player.h" +#include "baseui.h" +#include "game_message.h" +#include "output.h" +#include "input_buttons.h" + +ChatOverlay::ChatOverlay() : Drawable(Priority_Overlay, Drawable::Flags::Global) +{ + +} + +void ChatOverlay::Draw(Bitmap& dst) { + dst.Blit(ox, oy, *bitmap, bitmap->GetRect(), 255); + if (!dirty) return; + + bitmap->Clear(); + + constexpr int y_end = 240; + + + int i = 0; + // we're doing the inverse of MessageOverlay: draw from the bottom up + int text_height = 12; // Font::NameText()->vGetSize('T').height; + int half_screen = y_end / 2 / text_height; + + bool unlocked_start = scroll < half_screen; + bool unlocked_end = scroll > messages.size() - half_screen; + int last_sticky = (int)messages.size() - half_screen * 2; + // we want half a screen before first sticky and half a screen after the last sticky. + int viewport = std::clamp(scroll - half_screen, 0, std::min(scroll + half_screen, last_sticky)); + + for (auto message = messages.rbegin() + viewport; message != messages.rend(); ++message) { + if (!message->hidden || show_all) { + auto& bg = !show_all ? black : + unlocked_start ? (i == scroll ? grey : black) : + unlocked_end ? (last_sticky + i == scroll ? grey : black) : + i == half_screen ? grey : black; + + bitmap->Blit(0, y_end - (i + 1) * text_height, *bg, bg->GetRect(), 128); + constexpr Color white = Color(255, 255, 255, 255); + Text::Draw(*bitmap, 2, y_end - (i + 1) * text_height, *Font::NameText(), white, message->text); + ++i; + } + if (!show_all && i > message_max_minimized) break; + if (i * text_height > y_end) break; + } + + dirty = false; +} + +void ChatOverlay::AddMessage(const std::string& message) { + if (message.empty()) { + return; + } + + Game_Message::WordWrap( + message, + Player::screen_width, + [&](StringView line) { + messages.emplace_back(std::string(line)); + }, *Font::NameText() + ); + + while (messages.size() > message_max) { + messages.pop_front(); + } + + dirty = true; +} + +void ChatOverlay::Update() { + if (!DisplayUi) { + return; + } + + if (!bitmap) { + OnResolutionChange(); + } + + + if (show_all) { + if (Input::IsTriggered(Input::InputButton::SCROLL_DOWN) || + Input::IsTriggered(Input::InputButton::DOWN) || + Input::IsRepeated(Input::InputButton::DOWN)) { + DoScroll(-1); + } + if (Input::IsTriggered(Input::InputButton::SCROLL_UP) || + Input::IsTriggered(Input::InputButton::UP) || + Input::IsRepeated(Input::InputButton::UP)) { + DoScroll(1); + } + if (Input::IsTriggered(Input::InputButton::PAGE_DOWN)) { + scroll = 0; + dirty = true; + } + if (Input::IsTriggered(Input::InputButton::PAGE_UP)) { + scroll = messages.size() - 1; + dirty = true; + } + } + + + if (++counter > 150) { + counter = 0; + for (auto& message : messages) { + if (!message.hidden) { + message.hidden = true; + break; + } + } + dirty = true; + } +} + +void ChatOverlay::SetShowAll(bool show_all) { + Output::Warning("SetShowAll"); + this->show_all = show_all; + dirty = true; +} + +void ChatOverlay::DoScroll(int increase) { + scroll += increase; + scroll = (scroll + messages.size()) % messages.size(); + dirty = true; +} + +void ChatOverlay::OnResolutionChange() { + if (!bitmap) { + DrawableMgr::Register(this); + } + + int text_height = 12; // Font::NameText()->vGetSize('T').height; + black = Bitmap::Create(Player::screen_width, text_height, Color(0, 0, 0, 255)); + grey = Bitmap::Create(Player::screen_width, text_height, Color(100, 100, 100, 255)); + bitmap = Bitmap::Create(Player::screen_width, text_height * message_max, true); +} + +ChatOverlayMessage::ChatOverlayMessage(std::string text) : + text(std::move(text)) {} diff --git a/src/multiplayer/chat_overlay.h b/src/multiplayer/chat_overlay.h new file mode 100644 index 000000000..4282eb325 --- /dev/null +++ b/src/multiplayer/chat_overlay.h @@ -0,0 +1,63 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef EP_MP_CHAT_OVERLAY_H +#define EP_MP_CHAT_OVERLAY_H + +#include +#include + +#include "drawable.h" +#include "bitmap.h" +#include "memory_management.h" + +//using ChatOverlayMessage = std::string; +class ChatOverlayMessage { +public: + ChatOverlayMessage(std::string text); + std::string text; + bool hidden = false; +}; + +class ChatOverlay : public Drawable { +public: + ChatOverlay(); + void Draw(Bitmap& dst) override; + void Update(); + void AddMessage(const std::string& msg); + void OnResolutionChange(); + void SetShowAll(bool show_all); + inline void SetShowAll() { SetShowAll(!show_all); } + inline bool ShowingAll() const noexcept { return show_all; } + void DoScroll(int increase); +private: + bool dirty = false; + bool show_all = false; + int ox = 0; + int oy = 0; + + int message_max = 100; + int message_max_minimized = 4; + int counter = 0; + int scroll = 0; + + BitmapRef bitmap, black, grey; + + std::deque messages; +}; + +#endif diff --git a/src/multiplayer/game_multiplayer.cpp b/src/multiplayer/game_multiplayer.cpp index ba7b461ba..4617d7f4e 100644 --- a/src/multiplayer/game_multiplayer.cpp +++ b/src/multiplayer/game_multiplayer.cpp @@ -45,6 +45,8 @@ #ifndef EMSCRIPTEN # include +# include +using json = nlohmann::json; #endif static Game_Multiplayer _instance; @@ -472,11 +474,27 @@ void Game_Multiplayer::InitConnection() { session_active = true; if (room_id != -1) Connect(room_id); + + // sync chat messages + if (on_chat_msg) { + std::string endpoint = fmt::format("https://connect.ynoproject.net/2kki/api/chathistory?globalMsgLimit={}&partyMsgLimit={}&lastMessageId={}", 100, 100, lastmsgid); + cpr::Response resp = cpr::Get(cpr::Url{endpoint}); + if (resp.status_code >= 200 && resp.status_code < 300) { + json chatmsgs = json::parse(resp.text); + const auto& messages = chatmsgs["messages"]; + for (auto it = messages.begin(); it != messages.end(); ++it) { + on_chat_msg((std::string)(*it)["contents"], it != std::prev(messages.end())); + } + } + } }); sessionConn.RegisterSystemHandler(YSM::CLOSE, [this](MCo&) { }); - sessionConn.RegisterHandler("gsay", [](SessionGSay& p) { + sessionConn.RegisterHandler("gsay", [this](SessionGSay& p) { Output::Debug("{}: {}", p.uuid, p.msg); + if (on_chat_msg) { + on_chat_msg(p.msg, false); + } }); #endif } @@ -660,6 +678,11 @@ void Game_Multiplayer::MainPlayerChangedAnimState(bool stopping) { void Game_Multiplayer::SystemGraphicChanged(std::string_view sys) { connection.SendPacketAsync(ToString(sys)); Web_API::OnUpdateSystemGraphic(ToString(sys)); +#ifndef EMSCRIPTEN + if (on_system_graphic_change) { + on_system_graphic_change(ToString(sys)); + } +#endif } void Game_Multiplayer::SePlayed(const lcf::rpg::Sound& sound) { diff --git a/src/multiplayer/game_multiplayer.h b/src/multiplayer/game_multiplayer.h index e2c3b4a4e..fd3bf401d 100644 --- a/src/multiplayer/game_multiplayer.h +++ b/src/multiplayer/game_multiplayer.h @@ -59,6 +59,9 @@ class Game_Multiplayer { YNOConnection connection; #ifndef EMSCRIPTEN YNOConnection sessionConn; + std::function on_chat_msg; + std::function on_system_graphic_change; + std::string lastmsgid; #endif bool session_active{ false }; // if true, it will automatically reconnect when disconnected bool session_connected{ false }; diff --git a/src/multiplayer/yno_connection.cpp b/src/multiplayer/yno_connection.cpp index 420cc2e66..8c4407035 100644 --- a/src/multiplayer/yno_connection.cpp +++ b/src/multiplayer/yno_connection.cpp @@ -219,6 +219,7 @@ int WebSocketClient::CallbackFunction(struct lws* wsi, enum lws_callback_reasons break; case LWS_CALLBACK_WS_PEER_INITIATED_CLOSE: + client->running_.store(false, std::memory_order_release); if (client->on_disconnect_) { lcf::Span close_span( lws_get_close_payload(wsi), @@ -231,7 +232,6 @@ int WebSocketClient::CallbackFunction(struct lws* wsi, enum lws_callback_reasons } client->on_disconnect_(exit, client->userdata_); } - client->running_.store(false, std::memory_order_release); break; case LWS_CALLBACK_CLIENT_CLOSED: diff --git a/src/platform/sdl/sdl2_ui.cpp b/src/platform/sdl/sdl2_ui.cpp index db9efbdc6..a0a60f1e4 100644 --- a/src/platform/sdl/sdl2_ui.cpp +++ b/src/platform/sdl/sdl2_ui.cpp @@ -356,6 +356,9 @@ bool Sdl2Ui::RefreshDisplayMode() { #endif // Create our window + if (vcfg.foreign_window_handle) { + sdl_window = SDL_CreateWindowFrom(vcfg.foreign_window_handle); + } else if (vcfg.window_x.Get() < 0 || vcfg.window_y.Get() < 0 || vcfg.window_height.Get() <= 0 || vcfg.window_width.Get() <= 0) { sdl_window = SDL_CreateWindow(GAME_TITLE, SDL_WINDOWPOS_CENTERED, @@ -449,18 +452,20 @@ bool Sdl2Ui::RefreshDisplayMode() { window_sg.Dismiss(); } else { // Browser handles fast resizing for emscripten, TODO: use fullscreen API -#if !defined(__EMSCRIPTEN__) && !defined(__PS4__) - bool is_fullscreen = (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN_DESKTOP; - if (is_fullscreen) { - SDL_SetWindowFullscreen(sdl_window, SDL_WINDOW_FULLSCREEN_DESKTOP); - } else { - SDL_SetWindowFullscreen(sdl_window, 0); - if ((last_display_mode.flags & SDL_WINDOW_FULLSCREEN_DESKTOP) - == SDL_WINDOW_FULLSCREEN_DESKTOP) { - // Restore to pre-fullscreen size - SDL_SetWindowSize(sdl_window, 0, 0); +#ifndef EMSCRIPTEN + if (!vcfg.foreign_window_handle) { + bool is_fullscreen = (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN_DESKTOP; + if (is_fullscreen) { + SDL_SetWindowFullscreen(sdl_window, SDL_WINDOW_FULLSCREEN_DESKTOP); } else { - SDL_SetWindowSize(sdl_window, display_width_zoomed, display_height_zoomed); + SDL_SetWindowFullscreen(sdl_window, 0); + if ((last_display_mode.flags & SDL_WINDOW_FULLSCREEN_DESKTOP) + == SDL_WINDOW_FULLSCREEN_DESKTOP) { + // Restore to pre-fullscreen size + SDL_SetWindowSize(sdl_window, 0, 0); + } else { + SDL_SetWindowSize(sdl_window, display_width_zoomed, display_height_zoomed); + } } } #endif diff --git a/src/platform/ynoshell/main.cpp b/src/platform/ynoshell/main.cpp new file mode 100644 index 000000000..0d999b283 --- /dev/null +++ b/src/platform/ynoshell/main.cpp @@ -0,0 +1,218 @@ +// Start of wxWidgets "Hello World" Program +#include +#include +#include + +#include "output.h" +#include "player.h" +#include "cache.h" +#include "bitmap.h" +#include "graphics.h" +#include "multiplayer/chat_overlay.h" +#include "multiplayer/game_multiplayer.h" + +class YnoApp : public wxApp +{ +public: + bool OnInit() override; +}; + +wxIMPLEMENT_APP(YnoApp); + +//class YnoChatMsg : public wxPanel +//{ +//public: +// wxSizer* parentSizer; +// wxSizerItem* selfItem; +// std::string msg_; +// wxString msgref; +// wxStaticText* text; +// YnoChatMsg(wxBoxSizer* parentSizer, wxWindow* parent, std::string msg) +// : wxPanel(parent, wxID_ANY), +// msg_(std::move(msg)), +// msgref(wxString(msg_)), +// parentSizer(parentSizer), +// selfItem(parentSizer->Add(this, 0, wxALL, Zoom(6))) +// { +// auto sizer = new wxBoxSizer(wxVERTICAL); +// text = new wxStaticText(this, wxID_ANY, msgref, wxDefaultPosition); +// text->SetFont(GetFont().Scaled(zoom)); +// text->Wrap(Zoom(parentSizer->GetSize().GetWidth() - 24)); +// sizer->Add(text); +// SetSizer(sizer); +// } +// +// bool Destroy() override { +// parentSizer->Remove((wxSizer*)selfItem); +// return wxPanel::Destroy(); +// } +// +// void onPaint(wxPaintEvent&) { +// +// } +//}; + +//class YnoGameContainer : public wxCustomBackgroundWindow +//{ +//public: +// YnoGameContainer(wxWindow* parent) { +// Create(parent, wxID_ANY); +// }; +//}; +using YnoGameContainer = wxWindow; + +class YnoFrame : public wxFrame +{ +public: + YnoFrame(); + + YnoGameContainer* gameWindow; + //wxScrolledWindow* chatLog; + //wxBoxSizer* chatboxSizer; + //wxBoxSizer* chatLogSizer; + //wxTextCtrl* chatInput; + //bool shouldScrollChat = true; + //std::vector chatmsgs; + +private: + //void DoZoom(bool magnify); + void OnKeyDownFrame(wxKeyEvent& event); + //void OnKeyDownChatInput(wxKeyEvent& event); + //void OnScrollChatLog(wxScrollWinEvent& event); + //void OverlayPaint(wxPaintEvent&); +}; + + +bool YnoApp::OnInit() +{ + YnoFrame* frame = new YnoFrame(); + frame->Show(true); + + wxWindow* child = frame->gameWindow; + if (!child) { + Output::Error("no child frame"); + return false; + } + + Player::did_parse_config = [handle=child->GetHandle()](Game_Config& cfg) { + cfg.video.foreign_window_handle = (void*)handle; + }; + + auto& app = wxGetApp(); + std::vector args{ (char**)app.argv, (char**)app.argv + app.argc }; + Player::Init(std::move(args)); + Player::Run(); + + return true; +} + +YnoFrame::YnoFrame() + : wxFrame(nullptr, wxID_ANY, "YNOproject") +{ + //SetFont({ 16, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL }); + wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL); + + //childPanel = new wxWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE); + + gameWindow = new YnoGameContainer(this, wxID_ANY); + gameWindow->Create(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE); + gameWindow->Bind(wxEVT_KEY_DOWN, &YnoFrame::OnKeyDownFrame, this); + sizer->Add((wxWindow*)gameWindow, 1, wxEXPAND); + + GMI().on_chat_msg = [](std::string_view msg, bool) { + Graphics::GetChatOverlay().AddMessage(msg.data()); + }; + + + //childPanel->SetBackgroundColour(*wxBLACK); + //childPanel->Bind(wxEVT_KEY_DOWN, &YnoFrame::OnKeyDownFrame, this); + //sizer->Add(childPanel, 1, wxEXPAND); + + + + //chatboxSizer = new wxBoxSizer(wxVERTICAL); + + //auto chatwindow = new wxRpgWindow(this); + //auto chatwindowSizer = new wxBoxSizer(wxHORIZONTAL); + //chatwindowSizer->Add(chatboxSizer, 1, wxEXPAND); + //chatwindow->SetSizer(chatwindowSizer); + + //sizer->Add(chatwindowSizer, 0, wxEXPAND | wxDOWN); + + //chatLog = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(Zoom(350), -1), wxVSCROLL); + //chatLog->SetBackgroundColour(wxTRANSPARENT); + //chatLog->SetScrollRate(0, Zoom(25)); + //chatLogSizer = new wxBoxSizer(wxVERTICAL); + //chatLog->SetSizer(chatLogSizer); + //chatLog->Bind(wxEVT_SCROLLWIN_LINEUP, &YnoFrame::OnScrollChatLog, this); + //chatLog->Bind(wxEVT_SCROLLWIN_LINEDOWN, &YnoFrame::OnScrollChatLog, this); + //chatboxSizer->Add(chatLog, 1, wxEXPAND); + + //GMI().on_chat_msg = [this](std::string_view msg, bool syncing) { + // if (chatmsgs.size() > 100) { + // chatmsgs[0]->Destroy(); + // chatmsgs.erase(chatmsgs.begin()); + // } + // chatmsgs.emplace_back(new YnoChatMsg(chatLogSizer, chatLog, std::string(msg))); + // if (syncing) return; + // chatboxSizer->Layout(); + // Refresh(); + // if (shouldScrollChat) + // chatLog->Scroll(wxPoint(-1, chatLog->GetScrollRange(wxVSCROLL))); + //}; + + //chatInput = new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxSize(-1, Zoom(50))); + //chatInput->Bind(wxEVT_KEY_DOWN, &YnoFrame::OnKeyDownChatInput, this); + //chatInput->SetFont(GetFont().Scaled(zoom)); + + //auto chatInputSizer = new wxBoxSizer(wxHORIZONTAL); + //chatInputSizer->Add(chatInput, 1, wxEXPAND | wxALL); + //chatboxSizer->Add(chatInputSizer); + + SetSizer(sizer); +}; + +void YnoFrame::OnKeyDownFrame(wxKeyEvent& event) { + switch (event.GetKeyCode()) { + case WXK_RETURN: + if (event.AltDown()) + ShowFullScreen(!IsFullScreen(), wxFULLSCREEN_ALL); + return; + } + event.Skip(); +} +// +//void YnoFrame::OnKeyDownChatInput(wxKeyEvent& event) { +// switch (event.GetKeyCode()) { +// case WXK_TAB: +// childPanel->SetFocus(); +// break; +// case WXK_RETURN: +// chatInput->Clear(); +// break; +// default: +// event.Skip(); +// } +//} +// +//void YnoFrame::OnScrollChatLog(wxScrollWinEvent& event) { +// int diff = chatLog->GetScrollRange(wxVERTICAL) - chatLog->GetScrollPos(wxVERTICAL); +// shouldScrollChat = diff < Zoom(55); +// event.Skip(); +//} +// +//void YnoFrame::DoZoom(bool magnify) { +// float step = zoom <= 1 ? 0.1 : 0.25; +// float wanted = magnify ? zoom + step : zoom - step; +// zoom = std::clamp(wanted, 0.8f, 2.f); +// +// int oldPosition = chatLog->GetScrollPos(wxVERTICAL); +// chatboxSizer->SetMinSize(wxSize(Zoom(350), -1)); +// chatInput->SetMinSize(wxSize(-1, Zoom(50))); +// for (auto msg : chatmsgs) { +// msg->text->SetFont(msg->GetFont().Scaled(zoom)); +// } +// Layout(); +// Refresh(); +// chatLog->Scroll(wxPoint(-1, Zoom(oldPosition))); +//} diff --git a/src/player.cpp b/src/player.cpp index d29daa255..0ea797391 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -31,8 +31,9 @@ # include #endif -#ifndef EMSCRIPTEN +#ifdef PLAYER_YNO # include +# include "multiplayer/chat_overlay.h" #endif #include "async_handler.h" @@ -139,6 +140,7 @@ namespace Player { int rng_seed = -1; Game_ConfigPlayer player_config; Game_ConfigGame game_config; + std::function did_parse_config; #ifdef __EMSCRIPTEN__ std::string emscripten_game_name; #endif @@ -263,6 +265,9 @@ void Player::MainLoop() { Scene::instance->MainFunction(); Graphics::GetMessageOverlay().Update(); +#ifdef PLAYER_YNO + Graphics::GetChatOverlay().Update(); +#endif ++num_updates; } @@ -326,6 +331,11 @@ void Player::UpdateInput() { if (Input::IsSystemTriggered(Input::SHOW_LOG)) { Output::ToggleLog(); } +#ifdef PLAYER_YNO + if (Input::IsSystemTriggered(Input::SHOW_CHAT)) { + Graphics::GetChatOverlay().SetShowAll(); + } +#endif if (Input::IsSystemTriggered(Input::TOGGLE_ZOOM)) { DisplayUi->ToggleZoom(); } @@ -703,6 +713,8 @@ Game_Config Player::ParseCommandLine() { cp.SkipNext(); } + if (Player::did_parse_config) + Player::did_parse_config(cfg); return cfg; } diff --git a/src/player.h b/src/player.h index 0e0680167..007013578 100644 --- a/src/player.h +++ b/src/player.h @@ -431,6 +431,8 @@ namespace Player { /** game specific configuration */ extern Game_ConfigGame game_config; + extern std::function did_parse_config; + #ifdef __EMSCRIPTEN__ /** Name of game emscripten uses */ extern std::string emscripten_game_name; diff --git a/src/scene_settings.cpp b/src/scene_settings.cpp index 2597aef33..3590b4cb7 100644 --- a/src/scene_settings.cpp +++ b/src/scene_settings.cpp @@ -276,6 +276,7 @@ void Scene_Settings::vUpdate() { case Window_Settings::eInputListButtonsGame: case Window_Settings::eInputListButtonsEngine: case Window_Settings::eInputListButtonsDeveloper: + case Window_Settings::eInputListButtonsOnline: UpdateOptions(); break; case Window_Settings::eEngineFont1: diff --git a/src/window_settings.cpp b/src/window_settings.cpp index 8603b1d69..a888ce4cd 100644 --- a/src/window_settings.cpp +++ b/src/window_settings.cpp @@ -158,6 +158,7 @@ void Window_Settings::Refresh() { case eInputListButtonsGame: case eInputListButtonsEngine: case eInputListButtonsDeveloper: + case eInputListButtonsOnline: RefreshButtonList(); break; default: @@ -644,6 +645,8 @@ void Window_Settings::RefreshButtonCategory() { [this]() { Push(eInputListButtonsEngine, 1); }); AddOption(MenuItem("Developer", "Buttons useful for developers", ""), [this]() { Push(eInputListButtonsDeveloper, 2); }); + AddOption(MenuItem("Online", "Buttons related to online play", ""), + [this]() { Push(eInputListButtonsOnline, 3); }); } void Window_Settings::RefreshButtonList() { @@ -661,12 +664,14 @@ void Window_Settings::RefreshButtonList() { case 1: buttons = {Input::SETTINGS_MENU, Input::TOGGLE_FPS, Input::TOGGLE_FULLSCREEN, Input::TOGGLE_ZOOM, Input::TAKE_SCREENSHOT, Input::RESET, Input::FAST_FORWARD_A, Input::FAST_FORWARD_B, - Input::PAGE_UP, Input::PAGE_DOWN }; + Input::PAGE_UP, Input::PAGE_DOWN, }; break; case 2: buttons = { Input::DEBUG_MENU, Input::DEBUG_THROUGH, Input::DEBUG_SAVE, Input::DEBUG_ABORT_EVENT, Input::SHOW_LOG }; break; + case 3: + buttons = { Input::SHOW_CHAT }; } for (auto b: buttons) { diff --git a/src/window_settings.h b/src/window_settings.h index 21d2d3a42..1258c5df0 100644 --- a/src/window_settings.h +++ b/src/window_settings.h @@ -37,6 +37,7 @@ class Window_Settings : public Window_Selectable { eInputListButtonsGame, eInputListButtonsEngine, eInputListButtonsDeveloper, + eInputListButtonsOnline, eInputButtonOption, eInputButtonAdd, eInputButtonRemove, From 2b7a1d4b1dd64844052c2766f889a1b1252f05a6 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Fri, 20 Dec 2024 01:36:16 -0500 Subject: [PATCH 07/18] Finish screenspace implementation, chatter names --- CMakeLists.txt | 1 + src/baseui.cpp | 1 + src/baseui.h | 21 +++++ src/drawable.cpp | 26 ++++++ src/drawable.h | 20 +++-- src/drawable_list.cpp | 22 +++++ src/drawable_list.h | 1 + src/graphics.cpp | 17 +++- src/graphics.h | 8 +- src/input_buttons_desktop.cpp | 2 +- src/multiplayer/chat_overlay.cpp | 117 ++++++++++++++++++--------- src/multiplayer/chat_overlay.h | 16 +++- src/multiplayer/game_multiplayer.cpp | 28 ++++++- src/multiplayer/game_multiplayer.h | 11 ++- src/platform/sdl/sdl2_ui.cpp | 45 +++++++++-- src/platform/sdl/sdl2_ui.h | 1 + src/platform/ynoshell/main.cpp | 112 ++----------------------- src/player.cpp | 3 +- src/transition.cpp | 5 +- 19 files changed, 283 insertions(+), 174 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 22ac1e1ee..744788251 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1297,6 +1297,7 @@ if(PLAYER_BUILD_EXECUTABLE AND ${PLAYER_TARGET_PLATFORM} MATCHES "^SDL.*$" AND N if(WIN32) # Open console for Debug builds 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}) diff --git a/src/baseui.cpp b/src/baseui.cpp index e5fef264e..29c7a1e4b 100644 --- a/src/baseui.cpp +++ b/src/baseui.cpp @@ -68,6 +68,7 @@ BitmapRef BaseUi::CaptureScreen() { void BaseUi::CleanDisplay() { main_surface->Clear(); + screen_surface->Clear(); } void BaseUi::SetGameResolution(ConfigEnum::GameResolution resolution) { diff --git a/src/baseui.h b/src/baseui.h index d4ea9a4e8..67380afca 100644 --- a/src/baseui.h +++ b/src/baseui.h @@ -32,6 +32,8 @@ #include "game_config.h" #include "game_clock.h" #include "input.h" +#include "bitmap.h" +#include "drawable.h" #ifdef SUPPORT_AUDIO struct AudioInterface; @@ -132,6 +134,8 @@ class BaseUi { /** @return dimensions of the window */ virtual Rect GetWindowMetrics() const; + Rect GetScreenSurfaceRect() const; + /** * Gets client width size. * @@ -169,7 +173,9 @@ class BaseUi { std::array& GetTouchInput(); BitmapRef const& GetDisplaySurface() const; + BitmapRef const& GetScreenSurface() const; BitmapRef& GetDisplaySurface(); + BitmapRef& GetScreenSurface(); /** * Requests a resolution change of the framebuffer. @@ -302,6 +308,9 @@ class BaseUi { /** Surface used for zoom. */ BitmapRef main_surface; + /** Screen-sized surface. */ + BitmapRef screen_surface = nullptr; + /** Mouse position on screen relative to the window. */ Point mouse_pos; @@ -340,6 +349,10 @@ inline Rect BaseUi::GetWindowMetrics() const { return {-1, -1, -1, -1}; } +inline Rect BaseUi::GetScreenSurfaceRect() const { + return GetScreenSurface()->GetRect(); +} + inline bool BaseUi::IsFrameRateSynchronized() const { return external_frame_rate; } @@ -368,6 +381,14 @@ inline BitmapRef& BaseUi::GetDisplaySurface() { return main_surface; } +inline BitmapRef const& BaseUi::GetScreenSurface() const { + return screen_surface ? screen_surface : main_surface; +} + +inline BitmapRef& BaseUi::GetScreenSurface() { + return screen_surface ? screen_surface : main_surface; +} + inline bool BaseUi::vChangeDisplaySurfaceResolution(int new_width, int new_height) { (void)new_width; (void)new_height; diff --git a/src/drawable.cpp b/src/drawable.cpp index db4abd538..3d12443ae 100644 --- a/src/drawable.cpp +++ b/src/drawable.cpp @@ -18,9 +18,35 @@ #include "drawable.h" #include #include "drawable_mgr.h" +#include "baseui.h" +#include "player.h" + +std::vector Drawable::screen_drawables; + +Drawable::Drawable(Z_t z, Flags flags) + : _z(z), + _flags(flags) +{ + if (IsScreenspace()) + screen_drawables.emplace_back(this); +} Drawable::~Drawable() { DrawableMgr::Remove(this); + Drawable* items[]{ this }; + RemoveScreenDrawable(items); +} + +void Drawable::RemoveScreenDrawable(lcf::Span container) { + if (container.empty() || !DisplayUi) return; + for (auto to_remove = screen_drawables.begin(); to_remove != screen_drawables.end(); ++to_remove) { + for (auto& it : container) { + if (to_remove._Ptr == &it) { + screen_drawables.erase(to_remove); + break; + } + } + } } void Drawable::SetZ(Z_t nz) { diff --git a/src/drawable.h b/src/drawable.h index 6f5e9baec..3da8c977a 100644 --- a/src/drawable.h +++ b/src/drawable.h @@ -20,6 +20,8 @@ #include #include +#include +#include class Bitmap; class Drawable; @@ -44,6 +46,8 @@ class Drawable { Shared = 2, /** This flag indicates the drawable should not be drawn */ Invisible = 4, + /** Draws directly on the screen. OnResolutionChange should also be implemented to handle screen resolution changes. */ + Screenspace = 8, /** The default flag set */ Default = None }; @@ -70,6 +74,10 @@ class Drawable { /** @return true if the drawable is currently visible */ bool IsVisible() const; + bool IsScreenspace() const noexcept; + + virtual void OnResolutionChange() {} + /** * Set if the drawable should be visible * @@ -113,6 +121,9 @@ class Drawable { * @return Priority or 0 when not found */ static Z_t GetPriorityForBattleLayer(int which); + + static std::vector screen_drawables; + static void RemoveScreenDrawable(lcf::Span container); private: Z_t _z = 0; Flags _flags = Flags::Default; @@ -136,11 +147,6 @@ inline Drawable::Flags operator~(Drawable::Flags f) { return static_cast(~static_cast(f)); } -inline Drawable::Drawable(Z_t z, Flags flags) - : _z(z), - _flags(flags) -{ -} inline Drawable::Z_t Drawable::GetZ() const { return _z; @@ -158,6 +164,10 @@ inline bool Drawable::IsVisible() const { return !static_cast(_flags & Flags::Invisible); } +inline bool Drawable::IsScreenspace() const noexcept { + return static_cast(_flags & Flags::Screenspace); +} + inline void Drawable::SetVisible(bool value) { _flags = value ? _flags & ~Flags::Invisible : _flags | Flags::Invisible; } diff --git a/src/drawable_list.cpp b/src/drawable_list.cpp index bea6587ee..f675b6b05 100644 --- a/src/drawable_list.cpp +++ b/src/drawable_list.cpp @@ -32,6 +32,7 @@ DrawableList::~DrawableList() { } void DrawableList::Clear() { + Drawable::RemoveScreenDrawable(lcf::MakeSpan(_list)); _list.clear(); SetClean(); } @@ -111,3 +112,24 @@ void DrawableList::Draw(Bitmap& dst, Drawable::Z_t min_z, Drawable::Z_t max_z) { } } +void DrawableList::Draw(Bitmap& dst, Bitmap& dst_screen, Drawable::Z_t min_z, Drawable::Z_t max_z) { + if (IsDirty()) { + Sort(); + } else { + assert(IsSorted()); + } + + for (auto* drawable : _list) { + auto z = drawable->GetZ(); + if (z < min_z) { + continue; + } + if (z > max_z) { + break; + } + if (drawable->IsVisible()) { + drawable->Draw(drawable->IsScreenspace() ? dst_screen : dst); + } + } +} + diff --git a/src/drawable_list.h b/src/drawable_list.h index 2436e2238..bbdd1f16b 100644 --- a/src/drawable_list.h +++ b/src/drawable_list.h @@ -134,6 +134,7 @@ class DrawableList { */ void Draw(Bitmap& dst, Drawable::Z_t min_z, Drawable::Z_t max_z); + void Draw(Bitmap& dst, Bitmap& dst_screen, Drawable::Z_t min_z, Drawable::Z_t max_z); private: std::vector _list; bool _dirty = false; diff --git a/src/graphics.cpp b/src/graphics.cpp index ba0bb16c5..6fb4ca8f0 100644 --- a/src/graphics.cpp +++ b/src/graphics.cpp @@ -112,28 +112,37 @@ void Graphics::UpdateTitle() { #endif } -void Graphics::Draw(Bitmap& dst) { +void Graphics::Draw(BaseUi& ui) { auto& transition = Transition::instance(); auto min_z = std::numeric_limits::min(); auto constexpr max_z = std::numeric_limits::max(); + auto& dst = *ui.GetDisplaySurface(); + auto& dst_screen = *ui.GetScreenSurface(); if (transition.IsActive()) { min_z = transition.GetZ(); } else if (transition.IsErasedNotActive()) { min_z = transition.GetZ() + 1; dst.Clear(); + dst_screen.Clear(); } - LocalDraw(dst, min_z, max_z); + LocalDraw(dst, dst_screen, min_z, max_z); } -void Graphics::LocalDraw(Bitmap& dst, Drawable::Z_t min_z, Drawable::Z_t max_z) { +void Graphics::LocalDraw(Bitmap& dst, Bitmap& dst_screen, Drawable::Z_t min_z, Drawable::Z_t max_z) { auto& drawable_list = DrawableMgr::GetLocalList(); if (!drawable_list.empty() && min_z == std::numeric_limits::min()) { current_scene->DrawBackground(dst); } - drawable_list.Draw(dst, min_z, max_z); + drawable_list.Draw(dst, dst_screen, min_z, max_z); +} + +void Graphics::OnResolutionChange() { +#ifdef PLAYER_YNO + Graphics::GetChatOverlay().OnResolutionChange(); +#endif } std::shared_ptr Graphics::UpdateSceneCallback() { diff --git a/src/graphics.h b/src/graphics.h index 2e74577fb..ca8dce81d 100644 --- a/src/graphics.h +++ b/src/graphics.h @@ -24,6 +24,7 @@ #include "drawable.h" #include "drawable_list.h" #include "game_clock.h" +#include "baseui.h" class MessageOverlay; class Scene; @@ -51,9 +52,12 @@ namespace Graphics { */ void Update(); - void Draw(Bitmap& dst); + void Draw(BaseUi& ui); - void LocalDraw(Bitmap& dst, Drawable::Z_t min_z, Drawable::Z_t max_z); + void LocalDraw(Bitmap& dst, Bitmap& dst_screen, Drawable::Z_t min_z, Drawable::Z_t max_z); + + /** Screenspace drawables can put their resolution handlers here. Called after DisplayUi is done updating the surfaces. */ + void OnResolutionChange(); std::shared_ptr UpdateSceneCallback(); diff --git a/src/input_buttons_desktop.cpp b/src/input_buttons_desktop.cpp index 7a61f07eb..dceeb13b2 100644 --- a/src/input_buttons_desktop.cpp +++ b/src/input_buttons_desktop.cpp @@ -101,7 +101,7 @@ Input::ButtonMappingArray Input::GetDefaultButtonMappings() { {RESET, Keys::F12}, {FAST_FORWARD_A, Keys::F}, {FAST_FORWARD_B, Keys::G}, - {SHOW_CHAT, Keys::T}, + {SHOW_CHAT, Keys::TAB}, #if defined(USE_MOUSE) && defined(SUPPORT_MOUSE) {MOUSE_LEFT, Keys::MOUSE_LEFT}, diff --git a/src/multiplayer/chat_overlay.cpp b/src/multiplayer/chat_overlay.cpp index b6eff3cb1..eaed163f4 100644 --- a/src/multiplayer/chat_overlay.cpp +++ b/src/multiplayer/chat_overlay.cpp @@ -23,19 +23,24 @@ #include "game_message.h" #include "output.h" #include "input_buttons.h" +#include "cache.h" -ChatOverlay::ChatOverlay() : Drawable(Priority_Overlay, Drawable::Flags::Global) +ChatOverlay::ChatOverlay() : Drawable(Priority_Overlay, Drawable::Flags::Global | Drawable::Flags::Screenspace) { } void ChatOverlay::Draw(Bitmap& dst) { - dst.Blit(ox, oy, *bitmap, bitmap->GetRect(), 255); - if (!dirty) return; + if (!IsAnyMessageVisible() && !show_all) return; + + if (!dirty) { + dst.Blit(ox, oy, *bitmap, bitmap->GetRect(), 255); + return; + } bitmap->Clear(); - constexpr int y_end = 240; + int y_end = DisplayUi->GetScreenSurfaceRect().height; int i = 0; @@ -45,10 +50,15 @@ void ChatOverlay::Draw(Bitmap& dst) { bool unlocked_start = scroll < half_screen; bool unlocked_end = scroll > messages.size() - half_screen; - int last_sticky = (int)messages.size() - half_screen * 2; + int last_sticky = std::max(0, (int)messages.size() - half_screen * 2); // we want half a screen before first sticky and half a screen after the last sticky. int viewport = std::clamp(scroll - half_screen, 0, std::min(scroll + half_screen, last_sticky)); + std::vector lines; + int lidx = 0; + const auto& system = *Cache::SystemOrBlack(); + constexpr Color offwhite = Color(200, 200, 200, 255); + for (auto message = messages.rbegin() + viewport; message != messages.rend(); ++message) { if (!message->hidden || show_all) { auto& bg = !show_all ? black : @@ -56,30 +66,55 @@ void ChatOverlay::Draw(Bitmap& dst) { unlocked_end ? (last_sticky + i == scroll ? grey : black) : i == half_screen ? grey : black; - bitmap->Blit(0, y_end - (i + 1) * text_height, *bg, bg->GetRect(), 128); - constexpr Color white = Color(255, 255, 255, 255); - Text::Draw(*bitmap, 2, y_end - (i + 1) * text_height, *Font::NameText(), white, message->text); + // figure out how many lines we need to print + std::string temp = fmt::format("[{}] {}", message->sender, message->text); + bool first = true; + Game_Message::WordWrap( + temp, + bitmap->width(), + [&](StringView line) { + if (first) { + first = false; + lines.emplace_back(line.substr(1 + message->sender.size() + 2)); + } else + lines.emplace_back(line); + }, *Font::NameText()); + for (auto line = lines.rbegin(); line != lines.rend(); ++line) { + // semitransparent bg + int y = y_end - (lidx + 1) * text_height; + bitmap->Blit(0, y, *bg, bg->GetRect(), 200); + + int offset = 2; + if (std::next(line) == lines.rend()) { + // draw the username + offset += Text::Draw(*bitmap, offset, y, *Font::NameText(), offwhite, "[").x; + offset += Text::Draw(*bitmap, offset, y, *Font::NameText(), *message->system, 0, message->sender).x; + offset += Text::Draw(*bitmap, offset, y, *Font::NameText(), offwhite, "] ").x; + } + Text::Draw(*bitmap, offset, y, *Font::NameText(), offwhite, *line); + ++lidx; + if (lidx * text_height > y_end) break; + } ++i; + lines.clear(); } if (!show_all && i > message_max_minimized) break; - if (i * text_height > y_end) break; + if (lidx * text_height > y_end) break; } + dst.Blit(ox, oy, *bitmap, bitmap->GetRect(), 255); dirty = false; } -void ChatOverlay::AddMessage(const std::string& message) { +void ChatOverlay::AddMessage(std::string_view message, std::string_view sender, std::string_view system) { if (message.empty()) { return; } + counter = 0; + + auto sysref = system.empty() ? Cache::SystemOrBlack() : Cache::System(system.data()); - Game_Message::WordWrap( - message, - Player::screen_width, - [&](StringView line) { - messages.emplace_back(std::string(line)); - }, *Font::NameText() - ); + messages.emplace_back(std::string(message), std::string(sender), sysref); while (messages.size() > message_max) { messages.pop_front(); @@ -99,41 +134,38 @@ void ChatOverlay::Update() { if (show_all) { + static int delta; if (Input::IsTriggered(Input::InputButton::SCROLL_DOWN) || - Input::IsTriggered(Input::InputButton::DOWN) || - Input::IsRepeated(Input::InputButton::DOWN)) { + Input::IsTriggered(Input::InputButton::PAGE_DOWN)) { + delta = 5; DoScroll(-1); } + else if (Input::IsRepeated(Input::InputButton::PAGE_DOWN)) { + DoScroll(std::clamp(--delta / 5, -4, -1)); + } if (Input::IsTriggered(Input::InputButton::SCROLL_UP) || - Input::IsTriggered(Input::InputButton::UP) || - Input::IsRepeated(Input::InputButton::UP)) { + Input::IsTriggered(Input::InputButton::PAGE_UP)) { + delta = -5; DoScroll(1); } - if (Input::IsTriggered(Input::InputButton::PAGE_DOWN)) { - scroll = 0; - dirty = true; - } - if (Input::IsTriggered(Input::InputButton::PAGE_UP)) { - scroll = messages.size() - 1; - dirty = true; + else if (Input::IsRepeated(Input::InputButton::PAGE_UP)) { + DoScroll(std::clamp(++delta / 5, 1, 4)); } } - if (++counter > 150) { - counter = 0; - for (auto& message : messages) { - if (!message.hidden) { + if (IsAnyMessageVisible()) { + if (++counter > 450) { + counter = 0; + for (auto& message : messages) { message.hidden = true; - break; } + dirty = true; } - dirty = true; } } void ChatOverlay::SetShowAll(bool show_all) { - Output::Warning("SetShowAll"); this->show_all = show_all; dirty = true; } @@ -150,10 +182,15 @@ void ChatOverlay::OnResolutionChange() { } int text_height = 12; // Font::NameText()->vGetSize('T').height; - black = Bitmap::Create(Player::screen_width, text_height, Color(0, 0, 0, 255)); - grey = Bitmap::Create(Player::screen_width, text_height, Color(100, 100, 100, 255)); - bitmap = Bitmap::Create(Player::screen_width, text_height * message_max, true); + Rect rect = DisplayUi->GetScreenSurfaceRect(); + int width = rect.width; + black = Bitmap::Create(width, text_height, Color(0, 0, 0, 255)); + grey = Bitmap::Create(width, text_height, Color(100, 100, 100, 255)); + bitmap = Bitmap::Create(width, text_height * message_max, true); + dirty = true; +} + +bool ChatOverlay::IsAnyMessageVisible() const { + return std::any_of(messages.cbegin(), messages.cend(), [](const ChatOverlayMessage& m) { return !m.hidden; }); } -ChatOverlayMessage::ChatOverlayMessage(std::string text) : - text(std::move(text)) {} diff --git a/src/multiplayer/chat_overlay.h b/src/multiplayer/chat_overlay.h index 4282eb325..809c39719 100644 --- a/src/multiplayer/chat_overlay.h +++ b/src/multiplayer/chat_overlay.h @@ -28,8 +28,10 @@ //using ChatOverlayMessage = std::string; class ChatOverlayMessage { public: - ChatOverlayMessage(std::string text); + ChatOverlayMessage(std::string text, std::string sender, BitmapRef system); std::string text; + std::string sender; + BitmapRef system; bool hidden = false; }; @@ -38,13 +40,15 @@ class ChatOverlay : public Drawable { ChatOverlay(); void Draw(Bitmap& dst) override; void Update(); - void AddMessage(const std::string& msg); - void OnResolutionChange(); + void AddMessage(std::string_view msg, std::string_view sender, std::string_view system = ""); + void OnResolutionChange() override; void SetShowAll(bool show_all); inline void SetShowAll() { SetShowAll(!show_all); } inline bool ShowingAll() const noexcept { return show_all; } void DoScroll(int increase); private: + bool IsAnyMessageVisible() const; + bool dirty = false; bool show_all = false; int ox = 0; @@ -60,4 +64,10 @@ class ChatOverlay : public Drawable { std::deque messages; }; + +inline ChatOverlayMessage::ChatOverlayMessage(std::string text, std::string sender, BitmapRef system) : + text(std::move(text)), sender(std::move(sender)), system(system) +{ +} + #endif diff --git a/src/multiplayer/game_multiplayer.cpp b/src/multiplayer/game_multiplayer.cpp index 4617d7f4e..89e762889 100644 --- a/src/multiplayer/game_multiplayer.cpp +++ b/src/multiplayer/game_multiplayer.cpp @@ -481,9 +481,27 @@ void Game_Multiplayer::InitConnection() { cpr::Response resp = cpr::Get(cpr::Url{endpoint}); if (resp.status_code >= 200 && resp.status_code < 300) { json chatmsgs = json::parse(resp.text); + //playerdata = std::move(chatmsgs["players"]); + for (auto& it : chatmsgs["players"]) { + auto& entry = playerdata[(std::string)it["uuid"]]; + entry.name = it["name"]; + entry.systemName = it["systemName"]; + entry.rank = it["rank"]; + entry.account = it["account"]; + entry.badge = it["badge"]; + } + const auto& messages = chatmsgs["messages"]; for (auto it = messages.begin(); it != messages.end(); ++it) { - on_chat_msg((std::string)(*it)["contents"], it != std::prev(messages.end())); + std::string name = "Unknown Player"; + std::string system; + if (auto player = playerdata.find((std::string)(*it)["uuid"]); player != playerdata.end()) { + name = player->second.name; + system = player->second.systemName; + } + on_chat_msg( + (std::string)(*it)["contents"], name, system, + it != std::prev(messages.end())); } } } @@ -492,8 +510,14 @@ void Game_Multiplayer::InitConnection() { }); sessionConn.RegisterHandler("gsay", [this](SessionGSay& p) { Output::Debug("{}: {}", p.uuid, p.msg); + std::string name = "Unknown Player"; + std::string system; + if (auto player = playerdata.find(p.uuid); player != playerdata.end()) { + name = player->second.name; + system = player->second.systemName; + } if (on_chat_msg) { - on_chat_msg(p.msg, false); + on_chat_msg(p.msg, name, system, false); } }); #endif diff --git a/src/multiplayer/game_multiplayer.h b/src/multiplayer/game_multiplayer.h index fd3bf401d..a99f9a1d9 100644 --- a/src/multiplayer/game_multiplayer.h +++ b/src/multiplayer/game_multiplayer.h @@ -59,9 +59,18 @@ class Game_Multiplayer { YNOConnection connection; #ifndef EMSCRIPTEN YNOConnection sessionConn; - std::function on_chat_msg; + struct PlayerData { + public: + std::string name; + std::string systemName; + int rank; + bool account; + std::string badge; + }; + std::function on_chat_msg; std::function on_system_graphic_change; std::string lastmsgid; + std::map playerdata; #endif bool session_active{ false }; // if true, it will automatically reconnect when disconnected bool session_connected{ false }; diff --git a/src/platform/sdl/sdl2_ui.cpp b/src/platform/sdl/sdl2_ui.cpp index a0a60f1e4..e1319ca44 100644 --- a/src/platform/sdl/sdl2_ui.cpp +++ b/src/platform/sdl/sdl2_ui.cpp @@ -498,6 +498,11 @@ bool Sdl2Ui::RefreshDisplayMode() { display_width, display_height, Color(0, 0, 0, 255)); } + if (!screen_surface) { + int scale = window.scale ? static_cast(ceilf(window.scale)) : 1; + screen_surface = Bitmap::Create(scale * display_width, scale * display_height, true, main_surface->pitch()); + } + return true; } @@ -679,7 +684,11 @@ void Sdl2Ui::UpdateDisplay() { SDL_RenderSetViewport(sdl_renderer, &viewport); } - if (vcfg.scaling_mode.Get() == ConfigEnum::ScalingMode::Bilinear && window.scale > 0.f) { + // TODO: Remove the division by 2 to render at actual size + screen_surface = Bitmap::Create(viewport.w / 2, viewport.h / 2, true, main_surface->pitch()); + + if (vcfg.scaling_mode.Get() == ConfigEnum::ScalingMode::Bilinear && + window.scale > 0.f) { if (sdl_texture_scaled) { SDL_DestroyTexture(sdl_texture_scaled); } @@ -691,6 +700,17 @@ void Sdl2Ui::UpdateDisplay() { Output::Debug("SDL_CreateTexture failed : {}", SDL_GetError()); } } + if (sdl_texture_screen) + SDL_DestroyTexture(sdl_texture_screen); + sdl_texture_screen = SDL_CreateTexture(sdl_renderer, texture_format, SDL_TEXTUREACCESS_STREAMING, + screen_surface->width(), screen_surface->height()); + if (!sdl_texture_screen) + Output::Warning("failed to create screen texture: {}", SDL_GetError()); + else + Output::Debug("sdl_texture_screen: {}", SDL_GetPixelFormatName(texture_format)); + + for (auto& it : Drawable::screen_drawables) + it->OnResolutionChange(); } SDL_RenderClear(sdl_renderer); @@ -702,13 +722,26 @@ void Sdl2Ui::UpdateDisplay() { SDL_SetRenderTarget(sdl_renderer, nullptr); SDL_RenderCopy(sdl_renderer, sdl_texture_scaled, nullptr, nullptr); - } else { + } + //else if (screen_surface && sdl_texture_screen) { + + // SDL_SetRenderTarget(sdl_renderer, sdl_texture_scaled); + // SDL_RenderCopy(sdl_renderer, sdl_texture_game, nullptr, nullptr); + // SDL_RenderCopy(sdl_renderer, sdl_texture_screen, nullptr, nullptr); + + // SDL_SetRenderTarget(sdl_renderer, nullptr); + // SDL_RenderCopy(sdl_renderer, sdl_texture_scaled, nullptr, nullptr); + //} + else { SDL_RenderCopy(sdl_renderer, sdl_texture_game, nullptr, nullptr); } -#else - SDL_RenderClear(sdl_renderer); - SDL_RenderCopy(sdl_renderer, sdl_texture_game, nullptr, nullptr); -#endif + + if (screen_surface && sdl_texture_screen) { + SDL_UpdateTexture(sdl_texture_screen, nullptr, screen_surface->pixels(), screen_surface->pitch()); + SDL_SetTextureBlendMode(sdl_texture_screen, SDL_BLENDMODE_BLEND); + SDL_RenderCopy(sdl_renderer, sdl_texture_screen, nullptr, nullptr); + } + SDL_RenderPresent(sdl_renderer); } diff --git a/src/platform/sdl/sdl2_ui.h b/src/platform/sdl/sdl2_ui.h index 56849ee53..3afaa2203 100644 --- a/src/platform/sdl/sdl2_ui.h +++ b/src/platform/sdl/sdl2_ui.h @@ -131,6 +131,7 @@ class Sdl2Ui final : public BaseUi { /** Main SDL window. */ SDL_Texture* sdl_texture_game = nullptr; SDL_Texture* sdl_texture_scaled = nullptr; + SDL_Texture* sdl_texture_screen = nullptr; SDL_Window* sdl_window = nullptr; SDL_Renderer* sdl_renderer = nullptr; SDL_Joystick *sdl_joystick = nullptr; diff --git a/src/platform/ynoshell/main.cpp b/src/platform/ynoshell/main.cpp index 0d999b283..d15b6a4cb 100644 --- a/src/platform/ynoshell/main.cpp +++ b/src/platform/ynoshell/main.cpp @@ -52,13 +52,6 @@ wxIMPLEMENT_APP(YnoApp); // } //}; -//class YnoGameContainer : public wxCustomBackgroundWindow -//{ -//public: -// YnoGameContainer(wxWindow* parent) { -// Create(parent, wxID_ANY); -// }; -//}; using YnoGameContainer = wxWindow; class YnoFrame : public wxFrame @@ -67,19 +60,10 @@ class YnoFrame : public wxFrame YnoFrame(); YnoGameContainer* gameWindow; - //wxScrolledWindow* chatLog; - //wxBoxSizer* chatboxSizer; - //wxBoxSizer* chatLogSizer; - //wxTextCtrl* chatInput; - //bool shouldScrollChat = true; - //std::vector chatmsgs; + wxTextCtrl* chatInput; private: - //void DoZoom(bool magnify); void OnKeyDownFrame(wxKeyEvent& event); - //void OnKeyDownChatInput(wxKeyEvent& event); - //void OnScrollChatLog(wxScrollWinEvent& event); - //void OverlayPaint(wxPaintEvent&); }; @@ -102,73 +86,21 @@ bool YnoApp::OnInit() std::vector args{ (char**)app.argv, (char**)app.argv + app.argc }; Player::Init(std::move(args)); Player::Run(); - - return true; } YnoFrame::YnoFrame() : wxFrame(nullptr, wxID_ANY, "YNOproject") { - //SetFont({ 16, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL }); wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL); - //childPanel = new wxWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE); - - gameWindow = new YnoGameContainer(this, wxID_ANY); - gameWindow->Create(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE); + gameWindow = new YnoGameContainer(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE); gameWindow->Bind(wxEVT_KEY_DOWN, &YnoFrame::OnKeyDownFrame, this); sizer->Add((wxWindow*)gameWindow, 1, wxEXPAND); - GMI().on_chat_msg = [](std::string_view msg, bool) { - Graphics::GetChatOverlay().AddMessage(msg.data()); + GMI().on_chat_msg = [](std::string_view msg, std::string_view sender, std::string_view system, bool) { + Graphics::GetChatOverlay().AddMessage(msg, sender, system); }; - - //childPanel->SetBackgroundColour(*wxBLACK); - //childPanel->Bind(wxEVT_KEY_DOWN, &YnoFrame::OnKeyDownFrame, this); - //sizer->Add(childPanel, 1, wxEXPAND); - - - - //chatboxSizer = new wxBoxSizer(wxVERTICAL); - - //auto chatwindow = new wxRpgWindow(this); - //auto chatwindowSizer = new wxBoxSizer(wxHORIZONTAL); - //chatwindowSizer->Add(chatboxSizer, 1, wxEXPAND); - //chatwindow->SetSizer(chatwindowSizer); - - //sizer->Add(chatwindowSizer, 0, wxEXPAND | wxDOWN); - - //chatLog = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(Zoom(350), -1), wxVSCROLL); - //chatLog->SetBackgroundColour(wxTRANSPARENT); - //chatLog->SetScrollRate(0, Zoom(25)); - //chatLogSizer = new wxBoxSizer(wxVERTICAL); - //chatLog->SetSizer(chatLogSizer); - //chatLog->Bind(wxEVT_SCROLLWIN_LINEUP, &YnoFrame::OnScrollChatLog, this); - //chatLog->Bind(wxEVT_SCROLLWIN_LINEDOWN, &YnoFrame::OnScrollChatLog, this); - //chatboxSizer->Add(chatLog, 1, wxEXPAND); - - //GMI().on_chat_msg = [this](std::string_view msg, bool syncing) { - // if (chatmsgs.size() > 100) { - // chatmsgs[0]->Destroy(); - // chatmsgs.erase(chatmsgs.begin()); - // } - // chatmsgs.emplace_back(new YnoChatMsg(chatLogSizer, chatLog, std::string(msg))); - // if (syncing) return; - // chatboxSizer->Layout(); - // Refresh(); - // if (shouldScrollChat) - // chatLog->Scroll(wxPoint(-1, chatLog->GetScrollRange(wxVSCROLL))); - //}; - - //chatInput = new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxSize(-1, Zoom(50))); - //chatInput->Bind(wxEVT_KEY_DOWN, &YnoFrame::OnKeyDownChatInput, this); - //chatInput->SetFont(GetFont().Scaled(zoom)); - - //auto chatInputSizer = new wxBoxSizer(wxHORIZONTAL); - //chatInputSizer->Add(chatInput, 1, wxEXPAND | wxALL); - //chatboxSizer->Add(chatInputSizer); - SetSizer(sizer); }; @@ -181,38 +113,4 @@ void YnoFrame::OnKeyDownFrame(wxKeyEvent& event) { } event.Skip(); } -// -//void YnoFrame::OnKeyDownChatInput(wxKeyEvent& event) { -// switch (event.GetKeyCode()) { -// case WXK_TAB: -// childPanel->SetFocus(); -// break; -// case WXK_RETURN: -// chatInput->Clear(); -// break; -// default: -// event.Skip(); -// } -//} -// -//void YnoFrame::OnScrollChatLog(wxScrollWinEvent& event) { -// int diff = chatLog->GetScrollRange(wxVERTICAL) - chatLog->GetScrollPos(wxVERTICAL); -// shouldScrollChat = diff < Zoom(55); -// event.Skip(); -//} -// -//void YnoFrame::DoZoom(bool magnify) { -// float step = zoom <= 1 ? 0.1 : 0.25; -// float wanted = magnify ? zoom + step : zoom - step; -// zoom = std::clamp(wanted, 0.8f, 2.f); -// -// int oldPosition = chatLog->GetScrollPos(wxVERTICAL); -// chatboxSizer->SetMinSize(wxSize(Zoom(350), -1)); -// chatInput->SetMinSize(wxSize(-1, Zoom(50))); -// for (auto msg : chatmsgs) { -// msg->text->SetFont(msg->GetFont().Scaled(zoom)); -// } -// Layout(); -// Refresh(); -// chatLog->Scroll(wxPoint(-1, Zoom(oldPosition))); -//} + diff --git a/src/player.cpp b/src/player.cpp index 0ea797391..eae72e435 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -412,8 +412,9 @@ void Player::Update(bool update_scene) { void Player::Draw() { Graphics::Update(); - Graphics::Draw(*DisplayUi->GetDisplaySurface()); + Graphics::Draw(*DisplayUi); DisplayUi->UpdateDisplay(); + DisplayUi->GetScreenSurface()->Clear(); } void Player::IncFrame() { diff --git a/src/transition.cpp b/src/transition.cpp index deedb7714..796735b7c 100644 --- a/src/transition.cpp +++ b/src/transition.cpp @@ -442,18 +442,19 @@ void Transition::Update() { scene->UpdateGraphics(); } + auto& dst_screen = *DisplayUi->GetScreenSurface(); if (!screen1) { // erase -> erase is ingored // any -> erase - screen1 was drawn in init. assert(ToErase() && !FromErase()); screen1 = Bitmap::Create(Player::screen_width, Player::screen_height, false); - Graphics::LocalDraw(*screen1, std::numeric_limits::min(), GetZ() - 1); + Graphics::LocalDraw(*screen1, dst_screen, std::numeric_limits::min(), GetZ() - 1); } if (ToErase()) { screen2 = Bitmap::Create(Player::screen_width, Player::screen_height, Color(0, 0, 0, 255)); } else { screen2 = Bitmap::Create(Player::screen_width, Player::screen_height, false); - Graphics::LocalDraw(*screen2, std::numeric_limits::min(), GetZ() - 1); + Graphics::LocalDraw(*screen2, dst_screen, std::numeric_limits::min(), GetZ() - 1); } } From 2e64a369437af0f1a7a346b394053845568466f2 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Sun, 22 Dec 2024 23:50:19 -0500 Subject: [PATCH 08/18] Chat badges and emojis, wrapping spans, scrolling --- CMakeLists.txt | 2 + src/async_handler.cpp | 93 ++++++-- src/async_handler.h | 22 ++ src/bitmap.cpp | 24 ++ src/bitmap.h | 4 + src/cache.cpp | 13 + src/cache.h | 5 +- src/directory_tree.cpp | 5 +- src/filefinder.cpp | 11 +- src/font.cpp | 54 ++++- src/font.h | 4 + src/game_message.cpp | 80 ++++++- src/game_message.h | 16 ++ src/input_buttons.h | 10 +- src/input_buttons_desktop.cpp | 6 + src/multiplayer/chat_overlay.cpp | 345 +++++++++++++++++++++++---- src/multiplayer/chat_overlay.h | 102 +++++++- src/multiplayer/game_multiplayer.cpp | 64 ++++- src/multiplayer/game_multiplayer.h | 12 +- src/multiplayer/messages.h | 32 ++- src/multiplayer/scene_overlay.cpp | 10 + src/multiplayer/scene_overlay.h | 41 ++++ src/platform/sdl/sdl2_ui.cpp | 2 +- src/platform/ynoshell/main.cpp | 8 +- src/player.cpp | 5 + src/scene.h | 2 + src/text.cpp | 6 +- src/window_settings.cpp | 2 +- 28 files changed, 861 insertions(+), 119 deletions(-) create mode 100644 src/multiplayer/scene_overlay.cpp create mode 100644 src/multiplayer/scene_overlay.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 744788251..bc6f7ff71 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -521,6 +521,8 @@ if(NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") target_sources(${PROJECT_NAME} PRIVATE src/multiplayer/chat_overlay.h src/multiplayer/chat_overlay.cpp + src/multiplayer/scene_overlay.h + src/multiplayer/scene_overlay.cpp ) endif() diff --git a/src/async_handler.cpp b/src/async_handler.cpp index 9ce8695a5..ce69d4f87 100644 --- a/src/async_handler.cpp +++ b/src/async_handler.cpp @@ -25,6 +25,11 @@ # include # include using json = nlohmann::json; +#elif defined(PLAYER_YNO) +# include +# include +# include +# define EP_CONTAINER_OF(ptr, type, member) (type*)((char*)ptr - offsetof(type, member)) #endif #include "async_handler.h" @@ -46,9 +51,7 @@ namespace { std::unordered_map> async_requests; std::unordered_map file_mapping; int next_id = 0; -#ifdef __EMSCRIPTEN__ int index_version = 1; -#endif FileRequestAsync* GetRequest(const std::string& path) { auto it = async_requests.find(path); @@ -72,20 +75,27 @@ namespace { return std::make_shared(next_id++); } -#ifdef __EMSCRIPTEN__ constexpr size_t ASYNC_MAX_RETRY_COUNT{ 16 }; struct async_download_context { std::string url, file, param; std::weak_ptr obj; size_t count; +#ifndef EMSCRIPTEN + uv_work_t uvctx; + int http_status = 500; +#endif async_download_context( std::string u, std::string f, std::string p, FileRequestAsync* o - ) : url{ std::move(u) }, file{ std::move(f) }, param{ std::move(p) }, obj{ o->weak_from_this() }, count{} {} + ) : url{ std::move(u) }, file{ std::move(f) }, param{ std::move(p) }, obj{ o->weak_from_this() }, count{} +#ifndef EMSCRIPTEN + , uvctx{} +#endif + {} }; void download_success_retry(unsigned, void* userData, const char*) { @@ -110,7 +120,7 @@ namespace { if (flag1) Output::Warning("DL Failure: max retries exceeded: {}", download_path); else if (flag2) - Output::Warning("DL Failure: file not available: {}", download_path); + Output::Warning("DL Failure: file not available: {} ({})", download_path, status); if (sobj) sobj->DownloadDone(false); @@ -121,7 +131,9 @@ namespace { start_async_wget_with_retry(ctx); } + void start_async_wget_with_retry(async_download_context* ctx) { +#ifdef EMSCRIPTEN emscripten_async_wget2( ctx->url.data(), ctx->file.data(), @@ -132,8 +144,39 @@ namespace { download_failure_retry, nullptr ); - } +#else + uv_queue_work(uv_default_loop(), &ctx->uvctx, + [](uv_work_t *task) { + auto ctx = EP_CONTAINER_OF(task, async_download_context, uvctx); + auto sobj = ctx->obj.lock(); + if (sobj) { + std::string url_(ctx->url); + std::string path_(sobj->GetPath()); + if (!sobj->GetRequestExtension().empty()) { + url_ += sobj->GetRequestExtension(); + path_ += sobj->GetRequestExtension(); + } + std::ofstream out(path_, std::ios::binary); + + auto resp = cpr::Download(out, cpr::Url{url_}); + out.close(); + ctx->http_status = resp.status_code; + } + }, + [](uv_work_t *task, int status) { + auto ctx = EP_CONTAINER_OF(task, async_download_context, uvctx); + if (status) { + Output::Debug("Task cancelled ({})", status); + return download_failure_retry(0, ctx, 500); + } + if (ctx->http_status >= 200 && ctx->http_status < 300) + download_success_retry(0, ctx, ""); + else + download_failure_retry(0, ctx, ctx->http_status); + }); +#endif + } void async_wget_with_retry( std::string url, std::string file, @@ -144,8 +187,6 @@ namespace { auto ctx = new async_download_context{ url, file, param, obj }; start_async_wget_with_retry(ctx); } - -#endif } void AsyncHandler::CreateRequestMapping(const std::string& file) { @@ -305,7 +346,8 @@ FileRequestAsync::FileRequestAsync(std::string path, std::string directory, std: file(std::move(file)), path(std::move(path)), state(State_WaitForStart) -{ } +{ +} void FileRequestAsync::SetGraphicFile(bool graphic) { this->graphic = graphic; @@ -341,19 +383,31 @@ void FileRequestAsync::Start() { state = State_Pending; -#ifdef __EMSCRIPTEN__ +#if defined(__EMSCRIPTEN__) || defined(PLAYER_YNO) std::string request_path; # ifdef EM_GAME_URL request_path = EM_GAME_URL; # else - request_path = "games/"; + if (parent_scope) + request_path = "https://ynoproject.net/"; + else + request_path = "https://ynoproject.net/data/"; # endif - if (!Player::emscripten_game_name.empty()) { - request_path += Player::emscripten_game_name + "/"; - } else { - request_path += "default/"; +#ifndef EMSCRIPTEN + // TODO correct this + if (!parent_scope) { + DownloadDone(true); + return; } +#endif + + //if (!Player::emscripten_game_name.empty()) { + // request_path += Player::emscripten_game_name + "/"; + //} else { + // TODO: get the current game + request_path += "2kki/"; + //} std::string modified_path; if (index_version >= 2) { @@ -379,13 +433,18 @@ void FileRequestAsync::Start() { } } + auto it = file_mapping.find(modified_path); if (it != file_mapping.end()) { request_path += it->second; } else { if (file_mapping.empty()) { // index.json not fetched yet, fallthrough and fetch - request_path += path; + std::string path_(this->path); + size_t offset; + if (parent_scope && (offset = path_.find("../")) != std::string::npos) + path_ = path_.substr(offset + 3); + request_path += path_; } else { // Fire immediately (error) Output::Debug("{} not in index.json", modified_path); @@ -399,7 +458,7 @@ void FileRequestAsync::Start() { request_path = Utils::ReplaceAll(request_path, "#", "%23"); request_path = Utils::ReplaceAll(request_path, "+", "%2B"); - auto request_file = (it != file_mapping.end() ? it->second : path); + std::string request_file = (it != file_mapping.end() ? it->second : path); async_wget_with_retry(request_path, std::move(request_file), "", this); #else # ifdef EM_GAME_URL diff --git a/src/async_handler.h b/src/async_handler.h index 45ed19842..a24fc6391 100644 --- a/src/async_handler.h +++ b/src/async_handler.h @@ -160,6 +160,14 @@ class FileRequestAsync : public std::enable_shared_from_this { */ void SetGraphicFile(bool graphic); + /** Denotes that this file is meant to download into a folder outside the game. */ + void SetParentScope(bool parent_scope); + + /** Sets the extension of the file used when downloading only. */ + void SetRequestExtension(std::string_view ext); + + const std::string_view GetRequestExtension() const noexcept; + /** * Starts the async requests. * When the request was already started earlier and is pending this call @@ -222,6 +230,8 @@ class FileRequestAsync : public std::enable_shared_from_this { int state = State_DoneFailure; bool important = false; bool graphic = false; + bool parent_scope = false; + std::string request_ext = ""; }; /** @@ -256,6 +266,18 @@ inline void FileRequestAsync::SetImportantFile(bool important) { this->important = important; } +inline void FileRequestAsync::SetParentScope(bool parent_scope) { + this->parent_scope = parent_scope; +} + +inline void FileRequestAsync::SetRequestExtension(std::string_view ext) { + request_ext = ext; +} + +inline const std::string_view FileRequestAsync::GetRequestExtension() const noexcept { + return request_ext; +} + inline bool FileRequestAsync::IsGraphicFile() const { return graphic; } diff --git a/src/bitmap.cpp b/src/bitmap.cpp index 8a203f6a1..fcaaa1624 100644 --- a/src/bitmap.cpp +++ b/src/bitmap.cpp @@ -1129,6 +1129,10 @@ void Bitmap::Flip(bool horizontal, bool vertical) { } void Bitmap::MaskedBlit(Rect const& dst_rect, Bitmap const& mask, int mx, int my, Color const& color) { + return MaskedBlit(dst_rect, mask, mx, my, color, 1., 1.); +} + +void Bitmap::MaskedBlit(Rect const& dst_rect, Bitmap const& mask, int mx, int my, Color const& color, double dx, double dy) { pixman_color_t tcolor = { static_cast(color.red << 8), static_cast(color.green << 8), @@ -1137,21 +1141,41 @@ void Bitmap::MaskedBlit(Rect const& dst_rect, Bitmap const& mask, int mx, int my auto source = PixmanImagePtr{ pixman_image_create_solid_fill(&tcolor) }; + Transform xform = Transform::Scale(dx, dy); + if (dx != 1 || dy != 1) { + pixman_image_set_transform(source.get(), &xform.matrix); + pixman_image_set_transform(mask.bitmap.get(), &xform.matrix); + } + pixman_image_composite32(PIXMAN_OP_OVER, source.get(), mask.bitmap.get(), bitmap.get(), 0, 0, mx, my, dst_rect.x, dst_rect.y, dst_rect.width, dst_rect.height); + pixman_image_set_transform(mask.bitmap.get(), nullptr); } void Bitmap::MaskedBlit(Rect const& dst_rect, Bitmap const& mask, int mx, int my, Bitmap const& src, int sx, int sy) { + return MaskedBlit(dst_rect, mask, mx, my, src, sx, sy, 1., 1.); +} + +void Bitmap::MaskedBlit(Rect const& dst_rect, Bitmap const& mask, int mx, int my, Bitmap const& src, int sx, int sy, double dx, double dy) { + Transform xform = Transform::Scale(dx, dy); + if (dx != 1 || dy != 1) { + sx = ceilf(sx / dx); + sy = ceilf(sy / dy); + pixman_image_set_transform(src.bitmap.get(), &xform.matrix); + pixman_image_set_transform(mask.bitmap.get(), &xform.matrix); + } pixman_image_composite32(PIXMAN_OP_OVER, src.bitmap.get(), mask.bitmap.get(), bitmap.get(), sx, sy, mx, my, dst_rect.x, dst_rect.y, dst_rect.width, dst_rect.height); + pixman_image_set_transform(src.bitmap.get(), nullptr); + pixman_image_set_transform(mask.bitmap.get(), nullptr); } void Bitmap::Blit2x(Rect const& dst_rect, Bitmap const& src, Rect const& src_rect) { diff --git a/src/bitmap.h b/src/bitmap.h index 2060b274a..221750cf1 100644 --- a/src/bitmap.h +++ b/src/bitmap.h @@ -537,6 +537,8 @@ class Bitmap { * @param sy source y position */ void MaskedBlit(Rect const& dst_rect, Bitmap const& mask, int mx, int my, Bitmap const& src, int sx, int sy); + /** dx and dy specify the reciprocal of the transform scale */ + void MaskedBlit(Rect const& dst_rect, Bitmap const& mask, int mx, int my, Bitmap const& src, int sx, int sy, double dx, double dy); /** * Blits constant color to this one through a mask bitmap. @@ -548,6 +550,8 @@ class Bitmap { * @param color source color. */ void MaskedBlit(Rect const& dst_rect, Bitmap const& mask, int mx, int my, Color const& color); + /** dx and dy specify the reciprocal of the transform scale */ + void MaskedBlit(Rect const& dst_rect, Bitmap const& mask, int mx, int my, Color const& color, double dx, double dy); /** * Blits source bitmap scaled 2:1, with no transparency. diff --git a/src/cache.cpp b/src/cache.cpp index cd9e426c6..a5017701e 100644 --- a/src/cache.cpp +++ b/src/cache.cpp @@ -160,6 +160,9 @@ namespace { Battlecharset, Battleweapon, Frame, + // online-specific + Emoji, + Badge, END }; @@ -199,6 +202,8 @@ namespace { { "BattleCharSet", DrawCheckerboard, true, 144, 144, 384, 384, true, false }, { "BattleWeapon", DrawCheckerboard, true, 192, 192, 512, 512, true, false }, { "Frame", DrawCheckerboard, true, 320, 320, 240, 240, true, true }, + { "../images/ynomoji", DrawCheckerboard, true, 128, 128, 128, 128, false, true }, + { "../images/badge", DrawCheckerboard, true, 37, 37, 37, 37, false, true }, }; template @@ -415,6 +420,14 @@ BitmapRef Cache::System(std::string_view file, bool bg_preserve_transparent_colo return LoadBitmap(file, flags); } +BitmapRef Cache::Emoji(StringView file) { + return LoadBitmap(file); +} + +BitmapRef Cache::Badge(StringView file) { + return LoadBitmap(file); +} + BitmapRef Cache::Exfont() { const auto key = MakeHashKey("ExFont", "ExFont", false); diff --git a/src/cache.h b/src/cache.h index 4b8882243..0e4ce5e56 100644 --- a/src/cache.h +++ b/src/cache.h @@ -55,7 +55,10 @@ namespace Cache { BitmapRef System(std::string_view filename, bool bg_preserve_transparent_color = false); BitmapRef System2(std::string_view filename); - BitmapRef Tile(std::string_view filename, int tile_id); + BitmapRef Emoji(StringView filename); + BitmapRef Badge(StringView filename); + + BitmapRef Tile(StringView filename, int tile_id); BitmapRef SpriteEffect(const BitmapRef& src_bitmap, const Rect& rect, bool flip_x, bool flip_y, const Tone& tone, const Color& blend); void Clear(); diff --git a/src/directory_tree.cpp b/src/directory_tree.cpp index 3e42bfd16..82417fda1 100644 --- a/src/directory_tree.cpp +++ b/src/directory_tree.cpp @@ -30,8 +30,9 @@ static void DebugLog(const char* fmt, Args&&... args) { Output::Debug(fmt, std::forward(args)...); } #else -template -static void DebugLog(const char*, Args&&...) {} +//template +//static void DebugLog(const char*, Args&&...) {} +#define DebugLog(...) #endif namespace { diff --git a/src/filefinder.cpp b/src/filefinder.cpp index 97e51e5aa..8b749275d 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -182,11 +182,15 @@ std::string FileFinder::MakeCanonical(std::string_view path, int initial_deepnes if (path_comp == "..") { if (path_can.size() > 0) { path_can.pop_back(); - } else if (initial_deepness > 0) { + } + else if (initial_deepness > 0) { // Ignore, we are in root --initial_deepness; + } else if (initial_deepness == 0) { + // not -1, intented to go outside the game folder + path_can.push_back(path_comp); } else { - Output::Warning("Path traversal out of game directory: {}", path); + Output::Warning("Path traversal out of game directory"); } } else if (path_comp.empty() || path_comp == ".") { // ignore @@ -495,7 +499,8 @@ Filesystem_Stream::InputStream open_generic_with_fallback(std::string_view dir, } Filesystem_Stream::InputStream FileFinder::OpenImage(std::string_view dir, std::string_view name) { - DirectoryTree::Args args = { MakePath(dir, name), IMG_TYPES, 1, false }; + int initial_depth = dir.find("../") == 0 ? 0 : 1; + DirectoryTree::Args args = { MakePath(dir, name), IMG_TYPES, initial_depth, false }; return open_generic_with_fallback(dir, name, args); } diff --git a/src/font.cpp b/src/font.cpp index 69c213ce6..d0e081290 100644 --- a/src/font.cpp +++ b/src/font.cpp @@ -58,6 +58,10 @@ #include "game_clock.h" #include "multiplayer/game_multiplayer.h" +#include "graphics.h" +#ifdef PLAYER_YNO +# include "multiplayer/chat_overlay.h" +#endif // Static variables. namespace { @@ -202,6 +206,7 @@ namespace { FontRef default_mincho; FontRef name_text; FontRef name_text_2; + FontRef chat_text; struct ExFont final : public Font { public: @@ -698,6 +703,22 @@ void Font::SetNameText(FontRef new_name_text, bool slim) { } } +FontRef Font::ChatText() { + if (chat_text) + return chat_text; + if (name_text) + return name_text; + return Default(); +} + +void Font::SetChatText(FontRef new_chat_text) { + chat_text = new_chat_text; + chat_text->SetFallbackFont(DefaultBitmapFont()); +#ifdef PLAYER_YNO + Graphics::GetChatOverlay().OnResolutionChange(); +#endif +} + FontRef Font::CreateFtFont(Filesystem_Stream::InputStream is, int size, bool bold, bool italic) { #ifdef HAVE_FREETYPE if (!is) { @@ -791,6 +812,7 @@ Rect Font::GetSize(char32_t glyph) const { Rect size = vGetSize(glyph); size.width += current_style.letter_spacing; + size.width = ceilf(size.width * (current_style.size / 12.)); size.height = current_style.size; return size; @@ -800,6 +822,7 @@ Rect Font::GetSize(const ShapeRet& shape_ret) const { int width = shape_ret.advance.x + current_style.letter_spacing; int height = current_style.size; return {0, 0, width, height}; + return {0, 0, (int)ceilf(width * (current_style.size / 12.)), height}; } Point Font::Render(Bitmap& dest, int const x, int const y, const Bitmap& sys, int color, char32_t glyph) const { @@ -829,7 +852,9 @@ Point Font::Render(Bitmap& dest, int const x, int const y, const Bitmap& sys, in return {}; } - Point advance = { shape.advance.x + current_style.letter_spacing, shape.advance.y }; + double zoom = current_style.size / 12.; + + Point advance = { (int)ceilf((shape.advance.x + current_style.letter_spacing) * zoom), shape.advance.y }; return advance; } @@ -846,6 +871,11 @@ bool Font::RenderImpl(Bitmap& dest, int const x, int const y, const Bitmap& sys, // Drawing position of the glyph rect.x += gret.offset.x; rect.y -= gret.offset.y; + double zoom = 12. / current_style.size; + if (zoom != 1) { + rect.width = ceilf(rect.width / zoom); + rect.height = ceilf(rect.height / zoom); + } unsigned src_x = 0; unsigned src_y = 0; @@ -885,14 +915,15 @@ bool Font::RenderImpl(Bitmap& dest, int const x, int const y, const Bitmap& sys, if (!gret.has_color) { if (current_style.draw_gradient) { - dest.MaskedBlit(rect, *gret.bitmap, 0, 0, *sys_large, src_x, src_y); + dest.MaskedBlit(rect, *gret.bitmap, 0, 0, *sys_large, src_x, src_y, zoom, zoom); } else { auto col = sys.GetColorAt(current_style.color_offset.x + src_x, current_style.color_offset.y + src_y); auto col_bm = Bitmap::Create(gret.bitmap->width(), gret.bitmap->height(), col); - dest.MaskedBlit(rect, *gret.bitmap, 0, 0, *col_bm, 0, 0); + dest.MaskedBlit(rect, *gret.bitmap, 0, 0, *col_bm, 0, 0, zoom, zoom); } } else { // Color glyphs, emojis etc. + // TODO: Handle emojis zoom for chat dest.Blit(rect.x, rect.y, *gret.bitmap, gret.bitmap->GetRect(), Opacity::Opaque()); } @@ -903,8 +934,9 @@ bool Font::RenderImpl(Bitmap& dest, int const x, int const y, const Bitmap& sys, if (color != ColorShadow) { // First draw the shadow, offset by one if (!gret.has_color && current_style.draw_shadow) { - auto shadow_rect = Rect(rect.x + 1, rect.y + 1, rect.width, rect.height); - dest.MaskedBlit(shadow_rect, *gret.bitmap, 0, 0, sys, 16, 32); + int ozoom = ceilf(1 / zoom); + auto shadow_rect = Rect(rect.x + ozoom, rect.y + ozoom, rect.width, rect.height); + dest.MaskedBlit(shadow_rect, *gret.bitmap, 0, 0, sys, 16, 32, zoom, zoom); } src_x = color % 10 * 16 + 2; @@ -924,11 +956,11 @@ bool Font::RenderImpl(Bitmap& dest, int const x, int const y, const Bitmap& sys, src_y -= glyph_height - 12; } - dest.MaskedBlit(rect, *gret.bitmap, 0, 0, sys, src_x, src_y); + dest.MaskedBlit(rect, *gret.bitmap, 0, 0, sys, src_x, src_y, zoom, zoom); } else { auto col = sys.GetColorAt(current_style.color_offset.x + src_x, current_style.color_offset.y + src_y); auto col_bm = Bitmap::Create(gret.bitmap->width(), gret.bitmap->height(), col); - dest.MaskedBlit(rect, *gret.bitmap, 0, 0, *col_bm, 0, 0); + dest.MaskedBlit(rect, *gret.bitmap, 0, 0, *col_bm, 0, 0, zoom, zoom); } } else { // Color glyphs, emojis etc. @@ -948,10 +980,14 @@ Point Font::Render(Bitmap& dest, int x, int y, Color const& color, char32_t glyp return {}; } - auto rect = Rect(x, y, gret.bitmap->width(), gret.bitmap->height()); - dest.MaskedBlit(rect, *gret.bitmap, 0, 0, color); + double zoom = 12. / current_style.size; + auto rect = Rect(x, y, + ceilf(gret.bitmap->width() / zoom), + ceilf(gret.bitmap->height() / zoom)); + dest.MaskedBlit(rect, *gret.bitmap, 0, 0, color, zoom, zoom); gret.advance.x += current_style.letter_spacing; + gret.advance.x = (int)ceilf((double)gret.advance.x / zoom); return gret.advance; } diff --git a/src/font.h b/src/font.h index 0e7a55518..7dcbbbbbd 100644 --- a/src/font.h +++ b/src/font.h @@ -235,12 +235,16 @@ class Font { static void SetDefault(FontRef new_default, bool use_mincho); static FontRef NameText(); static void SetNameText(FontRef new_name_text, bool slim); + static FontRef ChatText(); + static void SetChatText(FontRef new_chat_text); static void ResetDefault(); static void ResetNameText(); static void Dispose(); static FontRef exfont; + static FontRef chat_font; + enum SystemColor { ColorShadow = -1, ColorDefault = 0, diff --git a/src/game_message.cpp b/src/game_message.cpp index 0404cbd71..1681236ea 100644 --- a/src/game_message.cpp +++ b/src/game_message.cpp @@ -33,6 +33,10 @@ #include "output.h" #include "feature.h" +#ifdef PLAYER_YNO +# include "multiplayer/chat_overlay.h" +#endif + #include static Window_Message* window = nullptr; @@ -81,11 +85,11 @@ int Game_Message::GetRealPosition() { } } -int Game_Message::WordWrap(std::string_view line, const int limit, const WordWrapCallback& callback) { +int Game_Message::WordWrap(StringView line, const int limit, const Game_Message::WordWrapCallback& callback) { return WordWrap(line, limit, callback, *Font::Default()); } -int Game_Message::WordWrap(std::string_view line, const int limit, const WordWrapCallback& callback, const Font& font) { +int Game_Message::WordWrap(StringView line, const int limit, const Game_Message::WordWrapCallback& callback, const Font& font) { int start = 0; int line_count = 0; @@ -125,6 +129,78 @@ int Game_Message::WordWrap(std::string_view line, const int limit, const WordWra return line_count; } +int Game_Message::WordWrap(lcf::Span> spans, const int limit, const typename Game_Message::ComponentWrapCallback& callback, const Font& font) { + int line_count = 0; + int line_width = 0; + + std::vector> line_spans; + auto flush = [&] { + if (line_spans.empty()) return; + callback({ line_spans.data(), line_spans.size() }); + line_count++; + line_width = 0; + line_spans.clear(); + }; + for (auto span = spans.begin(); span != spans.end(); ++span) { + if (auto fragment = span->get()->Downcast()) { + auto& line = fragment->string; + + int start = 0; + do { + int next = start; + int last_width = 0; + bool must_break = false; + do { + int found = line.find(' ', next); + if (found == std::string::npos) + found = line.size(); + + auto wrapped = line.substr(start, found - start); + auto width = Text::GetSize(font, wrapped).width; + if (line_width + width > limit) { + must_break = true; + if (next == start) { + next = found + 1; + } + break; + } + last_width = width; + next = found + 1; + } while (next < line.size()); + + line_width += last_width; + if (last_width == 0 || must_break) { + flush(); + } + + if (start == next - 1) { + start = next; + continue; + } + + auto wrapped = line.substr(start, (next - 1) - start); + line_spans.emplace_back(std::make_shared(wrapped)); + if (must_break) + flush(); + + start = next; + } while (start < line.size()); + } + else if (auto box = span->get()->Downcast()) { + int width = box->x; + if (line_width + width > limit) { + // next line por favor + flush(); + } + line_spans.emplace_back(*span); + line_width += width; + } + } + flush(); + + return line_count; +} + AsyncOp Game_Message::Update() { if (window) { window->Update(); diff --git a/src/game_message.h b/src/game_message.h index f30fc235a..3ea92df12 100644 --- a/src/game_message.h +++ b/src/game_message.h @@ -27,6 +27,17 @@ #include "pending_message.h" #include "memory_management.h" +#ifdef PLAYER_YNO +# include +# include +# include +# include +# include "point.h" +class ChatComponent; +class TextSpan; +# include "multiplayer/chat_overlay.h" +#endif + class Window_Message; class AsyncOp; @@ -83,6 +94,11 @@ namespace Game_Message { int WordWrap(std::string_view line, int limit, const WordWrapCallback& callback); int WordWrap(std::string_view line, int limit, const WordWrapCallback& callback, const Font& font); +#ifdef PLAYER_YNO + using ComponentWrapCallback = const std::function> spans)>; + int WordWrap(lcf::Span> spans, const int limit, const typename ComponentWrapCallback& callback, const Font& font); +#endif + /** * Return if it's legal to show a new message box. * diff --git a/src/input_buttons.h b/src/input_buttons.h index a7290d7b3..ae3d9cd2d 100644 --- a/src/input_buttons.h +++ b/src/input_buttons.h @@ -86,6 +86,8 @@ namespace Input { TOGGLE_FULLSCREEN, TOGGLE_ZOOM, SHOW_CHAT, + CHAT_SCROLL_DOWN, + CHAT_SCROLL_UP, BUTTON_COUNT }; @@ -132,7 +134,9 @@ namespace Input { "FAST_FORWARD_B", "TOGGLE_FULLSCREEN", "TOGGLE_ZOOM", - "SHOW_CHAT"); + "SHOW_CHAT", + "CHAT_SCROLL_DOWN", + "CHAT_SCROLL_UP"); static_assert(kInputButtonNames.size() == static_cast(BUTTON_COUNT)); constexpr auto kInputButtonHelp = lcf::makeEnumTags( @@ -178,7 +182,9 @@ namespace Input { "Run the game at x{} speed", "Toggle Fullscreen mode", "Toggle Window Zoom level", - "Toggle chat display"); + "Toggle chat display", + "Scroll chat window down", + "Scroll chat window up"); static_assert(kInputButtonHelp.size() == static_cast(BUTTON_COUNT)); /** diff --git a/src/input_buttons_desktop.cpp b/src/input_buttons_desktop.cpp index dceeb13b2..b1618f567 100644 --- a/src/input_buttons_desktop.cpp +++ b/src/input_buttons_desktop.cpp @@ -102,6 +102,12 @@ Input::ButtonMappingArray Input::GetDefaultButtonMappings() { {FAST_FORWARD_A, Keys::F}, {FAST_FORWARD_B, Keys::G}, {SHOW_CHAT, Keys::TAB}, + {CHAT_SCROLL_UP, Keys::UP}, + {CHAT_SCROLL_UP, Keys::JOY_DPAD_UP}, + {CHAT_SCROLL_UP, Keys::JOY_LSTICK_UP}, + {CHAT_SCROLL_DOWN, Keys::DOWN}, + {CHAT_SCROLL_DOWN, Keys::JOY_DPAD_DOWN}, + {CHAT_SCROLL_DOWN, Keys::JOY_LSTICK_DOWN}, #if defined(USE_MOUSE) && defined(SUPPORT_MOUSE) {MOUSE_LEFT, Keys::MOUSE_LEFT}, diff --git a/src/multiplayer/chat_overlay.cpp b/src/multiplayer/chat_overlay.cpp index eaed163f4..446cbe472 100644 --- a/src/multiplayer/chat_overlay.cpp +++ b/src/multiplayer/chat_overlay.cpp @@ -15,15 +15,25 @@ * along with EasyRPG Player. If not, see . */ #include +#include +#include #include "chat_overlay.h" #include "drawable_mgr.h" #include "player.h" #include "baseui.h" -#include "game_message.h" #include "output.h" #include "input_buttons.h" #include "cache.h" +#include "game_message.h" +#include "scene.h" +#include "multiplayer/scene_overlay.h" + +namespace { + int ChatTextHeight() { + return Font::ChatText()->GetCurrentStyle().size; + } +} ChatOverlay::ChatOverlay() : Drawable(Priority_Overlay, Drawable::Flags::Global | Drawable::Flags::Screenspace) { @@ -32,6 +42,8 @@ ChatOverlay::ChatOverlay() : Drawable(Priority_Overlay, Drawable::Flags::Global void ChatOverlay::Draw(Bitmap& dst) { if (!IsAnyMessageVisible() && !show_all) return; + // We don't wanna paint over settings. + if (Scene::instance && Scene::instance->type == Scene::SceneType::Settings) return; if (!dirty) { dst.Blit(ox, oy, *bitmap, bitmap->GetRect(), 255); @@ -40,73 +52,138 @@ void ChatOverlay::Draw(Bitmap& dst) { bitmap->Clear(); - int y_end = DisplayUi->GetScreenSurfaceRect().height; - + Rect screen_rect = DisplayUi->GetScreenSurfaceRect(); + int y_end = screen_rect.height; + + auto& font = *Font::ChatText(); + const int text_height = font.GetCurrentStyle().size; int i = 0; - // we're doing the inverse of MessageOverlay: draw from the bottom up - int text_height = 12; // Font::NameText()->vGetSize('T').height; int half_screen = y_end / 2 / text_height; bool unlocked_start = scroll < half_screen; bool unlocked_end = scroll > messages.size() - half_screen; int last_sticky = std::max(0, (int)messages.size() - half_screen * 2); // we want half a screen before first sticky and half a screen after the last sticky. - int viewport = std::clamp(scroll - half_screen, 0, std::min(scroll + half_screen, last_sticky)); + int viewport = show_all + ? std::clamp(scroll - half_screen, 0, std::min(scroll + half_screen, last_sticky)) + : 0; - std::vector lines; + std::vector>> lines; int lidx = 0; const auto& system = *Cache::SystemOrBlack(); constexpr Color offwhite = Color(200, 200, 200, 255); + // draw input + int input_row_offset = 0; + if (show_all) { + input_row_offset = 1; + int offset = 2; + int y = y_end - (lidx + 1) * text_height; + bitmap->Blit(0, y, *black, black->GetRect(), 200); + int prompt_width = Text::Draw(*bitmap, offset, y, font, offwhite, ">").x; + offset += prompt_width; + if (!input.empty()) + offset += Text::Draw(*bitmap, offset, y, font, offwhite, input).x; + // cursor + bitmap->FillRect({ offset, y + text_height - 3, prompt_width, 3 }, offwhite); + } + + // we're doing the inverse of MessageOverlay: draw from the bottom up for (auto message = messages.rbegin() + viewport; message != messages.rend(); ++message) { if (!message->hidden || show_all) { auto& bg = !show_all ? black : unlocked_start ? (i == scroll ? grey : black) : unlocked_end ? (last_sticky + i == scroll ? grey : black) : i == half_screen ? grey : black; + bool highlight = bg == grey; - // figure out how many lines we need to print - std::string temp = fmt::format("[{}] {}", message->sender, message->text); bool first = true; Game_Message::WordWrap( - temp, + message->text, bitmap->width(), - [&](StringView line) { - if (first) { + [&](lcf::Span> line) { + if (first) { first = false; - lines.emplace_back(line.substr(1 + message->sender.size() + 2)); - } else - lines.emplace_back(line); - }, *Font::NameText()); + // we added the username as a virtual span, so now we remove it. + line = { line.begin() + 1, line.end() }; + } + if (!line.empty()) { + auto& nextline = lines.emplace_back(); + for (auto& span : line) { + if (auto str = span->Downcast()) + nextline.emplace_back(std::make_shared(str->string)); + else + nextline.emplace_back(span); + } + } + }, font); + for (auto line = lines.rbegin(); line != lines.rend(); ++line) { // semitransparent bg - int y = y_end - (lidx + 1) * text_height; - bitmap->Blit(0, y, *bg, bg->GetRect(), 200); + int y = y_end - (lidx + 1 + input_row_offset) * text_height; + bitmap->Blit(0, y, *bg, bg->GetRect(), show_all ? highlight ? 240 : 200 : 150); int offset = 2; if (std::next(line) == lines.rend()) { // draw the username - offset += Text::Draw(*bitmap, offset, y, *Font::NameText(), offwhite, "[").x; - offset += Text::Draw(*bitmap, offset, y, *Font::NameText(), *message->system, 0, message->sender).x; - offset += Text::Draw(*bitmap, offset, y, *Font::NameText(), offwhite, "] ").x; + offset += Text::Draw(*bitmap, offset, y, font, offwhite, "[").x; + offset += Text::Draw(*bitmap, offset, y, font, *message->system, 0, message->sender).x; + if (message->badge) { + // TODO: Handle badge sizes <36 + bitmap->Blit(offset, y, *message->badge, {0, 0, text_height, text_height}, 255); + offset += text_height; + } + offset += Text::Draw(*bitmap, offset, y, font, offwhite, "] ").x; + } + for (auto& span : *line) { + //auto& component = static_cast(span); + if (auto str = span->Downcast()) { + offset += Text::Draw(*bitmap, offset, y, font, offwhite, str->string).x; + } + else if (auto emoji = span->Downcast()) { + auto dims = *static_cast(emoji); + if (emoji->bitmap) { + double zoom = emoji->bitmap->GetRect().y / (double)text_height; + bitmap->StretchBlit({offset, y, text_height, text_height}, *emoji->bitmap, emoji->bitmap->GetRect(), 255); + } + else { + // the emoji is still live, request it now + emoji->RequestBitmap(this); + bitmap->FillRect({ offset, y, dims.x, text_height }, offwhite); + } + offset += dims.x; + } + else if (auto box = span->Downcast()) { + int width = box->x; + bitmap->FillRect({ offset, y, width, text_height }, offwhite); + offset += width; + } } - Text::Draw(*bitmap, offset, y, *Font::NameText(), offwhite, *line); ++lidx; - if (lidx * text_height > y_end) break; + if ((lidx + input_row_offset) * text_height > y_end) break; } ++i; lines.clear(); } if (!show_all && i > message_max_minimized) break; - if (lidx * text_height > y_end) break; + if ((lidx + input_row_offset) * text_height > y_end) break; + } + + if (show_all && scrollbar) { + double scrollbar_height = screen_rect.height / (double)text_height / messages.size() * screen_rect.height; + Rect dims{ 0, 0, (int)ceilf(0.008 * screen_rect.width), (int)ceilf(scrollbar_height) }; + //how far should the scrollbar go + double scrollbar_extent = scroll / (double)message_max; + int desired_y = floorf(y_end * (1 - scrollbar_extent)) - dims.height; + bitmap->BlitFast(bitmap->width() - dims.width, std::clamp(desired_y, 0, y_end - dims.height), *scrollbar, dims, 255); } dst.Blit(ox, oy, *bitmap, bitmap->GetRect(), 255); dirty = false; } -void ChatOverlay::AddMessage(std::string_view message, std::string_view sender, std::string_view system) { +void ChatOverlay::AddMessage(std::string_view message, std::string_view sender, std::string_view system, std::string_view badge) { if (message.empty()) { return; } @@ -114,12 +191,15 @@ void ChatOverlay::AddMessage(std::string_view message, std::string_view sender, auto sysref = system.empty() ? Cache::SystemOrBlack() : Cache::System(system.data()); - messages.emplace_back(std::string(message), std::string(sender), sysref); + messages.emplace_back(this, std::string(message), std::string(sender), sysref, std::string(badge)); while (messages.size() > message_max) { messages.pop_front(); } + if (scroll && (scroll + 1 < messages.size())) + scroll += 1; + dirty = true; } @@ -132,29 +212,7 @@ void ChatOverlay::Update() { OnResolutionChange(); } - - if (show_all) { - static int delta; - if (Input::IsTriggered(Input::InputButton::SCROLL_DOWN) || - Input::IsTriggered(Input::InputButton::PAGE_DOWN)) { - delta = 5; - DoScroll(-1); - } - else if (Input::IsRepeated(Input::InputButton::PAGE_DOWN)) { - DoScroll(std::clamp(--delta / 5, -4, -1)); - } - if (Input::IsTriggered(Input::InputButton::SCROLL_UP) || - Input::IsTriggered(Input::InputButton::PAGE_UP)) { - delta = -5; - DoScroll(1); - } - else if (Input::IsRepeated(Input::InputButton::PAGE_UP)) { - DoScroll(std::clamp(++delta / 5, 1, 4)); - } - } - - - if (IsAnyMessageVisible()) { + if (!show_all && IsAnyMessageVisible()) { if (++counter > 450) { counter = 0; for (auto& message : messages) { @@ -165,14 +223,100 @@ void ChatOverlay::Update() { } } +void ChatOverlay::UpdateScene() { + if (!show_all) { + Scene::Pop(); + return; + } + static int delta; + if ( + Input::IsTriggered(Input::InputButton::CHAT_SCROLL_DOWN)) { + delta = 5; + DoScroll(-1); + } + else if (Input::IsRepeated(Input::InputButton::DOWN)) { + DoScroll(std::clamp(--delta / 4, -4, -1)); + } + + if (Input::IsPressed(Input::InputButton::SCROLL_UP) || + Input::IsTriggered(Input::InputButton::CHAT_SCROLL_UP)) { + delta = -5; + DoScroll(1); + } + else if (Input::IsRepeated(Input::InputButton::UP)) { + DoScroll(std::clamp(++delta / 4, 1, 4)); + } + + if (Input::IsPressed(Input::InputButton::SCROLL_DOWN)) { + DoScroll(-3); + } + else if (Input::IsPressed(Input::InputButton::SCROLL_UP)) { + DoScroll(3); + } + + constexpr int offset = U'A' - Input::Keys::A; + constexpr int uppercase = U'a' - U'A'; + bool shift = Input::IsRawKeyPressed(Input::Keys::LSHIFT) || Input::IsRawKeyPressed(Input::Keys::RSHIFT); + static std::map symbols{ + {Input::Keys::N0, '0'}, + {Input::Keys::N1, '1'}, + {Input::Keys::N2, '2'}, + {Input::Keys::N3, '3'}, + {Input::Keys::N4, '4'}, + {Input::Keys::N5, '5'}, + {Input::Keys::N6, '6'}, + {Input::Keys::N7, '7'}, + {Input::Keys::N8, '8'}, + {Input::Keys::N9, '9'}, + {Input::Keys::SPACE, ' '}, + //{Input::Keys::SEMICOLON, ';'}, + }; + for (int letter = Input::Keys::A; letter <= Input::Keys::Z; ++letter) { + if (Input::IsRawKeyTriggered((Input::Keys::InputKey)letter)) { + input.push_back(letter + offset + (shift ? 0 : uppercase)); + dirty = true; + } + } + if (Input::IsRawKeyTriggered(Input::Keys::SEMICOLON)) { + input.push_back(shift ? ':' : ';'); + dirty = true; + } + for (auto& [key, chara] : symbols) { + if (Input::IsRawKeyTriggered(key)) { + input.push_back(chara); + dirty = true; + } + } + if (Input::IsRawKeyPressed(Input::Keys::BACKSPACE) && !input.empty()) { + input.pop_back(); + dirty = true; + } + if (Input::IsRawKeyTriggered(Input::Keys::RETURN)) { + AddMessage(input, "YNO"); + input.clear(); + dirty = true; + } +} + void ChatOverlay::SetShowAll(bool show_all) { + if (this->show_all == show_all) return; this->show_all = show_all; + counter = 0; dirty = true; + + if (show_all) { + bool in_overlay = Scene::instance && Scene::instance->type == Scene::SceneType::ChatOverlay; + if (in_overlay) + ((Scene_Overlay*)Scene::instance.get())->SetOnUpdate([this] { UpdateScene(); }); + else + Scene::Push(std::make_shared([this] { UpdateScene(); })); + } } void ChatOverlay::DoScroll(int increase) { scroll += increase; scroll = (scroll + messages.size()) % messages.size(); + counter = 0; dirty = true; } @@ -181,12 +325,13 @@ void ChatOverlay::OnResolutionChange() { DrawableMgr::Register(this); } - int text_height = 12; // Font::NameText()->vGetSize('T').height; + int text_height = ChatTextHeight(); Rect rect = DisplayUi->GetScreenSurfaceRect(); int width = rect.width; black = Bitmap::Create(width, text_height, Color(0, 0, 0, 255)); - grey = Bitmap::Create(width, text_height, Color(100, 100, 100, 255)); - bitmap = Bitmap::Create(width, text_height * message_max, true); + grey = Bitmap::Create(width, text_height, Color(80, 80, 80, 255)); + bitmap = Bitmap::Create(width, rect.height, true); + scrollbar = Bitmap::Create((int)ceilf(0.008 * width), rect.height, Color(255, 255, 255, 255)); dirty = true; } @@ -194,3 +339,99 @@ bool ChatOverlay::IsAnyMessageVisible() const { return std::any_of(messages.cbegin(), messages.cend(), [](const ChatOverlayMessage& m) { return !m.hidden; }); } +std::vector> ChatOverlayMessage::Convert(ChatOverlay* parent, bool has_badge) const { + StringView msg(text_orig); + std::vector> out; + static std::regex pattern(":(\\w+):"); + + out.emplace_back(std::make_shared( + Text::GetSize(*Font::ChatText(), fmt::format("[{}] ", sender)).width + (has_badge ? ChatTextHeight() : 0), + ChatTextHeight())); + + auto begin = std::cregex_iterator(msg.begin(), msg.end(), pattern); + auto end = decltype(begin){}; + + auto last_end = msg.begin(); + for (auto& it = begin; it != end; ++it) { + auto& match = *it; + + StringView between(last_end, match[0].first - last_end); + if (!between.empty()) { + out.emplace_back(std::make_shared(between)); + } + + //TODO: parse ynomojis + out.emplace_back(std::make_shared(parent, ChatTextHeight(), ChatTextHeight(), match[1].str())); + last_end = match[0].second; + } + + StringView last(last_end, msg.end() - last_end); + if (!last.empty()) { + out.emplace_back(std::make_shared(last)); + } + + return out; +} + +ChatOverlayMessage::ChatOverlayMessage(ChatOverlay* parent, std::string text, std::string sender, BitmapRef system, std::string badge) : + parent(parent), text_orig(std::move(text)), sender(std::move(sender)), system(system) +{ + bool has_badge = badge != "null" && !badge.empty(); + this->text = Convert(parent, has_badge); + + if (has_badge) { + auto req = AsyncHandler::RequestFile("../images/badge", badge); + req->SetGraphicFile(true); + req->SetParentScope(true); + req->SetRequestExtension(".png"); + badge_request = req->Bind(&ChatOverlayMessage::OnBadgeReady, this); + req->Start(); + } +} + +void ChatOverlayMessage::OnBadgeReady(FileRequestResult* result) { + if (!result->success) return; + badge = Cache::Badge(result->file); + parent->MarkDirty(); +} + +ChatEmoji::ChatEmoji(ChatOverlay* parent_, int width, int height, std::string emoji_) : + ChatComponent(width, height, ChatComponents::Emoji), emoji(std::move(emoji_)), parent(parent) +{ + assert(!emoji.empty() && "Cannot create an empty emoji component"); + assert(parent); + + RequestBitmap(parent_); +} + +void ChatEmoji::RequestBitmap(ChatOverlay* parent_) { + parent = parent_; + auto req = AsyncHandler::RequestFile("../images/ynomoji", emoji); + req->SetGraphicFile(true); + req->SetParentScope(true); + req->SetRequestExtension(".png"); + request = req->Bind([wself=weak_from_this()](FileRequestResult* result) { + if (!result->success) return; + if (auto sobj = wself.lock()) { + auto* self = static_cast(sobj.get()); + self->bitmap = Cache::Emoji(result->file); + self->parent->MarkDirty(); + } + else Output::Debug("expired: {}", result->file); + }); + req->Start(); +} + +ChatScreenshot::ChatScreenshot(ChatOverlay* parent, std::string id_, bool temp, bool spoiler) : + parent(parent), id(std::move(id_)), temp(temp), spoiler(spoiler) +{ +} + +int ChatEmoji::Draw(Bitmap& dest) { + // nothing yet + return ChatTextHeight(); +} + +int ChatString::Draw(Bitmap& dest) { + return 0; +} diff --git a/src/multiplayer/chat_overlay.h b/src/multiplayer/chat_overlay.h index 809c39719..155fb5033 100644 --- a/src/multiplayer/chat_overlay.h +++ b/src/multiplayer/chat_overlay.h @@ -20,19 +20,56 @@ #include #include +#include #include "drawable.h" #include "bitmap.h" #include "memory_management.h" +#include "async_handler.h" + +enum ChatComponents { + Box, + String, + Emoji, + END +}; + +template +struct ChatComponentsMap { using type = void; }; + +class ChatOverlay; + +class ChatComponent : public Point, public std::enable_shared_from_this { +protected: + const ChatComponents runtime_type; + ChatComponent(ChatComponents type = ChatComponents::Box) noexcept : + Point(0, 0), runtime_type(type) {} +public: + // fields for children classes + + ChatComponent(int width, int height, ChatComponents type = ChatComponents::Box) noexcept : + Point(width, height), runtime_type(type) {} + virtual int Draw(Bitmap& dest) { return x; } + template + typename ChatComponentsMap::type* Downcast(); +}; -//using ChatOverlayMessage = std::string; class ChatOverlayMessage { public: - ChatOverlayMessage(std::string text, std::string sender, BitmapRef system); - std::string text; + ChatOverlayMessage(ChatOverlay* parent, std::string text, std::string sender, BitmapRef system, std::string badge); + std::string text_orig; + std::vector> text; std::string sender; BitmapRef system; + BitmapRef badge; bool hidden = false; + ChatOverlay* parent; + + std::vector> Convert(ChatOverlay* parent, bool has_badge) const; +private: + FileRequestBinding badge_request; + + void OnBadgeReady(FileRequestResult*); }; class ChatOverlay : public Drawable { @@ -40,12 +77,14 @@ class ChatOverlay : public Drawable { ChatOverlay(); void Draw(Bitmap& dst) override; void Update(); - void AddMessage(std::string_view msg, std::string_view sender, std::string_view system = ""); + void UpdateScene(); + void AddMessage(std::string_view msg, std::string_view sender, std::string_view system = "", std::string_view badge = ""); void OnResolutionChange() override; void SetShowAll(bool show_all); inline void SetShowAll() { SetShowAll(!show_all); } inline bool ShowingAll() const noexcept { return show_all; } void DoScroll(int increase); + void MarkDirty() noexcept; private: bool IsAnyMessageVisible() const; @@ -54,20 +93,65 @@ class ChatOverlay : public Drawable { int ox = 0; int oy = 0; - int message_max = 100; + int message_max = 200; int message_max_minimized = 4; int counter = 0; int scroll = 0; - BitmapRef bitmap, black, grey; + BitmapRef bitmap, black, grey, scrollbar; + std::string input; std::deque messages; }; +inline void ChatOverlay::MarkDirty() noexcept { dirty = true; } + +class ChatString : public ChatComponent { +public: + StringView string; + + ChatString(StringView other) : ChatComponent(0, 0, ChatComponents::String), string(other) {} + int Draw(Bitmap& dst) override; +}; + +class ChatEmoji : public ChatComponent { +public: + std::string emoji; + BitmapRef bitmap = nullptr; + ChatOverlay* parent = nullptr; + FileRequestBinding request = nullptr; + + ChatEmoji(ChatOverlay* parent, int width, int height, std::string emoji); + int Draw(Bitmap& dest) override; + void RequestBitmap(ChatOverlay* parent); +}; + +class ChatScreenshot : public ChatComponent { +public: + bool spoiler; + bool temp; + std::string id; + BitmapRef bitmap = nullptr; + FileRequestBinding request = nullptr; + ChatOverlay* parent = nullptr; + + ChatScreenshot(ChatOverlay* parent, std::string id, bool temp, bool spoiler); +}; + + +template<> +struct ChatComponentsMap { using type = ChatComponent; }; +template<> +struct ChatComponentsMap { using type = ChatString; }; +template<> +struct ChatComponentsMap { using type = ChatEmoji; }; -inline ChatOverlayMessage::ChatOverlayMessage(std::string text, std::string sender, BitmapRef system) : - text(std::move(text)), sender(std::move(sender)), system(system) -{ +template +inline typename ChatComponentsMap::type* ChatComponent::Downcast() { + if (runtime_type == Wanted || Wanted == ChatComponents::Box) { + return static_cast::type*>(this); + } + return nullptr; } #endif diff --git a/src/multiplayer/game_multiplayer.cpp b/src/multiplayer/game_multiplayer.cpp index 89e762889..74f86a97a 100644 --- a/src/multiplayer/game_multiplayer.cpp +++ b/src/multiplayer/game_multiplayer.cpp @@ -73,7 +73,7 @@ void Game_Multiplayer::SpawnOtherPlayer(int id) { auto scene_map = Scene::Find(Scene::SceneType::Map); if (!scene_map) { - Output::Error("unexpected, {}:{}", __FILE__, __LINE__); + Output::Debug("unexpected, {}:{}", __FILE__, __LINE__); return; } auto old_list = &DrawableMgr::GetLocalList(); @@ -166,6 +166,12 @@ void Game_Multiplayer::InitConnection() { session_connected = true; SendBasicData(); Web_API::SyncPlayerData(p.uuid, p.rank, p.account_bin, p.badge, p.medals); +#ifdef PLAYER_YNO + auto& player = playerdata[p.uuid]; + player.rank = p.rank; + player.badge = p.badge; + player.account = p.account_bin == 1; +#endif }); connection.RegisterHandler("ri", [this] (RoomInfoPacket& p) { if (p.room_id != room_id) { @@ -247,6 +253,12 @@ void Game_Multiplayer::InitConnection() { players[p.id].account = p.account_bin == 1; Web_API::SyncPlayerData(p.uuid, p.rank, p.account_bin, p.badge, p.medals, p.id); +#ifdef PLAYER_YNO + auto& player = playerdata[p.uuid]; + player.rank = p.rank; + player.badge = p.badge; + player.account = p.account_bin == 1; +#endif }); connection.RegisterHandler("d", [this] (DisconnectPacket& p) { auto it = players.find(p.id); @@ -457,8 +469,8 @@ void Game_Multiplayer::InitConnection() { auto& player = players[p.id]; auto scene_map = Scene::Find(Scene::SceneType::Map); if (!scene_map) { - Output::Error("unexpected, {}:{}", __FILE__, __LINE__); - //return; + Output::Debug("unexpected, {}:{}", __FILE__, __LINE__); + return; } auto old_list = &DrawableMgr::GetLocalList(); DrawableMgr::SetLocalList(&scene_map->GetDrawableList()); @@ -466,6 +478,8 @@ void Game_Multiplayer::InitConnection() { DrawableMgr::SetLocalList(old_list); Web_API::OnPlayerNameUpdated(p.name, p.id); +#ifdef PLAYER_YNO +#endif }); #ifndef EMSCRIPTEN sessionConn.RegisterSystemHandler(YSM::OPEN, [this](MCo&) { @@ -477,7 +491,7 @@ void Game_Multiplayer::InitConnection() { // sync chat messages if (on_chat_msg) { - std::string endpoint = fmt::format("https://connect.ynoproject.net/2kki/api/chathistory?globalMsgLimit={}&partyMsgLimit={}&lastMessageId={}", 100, 100, lastmsgid); + std::string endpoint = fmt::format("https://connect.ynoproject.net/2kki/api/chathistory?globalMsgLimit={}&partyMsgLimit={}&lastMessageId={}", 200, 100, lastmsgid); cpr::Response resp = cpr::Get(cpr::Url{endpoint}); if (resp.status_code >= 200 && resp.status_code < 300) { json chatmsgs = json::parse(resp.text); @@ -495,13 +509,16 @@ void Game_Multiplayer::InitConnection() { for (auto it = messages.begin(); it != messages.end(); ++it) { std::string name = "Unknown Player"; std::string system; + std::string badge; if (auto player = playerdata.find((std::string)(*it)["uuid"]); player != playerdata.end()) { name = player->second.name; system = player->second.systemName; + badge = player->second.badge; } - on_chat_msg( - (std::string)(*it)["contents"], name, system, - it != std::prev(messages.end())); + on_chat_msg({ + (std::string)(*it)["contents"], name, system, badge, + std::next(it) != messages.end(), + }); } } } @@ -509,17 +526,44 @@ void Game_Multiplayer::InitConnection() { sessionConn.RegisterSystemHandler(YSM::CLOSE, [this](MCo&) { }); sessionConn.RegisterHandler("gsay", [this](SessionGSay& p) { - Output::Debug("{}: {}", p.uuid, p.msg); std::string name = "Unknown Player"; std::string system; + std::string badge; + if (auto player = playerdata.find(p.uuid); player != playerdata.end()) { + if (!player->second.name.empty()) + name = player->second.name; + system = player->second.systemName; + badge = player->second.badge; + } + Output::Debug("{}: {}", name, p.msg); + if (on_chat_msg) { + on_chat_msg({ p.msg, name, system, badge }); + } + }); + sessionConn.RegisterHandler("say", [this](SessionSay& p) { + std::string name = "Unknown Player"; + std::string system; + std::string badge; if (auto player = playerdata.find(p.uuid); player != playerdata.end()) { - name = player->second.name; + if (!player->second.name.empty()) + name = player->second.name; system = player->second.systemName; + badge = player->second.badge; } + Output::Debug("{}: {}", name, p.msg); if (on_chat_msg) { - on_chat_msg(p.msg, name, system, false); + on_chat_msg({ p.msg, name, system, badge }); } }); + sessionConn.RegisterHandler("p", [this](SessionPlayerInfo& p) { + auto& player = playerdata[p.uuid]; + Output::Debug("p: {}", p.name); + player.name = p.name; + player.systemName = p.systemName; + player.rank = p.rank; + player.account = p.account; + player.badge = p.badge; + }); #endif } diff --git a/src/multiplayer/game_multiplayer.h b/src/multiplayer/game_multiplayer.h index a99f9a1d9..e578d355e 100644 --- a/src/multiplayer/game_multiplayer.h +++ b/src/multiplayer/game_multiplayer.h @@ -57,7 +57,7 @@ class Game_Multiplayer { } settings; YNOConnection connection; -#ifndef EMSCRIPTEN +#ifdef PLAYER_YNO YNOConnection sessionConn; struct PlayerData { public: @@ -67,7 +67,15 @@ class Game_Multiplayer { bool account; std::string badge; }; - std::function on_chat_msg; + struct ChatMsg { + public: + std::string_view content; + std::string_view sender; + std::string_view system; + std::string_view badge; + bool syncing = false; + }; + std::function on_chat_msg; std::function on_system_graphic_change; std::string lastmsgid; std::map playerdata; diff --git a/src/multiplayer/messages.h b/src/multiplayer/messages.h index 8c88b86d1..be40aa1df 100644 --- a/src/multiplayer/messages.h +++ b/src/multiplayer/messages.h @@ -380,7 +380,7 @@ namespace S2C { public: BadgeUpdatePacket(const PL& v) {} }; -#ifndef EMSCRIPTEN +#ifdef PLAYER_YNO class SessionGSay : public S2CPacket { public: SessionGSay(const PL& v) @@ -388,8 +388,8 @@ namespace S2C { 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))), + x(Decode(v.at(4))), + y(Decode(v.at(5))), msg(v.at(6)), msgid(v.at(7)) {} int x, y; @@ -400,6 +400,32 @@ namespace S2C { std::string msg; std::string msgid; }; + class SessionSay : public S2CPacket { + public: + SessionSay(const PL& v) + : uuid(v.at(0)), + msg(v.at(1)) {} + std::string uuid; + std::string msg; + }; + class SessionPlayerInfo : public S2CPacket { + public: + SessionPlayerInfo(const PL& v) : + uuid(v.at(0)), + name(v.at(1)), + systemName(v.at(2)), + rank(Decode(v.at(3))), + account(Decode(v.at(4))), + badge(v.at(5)) + {} + int rank; + bool account; + std::string uuid; + std::string name; + std::string systemName; + std::string badge; + // TODO: Medals? + }; #endif } namespace C2S { diff --git a/src/multiplayer/scene_overlay.cpp b/src/multiplayer/scene_overlay.cpp new file mode 100644 index 000000000..61f1dd4a5 --- /dev/null +++ b/src/multiplayer/scene_overlay.cpp @@ -0,0 +1,10 @@ +#include "scene_overlay.h" + +Scene_Overlay::Scene_Overlay(UpdateFn fn) : OnUpdate(fn) { + type = SceneType::ChatOverlay; +} + +void Scene_Overlay::vUpdate() { + if (OnUpdate) + OnUpdate(); +} diff --git a/src/multiplayer/scene_overlay.h b/src/multiplayer/scene_overlay.h new file mode 100644 index 000000000..0659ddfc0 --- /dev/null +++ b/src/multiplayer/scene_overlay.h @@ -0,0 +1,41 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef EP_MP_SCENE_OVERLAY_H +#define EP_MP_SCENE_OVERLAY_H + +#include + +#include "scene.h" + +/** A black hole for all input, intended to allow overlays to work. */ +class Scene_Overlay : public Scene { +public: + using UpdateFn = std::function; + Scene_Overlay(UpdateFn on_update = nullptr); + + UpdateFn OnUpdate; + + void vUpdate() override; + void SetOnUpdate(UpdateFn on_update); +}; + +inline void Scene_Overlay::SetOnUpdate(UpdateFn on_update) { + OnUpdate = on_update; +} + +#endif diff --git a/src/platform/sdl/sdl2_ui.cpp b/src/platform/sdl/sdl2_ui.cpp index e1319ca44..d9bd39253 100644 --- a/src/platform/sdl/sdl2_ui.cpp +++ b/src/platform/sdl/sdl2_ui.cpp @@ -685,7 +685,7 @@ void Sdl2Ui::UpdateDisplay() { } // TODO: Remove the division by 2 to render at actual size - screen_surface = Bitmap::Create(viewport.w / 2, viewport.h / 2, true, main_surface->pitch()); + screen_surface = Bitmap::Create(viewport.w, viewport.h, true, main_surface->pitch()); if (vcfg.scaling_mode.Get() == ConfigEnum::ScalingMode::Bilinear && window.scale > 0.f) { diff --git a/src/platform/ynoshell/main.cpp b/src/platform/ynoshell/main.cpp index d15b6a4cb..17cee4562 100644 --- a/src/platform/ynoshell/main.cpp +++ b/src/platform/ynoshell/main.cpp @@ -60,7 +60,7 @@ class YnoFrame : public wxFrame YnoFrame(); YnoGameContainer* gameWindow; - wxTextCtrl* chatInput; + //wxTextCtrl* chatInput; private: void OnKeyDownFrame(wxKeyEvent& event); @@ -86,6 +86,8 @@ bool YnoApp::OnInit() std::vector args{ (char**)app.argv, (char**)app.argv + app.argc }; Player::Init(std::move(args)); Player::Run(); + + return true; } YnoFrame::YnoFrame() @@ -97,8 +99,8 @@ YnoFrame::YnoFrame() gameWindow->Bind(wxEVT_KEY_DOWN, &YnoFrame::OnKeyDownFrame, this); sizer->Add((wxWindow*)gameWindow, 1, wxEXPAND); - GMI().on_chat_msg = [](std::string_view msg, std::string_view sender, std::string_view system, bool) { - Graphics::GetChatOverlay().AddMessage(msg, sender, system); + GMI().on_chat_msg = [](Game_Multiplayer::ChatMsg msg) { + Graphics::GetChatOverlay().AddMessage(msg.content, msg.sender, msg.system, msg.badge); }; SetSizer(sizer); diff --git a/src/player.cpp b/src/player.cpp index eae72e435..a24d294e6 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -1170,6 +1170,11 @@ void Player::LoadFonts() { if (name_text) { Font::SetNameText(Font::CreateFtFont(std::move(name_text), 11, false, false), false); } + + auto chat_text = FileFinder::OpenFont("ChatText"); + if (chat_text) { + Font::SetChatText(Font::CreateFtFont(std::move(chat_text), 36, false, false)); + } auto name_text_2 = FileFinder::OpenFont("NameText2"); if (name_text_2) { diff --git a/src/scene.h b/src/scene.h index 51d077b43..ff2e7288c 100644 --- a/src/scene.h +++ b/src/scene.h @@ -60,6 +60,8 @@ class Scene { Teleport, Settings, LanguageMenu, + // Online play only + ChatOverlay, SceneMax }; diff --git a/src/text.cpp b/src/text.cpp index 4fdc5d441..eb241ceeb 100644 --- a/src/text.cpp +++ b/src/text.cpp @@ -189,6 +189,8 @@ Rect Text::GetSize(const Font& font, std::string_view text) { auto iter = text.data(); const auto end = iter + text.size(); + double zoom = font.GetCurrentStyle().size / 12.; + if (font.CanShape()) { std::u32string text32; while (iter != end) { @@ -213,7 +215,7 @@ Rect Text::GetSize(const Font& font, std::string_view text) { for (const auto& ch: shape_ret) { Rect size = font.GetSize(ch); - rect.width += ch.offset.x + size.width; + rect.width += ceilf((ch.offset.x + size.width) * zoom); rect.height = std::max(rect.height, size.height); } } @@ -232,7 +234,7 @@ Rect Text::GetSize(const Font& font, std::string_view text) { for (const auto& ch: shape_ret) { Rect size = font.GetSize(ch); - rect.width += ch.offset.x + size.width; + rect.width += ceilf((ch.offset.x + size.width) * zoom); rect.height = std::max(rect.height, size.height); } } diff --git a/src/window_settings.cpp b/src/window_settings.cpp index a888ce4cd..ea1385947 100644 --- a/src/window_settings.cpp +++ b/src/window_settings.cpp @@ -671,7 +671,7 @@ void Window_Settings::RefreshButtonList() { Input::SHOW_LOG }; break; case 3: - buttons = { Input::SHOW_CHAT }; + buttons = { Input::SHOW_CHAT, Input::CHAT_SCROLL_UP, Input::CHAT_SCROLL_DOWN }; } for (auto b: buttons) { From ebdad91c42071b91845517e1467f7cc7212d1e2e Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Thu, 26 Dec 2024 00:02:41 -0500 Subject: [PATCH 09/18] Login screen, headless mode, websockets stability --- CMakeLists.txt | 13 +- src/async_handler.cpp | 62 ++++--- src/font.cpp | 23 ++- src/font.h | 2 + src/game_config.cpp | 12 ++ src/game_config.h | 22 +++ src/game_message.cpp | 10 +- src/multiplayer/chat_overlay.cpp | 236 +++++++++++++++++++-------- src/multiplayer/chat_overlay.h | 65 ++++++-- src/multiplayer/chatname.h | 3 +- src/multiplayer/connection.h | 1 + src/multiplayer/game_multiplayer.cpp | 190 ++++++++++++++++++--- src/multiplayer/game_multiplayer.h | 44 ++++- src/multiplayer/messages.h | 10 +- src/multiplayer/playerother.h | 3 + src/multiplayer/scene_online.cpp | 63 +++++++ src/multiplayer/scene_online.h | 45 +++++ src/multiplayer/yno_connection.cpp | 155 +++++++++++++----- src/multiplayer/yno_connection.h | 7 +- src/platform.cpp | 23 +++ src/platform.h | 3 + src/platform/ynoshell/main.cpp | 9 +- src/player.cpp | 7 +- src/player.h | 3 +- src/scene_logo.cpp | 2 - src/scene_menu.cpp | 19 +++ src/scene_menu.h | 1 + src/scene_settings.cpp | 52 +++++- src/scene_settings.h | 3 + src/text.cpp | 8 +- src/window_input_settings.h | 1 + src/window_settings.cpp | 79 ++++++++- src/window_settings.h | 9 +- src/window_stringinput.cpp | 71 ++++++++ src/window_stringinput.h | 32 ++++ 35 files changed, 1086 insertions(+), 202 deletions(-) create mode 100644 src/multiplayer/scene_online.cpp create mode 100644 src/multiplayer/scene_online.h create mode 100644 src/window_stringinput.cpp create mode 100644 src/window_stringinput.h diff --git a/CMakeLists.txt b/CMakeLists.txt index bc6f7ff71..b60e64f70 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -515,6 +515,10 @@ target_sources(${PROJECT_NAME} PRIVATE src/multiplayer/playerother.h src/multiplayer/playerother.cpp src/external/TinySHA1.hpp + + # to be upstreamed + src/window_stringinput.h + src/window_stringinput.cpp ) if(NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") @@ -523,7 +527,12 @@ if(NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") src/multiplayer/chat_overlay.cpp src/multiplayer/scene_overlay.h src/multiplayer/scene_overlay.cpp + src/multiplayer/scene_online.h + src/multiplayer/scene_online.cpp ) + if(WIN32) + target_link_libraries(${PROJECT_NAME} synchronization) # provides WaitOnAddress + endif() endif() include(CMakeDependentOption) @@ -1298,8 +1307,8 @@ if(PLAYER_BUILD_EXECUTABLE AND ${PLAYER_TARGET_PLATFORM} MATCHES "^SDL.*$" AND N if(WIN32) # Open console for Debug builds - set_target_properties(${EXE_NAME} PROPERTIES WIN32_EXECUTABLE $<$>:TRUE>) - #set_target_properties(${EXE_NAME} PROPERTIES WIN32_EXECUTABLE FALSE) + #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}) diff --git a/src/async_handler.cpp b/src/async_handler.cpp index ce69d4f87..25b9773b9 100644 --- a/src/async_handler.cpp +++ b/src/async_handler.cpp @@ -20,16 +20,21 @@ #include #include +#include +using json = nlohmann::json; + #ifdef __EMSCRIPTEN__ # include # include -# include - using json = nlohmann::json; #elif defined(PLAYER_YNO) # include # include # include +# include "platform.h" # define EP_CONTAINER_OF(ptr, type, member) (type*)((char*)ptr - offsetof(type, member)) +# if defined(_WIN32) +# define timegm _mkgmtime +# endif #endif #include "async_handler.h" @@ -52,6 +57,7 @@ namespace { std::unordered_map file_mapping; int next_id = 0; int index_version = 1; + int64_t db_lastwrite = LLONG_MAX; FileRequestAsync* GetRequest(const std::string& path) { auto it = async_requests.find(path); @@ -151,17 +157,35 @@ namespace { auto sobj = ctx->obj.lock(); if (sobj) { std::string url_(ctx->url); - std::string path_(sobj->GetPath()); + std::string path_(ctx->file); + if (path_.empty()) + path_ = sobj->GetPath(); if (!sobj->GetRequestExtension().empty()) { url_ += sobj->GetRequestExtension(); path_ += sobj->GetRequestExtension(); } - std::ofstream out(path_, std::ios::binary); - auto resp = cpr::Download(out, cpr::Url{url_}); - out.close(); + auto resp = cpr::Download(out, cpr::Url{ url_ }); ctx->http_status = resp.status_code; + if (resp.status_code == 0) { + ctx->http_status = 1000 + (int)resp.error.code; + Output::Debug("failed {}: {}", resp.error.message); + } + if (ctx->file == "RPG_RT.ldb") { + // use this file to determine cache freshness + if (auto lm = resp.header.find("last-modified"); lm != resp.header.end()) { + std::tm time{}; + std::istringstream ss(lm->second); + ss >> std::get_time(&time, "%a, %d %b %Y %H:%M:%S"); + if (ss.fail()) + Output::Debug("could not parse Last-Modified: {}", lm->second); + else { + Output::Debug("ldb last-modified: {}", lm->second); + db_lastwrite = timegm(&time); + } + } + } } }, [](uv_work_t *task, int status) { @@ -190,7 +214,6 @@ namespace { } void AsyncHandler::CreateRequestMapping(const std::string& file) { -#ifdef __EMSCRIPTEN__ auto f = FileFinder::Game().OpenInputStream(file); if (!f) { Output::Error("Emscripten: Reading index.json failed"); @@ -257,10 +280,6 @@ void AsyncHandler::CreateRequestMapping(const std::string& file) { } } } -#else - // no-op - (void)file; -#endif } void AsyncHandler::ClearRequests() { @@ -394,14 +413,6 @@ void FileRequestAsync::Start() { request_path = "https://ynoproject.net/data/"; # endif -#ifndef EMSCRIPTEN - // TODO correct this - if (!parent_scope) { - DownloadDone(true); - return; - } -#endif - //if (!Player::emscripten_game_name.empty()) { // request_path += Player::emscripten_game_name + "/"; //} else { @@ -438,7 +449,7 @@ void FileRequestAsync::Start() { if (it != file_mapping.end()) { request_path += it->second; } else { - if (file_mapping.empty()) { + if (file_mapping.empty() || parent_scope) { // index.json not fetched yet, fallthrough and fetch std::string path_(this->path); size_t offset; @@ -457,8 +468,19 @@ void FileRequestAsync::Start() { request_path = Utils::ReplaceAll(request_path, "%", "%25"); request_path = Utils::ReplaceAll(request_path, "#", "%23"); request_path = Utils::ReplaceAll(request_path, "+", "%2B"); + request_path = Utils::ReplaceAll(request_path, " ", "%20"); std::string request_file = (it != file_mapping.end() ? it->second : path); + +#ifndef EMSCRIPTEN + auto handle = Platform::File{std::string(request_file)}; + auto lastmodified = handle.GetLastModified(); + if (lastmodified >= db_lastwrite) { + DownloadDone(true); + return; + } +#endif + async_wget_with_retry(request_path, std::move(request_file), "", this); #else # ifdef EM_GAME_URL diff --git a/src/font.cpp b/src/font.cpp index d0e081290..2d94ed8ca 100644 --- a/src/font.cpp +++ b/src/font.cpp @@ -421,6 +421,8 @@ Rect FTFont::vGetSize(char32_t glyph) const { FT_GlyphSlot slot = face->glyph; + //double zoom = current_style.size / (double)original_style.size; + Point advance; advance.x = Utils::RoundTo(slot->advance.x / 64.0); advance.y = Utils::RoundTo(slot->advance.y / 64.0); @@ -812,7 +814,7 @@ Rect Font::GetSize(char32_t glyph) const { Rect size = vGetSize(glyph); size.width += current_style.letter_spacing; - size.width = ceilf(size.width * (current_style.size / 12.)); + size.width = ceilf(size.width * (current_style.size / (double)original_style.size)); size.height = current_style.size; return size; @@ -821,8 +823,8 @@ Rect Font::GetSize(char32_t glyph) const { Rect Font::GetSize(const ShapeRet& shape_ret) const { int width = shape_ret.advance.x + current_style.letter_spacing; int height = current_style.size; - return {0, 0, width, height}; - return {0, 0, (int)ceilf(width * (current_style.size / 12.)), height}; + //return {0, 0, width, height}; + return {0, 0, (int)ceilf(width * (current_style.size / (double)original_style.size)), height}; } Point Font::Render(Bitmap& dest, int const x, int const y, const Bitmap& sys, int color, char32_t glyph) const { @@ -837,6 +839,7 @@ Point Font::Render(Bitmap& dest, int const x, int const y, const Bitmap& sys, in } gret.advance.x += current_style.letter_spacing; + gret.advance.x = ceilf(gret.advance.x * GetScaleRatio()); return gret.advance; } @@ -852,9 +855,10 @@ Point Font::Render(Bitmap& dest, int const x, int const y, const Bitmap& sys, in return {}; } - double zoom = current_style.size / 12.; + double zoom = current_style.size / (double)original_style.size; Point advance = { (int)ceilf((shape.advance.x + current_style.letter_spacing) * zoom), shape.advance.y }; + //Point advance = { shape.advance.x + current_style.letter_spacing, shape.advance.y }; return advance; } @@ -871,7 +875,7 @@ bool Font::RenderImpl(Bitmap& dest, int const x, int const y, const Bitmap& sys, // Drawing position of the glyph rect.x += gret.offset.x; rect.y -= gret.offset.y; - double zoom = 12. / current_style.size; + double zoom = original_style.size / (double)current_style.size; if (zoom != 1) { rect.width = ceilf(rect.width / zoom); rect.height = ceilf(rect.height / zoom); @@ -934,8 +938,9 @@ bool Font::RenderImpl(Bitmap& dest, int const x, int const y, const Bitmap& sys, if (color != ColorShadow) { // First draw the shadow, offset by one if (!gret.has_color && current_style.draw_shadow) { - int ozoom = ceilf(1 / zoom); + int ozoom = floorf(1 / zoom); auto shadow_rect = Rect(rect.x + ozoom, rect.y + ozoom, rect.width, rect.height); + //auto shadow_rect = Rect(rect.x, rect.y, rect.width, rect.height); dest.MaskedBlit(shadow_rect, *gret.bitmap, 0, 0, sys, 16, 32, zoom, zoom); } @@ -957,10 +962,12 @@ bool Font::RenderImpl(Bitmap& dest, int const x, int const y, const Bitmap& sys, } dest.MaskedBlit(rect, *gret.bitmap, 0, 0, sys, src_x, src_y, zoom, zoom); + //dest.MaskedBlit(rect, *gret.bitmap, 0, 0, sys, src_x, src_y); } else { auto col = sys.GetColorAt(current_style.color_offset.x + src_x, current_style.color_offset.y + src_y); auto col_bm = Bitmap::Create(gret.bitmap->width(), gret.bitmap->height(), col); dest.MaskedBlit(rect, *gret.bitmap, 0, 0, *col_bm, 0, 0, zoom, zoom); + //dest.MaskedBlit(rect, *gret.bitmap, 0, 0, *col_bm, 0, 0); } } else { // Color glyphs, emojis etc. @@ -980,10 +987,12 @@ Point Font::Render(Bitmap& dest, int x, int y, Color const& color, char32_t glyp return {}; } - double zoom = 12. / current_style.size; + double zoom = original_style.size / (double)current_style.size; auto rect = Rect(x, y, ceilf(gret.bitmap->width() / zoom), ceilf(gret.bitmap->height() / zoom)); + //dest.MaskedBlit(rect, *gret.bitmap, 0, 0, color, zoom, zoom); + //auto rect = Rect(x, y, gret.bitmap->width(), gret.bitmap->height()); dest.MaskedBlit(rect, *gret.bitmap, 0, 0, color, zoom, zoom); gret.advance.x += current_style.letter_spacing; diff --git a/src/font.h b/src/font.h index 7dcbbbbbd..486395eb6 100644 --- a/src/font.h +++ b/src/font.h @@ -27,6 +27,7 @@ #include "string_view.h" #include #include +#include class Color; class Rect; @@ -260,6 +261,7 @@ class Font { virtual bool vCanShape() const { return false; } virtual std::vector vShape(std::u32string_view) const { return {}; } virtual void vApplyStyle(const Style& style) { (void)style; }; + inline double GetScaleRatio() const { return current_style.size / (double)original_style.size; } protected: Font(std::string_view name, int size, bool bold, bool italic); diff --git a/src/game_config.cpp b/src/game_config.cpp index 77359272e..373622eb7 100644 --- a/src/game_config.cpp +++ b/src/game_config.cpp @@ -692,6 +692,11 @@ void Game_Config::LoadFromStream(Filesystem_Stream::InputStream& is) { player.automatic_screenshots.FromIni(ini); player.automatic_screenshots_interval.FromIni(ini); player.prefer_easyrpg_map_files.FromIni(ini); + + /** online */ + online.session_token.FromIni(ini); + online.username.FromIni(ini); + online.nametag_mode.FromIni(ini); } void Game_Config::WriteToStream(Filesystem_Stream::OutputStream& os) const { @@ -788,4 +793,11 @@ void Game_Config::WriteToStream(Filesystem_Stream::OutputStream& os) const { player.prefer_easyrpg_map_files.ToIni(os); os << "\n"; + + // online section + os << "[Online]\n"; + online.session_token.ToIni(os); + online.username.ToIni(os); + online.nametag_mode.ToIni(os); + os << "\n"; } diff --git a/src/game_config.h b/src/game_config.h index 0adc656f6..5962f1f88 100644 --- a/src/game_config.h +++ b/src/game_config.h @@ -74,6 +74,13 @@ namespace ConfigEnum { /** Always overlay */ Overlay }; + + enum class NametagMode { + NONE, + CLASSIC, + COMPACT, + SLIM + }; }; #ifdef HAVE_FLUIDLITE @@ -171,6 +178,19 @@ struct Game_ConfigInput { void Hide(); }; +struct Game_ConfigOnline { + StringConfigParam session_token{ "", "", "Online", "SessionToken" }; + StringConfigParam username{ "Username", "Your chat name or YNOproject account; max 15 characters", "Online", "Username" }; + // not persisted + StringConfigParam password{ "Password", "Your YNOproject password", "", "" }; + EnumConfigParam nametag_mode{ + "Nametags", "Change the nametag display style", "Online", "Nametags", /*default: */ConfigEnum::NametagMode::CLASSIC, + Utils::MakeSvArray("None", "Classic", "Compact", "Slim"), + Utils::MakeSvArray("none", "classic", "compact", "slim"), + Utils::MakeSvArray("Hide nametags", "Same as game text", "Smaller size", "Extra slim") + }; +}; + struct Game_Config { /** Gameplay subsystem options */ Game_ConfigPlayer player; @@ -184,6 +204,8 @@ struct Game_Config { /** Input subsystem options */ Game_ConfigInput input; + Game_ConfigOnline online; + /** * Create an application config. This first determines the config file path if any, * loads the config file, then loads command line arguments. diff --git a/src/game_message.cpp b/src/game_message.cpp index 1681236ea..8a25e421b 100644 --- a/src/game_message.cpp +++ b/src/game_message.cpp @@ -169,17 +169,17 @@ int Game_Message::WordWrap(lcf::Span> spans, cons } while (next < line.size()); line_width += last_width; - if (last_width == 0 || must_break) { - flush(); - } if (start == next - 1) { start = next; + if (must_break) { + flush(); + } continue; } auto wrapped = line.substr(start, (next - 1) - start); - line_spans.emplace_back(std::make_shared(wrapped)); + line_spans.emplace_back(std::make_shared(wrapped, fragment->color)); if (must_break) flush(); @@ -187,7 +187,7 @@ int Game_Message::WordWrap(lcf::Span> spans, cons } while (start < line.size()); } else if (auto box = span->get()->Downcast()) { - int width = box->x; + int width = box->GetSize().x; if (line_width + width > limit) { // next line por favor flush(); diff --git a/src/multiplayer/chat_overlay.cpp b/src/multiplayer/chat_overlay.cpp index 446cbe472..ac067357a 100644 --- a/src/multiplayer/chat_overlay.cpp +++ b/src/multiplayer/chat_overlay.cpp @@ -18,6 +18,12 @@ #include #include +#include +#include +#include +#include +using json = nlohmann::json; + #include "chat_overlay.h" #include "drawable_mgr.h" #include "player.h" @@ -27,17 +33,59 @@ #include "cache.h" #include "game_message.h" #include "scene.h" +#include "multiplayer/game_multiplayer.h" #include "multiplayer/scene_overlay.h" +#include "messages.h" namespace { + bool LargeScreen() { + return DisplayUi->GetScreenSurfaceRect().width >= 640; + } int ChatTextHeight() { - return Font::ChatText()->GetCurrentStyle().size; + return LargeScreen() ? 37 : 12; + } + Point ChatTextSquare() { + int text_height = ChatTextHeight(); + return { text_height, text_height }; + } + enum class FileExtensions : char { png, gif, webp }; + struct EmojiInfo { + public: + FileExtensions extensions = FileExtensions::png; + }; + std::map emojis; + + void InitializeEmojiMappings() { + if (!emojis.empty()) return; + + uv_queue_work(uv_default_loop(), new uv_work_t{}, + [](uv_work_t*) { + auto resp = cpr::Get(cpr::Url{"https://ynoproject.net/game/ynomoji.json"}); + if (!(resp.status_code >= 200 && resp.status_code < 300)) + Output::Error("no emojis??"); + json data = json::parse(resp.text, nullptr, false); + if (data.is_discarded()) + Output::Error("invalid ynomoji json"); + + for (const auto& [key, value] : data.items()) { + std::string path(value); + if (auto extoffset = path.rfind('.'); extoffset != std::string::npos) { + auto ext = path.substr(extoffset); + if (ext == ".png") { + auto& emoji = emojis[(std::string)key]; + emoji.extensions = FileExtensions::png; + } + //else Output::Debug("unimplemented emoji {}.{}", key, ext); + } + } + }, + [](uv_work_t* task, int) { delete task; }); } } ChatOverlay::ChatOverlay() : Drawable(Priority_Overlay, Drawable::Flags::Global | Drawable::Flags::Screenspace) { - + InitializeEmojiMappings(); } void ChatOverlay::Draw(Bitmap& dst) { @@ -56,7 +104,18 @@ void ChatOverlay::Draw(Bitmap& dst) { int y_end = screen_rect.height; auto& font = *Font::ChatText(); - const int text_height = font.GetCurrentStyle().size; + int text_height; + std::optional _guard; + if (LargeScreen()) { + Font::Style style{}; + style.italic = true; + style.size = 33; // ChatTextHeight(); + _guard = font.ApplyStyle(style); + text_height = 37; // font.GetCurrentStyle().size; + } + else { + text_height = 12; + } int i = 0; int half_screen = y_end / 2 / text_height; @@ -81,12 +140,18 @@ void ChatOverlay::Draw(Bitmap& dst) { int offset = 2; int y = y_end - (lidx + 1) * text_height; bitmap->Blit(0, y, *black, black->GetRect(), 200); - int prompt_width = Text::Draw(*bitmap, offset, y, font, offwhite, ">").x; - offset += prompt_width; - if (!input.empty()) - offset += Text::Draw(*bitmap, offset, y, font, offwhite, input).x; - // cursor - bitmap->FillRect({ offset, y + text_height - 3, prompt_width, 3 }, offwhite); + if (GMI().CanChat()) { + int prompt_width = Text::Draw(*bitmap, offset, y, font, offwhite, ">").x; + offset += prompt_width; + if (!input.empty()) + offset += Text::Draw(*bitmap, offset, y, font, offwhite, input).x; + // cursor + bitmap->FillRect({ offset, y + text_height - 3, prompt_width, 3 }, offwhite); + } + else { + Text::Draw(*bitmap, offset, y, font, Color(160, 160, 160, 255), + "Set a username in Online settings to join the chat."); + } } // we're doing the inverse of MessageOverlay: draw from the bottom up @@ -99,6 +164,7 @@ void ChatOverlay::Draw(Bitmap& dst) { bool highlight = bg == grey; bool first = true; + bool has_header = true; Game_Message::WordWrap( message->text, bitmap->width(), @@ -106,15 +172,15 @@ void ChatOverlay::Draw(Bitmap& dst) { if (first) { first = false; // we added the username as a virtual span, so now we remove it. - line = { line.begin() + 1, line.end() }; + if (line[0]->Downcast()) + line = { line.begin() + 1, line.end() }; + else + has_header = false; } if (!line.empty()) { auto& nextline = lines.emplace_back(); for (auto& span : line) { - if (auto str = span->Downcast()) - nextline.emplace_back(std::make_shared(str->string)); - else - nextline.emplace_back(span); + nextline.emplace_back(span); } } }, font); @@ -125,24 +191,24 @@ void ChatOverlay::Draw(Bitmap& dst) { bitmap->Blit(0, y, *bg, bg->GetRect(), show_all ? highlight ? 240 : 200 : 150); int offset = 2; - if (std::next(line) == lines.rend()) { + if (std::next(line) == lines.rend() && has_header) { // draw the username - offset += Text::Draw(*bitmap, offset, y, font, offwhite, "[").x; + offset += Text::Draw(*bitmap, offset, y, font, offwhite, message->account ? "[" : "<").x; offset += Text::Draw(*bitmap, offset, y, font, *message->system, 0, message->sender).x; if (message->badge) { - // TODO: Handle badge sizes <36 - bitmap->Blit(offset, y, *message->badge, {0, 0, text_height, text_height}, 255); + bitmap->StretchBlit({offset, y, text_height, text_height}, *message->badge, message->badge->GetRect(), 255); offset += text_height; } - offset += Text::Draw(*bitmap, offset, y, font, offwhite, "] ").x; + offset += Text::Draw(*bitmap, offset, y, font, offwhite, message->account ? "] " : "> ").x; } for (auto& span : *line) { //auto& component = static_cast(span); if (auto str = span->Downcast()) { - offset += Text::Draw(*bitmap, offset, y, font, offwhite, str->string).x; + offset += Text::Draw(*bitmap, offset, y, font, str->color, str->string).x; } else if (auto emoji = span->Downcast()) { - auto dims = *static_cast(emoji); + //auto dims = *static_cast(emoji); + Point dims = emoji->GetSize(); if (emoji->bitmap) { double zoom = emoji->bitmap->GetRect().y / (double)text_height; bitmap->StretchBlit({offset, y, text_height, text_height}, *emoji->bitmap, emoji->bitmap->GetRect(), 255); @@ -150,12 +216,12 @@ void ChatOverlay::Draw(Bitmap& dst) { else { // the emoji is still live, request it now emoji->RequestBitmap(this); - bitmap->FillRect({ offset, y, dims.x, text_height }, offwhite); + bitmap->FillRect({ offset, y, text_height, text_height }, offwhite); } - offset += dims.x; + offset += text_height; } else if (auto box = span->Downcast()) { - int width = box->x; + int width = box->GetSize().x; bitmap->FillRect({ offset, y, width, text_height }, offwhite); offset += width; } @@ -183,15 +249,13 @@ void ChatOverlay::Draw(Bitmap& dst) { dirty = false; } -void ChatOverlay::AddMessage(std::string_view message, std::string_view sender, std::string_view system, std::string_view badge) { - if (message.empty()) { - return; - } +ChatOverlayMessage& ChatOverlay::AddMessage( + std::string_view message, std::string_view sender, std::string_view system, std::string_view badge, bool account) { counter = 0; auto sysref = system.empty() ? Cache::SystemOrBlack() : Cache::System(system.data()); - messages.emplace_back(this, std::string(message), std::string(sender), sysref, std::string(badge)); + auto& ret = messages.emplace_back(this, std::string(message), std::string(sender), sysref, std::string(badge), account); while (messages.size() > message_max) { messages.pop_front(); @@ -201,6 +265,18 @@ void ChatOverlay::AddMessage(std::string_view message, std::string_view sender, scroll += 1; dirty = true; + return ret; +} + +void ChatOverlay::AddSystemMessage(StringView msg) { + auto& chatmsg = messages.emplace_back(this, std::string(msg), std::string(""), Cache::SystemOrBlack(), std::string(""), false); + chatmsg.text.erase(chatmsg.text.begin()); + for (auto& span : chatmsg.text) { + if (auto string = span->Downcast()) { + string->color = Color(230, 230, 180, 255); + break; + } + } } void ChatOverlay::Update() { @@ -229,31 +305,32 @@ void ChatOverlay::UpdateScene() { return; } static int delta; - if ( - Input::IsTriggered(Input::InputButton::CHAT_SCROLL_DOWN)) { + if (Input::IsTriggered(Input::InputButton::CHAT_SCROLL_DOWN)) { delta = 5; DoScroll(-1); } - else if (Input::IsRepeated(Input::InputButton::DOWN)) { + else if (Input::IsRepeated(Input::InputButton::CHAT_SCROLL_DOWN)) { DoScroll(std::clamp(--delta / 4, -4, -1)); } - if (Input::IsPressed(Input::InputButton::SCROLL_UP) || - Input::IsTriggered(Input::InputButton::CHAT_SCROLL_UP)) { + if (Input::IsTriggered(Input::InputButton::CHAT_SCROLL_UP)) { delta = -5; DoScroll(1); } - else if (Input::IsRepeated(Input::InputButton::UP)) { + else if (Input::IsRepeated(Input::InputButton::CHAT_SCROLL_UP)) { DoScroll(std::clamp(++delta / 4, 1, 4)); } - if (Input::IsPressed(Input::InputButton::SCROLL_DOWN)) { + if (Input::IsRawKeyPressed(Input::Keys::MOUSE_SCROLLDOWN)) { DoScroll(-3); } - else if (Input::IsPressed(Input::InputButton::SCROLL_UP)) { + else if (Input::IsRawKeyPressed(Input::Keys::MOUSE_SCROLLUP)) { DoScroll(3); } + if (!GMI().CanChat()) return; + // chat input only below this point + constexpr int offset = U'A' - Input::Keys::A; constexpr int uppercase = U'a' - U'A'; bool shift = Input::IsRawKeyPressed(Input::Keys::LSHIFT) || Input::IsRawKeyPressed(Input::Keys::RSHIFT); @@ -287,12 +364,12 @@ void ChatOverlay::UpdateScene() { dirty = true; } } - if (Input::IsRawKeyPressed(Input::Keys::BACKSPACE) && !input.empty()) { + if (IsTriggeredOrRepeating(CustomKeyTimers::backspace) && !input.empty()) { input.pop_back(); dirty = true; } - if (Input::IsRawKeyTriggered(Input::Keys::RETURN)) { - AddMessage(input, "YNO"); + if (Input::IsRawKeyTriggered(Input::Keys::RETURN) && !input.empty()) { + GMI().sessionConn.SendPacketAsync(input); input.clear(); dirty = true; } @@ -339,14 +416,45 @@ bool ChatOverlay::IsAnyMessageVisible() const { return std::any_of(messages.cbegin(), messages.cend(), [](const ChatOverlayMessage& m) { return !m.hidden; }); } +bool ChatOverlay::IsTriggeredOrRepeating(CustomKeyTimers key) { + assert(key < CustomKeyTimers::END && "invalid key"); + auto& timer = key_timers[static_cast(key)]; + auto now = Game_Clock::GetFrameTime(); + + if (Input::IsRawKeyTriggered(timer.raw_key)) { + timer.last_triggered = now; + return true; + } + + if (Input::IsRawKeyPressed(timer.raw_key)) { + auto held_duration = now - timer.last_triggered; + if (held_duration >= timer.delay && (now - timer.last_repeated) >= timer.rate) { + timer.last_repeated = now; + return true; + } + } + + return false; +} + std::vector> ChatOverlayMessage::Convert(ChatOverlay* parent, bool has_badge) const { StringView msg(text_orig); std::vector> out; static std::regex pattern(":(\\w+):"); - out.emplace_back(std::make_shared( - Text::GetSize(*Font::ChatText(), fmt::format("[{}] ", sender)).width + (has_badge ? ChatTextHeight() : 0), - ChatTextHeight())); + int text_height = ChatTextHeight(); + auto font = Font::ChatText(); + Font::Style style{}; + style.size = text_height; + auto _guard = font->ApplyStyle(style); + + out.emplace_back(std::make_shared([this, has_badge] { + auto& font = Font::ChatText(); + Rect rect = Text::GetSize(*font, fmt::format("[{}] ", sender)); + if (has_badge) + rect.width += ChatTextHeight(); + return Point{ rect.width, rect.height }; + }, ChatComponents::Header)); auto begin = std::cregex_iterator(msg.begin(), msg.end(), pattern); auto end = decltype(begin){}; @@ -360,8 +468,11 @@ std::vector> ChatOverlayMessage::Convert(ChatOver out.emplace_back(std::make_shared(between)); } - //TODO: parse ynomojis - out.emplace_back(std::make_shared(parent, ChatTextHeight(), ChatTextHeight(), match[1].str())); + auto emoji_key = match[1].str(); + if (emojis.empty() || emojis.find(emoji_key) != emojis.end()) + out.emplace_back(std::make_shared(parent, ChatTextHeight(), ChatTextHeight(), emoji_key)); + else + out.emplace_back(std::make_shared(match[0].str())); last_end = match[0].second; } @@ -373,10 +484,12 @@ std::vector> ChatOverlayMessage::Convert(ChatOver return out; } -ChatOverlayMessage::ChatOverlayMessage(ChatOverlay* parent, std::string text, std::string sender, BitmapRef system, std::string badge) : - parent(parent), text_orig(std::move(text)), sender(std::move(sender)), system(system) +ChatOverlayMessage::ChatOverlayMessage( + ChatOverlay* parent, std::string text, std::string sender, BitmapRef system, std::string badge, bool account) : + parent(parent), text_orig(std::move(text)), sender(std::move(sender)), system(system), account(account) { bool has_badge = badge != "null" && !badge.empty(); + this->text = Convert(parent, has_badge); if (has_badge) { @@ -396,28 +509,31 @@ void ChatOverlayMessage::OnBadgeReady(FileRequestResult* result) { } ChatEmoji::ChatEmoji(ChatOverlay* parent_, int width, int height, std::string emoji_) : - ChatComponent(width, height, ChatComponents::Emoji), emoji(std::move(emoji_)), parent(parent) + ChatComponent(width, height, ChatComponents::Emoji), emoji(std::move(emoji_)), parent(parent_) { assert(!emoji.empty() && "Cannot create an empty emoji component"); assert(parent); + sizer = ChatTextSquare; - RequestBitmap(parent_); + RequestBitmap(parent); } + void ChatEmoji::RequestBitmap(ChatOverlay* parent_) { + if (emoji.empty()) return; parent = parent_; auto req = AsyncHandler::RequestFile("../images/ynomoji", emoji); req->SetGraphicFile(true); req->SetParentScope(true); req->SetRequestExtension(".png"); - request = req->Bind([wself=weak_from_this()](FileRequestResult* result) { - if (!result->success) return; - if (auto sobj = wself.lock()) { - auto* self = static_cast(sobj.get()); - self->bitmap = Cache::Emoji(result->file); - self->parent->MarkDirty(); + request = req->Bind([this](FileRequestResult* result) { + if (!result->success) { + emoji.clear(); + Output::Debug("failed: {}", result->file); + return; } - else Output::Debug("expired: {}", result->file); + bitmap = Cache::Emoji(result->file); + parent->MarkDirty(); }); req->Start(); } @@ -427,11 +543,3 @@ ChatScreenshot::ChatScreenshot(ChatOverlay* parent, std::string id_, bool temp, { } -int ChatEmoji::Draw(Bitmap& dest) { - // nothing yet - return ChatTextHeight(); -} - -int ChatString::Draw(Bitmap& dest) { - return 0; -} diff --git a/src/multiplayer/chat_overlay.h b/src/multiplayer/chat_overlay.h index 155fb5033..d8cfcd059 100644 --- a/src/multiplayer/chat_overlay.h +++ b/src/multiplayer/chat_overlay.h @@ -21,16 +21,21 @@ #include #include #include +#include #include "drawable.h" #include "bitmap.h" #include "memory_management.h" #include "async_handler.h" +#include "game_clock.h" +#include "input.h" enum ChatComponents { Box, String, Emoji, + // no associated components + Header, END }; @@ -39,24 +44,34 @@ struct ChatComponentsMap { using type = void; }; class ChatOverlay; -class ChatComponent : public Point, public std::enable_shared_from_this { +class ChatComponent : public std::enable_shared_from_this { protected: const ChatComponents runtime_type; ChatComponent(ChatComponents type = ChatComponents::Box) noexcept : - Point(0, 0), runtime_type(type) {} + static_dim(0, 0), runtime_type(type) { } public: - // fields for children classes + const Point static_dim; + std::function sizer; ChatComponent(int width, int height, ChatComponents type = ChatComponents::Box) noexcept : - Point(width, height), runtime_type(type) {} - virtual int Draw(Bitmap& dest) { return x; } + static_dim(width, height), runtime_type(type) {} + template + ChatComponent(Sizer&& sizer, ChatComponents type = ChatComponents::Box) noexcept : + runtime_type(type), sizer(std::forward(sizer)), static_dim(0, 0) {} + virtual Point Draw(Bitmap& dest) { return static_dim; } template typename ChatComponentsMap::type* Downcast(); + Point GetSize() const; }; +inline Point ChatComponent::GetSize() const { + return sizer ? sizer() : static_dim; +} + class ChatOverlayMessage { public: - ChatOverlayMessage(ChatOverlay* parent, std::string text, std::string sender, BitmapRef system, std::string badge); + ChatOverlayMessage( + ChatOverlay* parent, std::string text, std::string sender, BitmapRef system, std::string badge, bool account); std::string text_orig; std::vector> text; std::string sender; @@ -64,27 +79,33 @@ class ChatOverlayMessage { BitmapRef badge; bool hidden = false; ChatOverlay* parent; + bool account = false; std::vector> Convert(ChatOverlay* parent, bool has_badge) const; + void SetOnInteract(std::function on_interact); private: FileRequestBinding badge_request; void OnBadgeReady(FileRequestResult*); + std::function OnInteract; }; +inline void ChatOverlayMessage::SetOnInteract(std::function on_interact) { OnInteract = on_interact; } + class ChatOverlay : public Drawable { public: ChatOverlay(); void Draw(Bitmap& dst) override; void Update(); void UpdateScene(); - void AddMessage(std::string_view msg, std::string_view sender, std::string_view system = "", std::string_view badge = ""); + ChatOverlayMessage& AddMessage(std::string_view msg, std::string_view sender, std::string_view system = "", std::string_view badge = "", bool account = true); void OnResolutionChange() override; void SetShowAll(bool show_all); inline void SetShowAll() { SetShowAll(!show_all); } inline bool ShowingAll() const noexcept { return show_all; } void DoScroll(int increase); void MarkDirty() noexcept; + void AddSystemMessage(StringView string); private: bool IsAnyMessageVisible() const; @@ -102,16 +123,38 @@ class ChatOverlay : public Drawable { std::string input; std::deque messages; + + struct KeyTimer { + public: + const Input::Keys::InputKey raw_key; + const std::chrono::milliseconds delay; + const std::chrono::milliseconds rate; + Game_Clock::time_point last_triggered; + Game_Clock::time_point last_repeated; + }; + + enum class CustomKeyTimers : char { + backspace, + END, + }; + std::array key_timers { + {Input::Keys::BACKSPACE, std::chrono::milliseconds{300}, std::chrono::milliseconds{24}} + }; + + bool IsTriggeredOrRepeating(CustomKeyTimers key); }; inline void ChatOverlay::MarkDirty() noexcept { dirty = true; } class ChatString : public ChatComponent { + constexpr static Color default_color = Color(200, 200, 200, 255); public: StringView string; + Color color; - ChatString(StringView other) : ChatComponent(0, 0, ChatComponents::String), string(other) {} - int Draw(Bitmap& dst) override; + ChatString(StringView other, Color color = default_color) : ChatComponent(0, 0, ChatComponents::String), + string(other), color(color) {} + //Point Draw(Bitmap& dst) override; }; class ChatEmoji : public ChatComponent { @@ -122,7 +165,7 @@ class ChatEmoji : public ChatComponent { FileRequestBinding request = nullptr; ChatEmoji(ChatOverlay* parent, int width, int height, std::string emoji); - int Draw(Bitmap& dest) override; + //Point Draw(Bitmap& dest) override; void RequestBitmap(ChatOverlay* parent); }; @@ -145,6 +188,8 @@ template<> struct ChatComponentsMap { using type = ChatString; }; template<> struct ChatComponentsMap { using type = ChatEmoji; }; +template<> +struct ChatComponentsMap { using type = ChatComponent; }; template inline typename ChatComponentsMap::type* ChatComponent::Downcast() { diff --git a/src/multiplayer/chatname.h b/src/multiplayer/chatname.h index 7ca67be6a..9b3d420be 100644 --- a/src/multiplayer/chatname.h +++ b/src/multiplayer/chatname.h @@ -21,9 +21,10 @@ class ChatName : public Drawable { void SetTransparent(bool val); + std::string nickname; + std::string uuid; private: PlayerOther& player; - std::string nickname; BitmapRef nick_img; BitmapRef sys_graphic; BitmapRef effects_img; diff --git a/src/multiplayer/connection.h b/src/multiplayer/connection.h index 2fee28fde..a91c2a043 100644 --- a/src/multiplayer/connection.h +++ b/src/multiplayer/connection.h @@ -59,6 +59,7 @@ class Connection { void Dispatch(std::string_view name, ParameterList args = ParameterList()); bool IsConnected() const { return connected; } + inline volatile void* ConnectedFutex() { return static_cast(&connected); } virtual ~Connection() = default; diff --git a/src/multiplayer/game_multiplayer.cpp b/src/multiplayer/game_multiplayer.cpp index 74f86a97a..8b468ae5c 100644 --- a/src/multiplayer/game_multiplayer.cpp +++ b/src/multiplayer/game_multiplayer.cpp @@ -43,9 +43,16 @@ #include "yno_connection.h" #include "messages.h" -#ifndef EMSCRIPTEN +#ifdef PLAYER_YNO +# include # include # include +# include +# include "scene_settings.h" +# include "chat_overlay.h" +# include "graphics.h" + +using namespace std::literals::chrono_literals; using json = nlohmann::json; #endif @@ -57,6 +64,9 @@ Game_Multiplayer& Game_Multiplayer::Instance() { Game_Multiplayer::Game_Multiplayer() { InitConnection(); + for (auto& timer : timers) { + timer = Game_Clock::GetFrameTime(); + } } void Game_Multiplayer::SpawnOtherPlayer(int id) { @@ -251,10 +261,13 @@ void Game_Multiplayer::InitConnection() { return; if (players.find(p.id) == players.end()) SpawnOtherPlayer(p.id); players[p.id].account = p.account_bin == 1; + players[p.id].uuid = p.uuid; Web_API::SyncPlayerData(p.uuid, p.rank, p.account_bin, p.badge, p.medals, p.id); #ifdef PLAYER_YNO auto& player = playerdata[p.uuid]; + if (players[p.id].chat_name) + player.name = players[p.id].chat_name->nickname; player.rank = p.rank; player.badge = p.badge; player.account = p.account_bin == 1; @@ -479,6 +492,9 @@ void Game_Multiplayer::InitConnection() { Web_API::OnPlayerNameUpdated(p.name, p.id); #ifdef PLAYER_YNO + if (auto pdata = playerdata.find(player.uuid); pdata != playerdata.end()) { + pdata->second.name = p.name; + } #endif }); #ifndef EMSCRIPTEN @@ -491,11 +507,10 @@ void Game_Multiplayer::InitConnection() { // sync chat messages if (on_chat_msg) { - std::string endpoint = fmt::format("https://connect.ynoproject.net/2kki/api/chathistory?globalMsgLimit={}&partyMsgLimit={}&lastMessageId={}", 200, 100, lastmsgid); + std::string endpoint = fmt::format("https://connect.ynoproject.net/2kki/api/chathistory?globalMsgLimit={}&partyMsgLimit={}&lastMsgId={}", 200, 100, lastmsgid); cpr::Response resp = cpr::Get(cpr::Url{endpoint}); if (resp.status_code >= 200 && resp.status_code < 300) { json chatmsgs = json::parse(resp.text); - //playerdata = std::move(chatmsgs["players"]); for (auto& it : chatmsgs["players"]) { auto& entry = playerdata[(std::string)it["uuid"]]; entry.name = it["name"]; @@ -510,49 +525,70 @@ void Game_Multiplayer::InitConnection() { std::string name = "Unknown Player"; std::string system; std::string badge; + bool account = false; if (auto player = playerdata.find((std::string)(*it)["uuid"]); player != playerdata.end()) { name = player->second.name; system = player->second.systemName; badge = player->second.badge; + account = player->second.account; } + std::string contents((*it)["contents"]); on_chat_msg({ - (std::string)(*it)["contents"], name, system, badge, - std::next(it) != messages.end(), + contents, name, system, badge, + /*syncing: */std::next(it) != messages.end(), + account, }); + lastmsgid = (std::string)(*it)["msgId"]; + } + if (!username.empty()) { + if (session_token.empty()) + Graphics::GetChatOverlay().AddSystemMessage(fmt::format("Connected to YNOproject as <{}>.", username)); + else + Graphics::GetChatOverlay().AddSystemMessage(fmt::format("Connected to YNOproject as [{}].", username)); + } + else { + Graphics::GetChatOverlay().AddSystemMessage( + "Welcome to YNOproject! To join the chat, first set your name in the Online settings."); } } } }); sessionConn.RegisterSystemHandler(YSM::CLOSE, [this](MCo&) { + session_active = false; }); sessionConn.RegisterHandler("gsay", [this](SessionGSay& p) { std::string name = "Unknown Player"; std::string system; std::string badge; + bool account = false; if (auto player = playerdata.find(p.uuid); player != playerdata.end()) { if (!player->second.name.empty()) name = player->second.name; system = player->second.systemName; badge = player->second.badge; + account = player->second.account; } - Output::Debug("{}: {}", name, p.msg); + Output::Debug("(gc) {}: {}", name, p.msg); if (on_chat_msg) { - on_chat_msg({ p.msg, name, system, badge }); + on_chat_msg({ p.msg, name, system, badge, account }); + lastmsgid = p.msgid; } }); sessionConn.RegisterHandler("say", [this](SessionSay& p) { std::string name = "Unknown Player"; std::string system; std::string badge; + bool account = false; if (auto player = playerdata.find(p.uuid); player != playerdata.end()) { if (!player->second.name.empty()) name = player->second.name; system = player->second.systemName; badge = player->second.badge; + account = player->second.account; } - Output::Debug("{}: {}", name, p.msg); + Output::Debug("(mc) {}: {}", name, p.msg); if (on_chat_msg) { - on_chat_msg({ p.msg, name, system, badge }); + on_chat_msg({ p.msg, name, system, badge, account, /*global: */false }); } }); sessionConn.RegisterHandler("p", [this](SessionPlayerInfo& p) { @@ -627,25 +663,13 @@ 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/2kki/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); +#ifdef PLAYER_YNO + if (!session_token.empty() || !cfg.session_token.Get().empty()) { + CheckLogin(false); } + sessionConn.need_header = false; - sessionConn.Open(sessionEndpoint); + sessionConn.Open(GetSessionEndpoint()); #endif } @@ -875,6 +899,14 @@ void Game_Multiplayer::ApplyPlayerBattleAnimUpdates() { } } +void Game_Multiplayer::ApplyFlash(int r, int g, int b, int power, int frames) { + for (auto& p : players) { + p.second.ch->Flash(r, g, b, power, frames); + if (p.second.chat_name) + p.second.chat_name->SetFlashFramesLeft(frames); + } +} + void Game_Multiplayer::ApplyRepeatingFlashes() { for (auto& rf : repeating_flashes) { if (players.find(rf.first) != players.end()) { @@ -1037,4 +1069,110 @@ void Game_Multiplayer::Update() { sessionConn.FlushQueue(); #endif } + + UpdateTimers(); +} + +void Game_Multiplayer::SetConfig(const Game_ConfigOnline& cfg) { + this->cfg = cfg; + session_token = cfg.session_token.Get(); + SetNametagMode((int)cfg.nametag_mode.Get()); + if (!cfg.username.Get().empty()) + SetNickname(cfg.username.Get()); +} + +std::string Game_Multiplayer::GetSessionEndpoint() const { + return fmt::format("{}session?token={}", Web_API::GetSocketURL(), session_token); +} + +void Game_Multiplayer::CheckLogin(bool async) { + std::string token = session_token; + if (token.empty()) + token = cfg.session_token.Get(); + if (token.empty()) return; + session_token = token; + + if (async) { + uv_queue_work(uv_default_loop(), new uv_work_t{}, + [](uv_work_t*) { GMI().CheckLogin(false); }, + [](uv_work_t* task, int) { delete task; }); + return; + } + + cpr::Header header; + header["Authorization"] = token; + auto resp = cpr::Get(cpr::Url{ "https://connect.ynoproject.net/2kki/api/info" }, header); + CheckLoginCallback(resp); +} + +void Game_Multiplayer::CheckLoginCallback(cpr::Response resp) { + if (!(resp.status_code >= 200 && resp.status_code < 300)) + return; + json body = json::parse(resp.text); + std::string uuid(body["uuid"]); + if (uuid.empty()) + Logout(); + else { + username = (std::string)body["name"]; + cfg.username.Set(username); + Output::Debug("username: {}", username); + } + Graphics::GetChatOverlay().MarkDirty(); +} + +void Game_Multiplayer::Login(std::string_view username, std::string_view password) { + std::string formdata = fmt::format("user={}&password={}", username, password); + cpr::Header header; + header["Content-Type"] = "application/x-www-form-urlencoded"; + + auto resp = cpr::Post( + cpr::Url{ "https://connect.ynoproject.net/2kki/api/login" }, + cpr::Body{ formdata }, header); + if (resp.status_code >= 200 && resp.status_code < 300) { + login_failure.clear(); + session_token = resp.text; + cfg.session_token.Set(session_token); + CheckLogin(false); + ReconnectSession(); + } + else { + Output::Warning("login failed: {} {}", resp.status_code, resp.text); + login_failure = resp.text; + } +} + +void Game_Multiplayer::Logout() { + session_token.clear(); + cfg.session_token.Set(""); + ReconnectSession(); +} + +void Game_Multiplayer::ReconnectSession() { + Scene_Settings::SaveConfig(true); + sessionConn.Open(GetSessionEndpoint()); +} + +void Game_Multiplayer::UpdateTimers() { + auto now = Game_Clock::GetFrameTime(); + + if (!sessionConn.IsConnected() && session_active) { + if (now - timers[Timers::conn_check] > 5s) { + Graphics::GetChatOverlay().AddSystemMessage("Session disconnected, attempting to reconnect..."); + timers[Timers::conn_check] = now; + sessionConn.Open(GetSessionEndpoint()); + } + } + + if (now - timers[Timers::token_check] > 600s) { + timers[Timers::token_check] = now; + CheckLogin(); + } +} + +void Game_Multiplayer::SetNickname(StringView name) { + std::string value(name); + GMI().sessionConn.SendPacketAsync(value); + username = value; + cfg.username.Set(value); + Graphics::GetChatOverlay().MarkDirty(); } diff --git a/src/multiplayer/game_multiplayer.h b/src/multiplayer/game_multiplayer.h index e578d355e..795698e54 100644 --- a/src/multiplayer/game_multiplayer.h +++ b/src/multiplayer/game_multiplayer.h @@ -10,6 +10,12 @@ #include #include "yno_connection.h" +#ifdef PLAYER_YNO +# include +# include "game_config.h" +# include "game_clock.h" +#endif + class PlayerOther; class Game_Multiplayer { @@ -74,6 +80,8 @@ class Game_Multiplayer { std::string_view system; std::string_view badge; bool syncing = false; + bool account = true; + bool global = true; }; std::function on_chat_msg; std::function on_system_graphic_change; @@ -89,12 +97,7 @@ class Game_Multiplayer { int room_id{-1}; int frame_index{0}; - enum class NametagMode { - NONE, - CLASSIC, - COMPACT, - SLIM - }; + using NametagMode = ConfigEnum::NametagMode; NametagMode GetNametagMode() { return nametag_mode; } void SetNametagMode(int mode) { @@ -122,8 +125,37 @@ class Game_Multiplayer { void ResetRepeatingFlash(); void InitConnection(); void SendShownPictures(); + + std::string username; + std::string login_failure; + Game_ConfigOnline cfg; + + Game_ConfigOnline& GetConfig() noexcept; + void SetConfig(const Game_ConfigOnline& cfg); + std::string GetSessionEndpoint() const; + /** Logs the player out if the session token is no longer valid */ + void CheckLogin(bool async = true); + void CheckLoginCallback(cpr::Response resp); + void Login(std::string_view username, std::string_view password); + void Logout(); + void ReconnectSession(); + bool CanChat() const noexcept; + + enum Timers { + conn_check, + token_check, + END + }; + + std::array timers{}; + + void UpdateTimers(); + void SetNickname(StringView name); }; inline Game_Multiplayer& GMI() { return Game_Multiplayer::Instance(); } +inline Game_ConfigOnline& Game_Multiplayer::GetConfig() noexcept { return cfg; } +inline bool Game_Multiplayer::CanChat() const noexcept { return !username.empty(); } + #endif diff --git a/src/multiplayer/messages.h b/src/multiplayer/messages.h index be40aa1df..4d0223ebc 100644 --- a/src/multiplayer/messages.h +++ b/src/multiplayer/messages.h @@ -666,7 +666,7 @@ namespace C2S { int event_id; int action_bin; }; -#ifndef EMSCRIPTEN +#ifdef PLAYER_YNO class SessionPlayerName : public C2SPacket { public: SessionPlayerName(std::string name_) : C2SPacket("name"), @@ -675,6 +675,14 @@ namespace C2S { protected: std::string name; }; + class SessionGSay : public C2SPacket { + public: + SessionGSay(std::string contents) : C2SPacket("gsay"), + contents(std::move(contents)) {} + std::string ToBytes() const override { return Build(contents); } + protected: + std::string contents; + }; #endif } } diff --git a/src/multiplayer/playerother.h b/src/multiplayer/playerother.h index 5feacc886..5829553e2 100644 --- a/src/multiplayer/playerother.h +++ b/src/multiplayer/playerother.h @@ -3,6 +3,7 @@ #include #include +#include struct Game_PlayerOther; struct Sprite_Character; @@ -17,6 +18,8 @@ struct PlayerOther { std::unique_ptr chat_name; std::unique_ptr battle_animation; // battle animation + std::string uuid; + // create a shadow of this // shadow has no name, no battle animation and no move commands // but it is visible, in other words this function modifies the diff --git a/src/multiplayer/scene_online.cpp b/src/multiplayer/scene_online.cpp new file mode 100644 index 000000000..795c1948f --- /dev/null +++ b/src/multiplayer/scene_online.cpp @@ -0,0 +1,63 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#include "scene_online.h" +#include "main_data.h" +#include "game_system.h" + +Scene_Online::Scene_Online() { + type = SceneType::Settings; +} + +void Scene_Online::Start() { + CreateOptionsWindow(); + + options_window->Push(Window_Settings::eOnlineAccount); +} + +void Scene_Online::Refresh() { + +} + +void Scene_Online::vUpdate() { + auto opt_mode = options_window->GetMode(); + + if (Input::IsTriggered(Input::CANCEL)) { + Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Game_System::SFX_Cancel)); + + options_window->Pop(); + SetMode(options_window->GetMode()); + if (mode == Window_Settings::eNone) + Scene::Pop(); + } + + UpdateOptions(); +} + +void Scene_Online::CreateOptionsWindow() { + +} + +void Scene_Online::UpdateOptions() { + options_window->Update(); +} + +void Scene_Online::SetMode(Window_Settings::UiMode new_mode) { + if (new_mode == mode) + return; + mode = new_mode; +} diff --git a/src/multiplayer/scene_online.h b/src/multiplayer/scene_online.h new file mode 100644 index 000000000..ba1c41ca5 --- /dev/null +++ b/src/multiplayer/scene_online.h @@ -0,0 +1,45 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef EP_MP_SCENE_ONLINE_H +#define EP_MP_SCENE_ONLINE_H + +#include "scene.h" +#include "window_settings.h" +#include "window_help.h" + +/** Online settings. */ +class Scene_Online : public Scene { +public: + Scene_Online(); + void Start() override; + void Refresh() override; + void vUpdate() override; +private: + void CreateOptionsWindow(); + + void UpdateOptions(); + + void SetMode(Window_Settings::UiMode new_mode); + + std::unique_ptr options_window; + std::unique_ptr help_window; + + Window_Settings::UiMode mode = Window_Settings::eNone; +}; + +#endif diff --git a/src/multiplayer/yno_connection.cpp b/src/multiplayer/yno_connection.cpp index 8c4407035..639018b2d 100644 --- a/src/multiplayer/yno_connection.cpp +++ b/src/multiplayer/yno_connection.cpp @@ -12,6 +12,9 @@ # include # include # include +# if defined(_WIN32) +# include +# endif #endif #include "../external/TinySHA1.hpp" @@ -115,6 +118,36 @@ struct YNOConnection::IMPL { #endif }; +namespace { + lws_context* default_context; + + static struct lws_protocols protocols_[] = { + {"binary", WebSocketClient::CallbackFunction, 0, 65536, 0, nullptr, 0}, + {nullptr, nullptr} // terminator + }; + + lws_context* GetDefaultContext() { + if (default_context) + return default_context; + 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; + + default_context = lws_create_context(&info); + if (!default_context) { + Output::ErrorStr("Failed to create LWS context"); + } + return default_context; + } +} + #ifndef EMSCRIPTEN WebSocketClient::WebSocketClient(const std::string& url) : url_(url), context_(nullptr), wsi_(nullptr), running_(false), userdata_(nullptr) { } @@ -126,22 +159,7 @@ WebSocketClient::~WebSocketClient() { 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"); - } + context_ = GetDefaultContext(); const char *prot, *addr, *path; int port; @@ -158,21 +176,24 @@ void WebSocketClient::Start() { ccinfo.origin = lws_canonical_hostname(context_); ccinfo.ssl_connection = LCCSCF_USE_SSL; ccinfo.protocol = protocols_[0].name; + ccinfo.opaque_user_data = this; wsi_ = lws_client_connect_via_info(&ccinfo); if (!wsi_) { lws_context_destroy(context_); - Output::ErrorStr("Failed to connect to the WebSocket server"); + Output::Debug("Failed to connect to the WebSocket server"); + return; } running_.store(true, std::memory_order_acquire); } void WebSocketClient::Stop() { - Output::Debug("Stopping {}", url_); if (!running_) return; + Output::Debug("Stopping {}", url_); running_.store(false, std::memory_order_release); lws_set_timeout(wsi_, PENDING_TIMEOUT_AWAITING_PROXY_RESPONSE, LWS_TO_KILL_ASYNC); + //lws_cancel_service(lws_get_context(wsi_)); } void WebSocketClient::Send(std::string_view data) { @@ -183,7 +204,26 @@ void WebSocketClient::Send(std::string_view data) { int WebSocketClient::CallbackFunction(struct lws* wsi, enum lws_callback_reasons reason, void* user, void* in, size_t len) { - WebSocketClient* client = reinterpret_cast(lws_context_user(lws_get_context(wsi))); + WebSocketClient* client = reinterpret_cast(lws_get_opaque_user_data(wsi)); +// if (client && !client->running_ && client->ready_) { +//#define $print(x) case (x): Output::Debug("{}: " #x, client->url_); break; +// switch (reason) { +// $print(LWS_CALLBACK_CLIENT_ESTABLISHED) +// $print(LWS_CALLBACK_CLIENT_WRITEABLE) +// $print(LWS_CALLBACK_CLIENT_RECEIVE) +// $print(LWS_CALLBACK_WS_PEER_INITIATED_CLOSE) +// $print(LWS_CALLBACK_CLIENT_CLOSED) +// $print(LWS_CALLBACK_CLIENT_CONNECTION_ERROR) +// $print(LWS_CALLBACK_EVENT_WAIT_CANCELLED) +// $print(LWS_CALLBACK_LOCK_POLL) +// $print(LWS_CALLBACK_UNLOCK_POLL) +// $print(LWS_CALLBACK_CHANGE_MODE_POLL_FD) +// $print(LWS_CALLBACK_DEL_POLL_FD) +// default: +// Output::Debug("Unknown LWS event {}", (int)reason); +// } +//#undef $print +// } switch (reason) { case LWS_CALLBACK_CLIENT_ESTABLISHED: @@ -227,7 +267,8 @@ int WebSocketClient::CallbackFunction(struct lws* wsi, enum lws_callback_reasons ); bool exit = false; if (close_span.size() >= 2) { - int16_t close_reason = static_cast((close_span[0] << 8) | close_span[1]); + // close reason in network order (bigendian) + int16_t close_reason = ((int16_t)(close_span[1]) << 8) | close_span[0]; exit = close_reason == 1028; } client->on_disconnect_(exit, client->userdata_); @@ -250,27 +291,26 @@ int WebSocketClient::CallbackFunction(struct lws* wsi, enum lws_callback_reasons break; case LWS_CALLBACK_EVENT_WAIT_CANCELLED: - if (client->ready_) { - Output::Warning("{}: event wait cancelled", client->url_); + if (client && client->ready_) { + Output::Debug("{}: event wait cancelled", client->url_); client->running_.store(false, std::memory_order_release); + if (client->on_disconnect_) { + client->on_disconnect_(false, client->userdata_); + } } break; - //case LWS_CALLBACK_OPENSSL_PERFORM_SERVER_CERT_VERIFICATION: - // X509_STORE_CTX_set_error((X509_STORE_CTX*)user, X509_V_OK); + //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; - 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 LWS event {}", (int)reason); - break; + //default: + // Output::Debug("Unhandled LWS event {}", (int)reason); + // break; } return 0; @@ -313,12 +353,11 @@ YNOConnection::~YNOConnection() { } void YNOConnection::Open(std::string_view uri) { + std::string s {uri}; +#ifdef __EMSCRIPTEN__ if (!impl->closed) { Close(); } - - std::string s {uri}; -#ifdef __EMSCRIPTEN__ EmscriptenWebSocketCreateAttributes ws_attrs = { s.data(), "binary", @@ -328,10 +367,41 @@ void YNOConnection::Open(std::string_view uri) { impl->closed = false; IMPL::set_callbacks(impl->socket, this); #else - impl->create_client(s); - impl->closed = false; - impl->set_callbacks(this); - impl->start(); + if (!impl->_wsclient) { + impl->create_client(s); + impl->closed = false; + impl->set_callbacks(this); + impl->start(); + return; + } + uv_work_t* task = new uv_work_t{}; + using work_data_t = std::pair; + task->data = new work_data_t{this, std::move(s)}; + + uv_queue_work(uv_default_loop(), task, + [](uv_work_t* task) { + auto& [self, s] = *static_cast(task->data); +#if defined(_WIN32) + if (!self->IsConnected()) return; + + self->Close(); + bool expected_value = false; + if (!WaitOnAddress(self->ConnectedFutex(), &expected_value, sizeof(expected_value), INFINITE)) + Output::Debug("Failed to wait for previous session to close: {}", GetLastError()); +#else +# error TODO: wait on futex +#endif + }, + [](uv_work_t* task, int) { + auto& [self, s] = *static_cast(task->data); + self->impl->create_client(s); + self->impl->closed = false; + self->impl->set_callbacks(self); + self->impl->start(); + + delete (work_data_t*)task->data; + delete task; + }); #endif } @@ -348,6 +418,7 @@ void YNOConnection::Close() { emscripten_websocket_delete(impl->socket); #else // handled by create_client + //impl->_wsclient->Stop(); #endif } diff --git a/src/multiplayer/yno_connection.h b/src/multiplayer/yno_connection.h index af7664ba1..ae1dfd591 100644 --- a/src/multiplayer/yno_connection.h +++ b/src/multiplayer/yno_connection.h @@ -19,11 +19,12 @@ class YNOConnection : public Multiplayer::Connection { YNOConnection& operator=(YNOConnection&&); ~YNOConnection(); + bool need_header = true; + void Open(std::string_view uri) override; - void Close() override; void Send(std::string_view data) override; void FlushQueue() override; - bool need_header = true; + void Close() override; protected: struct IMPL; std::unique_ptr impl; @@ -52,9 +53,9 @@ class WebSocketClient : public std::enable_shared_from_this { 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); +private: static inline struct lws_protocols protocols_[] = { {"binary", CallbackFunction, 0, 65536, 0, nullptr, 0}, diff --git a/src/platform.cpp b/src/platform.cpp index c29241984..6ee057699 100644 --- a/src/platform.cpp +++ b/src/platform.cpp @@ -123,6 +123,29 @@ int64_t Platform::File::GetSize() const { #endif } +int64_t Platform::File::GetLastModified() const { +#if defined(_WIN32) + WIN32_FILE_ATTRIBUTE_DATA data; + BOOL res = ::GetFileAttributesExW(filename.c_str(), GetFileExInfoStandard, &data); + if (!res) + return -1; + + // the difference between Windows epoch and Unix epoch + constexpr ULONGLONG epoch_diff = 11644473600ULL; + ULARGE_INTEGER ts; + FILETIME ft = data.ftLastWriteTime; + ts.LowPart = ft.dwLowDateTime; + ts.HighPart = ft.dwHighDateTime; + return (int64_t)ts.QuadPart / 10000000ULL - epoch_diff; + //return (int64_t)((ts.QuadPart - epoch_diff * 10000000ULL) / 10000000ULL); +#else + struct stat sb = {}; + int result = ::stat(filename.c_str(), &sb); + struct timspec ts = sb.st_mtim; + return !result ? (int64_t)ts.tv_sec + (int64_t)ts.tv_nsec / 1000000000 : -1; +#endif +} + bool Platform::File::MakeDirectory(bool follow_symlinks) const { if (IsDirectory(follow_symlinks)) { return true; diff --git a/src/platform.h b/src/platform.h index a285822c7..38414fc8a 100644 --- a/src/platform.h +++ b/src/platform.h @@ -89,6 +89,9 @@ namespace Platform { /** @return Filesize or -1 on error */ int64_t GetSize() const; + /** Gets the Unix timestamp since last write or -1 on fail */ + int64_t GetLastModified() const; + /** * Creates a directory recursively at the filename path. * @param follow_symlinks Whether to follow symlinks (if supported on this platform) diff --git a/src/platform/ynoshell/main.cpp b/src/platform/ynoshell/main.cpp index 17cee4562..26d9150fd 100644 --- a/src/platform/ynoshell/main.cpp +++ b/src/platform/ynoshell/main.cpp @@ -17,7 +17,8 @@ class YnoApp : public wxApp bool OnInit() override; }; -wxIMPLEMENT_APP(YnoApp); +//wxIMPLEMENT_APP(YnoApp); +wxIMPLEMENT_APP_NO_MAIN(YnoApp); //class YnoChatMsg : public wxPanel //{ @@ -100,7 +101,7 @@ YnoFrame::YnoFrame() sizer->Add((wxWindow*)gameWindow, 1, wxEXPAND); GMI().on_chat_msg = [](Game_Multiplayer::ChatMsg msg) { - Graphics::GetChatOverlay().AddMessage(msg.content, msg.sender, msg.system, msg.badge); + Graphics::GetChatOverlay().AddMessage(msg.content, msg.sender, msg.system, msg.badge, msg.account); }; SetSizer(sizer); @@ -116,3 +117,7 @@ void YnoFrame::OnKeyDownFrame(wxKeyEvent& event) { event.Skip(); } +int main(int argc, char** argv) { + Output::SetLogLevel(LogLevel::Debug); + wxEntry(argc, argv); +} diff --git a/src/player.cpp b/src/player.cpp index a24d294e6..9e1b19308 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -206,6 +206,9 @@ void Player::Init(std::vector args) { Input::Init(cfg.input, replay_input_path, record_input_path); Input::AddRecordingData(Input::RecordingData::CommandLine, command_line); +#ifdef PLAYER_YNO + GMI().SetConfig(cfg.online); +#endif player_config = std::move(cfg.player); @@ -1171,9 +1174,9 @@ void Player::LoadFonts() { Font::SetNameText(Font::CreateFtFont(std::move(name_text), 11, false, false), false); } - auto chat_text = FileFinder::OpenFont("ChatText"); + auto chat_text = FileFinder::OpenFont("NameText"); if (chat_text) { - Font::SetChatText(Font::CreateFtFont(std::move(chat_text), 36, false, false)); + Font::SetChatText(Font::CreateFtFont(std::move(chat_text), 11, false, false)); } auto name_text_2 = FileFinder::OpenFont("NameText2"); diff --git a/src/player.h b/src/player.h index 007013578..c7acfad70 100644 --- a/src/player.h +++ b/src/player.h @@ -564,7 +564,8 @@ inline bool Player::IsPatchUnlockPics() { } inline bool Player::HasEasyRpgExtensions() { - return game_config.patch_easyrpg.Get(); + return true; + //return game_config.patch_easyrpg.Get(); } #ifdef ENABLE_DYNAMIC_INTERPRETER_CONFIG diff --git a/src/scene_logo.cpp b/src/scene_logo.cpp index 123cc1f06..2cc3e9b06 100644 --- a/src/scene_logo.cpp +++ b/src/scene_logo.cpp @@ -132,7 +132,6 @@ bool Scene_Logo::DetectGame() { FileFinder::SetGameFilesystem(fs); } -#ifdef __EMSCRIPTEN__ static bool once = true; if (once) { FileRequestAsync* index = AsyncHandler::RequestFile("index.json"); @@ -145,7 +144,6 @@ bool Scene_Logo::DetectGame() { if (!async_ready) { return false; } -#endif if (FileFinder::IsValidProject(fs) || FileFinder::OpenViewToEasyRpgFile(fs)) { FileFinder::SetGameFilesystem(fs); diff --git a/src/scene_menu.cpp b/src/scene_menu.cpp index 98c3b8112..a0cae020f 100644 --- a/src/scene_menu.cpp +++ b/src/scene_menu.cpp @@ -36,6 +36,10 @@ #include "bitmap.h" #include "feature.h" +#ifdef PLAYER_YNO +# include "multiplayer/scene_online.h" +#endif + constexpr int menu_command_width = 88; constexpr int gold_window_width = 88; constexpr int gold_window_height = 32; @@ -89,6 +93,9 @@ void Scene_Menu::CreateCommandWindow() { if (Player::debug_flag) { command_options.push_back(Debug); } +#ifdef PLAYER_YNO + command_options.push_back(Online); +#endif command_options.push_back(Quit); } else { for (std::vector::iterator it = lcf::Data::system.menu_commands.begin(); @@ -115,6 +122,9 @@ void Scene_Menu::CreateCommandWindow() { if (Player::debug_flag) { command_options.push_back(Debug); } +#ifdef PLAYER_YNO + command_options.push_back(Online); +#endif command_options.push_back(Quit); } @@ -152,6 +162,9 @@ void Scene_Menu::CreateCommandWindow() { case Debug: options.push_back("Debug"); break; + case Online: + options.push_back("Online"); + break; default: options.push_back(ToString(lcf::Data::terms.menu_quit)); break; @@ -249,6 +262,12 @@ void Scene_Menu::UpdateCommand() { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); Scene::Push(std::make_shared()); break; +#ifdef PLAYER_YNO + case Online: + Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); + Scene::Push(std::make_shared(Window_Settings::eOnlineAccount)); + break; +#endif case Quit: Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); Scene::Push(std::make_shared()); diff --git a/src/scene_menu.h b/src/scene_menu.h index 0b7eec1d9..bcad9b141 100644 --- a/src/scene_menu.h +++ b/src/scene_menu.h @@ -69,6 +69,7 @@ class Scene_Menu : public Scene { // EasyRPG extra Debug = 100, Settings = 101, + Online = 200, }; private: diff --git a/src/scene_settings.cpp b/src/scene_settings.cpp index 3590b4cb7..07f786b24 100644 --- a/src/scene_settings.cpp +++ b/src/scene_settings.cpp @@ -44,10 +44,18 @@ # include #endif +#ifdef PLAYER_YNO +# include "multiplayer/game_multiplayer.h" +#endif + Scene_Settings::Scene_Settings() { Scene::type = Scene::Settings; } +Scene_Settings::Scene_Settings(Window_Settings::UiMode initial_mode) : Scene_Settings() { + mode = initial_mode; +} + void Scene_Settings::CreateTitleGraphic() { // Load Title Graphic if (lcf::Data::system.title_name.empty()) { @@ -68,6 +76,7 @@ void Scene_Settings::CreateMainWindow() { { Window_Settings::eInput, "Input"}, { Window_Settings::eEngine, "Engine"}, { Window_Settings::eLicense,"License"}, + { Window_Settings::eOnlineAccount, "Online"}, { Window_Settings::eSave, ""} }); @@ -135,8 +144,13 @@ void Scene_Settings::Start() { CreateMainWindow(); CreateOptionsWindow(); - options_window->Push(Window_Settings::eMain); - SetMode(Window_Settings::eMain); + auto start_mode = Window_Settings::eMain; + if (mode != Window_Settings::eNone) { + start_mode = mode; + mode = Window_Settings::eNone; + } + options_window->Push(start_mode); + SetMode(start_mode); } void Scene_Settings::SetMode(Window_Settings::UiMode new_mode) { @@ -160,6 +174,7 @@ void Scene_Settings::SetMode(Window_Settings::UiMode new_mode) { picker_window.reset(); font_size_window.reset(); + string_window.reset(); switch (mode) { case Window_Settings::eNone: @@ -226,6 +241,14 @@ void Scene_Settings::vUpdate() { SetMode(opt_mode); + if (string_window) { + if (Input::IsRawKeyTriggered(Input::Keys::ESCAPE)) { + Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Game_System::SFX_Cancel)); + string_window.reset(); + options_window->SetActive(true); + } + } + else if (Input::IsTriggered(Input::CANCEL) && opt_mode != Window_Settings::eInputButtonAdd) { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Game_System::SFX_Cancel)); @@ -277,6 +300,7 @@ void Scene_Settings::vUpdate() { case Window_Settings::eInputListButtonsEngine: case Window_Settings::eInputListButtonsDeveloper: case Window_Settings::eInputListButtonsOnline: + case Window_Settings::eOnlineAccount: UpdateOptions(); break; case Window_Settings::eEngineFont1: @@ -375,6 +399,20 @@ void Scene_Settings::UpdateOptions() { } return; } + else if (string_window) { + string_window->Update(); + auto& option = options_window->GetCurrentOption(); + option.value_text = string_window->value; + option.action(); + + if (Input::IsRawKeyTriggered(Input::Keys::RETURN)) { + options_window->Refresh(); + string_window.reset(); + options_window->SetActive(true); + Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Game_System::SFX_Decision)); + } + return; + } if (Input::IsTriggered(Input::DECISION)) { if (options_window->IsCurrentActionEnabled()) { @@ -407,6 +445,15 @@ void Scene_Settings::UpdateOptions() { win.SetText(options_window->GetCurrentOption().options_help[index]); }; } + else if (option.mode == Window_Settings::eOptionStringInput) { + string_window.reset(new Window_StringInput(option.value_text, 0, 0, 128, 32)); + string_window->SetX(options_window->GetX() + options_window->GetWidth() / 2 - string_window->GetWidth() / 2); + string_window->SetY(options_window->GetY() + options_window->GetHeight() / 2 - string_window->GetHeight() / 2); + string_window->SetZ(options_window->GetZ() + 1); + string_window->SetOpacity(255); + string_window->SetActive(true); + options_window->SetActive(false); + } } else { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Game_System::SFX_Buzzer)); } @@ -667,6 +714,7 @@ bool Scene_Settings::SaveConfig(bool silent) { cfg.audio = Audio().GetConfig(); cfg.input = Input::GetInputSource()->GetConfig(); cfg.player = Player::player_config; + cfg.online = GMI().GetConfig(); cfg.WriteToStream(cfg_out); diff --git a/src/scene_settings.h b/src/scene_settings.h index d62b488da..af1f9c8dc 100644 --- a/src/scene_settings.h +++ b/src/scene_settings.h @@ -43,6 +43,8 @@ class Scene_Settings : public Scene { */ Scene_Settings(); + Scene_Settings(Window_Settings::UiMode initial_mode); + void Start() override; void Refresh() override; void vUpdate() override; @@ -83,6 +85,7 @@ class Scene_Settings : public Scene { std::unique_ptr input_mode_window; std::unique_ptr picker_window; std::unique_ptr number_window; + std::unique_ptr string_window; std::unique_ptr font_size_window; std::unique_ptr title; diff --git a/src/text.cpp b/src/text.cpp index eb241ceeb..7096da70a 100644 --- a/src/text.cpp +++ b/src/text.cpp @@ -189,8 +189,6 @@ Rect Text::GetSize(const Font& font, std::string_view text) { auto iter = text.data(); const auto end = iter + text.size(); - double zoom = font.GetCurrentStyle().size / 12.; - if (font.CanShape()) { std::u32string text32; while (iter != end) { @@ -215,7 +213,8 @@ Rect Text::GetSize(const Font& font, std::string_view text) { for (const auto& ch: shape_ret) { Rect size = font.GetSize(ch); - rect.width += ceilf((ch.offset.x + size.width) * zoom); + //rect.width += ceilf((ch.offset.x + size.width) * zoom); + rect.width += ch.offset.x + size.width; rect.height = std::max(rect.height, size.height); } } @@ -234,7 +233,8 @@ Rect Text::GetSize(const Font& font, std::string_view text) { for (const auto& ch: shape_ret) { Rect size = font.GetSize(ch); - rect.width += ceilf((ch.offset.x + size.width) * zoom); + //rect.width += ceilf((ch.offset.x + size.width) * zoom); + rect.width += ch.offset.x + size.width; rect.height = std::max(rect.height, size.height); } } diff --git a/src/window_input_settings.h b/src/window_input_settings.h index 67d184588..c7cc69916 100644 --- a/src/window_input_settings.h +++ b/src/window_input_settings.h @@ -24,6 +24,7 @@ #include "input_buttons.h" #include "window_numberinput.h" #include "window_selectable.h" +#include "window_stringinput.h" /** * Window_InputSettings class. diff --git a/src/window_settings.cpp b/src/window_settings.cpp index ea1385947..9047edc29 100644 --- a/src/window_settings.cpp +++ b/src/window_settings.cpp @@ -39,7 +39,11 @@ # include "platform/emscripten/interface.h" #endif -class MenuItem final : public ConfigParam { +#ifdef PLAYER_YNO +# include "multiplayer/messages.h" +#endif + +class MenuItem final : public ConfigParam { public: explicit MenuItem(std::string_view name, std::string_view description, std::string_view value) : ConfigParam(name, description, "", "", value) { @@ -152,6 +156,9 @@ void Window_Settings::Refresh() { case eLicense: RefreshLicense(); break; + case eOnlineAccount: + RefreshOnline(); + break; case eInputButtonCategory: RefreshButtonCategory(); break; @@ -271,6 +278,21 @@ void Window_Settings::AddOption(const EnumConfigParam& param, GetFrame().options.push_back(std::move(opt)); } +template +void Window_Settings::AddOption(const StringConfigParam& param, Action&& action) { + if (!param.IsOptionVisible()) { + return; + } + Option opt; + opt.text = ToString(param.GetName()); + opt.help = ToString(param.GetDescription()); + opt.value_text = param.Get(); + opt.mode = eOptionStringInput; + if (!param.IsLocked()) + opt.action = std::forward(action); + GetFrame().options.push_back(std::move(opt)); +} + void Window_Settings::RefreshVideo() { auto cfg = DisplayUi->GetConfig(); @@ -750,3 +772,58 @@ void Window_Settings::RefreshButtonList() { }); } } + +void Window_Settings::RefreshOnline() { + Game_ConfigOnline& cfg = GMI().GetConfig(); + + if (!cfg.username.Get().empty() && !cfg.session_token.Get().empty()) { + AddOption(MenuItem("Status", "", fmt::format("Logged in as {}", cfg.username.Get())), [] {}); + AddOption(MenuItem("Logout", "", ""), [this] { + GMI().Logout(); + Refresh(); + }); + } + else { + if (cfg.username.Get().empty()) + AddOption(MenuItem("Status", "", "Username not set"), [] {}); + else + AddOption(MenuItem("Status", "", fmt::format("Playing as guest user")), [] {}); + + AddOption(cfg.username, [this, &cfg] { + auto& value = GetCurrentOption().value_text; + cfg.username.Set(value); + if (!value.empty() && GMI().sessionConn.IsConnected() && Input::IsRawKeyTriggered(Input::Keys::RETURN)) { + GMI().SetNickname(value); + //Output::Debug("set: {}", value); + } + }); + AddOption(cfg.password, [this, &cfg] { + cfg.password.Set(GetCurrentOption().value_text); + }); + if (!GMI().login_failure.empty()) { + AddOption(MenuItem(GMI().login_failure, "", ""), [] {}); + } + AddOption(MenuItem("Login", "", ""), [this, &cfg] { + GMI().Login(cfg.username.Get(), cfg.password.Get()); + cfg.password.Set(""); + Refresh(); + }); + AddOption(MenuItem("Register", "", ""), [this, &cfg] { + Output::Warning("Register not implemented"); + cfg.password.Set(""); + Refresh(); + }); + } + + AddOption(cfg.nametag_mode, [this, &cfg] { + int option = GetCurrentOption().current_value; + cfg.nametag_mode.Set((ConfigEnum::NametagMode)option); + GMI().SetNametagMode(option); + }); + + // some debug options + AddOption(MenuItem("[DEBUG] Force Disconnect Session", "", ""), [] { + Scene::PopUntil(Scene::SceneType::Map); + GMI().sessionConn.Open(GMI().GetSessionEndpoint()); + }); +} diff --git a/src/window_settings.h b/src/window_settings.h index 1258c5df0..f784a234c 100644 --- a/src/window_settings.h +++ b/src/window_settings.h @@ -53,13 +53,16 @@ class Window_Settings : public Window_Selectable { eEnd, eAbout, eLanguage, + // online only + eOnlineAccount, eLastMode }; enum OptionMode { eOptionNone, eOptionRangeInput, - eOptionPicker + eOptionPicker, + eOptionStringInput, }; struct Option { @@ -140,6 +143,9 @@ class Window_Settings : public Window_Selectable { Action&& action ); + template + void AddOption(const StringConfigParam& p, Action&& action); + void RefreshInput(); void RefreshButtonCategory(); void RefreshButtonList(); @@ -150,6 +156,7 @@ class Window_Settings : public Window_Selectable { void RefreshEngine(); void RefreshEngineFont(bool mincho); void RefreshLicense(); + void RefreshOnline(); void UpdateHelp() override; diff --git a/src/window_stringinput.cpp b/src/window_stringinput.cpp new file mode 100644 index 000000000..7413b4351 --- /dev/null +++ b/src/window_stringinput.cpp @@ -0,0 +1,71 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#include "window_stringinput.h" +#include "input.h" +#include "bitmap.h" + +Window_StringInput::Window_StringInput(StringView initial_value, int ix, int iy, int iwidth, int iheight) : + Window_Selectable(ix, iy, iwidth, iheight), value(initial_value) +{ + SetContents(Bitmap::Create(width - 16, height - 16)); + // Above the message window + SetZ(Priority_Window + 150); + opacity = 0; + active = false; + + Refresh(); +} + +void Window_StringInput::Refresh() { + contents->Clear(); + contents->TextDraw(0, 2, Font::ColorDefault, value); + contents->FillRect(GetCursorRect(), Color(255, 255, 255, 200)); +} + +void Window_StringInput::Update() { + Window_Selectable::Update(); + if (!active) return; + + bool dirty = true; + + constexpr int offset = U'A' - Input::Keys::A; + constexpr int uppercase = U'a' - U'A'; + bool shift = Input::IsRawKeyPressed(Input::Keys::LSHIFT) || Input::IsRawKeyPressed(Input::Keys::RSHIFT); + static std::map symbols{ + {Input::Keys::N0, '0'}, + {Input::Keys::N1, '1'}, + {Input::Keys::N2, '2'}, + {Input::Keys::N3, '3'}, + {Input::Keys::N4, '4'}, + {Input::Keys::N5, '5'}, + {Input::Keys::N6, '6'}, + {Input::Keys::N7, '7'}, + {Input::Keys::N8, '8'}, + {Input::Keys::N9, '9'}, + {Input::Keys::SPACE, ' '}, + }; + for (int letter = Input::Keys::A; letter <= Input::Keys::Z; ++letter) { + if (Input::IsRawKeyTriggered((Input::Keys::InputKey)letter)) { + value.push_back(letter + offset + (shift ? 0 : uppercase)); + } + } + if (Input::IsRawKeyTriggered(Input::Keys::BACKSPACE)) { + value.clear(); + } + Refresh(); +} diff --git a/src/window_stringinput.h b/src/window_stringinput.h new file mode 100644 index 000000000..3660e9936 --- /dev/null +++ b/src/window_stringinput.h @@ -0,0 +1,32 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef EP_WINDOW_STRINGINPUT_H +#define EP_WINDOW_STRINGINPUT_H + +#include "window_selectable.h" + +class Window_StringInput : public Window_Selectable { +public: + Window_StringInput(StringView initial_value, int ix, int iy, int iwidth = 320, int iheight = 80); + + void Refresh(); + void Update() override; + std::string value; +}; + +#endif From e228bf8ea64eaae23932e669724e1e7cd4965967 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Fri, 27 Dec 2024 01:48:48 -0500 Subject: [PATCH 10/18] Receive and display text composition; add WebView --- CMakeLists.txt | 20 +++-- src/baseui.h | 4 + src/config_param.h | 8 +- src/game_config.h | 6 +- src/input.cpp | 46 ++++++++++ src/input.h | 18 ++++ src/multiplayer/chat_overlay.cpp | 55 +++++------- src/multiplayer/chat_overlay.h | 2 +- src/multiplayer/game_multiplayer.cpp | 94 ++++++++++---------- src/multiplayer/game_multiplayer.h | 2 +- src/platform/sdl/main.cpp | 2 +- src/platform/sdl/sdl2_ui.cpp | 127 +++++++++++++++++++++++---- src/platform/sdl/sdl2_ui.h | 12 +++ src/platform/ynoshell/main.cpp | 20 +++-- src/scene_settings.cpp | 4 + src/window_settings.cpp | 12 ++- src/window_settings.h | 1 + src/window_stringinput.cpp | 65 ++++++++------ src/window_stringinput.h | 5 ++ 19 files changed, 356 insertions(+), 147 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b60e64f70..97196a5b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ include(PlayerBuildType) include(PlayerMisc) include(GetGitRevisionDescription) include(CheckCSourceCompiles) +include(FetchContent) # Dependencies provided by CMake Presets option(PLAYER_FIND_ROOT_PATH_APPEND @@ -607,8 +608,16 @@ if(${PLAYER_SHELL} STREQUAL "yno") set(PLAYER_YNO TRUE) target_compile_definitions(${PROJECT_NAME} PUBLIC PLAYER_YNO) - player_find_package(NAME wxWidgets REQUIRED) - target_link_libraries(${PROJECT_NAME} wx::core wx::base) + #player_find_package(NAME wxWidgets REQUIRED) + #target_link_libraries(${PROJECT_NAME} wx::core wx::base) + #find_package(unofficial-webview2 CONFIG REQUIRED) + #target_link_libraries(${PROJECT_NAME} unofficial::webview2::webview2) + FetchContent_Declare(webview + GIT_REPOSITORY https://github.com/webview/webview + GIT_TAG 0.12.0) + FetchContent_MakeAvailable(webview) + target_link_libraries(${PROJECT_NAME} webview::core) + elseif(${PLAYER_SHELL} STREQUAL "none") # do nothing else() @@ -659,7 +668,8 @@ elseif(PLAYER_TARGET_PLATFORM STREQUAL "SDL2") TARGET SDL2::SDL2 REQUIRED) - if(TARGET SDL2::SDL2main AND ${PLAYER_SHELL} STREQUAL "none") + if(TARGET SDL2::SDL2main #AND ${PLAYER_SHELL} STREQUAL "none" + ) target_link_libraries(${PROJECT_NAME} SDL2::SDL2main) endif() @@ -1297,8 +1307,8 @@ if(PLAYER_BUILD_EXECUTABLE AND ${PLAYER_TARGET_PLATFORM} MATCHES "^SDL.*$" AND N set(EXE_NAME ${PROJECT_NAME}_exe) if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") add_executable(${EXE_NAME} "src/platform/emscripten/main.cpp") - elseif(PLAYER_YNO) - add_executable(${EXE_NAME} "src/platform/ynoshell/main.cpp") + #elseif(PLAYER_YNO) + #add_executable(${EXE_NAME} "src/platform/ynoshell/main.cpp") else() add_executable(${EXE_NAME} "src/platform/sdl/main.cpp") endif() diff --git a/src/baseui.h b/src/baseui.h index 67380afca..bc75af15a 100644 --- a/src/baseui.h +++ b/src/baseui.h @@ -274,6 +274,10 @@ class BaseUi { */ Game_ConfigVideo GetConfig() const; + /** Enables receiving input from IME. Meant to be paired with EndTextCapture. */ + virtual void BeginTextCapture() {} + virtual void EndTextCapture() {} + protected: /** * Protected Constructor. Use CreateUi instead. diff --git a/src/config_param.h b/src/config_param.h index 9398ab149..ff0b99919 100644 --- a/src/config_param.h +++ b/src/config_param.h @@ -249,7 +249,13 @@ class LockedConfigParam final : public ConfigParam { } }; -using StringConfigParam = ConfigParam; +class StringConfigParam : public ConfigParam { +public: + explicit StringConfigParam( + StringView name, StringView description, StringView config_section, StringView config_key, std::string value = "", bool secret = false) : + ConfigParam(name, description, config_section, config_key, value), secret(secret) {} + bool secret; +}; /** A configuration parameter with a range */ template diff --git a/src/game_config.h b/src/game_config.h index 5962f1f88..0a8b0cfdb 100644 --- a/src/game_config.h +++ b/src/game_config.h @@ -150,8 +150,6 @@ struct Game_ConfigVideo { ConfigParam window_width{ "", "", "Video", "WindowWidth", -1 }; ConfigParam window_height{ "", "", "Video", "WindowHeight", -1 }; - void* foreign_window_handle = nullptr; - void Hide(); }; @@ -180,9 +178,9 @@ struct Game_ConfigInput { struct Game_ConfigOnline { StringConfigParam session_token{ "", "", "Online", "SessionToken" }; - StringConfigParam username{ "Username", "Your chat name or YNOproject account; max 15 characters", "Online", "Username" }; + StringConfigParam username{ "Username", "Chat nickname or YNOproject account; max 12 characters", "Online", "Username" }; // not persisted - StringConfigParam password{ "Password", "Your YNOproject password", "", "" }; + StringConfigParam password{ "Password", "YNOproject password", "", "", "", true }; EnumConfigParam nametag_mode{ "Nametags", "Change the nametag display style", "Online", "Nametags", /*default: */ConfigEnum::NametagMode::CLASSIC, Utils::MakeSvArray("None", "Classic", "Compact", "Slim"), diff --git a/src/input.cpp b/src/input.cpp index 00aa90ca7..8d31967c9 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -24,6 +24,8 @@ #include "player.h" #include "system.h" #include "baseui.h" +#include "bitmap.h" +#include "cache.h" #include #include @@ -49,6 +51,8 @@ namespace Input { std::array press_time; std::bitset triggered, repeated, released; Input::KeyStatus raw_triggered, raw_pressed, raw_released; + Composition composition{}; + std::string text_input; int dir4; int dir8; std::unique_ptr source; @@ -432,3 +436,45 @@ void Input::SimulateButtonPress(Input::InputButton button) { } UpdateButton(button, true); } + +int Input::RenderTextComposition(Bitmap& contents, int offset, int y, Font* font_) { + auto& font = font_ ? *font_ : *Font::Default(); + StringView text(Input::composition.text.data()); + std::u32string text32; + auto iter = text.data(); + auto end = iter + text.size(); + + while (iter != text.end()) { + auto ret = Utils::TextNext(iter, end, 0); + iter = ret.next; + if (!ret) continue; + + // TODO: handle exfont, control characters + if (Utils::IsControlCharacter(ret.ch) || ret.is_exfont) + continue; + text32.push_back(ret.ch); + } + + if (!text32.empty()) { + auto shape = font.Shape(text32); + double zoom = font.GetScaleRatio(); + auto& system = *Cache::SystemOrBlack(); + for (int i = 0; i < shape.size(); ++i) { + auto& ch = shape[i]; + if (i >= Input::composition.sel_start - 1 && i <= Input::composition.sel_start - 1 + Input::composition.sel_length) { + // white on offblue + contents.FillRect({ offset, y, (int)ceilf(ch.advance.x * zoom), (int)ceilf(ch.advance.y * zoom) }, Color(100, 100, 255, 255)); + offset += font.Render(contents, offset, y, Color(255, 255, 255, 255), ch.code).x; + } + else { + // black on white + contents.FillRect({ offset, y, (int)ceilf(ch.advance.x * zoom), (int)ceilf(ch.advance.y * zoom) }, Color(255, 255, 255, 255)); + offset += font.Render(contents, offset, y, Color(0, 0, 0, 255), ch.code).x; + } + } + } + + composition.active = false; + composition.text.clear(); + return offset; +} diff --git a/src/input.h b/src/input.h index ec96215c3..14ff31529 100644 --- a/src/input.h +++ b/src/input.h @@ -341,6 +341,24 @@ namespace Input { bool IsWaitingInput(); void WaitInput(bool val); + + struct Composition { + public: + /** if false, all other fields are considered garbage */ + bool active = false; + std::string text; + int sel_start; + int sel_length; + }; + + /** The current composition received from IME */ + extern Composition composition; + /** The text input received in this frame */ + //extern std::array text_input; + extern std::string text_input; + + /** `composition` has to be active before calling this function */ + int RenderTextComposition(Bitmap& contents, int offset, int y, Font* font_ = nullptr); } #endif diff --git a/src/multiplayer/chat_overlay.cpp b/src/multiplayer/chat_overlay.cpp index ac067357a..0e6b6c050 100644 --- a/src/multiplayer/chat_overlay.cpp +++ b/src/multiplayer/chat_overlay.cpp @@ -22,6 +22,7 @@ #include #include #include + using json = nlohmann::json; #include "chat_overlay.h" @@ -36,6 +37,7 @@ using json = nlohmann::json; #include "multiplayer/game_multiplayer.h" #include "multiplayer/scene_overlay.h" #include "messages.h" +#include "input.h" namespace { bool LargeScreen() { @@ -141,12 +143,14 @@ void ChatOverlay::Draw(Bitmap& dst) { int y = y_end - (lidx + 1) * text_height; bitmap->Blit(0, y, *black, black->GetRect(), 200); if (GMI().CanChat()) { - int prompt_width = Text::Draw(*bitmap, offset, y, font, offwhite, ">").x; + int prompt_width = 0; + prompt_width = Text::Draw(*bitmap, offset, y, font, offwhite, ">").x; offset += prompt_width; - if (!input.empty()) - offset += Text::Draw(*bitmap, offset, y, font, offwhite, input).x; + for (const auto& ch : input) + offset += Text::Draw(*bitmap, offset, y, font, offwhite, ch, false).x; // cursor bitmap->FillRect({ offset, y + text_height - 3, prompt_width, 3 }, offwhite); + offset += Input::RenderTextComposition(*bitmap, offset, y, &font); } else { Text::Draw(*bitmap, offset, y, font, Color(160, 160, 160, 255), @@ -329,47 +333,26 @@ void ChatOverlay::UpdateScene() { } if (!GMI().CanChat()) return; + // chat input only below this point - constexpr int offset = U'A' - Input::Keys::A; - constexpr int uppercase = U'a' - U'A'; - bool shift = Input::IsRawKeyPressed(Input::Keys::LSHIFT) || Input::IsRawKeyPressed(Input::Keys::RSHIFT); - static std::map symbols{ - {Input::Keys::N0, '0'}, - {Input::Keys::N1, '1'}, - {Input::Keys::N2, '2'}, - {Input::Keys::N3, '3'}, - {Input::Keys::N4, '4'}, - {Input::Keys::N5, '5'}, - {Input::Keys::N6, '6'}, - {Input::Keys::N7, '7'}, - {Input::Keys::N8, '8'}, - {Input::Keys::N9, '9'}, - {Input::Keys::SPACE, ' '}, - //{Input::Keys::SEMICOLON, ';'}, - }; - for (int letter = Input::Keys::A; letter <= Input::Keys::Z; ++letter) { - if (Input::IsRawKeyTriggered((Input::Keys::InputKey)letter)) { - input.push_back(letter + offset + (shift ? 0 : uppercase)); - dirty = true; - } - } - if (Input::IsRawKeyTriggered(Input::Keys::SEMICOLON)) { - input.push_back(shift ? ':' : ';'); + StringView ui_input(Input::text_input.data()); + if (!ui_input.empty()) { + input.append(Utils::DecodeUTF32(ui_input)); dirty = true; + Input::text_input.clear(); } - for (auto& [key, chara] : symbols) { - if (Input::IsRawKeyTriggered(key)) { - input.push_back(chara); - dirty = true; - } + else if (Input::composition.active) { + dirty = true; } + if (IsTriggeredOrRepeating(CustomKeyTimers::backspace) && !input.empty()) { input.pop_back(); dirty = true; } if (Input::IsRawKeyTriggered(Input::Keys::RETURN) && !input.empty()) { - GMI().sessionConn.SendPacketAsync(input); + std::string encoded = Utils::EncodeUTF(input); + GMI().sessionConn.SendPacketAsync(std::move(encoded)); input.clear(); dirty = true; } @@ -387,10 +370,14 @@ void ChatOverlay::SetShowAll(bool show_all) { ((Scene_Overlay*)Scene::instance.get())->SetOnUpdate([this] { UpdateScene(); }); else Scene::Push(std::make_shared([this] { UpdateScene(); })); + DisplayUi->BeginTextCapture(); } + else + DisplayUi->EndTextCapture(); } void ChatOverlay::DoScroll(int increase) { + if (messages.empty()) return; scroll += increase; scroll = (scroll + messages.size()) % messages.size(); counter = 0; diff --git a/src/multiplayer/chat_overlay.h b/src/multiplayer/chat_overlay.h index d8cfcd059..a7915f932 100644 --- a/src/multiplayer/chat_overlay.h +++ b/src/multiplayer/chat_overlay.h @@ -120,7 +120,7 @@ class ChatOverlay : public Drawable { int scroll = 0; BitmapRef bitmap, black, grey, scrollbar; - std::string input; + std::u32string input; std::deque messages; diff --git a/src/multiplayer/game_multiplayer.cpp b/src/multiplayer/game_multiplayer.cpp index 8b468ae5c..9d2df4a02 100644 --- a/src/multiplayer/game_multiplayer.cpp +++ b/src/multiplayer/game_multiplayer.cpp @@ -506,50 +506,44 @@ void Game_Multiplayer::InitConnection() { Connect(room_id); // sync chat messages - if (on_chat_msg) { - std::string endpoint = fmt::format("https://connect.ynoproject.net/2kki/api/chathistory?globalMsgLimit={}&partyMsgLimit={}&lastMsgId={}", 200, 100, lastmsgid); - cpr::Response resp = cpr::Get(cpr::Url{endpoint}); - if (resp.status_code >= 200 && resp.status_code < 300) { - json chatmsgs = json::parse(resp.text); - for (auto& it : chatmsgs["players"]) { - auto& entry = playerdata[(std::string)it["uuid"]]; - entry.name = it["name"]; - entry.systemName = it["systemName"]; - entry.rank = it["rank"]; - entry.account = it["account"]; - entry.badge = it["badge"]; - } + std::string endpoint = fmt::format("https://connect.ynoproject.net/2kki/api/chathistory?globalMsgLimit={}&partyMsgLimit={}&lastMsgId={}", 200, 100, lastmsgid); cpr::Response resp = cpr::Get(cpr::Url{endpoint}); + if (resp.status_code >= 200 && resp.status_code < 300) { + json chatmsgs = json::parse(resp.text); + for (auto& it : chatmsgs["players"]) { + auto& entry = playerdata[(std::string)it["uuid"]]; + entry.name = it["name"]; + entry.systemName = it["systemName"]; + entry.rank = it["rank"]; + entry.account = it["account"]; + entry.badge = it["badge"]; + } - const auto& messages = chatmsgs["messages"]; - for (auto it = messages.begin(); it != messages.end(); ++it) { - std::string name = "Unknown Player"; - std::string system; - std::string badge; - bool account = false; - if (auto player = playerdata.find((std::string)(*it)["uuid"]); player != playerdata.end()) { - name = player->second.name; - system = player->second.systemName; - badge = player->second.badge; - account = player->second.account; - } - std::string contents((*it)["contents"]); - on_chat_msg({ - contents, name, system, badge, - /*syncing: */std::next(it) != messages.end(), - account, - }); - lastmsgid = (std::string)(*it)["msgId"]; - } - if (!username.empty()) { - if (session_token.empty()) - Graphics::GetChatOverlay().AddSystemMessage(fmt::format("Connected to YNOproject as <{}>.", username)); - else - Graphics::GetChatOverlay().AddSystemMessage(fmt::format("Connected to YNOproject as [{}].", username)); - } - else { - Graphics::GetChatOverlay().AddSystemMessage( - "Welcome to YNOproject! To join the chat, first set your name in the Online settings."); + const auto& messages = chatmsgs["messages"]; + auto& chatoverlay = Graphics::GetChatOverlay(); + for (auto it = messages.begin(); it != messages.end(); ++it) { + std::string name = "Unknown Player"; + std::string system; + std::string badge; + bool account = false; + if (auto player = playerdata.find((std::string)(*it)["uuid"]); player != playerdata.end()) { + name = player->second.name; + system = player->second.systemName; + badge = player->second.badge; + account = player->second.account; } + std::string contents((*it)["contents"]); + chatoverlay.AddMessage(contents, name, system, badge, account); + lastmsgid = (std::string)(*it)["msgId"]; + } + if (!username.empty()) { + if (session_token.empty()) + Graphics::GetChatOverlay().AddSystemMessage(fmt::format("Connected to YNOproject as <{}>.", username)); + else + Graphics::GetChatOverlay().AddSystemMessage(fmt::format("Connected to YNOproject as [{}].", username)); + } + else { + Graphics::GetChatOverlay().AddSystemMessage( + "Welcome to YNOproject! To join the chat, first set your name in the Online settings."); } } }); @@ -569,10 +563,8 @@ void Game_Multiplayer::InitConnection() { account = player->second.account; } Output::Debug("(gc) {}: {}", name, p.msg); - if (on_chat_msg) { - on_chat_msg({ p.msg, name, system, badge, account }); - lastmsgid = p.msgid; - } + Graphics::GetChatOverlay().AddMessage(p.msg, name, system, badge, account); + lastmsgid = p.msgid; }); sessionConn.RegisterHandler("say", [this](SessionSay& p) { std::string name = "Unknown Player"; @@ -587,9 +579,7 @@ void Game_Multiplayer::InitConnection() { account = player->second.account; } Output::Debug("(mc) {}: {}", name, p.msg); - if (on_chat_msg) { - on_chat_msg({ p.msg, name, system, badge, account, /*global: */false }); - } + Graphics::GetChatOverlay().AddMessage(p.msg, name, system, badge, account); }); sessionConn.RegisterHandler("p", [this](SessionPlayerInfo& p) { auto& player = playerdata[p.uuid]; @@ -1115,7 +1105,7 @@ void Game_Multiplayer::CheckLoginCallback(cpr::Response resp) { else { username = (std::string)body["name"]; cfg.username.Set(username); - Output::Debug("username: {}", username); + Graphics::GetChatOverlay().AddSystemMessage(fmt::format("Username set to {}.", username)); } Graphics::GetChatOverlay().MarkDirty(); } @@ -1131,6 +1121,7 @@ void Game_Multiplayer::Login(std::string_view username, std::string_view passwor if (resp.status_code >= 200 && resp.status_code < 300) { login_failure.clear(); session_token = resp.text; + this->username = username; cfg.session_token.Set(session_token); CheckLogin(false); ReconnectSession(); @@ -1143,7 +1134,9 @@ void Game_Multiplayer::Login(std::string_view username, std::string_view passwor void Game_Multiplayer::Logout() { session_token.clear(); + username.clear(); cfg.session_token.Set(""); + cfg.username.Set(""); ReconnectSession(); } @@ -1175,4 +1168,5 @@ void Game_Multiplayer::SetNickname(StringView name) { username = value; cfg.username.Set(value); Graphics::GetChatOverlay().MarkDirty(); + CheckLogin(); } diff --git a/src/multiplayer/game_multiplayer.h b/src/multiplayer/game_multiplayer.h index 795698e54..6cc576290 100644 --- a/src/multiplayer/game_multiplayer.h +++ b/src/multiplayer/game_multiplayer.h @@ -83,7 +83,7 @@ class Game_Multiplayer { bool account = true; bool global = true; }; - std::function on_chat_msg; + //std::function on_chat_msg; std::function on_system_graphic_change; std::string lastmsgid; std::map playerdata; diff --git a/src/platform/sdl/main.cpp b/src/platform/sdl/main.cpp index cf876f535..a95c22505 100644 --- a/src/platform/sdl/main.cpp +++ b/src/platform/sdl/main.cpp @@ -83,7 +83,7 @@ int main(int argc, char* argv[]) { EpAndroid::env = (JNIEnv*)SDL_GetAndroidJNIEnv(); #endif - //Output::SetLogLevel(LogLevel::Debug); + Output::SetLogLevel(LogLevel::Debug); Player::Init(std::move(args)); Player::Run(); diff --git a/src/platform/sdl/sdl2_ui.cpp b/src/platform/sdl/sdl2_ui.cpp index d9bd39253..c296cd3da 100644 --- a/src/platform/sdl/sdl2_ui.cpp +++ b/src/platform/sdl/sdl2_ui.cpp @@ -232,6 +232,10 @@ Sdl2Ui::~Sdl2Ui() { audio_.reset(); #endif + if (webview) { + webview->terminate(); + webview_thread.join(); + } SDL_Quit(); } @@ -356,9 +360,6 @@ bool Sdl2Ui::RefreshDisplayMode() { #endif // Create our window - if (vcfg.foreign_window_handle) { - sdl_window = SDL_CreateWindowFrom(vcfg.foreign_window_handle); - } else if (vcfg.window_x.Get() < 0 || vcfg.window_y.Get() < 0 || vcfg.window_height.Get() <= 0 || vcfg.window_width.Get() <= 0) { sdl_window = SDL_CreateWindow(GAME_TITLE, SDL_WINDOWPOS_CENTERED, @@ -450,22 +451,52 @@ bool Sdl2Ui::RefreshDisplayMode() { renderer_sg.Dismiss(); window_sg.Dismiss(); +#ifdef PLAYER_YNO + webview_thread = std::thread([hwnd, this] { + try { + webview = std::make_unique(true, nullptr); + auto& w = *webview; + w.set_title("YNOproject sidecar"); + w.set_size(window.width, window.height, WEBVIEW_HINT_NONE); + w.set_html(R"html()html"); + if (auto widget = w.window(); widget.ok()) { +#ifdef _WIN32 + HWND childHwnd = (HWND)widget.value(); + SetParent(childHwnd, hwnd); + SetWindowLongPtr(childHwnd, GWLP_HWNDPARENT, (long long)hwnd); + SetWindowLongPtr(childHwnd, GWL_STYLE, WS_OVERLAPPED | WS_CHILD); + //RECT rect{ 0, 0, window.width, window.height }; + //AdjustWindowRectEx(&rect, WS_OVERLAPPED | WS_CHILD, false, 0); + //SetWindowPos(childHwnd, nullptr, + // 0, 0, + // rect.right - rect.left, + // rect.bottom - rect.top, + // SWP_FRAMECHANGED | SWP_SHOWWINDOW); +#else +# error TODO: implement adopting webview as child +#endif + } + w.run(); + } + catch (const webview::exception& err) { + Output::Error("webview died: {}", (int)err.error().code()); + } + }); +#endif } else { // Browser handles fast resizing for emscripten, TODO: use fullscreen API #ifndef EMSCRIPTEN - if (!vcfg.foreign_window_handle) { - bool is_fullscreen = (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN_DESKTOP; - if (is_fullscreen) { - SDL_SetWindowFullscreen(sdl_window, SDL_WINDOW_FULLSCREEN_DESKTOP); + bool is_fullscreen = (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN_DESKTOP; + if (is_fullscreen) { + SDL_SetWindowFullscreen(sdl_window, SDL_WINDOW_FULLSCREEN_DESKTOP); + } else { + SDL_SetWindowFullscreen(sdl_window, 0); + if ((last_display_mode.flags & SDL_WINDOW_FULLSCREEN_DESKTOP) + == SDL_WINDOW_FULLSCREEN_DESKTOP) { + // Restore to pre-fullscreen size + SDL_SetWindowSize(sdl_window, 0, 0); } else { - SDL_SetWindowFullscreen(sdl_window, 0); - if ((last_display_mode.flags & SDL_WINDOW_FULLSCREEN_DESKTOP) - == SDL_WINDOW_FULLSCREEN_DESKTOP) { - // Restore to pre-fullscreen size - SDL_SetWindowSize(sdl_window, 0, 0); - } else { - SDL_SetWindowSize(sdl_window, display_width_zoomed, display_height_zoomed); - } + SDL_SetWindowSize(sdl_window, display_width_zoomed, display_height_zoomed); } } #endif @@ -479,6 +510,8 @@ bool Sdl2Ui::RefreshDisplayMode() { // Not using the enum names because this will fail to build when not using a recent Windows 11 SDK int window_rounding = 1; // DWMWCP_DONOTROUND DwmSetWindowAttribute(window, 33 /* DWMWA_WINDOW_CORNER_PREFERENCE */, &window_rounding, sizeof(window_rounding)); + auto styles = GetWindowLongPtr(hwnd, GWL_STYLE); + SetWindowLongPtr(hwnd, GWL_STYLE, styles | WS_CLIPCHILDREN); #endif uint32_t sdl_pixel_fmt = GetDefaultFormat(); @@ -679,12 +712,27 @@ void Sdl2Ui::UpdateDisplay() { viewport.y = border_y; viewport.h = win_height; viewport.w = static_cast(ceilf(main_surface->width() * window.scale)); - viewport.x = (win_width - viewport.w) / 2 + border_x; + viewport.x = (win_width - viewport.w - 480) / 2 + border_x; do_stretch(); SDL_RenderSetViewport(sdl_renderer, &viewport); } - // TODO: Remove the division by 2 to render at actual size + if (webview) { +#ifdef _WIN32 + HWND hwnd = (HWND&)webview->window(); + + RECT rect{ 0, 0, 480, window.height }; + AdjustWindowRectEx(&rect, WS_OVERLAPPED | WS_CHILD, false, 0); + SetWindowPos(hwnd, nullptr, + window.width - 480, 0, + rect.right - rect.left, + rect.bottom - rect.top, + SWP_FRAMECHANGED | SWP_SHOWWINDOW); +#else +# error unimplemented +#endif + } + screen_surface = Bitmap::Create(viewport.w, viewport.h, true, main_surface->pitch()); if (vcfg.scaling_mode.Get() == ConfigEnum::ScalingMode::Bilinear && @@ -774,6 +822,18 @@ bool Sdl2Ui::HandleErrorOutput(const std::string &message) { return true; } +void Sdl2Ui::BeginTextCapture() { + SDL_SetHint(SDL_HINT_IME_SUPPORT_EXTENDED_TEXT, "1"); + SDL_RaiseWindow(sdl_window); + SDL_StartTextInput(); + Output::Debug("begin capture"); +} + +void Sdl2Ui::EndTextCapture() { + SDL_StopTextInput(); + Output::Debug("end capture"); +} + void Sdl2Ui::ProcessEvent(SDL_Event &evnt) { switch (evnt.type) { case SDL_WINDOWEVENT: @@ -827,6 +887,12 @@ void Sdl2Ui::ProcessEvent(SDL_Event &evnt) { case SDL_FINGERMOTION: ProcessFingerEvent(evnt); return; + + case SDL_TEXTINPUT: + case SDL_TEXTEDITING: + case SDL_TEXTEDITING_EXT: + ProcessTextEditEvent(evnt); + break; } } @@ -1097,6 +1163,33 @@ void Sdl2Ui::ProcessFingerEvent(SDL_Event& evnt) { #endif } +void Sdl2Ui::ProcessTextEditEvent(SDL_Event& event) { + if (event.type != SDL_TEXTINPUT) { + auto& composition = Input::composition; + composition.text.clear(); + + if (event.type == SDL_TEXTEDITING_EXT) { + composition.text.append(event.editExt.text); + SDL_free(event.editExt.text); + } + else composition.text.append(event.edit.text); + Output::Debug("`{}` {} {} ext={} win={}", composition.text, event.editExt.start, event.editExt.length, (int)(event.type == SDL_TEXTEDITING_EXT), event.editExt.windowID); + + if (composition.text.empty()) { + composition.active = false; + return; + } + + composition.active = true; + composition.sel_start = event.editExt.start; + composition.sel_length = event.editExt.length; + return; + } + + Input::text_input.clear(); + Input::text_input.append(event.text.text); +} + void Sdl2Ui::SetAppIcon() { #if defined(__MORPHOS__) // do nothing diff --git a/src/platform/sdl/sdl2_ui.h b/src/platform/sdl/sdl2_ui.h index 3afaa2203..69e3f75ad 100644 --- a/src/platform/sdl/sdl2_ui.h +++ b/src/platform/sdl/sdl2_ui.h @@ -27,6 +27,10 @@ #include #include +#ifdef PLAYER_YNO +# include +#endif + extern "C" { union SDL_Event; struct SDL_Texture; @@ -79,6 +83,10 @@ class Sdl2Ui final : public BaseUi { AudioInterface& GetAudio() override; #endif + /** begin populating Input::text_input and Input::composition */ + void BeginTextCapture() override; + void EndTextCapture() override; + /** @} */ private: @@ -110,6 +118,7 @@ class Sdl2Ui final : public BaseUi { void ProcessControllerButtonEvent(SDL_Event &evnt); void ProcessControllerAxisEvent(SDL_Event &evnt); void ProcessFingerEvent(SDL_Event & evnt); + void ProcessTextEditEvent(SDL_Event& evnt); /** @} */ @@ -150,6 +159,9 @@ class Sdl2Ui final : public BaseUi { #ifdef SUPPORT_AUDIO std::unique_ptr audio_; #endif + + std::unique_ptr webview; + std::thread webview_thread; }; #endif diff --git a/src/platform/ynoshell/main.cpp b/src/platform/ynoshell/main.cpp index 26d9150fd..5494adeaf 100644 --- a/src/platform/ynoshell/main.cpp +++ b/src/platform/ynoshell/main.cpp @@ -11,10 +11,13 @@ #include "multiplayer/chat_overlay.h" #include "multiplayer/game_multiplayer.h" +class YnoFrame; + class YnoApp : public wxApp { public: bool OnInit() override; + YnoFrame* frame; }; //wxIMPLEMENT_APP(YnoApp); @@ -65,21 +68,22 @@ class YnoFrame : public wxFrame private: void OnKeyDownFrame(wxKeyEvent& event); + void OnFocus(wxActivateEvent& event); }; bool YnoApp::OnInit() { - YnoFrame* frame = new YnoFrame(); + frame = new YnoFrame(); frame->Show(true); - wxWindow* child = frame->gameWindow; + YnoGameContainer* child = frame->gameWindow; if (!child) { Output::Error("no child frame"); return false; } - Player::did_parse_config = [handle=child->GetHandle()](Game_Config& cfg) { + Player::did_parse_config = [handle=child->GetHWND()](Game_Config& cfg) { cfg.video.foreign_window_handle = (void*)handle; }; @@ -88,7 +92,7 @@ bool YnoApp::OnInit() Player::Init(std::move(args)); Player::Run(); - return true; + exit(Player::exit_code); } YnoFrame::YnoFrame() @@ -96,7 +100,7 @@ YnoFrame::YnoFrame() { wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL); - gameWindow = new YnoGameContainer(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE); + gameWindow = new YnoGameContainer(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE | wxWANTS_CHARS); gameWindow->Bind(wxEVT_KEY_DOWN, &YnoFrame::OnKeyDownFrame, this); sizer->Add((wxWindow*)gameWindow, 1, wxEXPAND); @@ -104,9 +108,15 @@ YnoFrame::YnoFrame() Graphics::GetChatOverlay().AddMessage(msg.content, msg.sender, msg.system, msg.badge, msg.account); }; + Bind(wxEVT_ACTIVATE, &YnoFrame::OnFocus, this); + SetSizer(sizer); }; +void YnoFrame::OnFocus(wxActivateEvent& event) { + event.Skip(); +} + void YnoFrame::OnKeyDownFrame(wxKeyEvent& event) { switch (event.GetKeyCode()) { case WXK_RETURN: diff --git a/src/scene_settings.cpp b/src/scene_settings.cpp index 07f786b24..549f4def4 100644 --- a/src/scene_settings.cpp +++ b/src/scene_settings.cpp @@ -272,6 +272,9 @@ void Scene_Settings::vUpdate() { help_window2->SetFont(nullptr); options_window->Pop(); + if (mode == Window_Settings::eOnlineAccount) { + SaveConfig(false); + } SetMode(options_window->GetMode()); if (mode == Window_Settings::eNone) { Scene::Pop(); @@ -452,6 +455,7 @@ void Scene_Settings::UpdateOptions() { string_window->SetZ(options_window->GetZ() + 1); string_window->SetOpacity(255); string_window->SetActive(true); + string_window->SetSecret(option.secret); options_window->SetActive(false); } } else { diff --git a/src/window_settings.cpp b/src/window_settings.cpp index 9047edc29..363bdd9a7 100644 --- a/src/window_settings.cpp +++ b/src/window_settings.cpp @@ -18,6 +18,7 @@ // Headers #include #include +#include #include "game_map.h" #include "input.h" #include "text.h" @@ -65,7 +66,12 @@ void Window_Settings::DrawOption(int index) { Font::SystemColor color = enabled ? option.color : Font::ColorDisabled; contents->TextDraw(rect, color, option.text); - contents->TextDraw(rect, color, option.value_text, Text::AlignRight); + if (!option.secret) + contents->TextDraw(rect, color, option.value_text, Text::AlignRight); + else { + std::string value(std::min((size_t)10, option.value_text.size()), '*'); + contents->TextDraw(rect, color, value, Text::AlignRight); + } } Window_Settings::StackFrame& Window_Settings::GetFrame(int n) { @@ -288,6 +294,7 @@ void Window_Settings::AddOption(const StringConfigParam& param, Action&& action) opt.help = ToString(param.GetDescription()); opt.value_text = param.Get(); opt.mode = eOptionStringInput; + opt.secret = param.secret; if (!param.IsLocked()) opt.action = std::forward(action); GetFrame().options.push_back(std::move(opt)); @@ -791,10 +798,9 @@ void Window_Settings::RefreshOnline() { AddOption(cfg.username, [this, &cfg] { auto& value = GetCurrentOption().value_text; - cfg.username.Set(value); if (!value.empty() && GMI().sessionConn.IsConnected() && Input::IsRawKeyTriggered(Input::Keys::RETURN)) { + cfg.username.Set(value); GMI().SetNickname(value); - //Output::Debug("set: {}", value); } }); AddOption(cfg.password, [this, &cfg] { diff --git a/src/window_settings.h b/src/window_settings.h index f784a234c..48132b0fd 100644 --- a/src/window_settings.h +++ b/src/window_settings.h @@ -77,6 +77,7 @@ class Window_Settings : public Window_Selectable { int original_value; int min_value; int max_value; + bool secret = false; std::vector options_index; std::vector options_text; std::vector options_help; diff --git a/src/window_stringinput.cpp b/src/window_stringinput.cpp index 7413b4351..5da346a59 100644 --- a/src/window_stringinput.cpp +++ b/src/window_stringinput.cpp @@ -18,6 +18,9 @@ #include "window_stringinput.h" #include "input.h" #include "bitmap.h" +#include "baseui.h" +#include "cache.h" +#include "output.h" Window_StringInput::Window_StringInput(StringView initial_value, int ix, int iy, int iwidth, int iheight) : Window_Selectable(ix, iy, iwidth, iheight), value(initial_value) @@ -28,12 +31,34 @@ Window_StringInput::Window_StringInput(StringView initial_value, int ix, int iy, opacity = 0; active = false; + DisplayUi->BeginTextCapture(); + + Refresh(); +} + +Window_StringInput::~Window_StringInput() { + Window_Selectable::~Window_Selectable(); + DisplayUi->EndTextCapture(); +} + +void Window_StringInput::SetSecret(bool secret) { + this->secret = secret; Refresh(); } void Window_StringInput::Refresh() { contents->Clear(); - contents->TextDraw(0, 2, Font::ColorDefault, value); + int offset = 0; + const int y = 2; + if (!secret) + offset += contents->TextDraw(0, y, Font::ColorDefault, value).x; + else { + std::string masked(value.size(), '*'); + offset += contents->TextDraw(0, y, Font::ColorDefault, masked).x; + } + if (Input::composition.active) { + offset += Input::RenderTextComposition(*contents, offset, y, this->font.get()); + } contents->FillRect(GetCursorRect(), Color(255, 255, 255, 200)); } @@ -41,31 +66,21 @@ void Window_StringInput::Update() { Window_Selectable::Update(); if (!active) return; - bool dirty = true; + bool dirty = false; - constexpr int offset = U'A' - Input::Keys::A; - constexpr int uppercase = U'a' - U'A'; - bool shift = Input::IsRawKeyPressed(Input::Keys::LSHIFT) || Input::IsRawKeyPressed(Input::Keys::RSHIFT); - static std::map symbols{ - {Input::Keys::N0, '0'}, - {Input::Keys::N1, '1'}, - {Input::Keys::N2, '2'}, - {Input::Keys::N3, '3'}, - {Input::Keys::N4, '4'}, - {Input::Keys::N5, '5'}, - {Input::Keys::N6, '6'}, - {Input::Keys::N7, '7'}, - {Input::Keys::N8, '8'}, - {Input::Keys::N9, '9'}, - {Input::Keys::SPACE, ' '}, - }; - for (int letter = Input::Keys::A; letter <= Input::Keys::Z; ++letter) { - if (Input::IsRawKeyTriggered((Input::Keys::InputKey)letter)) { - value.push_back(letter + offset + (shift ? 0 : uppercase)); - } + //StringView input(Input::text_input.data()); + if (!Input::text_input.empty()) { + value.append(Input::text_input); + dirty = true; + Input::text_input.clear(); + } else if (Input::composition.active) { + dirty = true; } - if (Input::IsRawKeyTriggered(Input::Keys::BACKSPACE)) { - value.clear(); + + if (Input::IsRawKeyTriggered(Input::Keys::BACKSPACE) && !value.empty()) { + value.pop_back(); + dirty = true; } - Refresh(); + if (dirty) + Refresh(); } diff --git a/src/window_stringinput.h b/src/window_stringinput.h index 3660e9936..502ec45ba 100644 --- a/src/window_stringinput.h +++ b/src/window_stringinput.h @@ -23,10 +23,15 @@ class Window_StringInput : public Window_Selectable { public: Window_StringInput(StringView initial_value, int ix, int iy, int iwidth = 320, int iheight = 80); + ~Window_StringInput(); void Refresh(); void Update() override; std::string value; + + void SetSecret(bool secret); +private: + bool secret; }; #endif From d952f7277c8f39644b07473f870161d8f691f323 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Fri, 27 Dec 2024 17:50:58 -0500 Subject: [PATCH 11/18] Fix requests handling, adjust webview --- src/async_handler.cpp | 38 +++++++++++++--- src/multiplayer/game_multiplayer.cpp | 65 +++++++++++++++++++++++++++- src/multiplayer/game_multiplayer.h | 2 + src/platform/sdl/sdl2_ui.cpp | 30 ++++++------- src/scene_save.cpp | 3 ++ src/scene_settings.cpp | 2 +- src/window_settings.cpp | 19 +++++--- 7 files changed, 128 insertions(+), 31 deletions(-) diff --git a/src/async_handler.cpp b/src/async_handler.cpp index 25b9773b9..e51541dec 100644 --- a/src/async_handler.cpp +++ b/src/async_handler.cpp @@ -31,6 +31,7 @@ using json = nlohmann::json; # include # include # include "platform.h" +# include "multiplayer/game_multiplayer.h" # define EP_CONTAINER_OF(ptr, type, member) (type*)((char*)ptr - offsetof(type, member)) # if defined(_WIN32) # define timegm _mkgmtime @@ -59,6 +60,20 @@ namespace { int index_version = 1; int64_t db_lastwrite = LLONG_MAX; + std::vector> session_pool; + + std::unique_ptr AcquireSession() { + if (session_pool.empty()) + return std::make_unique(); + std::unique_ptr out; + out.swap(session_pool.back()); + session_pool.pop_back(); + return out; + } + void ReleaseSession(std::unique_ptr&& session) { + session_pool.push_back(std::move(session)); + } + FileRequestAsync* GetRequest(const std::string& path) { auto it = async_requests.find(path); @@ -164,9 +179,18 @@ namespace { url_ += sobj->GetRequestExtension(); path_ += sobj->GetRequestExtension(); } - std::ofstream out(path_, std::ios::binary); - auto resp = cpr::Download(out, cpr::Url{ url_ }); + Platform::File handle(FileFinder::MakeCanonical(fmt::format("{}/..", path_), 0)); + handle.MakeDirectory(true); + + std::ofstream out(std::filesystem::u8path(path_), std::ios::binary); + + auto session = AcquireSession(); + session->SetUrl(cpr::Url{ url_ }); + auto resp = session->Download(out); + out.flush(); + ReleaseSession(std::move(session)); + ctx->http_status = resp.status_code; if (resp.status_code == 0) { ctx->http_status = 1000 + (int)resp.error.code; @@ -216,13 +240,13 @@ namespace { void AsyncHandler::CreateRequestMapping(const std::string& file) { auto f = FileFinder::Game().OpenInputStream(file); if (!f) { - Output::Error("Emscripten: Reading index.json failed"); + Output::Error("Reading index.json failed"); return; } json j = json::parse(f, nullptr, false); if (j.is_discarded()) { - Output::Error("Emscripten: index.json is not a valid JSON file"); + Output::Error("index.json is not a valid JSON file"); return; } @@ -303,7 +327,6 @@ FileRequestAsync* AsyncHandler::RequestFile(std::string_view folder_name, std::s return request; } - //Output::Debug("Waiting for {}", path); Web_API::OnRequestFile(path); return RegisterRequest(std::move(path), std::string(folder_name), std::string(file_name)); @@ -349,6 +372,9 @@ void AsyncHandler::SaveFilesystem(int slot_id) { onSaveSlotUpdated($0); }); }, slot_id); +#elif defined(PLAYER_YNO) + if (slot_id != 1) return; + GMI().UploadSaveFile(); #endif } @@ -541,14 +567,12 @@ void FileRequestAsync::DownloadDone(bool success) { } if (success) { -#ifdef __EMSCRIPTEN__ if (state == State_Pending) { // Update directory structure (new file was added) if (FileFinder::Game()) { FileFinder::Game().ClearCache(); } } -#endif state = State_DoneSuccess; diff --git a/src/multiplayer/game_multiplayer.cpp b/src/multiplayer/game_multiplayer.cpp index 9d2df4a02..fce8fd496 100644 --- a/src/multiplayer/game_multiplayer.cpp +++ b/src/multiplayer/game_multiplayer.cpp @@ -51,7 +51,11 @@ # include "scene_settings.h" # include "chat_overlay.h" # include "graphics.h" - +# include "platform.h" +# include +# if defined(_WIN32) && !defined(timegm) +# define timegm _mkgmtime +# endif using namespace std::literals::chrono_literals; using json = nlohmann::json; #endif @@ -1066,6 +1070,8 @@ void Game_Multiplayer::Update() { void Game_Multiplayer::SetConfig(const Game_ConfigOnline& cfg) { this->cfg = cfg; session_token = cfg.session_token.Get(); + if (!session_token.empty()) + SyncSaveFile(); SetNametagMode((int)cfg.nametag_mode.Get()); if (!cfg.username.Get().empty()) SetNickname(cfg.username.Get()); @@ -1105,7 +1111,6 @@ void Game_Multiplayer::CheckLoginCallback(cpr::Response resp) { else { username = (std::string)body["name"]; cfg.username.Set(username); - Graphics::GetChatOverlay().AddSystemMessage(fmt::format("Username set to {}.", username)); } Graphics::GetChatOverlay().MarkDirty(); } @@ -1170,3 +1175,59 @@ void Game_Multiplayer::SetNickname(StringView name) { Graphics::GetChatOverlay().MarkDirty(); CheckLogin(); } + +void Game_Multiplayer::SyncSaveFile() const { + cpr::Header header; + header["Authorization"] = session_token; + auto timestampResp = cpr::Get(cpr::Url{"https://connect.ynoproject.net/2kki/api/savesync?command=timestamp"}, header); + if (!(timestampResp.status_code >= 200 && timestampResp.status_code < 300)) + return Output::Warning("failed to get server save timestamp: {}", timestampResp.status_code); + + std::istringstream ss(timestampResp.text); + std::tm timestamp; + ss >> std::get_time(×tamp, "%Y-%m-%dT%H:%M:%SZ"); // RFC3339 + if (ss.fail()) + return Output::Warning("could not parse save timestamp: `{}`", timestampResp.text); + time_t last_synced = timegm(×tamp); + + const char* path = "Save01.lsd"; + Platform::File savefile(path); + if (savefile.GetLastModified() >= last_synced) + return Output::Info("Save slot 1 is up to date."); + + std::ofstream output(path, std::ios::binary); + auto resp = cpr::Download(output, cpr::Url{"https://connect.ynoproject.net/2kki/api/savesync?command=get"}, header); + if (resp.status_code >= 200 && resp.status_code < 300) + Output::Info("Save slot 1 has been updated."); + else + Output::Warning("failed to update save slot 1: {}", resp.status_code); +} + +void Game_Multiplayer::UploadSaveFile() const { + if (session_token.empty()) return; + + const char* path = "Save01.lsd"; + Platform::File savefile(path); + if (!savefile.Exists()) + return Output::Warning("bug: no save file to upload!"); + + cpr::Header header; + header["Authorization"] = session_token; + + //auto stream = FileFinder::Save().OpenFile(path); + //size_t size = stream.GetSize(); + //std::string data(size, '\0'); + //stream.read(&data[0], size); + //if (stream.fail() || stream.bad()) + // return Output::Warning("failed to read save file for uploading"); + + auto resp = cpr::Post( + cpr::Url{ "https://connect.ynoproject.net/2kki/api/savesync?command=push" }, header, + //cpr::Body{ data } + cpr::Body{ cpr::File{path} } + ); + if (resp.status_code >= 200 && resp.status_code < 300) + Output::Info("Uploaded save slot 1. ({} bytes up)", resp.uploaded_bytes); + else + Output::Warning("failed to upload save slot 1: {}", resp.status_code); +} diff --git a/src/multiplayer/game_multiplayer.h b/src/multiplayer/game_multiplayer.h index 6cc576290..f377392fb 100644 --- a/src/multiplayer/game_multiplayer.h +++ b/src/multiplayer/game_multiplayer.h @@ -132,6 +132,8 @@ class Game_Multiplayer { Game_ConfigOnline& GetConfig() noexcept; void SetConfig(const Game_ConfigOnline& cfg); + void SyncSaveFile() const; + void UploadSaveFile() const; std::string GetSessionEndpoint() const; /** Logs the player out if the session token is no longer valid */ void CheckLogin(bool async = true); diff --git a/src/platform/sdl/sdl2_ui.cpp b/src/platform/sdl/sdl2_ui.cpp index c296cd3da..41f4903fc 100644 --- a/src/platform/sdl/sdl2_ui.cpp +++ b/src/platform/sdl/sdl2_ui.cpp @@ -454,7 +454,7 @@ bool Sdl2Ui::RefreshDisplayMode() { #ifdef PLAYER_YNO webview_thread = std::thread([hwnd, this] { try { - webview = std::make_unique(true, nullptr); + webview = std::make_unique(false, nullptr); auto& w = *webview; w.set_title("YNOproject sidecar"); w.set_size(window.width, window.height, WEBVIEW_HINT_NONE); @@ -463,15 +463,16 @@ bool Sdl2Ui::RefreshDisplayMode() { #ifdef _WIN32 HWND childHwnd = (HWND)widget.value(); SetParent(childHwnd, hwnd); - SetWindowLongPtr(childHwnd, GWLP_HWNDPARENT, (long long)hwnd); - SetWindowLongPtr(childHwnd, GWL_STYLE, WS_OVERLAPPED | WS_CHILD); - //RECT rect{ 0, 0, window.width, window.height }; - //AdjustWindowRectEx(&rect, WS_OVERLAPPED | WS_CHILD, false, 0); - //SetWindowPos(childHwnd, nullptr, - // 0, 0, - // rect.right - rect.left, - // rect.bottom - rect.top, - // SWP_FRAMECHANGED | SWP_SHOWWINDOW); + //auto styles = GetWindowLongPtr(childHwnd, GWL_STYLE); + auto styles = GW_CHILD; + SetWindowLongPtr(childHwnd, GWL_STYLE, styles); + RECT rect{ 0, 0, 800, window.height }; + AdjustWindowRectEx(&rect, styles, false, 0); + SetWindowPos(childHwnd, nullptr, + window.width - 800, 0, + rect.right - rect.left, + rect.bottom - rect.top, + SWP_FRAMECHANGED | SWP_HIDEWINDOW); #else # error TODO: implement adopting webview as child #endif @@ -510,8 +511,6 @@ bool Sdl2Ui::RefreshDisplayMode() { // Not using the enum names because this will fail to build when not using a recent Windows 11 SDK int window_rounding = 1; // DWMWCP_DONOTROUND DwmSetWindowAttribute(window, 33 /* DWMWA_WINDOW_CORNER_PREFERENCE */, &window_rounding, sizeof(window_rounding)); - auto styles = GetWindowLongPtr(hwnd, GWL_STYLE); - SetWindowLongPtr(hwnd, GWL_STYLE, styles | WS_CLIPCHILDREN); #endif uint32_t sdl_pixel_fmt = GetDefaultFormat(); @@ -712,7 +711,7 @@ void Sdl2Ui::UpdateDisplay() { viewport.y = border_y; viewport.h = win_height; viewport.w = static_cast(ceilf(main_surface->width() * window.scale)); - viewport.x = (win_width - viewport.w - 480) / 2 + border_x; + viewport.x = (win_width - viewport.w) / 2 + border_x; do_stretch(); SDL_RenderSetViewport(sdl_renderer, &viewport); } @@ -721,10 +720,9 @@ void Sdl2Ui::UpdateDisplay() { #ifdef _WIN32 HWND hwnd = (HWND&)webview->window(); - RECT rect{ 0, 0, 480, window.height }; - AdjustWindowRectEx(&rect, WS_OVERLAPPED | WS_CHILD, false, 0); + RECT rect{ 0, 0, 800, window.height }; SetWindowPos(hwnd, nullptr, - window.width - 480, 0, + window.width - 800, 0, rect.right - rect.left, rect.bottom - rect.top, SWP_FRAMECHANGED | SWP_SHOWWINDOW); diff --git a/src/scene_save.cpp b/src/scene_save.cpp index 95019d551..155e8ead0 100644 --- a/src/scene_save.cpp +++ b/src/scene_save.cpp @@ -155,6 +155,9 @@ bool Scene_Save::Save(std::ostream& os, int slot_id, bool prepare_save) { } auto lcf_engine = Player::IsRPG2k3() ? lcf::EngineVersion::e2k3 : lcf::EngineVersion::e2k; bool res = lcf::LSD_Reader::Save(os, save, lcf_engine, Player::encoding); +#ifndef EMSCRIPTEN + os.flush(); // save sync will read this same file again +#endif Main_Data::game_dynrpg->Save(slot_id); diff --git a/src/scene_settings.cpp b/src/scene_settings.cpp index 549f4def4..fa79ae6f5 100644 --- a/src/scene_settings.cpp +++ b/src/scene_settings.cpp @@ -273,7 +273,7 @@ void Scene_Settings::vUpdate() { help_window2->SetFont(nullptr); options_window->Pop(); if (mode == Window_Settings::eOnlineAccount) { - SaveConfig(false); + SaveConfig(true); } SetMode(options_window->GetMode()); if (mode == Window_Settings::eNone) { diff --git a/src/window_settings.cpp b/src/window_settings.cpp index 363bdd9a7..554e500d0 100644 --- a/src/window_settings.cpp +++ b/src/window_settings.cpp @@ -783,18 +783,21 @@ void Window_Settings::RefreshButtonList() { void Window_Settings::RefreshOnline() { Game_ConfigOnline& cfg = GMI().GetConfig(); + using LockedMenuItem = LockedConfigParam; + if (!cfg.username.Get().empty() && !cfg.session_token.Get().empty()) { - AddOption(MenuItem("Status", "", fmt::format("Logged in as {}", cfg.username.Get())), [] {}); + AddOption(LockedMenuItem("Status", "", fmt::format("Logged in as {}", cfg.username.Get())), [] {}); AddOption(MenuItem("Logout", "", ""), [this] { GMI().Logout(); Refresh(); }); + GetFrame().options.back().color = Font::ColorKnockout; } else { if (cfg.username.Get().empty()) - AddOption(MenuItem("Status", "", "Username not set"), [] {}); + AddOption(LockedMenuItem("Status", "", "Username not set"), [] {}); else - AddOption(MenuItem("Status", "", fmt::format("Playing as guest user")), [] {}); + AddOption(LockedMenuItem("Status", "", fmt::format("Playing as guest user")), [] {}); AddOption(cfg.username, [this, &cfg] { auto& value = GetCurrentOption().value_text; @@ -806,19 +809,25 @@ void Window_Settings::RefreshOnline() { AddOption(cfg.password, [this, &cfg] { cfg.password.Set(GetCurrentOption().value_text); }); - if (!GMI().login_failure.empty()) { - AddOption(MenuItem(GMI().login_failure, "", ""), [] {}); + auto& failure = GMI().login_failure; + if (!failure.empty()) { + if (failure.find("bad login") != std::string::npos) + AddOption(LockedMenuItem("Invalid username or password", "", ""), [] {}); + else + AddOption(LockedMenuItem("Could not login", "", failure), [] {}); } AddOption(MenuItem("Login", "", ""), [this, &cfg] { GMI().Login(cfg.username.Get(), cfg.password.Get()); cfg.password.Set(""); Refresh(); }); + GetFrame().options.back().color = Font::ColorKnockout; AddOption(MenuItem("Register", "", ""), [this, &cfg] { Output::Warning("Register not implemented"); cfg.password.Set(""); Refresh(); }); + GetFrame().options.back().color = Font::ColorKnockout; } AddOption(cfg.nametag_mode, [this, &cfg] { From e58a0dcc77ee02a68593077d0095fd45ec418554 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Thu, 2 Jan 2025 00:36:46 -0500 Subject: [PATCH 12/18] Chat icons, landing page, IME bug fixes --- CMakeLists.txt | 2 + resources/ynoicons.png | Bin 0 -> 561 bytes src/async_handler.cpp | 20 ++-- src/baseui.h | 20 +++- src/bitmap.cpp | 5 + src/bitmap.h | 1 + src/input_buttons.h | 9 +- src/input_buttons_desktop.cpp | 1 + src/multiplayer/chat_overlay.cpp | 164 ++++++++++++++++++++------- src/multiplayer/chat_overlay.h | 30 +++-- src/multiplayer/connection.cpp | 9 ++ src/multiplayer/connection.h | 4 + src/multiplayer/game_multiplayer.cpp | 71 +++++++----- src/multiplayer/game_multiplayer.h | 1 + src/multiplayer/scene_nexus.cpp | 124 ++++++++++++++++++++ src/multiplayer/scene_nexus.h | 39 +++++++ src/platform/sdl/sdl2_ui.cpp | 111 ++++++++++++++---- src/platform/sdl/sdl2_ui.h | 11 +- src/player.cpp | 16 ++- src/player.h | 1 - src/scene_logo.cpp | 24 +++- src/web_api.cpp | 119 ++++++++++++++++++- src/web_api.h | 3 + src/window_settings.cpp | 10 +- src/window_stringinput.cpp | 3 +- src/ynoicons.h | 50 ++++++++ 26 files changed, 709 insertions(+), 139 deletions(-) create mode 100644 resources/ynoicons.png create mode 100644 src/multiplayer/scene_nexus.cpp create mode 100644 src/multiplayer/scene_nexus.h create mode 100644 src/ynoicons.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 97196a5b0..4ee61dbc3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -530,6 +530,8 @@ if(NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") src/multiplayer/scene_overlay.cpp src/multiplayer/scene_online.h src/multiplayer/scene_online.cpp + src/multiplayer/scene_nexus.h + src/multiplayer/scene_nexus.cpp ) if(WIN32) target_link_libraries(${PROJECT_NAME} synchronization) # provides WaitOnAddress diff --git a/resources/ynoicons.png b/resources/ynoicons.png new file mode 100644 index 0000000000000000000000000000000000000000..4388738a88c53b4c9d6e0ae92be3224e12c70eea GIT binary patch literal 561 zcmeAS@N?(olHy`uVBq!ia0vp^bAXtKkr_y?6>#$fQjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`*#dk*T!Hle|NocXoPQU{;wb|fr1hxt`Q}{`DrEP ziAAXl0g0J;C3=3YAqu8?hI&T7_b_h*s@awr;hE;?sl~tnkOkFcXkY-6efLUN;@BCmbIy6XIEF}EPHteZ zV`H~tW8-UMQMm2f`0${08#@orCmwztY55ZyYLXIiR@5ITc))v@ae+dy%Yw@+*8AS` zG4VMZY!m0<<>C6|qra!-*XzhhLB0-6HT(^`jDJLI-;z;s>O<}MTay?>Q)(5@%YXmx zy8O+~_o4IU6<7{xH8wqEm2YTze~;1C_|5FNipmnBdB5jvJ$UFJgG<1{eU?fWKCArR z|9%;#k%1%I?>6?f*jZ=%?L1{4OwMQEVrp&dD}V5KV!F+Q1LqDLI^V!0<-j7%%piC( Vs#x&pYh{oxJzf1=);T3K0RRH-nl}Id literal 0 HcmV?d00001 diff --git a/src/async_handler.cpp b/src/async_handler.cpp index e51541dec..f43a34845 100644 --- a/src/async_handler.cpp +++ b/src/async_handler.cpp @@ -30,8 +30,10 @@ using json = nlohmann::json; # include # include # include +# include # include "platform.h" # include "multiplayer/game_multiplayer.h" +# include "player.h" # define EP_CONTAINER_OF(ptr, type, member) (type*)((char*)ptr - offsetof(type, member)) # if defined(_WIN32) # define timegm _mkgmtime @@ -61,8 +63,10 @@ namespace { int64_t db_lastwrite = LLONG_MAX; std::vector> session_pool; + std::mutex session_mutex{}; std::unique_ptr AcquireSession() { + std::lock_guard _guard(session_mutex); if (session_pool.empty()) return std::make_unique(); std::unique_ptr out; @@ -71,6 +75,7 @@ namespace { return out; } void ReleaseSession(std::unique_ptr&& session) { + std::lock_guard _guard(session_mutex); session_pool.push_back(std::move(session)); } @@ -315,7 +320,7 @@ void AsyncHandler::ClearRequests() { ++it; } } - async_requests.clear(); + db_lastwrite = LLONG_MAX; } FileRequestAsync* AsyncHandler::RequestFile(std::string_view folder_name, std::string_view file_name) { @@ -439,12 +444,11 @@ void FileRequestAsync::Start() { request_path = "https://ynoproject.net/data/"; # endif - //if (!Player::emscripten_game_name.empty()) { - // request_path += Player::emscripten_game_name + "/"; - //} else { - // TODO: get the current game + if (!Player::emscripten_game_name.empty()) { + request_path += Player::emscripten_game_name + "/"; + } else { request_path += "2kki/"; - //} + } std::string modified_path; if (index_version >= 2) { @@ -499,9 +503,7 @@ void FileRequestAsync::Start() { std::string request_file = (it != file_mapping.end() ? it->second : path); #ifndef EMSCRIPTEN - auto handle = Platform::File{std::string(request_file)}; - auto lastmodified = handle.GetLastModified(); - if (lastmodified >= db_lastwrite) { + if (Platform::File(request_path).GetLastModified() >= db_lastwrite) { DownloadDone(true); return; } diff --git a/src/baseui.h b/src/baseui.h index bc75af15a..09b9d6e5b 100644 --- a/src/baseui.h +++ b/src/baseui.h @@ -275,9 +275,23 @@ class BaseUi { Game_ConfigVideo GetConfig() const; /** Enables receiving input from IME. Meant to be paired with EndTextCapture. */ - virtual void BeginTextCapture() {} + virtual void BeginTextCapture(Rect* textbox = nullptr) {} virtual void EndTextCapture() {} + enum class Intent : char { + ToggleWebview, + ToggleDetachWebview, + }; + + /** Entrypoint for special actions that UI implementations may opt in. */ + virtual void Dispatch(Intent) {} + + enum class WebviewLayout : char { + Sidebar, + Expanded, + }; + + virtual void SetWebviewLayout(WebviewLayout layout); protected: /** * Protected Constructor. Use CreateUi instead. @@ -344,6 +358,8 @@ class BaseUi { /** Used by the F2 toggle: Remembers which configuration (ON or Overlay) was used */ ConfigEnum::ShowFps original_fps_show_state = ConfigEnum::ShowFps::OFF; + + WebviewLayout webview_layout = WebviewLayout::Sidebar; }; /** Global DisplayUi variable. */ @@ -463,4 +479,6 @@ inline void BaseUi::SetFrameLimit(int fps_limit) { frame_limit = (fps_limit == 0 ? Game_Clock::duration(0) : Game_Clock::TimeStepFromFps(fps_limit)); } +inline void BaseUi::SetWebviewLayout(WebviewLayout layout) { webview_layout = layout; } + #endif diff --git a/src/bitmap.cpp b/src/bitmap.cpp index fcaaa1624..c78511fc4 100644 --- a/src/bitmap.cpp +++ b/src/bitmap.cpp @@ -272,6 +272,11 @@ ImageOpacity Bitmap::ComputeImageOpacity(Rect rect) const { ImageOpacity::Alpha_8Bit; } +void Bitmap::SetBilinear() { + pixman_image_set_filter(bitmap.get(), PIXMAN_FILTER_BILINEAR, nullptr, 0); + pixman_image_set_component_alpha(bitmap.get(), true); +} + void Bitmap::CheckPixels(uint32_t flags) { if (flags & Flag_System) { DynamicFormat format(32,8,24,8,16,8,8,8,0,PF::Alpha); diff --git a/src/bitmap.h b/src/bitmap.h index 221750cf1..a9b3c678d 100644 --- a/src/bitmap.h +++ b/src/bitmap.h @@ -616,6 +616,7 @@ class Bitmap { ImageOpacity ComputeImageOpacity() const; ImageOpacity ComputeImageOpacity(Rect rect) const; + void SetBilinear(); protected: DynamicFormat format; diff --git a/src/input_buttons.h b/src/input_buttons.h index ae3d9cd2d..a684603ad 100644 --- a/src/input_buttons.h +++ b/src/input_buttons.h @@ -88,6 +88,7 @@ namespace Input { SHOW_CHAT, CHAT_SCROLL_DOWN, CHAT_SCROLL_UP, + TOGGLE_SIDEBAR, BUTTON_COUNT }; @@ -136,9 +137,9 @@ namespace Input { "TOGGLE_ZOOM", "SHOW_CHAT", "CHAT_SCROLL_DOWN", - "CHAT_SCROLL_UP"); + "CHAT_SCROLL_UP", + "TOGGLE_SIDEBAR"); static_assert(kInputButtonNames.size() == static_cast(BUTTON_COUNT)); - constexpr auto kInputButtonHelp = lcf::makeEnumTags( "Up Direction", "Down Direction", @@ -184,7 +185,8 @@ namespace Input { "Toggle Window Zoom level", "Toggle chat display", "Scroll chat window down", - "Scroll chat window up"); + "Scroll chat window up", + "Toggle sidebar display"); static_assert(kInputButtonHelp.size() == static_cast(BUTTON_COUNT)); /** @@ -201,6 +203,7 @@ namespace Input { case FAST_FORWARD_A: case FAST_FORWARD_B: case SHOW_CHAT: + case TOGGLE_SIDEBAR: return true; default: return false; diff --git a/src/input_buttons_desktop.cpp b/src/input_buttons_desktop.cpp index b1618f567..58305ba33 100644 --- a/src/input_buttons_desktop.cpp +++ b/src/input_buttons_desktop.cpp @@ -108,6 +108,7 @@ Input::ButtonMappingArray Input::GetDefaultButtonMappings() { {CHAT_SCROLL_DOWN, Keys::DOWN}, {CHAT_SCROLL_DOWN, Keys::JOY_DPAD_DOWN}, {CHAT_SCROLL_DOWN, Keys::JOY_LSTICK_DOWN}, + {TOGGLE_SIDEBAR, Keys::F2}, #if defined(USE_MOUSE) && defined(SUPPORT_MOUSE) {MOUSE_LEFT, Keys::MOUSE_LEFT}, diff --git a/src/multiplayer/chat_overlay.cpp b/src/multiplayer/chat_overlay.cpp index 0e6b6c050..5e9f60ff8 100644 --- a/src/multiplayer/chat_overlay.cpp +++ b/src/multiplayer/chat_overlay.cpp @@ -38,6 +38,9 @@ using json = nlohmann::json; #include "multiplayer/scene_overlay.h" #include "messages.h" #include "input.h" +#include "ynoicons.h" +#include "font.h" +#include "compiler.h" namespace { bool LargeScreen() { @@ -83,6 +86,50 @@ namespace { }, [](uv_work_t* task, int) { delete task; }); } + + /** formatted in the same way as an exfont */ + BitmapRef ynoicons = nullptr; + BitmapRef glyph_bm = nullptr; + + void InitializeYnoIcons() { + ynoicons = Bitmap::Create(resources_ynoicons_png, sizeof(resources_ynoicons_png)); + if (!ynoicons) + Output::Error("bug: invalid ynoicons"); + glyph_bm = Bitmap::Create(12, 12); + } + + enum class YnoIcons : char { + connection_3, + connection_2, + connection_1, + megaphone, + person, + compass, + globe, + map, + updown, + group, + crown, + wrench, + shield, + }; + + int RenderIcon(Bitmap& dst, int x, int y, YnoIcons icon_index, const Color& color, const Font& font) { + if (!ynoicons) + InitializeYnoIcons(); + + constexpr int dim = 12; + Rect icon_rect{ ((int)icon_index % 13) * dim, ((int)icon_index / 13) * dim, dim, dim}; + glyph_bm->Clear(); + glyph_bm->BlitFast(0, 0, *ynoicons, icon_rect, Opacity::Opaque()); + + // TODO: support for system theming a la fonts + double zoom = font.GetScaleRatio(); + int dim_zoom = ceilf(zoom * dim); + Rect dst_rect{ x, y, dim_zoom, dim_zoom }; + dst.MaskedBlit(dst_rect, *glyph_bm, 0, 0, color, 1 / zoom, 1 / zoom); + return dim_zoom; + } } ChatOverlay::ChatOverlay() : Drawable(Priority_Overlay, Drawable::Flags::Global | Drawable::Flags::Screenspace) @@ -141,7 +188,7 @@ void ChatOverlay::Draw(Bitmap& dst) { input_row_offset = 1; int offset = 2; int y = y_end - (lidx + 1) * text_height; - bitmap->Blit(0, y, *black, black->GetRect(), 200); + bitmap->FillRect({ 0, y, screen_rect.width, text_height }, Color{0, 0, 0, 200}); if (GMI().CanChat()) { int prompt_width = 0; prompt_width = Text::Draw(*bitmap, offset, y, font, offwhite, ">").x; @@ -158,6 +205,8 @@ void ChatOverlay::Draw(Bitmap& dst) { } } + int y_offset = 0; + // we're doing the inverse of MessageOverlay: draw from the bottom up for (auto message = messages.rbegin() + viewport; message != messages.rend(); ++message) { if (!message->hidden || show_all) { @@ -165,7 +214,7 @@ void ChatOverlay::Draw(Bitmap& dst) { unlocked_start ? (i == scroll ? grey : black) : unlocked_end ? (last_sticky + i == scroll ? grey : black) : i == half_screen ? grey : black; - bool highlight = bg == grey; + bool highlight = bg != black; bool first = true; bool has_header = true; @@ -191,38 +240,59 @@ void ChatOverlay::Draw(Bitmap& dst) { for (auto line = lines.rbegin(); line != lines.rend(); ++line) { // semitransparent bg - int y = y_end - (lidx + 1 + input_row_offset) * text_height; - bitmap->Blit(0, y, *bg, bg->GetRect(), show_all ? highlight ? 240 : 200 : 150); + int yidx = lidx + 1 + input_row_offset; + int line_height = text_height; + + // begin override line height for components + + // expand messages with only emojis + if (std::all_of(line->cbegin(), line->cend(), [](const std::shared_ptr& comp) { return bool(comp->Downcast()); })) { + line_height = LargeScreen() ? 56 : 18; + } + + // end override line height + + y_offset += line_height - text_height; + int y = y_end - yidx * text_height - y_offset; + + uint8_t opacity = show_all ? highlight ? 240 : 200 : 150; + bitmap->FillRect({ 0, y, screen_rect.width, line_height }, Color{bg.red, bg.green, bg.blue, opacity}); int offset = 2; - if (std::next(line) == lines.rend() && has_header) { + if (std::next(line) == lines.rend() && EP_LIKELY(has_header)) { // draw the username + if (message->global) + offset += RenderIcon(*bitmap, offset, y, YnoIcons::megaphone, offwhite, font); offset += Text::Draw(*bitmap, offset, y, font, offwhite, message->account ? "[" : "<").x; - offset += Text::Draw(*bitmap, offset, y, font, *message->system, 0, message->sender).x; + offset += Text::Draw(*bitmap, offset, y, font, *message->system_bm, 0, message->sender).x; if (message->badge) { - bitmap->StretchBlit({offset, y, text_height, text_height}, *message->badge, message->badge->GetRect(), 255); + bitmap->StretchBlit({offset, y, text_height, text_height}, *message->badge, message->badge->GetRect(), Opacity::Opaque()); offset += text_height; } + switch (message->rank) { + case 1: // mod + offset += RenderIcon(*bitmap, offset, y, YnoIcons::shield, offwhite, font); break; + case 2: // dev + offset += RenderIcon(*bitmap, offset, y, YnoIcons::wrench, offwhite, font); break; + } offset += Text::Draw(*bitmap, offset, y, font, offwhite, message->account ? "] " : "> ").x; } for (auto& span : *line) { - //auto& component = static_cast(span); if (auto str = span->Downcast()) { offset += Text::Draw(*bitmap, offset, y, font, str->color, str->string).x; } else if (auto emoji = span->Downcast()) { - //auto dims = *static_cast(emoji); Point dims = emoji->GetSize(); if (emoji->bitmap) { double zoom = emoji->bitmap->GetRect().y / (double)text_height; - bitmap->StretchBlit({offset, y, text_height, text_height}, *emoji->bitmap, emoji->bitmap->GetRect(), 255); + bitmap->StretchBlit({offset, y, line_height, line_height}, *emoji->bitmap, emoji->bitmap->GetRect(), 255); } else { // the emoji is still live, request it now emoji->RequestBitmap(this); - bitmap->FillRect({ offset, y, text_height, text_height }, offwhite); + bitmap->FillRect({ offset, y, line_height, line_height }, offwhite); } - offset += text_height; + offset += line_height; } else if (auto box = span->Downcast()) { int width = box->GetSize().x; @@ -231,22 +301,22 @@ void ChatOverlay::Draw(Bitmap& dst) { } } ++lidx; - if ((lidx + input_row_offset) * text_height > y_end) break; + if ((lidx + input_row_offset) * text_height + y_offset > y_end) break; } ++i; lines.clear(); } if (!show_all && i > message_max_minimized) break; - if ((lidx + input_row_offset) * text_height > y_end) break; + if ((lidx + input_row_offset) * text_height + y_offset > y_end) break; } - if (show_all && scrollbar) { + if (show_all) { double scrollbar_height = screen_rect.height / (double)text_height / messages.size() * screen_rect.height; Rect dims{ 0, 0, (int)ceilf(0.008 * screen_rect.width), (int)ceilf(scrollbar_height) }; //how far should the scrollbar go double scrollbar_extent = scroll / (double)message_max; int desired_y = floorf(y_end * (1 - scrollbar_extent)) - dims.height; - bitmap->BlitFast(bitmap->width() - dims.width, std::clamp(desired_y, 0, y_end - dims.height), *scrollbar, dims, 255); + bitmap->FillRect({ bitmap->width() - dims.width, std::clamp(desired_y, 0, y_end - dims.height), scrollbar_width, (int)ceilf(scrollbar_height) }, scrollbar); } dst.Blit(ox, oy, *bitmap, bitmap->GetRect(), 255); @@ -254,12 +324,11 @@ void ChatOverlay::Draw(Bitmap& dst) { } ChatOverlayMessage& ChatOverlay::AddMessage( - std::string_view message, std::string_view sender, std::string_view system, std::string_view badge, bool account) { + std::string_view message, std::string_view sender, std::string_view system, std::string_view badge, + bool account, bool global, int rank) { counter = 0; - auto sysref = system.empty() ? Cache::SystemOrBlack() : Cache::System(system.data()); - - auto& ret = messages.emplace_back(this, std::string(message), std::string(sender), sysref, std::string(badge), account); + auto& ret = messages.emplace_back(this, std::string(message), std::string(sender), std::string(system), std::string(badge), account, global, rank); while (messages.size() > message_max) { messages.pop_front(); @@ -273,7 +342,7 @@ ChatOverlayMessage& ChatOverlay::AddMessage( } void ChatOverlay::AddSystemMessage(StringView msg) { - auto& chatmsg = messages.emplace_back(this, std::string(msg), std::string(""), Cache::SystemOrBlack(), std::string(""), false); + auto& chatmsg = messages.emplace_back(this, std::string(msg), std::string(""), "", std::string(""), false, true, 0); chatmsg.text.erase(chatmsg.text.begin()); for (auto& span : chatmsg.text) { if (auto string = span->Downcast()) { @@ -352,7 +421,7 @@ void ChatOverlay::UpdateScene() { } if (Input::IsRawKeyTriggered(Input::Keys::RETURN) && !input.empty()) { std::string encoded = Utils::EncodeUTF(input); - GMI().sessionConn.SendPacketAsync(std::move(encoded)); + GMI().sessionConn.SendPacket(Messages::C2S::SessionGSay{ std::move(encoded) }); input.clear(); dirty = true; } @@ -370,7 +439,11 @@ void ChatOverlay::SetShowAll(bool show_all) { ((Scene_Overlay*)Scene::instance.get())->SetOnUpdate([this] { UpdateScene(); }); else Scene::Push(std::make_shared([this] { UpdateScene(); })); - DisplayUi->BeginTextCapture(); + Rect screen_rect = DisplayUi->GetScreenSurfaceRect(); + int text_height = ChatTextHeight(); + //Rect rect{ 0, screen_rect.height - text_height, screen_rect.width, text_height }; + Rect rect{ -100, -100, screen_rect.width, text_height }; + DisplayUi->BeginTextCapture(&rect); } else DisplayUi->EndTextCapture(); @@ -391,11 +464,8 @@ void ChatOverlay::OnResolutionChange() { int text_height = ChatTextHeight(); Rect rect = DisplayUi->GetScreenSurfaceRect(); - int width = rect.width; - black = Bitmap::Create(width, text_height, Color(0, 0, 0, 255)); - grey = Bitmap::Create(width, text_height, Color(80, 80, 80, 255)); - bitmap = Bitmap::Create(width, rect.height, true); - scrollbar = Bitmap::Create((int)ceilf(0.008 * width), rect.height, Color(255, 255, 255, 255)); + bitmap = Bitmap::Create(rect.width, rect.height); + scrollbar_width = ceilf(0.008 * rect.width); dirty = true; } @@ -438,8 +508,13 @@ std::vector> ChatOverlayMessage::Convert(ChatOver out.emplace_back(std::make_shared([this, has_badge] { auto& font = Font::ChatText(); Rect rect = Text::GetSize(*font, fmt::format("[{}] ", sender)); + int square = ChatTextHeight(); if (has_badge) - rect.width += ChatTextHeight(); + rect.width += square; + if (global) + rect.width += square; + if (rank > 0) + rect.width += square; return Point{ rect.width, rect.height }; }, ChatComponents::Header)); @@ -472,29 +547,41 @@ std::vector> ChatOverlayMessage::Convert(ChatOver } ChatOverlayMessage::ChatOverlayMessage( - ChatOverlay* parent, std::string text, std::string sender, BitmapRef system, std::string badge, bool account) : - parent(parent), text_orig(std::move(text)), sender(std::move(sender)), system(system), account(account) + ChatOverlay* parent_, std::string text, std::string sender, std::string system_, std::string badge, + bool account, bool global, int rank) : + parent(parent_), text_orig(std::move(text)), sender(std::move(sender)), system(system_), account(account), global(global), rank(rank) { bool has_badge = badge != "null" && !badge.empty(); this->text = Convert(parent, has_badge); + system_bm = Cache::SystemOrBlack(); + if (!system.empty()) { + auto req = AsyncHandler::RequestFile("System", system); + req->SetGraphicFile(true); + system_request = req->Bind([this](FileRequestResult* result) { + if (!result->success) return; + system_bm = Cache::System(result->file); + parent->MarkDirty(); + }); + req->Start(); + } + if (has_badge) { auto req = AsyncHandler::RequestFile("../images/badge", badge); req->SetGraphicFile(true); req->SetParentScope(true); req->SetRequestExtension(".png"); - badge_request = req->Bind(&ChatOverlayMessage::OnBadgeReady, this); + badge_request = req->Bind([this](FileRequestResult* result) { + if (!result->success) return; + this->badge = Cache::Badge(result->file); + this->badge->SetBilinear(); + parent->MarkDirty(); + }); req->Start(); } } -void ChatOverlayMessage::OnBadgeReady(FileRequestResult* result) { - if (!result->success) return; - badge = Cache::Badge(result->file); - parent->MarkDirty(); -} - ChatEmoji::ChatEmoji(ChatOverlay* parent_, int width, int height, std::string emoji_) : ChatComponent(width, height, ChatComponents::Emoji), emoji(std::move(emoji_)), parent(parent_) { @@ -520,6 +607,7 @@ void ChatEmoji::RequestBitmap(ChatOverlay* parent_) { return; } bitmap = Cache::Emoji(result->file); + bitmap->SetBilinear(); parent->MarkDirty(); }); req->Start(); diff --git a/src/multiplayer/chat_overlay.h b/src/multiplayer/chat_overlay.h index a7915f932..b7a4adb7c 100644 --- a/src/multiplayer/chat_overlay.h +++ b/src/multiplayer/chat_overlay.h @@ -71,22 +71,25 @@ inline Point ChatComponent::GetSize() const { class ChatOverlayMessage { public: ChatOverlayMessage( - ChatOverlay* parent, std::string text, std::string sender, BitmapRef system, std::string badge, bool account); + ChatOverlay* parent, std::string text, std::string sender, std::string system, std::string badge, bool account, bool global, int rank); std::string text_orig; std::vector> text; std::string sender; - BitmapRef system; + std::string system; + BitmapRef system_bm; BitmapRef badge; bool hidden = false; ChatOverlay* parent; - bool account = false; + bool account; + bool global; + int rank; std::vector> Convert(ChatOverlay* parent, bool has_badge) const; void SetOnInteract(std::function on_interact); private: FileRequestBinding badge_request; + FileRequestBinding system_request; - void OnBadgeReady(FileRequestResult*); std::function OnInteract; }; @@ -98,7 +101,9 @@ class ChatOverlay : public Drawable { void Draw(Bitmap& dst) override; void Update(); void UpdateScene(); - ChatOverlayMessage& AddMessage(std::string_view msg, std::string_view sender, std::string_view system = "", std::string_view badge = "", bool account = true); + ChatOverlayMessage& AddMessage( + std::string_view msg, std::string_view sender, std::string_view system = "", std::string_view badge = "", + bool account = true, bool global = true, int rank = 0); void OnResolutionChange() override; void SetShowAll(bool show_all); inline void SetShowAll() { SetShowAll(!show_all); } @@ -118,8 +123,13 @@ class ChatOverlay : public Drawable { int message_max_minimized = 4; int counter = 0; int scroll = 0; + int scrollbar_width = 0; + + BitmapRef bitmap; + constexpr static Color black{ 0, 0, 0, 255 }; + constexpr static Color grey{ 80, 80, 80, 255 }; + constexpr static Color scrollbar{ 255, 255, 255, 255 }; - BitmapRef bitmap, black, grey, scrollbar; std::u32string input; std::deque messages; @@ -154,7 +164,6 @@ class ChatString : public ChatComponent { ChatString(StringView other, Color color = default_color) : ChatComponent(0, 0, ChatComponents::String), string(other), color(color) {} - //Point Draw(Bitmap& dst) override; }; class ChatEmoji : public ChatComponent { @@ -165,7 +174,6 @@ class ChatEmoji : public ChatComponent { FileRequestBinding request = nullptr; ChatEmoji(ChatOverlay* parent, int width, int height, std::string emoji); - //Point Draw(Bitmap& dest) override; void RequestBitmap(ChatOverlay* parent); }; @@ -193,7 +201,11 @@ struct ChatComponentsMap { using type = ChatComponent; } template inline typename ChatComponentsMap::type* ChatComponent::Downcast() { - if (runtime_type == Wanted || Wanted == ChatComponents::Box) { + if (runtime_type == Wanted) { + return static_cast::type*>(this); + } + if (Wanted == ChatComponents::Box && runtime_type != ChatComponents::String) { + // a string doesn't have an intrinsic size return static_cast::type*>(this); } return nullptr; diff --git a/src/multiplayer/connection.cpp b/src/multiplayer/connection.cpp index 93db5d5fb..a71380061 100644 --- a/src/multiplayer/connection.cpp +++ b/src/multiplayer/connection.cpp @@ -21,6 +21,15 @@ void Connection::FlushQueue() { } void Connection::Dispatch(std::string_view name, ParameterList args) { + if (raw_handler) { + std::ostringstream os{}; + for (auto it = args.begin(); it != args.end(); ++it) { + os << *it; + if (std::next(it) != args.end()) + os << Packet::PARAM_DELIM; + } + raw_handler(name, os.str()); + } auto it = handlers.find(std::string(name)); if (it != handlers.end()) { std::invoke(it->second, args); diff --git a/src/multiplayer/connection.h b/src/multiplayer/connection.h index a91c2a043..f0d670c9d 100644 --- a/src/multiplayer/connection.h +++ b/src/multiplayer/connection.h @@ -56,6 +56,9 @@ class Connection { using SystemMessageHandler = std::function; void RegisterSystemHandler(SystemMessage m, SystemMessageHandler h); + template + inline void RegisterRawHandler(F&& callback) { raw_handler.swap(std::function(std::forward(callback))); } + void Dispatch(std::string_view name, ParameterList args = ParameterList()); bool IsConnected() const { return connected; } @@ -76,6 +79,7 @@ class Connection { void DispatchSystem(SystemMessage m); std::map> handlers; + std::function raw_handler; SystemMessageHandler sys_handlers[static_cast(SystemMessage::_PLACEHOLDER)]; uint32_t key; diff --git a/src/multiplayer/game_multiplayer.cpp b/src/multiplayer/game_multiplayer.cpp index fce8fd496..6f9c3d29b 100644 --- a/src/multiplayer/game_multiplayer.cpp +++ b/src/multiplayer/game_multiplayer.cpp @@ -510,7 +510,10 @@ void Game_Multiplayer::InitConnection() { Connect(room_id); // sync chat messages - std::string endpoint = fmt::format("https://connect.ynoproject.net/2kki/api/chathistory?globalMsgLimit={}&partyMsgLimit={}&lastMsgId={}", 200, 100, lastmsgid); cpr::Response resp = cpr::Get(cpr::Url{endpoint}); + cpr::Response resp = cpr::Get( + cpr::Url{ ApiEndpoint("chathistory") }, + cpr::Parameters{ {"globalMsgLimit", "200"}, {"partyMsgLimit", "100"}, {"lastMsgId", lastmsgid} } + ); if (resp.status_code >= 200 && resp.status_code < 300) { json chatmsgs = json::parse(resp.text); for (auto& it : chatmsgs["players"]) { @@ -529,14 +532,16 @@ void Game_Multiplayer::InitConnection() { std::string system; std::string badge; bool account = false; + int rank = 0; if (auto player = playerdata.find((std::string)(*it)["uuid"]); player != playerdata.end()) { name = player->second.name; system = player->second.systemName; badge = player->second.badge; account = player->second.account; + rank = player->second.rank; } std::string contents((*it)["contents"]); - chatoverlay.AddMessage(contents, name, system, badge, account); + chatoverlay.AddMessage(contents, name, system, badge, account, true, rank); lastmsgid = (std::string)(*it)["msgId"]; } if (!username.empty()) { @@ -559,15 +564,16 @@ void Game_Multiplayer::InitConnection() { std::string system; std::string badge; bool account = false; + int rank = 0; if (auto player = playerdata.find(p.uuid); player != playerdata.end()) { if (!player->second.name.empty()) name = player->second.name; system = player->second.systemName; badge = player->second.badge; account = player->second.account; + rank = player->second.rank; } - Output::Debug("(gc) {}: {}", name, p.msg); - Graphics::GetChatOverlay().AddMessage(p.msg, name, system, badge, account); + Graphics::GetChatOverlay().AddMessage(p.msg, name, system, badge, account, true, rank); lastmsgid = p.msgid; }); sessionConn.RegisterHandler("say", [this](SessionSay& p) { @@ -575,19 +581,19 @@ void Game_Multiplayer::InitConnection() { std::string system; std::string badge; bool account = false; + int rank = 0; if (auto player = playerdata.find(p.uuid); player != playerdata.end()) { if (!player->second.name.empty()) name = player->second.name; system = player->second.systemName; badge = player->second.badge; account = player->second.account; + rank = player->second.rank; } - Output::Debug("(mc) {}: {}", name, p.msg); - Graphics::GetChatOverlay().AddMessage(p.msg, name, system, badge, account); + Graphics::GetChatOverlay().AddMessage(p.msg, name, system, badge, account, false, rank); }); sessionConn.RegisterHandler("p", [this](SessionPlayerInfo& p) { auto& player = playerdata[p.uuid]; - Output::Debug("p: {}", p.name); player.name = p.name; player.systemName = p.systemName; player.rank = p.rank; @@ -1070,8 +1076,7 @@ void Game_Multiplayer::Update() { void Game_Multiplayer::SetConfig(const Game_ConfigOnline& cfg) { this->cfg = cfg; session_token = cfg.session_token.Get(); - if (!session_token.empty()) - SyncSaveFile(); + SyncSaveFile(); SetNametagMode((int)cfg.nametag_mode.Get()); if (!cfg.username.Get().empty()) SetNickname(cfg.username.Get()); @@ -1097,7 +1102,7 @@ void Game_Multiplayer::CheckLogin(bool async) { cpr::Header header; header["Authorization"] = token; - auto resp = cpr::Get(cpr::Url{ "https://connect.ynoproject.net/2kki/api/info" }, header); + auto resp = cpr::Get(cpr::Url{ ApiEndpoint("info")}, header); CheckLoginCallback(resp); } @@ -1121,7 +1126,7 @@ void Game_Multiplayer::Login(std::string_view username, std::string_view passwor header["Content-Type"] = "application/x-www-form-urlencoded"; auto resp = cpr::Post( - cpr::Url{ "https://connect.ynoproject.net/2kki/api/login" }, + cpr::Url{ ApiEndpoint("login") }, cpr::Body{ formdata }, header); if (resp.status_code >= 200 && resp.status_code < 300) { login_failure.clear(); @@ -1176,10 +1181,19 @@ void Game_Multiplayer::SetNickname(StringView name) { CheckLogin(); } +std::string Game_Multiplayer::ApiEndpoint(std::string_view path) const { + std::string_view game = Player::emscripten_game_name; + if (game.empty()) + game = "2kki"; + return fmt::format("https://connect.ynoproject.net/{}/api/{}", game, path); +} + void Game_Multiplayer::SyncSaveFile() const { + if (session_token.empty() || !FileFinder::Game()) return; + cpr::Header header; header["Authorization"] = session_token; - auto timestampResp = cpr::Get(cpr::Url{"https://connect.ynoproject.net/2kki/api/savesync?command=timestamp"}, header); + auto timestampResp = cpr::Get(cpr::Url{ ApiEndpoint("savesync?command=timestamp") }, header); if (!(timestampResp.status_code >= 200 && timestampResp.status_code < 300)) return Output::Warning("failed to get server save timestamp: {}", timestampResp.status_code); @@ -1191,40 +1205,37 @@ void Game_Multiplayer::SyncSaveFile() const { time_t last_synced = timegm(×tamp); const char* path = "Save01.lsd"; - Platform::File savefile(path); - if (savefile.GetLastModified() >= last_synced) + auto savefile = FileFinder::Save().FindFile(path); + if (!savefile.empty() && Platform::File(savefile).GetLastModified() >= last_synced) return Output::Info("Save slot 1 is up to date."); + if (savefile.empty()) + savefile = FileFinder::MakeCanonical(FileFinder::MakePath(FileFinder::Save().GetFullPath(), path)); - std::ofstream output(path, std::ios::binary); - auto resp = cpr::Download(output, cpr::Url{"https://connect.ynoproject.net/2kki/api/savesync?command=get"}, header); - if (resp.status_code >= 200 && resp.status_code < 300) + std::ofstream output(savefile, std::ios::binary); + auto resp = cpr::Download(output, cpr::Url{ ApiEndpoint("savesync?command=get") }, header); + if (resp.status_code >= 200 && resp.status_code < 300) { + output.flush(); + FileFinder::Save().ClearCache(); Output::Info("Save slot 1 has been updated."); + } else Output::Warning("failed to update save slot 1: {}", resp.status_code); } void Game_Multiplayer::UploadSaveFile() const { - if (session_token.empty()) return; + if (session_token.empty() || !FileFinder::Game()) return; const char* path = "Save01.lsd"; - Platform::File savefile(path); - if (!savefile.Exists()) + auto savefile = FileFinder::Save().FindFile(path); + if (savefile.empty()) return Output::Warning("bug: no save file to upload!"); cpr::Header header; header["Authorization"] = session_token; - //auto stream = FileFinder::Save().OpenFile(path); - //size_t size = stream.GetSize(); - //std::string data(size, '\0'); - //stream.read(&data[0], size); - //if (stream.fail() || stream.bad()) - // return Output::Warning("failed to read save file for uploading"); - auto resp = cpr::Post( - cpr::Url{ "https://connect.ynoproject.net/2kki/api/savesync?command=push" }, header, - //cpr::Body{ data } - cpr::Body{ cpr::File{path} } + cpr::Url{ ApiEndpoint("savesync?command=push") }, header, + cpr::Body{ cpr::File{savefile} } ); if (resp.status_code >= 200 && resp.status_code < 300) Output::Info("Uploaded save slot 1. ({} bytes up)", resp.uploaded_bytes); diff --git a/src/multiplayer/game_multiplayer.h b/src/multiplayer/game_multiplayer.h index f377392fb..8a02850fb 100644 --- a/src/multiplayer/game_multiplayer.h +++ b/src/multiplayer/game_multiplayer.h @@ -153,6 +153,7 @@ class Game_Multiplayer { void UpdateTimers(); void SetNickname(StringView name); + std::string ApiEndpoint(std::string_view path) const; }; inline Game_Multiplayer& GMI() { return Game_Multiplayer::Instance(); } diff --git a/src/multiplayer/scene_nexus.cpp b/src/multiplayer/scene_nexus.cpp new file mode 100644 index 000000000..c97dc1a4e --- /dev/null +++ b/src/multiplayer/scene_nexus.cpp @@ -0,0 +1,124 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#include + +#include "scene_nexus.h" +#include "baseui.h" +#include "platform/sdl/sdl2_ui.h" +#include "player.h" +#include "scene_title.h" +#include "scene_logo.h" +#include "game_clock.h" +#include "main_data.h" +#include "cache.h" +#include "audio.h" +#include "game_system.h" +#include "platform.h" +#include "output.h" +#include "multiplayer/game_multiplayer.h" +#include "graphics.h" + +using json = nlohmann::json; + +Scene_Nexus::Scene_Nexus() { + type = Scene::GameBrowser; +} + +void Scene_Nexus::Start() { + Main_Data::game_system = std::make_unique(); + Game_Clock::ResetFrame(Game_Clock::now()); + InitWebview(); +} + +void Scene_Nexus::DrawBackground(Bitmap&) { + // draw nothing +} + +void Scene_Nexus::Continue(SceneType) { + Main_Data::game_system->BgmStop(); + std::filesystem::current_path(old_pwd); + + Cache::ClearAll(); + AudioSeCache::Clear(); + MidiDecoder::Reset(); + lcf::Data::Clear(); + Main_Data::Cleanup(); + + // Restore the base resolution + Player::RestoreBaseResolution(); + + Player::game_title = ""; + Player::emscripten_game_name = ""; + selected_game = ""; + + Font::ResetDefault(); + InitWebview(); +} + +void Scene_Nexus::InitWebview() { + DisplayUi->SetWebviewLayout(BaseUi::WebviewLayout::Expanded); + auto& webview = ((Sdl2Ui*)DisplayUi.get())->GetWebview(); + webview.dispatch([&] { + webview.unbind("webviewLaunchGame"); + webview.bind("webviewLaunchGame", [this](const std::string& args) -> std::string { + json args_ = json::parse(args); + selected_game = args_[0]; + return "null"; + }); + webview.navigate("https://localhost:8028/home"); + }); +} + +void Scene_Nexus::vUpdate() { + if (!selected_game.empty()) { + LaunchGame(selected_game); + selected_game = ""; + return; + } +} + +void Scene_Nexus::LaunchGame(StringView game) { +#ifdef _WIN32 + std::string userhome(std::getenv("APPDATA")); + userhome.append("/ynoproject"); +#else + std::string userhome(std::getenv("HOME")); + userhome.append("/.local/ynoproject"); +#endif + + auto game_ = ToString(game); + Player::emscripten_game_name = game_; + userhome = FileFinder::MakeCanonical(FileFinder::MakePath(userhome, game_)); + Platform::File(userhome).MakeDirectory(true); + + // since FileFinder::Game can't really work with absolute paths, that leaves + // using relative paths while changing our PWD. + old_pwd = std::filesystem::current_path(); + std::filesystem::current_path(userhome); + + auto& webview = ((Sdl2Ui*)DisplayUi.get())->GetWebview(); + webview.dispatch([&webview] { + webview.navigate(fmt::format("https://localhost:8028")); + }); + DisplayUi->Dispatch(BaseUi::Intent::ToggleWebview); // hide the webview on entering the game + DisplayUi->SetWebviewLayout(BaseUi::WebviewLayout::Sidebar); + + GMI().SyncSaveFile(); + Game_Clock::ResetFrame(Game_Clock::now()); + Scene::Push(std::make_shared()); +} diff --git a/src/multiplayer/scene_nexus.h b/src/multiplayer/scene_nexus.h new file mode 100644 index 000000000..8f701443f --- /dev/null +++ b/src/multiplayer/scene_nexus.h @@ -0,0 +1,39 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef EP_MP_SCENE_NEXUS_H +#define EP_MP_SCENE_NEXUS_H + +#include + +#include "scene.h" + +class Scene_Nexus : public Scene { +public: + Scene_Nexus(); + void Start() override; + void Continue(SceneType) override; + void DrawBackground(Bitmap&) override; + void vUpdate() override; +private: + void LaunchGame(StringView); + void InitWebview(); + std::string selected_game; + std::filesystem::path old_pwd; +}; + +#endif diff --git a/src/platform/sdl/sdl2_ui.cpp b/src/platform/sdl/sdl2_ui.cpp index 41f4903fc..23a9fe77a 100644 --- a/src/platform/sdl/sdl2_ui.cpp +++ b/src/platform/sdl/sdl2_ui.cpp @@ -43,6 +43,7 @@ #include "player.h" #include "bitmap.h" #include "lcf/scope_guard.h" +#include "web_api.h" #if defined(__APPLE__) && TARGET_OS_OSX # include "platform/macos/macos_utils.h" @@ -359,6 +360,11 @@ bool Sdl2Ui::RefreshDisplayMode() { flags |= SDL_WINDOW_ALLOW_HIGHDPI; #endif + // Some hints that need to be set even before the window is created + // courtesy of https://github.com/etorth/mir2x/issues/48#issuecomment-2536136453 + SDL_SetHint(SDL_HINT_IME_SUPPORT_EXTENDED_TEXT, "1"); + SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1"); + // Create our window if (vcfg.window_x.Get() < 0 || vcfg.window_y.Get() < 0 || vcfg.window_height.Get() <= 0 || vcfg.window_width.Get() <= 0) { sdl_window = SDL_CreateWindow(GAME_TITLE, @@ -454,11 +460,15 @@ bool Sdl2Ui::RefreshDisplayMode() { #ifdef PLAYER_YNO webview_thread = std::thread([hwnd, this] { try { - webview = std::make_unique(false, nullptr); + webview = std::make_unique(true, nullptr); auto& w = *webview; w.set_title("YNOproject sidecar"); w.set_size(window.width, window.height, WEBVIEW_HINT_NONE); - w.set_html(R"html()html"); + //w.navigate("https://localhost:8028"); + //std::string_view game = Player::emscripten_game_name; + //if (game.empty()) + // game = "2kki"; + //w.navigate(fmt::format("https://ynoproject.net/{}", game)); if (auto widget = w.window(); widget.ok()) { #ifdef _WIN32 HWND childHwnd = (HWND)widget.value(); @@ -472,18 +482,19 @@ bool Sdl2Ui::RefreshDisplayMode() { window.width - 800, 0, rect.right - rect.left, rect.bottom - rect.top, - SWP_FRAMECHANGED | SWP_HIDEWINDOW); + SWP_FRAMECHANGED | SWP_SHOWWINDOW); #else # error TODO: implement adopting webview as child #endif } + Web_API::InitializeBindings(); w.run(); } catch (const webview::exception& err) { Output::Error("webview died: {}", (int)err.error().code()); } }); -#endif +#endif // PLAYER_YNO } else { // Browser handles fast resizing for emscripten, TODO: use fullscreen API #ifndef EMSCRIPTEN @@ -500,6 +511,8 @@ bool Sdl2Ui::RefreshDisplayMode() { SDL_SetWindowSize(sdl_window, display_width_zoomed, display_height_zoomed); } } + // TODO: Conditionally request focus only if previously held + SDL_RaiseWindow(sdl_window); #endif } // Need to set up icon again, some platforms recreate the window when @@ -680,6 +693,7 @@ void Sdl2Ui::UpdateDisplay() { viewport.x = border_x; viewport.w = win_width; } + ModifyViewport(); }; if (vcfg.scaling_mode.Get() == ConfigEnum::ScalingMode::Integer) { @@ -716,19 +730,8 @@ void Sdl2Ui::UpdateDisplay() { SDL_RenderSetViewport(sdl_renderer, &viewport); } - if (webview) { -#ifdef _WIN32 - HWND hwnd = (HWND&)webview->window(); - - RECT rect{ 0, 0, 800, window.height }; - SetWindowPos(hwnd, nullptr, - window.width - 800, 0, - rect.right - rect.left, - rect.bottom - rect.top, - SWP_FRAMECHANGED | SWP_SHOWWINDOW); -#else -# error unimplemented -#endif + if (webview && webview_visible) { + LayoutWebview(); } screen_surface = Bitmap::Create(viewport.w, viewport.h, true, main_surface->pitch()); @@ -820,16 +823,18 @@ bool Sdl2Ui::HandleErrorOutput(const std::string &message) { return true; } -void Sdl2Ui::BeginTextCapture() { - SDL_SetHint(SDL_HINT_IME_SUPPORT_EXTENDED_TEXT, "1"); +void Sdl2Ui::BeginTextCapture(Rect* textbox) { SDL_RaiseWindow(sdl_window); + if (textbox) { + auto& box = *textbox; + SDL_Rect rect{ box.x, box.y, box.width, box.height }; + SDL_SetTextInputRect(&rect); + } SDL_StartTextInput(); - Output::Debug("begin capture"); } void Sdl2Ui::EndTextCapture() { SDL_StopTextInput(); - Output::Debug("end capture"); } void Sdl2Ui::ProcessEvent(SDL_Event &evnt) { @@ -1168,10 +1173,15 @@ void Sdl2Ui::ProcessTextEditEvent(SDL_Event& event) { if (event.type == SDL_TEXTEDITING_EXT) { composition.text.append(event.editExt.text); + composition.sel_start = event.editExt.start; + composition.sel_length = event.editExt.length; SDL_free(event.editExt.text); } - else composition.text.append(event.edit.text); - Output::Debug("`{}` {} {} ext={} win={}", composition.text, event.editExt.start, event.editExt.length, (int)(event.type == SDL_TEXTEDITING_EXT), event.editExt.windowID); + else { + composition.text.append(event.edit.text); + composition.sel_start = event.edit.start; + composition.sel_length = event.edit.length; + } if (composition.text.empty()) { composition.active = false; @@ -1179,8 +1189,6 @@ void Sdl2Ui::ProcessTextEditEvent(SDL_Event& event) { } composition.active = true; - composition.sel_start = event.editExt.start; - composition.sel_length = event.editExt.length; return; } @@ -1493,3 +1501,56 @@ bool Sdl2Ui::OpenURL(std::string_view url) { return true; } + +void Sdl2Ui::Dispatch(Intent intent) { + switch (intent) { + case Intent::ToggleWebview: { + webview_visible = !webview_visible; +#ifdef _WIN32 + HWND hwnd = (HWND&)webview->window().value(); + ShowWindow(hwnd, webview_visible ? SW_SHOW : SW_HIDE); + SDL_RaiseWindow(sdl_window); +#endif + if (webview_visible) + LayoutWebview(); + } break; + case Intent::ToggleDetachWebview: { + // nothing yet + } break; + } +} + +void Sdl2Ui::SetWebviewLayout(WebviewLayout layout) { + BaseUi::SetWebviewLayout(layout); + ModifyViewport(); + if (layout == WebviewLayout::Expanded && !webview_visible) + webview_visible = true; + if (webview_visible) + LayoutWebview(); +} + +void Sdl2Ui::ModifyViewport() { + switch (webview_layout) { + case WebviewLayout::Sidebar: { + webview_dims = { window.width - 800, 0, 800, window.height }; + } break; + case WebviewLayout::Expanded: { + webview_dims = { 0, 0, window.width, window.height }; + } break; + } +} + +void Sdl2Ui::LayoutWebview() { + if (!webview) return; +#ifdef _WIN32 + HWND hwnd = (HWND&)webview->window().value(); + RECT rect{ 0, 0, webview_dims.width, webview_dims.height }; + SetWindowPos(hwnd, nullptr, + webview_dims.x, webview_dims.y, + rect.right - rect.left, + rect.bottom - rect.top, + SWP_FRAMECHANGED | SWP_SHOWWINDOW); +#else +# error TODO: implement webview resize +#endif +} diff --git a/src/platform/sdl/sdl2_ui.h b/src/platform/sdl/sdl2_ui.h index 69e3f75ad..e98420753 100644 --- a/src/platform/sdl/sdl2_ui.h +++ b/src/platform/sdl/sdl2_ui.h @@ -84,8 +84,11 @@ class Sdl2Ui final : public BaseUi { #endif /** begin populating Input::text_input and Input::composition */ - void BeginTextCapture() override; + void BeginTextCapture(Rect* textbox = nullptr) override; void EndTextCapture() override; + void Dispatch(Intent) override; + webview::webview& GetWebview(); + void SetWebviewLayout(WebviewLayout) override; /** @} */ @@ -162,6 +165,12 @@ class Sdl2Ui final : public BaseUi { std::unique_ptr webview; std::thread webview_thread; + bool webview_visible = true; + Rect webview_dims{}; + void ModifyViewport(); + void LayoutWebview(); }; +inline webview::webview& Sdl2Ui::GetWebview() { return *webview.get(); } + #endif diff --git a/src/player.cpp b/src/player.cpp index 9e1b19308..8d227ad65 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -34,6 +34,7 @@ #ifdef PLAYER_YNO # include # include "multiplayer/chat_overlay.h" +# include "web_api.h" #endif #include "async_handler.h" @@ -141,9 +142,7 @@ namespace Player { Game_ConfigPlayer player_config; Game_ConfigGame game_config; std::function did_parse_config; -#ifdef __EMSCRIPTEN__ std::string emscripten_game_name; -#endif Game_Clock::time_point last_auto_screenshot; } @@ -218,8 +217,6 @@ void Player::Init(std::vector args) { void Player::Run() { Instrumentation::Init("EasyRPG-Player"); - GMI().InitSession(); - Scene::Push(std::make_shared()); Graphics::UpdateSceneCallback(); @@ -252,6 +249,8 @@ void Player::MainLoop() { return; } + uv_run(uv_default_loop(), UV_RUN_NOWAIT); + int num_updates = 0; while (Game_Clock::NextGameTimeStep()) { if (num_updates > 0) { @@ -283,8 +282,6 @@ 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; @@ -338,6 +335,9 @@ void Player::UpdateInput() { if (Input::IsSystemTriggered(Input::SHOW_CHAT)) { Graphics::GetChatOverlay().SetShowAll(); } + if (Input::IsSystemTriggered(Input::TOGGLE_SIDEBAR)) { + DisplayUi->Dispatch(BaseUi::Intent::ToggleWebview); + } #endif if (Input::IsSystemTriggered(Input::TOGGLE_ZOOM)) { DisplayUi->ToggleZoom(); @@ -706,14 +706,12 @@ Game_Config Player::ParseCommandLine() { exit(0); break; }*/ -#ifdef __EMSCRIPTEN__ if (cp.ParseNext(arg, 1, "--game")) { if (arg.NumValues() > 0) { emscripten_game_name = arg.Value(0); } continue; } -#endif cp.SkipNext(); } @@ -1173,7 +1171,7 @@ void Player::LoadFonts() { if (name_text) { Font::SetNameText(Font::CreateFtFont(std::move(name_text), 11, false, false), false); } - + auto chat_text = FileFinder::OpenFont("NameText"); if (chat_text) { Font::SetChatText(Font::CreateFtFont(std::move(chat_text), 11, false, false)); diff --git a/src/player.h b/src/player.h index c7acfad70..dd3ec7912 100644 --- a/src/player.h +++ b/src/player.h @@ -433,7 +433,6 @@ namespace Player { extern std::function did_parse_config; -#ifdef __EMSCRIPTEN__ /** Name of game emscripten uses */ extern std::string emscripten_game_name; #endif diff --git a/src/scene_logo.cpp b/src/scene_logo.cpp index 2cc3e9b06..5ccba9e9b 100644 --- a/src/scene_logo.cpp +++ b/src/scene_logo.cpp @@ -37,6 +37,11 @@ #include #include +#ifdef PLAYER_YNO +# include "multiplayer/game_multiplayer.h" +# include "multiplayer/scene_nexus.h" +#endif + Scene_Logo::Scene_Logo() : frame_counter(0) { type = Scene::Logo; @@ -115,9 +120,16 @@ void Scene_Logo::vUpdate() { std::string save_name = save.FindFile(ss.str()); Player::LoadSavegame(save_name, Player::load_game_id); } +#ifdef PLAYER_YNO + GMI().InitSession(); +#endif } else { +#ifdef PLAYER_YNO + Scene::Push(std::make_shared(), true); +#else Scene::Push(std::make_shared(), true); +#endif } } } @@ -132,12 +144,18 @@ bool Scene_Logo::DetectGame() { FileFinder::SetGameFilesystem(fs); } - static bool once = true; - if (once) { +#ifdef PLAYER_YNO + if (Player::emscripten_game_name.empty()) { + async_ready = true; + return true; + } +#endif + + //static bool once = true; + if (!request_id) { FileRequestAsync* index = AsyncHandler::RequestFile("index.json"); index->SetImportantFile(true); request_id = index->Bind(&Scene_Logo::OnIndexReady, this); - once = false; index->Start(); return false; } diff --git a/src/web_api.cpp b/src/web_api.cpp index 5f0fa386d..11eb79f8d 100644 --- a/src/web_api.cpp +++ b/src/web_api.cpp @@ -2,15 +2,34 @@ #ifdef __EMSCRIPTEN__ #include #else -#define EM_ASM_INT(x, ...) 0 -#define EM_ASM(x, ...) +# if defined(PLAYER_YNO) +# include +# include +# include + +# include "baseui.h" +# include "platform/sdl/sdl2_ui.h" +# include "output.h" +# include "multiplayer/game_multiplayer.h" +# include "player.h" +# define JS_EVAL(...) (DisplayUi ? ((Sdl2Ui*)DisplayUi.get())->GetWebview().dispatch([=]{ ((Sdl2Ui*)DisplayUi.get())->GetWebview().eval(fmt::format(__VA_ARGS__)); }) : webview::noresult{}) + using json = nlohmann::json; +# endif +# define EM_ASM_INT(...) 0 +# define EM_ASM(...) #endif using namespace Web_API; std::string Web_API::GetSocketURL() { - return "wss://connect.ynoproject.net/2kki/"; +#ifdef PLAYER_YNO + //return "wss://connect.ynoproject.net/2kki/"; //return "wss://localhost:8028/backend/2kki/"; + std::string_view game = Player::emscripten_game_name; + if (game.empty()) + game = "2kki"; + return fmt::format("wss://connect.ynoproject.net/{}/", game); +#else return reinterpret_cast(EM_ASM_INT({ var ws = Module.wsUrl; var len = lengthBytesUTF8(ws)+1; @@ -18,102 +37,194 @@ std::string Web_API::GetSocketURL() { stringToUTF8(ws, wasm_str, len); return wasm_str; })); +#endif } void Web_API::OnLoadMap(std::string_view name) { +#ifdef PLAYER_YNO + std::string name_(name); + JS_EVAL(R"js(onLoadMap("{}"))js", name_); + return; +#else EM_ASM({ onLoadMap(UTF8ToString($0)); }, name.data(), name.size()); +#endif } void Web_API::OnRoomSwitch() { +#ifdef PLAYER_YNO + JS_EVAL("onRoomSwitch()"); +#else EM_ASM({ onRoomSwitch(); }); +#endif } void Web_API::SyncPlayerData(std::string_view uuid, int rank, int account_bin, std::string_view badge, const int medals[5], int id) { +#ifdef PLAYER_YNO + std::string uuid_(uuid), badge_(badge); + JS_EVAL(R"js(syncPlayerData("{}", {}, {}, "{}", [{}, {}, {}, {}, {}], {}))js", uuid_, rank, account_bin, badge_, medals[0], medals[1], medals[2], medals[3], medals[4], id); +#else EM_ASM({ syncPlayerData(UTF8ToString($0, $1), $2, $3, UTF8ToString($4, $5), [ $6, $7, $8, $9, $10 ], $11); }, uuid.data(), uuid.size(), rank, account_bin, badge.data(), badge.size(), medals[0], medals[1], medals[2], medals[3], medals[4], id); +#endif } void Web_API::OnPlayerDisconnect(int id) { +#ifdef PLAYER_YNO + JS_EVAL("onPlayerDisconnected({})", id); +#else EM_ASM({ onPlayerDisconnected($0); }, id); +#endif } void Web_API::OnPlayerNameUpdated(std::string_view name, int id) { +#ifdef PLAYER_YNO + std::string name_(name); + JS_EVAL(R"js(onPlayerConnectedOrUpdated("", "{}", {}))js", name_, id); +#else EM_ASM({ onPlayerConnectedOrUpdated("", UTF8ToString($0, $1), $2); }, name.data(), name.size(), id); +#endif } void Web_API::OnPlayerSystemUpdated(std::string_view system, int id) { +#ifdef PLAYER_YNO + std::string system_(system); + JS_EVAL(R"js(onPlayerConnectedOrUpdated("{}", "", {}))js", system_, id); +#else EM_ASM({ onPlayerConnectedOrUpdated(UTF8ToString($0, $1), "", $2); }, system.data(), system.size(), id); +#endif } void Web_API::UpdateConnectionStatus(int status) { +#ifdef PLAYER_YNO + JS_EVAL("onUpdateConnectionStatus({})", status); +#else EM_ASM({ onUpdateConnectionStatus($0); }, status); +#endif } void Web_API::ReceiveInputFeedback(int s) { +#ifdef PLAYER_YNO + JS_EVAL("onReceiveInputFeedback({})", s); +#else EM_ASM({ onReceiveInputFeedback($0); }, s); +#endif } void Web_API::NametagModeUpdated(int m) { +#ifdef PLAYER_YNO + JS_EVAL("onNametagModeUpdated({})", m); +#else EM_ASM({ onNametagModeUpdated($0); }, m); +#endif } void Web_API::OnPlayerSpriteUpdated(std::string_view name, int index, int id) { +#ifdef PLAYER_YNO + std::string name_(name); + JS_EVAL(R"js(onPlayerSpriteUpdated("{}", {}, {}))js", name_, index, id); +#else EM_ASM({ onPlayerSpriteUpdated(UTF8ToString($0, $1), $2, $3); }, name.data(), name.size(), index, id); +#endif } void Web_API::OnPlayerTeleported(int map_id, int x, int y) { - EM_ASM({ +#ifdef PLAYER_YNO + JS_EVAL("onPlayerTeleported({}, {}, {})", map_id, x, y); +#else + EM_ASM({ onPlayerTeleported($0, $1, $2); }, map_id, x, y); +#endif } void Web_API::OnUpdateSystemGraphic(std::string_view sys) { +#ifdef PLAYER_YNO + std::string sys_(sys); + JS_EVAL(R"js( onUpdateSystemGraphic("{}") )js", sys_); +#else EM_ASM({ onUpdateSystemGraphic(UTF8ToString($0, $1)); }, sys.data(), sys.size()); +#endif } void Web_API::OnRequestBadgeUpdate() { +#ifdef PLAYER_YNO + JS_EVAL("onBadgeUpdateRequested()"); +#else EM_ASM({ onBadgeUpdateRequested(); }); +#endif } void Web_API::ShowToastMessage(std::string_view msg, std::string_view icon) { +#ifdef PLAYER_YNO + std::string msg_(msg), icon_(icon); + JS_EVAL(R"js( showClientToastMessage("{}", "{}") )js", msg_, icon_); +#else EM_ASM({ showClientToastMessage(UTF8ToString($0, $1), UTF8ToString($2, $3)); }, msg.data(), msg.size(), icon.data(), icon.size()); +#endif } bool Web_API::ShouldConnectPlayer(std::string_view uuid) { +#ifdef PLAYER_YNO return true; +#else int result = EM_ASM_INT({ return shouldConnectPlayer(UTF8ToString($0, $1)) ? 1 : 0; }, uuid.data(), uuid.size()); return result == 1; +#endif } void Web_API::OnRequestFile(std::string_view path) { +#ifdef PLAYER_YNO + std::string path_(path); + JS_EVAL(R"js( onRequestFile("{}") )js", path_); +#else EM_ASM({ onRequestFile(UTF8ToString($0, $1)); }, path.data(), path.size()); +#endif +} + +void Web_API::InitializeBindings() { +#ifdef PLAYER_YNO + auto& w = ((Sdl2Ui*)DisplayUi.get())->GetWebview(); + w.bind("webviewSendSession", [](const std::string& args) -> std::string { + json args_ = json::parse(args); + GMI().sessionConn.Send((std::string)args_[0]); + return ""; + }); + w.bind("webviewSessionToken", [](const std::string&) -> std::string { + if (GMI().session_token.empty()) + return "null"; + return fmt::format("\"{}\"", GMI().session_token); + }); + GMI().sessionConn.RegisterRawHandler([](std::string_view name, std::string_view args) { + JS_EVAL(R"js( receiveSessionMessage("{}", `{}`) )js", name, args); + }); +#endif } diff --git a/src/web_api.h b/src/web_api.h index 1862d8929..759af6799 100644 --- a/src/web_api.h +++ b/src/web_api.h @@ -27,6 +27,9 @@ namespace Web_API { bool ShouldConnectPlayer(std::string_view uuid); void OnRequestFile(std::string_view path); + + /** For non-Emscripten platforms, initialize bindings to receive messages from the webview. */ + void InitializeBindings(); } #endif diff --git a/src/window_settings.cpp b/src/window_settings.cpp index 554e500d0..068d61b5e 100644 --- a/src/window_settings.cpp +++ b/src/window_settings.cpp @@ -700,7 +700,7 @@ void Window_Settings::RefreshButtonList() { Input::SHOW_LOG }; break; case 3: - buttons = { Input::SHOW_CHAT, Input::CHAT_SCROLL_UP, Input::CHAT_SCROLL_DOWN }; + buttons = { Input::SHOW_CHAT, Input::CHAT_SCROLL_UP, Input::CHAT_SCROLL_DOWN, Input::TOGGLE_SIDEBAR }; } for (auto b: buttons) { @@ -791,7 +791,7 @@ void Window_Settings::RefreshOnline() { GMI().Logout(); Refresh(); }); - GetFrame().options.back().color = Font::ColorKnockout; + GetFrame().options.back().color = Font::ColorCritical; } else { if (cfg.username.Get().empty()) @@ -821,13 +821,13 @@ void Window_Settings::RefreshOnline() { cfg.password.Set(""); Refresh(); }); - GetFrame().options.back().color = Font::ColorKnockout; + GetFrame().options.back().color = Font::ColorCritical; AddOption(MenuItem("Register", "", ""), [this, &cfg] { Output::Warning("Register not implemented"); cfg.password.Set(""); Refresh(); }); - GetFrame().options.back().color = Font::ColorKnockout; + GetFrame().options.back().color = Font::ColorCritical; } AddOption(cfg.nametag_mode, [this, &cfg] { @@ -837,7 +837,7 @@ void Window_Settings::RefreshOnline() { }); // some debug options - AddOption(MenuItem("[DEBUG] Force Disconnect Session", "", ""), [] { + AddOption(MenuItem("[DEBUG] Force Reconnect", "", ""), [] { Scene::PopUntil(Scene::SceneType::Map); GMI().sessionConn.Open(GMI().GetSessionEndpoint()); }); diff --git a/src/window_stringinput.cpp b/src/window_stringinput.cpp index 5da346a59..53641a6a4 100644 --- a/src/window_stringinput.cpp +++ b/src/window_stringinput.cpp @@ -31,7 +31,8 @@ Window_StringInput::Window_StringInput(StringView initial_value, int ix, int iy, opacity = 0; active = false; - DisplayUi->BeginTextCapture(); + Rect rect{ ix, iy + 2, width - 16, height - 16 }; + DisplayUi->BeginTextCapture(&rect); Refresh(); } diff --git a/src/ynoicons.h b/src/ynoicons.h new file mode 100644 index 000000000..7bade45bb --- /dev/null +++ b/src/ynoicons.h @@ -0,0 +1,50 @@ +unsigned char resources_ynoicons_png[] = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x9c, 0x00, 0x00, 0x00, 0x0c, + 0x01, 0x03, 0x00, 0x00, 0x00, 0xad, 0x10, 0x46, 0x4d, 0x00, 0x00, 0x00, + 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, + 0x00, 0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, + 0x61, 0x05, 0x00, 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0x00, 0x00, + 0x00, 0xff, 0xff, 0xff, 0xa5, 0xd9, 0x9f, 0xdd, 0x00, 0x00, 0x00, 0x09, + 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0e, 0xc4, 0x00, 0x00, 0x0e, 0xc4, + 0x01, 0x95, 0x2b, 0x0e, 0x1b, 0x00, 0x00, 0x00, 0x18, 0x74, 0x45, 0x58, + 0x74, 0x53, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x00, 0x50, 0x61, + 0x69, 0x6e, 0x74, 0x2e, 0x4e, 0x45, 0x54, 0x20, 0x35, 0x2e, 0x31, 0x2e, + 0x32, 0xfb, 0xbc, 0x03, 0xb6, 0x00, 0x00, 0x00, 0xb6, 0x65, 0x58, 0x49, + 0x66, 0x49, 0x49, 0x2a, 0x00, 0x08, 0x00, 0x00, 0x00, 0x05, 0x00, 0x1a, + 0x01, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x1b, + 0x01, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x28, + 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x31, + 0x01, 0x02, 0x00, 0x10, 0x00, 0x00, 0x00, 0x5a, 0x00, 0x00, 0x00, 0x69, + 0x87, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0c, 0x77, 0x01, 0x00, 0xe8, 0x03, 0x00, 0x00, 0x0c, + 0x77, 0x01, 0x00, 0xe8, 0x03, 0x00, 0x00, 0x50, 0x61, 0x69, 0x6e, 0x74, + 0x2e, 0x4e, 0x45, 0x54, 0x20, 0x35, 0x2e, 0x31, 0x2e, 0x32, 0x00, 0x03, + 0x00, 0x00, 0x90, 0x07, 0x00, 0x04, 0x00, 0x00, 0x00, 0x30, 0x32, 0x33, + 0x30, 0x01, 0xa0, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x05, 0xa0, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x94, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x52, 0x39, 0x38, 0x00, 0x02, 0x00, 0x07, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x30, 0x31, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0xee, + 0xea, 0x2d, 0x18, 0xc6, 0xcc, 0x00, 0x02, 0x00, 0x00, 0x00, 0xce, 0x49, + 0x44, 0x41, 0x54, 0x18, 0xd3, 0x63, 0x80, 0x00, 0x3e, 0x06, 0x07, 0x3e, + 0x06, 0x06, 0x0e, 0x86, 0x04, 0x20, 0xdb, 0x4d, 0x81, 0xe1, 0xc1, 0x3b, + 0x86, 0x07, 0x0c, 0x0c, 0xf2, 0x0c, 0x0f, 0x0c, 0x1b, 0x1f, 0xc8, 0xb0, + 0x7c, 0x62, 0x60, 0x6c, 0xa8, 0x7f, 0xc0, 0x70, 0xe0, 0x0d, 0xc3, 0x01, + 0xa0, 0x20, 0x73, 0x44, 0xa0, 0xd3, 0x04, 0x3b, 0xbe, 0xef, 0x0e, 0x02, + 0x0e, 0x40, 0xc1, 0x86, 0x17, 0x0c, 0x0d, 0x0c, 0x0a, 0xf2, 0x4c, 0x2f, + 0xbc, 0x7c, 0xfa, 0xeb, 0x59, 0x92, 0x52, 0x4d, 0x40, 0x82, 0x7c, 0x0f, + 0x80, 0xba, 0x33, 0xf8, 0x58, 0xb7, 0xb4, 0x68, 0x74, 0xca, 0xf0, 0x7d, + 0xcf, 0xda, 0x92, 0x00, 0x15, 0x64, 0x7d, 0x21, 0xcf, 0x1f, 0xf7, 0xff, + 0x45, 0xa7, 0xec, 0xb9, 0xef, 0x55, 0x9f, 0x1f, 0x20, 0x04, 0xc1, 0x2a, + 0x81, 0x82, 0xe5, 0x05, 0x1f, 0x80, 0x82, 0xef, 0xde, 0x01, 0x45, 0x33, + 0xec, 0x9b, 0x5e, 0x78, 0x79, 0x74, 0x32, 0x9e, 0xfb, 0x9e, 0xb5, 0xc1, + 0xc2, 0xfe, 0x00, 0x44, 0x50, 0xc1, 0xbe, 0x39, 0x22, 0xd0, 0xf3, 0x24, + 0xfb, 0xbf, 0xef, 0xa6, 0x09, 0x32, 0x30, 0x41, 0x06, 0xfb, 0x86, 0x07, + 0x86, 0x5d, 0x9a, 0xcc, 0x4f, 0x3e, 0x49, 0x1d, 0xe0, 0x93, 0x6f, 0x00, + 0x0a, 0x02, 0x85, 0x81, 0x8e, 0x77, 0xe0, 0xe3, 0x91, 0x67, 0x3c, 0x90, + 0xc0, 0xce, 0xc0, 0xc2, 0xcf, 0x80, 0x0a, 0x1a, 0x40, 0x04, 0x1b, 0x03, + 0x00, 0x11, 0xd9, 0x5a, 0x73, 0x11, 0xe5, 0xeb, 0x23, 0x00, 0x00, 0x00, + 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 +}; +unsigned int resources_ynoicons_png_len = 561; From f058fb33cd9ceab903c752d2b83a50084d0c974e Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Mon, 6 Jan 2025 16:39:52 -0500 Subject: [PATCH 13/18] Compile on Linux --- CMakeLists.txt | 27 ++++++++++---------- CMakePresets.json | 16 ++++++++++++ src/drawable.cpp | 2 +- src/game_message.h | 2 +- src/multiplayer/chat_overlay.cpp | 9 +++---- src/multiplayer/chat_overlay.h | 4 +-- src/multiplayer/connection.h | 2 +- src/multiplayer/game_multiplayer.h | 5 +++- src/multiplayer/scene_nexus.cpp | 2 +- src/multiplayer/webview.h | 37 +++++++++++++++++++++++++++ src/multiplayer/yno_connection.cpp | 13 +++++++--- src/platform.cpp | 2 +- src/platform/sdl/sdl2_ui.cpp | 41 ++++++++++++++++++++---------- src/platform/sdl/sdl2_ui.h | 2 +- src/web_api.cpp | 1 - 15 files changed, 119 insertions(+), 46 deletions(-) create mode 100644 src/multiplayer/webview.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ee61dbc3..aaa7c894e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -532,6 +532,7 @@ if(NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") src/multiplayer/scene_online.cpp src/multiplayer/scene_nexus.h src/multiplayer/scene_nexus.cpp + src/multiplayer/webview.h ) if(WIN32) target_link_libraries(${PROJECT_NAME} synchronization) # provides WaitOnAddress @@ -1059,22 +1060,15 @@ 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_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) + # require_lws_config(LWS_ROLE_H1 1 requirements) + # require_lws_config(LWS_WITHOUT_CLIENT 0 requirements) + # require_lws_config(LWS_WITH_TLS 1 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_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) @@ -1086,7 +1080,12 @@ else() #add_dependencies(${PROJECT_NAME} websocketpp::websocketpp) #find_package(httplib CONFIG REQUIRED) #target_link_libraries(${PROJECT_NAME} httplib::httplib) - find_package(cpr CONFIG REQUIRED) + # find_package(cpr CONFIG REQUIRED) + include(FetchContent) + FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git + GIT_TAG dec9422db3af470641f8b0d90e4b451c4daebf64) # Replace with your desired git commit from: https://github.com/libcpr/cpr/releases + + FetchContent_MakeAvailable(cpr) target_link_libraries(${PROJECT_NAME} cpr::cpr) endif() diff --git a/CMakePresets.json b/CMakePresets.json index 178682412..19d5f9f50 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -294,6 +294,22 @@ "type-release" ] }, + { + "name": "linux-ynoshell-relwithdebinfo", + "displayName": "Linux (ynoshell, RelWithDebInfo)", + "inherits": "linux-relwithdebinfo", + "cacheVariables": { + "PLAYER_SHELL": "yno" + } + }, + { + "name": "linux-ynoshell-release", + "displayName": "Linux (ynoshell, Release)", + "inherits": "linux-release", + "cacheVariables": { + "PLAYER_SHELL": "yno" + } + }, { "name": "windows-parent", "cacheVariables": { diff --git a/src/drawable.cpp b/src/drawable.cpp index 3d12443ae..31cb20344 100644 --- a/src/drawable.cpp +++ b/src/drawable.cpp @@ -41,7 +41,7 @@ void Drawable::RemoveScreenDrawable(lcf::Span container) { if (container.empty() || !DisplayUi) return; for (auto to_remove = screen_drawables.begin(); to_remove != screen_drawables.end(); ++to_remove) { for (auto& it : container) { - if (to_remove._Ptr == &it) { + if (to_remove.base() == &it) { screen_drawables.erase(to_remove); break; } diff --git a/src/game_message.h b/src/game_message.h index 3ea92df12..9c851eb48 100644 --- a/src/game_message.h +++ b/src/game_message.h @@ -96,7 +96,7 @@ namespace Game_Message { #ifdef PLAYER_YNO using ComponentWrapCallback = const std::function> spans)>; - int WordWrap(lcf::Span> spans, const int limit, const typename ComponentWrapCallback& callback, const Font& font); + int WordWrap(lcf::Span> spans, const int limit, const ComponentWrapCallback& callback, const Font& font); #endif /** diff --git a/src/multiplayer/chat_overlay.cpp b/src/multiplayer/chat_overlay.cpp index 5e9f60ff8..17b57c1ff 100644 --- a/src/multiplayer/chat_overlay.cpp +++ b/src/multiplayer/chat_overlay.cpp @@ -16,18 +16,15 @@ */ #include #include -#include #include #include #include -#include using json = nlohmann::json; #include "chat_overlay.h" #include "drawable_mgr.h" -#include "player.h" #include "baseui.h" #include "output.h" #include "input_buttons.h" @@ -67,7 +64,7 @@ namespace { [](uv_work_t*) { auto resp = cpr::Get(cpr::Url{"https://ynoproject.net/game/ynomoji.json"}); if (!(resp.status_code >= 200 && resp.status_code < 300)) - Output::Error("no emojis??"); + Output::Error("no emojis: {}", resp.text); json data = json::parse(resp.text, nullptr, false); if (data.is_discarded()) Output::Error("invalid ynomoji json"); @@ -506,8 +503,8 @@ std::vector> ChatOverlayMessage::Convert(ChatOver auto _guard = font->ApplyStyle(style); out.emplace_back(std::make_shared([this, has_badge] { - auto& font = Font::ChatText(); - Rect rect = Text::GetSize(*font, fmt::format("[{}] ", sender)); + auto& font = *Font::ChatText(); + Rect rect = Text::GetSize(font, fmt::format("[{}] ", sender)); int square = ChatTextHeight(); if (has_badge) rect.width += square; diff --git a/src/multiplayer/chat_overlay.h b/src/multiplayer/chat_overlay.h index b7a4adb7c..9fc14a8d9 100644 --- a/src/multiplayer/chat_overlay.h +++ b/src/multiplayer/chat_overlay.h @@ -202,11 +202,11 @@ struct ChatComponentsMap { using type = ChatComponent; } template inline typename ChatComponentsMap::type* ChatComponent::Downcast() { if (runtime_type == Wanted) { - return static_cast::type*>(this); + return static_cast::type*>(this); } if (Wanted == ChatComponents::Box && runtime_type != ChatComponents::String) { // a string doesn't have an intrinsic size - return static_cast::type*>(this); + return static_cast::type*>(this); } return nullptr; } diff --git a/src/multiplayer/connection.h b/src/multiplayer/connection.h index f0d670c9d..322680beb 100644 --- a/src/multiplayer/connection.h +++ b/src/multiplayer/connection.h @@ -57,7 +57,7 @@ class Connection { void RegisterSystemHandler(SystemMessage m, SystemMessageHandler h); template - inline void RegisterRawHandler(F&& callback) { raw_handler.swap(std::function(std::forward(callback))); } + inline void RegisterRawHandler(F&& callback) { raw_handler = callback; } void Dispatch(std::string_view name, ParameterList args = ParameterList()); diff --git a/src/multiplayer/game_multiplayer.h b/src/multiplayer/game_multiplayer.h index 8a02850fb..e09790763 100644 --- a/src/multiplayer/game_multiplayer.h +++ b/src/multiplayer/game_multiplayer.h @@ -11,9 +11,12 @@ #include "yno_connection.h" #ifdef PLAYER_YNO -# include # include "game_config.h" # include "game_clock.h" + +namespace cpr { + class Response; +} #endif class PlayerOther; diff --git a/src/multiplayer/scene_nexus.cpp b/src/multiplayer/scene_nexus.cpp index c97dc1a4e..a8dda937a 100644 --- a/src/multiplayer/scene_nexus.cpp +++ b/src/multiplayer/scene_nexus.cpp @@ -80,7 +80,7 @@ void Scene_Nexus::InitWebview() { selected_game = args_[0]; return "null"; }); - webview.navigate("https://localhost:8028/home"); + webview.navigate("https://ynoproject.net"); }); } diff --git a/src/multiplayer/webview.h b/src/multiplayer/webview.h new file mode 100644 index 000000000..cb7f72144 --- /dev/null +++ b/src/multiplayer/webview.h @@ -0,0 +1,37 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef EP_MP_WEBVIEW_EXPORT_H +#define EP_MP_WEBVIEW_EXPORT_H + +// include this file in place of webview/webview.h +// for compatibility with X11 types + +#define Drawable XDrawable +#define Font XFont +#define Window XID +#define None XNone + +#include +#include + +#undef Drawable +#undef Font +#undef Window +#undef None + +#endif diff --git a/src/multiplayer/yno_connection.cpp b/src/multiplayer/yno_connection.cpp index 639018b2d..5b22f5ee4 100644 --- a/src/multiplayer/yno_connection.cpp +++ b/src/multiplayer/yno_connection.cpp @@ -1,6 +1,7 @@ #include "yno_connection.h" #include "multiplayer/packet.h" #include "output.h" +#include #include #include #include @@ -14,6 +15,11 @@ # include # if defined(_WIN32) # include +# elif defined(__linux__) +# include +# include +# else +# error No futex implementation available for this platform # endif #endif #include "../external/TinySHA1.hpp" @@ -381,15 +387,16 @@ void YNOConnection::Open(std::string_view uri) { uv_queue_work(uv_default_loop(), task, [](uv_work_t* task) { auto& [self, s] = *static_cast(task->data); -#if defined(_WIN32) if (!self->IsConnected()) return; - self->Close(); bool expected_value = false; +#if defined(_WIN32) if (!WaitOnAddress(self->ConnectedFutex(), &expected_value, sizeof(expected_value), INFINITE)) Output::Debug("Failed to wait for previous session to close: {}", GetLastError()); #else -# error TODO: wait on futex + if (syscall(SYS_futex, self->ConnectedFutex(), FUTEX_WAKE, expected_value, nullptr, nullptr, 0)) { + Output::Debug("Failed to wait for previous session to close: {}", strerror(errno)); + } #endif }, [](uv_work_t* task, int) { diff --git a/src/platform.cpp b/src/platform.cpp index 6ee057699..1f537b3a1 100644 --- a/src/platform.cpp +++ b/src/platform.cpp @@ -141,7 +141,7 @@ int64_t Platform::File::GetLastModified() const { #else struct stat sb = {}; int result = ::stat(filename.c_str(), &sb); - struct timspec ts = sb.st_mtim; + struct timespec ts = sb.st_mtim; return !result ? (int64_t)ts.tv_sec + (int64_t)ts.tv_nsec / 1000000000 : -1; #endif } diff --git a/src/platform/sdl/sdl2_ui.cpp b/src/platform/sdl/sdl2_ui.cpp index 23a9fe77a..bff9b1715 100644 --- a/src/platform/sdl/sdl2_ui.cpp +++ b/src/platform/sdl/sdl2_ui.cpp @@ -17,13 +17,15 @@ #include #include #include + #include "game_config.h" #include "system.h" #include "sdl2_ui.h" +#include + #ifdef _WIN32 # include -# include # include #elif defined(__ANDROID__) # include @@ -33,6 +35,9 @@ # include "platform/emscripten/audio.h" #elif defined(__WIIU__) # include "platform/wiiu/main.h" +#elif defined(GTK_MAJOR_VERSION) +# include +# include #endif #include "icon.h" @@ -140,8 +145,7 @@ static uint32_t SelectFormat(const SDL_RendererInfo& rinfo, bool print_all) { return current_fmt; } -#ifdef _WIN32 -HWND GetWindowHandle(SDL_Window* window) { +auto GetWindowHandle(SDL_Window* window) { SDL_SysWMinfo wminfo; SDL_VERSION(&wminfo.version) SDL_bool success = SDL_GetWindowWMInfo(window, &wminfo); @@ -149,9 +153,16 @@ HWND GetWindowHandle(SDL_Window* window) { if (success < 0) Output::Error("Wrong SDL version"); +#if defined(SDL_VIDEO_DRIVER_WINDOWS) return wminfo.info.win.window; -} +#elif defined(SDL_VIDEO_DRIVER_X11) + return std::make_pair(wminfo.info.x11.display, wminfo.info.x11.window); +#elif defined(SDL_VIDEO_DRIVER_WAYLAND) + // return std::make_pair(wminfo); +#else +# error not implemented on this platform #endif +} static int FilterUntilFocus(const SDL_Event* evnt); @@ -362,7 +373,9 @@ bool Sdl2Ui::RefreshDisplayMode() { // Some hints that need to be set even before the window is created // courtesy of https://github.com/etorth/mir2x/issues/48#issuecomment-2536136453 + #ifdef SDL_HINT_IME_SUPPORT_EXTENDED_TEXT SDL_SetHint(SDL_HINT_IME_SUPPORT_EXTENDED_TEXT, "1"); + #endif SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1"); // Create our window @@ -458,17 +471,12 @@ bool Sdl2Ui::RefreshDisplayMode() { renderer_sg.Dismiss(); window_sg.Dismiss(); #ifdef PLAYER_YNO - webview_thread = std::thread([hwnd, this] { + webview_thread = std::thread([hwnd, display, this] { try { webview = std::make_unique(true, nullptr); auto& w = *webview; w.set_title("YNOproject sidecar"); w.set_size(window.width, window.height, WEBVIEW_HINT_NONE); - //w.navigate("https://localhost:8028"); - //std::string_view game = Player::emscripten_game_name; - //if (game.empty()) - // game = "2kki"; - //w.navigate(fmt::format("https://ynoproject.net/{}", game)); if (auto widget = w.window(); widget.ok()) { #ifdef _WIN32 HWND childHwnd = (HWND)widget.value(); @@ -484,7 +492,7 @@ bool Sdl2Ui::RefreshDisplayMode() { rect.bottom - rect.top, SWP_FRAMECHANGED | SWP_SHOWWINDOW); #else -# error TODO: implement adopting webview as child + GtkWidget* childHandle = (GtkWidget*&)widget.value(); #endif } Web_API::InitializeBindings(); @@ -524,6 +532,9 @@ bool Sdl2Ui::RefreshDisplayMode() { // Not using the enum names because this will fail to build when not using a recent Windows 11 SDK int window_rounding = 1; // DWMWCP_DONOTROUND DwmSetWindowAttribute(window, 33 /* DWMWA_WINDOW_CORNER_PREFERENCE */, &window_rounding, sizeof(window_rounding)); +#elif defined(_X11_XLIB_H_) + XID hwnd; + std::tie(display, hwnd) = GetWindowHandle(sdl_window); #endif uint32_t sdl_pixel_fmt = GetDefaultFormat(); @@ -893,7 +904,9 @@ void Sdl2Ui::ProcessEvent(SDL_Event &evnt) { case SDL_TEXTINPUT: case SDL_TEXTEDITING: +#ifdef SDL_TEXTEDITING_EXT case SDL_TEXTEDITING_EXT: +#endif ProcessTextEditEvent(evnt); break; } @@ -1171,13 +1184,16 @@ void Sdl2Ui::ProcessTextEditEvent(SDL_Event& event) { auto& composition = Input::composition; composition.text.clear(); +#ifdef SDL_TEXTEDITING_EXT if (event.type == SDL_TEXTEDITING_EXT) { composition.text.append(event.editExt.text); composition.sel_start = event.editExt.start; composition.sel_length = event.editExt.length; SDL_free(event.editExt.text); } - else { + else +#endif + { composition.text.append(event.edit.text); composition.sel_start = event.edit.start; composition.sel_length = event.edit.length; @@ -1551,6 +1567,5 @@ void Sdl2Ui::LayoutWebview() { rect.bottom - rect.top, SWP_FRAMECHANGED | SWP_SHOWWINDOW); #else -# error TODO: implement webview resize #endif } diff --git a/src/platform/sdl/sdl2_ui.h b/src/platform/sdl/sdl2_ui.h index e98420753..c70418fba 100644 --- a/src/platform/sdl/sdl2_ui.h +++ b/src/platform/sdl/sdl2_ui.h @@ -28,7 +28,7 @@ #include #ifdef PLAYER_YNO -# include +# include "multiplayer/webview.h" #endif extern "C" { diff --git a/src/web_api.cpp b/src/web_api.cpp index 11eb79f8d..9571cf7ba 100644 --- a/src/web_api.cpp +++ b/src/web_api.cpp @@ -3,7 +3,6 @@ #include #else # if defined(PLAYER_YNO) -# include # include # include From 647952d679a6a1f77b10ba5fe23911d7d76ab203 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Tue, 7 Jan 2025 22:52:12 -0500 Subject: [PATCH 14/18] Status overlay --- CMakeLists.txt | 19 ++-- src/async_handler.cpp | 1 + src/drawable.cpp | 2 +- src/font.cpp | 2 + src/graphics.cpp | 14 +-- src/graphics.h | 6 +- src/icons.cpp | 50 +++++++++++ src/icons.h | 45 ++++++++++ src/input_buttons_desktop.cpp | 2 +- src/multiplayer/chat_overlay.cpp | 96 ++++++-------------- src/multiplayer/chat_overlay.h | 9 +- src/multiplayer/game_multiplayer.cpp | 15 ++++ src/multiplayer/messages.h | 6 ++ src/multiplayer/overlay_utils.h | 32 +++++++ src/multiplayer/scene_nexus.cpp | 10 ++- src/multiplayer/status_overlay.cpp | 130 +++++++++++++++++++++++++++ src/multiplayer/status_overlay.h | 40 +++++++++ src/multiplayer/yno_connection.cpp | 1 + src/platform/sdl/sdl2_ui.cpp | 20 ++++- src/platform/sdl/sdl2_ui.h | 1 + src/player.cpp | 2 + src/scene_menu.cpp | 8 ++ src/translation.cpp | 2 +- src/web_api.cpp | 13 ++- src/window_settings.cpp | 4 +- src/window_stringinput.cpp | 2 +- src/ynoicons.h | 2 + 27 files changed, 429 insertions(+), 105 deletions(-) create mode 100644 src/icons.cpp create mode 100644 src/icons.h create mode 100644 src/multiplayer/overlay_utils.h create mode 100644 src/multiplayer/status_overlay.cpp create mode 100644 src/multiplayer/status_overlay.h diff --git a/CMakeLists.txt b/CMakeLists.txt index aaa7c894e..8e5c2f039 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -516,6 +516,9 @@ target_sources(${PROJECT_NAME} PRIVATE src/multiplayer/playerother.h src/multiplayer/playerother.cpp src/external/TinySHA1.hpp + src/icons.h + src/icons.cpp + src/multiplayer/overlay_utils.h # to be upstreamed src/window_stringinput.h @@ -526,6 +529,8 @@ if(NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") target_sources(${PROJECT_NAME} PRIVATE src/multiplayer/chat_overlay.h src/multiplayer/chat_overlay.cpp + src/multiplayer/status_overlay.h + src/multiplayer/status_overlay.cpp src/multiplayer/scene_overlay.h src/multiplayer/scene_overlay.cpp src/multiplayer/scene_online.h @@ -1080,12 +1085,14 @@ else() #add_dependencies(${PROJECT_NAME} websocketpp::websocketpp) #find_package(httplib CONFIG REQUIRED) #target_link_libraries(${PROJECT_NAME} httplib::httplib) - # find_package(cpr CONFIG REQUIRED) - include(FetchContent) - FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git - GIT_TAG dec9422db3af470641f8b0d90e4b451c4daebf64) # Replace with your desired git commit from: https://github.com/libcpr/cpr/releases + find_package(cpr CONFIG) + if(NOT cpr_FOUND) + include(FetchContent) + FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git + GIT_TAG dec9422db3af470641f8b0d90e4b451c4daebf64) # Replace with your desired git commit from: https://github.com/libcpr/cpr/releases - FetchContent_MakeAvailable(cpr) + FetchContent_MakeAvailable(cpr) + endif() target_link_libraries(${PROJECT_NAME} cpr::cpr) endif() @@ -1308,7 +1315,7 @@ if(PLAYER_BUILD_EXECUTABLE AND ${PLAYER_TARGET_PLATFORM} MATCHES "^SDL.*$" AND N set(EXE_NAME ${PROJECT_NAME}_exe) if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") add_executable(${EXE_NAME} "src/platform/emscripten/main.cpp") - #elseif(PLAYER_YNO) + #elseif(PLAYER_YNO) #add_executable(${EXE_NAME} "src/platform/ynoshell/main.cpp") else() add_executable(${EXE_NAME} "src/platform/sdl/main.cpp") diff --git a/src/async_handler.cpp b/src/async_handler.cpp index f43a34845..ea47fbcf3 100644 --- a/src/async_handler.cpp +++ b/src/async_handler.cpp @@ -320,6 +320,7 @@ void AsyncHandler::ClearRequests() { ++it; } } + async_requests.clear(); db_lastwrite = LLONG_MAX; } diff --git a/src/drawable.cpp b/src/drawable.cpp index 31cb20344..62a2fd61a 100644 --- a/src/drawable.cpp +++ b/src/drawable.cpp @@ -41,7 +41,7 @@ void Drawable::RemoveScreenDrawable(lcf::Span container) { if (container.empty() || !DisplayUi) return; for (auto to_remove = screen_drawables.begin(); to_remove != screen_drawables.end(); ++to_remove) { for (auto& it : container) { - if (to_remove.base() == &it) { + if (&*to_remove == &it) { screen_drawables.erase(to_remove); break; } diff --git a/src/font.cpp b/src/font.cpp index 2d94ed8ca..42fabbd15 100644 --- a/src/font.cpp +++ b/src/font.cpp @@ -61,6 +61,7 @@ #include "graphics.h" #ifdef PLAYER_YNO # include "multiplayer/chat_overlay.h" +# include "multiplayer/status_overlay.h" #endif // Static variables. @@ -718,6 +719,7 @@ void Font::SetChatText(FontRef new_chat_text) { chat_text->SetFallbackFont(DefaultBitmapFont()); #ifdef PLAYER_YNO Graphics::GetChatOverlay().OnResolutionChange(); + Graphics::GetStatusOverlay().OnResolutionChange(); #endif } diff --git a/src/graphics.cpp b/src/graphics.cpp index 6fb4ca8f0..6a0fe461b 100644 --- a/src/graphics.cpp +++ b/src/graphics.cpp @@ -32,6 +32,7 @@ #include "game_clock.h" #ifdef PLAYER_YNO # include "multiplayer/chat_overlay.h" +# include "multiplayer/status_overlay.h" #endif using namespace std::chrono_literals; @@ -45,6 +46,7 @@ namespace Graphics { std::unique_ptr fps_overlay; #ifdef PLAYER_YNO std::unique_ptr chat_overlay; + std::unique_ptr status_overlay; #endif std::string window_title_key; @@ -57,12 +59,14 @@ void Graphics::Init() { message_overlay = std::make_unique(); fps_overlay = std::make_unique(); chat_overlay = std::make_unique(); + status_overlay = std::make_unique(); } void Graphics::Quit() { fps_overlay.reset(); message_overlay.reset(); chat_overlay.reset(); + status_overlay.reset(); Cache::ClearAll(); @@ -139,12 +143,6 @@ void Graphics::LocalDraw(Bitmap& dst, Bitmap& dst_screen, Drawable::Z_t min_z, D drawable_list.Draw(dst, dst_screen, min_z, max_z); } -void Graphics::OnResolutionChange() { -#ifdef PLAYER_YNO - Graphics::GetChatOverlay().OnResolutionChange(); -#endif -} - std::shared_ptr Graphics::UpdateSceneCallback() { auto prev_scene = current_scene; current_scene = Scene::instance; @@ -170,4 +168,8 @@ MessageOverlay& Graphics::GetMessageOverlay() { ChatOverlay& Graphics::GetChatOverlay() { return *chat_overlay; } + +StatusOverlay& Graphics::GetStatusOverlay() { + return *status_overlay; +} #endif diff --git a/src/graphics.h b/src/graphics.h index ca8dce81d..ffc345bdc 100644 --- a/src/graphics.h +++ b/src/graphics.h @@ -30,6 +30,7 @@ class MessageOverlay; class Scene; #ifdef PLAYER_YNO class ChatOverlay; +class StatusOverlay; #endif /** @@ -56,9 +57,6 @@ namespace Graphics { void LocalDraw(Bitmap& dst, Bitmap& dst_screen, Drawable::Z_t min_z, Drawable::Z_t max_z); - /** Screenspace drawables can put their resolution handlers here. Called after DisplayUi is done updating the surfaces. */ - void OnResolutionChange(); - std::shared_ptr UpdateSceneCallback(); /** @@ -70,6 +68,8 @@ namespace Graphics { MessageOverlay& GetMessageOverlay(); ChatOverlay& GetChatOverlay(); + + StatusOverlay& GetStatusOverlay(); } #endif diff --git a/src/icons.cpp b/src/icons.cpp new file mode 100644 index 000000000..ba3c1dfd2 --- /dev/null +++ b/src/icons.cpp @@ -0,0 +1,50 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#include "icons.h" +#include "ynoicons.h" +#include "output.h" + +namespace { + /** formatted in the same way as an exfont */ + BitmapRef ynoicons = nullptr; + BitmapRef glyph_bm = nullptr; + + void InitializeYnoIcons() { + ynoicons = Bitmap::Create(resources_ynoicons_png, sizeof(resources_ynoicons_png)); + if (!ynoicons) + Output::Error("bug: invalid ynoicons"); + glyph_bm = Bitmap::Create(12, 12); + } +} + +int Icons::RenderIcon(Bitmap& dst, int x, int y, YnoIcons icon_index, const Color& color, const Font& font) { + if (!ynoicons) + InitializeYnoIcons(); + + constexpr int dim = 12; + Rect icon_rect{ ((int)icon_index % 13) * dim, ((int)icon_index / 13) * dim, dim, dim}; + glyph_bm->Clear(); + glyph_bm->BlitFast(0, 0, *ynoicons, icon_rect, Opacity::Opaque()); + + // TODO: support for system theming a la fonts + double zoom = font.GetScaleRatio(); + int dim_zoom = ceilf(zoom * dim); + Rect dst_rect{ x, y, dim_zoom, dim_zoom }; + dst.MaskedBlit(dst_rect, *glyph_bm, 0, 0, color, 1 / zoom, 1 / zoom); + return dim_zoom; +} diff --git a/src/icons.h b/src/icons.h new file mode 100644 index 000000000..bb6169679 --- /dev/null +++ b/src/icons.h @@ -0,0 +1,45 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef EP_ICONS_H +#define EP_ICONS_H + +#include "font.h" +#include "bitmap.h" +#include "color.h" + +namespace Icons { + enum class YnoIcons : char { + connection_3, + connection_2, + connection_1, + megaphone, + person, + compass, + globe, + map, + updown, + group, + crown, + wrench, + shield, + }; + + int RenderIcon(Bitmap& dst, int x, int y, YnoIcons icon_index, const Color& color, const Font& font); +} + +#endif diff --git a/src/input_buttons_desktop.cpp b/src/input_buttons_desktop.cpp index 58305ba33..6694aeb09 100644 --- a/src/input_buttons_desktop.cpp +++ b/src/input_buttons_desktop.cpp @@ -108,7 +108,7 @@ Input::ButtonMappingArray Input::GetDefaultButtonMappings() { {CHAT_SCROLL_DOWN, Keys::DOWN}, {CHAT_SCROLL_DOWN, Keys::JOY_DPAD_DOWN}, {CHAT_SCROLL_DOWN, Keys::JOY_LSTICK_DOWN}, - {TOGGLE_SIDEBAR, Keys::F2}, + {TOGGLE_SIDEBAR, Keys::PAUSE}, #if defined(USE_MOUSE) && defined(SUPPORT_MOUSE) {MOUSE_LEFT, Keys::MOUSE_LEFT}, diff --git a/src/multiplayer/chat_overlay.cpp b/src/multiplayer/chat_overlay.cpp index 17b57c1ff..d64823f35 100644 --- a/src/multiplayer/chat_overlay.cpp +++ b/src/multiplayer/chat_overlay.cpp @@ -35,19 +35,14 @@ using json = nlohmann::json; #include "multiplayer/scene_overlay.h" #include "messages.h" #include "input.h" -#include "ynoicons.h" #include "font.h" #include "compiler.h" +#include "icons.h" +#include "overlay_utils.h" namespace { - bool LargeScreen() { - return DisplayUi->GetScreenSurfaceRect().width >= 640; - } - int ChatTextHeight() { - return LargeScreen() ? 37 : 12; - } Point ChatTextSquare() { - int text_height = ChatTextHeight(); + int text_height = OverlayUtils::ChatTextHeight(); return { text_height, text_height }; } enum class FileExtensions : char { png, gif, webp }; @@ -83,50 +78,6 @@ namespace { }, [](uv_work_t* task, int) { delete task; }); } - - /** formatted in the same way as an exfont */ - BitmapRef ynoicons = nullptr; - BitmapRef glyph_bm = nullptr; - - void InitializeYnoIcons() { - ynoicons = Bitmap::Create(resources_ynoicons_png, sizeof(resources_ynoicons_png)); - if (!ynoicons) - Output::Error("bug: invalid ynoicons"); - glyph_bm = Bitmap::Create(12, 12); - } - - enum class YnoIcons : char { - connection_3, - connection_2, - connection_1, - megaphone, - person, - compass, - globe, - map, - updown, - group, - crown, - wrench, - shield, - }; - - int RenderIcon(Bitmap& dst, int x, int y, YnoIcons icon_index, const Color& color, const Font& font) { - if (!ynoicons) - InitializeYnoIcons(); - - constexpr int dim = 12; - Rect icon_rect{ ((int)icon_index % 13) * dim, ((int)icon_index / 13) * dim, dim, dim}; - glyph_bm->Clear(); - glyph_bm->BlitFast(0, 0, *ynoicons, icon_rect, Opacity::Opaque()); - - // TODO: support for system theming a la fonts - double zoom = font.GetScaleRatio(); - int dim_zoom = ceilf(zoom * dim); - Rect dst_rect{ x, y, dim_zoom, dim_zoom }; - dst.MaskedBlit(dst_rect, *glyph_bm, 0, 0, color, 1 / zoom, 1 / zoom); - return dim_zoom; - } } ChatOverlay::ChatOverlay() : Drawable(Priority_Overlay, Drawable::Flags::Global | Drawable::Flags::Screenspace) @@ -140,7 +91,7 @@ void ChatOverlay::Draw(Bitmap& dst) { if (Scene::instance && Scene::instance->type == Scene::SceneType::Settings) return; if (!dirty) { - dst.Blit(ox, oy, *bitmap, bitmap->GetRect(), 255); + dst.BlitFast(ox, oy, *bitmap, bitmap->GetRect(), Opacity::Opaque()); return; } @@ -152,7 +103,7 @@ void ChatOverlay::Draw(Bitmap& dst) { auto& font = *Font::ChatText(); int text_height; std::optional _guard; - if (LargeScreen()) { + if (OverlayUtils::LargeScreen()) { Font::Style style{}; style.italic = true; style.size = 33; // ChatTextHeight(); @@ -203,6 +154,7 @@ void ChatOverlay::Draw(Bitmap& dst) { } int y_offset = 0; + int message_max_minimized = OverlayUtils::LargeScreen() ? 4 : 2; // we're doing the inverse of MessageOverlay: draw from the bottom up for (auto message = messages.rbegin() + viewport; message != messages.rend(); ++message) { @@ -219,7 +171,7 @@ void ChatOverlay::Draw(Bitmap& dst) { message->text, bitmap->width(), [&](lcf::Span> line) { - if (first) { + if (first) { first = false; // we added the username as a virtual span, so now we remove it. if (line[0]->Downcast()) @@ -244,7 +196,7 @@ void ChatOverlay::Draw(Bitmap& dst) { // expand messages with only emojis if (std::all_of(line->cbegin(), line->cend(), [](const std::shared_ptr& comp) { return bool(comp->Downcast()); })) { - line_height = LargeScreen() ? 56 : 18; + line_height = OverlayUtils::LargeScreen() ? 56 : 18; } // end override line height @@ -259,7 +211,7 @@ void ChatOverlay::Draw(Bitmap& dst) { if (std::next(line) == lines.rend() && EP_LIKELY(has_header)) { // draw the username if (message->global) - offset += RenderIcon(*bitmap, offset, y, YnoIcons::megaphone, offwhite, font); + offset += Icons::RenderIcon(*bitmap, offset, y, Icons::YnoIcons::megaphone, offwhite, font); offset += Text::Draw(*bitmap, offset, y, font, offwhite, message->account ? "[" : "<").x; offset += Text::Draw(*bitmap, offset, y, font, *message->system_bm, 0, message->sender).x; if (message->badge) { @@ -268,9 +220,9 @@ void ChatOverlay::Draw(Bitmap& dst) { } switch (message->rank) { case 1: // mod - offset += RenderIcon(*bitmap, offset, y, YnoIcons::shield, offwhite, font); break; + offset += Icons::RenderIcon(*bitmap, offset, y, Icons::YnoIcons::shield, offwhite, font); break; case 2: // dev - offset += RenderIcon(*bitmap, offset, y, YnoIcons::wrench, offwhite, font); break; + offset += RenderIcon(*bitmap, offset, y, Icons::YnoIcons::wrench, offwhite, font); break; } offset += Text::Draw(*bitmap, offset, y, font, offwhite, message->account ? "] " : "> ").x; } @@ -303,7 +255,7 @@ void ChatOverlay::Draw(Bitmap& dst) { ++i; lines.clear(); } - if (!show_all && i > message_max_minimized) break; + if (!show_all && lidx >= message_max_minimized) break; if ((lidx + input_row_offset) * text_height + y_offset > y_end) break; } @@ -316,7 +268,7 @@ void ChatOverlay::Draw(Bitmap& dst) { bitmap->FillRect({ bitmap->width() - dims.width, std::clamp(desired_y, 0, y_end - dims.height), scrollbar_width, (int)ceilf(scrollbar_height) }, scrollbar); } - dst.Blit(ox, oy, *bitmap, bitmap->GetRect(), 255); + dst.BlitFast(ox, oy, *bitmap, bitmap->GetRect(), Opacity::Opaque()); dirty = false; } @@ -398,6 +350,11 @@ void ChatOverlay::UpdateScene() { DoScroll(3); } + if (Input::IsRawKeyPressed(Input::Keys::ENDS)) { + scroll = 0; + MarkDirty(); + } + if (!GMI().CanChat()) return; // chat input only below this point @@ -417,6 +374,7 @@ void ChatOverlay::UpdateScene() { dirty = true; } if (Input::IsRawKeyTriggered(Input::Keys::RETURN) && !input.empty()) { + // TODO: map chat and party chat std::string encoded = Utils::EncodeUTF(input); GMI().sessionConn.SendPacket(Messages::C2S::SessionGSay{ std::move(encoded) }); input.clear(); @@ -437,9 +395,9 @@ void ChatOverlay::SetShowAll(bool show_all) { else Scene::Push(std::make_shared([this] { UpdateScene(); })); Rect screen_rect = DisplayUi->GetScreenSurfaceRect(); - int text_height = ChatTextHeight(); - //Rect rect{ 0, screen_rect.height - text_height, screen_rect.width, text_height }; - Rect rect{ -100, -100, screen_rect.width, text_height }; + int text_height = OverlayUtils::ChatTextHeight(); + Rect rect{ 0, screen_rect.height - text_height, screen_rect.width, text_height }; + // Rect rect{ -100, -100, screen_rect.width, text_height }; DisplayUi->BeginTextCapture(&rect); } else @@ -459,7 +417,7 @@ void ChatOverlay::OnResolutionChange() { DrawableMgr::Register(this); } - int text_height = ChatTextHeight(); + int text_height = OverlayUtils::ChatTextHeight(); Rect rect = DisplayUi->GetScreenSurfaceRect(); bitmap = Bitmap::Create(rect.width, rect.height); scrollbar_width = ceilf(0.008 * rect.width); @@ -481,7 +439,7 @@ bool ChatOverlay::IsTriggeredOrRepeating(CustomKeyTimers key) { } if (Input::IsRawKeyPressed(timer.raw_key)) { - auto held_duration = now - timer.last_triggered; + auto held_duration = now - timer.last_triggered; if (held_duration >= timer.delay && (now - timer.last_repeated) >= timer.rate) { timer.last_repeated = now; return true; @@ -496,7 +454,7 @@ std::vector> ChatOverlayMessage::Convert(ChatOver std::vector> out; static std::regex pattern(":(\\w+):"); - int text_height = ChatTextHeight(); + int text_height = OverlayUtils::ChatTextHeight(); auto font = Font::ChatText(); Font::Style style{}; style.size = text_height; @@ -505,7 +463,7 @@ std::vector> ChatOverlayMessage::Convert(ChatOver out.emplace_back(std::make_shared([this, has_badge] { auto& font = *Font::ChatText(); Rect rect = Text::GetSize(font, fmt::format("[{}] ", sender)); - int square = ChatTextHeight(); + int square = OverlayUtils::ChatTextHeight(); if (has_badge) rect.width += square; if (global) @@ -529,7 +487,7 @@ std::vector> ChatOverlayMessage::Convert(ChatOver auto emoji_key = match[1].str(); if (emojis.empty() || emojis.find(emoji_key) != emojis.end()) - out.emplace_back(std::make_shared(parent, ChatTextHeight(), ChatTextHeight(), emoji_key)); + out.emplace_back(std::make_shared(parent, OverlayUtils::ChatTextHeight(), OverlayUtils::ChatTextHeight(), emoji_key)); else out.emplace_back(std::make_shared(match[0].str())); last_end = match[0].second; diff --git a/src/multiplayer/chat_overlay.h b/src/multiplayer/chat_overlay.h index 9fc14a8d9..c0e699365 100644 --- a/src/multiplayer/chat_overlay.h +++ b/src/multiplayer/chat_overlay.h @@ -99,12 +99,14 @@ class ChatOverlay : public Drawable { public: ChatOverlay(); void Draw(Bitmap& dst) override; + void OnResolutionChange() override; + void Update(); + /** Run by Scene_Overlay on behalf of this component. */ void UpdateScene(); ChatOverlayMessage& AddMessage( std::string_view msg, std::string_view sender, std::string_view system = "", std::string_view badge = "", bool account = true, bool global = true, int rank = 0); - void OnResolutionChange() override; void SetShowAll(bool show_all); inline void SetShowAll() { SetShowAll(!show_all); } inline bool ShowingAll() const noexcept { return show_all; } @@ -120,7 +122,6 @@ class ChatOverlay : public Drawable { int oy = 0; int message_max = 200; - int message_max_minimized = 4; int counter = 0; int scroll = 0; int scrollbar_width = 0; @@ -159,11 +160,11 @@ inline void ChatOverlay::MarkDirty() noexcept { dirty = true; } class ChatString : public ChatComponent { constexpr static Color default_color = Color(200, 200, 200, 255); public: - StringView string; + std::string string; Color color; ChatString(StringView other, Color color = default_color) : ChatComponent(0, 0, ChatComponents::String), - string(other), color(color) {} + string(ToString(other)), color(color) {} }; class ChatEmoji : public ChatComponent { diff --git a/src/multiplayer/game_multiplayer.cpp b/src/multiplayer/game_multiplayer.cpp index 6f9c3d29b..3a9ee8b43 100644 --- a/src/multiplayer/game_multiplayer.cpp +++ b/src/multiplayer/game_multiplayer.cpp @@ -53,6 +53,7 @@ # include "graphics.h" # include "platform.h" # include +# include "status_overlay.h" # if defined(_WIN32) && !defined(timegm) # define timegm _mkgmtime # endif @@ -153,7 +154,15 @@ static std::string get_room_url(int room_id, std::string_view session_token) { void Game_Multiplayer::InitConnection() { using YSM = YNOConnection::SystemMessage; using MCo = Multiplayer::Connection; + connection.RegisterSystemHandler(YSM::OPEN, [this] (MCo& c) { +#ifdef PLAYER_YNO + Graphics::GetStatusOverlay().MarkDirty(); +#endif + }); connection.RegisterSystemHandler(YSM::CLOSE, [this] (MCo& c) { +#ifdef PLAYER_YNO + Graphics::GetStatusOverlay().MarkDirty(); +#endif if (session_active) { Output::Info("Reconnecting: ID={}", room_id); Connect(room_id); @@ -164,6 +173,9 @@ void Game_Multiplayer::InitConnection() { connection.RegisterSystemHandler(YSM::EXIT, [this] (MCo& c) { // an exit happens outside ynoclient // resume with SessionReady() +#ifdef PLAYER_YNO + Graphics::GetStatusOverlay().MarkDirty(); +#endif Output::Debug("MP: socket exited with code 1028"); session_active = false; Quit(); @@ -600,6 +612,9 @@ void Game_Multiplayer::InitConnection() { player.account = p.account; player.badge = p.badge; }); + sessionConn.RegisterHandler("pc", [this](SessionPlayerCount& p) { + Graphics::GetStatusOverlay().SetPlayerCount(p.player_count); + }); #endif } diff --git a/src/multiplayer/messages.h b/src/multiplayer/messages.h index 4d0223ebc..757f1572a 100644 --- a/src/multiplayer/messages.h +++ b/src/multiplayer/messages.h @@ -426,6 +426,12 @@ namespace S2C { std::string badge; // TODO: Medals? }; + class SessionPlayerCount : public S2CPacket { + public: + SessionPlayerCount(const PL& v) : + player_count(Decode(v.at(0))) {} + int player_count; + }; #endif } namespace C2S { diff --git a/src/multiplayer/overlay_utils.h b/src/multiplayer/overlay_utils.h new file mode 100644 index 000000000..2d813ee86 --- /dev/null +++ b/src/multiplayer/overlay_utils.h @@ -0,0 +1,32 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef EP_MP_STATUS_OVERLAY_H +#define EP_MP_STATUS_OVERLAY_H + +#include "baseui.h" + +namespace OverlayUtils { + inline bool LargeScreen() { + return DisplayUi->GetScreenSurfaceRect().width >= 640; + } + inline int ChatTextHeight() { + return LargeScreen() ? 37 : 12; + } +} + +#endif diff --git a/src/multiplayer/scene_nexus.cpp b/src/multiplayer/scene_nexus.cpp index a8dda937a..a098a6210 100644 --- a/src/multiplayer/scene_nexus.cpp +++ b/src/multiplayer/scene_nexus.cpp @@ -39,8 +39,9 @@ Scene_Nexus::Scene_Nexus() { type = Scene::GameBrowser; } -void Scene_Nexus::Start() { +void Scene_Nexus::Start() { Main_Data::game_system = std::make_unique(); + Main_Data::game_system->SetSystemGraphic(CACHE_DEFAULT_BITMAP, lcf::rpg::System::Stretch_stretch, lcf::rpg::System::Font_gothic); Game_Clock::ResetFrame(Game_Clock::now()); InitWebview(); } @@ -67,6 +68,10 @@ void Scene_Nexus::Continue(SceneType) { selected_game = ""; Font::ResetDefault(); + + Main_Data::game_system = std::make_unique(); + Main_Data::game_system->SetSystemGraphic(CACHE_DEFAULT_BITMAP, lcf::rpg::System::Stretch_stretch, lcf::rpg::System::Font_gothic); + InitWebview(); } @@ -113,7 +118,8 @@ void Scene_Nexus::LaunchGame(StringView game) { auto& webview = ((Sdl2Ui*)DisplayUi.get())->GetWebview(); webview.dispatch([&webview] { - webview.navigate(fmt::format("https://localhost:8028")); + // webview.navigate(fmt::format("https://localhost:8028")); + webview.navigate("https://playtest.ynoproject.net/dev"); }); DisplayUi->Dispatch(BaseUi::Intent::ToggleWebview); // hide the webview on entering the game DisplayUi->SetWebviewLayout(BaseUi::WebviewLayout::Sidebar); diff --git a/src/multiplayer/status_overlay.cpp b/src/multiplayer/status_overlay.cpp new file mode 100644 index 000000000..d0dac4f81 --- /dev/null +++ b/src/multiplayer/status_overlay.cpp @@ -0,0 +1,130 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#include +#include "status_overlay.h" +#include "drawable_mgr.h" +#include "baseui.h" +#include "multiplayer/overlay_utils.h" +#include "multiplayer/game_multiplayer.h" +#include "icons.h" +#include "font.h" +#include "cache.h" +#include "main_data.h" +#include "game_variables.h" +#include "scene.h" + +using namespace std::chrono_literals; + +StatusOverlay::StatusOverlay() : Drawable(Priority_Overlay, Drawable::Flags::Global | Drawable::Flags::Screenspace) { +} + +void StatusOverlay::Draw(Bitmap& dst) { + auto scene_type = Scene::instance->type; + + auto now = Game_Clock::GetFrameTime(); + bool should_display = now - last_shown < 5s && scene_type != Scene::SceneType::ChatOverlay; + + if (!dirty) { + if (should_display) + dst.BlitFast(0, 0, *bitmap, bitmap->GetRect(), Opacity::Opaque()); + return; + } + + bitmap->Clear(); + + Rect rect = bitmap->GetRect(); + + int overlay_height = OverlayUtils::LargeScreen() ? 24 : 12; + + // draw background + bitmap->FillRect({0, 0, rect.width, overlay_height}, Color(0, 0, 0, 150)); + + int offset = 0; + int y = 0; + + auto font = Font::ChatText(); + Font::Style style; + style.size = overlay_height; + auto _guard = font->ApplyStyle(style); + + if (GMI().session_connected) { + offset += Icons::RenderIcon(*bitmap, offset, y, Icons::YnoIcons::connection_3, Color(0, 255, 200, 255), *font); + } else if (GMI().session_active) { + offset += Icons::RenderIcon(*bitmap, offset, y, Icons::YnoIcons::connection_2, Color(255, 200, 0, 255), *font); + } else { + offset += Icons::RenderIcon(*bitmap, offset, y, Icons::YnoIcons::connection_1, Color(255, 50, 50, 255), *font); + } + + constexpr Color offwhite { 200, 200, 200, 255 }; + + // draw player count + offset += Icons::RenderIcon(*bitmap, offset, y, Icons::YnoIcons::person, offwhite, *font); + offset += Text::Draw(*bitmap, offset, y, *font, offwhite, fmt::format("{} ", player_count)).x; + + // TODO: Draw available maps with interaction + + if (!location.empty()) + Text::Draw(*bitmap, rect.width, y, *font, *Cache::SystemOrBlack(), 0, location, Text::Alignment::AlignRight); + + if (should_display) + dst.BlitFast(0, 0, *bitmap, bitmap->GetRect(), Opacity::Opaque()); + dirty = false; +} + +void StatusOverlay::Update() { + if (!DisplayUi) { + return; + } + + if (!bitmap) { + OnResolutionChange(); + } +} + +void StatusOverlay::MarkDirty(bool reset_timer) { + dirty = true; + if (reset_timer) + ResetTimer(); +} + +void StatusOverlay::OnResolutionChange() { + if (!bitmap) { + DrawableMgr::Register(this); + } + + Rect rect = DisplayUi->GetScreenSurfaceRect(); + bitmap = Bitmap::Create(rect.width, OverlayUtils::ChatTextHeight()); + dirty = true; +} + +void StatusOverlay::SetLocation(std::string_view location) { + this->location = location; + ResetTimer(); + MarkDirty(); +} + +void StatusOverlay::SetPlayerCount(int player_count) { + this->player_count = player_count; + if (location.empty()) // game just started + ResetTimer(); + MarkDirty(); +} + +void StatusOverlay::ResetTimer() { + last_shown = Game_Clock::GetFrameTime(); +} diff --git a/src/multiplayer/status_overlay.h b/src/multiplayer/status_overlay.h new file mode 100644 index 000000000..7803c3398 --- /dev/null +++ b/src/multiplayer/status_overlay.h @@ -0,0 +1,40 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#include "drawable.h" +#include "bitmap.h" +#include "game_clock.h" + +/** Shows player count, connection status, current world etc. */ +class StatusOverlay : public Drawable { +public: + StatusOverlay(); + void Draw(Bitmap&) override; + void OnResolutionChange() override; + + void Update(); + void MarkDirty(bool reset_timer = false); + void SetLocation(std::string_view loc); + void SetPlayerCount(int player_count); + void ResetTimer(); +private: + bool dirty = true; + BitmapRef bitmap = nullptr; + std::string location = ""; + int player_count = 0; + Game_Clock::time_point last_shown; +}; diff --git a/src/multiplayer/yno_connection.cpp b/src/multiplayer/yno_connection.cpp index 5b22f5ee4..a9f568d1d 100644 --- a/src/multiplayer/yno_connection.cpp +++ b/src/multiplayer/yno_connection.cpp @@ -62,6 +62,7 @@ struct YNOConnection::IMPL { } static bool onmessage_common(const std::string& cstr, void* userData, bool isText) { auto _this = static_cast(userData); + if (!_this->IsConnected()) return false; // IMPORTANT!! numBytes is always one byte larger than the actual length // so the actual length is numBytes - 1 diff --git a/src/platform/sdl/sdl2_ui.cpp b/src/platform/sdl/sdl2_ui.cpp index bff9b1715..b90aa578b 100644 --- a/src/platform/sdl/sdl2_ui.cpp +++ b/src/platform/sdl/sdl2_ui.cpp @@ -247,6 +247,9 @@ Sdl2Ui::~Sdl2Ui() { if (webview) { webview->terminate(); webview_thread.join(); + // webview doesn't terminate cleanly, but it will be closed alongside + // the main process anyway + webview.release(); } SDL_Quit(); } @@ -1525,13 +1528,22 @@ void Sdl2Ui::Dispatch(Intent intent) { #ifdef _WIN32 HWND hwnd = (HWND&)webview->window().value(); ShowWindow(hwnd, webview_visible ? SW_SHOW : SW_HIDE); - SDL_RaiseWindow(sdl_window); -#endif if (webview_visible) LayoutWebview(); + SDL_RaiseWindow(sdl_window); +#endif } break; case Intent::ToggleDetachWebview: { - // nothing yet + webview_detach = !webview_detach; +#ifdef _WIN32 + HWND hwnd = (HWND&)webview->window().value(); + if (webview_detach) { + SetParent(hwnd, nullptr); + } else { + HWND parent = GetWindowHandle(sdl_window); + SetParent(hwnd, parent); + } +#endif } break; } } @@ -1548,7 +1560,7 @@ void Sdl2Ui::SetWebviewLayout(WebviewLayout layout) { void Sdl2Ui::ModifyViewport() { switch (webview_layout) { case WebviewLayout::Sidebar: { - webview_dims = { window.width - 800, 0, 800, window.height }; + webview_dims = { std::max(0, window.width - 800), 0, std::min(800, window.width), window.height }; } break; case WebviewLayout::Expanded: { webview_dims = { 0, 0, window.width, window.height }; diff --git a/src/platform/sdl/sdl2_ui.h b/src/platform/sdl/sdl2_ui.h index c70418fba..1acbfa4af 100644 --- a/src/platform/sdl/sdl2_ui.h +++ b/src/platform/sdl/sdl2_ui.h @@ -166,6 +166,7 @@ class Sdl2Ui final : public BaseUi { std::unique_ptr webview; std::thread webview_thread; bool webview_visible = true; + bool webview_detach = false; Rect webview_dims{}; void ModifyViewport(); void LayoutWebview(); diff --git a/src/player.cpp b/src/player.cpp index 8d227ad65..7a45073f7 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -34,6 +34,7 @@ #ifdef PLAYER_YNO # include # include "multiplayer/chat_overlay.h" +# include "multiplayer/status_overlay.h" # include "web_api.h" #endif @@ -269,6 +270,7 @@ void Player::MainLoop() { Graphics::GetMessageOverlay().Update(); #ifdef PLAYER_YNO Graphics::GetChatOverlay().Update(); + Graphics::GetStatusOverlay().Update(); #endif ++num_updates; diff --git a/src/scene_menu.cpp b/src/scene_menu.cpp index a0cae020f..8af94caa2 100644 --- a/src/scene_menu.cpp +++ b/src/scene_menu.cpp @@ -38,6 +38,8 @@ #ifdef PLAYER_YNO # include "multiplayer/scene_online.h" +# include "graphics.h" +# include "multiplayer/status_overlay.h" #endif constexpr int menu_command_width = 88; @@ -58,11 +60,17 @@ void Scene_Menu::Start() { // Status Window menustatus_window.reset(new Window_MenuStatus(Player::menu_offset_x + menu_command_width, Player::menu_offset_y, (MENU_WIDTH - menu_command_width), MENU_HEIGHT)); menustatus_window->SetActive(false); +#ifdef PLAYER_YNO + Graphics::GetStatusOverlay().MarkDirty(true); +#endif } void Scene_Menu::Continue(SceneType /* prev_scene */) { menustatus_window->Refresh(); gold_window->Refresh(); +#ifdef PLAYER_YNO + Graphics::GetStatusOverlay().MarkDirty(true); +#endif } void Scene_Menu::vUpdate() { diff --git a/src/translation.cpp b/src/translation.cpp index 6195a14ef..b5fffd612 100644 --- a/src/translation.cpp +++ b/src/translation.cpp @@ -235,7 +235,7 @@ void Translation::SelectLanguageAsync(FileRequestResult*, std::string_view lang_ } // Reset the cache, so that all images load fresh. - Cache::Clear(); + Cache::ClearAll(); Scene::instance->OnTranslationChanged(); } diff --git a/src/web_api.cpp b/src/web_api.cpp index 9571cf7ba..f8658f08f 100644 --- a/src/web_api.cpp +++ b/src/web_api.cpp @@ -9,8 +9,10 @@ # include "baseui.h" # include "platform/sdl/sdl2_ui.h" # include "output.h" -# include "multiplayer/game_multiplayer.h" # include "player.h" +# include "graphics.h" +# include "multiplayer/game_multiplayer.h" +# include "multiplayer/status_overlay.h" # define JS_EVAL(...) (DisplayUi ? ((Sdl2Ui*)DisplayUi.get())->GetWebview().dispatch([=]{ ((Sdl2Ui*)DisplayUi.get())->GetWebview().eval(fmt::format(__VA_ARGS__)); }) : webview::noresult{}) using json = nlohmann::json; # endif @@ -22,8 +24,6 @@ using namespace Web_API; std::string Web_API::GetSocketURL() { #ifdef PLAYER_YNO - //return "wss://connect.ynoproject.net/2kki/"; - //return "wss://localhost:8028/backend/2kki/"; std::string_view game = Player::emscripten_game_name; if (game.empty()) game = "2kki"; @@ -215,7 +215,7 @@ void Web_API::InitializeBindings() { w.bind("webviewSendSession", [](const std::string& args) -> std::string { json args_ = json::parse(args); GMI().sessionConn.Send((std::string)args_[0]); - return ""; + return "null"; }); w.bind("webviewSessionToken", [](const std::string&) -> std::string { if (GMI().session_token.empty()) @@ -225,5 +225,10 @@ void Web_API::InitializeBindings() { GMI().sessionConn.RegisterRawHandler([](std::string_view name, std::string_view args) { JS_EVAL(R"js( receiveSessionMessage("{}", `{}`) )js", name, args); }); + w.bind("webviewSetLocation", [](const std::string& args) -> std::string { + json args_ = json::parse(args); + Graphics::GetStatusOverlay().SetLocation(args_[0]); + return "null"; + }); #endif } diff --git a/src/window_settings.cpp b/src/window_settings.cpp index 068d61b5e..9d25f0de0 100644 --- a/src/window_settings.cpp +++ b/src/window_settings.cpp @@ -791,14 +791,13 @@ void Window_Settings::RefreshOnline() { GMI().Logout(); Refresh(); }); - GetFrame().options.back().color = Font::ColorCritical; } else { if (cfg.username.Get().empty()) AddOption(LockedMenuItem("Status", "", "Username not set"), [] {}); else AddOption(LockedMenuItem("Status", "", fmt::format("Playing as guest user")), [] {}); - + AddOption(cfg.username, [this, &cfg] { auto& value = GetCurrentOption().value_text; if (!value.empty() && GMI().sessionConn.IsConnected() && Input::IsRawKeyTriggered(Input::Keys::RETURN)) { @@ -827,7 +826,6 @@ void Window_Settings::RefreshOnline() { cfg.password.Set(""); Refresh(); }); - GetFrame().options.back().color = Font::ColorCritical; } AddOption(cfg.nametag_mode, [this, &cfg] { diff --git a/src/window_stringinput.cpp b/src/window_stringinput.cpp index 53641a6a4..59c813781 100644 --- a/src/window_stringinput.cpp +++ b/src/window_stringinput.cpp @@ -79,7 +79,7 @@ void Window_StringInput::Update() { } if (Input::IsRawKeyTriggered(Input::Keys::BACKSPACE) && !value.empty()) { - value.pop_back(); + value.clear(); dirty = true; } if (dirty) diff --git a/src/ynoicons.h b/src/ynoicons.h index 7bade45bb..caa58900f 100644 --- a/src/ynoicons.h +++ b/src/ynoicons.h @@ -1,3 +1,5 @@ +#pragma once + unsigned char resources_ynoicons_png[] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x9c, 0x00, 0x00, 0x00, 0x0c, From aab3c642fd2446a21019afd1e0700a260bfae518 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Wed, 8 Jan 2025 23:56:46 -0500 Subject: [PATCH 15/18] Chat screenshots --- CMakePresets.json | 42 +++- builds/cmake/VSWhere.cmake | 132 ++++++++++ builds/cmake/Windows.Kits.cmake | 153 ++++++++++++ builds/cmake/Windows.MSVC.toolchain.cmake | 279 ++++++++++++++++++++++ src/input.cpp | 2 +- src/input.h | 6 +- src/multiplayer/chat_overlay.cpp | 111 +++++++-- src/multiplayer/chat_overlay.h | 16 +- src/multiplayer/game_multiplayer.cpp | 9 +- src/multiplayer/yno_connection.cpp | 4 +- 10 files changed, 712 insertions(+), 42 deletions(-) create mode 100644 builds/cmake/VSWhere.cmake create mode 100644 builds/cmake/Windows.Kits.cmake create mode 100644 builds/cmake/Windows.MSVC.toolchain.cmake diff --git a/CMakePresets.json b/CMakePresets.json index 19d5f9f50..6869fcd2d 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -669,6 +669,30 @@ "PLAYER_SHELL": "yno" } }, + { + "name": "ynoshell-parent", + "hidden": true, + "cacheVariables": { + "PLAYER_SHELL": "yno", + "VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "${sourceDir}/builds/cmake/Windows.MSVC.toolchain.cmake" + } + }, + { + "name": "windows-ynoshell-release", + "displayName": "Windows (ynoshell) (Release)", + "inherits": [ + "ynoshell-parent", + "windows-release" + ] + }, + { + "name": "windows-ynoshell-relwithdebinfo", + "displayName": "Windows (ynoshell) (RelWithDebInfo)", + "inherits": [ + "ynoshell-parent", + "windows-relwithdebinfo" + ] + }, { "name": "macos-parent", "cacheVariables": { @@ -1622,11 +1646,13 @@ }, { "name": "windows-relwithdebinfo", - "configurePreset": "windows-relwithdebinfo" + "configurePreset": "windows-relwithdebinfo", + "configuration": "RelWithDebInfo" }, { "name": "windows-release", - "configurePreset": "windows-release" + "configurePreset": "windows-release", + "configuration": "Release" }, { "name": "windows-sdl3-debug", @@ -1760,6 +1786,16 @@ "name": "windows-x64-vs2022-libretro-release", "configurePreset": "windows-x64-vs2022-libretro-release" }, + { + "name": "windows-x64-vs2022-ynoshell-relwithdebinfo", + "configurePreset": "windows-x64-vs2022-ynoshell-relwithdebinfo", + "configuration": "RelWithDebInfo" + }, + { + "name": "windows-x64-vs2022-ynoshell-release", + "configurePreset": "windows-x64-vs2022-ynoshell-relwithdebinfo", + "configuration": "Release" + }, { "name": "macos-debug", "configurePreset": "macos-debug" @@ -2082,4 +2118,4 @@ } ], "testPresets": [] -} \ No newline at end of file +} diff --git a/builds/cmake/VSWhere.cmake b/builds/cmake/VSWhere.cmake new file mode 100644 index 000000000..c2ea310a9 --- /dev/null +++ b/builds/cmake/VSWhere.cmake @@ -0,0 +1,132 @@ +#---------------------------------------------------------------------------------------------------------------------- +# MIT License +# +# Copyright (c) 2021 Mark Schofield +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +#---------------------------------------------------------------------------------------------------------------------- +include_guard() + +#[[==================================================================================================================== + toolchain_validate_vs_files + --------------------------- + + Note: Not supported for consumption outside of the toolchain files. + + Validates the the specified folder exists and contains the specified files. + + toolchain_validate_vs_files( + > + > + ...> + ) + + If the folder or files are missing, then a FATAL_ERROR is reported. +====================================================================================================================]]# +function(toolchain_validate_vs_files) + set(OPTIONS) + set(ONE_VALUE_KEYWORDS FOLDER DESCRIPTION) + set(MULTI_VALUE_KEYWORDS FILES) + + cmake_parse_arguments(PARSE_ARGV 0 VS "${OPTIONS}" "${ONE_VALUE_KEYWORDS}" "${MULTI_VALUE_KEYWORDS}") + + if(NOT EXISTS ${VS_FOLDER}) + message(FATAL_ERROR "Folder not present - ${VS_FOLDER} - ensure that the ${VS_DESCRIPTION} are installed with Visual Studio.") + endif() + + foreach(FILE ${VS_FILES}) + if(NOT EXISTS "${VS_FOLDER}/${FILE}") + message(FATAL_ERROR "File not present - ${VS_FOLDER}/${FILE} - ensure that the ${VS_DESCRIPTION} are installed with Visual Studio.") + endif() + endforeach() +endfunction() + +#[[==================================================================================================================== + findVisualStudio + ---------------- + + Finds a Visual Studio instance, and sets CMake variables based on properties of the found instance. + + findVisualStudio( + [VERSION ] + [PRERELEASE ] + [PRODUCTS ] + [REQUIRES ...] + PROPERTIES + < > + ) +====================================================================================================================]]# +function(findVisualStudio) + set(OPTIONS) + set(ONE_VALUE_KEYWORDS VERSION PRERELEASE PRODUCTS) + set(MULTI_VALUE_KEYWORDS REQUIRES PROPERTIES) + + cmake_parse_arguments(PARSE_ARGV 0 FIND_VS "${OPTIONS}" "${ONE_VALUE_KEYWORDS}" "${MULTI_VALUE_KEYWORDS}") + + find_program(VSWHERE_PATH + NAMES vswhere vswhere.exe + HINTS "$ENV{ProgramFiles\(x86\)}/Microsoft Visual Studio/Installer" + ) + + if(VSWHERE_PATH STREQUAL "VSWHERE_PATH-NOTFOUND") + message(FATAL_ERROR "'vswhere' isn't found.") + endif() + + set(VSWHERE_COMMAND ${VSWHERE_PATH} -latest) + + if(FIND_VS_PRERELEASE) + list(APPEND VSWHERE_COMMAND -prerelease) + endif() + + if(FIND_VS_PRODUCTS) + list(APPEND VSWHERE_COMMAND -products ${FIND_VS_PRODUCTS}) + endif() + + if(FIND_VS_REQUIRES) + list(APPEND VSWHERE_COMMAND -requires ${FIND_VS_REQUIRES}) + endif() + + if(FIND_VS_VERSION) + list(APPEND VSWHERE_COMMAND -version "${FIND_VS_VERSION}") + endif() + + message(VERBOSE "findVisualStudio: VSWHERE_COMMAND = ${VSWHERE_COMMAND}") + + execute_process( + COMMAND ${VSWHERE_COMMAND} + OUTPUT_VARIABLE VSWHERE_OUTPUT + ) + + message(VERBOSE "findVisualStudio: VSWHERE_OUTPUT = ${VSWHERE_OUTPUT}") + + # Matches `VSWHERE_PROPERTY` in the `VSWHERE_OUTPUT` text in the format written by vswhere. + # The matched value is assigned to the variable `VARIABLE_NAME` in the parent scope. + function(getVSWhereProperty VSWHERE_OUTPUT VSWHERE_PROPERTY VARIABLE_NAME) + string(REGEX MATCH "${VSWHERE_PROPERTY}: [^\r\n]*" VSWHERE_VALUE "${VSWHERE_OUTPUT}") + string(REPLACE "${VSWHERE_PROPERTY}: " "" VSWHERE_VALUE "${VSWHERE_VALUE}") + set(${VARIABLE_NAME} "${VSWHERE_VALUE}" PARENT_SCOPE) + endfunction() + + while(FIND_VS_PROPERTIES) + list(POP_FRONT FIND_VS_PROPERTIES VSWHERE_PROPERTY) + list(POP_FRONT FIND_VS_PROPERTIES VSWHERE_CMAKE_VARIABLE) + getVSWhereProperty("${VSWHERE_OUTPUT}" ${VSWHERE_PROPERTY} VSWHERE_VALUE) + set(${VSWHERE_CMAKE_VARIABLE} ${VSWHERE_VALUE} PARENT_SCOPE) + endwhile() +endfunction() diff --git a/builds/cmake/Windows.Kits.cmake b/builds/cmake/Windows.Kits.cmake new file mode 100644 index 000000000..fd3321487 --- /dev/null +++ b/builds/cmake/Windows.Kits.cmake @@ -0,0 +1,153 @@ +#---------------------------------------------------------------------------------------------------------------------- +# MIT License +# +# Copyright (c) 2021 Mark Schofield +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +#---------------------------------------------------------------------------------------------------------------------- +# +# | CMake Variable | Description | +# |-----------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| +# | CMAKE_SYSTEM_VERSION | The version of the operating system for which CMake is to build. Defaults to the host version. | +# | CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE | The architecture of the tooling to use. Defaults to 'arm64' on ARM64 systems, otherwise 'x64'. | +# | CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION | The version of the Windows SDK to use. Defaults to the highest installed, that is no higher than CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION_MAXIMUM | +# | CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION_MAXIMUM | The maximum version of the Windows SDK to use, for example '10.0.19041.0'. Defaults to nothing | +# | CMAKE_WINDOWS_KITS_10_DIR | The location of the root of the Windows Kits 10 directory. | +# +# The following variables will be set: +# +# | CMake Variable | Description | +# |---------------------------------------------|-------------------------------------------------------------------------------------------------------| +# | CMAKE_MT | The path to the 'mt' tool. | +# | CMAKE_RC_COMPILER | The path to the 'rc' tool. | +# | CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION | The version of the Windows SDK to be used. | +# | MDMERGE_TOOL | The path to the 'mdmerge' tool. | +# | MIDL_COMPILER | The path to the 'midl' compiler. | +# | WINDOWS_KITS_BIN_PATH | The path to the folder containing the Windows Kits binaries. | +# | WINDOWS_KITS_INCLUDE_PATH | The path to the folder containing the Windows Kits include files. | +# | WINDOWS_KITS_LIB_PATH | The path to the folder containing the Windows Kits library files. | +# | WINDOWS_KITS_REFERENCES_PATH | The path to the folder containing the Windows Kits references. | +# +include_guard() + +if(NOT CMAKE_SYSTEM_VERSION) + set(CMAKE_SYSTEM_VERSION ${CMAKE_HOST_SYSTEM_VERSION}) +endif() + +if(NOT CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE) + if(CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL ARM64) + set(CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE arm64) + else() + set(CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE x64) + endif() +endif() + +if(NOT CMAKE_WINDOWS_KITS_10_DIR) + get_filename_component(CMAKE_WINDOWS_KITS_10_DIR "[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Microsoft SDKs\\Windows\\v10.0;InstallationFolder]" ABSOLUTE CACHE) + if ("${CMAKE_WINDOWS_KITS_10_DIR}" STREQUAL "/registry") + unset(CMAKE_WINDOWS_KITS_10_DIR) + endif() +endif() + +if(NOT CMAKE_WINDOWS_KITS_10_DIR) + message(FATAL_ERROR "Unable to find an installed Windows SDK, and one wasn't specified.") +endif() + +# If a CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION wasn't specified, find the highest installed version that is no higher +# than the host version +if(NOT CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION) + file(GLOB WINDOWS_KITS_VERSIONS RELATIVE "${CMAKE_WINDOWS_KITS_10_DIR}/lib" "${CMAKE_WINDOWS_KITS_10_DIR}/lib/*") + list(FILTER WINDOWS_KITS_VERSIONS INCLUDE REGEX "10\\.0\\.") + list(SORT WINDOWS_KITS_VERSIONS COMPARE NATURAL ORDER DESCENDING) + while(WINDOWS_KITS_VERSIONS) + list(POP_FRONT WINDOWS_KITS_VERSIONS CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION) + if(NOT CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION_MAXIMUM) + message(VERBOSE "Windows.Kits: Defaulting version: ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}") + break() + endif() + + if(CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION VERSION_LESS_EQUAL CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION_MAXIMUM) + message(VERBOSE "Windows.Kits: Choosing version: ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}") + break() + endif() + + message(VERBOSE "Windows.Kits: Not suitable: ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}") + set(CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION) + endwhile() +endif() + +if(NOT CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION) + message(FATAL_ERROR "A Windows SDK could not be found.") +endif() + +set(WINDOWS_KITS_BIN_PATH "${CMAKE_WINDOWS_KITS_10_DIR}/bin/${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}" CACHE PATH "" FORCE) +set(WINDOWS_KITS_INCLUDE_PATH "${CMAKE_WINDOWS_KITS_10_DIR}/include/${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}" CACHE PATH "" FORCE) +set(WINDOWS_KITS_LIB_PATH "${CMAKE_WINDOWS_KITS_10_DIR}/lib/${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}" CACHE PATH "" FORCE) +set(WINDOWS_KITS_REFERENCES_PATH "${CMAKE_WINDOWS_KITS_10_DIR}/References" CACHE PATH "" FORCE) +set(WINDOWS_KITS_PLATFORM_PATH "${CMAKE_WINDOWS_KITS_10_DIR}/Platforms/UAP/${CMAKE_SYSTEM_VERSION}/Platform.xml" CACHE PATH "" FORCE) + +if(NOT EXISTS ${WINDOWS_KITS_BIN_PATH}) + message(FATAL_ERROR "Windows SDK ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION} cannot be found: Folder '${WINDOWS_KITS_BIN_PATH}' does not exist.") +endif() + +if(NOT EXISTS ${WINDOWS_KITS_INCLUDE_PATH}) + message(FATAL_ERROR "Windows SDK ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION} cannot be found: Folder '${WINDOWS_KITS_INCLUDE_PATH}' does not exist.") +endif() + +if(NOT EXISTS ${WINDOWS_KITS_LIB_PATH}) + message(FATAL_ERROR "Windows SDK ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION} cannot be found: Folder '${WINDOWS_KITS_LIB_PATH}' does not exist.") +endif() + +set(CMAKE_MT "${WINDOWS_KITS_BIN_PATH}/${CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE}/mt.exe") +set(CMAKE_RC_COMPILER_INIT "${WINDOWS_KITS_BIN_PATH}/${CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE}/rc.exe") +set(CMAKE_RC_FLAGS_INIT "/nologo") + +set(MIDL_COMPILER "${WINDOWS_KITS_BIN_PATH}/${CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE}/midl.exe") +set(MDMERGE_TOOL "${WINDOWS_KITS_BIN_PATH}/${CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE}/mdmerge.exe") + +# Windows SDK +if(CMAKE_SYSTEM_PROCESSOR STREQUAL AMD64) + set(WINDOWS_KITS_TARGET_ARCHITECTURE x64) +elseif((CMAKE_SYSTEM_PROCESSOR STREQUAL ARM) + OR (CMAKE_SYSTEM_PROCESSOR STREQUAL ARM64) + OR (CMAKE_SYSTEM_PROCESSOR STREQUAL X86)) + set(WINDOWS_KITS_TARGET_ARCHITECTURE ${CMAKE_SYSTEM_PROCESSOR}) +elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL x64) + message(WARNING "CMAKE_SYSTEM_PROCESSOR should be 'AMD64', not 'x64'. WindowsToolchain will stop recognizing 'x64' in a future release.") + set(WINDOWS_KITS_TARGET_ARCHITECTURE x64) +elseif((CMAKE_SYSTEM_PROCESSOR STREQUAL arm) + OR (CMAKE_SYSTEM_PROCESSOR STREQUAL arm64) + OR (CMAKE_SYSTEM_PROCESSOR STREQUAL x86)) + message(WARNING "CMAKE_SYSTEM_PROCESSOR (${CMAKE_SYSTEM_PROCESSOR}) should be upper-case. WindowsToolchain will stop recognizing non-upper-case forms in a future release.") + set(WINDOWS_KITS_TARGET_ARCHITECTURE ${CMAKE_SYSTEM_PROCESSOR}) +else() + message(FATAL_ERROR "Unable identify Windows Kits architecture for CMAKE_SYSTEM_PROCESSOR ${CMAKE_SYSTEM_PROCESSOR}") +endif() + +foreach(LANG C CXX RC ASM_MASM) + list(APPEND CMAKE_${LANG}_STANDARD_INCLUDE_DIRECTORIES "${WINDOWS_KITS_INCLUDE_PATH}/ucrt") + list(APPEND CMAKE_${LANG}_STANDARD_INCLUDE_DIRECTORIES "${WINDOWS_KITS_INCLUDE_PATH}/shared") + list(APPEND CMAKE_${LANG}_STANDARD_INCLUDE_DIRECTORIES "${WINDOWS_KITS_INCLUDE_PATH}/um") + list(APPEND CMAKE_${LANG}_STANDARD_INCLUDE_DIRECTORIES "${WINDOWS_KITS_INCLUDE_PATH}/winrt") + list(APPEND CMAKE_${LANG}_STANDARD_INCLUDE_DIRECTORIES "${WINDOWS_KITS_INCLUDE_PATH}/cppwinrt") +endforeach() + +link_directories("${WINDOWS_KITS_LIB_PATH}/ucrt/${WINDOWS_KITS_TARGET_ARCHITECTURE}") +link_directories("${WINDOWS_KITS_LIB_PATH}/um/${WINDOWS_KITS_TARGET_ARCHITECTURE}") +link_directories("${WINDOWS_KITS_REFERENCES_PATH}/${WINDOWS_KITS_TARGET_ARCHITECTURE}") diff --git a/builds/cmake/Windows.MSVC.toolchain.cmake b/builds/cmake/Windows.MSVC.toolchain.cmake new file mode 100644 index 000000000..7edab8e26 --- /dev/null +++ b/builds/cmake/Windows.MSVC.toolchain.cmake @@ -0,0 +1,279 @@ +#---------------------------------------------------------------------------------------------------------------------- +# MIT License +# +# Copyright (c) 2021 Mark Schofield +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +#---------------------------------------------------------------------------------------------------------------------- +# +# This CMake toolchain file configures a CMake, non-'Visual Studio Generator' build to use +# the MSVC compilers and tools. +# +# The following variables can be used to configure the behavior of this toolchain file: +# +# | CMake Variable | Description | +# |---------------------------------------------|--------------------------------------------------------------------------------------------------------------------------| +# | CMAKE_SYSTEM_PROCESSOR | The processor to compiler for. One of 'X86', 'AMD64', 'ARM', 'ARM64'. Defaults to ${CMAKE_HOST_SYSTEM_PROCESSOR}. | +# | CMAKE_SYSTEM_VERSION | The version of the operating system for which CMake is to build. Defaults to the host version. | +# | CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE | The architecture of the tooling to use. Defaults to 'arm64' on ARM64 systems, otherwise 'x64'. | +# | CMAKE_VS_PRODUCTS | One or more Visual Studio Product IDs to consider. Defaults to '*' | +# | CMAKE_VS_VERSION_PRERELEASE | Whether 'prerelease' versions of Visual Studio should be considered. Defaults to 'OFF' | +# | CMAKE_VS_VERSION_RANGE | A verson range for VS instances to find. For example, '[16.0,17.0)' will find versions '16.*'. Defaults to '[16.0,17.0)' | +# | CMAKE_WINDOWS_KITS_10_DIR | The location of the root of the Windows Kits 10 directory. | +# | TOOLCHAIN_UPDATE_PROGRAM_PATH | Whether the toolchain should update CMAKE_PROGRAM_PATH. Defaults to 'ON'. | +# | TOOLCHAIN_ADD_VS_NINJA_PATH | Whether the toolchain should add the path to the VS Ninja to the CMAKE_SYSTEM_PROGRAM_PATH. Defaults to 'ON'. | +# | VS_EXPERIMENTAL_MODULE | Whether experimental module support should be enabled. | +# | VS_INSTALLATION_PATH | The location of the root of the Visual Studio installation. If not specified VSWhere will be used to search for one. | +# | VS_PLATFORM_TOOLSET_VERSION | The version of the MSVC toolset to use. For example, 14.29.30133. Defaults to the highest available. | +# | VS_USE_SPECTRE_MITIGATION_ATLMFC_RUNTIME | Whether the compiler should link with the ATLMFC runtime that uses 'Spectre' mitigations. Defaults to 'OFF'. | +# | VS_USE_SPECTRE_MITIGATION_RUNTIME | Whether the compiler should link with a runtime that uses 'Spectre' mitigations. Defaults to 'OFF'. | +# +# The toolchain file will set the following variables: +# +# | CMake Variable | Description | +# |---------------------------------------------|-------------------------------------------------------------------------------------------------------| +# | CMAKE_C_COMPILER | The path to the C compiler to use. | +# | CMAKE_CXX_COMPILER | The path to the C++ compiler to use. | +# | CMAKE_MT | The path to the 'mt.exe' tool to use. | +# | CMAKE_RC_COMPILER | The path tp the 'rc.exe' tool to use. | +# | CMAKE_SYSTEM_NAME | "Windows", when cross-compiling | +# | CMAKE_VS_PLATFORM_TOOLSET_VERSION | The version of the MSVC toolset being used - e.g. 14.29.30133. | +# | WIN32 | 1 | +# | MSVC | 1 | +# | MSVC_VERSION | The '' version of the C++ compiler being used. For example, '1929' | +# +# Other configuration: +# +# * If the 'CMAKE_CUDA_COMPILER' is set, and 'CMAKE_CUDA_HOST_COMPILER' is not set, and ENV{CUDAHOSTCXX} not defined +# then 'CMAKE_CUDA_HOST_COMPILER' is set to the value of 'CMAKE_CXX_COMPILER'. +# +# Resources: +# +# +cmake_minimum_required(VERSION 3.20) + +include_guard() + +# If `CMAKE_HOST_SYSTEM_NAME` is not 'Windows', there's nothing to do. +if(NOT (CMAKE_HOST_SYSTEM_NAME STREQUAL Windows)) + return() +endif() + +option(TOOLCHAIN_UPDATE_PROGRAM_PATH "Whether the toolchain should update CMAKE_PROGRAM_PATH." ON) +option(TOOLCHAIN_ADD_VS_NINJA_PATH "Whether the toolchain should add the path to the VS Ninja to the CMAKE_SYSTEM_PROGRAM_PATH." ON) + +set(UNUSED ${CMAKE_TOOLCHAIN_FILE}) # Note: only to prevent cmake unused variable warninig +list(APPEND CMAKE_TRY_COMPILE_PLATFORM_VARIABLES + CMAKE_SYSTEM_PROCESSOR + CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE + CMAKE_VS_PRODUCTS + CMAKE_VS_VERSION_PRERELEASE + CMAKE_VS_VERSION_RANGE + VS_INSTALLATION_PATH + VS_INSTALLATION_VERSION + VS_PLATFORM_TOOLSET_VERSION +) +set(WIN32 1) +set(MSVC 1) + +include("${CMAKE_CURRENT_LIST_DIR}/VSWhere.cmake") + +# If `CMAKE_SYSTEM_PROCESSOR` isn't set, default to `CMAKE_HOST_SYSTEM_PROCESSOR` +if(NOT CMAKE_SYSTEM_PROCESSOR) + set(CMAKE_SYSTEM_PROCESSOR ${CMAKE_HOST_SYSTEM_PROCESSOR}) +endif() + +# If `CMAKE_SYSTEM_PROCESSOR` is not equal to `CMAKE_HOST_SYSTEM_PROCESSOR`, this is cross-compilation. +# CMake expects `CMAKE_SYSTEM_NAME` to be set to reflect cross-compilation. +if(NOT (CMAKE_SYSTEM_PROCESSOR STREQUAL ${CMAKE_HOST_SYSTEM_PROCESSOR})) + set(CMAKE_SYSTEM_NAME Windows) +endif() + +if(NOT CMAKE_VS_VERSION_RANGE) + set(CMAKE_VS_VERSION_RANGE "[16.0,)") +endif() + +if(NOT CMAKE_VS_VERSION_PRERELEASE) + set(CMAKE_VS_VERSION_PRERELEASE OFF) +endif() + +if(NOT CMAKE_VS_PRODUCTS) + set(CMAKE_VS_PRODUCTS "*") +endif() + +if(NOT CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE) + if(CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL ARM64) + set(CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE arm64) + else() + set(CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE x64) + endif() +endif() + +if(NOT VS_USE_SPECTRE_MITIGATION_RUNTIME) + set(VS_USE_SPECTRE_MITIGATION_RUNTIME OFF) +endif() + +# Find Visual Studio +# +if(NOT VS_INSTALLATION_PATH) + findVisualStudio( + VERSION ${CMAKE_VS_VERSION_RANGE} + PRERELEASE ${CMAKE_VS_VERSION_PRERELEASE} + PRODUCTS ${CMAKE_VS_PRODUCTS} + PROPERTIES + installationVersion VS_INSTALLATION_VERSION + installationPath VS_INSTALLATION_PATH + ) +endif() + +message(VERBOSE "VS_INSTALLATION_VERSION = ${VS_INSTALLATION_VERSION}") +message(VERBOSE "VS_INSTALLATION_PATH = ${VS_INSTALLATION_PATH}") + +if(NOT VS_INSTALLATION_PATH) + message(FATAL_ERROR "Unable to find Visual Studio") +endif() + +cmake_path(NORMAL_PATH VS_INSTALLATION_PATH) + +set(VS_MSVC_PATH "${VS_INSTALLATION_PATH}/VC/Tools/MSVC") + +# Use 'VS_PLATFORM_TOOLSET_VERSION' to resolve 'CMAKE_VS_PLATFORM_TOOLSET_VERSION' +# +if(NOT VS_PLATFORM_TOOLSET_VERSION) + if(VS_TOOLSET_VERSION) + message(WARNING "Old versions of WindowsToolchain incorrectly used 'VS_TOOLSET_VERSION' to specify the VS toolset version. This functionality is being deprecated - please use 'VS_PLATFORM_TOOLSET_VERSION' instead.") + set(VS_PLATFORM_TOOLSET_VERSION ${VS_TOOLSET_VERSION}) + else() + file(GLOB VS_PLATFORM_TOOLSET_VERSIONS RELATIVE ${VS_MSVC_PATH} ${VS_MSVC_PATH}/*) + list(SORT VS_PLATFORM_TOOLSET_VERSIONS COMPARE NATURAL ORDER DESCENDING) + list(POP_FRONT VS_PLATFORM_TOOLSET_VERSIONS VS_PLATFORM_TOOLSET_VERSION) + unset(VS_PLATFORM_TOOLSET_VERSIONS) + endif() +endif() + +set(CMAKE_VS_PLATFORM_TOOLSET_VERSION ${VS_PLATFORM_TOOLSET_VERSION}) +set(VS_TOOLSET_PATH "${VS_INSTALLATION_PATH}/VC/Tools/MSVC/${CMAKE_VS_PLATFORM_TOOLSET_VERSION}") + +# Set the tooling variables, include_directories and link_directories +# + +# Map CMAKE_SYSTEM_PROCESSOR values to CMAKE_VS_PLATFORM_TOOLSET_ARCHITECTURE that identifies the tools that should +# be used to produce code for the CMAKE_SYSTEM_PROCESSOR. +if(CMAKE_SYSTEM_PROCESSOR STREQUAL AMD64) + set(CMAKE_VS_PLATFORM_TOOLSET_ARCHITECTURE x64) +elseif((CMAKE_SYSTEM_PROCESSOR STREQUAL ARM) + OR (CMAKE_SYSTEM_PROCESSOR STREQUAL ARM64) + OR (CMAKE_SYSTEM_PROCESSOR STREQUAL X86)) + set(CMAKE_VS_PLATFORM_TOOLSET_ARCHITECTURE ${CMAKE_SYSTEM_PROCESSOR}) +elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL x64) + message(WARNING "CMAKE_SYSTEM_PROCESSOR should be 'AMD64', not 'x64'. WindowsToolchain will stop recognizing 'x64' in a future release.") + set(CMAKE_VS_PLATFORM_TOOLSET_ARCHITECTURE x64) +elseif((CMAKE_SYSTEM_PROCESSOR STREQUAL arm) + OR (CMAKE_SYSTEM_PROCESSOR STREQUAL arm64) + OR (CMAKE_SYSTEM_PROCESSOR STREQUAL x86)) + message(WARNING "CMAKE_SYSTEM_PROCESSOR (${CMAKE_SYSTEM_PROCESSOR}) should be upper-case. WindowsToolchain will stop recognizing non-upper-case forms in a future release.") + set(CMAKE_VS_PLATFORM_TOOLSET_ARCHITECTURE ${CMAKE_SYSTEM_PROCESSOR}) +else() + message(FATAL_ERROR "Unable identify compiler architecture for CMAKE_SYSTEM_PROCESSOR ${CMAKE_SYSTEM_PROCESSOR}") +endif() + +set(CMAKE_CXX_COMPILER "${VS_TOOLSET_PATH}/bin/Host${CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE}/${CMAKE_VS_PLATFORM_TOOLSET_ARCHITECTURE}/cl.exe") +set(CMAKE_C_COMPILER "${VS_TOOLSET_PATH}/bin/Host${CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE}/${CMAKE_VS_PLATFORM_TOOLSET_ARCHITECTURE}/cl.exe") + +if(CMAKE_SYSTEM_PROCESSOR STREQUAL ARM) + set(CMAKE_CXX_FLAGS_INIT "${CMAKE_CXX_FLAGS_INIT} /EHsc") +elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL arm) + message(WARNING "CMAKE_SYSTEM_PROCESSOR (${CMAKE_SYSTEM_PROCESSOR}) should be upper-case. WindowsToolchain will stop recognizing non-upper-case forms in a future release.") + set(CMAKE_CXX_FLAGS_INIT "${CMAKE_CXX_FLAGS_INIT} /EHsc") +endif() + +# Compiler +foreach(LANG C CXX RC) + list(APPEND CMAKE_${LANG}_STANDARD_INCLUDE_DIRECTORIES "${VS_TOOLSET_PATH}/ATLMFC/include") + list(APPEND CMAKE_${LANG}_STANDARD_INCLUDE_DIRECTORIES "${VS_TOOLSET_PATH}/include") +endforeach() + +foreach(LANG C CXX) + # Add '/X': Do not add %INCLUDE% to include search path + set(CMAKE_${LANG}_FLAGS_INIT "${CMAKE_${LANG}_FLAGS_INIT} /X") +endforeach() + +if(VS_USE_SPECTRE_MITIGATION_ATLMFC_RUNTIME) + # Ensure that the necessary folder and files are present before adding the 'link_directories' + toolchain_validate_vs_files( + DESCRIPTION "ATLMFC Spectre libraries" + FOLDER "${VS_TOOLSET_PATH}/ATLMFC/lib/spectre/${CMAKE_VS_PLATFORM_TOOLSET_ARCHITECTURE}" + FILES + atls.lib + ) + link_directories("${VS_TOOLSET_PATH}/ATLMFC/lib/spectre/${CMAKE_VS_PLATFORM_TOOLSET_ARCHITECTURE}") +else() + link_directories("${VS_TOOLSET_PATH}/ATLMFC/lib/${CMAKE_VS_PLATFORM_TOOLSET_ARCHITECTURE}") +endif() + +if(VS_USE_SPECTRE_MITIGATION_RUNTIME) + # Ensure that the necessary folder and files are present before adding the 'link_directories' + toolchain_validate_vs_files( + DESCRIPTION "Spectre libraries" + FOLDER "${VS_TOOLSET_PATH}/lib/spectre/${CMAKE_VS_PLATFORM_TOOLSET_ARCHITECTURE}" + FILES + msvcrt.lib vcruntime.lib vcruntimed.lib + ) + link_directories("${VS_TOOLSET_PATH}/lib/spectre/${CMAKE_VS_PLATFORM_TOOLSET_ARCHITECTURE}") +else() + link_directories("${VS_TOOLSET_PATH}/lib/${CMAKE_VS_PLATFORM_TOOLSET_ARCHITECTURE}") +endif() + +link_directories("${VS_TOOLSET_PATH}/lib/x86/store/references") + +# Module support +if(VS_EXPERIMENTAL_MODULE) + set(CMAKE_CXX_FLAGS_INIT "${CMAKE_CXX_FLAGS_INIT} /experimental:module") + set(CMAKE_CXX_FLAGS_INIT "${CMAKE_CXX_FLAGS_INIT} /stdIfcDir \"${VS_TOOLSET_PATH}/ifc/${CMAKE_VS_PLATFORM_TOOLSET_ARCHITECTURE}\"") +endif() + +# Windows Kits +include("${CMAKE_CURRENT_LIST_DIR}/Windows.Kits.cmake") + +# CUDA support +# +# If a CUDA compiler is specified, and a host compiler wasn't specified, set 'CMAKE_CXX_COMPILER' +# as the host compiler. +if(CMAKE_CUDA_COMPILER) + if((NOT CMAKE_CUDA_HOST_COMPILER) AND (NOT DEFINED ENV{CUDAHOSTCXX})) + set(CMAKE_CUDA_HOST_COMPILER "${CMAKE_CXX_COMPILER}") + endif() +endif() + +# If 'TOOLCHAIN_UPDATE_PROGRAM_PATH' is selected, update CMAKE_PROGRAM_PATH. +# +if(TOOLCHAIN_UPDATE_PROGRAM_PATH) + list(APPEND CMAKE_PROGRAM_PATH "${VS_TOOLSET_PATH}/bin/Host${CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE}/${CMAKE_VS_PLATFORM_TOOLSET_ARCHITECTURE}") + list(APPEND CMAKE_PROGRAM_PATH "${WINDOWS_KITS_BIN_PATH}/${CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE}") +endif() + +# If the CMAKE_GENERATOR is Ninja-based, and the path to the Visual Studio-installed Ninja is present, add it to +# the CMAKE_SYSTEM_PROGRAM_PATH. 'find_program' searches CMAKE_SYSTEM_PROGRAM_PATH after the environment path, so +# an installed Ninja would be preferred. +# +if( (CMAKE_GENERATOR MATCHES "^Ninja") AND + (EXISTS "${VS_INSTALLATION_PATH}/Common7/IDE/CommonExtensions/Microsoft/CMake/Ninja") AND + (TOOLCHAIN_ADD_VS_NINJA_PATH)) + list(APPEND CMAKE_SYSTEM_PROGRAM_PATH "${VS_INSTALLATION_PATH}/Common7/IDE/CommonExtensions/Microsoft/CMake/Ninja") +endif() diff --git a/src/input.cpp b/src/input.cpp index 8d31967c9..6eb924e17 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -52,7 +52,7 @@ namespace Input { std::bitset triggered, repeated, released; Input::KeyStatus raw_triggered, raw_pressed, raw_released; Composition composition{}; - std::string text_input; + std::string text_input = ""; int dir4; int dir8; std::unique_ptr source; diff --git a/src/input.h b/src/input.h index 14ff31529..a84a8cf47 100644 --- a/src/input.h +++ b/src/input.h @@ -346,9 +346,9 @@ namespace Input { public: /** if false, all other fields are considered garbage */ bool active = false; - std::string text; - int sel_start; - int sel_length; + std::string text = ""; + int sel_start = 0; + int sel_length = 0; }; /** The current composition received from IME */ diff --git a/src/multiplayer/chat_overlay.cpp b/src/multiplayer/chat_overlay.cpp index d64823f35..90626d2b1 100644 --- a/src/multiplayer/chat_overlay.cpp +++ b/src/multiplayer/chat_overlay.cpp @@ -14,6 +14,7 @@ * You should have received a copy of the GNU General Public License * along with EasyRPG Player. If not, see . */ +#include "multiplayer/overlay_utils.h" #include #include @@ -199,6 +200,10 @@ void ChatOverlay::Draw(Bitmap& dst) { line_height = OverlayUtils::LargeScreen() ? 56 : 18; } + if (std::any_of(line->cbegin(), line->cend(), [](const std::shared_ptr& comp) { return bool(comp->Downcast()); })) { + line_height = ChatScreenshot::sizer().y; + } + // end override line height y_offset += line_height - text_height; @@ -226,26 +231,40 @@ void ChatOverlay::Draw(Bitmap& dst) { } offset += Text::Draw(*bitmap, offset, y, font, offwhite, message->account ? "] " : "> ").x; } + int baseline = 0; for (auto& span : *line) { if (auto str = span->Downcast()) { - offset += Text::Draw(*bitmap, offset, y, font, str->color, str->string).x; + offset += Text::Draw(*bitmap, offset, y + (baseline ? baseline - text_height : 0), font, str->color, str->string).x; } else if (auto emoji = span->Downcast()) { Point dims = emoji->GetSize(); + int comp_y = y + (baseline ? baseline - text_height : 0); if (emoji->bitmap) { double zoom = emoji->bitmap->GetRect().y / (double)text_height; - bitmap->StretchBlit({offset, y, line_height, line_height}, *emoji->bitmap, emoji->bitmap->GetRect(), 255); + bitmap->StretchBlit({offset, comp_y, line_height, line_height}, *emoji->bitmap, emoji->bitmap->GetRect(), 255); } else { // the emoji is still live, request it now emoji->RequestBitmap(this); - bitmap->FillRect({ offset, y, line_height, line_height }, offwhite); + bitmap->FillRect({ offset, comp_y, line_height, line_height }, offwhite); } offset += line_height; } + else if (auto screenshot = span->Downcast()) { + Point dims = screenshot->GetSize(); + if (screenshot->bitmap) { + double zoom = screenshot->bitmap->GetRect().y / (double)text_height; + bitmap->StretchBlit({offset, y, dims.x, dims.y}, *screenshot->bitmap, screenshot->bitmap->GetRect(), 255); + } + else { + bitmap->FillRect({ offset, y, dims.x, dims.y }, offwhite); + } + offset += dims.x; + baseline = std::max(baseline, dims.y); + } else if (auto box = span->Downcast()) { int width = box->GetSize().x; - bitmap->FillRect({ offset, y, width, text_height }, offwhite); + bitmap->FillRect({ offset, y + (baseline ? baseline - text_height : 0), width, text_height }, offwhite); offset += width; } } @@ -273,11 +292,11 @@ void ChatOverlay::Draw(Bitmap& dst) { } ChatOverlayMessage& ChatOverlay::AddMessage( - std::string_view message, std::string_view sender, std::string_view system, std::string_view badge, + std::string_view message, std::string_view sender, std::string_view sender_uuid, std::string_view system, std::string_view badge, bool account, bool global, int rank) { counter = 0; - auto& ret = messages.emplace_back(this, std::string(message), std::string(sender), std::string(system), std::string(badge), account, global, rank); + auto& ret = messages.emplace_back(this, std::string(message), std::string(sender), std::string(sender_uuid), std::string(system), std::string(badge), account, global, rank); while (messages.size() > message_max) { messages.pop_front(); @@ -291,7 +310,7 @@ ChatOverlayMessage& ChatOverlay::AddMessage( } void ChatOverlay::AddSystemMessage(StringView msg) { - auto& chatmsg = messages.emplace_back(this, std::string(msg), std::string(""), "", std::string(""), false, true, 0); + auto& chatmsg = messages.emplace_back(this, std::string(msg), "", "", "", std::string(""), false, true, 0); chatmsg.text.erase(chatmsg.text.begin()); for (auto& span : chatmsg.text) { if (auto string = span->Downcast()) { @@ -452,7 +471,8 @@ bool ChatOverlay::IsTriggeredOrRepeating(CustomKeyTimers key) { std::vector> ChatOverlayMessage::Convert(ChatOverlay* parent, bool has_badge) const { StringView msg(text_orig); std::vector> out; - static std::regex pattern(":(\\w+):"); + static std::regex emoji_pattern(":(\\w+):"); + static std::regex screenshot_pattern(R"regex(\[(t)?([\w\d]{16})(?::(\d)+)?\])regex"); int text_height = OverlayUtils::ChatTextHeight(); auto font = Font::ChatText(); @@ -473,24 +493,39 @@ std::vector> ChatOverlayMessage::Convert(ChatOver return Point{ rect.width, rect.height }; }, ChatComponents::Header)); - auto begin = std::cregex_iterator(msg.begin(), msg.end(), pattern); - auto end = decltype(begin){}; - + auto it = msg.begin(); auto last_end = msg.begin(); - for (auto& it = begin; it != end; ++it) { - auto& match = *it; + while (it != msg.end()) { + std::cmatch match; + if (std::regex_search(it, msg.end(), match, emoji_pattern)) { + StringView between(last_end, match[0].first - last_end); + if (!between.empty()) { + out.emplace_back(std::make_shared(between)); + } - StringView between(last_end, match[0].first - last_end); - if (!between.empty()) { - out.emplace_back(std::make_shared(between)); + auto emoji_key = match[1].str(); + if (emojis.empty() || emojis.find(emoji_key) != emojis.end()) + out.emplace_back(std::make_shared(parent, text_height, text_height, emoji_key)); + else + out.emplace_back(std::make_shared(match[0].str())); + last_end = match[0].second; + it = last_end; } + else if (std::regex_search(it, msg.end(), match, screenshot_pattern)) { + StringView between(last_end, match[0].first - last_end); + if (!between.empty()) { + out.emplace_back(std::make_shared(between)); + } - auto emoji_key = match[1].str(); - if (emojis.empty() || emojis.find(emoji_key) != emojis.end()) - out.emplace_back(std::make_shared(parent, OverlayUtils::ChatTextHeight(), OverlayUtils::ChatTextHeight(), emoji_key)); - else - out.emplace_back(std::make_shared(match[0].str())); - last_end = match[0].second; + auto temp = match[1].str() == "t"; + auto id = match[2].str(); + out.emplace_back(std::make_shared(parent, sender_uuid, id, temp, false)); + last_end = match[0].second; + it = last_end; + } + else { + break; + } } StringView last(last_end, msg.end() - last_end); @@ -502,9 +537,9 @@ std::vector> ChatOverlayMessage::Convert(ChatOver } ChatOverlayMessage::ChatOverlayMessage( - ChatOverlay* parent_, std::string text, std::string sender, std::string system_, std::string badge, + ChatOverlay* parent_, std::string text, std::string sender, std::string sender_uuid, std::string system_, std::string badge, bool account, bool global, int rank) : - parent(parent_), text_orig(std::move(text)), sender(std::move(sender)), system(system_), account(account), global(global), rank(rank) + parent(parent_), text_orig(std::move(text)), sender(std::move(sender)), sender_uuid(std::move(sender_uuid)), system(system_), account(account), global(global), rank(rank) { bool has_badge = badge != "null" && !badge.empty(); @@ -568,8 +603,32 @@ void ChatEmoji::RequestBitmap(ChatOverlay* parent_) { req->Start(); } -ChatScreenshot::ChatScreenshot(ChatOverlay* parent, std::string id_, bool temp, bool spoiler) : - parent(parent), id(std::move(id_)), temp(temp), spoiler(spoiler) +ChatScreenshot::ChatScreenshot(ChatOverlay* parent, std::string uuid_, std::string id_, bool temp, bool spoiler) : + ChatComponent(sizer, ChatComponents::Screenshot), parent(parent), uuid(std::move(uuid_)), id(std::move(id_)), temp(temp), spoiler(spoiler) { + uv_queue_work(uv_default_loop(), &task, + [](uv_work_t* task) { + #define EP_CONTAINER_OF(ptr, type, member) (type*)((char*)ptr - offsetof(type, member)) + auto comp = EP_CONTAINER_OF(task, ChatScreenshot, task); + #undef EP_CONTAINER_OF + + std::string endpoint(GMI().ApiEndpoint("screenshots/")); + if (comp->temp) + endpoint.append("temp/"); + endpoint.append(fmt::format("{}/{}.png", comp->uuid, comp->id)); + auto resp = cpr::Get(cpr::Url{endpoint}); + Output::Debug("{} {}", resp.status_code, resp.url.c_str()); + if (!(resp.status_code >= 200 && resp.status_code < 300)) + return; + comp->bitmap = Bitmap::Create((uint8_t*)resp.text.c_str(), resp.text.size(), false); + comp->parent->MarkDirty(); + }, + [](uv_work_t* task, int) { + }); } +Point ChatScreenshot::sizer() { + if (OverlayUtils::LargeScreen()) + return { 192, 144 }; + return { 64, 48 }; +} diff --git a/src/multiplayer/chat_overlay.h b/src/multiplayer/chat_overlay.h index c0e699365..32d4a1128 100644 --- a/src/multiplayer/chat_overlay.h +++ b/src/multiplayer/chat_overlay.h @@ -22,6 +22,7 @@ #include #include #include +#include #include "drawable.h" #include "bitmap.h" @@ -29,11 +30,13 @@ #include "async_handler.h" #include "game_clock.h" #include "input.h" +#include "point.h" enum ChatComponents { Box, String, Emoji, + Screenshot, // no associated components Header, END @@ -71,10 +74,11 @@ inline Point ChatComponent::GetSize() const { class ChatOverlayMessage { public: ChatOverlayMessage( - ChatOverlay* parent, std::string text, std::string sender, std::string system, std::string badge, bool account, bool global, int rank); + ChatOverlay* parent, std::string text, std::string sender, std::string sender_uuid, std::string system, std::string badge, bool account, bool global, int rank); std::string text_orig; std::vector> text; std::string sender; + std::string sender_uuid; std::string system; BitmapRef system_bm; BitmapRef badge; @@ -105,7 +109,7 @@ class ChatOverlay : public Drawable { /** Run by Scene_Overlay on behalf of this component. */ void UpdateScene(); ChatOverlayMessage& AddMessage( - std::string_view msg, std::string_view sender, std::string_view system = "", std::string_view badge = "", + std::string_view msg, std::string_view sender, std::string_view sender_uuid, std::string_view system = "", std::string_view badge = "", bool account = true, bool global = true, int rank = 0); void SetShowAll(bool show_all); inline void SetShowAll() { SetShowAll(!show_all); } @@ -182,12 +186,16 @@ class ChatScreenshot : public ChatComponent { public: bool spoiler; bool temp; + std::string uuid; std::string id; BitmapRef bitmap = nullptr; FileRequestBinding request = nullptr; ChatOverlay* parent = nullptr; - ChatScreenshot(ChatOverlay* parent, std::string id, bool temp, bool spoiler); + ChatScreenshot(ChatOverlay* parent, std::string uuid, std::string id, bool temp, bool spoiler); + static Point sizer(); +private: + uv_work_t task{}; }; @@ -199,6 +207,8 @@ template<> struct ChatComponentsMap { using type = ChatEmoji; }; template<> struct ChatComponentsMap { using type = ChatComponent; }; +template<> +struct ChatComponentsMap { using type = ChatScreenshot; }; template inline typename ChatComponentsMap::type* ChatComponent::Downcast() { diff --git a/src/multiplayer/game_multiplayer.cpp b/src/multiplayer/game_multiplayer.cpp index 3a9ee8b43..65cd72c3e 100644 --- a/src/multiplayer/game_multiplayer.cpp +++ b/src/multiplayer/game_multiplayer.cpp @@ -545,7 +545,8 @@ void Game_Multiplayer::InitConnection() { std::string badge; bool account = false; int rank = 0; - if (auto player = playerdata.find((std::string)(*it)["uuid"]); player != playerdata.end()) { + std::string uuid((*it)["uuid"]); + if (auto player = playerdata.find(uuid); player != playerdata.end()) { name = player->second.name; system = player->second.systemName; badge = player->second.badge; @@ -553,7 +554,7 @@ void Game_Multiplayer::InitConnection() { rank = player->second.rank; } std::string contents((*it)["contents"]); - chatoverlay.AddMessage(contents, name, system, badge, account, true, rank); + chatoverlay.AddMessage(contents, name, uuid, system, badge, account, true, rank); lastmsgid = (std::string)(*it)["msgId"]; } if (!username.empty()) { @@ -585,7 +586,7 @@ void Game_Multiplayer::InitConnection() { account = player->second.account; rank = player->second.rank; } - Graphics::GetChatOverlay().AddMessage(p.msg, name, system, badge, account, true, rank); + Graphics::GetChatOverlay().AddMessage(p.msg, name, p.uuid, system, badge, account, true, rank); lastmsgid = p.msgid; }); sessionConn.RegisterHandler("say", [this](SessionSay& p) { @@ -602,7 +603,7 @@ void Game_Multiplayer::InitConnection() { account = player->second.account; rank = player->second.rank; } - Graphics::GetChatOverlay().AddMessage(p.msg, name, system, badge, account, false, rank); + Graphics::GetChatOverlay().AddMessage(p.msg, name, p.uuid, system, badge, account, false, rank); }); sessionConn.RegisterHandler("p", [this](SessionPlayerInfo& p) { auto& player = playerdata[p.uuid]; diff --git a/src/multiplayer/yno_connection.cpp b/src/multiplayer/yno_connection.cpp index a9f568d1d..6c3ca126b 100644 --- a/src/multiplayer/yno_connection.cpp +++ b/src/multiplayer/yno_connection.cpp @@ -13,7 +13,7 @@ # include # include # include -# if defined(_WIN32) +# if defined(_MSC_VER) # include # elif defined(__linux__) # include @@ -391,7 +391,7 @@ void YNOConnection::Open(std::string_view uri) { if (!self->IsConnected()) return; self->Close(); bool expected_value = false; -#if defined(_WIN32) +#if defined(_MSC_VER) if (!WaitOnAddress(self->ConnectedFutex(), &expected_value, sizeof(expected_value), INFINITE)) Output::Debug("Failed to wait for previous session to close: {}", GetLastError()); #else From 85dcf7c99bc33746e0f91af037a36c8361ee2ab0 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Wed, 16 Apr 2025 00:58:48 -0400 Subject: [PATCH 16/18] Deprecate StringView, add webp emojis --- CMakeLists.txt | 6 + src/audio_generic.cpp | 53 +-------- src/audio_generic.h | 14 +-- src/cache.cpp | 4 +- src/cache.h | 6 +- src/config_param.h | 2 +- src/filefinder.cpp | 2 +- src/filefinder.h | 2 +- src/game_actor.cpp | 2 +- src/game_actor.h | 2 +- src/game_message.cpp | 8 +- src/image_webp.cpp | 76 ++++++++++++ src/image_webp.h | 45 +++++++ src/input.cpp | 4 +- src/multiplayer/chat_overlay.cpp | 168 ++++++++++++++++++++++++--- src/multiplayer/chat_overlay.h | 16 ++- src/multiplayer/connection.cpp | 8 +- src/multiplayer/game_multiplayer.cpp | 2 +- src/multiplayer/game_multiplayer.h | 2 +- src/multiplayer/scene_nexus.cpp | 2 +- src/multiplayer/scene_nexus.h | 2 +- src/platform/sdl/sdl2_ui.cpp | 3 +- src/window_settings.cpp | 4 +- src/window_stringinput.cpp | 4 +- src/window_stringinput.h | 2 +- 25 files changed, 332 insertions(+), 107 deletions(-) create mode 100644 src/image_webp.cpp create mode 100644 src/image_webp.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e5c2f039..4a98de92e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -253,6 +253,8 @@ add_library(${PROJECT_NAME} OBJECT src/image_png.h src/image_xyz.cpp src/image_xyz.h + src/image_webp.cpp + src/image_webp.h src/input_buttons_desktop.cpp src/input_buttons.h src/input.cpp @@ -626,6 +628,10 @@ if(${PLAYER_SHELL} STREQUAL "yno") FetchContent_MakeAvailable(webview) target_link_libraries(${PROJECT_NAME} webview::core) + player_find_package(NAME GIF TARGET GIF::GIF REQUIRED) + find_package(WebP CONFIG REQUIRED) + target_link_libraries(${PROJECT_NAME} WebP::webp WebP::webpdecoder WebP::webpdemux) + elseif(${PLAYER_SHELL} STREQUAL "none") # do nothing else() diff --git a/src/audio_generic.cpp b/src/audio_generic.cpp index 6f2028317..03032723c 100644 --- a/src/audio_generic.cpp +++ b/src/audio_generic.cpp @@ -301,59 +301,8 @@ bool GenericAudio::PlayOnChannel(SeChannel& chan, std::unique_ptr chan.paused = false; // Unpause channel -> Play it. return true; } -void GenericAudio::Decode(uint8_t* output_buffer, int buffer_length) { - auto [channel_active, total_volume, samples_per_frame] = GenericAudio::Decode(buffer_length); - if (channel_active) { - if (total_volume > 1.0) { - float threshold = 0.8; - for (unsigned i = 0; i < (unsigned)(samples_per_frame * 2); i++) { - float sample = mixer_buffer[i]; - float sign = (sample < 0) ? -1.0 : 1.0; - sample /= sign; - //dynamic range compression - if (sample > threshold) { - sample_buffer[i] = sign * 32768.0 * (threshold + (1.0 - threshold) * (sample - threshold) / (total_volume - threshold)); - } else { - sample_buffer[i] = sign * sample * 32768.0; - } - } - } else { - //No dynamic range compression necessary - for (unsigned i = 0; i < (unsigned)(samples_per_frame * 2); i++) { - sample_buffer[i] = mixer_buffer[i] * 32768.0; - } - } - - memcpy(output_buffer, sample_buffer.data(), buffer_length); - } else { - memset(output_buffer, '\0', buffer_length); - } -} -void GenericAudio::Decode(float* output_buffer, int buffer_length) { - auto [channel_active, total_volume, samples_per_frame] = GenericAudio::Decode(buffer_length); - if (channel_active) { - if (total_volume > 1.0) { - float threshold = 0.8; - for (unsigned i = 0; i < (unsigned)(samples_per_frame * 2); ++i) { - float sample = mixer_buffer[i]; - float sign = (sample < 0) ? -1.0 : 1.0; - sample /= sign; - if (sample > threshold) { - output_buffer[i] = sign * (threshold + (1.0 - threshold) * (sample - threshold) / (total_volume - threshold)); - } else { - output_buffer[i] = sign * sample; - } - } - } else { - memcpy(output_buffer, mixer_buffer.data(), buffer_length); - } - } else { - memset(output_buffer, '\0', buffer_length); - } -} - -GenericAudio::DecodeResult GenericAudio::Decode(int buffer_length) { +void GenericAudio::Decode(uint8_t* output_buffer, int buffer_length) { bool channel_active = false; float total_volume = 0; int samples_per_frame = buffer_length / output_format.channels / AudioDecoder::GetSamplesizeForFormat(output_format.format); diff --git a/src/audio_generic.h b/src/audio_generic.h index 1fd11b298..ec2cc80e5 100644 --- a/src/audio_generic.h +++ b/src/audio_generic.h @@ -70,7 +70,7 @@ class GenericAudio : public AudioInterface { virtual void UnlockMutex() const = 0; void Decode(uint8_t* output_buffer, int buffer_length); - void Decode(float* output_buffer, int buffer_length); + //void Decode(float* output_buffer, int buffer_length); private: struct BgmChannel { @@ -119,12 +119,12 @@ class GenericAudio : public AudioInterface { std::vector mixer_buffer = {}; std::unique_ptr midi_thread; - typedef struct DecodeResult { - bool channel_active; - float total_volume; - int samples_per_frame; - } DecodeResult; - DecodeResult Decode(int buffer_length); + //typedef struct DecodeResult { + // bool channel_active; + // float total_volume; + // int samples_per_frame; + //} DecodeResult; + //DecodeResult Decode(int buffer_length); }; #endif diff --git a/src/cache.cpp b/src/cache.cpp index a5017701e..3d3ec692c 100644 --- a/src/cache.cpp +++ b/src/cache.cpp @@ -420,11 +420,11 @@ BitmapRef Cache::System(std::string_view file, bool bg_preserve_transparent_colo return LoadBitmap(file, flags); } -BitmapRef Cache::Emoji(StringView file) { +BitmapRef Cache::Emoji(std::string_view file) { return LoadBitmap(file); } -BitmapRef Cache::Badge(StringView file) { +BitmapRef Cache::Badge(std::string_view file) { return LoadBitmap(file); } diff --git a/src/cache.h b/src/cache.h index 0e4ce5e56..000804d26 100644 --- a/src/cache.h +++ b/src/cache.h @@ -55,10 +55,10 @@ namespace Cache { BitmapRef System(std::string_view filename, bool bg_preserve_transparent_color = false); BitmapRef System2(std::string_view filename); - BitmapRef Emoji(StringView filename); - BitmapRef Badge(StringView filename); + BitmapRef Emoji(std::string_view filename); + BitmapRef Badge(std::string_view filename); - BitmapRef Tile(StringView filename, int tile_id); + BitmapRef Tile(std::string_view filename, int tile_id); BitmapRef SpriteEffect(const BitmapRef& src_bitmap, const Rect& rect, bool flip_x, bool flip_y, const Tone& tone, const Color& blend); void Clear(); diff --git a/src/config_param.h b/src/config_param.h index ff0b99919..b20469d9a 100644 --- a/src/config_param.h +++ b/src/config_param.h @@ -252,7 +252,7 @@ class LockedConfigParam final : public ConfigParam { class StringConfigParam : public ConfigParam { public: explicit StringConfigParam( - StringView name, StringView description, StringView config_section, StringView config_key, std::string value = "", bool secret = false) : + std::string_view name, std::string_view description, std::string_view config_section, std::string_view config_key, std::string value = "", bool secret = false) : ConfigParam(name, description, config_section, config_key, value), secret(secret) {} bool secret; }; diff --git a/src/filefinder.cpp b/src/filefinder.cpp index 8b749275d..50abc2663 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -499,7 +499,7 @@ Filesystem_Stream::InputStream open_generic_with_fallback(std::string_view dir, } Filesystem_Stream::InputStream FileFinder::OpenImage(std::string_view dir, std::string_view name) { - int initial_depth = dir.find("../") == 0 ? 0 : 1; + int initial_depth = StartsWith(dir, "../") ? 0 : 1; DirectoryTree::Args args = { MakePath(dir, name), IMG_TYPES, initial_depth, false }; return open_generic_with_fallback(dir, name, args); } diff --git a/src/filefinder.h b/src/filefinder.h index effa98df6..b4027b757 100644 --- a/src/filefinder.h +++ b/src/filefinder.h @@ -38,7 +38,7 @@ * insensitive files paths. */ namespace FileFinder { - constexpr const auto IMG_TYPES = Utils::MakeSvArray(".bmp", ".png", ".xyz"); + constexpr const auto IMG_TYPES = Utils::MakeSvArray(".bmp", ".png", ".xyz", ".gif", ".webp"); constexpr const auto MUSIC_TYPES = Utils::MakeSvArray( ".opus", ".oga", ".ogg", ".wav", ".mid", ".midi", ".mp3", ".wma"); constexpr const auto SOUND_TYPES = Utils::MakeSvArray( diff --git a/src/game_actor.cpp b/src/game_actor.cpp index ef2188003..f1ef1f0e2 100644 --- a/src/game_actor.cpp +++ b/src/game_actor.cpp @@ -1079,7 +1079,7 @@ void Game_Actor::ChangeClass(int new_class_id, } } -StringView Game_Actor::ClassName() const { +std::string_view Game_Actor::ClassName() const { if (!GetClass()) { return {}; } diff --git a/src/game_actor.h b/src/game_actor.h index de4dee8ee..530d68e1b 100644 --- a/src/game_actor.h +++ b/src/game_actor.h @@ -841,7 +841,7 @@ class Game_Actor final : public Game_Battler { * * @return Rpg2k3 hero class name */ - StringView ClassName() const; + std::string_view ClassName() const; /** * Gets battle commands. diff --git a/src/game_message.cpp b/src/game_message.cpp index 8a25e421b..81a19f06a 100644 --- a/src/game_message.cpp +++ b/src/game_message.cpp @@ -85,11 +85,11 @@ int Game_Message::GetRealPosition() { } } -int Game_Message::WordWrap(StringView line, const int limit, const Game_Message::WordWrapCallback& callback) { +int Game_Message::WordWrap(std::string_view line, const int limit, const Game_Message::WordWrapCallback& callback) { return WordWrap(line, limit, callback, *Font::Default()); } -int Game_Message::WordWrap(StringView line, const int limit, const Game_Message::WordWrapCallback& callback, const Font& font) { +int Game_Message::WordWrap(std::string_view line, const int limit, const Game_Message::WordWrapCallback& callback, const Font& font) { int start = 0; int line_count = 0; @@ -129,7 +129,7 @@ int Game_Message::WordWrap(StringView line, const int limit, const Game_Message: return line_count; } -int Game_Message::WordWrap(lcf::Span> spans, const int limit, const typename Game_Message::ComponentWrapCallback& callback, const Font& font) { +int Game_Message::WordWrap(const lcf::Span> spans, const int limit, const typename Game_Message::ComponentWrapCallback& callback, const Font& font) { int line_count = 0; int line_width = 0; @@ -141,7 +141,7 @@ int Game_Message::WordWrap(lcf::Span> spans, cons line_width = 0; line_spans.clear(); }; - for (auto span = spans.begin(); span != spans.end(); ++span) { + for (auto span = spans.cbegin(); span != spans.cend(); ++span) { if (auto fragment = span->get()->Downcast()) { auto& line = fragment->string; diff --git a/src/image_webp.cpp b/src/image_webp.cpp new file mode 100644 index 000000000..81ffd8258 --- /dev/null +++ b/src/image_webp.cpp @@ -0,0 +1,76 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#include "image_webp.h" +#include +#include "output.h" + +ImageWebP::Decoder::~Decoder() noexcept { + // if (decoder) WebPAnimDecoderDelete(decoder); + // WebPDataClear(&data); +} + +std::optional ImageWebP::Decoder::Create(Filesystem_Stream::InputStream& is) noexcept { + ImageWebP::Decoder dec; + + // is.read(reinterpret_cast(&dec.data.bytes)); + // WebPMalloc + dec.data.size = is.GetSize(); + dec.data.bytes = (uint8_t*)WebPMalloc(dec.data.size + 1); + ((char*)dec.data.bytes)[dec.data.size] = 0; + is.read((char*)dec.data.bytes, dec.data.size); + if (is.fail()) return {}; + + if (!WebPGetInfo(dec.data.bytes, dec.data.size, nullptr, nullptr)) { + Output::Warning("ImageWebP: {} is not webp", is.GetName()); + return {}; + } + + dec.decoder = WebPAnimDecoderNew(&dec.data, nullptr); + if (!dec.decoder) { + Output::Warning("ImageWebP: Failed to create decoder for {}", is.GetName()); + return {}; + } + + if (!WebPAnimDecoderGetInfo(dec.decoder, &dec.animData)) { + Output::Warning("ImageWebP: Failed to get animation info for {}", is.GetName()); + return {}; + } + + return dec; +} + + +bool ImageWebP::Decoder::ReadNext(ImageOut& output, TimingInfo& timing) { + if (!WebPAnimDecoderHasMoreFrames(decoder)) { + output.pixels = nullptr; + return false; + } + + uint8_t* pixels; + if (!WebPAnimDecoderGetNext(decoder, &pixels, &timing.timestamp)) { + Output::Warning("ImageWebP: Failed to decode next frame"); + return false; + } + output.pixels = new uint32_t[animData.canvas_width * animData.canvas_height * 4]; + memcpy(output.pixels, pixels, animData.canvas_width * animData.canvas_height * 4); + output.bpp = 32; // WebP always uses 32 bits per pixel (RGBA) + output.height = animData.canvas_height; + output.width = animData.canvas_width; + + return true; +} \ No newline at end of file diff --git a/src/image_webp.h b/src/image_webp.h new file mode 100644 index 000000000..e900a1219 --- /dev/null +++ b/src/image_webp.h @@ -0,0 +1,45 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef EP_IMAGE_WEBP_H +#define EP_IMAGE_WEBP_H + +#include +#include +#include "bitmap.h" +#include "filesystem_stream.h" +#include + +struct TimingInfo { + int timestamp; +}; + +namespace ImageWebP { + struct Decoder { + ~Decoder() noexcept; + + bool ReadNext(ImageOut& output, TimingInfo& timing); + static std::optional Create(Filesystem_Stream::InputStream& is) noexcept; + private: + explicit Decoder() noexcept = default; + WebPAnimDecoder* decoder; + WebPData data; + WebPAnimInfo animData; + }; +} + +#endif \ No newline at end of file diff --git a/src/input.cpp b/src/input.cpp index 6eb924e17..4cc7a29f0 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -439,12 +439,12 @@ void Input::SimulateButtonPress(Input::InputButton button) { int Input::RenderTextComposition(Bitmap& contents, int offset, int y, Font* font_) { auto& font = font_ ? *font_ : *Font::Default(); - StringView text(Input::composition.text.data()); + std::string_view text(Input::composition.text.data()); std::u32string text32; auto iter = text.data(); auto end = iter + text.size(); - while (iter != text.end()) { + while (iter != &*text.end()) { auto ret = Utils::TextNext(iter, end, 0); iter = ret.next; if (!ret) continue; diff --git a/src/multiplayer/chat_overlay.cpp b/src/multiplayer/chat_overlay.cpp index 90626d2b1..6ee722e44 100644 --- a/src/multiplayer/chat_overlay.cpp +++ b/src/multiplayer/chat_overlay.cpp @@ -40,6 +40,7 @@ using json = nlohmann::json; #include "compiler.h" #include "icons.h" #include "overlay_utils.h" +#include "image_webp.h" namespace { Point ChatTextSquare() { @@ -47,11 +48,13 @@ namespace { return { text_height, text_height }; } enum class FileExtensions : char { png, gif, webp }; + struct EmojiInfo { public: FileExtensions extensions = FileExtensions::png; }; std::map emojis; + std::unordered_map> emojiCache; // Cache for animated emojis void InitializeEmojiMappings() { if (!emojis.empty()) return; @@ -72,8 +75,14 @@ namespace { if (ext == ".png") { auto& emoji = emojis[(std::string)key]; emoji.extensions = FileExtensions::png; + } else if (ext == ".gif") { + auto& emoji = emojis[(std::string)key]; + emoji.extensions = FileExtensions::gif; + } else if (ext == ".webp") { + auto& emoji = emojis[(std::string)key]; + emoji.extensions = FileExtensions::webp; } - //else Output::Debug("unimplemented emoji {}.{}", key, ext); + // else Output::Debug("unimplemented emoji {}.{}", key, ext); } } }, @@ -309,7 +318,7 @@ ChatOverlayMessage& ChatOverlay::AddMessage( return ret; } -void ChatOverlay::AddSystemMessage(StringView msg) { +void ChatOverlay::AddSystemMessage(std::string_view msg) { auto& chatmsg = messages.emplace_back(this, std::string(msg), "", "", "", std::string(""), false, true, 0); chatmsg.text.erase(chatmsg.text.begin()); for (auto& span : chatmsg.text) { @@ -338,6 +347,17 @@ void ChatOverlay::Update() { dirty = true; } } + + // Update animations for all visible emojis + for (auto it = emojiCache.begin(); it != emojiCache.end();) { + auto& emoji = it->second; + if (auto emojiPtr = emoji.lock()) { + emojiPtr->UpdateAnimation(); + ++it; // Move to the next element + } else { + it = emojiCache.erase(it); // Remove expired weak pointers + } + } } void ChatOverlay::UpdateScene() { @@ -378,7 +398,7 @@ void ChatOverlay::UpdateScene() { // chat input only below this point - StringView ui_input(Input::text_input.data()); + std::string_view ui_input(Input::text_input.data()); if (!ui_input.empty()) { input.append(Utils::DecodeUTF32(ui_input)); dirty = true; @@ -395,7 +415,8 @@ void ChatOverlay::UpdateScene() { if (Input::IsRawKeyTriggered(Input::Keys::RETURN) && !input.empty()) { // TODO: map chat and party chat std::string encoded = Utils::EncodeUTF(input); - GMI().sessionConn.SendPacket(Messages::C2S::SessionGSay{ std::move(encoded) }); + ChatOverlay::AddMessage(encoded, "blah", "00000000000000000000", "", "", true, true, 0); + // GMI().sessionConn.SendPacket(Messages::C2S::SessionGSay{ std::move(encoded) }); input.clear(); dirty = true; } @@ -469,7 +490,7 @@ bool ChatOverlay::IsTriggeredOrRepeating(CustomKeyTimers key) { } std::vector> ChatOverlayMessage::Convert(ChatOverlay* parent, bool has_badge) const { - StringView msg(text_orig); + std::string_view msg(text_orig); std::vector> out; static std::regex emoji_pattern(":(\\w+):"); static std::regex screenshot_pattern(R"regex(\[(t)?([\w\d]{16})(?::(\d)+)?\])regex"); @@ -496,23 +517,24 @@ std::vector> ChatOverlayMessage::Convert(ChatOver auto it = msg.begin(); auto last_end = msg.begin(); while (it != msg.end()) { - std::cmatch match; + std::match_results match; if (std::regex_search(it, msg.end(), match, emoji_pattern)) { - StringView between(last_end, match[0].first - last_end); + std::string_view between(&*last_end, match[0].first - last_end); if (!between.empty()) { out.emplace_back(std::make_shared(between)); } auto emoji_key = match[1].str(); if (emojis.empty() || emojis.find(emoji_key) != emojis.end()) - out.emplace_back(std::make_shared(parent, text_height, text_height, emoji_key)); + // out.emplace_back(std::make_shared(parent, text_height, text_height, emoji_key)); + out.emplace_back(ChatEmoji::GetOrCreate(emoji_key, parent)); else out.emplace_back(std::make_shared(match[0].str())); last_end = match[0].second; it = last_end; } else if (std::regex_search(it, msg.end(), match, screenshot_pattern)) { - StringView between(last_end, match[0].first - last_end); + std::string_view between(&*last_end, match[0].first - last_end); if (!between.empty()) { out.emplace_back(std::make_shared(between)); } @@ -528,7 +550,7 @@ std::vector> ChatOverlayMessage::Convert(ChatOver } } - StringView last(last_end, msg.end() - last_end); + std::string_view last(&*last_end, msg.end() - last_end); if (!last.empty()) { out.emplace_back(std::make_shared(last)); } @@ -582,27 +604,141 @@ ChatEmoji::ChatEmoji(ChatOverlay* parent_, int width, int height, std::string em RequestBitmap(parent); } - void ChatEmoji::RequestBitmap(ChatOverlay* parent_) { if (emoji.empty()) return; parent = parent_; + auto req = AsyncHandler::RequestFile("../images/ynomoji", emoji); req->SetGraphicFile(true); req->SetParentScope(true); - req->SetRequestExtension(".png"); - request = req->Bind([this](FileRequestResult* result) { + + // Determine the file extension to request based on the emoji's extension type + auto extension = emojis[emoji].extensions; + switch (extension) { + case FileExtensions::png: + req->SetRequestExtension(".png"); + break; + case FileExtensions::gif: + req->SetRequestExtension(".gif"); + break; + case FileExtensions::webp: + req->SetRequestExtension(".webp"); + break; + } + + request = req->Bind([this, extension](FileRequestResult* result) { if (!result->success) { emoji.clear(); Output::Debug("failed: {}", result->file); return; } - bitmap = Cache::Emoji(result->file); - bitmap->SetBilinear(); - parent->MarkDirty(); + switch (extension) { + case FileExtensions::gif: + DecodeGif(result->file); + break; + case FileExtensions::webp: + DecodeWebP(result->file); + break; + case FileExtensions::png: + bitmap = Cache::Emoji(result->file); + bitmap->SetBilinear(); + break; + } + if (parent) parent->MarkDirty(); }); req->Start(); } +void ChatEmoji::DecodeGif(const std::string& filePath) { + // Use a GIF decoding library to load frames and delays + // Example: giflib or similar library + // Pseudo-code: + frames.clear(); + frameDelays.clear(); + // Load GIF file and extract frames and delays + // for each frame in GIF: + // Convert frame to Bitmap and store in gifFrames + // Store delay in frameDelays + // Set currentFrame to 0 + currentFrame = 0; + lastFrameTime = std::chrono::steady_clock::now(); +} + +void ChatEmoji::DecodeWebP(const std::string& filePath) { + // Use a WebP decoding library to load the image + // Example: libwebp or similar library + // Pseudo-code: + frames.clear(); + auto fs = FileFinder::OpenImage("../images/ynomoji", filePath); + if (!fs) return; + + auto dec = ImageWebP::Decoder::Create(fs); + if (!dec) return; + + ImageOut image{}; + TimingInfo timing{}; + int smear = 0; + while (dec.value().ReadNext(image, timing)) { + auto bitmap = Bitmap::Create(image.pixels, image.width, image.height, image.bpp, format_R8G8B8A8_a().format()); + if (!bitmap) { + delete image.pixels; + continue; + } + bitmap->SetBilinear(); + frames.push_back(std::move(bitmap)); + + if (!timing.timestamp) + timing.timestamp = 100; + + int lastDelay = frameDelays.empty() ? 0 : frameDelays.back(); + if (timing.timestamp - lastDelay < 20) { + smear += timing.timestamp - lastDelay; + frameDelays.push_back(20); + } else { + frameDelays.push_back(timing.timestamp - lastDelay + smear); + smear = 0; + } + loopLength += frameDelays.back(); + } + // if (image.pixels) delete image.pixels; + + currentFrame = 0; + if (loopLength && frames.size() > 1) { + lastFrameTime = std::chrono::steady_clock::now(); + int loopDelta = std::chrono::duration_cast(lastFrameTime.time_since_epoch()).count() % loopLength; + while (currentFrame < frameDelays.size() && loopDelta >= frameDelays[currentFrame]) { + loopDelta -= frameDelays[currentFrame]; + currentFrame = (currentFrame + 1) % frames.size(); + } + } +} + +void ChatEmoji::UpdateAnimation() { + if (frames.size() < 2 || frameDelays.empty()) return; // No animation to update + + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - lastFrameTime).count(); + if (elapsed >= frameDelays[currentFrame]) { + // Move to the next frame + currentFrame = (currentFrame + 1) % frames.size(); + lastFrameTime = now; + bitmap = frames[currentFrame]; // Update the displayed frame + if (parent) parent->MarkDirty(); // Mark the parent as dirty to trigger a redraw + } +} + +std::shared_ptr ChatEmoji::GetOrCreate(const std::string& emojiKey, ChatOverlay* parent) { + // Check if the emoji is already in the cache + if (auto existingEmoji = emojiCache[emojiKey].lock()) { + return existingEmoji; // Return the existing shared reference + } + + // Create a new instance and store it in the cache + auto newEmoji = std::make_shared(parent, 0, 0, emojiKey); + emojiCache[emojiKey] = newEmoji; // Store the weak reference + return newEmoji; +} + ChatScreenshot::ChatScreenshot(ChatOverlay* parent, std::string uuid_, std::string id_, bool temp, bool spoiler) : ChatComponent(sizer, ChatComponents::Screenshot), parent(parent), uuid(std::move(uuid_)), id(std::move(id_)), temp(temp), spoiler(spoiler) { diff --git a/src/multiplayer/chat_overlay.h b/src/multiplayer/chat_overlay.h index 32d4a1128..a2c4d740f 100644 --- a/src/multiplayer/chat_overlay.h +++ b/src/multiplayer/chat_overlay.h @@ -116,7 +116,7 @@ class ChatOverlay : public Drawable { inline bool ShowingAll() const noexcept { return show_all; } void DoScroll(int increase); void MarkDirty() noexcept; - void AddSystemMessage(StringView string); + void AddSystemMessage(std::string_view string); private: bool IsAnyMessageVisible() const; @@ -167,7 +167,7 @@ class ChatString : public ChatComponent { std::string string; Color color; - ChatString(StringView other, Color color = default_color) : ChatComponent(0, 0, ChatComponents::String), + ChatString(std::string_view other, Color color = default_color) : ChatComponent(0, 0, ChatComponents::String), string(ToString(other)), color(color) {} }; @@ -180,6 +180,18 @@ class ChatEmoji : public ChatComponent { ChatEmoji(ChatOverlay* parent, int width, int height, std::string emoji); void RequestBitmap(ChatOverlay* parent); + void UpdateAnimation(); + static std::shared_ptr GetOrCreate(const std::string& emojiKey, ChatOverlay* parent); + +private: + std::vector> frames; // Store frames for GIFs + std::vector frameDelays; // Store delays for each frame in milliseconds + int currentFrame = 0; // Current frame index for animated GIFs + int loopLength = 0; + std::chrono::steady_clock::time_point lastFrameTime; // Time of the last frame update + + void DecodeGif(const std::string& filePath); + void DecodeWebP(const std::string& filePath); }; class ChatScreenshot : public ChatComponent { diff --git a/src/multiplayer/connection.cpp b/src/multiplayer/connection.cpp index a71380061..4ad81ed13 100644 --- a/src/multiplayer/connection.cpp +++ b/src/multiplayer/connection.cpp @@ -22,13 +22,13 @@ void Connection::FlushQueue() { void Connection::Dispatch(std::string_view name, ParameterList args) { if (raw_handler) { - std::ostringstream os{}; + std::string os{}; for (auto it = args.begin(); it != args.end(); ++it) { - os << *it; + os.append(*it); if (std::next(it) != args.end()) - os << Packet::PARAM_DELIM; + os.append(Packet::PARAM_DELIM); } - raw_handler(name, os.str()); + raw_handler(name, os); } auto it = handlers.find(std::string(name)); if (it != handlers.end()) { diff --git a/src/multiplayer/game_multiplayer.cpp b/src/multiplayer/game_multiplayer.cpp index 65cd72c3e..0f572cfcc 100644 --- a/src/multiplayer/game_multiplayer.cpp +++ b/src/multiplayer/game_multiplayer.cpp @@ -1188,7 +1188,7 @@ void Game_Multiplayer::UpdateTimers() { } } -void Game_Multiplayer::SetNickname(StringView name) { +void Game_Multiplayer::SetNickname(std::string_view name) { std::string value(name); GMI().sessionConn.SendPacketAsync(value); username = value; diff --git a/src/multiplayer/game_multiplayer.h b/src/multiplayer/game_multiplayer.h index e09790763..a439bc880 100644 --- a/src/multiplayer/game_multiplayer.h +++ b/src/multiplayer/game_multiplayer.h @@ -155,7 +155,7 @@ class Game_Multiplayer { std::array timers{}; void UpdateTimers(); - void SetNickname(StringView name); + void SetNickname(std::string_view name); std::string ApiEndpoint(std::string_view path) const; }; diff --git a/src/multiplayer/scene_nexus.cpp b/src/multiplayer/scene_nexus.cpp index a098a6210..04141f576 100644 --- a/src/multiplayer/scene_nexus.cpp +++ b/src/multiplayer/scene_nexus.cpp @@ -97,7 +97,7 @@ void Scene_Nexus::vUpdate() { } } -void Scene_Nexus::LaunchGame(StringView game) { +void Scene_Nexus::LaunchGame(std::string_view game) { #ifdef _WIN32 std::string userhome(std::getenv("APPDATA")); userhome.append("/ynoproject"); diff --git a/src/multiplayer/scene_nexus.h b/src/multiplayer/scene_nexus.h index 8f701443f..f8b39afea 100644 --- a/src/multiplayer/scene_nexus.h +++ b/src/multiplayer/scene_nexus.h @@ -30,7 +30,7 @@ class Scene_Nexus : public Scene { void DrawBackground(Bitmap&) override; void vUpdate() override; private: - void LaunchGame(StringView); + void LaunchGame(std::string_view); void InitWebview(); std::string selected_game; std::filesystem::path old_pwd; diff --git a/src/platform/sdl/sdl2_ui.cpp b/src/platform/sdl/sdl2_ui.cpp index b90aa578b..a81554287 100644 --- a/src/platform/sdl/sdl2_ui.cpp +++ b/src/platform/sdl/sdl2_ui.cpp @@ -496,8 +496,9 @@ bool Sdl2Ui::RefreshDisplayMode() { SWP_FRAMECHANGED | SWP_SHOWWINDOW); #else GtkWidget* childHandle = (GtkWidget*&)widget.value(); -#endif +#endif // PLAYER_YNO } + Sleep(200); Web_API::InitializeBindings(); w.run(); } diff --git a/src/window_settings.cpp b/src/window_settings.cpp index 9d25f0de0..6e2578bcc 100644 --- a/src/window_settings.cpp +++ b/src/window_settings.cpp @@ -44,7 +44,7 @@ # include "multiplayer/messages.h" #endif -class MenuItem final : public ConfigParam { +class MenuItem final : public ConfigParam { public: explicit MenuItem(std::string_view name, std::string_view description, std::string_view value) : ConfigParam(name, description, "", "", value) { @@ -783,7 +783,7 @@ void Window_Settings::RefreshButtonList() { void Window_Settings::RefreshOnline() { Game_ConfigOnline& cfg = GMI().GetConfig(); - using LockedMenuItem = LockedConfigParam; + using LockedMenuItem = LockedConfigParam; if (!cfg.username.Get().empty() && !cfg.session_token.Get().empty()) { AddOption(LockedMenuItem("Status", "", fmt::format("Logged in as {}", cfg.username.Get())), [] {}); diff --git a/src/window_stringinput.cpp b/src/window_stringinput.cpp index 59c813781..31140e0e2 100644 --- a/src/window_stringinput.cpp +++ b/src/window_stringinput.cpp @@ -22,7 +22,7 @@ #include "cache.h" #include "output.h" -Window_StringInput::Window_StringInput(StringView initial_value, int ix, int iy, int iwidth, int iheight) : +Window_StringInput::Window_StringInput(std::string_view initial_value, int ix, int iy, int iwidth, int iheight) : Window_Selectable(ix, iy, iwidth, iheight), value(initial_value) { SetContents(Bitmap::Create(width - 16, height - 16)); @@ -69,7 +69,7 @@ void Window_StringInput::Update() { bool dirty = false; - //StringView input(Input::text_input.data()); + //std::string_view input(Input::text_input.data()); if (!Input::text_input.empty()) { value.append(Input::text_input); dirty = true; diff --git a/src/window_stringinput.h b/src/window_stringinput.h index 502ec45ba..0d69bae1a 100644 --- a/src/window_stringinput.h +++ b/src/window_stringinput.h @@ -22,7 +22,7 @@ class Window_StringInput : public Window_Selectable { public: - Window_StringInput(StringView initial_value, int ix, int iy, int iwidth = 320, int iheight = 80); + Window_StringInput(std::string_view initial_value, int ix, int iy, int iwidth = 320, int iheight = 80); ~Window_StringInput(); void Refresh(); From c3d154b90ed77ceb3b3ae29320339d91a322879a Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Sat, 19 Apr 2025 00:50:55 -0400 Subject: [PATCH 17/18] Support for GIF emojis --- CMakeLists.txt | 2 + CMakePresets.json | 19 +++--- src/image_gif.cpp | 109 +++++++++++++++++++++++++++++++ src/image_gif.h | 48 ++++++++++++++ src/image_webp.cpp | 4 +- src/multiplayer/chat_overlay.cpp | 71 ++++++++++++++------ src/multiplayer/chat_overlay.h | 4 ++ 7 files changed, 227 insertions(+), 30 deletions(-) create mode 100644 src/image_gif.cpp create mode 100644 src/image_gif.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a98de92e..df3dcb547 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -255,6 +255,8 @@ add_library(${PROJECT_NAME} OBJECT src/image_xyz.h src/image_webp.cpp src/image_webp.h + src/image_gif.cpp + src/image_gif.h src/input_buttons_desktop.cpp src/input_buttons.h src/input.cpp diff --git a/CMakePresets.json b/CMakePresets.json index 6869fcd2d..5092d8469 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -653,6 +653,14 @@ "type-release" ] }, + { + "name": "windows-x64-vs2022-ynoshell-debug", + "displayName": "Windows (x64) using Visual Studio 2022 (ynoshell) (Debug)", + "inherits": "windows-x64-vs2022-debug", + "cacheVariables": { + "PLAYER_SHELL": "yno" + } + }, { "name": "windows-x64-vs2022-ynoshell-release", "displayName": "Windows (x64) using Visual Studio 2022 (ynoshell) (Release)", @@ -1786,15 +1794,10 @@ "name": "windows-x64-vs2022-libretro-release", "configurePreset": "windows-x64-vs2022-libretro-release" }, - { - "name": "windows-x64-vs2022-ynoshell-relwithdebinfo", - "configurePreset": "windows-x64-vs2022-ynoshell-relwithdebinfo", - "configuration": "RelWithDebInfo" - }, { "name": "windows-x64-vs2022-ynoshell-release", - "configurePreset": "windows-x64-vs2022-ynoshell-relwithdebinfo", - "configuration": "Release" + "configurePreset": "windows-x64-vs2022-ynoshell-release", + "configuration": "RelWithDebInfo" }, { "name": "macos-debug", @@ -2118,4 +2121,4 @@ } ], "testPresets": [] -} +} \ No newline at end of file diff --git a/src/image_gif.cpp b/src/image_gif.cpp new file mode 100644 index 000000000..fe64d3a7f --- /dev/null +++ b/src/image_gif.cpp @@ -0,0 +1,109 @@ +#include "image_gif.h" +#include +#include "output.h" + +static int read_data(GifFileType* gifFile, GifByteType* buffer, int size) { + auto* bufp = reinterpret_cast(gifFile->UserData); + if (bufp && *bufp) { + bufp->read(reinterpret_cast(buffer), size); + if (bufp->fail()) return 0; // Read failed + return size; // Return number of bytes read + } + return 0; // No data available +} + +ImageGif::Decoder::Decoder(Filesystem_Stream::InputStream& is) noexcept : ImageGif::Decoder() { + int error = D_GIF_SUCCEEDED; + gifFile = DGifOpen(&is, read_data, &error); + + if (!gifFile || error != D_GIF_SUCCEEDED) { + Output::Warning("ImageGif: Failed to open GIF file: {} ({})", is.GetName(), GifErrorString(error)); + return; + } + + if (DGifSlurp(gifFile) != GIF_OK) { + Output::Warning("ImageGif: Failed to read GIF data: {} ({})", is.GetName()); + return; + } + + currentFrame = 0; +} + +ImageGif::Decoder::~Decoder() noexcept { + if (gifFile) DGifCloseFile(gifFile, nullptr); +} + +bool ImageGif::Decoder::ReadNext(ImageOut& output, GifTimingInfo& timing) { + memset(&output, 0, sizeof(ImageOut)); + memset(&timing, 0, sizeof(GifTimingInfo)); + + if (!gifFile || currentFrame >= gifFile->ImageCount) { + output.pixels = nullptr; + return false; + } + + const SavedImage* frame = &gifFile->SavedImages[currentFrame]; + const GifImageDesc& image = frame->ImageDesc; + + ColorMapObject* colorMap = image.ColorMap ? image.ColorMap : gifFile->SColorMap; + if (!colorMap) { + Output::Warning("ImageGif: No color map found for frame {}", currentFrame); + output.pixels = nullptr; + return false; + } + + output.width = image.Width; + output.height = image.Height; + output.bpp = 32; + output.pixels = new uint32_t[image.Width * image.Height]; + + const int left = image.Left; + const int top = image.Top; + const int width = image.Width; + const int height = image.Height; + const int bg = gifFile->SBackGroundColor; + + const GifPixelType* src = frame->RasterBits; + // const GifPixelType* srcPrev = currentFrame > 0 ? gifFile->SavedImages[currentFrame - 1].RasterBits : nullptr; + + int transparentIndex = -1; + int disposalMethod = 0; + + for (int i = 0; i < frame->ExtensionBlockCount; ++i) { + if (frame->ExtensionBlocks[i].Function == GRAPHICS_EXT_FUNC_CODE) { + uint8_t fields = frame->ExtensionBlocks[i].Bytes[0]; + disposalMethod = (fields & 0x1C) >> 2; // 3 bits for disposal method + bool transparency = (fields & 0x01) != 0; // 1 bit for transparency + if (transparency) { + transparentIndex = frame->ExtensionBlocks[i].Bytes[3]; // Transparency index + } + timing.delay = ((int)frame->ExtensionBlocks[i].Bytes[1] | ((int)frame->ExtensionBlocks[i].Bytes[2] << 8)) * 10; // Convert to milliseconds + break; + } + } + + size_t srcIndex = 0; + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + GifPixelType index = src[srcIndex++]; + if (index == transparentIndex) { + ((uint32_t*)output.pixels)[(top + y) * output.width + (left + x)] = 0; // Transparent pixel + continue; + } + // if (disposalMethod == 2) + // ((uint32_t*)output.pixels)[(top + y) * output.width + (left + x)] = bg; + if (index < colorMap->ColorCount) { + const GifColorType& color = colorMap->Colors[index]; + int dstX = left + x; + int dstY = top + y; + if (dstX < output.width && dstY < output.height) { + ((uint32_t*)output.pixels)[dstY * output.width + dstX] = + color.Red | (color.Green << 8) | (color.Blue << 16) | 0xFF000000; + } + } + } + } + + currentFrame++; + return true; +} diff --git a/src/image_gif.h b/src/image_gif.h new file mode 100644 index 000000000..f985d9acf --- /dev/null +++ b/src/image_gif.h @@ -0,0 +1,48 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef EP_IMAGE_GIF_H +#define EP_IMAGE_GIF_H + +#include +#include +#include "bitmap.h" +#include "filesystem_stream.h" +#include + +struct GifTimingInfo { + int delay; +}; + +namespace ImageGif { + struct Decoder { + ~Decoder() noexcept; + + bool ReadNext(ImageOut& output, GifTimingInfo& timing); + Decoder(Filesystem_Stream::InputStream& is) noexcept; + + inline operator bool() const noexcept { return gifFile != nullptr; } + private: + explicit Decoder() noexcept = default; + + GifFileType* gifFile; + int currentFrame{}; + }; + +} + +#endif diff --git a/src/image_webp.cpp b/src/image_webp.cpp index 81ffd8258..4e7b9af94 100644 --- a/src/image_webp.cpp +++ b/src/image_webp.cpp @@ -66,8 +66,8 @@ bool ImageWebP::Decoder::ReadNext(ImageOut& output, TimingInfo& timing) { Output::Warning("ImageWebP: Failed to decode next frame"); return false; } - output.pixels = new uint32_t[animData.canvas_width * animData.canvas_height * 4]; - memcpy(output.pixels, pixels, animData.canvas_width * animData.canvas_height * 4); + output.pixels = new uint32_t[animData.canvas_width * animData.canvas_height]; + memcpy(output.pixels, pixels, animData.canvas_width * animData.canvas_height * sizeof(uint32_t)); output.bpp = 32; // WebP always uses 32 bits per pixel (RGBA) output.height = animData.canvas_height; output.width = animData.canvas_width; diff --git a/src/multiplayer/chat_overlay.cpp b/src/multiplayer/chat_overlay.cpp index 6ee722e44..23a1a328a 100644 --- a/src/multiplayer/chat_overlay.cpp +++ b/src/multiplayer/chat_overlay.cpp @@ -41,6 +41,7 @@ using json = nlohmann::json; #include "icons.h" #include "overlay_utils.h" #include "image_webp.h" +#include "image_gif.h" namespace { Point ChatTextSquare() { @@ -201,17 +202,18 @@ void ChatOverlay::Draw(Bitmap& dst) { // semitransparent bg int yidx = lidx + 1 + input_row_offset; int line_height = text_height; + bool emojis_only = false; // begin override line height for components // expand messages with only emojis - if (std::all_of(line->cbegin(), line->cend(), [](const std::shared_ptr& comp) { return bool(comp->Downcast()); })) { - line_height = OverlayUtils::LargeScreen() ? 56 : 18; - } - if (std::any_of(line->cbegin(), line->cend(), [](const std::shared_ptr& comp) { return bool(comp->Downcast()); })) { line_height = ChatScreenshot::sizer().y; } + else if (std::all_of(line->cbegin(), line->cend(), [](const std::shared_ptr& comp) { return bool(comp->Downcast()); })) { + emojis_only = true; + line_height = OverlayUtils::LargeScreen() ? 56 : 18; + } // end override line height @@ -247,22 +249,21 @@ void ChatOverlay::Draw(Bitmap& dst) { } else if (auto emoji = span->Downcast()) { Point dims = emoji->GetSize(); + if (emojis_only) dims.x = dims.y = line_height; int comp_y = y + (baseline ? baseline - text_height : 0); if (emoji->bitmap) { - double zoom = emoji->bitmap->GetRect().y / (double)text_height; - bitmap->StretchBlit({offset, comp_y, line_height, line_height}, *emoji->bitmap, emoji->bitmap->GetRect(), 255); + bitmap->StretchBlit({offset, comp_y, dims.x, dims.y}, *emoji->bitmap, emoji->bitmap->GetRect(), 255); } - else { + else if (!emoji->HasAnimation()) { // the emoji is still live, request it now emoji->RequestBitmap(this); - bitmap->FillRect({ offset, comp_y, line_height, line_height }, offwhite); + bitmap->FillRect({ offset, comp_y, dims.y, dims.y }, offwhite); } - offset += line_height; + offset += dims.x; } else if (auto screenshot = span->Downcast()) { Point dims = screenshot->GetSize(); if (screenshot->bitmap) { - double zoom = screenshot->bitmap->GetRect().y / (double)text_height; bitmap->StretchBlit({offset, y, dims.x, dims.y}, *screenshot->bitmap, screenshot->bitmap->GetRect(), 255); } else { @@ -608,6 +609,8 @@ void ChatEmoji::RequestBitmap(ChatOverlay* parent_) { if (emoji.empty()) return; parent = parent_; + if (request) return; + auto req = AsyncHandler::RequestFile("../images/ynomoji", emoji); req->SetGraphicFile(true); req->SetParentScope(true); @@ -627,6 +630,7 @@ void ChatEmoji::RequestBitmap(ChatOverlay* parent_) { } request = req->Bind([this, extension](FileRequestResult* result) { + request.reset(); // Clear the request after processing if (!result->success) { emoji.clear(); Output::Debug("failed: {}", result->file); @@ -650,18 +654,43 @@ void ChatEmoji::RequestBitmap(ChatOverlay* parent_) { } void ChatEmoji::DecodeGif(const std::string& filePath) { - // Use a GIF decoding library to load frames and delays - // Example: giflib or similar library - // Pseudo-code: + constexpr int minimum_frame_delay = 64; frames.clear(); frameDelays.clear(); - // Load GIF file and extract frames and delays - // for each frame in GIF: - // Convert frame to Bitmap and store in gifFrames - // Store delay in frameDelays - // Set currentFrame to 0 + auto fs = FileFinder::OpenImage("../images/ynomoji", filePath); + if (!fs) return; + + ImageGif::Decoder dec(fs); + if (!dec) return; + + ImageOut image{}; + GifTimingInfo timing{}; + while (dec.ReadNext(image, timing)) { + auto bitmap = Bitmap::Create(image.pixels, image.width, image.height, 0, format_R8G8B8A8_a().format()); + if (!bitmap) { + if (image.pixels) delete[] image.pixels; + continue; + } + bitmap->SetBilinear(); + frames.push_back(std::move(bitmap)); + + if (!timing.delay) + timing.delay = 100; + + int delay = std::min(minimum_frame_delay, timing.delay); + frameDelays.push_back(delay); + loopLength += frameDelays.back(); + } + currentFrame = 0; - lastFrameTime = std::chrono::steady_clock::now(); + if (loopLength && frames.size() > 1) { + lastFrameTime = std::chrono::steady_clock::now(); + int loopDelta = std::chrono::duration_cast(lastFrameTime.time_since_epoch()).count() % loopLength; + while (currentFrame < frameDelays.size() && loopDelta >= frameDelays[currentFrame]) { + loopDelta -= frameDelays[currentFrame]; + currentFrame = (currentFrame + 1) % frames.size(); + } + } } void ChatEmoji::DecodeWebP(const std::string& filePath) { @@ -669,6 +698,7 @@ void ChatEmoji::DecodeWebP(const std::string& filePath) { // Example: libwebp or similar library // Pseudo-code: frames.clear(); + frameDelays.clear(); auto fs = FileFinder::OpenImage("../images/ynomoji", filePath); if (!fs) return; @@ -714,7 +744,8 @@ void ChatEmoji::DecodeWebP(const std::string& filePath) { } void ChatEmoji::UpdateAnimation() { - if (frames.size() < 2 || frameDelays.empty()) return; // No animation to update + if (frames.size() == 1) bitmap = frames[0]; // If there's only one frame, just display it + else if (frames.size() < 2 || frameDelays.empty()) return; // No animation to update auto now = std::chrono::steady_clock::now(); auto elapsed = std::chrono::duration_cast(now - lastFrameTime).count(); diff --git a/src/multiplayer/chat_overlay.h b/src/multiplayer/chat_overlay.h index a2c4d740f..2bfe6ca65 100644 --- a/src/multiplayer/chat_overlay.h +++ b/src/multiplayer/chat_overlay.h @@ -23,6 +23,7 @@ #include #include #include +#include #include "drawable.h" #include "bitmap.h" @@ -183,6 +184,9 @@ class ChatEmoji : public ChatComponent { void UpdateAnimation(); static std::shared_ptr GetOrCreate(const std::string& emojiKey, ChatOverlay* parent); + inline bool HasAnimation() const noexcept { + return frames.size() > 1; + } private: std::vector> frames; // Store frames for GIFs std::vector frameDelays; // Store delays for each frame in milliseconds From c896edb2f0dd9d2ac57ac79b1cfcebe8b1725a7d Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Sat, 19 Apr 2025 21:23:55 -0400 Subject: [PATCH 18/18] Fix animated emojis decoding, webview layout --- src/audio_generic.cpp | 26 ++++++++++++- src/image_gif.cpp | 57 ++++++++++++++------------- src/image_gif.h | 1 + src/image_webp.cpp | 37 ++++++++---------- src/image_webp.h | 4 +- src/multiplayer/chat_overlay.cpp | 66 +++++++++++++++++++++++++------ src/multiplayer/chat_overlay.h | 7 ++++ src/multiplayer/messages.h | 8 ++++ src/multiplayer/overlay_utils.h | 2 +- src/platform/sdl/sdl2_ui.cpp | 67 +++++++++++++++++++------------- src/window_settings.cpp | 2 + 11 files changed, 186 insertions(+), 91 deletions(-) diff --git a/src/audio_generic.cpp b/src/audio_generic.cpp index 03032723c..c00dcd9df 100644 --- a/src/audio_generic.cpp +++ b/src/audio_generic.cpp @@ -471,7 +471,31 @@ void GenericAudio::Decode(uint8_t* output_buffer, int buffer_length) { channel_active = true; } } - return {channel_active, total_volume, samples_per_frame}; + if (channel_active) { + if (total_volume > 1.0) { + float threshold = 0.8f; + for (unsigned i = 0; i < (unsigned)(samples_per_frame * 2); i++) { + float sample = mixer_buffer[i]; + float sign = (sample < 0) ? -1.0 : 1.0; + sample /= sign; + //dynamic range compression + if (sample > threshold) { + sample_buffer[i] = sign * 32768.0 * (threshold + (1.0 - threshold) * (sample - threshold) / (total_volume - threshold)); + } else { + sample_buffer[i] = sign * sample * 32768.0; + } + } + } else { + //No dynamic range compression necessary + for (unsigned i = 0; i < (unsigned)(samples_per_frame * 2); i++) { + sample_buffer[i] = mixer_buffer[i] * 32768.0; + } + } + + memcpy(output_buffer, sample_buffer.data(), buffer_length); + } else { + memset(output_buffer, '\0', buffer_length); + } } void GenericAudio::BgmChannel::Stop() { diff --git a/src/image_gif.cpp b/src/image_gif.cpp index fe64d3a7f..77530487b 100644 --- a/src/image_gif.cpp +++ b/src/image_gif.cpp @@ -26,6 +26,9 @@ ImageGif::Decoder::Decoder(Filesystem_Stream::InputStream& is) noexcept : ImageG return; } + scratch.resize(gifFile->SWidth * gifFile->SHeight); + std::fill(scratch.begin(), scratch.end(), gifFile->SBackGroundColor | 0xFF000000); + currentFrame = 0; } @@ -34,37 +37,35 @@ ImageGif::Decoder::~Decoder() noexcept { } bool ImageGif::Decoder::ReadNext(ImageOut& output, GifTimingInfo& timing) { - memset(&output, 0, sizeof(ImageOut)); - memset(&timing, 0, sizeof(GifTimingInfo)); - if (!gifFile || currentFrame >= gifFile->ImageCount) { output.pixels = nullptr; return false; } const SavedImage* frame = &gifFile->SavedImages[currentFrame]; - const GifImageDesc& image = frame->ImageDesc; + const GifImageDesc& frameInfo = frame->ImageDesc; - ColorMapObject* colorMap = image.ColorMap ? image.ColorMap : gifFile->SColorMap; + ColorMapObject* colorMap = frameInfo.ColorMap ? frameInfo.ColorMap : gifFile->SColorMap; if (!colorMap) { Output::Warning("ImageGif: No color map found for frame {}", currentFrame); output.pixels = nullptr; return false; } - output.width = image.Width; - output.height = image.Height; - output.bpp = 32; - output.pixels = new uint32_t[image.Width * image.Height]; + const int fullWidth = gifFile->SWidth; + const int fullHeight = gifFile->SHeight; + output.width = fullWidth; + output.height = fullHeight; + output.bpp = 0; - const int left = image.Left; - const int top = image.Top; - const int width = image.Width; - const int height = image.Height; + const int left = frameInfo.Left; + const int top = frameInfo.Top; + const int width = frameInfo.Width; + const int height = frameInfo.Height; const int bg = gifFile->SBackGroundColor; + timing.delay = 0; const GifPixelType* src = frame->RasterBits; - // const GifPixelType* srcPrev = currentFrame > 0 ? gifFile->SavedImages[currentFrame - 1].RasterBits : nullptr; int transparentIndex = -1; int disposalMethod = 0; @@ -82,28 +83,28 @@ bool ImageGif::Decoder::ReadNext(ImageOut& output, GifTimingInfo& timing) { } } + if (disposalMethod == 2) { + std::fill(scratch.begin(), scratch.end(), bg | 0xFF000000); // Fill with background color + } + size_t srcIndex = 0; - for (int y = 0; y < height; ++y) { - for (int x = 0; x < width; ++x) { + for (int y = top; y < top + height; ++y) { + for (int x = left; x < left + width; ++x) { GifPixelType index = src[srcIndex++]; if (index == transparentIndex) { - ((uint32_t*)output.pixels)[(top + y) * output.width + (left + x)] = 0; // Transparent pixel - continue; - } - // if (disposalMethod == 2) - // ((uint32_t*)output.pixels)[(top + y) * output.width + (left + x)] = bg; - if (index < colorMap->ColorCount) { + if (disposalMethod != 1) + scratch[y * fullWidth + x] = 0; // Transparent pixel + } else if (index < colorMap->ColorCount) { const GifColorType& color = colorMap->Colors[index]; - int dstX = left + x; - int dstY = top + y; - if (dstX < output.width && dstY < output.height) { - ((uint32_t*)output.pixels)[dstY * output.width + dstX] = - color.Red | (color.Green << 8) | (color.Blue << 16) | 0xFF000000; - } + scratch[y * fullWidth + x] = + color.Red | (color.Green << 8) | (color.Blue << 16) | 0xFF000000; } } } + output.pixels = new uint32_t[fullWidth * fullHeight]; + memcpy(output.pixels, scratch.data(), fullWidth * fullHeight * sizeof(uint32_t)); + currentFrame++; return true; } diff --git a/src/image_gif.h b/src/image_gif.h index f985d9acf..dcff1022c 100644 --- a/src/image_gif.h +++ b/src/image_gif.h @@ -38,6 +38,7 @@ namespace ImageGif { inline operator bool() const noexcept { return gifFile != nullptr; } private: explicit Decoder() noexcept = default; + std::vector scratch; GifFileType* gifFile; int currentFrame{}; diff --git a/src/image_webp.cpp b/src/image_webp.cpp index 4e7b9af94..4d18bc11c 100644 --- a/src/image_webp.cpp +++ b/src/image_webp.cpp @@ -20,38 +20,36 @@ #include "output.h" ImageWebP::Decoder::~Decoder() noexcept { - // if (decoder) WebPAnimDecoderDelete(decoder); - // WebPDataClear(&data); + if (decoder) WebPAnimDecoderDelete(decoder); + WebPDataClear(&data); } -std::optional ImageWebP::Decoder::Create(Filesystem_Stream::InputStream& is) noexcept { - ImageWebP::Decoder dec; - +ImageWebP::Decoder::Decoder(Filesystem_Stream::InputStream& is) noexcept { // is.read(reinterpret_cast(&dec.data.bytes)); // WebPMalloc - dec.data.size = is.GetSize(); - dec.data.bytes = (uint8_t*)WebPMalloc(dec.data.size + 1); - ((char*)dec.data.bytes)[dec.data.size] = 0; - is.read((char*)dec.data.bytes, dec.data.size); - if (is.fail()) return {}; + data.size = is.GetSize(); + data.bytes = (uint8_t*)WebPMalloc(data.size + 1); + ((char*)data.bytes)[data.size] = 0; + is.read((char*)data.bytes, data.size); + if (is.fail()) return; - if (!WebPGetInfo(dec.data.bytes, dec.data.size, nullptr, nullptr)) { + if (!WebPGetInfo(data.bytes, data.size, nullptr, nullptr)) { Output::Warning("ImageWebP: {} is not webp", is.GetName()); - return {}; + return; } - dec.decoder = WebPAnimDecoderNew(&dec.data, nullptr); - if (!dec.decoder) { + decoder = WebPAnimDecoderNew(&data, nullptr); + if (!decoder) { Output::Warning("ImageWebP: Failed to create decoder for {}", is.GetName()); - return {}; + return; } - if (!WebPAnimDecoderGetInfo(dec.decoder, &dec.animData)) { + if (!WebPAnimDecoderGetInfo(decoder, &animData)) { + if (decoder) WebPAnimDecoderDelete(decoder); + decoder = nullptr; Output::Warning("ImageWebP: Failed to get animation info for {}", is.GetName()); - return {}; + return; } - - return dec; } @@ -68,7 +66,6 @@ bool ImageWebP::Decoder::ReadNext(ImageOut& output, TimingInfo& timing) { } output.pixels = new uint32_t[animData.canvas_width * animData.canvas_height]; memcpy(output.pixels, pixels, animData.canvas_width * animData.canvas_height * sizeof(uint32_t)); - output.bpp = 32; // WebP always uses 32 bits per pixel (RGBA) output.height = animData.canvas_height; output.width = animData.canvas_width; diff --git a/src/image_webp.h b/src/image_webp.h index e900a1219..e57fa875a 100644 --- a/src/image_webp.h +++ b/src/image_webp.h @@ -33,7 +33,9 @@ namespace ImageWebP { ~Decoder() noexcept; bool ReadNext(ImageOut& output, TimingInfo& timing); - static std::optional Create(Filesystem_Stream::InputStream& is) noexcept; + // static std::optional Create(Filesystem_Stream::InputStream& is) noexcept; + Decoder(Filesystem_Stream::InputStream& is) noexcept; + inline operator bool() const noexcept { return decoder != nullptr; } private: explicit Decoder() noexcept = default; WebPAnimDecoder* decoder; diff --git a/src/multiplayer/chat_overlay.cpp b/src/multiplayer/chat_overlay.cpp index 23a1a328a..fcfb0bea0 100644 --- a/src/multiplayer/chat_overlay.cpp +++ b/src/multiplayer/chat_overlay.cpp @@ -125,6 +125,12 @@ void ChatOverlay::Draw(Bitmap& dst) { text_height = 12; } + for (auto it = emojiCache.begin(); it != emojiCache.end(); ++it) { + if (auto emoji = it->second.lock()) { + emoji->in_viewport = false; + } + } + int i = 0; int half_screen = y_end / 2 / text_height; @@ -205,6 +211,7 @@ void ChatOverlay::Draw(Bitmap& dst) { bool emojis_only = false; // begin override line height for components + // if adding new components, remember to update LayoutViewport // expand messages with only emojis if (std::any_of(line->cbegin(), line->cend(), [](const std::shared_ptr& comp) { return bool(comp->Downcast()); })) { @@ -248,6 +255,7 @@ void ChatOverlay::Draw(Bitmap& dst) { offset += Text::Draw(*bitmap, offset, y + (baseline ? baseline - text_height : 0), font, str->color, str->string).x; } else if (auto emoji = span->Downcast()) { + emoji->in_viewport = true; Point dims = emoji->GetSize(); if (emojis_only) dims.x = dims.y = line_height; int comp_y = y + (baseline ? baseline - text_height : 0); @@ -301,6 +309,44 @@ void ChatOverlay::Draw(Bitmap& dst) { dirty = false; } +ViewportInfo ChatOverlay::LayoutViewport(int text_height, const Font& font) const { + Rect screen_rect = DisplayUi->GetScreenSurfaceRect(); + int y_end = screen_rect.height; + int half_screen = y_end / 2 / text_height; + + bool unlocked_start = scroll < half_screen; + bool unlocked_end = scroll > messages.size() - half_screen; + int last_sticky = std::max(0, (int)messages.size() - half_screen * 2); + int viewport = show_all + ? std::clamp(scroll - half_screen, 0, std::min(scroll + half_screen, last_sticky)) + : 0; + + int extra = 0, lidx = viewport, scroll_extra = 0, last_sticky_extra = 0; + for (auto message = messages.rbegin() + viewport; message != messages.rend(); ++message) { + const auto& components = message->text; + if (std::any_of(components.cbegin(), components.cend(), [](const std::shared_ptr& comp) { return bool(comp->Downcast()); })) { + last_sticky_extra += extra = ChatScreenshot::sizer().y - text_height; + } else if (std::all_of(components.cbegin(), components.cend(), [](const std::shared_ptr& comp) { return bool(comp->Downcast()); })) { + last_sticky_extra += extra = (OverlayUtils::LargeScreen() ? 56 : 18) - text_height; + } else { + auto dims = Text::GetSize(font, message->text_orig); + int lines = ceilf(dims.width / (double)bitmap->width()); + last_sticky_extra += extra = std::max(0, lines - 1) * text_height; + lidx += std::max(0, lines - 1); + } + if (lidx <= scroll) scroll_extra += extra; + if (lidx >= last_sticky) break; + lidx += 1; + } + + ViewportInfo out{}; + out.scroll_px = scroll * text_height + scroll_extra; + out.last_sticky_px = last_sticky * text_height + last_sticky_extra; + out.extra = last_sticky_extra; + + return out; +} + ChatOverlayMessage& ChatOverlay::AddMessage( std::string_view message, std::string_view sender, std::string_view sender_uuid, std::string_view system, std::string_view badge, bool account, bool global, int rank) { @@ -416,8 +462,8 @@ void ChatOverlay::UpdateScene() { if (Input::IsRawKeyTriggered(Input::Keys::RETURN) && !input.empty()) { // TODO: map chat and party chat std::string encoded = Utils::EncodeUTF(input); - ChatOverlay::AddMessage(encoded, "blah", "00000000000000000000", "", "", true, true, 0); - // GMI().sessionConn.SendPacket(Messages::C2S::SessionGSay{ std::move(encoded) }); + // ChatOverlay::AddMessage(encoded, "blah", "00000000000000000000", "", "", true, true, 0); + GMI().sessionConn.SendPacket(Messages::C2S::SessionSay{ std::move(encoded) }); input.clear(); dirty = true; } @@ -654,7 +700,7 @@ void ChatEmoji::RequestBitmap(ChatOverlay* parent_) { } void ChatEmoji::DecodeGif(const std::string& filePath) { - constexpr int minimum_frame_delay = 64; + constexpr int minimum_frame_delay = 20; frames.clear(); frameDelays.clear(); auto fs = FileFinder::OpenImage("../images/ynomoji", filePath); @@ -694,24 +740,21 @@ void ChatEmoji::DecodeGif(const std::string& filePath) { } void ChatEmoji::DecodeWebP(const std::string& filePath) { - // Use a WebP decoding library to load the image - // Example: libwebp or similar library - // Pseudo-code: frames.clear(); frameDelays.clear(); auto fs = FileFinder::OpenImage("../images/ynomoji", filePath); if (!fs) return; - auto dec = ImageWebP::Decoder::Create(fs); + ImageWebP::Decoder dec(fs); if (!dec) return; ImageOut image{}; TimingInfo timing{}; int smear = 0; - while (dec.value().ReadNext(image, timing)) { - auto bitmap = Bitmap::Create(image.pixels, image.width, image.height, image.bpp, format_R8G8B8A8_a().format()); + while (dec.ReadNext(image, timing)) { + auto bitmap = Bitmap::Create(image.pixels, image.width, image.height, 0, format_R8G8B8A8_a().format()); if (!bitmap) { - delete image.pixels; + delete[] image.pixels; continue; } bitmap->SetBilinear(); @@ -730,7 +773,6 @@ void ChatEmoji::DecodeWebP(const std::string& filePath) { } loopLength += frameDelays.back(); } - // if (image.pixels) delete image.pixels; currentFrame = 0; if (loopLength && frames.size() > 1) { @@ -754,7 +796,7 @@ void ChatEmoji::UpdateAnimation() { currentFrame = (currentFrame + 1) % frames.size(); lastFrameTime = now; bitmap = frames[currentFrame]; // Update the displayed frame - if (parent) parent->MarkDirty(); // Mark the parent as dirty to trigger a redraw + if (parent && in_viewport) parent->MarkDirty(); // Mark the parent as dirty to trigger a redraw } } diff --git a/src/multiplayer/chat_overlay.h b/src/multiplayer/chat_overlay.h index 2bfe6ca65..45351606f 100644 --- a/src/multiplayer/chat_overlay.h +++ b/src/multiplayer/chat_overlay.h @@ -100,6 +100,10 @@ class ChatOverlayMessage { inline void ChatOverlayMessage::SetOnInteract(std::function on_interact) { OnInteract = on_interact; } +struct ViewportInfo { + int scroll_px, last_sticky_px, extra; +}; + class ChatOverlay : public Drawable { public: ChatOverlay(); @@ -121,6 +125,8 @@ class ChatOverlay : public Drawable { private: bool IsAnyMessageVisible() const; + ViewportInfo LayoutViewport(int text_height, const Font& font) const; + bool dirty = false; bool show_all = false; int ox = 0; @@ -178,6 +184,7 @@ class ChatEmoji : public ChatComponent { BitmapRef bitmap = nullptr; ChatOverlay* parent = nullptr; FileRequestBinding request = nullptr; + bool in_viewport = false; ChatEmoji(ChatOverlay* parent, int width, int height, std::string emoji); void RequestBitmap(ChatOverlay* parent); diff --git a/src/multiplayer/messages.h b/src/multiplayer/messages.h index 757f1572a..9960fac0c 100644 --- a/src/multiplayer/messages.h +++ b/src/multiplayer/messages.h @@ -689,6 +689,14 @@ namespace C2S { protected: std::string contents; }; + class SessionSay : public C2SPacket { + public: + SessionSay(std::string contents) : C2SPacket("say"), + contents(std::move(contents)) {} + std::string ToBytes() const override { return Build(contents); } + protected: + std::string contents; + }; #endif } } diff --git a/src/multiplayer/overlay_utils.h b/src/multiplayer/overlay_utils.h index 2d813ee86..fda19ed67 100644 --- a/src/multiplayer/overlay_utils.h +++ b/src/multiplayer/overlay_utils.h @@ -22,7 +22,7 @@ namespace OverlayUtils { inline bool LargeScreen() { - return DisplayUi->GetScreenSurfaceRect().width >= 640; + return DisplayUi->GetScreenSurfaceRect().width >= 720; } inline int ChatTextHeight() { return LargeScreen() ? 37 : 12; diff --git a/src/platform/sdl/sdl2_ui.cpp b/src/platform/sdl/sdl2_ui.cpp index a81554287..a4a9030dc 100644 --- a/src/platform/sdl/sdl2_ui.cpp +++ b/src/platform/sdl/sdl2_ui.cpp @@ -700,6 +700,16 @@ void Sdl2Ui::UpdateDisplay() { float width_float = static_cast(win_width); float height_float = static_cast(win_height); + int available_width = window.width; + if (webview) { + LayoutWebview(); + // Adjust SDL viewport to avoid overlapping with the webview + if (webview_layout == WebviewLayout::Sidebar && webview_visible) { + available_width -= webview_dims.width; + width_float = static_cast(available_width); + } + } + float want_aspect = (float)main_surface->width() / main_surface->height(); float real_aspect = width_float / height_float; @@ -708,7 +718,6 @@ void Sdl2Ui::UpdateDisplay() { viewport.x = border_x; viewport.w = win_width; } - ModifyViewport(); }; if (vcfg.scaling_mode.Get() == ConfigEnum::ScalingMode::Integer) { @@ -720,7 +729,8 @@ void Sdl2Ui::UpdateDisplay() { } viewport.w = static_cast(ceilf(main_surface->width() * window.scale)); - viewport.x = (win_width - viewport.w) / 2 + border_x; + // viewport.x = (win_width - viewport.w) / 2 + border_x; + viewport.x = (available_width - viewport.w) / 2; viewport.h = static_cast(ceilf(main_surface->height() * window.scale)); viewport.y = (win_height - viewport.h) / 2 + border_y; do_stretch(); @@ -728,8 +738,10 @@ void Sdl2Ui::UpdateDisplay() { } else if (want_aspect > real_aspect) { // Letterboxing (black bars top and bottom) window.scale = width_float / main_surface->width(); - viewport.x = border_x; - viewport.w = win_width; + // viewport.x = border_x; + // viewport.w = win_width; + viewport.x = 0; + viewport.w = available_width; viewport.h = static_cast(ceilf(main_surface->height() * window.scale)); viewport.y = (win_height - viewport.h) / 2 + border_y; do_stretch(); @@ -740,15 +752,12 @@ void Sdl2Ui::UpdateDisplay() { viewport.y = border_y; viewport.h = win_height; viewport.w = static_cast(ceilf(main_surface->width() * window.scale)); - viewport.x = (win_width - viewport.w) / 2 + border_x; + // viewport.x = (win_width - viewport.w) / 2 + border_x; + viewport.x = (available_width - viewport.w) / 2; do_stretch(); SDL_RenderSetViewport(sdl_renderer, &viewport); } - if (webview && webview_visible) { - LayoutWebview(); - } - screen_surface = Bitmap::Create(viewport.w, viewport.h, true, main_surface->pitch()); if (vcfg.scaling_mode.Get() == ConfigEnum::ScalingMode::Bilinear && @@ -787,15 +796,6 @@ void Sdl2Ui::UpdateDisplay() { SDL_SetRenderTarget(sdl_renderer, nullptr); SDL_RenderCopy(sdl_renderer, sdl_texture_scaled, nullptr, nullptr); } - //else if (screen_surface && sdl_texture_screen) { - - // SDL_SetRenderTarget(sdl_renderer, sdl_texture_scaled); - // SDL_RenderCopy(sdl_renderer, sdl_texture_game, nullptr, nullptr); - // SDL_RenderCopy(sdl_renderer, sdl_texture_screen, nullptr, nullptr); - - // SDL_SetRenderTarget(sdl_renderer, nullptr); - // SDL_RenderCopy(sdl_renderer, sdl_texture_scaled, nullptr, nullptr); - //} else { SDL_RenderCopy(sdl_renderer, sdl_texture_game, nullptr, nullptr); } @@ -807,6 +807,13 @@ void Sdl2Ui::UpdateDisplay() { } SDL_RenderPresent(sdl_renderer); + +#ifdef _WIN32 + if (webview) { + HWND hwnd = (HWND&)webview->window().value(); + ShowWindow(hwnd, webview_visible ? SW_SHOW : SW_HIDE); + } +#endif } void Sdl2Ui::SetTitle(const std::string &title) { @@ -1526,11 +1533,11 @@ void Sdl2Ui::Dispatch(Intent intent) { switch (intent) { case Intent::ToggleWebview: { webview_visible = !webview_visible; + window.size_changed = true; + UpdateDisplay(); #ifdef _WIN32 HWND hwnd = (HWND&)webview->window().value(); ShowWindow(hwnd, webview_visible ? SW_SHOW : SW_HIDE); - if (webview_visible) - LayoutWebview(); SDL_RaiseWindow(sdl_window); #endif } break; @@ -1549,28 +1556,31 @@ void Sdl2Ui::Dispatch(Intent intent) { } } +namespace { + // constexpr int webview_min_width = 320; + constexpr int webview_max_width = 600; +} + void Sdl2Ui::SetWebviewLayout(WebviewLayout layout) { BaseUi::SetWebviewLayout(layout); - ModifyViewport(); if (layout == WebviewLayout::Expanded && !webview_visible) webview_visible = true; - if (webview_visible) - LayoutWebview(); + LayoutWebview(); } -void Sdl2Ui::ModifyViewport() { +void Sdl2Ui::LayoutWebview() { + if (!webview) return; + switch (webview_layout) { case WebviewLayout::Sidebar: { - webview_dims = { std::max(0, window.width - 800), 0, std::min(800, window.width), window.height }; + int available_width = window.width - webview_max_width; + webview_dims = { available_width, 0, std::min(webview_max_width, window.width - available_width), window.height }; } break; case WebviewLayout::Expanded: { webview_dims = { 0, 0, window.width, window.height }; } break; } -} -void Sdl2Ui::LayoutWebview() { - if (!webview) return; #ifdef _WIN32 HWND hwnd = (HWND&)webview->window().value(); RECT rect{ 0, 0, webview_dims.width, webview_dims.height }; @@ -1580,5 +1590,6 @@ void Sdl2Ui::LayoutWebview() { rect.bottom - rect.top, SWP_FRAMECHANGED | SWP_SHOWWINDOW); #else + // Handle other platforms if necessary #endif } diff --git a/src/window_settings.cpp b/src/window_settings.cpp index 6e2578bcc..c992f9b57 100644 --- a/src/window_settings.cpp +++ b/src/window_settings.cpp @@ -788,6 +788,7 @@ void Window_Settings::RefreshOnline() { if (!cfg.username.Get().empty() && !cfg.session_token.Get().empty()) { AddOption(LockedMenuItem("Status", "", fmt::format("Logged in as {}", cfg.username.Get())), [] {}); AddOption(MenuItem("Logout", "", ""), [this] { + Scene::PopUntil(Scene::SceneType::Map); GMI().Logout(); Refresh(); }); @@ -816,6 +817,7 @@ void Window_Settings::RefreshOnline() { AddOption(LockedMenuItem("Could not login", "", failure), [] {}); } AddOption(MenuItem("Login", "", ""), [this, &cfg] { + Scene::PopUntil(Scene::SceneType::Map); GMI().Login(cfg.username.Get(), cfg.password.Get()); cfg.password.Set(""); Refresh();