# 如何建立一個 CustomData
###### tags: `reallusion` `venus`
[toc]
## 前言
## 類別宣告
定義的類別需要繼承 `CCustomData`,最少要加入 `RLDeclareRtti`、`RLDeclareStream`。
* `RLDeclareRtti`: 專門給 `RL::DynamicCast` 用,生成 `Rtti`。
* `RLDeclareStream`: 宣告版號
如果這個類別可能會被用於 `CPointer` 的話要加入 `DeclareSmartPointer`。
如果這個類別有包含其他 `CCustomData` 的話那需要加入 `RLDeclareHoldObject`
如果這個類別有記錄到 Pointer 的話那需要加入 `RLDeclareReferenceObject`
> 這邊的 Pointer 係指原生指標例如 `CBase*` 或是 `CNode*`,寫入使用 `StreamWriteReference()`;如果是 `CPointer< T >` 則使用 `StreamWriteLink`;但兩種讀檔都是要使用 `StreamReadLink()`

以下為範例:
```cpp=
#include "RLZeus/RLCustomData.h"
#include "RLPangu/RLPointer.h"
namespace RL
{
class CYourData : public CCustomData
{
DeclareSmartPointer;
RLDeclareRtti;
RLDeclareStream( 1, 0 );
public:
CYourData() = default;
virtual ~CYourData() = default;
CYourData( const CYourData& kData );
};
typedef CPointer< CYourData > CYourDataPtr;
}
```
## 類別實作
### 基本款
首先最基礎是一定要實作 Rtti 跟 Stream:
```cpp=
RLImplementRtti( CYourData, CCustomData );
RLImplementStream( CYourData );
```
和實作以下函數:
```cpp=
// override CBase
size_t CYourData::GetMemoryUsed() const
{
size_t uMemused = 0;
size_t uParentdUsed = 0;
// 注意:公式一定是 當前類別大小 減去 父類別大小
size_t uClassSize = sizeof( CYourData ) - sizeof( CCustomData );
// 計算記憶體大小
uParentdUsed += CCustomData::GetMemoryUsed();
return uMemused + uParentdUsed + uClassSize;
}
// override CBase
size_t CYourData::GetDiskUsed() const
{
size_t uDynamicSize = 0;
size_t uParentSize = CCustomData::GetDiskUsed();
// 計算硬碟大小
return uDynamicSize + uParentSize;
}
// override CBase
Result CYourData::AssignTo( RL::CBase* pObject, ObjectContainer& rkObjects, CProgress* pProgress )
{
RLFRA( CCustomData::AssignTo( pObject, rkObjects, pProgress ) );
// Undo / Redo 會呼叫到
CYourData* pkYourData = StaticCast< CYourData >( pObject );
RLPtrInvalidCall( pkYourData );
// 只需要在乎你在 .h 定義的資料就好,直接指派即可
pkYourData->m_kSomeVector = m_kSomeVector;
return RL_OK;
}
// override CBase
Result CYourData::Save( CIoStream& rkStream, CProgress* pProgress ) const
{
RLFRA( CCustomData::Save( rkStream, pProgress ) );
// 存檔案
return RL_OK;
}
// override CBase
Result CYourData::Load( CIoStream& rkStream, CMemberLink* pkLink, CProgress* pProgress )
{
RLFRA( CCustomData::Load( rkStream, pkLink, pProgress ) );
const auto& kStreamVersion = Version( rkStream );
if ( kStreamVersion >= CVersion( 1, 0 ) )
{
// 讀檔案
}
return RL_OK;
}
// override CBase
bool CYourData::IsLinkObjectRequired() const
{
return CCustomData::IsLinkObjectRequired();
}
// override CBase
void CYourData::DumpDetail( CDumpDetailInterface* pInterface )
{
// 給 Zeus Viwer 用的,看要怎麼呈現資料
// ...
CCustomData::DumpDetail( pInterface );
}
// override CBase
Result CYourData::Link( CIoStream& rkStream, CMemberLink* pkMemberLink, CProgress* pProgress )
{
RLFRA( CCustomData::Link( rkStream, pkMemberLink, pProgress ) );
return RL_OK;
}
// override CBase
Result CYourData::RegisterUniqueObject( CIoStream& rkStream, CProgress* pProgress ) const
{
RLSafeReturn( CCustomData::RegisterUniqueObject( rkStream, pProgress ) );
return RL_OK;
}
```
> 注意如果有修改到儲存的資料,要記得 `SetModified()`
千萬別忘記註冊你的類別,可參考 `ICDataFactory.cpp` 或 `RLMotionMatchingFactory.cpp`。
```cpp=
RLRegisterStreamFactory( CYourData );
```
:::spoiler
實際在 LoadObject 的時候,會用 class 的名稱去建立 object,如果沒註冊則無法 load

:::
### 如果內含 CustomData
如果該類別有宣告 `RLDeclareHoldObject`,則實作:
```cpp=
bool CMDInteractiveData::IsLinkObjectRequired() const
{
if ( m_spMatchingData )
{
return true;
}
return CCustomData::IsLinkObjectRequired();
}
// 如果你的 CustomData 持有 SmartPointer 的話才需要 RegisterUniqueObject
Result CMDInteractiveData::RegisterUniqueObject( CIoStream& rkStream, CProgress* pProgress ) const
{
RLSafeReturn( CCustomData::RegisterUniqueObject( rkStream, pProgress ) );
if ( m_spMatchingData )
{
RLFailReturnAssert( m_spMatchingData->RegisterUniqueObject( rkStream, pProgress ) );
}
return RL_OK;
}
CBase* CUnusedBoneData::GetObjectByNameRtti( const wchar_t* acName, NameCmpFunction pNameCmpFunc, const CRtti* pkRtti, bool bDerivedFromClass, bool bMarkOnly ) const
{
return CCustomData::GetObjectByNameRtti( acName, pNameCmpFunc, pkRtti, bDerivedFromClass, bMarkOnly );;
}
void CUnusedBoneData::GetAllObjectsByNameRtti( std::vector< CBase* >& rkObjects, const wchar_t* acName, NameCmpFunction pNameCmpFunc, const CRtti* pkRtti, bool bDerivedFromClass, bool bMarkOnly ) const
{
CCustomData::GetAllObjectsByNameRtti( rkObjects, acName, pNameCmpFunc, pkRtti, bDerivedFromClass, bMarkOnly );
}
bool CUnusedBoneData::SetObjectTreeMarked( std::set< CZeusID >& kMarkId )
{
return CCustomData::SetObjectTreeMarked( kMarkId );
}
void CUnusedBoneData::ResetObjectTreeMarked()
{
CCustomData::ResetObjectTreeMarked();
}
void CUnusedBoneData::DumpTree( CDumpTreeInterface* pInterface )
{
if ( m_spMatchingData )
{
pInterface->DumpObjectTree( m_spMatchingData );
}
CCustomData::DumpTree( pInterface );
}
// spObject 是 Copy 出來的複製體
// ObjectMap 是過程複製的對應 Map
// 該 Data 的 smart pointer 需要寫進來這
Result CUnusedBoneData::DeepCopyTo( CPointer< CBase >& spObject, ObjectMap& rkObjects, CProgress* pProgress ) const
{
RLSafeReturn( CCustomData::DeepCopyTo( spObject, rkObjects, pProgress ) );
CMDInteractiveData* pInteractiveData = StaticCast< CMDInteractiveData >( spObject );
if ( m_spMatchingData )
{
// 這邊基本上就是說,目前的資料 'm_spMatchingData' 複製一份 DeepCopyTo
// sp 是複製出來的,然後這邊同時也會設定值給 spObject
// 所以跑完這個流程你資料內的 smart pointer 都會是指向複製版本的 data
// 不需要 GraftToByMap
CMotionMatchingDataPtr sp;
RLSubDeepCopyTo( sp, m_spMatchingData, CMotionMatchingData );
RLMemoryCheck( sp );
pInteractiveData->SetMatchingData( sp );
}
return RL_OK;
}
```
### 如果儲存到指標
如果該類別有宣告 `RLDeclareReferenceObject`,則實作:
```cpp=
RLImplementReferenceObject( CYourData );
```
```cpp=
Result CYourData::Link( CIoStream& rkStream, CMemberLink* pkMemberLink, CProgress* pProgress )
{
RLFRA( CCustomData::Link( rkStream, pkMemberLink, pProgress ) );
// 這裡要寫讀取那些指標
// Link 順序很重要! 跟你的 Load 呼叫的 StreamReadLink() 順序要一致
return RL_OK;
}
Result CYourData::GraftToByMap( ObjectMap& rkObjects )
{
// 因為 RLDeclareReferenceObject 導致必定要實作
return GraftToByMap( rkObjects, ENotFoundAction::KeepOriginal );
}
// CBase 有宣告,但不使用巨集會不能繼承
Result CYourData::GraftToByMap( ObjectMap& rkObjects, const ENotFoundAction& eAction )
{
// 基本雛形
auto kCopyObjectPair = rkObjects.find( m_pMyPointer );
if ( kCopyObjectPair != rkObjects.end() )
{
m_pMyPointer = kCopyObjectPair->second;
}
else
{
switch ( eAction )
{
case ENotFoundAction::KeepOriginal: break;
case ENotFoundAction::SetNull: m_pMyPointer = nullptr break;
case ENotFoundAction::ReturnFail: return RL_FAIL;
}
}
return CCustomData::GraftToByMap( rkObjects );
}
```
要使用上面的 Marco 才可以呼叫到 `StreamWriteReference()`(讀檔時)
### 如果儲存到 smart pointer (持有)
如果有持有指標(smart pointer)的話,要確保這兩個 function 都能 query 到那個成員,否則存出去會沒存到那個物件,導致回來會 link error
- `GetAllObjectsByNameRtti()`
- `RegisterUniqueObject()`
#### GraftToByMap
使用時機: 當 class 內有 pointer 時,`DeepCopy` 時仍然會指向舊的物件,這時候就需要 `GraftToByMap()`
雖然 `RLDeclareReferenceObject` marco 會幫你宣告 `GraftToByMap(ObjectMap& rkObjects)`,導致你一定要覆寫,不然根據下面的 code
```cpp
// RLBase.cpp
Result CBase::MapGraftTo( ObjectMap& rkObjects, const ENotFoundAction& eAction )
{
CSortedSet< CBase* > kAllObject;
RLFRA( FindAllObjects< CBase >( kAllObject, this, nullptr, true ) );
for ( const auto pBase : kAllObject )
{
if ( pBase->GraftToByMap( rkObjects, eAction ) == RL_NOT_IMPLEMENTED )
{
RLFRA( pBase->GraftToByMap( rkObjects ) ); // 應該要慢慢改成用 Enum 的版本
}
}
return RL_OK;
}
```
理論上會先採用 enum 版的 `GraftToByMap()`,現在都是提倡你**使用 enum 版**的 `GraftToByMap()`
### 讀檔存檔
```
StreamBytesLink() 和 MemoryBytes()
```
### Copy Constructor & Assignment Operator
一般會看到把 copy ctor 和 assignment operator 關在 `protected`

- copy ctor
- 需要實作 `CopyConstructTo()`
- assignment operator 不開放的原因是因為 shallow/deep copy 的問題
- 所以不開放 `operator=`
### 如果要刪除成員,但還要支援載入
當開發過程中,想要刪除資料結構中的成員,但因為 TA or Art 已經存出 content 了,還要支援載入,那麼要做的就是:
- 步驟:
- 升級版號
- 拿掉該變數除了 `Load()` 以外的 Usage
- 將原本 Load 的 `StreamRead` 改成一個 dummy (建立 same type local variable) 並傳入,並且要將整個區塊用 `if ( kVersion <= 新的板號 )` 隔開
- 可以在這邊進行將舊資料升級成新資料
但如果該資料已經在 validate 中有用過,就拿不掉了,能做的就是建立一個 Deprecated 區塊,並將該變數,以及相關的 member function 移進去,並祈禱有一天這個東西能被拿掉
> 至少在撰文的此刻 2025/01/17 仍是如此
## 如何使用
### 建立並掛載 CustomData
```cpp=
RL::CYourDataPtr spYourData = RLNEW CYourData();
RLMemoryCheck( spYourData );
RLFRA( pNode->AttachCustomData( spYourData ) );
```
> `AttachCustomData()` 來自於 `CBase*`
### 找尋並使用
```cpp=
RL::CYourDataPtr spYourData = RL::FindObject< RL::CYourData >( pNode->GetCustomData() );
RLPidA( spYourData );
RLFRA( spYourData->DoSomething() );
```
### 刪除
```cpp=
RL::CYourDataPtr spYourData = RL::FindObject< RL::CYourData >( pNode->GetCustomData() );
RLPidA( spYourData );
RLFRA( pNode->DetachCustomData( spYourData ) );
```
## 轉型
```cpp=
CCustomData* pkCustomData;
CCustomDataHP* pDataHP = DynamicCast< CCustomDataHP >( pkCustomData );
```
> 參考 `CBase::AttachCustomData()`
## 淺談 CustomData 記指標
> ~~CustomData 不太會去記指標,記名字就好~~ [name=Aren, 2023/04/25]
> 但其實你如果記 string 的話那它效率就變差了... 因為字串要每個字元比較但指標就比一次就好了 [name=Taco, 2023/04/26]
> 記指標的 CustomData 可以參考 `CLinkMapData` [name=Aren]
> 記指標的話要特別
> 你如果 Data 存 SmartPointer 就是持有它,反之用 Raw Pointer 就是 Reference [name=Joe, 2024/03/12]
但如果真的記了(例如 `CBase*` ),可以讓 `CYourData` 類別繼承 `CDestroyWatcher`,並且實作 `OnObjectDestroy()`。
然後在 `Link()` 階段讀取資料後記得要使用 `AddDestroyWatcher()`,才有意義。
這個用途就是當該節點被刪除後,你不用特地呼叫刪除,例如你把該 `CBase*` 儲存在一個陣列中,它會去刪掉對應的資料(其實就是呼叫 `OnObjectDestroy()`)。
詳情程式碼可以參考 `CMDInteractiveData` 的 `m_kMDPropNodeDataMap`。
::: warning
DestroyWatcher: 有 Add 必有 Remove
:::
## 疑難排解
### 讀檔結果跟指標有關的都讀不到(部分讀不到)
我原本在 `CInteractiveDefition` 中新增了 `CArrayList<CLookAtConfig>`,其中 `CLookAtConfig` 的成員 `m_pTarget` 是一個 `RL::CNode*` 原生指標。
我遇到的問題就是在讀檔階段會讀不到,甚至不只 `CArrayList<CLookAtConfig>`,連紀錄在 `CMDInteractiveData` 中的 `m_spMatchingData` 和 `m_kMDPropNodeDataMap` 指標部分全部都讀不到,變成 `nullptr`。
原因很簡單,就是因為我的讀 pointer 的順序,跟你 Link 的順序是有關聯的!讀 pointer 的順序就是看 `StreamReadLink()`。原先因為 `CArrayList<CLookAtConfig>` 是在 `CInteractiveDefition`,而 `CInteractiveDefition` 讀檔的順序比 `m_spMatchingData` 和 `m_kMDPropNodeDataMap` 都來的早。
所以 `Link()` 的**順序**也要對應才正確!
### 儲存 Project 正常但儲存 Prop 就會 Link Error
儲存 Prop 之前會先 Copy 一份,之所以會 link failed 就是因為它指向的指標(`RL::CBase*`)沒有一起被儲存下來,它在 `Link()` 的時候透過 `Load()` 讀取到的 `ZeusID` 是找不到的。
所以要檢查的方向可以從 `GraftToByMap()` 或是 `MapGraftTo()` 開始查起,先把在 `GraftToByMap()` 之前 pointer 指向的位置以及之後的位置記錄起來,然後再看 `Save()` 的時候是不是儲存 graft 之後的位置,基本上會 link failed 就是這個時候它實際 save 到的還是 graft 前的 pointer 位置,所以之後載入 Prop 就會發生 link failed。
如果檢查 `GraftToByMap()` 還是看不出原因,基本上就跟該物件的 copy assginment 或是過程中 function 傳遞是用 copy 而不是 reference 等所導致的。