diff --git a/src/Common/CFourCC.h b/src/Common/CFourCC.h index c605e0d6..3833d511 100644 --- a/src/Common/CFourCC.h +++ b/src/Common/CFourCC.h @@ -9,6 +9,9 @@ #define FOURCC(Text) (Text[0] << 24 | Text[1] << 16 | Text[2] << 8 | Text[3]) #define FOURCC_CONSTEXPR(A, B, C, D) (A << 24 | B << 16 | C << 8 | D) +// todo: replace usages of FOURCC and FOURCC_CONSTEXPR with this macro +// (it should be renamed to just FOURCC when the others aren't in use anymore) +#define IFOURCC(Value) Value class CFourCC { diff --git a/src/Common/EGame.cpp b/src/Common/EGame.cpp index cdf15045..a4af5977 100644 --- a/src/Common/EGame.cpp +++ b/src/Common/EGame.cpp @@ -65,3 +65,27 @@ void Serialize(IArchive& rArc, EGame& rGame) rArc.SerializePrimitive(GameID); if (rArc.IsReader()) rGame = GetGameForID(GameID); } + +// ERegion +void Serialize(IArchive& rArc, ERegion& rRegion) +{ + static const TString skRegionNames[] = { "NTSC", "PAL", "JPN" }; + TString Name; + + if (rArc.IsWriter()) + Name = skRegionNames[rRegion]; + + rArc.SerializePrimitive(Name); + + if (rArc.IsReader()) + { + for (u32 iReg = 0; iReg < 3; iReg++) + { + if (skRegionNames[iReg] == Name) + { + rRegion = (ERegion) iReg; + break; + } + } + } +} diff --git a/src/Common/EGame.h b/src/Common/EGame.h index b53850b0..101a6368 100644 --- a/src/Common/EGame.h +++ b/src/Common/EGame.h @@ -28,4 +28,14 @@ TString GetGameName(EGame Game); TString GetGameShortName(EGame Game); void Serialize(IArchive& rArc, EGame& rGame); +// ERegion +enum ERegion +{ + eRegion_NTSC, + eRegion_PAL, + eRegion_JPN, + eRegion_Unknown = -1 +}; +void Serialize(IArchive& rArc, ERegion& rRegion); + #endif // EGAME_H diff --git a/src/Common/FileUtil.cpp b/src/Common/FileUtil.cpp index 57df00cd..719bd250 100644 --- a/src/Common/FileUtil.cpp +++ b/src/Common/FileUtil.cpp @@ -54,7 +54,7 @@ bool IsEmpty(const TWideString& rkDirPath) return is_empty(*rkDirPath); } -bool CreateDirectory(const TWideString& rkNewDir) +bool MakeDirectory(const TWideString& rkNewDir) { if (!IsValidPath(rkNewDir, true)) { @@ -73,7 +73,7 @@ bool CopyFile(const TWideString& rkOrigPath, const TWideString& rkNewPath) return false; } - CreateDirectory(rkNewPath.GetFileDirectory()); + MakeDirectory(rkNewPath.GetFileDirectory()); boost::system::error_code Error; copy(*rkOrigPath, *rkNewPath, Error); return (Error == boost::system::errc::success); @@ -87,7 +87,7 @@ bool CopyDirectory(const TWideString& rkOrigPath, const TWideString& rkNewPath) return false; } - CreateDirectory(rkNewPath.GetFileDirectory()); + MakeDirectory(rkNewPath.GetFileDirectory()); boost::system::error_code Error; copy_directory(*rkOrigPath, *rkNewPath, Error); return (Error == boost::system::errc::success); diff --git a/src/Common/FileUtil.h b/src/Common/FileUtil.h index 2b8de34f..a68d3384 100644 --- a/src/Common/FileUtil.h +++ b/src/Common/FileUtil.h @@ -14,7 +14,7 @@ bool IsDirectory(const TWideString& rkDirPath); bool IsAbsolute(const TWideString& rkDirPath); bool IsRelative(const TWideString& rkDirPath); bool IsEmpty(const TWideString& rkDirPath); -bool CreateDirectory(const TWideString& rkNewDir); +bool MakeDirectory(const TWideString& rkNewDir); bool CopyFile(const TWideString& rkOrigPath, const TWideString& rkNewPath); bool CopyDirectory(const TWideString& rkOrigPath, const TWideString& rkNewPath); bool MoveFile(const TWideString& rkOldPath, const TWideString& rkNewPath); diff --git a/src/Core/Core.pro b/src/Core/Core.pro index 3a031b9d..63243c05 100644 --- a/src/Core/Core.pro +++ b/src/Core/Core.pro @@ -29,6 +29,8 @@ CONFIG (debug, debug|release) { -L$$BUILD_DIR/Math/ -lMathd \ -L$$EXTERNALS_DIR/assimp/lib/ -lassimp-vc140-mtd \ -L$$EXTERNALS_DIR/lzo-2.09/lib/ -llzo2d \ + -L$$EXTERNALS_DIR/nodtool/build/debug/lib/ -lnod \ + -L$$EXTERNALS_DIR/nodtool/build/debug/logvisor/ -llogvisor \ -L$$EXTERNALS_DIR/tinyxml2/lib/ -ltinyxml2d \ -L$$EXTERNALS_DIR/zlib/lib/ -lzlibd @@ -51,6 +53,8 @@ CONFIG (release, debug|release) { -L$$BUILD_DIR/Math/ -lMath \ -L$$EXTERNALS_DIR/assimp/lib/ -lassimp-vc140-mt \ -L$$EXTERNALS_DIR/lzo-2.09/lib/ -llzo2 \ + -L$$EXTERNALS_DIR/nodtool/build/release/lib/ -lnod \ + -L$$EXTERNALS_DIR/nodtool/build/release/logvisor -llogvisor \ -L$$EXTERNALS_DIR/tinyxml2/lib/ -ltinyxml2 \ -L$$EXTERNALS_DIR/zlib/lib/ -lzlib @@ -72,6 +76,8 @@ INCLUDEPATH += $$PWE_MAIN_INCLUDE \ $$EXTERNALS_DIR/glew-2.0.0/include \ $$EXTERNALS_DIR/glm/glm \ $$EXTERNALS_DIR/lzo-2.09/include \ + $$EXTERNALS_DIR/nodtool/include \ + $$EXTERNALS_DIR/nodtool/logvisor/include \ $$EXTERNALS_DIR/tinyxml2/include \ $$EXTERNALS_DIR/zlib/include diff --git a/src/Core/GameProject/CAssetNameMap.h b/src/Core/GameProject/CAssetNameMap.h index 0c3916d3..5096abb5 100644 --- a/src/Core/GameProject/CAssetNameMap.h +++ b/src/Core/GameProject/CAssetNameMap.h @@ -9,6 +9,7 @@ #include const TString gkAssetMapPath = "..\\resources\\gameinfo\\AssetNameMap.xml"; +const TString gkAssetMapExt = "xml"; class CAssetNameMap { @@ -36,6 +37,9 @@ public: void SaveAssetNames(TString Path = gkAssetMapPath); bool GetNameInfo(CAssetID ID, TString& rOutDirectory, TString& rOutName); void CopyFromStore(CResourceStore *pStore); + + inline static TString DefaultNameMapPath() { return gkAssetMapPath; } + inline static TString GetExtension() { return gkAssetMapExt; } }; #endif // CASSETNAMEMAP diff --git a/src/Core/GameProject/CGameExporter.cpp b/src/Core/GameProject/CGameExporter.cpp index c42291ab..c68aba71 100644 --- a/src/Core/GameProject/CGameExporter.cpp +++ b/src/Core/GameProject/CGameExporter.cpp @@ -12,65 +12,77 @@ #include #include -#define COPY_DISC_DATA 1 #define LOAD_PAKS 1 #define SAVE_PACKAGE_DEFINITIONS 1 #define USE_ASSET_NAME_MAP 1 #define EXPORT_COOKED 1 -CGameExporter::CGameExporter(const TString& rkInputDir, const TString& rkOutputDir) +CGameExporter::CGameExporter(EGame Game, ERegion Region, const TString& rkGameName, const TString& rkGameID, float BuildVersion) + : mGame(Game) + , mRegion(Region) + , mGameName(rkGameName) + , mGameID(rkGameID) + , mBuildVersion(BuildVersion) { - mGame = eUnknownGame; - mBuildVersion = 0.f; - - mGameDir = FileUtil::MakeAbsolute(rkInputDir); - mExportDir = FileUtil::MakeAbsolute(rkOutputDir); - - mpProject = new CGameProject(mExportDir); - mDiscDir = L"Disc\\"; - mWorldsDirName = L"Worlds\\"; + ASSERT(mGame != eUnknownGame); + ASSERT(mRegion != eRegion_Unknown); } #if PUBLIC_RELEASE #error Fix export directory being cleared! #endif -bool CGameExporter::Export() +bool CGameExporter::Export(nod::DiscBase *pDisc, const TString& rkOutputDir, CAssetNameMap *pNameMap, CGameInfo *pGameInfo) { SCOPED_TIMER(ExportGame); - FileUtil::CreateDirectory(mExportDir); - FileUtil::ClearDirectory(mExportDir); + mpDisc = pDisc; + mpNameMap = pNameMap; + mpGameInfo = pGameInfo; - // Initial analyze/copy of disc data - CopyDiscData(); - FindBuildVersion(); + mExportDir = FileUtil::MakeAbsolute(rkOutputDir); + mDiscDir = L"Disc\\"; + mWorldsDirName = L"Worlds\\"; // Create project - mpProject = new CGameProject(this, mExportDir, mGame, mBuildVersion); - mpProject->SetProjectName(CMasterTemplate::FindGameName(mGame)); + FileUtil::MakeDirectory(mExportDir); + FileUtil::ClearDirectory(mExportDir); + + // Extract disc + if (!ExtractDiscData()) + return false; + + // Create project + CGameProject *pOldActiveProj = CGameProject::ActiveProject(); + + mpProject = CGameProject::CreateProjectForExport( + this, + mExportDir, + mGame, + mRegion, + mGameID, + mBuildVersion, + mDolPath, + mApploaderPath, + mPartitionHeaderPath, + mFilesystemAddress); + + mpProject->SetProjectName(mGameName); mpProject->SetActive(); mpStore = mpProject->ResourceStore(); mContentDir = mpStore->RawDir(false); mCookedDir = mpStore->CookedDir(false); -#if USE_ASSET_NAME_MAP - mNameMap.LoadAssetNames(); -#endif - // Export game data - CResourceStore *pOldStore = gpResourceStore; - gpResourceStore = mpStore; - LoadPaks(); ExportCookedResources(); mpProject->AudioManager()->LoadAssets(); ExportResourceEditorData(); // Export finished! + mProjectPath = mpProject->ProjectPath(); delete mpProject; - gpResourceStore = pOldStore; - + if (pOldActiveProj) pOldActiveProj->SetActive(); return true; } @@ -81,100 +93,85 @@ void CGameExporter::LoadResource(const CAssetID& rkID, std::vector& rBuffer) } // ************ PROTECTED ************ -void CGameExporter::CopyDiscData() +bool CGameExporter::ExtractDiscData() { -#if COPY_DISC_DATA - SCOPED_TIMER(CopyDiscData); + // todo: handle dol, apploader, multiple partitions, wii ticket blob + SCOPED_TIMER(ExtractDiscData); // Create Disc output folder - FileUtil::CreateDirectory(mExportDir + mDiscDir); -#endif + TWideString AbsDiscDir = mExportDir + mDiscDir; + FileUtil::MakeDirectory(AbsDiscDir); - // Copy data - TWideStringList DiscFiles; - FileUtil::GetDirectoryContents(mGameDir, DiscFiles); + // Extract disc filesystem + nod::Partition *pDataPartition = mpDisc->getDataPartition(); + nod::ExtractionContext Context; + Context.force = false; + Context.verbose = false; + Context.progressCB = nullptr; + bool Success = ExtractDiscNodeRecursive(&pDataPartition->getFSTRoot(), AbsDiscDir, Context); + if (!Success) return false; - for (auto It = DiscFiles.begin(); It != DiscFiles.end(); It++) + // Extract dol + mDolPath = L"boot.dol"; + CFileOutStream DolFile(TWideString(mExportDir + mDolPath).ToUTF8().ToStdString()); + if (!DolFile.IsValid()) return false; + + std::unique_ptr pDolBuffer = pDataPartition->getDOLBuf(); + DolFile.WriteBytes(pDolBuffer.get(), (u32) pDataPartition->getDOLSize()); + DolFile.Close(); + + // Extract apploader + mApploaderPath = L"apploader.img"; + CFileOutStream ApploaderFile(TWideString(mExportDir + mApploaderPath).ToUTF8().ToStdString()); + if (!ApploaderFile.IsValid()) return false; + + std::unique_ptr pApploaderBuffer = pDataPartition->getApploaderBuf(); + ApploaderFile.WriteBytes(pApploaderBuffer.get(), (u32) pDataPartition->getApploaderSize()); + ApploaderFile.Close(); + + // Extract Wii partition header + bool IsWii = (mBuildVersion >= 3.f); + + if (IsWii) { - TWideString FullPath = *It; - TWideString RelPath = FullPath.ChopFront(mGameDir.Size()); - - // Exclude PakTool files and folders - if (FullPath.GetFileName(false) == L"PakTool" || FullPath.GetFileName(false) == L"zlib1" || RelPath.Contains(L"-pak")) - continue; - - // Hack to determine game - if (mGame == eUnknownGame) - { - TWideString Name = FullPath.GetFileName(false); - if (Name == L"MetroidCWP") mGame = ePrimeDemo; - else if (Name == L"NESemu") mGame = ePrime; - else if (Name == L"PirateGun") mGame = eEchoesDemo; - else if (Name == L"AtomicBeta") mGame = eEchoes; - else if (Name == L"InGameAudio") mGame = eCorruptionProto; - else if (Name == L"GuiDVD") mGame = eCorruption; - else if (Name == L"PreloadData") mGame = eReturns; - } - - // Mark dol path. Note size != 0 check is needed because some ISO unpackers (*cough* GCRebuilder) can export bad dol files - if (mDolPath.IsEmpty() && FullPath.EndsWith(L".dol", false) && FileUtil::FileSize(FullPath) != 0) - mDolPath = FullPath; - - // Detect paks - if (FullPath.GetFileExtension().ToLower() == L"pak") - mPaks.push_back(FullPath); - -#if COPY_DISC_DATA - // Create directory - TWideString OutFile = mExportDir + mDiscDir + RelPath; - FileUtil::CreateDirectory(OutFile.GetFileDirectory()); - - // Copy file - if (FileUtil::IsFile(FullPath)) - FileUtil::CopyFile(FullPath, OutFile); -#endif + mFilesystemAddress = 0; + mPartitionHeaderPath = L"partition_header.bin"; + nod::DiscWii *pDiscWii = static_cast(mpDisc); + Success = pDiscWii->writeOutDataPartitionHeader(*(mExportDir + mPartitionHeaderPath)); + if (!Success) return false; } + else + mFilesystemAddress = (u32) pDataPartition->getFSTMemoryAddr(); - ASSERT(mGame != eUnknownGame); + return true; } -void CGameExporter::FindBuildVersion() +bool CGameExporter::ExtractDiscNodeRecursive(const nod::Node *pkNode, const TWideString& rkDir, const nod::ExtractionContext& rkContext) { - ASSERT(!mDolPath.IsEmpty()); - - // MP1 demo build doesn't have a build version - if (mGame == ePrimeDemo) return; - - // Read entire file into a big buffer - CFileInStream File(mDolPath.ToUTF8().ToStdString(), IOUtil::eBigEndian); - std::vector FileContents(File.Size()); - File.ReadBytes(FileContents.data(), FileContents.size()); - File.Close(); - - // Find build info string - const char *pkSearchText = "!#$MetroidBuildInfo!#$"; - const int SearchTextSize = strlen(pkSearchText); - - for (u32 SearchIdx = 0; SearchIdx < FileContents.size() - SearchTextSize + 1; SearchIdx++) + for (nod::Node::DirectoryIterator Iter = pkNode->begin(); Iter != pkNode->end(); ++Iter) { - int Match = 0; - - while (FileContents[SearchIdx + Match] == pkSearchText[Match] && Match < SearchTextSize) - Match++; - - if (Match == SearchTextSize) + if (Iter->getKind() == nod::Node::Kind::File) { - // Found the build info string; extract version number - TString BuildInfo = &FileContents[SearchIdx + SearchTextSize]; - int BuildVerStart = BuildInfo.IndexOfPhrase("Build v") + 7; - ASSERT(BuildVerStart != 6); + TWideString FilePath = rkDir + TString(Iter->getName()).ToUTF16(); + bool Success = Iter->extractToDirectory(*rkDir, rkContext); + if (!Success) return false; - mBuildVersion = BuildInfo.SubString(BuildVerStart, 5).ToFloat(); - return; + if (FilePath.GetFileExtension() == L"pak") + mPaks.push_back(FilePath); + } + + else + { + TWideString Subdir = rkDir + TString(Iter->getName()).ToUTF16() + L"\\"; + bool Success = FileUtil::MakeDirectory(Subdir); + if (!Success) return false; + + Success = ExtractDiscNodeRecursive(&*Iter, Subdir, rkContext); + if (!Success) return false; } } - Log::Error("Failed to find MetroidBuildInfo string. Build Version will be set to 0. DOL file: " + mDolPath.ToUTF8()); + return true; } // ************ RESOURCE LOADING ************ @@ -183,10 +180,13 @@ void CGameExporter::LoadPaks() #if LOAD_PAKS SCOPED_TIMER(LoadPaks); + mPaks.sort([](const TWideString& rkLeft, const TWideString& rkRight) -> bool { + return rkLeft.ToUpper() < rkRight.ToUpper(); + }); + for (auto It = mPaks.begin(); It != mPaks.end(); It++) { TWideString PakPath = *It; - TWideString PakName = PakPath.GetFileName(false); TString CharPak = PakPath.ToUTF8(); CFileInStream Pak(CharPak.ToStdString(), IOUtil::eBigEndian); @@ -196,7 +196,7 @@ void CGameExporter::LoadPaks() continue; } - CPackage *pPackage = new CPackage(mpProject, CharPak.GetFileName(false), FileUtil::MakeRelative(PakPath.GetFileDirectory(), mGameDir)); + CPackage *pPackage = new CPackage(mpProject, CharPak.GetFileName(false), FileUtil::MakeRelative(PakPath.GetFileDirectory(), mExportDir + mDiscDir)); CResourceCollection *pCollection = pPackage->AddCollection("Default"); // MP1-MP3Proto @@ -448,7 +448,7 @@ void CGameExporter::ExportCookedResources() { { SCOPED_TIMER(ExportCookedResources); - FileUtil::CreateDirectory(mCookedDir); + FileUtil::MakeDirectory(mCookedDir); for (auto It = mResourceMap.begin(); It != mResourceMap.end(); It++) { @@ -517,13 +517,20 @@ void CGameExporter::ExportResource(SResourceInstance& rRes) // Register resource and write to file TString Directory, Name; - mNameMap.GetNameInfo(rRes.ResourceID, Directory, Name); + +#if USE_ASSET_NAME_MAP + mpNameMap->GetNameInfo(rRes.ResourceID, Directory, Name); +#else + Directory = "Uncategorized"; + Name = rRes.ResourceID.ToString(); +#endif + CResourceEntry *pEntry = mpStore->RegisterResource(rRes.ResourceID, CResTypeInfo::TypeForCookedExtension(mGame, rRes.ResourceType)->Type(), Directory, Name); #if EXPORT_COOKED // Save cooked asset TWideString OutCookedPath = pEntry->CookedAssetPath(); - FileUtil::CreateDirectory(OutCookedPath.GetFileDirectory()); + FileUtil::MakeDirectory(OutCookedPath.GetFileDirectory()); CFileOutStream Out(OutCookedPath.ToUTF8().ToStdString(), IOUtil::eBigEndian); if (Out.IsValid()) diff --git a/src/Core/GameProject/CGameExporter.h b/src/Core/GameProject/CGameExporter.h index 66c76984..5041a313 100644 --- a/src/Core/GameProject/CGameExporter.h +++ b/src/Core/GameProject/CGameExporter.h @@ -10,17 +10,25 @@ #include #include #include +#include class CGameExporter { - // Project + // Project Data CGameProject *mpProject; + TWideString mProjectPath; CResourceStore *mpStore; EGame mGame; + ERegion mRegion; + TString mGameName; + TString mGameID; float mBuildVersion; + TWideString mDolPath; + TWideString mApploaderPath; + TWideString mPartitionHeaderPath; + u32 mFilesystemAddress; // Directories - TWideString mGameDir; TWideString mExportDir; TWideString mDiscDir; TWideString mContentDir; @@ -29,13 +37,13 @@ class CGameExporter TWideString mWorldsDirName; // Files - TWideString mDolPath; + nod::DiscBase *mpDisc; // Resources TWideStringList mPaks; std::map mAreaDuplicateMap; - CAssetNameMap mNameMap; - CGameInfo mGameInfo; + CAssetNameMap *mpNameMap; + CGameInfo *mpGameInfo; struct SResourceInstance { @@ -50,13 +58,15 @@ class CGameExporter std::map mResourceMap; public: - CGameExporter(const TString& rkInputDir, const TString& rkOutputDir); - bool Export(); + CGameExporter(EGame Game, ERegion Region, const TString& rkGameName, const TString& rkGameID, float BuildVersion); + bool Export(nod::DiscBase *pDisc, const TString& rkOutputDir, CAssetNameMap *pNameMap, CGameInfo *pGameInfo); void LoadResource(const CAssetID& rkID, std::vector& rBuffer); + inline TWideString ProjectPath() const { return mProjectPath; } + protected: - void CopyDiscData(); - void FindBuildVersion(); + bool ExtractDiscData(); + bool ExtractDiscNodeRecursive(const nod::Node *pkNode, const TWideString& rkDir, const nod::ExtractionContext& rkContext); void LoadPaks(); void LoadResource(const SResourceInstance& rkResource, std::vector& rBuffer); void ExportCookedResources(); diff --git a/src/Core/GameProject/CGameInfo.cpp b/src/Core/GameProject/CGameInfo.cpp index fa77c766..25558a14 100644 --- a/src/Core/GameProject/CGameInfo.cpp +++ b/src/Core/GameProject/CGameInfo.cpp @@ -1,11 +1,14 @@ #include "CGameInfo.h" +#include void CGameInfo::LoadGameInfo(EGame Game) { Game = RoundGame(Game); mGame = Game; - LoadGameInfo(GetDefaultGameInfoPath(Game)); + TString Path = GetDefaultGameInfoPath(Game); + if (FileUtil::Exists(Path)) + LoadGameInfo(Path); } void CGameInfo::LoadGameInfo(TString Path) @@ -59,5 +62,5 @@ TString CGameInfo::GetDefaultGameInfoPath(EGame Game) return ""; TString GameName = GetGameShortName(Game); - return TString::Format("%s\\GameInfo%s.xml", *gkGameInfoDir, *GameName); + return TString::Format("%s\\GameInfo%s.%s", *gkGameInfoDir, *GameName, *gkGameInfoExt); } diff --git a/src/Core/GameProject/CGameInfo.h b/src/Core/GameProject/CGameInfo.h index ca7b31a5..8a9227bb 100644 --- a/src/Core/GameProject/CGameInfo.h +++ b/src/Core/GameProject/CGameInfo.h @@ -9,6 +9,7 @@ #include const TString gkGameInfoDir = "..\\resources\\gameinfo"; +const TString gkGameInfoExt = "xml"; class CGameInfo { @@ -34,6 +35,8 @@ public: static CGameInfo* GetGameInfo(EGame Game); static EGame RoundGame(EGame Game); static TString GetDefaultGameInfoPath(EGame Game); + + inline static TString GetExtension() { return gkGameInfoExt; } }; #endif // CGAMEINFO diff --git a/src/Core/GameProject/CGameProject.cpp b/src/Core/GameProject/CGameProject.cpp index 683d0eb3..eae5fd55 100644 --- a/src/Core/GameProject/CGameProject.cpp +++ b/src/Core/GameProject/CGameProject.cpp @@ -10,30 +10,16 @@ CGameProject::~CGameProject() ASSERT(!mpResourceStore->IsDirty()); if (IsActive()) + { mspActiveProject = nullptr; + gpResourceStore = nullptr; + } delete mpAudioManager; delete mpGameInfo; delete mpResourceStore; } -bool CGameProject::Load(const TWideString& rkPath) -{ - mProjectRoot = rkPath.GetFileDirectory(); - mProjectRoot.Replace(L"/", L"\\"); - - TString ProjPath = rkPath.ToUTF8(); - CXMLReader Reader(ProjPath); - mGame = Reader.Game(); - Serialize(Reader); - CTemplateLoader::LoadGameTemplates(mGame); - - mpResourceStore->LoadResourceDatabase(); - mpGameInfo->LoadGameInfo(mGame); - mpAudioManager->LoadAssets(); - return true; -} - void CGameProject::Save() { TString ProjPath = ProjectPath().ToUTF8(); @@ -44,8 +30,19 @@ void CGameProject::Save() void CGameProject::Serialize(IArchive& rArc) { rArc << SERIAL("Name", mProjectName) + << SERIAL("Region", mRegion) + << SERIAL("GameID", mGameID) << SERIAL("BuildVersion", mBuildVersion) - << SERIAL("ResourceDB", mResourceDBPath); + << SERIAL("DolPath", mDolPath) + << SERIAL("ApploaderPath", mApploaderPath); + + if (rArc.Game() >= eCorruption) + rArc << SERIAL("PartitionHeaderPath", mPartitionHeaderPath); + + if (!IsWiiBuild()) + rArc << SERIAL("FstAddress", mFilesystemAddress); + + rArc << SERIAL("ResourceDB", mResourceDBPath); // Packages std::vector PackageList; @@ -129,3 +126,52 @@ CAssetID CGameProject::FindNamedResource(const TString& rkName) const return CAssetID::InvalidID(mGame); } + +CGameProject* CGameProject::CreateProjectForExport( + CGameExporter *pExporter, + const TWideString& rkProjRootDir, + EGame Game, + ERegion Region, + const TString& rkGameID, + float BuildVer, + const TWideString& rkDolPath, + const TWideString& rkApploaderPath, + const TWideString& rkPartitionHeaderPath, + u32 FstAddress + ) +{ + CGameProject *pProj = new CGameProject; + pProj->mGame = Game; + pProj->mRegion = Region; + pProj->mGameID = rkGameID; + pProj->mBuildVersion = BuildVer; + pProj->mDolPath = rkDolPath; + pProj->mApploaderPath = rkApploaderPath; + pProj->mPartitionHeaderPath = rkPartitionHeaderPath; + pProj->mFilesystemAddress = FstAddress; + + pProj->mProjectRoot = rkProjRootDir; + pProj->mProjectRoot.Replace(L"/", L"\\"); + pProj->mpResourceStore = new CResourceStore(pProj, pExporter, L"Content\\", L"Cooked\\", Game); + pProj->mpGameInfo->LoadGameInfo(Game); + return pProj; +} + +CGameProject* CGameProject::LoadProject(const TWideString& rkProjPath) +{ + CGameProject *pProj = new CGameProject; + pProj->mProjectRoot = rkProjPath.GetFileDirectory(); + pProj->mProjectRoot.Replace(L"/", L"\\"); + + TString ProjPath = rkProjPath.ToUTF8(); + CXMLReader Reader(ProjPath); + pProj->mGame = Reader.Game(); + pProj->Serialize(Reader); + CTemplateLoader::LoadGameTemplates(pProj->mGame); + + pProj->mpResourceStore = new CResourceStore(pProj); + pProj->mpResourceStore->LoadResourceDatabase(); + pProj->mpGameInfo->LoadGameInfo(pProj->mGame); + pProj->mpAudioManager->LoadAssets(); + return pProj; +} diff --git a/src/Core/GameProject/CGameProject.h b/src/Core/GameProject/CGameProject.h index 09d66047..0b4c2a03 100644 --- a/src/Core/GameProject/CGameProject.h +++ b/src/Core/GameProject/CGameProject.h @@ -14,9 +14,16 @@ class CGameProject { - EGame mGame; - float mBuildVersion; TString mProjectName; + EGame mGame; + ERegion mRegion; + TString mGameID; + float mBuildVersion; + TWideString mDolPath; + TWideString mApploaderPath; + TWideString mPartitionHeaderPath; + u32 mFilesystemAddress; + TWideString mProjectRoot; TWideString mResourceDBPath; std::vector mPackages; @@ -34,52 +41,45 @@ class CGameProject static CGameProject *mspActiveProject; -public: + // Private Constructor CGameProject() - : mGame(eUnknownGame) - , mProjectName("Unnamed Project") + : mProjectName("Unnamed Project") + , mGame(eUnknownGame) + , mRegion(eRegion_Unknown) + , mGameID("000000") + , mBuildVersion(0.f) , mResourceDBPath(L"ResourceDB.rdb") + , mpResourceStore(nullptr) { - mpResourceStore = new CResourceStore(this); mpGameInfo = new CGameInfo(); mpAudioManager = new CAudioManager(this); } - CGameProject(const TWideString& rkProjRootDir) - : mGame(eUnknownGame) - , mProjectName("Unnamed Project") - , mProjectRoot(rkProjRootDir) - , mResourceDBPath(L"ResourceDB.rdb") - { - mProjectRoot.Replace(L"/", L"\\"); - mpResourceStore = new CResourceStore(this); - mpGameInfo = new CGameInfo(); - mpAudioManager = new CAudioManager(this); - } - - CGameProject(CGameExporter *pExporter, const TWideString& rkProjRootDir, EGame Game, float BuildVer) - : mGame(Game) - , mBuildVersion(BuildVer) - , mProjectName(CMasterTemplate::FindGameName(Game)) - , mProjectRoot(rkProjRootDir) - , mResourceDBPath(L"ResourceDB.rdb") - { - mProjectRoot.Replace(L"/", L"\\"); - mpResourceStore = new CResourceStore(this, pExporter, L"Content\\", L"Cooked\\", Game); - mpGameInfo = new CGameInfo(); - mpGameInfo->LoadGameInfo(mGame); - mpAudioManager = new CAudioManager(this); - } - +public: ~CGameProject(); - bool Load(const TWideString& rkPath); void Save(); void Serialize(IArchive& rArc); void SetActive(); void GetWorldList(std::list& rOut) const; CAssetID FindNamedResource(const TString& rkName) const; + // Static + static CGameProject* CreateProjectForExport( + CGameExporter *pExporter, + const TWideString& rkProjRootDir, + EGame Game, + ERegion Region, + const TString& rkGameID, + float BuildVer, + const TWideString& rkDolPath, + const TWideString& rkApploaderPath, + const TWideString& rkPartitionHeaderPath, + u32 FstAddress + ); + + static CGameProject* LoadProject(const TWideString& rkProjPath); + // Directory Handling inline TWideString ProjectRoot() const { return mProjectRoot; } inline TWideString ResourceDBPath(bool Relative) const { return Relative ? mResourceDBPath : mProjectRoot + mResourceDBPath; } @@ -101,6 +101,7 @@ public: inline EGame Game() const { return mGame; } inline float BuildVersion() const { return mBuildVersion; } inline bool IsActive() const { return mspActiveProject == this; } + inline bool IsWiiBuild() const { return mBuildVersion >= 3.f; } static inline CGameProject* ActiveProject() { return mspActiveProject; } }; diff --git a/src/Core/GameProject/CPackage.cpp b/src/Core/GameProject/CPackage.cpp index 51749485..83078924 100644 --- a/src/Core/GameProject/CPackage.cpp +++ b/src/Core/GameProject/CPackage.cpp @@ -20,7 +20,7 @@ void CPackage::Load() void CPackage::Save() { TWideString DefPath = DefinitionPath(false); - FileUtil::CreateDirectory(DefPath.GetFileDirectory()); + FileUtil::MakeDirectory(DefPath.GetFileDirectory()); CXMLWriter Writer(DefPath.ToUTF8(), "PackageDefinition", 0, mpProject ? mpProject->Game() : eUnknownGame); Serialize(Writer); diff --git a/src/Core/GameProject/CResourceEntry.cpp b/src/Core/GameProject/CResourceEntry.cpp index 484680b3..5a99d84f 100644 --- a/src/Core/GameProject/CResourceEntry.cpp +++ b/src/Core/GameProject/CResourceEntry.cpp @@ -190,7 +190,7 @@ bool CResourceEntry::Save(bool SkipCacheSave /*= false*/) // Note: We call Serialize directly for resources to avoid having a redundant resource root node in the output file. TString Path = RawAssetPath(); TString Dir = Path.GetFileDirectory(); - FileUtil::CreateDirectory(Dir.ToUTF16()); + FileUtil::MakeDirectory(Dir.ToUTF16()); TString SerialName = mpTypeInfo->TypeName(); SerialName.RemoveWhitespace(); @@ -268,7 +268,8 @@ CResource* CResourceEntry::LoadCooked(IInputStream& rInput) gpResourceStore = mpStore; mpResource = CResourceFactory::LoadCookedResource(this, rInput); - mpStore->TrackLoadedResource(this); + if (mpResource) + mpStore->TrackLoadedResource(this); gpResourceStore = pOldStore; return mpResource; diff --git a/src/Core/Resource/Animation/CAnimSet.h b/src/Core/Resource/Animation/CAnimSet.h index 05f9d4a2..3e5ed1d7 100644 --- a/src/Core/Resource/Animation/CAnimSet.h +++ b/src/Core/Resource/Animation/CAnimSet.h @@ -81,18 +81,29 @@ class CAnimSet : public CResource std::vector mAnimEvents; // note: these are for MP2, where event data isn't a standalone resource; these are owned by the animset public: - CAnimSet(CResourceEntry *pEntry = 0) : CResource(pEntry) {} + CAnimSet(CResourceEntry *pEntry = 0) + : CResource(pEntry) + , mpDefaultTransition(nullptr) + {} ~CAnimSet() { + for (u32 iAnim = 0; iAnim < mAnimations.size(); iAnim++) + delete mAnimations[iAnim].pMetaAnim; + + for (u32 iTrans = 0; iTrans < mTransitions.size(); iTrans++) + delete mTransitions[iTrans].pMetaTrans; + + for (u32 iHalf = 0; iHalf < mHalfTransitions.size(); iHalf++) + delete mHalfTransitions[iHalf].pMetaTrans; + + delete mpDefaultTransition; + // For MP2, anim events need to be cleaned up manually - if (Game() >= eEchoesDemo) + for (u32 iEvent = 0; iEvent < mAnimEvents.size(); iEvent++) { - for (u32 iEvent = 0; iEvent < mAnimEvents.size(); iEvent++) - { - ASSERT(mAnimEvents[iEvent] && !mAnimEvents[iEvent]->Entry()); - delete mAnimEvents[iEvent]; - } + ASSERT(mAnimEvents[iEvent] && !mAnimEvents[iEvent]->Entry()); + delete mAnimEvents[iEvent]; } } diff --git a/src/Core/Resource/Cooker/CTemplateWriter.cpp b/src/Core/Resource/Cooker/CTemplateWriter.cpp index 858f5ed3..19a8198f 100644 --- a/src/Core/Resource/Cooker/CTemplateWriter.cpp +++ b/src/Core/Resource/Cooker/CTemplateWriter.cpp @@ -42,7 +42,7 @@ void CTemplateWriter::SaveAllTemplates() { // Create directory std::list MasterList = CMasterTemplate::MasterList(); - FileUtil::CreateDirectory(smTemplatesDir); + FileUtil::MakeDirectory(smTemplatesDir); // Resave property list SavePropertyList(); @@ -95,7 +95,7 @@ void CTemplateWriter::SaveGameTemplates(CMasterTemplate *pMaster) // Create directory TString OutFile = smTemplatesDir + pMaster->mSourceFile; TString OutDir = OutFile.GetFileDirectory(); - FileUtil::CreateDirectory(OutDir); + FileUtil::MakeDirectory(OutDir); // Resave script templates for (auto it = pMaster->mTemplates.begin(); it != pMaster->mTemplates.end(); it++) @@ -226,7 +226,7 @@ void CTemplateWriter::SaveScriptTemplate(CScriptTemplate *pTemp) // Create directory TString OutFile = smTemplatesDir + pMaster->GetDirectory() + pTemp->mSourceFile; TString OutDir = OutFile.GetFileDirectory(); - FileUtil::CreateDirectory(*OutDir); + FileUtil::MakeDirectory(*OutDir); // Create new document XMLDocument ScriptXML; @@ -432,7 +432,7 @@ void CTemplateWriter::SaveStructTemplate(CStructTemplate *pTemp) TString OutFile = smTemplatesDir + pMaster->GetDirectory() + pTemp->mSourceFile; TString OutDir = OutFile.GetFileDirectory(); TString Name = OutFile.GetFileName(false); - FileUtil::CreateDirectory(OutDir); + FileUtil::MakeDirectory(OutDir); // Create new document and write struct properties to it XMLDocument StructXML; @@ -456,7 +456,7 @@ void CTemplateWriter::SaveEnumTemplate(CEnumTemplate *pTemp) TString OutFile = smTemplatesDir + pMaster->GetDirectory() + pTemp->mSourceFile; TString OutDir = OutFile.GetFileDirectory(); TString Name = OutFile.GetFileName(false); - FileUtil::CreateDirectory(OutDir); + FileUtil::MakeDirectory(OutDir); // Create new document and write enumerators to it XMLDocument EnumXML; @@ -479,7 +479,7 @@ void CTemplateWriter::SaveBitfieldTemplate(CBitfieldTemplate *pTemp) TString OutFile = smTemplatesDir + pMaster->GetDirectory() + pTemp->mSourceFile; TString OutDir = OutFile.GetFileDirectory(); TString Name = pTemp->mSourceFile.GetFileName(false); - FileUtil::CreateDirectory(OutDir); + FileUtil::MakeDirectory(OutDir); // Create new document and write enumerators to it XMLDocument BitfieldXML; diff --git a/src/Core/Resource/Factory/CAnimSetLoader.cpp b/src/Core/Resource/Factory/CAnimSetLoader.cpp index 5d24e15b..89c63f4b 100644 --- a/src/Core/Resource/Factory/CAnimSetLoader.cpp +++ b/src/Core/Resource/Factory/CAnimSetLoader.cpp @@ -162,7 +162,8 @@ void CAnimSetLoader::ProcessPrimitives() for (u32 iTrans = 0; iTrans < pSet->mTransitions.size(); iTrans++) pSet->mTransitions[iTrans].pMetaTrans->GetUniquePrimitives(UniquePrimitives); - pSet->mpDefaultTransition->GetUniquePrimitives(UniquePrimitives); + if (pSet->mpDefaultTransition) + pSet->mpDefaultTransition->GetUniquePrimitives(UniquePrimitives); for (u32 iTrans = 0; iTrans < pSet->mHalfTransitions.size(); iTrans++) pSet->mHalfTransitions[iTrans].pMetaTrans->GetUniquePrimitives(UniquePrimitives); @@ -181,17 +182,20 @@ void CAnimSetLoader::ProcessPrimitives() } // Add animations referenced by default transition - std::set DefaultTransPrimitives; - pSet->mpDefaultTransition->GetUniquePrimitives(DefaultTransPrimitives); - - for (u32 iChar = 0; iChar < pSet->mCharacters.size(); iChar++) + if (pSet->mpDefaultTransition) { - SSetCharacter& rChar = pSet->mCharacters[iChar]; + std::set DefaultTransPrimitives; + pSet->mpDefaultTransition->GetUniquePrimitives(DefaultTransPrimitives); - for (auto Iter = DefaultTransPrimitives.begin(); Iter != DefaultTransPrimitives.end(); Iter++) + for (u32 iChar = 0; iChar < pSet->mCharacters.size(); iChar++) { - const CAnimPrimitive& rkPrim = *Iter; - rChar.UsedAnimationIndices.insert(rkPrim.ID()); + SSetCharacter& rChar = pSet->mCharacters[iChar]; + + for (auto Iter = DefaultTransPrimitives.begin(); Iter != DefaultTransPrimitives.end(); Iter++) + { + const CAnimPrimitive& rkPrim = *Iter; + rChar.UsedAnimationIndices.insert(rkPrim.ID()); + } } } diff --git a/src/Editor/CEditorApplication.h b/src/Editor/CEditorApplication.h index 6a48ab5d..3dc8d011 100644 --- a/src/Editor/CEditorApplication.h +++ b/src/Editor/CEditorApplication.h @@ -12,6 +12,8 @@ class CResourceEntry; class CWorldEditor; class IEditor; +const int gkTickFrequencyMS = 8; + class CEditorApplication : public QApplication { Q_OBJECT @@ -35,6 +37,9 @@ public: inline CResourceBrowser* ResourceBrowser() const { return mpResourceBrowser; } inline CProjectOverviewDialog* ProjectDialog() const { return mpProjectDialog; } + inline void SetEditorTicksEnabled(bool Enabled) { Enabled ? mRefreshTimer.start(gkTickFrequencyMS) : mRefreshTimer.stop(); } + inline bool AreEditorTicksEnabled() const { return mRefreshTimer.isActive(); } + public slots: void AddEditor(IEditor *pEditor); void TickEditors(); diff --git a/src/Editor/CExportGameDialog.cpp b/src/Editor/CExportGameDialog.cpp new file mode 100644 index 00000000..024491b9 --- /dev/null +++ b/src/Editor/CExportGameDialog.cpp @@ -0,0 +1,360 @@ +#include "CExportGameDialog.h" +#include "ui_CExportGameDialog.h" +#include "UICommon.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +CExportGameDialog::CExportGameDialog(const QString& rkIsoPath, const QString& rkExportDir, QWidget *pParent /*= 0*/) + : QDialog(pParent) + , mpUI(new Ui::CExportGameDialog) + , mGame(eUnknownGame) + , mRegion(eRegion_Unknown) + , mTrilogy(false) + , mExportSuccess(true) +{ + mpUI->setupUi(this); + + // Set up disc + TWideString StrPath = TO_TWIDESTRING(rkIsoPath); + mpDisc = nod::OpenDiscFromImage(*StrPath).release(); + + if (ValidateGame()) + { + mBuildVer = FindBuildVersion(); + InitUI(rkExportDir); + + TString IsoName = TO_TSTRING(rkIsoPath).GetFileName(); + setWindowTitle(QString("Export Settings - %1").arg( TO_QSTRING(IsoName) )); + } + else + { + if (!mTrilogy) + UICommon::ErrorMsg(this, "Invalid ISO!"); + + delete mpDisc; + mpDisc = nullptr; + } +} + +CExportGameDialog::~CExportGameDialog() +{ + delete mpUI; + delete mpDisc; +} + +void RecursiveAddToTree(const nod::Node *pkNode, QTreeWidgetItem *pParent); + +void CExportGameDialog::InitUI(QString ExportDir) +{ + ASSERT(mpDisc != nullptr); + + // Export settings + ExportDir.replace('/', '\\'); + + TWideString DefaultNameMapPath = CAssetNameMap::DefaultNameMapPath(); + if (!FileUtil::Exists(DefaultNameMapPath)) DefaultNameMapPath = L""; + + TWideString DefaultGameInfoPath = CGameInfo::GetDefaultGameInfoPath(mGame); + if (!FileUtil::Exists(DefaultGameInfoPath)) DefaultGameInfoPath = L""; + + mpUI->OutputDirectoryLineEdit->setText(ExportDir); + mpUI->AssetNameMapLineEdit->setText(TO_QSTRING(DefaultNameMapPath)); + mpUI->GameEditorInfoLineEdit->setText(TO_QSTRING(DefaultGameInfoPath)); + + // Info boxes + mpUI->GameTitleLineEdit->setText( TO_QSTRING(mGameTitle) ); + mpUI->GameIdLineEdit->setText( TO_QSTRING(mGameID) ); + mpUI->BuildVersionLineEdit->setText( QString::number(mBuildVer) ); + mpUI->RegionLineEdit->setText( mRegion == eRegion_NTSC ? "NTSC" : + mRegion == eRegion_PAL ? "PAL" : "JPN" ); + + // Disc tree widget + nod::Partition *pPartition = mpDisc->getDataPartition(); + ASSERT(pPartition); + + const nod::Node *pkDiscRoot = &pPartition->getFSTRoot(); + if (mTrilogy) + pkDiscRoot = &*pkDiscRoot->find( GetGameShortName(mGame).ToStdString() ); + + QTreeWidgetItem *pTreeRoot = new QTreeWidgetItem((QTreeWidgetItem*) nullptr, QStringList(QString("Disc"))); + mpUI->DiscFstTreeWidget->addTopLevelItem(pTreeRoot); + RecursiveAddToTree(pkDiscRoot, pTreeRoot); + pTreeRoot->setExpanded(true); + + // Signals and slots + connect(mpUI->OutputDirectoryBrowseButton, SIGNAL(pressed()), this, SLOT(BrowseOutputDirectory())); + connect(mpUI->AssetNameMapBrowseButton, SIGNAL(pressed()), this, SLOT(BrowseAssetNameMap())); + connect(mpUI->GameEditorInfoBrowseButton, SIGNAL(pressed()), this, SLOT(BrowseGameEditorInfo())); + connect(mpUI->CancelButton, SIGNAL(pressed()), this, SLOT(close())); + connect(mpUI->ExportButton, SIGNAL(pressed()), this, SLOT(Export())); +} + +bool CExportGameDialog::ValidateGame() +{ + if (!mpDisc) return false; + + const nod::Header& rkHeader = mpDisc->getHeader(); + mGameTitle = rkHeader.m_gameTitle; + mGameID = TString(6, 0); + memcpy(&mGameID[0], rkHeader.m_gameID, 6); + + // Check region byte + switch (mGameID[3]) + { + case 'E': + mRegion = eRegion_NTSC; + break; + + case 'P': + mRegion = eRegion_PAL; + break; + + case 'J': + mRegion = eRegion_JPN; + break; + + default: + return false; + } + + // Set region byte to X so we don't need to compare every regional variant of the ID + // Then figure out what game this is + CFourCC GameID(&mGameID[0]); + GameID[3] = 'X'; + + switch (GameID.ToLong()) + { + case IFOURCC('GM8X'): + // This ID is normally MP1, but it's used by the MP1 NTSC demo and the MP2 bonus disc demo as well + if (strcmp(rkHeader.m_gameTitle, "Long Game Name") == 0) + { + // todo - not handling demos yet + return false; + } + + mGame = ePrime; + break; + + case IFOURCC('G2MX'): + // Echoes, but also appears in the MP3 proto + if (mGameID[4] == 'A' && mGameID[5] == 'B') + mGame = eCorruptionProto; + else + mGame = eEchoes; + break; + + case IFOURCC('RM3X'): + mGame = eCorruption; + break; + + case IFOURCC('SF8X'): + mGame = eReturns; + break; + + case IFOURCC('R3MX'): + // Trilogy + mTrilogy = true; + if (!RequestTrilogyGame()) return false; + break; + + case IFOURCC('R3IX'): + // MP1 Wii de Asobu + case IFOURCC('R32X'): + // MP2 Wii de Asobu + default: + // Unrecognized game ID + return false; + } + + return true; +} + +bool CExportGameDialog::RequestTrilogyGame() +{ + QDialog Dialog; + Dialog.setWindowTitle("Select Trilogy Game"); + + QLabel Label("You have selected a Metroid Prime: Trilogy ISO. Please pick a game to export:", &Dialog); + QComboBox ComboBox(&Dialog); + ComboBox.addItem("Metroid Prime"); + ComboBox.addItem("Metroid Prime 2: Echoes"); + ComboBox.addItem("Metroid Prime 3: Corruption"); + QDialogButtonBox ButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &Dialog); + connect(&ButtonBox, SIGNAL(accepted()), &Dialog, SLOT(accept())); + connect(&ButtonBox, SIGNAL(rejected()), &Dialog, SLOT(reject())); + + QVBoxLayout Layout; + Layout.addWidget(&Label); + Layout.addWidget(&ComboBox); + Layout.addWidget(&ButtonBox); + Dialog.setLayout(&Layout); + + int Result = Dialog.exec(); + + if (Result == QDialog::Accepted) + { + switch (ComboBox.currentIndex()) + { + case 0: mGame = ePrime; break; + case 1: mGame = eEchoes; break; + case 2: mGame = eCorruption; break; + } + return true; + } + else return false; +} + +float CExportGameDialog::FindBuildVersion() +{ + ASSERT(mpDisc != nullptr); + + // MP1 demo build doesn't have a build version + if (mGame == ePrimeDemo) return 0.f; + + // Get DOL buffer + std::unique_ptr pDolData = mpDisc->getDataPartition()->getDOLBuf(); + u32 DolSize = (u32) mpDisc->getDataPartition()->getDOLSize(); + + // Find build info string + const char *pkSearchText = "!#$MetroidBuildInfo!#$"; + const int SearchTextSize = strlen(pkSearchText); + + for (u32 SearchIdx = 0; SearchIdx < DolSize - SearchTextSize + 1; SearchIdx++) + { + int Match = 0; + + while (pDolData[SearchIdx + Match] == pkSearchText[Match] && Match < SearchTextSize) + Match++; + + if (Match == SearchTextSize) + { + // Found the build info string; extract version number + TString BuildInfo = (char*) &pDolData[SearchIdx + SearchTextSize]; + int BuildVerStart = BuildInfo.IndexOfPhrase("Build v") + 7; + ASSERT(BuildVerStart != 6); + + return BuildInfo.SubString(BuildVerStart, 5).ToFloat(); + } + } + + Log::Error("Failed to find MetroidBuildInfo string. Build Version will be set to 0."); + return 0.f; +} + +void RecursiveAddToTree(const nod::Node *pkNode, QTreeWidgetItem *pParent) +{ + // Get sorted list of nodes + std::list NodeList; + for (nod::Node::DirectoryIterator Iter = pkNode->begin(); Iter != pkNode->end(); ++Iter) + NodeList.push_back(&*Iter); + + NodeList.sort([](const nod::Node *pkLeft, const nod::Node *pkRight) -> bool + { + if (pkLeft->getKind() != pkRight->getKind()) + return pkLeft->getKind() == nod::Node::Kind::Directory; + else + return TString(pkLeft->getName()).ToUpper() < TString(pkRight->getName()).ToUpper(); + }); + + // Add nodes to tree + static const QIcon skFileIcon = QIcon(":/icons/New.png"); + static const QIcon skDirIcon = QIcon(":/icons/Open_24px.png"); + + for (auto Iter = NodeList.begin(); Iter != NodeList.end(); Iter++) + { + const nod::Node *pkNode = *Iter; + bool IsDir = pkNode->getKind() == nod::Node::Kind::Directory; + + QTreeWidgetItem *pItem = new QTreeWidgetItem(pParent, QStringList(QString::fromStdString(pkNode->getName())) ); + pItem->setIcon(0, QIcon(IsDir ? skDirIcon : skFileIcon)); + + if (IsDir) + RecursiveAddToTree(pkNode, pItem); + } +} + +void CExportGameDialog::BrowseOutputDirectory() +{ + QString NewOutputDir = UICommon::OpenDirDialog(this, "Choose export directory"); + if (!NewOutputDir.isEmpty()) mpUI->OutputDirectoryLineEdit->setText(NewOutputDir); +} + +void CExportGameDialog::BrowseAssetNameMap() +{ + QString Filter = "*." + TO_QSTRING(CAssetNameMap::GetExtension()); + QString NewNameMap = UICommon::OpenFileDialog(this, "Choose Asset Name Map", Filter); + if (!NewNameMap.isEmpty()) mpUI->AssetNameMapLineEdit->setText(NewNameMap); +} + +void CExportGameDialog::BrowseGameEditorInfo() +{ + QString Filter = "*." + TO_QSTRING(CGameInfo::GetExtension()); + QString NewGameInfo = UICommon::OpenFileDialog(this, "Choose Game Editor Info", Filter); + if (!NewGameInfo.isEmpty()) mpUI->GameEditorInfoLineEdit->setText(NewGameInfo); +} + +void CExportGameDialog::Export() +{ + QString ExportDir = mpUI->OutputDirectoryLineEdit->text(); + QString NameMapPath = mpUI->AssetNameMapLineEdit->text(); + QString GameInfoPath = mpUI->GameEditorInfoLineEdit->text(); + + // Validate export dir + if (ExportDir.isEmpty()) + { + UICommon::ErrorMsg(this, "Please specify an output directory!"); + return; + } + + else if (!FileUtil::IsEmpty( TO_TSTRING(ExportDir) )) + { + QMessageBox::Button Button = QMessageBox::warning(this, "Warning", "Warning: The specified directory is not empty. Export anyway?", QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::NoButton); + if (Button != QMessageBox::Ok) return; + } + + // Verify name map path and game info path + if (!NameMapPath.isEmpty() && !FileUtil::Exists(TO_TSTRING(NameMapPath))) + { + UICommon::ErrorMsg(this, "The Asset Name Map path is invalid!"); + return; + } + + if (!GameInfoPath.isEmpty() && !FileUtil::Exists(TO_TSTRING(GameInfoPath))) + { + UICommon::ErrorMsg(this, "The Game Editor Info path is invalid!"); + return; + } + + // Do export + close(); + + CAssetNameMap NameMap; + if (!NameMapPath.isEmpty()) + NameMap.LoadAssetNames( TO_TSTRING(NameMapPath) ); + + CGameInfo GameInfo; + if (!GameInfoPath.isEmpty()) + GameInfo.LoadGameInfo( TO_TSTRING(GameInfoPath) ); + + CGameExporter Exporter(mGame, mRegion, mGameTitle, mGameID, mBuildVer); + TString StrExportDir = TO_TSTRING(ExportDir); + StrExportDir.EnsureEndsWith('\\'); + mExportSuccess = Exporter.Export(mpDisc, StrExportDir, &NameMap, &GameInfo); + + if (!mExportSuccess) + UICommon::ErrorMsg(this, "Export failed!"); + else + mNewProjectPath = TO_QSTRING(Exporter.ProjectPath()); +} diff --git a/src/Editor/CExportGameDialog.h b/src/Editor/CExportGameDialog.h new file mode 100644 index 00000000..936b97e5 --- /dev/null +++ b/src/Editor/CExportGameDialog.h @@ -0,0 +1,52 @@ +#ifndef CEXPORTGAMEDIALOG_H +#define CEXPORTGAMEDIALOG_H + +#include +#include +#include +#include +#include + +namespace Ui { +class CExportGameDialog; +} + +class CExportGameDialog : public QDialog +{ + Q_OBJECT + + Ui::CExportGameDialog *mpUI; + nod::DiscBase *mpDisc; + + TString mGameTitle; + TString mGameID; + EGame mGame; + ERegion mRegion; + float mBuildVer; + bool mTrilogy; + + bool mExportSuccess; + QString mNewProjectPath; + +public: + explicit CExportGameDialog(const QString& rkIsoPath, const QString& rkExportDir, QWidget *pParent = 0); + ~CExportGameDialog(); + + void InitUI(QString ExportDir); + bool ValidateGame(); + bool RequestTrilogyGame(); + float FindBuildVersion(); + + // Accessors + inline bool HasValidDisc() const { return mpDisc != nullptr; } + inline bool ExportSucceeded() const { return mExportSuccess; } + inline QString ProjectPath() const { return mNewProjectPath; } + +public slots: + void BrowseOutputDirectory(); + void BrowseAssetNameMap(); + void BrowseGameEditorInfo(); + void Export(); +}; + +#endif // CEXPORTGAMEDIALOG_H diff --git a/src/Editor/CExportGameDialog.ui b/src/Editor/CExportGameDialog.ui new file mode 100644 index 00000000..0f8d4aec --- /dev/null +++ b/src/Editor/CExportGameDialog.ui @@ -0,0 +1,271 @@ + + + CExportGameDialog + + + + 0 + 0 + 339 + 642 + + + + Export Settings + + + + + + + 14 + 75 + true + + + + Game Export Settings + + + + + + + + + Output Directory: + + + + + + + + 1 + 0 + + + + + + + + + + + + 0 + 0 + + + + + 20 + 16777215 + + + + ... + + + + + + + Asset Name Map: + + + + + + + + 1 + 0 + + + + + + + + + + + + 20 + 16777215 + + + + ... + + + + + + + Game Editor Info: + + + + + + + + 1 + 0 + + + + + + + + + + + + 20 + 16777215 + + + + ... + + + + + + + + + Game + + + + + + + + Game Title: + + + + + + + false + + + + + + + Game ID: + + + + + + + false + + + + + + + Region: + + + + + + + false + + + + + + + Build Version: + + + + + + + false + + + + + + + + + QAbstractItemView::NoEditTriggers + + + true + + + QAbstractItemView::ScrollPerPixel + + + 10 + + + false + + + + 1 + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Export + + + true + + + + + + + Cancel + + + + + + + + + + diff --git a/src/Editor/CProjectOverviewDialog.cpp b/src/Editor/CProjectOverviewDialog.cpp index e57c5d91..8a392570 100644 --- a/src/Editor/CProjectOverviewDialog.cpp +++ b/src/Editor/CProjectOverviewDialog.cpp @@ -1,6 +1,7 @@ #include "CProjectOverviewDialog.h" #include "ui_CProjectOverviewDialog.h" #include "CEditorApplication.h" +#include "CExportGameDialog.h" #include "UICommon.h" #include "Editor/ResourceBrowser/CResourceBrowser.h" #include @@ -28,17 +29,13 @@ CProjectOverviewDialog::~CProjectOverviewDialog() delete mpUI; } -void CProjectOverviewDialog::OpenProject() +void CProjectOverviewDialog::InternalLoadProject(const QString& rkPath) { - // Open project file - QString ProjPath = QFileDialog::getOpenFileName(this, "Open Project", "", "Game Project (*.prj)"); - if (ProjPath.isEmpty()) return; - // Load project - TWideString Path = TO_TWIDESTRING(ProjPath); - CGameProject *pNewProj = new CGameProject(Path.GetFileDirectory()); + TWideString Path = TO_TWIDESTRING(rkPath); + CGameProject *pNewProj = CGameProject::LoadProject(Path); - if (pNewProj->Load(Path)) + if (pNewProj) { if (mpProject) delete mpProject; mpProject = pNewProj; @@ -49,49 +46,34 @@ void CProjectOverviewDialog::OpenProject() } else - { Log::Error("Failed to load project"); - delete pNewProj; - } +} + +void CProjectOverviewDialog::OpenProject() +{ + // Open project file + QString ProjPath = UICommon::OpenFileDialog(this, "Open Project", "Game Project (*.prj)"); + if (!ProjPath.isEmpty()) InternalLoadProject(ProjPath); } void CProjectOverviewDialog::ExportGame() { - // TEMP - hardcoded names for convenience. will remove later! -#define USE_HARDCODED_GAME_ROOT 0 -#define USE_HARDCODED_EXPORT_DIR 0 + QString IsoPath = UICommon::OpenFileDialog(this, "Select ISO", "*.iso *.gcm *.tgc *.wbfs"); + if (IsoPath.isEmpty()) return; -#if USE_HARDCODED_GAME_ROOT - QString GameRoot = "E:/Unpacked/Metroid Prime"; -#else - QString GameRoot = QFileDialog::getExistingDirectory(this, "Select game root directory"); - if (GameRoot.isEmpty()) return; -#endif - -#if USE_HARDCODED_EXPORT_DIR - QString ExportDir = "E:/Unpacked/ExportTest"; -#else - QString ExportDir = QFileDialog::getExistingDirectory(this, "Select output export directory"); + QString ExportDir = UICommon::OpenDirDialog(this, "Select output export directory"); 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())) + CExportGameDialog ExportDialog(IsoPath, ExportDir, this); + if (ExportDialog.HasValidDisc()) ExportDialog.exec(); + + if (ExportDialog.ExportSucceeded()) { - QMessageBox::warning(this, "Error", "Error; this is not a valid game root directory!"); - return; - } + int OpenChoice = QMessageBox::information(this, "Export complete", "Export finished successfully! Open new project?", QMessageBox::Yes, QMessageBox::No); - // Verify export directory is empty - if (!FileUtil::IsEmpty(TO_TSTRING(ExportDir))) - { - QMessageBox::Button Button = QMessageBox::warning(this, "Warning", "Warning: The specified directory is not empty. Export anyway?", QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::NoButton); - if (Button != QMessageBox::Ok) return; + if (OpenChoice == QMessageBox::Yes) + InternalLoadProject(ExportDialog.ProjectPath()); } - - CGameExporter Exporter(TO_TSTRING(GameRoot), TO_TSTRING(ExportDir)); - Exporter.Export(); } void CProjectOverviewDialog::SetupWorldsList() diff --git a/src/Editor/CProjectOverviewDialog.h b/src/Editor/CProjectOverviewDialog.h index 78b28ecb..72ef1a1d 100644 --- a/src/Editor/CProjectOverviewDialog.h +++ b/src/Editor/CProjectOverviewDialog.h @@ -24,6 +24,9 @@ public: explicit CProjectOverviewDialog(QWidget *pParent = 0); ~CProjectOverviewDialog(); +protected: + void InternalLoadProject(const QString& rkPath); + public slots: void OpenProject(); void ExportGame(); diff --git a/src/Editor/CStartWindow.cpp b/src/Editor/CStartWindow.cpp index a1debe8e..48bbfef6 100644 --- a/src/Editor/CStartWindow.cpp +++ b/src/Editor/CStartWindow.cpp @@ -7,7 +7,6 @@ #include "Editor/ModelEditor/CModelEditorWindow.h" #include "Editor/WorldEditor/CWorldEditor.h" -#include #include #include @@ -26,7 +25,6 @@ CStartWindow::CStartWindow(QWidget *parent) connect(ui->ActionAbout, SIGNAL(triggered()), this, SLOT(About())); connect(ui->ActionCharacterEditor, SIGNAL(triggered()), this, SLOT(LaunchCharacterEditor())); - connect(ui->ActionExportGame, SIGNAL(triggered()), this, SLOT(ExportGame())); } CStartWindow::~CStartWindow() @@ -245,35 +243,3 @@ void CStartWindow::About() CAboutDialog Dialog(this); Dialog.exec(); } - -void CStartWindow::ExportGame() -{ - // TEMP - hardcoded names for convenience. will remove later! -#define USE_HARDCODED_GAME_ROOT 0 -#define USE_HARDCODED_EXPORT_DIR 1 - -#if USE_HARDCODED_GAME_ROOT - QString GameRoot = "E:/Unpacked/Metroid Prime 2"; -#else - QString GameRoot = QFileDialog::getExistingDirectory(this, "Select game root directory"); - if (GameRoot.isEmpty()) return; -#endif - -#if USE_HARDCODED_EXPORT_DIR - QString ExportDir = "E:/Unpacked/ExportTest"; -#else - QString ExportDir = QFileDialog::getExistingDirectory(this, "Select output export directory"); - 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(); -} diff --git a/src/Editor/CStartWindow.h b/src/Editor/CStartWindow.h index 1758bb81..93ffec4c 100644 --- a/src/Editor/CStartWindow.h +++ b/src/Editor/CStartWindow.h @@ -40,7 +40,6 @@ private slots: void LaunchCharacterEditor(); void About(); - void ExportGame(); private: void FillWorldUI(); diff --git a/src/Editor/Editor.pro b/src/Editor/Editor.pro index 29243fd3..1e5c854a 100644 --- a/src/Editor/Editor.pro +++ b/src/Editor/Editor.pro @@ -35,6 +35,8 @@ CONFIG(debug, debug|release) { -L$$EXTERNALS_DIR/assimp/lib/ -lassimp-vc140-mtd \ -L$$EXTERNALS_DIR/boost_1_63_0/lib64-msvc-14.0 -llibboost_filesystem-vc140-mt-gd-1_63 \ -L$$EXTERNALS_DIR/lzo-2.09/lib/ -llzo2d \ + -L$$EXTERNALS_DIR/nodtool/build/debug/lib/ -lnod \ + -L$$EXTERNALS_DIR/nodtool/build/debug/logvisor/ -llogvisor \ -L$$EXTERNALS_DIR/tinyxml2/lib/ -ltinyxml2d \ -L$$EXTERNALS_DIR/zlib/lib/ -lzlibd @@ -62,6 +64,8 @@ CONFIG(release, debug|release) { -L$$EXTERNALS_DIR/assimp/lib/ -lassimp-vc140-mt \ -L$$EXTERNALS_DIR/boost_1_63_0/lib64-msvc-14.0 -llibboost_filesystem-vc140-mt-1_63 \ -L$$EXTERNALS_DIR/lzo-2.09/lib/ -llzo2 \ + -L$$EXTERNALS_DIR/nodtool/build/release/lib/ -lnod \ + -L$$EXTERNALS_DIR/nodtool/build/release/logvisor -llogvisor \ -L$$EXTERNALS_DIR/tinyxml2/lib/ -ltinyxml2 \ -L$$EXTERNALS_DIR/zlib/lib/ -lzlib @@ -84,6 +88,8 @@ INCLUDEPATH += $$PWE_MAIN_INCLUDE \ $$EXTERNALS_DIR/glew-2.0.0/include \ $$EXTERNALS_DIR/glm/glm \ $$EXTERNALS_DIR/lzo-2.09/include \ + $$EXTERNALS_DIR/nodtool/include \ + $$EXTERNALS_DIR/nodtool/logvisor/include \ $$EXTERNALS_DIR/tinyxml2/include \ $$EXTERNALS_DIR/zlib/include @@ -174,7 +180,8 @@ HEADERS += \ ResourceBrowser/CVirtualDirectoryModel.h \ CEditorApplication.h \ IEditor.h \ - Widgets/CResourceSelector.h + Widgets/CResourceSelector.h \ + CExportGameDialog.h # Source Files SOURCES += \ @@ -238,7 +245,8 @@ SOURCES += \ CProjectOverviewDialog.cpp \ ResourceBrowser/CResourceBrowser.cpp \ CEditorApplication.cpp \ - Widgets/CResourceSelector.cpp + Widgets/CResourceSelector.cpp \ + CExportGameDialog.cpp # UI Files FORMS += \ @@ -262,4 +270,5 @@ FORMS += \ CharacterEditor/CCharacterEditor.ui \ WorldEditor/CCollisionRenderSettingsDialog.ui \ CProjectOverviewDialog.ui \ - ResourceBrowser/CResourceBrowser.ui + ResourceBrowser/CResourceBrowser.ui \ + CExportGameDialog.ui diff --git a/src/Editor/ResourceBrowser/CResourceBrowser.cpp b/src/Editor/ResourceBrowser/CResourceBrowser.cpp index 4846a0be..4f48cacd 100644 --- a/src/Editor/ResourceBrowser/CResourceBrowser.cpp +++ b/src/Editor/ResourceBrowser/CResourceBrowser.cpp @@ -292,7 +292,7 @@ void CResourceBrowser::OnResourceSelectionChanged(const QModelIndex& rkNewIndex, void CResourceBrowser::OnImportPakContentsTxt() { - QStringList PathList = QFileDialog::getOpenFileNames(this, "Open pak contents list", "", "*.pak.contents.txt"); + QStringList PathList = UICommon::OpenFilesDialog(this, "Open pak contents list", "*.pak.contents.txt"); if (PathList.isEmpty()) return; foreach(const QString& rkPath, PathList) @@ -326,7 +326,7 @@ void CResourceBrowser::OnImportNamesFromAssetNameMap() void CResourceBrowser::ExportAssetNames() { - QString OutFile = QFileDialog::getSaveFileName(this, "Export asset name map", "../resources/gameinfo/", "*.xml"); + QString OutFile = UICommon::SaveFileDialog(this, "Export asset name map", "*.xml", "../resources/gameinfo/"); if (OutFile.isEmpty()) return; CAssetNameMap NameMap; diff --git a/src/Editor/UICommon.h b/src/Editor/UICommon.h index 430217b9..65774903 100644 --- a/src/Editor/UICommon.h +++ b/src/Editor/UICommon.h @@ -1,8 +1,11 @@ #ifndef UICOMMON #define UICOMMON +#include "CEditorApplication.h" #include +#include #include +#include #include // App string variable handling - automatically fill in application name/version @@ -57,7 +60,54 @@ inline TWideString ToTWideString(const QString& rkStr) { return TWideString(rkStr.toStdWString()); } + +// QFileDialog wrappers +// Note: pause editor ticks while file dialogs are open because otherwise there's a bug that makes it really difficult to tab out and back in +#define PUSH_TICKS_ENABLED \ + bool TicksEnabled = gpEdApp->AreEditorTicksEnabled(); \ + gpEdApp->SetEditorTicksEnabled(false); +#define POP_TICKS_ENABLED \ + gpEdApp->SetEditorTicksEnabled(TicksEnabled); + +inline QString OpenFileDialog(QWidget *pParent, const QString& rkCaption, const QString& rkFilter, const QString& rkStartingDir = "") +{ + PUSH_TICKS_ENABLED; + QString Result = QFileDialog::getOpenFileName(pParent, rkCaption, rkStartingDir, rkFilter); + POP_TICKS_ENABLED; + return Result; } +inline QStringList OpenFilesDialog(QWidget *pParent, const QString& rkCaption, const QString& rkFilter, const QString& rkStartingDir = "") +{ + PUSH_TICKS_ENABLED; + QStringList Result = QFileDialog::getOpenFileNames(pParent, rkCaption, rkStartingDir, rkFilter); + POP_TICKS_ENABLED; + return Result; +} + +inline QString SaveFileDialog(QWidget *pParent, const QString& rkCaption, const QString& rkFilter, const QString& rkStartingDir = "") +{ + PUSH_TICKS_ENABLED; + QString Result = QFileDialog::getSaveFileName(pParent, rkCaption, rkStartingDir, rkFilter); + POP_TICKS_ENABLED; + return Result; +} + +inline QString OpenDirDialog(QWidget *pParent, const QString& rkCaption, const QString& rkStartingDir = "") +{ + PUSH_TICKS_ENABLED; + QString Result = QFileDialog::getExistingDirectory(pParent, rkCaption, rkStartingDir); + POP_TICKS_ENABLED; + return Result; +} + +// QMessageBox wrappers +inline void ErrorMsg(QWidget *pParent, QString ErrorText) +{ + QMessageBox::warning(pParent, "Error", ErrorText); +} + +} // UICommon Namespace End + #endif // UICOMMON diff --git a/src/Editor/Widgets/WResourceSelector.cpp b/src/Editor/Widgets/WResourceSelector.cpp index 23b25860..8a08ab25 100644 --- a/src/Editor/Widgets/WResourceSelector.cpp +++ b/src/Editor/Widgets/WResourceSelector.cpp @@ -215,7 +215,7 @@ void WResourceSelector::OnBrowseButtonClicked() Filter += UICommon::ExtensionFilterString(mSupportedExtensions[iExt]); } - QString NewRes = QFileDialog::getOpenFileName(this, "Select resource", "", Filter); + QString NewRes = UICommon::OpenFileDialog(this, "Select resource", Filter); if (!NewRes.isEmpty()) { diff --git a/src/Editor/WorldEditor/CRepackInfoDialog.cpp b/src/Editor/WorldEditor/CRepackInfoDialog.cpp index 686bf801..635ad9e4 100644 --- a/src/Editor/WorldEditor/CRepackInfoDialog.cpp +++ b/src/Editor/WorldEditor/CRepackInfoDialog.cpp @@ -63,7 +63,7 @@ QString CRepackInfoDialog::OutputPak() const // ************ PUBLIC SLOTS ************ void CRepackInfoDialog::BrowseFolderClicked() { - QString Folder = QFileDialog::getExistingDirectory(this, "Choose directory"); + QString Folder = UICommon::OpenDirDialog(this, "Choose directory"); if (!Folder.isEmpty()) { @@ -74,7 +74,7 @@ void CRepackInfoDialog::BrowseFolderClicked() void CRepackInfoDialog::BrowseListClicked() { - QString List = QFileDialog::getOpenFileName(this, "Open list file", "", "All supported files (*.txt *.pak);;Text file (*.txt);;Pak file (*.pak)"); + QString List = UICommon::OpenFileDialog(this, "Open list file", "All supported files (*.txt *.pak);;Text file (*.txt);;Pak file (*.pak)"); if (!List.isEmpty()) { @@ -94,7 +94,7 @@ void CRepackInfoDialog::BrowseListClicked() void CRepackInfoDialog::BrowseOutPakClicked() { - QString Pak = QFileDialog::getSaveFileName(this, "Save pak", "", "Pak File (*.pak)"); + QString Pak = UICommon::SaveFileDialog(this, "Save pak", "Pak File (*.pak)"); if (!Pak.isEmpty()) {