Added support for saving/loading game projects

This commit is contained in:
parax0 2016-07-05 20:09:21 -06:00
parent f55b3666a0
commit 12bd4eff90
7 changed files with 260 additions and 146 deletions

View File

@ -1,6 +1,7 @@
#include "CGameExporter.h"
#include "Core/GameProject/CResourceStore.h"
#include "Core/Resource/CWorld.h"
#include "Core/Resource/Script/CMasterTemplate.h"
#include <FileIO/FileIO.h>
#include <Common/AssertMacro.h>
#include <Common/CompressionUtil.h>
@ -8,11 +9,11 @@
#include <Common/FileUtil.h>
#include <tinyxml2.h>
#define COPY_DISC_DATA 0
#define COPY_DISC_DATA 1
#define LOAD_PAKS 1
#define SAVE_PACKAGE_DEFINITIONS 1
#define EXPORT_WORLDS 0
#define EXPORT_COOKED 0
#define EXPORT_WORLDS 1
#define EXPORT_COOKED 1
CGameExporter::CGameExporter(const TString& rkInputDir, const TString& rkOutputDir)
: mStore(this)
@ -91,12 +92,7 @@ void CGameExporter::CopyDiscData()
// Detect paks
if (FullPath.GetFileExtension().ToLower() == L"pak")
{
if (FullPath.GetFileName(false).StartsWith(L"Metroid", false) || RelPath.Contains(L"Worlds", false))
mWorldPaks.push_back(FullPath);
else
mResourcePaks.push_back(FullPath);
}
mPaks.push_back(FullPath);
#if COPY_DISC_DATA
// Create directory
@ -110,6 +106,8 @@ void CGameExporter::CopyDiscData()
}
ASSERT(Game() != eUnknownVersion);
mpProject->SetGame(Game());
mpProject->SetProjectName(CMasterTemplate::FindGameName(Game()));
}
void CGameExporter::LoadAssetList()
@ -168,139 +166,133 @@ void CGameExporter::LoadPaks()
{
#if LOAD_PAKS
SCOPED_TIMER(LoadPaks);
EUIDLength IDLength = (Game() < eCorruptionProto ? e32Bit : e64Bit);
for (u32 iList = 0; iList < 2; iList++)
for (auto It = mPaks.begin(); It != mPaks.end(); It++)
{
const TWideStringList& rkList = (iList == 0 ? mWorldPaks : mResourcePaks);
bool IsWorldPak = (iList == 0);
EUIDLength IDLength = (Game() < eCorruptionProto ? e32Bit : e64Bit);
TWideString PakPath = *It;
TString CharPak = PakPath.ToUTF8();
CFileInStream Pak(CharPak.ToStdString(), IOUtil::eBigEndian);
for (auto It = rkList.begin(); It != rkList.end(); It++)
if (!Pak.IsValid())
{
TWideString PakPath = *It;
TString CharPak = PakPath.ToUTF8();
CFileInStream Pak(CharPak.ToStdString(), IOUtil::eBigEndian);
Log::Error("Couldn't open pak: " + CharPak);
continue;
}
if (!Pak.IsValid())
CPackage *pPackage = new CPackage(mpProject, CharPak.GetFileName(false), FileUtil::MakeRelative(PakPath.GetFileDirectory(), mGameDir));
CResourceCollection *pCollection = pPackage->AddCollection("Default");
// MP1-MP3Proto
if (Game() < eCorruption)
{
u32 PakVersion = Pak.ReadLong();
Pak.Seek(0x4, SEEK_CUR);
ASSERT(PakVersion == 0x00030005);
// Echoes demo disc has a pak that ends right here.
if (!Pak.EoF())
{
Log::Error("Couldn't open pak: " + CharPak);
continue;
u32 NumNamedResources = Pak.ReadLong();
ASSERT(NumNamedResources > 0);
for (u32 iName = 0; iName < NumNamedResources; iName++)
{
CFourCC ResType = Pak.ReadLong();
CUniqueID ResID(Pak, IDLength);
u32 NameLen = Pak.ReadLong();
TString Name = Pak.ReadString(NameLen);
pCollection->AddResource(Name, ResID, ResType);
}
u32 NumResources = Pak.ReadLong();
for (u32 iRes = 0; iRes < NumResources; iRes++)
{
bool Compressed = (Pak.ReadLong() == 1);
CFourCC ResType = Pak.ReadLong();
CUniqueID ResID(Pak, IDLength);
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 };
}
}
}
CPackage *pPackage = new CPackage(mpProject, CharPak.GetFileName(false), FileUtil::MakeRelative(PakPath.GetFileDirectory(), mGameDir));
CResourceCollection *pCollection = pPackage->AddCollection("Default");
// MP3 + DKCR
else
{
u32 PakVersion = Pak.ReadLong();
u32 PakHeaderLen = Pak.ReadLong();
Pak.Seek(PakHeaderLen - 0x8, SEEK_CUR);
ASSERT(PakVersion == 2);
// MP1-MP3Proto
if (Game() < eCorruption)
struct SPakSection {
CFourCC Type; u32 Size;
};
std::vector<SPakSection> PakSections;
u32 NumPakSections = Pak.ReadLong();
ASSERT(NumPakSections == 3);
for (u32 iSec = 0; iSec < NumPakSections; iSec++)
{
u32 PakVersion = Pak.ReadLong();
Pak.Seek(0x4, SEEK_CUR);
ASSERT(PakVersion == 0x00030005);
CFourCC Type = Pak.ReadLong();
u32 Size = Pak.ReadLong();
PakSections.push_back(SPakSection { Type, Size });
}
Pak.SeekToBoundary(64);
// Echoes demo disc has a pak that ends right here.
if (!Pak.EoF())
for (u32 iSec = 0; iSec < NumPakSections; iSec++)
{
u32 Next = Pak.Tell() + PakSections[iSec].Size;
// Named Resources
if (PakSections[iSec].Type == "STRG")
{
u32 NumNamedResources = Pak.ReadLong();
ASSERT(NumNamedResources > 0);
for (u32 iName = 0; iName < NumNamedResources; iName++)
{
TString Name = Pak.ReadString();
CFourCC ResType = Pak.ReadLong();
CUniqueID ResID(Pak, IDLength);
u32 NameLen = Pak.ReadLong();
TString Name = Pak.ReadString(NameLen);
pCollection->AddResource(Name, ResID, ResType);
}
}
else if (PakSections[iSec].Type == "RSHD")
{
ASSERT(PakSections[iSec + 1].Type == "DATA");
u32 DataStart = Next;
u32 NumResources = Pak.ReadLong();
for (u32 iRes = 0; iRes < NumResources; iRes++)
{
bool Compressed = (Pak.ReadLong() == 1);
CFourCC ResType = Pak.ReadLong();
CFourCC Type = Pak.ReadLong();
CUniqueID ResID(Pak, IDLength);
u32 ResSize = Pak.ReadLong();
u32 ResOffset = Pak.ReadLong();
u32 Size = Pak.ReadLong();
u32 Offset = DataStart + Pak.ReadLong();
u64 IntegralID = ResID.ToLongLong();
if (mResourceMap.find(IntegralID) == mResourceMap.end())
mResourceMap[IntegralID] = SResourceInstance { PakPath, ResID, ResType, ResOffset, ResSize, Compressed, false };
mResourceMap[IntegralID] = SResourceInstance { PakPath, ResID, Type, Offset, Size, Compressed, false };
}
}
Pak.Seek(Next, SEEK_SET);
}
// MP3 + DKCR
else
{
u32 PakVersion = Pak.ReadLong();
u32 PakHeaderLen = Pak.ReadLong();
Pak.Seek(PakHeaderLen - 0x8, SEEK_CUR);
ASSERT(PakVersion == 2);
struct SPakSection {
CFourCC Type; u32 Size;
};
std::vector<SPakSection> PakSections;
u32 NumPakSections = Pak.ReadLong();
ASSERT(NumPakSections == 3);
for (u32 iSec = 0; iSec < NumPakSections; iSec++)
{
CFourCC Type = Pak.ReadLong();
u32 Size = Pak.ReadLong();
PakSections.push_back(SPakSection { Type, Size });
}
Pak.SeekToBoundary(64);
for (u32 iSec = 0; iSec < NumPakSections; iSec++)
{
u32 Next = Pak.Tell() + PakSections[iSec].Size;
// Named Resources
if (PakSections[iSec].Type == "STRG")
{
u32 NumNamedResources = Pak.ReadLong();
for (u32 iName = 0; iName < NumNamedResources; iName++)
{
TString Name = Pak.ReadString();
CFourCC ResType = Pak.ReadLong();
CUniqueID ResID(Pak, IDLength);
pCollection->AddResource(Name, ResID, ResType);
}
}
else if (PakSections[iSec].Type == "RSHD")
{
ASSERT(PakSections[iSec + 1].Type == "DATA");
u32 DataStart = Next;
u32 NumResources = Pak.ReadLong();
for (u32 iRes = 0; iRes < NumResources; iRes++)
{
bool Compressed = (Pak.ReadLong() == 1);
CFourCC Type = Pak.ReadLong();
CUniqueID ResID(Pak, IDLength);
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 };
}
}
Pak.Seek(Next, SEEK_SET);
}
}
// Add package to project and save
mpProject->AddPackage(pPackage, IsWorldPak);
#if SAVE_PACKAGE_DEFINITIONS
pPackage->Save();
#endif
}
// Add package to project and save
mpProject->AddPackage(pPackage);
#if SAVE_PACKAGE_DEFINITIONS
pPackage->Save();
#endif
}
#endif
}
@ -406,9 +398,9 @@ void CGameExporter::ExportWorlds()
#if EXPORT_WORLDS
SCOPED_TIMER(ExportWorlds);
for (u32 iPak = 0; iPak < mpProject->NumWorldPaks(); iPak++)
for (u32 iPak = 0; iPak < mpProject->NumPackages(); iPak++)
{
CPackage *pPak = mpProject->WorldPakByIndex(iPak);
CPackage *pPak = mpProject->PackageByIndex(iPak);
// Get output path. DKCR paks are stored in a Worlds folder so we should get the path relative to that so we don't have Worlds\Worlds\.
// Other games have all paks in the game root dir so we're fine just taking the original root dir-relative directory.
@ -460,20 +452,11 @@ void CGameExporter::ExportWorlds()
if (pTable) GameAreaName = pTable->String("ENGL", 0);
if (GameAreaName.IsEmpty()) GameAreaName = InternalAreaName;
// Load area
CUniqueID AreaID = pWorld->AreaResourceID(iArea);
CGameArea *pArea = (CGameArea*) mStore.LoadResource(AreaID, "MREA");
if (!pArea)
{
Log::Error("Unable to export area " + GameAreaName.ToUTF8() + " from world " + rkRes.Name + "; couldn't load area");
continue;
}
// Export area
TWideString AreaDir = WorldDir + TWideString::FromInt32(iArea, 2, 10) + L"_" + FileUtil::SanitizeName(GameAreaName, true) + L"\\";
FileUtil::CreateDirectory(mCookedDir + AreaDir);
CUniqueID AreaID = pWorld->AreaResourceID(iArea);
SResourceInstance *pInst = FindResourceInstance(AreaID);
ASSERT(pInst != nullptr);
@ -483,11 +466,6 @@ void CGameExporter::ExportWorlds()
mStore.DestroyUnreferencedResources();
}
else
{
Log::Error("Unexpected named resource type in world pak: " + rkRes.Type.ToString());
}
}
}
#endif
@ -506,11 +484,14 @@ void CGameExporter::ExportCookedResources()
ExportResource(rRes);
}
}
#endif
{
SCOPED_TIMER(SaveResourceDatabase);
#if EXPORT_COOKED
mStore.SaveResourceDatabase(this->mExportDir.ToUTF8() + "ResourceDatabase.rdb");
}
#endif
mpProject->Save();
}
}
void CGameExporter::ExportResource(SResourceInstance& rRes)

View File

@ -25,8 +25,7 @@ class CGameExporter
TWideString mWorldsDirName;
// Resources
TWideStringList mWorldPaks;
TWideStringList mResourcePaks;
TWideStringList mPaks;
struct SResourceInstance
{

View File

@ -1,10 +1,103 @@
#include "CGameProject.h"
#include "Core/Resource/Script/CMasterTemplate.h"
#include <tinyxml2.h>
void CGameProject::AddPackage(CPackage *pPackage, bool WorldPak)
using namespace tinyxml2;
void CGameProject::Load()
{
if (WorldPak)
mWorldPaks.push_back(pPackage);
else
mResourcePaks.push_back(pPackage);
TString ProjPath = ProjectPath().ToUTF8();
XMLDocument Doc;
Doc.LoadFile(*ProjPath);
if (Doc.Error())
{
Log::Error("Unable to open game project at " + ProjPath);
return;
}
XMLElement *pRoot = Doc.FirstChildElement("GameProject");
//EProjectVersion Version = (EProjectVersion) TString(pRoot->Attribute("Version")).ToInt32(10);
// Verify all elements are present
XMLElement *pProjName = pRoot->FirstChildElement("Name");
XMLElement *pGame = pRoot->FirstChildElement("Game");
XMLElement *pResDB = pRoot->FirstChildElement("ResourceDB");
XMLElement *pPackages = pRoot->FirstChildElement("Packages");
if (!pProjName || !pGame || !pResDB || !pPackages)
{
TString MissingElem = pProjName ? (pGame ? (pResDB ? "Packages" : "ResourceDB") : "Game") : "Name";
Log::Error("Unable to load game project at " + ProjPath + "; " + MissingElem + " element is missing");
return;
}
mProjectName = pProjName->GetText();
mGame = CMasterTemplate::FindGameForName( pGame->GetText() );
mResourceDBPath = pResDB->GetText();
// Load packages
XMLElement *pPkgElem = pPackages->FirstChildElement("Package");
while (pPkgElem)
{
pPkgElem = pPkgElem->NextSiblingElement("Package");
TString Path = pPkgElem->Attribute("Path");
if (Path.IsEmpty())
Log::Error("Failed to load package in game project " + ProjPath + "; Path attribute is missing or empty");
else
{
CPackage *pPackage = new CPackage(this, Path.GetFileName(false), TString(Path.GetFileDirectory()).ToUTF16());
pPackage->Load();
mPackages.push_back(pPackage);
}
}
}
void CGameProject::Save()
{
XMLDocument Doc;
XMLDeclaration *pDecl = Doc.NewDeclaration();
Doc.LinkEndChild(pDecl);
XMLElement *pRoot = Doc.NewElement("GameProject");
pRoot->SetAttribute("Version", eVer_Current);
Doc.LinkEndChild(pRoot);
XMLElement *pProjName = Doc.NewElement("Name");
pProjName->SetText(*mProjectName);
pRoot->LinkEndChild(pProjName);
XMLElement *pGame = Doc.NewElement("Game");
pGame->SetText(*CMasterTemplate::FindGameName(mGame));
pRoot->LinkEndChild(pGame);
XMLElement *pResDB = Doc.NewElement("ResourceDB");
pResDB->SetText(*mResourceDBPath.ToUTF8());
pRoot->LinkEndChild(pResDB);
XMLElement *pPackages = Doc.NewElement("Packages");
pRoot->LinkEndChild(pPackages);
for (u32 iPkg = 0; iPkg < mPackages.size(); iPkg++)
{
CPackage *pPackage = mPackages[iPkg];
TWideString FullDefPath = pPackage->DefinitionPath(false);
TWideString RelDefPath = FileUtil::MakeRelative(FullDefPath.GetFileDirectory(), PackagesDir(false));
TString DefPath = TWideString(RelDefPath + FullDefPath.GetFileName()).ToUTF8();
XMLElement *pPakElem = Doc.NewElement("Package");
pPakElem->SetAttribute("Path", *DefPath);
pPackages->LinkEndChild(pPakElem);
}
// Save Project
TString ProjPath = ProjectPath().ToUTF8();
XMLError Result = Doc.SaveFile(*ProjPath);
if (Result != XML_SUCCESS)
Log::Error("Failed to save game project at: " + ProjPath);
}

View File

@ -4,6 +4,7 @@
#include "CPackage.h"
#include "CResourceStore.h"
#include "Core/Resource/EGame.h"
#include <Common/FileUtil.h>
#include <Common/CUniqueID.h>
#include <Common/TString.h>
#include <Common/types.h>
@ -14,18 +15,25 @@ class CGameProject
TString mProjectName;
TWideString mProjectRoot;
TWideString mResourceDBPath;
std::vector<CPackage*> mWorldPaks;
std::vector<CPackage*> mResourcePaks;
std::vector<CPackage*> mPackages;
enum EProjectVersion
{
eVer_Initial,
eVer_Max,
eVer_Current = eVer_Max - 1
};
public:
CGameProject(const TWideString& rkProjRootDir)
: mGame(eUnknownVersion)
, mProjectName("UnnamedProject")
, mProjectName("Unnamed Project")
, mProjectRoot(rkProjRootDir)
, mResourceDBPath(L"ResourceDB.rdb")
{}
void AddPackage(CPackage *pPackage, bool WorldPak);
void Load();
void Save();
// Directory Handling
inline TWideString ProjectRoot() const { return mProjectRoot; }
@ -34,13 +42,15 @@ public:
inline TWideString ContentDir(bool Relative) const { return Relative ? L"Content\\" : mProjectRoot + L"Content\\"; }
inline TWideString CookedDir(bool Relative) const { return Relative ? L"Cooked\\" : mProjectRoot + L"Cooked\\"; }
inline TWideString PackagesDir(bool Relative) const { return Relative ? L"Packages\\" : mProjectRoot + L"Packages\\"; }
inline TWideString ProjectPath() const { return mProjectRoot + FileUtil::SanitizeName(mProjectName.ToUTF16(), false) + L".prj"; }
// Accessors
inline void SetGame(EGame Game) { mGame = Game; }
inline void SetProjectName(const TString& rkName) { mProjectName = rkName; }
inline u32 NumWorldPaks() const { return mWorldPaks.size(); }
inline CPackage* WorldPakByIndex(u32 Index) const { return mWorldPaks[Index]; }
inline u32 NumPackages() const { return mPackages.size(); }
inline CPackage* PackageByIndex(u32 Index) const { return mPackages[Index]; }
inline void AddPackage(CPackage *pPackage) { mPackages.push_back(pPackage); }
inline EGame Game() const { return mGame; }
};

View File

@ -119,6 +119,26 @@ std::list<CMasterTemplate*> CMasterTemplate::MasterList()
return list;
}
TString CMasterTemplate::FindGameName(EGame Game)
{
CMasterTemplate *pMaster = MasterForGame(Game);
return pMaster ? pMaster->GameName() : "Unknown Game";
}
EGame CMasterTemplate::FindGameForName(const TString& rkName)
{
std::list<CMasterTemplate*> Masters = MasterList();
for (auto It = Masters.begin(); It != Masters.end(); It++)
{
CMasterTemplate *pMaster = *It;
if (pMaster->GameName() == rkName)
return pMaster->Game();
}
return eUnknownVersion;
}
TString CMasterTemplate::PropertyName(u32 PropertyID)
{
auto it = smPropertyNames.find(PropertyID);

View File

@ -51,17 +51,20 @@ public:
CStructTemplate* StructAtSource(const TString& rkSource);
// Inline Accessors
EGame Game() const { return mGame; }
u32 NumGameVersions() const { return mGameVersions.empty() ? 1 : mGameVersions.size(); }
u32 NumScriptTemplates() const { return mTemplates.size(); }
u32 NumStates() const { return mStates.size(); }
u32 NumMessages() const { return mMessages.size(); }
bool IsLoadedSuccessfully() { return mFullyLoaded; }
TString GetDirectory() const { return mSourceFile.GetFileDirectory(); }
inline EGame Game() const { return mGame; }
inline TString GameName() const { return mGameName; }
inline u32 NumGameVersions() const { return mGameVersions.empty() ? 1 : mGameVersions.size(); }
inline u32 NumScriptTemplates() const { return mTemplates.size(); }
inline u32 NumStates() const { return mStates.size(); }
inline u32 NumMessages() const { return mMessages.size(); }
inline bool IsLoadedSuccessfully() { return mFullyLoaded; }
inline TString GetDirectory() const { return mSourceFile.GetFileDirectory(); }
// Static
static CMasterTemplate* MasterForGame(EGame Game);
static std::list<CMasterTemplate*> MasterList();
static TString FindGameName(EGame Game);
static EGame FindGameForName(const TString& rkName);
static TString PropertyName(u32 PropertyID);
static u32 CreatePropertyID(IPropertyTemplate *pTemp);
static void AddProperty(IPropertyTemplate *pTemp, const TString& rkTemplateName = "");

View File

@ -274,6 +274,14 @@ void CStartWindow::ExportGame()
if (ExportDir.isEmpty()) return;
#endif
// Verify valid game root by checking if opening.bnr exists
TString OpeningBNR = TO_TSTRING(GameRoot) + "/opening.bnr";
if (!FileUtil::Exists(OpeningBNR.ToUTF16()))
{
QMessageBox::warning(this, "Error", "Error; this is not a valid game root directory!");
return;
}
CGameExporter Exporter(TO_TSTRING(GameRoot), TO_TSTRING(ExportDir));
Exporter.Export();
}