PrimeWorldEditor/src/Core/GameProject/CResourceStore.cpp

621 lines
17 KiB
C++

#include "CResourceStore.h"
#include "CGameExporter.h"
#include "CGameProject.h"
#include "CResourceIterator.h"
#include "Core/IUIRelay.h"
#include "Core/Resource/CResource.h"
#include <Common/AssertMacro.h>
#include <Common/FileUtil.h>
#include <Common/Log.h>
#include <Common/Serialization/Binary.h>
#include <Common/Serialization/XML.h>
#include <tinyxml2.h>
using namespace tinyxml2;
CResourceStore *gpResourceStore = nullptr;
CResourceStore *gpEditorStore = nullptr;
// Constructor for editor store
CResourceStore::CResourceStore(const TString& rkDatabasePath)
: mpProj(nullptr)
, mGame(ePrime)
, mDatabaseCacheDirty(false)
{
mpDatabaseRoot = new CVirtualDirectory(this);
mDatabasePath = FileUtil::MakeAbsolute(rkDatabasePath.GetFileDirectory());
LoadDatabaseCache();
}
// Main constructor for game projects and game exporter
CResourceStore::CResourceStore(CGameProject *pProject)
: mpProj(nullptr)
, mGame(eUnknownGame)
, mpDatabaseRoot(nullptr)
, mDatabaseCacheDirty(false)
{
SetProject(pProject);
}
CResourceStore::~CResourceStore()
{
CloseProject();
DestroyUnreferencedResources();
for (auto It = mResourceEntries.begin(); It != mResourceEntries.end(); It++)
delete It->second;
}
void RecursiveGetListOfEmptyDirectories(CVirtualDirectory *pDir, TStringList& rOutList)
{
// Helper function for SerializeResourceDatabase
if (pDir->IsEmpty(false))
{
rOutList.push_back(pDir->FullPath());
}
else
{
for (u32 SubIdx = 0; SubIdx < pDir->NumSubdirectories(); SubIdx++)
RecursiveGetListOfEmptyDirectories(pDir->SubdirectoryByIndex(SubIdx), rOutList);
}
}
bool CResourceStore::SerializeDatabaseCache(IArchive& rArc)
{
// Serialize resources
if (rArc.ParamBegin("Resources"))
{
// Serialize resources
u32 ResourceCount = mResourceEntries.size();
rArc << SERIAL_AUTO(ResourceCount);
if (rArc.IsReader())
{
for (u32 ResIdx = 0; ResIdx < ResourceCount; ResIdx++)
{
if (rArc.ParamBegin("Resource"))
{
CResourceEntry *pEntry = CResourceEntry::BuildFromArchive(this, rArc);
ASSERT( FindEntry(pEntry->ID()) == nullptr );
mResourceEntries[pEntry->ID()] = pEntry;
rArc.ParamEnd();
}
}
}
else
{
for (CResourceIterator It(this); It; ++It)
{
if (rArc.ParamBegin("Resource"))
{
It->SerializeEntryInfo(rArc, false);
rArc.ParamEnd();
}
}
}
rArc.ParamEnd();
}
// Serialize empty directory list
TStringList EmptyDirectories;
if (!rArc.IsReader())
RecursiveGetListOfEmptyDirectories(mpDatabaseRoot, EmptyDirectories);
rArc << SERIAL_CONTAINER_AUTO(EmptyDirectories, "Directory");
if (rArc.IsReader())
{
for (auto Iter = EmptyDirectories.begin(); Iter != EmptyDirectories.end(); Iter++)
CreateVirtualDirectory(*Iter);
}
return true;
}
bool CResourceStore::LoadDatabaseCache()
{
ASSERT(!mDatabasePath.IsEmpty());
TString Path = DatabasePath();
if (!mpDatabaseRoot)
mpDatabaseRoot = new CVirtualDirectory(this);
// Load the resource database
CBasicBinaryReader Reader(Path, FOURCC('CACH'));
if (!Reader.IsValid() || !SerializeDatabaseCache(Reader))
{
if (gpUIRelay->AskYesNoQuestion("Error", "Failed to load the resource database. Attempt to build from the directory? (This may take a while.)"))
{
if (!BuildFromDirectory(true))
return false;
}
else return false;
}
else
{
// Database is succesfully loaded at this point
if (mpProj)
ASSERT(mpProj->Game() == Reader.Game());
}
mGame = Reader.Game();
return true;
}
bool CResourceStore::SaveDatabaseCache()
{
TString Path = DatabasePath();
CBasicBinaryWriter Writer(Path, FOURCC('CACH'), 0, mGame);
if (!Writer.IsValid())
return false;
SerializeDatabaseCache(Writer);
mDatabaseCacheDirty = false;
return true;
}
void CResourceStore::ConditionalSaveStore()
{
if (mDatabaseCacheDirty) SaveDatabaseCache();
}
void CResourceStore::SetProject(CGameProject *pProj)
{
if (mpProj == pProj) return;
if (mpProj)
CloseProject();
mpProj = pProj;
if (mpProj)
{
mDatabasePath = mpProj->ProjectRoot();
mpDatabaseRoot = new CVirtualDirectory(this);
mGame = mpProj->Game();
}
}
void CResourceStore::CloseProject()
{
// Destroy unreferenced resources first. (This is necessary to avoid invalid memory accesses when
// various TResPtrs are destroyed. There might be a cleaner solution than this.)
DestroyUnreferencedResources();
// There should be no loaded resources!!!
// If there are, that means something didn't clean up resource references properly on project close!!!
if (!mLoadedResources.empty())
{
Log::Error(TString::FromInt32(mLoadedResources.size(), 0, 10) + " resources still loaded on project close:");
for (auto Iter = mLoadedResources.begin(); Iter != mLoadedResources.end(); Iter++)
{
CResourceEntry *pEntry = Iter->second;
Log::Write("\t" + pEntry->Name() + "." + pEntry->CookedExtension().ToString());
}
ASSERT(false);
}
// Delete all entries from old project
auto It = mResourceEntries.begin();
while (It != mResourceEntries.end())
{
delete It->second;
It = mResourceEntries.erase(It);
}
delete mpDatabaseRoot;
mpDatabaseRoot = nullptr;
mpProj = nullptr;
mGame = eUnknownGame;
}
CVirtualDirectory* CResourceStore::GetVirtualDirectory(const TString& rkPath, bool AllowCreate)
{
if (rkPath.IsEmpty())
return mpDatabaseRoot;
else if (mpDatabaseRoot)
return mpDatabaseRoot->FindChildDirectory(rkPath, AllowCreate);
else
return nullptr;
}
void CResourceStore::CreateVirtualDirectory(const TString& rkPath)
{
if (!rkPath.IsEmpty())
mpDatabaseRoot->FindChildDirectory(rkPath, true);
}
void CResourceStore::ConditionalDeleteDirectory(CVirtualDirectory *pDir, bool Recurse)
{
if (pDir->IsEmpty(true) && !pDir->IsRoot())
{
CVirtualDirectory *pParent = pDir->Parent();
pParent->RemoveChildDirectory(pDir);
if (Recurse)
{
ConditionalDeleteDirectory(pParent, true);
}
}
}
TString CResourceStore::DefaultResourceDirPath() const
{
return StaticDefaultResourceDirPath( mGame );
}
CResourceEntry* CResourceStore::FindEntry(const CAssetID& rkID) const
{
if (!rkID.IsValid()) return nullptr;
auto Found = mResourceEntries.find(rkID);
if (Found == mResourceEntries.end()) return nullptr;
else return Found->second;
}
CResourceEntry* CResourceStore::FindEntry(const TString& rkPath) const
{
return (mpDatabaseRoot ? mpDatabaseRoot->FindChildResource(rkPath) : nullptr);
}
bool CResourceStore::AreAllEntriesValid() const
{
for (CResourceIterator Iter(this); Iter; ++Iter)
{
if (!Iter->HasCookedVersion() && !Iter->HasRawVersion())
return false;
}
return true;
}
void CResourceStore::ClearDatabase()
{
// THIS OPERATION REQUIRES THAT ALL RESOURCES ARE UNREFERENCED
DestroyUnreferencedResources();
ASSERT(mLoadedResources.empty());
// Clear out existing resource entries and directories
for (auto Iter = mResourceEntries.begin(); Iter != mResourceEntries.end(); Iter++)
delete Iter->second;
mResourceEntries.clear();
delete mpDatabaseRoot;
mpDatabaseRoot = new CVirtualDirectory(this);
mDatabaseCacheDirty = true;
}
bool CResourceStore::BuildFromDirectory(bool ShouldGenerateCacheFile)
{
ASSERT(mResourceEntries.empty());
// Get list of resources
TString ResDir = ResourcesDir();
TStringList ResourceList;
FileUtil::GetDirectoryContents(ResDir, ResourceList);
for (auto Iter = ResourceList.begin(); Iter != ResourceList.end(); Iter++)
{
TString Path = *Iter;
TString RelPath = Path.ChopFront( ResDir.Size() );
if (FileUtil::IsFile(Path) && Path.EndsWith(".rsmeta"))
{
// Determine resource name
TString DirPath = RelPath.GetFileDirectory();
TString CookedFilename = RelPath.GetFileName(false); // This call removes the .rsmeta extension
TString ResName = CookedFilename.GetFileName(false); // This call removes the cooked extension
ASSERT( IsValidResourcePath(DirPath, ResName) );
// Determine resource type
TString CookedExtension = CookedFilename.GetFileExtension();
CResTypeInfo *pTypeInfo = CResTypeInfo::TypeForCookedExtension( Game(), CFourCC(CookedExtension) );
if (!pTypeInfo)
{
Log::Error("Found resource but couldn't register because failed to identify resource type: " + RelPath);
continue;
}
// Create resource entry
CResourceEntry *pEntry = CResourceEntry::BuildFromDirectory(this, pTypeInfo, DirPath, ResName);
// Validate the entry
CAssetID ID = pEntry->ID();
ASSERT( mResourceEntries.find(ID) == mResourceEntries.end() );
ASSERT( ID.Length() == CAssetID::GameIDLength(mGame) );
mResourceEntries[ID] = pEntry;
}
else if (FileUtil::IsDirectory(Path))
CreateVirtualDirectory(RelPath);
}
// Generate new cache file
if (ShouldGenerateCacheFile)
{
// Make sure gpResourceStore points to this store
CResourceStore *pOldStore = gpResourceStore;
gpResourceStore = this;
// Make sure audio manager is loaded correctly so AGSC dependencies can be looked up
if (mpProj)
mpProj->AudioManager()->LoadAssets();
// Update dependencies
for (CResourceIterator It(this); It; ++It)
It->UpdateDependencies();
// Update database file
mDatabaseCacheDirty = true;
ConditionalSaveStore();
// Restore old gpResourceStore
gpResourceStore = pOldStore;
}
return true;
}
void CResourceStore::RebuildFromDirectory()
{
if (mpProj)
mpProj->AudioManager()->ClearAssets();
ClearDatabase();
BuildFromDirectory(true);
}
bool CResourceStore::IsResourceRegistered(const CAssetID& rkID) const
{
return FindEntry(rkID) != nullptr;
}
CResourceEntry* CResourceStore::RegisterResource(const CAssetID& rkID, EResType Type, const TString& rkDir, const TString& rkName)
{
CResourceEntry *pEntry = FindEntry(rkID);
if (pEntry)
Log::Error("Attempted to register resource that's already tracked in the database: " + rkID.ToString() + " / " + rkDir + " / " + rkName);
else
{
// Validate directory
if (IsValidResourcePath(rkDir, rkName))
{
pEntry = CResourceEntry::CreateNewResource(this, rkID, rkDir, rkName, Type);
mResourceEntries[rkID] = pEntry;
}
else
Log::Error("Invalid resource path, failed to register: " + rkDir + rkName);
}
return pEntry;
}
CResource* CResourceStore::LoadResource(const CAssetID& rkID)
{
if (!rkID.IsValid()) return nullptr;
CResourceEntry *pEntry = FindEntry(rkID);
if (pEntry)
return pEntry->Load();
else
{
// Resource doesn't seem to exist
Log::Error("Can't find requested resource with ID \"" + rkID.ToString() + "\".");;
return nullptr;
}
}
CResource* CResourceStore::LoadResource(const CAssetID& rkID, EResType Type)
{
CResource *pRes = LoadResource(rkID);
if (pRes)
{
if (pRes->Type() == Type)
{
return pRes;
}
else
{
CResTypeInfo *pExpectedType = CResTypeInfo::FindTypeInfo(Type);
CResTypeInfo *pGotType = pRes->TypeInfo();
ASSERT(pExpectedType && pGotType);
Log::Error("Resource with ID \"" + rkID.ToString() + "\" requested with the wrong type; expected " + pExpectedType->TypeName() + " asset, got " + pGotType->TypeName() + " asset");
return nullptr;
}
}
else
return nullptr;
}
CResource* CResourceStore::LoadResource(const TString& rkPath)
{
// If this is a relative path then load via the resource DB
CResourceEntry *pEntry = FindEntry(rkPath);
if (pEntry)
{
// Verify extension matches the entry + load resource
TString Ext = rkPath.GetFileExtension();
if (!Ext.IsEmpty())
{
if (Ext.Length() == 4)
{
ASSERT( Ext.CaseInsensitiveCompare(pEntry->CookedExtension().ToString()) );
}
else
{
ASSERT( rkPath.EndsWith(pEntry->RawExtension()) );
}
}
return pEntry->Load();
}
else return nullptr;
}
void CResourceStore::TrackLoadedResource(CResourceEntry *pEntry)
{
ASSERT(pEntry->IsLoaded());
ASSERT(mLoadedResources.find(pEntry->ID()) == mLoadedResources.end());
mLoadedResources[pEntry->ID()] = pEntry;
}
void CResourceStore::DestroyUnreferencedResources()
{
// This can be updated to avoid the do-while loop when reference lookup is implemented.
u32 NumDeleted;
do
{
NumDeleted = 0;
auto It = mLoadedResources.begin();
while (It != mLoadedResources.end())
{
CResourceEntry *pEntry = It->second;
if (!pEntry->Resource()->IsReferenced() && pEntry->Unload())
{
It = mLoadedResources.erase(It);
NumDeleted++;
}
else It++;
}
} while (NumDeleted > 0);
}
bool CResourceStore::DeleteResourceEntry(CResourceEntry *pEntry)
{
CAssetID ID = pEntry->ID();
if (pEntry->IsLoaded())
{
if (!pEntry->Unload())
return false;
auto It = mLoadedResources.find(ID);
ASSERT(It != mLoadedResources.end());
mLoadedResources.erase(It);
}
if (pEntry->Directory())
pEntry->Directory()->RemoveChildResource(pEntry);
auto It = mResourceEntries.find(ID);
ASSERT(It != mResourceEntries.end());
mResourceEntries.erase(It);
delete pEntry;
return true;
}
void CResourceStore::ImportNamesFromPakContentsTxt(const TString& rkTxtPath, bool UnnamedOnly)
{
// Read file contents -first- then move assets -after-; this
// 1. avoids anything fucking up if the contents file is badly formatted and we crash, and
// 2. avoids extra redundant moves (since there are redundant entries in the file)
// todo: move to CAssetNameMap?
std::map<CResourceEntry*, TString> PathMap;
FILE *pContentsFile;
fopen_s(&pContentsFile, *rkTxtPath, "r");
if (!pContentsFile)
{
Log::Error("Failed to open .contents.txt file: " + rkTxtPath);
return;
}
while (!feof(pContentsFile))
{
// Get new line, parse to extract the ID/path
char LineBuffer[512];
fgets(LineBuffer, 512, pContentsFile);
TString Line(LineBuffer);
if (Line.IsEmpty()) break;
u32 IDStart = Line.IndexOfPhrase("0x") + 2;
if (IDStart == 1) continue;
u32 IDEnd = Line.IndexOf(" \t", IDStart);
u32 PathStart = IDEnd + 1;
u32 PathEnd = Line.Size() - 5;
TString IDStr = Line.SubString(IDStart, IDEnd - IDStart);
TString Path = Line.SubString(PathStart, PathEnd - PathStart);
CAssetID ID = CAssetID::FromString(IDStr);
CResourceEntry *pEntry = FindEntry(ID);
// Only process this entry if the ID exists
if (pEntry)
{
// Chop name to just after "x_rep"
u32 RepStart = Path.IndexOfPhrase("_rep");
if (RepStart != -1)
Path = Path.ChopFront(RepStart + 5);
// If the "x_rep" folder doesn't exist in this path for some reason, but this is still a path, then just chop off the drive letter.
// Otherwise, this is most likely just a standalone name, so use the full name as-is.
else if (Path[1] == ':')
Path = Path.ChopFront(3);
PathMap[pEntry] = Path;
}
}
fclose(pContentsFile);
// Assign names
for (auto Iter = PathMap.begin(); Iter != PathMap.end(); Iter++)
{
CResourceEntry *pEntry = Iter->first;
if (UnnamedOnly && pEntry->IsNamed()) continue;
TString Path = Iter->second;
TString Dir = Path.GetFileDirectory();
TString Name = Path.GetFileName(false);
if (Dir.IsEmpty()) Dir = pEntry->DirectoryPath();
pEntry->MoveAndRename(Dir, Name);
}
// Save
ConditionalSaveStore();
}
bool CResourceStore::IsValidResourcePath(const TString& rkPath, const TString& rkName)
{
// Path must not be an absolute path and must not go outside the project structure.
// Name must not be a path.
return ( CVirtualDirectory::IsValidDirectoryPath(rkPath) &&
FileUtil::IsValidName(rkName, false) &&
!rkName.Contains('/') &&
!rkName.Contains('\\') );
}
TString CResourceStore::StaticDefaultResourceDirPath(EGame Game)
{
return (Game < eCorruptionProto ? "Uncategorized/" : "uncategorized/");
}