From a2fdf9849887de5f85ad51007eb1facb3f445cd5 Mon Sep 17 00:00:00 2001 From: KiritoDv Date: Sat, 9 Aug 2025 13:30:21 -0600 Subject: [PATCH 01/73] Fixed 32 bit issues --- include/sf64audio_provisional.h | 4 ++-- src/audio/audio_load.c | 2 +- src/port/resource/importers/audio/AudioTableFactory.cpp | 4 ++-- src/port/resource/type/audio/AudioTable.h | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/include/sf64audio_provisional.h b/include/sf64audio_provisional.h index 4259928b4..5c27e7335 100644 --- a/include/sf64audio_provisional.h +++ b/include/sf64audio_provisional.h @@ -847,12 +847,12 @@ typedef struct { typedef struct { /* 0x00 */ s16 numEntries; /* 0x02 */ s16 unkMediumParam; - /* 0x04 */ uintptr_t romAddr; + /* 0x04 */ u64 romAddr; /* 0x08 */ char pad[8]; } AudioTableBase; typedef struct { - /* 0x00 */ uintptr_t romAddr; + /* 0x00 */ u64 romAddr; /* 0x04 */ u32 size; /* 0x08 */ s8 medium; /* 0x09 */ s8 cachePolicy; diff --git a/src/audio/audio_load.c b/src/audio/audio_load.c index dccbb28f6..9d3766957 100644 --- a/src/audio/audio_load.c +++ b/src/audio/audio_load.c @@ -713,7 +713,7 @@ void* AudioLoad_AsyncLoadInner(s32 tableType, s32 id, s32 nChunks, s32 retData, u8* ramAddr; s32 medium; s32 cachePolicy; - u32 romAddr; + u64 romAddr; s32 loadStatus = LOAD_STATUS_COMPLETE; switch (tableType) { diff --git a/src/port/resource/importers/audio/AudioTableFactory.cpp b/src/port/resource/importers/audio/AudioTableFactory.cpp index 7833e8bce..62b308276 100644 --- a/src/port/resource/importers/audio/AudioTableFactory.cpp +++ b/src/port/resource/importers/audio/AudioTableFactory.cpp @@ -26,8 +26,8 @@ std::shared_ptr ResourceFactoryBinaryAudioTableV0::ReadResource int16_t sd3 = reader->ReadInt16(); table->mEntries.push_back({ - (uintptr_t) crc, - size, medium, policy, sd1, sd2, sd3 + crc, size, medium, + policy, sd1, sd2, sd3 }); } diff --git a/src/port/resource/type/audio/AudioTable.h b/src/port/resource/type/audio/AudioTable.h index 42fe8e299..f29268940 100644 --- a/src/port/resource/type/audio/AudioTable.h +++ b/src/port/resource/type/audio/AudioTable.h @@ -7,12 +7,12 @@ namespace SF64 { struct AudioTableBase { /* 0x00 */ int16_t numEntries; /* 0x02 */ int16_t unkMediumParam; - /* 0x04 */ uintptr_t romAddr; + /* 0x04 */ uint64_t romAddr; /* 0x08 */ char pad[8]; }; struct AudioTableEntry { - /* 0x00 */ uintptr_t romAddr; + /* 0x00 */ uint64_t romAddr; /* 0x04 */ uint32_t size; /* 0x08 */ int8_t medium; /* 0x09 */ int8_t cachePolicy; From ad19b9d6ccbf474f14edd2dcfdac62244123c2de Mon Sep 17 00:00:00 2001 From: AltoXorg <56553686+Alto1772@users.noreply.github.com> Date: Thu, 31 Jul 2025 09:33:02 +0800 Subject: [PATCH 02/73] Fix non-portable build --- src/port/Engine.cpp | 24 ++++++++++-------------- src/port/extractor/GameExtractor.cpp | 7 +++++-- tools/Torch | 2 +- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/port/Engine.cpp b/src/port/Engine.cpp index d2bee9ecf..bc0ccb1f7 100644 --- a/src/port/Engine.cpp +++ b/src/port/Engine.cpp @@ -63,6 +63,9 @@ std::vector MemoryPool; GameEngine* GameEngine::Instance; GameEngine::GameEngine() { + // Initialize context properties early to recognize paths properly for non-portable builds + this->context = Ship::Context::CreateUninitializedInstance("Starship", "ship", "starship.cfg.json"); + #ifdef __SWITCH__ Ship::Switch::Init(Ship::PreInitPhase); Ship::Switch::Init(Ship::PostInitPhase); @@ -70,21 +73,12 @@ GameEngine::GameEngine() { std::vector archiveFiles; const std::string main_path = Ship::Context::GetPathRelativeToAppDirectory("sf64.o2r"); -#ifdef __linux__ - const std::string assets_path = Ship::Context::GetPathRelativeToAppBundle("starship.o2r"); -#else - const std::string assets_path = Ship::Context::GetPathRelativeToAppDirectory("starship.o2r"); -#endif - + const std::string assets_path = Ship::Context::LocateFileAcrossAppDirs("starship.o2r"); #ifdef _WIN32 AllocConsole(); #endif - if (!fs::exists("mods")) { - fs::create_directories("mods"); - } - if (std::filesystem::exists(main_path)) { archiveFiles.push_back(main_path); } else { @@ -111,7 +105,11 @@ GameEngine::GameEngine() { } if (const std::string patches_path = Ship::Context::GetPathRelativeToAppDirectory("mods"); - !patches_path.empty() && std::filesystem::exists(patches_path)) { + !patches_path.empty()) { + if (!std::filesystem::exists(patches_path)) { + std::filesystem::create_directories(patches_path); + } + if (std::filesystem::is_directory(patches_path)) { for (const auto& p : std::filesystem::recursive_directory_iterator(patches_path)) { const auto ext = p.path().extension().string(); @@ -127,8 +125,6 @@ GameEngine::GameEngine() { } } - this->context = Ship::Context::CreateUninitializedInstance("Starship", "ship", "starship.cfg.json"); - this->context->InitConfiguration(); // without this line InitConsoleVariables fails at Config::Reload() this->context->InitConsoleVariables(); // without this line the controldeck constructor failes in // ShipDeviceIndexMappingManager::UpdateControllerNamesFromConfig() @@ -849,4 +845,4 @@ extern "C" void GameEngine_Free(void* ptr) { break; } } -} \ No newline at end of file +} diff --git a/src/port/extractor/GameExtractor.cpp b/src/port/extractor/GameExtractor.cpp index f8392bc8b..d75ab3c3a 100644 --- a/src/port/extractor/GameExtractor.cpp +++ b/src/port/extractor/GameExtractor.cpp @@ -54,7 +54,10 @@ std::optional GameExtractor::ValidateChecksum() const { } bool GameExtractor::GenerateOTR() const { - Companion::Instance = new Companion(this->mGameData, ArchiveType::O2R, false); + const std::string assets_path = Ship::Context::GetAppBundlePath(); + const std::string game_path = Ship::Context::GetAppDirectoryPath(); + + Companion::Instance = new Companion(this->mGameData, ArchiveType::O2R, false, assets_path, game_path); try { Companion::Instance->Init(ExportType::Binary); @@ -63,4 +66,4 @@ bool GameExtractor::GenerateOTR() const { } return true; -} \ No newline at end of file +} diff --git a/tools/Torch b/tools/Torch index f75facb20..a81b16b01 160000 --- a/tools/Torch +++ b/tools/Torch @@ -1 +1 @@ -Subproject commit f75facb20883570ed091e8ae733ec0539f606e57 +Subproject commit a81b16b01fdda0dc82e26cb37f19e3e54b275cf4 From c65b7784a5065dc9aba009db78d5ef51e72fe1cb Mon Sep 17 00:00:00 2001 From: AltoXorg <56553686+Alto1772@users.noreply.github.com> Date: Sun, 3 Aug 2025 19:32:27 +0800 Subject: [PATCH 03/73] submodule update Torch, [TEMP] libultraship --- libultraship | 2 +- tools/Torch | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libultraship b/libultraship index 45c4f8d6c..8ddd3650b 160000 --- a/libultraship +++ b/libultraship @@ -1 +1 @@ -Subproject commit 45c4f8d6c19c6176f5e0918917c655ea09ecc212 +Subproject commit 8ddd3650bb174ac6d3ec1eff8a79594ab1d1f308 diff --git a/tools/Torch b/tools/Torch index a81b16b01..cd92cc0f1 160000 --- a/tools/Torch +++ b/tools/Torch @@ -1 +1 @@ -Subproject commit a81b16b01fdda0dc82e26cb37f19e3e54b275cf4 +Subproject commit cd92cc0f161c5e79e36f5dda0d0029edd3fc8d50 From 93545c0b340230e7b938a79c811121ebc96a59b7 Mon Sep 17 00:00:00 2001 From: AltoXorg <56553686+Alto1772@users.noreply.github.com> Date: Sat, 9 Aug 2025 14:48:36 +0800 Subject: [PATCH 04/73] submodule update libultraship --- libultraship | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libultraship b/libultraship index 8ddd3650b..09dfab5fb 160000 --- a/libultraship +++ b/libultraship @@ -1 +1 @@ -Subproject commit 8ddd3650bb174ac6d3ec1eff8a79594ab1d1f308 +Subproject commit 09dfab5fb2a9a047a6e268dc9db2daad9b2ce5f0 From 6984f9cee49df1d343c9f9f7b9074cdc12c11391 Mon Sep 17 00:00:00 2001 From: AltoXorg <56553686+Alto1772@users.noreply.github.com> Date: Sat, 9 Aug 2025 16:06:36 +0800 Subject: [PATCH 05/73] place assets in exe's dir on post build --- CMakeLists.txt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 95e07b00d..d95644030 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,11 +49,6 @@ set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT ${PROJECT set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) endif() -if(NOT EXISTS ${CMAKE_BINARY_DIR}/config.yml) - execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/config.yml" "${CMAKE_BINARY_DIR}/config.yml") - execute_process(COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/assets/" "${CMAKE_BINARY_DIR}/assets/") -endif() - if (MSVC) set(CPP "${CMAKE_C_COMPILER}" "/EP") else() @@ -602,6 +597,13 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang") endif() endif() +add_custom_command( + TARGET ${PROJECT_NAME} POST_BUILD + COMMENT "Copying asset yamls..." + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/config.yml" "$/config.yml" + COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/assets/" "$/assets/" +) + if(NOT CMAKE_SYSTEM_NAME STREQUAL "NintendoSwitch") include(ExternalProject) ExternalProject_Add(TorchExternal @@ -670,4 +672,4 @@ elseif(CMAKE_SYSTEM_NAME MATCHES "Darwin") endif() set(CPACK_PROJECT_CONFIG_FILE ${CMAKE_SOURCE_DIR}/cmake/configure-packaging.cmake) -include(cmake/packaging.cmake) \ No newline at end of file +include(cmake/packaging.cmake) From 3d63f60f0e5762f88b8d04073e69c6cfa8a0bbb5 Mon Sep 17 00:00:00 2001 From: AltoXorg <56553686+Alto1772@users.noreply.github.com> Date: Sun, 10 Aug 2025 10:15:28 +0800 Subject: [PATCH 06/73] add NOMINMAX --- src/port/ui/UIWidgets.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/port/ui/UIWidgets.cpp b/src/port/ui/UIWidgets.cpp index 00f6499b5..1aa98ec15 100644 --- a/src/port/ui/UIWidgets.cpp +++ b/src/port/ui/UIWidgets.cpp @@ -1,3 +1,4 @@ +#define NOMINMAX // prevent defining min/max macros on windows #include "UIWidgets.h" #include "libultraship/src/Context.h" From c02842d31c65ed5a40b886140bb0ce019c1a64d5 Mon Sep 17 00:00:00 2001 From: Pete Woods Date: Mon, 11 Aug 2025 12:50:16 +0100 Subject: [PATCH 07/73] Fix Mac build game controller download --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a3a11da7b..2e6fc63ff 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -98,7 +98,7 @@ jobs: mv build-cmake/starship.o2r starship-release/ mv config.yml starship-release/ mv assets starship-release/ - wget -O starship-release/gamecontrollerdb.txt https://github.com/mdqinc/SDL_GameControllerDB/blob/master/gamecontrollerdb.txt + curl -o starship-release/gamecontrollerdb.txt -sSL 'https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt' - name: Publish packaged artifacts uses: actions/upload-artifact@v4 with: @@ -226,4 +226,4 @@ jobs: Starship.nro config.yml assets - gamecontrollerdb.txt \ No newline at end of file + gamecontrollerdb.txt From 48f3abef5b1a9e3cc0f3807c35e19bfb384fe133 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Wed, 10 Sep 2025 20:28:19 -0300 Subject: [PATCH 08/73] use ARRAY_COUNT for SEQ_NUM_CHANNELS in audio_seqplayer.c --- src/audio/audio_seqplayer.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/audio/audio_seqplayer.c b/src/audio/audio_seqplayer.c index ec0c57830..dd5b2a717 100644 --- a/src/audio/audio_seqplayer.c +++ b/src/audio/audio_seqplayer.c @@ -184,7 +184,7 @@ void AudioSeq_SequencePlayerSetupChannels(SequencePlayer* seqPlayer, u16 channel SequenceChannel* channel; s32 i; - for (i = 0; i < SEQ_NUM_CHANNELS; i++) { + for (i = 0; i < ARRAY_COUNT(seqPlayer->channels); i++) { if (channelBits & 1) { channel = seqPlayer->channels[i]; if ((IS_SEQUENCE_CHANNEL_VALID(channel) == 1) && (seqPlayer == channel->seqPlayer)) { @@ -211,7 +211,7 @@ void AudioSeq_SequencePlayerSetupChannels(SequencePlayer* seqPlayer, u16 channel void AudioSeq_SequencePlayerDisableChannels(SequencePlayer* seqPlayer, u16 channelBitsUnused) { s32 i; - for (i = 0; i < SEQ_NUM_CHANNELS; i++) { + for (i = 0; i < ARRAY_COUNT(seqPlayer->channels); i++) { if (channelBitsUnused & 1) { SequenceChannel* channel = seqPlayer->channels[i]; @@ -1517,7 +1517,7 @@ void AudioSeq_SequencePlayerProcessSequence(SequencePlayer* seqPlayer) { } } - for (i = 0; i < SEQ_NUM_CHANNELS; i++) { + for (i = 0; i < ARRAY_COUNT(seqPlayer->channels); i++) { if (IS_SEQUENCE_CHANNEL_VALID(seqPlayer->channels[i]) == 1) { AudioSeq_SequenceChannelProcessScript(seqPlayer->channels[i]); } From 964bc598221b8c23d70f05e4c2c6b5adfa502029 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Tue, 16 Sep 2025 05:31:16 -0300 Subject: [PATCH 09/73] SEQ_NUM_CHANNELS macro usage --- src/audio/audio_seqplayer.c | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/audio/audio_seqplayer.c b/src/audio/audio_seqplayer.c index dd5b2a717..91a4462c5 100644 --- a/src/audio/audio_seqplayer.c +++ b/src/audio/audio_seqplayer.c @@ -172,7 +172,7 @@ void AudioSeq_SequenceChannelDisable(SequenceChannel* channel) { SequenceChannel* AudioSeq_RequestFreeSeqChannel(void) { s32 i; - for (i = 0; i < 48; i++) { + for (i = 0; i < ARRAY_COUNT(gSeqChannels); i++) { if (gSeqChannels[i].seqPlayer == NULL) { return &gSeqChannels[i]; } @@ -184,7 +184,7 @@ void AudioSeq_SequencePlayerSetupChannels(SequencePlayer* seqPlayer, u16 channel SequenceChannel* channel; s32 i; - for (i = 0; i < ARRAY_COUNT(seqPlayer->channels); i++) { + for (i = 0; i < SEQ_NUM_CHANNELS; i++) { if (channelBits & 1) { channel = seqPlayer->channels[i]; if ((IS_SEQUENCE_CHANNEL_VALID(channel) == 1) && (seqPlayer == channel->seqPlayer)) { @@ -211,7 +211,7 @@ void AudioSeq_SequencePlayerSetupChannels(SequencePlayer* seqPlayer, u16 channel void AudioSeq_SequencePlayerDisableChannels(SequencePlayer* seqPlayer, u16 channelBitsUnused) { s32 i; - for (i = 0; i < ARRAY_COUNT(seqPlayer->channels); i++) { + for (i = 0; i < SEQ_NUM_CHANNELS; i++) { if (channelBitsUnused & 1) { SequenceChannel* channel = seqPlayer->channels[i]; @@ -1516,8 +1516,7 @@ void AudioSeq_SequencePlayerProcessSequence(SequencePlayer* seqPlayer) { } } } - - for (i = 0; i < ARRAY_COUNT(seqPlayer->channels); i++) { + for (i = 0; i < SEQ_NUM_CHANNELS; i++) { if (IS_SEQUENCE_CHANNEL_VALID(seqPlayer->channels[i]) == 1) { AudioSeq_SequenceChannelProcessScript(seqPlayer->channels[i]); } @@ -1568,7 +1567,7 @@ void AudioSeq_InitSequencePlayers(void) { #ifdef AVOID_UB for (j = 0; j < ARRAY_COUNT(gSeqChannels->layers); j++) { #else - for (j = 0; j < 64; + for (j = 0; j < ARRAY_COUNT(gSeqLayers); j++) { // bug: this is ARRAY_COUNT(gSeqLayers) instead of ARRAY_COUNT(gSeqChannels[i].layers) #endif gSeqChannels[i].layers[j] = NULL; @@ -1583,7 +1582,7 @@ void AudioSeq_InitSequencePlayers(void) { } for (i = 0; i < ARRAY_COUNT(gSeqPlayers); i++) { - for (j = 0; j < 16; j++) { + for (j = 0; j < SEQ_NUM_CHANNELS; j++) { gSeqPlayers[i].channels[j] = &gSeqChannelNone; } gSeqPlayers[i].unk_07[0] = -1; From 1f14108143e4ed3c2dde052d46cb1fda00c9511f Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Thu, 16 Oct 2025 23:05:53 -0300 Subject: [PATCH 10/73] Restore beta bomb explosion --- src/engine/fox_beam.c | 16 ++++++++++++++-- src/port/ui/ImguiUI.cpp | 4 ++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/engine/fox_beam.c b/src/engine/fox_beam.c index 7defccb28..ae19d6f71 100644 --- a/src/engine/fox_beam.c +++ b/src/engine/fox_beam.c @@ -1310,7 +1310,14 @@ void PlayerShot_DrawShot(PlayerShot* shot) { Matrix_SetGfxMtx(&gMasterDisp); gSPDisplayList(gMasterDisp++, aOrbDL); } else { - Matrix_Scale(gGfxMatrix, shot->scale, shot->scale, shot->scale, MTXF_APPLY); + + if (CVarGetInteger("gRestoreBetaBombExplosion", 0) != 1) { + Matrix_Scale(gGfxMatrix, shot->scale, shot->scale, shot->scale, MTXF_APPLY); + } else { + Matrix_Scale(gGfxMatrix, shot->scale / 1.5f, shot->scale / 1.5f, shot->scale / 1.5f, + MTXF_APPLY); + } + Matrix_SetGfxMtx(&gMasterDisp); if (gVersusMode) { RCP_SetupDL_49(); @@ -1333,7 +1340,12 @@ void PlayerShot_DrawShot(PlayerShot* shot) { RCP_SetupDL_64_2(); gDPSetPrimColor(gMasterDisp++, 0x00, 0x00, 255, 255, 255, shot->unk_58); } - gSPDisplayList(gMasterDisp++, D_1031EC0); + + if (CVarGetInteger("gRestoreBetaBombExplosion", 0) != 1) { + gSPDisplayList(gMasterDisp++, D_1031EC0); + } else { + gSPDisplayList(gMasterDisp++, D_102FF90); + } } break; case PLAYERSHOT_TANK: diff --git a/src/port/ui/ImguiUI.cpp b/src/port/ui/ImguiUI.cpp index f488aedf4..67dfb7e1f 100644 --- a/src/port/ui/ImguiUI.cpp +++ b/src/port/ui/ImguiUI.cpp @@ -606,6 +606,10 @@ void DrawEnhancementsMenu() { .tooltip = "Restores the missile cutscene bug present in JP 1.0" }); + UIWidgets::CVarCheckbox("Beta: Restore beta bomb explosion", "gRestoreBetaBombExplosion", { + .tooltip = "Restores the beta bomb explosion found inside the game" + }); + UIWidgets::CVarCheckbox("Beta: Restore beta coin", "gRestoreBetaCoin", { .tooltip = "Restores the beta coin that got replaced with the gold ring" }); From 1979ff3208949a2a95f371126dd06a166b8f96e6 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Tue, 21 Oct 2025 01:29:25 -0300 Subject: [PATCH 11/73] Level Boss Tester --- src/engine/fox_enmy.c | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/engine/fox_enmy.c b/src/engine/fox_enmy.c index e9c30568d..a9cf921c2 100644 --- a/src/engine/fox_enmy.c +++ b/src/engine/fox_enmy.c @@ -597,6 +597,31 @@ void Object_LoadLevelObjects(void) { gLastPathChange = 0; +// Level Boss Tester +#if 0 + switch (gCurrentLevel) { + case LEVEL_AQUAS: + static ObjectInit bossaqInit[] = { { 100.6f, -4035, 0, 0, { 0, 0, 0 }, OBJ_BOSS_AQ_BACOON } }; + gLevelObjects = bossaqInit; + break; + + case LEVEL_TITANIA: + static ObjectInit aTiLevelObjects_Boss[] = { { 300.0f, -1000, 0, 0, { 0, 0, 0 }, OBJ_BOSS_TI_GORAS } }; + gLevelObjects = aTiLevelObjects_Boss; + break; + + case LEVEL_SECTOR_X: + static ObjectInit aSxLevelObjects_Boss[] = { { 100.0f, 4000, 0, 1103, { 0, 0, 0 }, OBJ_BOSS_SX_SPYBORG } }; + gLevelObjects = aSxLevelObjects_Boss; + break; + + case LEVEL_SOLAR: + static ObjectInit aSoLevelObjects_Boss[] = { { 100.0f, 0, 0, 0, { 0, 0, 0 }, OBJ_BOSS_SO_VULKAIN } }; + gLevelObjects = aSoLevelObjects_Boss; + break; + } +#endif + for (i = 0, objInit = &gLevelObjects[gObjectLoadIndex]; i < 10000; i++, gObjectLoadIndex++, objInit++) { if (objInit->id <= OBJ_INVALID) { break; From fff98a73be4bf653a210a9d9ce676eabc3dbe3b3 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Tue, 9 Dec 2025 01:26:36 -0300 Subject: [PATCH 12/73] fix solar ending cutscene fox head magnitude --- src/overlays/ovl_i3/fox_so.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/overlays/ovl_i3/fox_so.c b/src/overlays/ovl_i3/fox_so.c index 456ceea57..32b3cc97e 100644 --- a/src/overlays/ovl_i3/fox_so.c +++ b/src/overlays/ovl_i3/fox_so.c @@ -3285,7 +3285,7 @@ void Solar_LevelComplete(Player* player) { gFillScreenAlphaStep = 4; if (gMsgCharIsPrinting) { - player->arwing.teamFaceXrot = (s32) (gGameFrameCount % 2U) * 5.0f; + player->arwing.teamFaceXrot = (s32) (gGameFrameCount & 2) * 2.5f; } switch (gCsFrameCount) { From 20c0b41c1e946d4d273293ccbff22290e891f279 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Sat, 13 Dec 2025 03:35:18 -0300 Subject: [PATCH 13/73] set hit count cap to 999 to restore US/JP 1.0 behaviour --- src/engine/fox_display.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/engine/fox_display.c b/src/engine/fox_display.c index ffbdcebe1..f617cf6ca 100644 --- a/src/engine/fox_display.c +++ b/src/engine/fox_display.c @@ -1814,11 +1814,11 @@ void Display_Update(void) { // @port: Display player's face at all times. gPlayer[0].arwing.drawFace = true; - // @port remove 511 hit count cap, hated by generations -#if 0 - // 511 hit count cap - if (gHitCount > 511) { - gHitCount = 511; + // @port: set hit count cap to 999 to restore US/JP 1.0 behaviour +#if 1 + // 999 hit count cap (511 in 1.1 US) + if (gHitCount > 999) { + gHitCount = 999; } #endif From e8246fa8e98b9fe00e33a16249aed403d4f81c1a Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Tue, 23 Dec 2025 03:36:54 -0300 Subject: [PATCH 14/73] Fix Player being visible in Solar LevelStart cutscene --- src/overlays/ovl_i3/fox_so.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/overlays/ovl_i3/fox_so.c b/src/overlays/ovl_i3/fox_so.c index 32b3cc97e..59c6b8f3e 100644 --- a/src/overlays/ovl_i3/fox_so.c +++ b/src/overlays/ovl_i3/fox_so.c @@ -1071,6 +1071,9 @@ void Solar_LevelStart(Player* player) { player->camRoll = 0.0f; player->baseSpeed = 0.0f; + // @port: get the player off the screen + player->pos.z = 10000.0f; + gFillScreenAlpha = 255; gFillScreenAlphaTarget = 0; Solar_801A0DF8(-750.0f, -2600.0f, 300.0f, 2, 1.0f); From c3b4a8158a7e42b5b1e9e53bb349704196e61a24 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Fri, 26 Dec 2025 21:05:49 -0300 Subject: [PATCH 15/73] Andross cutscene Recording + tune up --- src/overlays/ovl_i6/fox_andross.c | 85 ++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/src/overlays/ovl_i6/fox_andross.c b/src/overlays/ovl_i6/fox_andross.c index 349df18e4..ba243c313 100644 --- a/src/overlays/ovl_i6/fox_andross.c +++ b/src/overlays/ovl_i6/fox_andross.c @@ -8,6 +8,7 @@ #include "prevent_bss_reordering.h" #include "prevent_bss_reordering2.h" #include "port/hooks/Events.h" +#include "fox_record.h" f32 D_i6_801A7F5C; f32 D_i6_801A7F64; @@ -3851,6 +3852,81 @@ f32 D_i6_801A7F4C; f32 D_i6_801A7F50; f32 D_i6_801A7F54; +// clang-format off + +// After defeating Andross +/* +03 00 00 00 02 00 00 08 03 00 00 13 02 00 00 23 03 00 00 9C 02 00 00 A6 03 00 00 C1 04 00 00 C6 03 00 00 C7 04 00 00 C9 +03 00 00 CD 04 00 00 CF 03 00 00 D7 04 00 00 EA 03 00 00 EC 04 00 00 ED 03 00 00 EF 04 00 00 F4 03 00 00 F5 04 00 00 F6 +03 00 01 10 04 00 01 12 03 00 01 22 04 00 01 24 03 00 01 27 04 00 01 2D 03 00 01 2E 04 00 01 30 03 00 01 31 04 00 01 33 +03 00 01 4C 04 00 01 4E 03 00 01 4F +*/ +Record gAndrossRobotKillCutscene1[] = { + { 3, 0 }, + { 2, 8 }, + { 3, 19 }, + { 2, 35 }, + { 3, 156 }, + { 2, 166 }, + { 3, 193 }, + { 4, 198 }, + { 3, 199 }, + { 4, 201 }, + { 3, 205 }, + { 4, 207 }, + { 3, 215 }, + { 4, 234 }, + // { 3, 236 }, + { 4, 237 }, + { 3, 239 }, + { 4, 244 }, + // { 3, 245 }, + { 4, 246 }, + // { 3, 272 }, + { 4, 274 }, + // { 3, 290 }, + { 4, 292 }, + // { 3, 295 }, // too much? + { 4, 301 }, + // { 3, 302 }, + { 4, 304 }, + // { 3, 305 }, + { 4, 307 }, + // { 3, 332 }, + { 4, 334 }, + // { 3, 335 }, + { 2, 339 }, +}; + +/* +04 00 00 00 02 00 00 01 05 00 00 02 02 00 00 03 03 00 00 2A 02 00 00 2B 03 00 00 39 02 00 00 3A 03 00 00 3F 02 00 00 45 +03 00 00 9E 04 00 00 A1 05 00 00 A3 04 00 00 B6 03 00 00 B9 04 00 00 BD 05 00 00 C1 04 00 00 CD 03 00 00 CF 02 00 00 D3 +*/ +Record gAndrossRobotKillCutscene2[] = { + { 4, 0 }, + { 2, 1 }, + { 5, 2 }, + { 2, 3 }, + { 3, 42 }, + { 2, 43 }, + { 3, 57 }, + { 2, 58 }, + { 3, 63 }, + { 2, 69 }, + { 3, 158 }, + { 4, 161 }, + { 5, 163 }, + { 4, 182 }, + { 3, 185 }, + { 4, 189 }, + { 5, 193 }, + { 4, 205 }, + { 3, 207 }, + { 2, 211 }, +}; +// clang-format on + +// Andross_LevelComplete void Andross_80193C4C(Player* player) { s32 i; s32 sp90; @@ -3862,6 +3938,12 @@ void Andross_80193C4C(Player* player) { Vec3f sp68; s32 rnd; + if (player->csState < 3) { + UpdateVisPerFrameFromRecording(gAndrossRobotKillCutscene1, ARRAY_COUNT(gAndrossRobotKillCutscene1)); + } else if ((player->csState > 2) && player->csState < 6) { + UpdateVisPerFrameFromRecording(gAndrossRobotKillCutscene2, ARRAY_COUNT(gAndrossRobotKillCutscene2)); + } + Math_SmoothStepToF(D_ctx_80177A48, 1.0f, 1.0f, 0.01f, 0.0f); player->arwing.upperRightFlapYrot = player->arwing.bottomRightFlapYrot = player->arwing.upperLeftFlapYrot = @@ -4166,7 +4248,8 @@ void Andross_80193C4C(Player* player) { Math_SmoothStepToF(&D_ctx_80177A48[2], 10000.0f, 0.05f, 20.0f, 0.0f); - if (gCsFrameCount == 220) { + // @port: Make your team spawn early in the cutscene to compensate for widescreen + if (gCsFrameCount == /* 220 */ 200) { Andross_80193AE4(0); if (gTeamShields[TEAM_ID_FALCO] > 0) { Andross_80193AE4(1); From 23d07e9949646b707d6b7a06b3b962eb306f4d5b Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Sat, 27 Dec 2025 03:07:05 -0300 Subject: [PATCH 16/73] Enter Warpzone cutscene recording --- src/engine/fox_demo.c | 27 +++++++++++++++++++++++++++ src/engine/fox_display.c | 6 +++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/engine/fox_demo.c b/src/engine/fox_demo.c index ed5334fa4..9a292d9d3 100644 --- a/src/engine/fox_demo.c +++ b/src/engine/fox_demo.c @@ -22,6 +22,13 @@ #include "fox_co.h" #include "fox_record.h" +int gWarpzoneCsFrameCount = 0; + +Record gWarpzoneCsRecord[] = { + { 2, 1 }, { 3, 3 }, { 4, 4 }, { 3, 8 }, { 2, 9 }, { 3, 52 }, { 2, 54 }, { 3, 69 }, { 2, 70 }, { 3, 75 }, + { 2, 76 }, { 3, 79 }, { 2, 117 }, { 3, 118 }, { 2, 120 }, { 3, 145 }, { 2, 215 }, { 3, 216 }, { 2, 230 }, +}; + void UpdateVisPerFrameFromRecording(Record* record, s32 maxFrames) { int i; @@ -36,6 +43,20 @@ void UpdateVisPerFrameFromRecording(Record* record, s32 maxFrames) { } } +void UpdateVisPerFrameFromRecording_Warpzone(Record* record, s32 maxFrames) { + int i; + + if (gWarpzoneCsFrameCount > record[maxFrames - 1].frame) { + return; + } + + for (i = 0; i < maxFrames; i++) { + if (gWarpzoneCsFrameCount == record[i].frame) { + gVIsPerFrame = record[i].vis; + } + } +} + void func_demo_80048AC0(TeamId teamId) { s32 teamShield; @@ -413,6 +434,9 @@ void Cutscene_EnterWarpZone(Player* player) { s32 var_v0; s32 pad[4]; + gWarpzoneCsFrameCount++; + UpdateVisPerFrameFromRecording_Warpzone(gWarpzoneCsRecord, ARRAY_COUNT(gWarpzoneCsRecord)); + player->pos.x += player->vel.x; player->flags_228 = 0; player->alternateView = false; @@ -440,6 +464,9 @@ void Cutscene_EnterWarpZone(Player* player) { switch (player->csState) { case 0: + // @port: Initialize warpzone frame counter for recording. + gWarpzoneCsFrameCount = 0; + player->somersault = false; gStarWarpDistortion = 100.0f; player->csState = 1; diff --git a/src/engine/fox_display.c b/src/engine/fox_display.c index f617cf6ca..bcf769a92 100644 --- a/src/engine/fox_display.c +++ b/src/engine/fox_display.c @@ -2071,10 +2071,14 @@ void Display_Update(void) { #if 0 RCP_SetupDL(&gMasterDisp, SETUPDL_83); gDPSetPrimColor(gMasterDisp++, 0, 0, 255, 255, 0, 255); + Graphics_DisplaySmallText(10 + 210, 180, 1.0f, 1.0f, "VIS:"); + Graphics_DisplaySmallNumber(60 + 210, 180, (int) gVIsPerFrame); Graphics_DisplaySmallText(10 + 210, 190, 1.0f, 1.0f, "CSFMS:"); Graphics_DisplaySmallNumber(60 + 210, 190, (int) gCsFrameCount); Graphics_DisplaySmallText(10 + 210, 200, 1.0f, 1.0f, "PLTIM:"); - Graphics_DisplaySmallNumber(60 + 210, 200, (int) gPlayer->csTimer); + Graphics_DisplaySmallNumber(60 + 220, 200, (int) gPlayer->csTimer); + Graphics_DisplaySmallText(10 + 210, 210, 1.0f, 1.0f, "CSSTATE:"); + Graphics_DisplaySmallNumber(60 + 220, 210, (int) gPlayer->csState); #endif // @port: @event: Call DisplayPostUpdateEvent From b1e544da57c3e4fd9e0f9b87a301c39d18aa6040 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Sat, 3 Jan 2026 00:32:19 -0300 Subject: [PATCH 17/73] fix partial starfield --- src/engine/fox_bg.c | 52 +++++++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/src/engine/fox_bg.c b/src/engine/fox_bg.c index 20a4b836e..0e0ca9a47 100644 --- a/src/engine/fox_bg.c +++ b/src/engine/fox_bg.c @@ -372,29 +372,31 @@ void Background_DrawPartialStarfield(s32 yMin, s32 yMax) { // Stars that are in f32* sp5C; u32* sp58; + yMin += 245; + yMax += 245; + // Get current screen dimensions float currentScreenWidth = gCurrentScreenWidth; float currentScreenHeight = gCurrentScreenHeight; float starfieldWidth = 1.0f * currentScreenWidth; float starfieldHeight = 1.0f * currentScreenHeight; - // Graphics pipeline setup - gDPPipeSync(gMasterDisp++); - gDPSetCycleType(gMasterDisp++, G_CYC_FILL); - gDPSetCombineMode(gMasterDisp++, G_CC_SHADE, G_CC_SHADE); - gDPSetRenderMode(gMasterDisp++, G_RM_OPA_SURF, G_RM_OPA_SURF2); + // Set projection to orthographic before drawing stars + Lib_InitOrtho(&gMasterDisp); - if (gStarfieldX >= 1.5f * currentScreenWidth) { - gStarfieldX -= 1.5f * currentScreenWidth; + gSPDisplayList(gMasterDisp++, starSetupDL); + + if (gStarfieldX >= starfieldWidth) { + gStarfieldX -= starfieldWidth; } - if (gStarfieldY >= 1.5f * currentScreenHeight) { - gStarfieldY -= 1.5f * currentScreenHeight; + if (gStarfieldY >= starfieldHeight) { + gStarfieldY -= starfieldHeight; } if (gStarfieldX < 0.0f) { - gStarfieldX += 1.5f * currentScreenWidth; + gStarfieldX += starfieldWidth; } if (gStarfieldY < 0.0f) { - gStarfieldY += 1.5f * currentScreenHeight; + gStarfieldY += starfieldHeight; } spf68 = gStarfieldX; @@ -405,19 +407,33 @@ void Background_DrawPartialStarfield(s32 yMin, s32 yMax) { // Stars that are in sp58 = gStarFillColors; var_s2 = 500; + var_s2 = var_s2 * 3; // Adjust multiplier as needed + cos = __cosf(gStarfieldRoll); sin = __sinf(gStarfieldRoll); for (i = 0; i < var_s2; i++, sp5C++, sp60++, sp58++) { bx = *sp60 + spf68; by = *sp5C + spf64; - if (bx >= starfieldWidth * 1.25f) { - bx -= 1.5f * starfieldWidth; + + // Wrapping logic for individual stars along X-axis + if (bx >= starfieldWidth) { + bx -= starfieldWidth; } - bx -= starfieldWidth / 2.0f; - if (by >= starfieldHeight * 1.25f) { - by -= 1.5f * starfieldHeight; + if (bx < 0.0f) { + bx += starfieldWidth; } + + // Wrapping logic for individual stars along Y-axis + if (by >= starfieldHeight) { + by -= starfieldHeight; + } + if (by < 0.0f) { + by += starfieldHeight; + } + + // Center the positions + bx -= starfieldWidth / 2.0f; by -= starfieldHeight / 2.0f; // Apply rotation @@ -454,6 +470,10 @@ void Background_DrawPartialStarfield(s32 yMin, s32 yMax) { // Stars that are in FrameInterpolation_RecordCloseChild(); } } + // Restore original perspective after drawing stars + Lib_InitPerspective(&gMasterDisp); + + // Finalize rendering state gDPPipeSync(gMasterDisp++); gDPSetColorDither(gMasterDisp++, G_CD_MAGICSQ); } From e8391e8e0fec3b665e6f17c8809d0c899cef24ab Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Sat, 3 Jan 2026 02:38:21 -0300 Subject: [PATCH 18/73] partial starfield floats --- src/engine/fox_bg.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/fox_bg.c b/src/engine/fox_bg.c index 0e0ca9a47..9c3c60c80 100644 --- a/src/engine/fox_bg.c +++ b/src/engine/fox_bg.c @@ -360,8 +360,8 @@ void Background_DrawStarfield(void) { void Background_DrawPartialStarfield(s32 yMin, s32 yMax) { // Stars that are in the Epilogue f32 by; f32 bx; - s16 vy; - s16 vx; + f32 vy; + f32 vx; s32 i; s32 var_s2; f32 cos; From 1aa6dff53ee6e068ec8eb3b313bb0ff7980482b3 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Sun, 4 Jan 2026 07:53:51 -0300 Subject: [PATCH 19/73] Apply F-Zero X version of AudioHeap_DiscardSampleCaches bugfix --- src/audio/audio_heap.c | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/audio/audio_heap.c b/src/audio/audio_heap.c index dbc37c578..b1e0cb493 100644 --- a/src/audio/audio_heap.c +++ b/src/audio/audio_heap.c @@ -991,16 +991,14 @@ void AudioHeap_DiscardSampleCaches(void) { Instrument* instrument; SampleCacheEntry* entry; -#ifdef AVOID_UB - entry = gPersistentSampleCache.entries; -#endif - for (fontId = 0; fontId < numFonts; fontId++) { sampleBankId1 = gSoundFontList[fontId].sampleBankId1; sampleBankId2 = gSoundFontList[fontId].sampleBankId2; - if (((sampleBankId1 != SAMPLES_NONE_U) && (entry->sampleBankId == sampleBankId1)) || - ((sampleBankId2 != SAMPLES_NONE) && (entry->sampleBankId == sampleBankId2)) || - (entry->sampleBankId == SAMPLES_SFX)) { + // @port: avoid reading from garbage memory, + // F-Zero X newer version of this audio driver has this fix: + if (((sampleBankId1 != SAMPLES_NONE_U) /* && (entry->sampleBankId == sampleBankId1) */) || + ((sampleBankId2 != SAMPLES_NONE) /* && (entry->sampleBankId == sampleBankId2)*/ ) /* || + (entry->sampleBankId == SAMPLES_SFX) */) { if (((void*) AudioHeap_SearchCaches(FONT_TABLE, CACHE_PERMANENT, fontId) != NULL) && ((gFontLoadStatus[fontId] > 1) != 0)) { for (i = 0; i < gPersistentSampleCache.numEntries; i++) { From a6429d729b4ffb7939ac73fd9c8f6c46e4760131 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Sun, 4 Jan 2026 08:10:28 -0300 Subject: [PATCH 20/73] audio UB fix correction --- src/audio/audio_heap.c | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/audio/audio_heap.c b/src/audio/audio_heap.c index b1e0cb493..d8207e8a4 100644 --- a/src/audio/audio_heap.c +++ b/src/audio/audio_heap.c @@ -994,15 +994,25 @@ void AudioHeap_DiscardSampleCaches(void) { for (fontId = 0; fontId < numFonts; fontId++) { sampleBankId1 = gSoundFontList[fontId].sampleBankId1; sampleBankId2 = gSoundFontList[fontId].sampleBankId2; - // @port: avoid reading from garbage memory, +#ifdef AVOID_UB // F-Zero X newer version of this audio driver has this fix: - if (((sampleBankId1 != SAMPLES_NONE_U) /* && (entry->sampleBankId == sampleBankId1) */) || - ((sampleBankId2 != SAMPLES_NONE) /* && (entry->sampleBankId == sampleBankId2)*/ ) /* || - (entry->sampleBankId == SAMPLES_SFX) */) { + if ((sampleBankId1 != SAMPLES_NONE_U) || (sampleBankId2 != SAMPLES_NONE)) { +#else + //! @bug: entry is uninitialized, reading from garbage memory. + if (((sampleBankId1 != SAMPLES_NONE_U) && (entry->sampleBankId == sampleBankId1)) || + ((sampleBankId2 != SAMPLES_NONE) && (entry->sampleBankId == sampleBankId2)) || + (entry->sampleBankId == SAMPLES_SFX)) { +#endif if (((void*) AudioHeap_SearchCaches(FONT_TABLE, CACHE_PERMANENT, fontId) != NULL) && ((gFontLoadStatus[fontId] > 1) != 0)) { for (i = 0; i < gPersistentSampleCache.numEntries; i++) { entry = &gPersistentSampleCache.entries[i]; +#ifdef AVOID_UB + if ((sampleBankId1 != entry->sampleBankId) && (sampleBankId2 != entry->sampleBankId) && + (entry->sampleBankId != SAMPLES_SFX)) { + break; + } +#endif for (instId = 0; instId < gSoundFontList[fontId].numInstruments; instId++) { instrument = Audio_GetInstrument(fontId, instId); if (instrument != NULL) { From 510779550138782c39b5c8086073b2638b3a0536 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Tue, 6 Jan 2026 05:07:48 -0300 Subject: [PATCH 21/73] fix seams in backgrounds --- src/engine/fox_bg.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/engine/fox_bg.c b/src/engine/fox_bg.c index 9c3c60c80..9cdb5f073 100644 --- a/src/engine/fox_bg.c +++ b/src/engine/fox_bg.c @@ -576,7 +576,7 @@ void Background_DrawBackdrop(void) { } // Translate to the next position (move right by 7280.0f each time) - Matrix_Translate(gGfxMatrix, 7280.0f, 0.0f, 0.0f, MTXF_APPLY); + Matrix_Translate(gGfxMatrix, 7279.0f, 0.0f, 0.0f, MTXF_APPLY); Matrix_SetGfxMtx(&gMasterDisp); if (skipInterpolation) { @@ -640,7 +640,7 @@ void Background_DrawBackdrop(void) { } // Translate to the next position (move right by 7280.0f each time) - Matrix_Translate(gGfxMatrix, 7280.0f, 0.0f, 0.0f, MTXF_APPLY); + Matrix_Translate(gGfxMatrix, 7279.0f, 0.0f, 0.0f, MTXF_APPLY); Matrix_SetGfxMtx(&gMasterDisp); if (skipInterpolation) { @@ -779,7 +779,7 @@ void Background_DrawBackdrop(void) { } // Translate to the next position (move right by 7280.0f each time) - Matrix_Translate(gGfxMatrix, 7280.0f, 0.0f, 0.0f, MTXF_APPLY); + Matrix_Translate(gGfxMatrix, 7279.0f, 0.0f, 0.0f, MTXF_APPLY); Matrix_SetGfxMtx(&gMasterDisp); FrameInterpolation_RecordCloseChild(); @@ -853,7 +853,7 @@ void Background_DrawBackdrop(void) { } // Move the matrix to the right by 7280.0f each time to draw the next texture - Matrix_Translate(gGfxMatrix, 7280.0f, 0.0f, 0.0f, MTXF_APPLY); + Matrix_Translate(gGfxMatrix, 7279.0f, 0.0f, 0.0f, MTXF_APPLY); Matrix_SetGfxMtx(&gMasterDisp); From b9031d633ad5edf2c239895720d28f406f93ae41 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Tue, 6 Jan 2026 10:12:32 -0300 Subject: [PATCH 22/73] Ending Recording --- include/fox_record.h | 1 + src/engine/fox_demo.c | 14 ++++++++++++++ src/overlays/ovl_ending/fox_end1.c | 23 +++++++++++++++++++++++ src/overlays/ovl_ending/fox_end2.c | 3 ++- 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/include/fox_record.h b/include/fox_record.h index ec4ba8272..531b6c422 100644 --- a/include/fox_record.h +++ b/include/fox_record.h @@ -17,5 +17,6 @@ typedef struct Record { extern Record gCarrierCutsceneRecord[13]; void UpdateVisPerFrameFromRecording(Record* record, s32 maxFrames); +void UpdateVisPerFrameFromRecording_Ending(Record* record, s32 maxFrames); #endif diff --git a/src/engine/fox_demo.c b/src/engine/fox_demo.c index 9a292d9d3..392fe14c0 100644 --- a/src/engine/fox_demo.c +++ b/src/engine/fox_demo.c @@ -57,6 +57,20 @@ void UpdateVisPerFrameFromRecording_Warpzone(Record* record, s32 maxFrames) { } } +void UpdateVisPerFrameFromRecording_Ending(Record* record, s32 maxFrames) { + int i; + + if (gGameFrameCount > record[maxFrames - 1].frame) { + return; + } + + for (i = 0; i < maxFrames; i++) { + if (gGameFrameCount == record[i].frame) { + gVIsPerFrame = record[i].vis; + } + } +} + void func_demo_80048AC0(TeamId teamId) { s32 teamShield; diff --git a/src/overlays/ovl_ending/fox_end1.c b/src/overlays/ovl_ending/fox_end1.c index 79223f923..4116210c7 100644 --- a/src/overlays/ovl_ending/fox_end1.c +++ b/src/overlays/ovl_ending/fox_end1.c @@ -7,6 +7,15 @@ #include "assets/ast_ending.h" #include "prevent_bss_reordering.h" #include "port/mods/PortEnhancements.h" +#include "fox_record.h" + +Record gEndingCsRecord[] = { + { 2, 0 }, { 5, 1 }, { 2, 2 }, { 3, 277 }, { 2, 278 }, { 3, 340 }, { 2, 397 }, { 3, 537 }, + { 2, 538 }, { 3, 548 }, { 2, 549 }, { 3, 568 }, { 2, 569 }, { 3, 620 }, { 2, 623 }, { 3, 803 }, + { 2, 806 }, { 3, 814 }, { 2, 823 }, { 3, 1282 }, { 2, 1284 }, { 5, 3703 }, { 2, 3704 }, { 4, 3904 }, + { 2, 3905 }, { 3, 4782 }, { 2, 4783 }, { 3, 4785 }, { 2, 4820 }, { 3, 4986 }, { 2, 6779 }, { 5, 6780 }, + { 2, 6781 }, { 3, 6785 }, { 4, 6786 }, { 3, 6793 }, { 2, 6799 }, +}; void Ending_8018CE20(s32); void Ending_801926D4(void); @@ -1106,6 +1115,8 @@ void Ending_Main(void) { Ending_8018A8FC(); Ending_8018B3D8(); Ending_8018ABE8(); + + UpdateVisPerFrameFromRecording_Ending(gEndingCsRecord, ARRAY_COUNT(gEndingCsRecord)); } void DrawBorders(void) { @@ -1183,6 +1194,18 @@ void Ending_Draw(void) { } #endif +// Recording debug +#if 0 + RCP_SetupDL(&gMasterDisp, SETUPDL_83); + gDPSetPrimColor(gMasterDisp++, 0, 0, 255, 255, 0, 255); + Graphics_DisplaySmallText(10 + 210, 180, 1.0f, 1.0f, "VIS:"); + Graphics_DisplaySmallNumber(60 + 210, 180, (int) gVIsPerFrame); + Graphics_DisplaySmallText(10 + 210, 190, 1.0f, 1.0f, "GMFMS:"); + Graphics_DisplaySmallNumber(60 + 210, 190, (int) gGameFrameCount); + Graphics_DisplaySmallText(10 + 210, 200, 1.0f, 1.0f, "ENDSTATE:"); + Graphics_DisplaySmallNumber(60 + 220, 210, (int) D_ending_80196D00); +#endif + Matrix_Pop(&gGfxMatrix); } diff --git a/src/overlays/ovl_ending/fox_end2.c b/src/overlays/ovl_ending/fox_end2.c index 8ca480afb..4525c76f6 100644 --- a/src/overlays/ovl_ending/fox_end2.c +++ b/src/overlays/ovl_ending/fox_end2.c @@ -65,7 +65,8 @@ void Ending_8018CE20(u32 arg0) { void Ending_8018D250(u32 arg0, AssetInfo* asset) { gSceneSetup = asset->unk_08; - gVIsPerFrame = asset->unk_70; + // @port: avoid updating gVIsPerFrame since we're using a recording + // gVIsPerFrame = asset->unk_70; gStarCount = asset->unk_14; } From d4094ec9d27d4e4ebd3ef35e6859d164c4ff8144 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Thu, 15 Jan 2026 20:11:34 -0300 Subject: [PATCH 23/73] Add gDebugLevelComplete special condition for LEVEL_FORTUNA --- src/port/mods/PortEnhancements.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/port/mods/PortEnhancements.c b/src/port/mods/PortEnhancements.c index 13fb39941..c74f07210 100644 --- a/src/port/mods/PortEnhancements.c +++ b/src/port/mods/PortEnhancements.c @@ -99,8 +99,13 @@ void OnDisplayUpdatePost(IEvent* event) { } if (gControllerPress[0].button & L_TRIG) { - pl->state = PLAYERSTATE_LEVEL_COMPLETE; gMissionStatus = MISSION_ACCOMPLISHED; + if (gCurrentLevel == LEVEL_FORTUNA) { + gMissionStatus = MISSION_COMPLETE; + gAllRangeEventTimer = 9200; + return; + } + pl->state = PLAYERSTATE_LEVEL_COMPLETE; } } From d2ae1bae4a37173a712ccd7ca8f990afdf464adb Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Thu, 15 Jan 2026 20:11:54 -0300 Subject: [PATCH 24/73] Add rumble to Fortuna exploding cutscene --- src/engine/fox_demo.c | 4 +++- src/engine/fox_hud.c | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/engine/fox_demo.c b/src/engine/fox_demo.c index 392fe14c0..8b243ca64 100644 --- a/src/engine/fox_demo.c +++ b/src/engine/fox_demo.c @@ -2422,7 +2422,7 @@ void ActorCutscene_Update(ActorCutscene* this) { break; case LEVEL_FORTUNA: - if (this->animFrame == 11) { + if (this->animFrame == ACTOR_CS_FO_EXPLOSION) { switch (this->state) { case 0: if (gCsFrameCount == 100) { @@ -2430,6 +2430,8 @@ void ActorCutscene_Update(ActorCutscene* this) { this->timer_0BC = 50; this->iwork[0] = 255; AUDIO_PLAY_SFX(NA_SE_EN_BOSS_EXPLOSION, this->sfxSource, 0); + // @port: Add rumble to this explosion + gControllerRumbleTimers[0] = 4; } break; diff --git a/src/engine/fox_hud.c b/src/engine/fox_hud.c index b35256d4f..e31a1f40c 100644 --- a/src/engine/fox_hud.c +++ b/src/engine/fox_hud.c @@ -3875,6 +3875,8 @@ s32 FoBase_ExplodeCs(FoBase* this) { Math_SmoothStepToF(&this->fwork[4], this->obj.pos.x + 0.0f, 0.02f, 10000.0f, 0.0f); Math_SmoothStepToF(&this->fwork[5], this->obj.pos.y + 500.0f, 0.02f, 10000.0f, 0.0f); Math_SmoothStepToF(&this->fwork[6], this->obj.pos.z + 1500.0f, 0.02f, 10000.0f, 0.0f); + // @port: Add rumble to this cutscene + gControllerRumbleTimers[0] = 60; break; case 2: @@ -3890,6 +3892,8 @@ s32 FoBase_ExplodeCs(FoBase* this) { Math_SmoothStepToF(&this->fwork[4], this->obj.pos.x + 0.0f, 0.02f, 10000.0f, 0.0f); Math_SmoothStepToF(&this->fwork[5], this->obj.pos.y + 1500.0f, 0.02f, 10000.0f, 0.0f); Math_SmoothStepToF(&this->fwork[6], this->obj.pos.z + 1500.0f, 0.02f, 10000.0f, 0.0f); + // @port: Add rumble to this cutscene + gControllerRumbleTimers[0] = 60; break; case 3: From 5019c5a644bfe6b104531723e6abe6b0d6743ede Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Sat, 7 Feb 2026 12:50:06 -0300 Subject: [PATCH 25/73] Fix EdgeArrows texture scrolling --- src/engine/fox_hud.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/engine/fox_hud.c b/src/engine/fox_hud.c index e31a1f40c..19a300fc2 100644 --- a/src/engine/fox_hud.c +++ b/src/engine/fox_hud.c @@ -2718,6 +2718,7 @@ void HUD_Texture_Scroll(u8* texturePtr, s32 xPos, s32 yPos, u8 type) { default: break; } + gSPInvalidateTexCache(gMasterDisp++, texture); } void HUD_Texture_Wave(u16* srcTexture, u16* dstTexture) { From 60d9ea940e6c1673b196df35901c1ee5ba4420cd Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Mon, 23 Feb 2026 02:58:42 -0300 Subject: [PATCH 26/73] Interpolation: Camera Skipping for Title Cutscene --- include/sf64context.h | 2 +- src/engine/fox_context.c | 1 + src/overlays/ovl_menu/fox_title.c | 26 ++++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/include/sf64context.h b/include/sf64context.h index e4bfaf61a..fa990dd02 100644 --- a/include/sf64context.h +++ b/include/sf64context.h @@ -349,6 +349,6 @@ extern UNK_TYPE F_80178334; extern UNK_TYPE F_8017833C; extern UNK_TYPE F_80178344; extern UNK_TYPE F_8017834C; - +extern bool gCamera1Skipped; #endif diff --git a/src/engine/fox_context.c b/src/engine/fox_context.c index 25b1ec721..5b92b875f 100644 --- a/src/engine/fox_context.c +++ b/src/engine/fox_context.c @@ -397,3 +397,4 @@ UNK_TYPE F_80178334; UNK_TYPE F_8017833C; UNK_TYPE F_80178344; UNK_TYPE F_8017834C; +bool gCamera1Skipped = false; \ No newline at end of file diff --git a/src/overlays/ovl_menu/fox_title.c b/src/overlays/ovl_menu/fox_title.c index 216263fe1..5b8791755 100644 --- a/src/overlays/ovl_menu/fox_title.c +++ b/src/overlays/ovl_menu/fox_title.c @@ -273,7 +273,23 @@ void Title_UpdateEntry(void) { Title_NextState_Check(); } +static bool sSkipInterpolation = false; + void Title_Draw(void) { + static s32 camSkipTimes; + + if (sSkipInterpolation) { + // @port Skip interpolation + FrameInterpolation_ShouldInterpolateFrame(false); + printf("CAMERA 1 SKIPED: %d\n", camSkipTimes++); + gCamera1Skipped = true; + sSkipInterpolation = false; + } else { + FrameInterpolation_RecordOpenChild("TitleCam", 0); + FrameInterpolation_RecordMarker(__FILE__, __LINE__); + gCamera1Skipped = false; + } + switch (sCutsceneState) { case TITLE_RANKING: Title_Ranking_Draw(); @@ -353,6 +369,14 @@ void Title_Draw(void) { gFillScreenBlue = 0; Wipe_Draw(WIPE_VERTICAL, sWipeHeight); } + + if (sSkipInterpolation) { + // @port Re-enable Interpolation if it was skipped + FrameInterpolation_ShouldInterpolateFrame(true); + } else { + FrameInterpolation_RecordCloseChild(); + } + #if 0 // @decomp Debug: RCP_SetupDL(&gMasterDisp, SETUPDL_83); @@ -1793,6 +1817,7 @@ void Title_CsTakeOff_Update(void) { gAmbientB = 46; sSceneState++; + sSkipInterpolation = true; } sTimer3++; break; @@ -1893,6 +1918,7 @@ void Title_CsTakeOff_Update(void) { Audio_SetEnvSfxReverb(0); sSceneState = 0; sCutsceneState = 5; // TITLE_TAKE_OFF_SPACE + sSkipInterpolation = true; } sTimer3++; } From 834eaa9a65e2c3a8b7e3855d36367cdb94e250cc Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Mon, 23 Feb 2026 03:37:34 -0300 Subject: [PATCH 27/73] Interpolation: Camera interpolation skip for cutscenes --- src/engine/camera_interp_skip.c | 108 ++++++++++++++++++++++++++++++++ src/engine/fox_display.c | 34 ++++++++++ 2 files changed, 142 insertions(+) create mode 100644 src/engine/camera_interp_skip.c diff --git a/src/engine/camera_interp_skip.c b/src/engine/camera_interp_skip.c new file mode 100644 index 000000000..2a458a5ab --- /dev/null +++ b/src/engine/camera_interp_skip.c @@ -0,0 +1,108 @@ +#include "global.h" + +#define SQXYZ(vec) ((vec.x) * (vec.x) + (vec.y) * (vec.y) + (vec.z) * (vec.z)) + +void Math_Vec3f_Diff(Vec3f* l, Vec3f* r, Vec3f* dest) { + dest->x = l->x - r->x; + dest->y = l->y - r->y; + dest->z = l->z - r->z; +} + +void Math_Vec3f_Sum(Vec3f* l, Vec3f* r, Vec3f* dest) { + dest->x = l->x + r->x; + dest->y = l->y + r->y; + dest->z = l->z + r->z; +} + +f32 Math_Vec3f_DistXYZ(Vec3f* a, Vec3f* b) { + Vec3f diff; + Math_Vec3f_Diff(b, a, &diff); + return sqrtf(SQXYZ(diff)); +} + +bool should_interpolate_perspective(Vec3f* eye, Vec3f* at) { + static Vec3f prev_eye = { 0, 0, 0 }; + static Vec3f prev_at = { 0, 0, 0 }; + static Vec3f eye_velocity = { 0, 0, 0 }; + static Vec3f at_velocity = { 0, 0, 0 }; + + Vec3f predicted_eye; + Vec3f predicted_at; + // Predict the new eye and at positions based on the previous velocity and positions. + Math_Vec3f_Sum(&prev_eye, &eye_velocity, &predicted_eye); + Math_Vec3f_Sum(&prev_at, &at_velocity, &predicted_at); + + // Calculate the current velocities from the previous and current positions. + Math_Vec3f_Diff(eye, &prev_eye, &eye_velocity); + Math_Vec3f_Diff(at, &prev_at, &at_velocity); + + // Compare the predicted positions to the real positions. + float eye_dist = Math_Vec3f_DistXYZ(&predicted_eye, eye); + float at_dist = Math_Vec3f_DistXYZ(&predicted_at, at); + + // Compare the velocities of the eye and at positions. + float velocity_diff = Math_Vec3f_DistXYZ(&eye_velocity, &at_velocity); + + // Update the tracking for the previous positions with the new ones. + prev_eye = *eye; + prev_at = *at; + + // These numbers are all picked via testing. + + // If the velocity of both positions was the same, then they're moving together and should interpolate. + if (velocity_diff <= 3.0f && eye_dist <= 100.0f && at_dist <= 100.0f) { + return true; + } + + // If the focus or position are basically the same across frames and the eye didn't move too far then it should + // probably be interpolated. + if (at_dist <= 20.0f && eye_dist <= 300.0f) { + return true; + } + if (eye_dist <= 20.0f && at_dist <= 300.0f) { + return true; + } + + // Force camera interpolation during the all-range transition + if (gPlayer[0].state == PLAYERSTATE_START_360) { + return true; + } + + // Force camera interpolation during camera shake + if (gCameraShake > 0 || gCameraShake > 0) { + return true; + } + + switch (gCurrentLevel) { + // For Robot destroy cutscenes + case LEVEL_SECTOR_Y: + if (velocity_diff > 100.0f || at_dist > 500.0f || eye_dist > 500.0f) { + eye_velocity.x = 0.0f; + eye_velocity.y = 0.0f; + eye_velocity.z = 0.0f; + at_velocity.x = 0.0f; + at_velocity.y = 0.0f; + at_velocity.z = 0.0f; + + printf("velocity_diff: %f\n at_dist: %f\n eye_dist: %f\n", velocity_diff, at_dist, eye_dist); + return false; + } + break; + + default: + if (velocity_diff > 200.0f || at_dist > 150.0f || eye_dist > 300.0f) { + eye_velocity.x = 0.0f; + eye_velocity.y = 0.0f; + eye_velocity.z = 0.0f; + at_velocity.x = 0.0f; + at_velocity.y = 0.0f; + at_velocity.z = 0.0f; + + printf("velocity_diff: %f\n at_dist: %f\n eye_dist: %f\n", velocity_diff, at_dist, eye_dist); + return false; + } + break; + } + + return true; +} \ No newline at end of file diff --git a/src/engine/fox_display.c b/src/engine/fox_display.c index bcf769a92..fae15d6f8 100644 --- a/src/engine/fox_display.c +++ b/src/engine/fox_display.c @@ -1,5 +1,6 @@ #include "global.h" #include "mods/hit64.c" +#include "camera_interp_skip.c" #include "assets/ast_arwing.h" #include "assets/ast_allies.h" #include "assets/ast_landmaster.h" @@ -1877,6 +1878,31 @@ void Display_Update(void) { gPlayCamAt.y = camPlayer->cam.at.y; gPlayCamAt.z = camPlayer->cam.at.z; } + + static PlayState prevPlayState = 0; + static int camSkipTimes = 0; + + bool bigJump = !should_interpolate_perspective(&gPlayCamEye, &gPlayCamAt); + + // @port: Force interpolation camera skip if we're transitioning to or from a pause state. + if (((prevPlayState == PLAY_PAUSE) && (gPlayState == PLAY_UPDATE)) || + ((prevPlayState == PLAY_UPDATE) && (gPlayState == PLAY_PAUSE))) { + bigJump = true; + } + + if (bigJump) { + // @port Skip interpolation + FrameInterpolation_ShouldInterpolateFrame(false); + printf("CAMERA 1 SKIPED: %d\n", camSkipTimes++); + gCamera1Skipped = true; + } else { + FrameInterpolation_RecordOpenChild("GamePlayCam", 0); + FrameInterpolation_RecordMarker(__FILE__, __LINE__); + gCamera1Skipped = false; + } + + prevPlayState = gPlayState; + camPlayer->camYaw = -Math_Atan2F(gPlayCamEye.x - gPlayCamAt.x, gPlayCamEye.z - gPlayCamAt.z); camPlayer->camPitch = -Math_Atan2F(gPlayCamEye.y - gPlayCamAt.y, sqrtf(SQ(gPlayCamEye.z - gPlayCamAt.z) + SQ(gPlayCamEye.x - gPlayCamAt.x))); @@ -2051,6 +2077,14 @@ void Display_Update(void) { HUD_Draw(); HUD_EdgeArrows_Update(); } + + if (bigJump) { + // @port Re-enable Interpolation if it was skipped + FrameInterpolation_ShouldInterpolateFrame(true); + } else { + FrameInterpolation_RecordCloseChild(); + } + Matrix_Pop(&gGfxMatrix); Display_DrawHelpAlert(); sPlayersVisible[gPlayerNum] = false; From b8f61dd4402c797653e9472785311e6aeacded88 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Mon, 23 Feb 2026 03:53:41 -0300 Subject: [PATCH 28/73] camera_interp_skip.c: include guards --- src/engine/camera_interp_skip.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/engine/camera_interp_skip.c b/src/engine/camera_interp_skip.c index 2a458a5ab..0d2f6569c 100644 --- a/src/engine/camera_interp_skip.c +++ b/src/engine/camera_interp_skip.c @@ -1,3 +1,6 @@ +#ifndef CAMERA_INTERP_SKIP_C +#define CAMERA_INTERP_SKIP_C + #include "global.h" #define SQXYZ(vec) ((vec.x) * (vec.x) + (vec.y) * (vec.y) + (vec.z) * (vec.z)) @@ -105,4 +108,6 @@ bool should_interpolate_perspective(Vec3f* eye, Vec3f* at) { } return true; -} \ No newline at end of file +} + +#endif From 0f0132b60ea60ee9786bb5be9bb37d093dbfc880 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Mon, 23 Feb 2026 04:10:12 -0300 Subject: [PATCH 29/73] Fix duplicate symbol definition --- src/engine/camera_interp_skip.c | 5 ----- src/engine/fox_display.c | 1 - 2 files changed, 6 deletions(-) diff --git a/src/engine/camera_interp_skip.c b/src/engine/camera_interp_skip.c index 0d2f6569c..1ec0ef8f5 100644 --- a/src/engine/camera_interp_skip.c +++ b/src/engine/camera_interp_skip.c @@ -1,6 +1,3 @@ -#ifndef CAMERA_INTERP_SKIP_C -#define CAMERA_INTERP_SKIP_C - #include "global.h" #define SQXYZ(vec) ((vec.x) * (vec.x) + (vec.y) * (vec.y) + (vec.z) * (vec.z)) @@ -109,5 +106,3 @@ bool should_interpolate_perspective(Vec3f* eye, Vec3f* at) { return true; } - -#endif diff --git a/src/engine/fox_display.c b/src/engine/fox_display.c index fae15d6f8..8351cd541 100644 --- a/src/engine/fox_display.c +++ b/src/engine/fox_display.c @@ -1,6 +1,5 @@ #include "global.h" #include "mods/hit64.c" -#include "camera_interp_skip.c" #include "assets/ast_arwing.h" #include "assets/ast_allies.h" #include "assets/ast_landmaster.h" From 122f479b1a8bb93d1cd03eca6f525ee6f002c853 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Mon, 23 Feb 2026 04:11:22 -0300 Subject: [PATCH 30/73] Interpolation: Tag SyShogun Engine Glow --- src/overlays/ovl_i6/fox_sy.c | 45 ++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/overlays/ovl_i6/fox_sy.c b/src/overlays/ovl_i6/fox_sy.c index c81201872..c24562b4f 100644 --- a/src/overlays/ovl_i6/fox_sy.c +++ b/src/overlays/ovl_i6/fox_sy.c @@ -1932,11 +1932,18 @@ void SectorY_SyShogun_Draw(SyShogun* this) { gDPSetPrimColor(gMasterDisp++, 0x00, 0x00, 255, 255, 255, 255); gDPSetEnvColor(gMasterDisp++, 255, 48, 0, 255); Matrix_Pop(&gGfxMatrix); + + // @port: Tag the transform. + FrameInterpolation_RecordOpenChild("SyShogun_EngineGlow", 0); + Matrix_Push(&gGfxMatrix); Matrix_Translate(gGfxMatrix, this->fwork[1], this->fwork[2], this->fwork[3], MTXF_APPLY); Matrix_Scale(gGfxMatrix, sp9C, sp9C, sp9C, MTXF_APPLY); Matrix_SetGfxMtx(&gMasterDisp); gSPDisplayList(gMasterDisp++, aOrbDL); + + // @port Pop the transform id. + FrameInterpolation_RecordCloseChild(); } if (this->fwork[34] != 0.0f) { @@ -1944,6 +1951,10 @@ void SectorY_SyShogun_Draw(SyShogun* this) { gDPSetPrimColor(gMasterDisp++, 0x00, 0x00, 255, 255, 255, 255); gDPSetEnvColor(gMasterDisp++, 32, 255, 32, 255); Matrix_Pop(&gGfxMatrix); + + // @port: Tag the transform. + FrameInterpolation_RecordOpenChild("SyShogun_EngineGlow", 1); + Matrix_Push(&gGfxMatrix); Matrix_Translate(gGfxMatrix, this->fwork[31], this->fwork[32], this->fwork[33], MTXF_APPLY); Matrix_Scale(gGfxMatrix, this->fwork[34], this->fwork[34], this->fwork[34], MTXF_APPLY); @@ -1959,6 +1970,9 @@ void SectorY_SyShogun_Draw(SyShogun* this) { Matrix_RotateX(gGfxMatrix, -sp88, MTXF_APPLY); Matrix_SetGfxMtx(&gMasterDisp); gSPDisplayList(gMasterDisp++, aOrbDL); + + // @port Pop the transform id. + FrameInterpolation_RecordCloseChild(); } if (this->index == 0) { @@ -1967,7 +1981,12 @@ void SectorY_SyShogun_Draw(SyShogun* this) { gDPSetPrimColor(gMasterDisp++, 0x00, 0x00, 255, 255, 255, 255); gDPSetEnvColor(gMasterDisp++, 32, 255, 32, 255); Matrix_Pop(&gGfxMatrix); + + // @port: Tag the transform. + FrameInterpolation_RecordOpenChild("SyShogun_EngineGlow", 2); + Matrix_Push(&gGfxMatrix); + Matrix_Translate(gGfxMatrix, this->fwork[35], this->fwork[36], this->fwork[37], MTXF_APPLY); Matrix_RotateY(gGfxMatrix, this->obj.rot.y * M_DTOR, MTXF_APPLY); Matrix_RotateX(gGfxMatrix, this->obj.rot.x * M_DTOR, MTXF_APPLY); @@ -1975,6 +1994,9 @@ void SectorY_SyShogun_Draw(SyShogun* this) { Matrix_Scale(gGfxMatrix, this->fwork[41] * 2.0f, this->fwork[41], this->fwork[41] * 2.0f, MTXF_APPLY); Matrix_SetGfxMtx(&gMasterDisp); gSPDisplayList(gMasterDisp++, aOrbDL); + + // @port Pop the transform id. + FrameInterpolation_RecordCloseChild(); } if (this->fwork[42] != 0.0f) { @@ -1982,7 +2004,12 @@ void SectorY_SyShogun_Draw(SyShogun* this) { gDPSetPrimColor(gMasterDisp++, 0x00, 0x00, 255, 255, 255, 255); gDPSetEnvColor(gMasterDisp++, 32, 255, 32, 255); Matrix_Pop(&gGfxMatrix); + + // @port: Tag the transform. + FrameInterpolation_RecordOpenChild("SyShogun_EngineGlow", 3); + Matrix_Push(&gGfxMatrix); + Matrix_Translate(gGfxMatrix, this->fwork[38], this->fwork[39], this->fwork[40], MTXF_APPLY); Matrix_RotateY(gGfxMatrix, this->obj.rot.y * M_DTOR, MTXF_APPLY); Matrix_RotateX(gGfxMatrix, this->obj.rot.x * M_DTOR, MTXF_APPLY); @@ -1990,6 +2017,9 @@ void SectorY_SyShogun_Draw(SyShogun* this) { Matrix_Scale(gGfxMatrix, this->fwork[42] * 2.0f, this->fwork[42], this->fwork[42] * 2.0f, MTXF_APPLY); Matrix_SetGfxMtx(&gMasterDisp); gSPDisplayList(gMasterDisp++, aOrbDL); + + // @port Pop the transform id. + FrameInterpolation_RecordCloseChild(); } } @@ -1998,12 +2028,20 @@ void SectorY_SyShogun_Draw(SyShogun* this) { gDPSetPrimColor(gMasterDisp++, 0x00, 0x00, 0, 0, 0, 255); gDPSetEnvColor(gMasterDisp++, 0, 0, 0, 0); Matrix_Pop(&gGfxMatrix); + + // @port: Tag the transform. + FrameInterpolation_RecordOpenChild("SyShogun_EngineGlow", 4); + Matrix_Push(&gGfxMatrix); + Matrix_Translate(gGfxMatrix, this->obj.pos.x, 157.0f, this->obj.pos.z, MTXF_APPLY); Matrix_RotateX(gGfxMatrix, M_PI / 2, MTXF_APPLY); Matrix_Scale(gGfxMatrix, this->fwork[43], this->fwork[43], this->fwork[43], MTXF_APPLY); Matrix_SetGfxMtx(&gMasterDisp); gSPDisplayList(gMasterDisp++, aOrbDL); + + // @port Pop the transform id. + FrameInterpolation_RecordCloseChild(); } if (this->fwork[46] != 0.0f) { @@ -2013,6 +2051,10 @@ void SectorY_SyShogun_Draw(SyShogun* this) { gDPSetEnvColor(gMasterDisp++, 0, 0, 0, 0); Matrix_Pop(&gGfxMatrix); Matrix_Push(&gGfxMatrix); + + // @port: Tag the transform. + FrameInterpolation_RecordOpenChild("SyShogun_EngineGlow", 5); + Matrix_Translate(gGfxMatrix, this->obj.pos.x + 10.0f, this->obj.pos.y + 70.0f, this->obj.pos.z + 60.0f, MTXF_APPLY); Matrix_Scale(gGfxMatrix, 0.4f, 0.2f, 0.2f, MTXF_APPLY); @@ -2021,6 +2063,9 @@ void SectorY_SyShogun_Draw(SyShogun* this) { Matrix_Translate(gGfxMatrix, -46.0f, 0, 0, MTXF_APPLY); Matrix_SetGfxMtx(&gMasterDisp); gSPDisplayList(gMasterDisp++, aOrbDL); + + // @port Pop the transform id. + FrameInterpolation_RecordCloseChild(); } } } From 44132ee370c1c01ae4771ef057b2d2b17b165ed9 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Mon, 23 Feb 2026 04:18:10 -0300 Subject: [PATCH 31/73] prototype for should_interpolate_perspective --- include/functions.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/functions.h b/include/functions.h index b85f8490c..a7217af07 100644 --- a/include/functions.h +++ b/include/functions.h @@ -513,5 +513,6 @@ void gSPDisplayList(Gfx* pkt, Gfx* dl); void gSPDisplayListOffset(Gfx* pkt, Gfx* dl, int offset); void gSPVertex(Gfx* pkt, uintptr_t v, int n, int v0); void gSPInvalidateTexCache(Gfx* pkt, uintptr_t texAddr); +bool should_interpolate_perspective(Vec3f* eye, Vec3f* at); #endif From 1249a85bf2e28bcae1da8de857c0e71951605225 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Tue, 24 Feb 2026 03:35:56 -0300 Subject: [PATCH 32/73] Interpolation: Tag radar marks --- src/engine/fox_hud.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/engine/fox_hud.c b/src/engine/fox_hud.c index 19a300fc2..161a2052f 100644 --- a/src/engine/fox_hud.c +++ b/src/engine/fox_hud.c @@ -2197,6 +2197,10 @@ s32 HUD_RadarMarks_Update(void) { } Matrix_Push(&gGfxMatrix); + + // @port: Tag the transform. + FrameInterpolation_RecordOpenChild(&gRadarMarks[i], i); + Matrix_Translate(gGfxMatrix, gRadarMarks[i].pos.x * 0.008f, -gRadarMarks[i].pos.z * 0.008f, 0.0f, MTXF_APPLY); if (gRadarMarks[i].type == 103) { @@ -2208,6 +2212,10 @@ s32 HUD_RadarMarks_Update(void) { Matrix_SetGfxMtx(&gMasterDisp); HUD_RadarMark_Draw(gRadarMarks[i].type); + + // @port Pop the transform id. + FrameInterpolation_RecordCloseChild(); + Matrix_Pop(&gGfxMatrix); gRadarMarks[i].enabled = false; From 8ec04cbcc188a9fe863f7e478815d114b0b3d1e4 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Tue, 24 Feb 2026 03:37:55 -0300 Subject: [PATCH 33/73] ARRAY_COUNT usage --- src/engine/fox_hud.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/fox_hud.c b/src/engine/fox_hud.c index 161a2052f..bbf8de247 100644 --- a/src/engine/fox_hud.c +++ b/src/engine/fox_hud.c @@ -2230,7 +2230,7 @@ s32 ActorMissileSeek_ModeCheck(ActorMissileSeekMode mode) { s32 i; s32 ret = 0; - for (i = 0, actor = &gActors[0]; i < 60; i++, actor++) { + for (i = 0, actor = &gActors[0]; i < ARRAY_COUNT(gActors); i++, actor++) { switch (mode) { case MISSILE_SEEK_TEAMMATES: if ((actor->obj.status == OBJ_ACTIVE) && (actor->obj.id == OBJ_ACTOR_MISSILE_SEEK_TEAM)) { From 28b43e1b6007fb76fbec21a1e6567356b88861a7 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Tue, 24 Feb 2026 06:02:54 -0300 Subject: [PATCH 34/73] fix scene change code --- src/audio/audio_general.c | 3 ++- src/audio/audio_thread.c | 2 +- src/engine/fox_load.c | 21 ++++++++++++++++++--- src/engine/fox_versus.c | 3 --- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/audio/audio_general.c b/src/audio/audio_general.c index 22f0f7c69..55142c193 100644 --- a/src/audio/audio_general.c +++ b/src/audio/audio_general.c @@ -2824,7 +2824,8 @@ void Audio_StartReset(u8 oldSpecId) { } else { sAudioResetStatus = AUDIORESET_WAIT; } - // LTODO: Workaround for the crash after quitting the Practice mode + + // @port: Workaround for the hang after quitting Training mode sAudioResetStatus = AUDIORESET_WAIT; AUDIOCMD_GLOBAL_UNMUTE(true); diff --git a/src/audio/audio_thread.c b/src/audio/audio_thread.c index 1bd9b1e98..c9074071a 100644 --- a/src/audio/audio_thread.c +++ b/src/audio/audio_thread.c @@ -471,7 +471,7 @@ u8* AudioThread_GetFontsForSequence(s32 seqId, u32* outNumFonts) { bool AudioThread_ResetComplete(void) { s32 pad; - OSMesg sp18; + OSMesg sp18 = { 0 }; if (!MQ_GET_MESG(gAudioResetQueue, &sp18)) { return false; diff --git a/src/engine/fox_load.c b/src/engine/fox_load.c index 9ba73553d..9baf5d484 100644 --- a/src/engine/fox_load.c +++ b/src/engine/fox_load.c @@ -34,11 +34,26 @@ void Load_RomFile(void* vRomAddress, void* dest, ptrdiff_t size) { u8 Load_SceneFiles(Scene* scene) { #if 1 - // TODO: BUG! sCurrentScene and scene never change so this will never be true. - bool hasSceneChanged = memcmp(&sCurrentScene, scene, sizeof(Scene)) != 0; - sCurrentScene = *scene; + // @port: + static void* sCurrentScenePtr = NULL; + + bool hasSceneChanged = (uintptr_t) sCurrentScenePtr != (uintptr_t) scene; + sCurrentScenePtr = scene; + + if (sFillTimer != 0) { + sFillTimer--; + } else if (gStartNMI == 0) { + Lib_FillScreen(false); + } + + if (hasSceneChanged) { + printf("Scene Changed!\n"); + } + return hasSceneChanged; #else + // Original code: + u8* ramPtr = SEGMENT_VRAM_START(ovl_i1); u8 segment; u8 changeScene = false; diff --git a/src/engine/fox_versus.c b/src/engine/fox_versus.c index 2bba4e295..2c301d8a4 100644 --- a/src/engine/fox_versus.c +++ b/src/engine/fox_versus.c @@ -1261,9 +1261,6 @@ s32 func_versus_800C1138(s32 max, s32 arg1) { void Versus_InitMatch(void) { s32 i; - // Until Load_SceneFiles gets fixed, this fixes most of the audio issues in versus - AUDIO_SET_SPEC_ALT(SFXCHAN_3, AUDIOSPEC_16); - for (i = 0, sVsPlayerCount = 0; i < 4; i++) { if (!gPlayerInactive[i]) { sVsPlayerCount++; From 9d8172f45caa4ae9120ea1172fa398b5e07544f1 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Wed, 25 Feb 2026 00:06:47 -0300 Subject: [PATCH 35/73] Disable Title AUDIO_SPEC set when scene changes for now. --- src/engine/fox_load.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/engine/fox_load.c b/src/engine/fox_load.c index 9baf5d484..13fe034f0 100644 --- a/src/engine/fox_load.c +++ b/src/engine/fox_load.c @@ -109,7 +109,8 @@ u8 Load_SceneSetup(u8 sceneId, u8 sceneSetup) { case SCENE_TITLE: changeScene = Load_SceneFiles(&sOvlmenu_Title[sceneSetup]); if (changeScene == true) { - AUDIO_SET_SPEC(SFXCHAN_0, AUDIOSPEC_22); + // Causes audio hang for a few seconds when going from the menu to the title + // AUDIO_SET_SPEC(SFXCHAN_0, AUDIOSPEC_22); } break; case SCENE_MENU: From f20465ac724bb28664081f1980f987bbd060fa0d Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Wed, 25 Feb 2026 02:00:27 -0300 Subject: [PATCH 36/73] Sector X: Hide Falco's spawn position behind Fox's Arwing during the level complete cutscene. --- src/overlays/ovl_i2/fox_sx.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/overlays/ovl_i2/fox_sx.c b/src/overlays/ovl_i2/fox_sx.c index 2b842332b..4d376b3d4 100644 --- a/src/overlays/ovl_i2/fox_sx.c +++ b/src/overlays/ovl_i2/fox_sx.c @@ -1675,6 +1675,13 @@ void SectorX_LevelComplete_SetupTeam(ActorCutscene* this, s32 teamIdx) { srcA.x = D_i2_80195710[teamIdx]; srcA.y = D_i2_80195720[teamIdx]; srcA.z = D_i2_80195730[teamIdx]; + + // @port: Hide Falco's spawn behind Fox's Arwing so he doesn't look like he came out of nowhere on widescreen. + if (teamIdx == 0) { + srcA.y -= 100.0f; + srcA.z += 1000.0f; + } + srcB.x = D_i2_80195740[teamIdx]; srcB.y = D_i2_80195750[teamIdx]; srcB.z = D_i2_80195760[teamIdx]; From 7b6efd3540b0cf672a4ca408a98873228352cfa7 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Wed, 25 Feb 2026 02:39:23 -0300 Subject: [PATCH 37/73] use GameEngine_GetAspectRatio --- src/overlays/ovl_i2/fox_sx.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/overlays/ovl_i2/fox_sx.c b/src/overlays/ovl_i2/fox_sx.c index 4d376b3d4..3934200a3 100644 --- a/src/overlays/ovl_i2/fox_sx.c +++ b/src/overlays/ovl_i2/fox_sx.c @@ -1677,7 +1677,7 @@ void SectorX_LevelComplete_SetupTeam(ActorCutscene* this, s32 teamIdx) { srcA.z = D_i2_80195730[teamIdx]; // @port: Hide Falco's spawn behind Fox's Arwing so he doesn't look like he came out of nowhere on widescreen. - if (teamIdx == 0) { + if ((GameEngine_GetAspectRatio() > 1.66f) && (teamIdx == 0)) { srcA.y -= 100.0f; srcA.z += 1000.0f; } From fd1a1d6ca134be2130a84cf10c4eeaea10b16b78 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Mon, 2 Mar 2026 11:26:03 -0300 Subject: [PATCH 38/73] Optimize Draw Distance settings --- src/engine/fox_edisplay.c | 41 ++++++++++++++++++++------------------- src/engine/fox_enmy.c | 3 ++- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/engine/fox_edisplay.c b/src/engine/fox_edisplay.c index b0f8278b9..8ac59c452 100644 --- a/src/engine/fox_edisplay.c +++ b/src/engine/fox_edisplay.c @@ -1196,12 +1196,12 @@ void Actor_DrawAllRange(Actor* this) { } // @port draw no matter what - goto render; + // goto render; if ((var_fv0 > sViewPos.z) && (sViewPos.z > var_fv1)) { - if (fabsf(sViewPos.x) < (fabsf(sViewPos.z * 0.5f) + 500.0f)) { + if (fabsf(sViewPos.x) < (fabsf(sViewPos.z * 0.5f) + 500.0f) * 2.6f) { if (fabsf(sViewPos.y) < (fabsf(sViewPos.z * 0.5f) + 500.0f)) { - render: + // render: Matrix_RotateY(gCalcMatrix, this->obj.rot.y * M_DTOR, MTXF_APPLY); Matrix_RotateX(gCalcMatrix, this->obj.rot.x * M_DTOR, MTXF_APPLY); Matrix_RotateZ(gCalcMatrix, this->obj.rot.z * M_DTOR, MTXF_APPLY); @@ -1234,10 +1234,10 @@ void Actor_DrawAllRange(Actor* this) { } // @port draw no matter what - goto render2; + // goto render2; if ((var_fv0 > sViewPos.z) && (sViewPos.z > var_fv1)) { - if (fabsf(sViewPos.x) < (fabsf(sViewPos.z * var_fa1) + var_ft5)) { + if (fabsf(sViewPos.x) < (fabsf(sViewPos.z * var_fa1) + var_ft5) * 2.6f) { if (fabsf(sViewPos.y) < (fabsf(sViewPos.z * var_fa1) + var_ft5)) { render2: if (this->info.draw != NULL) { @@ -1325,15 +1325,15 @@ void Boss_Draw(Boss* this, s32 arg1) { sp3C = -1.0f; // @port draw no matter what - if ((gCurrentLevel != LEVEL_KATINA) && - (gCurrentLevel != LEVEL_SECTOR_Y)) { // Excepting Katina because of KaSaucerer's bug - goto render; - } + // if ((gCurrentLevel != LEVEL_KATINA) && + // (gCurrentLevel != LEVEL_SECTOR_Y)) { // Excepting Katina because of KaSaucerer's bug + // goto render; + // } if ((D_edisplay_801615D0.z < var_fv0) && (var_fv1 < D_edisplay_801615D0.z)) { - if (fabsf(D_edisplay_801615D0.x) < (fabsf(D_edisplay_801615D0.z * var_ft5) + var_fa1)) { + if (fabsf(D_edisplay_801615D0.x) < (fabsf(D_edisplay_801615D0.z * var_ft5) + var_fa1) * 1.77f) { if (fabsf(D_edisplay_801615D0.y) < (fabsf(D_edisplay_801615D0.z * var_ft5) + var_fa1)) { - render: + // render: sp3C = 1.0f; if (this->obj.id != OBJ_BOSS_BO_BASE) { if (this->obj.id != OBJ_BOSS_KA_SAUCERER) { @@ -1655,23 +1655,24 @@ void Scenery360_Draw(Scenery360* this) { Matrix_MultVec3f(gGfxMatrix, &src, &dest); +#ifndef __SWITCH__ if (gCurrentLevel == LEVEL_SECTOR_Z) { behindZdist = 6000.0f; frontZdist = -20000.0f * 2; - xyOffsetBounds = 6000.0f * 2; - xyObjDistBoundMod = 0.9f; - goto check; + // xyOffsetBounds = 6000.0f * 2; + // xyObjDistBoundMod = 0.9f; } +#endif - if ((gCurrentLevel != LEVEL_SECTOR_Y) && (gCurrentLevel != LEVEL_VENOM_ANDROSS)) { - goto render; - } + // if ((gCurrentLevel != LEVEL_SECTOR_Y) && (gCurrentLevel != LEVEL_VENOM_ANDROSS)) { + // goto render; + // } -check: + // check: if ((dest.z < behindZdist) && (frontZdist < dest.z)) { if (fabsf(dest.y) < (fabsf(dest.z * xyObjDistBoundMod) + xyOffsetBounds)) { - if (fabsf(dest.x) < (fabsf(dest.z * xyObjDistBoundMod) + xyOffsetBounds)) { - render: + if (fabsf(dest.x) < (fabsf(dest.z * xyObjDistBoundMod) + xyOffsetBounds) * 2.6f) { + // render: Display_SetSecondLight(&this->obj.pos); if (this->obj.id == OBJ_SCENERY_AND_PASSAGE) { Matrix_RotateY(gGfxMatrix, this->obj.rot.y * M_DTOR, MTXF_APPLY); diff --git a/src/engine/fox_enmy.c b/src/engine/fox_enmy.c index a9cf921c2..5c7663b29 100644 --- a/src/engine/fox_enmy.c +++ b/src/engine/fox_enmy.c @@ -111,7 +111,8 @@ bool func_enmy_80060FE4(Vec3f* arg0, f32 arg1) { Matrix_MultVec3fNoTranslate(gCalcMatrix, &src, &dest); - if ((dest.z < 1000.0f) && (arg1 < dest.z) && (fabsf(dest.x) < (fabsf(dest.z * 0.5f) + 2000.0f))) { + // @port: Extend draw distance up to 32/9 + if ((dest.z < 1000.0f) && (arg1 < dest.z) && (fabsf(dest.x) < (fabsf(dest.z * /* 0.5f */ 1.5f) + 2000.0f))) { return true; } return false; From faf5c506fff7dedcdb0cb760e4862bb2bc660f44 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Tue, 10 Mar 2026 13:06:50 -0300 Subject: [PATCH 39/73] Add A6Gorgon Cutscene Record from N64 Hardware & Refactor code. --- include/fox_record.h | 17 ++- src/engine/cutscene_record.c | 182 +++++++++++++++++++++++++++++ src/engine/fox_demo.c | 55 +-------- src/overlays/ovl_ending/fox_end1.c | 11 +- src/overlays/ovl_i1/fox_co.c | 37 +----- src/overlays/ovl_i2/fox_me.c | 10 +- src/overlays/ovl_i3/fox_a6.c | 8 ++ src/overlays/ovl_i5/fox_ma.c | 26 +---- src/overlays/ovl_i6/fox_andross.c | 79 +------------ src/overlays/ovl_i6/fox_sy.c | 10 +- 10 files changed, 221 insertions(+), 214 deletions(-) create mode 100644 src/engine/cutscene_record.c diff --git a/include/fox_record.h b/include/fox_record.h index 531b6c422..6077c6bd2 100644 --- a/include/fox_record.h +++ b/include/fox_record.h @@ -1,6 +1,6 @@ /** * Used to reproduce recordings made from real N64 hardware - * to accurately reproduce Cutscenes at the correct speed. + * to accurately play cutscenes at the correct speed. * These recordings adjust gVisPerFrame during runtime to produce * the same behaviour as the original game. */ @@ -15,8 +15,19 @@ typedef struct Record { } Record; extern Record gCarrierCutsceneRecord[13]; +extern Record gWarpzoneCsRecord[19]; +extern Record gA6GorgonCsRecord[12]; +extern Record gSyRobotCutsceneRecord[3]; +extern Record gAndrossRobotKillCutscene2[20]; +extern Record gAndrossRobotKillCutscene1[25]; +extern Record gMacbethCutsceneRecord[14]; +extern Record gGrangaCutsceneRecord[13]; +extern Record gMeCrusherCutsceneRecord[3]; +extern Record gEndingCsRecord[37]; -void UpdateVisPerFrameFromRecording(Record* record, s32 maxFrames); -void UpdateVisPerFrameFromRecording_Ending(Record* record, s32 maxFrames); +extern int gA6GorgonCsFrameCount; +extern int gWarpzoneCsFrameCount; + +void UpdateVisPerFrameFromRecording(Record* record, s32 maxFrames, int* frameCounter); #endif diff --git a/src/engine/cutscene_record.c b/src/engine/cutscene_record.c new file mode 100644 index 000000000..611c2bbef --- /dev/null +++ b/src/engine/cutscene_record.c @@ -0,0 +1,182 @@ +#include "global.h" +#include "fox_record.h" + +void UpdateVisPerFrameFromRecording(Record* record, s32 maxFrames, int* frameCounter) { + int i; + + if (*frameCounter > record[maxFrames - 1].frame) { + return; + } + + for (i = 0; i < maxFrames; i++) { + if (*frameCounter == record[i].frame) { + gVIsPerFrame = record[i].vis; + } + } +} + +int gWarpzoneCsFrameCount = 0; +int gA6GorgonCsFrameCount = 0; + +Record gWarpzoneCsRecord[19] = { + { 2, 1 }, { 3, 3 }, { 4, 4 }, { 3, 8 }, { 2, 9 }, { 3, 52 }, { 2, 54 }, { 3, 69 }, { 2, 70 }, { 3, 75 }, + { 2, 76 }, { 3, 79 }, { 2, 117 }, { 3, 118 }, { 2, 120 }, { 3, 145 }, { 2, 215 }, { 3, 216 }, { 2, 230 }, +}; + +Record gA6GorgonCsRecord[12] = { + { 3, 0 }, { 4, 3 }, { 5, 6 }, { 4, 37 }, { 5, 38 }, { 4, 44 }, + { 3, 53 }, { 4, 54 }, { 3, 57 }, { 4, 58 }, { 3, 59 }, { 2, 170 }, +}; + +Record gEndingCsRecord[37] = { + { 2, 0 }, { 5, 1 }, { 2, 2 }, { 3, 277 }, { 2, 278 }, { 3, 340 }, { 2, 397 }, { 3, 537 }, + { 2, 538 }, { 3, 548 }, { 2, 549 }, { 3, 568 }, { 2, 569 }, { 3, 620 }, { 2, 623 }, { 3, 803 }, + { 2, 806 }, { 3, 814 }, { 2, 823 }, { 3, 1282 }, { 2, 1284 }, { 5, 3703 }, { 2, 3704 }, { 4, 3904 }, + { 2, 3905 }, { 3, 4782 }, { 2, 4783 }, { 3, 4785 }, { 2, 4820 }, { 3, 4986 }, { 2, 6779 }, { 5, 6780 }, + { 2, 6781 }, { 3, 6785 }, { 4, 6786 }, { 3, 6793 }, { 2, 6799 }, +}; + +// MeCrusher destroy cutscene timings recorded from a real N64 +Record gMeCrusherCutsceneRecord[3] = { + { 2, 0 }, + { 3, 200 }, + { 2, 224 }, +}; + +// Carrier destroy cutscene timings recorded from a real N64 +Record gCarrierCutsceneRecord[13] = { + { 2, 0 }, + { 3, 2 }, + { 4, 16 }, + { 3, 50 }, + { 4, 58 }, + { 5, 67 }, + { 4, 68 }, + { 5, 71 }, + { 4, 73 }, + { 5, 74 }, + { 4, 106 }, + { 3, 146 }, + { 2, 194 }, +}; + +// Granga destroy cutscene timings recorded from a real N64 +Record gGrangaCutsceneRecord[13] = { + { 2, 0 }, + { 3, 1 }, + { 2, 5 }, + { 3, 7 }, + { 2, 52 }, + { 3, 78 }, + { 4, 103 }, + { 3, 125 }, + { 2, 153 }, + { 3, 155 }, + { 2, 157 }, + { 3, 158 }, + { 2, 160 }, +}; + +// Train cutscene timings recorded from a real N64 +Record gMacbethCutsceneRecord[14] = { + // Train breaking barriers + { 2, 0 }, + { 3, 2 }, + { 2, 31 }, + { 3, 400 }, + { 2, 418 }, + { 3, 433 }, + { 4, 435 }, + { 3, 444 }, + { 2, 509 }, + // { 3, 559 }, + // { 2, 581 }, + // { 3, 587 }, + // Explosions + { 2, 589 }, + { 3, 714 }, + { 4, 821 }, + { 5, 849 }, + { 2, 942 }, +}; + +// clang-format off + +// After defeating Andross +/* +03 00 00 00 02 00 00 08 03 00 00 13 02 00 00 23 03 00 00 9C 02 00 00 A6 03 00 00 C1 04 00 00 C6 03 00 00 C7 04 00 00 C9 +03 00 00 CD 04 00 00 CF 03 00 00 D7 04 00 00 EA 03 00 00 EC 04 00 00 ED 03 00 00 EF 04 00 00 F4 03 00 00 F5 04 00 00 F6 +03 00 01 10 04 00 01 12 03 00 01 22 04 00 01 24 03 00 01 27 04 00 01 2D 03 00 01 2E 04 00 01 30 03 00 01 31 04 00 01 33 +03 00 01 4C 04 00 01 4E 03 00 01 4F +*/ +Record gAndrossRobotKillCutscene1[25] = { + { 3, 0 }, + { 2, 8 }, + { 3, 19 }, + { 2, 35 }, + { 3, 156 }, + { 2, 166 }, + { 3, 193 }, + { 4, 198 }, + { 3, 199 }, + { 4, 201 }, + { 3, 205 }, + { 4, 207 }, + { 3, 215 }, + { 4, 234 }, + // { 3, 236 }, + { 4, 237 }, + { 3, 239 }, + { 4, 244 }, + // { 3, 245 }, + { 4, 246 }, + // { 3, 272 }, + { 4, 274 }, + // { 3, 290 }, + { 4, 292 }, + // { 3, 295 }, // too much? + { 4, 301 }, + // { 3, 302 }, + { 4, 304 }, + // { 3, 305 }, + { 4, 307 }, + // { 3, 332 }, + { 4, 334 }, + // { 3, 335 }, + { 2, 339 }, +}; + +/* +04 00 00 00 02 00 00 01 05 00 00 02 02 00 00 03 03 00 00 2A 02 00 00 2B 03 00 00 39 02 00 00 3A 03 00 00 3F 02 00 00 45 +03 00 00 9E 04 00 00 A1 05 00 00 A3 04 00 00 B6 03 00 00 B9 04 00 00 BD 05 00 00 C1 04 00 00 CD 03 00 00 CF 02 00 00 D3 +*/ +Record gAndrossRobotKillCutscene2[20] = { + { 4, 0 }, + { 2, 1 }, + { 5, 2 }, + { 2, 3 }, + { 3, 42 }, + { 2, 43 }, + { 3, 57 }, + { 2, 58 }, + { 3, 63 }, + { 2, 69 }, + { 3, 158 }, + { 4, 161 }, + { 5, 163 }, + { 4, 182 }, + { 3, 185 }, + { 4, 189 }, + { 5, 193 }, + { 4, 205 }, + { 3, 207 }, + { 2, 211 }, +}; +// clang-format on + +// SyRobot destroy cutscene timings recorded from a real N64 +Record gSyRobotCutsceneRecord[3] = { + { 2, 0 }, + { 3, 129 }, + { 2, 230 }, +}; diff --git a/src/engine/fox_demo.c b/src/engine/fox_demo.c index 8b243ca64..ce091ef0b 100644 --- a/src/engine/fox_demo.c +++ b/src/engine/fox_demo.c @@ -22,55 +22,6 @@ #include "fox_co.h" #include "fox_record.h" -int gWarpzoneCsFrameCount = 0; - -Record gWarpzoneCsRecord[] = { - { 2, 1 }, { 3, 3 }, { 4, 4 }, { 3, 8 }, { 2, 9 }, { 3, 52 }, { 2, 54 }, { 3, 69 }, { 2, 70 }, { 3, 75 }, - { 2, 76 }, { 3, 79 }, { 2, 117 }, { 3, 118 }, { 2, 120 }, { 3, 145 }, { 2, 215 }, { 3, 216 }, { 2, 230 }, -}; - -void UpdateVisPerFrameFromRecording(Record* record, s32 maxFrames) { - int i; - - if (gCsFrameCount > record[maxFrames - 1].frame) { - return; - } - - for (i = 0; i < maxFrames; i++) { - if (gCsFrameCount == record[i].frame) { - gVIsPerFrame = record[i].vis; - } - } -} - -void UpdateVisPerFrameFromRecording_Warpzone(Record* record, s32 maxFrames) { - int i; - - if (gWarpzoneCsFrameCount > record[maxFrames - 1].frame) { - return; - } - - for (i = 0; i < maxFrames; i++) { - if (gWarpzoneCsFrameCount == record[i].frame) { - gVIsPerFrame = record[i].vis; - } - } -} - -void UpdateVisPerFrameFromRecording_Ending(Record* record, s32 maxFrames) { - int i; - - if (gGameFrameCount > record[maxFrames - 1].frame) { - return; - } - - for (i = 0; i < maxFrames; i++) { - if (gGameFrameCount == record[i].frame) { - gVIsPerFrame = record[i].vis; - } - } -} - void func_demo_80048AC0(TeamId teamId) { s32 teamShield; @@ -448,8 +399,9 @@ void Cutscene_EnterWarpZone(Player* player) { s32 var_v0; s32 pad[4]; + // @Port: Vi recording gWarpzoneCsFrameCount++; - UpdateVisPerFrameFromRecording_Warpzone(gWarpzoneCsRecord, ARRAY_COUNT(gWarpzoneCsRecord)); + UpdateVisPerFrameFromRecording(gWarpzoneCsRecord, ARRAY_COUNT(gWarpzoneCsRecord), &gWarpzoneCsFrameCount); player->pos.x += player->vel.x; player->flags_228 = 0; @@ -982,7 +934,8 @@ void Cutscene_CoComplete2(Player* player) { Math_SmoothStepToF(&player->camRoll, 0.0f, 0.1f, 5.0f, 0.01f); - UpdateVisPerFrameFromRecording(gCarrierCutsceneRecord, ARRAY_COUNT(gCarrierCutsceneRecord)); + // @Port: Vi recording + UpdateVisPerFrameFromRecording(gCarrierCutsceneRecord, ARRAY_COUNT(gCarrierCutsceneRecord), &gCsFrameCount); switch (player->csState) { case 10: diff --git a/src/overlays/ovl_ending/fox_end1.c b/src/overlays/ovl_ending/fox_end1.c index 4116210c7..ec1890e1c 100644 --- a/src/overlays/ovl_ending/fox_end1.c +++ b/src/overlays/ovl_ending/fox_end1.c @@ -9,14 +9,6 @@ #include "port/mods/PortEnhancements.h" #include "fox_record.h" -Record gEndingCsRecord[] = { - { 2, 0 }, { 5, 1 }, { 2, 2 }, { 3, 277 }, { 2, 278 }, { 3, 340 }, { 2, 397 }, { 3, 537 }, - { 2, 538 }, { 3, 548 }, { 2, 549 }, { 3, 568 }, { 2, 569 }, { 3, 620 }, { 2, 623 }, { 3, 803 }, - { 2, 806 }, { 3, 814 }, { 2, 823 }, { 3, 1282 }, { 2, 1284 }, { 5, 3703 }, { 2, 3704 }, { 4, 3904 }, - { 2, 3905 }, { 3, 4782 }, { 2, 4783 }, { 3, 4785 }, { 2, 4820 }, { 3, 4986 }, { 2, 6779 }, { 5, 6780 }, - { 2, 6781 }, { 3, 6785 }, { 4, 6786 }, { 3, 6793 }, { 2, 6799 }, -}; - void Ending_8018CE20(s32); void Ending_801926D4(void); extern u32 D_ending_80192E70; @@ -1116,7 +1108,8 @@ void Ending_Main(void) { Ending_8018B3D8(); Ending_8018ABE8(); - UpdateVisPerFrameFromRecording_Ending(gEndingCsRecord, ARRAY_COUNT(gEndingCsRecord)); + // @Port: Vi recording + UpdateVisPerFrameFromRecording(gEndingCsRecord, ARRAY_COUNT(gEndingCsRecord), &gGameFrameCount); } void DrawBorders(void) { diff --git a/src/overlays/ovl_i1/fox_co.c b/src/overlays/ovl_i1/fox_co.c index e1a6ddf2b..e8bbbea71 100644 --- a/src/overlays/ovl_i1/fox_co.c +++ b/src/overlays/ovl_i1/fox_co.c @@ -11,40 +11,6 @@ #include "port/hooks/Events.h" #include "fox_record.h" -// Carrier destroy cutscene timings recorded from a real N64 -Record gCarrierCutsceneRecord[] = { - { 2, 0 }, - { 3, 2 }, - { 4, 16 }, - { 3, 50 }, - { 4, 58 }, - { 5, 67 }, - { 4, 68 }, - { 5, 71 }, - { 4, 73 }, - { 5, 74 }, - { 4, 106 }, - { 3, 146 }, - { 2, 194 }, -}; - -// Granga destroy cutscene timings recorded from a real N64 -Record gGrangaCutsceneRecord[] = { - { 2, 0 }, - { 3, 1 }, - { 2, 5 }, - { 3, 7 }, - { 2, 52 }, - { 3, 78 }, - { 4, 103 }, - { 3, 125 }, - { 2, 153 }, - { 3, 155 }, - { 2, 157 }, - { 3, 158 }, - { 2, 160 }, -}; - u8 sFightCarrier; f32 sCoGrangaWork[68]; @@ -3489,7 +3455,8 @@ void Corneria_LevelComplete1(Player* player) { f32 temp_fa1; f32 temp_deg; - UpdateVisPerFrameFromRecording(gGrangaCutsceneRecord, ARRAY_COUNT(gGrangaCutsceneRecord)); + // @Port: Vi recording + UpdateVisPerFrameFromRecording(gGrangaCutsceneRecord, ARRAY_COUNT(gGrangaCutsceneRecord), &gCsFrameCount); player->arwing.upperRightFlapYrot = player->arwing.upperLeftFlapYrot = player->arwing.bottomRightFlapYrot = player->arwing.bottomLeftFlapYrot = 0.0f; diff --git a/src/overlays/ovl_i2/fox_me.c b/src/overlays/ovl_i2/fox_me.c index f2cec0424..02be4e008 100644 --- a/src/overlays/ovl_i2/fox_me.c +++ b/src/overlays/ovl_i2/fox_me.c @@ -8,13 +8,6 @@ #include "assets/ast_meteo.h" #include "fox_record.h" -// MeCrusher destroy cutscene timings recorded from a real N64 -Record gMeCrusherCutsceneRecord[] = { - { 2, 0 }, - { 3, 200 }, - { 2, 224 }, -}; - Vec3f D_i2_80195430[] = { { 122.0, -5.0, -1200.0 }, { 122.0, -103.0, -727.0 }, { 142.0, -323.0, -848.0 }, { 362.0, -59.0, -435.0 }, { 692.0, -84.0, -308.0 }, { 1000.0, -7.0, -308.0 }, { 164.0, -700.0, -308.0 }, { 263.0, -370.0, -11.0 }, @@ -2434,7 +2427,8 @@ void Meteo_LevelComplete(Player* player) { Math_SmoothStepToAngle(&player->aerobaticPitch, 0.0f, 0.1f, 20.0f, 0.0f); Math_SmoothStepToF(&player->boostSpeed, 0.0f, 0.1f, 3.0f, 0.0f); - UpdateVisPerFrameFromRecording(gMeCrusherCutsceneRecord, ARRAY_COUNT(gMeCrusherCutsceneRecord)); + // @Port: Vi recording + UpdateVisPerFrameFromRecording(gMeCrusherCutsceneRecord, ARRAY_COUNT(gMeCrusherCutsceneRecord), &gCsFrameCount); switch (player->csState) { case 0: diff --git a/src/overlays/ovl_i3/fox_a6.c b/src/overlays/ovl_i3/fox_a6.c index f2426fa82..1facf2b2f 100644 --- a/src/overlays/ovl_i3/fox_a6.c +++ b/src/overlays/ovl_i3/fox_a6.c @@ -6,6 +6,7 @@ #include "global.h" #include "assets/ast_area_6.h" +#include "fox_record.h" void Area6_8018A1B0(Boss* this, s32 arg1); void Area6_8018A2C4(Boss* this); @@ -425,6 +426,9 @@ void Area6_A6Gorgon_Init(A6Gorgon* this) { gProjectFar = 25000.0f; gBossFrameCount = 0; + // @Port: Vi recording + gA6GorgonCsFrameCount = 0; + this->health = 780; this->fwork[A6_FWK_2] = this->fwork[A6_FWK_34] = 2.0f; @@ -1187,6 +1191,10 @@ void Area6_A6Gorgon_Update(A6Gorgon* this) { break; case 11: + // @Port: Vi recording + UpdateVisPerFrameFromRecording(gA6GorgonCsRecord, ARRAY_COUNT(gA6GorgonCsRecord), &gA6GorgonCsFrameCount); + gA6GorgonCsFrameCount++; + if ((this->timer_052 == 160) && ((gPlayer[0].state == PLAYERSTATE_ACTIVE) || (gPlayer[0].state == PLAYERSTATE_U_TURN))) { gPlayer[0].state = PLAYERSTATE_LEVEL_COMPLETE; diff --git a/src/overlays/ovl_i5/fox_ma.c b/src/overlays/ovl_i5/fox_ma.c index edc31257f..f6b6bd527 100644 --- a/src/overlays/ovl_i5/fox_ma.c +++ b/src/overlays/ovl_i5/fox_ma.c @@ -60,29 +60,6 @@ Vec3f D_i5_801BE688[2]; Vec3f D_i5_801BE6A0[12]; s32 D_i5_801BE734[4]; -// Train cutscene timings recorded from a real N64 -Record gMacbethCutsceneRecord[] = { - // Train breaking barriers - { 2, 0 }, - { 3, 2 }, - { 2, 31 }, - { 3, 400 }, - { 2, 418 }, - { 3, 433 }, - { 4, 435 }, - { 3, 444 }, - { 2, 509 }, - // { 3, 559 }, - // { 2, 581 }, - // { 3, 587 }, - // Explosions - { 2, 589 }, - { 3, 714 }, - { 4, 821 }, - { 5, 849 }, - { 2, 942 }, -}; - UnkStruct_D_i5_801B8E50 D_i5_801B8E50[156] = { { 5174.4f, -2141.0f, 0.0f, 350.0f, OBJ_SCENERY_MA_TRAIN_TRACK_3 }, { 3401.4f, -1828.0f, 0.0f, 350.0f, OBJ_SCENERY_MA_TRAIN_TRACK_3 }, @@ -6509,7 +6486,8 @@ void Macbeth_LevelComplete2(Player* player) { Vec3f spD8; f32 zeroVar = 0.0f; - UpdateVisPerFrameFromRecording(gMacbethCutsceneRecord, ARRAY_COUNT(gMacbethCutsceneRecord)); + // @Port: Vi recording + UpdateVisPerFrameFromRecording(gMacbethCutsceneRecord, ARRAY_COUNT(gMacbethCutsceneRecord), &gCsFrameCount); switch (player->csState) { case 0: diff --git a/src/overlays/ovl_i6/fox_andross.c b/src/overlays/ovl_i6/fox_andross.c index ba243c313..18d6cf82b 100644 --- a/src/overlays/ovl_i6/fox_andross.c +++ b/src/overlays/ovl_i6/fox_andross.c @@ -3852,80 +3852,6 @@ f32 D_i6_801A7F4C; f32 D_i6_801A7F50; f32 D_i6_801A7F54; -// clang-format off - -// After defeating Andross -/* -03 00 00 00 02 00 00 08 03 00 00 13 02 00 00 23 03 00 00 9C 02 00 00 A6 03 00 00 C1 04 00 00 C6 03 00 00 C7 04 00 00 C9 -03 00 00 CD 04 00 00 CF 03 00 00 D7 04 00 00 EA 03 00 00 EC 04 00 00 ED 03 00 00 EF 04 00 00 F4 03 00 00 F5 04 00 00 F6 -03 00 01 10 04 00 01 12 03 00 01 22 04 00 01 24 03 00 01 27 04 00 01 2D 03 00 01 2E 04 00 01 30 03 00 01 31 04 00 01 33 -03 00 01 4C 04 00 01 4E 03 00 01 4F -*/ -Record gAndrossRobotKillCutscene1[] = { - { 3, 0 }, - { 2, 8 }, - { 3, 19 }, - { 2, 35 }, - { 3, 156 }, - { 2, 166 }, - { 3, 193 }, - { 4, 198 }, - { 3, 199 }, - { 4, 201 }, - { 3, 205 }, - { 4, 207 }, - { 3, 215 }, - { 4, 234 }, - // { 3, 236 }, - { 4, 237 }, - { 3, 239 }, - { 4, 244 }, - // { 3, 245 }, - { 4, 246 }, - // { 3, 272 }, - { 4, 274 }, - // { 3, 290 }, - { 4, 292 }, - // { 3, 295 }, // too much? - { 4, 301 }, - // { 3, 302 }, - { 4, 304 }, - // { 3, 305 }, - { 4, 307 }, - // { 3, 332 }, - { 4, 334 }, - // { 3, 335 }, - { 2, 339 }, -}; - -/* -04 00 00 00 02 00 00 01 05 00 00 02 02 00 00 03 03 00 00 2A 02 00 00 2B 03 00 00 39 02 00 00 3A 03 00 00 3F 02 00 00 45 -03 00 00 9E 04 00 00 A1 05 00 00 A3 04 00 00 B6 03 00 00 B9 04 00 00 BD 05 00 00 C1 04 00 00 CD 03 00 00 CF 02 00 00 D3 -*/ -Record gAndrossRobotKillCutscene2[] = { - { 4, 0 }, - { 2, 1 }, - { 5, 2 }, - { 2, 3 }, - { 3, 42 }, - { 2, 43 }, - { 3, 57 }, - { 2, 58 }, - { 3, 63 }, - { 2, 69 }, - { 3, 158 }, - { 4, 161 }, - { 5, 163 }, - { 4, 182 }, - { 3, 185 }, - { 4, 189 }, - { 5, 193 }, - { 4, 205 }, - { 3, 207 }, - { 2, 211 }, -}; -// clang-format on - // Andross_LevelComplete void Andross_80193C4C(Player* player) { s32 i; @@ -3938,10 +3864,11 @@ void Andross_80193C4C(Player* player) { Vec3f sp68; s32 rnd; + // @Port: Vi recording if (player->csState < 3) { - UpdateVisPerFrameFromRecording(gAndrossRobotKillCutscene1, ARRAY_COUNT(gAndrossRobotKillCutscene1)); + UpdateVisPerFrameFromRecording(gAndrossRobotKillCutscene1, ARRAY_COUNT(gAndrossRobotKillCutscene1), &gCsFrameCount); } else if ((player->csState > 2) && player->csState < 6) { - UpdateVisPerFrameFromRecording(gAndrossRobotKillCutscene2, ARRAY_COUNT(gAndrossRobotKillCutscene2)); + UpdateVisPerFrameFromRecording(gAndrossRobotKillCutscene2, ARRAY_COUNT(gAndrossRobotKillCutscene2), &gCsFrameCount); } Math_SmoothStepToF(D_ctx_80177A48, 1.0f, 1.0f, 0.01f, 0.0f); diff --git a/src/overlays/ovl_i6/fox_sy.c b/src/overlays/ovl_i6/fox_sy.c index c24562b4f..702f11819 100644 --- a/src/overlays/ovl_i6/fox_sy.c +++ b/src/overlays/ovl_i6/fox_sy.c @@ -30,13 +30,6 @@ void SectorY_801A0510(ActorCutscene*, s32); void SectorY_ActorDebris_Setup(Actor*, f32, f32, f32, f32, f32, f32, s32); void SectorY_ActorDebris_Spawn(f32, f32, f32, f32, f32, f32, s32); -// SyRobot destroy cutscene timings recorded from a real N64 -Record gSyRobotCutsceneRecord[] = { - { 2, 0 }, - { 3, 129 }, - { 2, 230 }, -}; - f32 D_i6_801A8440[3]; void SectorY_80197B30(ActorCutscene* this, s32 timer) { @@ -2144,7 +2137,8 @@ void SectorY_LevelComplete(Player* player) { SyShogun* boss = &gBosses[0]; f32 temp_ft1; - UpdateVisPerFrameFromRecording(gSyRobotCutsceneRecord, ARRAY_COUNT(gSyRobotCutsceneRecord)); + // @Port: Vi recording + UpdateVisPerFrameFromRecording(gSyRobotCutsceneRecord, ARRAY_COUNT(gSyRobotCutsceneRecord), &gCsFrameCount); switch (player->csState) { case 0: From 00cde39873277cb34d4eef96c4c5e928d2e7748d Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Wed, 11 Mar 2026 13:31:17 -0300 Subject: [PATCH 40/73] Selectable Radio Comm Box position --- src/engine/fox_hud.c | 91 +++++++++++++++++++++----------- src/engine/fox_radio.c | 111 +++++++++++++++++++++++++++------------- src/port/ui/ImguiUI.cpp | 19 +++++++ 3 files changed, 155 insertions(+), 66 deletions(-) diff --git a/src/engine/fox_hud.c b/src/engine/fox_hud.c index bbf8de247..363f75551 100644 --- a/src/engine/fox_hud.c +++ b/src/engine/fox_hud.c @@ -210,22 +210,35 @@ void HUD_TeamDownWrench_Draw(s32 arg0) { } } } else { - f32 x = 48.0f; - f32 y = 0.0f; - HUD_MatrixTranslateCoordLeft(&x, &y); - y = -81.0f; - Lib_InitOrtho(&gMasterDisp); - Matrix_Push(&gGfxMatrix); - Matrix_Translate(gGfxMatrix, x, y, -600.0f, MTXF_APPLY); - Matrix_RotateZ(gGfxMatrix, M_PI / 4, MTXF_APPLY); - Matrix_Scale(gGfxMatrix, 0.31f, 0.31f, 1.0f, MTXF_APPLY); - Matrix_SetGfxMtx(&gMasterDisp); - gSPDisplayList(gMasterDisp++, aDownWrenchDL); - Matrix_RotateZ(gGfxMatrix, 3 * M_PI / 2, MTXF_APPLY); - Matrix_SetGfxMtx(&gMasterDisp); - gSPDisplayList(gMasterDisp++, aDownWrenchDL); - Matrix_Pop(&gGfxMatrix); - Lib_InitPerspective(&gMasterDisp); + if (CVarGetInteger("gRadioCommBox.expand", 0) == 1) { + f32 x = 48.0f; + f32 y = 0.0f; + HUD_MatrixTranslateCoordLeft(&x, &y); + y = -81.0f; + Lib_InitOrtho(&gMasterDisp); + Matrix_Push(&gGfxMatrix); + Matrix_Translate(gGfxMatrix, x, y, -600.0f, MTXF_APPLY); + Matrix_RotateZ(gGfxMatrix, M_PI / 4, MTXF_APPLY); + Matrix_Scale(gGfxMatrix, 0.31f, 0.31f, 1.0f, MTXF_APPLY); + Matrix_SetGfxMtx(&gMasterDisp); + gSPDisplayList(gMasterDisp++, aDownWrenchDL); + Matrix_RotateZ(gGfxMatrix, 3 * M_PI / 2, MTXF_APPLY); + Matrix_SetGfxMtx(&gMasterDisp); + gSPDisplayList(gMasterDisp++, aDownWrenchDL); + Matrix_Pop(&gGfxMatrix); + Lib_InitPerspective(&gMasterDisp); + } else { + Matrix_Push(&gGfxMatrix); + Matrix_Translate(gGfxMatrix, -234.0f, -167.0f, -600.0f, MTXF_APPLY); + Matrix_RotateZ(gGfxMatrix, M_PI / 4, MTXF_APPLY); + Matrix_Scale(gGfxMatrix, 0.68f, 0.68f, 1.0f, MTXF_APPLY); + Matrix_SetGfxMtx(&gMasterDisp); + gSPDisplayList(gMasterDisp++, aDownWrenchDL); + Matrix_RotateZ(gGfxMatrix, 3 * M_PI / 2, MTXF_APPLY); + Matrix_SetGfxMtx(&gMasterDisp); + gSPDisplayList(gMasterDisp++, aDownWrenchDL); + Matrix_Pop(&gGfxMatrix); + } } } @@ -2262,7 +2275,12 @@ void HUD_RadioCharacterName_Draw(void) { RCP_SetupDL(&gMasterDisp, SETUPDL_76_OPTIONAL); gDPSetPrimColor(gMasterDisp++, 0, 0, 255, 255, 0, 255); - f32 xPos = OTRGetRectDimensionFromLeftEdgeOverride(73.0f); + f32 xPos; + if (CVarGetInteger("gRadioCommBox.expand", 0) == 1) { + xPos = OTRGetRectDimensionFromLeftEdgeOverride(73.0f); + } else { + xPos = 73.0f; + } switch ((s32) gRadioMsgRadioId) { case RCID_FOX: @@ -2635,20 +2653,31 @@ void HUD_RadioDamage_Draw(void) { } if ((D_80161788 != 0) || (D_8016178C != 0)) { - RCP_SetupDL(&gMasterDisp, SETUPDL_12); - gDPSetPrimColor(gMasterDisp++, 0, 0, r, g, b, alpha); - f32 x = 48.0f; - f32 y = 0.0f; - HUD_MatrixTranslateCoordLeft(&x, &y); - y = -81.0f; - Lib_InitOrtho(&gMasterDisp); - Matrix_Push(&gGfxMatrix); - Matrix_Translate(gGfxMatrix, x, y, -139.4f, MTXF_APPLY); - Matrix_Scale(gGfxMatrix, 2.25f, 2.25f, 2.25f, MTXF_APPLY); - Matrix_SetGfxMtx(&gMasterDisp); - gSPDisplayList(gMasterDisp++, sRadioDamageDL); - Matrix_Pop(&gGfxMatrix); - Lib_InitPerspective(&gMasterDisp); + if (CVarGetInteger("gRadioCommBox.expand", 0) == 1) { + RCP_SetupDL(&gMasterDisp, SETUPDL_12); + gDPSetPrimColor(gMasterDisp++, 0, 0, r, g, b, alpha); + f32 x = 48.0f; + f32 y = 0.0f; + HUD_MatrixTranslateCoordLeft(&x, &y); + y = -81.0f; + Lib_InitOrtho(&gMasterDisp); + Matrix_Push(&gGfxMatrix); + Matrix_Translate(gGfxMatrix, x, y, -139.4f, MTXF_APPLY); + Matrix_Scale(gGfxMatrix, 2.25f, 2.25f, 2.25f, MTXF_APPLY); + Matrix_SetGfxMtx(&gMasterDisp); + gSPDisplayList(gMasterDisp++, sRadioDamageDL); + Matrix_Pop(&gGfxMatrix); + Lib_InitPerspective(&gMasterDisp); + } else { + RCP_SetupDL(&gMasterDisp, SETUPDL_12); + gDPSetPrimColor(gMasterDisp++, 0, 0, r, g, b, alpha); + Matrix_Push(&gGfxMatrix); + Matrix_Translate(gGfxMatrix, -53.9f, -38.5f, -139.4f, MTXF_APPLY); + Matrix_Scale(gGfxMatrix, 1.0f, 1.0f, 1.0f, MTXF_APPLY); + Matrix_SetGfxMtx(&gMasterDisp); + gSPDisplayList(gMasterDisp++, sRadioDamageDL); + Matrix_Pop(&gGfxMatrix); + } } } diff --git a/src/engine/fox_radio.c b/src/engine/fox_radio.c index 365b98b3f..e60f80c1e 100644 --- a/src/engine/fox_radio.c +++ b/src/engine/fox_radio.c @@ -130,36 +130,61 @@ void Radio_PlayMessage(u16* msg, RadioCharacterId character) { } void Radio_CalculatePositions() { - switch (gGameState) { - case GSTATE_TITLE: - gRadioPrintPosY = 176; - gRadioPrintPosX = OTRGetRectDimensionFromLeftEdgeOverride(85.0f); - gRadioTextBoxPosX = OTRGetRectDimensionFromLeftEdgeOverride(80.0f); - gRadioTextBoxPosY = 174.0f; - gRadioTextBoxScaleX = 4.63f; - gRadioPortraitPosX = OTRGetRectDimensionFromLeftEdgeOverride(32.0f); - gRadioPortraitPosY = 174.0f; - break; - - case GSTATE_ENDING: - gRadioPrintPosY = 176; - gRadioPrintPosX = 85.0f; - gRadioTextBoxPosX = 80.0f; - gRadioTextBoxPosY = 174.0f; - gRadioTextBoxScaleX = 4.63f; - gRadioPortraitPosX = 32.0f; - gRadioPortraitPosY = 174.0f; - break; - - case GSTATE_PLAY: - gRadioPrintPosY = 180; - gRadioPrintPosX = OTRGetRectDimensionFromLeftEdgeOverride(79.0f); - gRadioTextBoxPosX = OTRGetRectDimensionFromLeftEdgeOverride(74.0f); - gRadioTextBoxPosY = 178.0f; - gRadioTextBoxScaleX = 4.53f; - gRadioPortraitPosX = OTRGetRectDimensionFromLeftEdgeOverride(26.0f); - gRadioPortraitPosY = 178.0f; - break; + if (CVarGetInteger("gRadioCommBox.expand", 0) == 1) { + switch (gGameState) { + case GSTATE_TITLE: + gRadioPrintPosY = 176; + gRadioPrintPosX = OTRGetRectDimensionFromLeftEdgeOverride(85.0f); + gRadioTextBoxPosX = OTRGetRectDimensionFromLeftEdgeOverride(80.0f); + gRadioTextBoxPosY = 174.0f; + gRadioTextBoxScaleX = 4.63f; + gRadioPortraitPosX = OTRGetRectDimensionFromLeftEdgeOverride(32.0f); + gRadioPortraitPosY = 174.0f; + break; + + case GSTATE_ENDING: + gRadioPrintPosY = 176; + gRadioPrintPosX = 85.0f; + gRadioTextBoxPosX = 80.0f; + gRadioTextBoxPosY = 174.0f; + gRadioTextBoxScaleX = 4.63f; + gRadioPortraitPosX = 32.0f; + gRadioPortraitPosY = 174.0f; + break; + + case GSTATE_PLAY: + gRadioPrintPosY = 180; + gRadioPrintPosX = OTRGetRectDimensionFromLeftEdgeOverride(79.0f); + gRadioTextBoxPosX = OTRGetRectDimensionFromLeftEdgeOverride(74.0f); + gRadioTextBoxPosY = 178.0f; + gRadioTextBoxScaleX = 4.53f; + gRadioPortraitPosX = OTRGetRectDimensionFromLeftEdgeOverride(26.0f); + gRadioPortraitPosY = 178.0f; + break; + } + } else { + switch (gGameState) { + case GSTATE_TITLE: + case GSTATE_ENDING: + gRadioPrintPosY = 176; + gRadioPrintPosX = 85; + gRadioTextBoxPosX = 80.0f; + gRadioTextBoxPosY = 174.0f; + gRadioTextBoxScaleX = 4.63f; + gRadioPortraitPosX = 32.0f; + gRadioPortraitPosY = 174.0f; + break; + + case GSTATE_PLAY: + gRadioPrintPosY = 180; + gRadioPrintPosX = 79; + gRadioTextBoxPosX = 74.0f; + gRadioTextBoxPosY = 178.0f; + gRadioTextBoxScaleX = 4.53f; + gRadioPortraitPosX = 26.0f; + gRadioPortraitPosY = 178.0f; + break; + } } } @@ -718,12 +743,20 @@ void Radio_Draw(void) { (gCurrentRadioPortrait != RCID_1000)) { RCP_SetupDL(&gMasterDisp, SETUPDL_76_OPTIONAL); gDPSetPrimColor(gMasterDisp++, 0x00, 0x00, 255, 255, 0, 255); - Graphics_DisplaySmallText(OTRGetRectDimensionFromLeftEdgeOverride(31.0f), 167, 1.0f, 1.0f, "DOWN"); + if (CVarGetInteger("gRadioCommBox.expand", 0) == 1) { + Graphics_DisplaySmallText(OTRGetRectDimensionFromLeftEdgeOverride(31.0f), 167, 1.0f, 1.0f, "DOWN"); + } else { + Graphics_DisplaySmallText(31, 167, 1.0f, 1.0f, "DOWN"); + } HUD_TeamDownWrench_Draw(1); } if (((gCurrentRadioPortrait != RCID_STATIC) && (gCurrentRadioPortrait != RCID_STATIC + 1)) && (gCurrentRadioPortrait != RCID_1000)) { - HUD_TeamShields_Draw(OTRGetRectDimensionFromLeftEdgeOverride(22.0f), 165.0f, gTeamShields[idx]); + if (CVarGetInteger("gRadioCommBox.expand", 0) == 1) { + HUD_TeamShields_Draw(OTRGetRectDimensionFromLeftEdgeOverride(22.0f), 165.0f, gTeamShields[idx]); + } else { + HUD_TeamShields_Draw(22.0f, 165.0f, gTeamShields[idx]); + } } } @@ -767,12 +800,20 @@ void Radio_Draw(void) { (gCurrentRadioPortrait != RCID_STATIC + 1) && (gCurrentRadioPortrait != RCID_1000)) { RCP_SetupDL(&gMasterDisp, SETUPDL_76_OPTIONAL); gDPSetPrimColor(gMasterDisp++, 0x00, 0x00, 255, 255, 0, 255); - Graphics_DisplaySmallText(OTRGetRectDimensionFromLeftEdgeOverride(31.0f), 167, 1.0f, 1.0f, "DOWN"); + if (CVarGetInteger("gRadioCommBox.expand", 0) == 1) { + Graphics_DisplaySmallText(OTRGetRectDimensionFromLeftEdgeOverride(31.0f), 167, 1.0f, 1.0f, "DOWN"); + } else { + Graphics_DisplaySmallText(31.0f, 167, 1.0f, 1.0f, "DOWN"); + } } if (((gCurrentRadioPortrait != RCID_STATIC) && (gCurrentRadioPortrait != RCID_STATIC + 1)) && (gCurrentRadioPortrait != RCID_1000)) { - HUD_TeamShields_Draw(OTRGetRectDimensionFromLeftEdgeOverride(22.0f), 165.0f, - gActors[idx].health * 2.55f); + if (CVarGetInteger("gRadioCommBox.expand", 0) == 1) { + HUD_TeamShields_Draw(OTRGetRectDimensionFromLeftEdgeOverride(22.0f), 165.0f, + gActors[idx].health * 2.55f); + } else { + HUD_TeamShields_Draw(22.0f, 165.0f, gActors[idx].health * 2.55f); + } } } if (((gCurrentRadioPortrait != RCID_STATIC) && (gCurrentRadioPortrait != RCID_STATIC + 1)) && diff --git a/src/port/ui/ImguiUI.cpp b/src/port/ui/ImguiUI.cpp index 67dfb7e1f..b9bc2c32f 100644 --- a/src/port/ui/ImguiUI.cpp +++ b/src/port/ui/ImguiUI.cpp @@ -570,6 +570,10 @@ static const char* hudAspects[] = { "Expand", "Custom", "Original (4:3)", "Widescreen (16:9)", "Nintendo 3DS (5:3)", "16:10 (8:5)", "Ultrawide (21:9)" }; +static const char* radioCommBox[] = { + "Original", "Expand" +}; + void DrawEnhancementsMenu() { if (UIWidgets::BeginMenu("Enhancements")) { @@ -622,6 +626,21 @@ void DrawEnhancementsMenu() { } if (UIWidgets::BeginMenu("HUD")) { + if (UIWidgets::CVarCombobox("Radio Communication Box", "gRadioCommBox.Selection", radioCommBox, + { + .tooltip = "Which Aspect Ratio to use when drawing the Radio Communication Box", + .defaultIndex = 0, + })) { + switch (CVarGetInteger("gRadioCommBox.Selection", 0)) { + case 0: + CVarSetInteger("gRadioCommBox.expand", 0); + break; + case 1: + CVarSetInteger("gRadioCommBox.expand", 1); + break; + } + } + if (UIWidgets::CVarCombobox("HUD Aspect Ratio", "gHUDAspectRatio.Selection", hudAspects, { .tooltip = "Which Aspect Ratio to use when drawing the HUD (Radar, gauges and radio messages)", From 47831f54ae2dbc274252aa556f4b1fa4b9731d65 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Thu, 12 Mar 2026 19:44:03 -0300 Subject: [PATCH 41/73] Refactor Interpolation System --- include/functions.h | 2 +- src/engine/fox_beam.c | 10 +- src/engine/fox_bg.c | 25 ++-- src/engine/fox_display.c | 109 ++++++++------- src/engine/fox_edisplay.c | 34 +++-- src/engine/fox_effect.c | 3 +- src/engine/fox_play.c | 2 +- src/overlays/ovl_i5/fox_ground.c | 224 +++++++++++++++++-------------- src/overlays/ovl_i5/fox_ti.c | 2 +- 9 files changed, 225 insertions(+), 186 deletions(-) diff --git a/include/functions.h b/include/functions.h index a7217af07..b224066db 100644 --- a/include/functions.h +++ b/include/functions.h @@ -505,7 +505,7 @@ void Ending_Main(void); void Ending_Draw(void); // sf_i5_5 -bool Ground_801B49D0(Actor* actor); +bool Ground_List_Setup(Actor* actor); // libultra void gSPSegmentLoadRes(void* value, int segNum, uintptr_t target); diff --git a/src/engine/fox_beam.c b/src/engine/fox_beam.c index ae19d6f71..4006d4dcc 100644 --- a/src/engine/fox_beam.c +++ b/src/engine/fox_beam.c @@ -1067,6 +1067,8 @@ void PlayerShot_DrawHitmark(PlayerShot* shot) { return; } } else { + FrameInterpolation_RecordOpenChild(shot, 1); + shot->obj.rot.y = -gPlayer[gPlayerNum].camYaw; Matrix_RotateY(gGfxMatrix, shot->obj.rot.y, MTXF_APPLY); Matrix_Scale(gGfxMatrix, 2.0f, 2.0f, 2.0f, MTXF_APPLY); @@ -1088,6 +1090,8 @@ void PlayerShot_DrawHitmark(PlayerShot* shot) { gSPDisplayList(gMasterDisp++, D_1025800); break; } + + FrameInterpolation_RecordCloseChild(); } } @@ -1249,6 +1253,8 @@ void PlayerShot_DrawShot(PlayerShot* shot) { Object_Kill(&shot->obj, shot->sfxSource); } } else { + FrameInterpolation_RecordOpenChild(shot, 0); + if ((shot->obj.id == PLAYERSHOT_TANK) || (shot->obj.id == PLAYERSHOT_ON_FOOT) || (shot->obj.id == PLAYERSHOT_7)) { shot->obj.rot.y = RAD_TO_DEG(-gPlayer[gPlayerNum].camYaw); @@ -1512,12 +1518,11 @@ void PlayerShot_DrawShot(PlayerShot* shot) { gSPDisplayList(gMasterDisp++, D_GREAT_FOX_E00DFB0); break; } + FrameInterpolation_RecordCloseChild(); } } void PlayerShot_Draw(PlayerShot* shot) { - FrameInterpolation_RecordOpenChild(shot, 0); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); switch (shot->obj.status) { case SHOT_ACTIVE: PlayerShot_DrawShot(shot); @@ -1526,7 +1531,6 @@ void PlayerShot_Draw(PlayerShot* shot) { PlayerShot_DrawHitmark(shot); break; } - FrameInterpolation_RecordCloseChild(); } void PlayerShot_UpdateHitmark(PlayerShot* shot) { diff --git a/src/engine/fox_bg.c b/src/engine/fox_bg.c index 9cdb5f073..06f83608a 100644 --- a/src/engine/fox_bg.c +++ b/src/engine/fox_bg.c @@ -308,7 +308,6 @@ void Background_DrawStarfield(void) { FrameInterpolation_ShouldInterpolateFrame(false); } else { FrameInterpolation_RecordOpenChild("Starfield", i); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); } // Translate to (vx, vy) in ortho coordinates @@ -442,9 +441,8 @@ void Background_DrawPartialStarfield(s32 yMin, s32 yMax) { // Stars that are in // Check if the star is within the visible screen area if ((vx >= 0) && (vx < currentScreenWidth) && (yMin < vy) && (vy < yMax)) { - // Tag the transform. Assuming TAG_STARFIELD is a defined base tag value - FrameInterpolation_RecordOpenChild("SmallStarfield", i); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); + + FrameInterpolation_RecordOpenChild("PartialStarfield", i); // Translate to (vx, vy) in ortho coordinates Matrix_Push(&gGfxMatrix); Matrix_Translate(gGfxMatrix, vx - (currentScreenWidth / 2.0f), -(vy - (currentScreenHeight / 2.0f)), 0.0f, @@ -551,7 +549,6 @@ void Background_DrawBackdrop(void) { } else { // @port: Tag the transform. FrameInterpolation_RecordOpenChild("Backdrop", i); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); } switch (gCurrentLevel) { @@ -627,7 +624,6 @@ void Background_DrawBackdrop(void) { } else { // @port: Tag the transform. FrameInterpolation_RecordOpenChild("Backdrop", i); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); } switch ((s32) gCurrentLevel) { @@ -669,7 +665,6 @@ void Background_DrawBackdrop(void) { } else { // @port: Tag the transform. FrameInterpolation_RecordOpenChild("Backdrop", 0); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); } if ((gDrawBackdrop == 2) || (gDrawBackdrop == 7)) { @@ -771,7 +766,6 @@ void Background_DrawBackdrop(void) { // Render the textures across the screen (left to right) for (int i = 0; i < 5; i++) { FrameInterpolation_RecordOpenChild("Backdrop", i); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); if (gPlayer[0].state == PLAYERSTATE_LEVEL_INTRO) { gSPDisplayList(gMasterDisp++, D_AQ_601AFF0); } else { @@ -839,7 +833,6 @@ void Background_DrawBackdrop(void) { } else { // @port: Tag the transform. FrameInterpolation_RecordOpenChild("Backdrop", i); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); } if (gCurrentLevel == LEVEL_TITANIA) { @@ -1139,12 +1132,16 @@ void Background_DrawSun(void) { sunScale = sKaSunScales; } for (i = 0; i < 5; i++, sunColor++, sunAlpha++, sunDL++, sunScale++) { + FrameInterpolation_RecordOpenChild("Sun", i); + Matrix_Push(&gGfxMatrix); Matrix_Scale(gGfxMatrix, *sunScale, *sunScale, *sunScale, MTXF_APPLY); Matrix_SetGfxMtx(&gMasterDisp); gDPSetPrimColor(gMasterDisp++, 0x00, 0x00, sunColor->r, sunColor->g, sunColor->b, *sunAlpha); gSPDisplayList(gMasterDisp++, *sunDL); Matrix_Pop(&gGfxMatrix); + + FrameInterpolation_RecordCloseChild(); } Matrix_Pop(&gGfxMatrix); } @@ -1265,7 +1262,7 @@ void AllRangeGround_Draw(void) { // LOWER RIGHT QUADRANT if (gPlayer[0].pos.x > maxDistX && gPlayer[0].pos.z > maxDistZ) { // lower right corner piece : 4 - // // lower middle left piece : 5 + // lower middle left piece : 5 // lower middle right piece : 6 // side upper right piece: 14 // side lower right piece : 15 @@ -1274,11 +1271,11 @@ void AllRangeGround_Draw(void) { } } - Matrix_Push(&gGfxMatrix); - // @port: Tag the transform. FrameInterpolation_RecordOpenChild("360Ground", i); + Matrix_Push(&gGfxMatrix); + Matrix_Translate(gGfxMatrix, sGroundPositions360x_FIX[i], 0.0f, sGroundPositions360z_FIX[i], MTXF_APPLY); if (gCurrentLevel == LEVEL_TRAINING) { @@ -1394,6 +1391,8 @@ void Background_DrawGround(void) { #endif } + FrameInterpolation_RecordOpenChild("Ground", 0); + Matrix_Push(&gGfxMatrix); Matrix_Translate(gGfxMatrix, gPlayer[gPlayerNum].xPath, -3.0f + gCameraShakeY, sp1D4, MTXF_APPLY); @@ -2255,6 +2254,8 @@ void Background_DrawGround(void) { break; } Matrix_Pop(&gGfxMatrix); + + FrameInterpolation_RecordCloseChild(); } // Unused. Early water implementation in Aquas? diff --git a/src/engine/fox_display.c b/src/engine/fox_display.c index 8351cd541..1434d68a2 100644 --- a/src/engine/fox_display.c +++ b/src/engine/fox_display.c @@ -271,11 +271,11 @@ void Display_OnFootFalco_PostLimbDraw(s32 limbIndex, Vec3f* rot, void* data) { } void Display_OnFootMuzzleFlash(Player* player) { - Matrix_Push(&gGfxMatrix); - // @port: Tag the transform. FrameInterpolation_RecordOpenChild("Display_OnFootMuzzleFlash", player->num); + Matrix_Push(&gGfxMatrix); + Matrix_Copy(gGfxMatrix, &gIdentityMatrix); if ((player->state == PLAYERSTATE_ACTIVE) && (player->csTimer != 0)) { Matrix_Translate(gGfxMatrix, D_display_801613B0[player->num].x, D_display_801613B0[player->num].y, @@ -325,6 +325,8 @@ void Display_Landmaster(Player* player) { Vec3f sp4C = { 0.0f, 0.0f, 90.0f }; Vec3f sp40 = { 0.0f, 40.0f, -70.0f }; + FrameInterpolation_RecordOpenChild("Landmaster", player->num); + Matrix_Push(&gGfxMatrix); if (!gVersusMode) { @@ -364,6 +366,8 @@ void Display_Landmaster(Player* player) { Matrix_MultVec3f(gGfxMatrix, &sp4C, &D_display_80161548[player->num]); Matrix_Pop(&gGfxMatrix); + + FrameInterpolation_RecordCloseChild(); } Gfx* sFaceDL[] = { aAwFoxHeadDL, aAwFalcoHeadDL, aAwSlippyHeadDL, aAwPeppyHeadDL }; @@ -375,11 +379,11 @@ f32 sReticleScales[] = { 0.0f, 0.0f, 0.0f, 0.0f }; f32 sPlayerShadowing = 0.0f; void Display_LandmasterMuzzleFlash(Player* player) { - Matrix_Push(&gGfxMatrix); - // @port: Tag the transform. FrameInterpolation_RecordOpenChild("Display_LandmasterMuzzleFlash", player->num); + Matrix_Push(&gGfxMatrix); + if ((player->state == PLAYERSTATE_ACTIVE) && (player->unk_1A0 != 0)) { Matrix_Translate(gGfxMatrix, D_display_80161548[player->num].x, D_display_80161548[player->num].y, D_display_80161548[player->num].z, MTXF_APPLY); @@ -431,11 +435,11 @@ void Display_LandmasterThrusters(Player* player) { sp2C *= 1.1f; } - Matrix_Push(&gGfxMatrix); - // @port: Tag the transform. FrameInterpolation_RecordOpenChild("Display_LandmasterThrusters_1", player->num); + Matrix_Push(&gGfxMatrix); + Matrix_Translate(gGfxMatrix, 20.0f, 30.0f, -10.0f, MTXF_APPLY); if (!gVersusMode) { @@ -468,11 +472,11 @@ void Display_LandmasterThrusters(Player* player) { sp2C *= 1.1f; } - Matrix_Push(&gGfxMatrix); - // @port: Tag the transform. FrameInterpolation_RecordOpenChild("Display_LandmasterThrusters_2", player->num); + Matrix_Push(&gGfxMatrix); + Matrix_Translate(gGfxMatrix, -20.0f, 30.0f, -10.0f, MTXF_APPLY); if (!gVersusMode) { @@ -697,10 +701,11 @@ void Display_ArwingWings(ArwingInfo* arwing) { } gSPSetGeometryMode(gMasterDisp++, G_CULL_BACK); - Matrix_Pop(&gGfxMatrix); // @port Pop the transform id. FrameInterpolation_RecordCloseChild(); + + Matrix_Pop(&gGfxMatrix); } void Display_Unused(f32 arg0, f32 arg1, UNK_TYPE arg2, UNK_TYPE arg3) { @@ -708,11 +713,11 @@ void Display_Unused(f32 arg0, f32 arg1, UNK_TYPE arg2, UNK_TYPE arg3) { } void Display_CockpitGlass(void) { - Matrix_Push(&gGfxMatrix); - // @port: Tag the transform. FrameInterpolation_RecordOpenChild("Display_CockpitGlass", 0); + Matrix_Push(&gGfxMatrix); + Matrix_Copy(gGfxMatrix, &D_display_80161418[0]); Matrix_Translate(gGfxMatrix, 0.0f, D_display_800CA290, D_display_800CA294, MTXF_APPLY); Matrix_Scale(gGfxMatrix, D_display_800CA28C, D_display_800CA28C, D_display_800CA28C, MTXF_APPLY); @@ -793,10 +798,10 @@ void Display_Reticle(Player* player) { (((gGameState == GSTATE_PLAY) && (player->state == PLAYERSTATE_ACTIVE)) || (gGameState == GSTATE_MENU))) { for (i = 0; i < 2; i++) { FrameInterpolation_RecordOpenChild("Reticle", (player->num << 16) + i); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); translate = &D_display_801613E0[i]; Matrix_Push(&gGfxMatrix); Matrix_Translate(gGfxMatrix, translate->x, translate->y, translate->z, MTXF_APPLY); + if (gChargeTimers[player->num] >= 20) { RCP_SetupDL(&gMasterDisp, SETUPDL_63_OPTIONAL); if (i == 1) { @@ -815,6 +820,7 @@ void Display_Reticle(Player* player) { Matrix_Scale(gGfxMatrix, sReticleScales[player->num], sReticleScales[player->num], 1.0f, MTXF_APPLY); Math_SmoothStepToF(&sReticleScales[player->num], 1.0f, 1.0f, 0.2f, 0.0f); } + Matrix_Scale(gGfxMatrix, 4.0f, 4.0f, 4.0f, MTXF_APPLY); Matrix_SetGfxMtx(&gMasterDisp); gSPDisplayList(gMasterDisp++, D_1024F60); @@ -856,6 +862,8 @@ void Display_PlayerShadow_Draw(Player* player) { player->shadowing = 180; } + FrameInterpolation_RecordOpenChild("PlayerShadow", player->num); + switch (player->form) { case FORM_ARWING: fake_label: @@ -895,6 +903,8 @@ void Display_PlayerShadow_Draw(Player* player) { Matrix_Pop(&gGfxMatrix); break; } + + FrameInterpolation_RecordCloseChild(); } void Display_DrawEngineGlow(EngineGlowColor color) { @@ -921,6 +931,9 @@ void Display_DrawEngineGlow(EngineGlowColor color) { void Display_LandmasterEngineGlow_Draw(Player* player) { RCP_SetupDL_64(); gDPSetPrimColor(gMasterDisp++, 0x00, 0x00, 255, 255, 255, 100); + + FrameInterpolation_RecordOpenChild("LandmasterEngineGlow", player->num); + Matrix_Push(&gGfxMatrix); Matrix_RotateZ(gGfxMatrix, player->bankAngle * M_DTOR, MTXF_APPLY); if (player->form == FORM_LANDMASTER) { @@ -941,6 +954,8 @@ void Display_LandmasterEngineGlow_Draw(Player* player) { Matrix_SetGfxMtx(&gMasterDisp); Display_DrawEngineGlow(gLevelType); Matrix_Pop(&gGfxMatrix); + + FrameInterpolation_RecordCloseChild(); } void Display_BarrelRollShield(Player* player) { @@ -970,10 +985,10 @@ void Display_BarrelRollShield(Player* player) { zRotDirection = -1.0f; } - Matrix_Push(&gGfxMatrix); - // @port: Tag the transform. - FrameInterpolation_RecordOpenChild("BarrelRollShield", 0); + FrameInterpolation_RecordOpenChild("BarrelRollShield", player->num); + + Matrix_Push(&gGfxMatrix); Matrix_Translate(gGfxMatrix, player->pos.x + dest.x, player->pos.y + dest.y, player->trueZpos + player->zPath + dest.z, MTXF_APPLY); @@ -1005,10 +1020,10 @@ void Display_BarrelRollShield(Player* player) { void Display_UnusedShield(Player* player) { if (gShieldAlpha[player->num] > 1.0f) { - Matrix_Push(&gGfxMatrix); - // @port: Tag the transform. - FrameInterpolation_RecordOpenChild("Display_UnusedShield", 0); + FrameInterpolation_RecordOpenChild("Display_UnusedShield", player->num); + + Matrix_Push(&gGfxMatrix); Matrix_Copy(gGfxMatrix, &D_display_80161418[player->num]); Matrix_Translate(gGfxMatrix, 0.0f, -5.0f, 10.0f, MTXF_APPLY); @@ -1046,12 +1061,16 @@ void Display_ArwingLaserCharge(Player* player) { Matrix_MultVec3f(gCalcMatrix, &spC4, &sp94); } + Matrix_Push(&gGfxMatrix); - + sp80 = gChargeTimers[player->num] / 20.0f; - + Matrix_Translate(gGfxMatrix, sp94.x, sp94.y, sp94.z, MTXF_NEW); Matrix_Scale(gGfxMatrix, sp80, sp80, 1.0f, MTXF_APPLY); + + FrameInterpolation_RecordOpenChild("ArwingLaserCharge", player->num); + Matrix_Push(&gGfxMatrix); if (player->alternateView && (gLevelMode == LEVELMODE_ON_RAILS)) { @@ -1089,6 +1108,10 @@ void Display_ArwingLaserCharge(Player* player) { gSPDisplayList(gMasterDisp++, aStarDL); Matrix_Pop(&gGfxMatrix); + FrameInterpolation_RecordCloseChild(); + + FrameInterpolation_RecordOpenChild("ArwingLaserCharge2", player->num); + if (player->alternateView && (gLevelMode == LEVELMODE_ON_RAILS)) { Matrix_Scale(gGfxMatrix, 0.3f, 0.3f, 0.3f, MTXF_APPLY); } @@ -1103,6 +1126,8 @@ void Display_ArwingLaserCharge(Player* player) { Matrix_SetGfxMtx(&gMasterDisp); gSPDisplayList(gMasterDisp++, aOrbDL); Matrix_Pop(&gGfxMatrix); + + FrameInterpolation_RecordCloseChild(); } if (gMuzzleFlashScale[player->num] > 0.1f) { Matrix_Push(&gGfxMatrix); @@ -1124,10 +1149,10 @@ void Display_ArwingLaserCharge(Player* player) { Matrix_MultVec3f(gCalcMatrix, &spC4, &sp94); } - Matrix_Push(&gGfxMatrix); - // @port: Tag the transform. - FrameInterpolation_RecordOpenChild("ArwingMuzzleFlash", 0); + FrameInterpolation_RecordOpenChild("ArwingMuzzleFlash1", player->num); + + Matrix_Push(&gGfxMatrix); Matrix_Translate(gGfxMatrix, sp94.x, sp94.y, sp94.z, MTXF_NEW); Matrix_Scale(gGfxMatrix, gMuzzleFlashScale[player->num], gMuzzleFlashScale[player->num], 1.0f, @@ -1152,10 +1177,11 @@ void Display_ArwingLaserCharge(Player* player) { } Matrix_MultVec3f(gCalcMatrix, &spAC, &sp94); Matrix_MultVec3f(gCalcMatrix, &spA0, &sp88); - Matrix_Push(&gGfxMatrix); // @port: Tag the transform. - FrameInterpolation_RecordOpenChild("ArwingMuzzleFlash", 0); + FrameInterpolation_RecordOpenChild("ArwingMuzzleFlash2", player->num); + + Matrix_Push(&gGfxMatrix); Matrix_Translate(gGfxMatrix, sp94.x, sp94.y, sp94.z, MTXF_NEW); Matrix_Scale(gGfxMatrix, gMuzzleFlashScale[player->num], gMuzzleFlashScale[player->num], 1.0f, @@ -1170,7 +1196,7 @@ void Display_ArwingLaserCharge(Player* player) { Matrix_Push(&gGfxMatrix); // @port: Tag the transform. - FrameInterpolation_RecordOpenChild("ArwingMuzzleFlash", 1); + FrameInterpolation_RecordOpenChild("ArwingMuzzleFlash3", player->num); Matrix_Translate(gGfxMatrix, sp88.x, sp88.y, sp88.z, MTXF_NEW); Matrix_Scale(gGfxMatrix, gMuzzleFlashScale[player->num], gMuzzleFlashScale[player->num], 1.0f, @@ -1343,7 +1369,7 @@ void Display_ArwingWingTrail_Draw(Player* player) { gDPSetPrimColor(gMasterDisp++, 0x00, 0x00, 255, 255, 255, 100); // @port: Tag the transform. - FrameInterpolation_RecordOpenChild("WingTrail", 0); + FrameInterpolation_RecordOpenChild("WingTrail", player->num); if (player->arwing.leftWingState == WINGSTATE_INTACT) { Matrix_Push(&gGfxMatrix); @@ -1363,7 +1389,7 @@ void Display_ArwingWingTrail_Draw(Player* player) { FrameInterpolation_RecordCloseChild(); // @port: Tag the transform. - FrameInterpolation_RecordOpenChild("WingTrail", 1); + FrameInterpolation_RecordOpenChild("WingTrail2", player->num); if (player->arwing.rightWingState == WINGSTATE_INTACT) { Matrix_Push(&gGfxMatrix); @@ -1637,8 +1663,8 @@ void Display_ActorMarks(void) { for (i = 0; i < ARRAY_COUNT(gTeamArrowsViewPos); i++) { if (gTeamArrowsViewPos[i].z < 0.0f) { - FrameInterpolation_RecordOpenChild(&gTeamArrowsViewPos[i], i); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); + FrameInterpolation_RecordOpenChild("ActorMarks", i); + var_fs0 = (VEC3F_MAG(&gTeamArrowsViewPos[i])) * 0.0015f; if (var_fs0 > 100.0f) { var_fs0 = 100.0f; @@ -1683,8 +1709,7 @@ void Display_LockOnIndicator(void) { if (gLockOnTargetViewPos[i].z < 0.0f) { var_fs0 = VEC3F_MAG(&gLockOnTargetViewPos[i]); if (var_fs0 < 20000.0f) { - FrameInterpolation_RecordOpenChild("LcckOnIndicator", 0); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); + FrameInterpolation_RecordOpenChild("LockOnIndicator", i); var_fs0 *= 0.0015f; if (var_fs0 > 100.0f) { var_fs0 = 100.0f; @@ -1797,9 +1822,7 @@ void Display_CsLevelCompleteHandleCamera(Player* player) { break; } } -#if 1 -f32 gTestVarF = 0.0f; -#endif + void Display_Update(void) { s32 i; Vec3f tempVec; @@ -1895,8 +1918,7 @@ void Display_Update(void) { printf("CAMERA 1 SKIPED: %d\n", camSkipTimes++); gCamera1Skipped = true; } else { - FrameInterpolation_RecordOpenChild("GamePlayCam", 0); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); + FrameInterpolation_RecordOpenChild("GamePlayCam", camPlayer->num); gCamera1Skipped = false; } @@ -1919,10 +1941,8 @@ void Display_Update(void) { } Background_DrawBackdrop(); - FrameInterpolation_RecordOpenChild("Sun", 0); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); Background_DrawSun(); - FrameInterpolation_RecordCloseChild(); + Matrix_Push(&gGfxMatrix); Matrix_LookAt(gGfxMatrix, gPlayCamEye.x, gPlayCamEye.y, gPlayCamEye.z, gPlayCamAt.x, gPlayCamAt.y, gPlayCamAt.z, playerCamUp.x, playerCamUp.y, playerCamUp.z, MTXF_APPLY); @@ -1938,10 +1958,7 @@ void Display_Update(void) { Matrix_Pop(&gGfxMatrix); } else if (gGroundSurface != SURFACE_WATER) { D_bg_8015F964 = false; - FrameInterpolation_RecordOpenChild("Ground", 0); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); Background_DrawGround(); - FrameInterpolation_RecordCloseChild(); } } @@ -2010,10 +2027,7 @@ void Display_Update(void) { if ((gGroundSurface == SURFACE_WATER) || (gAqDrawMode != 0)) { D_bg_8015F964 = true; Effect_Draw(1); - FrameInterpolation_RecordOpenChild("Ground", 0); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); Background_DrawGround(); - FrameInterpolation_RecordCloseChild(); } if ((gCurrentLevel != LEVEL_AQUAS) && @@ -2026,12 +2040,9 @@ void Display_Update(void) { for (i = 0, player = &gPlayer[0]; i < gCamCount; i++, player++) { if (sPlayersVisible[i]) { - FrameInterpolation_RecordOpenChild(player, i); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); Display_PlayerShadow_Update(player); Display_PlayerFeatures(player); Display_ArwingWingTrail_Update(player); - FrameInterpolation_RecordCloseChild(); } } diff --git a/src/engine/fox_edisplay.c b/src/engine/fox_edisplay.c index 8ac59c452..969cd8ad3 100644 --- a/src/engine/fox_edisplay.c +++ b/src/engine/fox_edisplay.c @@ -833,8 +833,7 @@ void ItemCheckpoint_Draw(ItemCheckpoint* this) { s32 i; if (((gGameFrameCount & 0x18) != 0) && (this->state == 0)) { - FrameInterpolation_RecordOpenChild(this, 0); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); + FrameInterpolation_RecordOpenChild("ItemCheckpoint", 0); Matrix_Push(&gGfxMatrix); RCP_SetupDL(&gMasterDisp, SETUPDL_64); gDPSetPrimColor(gMasterDisp++, 0x00, 0x00, 255, 255, 0, 255); @@ -846,14 +845,14 @@ void ItemCheckpoint_Draw(ItemCheckpoint* this) { Matrix_Pop(&gGfxMatrix); FrameInterpolation_RecordCloseChild(); } - FrameInterpolation_RecordOpenChild(this, 1); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); RCP_SetupDL(&gMasterDisp, SETUPDL_29); gSPTexture(gMasterDisp++, 2000, 2000, 0, G_TX_RENDERTILE, G_ON); gSPSetGeometryMode(gMasterDisp++, G_TEXTURE_GEN); Matrix_RotateZ(gGfxMatrix, this->unk_58 * M_DTOR, MTXF_APPLY); for (i = 0; i < 8; i++) { + FrameInterpolation_RecordOpenChild("ItemCheckpoint", i + 1); + Matrix_Push(&gGfxMatrix); Matrix_RotateZ(gGfxMatrix, i * 45.0f * M_DTOR, MTXF_APPLY); Matrix_Translate(gGfxMatrix, 2.0f * this->width, 0.0f, 0.0f, MTXF_APPLY); @@ -861,9 +860,10 @@ void ItemCheckpoint_Draw(ItemCheckpoint* this) { Graphics_SetScaleMtx(2.0f * this->unk_50); gSPDisplayList(gMasterDisp++, D_101CAE0); Matrix_Pop(&gGfxMatrix); + + FrameInterpolation_RecordCloseChild(); } gSPClearGeometryMode(gMasterDisp++, G_TEXTURE_GEN); - FrameInterpolation_RecordCloseChild(); } void ItemSilverRing_Draw(ItemSilverRing* this) { @@ -1797,7 +1797,7 @@ void Object_DrawAll(s32 cullDirection) { for (i = 0, scenery360 = gScenery360; i < 200; i++, scenery360++) { FrameInterpolation_RecordOpenChild(scenery360, i); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); + if ((scenery360->obj.status == OBJ_ACTIVE) && (scenery360->obj.id != OBJ_SCENERY_LEVEL_OBJECTS)) { if (gCurrentLevel == LEVEL_BOLSE) { spAC.x = scenery360->sfxSource[0]; @@ -1810,6 +1810,7 @@ void Object_DrawAll(s32 cullDirection) { Scenery360_Draw(scenery360); Matrix_Pop(&gGfxMatrix); } + FrameInterpolation_RecordCloseChild(); } } else { @@ -1817,7 +1818,7 @@ void Object_DrawAll(s32 cullDirection) { for (i = 0, scenery = gScenery; i < ARRAY_COUNT(gScenery); i++, scenery++) { if (scenery->obj.status >= OBJ_ACTIVE) { FrameInterpolation_RecordOpenChild(scenery, i); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); + if (cullDirection > 0) { Display_SetSecondLight(&scenery->obj.pos); } @@ -1825,6 +1826,7 @@ void Object_DrawAll(s32 cullDirection) { Scenery_Draw(scenery, cullDirection); Matrix_Pop(&gGfxMatrix); Object_UpdateSfxSource(scenery->sfxSource); + FrameInterpolation_RecordCloseChild(); } } @@ -1833,7 +1835,7 @@ void Object_DrawAll(s32 cullDirection) { for (i = 0, boss = &gBosses[0]; i < ARRAY_COUNT(gBosses); i++, boss++) { if ((boss->obj.status >= OBJ_ACTIVE) && (boss->obj.id != OBJ_BOSS_BO_BASE_SHIELD)) { FrameInterpolation_RecordOpenChild(boss, i); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); + if ((boss->timer_05C % 2) == 0) { RCP_SetupDL_29(gFogRed, gFogGreen, gFogBlue, gFogAlpha, gFogNear, gFogFar); } else { @@ -1850,6 +1852,7 @@ void Object_DrawAll(s32 cullDirection) { Object_DrawShadow(i, &boss->obj); Matrix_Pop(&gGfxMatrix); } + FrameInterpolation_RecordCloseChild(); } } @@ -1860,7 +1863,7 @@ void Object_DrawAll(s32 cullDirection) { for (i = 0, sprite = &gSprites[0]; i < ARRAY_COUNT(gSprites); i++, sprite++) { if ((sprite->obj.status >= OBJ_ACTIVE) && func_enmy_80060FE4(&sprite->obj.pos, -12000.0f)) { FrameInterpolation_RecordOpenChild(sprite, i); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); + Matrix_Push(&gGfxMatrix); if ((sprite->obj.id == OBJ_SPRITE_CO_RUIN1) || (sprite->obj.id == OBJ_SPRITE_CO_RUIN2)) { @@ -1871,6 +1874,7 @@ void Object_DrawAll(s32 cullDirection) { Sprite_Draw(sprite, cullDirection); Matrix_Pop(&gGfxMatrix); + FrameInterpolation_RecordCloseChild(); } } @@ -1878,7 +1882,7 @@ void Object_DrawAll(s32 cullDirection) { for (i = 0, actor = &gActors[0]; i < ARRAY_COUNT(gActors); i++, actor++) { if (actor->obj.status >= OBJ_ACTIVE) { FrameInterpolation_RecordOpenChild(actor, i); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); + if ((actor->timer_0C6 % 2) == 0) { if (gCurrentLevel == LEVEL_UNK_15) { RCP_SetupDL_23(); @@ -1940,12 +1944,13 @@ void Object_DrawAll(s32 cullDirection) { for (i = 0, item = &gItems[0]; i < ARRAY_COUNT(gItems); i++, item++) { if (item->obj.status >= OBJ_ACTIVE) { FrameInterpolation_RecordOpenChild(item, i); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); + Matrix_Push(&gGfxMatrix); RCP_SetupDL(&gMasterDisp, SETUPDL_29); Object_SetCullDirection(cullDirection); Item_Draw(item, cullDirection); Matrix_Pop(&gGfxMatrix); + FrameInterpolation_RecordCloseChild(); } } @@ -1964,7 +1969,7 @@ void Effect_DrawAll(s32 arg0) { for (i = 0, effect = &gEffects[0]; i < ARRAY_COUNT(gEffects); i++, effect++) { if (effect->obj.status >= OBJ_ACTIVE) { FrameInterpolation_RecordOpenChild(effect, i); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); + if (effect->info.unk_14 == 1) { effect->obj.rot.y = RAD_TO_DEG(-gPlayer[gPlayerNum].camYaw); effect->obj.rot.x = RAD_TO_DEG(gPlayer[gPlayerNum].camPitch); @@ -1985,6 +1990,7 @@ void Effect_DrawAll(s32 arg0) { Matrix_Pop(&gGfxMatrix); } } + FrameInterpolation_RecordCloseChild(); } } @@ -1992,7 +1998,7 @@ void Effect_DrawAll(s32 arg0) { for (i = 0, boss = &gBosses[0]; i < ARRAY_COUNT(gBosses); i++, boss++) { if ((boss->obj.status >= OBJ_ACTIVE) && (boss->obj.id == OBJ_BOSS_BO_BASE_SHIELD)) { FrameInterpolation_RecordOpenChild(boss, i); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); + if ((boss->timer_05C % 2) == 0) { RCP_SetupDL_29(gFogRed, gFogGreen, gFogBlue, gFogAlpha, gFogNear, gFogFar); } else { @@ -2002,6 +2008,7 @@ void Effect_DrawAll(s32 arg0) { Matrix_Push(&gGfxMatrix); Boss_Draw(boss, arg0); Matrix_Pop(&gGfxMatrix); + FrameInterpolation_RecordCloseChild(); } } @@ -2065,7 +2072,6 @@ void TexturedLine_Draw(void) { for (i = 0; i < ARRAY_COUNT(gTexturedLines); i++) { TexturedLine* texLine = &gTexturedLines[i]; FrameInterpolation_RecordOpenChild(texLine, i); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); if (gTexturedLines[i].mode != 0) { Matrix_Push(&gGfxMatrix); diff --git a/src/engine/fox_effect.c b/src/engine/fox_effect.c index 3a7665a52..ada1e7ca7 100644 --- a/src/engine/fox_effect.c +++ b/src/engine/fox_effect.c @@ -148,10 +148,11 @@ void BonusText_DrawAll(void) { for (i = 0, bonus = gBonusText; i < ARRAY_COUNT(gBonusText); i++, bonus++) { if (bonus->hits != 0) { FrameInterpolation_RecordOpenChild(bonus, i); - FrameInterpolation_RecordMarker(__FILE__, __LINE__); + Matrix_Push(&gGfxMatrix); BonusText_Draw(bonus); Matrix_Pop(&gGfxMatrix); + FrameInterpolation_RecordCloseChild(); } } diff --git a/src/engine/fox_play.c b/src/engine/fox_play.c index cb8cc26c3..c11c9dd64 100644 --- a/src/engine/fox_play.c +++ b/src/engine/fox_play.c @@ -2919,7 +2919,7 @@ void Play_Init(void) { D_ctx_801782C8 = 0; if (gCurrentLevel == LEVEL_TITANIA) { - Ground_801B5110(0.0f, 0.0f, 200.0f); + Ground_Init(0.0f, 0.0f, 200.0f); } if (gCurrentLevel == LEVEL_AQUAS) { gWaterLevel = 1000.0f; diff --git a/src/overlays/ovl_i5/fox_ground.c b/src/overlays/ovl_i5/fox_ground.c index 1e761284e..19622d13a 100644 --- a/src/overlays/ovl_i5/fox_ground.c +++ b/src/overlays/ovl_i5/fox_ground.c @@ -9,15 +9,15 @@ void Ground_801B68A8(Gfx** dlist, s32 arg1, s32 arg2); typedef struct { /* 0x00 */ s32 unk_00; - /* 0x04 */ s32 unk_04; - /* 0x08 */ f32 unk_08; - /* 0x0C */ f32 unk_0C; - /* 0x10 */ f32 unk_10; - /* 0x14 */ f32 unk_14; - /* 0x18 */ f32 unk_18; + /* 0x04 */ s32 type; + /* 0x08 */ f32 xPos; + /* 0x0C */ f32 zPos; + /* 0x10 */ f32 xRot; + /* 0x14 */ f32 yRot; + /* 0x18 */ f32 zRot; /* 0x1C */ s16 unk_1C; /* 0x20 */ f32 unk_20; -} UnkStruct_801C62E8; // size = 0x24 +} GroundObj; // size = 0x24 typedef union { f32 a[3]; @@ -64,79 +64,80 @@ s32 D_i5_801C5C04; s32 D_i5_801C5C08; s32 D_i5_801C5C0C; f32 D_i5_801C5C10; -s32 D_i5_801C5C14; -Mtx D_i5_801C5C18[27]; +s32 sGroundMode; +Mtx sGroundMtx[27]; Vec3f D_i5_801C62D8; -UnkStruct_801C62E8 D_i5_801C62E8[20]; +GroundObj sGroundList[20]; Vec3fa D_i5_801C65B8[3][15][4]; s32 D_i5_801C6E28[4][3]; -bool Ground_801B49D0(Actor* actor) { +bool Ground_List_Setup(TiTerrain* actor) { s32 i; bool found = false; - UnkStruct_801C62E8* var_v0 = D_i5_801C62E8; + GroundObj* ground = sGroundList; - for (i = 0; i < ARRAY_COUNT(D_i5_801C62E8); i++, var_v0++) { - if (var_v0->unk_00 == 0) { + for (i = 0; i < ARRAY_COUNT(sGroundList); i++, ground++) { + if (ground->unk_00 == 0) { found = true; - var_v0->unk_00 = 1; - var_v0->unk_04 = actor->obj.pos.y; - var_v0->unk_08 = actor->obj.pos.x; - var_v0->unk_0C = actor->obj.pos.z; - var_v0->unk_10 = actor->obj.rot.x; - var_v0->unk_14 = actor->obj.rot.y; - var_v0->unk_18 = actor->obj.rot.z; + ground->unk_00 = 1; + ground->type = actor->obj.pos.y; // ??? + ground->xPos = actor->obj.pos.x; + ground->zPos = actor->obj.pos.z; + ground->xRot = actor->obj.rot.x; + ground->yRot = actor->obj.rot.y; + ground->zRot = actor->obj.rot.z; break; } } - actor->obj.status = 0; + actor->obj.status = OBJ_FREE; + return found; } -void Ground_801B4A54(UnkStruct_801C62E8* arg0) { +void Ground_GroundObj_Clear(GroundObj* arg0) { // Lots of conversions between floats, and ints unnecessarily here because of this chain assignment. - arg0->unk_00 = arg0->unk_04 = arg0->unk_08 = arg0->unk_0C = arg0->unk_10 = arg0->unk_14 = arg0->unk_18 = - arg0->unk_1C = arg0->unk_20 = 0; + arg0->unk_00 = arg0->type = arg0->xPos = arg0->zPos = arg0->xRot = arg0->yRot = arg0->zRot = arg0->unk_1C = + arg0->unk_20 = 0; } void Ground_801B4AA8(s32* arg0, s32* arg1) { Actor actor; - UnkStruct_801C62E8* var_s2 = D_i5_801C62E8; + GroundObj* ground = sGroundList; s32* var_s1; f32 temp_fa0; f32 temp_fs0; f32 temp_fs1; f32 var_fv0; f32 var_fv1; - s32 unk_04; - f32 unk_08; - f32 unk_10; - f32 unk_14; - f32 unk_18; + s32 type; + f32 xPos; + f32 xRot; + f32 yRot; + f32 zRot; s32 i; s32 j; - for (i = 0; i < ARRAY_COUNT(D_i5_801C62E8); i++, var_s2++) { - if (var_s2->unk_00 != 0) { - unk_04 = var_s2->unk_04; - unk_08 = var_s2->unk_08; - unk_10 = var_s2->unk_10; - unk_14 = var_s2->unk_14; - unk_18 = var_s2->unk_18; + for (i = 0; i < ARRAY_COUNT(sGroundList); i++, ground++) { + if (ground->unk_00 != 0) { + type = ground->type; + xPos = ground->xPos; + xRot = ground->xRot; + yRot = ground->yRot; + zRot = ground->zRot; var_s1 = arg0; if (arg0 == NULL) { - unk_04 = D_i5_801BA970[unk_04]; + type = D_i5_801BA970[type]; } - if (var_s2->unk_00 == 1) { - switch (unk_04) { + if (ground->unk_00 == 1) { + switch (type) { case 0: break; case 1: case 3: - var_s2->unk_20 = var_s2->unk_18; + ground->unk_20 = ground->zRot; break; case 4: @@ -161,109 +162,107 @@ void Ground_801B4AA8(s32* arg0, s32* arg1) { case 8: PRINTF("RANDAMU YAMA\n"); // RANDOM MOUNTAIN - var_s2->unk_20 = 5000.0f; + ground->unk_20 = 5000.0f; break; } - var_s2->unk_00 = 2; + ground->unk_00 = 2; } - switch (unk_04) { + switch (type) { case 0: break; case 1: for (j = 0; j < 16; j++, var_s1++) { - temp_fs0 = (j * 220.0f * D_i5_801BE740) - 1760.0f - unk_08; - if (fabsf(temp_fs0) <= unk_10) { - temp_fs1 = __sinf((var_s2->unk_20 / unk_18) * (M_DTOR * 180.0f)); - var_fv0 = __cosf((temp_fs0 / unk_10) * (M_DTOR * 90.0f)); - *var_s1 += var_fv0 * unk_14 * temp_fs1; + temp_fs0 = (j * 220.0f * D_i5_801BE740) - 1760.0f - xPos; + if (fabsf(temp_fs0) <= xRot) { + temp_fs1 = __sinf((ground->unk_20 / zRot) * (M_DTOR * 180.0f)); + var_fv0 = __cosf((temp_fs0 / xRot) * (M_DTOR * 90.0f)); + *var_s1 += var_fv0 * yRot * temp_fs1; } } - var_s2->unk_20 -= 220.0f; - if (var_s2->unk_20 <= 0.0f) { - Ground_801B4A54(var_s2); + ground->unk_20 -= 220.0f; + if (ground->unk_20 <= 0.0f) { + Ground_GroundObj_Clear(ground); } break; case 2: - unk_08 += (1760.0f - (unk_10 * 0.5f)); - var_fv1 = unk_14; + xPos += (1760.0f - (xRot * 0.5f)); + var_fv1 = yRot; - if (var_s2->unk_20 <= var_fv1 / (70.0f * M_DTOR)) { - var_fv1 = var_s2->unk_20 * (70.0f * M_DTOR); - } else if (var_fv1 / (70.0f * M_DTOR) >= (unk_18 - var_s2->unk_20)) { - var_fv1 = (unk_18 - var_s2->unk_20) * (70.0f * M_DTOR); + if (ground->unk_20 <= var_fv1 / (70.0f * M_DTOR)) { + var_fv1 = ground->unk_20 * (70.0f * M_DTOR); + } else if (var_fv1 / (70.0f * M_DTOR) >= (zRot - ground->unk_20)) { + var_fv1 = (zRot - ground->unk_20) * (70.0f * M_DTOR); } for (j = 0, var_fv0 = 0.0f; j < 16; j++, var_fv0 += 220.0f, var_s1++) { - if (unk_08 <= var_fv0 && var_fv0 <= (unk_08 + unk_10) && *var_s1 < var_fv1) { + if (xPos <= var_fv0 && var_fv0 <= (xPos + xRot) && *var_s1 < var_fv1) { *var_s1 = var_fv1; } } - var_s2->unk_20 += 220.0f; - if (unk_18 <= var_s2->unk_20) { - Ground_801B4A54(var_s2); + ground->unk_20 += 220.0f; + if (zRot <= ground->unk_20) { + Ground_GroundObj_Clear(ground); } break; case 3: for (j = 0; j < 16; j++, var_s1++) { - temp_fs0 = - (j * 220.0f * D_i5_801BE740) - 1760.0f - var_s2->unk_08 + - (__sinf(((var_s2->unk_20 * 8.0f) / var_s2->unk_18) * 2 * (M_DTOR * 180.0f)) * 500.0f); - if (fabsf(temp_fs0) <= var_s2->unk_10) { - temp_fa0 = __cosf((temp_fs0 / var_s2->unk_10) * (M_DTOR * 90.0f)); + temp_fs0 = (j * 220.0f * D_i5_801BE740) - 1760.0f - ground->xPos + + (__sinf(((ground->unk_20 * 8.0f) / ground->zRot) * 2 * (M_DTOR * 180.0f)) * 500.0f); + if (fabsf(temp_fs0) <= ground->xRot) { + temp_fa0 = __cosf((temp_fs0 / ground->xRot) * (M_DTOR * 90.0f)); if (temp_fa0 >= 0.7) { temp_fa0 = 0.7f; } *var_s1 -= - temp_fa0 * var_s2->unk_14 * 4.0f * ((var_s2->unk_18 - var_s2->unk_20) / var_s2->unk_18); + temp_fa0 * ground->yRot * 4.0f * ((ground->zRot - ground->unk_20) / ground->zRot); } - *var_s1 += - (var_s2->unk_14 - ((var_s2->unk_20 / var_s2->unk_18) * var_s2->unk_14)) * 4.0f * 0.7f; + *var_s1 += (ground->yRot - ((ground->unk_20 / ground->zRot) * ground->yRot)) * 4.0f * 0.7f; } - var_s2->unk_20 -= 220.0f; - if (var_s2->unk_20 <= 0.0f) { - Ground_801B4A54(var_s2); + ground->unk_20 -= 220.0f; + if (ground->unk_20 <= 0.0f) { + Ground_GroundObj_Clear(ground); } break; case 8: - if (var_s2->unk_1C <= 0) { - for (j = 0; j < (s32) var_s2->unk_18; j++) { - var_s2->unk_1C = 1; + if (ground->unk_1C <= 0) { + for (j = 0; j < (s32) ground->zRot; j++) { + ground->unk_1C = 1; actor.obj.pos.x = RAND_FLOAT(3300.0f) - 1650.0f; actor.obj.pos.z = 0.0f; actor.obj.pos.y = 1.0f; - actor.obj.rot.x = var_s2->unk_10 + RAND_FLOAT(var_s2->unk_10) * 0.25f; + actor.obj.rot.x = ground->xRot + RAND_FLOAT(ground->xRot) * 0.25f; actor.obj.rot.z = 2.0f * actor.obj.rot.x; // Should this have been obj.rot.y? - actor.obj.rot.z = var_s2->unk_14 + RAND_FLOAT(var_s2->unk_14) * 0.25f; - Ground_801B49D0(&actor); + actor.obj.rot.z = ground->yRot + RAND_FLOAT(ground->yRot) * 0.25f; + Ground_List_Setup(&actor); } - var_s2->unk_20 -= 220.0f; - if (var_s2->unk_20 <= 0.0f) { - Ground_801B4A54(var_s2); + ground->unk_20 -= 220.0f; + if (ground->unk_20 <= 0.0f) { + Ground_GroundObj_Clear(ground); } } else { - var_s2->unk_1C--; + ground->unk_1C--; } break; default: - Ground_801B4A54(var_s2); + Ground_GroundObj_Clear(ground); break; } } } } -void Ground_801B5110(f32 x, f32 y, f32 z) { - UnkStruct_801C62E8* ptr; +void Ground_Init(f32 x, f32 y, f32 z) { + GroundObj* ptr; s32 i; s32 k; @@ -277,10 +276,10 @@ void Ground_801B5110(f32 x, f32 y, f32 z) { D_i5_801C62D8.x = x; D_i5_801C62D8.y = y; D_i5_801C62D8.z = z; - D_i5_801C5C14 = 1; + sGroundMode = 1; - for (i = 0, ptr = D_i5_801C62E8; i < ARRAY_COUNT(D_i5_801C62E8); i++, ptr++) { - Ground_801B4A54(ptr); + for (i = 0, ptr = sGroundList; i < ARRAY_COUNT(sGroundList); i++, ptr++) { + Ground_GroundObj_Clear(ptr); } for (i = 0; i < ARRAY_COUNT(D_i5_801C1D48); i++) { @@ -350,8 +349,8 @@ void Ground_801B5244(s32 arg0, s32 arg1) { } } Matrix_Translate(gGfxMatrix, 0.0f, 0.0f, D_i5_801C24B8[sp5C] * -220.0f, MTXF_NEW); - Matrix_ToMtx(&D_i5_801C5C18[sp60]); - gSPMatrix(D_i5_801C5C00++, &D_i5_801C5C18[sp60], G_MTX_NOPUSH | G_MTX_MUL | G_MTX_MODELVIEW); + Matrix_ToMtx(&sGroundMtx[sp60]); + gSPMatrix(D_i5_801C5C00++, &sGroundMtx[sp60], G_MTX_NOPUSH | G_MTX_MUL | G_MTX_MODELVIEW); gSPEndDisplayList(D_i5_801C5C00++); sp5C = (sp5C + 1) % 28; sp60 = (sp60 + 1) % 27; @@ -369,10 +368,13 @@ void Ground_801B58AC(Gfx** dList, f32 arg1) { RCP_SetupDL(dList, 0x1D); RCP_SetFog(dList, gFogRed, gFogGreen, gFogBlue, gFogAlpha, gFogNear, gFogFar); - spC4 = D_i5_801C5C14; + spC4 = sGroundMode; + + // ONE PLANE POLYGON + if (sGroundMode & 2) { + // @port Skip interpolation + FrameInterpolation_ShouldInterpolateFrame(false); - if (D_i5_801C5C14 & 2) { - FrameInterpolation_RecordOpenChild(dList, D_i5_801C5C10); gDPSetupTile((*dList)++, G_IM_FMT_RGBA, G_IM_SIZ_16b, 32, 32, 0, 0, G_TX_MIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, 5, 5, G_TX_NOLOD, G_TX_NOLOD); gDPLoadTileTexture((*dList)++, D_TI_6001BA8, G_IM_FMT_RGBA, G_IM_SIZ_16b, 32, 32); @@ -383,10 +385,16 @@ void Ground_801B58AC(Gfx** dList, f32 arg1) { gSPDisplayList((*dList)++, D_i5_801BA950); gSPPopMatrix((*dList)++, G_MTX_MODELVIEW); Ground_801B4AA8(NULL, &spC4); - FrameInterpolation_RecordCloseChild(); + + // @port renable interpolation + FrameInterpolation_ShouldInterpolateFrame(true); } - if (D_i5_801C5C14 & 1) { + // TERRAIN POLYGON + if (sGroundMode & 1) { + // @port Skip interpolation + FrameInterpolation_ShouldInterpolateFrame(false); + if (D_i5_801C5C0C == 1) { D_i5_801C5C0C = 0; temp_hi = (D_i5_801C5C08 + 27) % 28; @@ -421,29 +429,33 @@ void Ground_801B58AC(Gfx** dList, f32 arg1) { D_i5_801BE748[(D_i5_801C5C04 + 27) % 27][i][1].v.ob[2] = 0; Matrix_Translate(gGfxMatrix, 0.0f, 0.0f, D_i5_801C24B8[D_i5_801C5C08] * -220.0f, MTXF_NEW); - Matrix_ToMtx(&D_i5_801C5C18[D_i5_801C5C04]); + Matrix_ToMtx(&sGroundMtx[D_i5_801C5C04]); } Ground_801B5FE0(D_i5_801C5C08, D_i5_801C5C04, 1); } + gSPMatrix((*dList)++, &gIdentityMtx, G_MTX_PUSH | G_MTX_MUL | G_MTX_MODELVIEW); Ground_801B68A8(dList, D_i5_801C5C04, 1); gSPPopMatrix((*dList)++, G_MTX_MODELVIEW); + + // @port renable interpolation + FrameInterpolation_ShouldInterpolateFrame(true); } D_i5_801C5C10 += arg1; - if ((D_i5_801C5C14 & 1) && (D_i5_801C24B8[(D_i5_801C5C08 + 25) % 28] * 220.0f <= D_i5_801C5C10)) { + if ((sGroundMode & 1) && (D_i5_801C24B8[(D_i5_801C5C08 + 25) % 28] * 220.0f <= D_i5_801C5C10)) { D_i5_801C5C0C = 1; D_i5_801C5C10 = Math_ModF(D_i5_801C5C10, D_i5_801C24B8[(D_i5_801C5C08 + 25) % 28] * 220.0f); D_i5_801C5C04 = (D_i5_801C5C04 + 26) % 27; D_i5_801C5C08 = (D_i5_801C5C08 + 27) % 28; } - if (D_i5_801C5C14 & 2) { + if (sGroundMode & 2) { D_i5_801C5C10 = Math_ModF(D_i5_801C5C10, 220.0f * D_i5_801BE744); } - D_i5_801C5C14 = spC4; + sGroundMode = spC4; } void Ground_801B5FE0(s32 arg0, s32 arg1, s32 arg2) { @@ -565,10 +577,12 @@ void Ground_801B68A8(Gfx** dlist, s32 arg1, s32 arg2) { s32 j; s32 var; + // @port Skip interpolation + FrameInterpolation_ShouldInterpolateFrame(false); + gDPSetupTile((*dlist)++, G_IM_FMT_RGBA, G_IM_SIZ_16b, 32, 32, 0, 0, G_TX_MIRROR | G_TX_WRAP, G_TX_MIRROR | G_TX_WRAP, 5, 5, G_TX_NOLOD, G_TX_NOLOD); gDPLoadTileTexture((*dlist)++, D_TI_6001BA8, G_IM_FMT_RGBA, G_IM_SIZ_16b, 32, 32); - FrameInterpolation_RecordOpenChild(dlist, D_i5_801C5C10); Matrix_Translate(gGfxMatrix, D_i5_801C62D8.x, D_i5_801C62D8.y, D_i5_801C62D8.z + D_i5_801C5C10, MTXF_NEW); Matrix_ToMtx(gGfxMtx); gSPMatrix((*dlist)++, gGfxMtx++, G_MTX_NOPUSH | G_MTX_MUL | G_MTX_MODELVIEW); @@ -579,7 +593,9 @@ void Ground_801B68A8(Gfx** dlist, s32 arg1, s32 arg2) { gSPDisplayList((*dlist)++, &D_i5_801C2528[j]); j = (j + 26) % 27; } - FrameInterpolation_RecordCloseChild(); + + // @port renable interpolation + FrameInterpolation_ShouldInterpolateFrame(true); } bool Ground_801B6AEC(f32 arg0, f32 arg1, f32 arg2) { @@ -694,7 +710,7 @@ s32 Ground_801B6E20(f32 arg0, f32 arg1, f32* arg2, f32* arg3, f32* arg4) { *arg4 = 0.0f; *arg2 = 0.0f; - if (D_i5_801C5C14 & 1) { + if (sGroundMode & 1) { temp_fs2 = arg1 - D_i5_801C5C10 - D_i5_801C62D8.z; var_v1 = 0; var_s6 = (D_i5_801C5C04 + 25) % 27; diff --git a/src/overlays/ovl_i5/fox_ti.c b/src/overlays/ovl_i5/fox_ti.c index f484ce3b8..30cf25c9d 100644 --- a/src/overlays/ovl_i5/fox_ti.c +++ b/src/overlays/ovl_i5/fox_ti.c @@ -121,7 +121,7 @@ void Titania_Effect368_Spawn(f32 xPos, f32 yPos, f32 zPos, f32 yRot, f32 scale2) } void Titania_TiTerrain_Update(TiTerrain* this) { - Ground_801B49D0(this); + Ground_List_Setup(this); } Vec3f D_i5_801B7520 = { -150.0f, 0.0f, 0.0f }; From a69e9659393f88dcca22ff9d563ee4e9e0019d33 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Thu, 12 Mar 2026 20:01:06 -0300 Subject: [PATCH 42/73] fix function name --- include/i5.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/i5.h b/include/i5.h index ea4c4b94e..af06200cc 100644 --- a/include/i5.h +++ b/include/i5.h @@ -49,7 +49,7 @@ void Macbeth_MaProximityLight_Init(Scenery *); void Macbeth_Actor207_Init(Actor *); void Macbeth_801ACFBC(void); void Macbeth_801AD080(void); -void Ground_801B5110(f32, f32, f32); +void Ground_Init(f32, f32, f32); void Ground_801B58AC(Gfx **, f32); bool Ground_801B6AEC(f32, f32, f32); s32 Ground_801B6E20(f32, f32, f32 *, f32 *, f32 *); From 1bb941bcb1786d3ebf6d6349b4d35200a8328573 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Thu, 12 Mar 2026 21:49:26 -0300 Subject: [PATCH 43/73] Fix CI try --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2e6fc63ff..89c2dc2be 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -181,7 +181,7 @@ jobs: cmake -H. -Bbuild-cmake -GNinja -DCMAKE_BUILD_TYPE=Release cmake --build build-cmake --config Release -j3 (cd build-cmake && cpack -G External) - wget -O gamecontrollerdb.txt https://github.com/mdqinc/SDL_GameControllerDB/blob/master/gamecontrollerdb.txt + wget -O gamecontrollerdb.txt https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt mv README.md readme.txt mv build-cmake/*.appimage starship.appimage - name: Upload build @@ -215,7 +215,7 @@ jobs: sudo sh /tmp/cmake.sh --prefix=/usr/local/ --exclude-subdir cmake -H. -Bbuild-switch -GNinja -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/Switch.cmake cmake --build build-switch --config Release -j3 - wget -O gamecontrollerdb.txt https://github.com/mdqinc/SDL_GameControllerDB/blob/master/gamecontrollerdb.txt + wget -O gamecontrollerdb.txt https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt mv README.md readme.txt mv build-switch/*.nro Starship.nro - name: Upload build From 3d882079075d7725d6c16049017149c4f2c0c531 Mon Sep 17 00:00:00 2001 From: Garrett Cox Date: Fri, 13 Mar 2026 11:09:47 -0500 Subject: [PATCH 44/73] Fix macOS CI (#243) * Fix macOS CI * fix switch * Fix readme links --- .github/workflows/mac.yml | 20 ++++------- .github/workflows/main.yml | 62 ++++++++++++++++++++++++++------- .github/workflows/switch.yml | 2 +- Brewfile | 8 +++++ CMakeLists.txt | 14 ++++++++ Info.plist | 5 +++ README.md | 3 +- cmake/configure-packaging.cmake | 2 +- libultraship | 2 +- 9 files changed, 89 insertions(+), 29 deletions(-) create mode 100644 Brewfile diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index f70c52e23..27a0a5aca 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -6,24 +6,18 @@ on: jobs: build: - runs-on: macOS-latest + runs-on: macOS-15 steps: - uses: actions/checkout@v4 with: submodules: recursive + - uses: actions/cache@v4 + with: + path: ~/Library/Caches/Homebrew + key: ${{ runner.os }}-${{ runner.arch }}-homebrew-${{ github.ref }}-${{ github.sha }} - name: Install dependencies - run: brew install sdl2 libpng glew ninja cmake libzip nlohmann-json tinyxml2 spdlog vorbis-tools + run: HOMEBREW_NO_AUTO_UPDATE=1 brew bundle --no-upgrade - name: Build run: | cmake -H. -Bbuild-cmake -GNinja -DCMAKE_BUILD_TYPE=Release - cmake --build build-cmake -j - - name: Create Package - run: | - mkdir starship-release - mv build-cmake/Starship starship-release/ - - name: Publish packaged artifacts - uses: actions/upload-artifact@v4 - with: - name: starship-mac-x64 - path: starship-release - retention-days: 1 \ No newline at end of file + cmake --build build-cmake -j \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 89c2dc2be..2df69ed5e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -73,19 +73,23 @@ jobs: name: starship-windows path: starship-release - build-macos: + build-macos-x64: needs: generate-port-o2r - runs-on: macOS-latest + runs-on: macos-15-intel steps: - uses: actions/checkout@v4 with: submodules: recursive + - uses: actions/cache@v4 + with: + path: ~/Library/Caches/Homebrew + key: ${{ runner.os }}-${{ runner.arch }}-homebrew-${{ github.ref }}-${{ github.sha }} - name: Install dependencies - run: brew install sdl2 libpng glew ninja cmake libzip nlohmann-json tinyxml2 spdlog vorbis-tools + run: HOMEBREW_NO_AUTO_UPDATE=1 brew bundle --no-upgrade - name: Build run: | cmake -H. -Bbuild-cmake -GNinja -DCMAKE_BUILD_TYPE=Release - cmake --build build-cmake --config Release -j3 + cmake --build build-cmake -j - name: Download starship.o2r uses: actions/download-artifact@v4 with: @@ -93,17 +97,51 @@ jobs: path: ./build-cmake - name: Create Package run: | - mkdir starship-release - mv build-cmake/Starship starship-release/ - mv build-cmake/starship.o2r starship-release/ - mv config.yml starship-release/ - mv assets starship-release/ - curl -o starship-release/gamecontrollerdb.txt -sSL 'https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt' + (cd build-cmake && cpack) + mv _packages/*.dmg Starship.dmg + mv README.md readme.txt + - name: Publish packaged artifacts + uses: actions/upload-artifact@v4 + with: + name: Starship-mac-x64 + path: | + Starship.dmg + readme.txt + + build-macos-arm64: + needs: generate-port-o2r + runs-on: macos-15 + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: actions/cache@v4 + with: + path: ~/Library/Caches/Homebrew + key: ${{ runner.os }}-${{ runner.arch }}-homebrew-${{ github.ref }}-${{ github.sha }} + - name: Install dependencies + run: HOMEBREW_NO_AUTO_UPDATE=1 brew bundle --no-upgrade + - name: Build + run: | + cmake -H. -Bbuild-cmake -GNinja -DCMAKE_BUILD_TYPE=Release + cmake --build build-cmake -j + - name: Download starship.o2r + uses: actions/download-artifact@v4 + with: + name: starship.o2r + path: ./build-cmake + - name: Create Package + run: | + (cd build-cmake && cpack) + mv _packages/*.dmg Starship.dmg + mv README.md readme.txt - name: Publish packaged artifacts uses: actions/upload-artifact@v4 with: - name: starship-mac-x64 - path: starship-release + name: Starship-mac-arm64 + path: | + Starship.dmg + readme.txt build-linux: needs: generate-port-o2r diff --git a/.github/workflows/switch.yml b/.github/workflows/switch.yml index 5fe784c37..784bfb058 100644 --- a/.github/workflows/switch.yml +++ b/.github/workflows/switch.yml @@ -25,7 +25,7 @@ jobs: sudo sh /tmp/cmake.sh --prefix=/usr/local/ --exclude-subdir cmake -H. -Bbuild-switch -GNinja -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/Switch.cmake cmake --build build-switch --config Release -j3 - wget -O gamecontrollerdb.txt https://github.com/mdqinc/SDL_GameControllerDB/blob/master/gamecontrollerdb.txt + wget -O gamecontrollerdb.txt https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt mv README.md readme.txt mv build-switch/*.nro Starship.nro - name: Upload build diff --git a/Brewfile b/Brewfile new file mode 100644 index 000000000..ce9675fae --- /dev/null +++ b/Brewfile @@ -0,0 +1,8 @@ +brew "sdl2" +brew "libpng" +brew "glew" +brew "libzip" +brew "nlohmann-json" +brew "tinyxml2" +brew "spdlog" +brew "vorbis-tools" \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index d95644030..e7d0e797a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -633,6 +633,9 @@ add_custom_target( COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/starship.o2r" "${CMAKE_BINARY_DIR}/starship.o2r" ) +find_program(CURL NAMES curl DOC "Path to the curl program. Used to download files.") +execute_process(COMMAND ${CURL} -sSfL https://raw.githubusercontent.com/gabomdq/SDL_GameControllerDB/master/gamecontrollerdb.txt -o ${CMAKE_BINARY_DIR}/gamecontrollerdb.txt OUTPUT_VARIABLE RESULT) + if("${CMAKE_SYSTEM_NAME}" STREQUAL "Linux") install(FILES "${CMAKE_BINARY_DIR}/starship.o2r" DESTINATION . COMPONENT ${PROJECT_NAME}) endif() @@ -656,6 +659,17 @@ add_custom_target(CreateOSXIcons ) add_dependencies(${PROJECT_NAME} CreateOSXIcons) configure_file("${CMAKE_SOURCE_DIR}/Info.plist" "${CMAKE_BINARY_DIR}/Info.plist" COPYONLY) + INSTALL(TARGETS ${PROJECT_NAME} DESTINATION ../MacOS COMPONENT Starship) + INSTALL(FILES ${CMAKE_BINARY_DIR}/gamecontrollerdb.txt DESTINATION ../MacOS COMPONENT Starship) + INSTALL(FILES ${CMAKE_BINARY_DIR}/starship.o2r DESTINATION ../Resources COMPONENT Starship) + INSTALL(FILES ${CMAKE_BINARY_DIR}/config.yml DESTINATION ../Resources COMPONENT Starship) + INSTALL(DIRECTORY ${CMAKE_BINARY_DIR}/assets/ DESTINATION ../Resources/assets COMPONENT Starship) + + # Fix bundle to include and relink all dependencies + install(CODE " + include(BundleUtilities) + fixup_bundle(\"\${CMAKE_INSTALL_PREFIX}/../MacOS/Starship\" \"\" \"${dirs}\") + ") endif() set_property(TARGET ${PROJECT_NAME} PROPERTY APPIMAGE_DESKTOP_FILE_TERMINAL YES) diff --git a/Info.plist b/Info.plist index 39fb6bc13..c0f51ab9f 100644 --- a/Info.plist +++ b/Info.plist @@ -33,6 +33,11 @@ public.app-category.games LSMinimumSystemVersion 10.15 + LSEnvironment + + SHIP_HOME + ~/Library/Application Support/com.Starship.Starship + LSArchitecturePriority arm64 diff --git a/README.md b/README.md index 75c9d0b09..f8690383a 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,8 @@ If you want to manually compile Starship, please consult the [building instructi If you want to playtest a continuous integration build, you can find them at the links below. Keep in mind that these are for playtesting only, and you will likely encounter bugs and possibly crashes. * [Windows](https://nightly.link/HarbourMasters/Starship/workflows/main/main/starship-windows.zip) -* [macOS](https://nightly.link/HarbourMasters/Starship/workflows/main/main/starship-mac-x64.zip) +* [macOS (Apple Silicon)](https://nightly.link/HarbourMasters/Starship/workflows/main/main/Starship-mac-arm64.zip) +* [macOS (Intel)](https://nightly.link/HarbourMasters/Starship/workflows/main/main/Starship-mac-x64.zip) * [Linux](https://nightly.link/HarbourMasters/Starship/workflows/main/main/Starship-linux.zip) * [Switch](https://nightly.link/HarbourMasters/Starship/workflows/main/main/Starship-switch.zip) diff --git a/cmake/configure-packaging.cmake b/cmake/configure-packaging.cmake index f3d2c115a..8fb030b10 100644 --- a/cmake/configure-packaging.cmake +++ b/cmake/configure-packaging.cmake @@ -22,7 +22,7 @@ endif() if (CPACK_GENERATOR MATCHES "Bundle") set(CPACK_BUNDLE_NAME "Starship") - set(CPACK_BUNDLE_PLIST "macosx/Info.plist") + set(CPACK_BUNDLE_PLIST "Info.plist") set(CPACK_BUNDLE_ICON "macosx/Starship.icns") # set(CPACK_BUNDLE_STARTUP_COMMAND "macosx/Starship-macos.sh") set(CPACK_BUNDLE_APPLE_CERT_APP "-") diff --git a/libultraship b/libultraship index 09dfab5fb..eaaf9d0fc 160000 --- a/libultraship +++ b/libultraship @@ -1 +1 @@ -Subproject commit 09dfab5fb2a9a047a6e268dc9db2daad9b2ce5f0 +Subproject commit eaaf9d0fc91e2c400f49ef2a1f8547a691ce4d3c From 10ec999591f5fb680214acca5cb045eeb9373e3f Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Tue, 17 Mar 2026 22:58:02 -0300 Subject: [PATCH 45/73] Solar Intro Cutscene Vi Recording --- include/fox_record.h | 1 + src/engine/cutscene_record.c | 36 +++++++++++++++++++++++------------- src/engine/fox_demo.c | 2 ++ 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/include/fox_record.h b/include/fox_record.h index 6077c6bd2..6d9cec6e7 100644 --- a/include/fox_record.h +++ b/include/fox_record.h @@ -24,6 +24,7 @@ extern Record gMacbethCutsceneRecord[14]; extern Record gGrangaCutsceneRecord[13]; extern Record gMeCrusherCutsceneRecord[3]; extern Record gEndingCsRecord[37]; +extern Record gSolarIntroCsRecord[16]; extern int gA6GorgonCsFrameCount; extern int gWarpzoneCsFrameCount; diff --git a/src/engine/cutscene_record.c b/src/engine/cutscene_record.c index 611c2bbef..3bf8a5120 100644 --- a/src/engine/cutscene_record.c +++ b/src/engine/cutscene_record.c @@ -18,6 +18,8 @@ void UpdateVisPerFrameFromRecording(Record* record, s32 maxFrames, int* frameCou int gWarpzoneCsFrameCount = 0; int gA6GorgonCsFrameCount = 0; +// clang-format off + Record gWarpzoneCsRecord[19] = { { 2, 1 }, { 3, 3 }, { 4, 4 }, { 3, 8 }, { 2, 9 }, { 3, 52 }, { 2, 54 }, { 3, 69 }, { 2, 70 }, { 3, 75 }, { 2, 76 }, { 3, 79 }, { 2, 117 }, { 3, 118 }, { 2, 120 }, { 3, 145 }, { 2, 215 }, { 3, 216 }, { 2, 230 }, @@ -100,15 +102,7 @@ Record gMacbethCutsceneRecord[14] = { { 2, 942 }, }; -// clang-format off - // After defeating Andross -/* -03 00 00 00 02 00 00 08 03 00 00 13 02 00 00 23 03 00 00 9C 02 00 00 A6 03 00 00 C1 04 00 00 C6 03 00 00 C7 04 00 00 C9 -03 00 00 CD 04 00 00 CF 03 00 00 D7 04 00 00 EA 03 00 00 EC 04 00 00 ED 03 00 00 EF 04 00 00 F4 03 00 00 F5 04 00 00 F6 -03 00 01 10 04 00 01 12 03 00 01 22 04 00 01 24 03 00 01 27 04 00 01 2D 03 00 01 2E 04 00 01 30 03 00 01 31 04 00 01 33 -03 00 01 4C 04 00 01 4E 03 00 01 4F -*/ Record gAndrossRobotKillCutscene1[25] = { { 3, 0 }, { 2, 8 }, @@ -146,10 +140,6 @@ Record gAndrossRobotKillCutscene1[25] = { { 2, 339 }, }; -/* -04 00 00 00 02 00 00 01 05 00 00 02 02 00 00 03 03 00 00 2A 02 00 00 2B 03 00 00 39 02 00 00 3A 03 00 00 3F 02 00 00 45 -03 00 00 9E 04 00 00 A1 05 00 00 A3 04 00 00 B6 03 00 00 B9 04 00 00 BD 05 00 00 C1 04 00 00 CD 03 00 00 CF 02 00 00 D3 -*/ Record gAndrossRobotKillCutscene2[20] = { { 4, 0 }, { 2, 1 }, @@ -172,7 +162,6 @@ Record gAndrossRobotKillCutscene2[20] = { { 3, 207 }, { 2, 211 }, }; -// clang-format on // SyRobot destroy cutscene timings recorded from a real N64 Record gSyRobotCutsceneRecord[3] = { @@ -180,3 +169,24 @@ Record gSyRobotCutsceneRecord[3] = { { 3, 129 }, { 2, 230 }, }; + +Record gSolarIntroCsRecord[16] = { + { 2, 0 }, + { 3, 94 }, + { 2, 105 }, + { 3, 362 }, + { 4, 377 }, + { 3, 379 }, + { 4, 381 }, + { 2, 382 }, + { 3, 443 }, + { 2, 444 }, + { 3, 456 }, + { 2, 492 }, + { 3, 494 }, + { 2, 499 }, + { 3, 552 }, + { 2, 629 }, +}; + +// clang-format on diff --git a/src/engine/fox_demo.c b/src/engine/fox_demo.c index ce091ef0b..dfdacc778 100644 --- a/src/engine/fox_demo.c +++ b/src/engine/fox_demo.c @@ -630,6 +630,8 @@ void Cutscene_LevelStart(Player* player) { break; case LEVEL_SOLAR: + // @Port: Vi recording + UpdateVisPerFrameFromRecording(gSolarIntroCsRecord, ARRAY_COUNT(gSolarIntroCsRecord), &gCsFrameCount); Solar_LevelStart(player); break; From 5050031c1e8b5be9e7061da8498a32c3c06c7436 Mon Sep 17 00:00:00 2001 From: Garrett Cox Date: Thu, 19 Mar 2026 13:33:55 -0500 Subject: [PATCH 46/73] Try to use macos-14 (#244) --- .github/workflows/mac.yml | 2 +- .github/workflows/main.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index 27a0a5aca..d7471c189 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -6,7 +6,7 @@ on: jobs: build: - runs-on: macOS-15 + runs-on: macos-14 steps: - uses: actions/checkout@v4 with: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2df69ed5e..e35cc68eb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -75,7 +75,7 @@ jobs: build-macos-x64: needs: generate-port-o2r - runs-on: macos-15-intel + runs-on: macos-14-large # macos-14-large is the only way we can get x64 steps: - uses: actions/checkout@v4 with: @@ -110,7 +110,7 @@ jobs: build-macos-arm64: needs: generate-port-o2r - runs-on: macos-15 + runs-on: macos-14 steps: - uses: actions/checkout@v4 with: From 0b90f2620e565a622f6f2b1256022d4c12fcea50 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Fri, 20 Mar 2026 14:38:36 -0300 Subject: [PATCH 47/73] Corneria_CoCarrier_Init Correction --- src/overlays/ovl_i1/fox_co.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/overlays/ovl_i1/fox_co.c b/src/overlays/ovl_i1/fox_co.c index e8bbbea71..9d068d5a2 100644 --- a/src/overlays/ovl_i1/fox_co.c +++ b/src/overlays/ovl_i1/fox_co.c @@ -1690,7 +1690,7 @@ void Corneria_CoCarrier_Init(CoCarrier* this) { timer = this->timer_05A; // OBJ_BOSS_CO_CARRIER_LEFT to OBJ_BOSS_CO_CARRIER_BOTTOM - for (i = CARRIER_LEFT; i < ARRAY_COUNT(gBosses); i++) { + for (i = CARRIER_LEFT; i <= CARRIER_BOTTOM; i++) { Boss_Initialize(&gBosses[i]); gBosses[i].obj.status = OBJ_INIT; gBosses[i].obj.id = (i - 1) + OBJ_BOSS_CO_CARRIER_LEFT; From 3e3e7b8af9eac176f13f1aebef9a62e0e7971237 Mon Sep 17 00:00:00 2001 From: Garrett Cox Date: Fri, 20 Mar 2026 18:02:12 -0500 Subject: [PATCH 48/73] MacOS CI Universal Build (#246) * Another attempt * Use macports --- .github/macports.yml | 21 +++++++++++++++ .github/workflows/mac.yml | 14 +++++----- .github/workflows/main.yml | 53 +++++++------------------------------- README.md | 3 +-- 4 files changed, 38 insertions(+), 53 deletions(-) create mode 100644 .github/macports.yml diff --git a/.github/macports.yml b/.github/macports.yml new file mode 100644 index 000000000..976224723 --- /dev/null +++ b/.github/macports.yml @@ -0,0 +1,21 @@ +ports: + - name: libsdl2 + select: [ universal ] + - name: libpng + select: [ universal ] + - name: glew + select: [ universal ] + - name: libzip + select: [ universal ] + - name: nlohmann-json + select: [ universal ] + - name: tinyxml2 + select: [ universal ] + - name: libogg + select: [ universal ] + - name: libopus + select: [ universal ] + - name: opusfile + select: [ universal ] + - name: libvorbis + select: [ universal ] \ No newline at end of file diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index d7471c189..266a808a9 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -8,16 +8,16 @@ jobs: build: runs-on: macos-14 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: recursive - - uses: actions/cache@v4 + - uses: melusina-org/setup-macports@v1 with: - path: ~/Library/Caches/Homebrew - key: ${{ runner.os }}-${{ runner.arch }}-homebrew-${{ github.ref }}-${{ github.sha }} - - name: Install dependencies - run: HOMEBREW_NO_AUTO_UPDATE=1 brew bundle --no-upgrade + parameters: '.github/macports.yml' + - uses: hendrikmuhs/ccache-action@v1.2 + with: + key: macos-ccache-${{ github.ref }} - name: Build run: | - cmake -H. -Bbuild-cmake -GNinja -DCMAKE_BUILD_TYPE=Release + cmake -H. -Bbuild-cmake -GNinja -DCMAKE_BUILD_TYPE=Release -D CMAKE_C_COMPILER_LAUNCHER=ccache -D CMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" cmake --build build-cmake -j \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e35cc68eb..41c68a741 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -73,57 +73,22 @@ jobs: name: starship-windows path: starship-release - build-macos-x64: - needs: generate-port-o2r - runs-on: macos-14-large # macos-14-large is the only way we can get x64 - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - uses: actions/cache@v4 - with: - path: ~/Library/Caches/Homebrew - key: ${{ runner.os }}-${{ runner.arch }}-homebrew-${{ github.ref }}-${{ github.sha }} - - name: Install dependencies - run: HOMEBREW_NO_AUTO_UPDATE=1 brew bundle --no-upgrade - - name: Build - run: | - cmake -H. -Bbuild-cmake -GNinja -DCMAKE_BUILD_TYPE=Release - cmake --build build-cmake -j - - name: Download starship.o2r - uses: actions/download-artifact@v4 - with: - name: starship.o2r - path: ./build-cmake - - name: Create Package - run: | - (cd build-cmake && cpack) - mv _packages/*.dmg Starship.dmg - mv README.md readme.txt - - name: Publish packaged artifacts - uses: actions/upload-artifact@v4 - with: - name: Starship-mac-x64 - path: | - Starship.dmg - readme.txt - - build-macos-arm64: + build-macos: needs: generate-port-o2r runs-on: macos-14 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: recursive - - uses: actions/cache@v4 + - uses: melusina-org/setup-macports@v1 with: - path: ~/Library/Caches/Homebrew - key: ${{ runner.os }}-${{ runner.arch }}-homebrew-${{ github.ref }}-${{ github.sha }} - - name: Install dependencies - run: HOMEBREW_NO_AUTO_UPDATE=1 brew bundle --no-upgrade + parameters: '.github/macports.yml' + - uses: hendrikmuhs/ccache-action@v1.2 + with: + key: macos-ccache-${{ github.ref }} - name: Build run: | - cmake -H. -Bbuild-cmake -GNinja -DCMAKE_BUILD_TYPE=Release + cmake -H. -Bbuild-cmake -GNinja -DCMAKE_BUILD_TYPE=Release -D CMAKE_C_COMPILER_LAUNCHER=ccache -D CMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" cmake --build build-cmake -j - name: Download starship.o2r uses: actions/download-artifact@v4 @@ -138,7 +103,7 @@ jobs: - name: Publish packaged artifacts uses: actions/upload-artifact@v4 with: - name: Starship-mac-arm64 + name: Starship-mac path: | Starship.dmg readme.txt diff --git a/README.md b/README.md index f8690383a..eaccfeb83 100644 --- a/README.md +++ b/README.md @@ -83,8 +83,7 @@ If you want to manually compile Starship, please consult the [building instructi If you want to playtest a continuous integration build, you can find them at the links below. Keep in mind that these are for playtesting only, and you will likely encounter bugs and possibly crashes. * [Windows](https://nightly.link/HarbourMasters/Starship/workflows/main/main/starship-windows.zip) -* [macOS (Apple Silicon)](https://nightly.link/HarbourMasters/Starship/workflows/main/main/Starship-mac-arm64.zip) -* [macOS (Intel)](https://nightly.link/HarbourMasters/Starship/workflows/main/main/Starship-mac-x64.zip) +* [macOS](https://nightly.link/HarbourMasters/Starship/workflows/main/main/Starship-mac.zip) * [Linux](https://nightly.link/HarbourMasters/Starship/workflows/main/main/Starship-linux.zip) * [Switch](https://nightly.link/HarbourMasters/Starship/workflows/main/main/Starship-switch.zip) From 62eb5a198bc5a8806f8c1b43551cd689e929455e Mon Sep 17 00:00:00 2001 From: Garrett Cox Date: Fri, 20 Mar 2026 21:53:27 -0500 Subject: [PATCH 49/73] Tweak actions cache (#248) --- .github/workflows/mac.yml | 2 +- .github/workflows/main.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index 266a808a9..569dcca20 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -16,7 +16,7 @@ jobs: parameters: '.github/macports.yml' - uses: hendrikmuhs/ccache-action@v1.2 with: - key: macos-ccache-${{ github.ref }} + key: macos # ccache-macos - name: Build run: | cmake -H. -Bbuild-cmake -GNinja -DCMAKE_BUILD_TYPE=Release -D CMAKE_C_COMPILER_LAUNCHER=ccache -D CMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 41c68a741..2780b5213 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -85,7 +85,7 @@ jobs: parameters: '.github/macports.yml' - uses: hendrikmuhs/ccache-action@v1.2 with: - key: macos-ccache-${{ github.ref }} + key: macos # ccache-macos - name: Build run: | cmake -H. -Bbuild-cmake -GNinja -DCMAKE_BUILD_TYPE=Release -D CMAKE_C_COMPILER_LAUNCHER=ccache -D CMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" From 7dd7ec260bf1820caf95edd886d0893533f0e93c Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Mon, 23 Mar 2026 18:29:19 -0300 Subject: [PATCH 50/73] No need to rotate buildings twice if we're already doing it in Object_SetMatrix --- src/engine/fox_enmy.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/engine/fox_enmy.c b/src/engine/fox_enmy.c index 5c7663b29..18e992180 100644 --- a/src/engine/fox_enmy.c +++ b/src/engine/fox_enmy.c @@ -2005,6 +2005,13 @@ void Sprite167_Update(Sprite167* this) { // World-aligned billboarding void SceneryRotateTowardsCamera(Scenery* this) { + bool isBuilding = (this->obj.id >= OBJ_SCENERY_CO_BUILDING_5 && this->obj.id <= OBJ_SCENERY_CO_BUILDING_8 || + this->obj.id == OBJ_SCENERY_CO_BUILDING_10); + + if (isBuilding) { + return; + } + this->obj.rot.y = 0.0f; if (gPlayer[0].cam.eye.x < this->obj.pos.x) { this->obj.rot.y = 271.0f; From 0caf5e2d6693ab4101d98f035452a05efc92ef1b Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Sun, 26 Apr 2026 23:05:57 -0300 Subject: [PATCH 51/73] Fix Crash in Sector Y BOSS --- src/overlays/ovl_i6/fox_sy.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/overlays/ovl_i6/fox_sy.c b/src/overlays/ovl_i6/fox_sy.c index 702f11819..f05216114 100644 --- a/src/overlays/ovl_i6/fox_sy.c +++ b/src/overlays/ovl_i6/fox_sy.c @@ -87,6 +87,9 @@ void SectorY_SyShogun_Init(SyShogun* this) { if (gPlayer[0].state == PLAYERSTATE_START_360) { this->obj.pos.z = -28900.0f; gScenery360[SHOGUN_SHIP].obj.pos.z = -30000.0f; + + // @port: Fix crash with expanded draw distance. + gScenery360[SHOGUN_SHIP].info.dList = aSySaruzinDL; } this->rot_078.y = 0.0f; From fad71e70d7a39719988d1b94d6951b4615345146 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Thu, 30 Apr 2026 17:53:21 -0300 Subject: [PATCH 52/73] Add Spanish Translation ROMHACK support --- config.yml | 21 +++++++++++++++++++++ src/port/extractor/GameExtractor.cpp | 1 + 2 files changed, 22 insertions(+) diff --git a/config.yml b/config.yml index 3c4b0d501..bba722758 100644 --- a/config.yml +++ b/config.yml @@ -120,6 +120,27 @@ d064229a32cc05ab85e2381ce07744eb3ffaf530: # - include/sf64event.h # - include/sf64player.h +# EU SPANISH ROMHACK (For audio extraction) +e6dad7523ff8f83fad6fbdb59d472b4f76340c2b: + name: Star Fox 64 (EU) (SPANISH ROMHACK) (V1.0) + path: assets/yaml/eu/rev0 + config: + gbi: F3DEX + sort: OFFSET + logging: INFO + output: + binary: ./mods/sf64eu.o2r + code: src/eu/assets + headers: include/assets + modding: src/eu/assets + # enums: + # - include/sf64object.h + # - include/sf64level.h + # - include/sf64mesg.h + # - include/sf64audio_external.h + # - include/sf64event.h + # - include/sf64player.h + c8a10699dea52f4bb2e2311935c1376dfb352e7a: name: Star Fox 64 (CN) (V1.1) (Compressed) preprocess: diff --git a/src/port/extractor/GameExtractor.cpp b/src/port/extractor/GameExtractor.cpp index d75ab3c3a..9be7f4b7d 100644 --- a/src/port/extractor/GameExtractor.cpp +++ b/src/port/extractor/GameExtractor.cpp @@ -18,6 +18,7 @@ std::unordered_map mGameList = { { "d064229a32cc05ab85e2381ce07744eb3ffaf530", "Star Fox 64 (J) (V1.0) (Uncompressed)" }, { "05b307b8804f992af1a1e2fbafbd588501fdf799", "Star Fox 64 (E) (V1.0)" }, { "09f5d5c14219fc77a36c5a6ad5e63f7abd8b3385", "Star Fox 64 (E) (V1.0) (Uncompressed)" }, + { "e6dad7523ff8f83fad6fbdb59d472b4f76340c2b", "Star Fox 64 (E) (V1.0) (Spanish Romhack)" }, { "c8a10699dea52f4bb2e2311935c1376dfb352e7a", "Star Fox 64 (C) (V1.1)" }, { "3a05aba5549fa71e8b16a0c6e2c8481b070818a9", "Star Fox 64 (C) (V1.1) (Uncompressed)" }, }; From 39bf387e58342e5e2f090c823f3a09bc174ccd30 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Thu, 30 Apr 2026 19:21:33 -0300 Subject: [PATCH 53/73] Accomodate menu for the Spanish audio romhack support --- src/port/Engine.h | 1 + src/port/ui/ImguiUI.cpp | 32 +++++++++++++++++++++++--------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/port/Engine.h b/src/port/Engine.h index bbb50c60a..b937e9db0 100644 --- a/src/port/Engine.h +++ b/src/port/Engine.h @@ -6,6 +6,7 @@ typedef enum { SF64_VER_US = 0x94F1D5A7, SF64_VER_EU = 0x6EE9ADE7, + SF64_VER_EU_SPA = 0x3964945f, SF64_VER_JP = 0x3728D3E1 } SF64Version; diff --git a/src/port/ui/ImguiUI.cpp b/src/port/ui/ImguiUI.cpp index b9bc2c32f..5874ac2ac 100644 --- a/src/port/ui/ImguiUI.cpp +++ b/src/port/ui/ImguiUI.cpp @@ -115,6 +115,9 @@ static const char* filters[3] = { static const char* voiceLangs[] = { "Original", /*"Japanese",*/ "Lylat" }; +static const char* voiceLangsSPA[] = { + "Español", /*"Japanese",*/ "Lylat" +}; void DrawSpeakerPositionEditor() { static ImVec2 lastCanvasPos; @@ -309,15 +312,26 @@ void DrawSettingsMenu(){ UIWidgets::Spacer(0); if (UIWidgets::BeginMenu("Language")) { ImGui::Dummy(ImVec2(150, 0.0f)); - if (!GameEngine::HasVersion(SF64_VER_JP) && GameEngine::HasVersion(SF64_VER_EU)){ - //UIWidgets::Spacer(0); - if (UIWidgets::CVarCombobox("Voices", "gVoiceLanguage", voiceLangs, - { - .tooltip = "Changes the language of the voice acting in the game", - .defaultIndex = 0, - })) { - Audio_SetVoiceLanguage(CVarGetInteger("gVoiceLanguage", 0)); - }; + if (!GameEngine::HasVersion(SF64_VER_JP) && (GameEngine::HasVersion(SF64_VER_EU) || GameEngine::HasVersion(SF64_VER_EU_SPA))) { + if (GameEngine::HasVersion(SF64_VER_EU_SPA)) { + //UIWidgets::Spacer(0); + if (UIWidgets::CVarCombobox("Voices", "gVoiceLanguage", voiceLangsSPA, + { + .tooltip = "Changes the language of the voice acting in the game", + .defaultIndex = 0, + })) { + Audio_SetVoiceLanguage(CVarGetInteger("gVoiceLanguage", 0)); + }; + } else { + //UIWidgets::Spacer(0); + if (UIWidgets::CVarCombobox("Voices", "gVoiceLanguage", voiceLangs, + { + .tooltip = "Changes the language of the voice acting in the game", + .defaultIndex = 0, + })) { + Audio_SetVoiceLanguage(CVarGetInteger("gVoiceLanguage", 0)); + }; + } } else { if (UIWidgets::Button("Install JP/EU Audio")) { if (GameEngine::GenAssetFile(false)){ From ed6604310a74c2207716a5bee32ae05d5fd00f7a Mon Sep 17 00:00:00 2001 From: SonicDcer Date: Sat, 9 May 2026 01:48:23 -0300 Subject: [PATCH 54/73] CI Try --- .github/workflows/switch.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/switch.yml b/.github/workflows/switch.yml index 5fe784c37..00f9a283f 100644 --- a/.github/workflows/switch.yml +++ b/.github/workflows/switch.yml @@ -18,9 +18,9 @@ jobs: sudo apt-get update sudo apt-get install -y ninja-build sudo apt-get remove -y cmake - git -C libultraship remote add nx https://github.com/Net64DD/libultraship.git + git -C libultraship remote add nx https://github.com/sonicdcer/libultraship.git git -C libultraship fetch nx - git -C libultraship checkout nx/main-nx + git -C libultraship checkout nx/main-nx_backup wget https://github.com/Kitware/CMake/releases/download/v3.28.3/cmake-3.28.3-linux-x86_64.sh -O /tmp/cmake.sh sudo sh /tmp/cmake.sh --prefix=/usr/local/ --exclude-subdir cmake -H. -Bbuild-switch -GNinja -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/Switch.cmake From 073d2660d54cd447800ada9443e19eec886d276b Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Sat, 9 May 2026 01:49:20 -0300 Subject: [PATCH 55/73] CI Try --- .github/workflows/switch.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/switch.yml b/.github/workflows/switch.yml index 784bfb058..00f9a283f 100644 --- a/.github/workflows/switch.yml +++ b/.github/workflows/switch.yml @@ -18,14 +18,14 @@ jobs: sudo apt-get update sudo apt-get install -y ninja-build sudo apt-get remove -y cmake - git -C libultraship remote add nx https://github.com/Net64DD/libultraship.git + git -C libultraship remote add nx https://github.com/sonicdcer/libultraship.git git -C libultraship fetch nx - git -C libultraship checkout nx/main-nx + git -C libultraship checkout nx/main-nx_backup wget https://github.com/Kitware/CMake/releases/download/v3.28.3/cmake-3.28.3-linux-x86_64.sh -O /tmp/cmake.sh sudo sh /tmp/cmake.sh --prefix=/usr/local/ --exclude-subdir cmake -H. -Bbuild-switch -GNinja -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/Switch.cmake cmake --build build-switch --config Release -j3 - wget -O gamecontrollerdb.txt https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt + wget -O gamecontrollerdb.txt https://github.com/mdqinc/SDL_GameControllerDB/blob/master/gamecontrollerdb.txt mv README.md readme.txt mv build-switch/*.nro Starship.nro - name: Upload build From 50550e5a2ed1952160596a84a43eb71b81f89f4b Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Sat, 9 May 2026 01:54:07 -0300 Subject: [PATCH 56/73] Fix wget --- .github/workflows/switch.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/switch.yml b/.github/workflows/switch.yml index 00f9a283f..3d83c850f 100644 --- a/.github/workflows/switch.yml +++ b/.github/workflows/switch.yml @@ -25,7 +25,7 @@ jobs: sudo sh /tmp/cmake.sh --prefix=/usr/local/ --exclude-subdir cmake -H. -Bbuild-switch -GNinja -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/Switch.cmake cmake --build build-switch --config Release -j3 - wget -O gamecontrollerdb.txt https://github.com/mdqinc/SDL_GameControllerDB/blob/master/gamecontrollerdb.txt + wget -O gamecontrollerdb.txt https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt mv README.md readme.txt mv build-switch/*.nro Starship.nro - name: Upload build From 6cc3b1e4280dff309c21701e90d522deadfd95bf Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Sat, 9 May 2026 02:16:17 -0300 Subject: [PATCH 57/73] CI Try 2 --- .github/workflows/switch.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/switch.yml b/.github/workflows/switch.yml index 3d83c850f..fd676fbfa 100644 --- a/.github/workflows/switch.yml +++ b/.github/workflows/switch.yml @@ -18,9 +18,9 @@ jobs: sudo apt-get update sudo apt-get install -y ninja-build sudo apt-get remove -y cmake - git -C libultraship remote add nx https://github.com/sonicdcer/libultraship.git + git -C libultraship remote add nx https://github.com/Net64DD/libultraship.git git -C libultraship fetch nx - git -C libultraship checkout nx/main-nx_backup + git -C libultraship checkout 2623c6bad4e2e7d2e6191816439f7a3f6e5ccb21 wget https://github.com/Kitware/CMake/releases/download/v3.28.3/cmake-3.28.3-linux-x86_64.sh -O /tmp/cmake.sh sudo sh /tmp/cmake.sh --prefix=/usr/local/ --exclude-subdir cmake -H. -Bbuild-switch -GNinja -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/Switch.cmake From 9eb0d11d55425c386103afafcb8092ee66e2579a Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Sat, 9 May 2026 02:25:01 -0300 Subject: [PATCH 58/73] CI Try 3 --- .github/workflows/switch.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/switch.yml b/.github/workflows/switch.yml index fd676fbfa..07126f8dc 100644 --- a/.github/workflows/switch.yml +++ b/.github/workflows/switch.yml @@ -20,7 +20,7 @@ jobs: sudo apt-get remove -y cmake git -C libultraship remote add nx https://github.com/Net64DD/libultraship.git git -C libultraship fetch nx - git -C libultraship checkout 2623c6bad4e2e7d2e6191816439f7a3f6e5ccb21 + git -C libultraship checkout 09dfab5fb2a9a047a6e268dc9db2daad9b2ce5f0 wget https://github.com/Kitware/CMake/releases/download/v3.28.3/cmake-3.28.3-linux-x86_64.sh -O /tmp/cmake.sh sudo sh /tmp/cmake.sh --prefix=/usr/local/ --exclude-subdir cmake -H. -Bbuild-switch -GNinja -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/Switch.cmake From 2444d5e5b8d53f37aa129f8bf10fe3feb95ae925 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Sat, 9 May 2026 02:35:12 -0300 Subject: [PATCH 59/73] CI Try 4 --- include/endianness.h | 85 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 include/endianness.h diff --git a/include/endianness.h b/include/endianness.h new file mode 100644 index 000000000..3b17a8b97 --- /dev/null +++ b/include/endianness.h @@ -0,0 +1,85 @@ +#pragma once + +#ifdef __cplusplus +namespace Ship { +enum class Endianness { + Little = 0, + Big = 1, + +#if (defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)) || defined(__BIG_ENDIAN__) + Native = Big, +#else + Native = Little, +#endif +}; +} // namespace Ship +#endif + +#ifdef _MSC_VER +#include + +#define BSWAP16 _byteswap_ushort +#define BSWAP32 _byteswap_ulong +#define BSWAP64 _byteswap_uint64 + +#define BOMSWAP16 _byteswap_ushort +#define BOMSWAP32 _byteswap_ulong +#define BOMSWAP64 _byteswap_uint64 + +#define BOMSWAP16_CONST(x) ((((x) >> 8) & 0x00FF) | (((x) << 8) & 0xFF00)) +#define BOMSWAP32_CONST(x) \ + ((((x) >> 24) & 0x000000FF) | (((x) >> 8) & 0x0000FF00) | (((x) << 8) & 0x00FF0000) | (((x) << 24) & 0xFF000000)) +#define BOMSWAP64_CONST(x) \ + ((((x) >> 56) & 0x00000000000000FF) | (((x) >> 40) & 0x000000000000FF00) | (((x) >> 24) & 0x0000000000FF0000) | \ + (((x) >> 8) & 0x00000000FF000000) | (((x) << 8) & 0x000000FF00000000) | (((x) << 24) & 0x0000FF0000000000) | \ + (((x) << 40) & 0x00FF000000000000) | (((x) << 56) & 0xFF00000000000000)) +#else + +#define BSWAP16 __builtin_bswap16 +#define BSWAP32 __builtin_bswap32 +#define BSWAP64 __builtin_bswap64 + +#define BOMSWAP16 __builtin_bswap16 +#define BOMSWAP32 __builtin_bswap32 +#define BOMSWAP64 __builtin_bswap64 + +#define BOMSWAP16_CONST __builtin_bswap16 +#define BOMSWAP32_CONST __builtin_bswap32 +#define BOMSWAP64_CONST __builtin_bswap64 +#endif + +#if (defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)) || defined(__BIG_ENDIAN__) +#ifndef IS_BIGENDIAN +#define IS_BIGENDIAN +#endif +#endif + +#ifdef IS_BIGENDIAN +#define BE16SWAP(x) (x) +#define BE32SWAP(x) (x) +#define BE64SWAP(x) (x) +#define LE16SWAP(x) BOMSWAP16(x) +#define LE32SWAP(x) BOMSWAP32(x) +#define LE64SWAP(x) BOMSWAP64(x) + +#define BE16SWAP_CONST(x) (x) +#define BE32SWAP_CONST(x) (x) +#define BE64SWAP_CONST(x) (x) +#define LE16SWAP_CONST(x) BOMSWAP16_CONST(x) +#define LE32SWAP_CONST(x) BOMSWAP32_CONST(x) +#define LE64SWAP_CONST(x) BOMSWAP64_CONST(x) +#else +#define BE16SWAP(x) BOMSWAP16(x) +#define BE32SWAP(x) BOMSWAP32(x) +#define BE64SWAP(x) BOMSWAP64(x) +#define LE16SWAP(x) (x) +#define LE32SWAP(x) (x) +#define LE64SWAP(x) (x) + +#define BE16SWAP_CONST(x) BOMSWAP16_CONST(x) +#define BE32SWAP_CONST(x) BOMSWAP32_CONST(x) +#define BE64SWAP_CONST(x) BOMSWAP64_CONST(x) +#define LE16SWAP_CONST(x) (x) +#define LE32SWAP_CONST(x) (x) +#define LE64SWAP_CONST(x) (x) +#endif From be6c807be42d155cdf1b369f4e55ce8670169371 Mon Sep 17 00:00:00 2001 From: Sonic Dreamcaster Date: Sat, 9 May 2026 02:51:08 -0300 Subject: [PATCH 60/73] CI Try 5 --- .github/workflows/main.yml | 4 +- .github/workflows/switch.yml | 4 +- include/endianness.h | 85 ------------------------------------ 3 files changed, 4 insertions(+), 89 deletions(-) delete mode 100644 include/endianness.h diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2780b5213..6d98ae4e1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -211,9 +211,9 @@ jobs: sudo apt-get update sudo apt-get install -y ninja-build sudo apt-get remove -y cmake - git -C libultraship remote add nx https://github.com/Net64DD/libultraship.git + git -C libultraship remote add nx https://github.com/sonicdcer/libultraship.git git -C libultraship fetch nx - git -C libultraship checkout nx/main-nx + git -C libultraship checkout nx/main-nx_backup wget https://github.com/Kitware/CMake/releases/download/v3.28.3/cmake-3.28.3-linux-x86_64.sh -O /tmp/cmake.sh sudo sh /tmp/cmake.sh --prefix=/usr/local/ --exclude-subdir cmake -H. -Bbuild-switch -GNinja -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/Switch.cmake diff --git a/.github/workflows/switch.yml b/.github/workflows/switch.yml index 07126f8dc..3d83c850f 100644 --- a/.github/workflows/switch.yml +++ b/.github/workflows/switch.yml @@ -18,9 +18,9 @@ jobs: sudo apt-get update sudo apt-get install -y ninja-build sudo apt-get remove -y cmake - git -C libultraship remote add nx https://github.com/Net64DD/libultraship.git + git -C libultraship remote add nx https://github.com/sonicdcer/libultraship.git git -C libultraship fetch nx - git -C libultraship checkout 09dfab5fb2a9a047a6e268dc9db2daad9b2ce5f0 + git -C libultraship checkout nx/main-nx_backup wget https://github.com/Kitware/CMake/releases/download/v3.28.3/cmake-3.28.3-linux-x86_64.sh -O /tmp/cmake.sh sudo sh /tmp/cmake.sh --prefix=/usr/local/ --exclude-subdir cmake -H. -Bbuild-switch -GNinja -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/Switch.cmake diff --git a/include/endianness.h b/include/endianness.h deleted file mode 100644 index 3b17a8b97..000000000 --- a/include/endianness.h +++ /dev/null @@ -1,85 +0,0 @@ -#pragma once - -#ifdef __cplusplus -namespace Ship { -enum class Endianness { - Little = 0, - Big = 1, - -#if (defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)) || defined(__BIG_ENDIAN__) - Native = Big, -#else - Native = Little, -#endif -}; -} // namespace Ship -#endif - -#ifdef _MSC_VER -#include - -#define BSWAP16 _byteswap_ushort -#define BSWAP32 _byteswap_ulong -#define BSWAP64 _byteswap_uint64 - -#define BOMSWAP16 _byteswap_ushort -#define BOMSWAP32 _byteswap_ulong -#define BOMSWAP64 _byteswap_uint64 - -#define BOMSWAP16_CONST(x) ((((x) >> 8) & 0x00FF) | (((x) << 8) & 0xFF00)) -#define BOMSWAP32_CONST(x) \ - ((((x) >> 24) & 0x000000FF) | (((x) >> 8) & 0x0000FF00) | (((x) << 8) & 0x00FF0000) | (((x) << 24) & 0xFF000000)) -#define BOMSWAP64_CONST(x) \ - ((((x) >> 56) & 0x00000000000000FF) | (((x) >> 40) & 0x000000000000FF00) | (((x) >> 24) & 0x0000000000FF0000) | \ - (((x) >> 8) & 0x00000000FF000000) | (((x) << 8) & 0x000000FF00000000) | (((x) << 24) & 0x0000FF0000000000) | \ - (((x) << 40) & 0x00FF000000000000) | (((x) << 56) & 0xFF00000000000000)) -#else - -#define BSWAP16 __builtin_bswap16 -#define BSWAP32 __builtin_bswap32 -#define BSWAP64 __builtin_bswap64 - -#define BOMSWAP16 __builtin_bswap16 -#define BOMSWAP32 __builtin_bswap32 -#define BOMSWAP64 __builtin_bswap64 - -#define BOMSWAP16_CONST __builtin_bswap16 -#define BOMSWAP32_CONST __builtin_bswap32 -#define BOMSWAP64_CONST __builtin_bswap64 -#endif - -#if (defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)) || defined(__BIG_ENDIAN__) -#ifndef IS_BIGENDIAN -#define IS_BIGENDIAN -#endif -#endif - -#ifdef IS_BIGENDIAN -#define BE16SWAP(x) (x) -#define BE32SWAP(x) (x) -#define BE64SWAP(x) (x) -#define LE16SWAP(x) BOMSWAP16(x) -#define LE32SWAP(x) BOMSWAP32(x) -#define LE64SWAP(x) BOMSWAP64(x) - -#define BE16SWAP_CONST(x) (x) -#define BE32SWAP_CONST(x) (x) -#define BE64SWAP_CONST(x) (x) -#define LE16SWAP_CONST(x) BOMSWAP16_CONST(x) -#define LE32SWAP_CONST(x) BOMSWAP32_CONST(x) -#define LE64SWAP_CONST(x) BOMSWAP64_CONST(x) -#else -#define BE16SWAP(x) BOMSWAP16(x) -#define BE32SWAP(x) BOMSWAP32(x) -#define BE64SWAP(x) BOMSWAP64(x) -#define LE16SWAP(x) (x) -#define LE32SWAP(x) (x) -#define LE64SWAP(x) (x) - -#define BE16SWAP_CONST(x) BOMSWAP16_CONST(x) -#define BE32SWAP_CONST(x) BOMSWAP32_CONST(x) -#define BE64SWAP_CONST(x) BOMSWAP64_CONST(x) -#define LE16SWAP_CONST(x) (x) -#define LE32SWAP_CONST(x) (x) -#define LE64SWAP_CONST(x) (x) -#endif From 157100f2b4a68d27f46ff10a079fc2f16105e442 Mon Sep 17 00:00:00 2001 From: Alejandro Javier Asenjo Nitti <96613413+sonicdcer@users.noreply.github.com> Date: Mon, 15 Jun 2026 02:51:06 -0300 Subject: [PATCH 61/73] Refresh CI.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eaccfeb83..9c4b52cec 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [comment]: <> (Todo: Make Light Mode Image) [comment]: <> (Todo: Make Dark Mode Image) - + # Starship Lead Developers: From b96d7e4c530a11975b0f7a618253b9a0ea225880 Mon Sep 17 00:00:00 2001 From: briaguya <70942617+briaguya0@users.noreply.github.com> Date: Mon, 15 Jun 2026 01:59:47 -0400 Subject: [PATCH 62/73] remove space from readme added in previous commit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c4b52cec..eaccfeb83 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [comment]: <> (Todo: Make Light Mode Image) [comment]: <> (Todo: Make Dark Mode Image) - + # Starship Lead Developers: From d94893877ffafc034227bc93183165d45f0bab08 Mon Sep 17 00:00:00 2001 From: SonicDcer Date: Mon, 15 Jun 2026 03:14:08 -0300 Subject: [PATCH 63/73] Fix switch try --- .github/workflows/main.yml | 4 ++-- .github/workflows/switch.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6d98ae4e1..f5fbae7f8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -211,9 +211,9 @@ jobs: sudo apt-get update sudo apt-get install -y ninja-build sudo apt-get remove -y cmake - git -C libultraship remote add nx https://github.com/sonicdcer/libultraship.git + git -C libultraship remote add nx https://github.com/Net64DD/libultraship.git git -C libultraship fetch nx - git -C libultraship checkout nx/main-nx_backup + git -C libultraship checkout 09dfab5fb2a9a047a6e268dc9db2daad9b2ce5f0 wget https://github.com/Kitware/CMake/releases/download/v3.28.3/cmake-3.28.3-linux-x86_64.sh -O /tmp/cmake.sh sudo sh /tmp/cmake.sh --prefix=/usr/local/ --exclude-subdir cmake -H. -Bbuild-switch -GNinja -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/Switch.cmake diff --git a/.github/workflows/switch.yml b/.github/workflows/switch.yml index 3d83c850f..07126f8dc 100644 --- a/.github/workflows/switch.yml +++ b/.github/workflows/switch.yml @@ -18,9 +18,9 @@ jobs: sudo apt-get update sudo apt-get install -y ninja-build sudo apt-get remove -y cmake - git -C libultraship remote add nx https://github.com/sonicdcer/libultraship.git + git -C libultraship remote add nx https://github.com/Net64DD/libultraship.git git -C libultraship fetch nx - git -C libultraship checkout nx/main-nx_backup + git -C libultraship checkout 09dfab5fb2a9a047a6e268dc9db2daad9b2ce5f0 wget https://github.com/Kitware/CMake/releases/download/v3.28.3/cmake-3.28.3-linux-x86_64.sh -O /tmp/cmake.sh sudo sh /tmp/cmake.sh --prefix=/usr/local/ --exclude-subdir cmake -H. -Bbuild-switch -GNinja -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/Switch.cmake From 6202c44356fee70dd23e80a16933b211863d3e2d Mon Sep 17 00:00:00 2001 From: briaguya <70942617+briaguya0@users.noreply.github.com> Date: Mon, 15 Jun 2026 02:49:30 -0400 Subject: [PATCH 64/73] test: trigger CI --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eaccfeb83..9c4b52cec 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [comment]: <> (Todo: Make Light Mode Image) [comment]: <> (Todo: Make Dark Mode Image) - + # Starship Lead Developers: From 675f007caf74a36303d9ec04879406df546aeb2a Mon Sep 17 00:00:00 2001 From: izzy2lost Date: Mon, 3 Aug 2026 21:28:56 -0400 Subject: [PATCH 65/73] Android: generate sf64.o2r on the device MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The app used to require an sf64.o2r the user had produced on a PC with Torch, then hand it over through a SAF folder grant backed by MANAGE_EXTERNAL_STORAGE. Now the launcher takes the ROM itself, verifies it, and runs Torch — already linked into the game library — to build the archive on the device. No storage permission, no desktop step. The app is rebuilt in Kotlin around that flow, matching Ghostship and SpaghettiKart: LauncherActivity unpack bundled files, take a ROM, extract, hand off GameAssets where everything lives, plus the JNI extraction call MainActivity the game and the on-screen controller overlay Mods/SavesActivity managers reachable from the in-game menu The overlay drives a virtual SDL joystick (src/port/android/AndroidBridge.cpp) instead of synthesising input, so it goes through libultraship's normal mapping and is remappable in game. That also replaces the look-around area's old route into a fork-only libultraship camera API, which went away with the vendored copy: it now drives the right stick. Build side: - Groovy -> Kotlin DSL, AGP 8.13.2, Kotlin 2.2.10, NDK 30.0.15729638 - config.yml, assets/yaml/ and starship.o2r are bundled as one reproducible gamedata.zip with a digest beside it, wired through the variant API so a missing engine archive fails the build loudly rather than shipping an APK with nothing in it - cmake/dependencies/android.cmake supplies what the NDK build has no package manager for. libultraship's own Android cmake only fetches SDL2 and leaves libzip, nlohmann_json, tinyxml2 and spdlog to one. It also pins SDL 2.32.10, because the 2.28.1 libultraship asks for calls ALooper_pollAll, which NDK 27 removed from the headers; the Java glue's SDL_MICRO_VERSION moves with it or SDLActivity refuses to start. Ogg and Vorbis are fetched too — the Android branch of the platform switch links them but never had a find_package. - the host-only Torch ExternalProject, the asset copy next to the binary and CPack are all skipped on Android - .gitignore's "build*" was swallowing build.gradle.kts Verified: libStarship.so builds clean for arm64-v8a from an empty build directory, and exports all six JNI entry points the Kotlin side declares. Co-Authored-By: Claude Opus 5 --- .github/workflows/android.yml | 65 +- .github/workflows/release.yml | 72 +- .gitignore | 3 + CMakeLists.txt | 42 +- android/.gitignore | 4 + android/app/build.gradle | 156 --- android/app/build.gradle.kts | 237 ++++ android/app/gradle/wrapper/gradle-wrapper.jar | Bin 59536 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - android/app/gradlew | 234 ---- android/app/gradlew.bat | 89 -- android/app/src/main/AndroidManifest.xml | 136 ++- .../com/starship/android/AssetCopyUtil.java | 87 -- .../starship/android/ControllerButtons.java | 26 - .../com/starship/android/ControllerButtons.kt | 27 + .../com/starship/android/DialogActivity.java | 103 -- .../java/com/starship/android/GameAssets.kt | 186 +++ .../com/starship/android/LauncherActivity.kt | 212 ++++ .../com/starship/android/MainActivity.java | 1065 ----------------- .../java/com/starship/android/MainActivity.kt | 351 ++++++ .../java/com/starship/android/ModStore.kt | 243 ++++ .../java/com/starship/android/ModsActivity.kt | 252 ++++ .../java/com/starship/android/SaveStore.kt | 129 ++ .../com/starship/android/SavesActivity.kt | 224 ++++ .../java/com/starship/android/SystemBars.kt | 30 + .../main/java/org/libsdl/app/SDLActivity.java | 2 +- .../drawable/rounded_dialog_background.xml | 7 - .../main/res/layout/touchcontrol_overlay.xml | 15 + android/app/src/main/res/values/strings.xml | 6 + android/app/src/main/res/values/styles.xml | 35 - android/build.gradle | 25 - android/build.gradle.kts | 4 + android/gradle.properties | 26 +- android/gradlew | 0 android/settings.gradle | 1 - android/settings.gradle.kts | 18 + cmake/dependencies/android.cmake | 139 +++ src/port/android/AndroidBridge.cpp | 222 ++++ 38 files changed, 2486 insertions(+), 1992 deletions(-) delete mode 100644 android/app/build.gradle create mode 100644 android/app/build.gradle.kts delete mode 100644 android/app/gradle/wrapper/gradle-wrapper.jar delete mode 100644 android/app/gradle/wrapper/gradle-wrapper.properties delete mode 100644 android/app/gradlew delete mode 100644 android/app/gradlew.bat delete mode 100644 android/app/src/main/java/com/starship/android/AssetCopyUtil.java delete mode 100644 android/app/src/main/java/com/starship/android/ControllerButtons.java create mode 100644 android/app/src/main/java/com/starship/android/ControllerButtons.kt delete mode 100644 android/app/src/main/java/com/starship/android/DialogActivity.java create mode 100644 android/app/src/main/java/com/starship/android/GameAssets.kt create mode 100644 android/app/src/main/java/com/starship/android/LauncherActivity.kt delete mode 100644 android/app/src/main/java/com/starship/android/MainActivity.java create mode 100644 android/app/src/main/java/com/starship/android/MainActivity.kt create mode 100644 android/app/src/main/java/com/starship/android/ModStore.kt create mode 100644 android/app/src/main/java/com/starship/android/ModsActivity.kt create mode 100644 android/app/src/main/java/com/starship/android/SaveStore.kt create mode 100644 android/app/src/main/java/com/starship/android/SavesActivity.kt create mode 100644 android/app/src/main/java/com/starship/android/SystemBars.kt delete mode 100644 android/app/src/main/res/drawable/rounded_dialog_background.xml delete mode 100644 android/app/src/main/res/values/styles.xml delete mode 100644 android/build.gradle create mode 100644 android/build.gradle.kts mode change 100644 => 100755 android/gradlew delete mode 100644 android/settings.gradle create mode 100644 android/settings.gradle.kts create mode 100644 cmake/dependencies/android.cmake create mode 100644 src/port/android/AndroidBridge.cpp diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index bc69ac562..180ab1cc1 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -13,6 +13,10 @@ concurrency: cancel-in-progress: true jobs: + # The engine assets are the one thing the app cannot build on-device: packing + # them needs the Torch executable, and the NDK build has no host toolchain. + # Build it here and ship the result as an APK asset. The game's own assets are + # extracted from the user's ROM on-device. generate-port-o2r: runs-on: ubuntu-22.04 steps: @@ -20,7 +24,7 @@ jobs: with: submodules: true - name: Install dependencies - run: sudo apt-get install gcc g++ git cmake ninja-build lsb-release + run: sudo apt-get update && sudo apt-get install -y gcc g++ git cmake ninja-build lsb-release - name: ccache uses: hendrikmuhs/ccache-action@v1.2.18 with: @@ -58,58 +62,66 @@ jobs: - uses: actions/checkout@v4 with: submodules: true - + - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' - + - name: Setup Android SDK uses: android-actions/setup-android@v3 - - - name: Install NDK 28 + + - name: Install NDK run: | - echo "y" | ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --install "ndk;28.2.12604990" || \ - echo "y" | ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --install "ndk;28.0.12433566" - + echo "y" | ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --install "ndk;30.0.15729638" + - name: Install dependencies - run: sudo apt-get install -y ninja-build - + run: sudo apt-get update && sudo apt-get install -y ninja-build + + # The repository root, not the assets folder: the packaging task in + # android/app/build.gradle.kts bundles this into gamedata.zip itself. - name: Download starship.o2r uses: actions/download-artifact@v4 with: name: starship.o2r - path: android/app/src/main/assets - + path: . + - name: Configure Gradle permissions run: chmod +x android/gradlew - + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + - name: Build Debug APK if: matrix.build-type == 'debug' run: | cd android/ - ./gradlew assembleDebug -P elfBuildType=RelWithDebInfo + ./gradlew assembleDebug mv app/build/outputs/apk/debug/app-debug.apk ../starship-debug.apk - + + # Without a keystore the release build falls back to the debug key, so this + # step is allowed to be skipped on forks that have no secrets configured. - name: Setup signing keystore if: matrix.build-type == 'release' && env.KEYSTORE_FILE != '' run: | - echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > android/app/starship-release-key.keystore - echo "STARSHIP_STORE_FILE=starship-release-key.keystore" >> android/keystore.properties - echo "STARSHIP_STORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }}" >> android/keystore.properties - echo "STARSHIP_KEY_ALIAS=${{ secrets.KEY_ALIAS }}" >> android/keystore.properties - echo "STARSHIP_KEY_PASSWORD=${{ secrets.KEY_PASSWORD }}" >> android/keystore.properties + echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > android/app/release.keystore + test -s android/app/release.keystore || (echo "Failed to create keystore file" && exit 1) env: KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }} - + - name: Build Release APK if: matrix.build-type == 'release' run: | cd android/ - ./gradlew assembleRelease -P elfBuildType=RelWithDebInfo + ./gradlew assembleRelease mv app/build/outputs/apk/release/app-release.apk ../starship-release.apk - + env: + KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE != '' && 'release.keystore' || '' }} + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + - name: Upload Debug APK if: matrix.build-type == 'debug' uses: actions/upload-artifact@v4 @@ -117,7 +129,7 @@ jobs: name: starship-debug-apk path: starship-debug.apk retention-days: 7 - + - name: Upload Release APK if: matrix.build-type == 'release' uses: actions/upload-artifact@v4 @@ -125,6 +137,7 @@ jobs: name: starship-release-apk path: starship-release.apk retention-days: 30 - - + - name: Clean up keystore + if: always() + run: rm -f android/app/release.keystore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 89b01250a..038e9aa3c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,17 +70,15 @@ jobs: uses: actions/download-artifact@v4 with: name: starship.o2r - path: android/app/src/main/assets - - - name: Update version in build.gradle + path: . + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Install NDK run: | - sed -i "s/versionName \".*\"/versionName \"${{ github.event.inputs.version }}\"/" android/app/build.gradle - # Increment version code - CURRENT_CODE=$(grep "versionCode" android/app/build.gradle | sed 's/.*versionCode \([0-9]*\).*/\1/') - NEW_CODE=$((CURRENT_CODE + 1)) - sed -i "s/versionCode $CURRENT_CODE/versionCode $NEW_CODE/" android/app/build.gradle - echo "Updated to version ${{ github.event.inputs.version }} with code $NEW_CODE" - + echo "y" | ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --install "ndk;30.0.15729638" + - name: Configure Gradle permissions run: chmod +x android/gradlew @@ -90,27 +88,30 @@ jobs: - name: Setup signing keystore if: env.KEYSTORE_FILE != '' run: | - echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > android/app/starship-release-key.keystore - echo "STARSHIP_STORE_FILE=starship-release-key.keystore" >> android/keystore.properties - echo "STARSHIP_STORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }}" >> android/keystore.properties - echo "STARSHIP_KEY_ALIAS=${{ secrets.KEY_ALIAS }}" >> android/keystore.properties - echo "STARSHIP_KEY_PASSWORD=${{ secrets.KEY_PASSWORD }}" >> android/keystore.properties + echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > android/app/release.keystore + test -s android/app/release.keystore || (echo "Failed to create keystore file" && exit 1) env: KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }} - - - name: Build Release - run: | - cd android/ - ./gradlew clean - ./gradlew assembleRelease -P elfBuildType=RelWithDebInfo --no-daemon - + - name: Verify APK signing run: | - if [ -f android/app/starship-release-key.keystore ]; then + if [ -s android/app/release.keystore ]; then echo "✅ APK will be signed with release keystore" else - echo "⚠️ APK will be signed with debug keystore (unsigned release)" + echo "⚠️ No keystore secret configured; the release APK falls back to the debug key" fi + + - name: Build Release + run: | + cd android/ + ./gradlew assembleRelease --no-daemon \ + -PstarshipVersionName="${{ github.event.inputs.version }}" \ + -PstarshipVersionCode="${{ github.run_number }}" + env: + KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE != '' && 'release.keystore' || '' }} + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} - name: Prepare release files run: | @@ -133,14 +134,11 @@ jobs: ### 📱 Android Release [!IMPORTANT] - **💾 Saves Now Loading!** - **🎥 Immersive View!** - **🔥 Mods now working!** Put your mods in the `mods/` folder. - - **What's New:** - - Optimized release build with R8 code shrinking - - Improved performance and reduced APK size - - Enhanced file management with SAF integration + **🎮 No desktop setup needed!** The app converts your ROM on the device + itself — no more running Torch on a PC and copying `sf64.o2r` across. + **📂 No storage permission needed.** The ROM is read once through the + system file picker and nothing else on your device is touched. + **🔥 Mods and saves managers** are built into the in-game menu. ### 📦 Downloads - **APK**: `starship-v${{ github.event.inputs.version }}.apk` - Direct install for Android devices @@ -149,13 +147,13 @@ jobs: 1. Download the APK file 2. Enable "Install from unknown sources" in Android settings 3. Install the APK on your Android device - 4. Use [Torch](https://github.com/izzy2lost/Torch/releases) to extract your Star Fox 64 ROM - 5. Launch Starship and select your game folder + 4. Launch Starship and pick your own Star Fox 64 (US) `.z64` ROM + 5. Wait for the one-time extraction to finish — the game starts by itself ### ✅ Requirements - Android 7.0 (API 24) or higher - ARM64 or ARMv7 processor - - Star Fox 64 ROM (US 1.0 or 1.1) + - Star Fox 64 ROM (US 1.0 or 1.1), supplied by you ### 🔒 Security SHA256 checksums are provided in `checksums.txt` @@ -168,3 +166,7 @@ jobs: prerelease: ${{ github.event.inputs.prerelease }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Clean up keystore + if: always() + run: rm -f android/app/release.keystore diff --git a/.gitignore b/.gitignore index 9e00e9f32..4ec9281ff 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,9 @@ cmake-build-*/ .idea/ .vs build* +# ...but not the Gradle build scripts, which "build*" would otherwise swallow. +!build.gradle +!build.gradle.kts logs/ /mods/ starship.cfg.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b4dc6872..28d3fae08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -167,6 +167,14 @@ list(FILTER ALL_FILES EXCLUDE REGEX "src/sys/sys_timer.c") list(FILTER ALL_FILES EXCLUDE REGEX "src/sys/sys_fault.c") list(FILTER ALL_FILES EXCLUDE REGEX "src/mods/object_ram.c") +# Ahead of both the game target below and add_subdirectory(libultraship) further +# down: the include_directories() it adds only reach targets declared after it, +# and its OVERRIDE_FIND_PACKAGE declarations are what libultraship's +# find_package(... REQUIRED) calls resolve against. +if(ANDROID) + include(cmake/dependencies/android.cmake) +endif() + if (CMAKE_SYSTEM_NAME STREQUAL "iOS") set(IOS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libultraship/ios) @@ -645,14 +653,24 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang") endif() endif() +# On Android these ride into the APK as assets and are unpacked to the app's +# external files directory on first run, so there is nothing useful next to the +# .so to copy them to (see android/app/build.gradle.kts and GameAssets.kt). +if(NOT ANDROID) add_custom_command( TARGET ${PROJECT_NAME} POST_BUILD COMMENT "Copying asset yamls..." COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/config.yml" "$/config.yml" COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/assets/" "$/assets/" ) +endif() -if(NOT CMAKE_SYSTEM_NAME STREQUAL "NintendoSwitch") +# Android has no host toolchain available inside the NDK build, so the Torch +# executable these targets drive cannot be built here. starship.o2r is built on +# the CI host and shipped as an APK asset instead, and sf64.o2r is generated +# on-device by the Torch library that is linked into the game (see +# src/port/android/AndroidBridge.cpp). +if(NOT CMAKE_SYSTEM_NAME STREQUAL "NintendoSwitch" AND NOT ANDROID) include(ExternalProject) ExternalProject_Add(TorchExternal PREFIX TorchExternal @@ -725,13 +743,17 @@ set_property(TARGET ${PROJECT_NAME} PROPERTY APPIMAGE_DESKTOP_FILE "${CMAKE_SOUR set_property(TARGET ${PROJECT_NAME} PROPERTY APPIMAGE_ICON_FILE "${CMAKE_SOURCE_DIR}/logo.png") endif() -if(CMAKE_SYSTEM_NAME MATCHES "Linux") - set(CPACK_GENERATOR "External") -elseif(CMAKE_SYSTEM_NAME MATCHES "Windows|NintendoSwitch|CafeOS") - set(CPACK_GENERATOR "ZIP") -elseif(CMAKE_SYSTEM_NAME MATCHES "Darwin") - set(CPACK_GENERATOR "Bundle") -endif() +# Gradle packages the Android build into an APK; CPack has nothing to do there, +# and none of the generators below apply to it. +if(NOT ANDROID) + if(CMAKE_SYSTEM_NAME MATCHES "Linux") + set(CPACK_GENERATOR "External") + elseif(CMAKE_SYSTEM_NAME MATCHES "Windows|NintendoSwitch|CafeOS") + set(CPACK_GENERATOR "ZIP") + elseif(CMAKE_SYSTEM_NAME MATCHES "Darwin") + set(CPACK_GENERATOR "Bundle") + endif() -set(CPACK_PROJECT_CONFIG_FILE ${CMAKE_SOURCE_DIR}/cmake/configure-packaging.cmake) -include(cmake/packaging.cmake) + set(CPACK_PROJECT_CONFIG_FILE ${CMAKE_SOURCE_DIR}/cmake/configure-packaging.cmake) + include(cmake/packaging.cmake) +endif() diff --git a/android/.gitignore b/android/.gitignore index a25d1f865..a48523be7 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -27,6 +27,10 @@ render.experimental.xml *.jks *.keystore +# Release signing credentials (see app/build.gradle.kts) +key.properties +keystore.properties + # Google Services (e.g. APIs or Firebase) google-services.json diff --git a/android/app/build.gradle b/android/app/build.gradle deleted file mode 100644 index c38fccc41..000000000 --- a/android/app/build.gradle +++ /dev/null @@ -1,156 +0,0 @@ -def buildAsLibrary = project.hasProperty('BUILD_AS_LIBRARY'); -def buildAsApplication = !buildAsLibrary -if (buildAsApplication) { - apply plugin: 'com.android.application' -} -else { - apply plugin: 'com.android.library' -} - -// Load keystore properties -def keystorePropertiesFile = rootProject.file("keystore.properties") -def keystoreProperties = new Properties() -if (keystorePropertiesFile.exists()) { - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) -} - -android { - namespace("com.starship.android") - buildFeatures { - buildConfig true - } - - // Signing configuration - signingConfigs { - release { - if (keystorePropertiesFile.exists()) { - storeFile file(keystoreProperties['STARSHIP_STORE_FILE']) - storePassword keystoreProperties['STARSHIP_STORE_PASSWORD'] - keyAlias keystoreProperties['STARSHIP_KEY_ALIAS'] - keyPassword keystoreProperties['STARSHIP_KEY_PASSWORD'] - } - } - } - ndkVersion "27.0.12077973" - compileSdkVersion 36 - defaultConfig { - if (buildAsApplication) { - applicationId "com.starship.android" - } - minSdkVersion 24 - targetSdkVersion 36 - versionCode 6 - versionName "1.0.1" - externalNativeBuild { - cmake { - arguments "-DSDL_SHARED=ON", "-DANDROID_STL=c++_static", "-DHAVE_LD_VERSION_SCRIPT=OFF",'-DUSE_OPENGLES=ON' - abiFilters 'arm64-v8a', 'armeabi-v7a' - } - } - - // Optimize APK size - vectorDrawables { - useSupportLibrary true - } - } - - // ✅ Universal APK: disables split APKs per ABI - splits { - abi { - enable false - } - } - - buildTypes { - debug { - debuggable true - jniDebuggable true - minifyEnabled false - shrinkResources false - } - release { - debuggable false - jniDebuggable false - // Disable minification temporarily to isolate the issue - minifyEnabled false - shrinkResources false - - // Keep debug symbols for crash analysis - ndk { - debugSymbolLevel 'SYMBOL_TABLE' - } - - // Sign the release build - if (keystorePropertiesFile.exists()) { - signingConfig signingConfigs.release - } - } - } - applicationVariants.all { variant -> - tasks["merge${variant.name.capitalize()}Assets"] - .dependsOn("externalNativeBuild${variant.name.capitalize()}") - } - if (!project.hasProperty('EXCLUDE_NATIVE_LIBS')) { - sourceSets.main { - jniLibs.srcDir 'libs' - } - externalNativeBuild { - cmake { - path '../../CMakeLists.txt' - version "3.31.5" - } - } - } - lintOptions { - abortOnError false - checkReleaseBuilds false - } - - // Additional optimizations - packagingOptions { - // Exclude unnecessary files to reduce APK size - excludes += [ - 'META-INF/DEPENDENCIES', - 'META-INF/LICENSE', - 'META-INF/LICENSE.txt', - 'META-INF/NOTICE', - 'META-INF/NOTICE.txt', - 'META-INF/*.kotlin_module' - ] - // Pick first occurrence of duplicate files - pickFirsts += ['**/libc++_shared.so', '**/libjsc.so'] - } - - - - if (buildAsLibrary) { - libraryVariants.all { variant -> - variant.outputs.each { output -> - def outputFile = output.outputFile - if (outputFile != null && outputFile.name.endsWith(".aar")) { - def fileName = "org.libsdl.app.aar"; - output.outputFile = new File(outputFile.parent, fileName); - } - } - } - } -} - -dependencies { - implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'androidx.core:core:1.7.0' // Use the latest version - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.documentfile:documentfile:1.0.1' - implementation 'androidx.appcompat:appcompat:1.6.1' -} - -task wrapper(type: Wrapper) { - gradleVersion = '8.9' -} - -task prepareKotlinBuildScriptModel { -} - - - - diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 000000000..5e7aa6d5e --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,237 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import java.security.MessageDigest +import java.util.Properties +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +val repositoryRoot: File = rootProject.projectDir.parentFile + +// One source of truth for the version: the CMake project declaration. +val projectVersion: String = + Regex("""project\s*\([^)]*VERSION\s+([0-9.]+)""") + .find(File(repositoryRoot, "CMakeLists.txt").readText()) + ?.groupValues?.get(1) + ?: "0.0.0" + +// Release signing: android/key.properties locally, the environment on CI. The +// file holds passwords, so it is gitignored and never read into the build +// output. +val keystoreProperties = Properties().apply { + val file = rootProject.file("key.properties") + if (file.isFile) file.inputStream().use { load(it) } +} + +// With neither configured — a fork's CI, or a fresh clone — the release build +// falls back to the debug key so it still produces an installable APK rather +// than failing on a keystore nobody has. +// +// Blank counts as absent: CI hands these down as secrets, and an unset secret +// arrives as an empty string rather than nothing at all. Testing only for null +// would take an empty value as a configured keystore and fail on file(""). +private fun String?.orAbsent(): String? = this?.takeIf(String::isNotBlank) + +val keystoreFromFile: String? = keystoreProperties.getProperty("storeFile").orAbsent() +val keystoreFromEnv: String? = System.getenv("KEYSTORE_FILE").orAbsent() +val hasReleaseKeystore = keystoreFromFile != null || keystoreFromEnv != null + +// Torch runs on the device to turn the user's ROM into sf64.o2r, and it reads +// its extraction recipes off the filesystem. They ride into the APK in one zip +// alongside the engine archive; the launcher unpacks it into the app's external +// files directory on first run (see GameAssets.kt). The digest beside it is what +// the launcher compares against to decide whether to unpack again. +abstract class PackGameData : DefaultTask() { + + // config.yml points Torch at "assets/yaml//", so these have to + // land under that same relative path on the device. + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val recipes: DirectoryProperty + + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val config: RegularFileProperty + + // A collection rather than an InputFile so that a missing archive reaches + // the explanation below instead of Gradle's own input-validation failure. + @get:InputFiles + @get:PathSensitive(PathSensitivity.NONE) + abstract val engineArchive: ConfigurableFileCollection + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @TaskAction + fun pack() { + val engine = engineArchive.files.singleOrNull()?.takeIf(File::isFile) + ?: throw GradleException( + "starship.o2r not found at ${engineArchive.asPath} — build the GeneratePortO2R " + + "target on a desktop platform (or download the CI artifact) before building the APK." + ) + + val recipesRoot = recipes.get().asFile + val entries = buildList { + add("config.yml" to config.get().asFile) + add(engine.name to engine) + recipesRoot.walkTopDown().filter(File::isFile).forEach { + add("assets/${it.relativeTo(recipesRoot).invariantSeparatorsPath}" to it) + } + }.sortedBy { it.first } + + val target = outputDir.get().asFile + target.mkdirs() + val zipFile = File(target, "gamedata.zip") + + // Sorted, at a fixed timestamp: a reproducible zip means a stable + // digest, which means an app update only re-unpacks on the device when + // the contents actually changed. + ZipOutputStream(zipFile.outputStream().buffered()).use { zip -> + entries.forEach { (name, file) -> + zip.putNextEntry(ZipEntry(name).apply { time = FIXED_ENTRY_TIME }) + file.inputStream().use { it.copyTo(zip) } + zip.closeEntry() + } + } + + val digest = MessageDigest.getInstance("MD5").digest(zipFile.readBytes()) + File(target, "gamedata.version").writeText(digest.joinToString("") { "%02x".format(it) }) + } + + private companion object { + // 1980-02-01T00:00:00Z, the same instant Gradle's own archive tasks use + // for this. Anything earlier risks falling before the 1980 floor that a + // zip entry's DOS timestamp can represent. + const val FIXED_ENTRY_TIME = 318211200000L + } +} + +// Wired through the variant API rather than added as a source-set srcDir. The +// srcDir form looks equivalent and is not: the asset merge takes the path but +// not the dependency on the task that fills it, so a clean checkout silently +// packages an APK whose assets are missing everything below — and the +// missing-archive check above never runs to say so. +androidComponents { + onVariants { variant -> + val packGameData = + tasks.register("pack${variant.name.replaceFirstChar(Char::titlecase)}GameData") { + description = "Bundles the Torch recipes and the engine archive into the APK." + recipes.set(File(repositoryRoot, "assets")) + config.set(File(repositoryRoot, "config.yml")) + engineArchive.from(File(repositoryRoot, "starship.o2r")) + } + variant.sources.assets?.addGeneratedSourceDirectory(packGameData, PackGameData::outputDir) + } +} + +android { + namespace = "com.starship.android" + compileSdk = 36 + ndkVersion = "30.0.15729638" + + defaultConfig { + applicationId = "com.starship.android" + minSdk = 24 + targetSdk = 36 + + // Overridable from the release workflow, which knows the tag it is + // cutting. Previously that workflow rewrote this file with sed, which + // broke the moment the file changed shape. + versionCode = (findProperty("starshipVersionCode") as String?)?.toInt() ?: 7 + versionName = (findProperty("starshipVersionName") as String?) ?: projectVersion + + ndk { + abiFilters += listOf("arm64-v8a", "armeabi-v7a") + } + + externalNativeBuild { + cmake { + arguments += listOf( + "-DANDROID_STL=c++_shared", + "-DUSE_OPENGLES=ON", + "-DSDL_SHARED=ON", + "-DSDL_STATIC=OFF", + "-DHAVE_LD_VERSION_SCRIPT=OFF" + ) + targets += "Starship" + } + } + } + + signingConfigs { + if (hasReleaseKeystore) { + create("release") { + if (keystoreFromFile != null) { + // A relative path in key.properties reads against the file's + // own directory, which is what someone editing it would + // expect. + storeFile = rootProject.file(keystoreFromFile) + storePassword = keystoreProperties.getProperty("storePassword") + keyAlias = keystoreProperties.getProperty("keyAlias") + keyPassword = keystoreProperties.getProperty("keyPassword") + } else { + storeFile = file(keystoreFromEnv!!) + storePassword = System.getenv("KEYSTORE_PASSWORD") + keyAlias = System.getenv("KEY_ALIAS") + keyPassword = System.getenv("KEY_PASSWORD") + } + } + } + } + + buildTypes { + debug { + isJniDebuggable = true + applicationIdSuffix = ".debug" + } + release { + // The APK is almost entirely native code and assets, so shrinking + // the tiny Kotlin layer buys nothing and only risks stripping the + // classes the JNI bridge looks up by name. + isMinifyEnabled = false + signingConfig = signingConfigs.getByName(if (hasReleaseKeystore) "release" else "debug") + ndk { + debugSymbolLevel = "SYMBOL_TABLE" + } + } + } + + externalNativeBuild { + cmake { + path = File(repositoryRoot, "CMakeLists.txt") + // libultraship needs CMake >= 3.24; the NDK-bundled 3.22.1 is too old. + version = "3.30.3+" + } + } + + // Universal APK: one artifact covering both ABIs rather than a split per ABI. + splits { + abi { + isEnable = false + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + lint { + abortOnError = false + } +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + +dependencies { + implementation("androidx.core:core-ktx:1.13.1") + implementation("androidx.activity:activity-ktx:1.9.3") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") +} diff --git a/android/app/gradle/wrapper/gradle-wrapper.jar b/android/app/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 7454180f2ae8848c63b8b4dea2cb829da983f2fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59536 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 QuEJmfE|#`Dqx|px0L@K;Y5)KL diff --git a/android/app/gradle/wrapper/gradle-wrapper.properties b/android/app/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index e750102e0..000000000 --- a/android/app/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/android/app/gradlew b/android/app/gradlew deleted file mode 100644 index 1b6c78733..000000000 --- a/android/app/gradlew +++ /dev/null @@ -1,234 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 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. -# - -############################################################################## -# -# 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/master/subprojects/plugins/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 - -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" -APP_BASE_NAME=${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 "$*" -} >&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 - -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" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - 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" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - 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 - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# 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/app/gradlew.bat b/android/app/gradlew.bat deleted file mode 100644 index 107acd32c..000000000 --- a/android/app/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@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/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 998c8ec25..f24bb1bf0 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,13 +1,9 @@ - - + - - - - - - - - - - - - + android:required="false" /> - + - - - - - - - - - - - - - - - + + + + + + + + + android:label="@string/app_name" + android:process=":launcher" + android:screenOrientation="sensorLandscape" + android:theme="@android:style/Theme.Material.NoActionBar"> + + + - - + android:name=".ModsActivity" + android:exported="false" + android:label="@string/mods_button" + android:screenOrientation="sensorLandscape" + android:theme="@android:style/Theme.Material.NoActionBar" /> + + diff --git a/android/app/src/main/java/com/starship/android/AssetCopyUtil.java b/android/app/src/main/java/com/starship/android/AssetCopyUtil.java deleted file mode 100644 index de32a6225..000000000 --- a/android/app/src/main/java/com/starship/android/AssetCopyUtil.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.starship.android; - -import android.content.Context; -import android.content.res.AssetManager; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -public class AssetCopyUtil { - - public static void copyAssetsToExternal(Context context, String assetsFolderPath, String externalFolderPath) throws IOException { - AssetManager assetManager = context.getAssets(); - String[] assetFiles = assetManager.list(assetsFolderPath); - - for (String assetFile : assetFiles) { - String assetPath = assetsFolderPath + File.separator + assetFile; - String externalPath = externalFolderPath + File.separator + assetFile; - - if (assetManager.list(assetPath).length > 0) { - // It's a directory - // Check if the directory exists in the external storage - File externalDir = new File(externalPath); - if (!externalDir.exists()) { - externalDir.mkdirs(); // Create the directory if it doesn't exist - } - - // Recursively copy contents of the directory - copyAssetsToExternal(context, assetPath, externalPath); - } else { - // It's a file - File externalFile = new File(externalPath); - if (!externalFile.exists()) { - // Check if the file exists in the external storage - InputStream in = null; - OutputStream out = null; - - try { - in = assetManager.open(assetPath); - out = new FileOutputStream(externalPath); - - byte[] buffer = new byte[1024]; - int read; - while ((read = in.read(buffer)) != -1) { - out.write(buffer, 0, read); - } - - } finally { - if (in != null) { - in.close(); - } - if (out != null) { - out.close(); - } - } - } - } - } - } - - public static void copyDirectory(File sourceDir, File targetDir) throws IOException { - if (!targetDir.exists()) { - targetDir.mkdirs(); - } - for (File file : sourceDir.listFiles()) { - File dest = new File(targetDir, file.getName()); - if (file.isDirectory()) { - copyDirectory(file, dest); - } else { - copyFile(file, dest); - } - } - } - - public static void copyFile(File source, File dest) throws IOException { - try (InputStream in = new FileInputStream(source); - OutputStream out = new FileOutputStream(dest)) { - byte[] buf = new byte[64*1024]; - int len; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); - } - } - } -} \ No newline at end of file diff --git a/android/app/src/main/java/com/starship/android/ControllerButtons.java b/android/app/src/main/java/com/starship/android/ControllerButtons.java deleted file mode 100644 index f0f2d6a47..000000000 --- a/android/app/src/main/java/com/starship/android/ControllerButtons.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.starship.android; - -public class ControllerButtons { - // Xbox Buttons - public static final int BUTTON_A = 0; - public static final int BUTTON_B = 1; - public static final int BUTTON_X = 2; - public static final int BUTTON_Y = 3; - public static final int BUTTON_LB = 9; // Left Bumper - public static final int BUTTON_RB = -5; // Right Bumper - public static final int BUTTON_BACK = 4; - public static final int BUTTON_START = 6; - public static final int BUTTON_MENU = 111; // Escape/Menu button (KeyEvent.KEYCODE_ESCAPE) - public static final int BUTTON_DPAD_UP = 11; - public static final int BUTTON_DPAD_DOWN = 12; - public static final int BUTTON_DPAD_LEFT = 13; - public static final int BUTTON_DPAD_RIGHT = 14; - - // Xbox Axis (Joysticks and Triggers) - public static final int AXIS_LX = 0; // Left stick X - public static final int AXIS_LY = 1; // Left stick Y - public static final int AXIS_RX = 2; // Right stick X - public static final int AXIS_RY = 3; // Right stick Y - public static final int AXIS_LT = -2; // Left Trigger (negative axis) - public static final int AXIS_RT = -4; // Right Trigger (negative axis) -} \ No newline at end of file diff --git a/android/app/src/main/java/com/starship/android/ControllerButtons.kt b/android/app/src/main/java/com/starship/android/ControllerButtons.kt new file mode 100644 index 000000000..abd74c549 --- /dev/null +++ b/android/app/src/main/java/com/starship/android/ControllerButtons.kt @@ -0,0 +1,27 @@ +package com.starship.android + +/** + * Indices into the virtual SDL game controller created by the native bridge. + * + * They follow SDL's own `SDL_GameControllerButton` / `SDL_GameControllerAxis` + * ordering. A negative button id addresses the axis with that (positive) index + * instead, which is how the shoulder triggers are driven from a plain on/off + * button in the overlay. + */ +object ControllerButtons { + const val BUTTON_A = 0 + const val BUTTON_B = 1 + const val BUTTON_X = 2 + const val BUTTON_Y = 3 + const val BUTTON_BACK = 4 + const val BUTTON_START = 6 + const val BUTTON_LB = 9 + const val BUTTON_RB = -5 + + const val AXIS_LX = 0 + const val AXIS_LY = 1 + const val AXIS_RX = 2 + const val AXIS_RY = 3 + const val AXIS_LT = -2 + const val AXIS_RT = -4 +} diff --git a/android/app/src/main/java/com/starship/android/DialogActivity.java b/android/app/src/main/java/com/starship/android/DialogActivity.java deleted file mode 100644 index dc7f38131..000000000 --- a/android/app/src/main/java/com/starship/android/DialogActivity.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.starship.android; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.Intent; -import android.os.Bundle; - -public class DialogActivity extends Activity { - public static final String EXTRA_TITLE = "title"; - public static final String EXTRA_MESSAGE = "message"; - public static final String EXTRA_POSITIVE_BUTTON = "positive_button"; - public static final String EXTRA_NEGATIVE_BUTTON = "negative_button"; - public static final String EXTRA_CANCELABLE = "cancelable"; - public static final String EXTRA_DIALOG_TYPE = "dialog_type"; - - public static final int DIALOG_TYPE_FOLDER_PROMPT = 1; - public static final int DIALOG_TYPE_FILE_NOT_FOUND = 2; - public static final int DIALOG_TYPE_COPY_COMPLETE = 3; - public static final int DIALOG_TYPE_FILE_READY = 4; - - public static final int RESULT_POSITIVE = RESULT_OK; - public static final int RESULT_NEGATIVE = RESULT_CANCELED; - public static final int RESULT_FOLDER_PICKER = 100; - public static final int RESULT_TORCH_DOWNLOAD = 101; - public static final int RESULT_FILE_PICKER = 102; - public static final int RESULT_RESTART = 103; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - Intent intent = getIntent(); - String title = intent.getStringExtra(EXTRA_TITLE); - String message = intent.getStringExtra(EXTRA_MESSAGE); - String positiveButton = intent.getStringExtra(EXTRA_POSITIVE_BUTTON); - String negativeButton = intent.getStringExtra(EXTRA_NEGATIVE_BUTTON); - boolean cancelable = intent.getBooleanExtra(EXTRA_CANCELABLE, true); - int dialogType = intent.getIntExtra(EXTRA_DIALOG_TYPE, 0); - - AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.RoundedDialog); - builder.setTitle(title); - builder.setMessage(message); - builder.setCancelable(cancelable); - - // Set up buttons based on dialog type - switch (dialogType) { - case DIALOG_TYPE_FOLDER_PROMPT: - builder.setPositiveButton("Select Folder", (d, w) -> { - setResult(RESULT_FOLDER_PICKER); - finish(); - }); - break; - - case DIALOG_TYPE_FILE_NOT_FOUND: - builder.setPositiveButton("Download Torch App", (d, w) -> { - android.util.Log.i("DialogActivity", "Torch download button pressed"); - setResult(RESULT_TORCH_DOWNLOAD); - finish(); - }); - builder.setNegativeButton("Select sf64.o2r File", (d, w) -> { - android.util.Log.i("DialogActivity", "File picker button pressed"); - setResult(RESULT_FILE_PICKER); - finish(); - }); - break; - - case DIALOG_TYPE_COPY_COMPLETE: - builder.setPositiveButton("Restart", (d, w) -> { - setResult(RESULT_RESTART); - finish(); - }); - builder.setNegativeButton("Later", (d, w) -> { - setResult(RESULT_NEGATIVE); - finish(); - }); - break; - - case DIALOG_TYPE_FILE_READY: - builder.setPositiveButton("Restart", (d, w) -> { - setResult(RESULT_RESTART); - finish(); - }); - break; - - default: - builder.setPositiveButton(positiveButton != null ? positiveButton : "OK", (d, w) -> { - setResult(RESULT_POSITIVE); - finish(); - }); - if (negativeButton != null) { - builder.setNegativeButton(negativeButton, (d, w) -> { - setResult(RESULT_NEGATIVE); - finish(); - }); - } - break; - } - - AlertDialog dialog = builder.create(); - dialog.setOnDismissListener(d -> finish()); - dialog.show(); - } -} \ No newline at end of file diff --git a/android/app/src/main/java/com/starship/android/GameAssets.kt b/android/app/src/main/java/com/starship/android/GameAssets.kt new file mode 100644 index 000000000..80441ef7e --- /dev/null +++ b/android/app/src/main/java/com/starship/android/GameAssets.kt @@ -0,0 +1,186 @@ +package com.starship.android + +import android.content.Context +import android.util.Log +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.security.MessageDigest +import java.util.zip.ZipInputStream + +/** + * Everything the game needs on disk, and how it gets there. + * + * libultraship resolves every runtime path on Android through + * `SDL_AndroidGetExternalStoragePath()`, i.e. `getExternalFilesDir(null)`. That + * is also somewhere the user can reach with a file manager, which is what makes + * dropping mods in possible, so everything lives there: + * + * Android/data/com.starship.android/files/ + * baserom.us.rev1.z64 the user's ROM, copied in by the launcher + * config.yml, assets/ Torch's extraction recipes, unpacked from the APK + * starship.o2r engine assets, built on CI and shipped in the APK + * sf64.o2r generated on-device from the ROM + * mods/ user mods (.o2r, .zip or plain folders) + * + * The unpacking happens here rather than inside the engine because Torch needs + * config.yml and assets/ on disk before the game process starts, not after. + * The zip itself is assembled by android/app/build.gradle.kts. + */ +object GameAssets { + + // Named for the engine's own mobile fallback (GameExtractor.cpp), so a + // re-extraction triggered from inside the game finds it too. Torch hashes + // the contents, so the name says nothing about which revision it holds. + const val ROM_NAME = "baserom.us.rev1.z64" + + /** + * The ROMs that yield sf64.o2r, by SHA-1. + * + * Only the US revisions are here on purpose. config.yml routes JP, EU and CN + * to `mods/sf64jp.o2r` and friends — those are language overlays layered on + * top of the US archive, not a base game — so extracting one of them would + * finish without ever producing the archive the engine boots from. + */ + val SUPPORTED_ROMS = mapOf( + "09f0d105f476b00efa5303a3ebc42e60a7753b7a" to "Star Fox 64 (U) (V1.1)", + "f7475fb11e7e6830f82883412638e8390791ab87" to "Star Fox 64 (U) (V1.1) (Uncompressed)", + "d8b1088520f7c5f81433292a9258c1184afa1457" to "Star Fox 64 (U) (V1.0)", + "63b69f0ef36306257481afc250f9bc304c7162b2" to "Star Fox 64 (U) (V1.0) (Uncompressed)" + ) + + private const val TAG = "GameAssets" + private const val GAME_ARCHIVE = "sf64.o2r" + private const val CONTROLLER_DB = "gamecontrollerdb.txt" + private const val TORCH_HASHES = "torch.hash.yml" + private const val GAMEDATA_ZIP = "gamedata.zip" + private const val GAMEDATA_VERSION = "gamedata.version" + private const val STAMP = ".gamedata.version" + + fun gameDir(context: Context): File = + (context.getExternalFilesDir(null) ?: context.filesDir).also { it.mkdirs() } + + fun romFile(context: Context) = File(gameDir(context), ROM_NAME) + + fun gameArchive(context: Context) = File(gameDir(context), GAME_ARCHIVE) + + fun modsDir(context: Context) = File(gameDir(context), "mods") + + fun isExtracted(context: Context) = gameArchive(context).length() > 0 + + /** + * Unpacks the APK-bundled extraction inputs into [gameDir]. + * + * Re-runs whenever the packaged zip changes, so an app update ships new + * recipes and a new starship.o2r without the user clearing data. Returns an + * error message, or null when the directory is ready. + */ + fun stageBundledAssets(context: Context): String? { + val target = gameDir(context) + val stamp = File(target, STAMP) + + return try { + val expected = context.assets.open(GAMEDATA_VERSION).use { it.readBytes().decodeToString() } + if (stamp.takeIf { it.isFile }?.readText() == expected) { + return null + } + + context.assets.open(GAMEDATA_ZIP).use { input -> + ZipInputStream(input.buffered()).use { zip -> + while (true) { + val entry = zip.nextEntry ?: break + val destination = File(target, entry.name).canonicalFile + + // An entry named ../../something would otherwise write + // outside the game directory. + if (!destination.path.startsWith(target.canonicalPath + File.separator)) { + Log.w(TAG, "Skipping suspicious zip entry ${entry.name}") + zip.closeEntry() + continue + } + + if (entry.isDirectory) { + destination.mkdirs() + } else { + destination.parentFile?.mkdirs() + FileOutputStream(destination).use { output -> zip.copyTo(output, COPY_BUFFER_BYTES) } + } + zip.closeEntry() + } + } + } + + // Users are meant to edit this one, so only seed it once. + val controllerDb = File(target, CONTROLLER_DB) + if (!controllerDb.isFile) { + copyAsset(context, CONTROLLER_DB, controllerDb) + } + + val mods = modsDir(context) + mods.mkdirs() + if (mods.list().isNullOrEmpty()) { + // Only a hint for the player, and nothing reads it back, so it + // must not fail an extraction that has otherwise succeeded. + try { + copyAsset(context, "mods/place_mods_here.txt", File(mods, "place_mods_here.txt")) + } catch (error: IOException) { + Log.w(TAG, "Could not write the mods placeholder", error) + } + } + + // New recipes mean the previous archive is stale, and Torch skips + // work whose inputs it believes are unchanged. + gameArchive(context).delete() + File(target, TORCH_HASHES).delete() + + stamp.writeText(expected) + null + } catch (error: IOException) { + Log.e(TAG, "Could not unpack the bundled assets", error) + "Could not unpack the bundled game files: ${error.message}" + } + } + + /** + * Runs Torch over [romFile] to produce sf64.o2r. Blocking and slow — expect + * a minute or more on older hardware. Returns an error message, or null on + * success. + */ + fun generateGameArchive(context: Context): String? { + val dir = gameDir(context) + return nativeGenerateGameArchive(romFile(context).absolutePath, dir.absolutePath, dir.absolutePath) + } + + fun sha1Of(file: File): String { + val digest = MessageDigest.getInstance("SHA-1") + file.inputStream().use { input -> + val buffer = ByteArray(COPY_BUFFER_BYTES) + while (true) { + val read = input.read(buffer) + if (read < 0) break + digest.update(buffer, 0, read) + } + } + return digest.digest().joinToString("") { "%02x".format(it) } + } + + private const val COPY_BUFFER_BYTES = 1 shl 17 + + private fun copyAsset(context: Context, assetPath: String, target: File) { + target.parentFile?.mkdirs() + context.assets.open(assetPath).use { input -> + FileOutputStream(target).use { output -> + input.copyTo(output) + output.fd.sync() + } + } + } + + private external fun nativeGenerateGameArchive(rom: String, sourceDir: String, destDir: String): String? + + init { + // libStarship.so carries both the game and the Torch extractor. + System.loadLibrary("SDL2") + System.loadLibrary("Starship") + } +} diff --git a/android/app/src/main/java/com/starship/android/LauncherActivity.kt b/android/app/src/main/java/com/starship/android/LauncherActivity.kt new file mode 100644 index 000000000..a37873dfd --- /dev/null +++ b/android/app/src/main/java/com/starship/android/LauncherActivity.kt @@ -0,0 +1,212 @@ +package com.starship.android + +import android.content.Intent +import android.graphics.Color +import android.net.Uri +import android.os.Bundle +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.Button +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.ScrollView +import android.widget.TextView +import androidx.activity.ComponentActivity +import androidx.activity.result.contract.ActivityResultContracts +import java.io.File +import java.io.FileOutputStream +import kotlin.concurrent.thread + +/** + * First-run flow: unpack the bundled game files, take a ROM from the user, and + * turn it into sf64.o2r before handing off to the game. + * + * No game data is shipped in the APK — the ROM stays on the device and the + * extraction runs here, on demand. + */ +class LauncherActivity : ComponentActivity() { + + private lateinit var status: TextView + private lateinit var chooseButton: Button + private lateinit var progress: ProgressBar + + private val pickRom = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? -> + if (uri != null) importRom(uri) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + buildUi() + prepare() + } + + /** + * Unpacks the bundled game files, then decides what still has to happen: + * nothing (start the game), extract an already-imported ROM, or ask for one. + * + * The unpacking moves megabytes out of the APK, so it cannot run on the main + * thread. + */ + private fun prepare() { + setBusy(true, getString(R.string.launcher_preparing)) + + thread(name = "sf64-prepare") { + val stagingError = GameAssets.stageBundledAssets(this) + + runOnUiThread { + when { + // Nothing else will work without the bundled files, so + // leave the button disabled rather than invite a retry. + stagingError != null -> { + setBusy(false, stagingError) + chooseButton.isEnabled = false + } + GameAssets.isExtracted(this) -> startGame() + GameAssets.romFile(this).isFile -> extractRom() + else -> setBusy(false, romInstructions) + } + } + } + } + + private fun buildUi() { + val padding = (24 * resources.displayMetrics.density).toInt() + val root = LinearLayout(this).apply { + orientation = LinearLayout.VERTICAL + gravity = Gravity.CENTER + setPadding(padding, padding, padding, padding) + setBackgroundColor(Color.rgb(8, 16, 28)) + } + + status = TextView(this).apply { + setTextColor(Color.WHITE) + textSize = 16f + gravity = Gravity.CENTER + } + chooseButton = Button(this).apply { + text = getString(R.string.launcher_choose_rom) + setOnClickListener { pickRom.launch(arrayOf("*/*")) } + } + progress = ProgressBar(this).apply { visibility = View.GONE } + + // The failure messages can run long, and the launcher is fixed to + // landscape, so let the text scroll rather than push the button away. + val scroller = ScrollView(this).apply { + addView( + status, + ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + ) + } + + root.addView(scroller, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f)) + root.addView(chooseButton) + root.addView(progress) + setContentView(root) + root.padForSystemBars() + } + + private fun importRom(uri: Uri) { + setBusy(true, getString(R.string.launcher_verifying)) + + thread(name = "sf64-rom-import") { + val result = runCatching { + val target = GameAssets.romFile(this) + val partial = File(target.parentFile, "${GameAssets.ROM_NAME}.part") + partial.delete() + + contentResolver.openInputStream(uri).use { input -> + requireNotNull(input) { "The document provider did not open the file." } + FileOutputStream(partial).use { output -> + input.copyTo(output, COPY_BUFFER_BYTES) + output.fd.sync() + } + } + + val sha1 = GameAssets.sha1Of(partial) + if (sha1 !in GameAssets.SUPPORTED_ROMS) { + partial.delete() + error( + "That is not a supported Star Fox 64 ROM.\n\n" + + "Supported:\n" + + GameAssets.SUPPORTED_ROMS.entries.joinToString("\n") { " ${it.value} — ${it.key}" } + + "\n\nGot $sha1" + ) + } + + if (target.exists() && !target.delete()) { + error("Could not replace the previous ROM.") + } + check(partial.renameTo(target)) { "Could not store the ROM in the app's files directory." } + } + + runOnUiThread { + result.fold( + onSuccess = { extractRom() }, + onFailure = { + setBusy(false, "${it.message ?: it}\n\n$romInstructions") + } + ) + } + } + } + + private fun extractRom() { + setBusy(true, getString(R.string.launcher_extracting)) + + thread(name = "sf64-extract") { + val error = GameAssets.generateGameArchive(this) + + runOnUiThread { + if (error == null) { + startGame() + } else { + setBusy(false, "$error\n\n$romInstructions") + } + } + } + } + + /** + * Both long-running steps keep the screen awake: the extraction has no + * progress to report and can outlast the display timeout, and being killed + * part-way through leaves a truncated archive behind. + */ + private fun setBusy(busy: Boolean, message: String) { + status.text = message + chooseButton.isEnabled = !busy + progress.visibility = if (busy) View.VISIBLE else View.GONE + if (busy) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + + private fun startGame() { + startActivity(Intent(this, MainActivity::class.java)) + finish() + + // This activity has a process to itself and the game runs in the app's + // main one, so ending this one costs the game nothing and hands back + // everything the launcher touched: after an extraction that is Torch + // still holding the ROM and every decoded asset, and otherwise it is + // still the game library mapped in just to reach that one JNI call. + // startActivity() has already reached the activity manager by this + // point, so the game starts either way. + Runtime.getRuntime().exit(0) + } + + /** Spelled out from the real path so debug builds don't point at the release one. */ + private val romInstructions: String + get() = "Select your own Star Fox 64 (US) .z64 ROM. It is verified, kept in this app's private " + + "storage and converted into the game's asset archive on this device — no game data ships " + + "with the app.\n\n" + + "Mods go in ${GameAssets.modsDir(this)}." + + private companion object { + const val COPY_BUFFER_BYTES = 1 shl 17 + } +} diff --git a/android/app/src/main/java/com/starship/android/MainActivity.java b/android/app/src/main/java/com/starship/android/MainActivity.java deleted file mode 100644 index 967047b91..000000000 --- a/android/app/src/main/java/com/starship/android/MainActivity.java +++ /dev/null @@ -1,1065 +0,0 @@ -package com.starship.android; - -import org.libsdl.app.SDLActivity; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.Bundle; -import android.os.Environment; -import android.provider.DocumentsContract; -import android.util.Log; -import android.widget.Toast; -import android.app.AlertDialog; -import java.io.File; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.IOException; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.util.concurrent.CountDownLatch; -import androidx.documentfile.provider.DocumentFile; - -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.FrameLayout; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.widget.ImageView; -import android.view.KeyEvent; -import android.content.pm.ActivityInfo; -import android.os.Build; -import android.view.WindowInsets; -import android.view.WindowInsetsController; - -public class MainActivity extends SDLActivity { -static { - System.loadLibrary("Starship"); -} - -// ===== Constants / Prefs ===== -private static final String PREFS = "com.starship.android.prefs"; -private static final String KEY_USER_FOLDER_URI = "user_folder_uri"; -private static final String TAG = "MainActivity"; -private static final int REQ_PICK_FOLDER = 1001; -private static final int REQ_PICK_SF64 = 1002; - -// ===== State ===== -SharedPreferences preferences; -private static final CountDownLatch setupLatch = new CountDownLatch(1); -private Uri userFolderUri; // Persisted SAF tree URI - -// ===== Native methods ===== -public native void attachController(); -public native void detachController(); -public native void setButton(int button, boolean value); -public native void setCameraState(int axis, float value); -public native void setAxis(int axis, short value); - -// ===== Save dir for the engine (internal only; no extra subfolder) ===== -public static String getSaveDir() { - Context ctx = SDLActivity.getContext(); - File internal = ctx.getFilesDir(); // /data/data//files - if (!internal.exists()) internal.mkdirs(); - Log.i(TAG, "getSaveDir -> " + internal.getAbsolutePath()); - return internal.getAbsolutePath(); -} - -private Uri getUserFolderUri() { - String s = preferences.getString(KEY_USER_FOLDER_URI, null); - return (s != null) ? Uri.parse(s) : null; -} - -private void setUserFolderUri(Uri uri) { - preferences.edit().putString(KEY_USER_FOLDER_URI, uri != null ? uri.toString() : null).apply(); -} - -private String getUserFolderPath() { - if (userFolderUri == null) return null; - try { - String treeId = DocumentsContract.getTreeDocumentId(userFolderUri); - // Check if it's external storage - if (treeId.startsWith("primary:")) { - String relativePath = treeId.substring("primary:".length()); - File externalStorage = Environment.getExternalStorageDirectory(); - return new File(externalStorage, relativePath).getAbsolutePath(); - } - Log.w(TAG, "Could not extract path from URI: " + userFolderUri); - return null; - } catch (Exception e) { - Log.e(TAG, "Error extracting path from URI", e); - return null; - } -} - -private void syncModsFromUserFolder() { - try { - DocumentFile userRoot = DocumentFile.fromTreeUri(this, userFolderUri); - if (userRoot == null) return; - - DocumentFile userModsFolder = userRoot.findFile("mods"); - if (userModsFolder == null || !userModsFolder.isDirectory()) { - Log.i(TAG, "No mods folder found in user directory"); - return; - } - - // Prefer external app-specific storage so the engine (which uses SDL_AndroidGetExternalStoragePath) - // can discover mods at /Android/data//files/mods. Fallback to internal if external is unavailable. - File targetRoot = getExternalFilesDir(null); - if (targetRoot == null) { - Log.w(TAG, "External files dir is null, falling back to internal files dir for mods"); - targetRoot = getFilesDir(); - } - File targetModsFolder = new File(targetRoot, "mods"); - if (!targetModsFolder.exists()) { - targetModsFolder.mkdirs(); - } - - // Clear existing mods in target storage first - clearDirectory(targetModsFolder); - - // Copy all files from user mods folder to target mods folder - copyModsRecursively(userModsFolder, targetModsFolder); - Log.i(TAG, "Mods synced from user folder to: " + targetModsFolder.getAbsolutePath()); - } catch (Exception e) { - Log.e(TAG, "Error syncing mods from user folder", e); - } -} - -private void clearDirectory(File dir) { - if (dir.exists() && dir.isDirectory()) { - File[] files = dir.listFiles(); - if (files != null) { - for (File file : files) { - if (file.isDirectory()) { - clearDirectory(file); - } - file.delete(); - } - } - } -} - -private void copyModsRecursively(DocumentFile sourceDir, File destDir) { - try { - DocumentFile[] files = sourceDir.listFiles(); - if (files == null) return; - - for (DocumentFile file : files) { - String fileName = file.getName(); - if (fileName == null) continue; - - File destFile = new File(destDir, fileName); - if (file.isDirectory()) { - // Create directory and recurse - destFile.mkdirs(); - copyModsRecursively(file, destFile); - } else { - // Copy file - try (InputStream in = getContentResolver().openInputStream(file.getUri()); - FileOutputStream out = new FileOutputStream(destFile)) { - byte[] buf = new byte[8192]; - int r; - while ((r = in.read(buf)) != -1) { - out.write(buf, 0, r); - } - out.flush(); - Log.i(TAG, "Copied mod file: " + fileName); - } catch (IOException e) { - Log.e(TAG, "Failed to copy mod file: " + fileName, e); - } - } - } - } catch (Exception e) { - Log.e(TAG, "Error in copyModsRecursively", e); - } -} - -// ===== Lifecycle ===== -@Override -protected void onCreate(Bundle savedInstanceState) { - preferences = getSharedPreferences(PREFS, Context.MODE_PRIVATE); - userFolderUri = getUserFolderUri(); - - super.onCreate(savedInstanceState); - - // Enable immersive mode (edge-to-edge) - enableImmersiveMode(); - -setupControllerOverlay(); - attachController(); - - // Seed internal directory with assets if they exist (optional) - seedInternalFromAssetsIfPresent(); - - File internal = getFilesDir(); - File external = getExternalFilesDir(null); - Log.i(TAG, "Internal root: " + internal); - Log.i(TAG, "External root: " + external); - - // Check for sf64.o2r in internal storage first - File internalSf64 = new File(internal, "sf64.o2r"); - - // If not in internal, check if it exists in user's chosen folder and copy it - if (!internalSf64.exists() && userFolderUri != null) { - DocumentFile userRoot = DocumentFile.fromTreeUri(this, userFolderUri); - if (userRoot != null) { - DocumentFile userSf64 = userRoot.findFile("sf64.o2r"); - if (userSf64 != null && userSf64.exists()) { - Log.i(TAG, "Found sf64.o2r in user folder, copying to internal storage"); - try (InputStream in = getContentResolver().openInputStream(userSf64.getUri()); - FileOutputStream out = new FileOutputStream(internalSf64)) { - byte[] buf = new byte[8192]; - int r; - while ((r = in.read(buf)) != -1) { - out.write(buf, 0, r); - } - out.flush(); - out.getFD().sync(); - Log.i(TAG, "sf64.o2r copied from user folder to internal storage"); - -} catch (IOException e) { - Log.e(TAG, "Failed to copy sf64.o2r from user folder", e); - } - } - } - } - - // Always sync mods folder from user's chosen folder to internal storage - if (userFolderUri != null) { - syncModsFromUserFolder(); - } - - // Now check if sf64.o2r exists in internal storage - if (!internalSf64.exists()) { - Log.i(TAG, "sf64.o2r not found. Prompting for folder."); - if (userFolderUri == null) { - // First time - need to select folder - promptForUserFolder(); - } else { - // Folder already selected but no ROM - show file not found dialog - showPortraitDialog("sf64.o2r not found in selected folder", - "Pick an existing sf64.o2r file or use Torch to create one. It will be copied to your selected folder.", - DialogActivity.DIALOG_TYPE_FILE_NOT_FOUND); - } - } else { - Log.i(TAG, "sf64.o2r found in internal storage, game should start normally."); - } - - // Signal to C++ that setup is complete - setupLatch.countDown(); -} - -public static void waitForSetupFromNative() { - try { - setupLatch.await(); - } catch (InterruptedException ignored) { - Log.w(TAG, "waitForSetupFromNative interrupted"); - } -} - -// ===== Asset seeding (optional, safe if assets not present) ===== -private boolean assetExists(String name) { - try { - getAssets().open(name).close(); - return true; - } catch (IOException e) { - return false; - } -} - -private boolean assetDirExists(String dir) { - try { - String[] list = getAssets().list(dir); - return list != null && list.length > 0; - } catch (IOException e) { - return false; - } -} - -private void seedInternalFromAssetsIfPresent() { - File internal = getFilesDir(); - - // Prefer the real name: gamecontrollerdb.txt (but accept controllerdb.txt asset if that's what you ship) - File gcdb = new File(internal, "gamecontrollerdb.txt"); - File cdb = new File(internal, "controllerdb.txt"); - if (!gcdb.exists() && !cdb.exists()) { - if (assetExists("gamecontrollerdb.txt")) { - copyAssetFile("gamecontrollerdb.txt", gcdb); - } else if (assetExists("controllerdb.txt")) { - copyAssetFile("controllerdb.txt", cdb); - } else { - Log.i(TAG, "No controller DB asset shipped."); - } - } - - File starship = new File(internal, "starship.o2r"); - Log.i(TAG, "Checking starship.o2r - exists in internal: " + starship.exists() + ", exists in assets: " + assetExists("starship.o2r")); - if (!starship.exists() && assetExists("starship.o2r")) { - Log.i(TAG, "Copying starship.o2r from assets to internal"); - copyAssetFile("starship.o2r", starship); - } else if (!starship.exists()) { - Log.w(TAG, "starship.o2r not found in assets - this might be expected for development builds"); - // List available assets for debugging - try { - String[] assets = getAssets().list(""); - Log.i(TAG, "Available assets:"); - for (String asset : assets) { - Log.i(TAG, " - " + asset); - } - } catch (IOException e) { - Log.e(TAG, "Error listing assets", e); - } - } - - File modsDir = new File(internal, "mods"); - if (!modsDir.exists() && assetDirExists("mods")) { - copyAssetFolderRecursive("mods", modsDir); - } -} - -private void copyAssetFile(String assetName, File destFile) { - try { - File parent = destFile.getParentFile(); - if (parent != null && !parent.exists()) parent.mkdirs(); - - try (InputStream in = getAssets().open(assetName); - FileOutputStream out = new FileOutputStream(destFile)) { - byte[] buffer = new byte[8192]; - int read; - long total = 0; - while ((read = in.read(buffer)) != -1) { - out.write(buffer, 0, read); - total += read; - } - out.flush(); - out.getFD().sync(); - Log.i(TAG, "Seeded asset " + assetName + " (" + total + " bytes) -> " + destFile.getAbsolutePath()); - } - } catch (IOException e) { - Log.e(TAG, "copyAssetFile failed for " + assetName, e); - } -} - -private void copyAssetFolderRecursive(String assetDir, File destDir) { - try { - if (!destDir.exists()) destDir.mkdirs(); - - String[] kids = getAssets().list(assetDir); - if (kids == null) return; - - for (String name : kids) { - String assetPath = assetDir + "/" + name; - String[] sub = getAssets().list(assetPath); - if (sub != null && sub.length > 0) { - copyAssetFolderRecursive(assetPath, new File(destDir, name)); - } else { - copyAssetFile(assetPath, new File(destDir, name)); - } - } - Log.i(TAG, "Seeded asset dir " + assetDir + " -> " + destDir.getAbsolutePath()); - } catch (IOException e) { - Log.e(TAG, "copyAssetFolderRecursive failed for " + assetDir, e); - } -} - -// ===== UI helpers ===== -private void showPortraitDialog(String title, String message, int dialogType) { - Intent intent = new Intent(this, DialogActivity.class); - intent.putExtra(DialogActivity.EXTRA_TITLE, title); - intent.putExtra(DialogActivity.EXTRA_MESSAGE, message); - intent.putExtra(DialogActivity.EXTRA_DIALOG_TYPE, dialogType); - intent.putExtra(DialogActivity.EXTRA_CANCELABLE, false); - startActivityForResult(intent, dialogType); -} - -private void showToast(String msg) { - runOnUiThread(() -> Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()); -} - -private void restartApp() { - try { - Intent i = getPackageManager().getLaunchIntentForPackage(getPackageName()); - if (i != null) { - i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(i); - finish(); - System.exit(0); - } - } catch (Exception e) { - Log.e(TAG, "restartApp", e); - showToast("Please restart the app manually"); - } -} - -// ===== Folder / File pickers ===== -private void promptForUserFolder() { - showPortraitDialog("Choose Your Folder", - "Select a folder for your sf64.o2r file and mods location. This will be your main Starship folder where you can add mods and manage game files.", - DialogActivity.DIALOG_TYPE_FOLDER_PROMPT); -} - -public void openFolderPicker() { - Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION - | Intent.FLAG_GRANT_WRITE_URI_PERMISSION - | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION - | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); - startActivityForResult(i, REQ_PICK_FOLDER); -} - -public void openFilePickerForSf64() { - Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT); - i.setType("*/*"); - i.addCategory(Intent.CATEGORY_OPENABLE); - i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION - | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); - startActivityForResult(i, REQ_PICK_SF64); -} - -private void openTorchDownload() { - try { - Intent browser = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/izzy2lost/Torch/releases")); - Intent chooser = Intent.createChooser(browser, "Open Torch download page with:"); - chooser.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(chooser); - showToast("Opening Torch download. App will close so you can install Torch."); - - // Close the app after a short delay so user can install Torch - new android.os.Handler(getMainLooper()).postDelayed(() -> { - finish(); - System.exit(0); - }, 2000); - } catch (Exception e) { - Log.e(TAG, "openTorchDownload", e); - showToast("Visit: https://github.com/izzy2lost/Torch/releases"); - } -} - -// ===== Activity result ===== -@Override -protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - Log.i(TAG, "onActivityResult: requestCode=" + requestCode + ", resultCode=" + resultCode); - - // Handle DialogActivity results - if (requestCode == DialogActivity.DIALOG_TYPE_FOLDER_PROMPT && resultCode == DialogActivity.RESULT_FOLDER_PICKER) { - Log.i(TAG, "Opening folder picker"); - openFolderPicker(); - return; - } - if (requestCode == DialogActivity.DIALOG_TYPE_FILE_NOT_FOUND) { - if (resultCode == DialogActivity.RESULT_TORCH_DOWNLOAD) { - Log.i(TAG, "Opening Torch download"); - openTorchDownload(); - } else if (resultCode == DialogActivity.RESULT_FILE_PICKER) { - Log.i(TAG, "Opening file picker for sf64"); - openFilePickerForSf64(); - } - return; - } - if ((requestCode == DialogActivity.DIALOG_TYPE_COPY_COMPLETE || requestCode == DialogActivity.DIALOG_TYPE_FILE_READY) - && resultCode == DialogActivity.RESULT_RESTART) { - restartApp(); - return; - } - - // Handle file/folder picker results - if (resultCode != RESULT_OK || data == null) return; - - if (requestCode == REQ_PICK_FOLDER) { - handleFolderSelection(data.getData(), data.getFlags()); - } else if (requestCode == REQ_PICK_SF64) { - handleRomFileSelection(data.getData()); - } -} - -// ===== Folder selection & copy (SAF) ===== -private void handleFolderSelection(Uri treeUri, int returnedFlags) { - if (treeUri == null) { - showToast("No folder selected."); - return; - } - - // Persist read/write — try returned flags; if 0, try explicit - try { - final int permsReturned = returnedFlags & - (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - if (permsReturned != 0) { - getContentResolver().takePersistableUriPermission(treeUri, permsReturned); - } else { - getContentResolver().takePersistableUriPermission(treeUri, - Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - } - } catch (Exception e) { - Log.w(TAG, "takePersistableUriPermission failed (continuing with transient perms): " + e); - } - - setUserFolderUri(treeUri); - userFolderUri = treeUri; - showToast("Folder selected."); - - DocumentFile userRoot = DocumentFile.fromTreeUri(this, treeUri); - if (userRoot == null) { - showToast("Cannot access the selected folder."); - return; - } - - if (!verifyWritable(userRoot)) { - showToast("Can't write there. Try Downloads or a folder you create under Internal storage."); - return; - } - - boolean anyCopied = copyFromBestSourceToSaf(userRoot); - - // Check if sf64.o2r exists in the user's chosen folder and copy it to internal storage - DocumentFile sf64InUserFolder = userRoot.findFile("sf64.o2r"); - if (sf64InUserFolder != null && sf64InUserFolder.exists()) { - // Copy sf64.o2r to internal storage - File internalSf64 = new File(getFilesDir(), "sf64.o2r"); - try (InputStream in = getContentResolver().openInputStream(sf64InUserFolder.getUri()); - FileOutputStream out = new FileOutputStream(internalSf64)) { - byte[] buf = new byte[8192]; - int r; - while ((r = in.read(buf)) != -1) { - out.write(buf, 0, r); - } - out.flush(); - out.getFD().sync(); - Log.i(TAG, "sf64.o2r copied from user folder to internal storage during folder selection"); - - -// Also sync mods - syncModsFromUserFolder(); - - showToast("Files copied. Game should start now."); - } catch (IOException e) { - Log.e(TAG, "Failed to copy sf64.o2r during folder selection", e); - showToast("Failed to copy sf64.o2r: " + e.getMessage()); - } - } else { - runOnUiThread(() -> showPortraitDialog("sf64.o2r not found in selected folder", - "Pick an existing sf64.o2r file or use Torch to create one. It will be copied to your selected folder.", - DialogActivity.DIALOG_TYPE_FILE_NOT_FOUND)); - } -} - -private boolean verifyWritable(DocumentFile dir) { - try { - DocumentFile tmp = dir.createFile("application/octet-stream", ".saf_write_test"); - if (tmp == null) return false; - OutputStream os = getContentResolver().openOutputStream(tmp.getUri(), "w"); - if (os == null) return false; - os.write(1); - os.flush(); - os.close(); - tmp.delete(); - return true; - } catch (Exception e) { - Log.w(TAG, "verifyWritable failed: " + e); - return false; - } -} - -// Choose best source root: -// 1) Internal files dir -// 2) External files dir root (legacy) -// 3) External files dir /Starship (legacy leftover) -private File chooseBestSourceRoot() { - File internal = getFilesDir(); - File external = getExternalFilesDir(null); - File legacy = (external != null) ? new File(external, "Starship") : null; - - boolean hasInternal = hasAnyTarget(internal); - boolean hasExternal = external != null && hasAnyTarget(external); - boolean hasLegacy = legacy != null && hasAnyTarget(legacy); - - Log.i(TAG, "Source check -> internal=" + hasInternal + ", external=" + hasExternal + ", legacy=" + hasLegacy); - - if (hasInternal) return internal; - if (hasExternal) return external; - if (hasLegacy) return legacy; - return internal; // default (will report not found later) -} - -private boolean hasAnyTarget(File root) { - if (root == null) return false; - if (new File(root, "gamecontrollerdb.txt").exists()) return true; - if (new File(root, "controllerdb.txt").exists()) return true; // tolerate alt name - if (new File(root, "starship.o2r").exists()) return true; - File mods = new File(root, "mods"); - return mods.exists() && mods.isDirectory() && mods.listFiles() != null && mods.listFiles().length > 0; -} - -private boolean copyFromBestSourceToSaf(DocumentFile userRoot) { - File srcRoot = chooseBestSourceRoot(); - Log.i(TAG, "Copying from srcRoot=" + srcRoot); - int copied = 0; - - // gamecontrollerdb.txt (prefer this name), fall back to controllerdb.txt if that's what exists - File gcdb = new File(srcRoot, "gamecontrollerdb.txt"); - File cdb = new File(srcRoot, "controllerdb.txt"); - if (gcdb.exists()) { - if (copyFileToTree(gcdb, userRoot, "text/plain")) copied++; - } else if (cdb.exists()) { - if (copyFileToTree(cdb, userRoot, "text/plain")) copied++; - } else Log.w(TAG, "No controller DB at " + srcRoot); - - // starship.o2r - File starship = new File(srcRoot, "starship.o2r"); - if (starship.exists()) { - if (copyFileToTree(starship, userRoot, "application/octet-stream")) copied++; - } else Log.w(TAG, "No starship.o2r at " + srcRoot); - - // mods - File modsSrc = new File(srcRoot, "mods"); - if (modsSrc.exists() && modsSrc.isDirectory()) { - if (copyFolderToTree(modsSrc, userRoot)) copied++; - } else Log.w(TAG, "No mods folder at " + srcRoot); - - if (copied > 0) showToast("Copied " + copied + " item(s) to selected folder."); - else showToast("Nothing copied. Make sure files exist in app storage."); - return copied > 0; -} - -// When user picks sf64.o2r via SAF, copy into INTERNAL (engine reads from here) AND user folder (for persistence) -private void handleRomFileSelection(Uri selectedFileUri) { - if (selectedFileUri == null) { - showToast("No sf64.o2r selected."); - return; - } - - File dest = new File(getFilesDir(), "sf64.o2r"); - showToast("Copying sf64.o2r..."); - - try (InputStream in = getContentResolver().openInputStream(selectedFileUri); - FileOutputStream out = new FileOutputStream(dest)) { - byte[] buf = new byte[8192]; - int r; - long total = 0; - while ((r = in.read(buf)) != -1) { - out.write(buf, 0, r); - total += r; - } - out.flush(); - out.getFD().sync(); - Log.i(TAG, "sf64.o2r copied to internal (" + total + " bytes): " + dest.getAbsolutePath()); - - // Also copy to user's selected folder if one exists - if (userFolderUri != null) { - DocumentFile userRoot = DocumentFile.fromTreeUri(this, userFolderUri); - if (userRoot != null) { - if (copyFileToTree(dest, userRoot, "application/octet-stream")) { - Log.i(TAG, "sf64.o2r also copied to user's selected folder"); - } else { - Log.w(TAG, "Failed to copy sf64.o2r to user's selected folder"); - } - } - } - - runOnUiThread(() -> showPortraitDialog("sf64.o2r ready", - "sf64.o2r copied. Restart to load the game.", - DialogActivity.DIALOG_TYPE_FILE_READY)); - } catch (IOException e) { - Log.e(TAG, "handleRomFileSelection", e); - showToast("Failed to copy sf64.o2r: " + e.getMessage()); - } -} - -// ===== SAF copy helpers ===== -private boolean copyFileToTree(File src, DocumentFile dstParent, String mimeGuess) { - try { - // Overwrite if present - DocumentFile existing = findChild(dstParent, src.getName()); - if (existing != null && existing.isFile()) existing.delete(); - - String mime = (mimeGuess != null) ? mimeGuess : guessMime(src.getName()); - DocumentFile dest = dstParent.createFile(mime, src.getName()); - if (dest == null) { - Log.e(TAG, "Failed to create file in tree: " + src.getName()); - return false; - } - - try (InputStream in = new FileInputStream(src); - OutputStream out = getContentResolver().openOutputStream(dest.getUri(), "w")) { - if (out == null) throw new IOException("Null OutputStream from resolver"); - byte[] buf = new byte[8192]; - int r; - long total = 0; - while ((r = in.read(buf)) != -1) { - out.write(buf, 0, r); - total += r; - } - out.flush(); - Log.i(TAG, "Wrote " + total + " bytes → " + dest.getUri()); - } - Log.i(TAG, "Copied to SAF: " + src.getAbsolutePath() + " → " + dest.getUri()); - return true; - } catch (IOException e) { - Log.e(TAG, "copyFileToTree " + src.getName(), e); - showToast("Failed copying " + src.getName()); - return false; - } -} - -// Returns true if any item was copied -private boolean copyFolderToTree(File srcDir, DocumentFile dstParent) { - boolean any = false; - DocumentFile dstDir = ensureDirectory(dstParent, srcDir.getName()); - if (dstDir == null) return false; - - File[] kids = srcDir.listFiles(); - if (kids == null) return false; - - for (File kid : kids) { - if (kid.isDirectory()) { - if (copyFolderToTree(kid, dstDir)) any = true; - } else { - if (copyFileToTree(kid, dstDir, guessMime(kid.getName()))) any = true; - } - } - return any; -} - -private DocumentFile ensureDirectory(DocumentFile parent, String name) { - DocumentFile existing = findChild(parent, name); - if (existing != null && existing.isDirectory()) return existing; - if (existing != null) existing.delete(); - return parent.createDirectory(name); -} - -private DocumentFile findChild(DocumentFile parent, String name) { - for (DocumentFile f : parent.listFiles()) { - if (name.equals(f.getName())) return f; - } - return null; -} - -private String guessMime(String name) { - String n = name.toLowerCase(); - if (n.endsWith(".txt")) return "text/plain"; - if (n.endsWith(".json")) return "application/json"; - return "application/octet-stream"; -} - -// ================= Controller overlay and touch handling ================= -private Button buttonA, buttonB, buttonX, buttonY; -private Button buttonDpadUp, buttonDpadDown, buttonDpadLeft, buttonDpadRight; -private Button buttonLB, buttonRB, buttonZ, buttonStart, buttonBack, buttonToggle, buttonMenu; -private FrameLayout leftJoystick; -private ImageView leftJoystickKnob; -private View overlayView; - -private void setupControllerOverlay() { - LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); - overlayView = inflater.inflate(R.layout.touchcontrol_overlay, null); - FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.MATCH_PARENT - ); - overlayView.setLayoutParams(layoutParams); - ViewGroup rootView = (ViewGroup) this.getWindow().getDecorView().findViewById(android.R.id.content); - rootView.addView(overlayView); - - final ViewGroup buttonGroup = overlayView.findViewById(R.id.button_group); - - buttonA = overlayView.findViewById(R.id.buttonA); - buttonB = overlayView.findViewById(R.id.buttonB); - buttonX = overlayView.findViewById(R.id.buttonX); - buttonY = overlayView.findViewById(R.id.buttonY); - - buttonDpadUp = overlayView.findViewById(R.id.buttonDpadUp); - buttonDpadDown = overlayView.findViewById(R.id.buttonDpadDown); - buttonDpadLeft = overlayView.findViewById(R.id.buttonDpadLeft); - buttonDpadRight = overlayView.findViewById(R.id.buttonDpadRight); - - buttonLB = overlayView.findViewById(R.id.buttonLB); - buttonRB = overlayView.findViewById(R.id.buttonRB); - buttonZ = overlayView.findViewById(R.id.buttonZ); - - buttonStart = overlayView.findViewById(R.id.buttonStart); - buttonBack = overlayView.findViewById(R.id.buttonBack); - buttonMenu = overlayView.findViewById(R.id.buttonMenu); - - buttonToggle = overlayView.findViewById(R.id.buttonToggle); - - leftJoystick = overlayView.findViewById(R.id.left_joystick); - leftJoystickKnob = overlayView.findViewById(R.id.left_joystick_knob); - - FrameLayout rightScreenArea = overlayView.findViewById(R.id.right_screen_area); - - addTouchListener(buttonA, ControllerButtons.BUTTON_A); - addTouchListener(buttonB, ControllerButtons.BUTTON_B); - addTouchListener(buttonX, ControllerButtons.BUTTON_X); - addTouchListener(buttonY, ControllerButtons.BUTTON_Y); - - setupCButtons(buttonDpadUp, ControllerButtons.AXIS_RY, 1); - setupCButtons(buttonDpadDown, ControllerButtons.AXIS_RY, -1); - setupCButtons(buttonDpadLeft, ControllerButtons.AXIS_RX, 1); - setupCButtons(buttonDpadRight, ControllerButtons.AXIS_RX, -1); - - addTouchListener(buttonLB, ControllerButtons.BUTTON_LB); - addTouchListener(buttonRB, ControllerButtons.BUTTON_RB); - addTouchListener(buttonZ, ControllerButtons.AXIS_RT); - - addTouchListener(buttonStart, ControllerButtons.BUTTON_START); - addTouchListener(buttonBack, ControllerButtons.BUTTON_BACK); - setupMenuButton(buttonMenu); - - setupJoystick(leftJoystick, leftJoystickKnob, true); - setupLookAround(rightScreenArea); - setupToggleButton(buttonToggle, buttonGroup); -} - -private void enableImmersiveMode() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // Android 11+ (API 30+) - WindowInsetsController controller = getWindow().getInsetsController(); - if (controller != null) { - controller.hide(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars()); - controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); - } - } else { - // Android 10 and below - getWindow().getDecorView().setSystemUiVisibility( - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - | View.SYSTEM_UI_FLAG_FULLSCREEN - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - ); - } -} - -@Override -public void onWindowFocusChanged(boolean hasFocus) { - super.onWindowFocusChanged(hasFocus); - if (hasFocus) { - enableImmersiveMode(); - } -} - -@Override -public void onBackPressed() { - // Show confirmation dialog before exiting - new AlertDialog.Builder(this) - .setTitle("Exit Game") - .setMessage("Are you sure you want to quit?") - .setPositiveButton("Yes", (dialog, which) -> { - // Exit the app - finish(); - System.exit(0); - }) - .setNegativeButton("No", (dialog, which) -> { - // Dismiss dialog and continue playing - dialog.dismiss(); - }) - .setCancelable(true) - .show(); -} - -private void setupMenuButton(Button button) { - button.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - onNativeKeyDown(KeyEvent.KEYCODE_F1); - button.setPressed(true); - // Toggle menu state and controls - MenuOpen = !MenuOpen; - if (MenuOpen) { - DisableAllControls(); - } else { - EnableAllControls(); - } - return true; - case MotionEvent.ACTION_UP: - onNativeKeyUp(KeyEvent.KEYCODE_F1); - button.setPressed(false); - return true; - case MotionEvent.ACTION_CANCEL: - onNativeKeyUp(KeyEvent.KEYCODE_F1); - return true; - } - return false; - } - }); -} - -private void setupToggleButton(Button button, ViewGroup uiGroup) { - boolean isHidden = preferences.getBoolean("controlsVisible", false); - uiGroup.setVisibility(isHidden ? View.INVISIBLE : View.VISIBLE); - button.setOnClickListener(new View.OnClickListener() { - boolean isHidden = false; - @Override - public void onClick(View v) { - if (isHidden) { - uiGroup.setVisibility(View.VISIBLE); - } else { - uiGroup.setVisibility(View.INVISIBLE); - } - preferences.edit().putBoolean("controlsVisible", !isHidden).apply(); - isHidden = !isHidden; - } - }); -} - -private void addTouchListener(Button button, int buttonNum) { - button.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - if (!AllControlsEnabled) return false; - - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - setButton(buttonNum, true); - button.setPressed(true); - return true; - case MotionEvent.ACTION_UP: - setButton(buttonNum, false); - button.setPressed(false); - return true; - case MotionEvent.ACTION_CANCEL: - setButton(buttonNum, false); - return true; - } - return false; - } - }); -} - -private void setupCButtons(Button button, int buttonNum, int direction) { - button.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - if (!AllControlsEnabled) return false; - - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - setAxis(buttonNum, direction < 0 ? Short.MAX_VALUE : Short.MIN_VALUE); - button.setPressed(true); - return true; - case MotionEvent.ACTION_UP: - setAxis(buttonNum, (short) 0); - button.setPressed(false); - return true; - case MotionEvent.ACTION_CANCEL: - setAxis(buttonNum, (short) 0); - return true; - } - return false; - } - }); -} - -// Control state management -private boolean TouchAreaEnabled = true; -private boolean MenuOpen = false; -private boolean AllControlsEnabled = true; - -private void DisableTouchArea() { - TouchAreaEnabled = false; -} - -private void EnableTouchArea() { - TouchAreaEnabled = true; -} - -private void DisableAllControls() { - AllControlsEnabled = false; - TouchAreaEnabled = false; -} - -private void EnableAllControls() { - AllControlsEnabled = true; - TouchAreaEnabled = true; -} - -private void setupLookAround(FrameLayout rightScreenArea) { - rightScreenArea.setOnTouchListener(new View.OnTouchListener() { - private float lastX = 0; - private float lastY = 0; - private boolean isTouching = false; - - @Override - public boolean onTouch(View v, MotionEvent event) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - lastX = event.getX(); - lastY = event.getY(); - isTouching = true; - break; - case MotionEvent.ACTION_MOVE: - if (isTouching) { - float deltaX = event.getX() - lastX; - float deltaY = event.getY() - lastY; - lastX = event.getX(); - lastY = event.getY(); - float sensitivityMultiplier = 15; - float rx = (deltaX * sensitivityMultiplier); - float ry = (deltaY * sensitivityMultiplier); - setCameraState(0, rx); - setCameraState(1, ry); - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - isTouching = false; - setCameraState(0, 0.0f); - setCameraState(1, 0.0f); - break; - } - return TouchAreaEnabled && AllControlsEnabled; - } - }); -} - -private void setupJoystick(FrameLayout joystickLayout, ImageView joystickKnob, boolean isLeft) { - joystickLayout.post(() -> { - final float joystickCenterX = joystickLayout.getWidth() / 2f; - final float joystickCenterY = joystickLayout.getHeight() / 2f; - - joystickLayout.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - if (!AllControlsEnabled) return false; - - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_MOVE: - float deltaX = event.getX() - joystickCenterX; - float deltaY = event.getY() - joystickCenterY; - float maxRadius = joystickLayout.getWidth() / 2f - joystickKnob.getWidth() / 2f; - float distance = (float) Math.sqrt(deltaX * deltaX + deltaY * deltaY); - if (distance > maxRadius) { - float scale = maxRadius / distance; - deltaX *= scale; - deltaY *= scale; - } - joystickKnob.setX(joystickCenterX + deltaX - joystickKnob.getWidth() / 2f); - joystickKnob.setY(joystickCenterY + deltaY - joystickKnob.getHeight() / 2f); - - short x = (short) (deltaX / maxRadius * Short.MAX_VALUE); - short y = (short) (deltaY / maxRadius * Short.MAX_VALUE); - setAxis(isLeft ? ControllerButtons.AXIS_LX : ControllerButtons.AXIS_RX, x); - setAxis(isLeft ? ControllerButtons.AXIS_LY : ControllerButtons.AXIS_RY, y); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - joystickKnob.setX(joystickCenterX - joystickKnob.getWidth() / 2f); - joystickKnob.setY(joystickCenterY - joystickKnob.getHeight() / 2f); - setAxis(isLeft ? ControllerButtons.AXIS_LX : ControllerButtons.AXIS_RX, (short) 0); - setAxis(isLeft ? ControllerButtons.AXIS_LY : ControllerButtons.AXIS_RY, (short) 0); - break; - } - return true; - } - }); - }); -} -} \ No newline at end of file diff --git a/android/app/src/main/java/com/starship/android/MainActivity.kt b/android/app/src/main/java/com/starship/android/MainActivity.kt new file mode 100644 index 000000000..ec6d24b3a --- /dev/null +++ b/android/app/src/main/java/com/starship/android/MainActivity.kt @@ -0,0 +1,351 @@ +package com.starship.android + +import android.annotation.SuppressLint +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.FrameLayout +import android.widget.ImageView +import org.libsdl.app.SDLActivity +import kotlin.math.roundToInt +import kotlin.math.sqrt + +/** + * The game itself, plus the on-screen controller drawn over the SDL surface. + * + * The overlay drives a virtual SDL joystick (see src/port/android/AndroidBridge.cpp) + * rather than synthesising input events, so it goes through libultraship's normal + * controller mapping and can be remapped in-game like any other pad. + * + * [LauncherActivity] guarantees sf64.o2r exists before this activity starts. + */ +class MainActivity : SDLActivity() { + + private lateinit var preferences: SharedPreferences + private lateinit var modsButton: Button + + private var controlsEnabled = true + private var menuOpen = false + private var controlsHidden = false + + private val handler = Handler(Looper.getMainLooper()) + private val menuWatcher = object : Runnable { + override fun run() { + syncMenuState() + handler.postDelayed(this, MENU_POLL_MS) + } + } + + // org/libsdl/app is kept byte-identical to the SDL release libultraship + // pins, so the game library is named here rather than patched in there. + // SDLActivity refuses to start if the Java glue and libSDL2.so disagree on + // their version, so both have to move together. + override fun getLibraries(): Array = arrayOf("SDL2", "Starship") + + private external fun attachController() + private external fun detachController() + private external fun setButton(button: Int, value: Boolean) + private external fun setAxis(axis: Int, value: Short) + private external fun isMenuOpen(): Boolean + + override fun onCreate(savedInstanceState: Bundle?) { + preferences = getSharedPreferences(PREFS, MODE_PRIVATE) + super.onCreate(savedInstanceState) + setupControllerOverlay() + attachController() + } + + override fun onResume() { + super.onResume() + // Re-sync immediately as well as on the timer, so coming back from the + // mods manager never leaves the overlay showing a stale menu state. + syncMenuState() + handler.removeCallbacks(menuWatcher) + handler.postDelayed(menuWatcher, MENU_POLL_MS) + } + + override fun onPause() { + handler.removeCallbacks(menuWatcher) + super.onPause() + } + + override fun onDestroy() { + handler.removeCallbacks(menuWatcher) + detachController() + super.onDestroy() + } + + /** + * Mirrors the engine's menu state onto the overlay. + * + * The menu is not only driven by the on-screen Menu button: a physical + * keyboard, a gamepad, or the menu dismissing itself all change it without + * the overlay hearing about it. Tracking it with a local flag drifts out of + * sync and leaves the controls dead with the Mods button stuck on screen, + * so the engine is asked instead. + */ + private fun syncMenuState() { + val open = runCatching { isMenuOpen() }.getOrDefault(false) + if (open == menuOpen) return + + menuOpen = open + controlsEnabled = !open + // Managing mods is a settings action, so it stays out of reach until + // the menu is up. + modsButton.visibility = if (open) View.VISIBLE else View.GONE + + if (open) { + releaseAllInputs() + } + } + + /** + * Clears anything the overlay was holding down. Without this, opening the + * menu mid-input leaves that button or stick stuck on in the virtual pad, + * because the touch listeners stop reporting once controls are disabled. + */ + private fun releaseAllInputs() { + for (button in intArrayOf( + ControllerButtons.BUTTON_A, ControllerButtons.BUTTON_B, + ControllerButtons.BUTTON_X, ControllerButtons.BUTTON_Y, + ControllerButtons.BUTTON_LB, ControllerButtons.BUTTON_RB, + ControllerButtons.BUTTON_START, ControllerButtons.BUTTON_BACK, + ControllerButtons.AXIS_RT + )) { + setButton(button, false) + } + for (axis in intArrayOf( + ControllerButtons.AXIS_LX, ControllerButtons.AXIS_LY, + ControllerButtons.AXIS_RX, ControllerButtons.AXIS_RY + )) { + setAxis(axis, 0) + } + } + + private fun setupControllerOverlay() { + val inflater = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater + val overlay = inflater.inflate(R.layout.touchcontrol_overlay, null).apply { + layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ) + } + findViewById(android.R.id.content).addView(overlay) + + val buttonGroup = overlay.findViewById(R.id.button_group) + + overlay.bindButton(R.id.buttonA, ControllerButtons.BUTTON_A) + overlay.bindButton(R.id.buttonB, ControllerButtons.BUTTON_B) + overlay.bindButton(R.id.buttonX, ControllerButtons.BUTTON_X) + overlay.bindButton(R.id.buttonY, ControllerButtons.BUTTON_Y) + overlay.bindButton(R.id.buttonLB, ControllerButtons.BUTTON_LB) + overlay.bindButton(R.id.buttonRB, ControllerButtons.BUTTON_RB) + overlay.bindButton(R.id.buttonZ, ControllerButtons.AXIS_RT) + overlay.bindButton(R.id.buttonStart, ControllerButtons.BUTTON_START) + overlay.bindButton(R.id.buttonBack, ControllerButtons.BUTTON_BACK) + + // The C buttons sit on the right stick, which is how the N64 pad is + // mapped, so the d-pad cluster drives that axis rather than the d-pad. + overlay.bindAxisButton(R.id.buttonDpadUp, ControllerButtons.AXIS_RY, positive = false) + overlay.bindAxisButton(R.id.buttonDpadDown, ControllerButtons.AXIS_RY, positive = true) + overlay.bindAxisButton(R.id.buttonDpadLeft, ControllerButtons.AXIS_RX, positive = false) + overlay.bindAxisButton(R.id.buttonDpadRight, ControllerButtons.AXIS_RX, positive = true) + + modsButton = overlay.findViewById(R.id.buttonMods) + modsButton.setOnClickListener { startActivity(Intent(this, ModsActivity::class.java)) } + + setupMenuButton(overlay.findViewById(R.id.buttonMenu)) + setupJoystick(overlay.findViewById(R.id.left_joystick), overlay.findViewById(R.id.left_joystick_knob)) + setupLookArea(overlay.findViewById(R.id.right_screen_area)) + setupToggleButton(overlay.findViewById(R.id.buttonToggle), buttonGroup) + } + + @SuppressLint("ClickableViewAccessibility") + private fun View.bindButton(id: Int, button: Int) { + val view = findViewById