diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a91eb19..8a65ae81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,7 @@ jobs: - { name: 'msys2 mingw32', os: 'windows-latest', dx5: false, config: false, build-type: 'Debug', mingw: true, werror: true, clang-tidy: true, msystem: 'mingw32', msys-env: 'mingw-w64-i686', shell: 'msys2 {0}' } - { name: 'msys2 mingw64', os: 'windows-latest', dx5: false, config: true, build-type: 'Debug', mingw: true, werror: true, clang-tidy: true, msystem: 'mingw64', msys-env: 'mingw-w64-x86_64', shell: 'msys2 {0}' } - { name: 'macOS', os: 'macos-latest', dx5: false, config: true, build-type: 'Debug', brew: true, werror: true, clang-tidy: false } + - { name: 'Emscripten', os: 'ubuntu-latest', dx5: false, config: false, build-type: 'Debug', emsdk: true, werror: true, clang-tidy: false, cmake-wrapper: 'emcmake' } steps: - name: Setup vcvars if: ${{ !!matrix.msvc }} @@ -74,6 +75,10 @@ jobs: brew install cmake ninja llvm qt6 echo "LLVM_ROOT=$(brew --prefix llvm)/bin" >> $GITHUB_ENV + - name: Setup Emscripten + uses: mymindstorm/setup-emsdk@master + if: ${{ matrix.emsdk }} + - name: Setup ninja if: ${{ matrix.msvc }} uses: ashutoshvarma/setup-ninja@master @@ -82,7 +87,7 @@ jobs: - name: Configure (CMake) run: | - cmake -S . -B build -GNinja \ + ${{ matrix.cmake-wrapper || '' }} cmake -S . -B build -GNinja \ -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} \ -DISLE_USE_DX5=${{ !!matrix.dx5 }} \ -DISLE_BUILD_CONFIG=${{ matrix.config }} \ diff --git a/CMakeLists.txt b/CMakeLists.txt index 0ac7a25f..55007410 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,12 @@ cmake_minimum_required(VERSION 3.25...4.0 FATAL_ERROR) project(isle LANGUAGES CXX C VERSION 0.1) +if (EMSCRIPTEN) + add_compile_options(-pthread) + add_link_options(-sALLOW_MEMORY_GROWTH=1 -sMAXIMUM_MEMORY=2gb -sUSE_PTHREADS=1 -sPROXY_TO_PTHREAD=1 -sPTHREAD_POOL_SIZE_STRICT=0 -sFORCE_FILESYSTEM=1 -sWASMFS=1 -sEXIT_RUNTIME=1) + set(SDL_PTHREADS ON CACHE BOOL "Enable SDL pthreads" FORCE) +endif() + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") include(CheckCXXSourceCompiles) @@ -34,6 +40,8 @@ option(CMAKE_POSITION_INDEPENDENT_CODE "Build with -fPIC" ON) option(ENABLE_CLANG_TIDY "Enable clang-tidy") option(DOWNLOAD_DEPENDENCIES "Download dependencies" ON) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" CACHE PATH "Directory where to put executables and dll") +set(ISLE_EMSCRIPTEN_HOST "" CACHE STRING "Host URL for Emscripten streaming (e.g., https://test.com)") +cmake_dependent_option(BUILD_SHARED_LIBS "Build lego1 as a shared library" ON "NOT EMSCRIPTEN" OFF) message(STATUS "Isle app: ${ISLE_BUILD_APP}") message(STATUS "Config app: ${ISLE_BUILD_CONFIG}") @@ -59,9 +67,11 @@ if (DOWNLOAD_DEPENDENCIES) GIT_TAG "main" EXCLUDE_FROM_ALL ) - set(BUILD_DOCS off) - set(BUILD_SHARED_LIBS off) - FetchContent_MakeAvailable(iniparser) + block() + set(BUILD_DOCS off) + set(BUILD_SHARED_LIBS off) + FetchContent_MakeAvailable(iniparser) + endblock() else() # find_package looks for already-installed system packages. # Configure with `-DCMAKE_PREFIX_PATH="/path/to/package1;/path/to/package2"` @@ -129,7 +139,7 @@ target_link_directories(DirectX5::DirectX5 INTERFACE "${CMAKE_SOURCE_DIR}/3rdpar add_library(Vec::Vec INTERFACE IMPORTED) target_include_directories(Vec::Vec INTERFACE "${CMAKE_SOURCE_DIR}/3rdparty/vec") -add_library(lego1 SHARED +add_library(lego1 LEGO1/main.cpp ) target_precompile_headers(lego1 PRIVATE "LEGO1/lego1_pch.h") @@ -497,6 +507,15 @@ if (ISLE_BUILD_APP) target_include_directories(isle PRIVATE ${valgrind_INCLUDE_PATH}) endif() endif() + if(EMSCRIPTEN) + target_sources(isle PRIVATE + ISLE/emscripten/events.cpp + ISLE/emscripten/filesystem.cpp + ISLE/emscripten/messagebox.cpp + ) + target_compile_definitions(isle PRIVATE "ISLE_EMSCRIPTEN_HOST=\"${ISLE_EMSCRIPTEN_HOST}\"") + set_property(TARGET isle PROPERTY SUFFIX ".html") + endif() endif() if (ISLE_BUILD_CONFIG) @@ -602,10 +621,18 @@ else() endif() set(ISLE_PACKAGE_NAME "${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}" CACHE STRING "Platform name of the package") -install(TARGETS isle lego1 ${install_extra_targets} +if(BUILD_SHARED_LIBS) + list(APPEND install_extra_targets lego1) +endif() +install(TARGETS isle ${install_extra_targets} RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" ) +if(EMSCRIPTEN) + install(FILES "$/isle.js" "$/isle.wasm" + DESTINATION "${CMAKE_INSTALL_BINDIR}" + ) +endif() set(CPACK_PACKAGE_DIRECTORY "dist") set(CPACK_PACKAGE_FILE_NAME "isle-${PROJECT_VERSION}-${ISLE_PACKAGE_NAME}-${CMAKE_SYSTEM_PROCESSOR}") diff --git a/ISLE/emscripten/events.cpp b/ISLE/emscripten/events.cpp new file mode 100644 index 00000000..67139dc6 --- /dev/null +++ b/ISLE/emscripten/events.cpp @@ -0,0 +1,38 @@ +#include "events.h" + +#include "mxdsaction.h" + +#include + +// clang-format off +void Emscripten_SendEvent(const char* p_event, const char* p_json) +{ + MAIN_THREAD_EM_ASM({ + const eventName = UTF8ToString($0); + let eventDetail = {}; + + if ($1 && UTF8ToString($1).length > 0) { + eventDetail = JSON.parse(UTF8ToString($1)); + } + + const targetElement = Module.canvas || window; + const event = new CustomEvent(eventName, {detail : eventDetail}); + targetElement.dispatchEvent(event); + }, p_event, p_json); +} +// clang-format on + +void Emscripten_SendPresenterProgress(MxPresenter* p_presenter, MxPresenter::TickleState p_tickleState) +{ + char buf[128]; + SDL_snprintf( + buf, + sizeof(buf), + "{\"objectId\": %d, \"objectName\": \"%s\", \"tickleState\": %d}", + p_presenter->GetAction() ? p_presenter->GetAction()->GetObjectId() : 0, + p_presenter->GetAction() ? p_presenter->GetAction()->GetObjectName() : "", + p_tickleState + ); + + Emscripten_SendEvent("presenterProgress", buf); +} diff --git a/ISLE/emscripten/events.h b/ISLE/emscripten/events.h new file mode 100644 index 00000000..cf0b2ed6 --- /dev/null +++ b/ISLE/emscripten/events.h @@ -0,0 +1,8 @@ +#ifndef EMSCRIPTEN_EVENTS_H +#define EMSCRIPTEN_EVENTS_H + +#include "mxpresenter.h" + +void Emscripten_SendPresenterProgress(MxPresenter* p_presenter, MxPresenter::TickleState p_tickleState); + +#endif // EMSCRIPTEN_EVENTS_H diff --git a/ISLE/emscripten/filesystem.cpp b/ISLE/emscripten/filesystem.cpp new file mode 100644 index 00000000..03b5b582 --- /dev/null +++ b/ISLE/emscripten/filesystem.cpp @@ -0,0 +1,127 @@ +#include "filesystem.h" + +#include "legogamestate.h" +#include "misc.h" +#include "mxomni.h" + +#include +#include +#include + +static backend_t opfs = nullptr; +static backend_t fetchfs = nullptr; + +void Emscripten_SetupConfig(const char* p_iniConfig) +{ + if (!p_iniConfig || !*p_iniConfig) { + return; + } + + opfs = wasmfs_create_opfs_backend(); + MxString iniConfig = p_iniConfig; + + char* parse = iniConfig.GetData(); + while ((parse = SDL_strchr(++parse, '/'))) { + *parse = '\0'; + wasmfs_create_directory(iniConfig.GetData(), 0644, opfs); + *parse = '/'; + } +} + +void Emscripten_SetupFilesystem() +{ + fetchfs = wasmfs_create_fetch_backend((MxString(Emscripten_streamHost) + MxString("/LEGO")).GetData(), 512 * 1024); + + wasmfs_create_directory("/LEGO", 0644, fetchfs); + wasmfs_create_directory("/LEGO/Scripts", 0644, fetchfs); + wasmfs_create_directory("/LEGO/Scripts/Act2", 0644, fetchfs); + wasmfs_create_directory("/LEGO/Scripts/Act3", 0644, fetchfs); + wasmfs_create_directory("/LEGO/Scripts/Build", 0644, fetchfs); + wasmfs_create_directory("/LEGO/Scripts/Garage", 0644, fetchfs); + wasmfs_create_directory("/LEGO/Scripts/Hospital", 0644, fetchfs); + wasmfs_create_directory("/LEGO/Scripts/Infocntr", 0644, fetchfs); + wasmfs_create_directory("/LEGO/Scripts/Isle", 0644, fetchfs); + wasmfs_create_directory("/LEGO/Scripts/Police", 0644, fetchfs); + wasmfs_create_directory("/LEGO/Scripts/Race", 0644, fetchfs); + wasmfs_create_directory("/LEGO/data", 0644, fetchfs); + + const auto registerFile = [](const char* p_path) { + MxString path = MxString(Emscripten_bundledPath) + MxString(p_path); + path.MapPathToFilesystem(); + + if (SDL_GetPathInfo(path.GetData(), NULL)) { + SDL_Log("File %s is bundled and won't be streamed", p_path); + } + else { + wasmfs_create_file(p_path, 0644, fetchfs); + MxOmni::GetCDFiles().emplace_back(p_path); + + SDL_Log("File %s set up for streaming", p_path); + } + }; + + registerFile("/LEGO/Scripts/CREDITS.SI"); + registerFile("/LEGO/Scripts/INTRO.SI"); + registerFile("/LEGO/Scripts/NOCD.SI"); + registerFile("/LEGO/Scripts/SNDANIM.SI"); + registerFile("/LEGO/Scripts/Act2/ACT2MAIN.SI"); + registerFile("/LEGO/Scripts/Act3/ACT3.SI"); + registerFile("/LEGO/Scripts/Build/COPTER.SI"); + registerFile("/LEGO/Scripts/Build/DUNECAR.SI"); + registerFile("/LEGO/Scripts/Build/JETSKI.SI"); + registerFile("/LEGO/Scripts/Build/RACECAR.SI"); + registerFile("/LEGO/Scripts/Garage/GARAGE.SI"); + registerFile("/LEGO/Scripts/Hospital/HOSPITAL.SI"); + registerFile("/LEGO/Scripts/Infocntr/ELEVBOTT.SI"); + registerFile("/LEGO/Scripts/Infocntr/HISTBOOK.SI"); + registerFile("/LEGO/Scripts/Infocntr/INFODOOR.SI"); + registerFile("/LEGO/Scripts/Infocntr/INFOMAIN.SI"); + registerFile("/LEGO/Scripts/Infocntr/INFOSCOR.SI"); + registerFile("/LEGO/Scripts/Infocntr/REGBOOK.SI"); + registerFile("/LEGO/Scripts/Isle/ISLE.SI"); + registerFile("/LEGO/Scripts/Isle/JUKEBOX.SI"); + registerFile("/LEGO/Scripts/Isle/JUKEBOXW.SI"); + registerFile("/LEGO/Scripts/Police/POLICE.SI"); + registerFile("/LEGO/Scripts/Race/CARRACE.SI"); + registerFile("/LEGO/Scripts/Race/CARRACER.SI"); + registerFile("/LEGO/Scripts/Race/JETRACE.SI"); + registerFile("/LEGO/Scripts/Race/JETRACER.SI"); + registerFile("/LEGO/data/ACT1INF.DTA"); + registerFile("/LEGO/data/ACT2INF.DTA"); + registerFile("/LEGO/data/ACT3INF.DTA"); + registerFile("/LEGO/data/BLDDINF.DTA"); + registerFile("/LEGO/data/BLDHINF.DTA"); + registerFile("/LEGO/data/BLDJINF.DTA"); + registerFile("/LEGO/data/BLDRINF.DTA"); + registerFile("/LEGO/data/GMAININF.DTA"); + registerFile("/LEGO/data/HOSPINF.DTA"); + registerFile("/LEGO/data/ICUBEINF.DTA"); + registerFile("/LEGO/data/IELEVINF.DTA"); + registerFile("/LEGO/data/IISLEINF.DTA"); + registerFile("/LEGO/data/IMAININF.DTA"); + registerFile("/LEGO/data/IREGINF.DTA"); + registerFile("/LEGO/data/OBSTINF.DTA"); + registerFile("/LEGO/data/PMAININF.DTA"); + registerFile("/LEGO/data/RACCINF.DTA"); + registerFile("/LEGO/data/RACJINF.DTA"); + registerFile("/LEGO/data/WORLD.WDB"); + registerFile("/LEGO/data/testinf.dta"); + + if (GameState()->GetSavePath() && *GameState()->GetSavePath()) { + if (!opfs) { + opfs = wasmfs_create_opfs_backend(); + } + + MxString savePath = GameState()->GetSavePath(); + if (savePath.GetData()[savePath.GetLength() - 1] != '/') { + savePath += "/"; + } + + char* parse = savePath.GetData(); + while ((parse = SDL_strchr(++parse, '/'))) { + *parse = '\0'; + wasmfs_create_directory(savePath.GetData(), 0644, opfs); + *parse = '/'; + } + } +} diff --git a/ISLE/emscripten/filesystem.h b/ISLE/emscripten/filesystem.h new file mode 100644 index 00000000..131df1c8 --- /dev/null +++ b/ISLE/emscripten/filesystem.h @@ -0,0 +1,16 @@ +#ifndef EMSCRIPTEN_FILESYSTEM_H +#define EMSCRIPTEN_FILESYSTEM_H + +#ifndef ISLE_EMSCRIPTEN_HOST +#define ISLE_EMSCRIPTEN_HOST "" +#endif + +inline static const char* Emscripten_bundledPath = "/bundled"; +inline static const char* Emscripten_savePath = "/save"; +inline static const char* Emscripten_streamPath = "/"; +inline static const char* Emscripten_streamHost = ISLE_EMSCRIPTEN_HOST; + +void Emscripten_SetupConfig(const char* p_iniConfig); +void Emscripten_SetupFilesystem(); + +#endif // EMSCRIPTEN_FILESYSTEM_H diff --git a/ISLE/emscripten/messagebox.cpp b/ISLE/emscripten/messagebox.cpp new file mode 100644 index 00000000..4977554c --- /dev/null +++ b/ISLE/emscripten/messagebox.cpp @@ -0,0 +1,14 @@ +#include "messagebox.h" + +#include + +bool Emscripten_ShowSimpleMessageBox( + SDL_MessageBoxFlags flags, + const char* title, + const char* message, + SDL_Window* window +) +{ + MAIN_THREAD_EM_ASM({alert(UTF8ToString($0) + "\n\n" + UTF8ToString($1))}, title, message); + return true; +} diff --git a/ISLE/emscripten/messagebox.h b/ISLE/emscripten/messagebox.h new file mode 100644 index 00000000..a944c64f --- /dev/null +++ b/ISLE/emscripten/messagebox.h @@ -0,0 +1,13 @@ +#ifndef EMSCRIPTEN_MESSAGE_BOX_H +#define EMSCRIPTEN_MESSAGE_BOX_H + +#include + +bool Emscripten_ShowSimpleMessageBox( + SDL_MessageBoxFlags flags, + const char* title, + const char* message, + SDL_Window* window +); + +#endif // EMSCRIPTEN_MESSAGE_BOX_H diff --git a/ISLE/isleapp.cpp b/ISLE/isleapp.cpp index 6a8068f2..be4842e2 100644 --- a/ISLE/isleapp.cpp +++ b/ISLE/isleapp.cpp @@ -44,6 +44,12 @@ #include #include +#ifdef __EMSCRIPTEN__ +#include "emscripten/events.h" +#include "emscripten/filesystem.h" +#include "emscripten/messagebox.h" +#endif + DECOMP_SIZE_ASSERT(IsleApp, 0x8c) // GLOBAL: ISLE 0x410030 @@ -88,7 +94,11 @@ IsleApp::IsleApp() m_cdPath = NULL; m_deviceId = NULL; m_savePath = NULL; +#ifdef __EMSCRIPTEN__ + m_fullScreen = FALSE; +#else m_fullScreen = TRUE; +#endif m_flipSurfaces = FALSE; m_backBuffersInVram = TRUE; m_using8bit = FALSE; @@ -100,7 +110,7 @@ IsleApp::IsleApp() m_useJoystick = FALSE; m_joystickIndex = 0; m_wideViewAngle = TRUE; - m_islandQuality = 1; + m_islandQuality = 2; m_islandTexture = 1; m_gameStarted = FALSE; m_frameDelta = 10; @@ -246,6 +256,9 @@ SDL_AppResult SDL_AppInit(void** appstate, int argc, char** argv) { *appstate = NULL; + SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"); + SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"); + if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_JOYSTICK)) { char buffer[256]; SDL_snprintf( @@ -254,7 +267,7 @@ SDL_AppResult SDL_AppInit(void** appstate, int argc, char** argv) "\"LEGO® Island\" failed to start.\nPlease quit all other applications and try again.\nSDL error: %s", SDL_GetError() ); - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "LEGO® Island Error", buffer, NULL); + Any_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "LEGO® Island Error", buffer, NULL); return SDL_APP_FAILURE; } @@ -266,7 +279,7 @@ SDL_AppResult SDL_AppInit(void** appstate, int argc, char** argv) g_isle = new IsleApp(); if (g_isle->ParseArguments(argc, argv) != SUCCESS) { - SDL_ShowSimpleMessageBox( + Any_ShowSimpleMessageBox( SDL_MESSAGEBOX_ERROR, "LEGO® Island Error", "\"LEGO® Island\" failed to start. Invalid CLI arguments.", @@ -277,7 +290,7 @@ SDL_AppResult SDL_AppInit(void** appstate, int argc, char** argv) // Create window if (g_isle->SetupWindow() != SUCCESS) { - SDL_ShowSimpleMessageBox( + Any_ShowSimpleMessageBox( SDL_MESSAGEBOX_ERROR, "LEGO® Island Error", "\"LEGO® Island\" failed to start.\nPlease quit all other applications and try again.", @@ -288,6 +301,20 @@ SDL_AppResult SDL_AppInit(void** appstate, int argc, char** argv) // Get reference to window *appstate = g_isle->GetWindowHandle(); + +#ifdef __EMSCRIPTEN__ + SDL_AddEventWatch( + [](void* userdata, SDL_Event* event) -> bool { + if (event->type == SDL_EVENT_TERMINATING && g_isle && g_isle->GetGameStarted()) { + GameState()->Save(0); + return false; + } + + return true; + }, + NULL + ); +#endif return SDL_APP_CONTINUE; } @@ -298,7 +325,7 @@ SDL_AppResult SDL_AppIterate(void* appstate) } if (!g_isle->Tick()) { - SDL_ShowSimpleMessageBox( + Any_ShowSimpleMessageBox( SDL_MESSAGEBOX_ERROR, "LEGO® Island Error", "\"LEGO® Island\" failed to start.\nPlease quit all other applications and try again." @@ -324,7 +351,7 @@ SDL_AppResult SDL_AppIterate(void* appstate) if (g_mousedown && g_mousemoved && g_isle) { if (!g_isle->Tick()) { - SDL_ShowSimpleMessageBox( + Any_ShowSimpleMessageBox( SDL_MESSAGEBOX_ERROR, "LEGO® Island Error", "\"LEGO® Island\" failed to start.\nPlease quit all other applications and try again." @@ -357,6 +384,13 @@ SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) // Remaining functionality to be implemented: // WM_TIMER - use SDL_Timer functionality instead +#ifdef __EMSCRIPTEN__ + // Workaround for the fact we are getting both mouse & touch events on mobile devices running Emscripten. + // On desktops, we are only getting mouse events, but a touch device (pen_input) may also be present... + // See: https://github.com/libsdl-org/SDL/issues/13161 + static bool detectedTouchEvents = false; +#endif + switch (event->type) { case SDL_EVENT_MOUSE_MOTION: case SDL_EVENT_MOUSE_BUTTON_DOWN: @@ -376,9 +410,12 @@ SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) } break; case SDL_EVENT_WINDOW_FOCUS_LOST: - if (!IsleDebug_Enabled()) { + if (!IsleDebug_Enabled() && g_isle->GetGameStarted()) { g_isle->SetWindowActive(FALSE); Lego()->Pause(); +#ifdef __EMSCRIPTEN__ + GameState()->Save(0); +#endif } break; case SDL_EVENT_WINDOW_CLOSE_REQUESTED: @@ -400,6 +437,11 @@ SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) break; } case SDL_EVENT_MOUSE_MOTION: +#ifdef __EMSCRIPTEN__ + if (detectedTouchEvents) { + break; + } +#endif g_mousemoved = TRUE; if (InputManager()) { @@ -416,7 +458,30 @@ SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) VideoManager()->MoveCursor(Min((MxS32) event->motion.x, 639), Min((MxS32) event->motion.y, 479)); } break; + case SDL_EVENT_FINGER_MOTION: { +#ifdef __EMSCRIPTEN__ + detectedTouchEvents = true; +#endif + g_mousemoved = TRUE; + + float x = SDL_clamp(event->tfinger.x, 0, 1) * 640; + float y = SDL_clamp(event->tfinger.y, 0, 1) * 480; + + if (InputManager()) { + InputManager()->QueueEvent(c_notificationMouseMove, LegoEventNotificationParam::c_lButtonState, x, y, 0); + } + + if (g_isle->GetDrawCursor()) { + VideoManager()->MoveCursor(Min((MxS32) x, 639), Min((MxS32) y, 479)); + } + break; + } case SDL_EVENT_MOUSE_BUTTON_DOWN: +#ifdef __EMSCRIPTEN__ + if (detectedTouchEvents) { + break; + } +#endif g_mousedown = TRUE; if (InputManager()) { @@ -429,7 +494,32 @@ SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) ); } break; + case SDL_EVENT_FINGER_DOWN: { +#ifdef __EMSCRIPTEN__ + detectedTouchEvents = true; +#endif + g_mousedown = TRUE; + + float x = SDL_clamp(event->tfinger.x, 0, 1) * 640; + float y = SDL_clamp(event->tfinger.y, 0, 1) * 480; + + if (InputManager()) { + InputManager()->QueueEvent(c_notificationButtonDown, LegoEventNotificationParam::c_lButtonState, x, y, 0); + } + break; + } case SDL_EVENT_MOUSE_BUTTON_UP: +#ifdef __EMSCRIPTEN__ + if (detectedTouchEvents) { + // Abusing the fact (bug?) that we are always getting mouse events on Emscripten. + // This functionality should be enabled in a more general way with touch events, + // but SDL touch event's don't have a "double tap" indicator right now. + if (event->button.clicks == 2) { + InputManager()->QueueEvent(c_notificationKeyPress, SDLK_SPACE, 0, 0, SDLK_SPACE); + } + break; + } +#endif g_mousedown = FALSE; if (InputManager()) { @@ -442,6 +532,20 @@ SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) ); } break; + case SDL_EVENT_FINGER_UP: { +#ifdef __EMSCRIPTEN__ + detectedTouchEvents = true; +#endif + g_mousedown = FALSE; + + float x = SDL_clamp(event->tfinger.x, 0, 1) * 640; + float y = SDL_clamp(event->tfinger.y, 0, 1) * 480; + + if (InputManager()) { + InputManager()->QueueEvent(c_notificationButtonUp, 0, x, y, 0); + } + break; + } case SDL_EVENT_QUIT: return SDL_APP_SUCCESS; break; @@ -462,6 +566,23 @@ SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) break; } } + else if (event->user.type == g_legoSdlEvents.m_presenterProgress) { + MxPresenter* presenter = static_cast(event->user.data1); + MxDSAction* action = presenter->GetAction(); + MxPresenter::TickleState state = static_cast(event->user.code); + +#ifdef __EMSCRIPTEN__ + if (!g_isle->GetGameStarted()) { + Emscripten_SendPresenterProgress(presenter, state); + } +#endif + + if (!g_isle->GetGameStarted() && action && state == MxPresenter::e_ready && + !SDL_strncmp(action->GetObjectName(), "Lego_Smk", 8)) { + g_isle->SetGameStarted(TRUE); + SDL_Log("Game started"); + } + } return SDL_APP_CONTINUE; } @@ -565,6 +686,11 @@ MxResult IsleApp::SetupWindow() } GameState()->SetSavePath(m_savePath); + +#ifdef __EMSCRIPTEN__ + Emscripten_SetupFilesystem(); +#endif + GameState()->SerializePlayersInfo(LegoStorage::c_read); GameState()->SerializeScoreHistory(LegoStorage::c_read); @@ -631,6 +757,10 @@ bool IsleApp::LoadConfig() } SDL_Log("Reading configuration from \"%s\"", iniConfig); +#ifdef __EMSCRIPTEN__ + Emscripten_SetupConfig(iniConfig); +#endif + dictionary* dict = iniparser_load(iniConfig); // [library:config] @@ -687,12 +817,20 @@ bool IsleApp::LoadConfig() fclose(iniFP); } +#ifdef __EMSCRIPTEN__ + const char* hdPath = Emscripten_bundledPath; +#else const char* hdPath = iniparser_getstring(dict, "isle:diskpath", SDL_GetBasePath()); +#endif m_hdPath = new char[strlen(hdPath) + 1]; strcpy(m_hdPath, hdPath); MxOmni::SetHD(m_hdPath); +#ifdef __EMSCRIPTEN__ + const char* cdPath = Emscripten_streamPath; +#else const char* cdPath = iniparser_getstring(dict, "isle:cdpath", MxOmni::GetCD()); +#endif m_cdPath = new char[strlen(cdPath) + 1]; strcpy(m_cdPath, cdPath); MxOmni::SetCD(m_cdPath); @@ -739,7 +877,11 @@ bool IsleApp::LoadConfig() // [library:config] // The original game does not save any data if no savepath is given. // Instead, we use SDLs prefPath as a default fallback and always save data. +#ifdef __EMSCRIPTEN__ + const char* savePath = Emscripten_savePath; +#else const char* savePath = iniparser_getstring(dict, "isle:savepath", prefPath); +#endif m_savePath = new char[strlen(savePath) + 1]; strcpy(m_savePath, savePath); @@ -838,7 +980,6 @@ inline bool IsleApp::Tick() SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to open ISLE.si: Failed to start initial action"); return false; } - m_gameStarted = TRUE; } return true; @@ -881,7 +1022,6 @@ void IsleApp::SetupCursor(Cursor p_cursor) MxResult IsleApp::ParseArguments(int argc, char** argv) { - for (int i = 1, consumed; i < argc; i += consumed) { consumed = -1; @@ -902,6 +1042,7 @@ MxResult IsleApp::ParseArguments(int argc, char** argv) return FAILURE; } } + return SUCCESS; } diff --git a/ISLE/isleapp.h b/ISLE/isleapp.h index e35d56a6..b0b7805e 100644 --- a/ISLE/isleapp.h +++ b/ISLE/isleapp.h @@ -49,8 +49,10 @@ public: SDL_Cursor* GetCursorBusy() { return m_cursorBusy; } SDL_Cursor* GetCursorNo() { return m_cursorNo; } MxS32 GetDrawCursor() { return m_drawCursor; } + MxS32 GetGameStarted() { return m_gameStarted; } void SetWindowActive(MxS32 p_windowActive) { m_windowActive = p_windowActive; } + void SetGameStarted(MxS32 p_gameStarted) { m_gameStarted = p_gameStarted; } MxResult ParseArguments(int argc, char** argv); diff --git a/LEGO1/lego/legoomni/include/legogamestate.h b/LEGO1/lego/legoomni/include/legogamestate.h index 1191a867..850a8055 100644 --- a/LEGO1/lego/legoomni/include/legogamestate.h +++ b/LEGO1/lego/legoomni/include/legogamestate.h @@ -224,6 +224,8 @@ public: void FindLoadedAct(); void RegisterState(LegoState* p_state); + const char* GetSavePath() { return m_savePath; } + private: MxResult WriteVariable(LegoStorage* p_storage, MxVariableTable* p_from, const char* p_variableName); MxResult WriteEndOfVariables(LegoStorage* p_storage); diff --git a/LEGO1/lego/legoomni/include/legoinputmanager.h b/LEGO1/lego/legoomni/include/legoinputmanager.h index 2c847cab..7ccad404 100644 --- a/LEGO1/lego/legoomni/include/legoinputmanager.h +++ b/LEGO1/lego/legoomni/include/legoinputmanager.h @@ -143,6 +143,7 @@ public: MxBool FUN_1005cdf0(LegoEventNotificationParam& p_param); void GetKeyboardState(); MxResult GetNavigationKeyStates(MxU32& p_keyFlags); + MxResult GetNavigationTouchStates(MxU32& p_keyFlags); // SYNTHETIC: LEGO1 0x1005b8d0 // LegoInputManager::`scalar deleting destructor' diff --git a/LEGO1/lego/legoomni/include/legoutils.h b/LEGO1/lego/legoomni/include/legoutils.h index 0bc229cd..3862465f 100644 --- a/LEGO1/lego/legoomni/include/legoutils.h +++ b/LEGO1/lego/legoomni/include/legoutils.h @@ -4,10 +4,8 @@ #include "actionsfwd.h" #include "decomp.h" #include "extra.h" -#include "lego1_export.h" #include "mxtypes.h" -#include #ifdef MINIWIN #include "miniwin/windows.h" #else @@ -19,12 +17,6 @@ // name verified by BETA10 0x100d4054 #define DS_NOT_A_STREAM -1 -struct LegoSdlEvents { - Uint32 m_windowsMessage; -}; - -LEGO1_EXPORT extern LegoSdlEvents g_legoSdlEvents; - enum Cursor { e_cursorArrow = 0, e_cursorBusy, @@ -70,7 +62,6 @@ void PlayCamAnim(LegoPathActor* p_actor, MxBool p_unused, MxU32 p_location, MxBo void FUN_1003eda0(); MxBool RemoveFromCurrentWorld(const MxAtomId& p_atomId, MxS32 p_id); void EnableAnimations(MxBool p_enable); -void InitSdlEvents(); void SetAppCursor(Cursor p_cursor); MxBool FUN_1003ef60(); MxBool RemoveFromWorld(MxAtomId& p_entityAtom, MxS32 p_entityId, MxAtomId& p_worldAtom, MxS32 p_worldEntityId); diff --git a/LEGO1/lego/legoomni/src/common/legoutils.cpp b/LEGO1/lego/legoomni/src/common/legoutils.cpp index c07a8cb3..26948049 100644 --- a/LEGO1/lego/legoomni/src/common/legoutils.cpp +++ b/LEGO1/lego/legoomni/src/common/legoutils.cpp @@ -37,8 +37,6 @@ #include #include -LegoSdlEvents g_legoSdlEvents; - // FUNCTION: LEGO1 0x1003dd70 // FUNCTION: BETA10 0x100d3410 LegoROI* PickROI(MxLong p_x, MxLong p_y) @@ -568,17 +566,6 @@ void EnableAnimations(MxBool p_enable) AnimationManager()->FUN_100604d0(p_enable); } -void InitSdlEvents() -{ - static bool g_initialized = false; - - if (!g_initialized) { - g_initialized = true; - Uint32 event = SDL_RegisterEvents(1); - g_legoSdlEvents.m_windowsMessage = event + 0; - } -} - // FUNCTION: LEGO1 0x1003ef40 void SetAppCursor(Cursor p_cursor) { diff --git a/LEGO1/lego/legoomni/src/input/legoinputmanager.cpp b/LEGO1/lego/legoomni/src/input/legoinputmanager.cpp index 84edb919..4a852c6f 100644 --- a/LEGO1/lego/legoomni/src/input/legoinputmanager.cpp +++ b/LEGO1/lego/legoomni/src/input/legoinputmanager.cpp @@ -136,6 +136,8 @@ MxResult LegoInputManager::GetNavigationKeyStates(MxU32& p_keyFlags) keyFlags |= c_ctrl; } + GetNavigationTouchStates(keyFlags); + p_keyFlags = keyFlags; return SUCCESS; @@ -542,3 +544,45 @@ void LegoInputManager::EnableInputProcessing() g_unk0x100f31b0 = -1; g_unk0x100f31b4 = NULL; } + +MxResult LegoInputManager::GetNavigationTouchStates(MxU32& p_keyStates) +{ + int count; + SDL_TouchID* touchDevices = SDL_GetTouchDevices(&count); + + if (touchDevices) { + auto applyFingerNavigation = [&p_keyStates](SDL_TouchID p_touchId) { + int count; + SDL_Finger** fingers = SDL_GetTouchFingers(p_touchId, &count); + + if (fingers) { + for (int i = 0; i < count; i++) { + if (fingers[i]->y > 3.0 / 4.0) { + if (fingers[i]->x < 1.0 / 3.0) { + p_keyStates |= c_left; + } + else if (fingers[i]->x > 2.0 / 3.0) { + p_keyStates |= c_right; + } + else { + p_keyStates |= c_down; + } + } + else { + p_keyStates |= c_up; + } + } + + SDL_free(fingers); + } + }; + + for (int i = 0; i < count; i++) { + applyFingerNavigation(touchDevices[i]); + } + + SDL_free(touchDevices); + } + + return SUCCESS; +} diff --git a/LEGO1/lego/legoomni/src/main/legomain.cpp b/LEGO1/lego/legoomni/src/main/legomain.cpp index 4ccfabb8..3b99f0b5 100644 --- a/LEGO1/lego/legoomni/src/main/legomain.cpp +++ b/LEGO1/lego/legoomni/src/main/legomain.cpp @@ -281,8 +281,6 @@ MxResult LegoOmni::Create(MxOmniCreateParam& p_param) SetAppCursor(e_cursorBusy); m_gameState->SetCurrentAct(LegoGameState::e_act1); - InitSdlEvents(); - result = SUCCESS; done: diff --git a/LEGO1/mxdirectx/legodxinfo.cpp b/LEGO1/mxdirectx/legodxinfo.cpp index b9b1d340..38ced82d 100644 --- a/LEGO1/mxdirectx/legodxinfo.cpp +++ b/LEGO1/mxdirectx/legodxinfo.cpp @@ -204,7 +204,7 @@ int LegoDeviceEnumerate::GetBestDevice() // FUNCTION: BETA10 0x1011cf54 bool LegoDeviceEnumerate::SupportsSIMD() { -#if defined(__x86_64__) || defined(_M_X64) || defined(__aarch64__) || defined(_M_ARM64) +#if defined(__x86_64__) || defined(_M_X64) || defined(__aarch64__) || defined(_M_ARM64) || defined(__EMSCRIPTEN__) // All x86_64 and 64-bit ARM CPUs support at least SSE2 or NEON return true; #elif defined(__i386__) || defined(_M_IX86) diff --git a/LEGO1/omni/include/mxomni.h b/LEGO1/omni/include/mxomni.h index 38a077f8..0e6bd07c 100644 --- a/LEGO1/omni/include/mxomni.h +++ b/LEGO1/omni/include/mxomni.h @@ -43,8 +43,8 @@ public: LEGO1_EXPORT static void SetCD(const char* p_cd); LEGO1_EXPORT static void SetHD(const char* p_hd); LEGO1_EXPORT static void SetSound3D(MxBool p_use3dSound); - static const vector& GetHDFiles() { return g_hdFiles; } - static const vector& GetCDFiles() { return g_cdFiles; } + static vector& GetHDFiles() { return g_hdFiles; } + static vector& GetCDFiles() { return g_cdFiles; } MxOmni(); ~MxOmni() override; diff --git a/LEGO1/omni/include/mxpresenter.h b/LEGO1/omni/include/mxpresenter.h index e257f3c4..0c24881e 100644 --- a/LEGO1/omni/include/mxpresenter.h +++ b/LEGO1/omni/include/mxpresenter.h @@ -5,6 +5,9 @@ #include "mxcore.h" #include "mxcriticalsection.h" #include "mxgeometry.h" +#include "mxutilities.h" + +#include class MxCompositePresenter; class MxDSAction; @@ -62,6 +65,12 @@ protected: { m_previousTickleStates |= 1 << (MxU8) m_currentTickleState; m_currentTickleState = p_tickleState; + + SDL_Event event; + event.user.type = g_legoSdlEvents.m_presenterProgress; + event.user.code = m_currentTickleState; + event.user.data1 = (void*) this; + SDL_PushEvent(&event); } public: diff --git a/LEGO1/omni/include/mxutilities.h b/LEGO1/omni/include/mxutilities.h index b345ea79..c68d4201 100644 --- a/LEGO1/omni/include/mxutilities.h +++ b/LEGO1/omni/include/mxutilities.h @@ -1,10 +1,19 @@ #ifndef MXUTILITIES_H #define MXUTILITIES_H +#include "lego1_export.h" #include "mxtypes.h" +#include #include +struct LegoSdlEvents { + Uint32 m_windowsMessage; + Uint32 m_presenterProgress; +}; + +LEGO1_EXPORT extern LegoSdlEvents g_legoSdlEvents; + class MxDSFile; class MxDSObject; class MxDSAction; diff --git a/LEGO1/omni/src/common/mxutilities.cpp b/LEGO1/omni/src/common/mxutilities.cpp index 4d71062d..c17e41e5 100644 --- a/LEGO1/omni/src/common/mxutilities.cpp +++ b/LEGO1/omni/src/common/mxutilities.cpp @@ -12,6 +12,8 @@ #include #include +LegoSdlEvents g_legoSdlEvents; + // GLOBAL: LEGO1 0x101020e8 void (*g_omniUserMessage)(const char*, MxS32) = NULL; diff --git a/LEGO1/omni/src/main/mxomni.cpp b/LEGO1/omni/src/main/mxomni.cpp index 7c701c49..d7b76847 100644 --- a/LEGO1/omni/src/main/mxomni.cpp +++ b/LEGO1/omni/src/main/mxomni.cpp @@ -162,6 +162,12 @@ MxResult MxOmni::Create(MxOmniCreateParam& p_param) } } + { + Uint32 event = SDL_RegisterEvents(2); + g_legoSdlEvents.m_windowsMessage = event + 0; + g_legoSdlEvents.m_presenterProgress = event + 1; + } + result = SUCCESS; done: diff --git a/README.md b/README.md index 682a1f2c..fa1a8f4a 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Please note: this project is dedicated to achieving platform independence withou | Windows | [![CI](https://github.com/isledecomp/isle-portable/actions/workflows/ci.yml/badge.svg)](https://github.com/isledecomp/isle-portable/actions/workflows/ci.yml) | | Linux | [![CI](https://github.com/isledecomp/isle-portable/actions/workflows/ci.yml/badge.svg)](https://github.com/isledecomp/isle-portable/actions/workflows/ci.yml) | | macOS | [![CI](https://github.com/isledecomp/isle-portable/actions/workflows/ci.yml/badge.svg)](https://github.com/isledecomp/isle-portable/actions/workflows/ci.yml) | +| [Web](https://isle.pizza) | [![CI](https://github.com/isledecomp/isle-portable/actions/workflows/ci.yml/badge.svg)](https://github.com/isledecomp/isle-portable/actions/workflows/ci.yml) | ### Library substitutions diff --git a/miniwin/src/ddraw/ddraw.cpp b/miniwin/src/ddraw/ddraw.cpp index aeba3972..fc81ddd3 100644 --- a/miniwin/src/ddraw/ddraw.cpp +++ b/miniwin/src/ddraw/ddraw.cpp @@ -283,6 +283,7 @@ HRESULT DirectDrawImpl::RestoreDisplayMode() HRESULT DirectDrawImpl::SetCooperativeLevel(HWND hWnd, DDSCLFlags dwFlags) { SDL_Window* sdlWindow = reinterpret_cast(hWnd); + if (sdlWindow) { bool fullscreen; if ((dwFlags & DDSCL_NORMAL) == DDSCL_NORMAL) { @@ -296,7 +297,9 @@ HRESULT DirectDrawImpl::SetCooperativeLevel(HWND hWnd, DDSCLFlags dwFlags) } if (!SDL_SetWindowFullscreen(sdlWindow, fullscreen)) { +#ifndef __EMSCRIPTEN__ return DDERR_GENERIC; +#endif } DDWindow = sdlWindow; DDRenderer = SDL_CreateRenderer(DDWindow, NULL); diff --git a/util/compat.h b/util/compat.h index d3abca3e..e3b2aca9 100644 --- a/util/compat.h +++ b/util/compat.h @@ -17,6 +17,14 @@ #define DDBitDepths DWORD #endif +// SDL will not put the message box on the main thread by default. +// See: https://github.com/libsdl-org/SDL/issues/12943 +#ifdef __EMSCRIPTEN__ +#define Any_ShowSimpleMessageBox Emscripten_ShowSimpleMessageBox +#else +#define Any_ShowSimpleMessageBox SDL_ShowSimpleMessageBox +#endif + // Disable "identifier was truncated to '255' characters" warning. // Impossible to avoid this if using STL map or set. // This removes most (but not all) occurrences of the warning.