From 7f18a33fae8d3ad2ec80a223dc0a7b8b1eaa18f4 Mon Sep 17 00:00:00 2001 From: Aruki Date: Tue, 9 May 2017 21:08:52 -0600 Subject: [PATCH] Added support for Prime 3 package cooking --- src/Core/Core.pro | 3 +- src/Core/GameProject/CPackage.cpp | 159 ++++++++++++++---- .../GameProject/DependencyListBuilders.cpp | 4 + src/Core/Resource/CSavedStateID.h | 85 ++++++++++ src/Core/Resource/CWorld.cpp | 18 ++ src/Core/Resource/CWorld.h | 14 +- src/Core/Resource/Cooker/CAreaCooker.cpp | 92 +++++++++- src/Core/Resource/Cooker/CAreaCooker.h | 6 + src/Core/Resource/Cooker/CWorldCooker.cpp | 33 +++- src/Core/Resource/Factory/CWorldLoader.cpp | 40 ++++- 10 files changed, 402 insertions(+), 52 deletions(-) create mode 100644 src/Core/Resource/CSavedStateID.h diff --git a/src/Core/Core.pro b/src/Core/Core.pro index 0f14781c..2aa2884c 100644 --- a/src/Core/Core.pro +++ b/src/Core/Core.pro @@ -226,7 +226,8 @@ HEADERS += \ Resource/CAudioMacro.h \ CompressionUtil.h \ Resource/Animation/CSourceAnimData.h \ - Resource/CMapArea.h + Resource/CMapArea.h \ + Resource/CSavedStateID.h # Source Files SOURCES += \ diff --git a/src/Core/GameProject/CPackage.cpp b/src/Core/GameProject/CPackage.cpp index f761935f..fe2a9ab4 100644 --- a/src/Core/GameProject/CPackage.cpp +++ b/src/Core/GameProject/CPackage.cpp @@ -77,48 +77,94 @@ void CPackage::Cook() return; } - // todo: MP3/DKCR pak format support - Pak.WriteLong(0x00030005); // Major/Minor Version - Pak.WriteLong(0); // Unknown + EGame Game = mpProject->Game(); + u32 Alignment = (Game <= eCorruptionProto ? 0x20 : 0x40); + u32 AlignmentMinusOne = Alignment - 1; - // Named Resources - Pak.WriteLong(mResources.size()); + u32 TocOffset = 0; + u32 NamesSize = 0; + u32 ResTableOffset = 0; + u32 ResTableSize = 0; + u32 ResDataSize = 0; - for (auto Iter = mResources.begin(); Iter != mResources.end(); Iter++) + // Write MP1 pak header + if (Game <= eCorruptionProto) { - const SNamedResource& rkRes = *Iter; - rkRes.Type.Write(Pak); - rkRes.ID.Write(Pak); - Pak.WriteSizedString(rkRes.Name); + Pak.WriteLong(0x00030005); // Major/Minor Version + Pak.WriteLong(0); // Unknown + + // Named Resources + Pak.WriteLong(mResources.size()); + + for (auto Iter = mResources.begin(); Iter != mResources.end(); Iter++) + { + const SNamedResource& rkRes = *Iter; + rkRes.Type.Write(Pak); + rkRes.ID.Write(Pak); + Pak.WriteSizedString(rkRes.Name); + } } - // Fill in table of contents with junk, write later + // Write MP3 pak header + else + { + // Header + Pak.WriteLong(2); // Version + Pak.WriteLong(0x40); // Header size + Pak.WriteToBoundary(0x40, 0); // We don't care about the MD5 hash; the game doesn't use it + + // PAK table of contents; write later + TocOffset = Pak.Tell(); + Pak.WriteLong(0); + Pak.WriteToBoundary(0x40, 0); + + // Named Resources + u32 NamesStart = Pak.Tell(); + Pak.WriteLong(mResources.size()); + + for (auto Iter = mResources.begin(); Iter != mResources.end(); Iter++) + { + const SNamedResource& rkRes = *Iter; + Pak.WriteString(rkRes.Name); + rkRes.Type.Write(Pak); + rkRes.ID.Write(Pak); + } + + Pak.WriteToBoundary(0x40, 0); + NamesSize = Pak.Tell() - NamesStart; + } + + // Fill in resource table with junk, write later + ResTableOffset = Pak.Tell(); Pak.WriteLong(AssetList.size()); - u32 TocOffset = Pak.Tell(); + CAssetID Dummy = CAssetID::InvalidID(Game); for (u32 iRes = 0; iRes < AssetList.size(); iRes++) { Pak.WriteLongLong(0); + Dummy.Write(Pak); Pak.WriteLongLong(0); - Pak.WriteLong(0); } - Pak.WriteToBoundary(32, 0); + Pak.WriteToBoundary(Alignment, 0); + ResTableSize = Pak.Tell() - ResTableOffset; // Start writing resources - struct SResourceTocInfo + struct SResourceTableInfo { CResourceEntry *pEntry; u32 Offset; u32 Size; bool Compressed; }; - std::vector ResourceTocData(AssetList.size()); + std::vector ResourceTableData(AssetList.size()); u32 ResIdx = 0; + u32 ResDataOffset = Pak.Tell(); for (auto Iter = AssetList.begin(); Iter != AssetList.end(); Iter++, ResIdx++) { // Initialize entry, recook assets if needed + u32 AssetOffset = Pak.Tell(); CAssetID ID = *Iter; CResourceEntry *pEntry = gpResourceStore->FindEntry(ID); ASSERT(pEntry != nullptr); @@ -126,9 +172,9 @@ void CPackage::Cook() if (pEntry->NeedsRecook()) pEntry->Cook(); - SResourceTocInfo& rTocInfo = ResourceTocData[ResIdx]; - rTocInfo.pEntry = pEntry; - rTocInfo.Offset = Pak.Tell(); + SResourceTableInfo& rTableInfo = ResourceTableData[ResIdx]; + rTableInfo.pEntry = pEntry; + rTableInfo.Offset = (Game <= eEchoes ? AssetOffset : AssetOffset - ResDataOffset); // Load resource data CFileInStream CookedAsset(pEntry->CookedAssetPath(), IOUtil::eBigEndian); @@ -141,21 +187,31 @@ void CPackage::Cook() // Check if this asset should be compressed; there are a few resource types that are // always compressed, and some types that are compressed if they're over a certain size EResType Type = pEntry->ResourceType(); + u32 CompressThreshold = (Game <= eCorruptionProto ? 0x400 : 0x80); bool ShouldAlwaysCompress = (Type == eTexture || Type == eModel || Type == eSkin || Type == eAnimSet || Type == eAnimation || Type == eFont); + if (Game >= eCorruption) + { + ShouldAlwaysCompress = ShouldAlwaysCompress || + (Type == eCharacter || Type == eSourceAnimData || Type == eScan || + Type == eAudioSample || Type == eStringTable || Type == eAudioAmplitudeData || + Type == eDynamicCollision); + } + bool ShouldCompressConditional = !ShouldAlwaysCompress && (Type == eParticle || Type == eParticleElectric || Type == eParticleSwoosh || - Type == eParticleWeapon || Type == eParticleDecal || Type == eParticleCollisionResponse); + Type == eParticleWeapon || Type == eParticleDecal || Type == eParticleCollisionResponse || + Type == eParticleSpawn || Type == eParticleSorted || Type == eBurstFireData); - bool ShouldCompress = ShouldAlwaysCompress || (ShouldCompressConditional && ResourceSize >= 0x400); + bool ShouldCompress = ShouldAlwaysCompress || (ShouldCompressConditional && ResourceSize >= CompressThreshold); // Write resource data to pak if (!ShouldCompress) { Pak.WriteBytes(ResourceData.data(), ResourceSize); - rTocInfo.Compressed = false; + rTableInfo.Compressed = false; } else @@ -164,7 +220,7 @@ void CPackage::Cook() std::vector CompressedData(ResourceData.size() * 2); bool Success = false; - if (mpProject->Game() <= eEchoesDemo) + if (Game <= eEchoesDemo || Game == eReturns) Success = CompressionUtil::CompressZlib(ResourceData.data(), ResourceData.size(), CompressedData.data(), CompressedData.size(), CompressedSize); else Success = CompressionUtil::CompressLZOSegmented(ResourceData.data(), ResourceData.size(), CompressedData.data(), CompressedSize, false); @@ -172,44 +228,77 @@ void CPackage::Cook() // Make sure that the compressed data is actually smaller, accounting for padding + uncompressed size value if (Success) { - u32 PaddedUncompressedSize = (ResourceSize + 0x1F) & ~0x1F; - u32 PaddedCompressedSize = (CompressedSize + 4 + 0x1F) & ~0x1F; + u32 CompressionHeaderSize = (Game <= eCorruptionProto ? 4 : 0x10); + u32 PaddedUncompressedSize = (ResourceSize + AlignmentMinusOne) & ~AlignmentMinusOne; + u32 PaddedCompressedSize = (CompressedSize + CompressionHeaderSize + AlignmentMinusOne) & ~AlignmentMinusOne; Success = (PaddedCompressedSize < PaddedUncompressedSize); } // Write file to pak if (Success) { - Pak.WriteLong(ResourceSize); + // Write MP1/2 compressed asset + if (Game <= eCorruptionProto) + { + Pak.WriteLong(ResourceSize); + } + // Write MP3/DKCR compressed asset + else + { + // Note: Compressed asset data can be stored in multiple blocks. Normally, the only assets that make use of this are textures, + // which can store each separate component of the file (header, palette, image data) in separate blocks. However, some textures + // are stored in one block, and I've had no luck figuring out why. The game doesn't generally seem to care whether textures use + // multiple blocks or not, so for the sake of complicity we compress everything to one block. + Pak.WriteFourCC( FOURCC('CMPD') ); + Pak.WriteLong(1); + Pak.WriteLong(0xA0000000 | CompressedSize); + Pak.WriteLong(ResourceSize); + } Pak.WriteBytes(CompressedData.data(), CompressedSize); } else Pak.WriteBytes(ResourceData.data(), ResourceSize); - rTocInfo.Compressed = Success; + rTableInfo.Compressed = Success; } - Pak.WriteToBoundary(32, 0xFF); - rTocInfo.Size = Pak.Tell() - rTocInfo.Offset; + Pak.WriteToBoundary(Alignment, 0xFF); + rTableInfo.Size = Pak.Tell() - AssetOffset; } + ResDataSize = Pak.Tell() - ResDataOffset; // Write table of contents for real - Pak.Seek(TocOffset, SEEK_SET); + if (Game >= eCorruption) + { + Pak.Seek(TocOffset, SEEK_SET); + Pak.WriteLong(3); // Always 3 pak sections + Pak.WriteFourCC( FOURCC('STRG') ); + Pak.WriteLong(NamesSize); + Pak.WriteFourCC( FOURCC('RSHD') ); + Pak.WriteLong(ResTableSize); + Pak.WriteFourCC( FOURCC('DATA') ); + Pak.WriteLong(ResDataSize); + } + + // Write resource table for real + Pak.Seek(ResTableOffset+4, SEEK_SET); for (u32 iRes = 0; iRes < AssetList.size(); iRes++) { - const SResourceTocInfo& rkTocInfo = ResourceTocData[iRes]; - CResourceEntry *pEntry = rkTocInfo.pEntry; + const SResourceTableInfo& rkInfo = ResourceTableData[iRes]; + CResourceEntry *pEntry = rkInfo.pEntry; - Pak.WriteLong( rkTocInfo.Compressed ? 1 : 0 ); + Pak.WriteLong( rkInfo.Compressed ? 1 : 0 ); pEntry->CookedExtension().Write(Pak); pEntry->ID().Write(Pak); - Pak.WriteLong(rkTocInfo.Size); - Pak.WriteLong(rkTocInfo.Offset); + Pak.WriteLong(rkInfo.Size); + Pak.WriteLong(rkInfo.Offset); } + // Clear recook flag mNeedsRecook = false; Save(); + Log::Write("Finished writing " + PakPath); // Update resource store in case we recooked any assets diff --git a/src/Core/GameProject/DependencyListBuilders.cpp b/src/Core/GameProject/DependencyListBuilders.cpp index 2b5fc36f..f15a0d3f 100644 --- a/src/Core/GameProject/DependencyListBuilders.cpp +++ b/src/Core/GameProject/DependencyListBuilders.cpp @@ -461,6 +461,10 @@ void CAreaDependencyListBuilder::AddDependency(const CAssetID& rkID, std::list + +// GUID representing a value stored in the save file for MP3/DKCR +class CSavedStateID +{ + u64 m[2]; + +public: + CSavedStateID() + { + m[0] = 0; + m[1] = 0; + } + + CSavedStateID(u64 Part1, u64 Part2) + { + m[0] = Part1; + m[1] = Part2; + } + + CSavedStateID(IInputStream& rInput) + { + m[0] = rInput.ReadLongLong(); + m[1] = rInput.ReadLongLong(); + } + + TString ToString() + { + u32 Part1 = (m[0] >> 32) & 0xFFFFFFFF; + u32 Part2 = (m[0] >> 16) & 0x0000FFFF; + u32 Part3 = (m[0] >> 00) & 0x0000FFFF; + u32 Part4 = (m[1] >> 48) & 0x0000FFFF; + u32 Part5 = (m[1] >> 32) & 0x0000FFFF; + u32 Part6 = (m[1] >> 00) & 0xFFFFFFFF; + return TString::Format("%08X-%04X-%04X-%04X-%04X%08X", Part1, Part2, Part3, Part4, Part5, Part6); + } + + void Write(IOutputStream& rOutput) + { + rOutput.WriteLongLong(m[0]); + rOutput.WriteLongLong(m[1]); + } + + void Serialize(IArchive& rArc) + { + TString Str; + if (rArc.IsWriter()) Str = ToString(); + rArc.SerializePrimitive(Str); + if (rArc.IsReader()) *this = FromString(Str); + } + + // Operators + inline bool operator==(const CSavedStateID& rkOther) + { + return (m[0] == rkOther.m[0] && m[1] == rkOther.m[1]); + } + + inline bool operator!=(const CSavedStateID& rkOther) + { + return !(*this == rkOther); + } + + inline bool operator<(const CSavedStateID& rkOther) + { + return (m[0] == rkOther.m[0] ? m[1] < rkOther.m[1] : m[0] < rkOther.m[0]); + } + + // Static + static CSavedStateID FromString(TString Str) + { + Str.Remove('-'); + ASSERT(Str.Size() == 32); + + CSavedStateID Out; + Out.m[0] = Str.SubString(0, 16).ToInt64(16); + Out.m[1] = Str.SubString(16, 16).ToInt64(16); + return Out; + } + +}; + +#endif // CSAVEDSTATEID_H diff --git a/src/Core/Resource/CWorld.cpp b/src/Core/Resource/CWorld.cpp index 317a7c3f..0a1f59d5 100644 --- a/src/Core/Resource/CWorld.cpp +++ b/src/Core/Resource/CWorld.cpp @@ -94,12 +94,25 @@ void CWorld::Serialize(IArchive& rArc) if (rArc.Game() >= eEchoesDemo && rArc.Game() <= eCorruption) rArc << SERIAL("TempleKeyWorldIndex", mTempleKeyWorldIndex); + if (rArc.Game() == eReturns) + rArc << SERIAL("TimeAttackData", mTimeAttackData); + if (rArc.Game() == ePrime) rArc << SERIAL_CONTAINER("MemoryRelays", mMemoryRelays, "MemoryRelay"); rArc << SERIAL_CONTAINER("Areas", mAreas, "Area"); } +void Serialize(IArchive& rArc, CWorld::STimeAttackData& rData) +{ + rArc << SERIAL("HasTimeAttack", rData.HasTimeAttack) + << SERIAL("ActNumber", rData.ActNumber) + << SERIAL("BronzeTime", rData.BronzeTime) + << SERIAL("SilverTime", rData.SilverTime) + << SERIAL("GoldTime", rData.GoldTime) + << SERIAL("ShinyGoldTime", rData.ShinyGoldTime); +} + void Serialize(IArchive& rArc, CWorld::SMemoryRelay& rMemRelay) { rArc << SERIAL_HEX("MemoryRelayID", rMemRelay.InstanceID) @@ -140,6 +153,11 @@ void Serialize(IArchive& rArc, CWorld::SArea::SLayer& rLayer) { rArc << SERIAL("Name", rLayer.LayerName) << SERIAL("Active", rLayer.Active); + + if (rArc.Game() >= eCorruption) + { + rArc << SERIAL("StateID", rLayer.LayerStateID); + } } void Serialize(IArchive& rArc, CWorld::SAudioGrp& rAudioGrp) diff --git a/src/Core/Resource/CWorld.h b/src/Core/Resource/CWorld.h index 9cf2b228..8680f64b 100644 --- a/src/Core/Resource/CWorld.h +++ b/src/Core/Resource/CWorld.h @@ -2,6 +2,7 @@ #define CWORLD_H #include "CResource.h" +#include "CSavedStateID.h" #include "CStringTable.h" #include "Core/Resource/Area/CGameArea.h" #include "Core/Resource/Model/CModel.h" @@ -22,6 +23,16 @@ class CWorld : public CResource TResPtr mpMapWorld; u32 mTempleKeyWorldIndex; + struct STimeAttackData + { + bool HasTimeAttack; + TString ActNumber; + float BronzeTime; + float SilverTime; + float GoldTime; + float ShinyGoldTime; + } mTimeAttackData; + struct SAudioGrp { CAssetID ResID; @@ -69,7 +80,7 @@ class CWorld : public CResource { TString LayerName; bool Active; - u8 LayerID[16]; + CSavedStateID LayerStateID; }; std::vector Layers; }; @@ -86,6 +97,7 @@ public: // Serialization virtual void Serialize(IArchive& rArc); + friend void Serialize(IArchive& rArc, STimeAttackData& rTimeAttackData); friend void Serialize(IArchive& rArc, SMemoryRelay& rMemRelay); friend void Serialize(IArchive& rArc, SArea& rArea); friend void Serialize(IArchive& rArc, SArea::SDock& rDock); diff --git a/src/Core/Resource/Cooker/CAreaCooker.cpp b/src/Core/Resource/Cooker/CAreaCooker.cpp index 8de2144d..c617a375 100644 --- a/src/Core/Resource/Cooker/CAreaCooker.cpp +++ b/src/Core/Resource/Cooker/CAreaCooker.cpp @@ -1,11 +1,26 @@ #include "CAreaCooker.h" #include "CScriptCooker.h" #include "Core/CompressionUtil.h" +#include "Core/GameProject/DependencyListBuilders.h" #include -const bool gkForceDisableCompression = false; +const bool gkForceDisableCompression = true; CAreaCooker::CAreaCooker() + : mGeometrySecNum(-1) + , mSCLYSecNum(-1) + , mSCGNSecNum(-1) + , mCollisionSecNum(-1) + , mUnknownSecNum(-1) + , mLightsSecNum(-1) + , mVISISecNum(-1) + , mPATHSecNum(-1) + , mAROTSecNum(-1) + , mFFFFSecNum(-1) + , mPTLASecNum(-1) + , mEGMCSecNum(-1) + , mDepsSecNum(-1) + , mModulesSecNum(-1) { } @@ -69,8 +84,10 @@ void CAreaCooker::DetermineSectionNumbersCorruption() for (u32 iNum = 0; iNum < mpArea->mSectionNumbers.size(); iNum++) { CGameArea::SSectionNumber& rNum = mpArea->mSectionNumbers[iNum]; - if (rNum.SectionID == "SOBJ") mSCLYSecNum = rNum.Index; + if (rNum.SectionID == "SOBJ") mSCLYSecNum = rNum.Index; else if (rNum.SectionID == "SGEN") mSCGNSecNum = rNum.Index; + else if (rNum.SectionID == "DEPS") mDepsSecNum = rNum.Index; + else if (rNum.SectionID == "RSOS") mModulesSecNum = rNum.Index; } } @@ -228,6 +245,57 @@ void CAreaCooker::WriteEchoesSCLY(IOutputStream& rOut) FinishSection(true); } +void CAreaCooker::WriteDependencies(IOutputStream& rOut) +{ + // Build dependency list + std::list Dependencies; + std::list LayerOffsets; + + CAreaDependencyListBuilder Builder(mpArea->Entry()); + Builder.BuildDependencyList(Dependencies, LayerOffsets); + + // Write + rOut.WriteLong(Dependencies.size()); + + for (auto Iter = Dependencies.begin(); Iter != Dependencies.end(); Iter++) + { + CAssetID ID = *Iter; + CResourceEntry *pEntry = gpResourceStore->FindEntry(ID); + ID.Write(rOut); + pEntry->CookedExtension().Write(rOut); + } + + rOut.WriteLong(LayerOffsets.size()); + + for (auto Iter = LayerOffsets.begin(); Iter != LayerOffsets.end(); Iter++) + rOut.WriteLong(*Iter); + + FinishSection(false); +} + +void CAreaCooker::WriteModules(IOutputStream& rOut) +{ + // Build module list + std::vector ModuleNames; + std::vector LayerOffsets; + + CAreaDependencyTree *pAreaDeps = static_cast(mpArea->Entry()->Dependencies()); + pAreaDeps->GetModuleDependencies(mpArea->Game(), ModuleNames, LayerOffsets); + + // Write + rOut.WriteLong(ModuleNames.size()); + + for (u32 ModuleIdx = 0; ModuleIdx < ModuleNames.size(); ModuleIdx++) + rOut.WriteString( ModuleNames[ModuleIdx] ); + + rOut.WriteLong(LayerOffsets.size()); + + for (u32 OffsetIdx = 0; OffsetIdx < LayerOffsets.size(); OffsetIdx++) + rOut.WriteLong(LayerOffsets[OffsetIdx]); + + FinishSection(false); +} + // ************ SECTION MANAGEMENT ************ void CAreaCooker::AddSectionToBlock() { @@ -322,8 +390,14 @@ bool CAreaCooker::CookMREA(CGameArea *pArea, IOutputStream& rOut) // Write pre-SCLY data sections for (u32 iSec = 0; iSec < Cooker.mSCLYSecNum; iSec++) { - Cooker.mSectionData.WriteBytes(pArea->mSectionDataBuffers[iSec].data(), pArea->mSectionDataBuffers[iSec].size()); - Cooker.FinishSection(false); + if (iSec == Cooker.mDepsSecNum) + Cooker.WriteDependencies(Cooker.mSectionData); + + else + { + Cooker.mSectionData.WriteBytes(pArea->mSectionDataBuffers[iSec].data(), pArea->mSectionDataBuffers[iSec].size()); + Cooker.FinishSection(false); + } } // Write SCLY @@ -336,8 +410,14 @@ bool CAreaCooker::CookMREA(CGameArea *pArea, IOutputStream& rOut) u32 PostSCLY = (Cooker.mVersion <= ePrime ? Cooker.mSCLYSecNum + 1 : Cooker.mSCGNSecNum + 1); for (u32 iSec = PostSCLY; iSec < pArea->mSectionDataBuffers.size(); iSec++) { - Cooker.mSectionData.WriteBytes(pArea->mSectionDataBuffers[iSec].data(), pArea->mSectionDataBuffers[iSec].size()); - Cooker.FinishSection(false); + if (iSec == Cooker.mModulesSecNum) + Cooker.WriteModules(Cooker.mSectionData); + + else + { + Cooker.mSectionData.WriteBytes(pArea->mSectionDataBuffers[iSec].data(), pArea->mSectionDataBuffers[iSec].size()); + Cooker.FinishSection(false); + } } Cooker.FinishBlock(); diff --git a/src/Core/Resource/Cooker/CAreaCooker.h b/src/Core/Resource/Cooker/CAreaCooker.h index 53893e76..fe9b0e01 100644 --- a/src/Core/Resource/Cooker/CAreaCooker.h +++ b/src/Core/Resource/Cooker/CAreaCooker.h @@ -25,6 +25,8 @@ class CAreaCooker u32 mFFFFSecNum; u32 mPTLASecNum; u32 mEGMCSecNum; + u32 mDepsSecNum; + u32 mModulesSecNum; struct SCompressedBlock { @@ -57,6 +59,10 @@ class CAreaCooker void WritePrimeSCLY(IOutputStream& rOut); void WriteEchoesSCLY(IOutputStream& rOut); + // Other Sections + void WriteDependencies(IOutputStream& rOut); + void WriteModules(IOutputStream& rOut); + // Section Management void AddSectionToBlock(); void FinishSection(bool ForceFinishBlock); diff --git a/src/Core/Resource/Cooker/CWorldCooker.cpp b/src/Core/Resource/Cooker/CWorldCooker.cpp index 8abdfe47..19aedeac 100644 --- a/src/Core/Resource/Cooker/CWorldCooker.cpp +++ b/src/Core/Resource/Cooker/CWorldCooker.cpp @@ -25,7 +25,24 @@ bool CWorldCooker::CookMLVL(CWorld *pWorld, IOutputStream& rMLVL) if (Game == eEchoesDemo || Game == eEchoes) { DarkWorldNameID.Write(rMLVL); - rMLVL.WriteLong(0); + } + if (Game >= eEchoesDemo && Game <= eCorruption) + { + rMLVL.WriteLong(pWorld->mTempleKeyWorldIndex); + } + if (Game == eReturns) + { + const CWorld::STimeAttackData& rkData = pWorld->mTimeAttackData; + rMLVL.WriteBool(rkData.HasTimeAttack); + + if (rkData.HasTimeAttack) + { + rMLVL.WriteString(rkData.ActNumber); + rMLVL.WriteFloat(rkData.BronzeTime); + rMLVL.WriteFloat(rkData.SilverTime); + rMLVL.WriteFloat(rkData.GoldTime); + rMLVL.WriteFloat(rkData.ShinyGoldTime); + } } SaveWorldID.Write(rMLVL); @@ -142,6 +159,10 @@ bool CWorldCooker::CookMLVL(CWorld *pWorld, IOutputStream& rMLVL) rMLVL.WriteLong(ModuleLayerOffsets[iOff]); } + // Unknown + if (Game == eReturns) + rMLVL.WriteLong(0); + // Internal Name if (Game >= eEchoesDemo) rMLVL.WriteString(rArea.InternalName); @@ -191,6 +212,7 @@ bool CWorldCooker::CookMLVL(CWorld *pWorld, IOutputStream& rMLVL) // Layers rMLVL.WriteLong(pWorld->mAreas.size()); std::vector LayerNames; + std::vector LayerStateIDs; std::vector LayerNameOffsets; // Layer Flags @@ -209,6 +231,7 @@ bool CWorldCooker::CookMLVL(CWorld *pWorld, IOutputStream& rMLVL) LayerActiveFlags &= ~(1 << iLyr); LayerNames.push_back(rLayer.LayerName); + LayerStateIDs.push_back(rLayer.LayerStateID); } rMLVL.WriteLongLong(LayerActiveFlags); @@ -220,9 +243,13 @@ bool CWorldCooker::CookMLVL(CWorld *pWorld, IOutputStream& rMLVL) for (u32 iLyr = 0; iLyr < LayerNames.size(); iLyr++) rMLVL.WriteString(LayerNames[iLyr]); - // todo: Layer Saved State IDs go here for MP3/DKCR; need support for saved state IDs to implement - if (Game == eCorruption || Game == eReturns) + // Layer Saved State IDs + if (Game >= eCorruption) { + rMLVL.WriteLong(LayerStateIDs.size()); + + for (u32 iLyr = 0; iLyr < LayerStateIDs.size(); iLyr++) + LayerStateIDs[iLyr].Write(rMLVL); } // Layer Name Offsets diff --git a/src/Core/Resource/Factory/CWorldLoader.cpp b/src/Core/Resource/Factory/CWorldLoader.cpp index e5d7edd4..fcac5205 100644 --- a/src/Core/Resource/Factory/CWorldLoader.cpp +++ b/src/Core/Resource/Factory/CWorldLoader.cpp @@ -167,19 +167,37 @@ void CWorldLoader::LoadPrimeMLVL(IInputStream& rMLVL) pArea->Layers[iLayer].LayerName = rMLVL.ReadString(); } + // Layer state IDs + if (mVersion >= eCorruption) + { + rMLVL.Seek(0x4, SEEK_CUR); // Skipping redundant layer count + for (u32 iArea = 0; iArea < NumAreas; iArea++) + { + CWorld::SArea *pArea = &mpWorld->mAreas[iArea]; + u32 NumLayers = pArea->Layers.size(); + + for (u32 iLayer = 0; iLayer < NumLayers; iLayer++) + pArea->Layers[iLayer].LayerStateID = CSavedStateID(rMLVL); + } + } + // Last part of the file is layer name offsets, but we don't need it - // todo: Layer ID support for MP3 } void CWorldLoader::LoadReturnsMLVL(IInputStream& rMLVL) { mpWorld->mpWorldName = gpResourceStore->LoadResource(rMLVL.ReadLongLong()); - bool Check = (rMLVL.ReadByte() != 0); - if (Check) + CWorld::STimeAttackData& rData = mpWorld->mTimeAttackData; + rData.HasTimeAttack = rMLVL.ReadBool(); + + if (rData.HasTimeAttack) { - rMLVL.ReadString(); - rMLVL.Seek(0x10, SEEK_CUR); + rData.ActNumber = rMLVL.ReadString(); + rData.BronzeTime = rMLVL.ReadFloat(); + rData.SilverTime = rMLVL.ReadFloat(); + rData.GoldTime = rMLVL.ReadFloat(); + rData.ShinyGoldTime = rMLVL.ReadFloat(); } mpWorld->mpSaveWorld = gpResourceStore->LoadResource(rMLVL.ReadLongLong(), eSaveWorld); @@ -229,8 +247,18 @@ void CWorldLoader::LoadReturnsMLVL(IInputStream& rMLVL) pArea->Layers[iLayer].LayerName = rMLVL.ReadString(); } + // Layer state IDs + rMLVL.Seek(0x4, SEEK_CUR); // Skipping redundant layer count + for (u32 iArea = 0; iArea < NumAreas; iArea++) + { + CWorld::SArea *pArea = &mpWorld->mAreas[iArea]; + u32 NumLayers = pArea->Layers.size(); + + for (u32 iLayer = 0; iLayer < NumLayers; iLayer++) + pArea->Layers[iLayer].LayerStateID = CSavedStateID(rMLVL); + } + // Last part of the file is layer name offsets, but we don't need it - // todo: Layer ID support } void CWorldLoader::GenerateEditorData()