Reimplemented save & repack button to work in the project system
This commit is contained in:
parent
1b97cd459a
commit
a7b0a2480c
|
@ -62,6 +62,10 @@ public:
|
|||
}
|
||||
|
||||
// We couldn't find a matching element, so we can't load this parameter.
|
||||
// If we pushed a parent earlier, undo it.
|
||||
if (!mJustEndedParam)
|
||||
mpCurElem = mpCurElem->Parent()->ToElement();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -276,7 +276,7 @@ void GenerateAssetNames(CGameProject *pProj)
|
|||
{
|
||||
TString Name = pInst->InstanceName();
|
||||
|
||||
if (Name.EndsWith(".SCAN", false))
|
||||
if (Name.StartsWith("POI_", false))
|
||||
{
|
||||
TIDString ScanIDString = (pProj->Game() <= ePrime ? "0x4:0x0" : "0xBDBEC295:0xB94E9BE7");
|
||||
TAssetProperty *pScanProperty = TPropCast<TAssetProperty>(pInst->PropertyByIDString(ScanIDString));
|
||||
|
@ -289,10 +289,10 @@ void GenerateAssetNames(CGameProject *pProj)
|
|||
|
||||
if (pEntry && !pEntry->IsNamed())
|
||||
{
|
||||
TWideString ScanName = Name.ToUTF16().ChopBack(5);
|
||||
TWideString ScanName = Name.ToUTF16().ChopFront(4);
|
||||
|
||||
if (ScanName.StartsWith(L"POI_"))
|
||||
ScanName = ScanName.ChopFront(4);
|
||||
if (ScanName.EndsWith(L".SCAN", false))
|
||||
ScanName = ScanName.ChopBack(5);
|
||||
|
||||
ApplyGeneratedName(pEntry, pEntry->DirectoryPath(), ScanName);
|
||||
|
||||
|
@ -419,7 +419,7 @@ void GenerateAssetNames(CGameProject *pProj)
|
|||
{
|
||||
CAudioGroup *pGroup = (CAudioGroup*) It->Load();
|
||||
TWideString GroupName = pGroup->GroupName().ToUTF16();
|
||||
ApplyGeneratedName(*It, L"AudioGrp\\", GroupName);
|
||||
ApplyGeneratedName(*It, L"Audio\\", GroupName);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
|
@ -34,6 +34,24 @@ bool CAssetNameMap::GetNameInfo(CAssetID ID, TString& rOutDirectory, TString& rO
|
|||
|
||||
void CAssetNameMap::CopyFromStore(CResourceStore *pStore /*= gpResourceStore*/)
|
||||
{
|
||||
// Do a first pass to remove old paths from used set to prevent false positives from eg. if two resources switch places
|
||||
for (CResourceIterator It(pStore); It; ++It)
|
||||
{
|
||||
if (It->IsCategorized() || It->IsNamed())
|
||||
{
|
||||
auto Find = mMap.find(It->ID());
|
||||
|
||||
if (Find != mMap.end())
|
||||
{
|
||||
SAssetNameInfo& rInfo = Find->second;
|
||||
auto UsedFind = mUsedSet.find(rInfo);
|
||||
ASSERT(UsedFind != mUsedSet.end());
|
||||
mUsedSet.erase(UsedFind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do a second pass to add the new paths to the map
|
||||
for (CResourceIterator It(pStore); It; ++It)
|
||||
{
|
||||
if (It->IsCategorized() || It->IsNamed())
|
||||
|
@ -44,17 +62,6 @@ void CAssetNameMap::CopyFromStore(CResourceStore *pStore /*= gpResourceStore*/)
|
|||
CFourCC Type = It->CookedExtension();
|
||||
SAssetNameInfo NameInfo { Name, Directory, Type };
|
||||
|
||||
// Remove old path from used set
|
||||
auto Find = mMap.find(ID);
|
||||
|
||||
if (Find != mMap.end())
|
||||
{
|
||||
SAssetNameInfo& rInfo = Find->second;
|
||||
auto UsedFind = mUsedSet.find(rInfo);
|
||||
ASSERT(UsedFind != mUsedSet.end());
|
||||
mUsedSet.erase(UsedFind);
|
||||
}
|
||||
|
||||
// Check for conflicts with new name
|
||||
if (mUsedSet.find(NameInfo) != mUsedSet.end())
|
||||
{
|
||||
|
|
|
@ -57,6 +57,12 @@ void CGameProject::Serialize(IArchive& rArc)
|
|||
|
||||
if (rArc.IsReader())
|
||||
{
|
||||
// Load resource store
|
||||
ASSERT(mpResourceStore == nullptr);
|
||||
mpResourceStore = new CResourceStore(this);
|
||||
mpResourceStore->LoadResourceDatabase();
|
||||
|
||||
// Load packages
|
||||
for (u32 iPkg = 0; iPkg < mPackages.size(); iPkg++)
|
||||
delete mPackages[iPkg];
|
||||
mPackages.clear();
|
||||
|
@ -169,8 +175,6 @@ CGameProject* CGameProject::LoadProject(const TWideString& rkProjPath)
|
|||
pProj->Serialize(Reader);
|
||||
CTemplateLoader::LoadGameTemplates(pProj->mGame);
|
||||
|
||||
pProj->mpResourceStore = new CResourceStore(pProj);
|
||||
pProj->mpResourceStore->LoadResourceDatabase();
|
||||
pProj->mpGameInfo->LoadGameInfo(pProj->mGame);
|
||||
pProj->mpAudioManager->LoadAssets();
|
||||
return pProj;
|
||||
|
|
|
@ -15,6 +15,7 @@ void CPackage::Load()
|
|||
TWideString DefPath = DefinitionPath(false);
|
||||
CXMLReader Reader(DefPath.ToUTF8());
|
||||
Serialize(Reader);
|
||||
UpdateDependencyCache();
|
||||
}
|
||||
|
||||
void CPackage::Save()
|
||||
|
@ -28,7 +29,18 @@ void CPackage::Save()
|
|||
|
||||
void CPackage::Serialize(IArchive& rArc)
|
||||
{
|
||||
rArc << SERIAL_CONTAINER("Collections", mCollections, "ResourceCollection");
|
||||
rArc << SERIAL("NeedsRecook", mNeedsRecook)
|
||||
<< SERIAL_CONTAINER("Collections", mCollections, "ResourceCollection");
|
||||
}
|
||||
|
||||
void CPackage::UpdateDependencyCache()
|
||||
{
|
||||
CPackageDependencyListBuilder Builder(this);
|
||||
std::list<CAssetID> AssetList;
|
||||
Builder.BuildDependencyList(false, AssetList);
|
||||
|
||||
for (auto Iter = AssetList.begin(); Iter != AssetList.end(); Iter++)
|
||||
mCachedDependencies.insert(*Iter);
|
||||
}
|
||||
|
||||
void CPackage::Cook()
|
||||
|
@ -74,18 +86,6 @@ void CPackage::Cook()
|
|||
pkRes->ID.Write(Pak);
|
||||
Pak.WriteLong(pkRes->Name.Size());
|
||||
Pak.WriteString(pkRes->Name.ToStdString(), pkRes->Name.Size()); // Note: Explicitly specifying size means we don't write the terminating 0
|
||||
|
||||
// TEMP: recook world
|
||||
if (pkRes->Type == "MLVL")
|
||||
{
|
||||
CResourceEntry *pEntry = gpResourceStore->FindEntry(pkRes->ID);
|
||||
ASSERT(pEntry);
|
||||
CWorld *pWorld = (CWorld*) pEntry->Load();
|
||||
ASSERT(pWorld);
|
||||
CFileOutStream MLVL(pEntry->CookedAssetPath().ToStdString(), IOUtil::eBigEndian);
|
||||
ASSERT(MLVL.IsValid());
|
||||
CWorldCooker::CookMLVL(pWorld, MLVL);
|
||||
}
|
||||
}
|
||||
|
||||
// Fill in table of contents with junk, write later
|
||||
|
@ -114,10 +114,14 @@ void CPackage::Cook()
|
|||
|
||||
for (auto Iter = AssetList.begin(); Iter != AssetList.end(); Iter++, ResIdx++)
|
||||
{
|
||||
// Initialize entry, recook assets if needed
|
||||
CAssetID ID = *Iter;
|
||||
CResourceEntry *pEntry = gpResourceStore->FindEntry(ID);
|
||||
ASSERT(pEntry != nullptr);
|
||||
|
||||
if (pEntry->NeedsRecook())
|
||||
pEntry->Cook();
|
||||
|
||||
SResourceTocInfo& rTocInfo = ResourceTocData[ResIdx];
|
||||
rTocInfo.pEntry = pEntry;
|
||||
rTocInfo.Offset = Pak.Tell();
|
||||
|
@ -200,6 +204,8 @@ void CPackage::Cook()
|
|||
Pak.WriteLong(rkTocInfo.Offset);
|
||||
}
|
||||
|
||||
mNeedsRecook = false;
|
||||
Save();
|
||||
Log::Write("Finished writing " + PakPath.ToUTF8());
|
||||
}
|
||||
|
||||
|
|
|
@ -50,6 +50,8 @@ class CPackage
|
|||
TString mPakName;
|
||||
TWideString mPakPath;
|
||||
std::vector<CResourceCollection*> mCollections;
|
||||
std::set<CAssetID> mCachedDependencies;
|
||||
bool mNeedsRecook;
|
||||
|
||||
enum EPackageDefinitionVersion
|
||||
{
|
||||
|
@ -66,11 +68,13 @@ public:
|
|||
: mpProject(pProj)
|
||||
, mPakName(rkName)
|
||||
, mPakPath(rkPath)
|
||||
, mNeedsRecook(false)
|
||||
{}
|
||||
|
||||
void Load();
|
||||
void Save();
|
||||
void Serialize(IArchive& rArc);
|
||||
void UpdateDependencyCache();
|
||||
|
||||
void Cook();
|
||||
void CompareOriginalAssetList(const std::list<CAssetID>& rkNewList);
|
||||
|
@ -88,8 +92,11 @@ public:
|
|||
inline CGameProject* Project() const { return mpProject; }
|
||||
inline u32 NumCollections() const { return mCollections.size(); }
|
||||
inline CResourceCollection* CollectionByIndex(u32 Idx) const { return mCollections[Idx]; }
|
||||
inline bool ContainsAsset(const CAssetID& rkID) const { return mCachedDependencies.find(rkID) != mCachedDependencies.end(); }
|
||||
inline bool NeedsRecook() const { return mNeedsRecook; }
|
||||
|
||||
inline void SetPakName(TString NewName) { mPakName = NewName; }
|
||||
inline void MarkDirty() { mNeedsRecook = true; Save(); UpdateDependencyCache(); }
|
||||
};
|
||||
|
||||
#endif // CPACKAGE
|
||||
|
|
|
@ -213,13 +213,24 @@ bool CResourceEntry::Save(bool SkipCacheSave /*= false*/)
|
|||
}
|
||||
}
|
||||
|
||||
// Resource has been saved, now update dependencies + cache file
|
||||
// Resource has been saved; now make sure dependencies, cache data, and packages are all up to date
|
||||
UpdateDependencies();
|
||||
mpStore->SetCacheDataDirty();
|
||||
|
||||
if (!SkipCacheSave)
|
||||
{
|
||||
mpStore->ConditionalSaveStore();
|
||||
|
||||
// Flag dirty any packages that contain this resource.
|
||||
for (u32 iPkg = 0; iPkg < mpStore->Project()->NumPackages(); iPkg++)
|
||||
{
|
||||
CPackage *pPkg = mpStore->Project()->PackageByIndex(iPkg);
|
||||
|
||||
if (pPkg->ContainsAsset(ID()))
|
||||
pPkg->MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (ShouldCollectGarbage)
|
||||
mpStore->DestroyUnreferencedResources();
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ void CCharacterUsageMap::FindUsagesForArea(CWorld *pWorld, u32 AreaIndex)
|
|||
mCurrentAreaAllowsDupes = pWorld->DoesAreaAllowPakDuplicates(iArea);
|
||||
|
||||
CAssetID AreaID = pWorld->AreaResourceID(iArea);
|
||||
CResourceEntry *pEntry = gpResourceStore->FindEntry(AreaID);
|
||||
CResourceEntry *pEntry = mpStore->FindEntry(AreaID);
|
||||
ASSERT(pEntry && pEntry->ResourceType() == eArea);
|
||||
|
||||
ParseDependencyNode(pEntry->Dependencies());
|
||||
|
@ -98,7 +98,7 @@ void CCharacterUsageMap::DebugPrintContents()
|
|||
{
|
||||
CAssetID ID = Iter->first;
|
||||
std::vector<bool>& rUsedList = Iter->second;
|
||||
CAnimSet *pSet = (CAnimSet*) gpResourceStore->LoadResource(ID, "ANCS");
|
||||
CAnimSet *pSet = (CAnimSet*) mpStore->LoadResource(ID, "ANCS");
|
||||
|
||||
for (u32 iChar = 0; iChar < pSet->NumCharacters(); iChar++)
|
||||
{
|
||||
|
@ -152,7 +152,7 @@ void CCharacterUsageMap::ParseDependencyNode(IDependencyNode *pNode)
|
|||
else if (Type == eDNT_ResourceDependency || Type == eDNT_ScriptProperty)
|
||||
{
|
||||
CResourceDependency *pDep = static_cast<CResourceDependency*>(pNode);
|
||||
CResourceEntry *pEntry = gpResourceStore->FindEntry(pDep->ID());
|
||||
CResourceEntry *pEntry = mpStore->FindEntry(pDep->ID());
|
||||
|
||||
if (pEntry)
|
||||
{
|
||||
|
@ -184,7 +184,7 @@ void CPackageDependencyListBuilder::BuildDependencyList(bool AllowDuplicates, st
|
|||
for (u32 iRes = 0; iRes < pCollection->NumResources(); iRes++)
|
||||
{
|
||||
const SNamedResource& rkRes = pCollection->ResourceByIndex(iRes);
|
||||
CResourceEntry *pEntry = gpResourceStore->FindEntry(rkRes.ID);
|
||||
CResourceEntry *pEntry = mpStore->FindEntry(rkRes.ID);
|
||||
if (!pEntry) continue;
|
||||
|
||||
if (rkRes.Name.EndsWith("NODEPEND") || rkRes.Type == "CSNG")
|
||||
|
@ -211,7 +211,7 @@ void CPackageDependencyListBuilder::BuildDependencyList(bool AllowDuplicates, st
|
|||
void CPackageDependencyListBuilder::AddDependency(CResourceEntry *pCurEntry, const CAssetID& rkID, std::list<CAssetID>& rOut)
|
||||
{
|
||||
if (pCurEntry && pCurEntry->ResourceType() == eDependencyGroup) return;
|
||||
CResourceEntry *pEntry = gpResourceStore->FindEntry(rkID);
|
||||
CResourceEntry *pEntry = mpStore->FindEntry(rkID);
|
||||
if (!pEntry) return;
|
||||
|
||||
EResType ResType = pEntry->ResourceType();
|
||||
|
@ -395,7 +395,7 @@ void CAreaDependencyListBuilder::BuildDependencyList(std::list<CAssetID>& rAsset
|
|||
|
||||
void CAreaDependencyListBuilder::AddDependency(const CAssetID& rkID, std::list<CAssetID>& rOut, std::set<CAssetID> *pAudioGroupsOut)
|
||||
{
|
||||
CResourceEntry *pEntry = gpResourceStore->FindEntry(rkID);
|
||||
CResourceEntry *pEntry = mpStore->FindEntry(rkID);
|
||||
if (!pEntry) return;
|
||||
|
||||
EResType ResType = pEntry->ResourceType();
|
||||
|
|
|
@ -12,12 +12,16 @@ class CCharacterUsageMap
|
|||
{
|
||||
std::map<CAssetID, std::vector<bool>> mUsageMap;
|
||||
std::set<CAssetID> mStillLookingIDs;
|
||||
CResourceStore *mpStore;
|
||||
u32 mLayerIndex;
|
||||
bool mIsInitialArea;
|
||||
bool mCurrentAreaAllowsDupes;
|
||||
|
||||
public:
|
||||
CCharacterUsageMap() : mLayerIndex(-1), mIsInitialArea(true), mCurrentAreaAllowsDupes(false) {}
|
||||
CCharacterUsageMap(CResourceStore *pStore)
|
||||
: mpStore(pStore), mLayerIndex(-1), mIsInitialArea(true), mCurrentAreaAllowsDupes(false)
|
||||
{}
|
||||
|
||||
bool IsCharacterUsed(const CAssetID& rkID, u32 CharacterIndex) const;
|
||||
bool IsAnimationUsed(const CAssetID& rkID, CSetAnimationDependency *pAnim) const;
|
||||
void FindUsagesForAsset(CResourceEntry *pEntry);
|
||||
|
@ -35,6 +39,7 @@ protected:
|
|||
class CPackageDependencyListBuilder
|
||||
{
|
||||
CPackage *mpPackage;
|
||||
CResourceStore *mpStore;
|
||||
EGame mGame;
|
||||
TResPtr<CWorld> mpWorld;
|
||||
CAssetID mCurrentAnimSetID;
|
||||
|
@ -49,9 +54,12 @@ public:
|
|||
CPackageDependencyListBuilder(CPackage *pPackage)
|
||||
: mpPackage(pPackage)
|
||||
, mGame(pPackage->Project()->Game())
|
||||
, mpStore(pPackage->Project()->ResourceStore())
|
||||
, mCharacterUsageMap(pPackage->Project()->ResourceStore())
|
||||
, mCurrentAreaHasDuplicates(false)
|
||||
, mIsPlayerActor(false)
|
||||
{}
|
||||
{
|
||||
}
|
||||
|
||||
void BuildDependencyList(bool AllowDuplicates, std::list<CAssetID>& rOut);
|
||||
void AddDependency(CResourceEntry *pCurEntry, const CAssetID& rkID, std::list<CAssetID>& rOut);
|
||||
|
@ -62,6 +70,7 @@ public:
|
|||
class CAreaDependencyListBuilder
|
||||
{
|
||||
CResourceEntry *mpAreaEntry;
|
||||
CResourceStore *mpStore;
|
||||
EGame mGame;
|
||||
CAssetID mCurrentAnimSetID;
|
||||
CCharacterUsageMap mCharacterUsageMap;
|
||||
|
@ -72,7 +81,9 @@ class CAreaDependencyListBuilder
|
|||
public:
|
||||
CAreaDependencyListBuilder(CResourceEntry *pAreaEntry)
|
||||
: mpAreaEntry(pAreaEntry)
|
||||
, mpStore(pAreaEntry->ResourceStore())
|
||||
, mGame(pAreaEntry->Game())
|
||||
, mCharacterUsageMap(pAreaEntry->ResourceStore())
|
||||
{
|
||||
ASSERT(mpAreaEntry->ResourceType() == eArea);
|
||||
}
|
||||
|
|
|
@ -84,6 +84,26 @@ void CEditorApplication::EditResource(CResourceEntry *pEntry)
|
|||
}
|
||||
}
|
||||
|
||||
void CEditorApplication::NotifyAssetsModified()
|
||||
{
|
||||
emit AssetsModified();
|
||||
}
|
||||
|
||||
void CEditorApplication::CookAllDirtyPackages()
|
||||
{
|
||||
CGameProject *pProj = CGameProject::ActiveProject();
|
||||
|
||||
for (u32 iPkg = 0; iPkg < pProj->NumPackages(); iPkg++)
|
||||
{
|
||||
CPackage *pPackage = pProj->PackageByIndex(iPkg);
|
||||
|
||||
if (pPackage->NeedsRecook())
|
||||
pPackage->Cook();
|
||||
}
|
||||
|
||||
mpProjectDialog->SetupPackagesList();
|
||||
}
|
||||
|
||||
void CEditorApplication::AddEditor(IEditor *pEditor)
|
||||
{
|
||||
mEditorWindows << pEditor;
|
||||
|
|
|
@ -31,6 +31,8 @@ public:
|
|||
~CEditorApplication();
|
||||
void InitEditor();
|
||||
void EditResource(CResourceEntry *pEntry);
|
||||
void NotifyAssetsModified();
|
||||
void CookAllDirtyPackages();
|
||||
|
||||
// Accessors
|
||||
inline CWorldEditor* WorldEditor() const { return mpWorldEditor; }
|
||||
|
@ -44,6 +46,9 @@ public slots:
|
|||
void AddEditor(IEditor *pEditor);
|
||||
void TickEditors();
|
||||
void OnEditorClose();
|
||||
|
||||
signals:
|
||||
void AssetsModified();
|
||||
};
|
||||
|
||||
#define gpEdApp static_cast<CEditorApplication*>(qApp)
|
||||
|
|
|
@ -22,6 +22,8 @@ CProjectOverviewDialog::CProjectOverviewDialog(QWidget *pParent)
|
|||
connect(mpUI->LaunchEditorButton, SIGNAL(clicked()), this, SLOT(LaunchEditor()));
|
||||
connect(mpUI->ViewResourcesButton, SIGNAL(clicked()), this, SLOT(LaunchResourceBrowser()));
|
||||
connect(mpUI->CookPackageButton, SIGNAL(clicked()), this, SLOT(CookPackage()));
|
||||
|
||||
connect(gpEdApp, SIGNAL(AssetsModified()), this, SLOT(SetupPackagesList()));
|
||||
}
|
||||
|
||||
CProjectOverviewDialog::~CProjectOverviewDialog()
|
||||
|
@ -113,7 +115,10 @@ void CProjectOverviewDialog::SetupPackagesList()
|
|||
{
|
||||
CPackage *pPackage = mpProject->PackageByIndex(iPkg);
|
||||
ASSERT(pPackage != nullptr);
|
||||
mpUI->PackagesList->addItem(TO_QSTRING(pPackage->Name()));
|
||||
|
||||
QString PackageName = TO_QSTRING(pPackage->Name());
|
||||
if (pPackage->NeedsRecook()) PackageName += '*';
|
||||
mpUI->PackagesList->addItem(PackageName);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
#include "Editor/Widgets/WVectorEditor.h"
|
||||
#include "Editor/Undo/UndoCommands.h"
|
||||
|
||||
#include <Core/GameProject/CGameProject.h>
|
||||
#include <Core/Render/CDrawUtil.h>
|
||||
#include <Core/Scene/CSceneIterator.h>
|
||||
#include <Common/Log.h>
|
||||
|
@ -360,6 +361,9 @@ bool CWorldEditor::Save()
|
|||
bool SaveEGMCSuccess = mpArea->PoiToWorldMap() ? mpArea->PoiToWorldMap()->Entry()->Save() : true;
|
||||
bool SaveWorldSuccess = mpWorld->Entry()->Save();
|
||||
|
||||
if (SaveAreaSuccess || SaveEGMCSuccess || SaveWorldSuccess)
|
||||
gpEdApp->NotifyAssetsModified();
|
||||
|
||||
if (SaveAreaSuccess && SaveEGMCSuccess && SaveWorldSuccess)
|
||||
{
|
||||
mUndoStack.setClean();
|
||||
|
@ -376,32 +380,8 @@ bool CWorldEditor::Save()
|
|||
bool CWorldEditor::SaveAndRepack()
|
||||
{
|
||||
if (!Save()) return false;
|
||||
|
||||
if (!CanRepack())
|
||||
{
|
||||
CRepackInfoDialog Dialog(mWorldDir, mPakFileList, mPakTarget, this);
|
||||
Dialog.exec();
|
||||
|
||||
if (Dialog.result() == QDialog::Accepted)
|
||||
{
|
||||
SetWorldDir(Dialog.TargetFolder());
|
||||
SetPakFileList(Dialog.ListFile());
|
||||
SetPakTarget(Dialog.OutputPak());
|
||||
if (!CanRepack()) return false;
|
||||
}
|
||||
|
||||
else return false;
|
||||
}
|
||||
|
||||
QString PakOut;
|
||||
CPakToolDialog::EResult Result = CPakToolDialog::Repack(CurrentGame(), mPakTarget, mPakFileList, mWorldDir, &PakOut, this);
|
||||
|
||||
if (Result == CPakToolDialog::eError)
|
||||
QMessageBox::warning(this, "Error", "Failed to repack!");
|
||||
else if (Result == CPakToolDialog::eSuccess)
|
||||
QMessageBox::information(this, "Success", "Successfully saved pak: " + PakOut);
|
||||
|
||||
return (Result == CPakToolDialog::eSuccess);
|
||||
gpEdApp->CookAllDirtyPackages();
|
||||
return true;
|
||||
}
|
||||
|
||||
void CWorldEditor::OnLinksModified(const QList<CScriptObject*>& rkInstances)
|
||||
|
|
Loading…
Reference in New Issue