PrimeWorldEditor/src/Core/Resource/Script/Property/IProperty.cpp

682 lines
24 KiB
C++

#include "IProperty.h"
#include "CAssetProperty.h"
#include "CArrayProperty.h"
#include "CEnumProperty.h"
#include "CFlagsProperty.h"
#include "CPointerProperty.h"
#include "Core/Resource/Script/CGameTemplate.h"
#include "Core/Resource/Script/CScriptTemplate.h"
#include "Core/Resource/Script/NGameList.h"
#include "Core/Resource/Script/NPropertyMap.h"
/** IProperty */
IProperty::IProperty(EGame Game)
: mpParent( nullptr )
, mpPointerParent( nullptr )
, mpArchetype( nullptr )
, mGame( Game )
, mpScriptTemplate( nullptr )
, mOffset( -1 )
, mID( -1 )
, mCookPreference( ECookPreference::Default )
, mMinVersion( 0.0f )
, mMaxVersion( FLT_MAX )
{}
void IProperty::_ClearChildren()
{
for (int ChildIdx = 0; ChildIdx < mChildren.size(); ChildIdx++)
{
// Unregister children from the name map. This has to be done before actually deleting them.
if (mChildren[ChildIdx]->UsesNameMap())
{
NPropertyMap::UnregisterProperty(mChildren[ChildIdx]);
}
delete mChildren[ChildIdx];
}
mChildren.clear();
}
IProperty::~IProperty()
{
// Remove from archetype
if( mpArchetype != nullptr )
{
// If you crash here, it most likely means this property was not added to the archetype's sub-instances list.
NBasics::VectorRemoveOne(mpArchetype->mSubInstances, this);
}
// If this is an archetype, make sure no sub-instances have a reference to us.
if( IsArchetype() )
{
for( int SubIdx = 0; SubIdx < mSubInstances.size(); SubIdx++ )
{
mSubInstances[SubIdx]->mpArchetype = nullptr;
}
}
// Delete children
_ClearChildren();
}
const char* IProperty::HashableTypeName() const
{
return PropEnumToHashableTypeName( Type() );
}
void* IProperty::GetChildDataPointer(void* pPropertyData) const
{
return pPropertyData;
}
void IProperty::Serialize(IArchive& rArc)
{
// Always serialize ID first! ID is always required (except for root properties, which have an ID of 0xFFFFFFFF)
// because they are needed to look up the correct property to apply parameter overrides to.
rArc << SerialParameter("ID", mID, SH_HexDisplay | SH_Attribute | SH_Optional, (uint32) 0xFFFFFFFF);
// Now we can serialize the archetype reference and initialize if needed
if ( ((mpArchetype && mpArchetype->IsRootParent()) || rArc.IsReader()) && rArc.CanSkipParameters() )
{
TString ArchetypeName = (mpArchetype ? mpArchetype->Name() : "");
rArc << SerialParameter("Archetype", ArchetypeName, SH_Attribute);
if (rArc.IsReader() && !ArchetypeName.IsEmpty())
{
CGameTemplate* pGame = NGameList::GetGameTemplate( Game() );
IProperty* pArchetype = pGame->FindPropertyArchetype(ArchetypeName);
// The archetype must exist, or else the template file is malformed.
ASSERT(pArchetype != nullptr);
ASSERT(pArchetype->Type() == Type());
InitFromArchetype(pArchetype);
}
}
// In MP1, the game data does not use property IDs, so we serialize the name directly.
// In MP2 and on, property names are looked up based on the property ID via the property name map.
// Exceptions: Properties that are not in the name map still need to serialize their names.
// This includes root-level properties, and properties of atomic structs.
//
// We can't currently tell if this property is atomic, as the flag hasn't been serialized and the parent
// hasn't been set, but atomic sub-properties don't use hash IDs, so we can do a pseudo-check against the ID.
if (rArc.Game() <= EGame::Prime || IsRootParent() || IsArrayArchetype() || mID <= 0xFF)
{
rArc << SerialParameter("Name", mName, mpArchetype ? SH_Optional : 0, mpArchetype ? mpArchetype->mName : "");
}
rArc << SerialParameter("Description", mDescription, SH_Optional, mpArchetype ? mpArchetype->mDescription : "")
<< SerialParameter("CookPreference", mCookPreference, SH_Optional, mpArchetype ? mpArchetype->mCookPreference : ECookPreference::Default)
<< SerialParameter("MinVersion", mMinVersion, SH_Optional, mpArchetype ? mpArchetype->mMinVersion : 0.f)
<< SerialParameter("MaxVersion", mMaxVersion, SH_Optional, mpArchetype ? mpArchetype->mMaxVersion : FLT_MAX)
<< SerialParameter("Suffix", mSuffix, SH_Optional, mpArchetype ? mpArchetype->mSuffix : "");
// Children don't get serialized for most property types
}
void IProperty::InitFromArchetype(IProperty* pOther)
{
//@todo maybe somehow use Serialize for this instead?
mpArchetype = pOther;
mpArchetype->mSubInstances.push_back(this);
mFlags = pOther->mFlags & EPropertyFlag::ArchetypeCopyFlags;
mName = pOther->mName;
mDescription = pOther->mDescription;
mSuffix = pOther->mSuffix;
mCookPreference = pOther->mCookPreference;
mMinVersion = pOther->mMinVersion;
mMaxVersion = pOther->mMaxVersion;
// Copy ID only if our existing ID is not valid.
if (mID == 0xFFFFFFFF)
{
mID = pOther->mID;
}
}
bool IProperty::ShouldSerialize() const
{
return mpArchetype == nullptr ||
mName != mpArchetype->mName ||
mDescription != mpArchetype->mDescription ||
mSuffix != mpArchetype->mSuffix ||
mCookPreference != mpArchetype->mCookPreference ||
mMinVersion != mpArchetype->mMinVersion ||
mMaxVersion != mpArchetype->mMaxVersion;
}
void IProperty::Initialize(IProperty* pInParent, CScriptTemplate* pInTemplate, uint32 InOffset)
{
// Make sure we only get initialized once.
ASSERT( (mFlags & EPropertyFlag::IsInitialized) == 0 );
mpParent = pInParent;
mOffset = InOffset;
mpScriptTemplate = pInTemplate;
// Set any fields dependent on the parent...
if (mpParent)
{
mFlags |= mpParent->mFlags & EPropertyFlag::InheritableFlags;
if (mpParent->IsPointerType())
{
mpPointerParent = mpParent;
}
else
{
mpPointerParent = mpParent->mpPointerParent;
}
if (mpParent->Type() == EPropertyType::Array)
{
mFlags |= EPropertyFlag::IsArrayArchetype;
}
// MP1 has some weirdness we need to account for, most likely due to incorrect templates
// The templates we have right now have non-atomic structs inside atomic structs...
if (Game() <= EGame::Prime && mpParent->IsAtomic() && mpArchetype && !mpArchetype->IsAtomic())
{
mFlags.ClearFlag(EPropertyFlag::IsAtomic);
}
}
else if (!mpScriptTemplate)
{
mFlags |= EPropertyFlag::IsArchetype;
}
// Register the property if needed.
if (UsesNameMap())
{
NPropertyMap::RegisterProperty(this);
}
// Allow subclasses to handle any initialization tasks
PostInitialize();
// Now, route initialization to any child properties...
uint32 ChildOffset = mOffset;
for (int ChildIdx = 0; ChildIdx < mChildren.size(); ChildIdx++)
{
IProperty* pChild = mChildren[ChildIdx];
// update offset and round up to the child's alignment
if (ChildIdx > 0)
{
ChildOffset += mChildren[ChildIdx-1]->DataSize();
}
ChildOffset = ALIGN(ChildOffset, pChild->DataAlignment());
// Don't call Initialize on intrinsic children as they have already been initialized.
if (!pChild->IsIntrinsic())
{
pChild->Initialize(this, pInTemplate, ChildOffset);
}
}
mFlags |= EPropertyFlag::IsInitialized;
}
void* IProperty::RawValuePtr(void* pData) const
{
// For array archetypes, the caller needs to provide the pointer to the correct array item
// Array archetypes can't store their index in the array so it's impossible to determine the correct pointer.
void* pBasePtr = (mpPointerParent && !IsArrayArchetype() ? mpPointerParent->GetChildDataPointer(pData) : pData);
void* pValuePtr = ((char*)pBasePtr + mOffset);
return pValuePtr;
}
IProperty* IProperty::ChildByID(uint32 ID) const
{
for (uint32 ChildIdx = 0; ChildIdx < mChildren.size(); ChildIdx++)
{
if (mChildren[ChildIdx]->mID == ID)
return mChildren[ChildIdx];
}
return nullptr;
}
IProperty* IProperty::ChildByIDString(const TIDString& rkIdString)
{
// String must contain at least one ID!
// some ID strings are formatted with 8 characters and some with 2 (plus the beginning "0x")
ASSERT(rkIdString.Size() >= 4);
uint32 IDEndPos = rkIdString.IndexOf(':');
uint32 NextChildID = -1;
if (IDEndPos == -1)
NextChildID = rkIdString.ToInt32(16);
else
NextChildID = rkIdString.SubString(2, IDEndPos - 2).ToInt32(16);
if (NextChildID == 0xFFFFFFFF)
{
return nullptr;
}
IProperty* pNextChild = ChildByID(NextChildID);
// Check if we need to recurse
if (IDEndPos != -1)
{
return pNextChild->ChildByIDString(rkIdString.ChopFront(IDEndPos + 1));
}
else
{
return pNextChild;
}
}
void IProperty::GatherAllSubInstances(std::list<IProperty*>& OutList, bool Recursive)
{
OutList.push_back(this);
for( uint32 SubIdx = 0; SubIdx < mSubInstances.size(); SubIdx++ )
{
IProperty* pSubInstance = mSubInstances[SubIdx];
if( Recursive )
pSubInstance->GatherAllSubInstances( OutList, true );
else
OutList.push_back( pSubInstance );
}
}
TString IProperty::GetTemplateFileName()
{
// We want to return the path to the XML file that this property originally belongs to.
// So, for example, if this is a property of a script template, we want to return that script template.
// However, if this property was copied from a property archetype... If we are a direct instance of an
// archetype property (for instance a DamageInfo struct instance), then we want to return the template
// that contains the instance. However, if we are a sub-property of an archetype, then we want to return
// the path to that archetype instead. Hopefully that makes sense!
IProperty* pTemplateRoot = this;
// If our archetype has a parent, then our archetype is a sub-property of the main archetype, and we
// need to go deeper to find the original source XML file.
//
// If our archetype doesn't have a parent, then we are an instance of the main archetype, and we stop here.
while (pTemplateRoot->Archetype() && pTemplateRoot->Archetype()->Parent())
{
pTemplateRoot = pTemplateRoot->Archetype();
}
pTemplateRoot = pTemplateRoot->RootParent();
// Now that we have the base property of our template, we can return the file path.
static const uint32 kChopAmount = strlen("../templates/");
if (pTemplateRoot->ScriptTemplate())
{
return pTemplateRoot->ScriptTemplate()->SourceFile().ChopFront(kChopAmount);
}
else
{
CGameTemplate* pGameTemplate = NGameList::GetGameTemplate(Game());
return pGameTemplate->GetPropertyArchetypeFilePath( pTemplateRoot->Name() ).ChopFront(kChopAmount);
}
}
bool IProperty::ShouldCook(void* pPropertyData) const
{
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;
case ECookPreference::Never:
return false;
case ECookPreference::OnlyIfModified:
return !MatchesDefault(pPropertyData);
default:
// Unhandled case
ASSERT(false);
return true;
}
}
void IProperty::SetName(const TString& rkNewName)
{
if (mName != rkNewName)
{
mName = rkNewName;
mFlags.ClearFlag(EPropertyFlag::HasCachedNameCheck);
MarkDirty();
}
}
void IProperty::SetDescription(const TString& rkNewDescription)
{
if (mDescription != rkNewDescription)
{
mDescription = rkNewDescription;
MarkDirty();
}
}
void IProperty::SetSuffix(const TString& rkNewSuffix)
{
if (mSuffix != rkNewSuffix)
{
mSuffix = rkNewSuffix;
MarkDirty();
}
}
void IProperty::MarkDirty()
{
// Don't allow properties to be marked dirty before they are fully initialized.
if (IsInitialized())
{
// Mark the root parent as dirty so the template file will get resaved
RootParent()->mFlags |= EPropertyFlag::IsDirty;
// Clear property name cache in case something has been modified that affects the hash
mFlags &= ~(EPropertyFlag::HasCachedNameCheck | EPropertyFlag::HasCorrectPropertyName);
// Mark sub-instances as dirty since they may need to resave as well
for (uint32 SubIdx = 0; SubIdx < mSubInstances.size(); SubIdx++)
{
mSubInstances[SubIdx]->MarkDirty();
}
}
}
void IProperty::ClearDirtyFlag()
{
RootParent()->mFlags &= ~EPropertyFlag::IsDirty;
}
bool IProperty::ConvertType(EPropertyType NewType, IProperty* pNewArchetype /*= nullptr*/)
{
if (mpArchetype && !pNewArchetype)
{
// We need to start from the root archetype and cascade down sub-instances.
// The archetype will re-call this function with a valid pNewArchetype pointer.
return mpArchetype->ConvertType(NewType, nullptr);
}
IProperty* pNewProperty = Create(NewType, Game());
// We can only replace properties with types that have the same size and alignment
if( pNewProperty->DataSize() != DataSize() || pNewProperty->DataAlignment() != DataAlignment() )
{
delete pNewProperty;
return false;
}
// Use InitFromArchetype to copy most parameters over from the original property.
// Note we do *not* want to call the virtual version, because the new property isn't
// actually the same type, so the virtual overrides will likely crash.
pNewProperty->IProperty::InitFromArchetype(this);
pNewProperty->mpArchetype = pNewArchetype;
NBasics::VectorRemoveOne(mSubInstances, pNewProperty);
if( pNewArchetype )
{
pNewArchetype->mSubInstances.push_back(pNewProperty);
}
// We use CopyDefaultValueTo to ensure that the default value is preserved (as the default value
// is important in most games, and necessary to cook correctly in DKCR). However, note that
// other type-specific parameters (such as min/max values) are lost in the conversion.
CopyDefaultValueTo(pNewProperty);
// Since we are about to delete this property, we need to unregister it and all its sub-instances
// from the name map, and change the type name. The reason we need to do it this way is because
// after we change the type name in the map, we won't be able to unregister the original properties
// because their type name won't match what's in the map. However, the change has to be done before
// initializing any new properties, or else they won't be able to initialize correctly, as the
// name won't be tracked in the map under the new type name. So we need to manually unregister
// everything to clear the original properties from the map, then change the type name, and then
// we're free to start creating and initializing new properties.
if (IsRootArchetype() && mGame >= EGame::EchoesDemo)
{
std::list<IProperty*> SubInstances;
GatherAllSubInstances(SubInstances, true);
for (auto Iter = SubInstances.begin(); Iter != SubInstances.end(); Iter++)
{
IProperty* pProperty = *Iter;
if (pProperty->UsesNameMap())
{
NPropertyMap::UnregisterProperty(pProperty);
}
}
NPropertyMap::ChangeTypeName(this, HashableTypeName(), pNewProperty->HashableTypeName());
}
// Swap out our parent's reference to us to point to the new property.
if (mpParent)
{
for (uint32 SiblingIdx = 0; SiblingIdx < mpParent->mChildren.size(); SiblingIdx++)
{
IProperty* pSibling = mpParent->mChildren[SiblingIdx];
if (pSibling == this)
{
mpParent->mChildren[SiblingIdx] = pNewProperty;
break;
}
}
}
// Change all our child properties to be parented under the new property. (Is this adoption?)
for (uint32 ChildIdx = 0; ChildIdx < mChildren.size(); ChildIdx++)
{
mChildren[ChildIdx]->mpParent = pNewProperty;
pNewProperty->mChildren.push_back(mChildren[ChildIdx]);
}
ASSERT( pNewProperty->mChildren.size() == mChildren.size() );
mChildren.clear();
// Create new versions of all sub-instances that inherit from the new property.
// Note that when the sub-instances complete their conversion, they delete themselves.
// The IProperty destructor removes the property from the archetype's sub-instance list.
// So we shouldn't use a for loop, instead we should just wait until the array is empty
uint32 SubCount = mSubInstances.size();
while (!mSubInstances.empty())
{
bool SubSuccess = mSubInstances[0]->ConvertType(NewType, pNewProperty);
ASSERT( SubSuccess );
}
ASSERT( pNewProperty->mSubInstances.size() == SubCount );
// Conversion is complete! Initialize the new property and flag it dirty.
pNewProperty->Initialize( mpParent, mpScriptTemplate, mOffset );
pNewProperty->MarkDirty();
// Finally, if we are done converting this property and all its instances, resave the templates.
if (IsRootArchetype())
{
NGameList::SaveTemplates();
if (mGame >= EGame::EchoesDemo)
{
NPropertyMap::SaveMap(true);
}
}
// We're done!
delete this;
return true;
}
bool IProperty::UsesNameMap()
{
return Game() >= EGame::EchoesDemo &&
!IsRootParent() &&
!IsIntrinsic() &&
!mpParent->IsAtomic() && // Atomic properties can use the name map, but their children shouldn't
!IsArrayArchetype();
}
bool IProperty::HasAccurateName()
{
// Exceptions for the three hardcoded 4CC property IDs, and for 0xFFFFFFFF (root properties)
if (mID == FOURCC('XFRM') ||
mID == FOURCC('INAM') ||
mID == FOURCC('ACTV') ||
mID == 0xFFFFFFFF)
{
return true;
}
// Children of atomic properties defer to parents. Intrinsic properties and array archetypes also defer to parents.
if ( (mpParent && mpParent->IsAtomic()) || IsIntrinsic() || IsArrayArchetype() )
{
if (mpParent)
return mpParent->HasAccurateName();
else
return true;
}
// For everything else, hash the property name and check if it is a match for the property ID
if (!mFlags.HasFlag(EPropertyFlag::HasCachedNameCheck))
{
CCRC32 Hash;
Hash.Hash(*mName);
Hash.Hash(HashableTypeName());
uint32 GeneratedID = Hash.Digest();
// Some choice properties are incorrectly declared as ints, so account for
// this and allow matching ints against choice typenames as well.
if (GeneratedID != mID && Type() == EPropertyType::Int)
{
Hash = CCRC32();
Hash.Hash(*mName);
Hash.Hash("choice");
GeneratedID = Hash.Digest();
}
if (GeneratedID == mID)
{
mFlags.SetFlag( EPropertyFlag::HasCorrectPropertyName );
}
else
{
mFlags.ClearFlag( EPropertyFlag::HasCorrectPropertyName );
}
mFlags.SetFlag(EPropertyFlag::HasCachedNameCheck);
}
return mFlags.HasFlag( EPropertyFlag::HasCorrectPropertyName );
}
/** IProperty Accessors */
EGame IProperty::Game() const
{
return mGame;
}
IProperty* IProperty::Create(EPropertyType Type,
EGame Game)
{
IProperty* pOut = nullptr;
switch (Type)
{
case EPropertyType::Bool: pOut = new CBoolProperty(Game); break;
case EPropertyType::Byte: pOut = new CByteProperty(Game); break;
case EPropertyType::Short: pOut = new CShortProperty(Game); break;
case EPropertyType::Int: pOut = new CIntProperty(Game); break;
case EPropertyType::Float: pOut = new CFloatProperty(Game); break;
case EPropertyType::Choice: pOut = new CChoiceProperty(Game); break;
case EPropertyType::Enum: pOut = new CEnumProperty(Game); break;
case EPropertyType::Flags: pOut = new CFlagsProperty(Game); break;
case EPropertyType::String: pOut = new CStringProperty(Game); break;
case EPropertyType::Vector: pOut = new CVectorProperty(Game); break;
case EPropertyType::Color: pOut = new CColorProperty(Game); break;
case EPropertyType::Asset: pOut = new CAssetProperty(Game); break;
case EPropertyType::Sound: pOut = new CSoundProperty(Game); break;
case EPropertyType::Animation: pOut = new CAnimationProperty(Game); break;
case EPropertyType::AnimationSet: pOut = new CAnimationSetProperty(Game); break;
case EPropertyType::Sequence: pOut = new CSequenceProperty(Game); break;
case EPropertyType::Spline: pOut = new CSplineProperty(Game); break;
case EPropertyType::Guid: pOut = new CGuidProperty(Game); break;
case EPropertyType::Pointer: pOut = new CPointerProperty(Game); break;
case EPropertyType::Struct: pOut = new CStructProperty(Game); break;
case EPropertyType::Array: pOut = new CArrayProperty(Game); break;
}
// If this assertion fails, then there is an unhandled type!
ASSERT(pOut != nullptr);
return pOut;
}
IProperty* IProperty::CreateCopy(IProperty* pArchetype)
{
IProperty* pOut = Create(pArchetype->Type(), pArchetype->mGame);
pOut->InitFromArchetype(pArchetype);
return pOut;
}
IProperty* IProperty::CreateIntrinsic(EPropertyType Type,
EGame Game,
uint32 Offset,
const TString& rkName)
{
IProperty* pOut = Create(Type, Game);
pOut->mFlags |= EPropertyFlag::IsIntrinsic;
pOut->SetName(rkName);
pOut->Initialize(nullptr, nullptr, Offset);
return pOut;
}
IProperty* IProperty::CreateIntrinsic(EPropertyType Type,
IProperty* pParent,
uint32 Offset,
const TString& rkName)
{
// pParent should always be valid.
// If you are creating a root property, call the other overload takes an EGame instead of a parent.
ASSERT(pParent != nullptr);
IProperty* pOut = Create(Type, pParent->mGame);
pOut->mFlags |= EPropertyFlag::IsIntrinsic;
pOut->SetName(rkName);
pOut->Initialize(pParent, nullptr, Offset);
pParent->mChildren.push_back(pOut);
return pOut;
}
IProperty* IProperty::ArchiveConstructor(EPropertyType Type,
const IArchive& Arc)
{
return Create(Type, Arc.Game());
}