Merge branch 'StringEditing'

This commit is contained in:
Aruki 2019-02-03 03:32:34 -07:00
commit 1baa48de34
770 changed files with 50176 additions and 4586 deletions

2
externals/LibCommon vendored

@ -1 +1 @@
Subproject commit 47d83075e0fb20786ad28c2e945a204ed1837856
Subproject commit 3c6a40742551d7afd0737d1293d036df69f34ec6

View File

@ -26,6 +26,7 @@ about
above
absolute
absorb
accel
acceleration
accumulate
achievement
@ -962,6 +963,7 @@ orb
orbit
orbitable
order
organic
orient
orientation
origin
@ -1260,6 +1262,8 @@ shoulder
show
shredder
shrink
shrub
shrubbery
shut
shutdown
side
@ -1555,6 +1559,7 @@ while
whip
white
widget
width
window
wing
with

View File

@ -109,6 +109,7 @@ HEADERS += \
Resource/Script/CScriptObject.h \
Resource/Script/CScriptTemplate.h \
Resource/Script/EVolumeShape.h \
Resource/StringTable/CStringTable.h \
Resource/CCollisionMesh.h \
Resource/CCollisionMeshGroup.h \
Resource/CFont.h \
@ -117,8 +118,6 @@ HEADERS += \
Resource/CMaterialPass.h \
Resource/CMaterialSet.h \
Resource/CResource.h \
Resource/CScan.h \
Resource/CStringTable.h \
Resource/CTexture.h \
Resource/CWorld.h \
Resource/EResType.h \
@ -246,7 +245,18 @@ HEADERS += \
Resource/Script/Property/CGuidProperty.h \
Resource/Script/CGameTemplate.h \
Resource/Script/NPropertyMap.h \
Resource/Script/NGameList.h
Resource/Script/NGameList.h \
Resource/StringTable/ELanguage.h \
Tweaks/CTweakManager.h \
Tweaks/CTweakData.h \
Tweaks/CTweakLoader.h \
Tweaks/CTweakCooker.h \
Resource/Cooker/CStringCooker.h \
Resource/Scan/CScan.h \
Resource/Scan/SScanParametersMP1.h \
Resource/Scan/ELogbookCategory.h \
Resource/Cooker/CScanCooker.h \
NCoreTests.h
# Source Files
SOURCES += \
@ -357,7 +367,15 @@ SOURCES += \
Resource/Script/Property/CFlagsProperty.cpp \
Resource/Script/CGameTemplate.cpp \
Resource/Script/NPropertyMap.cpp \
Resource/Script/NGameList.cpp
Resource/Script/NGameList.cpp \
Resource/StringTable/CStringTable.cpp \
Tweaks/CTweakManager.cpp \
Tweaks/CTweakLoader.cpp \
Tweaks/CTweakCooker.cpp \
Resource/Cooker/CStringCooker.cpp \
Resource/Scan/CScan.cpp \
Resource/Cooker/CScanCooker.cpp \
NCoreTests.cpp
# Codegen
CODEGEN_DIR = $$EXTERNALS_DIR/CodeGen

View File

@ -3,9 +3,10 @@
#include "CResourceIterator.h"
#include "Core/Resource/CAudioMacro.h"
#include "Core/Resource/CFont.h"
#include "Core/Resource/CScan.h"
#include "Core/Resource/CWorld.h"
#include "Core/Resource/Animation/CAnimSet.h"
#include "Core/Resource/Scan/CScan.h"
#include "Core/Resource/Scan/SScanParametersMP1.h"
#include "Core/Resource/Script/CScriptLayer.h"
#include <Common/Math/MathUtil.h>
@ -311,10 +312,15 @@ void GenerateAssetNames(CGameProject *pProj)
ApplyGeneratedName(pEntry, pEntry->DirectoryPath(), ScanName);
CScan *pScan = (CScan*) pEntry->Load();
if (pScan && pScan->ScanText())
if (pScan)
{
CResourceEntry *pStringEntry = pScan->ScanText()->Entry();
ApplyGeneratedName(pStringEntry, pStringEntry->DirectoryPath(), ScanName);
CAssetID StringID = pScan->ScanStringPropertyRef();
CResourceEntry* pStringEntry = gpResourceStore->FindEntry(StringID);
if (pStringEntry)
{
ApplyGeneratedName(pStringEntry, pStringEntry->DirectoryPath(), ScanName);
}
}
}
}
@ -585,7 +591,7 @@ void GenerateAssetNames(CGameProject *pProj)
TString String;
for (uint32 iStr = 0; iStr < pString->NumStrings() && String.IsEmpty(); iStr++)
String = CStringTable::StripFormatting( pString->String("ENGL", iStr) ).Trimmed();
String = CStringTable::StripFormatting( pString->GetString(ELanguage::English, iStr) ).Trimmed();
if (!String.IsEmpty())
{
@ -609,16 +615,10 @@ void GenerateAssetNames(CGameProject *pProj)
CScan *pScan = (CScan*) It->Load();
TString ScanName;
if (pProj->Game() >= EGame::EchoesDemo)
{
CAssetID DisplayAsset = pScan->LogbookDisplayAssetID();
CResourceEntry *pEntry = pStore->FindEntry(DisplayAsset);
if (pEntry && pEntry->IsNamed()) ScanName = pEntry->Name();
}
if (ScanName.IsEmpty())
{
CStringTable *pString = pScan->ScanText();
CAssetID StringID = pScan->ScanStringPropertyRef().Get();
CStringTable *pString = (CStringTable*) gpResourceStore->LoadResource(StringID, EResourceType::StringTable);
if (pString) ScanName = pString->Entry()->Name();
}
@ -626,13 +626,14 @@ void GenerateAssetNames(CGameProject *pProj)
if (!ScanName.IsEmpty() && pProj->Game() <= EGame::Prime)
{
CAssetID FrameID = pScan->GuiFrame();
CResourceEntry *pEntry = pStore->FindEntry(FrameID);
const SScanParametersMP1& kParms = *static_cast<SScanParametersMP1*>(pScan->ScanData().DataPointer());
CResourceEntry *pEntry = pStore->FindEntry(kParms.GuiFrame);
if (pEntry) ApplyGeneratedName(pEntry, pEntry->DirectoryPath(), "ScanFrame");
for (uint32 iImg = 0; iImg < 4; iImg++)
{
CAssetID ImageID = pScan->ScanImage(iImg);
CAssetID ImageID = kParms.ScanImages[iImg].Texture;
CResourceEntry *pImgEntry = pStore->FindEntry(ImageID);
if (pImgEntry) ApplyGeneratedName(pImgEntry, pImgEntry->DirectoryPath(), TString::Format("%s_Image%d", *ScanName, iImg));
}

View File

@ -30,6 +30,70 @@ void IDependencyNode::GetAllResourceReferences(std::set<CAssetID>& rOutSet) cons
mChildren[iChild]->GetAllResourceReferences(rOutSet);
}
void IDependencyNode::ParseProperties(CResourceEntry* pParentEntry, CStructProperty* pProperties, void* pData)
{
// Recursive function for parsing dependencies in properties
for (uint32 PropertyIdx = 0; PropertyIdx < pProperties->NumChildren(); PropertyIdx++)
{
IProperty* pProp = pProperties->ChildByIndex(PropertyIdx);
EPropertyType Type = pProp->Type();
// Technically we aren't parsing array children, but it's not really worth refactoring this function
// to support it when there aren't any array properties that contain any asset references anyway...
if (Type == EPropertyType::Struct)
ParseProperties( pParentEntry, TPropCast<CStructProperty>(pProp), pData );
else if (Type == EPropertyType::Sound)
{
uint32 SoundID = TPropCast<CSoundProperty>(pProp)->Value(pData);
if (SoundID != -1)
{
CGameProject* pProj = pParentEntry->Project();
SSoundInfo Info = pProj->AudioManager()->GetSoundInfo(SoundID);
if (Info.pAudioGroup)
{
CPropertyDependency *pDep = new CPropertyDependency(pProp->IDString(true), Info.pAudioGroup->ID());
mChildren.push_back(pDep);
}
}
}
else if (Type == EPropertyType::Asset)
{
CAssetID ID = TPropCast<CAssetProperty>(pProp)->Value(pData);
if (ID.IsValid())
{
CPropertyDependency *pDep = new CPropertyDependency(pProp->IDString(true), ID);
mChildren.push_back(pDep);
}
}
else if (Type == EPropertyType::AnimationSet)
{
CAnimationParameters Params = TPropCast<CAnimationSetProperty>(pProp)->Value(pData);
CAssetID ID = Params.ID();
if (ID.IsValid())
{
// Character sets are removed starting in MP3, so we only need char property dependencies in Echoes and earlier
if (pProperties->Game() <= EGame::Echoes)
{
CCharPropertyDependency *pDep = new CCharPropertyDependency(pProp->IDString(true), ID, Params.CharacterIndex());
mChildren.push_back(pDep);
}
else
{
CPropertyDependency *pDep = new CPropertyDependency(pProp->IDString(true), ID);
mChildren.push_back(pDep);
}
}
}
}
}
// Serialization constructor
IDependencyNode* IDependencyNode::ArchiveConstructor(EDependencyNodeType Type)
{
@ -149,76 +213,10 @@ CScriptInstanceDependency* CScriptInstanceDependency::BuildTree(CScriptObject *p
{
CScriptInstanceDependency *pInst = new CScriptInstanceDependency();
pInst->mObjectType = pInstance->ObjectTypeID();
ParseStructDependencies(pInst, pInstance, pInstance->Template()->Properties());
pInst->ParseProperties(pInstance->Area()->Entry(), pInstance->Template()->Properties(), pInstance->PropertyData());
return pInst;
}
void CScriptInstanceDependency::ParseStructDependencies(CScriptInstanceDependency* pInst, CScriptObject* pInstance, CStructProperty *pStruct)
{
// Recursive function for parsing script dependencies and loading them into the script instance dependency
void* pPropertyData = pInstance->PropertyData();
for (uint32 PropertyIdx = 0; PropertyIdx < pStruct->NumChildren(); PropertyIdx++)
{
IProperty *pProp = pStruct->ChildByIndex(PropertyIdx);
EPropertyType Type = pProp->Type();
// Technically we aren't parsing array children, but it's not really worth refactoring this function
// to support it when there aren't any array properties that contain any asset references anyway...
if (Type == EPropertyType::Struct)
ParseStructDependencies(pInst, pInstance, TPropCast<CStructProperty>(pProp));
else if (Type == EPropertyType::Sound)
{
uint32 SoundID = TPropCast<CSoundProperty>(pProp)->Value(pPropertyData);
if (SoundID != -1)
{
CGameProject *pProj = pInstance->Area()->Entry()->Project();
SSoundInfo Info = pProj->AudioManager()->GetSoundInfo(SoundID);
if (Info.pAudioGroup)
{
CPropertyDependency *pDep = new CPropertyDependency(pProp->IDString(true), Info.pAudioGroup->ID());
pInst->mChildren.push_back(pDep);
}
}
}
else if (Type == EPropertyType::Asset)
{
CAssetID ID = TPropCast<CAssetProperty>(pProp)->Value(pPropertyData);
if (ID.IsValid())
{
CPropertyDependency *pDep = new CPropertyDependency(pProp->IDString(true), ID);
pInst->mChildren.push_back(pDep);
}
}
else if (Type == EPropertyType::AnimationSet)
{
CAnimationParameters Params = TPropCast<CAnimationSetProperty>(pProp)->Value(pPropertyData);
CAssetID ID = Params.ID();
if (ID.IsValid())
{
// Character sets are removed starting in MP3, so we only need char property dependencies in Echoes and earlier
if (pStruct->Game() <= EGame::Echoes)
{
CCharPropertyDependency *pDep = new CCharPropertyDependency(pProp->IDString(true), ID, Params.CharacterIndex());
pInst->mChildren.push_back(pDep);
}
else
{
CPropertyDependency *pDep = new CPropertyDependency(pProp->IDString(true), ID);
pInst->mChildren.push_back(pDep);
}
}
}
}
}
// ************ CSetCharacterDependency ************
EDependencyNodeType CSetCharacterDependency::Type() const
{

View File

@ -39,6 +39,7 @@ public:
virtual void Serialize(IArchive& rArc) = 0;
virtual void GetAllResourceReferences(std::set<CAssetID>& rOutSet) const;
virtual bool HasDependency(const CAssetID& rkID) const;
void ParseProperties(CResourceEntry* pParentEntry, CStructProperty* pProperties, void* pData);
// Serialization constructor
static IDependencyNode* ArchiveConstructor(EDependencyNodeType Type);
@ -144,8 +145,6 @@ public:
// Static
static CScriptInstanceDependency* BuildTree(CScriptObject *pInstance);
protected:
static void ParseStructDependencies(CScriptInstanceDependency *pTree, CScriptObject* pInstance, CStructProperty *pStruct);
};
// Node representing an animset character. Indicates what index the character is within the animset.

View File

@ -198,16 +198,16 @@ bool CGameExporter::ExtractDiscData()
if (IsWii)
{
// Extract crypto files
if (!pDataPartition->extractCryptoFiles(*AbsDiscDir.ToUTF16(), Context))
if (!pDataPartition->extractCryptoFiles(ToWChar(AbsDiscDir), Context))
return false;
// Extract disc header files
if (!mpDisc->extractDiscHeaderFiles(*AbsDiscDir.ToUTF16(), Context))
if (!mpDisc->extractDiscHeaderFiles(ToWChar(AbsDiscDir), Context))
return false;
}
// Extract system files
if (!pDataPartition->extractSysFiles(*AbsDiscDir.ToUTF16(), Context))
if (!pDataPartition->extractSysFiles(ToWChar(AbsDiscDir), Context))
return false;
return true;
@ -226,7 +226,7 @@ bool CGameExporter::ExtractDiscNodeRecursive(const nod::Node *pkNode, const TStr
if (Iter->getKind() == nod::Node::Kind::File)
{
TString FilePath = rkDir + Iter->getName().data();
bool Success = Iter->extractToDirectory(*rkDir.ToUTF16(), rkContext);
bool Success = Iter->extractToDirectory(ToWChar(rkDir), rkContext);
if (!Success) return false;
if (FilePath.GetFileExtension().CaseInsensitiveCompare("pak"))
@ -626,7 +626,7 @@ void CGameExporter::ExportResource(SResourceInstance& rRes)
Name = rRes.ResourceID.ToString();
#endif
CResourceEntry *pEntry = mpStore->RegisterResource(rRes.ResourceID, CResTypeInfo::TypeForCookedExtension(mGame, rRes.ResourceType)->Type(), Directory, Name);
CResourceEntry *pEntry = mpStore->CreateNewResource(rRes.ResourceID, CResTypeInfo::TypeForCookedExtension(mGame, rRes.ResourceType)->Type(), Directory, Name);
// Set flags
pEntry->SetFlag(EResEntryFlag::IsBaseGameResource);

View File

@ -1,4 +1,5 @@
#include "CGameProject.h"
#include "CResourceIterator.h"
#include "IUIRelay.h"
#include "Core/Resource/Script/CGameTemplate.h"
#include <Common/Serialization/XML.h>
@ -10,16 +11,12 @@ CGameProject::~CGameProject()
{
ASSERT(!mpResourceStore->IsCacheDirty());
if (gpResourceStore == mpResourceStore)
if (gpResourceStore == mpResourceStore.get())
gpResourceStore = nullptr;
}
for (uint32 iPkg = 0; iPkg < mPackages.size(); iPkg++)
delete mPackages[iPkg];
delete mpAudioManager;
delete mpGameInfo;
delete mpResourceStore;
}
bool CGameProject::Save()
@ -83,21 +80,21 @@ bool CGameProject::BuildISO(const TString& rkIsoPath, IProgressNotifier *pProgre
auto ProgressCallback = [&](float ProgressPercent, const nod::SystemStringView& rkInfoString, size_t)
{
pProgress->Report((int) (ProgressPercent * 10000), 10000, TWideString(rkInfoString.data()).ToUTF8());
pProgress->Report((int) (ProgressPercent * 10000), 10000, nod::SystemUTF8Conv(rkInfoString).c_str());
};
pProgress->SetTask(0, "Building " + rkIsoPath.GetFileName());
TWideString DiscRoot = DiscDir(false).ToUTF16();
TString DiscRoot = DiscDir(false);
if (!IsWiiBuild())
{
nod::DiscBuilderGCN Builder(*rkIsoPath.ToUTF16(), ProgressCallback);
return Builder.buildFromDirectory(*DiscRoot) == nod::EBuildResult::Success;
nod::DiscBuilderGCN Builder(ToWChar(rkIsoPath), ProgressCallback);
return Builder.buildFromDirectory(ToWChar(DiscRoot)) == nod::EBuildResult::Success;
}
else
{
nod::DiscBuilderWii Builder(*rkIsoPath.ToUTF16(), IsTrilogy(), ProgressCallback);
return Builder.buildFromDirectory(*DiscRoot) == nod::EBuildResult::Success;
nod::DiscBuilderWii Builder(ToWChar(rkIsoPath), IsTrilogy(), ProgressCallback);
return Builder.buildFromDirectory(ToWChar(DiscRoot)) == nod::EBuildResult::Success;
}
}
@ -109,15 +106,15 @@ bool CGameProject::MergeISO(const TString& rkIsoPath, nod::DiscWii *pOriginalIso
auto ProgressCallback = [&](float ProgressPercent, const nod::SystemStringView& rkInfoString, size_t)
{
pProgress->Report((int) (ProgressPercent * 10000), 10000, TWideString(rkInfoString.data()).ToUTF8());
pProgress->Report((int) (ProgressPercent * 10000), 10000, nod::SystemUTF8Conv(rkInfoString).c_str());
};
pProgress->SetTask(0, "Building " + rkIsoPath.GetFileName());
TWideString DiscRoot = DiscFilesystemRoot(false).ToUTF16();
TString DiscRoot = DiscFilesystemRoot(false);
nod::DiscMergerWii Merger(*rkIsoPath.ToUTF16(), *pOriginalIso, IsTrilogy(), ProgressCallback);
return Merger.mergeFromDirectory(*DiscRoot) == nod::EBuildResult::Success;
nod::DiscMergerWii Merger(ToWChar(rkIsoPath), *pOriginalIso, IsTrilogy(), ProgressCallback);
return Merger.mergeFromDirectory(ToWChar(DiscRoot)) == nod::EBuildResult::Success;
}
void CGameProject::GetWorldList(std::list<CAssetID>& rOut) const
@ -200,7 +197,7 @@ CGameProject* CGameProject::CreateProjectForExport(
pProj->mProjectRoot = rkProjRootDir;
pProj->mProjectRoot.Replace("\\", "/");
pProj->mpResourceStore = new CResourceStore(pProj);
pProj->mpResourceStore = std::make_unique<CResourceStore>(pProj);
pProj->mpGameInfo->LoadGameInfo(Game);
return pProj;
}
@ -234,10 +231,14 @@ CGameProject* CGameProject::LoadProject(const TString& rkProjPath, IProgressNoti
{
// Load resource database
pProgress->Report("Loading resource database");
pProj->mpResourceStore = new CResourceStore(pProj);
pProj->mpResourceStore = std::make_unique<CResourceStore>(pProj);
LoadSuccess = pProj->mpResourceStore->LoadDatabaseCache();
// Validate resource database
// Removed database validation step. We used to do this on project load to make sure all data was correct, but this takes a long
// time and significantly extends how long it takes to open a project. In actual practice, this isn't needed most of the time, and
// in the odd case that it is needed, there is a button in the resource browser to rebuild the database. So in the interest of
// making project startup faster, we no longer validate the database.
#if 0
if (LoadSuccess)
{
pProgress->Report("Validating resource database");
@ -257,6 +258,7 @@ CGameProject* CGameProject::LoadProject(const TString& rkProjPath, IProgressNoti
LoadSuccess = false;
}
}
#endif
}
if (!LoadSuccess)
@ -267,6 +269,44 @@ CGameProject* CGameProject::LoadProject(const TString& rkProjPath, IProgressNoti
pProj->mProjFileLock.Lock(ProjPath);
pProj->mpGameInfo->LoadGameInfo(pProj->mGame);
// Perform update
if (Reader.FileVersion() < (uint16) EProjectVersion::Current)
{
pProgress->Report("Updating project");
CResourceStore* pOldStore = gpResourceStore;
gpResourceStore = pProj->mpResourceStore.get();
for (CResourceIterator It; It; ++It)
{
if (It->TypeInfo()->CanBeSerialized() && !It->HasRawVersion())
{
It->Save(true, false);
// Touch the cooked file to update its last modified time.
// This prevents PWE from erroneously thinking the cooked file is outdated
// (due to the raw file we just made having a more recent last modified time)
FileUtil::UpdateLastModifiedTime( It->CookedAssetPath() );
}
}
pProj->mpResourceStore->ConditionalSaveStore();
pProj->Save();
gpResourceStore = pOldStore;
}
// Create hidden files directory, if needed
TString HiddenDir = pProj->HiddenFilesDir();
if (!FileUtil::Exists(HiddenDir))
{
FileUtil::MakeDirectory(HiddenDir);
FileUtil::MarkHidden(HiddenDir, true);
}
pProj->mpAudioManager->LoadAssets();
pProj->mpTweakManager->LoadTweaks();
return pProj;
}

View File

@ -7,6 +7,7 @@
#include "Core/CAudioManager.h"
#include "Core/IProgressNotifier.h"
#include "Core/Resource/Script/CGameTemplate.h"
#include "Core/Tweaks/CTweakManager.h"
#include <Common/CAssetID.h>
#include <Common/EGame.h>
#include <Common/FileUtil.h>
@ -18,6 +19,7 @@ namespace nod { class DiscWii; }
enum class EProjectVersion
{
Initial,
RawStrings,
// Add new versions before this line
Max,
@ -34,9 +36,10 @@ class CGameProject
TString mProjectRoot;
std::vector<CPackage*> mPackages;
CResourceStore *mpResourceStore;
CGameInfo *mpGameInfo;
CAudioManager *mpAudioManager;
std::unique_ptr<CResourceStore> mpResourceStore;
std::unique_ptr<CGameInfo> mpGameInfo;
std::unique_ptr<CAudioManager> mpAudioManager;
std::unique_ptr<CTweakManager> mpTweakManager;
// Keep file handle open for the .prj file to prevent users from opening the same project
// in multiple instances of PWE
@ -51,14 +54,14 @@ class CGameProject
, mBuildVersion(0.f)
, mpResourceStore(nullptr)
{
mpGameInfo = new CGameInfo();
mpAudioManager = new CAudioManager(this);
mpGameInfo = std::make_unique<CGameInfo>();
mpAudioManager = std::make_unique<CAudioManager>(this);
mpTweakManager = std::make_unique<CTweakManager>(this);
}
public:
~CGameProject();
bool Save();
bool Serialize(IArchive& rArc);
bool BuildISO(const TString& rkIsoPath, IProgressNotifier *pProgress);
@ -81,6 +84,7 @@ public:
// Directory Handling
inline TString ProjectRoot() const { return mProjectRoot; }
inline TString ProjectPath() const { return mProjectRoot + FileUtil::SanitizeName(mProjectName, false) + ".prj"; }
inline TString HiddenFilesDir() const { return mProjectRoot + ".project/"; }
inline TString DiscDir(bool Relative) const { return Relative ? "Disc/" : mProjectRoot + "Disc/"; }
inline TString PackagesDir(bool Relative) const { return Relative ? "Packages/" : mProjectRoot + "Packages/"; }
inline TString ResourcesDir(bool Relative) const { return Relative ? "Resources/" : mProjectRoot + "Resources/"; }
@ -95,9 +99,10 @@ public:
inline uint32 NumPackages() const { return mPackages.size(); }
inline CPackage* PackageByIndex(uint32 Index) const { return mPackages[Index]; }
inline void AddPackage(CPackage *pPackage) { mPackages.push_back(pPackage); }
inline CResourceStore* ResourceStore() const { return mpResourceStore; }
inline CGameInfo* GameInfo() const { return mpGameInfo; }
inline CAudioManager* AudioManager() const { return mpAudioManager; }
inline CResourceStore* ResourceStore() const { return mpResourceStore.get(); }
inline CGameInfo* GameInfo() const { return mpGameInfo.get(); }
inline CAudioManager* AudioManager() const { return mpAudioManager.get(); }
inline CTweakManager* TweakManager() const { return mpTweakManager.get(); }
inline EGame Game() const { return mGame; }
inline ERegion Region() const { return mRegion; }
inline TString GameID() const { return mGameID; }

View File

@ -30,12 +30,12 @@ TString COpeningBanner::EnglishGameName() const
Banner.ReadBytes(NameBuffer.data(), MaxLen * CharSize);
Banner.SetData(NameBuffer.data(), NameBuffer.size(), EEndian::BigEndian);
return mWii ? Banner.ReadWString().ToUTF8() : Banner.ReadString();
return mWii ? Banner.Read16String().ToUTF8() : Banner.ReadString();
}
void COpeningBanner::SetEnglishGameName(const TString& rkName)
{
CMemoryOutStream Banner(mBannerData.data(), mBannerData.size(), EEndian::BigEndian);
CMemoryOutStream Banner(mBannerData.data(), mBannerData.size(), EEndian::BigEndian);
uint32 PadCount = 0;
uint32 MaxLen = MaxGameNameLength();
@ -44,7 +44,7 @@ void COpeningBanner::SetEnglishGameName(const TString& rkName)
if (mWii)
{
Banner.GoTo(0xB0);
Banner.WriteWString(rkName.ToUTF16(), -1, false);
Banner.Write16String(rkName.ToUTF16(), -1, false);
PadCount = (MaxLen - rkName.Size()) * 2;
}
else

View File

@ -59,6 +59,16 @@ void CPackage::UpdateDependencyCache() const
mCacheDirty = false;
}
void CPackage::MarkDirty()
{
if (!mNeedsRecook)
{
mNeedsRecook = true;
Save();
UpdateDependencyCache();
}
}
void CPackage::Cook(IProgressNotifier *pProgress)
{
SCOPED_TIMER(CookPackage);

View File

@ -60,6 +60,7 @@ public:
void Serialize(IArchive& rArc);
void AddResource(const TString& rkName, const CAssetID& rkID, const CFourCC& rkType);
void UpdateDependencyCache() const;
void MarkDirty();
void Cook(IProgressNotifier *pProgress);
void CompareOriginalAssetList(const std::list<CAssetID>& rkNewList);
@ -77,7 +78,6 @@ public:
inline bool NeedsRecook() const { return mNeedsRecook; }
inline void SetPakName(TString NewName) { mPakName = NewName; }
inline void MarkDirty() { mNeedsRecook = true; Save(); UpdateDependencyCache(); }
};
#endif // CPACKAGE

View File

@ -40,6 +40,15 @@ CResourceEntry* CResourceEntry::CreateNewResource(CResourceStore *pStore, const
pEntry->mpDirectory->AddChild("", pEntry);
pEntry->mMetadataDirty = true;
// Check if the data exists or not. If so, then we are creating an entry for an existing resource (game exporter).
// If not, we want to initiate the new resource data and save it as soon as possible.
if (!pEntry->HasCookedVersion())
{
pEntry->mpResource = CResourceFactory::SpawnResource(pEntry);
pEntry->mpResource->InitializeNewResource();
}
return pEntry;
}
@ -104,6 +113,9 @@ bool CResourceEntry::LoadMetadata()
bool CResourceEntry::SaveMetadata(bool ForceSave /*= false*/)
{
// Make sure we aren't saving a deleted resource
ASSERT( !HasFlag(EResEntryFlag::MarkedForDeletion) );
if (mMetadataDirty || ForceSave)
{
TString Path = MetadataFilePath();
@ -261,7 +273,7 @@ bool CResourceEntry::NeedsRecook() const
return (FileUtil::LastModifiedTime(CookedAssetPath()) < FileUtil::LastModifiedTime(RawAssetPath()));
}
bool CResourceEntry::Save(bool SkipCacheSave /*= false*/)
bool CResourceEntry::Save(bool SkipCacheSave /*= false*/, bool FlagForRecook /*= true*/)
{
// SkipCacheSave argument tells us not to save the resource cache file. This is generally not advised because we don't
// want the actual resource data to desync from the cache data. However, there are occasions where we save multiple
@ -270,7 +282,6 @@ bool CResourceEntry::Save(bool SkipCacheSave /*= false*/)
//
// For now, always save the resource when this function is called even if there's been no changes made to it in memory.
// In the future this might not be desired behavior 100% of the time.
// We also might want this function to trigger a cook for certain resource types eventually.
bool ShouldCollectGarbage = false;
// Save raw resource
@ -298,7 +309,10 @@ bool CResourceEntry::Save(bool SkipCacheSave /*= false*/)
return false;
}
SetFlag(EResEntryFlag::NeedsRecook);
if (FlagForRecook)
{
SetFlag(EResEntryFlag::NeedsRecook);
}
}
// This resource type doesn't have a raw format; save cooked instead
@ -321,13 +335,16 @@ bool CResourceEntry::Save(bool SkipCacheSave /*= false*/)
if (!SkipCacheSave)
{
mpStore->ConditionalSaveStore();
}
// Flag dirty any packages that contain this resource.
// Flag dirty any packages that contain this resource.
if (FlagForRecook)
{
for (uint32 iPkg = 0; iPkg < mpStore->Project()->NumPackages(); iPkg++)
{
CPackage *pPkg = mpStore->Project()->PackageByIndex(iPkg);
if (pPkg->ContainsAsset(ID()))
if (!pPkg->NeedsRecook() && pPkg->ContainsAsset(ID()))
pPkg->MarkDirty();
}
}
@ -477,6 +494,9 @@ bool CResourceEntry::CanMoveTo(const TString& rkDir, const TString& rkName)
bool CResourceEntry::MoveAndRename(const TString& rkDir, const TString& rkName, bool IsAutoGenDir /*= false*/, bool IsAutoGenName /*= false*/)
{
// Make sure we are not moving a deleted resource.
ASSERT( !IsMarkedForDeletion() );
if (!CanMoveTo(rkDir, rkName)) return false;
// Store old paths
@ -487,7 +507,15 @@ bool CResourceEntry::MoveAndRename(const TString& rkDir, const TString& rkName,
TString OldMetaPath = MetadataFilePath();
// Set new directory and name
CVirtualDirectory *pNewDir = mpStore->GetVirtualDirectory(rkDir, true);
bool DirAlreadyExisted = true;
CVirtualDirectory *pNewDir = mpStore->GetVirtualDirectory(rkDir, false);
if (!pNewDir)
{
pNewDir = mpStore->GetVirtualDirectory(rkDir, true);
DirAlreadyExisted = false;
}
if (pNewDir == mpDirectory && rkName == mName) return false;
// Check if we can legally move to this spot
@ -566,7 +594,7 @@ bool CResourceEntry::MoveAndRename(const TString& rkDir, const TString& rkName,
// If we succeeded, finish the move
if (FSMoveSuccess)
{
if (mpDirectory != pOldDir)
if (mpDirectory != pOldDir && pOldDir != nullptr)
{
FSMoveSuccess = pOldDir->RemoveChildResource(this);
ASSERT(FSMoveSuccess == true); // this shouldn't be able to fail
@ -591,7 +619,11 @@ bool CResourceEntry::MoveAndRename(const TString& rkDir, const TString& rkName,
errorf("MOVE FAILED: %s", *MoveFailReason);
mpDirectory = pOldDir;
mName = OldName;
mpStore->ConditionalDeleteDirectory(pNewDir, false);
if (!DirAlreadyExisted)
{
mpStore->ConditionalDeleteDirectory(pNewDir, true);
}
if (FileUtil::Exists(NewRawPath))
FileUtil::MoveFile(NewRawPath, OldRawPath);
@ -613,6 +645,96 @@ bool CResourceEntry::Rename(const TString& rkName, bool IsAutoGenName /*= false*
return MoveAndRename(mpDirectory->FullPath(), rkName, false, IsAutoGenName);
}
void CResourceEntry::MarkDeleted(bool InDeleted)
{
// Flags resource for future deletion. "Deleted" resources remain in memory (which
// allows them to easily be un-deleted) but cannot be looked up in the resource
// store and will not save back out to the resource database. Their file data is
// stored in a temporary directory, which allows them to be moved back if the user
// un-does the deletion.
if (IsMarkedForDeletion() != InDeleted)
{
SetFlagEnabled(EResEntryFlag::MarkedForDeletion, InDeleted);
// Restore old name/directory if un-deleting
if (!InDeleted)
{
// Our directory path is stored in the Name field - see below for explanation
int NameEnd = mName.IndexOf('|');
ASSERT( NameEnd != -1 );
TString DirPath = mName.ChopFront(NameEnd + 1);
mName = mName.ChopBack( mName.Size() - NameEnd);
mpDirectory = mpStore->GetVirtualDirectory( DirPath, true );
ASSERT( mpDirectory != nullptr );
mpDirectory->AddChild("", this);
}
TString CookedPath = CookedAssetPath();
TString RawPath = RawAssetPath();
TString MetaPath = MetadataFilePath();
TString PathBase = mpStore->DeletedResourcePath() + mID.ToString() + ".";
TString DelCookedPath = PathBase + CookedExtension().ToString();
TString DelRawPath = DelCookedPath + ".rsraw";
TString DelMetaPath = DelCookedPath + ".rsmeta";
// If we are deleting...
if (InDeleted)
{
// Temporarily store our directory path in the name string.
// This is a hack, but we can't store the directory pointer because it may have been
// deleted and remade by the user by the time the resource is un-deleted, which
// means it is not safe to access later. Separating the name and the path with
// the '|' character is safe because this character is not allowed in filenames
// (which is enforced in FileUtil::IsValidName()).
mName = mName + "|" + mpDirectory->FullPath();
// Remove from parent directory.
mpDirectory->RemoveChildResource(this);
mpDirectory = nullptr;
// Move any resource files out of the project into a temporary folder.
FileUtil::MakeDirectory(DelMetaPath.GetFileDirectory());
if (FileUtil::Exists(MetaPath))
{
FileUtil::MoveFile(MetaPath, DelMetaPath);
}
if (FileUtil::Exists(RawPath))
{
FileUtil::MoveFile(RawPath, DelRawPath);
}
if (FileUtil::Exists(CookedPath))
{
FileUtil::MoveFile(CookedPath, DelCookedPath);
}
}
// If we are un-deleting...
else
{
// Move any resource files out of the temporary folder back into the project.
FileUtil::MakeDirectory(MetaPath.GetFileDirectory());
if (FileUtil::Exists(DelMetaPath))
{
FileUtil::MoveFile(DelMetaPath, MetaPath);
}
if (FileUtil::Exists(DelRawPath))
{
FileUtil::MoveFile(DelRawPath, RawPath);
}
if (FileUtil::Exists(DelCookedPath))
{
FileUtil::MoveFile(DelCookedPath, CookedPath);
}
}
mpStore->SetCacheDirty();
debugf("%s FOR DELETION: [%s] %s", InDeleted ? "MARKED" : "UNMARKED", *ID().ToString(), *CookedPath.GetFileName());
}
}
CGameProject* CResourceEntry::Project() const
{
return mpStore ? mpStore->Project() : nullptr;

View File

@ -21,6 +21,7 @@ enum class EResEntryFlag
HasBeenModified = 0x00000008, // Resource has been modified and resaved by the user
AutoResName = 0x00000010, // Resource name is auto-generated
AutoResDir = 0x00000020, // Resource directory name is auto-generated
MarkedForDeletion = 0x00000040, // Resource has been marked for deletion by the user
};
DECLARE_FLAGS(EResEntryFlag, FResEntryFlags)
@ -67,7 +68,7 @@ public:
bool IsInDirectory(CVirtualDirectory *pDir) const;
uint64 Size() const;
bool NeedsRecook() const;
bool Save(bool SkipCacheSave = false);
bool Save(bool SkipCacheSave = false, bool FlagForRecook = true);
bool Cook();
CResource* Load();
CResource* LoadCooked(IInputStream& rInput);
@ -76,6 +77,7 @@ public:
bool MoveAndRename(const TString& rkDir, const TString& rkName, bool IsAutoGenDir = false, bool IsAutoGenName = false);
bool Move(const TString& rkDir, bool IsAutoGenDir = false);
bool Rename(const TString& rkName, bool IsAutoGenName = false);
void MarkDeleted(bool InDeleted);
CGameProject* Project() const;
EGame Game() const;
@ -87,9 +89,10 @@ public:
inline void SetFlagEnabled(EResEntryFlag Flag, bool Enabled) { Enabled ? SetFlag(Flag) : ClearFlag(Flag); }
inline void SetDirty() { SetFlag(EResEntryFlag::NeedsRecook); }
inline void SetHidden(bool Hidden) { Hidden ? SetFlag(EResEntryFlag::Hidden) : ClearFlag(EResEntryFlag::Hidden); }
inline void SetHidden(bool Hidden) { SetFlagEnabled(EResEntryFlag::Hidden, Hidden); }
inline bool HasFlag(EResEntryFlag Flag) const { return mFlags.HasFlag(Flag); }
inline bool IsHidden() const { return HasFlag(EResEntryFlag::Hidden); }
inline bool IsMarkedForDeletion() const { return HasFlag(EResEntryFlag::MarkedForDeletion); }
inline bool IsLoaded() const { return mpResource != nullptr; }
inline bool IsCategorized() const { return mpDirectory && !mpDirectory->FullPath().CaseInsensitiveCompare( mpStore->DefaultResourceDirPath() ); }
@ -103,7 +106,7 @@ public:
inline TString DirectoryPath() const { return mpDirectory->FullPath(); }
inline TString Name() const { return mName; }
inline const TString& UppercaseName() const { return mCachedUppercaseName; }
inline EResourceType ResourceType() const { return mpTypeInfo->Type(); }
inline EResourceType ResourceType() const { return mpTypeInfo->Type(); }
protected:
CResource* InternalLoad(IInputStream& rInput);

View File

@ -22,12 +22,16 @@ public:
virtual CResourceEntry* Next()
{
if (mIter != mpkStore->mResourceEntries.end())
do
{
mpCurEntry = mIter->second;
mIter++;
if (mIter != mpkStore->mResourceEntries.end())
{
mpCurEntry = mIter->second;
mIter++;
}
else mpCurEntry = nullptr;
}
else mpCurEntry = nullptr;
while (mpCurEntry && mpCurEntry->IsMarkedForDeletion());
return mpCurEntry;
}

View File

@ -66,6 +66,22 @@ bool CResourceStore::SerializeDatabaseCache(IArchive& rArc)
{
// Serialize resources
uint32 ResourceCount = mResourceEntries.size();
if (rArc.IsWriter())
{
// Make sure deleted resources aren't included in the count.
// We can't use CResourceIterator because it skips MarkedForDeletion resources.
for (auto Iter = mResourceEntries.begin(); Iter != mResourceEntries.end(); Iter++)
{
CResourceEntry* pEntry = Iter->second;
if (pEntry->IsMarkedForDeletion())
{
ResourceCount--;
}
}
}
rArc << SerialParameter("ResourceCount", ResourceCount);
if (rArc.IsReader())
@ -85,10 +101,13 @@ bool CResourceStore::SerializeDatabaseCache(IArchive& rArc)
{
for (CResourceIterator It(this); It; ++It)
{
if (rArc.ParamBegin("Resource", 0))
if (!It->IsMarkedForDeletion())
{
It->SerializeEntryInfo(rArc, false);
rArc.ParamEnd();
if (rArc.ParamBegin("Resource", 0))
{
It->SerializeEntryInfo(rArc, false);
rArc.ParamEnd();
}
}
}
}
@ -140,18 +159,22 @@ bool CResourceStore::LoadDatabaseCache()
}
else
{
// Database is succesfully loaded at this point
// Database is successfully loaded at this point
if (mpProj)
{
ASSERT(mpProj->Game() == Reader.Game());
}
mGame = Reader.Game();
}
mGame = Reader.Game();
return true;
}
bool CResourceStore::SaveDatabaseCache()
{
TString Path = DatabasePath();
debugf("Saving database cache...");
CBasicBinaryWriter Writer(Path, FOURCC('CACH'), 0, mGame);
@ -182,6 +205,14 @@ void CResourceStore::SetProject(CGameProject *pProj)
mDatabasePath = mpProj->ProjectRoot();
mpDatabaseRoot = new CVirtualDirectory(this);
mGame = mpProj->Game();
// Clear deleted files from previous runs
TString DeletedPath = DeletedResourcePath();
if (FileUtil::Exists(DeletedPath))
{
FileUtil::ClearDirectory(DeletedPath);
}
}
}
@ -215,6 +246,14 @@ void CResourceStore::CloseProject()
It = mResourceEntries.erase(It);
}
// Clear deleted files from previous runs
TString DeletedPath = DeletedResourcePath();
if (FileUtil::Exists(DeletedPath))
{
FileUtil::ClearDirectory(DeletedPath);
}
delete mpDatabaseRoot;
mpDatabaseRoot = nullptr;
mpProj = nullptr;
@ -256,12 +295,27 @@ TString CResourceStore::DefaultResourceDirPath() const
return StaticDefaultResourceDirPath( mGame );
}
TString CResourceStore::DeletedResourcePath() const
{
return mpProj->HiddenFilesDir() / "delete/";
}
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;
if (rkID.IsValid())
{
auto Found = mResourceEntries.find(rkID);
if (Found != mResourceEntries.end())
{
CResourceEntry* pEntry = Found->second;
if (!pEntry->IsMarkedForDeletion())
return pEntry;
}
}
return nullptr;
}
CResourceEntry* CResourceStore::FindEntry(const TString& rkPath) const
@ -284,7 +338,14 @@ void CResourceStore::ClearDatabase()
{
// THIS OPERATION REQUIRES THAT ALL RESOURCES ARE UNREFERENCED
DestroyUnreferencedResources();
ASSERT(mLoadedResources.empty());
if (!mLoadedResources.empty())
{
debugf("ERROR: Resources still loaded:");
for (auto Iter = mLoadedResources.begin(); Iter != mLoadedResources.end(); Iter++)
debugf("\t[%s] %s", *Iter->first.ToString(), *Iter->second->CookedAssetPath(true));
ASSERT(false);
}
// Clear out existing resource entries and directories
for (auto Iter = mResourceEntries.begin(); Iter != mResourceEntries.end(); Iter++)
@ -384,7 +445,7 @@ bool CResourceStore::IsResourceRegistered(const CAssetID& rkID) const
return FindEntry(rkID) != nullptr;
}
CResourceEntry* CResourceStore::RegisterResource(const CAssetID& rkID, EResourceType Type, const TString& rkDir, const TString& rkName)
CResourceEntry* CResourceStore::CreateNewResource(const CAssetID& rkID, EResourceType Type, const TString& rkDir, const TString& rkName)
{
CResourceEntry *pEntry = FindEntry(rkID);
@ -398,6 +459,14 @@ CResourceEntry* CResourceStore::RegisterResource(const CAssetID& rkID, EResource
{
pEntry = CResourceEntry::CreateNewResource(this, rkID, rkDir, rkName, Type);
mResourceEntries[rkID] = pEntry;
mDatabaseCacheDirty = true;
if (pEntry->IsLoaded())
{
TrackLoadedResource(pEntry);
}
debugf("CREATED NEW RESOURCE: [%s] %s", *rkID.ToString(), *pEntry->CookedAssetPath());
}
else

View File

@ -52,9 +52,10 @@ public:
void CreateVirtualDirectory(const TString& rkPath);
void ConditionalDeleteDirectory(CVirtualDirectory *pDir, bool Recurse);
TString DefaultResourceDirPath() const;
TString DeletedResourcePath() const;
bool IsResourceRegistered(const CAssetID& rkID) const;
CResourceEntry* RegisterResource(const CAssetID& rkID, EResourceType Type, const TString& rkDir, const TString& rkName);
CResourceEntry* CreateNewResource(const CAssetID& rkID, EResourceType Type, const TString& rkDir, const TString& rkName);
CResourceEntry* FindEntry(const CAssetID& rkID) const;
CResourceEntry* FindEntry(const TString& rkPath) const;
bool AreAllEntriesValid() const;

View File

@ -46,6 +46,28 @@ bool CVirtualDirectory::IsDescendantOf(CVirtualDirectory *pDir) const
return (this == pDir) || (mpParent && pDir && (mpParent == pDir || mpParent->IsDescendantOf(pDir)));
}
bool CVirtualDirectory::IsSafeToDelete() const
{
// Return false if we contain any referenced assets.
for (CResourceEntry* pEntry : mResources)
{
if (pEntry->IsLoaded() && pEntry->Resource()->IsReferenced())
{
return false;
}
}
for (CVirtualDirectory* pSubdir : mSubdirectories)
{
if (!pSubdir->IsSafeToDelete())
{
return false;
}
}
return true;
}
TString CVirtualDirectory::FullPath() const
{
if (IsRoot())

View File

@ -26,6 +26,7 @@ public:
bool IsEmpty(bool CheckFilesystem) const;
bool IsDescendantOf(CVirtualDirectory *pDir) const;
bool IsSafeToDelete() const;
TString FullPath() const;
TString AbsolutePath() const;
CVirtualDirectory* GetRoot();

View File

@ -113,6 +113,7 @@ void CCharacterUsageMap::DebugPrintContents()
// ************ PROTECTED ************
void CCharacterUsageMap::ParseDependencyNode(IDependencyNode *pNode)
{
if (!pNode) return;
EDependencyNodeType Type = pNode->Type();
if (Type == EDependencyNodeType::CharacterProperty)
@ -271,6 +272,7 @@ void CPackageDependencyListBuilder::AddDependency(CResourceEntry *pCurEntry, con
void CPackageDependencyListBuilder::EvaluateDependencyNode(CResourceEntry *pCurEntry, IDependencyNode *pNode, std::list<CAssetID>& rOut)
{
if (!pNode) return;
EDependencyNodeType Type = pNode->Type();
bool ParseChildren = false;
@ -513,6 +515,7 @@ void CAreaDependencyListBuilder::AddDependency(const CAssetID& rkID, std::list<C
void CAreaDependencyListBuilder::EvaluateDependencyNode(CResourceEntry *pCurEntry, IDependencyNode *pNode, std::list<CAssetID>& rOut, std::set<CAssetID> *pAudioGroupsOut)
{
if (!pNode) return;
EDependencyNodeType Type = pNode->Type();
bool ParseChildren = false;
@ -556,3 +559,82 @@ void CAreaDependencyListBuilder::EvaluateDependencyNode(CResourceEntry *pCurEntr
EvaluateDependencyNode(pCurEntry, pNode->ChildByIndex(iChild), rOut, pAudioGroupsOut);
}
}
// ************ CAssetDependencyListBuilder ************
void CAssetDependencyListBuilder::BuildDependencyList(std::vector<CAssetID>& OutAssets)
{
mCharacterUsageMap.FindUsagesForAsset(mpResourceEntry);
EvaluateDependencyNode(mpResourceEntry, mpResourceEntry->Dependencies(), OutAssets);
}
void CAssetDependencyListBuilder::AddDependency(const CAssetID& kID, std::vector<CAssetID>& Out)
{
CResourceEntry *pEntry = mpResourceEntry->ResourceStore()->FindEntry(kID);
if (!pEntry) return;
EResourceType ResType = pEntry->ResourceType();
if (mUsedAssets.find(kID) != mUsedAssets.end())
return;
// Dependency is valid! Evaluate the node tree
if (ResType == EResourceType::AnimSet)
{
ASSERT(!mCurrentAnimSetID.IsValid());
mCurrentAnimSetID = pEntry->ID();
}
EvaluateDependencyNode(pEntry, pEntry->Dependencies(), Out);
if (ResType == EResourceType::AnimSet)
{
ASSERT(mCurrentAnimSetID.IsValid());
mCurrentAnimSetID = CAssetID::InvalidID(mpResourceEntry->Game());
}
Out.push_back(kID);
mUsedAssets.insert(kID);
}
void CAssetDependencyListBuilder::EvaluateDependencyNode(CResourceEntry* pCurEntry, IDependencyNode* pNode, std::vector<CAssetID>& Out)
{
if (!pNode) return;
EDependencyNodeType Type = pNode->Type();
bool ParseChildren = false;
if (Type == EDependencyNodeType::Resource || Type == EDependencyNodeType::ScriptProperty || Type == EDependencyNodeType::CharacterProperty)
{
CResourceDependency* pDep = static_cast<CResourceDependency*>(pNode);
AddDependency(pDep->ID(), Out);
}
else if (Type == EDependencyNodeType::AnimEvent)
{
CAnimEventDependency* pDep = static_cast<CAnimEventDependency*>(pNode);
uint32 CharIndex = pDep->CharIndex();
if (CharIndex == -1 || mCharacterUsageMap.IsCharacterUsed(mCurrentAnimSetID, CharIndex))
AddDependency(pDep->ID(), Out);
}
else if (Type == EDependencyNodeType::SetCharacter)
{
CSetCharacterDependency* pChar = static_cast<CSetCharacterDependency*>(pNode);
ParseChildren = mCharacterUsageMap.IsCharacterUsed(mCurrentAnimSetID, pChar->CharSetIndex());
}
else if (Type == EDependencyNodeType::SetAnimation)
{
CSetAnimationDependency* pAnim = static_cast<CSetAnimationDependency*>(pNode);
ParseChildren = mCharacterUsageMap.IsAnimationUsed(mCurrentAnimSetID, pAnim);
}
else
ParseChildren = true;
if (ParseChildren)
{
for (uint32 iChild = 0; iChild < pNode->NumChildren(); iChild++)
EvaluateDependencyNode(pCurEntry, pNode->ChildByIndex(iChild), Out);
}
}

View File

@ -96,5 +96,25 @@ public:
void EvaluateDependencyNode(CResourceEntry *pCurEntry, IDependencyNode *pNode, std::list<CAssetID>& rOut, std::set<CAssetID> *pAudioGroupsOut);
};
// ************ CAssetDependencyListBuilder ************
//@todo merge with CAreaDependencyListBuilder; code is very similar
class CAssetDependencyListBuilder
{
CResourceEntry* mpResourceEntry;
CCharacterUsageMap mCharacterUsageMap;
std::set<CAssetID> mUsedAssets;
CAssetID mCurrentAnimSetID;
public:
CAssetDependencyListBuilder(CResourceEntry* pEntry)
: mpResourceEntry(pEntry)
, mCharacterUsageMap(pEntry->ResourceStore())
{}
void BuildDependencyList(std::vector<CAssetID>& OutAssets);
void AddDependency(const CAssetID& kID, std::vector<CAssetID>& Out);
void EvaluateDependencyNode(CResourceEntry* pCurEntry, IDependencyNode* pNode, std::vector<CAssetID>& Out);
};
#endif // DEPENDENCYLISTBUILDERS

View File

@ -6,8 +6,10 @@
class IUIRelay
{
public:
virtual void AsyncMessageBox(const TString& rkInfoBoxTitle, const TString& rkMessage) = 0;
virtual void ShowMessageBox(const TString& rkInfoBoxTitle, const TString& rkMessage) = 0;
virtual void ShowMessageBoxAsync(const TString& rkInfoBoxTitle, const TString& rkMessage) = 0;
virtual bool AskYesNoQuestion(const TString& rkInfoBoxTitle, const TString& rkQuestion) = 0;
virtual bool OpenProject(const TString& kPath = "") = 0;
};
extern IUIRelay *gpUIRelay;

207
src/Core/NCoreTests.cpp Normal file
View File

@ -0,0 +1,207 @@
#include "NCoreTests.h"
#include "IUIRelay.h"
#include "Core/GameProject/CGameProject.h"
#include "Core/GameProject/CResourceEntry.h"
#include "Core/GameProject/CResourceIterator.h"
#include "Core/Resource/Cooker/CResourceCooker.h"
namespace NCoreTests
{
/** Checks for a parameter in the commandline stream */
const char* ParseParameter(const char* pkParmName, int argc, char* argv[])
{
const uint kParmLen = strlen(pkParmName);
for (int i=0; i<argc; i++)
{
if( strncmp(argv[i], pkParmName, kParmLen) == 0 )
{
// Found the parameter. Make sure there is enough space in the
// string for the parameter value before returning it.
if( strlen(argv[i]) >= kParmLen+2 &&
argv[i][kParmLen] == '=')
{
return &argv[i][kParmLen+1];
}
}
}
// Couldn't find the parameter.
return nullptr;
}
/** Checks for the existence of a token in the commandline stream */
bool ParseToken(const char* pkToken, int argc, char* argv[])
{
for (int i=0; i<argc; i++)
{
if( strcmp(argv[i], pkToken) == 0 )
{
return true;
}
}
// Couldn't find the token.
return false;
}
/** Check commandline input to see if the user is running a test */
bool RunTests(int argc, char* argv[])
{
if( ParseToken("ValidateCooker", argc, argv) )
{
// Fetch parameters
const char* pkType = ParseParameter("-type", argc, argv);
EResourceType Type = TEnumReflection<EResourceType>::ConvertStringToValue(pkType);
bool AllowDump = ParseToken("-allowdump", argc, argv);
if( Type == EResourceType::Invalid )
{
gpUIRelay->ShowMessageBox("ValidateCooker", "Usage: ValidateCooker -type=<ResourceType> [-allowdump] [-project=<Project>]");
}
else if( gpUIRelay->OpenProject(ParseParameter("-project", argc, argv)) )
{
ValidateCooker(Type, AllowDump);
}
return true;
}
// No test being run.
return false;
}
/** Validate all cooker output for the given resource type matches the original asset data */
bool ValidateCooker(EResourceType ResourceType, bool DumpInvalidFileContents)
{
debugf( "Validating output of %s cooker...",
TEnumReflection<EResourceType>::ConvertValueToString(ResourceType) );
// There must be a project loaded
CResourceStore* pStore = gpResourceStore;
CGameProject* pProject = (pStore ? pStore->Project() : nullptr);
if (!pProject)
{
errorf("Cooker unit test failed; no project loaded");
return false;
}
TString ResourcesDir = pProject->ResourcesDir(false);
uint NumValid = 0, NumInvalid = 0;
// Iterate through all resources
for (CResourceIterator It(pStore); It; ++It)
{
if (It->ResourceType() != ResourceType || !It->HasCookedVersion())
continue;
// Get original cooked data
TString CookedPath = It->CookedAssetPath(true);
CFileInStream FileStream(ResourcesDir / CookedPath, EEndian::BigEndian);
if (!FileStream.IsValid())
continue;
std::vector<uint8> OriginalData( FileStream.Size() );
FileStream.ReadBytes(OriginalData.data(), OriginalData.size());
FileStream.Close();
// Generate new cooked data
std::vector<char> NewData;
CVectorOutStream MemoryStream(&NewData, EEndian::BigEndian);
CResourceCooker::CookResource(*It, MemoryStream);
// Start our comparison by making sure the sizes match up
const uint kAlignment = (It->Game() >= EGame::Corruption ? 64 : 32);
const uint kAlignedOriginalSize = ALIGN( (uint) OriginalData.size(), kAlignment );
const uint kAlignedNewSize = ALIGN( (uint) NewData.size(), kAlignment );
const char* pkInvalidReason = "";
bool IsValid = false;
if( kAlignedOriginalSize == kAlignedNewSize &&
OriginalData.size() >= NewData.size() )
{
// Compare actual data. Note that the original asset can have alignment padding
// at the end, which is applied by the pak but usually preserved in extracted
// files. We do not include this in the comparison as missing padding does not
// indicate malformed data.
uint DataSize = Math::Min(OriginalData.size(), NewData.size());
if( memcmp(OriginalData.data(), NewData.data(), DataSize) == 0 )
{
// Verify any missing data at the end is padding.
bool MissingData = false;
if( OriginalData.size() > NewData.size() )
{
for( uint i=DataSize; i<OriginalData.size(); i++ )
{
if( OriginalData[i] != 0xFF )
{
MissingData = true;
break;
}
}
}
if( !MissingData )
{
// All tests passed!
IsValid = true;
}
else
{
pkInvalidReason = "missing data";
}
}
else
{
pkInvalidReason = "data mismatch";
}
}
else
{
pkInvalidReason = "size mismatch";
}
// Print test results
if( IsValid )
{
debugf( "[SUCCESS] %s", *CookedPath );
NumValid++;
}
else
{
debugf( "[FAILED: %s] %s", pkInvalidReason, *CookedPath );
NumInvalid++;
}
if( DumpInvalidFileContents )
{
TString DumpPath = "dump" / CookedPath;
FileUtil::MakeDirectory( DumpPath.GetFileDirectory() );
CFileOutStream DumpFile(DumpPath, EEndian::BigEndian);
DumpFile.WriteBytes( NewData.data(), NewData.size() );
DumpFile.Close();
}
if( NumInvalid >= 100 )
{
debugf( "Test aborted; at least 100 invalid resources. Checked %d resources, %d passed, %d failed",
NumValid + NumInvalid, NumValid, NumInvalid );
return false;
}
}
// Test complete
bool TestSuccess = (NumInvalid == 0);
debugf( "Test %s; checked %d resources, %d passed, %d failed",
TestSuccess ? "SUCCEEDED" : "FAILED",
NumValid + NumInvalid, NumValid, NumInvalid );
return TestSuccess;
}
} // end namespace NCoreTests

18
src/Core/NCoreTests.h Normal file
View File

@ -0,0 +1,18 @@
#ifndef NCORETESTS_H
#define NCORETESTS_H
#include "Core/Resource/EResType.h"
/** Unit tests for Core */
namespace NCoreTests
{
/** Check commandline input to see if the user is running a unit test */
bool RunTests(int argc, char *argv[]);
/** Validate all cooker output for the given resource type matches the original asset data */
bool ValidateCooker(EResourceType ResourceType, bool DumpInvalidFileContents);
}
#endif // NCORETESTS_H

View File

@ -92,7 +92,7 @@ void CAnimationParameters::Write(IOutputStream& rSCLY)
{
CAssetID::skInvalidID32.Write(rSCLY);
rSCLY.WriteLong(0);
rSCLY.WriteLong(0xFFFFFFFF);
rSCLY.WriteLong(mGame >= EGame::EchoesDemo ? 0 : 0xFFFFFFFF);
}
}

View File

@ -2,9 +2,10 @@
#define CANIMATIONPARAMETERS_H
#include "Core/Resource/TResPtr.h"
#include "Core/Resource/Model/CModel.h"
#include <Common/EGame.h>
class CModel;
class CAnimationParameters
{
EGame mGame;

View File

@ -10,6 +10,7 @@ CResTypeInfo::CResTypeInfo(EResourceType Type, const TString& rkTypeName, const
, mRetroExtension(rkRetroExtension)
, mCanBeSerialized(false)
, mCanHaveDependencies(true)
, mCanBeCreated(false)
{
#if !PUBLIC_RELEASE
ASSERT(smTypeMap.find(Type) == smTypeMap.end());
@ -195,7 +196,7 @@ void CResTypeInfo::CResTypeInfoFactory::InitTypes()
}
{
CResTypeInfo *pType = new CResTypeInfo(EResourceType::AudioGroup, "Audio Group", "agsc");
AddExtension(pType, "AGSC", EGame::PrimeDemo, EGame::Echoes);
AddExtension(pType, "AGSC", EGame::PrimeDemo, EGame::CorruptionProto);
pType->mCanHaveDependencies = false;
}
{
@ -332,6 +333,7 @@ void CResTypeInfo::CResTypeInfoFactory::InitTypes()
{
CResTypeInfo *pType = new CResTypeInfo(EResourceType::Scan, "Scan", "scan");
AddExtension(pType, "SCAN", EGame::PrimeDemo, EGame::Corruption);
pType->mCanBeCreated = true;
}
{
CResTypeInfo *pType = new CResTypeInfo(EResourceType::Skeleton, "Skeleton", "cin");
@ -381,6 +383,8 @@ void CResTypeInfo::CResTypeInfoFactory::InitTypes()
{
CResTypeInfo *pType = new CResTypeInfo(EResourceType::StringTable, "String Table", "strg");
AddExtension(pType, "STRG", EGame::PrimeDemo, EGame::DKCReturns);
pType->mCanBeSerialized = true;
pType->mCanBeCreated = true;
}
{
CResTypeInfo *pType = new CResTypeInfo(EResourceType::Texture, "Texture", "txtr");
@ -388,7 +392,7 @@ void CResTypeInfo::CResTypeInfoFactory::InitTypes()
pType->mCanHaveDependencies = false;
}
{
CResTypeInfo *pType = new CResTypeInfo(EResourceType::Tweak, "Tweak Data", "ctwk");
CResTypeInfo *pType = new CResTypeInfo(EResourceType::Tweaks, "Tweak Data", "ctwk");
AddExtension(pType, "CTWK", EGame::PrimeDemo, EGame::Prime);
pType->mCanHaveDependencies = false;
}

View File

@ -23,6 +23,7 @@ class CResTypeInfo
TString mRetroExtension; // File extension in Retro's directory tree. We don't use it directly but it is needed for generating asset ID hashes
bool mCanBeSerialized;
bool mCanHaveDependencies;
bool mCanBeCreated;
static std::unordered_map<EResourceType, CResTypeInfo*> smTypeMap;
@ -36,10 +37,11 @@ public:
CFourCC CookedExtension(EGame Game) const;
// Accessors
inline EResourceType Type() const { return mType; }
inline EResourceType Type() const { return mType; }
inline TString TypeName() const { return mTypeName; }
inline bool CanBeSerialized() const { return mCanBeSerialized; }
inline bool CanHaveDependencies() const { return mCanHaveDependencies; }
inline bool CanBeCreated() const { return mCanBeCreated; }
// Static
static void GetAllTypesInGame(EGame Game, std::list<CResTypeInfo*>& rOut);

View File

@ -43,6 +43,7 @@ public:
virtual ~CResource() {}
virtual CDependencyTree* BuildDependencyTree() const { return new CDependencyTree(); }
virtual void Serialize(IArchive& /*rArc*/) {}
virtual void InitializeNewResource() {}
inline CResourceEntry* Entry() const { return mpEntry; }
inline CResTypeInfo* TypeInfo() const { return mpEntry->TypeInfo(); }

View File

@ -1,122 +0,0 @@
#ifndef CSCAN_H
#define CSCAN_H
#include "CResource.h"
#include "CStringTable.h"
#include "TResPtr.h"
#include "Core/Resource/Animation/CAnimationParameters.h"
#include <Common/EGame.h>
class CScan : public CResource
{
DECLARE_RESOURCE_TYPE(Scan)
friend class CScanLoader;
public:
// This likely needs revising when MP2/MP3 support is added
enum class ELogbookCategory
{
None,
PirateData,
ChozoLore,
Creatures,
Research
};
struct SScanInfoSecondaryModel
{
CAssetID ModelID;
CAnimationParameters AnimParams;
TString AttachBoneName;
};
private:
// Common
TResPtr<CStringTable> mpStringTable;
bool mIsSlow;
bool mIsImportant;
ELogbookCategory mCategory;
// MP1
CAssetID mFrameID;
CAssetID mScanImageTextures[4];
// MP2/3
bool mUseLogbookModelPostScan;
CAssetID mPostOverrideTexture;
float mLogbookDefaultRotX;
float mLogbookDefaultRotZ;
float mLogbookScale;
CAssetID mLogbookModel;
CAnimationParameters mLogbookAnimParams;
CAnimationParameters mUnknownAnimParams;
std::vector<SScanInfoSecondaryModel> mSecondaryModels;
// MP3
std::vector<CAssetID> mDependencyList;
public:
CScan(CResourceEntry *pEntry = 0)
: CResource(pEntry)
, mpStringTable(nullptr)
, mIsSlow(false)
, mIsImportant(false)
, mCategory(ELogbookCategory::None)
{}
CDependencyTree* BuildDependencyTree() const
{
CDependencyTree *pTree = new CDependencyTree();
// Corruption's SCAN has a list of all assets - just grab that
if (Game() >= EGame::CorruptionProto)
{
for (uint32 iDep = 0; iDep < mDependencyList.size(); iDep++)
{
pTree->AddDependency(mDependencyList[iDep]);
}
return pTree;
}
// Otherwise add all the dependencies we need from the properties
if (Game() <= EGame::Prime)
pTree->AddDependency(mFrameID);
pTree->AddDependency(mpStringTable);
if (Game() <= EGame::Prime)
{
for (uint32 iImg = 0; iImg < 4; iImg++)
pTree->AddDependency(mScanImageTextures[iImg]);
}
else if (Game() <= EGame::Echoes)
{
pTree->AddDependency(mPostOverrideTexture);
pTree->AddDependency(mLogbookModel);
pTree->AddCharacterDependency(mLogbookAnimParams);
pTree->AddCharacterDependency(mUnknownAnimParams);
for (uint32 iSec = 0; iSec < mSecondaryModels.size(); iSec++)
{
const SScanInfoSecondaryModel& rkSecModel = mSecondaryModels[iSec];
pTree->AddDependency(rkSecModel.ModelID);
pTree->AddCharacterDependency(rkSecModel.AnimParams);
}
}
return pTree;
}
// Accessors
inline CStringTable* ScanText() const { return mpStringTable; }
inline bool IsImportant() const { return mIsImportant; }
inline bool IsSlow() const { return mIsSlow; }
inline ELogbookCategory LogbookCategory() const { return mCategory; }
inline CAssetID GuiFrame() const { return mFrameID; }
inline CAssetID ScanImage(uint32 ImgIndex) const { return mScanImageTextures[ImgIndex]; }
inline CAssetID LogbookDisplayAssetID() const { return (mLogbookAnimParams.ID().IsValid() ? mLogbookAnimParams.ID() : mLogbookModel); }
};
#endif // CSCAN_H

View File

@ -1,180 +0,0 @@
#ifndef CSTRINGTABLE_H
#define CSTRINGTABLE_H
#include "CResource.h"
#include <Common/BasicTypes.h>
#include <Common/CFourCC.h>
#include <Common/TString.h>
#include <vector>
class CStringTable : public CResource
{
DECLARE_RESOURCE_TYPE(StringTable)
friend class CStringLoader;
std::vector<TString> mStringNames;
uint32 mNumStrings;
struct SLangTable
{
CFourCC Language;
std::vector<TString> Strings;
};
std::vector<SLangTable> mLangTables;
public:
CStringTable(CResourceEntry *pEntry = 0) : CResource(pEntry) {}
inline uint32 NumStrings() const { return mNumStrings; }
inline uint32 NumLanguages() const { return mLangTables.size(); }
inline CFourCC LanguageTag(uint32 Index) const { return mLangTables[Index].Language; }
inline TString String(uint32 LangIndex, uint32 StringIndex) const { return mLangTables[LangIndex].Strings[StringIndex]; }
inline TString StringName(uint32 StringIndex) const { return mStringNames[StringIndex]; }
TString String(CFourCC Lang, uint32 StringIndex) const
{
for (uint32 iLang = 0; iLang < NumLanguages(); iLang++)
{
if (LanguageTag(iLang) == Lang)
return String(iLang, StringIndex);
}
return TString();
}
CDependencyTree* BuildDependencyTree() const
{
// STRGs can reference FONTs with the &font=; formatting tag and TXTRs with the &image=; tag
CDependencyTree *pTree = new CDependencyTree();
EIDLength IDLength = (Game() <= EGame::Echoes ? k32Bit : k64Bit);
for (uint32 iLang = 0; iLang < mLangTables.size(); iLang++)
{
const SLangTable& rkTable = mLangTables[iLang];
for (uint32 iStr = 0; iStr < rkTable.Strings.size(); iStr++)
{
const TString& rkStr = rkTable.Strings[iStr];
for (uint32 TagIdx = rkStr.IndexOf('&'); TagIdx != -1; TagIdx = rkStr.IndexOf('&', TagIdx + 1))
{
// Check for double ampersand (escape character in DKCR, not sure about other games)
if (rkStr.At(TagIdx + 1) == '&')
{
TagIdx++;
continue;
}
// Get tag name and parameters
uint32 NameEnd = rkStr.IndexOf('=', TagIdx);
uint32 TagEnd = rkStr.IndexOf(';', TagIdx);
if (NameEnd == -1 || TagEnd == -1) continue;
TString TagName = rkStr.SubString(TagIdx + 1, NameEnd - TagIdx - 1);
TString ParamString = rkStr.SubString(NameEnd + 1, TagEnd - NameEnd - 1);
if (ParamString.IsEmpty()) continue;
// Font
if (TagName == "font")
{
if (Game() >= EGame::CorruptionProto)
{
ASSERT(ParamString.StartsWith("0x"));
ParamString = ParamString.ChopFront(2);
}
ASSERT(ParamString.Size() == IDLength * 2);
pTree->AddDependency( CAssetID::FromString(ParamString) );
}
// Image
else if (TagName == "image")
{
// Determine which params are textures based on image type
TStringList Params = ParamString.Split(",");
TString ImageType = Params.front();
uint32 TexturesStart = -1;
if (ImageType == "A")
TexturesStart = 2;
else if (ImageType == "SI")
TexturesStart = 3;
else if (ImageType == "SA")
TexturesStart = 4;
else if (ImageType == "B")
TexturesStart = 2;
else if (ImageType.IsHexString(false, IDLength * 2))
TexturesStart = 0;
else
{
errorf("Unrecognized image type: %s", *ImageType);
continue;
}
// Load texture IDs
TStringList::iterator Iter = Params.begin();
for (uint32 iParam = 0; iParam < Params.size(); iParam++, Iter++)
{
if (iParam >= TexturesStart)
{
TString Param = *Iter;
if (Game() >= EGame::CorruptionProto)
{
ASSERT(Param.StartsWith("0x"));
Param = Param.ChopFront(2);
}
ASSERT(Param.Size() == IDLength * 2);
pTree->AddDependency( CAssetID::FromString(Param) );
}
}
}
}
}
}
return pTree;
}
static TString StripFormatting(const TString& rkStr)
{
TString Out = rkStr;
int TagStart = -1;
for (uint32 iChr = 0; iChr < Out.Size(); iChr++)
{
if (Out[iChr] == '&')
{
if (TagStart == -1)
TagStart = iChr;
else
{
Out.Remove(TagStart, 1);
TagStart = -1;
iChr--;
}
}
else if (TagStart != -1 && Out[iChr] == ';')
{
int TagEnd = iChr + 1;
int TagLen = TagEnd - TagStart;
Out.Remove(TagStart, TagLen);
iChr = TagStart - 1;
TagStart = -1;
}
}
return Out;
}
};
#endif // CSTRINGTABLE_H

View File

@ -63,7 +63,7 @@ void CWorld::SetAreaLayerInfo(CGameArea *pArea)
TString CWorld::InGameName() const
{
if (mpWorldName)
return mpWorldName->String("ENGL", 0);
return mpWorldName->GetString(ELanguage::English, 0);
else
return Entry()->Name();
}
@ -73,7 +73,7 @@ TString CWorld::AreaInGameName(uint32 AreaIndex) const
const SArea& rkArea = mAreas[AreaIndex];
if (rkArea.pAreaName)
return rkArea.pAreaName->String("ENGL", 0);
return rkArea.pAreaName->GetString(ELanguage::English, 0);
else
return "!!" + rkArea.InternalName;
}

View File

@ -3,9 +3,9 @@
#include "CResource.h"
#include "CSavedStateID.h"
#include "CStringTable.h"
#include "Core/Resource/Area/CGameArea.h"
#include "Core/Resource/Model/CModel.h"
#include "Core/Resource/StringTable/CStringTable.h"
#include <Common/Math/CTransform4f.h>
class CWorld : public CResource

View File

@ -4,8 +4,12 @@
#include "CAreaCooker.h"
#include "CModelCooker.h"
#include "CPoiToWorldCooker.h"
#include "CScanCooker.h"
#include "CStringCooker.h"
#include "CWorldCooker.h"
#include "Core/Tweaks/CTweakCooker.h"
#include "Core/GameProject/CResourceEntry.h"
class CResourceCooker
@ -22,7 +26,10 @@ public:
{
case EResourceType::Area: return CAreaCooker::CookMREA((CGameArea*) pRes, rOutput);
case EResourceType::Model: return CModelCooker::CookCMDL((CModel*) pRes, rOutput);
case EResourceType::Scan: return CScanCooker::CookSCAN((CScan*) pRes, rOutput);
case EResourceType::StaticGeometryMap: return CPoiToWorldCooker::CookEGMC((CPoiToWorld*) pRes, rOutput);
case EResourceType::StringTable: return CStringCooker::CookSTRG((CStringTable*) pRes, rOutput);
case EResourceType::Tweaks: return CTweakCooker::CookCTWK((CTweakData*) pRes, rOutput);
case EResourceType::World: return CWorldCooker::CookMLVL((CWorld*) pRes, rOutput);
default:

View File

@ -0,0 +1,63 @@
#include "CScanCooker.h"
#include "Core/Resource/Cooker/CScriptCooker.h"
#include "Core/GameProject/DependencyListBuilders.h"
bool CScanCooker::CookSCAN(CScan* pScan, IOutputStream& SCAN)
{
// File header
if (pScan->Game() <= EGame::Prime)
{
// We currently do not support cooking for the MP1 demo build
ASSERT( pScan->Game() != EGame::PrimeDemo );
SCAN.WriteLong( 5 ); // Version number; must be 5
SCAN.WriteLong( 0x0BADBEEF ); // SCAN magic
CStructRef ScanProperties = pScan->ScanData();
CScriptCooker Cooker(pScan->Game());
Cooker.WriteProperty(SCAN, ScanProperties.Property(), ScanProperties.DataPointer(), true);
}
else
{
SCAN.WriteFourCC( FOURCC('SCAN') ); // SCAN magic
SCAN.WriteLong( 2 ); // Version number; must be 2
SCAN.WriteByte( 1 ); // Layer version number; must be 1
SCAN.WriteLong( 1 ); // Instance count
// Scans in MP2/3 are saved with the script object data format
// Write a dummy script object header here
SCAN.WriteLong( FOURCC('SNFO') ); // ScannableObjectInfo object ID
uint ScanInstanceSizeOffset = SCAN.Tell();
SCAN.WriteShort( 0 ); // Object size
SCAN.WriteLong( 0 ); // Instance ID
SCAN.WriteShort( 0 ); // Link count
CStructRef ScanProperties = pScan->ScanData();
CScriptCooker Cooker(pScan->Game());
Cooker.WriteProperty(SCAN, ScanProperties.Property(), ScanProperties.DataPointer(), false);
uint ScanInstanceEnd = SCAN.Tell();
uint ScanInstanceSize = ScanInstanceEnd - ScanInstanceSizeOffset - 2;
SCAN.GoTo(ScanInstanceSizeOffset);
SCAN.WriteShort( (uint16) ScanInstanceSize );
SCAN.GoTo(ScanInstanceEnd);
// Write dependency list
// @todo this output may not be 100% correct. Some dependencies seem to be conditionally excluded in base game.
// This may cause some assets to be unnecessarily loaded into memory ingame.
std::vector<CAssetID> Dependencies;
CAssetDependencyListBuilder Builder(pScan->Entry());
Builder.BuildDependencyList(Dependencies);
SCAN.WriteLong(Dependencies.size());
for (const CAssetID& kID : Dependencies)
{
CResourceEntry* pEntry = pScan->Entry()->ResourceStore()->FindEntry(kID);
ASSERT( pEntry != nullptr );
pEntry->CookedExtension().Write(SCAN);
kID.Write(SCAN);
}
}
return true;
}

View File

@ -0,0 +1,15 @@
#ifndef CSCANCOOKER_H
#define CSCANCOOKER_H
#include "Core/Resource/Scan/CScan.h"
/** Cooker class for writing game-compatible SCAN assets */
class CScanCooker
{
CScanCooker() {}
public:
static bool CookSCAN(CScan* pScan, IOutputStream& SCAN);
};
#endif // CSCANCOOKER_H

View File

@ -5,10 +5,9 @@
#include <Core/Resource/Script/Property/CEnumProperty.h>
#include <Core/Resource/Script/Property/CFlagsProperty.h>
void CScriptCooker::WriteProperty(IOutputStream& rOut, IProperty* pProperty, bool InAtomicStruct)
void CScriptCooker::WriteProperty(IOutputStream& rOut, IProperty* pProperty, void* pData, bool InAtomicStruct)
{
uint32 SizeOffset = 0, PropStart = 0;
void* pData = (mpArrayItemData ? mpArrayItemData : mpObject->PropertyData());
if (mGame >= EGame::EchoesDemo && !InAtomicStruct)
{
@ -197,7 +196,7 @@ void CScriptCooker::WriteProperty(IOutputStream& rOut, IProperty* pProperty, boo
}
for (uint32 PropertyIdx = 0; PropertyIdx < PropertiesToWrite.size(); PropertyIdx++)
WriteProperty(rOut, PropertiesToWrite[PropertyIdx], pStruct->IsAtomic());
WriteProperty(rOut, PropertiesToWrite[PropertyIdx], pData, pStruct->IsAtomic());
break;
}
@ -208,15 +207,11 @@ void CScriptCooker::WriteProperty(IOutputStream& rOut, IProperty* pProperty, boo
uint32 Count = pArray->ArrayCount(pData);
rOut.WriteLong(Count);
void* pOldItemData = mpArrayItemData;
for (uint32 ElementIdx = 0; ElementIdx < pArray->ArrayCount(pData); ElementIdx++)
{
mpArrayItemData = pArray->ItemPointer(pData, ElementIdx);
WriteProperty(rOut, pArray->ItemArchetype(), true);
WriteProperty(rOut, pArray->ItemArchetype(), pArray->ItemPointer(pData, ElementIdx), true);
}
mpArrayItemData = pOldItemData;
break;
}
@ -231,7 +226,6 @@ void CScriptCooker::WriteProperty(IOutputStream& rOut, IProperty* pProperty, boo
}
}
// ************ PUBLIC ************
void CScriptCooker::WriteInstance(IOutputStream& rOut, CScriptObject *pInstance)
{
ASSERT(pInstance->Area()->Game() == mGame);
@ -261,8 +255,7 @@ void CScriptCooker::WriteInstance(IOutputStream& rOut, CScriptObject *pInstance)
rOut.WriteLong(pLink->ReceiverID());
}
mpObject = pInstance;
WriteProperty(rOut, pInstance->Template()->Properties(), false);
WriteProperty(rOut, pInstance->Template()->Properties(), pInstance->PropertyData(), false);
uint32 InstanceEnd = rOut.Tell();
rOut.Seek(SizeOffset, SEEK_SET);

View File

@ -10,21 +10,16 @@
class CScriptCooker
{
EGame mGame;
CScriptObject* mpObject;
void* mpArrayItemData;
std::vector<CScriptObject*> mGeneratedObjects;
bool mWriteGeneratedSeparately;
void WriteProperty(IOutputStream& rOut, IProperty* pProperty, bool InAtomicStruct);
public:
CScriptCooker(EGame Game, bool WriteGeneratedObjectsSeparately = true)
: mGame(Game)
, mpObject(nullptr)
, mpArrayItemData(nullptr)
, mWriteGeneratedSeparately(WriteGeneratedObjectsSeparately && mGame >= EGame::EchoesDemo)
{}
void WriteProperty(IOutputStream& rOut, IProperty* pProperty, void* pData, bool InAtomicStruct);
void WriteInstance(IOutputStream& rOut, CScriptObject *pInstance);
void WriteLayer(IOutputStream& rOut, CScriptLayer *pLayer);
void WriteGeneratedLayer(IOutputStream& rOut);

View File

@ -0,0 +1,328 @@
#include "CStringCooker.h"
#include <Common/NBasics.h>
#include <algorithm>
void CStringCooker::WritePrimeDemoSTRG(IOutputStream& STRG)
{
uint StartOffset = STRG.Tell();
uint NumStrings = mpStringTable->NumStrings();
// Start writing the file...
STRG.WriteLong(0); // Dummy file size
uint TableStart = STRG.Tell();
STRG.WriteLong(NumStrings);
// Dummy string offsets
for (uint StringIdx = 0; StringIdx < NumStrings; StringIdx++)
STRG.WriteLong(0);
// Write strings
std::vector<uint> StringOffsets(NumStrings);
for (uint StringIdx = 0; StringIdx < NumStrings; StringIdx++)
{
StringOffsets[StringIdx] = STRG.Tell() - TableStart;
STRG.Write16String( mpStringTable->GetString(ELanguage::English, StringIdx).ToUTF16() );
}
// Fill in offsets
uint FileSize = STRG.Tell() - StartOffset;
STRG.GoTo(StartOffset);
STRG.WriteLong(FileSize);
STRG.Skip(4);
for (uint StringIdx = 0; StringIdx < NumStrings; StringIdx++)
STRG.WriteLong( StringOffsets[StringIdx] );
}
void CStringCooker::WritePrimeSTRG(IOutputStream& STRG)
{
// Magic/Version
STRG.WriteLong( 0x87654321 );
STRG.WriteLong( mpStringTable->Game() >= EGame::EchoesDemo ? 1 : 0 );
STRG.WriteLong( mpStringTable->NumLanguages() );
STRG.WriteLong( mpStringTable->NumStrings() );
// Language info
uint LanguagesStart = STRG.Tell();
for (uint LanguageIdx = 0; LanguageIdx < mpStringTable->NumLanguages(); LanguageIdx++)
{
const CStringTable::SLanguageData& kLanguage = mpStringTable->mLanguages[LanguageIdx];
STRG.WriteLong( (uint) kLanguage.Language );
STRG.WriteLong( 0 ); // Dummy offset
if ( mpStringTable->Game() >= EGame::EchoesDemo )
{
STRG.WriteLong( 0 ); // Dummy size
}
}
// Name Table
if ( mpStringTable->Game() >= EGame::EchoesDemo )
{
WriteNameTable(STRG);
}
// Strings
uint StringDataStart = STRG.Tell();
std::vector<uint> LanguageOffsets( mpStringTable->NumLanguages() );
std::vector<uint> LanguageSizes( mpStringTable->NumLanguages() );
for (uint LanguageIdx = 0; LanguageIdx < mpStringTable->NumLanguages(); LanguageIdx++)
{
const CStringTable::SLanguageData& kLanguage = mpStringTable->mLanguages[LanguageIdx];
uint LanguageStart = STRG.Tell();
LanguageOffsets[LanguageIdx] = LanguageStart - StringDataStart;
if ( mpStringTable->Game() == EGame::Prime )
{
STRG.WriteLong( 0 ); // Dummy size
}
// Fill dummy string offsets
uint StringOffsetBase = STRG.Tell();
for (uint StringIdx = 0; StringIdx < mpStringTable->NumStrings(); StringIdx++)
{
STRG.WriteLong( 0 );
}
// Write strings
std::vector<uint> StringOffsets( mpStringTable->NumStrings() );
for (uint StringIdx = 0; StringIdx < mpStringTable->NumStrings(); StringIdx++)
{
StringOffsets[StringIdx] = STRG.Tell() - StringOffsetBase;
STRG.Write16String( kLanguage.Strings[StringIdx].String.ToUTF16() );
}
// Go back and fill in size/offsets
uint LanguageEnd = STRG.Tell();
LanguageSizes[LanguageIdx] = LanguageEnd - StringOffsetBase;
STRG.GoTo(LanguageStart);
if ( mpStringTable->Game() == EGame::Prime )
{
STRG.WriteLong( LanguageSizes[LanguageIdx] );
}
for (uint StringIdx = 0; StringIdx < mpStringTable->NumStrings(); StringIdx++)
{
STRG.WriteLong( StringOffsets[StringIdx] );
}
STRG.GoTo(LanguageEnd);
}
uint STRGEnd = STRG.Tell();
// Fill in missing language data
STRG.GoTo(LanguagesStart);
for (uint LanguageIdx = 0; LanguageIdx < mpStringTable->NumLanguages(); LanguageIdx++)
{
STRG.Skip(4); // Skip language ID
STRG.WriteLong( LanguageOffsets[LanguageIdx] );
if ( mpStringTable->Game() >= EGame::EchoesDemo )
{
STRG.WriteLong( LanguageSizes[LanguageIdx] );
}
}
STRG.GoTo(STRGEnd);
}
void CStringCooker::WriteCorruptionSTRG(IOutputStream& STRG)
{
// Magic/Version
STRG.WriteLong( 0x87654321 );
STRG.WriteLong( 3 );
STRG.WriteLong( mpStringTable->NumLanguages() );
STRG.WriteLong( mpStringTable->NumStrings() );
// Name Table
WriteNameTable(STRG);
// Create some structures before continuing...
// In MP3 and DKCR, if a string has not been localized, then the English text
// is reused, instead of duplicating the string data like MP1 and MP2 would have.
struct SCookedLanguageData
{
ELanguage Language;
std::vector<uint> StringOffsets;
uint TotalSize;
};
std::vector<SCookedLanguageData> CookedLanguageData( mpStringTable->NumLanguages() );
for (uint LanguageIdx = 0; LanguageIdx < mpStringTable->NumLanguages(); LanguageIdx++)
{
const CStringTable::SLanguageData& kLanguageData = mpStringTable->mLanguages[LanguageIdx];
SCookedLanguageData& CookedData = CookedLanguageData[LanguageIdx];
CookedData.Language = kLanguageData.Language;
CookedData.StringOffsets.resize( mpStringTable->NumStrings() );
CookedData.TotalSize = 0;
}
// Language IDs
for (uint LanguageIdx = 0; LanguageIdx < mpStringTable->NumLanguages(); LanguageIdx++)
{
STRG.WriteLong( (uint) CookedLanguageData[LanguageIdx].Language );
}
// Language Info
uint LanguageInfoStart = STRG.Tell();
for (uint LanguageIdx = 0; LanguageIdx < mpStringTable->NumLanguages(); LanguageIdx++)
{
// Fill language size/offsets with dummy data...
STRG.WriteLong( 0 );
for (uint StringIdx = 0; StringIdx < mpStringTable->NumStrings(); StringIdx++)
STRG.WriteLong( 0 );
}
// Some of the following code assumes that language 0 is English.
ASSERT( mpStringTable->mLanguages[0].Language == ELanguage::English );
// Strings
uint StringsStart = STRG.Tell();
for (uint StringIdx = 0; StringIdx < mpStringTable->NumStrings(); StringIdx++)
{
for (uint LanguageIdx = 0; LanguageIdx < mpStringTable->NumLanguages(); LanguageIdx++)
{
const CStringTable::SLanguageData& kLanguageData = mpStringTable->mLanguages[LanguageIdx];
const CStringTable::SStringData& kStringData = kLanguageData.Strings[StringIdx];
SCookedLanguageData& CookedData = CookedLanguageData[LanguageIdx];
// If the "localized" flag is disabled, then we will not write this string. Instead, it will
// reuse the offset for the English text.
if (LanguageIdx == 0 || kStringData.IsLocalized)
{
CookedData.StringOffsets[StringIdx] = STRG.Tell() - StringsStart;
CookedData.TotalSize += kStringData.String.Size() + 1; // +1 for terminating zero
STRG.WriteLong( kStringData.String.Size() + 1 );
STRG.WriteString( kStringData.String );
}
else
{
CookedData.StringOffsets[StringIdx] = CookedLanguageData[0].StringOffsets[StringIdx];
CookedData.TotalSize += mpStringTable->mLanguages[0].Strings[StringIdx].String.Size() + 1; // +1 for terminating zero
}
}
}
uint STRGEnd = STRG.Tell();
// Fill in missing language data
STRG.GoTo(LanguageInfoStart);
for (uint LanguageIdx = 0; LanguageIdx < mpStringTable->NumLanguages(); LanguageIdx++)
{
const SCookedLanguageData& kCookedData = CookedLanguageData[LanguageIdx];
STRG.WriteLong( kCookedData.TotalSize );
for (uint StringIdx = 0; StringIdx < mpStringTable->NumStrings(); StringIdx++)
STRG.WriteLong( kCookedData.StringOffsets[StringIdx] );
}
STRG.GoTo(STRGEnd);
}
void CStringCooker::WriteNameTable(IOutputStream& STRG)
{
// Build a list of name entries to put in the map
struct SNameEntry
{
uint Offset;
uint Index;
TString Name;
};
std::vector<SNameEntry> NameEntries;
for (uint StringIdx = 0; StringIdx < mpStringTable->NumStrings(); StringIdx++)
{
SNameEntry Entry;
Entry.Offset = 0;
Entry.Index = StringIdx;
Entry.Name = mpStringTable->StringNameByIndex(StringIdx);
if (!Entry.Name.IsEmpty())
{
NameEntries.push_back(Entry);
}
}
std::stable_sort( NameEntries.begin(), NameEntries.end(), [](const SNameEntry& kLHS, const SNameEntry& kRHS) -> bool {
return kLHS.Name < kRHS.Name;
});
// Write out name entries
uint NameTableStart = STRG.Tell();
STRG.WriteLong( NameEntries.size() );
STRG.WriteLong( 0 ); // Dummy name table size
uint NameTableOffsetsStart = STRG.Tell();
for (uint NameIdx = 0; NameIdx < NameEntries.size(); NameIdx++)
{
STRG.WriteLong( 0 ); // Dummy name offset
STRG.WriteLong( NameEntries[NameIdx].Index );
}
// Write out names
std::vector<uint> NameOffsets( NameEntries.size() );
for (uint NameIdx = 0; NameIdx < NameEntries.size(); NameIdx++)
{
NameOffsets[NameIdx] = STRG.Tell() - NameTableOffsetsStart;
STRG.WriteString( NameEntries[NameIdx].Name );
}
// Fill out sizes and offsets
uint NameTableEnd = STRG.Tell();
uint NameTableSize = NameTableEnd - NameTableOffsetsStart;
STRG.GoTo(NameTableStart);
STRG.Skip(4);
STRG.WriteLong(NameTableSize);
for (uint NameIdx = 0; NameIdx < NameEntries.size(); NameIdx++)
{
STRG.WriteLong( NameOffsets[NameIdx] );
STRG.Skip(4);
}
STRG.GoTo(NameTableEnd);
}
/** Static entry point */
bool CStringCooker::CookSTRG(CStringTable* pStringTable, IOutputStream& STRG)
{
CStringCooker Cooker(pStringTable);
switch (pStringTable->Game())
{
case EGame::PrimeDemo:
Cooker.WritePrimeDemoSTRG(STRG);
return true;
case EGame::Prime:
case EGame::EchoesDemo:
case EGame::Echoes:
case EGame::CorruptionProto:
Cooker.WritePrimeSTRG(STRG);
return true;
case EGame::Corruption:
case EGame::DKCReturns:
Cooker.WriteCorruptionSTRG(STRG);
return true;
default:
return false;
}
}

View File

@ -0,0 +1,25 @@
#ifndef CSTRINGCOOKER_H
#define CSTRINGCOOKER_H
#include "Core/Resource/TResPtr.h"
#include "Core/Resource/StringTable/CStringTable.h"
/** Cooker class for writing game-compatible STRG assets */
class CStringCooker
{
TResPtr<CStringTable> mpStringTable;
CStringCooker(CStringTable* pStringTable)
: mpStringTable(pStringTable) {}
public:
void WritePrimeDemoSTRG(IOutputStream& STRG);
void WritePrimeSTRG(IOutputStream& STRG);
void WriteCorruptionSTRG(IOutputStream& STRG);
void WriteNameTable(IOutputStream& STRG);
/** Static entry point */
static bool CookSTRG(CStringTable* pStringTable, IOutputStream& STRG);
};
#endif // CSTRINGCOOKER_H

View File

@ -66,7 +66,7 @@ enum class EResourceType
StringList,
StringTable,
Texture,
Tweak,
Tweaks,
UserEvaluatorData,
Video,
World,

View File

@ -81,7 +81,7 @@ void CAnimationLoader::ReadUncompressedANIM()
// Echoes only - rotation channel indices
std::vector<uint8> RotationIndices;
if (mGame == EGame::Echoes)
if (mGame >= EGame::EchoesDemo)
{
uint32 NumRotationIndices = mpInput->ReadLong();
RotationIndices.resize(NumRotationIndices);
@ -125,7 +125,7 @@ void CAnimationLoader::ReadUncompressedANIM()
// Echoes only - scale channel indices
std::vector<uint8> ScaleIndices;
if (mGame == EGame::Echoes)
if (mGame >= EGame::EchoesDemo)
{
uint32 NumScaleIndices = mpInput->ReadLong();
ScaleIndices.resize(NumScaleIndices);
@ -161,7 +161,7 @@ void CAnimationLoader::ReadUncompressedANIM()
}
// Read bone transforms
if (mGame == EGame::Echoes)
if (mGame >= EGame::EchoesDemo)
{
mpInput->Seek(0x4, SEEK_CUR); // Skipping scale key count
mpAnim->mScaleChannels.resize(NumScaleChannels);
@ -208,23 +208,24 @@ void CAnimationLoader::ReadCompressedANIM()
// Header
mpInput->Seek(0x4, SEEK_CUR); // Skip alloc size
if (mGame == EGame::Invalid)
mGame = (mpInput->PeekShort() == 0x0101 ? EGame::Echoes : EGame::Prime);
// Version check
mGame = (mpInput->PeekShort() == 0x0101 ? EGame::Echoes : EGame::Prime);
if (mGame == EGame::Prime)
// Check the ANIM resource's game instead of the version check we just determined.
// The Echoes demo has some ANIMs that use MP1's format, but don't have the EVNT reference.
if (mpAnim->Game() <= EGame::Prime)
{
mpAnim->mpEventData = gpResourceStore->LoadResource<CAnimEventData>(mpInput->ReadLong());
mpInput->Seek(0x4, SEEK_CUR); // Skip unknown
}
else mpInput->Seek(0x2, SEEK_CUR); // Skip unknowns
mpInput->Seek(mGame <= EGame::Prime ? 4 : 2, SEEK_CUR); // Skip unknowns
mpAnim->mDuration = mpInput->ReadFloat();
mpAnim->mTickInterval = mpInput->ReadFloat();
mpInput->Seek(0x8, SEEK_CUR); // Skip two unknown values
mRotationDivisor = mpInput->ReadLong();
mTranslationMultiplier = mpInput->ReadFloat();
if (mGame == EGame::Echoes) mScaleMultiplier = mpInput->ReadFloat();
if (mGame >= EGame::EchoesDemo) mScaleMultiplier = mpInput->ReadFloat();
uint32 NumBoneChannels = mpInput->ReadLong();
mpInput->Seek(0x4, SEEK_CUR); // Skip unknown value
@ -284,7 +285,7 @@ void CAnimationLoader::ReadCompressedANIM()
// Read scale parameters
uint8 ScaleIdx = 0xFF;
if (mGame == EGame::Echoes)
if (mGame >= EGame::EchoesDemo)
{
rChan.NumScaleKeys = mpInput->ReadShort();

View File

@ -22,6 +22,8 @@
#include "CUnsupportedParticleLoader.h"
#include "CWorldLoader.h"
#include "Core/Tweaks/CTweakLoader.h"
#include "Core/Resource/Resources.h"
// Static helper class to allow spawning resources based on an EResType
@ -97,6 +99,7 @@ public:
case EResourceType::StringList: pRes = CAudioGroupLoader::LoadSTLC(rInput, pEntry); break;
case EResourceType::StringTable: pRes = CStringLoader::LoadSTRG(rInput, pEntry); break;
case EResourceType::Texture: pRes = CTextureDecoder::LoadTXTR(rInput, pEntry); break;
case EResourceType::Tweaks: pRes = CTweakLoader::LoadCTWK(rInput, pEntry); break;
case EResourceType::World: pRes = CWorldLoader::LoadMLVL(rInput, pEntry); break;
case EResourceType::StateMachine:

View File

@ -1,306 +1,83 @@
#include "CScanLoader.h"
#include "Core/GameProject/CResourceStore.h"
#include "CScriptLoader.h"
#include <Common/Log.h>
CScanLoader::CScanLoader()
CScan* CScanLoader::LoadScanMP1(IInputStream& SCAN, CResourceEntry* pEntry)
{
}
CScan* CScanLoader::LoadScanMP1(IInputStream& rSCAN)
{
// Basic support at the moment - don't read animation/scan image data
mpScan->mFrameID = CAssetID(rSCAN, k32Bit);
mpScan->mpStringTable = gpResourceStore->LoadResource(rSCAN.ReadLong(), EResourceType::StringTable);
mpScan->mIsSlow = (rSCAN.ReadLong() != 0);
mpScan->mCategory = (CScan::ELogbookCategory) rSCAN.ReadLong();
mpScan->mIsImportant = (rSCAN.ReadByte() == 1);
for (uint32 iImg = 0; iImg < 4; iImg++)
{
mpScan->mScanImageTextures[iImg] = CAssetID(rSCAN, k32Bit);
rSCAN.Seek(0x18, SEEK_CUR);
}
return mpScan;
}
CScan* CScanLoader::LoadScanMP2(IInputStream& rSCAN)
{
// The SCAN format in MP2 embeds a SNFO object using the same format as SCLY
// However since the contents of the file are consistent there's no need to delegate to CScriptLoader
rSCAN.Skip(0x1);
uint32 NumInstances = rSCAN.ReadLong();
if (NumInstances != 1)
{
errorf("%s: SCAN has multiple instances", *rSCAN.GetSourceString());
return nullptr;
}
uint32 ScanInfoStart = rSCAN.Tell();
CFourCC SNFO(rSCAN);
if (SNFO != FOURCC('SNFO'))
{
errorf("%s [0x%X]: Unrecognized SCAN object type: %s", *rSCAN.GetSourceString(), ScanInfoStart, *SNFO.ToString());
return nullptr;
}
uint16 InstanceSize = rSCAN.ReadShort();
uint32 InstanceEnd = rSCAN.Tell() + InstanceSize;
rSCAN.Skip(0x4);
uint16 NumConnections = rSCAN.ReadShort();
if (NumConnections > 0)
{
warnf("%s [0x%X]: SNFO object in SCAN has connections", *rSCAN.GetSourceString(), ScanInfoStart);
rSCAN.Skip(NumConnections * 0xC);
}
uint32 BasePropID = rSCAN.ReadLong();
if (BasePropID != 0xFFFFFFFF)
{
errorf("%s [0x%X]: Invalid base property ID: 0x%08X", *rSCAN.GetSourceString(), rSCAN.Tell() - 4, BasePropID);
return nullptr;
}
rSCAN.Skip(0x2);
uint16 NumProperties = rSCAN.ReadShort();
switch (NumProperties)
{
case 0x14:
case 0xB:
mpScan = new CScan(mpEntry);
LoadParamsMP2(rSCAN, NumProperties);
break;
case 0x12:
case 0x15:
case 0x16:
mpScan = new CScan(mpEntry);
LoadParamsMP3(rSCAN, NumProperties);
break;
default:
errorf("%s [0x%X]: Invalid SNFO property count: 0x%X", *rSCAN.GetSourceString(), rSCAN.Tell() - 2, NumProperties);
return nullptr;
}
// Load MP3 dependency list
if (mpScan->Game() == EGame::Corruption)
{
rSCAN.GoTo(InstanceEnd);
uint32 NumDeps = rSCAN.ReadLong();
for (uint32 DepIdx = 0; DepIdx < NumDeps; DepIdx++)
{
rSCAN.Skip(4);
CAssetID ID(rSCAN, mpScan->Game());
mpScan->mDependencyList.push_back(ID);
}
}
return mpScan;
}
void CScanLoader::LoadParamsMP2(IInputStream& rSCAN, uint16 NumProperties)
{
// Function begins after the SNFO property count
mpScan->mSecondaryModels.resize(9);
for (uint32 iProp = 0; iProp < NumProperties; iProp++)
{
uint32 PropertyID = rSCAN.ReadLong();
uint16 PropertySize = rSCAN.ReadShort();
uint32 Next = rSCAN.Tell() + PropertySize;
switch (PropertyID)
{
case 0x2F5B6423:
mpScan->mpStringTable = gpResourceStore->LoadResource(rSCAN.ReadLong(), EResourceType::StringTable);
break;
case 0xC308A322:
mpScan->mIsSlow = (rSCAN.ReadLong() != 0);
break;
case 0x7B714814:
mpScan->mIsImportant = rSCAN.ReadBool();
break;
case 0x1733B1EC:
mpScan->mUseLogbookModelPostScan = rSCAN.ReadBool();
break;
case 0x53336141:
mpScan->mPostOverrideTexture = CAssetID(rSCAN, mVersion);
break;
case 0x3DE0BA64:
mpScan->mLogbookDefaultRotX = rSCAN.ReadFloat();
break;
case 0x2ADD6628:
mpScan->mLogbookDefaultRotZ = rSCAN.ReadFloat();
break;
case 0xD0C15066:
mpScan->mLogbookScale = rSCAN.ReadFloat();
break;
case 0xB7ADC418:
mpScan->mLogbookModel = CAssetID(rSCAN, mVersion);
break;
case 0x15694EE1:
mpScan->mLogbookAnimParams = CAnimationParameters(rSCAN, mVersion);
break;
case 0x58F9FE99:
mpScan->mUnknownAnimParams = CAnimationParameters(rSCAN, mVersion);
break;
case 0x1C5B4A3A:
LoadScanInfoSecondaryModel( rSCAN, mpScan->mSecondaryModels[0] );
break;
case 0x8728A0EE:
LoadScanInfoSecondaryModel( rSCAN, mpScan->mSecondaryModels[1] );
break;
case 0xF1CD99D3:
LoadScanInfoSecondaryModel( rSCAN, mpScan->mSecondaryModels[2] );
break;
case 0x6ABE7307:
LoadScanInfoSecondaryModel( rSCAN, mpScan->mSecondaryModels[3] );
break;
case 0x1C07EBA9:
LoadScanInfoSecondaryModel( rSCAN, mpScan->mSecondaryModels[4] );
break;
case 0x8774017D:
LoadScanInfoSecondaryModel( rSCAN, mpScan->mSecondaryModels[5] );
break;
case 0xF1913840:
LoadScanInfoSecondaryModel( rSCAN, mpScan->mSecondaryModels[6] );
break;
case 0x6AE2D294:
LoadScanInfoSecondaryModel( rSCAN, mpScan->mSecondaryModels[7] );
break;
case 0x1CE2091C:
LoadScanInfoSecondaryModel( rSCAN, mpScan->mSecondaryModels[8] );
break;
}
rSCAN.GoTo(Next);
}
mpScan->mCategory = CScan::ELogbookCategory::None;
}
void CScanLoader::LoadParamsMP3(IInputStream& rSCAN, uint16 NumProperties)
{
// Function begins after the SNFO property count
for (uint32 iProp = 0; iProp < NumProperties; iProp++)
{
uint32 PropertyID = rSCAN.ReadLong();
uint16 PropertySize = rSCAN.ReadShort();
uint32 Next = rSCAN.Tell() + PropertySize;
switch (PropertyID)
{
case 0x2F5B6423:
mpScan->mpStringTable = gpResourceStore->LoadResource(rSCAN.ReadLongLong(), EResourceType::Scan);
break;
case 0xC308A322:
mpScan->mIsSlow = (rSCAN.ReadLong() != 0);
break;
case 0x7B714814:
mpScan->mIsImportant = (rSCAN.ReadByte() != 0);
break;
}
rSCAN.GoTo(Next);
}
mpScan->mCategory = CScan::ELogbookCategory::None;
}
void CScanLoader::LoadScanInfoSecondaryModel(IInputStream& rSCAN, CScan::SScanInfoSecondaryModel& rSecondaryModel)
{
uint16 NumProperties = rSCAN.ReadShort();
for (uint32 iProp = 0; iProp < NumProperties; iProp++)
{
uint32 PropertyID = rSCAN.ReadLong();
uint16 PropertySize = rSCAN.ReadShort();
uint32 Next = rSCAN.Tell() + PropertySize;
switch (PropertyID)
{
case 0x1F7921BC:
rSecondaryModel.ModelID = CAssetID(rSCAN, mVersion);
break;
case 0xCDD202D1:
rSecondaryModel.AnimParams = CAnimationParameters(rSCAN, mVersion);
break;
case 0x3EA2BED8:
rSecondaryModel.AttachBoneName = rSCAN.ReadString();
break;
}
rSCAN.GoTo(Next);
}
}
// ************ STATIC/PUBLIC ************
CScan* CScanLoader::LoadSCAN(IInputStream& rSCAN, CResourceEntry *pEntry)
{
if (!rSCAN.IsValid()) return nullptr;
/* Switching to EGame enum here isn't really useful unfortunately
* because the MP1 demo can be 1, 2, or 3, while MP1 is 5 and MP2+ is 2
* MP1 is the only one that starts with 5 so that is a consistent check for now
* Better version checks will be implemented when the other versions are
* better-understood. */
uint32 FileVersion = rSCAN.ReadLong();
uint32 Magic = rSCAN.ReadLong();
// Echoes+
if (FileVersion == FOURCC('SCAN'))
{
// The MP2 load function will check for MP3
CScanLoader Loader;
Loader.mVersion = EGame::Echoes;
Loader.mpEntry = pEntry;
if (Magic == 0x01000000) rSCAN.Seek(-4, SEEK_CUR); // The version number isn't present in the Echoes demo
return Loader.LoadScanMP2(rSCAN);
}
// Validate magic
uint Magic = SCAN.ReadLong();
if (Magic != 0x0BADBEEF)
{
errorf("%s: Invalid SCAN magic: 0x%08X", *rSCAN.GetSourceString(), Magic);
errorf("Invalid magic in SCAN asset: 0x%08X", Magic);
return nullptr;
}
if (FileVersion != 5)
{
errorf("%s: Unsupported SCAN version: 0x%X", *rSCAN.GetSourceString(), FileVersion);
return nullptr;
}
// MP1 SCAN - read the file!
CScanLoader Loader;
Loader.mVersion = EGame::Prime;
Loader.mpScan = new CScan(pEntry);
Loader.mpEntry = pEntry;
return Loader.LoadScanMP1(rSCAN);
// The SCAN format in MP2 and later games uses the script loader to load SCAN parameters.
// The MP1 format is not loaded the same way, as far as I'm aware, and is loaded the same
// way as a normal file format... however, since we support all games, we need to support
// the script object method for proper MP2/3 support (including dealing with property names/IDs).
// So, it's simplest to use the script loader to load the MP1 SCAN format as well... that enables
// us to just create one class for all SCAN assets that works for every game.
mpScan = new CScan(pEntry);
CScriptLoader::LoadStructData(SCAN, mpScan->ScanData());
return mpScan;
}
CScan* CScanLoader::LoadScanMP2(IInputStream& SCAN, CResourceEntry* pEntry)
{
// Validate version
uint Version = SCAN.ReadLong();
if (Version != 2)
{
errorf("Unrecognized SCAN version: %d", Version);
return nullptr;
}
// The SCAN format in MP2 embeds a ScannableObjectInfo script object using the same file format as SCLY.
// As such we use CScriptLoader to load parameters, but since we don't actually want to create a script
// object, we will skip past the script object/layer header and just load the properties directly.
SCAN.Skip(0x17);
mpScan = new CScan(pEntry);
CScriptLoader::LoadStructData(SCAN, mpScan->ScanData());
return mpScan;
}
// ************ STATIC/PUBLIC ************
CScan* CScanLoader::LoadSCAN(IInputStream& SCAN, CResourceEntry *pEntry)
{
if (!SCAN.IsValid()) return nullptr;
// MP1 SCAN format starts with a version number and then follows with a magic.
// The demo can be 1, 2, or 3, while the final build is 5.
// The MP2 SCAN format starts with a 'SCAN' magic.
uint VersionCheck = SCAN.ReadLong();
// Echoes+
if (VersionCheck == FOURCC('SCAN'))
{
CScanLoader Loader;
return Loader.LoadScanMP2(SCAN, pEntry);
}
// MP1
else if (VersionCheck <= 5)
{
if (VersionCheck == 5)
{
CScanLoader Loader;
return Loader.LoadScanMP1(SCAN, pEntry);
}
else
{
errorf("%s: Unsupported SCAN version: %d", VersionCheck);
return nullptr;
}
}
else
{
errorf("Failed to identify SCAN version: 0x%X", VersionCheck);
return nullptr;
}
}

View File

@ -1,21 +1,16 @@
#ifndef CSCANLOADER_H
#define CSCANLOADER_H
#include "Core/Resource/CScan.h"
#include "Core/Resource/Scan/CScan.h"
#include <Common/EGame.h>
class CScanLoader
{
TResPtr<CScan> mpScan;
CResourceEntry *mpEntry;
EGame mVersion;
CScanLoader();
CScan* LoadScanMP1(IInputStream& rSCAN);
CScan* LoadScanMP2(IInputStream& rSCAN);
void LoadParamsMP2(IInputStream& rSCAN, uint16 NumProperties);
void LoadParamsMP3(IInputStream& rSCAN, uint16 NumProperties);
void LoadScanInfoSecondaryModel(IInputStream& rSCAN, CScan::SScanInfoSecondaryModel& rSecondaryModel);
CScanLoader() {}
CScan* LoadScanMP1(IInputStream& SCAN, CResourceEntry* pEntry);
CScan* LoadScanMP2(IInputStream& SCAN, CResourceEntry* pEntry);
public:
static CScan* LoadSCAN(IInputStream& rSCAN, CResourceEntry *pEntry);

View File

@ -15,13 +15,13 @@
CScriptLoader::CScriptLoader()
: mpObj(nullptr)
, mpArrayItemData(nullptr)
, mpCurrentData(nullptr)
{
}
void CScriptLoader::ReadProperty(IProperty *pProp, uint32 Size, IInputStream& rSCLY)
{
void* pData = (mpArrayItemData ? mpArrayItemData : mpObj->mPropertyData.data());
void* pData = (mpCurrentData ? mpCurrentData : mpObj->mPropertyData.data());
switch (pProp->Type())
{
@ -151,7 +151,7 @@ void CScriptLoader::ReadProperty(IProperty *pProp, uint32 Size, IInputStream& rS
#if VALIDATE_PROPERTY_VALUES
CAssetID ID = pAsset->ValueRef(pData);
if (ID.IsValid())
if (ID.IsValid() && gpResourceStore)
{
CResourceEntry *pEntry = gpResourceStore->FindEntry(ID);
@ -237,7 +237,7 @@ void CScriptLoader::ReadProperty(IProperty *pProp, uint32 Size, IInputStream& rS
int Count = rSCLY.ReadLong();
pArray->Resize(pData, Count);
void* pOldArrayItemData = mpArrayItemData;
void* pOldData = mpCurrentData;
// Make sure the array archetype is atomic... non-atomic array archetypes is not supported
// because arrays can only have one possible archetype so having property IDs here wouldn't make sense
@ -257,11 +257,11 @@ void CScriptLoader::ReadProperty(IProperty *pProp, uint32 Size, IInputStream& rS
* migrated to Sequence properties eventually, so there isn't really any good reason to spend a lot of effort refactoring
* things to make this cleaner
*/
mpArrayItemData = pArray->ItemPointer(pData, ElementIdx);
mpCurrentData = pArray->ItemPointer(pData, ElementIdx);
ReadProperty(pArray->ItemArchetype(), 0, rSCLY);
}
mpArrayItemData = pOldArrayItemData;
mpCurrentData = pOldData;
break;
}
@ -505,3 +505,20 @@ CScriptObject* CScriptLoader::LoadInstance(IInputStream& rSCLY, CGameArea *pArea
else
return Loader.LoadObjectMP2(rSCLY);
}
void CScriptLoader::LoadStructData(IInputStream& rInput, CStructRef InStruct)
{
if (!rInput.IsValid()) return;
CScriptLoader Loader;
Loader.mVersion = InStruct.Property()->Game();
Loader.mpGameTemplate = NGameList::GetGameTemplate(Loader.mVersion);
Loader.mpArea = nullptr;
Loader.mpLayer = nullptr;
Loader.mpCurrentData = InStruct.DataPointer();
if (Loader.mVersion <= EGame::Prime)
Loader.LoadStructMP1(rInput, InStruct.Property());
else
Loader.LoadStructMP2(rInput, InStruct.Property());
}

View File

@ -15,8 +15,8 @@ class CScriptLoader
CGameArea* mpArea;
CGameTemplate *mpGameTemplate;
// Current array item pointer
void* mpArrayItemData;
// Current data pointer
void* mpCurrentData;
CScriptLoader();
void ReadProperty(IProperty* pProp, uint32 Size, IInputStream& rSCLY);
@ -32,6 +32,7 @@ class CScriptLoader
public:
static CScriptLayer* LoadLayer(IInputStream& rSCLY, CGameArea *pArea, EGame Version);
static CScriptObject* LoadInstance(IInputStream& rSCLY, CGameArea *pArea, CScriptLayer *pLayer, EGame Version, bool ForceReturnsFormat);
static void LoadStructData(IInputStream& rInput, CStructRef InStruct);
};
#endif // CSCRIPTLOADER_H

View File

@ -1,193 +1,234 @@
#include "CStringLoader.h"
#include <Common/Log.h>
#include <Common/Math/MathUtil.h>
void CStringLoader::LoadPrimeDemoSTRG(IInputStream& rSTRG)
void CStringLoader::LoadPrimeDemoSTRG(IInputStream& STRG)
{
// This function starts at 0x4 in the file - right after the size
// This STRG version only supports one language per file
mpStringTable->mLangTables.resize(1);
CStringTable::SLangTable* Lang = &mpStringTable->mLangTables[1];
Lang->Language = "ENGL";
uint32 TableStart = rSTRG.Tell();
mpStringTable->mLanguages.resize(1);
CStringTable::SLanguageData& Language = mpStringTable->mLanguages[0];
Language.Language = ELanguage::English;
uint TableStart = STRG.Tell();
// Header
uint32 NumStrings = rSTRG.ReadLong();
Lang->Strings.resize(NumStrings);
mpStringTable->mNumStrings = NumStrings;
uint NumStrings = STRG.ReadLong();
Language.Strings.resize(NumStrings);
// String offsets (yeah, that wasn't much of a header)
std::vector<uint32> StringOffsets(NumStrings);
for (uint32 iOff = 0; iOff < StringOffsets.size(); iOff++)
StringOffsets[iOff] = rSTRG.ReadLong();
std::vector<uint> StringOffsets(NumStrings);
for (uint32 OffsetIdx = 0; OffsetIdx < NumStrings; OffsetIdx++)
StringOffsets[OffsetIdx] = STRG.ReadLong();
// Strings
for (uint32 iStr = 0; iStr < NumStrings; iStr++)
for (uint StringIdx = 0; StringIdx < NumStrings; StringIdx++)
{
rSTRG.Seek(TableStart + StringOffsets[iStr], SEEK_SET);
Lang->Strings[iStr] = rSTRG.ReadWString().ToUTF8();
STRG.GoTo( TableStart + StringOffsets[StringIdx] );
Language.Strings[StringIdx].String = STRG.Read16String().ToUTF8();
}
}
void CStringLoader::LoadPrimeSTRG(IInputStream& rSTRG)
void CStringLoader::LoadPrimeSTRG(IInputStream& STRG)
{
// This function starts at 0x8 in the file, after magic/version
// Header
uint32 NumLanguages = rSTRG.ReadLong();
uint32 NumStrings = rSTRG.ReadLong();
mpStringTable->mNumStrings = NumStrings;
uint NumLanguages = STRG.ReadLong();
uint NumStrings = STRG.ReadLong();
// Language definitions
mpStringTable->mLangTables.resize(NumLanguages);
std::vector<uint32> LangOffsets(NumLanguages);
mpStringTable->mLanguages.resize(NumLanguages);
std::vector<uint> LanguageOffsets(NumLanguages);
for (uint32 iLang = 0; iLang < NumLanguages; iLang++)
for (uint LanguageIdx = 0; LanguageIdx < NumLanguages; LanguageIdx++)
{
mpStringTable->mLangTables[iLang].Language = CFourCC(rSTRG);
LangOffsets[iLang] = rSTRG.ReadLong();
if (mVersion == EGame::Echoes) rSTRG.Seek(0x4, SEEK_CUR); // Skipping strings size
mpStringTable->mLanguages[LanguageIdx].Language = (ELanguage) STRG.ReadFourCC();
LanguageOffsets[LanguageIdx] = STRG.ReadLong();
// Skip strings size in MP2
if (mVersion >= EGame::EchoesDemo)
{
STRG.Skip(4);
}
}
// Some of the following code assumes that language 0 is English
ASSERT( mpStringTable->mLanguages[0].Language == ELanguage::English );
// String names
if (mVersion == EGame::Echoes)
LoadNameTable(rSTRG);
if (mVersion >= EGame::EchoesDemo)
{
LoadNameTable(STRG);
}
// Strings
uint32 StringsStart = rSTRG.Tell();
for (uint32 iLang = 0; iLang < NumLanguages; iLang++)
uint StringsStart = STRG.Tell();
for (uint32 LanguageIdx = 0; LanguageIdx < NumLanguages; LanguageIdx++)
{
rSTRG.Seek(StringsStart + LangOffsets[iLang], SEEK_SET);
if (mVersion == EGame::Prime) rSTRG.Seek(0x4, SEEK_CUR); // Skipping strings size
STRG.GoTo( StringsStart + LanguageOffsets[LanguageIdx] );
uint32 LangStart = rSTRG.Tell();
CStringTable::SLangTable* pLang = &mpStringTable->mLangTables[iLang];
pLang->Strings.resize(NumStrings);
// Skip strings size in MP1
if (mVersion == EGame::Prime)
{
STRG.Skip(4);
}
CStringTable::SLanguageData& Language = mpStringTable->mLanguages[LanguageIdx];
Language.Strings.resize(NumStrings);
// Offsets
std::vector<uint32> StringOffsets(NumStrings);
for (uint32 iOff = 0; iOff < NumStrings; iOff++)
StringOffsets[iOff] = rSTRG.ReadLong();
uint LanguageStart = STRG.Tell();
std::vector<uint> StringOffsets(NumStrings);
for (uint StringIdx = 0; StringIdx < NumStrings; StringIdx++)
{
StringOffsets[StringIdx] = LanguageStart + STRG.ReadLong();
}
// The actual strings
for (uint32 iStr = 0; iStr < NumStrings; iStr++)
for (uint StringIdx = 0; StringIdx < NumStrings; StringIdx++)
{
rSTRG.Seek(LangStart + StringOffsets[iStr], SEEK_SET);
pLang->Strings[iStr] = rSTRG.ReadWString().ToUTF8();
STRG.GoTo( StringOffsets[StringIdx] );
TString String = STRG.Read16String().ToUTF8();
// Flag the string as localized if it is different than the English
// version of the same string.
const TString& kEnglishString = (StringIdx == 0 ? String :
mpStringTable->mLanguages[0].Strings[StringIdx].String);
bool IsLocalized = (LanguageIdx == 0 || String != kEnglishString);
Language.Strings[StringIdx].String = String;
Language.Strings[StringIdx].IsLocalized = IsLocalized;
}
}
}
void CStringLoader::LoadCorruptionSTRG(IInputStream& rSTRG)
void CStringLoader::LoadCorruptionSTRG(IInputStream& STRG)
{
// This function starts at 0x8 in the file, after magic/version
// Header
uint32 NumLanguages = rSTRG.ReadLong();
uint32 NumStrings = rSTRG.ReadLong();
mpStringTable->mNumStrings = NumStrings;
uint NumLanguages = STRG.ReadLong();
uint NumStrings = STRG.ReadLong();
// String names
LoadNameTable(rSTRG);
LoadNameTable(STRG);
// Language definitions
mpStringTable->mLangTables.resize(NumLanguages);
std::vector<std::vector<uint32>> LangOffsets(NumLanguages);
mpStringTable->mLanguages.resize(NumLanguages);
std::vector< std::vector<uint> > LanguageOffsets(NumLanguages);
for (uint32 iLang = 0; iLang < NumLanguages; iLang++)
mpStringTable->mLangTables[iLang].Language = CFourCC(rSTRG);
for (uint32 iLang = 0; iLang < NumLanguages; iLang++)
for (uint LanguageIdx = 0; LanguageIdx < NumLanguages; LanguageIdx++)
{
LangOffsets[iLang].resize(NumStrings);
rSTRG.Seek(0x4, SEEK_CUR); // Skipping total string size
for (uint32 iStr = 0; iStr < NumStrings; iStr++)
LangOffsets[iLang][iStr] = rSTRG.ReadLong();
mpStringTable->mLanguages[LanguageIdx].Language = (ELanguage) STRG.ReadFourCC();
}
// Strings
uint32 StringsStart = rSTRG.Tell();
for (uint32 iLang = 0; iLang < NumLanguages; iLang++)
for (uint LanguageIdx = 0; LanguageIdx < NumLanguages; LanguageIdx++)
{
CStringTable::SLangTable *pLang = &mpStringTable->mLangTables[iLang];
pLang->Strings.resize(NumStrings);
LanguageOffsets[LanguageIdx].resize(NumStrings);
STRG.Skip(4); // Skipping total string size
for (uint32 iStr = 0; iStr < NumStrings; iStr++)
for (uint StringIdx = 0; StringIdx < NumStrings; StringIdx++)
{
rSTRG.Seek(StringsStart + LangOffsets[iLang][iStr], SEEK_SET);
rSTRG.Seek(0x4, SEEK_CUR); // Skipping string size
LanguageOffsets[LanguageIdx][StringIdx] = STRG.ReadLong();
}
}
pLang->Strings[iStr] = rSTRG.ReadString();
// Some of the following code assumes that language 0 is English
ASSERT( mpStringTable->mLanguages[0].Language == ELanguage::English );
// Strings
uint StringsStart = STRG.Tell();
for (uint LanguageIdx = 0; LanguageIdx < NumLanguages; LanguageIdx++)
{
CStringTable::SLanguageData& Language = mpStringTable->mLanguages[LanguageIdx];
Language.Strings.resize(NumStrings);
for (uint StringIdx = 0; StringIdx < NumStrings; StringIdx++)
{
STRG.GoTo( StringsStart + LanguageOffsets[LanguageIdx][StringIdx] );
STRG.Skip(4); // Skipping string size
Language.Strings[StringIdx].String = STRG.ReadString();
// Flag the string as localized if it has a different offset than the English string
Language.Strings[StringIdx].IsLocalized = (LanguageIdx == 0 ||
LanguageOffsets[LanguageIdx][StringIdx] != LanguageOffsets[0][StringIdx]);
}
}
}
void CStringLoader::LoadNameTable(IInputStream& rSTRG)
void CStringLoader::LoadNameTable(IInputStream& STRG)
{
// Name table header
uint32 NameCount = rSTRG.ReadLong();
uint32 NameTableSize = rSTRG.ReadLong();
uint32 NameTableStart = rSTRG.Tell();
uint32 NameTableEnd = NameTableStart + NameTableSize;
uint NameCount = STRG.ReadLong();
uint NameTableSize = STRG.ReadLong();
uint NameTableStart = STRG.Tell();
uint NameTableEnd = NameTableStart + NameTableSize;
// Name definitions
struct SNameDef {
uint32 NameOffset, StringIndex;
uint NameOffset, StringIndex;
};
std::vector<SNameDef> NameDefs(NameCount);
for (uint32 iName = 0; iName < NameCount; iName++)
// Keep track of max string index so we can size the names array appropriately.
// Note that it is possible that not every string in the table has a name.
int MaxIndex = -1;
for (uint NameIdx = 0; NameIdx < NameCount; NameIdx++)
{
NameDefs[iName].NameOffset = rSTRG.ReadLong() + NameTableStart;
NameDefs[iName].StringIndex = rSTRG.ReadLong();
NameDefs[NameIdx].NameOffset = STRG.ReadLong() + NameTableStart;
NameDefs[NameIdx].StringIndex = STRG.ReadLong();
MaxIndex = Math::Max(MaxIndex, (int) NameDefs[NameIdx].StringIndex);
}
// Name strings
mpStringTable->mStringNames.resize(mpStringTable->mNumStrings);
for (uint32 iName = 0; iName < NameCount; iName++)
mpStringTable->mStringNames.resize(MaxIndex + 1);
for (uint NameIdx = 0; NameIdx < NameCount; NameIdx++)
{
SNameDef *pDef = &NameDefs[iName];
rSTRG.Seek(pDef->NameOffset, SEEK_SET);
mpStringTable->mStringNames[pDef->StringIndex] = rSTRG.ReadString();
SNameDef& NameDef = NameDefs[NameIdx];
STRG.GoTo(NameDef.NameOffset);
mpStringTable->mStringNames[NameDef.StringIndex] = STRG.ReadString();
}
rSTRG.Seek(NameTableEnd, SEEK_SET);
STRG.GoTo(NameTableEnd);
}
// ************ STATIC ************
CStringTable* CStringLoader::LoadSTRG(IInputStream& rSTRG, CResourceEntry *pEntry)
CStringTable* CStringLoader::LoadSTRG(IInputStream& STRG, CResourceEntry* pEntry)
{
// Verify that this is a valid STRG
if (!rSTRG.IsValid()) return nullptr;
if (!STRG.IsValid()) return nullptr;
uint32 Magic = rSTRG.ReadLong();
// Verify that this is a valid STRG
uint Magic = STRG.ReadLong();
EGame Version = EGame::Invalid;
if (Magic != 0x87654321)
{
// Check for MP1 Demo STRG format - no magic/version; the first value is actually the filesize
// so the best I can do is verify the first value actually points to the end of the file
if (Magic <= (uint32) rSTRG.Size())
// so the best I can do is verify the first value actually points to the end of the file.
// The file can have up to 31 padding bytes at the end so we account for that
if (Magic <= (uint) STRG.Size() && Magic > STRG.Size() - 32)
{
rSTRG.Seek(Magic, SEEK_SET);
if ((rSTRG.EoF()) || (rSTRG.ReadShort() == 0xFFFF))
Version = EGame::PrimeDemo;
Version = EGame::PrimeDemo;
}
// If not, then we seem to have an invalid file...
if (Version != EGame::PrimeDemo)
{
errorf("%s: Invalid STRG magic: 0x%08X", *rSTRG.GetSourceString(), Magic);
errorf("%s: Invalid STRG magic: 0x%08X", *STRG.GetSourceString(), Magic);
return nullptr;
}
}
else
{
uint32 FileVersion = rSTRG.ReadLong();
uint FileVersion = STRG.ReadLong();
Version = GetFormatVersion(FileVersion);
if (Version == EGame::Invalid)
{
errorf("%s: Unsupported STRG version: 0x%X", *rSTRG.GetSourceString(), FileVersion);
errorf("%s: Unrecognized STRG version: 0x%X", *STRG.GetSourceString(), FileVersion);
return nullptr;
}
}
@ -197,9 +238,9 @@ CStringTable* CStringLoader::LoadSTRG(IInputStream& rSTRG, CResourceEntry *pEntr
Loader.mpStringTable = new CStringTable(pEntry);
Loader.mVersion = Version;
if (Version == EGame::PrimeDemo) Loader.LoadPrimeDemoSTRG(rSTRG);
else if (Version < EGame::Corruption) Loader.LoadPrimeSTRG(rSTRG);
else Loader.LoadCorruptionSTRG(rSTRG);
if (Version == EGame::PrimeDemo) Loader.LoadPrimeDemoSTRG(STRG);
else if (Version < EGame::Corruption) Loader.LoadPrimeSTRG(STRG);
else Loader.LoadCorruptionSTRG(STRG);
return Loader.mpStringTable;
}

View File

@ -2,8 +2,8 @@
#define CSTRINGLOADER_H
#include "Core/GameProject/CResourceStore.h"
#include "Core/Resource/CStringTable.h"
#include "Core/Resource/TResPtr.h"
#include "Core/Resource/StringTable/CStringTable.h"
#include <Common/EGame.h>
class CStringLoader
@ -12,14 +12,14 @@ class CStringLoader
EGame mVersion;
CStringLoader() {}
void LoadPrimeDemoSTRG(IInputStream& rSTRG);
void LoadPrimeSTRG(IInputStream& rSTRG);
void LoadCorruptionSTRG(IInputStream& rSTRG);
void LoadNameTable(IInputStream& rSTRG);
void LoadPrimeDemoSTRG(IInputStream& STRG);
void LoadPrimeSTRG(IInputStream& STRG);
void LoadCorruptionSTRG(IInputStream& STRG);
void LoadNameTable(IInputStream& STRG);
public:
static CStringTable* LoadSTRG(IInputStream &rSTRG, CResourceEntry *pEntry);
static EGame GetFormatVersion(uint32 Version);
static CStringTable* LoadSTRG(IInputStream& STRG, CResourceEntry* pEntry);
static EGame GetFormatVersion(uint Version);
};
#endif // CSTRINGLOADER_H

View File

@ -562,7 +562,7 @@ void CTextureDecoder::ReadPixelC8(IInputStream& rSrc, IOutputStream& rDst)
((Index >> 2) & 0x1) ? G = 0xFF : G = 0x0;
((Index >> 1) & 0x1) ? B = 0xFF : B = 0x0;
((Index >> 0) & 0x1) ? A = 0xFF : A = 0x0;
u32 RGBA = (R << 24) | (G << 16) | (B << 8) | (A);
uint32 RGBA = (R << 24) | (G << 16) | (B << 8) | (A);
dst.WriteLong(RGBA);*/
mPaletteInput.Seek(Index * 2, SEEK_SET);

View File

@ -6,8 +6,6 @@
#include "CFont.h"
#include "CPoiToWorld.h"
#include "CResource.h"
#include "CScan.h"
#include "CStringTable.h"
#include "CTexture.h"
#include "CWorld.h"
#include "Core/Resource/Animation/CAnimation.h"
@ -16,6 +14,8 @@
#include "Core/Resource/Animation/CSkin.h"
#include "Core/Resource/Area/CGameArea.h"
#include "Core/Resource/Model/CModel.h"
#include "Core/Resource/Scan/CScan.h"
#include "Core/Resource/StringTable/CStringTable.h"
#endif // RESOURCES_H

View File

@ -0,0 +1,51 @@
#include "CScan.h"
CScan::CScan(CResourceEntry* pEntry /*= 0*/)
: CResource(pEntry)
{
CGameTemplate* pGameTemplate = NGameList::GetGameTemplate( Game() );
mpTemplate = pGameTemplate->FindMiscTemplate("ScannableObjectInfo");
ASSERT( mpTemplate != nullptr );
CStructProperty* pProperties = mpTemplate->Properties();
mPropertyData.resize( pProperties->DataSize() );
pProperties->Construct( mPropertyData.data() );
}
CStructRef CScan::ScanData() const
{
return CStructRef((void*) mPropertyData.data(), mpTemplate->Properties());
}
/** Convenience property accessors */
CAssetRef CScan::ScanStringPropertyRef() const
{
const uint kStringIdMP1 = 0x1;
const uint kStringIdMP2 = 0x2F5B6423;
IProperty* pProperty = mpTemplate->Properties()->ChildByID(
Game() <= EGame::Prime ? kStringIdMP1 : kStringIdMP2
);
return CAssetRef( (void*) mPropertyData.data(), pProperty );
}
CBoolRef CScan::IsCriticalPropertyRef() const
{
const uint kIsCriticalIdMP1 = 0x4;
const uint kIsCriticalIdMP2 = 0x7B714814;
IProperty* pProperty = mpTemplate->Properties()->ChildByID(
Game() <= EGame::Prime ? kIsCriticalIdMP1 : kIsCriticalIdMP2
);
return CBoolRef( (void*) mPropertyData.data(), pProperty );
}
/** CResource interface */
CDependencyTree* CScan::BuildDependencyTree() const
{
CDependencyTree* pTree = new CDependencyTree();
pTree->ParseProperties(Entry(), ScanData().Property(), ScanData().DataPointer());
return pTree;
}

View File

@ -0,0 +1,34 @@
#ifndef CSCAN_H
#define CSCAN_H
#include <Common/Common.h>
#include "Core/Resource/Animation/CAnimationParameters.h"
#include "Core/Resource/Script/CGameTemplate.h"
#include "Core/Resource/Script/NGameList.h"
/** Scannable object parameters from SCAN assets */
class CScan : public CResource
{
DECLARE_RESOURCE_TYPE(Scan)
friend class CScanLoader;
friend class CScanCooker;
/** Script template specifying scan data layout */
CScriptTemplate* mpTemplate;
/** Scan property data */
std::vector<uint8> mPropertyData;
public:
CScan(CResourceEntry* pEntry = 0);
CStructRef ScanData() const;
/** Convenience property accessors */
CAssetRef ScanStringPropertyRef() const;
CBoolRef IsCriticalPropertyRef() const;
/** CResource interface */
virtual CDependencyTree* BuildDependencyTree() const override;
};
#endif // CSCAN_H

View File

@ -0,0 +1,15 @@
#ifndef ELOGBOOKCATEGORY_H
#define ELOGBOOKCATEGORY_H
/** Logbook category for scannable objects in MP1, used in SCAN and SAVW */
enum class ELogbookCategory
{
None = 0,
SpacePirateData = 1,
ChozoLore = 2,
Creatures = 3,
Research = 4,
Artifacts = 5
};
#endif // ELOGBOOKCATEGORY_H

View File

@ -0,0 +1,60 @@
#ifndef SSCANPARAMETERSMP1_H
#define SSCANPARAMETERSMP1_H
#include "ELogbookCategory.h"
#include <Common/Common.h>
/** Struct mapping to SCAN property layout in MP1 */
enum class EScanSpeed
{
Normal = 0,
Slow = 1,
};
enum class EScanImagePane
{
Pane0 = 0,
Pane1 = 1,
Pane2 = 2,
Pane3 = 3,
Pane01 = 4,
Pane12 = 5,
Pane23 = 6,
Pane012 = 7,
Pane123 = 8,
Pane0123 = 9,
Pane4 = 10,
Pane5 = 11,
Pane6 = 12,
Pane7 = 13,
Pane45 = 14,
Pane56 = 15,
Pane67 = 16,
Pane456 = 17,
Pane567 = 18,
Pane4567 = 19,
None = -1
};
struct SScanImage
{
CAssetID Texture;
float AppearPercentage;
EScanImagePane Pane;
int32 AnimationCellWidth;
int32 AnimationCellHeight;
float AnimationSwapInterval;
float FadeDuration;
};
struct SScanParametersMP1
{
CAssetID GuiFrame;
CAssetID String;
EScanSpeed Speed;
ELogbookCategory LogbookCategory;
bool IsCritical;
SScanImage ScanImages[4];
};
#endif // SSCANPARAMETERSMP1_H

View File

@ -13,6 +13,7 @@ void CGameTemplate::Serialize(IArchive& Arc)
{
Arc << SerialParameter("ScriptObjects", mScriptTemplates)
<< SerialParameter("PropertyArchetypes", mPropertyTemplates)
<< SerialParameter("MiscTemplates", mMiscTemplates)
<< SerialParameter("States", mStates)
<< SerialParameter("Messages", mMessages);
}
@ -51,6 +52,13 @@ void CGameTemplate::Load(const TString& kFilePath)
Internal_LoadPropertyTemplate(Iter->second);
}
}
for (auto Iter = mMiscTemplates.begin(); Iter != mMiscTemplates.end(); Iter++)
{
SScriptTemplatePath& MiscPath = Iter->second;
TString AbsPath = gkGameRoot + MiscPath.Path;
MiscPath.pTemplate = std::make_shared<CScriptTemplate>(this, -1, AbsPath);
}
}
void CGameTemplate::Save()
@ -118,6 +126,16 @@ void CGameTemplate::SaveGameTemplates(bool ForceAll /*= false*/)
}
}
}
for (auto Iter = mMiscTemplates.begin(); Iter != mMiscTemplates.end(); Iter++)
{
SScriptTemplatePath& Path = Iter->second;
if( Path.pTemplate )
{
Path.pTemplate->Save(ForceAll);
}
}
}
uint32 CGameTemplate::GameVersion(TString VersionName)
@ -286,6 +304,21 @@ bool CGameTemplate::RenamePropertyArchetype(const TString& kTypeName, const TStr
return false;
}
CScriptTemplate* CGameTemplate::FindMiscTemplate(const TString& kTemplateName)
{
auto Iter = mMiscTemplates.find(kTemplateName);
if (Iter == mMiscTemplates.end())
{
return nullptr;
}
else
{
SScriptTemplatePath& Path = Iter->second;
return Path.pTemplate.get();
}
}
TString CGameTemplate::GetGameDirectory() const
{
return mSourceFile.GetFileDirectory();

View File

@ -35,54 +35,23 @@ struct SObjId
}
};
/** Struct holding a reference to a script object template */
struct SScriptTemplatePath
/** Struct holding a reference to a template */
template<typename TemplateT>
struct TTemplatePath
{
/** File path to the template file, relative to the game directory */
TString Path;
/** Template in memory */
std::shared_ptr<CScriptTemplate> pTemplate;
std::shared_ptr<TemplateT> pTemplate;
/** Constructor */
SScriptTemplatePath()
TTemplatePath()
{}
SScriptTemplatePath(const TString& kInPath, CScriptTemplate* pInTemplate)
TTemplatePath(const TString& kInPath, TemplateT* pInTemplate)
: Path(kInPath)
, pTemplate( std::shared_ptr<CScriptTemplate>(pInTemplate) )
{}
/** Serializer */
void Serialize(IArchive& Arc)
{
if (Arc.FileVersion() == 0)
{
Arc << SerialParameter("Path", Path, SH_Attribute);
}
else
{
Arc.SerializePrimitive(Path, 0);
}
}
};
/** Struct holding a reference to a property template */
struct SPropertyTemplatePath
{
/** File path to the template file, relative to the game directory */
TString Path;
/** Template in memory */
std::shared_ptr<IProperty> pTemplate;
/** Constructor */
SPropertyTemplatePath()
{}
SPropertyTemplatePath(const TString& kInPath, IProperty* pInTemplate)
: Path(kInPath)
, pTemplate( std::shared_ptr<IProperty>(pInTemplate) )
, pTemplate( std::shared_ptr<TemplateT>(pInTemplate) )
{}
/** Serializer */
@ -92,6 +61,9 @@ struct SPropertyTemplatePath
}
};
typedef TTemplatePath<CScriptTemplate> SScriptTemplatePath;
typedef TTemplatePath<IProperty> SPropertyTemplatePath;
/** CGameTemplate - Per-game template data */
class CGameTemplate
{
@ -103,6 +75,7 @@ class CGameTemplate
/** Template arrays */
std::map<SObjId, SScriptTemplatePath> mScriptTemplates;
std::map<TString, SPropertyTemplatePath> mPropertyTemplates;
std::map<TString, SScriptTemplatePath> mMiscTemplates;
std::map<SObjId, TString> mStates;
std::map<SObjId, TString> mMessages;
@ -130,6 +103,7 @@ public:
IProperty* FindPropertyArchetype(const TString& kTypeName);
TString GetPropertyArchetypeFilePath(const TString& kTypeName);
bool RenamePropertyArchetype(const TString& kTypeName, const TString& kNewTypeName);
CScriptTemplate* FindMiscTemplate(const TString& kTemplateName);
TString GetGameDirectory() const;
// Inline Accessors

View File

@ -106,7 +106,8 @@ struct SNameValue
bool IsValid;
/** List of all properties using this ID */
std::list<IProperty*> PropertyList;
/** @todo - make this an intrusively linked list */
std::set<IProperty*> PropertyList;
void Serialize(IArchive& Arc)
{
@ -285,7 +286,7 @@ bool IsValidPropertyID(uint32 ID, const char* pkTypeName, bool* pOutIsValid /*=
}
/** Retrieves a list of all properties that match the requested property ID. */
void RetrievePropertiesWithID(uint32 ID, const char* pkTypeName, std::list<IProperty*>& OutList)
void RetrievePropertiesWithID(uint32 ID, const char* pkTypeName, std::vector<IProperty*>& OutList)
{
SNameKey Key = CreateKey(ID, pkTypeName);
auto MapFind = gNameMap.find(Key);
@ -293,7 +294,12 @@ void RetrievePropertiesWithID(uint32 ID, const char* pkTypeName, std::list<IProp
if (MapFind != gNameMap.end())
{
SNameValue& Value = MapFind->second;
OutList = Value.PropertyList;
OutList.reserve(Value.PropertyList.size());
for (auto Iter = Value.PropertyList.begin(); Iter != Value.PropertyList.end(); Iter++)
{
OutList.push_back(*Iter);
}
}
}
@ -391,7 +397,7 @@ void ChangeTypeName(IProperty* pProperty, const char* pkOldTypeName, const char*
if (Find != gNameMap.end())
{
SNameValue& Value = Find->second;
WasRegistered = NBasics::ListRemoveOne(Value.PropertyList, pProperty);
WasRegistered = (Value.PropertyList.find(pProperty) != Value.PropertyList.end());
}
// Create a key for the new property and add it to the list.
@ -409,7 +415,7 @@ void ChangeTypeName(IProperty* pProperty, const char* pkOldTypeName, const char*
if (WasRegistered)
{
Find->second.PropertyList.push_back(pProperty);
Find->second.PropertyList.insert(pProperty);
}
gMapIsDirty = true;
@ -527,7 +533,7 @@ void RegisterProperty(IProperty* pProperty)
pProperty->SetName( MapFind->second.Name );
}
MapFind->second.PropertyList.push_back(pProperty);
MapFind->second.PropertyList.insert(pProperty);
// Update the property's Name field to match the mapped name.
pProperty->SetName( MapFind->second.Name );
@ -543,7 +549,7 @@ void UnregisterProperty(IProperty* pProperty)
{
// Found the value, now remove the element from the list.
SNameValue& Value = Iter->second;
NBasics::ListRemoveOne(Value.PropertyList, pProperty);
Value.PropertyList.erase(pProperty);
}
}

View File

@ -32,7 +32,7 @@ uint32 CalculatePropertyID(const char* pkName, const char* pkTypeName);
bool IsValidPropertyID(uint32 ID, const char* pkTypeName, bool* pOutIsValid = nullptr);
/** Retrieves a list of all properties that match the requested property ID. */
void RetrievePropertiesWithID(uint32 ID, const char* pkTypeName, std::list<IProperty*>& OutList);
void RetrievePropertiesWithID(uint32 ID, const char* pkTypeName, std::vector<IProperty*>& OutList);
/** Retrieves a list of all XML templates that contain a given property ID. */
void RetrieveXMLsWithProperty(uint32 ID, const char* pkTypeName, std::set<TString>& OutSet);

View File

@ -2,6 +2,7 @@
#define CANIMATIONSETPROPERTY_H
#include "IProperty.h"
#include "Core/Resource/Animation/CAnimationParameters.h"
class CAnimationSetProperty : public TSerializeableTypedProperty< CAnimationParameters, EPropertyType::AnimationSet >
{
@ -17,7 +18,7 @@ protected:
public:
virtual void SerializeValue(void* pData, IArchive& Arc) const
{
Value(pData).Serialize(Arc);
ValueRef(pData).Serialize(Arc);
}
virtual const char* HashableTypeName() const

View File

@ -25,7 +25,7 @@ public:
virtual void SerializeValue(void* pData, IArchive& Arc) const
{
Value(pData).Serialize(Arc);
ValueRef(pData).Serialize(Arc);
}
};

View File

@ -69,9 +69,9 @@ public:
// Skip TSerializeableTypedProperty, serialize default value ourselves so we can set SH_HexDisplay
TTypedProperty::Serialize(rArc);
// Serialize default value
TEnumPropertyBase* pArchetype = static_cast<TEnumPropertyBase*>(mpArchetype);
uint32 DefaultValueFlags = SH_HexDisplay | (pArchetype || Game() <= EGame::Prime ? SH_Optional : 0);
uint32 DefaultValueFlags = SH_Optional | (TypeEnum == EPropertyType::Enum ? SH_HexDisplay : 0);
rArc << SerialParameter("DefaultValue", mDefaultValue, DefaultValueFlags, pArchetype ? pArchetype->mDefaultValue : 0);
// Only serialize type name override for root archetypes.

View File

@ -69,7 +69,7 @@ void CPropertyNameGenerator::Generate(const SPropertyNameGenerationParameters& r
}
// If TestIntsAsChoices is enabled, and int is in the type list, then choice must be in the type list too.
if (rkParams.TestIntsAsChoices && NBasics::VectorContains(mTypeNames, TString("int")))
if (rkParams.TestIntsAsChoices && NBasics::VectorFind(mTypeNames, TString("int")) >= 0)
{
NBasics::VectorAddUnique(mTypeNames, TString("choice"));
}
@ -218,7 +218,7 @@ void CPropertyNameGenerator::Generate(const SPropertyNameGenerationParameters& r
// If we have too many saved results, then to avoid crashing we will force enable log output.
if (mGeneratedNames.size() > 9999)
{
gpUIRelay->AsyncMessageBox("Warning", "There are over 10,000 results. Results will no longer print to the screen. Check the log for the remaining output.");
gpUIRelay->ShowMessageBoxAsync("Warning", "There are over 10,000 results. Results will no longer print to the screen. Check the log for the remaining output.");
WriteToLog = true;
SaveResults = false;
}

View File

@ -69,6 +69,14 @@ void CStructProperty::RevertToDefault(void* pData) const
}
}
void CStructProperty::SetDefaultFromData(void* pData)
{
for (int ChildIdx = 0; ChildIdx < mChildren.size(); ChildIdx++)
{
mChildren[ChildIdx]->SetDefaultFromData(pData);
}
}
const char* CStructProperty::HashableTypeName() const
{
return mpArchetype ? mpArchetype->HashableTypeName() : *mName;

View File

@ -25,6 +25,7 @@ public:
virtual void Destruct(void* pData) const;
virtual bool MatchesDefault(void* pData) const;
virtual void RevertToDefault(void* pData) const;
virtual void SetDefaultFromData(void* pData);
virtual const char* HashableTypeName() const;
virtual void Serialize(IArchive& rArc);
virtual void SerializeValue(void* pData, IArchive& Arc) const;

View File

@ -91,6 +91,7 @@ void IProperty::Serialize(IArchive& rArc)
// The archetype must exist, or else the template file is malformed.
ASSERT(pArchetype != nullptr);
ASSERT(pArchetype->Type() == Type());
InitFromArchetype(pArchetype);
}
@ -325,7 +326,29 @@ TString IProperty::GetTemplateFileName()
bool IProperty::ShouldCook(void* pPropertyData) const
{
switch (mCookPreference)
ECookPreference Preference = mCookPreference;
// Determine the real cook preference to use.
if (Preference == ECookPreference::Default)
{
if (Game() == EGame::DKCReturns)
{
// DKCR properties usually don't write unless they have been modified.
Preference = ECookPreference::OnlyIfModified;
}
else
{
// MP2 and MP3 properties usually always write no matter what.
Preference = ECookPreference::Always;
}
}
else if (Preference == ECookPreference::OnlyIfModified && Game() <= EGame::Prime)
{
// OnlyIfModified not supported for MP1.
Preference = ECookPreference::Always;
}
switch (Preference)
{
case ECookPreference::Always:
return true;
@ -333,8 +356,13 @@ bool IProperty::ShouldCook(void* pPropertyData) const
case ECookPreference::Never:
return false;
case ECookPreference::OnlyIfModified:
return !MatchesDefault(pPropertyData);
default:
return (Game() < EGame::DKCReturns ? true : !MatchesDefault(pPropertyData));
// Unhandled case
ASSERT(false);
return true;
}
}

View File

@ -1,7 +1,6 @@
#ifndef IPROPERTY_H
#define IPROPERTY_H
#include "Core/Resource/Animation/CAnimationParameters.h"
#include <Common/Common.h>
#include <Common/CFourCC.h>
#include <Common/Math/CVector3f.h>
@ -106,7 +105,8 @@ enum class ECookPreference
{
Default,
Always,
Never
Never,
OnlyIfModified
};
/** New property class */
@ -178,6 +178,7 @@ public:
virtual void PostInitialize() {}
virtual void PropertyValueChanged(void* pPropertyData) {}
virtual void CopyDefaultValueTo(IProperty* pOtherProperty) {}
virtual void SetDefaultFromData(void* pData) {}
virtual bool IsNumericalType() const { return false; }
virtual bool IsPointerType() const { return false; }
virtual TString ValueAsString(void* pData) const { return ""; }
@ -367,6 +368,7 @@ public:
virtual void Destruct(void* pData) const { ValueRef(pData).~PropType(); }
virtual bool MatchesDefault(void* pData) const { return ValueRef(pData) == mDefaultValue; }
virtual void RevertToDefault(void* pData) const { ValueRef(pData) = mDefaultValue; }
virtual void SetDefaultFromData(void* pData) { mDefaultValue = ValueRef(pData); MarkDirty(); }
virtual bool CanHaveDefault() const { return true; }

View File

@ -0,0 +1,396 @@
#include "CStringTable.h"
#include "Core/GameProject/CGameProject.h"
#include <Common/Math/MathUtil.h>
#include <algorithm>
#include <iterator>
/**
* Listing of supported languages for different engine versions. Note we ignore the "unused" languages.
* This is also the order that languages appear in game STRG assets.
*/
// Supported languages in the original NTSC release of Metroid Prime
const std::vector<ELanguage> gkSupportedLanguagesMP1 =
{
ELanguage::English
};
// Supported languages in the PAL version of Metroid Prime, and also Metroid Prime 2
const std::vector<ELanguage> gkSupportedLanguagesMP1PAL =
{
ELanguage::English, ELanguage::French, ELanguage::German,
ELanguage::Spanish, ELanguage::Italian, ELanguage::Japanese
};
// Supported languages in Metroid Prime 3
const std::vector<ELanguage> gkSupportedLanguagesMP3 =
{
ELanguage::English, ELanguage::Japanese, ELanguage::German,
ELanguage::French, ELanguage::Spanish, ELanguage::Italian
};
// Supported languages in DKCR
const std::vector<ELanguage> gkSupportedLanguagesDKCR =
{
ELanguage::English, ELanguage::Japanese, ELanguage::German,
ELanguage::French, ELanguage::Spanish, ELanguage::Italian,
ELanguage::UKEnglish, ELanguage::Korean,
ELanguage::NAFrench, ELanguage::NASpanish
};
// Utility function - retrieve the language array for a given game/region
static const std::vector<ELanguage>& GetSupportedLanguages(EGame Game, ERegion Region)
{
switch (Game)
{
default:
case EGame::PrimeDemo:
case EGame::Prime:
if (Region == ERegion::NTSC)
return gkSupportedLanguagesMP1;
else
return gkSupportedLanguagesMP1PAL;
case EGame::EchoesDemo:
case EGame::Echoes:
case EGame::CorruptionProto:
return gkSupportedLanguagesMP1PAL;
case EGame::Corruption:
return gkSupportedLanguagesMP3;
case EGame::DKCReturns:
return gkSupportedLanguagesDKCR;
}
}
// Utility function - retrieve the index of a given language
static int FindLanguageIndex(const CStringTable* pkInTable, ELanguage InLanguage)
{
for (uint LanguageIdx = 0; LanguageIdx < pkInTable->NumLanguages(); LanguageIdx++)
{
if (pkInTable->LanguageByIndex(LanguageIdx) == InLanguage)
{
return LanguageIdx;
}
}
return -1;
}
/** Returns a string given a language/index pair */
TString CStringTable::GetString(ELanguage Language, uint StringIndex) const
{
int LanguageIdx = FindLanguageIndex(this, Language);
if (LanguageIdx >= 0 && mLanguages[LanguageIdx].Strings.size() > StringIndex)
{
return mLanguages[LanguageIdx].Strings[StringIndex].String;
}
else
{
return "";
}
}
/** Updates a string for a given language */
void CStringTable::SetString(ELanguage Language, uint StringIndex, const TString& kNewString)
{
int LanguageIdx = FindLanguageIndex(this, Language);
if (LanguageIdx >= 0 && mLanguages[LanguageIdx].Strings.size() > StringIndex)
{
mLanguages[LanguageIdx].Strings[StringIndex].String = kNewString;
mLanguages[LanguageIdx].Strings[StringIndex].IsLocalized =
(LanguageIdx == 0 || kNewString != mLanguages[0].Strings[StringIndex].String);
}
}
/** Updates a string name */
void CStringTable::SetStringName(uint StringIndex, const TString& kNewName)
{
// Sanity check - make sure the string index is valid
ASSERT( NumStrings() > StringIndex );
// Expand the name listing if needed and assign the name
if (mStringNames.size() <= StringIndex)
{
mStringNames.resize( StringIndex + 1 );
}
mStringNames[StringIndex] = kNewName;
// Strip empty string names
while (mStringNames.back().IsEmpty())
mStringNames.pop_back();
}
/** Move string to another position in the table */
void CStringTable::MoveString(uint StringIndex, uint NewIndex)
{
ASSERT( NumStrings() > StringIndex );
ASSERT( NumStrings() > NewIndex );
if (NewIndex == StringIndex)
return;
// Update string data
for (uint LanguageIdx = 0; LanguageIdx < mLanguages.size(); LanguageIdx++)
{
SLanguageData& Language = mLanguages[LanguageIdx];
SStringData String = Language.Strings[StringIndex];
if (NewIndex > StringIndex)
{
for (uint i=StringIndex; i<NewIndex; i++)
Language.Strings[i] = Language.Strings[i+1];
}
else
{
for (uint i=StringIndex; i>NewIndex; i--)
Language.Strings[i] = Language.Strings[i-1];
}
Language.Strings[NewIndex] = String;
}
// Update string name
uint MinIndex = Math::Min(StringIndex, NewIndex);
uint MaxIndex = Math::Max(StringIndex, NewIndex);
if (MinIndex < mStringNames.size())
{
if (MaxIndex >= mStringNames.size())
{
mStringNames.resize(MaxIndex + 1);
}
TString Name = mStringNames[StringIndex];
if (NewIndex > StringIndex)
{
for (uint i=StringIndex; i<NewIndex; i++)
mStringNames[i] = mStringNames[i+1];
}
else
{
for (uint i=StringIndex; i>NewIndex; i--)
mStringNames[i] = mStringNames[i-1];
}
mStringNames[NewIndex] = Name;
// Strip empty string names
while (mStringNames.back().IsEmpty())
mStringNames.pop_back();
}
}
/** Add a new string to the table */
void CStringTable::AddString(uint AtIndex)
{
if (AtIndex < NumStrings())
{
if (mStringNames.size() > AtIndex)
{
mStringNames.insert( mStringNames.begin() + AtIndex, 1, "" );
}
}
else
AtIndex = NumStrings();
for (uint LanguageIdx = 0; LanguageIdx < mLanguages.size(); LanguageIdx++)
{
SLanguageData& Language = mLanguages[LanguageIdx];
Language.Strings.insert( Language.Strings.begin() + AtIndex, 1, SStringData() );
}
}
/** Remove a string from the table */
void CStringTable::RemoveString(uint StringIndex)
{
ASSERT( StringIndex < NumStrings() );
if (mStringNames.size() > StringIndex)
mStringNames.erase( mStringNames.begin() + StringIndex );
for (uint LanguageIdx = 0; LanguageIdx < mLanguages.size(); LanguageIdx++)
{
SLanguageData& Language = mLanguages[LanguageIdx];
Language.Strings.erase( Language.Strings.begin() + StringIndex );
}
}
/** Initialize new resource data */
void CStringTable::InitializeNewResource()
{
// Initialize data for whatever languages are supported by our game/region
ERegion Region = ( Entry() && Entry()->Project() ? Entry()->Project()->Region() : ERegion::NTSC );
const std::vector<ELanguage>& kLanguageArray = GetSupportedLanguages(Game(), Region);
mLanguages.resize( kLanguageArray.size() );
for (uint i=0; i < kLanguageArray.size(); i++)
{
mLanguages[i].Language = kLanguageArray[i];
mLanguages[i].Strings.resize(1);
}
}
/** Serialize resource data */
void CStringTable::Serialize(IArchive& Arc)
{
Arc << SerialParameter("StringNames", mStringNames, SH_Optional)
<< SerialParameter("Languages", mLanguages);
}
/** Build the dependency tree for this resource */
CDependencyTree* CStringTable::BuildDependencyTree() const
{
// STRGs can reference FONTs with the &font=; formatting tag and TXTRs with the &image=; tag
CDependencyTree* pTree = new CDependencyTree();
EIDLength IDLength = CAssetID::GameIDLength( Game() );
for (uint LanguageIdx = 0; LanguageIdx < mLanguages.size(); LanguageIdx++)
{
const SLanguageData& kLanguage = mLanguages[LanguageIdx];
for (uint StringIdx = 0; StringIdx < kLanguage.Strings.size(); StringIdx++)
{
const TString& kString = kLanguage.Strings[StringIdx].String;
for (int TagIdx = kString.IndexOf('&'); TagIdx != -1; TagIdx = kString.IndexOf('&', TagIdx + 1))
{
// Check for double ampersand (escape character in DKCR, not sure about other games)
if (kString.At(TagIdx + 1) == '&')
{
TagIdx++;
continue;
}
// Get tag name and parameters
int NameEnd = kString.IndexOf('=', TagIdx);
int TagEnd = kString.IndexOf(';', TagIdx);
if (NameEnd == -1 || TagEnd == -1) continue;
TString TagName = kString.SubString(TagIdx + 1, NameEnd - TagIdx - 1);
TString ParamString = kString.SubString(NameEnd + 1, TagEnd - NameEnd - 1);
if (ParamString.IsEmpty()) continue;
// Font
if (TagName == "font")
{
if (Game() >= EGame::CorruptionProto)
{
ASSERT(ParamString.StartsWith("0x"));
ParamString = ParamString.ChopFront(2);
}
ASSERT(ParamString.Size() == IDLength * 2);
pTree->AddDependency( CAssetID::FromString(ParamString) );
}
// Image
else if (TagName == "image")
{
// Determine which params are textures based on image type
TStringList Params = ParamString.Split(",");
TString ImageType = Params.front();
uint TexturesStart = 0;
if (ImageType == "A")
TexturesStart = 2;
else if (ImageType == "SI")
TexturesStart = 3;
else if (ImageType == "SA")
TexturesStart = 4;
else if (ImageType == "B")
TexturesStart = 2;
else if (ImageType.IsHexString(false, IDLength * 2))
TexturesStart = 0;
else
{
errorf("Unrecognized image type: %s", *ImageType);
continue;
}
// Load texture IDs
TStringList::iterator Iter = Params.begin();
for (uint ParamIdx = 0; ParamIdx < Params.size(); ParamIdx++, Iter++)
{
if (ParamIdx >= TexturesStart)
{
TString Param = *Iter;
if (Game() >= EGame::CorruptionProto)
{
ASSERT(Param.StartsWith("0x"));
Param = Param.ChopFront(2);
}
ASSERT(Param.Size() == IDLength * 2);
pTree->AddDependency( CAssetID::FromString(Param) );
}
}
}
}
}
}
return pTree;
}
/** Static - Strip all formatting tags for a given string */
TString CStringTable::StripFormatting(const TString& kInString)
{
TString Out = kInString;
int TagStart = -1;
for (uint CharIdx = 0; CharIdx < Out.Size(); CharIdx++)
{
if (Out[CharIdx] == '&')
{
if (TagStart == -1)
TagStart = CharIdx;
else
{
Out.Remove(TagStart, 1);
TagStart = -1;
CharIdx--;
}
}
else if (TagStart != -1 && Out[CharIdx] == ';')
{
int TagEnd = CharIdx + 1;
int TagLen = TagEnd - TagStart;
Out.Remove(TagStart, TagLen);
CharIdx = TagStart - 1;
TagStart = -1;
}
}
return Out;
}
/** Static - Returns whether a given language is supported by the given game/region combination */
bool CStringTable::IsLanguageSupported(ELanguage Language, EGame Game, ERegion Region)
{
const std::vector<ELanguage>& kLanguageArray = GetSupportedLanguages(Game, Region);
// Check if the requested language is in the array.
for (uint LanguageIdx = 0; LanguageIdx < kLanguageArray.size(); LanguageIdx++)
{
if (kLanguageArray[LanguageIdx] == Language)
{
return true;
}
}
// Unsupported
return false;
}

View File

@ -0,0 +1,103 @@
#ifndef CSTRINGTABLE_H
#define CSTRINGTABLE_H
#include "ELanguage.h"
#include "Core/Resource/CResource.h"
#include <Common/BasicTypes.h>
#include <Common/CFourCC.h>
#include <Common/TString.h>
#include <vector>
/** A table of localized strings from STRG assets.
* Strings are always internally stored as UTF-8.
*/
class CStringTable : public CResource
{
DECLARE_RESOURCE_TYPE(StringTable)
friend class CStringLoader;
friend class CStringCooker;
/** List of string names. Optional data, can be empty. */
std::vector<TString> mStringNames;
/** String data for a language */
struct SStringData
{
TString String;
bool IsLocalized;
SStringData()
: IsLocalized(false)
{}
void Serialize(IArchive& Arc)
{
Arc << SerialParameter("String", String)
<< SerialParameter("IsLocalized", IsLocalized, SH_Optional, true);
}
};
struct SLanguageData
{
ELanguage Language;
std::vector<SStringData> Strings;
void Serialize(IArchive& Arc)
{
Arc << SerialParameter("Language", Language)
<< SerialParameter("Strings", Strings);
}
};
std::vector<SLanguageData> mLanguages;
public:
/** Constructor */
CStringTable(CResourceEntry *pEntry = 0) : CResource(pEntry) {}
/** Returns the number of languages in the table */
inline uint NumLanguages() const { return mLanguages.size(); }
/** Returns the number of strings in the table */
inline uint NumStrings() const { return mLanguages.empty() ? 0 : mLanguages[0].Strings.size(); }
/** Returns languages used by index */
inline ELanguage LanguageByIndex(uint Index) const { return mLanguages.size() > Index ? mLanguages[Index].Language : ELanguage::Invalid; }
/** Returns the string name by string index. May be blank if the string at the requested index is unnamed */
inline TString StringNameByIndex(uint Index) const { return mStringNames.size() > Index ? mStringNames[Index] : ""; }
/** Returns a string given a language/index pair */
TString GetString(ELanguage Language, uint StringIndex) const;
/** Updates a string for a given language */
void SetString(ELanguage Language, uint StringIndex, const TString& kNewString);
/** Updates a string name */
void SetStringName(uint StringIndex, const TString& kNewName);
/** Move string to another position in the table */
void MoveString(uint StringIndex, uint NewIndex);
/** Add a new string to the table */
void AddString(uint AtIndex);
/** Remove a string from the table */
void RemoveString(uint StringIndex);
/** Initialize new resource data */
virtual void InitializeNewResource() override;
/** Serialize resource data */
virtual void Serialize(IArchive& Arc) override;
/** Build the dependency tree for this resource */
virtual CDependencyTree* BuildDependencyTree() const override;
/** Static - Strip all formatting tags for a given string */
static TString StripFormatting(const TString& kInString);
/** Static - Returns whether a given language is supported by the given game/region combination */
static bool IsLanguageSupported(ELanguage Language, EGame Game, ERegion Region);
};
#endif // CSTRINGTABLE_H

View File

@ -0,0 +1,29 @@
#ifndef ELANGUAGE_H
#define ELANGUAGE_H
#include <Common/CFourCC.h>
/** A language in the game's localization system */
enum class ELanguage
{
// The original release of Metroid Prime only supported English
English = FOURCC('ENGL'),
// Support for these languages was added in the PAL version of Metroid Prime
German = FOURCC('GERM'),
French = FOURCC('FREN'),
Spanish = FOURCC('SPAN'),
Italian = FOURCC('ITAL'),
Dutch = FOURCC('DUTC'), // Unused
Japanese = FOURCC('JAPN'),
// The rest of these languages were added in Donkey Kong Country Returns
SimplifiedChinese = FOURCC('SCHN'), // Unused
TraditionalChinese = FOURCC('TCHN'), // Unused
UKEnglish = FOURCC('UKEN'),
Korean = FOURCC('KORE'),
NAFrench = FOURCC('NAFR'),
NASpanish = FOURCC('NASP'),
// Invalid
Invalid = FOURCC('INVD')
};
#endif // ELANGUAGE_H

View File

@ -1,5 +1,6 @@
#include "CPointOfInterestExtra.h"
//@todo pull these values from tweaks instead of hardcoding them
const CColor CPointOfInterestExtra::skRegularColor = CColor::Integral(0xFF,0x70,0x00);
const CColor CPointOfInterestExtra::skImportantColor = CColor::Integral(0xFF,0x00,0x00);
@ -19,14 +20,17 @@ CPointOfInterestExtra::CPointOfInterestExtra(CScriptObject *pInstance, CScene *p
void CPointOfInterestExtra::PropertyModified(IProperty* pProperty)
{
if (mScanProperty.Property() == pProperty)
{
mpScanData = gpResourceStore->LoadResource<CScan>( mScanProperty.Get() );
mScanIsCritical = (mpScanData ? mpScanData->IsCriticalPropertyRef() : CBoolRef());
}
}
void CPointOfInterestExtra::ModifyTintColor(CColor& Color)
{
if (mpScanData)
{
if (mpScanData->IsImportant()) Color *= skImportantColor;
if (mScanIsCritical) Color *= skImportantColor;
else Color *= skRegularColor;
}
}

View File

@ -2,7 +2,7 @@
#define CPOINTOFINTERESTEXTRA_H
#include "CScriptExtra.h"
#include "Core/Resource/CScan.h"
#include "Core/Resource/Scan/CScan.h"
#include <Common/CColor.h>
class CPointOfInterestExtra : public CScriptExtra
@ -10,6 +10,7 @@ class CPointOfInterestExtra : public CScriptExtra
// Tint POI billboard orange/red depending on scan importance
CAssetRef mScanProperty;
TResPtr<CScan> mpScanData;
CBoolRef mScanIsCritical;
public:
explicit CPointOfInterestExtra(CScriptObject *pInstance, CScene *pScene, CScriptNode *pParent = 0);

View File

@ -0,0 +1,44 @@
#include "CTweakCooker.h"
#include "Core/Resource/Cooker/CScriptCooker.h"
/** Cooker entry point */
bool CTweakCooker::CookCTWK(CTweakData* pTweakData, IOutputStream& CTWK)
{
CStructRef TweakProperties = pTweakData->TweakData();
CScriptCooker ScriptCooker(pTweakData->Game());
ScriptCooker.WriteProperty(CTWK, TweakProperties.Property(), TweakProperties.DataPointer(), true);
return true;
}
bool CTweakCooker::CookNTWK(const std::vector<CTweakData*>& kTweaks, IOutputStream& NTWK)
{
NTWK.WriteFourCC( FOURCC('NTWK') ); // NTWK magic
NTWK.WriteByte( 1 ); // Version number; must be 1
NTWK.WriteLong( kTweaks.size() ); // Number of tweak objects
for (uint TweakIdx = 0; TweakIdx < kTweaks.size(); TweakIdx++)
{
CTweakData* pTweakData = kTweaks[TweakIdx];
// Tweaks in MP2+ are saved with the script object data format
// Write a dummy script object header here
uint TweakObjectStart = NTWK.Tell();
NTWK.WriteLong( pTweakData->TweakID() ); // Object ID
NTWK.WriteShort( 0 ); // Object size
NTWK.WriteLong( TweakIdx ); // Instance ID
NTWK.WriteShort( 0 ); // Link count
CStructRef TweakProperties = pTweakData->TweakData();
CScriptCooker ScriptCooker(TweakProperties.Property()->Game());
ScriptCooker.WriteProperty(NTWK, TweakProperties.Property(), TweakProperties.DataPointer(), false);
uint TweakObjectEnd = NTWK.Tell();
uint TweakObjectSize = (uint16) (TweakObjectEnd - TweakObjectStart - 6);
NTWK.GoTo(TweakObjectStart + 4);
NTWK.WriteShort(TweakObjectSize);
NTWK.GoTo(TweakObjectEnd);
}
NTWK.WriteToBoundary(32, 0);
return true;
}

View File

@ -0,0 +1,18 @@
#ifndef CTWEAKCOOKER_H
#define CTWEAKCOOKER_H
#include "CTweakData.h"
/** Class responsible for cooking tweak data */
class CTweakCooker
{
/** Private constructor */
CTweakCooker() {}
public:
/** Cooker entry point */
static bool CookCTWK(CTweakData* pTweakData, IOutputStream& CTWK);
static bool CookNTWK(const std::vector<CTweakData*>& kTweaks, IOutputStream& NTWK);
};
#endif // CTWEAKCOOKER_H

View File

@ -0,0 +1,62 @@
#ifndef CTWEAKDATA_H
#define CTWEAKDATA_H
#include "Core/Resource/CResource.h"
#include "Core/Resource/Script/CScriptTemplate.h"
#include "Core/Resource/Script/Property/TPropertyRef.h"
/** Tweak data assets for MP1 */
class CTweakData : public CResource
{
DECLARE_RESOURCE_TYPE(Tweaks)
/** Script template specifying tweak data layout */
CScriptTemplate* mpTemplate;
/** Tweak ID for MP2+ */
uint mTweakID;
/** Tweak data */
std::vector<uint8> mTweakData;
public:
CTweakData(CScriptTemplate* pTemplate, uint TweakID, CResourceEntry* pEntry = 0)
: mpTemplate(pTemplate)
, mTweakID(TweakID)
, CResource(pEntry)
{
CStructProperty* pProperties = pTemplate->Properties();
mTweakData.resize(pProperties->DataSize());
pProperties->Construct(mTweakData.data());
}
TString TweakName()
{
if (Entry() != nullptr)
{
return Entry()->Name();
}
else
{
IProperty* pNameProperty = mpTemplate->Properties()->ChildByID(0x7FDA1466);
return CStringRef(mTweakData.data(), pNameProperty);
}
}
inline CScriptTemplate* TweakTemplate() const
{
return mpTemplate;
}
inline uint32 TweakID() const
{
return mTweakID;
}
inline CStructRef TweakData() const
{
return CStructRef((void*) mTweakData.data(), mpTemplate->Properties());
}
};
#endif // CTWEAKDATA_H

View File

@ -0,0 +1,130 @@
#include "CTweakLoader.h"
#include "Core/Resource/Factory/CScriptLoader.h"
#include "Core/Resource/Script/NGameList.h"
CTweakData* CTweakLoader::LoadCTWK(IInputStream& CTWK, CResourceEntry* pEntry)
{
// Find the correct template based on the asset ID.
static const std::unordered_map<uint, const char*> skIdToTemplateName =
{
{ 0x1D180D7C, "TweakParticle" },
{ 0x264A4972, "TweakPlayer" },
{ 0x33B3323A, "TweakGunRes" },
{ 0x39AD28D3, "TweakCameraBob" },
{ 0x3FAEC012, "TweakPlayerControls", },
{ 0x5ED56350, "TweakBall", },
{ 0x5F24EFF8, "TweakSlideShow", },
{ 0x6907A32D, "TweakPlayerGun", },
{ 0x85CA11E9, "TweakPlayerRes", },
{ 0x94C76ECD, "TweakTargeting", },
{ 0x953A7C63, "TweakGame", },
{ 0xC9954E56, "TweakGuiColors", },
{ 0xE66A4F86, "TweakAutoMapper", },
{ 0xED2E48A9, "TweakGui", },
{ 0xF1ED8FD7, "TweakPlayerControls", }
};
auto Find = skIdToTemplateName.find( pEntry->ID().ToLong() );
ASSERT( Find != skIdToTemplateName.end() );
const char* pkTemplateName = Find->second;
// Fetch template
CGameTemplate* pGameTemplate = NGameList::GetGameTemplate( pEntry->Game() );
ASSERT( pGameTemplate != nullptr );
CScriptTemplate* pTweakTemplate = pGameTemplate->FindMiscTemplate(pkTemplateName);
ASSERT( pTweakTemplate != nullptr );
// Load tweak data
CTweakData* pTweakData = new CTweakData(pTweakTemplate, pEntry->ID().ToLong(), pEntry);
CScriptLoader::LoadStructData( CTWK, pTweakData->TweakData() );
// Verify
if (!CTWK.EoF() && CTWK.PeekShort() != -1)
{
errorf("%s: unread property data, tweak template may be malformed (%d bytes left)", *CTWK.GetSourceString(), CTWK.Size() - CTWK.Tell());
delete pTweakData;
return nullptr;
}
return pTweakData;
}
void CTweakLoader::LoadNTWK(IInputStream& NTWK, EGame Game, std::vector<CTweakData*>& OutTweaks)
{
// Validate file. NTWK basically embeds a bunch of tweak objects using the script layers
// format, so it has the same version byte that script layers have.
uint Magic = NTWK.ReadLong();
uint8 LayerVersion = NTWK.ReadByte();
if (Magic != FOURCC('NTWK'))
{
errorf("Unrecognized NTWK magic: 0x%08X", Magic);
return;
}
else if (LayerVersion != 1)
{
errorf("Unrecognized layer version in NTWK: %d", LayerVersion);
return;
}
CGameTemplate* pGameTemplate = NGameList::GetGameTemplate( Game );
ASSERT( pGameTemplate != nullptr );
// Start reading tweaks
uint NumTweaks = NTWK.ReadLong();
for (uint TweakIdx = 0; TweakIdx < NumTweaks; TweakIdx++)
{
// Find the correct template based on the tweak ID.
static const std::unordered_map<uint, const char*> skIdToTemplateName =
{
{ FOURCC('TWAC'), "TweakAdvancedControls" },
{ FOURCC('TWAM'), "TweakAutoMapper" },
{ FOURCC('TWBL'), "TweakBall" },
{ FOURCC('TWC2'), "TweakPlayerControls" },
{ FOURCC('TWCB'), "TweakCameraBob" },
{ FOURCC('TWCC'), "TweakGamecubeControls" },
{ FOURCC('TWCT'), "TweakControls" },
{ FOURCC('TWEC'), "TweakExpertControls" },
{ FOURCC('TWGM'), "TweakGame" },
{ FOURCC('TWGT'), "TweakGraphicalTransitions" },
{ FOURCC('TWGU'), "TweakGui" },
{ FOURCC('TWGC'), "TweakGuiColors" },
{ FOURCC('TWP2'), "TweakPlayer" },
{ FOURCC('TWPC'), "TweakPlayerControls" },
{ FOURCC('TWPG'), "TweakPlayerGun" },
{ FOURCC('TWPL'), "TweakPlayer" },
{ FOURCC('TWPM'), "TweakPlayerGun" },
{ FOURCC('TWPA'), "TweakParticle" },
{ FOURCC('TWPR'), "TweakPlayerRes" },
{ FOURCC('TWRC'), "TweakRevolutionControls" },
{ FOURCC('TWSS'), "TweakSlideShow" },
{ FOURCC('TWTG'), "TweakTargeting" },
};
uint TweakID = NTWK.ReadLong();
uint16 TweakSize = NTWK.ReadShort();
uint NextTweak = NTWK.Tell() + TweakSize;
auto Find = skIdToTemplateName.find(TweakID);
if (Find == skIdToTemplateName.end())
{
errorf("Unrecognized tweak ID: %s (0x%08X)", *CFourCC(TweakID).ToString(), TweakID);
NTWK.GoTo(NextTweak);
continue;
}
CScriptTemplate* pTweakTemplate = pGameTemplate->FindMiscTemplate( Find->second );
ASSERT( pTweakTemplate != nullptr );
// Load tweak data
NTWK.Skip(0xC);
CTweakData* pTweakData = new CTweakData(pTweakTemplate, TweakID);
CScriptLoader::LoadStructData( NTWK, pTweakData->TweakData() );
OutTweaks.push_back(pTweakData);
NTWK.GoTo(NextTweak);
}
}

View File

@ -0,0 +1,18 @@
#ifndef CTWEAKLOADER_H
#define CTWEAKLOADER_H
#include "CTweakData.h"
/** Class responsible for loading tweak data */
class CTweakLoader
{
/** Private constructor */
CTweakLoader() {}
public:
/** Loader entry point */
static CTweakData* LoadCTWK(IInputStream& CTWK, CResourceEntry* pEntry);
static void LoadNTWK(IInputStream& NTWK, EGame Game, std::vector<CTweakData*>& OutTweaks);
};
#endif // CTWEAKLOADER_H

View File

@ -0,0 +1,90 @@
#include "CTweakManager.h"
#include "Core/GameProject/CGameProject.h"
#include "Core/GameProject/CResourceIterator.h"
#include "Core/Tweaks/CTweakLoader.h"
#include "Core/Tweaks/CTweakCooker.h"
CTweakManager::CTweakManager(CGameProject* pInProject)
: mpProject(pInProject)
{
}
CTweakManager::~CTweakManager()
{
ClearTweaks();
}
void CTweakManager::LoadTweaks()
{
ASSERT( mTweakObjects.empty() );
// MP1 - Load all tweak assets into memory
if (mpProject->Game() <= EGame::Prime)
{
for (TResourceIterator<EResourceType::Tweaks> It(mpProject->ResourceStore()); It; ++It)
{
CTweakData* pTweaks = (CTweakData*) It->Load();
pTweaks->Lock();
mTweakObjects.push_back(pTweaks);
}
}
// MP2+ - Load tweaks from Standard.ntwk
else
{
TString FilePath = mpProject->DiscFilesystemRoot(false) + "Standard.ntwk";
CFileInStream StandardNTWK(FilePath, EEndian::BigEndian);
CTweakLoader::LoadNTWK(StandardNTWK, mpProject->Game(), mTweakObjects);
}
}
bool CTweakManager::SaveTweaks()
{
// MP1 - Save all tweak assets
if (mpProject->Game() <= EGame::Prime)
{
bool SavedAll = true, SavedAny = false;
for (CTweakData* pTweakData : mTweakObjects)
{
if (!pTweakData->Entry()->Save(true))
{
SavedAll = false;
}
else
{
SavedAny = true;
}
}
if (SavedAny)
{
mpProject->ResourceStore()->ConditionalSaveStore();
}
return SavedAll;
}
// MP2+ - Save tweaks to Standard.ntwk
else
{
TString FilePath = mpProject->DiscFilesystemRoot(false) + "Standard.ntwk";
CFileOutStream StandardNTWK(FilePath, EEndian::BigEndian);
return CTweakCooker::CookNTWK(mTweakObjects, StandardNTWK);
}
}
void CTweakManager::ClearTweaks()
{
for (CTweakData* pTweakData : mTweakObjects)
{
if (pTweakData->Entry() != nullptr)
{
pTweakData->Release();
}
else
{
delete pTweakData;
}
}
mTweakObjects.clear();
}

View File

@ -0,0 +1,29 @@
#ifndef CTWEAKMANAGER_H
#define CTWEAKMANAGER_H
#include "CTweakData.h"
/** Class responsible for managing game tweak data, including saving/loading and providing access */
class CTweakManager
{
/** Project */
CGameProject* mpProject;
/** All tweak resources in the current game */
std::vector< CTweakData* > mTweakObjects;
public:
CTweakManager(CGameProject* pInProject);
~CTweakManager();
void LoadTweaks();
bool SaveTweaks();
void ClearTweaks();
// Accessors
inline const std::vector<CTweakData*>& TweakObjects() const
{
return mTweakObjects;
}
};
#endif // CTWEAKMANAGER_H

View File

@ -0,0 +1,59 @@
#ifndef CCUSTOMDELEGATE_H
#define CCUSTOMDELEGATE_H
#include <QFont>
#include <QFontMetrics>
#include <QPen>
#include <QStyledItemDelegate>
/** Font parameters for rendering text */
struct SDelegateFontInfo
{
QFont NameFont;
QFont InfoFont;
QFontMetrics NameFontMetrics;
QFontMetrics InfoFontMetrics;
QPen NamePen;
QPen InfoPen;
int Margin;
int Spacing;
SDelegateFontInfo()
: NameFontMetrics(NameFont), InfoFontMetrics(InfoFont) {}
};
/** Common base class of custom item delegate implementations */
class CCustomDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
explicit CCustomDelegate(QObject* pParent = 0)
: QStyledItemDelegate(pParent)
{}
virtual SDelegateFontInfo GetFontInfo(const QStyleOptionViewItem& rkOption) const
{
SDelegateFontInfo Info;
Info.NameFont = rkOption.font;
Info.NameFont.setPointSize( rkOption.font.pointSize() + 1 );
Info.NameFontMetrics = QFontMetrics(Info.NameFont);
Info.InfoFont = rkOption.font;
Info.InfoFont.setPointSize( rkOption.font.pointSize() - 1 );
Info.InfoFontMetrics = QFontMetrics(Info.InfoFont);
Info.NamePen = QPen(rkOption.palette.text(), 1.f);
Info.InfoPen = QPen(rkOption.palette.text(), 1.f);
Info.InfoPen.setColor( Info.InfoPen.color().darker(140) );
Info.Margin = 3;
Info.Spacing = 3;
return Info;
}
};
#endif // CCUSTOMDELEGATE_H

View File

@ -5,6 +5,8 @@
#include "CProjectSettingsDialog.h"
#include "Editor/CharacterEditor/CCharacterEditor.h"
#include "Editor/ModelEditor/CModelEditorWindow.h"
#include "Editor/ScanEditor/CScanEditor.h"
#include "Editor/StringEditor/CStringEditor.h"
#include "Editor/ResourceBrowser/CResourceBrowser.h"
#include "Editor/WorldEditor/CWorldEditor.h"
#include <Common/Macros.h>
@ -19,6 +21,7 @@ CEditorApplication::CEditorApplication(int& rArgc, char **ppArgv)
, mpActiveProject(nullptr)
, mpWorldEditor(nullptr)
, mpProjectDialog(nullptr)
, mInitialized(false)
{
mLastUpdate = CTimer::GlobalTime();
@ -38,10 +41,14 @@ void CEditorApplication::InitEditor()
mpWorldEditor = new CWorldEditor();
mpProjectDialog = new CProjectSettingsDialog(mpWorldEditor);
mpWorldEditor->showMaximized();
mInitialized = true;
}
bool CEditorApplication::CloseAllEditors()
{
if (!mInitialized)
return true;
// Close active editor windows.
foreach (IEditor *pEditor, mEditorWindows)
{
@ -152,12 +159,31 @@ void CEditorApplication::EditResource(CResourceEntry *pEntry)
case EResourceType::AnimSet:
pEd = new CCharacterEditor((CAnimSet*) pRes, mpWorldEditor);
break;
case EResourceType::Scan:
pEd = new CScanEditor((CScan*) pRes, mpWorldEditor);
break;
case EResourceType::StringTable:
pEd = new CStringEditor((CStringTable*) pRes, mpWorldEditor);
break;
case EResourceType::Tweaks:
{
CTweakEditor* pTweakEditor = mpWorldEditor->TweakEditor();
pTweakEditor->SetActiveTweakData( (CTweakData*) pRes );
pEd = pTweakEditor;
break;
}
}
if (pEd)
{
pEd->show();
mEditingMap[pEntry] = pEd;
if (pEntry->ResourceType() != EResourceType::Tweaks)
mEditingMap[pEntry] = pEd;
}
else if (pEntry->ResourceType() != EResourceType::Area)
UICommon::InfoMsg(mpWorldEditor, "Unsupported Resource", "This resource type is currently unsupported for editing.");
@ -223,6 +249,7 @@ bool CEditorApplication::RebuildResourceDatabase()
{
// Fake-close the project, but keep it in memory so we can modify the resource store
CGameProject *pProj = mpActiveProject;
mpActiveProject->TweakManager()->ClearTweaks();
mpActiveProject = nullptr;
emit ActiveProjectChanged(nullptr);
@ -237,6 +264,7 @@ bool CEditorApplication::RebuildResourceDatabase()
// Set project to active again
mpActiveProject = pProj;
mpActiveProject->TweakManager()->LoadTweaks();
emit ActiveProjectChanged(pProj);
UICommon::InfoMsg(mpWorldEditor, "Success", "Resource database rebuilt successfully!");
@ -303,7 +331,11 @@ void CEditorApplication::OnEditorClose()
}
mEditorWindows.removeOne(pEditor);
delete pEditor;
if (pEditor != mpWorldEditor->TweakEditor())
{
delete pEditor;
}
if (mpActiveProject)
{

View File

@ -25,6 +25,7 @@ class CEditorApplication : public QApplication
CProjectSettingsDialog *mpProjectDialog;
QVector<IEditor*> mEditorWindows;
QMap<CResourceEntry*,IEditor*> mEditingMap;
bool mInitialized;
QTimer mRefreshTimer;
double mLastUpdate;

View File

@ -33,8 +33,7 @@ CExportGameDialog::CExportGameDialog(const QString& rkIsoPath, const QString& rk
mpUI->setupUi(this);
// Set up disc
TWideString StrPath = TO_TWIDESTRING(rkIsoPath);
mpDisc = nod::OpenDiscFromImage(*StrPath).release();
mpDisc = nod::OpenDiscFromImage(TO_WCHAR(rkIsoPath)).release();
if (ValidateGame())
{
@ -151,12 +150,32 @@ bool CExportGameDialog::ValidateGame()
// 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;
}
// Calculate the CRC of the apploader to figure out which game this is.
std::unique_ptr<uint8_t[]> pApploaderData = mpDisc->getDataPartition()->getApploaderBuf();
uint ApploaderSize = (uint) mpDisc->getDataPartition()->getApploaderSize();
uint ApploaderHash = CCRC32::StaticHashData(pApploaderData.get(), ApploaderSize);
mGame = EGame::Prime;
break;
if (ApploaderHash == 0x21B7AFF5)
{
// This is the hash for the NTSC MP1 demo.
mGame = EGame::PrimeDemo;
}
else
{
// Hash is different, so this is most likely an Echoes demo build
mGame = EGame::EchoesDemo;
}
break;
}
else
{
// This could be either Metroid Prime, or the PAL demo of it...
// In either case, the PAL demo is based on a later build of the game than the NTSC demo
// So the PAL demo should be configured the same way as the release build of the game anyway
mGame = EGame::Prime;
break;
}
case FOURCC('G2MX'):
// Echoes, but also appears in the MP3 proto
@ -203,6 +222,16 @@ bool CExportGameDialog::ValidateGame()
return false;
}
// The demo builds are not supported. The MP1 demo does not have script templates currently.
// Additionally, a lot of file format loaders currently don't support the demo variants of the
// file formats, meaning that attempting to export results in crashes.
if (mGame == EGame::PrimeDemo || mGame == EGame::EchoesDemo || mGame == EGame::CorruptionProto)
{
// we cannot parent the error message box to ourselves because this window hasn't been shown
UICommon::ErrorMsg(parentWidget(), "The demo builds are currently not supported.");
return false;
}
return true;
}

View File

@ -164,7 +164,7 @@ void CProjectSettingsDialog::BuildISO()
// Verify this ISO matches the original
bool IsWii;
pBaseDisc = nod::OpenDiscFromImage(*TO_TWIDESTRING(SourceIsoPath), IsWii);
pBaseDisc = nod::OpenDiscFromImage(TO_WCHAR(SourceIsoPath), IsWii);
if (!pBaseDisc || !IsWii)
{

150
src/Editor/CTweakEditor.cpp Normal file
View File

@ -0,0 +1,150 @@
#include "CTweakEditor.h"
#include "ui_CTweakEditor.h"
#include "Editor/Undo/IUndoCommand.h"
/** Internal undo command for changing tabs */
class CSetTweakIndexCommand : public IUndoCommand
{
CTweakEditor* mpEditor;
int mOldIndex, mNewIndex;
public:
CSetTweakIndexCommand(CTweakEditor* pEditor, int OldIndex, int NewIndex)
: IUndoCommand("Change Tab")
, mpEditor(pEditor)
, mOldIndex(OldIndex)
, mNewIndex(NewIndex)
{}
virtual void undo() override { mpEditor->SetActiveTweakIndex(mOldIndex); }
virtual void redo() override { mpEditor->SetActiveTweakIndex(mNewIndex); }
virtual bool AffectsCleanState() const { return false; }
};
/** CTweakEditor functions */
CTweakEditor::CTweakEditor(QWidget* pParent)
: IEditor(pParent)
, mpUI(new Ui::CTweakEditor)
, mCurrentTweakIndex(-1)
, mHasBeenShown(false)
{
mpUI->setupUi(this);
mpUI->TweakTabs->setExpanding(false);
mpUI->ToolBar->addSeparator();
AddUndoActions(mpUI->ToolBar);
SET_WINDOWTITLE_APPVARS("%APP_FULL_NAME% - Tweak Editor[*]");
connect(mpUI->TweakTabs, SIGNAL(currentChanged(int)), this, SLOT(OnTweakTabClicked(int)));
connect(mpUI->ActionSave, SIGNAL(triggered(bool)), this, SLOT(Save()));
connect(mpUI->ActionSaveAndRepack, SIGNAL(triggered(bool)), this, SLOT(SaveAndRepack()));
}
CTweakEditor::~CTweakEditor()
{
delete mpUI;
}
bool CTweakEditor::HasTweaks()
{
return !mTweakAssets.isEmpty();
}
bool CTweakEditor::Save()
{
if (!gpEdApp->ActiveProject()->TweakManager()->SaveTweaks())
{
UICommon::ErrorMsg(this, "Tweaks failed to save!");
return false;
}
else
{
UndoStack().setClean();
setWindowModified(false);
return true;
}
}
void CTweakEditor::SetActiveTweakData(CTweakData* pTweakData)
{
for( int TweakIdx = 0; TweakIdx < mTweakAssets.size(); TweakIdx++ )
{
if (mTweakAssets[TweakIdx] == pTweakData)
{
CSetTweakIndexCommand* pCommand = new CSetTweakIndexCommand(this, mCurrentTweakIndex, TweakIdx);
UndoStack().push(pCommand);
break;
}
}
}
void CTweakEditor::SetActiveTweakIndex(int Index)
{
if( mCurrentTweakIndex != Index )
{
mCurrentTweakIndex = Index;
CTweakData* pTweakData = mTweakAssets[Index];
mpUI->PropertyView->SetIntrinsicProperties(pTweakData->TweakData());
mpUI->TweakTabs->blockSignals(true);
mpUI->TweakTabs->setCurrentIndex(Index);
mpUI->TweakTabs->blockSignals(false);
}
}
void CTweakEditor::OnTweakTabClicked(int Index)
{
if (Index != mCurrentTweakIndex)
{
CSetTweakIndexCommand* pCommand = new CSetTweakIndexCommand(this, mCurrentTweakIndex, Index);
UndoStack().push(pCommand);
}
}
void CTweakEditor::OnProjectChanged(CGameProject* pNewProject)
{
// Close and clear tabs
mCurrentTweakIndex = -1;
mpUI->PropertyView->ClearProperties();
close();
mpUI->TweakTabs->blockSignals(true);
while (mpUI->TweakTabs->count() > 0)
{
mpUI->TweakTabs->removeTab(0);
}
mTweakAssets.clear();
UndoStack().clear();
// Create tweak list
if (pNewProject != nullptr)
{
for (CTweakData* pTweakData : pNewProject->TweakManager()->TweakObjects())
{
mTweakAssets << pTweakData;
}
}
// Sort in alphabetical order and create tabs
if (!mTweakAssets.isEmpty())
{
qSort(mTweakAssets.begin(), mTweakAssets.end(), [](CTweakData* pLeft, CTweakData* pRight) -> bool {
return pLeft->TweakName().ToUpper() < pRight->TweakName().ToUpper();
});
foreach (CTweakData* pTweakData, mTweakAssets)
{
QString TweakName = TO_QSTRING( pTweakData->TweakName() );
mpUI->TweakTabs->addTab(TweakName);
}
SetActiveTweakIndex(0);
}
mpUI->TweakTabs->blockSignals(false);
// Hide "save and repack" button for MP2+ as it doesn't do anything different from the regular Save button
mpUI->ActionSaveAndRepack->setVisible( !pNewProject || pNewProject->Game() <= EGame::Prime );
}

40
src/Editor/CTweakEditor.h Normal file
View File

@ -0,0 +1,40 @@
#ifndef CTWEAKEDITOR_H
#define CTWEAKEDITOR_H
#include "Editor/IEditor.h"
namespace Ui {
class CTweakEditor;
}
class CTweakEditor : public IEditor
{
Q_OBJECT
/** Qt UI */
Ui::CTweakEditor* mpUI;
/** List of editable tweak assets */
QVector<CTweakData*> mTweakAssets;
/** Whether the editor window has been shown before */
bool mHasBeenShown;
/** Index of tweak data currently being edited */
int mCurrentTweakIndex;
public:
explicit CTweakEditor(QWidget* pParent = 0);
~CTweakEditor();
bool HasTweaks();
virtual bool Save() override;
public slots:
void SetActiveTweakData(CTweakData* pTweakData);
void SetActiveTweakIndex(int Index);
void OnTweakTabClicked(int Index);
void OnProjectChanged(CGameProject* pNewProject);
};
#endif // CTWEAKEDITOR_H

120
src/Editor/CTweakEditor.ui Normal file
View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CTweakEditor</class>
<widget class="QMainWindow" name="CTweakEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>452</width>
<height>644</height>
</rect>
</property>
<property name="windowTitle">
<string>Tweaks Editor</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTabBar" name="TweakTabs" native="true"/>
</item>
<item>
<widget class="CPropertyView" name="PropertyView">
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum>
</property>
<property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QToolBar" name="ToolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="ActionSave"/>
<addaction name="ActionSaveAndRepack"/>
</widget>
<action name="ActionSave">
<property name="icon">
<iconset resource="Icons.qrc">
<normaloff>:/icons/Save.png</normaloff>:/icons/Save.png</iconset>
</property>
<property name="text">
<string>Save</string>
</property>
<property name="toolTip">
<string>Save</string>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</action>
<action name="ActionSaveAndRepack">
<property name="icon">
<iconset resource="Icons.qrc">
<normaloff>:/icons/SaveAndRepack_32px.png</normaloff>:/icons/SaveAndRepack_32px.png</iconset>
</property>
<property name="text">
<string>Save and Cook</string>
</property>
<property name="toolTip">
<string>Save and Cook</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>QTabBar</class>
<extends>QWidget</extends>
<header location="global">QTabBar</header>
<container>1</container>
</customwidget>
<customwidget>
<class>CPropertyView</class>
<extends>QTreeView</extends>
<header>Editor/PropertyEdit/CPropertyView.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="Icons.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -25,9 +25,16 @@ public:
// Note: All function calls should be deferred with QMetaObject::invokeMethod to ensure
// that they run on the UI thread instead of whatever thread we happen to be on.
virtual void AsyncMessageBox(const TString& rkInfoBoxTitle, const TString& rkMessage)
virtual void ShowMessageBox(const TString& rkInfoBoxTitle, const TString& rkMessage)
{
QMetaObject::invokeMethod(this, "AsyncMessageBoxSlot", Qt::QueuedConnection,
QMetaObject::invokeMethod(this, "MessageBoxSlot", GetConnectionType(),
Q_ARG(QString, TO_QSTRING(rkInfoBoxTitle)),
Q_ARG(QString, TO_QSTRING(rkMessage)) );
}
virtual void ShowMessageBoxAsync(const TString& rkInfoBoxTitle, const TString& rkMessage)
{
QMetaObject::invokeMethod(this, "MessageBoxSlot", Qt::QueuedConnection,
Q_ARG(QString, TO_QSTRING(rkInfoBoxTitle)),
Q_ARG(QString, TO_QSTRING(rkMessage)) );
}
@ -42,8 +49,17 @@ public:
return RetVal;
}
public slots:
void AsyncMessageBoxSlot(const QString& rkInfoBoxTitle, const QString& rkMessage)
virtual bool OpenProject(const TString& kPath = "")
{
bool RetVal;
QMetaObject::invokeMethod(this, "OpenProjectSlot", GetConnectionType(),
Q_RETURN_ARG(bool, RetVal),
Q_ARG(QString, TO_QSTRING(kPath)) );
return RetVal;
}
private slots:
void MessageBoxSlot(const QString& rkInfoBoxTitle, const QString& rkMessage)
{
UICommon::InfoMsg(gpEdApp->WorldEditor(), rkInfoBoxTitle, rkMessage);
}
@ -52,6 +68,11 @@ public slots:
{
return UICommon::YesNoQuestion(gpEdApp->WorldEditor(), rkInfoBoxTitle, rkQuestion);
}
bool OpenProjectSlot(const QString& kPath)
{
return !kPath.isEmpty() ? gpEdApp->OpenProject(kPath) : UICommon::OpenProject();
}
};
#endif // CUIRELAY_H

View File

@ -96,14 +96,9 @@ HEADERS += \
Undo/CTranslateNodeCommand.h \
Undo/EUndoCommand.h \
Undo/UndoCommands.h \
Widgets/IPreviewPanel.h \
Widgets/WColorPicker.h \
Widgets/WDraggableSpinBox.h \
Widgets/WIntegralSpinBox.h \
Widgets/WScanPreviewPanel.h \
Widgets/WStringPreviewPanel.h \
Widgets/WTextureGLWidget.h \
Widgets/WTexturePreviewPanel.h \
Widgets/WVectorEditor.h \
WorldEditor/CLayerEditor.h \
WorldEditor/CLayerModel.h \
@ -198,7 +193,18 @@ HEADERS += \
Widgets/CCheckableTreeWidgetItem.h \
Widgets/CCheckableTreeWidget.h \
Undo/IEditPropertyCommand.h \
Widgets/TEnumComboBox.h
Widgets/TEnumComboBox.h \
StringEditor/CStringEditor.h \
StringEditor/CStringListModel.h \
StringEditor/CStringDelegate.h \
CCustomDelegate.h \
CTweakEditor.h \
Undo/CEditIntrinsicPropertyCommand.h \
Undo/TSerializeUndoCommand.h \
StringEditor/CStringMimeData.h \
ScanEditor/CScanEditor.h \
Undo/ICreateDeleteResourceCommand.h \
Undo/CSaveStoreCommand.h
# Source Files
SOURCES += \
@ -207,14 +213,9 @@ SOURCES += \
Undo/CRotateNodeCommand.cpp \
Undo/CScaleNodeCommand.cpp \
Undo/CTranslateNodeCommand.cpp \
Widgets/IPreviewPanel.cpp \
Widgets/WColorPicker.cpp \
Widgets/WDraggableSpinBox.cpp \
Widgets/WIntegralSpinBox.cpp \
Widgets/WScanPreviewPanel.cpp \
Widgets/WStringPreviewPanel.cpp \
Widgets/WTextureGLWidget.cpp \
Widgets/WTexturePreviewPanel.cpp \
Widgets/WVectorEditor.cpp \
WorldEditor/CLayerEditor.cpp \
WorldEditor/CLayerModel.cpp \
@ -273,14 +274,18 @@ SOURCES += \
ResourceBrowser/CVirtualDirectoryTreeView.cpp \
CPropertyNameValidator.cpp \
CGeneratePropertyNamesDialog.cpp \
Undo/IEditPropertyCommand.cpp
Undo/IEditPropertyCommand.cpp \
StringEditor/CStringEditor.cpp \
StringEditor/CStringListModel.cpp \
IEditor.cpp \
StringEditor/CStringDelegate.cpp \
CTweakEditor.cpp \
ScanEditor/CScanEditor.cpp
# UI Files
FORMS += \
TestDialog.ui \
ModelEditor/CModelEditorWindow.ui \
Widgets/WScanPreviewPanel.ui \
Widgets/WTexturePreviewPanel.ui \
WorldEditor/CLayerEditor.ui \
WorldEditor/CWorldEditor.ui \
WorldEditor/WCreateTab.ui \
@ -300,7 +305,10 @@ FORMS += \
WorldEditor/CPoiMapSidebar.ui \
CProgressDialog.ui \
Widgets/CSelectResourcePanel.ui \
CGeneratePropertyNamesDialog.ui
CGeneratePropertyNamesDialog.ui \
StringEditor/CStringEditor.ui \
CTweakEditor.ui \
ScanEditor/CScanEditor.ui
# Codegen
CODEGEN_DIR = $$EXTERNALS_DIR/CodeGen

148
src/Editor/IEditor.cpp Normal file
View File

@ -0,0 +1,148 @@
#include "IEditor.h"
#include "Editor/Undo/IUndoCommand.h"
#include <QMenu>
#include <QMessageBox>
#include <QToolBar>
IEditor::IEditor(QWidget* pParent)
: QMainWindow(pParent)
{
// Register the editor window
gpEdApp->AddEditor(this);
// Create undo actions
QAction *pUndoAction = mUndoStack.createUndoAction(this);
QAction *pRedoAction = mUndoStack.createRedoAction(this);
pUndoAction->setShortcut(QKeySequence::Undo);
pRedoAction->setShortcut(QKeySequence::Redo);
pUndoAction->setIcon(QIcon(":/icons/Undo.png"));
pRedoAction->setIcon(QIcon(":/icons/Redo.png"));
mUndoActions.push_back(pUndoAction);
mUndoActions.push_back(pRedoAction);
connect(&mUndoStack, SIGNAL(indexChanged(int)), this, SLOT(OnUndoStackIndexChanged()));
}
QUndoStack& IEditor::UndoStack()
{
return mUndoStack;
}
void IEditor::AddUndoActions(QToolBar* pToolBar, QAction* pBefore /*= 0*/)
{
pToolBar->insertActions(pBefore, mUndoActions);
}
void IEditor::AddUndoActions(QMenu* pMenu, QAction* pBefore /*= 0*/)
{
pMenu->insertActions(pBefore, mUndoActions);
}
bool IEditor::CheckUnsavedChanges()
{
// Check whether the user has unsaved changes, return whether it's okay to clear the scene
bool OkToClear = !isWindowModified();
if (!OkToClear)
{
int Result = QMessageBox::warning(this, "Save", "You have unsaved changes. Save?", QMessageBox::Yes, QMessageBox::No, QMessageBox::Cancel);
if (Result == QMessageBox::Yes)
OkToClear = Save();
else if (Result == QMessageBox::No)
{
mUndoStack.setIndex(0); // Revert all changes
OkToClear = true;
}
else if (Result == QMessageBox::Cancel)
OkToClear = false;
}
return OkToClear;
}
/** QMainWindow overrides */
void IEditor::closeEvent(QCloseEvent* pEvent)
{
if (CheckUnsavedChanges())
{
mUndoStack.clear();
pEvent->accept();
emit Closed();
}
else
{
pEvent->ignore();
}
}
/** Non-virtual slots */
bool IEditor::SaveAndRepack()
{
if (Save())
{
gpEdApp->CookAllDirtyPackages();
return true;
}
else return false;
}
void IEditor::OnUndoStackIndexChanged()
{
// Check the commands that have been executed on the undo stack and find out whether any of them affect the clean state.
// This is to prevent commands like select/deselect from altering the clean state.
int CurrentIndex = mUndoStack.index();
int CleanIndex = mUndoStack.cleanIndex();
if (CleanIndex == -1)
{
if (!isWindowModified())
mUndoStack.setClean();
return;
}
if (CurrentIndex == CleanIndex)
setWindowModified(false);
else
{
bool IsClean = true;
int LowIndex = (CurrentIndex > CleanIndex ? CleanIndex : CurrentIndex);
int HighIndex = (CurrentIndex > CleanIndex ? CurrentIndex - 1 : CleanIndex - 1);
for (int i = LowIndex; i <= HighIndex; i++)
{
const QUndoCommand *pkQCmd = mUndoStack.command(i);
if (const IUndoCommand* pkCmd = dynamic_cast<const IUndoCommand*>(pkQCmd))
{
if (pkCmd->AffectsCleanState())
IsClean = false;
}
else if (pkQCmd->childCount() > 0)
{
for (int ChildIdx = 0; ChildIdx < pkQCmd->childCount(); ChildIdx++)
{
const IUndoCommand *pkCmd = static_cast<const IUndoCommand*>(pkQCmd->child(ChildIdx));
if (pkCmd->AffectsCleanState())
{
IsClean = false;
break;
}
}
}
if (!IsClean) break;
}
setWindowModified(!IsClean);
}
}

View File

@ -2,23 +2,50 @@
#define IEDITOR
#include <QMainWindow>
#include <QAction>
#include <QList>
#include <QUndoStack>
#include "CEditorApplication.h"
/** Base class of all editor windows */
class IEditor : public QMainWindow
{
Q_OBJECT
public:
IEditor(QWidget *pParent)
: QMainWindow(pParent)
{
gpEdApp->AddEditor(this);
}
protected:
// Undo stack
QUndoStack mUndoStack;
QList<QAction*> mUndoActions;
virtual void closeEvent(QCloseEvent*) { emit Closed(); }
public:
IEditor(QWidget* pParent);
QUndoStack& UndoStack();
void AddUndoActions(QToolBar* pToolBar, QAction* pBefore = 0);
void AddUndoActions(QMenu* pMenu, QAction* pBefore = 0);
bool CheckUnsavedChanges();
/** QMainWindow overrides */
virtual void closeEvent(QCloseEvent*);
/** Interface */
virtual void EditorTick(float /*DeltaTime*/) { }
virtual CBasicViewport* Viewport() const { return nullptr; }
public slots:
/** Virtual slots */
virtual bool Save()
{
// Default implementation for editor windows that do not support resaving assets.
// This should not be called.
errorf("Base IEditor::Save() implementation called. Changes will not be saved.");
return true;
}
/** Non-virtual slots */
bool SaveAndRepack();
void OnUndoStackIndexChanged();
signals:
void Closed();
};

View File

@ -15,14 +15,6 @@ INodeEditor::INodeEditor(QWidget *pParent)
, mRotateSpace(ETransformSpace::World)
, mCloneState(eNotCloning)
{
// Create undo actions
QAction *pUndoAction = mUndoStack.createUndoAction(this);
QAction *pRedoAction = mUndoStack.createRedoAction(this);
pUndoAction->setShortcut(QKeySequence::Undo);
pRedoAction->setShortcut(QKeySequence::Redo);
mUndoActions.push_back(pUndoAction);
mUndoActions.push_back(pRedoAction);
// Create gizmo actions
mGizmoActions.append(new QAction(QIcon(":/icons/SelectMode.png"), "Select Objects", this));
mGizmoActions.append(new QAction(QIcon(":/icons/Translate.png"), "Translate", this));
@ -63,11 +55,6 @@ INodeEditor::~INodeEditor()
delete mpSelection;
}
QUndoStack* INodeEditor::UndoStack()
{
return &mUndoStack;
}
CScene* INodeEditor::Scene()
{
return &mScene;

View File

@ -12,17 +12,12 @@
#include <QActionGroup>
#include <QComboBox>
#include <QList>
#include <QUndoStack>
class INodeEditor : public IEditor
{
Q_OBJECT
protected:
// Undo stack
QUndoStack mUndoStack;
QList<QAction*> mUndoActions;
// Node management
CScene mScene;
CNodeSelection *mpSelection;
@ -55,7 +50,6 @@ protected:
public:
explicit INodeEditor(QWidget *pParent = 0);
virtual ~INodeEditor();
QUndoStack* UndoStack();
CScene* Scene();
CGizmo* Gizmo();
bool IsGizmoVisible();

View File

@ -151,6 +151,17 @@ CModelEditorWindow::~CModelEditorWindow()
delete ui;
}
bool CModelEditorWindow::Save()
{
if (!mpCurrentModel) return true;
bool SaveSuccess = mpCurrentModel->Entry()->Save();
if (SaveSuccess)
gpEdApp->NotifyAssetsModified();
return SaveSuccess;
}
void CModelEditorWindow::RefreshViewport()
{
ui->Viewport->ProcessInput();
@ -749,15 +760,6 @@ void CModelEditorWindow::Import()
gpResourceStore->DestroyUnreferencedResources();
}
void CModelEditorWindow::Save()
{
if (!mpCurrentModel) return;
bool SaveSuccess = mpCurrentModel->Entry()->Save();
if (SaveSuccess)
gpEdApp->NotifyAssetsModified();
}
void CModelEditorWindow::ConvertToDDS()
{
QString Input = QFileDialog::getOpenFileName(this, "Retro Texture (*.TXTR)", "", "*.TXTR");

View File

@ -34,6 +34,7 @@ class CModelEditorWindow : public IEditor
public:
explicit CModelEditorWindow(CModel *pModel, QWidget *pParent = 0);
~CModelEditorWindow();
bool Save();
void SetActiveModel(CModel *pModel);
CModelEditorViewport* Viewport() const;
@ -100,7 +101,6 @@ private:
private slots:
void Import();
void Save();
void ConvertToDDS();
void ConvertToTXTR();
void SetMeshPreview();

View File

@ -3,6 +3,7 @@
#include "Editor/UICommon.h"
#include "Editor/Undo/CEditScriptPropertyCommand.h"
#include "Editor/Undo/CEditIntrinsicPropertyCommand.h"
#include "Editor/Undo/CResizeScriptArrayCommand.h"
#include "Editor/Widgets/CResourceSelector.h"
#include "Editor/Widgets/WColorPicker.h"
@ -25,7 +26,7 @@
connect(pRelay, SIGNAL(WidgetEdited(QWidget*, const QModelIndex&)), this, SLOT(WidgetEdited(QWidget*, const QModelIndex&))); \
}
CPropertyDelegate::CPropertyDelegate(QObject *pParent /*= 0*/)
CPropertyDelegate::CPropertyDelegate(QObject* pParent /*= 0*/)
: QStyledItemDelegate(pParent)
, mpEditor(nullptr)
, mpModel(nullptr)
@ -33,19 +34,20 @@ CPropertyDelegate::CPropertyDelegate(QObject *pParent /*= 0*/)
, mEditInProgress(false)
, mRelaysBlocked(false)
{
mpEditor = UICommon::FindAncestor<IEditor>(pParent);
}
void CPropertyDelegate::SetPropertyModel(CPropertyModel *pModel)
{
mpModel = pModel;
}
void CPropertyDelegate::SetEditor(CWorldEditor *pEditor)
void CPropertyDelegate::SetEditor(IEditor* pEditor)
{
mpEditor = pEditor;
}
QWidget* CPropertyDelegate::createEditor(QWidget *pParent, const QStyleOptionViewItem& /*rkOption*/, const QModelIndex& rkIndex) const
void CPropertyDelegate::SetPropertyModel(CPropertyModel* pModel)
{
mpModel = pModel;
}
QWidget* CPropertyDelegate::createEditor(QWidget* pParent, const QStyleOptionViewItem& /*rkOption*/, const QModelIndex& rkIndex) const
{
if (!mpModel) return nullptr;
IProperty *pProp = mpModel->PropertyForIndex(rkIndex, false);
@ -366,16 +368,28 @@ void CPropertyDelegate::setModelData(QWidget *pEditor, QAbstractItemModel* /*pMo
if (pProp)
{
EPropertyType Type = mpModel->GetEffectiveFieldType(pProp);
CScriptObject* pObject = mpModel->GetScriptObject();
QVector<CScriptObject*> Objects;
Objects << mpModel->GetScriptObject();
if (!pObject)
{
QVector<void*> DataPointers;
DataPointers << pData;
pCommand = new CEditIntrinsicPropertyCommand(pProp, DataPointers, mpModel, rkIndex);
}
else
{
QVector<CScriptObject*> Objects;
Objects << pObject;
pCommand = (Type != EPropertyType::Array) ?
new CEditScriptPropertyCommand(pProp, Objects, mpModel, rkIndex) :
new CResizeScriptArrayCommand (pProp, Objects, mpModel, rkIndex);
}
pCommand->SaveOldData();
if (Type != EPropertyType::Array)
{
// TODO: support this for non script object properties
pCommand = new CEditScriptPropertyCommand(pProp, mpEditor, Objects, rkIndex);
pCommand->SaveOldData();
// Handle sub-properties of flags and animation sets
if (rkIndex.internalId() & 0x80000000)
{
@ -487,9 +501,6 @@ void CPropertyDelegate::setModelData(QWidget *pEditor, QAbstractItemModel* /*pMo
// Array
else
{
pCommand = new CResizeScriptArrayCommand(pProp, mpEditor, Objects, mpModel, rkIndex);
pCommand->SaveOldData();
WIntegralSpinBox* pSpinBox = static_cast<WIntegralSpinBox*>(pEditor);
CArrayProperty* pArray = static_cast<CArrayProperty*>(pProp);
int OldCount = pArray->ArrayCount(pData);
@ -525,7 +536,7 @@ void CPropertyDelegate::setModelData(QWidget *pEditor, QAbstractItemModel* /*pMo
{
// Always consider the edit done for bool properties
pCommand->SetEditComplete(!mEditInProgress || pProp->Type() == EPropertyType::Bool);
mpEditor->UndoStack()->push(pCommand);
mpEditor->UndoStack().push(pCommand);
}
else
@ -565,9 +576,9 @@ QWidget* CPropertyDelegate::CreateCharacterEditor(QWidget *pParent, const QModel
pSelector->SetFrameVisible(false);
if (Params.Version() <= EGame::Echoes)
pSelector->SetTypeFilter(mpEditor->CurrentGame(), "ANCS");
pSelector->SetTypeFilter(gpEdApp->CurrentGame(), "ANCS");
else
pSelector->SetTypeFilter(mpEditor->CurrentGame(), "CHAR");
pSelector->SetTypeFilter(gpEdApp->CurrentGame(), "CHAR");
CONNECT_RELAY(pSelector, rkIndex, ResourceChanged(CResourceEntry*));
return pSelector;
@ -631,7 +642,7 @@ void CPropertyDelegate::SetCharacterModelData(QWidget *pEditor, const QModelInde
if (Type == EPropertyType::Asset)
{
CResourceEntry *pEntry = static_cast<CResourceSelector*>(pEditor)->Entry();
Params.SetResource( pEntry ? pEntry->ID() : CAssetID::InvalidID(mpEditor->CurrentGame()) );
Params.SetResource( pEntry ? pEntry->ID() : CAssetID::InvalidID(gpEdApp->CurrentGame()) );
}
else if (Type == EPropertyType::Enum || Type == EPropertyType::Choice)

View File

@ -9,29 +9,29 @@ class CPropertyDelegate : public QStyledItemDelegate
{
Q_OBJECT
CWorldEditor *mpEditor;
CPropertyModel *mpModel;
IEditor* mpEditor;
CPropertyModel* mpModel;
bool mInRelayWidgetEdit;
mutable bool mEditInProgress;
mutable bool mRelaysBlocked;
public:
CPropertyDelegate(QObject *pParent = 0);
void SetPropertyModel(CPropertyModel *pModel);
void SetEditor(CWorldEditor *pEditor);
CPropertyDelegate(QObject* pParent = 0);
void SetEditor(IEditor* pEditor);
void SetPropertyModel(CPropertyModel* pModel);
virtual QWidget* createEditor(QWidget *pParent, const QStyleOptionViewItem& rkOption, const QModelIndex& rkIndex) const;
virtual void setEditorData(QWidget *pEditor, const QModelIndex &rkIndex) const;
virtual void setModelData(QWidget *pEditor, QAbstractItemModel *pModel, const QModelIndex &rkIndex) const;
bool eventFilter(QObject *pObject, QEvent *pEvent);
virtual QWidget* createEditor(QWidget* pParent, const QStyleOptionViewItem& rkOption, const QModelIndex& rkIndex) const;
virtual void setEditorData(QWidget* pEditor, const QModelIndex& rkIndex) const;
virtual void setModelData(QWidget* pEditor, QAbstractItemModel* pModel, const QModelIndex& rkIndex) const;
bool eventFilter(QObject* pObject, QEvent* pEvent);
QWidget* CreateCharacterEditor(QWidget *pParent, const QModelIndex& rkIndex) const;
void SetCharacterEditorData(QWidget *pEditor, const QModelIndex& rkIndex) const;
void SetCharacterModelData(QWidget *pEditor, const QModelIndex& rkIndex) const;
QWidget* CreateCharacterEditor(QWidget* pParent, const QModelIndex& rkIndex) const;
void SetCharacterEditorData(QWidget* pEditor, const QModelIndex& rkIndex) const;
void SetCharacterModelData(QWidget* pEditor, const QModelIndex& rkIndex) const;
EPropertyType DetermineCharacterPropType(EGame Game, const QModelIndex& rkIndex) const;
public slots:
void WidgetEdited(QWidget *pWidget, const QModelIndex& rkIndex);
void WidgetEdited(QWidget* pWidget, const QModelIndex& rkIndex);
protected:
void BlockRelays(bool Block) const { mRelaysBlocked = Block; }

View File

@ -9,7 +9,6 @@
CPropertyView::CPropertyView(QWidget *pParent)
: QTreeView(pParent)
, mpEditor(nullptr)
, mpMenuProperty(nullptr)
{
mpModel = new CPropertyModel(this);
@ -80,21 +79,36 @@ bool CPropertyView::event(QEvent *pEvent)
pEvent->ignore();
return true;
}
else if (pEvent->type() == QEvent::Resize && !isVisible())
{
resizeColumnToContents(0);
}
else return QTreeView::event(pEvent);
return QTreeView::event(pEvent);
}
void CPropertyView::SetEditor(CWorldEditor *pEditor)
int CPropertyView::sizeHintForColumn(int Column) const
{
if (Column == 0)
return width() * 0.6f;
else
return width() * 0.4f;
}
void CPropertyView::SetEditor(IEditor* pEditor)
{
mpEditor = pEditor;
mpDelegate->SetEditor(pEditor);
connect(mpEditor, SIGNAL(PropertyModified(CScriptObject*,IProperty*)), mpModel, SLOT(NotifyPropertyModified(CScriptObject*,IProperty*)));
}
void CPropertyView::ClearProperties()
{
mpObject = nullptr;
mpModel->ConfigureScript(nullptr, nullptr, nullptr);
}
void CPropertyView::SetIntrinsicProperties(CStructRef InProperties)
{
mpObject = nullptr;
mpModel->SetBoldModifiedProperties(false); // todo, we prob want this, but can't set default properties on non script yet
mpModel->ConfigureIntrinsic(nullptr, InProperties.Property(), InProperties.DataPointer());
SetPersistentEditors(QModelIndex());
}
@ -102,7 +116,7 @@ void CPropertyView::SetIntrinsicProperties(CStructRef InProperties)
void CPropertyView::SetInstance(CScriptObject *pObj)
{
mpObject = pObj;
mpModel->SetBoldModifiedProperties(mpEditor ? (mpEditor->CurrentGame() > EGame::Prime) : true);
mpModel->SetBoldModifiedProperties(gpEdApp->CurrentGame() > EGame::Prime);
if (pObj)
mpModel->ConfigureScript(pObj->Area()->Entry()->Project(), pObj->Template()->Properties(), pObj);
@ -121,7 +135,7 @@ void CPropertyView::SetInstance(CScriptObject *pObj)
void CPropertyView::UpdateEditorProperties(const QModelIndex& rkParent)
{
// Check what game this is
EGame Game = mpEditor->CurrentGame();
EGame Game = gpEdApp->CurrentGame();
// Iterate over all properties and update if they're an editor property.
for (int iRow = 0; iRow < mpModel->rowCount(rkParent); iRow++)
@ -179,8 +193,7 @@ void CPropertyView::SetPersistentEditors(const QModelIndex& rkParent)
if (pProp->Type() == EPropertyType::AnimationSet)
{
EGame Game = mpObject->Area()->Game();
Type = mpDelegate->DetermineCharacterPropType(Game, ChildIndex);
Type = mpDelegate->DetermineCharacterPropType(pProp->Game(), ChildIndex);
IsAnimSet = true;
}
@ -237,6 +250,10 @@ void CPropertyView::OnPropertyModified(const QModelIndex& rkIndex)
ClosePersistentEditors(rkIndex);
SetPersistentEditors(rkIndex);
}
scrollTo(rkIndex);
emit PropertyModified(rkIndex);
emit PropertyModified(pProperty);
}
void CPropertyView::RefreshView()
@ -260,7 +277,7 @@ void CPropertyView::CreateContextMenu(const QPoint& rkPos)
Menu.addAction(mpEditTemplateAction);
}
if (mpEditor->CurrentGame() >= EGame::EchoesDemo)
if (gpEdApp->CurrentGame() >= EGame::EchoesDemo)
{
Menu.addAction(mpShowNameValidityAction);
}
@ -297,7 +314,8 @@ void CPropertyView::ToggleShowNameValidity(bool ShouldShow)
void CPropertyView::EditPropertyTemplate()
{
CTemplateEditDialog Dialog(mpMenuProperty, mpEditor);
QMainWindow* pParentWindow = UICommon::FindAncestor<QMainWindow>(this);
CTemplateEditDialog Dialog(mpMenuProperty, pParentWindow);
connect(&Dialog, SIGNAL(PerformedTypeConversion()), this, SLOT(RefreshView()));
Dialog.exec();
}
@ -305,21 +323,21 @@ void CPropertyView::EditPropertyTemplate()
void CPropertyView::GenerateNamesForProperty()
{
CGeneratePropertyNamesDialog* pDialog = mpEditor->NameGeneratorDialog();
CGeneratePropertyNamesDialog* pDialog = gpEdApp->WorldEditor()->NameGeneratorDialog();
pDialog->AddToIDPool(mpMenuProperty);
pDialog->show();
}
void CPropertyView::GenerateNamesForSiblings()
{
CGeneratePropertyNamesDialog* pDialog = mpEditor->NameGeneratorDialog();
CGeneratePropertyNamesDialog* pDialog = gpEdApp->WorldEditor()->NameGeneratorDialog();
pDialog->AddChildrenToIDPool(mpMenuProperty->Parent(), false);
pDialog->show();
}
void CPropertyView::GenerateNamesForChildren()
{
CGeneratePropertyNamesDialog* pDialog = mpEditor->NameGeneratorDialog();
CGeneratePropertyNamesDialog* pDialog = gpEdApp->WorldEditor()->NameGeneratorDialog();
pDialog->AddChildrenToIDPool(mpMenuProperty, false);
pDialog->show();
}

View File

@ -10,25 +10,27 @@ class CPropertyView : public QTreeView
{
Q_OBJECT
CWorldEditor *mpEditor;
CPropertyModel *mpModel;
CPropertyDelegate *mpDelegate;
CScriptObject *mpObject;
CPropertyModel* mpModel;
CPropertyDelegate* mpDelegate;
CScriptObject* mpObject;
IProperty *mpMenuProperty;
QAction *mpShowNameValidityAction;
QAction *mpEditTemplateAction;
QAction *mpGenNamesForPropertyAction;
QAction *mpGenNamesForSiblingsAction;
QAction *mpGenNamesForChildrenAction;
IProperty* mpMenuProperty;
QAction* mpShowNameValidityAction;
QAction* mpEditTemplateAction;
QAction* mpGenNamesForPropertyAction;
QAction* mpGenNamesForSiblingsAction;
QAction* mpGenNamesForChildrenAction;
public:
CPropertyView(QWidget *pParent = 0);
void setModel(QAbstractItemModel *pModel);
bool event(QEvent *pEvent);
void SetEditor(CWorldEditor *pEditor);
CPropertyView(QWidget* pParent = 0);
void setModel(QAbstractItemModel* pModel);
bool event(QEvent* pEvent);
int sizeHintForColumn(int Column) const;
void SetEditor(IEditor* pEditor);
void ClearProperties();
void SetIntrinsicProperties(CStructRef InProperties);
void SetInstance(CScriptObject *pObj);
void SetInstance(CScriptObject* pObj);
void UpdateEditorProperties(const QModelIndex& rkParent);
inline CPropertyModel* PropertyModel() const { return mpModel; }
@ -46,6 +48,10 @@ public slots:
void GenerateNamesForProperty();
void GenerateNamesForSiblings();
void GenerateNamesForChildren();
signals:
void PropertyModified(const QModelIndex& kIndex);
void PropertyModified(IProperty* pProperty);
};
#endif // CPROPERTYVIEW_H

View File

@ -8,7 +8,9 @@
#include "Editor/Undo/CMoveResourceCommand.h"
#include "Editor/Undo/CRenameDirectoryCommand.h"
#include "Editor/Undo/CRenameResourceCommand.h"
#include "Editor/Undo/CSaveStoreCommand.h"
#include "Editor/Undo/ICreateDeleteDirectoryCommand.h"
#include "Editor/Undo/ICreateDeleteResourceCommand.h"
#include <Core/GameProject/AssetNameGeneration.h>
#include <Core/GameProject/CAssetNameMap.h>
@ -28,6 +30,7 @@ CResourceBrowser::CResourceBrowser(QWidget *pParent)
, mEditorStore(false)
, mAssetListMode(false)
, mSearching(false)
, mpAddMenu(nullptr)
, mpInspectedEntry(nullptr)
{
mpUI->setupUi(this);
@ -144,13 +147,12 @@ CResourceBrowser::CResourceBrowser(QWidget *pParent)
// Create context menu for the resource table
new CResourceTableContextMenu(this, mpUI->ResourceTableView, mpModel, mpProxyModel);
// Set up connections
connect(mpUI->SearchBar, SIGNAL(StoppedTyping(QString)), this, SLOT(OnSearchStringChanged(QString)));
connect(mpUI->ResourceTreeButton, SIGNAL(pressed()), this, SLOT(SetResourceTreeView()));
connect(mpUI->ResourceListButton, SIGNAL(pressed()), this, SLOT(SetResourceListView()));
connect(mpUI->SortComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(OnSortModeChanged(int)));
connect(mpUI->NewFolderButton, SIGNAL(pressed()), this, SLOT(CreateDirectory()));
connect(mpUI->ClearButton, SIGNAL(pressed()), this, SLOT(OnClearButtonPressed()));
connect(mpUI->DirectoryTreeView, SIGNAL(clicked(QModelIndex)), this, SLOT(OnDirectorySelectionChanged(QModelIndex)));
@ -281,6 +283,50 @@ void CResourceBrowser::CreateFilterCheckboxes()
mpFilterBoxesLayout->addSpacerItem(pSpacer);
}
void CResourceBrowser::CreateAddMenu()
{
// Delete any existing menu
if (mpAddMenu)
{
delete mpAddMenu;
mpAddMenu = nullptr;
}
// Create new one, only if we have a valid resource store.
if (mpStore)
{
mpAddMenu = new QMenu(this);
mpAddMenu->addAction("New Folder", this, SLOT(CreateDirectory()));
mpAddMenu->addSeparator();
QMenu* pCreateMenu = new QMenu("Create...");
mpAddMenu->addMenu(pCreateMenu);
AddCreateAssetMenuActions(pCreateMenu);
mpUI->AddButton->setMenu(mpAddMenu);
}
mpUI->AddButton->setEnabled( mpAddMenu != nullptr );
}
void CResourceBrowser::AddCreateAssetMenuActions(QMenu* pMenu)
{
std::list<CResTypeInfo*> TypeInfos;
CResTypeInfo::GetAllTypesInGame(mpStore->Game(), TypeInfos);
for (auto Iter = TypeInfos.begin(); Iter != TypeInfos.end(); Iter++)
{
CResTypeInfo* pTypeInfo = *Iter;
if (pTypeInfo->CanBeCreated())
{
QString TypeName = TO_QSTRING( pTypeInfo->TypeName() );
QAction* pAction = pMenu->addAction(TypeName, this, SLOT(OnCreateAssetAction()));
pAction->setProperty("TypeInfo", QVariant((int) pTypeInfo->Type()));
}
}
}
bool CResourceBrowser::RenameResource(CResourceEntry *pEntry, const TString& rkNewName)
{
if (pEntry->Name() == rkNewName)
@ -302,7 +348,11 @@ bool CResourceBrowser::RenameResource(CResourceEntry *pEntry, const TString& rkN
}
// Everything seems to be valid; proceed with the rename
mUndoStack.beginMacro("Rename Resource");
mUndoStack.push( new CSaveStoreCommand(mpStore) );
mUndoStack.push( new CRenameResourceCommand(pEntry, rkNewName) );
mUndoStack.push( new CSaveStoreCommand(mpStore) );
mUndoStack.endMacro();
return true;
}
@ -325,7 +375,11 @@ bool CResourceBrowser::RenameDirectory(CVirtualDirectory *pDir, const TString& r
}
// No conflicts, proceed with the rename
mUndoStack.beginMacro("Rename Directory");
mUndoStack.push( new CSaveStoreCommand(mpStore) );
mUndoStack.push( new CRenameDirectoryCommand(pDir, rkNewName) );
mUndoStack.push( new CSaveStoreCommand(mpStore) );
mUndoStack.endMacro();
return true;
}
@ -387,6 +441,7 @@ bool CResourceBrowser::MoveResources(const QList<CResourceEntry*>& rkResources,
if (!ValidResources.isEmpty() || !ValidDirs.isEmpty())
{
mUndoStack.beginMacro("Move Resources");
mUndoStack.push( new CSaveStoreCommand(mpStore) );
foreach (CVirtualDirectory *pDir, ValidDirs)
mUndoStack.push( new CMoveDirectoryCommand(mpStore, pDir, pNewDir) );
@ -394,12 +449,71 @@ bool CResourceBrowser::MoveResources(const QList<CResourceEntry*>& rkResources,
foreach (CResourceEntry *pEntry, ValidResources)
mUndoStack.push( new CMoveResourceCommand(pEntry, pNewDir) );
mUndoStack.push( new CSaveStoreCommand(mpStore) );
mUndoStack.endMacro();
}
return true;
}
CResourceEntry* CResourceBrowser::CreateNewResource(EResourceType Type,
TString Name /*= ""*/,
CVirtualDirectory* pDir /*= nullptr*/,
CAssetID ID /*= CAssetID()*/)
{
if (!pDir)
{
pDir = mpSelectedDir;
}
// Create new asset ID. Sanity check to make sure the ID is unused.
while (!ID.IsValid() || mpStore->FindEntry(ID) != nullptr)
{
ID = CAssetID::RandomID( mpStore->Game() );
}
// Boring generic default name - user will immediately be prompted to change this
TString BaseName = Name;
if (BaseName.IsEmpty())
{
BaseName = TString::Format(
"New %s", *CResTypeInfo::FindTypeInfo(Type)->TypeName()
);
}
Name = BaseName;
int Num = 0;
while (pDir->FindChildResource(Name, Type) != nullptr)
{
Num++;
Name = TString::Format("%s (%d)", *BaseName, Num);
}
// Create the actual resource
CResourceEntry* pEntry = mpStore->CreateNewResource(ID, Type, pDir->FullPath(), Name);
// Push undo command
mUndoStack.beginMacro("Create Resource");
mUndoStack.push( new CSaveStoreCommand(mpStore) );
mUndoStack.push( new CCreateResourceCommand(pEntry) );
mUndoStack.push( new CSaveStoreCommand(mpStore) );
mUndoStack.endMacro();
pEntry->Save();
// Select new resource so user can enter a name
QModelIndex Index = mpModel->GetIndexForEntry(pEntry);
ASSERT(Index.isValid());
QModelIndex ProxyIndex = mpProxyModel->mapFromSource(Index);
mpUI->ResourceTableView->selectionModel()->select(ProxyIndex, QItemSelectionModel::ClearAndSelect);
mpUI->ResourceTableView->edit(ProxyIndex);
return pEntry;
}
bool CResourceBrowser::eventFilter(QObject *pWatched, QEvent *pEvent)
{
if (pWatched == mpUI->ResourceTableView)
@ -498,6 +612,23 @@ void CResourceBrowser::OnSortModeChanged(int Index)
mpProxyModel->SetSortMode(Mode);
}
void CResourceBrowser::OnCreateAssetAction()
{
// Attempt to retrieve the asset type from the sender. If successful, create the asset.
QAction* pSender = qobject_cast<QAction*>(sender());
if (pSender)
{
bool Ok;
EResourceType Type = (EResourceType) pSender->property("TypeInfo").toInt(&Ok);
if (Ok)
{
CreateNewResource(Type);
}
}
}
bool CResourceBrowser::CreateDirectory()
{
if (mpSelectedDir)
@ -513,8 +644,12 @@ bool CResourceBrowser::CreateDirectory()
}
// Push create command to actually create the directory
mUndoStack.beginMacro("Create Directory");
mUndoStack.push( new CSaveStoreCommand(mpStore) );
CCreateDirectoryCommand *pCmd = new CCreateDirectoryCommand(mpStore, mpSelectedDir->FullPath(), DirName);
mUndoStack.push(pCmd);
mUndoStack.push( new CSaveStoreCommand(mpStore) );
mUndoStack.endMacro();
// Now fetch the new directory and start editing it so the user can enter a name
CVirtualDirectory *pNewDir = mpSelectedDir->FindChildDirectory(DirName, false);
@ -537,28 +672,101 @@ bool CResourceBrowser::CreateDirectory()
return false;
}
bool CResourceBrowser::DeleteDirectories(const QList<CVirtualDirectory*>& rkDirs)
bool CResourceBrowser::Delete(QVector<CResourceEntry*> Resources, QVector<CVirtualDirectory*> Directories)
{
QList<CVirtualDirectory*> DeletableDirs;
// Don't delete any resources/directories that are still referenced.
// This is kind of a hack but there's no good way to clear out these references right now.
QString ErrorPaths;
foreach (CVirtualDirectory *pDir, rkDirs)
for (int DirIdx = 0; DirIdx < Directories.size(); DirIdx++)
{
if (pDir && pDir->IsEmpty(true))
DeletableDirs << pDir;
if (!Directories[DirIdx]->IsSafeToDelete())
{
ErrorPaths += TO_QSTRING( Directories[DirIdx]->FullPath() ) + '\n';
Directories.removeAt(DirIdx);
DirIdx--;
}
}
if (DeletableDirs.size() > 0)
for (int ResIdx = 0; ResIdx < Resources.size(); ResIdx++)
{
mUndoStack.beginMacro("Delete Directories");
if (Resources[ResIdx]->IsLoaded() && Resources[ResIdx]->Resource()->IsReferenced())
{
ErrorPaths += TO_QSTRING( Resources[ResIdx]->CookedAssetPath(true) ) + '\n';
Resources.removeAt(ResIdx);
ResIdx--;
}
}
foreach (CVirtualDirectory *pDir, DeletableDirs)
if (!ErrorPaths.isEmpty())
{
// Remove trailing newline
ErrorPaths.chop(1);
UICommon::ErrorMsg(this, QString("The following resources/directories are still referenced and cannot be deleted:\n\n%1")
.arg(ErrorPaths));
}
// Gather a complete list of resources in subdirectories
for (int DirIdx = 0; DirIdx < Directories.size(); DirIdx++)
{
CVirtualDirectory* pDir = Directories[DirIdx];
Resources.reserve( Resources.size() + pDir->NumResources() );
Directories.reserve( Directories.size() + pDir->NumSubdirectories() );
for (uint ResourceIdx = 0; ResourceIdx < pDir->NumResources(); ResourceIdx++)
Resources << pDir->ResourceByIndex(ResourceIdx);
for (uint SubdirIdx = 0; SubdirIdx < pDir->NumSubdirectories(); SubdirIdx++)
Directories << pDir->SubdirectoryByIndex(SubdirIdx);
}
// Exit if we have nothing to do.
if (Resources.isEmpty() && Directories.isEmpty())
return false;
// Allow the user to confirm before proceeding.
QString ConfirmMsg = QString("Are you sure you want to permanently delete ");
if (Resources.size() > 0)
{
ConfirmMsg += QString("%1 resource%2").arg(Resources.size()).arg(Resources.size() == 1 ? "" : "s");
if (Directories.size() > 0)
{
ConfirmMsg += " and ";
}
}
if (Directories.size() > 0)
{
ConfirmMsg += QString("%1 %2").arg(Directories.size()).arg(Directories.size() == 1 ? "directory" : "directories");
}
ConfirmMsg += "?";
if (UICommon::YesNoQuestion(this, "Warning", ConfirmMsg))
{
// Note that the undo stack will undo actions in the reverse order they are pushed
// So we need to push commands last that we want to be undone first
// We want to delete subdirectories first, then parent directories, then resources
mUndoStack.beginMacro("Delete");
mUndoStack.push( new CSaveStoreCommand(mpStore) );
// Delete resources first.
foreach (CResourceEntry* pEntry, Resources)
mUndoStack.push( new CDeleteResourceCommand(pEntry) );
// Now delete directories in reverse order (so subdirectories delete first)
for (int DirIdx = Directories.size()-1; DirIdx >= 0; DirIdx--)
{
CVirtualDirectory* pDir = Directories[DirIdx];
mUndoStack.push( new CDeleteDirectoryCommand(mpStore, pDir->Parent()->FullPath(), pDir->Name()) );
}
mUndoStack.push( new CSaveStoreCommand(mpStore) );
mUndoStack.endMacro();
return true;
}
else return false;
else
return false;
}
void CResourceBrowser::OnSearchStringChanged(QString SearchString)
@ -685,7 +893,8 @@ void CResourceBrowser::UpdateStore()
mpUI->SearchBar->clear();
mSearching = false;
// Refresh type filter list
// Refresh project-specific UI
CreateAddMenu();
CreateFilterCheckboxes();
// Refresh directory tree

Some files were not shown because too many files have changed in this diff Show More