diff --git a/sonic3air-main/Oxygen/oxygenengine/source/oxygen/resources/ResourcesCache.cpp b/sonic3air-main/Oxygen/oxygenengine/source/oxygen/resources/ResourcesCache.cpp new file mode 100644 index 00000000..63621aaf --- /dev/null +++ b/sonic3air-main/Oxygen/oxygenengine/source/oxygen/resources/ResourcesCache.cpp @@ -0,0 +1,423 @@ +/* +* Part of the Oxygen Engine / Sonic 3 A.I.R. software distribution. +* Copyright (C) 2017-2024 by Eukaryot +* +* Published under the GNU GPLv3 open source software license, see license.txt +* or https://www.gnu.org/licenses/gpl-3.0.en.html +*/ + +#include "oxygen/pch.h" +#include "oxygen/resources/ResourcesCache.h" +#include "oxygen/application/Configuration.h" +#include "oxygen/application/modding/ModManager.h" +#include "oxygen/helper/FileHelper.h" +#include "oxygen/helper/JsonHelper.h" +#include "oxygen/helper/Logging.h" +#include "oxygen/platform/PlatformFunctions.h" + + +bool ResourcesCache::loadRom() +{ + mRom.clear(); + + // Load ROM content + Configuration& config = Configuration::instance(); + const GameProfile& gameProfile = GameProfile::instance(); + std::wstring romPath; + bool loaded = false; + bool saveRom = false; + + // First have a look at the game's app data, where the ROM gets copied to after it was found once + if (!loaded && !config.mAppDataPath.empty()) + { + for (const GameProfile::RomInfo& romInfo : gameProfile.mRomInfos) + { + romPath = config.mAppDataPath + romInfo.mSteamRomName; + loaded = loadRomFile(romPath, romInfo); + if (loaded) + break; + } + + // If ROM is not found yet, but in one of the next steps, then make sure to copy it into the app data folder afterwards + saveRom = !loaded; + } + +#if !defined(PLATFORM_ANDROID) + // Try at last known ROM location, if there is one + if (!loaded && !config.mLastRomPath.empty()) + { + romPath = config.mLastRomPath; + loaded = loadRomFile(romPath); + } + + // Then check at the configuration ROM path + if (!loaded && !config.mRomPath.empty()) + { + romPath = config.mRomPath; + loaded = loadRomFile(romPath); + } +#endif + + // Or is it the Steam ROM right inside the installation directory? + if (!loaded) + { + for (const GameProfile::RomInfo& romInfo : gameProfile.mRomInfos) + { + romPath = romInfo.mSteamRomName; + loaded = loadRomFile(romPath, romInfo); + if (loaded) + break; + } + } + +#if defined(PLATFORM_WINDOWS) || defined(PLATFORM_LINUX) + // If still not loaded, search for Steam installation of the game + if (!loaded && !gameProfile.mRomInfos.empty()) + { + RMX_LOG_INFO("Trying to find Steam ROM"); + for (const GameProfile::RomInfo& romInfo : gameProfile.mRomInfos) + { + romPath = PlatformFunctions::tryGetSteamRomPath(romInfo.mSteamRomName); + if (!romPath.empty()) + { + loaded = loadRomFile(romPath, romInfo); + if (loaded) + break; + } + } + } +#endif + + // If ROM was still not loaded, it's time to give up now... + if (!loaded) + { + return false; + } + + if (saveRom) + { + // Note that this updates the config + saveRomToAppData(); + } + else + { + // Update config if there was a change + if (romPath != config.mLastRomPath) + { + config.mLastRomPath = romPath; + config.saveSettings(); + } + } + + // Done + return true; +} + +bool ResourcesCache::loadRomFromFile(const std::wstring& filename) +{ + if (!loadRomFile(filename)) + return false; + + saveRomToAppData(); + return true; +} + +bool ResourcesCache::loadRomFromMemory(const std::vector& content) +{ + if (!loadRomMemory(content)) + return false; + + saveRomToAppData(); + return true; +} + +void ResourcesCache::loadAllResources() +{ + // Load raw data incl. ROM injections + mRawDataMap.clear(); + mRomInjections.clear(); + mRawDataPool.clear(); + loadRawData(L"data/rawdata", false); + for (const Mod* mod : ModManager::instance().getActiveMods()) + { + loadRawData(mod->mFullPath + L"rawdata", true); + } + + // Load palettes + mPalettes.clear(); + loadPalettes(L"data/palettes", false); + for (const Mod* mod : ModManager::instance().getActiveMods()) + { + loadPalettes(mod->mFullPath + L"palettes", true); + } +} + +const std::vector& ResourcesCache::getRawData(uint64 key) const +{ + static const std::vector EMPTY; + const auto it = mRawDataMap.find(key); + return (it == mRawDataMap.end()) ? EMPTY : it->second; +} + +const ResourcesCache::Palette* ResourcesCache::getPalette(uint64 key, uint8 line) const +{ + return mapFind(mPalettes, key + line); +} + +void ResourcesCache::applyRomInjections(uint8* rom, uint32 romSize) const +{ + for (const RawData* rawData : mRomInjections) + { + RMX_CHECK(rawData->mRomInjectAddress < romSize, "ROM injection at invalid address " << rmx::hexString(rawData->mRomInjectAddress, 6), continue); + const uint32 size = std::min((uint32)rawData->mContent.size(), romSize - rawData->mRomInjectAddress); + memcpy(&rom[rawData->mRomInjectAddress], &rawData->mContent[0], size); + } +} + +bool ResourcesCache::loadRomFile(const std::wstring& filename) +{ + const GameProfile::RomCheck& romCheck = GameProfile::instance().mRomCheck; + std::vector content; + content.reserve(romCheck.mSize > 0 ? romCheck.mSize : 0x400000); + if (!FTX::FileSystem->readFile(filename, content)) + return false; + + return loadRomMemory(content); +} + +bool ResourcesCache::loadRomFile(const std::wstring& filename, const GameProfile::RomInfo& romInfo) +{ + const GameProfile::RomCheck& romCheck = GameProfile::instance().mRomCheck; + mRom.reserve(romCheck.mSize > 0 ? romCheck.mSize : 0x400000); + if (!FTX::FileSystem->readFile(filename, mRom)) + return false; + + // If ROM info defines a required header checksum, make sure it fits (this is meant to be an early-out before doing the potentially expensive code below) + const uint64 headerChecksum = getHeaderChecksum(mRom); + if (romInfo.mHeaderChecksum != 0 && romInfo.mHeaderChecksum != headerChecksum) + return false; + + if (applyRomModifications(romInfo)) + { + if (checkRomContent()) + { + mLoadedRomInfo = &romInfo; + return true; + } + } + return false; +} + +bool ResourcesCache::loadRomMemory(const std::vector& content) +{ + const uint64 headerChecksum = getHeaderChecksum(content); + if (GameProfile::instance().mRomInfos.empty()) + { + mRom = content; + if (checkRomContent()) + return true; + } + else + { + for (const GameProfile::RomInfo& romInfo : GameProfile::instance().mRomInfos) + { + // If ROM info defines a required header checksum, make sure it fits (this is meant to be an early-out before doing the potentially expensive code below) + if (romInfo.mHeaderChecksum != 0 && romInfo.mHeaderChecksum != headerChecksum) + continue; + + mRom = content; + if (applyRomModifications(romInfo)) + { + if (checkRomContent()) + { + mLoadedRomInfo = &romInfo; + return true; + } + } + } + } + return false; +} + +uint64 ResourcesCache::getHeaderChecksum(const std::vector& content) +{ + if (content.empty()) + return 0; + + // Regard the first 512 byte as header + return rmx::getMurmur2_64(&content[0], std::min(512, content.size())); +} + +bool ResourcesCache::applyRomModifications(const GameProfile::RomInfo& romInfo) +{ + if (!romInfo.mDiffFileName.empty()) + { + // Load diff file if needed + std::vector* content = nullptr; + const auto it = mDiffFileCache.find(&romInfo); + if (it == mDiffFileCache.end()) + { + content = &mDiffFileCache[&romInfo]; + FTX::FileSystem->readFile(romInfo.mDiffFileName, *content); + } + else + { + content = &it->second; + } + + // Apply diff file by XORing it in + if (content->size() != mRom.size()) + return false; + + uint64* ptr = (uint64*)&mRom[0]; + uint64* diff = (uint64*)&(*content)[0]; + const size_t count = content->size() / 8; + for (size_t i = 0; i < count; ++i) + { + ptr[i] ^= diff[i]; + } + } + + for (auto& pair : romInfo.mBlankRegions) + { + if (pair.first > pair.second || pair.second >= mRom.size()) + return false; + memset(&mRom[pair.first], 0, pair.second - pair.first + 1); + } + + for (auto& pair : romInfo.mOverwrites) + { + if (pair.first >= mRom.size()) + return false; + mRom[pair.first] = pair.second; + } + + return true; +} + +bool ResourcesCache::checkRomContent() +{ + // Check that it's the right ROM + const GameProfile::RomCheck& romCheck = GameProfile::instance().mRomCheck; + if (romCheck.mSize > 0) + { + if (mRom.size() != romCheck.mSize) + return false; + } + + if (romCheck.mChecksum != 0) + { + const uint64 checksum = rmx::getMurmur2_64(&mRom[0], mRom.size()); + if (checksum != romCheck.mChecksum) + return false; + } + + // ROM check succeeded + mDiffFileCache.clear(); // This cache is not needed again now + return true; +} + +void ResourcesCache::saveRomToAppData() +{ + if (nullptr != mLoadedRomInfo && !mLoadedRomInfo->mSteamRomName.empty()) + { + const std::wstring filepath = Configuration::instance().mAppDataPath + mLoadedRomInfo->mSteamRomName; + const bool success = FTX::FileSystem->saveFile(filepath, mRom); + if (success) + { + Configuration::instance().mLastRomPath = filepath; + } + else + { + RMX_ERROR("Failed to store a copy of the ROM in the app data folder", ); + } + } +} + +void ResourcesCache::loadRawData(const std::wstring& path, bool isModded) +{ + // Load raw data from the given path + std::vector fileEntries; + fileEntries.reserve(8); + FTX::FileSystem->listFilesByMask(path + L"/*.json", true, fileEntries); + for (const rmx::FileIO::FileEntry& fileEntry : fileEntries) + { + const Json::Value root = JsonHelper::loadFile(fileEntry.mPath + fileEntry.mFilename); + + for (auto it = root.begin(); it != root.end(); ++it) + { + const Json::Value& entryJson = *it; + if (!entryJson.isObject()) + continue; + + RawData* rawData = nullptr; + if (entryJson["File"].isString()) + { + const char* filename = entryJson["File"].asCString(); + const uint64 key = rmx::getMurmur2_64(String(it.key().asCString())); + rawData = &mRawDataPool.createObject(); + rawData->mIsModded = isModded; + if (!FTX::FileSystem->readFile(fileEntry.mPath + String(filename).toStdWString(), rawData->mContent)) + { + mRawDataPool.destroyObject(*rawData); + continue; + } + mRawDataMap[key].push_back(rawData); + } + + if (nullptr == rawData) + continue; + + // Check if it's a ROM injection + if (!entryJson["RomInject"].isNull()) + { + rawData->mRomInjectAddress = (uint32)rmx::parseInteger(entryJson["RomInject"].asCString()); + mRomInjections.emplace_back(rawData); + } + } + } +} + +void ResourcesCache::loadPalettes(const std::wstring& path, bool isModded) +{ + // Load palettes from the given path + std::vector fileEntries; + fileEntries.reserve(8); + FTX::FileSystem->listFilesByMask(path + L"/*.png", true, fileEntries); + for (const rmx::FileIO::FileEntry& fileEntry : fileEntries) + { + if (!FTX::FileSystem->exists(fileEntry.mPath + fileEntry.mFilename)) + continue; + + std::vector content; + if (!FTX::FileSystem->readFile(fileEntry.mPath + fileEntry.mFilename, content)) + continue; + + Bitmap bitmap; + if (!bitmap.load(fileEntry.mPath + fileEntry.mFilename)) + { + RMX_ERROR("Failed to load PNG at '" << *WString(fileEntry.mPath + fileEntry.mFilename).toString() << "'", ); + continue; + } + + String name = WString(fileEntry.mFilename).toString(); + name.remove(name.length() - 4, 4); + + uint64 key = rmx::getMurmur2_64(name); // Hash is the key of the first palette, the others are enumerated from there + const int numLines = std::min(bitmap.getHeight(), 64); + const int numColorsPerLine = std::min(bitmap.getWidth(), 64); + + for (int y = 0; y < numLines; ++y) + { + Palette& palette = mPalettes[key]; + palette.mIsModded = isModded; + palette.mColors.resize(numColorsPerLine); + + for (int x = 0; x < numColorsPerLine; ++x) + { + palette.mColors[x] = Color::fromABGR32(bitmap.getPixel(x, y)); + } + ++key; + } + } +} diff --git a/sonic3air-main/Oxygen/oxygenengine/source/oxygen/resources/ResourcesCache.h b/sonic3air-main/Oxygen/oxygenengine/source/oxygen/resources/ResourcesCache.h new file mode 100644 index 00000000..ce0075bc --- /dev/null +++ b/sonic3air-main/Oxygen/oxygenengine/source/oxygen/resources/ResourcesCache.h @@ -0,0 +1,65 @@ +/* +* Part of the Oxygen Engine / Sonic 3 A.I.R. software distribution. +* Copyright (C) 2017-2024 by Eukaryot +* +* Published under the GNU GPLv3 open source software license, see license.txt +* or https://www.gnu.org/licenses/gpl-3.0.en.html +*/ + +#pragma once + +#include "oxygen/application/GameProfile.h" + + +class ResourcesCache : public SingleInstance +{ +public: + struct RawData + { + std::vector mContent; + uint32 mRomInjectAddress = 0xffffffff; + bool mIsModded = false; + }; + + struct Palette + { + std::vector mColors; + bool mIsModded = false; + }; + +public: + bool loadRom(); + bool loadRomFromFile(const std::wstring& filename); + bool loadRomFromMemory(const std::vector& content); + + void loadAllResources(); + + inline const std::vector& getUnmodifiedRom() const { return mRom; } + const std::vector& getRawData(uint64 key) const; + const Palette* getPalette(uint64 key, uint8 line) const; + + void applyRomInjections(uint8* rom, uint32 romSize) const; + +private: + bool loadRomFile(const std::wstring& filename); + bool loadRomFile(const std::wstring& filename, const GameProfile::RomInfo& romInfo); + bool loadRomMemory(const std::vector& content); + uint64 getHeaderChecksum(const std::vector& content); + bool applyRomModifications(const GameProfile::RomInfo& romInfo); + bool checkRomContent(); + void saveRomToAppData(); + + void loadRawData(const std::wstring& path, bool isModded); + void loadPalettes(const std::wstring& path, bool isModded); + +private: + std::vector mRom; // This is the original, unmodified ROM (i.e. without any raw data injections or ROM writes) + const GameProfile::RomInfo* mLoadedRomInfo = nullptr; + std::map> mDiffFileCache; + + std::map> mRawDataMap; + std::vector mRomInjections; + ObjectPool mRawDataPool; + + std::map mPalettes; +}; diff --git a/sonic3air-main/Oxygen/oxygenengine/source/oxygen/resources/SpriteCache.cpp b/sonic3air-main/Oxygen/oxygenengine/source/oxygen/resources/SpriteCache.cpp new file mode 100644 index 00000000..e8b2effb --- /dev/null +++ b/sonic3air-main/Oxygen/oxygenengine/source/oxygen/resources/SpriteCache.cpp @@ -0,0 +1,536 @@ +/* +* Part of the Oxygen Engine / Sonic 3 A.I.R. software distribution. +* Copyright (C) 2017-2024 by Eukaryot +* +* Published under the GNU GPLv3 open source software license, see license.txt +* or https://www.gnu.org/licenses/gpl-3.0.en.html +*/ + +#include "oxygen/pch.h" +#include "oxygen/resources/SpriteCache.h" +#include "oxygen/application/Configuration.h" +#include "oxygen/application/EngineMain.h" +#include "oxygen/application/modding/ModManager.h" +#include "oxygen/helper/FileHelper.h" +#include "oxygen/helper/JsonHelper.h" +#include "oxygen/rendering/sprite/SpriteDump.h" +#include "oxygen/rendering/utils/Kosinski.h" +#include "oxygen/simulation/EmulatorInterface.h" +#include "oxygen/simulation/LemonScriptRuntime.h" + +#ifdef DEBUG + //#define CREATE_SPRITEDUMP +#endif + + +namespace +{ + + void decodeROMSpriteData(EmulatorInterface& emulatorInterface, std::vector& patternBuffer, uint32 patternsBaseAddress, uint32 patternAddress, SpriteCache::ROMSpriteEncoding encoding) + { + switch (encoding) + { + case SpriteCache::ROMSpriteEncoding::NONE: + { + // Uncompressed / unpacked data + const uint16 numPatterns = patternAddress; + RenderUtils::expandMultiplePatternDataFromROM(patternBuffer, emulatorInterface.getMemoryPointer(patternsBaseAddress, false, numPatterns * 0x20), numPatterns); + break; + } + + case SpriteCache::ROMSpriteEncoding::CHARACTER: + { + // Variant for character sprites + int numSprites = emulatorInterface.readMemory16(patternAddress); + uint32 address = patternAddress + 2; + + for (int i = 0; i < numSprites; ++i) + { + const uint16 data = emulatorInterface.readMemory16(address); + address += 2; + + uint32 src = patternsBaseAddress + (data & 0x0fff) * 0x20; + const uint16 numPatterns = ((data & 0xf000) >> 12) + 1; + + RenderUtils::expandMultiplePatternDataFromROM(patternBuffer, emulatorInterface.getRom() + src, numPatterns); + } + break; + } + + case SpriteCache::ROMSpriteEncoding::OBJECT: + { + // Variant for other object sprites + int numSprites = emulatorInterface.readMemory16(patternAddress) + 1; + uint32 address = patternAddress + 2; + + for (int i = 0; i < numSprites; ++i) + { + const uint16 data = emulatorInterface.readMemory16(address); + address += 2; + + uint32 src = patternsBaseAddress + (data & 0x7ff0) * 2; + const uint16 numPatterns = (data & 0x000f) + 1; + + RenderUtils::expandMultiplePatternDataFromROM(patternBuffer, emulatorInterface.getRom() + src, numPatterns); + } + break; + } + + case SpriteCache::ROMSpriteEncoding::KOSINSKI: + { + // Using Kosinski compressed data + uint8 buffer[0x1000]; + + // Get the decompressed size + uint16 size = emulatorInterface.readMemory16(patternsBaseAddress); + if (size == 0xa000) + size = 0x8000; + uint32 inputAddress = patternsBaseAddress + 2; + + while (size > 0) + { + uint8* pointer = buffer; + Kosinski::decompress(emulatorInterface, pointer, inputAddress); + + const uint16 bytes = std::min(size, 0x1000); + RMX_ASSERT((bytes & 0x1f) == 0, "Expected decompressed data size to be divisible by 0x20"); + RenderUtils::expandMultiplePatternDataFromROM(patternBuffer, buffer, bytes / 0x20); + + if (size < 0x1000) + break; + + size -= bytes; + inputAddress += 8; // This is needed, but why...? + } + break; + } + } + } + + void createPaletteSpriteFromROM(EmulatorInterface& emulatorInterface, PaletteSprite& paletteSprite, uint32 patternsBaseAddress, uint32 patternAddress, uint32 mappingAddress, SpriteCache::ROMSpriteEncoding encoding, int16 indexOffset) + { + // Fill sprite pattern buffer + static std::vector patternBuffer; + patternBuffer.clear(); + decodeROMSpriteData(emulatorInterface, patternBuffer, patternsBaseAddress, patternAddress, encoding); + + // Fill sprite patterns + static std::vector patterns; + patterns.clear(); + if (!patternBuffer.empty()) + { + EmulatorInterface& emulatorInterface = EmulatorInterface::instance(); + const int count = emulatorInterface.readMemory16(mappingAddress); + uint32 address = mappingAddress + 2; + + for (int i = 0; i < count; ++i) + { + RenderUtils::fillPatternsFromSpriteData(patterns, emulatorInterface.getRom() + address, patternBuffer, indexOffset); + address += 6; + } + } + + // Create the palette sprite + paletteSprite.createFromSpritePatterns(patterns); + } + + void createPaletteSpriteFromROM(EmulatorInterface& emulatorInterface, PaletteSprite& paletteSprite, uint32 patternsBaseAddress, uint32 tableAddress, uint32 mappingOffset, uint8 animationSprite, SpriteCache::ROMSpriteEncoding encoding, int16 indexOffset) + { + const uint32 patternAddress = (encoding == SpriteCache::ROMSpriteEncoding::NONE || encoding == SpriteCache::ROMSpriteEncoding::KOSINSKI) ? tableAddress : (tableAddress + emulatorInterface.readMemory16(tableAddress + animationSprite * 2)); + const uint32 mappingAddress = mappingOffset + emulatorInterface.readMemory16(mappingOffset + animationSprite * 2); + + createPaletteSpriteFromROM(emulatorInterface, paletteSprite, patternsBaseAddress, patternAddress, mappingAddress, encoding, indexOffset); + } + + bool isHexDigit(char ch) + { + return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F'); + } + +} + + + +void SpriteCache::ROMSpriteData::serialize(VectorBinarySerializer& serializer) +{ + serializer.serialize(mPatternsBaseAddress); + serializer.serialize(mTableAddress); + serializer.serialize(mMappingOffset); + serializer.serialize(mAnimationSprite); + serializer.serializeAs(mEncoding); + serializer.serialize(mIndexOffset); +} + +uint64 SpriteCache::ROMSpriteData::getKey() const +{ + return (((uint64)mPatternsBaseAddress) << 42) ^ (((uint64)mTableAddress) << 25) ^ (((uint64)mMappingOffset) << 8) ^ (uint64)mAnimationSprite; +} + + +SpriteCache::SpriteCache() +{ +} + +SpriteCache::~SpriteCache() +{ + clear(); + + if (nullptr != mSpriteDump) + { + mSpriteDump->save(); + SAFE_DELETE(mSpriteDump); + } +} + +void SpriteCache::clear() +{ + // Delete the sprite instances + for (auto& pair : mCachedSprites) + { + delete pair.second.mSprite; + } + mCachedSprites.clear(); + ++mGlobalChangeCounter; +} + +void SpriteCache::loadAllSpriteDefinitions() +{ + // Load or reload from all mods + loadSpriteDefinitions(L"data/sprites"); + for (const Mod* mod : ModManager::instance().getActiveMods()) + { + loadSpriteDefinitions(mod->mFullPath + L"sprites"); + } +} + +bool SpriteCache::hasSprite(uint64 key) const +{ + return (mCachedSprites.count(key) != 0); +} + +const SpriteCache::CacheItem* SpriteCache::getSprite(uint64 key) +{ + CacheItem* item = mapFind(mCachedSprites, key); + if (nullptr != item) + { + // Resolve redirect + while (nullptr != item->mRedirect) + { + item = item->mRedirect; + } + return item; + } + else + { + // Output an error + if (EngineMain::getDelegate().useDeveloperFeatures()) + { + const std::string_view* str = LemonScriptRuntime::tryResolveStringHash(key); + if (nullptr != str) + { + RMX_ERROR("Invalid sprite cache key with string '" << *str << "'", ); + } + else + { + RMX_ERROR("Invalid sprite cache key with unknown hash " << rmx::hexString(key), ); + } + } + return nullptr; + } +} + +SpriteCache::CacheItem& SpriteCache::getOrCreatePaletteSprite(uint64 key) +{ + CacheItem* item = mapFind(mCachedSprites, key); + if (nullptr != item) + { + RMX_CHECK(!item->mUsesComponentSprite, "Sprite is not a palette sprite", ); + } + else + { + item = &createCacheItem(key); + item->mSprite = new PaletteSprite(); + item->mUsesComponentSprite = false; + } + return *item; +} + +SpriteCache::CacheItem& SpriteCache::getOrCreateComponentSprite(uint64 key) +{ + CacheItem* item = mapFind(mCachedSprites, key); + if (nullptr != item) + { + RMX_CHECK(item->mUsesComponentSprite, "Sprite is not a component sprite", ); + } + else + { + item = &createCacheItem(key); + item->mSprite = new ComponentSprite(); + item->mUsesComponentSprite = true; + } + return *item; +} + +SpriteCache::CacheItem& SpriteCache::setupSpriteFromROM(EmulatorInterface& emulatorInterface, const ROMSpriteData& romSpriteData, uint8 atex) +{ + const uint64 key = romSpriteData.getKey(); + CacheItem* item = mapFind(mCachedSprites, key); + if (nullptr == item) + { + item = &getOrCreatePaletteSprite(key); + item->mSourceInfo.mType = SourceInfo::Type::ROM_DATA; + item->mSourceInfo.mROMSpriteData = romSpriteData; + + PaletteSprite& paletteSprite = *static_cast(item->mSprite); + createPaletteSpriteFromROM(emulatorInterface, paletteSprite, romSpriteData.mPatternsBaseAddress, romSpriteData.mTableAddress, romSpriteData.mMappingOffset, romSpriteData.mAnimationSprite, romSpriteData.mEncoding, romSpriteData.mIndexOffset); + + #ifdef CREATE_SPRITEDUMP + if (romSpriteData.mAnimationSprite != 0) // TODO: Do this only for characters + { + String categoryKey(0, "%06x_%06x_%06x", romSpriteData.mPatternsBaseAddress, romSpriteData.mTableAddress, romSpriteData.mMappingOffset); + getSpriteDump().addSpriteWithTranslation(paletteSprite, *categoryKey, romSpriteData.mAnimationSprite, atex); + item.mGotDumped = true; + } + #endif + } + return *item; +} + +void SpriteCache::clearRedirect(uint64 sourceKey) +{ + CacheItem* source = mapFind(mCachedSprites, sourceKey); + if (nullptr != source) + { + source->mRedirect = nullptr; + } +} + +void SpriteCache::setupRedirect(uint64 sourceKey, uint64 targetKey) +{ + CacheItem* source = mapFind(mCachedSprites, sourceKey); + if (nullptr == source) + { + source = &createCacheItem(sourceKey); + } + + CacheItem* target = mapFind(mCachedSprites, targetKey); + source->mRedirect = target; +} + +SpriteDump& SpriteCache::getSpriteDump() +{ + if (nullptr == mSpriteDump) + { + mSpriteDump = new SpriteDump(); + mSpriteDump->load(); + } + return *mSpriteDump; +} + +void SpriteCache::dumpSprite(uint64 key, std::string_view categoryKey, uint8 spriteNumber, uint8 atex) +{ + CacheItem* item = mapFind(mCachedSprites, key); + if (nullptr != item && !item->mGotDumped) + { + if (!item->mUsesComponentSprite) + { + const PaletteSprite& paletteSprite = *static_cast(item->mSprite); + getSpriteDump().addSprite(paletteSprite, categoryKey, spriteNumber, atex); + } + else + { + RMX_ERROR("Can't dump component sprites (attempted to dump '" << categoryKey << "' sprite " << rmx::hexString(spriteNumber, 2) << ")", ); + } + item->mGotDumped = true; + } +} + +SpriteCache::CacheItem& SpriteCache::createCacheItem(uint64 key) +{ + CacheItem& item = mCachedSprites[key]; + item.mKey = key; + item.mSprite = nullptr; + item.mUsesComponentSprite = false; + item.mChangeCounter = mGlobalChangeCounter; + return item; +} + +void SpriteCache::loadSpriteDefinitions(const std::wstring& path) +{ + struct SheetCache + { + std::map mPaletteSpriteSheets; + std::map mComponentSpriteSheets; + }; + SheetCache sheetCache; + + std::vector fileEntries; + fileEntries.reserve(8); + FTX::FileSystem->listFilesByMask(path + L"/*.json", true, fileEntries); + if (fileEntries.empty()) + return; + + ++mGlobalChangeCounter; + for (const rmx::FileIO::FileEntry& fileEntry : fileEntries) + { + const Json::Value spritesJson = JsonHelper::loadFile(fileEntry.mPath + fileEntry.mFilename); + for (auto iterator = spritesJson.begin(); iterator != spritesJson.end(); ++iterator) + { + const String identifier(iterator.key().asString()); + uint64 key = 0; + { + // Check if it's an hex identifier or a string + if (identifier.length() >= 3 && identifier[0] == '0' && identifier[1] == 'x') + { + bool isHex = true; + for (int i = 2; i < identifier.length(); ++i) + { + if (!isHexDigit(identifier[i])) + { + isHex = false; + break; + } + } + if (isHex) + { + key = rmx::parseInteger(identifier); + } + } + + if (key == 0) + { + key = rmx::getMurmur2_64(identifier); + } + } + + std::wstring filename; + Vec2i center; + Recti rect; + + for (auto it = iterator->begin(); it != iterator->end(); ++it) + { + if (it.key().asString() == "File" && !it->asString().empty()) + { + filename = *String(it->asString()).toWString(); + } + else if (it.key().asString() == "Center" && !it->asString().empty()) + { + std::vector parts; + String(it->asString()).split(parts, ','); + if (parts.size() == 2) + { + center.x = parts[0].parseInt(); + center.y = parts[1].parseInt(); + } + } + else if (it.key().asString() == "Rect" && !it->asString().empty()) + { + std::vector parts; + String(it->asString()).split(parts, ','); + if (parts.size() == 4) + { + rect.x = parts[0].parseInt(); + rect.y = parts[1].parseInt(); + rect.width = parts[2].parseInt(); + rect.height = parts[3].parseInt(); + } + } + } + + if (!filename.empty()) + { + // Check for overloading + { + const auto it = mCachedSprites.find(key); + if (it != mCachedSprites.end()) + { + // This sprite got overloaded e.g. by a mod -- remove the old version + SAFE_DELETE(it->second.mSprite); + } + } + + CacheItem& item = createCacheItem(key); + #ifdef DEBUG + item.mSourceInfo.mSourceIdentifier = *identifier; + #endif + const std::wstring fullpath = fileEntry.mPath + filename; + + // Palette or RGBA? + item.mUsesComponentSprite = WString(filename).endsWith(L".png"); + + // Part of a sprite sheet? + const bool isPartOfSheet = (rect.width != 0); + + bool success = false; + if (!item.mUsesComponentSprite) + { + // Load palette sprite (= 8-bit palette sprite) + PaletteSprite* sprite = new PaletteSprite(); + item.mSprite = sprite; + + if (isPartOfSheet) + { + PaletteBitmap* bitmap = nullptr; + const auto it = sheetCache.mPaletteSpriteSheets.find(fullpath); + if (it == sheetCache.mPaletteSpriteSheets.end()) + { + bitmap = &sheetCache.mPaletteSpriteSheets[fullpath]; + success = FileHelper::loadPaletteBitmap(*bitmap, fullpath); + } + else + { + bitmap = &it->second; + success = true; + } + + if (success) + { + sprite->createFromBitmap(*bitmap, rect, -center); + } + } + else + { + PaletteBitmap bitmap; + success = FileHelper::loadPaletteBitmap(bitmap, fullpath); + if (success) + { + static_cast(item.mSprite)->createFromBitmap(std::move(bitmap), -center); + } + } + } + else + { + // Load component sprite (= 32-bit RGBA sprite) + ComponentSprite* sprite = new ComponentSprite(); + item.mSprite = sprite; + + if (isPartOfSheet) + { + Bitmap* bitmap = nullptr; + const auto it = sheetCache.mComponentSpriteSheets.find(fullpath); + if (it == sheetCache.mComponentSpriteSheets.end()) + { + bitmap = &sheetCache.mComponentSpriteSheets[fullpath]; + success = FileHelper::loadBitmap(*bitmap, fullpath); + } + else + { + bitmap = &it->second; + success = true; + } + + if (success) + { + sprite->accessBitmap().copy(*bitmap, rect); + } + } + else + { + success = FileHelper::loadBitmap(static_cast(item.mSprite)->accessBitmap(), fullpath); + } + item.mSprite->mOffset = -center; + } + } + } + } +} diff --git a/sonic3air-main/Oxygen/oxygenengine/source/oxygen/resources/SpriteCache.h b/sonic3air-main/Oxygen/oxygenengine/source/oxygen/resources/SpriteCache.h new file mode 100644 index 00000000..3efab4a3 --- /dev/null +++ b/sonic3air-main/Oxygen/oxygenengine/source/oxygen/resources/SpriteCache.h @@ -0,0 +1,100 @@ +/* +* Part of the Oxygen Engine / Sonic 3 A.I.R. software distribution. +* Copyright (C) 2017-2024 by Eukaryot +* +* Published under the GNU GPLv3 open source software license, see license.txt +* or https://www.gnu.org/licenses/gpl-3.0.en.html +*/ + +#pragma once + +#include +#include "oxygen/rendering/sprite/ComponentSprite.h" +#include "oxygen/rendering/sprite/PaletteSprite.h" + +class EmulatorInterface; +class SpriteDump; + + +class SpriteCache : public SingleInstance +{ +public: + enum class ROMSpriteEncoding : uint8 + { + NONE = 0, + CHARACTER = 1, + OBJECT = 2, + KOSINSKI = 3 + }; + + struct ROMSpriteData + { + uint32 mPatternsBaseAddress = 0; + uint32 mTableAddress = 0; + uint32 mMappingOffset = 0; + uint8 mAnimationSprite = 0; + ROMSpriteEncoding mEncoding = ROMSpriteEncoding::NONE; + int16 mIndexOffset = 0; + + void serialize(VectorBinarySerializer& serializer); + uint64 getKey() const; + }; + + struct SourceInfo + { + enum class Type : uint8 + { + UNKNOWN, + SPRITE_FILE, + ROM_DATA + }; + + Type mType = Type::UNKNOWN; + ROMSpriteData mROMSpriteData; // Only for type ROM_DATA + #ifdef DEBUG + std::string mSourceIdentifier; // Only for type SPRITE_FILE + #endif + }; + + struct CacheItem + { + uint64 mKey = 0; + bool mUsesComponentSprite = false; + SpriteBase* mSprite = nullptr; + uint32 mChangeCounter = 0; + CacheItem* mRedirect = nullptr; + SourceInfo mSourceInfo; + bool mGotDumped = false; + }; + +public: + SpriteCache(); + ~SpriteCache(); + + void clear(); + void loadAllSpriteDefinitions(); + + bool hasSprite(uint64 key) const; + const CacheItem* getSprite(uint64 key); + CacheItem& getOrCreatePaletteSprite(uint64 key); + CacheItem& getOrCreateComponentSprite(uint64 key); + + SpriteCache::CacheItem& setupSpriteFromROM(EmulatorInterface& emulatorInterface, const ROMSpriteData& romSpriteData, uint8 atex); + + void clearRedirect(uint64 sourceKey); + void setupRedirect(uint64 sourceKey, uint64 targetKey); + + inline uint32 getGlobalChangeCounter() const { return mGlobalChangeCounter; } + + SpriteDump& getSpriteDump(); + void dumpSprite(uint64 key, std::string_view categoryKey, uint8 spriteNumber, uint8 atex); + +private: + CacheItem& createCacheItem(uint64 key); + void loadSpriteDefinitions(const std::wstring& path); + +private: + std::unordered_map mCachedSprites; + SpriteDump* mSpriteDump = nullptr; + uint32 mGlobalChangeCounter = 0; +}; diff --git a/sonic3air-main/Oxygen/oxygenengine/source/oxygen/simulation/CodeExec.cpp b/sonic3air-main/Oxygen/oxygenengine/source/oxygen/simulation/CodeExec.cpp new file mode 100644 index 00000000..e3be158f --- /dev/null +++ b/sonic3air-main/Oxygen/oxygenengine/source/oxygen/simulation/CodeExec.cpp @@ -0,0 +1,835 @@ +/* +* Part of the Oxygen Engine / Sonic 3 A.I.R. software distribution. +* Copyright (C) 2017-2024 by Eukaryot +* +* Published under the GNU GPLv3 open source software license, see license.txt +* or https://www.gnu.org/licenses/gpl-3.0.en.html +*/ + +#include "oxygen/pch.h" +#include "oxygen/simulation/CodeExec.h" +#include "oxygen/simulation/EmulatorInterface.h" +#include "oxygen/simulation/LemonScriptProgram.h" +#include "oxygen/simulation/LogDisplay.h" +#include "oxygen/simulation/Simulation.h" +#include "oxygen/application/Application.h" +#include "oxygen/application/Configuration.h" +#include "oxygen/application/EngineMain.h" +#include "oxygen/application/GameProfile.h" +#include "oxygen/platform/PlatformFunctions.h" + +#include +#include + + +namespace +{ + const std::vector* getLemonStackByAsmStack(const std::vector& asmStack) + { + // Try to find the right stack + for (const GameProfile::StackLookupEntry& lookup : GameProfile::instance().mStackLookups) + { + if (lookup.mAsmStack.size() == asmStack.size()) + { + bool equal = true; + for (size_t i = 0; i < asmStack.size(); ++i) + { + if (lookup.mAsmStack[i] != asmStack[i]) + { + equal = false; + break; + } + } + + if (equal) + { + return &lookup.mLemonStack; + } + } + } + + return nullptr; + } + + const std::vector* findCurrentLemonStack(const std::vector& asmStack) + { + // Try to find the right stack + for (const GameProfile::StackLookupEntry& lookup : GameProfile::instance().mStackLookups) + { + if (lookup.mAsmStack.size() == asmStack.size()) + { + bool equal = true; + for (size_t i = 0; i < asmStack.size(); ++i) + { + if (lookup.mAsmStack[i] != asmStack[i]) + { + equal = false; + break; + } + } + + if (equal) + { + return &lookup.mLemonStack; + } + } + } + + return nullptr; + } + + const uint8* getCallingPCAfterCall() + { + // After a call was already added to the call stack, we need to go up to the second-to-last element on the stack to get the program counter there + // -> Note that this program counter is already pointing to the next runtime opcode, this needs to be accounted for later + const CArray& callStack = lemon::Runtime::getActiveControlFlow()->getCallStack(); + if (callStack.count >= 2) + return callStack[callStack.count - 2].mProgramCounter; + else + return nullptr; + } +} + + +struct RuntimeExecuteConnector : public lemon::Runtime::ExecuteConnector +{ + CodeExec& mCodeExec; + + inline explicit RuntimeExecuteConnector(CodeExec& codeExec) : mCodeExec(codeExec) {} + + bool handleCall(const lemon::Function* func, uint64 callTarget) override + { + if (nullptr == func) + { + mCodeExec.showErrorWithScriptLocation("Call failed, probably due to invalid function (target = " + rmx::hexString(callTarget, 16) + ")."); + return false; + } + return true; + } + + bool handleReturn() override + { + if (mCodeExec.mHasCallFramesToAdd) + { + mCodeExec.applyCallFramesToAdd(); + } + return true; + } + + bool handleExternalCall(uint64 address) override + { + // Check for address hook at the target address + // -> If it fails, we will just continue after the call + mCodeExec.tryCallAddressHook((uint32)address); + return true; + } + + bool handleExternalJump(uint64 address) override + { + handleReturn(); + return handleExternalCall(address); + } +}; + + +struct RuntimeExecuteConnectorDev : public RuntimeExecuteConnector +{ + inline explicit RuntimeExecuteConnectorDev(CodeExec& codeExec) : RuntimeExecuteConnector(codeExec) {} + + bool handleCall(const lemon::Function* func, uint64 callTarget) override + { + if (nullptr == func) + { + mCodeExec.showErrorWithScriptLocation("Call failed, probably due to invalid function (target = " + rmx::hexString(callTarget, 16) + ")."); + return false; + } + if (func->getType() == lemon::Function::Type::SCRIPT) + { + CodeExec::CallFrame& callFrame = mCodeExec.mActiveCallFrameTracking->pushCallFrame(CodeExec::CallFrame::Type::SCRIPT_DIRECT); + callFrame.mFunction = func; + callFrame.mCallingPC = getCallingPCAfterCall(); + } + return true; + } + + bool handleReturn() override + { + mCodeExec.popCallFrame(); + return true; + } + + bool handleExternalCall(uint64 address) override + { + // Check for address hook at the target address + // -> If it fails, we will just continue after the call + mCodeExec.tryCallAddressHookDev((uint32)address); + return true; + } + + bool handleExternalJump(uint64 address) override + { + handleReturn(); + return handleExternalCall(address); + } +}; + + + +CodeExec::CallFrame& CodeExec::CallFrameTracking::pushCallFrame(CallFrame::Type type) +{ + const int parentIndex = mCallStack.empty() ? -1 : (int)mCallStack.back(); + CallFrame& callFrame = (mCallFrames.size() == CALL_FRAMES_LIMIT) ? mCallFrames.back() : vectorAdd(mCallFrames); + callFrame.mType = type; + callFrame.mParentIndex = parentIndex; + callFrame.mDepth = (int)mCallStack.size(); + mCallStack.emplace_back(mCallFrames.size() - 1); + return callFrame; +} + +CodeExec::CallFrame& CodeExec::CallFrameTracking::pushCallFrameFailed(CallFrame::Type type) +{ + const int parentIndex = mCallFrames.empty() ? -1 : (int)mCallStack.back(); + CallFrame& callFrame = (mCallFrames.size() == CALL_FRAMES_LIMIT) ? mCallFrames.back() : vectorAdd(mCallFrames); + callFrame.mType = type; + callFrame.mParentIndex = parentIndex; + callFrame.mDepth = (int)mCallStack.size(); + return callFrame; +} + +void CodeExec::CallFrameTracking::popCallFrame() +{ + if (mCallStack.empty()) + return; + + const uint32 steps = (uint32)mCallFrames[mCallStack.back()].mSteps; + mCallStack.pop_back(); + + if (!mCallStack.empty() && mCallStack.back() < mCallFrames.size()) + { + // Accumulate steps of children + mCallFrames[mCallStack.back()].mSteps += steps; + } +} + +void CodeExec::CallFrameTracking::writeCurrentCallStack(std::vector& outCallStack) +{ + outCallStack.clear(); + outCallStack.reserve(mCallStack.size()); + for (int i = (int)mCallStack.size() - 1; i >= 0; --i) + { + const lemon::Function* function = mCallFrames[mCallStack[i]].mFunction; + outCallStack.push_back((nullptr != function) ? function->getName().getHash() : 0); + } +} + +void CodeExec::CallFrameTracking::writeCurrentCallStack(std::vector& outCallStack) +{ + outCallStack.clear(); + outCallStack.reserve(mCallStack.size()); + for (int i = (int)mCallStack.size() - 1; i >= 0; --i) + { + const lemon::Function* function = mCallFrames[mCallStack[i]].mFunction; + outCallStack.push_back((nullptr != function) ? std::string(function->getName().getString()) : ""); + } +} + +void CodeExec::CallFrameTracking::processCallFrames() +{ + for (size_t i = 0; i < mCallFrames.size(); ) + { + i = processCallFramesRecursive(i); + } +} + +size_t CodeExec::CallFrameTracking::processCallFramesRecursive(size_t index) +{ + CallFrame& current = mCallFrames[index]; + current.mAnyChildFailed = (current.mType == CallFrame::Type::FAILED_HOOK); + + ++index; + while (index < mCallFrames.size()) + { + CallFrame& child = mCallFrames[index]; + if (child.mDepth <= current.mDepth) + break; + + index = processCallFramesRecursive(index); + if (child.mAnyChildFailed) + { + current.mAnyChildFailed = true; + } + } + return index; +} + + + +CodeExec::CodeExec() : + mLemonScriptProgram(*new LemonScriptProgram()), + mEmulatorInterface(EngineMain::getDelegate().useDeveloperFeatures() ? *new EmulatorInterfaceDev() : *new EmulatorInterface()), + mLemonScriptRuntime(*new LemonScriptRuntime(mLemonScriptProgram, mEmulatorInterface)), + mDebugTracking(*this, mEmulatorInterface, mLemonScriptRuntime) +{ + mRuntimeEnvironment.mEmulatorInterface = &mEmulatorInterface; + + mIsDeveloperMode = EngineMain::getDelegate().useDeveloperFeatures(); + if (mIsDeveloperMode) + { + mLemonScriptProgram.getLemonScriptBindings().setDebugNotificationInterface(&mDebugTracking); + mEmulatorInterface.setDebugNotificationInterface(&mDebugTracking); + mMainCallFrameTracking.mCallFrames.reserve(CALL_FRAMES_LIMIT); + mMainCallFrameTracking.mCallStack.reserve(0x40); + mDebugTracking.setupForDevMode(); + } +} + +CodeExec::~CodeExec() +{ + delete &mEmulatorInterface; + delete &mLemonScriptRuntime; + delete &mLemonScriptProgram; +} + +void CodeExec::startup() +{ + mEmulatorInterface.clear(); + mEmulatorInterface.applyRomInjections(); // Not asking the engine delegate here, as it might not give a meaningful answer yet; just assume it's okay +} + +void CodeExec::reset() +{ + // Reset emulator interface + mEmulatorInterface.clear(); + if (EngineMain::instance().getDelegate().allowModdedData()) + { + mEmulatorInterface.applyRomInjections(); + } +} + +void CodeExec::cleanScriptDebug() +{ + LogDisplay::instance().clearLogErrors(); + + mMainCallFrameTracking.clear(); + mUnknownAddressesSet.clear(); + mUnknownAddressesInOrder.clear(); + mDebugTracking.clear(); +} + +bool CodeExec::reloadScripts(bool enforceFullReload, bool retainRuntimeState) +{ + if (retainRuntimeState) + { + // If the runtime is already active, save its current state + if (hasValidState() && mLemonScriptRuntime.getCallStackSize() != 0) + { + VectorBinarySerializer serializer(false, mSerializedRuntimeState); + if (!getLemonScriptRuntime().serializeRuntime(serializer)) + { + mSerializedRuntimeState.clear(); + } + } + } + else + { + // Clear the old serialization, it's not needed + mSerializedRuntimeState.clear(); + } + mExecutionState = ExecutionState::INACTIVE; + + const Configuration& config = Configuration::instance(); + LemonScriptProgram::LoadOptions options; + options.mEnforceFullReload = enforceFullReload; + options.mModuleSelection = EngineMain::getDelegate().mayLoadScriptMods() ? LemonScriptProgram::LoadOptions::ModuleSelection::ALL_MODS : LemonScriptProgram::LoadOptions::ModuleSelection::BASE_GAME_ONLY; + options.mAppVersion = EngineMain::getDelegate().getAppMetaData().mBuildVersionNumber; + const WString mainScriptPath = config.mScriptsDir + config.mMainScriptName; + + const LemonScriptProgram::LoadScriptsResult result = mLemonScriptProgram.loadScripts(mainScriptPath.toStdString(), options); + if (result == LemonScriptProgram::LoadScriptsResult::PROGRAM_CHANGED) + { + lemon::Runtime::setActiveEnvironment(&mRuntimeEnvironment); + mLemonScriptRuntime.onProgramUpdated(); + } + cleanScriptDebug(); + + return (result != LemonScriptProgram::LoadScriptsResult::FAILED); +} + +void CodeExec::restoreRuntimeState(bool hasSaveState) +{ + if (mSerializedRuntimeState.empty()) + { + // We don't have a valid runtime state, so it has to be reloaded from ASM, or we have to reset + reinitRuntime(nullptr, hasSaveState ? CallStackInitPolicy::READ_FROM_ASM : CallStackInitPolicy::RESET); + } + else + { + // Scripts got reloaded in-game + reinitRuntime(nullptr, CallStackInitPolicy::READ_FROM_ASM, &mSerializedRuntimeState); + mSerializedRuntimeState.clear(); // Not needed any more now + } +} + +void CodeExec::reinitRuntime(const LemonScriptRuntime::CallStackWithLabels* enforcedCallStack, CallStackInitPolicy callStackInitPolicy, const std::vector* serializedRuntimeState) +{ + cleanScriptDebug(); + + if (callStackInitPolicy == CallStackInitPolicy::USE_EXISTING) + { + // Nothing to do in this case, call stack was already loaded + RMX_ASSERT(nullptr == enforcedCallStack, "Can't use existing call stack and an enforced call stack at the same time"); + } + else + { + // The runtime requires a program + if (!mLemonScriptRuntime.hasValidProgram()) + return; + + mLemonScriptRuntime.getInternalLemonRuntime().clearAllControlFlows(); + + bool success = false; + if (nullptr != serializedRuntimeState && !serializedRuntimeState->empty()) + { + VectorBinarySerializer serializer(true, *serializedRuntimeState); + success = getLemonScriptRuntime().serializeRuntime(serializer); + } + + if (nullptr != enforcedCallStack && !enforcedCallStack->empty()) + { + for (const auto& pair : *enforcedCallStack) + { + success = mLemonScriptRuntime.callFunctionByNameAtLabel(pair.first, pair.second, true); + RMX_CHECK(success, "Could not apply previous lemon script stack", ); + } + } + + // Scan asm call stack if needed + if (!success && callStackInitPolicy == CallStackInitPolicy::READ_FROM_ASM) + { + std::vector callstack; + uint32 stackPointer = mEmulatorInterface.getRegister(EmulatorInterface::Register::A7); + RMX_CHECK((stackPointer & 0x00ff0000) == 0x00ff0000, "Stack pointer in register A7 is not pointing to a RAM address", ); + stackPointer |= 0xffff0000; + RMX_CHECK(stackPointer >= GameProfile::instance().mAsmStackRange.first && stackPointer <= GameProfile::instance().mAsmStackRange.second, "Stack pointer in register A7 is not inside the ASM stack range", ); + while (stackPointer < GameProfile::instance().mAsmStackRange.second) + { + callstack.push_back(mEmulatorInterface.readMemory32(stackPointer)); + stackPointer += 4; + } + + std::reverse(callstack.begin(), callstack.end()); + + // Build up initial script call stack + const std::vector* lemonStack = getLemonStackByAsmStack(callstack); + if (nullptr != lemonStack) + { + for (const GameProfile::LemonStackEntry& entry : *lemonStack) + { + success = mLemonScriptRuntime.callFunctionByNameAtLabel(entry.mFunctionName, entry.mLabelName, true); + RMX_CHECK(success, "Could not apply lemon script stack", ); + } + } + else + { + std::string str; + for (uint32 i : callstack) + str += " " + rmx::hexString(i, 8, ""); + RMX_ERROR("Save state stack could not be represented in lemon script:\n" + str, ); + } + } + + // Fallback if loading went wrong or there is no save state at all + if (!success || mLemonScriptRuntime.getCallStackSize() == 0) + { + // Start from scratch + mLemonScriptRuntime.callFunctionByName("scriptMainEntryPoint", true); + } + } + + // Execute init once + mLemonScriptRuntime.callFunctionByName("Init", false); + + EngineMain::getDelegate().onRuntimeInit(*this); + + mExecutionState = ExecutionState::READY; + mAccumulatedStepsOfCurrentFrame = 0; +} + +bool CodeExec::performFrameUpdate() +{ + if (!canExecute()) + return false; + + lemon::Runtime::setActiveEnvironment(&mRuntimeEnvironment); + + const bool beginningNewFrame = (mExecutionState != ExecutionState::INTERRUPTED); + if (beginningNewFrame) + { + mAccumulatedStepsOfCurrentFrame = 0; + + if (mIsDeveloperMode) + { + // Reset debug tracking (watches etc.) + mDebugTracking.onBeginFrame(); + + // Reset call frame tracking + mMainCallFrameTracking.clear(); + + static std::vector callstack; // This is static to avoid reallocations + mLemonScriptRuntime.getCallStack(callstack); + for (const lemon::Function* func : callstack) + { + CallFrame& callFrame = mMainCallFrameTracking.pushCallFrame(CallFrame::Type::SCRIPT_STACK); + callFrame.mFunction = func; + } + } + + // Perform pre-update hook, if there is one + // -> This acts like a call from wherever the last script execution stopped / yielded + tryCallUpdateHook(false); + } + + // Run script + runScript(false, &mMainCallFrameTracking); + + const bool completedNewFrame = (mExecutionState == ExecutionState::YIELDED); + if (completedNewFrame) + { + // Perform post-update hook, if there is one + // -> Note that the hook must yield execution, otherwise parts of the next frame get executed + if (canExecute() && tryCallUpdateHook(true)) + { + runScript(true, &mMainCallFrameTracking); + } + mAccumulatedStepsOfCurrentFrame = 0; + } + + // Return whether the frame was completed in any way (halted counts as completed) + return (mExecutionState != ExecutionState::INTERRUPTED); +} + +void CodeExec::yieldExecution() +{ + mCurrentlyRunningScript = false; + mLemonScriptRuntime.getInternalLemonRuntime().triggerStopSignal(); +} + +bool CodeExec::executeScriptFunction(const std::string& functionName, bool showErrorOnFail, FunctionExecData* execData) +{ + if (canExecute()) + { + // TODO: This would be a good use case for using a different control flow than the main one + lemon::Runtime::setActiveEnvironment(&mRuntimeEnvironment); + + bool success = false; + if (nullptr == execData) + { + success = mLemonScriptRuntime.callFunctionByName(functionName, showErrorOnFail); + } + else + { + success = mLemonScriptRuntime.getInternalLemonRuntime().callFunctionWithParameters(functionName, execData->mParams); + } + + if (success) + { + const size_t oldAccumulatedSteps = mAccumulatedStepsOfCurrentFrame; + + #if 0 + // Dead code, as call frame tracking is always disabled for single script function calls + // -> However, this might change later on, e.g. if we had tracking for multiple control flows individually (see TODO above) + CallFrameTracking* tracking = nullptr; + if (nullptr != tracking) + { + const size_t originalNumCallFrames = tracking->mCallFrames.size(); + + CallFrame& callFrame = tracking->pushCallFrame(CallFrame::Type::SCRIPT_STACK); + callFrame.mFunction = mLemonScriptRuntime.getCurrentFunction(); + runScript(true, tracking); + + // Revert call frames from that call + tracking->mCallFrames.resize(originalNumCallFrames); + } + else + #endif + { + runScript(true, nullptr); + } + + // Evaluate the return value + if (nullptr != execData && nullptr != execData->mParams.mReturnType) + { + execData->mReturnValueStorage = mLemonScriptRuntime.getInternalLemonRuntime().getSelectedControlFlowMutable().popValueStack(); + } + + // Revert accumulated steps + // -> This kind of script function execution should not count against the accumulator, as we might execute functions while the game is paused (i.e. the accumulator won't get reset) + mAccumulatedStepsOfCurrentFrame = oldAccumulatedSteps; + return true; + } + } + return false; +} + +void CodeExec::setupCallFrame(std::string_view functionName, std::string_view labelName) +{ + mCallFramesToAdd.emplace_back(functionName, labelName); + mHasCallFramesToAdd = true; +} + +void CodeExec::processCallFrames() +{ + mMainCallFrameTracking.processCallFrames(); +} + +bool CodeExec::canExecute() const +{ + switch (mExecutionState) + { + case ExecutionState::READY: + case ExecutionState::YIELDED: + case ExecutionState::INTERRUPTED: + return true; + + case ExecutionState::INACTIVE: + case ExecutionState::HALTED: + case ExecutionState::ERROR: + default: + return false; + } +} + +bool CodeExec::hasValidState() const +{ + switch (mExecutionState) + { + case ExecutionState::YIELDED: + case ExecutionState::INTERRUPTED: + return true; + + case ExecutionState::INACTIVE: + case ExecutionState::READY: + case ExecutionState::HALTED: + case ExecutionState::ERROR: + default: + return false; + } +} + +void CodeExec::runScript(bool executeSingleFunction, CallFrameTracking* callFrameTracking) +{ + // There are four stop conditions: + // a) Yield from script, sets mCurrentlyRunningScript to false -> this is the usual case if executeSingleFunction == false (but must not happen otherwise) + // b) Returned from function that is initially on top of the stack -> this is the usual case if executeSingleFunction == true (and can't happen otherwise) + // c) Reached runtime steps limit, i.e. script got stuck in a loop + // d) Script stopped because its runtime stack was completely emptied + if (!canExecute()) + return; + + mActiveInstance = this; + mCurrentlyRunningScript = true; + const size_t abortOnCallStackSize = executeSingleFunction ? (std::max(mLemonScriptRuntime.getCallStackSize(), 1) - 1) : 0; + mActiveCallFrameTracking = mIsDeveloperMode ? callFrameTracking : nullptr; + + size_t stepsCounter = 0; + size_t nextCheckSteps = 0x40000; + const uint32 ticksStart = SDL_GetTicks(); + + while (true) + { + // Execute next runtime steps + size_t stepsExecutedThisCall; + try + { + const bool success = (nullptr != mActiveCallFrameTracking) ? executeRuntimeStepsDev(stepsExecutedThisCall, abortOnCallStackSize) : executeRuntimeSteps(stepsExecutedThisCall, abortOnCallStackSize); + if (!success) + { + if (executeSingleFunction) + { + // Execution of function finished + mExecutionState = ExecutionState::YIELDED; + } + else + { + mExecutionState = ExecutionState::HALTED; + } + break; + } + + if (!mCurrentlyRunningScript) + { + // Execution got yielded -- this is the usual way to end a frame, i.e. with executeSingleFunction == false + mExecutionState = ExecutionState::YIELDED; + RMX_CHECK(!executeSingleFunction, "Single function script execution got yielded; don't use yieldExecution inside functions that get called directly from the engine", ); + break; + } + + if (executeSingleFunction && mLemonScriptRuntime.getCallStackSize() <= abortOnCallStackSize) + { + // Execution of function finished + mExecutionState = ExecutionState::YIELDED; + break; + } + } + catch (const std::exception& e) + { + RMX_ERROR("Caught exception during script execution: " << e.what(), ); + } + + // Regularly check if we should better interrupt execution + stepsCounter += stepsExecutedThisCall; + if (stepsCounter >= nextCheckSteps) + { + // Limit execution to this number of steps + const constexpr int32 MAX_STEPS = 0x8000000; // Needed for S3AIR entering special stage in OxygenApp + if (mAccumulatedStepsOfCurrentFrame + stepsCounter >= MAX_STEPS) + { + mExecutionState = ExecutionState::INTERRUPTED; + + // Show a message box (but only once) + static bool showMessageBox = true; + if (showMessageBox) + { + bool gameRecordingSaved = false; + if (Configuration::instance().mGameRecorder.mIsRecording) + { + gameRecordingSaved = (Application::instance().getSimulation().saveGameRecording() != 0); + } + + showErrorWithScriptLocation("Reached limit for runtime steps per update; if this happens, the program probably got stuck in a loop.", gameRecordingSaved ? "A game recording file was written that could be helpful for debugging this issue." : ""); + showMessageBox = false; + } + break; + } + + // Limit execution to 100 ms + if (SDL_GetTicks() - ticksStart >= 100 && !PlatformFunctions::isDebuggerPresent()) + { + mExecutionState = ExecutionState::INTERRUPTED; + break; + } + + nextCheckSteps += 0x20000; + } + } + + mAccumulatedStepsOfCurrentFrame += stepsCounter; + mCurrentlyRunningScript = false; + mActiveInstance = nullptr; +} + +bool CodeExec::executeRuntimeSteps(size_t& stepsExecuted, size_t minimumCallStackSize) +{ + lemon::Runtime& runtime = mLemonScriptRuntime.getInternalLemonRuntime(); + RuntimeExecuteConnector connector(*this); + runtime.executeSteps(connector, 5000, minimumCallStackSize); + + stepsExecuted = connector.mStepsExecuted; + return (connector.mResult != lemon::Runtime::ExecuteResult::Result::HALT); +} + +bool CodeExec::executeRuntimeStepsDev(size_t& stepsExecuted, size_t minimumCallStackSize) +{ + // Same as "executeRuntimeSteps", but with additional developer mode stuff, incl. tracking of call frames + if (mActiveCallFrameTracking->mCallFrames.empty()) + return false; + + lemon::Runtime& runtime = mLemonScriptRuntime.getInternalLemonRuntime(); + RuntimeExecuteConnectorDev connector(*this); + runtime.executeSteps(connector, 5000, minimumCallStackSize); + + { + mActiveCallFrameTracking->mCallFrames.back().mSteps += connector.mStepsExecuted; + + // Correct written values for all watches that triggered in this update + mDebugTracking.updateWatches(); + } + + stepsExecuted = connector.mStepsExecuted; + return (connector.mResult != lemon::Runtime::ExecuteResult::Result::HALT); +} + +bool CodeExec::tryCallAddressHook(uint32 address) +{ + return mLemonScriptRuntime.callAddressHook(address); +} + +bool CodeExec::tryCallAddressHookDev(uint32 address) +{ + if (mLemonScriptRuntime.callAddressHook(address)) + { + // Call script function + CallFrame& callFrame = mActiveCallFrameTracking->pushCallFrame(CallFrame::Type::SCRIPT_HOOK); + callFrame.mFunction = mLemonScriptRuntime.getCurrentFunction(); + callFrame.mAddress = address; + callFrame.mCallingPC = getCallingPCAfterCall(); + return true; + } + else + { + CallFrame& callFrame = mActiveCallFrameTracking->pushCallFrameFailed(CallFrame::Type::FAILED_HOOK); + callFrame.mAddress = address; + + if (mUnknownAddressesSet.count(address) == 0) + { + mUnknownAddressesSet.insert(address); + mUnknownAddressesInOrder.push_back(address); + } + return false; + } +} + +bool CodeExec::tryCallUpdateHook(bool postUpdate) +{ + if (mLemonScriptRuntime.callUpdateHook(postUpdate)) + { + if (nullptr != mActiveCallFrameTracking) + { + CallFrame& callFrame = mActiveCallFrameTracking->pushCallFrame(CallFrame::Type::SCRIPT_DIRECT); + callFrame.mFunction = mLemonScriptRuntime.getCurrentFunction(); + } + return true; + } + return false; +} + +void CodeExec::applyCallFramesToAdd() +{ + // Add call frames as defined via script function "System.insertOuterCallFrame" + for (const auto& pair : mCallFramesToAdd) + { + const bool success = mLemonScriptRuntime.callFunctionByNameAtLabel(pair.first, pair.second, true); + RMX_CHECK(success, "Could not insert outer call frame", continue); + + if (nullptr != mActiveCallFrameTracking) + { + CallFrame& callFrame = mActiveCallFrameTracking->pushCallFrame(CallFrame::Type::SCRIPT_STACK); + callFrame.mFunction = mLemonScriptRuntime.getCurrentFunction(); + } + } + mCallFramesToAdd.clear(); + mHasCallFramesToAdd = false; +} + +void CodeExec::popCallFrame() +{ + mActiveCallFrameTracking->popCallFrame(); + + if (mHasCallFramesToAdd) + { + applyCallFramesToAdd(); + } +} + +void CodeExec::showErrorWithScriptLocation(const std::string& errorText, const std::string& subText) +{ + std::string locationString = mLemonScriptRuntime.getOwnCurrentScriptLocationString(); + if (locationString.empty()) + { + RMX_ERROR(errorText << "\n" << subText, ); + } + else + { + RMX_ERROR(errorText << "\nIn " << locationString << "." << (subText.empty() ? "" : "\n") << subText, ); + } +}