diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5f3b1794d..a88b2f6d9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -89,7 +89,7 @@ jobs: uses: TheMrMilchmann/setup-msvc-dev@v3 with: arch: x64 - toolset: '14.41' + toolset: '14.42' - name: Setup MYSYS2 (Windows) if: matrix.config.os == 'windows' && ( matrix.config.environment == 'mingw' || matrix.config.environment == 'ucrt' ) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e8ce781d3..ad4814afe 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -52,7 +52,7 @@ jobs: tidy-checks: '' step-summary: true file-annotations: true - ignore: subprojects|build|android|assets|recordings|docs|toolchains|platforms|wrapper|src/libs/core/hash-library|tests|src/lobby/curl_client.* + ignore: subprojects|build|android|assets|recordings|docs|toolchains|platforms|wrapper|src/libs/core/hash-library|tests|src/helper/web_utils.*|src/lobby/web_client.*|src/lobby/curl_client.* - name: Fail CI run if linter checks failed if: steps.linter.outputs.checks-failed != 0 diff --git a/.github/workflows/web_build.yml b/.github/workflows/web_build.yml new file mode 100644 index 000000000..db6ea1b4d --- /dev/null +++ b/.github/workflows/web_build.yml @@ -0,0 +1,40 @@ +name: Web CI + +on: + release: + types: [published] + push: + branches: ['main'] + pull_request: + workflow_dispatch: + +jobs: + web-build: + name: Build for the web + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: '0' + + # NOTE: meson has no dependencies, so --break-system-packages doesn't really break anything! + - name: Setup Meson + run: | + pip install meson --break-system-packages + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install ninja-build pkg-config build-essential wabt -y --no-install-recommends + + - name: Build + run: | + bash ./platforms/build-web.sh + meson test -C build-web + + # TODO upload page to gh-pages! + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: oopetris-assets + path: build-web/src/executables/ diff --git a/.github/workflows/windows_installer.yml b/.github/workflows/windows_installer.yml index 5816b7a4b..68b5548e5 100644 --- a/.github/workflows/windows_installer.yml +++ b/.github/workflows/windows_installer.yml @@ -24,7 +24,7 @@ jobs: uses: TheMrMilchmann/setup-msvc-dev@v3 with: arch: x64 - toolset: '14.41' + toolset: '14.42' - name: Setup meson run: | diff --git a/assets/authors/mgerhold.jpg b/assets/authors/mgerhold.jpg deleted file mode 100644 index a2a1b0ab1..000000000 Binary files a/assets/authors/mgerhold.jpg and /dev/null differ diff --git a/assets/authors/mgerhold.png b/assets/authors/mgerhold.png new file mode 100644 index 000000000..2c7d4cd1e Binary files /dev/null and b/assets/authors/mgerhold.png differ diff --git a/platforms/build-android.sh b/platforms/build-android.sh index 6c80eee87..41719d90e 100755 --- a/platforms/build-android.sh +++ b/platforms/build-android.sh @@ -2,7 +2,9 @@ set -e -mkdir -p toolchains +if [ ! -d "toolchains" ]; then + mkdir -p toolchains +fi export NDK_VER_DOWNLOAD="r28-beta1" export NDK_VER_DESC="r28-beta1" @@ -291,6 +293,10 @@ for INDEX in "${ARCH_KEYS_INDEX[@]}"; do MESON_CPU_FAMILY="aarch64" fi + export COMPILE_FLAGS="'--sysroot=${SYS_ROOT:?}','-fPIE','-fPIC','--target=$ARM_COMPILER_TRIPLE','-DAUDIO_PREFER_MP3'" + + export LINK_FLAGS="'-fPIE','-L$SYS_ROOT/usr/lib'" + cat <"./platforms/crossbuild-android-$ARM_TARGET_ARCH.ini" [host_machine] system = 'android' @@ -318,10 +324,11 @@ llvm-config = '$LLVM_CONFIG' [built-in options] c_std = 'gnu11' cpp_std = 'c++23' -c_args = ['--sysroot=${SYS_ROOT:?}','-fPIE','-fPIC','--target=$ARM_COMPILER_TRIPLE','-DHAVE_USR_INCLUDE_MALLOC_H','-D_MALLOC_H','-D__BITNESS=$BITNESS'] -cpp_args = ['--sysroot=${SYS_ROOT:?}','-fPIE','-fPIC','--target=$ARM_COMPILER_TRIPLE','-D__BITNESS=$BITNESS'] -c_link_args = ['-fPIE','-L$SYS_ROOT/usr/lib'] -cpp_link_args = ['-fPIE','-L$SYS_ROOT/usr/lib'] +c_args = [$COMPILE_FLAGS] +cpp_args = [$COMPILE_FLAGS] +c_link_args = [$LINK_FLAGS] +cpp_link_args = [$LINK_FLAGS] + prefix = '$SYS_ROOT' libdir = '$LIB_PATH' @@ -331,12 +338,14 @@ sys_root = '${SYS_ROOT}' EOF - if [ ! -d "$PWD/subprojects/cpu-features" ]; then - mkdir -p "$PWD/subprojects/cpu-features/src/" - mkdir -p "$PWD/subprojects/cpu-features/include/" - ln -s "$BASE_PATH/sources/android/cpufeatures/cpu-features.c" "$PWD/subprojects/cpu-features/src/cpu-features.c" - ln -s "$BASE_PATH/sources/android/cpufeatures/cpu-features.h" "$PWD/subprojects/cpu-features/include/cpu-features.h" - cat <"$PWD/subprojects/cpu-features/meson.build" + CPU_FUTURES_ROOT="$PWD/subprojects/cpu-features" + + if [ ! -d "$CPU_FUTURES_ROOT" ]; then + mkdir -p "$CPU_FUTURES_ROOT/src/" + mkdir -p "$CPU_FUTURES_ROOT/include/" + ln -s "$BASE_PATH/sources/android/cpufeatures/cpu-features.c" "$CPU_FUTURES_ROOT/src/cpu-features.c" + ln -s "$BASE_PATH/sources/android/cpufeatures/cpu-features.h" "$CPU_FUTURES_ROOT/include/cpu-features.h" + cat <"$CPU_FUTURES_ROOT/meson.build" project('cpu-features','c') meson.override_dependency( @@ -367,7 +376,6 @@ EOF --cross-file "./platforms/crossbuild-android-$ARM_TARGET_ARCH.ini" \ "-Dbuildtype=$BUILDTYPE" \ -Dsdl2:use_hidapi=enabled \ - -Dcpp_args=-DAUDIO_PREFER_MP3 \ -Dclang_libcpp=disabled fi diff --git a/platforms/build-web.sh b/platforms/build-web.sh new file mode 100755 index 000000000..f2d099dc6 --- /dev/null +++ b/platforms/build-web.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash + +set -e + +if [ ! -d "toolchains" ]; then + mkdir -p toolchains +fi + +export EMSCRIPTEN_ROOT="$(pwd)/toolchains/emsdk" + +if [ ! -d "$EMSCRIPTEN_ROOT" ]; then + git clone https://github.com/emscripten-core/emsdk.git "$EMSCRIPTEN_ROOT" +fi + +"$EMSCRIPTEN_ROOT/emsdk" install latest +"$EMSCRIPTEN_ROOT/emsdk" activate latest + +EMSCRIPTEN_UPSTREAM_ROOT="$EMSCRIPTEN_ROOT/upstream/emscripten" + +EMSCRIPTEN_PACTH_FILE="$EMSCRIPTEN_UPSTREAM_ROOT/.patched_manually.meta" + +PATCH_DIR="platforms/emscripten" + +if ! [ -e "$EMSCRIPTEN_PACTH_FILE" ]; then + ##TODO: upstream those patches + # see: https://github.com/emscripten-core/emscripten/pull/18379/commits + # and: https://github.com/emscripten-core/emscripten/pull/18379 + + git apply --unsafe-paths -p1 --directory="$EMSCRIPTEN_UPSTREAM_ROOT" "$PATCH_DIR/sdl2_image_port.diff" + git apply --unsafe-paths -p1 --directory="$EMSCRIPTEN_UPSTREAM_ROOT" "$PATCH_DIR/sdl2_mixer_port.diff" + git apply --unsafe-paths -p1 --directory="$EMSCRIPTEN_UPSTREAM_ROOT" "$PATCH_DIR/default_settings.diff" + + touch "$EMSCRIPTEN_PACTH_FILE" +fi + +# git apply path + +# shellcheck disable=SC1091 +EMSDK_QUIET=1 source "$EMSCRIPTEN_ROOT/emsdk_env.sh" >/dev/null + +## build theneeded dependencies +embuilder build sdl2-mt harfbuzz-mt freetype zlib sdl2_ttf mpg123 "sdl2_mixer:formats=mp3" libpng-mt "sdl2_image:formats=png,svg:mt=1" icu-mt + +export EMSCRIPTEN_SYS_ROOT="$EMSCRIPTEN_UPSTREAM_ROOT/cache/sysroot" + +export BUILD_DIR="build-web" + +export CC="emcc" +export CXX="em++" +export AR="emar" +export RANLIB="emranlib" +export STRIP="emstrip" +export NM="emnm" + +export ARCH="wasm32" +export CPU_ARCH="wasm32" +export ENDIANESS="little" + +export ROMFS="platforms/romfs" + +export PACKAGE_FLAGS="'--use-port=sdl2', '--use-port=harfbuzz', '--use-port=freetype', '--use-port=zlib', '--use-port=sdl2_ttf', '--use-port=mpg123', '--use-port=sdl2_mixer', '-sSDL2_MIXER_FORMATS=[\"mp3\"]','--use-port=libpng', '--use-port=sdl2_image','-sSDL2_IMAGE_FORMATS=[\"png\",\"svg\"]', '--use-port=icu'" + +export COMMON_FLAGS="'-fexceptions', '-pthread', '-sUSE_PTHREADS=1', '-sEXCEPTION_CATCHING_ALLOWED=[..]', $PACKAGE_FLAGS" + +# TODO see if ALLOW_MEMORY_GROWTH is needed, but if we load ttf's and music it likely is and we don't have to debug OOm crashes, that aren't handled by some third party library, which is painful +export LINK_FLAGS="$COMMON_FLAGS, '-sEXPORT_ALL=1', '-sUSE_WEBGPU=1', '-sWASM=1', '-sALLOW_MEMORY_GROWTH=1', '-sASSERTIONS=1','-sERROR_ON_UNDEFINED_SYMBOLS=1', '-sFETCH=1', '-sEXIT_RUNTIME=1'" +export COMPILE_FLAGS="$COMMON_FLAGS ,'-DAUDIO_PREFER_MP3'" + +export CROSS_FILE="./platforms/crossbuild-web.ini" + +cat <"$CROSS_FILE" +[host_machine] +system = 'emscripten' +cpu_family = '$ARCH' +cpu = '$CPU_ARCH' +endian = '$ENDIANESS' + +[target_machine] +system = 'emscripten' +cpu_family = '$ARCH' +cpu = '$CPU_ARCH' +endian = '$ENDIANESS' + +[constants] +emscripten_root = '$(pwd)/emsdk' + +[binaries] +c = '$CC' +cpp = '$CXX' +ar = '$AR' +ranlib = '$RANLIB' +strip = '$STRIP' +nm = '$NM' + +pkg-config = ['emmake', 'pkg-config'] +cmake = ['emmake', 'cmake'] +sdl2-config = ['emconfigure', 'sdl2-config'] + +exe_wrapper = '$EMSDK_NODE' + +[built-in options] +c_std = 'c11' +cpp_std = 'c++23' +c_args = [$COMPILE_FLAGS] +cpp_args = [$COMPILE_FLAGS] +c_link_args = [$LINK_FLAGS] +cpp_link_args = [$LINK_FLAGS] + +[properties] +needs_exe_wrapper = true +sys_root = '$EMSCRIPTEN_SYS_ROOT' + +APP_ROMFS='$ROMFS/assets/' + +EOF + +## options: "smart, complete_rebuild" +export COMPILE_TYPE="smart" + +export BUILDTYPE="debug" + +if [ "$#" -eq 0 ]; then + # nothing + echo "Using compile type '$COMPILE_TYPE'" +elif [ "$#" -eq 1 ]; then + COMPILE_TYPE="$1" +elif [ "$#" -eq 2 ]; then + COMPILE_TYPE="$1" + BUILDTYPE="$2" +else + echo "Too many arguments given, expected 1 or 2" + exit 1 +fi + +if [ "$COMPILE_TYPE" == "smart" ]; then + : # noop +elif [ "$COMPILE_TYPE" == "complete_rebuild" ]; then + : # noop +else + echo "Invalid COMPILE_TYPE, expected: 'smart' or 'complete_rebuild'" + exit 1 +fi + +if [ ! -d "$ROMFS" ]; then + + mkdir -p "$ROMFS" + + cp -r assets "$ROMFS/" + +fi + +if [ "$COMPILE_TYPE" == "complete_rebuild" ] || [ ! -e "$BUILD_DIR" ]; then + + meson setup "$BUILD_DIR" \ + "--wipe" \ + --cross-file "$CROSS_FILE" \ + "-Dbuildtype=$BUILDTYPE" \ + -Ddefault_library=static \ + -Dtests=false + +fi + +meson compile -C "$BUILD_DIR" diff --git a/platforms/emscripten/default_settings.diff b/platforms/emscripten/default_settings.diff new file mode 100644 index 000000000..5a16b54b3 --- /dev/null +++ b/platforms/emscripten/default_settings.diff @@ -0,0 +1,13 @@ +diff --git a/src/settings.js b/src/settings.js +index 981c44fa..17ca0078 100644 +--- a/src/settings.js ++++ b/src/settings.js +@@ -1624,7 +1624,7 @@ var SDL2_IMAGE_FORMATS = []; + + // Formats to support in SDL2_mixer. Valid values: ogg, mp3, mod, mid + // [link] +-var SDL2_MIXER_FORMATS = ["ogg"]; ++var SDL2_MIXER_FORMATS = []; + + // 1 = use sqlite3 from emscripten-ports + // Alternate syntax: --use-port=sqlite3 diff --git a/platforms/emscripten/sdl2_image_port.diff b/platforms/emscripten/sdl2_image_port.diff new file mode 100644 index 000000000..402868e7a --- /dev/null +++ b/platforms/emscripten/sdl2_image_port.diff @@ -0,0 +1,57 @@ +diff --git a/tools/ports/sdl2_image.py b/tools/ports/sdl2_image.py +index c72ef576..0c12feba 100644 +--- a/tools/ports/sdl2_image.py ++++ b/tools/ports/sdl2_image.py +@@ -16,15 +16,17 @@ variants = { + } + + OPTIONS = { +- 'formats': 'A comma separated list of formats (ex: --use-port=sdl2_image:formats=png,jpg)' ++ 'formats': 'A comma separated list of formats (ex: --use-port=sdl2_image:formats=png,jpg)', ++ 'mt': 'use pthread' + } + + SUPPORTED_FORMATS = {'avif', 'bmp', 'gif', 'jpg', 'jxl', 'lbm', 'pcx', 'png', + 'pnm', 'qoi', 'svg', 'tga', 'tif', 'webp', 'xcf', 'xpm', 'xv'} + + # user options (from --use-port) +-opts: Dict[str, Set] = { +- 'formats': set() ++opts = { ++ 'formats': set(), ++ 'mt': 0 + } + + +@@ -42,7 +44,7 @@ def get_lib_name(settings): + libname = 'libSDL2_image' + if formats != '': + libname += '_' + formats +- return libname + '.a' ++ return libname + ('-mt' if opts['mt'] else '') + '.a' + + + def get(ports, settings, shared): +@@ -70,6 +72,8 @@ def get(ports, settings, shared): + + if 'jpg' in formats: + defs += ['-sUSE_LIBJPEG'] ++ if opts['mt']: ++ defs += ['-pthread'] + + ports.build_port(src_dir, final, 'sdl2_image', flags=defs, srcs=srcs) + +@@ -99,7 +103,12 @@ def handle_options(options, error_handler): + error_handler(f'{format} is not a supported format') + else: + opts['formats'].add(format) +- ++ ++ mt = options['mt'] ++ if mt not in ["1","0"]: ++ error_handler(f'{mt} has to be either 0 or 1') ++ else: ++ opts['mt'] = int(mt) + + def show(): + return 'sdl2_image (-sUSE_SDL_IMAGE=2 or --use-port=sdl2_image; zlib license)' diff --git a/platforms/emscripten/sdl2_mixer_port.diff b/platforms/emscripten/sdl2_mixer_port.diff new file mode 100644 index 000000000..f2d7efd01 --- /dev/null +++ b/platforms/emscripten/sdl2_mixer_port.diff @@ -0,0 +1,100 @@ +diff --git a/tools/ports/sdl2_mixer.py b/tools/ports/sdl2_mixer.py +index f77f906d..417b2a79 100644 +--- a/tools/ports/sdl2_mixer.py ++++ b/tools/ports/sdl2_mixer.py +@@ -14,14 +14,27 @@ variants = { + 'sdl2_mixer_none': {'SDL2_MIXER_FORMATS': []}, + } + ++OPTIONS = { ++ 'formats': 'A comma separated list of formats (ex: --use-port=sdl2_mixer:formats=mp3)' ++} ++ ++SUPPORTED_FORMATS = {'mp3', 'ogg', 'mod', 'mid'} ++ ++# user options (from --use-port) ++opts = { ++ 'formats': set(), ++} + + def needed(settings): + return settings.USE_SDL_MIXER == 2 + ++def get_formats(settings): ++ return set(settings.SDL2_MIXER_FORMATS).union(opts['formats']) ++ ++ + + def get_lib_name(settings): +- settings.SDL2_MIXER_FORMATS.sort() +- formats = '-'.join(settings.SDL2_MIXER_FORMATS) ++ formats = '-'.join(sorted(get_formats(settings))) + + libname = 'libSDL2_mixer' + if formats != '': +@@ -44,26 +57,28 @@ def get(ports, settings, shared): + '-O2', + '-DMUSIC_WAV', + ] ++ ++ formats = get_formats(settings) + +- if "ogg" in settings.SDL2_MIXER_FORMATS: ++ if "ogg" in formats: + flags += [ + '-sUSE_VORBIS', + '-DMUSIC_OGG', + ] + +- if "mp3" in settings.SDL2_MIXER_FORMATS: ++ if "mp3" in formats: + flags += [ + '-sUSE_MPG123', + '-DMUSIC_MP3_MPG123', + ] + +- if "mod" in settings.SDL2_MIXER_FORMATS: ++ if "mod" in formats: + flags += [ + '-sUSE_MODPLUG', + '-DMUSIC_MOD_MODPLUG', + ] + +- if "mid" in settings.SDL2_MIXER_FORMATS: ++ if "mid" in formats: + flags += [ + '-DMUSIC_MID_TIMIDITY', + ] +@@ -104,16 +119,29 @@ def clear(ports, settings, shared): + + def process_dependencies(settings): + settings.USE_SDL = 2 +- if "ogg" in settings.SDL2_MIXER_FORMATS: ++ ++ formats = get_formats(settings) ++ ++ if "ogg" in formats: + deps.append('vorbis') + settings.USE_VORBIS = 1 +- if "mp3" in settings.SDL2_MIXER_FORMATS: ++ if "mp3" in formats: + deps.append('mpg123') + settings.USE_MPG123 = 1 +- if "mod" in settings.SDL2_MIXER_FORMATS: ++ if "mod" in formats: + deps.append('libmodplug') + settings.USE_MODPLUG = 1 + ++def handle_options(options, error_handler): ++ formats = options['formats'].split(',') ++ for format in formats: ++ format = format.lower().strip() ++ if format not in SUPPORTED_FORMATS: ++ error_handler(f'{format} is not a supported format') ++ else: ++ opts['formats'].add(format) ++ ++ + + def show(): + return 'sdl2_mixer (-sUSE_SDL_MIXER=2 or --use-port=sdl2_mixer; zlib license)' diff --git a/platforms/emscripten/server.py b/platforms/emscripten/server.py new file mode 100644 index 000000000..c6dc7351c --- /dev/null +++ b/platforms/emscripten/server.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer, test +import os + + +class CORSRequestHandler(SimpleHTTPRequestHandler): + def end_headers(self): + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Cross-Origin-Embedder-Policy", "require-corp") + self.send_header("Cross-Origin-Opener-Policy", "same-origin") + + SimpleHTTPRequestHandler.end_headers(self) + + +# from: https://github.com/python/cpython/blob/3.13/Lib/http/server.py +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument( + "-b", + "--bind", + metavar="ADDRESS", + help="bind to this address " "(default: all interfaces)", + ) + parser.add_argument( + "-d", + "--directory", + default=os.getcwd(), + help="serve this directory " "(default: current directory)", + ) + parser.add_argument( + "port", + default=8000, + type=int, + nargs="?", + help="bind to this port " "(default: %(default)s)", + ) + args = parser.parse_args() + + # ensure dual-stack is not disabled; ref #38907 + class DualStackServer(ThreadingHTTPServer): + def finish_request(self, request, client_address): + self.RequestHandlerClass( + request, client_address, self, directory=args.directory + ) + + test( + HandlerClass=CORSRequestHandler, + ServerClass=DualStackServer, + port=args.port, + bind=args.bind, + ) diff --git a/platforms/emscripten/shell_file.html b/platforms/emscripten/shell_file.html new file mode 100644 index 000000000..a081c89e5 --- /dev/null +++ b/platforms/emscripten/shell_file.html @@ -0,0 +1,210 @@ + + + + + + Emscripten-Generated Code + + + +
+
+
+
+ emscripten +
+
+
Downloading...
+
+ +
+
+ +
+
+
+ Resize canvas + Lock/hide mouse + pointer     + +
+ +
+ +
+ + {{{ SCRIPT }}} + + diff --git a/src/discord/core.hpp b/src/discord/core.hpp index 2e00b567b..9d41062c8 100644 --- a/src/discord/core.hpp +++ b/src/discord/core.hpp @@ -12,6 +12,9 @@ #ifndef NOMINMAX #define NOMINMAX #endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif #endif #include diff --git a/src/executables/game/application.cpp b/src/executables/game/application.cpp index 1c3a29e1b..605b6a5eb 100644 --- a/src/executables/game/application.cpp +++ b/src/executables/game/application.cpp @@ -7,13 +7,11 @@ #include "helper/message_box.hpp" #include "input/input.hpp" #include "manager/music_manager.hpp" -#include "scenes/loading_screen/loading_screen.hpp" #include "scenes/scene.hpp" #include "ui/layout.hpp" -#include + #include -#include #include #include #include @@ -26,6 +24,11 @@ #include "graphics/text.hpp" #endif + +#if defined(__EMSCRIPTEN__) +#include +#endif + namespace { [[nodiscard]] helper::MessageBox::Type get_notification_level(helper::error::Severity severity) { @@ -37,12 +40,73 @@ namespace { } // namespace +#if !defined(NDEBUG) +helper::DebugInfo::DebugInfo(Uint64 start_time, u64 frame_counter, Uint64 update_time, double count_per_s) + : m_start_time{ start_time }, + m_frame_counter{ frame_counter }, + m_update_time{ update_time }, + m_count_per_s{ count_per_s } { } + +[[nodiscard]] Uint64 helper::DebugInfo::update_time() const { + return m_update_time; +} + +[[nodiscard]] double helper::DebugInfo::count_per_s() const { + return m_count_per_s; +} +#endif + +helper::TimeInfo::TimeInfo( + std::chrono::nanoseconds sleep_time, + std::chrono::steady_clock::time_point start_execution_time +) + : m_sleep_time{ sleep_time }, + m_start_execution_time{ start_execution_time } { } + +[[nodiscard]] std::chrono::nanoseconds helper::TimeInfo::sleep_time() const { + return m_sleep_time; +} + +helper::LoadingInfo::LoadingInfo( + std::chrono::nanoseconds sleep_time, + Uint64 start_time, + std::future&& load_everything_thread, + std::chrono::steady_clock::time_point start_execution_time, + bool finished_loading, + scenes::LoadingScreen&& loading_screen +) + : m_sleep_time{ sleep_time }, + m_start_time{ start_time }, + m_load_everything_thread{ std::move(load_everything_thread) }, + m_start_execution_time{ start_execution_time }, + m_finished_loading{ finished_loading }, + m_loading_screen{ std::move(loading_screen) } { } + +[[nodiscard]] std::chrono::nanoseconds helper::LoadingInfo::sleep_time() const { + return m_sleep_time; +} + +[[nodiscard]] Uint64 helper::LoadingInfo::start_time() const { + return m_start_time; +} + + +[[nodiscard]] const std::future& helper::LoadingInfo::load_everything_thread() const { + return m_load_everything_thread; +} + Application::Application(std::shared_ptr&& window, CommandLineArguments&& arguments) try : m_command_line_arguments{ std::move(arguments) }, m_window{ std::move(window) }, m_renderer{ *m_window, m_command_line_arguments.target_fps.has_value() ? Renderer::VSync::Disabled : Renderer::VSync::Enabled }, - m_target_framerate{ m_command_line_arguments.target_fps } { + m_target_framerate{ m_command_line_arguments.target_fps } + +#if defined(__EMSCRIPTEN__) + , + m_web_context{ this } +#endif +{ initialize(); } catch (const helper::GeneralError& general_error) { const auto severity = general_error.severity(); @@ -56,15 +120,72 @@ Application::Application(std::shared_ptr&& window, CommandLineArguments& } } +Application::~Application() = default; + +#if defined(__EMSCRIPTEN__) +void c_loop_entry(void* arg) { + auto application = reinterpret_cast(arg); + application->emscripten_do_process(); + application->loop_entry_emscripten(); +} + +void Application::load_emscripten() { + + if ((not m_loading_info->m_finished_loading) and m_is_running) { + load_loop(); + return; + } + + const auto duration = std::chrono::milliseconds(SDL_GetTicks64() - m_loading_info->start_time()); + + // we can reach this via SDL_QUIT or SDL_APP_TERMINATING + if (not m_loading_info->m_finished_loading or not m_is_running) { + + spdlog::debug("Aborted loading after {}", duration); + + // do some combination of the loading exit in a normal OS case and the emscripten normal game loop exit + this->~Application(); + emscripten_cancel_main_loop(); + utils::exit(0); + } + + + spdlog::debug("Took {} to load", duration); + + push_scene(scenes::create_scene(*this, SceneId::MainMenu, ui::FullScreenLayout{ *m_window })); + + // run this manually, in a normal case, this would be run after the loader has finished + this->run(); +} + +void Application::main_loop_emscripten() { + if (not this->m_is_running) { + // call the destructor manually, so that everything gets cleaned up + this->~Application(); + emscripten_cancel_main_loop(); + return; + } + + main_loop(); +} +void Application::emscripten_do_process() { + m_web_context.do_processing(); +} + +#endif void Application::run() { m_event_dispatcher.register_listener(this); #if !defined(NDEBUG) auto start_time = SDL_GetPerformanceCounter(); + const auto update_time = SDL_GetPerformanceFrequency() / 2; //0.5 s + const auto count_per_s = static_cast(SDL_GetPerformanceFrequency()); + u64 frame_counter = 0; + m_debug = std::make_unique(start_time, frame_counter, update_time, count_per_s); #endif using namespace std::chrono_literals; @@ -73,6 +194,13 @@ void Application::run() { : 0s; auto start_execution_time = std::chrono::steady_clock::now(); + m_time_info = std::make_unique(sleep_time, start_execution_time); + +#if defined(__EMSCRIPTEN__) + m_current_emscripten_func = std::bind(&Application::main_loop_emscripten, this); + return; +#else + while (m_is_running #if defined(__CONSOLE__) @@ -80,40 +208,102 @@ void Application::run() { #endif ) { + main_loop(); + } +#endif +} + +void Application::main_loop() { - m_event_dispatcher.dispatch_pending_events(); - update(); - render(); - m_renderer.present(); + m_event_dispatcher.dispatch_pending_events(); + update(); + render(); + m_renderer.present(); #if !defined(NDEBUG) - ++frame_counter; + m_debug->m_frame_counter++; - const Uint64 current_time = SDL_GetPerformanceCounter(); + const Uint64 current_time = SDL_GetPerformanceCounter(); - if (current_time - start_time >= update_time) { - const double elapsed = static_cast(current_time - start_time) / count_per_s; - m_fps_text->set_text(*this, fmt::format("FPS: {:.2f}", static_cast(frame_counter) / elapsed)); - start_time = current_time; - frame_counter = 0; - } + if (current_time - m_debug->m_start_time >= m_debug->update_time()) { + const double elapsed = static_cast(current_time - m_debug->m_start_time) / m_debug->count_per_s(); + + m_fps_text->set_text( + *this, fmt::format("FPS: {:.2f}", static_cast(m_debug->m_frame_counter) / elapsed) + ); + + m_debug->m_start_time = current_time; + m_debug->m_frame_counter = 0; + } #endif - if (m_target_framerate.has_value()) { + if (m_target_framerate.has_value()) { - const auto now = std::chrono::steady_clock::now(); - const auto runtime = (now - start_execution_time); - if (runtime < sleep_time) { - //TODO(totto): use SDL_DelayNS in sdl >= 3.0 - helper::sleep_nanoseconds(sleep_time - runtime); - start_execution_time = std::chrono::steady_clock::now(); - } else { - start_execution_time = now; - } + const auto now = std::chrono::steady_clock::now(); + const auto runtime = (now - m_time_info->m_start_execution_time); + + const auto sleep_time = m_time_info->sleep_time(); + + if (runtime < sleep_time) { + //TODO(totto): use SDL_DelayNS in sdl >= 3.0 + helper::sleep_nanoseconds(sleep_time - runtime); + m_time_info->m_start_execution_time = std::chrono::steady_clock::now(); + } else { + m_time_info->m_start_execution_time = now; + } + } +} + +void Application::load_loop() { + + // we can't use the normal event loop, so we have to do it manually + SDL_Event event; + while (SDL_PollEvent(&event) != 0) { + if (event.type == SDL_QUIT) { + m_is_running = false; + } + + // special event for android and IOS + if (event.type == SDL_APP_TERMINATING) { + m_is_running = false; + } + } + + if (not m_is_running) { + return; + } + + m_loading_info->m_loading_screen.update(); + // this service_provider only guarantees the renderer + the window to be accessible without race conditions + m_loading_info->m_loading_screen.render(*this); + + // present and wait (depending if vsync is on or not, this has to be done manually) + m_renderer.present(); + + if (m_target_framerate.has_value()) { + + const auto now = std::chrono::steady_clock::now(); + const auto runtime = (now - m_loading_info->m_start_execution_time); + + const auto sleep_time = m_loading_info->sleep_time(); + + if (runtime < sleep_time) { + //TODO(totto): use SDL_DelayNS in sdl >= 3.0 + helper::sleep_nanoseconds(sleep_time - runtime); + m_loading_info->m_start_execution_time = std::chrono::steady_clock::now(); + } else { + m_loading_info->m_start_execution_time = now; } } + // end waiting + + // wait until is faster, since it just compares two time_points instead of getting now() and than adding the wait-for argument + m_loading_info->m_finished_loading = + m_loading_info->load_everything_thread().wait_until(std::chrono::system_clock::time_point::min()) + == std::future_status::ready; } + void Application::handle_event(const SDL_Event& event) { if (event.type == SDL_QUIT) { m_is_running = false; @@ -257,14 +447,22 @@ void Application::render() const { #endif } +#if defined(__EMSCRIPTEN__) +void Application::loop_entry_emscripten() { + this->m_current_emscripten_func(); +} + +#endif + + void Application::initialize() { - auto loading_screen = scenes::LoadingScreen{ this }; + auto loading_screen_arg = scenes::LoadingScreen{ this }; const auto start_time = SDL_GetTicks64(); - const std::future load_everything = std::async(std::launch::async, [this] { - this->m_settings_manager = std::make_unique(); + std::future load_everything_thread = std::async(std::launch::async, [this] { + this->m_settings_manager = std::make_unique(this); this->m_settings_manager->add_callback([this](const auto& settings) { this->reload_api(settings); }); @@ -282,6 +480,7 @@ void Application::initialize() { this->load_resources(); #if !defined(NDEBUG) + //TODO(Totto): emscripten: this is using sdl rendering (to a texture) in another thread then the main thread, use proxying to the main thread here too, and disable OOPETRIS_DONT_USE_PRERENDERED_TEXT m_fps_text = std::make_unique( this, "FPS: ?", font_manager().get(FontId::Default), Color::white(), std::pair{ 0.95, 0.95 }, @@ -312,9 +511,12 @@ void Application::initialize() { const auto sleep_time = m_target_framerate.has_value() ? std::chrono::duration_cast(1s) / m_target_framerate.value() : 0s; - auto start_execution_time = std::chrono::steady_clock::now(); + auto start_execution_time_arg = std::chrono::steady_clock::now(); - bool finished_loading = false; + m_loading_info = std::make_unique( + sleep_time, start_time, std::move(load_everything_thread), start_execution_time_arg, false, + std::move(loading_screen_arg) + ); // this is a duplicate of below in some cases, but it's just for the loading screen and can't be factored out easily // this also only uses a subset of all things, the real event loop uses, so that nothing breaks while doing multithreading @@ -324,60 +526,32 @@ void Application::initialize() { // - m_renderer // - m_target_framerate - while ((not finished_loading) and m_is_running +#if defined(__EMSCRIPTEN__) + m_current_emscripten_func = std::bind(&Application::load_emscripten, this); + int selected_fps = m_target_framerate.has_value() ? m_target_framerate.value() : -1; + + // NOTE: this is complicated, especially in C++ + // see: https://wiki.libsdl.org/SDL2/README/emscripten#porting-your-app-to-emscripten + // and: https://emscripten.org/docs/api_reference/emscripten.h.html#c.emscripten_set_main_loop_arg + // for a basic understanding + // this sets up a loop,, throws an exception(a special kind, not c++ one) to exit this function, but nothing gets cleaned up (no destructors get called, this function NEVER returns) + // but after emscripten_cancel_main_loop we have to manually call the destructor, to clean up, + emscripten_set_main_loop_arg(c_loop_entry, this, selected_fps, true); + UNREACHABLE(); +#else + while ((not m_loading_info->m_finished_loading) and m_is_running #if defined(__CONSOLE__) and console::inMainLoop() #endif ) { - - // we can't use the normal event loop, so we have to do it manually - SDL_Event event; - while (SDL_PollEvent(&event) != 0) { - if (event.type == SDL_QUIT) { - m_is_running = false; - } - - // special event for android and IOS - if (event.type == SDL_APP_TERMINATING) { - m_is_running = false; - } - } - - if (not m_is_running) { - break; - } - - loading_screen.update(); - // this service_provider only guarantees the renderer + the window to be accessible without race conditions - loading_screen.render(*this); - - // present and wait (depending if vsync is on or not, this has to be done manually) - m_renderer.present(); - - if (m_target_framerate.has_value()) { - - const auto now = std::chrono::steady_clock::now(); - const auto runtime = (now - start_execution_time); - if (runtime < sleep_time) { - //TODO(totto): use SDL_DelayNS in sdl >= 3.0 - helper::sleep_nanoseconds(sleep_time - runtime); - start_execution_time = std::chrono::steady_clock::now(); - } else { - start_execution_time = now; - } - } - // end waiting - - // wait until is faster, since it just compares two time_points instead of getting now() and than adding the wait-for argument - finished_loading = - load_everything.wait_until(std::chrono::system_clock::time_point::min()) == std::future_status::ready; + load_loop(); } const auto duration = std::chrono::milliseconds(SDL_GetTicks64() - start_time); // we can reach this via SDL_QUIT, SDL_APP_TERMINATING or (not console::inMainLoop()) - if (not finished_loading or not m_is_running) { + if (not m_loading_info->m_finished_loading or not m_is_running) { spdlog::debug("Aborted loading after {}", duration); @@ -390,6 +564,7 @@ void Application::initialize() { spdlog::debug("Took {} to load", duration); push_scene(scenes::create_scene(*this, SceneId::MainMenu, ui::FullScreenLayout{ *m_window })); +#endif } void Application::load_resources() { @@ -405,6 +580,7 @@ void Application::load_resources() { { FontId::NotoColorEmoji, "NotoColorEmoji.ttf" }, { FontId::Symbola, "Symbola.ttf" } }; + for (const auto& [font_id, path] : fonts) { const auto font_path = utils::get_assets_folder() / "fonts" / path; m_font_manager->load(font_id, font_path, fonts_size); @@ -423,11 +599,23 @@ void Application::load_resources() { #endif +#if defined(__EMSCRIPTEN__) + +[[nodiscard]] web::WebContext& Application::web_context() { + return m_web_context; +} + +[[nodiscard]] const web::WebContext& Application::web_context() const { + return m_web_context; +} + +#endif + void Application::reload_api(const settings::Settings& settings) { if (auto api_url = settings.api_url; api_url.has_value()) { - auto maybe_api = lobby::API::get_api(api_url.value()); + auto maybe_api = lobby::API::get_api(this, api_url.value()); if (maybe_api.has_value()) { //TODO(Totto): do this somehow asynchronous m_api = std::make_unique(std::move(maybe_api.value())); @@ -438,3 +626,73 @@ void Application::reload_api(const settings::Settings& settings) { spdlog::info("No lobby API provided"); } } + + +void Application::push_scene(std::unique_ptr scene) { + m_scene_stack.push_back(std::move(scene)); +} + +// implementation of ServiceProvider +[[nodiscard]] EventDispatcher& Application::event_dispatcher() { + return m_event_dispatcher; +} + +[[nodiscard]] const EventDispatcher& Application::event_dispatcher() const { + return m_event_dispatcher; +} + +FontManager& Application::font_manager() { + return *m_font_manager; +} + +[[nodiscard]] const FontManager& Application::font_manager() const { + return *m_font_manager; +} + +CommandLineArguments& Application::command_line_arguments() { + return m_command_line_arguments; +} + +[[nodiscard]] const CommandLineArguments& Application::command_line_arguments() const { + return m_command_line_arguments; +} + +SettingsManager& Application::settings_manager() { + return *m_settings_manager; +} + +[[nodiscard]] const SettingsManager& Application::settings_manager() const { + return *m_settings_manager; +} + +MusicManager& Application::music_manager() { + return *m_music_manager; +} + +[[nodiscard]] const MusicManager& Application::music_manager() const { + return *m_music_manager; +} + +[[nodiscard]] const Renderer& Application::renderer() const { + return m_renderer; +} + +[[nodiscard]] const Window& Application::window() const { + return *m_window; +} + +[[nodiscard]] Window& Application::window() { + return *m_window; +} + +[[nodiscard]] input::InputManager& Application::input_manager() { + return *m_input_manager; +} + +[[nodiscard]] const input::InputManager& Application::input_manager() const { + return *m_input_manager; +} + +[[nodiscard]] const std::unique_ptr& Application::api() const { + return m_api; +} diff --git a/src/executables/game/application.hpp b/src/executables/game/application.hpp index b4fbaab93..6eb3a39da 100644 --- a/src/executables/game/application.hpp +++ b/src/executables/game/application.hpp @@ -12,12 +12,80 @@ #include "manager/resource_manager.hpp" #include "manager/service_provider.hpp" #include "manager/settings_manager.hpp" +#include "scenes/loading_screen/loading_screen.hpp" #include "scenes/scene.hpp" #include "ui/components/label.hpp" +#include +#include #include #include + +#if defined(__EMSCRIPTEN__) +#include "helper/web_utils.hpp" +#endif + +namespace helper { +#if !defined(NDEBUG) + struct DebugInfo { + Uint64 m_start_time; + u64 m_frame_counter; + + private: + Uint64 m_update_time; + double m_count_per_s; + + public: + DebugInfo(Uint64 start_time, u64 frame_counter, Uint64 update_time, double count_per_s); + + [[nodiscard]] Uint64 update_time() const; + [[nodiscard]] double count_per_s() const; + }; + +#endif + struct TimeInfo { + private: + std::chrono::nanoseconds m_sleep_time; + + public: + std::chrono::steady_clock::time_point m_start_execution_time; + + TimeInfo(std::chrono::nanoseconds sleep_time, std::chrono::steady_clock::time_point start_execution_time); + + [[nodiscard]] std::chrono::nanoseconds sleep_time() const; + }; + + struct LoadingInfo { + private: + std::chrono::nanoseconds m_sleep_time; + Uint64 m_start_time; + + std::future m_load_everything_thread; + + public: + std::chrono::steady_clock::time_point m_start_execution_time; + bool m_finished_loading; + scenes::LoadingScreen m_loading_screen; + + LoadingInfo( + std::chrono::nanoseconds sleep_time, + Uint64 start_time, + std::future&& load_everything_thread, + std::chrono::steady_clock::time_point start_execution_time, + bool finished_loading, + scenes::LoadingScreen&& loading_screen + ); + + [[nodiscard]] std::chrono::nanoseconds sleep_time() const; + + [[nodiscard]] Uint64 start_time() const; + + [[nodiscard]] const std::future& load_everything_thread() const; + }; +} // namespace helper + + struct Application final : public EventListener, public ServiceProvider { private: static constexpr auto num_audio_channels = u8{ 2 }; @@ -35,10 +103,22 @@ struct Application final : public EventListener, public ServiceProvider { std::unique_ptr m_font_manager; std::unique_ptr m_api; +#if defined(__EMSCRIPTEN__) + using EmscriptenFunction = std::function; + EmscriptenFunction m_current_emscripten_func; + web::WebContext m_web_context; + + void load_emscripten(); + void main_loop_emscripten(); +#endif + #if !defined(NDEBUG) std::unique_ptr m_fps_text{ nullptr }; + std::unique_ptr m_debug; #endif + std::unique_ptr m_time_info; + std::unique_ptr m_loading_info; #if defined(_HAVE_DISCORD_SDK) std::optional m_discord_instance{ std::nullopt }; @@ -51,88 +131,69 @@ struct Application final : public EventListener, public ServiceProvider { private: std::vector> m_scene_stack; + void main_loop(); + + void load_loop(); + public: Application(std::shared_ptr&& window, CommandLineArguments&& arguments); + Application(const Application&) = delete; Application& operator=(const Application&) = delete; + Application(Application&& other) noexcept = delete; + Application& operator=(Application&& other) noexcept = delete; + + ~Application(); + void run(); void handle_event(const SDL_Event& event) override; virtual void update(); + virtual void render() const; - //TODO(Totto): move those functions bodies to the cpp +#if defined(__EMSCRIPTEN__) + void loop_entry_emscripten(); + + void emscripten_do_process(); +#endif - void push_scene(std::unique_ptr scene) { - m_scene_stack.push_back(std::move(scene)); - } + void push_scene(std::unique_ptr scene); // implementation of ServiceProvider - [[nodiscard]] EventDispatcher& event_dispatcher() override { - return m_event_dispatcher; - } + [[nodiscard]] EventDispatcher& event_dispatcher() override; - [[nodiscard]] const EventDispatcher& event_dispatcher() const override { - return m_event_dispatcher; - } + [[nodiscard]] const EventDispatcher& event_dispatcher() const override; - FontManager& font_manager() override { - return *m_font_manager; - } + FontManager& font_manager() override; - [[nodiscard]] const FontManager& font_manager() const override { - return *m_font_manager; - } + [[nodiscard]] const FontManager& font_manager() const override; - CommandLineArguments& command_line_arguments() override { - return m_command_line_arguments; - } + CommandLineArguments& command_line_arguments() override; - [[nodiscard]] const CommandLineArguments& command_line_arguments() const override { - return m_command_line_arguments; - } + [[nodiscard]] const CommandLineArguments& command_line_arguments() const override; - SettingsManager& settings_manager() override { - return *m_settings_manager; - } + SettingsManager& settings_manager() override; - [[nodiscard]] const SettingsManager& settings_manager() const override { - return *m_settings_manager; - } + [[nodiscard]] const SettingsManager& settings_manager() const override; - MusicManager& music_manager() override { - return *m_music_manager; - } + MusicManager& music_manager() override; - [[nodiscard]] const MusicManager& music_manager() const override { - return *m_music_manager; - } + [[nodiscard]] const MusicManager& music_manager() const override; - [[nodiscard]] const Renderer& renderer() const override { - return m_renderer; - } + [[nodiscard]] const Renderer& renderer() const override; - [[nodiscard]] const Window& window() const override { - return *m_window; - } + [[nodiscard]] const Window& window() const override; - [[nodiscard]] Window& window() override { - return *m_window; - } + [[nodiscard]] Window& window() override; - [[nodiscard]] input::InputManager& input_manager() override { - return *m_input_manager; - } + [[nodiscard]] input::InputManager& input_manager() override; - [[nodiscard]] const input::InputManager& input_manager() const override { - return *m_input_manager; - } + [[nodiscard]] const input::InputManager& input_manager() const override; - [[nodiscard]] const std::unique_ptr& api() const override { - return m_api; - } + [[nodiscard]] const std::unique_ptr& api() const override; #if defined(_HAVE_DISCORD_SDK) @@ -143,6 +204,12 @@ struct Application final : public EventListener, public ServiceProvider { #endif +#if defined(__EMSCRIPTEN__) + + [[nodiscard]] web::WebContext& web_context() override; + [[nodiscard]] const web::WebContext& web_context() const override; + +#endif private: void initialize(); diff --git a/src/executables/game/main.cpp b/src/executables/game/main.cpp index 75bf9949f..4800a171c 100644 --- a/src/executables/game/main.cpp +++ b/src/executables/game/main.cpp @@ -24,6 +24,9 @@ #include "helper/console_helpers.hpp" #endif +#if defined(__EMSCRIPTEN__) +#include "helper/web_utils.hpp" +#endif #include #include @@ -33,28 +36,35 @@ namespace { void initialize_spdlog() { - const auto logs_path = utils::get_root_folder() / "logs"; - - auto created_log_dir = utils::create_directory(logs_path, true); - if (created_log_dir.has_value()) { - std::cerr << "warning: couldn't create logs directory '" << logs_path.string() - << "': disabled file logger\n"; - } - std::vector sinks; #if defined(__ANDROID__) sinks.push_back(std::make_shared()); #elif defined(__CONSOLE__) sinks.push_back(std::make_shared()); +#elif defined(__EMSCRIPTEN__) + sinks.push_back(web::get_console_sink()); #else sinks.push_back(std::make_shared()); #endif + +#if !(defined(__EMSCRIPTEN__)) + + const auto logs_path = utils::get_root_folder() / "logs"; + + auto created_log_dir = utils::create_directory(logs_path, true); + if (created_log_dir.has_value()) { + std::cerr << "warning: couldn't create logs directory '" << logs_path.string() + << "': disabled file logger\n"; + } + + if (not created_log_dir.has_value()) { sinks.push_back(std::make_shared( fmt::format("{}/oopetris.log", logs_path.string()), 1024 * 1024 * 10, 5, true )); } +#endif auto combined_logger = std::make_shared("combined_logger", begin(sinks), end(sinks)); spdlog::set_default_logger(combined_logger); @@ -99,7 +109,7 @@ namespace { try { -#if defined(__ANDROID__) or defined(__CONSOLE__) or defined(__SERENITY__) +#if defined(__ANDROID__) or defined(__CONSOLE__) or defined(__SERENITY__) or defined(__EMSCRIPTEN__) window = std::make_shared(window_name, WindowPosition::Centered); #else [[maybe_unused]] static constexpr int width = 1280; diff --git a/src/executables/meson.build b/src/executables/meson.build index af04785fa..bda24c0d1 100644 --- a/src/executables/meson.build +++ b/src/executables/meson.build @@ -4,32 +4,132 @@ if build_application subdir('game') - if meson.is_cross_build() and host_machine.system() == 'android' + if meson.is_cross_build() + + if host_machine.system() == 'android' + + library( + 'oopetris', + main_files, + dependencies: [liboopetris_graphics_dep, graphic_application_deps], + override_options: { + 'warning_level': '3', + 'werror': true, + }, + ) + + elif host_machine.system() == 'switch' + switch_options = [ + app_name, + main_files, + [liboopetris_graphics_dep, graphic_application_deps], + ] + subdir('platforms/switch') + elif host_machine.system() == '3ds' + _3ds_options = [ + app_name, + main_files, + [liboopetris_graphics_dep, graphic_application_deps], + ] + subdir('platforms/3ds') + elif host_machine.system() == 'emscripten' + emscripten_link_args = [] + + APP_ROMFS = meson.get_external_property('APP_ROMFS', '') + + if APP_ROMFS != '' + + fs = import('fs') + + if not fs.is_absolute(APP_ROMFS) + APP_ROMFS = meson.project_source_root() / APP_ROMFS + endif + + if not fs.exists(APP_ROMFS) + error( + 'APP_ROMFS should exist, but doesn\'t: \'' + + APP_ROMFS + + '\'', + ) + endif + + if not APP_ROMFS.endswith('/') + APP_ROMFS = APP_ROMFS + '/' + endif + + emscripten_link_args += [ + '--preload-file', APP_ROMFS + '@/assets/', + # based on: https://github.com/emscripten-core/emscripten/blob/main/src/shell_minimal.html + '--shell-file', meson.project_source_root() / 'platforms' / 'emscripten' / 'shell_file.html', + ] - library( - 'oopetris', - main_files, - dependencies: [liboopetris_graphics_dep, graphic_application_deps], - override_options: { + endif + + emscripten_name = 'oopetris' + emscripten_deps = [liboopetris_graphics_dep, graphic_application_deps] + + emscripten_options = { 'warning_level': '3', 'werror': true, - }, - ) + } + + oopetris_js = executable( + emscripten_name, + main_files, + dependencies: emscripten_deps, + link_args: emscripten_link_args, + override_options: emscripten_options, + install: true, + name_suffix: 'js', + ) + + oopetris_html = executable( + emscripten_name, + objects: oopetris_js.extract_all_objects(recursive: true), + dependencies: emscripten_deps, + link_args: emscripten_link_args, + override_options: emscripten_options, + install: true, + name_suffix: 'html', + link_depends: [oopetris_js], + ) + + python3 = find_program('python3', required: false) + + if python3.found() + + server_py = files( + meson.project_source_root() / 'platforms' / 'emscripten' / 'server.py', + ) - elif meson.is_cross_build() and host_machine.system() == 'switch' - switch_options = [ - app_name, - main_files, - [liboopetris_graphics_dep, graphic_application_deps], - ] - subdir('platforms/switch') - elif meson.is_cross_build() and host_machine.system() == '3ds' - _3ds_options = [ - app_name, - main_files, - [liboopetris_graphics_dep, graphic_application_deps], - ] - subdir('platforms/3ds') + dest_path = meson.project_build_root() / 'src' / 'executables' + + run_target( + 'serve_emscripten', + command: [python3, server_py, '-d', dest_path], + depends: [oopetris_js, oopetris_html], + ) + + endif + + wasm_validate_exe = find_program('wasm-validate', required: false) + + if wasm_validate_exe.found() + + wasm_file = meson.project_build_root() / 'src' / 'executables' / 'oopetris.wasm' + + test( + 'validate wasm file', + wasm_validate_exe, + args: ['--enable-threads', wasm_file], + depends: [oopetris_js, oopetris_html], + + ) + endif + + else + error('Unhandled cross built system: ' + host_machine.system()) + endif else if host_machine.system() == 'windows' diff --git a/src/graphics/text.cpp b/src/graphics/text.cpp index 0cc59d4d8..6396743e0 100644 --- a/src/graphics/text.cpp +++ b/src/graphics/text.cpp @@ -3,6 +3,34 @@ #include "manager/service_provider.hpp" +#if defined(OOPETRIS_DONT_USE_PRERENDERED_TEXT) + +Text::Text( + const ServiceProvider* service_provider, + const std::string& text, + const Font& font, + const Color& color, + const shapes::URect& dest +) + : m_font{ font }, + m_color{ color }, + m_dest{ dest }, + m_text{ text } { + UNUSED(service_provider); +} + + +void Text::render(const ServiceProvider& service_provider) const { + auto texture = service_provider.renderer().prerender_text(m_text, m_font, m_color); + service_provider.renderer().draw_texture(texture, m_dest); +} + +void Text::set_text(const ServiceProvider& service_provider, const std::string& text) { + UNUSED(service_provider); + m_text = text; +} + +#else Text::Text( const ServiceProvider* service_provider, const std::string& text, @@ -23,3 +51,5 @@ void Text::render(const ServiceProvider& service_provider) const { void Text::set_text(const ServiceProvider& service_provider, const std::string& text) { m_text = service_provider.renderer().prerender_text(text, m_font, m_color); } + +#endif diff --git a/src/graphics/text.hpp b/src/graphics/text.hpp index 52798319e..20884900f 100644 --- a/src/graphics/text.hpp +++ b/src/graphics/text.hpp @@ -8,13 +8,22 @@ #include "rect.hpp" #include "texture.hpp" +//TODO(Totto): set this flag in the build system, or maybe also fix https://github.com/OpenBrickProtocolFoundation/oopetris/issues/132 in the process +#if defined(__EMSCRIPTEN__) +#define OOPETRIS_DONT_USE_PRERENDERED_TEXT +#endif + + struct Text final { private: Font m_font; Color m_color; shapes::URect m_dest; +#if defined(OOPETRIS_DONT_USE_PRERENDERED_TEXT) + std::string m_text; +#else Texture m_text; - +#endif public: OOPETRIS_GRAPHICS_EXPORTED Text( const ServiceProvider* service_provider, diff --git a/src/helper/graphic_utils.cpp b/src/helper/graphic_utils.cpp index b57a015c2..2bafe4c36 100644 --- a/src/helper/graphic_utils.cpp +++ b/src/helper/graphic_utils.cpp @@ -2,6 +2,10 @@ #include "graphic_utils.hpp" #include +#if defined(__EMSCRIPTEN__) +#include +#endif + SDL_Color utils::sdl_color_from_color(const Color& color) { return SDL_Color{ color.r, color.g, color.b, color.a }; } @@ -35,6 +39,8 @@ std::vector utils::supported_features() { throw std::runtime_error{ "Failed in getting the Pref Path: " + std::string{ SDL_GetError() } }; } return std::filesystem::path{ std::string{ pref_path } }; +#elif defined(__EMSCRIPTEN__) + return std::filesystem::path{ "/" }; #elif defined(__CONSOLE__) // this is in the sdcard of the switch / 3ds , since internal storage is read-only for applications! return std::filesystem::path{ "." }; @@ -64,7 +70,7 @@ std::vector utils::supported_features() { #if defined(__SERENITY__) - // this is a read write location in the serenityos case build, see https://docs.flatpak.org/en/latest/conventions.html + // this is a read write location in the serenity-os case build, see https://docs.flatpak.org/en/latest/conventions.html const char* data_home = std::getenv("HOME"); if (data_home == nullptr) { throw std::runtime_error{ "Failed to get flatpak data directory (XDG_DATA_HOME)" }; @@ -95,8 +101,11 @@ std::vector utils::supported_features() { [[nodiscard]] std::filesystem::path utils::get_assets_folder() { #if defined(__ANDROID__) return std::filesystem::path{ "" }; +#elif defined(__EMSCRIPTEN__) + // emscripten mounts a memfs in the / location, we package assest into this dir, see: https://emscripten.org/docs/porting/files/packaging_files.html#packaging-using-emcc + return std::filesystem::path{ "/assets" }; #elif defined(__CONSOLE__) - // this is in the internal storage of the nintendo switch, it ios mounted by libnx (runtime switch support library) and filled at compile time with assets (its called ROMFS there) + // this is in the internal storage of the nintendo switch, it is mounted by libnx (runtime switch support library) and filled at compile time with assets (its called ROMFS there) return std::filesystem::path{ "romfs:/assets" }; #elif defined(BUILD_INSTALLER) // if you build in BUILD_INSTALLER mode, you have to assure that the data is there e.g. music + fonts! @@ -180,6 +189,8 @@ void utils::exit(int status_code) { // but is required here // see: https://github.com/libsdl-org/SDL/blob/main/docs/README-android.md throw utils::ExitException{ status_code }; +#elif defined(__EMSCRIPTEN__) + emscripten_force_exit(status_code); #else std::exit(status_code); #endif diff --git a/src/helper/meson.build b/src/helper/meson.build index 58d923797..ca607430c 100644 --- a/src/helper/meson.build +++ b/src/helper/meson.build @@ -12,6 +12,8 @@ graphics_src_files += files( 'music_utils.hpp', 'platform.cpp', 'platform.hpp', + 'web_utils.cpp', + 'web_utils.hpp', 'windows.hpp', ) diff --git a/src/helper/platform.cpp b/src/helper/platform.cpp index dd464240a..64650b1e1 100644 --- a/src/helper/platform.cpp +++ b/src/helper/platform.cpp @@ -15,7 +15,9 @@ namespace { - inline std::string get_error_from_errno() { +#if !(defined(__ANDROID__) || defined(__CONSOLE__) || defined(__SERENITY__) || defined(__EMSCRIPTEN__)) + + [[maybe_unused]] inline std::string get_error_from_errno() { #if defined(_MSC_VER) char buffer[256] = { 0 }; @@ -34,6 +36,7 @@ namespace { #endif } +#endif } // namespace @@ -55,6 +58,8 @@ namespace { return "MacOS"; #elif defined(__SERENITY__) return "Serenity OS"; +#elif defined(__EMSCRIPTEN__) + return "Emscripten"; #else #error "Unsupported platform" #endif @@ -78,6 +83,10 @@ namespace { #elif defined(__SERENITY__) UNUSED(url); return false; +#elif defined(__EMSCRIPTEN__) + //TODO: call window.open(url, "_blank") + UNUSED(url); + return false; #else //TODO(Totto): this is dangerous, if we supply user input, so use SDL_OpenURL preferably diff --git a/src/helper/platform.hpp b/src/helper/platform.hpp index f2d57258f..0c3bc1be0 100644 --- a/src/helper/platform.hpp +++ b/src/helper/platform.hpp @@ -8,7 +8,7 @@ #include "./windows.hpp" -enum class Platform : u8 { PC, Android, Console }; +enum class Platform : u8 { PC, Android, Console, Web }; namespace utils { @@ -19,6 +19,8 @@ namespace utils { return Platform::Android; #elif defined(__CONSOLE__) return Platform::Console; +#elif defined(__EMSCRIPTEN__) + return Platform::Web; #else return Platform::PC; #endif diff --git a/src/helper/web_utils.cpp b/src/helper/web_utils.cpp new file mode 100644 index 000000000..d463a68e4 --- /dev/null +++ b/src/helper/web_utils.cpp @@ -0,0 +1,194 @@ + + +#if defined(__EMSCRIPTEN__) + +#include + +#include "web_utils.hpp" + +#include +#include +#include +#include + + +namespace { + + emscripten::val get_local_storage() { + thread_local const emscripten::val localStorage = emscripten::val::global("localStorage"); + return localStorage; + } + + std::optional get_item_impl(const std::string& key) { + + thread_local const emscripten::val localStorage = get_local_storage(); + + emscripten::val value = localStorage.call("getItem", emscripten::val{ key }); + + if (value.isNull()) { + return std::nullopt; + } + + return value.as(); + } + + void set_item_impl(const std::string& key, const std::string& value) { + + thread_local const emscripten::val localStorage = get_local_storage(); + + localStorage.call("setItem", emscripten::val{ key }, emscripten::val{ value }); + } + + void remove_item_impl(const std::string& key) { + + thread_local const emscripten::val localStorage = get_local_storage(); + + localStorage.call("removeItem", emscripten::val{ key }); + } + + void clear_impl() { + + thread_local const emscripten::val localStorage = get_local_storage(); + + localStorage.call("clear"); + } + +} // namespace + +web::LocalStorage::LocalStorage(ServiceProvider* service_provider) : m_service_provider{ service_provider } { } + +//NOTE:we don't have access to the localStorage in threads (Web workers), so if we are in the main thread, we can call the impl directly, otherwise we have to use proxying +std::optional web::LocalStorage::get_item(const std::string& key) const { + if (m_service_provider->web_context().is_main_thread()) { + return get_item_impl(key); + } + + auto result = m_service_provider->web_context().proxy>([key = std::move(key)]() { + return get_item_impl(key); + }); + + if (not result.has_value()) { + return std::nullopt; + } + + return result.value(); +} + +bool web::LocalStorage::set_item(const std::string& key, const std::string& value) const { + if (m_service_provider->web_context().is_main_thread()) { + set_item_impl(key, value); + return true; + } + + auto result = m_service_provider->web_context().proxy([key = std::move(key), value = std::move(value)]() { + set_item_impl(key, value); + }); + + return result; +} + +bool web::LocalStorage::remove_item(const std::string& key) const { + if (m_service_provider->web_context().is_main_thread()) { + remove_item_impl(key); + return true; + } + + auto result = m_service_provider->web_context().proxy([key = std::move(key)]() { remove_item_impl(key); }); + + return result; +} + +bool web::LocalStorage::clear() const { + if (m_service_provider->web_context().is_main_thread()) { + clear_impl(); + return true; + } + + auto result = m_service_provider->web_context().proxy([]() { clear_impl(); }); + + return result; +} + + +void web::console::error(const std::string& message) { + emscripten_console_error(message.c_str()); +} + +void web::console::warn(const std::string& message) { + emscripten_console_warn(message.c_str()); +} + +void web::console::log(const std::string& message) { + emscripten_console_log(message.c_str()); +} + +void web::console::info(const std::string& message) { + emscripten_console_log(message.c_str()); +} + +void web::console::debug(const std::string& message) { + // NOTE: really the console, but also debug output + emscripten_dbg(message.c_str()); +} + +void web::console::trace(const std::string& message) { + emscripten_console_trace(message.c_str()); +} + +void web::console::clear() { + thread_local const emscripten::val console = emscripten::val::global("console"); + console.call("clear"); +} + +std::shared_ptr web::get_console_sink() { + return std::make_shared([](const spdlog::details::log_msg& msg) { + const auto message = std::string{ msg.payload.begin(), msg.payload.end() }; + + switch (msg.level) { + case spdlog::level::off: + return; + case spdlog::level::trace: + web::console::trace(message); + break; + case spdlog::level::debug: + web::console::debug(message); + break; + case spdlog::level::info: + web::console::info(message); + break; + case spdlog::level::warn: + web::console::warn(message); + break; + case spdlog::level::err: + case spdlog::level::critical: + web::console::error(message); + break; + default: + return; + } + }); +} + +web::WebContext::WebContext(ServiceProvider* service_provider) + : m_queue{}, + m_main_thread_id{ std::this_thread::get_id() }, + m_main_thread_handle{ pthread_self() }, + m_local_storage{ service_provider } { } + +web::WebContext::~WebContext() = default; + +[[nodiscard]] bool web::WebContext::is_main_thread() const { + return std::this_thread::get_id() == m_main_thread_id; +} + +void web::WebContext::do_processing() { + ASSERT(is_main_thread() && "can only process in main thread"); + + m_queue.execute(); +} + +[[nodiscard]] const web::LocalStorage& web::WebContext::local_storage() const { + return m_local_storage; +} + +#endif diff --git a/src/helper/web_utils.hpp b/src/helper/web_utils.hpp new file mode 100644 index 000000000..23c36a040 --- /dev/null +++ b/src/helper/web_utils.hpp @@ -0,0 +1,109 @@ + + +#pragma once + + +#if !defined(__EMSCRIPTEN__) +#error "this header is for emscripten only" +#endif + +#if !defined(__EMSCRIPTEN_PTHREADS__) +#error "need emscripten threads support" +#endif + +#include "manager/service_provider.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace web { + + struct LocalStorage { + private: + ServiceProvider* m_service_provider; + + public: + explicit LocalStorage(ServiceProvider* service_provider); + + [[nodiscard]] std::optional get_item(const std::string& key) const; + [[nodiscard]] bool set_item(const std::string& key, const std::string& value) const; + [[nodiscard]] bool remove_item(const std::string& key) const; + [[nodiscard]] bool clear() const; + }; + + [[nodiscard]] std::shared_ptr get_console_sink(); + + + namespace console { + void clear(); + //TODO, these support more arguments, write templates for that + void error(const std::string& message); + void warn(const std::string& message); + void log(const std::string& message); + void info(const std::string& message); + void debug(const std::string& message); + void trace(const std::string& message); + }; // namespace console + + + struct WebContext { + private: + emscripten::ProxyingQueue m_queue; + std::thread::id m_main_thread_id; + pthread_t m_main_thread_handle; + LocalStorage m_local_storage; + + public: + explicit WebContext(ServiceProvider* service_provider); + + ~WebContext(); + + [[nodiscard]] bool is_main_thread() const; + + void do_processing(); + + template + [[nodiscard]] auto proxy(std::function&& func) { + using ResultType = typename std::conditional_t, bool, std::optional>; + + if constexpr (std::is_same_v) { + + std::function proxy_func = + [func = std::move(func)](auto ctx) { + func(); + ctx.finish(); + }; + + auto successfull = m_queue.proxySyncWithCtx(m_main_thread_handle, proxy_func); + + return successfull; + } else { + ResultType result = std::nullopt; + + std::function proxy_func = + [&result, func = std::move(func)](auto ctx) { + result = func(); + ctx.finish(); + }; + + auto successfull = m_queue.proxySyncWithCtx(m_main_thread_handle, proxy_func); + + + if (not successfull) { + result = std::nullopt; + } + + return result; + } + } + + [[nodiscard]] const LocalStorage& local_storage() const; + }; + + +}; // namespace web diff --git a/src/libs/core/helper/sleep.cpp b/src/libs/core/helper/sleep.cpp index 5609fd6b0..1d9f76936 100644 --- a/src/libs/core/helper/sleep.cpp +++ b/src/libs/core/helper/sleep.cpp @@ -7,6 +7,9 @@ #ifndef NOMINMAX #define NOMINMAX #endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif #include #else #include diff --git a/src/lobby/api.cpp b/src/lobby/api.cpp index 77d10d895..72dcc7aa3 100644 --- a/src/lobby/api.cpp +++ b/src/lobby/api.cpp @@ -5,14 +5,22 @@ #include #include -#if defined(_OOPETRIS_ONLINE_USE_CURL) +#if !defined(_OOPETRIS_ONLINE_SYSTEM) +#error "_OOPETRIS_ONLINE_SYSTEM has to be defined" +#endif + +#if _OOPETRIS_ONLINE_SYSTEM == 0 +#include "./httplib_client.hpp" +#elif _OOPETRIS_ONLINE_SYSTEM == 1 +#include "./web_client.hpp" +#elif _OOPETRIS_ONLINE_SYSTEM == 2 #include "./curl_client.hpp" #else -#include "./httplib_client.hpp" +#error "_OOPETRIS_ONLINE_SYSTEM has an invalid value" #endif namespace { - namespace constants { + namespace token::constants { constexpr const char* api_token_key = "API_TOKEN_save"; } @@ -52,11 +60,11 @@ helper::expected lobby::API::check_reachability() { return {}; } -lobby::API::API(const std::string& api_url) +lobby::API::API(ServiceProvider* service_provider, const std::string& api_url) : m_client{ std::make_unique(api_url) }, - m_secret_storage{ std::make_unique(secret::KeyringType::User) } { + m_secret_storage{ std::make_unique(service_provider, secret::KeyringType::User) } { - auto value = m_secret_storage->load(constants::api_token_key); + auto value = m_secret_storage->load(token::constants::api_token_key); if (value.has_value()) { if (not this->setup_authentication(value.value().as_string())) { @@ -81,11 +89,12 @@ lobby::API::API(API&& other) noexcept lobby::API::~API() = default; -helper::expected lobby::API::get_api(const std::string& url) { +helper::expected +lobby::API::get_api(ServiceProvider* service_provider, const std::string& url) { try { - API api{ url }; + API api{ service_provider, url }; const auto reachable = api.check_reachability(); @@ -101,11 +110,15 @@ helper::expected lobby::API::get_api(const std::string& } } -void lobby::API::check_url(const std::string& url, std::function&& callback) { +void lobby::API::check_url( + ServiceProvider* service_provider, + const std::string& url, + std::function&& callback +) { - //TODO(Totto): is this doen correctly - std::ignore = std::async(std::launch::async, [url, callback = std::move(callback)] { - auto result = lobby::API::get_api(url); + //TODO(Totto): is this done correctly + std::ignore = std::async(std::launch::async, [url, callback = std::move(callback), service_provider] { + auto result = lobby::API::get_api(service_provider, url); callback(result.has_value()); }); @@ -148,7 +161,7 @@ bool lobby::API::authenticate(const Credentials& credentials) { void lobby::API::logout() { m_client->ResetBearerAuth(); - if (auto result = m_secret_storage->remove(constants::api_token_key); result.has_value()) { + if (auto result = m_secret_storage->remove(token::constants::api_token_key); result.has_value()) { spdlog::error("API: {}", result.value()); } } @@ -275,7 +288,8 @@ bool lobby::API::setup_authentication(const std::string& token) { m_authentication_token = token; m_client->SetBearerAuth(token); - if (auto result = m_secret_storage->store(constants::api_token_key, secret::Buffer{ token }); result.has_value()) { + if (auto result = m_secret_storage->store(token::constants::api_token_key, secret::Buffer{ token }); + result.has_value()) { spdlog::error("API {}", result.value()); } diff --git a/src/lobby/api.hpp b/src/lobby/api.hpp index 1ce9fefa4..3559e9cb0 100644 --- a/src/lobby/api.hpp +++ b/src/lobby/api.hpp @@ -26,7 +26,7 @@ namespace lobby { [[nodiscard]] helper::expected check_reachability(); - explicit API(const std::string& api_url); + explicit API(ServiceProvider* service_provider, const std::string& api_url); helper::expected get_version(); @@ -42,10 +42,17 @@ namespace lobby { OOPETRIS_GRAPHICS_EXPORTED ~API(); OOPETRIS_GRAPHICS_EXPORTED - [[nodiscard]] helper::expected static get_api(const std::string& url); + [[nodiscard]] helper::expected static get_api( + ServiceProvider* service_provider, + const std::string& url + ); OOPETRIS_GRAPHICS_EXPORTED - void static check_url(const std::string& url, std::function&& callback); + void static check_url( + ServiceProvider* service_provider, + const std::string& url, + std::function&& callback + ); OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] bool is_authenticated(); diff --git a/src/lobby/client.cpp b/src/lobby/client.cpp index c68fd11c8..69455c74f 100644 --- a/src/lobby/client.cpp +++ b/src/lobby/client.cpp @@ -3,23 +3,32 @@ #include "./client.hpp" -#if defined(_OOPETRIS_ONLINE_USE_CURL) -#include "./curl_client.hpp" +#if _OOPETRIS_ONLINE_SYSTEM == 0 +#include "./httplib_client.hpp" + +std::string oopetris::http::status_message(int status) { + return httplib::status_message(status); +} + +#elif _OOPETRIS_ONLINE_SYSTEM == 1 + +#include "./web_client.hpp" std::string oopetris::http::status_message([[maybe_unused]] int status) { return "Not Available"; } +#elif _OOPETRIS_ONLINE_SYSTEM == 2 -#else - -#include "./httplib_client.hpp" +#include "./curl_client.hpp" -std::string oopetris::http::status_message(int status) { - return httplib::status_message(status); +std::string oopetris::http::status_message([[maybe_unused]] int status) { + return "Not Available"; } +#else +#error "_OOPETRIS_ONLINE_SYSTEM has an invalid value" #endif oopetris::http::Result::~Result() = default; diff --git a/src/lobby/credentials/secret.cpp b/src/lobby/credentials/secret.cpp index 301e19cb2..8529dd941 100644 --- a/src/lobby/credentials/secret.cpp +++ b/src/lobby/credentials/secret.cpp @@ -7,14 +7,14 @@ #include namespace { - namespace constants { + namespace secrets::constants { constexpr const char* key_type_user = "user"; constexpr const char* key_name_prefix = "OOPetris_key__"; - } // namespace constants + } // namespace secrets::constants std::string get_key_name(const std::string& key) { - return constants::key_name_prefix + key; + return secrets::constants::key_name_prefix + key; } i64 get_id_from_name(key_serial_t keyring_id, const std::string& key) { @@ -22,14 +22,16 @@ namespace { const std::string full_key = get_key_name(key); // 0 stands for: do not create a link to another keyring - return keyctl_search(keyring_id, constants::key_type_user, full_key.c_str(), 0); + return keyctl_search(keyring_id, secrets::constants::key_type_user, full_key.c_str(), 0); } } // namespace -secret::SecretStorage::SecretStorage(KeyringType type) : m_type{ type } { +secret::SecretStorage::SecretStorage(ServiceProvider* service_provider, KeyringType type) + : m_service_provider{ service_provider }, + m_type{ type } { key_serial_t key_type{}; switch (m_type) { @@ -111,7 +113,7 @@ secret::SecretStorage::store(const std::string& key, const Buffer& value, bool u auto full_key = get_key_name(key); - auto new_id = add_key(constants::key_type_user, full_key.c_str(), value.data(), value.size(), m_ring_id); + auto new_id = add_key(secrets::constants::key_type_user, full_key.c_str(), value.data(), value.size(), m_ring_id); if (new_id < 0) { return fmt::format("Error while storing a key, can't add the key: {}", strerror(errno)); @@ -163,16 +165,16 @@ secret::SecretStorage::store(const std::string& key, const Buffer& value, bool u namespace { - namespace constants { + namespace secrets::constants { constexpr const wchar_t* property_name = L"OOPetris Payload"; constexpr const wchar_t* key_name_prefix = L"OOPetris_key__"; constexpr const wchar_t* used_algorithm = BCRYPT_AES_ALGORITHM; - } // namespace constants + } // namespace secrets::constants std::wstring get_key_name(const std::string& key) { - std::wstring result{ constants::key_name_prefix }; + std::wstring result{ secrets::constants::key_name_prefix }; for (auto& normal_char : key) { result += normal_char; } @@ -204,7 +206,10 @@ namespace { } // namespace -secret::SecretStorage::SecretStorage(KeyringType type) : m_type{ type }, m_phProvider{} { +secret::SecretStorage::SecretStorage(ServiceProvider* service_provider, KeyringType type) + : m_service_provider{ service_provider }, + m_type{ type }, + m_phProvider{} { if (type == KeyringType::Session) { spdlog::warn("KeyringType Session is not supported, using KeyringType User"); @@ -246,7 +251,7 @@ secret::SecretStorage::SecretStorage(SecretStorage&& other) noexcept DWORD bytes_needed{}; // no flags needed, so using 0 - auto result = NCryptGetProperty(key_handle, constants::property_name, nullptr, 0, &bytes_needed, 0); + auto result = NCryptGetProperty(key_handle, secrets::constants::property_name, nullptr, 0, &bytes_needed, 0); if (result != ERROR_SUCCESS) { NCryptFreeObject(key_handle); @@ -259,7 +264,8 @@ secret::SecretStorage::SecretStorage(SecretStorage&& other) noexcept DWORD bytes_written{}; - auto result2 = NCryptGetProperty(key_handle, constants::property_name, buffer, bytes_needed, &bytes_written, 0); + auto result2 = + NCryptGetProperty(key_handle, secrets::constants::property_name, buffer, bytes_needed, &bytes_written, 0); if (result2 != ERROR_SUCCESS) { NCryptFreeObject(key_handle); @@ -307,7 +313,7 @@ secret::SecretStorage::store(const std::string& key, const Buffer& value, bool u // 0 means no dwLegacyKeySpec auto result = NCryptCreatePersistedKey( - this->m_phProvider, &key_handle, constants::used_algorithm, key_name.c_str(), 0, flags + this->m_phProvider, &key_handle, secrets::constants::used_algorithm, key_name.c_str(), 0, flags ); if (result != ERROR_SUCCESS) { @@ -320,8 +326,9 @@ secret::SecretStorage::store(const std::string& key, const Buffer& value, bool u std::memcpy(buffer, value.data(), value.size()); - auto result2 = - NCryptSetProperty(key_handle, constants::property_name, buffer, static_cast(value.size()), flags2); + auto result2 = NCryptSetProperty( + key_handle, secrets::constants::property_name, buffer, static_cast(value.size()), flags2 + ); delete[] buffer; @@ -373,9 +380,9 @@ secret::SecretStorage::store(const std::string& key, const Buffer& value, bool u namespace { - namespace secrets_constants { + namespace secrets::constants { constexpr const char* store_file_name = ".secret_key_storage"; - } // namespace secrets_constants + } // namespace secrets::constants helper::expected get_json_from_file(const std::string& file) { auto result = json::try_parse_json_file(file); @@ -393,9 +400,11 @@ namespace { // This is a dummy fallback, but good enough for this platforms -secret::SecretStorage::SecretStorage(KeyringType type) : m_type{ type } { +secret::SecretStorage::SecretStorage(ServiceProvider* service_provider, KeyringType type) + : m_service_provider{ service_provider }, + m_type{ type } { - m_file_path = utils::get_root_folder() / secrets_constants::store_file_name; + m_file_path = utils::get_root_folder() / secrets::constants::store_file_name; } secret::SecretStorage::~SecretStorage() = default; @@ -423,9 +432,7 @@ secret::SecretStorage::SecretStorage(SecretStorage&& other) noexcept json_value.at(key).get_to(result); - auto result_buffer = Buffer{ result }; - - return result_buffer; + return Buffer{ result }; } [[nodiscard]] std::optional @@ -466,6 +473,81 @@ secret::SecretStorage::store(const std::string& key, const Buffer& value, bool u } +#elif defined(__EMSCRIPTEN__) + +#include "helper/web_utils.hpp" + + +namespace { + + namespace secrets::constants { + + constexpr const char* key_name_prefix = "OOPetris_key__"; + } // namespace secrets::constants + + std::string get_key_name(const std::string& key) { + return secrets::constants::key_name_prefix + key; + } +} // namespace + + +// This is a dummy fallback, but good enough for this platforms +secret::SecretStorage::SecretStorage(ServiceProvider* service_provider, KeyringType type) + : m_service_provider{ service_provider }, + m_type{ type } { } + +secret::SecretStorage::~SecretStorage() = default; + +secret::SecretStorage::SecretStorage(SecretStorage&& other) noexcept : m_type{ other.m_type } { } + + +[[nodiscard]] helper::expected secret::SecretStorage::load(const std::string& key) const { + + const auto key_name = get_key_name(key); + auto maybe_value = m_service_provider->web_context().local_storage().get_item(key_name); + if (not maybe_value.has_value()) { + return helper::unexpected{ "Key not found" }; + } + + return Buffer{ maybe_value.value() }; +} + +[[nodiscard]] std::optional +secret::SecretStorage::store(const std::string& key, const Buffer& value, bool update) const { + + const auto key_name = get_key_name(key); + + if (not update) { + auto maybe_value = m_service_provider->web_context().local_storage().get_item(key_name); + if (maybe_value.has_value()) { + return "Error while storing a key, it already exists and we can't update it"; + } + } + + auto is_successfull = m_service_provider->web_context().local_storage().set_item(key_name, value.as_string()); + + if (not is_successfull) { + return "Error while storing a key, LocalStorage set item error"; + } + + return std::nullopt; +} + + +[[nodiscard]] std::optional secret::SecretStorage::remove(const std::string& key) const { + + const auto key_name = get_key_name(key); + + auto is_successfull = m_service_provider->web_context().local_storage().remove_item(key_name); + + if (not is_successfull) { + return "Error while removing a key, LocalStorage remove item error"; + } + + return std::nullopt; +} + + #else #error "Unsupported platform" #endif diff --git a/src/lobby/credentials/secret.hpp b/src/lobby/credentials/secret.hpp index c2a91a934..1c96c3a65 100644 --- a/src/lobby/credentials/secret.hpp +++ b/src/lobby/credentials/secret.hpp @@ -9,7 +9,7 @@ #include "./buffer.hpp" #include "helper/windows.hpp" - +#include "manager/service_provider.hpp" #if defined(__linux__) @@ -29,6 +29,7 @@ namespace secret { struct SecretStorage { private: + [[maybe_unused]] ServiceProvider* m_service_provider; KeyringType m_type; #if defined(__linux__) || defined(__ANDROID__) @@ -41,7 +42,7 @@ namespace secret { public: - OOPETRIS_GRAPHICS_EXPORTED explicit SecretStorage(KeyringType type); + OOPETRIS_GRAPHICS_EXPORTED explicit SecretStorage(ServiceProvider* service_provider, KeyringType type); OOPETRIS_GRAPHICS_EXPORTED ~SecretStorage(); //NOLINT(performance-trivially-destructible) diff --git a/src/lobby/meson.build b/src/lobby/meson.build index 90779ffd6..057eca851 100644 --- a/src/lobby/meson.build +++ b/src/lobby/meson.build @@ -1,13 +1,24 @@ -if online_multiplayer_user_fallback +if online_multiplayer_system == 'curl' graphics_src_files += files( 'curl_client.cpp', 'curl_client.hpp', ) -else +elif online_multiplayer_system == 'httplib' graphics_src_files += files( 'httplib_client.cpp', 'httplib_client.hpp', ) +elif online_multiplayer_system == 'web' + graphics_src_files += files( + 'web_client.cpp', + 'web_client.hpp', + ) +else + error( + 'Unhandled online_multiplayer_system: \'' + + online_multiplayer_system + + '\'', + ) endif graphics_src_files += files( diff --git a/src/lobby/web_client.cpp b/src/lobby/web_client.cpp new file mode 100644 index 000000000..fb81fe18b --- /dev/null +++ b/src/lobby/web_client.cpp @@ -0,0 +1,269 @@ + +#include "./web_client.hpp" + +#include + +// Note: this uses emscripten fetch +//see: https://emscripten.org/docs/api_reference/fetch.html + +//Note: Synchronous Emscripten Fetch operations are subject to a number of restrictions, depending on which Emscripten build mode (linker flags) is used: +// we use: -pthread: Synchronous Fetch operations are available on pthreads, but not on the main thread. + +#define TRANSFORM_RESULT(result) std::make_unique((result)) //NOLINT(cppcoreguidelines-macro-usage + +namespace { + using FetchHeader = oopetris::http::implementation::details::FetchHeader; + + FetchHeader get_headers(const std::unique_ptr& request) { + + auto header_length = emscripten_fetch_get_response_headers_length(request.get()); + + auto* headersRawDst = new char[header_length + 1]; + emscripten_fetch_get_response_headers(request.get(), headersRawDst, header_length + 1); + + char** unpackedHeaders = emscripten_fetch_unpack_response_headers(headersRawDst); + + FetchHeader result{}; + + u32 i = 0; + while (true) { + auto* headerKey = unpackedHeaders[i]; + + if (headerKey == nullptr) { + break; + } + + auto* headerValue = unpackedHeaders[i + 1]; + if (headerValue == nullptr) { + //this is an error, since this has to be a valid char* + //TODO: report that properly + break; + } + + result.at(headerKey) = std::string{ headerValue }; + + i += 2; + } + + delete[] headersRawDst; + + emscripten_fetch_free_unpacked_response_headers(unpackedHeaders); + + return result; + } + + // Specifies the readyState of the XHR request: + // 0: UNSENT: request not sent yet + // 1: OPENED: emscripten_fetch has been called. + // 2: HEADERS_RECEIVED: emscripten_fetch has been called, and headers and + // status are available. + // 3: LOADING: download in progress. + // 4: DONE: download finished. + // See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState + enum class FetchReadyState { + UNSENT = 0, + OPENED = 1, + HEADERS_RECEIVED = 2, + LOADING = 3, + DONE = 4, + }; + + const constexpr u32 max_method_size = 32; + +} // namespace + + +oopetris::http::implementation::ActualResult::ActualResult(std::unique_ptr&& request) + : m_request{ std::move(request) }, + m_response_headers{ get_headers(m_request) } { } + +oopetris::http::implementation::ActualResult::~ActualResult() { + if (m_request) { + emscripten_fetch_close(m_request.get()); + } +}; + + +oopetris::http::implementation::ActualResult::ActualResult(ActualResult&& other) noexcept + : m_request{ std::move(other.m_request) }, + m_response_headers{ std::move(other.m_response_headers) } { } + +[[nodiscard]] std::optional oopetris::http::implementation::ActualResult::get_header(const std::string& key +) const { + if (m_response_headers.contains(key)) { + return std::nullopt; + } + + return m_response_headers.at(key); +} + +[[nodiscard]] std::string oopetris::http::implementation::ActualResult::body() const { + + //TODO: test if this is correct + auto size = m_request->numBytes; + + std::string result{ m_request->data, m_request->data + size }; + + return result; +} + +[[nodiscard]] int oopetris::http::implementation::ActualResult::status() const { + return m_request->status; +} + +[[nodiscard]] std::optional oopetris::http::implementation::ActualResult::get_error() const { + + auto readyState = static_cast(m_request->readyState); + + if (readyState != FetchReadyState::DONE) { + return fmt::format("Invalid readyState: {}", magic_enum::enum_name(readyState)); + } + + return std::nullopt; +} + +namespace { + using FetchData = std::pair; + + std::string normalize_url(const std::string& value) { + if (value.ends_with("/")) { + return value.substr(0, value.size() - 1); + } + + return value; + } + + std::string concat_url(const std::string& normalized_base, const std::string& value) { + if (value.starts_with("/")) { + return normalized_base + value; + } + + return normalized_base + "/" + value; + } + + std::unique_ptr make_request_impl( + const std::string& method, + const std::string& url, + const FetchHeader& header, + const std::optional& data + ) { + emscripten_fetch_attr_t attr{}; + emscripten_fetch_attr_init(&attr); + + assert(method.size() <= (max_method_size - 1)); + std::memcpy(attr.requestMethod, method.c_str(), method.size() + 1); + + //TODO(Totto): once the http Implementation "Interface" support async thing, use that: + // see: https://emscripten.org/docs/api_reference/fetch.html#synchronous-fetches + + attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY | EMSCRIPTEN_FETCH_SYNCHRONOUS; + + + std::vector raw_headers{}; + for (const auto& [key, value] : header) { + raw_headers.push_back(key.c_str()); + raw_headers.push_back(value.c_str()); + } + + raw_headers.push_back(nullptr); + + attr.requestHeaders = raw_headers.data(); + + if (data.has_value()) { + const auto& [mime_type, raw_data] = data.value(); + + attr.overriddenMimeType = mime_type.c_str(); + attr.requestData = raw_data.c_str(); + attr.requestDataSize = raw_data.size(); + } + + emscripten_fetch_t* result = emscripten_fetch(&attr, url.c_str()); + + return std::unique_ptr(result); + } + + std::unique_ptr + make_request(const std::string& method, const std::string& url, const FetchHeader& header) { + return make_request_impl(method, url, header, std::nullopt); + } + + std::unique_ptr make_request_with_data( + const std::string& method, + const std::string& url, + const FetchHeader& header, + const FetchData& data + ) { + return make_request_impl(method, url, header, data); + } +} // namespace + + +oopetris::http::implementation::ActualClient::ActualClient(ActualClient&& other) noexcept + : m_base_url{ std::move(other.m_base_url) }, + m_headers{ std::move(other.m_headers) } { } + +oopetris::http::implementation::ActualClient::~ActualClient() = default; + +oopetris::http::implementation::ActualClient::ActualClient(const std::string& api_url) + : m_base_url{ normalize_url(api_url) }, + m_headers{} { + + //NOTE: no Accept header or compression is set here, as emscriptens fetch does provide reasonable defaults (depending on what the browser supports) +} + +[[nodiscard]] std::unique_ptr oopetris::http::implementation::ActualClient::Get( + const std::string& url +) { + + const auto final_url = concat_url(m_base_url, url); + + return TRANSFORM_RESULT(make_request("GET", final_url, m_headers)); +} + +[[nodiscard]] std::unique_ptr oopetris::http::implementation::ActualClient::Delete( + const std::string& url +) { + const auto final_url = concat_url(m_base_url, url); + + return TRANSFORM_RESULT(make_request("DELETE", final_url, m_headers)); +} + +[[nodiscard]] std::unique_ptr oopetris::http::implementation::ActualClient::Post( + const std::string& url, + const std::optional>& payload +) { + const auto final_url = concat_url(m_base_url, url); + + if (not payload.has_value()) { + return TRANSFORM_RESULT(make_request("POST", final_url, m_headers)); + } + + auto [content, content_type] = payload.value(); + + return TRANSFORM_RESULT(make_request_with_data("POST", final_url, m_headers, payload.value())); +} + +[[nodiscard]] std::unique_ptr oopetris::http::implementation::ActualClient::Put( + const std::string& url, + const std::optional>& payload +) { + const auto final_url = concat_url(m_base_url, url); + + if (not payload.has_value()) { + return TRANSFORM_RESULT(make_request("PUT", final_url, m_headers)); + } + + auto [content, content_type] = payload.value(); + + return TRANSFORM_RESULT(make_request_with_data("PUT", final_url, m_headers, payload.value())); +} + +void oopetris::http::implementation::ActualClient::SetBearerAuth(const std::string& token) { + + m_headers.at("Authorization") = fmt::format("Bearer {}", token); +} + +void oopetris::http::implementation::ActualClient::ResetBearerAuth() { + + m_headers.erase("Authorization"); +} diff --git a/src/lobby/web_client.hpp b/src/lobby/web_client.hpp new file mode 100644 index 000000000..014d59e39 --- /dev/null +++ b/src/lobby/web_client.hpp @@ -0,0 +1,77 @@ + +#pragma once + +#include + +#include + +#include "./client.hpp" + + +namespace oopetris::http::implementation { + + namespace details { + using FetchHeader = std::unordered_map; + } + + struct ActualResult : ::oopetris::http::Result { + private: + std::unique_ptr m_request; + details::FetchHeader m_response_headers; + + public: + OOPETRIS_GRAPHICS_EXPORTED explicit ActualResult(std::unique_ptr&& request); + + OOPETRIS_GRAPHICS_EXPORTED ~ActualResult() override; + + OOPETRIS_GRAPHICS_EXPORTED ActualResult(ActualResult&& other) noexcept; + OOPETRIS_GRAPHICS_EXPORTED ActualResult& operator=(ActualResult&& other) noexcept = delete; + + OOPETRIS_GRAPHICS_EXPORTED ActualResult(const ActualResult& other) = delete; + OOPETRIS_GRAPHICS_EXPORTED ActualResult& operator=(const ActualResult& other) = delete; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::optional get_header(const std::string& key + ) const override; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::string body() const override; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] int status() const override; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::optional get_error() const override; + }; + + + struct ActualClient : ::oopetris::http::Client { + + private: + std::string m_base_url; + details::FetchHeader m_headers; + + public: + OOPETRIS_GRAPHICS_EXPORTED ActualClient(ActualClient&& other) noexcept; + OOPETRIS_GRAPHICS_EXPORTED ActualClient& operator=(ActualClient&& other) noexcept = delete; + + OOPETRIS_GRAPHICS_EXPORTED ActualClient(const ActualClient& other) = delete; + OOPETRIS_GRAPHICS_EXPORTED ActualClient& operator=(const ActualClient& other) = delete; + + OOPETRIS_GRAPHICS_EXPORTED ~ActualClient() override; + + OOPETRIS_GRAPHICS_EXPORTED explicit ActualClient(const std::string& api_url); + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::unique_ptr Get(const std::string& url) override; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::unique_ptr Delete(const std::string& url) override; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::unique_ptr + Post(const std::string& url, const std::optional>& payload) override; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::unique_ptr + Put(const std::string& url, const std::optional>& payload) override; + + OOPETRIS_GRAPHICS_EXPORTED void SetBearerAuth(const std::string& token) override; + + OOPETRIS_GRAPHICS_EXPORTED void ResetBearerAuth() override; + }; + + +} // namespace oopetris::http::implementation diff --git a/src/manager/music_manager.cpp b/src/manager/music_manager.cpp index 0f86404c9..7ff6ace37 100644 --- a/src/manager/music_manager.cpp +++ b/src/manager/music_manager.cpp @@ -56,14 +56,22 @@ MusicManager::MusicManager(ServiceProvider* service_provider, u8 channel_size) } } +#if defined(__EMSCRIPTEN__) + //TODO: do we need this, first do this: + // https://github.com/libsdl-org/SDL/issues/6385 +#else + const auto audio_channels = 2; + // 2 here means STEREO, note that channels above means tracks, e.g. simultaneous playing source that are mixed, // hence the name SDL2_mixer const auto audio_result = - Mix_OpenAudio(constants::audio_frequency, MIX_DEFAULT_FORMAT, 2, constants::audio_chunk_size); + Mix_OpenAudio(constants::audio_frequency, MIX_DEFAULT_FORMAT, audio_channels, constants::audio_chunk_size); + if (audio_result != 0) { throw helper::InitializationError{ fmt::format("Failed to open an audio device: {}", SDL_GetError()) }; } +#endif m_s_instance = this; diff --git a/src/manager/service_provider.hpp b/src/manager/service_provider.hpp index 3dc956ab7..6c98aad35 100644 --- a/src/manager/service_provider.hpp +++ b/src/manager/service_provider.hpp @@ -33,6 +33,10 @@ namespace lobby { } +namespace web { + struct WebContext; +} + struct ServiceProvider { ServiceProvider() = default; ServiceProvider(const ServiceProvider&) = delete; @@ -65,5 +69,12 @@ struct ServiceProvider { [[nodiscard]] virtual std::optional& discord_instance() = 0; [[nodiscard]] virtual const std::optional& discord_instance() const = 0; +#endif + +#if defined(__EMSCRIPTEN__) + + [[nodiscard]] virtual web::WebContext& web_context() = 0; + [[nodiscard]] virtual const web::WebContext& web_context() const = 0; + #endif }; diff --git a/src/manager/settings_manager.cpp b/src/manager/settings_manager.cpp index 6115da531..7e15ca2e5 100644 --- a/src/manager/settings_manager.cpp +++ b/src/manager/settings_manager.cpp @@ -3,23 +3,65 @@ #include "input/keyboard_input.hpp" #include "input/touch_input.hpp" + +#if defined(__EMSCRIPTEN__) +#include "helper/web_utils.hpp" +#endif + namespace { +#if defined(__EMSCRIPTEN__) + constexpr const auto settings_key = "settings_key"; +#else constexpr const auto settings_filename = "settings.json"; +#endif -} +} // namespace + + +SettingsManager::SettingsManager(ServiceProvider* service_provider) : m_service_provider{ service_provider } { + +#if defined(__EMSCRIPTEN__) + const auto content = m_service_provider->web_context().local_storage().get_item(settings_key); + helper::expected> result = + helper::unexpected>{ std::make_pair( + "Key not present in LocalStorage", json::ParseError::OpenError + ) }; -SettingsManager::SettingsManager() { + if (content.has_value()) { + auto parse_result = json::try_parse_json(content.value()); + + if (not parse_result.has_value()) { + result = helper::unexpected>{ + std::make_pair( + std::move(parse_result.error()), json::ParseError::FormatError + ) + }; + } else { + result = parse_result.value(); + } + } + +#else const std::filesystem::path settings_file = utils::get_root_folder() / settings_filename; const auto result = json::try_parse_json_file(settings_file); +#endif if (result.has_value()) { m_settings = result.value(); } else { auto [error, error_type] = result.error(); - spdlog::error("unable to load settings from \"{}\": {}", settings_filename, error); + spdlog::error( + "unable to load settings from \"{}\": {}", +#if defined(__EMSCRIPTEN__) + settings_key, +#else + settings_filename, +#endif + error + ); spdlog::warn("applying default settings"); m_settings = { @@ -48,14 +90,38 @@ void SettingsManager::add_callback(Callback&& callback) { } void SettingsManager::save() const { + +#if defined(__EMSCRIPTEN__) + const auto maybe_settings_json = json::try_json_to_string(m_settings); + + if (not maybe_settings_json.has_value()) { + spdlog::error( + "unable to save settings to LocalStorage\"{}\": unable to convert settings to json: {}", settings_key, + maybe_settings_json.error() + ); + return; + } + + auto is_successfull = + m_service_provider->web_context().local_storage().set_item(settings_key, maybe_settings_json.value()); + + if (not is_successfull) { + spdlog::error("unable to save settings to LocalStorage\"{}\": localstorage set error", settings_key); + return; + } + +#else const std::filesystem::path settings_file = utils::get_root_folder() / settings_filename; - const auto result = json::try_write_json_to_file(settings_file, m_settings, true); + const auto result = json::try_write_json_to_file(settings_file, m_settings, true); + if (result.has_value()) { spdlog::error("unable to save settings to \"{}\": {}", settings_filename, result.value()); return; } +#endif + this->fire_callbacks(); } diff --git a/src/manager/settings_manager.hpp b/src/manager/settings_manager.hpp index 71bae702d..2d73a0ca4 100644 --- a/src/manager/settings_manager.hpp +++ b/src/manager/settings_manager.hpp @@ -18,9 +18,10 @@ struct SettingsManager { private: settings::Settings m_settings; std::vector m_callbacks; + ServiceProvider* m_service_provider; public: - OOPETRIS_GRAPHICS_EXPORTED explicit SettingsManager(); + OOPETRIS_GRAPHICS_EXPORTED explicit SettingsManager(ServiceProvider* service_provider); OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] const settings::Settings& settings() const; diff --git a/src/scenes/about_page/about_page.hpp b/src/scenes/about_page/about_page.hpp index c18797ecd..e937bed00 100644 --- a/src/scenes/about_page/about_page.hpp +++ b/src/scenes/about_page/about_page.hpp @@ -15,7 +15,7 @@ namespace scenes { static constexpr std::initializer_list> authors{ - { "mgerhold", "https://github.com/mgerhold", "mgerhold.jpg" }, + { "mgerhold", "https://github.com/mgerhold", "mgerhold.png" }, { "Totto16", "https://github.com/Totto16", "Totto16.png" } }; diff --git a/src/scenes/settings_menu/settings_menu.cpp b/src/scenes/settings_menu/settings_menu.cpp index 85796462c..b5f1c7c1b 100644 --- a/src/scenes/settings_menu/settings_menu.cpp +++ b/src/scenes/settings_menu/settings_menu.cpp @@ -211,7 +211,7 @@ namespace scenes { this->m_status = Status::Loading; //TODO(Totto): do this somehow asynchronous - lobby::API::check_url(api_url, [this, api_url](bool success) { + lobby::API::check_url(m_service_provider, api_url, [this, api_url](bool success) { this->m_status = success ? Status::Ok : Status::Error; this->m_settings.api_url = api_url; this->m_did_change_settings = true; diff --git a/subprojects/fmt.wrap b/subprojects/fmt.wrap index e4c65b1f1..b90579833 100644 --- a/subprojects/fmt.wrap +++ b/subprojects/fmt.wrap @@ -8,7 +8,7 @@ patch_url = https://wrapdb.mesonbuild.com/v2/fmt_11.0.2-1/get_patch patch_hash = 90c9e3b8e8f29713d40ca949f6f93ad115d78d7fb921064112bc6179e6427c5e source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/fmt_11.0.2-1/fmt-11.0.2.tar.gz wrapdb_version = 11.0.2-1 -diff_files = fmt_dependency_override.diff +diff_files = fmt_dependency_override.diff, fmt_emscripten.diff [provide] fmt = fmt_dep diff --git a/subprojects/icu.wrap b/subprojects/icu.wrap index 982b9a5b2..c7fd1f47f 100644 --- a/subprojects/icu.wrap +++ b/subprojects/icu.wrap @@ -1,13 +1,13 @@ [wrap-file] directory = icu -source_url = https://github.com/unicode-org/icu/releases/download/release-73-2/icu4c-73_2-src.tgz -source_filename = icu4c-73_2-src.tgz -source_hash = 818a80712ed3caacd9b652305e01afc7fa167e6f2e94996da44b90c2ab604ce1 -patch_filename = icu_73.2-2_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/icu_73.2-2/get_patch -patch_hash = 218a5f20b58b6b2372e636c2eb2d611a898fdc11be17d6c4f35a3cd54d472010 -source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/icu_73.2-2/icu4c-73_2-src.tgz -wrapdb_version = 73.2-2 +source_url = https://github.com/unicode-org/icu/releases/download/release-76-1/icu4c-76_1-src.tgz +source_filename = icu4c-76_1-src.tgz +source_hash = dfacb46bfe4747410472ce3e1144bf28a102feeaa4e3875bac9b4c6cf30f4f3e +patch_filename = icu_76.1-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/icu_76.1-1/get_patch +patch_hash = e50941b3a3f2034032079bbeaccd2c59b54963f12d43aefb9673a607556d4abc +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/icu_76.1-1/icu4c-76_1-src.tgz +wrapdb_version = 76.1-1 [provide] icu-uc = icuuc_dep diff --git a/subprojects/magic_enum.wrap b/subprojects/magic_enum.wrap index 4cac0678a..031a6259c 100644 --- a/subprojects/magic_enum.wrap +++ b/subprojects/magic_enum.wrap @@ -1,12 +1,10 @@ [wrap-file] -directory = magic_enum-0.9.6 -source_url = https://github.com/Neargye/magic_enum/archive/refs/tags/v0.9.6.tar.gz -source_filename = magic_enum-v0.9.6.tar.gz -source_hash = 814791ff32218dc869845af7eb89f898ebbcfa18e8d81aa4d682d18961e13731 -source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/magic_enum_0.9.6-1/magic_enum-v0.9.6.tar.gz -wrapdb_version = 0.9.6-1 - -diff_files = magic_enum-0.9.6_installed.diff +directory = magic_enum-0.9.7 +source_url = https://github.com/Neargye/magic_enum/archive/refs/tags/v0.9.7.tar.gz +source_filename = magic_enum-v0.9.7.tar.gz +source_hash = b403d3dad4ef542fdc3024fa37d3a6cedb4ad33c72e31b6d9bab89dcaf69edf7 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/magic_enum_0.9.7-1/magic_enum-v0.9.7.tar.gz +wrapdb_version = 0.9.7-1 [provide] magic_enum = magic_enum_dep diff --git a/subprojects/packagefiles/fmt_emscripten.diff b/subprojects/packagefiles/fmt_emscripten.diff new file mode 100644 index 000000000..341f2855b --- /dev/null +++ b/subprojects/packagefiles/fmt_emscripten.diff @@ -0,0 +1,13 @@ +diff --git a/include/fmt/base.h b/include/fmt/base.h +index 6276494..869e075 100644 +--- a/include/fmt/base.h ++++ b/include/fmt/base.h +@@ -113,6 +113,8 @@ + // Detect consteval, C++20 constexpr extensions and std::is_constant_evaluated. + #if !defined(__cpp_lib_is_constant_evaluated) + # define FMT_USE_CONSTEVAL 0 ++#elif defined(__EMSCRIPTEN__) ++# define FMT_USE_CONSTEVAL 0 + #elif FMT_CPLUSPLUS < 201709L + # define FMT_USE_CONSTEVAL 0 + #elif FMT_GLIBCXX_RELEASE && FMT_GLIBCXX_RELEASE < 10 diff --git a/subprojects/packagefiles/magic_enum-0.9.6_installed.diff b/subprojects/packagefiles/magic_enum-0.9.6_installed.diff deleted file mode 100644 index e72b02b3f..000000000 --- a/subprojects/packagefiles/magic_enum-0.9.6_installed.diff +++ /dev/null @@ -1,37 +0,0 @@ -diff --git a/meson.build b/meson.build -index 207c834..9fe13a7 100644 ---- a/meson.build -+++ b/meson.build -@@ -4,7 +4,7 @@ project( - version: '0.9.6', - ) - --magic_enum_include = include_directories('include/magic_enum') -+magic_enum_include = include_directories('include') - - magic_enum_args = [] - -@@ -17,6 +17,23 @@ magic_enum_dep = declare_dependency( - compile_args: magic_enum_args, - ) - -+install_headers( -+ files( -+ 'include/magic_enum/magic_enum.hpp', -+ 'include/magic_enum/magic_enum_all.hpp', -+ 'include/magic_enum/magic_enum_containers.hpp', -+ 'include/magic_enum/magic_enum_flags.hpp', -+ 'include/magic_enum/magic_enum_format.hpp', -+ 'include/magic_enum/magic_enum_fuse.hpp', -+ 'include/magic_enum/magic_enum_iostream.hpp', -+ 'include/magic_enum/magic_enum_switch.hpp', -+ 'include/magic_enum/magic_enum_utility.hpp', -+ ), -+ subdir: 'magic_enum', -+) -+ - if get_option('test') - subdir('test') - endif -+ -+ diff --git a/subprojects/utfcpp.wrap b/subprojects/utfcpp.wrap index 1416c2c13..7d6b2c7df 100644 --- a/subprojects/utfcpp.wrap +++ b/subprojects/utfcpp.wrap @@ -1,13 +1,13 @@ [wrap-file] -directory = utfcpp-4.0.5 -source_url = https://github.com/nemtrif/utfcpp/archive/refs/tags/v4.0.5.tar.gz -source_filename = utfcpp-4.0.5.tar.gz -source_hash = ffc668a310e77607d393f3c18b32715f223da1eac4c4d6e0579a11df8e6b59cf -patch_filename = utfcpp_4.0.5-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/utfcpp_4.0.5-1/get_patch -patch_hash = bedf83d77b07a2fb84582722aaf748498cab0267b023ff8460dae3730a2d0819 -source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/utfcpp_4.0.5-1/utfcpp-4.0.5.tar.gz -wrapdb_version = 4.0.5-1 +directory = utfcpp-4.0.6 +source_url = https://github.com/nemtrif/utfcpp/archive/refs/tags/v4.0.6.tar.gz +source_filename = utfcpp-4.0.6.tar.gz +source_hash = 6920a6a5d6a04b9a89b2a89af7132f8acefd46e0c2a7b190350539e9213816c0 +patch_filename = utfcpp_4.0.6-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/utfcpp_4.0.6-1/get_patch +patch_hash = 49fac3078123b02019498cb58cd6d27df789ea831400b328360cca6533c3b6af +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/utfcpp_4.0.6-1/utfcpp-4.0.6.tar.gz +wrapdb_version = 4.0.6-1 [provide] utf8cpp = utfcpp_dep diff --git a/tools/dependencies/meson.build b/tools/dependencies/meson.build index d621887eb..b3827400d 100644 --- a/tools/dependencies/meson.build +++ b/tools/dependencies/meson.build @@ -28,7 +28,109 @@ if meson.is_cross_build() # language: ['cpp'], # ) # endif + elif host_machine.system() == 'android' + # noop + elif host_machine.system() == 'emscripten' + + # check if the command line flags are supported and a simple example compiles + can_compile = cpp.compiles( + ''' + int main() { + return 0; + } + ''', + ) + + if not can_compile + error('Not all Emscripten flags are supported: see logs') + endif + + only_allow_native_libs = true + + ## map native libraries to dependencies + map_native_dependencies = [ + ['SDL2-mt', 'SDL2'], + ['SDL2_ttf'], + ['mpg123'], + ['SDL2_mixer_mp3', 'SDL2_mixer'], + ['SDL2_image_png-svg-mt', 'SDL2_image'], + ['icu_common-mt', 'icu-uc'], + ] + foreach native_dependency_tuple : map_native_dependencies + native_dep_lib_name = native_dependency_tuple[0] + + native_dep_name = native_dependency_tuple.length() == 2 ? native_dependency_tuple[1] : native_dep_lib_name + + native_dep = cpp.find_library(native_dep_lib_name, required: true) + + if native_dep_name == 'SDL2' + + major_version = cpp.get_define( + 'SDL_MAJOR_VERSION', + prefix: '#include ', + ).strip('"') + assert( + major_version != '', + 'failed to get major_version from SDL_version.h', + ) + + minor_version = cpp.get_define( + 'SDL_MINOR_VERSION', + prefix: '#include ', + ).strip('"') + assert( + minor_version != '', + 'failed to get minor_version from SDL_version.h', + ) + + patch_version = cpp.get_define( + 'SDL_PATCHLEVEL', + prefix: '#include ', + ).strip('"') + assert( + patch_version != '', + 'failed to get patch_version from SDL_version.h', + ) + + native_dep_with_version = declare_dependency( + dependencies: native_dep, + version: major_version + + '.' + + minor_version + + '.' + + patch_version, + ) + meson.override_dependency(native_dep_name, native_dep_with_version) + else + meson.override_dependency(native_dep_name, native_dep) + + endif + + endforeach + native_deps = [] + + native_dep_names = [ + 'embind', + 'embind-rtti', + 'freetype', + 'harfbuzz-mt', + 'z', + 'mpg123', + 'png-mt', + 'fetch-mt', + ] + + foreach native_dep_name : native_dep_names + native_deps += cpp.find_library(native_dep_name, required: true) + endforeach + + graphics_lib += { + 'deps': [graphics_lib.get('deps'), native_deps], + } + + else + error('Unhandled cross built system: ' + host_machine.system()) endif endif @@ -254,15 +356,30 @@ if build_application }, ) - online_multiplayer_user_fallback = false + online_multiplayer_system = 'httplib' if cpp_httlib_dep.found() - graphics_lib += {'deps': [graphics_lib.get('deps'), cpp_httlib_dep]} + graphics_lib += { + 'deps': [graphics_lib.get('deps'), cpp_httlib_dep], + 'compile_args': [ + graphics_lib.get('compile_args'), + '-D_OOPETRIS_ONLINE_SYSTEM=0', + ], + } + + elif meson.is_cross_build() and host_machine.system() == 'emscripten' + online_multiplayer_system = 'web' + graphics_lib += { + 'compile_args': [ + graphics_lib.get('compile_args'), + '-D_OOPETRIS_ONLINE_SYSTEM=1', + ], + } else - online_multiplayer_user_fallback = true + online_multiplayer_system = 'curl' curl_cpp_wrapper = dependency( 'cpr', @@ -274,7 +391,7 @@ if build_application 'deps': [graphics_lib.get('deps'), curl_cpp_wrapper], 'compile_args': [ graphics_lib.get('compile_args'), - '-D_OOPETRIS_ONLINE_USE_CURL', + '-D_OOPETRIS_ONLINE_SYSTEM=2', ], } diff --git a/tools/options/meson.build b/tools/options/meson.build index 02a77ef8a..9032fdfd1 100644 --- a/tools/options/meson.build +++ b/tools/options/meson.build @@ -94,7 +94,8 @@ elif cpp.get_id() == 'clang' # TODO: once clang with libstdc++ (gcc c++ stdlib) supports std::expected, remove this special behaviour allow_tl_expected_fallback = true endif - +elif cpp.get_id() == 'emscripten' + allow_tl_expected_fallback = true endif build_application = true