From de18044ae01c0b058b70c249d54cae59ca544472 Mon Sep 17 00:00:00 2001 From: parax0 Date: Fri, 12 Aug 2016 04:27:19 -0600 Subject: [PATCH] Fixed some missed dependencies in a few formats and implemented support for building file lists for paks and MLVLs, and implemented support for package cooking for MP1 --- src/Common/CAssetID.cpp | 8 + src/Common/CAssetID.h | 1 + src/Common/CFourCC.h | 2 +- src/Core/Core.pro | 3 +- src/Core/GameProject/CDependencyTree.cpp | 53 +++- src/Core/GameProject/CDependencyTree.h | 22 +- src/Core/GameProject/CGameExporter.cpp | 71 ++++- src/Core/GameProject/CGameExporter.h | 12 +- src/Core/GameProject/CPackage.cpp | 242 ++++++++++++++++ src/Core/GameProject/CPackage.h | 4 + src/Core/GameProject/CResourceEntry.cpp | 12 +- src/Core/GameProject/CResourceEntry.h | 2 + src/Core/GameProject/DependencyListBuilders.h | 264 ++++++++++++++++++ src/Core/Resource/Area/CGameArea.cpp | 11 +- src/Core/Resource/CAnimSet.h | 15 +- src/Core/Resource/CMaterialSet.h | 15 + src/Core/Resource/CScan.h | 10 +- src/Core/Resource/CStringTable.h | 67 ++++- src/Core/Resource/CWorld.cpp | 1 + src/Core/Resource/CWorld.h | 7 +- .../Resource/Cooker/CPoiToWorldCooker.cpp | 2 - src/Core/Resource/Cooker/CWorldCooker.cpp | 153 ++++++++++ src/Core/Resource/Cooker/CWorldCooker.h | 2 + src/Core/Resource/Factory/CAreaLoader.cpp | 9 + .../Factory/CDependencyGroupLoader.cpp | 8 +- src/Core/Resource/Factory/CMaterialLoader.cpp | 4 +- src/Core/Resource/Factory/CScanLoader.cpp | 9 +- src/Core/Resource/Model/CModel.cpp | 17 +- src/Editor/CProjectOverviewDialog.cpp | 25 +- src/Editor/CProjectOverviewDialog.h | 2 + src/Editor/CProjectOverviewDialog.ui | 183 +++++++----- src/Editor/CStartWindow.cpp | 10 +- src/FileIO/IOutputStream.cpp | 2 +- src/FileIO/IOutputStream.h | 2 +- templates/mp1/Script/AreaAttributes.xml | 1 + templates/mp1/Script/Drone.xml | 6 +- templates/mp1/Script/FishCloud.xml | 2 +- templates/mp1/Script/NewIntroBoss.xml | 4 +- templates/mp1/Structs/LightParameters.xml | 2 + 39 files changed, 1094 insertions(+), 171 deletions(-) create mode 100644 src/Core/GameProject/DependencyListBuilders.h diff --git a/src/Common/CAssetID.cpp b/src/Common/CAssetID.cpp index 38dd8896..d766e834 100644 --- a/src/Common/CAssetID.cpp +++ b/src/Common/CAssetID.cpp @@ -38,6 +38,14 @@ CAssetID::CAssetID(const char* pkID) *this = CAssetID::FromString(pkID); } +void CAssetID::Write(IOutputStream& rOutput) const +{ + if (mLength == e32Bit) + rOutput.WriteLong(ToLong()); + else + rOutput.WriteLongLong(ToLongLong()); +} + CAssetID::CAssetID(IInputStream& rInput, EIDLength Length) : mLength(Length) { diff --git a/src/Common/CAssetID.h b/src/Common/CAssetID.h index 50c83cc8..c713e511 100644 --- a/src/Common/CAssetID.h +++ b/src/Common/CAssetID.h @@ -23,6 +23,7 @@ public: CAssetID(u64 ID, EIDLength Length); CAssetID(const char* pkID); CAssetID(IInputStream& rInput, EIDLength Length); + void Write(IOutputStream& rOutput) const; TString ToString() const; bool IsValid() const; diff --git a/src/Common/CFourCC.h b/src/Common/CFourCC.h index 87ebc4ea..776798cd 100644 --- a/src/Common/CFourCC.h +++ b/src/Common/CFourCC.h @@ -35,7 +35,7 @@ public: if (rInput.GetEndianness() == IOUtil::eLittleEndian) Reverse(); } - inline void Write(IOutputStream& rOutput) + inline void Write(IOutputStream& rOutput) const { u32 Val = mFourCC; if (rOutput.GetEndianness() == IOUtil::eLittleEndian) IOUtil::SwapBytes(Val); diff --git a/src/Core/Core.pro b/src/Core/Core.pro index c44e8236..564cf630 100644 --- a/src/Core/Core.pro +++ b/src/Core/Core.pro @@ -203,7 +203,8 @@ HEADERS += \ Resource/ParticleParameters.h \ Resource/Factory/CUnsupportedParticleLoader.h \ Resource/Resources.h \ - Resource/Factory/CResourceFactory.h + Resource/Factory/CResourceFactory.h \ + GameProject/DependencyListBuilders.h # Source Files SOURCES += \ diff --git a/src/Core/GameProject/CDependencyTree.cpp b/src/Core/GameProject/CDependencyTree.cpp index 5f0beb75..0d10a35c 100644 --- a/src/Core/GameProject/CDependencyTree.cpp +++ b/src/Core/GameProject/CDependencyTree.cpp @@ -115,16 +115,15 @@ CAssetID CDependencyTree::DependencyByIndex(u32 Index) const return mReferencedResources[Index]->ID(); } -void CDependencyTree::AddDependency(CResource *pRes) +void CDependencyTree::AddDependency(CResource *pRes, bool AvoidDuplicates /*= true*/) { - if (!pRes || HasDependency(pRes->ID())) return; - CResourceDependency *pDepend = new CResourceDependency(pRes->ID()); - mReferencedResources.push_back(pDepend); + if (!pRes) return; + AddDependency(pRes->ID(), AvoidDuplicates); } -void CDependencyTree::AddDependency(const CAssetID& rkID) +void CDependencyTree::AddDependency(const CAssetID& rkID, bool AvoidDuplicates /*= true*/) { - if (!rkID.IsValid() || HasDependency(rkID)) return; + if (!rkID.IsValid() || (AvoidDuplicates && HasDependency(rkID))) return; CResourceDependency *pDepend = new CResourceDependency(rkID); mReferencedResources.push_back(pDepend); } @@ -154,14 +153,15 @@ void CAnimSetDependencyTree::Write(IOutputStream& rFile, EIDLength IDLength) con rFile.WriteLong( mCharacterOffsets[iChar] ); } -void CAnimSetDependencyTree::AddCharacter(const SSetCharacter *pkChar) +void CAnimSetDependencyTree::AddCharacter(const SSetCharacter *pkChar, const std::set& rkBaseUsedSet) { mCharacterOffsets.push_back( NumDependencies() ); if (!pkChar) return; - AddDependency(pkChar->pModel); - AddDependency(pkChar->pSkeleton); - AddDependency(pkChar->pSkin); + std::set UsedSet = rkBaseUsedSet; + AddCharDependency(pkChar->pModel, UsedSet); + AddCharDependency(pkChar->pSkeleton, UsedSet); + AddCharDependency(pkChar->pSkin, UsedSet); const std::vector *pkParticleVectors[5] = { &pkChar->GenericParticles, &pkChar->ElectricParticles, @@ -172,11 +172,26 @@ void CAnimSetDependencyTree::AddCharacter(const SSetCharacter *pkChar) for (u32 iVec = 0; iVec < 5; iVec++) { for (u32 iPart = 0; iPart < pkParticleVectors[iVec]->size(); iPart++) - AddDependency(pkParticleVectors[iVec]->at(iPart)); + AddCharDependency(pkParticleVectors[iVec]->at(iPart), UsedSet); } - AddDependency(pkChar->IceModel); - AddDependency(pkChar->IceSkin); + AddCharDependency(pkChar->IceModel, UsedSet); + AddCharDependency(pkChar->IceSkin, UsedSet); +} + +void CAnimSetDependencyTree::AddCharDependency(const CAssetID& rkID, std::set& rUsedSet) +{ + if (rkID.IsValid() && rUsedSet.find(rkID) == rUsedSet.end()) + { + rUsedSet.insert(rkID); + AddDependency(rkID, false); + } +} + +void CAnimSetDependencyTree::AddCharDependency(CResource *pRes, std::set& rUsedSet) +{ + if (!pRes) return; + AddCharDependency(pRes->ID(), rUsedSet); } // ************ CScriptInstanceDependencyTree ************ @@ -240,6 +255,12 @@ bool CScriptInstanceDependencyTree::HasDependency(const CAssetID& rkID) return false; } +CAssetID CScriptInstanceDependencyTree::DependencyByIndex(u32 Index) const +{ + ASSERT(Index >= 0 && Index < mDependencies.size()); + return mDependencies[Index]->ID(); +} + // Static CScriptInstanceDependencyTree* CScriptInstanceDependencyTree::BuildTree(CScriptObject *pInstance) { @@ -343,3 +364,9 @@ void CAreaDependencyTree::AddScriptLayer(CScriptLayer *pLayer) delete pTree; } } + +CScriptInstanceDependencyTree* CAreaDependencyTree::ScriptInstanceByIndex(u32 Index) const +{ + ASSERT(Index >= 0 && Index < mScriptInstances.size()); + return mScriptInstances[Index]; +} diff --git a/src/Core/GameProject/CDependencyTree.h b/src/Core/GameProject/CDependencyTree.h index e0d22e0e..5e9b947b 100644 --- a/src/Core/GameProject/CDependencyTree.h +++ b/src/Core/GameProject/CDependencyTree.h @@ -90,8 +90,8 @@ public: u32 NumDependencies() const; bool HasDependency(const CAssetID& rkID); CAssetID DependencyByIndex(u32 Index) const; - void AddDependency(const CAssetID& rkID); - void AddDependency(CResource *pRes); + void AddDependency(const CAssetID& rkID, bool AvoidDuplicates = true); + void AddDependency(CResource *pRes, bool AvoidDuplicates = true); // Accessors inline void SetID(const CAssetID& rkID) { mID = rkID; } @@ -111,7 +111,13 @@ public: virtual void Read(IInputStream& rFile, EIDLength IDLength); virtual void Write(IOutputStream& rFile, EIDLength IDLength) const; - void AddCharacter(const SSetCharacter *pkChar); + void AddCharacter(const SSetCharacter *pkChar, const std::set& rkBaseUsedSet); + void AddCharDependency(const CAssetID& rkID, std::set& rUsedSet); + void AddCharDependency(CResource *pRes, std::set& rUsedSet); + + // Accessors + inline u32 NumCharacters() const { return mCharacterOffsets.size(); } + inline u32 CharacterOffset(u32 CharIdx) const { return mCharacterOffsets[CharIdx]; } }; // Node representing a script object. Indicates the type of object. @@ -128,9 +134,11 @@ public: virtual void Read(IInputStream& rFile, EIDLength IDLength); virtual void Write(IOutputStream& rFile, EIDLength IDLength) const; bool HasDependency(const CAssetID& rkID); + CAssetID DependencyByIndex(u32 Index) const; // Accessors - u32 NumDependencies() const { return mDependencies.size(); } + inline u32 NumDependencies() const { return mDependencies.size(); } + inline u32 ObjectType() const { return mObjectType; } // Static static CScriptInstanceDependencyTree* BuildTree(CScriptObject *pInstance); @@ -153,6 +161,12 @@ public: virtual void Write(IOutputStream& rFile, EIDLength IDLength) const; void AddScriptLayer(CScriptLayer *pLayer); + CScriptInstanceDependencyTree* ScriptInstanceByIndex(u32 Index) const; + + // Accessors + inline u32 NumScriptLayers() const { return mLayerOffsets.size(); } + inline u32 NumScriptInstances() const { return mScriptInstances.size(); } + inline u32 ScriptLayerOffset(u32 LayerIdx) const { return mLayerOffsets[LayerIdx]; } }; #endif // CDEPENDENCYTREE diff --git a/src/Core/GameProject/CGameExporter.cpp b/src/Core/GameProject/CGameExporter.cpp index 45b63da2..01d0f729 100644 --- a/src/Core/GameProject/CGameExporter.cpp +++ b/src/Core/GameProject/CGameExporter.cpp @@ -148,7 +148,7 @@ void CGameExporter::LoadAssetList() while (pAsset) { - u64 ResourceID = TString(pAsset->Attribute("ID")).ToInt64(16); + CAssetID ResourceID = TString(pAsset->Attribute("ID")).ToInt64(16); tinyxml2::XMLElement *pDir = pAsset->FirstChildElement("Dir"); TString Dir = pDir ? pDir->GetText() : ""; @@ -206,11 +206,15 @@ void CGameExporter::LoadPaks() u32 NameLen = Pak.ReadLong(); TString Name = Pak.ReadString(NameLen); pCollection->AddResource(Name, ResID, ResType); - SetResourcePath(ResID.ToLongLong(), PakName + L"\\", Name.ToUTF16()); + SetResourcePath(ResID, PakName + L"\\", Name.ToUTF16()); } u32 NumResources = Pak.ReadLong(); + // Keep track of which areas have duplicate resources + std::set PakResourceSet; + bool AreaHasDuplicates = true; // Default to true so that first area is always considered as having duplicates + for (u32 iRes = 0; iRes < NumResources; iRes++) { bool Compressed = (Pak.ReadLong() == 1); @@ -219,9 +223,21 @@ void CGameExporter::LoadPaks() u32 ResSize = Pak.ReadLong(); u32 ResOffset = Pak.ReadLong(); - u64 IntegralID = ResID.ToLongLong(); - if (mResourceMap.find(IntegralID) == mResourceMap.end()) - mResourceMap[IntegralID] = SResourceInstance { PakPath, ResID, ResType, ResOffset, ResSize, Compressed, false }; + if (mResourceMap.find(ResID) == mResourceMap.end()) + mResourceMap[ResID] = SResourceInstance { PakPath, ResID, ResType, ResOffset, ResSize, Compressed, false }; + + // Check for duplicate resources + if (ResType == "MREA") + { + mAreaDuplicateMap[ResID] = AreaHasDuplicates; + AreaHasDuplicates = false; + } + + else if (!AreaHasDuplicates && PakResourceSet.find(ResID) != PakResourceSet.end()) + AreaHasDuplicates = true; + + else + PakResourceSet.insert(ResID); } } } @@ -265,7 +281,7 @@ void CGameExporter::LoadPaks() CFourCC ResType = Pak.ReadLong(); CAssetID ResID(Pak, IDLength); pCollection->AddResource(Name, ResID, ResType); - SetResourcePath(ResID.ToLongLong(), PakName + L"\\", Name.ToUTF16()); + SetResourcePath(ResID, PakName + L"\\", Name.ToUTF16()); } } @@ -275,6 +291,10 @@ void CGameExporter::LoadPaks() u32 DataStart = Next; u32 NumResources = Pak.ReadLong(); + // Keep track of which areas have duplicate resources + std::set PakResourceSet; + bool AreaHasDuplicates = true; // Default to true so that first area is always considered as having duplicates + for (u32 iRes = 0; iRes < NumResources; iRes++) { bool Compressed = (Pak.ReadLong() == 1); @@ -283,9 +303,24 @@ void CGameExporter::LoadPaks() u32 Size = Pak.ReadLong(); u32 Offset = DataStart + Pak.ReadLong(); - u64 IntegralID = ResID.ToLongLong(); - if (mResourceMap.find(IntegralID) == mResourceMap.end()) - mResourceMap[IntegralID] = SResourceInstance { PakPath, ResID, Type, Offset, Size, Compressed, false }; + if (mResourceMap.find(ResID) == mResourceMap.end()) + mResourceMap[ResID] = SResourceInstance { PakPath, ResID, Type, Offset, Size, Compressed, false }; + + // Check for duplicate resources (unnecessary for DKCR) + if (Game() != eReturns) + { + if (Type == "MREA") + { + mAreaDuplicateMap[ResID] = AreaHasDuplicates; + AreaHasDuplicates = false; + } + + else if (!AreaHasDuplicates && PakResourceSet.find(ResID) != PakResourceSet.end()) + AreaHasDuplicates = true; + + else + PakResourceSet.insert(ResID); + } } } @@ -505,7 +540,25 @@ void CGameExporter::ExportCookedResources() for (CResourceIterator It(&mStore); It; ++It) { if (!It->IsTransient()) + { + // Worlds need to know which areas can have duplicates. We only have this info at export time. + if (It->ResourceType() == eWorld) + { + CWorld *pWorld = (CWorld*) It->Load(); + + for (u32 iArea = 0; iArea < pWorld->NumAreas(); iArea++) + { + CAssetID AreaID = pWorld->AreaResourceID(iArea); + auto Find = mAreaDuplicateMap.find(AreaID); + + if (Find != mAreaDuplicateMap.end()) + pWorld->SetAreaAllowsPakDuplicates(iArea, Find->second); + } + } + + // Save raw resource + cache data It->Save(); + } } } } diff --git a/src/Core/GameProject/CGameExporter.h b/src/Core/GameProject/CGameExporter.h index 2cfc475c..2c1ab0e9 100644 --- a/src/Core/GameProject/CGameExporter.h +++ b/src/Core/GameProject/CGameExporter.h @@ -26,6 +26,7 @@ class CGameExporter // Resources TWideStringList mPaks; + std::map mAreaDuplicateMap; struct SResourceInstance { @@ -37,14 +38,14 @@ class CGameExporter bool Compressed; bool Exported; }; - std::map mResourceMap; + std::map mResourceMap; struct SResourcePath { TWideString Dir; TWideString Name; }; - std::map mResourcePaths; + std::map mResourcePaths; public: CGameExporter(const TString& rkInputDir, const TString& rkOutputDir); @@ -77,12 +78,7 @@ protected: inline void SetResourcePath(const CAssetID& rkID, const TWideString& rkDir, const TWideString& rkName) { - SetResourcePath(rkID.ToLongLong(), rkDir, rkName); - } - - inline void SetResourcePath(u64 ID, const TWideString& rkDir, const TWideString& rkName) - { - mResourcePaths[ID] = SResourcePath { rkDir, rkName }; + mResourcePaths[rkID] = SResourcePath { rkDir, rkName }; } inline EGame Game() const { return mpProject->Game(); } diff --git a/src/Core/GameProject/CPackage.cpp b/src/Core/GameProject/CPackage.cpp index 5b8bf654..c952fc00 100644 --- a/src/Core/GameProject/CPackage.cpp +++ b/src/Core/GameProject/CPackage.cpp @@ -1,6 +1,10 @@ #include "CPackage.h" +#include "DependencyListBuilders.h" #include "CGameProject.h" +#include "Core/Resource/Cooker/CWorldCooker.h" +#include #include +#include #include #include @@ -106,6 +110,244 @@ void CPackage::Save() Log::Error("Failed to save pak definition at path: " + DefPath.ToUTF8()); } +void CPackage::Cook() +{ + // Build asset list + CPackageDependencyListBuilder Builder(this); + std::list AssetList; + Builder.BuildDependencyList(true, AssetList); + Log::Write(TString::FromInt32(AssetList.size(), 0, 10) + " assets in pak"); + + // Get named resources + std::list NamedResources; + + for (u32 iCol = 0; iCol < mCollections.size(); iCol++) + { + CResourceCollection *pCol = mCollections[iCol]; + + for (u32 iRes = 0; iRes < pCol->NumResources(); iRes++) + NamedResources.push_back(&pCol->ResourceByIndex(iRes)); + } + + // Write new pak + TWideString PakPath = CookedPackagePath(false); + CFileOutStream Pak(PakPath.ToUTF8().ToStdString(), IOUtil::eBigEndian); + + if (!Pak.IsValid()) + { + Log::Error("Couldn't cook package " + CookedPackagePath(true).ToUTF8() + "; unable to open package for writing"); + return; + } + + // todo: MP3/DKCR pak format support + Pak.WriteLong(0x00030005); // Major/Minor Version + Pak.WriteLong(0); // Unknown + + // Named Resources + Pak.WriteLong(NamedResources.size()); + + for (auto Iter = NamedResources.begin(); Iter != NamedResources.end(); Iter++) + { + const SNamedResource *pkRes = *Iter; + pkRes->Type.Write(Pak); + pkRes->ID.Write(Pak); + Pak.WriteLong(pkRes->Name.Size()); + Pak.WriteString(pkRes->Name.ToStdString(), pkRes->Name.Size()); // Note: Explicitly specifying size means we don't write the terminating 0 + + // TEMP: recook world + if (pkRes->Type == "MLVL") + { + CResourceEntry *pEntry = gpResourceStore->FindEntry(pkRes->ID); + ASSERT(pEntry); + CWorld *pWorld = (CWorld*) pEntry->Load(); + ASSERT(pWorld); + CFileOutStream MLVL(pEntry->CookedAssetPath().ToStdString(), IOUtil::eBigEndian); + ASSERT(MLVL.IsValid()); + CWorldCooker::CookMLVL(pWorld, MLVL); + } + } + + // Fill in table of contents with junk, write later + Pak.WriteLong(AssetList.size()); + u32 TocOffset = Pak.Tell(); + + for (u32 iRes = 0; iRes < AssetList.size(); iRes++) + { + Pak.WriteLongLong(0); + Pak.WriteLongLong(0); + Pak.WriteLong(0); + } + + Pak.WriteToBoundary(32, 0); + + // Start writing resources + struct SResourceTocInfo + { + CResourceEntry *pEntry; + u32 Offset; + u32 Size; + bool Compressed; + }; + std::vector ResourceTocData(AssetList.size()); + u32 ResIdx = 0; + + for (auto Iter = AssetList.begin(); Iter != AssetList.end(); Iter++, ResIdx++) + { + CAssetID ID = *Iter; + CResourceEntry *pEntry = gpResourceStore->FindEntry(ID); + ASSERT(pEntry != nullptr); + + SResourceTocInfo& rTocInfo = ResourceTocData[ResIdx]; + rTocInfo.pEntry = pEntry; + rTocInfo.Offset = Pak.Tell(); + + // Load resource data + CFileInStream CookedAsset(pEntry->CookedAssetPath().ToStdString(), IOUtil::eBigEndian); + ASSERT(CookedAsset.IsValid()); + u32 ResourceSize = CookedAsset.Size(); + + std::vector ResourceData(ResourceSize); + CookedAsset.ReadBytes(ResourceData.data(), ResourceData.size()); + + // 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(); + + bool ShouldAlwaysCompress = (Type == eTexture || Type == eModel || Type == eSkin || + Type == eAnimSet || Type == eAnimation || Type == eFont); + + bool ShouldCompressConditional = !ShouldAlwaysCompress && + (Type == eParticle || Type == eParticleElectric || Type == eParticleSwoosh || + Type == eParticleWeapon || Type == eParticleDecal || Type == eParticleCollisionResponse); + + bool ShouldCompress = ShouldAlwaysCompress || (ShouldCompressConditional && ResourceSize >= 0x400); + + // Write resource data to pak + if (!ShouldCompress) + { + Pak.WriteBytes(ResourceData.data(), ResourceSize); + rTocInfo.Compressed = false; + } + + else + { + u32 CompressedSize; + std::vector CompressedData(ResourceData.size() * 2); + bool Success = CompressionUtil::CompressZlib(ResourceData.data(), ResourceData.size(), CompressedData.data(), CompressedData.size(), CompressedSize); + + // 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; + Success = (PaddedCompressedSize < PaddedUncompressedSize); + } + + // Write file to pak + if (Success) + { + Pak.WriteLong(ResourceSize); + Pak.WriteBytes(CompressedData.data(), CompressedSize); + } + else + Pak.WriteBytes(ResourceData.data(), ResourceSize); + + rTocInfo.Compressed = Success; + } + + Pak.WriteToBoundary(32, 0xFF); + rTocInfo.Size = Pak.Tell() - rTocInfo.Offset; + } + + // Write table of contents for real + Pak.Seek(TocOffset, SEEK_SET); + + for (u32 iRes = 0; iRes < AssetList.size(); iRes++) + { + const SResourceTocInfo& rkTocInfo = ResourceTocData[iRes]; + CResourceEntry *pEntry = rkTocInfo.pEntry; + + Pak.WriteLong( rkTocInfo.Compressed ? 1 : 0 ); + pEntry->CookedExtension().Write(Pak); + pEntry->ID().Write(Pak); + Pak.WriteLong(rkTocInfo.Size); + Pak.WriteLong(rkTocInfo.Offset); + } + + Log::Write("Finished writing " + PakPath.ToUTF8()); +} + +void CPackage::CompareOriginalAssetList(const std::list& rkNewList) +{ + // Debug - take the newly generated rkNewList and compare it with the asset list + // from the original pak, and print info about any extra or missing resources + // Build a set out of the generated list + std::set NewListSet; + + for (auto Iter = rkNewList.begin(); Iter != rkNewList.end(); Iter++) + NewListSet.insert(*Iter); + + // Read the original pak + TWideString CookedPath = CookedPackagePath(false); + CFileInStream Pak(CookedPath.ToUTF8().ToStdString(), IOUtil::eBigEndian); + + if (!Pak.IsValid()) + { + Log::Error("Failed to compare to original asset list; couldn't open the original pak"); + return; + } + + // Skip past header + named resources + u32 PakVersion = Pak.ReadLong(); + ASSERT(PakVersion == 0x00030005); + Pak.Seek(0x4, SEEK_CUR); + u32 NumNamedResources = Pak.ReadLong(); + + for (u32 iName = 0; iName < NumNamedResources; iName++) + { + Pak.Seek(0x8, SEEK_CUR); + u32 NameLen = Pak.ReadLong(); + Pak.Seek(NameLen, SEEK_CUR); + } + + // Build a set out of the original pak resource list + u32 NumResources = Pak.ReadLong(); + std::set OldListSet; + + for (u32 iRes = 0; iRes < NumResources; iRes++) + { + Pak.Seek(0x8, SEEK_CUR); + OldListSet.insert( CAssetID(Pak, e32Bit) ); + Pak.Seek(0x8, SEEK_CUR); + } + + // Check for missing resources in the new list + for (auto Iter = OldListSet.begin(); Iter != OldListSet.end(); Iter++) + { + CAssetID ID = *Iter; + + if (NewListSet.find(ID) == NewListSet.end()) + { + CResourceEntry *pEntry = gpResourceStore->FindEntry(ID); + TString Extension = (pEntry ? "." + GetResourceCookedExtension(pEntry->ResourceType(), pEntry->Game()) : ""); + Log::Error("Missing resource: " + ID.ToString() + Extension); + } + } + + // Check for extra resources in the new list + for (auto Iter = NewListSet.begin(); Iter != NewListSet.end(); Iter++) + { + CAssetID ID = *Iter; + + if (OldListSet.find(ID) == OldListSet.end()) + { + CResourceEntry *pEntry = gpResourceStore->FindEntry(ID); + TString Extension = (pEntry ? "." + GetResourceCookedExtension(pEntry->ResourceType(), pEntry->Game()) : ""); + Log::Error("Extra resource: " + ID.ToString() + Extension); + } + } +} + TWideString CPackage::DefinitionPath(bool Relative) const { return mpProject->PackagesDir(Relative) + mPakPath + mPakName.ToUTF16() + L".pkd"; diff --git a/src/Core/GameProject/CPackage.h b/src/Core/GameProject/CPackage.h index 8705ba46..2914e731 100644 --- a/src/Core/GameProject/CPackage.h +++ b/src/Core/GameProject/CPackage.h @@ -60,6 +60,9 @@ public: void Load(); void Save(); + void Cook(); + void CompareOriginalAssetList(const std::list& rkNewList); + TWideString DefinitionPath(bool Relative) const; TWideString CookedPackagePath(bool Relative) const; @@ -70,6 +73,7 @@ public: // Accessors inline TString Name() const { return mPakName; } inline TWideString Path() const { return mPakPath; } + inline CGameProject* Project() const { return mpProject; } inline u32 NumCollections() const { return mCollections.size(); } inline CResourceCollection* CollectionByIndex(u32 Idx) const { return mCollections[Idx]; } diff --git a/src/Core/GameProject/CResourceEntry.cpp b/src/Core/GameProject/CResourceEntry.cpp index 357c03d1..58240529 100644 --- a/src/Core/GameProject/CResourceEntry.cpp +++ b/src/Core/GameProject/CResourceEntry.cpp @@ -54,6 +54,7 @@ bool CResourceEntry::LoadCacheData() // Dependency Tree u32 DepsTreeSize = File.ReadLong(); + u32 DepsTreeStart = File.Tell(); if (mpDependencies) { @@ -63,8 +64,12 @@ bool CResourceEntry::LoadCacheData() if (DepsTreeSize > 0) { - mpDependencies = new CDependencyTree(mID); + if (mType == eArea) mpDependencies = new CAreaDependencyTree(mID); + else if (mType == eAnimSet) mpDependencies = new CAnimSetDependencyTree(mID); + else mpDependencies = new CDependencyTree(mID); + mpDependencies->Read(File, Game() <= eEchoes ? e32Bit : e64Bit); + ASSERT(File.Tell() - DepsTreeStart == DepsTreeSize); } return true; @@ -166,6 +171,11 @@ TString CResourceEntry::CookedAssetPath(bool Relative) const return ((IsTransient() || Relative) ? Path + Name : mpStore->ActiveProject()->CookedDir(false) + Path + Name); } +CFourCC CResourceEntry::CookedExtension() const +{ + return CFourCC( GetResourceCookedExtension(mType, mGame) ); +} + bool CResourceEntry::IsInDirectory(CVirtualDirectory *pDir) const { CVirtualDirectory *pParentDir = mpDirectory; diff --git a/src/Core/GameProject/CResourceEntry.h b/src/Core/GameProject/CResourceEntry.h index f58ea318..294cfab5 100644 --- a/src/Core/GameProject/CResourceEntry.h +++ b/src/Core/GameProject/CResourceEntry.h @@ -4,6 +4,7 @@ #include "CVirtualDirectory.h" #include "Core/Resource/EResType.h" #include +#include #include #include @@ -52,6 +53,7 @@ public: bool HasCookedVersion() const; TString RawAssetPath(bool Relative = false) const; TString CookedAssetPath(bool Relative = false) const; + CFourCC CookedExtension() const; bool IsInDirectory(CVirtualDirectory *pDir) const; u64 Size() const; bool NeedsRecook() const; diff --git a/src/Core/GameProject/DependencyListBuilders.h b/src/Core/GameProject/DependencyListBuilders.h new file mode 100644 index 00000000..f506ca52 --- /dev/null +++ b/src/Core/GameProject/DependencyListBuilders.h @@ -0,0 +1,264 @@ +#ifndef DEPENDENCYLISTBUILDERS +#define DEPENDENCYLISTBUILDERS + +#include "CDependencyTree.h" +#include "CPackage.h" +#include "CResourceEntry.h" +#include "Core/Resource/CDependencyGroup.h" +#include "Core/Resource/CWorld.h" + +// ************ CPackageDependencyListBuilder ************ +class CPackageDependencyListBuilder +{ + CPackage *mpPackage; + TResPtr mpWorld; + std::set mPackageUsedAssets; + std::set mAreaUsedAssets; + std::list mScanIDs; + bool mEnableDuplicates; + bool mCurrentAreaHasDuplicates; + bool mAddingScans; + +public: + CPackageDependencyListBuilder(CPackage *pPackage) + : mpPackage(pPackage) + , mCurrentAreaHasDuplicates(false) + , mAddingScans(false) + {} + + virtual void BuildDependencyList(bool AllowDuplicates, std::list& rOut) + { + mEnableDuplicates = AllowDuplicates; + + for (u32 iCol = 0; iCol < mpPackage->NumCollections(); iCol++) + { + CResourceCollection *pCollection = mpPackage->CollectionByIndex(iCol); + + for (u32 iRes = 0; iRes < pCollection->NumResources(); iRes++) + { + const SNamedResource& rkRes = pCollection->ResourceByIndex(iRes); + CResourceEntry *pEntry = gpResourceStore->FindEntry(rkRes.ID); + + if (pEntry) + { + if (rkRes.Name.EndsWith("NODEPEND")) + rOut.push_back(rkRes.ID); + + else + { + mScanIDs.clear(); + mAddingScans = false; + + if (rkRes.Type == "MLVL") + { + CResourceEntry *pWorldEntry = gpResourceStore->FindEntry(rkRes.ID); + ASSERT(pWorldEntry); + mpWorld = (CWorld*) pWorldEntry->Load(); + ASSERT(mpWorld); + } + + EvaluateDependencies(pEntry, rOut); + + mAddingScans = true; + for (auto Iter = mScanIDs.begin(); Iter != mScanIDs.end(); Iter++) + AddDependency(pEntry, *Iter, rOut); + } + } + } + } + } + + inline void AddDependency(CResourceEntry *pCurEntry, const CAssetID& rkID, std::list& rOut) + { + if (pCurEntry->ResourceType() == eDependencyGroup) return; + CResourceEntry *pEntry = gpResourceStore->FindEntry(rkID); + EResType Type = (pEntry ? pEntry->ResourceType() : eResource); + + // Defer scans to the end of the list. This is to accomodate the way the game loads SCANs; they are + // loaded separately from everything else after the area itself is done loading. + if (Type == eScan && !mAddingScans) + { + mScanIDs.push_back(rkID); + return; + } + + // Make sure entry exists + is valid + if (pEntry && Type != eMidi && Type != eAudioGroupSet && Type != eWorld && (Type != eArea || pCurEntry->ResourceType() == eWorld)) + { + if ( ( mCurrentAreaHasDuplicates && mAreaUsedAssets.find(rkID) == mAreaUsedAssets.end()) || + (!mCurrentAreaHasDuplicates && mPackageUsedAssets.find(rkID) == mPackageUsedAssets.end()) ) + EvaluateDependencies(pEntry, rOut); + } + } + + void EvaluateDependencies(CResourceEntry *pEntry, std::list& rOut) + { + // Toggle duplicates + if (pEntry->ResourceType() == eArea && mEnableDuplicates) + { + mAreaUsedAssets.clear(); + + for (u32 iArea = 0; iArea < mpWorld->NumAreas(); iArea++) + { + if (mpWorld->AreaResourceID(iArea) == pEntry->ID()) + { + mCurrentAreaHasDuplicates = mpWorld->DoesAreaAllowPakDuplicates(iArea); + break; + } + } + } + + // Add dependencies + CDependencyTree *pTree = pEntry->Dependencies(); + mPackageUsedAssets.insert(pTree->ID()); + mAreaUsedAssets.insert(pTree->ID()); + + for (u32 iDep = 0; iDep < pTree->NumDependencies(); iDep++) + { + CAssetID ID = pTree->DependencyByIndex(iDep); + AddDependency(pEntry, ID, rOut); + } + + // Add area script dependencies + if (pEntry->ResourceType() == eArea) + { + CAreaDependencyTree *pAreaTree = static_cast(pTree); + + for (u32 iInst = 0; iInst < pAreaTree->NumScriptInstances(); iInst++) + { + CScriptInstanceDependencyTree *pInstTree = pAreaTree->ScriptInstanceByIndex(iInst); + + for (u32 iDep = 0; iDep < pInstTree->NumDependencies(); iDep++) + { + CAssetID ID = pInstTree->DependencyByIndex(iDep); + AddDependency(pEntry, ID, rOut); + } + } + } + + rOut.push_back(pTree->ID()); + } +}; + +// ************ CAreaDependencyListBuilder ************ +class CAreaDependencyListBuilder +{ + CResourceEntry *mpAreaEntry; + std::set mBaseUsedAssets; + std::set mLayerUsedAssets; + bool mIsPlayerActor; + +public: + CAreaDependencyListBuilder(CResourceEntry *pAreaEntry) + : mpAreaEntry(pAreaEntry) {} + + virtual void BuildDependencyList(std::list& rAssetsOut, std::list& rLayerOffsetsOut) + { + ASSERT(mpAreaEntry->ResourceType() == eArea); + CAreaDependencyTree *pTree = static_cast(mpAreaEntry->Dependencies()); + + // Fill area base used assets set (don't actually add to list yet) + for (u32 iDep = 0; iDep < pTree->NumDependencies(); iDep++) + mBaseUsedAssets.insert(pTree->DependencyByIndex(iDep)); + + // Get dependencies of each layer + for (u32 iLyr = 0; iLyr < pTree->NumScriptLayers(); iLyr++) + { + mLayerUsedAssets.clear(); + rLayerOffsetsOut.push_back(rAssetsOut.size()); + + bool IsLastLayer = (iLyr == pTree->NumScriptLayers() - 1); + u32 StartIndex = pTree->ScriptLayerOffset(iLyr); + u32 EndIndex = (IsLastLayer ? pTree->NumScriptInstances() : pTree->ScriptLayerOffset(iLyr + 1)); + + for (u32 iInst = StartIndex; iInst < EndIndex; iInst++) + { + CScriptInstanceDependencyTree *pInstTree = pTree->ScriptInstanceByIndex(iInst); + mIsPlayerActor = (pInstTree->ObjectType() == 0x4C); + + for (u32 iDep = 0; iDep < pInstTree->NumDependencies(); iDep++) + { + CAssetID ID = pInstTree->DependencyByIndex(iDep); + AddDependency(mpAreaEntry, ID, rAssetsOut); + } + } + } + + // Add base dependencies + mBaseUsedAssets.clear(); + mLayerUsedAssets.clear(); + rLayerOffsetsOut.push_back(rAssetsOut.size()); + + for (u32 iDep = 0; iDep < pTree->NumDependencies(); iDep++) + { + CAssetID ID = pTree->DependencyByIndex(iDep); + AddDependency(mpAreaEntry, ID, rAssetsOut); + } + } + + void AddDependency(CResourceEntry *pCurEntry, const CAssetID& rkID, std::list& rOut) + { + if (pCurEntry->ResourceType() == eDependencyGroup) return; + CResourceEntry *pEntry = gpResourceStore->FindEntry(rkID); + EResType Type = (pEntry ? pEntry->ResourceType() : eResource); + + // Make sure entry exists + is valid + if (pEntry && Type != eMidi && Type != eAudioGroupSet && Type != eScan && Type != eWorld && Type != eArea) + { + if ( mBaseUsedAssets.find(rkID) == mBaseUsedAssets.end() && mLayerUsedAssets.find(rkID) == mLayerUsedAssets.end()) + { + if (mIsPlayerActor && pEntry->ResourceType() == eAnimSet) + EvaluatePlayerActorAnimSet(pEntry, rOut); + else + EvaluateDependencies(pEntry, rOut); + } + } + } + + void EvaluateDependencies(CResourceEntry *pEntry, std::list& rOut) + { + CDependencyTree *pTree = pEntry->Dependencies(); + mLayerUsedAssets.insert(pTree->ID()); + + for (u32 iDep = 0; iDep < pTree->NumDependencies(); iDep++) + { + CAssetID ID = pTree->DependencyByIndex(iDep); + AddDependency(pEntry, ID, rOut); + } + + rOut.push_back(pTree->ID()); + } + + void EvaluatePlayerActorAnimSet(CResourceEntry *pEntry, std::list& rOut) + { + // For PlayerActor animsets we want to include only the empty suit (char 5) in the dependency list. This is to + // accomodate the dynamic loading the game does for PlayerActors to avoid having assets for suits the player + // doesn't have in memory. We want common assets (animations, etc) in the list but not per-character assets. + ASSERT(pEntry->ResourceType() == eAnimSet); + CAnimSetDependencyTree *pTree = static_cast(pEntry->Dependencies()); + mLayerUsedAssets.insert(pTree->ID()); + + // Add empty suit dependencies + ASSERT(pTree->NumCharacters() >= 7); + u32 StartIdx = pTree->CharacterOffset(5); + u32 EndIdx = pTree->CharacterOffset(6); + + for (u32 iDep = StartIdx; iDep < EndIdx; iDep++) + { + CAssetID ID = pTree->DependencyByIndex(iDep); + AddDependency(pEntry, ID, rOut); + } + + // Add common dependencies + for (u32 iDep = 0; iDep < pTree->CharacterOffset(0); iDep++) + { + CAssetID ID = pTree->DependencyByIndex(iDep); + AddDependency(pEntry, ID, rOut); + } + + rOut.push_back(pTree->ID()); + } +}; + +#endif // DEPENDENCYLISTBUILDERS + diff --git a/src/Core/Resource/Area/CGameArea.cpp b/src/Core/Resource/Area/CGameArea.cpp index 0a7a033f..c28d79a9 100644 --- a/src/Core/Resource/Area/CGameArea.cpp +++ b/src/Core/Resource/Area/CGameArea.cpp @@ -36,14 +36,11 @@ CDependencyTree* CGameArea::BuildDependencyTree() const // Base dependencies CAreaDependencyTree *pTree = new CAreaDependencyTree(ID()); - for (u32 iMat = 0; iMat < mpMaterialSet->NumMaterials(); iMat++) - { - CMaterial *pMat = mpMaterialSet->MaterialByIndex(iMat); - pTree->AddDependency(pMat->IndTexture()); + std::set MatTextures; + mpMaterialSet->GetUsedTextureIDs(MatTextures); - for (u32 iPass = 0; iPass < pMat->PassCount(); iPass++) - pTree->AddDependency(pMat->Pass(iPass)->Texture()); - } + for (auto Iter = MatTextures.begin(); Iter != MatTextures.end(); Iter++) + pTree->AddDependency(*Iter); pTree->AddDependency(mPathID); pTree->AddDependency(mPortalAreaID); diff --git a/src/Core/Resource/CAnimSet.h b/src/Core/Resource/CAnimSet.h index 23ce8ee3..309312d8 100644 --- a/src/Core/Resource/CAnimSet.h +++ b/src/Core/Resource/CAnimSet.h @@ -58,12 +58,23 @@ public: CDependencyTree* BuildDependencyTree() const { CAnimSetDependencyTree *pTree = new CAnimSetDependencyTree(ID()); + std::set BaseUsedSet; + // Base dependencies for (u32 iAnim = 0; iAnim < mAnims.size(); iAnim++) - pTree->AddDependency(mAnims[iAnim].pAnim); + { + CAnimation *pAnim = mAnims[iAnim].pAnim; + if (pAnim) + { + pTree->AddDependency(mAnims[iAnim].pAnim); + BaseUsedSet.insert(pAnim->ID()); + } + } + + // Character dependencies for (u32 iNode = 0; iNode < mCharacters.size(); iNode++) - pTree->AddCharacter(&mCharacters[iNode]); + pTree->AddCharacter(&mCharacters[iNode], BaseUsedSet); return pTree; } diff --git a/src/Core/Resource/CMaterialSet.h b/src/Core/Resource/CMaterialSet.h index 84250b75..057ea3ca 100644 --- a/src/Core/Resource/CMaterialSet.h +++ b/src/Core/Resource/CMaterialSet.h @@ -59,6 +59,21 @@ public: return -1; } + + void GetUsedTextureIDs(std::set& rOut) + { + for (u32 iMat = 0; iMat < mMaterials.size(); iMat++) + { + CMaterial *pMat = mMaterials[iMat]; + if (pMat->IndTexture()) rOut.insert(pMat->IndTexture()->ID()); + + for (u32 iPass = 0; iPass < pMat->PassCount(); iPass++) + { + CTexture *pTex = pMat->Pass(iPass)->Texture(); + if (pTex) rOut.insert(pTex->ID()); + } + } + } }; #endif // CMATERIALSET_H diff --git a/src/Core/Resource/CScan.h b/src/Core/Resource/CScan.h index 55b7918b..173942af 100644 --- a/src/Core/Resource/CScan.h +++ b/src/Core/Resource/CScan.h @@ -24,16 +24,16 @@ public: private: EGame mVersion; - TResPtr mpFrame; + CAssetID mFrameID; TResPtr mpStringTable; bool mIsSlow; bool mIsImportant; ELogbookCategory mCategory; + CAssetID mScanImageTextures[4]; public: CScan(CResourceEntry *pEntry = 0) : CResource(pEntry) - , mpFrame(nullptr) , mpStringTable(nullptr) , mIsSlow(false) , mIsImportant(false) @@ -46,8 +46,12 @@ public: Log::Warning("CScan::BuildDependencyTree not handling Echoes/Corruption dependencies"); CDependencyTree *pTree = new CDependencyTree(ID()); - pTree->AddDependency(mpFrame); + pTree->AddDependency(mFrameID); pTree->AddDependency(mpStringTable); + + for (u32 iImg = 0; iImg < 4; iImg++) + pTree->AddDependency(mScanImageTextures[iImg]); + return pTree; } diff --git a/src/Core/Resource/CStringTable.h b/src/Core/Resource/CStringTable.h index 8cdbf4fa..75af8a07 100644 --- a/src/Core/Resource/CStringTable.h +++ b/src/Core/Resource/CStringTable.h @@ -44,7 +44,7 @@ public: CDependencyTree* BuildDependencyTree() const { - // The only dependencies STRGs have is they can reference FONTs with the &font=; formatting tag + // STRGs can reference FONTs with the &font=; formatting tag and TXTRs with the &image=; tag CDependencyTree *pTree = new CDependencyTree(ID()); EIDLength IDLength = (Game() <= eEchoes ? e32Bit : e64Bit); @@ -54,14 +54,69 @@ public: for (u32 iStr = 0; iStr < rkTable.Strings.size(); iStr++) { - static const TWideString skTag = L"&font="; const TWideString& rkStr = rkTable.Strings[iStr]; - for (u32 FontIdx = rkStr.IndexOfPhrase(*skTag); FontIdx != -1; FontIdx = rkStr.IndexOfPhrase(*skTag, FontIdx + 1)) + for (u32 TagIdx = rkStr.IndexOf(L'&'); TagIdx != -1; TagIdx = rkStr.IndexOf(L'&', TagIdx + 1)) { - u32 IDStart = FontIdx + skTag.Size(); - TWideString StrFontID = rkStr.SubString(IDStart, IDLength * 2); - pTree->AddDependency( CAssetID::FromString(StrFontID) ); + // Check for double ampersand (escape character in DKCR, not sure about other games) + if (rkStr.At(TagIdx + 1) == L'&') + { + TagIdx++; + continue; + } + + // Get tag name and parameters + u32 NameEnd = rkStr.IndexOf(L'=', TagIdx); + u32 TagEnd = rkStr.IndexOf(L';', TagIdx); + if (NameEnd == -1 || TagEnd == -1) continue; + + TWideString TagName = rkStr.SubString(TagIdx + 1, NameEnd - TagIdx - 1); + TWideString ParamString = rkStr.SubString(NameEnd + 1, TagEnd - NameEnd - 1); + + // Font + if (TagName == L"font") + { + ASSERT(ParamString.Size() == IDLength * 2); + pTree->AddDependency( CAssetID::FromString(ParamString) ); + } + + // Image + else if (TagName == L"image") + { + // Determine which params are textures based on image type + TWideStringList Params = ParamString.Split(L","); + TWideString ImageType = Params.front(); + u32 TexturesStart = -1; + + if (ImageType == L"A") + TexturesStart = 2; + + else if (ImageType == L"SI") + TexturesStart = 3; + + else if (ImageType == L"SA") + TexturesStart = 4; + + else + { + Log::Error("Unrecognized image type: " + ImageType.ToUTF8()); + DEBUG_BREAK; + continue; + } + + // Load texture IDs + TWideStringList::iterator Iter = Params.begin(); + + for (u32 iParam = 0; iParam < Params.size(); iParam++, Iter++) + { + if (iParam >= TexturesStart) + { + TWideString Param = *Iter; + ASSERT(Param.Size() == IDLength * 2); + pTree->AddDependency( CAssetID::FromString(Param) ); + } + } + } } } } diff --git a/src/Core/Resource/CWorld.cpp b/src/Core/Resource/CWorld.cpp index 7ff7a1a3..727bf29a 100644 --- a/src/Core/Resource/CWorld.cpp +++ b/src/Core/Resource/CWorld.cpp @@ -92,6 +92,7 @@ void Serialize(IArchive& rArc, CWorld::SArea& rArea) << SERIAL("BoundingBox", rArea.AetherBox) << SERIAL("AreaMREA", rArea.AreaResID) << SERIAL_HEX("AreaID", rArea.AreaID) + << SERIAL("AllowPakDuplicates", rArea.AllowPakDuplicates) << SERIAL_CONTAINER("AttachedAreas", rArea.AttachedAreaIDs, "AreaIndex") << SERIAL_CONTAINER("Dependencies", rArea.Dependencies, "Dependency") << SERIAL_CONTAINER("RelModules", rArea.RelFilenames, "Module") diff --git a/src/Core/Resource/CWorld.h b/src/Core/Resource/CWorld.h index 7ce2d88a..5051cf52 100644 --- a/src/Core/Resource/CWorld.h +++ b/src/Core/Resource/CWorld.h @@ -11,6 +11,7 @@ class CWorld : public CResource { DECLARE_RESOURCE_TYPE(eWorld) friend class CWorldLoader; + friend class CWorldCooker; // Instances of CResource pointers are placeholders for unimplemented resource types (eg CMapWorld) EGame mWorldVersion; @@ -48,6 +49,7 @@ class CWorld : public CResource CAABox AetherBox; CAssetID AreaResID; // Loading every single area as a CResource would be a very bad idea u64 AreaID; + bool AllowPakDuplicates; std::vector AttachedAreaIDs; std::vector Dependencies; @@ -103,11 +105,14 @@ public: inline CResource* MapWorld() const { return mpMapWorld; } inline u32 NumAreas() const { return mAreas.size(); } - inline u64 AreaResourceID(u32 AreaIndex) const { return mAreas[AreaIndex].AreaResID.ToLongLong(); } + inline CAssetID AreaResourceID(u32 AreaIndex) const { return mAreas[AreaIndex].AreaResID; } inline u32 AreaAttachedCount(u32 AreaIndex) const { return mAreas[AreaIndex].AttachedAreaIDs.size(); } inline u32 AreaAttachedID(u32 AreaIndex, u32 AttachedIndex) const { return mAreas[AreaIndex].AttachedAreaIDs[AttachedIndex]; } inline TString AreaInternalName(u32 AreaIndex) const { return mAreas[AreaIndex].InternalName; } inline CStringTable* AreaName(u32 AreaIndex) const { return mAreas[AreaIndex].pAreaName; } + inline bool DoesAreaAllowPakDuplicates(u32 AreaIndex) const { return mAreas[AreaIndex].AllowPakDuplicates; } + + inline void SetAreaAllowsPakDuplicates(u32 AreaIndex, bool Allow) { mAreas[AreaIndex].AllowPakDuplicates = Allow; } }; #endif // CWORLD_H diff --git a/src/Core/Resource/Cooker/CPoiToWorldCooker.cpp b/src/Core/Resource/Cooker/CPoiToWorldCooker.cpp index 92222e7d..86911a03 100644 --- a/src/Core/Resource/Cooker/CPoiToWorldCooker.cpp +++ b/src/Core/Resource/Cooker/CPoiToWorldCooker.cpp @@ -31,6 +31,4 @@ void CPoiToWorldCooker::WriteEGMC(CPoiToWorld *pPoiToWorld, IOutputStream& rOut) rOut.WriteLong(Mappings[iMap].MeshID); rOut.WriteLong(Mappings[iMap].PoiID); } - - rOut.WriteToBoundary(32, -1); } diff --git a/src/Core/Resource/Cooker/CWorldCooker.cpp b/src/Core/Resource/Cooker/CWorldCooker.cpp index a462bb0e..684bfd68 100644 --- a/src/Core/Resource/Cooker/CWorldCooker.cpp +++ b/src/Core/Resource/Cooker/CWorldCooker.cpp @@ -1,9 +1,162 @@ #include "CWorldCooker.h" +#include "Core/GameProject/DependencyListBuilders.h" CWorldCooker::CWorldCooker() { } +// ************ STATIC ************ +bool CWorldCooker::CookMLVL(CWorld *pWorld, IOutputStream& rMLVL) +{ + ASSERT(rMLVL.IsValid()); + + // MLVL Header + rMLVL.WriteLong(0xDEAFBABE); + rMLVL.WriteLong( GetMLVLVersion(pWorld->Game()) ); + + CAssetID WorldNameID = pWorld->mpWorldName ? pWorld->mpWorldName->ID() : CAssetID::skInvalidID32; + CAssetID SaveWorldID = pWorld->mpSaveWorld ? pWorld->mpSaveWorld->ID() : CAssetID::skInvalidID32; + CAssetID DefaultSkyID = pWorld->mpDefaultSkybox ? pWorld->mpDefaultSkybox->ID() : CAssetID::skInvalidID32; + + WorldNameID.Write(rMLVL); + SaveWorldID.Write(rMLVL); + DefaultSkyID.Write(rMLVL); + + // Memory Relays + rMLVL.WriteLong( pWorld->mMemoryRelays.size() ); + + for (u32 iMem = 0; iMem < pWorld->mMemoryRelays.size(); iMem++) + { + CWorld::SMemoryRelay& rRelay = pWorld->mMemoryRelays[iMem]; + rMLVL.WriteLong(rRelay.InstanceID); + rMLVL.WriteLong(rRelay.TargetID); + rMLVL.WriteShort(rRelay.Message); + rMLVL.WriteByte(rRelay.Unknown); + } + + // Areas + rMLVL.WriteLong(pWorld->mAreas.size()); + rMLVL.WriteLong(1); // Unknown + + for (u32 iArea = 0; iArea < pWorld->mAreas.size(); iArea++) + { + // Area Header + CWorld::SArea& rArea = pWorld->mAreas[iArea]; + CResourceEntry *pAreaEntry = gpResourceStore->FindEntry(rArea.AreaResID); + ASSERT(pAreaEntry && pAreaEntry->ResourceType() == eArea); + + CAssetID AreaNameID = rArea.pAreaName ? rArea.pAreaName->ID() : CAssetID::skInvalidID32; + AreaNameID.Write(rMLVL); + rArea.Transform.Write(rMLVL); + rArea.AetherBox.Write(rMLVL); + rArea.AreaResID.Write(rMLVL); + rMLVL.WriteLong( (u32) rArea.AreaID ); + + // Attached Areas + rMLVL.WriteLong( rArea.AttachedAreaIDs.size() ); + + for (u32 iAttach = 0; iAttach < rArea.AttachedAreaIDs.size(); iAttach++) + rMLVL.WriteShort(rArea.AttachedAreaIDs[iAttach]); + + // Dependencies + std::list Dependencies; + std::list LayerDependsOffsets; + CAreaDependencyListBuilder Builder(pAreaEntry); + Builder.BuildDependencyList(Dependencies, LayerDependsOffsets); + + rMLVL.WriteLong(0); + rMLVL.WriteLong( Dependencies.size() ); + + for (auto Iter = Dependencies.begin(); Iter != Dependencies.end(); Iter++) + { + CAssetID ID = *Iter; + CResourceEntry *pEntry = gpResourceStore->FindEntry(ID); + ID.Write(rMLVL); + pEntry->CookedExtension().Write(rMLVL); + } + + rMLVL.WriteLong(LayerDependsOffsets.size()); + + for (auto Iter = LayerDependsOffsets.begin(); Iter != LayerDependsOffsets.end(); Iter++) + rMLVL.WriteLong(*Iter); + + // Docks + rMLVL.WriteLong( rArea.Docks.size() ); + + for (u32 iDock = 0; iDock < rArea.Docks.size(); iDock++) + { + CWorld::SArea::SDock& rDock = rArea.Docks[iDock]; + rMLVL.WriteLong( rDock.ConnectingDocks.size() ); + + for (u32 iCon = 0; iCon < rDock.ConnectingDocks.size(); iCon++) + { + CWorld::SArea::SDock::SConnectingDock& rConDock = rDock.ConnectingDocks[iCon]; + rMLVL.WriteLong(rConDock.AreaIndex); + rMLVL.WriteLong(rConDock.DockIndex); + } + + rMLVL.WriteLong( rDock.DockCoordinates.size() ); + + for (u32 iCoord = 0; iCoord < rDock.DockCoordinates.size(); iCoord++) + rDock.DockCoordinates[iCoord].Write(rMLVL); + } + } + + CAssetID MapWorldID = pWorld->mpMapWorld ? pWorld->mpMapWorld->ID() : CAssetID::skInvalidID32; + MapWorldID.Write(rMLVL); + rMLVL.WriteByte(0); + rMLVL.WriteLong(0); + + // Audio Groups + rMLVL.WriteLong(pWorld->mAudioGrps.size()); + + for (u32 iGrp = 0; iGrp < pWorld->mAudioGrps.size(); iGrp++) + { + CWorld::SAudioGrp& rAudioGroup = pWorld->mAudioGrps[iGrp]; + rMLVL.WriteLong(rAudioGroup.Unknown); + rAudioGroup.ResID.Write(rMLVL); + } + + rMLVL.WriteByte(0); + + // Layers + rMLVL.WriteLong(pWorld->mAreas.size()); + std::vector LayerNames; + std::vector LayerNameOffsets; + + for (u32 iArea = 0; iArea < pWorld->mAreas.size(); iArea++) + { + CWorld::SArea& rArea = pWorld->mAreas[iArea]; + LayerNameOffsets.push_back(LayerNames.size()); + rMLVL.WriteLong(rArea.Layers.size()); + + u64 LayerActiveFlags = -1; + + for (u32 iLyr = 0; iLyr < rArea.Layers.size(); iLyr++) + { + CWorld::SArea::SLayer& rLayer = rArea.Layers[iLyr]; + if (!rLayer.EnabledByDefault) + LayerActiveFlags &= ~(1 << iLyr); + + LayerNames.push_back(rLayer.LayerName); + } + + rMLVL.WriteLongLong(LayerActiveFlags); + } + + rMLVL.WriteLong(LayerNames.size()); + + for (u32 iLyr = 0; iLyr < LayerNames.size(); iLyr++) + rMLVL.WriteString(LayerNames[iLyr].ToStdString()); + + rMLVL.WriteLong(LayerNameOffsets.size()); + + for (u32 iOff = 0; iOff < LayerNameOffsets.size(); iOff++) + rMLVL.WriteLong(LayerNameOffsets[iOff]); + + return true; +} + u32 CWorldCooker::GetMLVLVersion(EGame Version) { switch (Version) diff --git a/src/Core/Resource/Cooker/CWorldCooker.h b/src/Core/Resource/Cooker/CWorldCooker.h index 0d7e24bf..68a9c1f3 100644 --- a/src/Core/Resource/Cooker/CWorldCooker.h +++ b/src/Core/Resource/Cooker/CWorldCooker.h @@ -1,6 +1,7 @@ #ifndef CWORLDCOOKER_H #define CWORLDCOOKER_H +#include "Core/Resource/CWorld.h" #include "Core/Resource/EGame.h" #include @@ -8,6 +9,7 @@ class CWorldCooker { CWorldCooker(); public: + static bool CookMLVL(CWorld *pWorld, IOutputStream& rOut); static u32 GetMLVLVersion(EGame Version); }; diff --git a/src/Core/Resource/Factory/CAreaLoader.cpp b/src/Core/Resource/Factory/CAreaLoader.cpp index dfb920ba..1ad6856c 100644 --- a/src/Core/Resource/Factory/CAreaLoader.cpp +++ b/src/Core/Resource/Factory/CAreaLoader.cpp @@ -681,6 +681,7 @@ CGameArea* CAreaLoader::LoadMREA(IInputStream& MREA, CResourceEntry *pEntry) Loader.ReadSCLYPrime(); Loader.ReadCollision(); Loader.ReadLightsPrime(); + Loader.ReadPATH(); break; case eEchoesDemo: Loader.ReadHeaderEchoes(); @@ -688,6 +689,8 @@ CGameArea* CAreaLoader::LoadMREA(IInputStream& MREA, CResourceEntry *pEntry) Loader.ReadSCLYPrime(); Loader.ReadCollision(); Loader.ReadLightsPrime(); + Loader.ReadPATH(); + Loader.ReadPTLA(); Loader.ReadEGMC(); break; case eEchoes: @@ -696,6 +699,8 @@ CGameArea* CAreaLoader::LoadMREA(IInputStream& MREA, CResourceEntry *pEntry) Loader.ReadSCLYEchoes(); Loader.ReadCollision(); Loader.ReadLightsPrime(); + Loader.ReadPATH(); + Loader.ReadPTLA(); Loader.ReadEGMC(); break; case eCorruptionProto: @@ -704,6 +709,8 @@ CGameArea* CAreaLoader::LoadMREA(IInputStream& MREA, CResourceEntry *pEntry) Loader.ReadSCLYEchoes(); Loader.ReadCollision(); Loader.ReadLightsCorruption(); + Loader.ReadPATH(); + Loader.ReadPTLA(); Loader.ReadEGMC(); break; case eCorruption: @@ -715,6 +722,8 @@ CGameArea* CAreaLoader::LoadMREA(IInputStream& MREA, CResourceEntry *pEntry) if (Loader.mVersion == eCorruption) { Loader.ReadLightsCorruption(); + Loader.ReadPATH(); + Loader.ReadPTLA(); Loader.ReadEGMC(); } break; diff --git a/src/Core/Resource/Factory/CDependencyGroupLoader.cpp b/src/Core/Resource/Factory/CDependencyGroupLoader.cpp index 80e4a0e5..096b889d 100644 --- a/src/Core/Resource/Factory/CDependencyGroupLoader.cpp +++ b/src/Core/Resource/Factory/CDependencyGroupLoader.cpp @@ -5,8 +5,8 @@ EGame CDependencyGroupLoader::VersionTest(IInputStream& rDGRP, u32 DepCount) { // Only difference between versions is asset ID length. Just check for EOF with 32-bit ID length. u32 Start = rDGRP.Tell(); - rDGRP.Seek(DepCount * 4, SEEK_CUR); - u32 Remaining = rDGRP.Size() - Start; + rDGRP.Seek(DepCount * 8, SEEK_CUR); + u32 Remaining = rDGRP.Size() - rDGRP.Tell(); EGame Game = ePrimeDemo; @@ -14,7 +14,9 @@ EGame CDependencyGroupLoader::VersionTest(IInputStream& rDGRP, u32 DepCount) { for (u32 iRem = 0; iRem < Remaining; iRem++) { - if (rDGRP.ReadByte() != 0xFF) + u8 Byte = rDGRP.ReadByte(); + + if (Byte != 0xFF) { Game = eCorruptionProto; break; diff --git a/src/Core/Resource/Factory/CMaterialLoader.cpp b/src/Core/Resource/Factory/CMaterialLoader.cpp index eb940a53..db2abc3f 100644 --- a/src/Core/Resource/Factory/CMaterialLoader.cpp +++ b/src/Core/Resource/Factory/CMaterialLoader.cpp @@ -120,7 +120,7 @@ CMaterial* CMaterialLoader::ReadPrimeMaterial() if (pMat->mOptions & CMaterial::eIndStage) { u32 IndTexIndex = mpFile->ReadLong(); - pMat->mpIndirectTexture = mTextures[IndTexIndex]; + pMat->mpIndirectTexture = mTextures[TextureIndices[IndTexIndex]]; } // Color channels @@ -162,7 +162,7 @@ CMaterial* CMaterialLoader::ReadPrimeMaterial() u8 TexSel = mpFile->ReadByte(); - if ((TexSel == 0xFF) || (TexSel >= mTextures.size())) + if ((TexSel == 0xFF) || (TexSel >= TextureIndices.size())) pPass->mpTexture = nullptr; else pPass->mpTexture = mTextures[TextureIndices[TexSel]]; diff --git a/src/Core/Resource/Factory/CScanLoader.cpp b/src/Core/Resource/Factory/CScanLoader.cpp index feac5edb..f85d3126 100644 --- a/src/Core/Resource/Factory/CScanLoader.cpp +++ b/src/Core/Resource/Factory/CScanLoader.cpp @@ -9,11 +9,18 @@ CScanLoader::CScanLoader() CScan* CScanLoader::LoadScanMP1(IInputStream& rSCAN) { // Basic support at the moment - don't read animation/scan image data - rSCAN.Seek(0x4, SEEK_CUR); // Skip FRME ID + mpScan->mFrameID = CAssetID(rSCAN, e32Bit); mpScan->mpStringTable = gpResourceStore->LoadResource(rSCAN.ReadLong(), "STRG"); mpScan->mIsSlow = (rSCAN.ReadLong() != 0); mpScan->mCategory = (CScan::ELogbookCategory) rSCAN.ReadLong(); mpScan->mIsImportant = (rSCAN.ReadByte() == 1); + + for (u32 iImg = 0; iImg < 4; iImg++) + { + mpScan->mScanImageTextures[iImg] = CAssetID(rSCAN, e32Bit); + rSCAN.Seek(0x18, SEEK_CUR); + } + mpScan->mVersion = ePrime; return mpScan; } diff --git a/src/Core/Resource/Model/CModel.cpp b/src/Core/Resource/Model/CModel.cpp index 09fb00ef..b5a26925 100644 --- a/src/Core/Resource/Model/CModel.cpp +++ b/src/Core/Resource/Model/CModel.cpp @@ -33,24 +33,17 @@ CModel::~CModel() CDependencyTree* CModel::BuildDependencyTree() const { CDependencyTree *pTree = new CDependencyTree(ID()); + std::set TextureIDs; for (u32 iSet = 0; iSet < mMaterialSets.size(); iSet++) { CMaterialSet *pSet = mMaterialSets[iSet]; - - for (u32 iMat = 0; iMat < pSet->NumMaterials(); iMat++) - { - CMaterial *pMat = pSet->MaterialByIndex(iMat); - pTree->AddDependency(pMat->IndTexture()); - - for (u32 iPass = 0; iPass < pMat->PassCount(); iPass++) - { - CMaterialPass *pPass = pMat->Pass(iPass); - pTree->AddDependency(pPass->Texture()); - } - } + pSet->GetUsedTextureIDs(TextureIDs); } + for (auto Iter = TextureIDs.begin(); Iter != TextureIDs.end(); Iter++) + pTree->AddDependency(*Iter); + return pTree; } diff --git a/src/Editor/CProjectOverviewDialog.cpp b/src/Editor/CProjectOverviewDialog.cpp index b6e99b6f..7671e4ea 100644 --- a/src/Editor/CProjectOverviewDialog.cpp +++ b/src/Editor/CProjectOverviewDialog.cpp @@ -20,6 +20,7 @@ CProjectOverviewDialog::CProjectOverviewDialog(QWidget *pParent) connect(mpUI->LoadWorldButton, SIGNAL(clicked()), this, SLOT(LoadWorld())); connect(mpUI->LaunchEditorButton, SIGNAL(clicked()), this, SLOT(LaunchEditor())); connect(mpUI->ViewResourcesButton, SIGNAL(clicked()), this, SLOT(LaunchResourceBrowser())); + connect(mpUI->CookPackageButton, SIGNAL(clicked()), this, SLOT(CookPackage())); } CProjectOverviewDialog::~CProjectOverviewDialog() @@ -44,6 +45,7 @@ void CProjectOverviewDialog::OpenProject() mpProject = pNewProj; mpProject->SetActive(); SetupWorldsList(); + SetupPackagesList(); } else @@ -113,6 +115,19 @@ void CProjectOverviewDialog::SetupWorldsList() mpUI->LoadWorldButton->setEnabled(!mWorldEntries.isEmpty()); } +void CProjectOverviewDialog::SetupPackagesList() +{ + ASSERT(mpProject != nullptr && mpProject->IsActive()); + mpUI->PackagesList->clear(); + + for (u32 iPkg = 0; iPkg < mpProject->NumPackages(); iPkg++) + { + CPackage *pPackage = mpProject->PackageByIndex(iPkg); + ASSERT(pPackage != nullptr); + mpUI->PackagesList->addItem(TO_QSTRING(pPackage->Name())); + } +} + void CProjectOverviewDialog::LoadWorld() { // Find world @@ -145,9 +160,6 @@ void CProjectOverviewDialog::LoadWorld() void CProjectOverviewDialog::LaunchEditor() { - CGameArea *pOldArea = mpWorldEditor->ActiveArea(); - (void) pOldArea; - // Load area u32 AreaIdx = mpUI->AreaComboBox->currentIndex(); CResourceEntry *pAreaEntry = mAreaEntries[AreaIdx]; @@ -172,3 +184,10 @@ void CProjectOverviewDialog::LaunchResourceBrowser() CResourceBrowser Browser(this); Browser.exec(); } + +void CProjectOverviewDialog::CookPackage() +{ + u32 PackageIdx = mpUI->PackagesList->currentRow(); + CPackage *pPackage = mpProject->PackageByIndex(PackageIdx); + pPackage->Cook(); +} diff --git a/src/Editor/CProjectOverviewDialog.h b/src/Editor/CProjectOverviewDialog.h index 4fd08b40..e6dce89d 100644 --- a/src/Editor/CProjectOverviewDialog.h +++ b/src/Editor/CProjectOverviewDialog.h @@ -31,8 +31,10 @@ public slots: void LoadWorld(); void LaunchEditor(); void LaunchResourceBrowser(); + void CookPackage(); void SetupWorldsList(); + void SetupPackagesList(); }; #endif // CPROJECTOVERVIEWDIALOG_H diff --git a/src/Editor/CProjectOverviewDialog.ui b/src/Editor/CProjectOverviewDialog.ui index ff4925e3..ec91e0bb 100644 --- a/src/Editor/CProjectOverviewDialog.ui +++ b/src/Editor/CProjectOverviewDialog.ui @@ -6,98 +6,129 @@ 0 0 - 268 - 367 + 492 + 445 Dialog - + - + - - - Open Project - - + + + + + Open Project + + + + + + + Export Game + + + + - + - Export Game + View Resources - - - View Resources - - - - - - - - 2 - 0 - - - - Worlds - - - - - - - - - false - - - Load - - - - - - - - - - - 3 - 0 - - - - Areas - - - - - - false - - - - - - - false - - - Launch World Editor - - - - - + + + + + + + + 2 + 0 + + + + Worlds + + + + + + + + + false + + + Load + + + + + + + + + + + 3 + 0 + + + + Areas + + + + + + false + + + + + + + false + + + Launch World Editor + + + + + + + + + + + + Packages + + + + + + + + + Cook Package + + + + + + + diff --git a/src/Editor/CStartWindow.cpp b/src/Editor/CStartWindow.cpp index 1782ee20..0e282611 100644 --- a/src/Editor/CStartWindow.cpp +++ b/src/Editor/CStartWindow.cpp @@ -144,14 +144,8 @@ void CStartWindow::FillAreaUI() else ui->AreaNameSTRGLineEdit->clear(); - u64 MREA = mpWorld->AreaResourceID(mSelectedAreaIndex); - TString MREAStr; - if (MREA & 0xFFFFFFFF00000000) - MREAStr = TString::FromInt64(MREA, 16); - else - MREAStr = TString::FromInt32(MREA, 8); - - ui->AreaMREALineEdit->setText(TO_QSTRING(MREAStr) + QString(".MREA")); + CAssetID MREA = mpWorld->AreaResourceID(mSelectedAreaIndex); + ui->AreaMREALineEdit->setText(TO_QSTRING(MREA.ToString()) + QString(".MREA")); u32 NumAttachedAreas = mpWorld->AreaAttachedCount(mSelectedAreaIndex); ui->AttachedAreasList->clear(); diff --git a/src/FileIO/IOutputStream.cpp b/src/FileIO/IOutputStream.cpp index 0e0f4114..1c2eedee 100644 --- a/src/FileIO/IOutputStream.cpp +++ b/src/FileIO/IOutputStream.cpp @@ -75,7 +75,7 @@ void IOutputStream::WriteWideString(const std::wstring& rkVal, unsigned long Cou WriteShort(0); } -void IOutputStream::WriteToBoundary(unsigned long Boundary, char Fill) +void IOutputStream::WriteToBoundary(unsigned long Boundary, unsigned char Fill) { long Num = Boundary - (Tell() % Boundary); if (Num == Boundary) return; diff --git a/src/FileIO/IOutputStream.h b/src/FileIO/IOutputStream.h index 5681d996..205fb1bf 100644 --- a/src/FileIO/IOutputStream.h +++ b/src/FileIO/IOutputStream.h @@ -22,7 +22,7 @@ public: void WriteWideString(const std::wstring& rkVal); void WriteWideString(const std::wstring& rkVal, unsigned long Count, bool Terminate = false); - void WriteToBoundary(unsigned long Boundary, char Fill); + void WriteToBoundary(unsigned long Boundary, unsigned char Fill); void SetEndianness(IOUtil::EEndianness Endianness); void SetDestString(const std::string& rkDest); IOUtil::EEndianness GetEndianness() const; diff --git a/templates/mp1/Script/AreaAttributes.xml b/templates/mp1/Script/AreaAttributes.xml index bf3d86e1..18773692 100644 --- a/templates/mp1/Script/AreaAttributes.xml +++ b/templates/mp1/Script/AreaAttributes.xml @@ -11,6 +11,7 @@ + diff --git a/templates/mp1/Script/Drone.xml b/templates/mp1/Script/Drone.xml index be8143c4..de8f75c7 100644 --- a/templates/mp1/Script/Drone.xml +++ b/templates/mp1/Script/Drone.xml @@ -13,9 +13,9 @@ - - - + + + diff --git a/templates/mp1/Script/FishCloud.xml b/templates/mp1/Script/FishCloud.xml index 01540969..cc28f867 100644 --- a/templates/mp1/Script/FishCloud.xml +++ b/templates/mp1/Script/FishCloud.xml @@ -27,7 +27,7 @@ - + diff --git a/templates/mp1/Script/NewIntroBoss.xml b/templates/mp1/Script/NewIntroBoss.xml index a4d54cd6..70825a86 100644 --- a/templates/mp1/Script/NewIntroBoss.xml +++ b/templates/mp1/Script/NewIntroBoss.xml @@ -8,8 +8,8 @@ - - + + diff --git a/templates/mp1/Structs/LightParameters.xml b/templates/mp1/Structs/LightParameters.xml index 28e9e1f3..b2d62277 100644 --- a/templates/mp1/Structs/LightParameters.xml +++ b/templates/mp1/Structs/LightParameters.xml @@ -14,6 +14,8 @@ + +