diff --git a/CMakeLists.txt b/CMakeLists.txt index b6f3443d0..df3dcb547 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,9 @@ include(PlayerConfigureWindows) include(PlayerFindPackage) include(PlayerBuildType) include(PlayerMisc) +include(GetGitRevisionDescription) +include(CheckCSourceCompiles) +include(FetchContent) # Dependencies provided by CMake Presets option(PLAYER_FIND_ROOT_PATH_APPEND @@ -250,6 +253,10 @@ 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/image_gif.cpp + src/image_gif.h src/input_buttons_desktop.cpp src/input_buttons.h src/input.cpp @@ -513,8 +520,34 @@ 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 + src/window_stringinput.cpp ) +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 + 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 + endif() +endif() + include(CMakeDependentOption) # Include directories @@ -580,6 +613,33 @@ 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) + #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) + + 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() + 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 @@ -624,7 +684,8 @@ 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() @@ -1008,7 +1069,39 @@ 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_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) + + 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() + + #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) + 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) + endif() + target_link_libraries(${PROJECT_NAME} cpr::cpr) endif() # Configure Audio backends @@ -1230,6 +1323,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() @@ -1238,7 +1333,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}) @@ -1253,6 +1349,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}) @@ -1273,7 +1370,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}") @@ -1803,3 +1902,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() diff --git a/CMakePresets.json b/CMakePresets.json index e49252ce0..5092d8469 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": { @@ -637,6 +653,54 @@ "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)", + "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": "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": { @@ -1590,11 +1654,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", @@ -1728,6 +1794,11 @@ "name": "windows-x64-vs2022-libretro-release", "configurePreset": "windows-x64-vs2022-libretro-release" }, + { + "name": "windows-x64-vs2022-ynoshell-release", + "configurePreset": "windows-x64-vs2022-ynoshell-release", + "configuration": "RelWithDebInfo" + }, { "name": "macos-debug", "configurePreset": "macos-debug" 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/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/resources/ynoicons.png b/resources/ynoicons.png new file mode 100644 index 000000000..4388738a8 Binary files /dev/null and b/resources/ynoicons.png differ diff --git a/src/async_handler.cpp b/src/async_handler.cpp index 9ce8695a5..ea47fbcf3 100644 --- a/src/async_handler.cpp +++ b/src/async_handler.cpp @@ -20,11 +20,24 @@ #include #include +#include +using json = nlohmann::json; + #ifdef __EMSCRIPTEN__ # include # include -# include - using json = nlohmann::json; +#elif defined(PLAYER_YNO) +# 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 +# endif #endif #include "async_handler.h" @@ -46,9 +59,25 @@ namespace { std::unordered_map> async_requests; std::unordered_map file_mapping; int next_id = 0; -#ifdef __EMSCRIPTEN__ int index_version = 1; -#endif + 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; + out.swap(session_pool.back()); + session_pool.pop_back(); + return out; + } + void ReleaseSession(std::unique_ptr&& session) { + std::lock_guard _guard(session_mutex); + session_pool.push_back(std::move(session)); + } FileRequestAsync* GetRequest(const std::string& path) { auto it = async_requests.find(path); @@ -72,20 +101,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 +146,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 +157,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 +170,66 @@ 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_(ctx->file); + if (path_.empty()) + path_ = sobj->GetPath(); + if (!sobj->GetRequestExtension().empty()) { + url_ += sobj->GetRequestExtension(); + path_ += sobj->GetRequestExtension(); + } + + 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; + 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) { + 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,21 +240,18 @@ 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) { -#ifdef __EMSCRIPTEN__ 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; } @@ -216,10 +309,6 @@ void AsyncHandler::CreateRequestMapping(const std::string& file) { } } } -#else - // no-op - (void)file; -#endif } void AsyncHandler::ClearRequests() { @@ -232,6 +321,7 @@ void AsyncHandler::ClearRequests() { } } async_requests.clear(); + db_lastwrite = LLONG_MAX; } FileRequestAsync* AsyncHandler::RequestFile(std::string_view folder_name, std::string_view file_name) { @@ -243,7 +333,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)); @@ -289,6 +378,9 @@ void AsyncHandler::SaveFilesystem(int slot_id) { onSaveSlotUpdated($0); }); }, slot_id); +#elif defined(PLAYER_YNO) + if (slot_id != 1) return; + GMI().UploadSaveFile(); #endif } @@ -305,7 +397,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,18 +434,21 @@ 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/"; + request_path += "2kki/"; } std::string modified_path; @@ -379,13 +475,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()) { + if (file_mapping.empty() || parent_scope) { // 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); @@ -398,8 +499,17 @@ 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 + if (Platform::File(request_path).GetLastModified() >= db_lastwrite) { + DownloadDone(true); + return; + } +#endif - auto 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 @@ -460,14 +570,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/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/audio_generic.cpp b/src/audio_generic.cpp index 6f2028317..c00dcd9df 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); @@ -522,7 +471,31 @@ GenericAudio::DecodeResult GenericAudio::Decode(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/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/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..09b9d6e5b 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. @@ -268,6 +274,24 @@ class BaseUi { */ Game_ConfigVideo GetConfig() const; + /** Enables receiving input from IME. Meant to be paired with EndTextCapture. */ + 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. @@ -302,6 +326,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; @@ -331,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. */ @@ -340,6 +369,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 +401,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; @@ -438,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 8a203f6a1..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); @@ -1129,6 +1134,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 +1146,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..a9b3c678d 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. @@ -612,6 +616,7 @@ class Bitmap { ImageOpacity ComputeImageOpacity() const; ImageOpacity ComputeImageOpacity(Rect rect) const; + void SetBilinear(); protected: DynamicFormat format; diff --git a/src/cache.cpp b/src/cache.cpp index cd9e426c6..3d3ec692c 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(std::string_view file) { + return LoadBitmap(file); +} + +BitmapRef Cache::Badge(std::string_view 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..000804d26 100644 --- a/src/cache.h +++ b/src/cache.h @@ -55,6 +55,9 @@ namespace Cache { BitmapRef System(std::string_view filename, bool bg_preserve_transparent_color = false); BitmapRef System2(std::string_view filename); + BitmapRef Emoji(std::string_view filename); + BitmapRef Badge(std::string_view filename); + 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); diff --git a/src/config_param.h b/src/config_param.h index 9398ab149..b20469d9a 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( + 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; +}; /** A configuration parameter with a range */ template 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/drawable.cpp b/src/drawable.cpp index db4abd538..62a2fd61a 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 == &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/filefinder.cpp b/src/filefinder.cpp index 97e51e5aa..50abc2663 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 = 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/font.cpp b/src/font.cpp index 69c213ce6..42fabbd15 100644 --- a/src/font.cpp +++ b/src/font.cpp @@ -58,6 +58,11 @@ #include "game_clock.h" #include "multiplayer/game_multiplayer.h" +#include "graphics.h" +#ifdef PLAYER_YNO +# include "multiplayer/chat_overlay.h" +# include "multiplayer/status_overlay.h" +#endif // Static variables. namespace { @@ -202,6 +207,7 @@ namespace { FontRef default_mincho; FontRef name_text; FontRef name_text_2; + FontRef chat_text; struct ExFont final : public Font { public: @@ -416,6 +422,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); @@ -698,6 +706,23 @@ 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(); + Graphics::GetStatusOverlay().OnResolutionChange(); +#endif +} + FontRef Font::CreateFtFont(Filesystem_Stream::InputStream is, int size, bool bold, bool italic) { #ifdef HAVE_FREETYPE if (!is) { @@ -791,6 +816,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 / (double)original_style.size)); size.height = current_style.size; return size; @@ -799,7 +825,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, 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 { @@ -814,6 +841,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; } @@ -829,7 +857,10 @@ 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 / (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; } @@ -846,6 +877,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 = original_style.size / (double)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 +921,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 +940,10 @@ 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 = 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); } src_x = color % 10 * 16 + 2; @@ -924,11 +963,13 @@ 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); + //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); + 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. @@ -948,10 +989,16 @@ 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 = 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; + 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..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; @@ -235,12 +236,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, @@ -256,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_actor.cpp b/src/game_actor.cpp index 91d7668f6..f1ef1f0e2 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 { +std::string_view Game_Actor::ClassName() const { if (!GetClass()) { return {}; } diff --git a/src/game_actor.h b/src/game_actor.h index cf2631573..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 */ - std::string_view GetClassName() const; + std::string_view ClassName() const; /** * Gets battle commands. 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 b64dd805f..0a8b0cfdb 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 @@ -169,6 +176,19 @@ struct Game_ConfigInput { void Hide(); }; +struct Game_ConfigOnline { + StringConfigParam session_token{ "", "", "Online", "SessionToken" }; + StringConfigParam username{ "Username", "Chat nickname or YNOproject account; max 12 characters", "Online", "Username" }; + // not persisted + 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"), + 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; @@ -182,6 +202,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_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/game_message.cpp b/src/game_message.cpp index 0404cbd71..81a19f06a 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(std::string_view 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(std::string_view 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(const 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.cbegin(); span != spans.cend(); ++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 (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, fragment->color)); + if (must_break) + flush(); + + start = next; + } while (start < line.size()); + } + else if (auto box = span->get()->Downcast()) { + int width = box->GetSize().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..9c851eb48 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 ComponentWrapCallback& callback, const Font& font); +#endif + /** * Return if it's legal to show a new message box. * diff --git a/src/graphics.cpp b/src/graphics.cpp index f19c4b031..6a0fe461b 100644 --- a/src/graphics.cpp +++ b/src/graphics.cpp @@ -30,6 +30,10 @@ #include "drawable_mgr.h" #include "baseui.h" #include "game_clock.h" +#ifdef PLAYER_YNO +# include "multiplayer/chat_overlay.h" +# include "multiplayer/status_overlay.h" +#endif using namespace std::chrono_literals; @@ -40,6 +44,10 @@ namespace Graphics { std::unique_ptr message_overlay; std::unique_ptr fps_overlay; +#ifdef PLAYER_YNO + std::unique_ptr chat_overlay; + std::unique_ptr status_overlay; +#endif std::string window_title_key; } @@ -50,11 +58,15 @@ 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(); @@ -104,28 +116,31 @@ 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 max_z = std::numeric_limits::max(); + 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); } std::shared_ptr Graphics::UpdateSceneCallback() { @@ -149,3 +164,12 @@ MessageOverlay& Graphics::GetMessageOverlay() { return *message_overlay; } +#ifdef PLAYER_YNO +ChatOverlay& Graphics::GetChatOverlay() { + return *chat_overlay; +} + +StatusOverlay& Graphics::GetStatusOverlay() { + return *status_overlay; +} +#endif diff --git a/src/graphics.h b/src/graphics.h index 6bf5ec988..ffc345bdc 100644 --- a/src/graphics.h +++ b/src/graphics.h @@ -24,9 +24,14 @@ #include "drawable.h" #include "drawable_list.h" #include "game_clock.h" +#include "baseui.h" class MessageOverlay; class Scene; +#ifdef PLAYER_YNO +class ChatOverlay; +class StatusOverlay; +#endif /** * Graphics namespace. @@ -48,9 +53,9 @@ 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); std::shared_ptr UpdateSceneCallback(); @@ -61,6 +66,10 @@ namespace Graphics { * @return message overlay */ 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/image_gif.cpp b/src/image_gif.cpp new file mode 100644 index 000000000..77530487b --- /dev/null +++ b/src/image_gif.cpp @@ -0,0 +1,110 @@ +#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; + } + + scratch.resize(gifFile->SWidth * gifFile->SHeight); + std::fill(scratch.begin(), scratch.end(), gifFile->SBackGroundColor | 0xFF000000); + + currentFrame = 0; +} + +ImageGif::Decoder::~Decoder() noexcept { + if (gifFile) DGifCloseFile(gifFile, nullptr); +} + +bool ImageGif::Decoder::ReadNext(ImageOut& output, GifTimingInfo& timing) { + if (!gifFile || currentFrame >= gifFile->ImageCount) { + output.pixels = nullptr; + return false; + } + + const SavedImage* frame = &gifFile->SavedImages[currentFrame]; + const GifImageDesc& frameInfo = frame->ImageDesc; + + 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; + } + + const int fullWidth = gifFile->SWidth; + const int fullHeight = gifFile->SHeight; + output.width = fullWidth; + output.height = fullHeight; + output.bpp = 0; + + 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; + + 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; + } + } + + if (disposalMethod == 2) { + std::fill(scratch.begin(), scratch.end(), bg | 0xFF000000); // Fill with background color + } + + size_t srcIndex = 0; + for (int y = top; y < top + height; ++y) { + for (int x = left; x < left + width; ++x) { + GifPixelType index = src[srcIndex++]; + if (index == transparentIndex) { + if (disposalMethod != 1) + scratch[y * fullWidth + x] = 0; // Transparent pixel + } else if (index < colorMap->ColorCount) { + const GifColorType& color = colorMap->Colors[index]; + 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 new file mode 100644 index 000000000..dcff1022c --- /dev/null +++ b/src/image_gif.h @@ -0,0 +1,49 @@ +/* + * 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; + std::vector scratch; + + GifFileType* gifFile; + int currentFrame{}; + }; + +} + +#endif diff --git a/src/image_webp.cpp b/src/image_webp.cpp new file mode 100644 index 000000000..4d18bc11c --- /dev/null +++ b/src/image_webp.cpp @@ -0,0 +1,73 @@ +/* + * 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); +} + +ImageWebP::Decoder::Decoder(Filesystem_Stream::InputStream& is) noexcept { + // is.read(reinterpret_cast(&dec.data.bytes)); + // WebPMalloc + 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(data.bytes, data.size, nullptr, nullptr)) { + Output::Warning("ImageWebP: {} is not webp", is.GetName()); + return; + } + + decoder = WebPAnimDecoderNew(&data, nullptr); + if (!decoder) { + Output::Warning("ImageWebP: Failed to create decoder for {}", is.GetName()); + return; + } + + if (!WebPAnimDecoderGetInfo(decoder, &animData)) { + if (decoder) WebPAnimDecoderDelete(decoder); + decoder = nullptr; + Output::Warning("ImageWebP: Failed to get animation info for {}", is.GetName()); + return; + } +} + + +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]; + memcpy(output.pixels, pixels, animData.canvas_width * animData.canvas_height * sizeof(uint32_t)); + 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..e57fa875a --- /dev/null +++ b/src/image_webp.h @@ -0,0 +1,47 @@ +/* + * 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; + Decoder(Filesystem_Stream::InputStream& is) noexcept; + inline operator bool() const noexcept { return decoder != nullptr; } + 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 00aa90ca7..4cc7a29f0 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(); + std::string_view 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..a84a8cf47 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 = 0; + int sel_length = 0; + }; + + /** 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/input_buttons.h b/src/input_buttons.h index 652406292..a684603ad 100644 --- a/src/input_buttons.h +++ b/src/input_buttons.h @@ -85,6 +85,10 @@ namespace Input { FAST_FORWARD_B, TOGGLE_FULLSCREEN, TOGGLE_ZOOM, + SHOW_CHAT, + CHAT_SCROLL_DOWN, + CHAT_SCROLL_UP, + TOGGLE_SIDEBAR, BUTTON_COUNT }; @@ -130,9 +134,12 @@ namespace Input { "FAST_FORWARD_A", "FAST_FORWARD_B", "TOGGLE_FULLSCREEN", - "TOGGLE_ZOOM"); + "TOGGLE_ZOOM", + "SHOW_CHAT", + "CHAT_SCROLL_DOWN", + "CHAT_SCROLL_UP", + "TOGGLE_SIDEBAR"); static_assert(kInputButtonNames.size() == static_cast(BUTTON_COUNT)); - constexpr auto kInputButtonHelp = lcf::makeEnumTags( "Up Direction", "Down Direction", @@ -175,7 +182,11 @@ 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", + "Scroll chat window down", + "Scroll chat window up", + "Toggle sidebar display"); static_assert(kInputButtonHelp.size() == static_cast(BUTTON_COUNT)); /** @@ -191,6 +202,8 @@ namespace Input { case TOGGLE_ZOOM: 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 9fb6bc772..6694aeb09 100644 --- a/src/input_buttons_desktop.cpp +++ b/src/input_buttons_desktop.cpp @@ -101,6 +101,14 @@ Input::ButtonMappingArray Input::GetDefaultButtonMappings() { {RESET, Keys::F12}, {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}, + {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 new file mode 100644 index 000000000..fcfb0bea0 --- /dev/null +++ b/src/multiplayer/chat_overlay.cpp @@ -0,0 +1,843 @@ +/* + * 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 "multiplayer/overlay_utils.h" +#include +#include + +#include +#include +#include + +using json = nlohmann::json; + +#include "chat_overlay.h" +#include "drawable_mgr.h" +#include "baseui.h" +#include "output.h" +#include "input_buttons.h" +#include "cache.h" +#include "game_message.h" +#include "scene.h" +#include "multiplayer/game_multiplayer.h" +#include "multiplayer/scene_overlay.h" +#include "messages.h" +#include "input.h" +#include "font.h" +#include "compiler.h" +#include "icons.h" +#include "overlay_utils.h" +#include "image_webp.h" +#include "image_gif.h" + +namespace { + Point ChatTextSquare() { + int text_height = OverlayUtils::ChatTextHeight(); + 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; + + 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: {}", resp.text); + 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 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); + } + } + }, + [](uv_work_t* task, int) { delete task; }); + } +} + +ChatOverlay::ChatOverlay() : Drawable(Priority_Overlay, Drawable::Flags::Global | Drawable::Flags::Screenspace) +{ + InitializeEmojiMappings(); +} + +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.BlitFast(ox, oy, *bitmap, bitmap->GetRect(), Opacity::Opaque()); + return; + } + + bitmap->Clear(); + + Rect screen_rect = DisplayUi->GetScreenSurfaceRect(); + int y_end = screen_rect.height; + + auto& font = *Font::ChatText(); + int text_height; + std::optional _guard; + if (OverlayUtils::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; + } + + 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; + + 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 = show_all + ? std::clamp(scroll - half_screen, 0, std::min(scroll + half_screen, last_sticky)) + : 0; + + 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->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; + offset += prompt_width; + 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), + "Set a username in Online settings to join the chat."); + } + } + + 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) { + 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 != black; + + bool first = true; + bool has_header = true; + Game_Message::WordWrap( + message->text, + bitmap->width(), + [&](lcf::Span> line) { + if (first) { + first = false; + // we added the username as a virtual span, so now we remove it. + 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) { + nextline.emplace_back(span); + } + } + }, font); + + for (auto line = lines.rbegin(); line != lines.rend(); ++line) { + // semitransparent bg + int yidx = lidx + 1 + input_row_offset; + int line_height = text_height; + 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()); })) { + 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 + + 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() && EP_LIKELY(has_header)) { + // draw the username + if (message->global) + 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) { + 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 += Icons::RenderIcon(*bitmap, offset, y, Icons::YnoIcons::shield, offwhite, font); break; + case 2: // dev + offset += RenderIcon(*bitmap, offset, y, Icons::YnoIcons::wrench, offwhite, font); break; + } + 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 + (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); + if (emoji->bitmap) { + bitmap->StretchBlit({offset, comp_y, dims.x, dims.y}, *emoji->bitmap, emoji->bitmap->GetRect(), 255); + } + else if (!emoji->HasAnimation()) { + // the emoji is still live, request it now + emoji->RequestBitmap(this); + bitmap->FillRect({ offset, comp_y, dims.y, dims.y }, offwhite); + } + offset += dims.x; + } + else if (auto screenshot = span->Downcast()) { + Point dims = screenshot->GetSize(); + if (screenshot->bitmap) { + 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 + (baseline ? baseline - text_height : 0), width, text_height }, offwhite); + offset += width; + } + } + ++lidx; + if ((lidx + input_row_offset) * text_height + y_offset > y_end) break; + } + ++i; + lines.clear(); + } + if (!show_all && lidx >= message_max_minimized) break; + if ((lidx + input_row_offset) * text_height + y_offset > y_end) break; + } + + 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->FillRect({ bitmap->width() - dims.width, std::clamp(desired_y, 0, y_end - dims.height), scrollbar_width, (int)ceilf(scrollbar_height) }, scrollbar); + } + + dst.BlitFast(ox, oy, *bitmap, bitmap->GetRect(), Opacity::Opaque()); + 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) { + counter = 0; + + 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(); + } + + if (scroll && (scroll + 1 < messages.size())) + scroll += 1; + + dirty = true; + return ret; +} + +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) { + if (auto string = span->Downcast()) { + string->color = Color(230, 230, 180, 255); + break; + } + } +} + +void ChatOverlay::Update() { + if (!DisplayUi) { + return; + } + + if (!bitmap) { + OnResolutionChange(); + } + + if (!show_all && IsAnyMessageVisible()) { + if (++counter > 450) { + counter = 0; + for (auto& message : messages) { + message.hidden = true; + } + 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() { + 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::CHAT_SCROLL_DOWN)) { + DoScroll(std::clamp(--delta / 4, -4, -1)); + } + + if (Input::IsTriggered(Input::InputButton::CHAT_SCROLL_UP)) { + delta = -5; + DoScroll(1); + } + else if (Input::IsRepeated(Input::InputButton::CHAT_SCROLL_UP)) { + DoScroll(std::clamp(++delta / 4, 1, 4)); + } + + if (Input::IsRawKeyPressed(Input::Keys::MOUSE_SCROLLDOWN)) { + DoScroll(-3); + } + else if (Input::IsRawKeyPressed(Input::Keys::MOUSE_SCROLLUP)) { + DoScroll(3); + } + + if (Input::IsRawKeyPressed(Input::Keys::ENDS)) { + scroll = 0; + MarkDirty(); + } + + if (!GMI().CanChat()) return; + + // chat input only below this point + + std::string_view ui_input(Input::text_input.data()); + if (!ui_input.empty()) { + input.append(Utils::DecodeUTF32(ui_input)); + dirty = true; + Input::text_input.clear(); + } + 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()) { + // 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::SessionSay{ std::move(encoded) }); + 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(); })); + Rect screen_rect = DisplayUi->GetScreenSurfaceRect(); + 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 + DisplayUi->EndTextCapture(); +} + +void ChatOverlay::DoScroll(int increase) { + if (messages.empty()) return; + scroll += increase; + scroll = (scroll + messages.size()) % messages.size(); + counter = 0; + dirty = true; +} + +void ChatOverlay::OnResolutionChange() { + if (!bitmap) { + DrawableMgr::Register(this); + } + + int text_height = OverlayUtils::ChatTextHeight(); + Rect rect = DisplayUi->GetScreenSurfaceRect(); + bitmap = Bitmap::Create(rect.width, rect.height); + scrollbar_width = ceilf(0.008 * rect.width); + dirty = true; +} + +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 { + 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"); + + int text_height = OverlayUtils::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)); + int square = OverlayUtils::ChatTextHeight(); + if (has_badge) + rect.width += square; + if (global) + rect.width += square; + if (rank > 0) + rect.width += square; + return Point{ rect.width, rect.height }; + }, ChatComponents::Header)); + + auto it = msg.begin(); + auto last_end = msg.begin(); + while (it != msg.end()) { + std::match_results match; + if (std::regex_search(it, msg.end(), match, emoji_pattern)) { + 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(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)) { + std::string_view between(&*last_end, match[0].first - last_end); + if (!between.empty()) { + out.emplace_back(std::make_shared(between)); + } + + 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; + } + } + + std::string_view 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, 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)), sender_uuid(std::move(sender_uuid)), 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([this](FileRequestResult* result) { + if (!result->success) return; + this->badge = Cache::Badge(result->file); + this->badge->SetBilinear(); + parent->MarkDirty(); + }); + req->Start(); + } +} + +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); + sizer = ChatTextSquare; + + RequestBitmap(parent); +} + +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); + + // 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) { + request.reset(); // Clear the request after processing + if (!result->success) { + emoji.clear(); + Output::Debug("failed: {}", result->file); + return; + } + 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) { + constexpr int minimum_frame_delay = 20; + frames.clear(); + frameDelays.clear(); + 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; + 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) { + frames.clear(); + frameDelays.clear(); + auto fs = FileFinder::OpenImage("../images/ynomoji", filePath); + if (!fs) return; + + ImageWebP::Decoder dec(fs); + if (!dec) return; + + ImageOut image{}; + TimingInfo timing{}; + int smear = 0; + 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; + 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(); + } + + 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() == 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(); + 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 && in_viewport) 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) +{ + 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 new file mode 100644 index 000000000..45351606f --- /dev/null +++ b/src/multiplayer/chat_overlay.h @@ -0,0 +1,248 @@ +/* + * 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 +#include +#include +#include + +#include "drawable.h" +#include "bitmap.h" +#include "memory_management.h" +#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 +}; + +template +struct ChatComponentsMap { using type = void; }; + +class ChatOverlay; + +class ChatComponent : public std::enable_shared_from_this { +protected: + const ChatComponents runtime_type; + ChatComponent(ChatComponents type = ChatComponents::Box) noexcept : + static_dim(0, 0), runtime_type(type) { } +public: + const Point static_dim; + std::function sizer; + + ChatComponent(int width, int height, ChatComponents type = ChatComponents::Box) noexcept : + 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, 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; + bool hidden = false; + ChatOverlay* parent; + 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; + + std::function OnInteract; +}; + +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(); + 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 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); } + inline bool ShowingAll() const noexcept { return show_all; } + void DoScroll(int increase); + void MarkDirty() noexcept; + void AddSystemMessage(std::string_view string); +private: + bool IsAnyMessageVisible() const; + + ViewportInfo LayoutViewport(int text_height, const Font& font) const; + + bool dirty = false; + bool show_all = false; + int ox = 0; + int oy = 0; + + int message_max = 200; + 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 }; + + std::u32string 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: + std::string string; + Color color; + + ChatString(std::string_view other, Color color = default_color) : ChatComponent(0, 0, ChatComponents::String), + string(ToString(other)), color(color) {} +}; + +class ChatEmoji : public ChatComponent { +public: + std::string emoji; + 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); + 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 + 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 { +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 uuid, std::string id, bool temp, bool spoiler); + static Point sizer(); +private: + uv_work_t task{}; +}; + + +template<> +struct ChatComponentsMap { using type = ChatComponent; }; +template<> +struct ChatComponentsMap { using type = ChatString; }; +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() { + 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; +} + +#endif 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.cpp b/src/multiplayer/connection.cpp index 93db5d5fb..4ad81ed13 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::string os{}; + for (auto it = args.begin(); it != args.end(); ++it) { + os.append(*it); + if (std::next(it) != args.end()) + os.append(Packet::PARAM_DELIM); + } + raw_handler(name, os); + } 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 2fee28fde..322680beb 100644 --- a/src/multiplayer/connection.h +++ b/src/multiplayer/connection.h @@ -56,9 +56,13 @@ class Connection { using SystemMessageHandler = std::function; void RegisterSystemHandler(SystemMessage m, SystemMessageHandler h); + template + inline void RegisterRawHandler(F&& callback) { raw_handler = callback; } + 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; @@ -75,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 e5e1a6ac6..0f572cfcc 100644 --- a/src/multiplayer/game_multiplayer.cpp +++ b/src/multiplayer/game_multiplayer.cpp @@ -43,6 +43,24 @@ #include "yno_connection.h" #include "messages.h" +#ifdef PLAYER_YNO +# include +# include +# include +# include +# include "scene_settings.h" +# include "chat_overlay.h" +# include "graphics.h" +# include "platform.h" +# include +# include "status_overlay.h" +# if defined(_WIN32) && !defined(timegm) +# define timegm _mkgmtime +# endif +using namespace std::literals::chrono_literals; +using json = nlohmann::json; +#endif + static Game_Multiplayer _instance; Game_Multiplayer& Game_Multiplayer::Instance() { @@ -51,6 +69,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) { @@ -67,7 +88,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(); @@ -131,12 +152,17 @@ 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::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); @@ -147,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(); @@ -163,6 +192,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) { @@ -242,8 +277,17 @@ 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; +#endif }); connection.RegisterHandler("d", [this] (DisconnectPacket& p) { auto it = players.find(p.id); @@ -454,8 +498,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()); @@ -463,10 +507,119 @@ void Game_Multiplayer::InitConnection() { DrawableMgr::SetLocalList(old_list); 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 + sessionConn.RegisterSystemHandler(YSM::OPEN, [this](MCo&) { + if (connection.IsConnected()) + connection.Close(); + session_active = true; + if (room_id != -1) + Connect(room_id); + + // sync chat messages + 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"]) { + 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"]; + 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; + int rank = 0; + 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; + account = player->second.account; + rank = player->second.rank; + } + std::string contents((*it)["contents"]); + chatoverlay.AddMessage(contents, name, uuid, system, badge, account, true, rank); + 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; + 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; + } + Graphics::GetChatOverlay().AddMessage(p.msg, name, p.uuid, system, badge, account, true, rank); + lastmsgid = p.msgid; + }); + sessionConn.RegisterHandler("say", [this](SessionSay& p) { + std::string name = "Unknown Player"; + 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; + } + Graphics::GetChatOverlay().AddMessage(p.msg, name, p.uuid, system, badge, account, false, rank); + }); + sessionConn.RegisterHandler("p", [this](SessionPlayerInfo& p) { + auto& player = playerdata[p.uuid]; + player.name = p.name; + player.systemName = p.systemName; + player.rank = p.rank; + player.account = p.account; + player.badge = p.badge; + }); + sessionConn.RegisterHandler("pc", [this](SessionPlayerCount& p) { + Graphics::GetStatusOverlay().SetPlayerCount(p.player_count); + }); +#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 +678,17 @@ void Game_Multiplayer::Initialize() { } } +void Game_Multiplayer::InitSession() { +#ifdef PLAYER_YNO + if (!session_token.empty() || !cfg.session_token.Get().empty()) { + CheckLogin(false); + } + + sessionConn.need_header = false; + sessionConn.Open(GetSessionEndpoint()); +#endif +} + void Game_Multiplayer::Quit() { Web_API::UpdateConnectionStatus(0); // disconnected connection.Close(); @@ -548,25 +712,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 +740,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 +784,18 @@ 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)); +#ifndef EMSCRIPTEN + if (on_system_graphic_change) { + on_system_graphic_change(ToString(sys)); + } +#endif } void Game_Multiplayer::SePlayed(const lcf::rpg::Sound& sound) { if (!Main_Data::game_player->IsMenuCalling()) { - connection.SendPacketAsync(sound); + connection.SendPacketAsync(sound); } } @@ -670,19 +839,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 +860,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 +876,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 +897,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); } } @@ -746,6 +915,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()) { @@ -759,7 +936,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(); } } @@ -782,7 +960,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); @@ -866,7 +1044,8 @@ void Game_Multiplayer::Update() { } } } - p.second.chat_name->SetTransparent(overlap); + if (p.second.chat_name) + p.second.chat_name->SetTransparent(overlap); } } @@ -900,6 +1079,182 @@ void Game_Multiplayer::Update() { DrawableMgr::SetLocalList(old_list); } - if (session_connected) + if (session_connected) { connection.FlushQueue(); +#ifndef EMSCRIPTEN + sessionConn.FlushQueue(); +#endif + } + + UpdateTimers(); +} + +void Game_Multiplayer::SetConfig(const Game_ConfigOnline& cfg) { + this->cfg = cfg; + session_token = cfg.session_token.Get(); + SyncSaveFile(); + 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{ ApiEndpoint("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); + } + 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{ ApiEndpoint("login") }, + cpr::Body{ formdata }, header); + 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(); + } + else { + Output::Warning("login failed: {} {}", resp.status_code, resp.text); + login_failure = resp.text; + } +} + +void Game_Multiplayer::Logout() { + session_token.clear(); + username.clear(); + cfg.session_token.Set(""); + cfg.username.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(std::string_view name) { + std::string value(name); + GMI().sessionConn.SendPacketAsync(value); + username = value; + cfg.username.Set(value); + Graphics::GetChatOverlay().MarkDirty(); + 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{ 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); + + 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"; + 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(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() || !FileFinder::Game()) return; + + const char* path = "Save01.lsd"; + 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 resp = cpr::Post( + 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); + 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 597759fb7..a439bc880 100644 --- a/src/multiplayer/game_multiplayer.h +++ b/src/multiplayer/game_multiplayer.h @@ -10,6 +10,15 @@ #include #include "yno_connection.h" +#ifdef PLAYER_YNO +# include "game_config.h" +# include "game_clock.h" + +namespace cpr { + class Response; +} +#endif + class PlayerOther; class Game_Multiplayer { @@ -20,6 +29,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 +66,31 @@ class Game_Multiplayer { } settings; YNOConnection connection; +#ifdef PLAYER_YNO + YNOConnection sessionConn; + struct PlayerData { + public: + std::string name; + std::string systemName; + int rank; + bool account; + std::string badge; + }; + struct ChatMsg { + public: + std::string_view content; + std::string_view sender; + 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; + std::string lastmsgid; + std::map playerdata; +#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 @@ -65,12 +100,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) { @@ -98,8 +128,40 @@ 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); + 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); + 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(std::string_view name); + std::string ApiEndpoint(std::string_view path) const; }; 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 040d68f65..9960fac0c 100644 --- a/src/multiplayer/messages.h +++ b/src/multiplayer/messages.h @@ -380,6 +380,59 @@ namespace S2C { public: BadgeUpdatePacket(const PL& v) {} }; +#ifdef PLAYER_YNO + 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(Decode(v.at(4))), + y(Decode(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; + }; + 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? + }; + class SessionPlayerCount : public S2CPacket { + public: + SessionPlayerCount(const PL& v) : + player_count(Decode(v.at(0))) {} + int player_count; + }; +#endif } namespace C2S { using C2SPacket = Multiplayer::C2SPacket; @@ -619,7 +672,32 @@ namespace C2S { int event_id; int action_bin; }; - +#ifdef PLAYER_YNO + 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; + }; + 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; + }; + 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 new file mode 100644 index 000000000..fda19ed67 --- /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 >= 720; + } + inline int ChatTextHeight() { + return LargeScreen() ? 37 : 12; + } +} + +#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_nexus.cpp b/src/multiplayer/scene_nexus.cpp new file mode 100644 index 000000000..04141f576 --- /dev/null +++ b/src/multiplayer/scene_nexus.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 "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(); + 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(); +} + +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(); + + 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(); +} + +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://ynoproject.net"); + }); +} + +void Scene_Nexus::vUpdate() { + if (!selected_game.empty()) { + LaunchGame(selected_game); + selected_game = ""; + return; + } +} + +void Scene_Nexus::LaunchGame(std::string_view 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")); + webview.navigate("https://playtest.ynoproject.net/dev"); + }); + 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..f8b39afea --- /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(std::string_view); + void InitWebview(); + std::string selected_game; + std::filesystem::path old_pwd; +}; + +#endif 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/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/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/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 17284da5a..6c3ca126b 100644 --- a/src/multiplayer/yno_connection.cpp +++ b/src/multiplayer/yno_connection.cpp @@ -1,42 +1,75 @@ #include "yno_connection.h" -#include +#include "multiplayer/packet.h" +#include "output.h" +#include +#include +#include +#include +#include +#ifdef __EMSCRIPTEN__ +# include +#else +# include +# include +# include +# include +# if defined(_MSC_VER) +# include +# elif defined(__linux__) +# include +# include +# else +# error No futex implementation available for this platform +# endif +#endif #include "../external/TinySHA1.hpp" #ifndef PLAYER_PSK #define PLAYER_PSK #endif +namespace { + std::shared_ptr _sessionclient; + std::string session_token; +} + struct YNOConnection::IMPL { +#ifdef __EMSCRIPTEN__ EMSCRIPTEN_WEBSOCKET_T socket; - uint32_t msg_count; - bool closed; +#else + std::shared_ptr _wsclient; + WebSocketClient* GetWsClient() { return _wsclient.get(); }; +#endif + + uint32_t msg_count = 0; + bool closed = true; - static EM_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 EM_TRUE; + return true; } - static EM_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 EM_TRUE; + return true; } - static EM_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); + if (!_this->IsConnected()) return false; // 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; + if (isText) { + return false; } - std::string_view cstr(reinterpret_cast(event->data), event->numBytes); std::vector mstrs = Split(cstr, Multiplayer::Packet::MSG_DELIM); for (auto& mstr : mstrs) { auto p = mstr.find(Multiplayer::Packet::PARAM_DELIM); @@ -48,22 +81,249 @@ 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)); } } - return EM_TRUE; + return true; } +#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 }; +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) { +} + +WebSocketClient::~WebSocketClient() { + Stop(); +} + +void WebSocketClient::Start() { + if (running_) return; + + context_ = GetDefaultContext(); + + const char *prot, *addr, *path; + int port; + if (lws_parse_uri(url_.data(), &prot, &addr, &port, &path)) { + Output::Error("Invalid wsurl: {}", url_); + } + + 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; + ccinfo.opaque_user_data = this; + + wsi_ = lws_client_connect_via_info(&ccinfo); + if (!wsi_) { + lws_context_destroy(context_); + Output::Debug("Failed to connect to the WebSocket server"); + return; + } + + running_.store(true, std::memory_order_acquire); +} + +void WebSocketClient::Stop() { + 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) { + std::lock_guard _guard(bmutex_); + buffer_.insert(buffer_.end(), data.begin(), data.end()); +} + +int WebSocketClient::CallbackFunction(struct lws* wsi, enum lws_callback_reasons reason, + void* user, void* in, size_t len) { + + 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: + if (!client->running_ && client->ready_) return -1; + client->ready_ = true; + if (client->on_connect_) client->on_connect_(client->userdata_); + break; + + case LWS_CALLBACK_CLIENT_WRITEABLE: { + if (!client->running_ && client->ready_) return -1; + if (!client->buffer_.empty()) { + size_t current_size = client->buffer_.size(); + std::vector buffer; + { + 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; + } + } + } break; + + case LWS_CALLBACK_CLIENT_RECEIVE: + if (!client->running_ && client->ready_) return -1; + if (client->on_message_) { + std::string message(reinterpret_cast(in), len); + client->on_message_(message, client->userdata_); + } + 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), + lws_get_close_length(wsi) + ); + bool exit = false; + if (close_span.size() >= 2) { + // 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_); + } + break; + + case LWS_CALLBACK_CLIENT_CLOSED: + client->running_.store(false, std::memory_order_release); + if (client->on_disconnect_) { + client->on_disconnect_(false, client->userdata_); + } + break; + + 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 && 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_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; + } + + return 0; +} +#endif + const size_t YNOConnection::MAX_QUEUE_SIZE{ 4088 }; @@ -74,14 +334,22 @@ 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); +#else + impl->set_callbacks(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); +#else + impl->set_callbacks(this); +#endif } return *this; } @@ -92,11 +360,11 @@ YNOConnection::~YNOConnection() { } void YNOConnection::Open(std::string_view uri) { + std::string s {uri}; +#ifdef __EMSCRIPTEN__ if (!impl->closed) { Close(); } - - std::string s {uri}; EmscriptenWebSocketCreateAttributes ws_attrs = { s.data(), "binary", @@ -105,6 +373,44 @@ void YNOConnection::Open(std::string_view uri) { impl->socket = emscripten_websocket_new(&ws_attrs); impl->closed = false; IMPL::set_callbacks(impl->socket, this); +#else + 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 (!self->IsConnected()) return; + self->Close(); + bool expected_value = false; +#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 + 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) { + 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 } void YNOConnection::Close() { @@ -112,11 +418,16 @@ 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 + // handled by create_client + //impl->_wsclient->Stop(); +#endif } template @@ -128,17 +439,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) { @@ -186,12 +489,26 @@ void YNOConnection::Send(std::string_view data) { if (!IsConnected()) return; unsigned short ready; +#ifdef __EMSCRIPTEN__ emscripten_websocket_get_ready_state(impl->socket, &ready); - if (ready == 1) { // OPEN +#else + auto wsclient = impl->GetWsClient(); + ready = wsclient->Ready(); +#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 + wsclient->Send(sendmsg); + wsclient->RequestFlush(); +#endif } } @@ -218,8 +535,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 7b8048e36..ae1dfd591 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 { @@ -12,13 +19,62 @@ 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; + void Close() override; protected: struct IMPL; 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_); + } + 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}, + {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/platform.cpp b/src/platform.cpp index c29241984..1f537b3a1 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 timespec 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/sdl/main.cpp b/src/platform/sdl/main.cpp index cb289d367..a95c22505 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(); diff --git a/src/platform/sdl/sdl2_ui.cpp b/src/platform/sdl/sdl2_ui.cpp index db9efbdc6..a4a9030dc 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" @@ -43,6 +48,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" @@ -139,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); @@ -148,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); @@ -232,6 +244,13 @@ Sdl2Ui::~Sdl2Ui() { audio_.reset(); #endif + 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(); } @@ -355,6 +374,13 @@ 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 + #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 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, @@ -447,9 +473,43 @@ bool Sdl2Ui::RefreshDisplayMode() { renderer_sg.Dismiss(); window_sg.Dismiss(); +#ifdef PLAYER_YNO + 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); + if (auto widget = w.window(); widget.ok()) { +#ifdef _WIN32 + HWND childHwnd = (HWND)widget.value(); + SetParent(childHwnd, hwnd); + //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_SHOWWINDOW); +#else + GtkWidget* childHandle = (GtkWidget*&)widget.value(); +#endif // PLAYER_YNO + } + Sleep(200); + Web_API::InitializeBindings(); + w.run(); + } + catch (const webview::exception& err) { + Output::Error("webview died: {}", (int)err.error().code()); + } + }); +#endif // PLAYER_YNO } else { // Browser handles fast resizing for emscripten, TODO: use fullscreen API -#if !defined(__EMSCRIPTEN__) && !defined(__PS4__) +#ifndef EMSCRIPTEN bool is_fullscreen = (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN_DESKTOP; if (is_fullscreen) { SDL_SetWindowFullscreen(sdl_window, SDL_WINDOW_FULLSCREEN_DESKTOP); @@ -463,6 +523,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 @@ -474,6 +536,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(); @@ -493,6 +558,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; } @@ -630,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; @@ -649,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(); @@ -657,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(); @@ -669,12 +752,16 @@ 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 (vcfg.scaling_mode.Get() == ConfigEnum::ScalingMode::Bilinear && window.scale > 0.f) { + screen_surface = Bitmap::Create(viewport.w, viewport.h, 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); } @@ -686,6 +773,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); @@ -697,14 +795,25 @@ void Sdl2Ui::UpdateDisplay() { SDL_SetRenderTarget(sdl_renderer, nullptr); SDL_RenderCopy(sdl_renderer, sdl_texture_scaled, nullptr, nullptr); - } else { + } + 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); + +#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) { @@ -736,6 +845,20 @@ bool Sdl2Ui::HandleErrorOutput(const std::string &message) { return true; } +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(); +} + +void Sdl2Ui::EndTextCapture() { + SDL_StopTextInput(); +} + void Sdl2Ui::ProcessEvent(SDL_Event &evnt) { switch (evnt.type) { case SDL_WINDOWEVENT: @@ -789,6 +912,14 @@ void Sdl2Ui::ProcessEvent(SDL_Event &evnt) { case SDL_FINGERMOTION: ProcessFingerEvent(evnt); return; + + case SDL_TEXTINPUT: + case SDL_TEXTEDITING: +#ifdef SDL_TEXTEDITING_EXT + case SDL_TEXTEDITING_EXT: +#endif + ProcessTextEditEvent(evnt); + break; } } @@ -1059,6 +1190,39 @@ 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(); + +#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 +#endif + { + 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; + return; + } + + composition.active = true; + return; + } + + Input::text_input.clear(); + Input::text_input.append(event.text.text); +} + void Sdl2Ui::SetAppIcon() { #if defined(__MORPHOS__) // do nothing @@ -1364,3 +1528,68 @@ bool Sdl2Ui::OpenURL(std::string_view url) { return true; } + +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); + SDL_RaiseWindow(sdl_window); +#endif + } break; + case Intent::ToggleDetachWebview: { + 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; + } +} + +namespace { + // constexpr int webview_min_width = 320; + constexpr int webview_max_width = 600; +} + +void Sdl2Ui::SetWebviewLayout(WebviewLayout layout) { + BaseUi::SetWebviewLayout(layout); + if (layout == WebviewLayout::Expanded && !webview_visible) + webview_visible = true; + LayoutWebview(); +} + +void Sdl2Ui::LayoutWebview() { + if (!webview) return; + + switch (webview_layout) { + case WebviewLayout::Sidebar: { + 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; + } + +#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 + // Handle other platforms if necessary +#endif +} diff --git a/src/platform/sdl/sdl2_ui.h b/src/platform/sdl/sdl2_ui.h index 56849ee53..1acbfa4af 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 "multiplayer/webview.h" +#endif + extern "C" { union SDL_Event; struct SDL_Texture; @@ -79,6 +83,13 @@ class Sdl2Ui final : public BaseUi { AudioInterface& GetAudio() override; #endif + /** begin populating Input::text_input and Input::composition */ + void BeginTextCapture(Rect* textbox = nullptr) override; + void EndTextCapture() override; + void Dispatch(Intent) override; + webview::webview& GetWebview(); + void SetWebviewLayout(WebviewLayout) override; + /** @} */ private: @@ -110,6 +121,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); /** @} */ @@ -131,6 +143,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; @@ -149,6 +162,16 @@ class Sdl2Ui final : public BaseUi { #ifdef SUPPORT_AUDIO std::unique_ptr audio_; #endif + + std::unique_ptr webview; + std::thread webview_thread; + bool webview_visible = true; + bool webview_detach = false; + Rect webview_dims{}; + void ModifyViewport(); + void LayoutWebview(); }; +inline webview::webview& Sdl2Ui::GetWebview() { return *webview.get(); } + #endif diff --git a/src/platform/ynoshell/main.cpp b/src/platform/ynoshell/main.cpp new file mode 100644 index 000000000..5494adeaf --- /dev/null +++ b/src/platform/ynoshell/main.cpp @@ -0,0 +1,133 @@ +// 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 YnoFrame; + +class YnoApp : public wxApp +{ +public: + bool OnInit() override; + YnoFrame* frame; +}; + +//wxIMPLEMENT_APP(YnoApp); +wxIMPLEMENT_APP_NO_MAIN(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&) { +// +// } +//}; + +using YnoGameContainer = wxWindow; + +class YnoFrame : public wxFrame +{ +public: + YnoFrame(); + + YnoGameContainer* gameWindow; + //wxTextCtrl* chatInput; + +private: + void OnKeyDownFrame(wxKeyEvent& event); + void OnFocus(wxActivateEvent& event); +}; + + +bool YnoApp::OnInit() +{ + frame = new YnoFrame(); + frame->Show(true); + + YnoGameContainer* child = frame->gameWindow; + if (!child) { + Output::Error("no child frame"); + return false; + } + + Player::did_parse_config = [handle=child->GetHWND()](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(); + + exit(Player::exit_code); +} + +YnoFrame::YnoFrame() + : wxFrame(nullptr, wxID_ANY, "YNOproject") +{ + wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL); + + 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); + + GMI().on_chat_msg = [](Game_Multiplayer::ChatMsg msg) { + 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: + if (event.AltDown()) + ShowFullScreen(!IsFullScreen(), wxFULLSCREEN_ALL); + return; + } + 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 6c63ec87d..7a45073f7 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -31,6 +31,13 @@ # include #endif +#ifdef PLAYER_YNO +# include +# include "multiplayer/chat_overlay.h" +# include "multiplayer/status_overlay.h" +# include "web_api.h" +#endif + #include "async_handler.h" #include "audio.h" #include "cache.h" @@ -135,9 +142,8 @@ namespace Player { int rng_seed = -1; Game_ConfigPlayer player_config; Game_ConfigGame game_config; -#ifdef __EMSCRIPTEN__ + std::function did_parse_config; std::string emscripten_game_name; -#endif Game_Clock::time_point last_auto_screenshot; } @@ -200,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); @@ -241,6 +250,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) { @@ -257,6 +268,10 @@ void Player::MainLoop() { Scene::instance->MainFunction(); Graphics::GetMessageOverlay().Update(); +#ifdef PLAYER_YNO + Graphics::GetChatOverlay().Update(); + Graphics::GetStatusOverlay().Update(); +#endif ++num_updates; } @@ -294,6 +309,7 @@ void Player::MainLoop() { iframe.End(); Game_Clock::SleepFor(next - now); } + } void Player::Pause() { @@ -317,6 +333,14 @@ void Player::UpdateInput() { if (Input::IsSystemTriggered(Input::SHOW_LOG)) { Output::ToggleLog(); } +#ifdef PLAYER_YNO + 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(); } @@ -393,8 +417,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() { @@ -683,17 +708,17 @@ 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(); } + if (Player::did_parse_config) + Player::did_parse_config(cfg); return cfg; } @@ -1149,6 +1174,11 @@ void Player::LoadFonts() { 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)); + } + auto name_text_2 = FileFinder::OpenFont("NameText2"); if (name_text_2) { Font::SetNameText(Font::CreateFtFont(std::move(name_text_2), 13, false, false), true); diff --git a/src/player.h b/src/player.h index 0e0680167..dd3ec7912 100644 --- a/src/player.h +++ b/src/player.h @@ -431,7 +431,8 @@ namespace Player { /** game specific configuration */ extern Game_ConfigGame game_config; -#ifdef __EMSCRIPTEN__ + extern std::function did_parse_config; + /** Name of game emscripten uses */ extern std::string emscripten_game_name; #endif @@ -562,7 +563,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.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/scene_logo.cpp b/src/scene_logo.cpp index 123cc1f06..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,20 +144,24 @@ bool Scene_Logo::DetectGame() { FileFinder::SetGameFilesystem(fs); } -#ifdef __EMSCRIPTEN__ - 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; } 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..8af94caa2 100644 --- a/src/scene_menu.cpp +++ b/src/scene_menu.cpp @@ -36,6 +36,12 @@ #include "bitmap.h" #include "feature.h" +#ifdef PLAYER_YNO +# include "multiplayer/scene_online.h" +# include "graphics.h" +# include "multiplayer/status_overlay.h" +#endif + constexpr int menu_command_width = 88; constexpr int gold_window_width = 88; constexpr int gold_window_height = 32; @@ -54,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() { @@ -89,6 +101,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 +130,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 +170,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 +270,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_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 2597aef33..fa79ae6f5 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)); @@ -249,6 +272,9 @@ void Scene_Settings::vUpdate() { help_window2->SetFont(nullptr); options_window->Pop(); + if (mode == Window_Settings::eOnlineAccount) { + SaveConfig(true); + } SetMode(options_window->GetMode()); if (mode == Window_Settings::eNone) { Scene::Pop(); @@ -276,6 +302,8 @@ void Scene_Settings::vUpdate() { case Window_Settings::eInputListButtonsGame: case Window_Settings::eInputListButtonsEngine: case Window_Settings::eInputListButtonsDeveloper: + case Window_Settings::eInputListButtonsOnline: + case Window_Settings::eOnlineAccount: UpdateOptions(); break; case Window_Settings::eEngineFont1: @@ -374,6 +402,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()) { @@ -406,6 +448,16 @@ 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); + string_window->SetSecret(option.secret); + options_window->SetActive(false); + } } else { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Game_System::SFX_Buzzer)); } @@ -666,6 +718,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 4fdc5d441..7096da70a 100644 --- a/src/text.cpp +++ b/src/text.cpp @@ -213,6 +213,7 @@ 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 += ch.offset.x + size.width; rect.height = std::max(rect.height, size.height); } @@ -232,6 +233,7 @@ 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 += ch.offset.x + size.width; rect.height = std::max(rect.height, size.height); } 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); } } 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 b116f8c1e..f8658f08f 100644 --- a/src/web_api.cpp +++ b/src/web_api.cpp @@ -1,10 +1,34 @@ #include "web_api.h" -#include "emscripten/emscripten.h" -#include "output.h" +#ifdef __EMSCRIPTEN__ +#include +#else +# if defined(PLAYER_YNO) +# include +# include + +# include "baseui.h" +# include "platform/sdl/sdl2_ui.h" +# include "output.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 +# define EM_ASM_INT(...) 0 +# define EM_ASM(...) +#endif using namespace Web_API; std::string Web_API::GetSocketURL() { +#ifdef PLAYER_YNO + 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; @@ -12,101 +36,199 @@ 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 "null"; + }); + 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); + }); + 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/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_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 { 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 8603b1d69..c992f9b57 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" @@ -39,6 +40,10 @@ # include "platform/emscripten/interface.h" #endif +#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) : @@ -61,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) { @@ -152,12 +162,16 @@ void Window_Settings::Refresh() { case eLicense: RefreshLicense(); break; + case eOnlineAccount: + RefreshOnline(); + break; case eInputButtonCategory: RefreshButtonCategory(); break; case eInputListButtonsGame: case eInputListButtonsEngine: case eInputListButtonsDeveloper: + case eInputListButtonsOnline: RefreshButtonList(); break; default: @@ -270,6 +284,22 @@ 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; + opt.secret = param.secret; + if (!param.IsLocked()) + opt.action = std::forward(action); + GetFrame().options.push_back(std::move(opt)); +} + void Window_Settings::RefreshVideo() { auto cfg = DisplayUi->GetConfig(); @@ -644,6 +674,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 +693,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, Input::CHAT_SCROLL_UP, Input::CHAT_SCROLL_DOWN, Input::TOGGLE_SIDEBAR }; } for (auto b: buttons) { @@ -745,3 +779,66 @@ 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(LockedMenuItem("Status", "", fmt::format("Logged in as {}", cfg.username.Get())), [] {}); + AddOption(MenuItem("Logout", "", ""), [this] { + Scene::PopUntil(Scene::SceneType::Map); + GMI().Logout(); + Refresh(); + }); + } + 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)) { + cfg.username.Set(value); + GMI().SetNickname(value); + } + }); + AddOption(cfg.password, [this, &cfg] { + cfg.password.Set(GetCurrentOption().value_text); + }); + 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] { + Scene::PopUntil(Scene::SceneType::Map); + GMI().Login(cfg.username.Get(), cfg.password.Get()); + cfg.password.Set(""); + Refresh(); + }); + GetFrame().options.back().color = Font::ColorCritical; + 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 Reconnect", "", ""), [] { + Scene::PopUntil(Scene::SceneType::Map); + GMI().sessionConn.Open(GMI().GetSessionEndpoint()); + }); +} diff --git a/src/window_settings.h b/src/window_settings.h index 21d2d3a42..48132b0fd 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, @@ -52,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 { @@ -73,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; @@ -139,6 +144,9 @@ class Window_Settings : public Window_Selectable { Action&& action ); + template + void AddOption(const StringConfigParam& p, Action&& action); + void RefreshInput(); void RefreshButtonCategory(); void RefreshButtonList(); @@ -149,6 +157,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..31140e0e2 --- /dev/null +++ b/src/window_stringinput.cpp @@ -0,0 +1,87 @@ +/* + * 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" +#include "baseui.h" +#include "cache.h" +#include "output.h" + +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)); + // Above the message window + SetZ(Priority_Window + 150); + opacity = 0; + active = false; + + Rect rect{ ix, iy + 2, width - 16, height - 16 }; + DisplayUi->BeginTextCapture(&rect); + + 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(); + 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)); +} + +void Window_StringInput::Update() { + Window_Selectable::Update(); + if (!active) return; + + bool dirty = false; + + //std::string_view 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.empty()) { + value.clear(); + dirty = true; + } + if (dirty) + Refresh(); +} diff --git a/src/window_stringinput.h b/src/window_stringinput.h new file mode 100644 index 000000000..0d69bae1a --- /dev/null +++ b/src/window_stringinput.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_WINDOW_STRINGINPUT_H +#define EP_WINDOW_STRINGINPUT_H + +#include "window_selectable.h" + +class Window_StringInput : public Window_Selectable { +public: + Window_StringInput(std::string_view 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 diff --git a/src/ynoicons.h b/src/ynoicons.h new file mode 100644 index 000000000..caa58900f --- /dev/null +++ b/src/ynoicons.h @@ -0,0 +1,52 @@ +#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, + 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;