diff --git a/.gitattributes b/.gitattributes index 04926c6b..430dfc15 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,3 +9,5 @@ **/*.png binary **/*.svg text eol=lf **/*.desktop text eol=lf +assets/widescreen/** filter=lfs diff=lfs merge=lfs -text +assets/hdmusic/** filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0ae04c8..fe7ce886 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,10 +43,11 @@ jobs: - { name: 'msys2 mingw32', os: 'windows-latest', generator: 'Ninja', dx5: false, config: false, mingw: true, werror: true, clang-tidy: true, msystem: 'mingw32', msys-env: 'mingw-w64-i686', shell: 'msys2 {0}' } - { name: 'msys2 mingw64', os: 'windows-latest', generator: 'Ninja', dx5: false, config: true, mingw: true, werror: true, clang-tidy: true, msystem: 'mingw64', msys-env: 'mingw-w64-x86_64', shell: 'msys2 {0}' } - { name: 'macOS', os: 'macos-latest', generator: 'Ninja', dx5: false, config: true, brew: true, werror: true, clang-tidy: false } - - { name: 'iOS', os: 'macos-15', generator: 'Xcode', dx5: false, config: false, brew: true, werror: true, clang-tidy: false, cmake-args: '-DCMAKE_SYSTEM_NAME=iOS' } + - { name: 'iOS', os: 'macos-15', generator: 'Xcode', dx5: false, config: false, brew: true, werror: true, clang-tidy: false, cmake-args: '-DCMAKE_SYSTEM_NAME=iOS', ios: true } - { name: 'Emscripten', os: 'ubuntu-latest', generator: 'Ninja', dx5: false, config: false, emsdk: true, werror: true, clang-tidy: false, cmake-wrapper: 'emcmake' } - { name: 'Nintendo 3DS', os: 'ubuntu-latest', generator: 'Ninja', dx5: false, config: false, n3ds: true, werror: true, clang-tidy: false, container: 'devkitpro/devkitarm:latest', cmake-args: '-DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/3DS.cmake' } - { name: 'Xbox One', os: 'windows-latest', generator: 'Visual Studio 17 2022', dx5: false, config: false, msvc: true, werror: false, clang-tidy: false, vc-arch: 'amd64', cmake-args: '-DCMAKE_SYSTEM_NAME=WindowsStore -DCMAKE_SYSTEM_VERSION=10.0.26100.0', xbox-one: true} + - { name: 'Android', os: 'ubuntu-latest', generator: 'Ninja', dx5: false, config: false, android: true, werror: true, clang-tidy: false,} steps: - name: Setup vcvars if: ${{ !!matrix.msvc }} @@ -95,6 +96,12 @@ jobs: brew update brew install cmake ninja llvm qt6 echo "LLVM_ROOT=$(brew --prefix llvm)/bin" >> $GITHUB_ENV + + - name: Use latest Xcode + if: ${{ matrix.ios }} + uses: maxim-lobanov/setup-xcode@v1.6.0 + with: + xcode-version: latest-stable - name: Setup Emscripten uses: mymindstorm/setup-emsdk@master @@ -106,7 +113,46 @@ jobs: - uses: actions/checkout@v4 + - name: Checkout LFS + if: ${{ matrix.build-assets }} + run: | + git lfs pull + + - name: Setup Java (Android) + if: ${{ matrix.android }} + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Get CMake (Android) + if: ${{ matrix.android }} + uses: lukka/get-cmake@latest + with: + cmakeVersion: 3.30.5 + + - name: Build (Android) + if: ${{ matrix.android }} + env: + SIGNING_KEY_ALIAS: ${{ secrets.keyAlias }} + SIGNING_KEY_PASSWORD: ${{ secrets.keyPassword }} + SIGNING_STORE_PASSWORD: ${{ secrets.keystorePassword }} + SIGNING_STORE_FILE: ${{ github.workspace }}/release.keystore + run: | + echo "${{ secrets.keystore }}" | base64 -d > release.keystore + cd android-project + ./gradlew $([ -n "$SIGNING_KEY_ALIAS" ] && echo packageRelease || echo assembleDebug ) \ + --info \ + -PcmakeArgs="-DCMAKE_BUILD_TYPE=Release \ + -DISLE_USE_DX5=${{ !!matrix.dx5 }} \ + -DISLE_BUILD_CONFIG=${{ !!matrix.config }} \ + -DENABLE_CLANG_TIDY=${{ !!matrix.clang-tidy }} \ + -DISLE_WERROR=${{ !!matrix.werror }} \ + -DISLE_DEBUG=${{ matrix.debug || 'OFF' }} \ + -Werror=dev" + - name: Configure (CMake) + if: ${{ !matrix.android }} run: | ${{ matrix.cmake-wrapper || '' }} cmake -S . -B build -G "${{ matrix.generator }}" \ ${{ matrix.cmake-args || '' }} \ @@ -120,10 +166,11 @@ jobs: -Werror=dev - name: Build (CMake) + if: ${{ !matrix.android }} run: cmake --build build --verbose --config Release - name: Package (CPack) - if: ${{ !matrix.n3ds }} + if: ${{ !matrix.n3ds && !matrix.android }} run: | cd build success=0 @@ -172,6 +219,12 @@ jobs: mv *.3dsx dist/ mv *.cia dist/ + - name: Package (Android) + if: ${{ matrix.android }} + run: | + mkdir -p build/dist + mv android-project/app/build/outputs/apk/*/*.apk build/dist/ + - name: Package Assets Separately if: matrix.build-assets run: (cd build/assets && zip -r ../dist/isle-assets.zip .) @@ -185,6 +238,7 @@ jobs: build/dist/*.AppImage build/dist/*.3dsx build/dist/*.cia + build/dist/*.apk flatpak: name: "Flatpak (${{ matrix.arch }})" diff --git a/.gitignore b/.gitignore index 1610ba3d..a4bd8a41 100644 --- a/.gitignore +++ b/.gitignore @@ -6,12 +6,14 @@ Release/ /.idea .env .venv +.gradle env/ venv/ ENV/ VENV/ env.bak/ venv.bak/ +local.properties /build/ /build_debug/ /legobin/ diff --git a/.lfsconfig b/.lfsconfig new file mode 100644 index 00000000..98f14551 --- /dev/null +++ b/.lfsconfig @@ -0,0 +1,2 @@ +[lfs] + url = https://f9622702b3bbcac0705052a10e62a5a5:31adabe20fe975a919bd1909a6f598f37b0d6486225efa84b38da1aaf27cbd35@assets.isle.pizza/a70a70ae5ebee06d2333bb1132711de1.r2.cloudflarestorage.com/assets diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt index 28b5aa15..54259e63 100644 --- a/3rdparty/CMakeLists.txt +++ b/3rdparty/CMakeLists.txt @@ -78,8 +78,8 @@ if(DOWNLOAD_DEPENDENCIES) include(FetchContent) FetchContent_Populate( libweaver - URL https://github.com/isledecomp/SIEdit/archive/6da93b2072c41c41d526b8b9df7d4292be1f0f55.tar.gz - URL_MD5 ae59007fcb9efadc06c67621e1e107cb + URL https://github.com/isledecomp/SIEdit/archive/ae447259389f3bf8273c7e7a4844743faf7cbdb8.tar.gz + URL_MD5 dee68424fde8db6d5cef3b9034a8151f ) else() set(libweaver_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libweaver") diff --git a/3rdparty/libweaver b/3rdparty/libweaver index 6da93b20..ae447259 160000 --- a/3rdparty/libweaver +++ b/3rdparty/libweaver @@ -1 +1 @@ -Subproject commit 6da93b2072c41c41d526b8b9df7d4292be1f0f55 +Subproject commit ae447259389f3bf8273c7e7a4844743faf7cbdb8 diff --git a/CMakeLists.txt b/CMakeLists.txt index 345c646f..a9c9d85c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,22 +80,28 @@ if (DOWNLOAD_DEPENDENCIES) # FetchContent downloads and configures dependencies message(STATUS "Fetching SDL3 and iniparser. This might take a while...") include(FetchContent) - if (WINDOWS_STORE) - FetchContent_Declare( - SDL3 - GIT_REPOSITORY "https://github.com/Helloyunho/SDL3-uwp.git" - GIT_TAG "main" - EXCLUDE_FROM_ALL - ) + + if(ANDROID) + # Built by Gradle + find_package(SDL3 REQUIRED CONFIG COMPONENTS Shared) else() - FetchContent_Declare( + if (WINDOWS_STORE) + FetchContent_Declare( + SDL3 + GIT_REPOSITORY "https://github.com/Helloyunho/SDL3-uwp.git" + GIT_TAG "main" + EXCLUDE_FROM_ALL + ) + else() + FetchContent_Declare( SDL3 GIT_REPOSITORY "https://github.com/libsdl-org/SDL.git" GIT_TAG "main" EXCLUDE_FROM_ALL - ) + ) + endif() + FetchContent_MakeAvailable(SDL3) endif() - FetchContent_MakeAvailable(SDL3) FetchContent_Declare( iniparser @@ -512,6 +518,13 @@ if (ISLE_EXTENSIONS) endif() if (ISLE_BUILD_APP) + + if (ANDROID) + function(add_executable TARGET PLATFORM) + add_library(${TARGET} SHARED ${ARGN}) + endfunction() + endif() + add_executable(isle WIN32 ISLE/res/isle.rc ISLE/isleapp.cpp @@ -591,6 +604,11 @@ if (ISLE_BUILD_APP) ISLE/ios/config.cpp ) endif() + if (ANDROID) + target_sources(isle PRIVATE + ISLE/android/config.cpp + ) + endif() if(Python3_FOUND) if(NOT DEFINED PYTHON_PIL_AVAILABLE) execute_process( @@ -684,10 +702,6 @@ if(ISLE_BUILD_ASSETS) add_custom_target(build_assets ALL DEPENDS ${GENERATED_ASSETS_DIR}/.stamp ) - - install(DIRECTORY ${GENERATED_ASSETS_DIR}/ - DESTINATION assets - ) endif() if (ISLE_MINIWIN) @@ -757,7 +771,7 @@ endif() set(install_extra_targets) if(DOWNLOAD_DEPENDENCIES) get_property(sdl3_type TARGET SDL3::SDL3 PROPERTY TYPE) - if(sdl3_type STREQUAL "SHARED_LIBRARY") + if(sdl3_type STREQUAL "SHARED_LIBRARY" AND NOT ANDROID) list(APPEND install_extra_targets "SDL3-shared") endif() endif() diff --git a/ISLE/android/config.cpp b/ISLE/android/config.cpp new file mode 100644 index 00000000..4883a239 --- /dev/null +++ b/ISLE/android/config.cpp @@ -0,0 +1,29 @@ +#include "config.h" + +#include "mxstring.h" + +#include +#include +#include +#include + +void Android_SetupDefaultConfigOverrides(dictionary* p_dictionary) +{ + SDL_Log("Overriding default config for Android"); + + const char* data = SDL_GetAndroidExternalStoragePath(); + MxString savedata = MxString(data) + "/saves/"; + + if (!SDL_GetPathInfo(savedata.GetData(), NULL)) { + SDL_CreateDirectory(savedata.GetData()); + } + + iniparser_set(p_dictionary, "isle:diskpath", data); + iniparser_set(p_dictionary, "isle:cdpath", data); + iniparser_set(p_dictionary, "isle:mediapath", data); + iniparser_set(p_dictionary, "isle:savepath", savedata.GetData()); + + // Default to Virtual Mouse + char buf[16]; + iniparser_set(p_dictionary, "isle:Touch Scheme", SDL_itoa(0, buf, 10)); +} diff --git a/ISLE/android/config.h b/ISLE/android/config.h new file mode 100644 index 00000000..5b4ac3d0 --- /dev/null +++ b/ISLE/android/config.h @@ -0,0 +1,8 @@ +#ifndef ANDROID_CONFIG_H +#define ANDROID_CONFIG_H + +#include "dictionary.h" + +void Android_SetupDefaultConfigOverrides(dictionary* p_dictionary); + +#endif // ANDROID_CONFIG_H diff --git a/ISLE/ios/config.cpp b/ISLE/ios/config.cpp index ee9349fa..22a17657 100644 --- a/ISLE/ios/config.cpp +++ b/ISLE/ios/config.cpp @@ -1,5 +1,7 @@ #include "config.h" +#include "mxstring.h" + #include #include #include @@ -11,15 +13,13 @@ void IOS_SetupDefaultConfigOverrides(dictionary* p_dictionary) // Use DevelopmentFiles path for disk and cd paths // It's good to use that path since user can easily // connect through SMB and copy the files - const char* documentFolder = SDL_GetUserFolder(SDL_FOLDER_DOCUMENTS); - char* diskPath = new char[strlen(documentFolder) + strlen("isle") + 1](); - strcpy(diskPath, documentFolder); - strcat(diskPath, "isle"); + MxString documentFolder = SDL_GetUserFolder(SDL_FOLDER_DOCUMENTS); + documentFolder += "isle"; - if (!SDL_GetPathInfo(diskPath, NULL)) { - SDL_CreateDirectory(diskPath); + if (!SDL_GetPathInfo(documentFolder.GetData(), NULL)) { + SDL_CreateDirectory(documentFolder.GetData()); } - iniparser_set(p_dictionary, "isle:diskpath", diskPath); - iniparser_set(p_dictionary, "isle:cdpath", diskPath); + iniparser_set(p_dictionary, "isle:diskpath", documentFolder.GetData()); + iniparser_set(p_dictionary, "isle:cdpath", documentFolder.GetData()); } diff --git a/ISLE/isleapp.cpp b/ISLE/isleapp.cpp index 4cc98f77..24ad0a14 100644 --- a/ISLE/isleapp.cpp +++ b/ISLE/isleapp.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #define SDL_MAIN_USE_CALLBACKS @@ -72,6 +73,10 @@ #include "ios/config.h" #endif +#ifdef ANDROID +#include "android/config.h" +#endif + DECOMP_SIZE_ASSERT(IsleApp, 0x8c) // GLOBAL: ISLE 0x410030 @@ -168,6 +173,7 @@ IsleApp::IsleApp() LegoOmni::CreateInstance(); + m_mediaPath = NULL; m_iniPath = NULL; m_maxLod = RealtimeView::GetUserMaxLOD(); m_maxAllowedExtras = m_islandQuality <= 1 ? 10 : 20; @@ -194,25 +200,11 @@ IsleApp::~IsleApp() MxOmni::DestroyInstance(); } - if (m_hdPath) { - delete[] m_hdPath; - } - - if (m_cdPath) { - delete[] m_cdPath; - } - - if (m_deviceId) { - delete[] m_deviceId; - } - - if (m_savePath) { - delete[] m_savePath; - } - - if (m_mediaPath) { - delete[] m_mediaPath; - } + SDL_free(m_hdPath); + SDL_free(m_cdPath); + SDL_free(m_deviceId); + SDL_free(m_savePath); + SDL_free(m_mediaPath); } // FUNCTION: ISLE 0x401260 @@ -1029,33 +1021,33 @@ bool IsleApp::LoadConfig() const char* prefPath = SDL_GetUserFolder(SDL_FOLDER_DOCUMENTS); #elif defined(PS2) const char* prefPath = "mc0:ISLE/"; -#else - char* prefPath = SDL_GetPrefPath("isledecomp", "isle"); -#endif - char* iniConfig; - -#ifdef __EMSCRIPTEN__ +#elif defined(ANDROID) + MxString androidPath = MxString(SDL_GetAndroidExternalStoragePath()) + "/"; + const char* prefPath = androidPath.GetData(); +#elif defined(EMSCRIPTEN) if (m_iniPath && !Emscripten_SetupConfig(m_iniPath)) { m_iniPath = NULL; } + char* prefPath = SDL_GetPrefPath("isledecomp", "isle"); +#else + char* prefPath = SDL_GetPrefPath("isledecomp", "isle"); #endif + MxString iniConfig; if (m_iniPath) { - iniConfig = new char[strlen(m_iniPath) + 1]; - strcpy(iniConfig, m_iniPath); + iniConfig = m_iniPath; } else if (prefPath) { - iniConfig = new char[strlen(prefPath) + strlen("isle.ini") + 1](); - strcat(iniConfig, prefPath); - strcat(iniConfig, "isle.ini"); + iniConfig = prefPath; + iniConfig += "isle.ini"; } else { - iniConfig = new char[strlen("isle.ini") + 1]; - strcpy(iniConfig, "isle.ini"); + iniConfig = "isle.ini"; } - SDL_Log("Reading configuration from \"%s\"", iniConfig); - dictionary* dict = iniparser_load(iniConfig); + SDL_Log("Reading configuration from \"%s\"", iniConfig.GetData()); + + dictionary* dict = iniparser_load(iniConfig.GetData()); // [library:config] // Load sane defaults if dictionary failed to load @@ -1066,20 +1058,20 @@ bool IsleApp::LoadConfig() } SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loading sane defaults"); - FILE* iniFP = fopen(iniConfig, "wb"); + FILE* iniFP = fopen(iniConfig.GetData(), "wb"); if (!iniFP) { SDL_LogError( SDL_LOG_CATEGORY_APPLICATION, "Failed to write config at '%s': %s", - iniConfig, + iniConfig.GetData(), strerror(errno) ); return false; } char buf[32]; - dict = iniparser_load(iniConfig); + dict = iniparser_load(iniConfig.GetData()); iniparser_set(dict, "isle", NULL); iniparser_set(dict, "isle:diskpath", SDL_GetBasePath()); @@ -1131,8 +1123,12 @@ bool IsleApp::LoadConfig() #ifdef IOS IOS_SetupDefaultConfigOverrides(dict); #endif +#ifdef ANDROID + Android_SetupDefaultConfigOverrides(dict); +#endif + iniparser_dump_ini(dict, iniFP); - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "New config written at '%s'", iniConfig); + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "New config written at '%s'", iniConfig.GetData()); fclose(iniFP); } @@ -1140,20 +1136,10 @@ bool IsleApp::LoadConfig() Emscripten_SetupDefaultConfigOverrides(dict); #endif - const char* hdPath = iniparser_getstring(dict, "isle:diskpath", SDL_GetBasePath()); - m_hdPath = new char[strlen(hdPath) + 1]; - strcpy(m_hdPath, hdPath); - MxOmni::SetHD(m_hdPath); - - const char* cdPath = iniparser_getstring(dict, "isle:cdpath", MxOmni::GetCD()); - m_cdPath = new char[strlen(cdPath) + 1]; - strcpy(m_cdPath, cdPath); - MxOmni::SetCD(m_cdPath); - - const char* mediaPath = iniparser_getstring(dict, "isle:mediapath", hdPath); - m_mediaPath = new char[strlen(mediaPath) + 1]; - strcpy(m_mediaPath, mediaPath); - + MxOmni::SetHD((m_hdPath = SDL_strdup(iniparser_getstring(dict, "isle:diskpath", SDL_GetBasePath())))); + MxOmni::SetCD((m_cdPath = SDL_strdup(iniparser_getstring(dict, "isle:cdpath", MxOmni::GetCD())))); + m_savePath = SDL_strdup(iniparser_getstring(dict, "isle:savepath", prefPath)); + m_mediaPath = SDL_strdup(iniparser_getstring(dict, "isle:mediapath", m_hdPath)); m_flipSurfaces = iniparser_getboolean(dict, "isle:Flip Surfaces", m_flipSurfaces); m_fullScreen = iniparser_getboolean(dict, "isle:Full Screen", m_fullScreen); m_exclusiveFullScreen = iniparser_getboolean(dict, "isle:Exclusive Full Screen", m_exclusiveFullScreen); @@ -1200,17 +1186,9 @@ bool IsleApp::LoadConfig() const char* deviceId = iniparser_getstring(dict, "isle:3D Device ID", NULL); if (deviceId != NULL) { - m_deviceId = new char[strlen(deviceId) + 1]; - strcpy(m_deviceId, deviceId); + m_deviceId = SDL_strdup(deviceId); } - // [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. - const char* savePath = iniparser_getstring(dict, "isle:savepath", prefPath); - m_savePath = new char[strlen(savePath) + 1]; - strcpy(m_savePath, savePath); - #ifdef EXTENSIONS for (const char* key : Extensions::availableExtensions) { if (iniparser_getboolean(dict, key, 0)) { @@ -1230,11 +1208,12 @@ bool IsleApp::LoadConfig() #endif iniparser_freedict(dict); - delete[] iniConfig; -#if !defined(IOS) && !defined(PS2) - SDL_free(prefPath); -#endif + [](auto path) { + if constexpr (std::is_same_v) { + SDL_free(path); + } + }(prefPath); return true; } diff --git a/ISLE/isleapp.h b/ISLE/isleapp.h index 470a5a72..0e0700e8 100644 --- a/ISLE/isleapp.h +++ b/ISLE/isleapp.h @@ -101,7 +101,7 @@ class IsleApp { MxFloat m_cursorSensitivity; void DisplayArgumentHelp(const char* p_execName); - char* m_iniPath; + const char* m_iniPath; MxFloat m_maxLod; MxU32 m_maxAllowedExtras; MxTransitionManager::TransitionType m_transitionType; diff --git a/LEGO1/lego/legoomni/src/audio/mxbackgroundaudiomanager.cpp b/LEGO1/lego/legoomni/src/audio/mxbackgroundaudiomanager.cpp index 2822e95a..a5bb51b7 100644 --- a/LEGO1/lego/legoomni/src/audio/mxbackgroundaudiomanager.cpp +++ b/LEGO1/lego/legoomni/src/audio/mxbackgroundaudiomanager.cpp @@ -119,7 +119,12 @@ void MxBackgroundAudioManager::FadeInPendingPresenter() MxS32 compare, volume; if (m_activePresenter == NULL) { - if (m_pendingPresenter) { + if (m_pendingPresenter && m_pendingPresenter->GetCurrentTickleState() >= MxPresenter::e_streaming) { + if (!m_pendingPresenter->IsEnabled()) { + m_pendingPresenter->Enable(TRUE); + m_pendingPresenter->SetTickleState(MxPresenter::e_streaming); + } + if (m_volumeSuppressionAmount != 0) { compare = 30; } @@ -221,6 +226,12 @@ void MxBackgroundAudioManager::StartAction(MxParam& p_param) m_action2.SetObjectId(m_pendingPresenter->GetAction()->GetObjectId()); m_targetVolume = ((MxDSSound*) (m_pendingPresenter->GetAction()))->GetVolume(); m_pendingPresenter->SetVolume(0); + + // Disabling the action here and starting it later once the actively presented music has been faded out. + // This was not necessary in retail because the streaming layer would implicitly not start another action + // before the previous one has ended (since it's all coming from JUKEBOX.SI), however since we now + // allow loading music from multiple SI files this would cause the new music to start immediately. + m_pendingPresenter->GetAction()->SetFlags(m_pendingPresenter->GetAction()->GetFlags() & ~MxDSAction::c_enabled); } // FUNCTION: LEGO1 0x1007f200 @@ -254,7 +265,8 @@ MxResult MxBackgroundAudioManager::PlayMusic( return SUCCESS; } - if (m_action2.GetObjectId() == -1 && m_action1.GetObjectId() != p_action.GetObjectId()) { + if (m_action2.GetObjectId() == -1 && + (m_action1.GetObjectId() != p_action.GetObjectId() || m_action1.GetAtomId() != p_action.GetAtomId())) { MxDSAction action; action.SetAtomId(GetCurrentAction().GetAtomId()); action.SetObjectId(GetCurrentAction().GetObjectId()); diff --git a/LEGO1/lego/legoomni/src/common/legoutils.cpp b/LEGO1/lego/legoomni/src/common/legoutils.cpp index c43e4dc9..f8ef1bab 100644 --- a/LEGO1/lego/legoomni/src/common/legoutils.cpp +++ b/LEGO1/lego/legoomni/src/common/legoutils.cpp @@ -503,6 +503,12 @@ MxBool RemoveFromCurrentWorld(const MxAtomId& p_atomId, MxS32 p_id) { LegoWorld* world = CurrentWorld(); + auto result = + Extension::Call(HandleRemove, SiLoader::StreamObject{p_atomId, p_id}, world).value_or(std::nullopt); + if (result) { + return result.value(); + } + if (world) { MxCore* object = world->Find(p_atomId, p_id); @@ -518,8 +524,6 @@ MxBool RemoveFromCurrentWorld(const MxAtomId& p_atomId, MxS32 p_id) } ((MxPresenter*) object)->EndAction(); - - Extension::Call(RemoveWith, SiLoader::StreamObject{p_atomId, p_id}, world); } return TRUE; @@ -539,6 +543,12 @@ MxBool RemoveFromWorld( { LegoWorld* world = FindWorld(p_worldAtom, p_worldEntityId); + auto result = Extension::Call(HandleRemove, SiLoader::StreamObject{p_entityAtom, p_entityId}, world) + .value_or(std::nullopt); + if (result) { + return result.value(); + } + if (world) { MxCore* object = world->Find(p_entityAtom, p_entityId); @@ -554,8 +564,6 @@ MxBool RemoveFromWorld( } ((MxPresenter*) object)->EndAction(); - - Extension::Call(RemoveWith, SiLoader::StreamObject{p_entityAtom, p_entityId}, world); } return TRUE; diff --git a/LEGO1/lego/legoomni/src/entity/legoworld.cpp b/LEGO1/lego/legoomni/src/entity/legoworld.cpp index 1ba828cb..fe4bbc63 100644 --- a/LEGO1/lego/legoomni/src/entity/legoworld.cpp +++ b/LEGO1/lego/legoomni/src/entity/legoworld.cpp @@ -1,6 +1,7 @@ #include "legoworld.h" #include "anim/legoanim.h" +#include "extensions/siloader.h" #include "legoanimationmanager.h" #include "legoanimpresenter.h" #include "legobuildingmanager.h" @@ -32,6 +33,8 @@ DECOMP_SIZE_ASSERT(LegoEntityListCursor, 0x10) DECOMP_SIZE_ASSERT(LegoCacheSoundList, 0x18) DECOMP_SIZE_ASSERT(LegoCacheSoundListCursor, 0x10) +using namespace Extensions; + // FUNCTION: LEGO1 0x1001ca40 LegoWorld::LegoWorld() : m_pathControllerList(TRUE) { @@ -636,6 +639,12 @@ MxCore* LegoWorld::Find(const char* p_class, const char* p_name) // FUNCTION: BETA10 0x100db3de MxCore* LegoWorld::Find(const MxAtomId& p_atom, MxS32 p_entityId) { + auto result = + Extension::Call(HandleFind, SiLoader::StreamObject{p_atom, p_entityId}, this).value_or(std::nullopt); + if (result) { + return result.value(); + } + LegoEntityListCursor entityCursor(m_entityList); LegoEntity* entity; diff --git a/LEGO1/lego/legoomni/src/main/legomain.cpp b/LEGO1/lego/legoomni/src/main/legomain.cpp index 975933ff..604701eb 100644 --- a/LEGO1/lego/legoomni/src/main/legomain.cpp +++ b/LEGO1/lego/legoomni/src/main/legomain.cpp @@ -474,6 +474,11 @@ LegoWorld* LegoOmni::FindWorld(const MxAtomId& p_atom, MxS32 p_entityid) // STUB: BETA10 0x1008e93e void LegoOmni::DeleteObject(MxDSAction& p_dsAction) { + auto result = Extension::Call(HandleDelete, p_dsAction).value_or(std::nullopt); + if (result && result.value()) { + return; + } + if (p_dsAction.GetAtomId().GetInternal() != NULL) { LegoWorld* world = FindWorld(p_dsAction.GetAtomId(), p_dsAction.GetObjectId()); if (world) { @@ -663,6 +668,13 @@ void LegoOmni::CreateBackgroundAudio() // FUNCTION: BETA10 0x1008f7e0 MxResult LegoOmni::Start(MxDSAction* p_dsAction) { + { + auto result = Extension::Call(HandleStart, *p_dsAction).value_or(std::nullopt); + if (result) { + return result.value(); + } + } + MxResult result = MxOmni::Start(p_dsAction); #ifdef BETA10 this->m_action = *p_dsAction; @@ -673,14 +685,6 @@ MxResult LegoOmni::Start(MxDSAction* p_dsAction) this->m_action.SetObjectId(p_dsAction->GetObjectId()); this->m_action.SetUnknown24(p_dsAction->GetUnknown24()); #endif - - if (result == SUCCESS) { - Extension::Call( - StartWith, - SiLoader::StreamObject{p_dsAction->GetAtomId(), p_dsAction->GetObjectId()} - ); - } - return result; } diff --git a/LEGO1/omni/include/mxdsbuffer.h b/LEGO1/omni/include/mxdsbuffer.h index a988ca57..09f6f826 100644 --- a/LEGO1/omni/include/mxdsbuffer.h +++ b/LEGO1/omni/include/mxdsbuffer.h @@ -91,6 +91,12 @@ class MxDSBuffer : public MxCore { void SetUnk30(MxDSStreamingAction* p_unk0x30) { m_unk0x30 = p_unk0x30; } + void SetSourceBuffer(MxDSBuffer* p_sourceBuffer) + { + m_sourceBuffer = p_sourceBuffer; + m_sourceBuffer->AddRef(NULL); + } + // SYNTHETIC: LEGO1 0x100c6510 // SYNTHETIC: BETA10 0x10158530 // MxDSBuffer::`scalar deleting destructor' @@ -107,6 +113,7 @@ class MxDSBuffer : public MxCore { MxU32 m_writeOffset; // 0x28 MxU32 m_bytesRemaining; // 0x2c MxDSStreamingAction* m_unk0x30; // 0x30 + MxDSBuffer* m_sourceBuffer; }; #endif // MXDSBUFFER_H diff --git a/LEGO1/omni/include/mxstring.h b/LEGO1/omni/include/mxstring.h index b4b25262..fad2e4ad 100644 --- a/LEGO1/omni/include/mxstring.h +++ b/LEGO1/omni/include/mxstring.h @@ -9,7 +9,7 @@ // SIZE 0x10 class MxString : public MxCore { public: - MxString(); + LEGO1_EXPORT MxString(); MxString(const MxString& p_str); LEGO1_EXPORT MxString(const char* p_str); MxString(const char* p_str, MxU16 p_maxlen); diff --git a/LEGO1/omni/src/stream/mxdsbuffer.cpp b/LEGO1/omni/src/stream/mxdsbuffer.cpp index 0d49741a..732f9fac 100644 --- a/LEGO1/omni/src/stream/mxdsbuffer.cpp +++ b/LEGO1/omni/src/stream/mxdsbuffer.cpp @@ -28,6 +28,7 @@ MxDSBuffer::MxDSBuffer() m_bytesRemaining = 0; m_mode = e_preallocated; m_unk0x30 = 0; + m_sourceBuffer = NULL; } // FUNCTION: LEGO1 0x100c6530 @@ -36,6 +37,10 @@ MxDSBuffer::~MxDSBuffer() { assert(m_referenceCount == 0); + if (m_sourceBuffer) { + m_sourceBuffer->ReleaseRef(NULL); + } + if (m_pBuffer != NULL) { switch (m_mode) { case e_allocate: @@ -267,6 +272,28 @@ MxResult MxDSBuffer::ParseChunk( return FAILURE; } + // START FIX: Ref-Counting Backpressure for Split Chunks + // + // PROBLEM: When a `DS_CHUNK_SPLIT` is found, the temporary `MxStreamChunk` + // header that holds a reference to the source buffer is immediately + // destroyed. This prematurely releases the reference, causing the source + // buffer's ref-count to drop to zero. + // + // EFFECT: The source buffer is freed immediately instead of being kept + // alive on the m_list0x74 "keep-alive" list. This breaks the natural + // ref-counting backpressure mechanism, as the controller is blind to the + // downstream workload and keeps reading new data from the stream at full + // speed, eventually leading to a memory leak. + // + // SOLUTION: We explicitly link the new reassembly buffer to the original + // source buffer. We then add an artificial reference to the source buffer. + // This reference is designed to be released by the reassembly buffer's + // destructor, ensuring the source buffer is kept alive for the correct + // duration and that the backpressure system functions as intended. + if (p_header->GetBuffer()) { + buffer->SetSourceBuffer(p_header->GetBuffer()); + } + MxU16* flags = MxStreamChunk::IntoFlags(buffer->GetBuffer()); *flags = p_header->GetChunkFlags() & ~DS_CHUNK_SPLIT; @@ -409,9 +436,7 @@ MxU8 MxDSBuffer::ReleaseRef(MxDSChunk*) // FUNCTION: LEGO1 0x100c6ee0 void MxDSBuffer::AddRef(MxDSChunk* p_chunk) { - if (p_chunk) { - m_referenceCount++; - } + m_referenceCount++; } // FUNCTION: LEGO1 0x100c6ef0 diff --git a/README.md b/README.md index 95237c85..a6bd8eff 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Please note: this project is primarily dedicated to achieving platform independe | Nintendo 3DS | [![CI](https://github.com/isledecomp/isle-portable/actions/workflows/ci.yml/badge.svg)](https://github.com/isledecomp/isle-portable/actions/workflows/ci.yml) | | Xbox One | [![CI](https://github.com/isledecomp/isle-portable/actions/workflows/ci.yml/badge.svg)](https://github.com/isledecomp/isle-portable/actions/workflows/ci.yml) | | iOS | [![CI](https://github.com/isledecomp/isle-portable/actions/workflows/ci.yml/badge.svg)](https://github.com/isledecomp/isle-portable/actions/workflows/ci.yml) | +| Android | [![CI](https://github.com/isledecomp/isle-portable/actions/workflows/ci.yml/badge.svg)](https://github.com/isledecomp/isle-portable/actions/workflows/ci.yml) | We are actively working to support more platforms. If you have experience with a particular platform, we encourage you to contribute to `isle-portable`. You can find a [list of ongoing efforts](https://github.com/isledecomp/isle-portable/wiki/Work%E2%80%90in%E2%80%90progress-ports) in our Wiki. @@ -43,7 +44,7 @@ To achieve our goal of platform independence, we need to replace any Windows-onl | WinMM, DirectSound (Audio) | [SDL3](https://www.libsdl.org/), [miniaudio](https://miniaud.io/) | ✅ | [Remarks](https://github.com/search?q=repo%3Aisledecomp%2Fisle-portable+%22%2F%2F+%5Blibrary%3Aaudio%5D%22&type=code) | | DirectDraw (2D video) | [SDL3](https://www.libsdl.org/) | ✅ | [Remarks](https://github.com/search?q=repo%3Aisledecomp%2Fisle-portable+%22%2F%2F+%5Blibrary%3A2d%5D%22&type=code) | | [Smacker](https://github.com/isledecomp/isle/tree/master/3rdparty/smacker) | [libsmacker](https://github.com/foxtacles/libsmacker) | ✅ | [Remarks](https://github.com/search?q=repo%3Aisledecomp%2Fisle-portable%20%22%2F%2F%20%5Blibrary%3Alibsmacker%5D%22&type=code) | -| Direct3D (3D video) | [SDL3 (Vulkan, Metal, D3D12)](https://www.libsdl.org/), D3D9, OpenGL, OpenGL ES, Software | ✅ | [Remarks](https://github.com/search?q=repo%3Aisledecomp%2Fisle-portable+%22%2F%2F+%5Blibrary%3A3d%5D%22&type=code) | +| Direct3D (3D video) | [SDL3 (Vulkan, Metal, D3D12)](https://www.libsdl.org/), D3D9, OpenGL 1.1, OpenGL ES 2.0, OpenGL ES 3.0, Software | ✅ | [Remarks](https://github.com/search?q=repo%3Aisledecomp%2Fisle-portable+%22%2F%2F+%5Blibrary%3A3d%5D%22&type=code) | | Direct3D Retained Mode | Custom re-implementation | ✅ | [Remarks](https://github.com/search?q=repo%3Aisledecomp%2Fisle-portable+%22%2F%2F+%5Blibrary%3Aretained%5D%22&type=code) | | [SmartHeap](https://github.com/isledecomp/isle/tree/master/3rdparty/smartheap) | Default memory allocator | - | - | diff --git a/android-project/.gitignore b/android-project/.gitignore new file mode 100644 index 00000000..bcd50fa6 --- /dev/null +++ b/android-project/.gitignore @@ -0,0 +1,5 @@ +.cxx +.gradle +local.properties +build +*.aar diff --git a/android-project/app/build.gradle b/android-project/app/build.gradle new file mode 100644 index 00000000..dbdb337b --- /dev/null +++ b/android-project/app/build.gradle @@ -0,0 +1,101 @@ +plugins { + id 'com.android.application' +} +def androidProject = projectDir.parentFile + +android { + namespace "org.legoisland.isle" + compileSdk 35 + + defaultConfig { + applicationId 'org.legoisland.isle' + minSdk 21 + targetSdk 35 + versionCode 1 + versionName '1.0' + + externalNativeBuild { + cmake { + arguments '-DANDROID_STL=c++_shared', "-DFETCHCONTENT_BASE_DIR=${androidProject}/build/_deps" + if (project.hasProperty('cmakeArgs')) { + project.cmakeArgs.split(" ").each {arg -> arguments arg} + } + abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' + } + } + } + externalNativeBuild { + cmake { + version "3.30.5" + path '../../CMakeLists.txt' + } + } + + signingConfigs { + register("release") { + enableV4Signing = true + keyAlias = System.getenv("SIGNING_KEY_ALIAS") + keyPassword = System.getenv("SIGNING_KEY_PASSWORD") + System.getenv("SIGNING_STORE_FILE")?.with { storeFile = file(it) } + storePassword = System.getenv("SIGNING_STORE_PASSWORD") + } + } + buildTypes { + release { + minifyEnabled true + signingConfig = signingConfigs.getByName("release") +// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + buildFeatures { + prefab true + } +} + +afterEvaluate { + def androidPath = System.getenv("ANDROID_HOME") ?: android.sdkDirectory + def ndkPath = System.getenv("ANDROID_NDK_HOME") ?: android.ndkDirectory + + tasks.named('compileSDL3AndroidArchive').configure { + environment "ANDROID_HOME", androidPath + environment "ANDROID_NDK_HOME", ndkPath + } +} + +def aarDest = project.providers.provider { file("${projectDir}/libs/SDL3.aar") } +tasks.register('downloadSDL3', Exec) { + workingDir = androidProject + commandLine 'cmake', '-P', 'downloadSDL3.cmake' +} +tasks.register('compileSDL3AndroidArchive', Exec) { + workingDir = androidProject + dependsOn(downloadSDL3) + + def sdl3Dir = "build/_deps/sdl3-src" + commandLine 'python', "${sdl3Dir}/build-scripts/build-release.py", + '--actions', 'android', + '--fast', '--force', + "--root=${sdl3Dir}" + + doLast { + def aarFile = file("${androidProject}/${sdl3Dir}/build-android").listFiles().find { + it.name.endsWith(".aar") + } + aarDest.get().parentFile.mkdirs() + aarFile.renameTo(aarDest.get()) + } +} +tasks.register('ensureSDL3') { + // if DOWNLOAD_DEPENDENCIES=OFF download the version appropriate + // 'devel android zip' and place the .aar at `aarDest` + if (aarDest.get().exists()) { return false } + dependsOn(compileSDL3AndroidArchive) +} + +dependencies { + implementation files('libs/SDL3.aar').builtBy(ensureSDL3) +} diff --git a/android-project/app/src/main/AndroidManifest.xml b/android-project/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..7d76ba78 --- /dev/null +++ b/android-project/app/src/main/AndroidManifest.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-project/app/src/main/ic_launcher-playstore.png b/android-project/app/src/main/ic_launcher-playstore.png new file mode 100644 index 00000000..d38af7d8 Binary files /dev/null and b/android-project/app/src/main/ic_launcher-playstore.png differ diff --git a/android-project/app/src/main/java/org/legoisland/isle/IsleActivity.java b/android-project/app/src/main/java/org/legoisland/isle/IsleActivity.java new file mode 100644 index 00000000..4e556c5b --- /dev/null +++ b/android-project/app/src/main/java/org/legoisland/isle/IsleActivity.java @@ -0,0 +1,9 @@ +package org.legoisland.isle; + +import org.libsdl.app.SDLActivity; + +public class IsleActivity extends SDLActivity { + protected String[] getLibraries() { + return new String[] { "SDL3", "lego1", "isle" }; + } +} diff --git a/android-project/app/src/main/res/drawable/ic_launcher_foreground.xml b/android-project/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..14440e6a --- /dev/null +++ b/android-project/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-project/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android-project/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..7353dbd1 --- /dev/null +++ b/android-project/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android-project/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android-project/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..7353dbd1 --- /dev/null +++ b/android-project/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android-project/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android-project/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 00000000..f4d86bca Binary files /dev/null and b/android-project/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/android-project/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android-project/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 00000000..60a5c417 Binary files /dev/null and b/android-project/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/android-project/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android-project/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 00000000..4a3da27a Binary files /dev/null and b/android-project/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/android-project/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android-project/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 00000000..047b4028 Binary files /dev/null and b/android-project/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/android-project/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android-project/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 00000000..22fb6853 Binary files /dev/null and b/android-project/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/android-project/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android-project/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..043c7aa7 Binary files /dev/null and b/android-project/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/android-project/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android-project/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 00000000..baea92cd Binary files /dev/null and b/android-project/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/android-project/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android-project/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..5555f64d Binary files /dev/null and b/android-project/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/android-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 00000000..8df86ac9 Binary files /dev/null and b/android-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/android-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..320691a8 Binary files /dev/null and b/android-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/android-project/app/src/main/res/values/ic_launcher_background.xml b/android-project/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 00000000..5e8204d2 --- /dev/null +++ b/android-project/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #39A0D9 + \ No newline at end of file diff --git a/android-project/app/src/main/res/values/strings.xml b/android-project/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..85a6dca0 --- /dev/null +++ b/android-project/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Lego Island + diff --git a/android-project/app/src/main/res/values/styles.xml b/android-project/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..ff6c9d2c --- /dev/null +++ b/android-project/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/android-project/build.gradle b/android-project/build.gradle new file mode 100644 index 00000000..586a2bbd --- /dev/null +++ b/android-project/build.gradle @@ -0,0 +1,4 @@ +plugins { + id 'com.android.application' version '8.7.0' apply false + id 'com.android.library' version '8.7.0' apply false +} diff --git a/android-project/downloadSDL3.cmake b/android-project/downloadSDL3.cmake new file mode 100644 index 00000000..77479e86 --- /dev/null +++ b/android-project/downloadSDL3.cmake @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.25...4.0 FATAL_ERROR) + +include(FetchContent) + +set(FETCHCONTENT_BASE_DIR "build/_deps") + +FetchContent_Populate( + SDL3 + GIT_REPOSITORY "https://github.com/libsdl-org/SDL.git" + GIT_TAG "main" + SOURCE_DIR "build/_deps/sdl3-src" + BINARY_DIR "build/_deps/sdl3-build" +) diff --git a/android-project/gradle.properties b/android-project/gradle.properties new file mode 100644 index 00000000..d5e8af31 --- /dev/null +++ b/android-project/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true diff --git a/android-project/gradle/wrapper/gradle-wrapper.jar b/android-project/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..e708b1c0 Binary files /dev/null and b/android-project/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android-project/gradle/wrapper/gradle-wrapper.properties b/android-project/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..8c558f26 --- /dev/null +++ b/android-project/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/android-project/gradlew b/android-project/gradlew new file mode 100755 index 00000000..4f906e0c --- /dev/null +++ b/android-project/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/android-project/gradlew.bat b/android-project/gradlew.bat new file mode 100644 index 00000000..ac1b06f9 --- /dev/null +++ b/android-project/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android-project/settings.gradle b/android-project/settings.gradle new file mode 100644 index 00000000..36e93e45 --- /dev/null +++ b/android-project/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} +rootProject.name = "legoisland" +include ':app' diff --git a/assets/hdmusic/Act3Music_HD.wav b/assets/hdmusic/Act3Music_HD.wav new file mode 100644 index 00000000..0480014b --- /dev/null +++ b/assets/hdmusic/Act3Music_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c3e3080ea1af626bc202d091ead654ef38ac8e27779c2908dca390f3106ced3 +size 14201934 diff --git a/assets/hdmusic/BeachBlvd_Music_HD.wav b/assets/hdmusic/BeachBlvd_Music_HD.wav new file mode 100644 index 00000000..9385be34 --- /dev/null +++ b/assets/hdmusic/BeachBlvd_Music_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1143a85549ca76ec8edccc164e8c54d803d7eb9a5bddd5c0387ce102b5397f2c +size 26919040 diff --git a/assets/hdmusic/Beach_Music_HD.wav b/assets/hdmusic/Beach_Music_HD.wav new file mode 100644 index 00000000..a198fec1 --- /dev/null +++ b/assets/hdmusic/Beach_Music_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e84733243a6d1d3044aab8032cf5d2b5769b1e3e3f0787c3b3bf18689c9f535 +size 22490016 diff --git a/assets/hdmusic/BrickHunt_HD.wav b/assets/hdmusic/BrickHunt_HD.wav new file mode 100644 index 00000000..4aaa8721 --- /dev/null +++ b/assets/hdmusic/BrickHunt_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfbd44517186635ad270a971e9328f65890b94258c456aafda6658830147dce2 +size 33980544 diff --git a/assets/hdmusic/BrickstrChase_HD.wav b/assets/hdmusic/BrickstrChase_HD.wav new file mode 100644 index 00000000..f7222876 --- /dev/null +++ b/assets/hdmusic/BrickstrChase_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a158faeb0b651da3c0663cfa871851ad226dae02577be97d049bc4f84e4d9251 +size 14614784 diff --git a/assets/hdmusic/Cave_Music_HD.wav b/assets/hdmusic/Cave_Music_HD.wav new file mode 100644 index 00000000..b02ed9da --- /dev/null +++ b/assets/hdmusic/Cave_Music_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ae808110329a64ae06a96b08210bea8938f92ac0006b850a62b941541339359 +size 12214400 diff --git a/assets/hdmusic/CentralRoads_Music_HD.wav b/assets/hdmusic/CentralRoads_Music_HD.wav new file mode 100644 index 00000000..43c0e5ee --- /dev/null +++ b/assets/hdmusic/CentralRoads_Music_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39bd028528e1c9e2c9cea0b090c3c54f78b1889135a863c09080b90d3b13b3d2 +size 34113102 diff --git a/assets/hdmusic/Hospital_Music_HD.wav b/assets/hdmusic/Hospital_Music_HD.wav new file mode 100644 index 00000000..718d46c7 --- /dev/null +++ b/assets/hdmusic/Hospital_Music_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f253c05dcf77d5b7d5109f2e95ac5af85ee7a699a8689d8c276951617bbfd0e6 +size 40701386 diff --git a/assets/hdmusic/InfoCenter_3rd_Floor_Music_HD.wav b/assets/hdmusic/InfoCenter_3rd_Floor_Music_HD.wav new file mode 100644 index 00000000..0320b078 --- /dev/null +++ b/assets/hdmusic/InfoCenter_3rd_Floor_Music_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de7bf7bc1512aa6430b35e94c952c9b815b13ceded533f458ff9806a3915f948 +size 20201550 diff --git a/assets/hdmusic/InformationCenter_Music_HD.wav b/assets/hdmusic/InformationCenter_Music_HD.wav new file mode 100644 index 00000000..f896b392 --- /dev/null +++ b/assets/hdmusic/InformationCenter_Music_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c1ade0044cef699f526898cd44c34f426f3f057312f8d457ebc4b454096bdb0 +size 27256398 diff --git a/assets/hdmusic/JBMusic1_HD.wav b/assets/hdmusic/JBMusic1_HD.wav new file mode 100644 index 00000000..a07f7354 --- /dev/null +++ b/assets/hdmusic/JBMusic1_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c11ed75397b677a069cd9302bec3e1178067b030c42a47ef6853cb9212ff716 +size 22200448 diff --git a/assets/hdmusic/JBMusic2_HD.wav b/assets/hdmusic/JBMusic2_HD.wav new file mode 100644 index 00000000..cdb64575 --- /dev/null +++ b/assets/hdmusic/JBMusic2_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8964ca0fb9698abab90398bfa62f04e52830b647af8b4014a129edd9fbfdae47 +size 28735604 diff --git a/assets/hdmusic/JBMusic3_HD.wav b/assets/hdmusic/JBMusic3_HD.wav new file mode 100644 index 00000000..cf22253b --- /dev/null +++ b/assets/hdmusic/JBMusic3_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98b02da1c3900a9e906b88692ecb2087f2091b4af7303565eaeff242cdf7c313 +size 21653144 diff --git a/assets/hdmusic/JBMusic4_HD.wav b/assets/hdmusic/JBMusic4_HD.wav new file mode 100644 index 00000000..acb73671 --- /dev/null +++ b/assets/hdmusic/JBMusic4_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13c787e61647e22a1f4535d2491cbad5638c835142c075ebe6a39964ff769466 +size 24699000 diff --git a/assets/hdmusic/JBMusic5_HD.wav b/assets/hdmusic/JBMusic5_HD.wav new file mode 100644 index 00000000..69ca50f6 --- /dev/null +++ b/assets/hdmusic/JBMusic5_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b744d204a6bcb3c659f490fc9caa6a2bff96e6c91ab7ba8770508b208f0fffd +size 12828932 diff --git a/assets/hdmusic/JBMusic6_HD.wav b/assets/hdmusic/JBMusic6_HD.wav new file mode 100644 index 00000000..a5235fce --- /dev/null +++ b/assets/hdmusic/JBMusic6_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8779860718e07a6093c979d77ec1723dcef93762bc7759e960e70ee730b66fb3 +size 10059690 diff --git a/assets/hdmusic/Jail_Music_HD.wav b/assets/hdmusic/Jail_Music_HD.wav new file mode 100644 index 00000000..a9f323c3 --- /dev/null +++ b/assets/hdmusic/Jail_Music_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b39644468a172178738aa8854b514580f2b098a810dba127b2f5ae25534fe87e +size 12140672 diff --git a/assets/hdmusic/JetskiRace_Music_HD.wav b/assets/hdmusic/JetskiRace_Music_HD.wav new file mode 100644 index 00000000..eac69605 --- /dev/null +++ b/assets/hdmusic/JetskiRace_Music_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82ccad0d9877ccf915fc050d9b245785cf6bcb2406a6fcb9f6bdb9a75a3fc8c4 +size 11367404 diff --git a/assets/hdmusic/Park_Music_HD.wav b/assets/hdmusic/Park_Music_HD.wav new file mode 100644 index 00000000..83c4f4da --- /dev/null +++ b/assets/hdmusic/Park_Music_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b645676140604d078be9cb1dc7d5f0ee17700878ff50a5280b426616898be64 +size 16089216 diff --git a/assets/hdmusic/PoliceStation_Music_HD.wav b/assets/hdmusic/PoliceStation_Music_HD.wav new file mode 100644 index 00000000..6a4dd58c --- /dev/null +++ b/assets/hdmusic/PoliceStation_Music_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59c5ad73804bf813a852a5486e6ab13739a16542d06c736c28c164694ed424fd +size 10070544 diff --git a/assets/hdmusic/RaceTrackRoad_Music_HD.wav b/assets/hdmusic/RaceTrackRoad_Music_HD.wav new file mode 100644 index 00000000..813d33b3 --- /dev/null +++ b/assets/hdmusic/RaceTrackRoad_Music_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:002c09354642fd8757ea999d62badf0b346f5ab1d4fb6b6ce31e471b0a6bdb1a +size 33341560 diff --git a/assets/hdmusic/ResidentalArea_Music_HD.wav b/assets/hdmusic/ResidentalArea_Music_HD.wav new file mode 100644 index 00000000..923ee0dc --- /dev/null +++ b/assets/hdmusic/ResidentalArea_Music_HD.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f24f80d0979e1794d42000b55e95f0674be196af94c30abfa2a8d971e106a309 +size 15794304 diff --git a/assets/main.cpp b/assets/main.cpp index e1189012..d239e013 100644 --- a/assets/main.cpp +++ b/assets/main.cpp @@ -16,30 +16,182 @@ si::MemoryBuffer mxHd; void CreateWidescreen() { + std::string result = out + "/widescreen.si"; + struct AssetView { + std::string name; + std::string extra; + }; + const AssetView widescreenBitmaps[] = { + {"GaraDoor_Background_Wide", + "World:current, StartWith:\\Lego\\Scripts\\Isle\\Isle;1160, RemoveWith:\\Lego\\Scripts\\Isle\\Isle;1161"} + }; + si::Interleaf si; - std::string file = out + "/widescreen.si"; mxHd.seek(0, si::MemoryBuffer::SeekStart); si.Read(&mxHd); - si::Object GaraDoor_Wide; - const char extra[] = - "World:current, StartWith:\\Lego\\Scripts\\Isle\\Isle;1160, RemoveWith:\\Lego\\Scripts\\Isle\\Isle;1161"; - GaraDoor_Wide.type_ = si::MxOb::Bitmap; - GaraDoor_Wide.flags_ = MxDSAction::c_enabled | MxDSAction::c_bit4; - GaraDoor_Wide.duration_ = -1; - GaraDoor_Wide.loops_ = 1; - GaraDoor_Wide.extra_ = si::bytearray(extra, sizeof(extra)); - GaraDoor_Wide.presenter_ = "MxStillPresenter"; - GaraDoor_Wide.name_ = "GaraDoor_Wide"; - GaraDoor_Wide.filetype_ = si::MxOb::STL; - GaraDoor_Wide.location_.x = -240.0; - GaraDoor_Wide.location_.z = -1.0; - GaraDoor_Wide.up_.y = 1.0; - GaraDoor_Wide.ReplaceWithFile("widescreen/garadoor.bmp"); - si.AppendChild(&GaraDoor_Wide); - depfile << file << ": " << (std::filesystem::current_path() / "widescreen/garadoor.bmp").string() << std::endl; + int i = 0; + for (const AssetView& asset : widescreenBitmaps) { + si::Object* object = new si::Object; + std::string file = std::string("widescreen/") + asset.name + ".bmp"; - si.Write(file.c_str()); + object->id_ = i; + object->type_ = si::MxOb::Bitmap; + object->flags_ = MxDSAction::c_enabled | MxDSAction::c_bit4; + object->duration_ = -1; + object->loops_ = 1; + object->extra_ = si::bytearray(asset.extra.c_str(), asset.extra.length() + 1); + object->presenter_ = "MxStillPresenter"; + object->name_ = asset.name; + object->filetype_ = si::MxOb::STL; + object->location_ = si::Vector3(-240.0, 0.0, -1.0); + object->direction_ = si::Vector3(0, 0, 0); + object->up_ = si::Vector3(0, 1.0, 0); + + if (!object->ReplaceWithFile(file.c_str())) { + abort(); + } + + si.AppendChild(object); + depfile << result << ": " << (std::filesystem::current_path() / file).string() << std::endl; + i++; + } + + si.Write(result.c_str()); +} + +void CreateHDMusic() +{ + std::string result = out + "/hdmusic.si"; + struct AssetView { + std::string name; + std::string extra; + uint32_t duration; + uint32_t loops; + uint32_t flags; + }; + const AssetView wavAudio[] = { + {"BrickstrChase_HD", + "Replace:\\Lego\\Scripts\\Isle\\Jukebox;3", + 82850, + 10000, + MxDSAction::c_enabled | MxDSAction::c_bit3}, + {"BrickHunt_HD", + "Replace:\\Lego\\Scripts\\Isle\\Jukebox;4", + 192630, + 10000, + MxDSAction::c_enabled | MxDSAction::c_bit3}, + {"ResidentalArea_Music_HD", + "Replace:\\Lego\\Scripts\\Isle\\Jukebox;5", + 89540, + 10000, + MxDSAction::c_enabled | MxDSAction::c_bit3}, + {"BeachBlvd_Music_HD", + "Replace:\\Lego\\Scripts\\Isle\\Jukebox;6", + 152600, + 10000, + MxDSAction::c_enabled | MxDSAction::c_bit3}, + {"Cave_Music_HD", + "Replace:\\Lego\\Scripts\\Isle\\Jukebox;7", + 69240, + 10000, + MxDSAction::c_enabled | MxDSAction::c_bit3}, + {"CentralRoads_Music_HD", + "Replace:\\Lego\\Scripts\\Isle\\Jukebox;8", + 193380, + 10000, + MxDSAction::c_enabled | MxDSAction::c_bit3}, + {"Jail_Music_HD", + "Replace:\\Lego\\Scripts\\Isle\\Jukebox;9", + 68820, + 10000, + MxDSAction::c_enabled | MxDSAction::c_bit3}, + {"Hospital_Music_HD", + "Replace:\\Lego\\Scripts\\Isle\\Jukebox;10", + 211990, + 10000, + MxDSAction::c_enabled | MxDSAction::c_bit3}, + {"InformationCenter_Music_HD", + "Replace:\\Lego\\Scripts\\Isle\\Jukebox;11", + 154510, + 10000, + MxDSAction::c_enabled | MxDSAction::c_bit3}, + {"PoliceStation_Music_HD", + "Replace:\\Lego\\Scripts\\Isle\\Jukebox;12", + 57090, + 10000, + MxDSAction::c_enabled | MxDSAction::c_bit3}, + {"Park_Music_HD", + "Replace:\\Lego\\Scripts\\Isle\\Jukebox;13", + 91210, + 10000, + MxDSAction::c_enabled | MxDSAction::c_bit3}, + {"RaceTrackRoad_Music_HD", + "Replace:\\Lego\\Scripts\\Isle\\Jukebox;16", + 189000, + 10000, + MxDSAction::c_enabled | MxDSAction::c_bit3}, + {"Beach_Music_HD", + "Replace:\\Lego\\Scripts\\Isle\\Jukebox;17", + 127490, + 10000, + MxDSAction::c_enabled | MxDSAction::c_bit3}, + {"JetskiRace_Music_HD", + "Replace:\\Lego\\Scripts\\Isle\\Jukebox;19", + 64440, + 10000, + MxDSAction::c_enabled | MxDSAction::c_bit3}, + {"Act3Music_HD", + "Replace:\\Lego\\Scripts\\Isle\\Jukebox;20", + 80510, + 10000, + MxDSAction::c_enabled | MxDSAction::c_bit3}, + {"JBMusic1_HD", "Replace:\\Lego\\Scripts\\Isle\\Jukebox;55", 125850, 1, MxDSAction::c_enabled}, + {"JBMusic2_HD", "Replace:\\Lego\\Scripts\\Isle\\Jukebox;56", 162900, 1, MxDSAction::c_enabled}, + {"JBMusic3_HD", "Replace:\\Lego\\Scripts\\Isle\\Jukebox;57", 122750, 1, MxDSAction::c_enabled}, + {"JBMusic4_HD", "Replace:\\Lego\\Scripts\\Isle\\Jukebox;58", 140000, 1, MxDSAction::c_enabled}, + {"JBMusic5_HD", "Replace:\\Lego\\Scripts\\Isle\\Jukebox;59", 72720, 1, MxDSAction::c_enabled}, + {"JBMusic6_HD", "Replace:\\Lego\\Scripts\\Isle\\Jukebox;60", 57030, 1, MxDSAction::c_enabled}, + {"InfoCenter_3rd_Floor_Music_HD", + "Replace:\\Lego\\Scripts\\Isle\\Jukebox;61", + 114520, + 10000, + MxDSAction::c_enabled | MxDSAction::c_bit3} + }; + + si::Interleaf si; + mxHd.seek(0, si::MemoryBuffer::SeekStart); + si.Read(&mxHd); + + int i = 0; + for (const AssetView& asset : wavAudio) { + si::Object* object = new si::Object; + std::string file = std::string("hdmusic/") + asset.name + ".wav"; + + object->id_ = i; + object->type_ = si::MxOb::Sound; + object->flags_ = asset.flags; + object->duration_ = asset.duration * asset.loops; + object->loops_ = asset.loops; + object->extra_ = si::bytearray(asset.extra.c_str(), asset.extra.length() + 1); + object->presenter_ = "MxWavePresenter"; + object->name_ = asset.name; + object->filetype_ = si::MxOb::WAV; + object->location_ = si::Vector3(0, 0, 0); + object->direction_ = si::Vector3(0, 0, 1); + object->up_ = si::Vector3(0, 1, 0); + object->volume_ = 79; + + if (!object->ReplaceWithFile(file.c_str())) { + abort(); + } + + si.AppendChild(object); + depfile << result << ": " << (std::filesystem::current_path() / file).string() << std::endl; + i++; + } + + si.Write(result.c_str()); } int main(int argc, char* argv[]) @@ -54,5 +206,6 @@ int main(int argc, char* argv[]) mxHd.WriteU32(bufferCount); CreateWidescreen(); + CreateHDMusic(); return 0; } diff --git a/assets/widescreen/GaraDoor_Background_Wide.bmp b/assets/widescreen/GaraDoor_Background_Wide.bmp new file mode 100644 index 00000000..8f557e6b --- /dev/null +++ b/assets/widescreen/GaraDoor_Background_Wide.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d245fde39c95f788a9406f2c4bbc94680d566cef925f03883a0824e3485c23d +size 538678 diff --git a/assets/widescreen/garadoor.bmp b/assets/widescreen/garadoor.bmp deleted file mode 100755 index 8fc4c08c..00000000 Binary files a/assets/widescreen/garadoor.bmp and /dev/null differ diff --git a/extensions/include/extensions/siloader.h b/extensions/include/extensions/siloader.h index 3fc7ab0a..ab3e69bc 100644 --- a/extensions/include/extensions/siloader.h +++ b/extensions/include/extensions/siloader.h @@ -7,6 +7,11 @@ #include #include +namespace si +{ +class Core; +} + namespace Extensions { class SiLoader { @@ -15,8 +20,10 @@ class SiLoader { static void Initialize(); static bool Load(); - static bool StartWith(StreamObject p_object); - static bool RemoveWith(StreamObject p_object, LegoWorld* world); + static std::optional HandleFind(StreamObject p_object, LegoWorld* world); + static std::optional HandleStart(MxDSAction& p_action); + static std::optional HandleRemove(StreamObject p_object, LegoWorld* world); + static std::optional HandleDelete(MxDSAction& p_action); static std::map options; static std::vector files; @@ -25,17 +32,23 @@ class SiLoader { private: static std::vector> startWith; static std::vector> removeWith; + static std::vector> replace; static bool LoadFile(const char* p_file); + static void ParseDirectives(const MxAtomId& p_atom, si::Core* p_core, MxAtomId p_parentReplacedAtom = MxAtomId()); }; #ifdef EXTENSIONS constexpr auto Load = &SiLoader::Load; -constexpr auto StartWith = &SiLoader::StartWith; -constexpr auto RemoveWith = &SiLoader::RemoveWith; +constexpr auto HandleFind = &SiLoader::HandleFind; +constexpr auto HandleStart = &SiLoader::HandleStart; +constexpr auto HandleRemove = &SiLoader::HandleRemove; +constexpr auto HandleDelete = &SiLoader::HandleDelete; #else constexpr decltype(&SiLoader::Load) Load = nullptr; -constexpr decltype(&SiLoader::StartWith) StartWith = nullptr; -constexpr decltype(&SiLoader::RemoveWith) RemoveWith = nullptr; +constexpr decltype(&SiLoader::HandleFind) HandleFind = nullptr; +constexpr decltype(&SiLoader::HandleStart) HandleStart = nullptr; +constexpr decltype(&SiLoader::HandleRemove) HandleRemove = nullptr; +constexpr decltype(&SiLoader::HandleDelete) HandleDelete = nullptr; #endif }; // namespace Extensions diff --git a/extensions/src/siloader.cpp b/extensions/src/siloader.cpp index bfbddd88..ab23b4a7 100644 --- a/extensions/src/siloader.cpp +++ b/extensions/src/siloader.cpp @@ -13,6 +13,7 @@ std::map SiLoader::options; std::vector SiLoader::files; std::vector> SiLoader::startWith; std::vector> SiLoader::removeWith; +std::vector> SiLoader::replace; bool SiLoader::enabled = false; void SiLoader::Initialize() @@ -36,21 +37,52 @@ bool SiLoader::Load() return true; } -bool SiLoader::StartWith(StreamObject p_object) +std::optional SiLoader::HandleFind(StreamObject p_object, LegoWorld* world) { - for (const auto& key : startWith) { + for (const auto& key : replace) { if (key.first == p_object) { + return world->Find(key.second.first, key.second.second); + } + } + + return std::nullopt; +} + +std::optional SiLoader::HandleStart(MxDSAction& p_action) +{ + StreamObject object{p_action.GetAtomId(), p_action.GetObjectId()}; + + for (const auto& key : startWith) { + if (key.first == object) { MxDSAction action; action.SetAtomId(key.second.first); action.SetObjectId(key.second.second); + action.SetUnknown24(p_action.GetUnknown24()); + action.SetNotificationObject(p_action.GetNotificationObject()); + action.SetOrigin(p_action.GetOrigin()); Start(&action); } } - return true; + for (const auto& key : replace) { + if (key.first == object) { + MxDSAction action; + action.SetAtomId(key.second.first); + action.SetObjectId(key.second.second); + action.SetUnknown24(p_action.GetUnknown24()); + action.SetNotificationObject(p_action.GetNotificationObject()); + action.SetOrigin(p_action.GetOrigin()); + + MxResult result = Start(&action); + p_action.SetUnknown24(action.GetUnknown24()); + return result; + } + } + + return std::nullopt; } -bool SiLoader::RemoveWith(StreamObject p_object, LegoWorld* world) +std::optional SiLoader::HandleRemove(StreamObject p_object, LegoWorld* world) { for (const auto& key : removeWith) { if (key.first == p_object) { @@ -58,7 +90,47 @@ bool SiLoader::RemoveWith(StreamObject p_object, LegoWorld* world) } } - return true; + for (const auto& key : replace) { + if (key.first == p_object) { + return RemoveFromWorld(key.second.first, key.second.second, world->GetAtomId(), world->GetEntityId()); + } + } + + return std::nullopt; +} + +std::optional SiLoader::HandleDelete(MxDSAction& p_action) +{ + StreamObject object{p_action.GetAtomId(), p_action.GetObjectId()}; + + for (const auto& key : removeWith) { + if (key.first == object) { + MxDSAction action; + action.SetAtomId(key.second.first); + action.SetObjectId(key.second.second); + action.SetUnknown24(p_action.GetUnknown24()); + action.SetNotificationObject(p_action.GetNotificationObject()); + action.SetOrigin(p_action.GetOrigin()); + DeleteObject(action); + } + } + + for (const auto& key : replace) { + if (key.first == object) { + MxDSAction action; + action.SetAtomId(key.second.first); + action.SetObjectId(key.second.second); + action.SetUnknown24(p_action.GetUnknown24()); + action.SetNotificationObject(p_action.GetNotificationObject()); + action.SetOrigin(p_action.GetOrigin()); + + DeleteObject(action); + p_action.SetUnknown24(action.GetUnknown24()); + return TRUE; + } + } + + return std::nullopt; } bool SiLoader::LoadFile(const char* p_file) @@ -68,10 +140,11 @@ bool SiLoader::LoadFile(const char* p_file) MxString path = MxString(MxOmni::GetHD()) + p_file; path.MapPathToFilesystem(); - if (si.Read(path.GetData()) != si::Interleaf::ERROR_SUCCESS) { + if (si.Read(path.GetData(), si::Interleaf::ObjectsOnly | si::Interleaf::NoInfo) != si::Interleaf::ERROR_SUCCESS) { path = MxString(MxOmni::GetCD()) + p_file; path.MapPathToFilesystem(); - if (si.Read(path.GetData()) != si::Interleaf::ERROR_SUCCESS) { + if (si.Read(path.GetData(), si::Interleaf::ObjectsOnly | si::Interleaf::NoInfo) != + si::Interleaf::ERROR_SUCCESS) { SDL_Log("Could not parse SI file %s", p_file); return false; } @@ -82,7 +155,15 @@ bool SiLoader::LoadFile(const char* p_file) return false; } - for (si::Core* child : si.GetChildren()) { + ParseDirectives(controller->GetAtom(), &si); + return true; +} + +void SiLoader::ParseDirectives(const MxAtomId& p_atom, si::Core* p_core, MxAtomId p_parentReplacedAtom) +{ + for (si::Core* child : p_core->GetChildren()) { + MxAtomId replacedAtom = p_parentReplacedAtom; + if (si::Object* object = dynamic_cast(child)) { if (object->type() != si::MxOb::Null) { std::string extra(object->extra_.data(), object->extra_.size()); @@ -94,7 +175,7 @@ bool SiLoader::LoadFile(const char* p_file) if (SDL_sscanf(directive, "StartWith:%255[^;];%d", atom, &id) == 2) { startWith.emplace_back( StreamObject{MxAtomId{atom, e_lowerCase2}, id}, - StreamObject{controller->GetAtom(), object->id_} + StreamObject{p_atom, object->id_} ); } } @@ -103,13 +184,31 @@ bool SiLoader::LoadFile(const char* p_file) if (SDL_sscanf(directive, "RemoveWith:%255[^;];%d", atom, &id) == 2) { removeWith.emplace_back( StreamObject{MxAtomId{atom, e_lowerCase2}, id}, - StreamObject{controller->GetAtom(), object->id_} + StreamObject{p_atom, object->id_} ); } } + + if (p_parentReplacedAtom.GetInternal()) { + replace.emplace_back( + StreamObject{p_parentReplacedAtom, object->id_}, + StreamObject{p_atom, object->id_} + ); + } + else { + if ((directive = SDL_strstr(extra.c_str(), "Replace:"))) { + if (SDL_sscanf(directive, "Replace:%255[^;];%d", atom, &id) == 2) { + replace.emplace_back( + StreamObject{MxAtomId{atom, e_lowerCase2}, id}, + StreamObject{p_atom, object->id_} + ); + replacedAtom = replace.back().first.first; + } + } + } } } - } - return true; + ParseDirectives(p_atom, child, replacedAtom); + } } diff --git a/miniwin/CMakeLists.txt b/miniwin/CMakeLists.txt index 615832cf..07f40b04 100644 --- a/miniwin/CMakeLists.txt +++ b/miniwin/CMakeLists.txt @@ -44,7 +44,19 @@ if(NOT WINDOWS_STORE) message(STATUS "🧩 OpenGL 1.x support not enabled — needs OpenGL") endif() - find_library(OPENGL_ES3_LIBRARY NAMES GLESv2) + find_library(OPENGL_ES2_LIBRARY NAMES GLESv2) + if(EMSCRIPTEN OR OPENGL_ES2_LIBRARY) + message(STATUS "Found OpenGL: enabling OpenGL ES 2.x renderer") + target_sources(miniwin PRIVATE src/d3drm/backends/opengles2/renderer.cpp) + list(APPEND GRAPHICS_BACKENDS USE_OPENGLES2) + if(OPENGL_ES2_LIBRARY) + target_link_libraries(miniwin PRIVATE ${OPENGL_ES2_LIBRARY}) + endif() + else() + message(STATUS "🧩 OpenGL ES 2.x support not enabled") + endif() + + find_library(OPENGL_ES3_LIBRARY NAMES GLESv3 GLESv2) if(EMSCRIPTEN OR OPENGL_ES3_LIBRARY) message(STATUS "Found OpenGL: enabling OpenGL ES 3.x renderer") target_sources(miniwin PRIVATE src/d3drm/backends/opengles3/renderer.cpp) diff --git a/miniwin/src/d3drm/backends/opengles2/renderer.cpp b/miniwin/src/d3drm/backends/opengles2/renderer.cpp new file mode 100644 index 00000000..2b46190b --- /dev/null +++ b/miniwin/src/d3drm/backends/opengles2/renderer.cpp @@ -0,0 +1,898 @@ +#include "d3drmrenderer_opengles2.h" +#include "meshutils.h" + +#include +#include +#include +#include +#include + +static GLuint CompileShader(GLenum type, const char* source) +{ + GLuint shader = glCreateShader(type); + glShaderSource(shader, 1, &source, nullptr); + glCompileShader(shader); + GLint success; + glGetShaderiv(shader, GL_COMPILE_STATUS, &success); + if (!success) { + GLint logLength = 0; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength); + if (logLength > 0) { + std::vector log(logLength); + glGetShaderInfoLog(shader, logLength, nullptr, log.data()); + SDL_Log("Shader compile error: %s", log.data()); + } + else { + SDL_Log("CompileShader (%s)", SDL_GetError()); + } + glDeleteShader(shader); + return 0; + } + return shader; +} + +struct SceneLightGLES2 { + float color[4]; + float position[4]; + float direction[4]; +}; + +Direct3DRMRenderer* OpenGLES2Renderer::Create(DWORD width, DWORD height, float anisotropic) +{ + // We have to reset the attributes here after having enumerated the + // OpenGL ES 2.0 renderer, or else SDL gets very confused by SDL_GL_DEPTH_SIZE + // call below when on an EGL-based backend, and crashes with EGL_BAD_MATCH. + SDL_GL_ResetAttributes(); + // But ResetAttributes resets it to 16. + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); + + if (!DDWindow) { + SDL_Log("No window handler"); + return nullptr; + } + + SDL_GLContext context = SDL_GL_CreateContext(DDWindow); + if (!context) { + SDL_Log("SDL_GL_CreateContext: %s", SDL_GetError()); + return nullptr; + } + + if (!SDL_GL_MakeCurrent(DDWindow, context)) { + SDL_GL_DestroyContext(context); + return nullptr; + } + + glDepthFunc(GL_LEQUAL); + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + glFrontFace(GL_CW); + + const char* vertexShaderSource = R"( + attribute vec3 a_position; + attribute vec3 a_normal; + attribute vec2 a_texCoord; + + uniform mat4 u_modelViewMatrix; + uniform mat3 u_normalMatrix; + uniform mat4 u_projectionMatrix; + + varying vec3 v_viewPos; + varying vec3 v_normal; + varying vec2 v_texCoord; + + void main() { + vec4 viewPos = u_modelViewMatrix * vec4(a_position, 1.0); + gl_Position = u_projectionMatrix * viewPos; + v_viewPos = viewPos.xyz; + v_normal = normalize(u_normalMatrix * a_normal); + v_texCoord = a_texCoord; + } + )"; + + const char* fragmentShaderSource = R"( + precision mediump float; + + struct SceneLight { + vec4 color; + vec4 position; + vec4 direction; + }; + + uniform SceneLight u_lights[3]; + uniform int u_lightCount; + + varying vec3 v_viewPos; + varying vec3 v_normal; + varying vec2 v_texCoord; + + uniform float u_shininess; + uniform vec4 u_color; + uniform int u_useTexture; + uniform sampler2D u_texture; + + void main() { + vec3 diffuse = vec3(0.0); + vec3 specular = vec3(0.0); + + for (int i = 0; i < 3; ++i) { + if (i >= u_lightCount) break; + + vec3 lightColor = u_lights[i].color.rgb; + + if (u_lights[i].position.w == 0.0 && u_lights[i].direction.w == 0.0) { + diffuse += lightColor; + continue; + } + + vec3 lightVec; + if (u_lights[i].direction.w == 1.0) { + lightVec = -normalize(u_lights[i].direction.xyz); + } + else { + lightVec = u_lights[i].position.xyz - v_viewPos; + } + lightVec = normalize(lightVec); + + float dotNL = max(dot(v_normal, lightVec), 0.0); + if (dotNL > 0.0) { + // Diffuse contribution + diffuse += dotNL * lightColor; + + // Specular + if (u_shininess > 0.0 && u_lights[i].direction.w == 1.0) { + vec3 viewVec = normalize(-v_viewPos); + vec3 H = normalize(lightVec + viewVec); + float dotNH = max(dot(v_normal, H), 0.0); + float spec = pow(dotNH, u_shininess); + specular += spec * lightColor; + } + } + } + + vec4 finalColor = u_color; + finalColor.rgb = clamp(diffuse * u_color.rgb + specular, 0.0, 1.0); + if (u_useTexture != 0) { + vec4 texel = texture2D(u_texture, v_texCoord); + finalColor.rgb = clamp(texel.rgb * finalColor.rgb, 0.0, 1.0); + finalColor.a = texel.a; + } + + gl_FragColor = finalColor; + } + )"; + + GLuint vs = CompileShader(GL_VERTEX_SHADER, vertexShaderSource); + GLuint fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShaderSource); + + GLuint shaderProgram = glCreateProgram(); + glAttachShader(shaderProgram, vs); + glAttachShader(shaderProgram, fs); + glBindAttribLocation(shaderProgram, 0, "a_position"); + glBindAttribLocation(shaderProgram, 1, "a_normal"); + glBindAttribLocation(shaderProgram, 2, "a_texCoord"); + glLinkProgram(shaderProgram); + glDeleteShader(vs); + glDeleteShader(fs); + + return new OpenGLES2Renderer(width, height, anisotropic, context, shaderProgram); +} + +GLES2MeshCacheEntry GLES2UploadMesh(const MeshGroup& meshGroup, bool forceUV = false) +{ + GLES2MeshCacheEntry cache{&meshGroup, meshGroup.version}; + + cache.flat = meshGroup.quality == D3DRMRENDER_FLAT || meshGroup.quality == D3DRMRENDER_UNLITFLAT; + + std::vector vertices; + if (cache.flat) { + FlattenSurfaces( + meshGroup.vertices.data(), + meshGroup.vertices.size(), + meshGroup.indices.data(), + meshGroup.indices.size(), + meshGroup.texture != nullptr || forceUV, + vertices, + cache.indices + ); + } + else { + vertices = meshGroup.vertices; + cache.indices.resize(meshGroup.indices.size()); + std::transform(meshGroup.indices.begin(), meshGroup.indices.end(), cache.indices.begin(), [](DWORD index) { + return static_cast(index); + }); + } + + std::vector texcoords; + if (meshGroup.texture || forceUV) { + texcoords.resize(vertices.size()); + std::transform(vertices.begin(), vertices.end(), texcoords.begin(), [](const D3DRMVERTEX& v) { + return v.texCoord; + }); + } + std::vector positions(vertices.size()); + std::transform(vertices.begin(), vertices.end(), positions.begin(), [](const D3DRMVERTEX& v) { + return v.position; + }); + std::vector normals(vertices.size()); + std::transform(vertices.begin(), vertices.end(), normals.begin(), [](const D3DRMVERTEX& v) { return v.normal; }); + + glGenBuffers(1, &cache.vboPositions); + glBindBuffer(GL_ARRAY_BUFFER, cache.vboPositions); + glBufferData(GL_ARRAY_BUFFER, positions.size() * sizeof(D3DVECTOR), positions.data(), GL_STATIC_DRAW); + + glGenBuffers(1, &cache.vboNormals); + glBindBuffer(GL_ARRAY_BUFFER, cache.vboNormals); + glBufferData(GL_ARRAY_BUFFER, normals.size() * sizeof(D3DVECTOR), normals.data(), GL_STATIC_DRAW); + + if (meshGroup.texture || forceUV) { + glGenBuffers(1, &cache.vboTexcoords); + glBindBuffer(GL_ARRAY_BUFFER, cache.vboTexcoords); + glBufferData(GL_ARRAY_BUFFER, texcoords.size() * sizeof(TexCoord), texcoords.data(), GL_STATIC_DRAW); + } + + glGenBuffers(1, &cache.ibo); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, cache.ibo); + glBufferData( + GL_ELEMENT_ARRAY_BUFFER, + cache.indices.size() * sizeof(cache.indices[0]), + cache.indices.data(), + GL_STATIC_DRAW + ); + + return cache; +} + +bool OpenGLES2Renderer::UploadTexture(SDL_Surface* source, GLuint& outTexId, bool isUI) +{ + SDL_Surface* surf = source; + if (source->format != SDL_PIXELFORMAT_RGBA32) { + surf = SDL_ConvertSurface(source, SDL_PIXELFORMAT_RGBA32); + if (!surf) { + return false; + } + } + + glGenTextures(1, &outTexId); + glBindTexture(GL_TEXTURE_2D, outTexId); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, surf->w, surf->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, surf->pixels); + + if (isUI) { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } + else { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + if (m_anisotropic > 1.0f) { + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, m_anisotropic); + } + glGenerateMipmap(GL_TEXTURE_2D); + } + + if (surf != source) { + SDL_DestroySurface(surf); + } + + return true; +} + +OpenGLES2Renderer::OpenGLES2Renderer( + DWORD width, + DWORD height, + float anisotropic, + SDL_GLContext context, + GLuint shaderProgram +) + : m_context(context), m_shaderProgram(shaderProgram), m_anisotropic(anisotropic) +{ + glGenFramebuffers(1, &m_fbo); + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + + bool anisoAvailable = SDL_GL_ExtensionSupported("GL_EXT_texture_filter_anisotropic"); + GLfloat maxAniso = 0.0f; + if (anisoAvailable) { + glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &maxAniso); + } + if (m_anisotropic > maxAniso) { + m_anisotropic = maxAniso; + } + SDL_Log( + "Anisotropic is %s. Requested: %f, active: %f, max aniso: %f", + m_anisotropic > 1.0f ? "on" : "off", + anisotropic, + m_anisotropic, + maxAniso + ); + + m_virtualWidth = width; + m_virtualHeight = height; + ViewportTransform viewportTransform = {1.0f, 0.0f, 0.0f}; + Resize(width, height, viewportTransform); + + SDL_Surface* dummySurface = SDL_CreateSurface(1, 1, SDL_PIXELFORMAT_RGBA32); + if (!dummySurface) { + SDL_Log("Failed to create surface: %s", SDL_GetError()); + return; + } + if (!SDL_LockSurface(dummySurface)) { + SDL_Log("Failed to lock surface: %s", SDL_GetError()); + SDL_DestroySurface(dummySurface); + return; + } + ((Uint32*) dummySurface->pixels)[0] = 0xFFFFFFFF; + SDL_UnlockSurface(dummySurface); + + UploadTexture(dummySurface, m_dummyTexture, false); + if (!m_dummyTexture) { + SDL_DestroySurface(dummySurface); + SDL_Log("Failed to create surface: %s", SDL_GetError()); + return; + } + SDL_DestroySurface(dummySurface); + + m_posLoc = glGetAttribLocation(m_shaderProgram, "a_position"); + m_normLoc = glGetAttribLocation(m_shaderProgram, "a_normal"); + m_texLoc = glGetAttribLocation(m_shaderProgram, "a_texCoord"); + m_colorLoc = glGetUniformLocation(m_shaderProgram, "u_color"); + m_shinLoc = glGetUniformLocation(m_shaderProgram, "u_shininess"); + m_lightCountLoc = glGetUniformLocation(m_shaderProgram, "u_lightCount"); + m_useTextureLoc = glGetUniformLocation(m_shaderProgram, "u_useTexture"); + m_textureLoc = glGetUniformLocation(m_shaderProgram, "u_texture"); + for (int i = 0; i < 3; ++i) { + std::string base = "u_lights[" + std::to_string(i) + "]"; + u_lightLocs[i][0] = glGetUniformLocation(m_shaderProgram, (base + ".color").c_str()); + u_lightLocs[i][1] = glGetUniformLocation(m_shaderProgram, (base + ".position").c_str()); + u_lightLocs[i][2] = glGetUniformLocation(m_shaderProgram, (base + ".direction").c_str()); + } + m_modelViewMatrixLoc = glGetUniformLocation(m_shaderProgram, "u_modelViewMatrix"); + m_normalMatrixLoc = glGetUniformLocation(m_shaderProgram, "u_normalMatrix"); + m_projectionMatrixLoc = glGetUniformLocation(m_shaderProgram, "u_projectionMatrix"); + + m_uiMesh.vertices = { + {{0.0f, 0.0f, 0.0f}, {0, 0, -1}, {0.0f, 0.0f}}, + {{1.0f, 0.0f, 0.0f}, {0, 0, -1}, {1.0f, 0.0f}}, + {{1.0f, 1.0f, 0.0f}, {0, 0, -1}, {1.0f, 1.0f}}, + {{0.0f, 1.0f, 0.0f}, {0, 0, -1}, {0.0f, 1.0f}} + }; + m_uiMesh.indices = {0, 1, 2, 0, 2, 3}; + m_uiMeshCache = GLES2UploadMesh(m_uiMesh, true); + + glUseProgram(m_shaderProgram); +} + +OpenGLES2Renderer::~OpenGLES2Renderer() +{ + SDL_DestroySurface(m_renderedImage); + glDeleteTextures(1, &m_dummyTexture); + glDeleteProgram(m_shaderProgram); + glDeleteTextures(1, &m_colorTarget); + glDeleteRenderbuffers(1, &m_depthTarget); + glDeleteFramebuffers(1, &m_fbo); + SDL_GL_DestroyContext(m_context); +} + +void OpenGLES2Renderer::PushLights(const SceneLight* lightsArray, size_t count) +{ + if (count > 3) { + SDL_Log("Unsupported number of lights (%d)", static_cast(count)); + count = 3; + } + + m_lights.assign(lightsArray, lightsArray + count); +} + +void OpenGLES2Renderer::SetFrustumPlanes(const Plane* frustumPlanes) +{ +} + +void OpenGLES2Renderer::SetProjection(const D3DRMMATRIX4D& projection, D3DVALUE front, D3DVALUE back) +{ + memcpy(&m_projection, projection, sizeof(D3DRMMATRIX4D)); +} + +struct TextureDestroyContextGLS2 { + OpenGLES2Renderer* renderer; + Uint32 textureId; +}; + +void OpenGLES2Renderer::AddTextureDestroyCallback(Uint32 id, IDirect3DRMTexture* texture) +{ + auto* ctx = new TextureDestroyContextGLS2{this, id}; + texture->AddDestroyCallback( + [](IDirect3DRMObject* obj, void* arg) { + auto* ctx = static_cast(arg); + auto& cache = ctx->renderer->m_textures[ctx->textureId]; + if (cache.glTextureId != 0) { + glDeleteTextures(1, &cache.glTextureId); + cache.glTextureId = 0; + cache.texture = nullptr; + } + delete ctx; + }, + ctx + ); +} + +Uint32 OpenGLES2Renderer::GetTextureId(IDirect3DRMTexture* iTexture, bool isUI, float scaleX, float scaleY) +{ + SDL_GL_MakeCurrent(DDWindow, m_context); + auto texture = static_cast(iTexture); + auto surface = static_cast(texture->m_surface); + + for (Uint32 i = 0; i < m_textures.size(); ++i) { + auto& tex = m_textures[i]; + if (tex.texture == texture) { + if (tex.version != texture->m_version) { + glDeleteTextures(1, &tex.glTextureId); + if (UploadTexture(surface->m_surface, tex.glTextureId, isUI)) { + tex.version = texture->m_version; + } + } + return i; + } + } + + GLuint texId; + if (!UploadTexture(surface->m_surface, texId, isUI)) { + return NO_TEXTURE_ID; + } + + for (Uint32 i = 0; i < m_textures.size(); ++i) { + auto& tex = m_textures[i]; + if (!tex.texture) { + tex.texture = texture; + tex.version = texture->m_version; + tex.glTextureId = texId; + tex.width = surface->m_surface->w; + tex.height = surface->m_surface->h; + AddTextureDestroyCallback(i, texture); + return i; + } + } + + m_textures.push_back( + {texture, texture->m_version, texId, (uint16_t) surface->m_surface->w, (uint16_t) surface->m_surface->h} + ); + AddTextureDestroyCallback((Uint32) (m_textures.size() - 1), texture); + return (Uint32) (m_textures.size() - 1); +} + +struct GLES2MeshDestroyContext { + OpenGLES2Renderer* renderer; + Uint32 id; +}; + +void OpenGLES2Renderer::AddMeshDestroyCallback(Uint32 id, IDirect3DRMMesh* mesh) +{ + auto* ctx = new GLES2MeshDestroyContext{this, id}; + mesh->AddDestroyCallback( + [](IDirect3DRMObject*, void* arg) { + auto* ctx = static_cast(arg); + auto& cache = ctx->renderer->m_meshs[ctx->id]; + cache.meshGroup = nullptr; + glDeleteBuffers(1, &cache.vboPositions); + glDeleteBuffers(1, &cache.vboNormals); + glDeleteBuffers(1, &cache.vboTexcoords); + glDeleteBuffers(1, &cache.ibo); + delete ctx; + }, + ctx + ); +} + +Uint32 OpenGLES2Renderer::GetMeshId(IDirect3DRMMesh* mesh, const MeshGroup* meshGroup) +{ + for (Uint32 i = 0; i < m_meshs.size(); ++i) { + auto& cache = m_meshs[i]; + if (cache.meshGroup == meshGroup) { + if (cache.version != meshGroup->version) { + cache = std::move(GLES2UploadMesh(*meshGroup)); + } + return i; + } + } + + auto newCache = GLES2UploadMesh(*meshGroup); + + for (Uint32 i = 0; i < m_meshs.size(); ++i) { + auto& cache = m_meshs[i]; + if (!cache.meshGroup) { + cache = std::move(newCache); + AddMeshDestroyCallback(i, mesh); + return i; + } + } + + m_meshs.push_back(std::move(newCache)); + AddMeshDestroyCallback((Uint32) (m_meshs.size() - 1), mesh); + return (Uint32) (m_meshs.size() - 1); +} + +HRESULT OpenGLES2Renderer::BeginFrame() +{ + SDL_GL_MakeCurrent(DDWindow, m_context); + m_dirty = true; + + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + + glEnable(GL_CULL_FACE); + glDisable(GL_BLEND); + glEnable(GL_DEPTH_TEST); + glDepthMask(GL_TRUE); + + SceneLightGLES2 lightData[3]; + int lightCount = std::min(static_cast(m_lights.size()), 3); + + for (int i = 0; i < lightCount; ++i) { + const auto& src = m_lights[i]; + lightData[i].color[0] = src.color.r; + lightData[i].color[1] = src.color.g; + lightData[i].color[2] = src.color.b; + lightData[i].color[3] = src.color.a; + + lightData[i].position[0] = src.position.x; + lightData[i].position[1] = src.position.y; + lightData[i].position[2] = src.position.z; + lightData[i].position[3] = src.positional; + + lightData[i].direction[0] = src.direction.x; + lightData[i].direction[1] = src.direction.y; + lightData[i].direction[2] = src.direction.z; + lightData[i].direction[3] = src.directional; + } + + for (int i = 0; i < lightCount; ++i) { + glUniform4fv(u_lightLocs[i][0], 1, lightData[i].color); + glUniform4fv(u_lightLocs[i][1], 1, lightData[i].position); + glUniform4fv(u_lightLocs[i][2], 1, lightData[i].direction); + } + glUniform1i(m_lightCountLoc, lightCount); + return DD_OK; +} + +void OpenGLES2Renderer::EnableTransparency() +{ + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthMask(GL_FALSE); +} + +void OpenGLES2Renderer::SubmitDraw( + DWORD meshId, + const D3DRMMATRIX4D& modelViewMatrix, + const D3DRMMATRIX4D& worldMatrix, + const D3DRMMATRIX4D& viewMatrix, + const Matrix3x3& normalMatrix, + const Appearance& appearance +) +{ + auto& mesh = m_meshs[meshId]; + + glUniformMatrix4fv(m_modelViewMatrixLoc, 1, GL_FALSE, &modelViewMatrix[0][0]); + glUniformMatrix3fv(m_normalMatrixLoc, 1, GL_FALSE, &normalMatrix[0][0]); + glUniformMatrix4fv(m_projectionMatrixLoc, 1, GL_FALSE, &m_projection[0][0]); + + glUniform4f( + m_colorLoc, + appearance.color.r / 255.0f, + appearance.color.g / 255.0f, + appearance.color.b / 255.0f, + appearance.color.a / 255.0f + ); + + glUniform1f(m_shinLoc, appearance.shininess); + + if (appearance.textureId != NO_TEXTURE_ID) { + glUniform1i(m_useTextureLoc, 1); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, m_textures[appearance.textureId].glTextureId); + glUniform1i(m_textureLoc, 0); + } + else { + glUniform1i(m_useTextureLoc, 0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, m_dummyTexture); + glUniform1i(m_textureLoc, 0); + } + + glBindBuffer(GL_ARRAY_BUFFER, mesh.vboPositions); + glEnableVertexAttribArray(m_posLoc); + glVertexAttribPointer(m_posLoc, 3, GL_FLOAT, GL_FALSE, 0, nullptr); + + glBindBuffer(GL_ARRAY_BUFFER, mesh.vboNormals); + glEnableVertexAttribArray(m_normLoc); + glVertexAttribPointer(m_normLoc, 3, GL_FLOAT, GL_FALSE, 0, nullptr); + + if (appearance.textureId != NO_TEXTURE_ID) { + glBindBuffer(GL_ARRAY_BUFFER, mesh.vboTexcoords); + glEnableVertexAttribArray(m_texLoc); + glVertexAttribPointer(m_texLoc, 2, GL_FLOAT, GL_FALSE, 0, nullptr); + } + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.ibo); + glDrawElements(GL_TRIANGLES, static_cast(mesh.indices.size()), GL_UNSIGNED_SHORT, nullptr); + + glDisableVertexAttribArray(m_normLoc); + glDisableVertexAttribArray(m_texLoc); +} + +HRESULT OpenGLES2Renderer::FinalizeFrame() +{ + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + + return DD_OK; +} + +void OpenGLES2Renderer::Resize(int width, int height, const ViewportTransform& viewportTransform) +{ + SDL_GL_MakeCurrent(DDWindow, m_context); + m_width = width; + m_height = height; + m_viewportTransform = viewportTransform; + if (m_renderedImage) { + SDL_DestroySurface(m_renderedImage); + } + m_renderedImage = SDL_CreateSurface(m_width, m_height, SDL_PIXELFORMAT_RGBA32); + + if (m_colorTarget) { + glDeleteTextures(1, &m_colorTarget); + m_colorTarget = 0; + } + if (m_depthTarget) { + glDeleteRenderbuffers(1, &m_depthTarget); + m_depthTarget = 0; + } + + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + + // Create color texture + glGenTextures(1, &m_colorTarget); + glBindTexture(GL_TEXTURE_2D, m_colorTarget); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_colorTarget, 0); + + // Create depth renderbuffer + glGenRenderbuffers(1, &m_depthTarget); + glBindRenderbuffer(GL_RENDERBUFFER, m_depthTarget); + + if (SDL_GL_ExtensionSupported("GL_OES_depth24")) { + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24_OES, width, height); + } + else if (SDL_GL_ExtensionSupported("GL_OES_depth32")) { + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT32_OES, width, height); + } + else { + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, width, height); + } + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_depthTarget); + GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) { + SDL_Log("FBO incomplete: 0x%X", status); + } + + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + + glViewport(0, 0, m_width, m_height); +} + +void OpenGLES2Renderer::Clear(float r, float g, float b) +{ + SDL_GL_MakeCurrent(DDWindow, m_context); + m_dirty = true; + + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + + glEnable(GL_DEPTH_TEST); + glDepthMask(GL_TRUE); + glClearColor(r, g, b, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); +} + +void OpenGLES2Renderer::Flip() +{ + SDL_GL_MakeCurrent(DDWindow, m_context); + if (!m_dirty) { + return; + } + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + glDisable(GL_DEPTH_TEST); + glFrontFace(GL_CCW); + glDepthMask(GL_FALSE); + + glUniform4f(m_colorLoc, 1.0f, 1.0f, 1.0f, 1.0f); + glUniform1f(m_shinLoc, 0.0f); + + float ambient[] = {1.0f, 1.0f, 1.0f, 1.0f}; + float blank[] = {0.0f, 0.0f, 0.0f, 0.0f}; + glUniform4fv(u_lightLocs[0][0], 1, ambient); + glUniform4fv(u_lightLocs[0][1], 1, blank); + glUniform4fv(u_lightLocs[0][2], 1, blank); + glUniform1i(m_lightCountLoc, 1); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, m_colorTarget); + glUniform1i(m_textureLoc, 0); + glUniform1i(m_useTextureLoc, 1); + + D3DRMMATRIX4D projection; + D3DRMMATRIX4D modelViewMatrix = { + {(float) m_width, 0.0f, 0.0f, 0.0f}, + {0.0f, (float) -m_height, 0.0f, 0.0f}, + {0.0f, 0.0f, 1.0f, 0.0f}, + {0.0f, (float) m_height, 0.0f, 1.0f} + }; + glUniformMatrix4fv(m_modelViewMatrixLoc, 1, GL_FALSE, &modelViewMatrix[0][0]); + Matrix3x3 identity = {{1.f, 0.f, 0.f}, {0.f, 1.f, 0.f}, {0.f, 0.f, 1.f}}; + glUniformMatrix3fv(m_normalMatrixLoc, 1, GL_FALSE, &identity[0][0]); + CreateOrthographicProjection((float) m_width, (float) m_height, projection); + glUniformMatrix4fv(m_projectionMatrixLoc, 1, GL_FALSE, &projection[0][0]); + + glDisable(GL_SCISSOR_TEST); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glBindBuffer(GL_ARRAY_BUFFER, m_uiMeshCache.vboPositions); + glEnableVertexAttribArray(m_posLoc); + glVertexAttribPointer(m_posLoc, 3, GL_FLOAT, GL_FALSE, 0, nullptr); + + glBindBuffer(GL_ARRAY_BUFFER, m_uiMeshCache.vboTexcoords); + glEnableVertexAttribArray(m_texLoc); + glVertexAttribPointer(m_texLoc, 2, GL_FLOAT, GL_FALSE, 0, nullptr); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_uiMeshCache.ibo); + glDrawElements(GL_TRIANGLES, static_cast(m_uiMeshCache.indices.size()), GL_UNSIGNED_SHORT, nullptr); + + glDisableVertexAttribArray(m_texLoc); + + SDL_GL_SwapWindow(DDWindow); + glFrontFace(GL_CW); + m_dirty = false; +} + +void OpenGLES2Renderer::Draw2DImage(Uint32 textureId, const SDL_Rect& srcRect, const SDL_Rect& dstRect, FColor color) +{ + SDL_GL_MakeCurrent(DDWindow, m_context); + m_dirty = true; + + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + + glDisable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); + + float ambient[] = {1.0f, 1.0f, 1.0f, 1.0f}; + float blank[] = {0.0f, 0.0f, 0.0f, 0.0f}; + glUniform4fv(u_lightLocs[0][0], 1, ambient); + glUniform4fv(u_lightLocs[0][1], 1, blank); + glUniform4fv(u_lightLocs[0][2], 1, blank); + glUniform1i(m_lightCountLoc, 1); + + glUniform4f(m_colorLoc, color.r, color.g, color.b, color.a); + glUniform1f(m_shinLoc, 0.0f); + + SDL_Rect expandedDstRect; + if (textureId != NO_TEXTURE_ID) { + const GLES2TextureCacheEntry& texture = m_textures[textureId]; + float scaleX = static_cast(dstRect.w) / srcRect.w; + float scaleY = static_cast(dstRect.h) / srcRect.h; + expandedDstRect = { + static_cast(std::round(dstRect.x - srcRect.x * scaleX)), + static_cast(std::round(dstRect.y - srcRect.y * scaleY)), + static_cast(std::round(texture.width * scaleX)), + static_cast(std::round(texture.height * scaleY)) + }; + + glUniform1i(m_useTextureLoc, 1); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, texture.glTextureId); + glUniform1i(m_textureLoc, 0); + } + else { + expandedDstRect = dstRect; + glUniform1i(m_useTextureLoc, 0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, m_dummyTexture); + glUniform1i(m_textureLoc, 0); + } + + D3DRMMATRIX4D modelView, projection; + Create2DTransformMatrix( + expandedDstRect, + m_viewportTransform.scale, + m_viewportTransform.offsetX, + m_viewportTransform.offsetY, + modelView + ); + + glUniformMatrix4fv(m_modelViewMatrixLoc, 1, GL_FALSE, &modelView[0][0]); + Matrix3x3 identity = {{1.f, 0.f, 0.f}, {0.f, 1.f, 0.f}, {0.f, 0.f, 1.f}}; + glUniformMatrix3fv(m_normalMatrixLoc, 1, GL_FALSE, &identity[0][0]); + CreateOrthographicProjection((float) m_width, (float) m_height, projection); + glUniformMatrix4fv(m_projectionMatrixLoc, 1, GL_FALSE, &projection[0][0]); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glEnable(GL_SCISSOR_TEST); + glScissor( + static_cast(std::round(dstRect.x * m_viewportTransform.scale + m_viewportTransform.offsetX)), + m_height - static_cast( + std::round((dstRect.y + dstRect.h) * m_viewportTransform.scale + m_viewportTransform.offsetY) + ), + static_cast(std::round(dstRect.w * m_viewportTransform.scale)), + static_cast(std::round(dstRect.h * m_viewportTransform.scale)) + ); + + glBindBuffer(GL_ARRAY_BUFFER, m_uiMeshCache.vboPositions); + glEnableVertexAttribArray(m_posLoc); + glVertexAttribPointer(m_posLoc, 3, GL_FLOAT, GL_FALSE, 0, nullptr); + + glBindBuffer(GL_ARRAY_BUFFER, m_uiMeshCache.vboTexcoords); + glEnableVertexAttribArray(m_texLoc); + glVertexAttribPointer(m_texLoc, 2, GL_FLOAT, GL_FALSE, 0, nullptr); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_uiMeshCache.ibo); + glDrawElements(GL_TRIANGLES, static_cast(m_uiMeshCache.indices.size()), GL_UNSIGNED_SHORT, nullptr); + + glDisableVertexAttribArray(m_texLoc); + glDisable(GL_SCISSOR_TEST); +} + +void OpenGLES2Renderer::Download(SDL_Surface* target) +{ + glFinish(); + + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + glReadPixels(0, 0, m_width, m_height, GL_RGBA, GL_UNSIGNED_BYTE, m_renderedImage->pixels); + + SDL_Rect srcRect = { + static_cast(m_viewportTransform.offsetX), + static_cast(m_viewportTransform.offsetY), + static_cast(target->w * m_viewportTransform.scale), + static_cast(target->h * m_viewportTransform.scale), + }; + + SDL_Surface* bufferClone = SDL_CreateSurface(target->w, target->h, SDL_PIXELFORMAT_RGBA32); + if (!bufferClone) { + SDL_Log("SDL_CreateSurface: %s", SDL_GetError()); + return; + } + + SDL_BlitSurfaceScaled(m_renderedImage, &srcRect, bufferClone, nullptr, SDL_SCALEMODE_NEAREST); + + // Flip image vertically into target + SDL_Rect rowSrc = {0, 0, bufferClone->w, 1}; + SDL_Rect rowDst = {0, 0, bufferClone->w, 1}; + for (int y = 0; y < bufferClone->h; ++y) { + rowSrc.y = y; + rowDst.y = bufferClone->h - 1 - y; + SDL_BlitSurface(bufferClone, &rowSrc, target, &rowDst); + } + + SDL_DestroySurface(bufferClone); +} + +void OpenGLES2Renderer::SetDither(bool dither) +{ + if (dither) { + glEnable(GL_DITHER); + } + else { + glDisable(GL_DITHER); + } +} diff --git a/miniwin/src/d3drm/d3drmrenderer.cpp b/miniwin/src/d3drm/d3drmrenderer.cpp index 8fe210b5..f1e1fb8b 100644 --- a/miniwin/src/d3drm/d3drmrenderer.cpp +++ b/miniwin/src/d3drm/d3drmrenderer.cpp @@ -2,6 +2,9 @@ #ifdef USE_OPENGL1 #include "d3drmrenderer_opengl1.h" #endif +#ifdef USE_OPENGLES2 +#include "d3drmrenderer_opengles2.h" +#endif #ifdef USE_OPENGLES3 #include "d3drmrenderer_opengles3.h" #endif @@ -44,6 +47,11 @@ Direct3DRMRenderer* CreateDirect3DRMRenderer( ); } #endif +#ifdef USE_OPENGLES2 + if (SDL_memcmp(guid, &OpenGLES2_GUID, sizeof(GUID)) == 0) { + return OpenGLES2Renderer::Create(DDSDesc.dwWidth, DDSDesc.dwHeight, d3d->GetAnisotropic()); + } +#endif #ifdef USE_OPENGL1 if (SDL_memcmp(guid, &OpenGL1_GUID, sizeof(GUID)) == 0) { return OpenGL1Renderer::Create(DDSDesc.dwWidth, DDSDesc.dwHeight, d3d->GetMSAASamples()); @@ -70,6 +78,9 @@ void Direct3DRMRenderer_EnumDevices(const IDirect3DMiniwin* d3d, LPD3DENUMDEVICE #ifdef USE_OPENGLES3 OpenGLES3Renderer_EnumDevice(d3d, cb, ctx); #endif +#ifdef USE_OPENGLES2 + OpenGLES2Renderer_EnumDevice(d3d, cb, ctx); +#endif #ifdef USE_OPENGL1 OpenGL1Renderer_EnumDevice(d3d, cb, ctx); #endif diff --git a/miniwin/src/internal/d3drmrenderer_opengles2.h b/miniwin/src/internal/d3drmrenderer_opengles2.h new file mode 100644 index 00000000..6a95d73f --- /dev/null +++ b/miniwin/src/internal/d3drmrenderer_opengles2.h @@ -0,0 +1,128 @@ +#pragma once + +#include "d3drmrenderer.h" +#include "d3drmtexture_impl.h" +#include "ddraw_impl.h" + +#include +#include +#include + +DEFINE_GUID(OpenGLES2_GUID, 0x682656F3, 0x0000, 0x0000, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06); + +struct GLES2TextureCacheEntry { + IDirect3DRMTexture* texture; + Uint32 version; + GLuint glTextureId; + uint16_t width; + uint16_t height; +}; + +struct GLES2MeshCacheEntry { + const MeshGroup* meshGroup; + int version; + bool flat; + + std::vector indices; + GLuint vboPositions; + GLuint vboNormals; + GLuint vboTexcoords; + GLuint ibo; +}; + +class OpenGLES2Renderer : public Direct3DRMRenderer { +public: + static Direct3DRMRenderer* Create(DWORD width, DWORD height, float anisotropic); + OpenGLES2Renderer(DWORD width, DWORD height, float anisotropic, SDL_GLContext context, GLuint shaderProgram); + ~OpenGLES2Renderer() override; + + void PushLights(const SceneLight* lightsArray, size_t count) override; + void SetProjection(const D3DRMMATRIX4D& projection, D3DVALUE front, D3DVALUE back) override; + void SetFrustumPlanes(const Plane* frustumPlanes) override; + Uint32 GetTextureId(IDirect3DRMTexture* texture, bool isUI, float scaleX, float scaleY) override; + Uint32 GetMeshId(IDirect3DRMMesh* mesh, const MeshGroup* meshGroup) override; + HRESULT BeginFrame() override; + void EnableTransparency() override; + void SubmitDraw( + DWORD meshId, + const D3DRMMATRIX4D& modelViewMatrix, + const D3DRMMATRIX4D& worldMatrix, + const D3DRMMATRIX4D& viewMatrix, + const Matrix3x3& normalMatrix, + const Appearance& appearance + ) override; + HRESULT FinalizeFrame() override; + void Resize(int width, int height, const ViewportTransform& viewportTransform) override; + void Clear(float r, float g, float b) override; + void Flip() override; + void Draw2DImage(Uint32 textureId, const SDL_Rect& srcRect, const SDL_Rect& dstRect, FColor color) override; + void Download(SDL_Surface* target) override; + void SetDither(bool dither) override; + +private: + void AddTextureDestroyCallback(Uint32 id, IDirect3DRMTexture* texture); + void AddMeshDestroyCallback(Uint32 id, IDirect3DRMMesh* mesh); + bool UploadTexture(SDL_Surface* source, GLuint& outTexId, bool isUI); + + MeshGroup m_uiMesh; + GLES2MeshCacheEntry m_uiMeshCache; + std::vector m_textures; + std::vector m_meshs; + D3DRMMATRIX4D m_projection; + SDL_Surface* m_renderedImage = nullptr; + bool m_dirty = false; + std::vector m_lights; + SDL_GLContext m_context; + float m_anisotropic; + GLuint m_fbo; + GLuint m_colorTarget; + GLuint m_depthTarget; + GLuint m_shaderProgram; + GLuint m_dummyTexture; + GLint m_posLoc; + GLint m_normLoc; + GLint m_texLoc; + GLint m_colorLoc; + GLint m_shinLoc; + GLint m_lightCountLoc; + GLint m_useTextureLoc; + GLint m_textureLoc; + GLint u_lightLocs[3][3]; + GLint m_modelViewMatrixLoc; + GLint m_normalMatrixLoc; + GLint m_projectionMatrixLoc; + ViewportTransform m_viewportTransform; +}; + +inline static void OpenGLES2Renderer_EnumDevice(const IDirect3DMiniwin* d3d, LPD3DENUMDEVICESCALLBACK cb, void* ctx) +{ + Direct3DRMRenderer* device = OpenGLES2Renderer::Create(640, 480, d3d->GetAnisotropic()); + if (!device) { + return; + } + + D3DDEVICEDESC halDesc = {}; + halDesc.dcmColorModel = D3DCOLOR_RGB; + halDesc.dwFlags = D3DDD_DEVICEZBUFFERBITDEPTH; + halDesc.dwDeviceZBufferBitDepth = DDBD_16; + halDesc.dwDeviceRenderBitDepth = DDBD_32; + halDesc.dpcTriCaps.dwTextureCaps = D3DPTEXTURECAPS_PERSPECTIVE; + halDesc.dpcTriCaps.dwShadeCaps = D3DPSHADECAPS_ALPHAFLATBLEND; + halDesc.dpcTriCaps.dwTextureFilterCaps = D3DPTFILTERCAPS_LINEAR; + + const char* extensions = (const char*) glGetString(GL_EXTENSIONS); + if (extensions) { + if (strstr(extensions, "GL_OES_depth24")) { + halDesc.dwDeviceZBufferBitDepth |= DDBD_24; + } + if (strstr(extensions, "GL_OES_depth32")) { + halDesc.dwDeviceZBufferBitDepth |= DDBD_32; + } + } + + delete device; + + D3DDEVICEDESC helDesc = {}; + + EnumDevice(cb, ctx, "OpenGL ES 2.0 HAL", &halDesc, &helDesc, OpenGLES2_GUID); +} diff --git a/packaging/ios/CMakeLists.txt b/packaging/ios/CMakeLists.txt index 418ad518..834b75d6 100644 --- a/packaging/ios/CMakeLists.txt +++ b/packaging/ios/CMakeLists.txt @@ -15,7 +15,7 @@ if(ISLE_BUILD_APP) "${CMAKE_CURRENT_BINARY_DIR}/isle/Info.plist" @ONLY ) - set(RESOURCE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/isle/LaunchScreen.storyboard" "${CMAKE_CURRENT_SOURCE_DIR}/isle/Assets.xcassets") + set(RESOURCE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/isle/Assets.xcassets") target_sources(${ISLE_TARGET_NAME} PRIVATE ${RESOURCE_FILES}) set_source_files_properties(${RESOURCE_FILES} TARGET_DIRECTORY isle diff --git a/packaging/ios/isle/Info.plist.in b/packaging/ios/isle/Info.plist.in index 714324a3..c5d5a80b 100644 --- a/packaging/ios/isle/Info.plist.in +++ b/packaging/ios/isle/Info.plist.in @@ -27,7 +27,7 @@ CFBundleVersion @MACOSX_BUNDLE_BUNDLE_VERSION@ UILaunchStoryboardName - LaunchScreen + NSHighResolutionCapable CSResourcesFileMapped diff --git a/packaging/ios/isle/LaunchScreen.storyboard b/packaging/ios/isle/LaunchScreen.storyboard deleted file mode 100644 index ad167a8a..00000000 --- a/packaging/ios/isle/LaunchScreen.storyboard +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..9bee1096 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +includeBuild('android-project')