diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 695a8afc..c28afcab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -190,3 +190,49 @@ jobs: name: roller-${{ matrix.os }}-${{ matrix.arch }} path: roller-${{ matrix.os }}-${{ matrix.arch }}.* retention-days: 7 + + android: + name: Build Android APK + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Setup mise + uses: jdx/mise-action@v4 + with: + install: true + experimental: true + cache: true + + - name: Setup Java + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "17" + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Install Android SDK packages + run: | + sdkmanager "platforms;android-36" "build-tools;36.0.0" "ndk;27.2.12479018" + echo "sdk.dir=$ANDROID_HOME" > android/local.properties + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: 8.7 + + - name: Build debug APK + run: gradle -p android assembleDebug --no-daemon + + - name: Upload Android APK + uses: actions/upload-artifact@v7 + with: + name: roller-android-debug + path: android/app/build/outputs/apk/debug/app-debug.apk + retention-days: 7 diff --git a/.gitignore b/.gitignore index 1c88b06e..1ac6071e 100644 --- a/.gitignore +++ b/.gitignore @@ -374,6 +374,17 @@ fatdata zig-out/ .zig-cache/ +# Android build artifacts +.idea/ +android/local.properties +android/.gradle/ +android/build/ +android/app/build/ +android/app/src/main/jniLibs/ +*.apk +*.ap_ +*.aab + # Shadercross tool (build locally, not portable) tools/shadercross/lib/ tools/shadercross/zig-out/ diff --git a/PROJECTS/ROLLER/3d.c b/PROJECTS/ROLLER/3d.c index 1c8a2b91..c3436ebd 100644 --- a/PROJECTS/ROLLER/3d.c +++ b/PROJECTS/ROLLER/3d.c @@ -23,6 +23,9 @@ #include "snapshot_scenes.h" #include "rollerinput.h" #include +#if defined(IS_ANDROID) +#include +#endif #include #include #include @@ -1625,6 +1628,24 @@ void race_update(void) initsoundlag(0); lagdone = -1; } + if (screenready && !fadedin) // Start race timing only once the race view is visible + { + game_render_begin_fade(g_pGameRenderer, 1, 0); + fadedin = -1; + holdmusic = 0; + SDL_SetAtomicInt(&iTicksPending, 0); + if (w95) { // Start appropriate music track (title song for replay, game track for race) + if (!MusicCD && !winner_mode && !loading_replay) { + if (replaytype == 2) + iSong = titlesong; + else if (game_track == TRACK_LOAD_COMMUNITY && nummusictracks > 0) + iSong = rand() % nummusictracks + 1; + else + iSong = game_track; + startmusic(iSong); + } + } + } updates = 0; if (g_bSnapshotMode) { // No SDL tick timer in snapshot mode: drive one logical tick per @@ -1632,20 +1653,23 @@ void race_update(void) // unredrawn region cannot leak from the previous frame. SnapshotZeroScreen(); SnapshotAdvanceTick(); - } - // Cap drained ticks per frame to prevent spiral-of-death: if a slow frame - // backs up iTicksPending, catching up burns main-thread time and starves - // the next render, which backs up more ticks, and so on. - int iDrained = 0; - while (iDrained < 4 && (iPendingTicks = SDL_GetAtomicInt(&iTicksPending)) != 0) { - // Claim one pending tick with CAS. The timer thread can enqueue between - // this read and update, so a plain read/add pair can race, especially - // when replay rewind changes the pending count's sign. - int iNextPendingTicks = iPendingTicks > 0 ? iPendingTicks - 1 : iPendingTicks + 1; - if (!SDL_CompareAndSwapAtomicInt(&iTicksPending, iPendingTicks, iNextPendingTicks)) - continue; - game_tick_step(); - ++iDrained; + } else if (fadedin || replaytype == 2) { + // Cap drained ticks per frame to prevent spiral-of-death: if a slow frame + // backs up iTicksPending, catching up burns main-thread time and starves + // the next render, which backs up more ticks, and so on. + int iDrained = 0; + while (iDrained < 4 && (iPendingTicks = SDL_GetAtomicInt(&iTicksPending)) != 0) { + // Claim one pending tick with CAS. The timer thread can enqueue between + // this read and update, so a plain read/add pair can race, especially + // when replay rewind changes the pending count's sign. + int iNextPendingTicks = iPendingTicks > 0 ? iPendingTicks - 1 : iPendingTicks + 1; + if (!SDL_CompareAndSwapAtomicInt(&iTicksPending, iPendingTicks, iNextPendingTicks)) + continue; + game_tick_step(); + ++iDrained; + } + } else { + SDL_SetAtomicInt(&iTicksPending, 0); } if (replaytype == 2 && !frontend_on && ticks != currentreplayframe) game_tick_step(); @@ -1792,25 +1816,6 @@ void race_update(void) } else if (forwarding) { slowing = -1; } - if (screenready) // Handle screen fade-in and music startup - { - if (!fadedin) { - game_render_begin_fade(g_pGameRenderer, 1, 0); - fadedin = -1; - holdmusic = 0; - if (w95) { // Start appropriate music track (title song for replay, game track for race) - if (!MusicCD && !winner_mode && !loading_replay) { - if (replaytype == 2) - iSong = titlesong; - else if (game_track == TRACK_LOAD_COMMUNITY && nummusictracks > 0) - iSong = rand() % nummusictracks + 1; - else - iSong = game_track; - startmusic(iSong); - } - } - } - } //NOCD error disabled for ROLLER //if (!intro && replaytype != 2) // Handle CD-ROM validation for copy protection //{ @@ -2107,6 +2112,22 @@ void *trybuffer(uint32 uiSize) return pBuf; } +//------------------------------------------------------------------------------------------------- +//00010890 helper +uint32 getbuffer_size(void *pData) +{ + int iMemBlocksIdx; + + if (!pData) + return 0; + + iMemBlocksIdx = find_mem_block(pData); + if (iMemBlocksIdx < 0) + return 0; + + return mem_blocks[iMemBlocksIdx].uiSize; +} + //------------------------------------------------------------------------------------------------- //000109F0 void fre(void **ppData) @@ -2607,6 +2628,14 @@ void draw_road(uint8 *pScrPtr, int iCarIdx, unsigned int uiViewMode, int iCopyIm //------------------------------------------------------------------------------------------------- //00011930 +#if defined(IS_ANDROID) +int main(int argc, const char **argv, const char **envp); +int SDL_main(int argc, char *argv[]) +{ + return main(argc, (const char **)argv, NULL); +} +#endif + int main(int argc, const char **argv, const char **envp) { int consumed = 0; @@ -2749,6 +2778,18 @@ int main(int argc, const char **argv, const char **envp) i += consumed; } +#if defined(IS_ANDROID) + if (!whiplash_root[0]) { + const char *szExt = SDL_GetAndroidExternalStoragePath(); + if (!szExt) { + ErrorBoxExit("External storage is not available. Cannot locate game data."); + return 1; + } + strncpy(whiplash_root, szExt, sizeof(whiplash_root) - 1); + whiplash_root[sizeof(whiplash_root) - 1] = '\0'; + } +#endif + if (g_bSnapshotMode) { if (g_SnapshotConfig.eKind == SNAPSHOT_KIND_REPLAY && g_SnapshotConfig.szReplayName[0] == '\0') { cli_fprintf(stderr, "ERROR: '--snapshot' requires a replay filename\n"); @@ -2777,7 +2818,7 @@ int main(int argc, const char **argv, const char **envp) } if (iCrashHandlerEnabled) - InitCrashHandler(); + InitCrashHandler(whiplash_root); if (InitSDL(whiplash_root, midi_root) != 0) { return 1; diff --git a/PROJECTS/ROLLER/3d.h b/PROJECTS/ROLLER/3d.h index e20bcfff..782ee57c 100644 --- a/PROJECTS/ROLLER/3d.h +++ b/PROJECTS/ROLLER/3d.h @@ -399,6 +399,7 @@ void copypic(uint8 *pSrc, uint8 *pDest); void init_screen(); void init(); void *getbuffer(uint32 uiSize); +uint32 getbuffer_size(void *pData); void *trybuffer(uint32 uiSize); void fre(void **ppData); void doexit(); diff --git a/PROJECTS/ROLLER/control.c b/PROJECTS/ROLLER/control.c index 8b20467e..d7cea2b2 100644 --- a/PROJECTS/ROLLER/control.c +++ b/PROJECTS/ROLLER/control.c @@ -2236,6 +2236,11 @@ void updatecar2(tCar *pCar) fAbsoluteSpeed = 0.0; fSpeedOverflow = 0.0; } + if (!race_started && iControlTypeCheck == 3) { + fAbsoluteSpeed = 0.0f; + fSpeedOverflow = 0.0f; + pCar->fBaseSpeed = 0.0f; + } dAbsoluteSpeed = fAbsoluteSpeed; pCar->fFinalSpeed = fAbsoluteSpeed; fAbsoluteSpeed = (float)fabs(dAbsoluteSpeed); diff --git a/PROJECTS/ROLLER/crashdump.c b/PROJECTS/ROLLER/crashdump.c index a2960102..f725910a 100644 --- a/PROJECTS/ROLLER/crashdump.c +++ b/PROJECTS/ROLLER/crashdump.c @@ -32,7 +32,9 @@ #include #else #include +#if !defined(IS_ANDROID) #include +#endif #include #include #include @@ -234,8 +236,9 @@ static void RotateCrashFiles(const char *szDir) //------------------------------------------------------------------------------------------------- -static void ResolveExecutableDir(char *szOut, int iOutSize) +static void ResolveExecutableDir(char *szOut, int iOutSize, const char *szDataRoot) { + (void)szDataRoot; DWORD dwLen = GetModuleFileNameA(NULL, szOut, (DWORD)iOutSize); if (dwLen == 0 || dwLen >= (DWORD)iOutSize) { snprintf(szOut, iOutSize, "."); @@ -246,11 +249,12 @@ static void ResolveExecutableDir(char *szOut, int iOutSize) //------------------------------------------------------------------------------------------------- -static void ResolveFallbackCrashDir(char *szOut, int iOutSize) +static void ResolveFallbackCrashDir(char *szOut, int iOutSize, const char *szDataRoot) { const char *pszLocalAppData = getenv("LOCALAPPDATA"); char szRollerDir[CRASHDUMP_MAX_PATH]; + (void)szDataRoot; szOut[0] = '\0'; if (!pszLocalAppData || !pszLocalAppData[0]) return; @@ -364,9 +368,15 @@ static void RotateCrashFiles(const char *szDir) //------------------------------------------------------------------------------------------------- -static void ResolveExecutableDir(char *szOut, int iOutSize) +static void ResolveExecutableDir(char *szOut, int iOutSize, const char *szDataRoot) { -#ifdef IS_LINUX +#if defined(IS_ANDROID) + if (szDataRoot && szDataRoot[0]) { + BuildPath(szOut, iOutSize, szDataRoot, "crashes"); + CreateDirIfNeeded(szOut); + return; + } +#elif defined(IS_LINUX) ssize_t llLen = readlink("/proc/self/exe", szOut, (size_t)iOutSize - 1); if (llLen > 0 && llLen < iOutSize) { szOut[llLen] = '\0'; @@ -387,13 +397,18 @@ static void ResolveExecutableDir(char *szOut, int iOutSize) //------------------------------------------------------------------------------------------------- -static void ResolveFallbackCrashDir(char *szOut, int iOutSize) +static void ResolveFallbackCrashDir(char *szOut, int iOutSize, const char *szDataRoot) { const char *pszHome; szOut[0] = '\0'; -#ifdef IS_MACOS +#if defined(IS_ANDROID) + if (szDataRoot && szDataRoot[0]) { + BuildPath(szOut, iOutSize, szDataRoot, "crashes"); + CreateDirIfNeeded(szOut); + } +#elif defined(IS_MACOS) pszHome = getenv("HOME"); if (pszHome && pszHome[0]) { char szLogsDir[CRASHDUMP_MAX_PATH]; @@ -509,13 +524,13 @@ static void GetSignalContextRegisters(void *pContext, uint64 *pullPc, uint64 *pu *pullPc = 0; *pullSp = 0; -#if defined(IS_LINUX) && defined(__x86_64__) +#if (defined(IS_LINUX) || defined(IS_ANDROID)) && defined(__x86_64__) { ucontext_t *pUContext = (ucontext_t *)pContext; *pullPc = (uint64)pUContext->uc_mcontext.gregs[REG_RIP]; *pullSp = (uint64)pUContext->uc_mcontext.gregs[REG_RSP]; } -#elif defined(IS_LINUX) && defined(__aarch64__) +#elif (defined(IS_LINUX) || defined(IS_ANDROID)) && defined(__aarch64__) { ucontext_t *pUContext = (ucontext_t *)pContext; *pullPc = (uint64)pUContext->uc_mcontext.pc; @@ -563,7 +578,7 @@ static void WriteMappings(int iFd) { WriteLiteral(iFd, "\nMappings:\n"); -#ifdef IS_LINUX +#if defined(IS_LINUX) || defined(IS_ANDROID) { int iMapsFd = open("/proc/self/maps", O_RDONLY | O_CLOEXEC); if (iMapsFd >= 0) { @@ -587,6 +602,10 @@ static void WriteMappings(int iFd) static void WriteBacktrace(int iFd) { +#if defined(IS_ANDROID) + WriteLiteral(iFd, "\nBacktrace:\n"); + WriteLiteral(iFd, "Backtrace symbols not available on Android\n"); +#else void *pFrames[64]; int iFrameCount; @@ -596,6 +615,7 @@ static void WriteBacktrace(int iFd) backtrace_symbols_fd(pFrames, iFrameCount, iFd); else WriteLiteral(iFd, "No frames captured\n"); +#endif } //------------------------------------------------------------------------------------------------- @@ -665,6 +685,9 @@ static void CrashSignalHandler(int iSig, siginfo_t *pSigInfo, void *pContext) static void WarmUpBacktrace(void) { +#if defined(IS_ANDROID) + return; +#else int iFd = open("/dev/null", O_WRONLY | O_CLOEXEC); void *pFrames[1]; int iFrameCount = backtrace(pFrames, 1); @@ -674,6 +697,7 @@ static void WarmUpBacktrace(void) backtrace_symbols_fd(pFrames, iFrameCount, iFd); close(iFd); } +#endif } //------------------------------------------------------------------------------------------------- @@ -798,13 +822,13 @@ static void BuildStaticCrashInfo(void) //------------------------------------------------------------------------------------------------- -void InitCrashHandler(void) +void InitCrashHandler(const char *szDataRoot) { if (!ShouldEnableCrashHandler()) return; - ResolveExecutableDir(s_szCrashDir, sizeof(s_szCrashDir)); - ResolveFallbackCrashDir(s_szFallbackCrashDir, sizeof(s_szFallbackCrashDir)); + ResolveExecutableDir(s_szCrashDir, sizeof(s_szCrashDir), szDataRoot); + ResolveFallbackCrashDir(s_szFallbackCrashDir, sizeof(s_szFallbackCrashDir), szDataRoot); #ifdef IS_WINDOWS BuildCrashBaseName(".dmp"); diff --git a/PROJECTS/ROLLER/crashdump.h b/PROJECTS/ROLLER/crashdump.h index fbecef62..0ff5f5c7 100644 --- a/PROJECTS/ROLLER/crashdump.h +++ b/PROJECTS/ROLLER/crashdump.h @@ -2,7 +2,7 @@ #define _ROLLER_CRASHDUMP_H //------------------------------------------------------------------------------------------------- -void InitCrashHandler(void); +void InitCrashHandler(const char *szDataRoot); //------------------------------------------------------------------------------------------------- #endif diff --git a/PROJECTS/ROLLER/func2.c b/PROJECTS/ROLLER/func2.c index b320e061..b8715fc0 100644 --- a/PROJECTS/ROLLER/func2.c +++ b/PROJECTS/ROLLER/func2.c @@ -3895,7 +3895,7 @@ void load_fatal_config() fatal_ini_loaded = 0; // Open FATAL.INI file - pFile = fopen("FATAL.INI", "rb"); + pFile = ROLLERfopen("FATAL.INI", "rb"); pFile2 = pFile; if (pFile) { fatal_ini_loaded = -1; diff --git a/PROJECTS/ROLLER/game_render_software.c b/PROJECTS/ROLLER/game_render_software.c index 852881ac..60a731dc 100644 --- a/PROJECTS/ROLLER/game_render_software.c +++ b/PROJECTS/ROLLER/game_render_software.c @@ -43,6 +43,33 @@ struct GameRendererSoftware { TextureHandle texIdxToHandle[32]; }; +static int game_render_sw_block_is_valid(tBlockHeader *blocks, uint32 blockBytes, + int blockIdx) { + if (!blocks || blockBytes < sizeof(tBlockHeader) || blockIdx < 0) + return 0; + + uint32 headerOffset = (uint32)blockIdx * (uint32)sizeof(tBlockHeader); + if (headerOffset > blockBytes - sizeof(tBlockHeader)) + return 0; + + tBlockHeader *block = &blocks[blockIdx]; + if (block->iWidth <= 0 || block->iHeight <= 0 || block->iDataOffset <= 0) + return 0; + if (block->iWidth > 640 || block->iHeight > 400) + return 0; + + uint32 width = (uint32)block->iWidth; + uint32 height = (uint32)block->iHeight; + if (width > UINT32_MAX / height) + return 0; + uint32 pixelBytes = width * height; + uint32 dataOffset = (uint32)block->iDataOffset; + if (dataOffset > blockBytes || pixelBytes > blockBytes - dataOffset) + return 0; + + return 1; +} + // --------------------------------------------------------------------------- // Lifecycle // --------------------------------------------------------------------------- @@ -334,8 +361,18 @@ void game_render_sw_sprite(GameRendererSoftware *sw, int slot, int blockIdx, void game_render_sw_print_block(GameRendererSoftware *sw, int slot, int blockIdx, uint8 *pDest) { - if (slot >= 0 && slot < GAME_RENDER_MAX_BLOCK_SLOTS && sw->blocks[slot]) - print_block(pDest, sw->blocks[slot], blockIdx); + if (!sw || slot < 0 || slot >= GAME_RENDER_MAX_BLOCK_SLOTS) + return; + + tBlockHeader *blocks = sw->blocks[slot]; + uint32 blockBytes = getbuffer_size(blocks); + if (!game_render_sw_block_is_valid(blocks, blockBytes, blockIdx)) { + SDL_Log("game_render_sw_print_block: skipped invalid block slot=%d idx=%d ptr=%p bytes=%u", + slot, blockIdx, (void *)blocks, blockBytes); + return; + } + + print_block(pDest, blocks, blockIdx); } // --------------------------------------------------------------------------- diff --git a/PROJECTS/ROLLER/png_writer.c b/PROJECTS/ROLLER/png_writer.c index bee98b0e..c2978c60 100644 --- a/PROJECTS/ROLLER/png_writer.c +++ b/PROJECTS/ROLLER/png_writer.c @@ -1,6 +1,8 @@ #include "png_writer.h" #include +#if !defined(IS_ANDROID) #include +#endif //------------------------------------------------------------------------------------------------- @@ -10,6 +12,14 @@ int RollerWriteIndexedPng(const char *szPath, int iWidth, int iHeight) { +#if defined(IS_ANDROID) + (void)szPath; + (void)pIndexedBuf; + (void)pPalette; + (void)iWidth; + (void)iHeight; + return 1; +#else if (!szPath || !pIndexedBuf || !pPalette || iWidth <= 0 || iHeight <= 0) return 1; @@ -44,6 +54,7 @@ int RollerWriteIndexedPng(const char *szPath, bool bOk = IMG_SavePNG(surface, szPath); SDL_DestroySurface(surface); return bOk ? 0 : 1; +#endif } //------------------------------------------------------------------------------------------------- diff --git a/PROJECTS/ROLLER/replay.c b/PROJECTS/ROLLER/replay.c index 25284e97..a688718c 100644 --- a/PROJECTS/ROLLER/replay.c +++ b/PROJECTS/ROLLER/replay.c @@ -242,12 +242,12 @@ void setreplaytrack() lastintro = iIntroFileNum1; sprintf(replayfilename, "INTRO%d.GSS", iIntroFileNum1); } - pFile = fopen(replayfilename, "rb"); // Open the selected intro GSS file + pFile = ROLLERfopen(replayfilename, "rb"); // Open the selected intro GSS file if (!pFile && introfiles > 1) { iIntroFileNum2 = SelectIntroFile(lastintro); // Fallback: try different intro file if first failed to open lastintro = iIntroFileNum2; sprintf(replayfilename, "INTRO%d.GSS", iIntroFileNum2); - pFile = fopen(replayfilename, "rb"); + pFile = ROLLERfopen(replayfilename, "rb"); } if (pFile) { fread(buffer, 1u, 1u, pFile); // Read track number from GSS file diff --git a/PROJECTS/ROLLER/roller.c b/PROJECTS/ROLLER/roller.c index 5b0dc0e6..61d139a7 100644 --- a/PROJECTS/ROLLER/roller.c +++ b/PROJECTS/ROLLER/roller.c @@ -17,14 +17,20 @@ #include #include #include +#if !defined(IS_ANDROID) #include +#endif +#if !defined(IS_ANDROID) #include +#endif #include #include +#if !defined(IS_ANDROID) #include #include #include #include +#endif #ifdef IS_WINDOWS #include #include @@ -45,11 +51,15 @@ #include #define O_BINARY 0 //linux does not differentiate between text and binary #endif -#ifdef IS_LINUX +#if defined(IS_ANDROID) +#define CDROM_SUPPORT 0 +#elif defined(IS_LINUX) #include #include #include #define CDROM_SUPPORT 1 +#else +#define CDROM_SUPPORT 0 #endif //------------------------------------------------------------------------------------------------- @@ -465,11 +475,24 @@ int InitSDL(char *whiplash_root, const char *midi_root) return 1; } } else { +#if defined(IS_ANDROID) + const char *szExt = SDL_GetAndroidExternalStoragePath(); + if (!szExt) { + ErrorBoxExit("External storage is not available. Cannot locate game data."); + return 1; + } + strncpy(whiplash_root, szExt, 259); + whiplash_root[259] = '\0'; + chdir(whiplash_root); +#else // Change to the base path of the application - strncpy(whiplash_root, SDL_GetBasePath(), 260); - if (whiplash_root) { + const char *szBasePath = SDL_GetBasePath(); + if (szBasePath) { + strncpy(whiplash_root, szBasePath, 259); + whiplash_root[259] = '\0'; chdir(whiplash_root); } +#endif } g_pTimerMutex = SDL_CreateMutex(); @@ -486,7 +509,11 @@ int InitSDL(char *whiplash_root, const char *midi_root) InputLoadStartupConfig(); #endif - s_pWindow = SDL_CreateWindow("ROLLER", 640, 400, SDL_WINDOW_RESIZABLE); + SDL_WindowFlags uiWindowFlags = SDL_WINDOW_RESIZABLE; +#if defined(IS_ANDROID) + uiWindowFlags |= SDL_WINDOW_FULLSCREEN; +#endif + s_pWindow = SDL_CreateWindow("ROLLER", 640, 400, uiWindowFlags); if (!s_pWindow) { ErrorBoxExit("Couldn't create window: %s", SDL_GetError()); return 1; @@ -533,8 +560,10 @@ int InitSDL(char *whiplash_root, const char *midi_root) return 1; } +#if !defined(IS_ANDROID) SDL_Surface *pIcon = IMG_Load("roller.ico"); SDL_SetWindowIcon(s_pWindow, pIcon); +#endif // Move the window to the display where the mouse is currently located float mouseX, mouseY; @@ -554,7 +583,11 @@ int InitSDL(char *whiplash_root, const char *midi_root) localMidiPath[lenMidiPath+1] = '\0'; } } else { +#if defined(IS_ANDROID) + midi_root = SDL_GetAndroidExternalStoragePath(); +#else midi_root = SDL_GetBasePath(); +#endif if (midi_root) { strcpy(localMidiPath, midi_root); } else { @@ -593,6 +626,25 @@ void InitFATDATA(const char *szDataRoot) // check if data folder exists (case-insensitive for linux) if (!ROLLERdirexists("./FATDATA") && !ROLLERdirexists("./fatdata")) { +#if defined(IS_ANDROID) + const char *szExternal = SDL_GetAndroidExternalStoragePath(); + const char *szShown = szExternal ? szExternal : ""; + char szMessage[1024]; + + snprintf(szMessage, sizeof(szMessage), + "ROLLER needs the FATDATA assets from a retail copy of the game.\n\n" + "Copy your game data so the folders end up here:\n\n" + "%s/FATDATA\n" + "%s/TRACKS (community tracks)\n" + "%s/REPLAYS (saved replays)\n\n" + "For CD music, also copy your extracted track WAVs to:\n" + "%s/audio\n\n" + "Then relaunch ROLLER.", + szShown, szShown, szShown, szShown); + + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, + "FATDATA not found", szMessage, s_pWindow); +#else debug_overlay_set_visible(s_pDebugOverlay, true); PresentDebugOverlayOnly(); @@ -634,6 +686,7 @@ void InitFATDATA(const char *szDataRoot) ROLLERRefreshStartupOverlay(); } } +#endif } //check if extraction was successful diff --git a/PROJECTS/ROLLER/rollersound.c b/PROJECTS/ROLLER/rollersound.c index 55e43cb8..cc522caf 100644 --- a/PROJECTS/ROLLER/rollersound.c +++ b/PROJECTS/ROLLER/rollersound.c @@ -2,12 +2,63 @@ #include "roller.h" #include "snapshot.h" #include +#if !defined(IS_ANDROID) #include +#endif //------------------------------------------------------------------------------------------------- #pragma region MIDI #define MIDI_RATE 44100 // not sure if this is the correct rate +#if defined(IS_ANDROID) +int8 MIDIMasterVolume = 127; + +bool MIDI_Init(const char *config_file) +{ + (void)config_file; + SDL_Log("MIDI_Init: WildMidi disabled on Android."); + return false; +} + +void MIDI_Shutdown() +{ +} + +void MIDIInitSong(tInitSong *data) +{ + (void)data; +} + +void MIDIStartSong() +{ +} + +void MIDIStopSong() +{ +} + +void MIDIInitStream() +{ +} + +void MIDIClearStream() +{ +} + +void MIDISetMasterVolume(int8 volume) +{ + if (volume < 0) + volume = 0; + if (volume > 127) + volume = 127; + MIDIMasterVolume = volume; +} + +int MIDIGetMasterVolume() +{ + return MIDIMasterVolume; +} +#else SDL_AudioStream *midi_stream; float midi_volume; midi *midi_music; @@ -209,6 +260,7 @@ int MIDIGetMasterVolume() { return MIDIMasterVolume; } +#endif #pragma endregion //------------------------------------------------------------------------------------------------- diff --git a/PROJECTS/ROLLER/types.h b/PROJECTS/ROLLER/types.h index 54dd16ee..0f12545d 100644 --- a/PROJECTS/ROLLER/types.h +++ b/PROJECTS/ROLLER/types.h @@ -3,10 +3,13 @@ //------------------------------------------------------------------------------------------------- #include #include +#include //------------------------------------------------------------------------------------------------- #if defined (WIN32) || defined (_WIN32) #define IS_WINDOWS +#elif defined(__ANDROID__) +#define IS_ANDROID #elif defined(__linux__) || defined(linux) || defined(__linux) #define IS_LINUX #elif defined(__APPLE__) || defined(__MACH__) @@ -122,6 +125,25 @@ typedef uint16_t uint16; typedef uint32_t uint32; typedef uint64_t uint64; +typedef enum +{ + PHONE_CONTROLS_DISABLED = 0, + PHONE_CONTROLS_TILT_TURN, + PHONE_CONTROLS_TOUCH_TURN +} ePhoneControls; + +extern ePhoneControls g_ePhoneControls; + +typedef struct +{ + int iId; + int iX; + int iY; + int iWidth; + int iHeight; + bool bVisible; +} tTouchButton; + // Windows-specific types when needed #if !defined(IS_WINDOWS) typedef uint32_t DWORD; diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 00000000..b761680a --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,135 @@ +plugins { id 'com.android.application' } + +def generatedAssetsDir = layout.buildDirectory.dir('generated/assets/roller') + +android { + namespace 'racing.fatal.roller' + compileSdk 36 + ndkVersion '27.2.12479018' + + defaultConfig { + applicationId 'racing.fatal.roller' + minSdk 26 + targetSdk 36 + versionCode 1 + versionName '0.1.0' + ndk { abiFilters 'arm64-v8a', 'x86_64' } + } + + buildFeatures { prefab true } + + sourceSets { + main { + jniLibs.srcDirs = ['src/main/jniLibs'] + assets.srcDirs = ['src/main/assets', generatedAssetsDir.get().asFile] + } + } + + buildTypes { + debug { debuggable true } + release { minifyEnabled false } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } +} + +dependencies { + implementation files('libs/SDL3-3.2.22.aar') +} + +tasks.register('syncMidiAssets', Copy) { + from rootProject.file('../midi') + into new File(generatedAssetsDir.get().asFile, 'midi') +} + +def sdlAar = file('libs/SDL3-3.2.22.aar') +def sdlPrefabDir = layout.buildDirectory.dir('sdl3-prefab') + +tasks.register('extractSdl3Prefab', Copy) { + from zipTree(sdlAar) + include 'prefab/**' + into sdlPrefabDir +} + +def isWindowsHost = System.getProperty('os.name').toLowerCase().contains('windows') +def zigExecutable = System.getenv('ZIG_EXE') ?: (isWindowsHost ? 'zig.exe' : 'zig') +def ndkHostTag = { + def osName = System.getProperty('os.name').toLowerCase() + if (osName.contains('windows')) { + return 'windows-x86_64' + } + if (osName.contains('mac')) { + return 'darwin-x86_64' + } + return 'linux-x86_64' +} +def requireNdkDir = { + def dir = android.ndkDirectory + if (dir == null) { + throw new GradleException('Android NDK not found. Install an NDK or set ndk.dir in local.properties.') + } + return dir +} + +def androidApi = '26' +def androidAbis = [ + [abi: 'arm64-v8a', triple: "aarch64-linux-android.${androidApi}", libTriple: 'aarch64-linux-android', taskName: 'zigBuildArm64V8a'], + [abi: 'x86_64', triple: "x86_64-linux-android.${androidApi}", libTriple: 'x86_64-linux-android', taskName: 'zigBuildX86_64'], +] + +def previousZigTask = null +androidAbis.each { cfg -> + def previousTask = previousZigTask + def provider = tasks.register(cfg.taskName, Exec) { + dependsOn tasks.named('extractSdl3Prefab') + workingDir rootProject.file('..') + + doFirst { + def ndkDir = requireNdkDir() + def sdlIncludeDir = sdlPrefabDir.get().dir('prefab/modules/SDL3-Headers/include').asFile + def sdlLibDir = sdlPrefabDir.get().dir("prefab/modules/SDL3-shared/libs/android.${cfg.abi}").asFile + commandLine zigExecutable, 'build', + "-Dtarget=${cfg.triple}", + '-Doptimize=ReleaseFast', + "-Dandroid-ndk=${ndkDir}", + "-Dandroid-api=${androidApi}", + "-Dandroid-ndk-host=${ndkHostTag()}", + "-Dsdl-android-include=${sdlIncludeDir.absolutePath}", + "-Dsdl-android-lib=${sdlLibDir.absolutePath}" + } + + doLast { + def ndkDir = requireNdkDir() + def jniDir = file("src/main/jniLibs/${cfg.abi}") + copy { + from rootProject.file('../zig-out/lib/libmain.so') + into jniDir + } + copy { + from "${ndkDir}/toolchains/llvm/prebuilt/${ndkHostTag()}/sysroot/usr/lib/${cfg.libTriple}/libc++_shared.so" + into jniDir + } + copy { + from zipTree(sdlAar) + include "prefab/modules/SDL3-shared/libs/android.${cfg.abi}/libSDL3.so" + into jniDir + eachFile { path = name } + includeEmptyDirs = false + } + } + } + if (previousTask != null) { + provider.configure { mustRunAfter previousTask } + } + previousZigTask = provider +} + +afterEvaluate { + tasks.named('preBuild').configure { + dependsOn tasks.named('syncMidiAssets') + dependsOn androidAbis.collect { it.taskName } + } +} diff --git a/android/app/libs/SDL3-3.2.22.aar b/android/app/libs/SDL3-3.2.22.aar new file mode 100644 index 00000000..6dbbde0c Binary files /dev/null and b/android/app/libs/SDL3-3.2.22.aar differ diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..d4e2a2fc --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/racing/fatal/roller/RollerActivity.java b/android/app/src/main/java/racing/fatal/roller/RollerActivity.java new file mode 100644 index 00000000..ccb0d09b --- /dev/null +++ b/android/app/src/main/java/racing/fatal/roller/RollerActivity.java @@ -0,0 +1,68 @@ +package racing.fatal.roller; + +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowInsetsController; +import android.view.WindowManager; + +import org.libsdl.app.SDLActivity; + +public class RollerActivity extends SDLActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + requestWindowFeature(Window.FEATURE_NO_TITLE); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + super.onCreate(savedInstanceState); + enterFullscreen(); + } + + @Override + protected String[] getLibraries() { + return new String[] { + "SDL3", + "main", + }; + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) { + enterFullscreen(); + } + } + + private void enterFullscreen() { + Window window = getWindow(); + window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + WindowManager.LayoutParams attrs = window.getAttributes(); + attrs.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + window.setAttributes(attrs); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + WindowInsetsController controller = window.getInsetsController(); + if (controller != null) { + controller.hide(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars()); + controller.setSystemBarsBehavior( + WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } + } else { + window.getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + } +} diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..c22de855 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,13 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 00000000..48978475 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,3 @@ +plugins { + id 'com.android.application' version '8.5.2' apply false +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 00000000..cf102c42 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,4 @@ +android.useAndroidX=true +android.prefab=true +android.suppressUnsupportedCompileSdk=36 +org.gradle.jvmargs=-Xmx2048m diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..b1b8ef56 Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..f3000444 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100644 index 00000000..b9bb139f --- /dev/null +++ b/android/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# 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 ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# 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 + if ! command -v java >/dev/null 2>&1 + then + 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 +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# 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"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 00000000..24c62d56 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,82 @@ +@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 +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +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% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 00000000..4d7998c8 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,4 @@ +pluginManagement { repositories { google(); mavenCentral(); gradlePluginPortal() } } +dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS); repositories { google(); mavenCentral() } } +rootProject.name = 'ROLLER' +include ':app' diff --git a/build.zig b/build.zig index 79aacc28..50d0cbdf 100644 --- a/build.zig +++ b/build.zig @@ -11,6 +11,12 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + const bAndroid = target.result.abi.isAndroid(); + const android_ndk = b.option([]const u8, "android-ndk", "Path to Android NDK") orelse ""; + const android_api = b.option([]const u8, "android-api", "Android API level for NDK library path") orelse "26"; + const android_ndk_host = b.option([]const u8, "android-ndk-host", "Android NDK prebuilt host tag") orelse androidNdkHostTag(); + const sdl_android_include = b.option([]const u8, "sdl-android-include", "Path to SDL Android AAR prefab include directory") orelse ""; + const sdl_android_lib = b.option([]const u8, "sdl-android-lib", "Path to SDL Android AAR shared library directory") orelse ""; const crash_debug = b.option(bool, "crash-debug", "Enable crash dump friendly C build flags") orelse false; const c_flags: []const []const u8 = if (crash_debug) &.{ "-fwrapv", "-fno-omit-frame-pointer" } @@ -83,7 +89,6 @@ pub fn build(b: *std.Build) void { "PROJECTS/ROLLER/replay.c", "PROJECTS/ROLLER/roller.c", "PROJECTS/ROLLER/rollerinput.c", - "PROJECTS/ROLLER/rollercd.c", "PROJECTS/ROLLER/rollercomms.c", "PROJECTS/ROLLER/rollersound.c", "PROJECTS/ROLLER/snapshot.c", @@ -96,12 +101,69 @@ pub fn build(b: *std.Build) void { }, }); - const exe = b.addExecutable(.{ + if (!bAndroid) { + exe_mod.addCSourceFiles(.{ + .flags = c_flags, + .files = &.{ + "PROJECTS/ROLLER/rollercd.c", + }, + }); + } + + const exe = if (bAndroid) b.addLibrary(.{ + .name = "main", + .linkage = .dynamic, + .root_module = exe_mod, + }) else b.addExecutable(.{ .name = "roller", .root_module = exe_mod, }); - switch (target.result.os.tag) { + if (bAndroid) { + exe.linker_allow_shlib_undefined = true; + exe_mod.addCMacro("SDL_MAIN_HANDLED", "1"); + + if (android_ndk.len > 0) { + const triple = androidNdkLibTriple(target.result) orelse + @panic("unsupported Android target architecture"); + const sysroot = b.pathJoin(&.{ + android_ndk, + "toolchains/llvm/prebuilt", + android_ndk_host, + "sysroot", + }); + const libc_write_files = b.addWriteFiles(); + const android_libc_file = libc_write_files.add( + "android-libc.txt", + b.fmt( + \\include_dir={s} + \\sys_include_dir={s} + \\crt_dir={s} + \\msvc_lib_dir= + \\kernel32_lib_dir= + \\gcc_dir={s} + \\ + , .{ + b.pathJoin(&.{ sysroot, "usr/include", triple }), + b.pathJoin(&.{ sysroot, "usr/include" }), + b.pathJoin(&.{ sysroot, "usr/lib", triple, android_api }), + b.pathJoin(&.{ sysroot, "usr/lib", triple, android_api }), + }), + ); + exe.setLibCFile(android_libc_file); + exe_mod.addLibraryPath(.{ .cwd_relative = b.pathJoin(&.{ + sysroot, + "usr/lib", + triple, + android_api, + }) }); + } + + exe.linkSystemLibrary("log"); + exe.linkSystemLibrary("android"); + exe.linkSystemLibrary("GLESv2"); + exe.linkSystemLibrary("EGL"); + } else switch (target.result.os.tag) { .windows => { exe_mod.addCMacro("WILDMIDI_STATIC", "1"); @@ -132,7 +194,7 @@ pub fn build(b: *std.Build) void { exe.step.dependOn(&scene_render_seam_check.step); } - configureDependencies(b, exe, target, optimize); + configureDependencies(b, exe, target, optimize, bAndroid, sdl_android_include, sdl_android_lib); const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); @@ -383,57 +445,102 @@ fn configureSnapshotTests( test_snapshots.dependOn(&diff_check.step); } -fn configureDependencies(b: *Build, exe: *Compile, target: ResolvedTarget, optimize: OptimizeMode) void { +fn configureDependencies( + b: *Build, + exe: *Compile, + target: ResolvedTarget, + optimize: OptimizeMode, + bAndroid: bool, + sdl_android_include: []const u8, + sdl_android_lib: []const u8, +) void { const exe_mod = exe.root_module; - // build dependencies - const wildmidi = b.dependency("wildmidi", .{ - .target = target, - .optimize = optimize, - }); - const wildmidi_lib = wildmidi.artifact("wildmidi"); + var cflags = compile_flagz.addCompileFlags(b); - const sdl_image = b.dependency("SDL_image", .{ - .target = target, - .optimize = optimize, - }); - const sdl_image_lib = sdl_image.artifact("SDL3_image"); + if (!bAndroid) { + const wildmidi = b.dependency("wildmidi", .{ + .target = target, + .optimize = optimize, + }); + exe_mod.addIncludePath(wildmidi.builder.path("include")); + exe_mod.linkLibrary(wildmidi.artifact("wildmidi")); + cflags.addIncludePath(wildmidi.builder.path("include")); + } - const sdl = b.dependency("sdl", .{ - .target = target, - .optimize = optimize, - .sanitize_c = .off, - .lto = .none, - }); - const sdl_lib = sdl.artifact("SDL3"); + if (bAndroid and sdl_android_include.len > 0) { + const sdl_include_path = LazyPath{ .cwd_relative = sdl_android_include }; + exe_mod.addIncludePath(sdl_include_path); + cflags.addIncludePath(sdl_include_path); + if (sdl_android_lib.len > 0) { + exe_mod.addLibraryPath(.{ .cwd_relative = sdl_android_lib }); + exe.linkSystemLibrary("SDL3"); + } + } else { + const sdl = b.dependency("sdl", .{ + .target = target, + .optimize = optimize, + .sanitize_c = .off, + .lto = .none, + }); + exe_mod.addIncludePath(sdl.builder.path("include")); + cflags.addIncludePath(sdl.builder.path("include")); - const libcdio = b.dependency("libcdio", .{ - .target = target, - .optimize = optimize, - }); - const libcdio_lib = libcdio.artifact("cdio"); + if (!bAndroid) + exe_mod.linkLibrary(sdl.artifact("SDL3")); + } - exe_mod.linkLibrary(sdl_lib); - exe_mod.linkLibrary(sdl_image_lib); - exe_mod.linkLibrary(wildmidi_lib); - exe_mod.linkLibrary(libcdio_lib); + if (!bAndroid) { + const sdl_image = b.dependency("SDL_image", .{ + .target = target, + .optimize = optimize, + }); + const sdl_image_lib = sdl_image.artifact("SDL3_image"); - const sdl_image_source = sdl_image.builder.dependency("SDL_image", .{ - .lto = .none, - }); + const sdl_image_source = sdl_image.builder.dependency("SDL_image", .{ + .lto = .none, + }); - var cflags = compile_flagz.addCompileFlags(b); - cflags.addIncludePath(sdl.builder.path("include")); - cflags.addIncludePath(sdl_image_source.builder.path("include")); - cflags.addIncludePath(wildmidi.builder.path("include")); - cflags.addIncludePath(libcdio.builder.path("include")); - cflags.addIncludePath(libcdio.builder.path("zig-config")); + const libcdio = b.dependency("libcdio", .{ + .target = target, + .optimize = optimize, + }); + const libcdio_lib = libcdio.artifact("cdio"); + + exe_mod.linkLibrary(sdl_image_lib); + exe_mod.linkLibrary(libcdio_lib); + exe_mod.addIncludePath(sdl_image_source.builder.path("include")); + exe_mod.addIncludePath(libcdio.builder.path("include")); + exe_mod.addIncludePath(libcdio.builder.path("zig-config")); + cflags.addIncludePath(sdl_image_source.builder.path("include")); + cflags.addIncludePath(libcdio.builder.path("include")); + cflags.addIncludePath(libcdio.builder.path("zig-config")); + } cflags.addIncludePath(b.path("external/Nuklear-4.13.2")); const cflags_step = b.step("compile-flags", "Generate compile flags"); cflags_step.dependOn(&cflags.step); } +fn androidNdkLibTriple(target: std.Target) ?[]const u8 { + return switch (target.cpu.arch) { + .aarch64 => "aarch64-linux-android", + .x86_64 => "x86_64-linux-android", + .arm => "arm-linux-androideabi", + .x86 => "i686-linux-android", + else => null, + }; +} + +fn androidNdkHostTag() []const u8 { + return switch (host_builtin.os.tag) { + .windows => "windows-x86_64", + .linux => "linux-x86_64", + .macos => "darwin-x86_64", + else => "linux-x86_64", + }; +} + fn pythonExe() []const u8 { return if (host_builtin.os.tag == .windows) "python" else "python3"; }