#include "BenPort.h" #include #include #include #include #include #include #include #include #include #include "z64animation.h" #include "z64bgcheck.h" #include #include #ifdef _WIN32 #include #else #include #endif #include #include "variables.h" #include "z64.h" #include "macros.h" #include #include #include "build.h" #include #include #ifdef __APPLE__ #include #else #include #endif #include "Extractor/Extract.h" // OTRTODO //#include #include "2s2h/Enhancements/FrameInterpolation/FrameInterpolation.h" #ifdef ENABLE_CROWD_CONTROL #include "Enhancements/crowd-control/CrowdControl.h" CrowdControl* CrowdControl::Instance; #endif #include #include #include #include #include "2s2h/GameInteractor/GameInteractor.h" #include "2s2h/Enhancements/Enhancements.h" #include "2s2h/Enhancements/GfxPatcher/AuthenticGfxPatches.h" #include "2s2h/Enhancements/GfxPatcher/PlayerCustomFlipbooks.h" #include "2s2h/DeveloperTools/DebugConsole.h" #include "2s2h/Rando/Rando.h" #include "2s2h/Rando/Spoiler/Spoiler.h" #include "2s2h/SaveManager/SaveManager.h" #include "2s2h/CustomMessage/CustomMessage.h" #include "2s2h/CustomItem/CustomItem.h" #include "2s2h/BenGui/Notification.h" #include "2s2h/ShipUtils.h" #include "2s2h/ShipInit.hpp" #include "2s2h/PresetManager/PresetManager.h" // Resource Types/Factories #include #include #include #include #include #include "2s2h/resource/type/2shResourceType.h" #include "2s2h/resource/type/Animation.h" #include "2s2h/resource/type/Array.h" #include "2s2h/resource/type/AudioSample.h" #include "2s2h/resource/type/AudioSequence.h" #include "2s2h/resource/type/AudioSoundFont.h" #include "2s2h/resource/type/CollisionHeader.h" #include "2s2h/resource/type/Cutscene.h" #include "2s2h/resource/type/Path.h" #include "2s2h/resource/type/PlayerAnimation.h" #include "2s2h/resource/type/Scene.h" #include "2s2h/resource/type/Skeleton.h" #include "2s2h/resource/type/SkeletonLimb.h" #include #include #include #include #include #include "2s2h/resource/importer/AnimationFactory.h" #include "2s2h/resource/importer/ArrayFactory.h" #include "2s2h/resource/importer/AudioSampleFactory.h" #include "2s2h/resource/importer/AudioSequenceFactory.h" #include "2s2h/resource/importer/AudioSoundFontFactory.h" #include "2s2h/resource/importer/CollisionHeaderFactory.h" #include "2s2h/resource/importer/CutsceneFactory.h" #include "2s2h/resource/importer/PathFactory.h" #include "2s2h/resource/importer/PlayerAnimationFactory.h" #include "2s2h/resource/importer/SceneFactory.h" #include "2s2h/resource/importer/SkeletonFactory.h" #include "2s2h/resource/importer/SkeletonLimbFactory.h" #include "2s2h/resource/importer/TextMMFactory.h" #include "2s2h/resource/importer/BackgroundFactory.h" #include "2s2h/resource/importer/TextureAnimationFactory.h" #include "2s2h/resource/importer/KeyFrameFactory.h" #include #include #include #include "2s2h/Enhancements/Audio/AudioCollection.h" #include "BenGui/BenInputEditorWindow.h" OTRGlobals* OTRGlobals::Instance; GameInteractor* GameInteractor::Instance; AudioCollection* AudioCollection::Instance; extern "C" char** cameraStrings; bool prevAltAssets = false; std::vector> cameraStdStrings; Color_RGB8 kokiriColor = { 0x1E, 0x69, 0x1B }; Color_RGB8 goronColor = { 0x64, 0x14, 0x00 }; Color_RGB8 zoraColor = { 0x00, 0xEC, 0x64 }; OTRGlobals::OTRGlobals() { std::vector archiveFiles; std::vector patchFiles; std::string mmPathO2R = Ship::Context::LocateFileAcrossAppDirs("mm.o2r", appShortName); std::string mmPathZIP = Ship::Context::LocateFileAcrossAppDirs("mm.zip", appShortName); if (std::filesystem::exists(mmPathO2R)) { archiveFiles.push_back(mmPathO2R); } else if (std::filesystem::exists(mmPathZIP)) { archiveFiles.push_back(mmPathZIP); } else { std::string mmPath = Ship::Context::LocateFileAcrossAppDirs("mm.otr", appShortName); if (std::filesystem::exists(mmPath)) { archiveFiles.push_back(mmPath); } } std::string shipOtrPath = Ship::Context::GetPathRelativeToAppBundle("2ship.o2r"); if (std::filesystem::exists(shipOtrPath)) { archiveFiles.push_back(shipOtrPath); } std::string patchesPath = Ship::Context::LocateFileAcrossAppDirs("mods", appShortName); if (patchesPath.length() > 0 && std::filesystem::exists(patchesPath)) { if (std::filesystem::is_directory(patchesPath)) { for (const auto& p : std::filesystem::recursive_directory_iterator(patchesPath)) { if (StringHelper::IEquals(p.path().extension().string(), ".o2r")) { patchFiles.push_back(p.path().generic_string()); } else if (StringHelper::IEquals(p.path().extension().string(), ".zip")) { patchFiles.push_back(p.path().generic_string()); } else if (StringHelper::IEquals(p.path().extension().string(), ".otr")) { patchFiles.push_back(p.path().generic_string()); } } } } // Sort all patch files from the mods directory lexigraphically to guarantee sort order across all platforms std::sort(patchFiles.begin(), patchFiles.end(), [](const std::string& a, const std::string& b) { // Sort based on file name alone, excluding file extension, so that order is not impacted by archive format const std::string aFileName = a.substr(0, a.find_last_of(".")); const std::string bFileName = b.substr(0, b.find_last_of(".")); return std::lexicographical_compare(aFileName.begin(), aFileName.end(), bFileName.begin(), bFileName.end(), [](char c1, char c2) { return std::tolower(c1) < std::tolower(c2); }); }); archiveFiles.insert(archiveFiles.end(), patchFiles.begin(), patchFiles.end()); std::unordered_set validHashes = { MM_NTSC_US_10, MM_NTSC_US_GC }; context = Ship::Context::CreateUninitializedInstance("2 Ship 2 Harkinian", appShortName, "2ship2harkinian.json"); context->InitFileDropMgr(); context->InitGfxDebugger(); context->InitConfiguration(); context->InitConsoleVariables(); #if (_DEBUG) auto defaultLogLevel = spdlog::level::trace; #else auto defaultLogLevel = spdlog::level::info; #endif auto logLevel = (spdlog::level::level_enum)CVarGetInteger("gDeveloperTools.LogLevel", defaultLogLevel); context->InitLogging(logLevel, logLevel); Ship::Context::GetInstance()->GetLogger()->set_pattern("[%H:%M:%S.%e] [%s:%#] [%^%l%$] %v"); // tell LUS to reserve 3 SoH specific threads (Game, Audio, Save) context->InitResourceManager(archiveFiles, {}, 3); prevAltAssets = CVarGetInteger("gEnhancements.Mods.AlternateAssets", 0); context->GetResourceManager()->SetAltAssetsEnabled(prevAltAssets); auto controlDeck = std::make_shared(std::vector({ BTN_CUSTOM_MODIFIER1, BTN_CUSTOM_MODIFIER2, })); context->InitControlDeck(controlDeck); context->InitCrashHandler(); context->InitConsole(); auto benInputEditorWindow = std::make_shared("gWindows.BenInputEditor", "2S2H Input Editor"); auto benFast3dWindow = std::make_shared(std::vector>({ benInputEditorWindow })); context->InitWindow(benFast3dWindow); // Override LUS defaults auto overlay = context->GetInstance()->GetWindow()->GetGui()->GetGameOverlay(); overlay->LoadFont("Press Start 2P", 12.0f, "fonts/PressStart2P-Regular.ttf"); overlay->LoadFont("Fipps", 32.0f, "fonts/Fipps-Regular.otf"); overlay->SetCurrentFont(CVarGetString(CVAR_GAME_OVERLAY_FONT, "Press Start 2P")); context->InitAudio({ .SampleRate = 32000, .SampleLength = 1024, .DesiredBuffered = 1680 }); SPDLOG_INFO("Starting 2 Ship 2 Harkinian version {} (Branch: {} | Commit: {})", (char*)gBuildVersion, (char*)gGitBranch, (char*)gGitCommitHash); auto loader = context->GetResourceManager()->GetResourceLoader(); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "Texture", static_cast(Fast::ResourceType::Texture), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "Texture", static_cast(Fast::ResourceType::Texture), 1); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "Vertex", static_cast(Fast::ResourceType::Vertex), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_XML, "Vertex", static_cast(Fast::ResourceType::Vertex), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "DisplayList", static_cast(Fast::ResourceType::DisplayList), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_XML, "DisplayList", static_cast(Fast::ResourceType::DisplayList), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "Matrix", static_cast(Fast::ResourceType::Matrix), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "Blob", static_cast(Ship::ResourceType::Blob), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "Array", static_cast(SOH::ResourceType::SOH_Array), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "Animation", static_cast(SOH::ResourceType::SOH_Animation), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "PlayerAnimation", static_cast(SOH::ResourceType::SOH_PlayerAnimation), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "Room", static_cast(SOH::ResourceType::SOH_Room), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "CollisionHeader", static_cast(SOH::ResourceType::SOH_CollisionHeader), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "Skeleton", static_cast(SOH::ResourceType::SOH_Skeleton), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "SkeletonLimb", static_cast(SOH::ResourceType::SOH_SkeletonLimb), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "Path", static_cast(SOH::ResourceType::SOH_Path), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "Cutscene", static_cast(SOH::ResourceType::SOH_Cutscene), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "TextMM", static_cast(SOH::ResourceType::TSH_TextMM), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "AudioSample", static_cast(SOH::ResourceType::SOH_AudioSample), 2); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_XML, "Sample", static_cast(SOH::ResourceType::SOH_AudioSample), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "AudioSoundFont", static_cast(SOH::ResourceType::SOH_AudioSoundFont), 2); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_XML, "SoundFont", static_cast(SOH::ResourceType::SOH_AudioSoundFont), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "AudioSequence", static_cast(SOH::ResourceType::SOH_AudioSequence), 2); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_XML, "Sequence", static_cast(SOH::ResourceType::SOH_AudioSequence), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "Background", static_cast(SOH::ResourceType::SOH_Background), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "TextureAnimation", static_cast(SOH::ResourceType::TSH_TexAnim), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "KeyFrameAnim", static_cast(SOH::ResourceType::TSH_CKeyFrameAnim), 0); loader->RegisterResourceFactory(std::make_shared(), RESOURCE_FORMAT_BINARY, "KeyFrameSkel", static_cast(SOH::ResourceType::TSH_CKeyFrameSkel), 0); // gSaveStateMgr = std::make_shared(); // gRandomizer = std::make_shared(); auto versions = context->GetResourceManager()->GetArchiveManager()->GetGameVersions(); for (uint32_t version : versions) { if (!validHashes.contains(version)) { #if defined(__SWITCH__) SPDLOG_ERROR("Invalid O2R File!"); #elif defined(__WIIU__) Ship::WiiU::ThrowInvalidOTR(); #else SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Invalid O2R File", "Attempted to load an invalid O2R file. Try regenerating.", nullptr); SPDLOG_ERROR("Invalid O2R File!"); #endif exit(1); } } fontMono = CreateFontWithSize(16.0f, "fonts/Inconsolata-Regular.ttf"); fontMonoLarger = CreateFontWithSize(20.0f, "fonts/Inconsolata-Regular.ttf"); fontMonoLargest = CreateFontWithSize(24.0f, "fonts/Inconsolata-Regular.ttf"); fontStandard = CreateFontWithSize(16.0f, "fonts/Montserrat-Regular.ttf"); fontStandardLarger = CreateFontWithSize(20.0f, "fonts/Montserrat-Regular.ttf"); fontStandardLargest = CreateFontWithSize(24.0f, "fonts/Montserrat-Regular.ttf"); ImGui::GetIO().FontDefault = fontMono; } OTRGlobals::~OTRGlobals() { } uint32_t OTRGlobals::GetInterpolationFPS() { if (CVarGetInteger("gMatchRefreshRate", 0)) { return Ship::Context::GetInstance()->GetWindow()->GetCurrentRefreshRate(); } else if (CVarGetInteger(CVAR_VSYNC_ENABLED, 1) || !Ship::Context::GetInstance()->GetWindow()->CanDisableVerticalSync()) { return std::min(Ship::Context::GetInstance()->GetWindow()->GetCurrentRefreshRate(), CVarGetInteger("gInterpolationFPS", 20)); } return CVarGetInteger("gInterpolationFPS", 20); } extern "C" uint32_t Ship_GetInterpolationFPS() { return OTRGlobals::Instance->GetInterpolationFPS(); } // Number of interpolated frames extern "C" uint32_t Ship_GetInterpolationFrameCount() { return ceil((float)Ship_GetInterpolationFPS() / 20.0f); } struct ExtensionEntry { std::string path; std::string ext; }; ImFont* OTRGlobals::CreateFontWithSize(float size, std::string fontPath) { auto mImGuiIo = &ImGui::GetIO(); ImFont* font; if (fontPath == "") { ImFontConfig fontCfg = ImFontConfig(); fontCfg.OversampleH = fontCfg.OversampleV = 1; fontCfg.PixelSnapH = true; fontCfg.SizePixels = size; font = mImGuiIo->Fonts->AddFontDefault(&fontCfg); } else { auto initData = std::make_shared(); ImFontConfig config; config.FontDataOwnedByAtlas = false; initData->Format = RESOURCE_FORMAT_BINARY; initData->Type = static_cast(RESOURCE_TYPE_FONT); initData->ResourceVersion = 0; initData->Path = fontPath; std::shared_ptr fontData = std::static_pointer_cast( Ship::Context::GetInstance()->GetResourceManager()->LoadResource(fontPath, false, initData)); font = mImGuiIo->Fonts->AddFontFromMemoryTTF(fontData->Data, fontData->DataSize, size, &config); } // FontAwesome fonts need to have their sizes reduced by 2.0f/3.0f in order to align correctly float iconFontSize = size * 2.0f / 3.0f; static const ImWchar sIconsRanges[] = { ICON_MIN_FA, ICON_MAX_16_FA, 0 }; ImFontConfig iconsConfig; iconsConfig.MergeMode = true; iconsConfig.PixelSnapH = true; iconsConfig.GlyphMinAdvanceX = iconFontSize; mImGuiIo->Fonts->AddFontFromMemoryCompressedBase85TTF(fontawesome_compressed_data_base85, iconFontSize, &iconsConfig, sIconsRanges); return font; } extern "C" void OTRMessage_Init(); extern "C" void AudioMgr_CreateNextAudioBuffer(s16* samples, u32 num_samples); extern "C" void AudioPlayer_Play(const uint8_t* buf, uint32_t len); extern "C" int AudioPlayer_Buffered(void); extern "C" int AudioPlayer_GetDesiredBuffered(void); extern "C" void ResourceMgr_LoadDirectory(const char* resName); std::unordered_map ExtensionCache; static struct { std::thread thread; std::condition_variable cv_to_thread, cv_from_thread; std::mutex mutex; bool running; bool processing; } audio; void OTRAudio_Thread() { while (audio.running) { { std::unique_lock Lock(audio.mutex); while (!audio.processing && audio.running) { audio.cv_to_thread.wait(Lock); } if (!audio.running) { break; } } std::unique_lock Lock(audio.mutex); // AudioMgr_ThreadEntry(&gAudioMgr); // 528 and 544 relate to 60 fps at 32 kHz 32000/60 = 533.333.. // in an ideal world, one third of the calls should use num_samples=544 and two thirds num_samples=528 #define SAMPLES_HIGH 560 #define SAMPLES_LOW 528 #define AUDIO_FRAMES_PER_UPDATE (R_UPDATE_RATE > 0 ? R_UPDATE_RATE : 1) #define NUM_AUDIO_CHANNELS 2 int samples_left = AudioPlayer_Buffered(); u32 num_audio_samples = samples_left < AudioPlayer_GetDesiredBuffered() ? SAMPLES_HIGH : SAMPLES_LOW; // 3 is the maximum authentic frame divisor. s16 audio_buffer[SAMPLES_HIGH * NUM_AUDIO_CHANNELS * 3]; for (int i = 0; i < AUDIO_FRAMES_PER_UPDATE; i++) { AudioMgr_CreateNextAudioBuffer(audio_buffer + i * (num_audio_samples * NUM_AUDIO_CHANNELS), num_audio_samples); } AudioPlayer_Play((u8*)audio_buffer, num_audio_samples * (sizeof(int16_t) * NUM_AUDIO_CHANNELS * AUDIO_FRAMES_PER_UPDATE)); audio.processing = false; audio.cv_from_thread.notify_one(); } } // C->C++ Bridge extern "C" void OTRAudio_Init() { // Precache all our samples, sequences, etc... ResourceMgr_LoadDirectory("audio"); if (!audio.running) { audio.running = true; audio.thread = std::thread(OTRAudio_Thread); } } extern "C" char** gSequenceMap; extern "C" size_t gSequenceMapSize; extern "C" char** gFontMap; extern "C" size_t gFontMapSize; extern "C" void OTRAudio_Exit() { // Tell the audio thread to stop { std::unique_lock Lock(audio.mutex); audio.running = false; } audio.cv_to_thread.notify_all(); // Wait until the audio thread quit audio.thread.join(); for (size_t i = 0; i < gSequenceMapSize; i++) { free(gSequenceMap[i]); } free(gSequenceMap); for (size_t i = 0; i < gFontMapSize; i++) { free(gFontMap[i]); } free(gFontMap); free(gAudioCtx.seqLoadStatus); free(gAudioCtx.fontLoadStatus); } extern "C" void OTRExtScanner() { auto lst = *Ship::Context::GetInstance()->GetResourceManager()->GetArchiveManager()->ListFiles("*").get(); for (auto& rPath : lst) { std::vector raw = StringHelper::Split(rPath, "."); std::string ext = raw[raw.size() - 1]; std::string nPath = rPath.substr(0, rPath.size() - (ext.size() + 1)); replace(nPath.begin(), nPath.end(), '\\', '/'); ExtensionCache[nPath] = { rPath, ext }; } } typedef struct { uint16_t major; uint16_t minor; uint16_t patch; } ArchiveVersion; // Read the port version from an archive file ArchiveVersion ReadPortVersionFromArchive(std::string archivePath, bool isO2rType) { ArchiveVersion version = {}; // Use a temporary archive instance to load the archive appropriately and read the version file std::shared_ptr archive; if (isO2rType) { archive = make_shared(archivePath); } else { #ifdef INCLUDE_MPQ_SUPPORT archive = make_shared(archivePath); #else SPDLOG_ERROR("An OTR File, {}, was found but support for them is not included. File will be ignored.", archivePath.c_str()); #endif } if (archive->Open()) { auto t = archive->LoadFile("portVersion"); if (t != nullptr && t->IsLoaded) { auto stream = std::make_shared(t->Buffer->data(), t->Buffer->size()); auto reader = std::make_shared(stream); Ship::Endianness endianness = (Ship::Endianness)reader->ReadUByte(); reader->SetEndianness(endianness); version.major = reader->ReadUInt16(); version.minor = reader->ReadUInt16(); version.patch = reader->ReadUInt16(); } } return version; } // Check that a 2ship.o2r exists and matches the version of 2ship running // Otherwise show a message and exit void Check2ShipArchiveVersion(std::string archivePath) { std::string msg; #if defined(__SWITCH__) msg = "\x1b[4;2HPlease re-extract it from the download." "\x1b[6;2HPress the Home button to exit..."; #elif defined(__WIIU__) msg = "Please extract the 2ship.o2r from the 2 Ship 2 Harkinian download\nto your folder.\n\n" "Press and hold the power button to shutdown..."; #else msg = "Please extract the 2ship.o2r from the 2 Ship 2 Harkinian download to your folder.\n\nExiting..."; #endif if (!std::filesystem::exists(archivePath)) { #if not defined(__SWITCH__) && not defined(__WIIU__) Extractor::ShowErrorBox("2ship.o2r file is missing", msg.c_str()); exit(1); #elif defined(__SWITCH__) Ship::Switch::PrintErrorMessageToScreen(("\x1b[2;2HYou are missing the 2ship.o2r file." + msg).c_str()); #elif defined(__WIIU__) OSFatal(("You are missing the 2ship.o2r file\n\n" + msg).c_str()); #endif } ArchiveVersion archiveVer = ReadPortVersionFromArchive(archivePath, true); if (archiveVer.major != gBuildVersionMajor || archiveVer.minor != gBuildVersionMinor || archiveVer.patch != gBuildVersionPatch) { #if not defined(__SWITCH__) && not defined(__WIIU__) Extractor::ShowErrorBox("2ship.o2r file version does not match", msg.c_str()); exit(1); #elif defined(__SWITCH__) Ship::Switch::PrintErrorMessageToScreen(("\x1b[2;2HYou have an old 2ship.o2r file." + msg).c_str()); #elif defined(__WIIU__) OSFatal(("You have an old 2ship.o2r file\n\n" + msg).c_str()); #endif } } // Checks the program version stored in the o2r and compares the major/minor value to 2ship // For Windows/Mac/Linux if the version doesn't match, offer to regenerate it void DetectArchiveVersion(std::string fileName, bool isO2rType) { bool isArchiveOld = false; std::string archivePath = Ship::Context::LocateFileAcrossAppDirs(fileName, appShortName); // Doesn't exist so nothing to do here if (!std::filesystem::exists(archivePath)) { return; } ArchiveVersion archiveVer = ReadPortVersionFromArchive(archivePath, isO2rType); // Check both major and minor for game archives if (archiveVer.major != gBuildVersionMajor || archiveVer.minor != gBuildVersionMinor) { isArchiveOld = true; } if (isArchiveOld) { #if not defined(__SWITCH__) && not defined(__WIIU__) char msgBuf[250]; char version[18]; // 5 digits for int16_max (x3) + separators + terminator if (archiveVer.major != 0 || archiveVer.minor != 0 || archiveVer.patch != 0) { snprintf(version, 18, "%d.%d.%d", archiveVer.major, archiveVer.minor, archiveVer.patch); } else { snprintf(version, 18, "no version found"); } snprintf(msgBuf, 250, "The %s file was generated with a different version of 2 Ship 2 Harkinian.\n" "O2R version: %s\n\n" "You must regenerate to be able to play, otherwise the program will exit.\n" "Would you like to regenerate it now?", fileName.c_str(), version); if (Extractor::ShowYesNoBox("Old O2R File Found", msgBuf) == IDYES) { std::string installPath = Ship::Context::GetAppBundlePath(); if (!std::filesystem::exists(installPath + "/assets")) { Extractor::ShowErrorBox( "Extractor assets not found", "Unable to regenerate. Missing assets folder needed to generate O2R file.\n\nExiting..."); exit(1); } Extractor extract; if (!extract.Run(Ship::Context::GetAppDirectoryPath(appShortName))) { Extractor::ShowErrorBox("Error", "An error occurred, no O2R file was generated.\n\nExiting..."); exit(1); } // We can only regenerate O2R archives, so we should just delete the old OTR file if (!isO2rType) { std::filesystem::remove(archivePath); } extract.CallZapd(installPath, Ship::Context::GetAppDirectoryPath(appShortName)); // Rename the new O2R with the previously used extension if (isO2rType) { std::filesystem::rename(Ship::Context::LocateFileAcrossAppDirs("mm.o2r", appShortName), archivePath); } } else { exit(1); } #elif defined(__SWITCH__) Ship::Switch::PrintErrorMessageToScreen("\x1b[2;2HYou've launched the 2Ship with an old game O2R file." "\x1b[4;2HPlease regenerate a new game O2R and relaunch." "\x1b[6;2HPress the Home button to exit..."); #elif defined(__WIIU__) OSFatal("You've launched the 2Ship with an old a game O2R file.\n\n" "Please generate a game O2R and relaunch.\n\n" "Press and hold the Power button to shutdown..."); #endif } } void CheckAndCreateModFolder() { try { std::string modsPath = Ship::Context::LocateFileAcrossAppDirs("mods", appShortName); if (!std::filesystem::exists(modsPath)) { // Create mods folder relative to app dir modsPath = Ship::Context::GetPathRelativeToAppDirectory("mods", appShortName); std::string filePath = modsPath + "/custom_mod_files_go_here.txt"; if (std::filesystem::create_directories(modsPath)) { std::ofstream(filePath).close(); } } } catch (std::filesystem::filesystem_error const& ex) { // Couldn't make the folder, continue silently return; } } extern "C" void InitOTR() { #ifdef __SWITCH__ Ship::Switch::Init(Ship::PreInitPhase); #elif defined(__WIIU__) Ship::WiiU::Init(appShortName); #endif // BENTODO: OTRExporter is filling the version file with garbage. Uncomment once fixed. // Check2ShipArchiveVersion(Ship::Context::GetPathRelativeToAppBundle("2ship.o2r")); std::string mmPathO2R = Ship::Context::LocateFileAcrossAppDirs("mm.o2r", appShortName); std::string mmPathZIP = Ship::Context::LocateFileAcrossAppDirs("mm.zip", appShortName); std::string mmPathOtr = Ship::Context::LocateFileAcrossAppDirs("mm.otr", appShortName); // Check game archives in preferred order if (std::filesystem::exists(mmPathO2R)) { DetectArchiveVersion("mm.o2r", true); } else if (std::filesystem::exists(mmPathZIP)) { DetectArchiveVersion("mm.zip", true); } else if (std::filesystem::exists(mmPathOtr)) { DetectArchiveVersion("mm.otr", false); } #if not defined(__SWITCH__) && not defined(__WIIU__) CheckAndCreateModFolder(); if (!std::filesystem::exists(mmPathO2R) && !std::filesystem::exists(mmPathZIP) && !std::filesystem::exists(mmPathOtr)) { std::string installPath = Ship::Context::GetAppBundlePath(); if (!std::filesystem::exists(installPath + "/assets")) { Extractor::ShowErrorBox( "Extractor assets not found", "No game O2R file found. Missing assets folder needed to generate O2R file. Exiting..."); exit(1); } if (Extractor::ShowYesNoBox("No O2R File", "No O2R files found. Generate one now?") == IDYES) { Extractor extract; if (!extract.Run(Ship::Context::GetAppDirectoryPath(appShortName))) { Extractor::ShowErrorBox("Error", "An error occurred, no O2R file was generated. Exiting..."); exit(1); } extract.CallZapd(installPath, Ship::Context::GetAppDirectoryPath(appShortName)); } else { exit(1); } } #endif OTRGlobals::Instance = new OTRGlobals(); GameInteractor::Instance = new GameInteractor(); AudioCollection::Instance = new AudioCollection(); LoadGuiTextures(); BenGui::SetupGuiElements(); ShipInit::InitAll(); Rando::Init(); GfxPatcher_ApplyNecessaryAuthenticPatches(); DebugConsole_Init(); GameInteractor::Instance->RegisterOwnHooks(); CustomItem::RegisterHooks(); CustomMessage::RegisterHooks(); Rando::StaticData::PopulateCheckNames(); OTRMessage_Init(); OTRAudio_Init(); OTRExtScanner(); PlayerCustomFlipbooks_Patch(); // Just came up with arbitrary numbers that seemed to work, this is // usually set once(?) in currently stubbed out areas of code. gIrqMgrRetraceTime = Ship_Random(700000, 850000); time_t now = time(NULL); tm* tm_now = localtime(&now); if (tm_now->tm_mon == 11 && tm_now->tm_mday >= 24 && tm_now->tm_mday <= 25) { CVarRegisterInteger("gLetItSnow", 1); } else { CVarClear("gLetItSnow"); } srand(now); #ifdef ENABLE_CROWD_CONTROL CrowdControl::Instance = new CrowdControl(); CrowdControl::Instance->Init(); if (CVarGetInteger("gCrowdControl", 0)) { CrowdControl::Instance->Enable(); } else { CrowdControl::Instance->Disable(); } #endif std::shared_ptr conf = OTRGlobals::Instance->context->GetConfig(); Ship::Context::GetInstance()->GetFileDropMgr()->RegisterDropHandler(BinarySaveConverter_HandleFileDropped); Ship::Context::GetInstance()->GetFileDropMgr()->RegisterDropHandler(SaveManager_HandleFileDropped); } extern "C" void SaveManager_ThreadPoolWait() { // SaveManager::Instance->ThreadPoolWait(); } extern "C" void DeinitOTR() { SaveManager_ThreadPoolWait(); OTRAudio_Exit(); #ifdef ENABLE_CROWD_CONTROL CrowdControl::Instance->Disable(); CrowdControl::Instance->Shutdown(); #endif // Destroying gui here because we have shared ptrs to LUS objects which output to SPDLOG which is destroyed before // these shared ptrs. BenGui::Destroy(); OTRGlobals::Instance->context = nullptr; delete AudioCollection::Instance; } #ifdef _WIN32 extern "C" uint64_t GetFrequency() { LARGE_INTEGER nFreq; QueryPerformanceFrequency(&nFreq); return nFreq.QuadPart; } extern "C" uint64_t GetPerfCounter() { LARGE_INTEGER ticks; QueryPerformanceCounter(&ticks); return ticks.QuadPart; } #else extern "C" uint64_t GetFrequency() { return 1000; // sec -> ms } extern "C" uint64_t GetPerfCounter() { struct timespec monotime; clock_gettime(CLOCK_MONOTONIC, &monotime); uint64_t remainingMs = (monotime.tv_nsec / 1000000); // in milliseconds return monotime.tv_sec * 1000 + remainingMs; } #endif extern "C" uint64_t GetUnixTimestamp() { auto time = std::chrono::system_clock::now(); auto since_epoch = time.time_since_epoch(); auto millis = std::chrono::duration_cast(since_epoch); long now = millis.count(); return now; } extern "C" void Graph_StartFrame() { #ifndef __WIIU__ using Ship::KbScancode; int32_t dwScancode = OTRGlobals::Instance->context->GetWindow()->GetLastScancode(); OTRGlobals::Instance->context->GetWindow()->SetLastScancode(-1); switch (dwScancode) { #if 0 case KbScancode::LUS_KB_F5: { if (CVarGetInteger("gSaveStatesEnabled", 0) == 0) { Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGameOverlay()->TextDrawNotification( 6.0f, true, "Save states not enabled. Check Cheats Menu."); return; } const unsigned int slot = OTRGlobals::Instance->gSaveStateMgr->GetCurrentSlot(); const SaveStateReturn stateReturn = OTRGlobals::Instance->gSaveStateMgr->AddRequest({ slot, RequestType::SAVE }); switch (stateReturn) { case SaveStateReturn::SUCCESS: SPDLOG_INFO("[SOH] Saved state to slot {}", slot); break; case SaveStateReturn::FAIL_WRONG_GAMESTATE: SPDLOG_ERROR("[SOH] Can not save a state outside of \"GamePlay\""); break; [[unlikely]] default : break; } break; } case KbScancode::LUS_KB_F6: { if (CVarGetInteger("gSaveStatesEnabled", 0) == 0) { Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGameOverlay()->TextDrawNotification( 6.0f, true, "Save states not enabled. Check Cheats Menu."); return; } unsigned int slot = OTRGlobals::Instance->gSaveStateMgr->GetCurrentSlot(); slot++; if (slot > 5) { slot = 0; } OTRGlobals::Instance->gSaveStateMgr->SetCurrentSlot(slot); SPDLOG_INFO("Set SaveState slot to {}.", slot); break; } case KbScancode::LUS_KB_F7: { if (CVarGetInteger("gSaveStatesEnabled", 0) == 0) { Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGameOverlay()->TextDrawNotification( 6.0f, true, "Save states not enabled. Check Cheats Menu."); return; } const unsigned int slot = OTRGlobals::Instance->gSaveStateMgr->GetCurrentSlot(); const SaveStateReturn stateReturn = OTRGlobals::Instance->gSaveStateMgr->AddRequest({ slot, RequestType::LOAD }); switch (stateReturn) { case SaveStateReturn::SUCCESS: SPDLOG_INFO("[SOH] Loaded state from slot {}", slot); break; case SaveStateReturn::FAIL_INVALID_SLOT: SPDLOG_ERROR("[SOH] Invalid State Slot Number {}", slot); break; case SaveStateReturn::FAIL_STATE_EMPTY: SPDLOG_ERROR("[SOH] State Slot {} is empty", slot); break; case SaveStateReturn::FAIL_WRONG_GAMESTATE: SPDLOG_ERROR("[SOH] Can not load a state outside of \"GamePlay\""); break; [[unlikely]] default : break; } break; } #endif #if defined(_WIN32) || defined(__APPLE__) case KbScancode::LUS_KB_F9: { // Toggle TTS CVarSetInteger("gA11yTTS", !CVarGetInteger("gA11yTTS", 0)); break; } #endif case KbScancode::LUS_KB_TAB: { // Toggle HD Assets if (CVarGetInteger("gEnhancements.Mods.AlternateAssetsHotkey", 1)) { CVarSetInteger("gEnhancements.Mods.AlternateAssets", !CVarGetInteger("gEnhancements.Mods.AlternateAssets", 0)); } break; } } #endif } void RunCommands(Gfx* Commands, const std::vector>& mtx_replacements) { auto wnd = std::dynamic_pointer_cast(OTRGlobals::Instance->context->GetWindow()); if (wnd == nullptr) { return; } // Process window events for resize, mouse, keyboard events wnd->HandleEvents(); auto intp = wnd->GetInterpreterWeak().lock().get(); intp->mInterpolationIndex = 0; for (const auto& m : mtx_replacements) { wnd->DrawAndRunGraphicsCommands(Commands, m); intp->mInterpolationIndex++; } } // C->C++ Bridge extern "C" void Graph_ProcessGfxCommands(Gfx* commands) { { std::unique_lock Lock(audio.mutex); audio.processing = true; } audio.cv_to_thread.notify_one(); std::vector> mtx_replacements; int target_fps = OTRGlobals::Instance->GetInterpolationFPS(); static int last_fps; static int last_update_rate; static int time; int fps = target_fps; int original_fps = 60 / R_UPDATE_RATE; auto wnd = std::dynamic_pointer_cast(Ship::Context::GetInstance()->GetWindow()); if (target_fps == 20 || original_fps > target_fps) { fps = original_fps; } if (last_fps != fps || last_update_rate != R_UPDATE_RATE) { time = 0; } // time_base = fps * original_fps (one second) int next_original_frame = fps; while (time + original_fps <= next_original_frame) { time += original_fps; if (time != next_original_frame) { mtx_replacements.push_back(FrameInterpolation_Interpolate((float)time / next_original_frame)); } else { mtx_replacements.emplace_back(); } } time -= fps; if (wnd != nullptr) { wnd->SetTargetFps(fps); } // When the gfx debugger is active, only run with the final mtx if (GfxDebuggerIsDebugging()) { mtx_replacements.clear(); mtx_replacements.emplace_back(); } RunCommands(commands, mtx_replacements); last_fps = fps; last_update_rate = R_UPDATE_RATE; { std::unique_lock Lock(audio.mutex); while (audio.processing) { audio.cv_from_thread.wait(Lock); } } bool curAltAssets = CVarGetInteger("gEnhancements.Mods.AlternateAssets", 0); if (prevAltAssets != curAltAssets) { prevAltAssets = curAltAssets; Ship::Context::GetInstance()->GetResourceManager()->SetAltAssetsEnabled(curAltAssets); gfx_texture_cache_clear(); // TODO: skeleton patch, hooks // SOH::SkeletonPatcher::UpdateSkeletons(); // GameInteractor::Instance->ExecuteHooks(); } // OTRTODO: FIGURE OUT END FRAME POINT /* if (OTRGlobals::Instance->context->GetWindow()->lastScancode != -1) OTRGlobals::Instance->context->GetWindow()->lastScancode = -1;*/ } float divisor_num = 0.0f; // Batch a coordinate to have its depth read later by OTRGetPixelDepth extern "C" void OTRGetPixelDepthPrepare(float x, float y) { // Invert the Y value to match the origin values used in the renderer float adjustedY = SCREEN_HEIGHT - y; auto wnd = std::dynamic_pointer_cast(Ship::Context::GetInstance()->GetWindow()); if (wnd == nullptr) { return; } wnd->GetPixelDepthPrepare(x, adjustedY); } extern "C" uint16_t OTRGetPixelDepth(float x, float y) { // Invert the Y value to match the origin values used in the renderer float adjustedY = SCREEN_HEIGHT - y; auto wnd = std::dynamic_pointer_cast(Ship::Context::GetInstance()->GetWindow()); if (wnd == nullptr) { return 0; } return wnd->GetPixelDepth(x, adjustedY); } extern "C" bool ResourceMgr_IsAltAssetsEnabled() { return Ship::Context::GetInstance()->GetResourceManager()->IsAltAssetsEnabled(); } extern "C" uint32_t ResourceMgr_GetNumGameVersions() { return Ship::Context::GetInstance()->GetResourceManager()->GetArchiveManager()->GetGameVersions().size(); } extern "C" uint32_t ResourceMgr_GetGameVersion(int index) { return Ship::Context::GetInstance()->GetResourceManager()->GetArchiveManager()->GetGameVersions()[index]; } extern "C" uint32_t ResourceMgr_GetGamePlatform(int index) { uint32_t version = Ship::Context::GetInstance()->GetResourceManager()->GetArchiveManager()->GetGameVersions()[index]; switch (version) { case MM_NTSC_US_10: return GAME_PLATFORM_N64; case MM_NTSC_US_GC: return GAME_PLATFORM_GC; } } extern "C" uint32_t ResourceMgr_GetGameRegion(int index) { uint32_t version = Ship::Context::GetInstance()->GetResourceManager()->GetArchiveManager()->GetGameVersions()[index]; switch (version) { case MM_NTSC_US_10: case MM_NTSC_US_GC: return GAME_REGION_NTSC; } } extern "C" void ResourceMgr_LoadDirectory(const char* resName) { Ship::Context::GetInstance()->GetResourceManager()->LoadResources(resName); } extern "C" void ResourceMgr_DirtyDirectory(const char* resName) { Ship::Context::GetInstance()->GetResourceManager()->DirtyResources(resName); } // OTRTODO: There is probably a more elegant way to go about this... // Kenix: This is definitely leaking memory when it's called. extern "C" char** ResourceMgr_ListFiles(const char* searchMask, int* resultSize) { auto lst = Ship::Context::GetInstance()->GetResourceManager()->GetArchiveManager()->ListFiles(searchMask); char** result = (char**)malloc(lst->size() * sizeof(char*)); for (size_t i = 0; i < lst->size(); i++) { char* str = (char*)malloc(lst.get()[0][i].size() + 1); memcpy(str, lst.get()[0][i].data(), lst.get()[0][i].size()); str[lst.get()[0][i].size()] = '\0'; result[i] = str; } *resultSize = lst->size(); return result; } extern "C" uint8_t ResourceMgr_FileExists(const char* filePath) { std::string path = filePath; if (path.substr(0, 7) == "__OTR__") { path = path.substr(7); } return ExtensionCache.contains(path); } extern "C" void ResourceMgr_LoadFile(const char* resName) { Ship::Context::GetInstance()->GetResourceManager()->LoadResource(resName); } std::shared_ptr GetResourceByName(const char* path) { return Ship::Context::GetInstance()->GetResourceManager()->LoadResource(path); } extern "C" char* ResourceMgr_LoadFileFromDisk(const char* filePath) { FILE* file = fopen(filePath, "r"); fseek(file, 0, SEEK_END); int fSize = ftell(file); fseek(file, 0, SEEK_SET); char* data = (char*)malloc(fSize); fread(data, 1, fSize, file); fclose(file); return data; } extern "C" uint8_t ResourceMgr_ResourceIsBackground(char* texPath) { auto res = GetResourceByName(texPath); return res->GetInitData()->Type == static_cast(SOH::ResourceType::SOH_Background); } extern "C" char* ResourceMgr_LoadJPEG(char* data, size_t dataSize) { static char* finalBuffer = 0; if (finalBuffer == 0) finalBuffer = (char*)malloc(dataSize); int w; int h; int comp; unsigned char* pixels = stbi_load_from_memory((const unsigned char*)data, 320 * 240 * 2, &w, &h, &comp, STBI_rgb_alpha); // unsigned char* pixels = stbi_load_from_memory((const unsigned char*)data, 480 * 240 * 2, &w, &h, &comp, // STBI_rgb_alpha); int idx = 0; for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { uint16_t* bufferTest = (uint16_t*)finalBuffer; int pixelIdx = ((y * w) + x) * 4; uint8_t r = pixels[pixelIdx + 0] / 8; uint8_t g = pixels[pixelIdx + 1] / 8; uint8_t b = pixels[pixelIdx + 2] / 8; uint8_t alphaBit = pixels[pixelIdx + 3] != 0; uint16_t data = (r << 11) + (g << 6) + (b << 1) + alphaBit; finalBuffer[idx++] = (data & 0xFF00) >> 8; finalBuffer[idx++] = (data & 0x00FF); } } return (char*)finalBuffer; } extern "C" uint16_t ResourceMgr_LoadTexWidthByName(char* texPath); extern "C" uint16_t ResourceMgr_LoadTexHeightByName(char* texPath); extern "C" char* ResourceMgr_LoadTexOrDListByName(const char* filePath) { auto res = GetResourceByName(filePath); if (res->GetInitData()->Type == static_cast(Fast::ResourceType::DisplayList)) return (char*)&((std::static_pointer_cast(res))->Instructions[0]); else if (res->GetInitData()->Type == static_cast(SOH::ResourceType::SOH_Array)) return (char*)(std::static_pointer_cast(res))->Vertices.data(); else { return (char*)ResourceGetDataByName(filePath); } } extern "C" char* ResourceMgr_LoadIfDListByName(const char* filePath) { auto res = GetResourceByName(filePath); if (res->GetInitData()->Type == static_cast(Fast::ResourceType::DisplayList)) return (char*)&((std::static_pointer_cast(res))->Instructions[0]); return nullptr; } // extern "C" Sprite* GetSeedTexture(uint8_t index) { // return OTRGlobals::Instance->gRandomizer->GetSeedTexture(index); // } extern "C" char* ResourceMgr_LoadPlayerAnimByName(const char* animPath) { auto anim = std::static_pointer_cast(GetResourceByName(animPath)); return (char*)&anim->limbRotData[0]; } extern "C" void ResourceMgr_PushCurrentDirectory(char* path) { Fast::gfx_push_current_dir(path); } extern "C" Gfx* ResourceMgr_LoadGfxByName(const char* path) { auto res = std::static_pointer_cast(GetResourceByName(path)); return (Gfx*)&res->Instructions[0]; } typedef struct { int index; Gfx instruction; } GfxPatch; std::unordered_map> originalGfx; // Attention! This is primarily for cosmetics & bug fixes. For things like mods and model replacement you should be // using OTRs instead (When that is available). Index can be found using the commented out section below. extern "C" void ResourceMgr_PatchGfxByName(const char* path, const char* patchName, int index, Gfx instruction) { auto res = std::static_pointer_cast( Ship::Context::GetInstance()->GetResourceManager()->LoadResource(path)); // Leaving this here for people attempting to find the correct Dlist index to patch /*if (strcmp("__OTR__objects/object_gi_longsword/gGiBiggoronSwordDL", path) == 0) { for (int i = 0; i < res->instructions.size(); i++) { Gfx* gfx = (Gfx*)&res->instructions[i]; // Log all commands // SPDLOG_INFO("index:{} command:{}", i, gfx->words.w0 >> 24); // Log only SetPrimColors if (gfx->words.w0 >> 24 == 250) { SPDLOG_INFO("index:{} r:{} g:{} b:{} a:{}", i, _SHIFTR(gfx->words.w1, 24, 8), _SHIFTR(gfx->words.w1, 16, 8), _SHIFTR(gfx->words.w1, 8, 8), _SHIFTR(gfx->words.w1, 0, 8)); } } }*/ // Index refers to individual gfx words, which are half the size on 32-bit // if (sizeof(uintptr_t) < 8) { // index /= 2; // } // Do not patch custom assets as they most likely do not have the same instructions as authentic assets if (res->GetInitData()->IsCustom) { return; } Gfx* gfx = (Gfx*)&res->Instructions[index]; if (!originalGfx.contains(path) || !originalGfx[path].contains(patchName)) { originalGfx[path][patchName] = { index, *gfx }; } *gfx = instruction; } extern "C" void ResourceMgr_PatchGfxCopyCommandByName(const char* path, const char* patchName, int destinationIndex, int sourceIndex) { auto res = std::static_pointer_cast( Ship::Context::GetInstance()->GetResourceManager()->LoadResource(path)); // Do not patch custom assets as they most likely do not have the same instructions as authentic assets if (res->GetInitData()->IsCustom) { return; } Gfx* destinationGfx = (Gfx*)&res->Instructions[destinationIndex]; Gfx sourceGfx = *(Gfx*)&res->Instructions[sourceIndex]; if (!originalGfx.contains(path) || !originalGfx[path].contains(patchName)) { originalGfx[path][patchName] = { destinationIndex, *destinationGfx }; } *destinationGfx = sourceGfx; } extern "C" void ResourceMgr_UnpatchGfxByName(const char* path, const char* patchName) { if (originalGfx.contains(path) && originalGfx[path].contains(patchName)) { auto res = std::static_pointer_cast( Ship::Context::GetInstance()->GetResourceManager()->LoadResource(path)); Gfx* gfx = (Gfx*)&res->Instructions[originalGfx[path][patchName].index]; *gfx = originalGfx[path][patchName].instruction; originalGfx[path].erase(patchName); } } extern "C" size_t ResourceMgr_GetPatchCountForDL(const char* path) { if (originalGfx.contains(path)) { return originalGfx[path].size(); } return 0; } extern "C" void ResourceMgr_ResetAllPatchesForDL(const char* path) { if (!originalGfx.contains(path)) { return; } auto res = std::static_pointer_cast( Ship::Context::GetInstance()->GetResourceManager()->LoadResource(path)); // Iterate through all patches and restore original instructions auto& patches = originalGfx[path]; for (auto it = patches.begin(); it != patches.end();) { Gfx* gfx = (Gfx*)&res->Instructions[it->second.index]; *gfx = it->second.instruction; // erase() returns the next iterator, allowing safe iteration during removal it = patches.erase(it); } // Clean up empty map entry if (patches.empty()) { originalGfx.erase(path); } } extern "C" char* ResourceMgr_LoadVtxArrayByName(const char* path) { auto res = std::static_pointer_cast(GetResourceByName(path)); return (char*)res->Vertices.data(); } extern "C" size_t ResourceMgr_GetVtxArraySizeByName(const char* path) { auto res = std::static_pointer_cast(GetResourceByName(path)); return res->Vertices.size(); } extern "C" char* ResourceMgr_LoadArrayByName(const char* path) { auto res = std::static_pointer_cast(GetResourceByName(path)); return (char*)res->Scalars.data(); } extern "C" size_t ResourceMgr_GetArraySizeByName(const char* path) { auto res = std::static_pointer_cast(GetResourceByName(path)); return res->Scalars.size(); } // Loads U8 data from an Array resource into an externally managed buffer, or mallocs a new buffer // if the passed in a nullptr. This malloced buffer must be freed by the caller. extern "C" u8* ResourceMgr_LoadArrayByNameAsU8(const char* path, u8* buffer) { auto res = std::static_pointer_cast(GetResourceByName(path)); if (buffer == nullptr) { buffer = (u8*)malloc(sizeof(u8) * res->Scalars.size()); } for (size_t i = 0; i < res->Scalars.size(); i++) { buffer[i] = res->Scalars[i].u8; } return buffer; } // Loads Vec3s data from an Array resource. // mallocs a new buffer that must be freed by the caller. extern "C" char* ResourceMgr_LoadArrayByNameAsVec3s(const char* path) { auto res = std::static_pointer_cast(GetResourceByName(path)); // if (res->CachedGameAsset != nullptr) // return (char*)res->CachedGameAsset; // else // { Vec3s* data = (Vec3s*)malloc(sizeof(Vec3s) * res->Scalars.size()); for (size_t i = 0; i < res->Scalars.size(); i += 3) { data[(i / 3)].x = res->Scalars[i + 0].s16; data[(i / 3)].y = res->Scalars[i + 1].s16; data[(i / 3)].z = res->Scalars[i + 2].s16; } // res->CachedGameAsset = data; return (char*)data; // } } extern "C" AnimatedMaterial* ResourceMgr_LoadAnimatedMatByName(const char* path) { return (AnimatedMaterial*)ResourceGetDataByName(path); } extern "C" CollisionHeader* ResourceMgr_LoadColByName(const char* path) { return (CollisionHeader*)ResourceGetDataByName(path); } extern "C" Vtx* ResourceMgr_LoadVtxByName(char* path) { return (Vtx*)ResourceGetDataByName(path); } extern "C" Mtx* ResourceMgr_LoadMtxByName(char* path) { return (Mtx*)ResourceGetDataByName(path); } extern "C" SequenceData ResourceMgr_LoadSeqByName(const char* path) { SequenceData* sequence = (SequenceData*)ResourceGetDataByName(path); return *sequence; } extern "C" SequenceData* ResourceMgr_LoadSeqPtrByName(const char* path) { SequenceData* sequence = (SequenceData*)ResourceGetDataByName(path); return sequence; } extern "C" KeyFrameSkeleton* ResourceMgr_LoadKeyFrameSkelByName(const char* path) { return (KeyFrameSkeleton*)ResourceGetDataByName(path); } extern "C" KeyFrameAnimation* ResourceMgr_LoadKeyFrameAnimByName(const char* path) { return (KeyFrameAnimation*)ResourceGetDataByName(path); } // std::map cachedCustomSFs; #if 0 extern "C" SoundFontSample* ReadCustomSample(const char* path) { return nullptr; /* if (!ExtensionCache.contains(path)) return nullptr; ExtensionEntry entry = ExtensionCache[path]; auto sampleRaw = Ship::Context::GetInstance()->GetResourceManager()->LoadFile(entry.path); uint32_t* strem = (uint32_t*)sampleRaw->Buffer.get(); uint8_t* strem2 = (uint8_t*)strem; SoundFontSample* sampleC = new SoundFontSample; if (entry.ext == "wav") { drwav_uint32 channels; drwav_uint32 sampleRate; drwav_uint64 totalPcm; drmp3_int16* pcmData = drwav_open_memory_and_read_pcm_frames_s16(strem2, sampleRaw->BufferSize, &channels, &sampleRate, &totalPcm, NULL); sampleC->size = totalPcm; sampleC->sampleAddr = (uint8_t*)pcmData; sampleC->codec = CODEC_S16; sampleC->loop = new AdpcmLoop; sampleC->loop->start = 0; sampleC->loop->end = sampleC->size - 1; sampleC->loop->count = 0; sampleC->sampleRateMagicValue = 'RIFF'; sampleC->sampleRate = sampleRate; cachedCustomSFs[path] = sampleC; return sampleC; } else if (entry.ext == "mp3") { drmp3_config mp3Info; drmp3_uint64 totalPcm; drmp3_int16* pcmData = drmp3_open_memory_and_read_pcm_frames_s16(strem2, sampleRaw->BufferSize, &mp3Info, &totalPcm, NULL); sampleC->size = totalPcm * mp3Info.channels * sizeof(short); sampleC->sampleAddr = (uint8_t*)pcmData; sampleC->codec = CODEC_S16; sampleC->loop = new AdpcmLoop; sampleC->loop->start = 0; sampleC->loop->end = sampleC->size; sampleC->loop->count = 0; sampleC->sampleRateMagicValue = 'RIFF'; sampleC->sampleRate = mp3Info.sampleRate; cachedCustomSFs[path] = sampleC; return sampleC; } return nullptr; */ } extern "C" SoundFontSample* ResourceMgr_LoadAudioSample(const char* path) { return (SoundFontSample*)ResourceGetDataByName(path); } #endif extern "C" SoundFont* ResourceMgr_LoadAudioSoundFontByName(const char* path) { return (SoundFont*)ResourceGetDataByName(path); } extern "C" SoundFont* ResourceMgr_LoadAudioSoundFontByCRC(uint64_t crc) { return (SoundFont*)ResourceGetDataByCrc(crc); } extern "C" int ResourceMgr_OTRSigCheck(char* imgData) { uintptr_t i = (uintptr_t)(imgData); // if (i == 0xD9000000 || i == 0xE7000000 || (i & 1) == 1) if ((i & 1) == 1) return 0; // if ((i & 0xFF000000) != 0xAB000000 && (i & 0xFF000000) != 0xCD000000 && i != 0) { if (i != 0) { if (imgData[0] == '_' && imgData[1] == '_' && imgData[2] == 'O' && imgData[3] == 'T' && imgData[4] == 'R' && imgData[5] == '_' && imgData[6] == '_') return 1; } return 0; } extern "C" AnimationHeaderCommon* ResourceMgr_LoadAnimByName(const char* path) { return (AnimationHeaderCommon*)ResourceGetDataByName(path); } extern "C" SkeletonHeader* ResourceMgr_LoadSkeletonByName(const char* path, SkelAnime* skelAnime) { std::string pathStr = std::string(path); static const std::string sOtr = "__OTR__"; if (pathStr.starts_with(sOtr)) { pathStr = pathStr.substr(sOtr.length()); } bool isAlt = ResourceMgr_IsAltAssetsEnabled(); if (isAlt) { pathStr = Ship::IResource::gAltAssetPrefix + pathStr; } SkeletonHeader* skelHeader = (SkeletonHeader*)ResourceGetDataByName(pathStr.c_str()); // If there isn't an alternate model, load the regular one if (isAlt && skelHeader == NULL) { skelHeader = (SkeletonHeader*)ResourceGetDataByName(path); } // This function is only called when a skeleton is initialized. // Therefore we can take this opportunity to take note of the Skeleton that is created... if (skelAnime != nullptr) { auto stringPath = std::string(path); // Ship::SkeletonPatcher::RegisterSkeleton(stringPath, skelAnime); } return skelHeader; } extern "C" void ResourceMgr_UnregisterSkeleton(SkelAnime* skelAnime) { if (skelAnime != nullptr) SOH::SkeletonPatcher::UnregisterSkeleton(skelAnime); } extern "C" void ResourceMgr_ClearSkeletons(SkelAnime* skelAnime) { if (skelAnime != nullptr) SOH::SkeletonPatcher::ClearSkeletons(); } extern "C" s32* ResourceMgr_LoadCSByName(const char* path) { return (s32*)ResourceGetDataByName(path); } std::filesystem::path GetSaveFile(std::shared_ptr Conf) { const std::string fileName = Conf->GetString("Game.SaveName", Ship::Context::GetPathRelativeToAppDirectory("oot_save.sav")); std::filesystem::path saveFile = std::filesystem::absolute(fileName); if (!exists(saveFile.parent_path())) { create_directories(saveFile.parent_path()); } return saveFile; } std::filesystem::path GetSaveFile() { const std::shared_ptr pConf = OTRGlobals::Instance->context->GetConfig(); return GetSaveFile(pConf); } void OTRGlobals::CheckSaveFile(size_t sramSize) const { const std::shared_ptr pConf = Instance->context->GetConfig(); std::filesystem::path savePath = GetSaveFile(pConf); std::fstream saveFile(savePath, std::fstream::in | std::fstream::out | std::fstream::binary); if (saveFile.fail()) { saveFile.open(savePath, std::fstream::in | std::fstream::out | std::fstream::binary | std::fstream::app); for (int i = 0; i < sramSize; ++i) { saveFile.write("\0", 1); } } saveFile.close(); } // extern "C" void Ctx_ReadSaveFile(uintptr_t addr, void* dramAddr, size_t size) { // SaveManager::ReadSaveFile(GetSaveFile(), addr, dramAddr, size); // } // extern "C" void Ctx_WriteSaveFile(uintptr_t addr, void* dramAddr, size_t size) { // SaveManager::WriteSaveFile(GetSaveFile(), addr, dramAddr, size); // } std::wstring StringToU16(const std::string& s) { std::vector result; size_t i = 0; while (i < s.size()) { unsigned long uni; size_t nbytes; bool error = false; unsigned char c = s[i++]; if (c < 0x80) { // ascii uni = c; nbytes = 0; } else if (c <= 0xBF) { // assuming kata/hiragana delimiter nbytes = 0; uni = '\1'; } else if (c <= 0xDF) { uni = c & 0x1F; nbytes = 1; } else if (c <= 0xEF) { uni = c & 0x0F; nbytes = 2; } else if (c <= 0xF7) { uni = c & 0x07; nbytes = 3; } for (size_t j = 0; j < nbytes; ++j) { unsigned char c = s[i++]; uni <<= 6; uni += c & 0x3F; } if (uni != '\1') result.push_back(uni); } std::wstring utf16; for (size_t i = 0; i < result.size(); ++i) { unsigned long uni = result[i]; if (uni <= 0xFFFF) { utf16 += (wchar_t)uni; } else { uni -= 0x10000; utf16 += (wchar_t)((uni >> 10) + 0xD800); utf16 += (wchar_t)((uni & 0x3FF) + 0xDC00); } } return utf16; } int CopyStringToCharBuffer(const std::string& inputStr, char* buffer, const int maxBufferSize) { if (!inputStr.empty()) { // Prevent potential horrible overflow due to implicit conversion of maxBufferSize to an unsigned. Prevents // negatives. memset(buffer, 0, std::max(0, maxBufferSize)); // Gaurentee that this value will be greater than 0, regardless of passed variables. const int copiedCharLen = std::min(std::max(0, maxBufferSize - 1), inputStr.length()); memcpy(buffer, inputStr.c_str(), copiedCharLen); return copiedCharLen; } return 0; } extern "C" void OTRGfxPrint(const char* str, void* printer, void (*printImpl)(void*, char)) { const std::vector hira1 = { u'を', u'ぁ', u'ぃ', u'ぅ', u'ぇ', u'ぉ', u'ゃ', u'ゅ', u'ょ', u'っ', u'-', u'あ', u'い', u'う', u'え', u'お', u'か', u'き', u'く', u'け', u'こ', u'さ', u'し', u'す', u'せ', u'そ', }; const std::vector hira2 = { u'た', u'ち', u'つ', u'て', u'と', u'な', u'に', u'ぬ', u'ね', u'の', u'は', u'ひ', u'ふ', u'へ', u'ほ', u'ま', u'み', u'む', u'め', u'も', u'や', u'ゆ', u'よ', u'ら', u'り', u'る', u'れ', u'ろ', u'わ', u'ん', u'゛', u'゜', }; std::wstring wstr = StringToU16(str); for (const auto& c : wstr) { if (c < 0x80) { printImpl(printer, c); } else if (c >= u'。' && c <= u'゚') { // katakana printImpl(printer, c - 0xFEC0); } else { auto it = std::find(hira1.begin(), hira1.end(), c); if (it != hira1.end()) { // hiragana block 1 printImpl(printer, 0x88 + std::distance(hira1.begin(), it)); } auto it2 = std::find(hira2.begin(), hira2.end(), c); if (it2 != hira2.end()) { // hiragana block 2 printImpl(printer, 0xe0 + std::distance(hira2.begin(), it2)); } } } } // Gets the width of the main ImGui window extern "C" uint32_t OTRGetCurrentWidth() { return OTRGlobals::Instance->context->GetWindow()->GetWidth(); } // Gets the height of the main ImGui window extern "C" uint32_t OTRGetCurrentHeight() { return OTRGlobals::Instance->context->GetWindow()->GetHeight(); } Color_RGB8 GetColorForControllerLED() { #if 0 auto brightness = CVarGetFloat("gLedBrightness", 1.0f) / 1.0f; Color_RGB8 color = { 0, 0, 0 }; if (brightness > 0.0f) { LEDColorSource source = static_cast(CVarGetInteger("gLedColorSource", LED_SOURCE_TUNIC_ORIGINAL)); bool criticalOverride = CVarGetInteger("gLedCriticalOverride", 1); if (gPlayState && (source == LED_SOURCE_TUNIC_ORIGINAL || source == LED_SOURCE_TUNIC_COSMETICS)) { switch (CUR_EQUIP_VALUE(EQUIP_TUNIC) - 1) { case PLAYER_TUNIC_KOKIRI: color = source == LED_SOURCE_TUNIC_COSMETICS ? CVarGetColor24("gCosmetics.Link_KokiriTunic.Value", kokiriColor) : kokiriColor; break; case PLAYER_TUNIC_GORON: color = source == LED_SOURCE_TUNIC_COSMETICS ? CVarGetColor24("gCosmetics.Link_GoronTunic.Value", goronColor) : goronColor; break; case PLAYER_TUNIC_ZORA: color = source == LED_SOURCE_TUNIC_COSMETICS ? CVarGetColor24("gCosmetics.Link_ZoraTunic.Value", zoraColor) : zoraColor; break; } } if (source == LED_SOURCE_CUSTOM) { color = CVarGetColor24("gLedPort1Color", { 255, 255, 255 }); } if (criticalOverride || source == LED_SOURCE_HEALTH) { if (HealthMeter_IsCritical()) { color = { 0xFF, 0, 0 }; } else if (source == LED_SOURCE_HEALTH) { if (gSaveContext.health / gSaveContext.healthCapacity <= 0.4f) { color = { 0xFF, 0xFF, 0 }; } else { color = { 0, 0xFF, 0 }; } } } color.r = color.r * brightness; color.g = color.g * brightness; color.b = color.b * brightness; } #endif return { 0, 0, 0 }; } extern "C" void OTRControllerCallback(uint8_t rumble) { // We call this every tick, SDL accounts for this use and prevents driver spam // https://github.com/libsdl-org/SDL/blob/f17058b562c8a1090c0c996b42982721ace90903/src/joystick/SDL_joystick.c#L1114-L1144 Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(0)->GetLED()->SetLEDColor( GetColorForControllerLED()); static std::shared_ptr controllerConfigWindow = nullptr; if (controllerConfigWindow == nullptr) { controllerConfigWindow = std::dynamic_pointer_cast( Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("2S2H Input Editor")); // note: the current implementation may not be desired in LUS, as "true" rumble support // using osMotor calls is planned: https://github.com/Kenix3/libultraship/issues/9 } if (controllerConfigWindow->TestingRumble()) { return; } // TODO: other ports? if (rumble) { Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(0)->GetRumble()->StartRumble(); } else { Ship::Context::GetInstance()->GetControlDeck()->GetControllerByPort(0)->GetRumble()->StopRumble(); } } extern "C" float OTRGetAspectRatio() { return Ship::Context::GetInstance()->GetWindow()->GetAspectRatio(); } extern "C" float OTRGetDimensionFromLeftEdge(float v) { auto fastWnd = dynamic_pointer_cast(Ship::Context::GetInstance()->GetWindow()); auto intP = fastWnd->GetInterpreterWeak().lock(); if (!intP) { assert(false && "Lost reference to Fast::Interpreter"); return v; } auto gfx_native_dimensions = intP->mNativeDimensions; return (gfx_native_dimensions.width / 2 - gfx_native_dimensions.height / 2 * OTRGetAspectRatio() + (v)); } extern "C" float OTRGetDimensionFromRightEdge(float v) { auto fastWnd = dynamic_pointer_cast(Ship::Context::GetInstance()->GetWindow()); auto intP = fastWnd->GetInterpreterWeak().lock(); if (!intP) { assert(false && "Lost reference to Fast::Interpreter"); return v; } auto gfx_native_dimensions = intP->mNativeDimensions; return (gfx_native_dimensions.width / 2 + gfx_native_dimensions.height / 2 * OTRGetAspectRatio() - (gfx_native_dimensions.width - v)); } // Gets the width of the current render target area extern "C" uint32_t OTRGetGameRenderWidth() { auto fastWnd = dynamic_pointer_cast(Ship::Context::GetInstance()->GetWindow()); auto intP = fastWnd->GetInterpreterWeak().lock(); if (!intP) { assert(false && "Lost reference to Fast::Interpreter"); return 320; } uint32_t height, width; intP->GetCurDimensions(&width, &height); return width; } // Gets the height of the current render target area extern "C" uint32_t OTRGetGameRenderHeight() { auto fastWnd = dynamic_pointer_cast(Ship::Context::GetInstance()->GetWindow()); auto intP = fastWnd->GetInterpreterWeak().lock(); if (!intP) { assert(false && "Lost reference to Fast::Interpreter"); return 240; } uint32_t height, width; intP->GetCurDimensions(&width, &height); return height; } f32 floorf(f32 x); f32 ceilf(f32 x); extern "C" int16_t OTRGetRectDimensionFromLeftEdge(float v) { return ((int)floorf(OTRGetDimensionFromLeftEdge(v))); } extern "C" int16_t OTRGetRectDimensionFromRightEdge(float v) { return ((int)ceilf(OTRGetDimensionFromRightEdge(v))); } // Takes a HUD coordinate(320x240) and converts it to the game window pixel coordinates (any size, any aspect ratio) // Though the HUD uses a 320x240 coordinates system, the size of the HUD box is scaled up to match the window height // If the game window is 4:3, this will return the same value. /* Example, if the game window is 16:9 at twice the resolution of the HUD: Calling with X (0,0) will return 8 Calling with Y (1,1) will return 10 . . . x _ _ _ _ _ _ _ . . . . . . _ y _ _ _ _ _ _ . . . . . . _ _ _ HUD _ _ _ . . . . . . _ _ _ _ _ _ _ _ . . . . . . _ _ _ _ _ _ _ _ . . . */ extern "C" int32_t OTRConvertHUDXToScreenX(int32_t v) { auto fastWnd = dynamic_pointer_cast(Ship::Context::GetInstance()->GetWindow()); auto intP = fastWnd->GetInterpreterWeak().lock(); if (!intP) { assert(false && "Lost reference to Fast::Interpreter"); return v; } uint32_t gameHeight, gameWidth; float gameAspectRatio = fastWnd->GetAspectRatio(); intP->GetCurDimensions(&gameWidth, &gameHeight); float hudAspectRatio = 4.0f / 3.0f; int32_t hudHeight = gameHeight; int32_t hudWidth = hudHeight * hudAspectRatio; float hudScreenRatio = (hudWidth / 320.0f); float hudCoord = v * hudScreenRatio; float gameOffset = (int32_t(gameWidth) - hudWidth) / 2; float gameCoord = hudCoord + gameOffset; float gameScreenRatio = (320.0f / gameWidth); float screenScaledCoord = gameCoord * gameScreenRatio; int32_t screenScaledCoordInt = screenScaledCoord; return screenScaledCoordInt; } extern "C" void Gfx_RegisterBlendedTexture(const char* name, u8* mask, u8* replacement) { if (auto intP = dynamic_pointer_cast(Ship::Context::GetInstance()->GetWindow()) ->GetInterpreterWeak() .lock()) { intP->RegisterBlendedTexture(name, mask, replacement); } else { assert(false && "Lost reference to Fast::Interpreter"); } } extern "C" void Gfx_UnregisterBlendedTexture(const char* name) { if (auto intP = dynamic_pointer_cast(Ship::Context::GetInstance()->GetWindow()) ->GetInterpreterWeak() .lock()) { intP->UnregisterBlendedTexture(name); } else { assert(false && "Lost reference to Fast::Interpreter"); } } extern "C" void Gfx_TextureCacheDelete(const uint8_t* texAddr) { char* imgName = (char*)texAddr; if (texAddr == nullptr) { return; } if (ResourceMgr_OTRSigCheck(imgName)) { texAddr = (const uint8_t*)ResourceGetDataByName(imgName); } if (auto intP = dynamic_pointer_cast(Ship::Context::GetInstance()->GetWindow()) ->GetInterpreterWeak() .lock()) { intP->TextureCacheDelete(texAddr); } else { assert(false && "Lost reference to Fast::Interpreter"); } } extern "C" int AudioPlayer_Buffered(void) { return AudioPlayerBuffered(); } extern "C" int AudioPlayer_GetDesiredBuffered(void) { return AudioPlayerGetDesiredBuffered(); } extern "C" void AudioPlayer_Play(const uint8_t* buf, uint32_t len) { AudioPlayerPlayFrame(buf, len); } extern "C" int Controller_ShouldRumble(size_t slot) { // don't rumble if we don't have rumble mappings if (Ship::Context::GetInstance() ->GetControlDeck() ->GetControllerByPort(static_cast(slot)) ->GetRumble() ->GetAllRumbleMappings() .empty()) { return 0; } // don't rumble if we don't have connected gamepads if (Ship::Context::GetInstance() ->GetControlDeck() ->GetConnectedPhysicalDeviceManager() ->GetConnectedSDLGamepadsForPort(slot) .empty()) { return 0; } // rumble return 1; } extern "C" void Messagebox_ShowErrorBox(char* title, char* body) { Extractor::ShowErrorBox(title, body); } // Helper to redirect the user to the boot screen in place of known console crash scenarios, and emits a notification extern "C" bool Ship_HandleConsoleCrashAsReset() { // If fix crashes is on, return false and let fallback handling process in source if (CVarGetInteger("gEnhancements.Fixes.ConsoleCrashes", 1)) { return false; } std::reinterpret_pointer_cast( Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console")) ->Dispatch("reset"); Notification::Emit({ .itemIcon = "__OTR__icon_item_24_static_yar/gQuestIconGoldSkulltulaTex", .message = "Crash prevented!", .remainingTime = 10.0f, }); return true; }