change bmap interface. update bmfile safe guard
This commit is contained in:
parent
9475e2abc5
commit
1d9bd09224
@ -105,7 +105,7 @@ bool BMFile_Load(
|
||||
|
||||
// create a now one and try to load data.
|
||||
std::unique_ptr<BMap::BMFile> file(new BMap::BMFile(temp_folder, texture_folder, encoding_count, encodings, false));
|
||||
if (file->IsFreezed()) return false;
|
||||
if (file->IsInitError()) return false;
|
||||
if (!file->Load(file_name)) return false;
|
||||
|
||||
// add into list and return
|
||||
@ -124,7 +124,7 @@ bool BMFile_Create(
|
||||
|
||||
// create a now one
|
||||
std::unique_ptr<BMap::BMFile> file(new BMap::BMFile(temp_folder, texture_folder, encoding_count, encodings, false));
|
||||
if (file->IsFreezed()) return false;
|
||||
if (file->IsInitError()) return false;
|
||||
|
||||
// add into list and return if success
|
||||
g_AllBMFiles.emplace(file.get());
|
||||
@ -307,9 +307,10 @@ bool BMMeshTrans_PrepareFaceMtlSlot(BMPARAM_MESHTRANS_DECL(trans), BMPARAM_OUT(L
|
||||
BMPARAM_OUT_ASSIGN(out_mem, trans->PrepareFaceMtlSlot());
|
||||
return BMPARAM_OUT_VAL(out_mem) != nullptr;
|
||||
}
|
||||
bool BMMeshTrans_Parse(BMPARAM_MESHTRANS_DECL(trans), BMPARAM_IN(LibCmo::CK2::ObjImpls::CKMesh*, write_into_mesh)) {
|
||||
bool BMMeshTrans_Parse(BMPARAM_MESHTRANS_DECL(trans), BMPARAM_IN(BMap::BMFile*, bmfile), BMPARAM_IN(LibCmo::CK2::CK_ID, objid)) {
|
||||
if (!CheckBMMeshTrans(trans)) return false;
|
||||
return trans->Parse(write_into_mesh);
|
||||
if (!CheckBMFile(bmfile)) return false;
|
||||
return trans->Parse(bmfile, objid);
|
||||
}
|
||||
|
||||
#pragma endregion
|
||||
|
@ -130,7 +130,7 @@ LIBCMO_EXPORT bool BMMeshTrans_PrepareFaceVertexIndices(BMPARAM_MESHTRANS_DECL(t
|
||||
LIBCMO_EXPORT bool BMMeshTrans_PrepareFaceNormalIndices(BMPARAM_MESHTRANS_DECL(trans), BMPARAM_OUT(LibCmo::CKDWORD*, out_mem));
|
||||
LIBCMO_EXPORT bool BMMeshTrans_PrepareFaceUVIndices(BMPARAM_MESHTRANS_DECL(trans), BMPARAM_OUT(LibCmo::CKDWORD*, out_mem));
|
||||
LIBCMO_EXPORT bool BMMeshTrans_PrepareFaceMtlSlot(BMPARAM_MESHTRANS_DECL(trans), BMPARAM_OUT(LibCmo::CKDWORD*, out_mem));
|
||||
LIBCMO_EXPORT bool BMMeshTrans_Parse(BMPARAM_MESHTRANS_DECL(trans), BMPARAM_IN(LibCmo::CK2::ObjImpls::CKMesh*, write_into_mesh));
|
||||
LIBCMO_EXPORT bool BMMeshTrans_Parse(BMPARAM_MESHTRANS_DECL(trans), BMPARAM_IN(BMap::BMFile*, bmfile), BMPARAM_IN(LibCmo::CK2::CK_ID, objid));
|
||||
|
||||
#pragma endregion
|
||||
|
||||
|
@ -107,12 +107,16 @@ namespace BMap {
|
||||
return m_FaceVertexs.data();
|
||||
}
|
||||
|
||||
bool BMMeshTransition::Parse(LibCmo::CK2::ObjImpls::CKMesh* write_into_mesh) {
|
||||
if (m_IsParsed || write_into_mesh == nullptr) return false;
|
||||
bool BMMeshTransition::Parse(BMFile* bmfile, LibCmo::CK2::CK_ID mesh_id) {
|
||||
// check basic status
|
||||
if (m_IsParsed || bmfile == nullptr) return false;
|
||||
if (!m_IsVertexOK || !m_IsNormalOK || !m_IsUVOK || !m_IsFaceOK || !m_IsMtlSlotOK) return false;
|
||||
m_IsParsed = true;
|
||||
// check pointer assign
|
||||
LibCmo::CK2::ObjImpls::CKObject* writing_mesh = bmfile->GetObjectPtr(mesh_id);
|
||||
if (writing_mesh == nullptr || writing_mesh->GetClassID() != LibCmo::CK2::CK_CLASSID::CKCID_MESH) return false;
|
||||
|
||||
// do parse
|
||||
m_IsParsed = true;
|
||||
DoRealParse();
|
||||
|
||||
// check vertex overflow
|
||||
@ -125,7 +129,7 @@ namespace BMap {
|
||||
}
|
||||
|
||||
// apply to mesh
|
||||
ApplyToMesh(write_into_mesh);
|
||||
ApplyToMesh(bmfile, static_cast<LibCmo::CK2::ObjImpls::CKMesh*>(writing_mesh));
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -139,7 +143,7 @@ namespace BMap {
|
||||
|
||||
// iterate face
|
||||
for (size_t faceid = 0; faceid < face_size; ++faceid) {
|
||||
LibCmo::CKDWORD idx[3];
|
||||
LibCmo::CKDWORD idx[3] { 0, 0, 0 };
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
// create one first
|
||||
TransitionVertex tvec(
|
||||
@ -163,7 +167,7 @@ namespace BMap {
|
||||
}
|
||||
}
|
||||
|
||||
void BMMeshTransition::ApplyToMesh(LibCmo::CK2::ObjImpls::CKMesh* write_into_mesh) {
|
||||
void BMMeshTransition::ApplyToMesh(BMFile* bmfile, LibCmo::CK2::ObjImpls::CKMesh* write_into_mesh) {
|
||||
LibCmo::CKDWORD vec_count = static_cast<LibCmo::CKDWORD>(m_ProcVertexs.size()),
|
||||
face_count = static_cast<LibCmo::CKDWORD>(m_ProcFaces.size()),
|
||||
mtl_count = static_cast<LibCmo::CKDWORD>(m_MtlSlots.size());
|
||||
@ -209,13 +213,12 @@ namespace BMap {
|
||||
}
|
||||
|
||||
// set mtl slot
|
||||
LibCmo::CK2::CKContext* correspondingCtx = write_into_mesh->GetCKContext();
|
||||
write_into_mesh->SetMaterialSlotCount(mtl_count);
|
||||
LibCmo::CK2::ObjImpls::CKMaterial** pMtlSlot = write_into_mesh->GetMaterialSlots();
|
||||
for (LibCmo::CKDWORD i = 0; i < mtl_count; ++i) {
|
||||
// convert id to CKMaterial* and check its type
|
||||
LibCmo::CK2::ObjImpls::CKObject* mtlptr = correspondingCtx->GetObject(m_MtlSlots[i]);
|
||||
if (mtlptr != nullptr && LibCmo::CK2::CKIsChildClassOf(mtlptr->GetClassID(), LibCmo::CK2::CK_CLASSID::CKCID_MATERIAL)) {
|
||||
LibCmo::CK2::ObjImpls::CKObject* mtlptr = bmfile->GetObjectPtr(m_MtlSlots[i]);
|
||||
if (mtlptr != nullptr && mtlptr->GetClassID() == LibCmo::CK2::CK_CLASSID::CKCID_MATERIAL) {
|
||||
*(pMtlSlot++) = static_cast<LibCmo::CK2::ObjImpls::CKMaterial*>(mtlptr);
|
||||
} else {
|
||||
*(pMtlSlot++) = nullptr;
|
||||
@ -228,13 +231,13 @@ namespace BMap {
|
||||
|
||||
#pragma region BMfile
|
||||
|
||||
BMFile::BMFile(LibCmo::CKSTRING temp_folder, LibCmo::CKSTRING texture_folder, LibCmo::CKDWORD encoding_count, LibCmo::CKSTRING* encodings, bool is_reader) :
|
||||
m_IsReader(is_reader), m_IsFreezed(false) {
|
||||
BMFile::BMFile(LibCmo::CKSTRING temp_folder, LibCmo::CKSTRING texture_folder, LibCmo::CKDWORD encoding_count, LibCmo::CKSTRING* encodings, bool is_loader) :
|
||||
m_IsInitError(false), m_IsLoader(is_loader), m_HasLoaded(false), m_HasSaved(false), m_Context(nullptr) {
|
||||
m_Context = new LibCmo::CK2::CKContext();
|
||||
// set temp folder and texture folder
|
||||
auto pm = m_Context->GetPathManager();
|
||||
m_IsFreezed = m_IsFreezed || !pm->AddPath(texture_folder);
|
||||
m_IsFreezed = m_IsFreezed || !pm->SetTempFolder(temp_folder);
|
||||
m_IsInitError = m_IsInitError || !pm->AddPath(texture_folder);
|
||||
m_IsInitError = m_IsInitError || !pm->SetTempFolder(temp_folder);
|
||||
// set encoding
|
||||
LibCmo::XContainer::XArray<LibCmo::XContainer::XString> cache;
|
||||
for (LibCmo::CKDWORD i = 0; i < encoding_count; ++i) {
|
||||
@ -253,12 +256,8 @@ namespace BMap {
|
||||
delete m_Context;
|
||||
}
|
||||
|
||||
bool BMFile::IsFreezed() {
|
||||
return m_IsFreezed;
|
||||
}
|
||||
|
||||
bool BMFile::Load(LibCmo::CKSTRING filename) {
|
||||
if (m_IsFreezed || !m_IsReader) return false;
|
||||
if (!CanExecLoad()) return false;
|
||||
|
||||
// create temp ckfile and load
|
||||
LibCmo::CK2::CKFileReader reader(m_Context);
|
||||
@ -299,11 +298,12 @@ namespace BMap {
|
||||
}
|
||||
}
|
||||
|
||||
m_HasLoaded = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BMFile::Save(LibCmo::CKSTRING filename, LibCmo::CKINT compress_level) {
|
||||
if (m_IsFreezed || m_IsReader) return false;
|
||||
if (!CanExecSave()) return false;
|
||||
|
||||
// create temp writer
|
||||
LibCmo::CK2::CKFileWriter writer(m_Context);
|
||||
@ -331,18 +331,11 @@ namespace BMap {
|
||||
// save to file and detect error
|
||||
LibCmo::CK2::CKERROR err = writer.Save(filename);
|
||||
|
||||
// set freezed to stop any change again.
|
||||
// aka, only allow save once.
|
||||
m_IsFreezed = true;
|
||||
|
||||
// return with error detect.
|
||||
m_HasSaved = true;
|
||||
return err == LibCmo::CK2::CKERROR::CKERR_OK;
|
||||
}
|
||||
|
||||
LibCmo::CK2::ObjImpls::CKObject* BMFile::GetObjectPtr(LibCmo::CK2::CK_ID objid) {
|
||||
return m_Context->GetObject(objid);;
|
||||
}
|
||||
|
||||
LibCmo::CKDWORD BMFile::GetGroupCount() { return CommonGetObjectCount(m_ObjGroups); }
|
||||
LibCmo::CK2::CK_ID BMFile::GetGroup(LibCmo::CKDWORD idx) { return CommonGetObject(m_ObjGroups, idx); }
|
||||
LibCmo::CK2::CK_ID BMFile::CreateGroup() { return CommonCreateObject(m_ObjGroups, LibCmo::CK2::CK_CLASSID::CKCID_GROUP); }
|
||||
|
200
BMap/BMap.hpp
200
BMap/BMap.hpp
@ -8,6 +8,127 @@
|
||||
|
||||
namespace BMap {
|
||||
|
||||
class BMFile {
|
||||
public:
|
||||
BMFile(LibCmo::CKSTRING temp_folder, LibCmo::CKSTRING texture_folder, LibCmo::CKDWORD encoding_count, LibCmo::CKSTRING* encodings, bool is_reader);
|
||||
~BMFile();
|
||||
LIBCMO_DISABLE_COPY_MOVE(BMFile);
|
||||
|
||||
// ===== safe visit functions =====
|
||||
|
||||
/**
|
||||
Safe Visit Function will make sure this class is visited with safe mode.
|
||||
These function will block all other functions if this class init failed.
|
||||
Or, block any more operations if this class has loaded or saved once. In this time you only can free this class
|
||||
*/
|
||||
|
||||
public:
|
||||
bool IsInitError() {
|
||||
return m_IsInitError;
|
||||
}
|
||||
|
||||
private:
|
||||
bool CanExecLoad() {
|
||||
// no error, is loader, no prev load
|
||||
return (!m_IsInitError && m_IsLoader && !m_HasLoaded);
|
||||
}
|
||||
bool CanExecSave() {
|
||||
// no error, is saver, no prev save
|
||||
return (!m_IsInitError && !m_IsLoader && !m_HasSaved);
|
||||
}
|
||||
bool CanExecLoaderVisitor() {
|
||||
// no error, is loader, has loaded
|
||||
return (!m_IsInitError && m_IsLoader && m_HasLoaded);
|
||||
}
|
||||
bool CanExecSaverVisitor() {
|
||||
// no error, is saver, not saveed yet
|
||||
// same as CanExecSave
|
||||
return (!m_IsInitError && !m_IsLoader && !m_HasSaved);
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief True if an error occurs when initializing this class.
|
||||
*/
|
||||
bool m_IsInitError;
|
||||
/**
|
||||
* @brief True if this class is a reader.
|
||||
*/
|
||||
bool m_IsLoader;
|
||||
/**
|
||||
* @brief True if this class has read. Only valid when this class is reader.
|
||||
*/
|
||||
bool m_HasLoaded;
|
||||
/**
|
||||
* @brief True if this class has written. Only valid when this class is writer.
|
||||
*/
|
||||
bool m_HasSaved;
|
||||
|
||||
// ===== help functions =====
|
||||
|
||||
public:
|
||||
bool Load(LibCmo::CKSTRING filename);
|
||||
bool Save(LibCmo::CKSTRING filename, LibCmo::CKINT compress_level);
|
||||
|
||||
LibCmo::CK2::ObjImpls::CKObject* GetObjectPtr(LibCmo::CK2::CK_ID objid) {
|
||||
return m_Context->GetObject(objid);;
|
||||
}
|
||||
|
||||
// ===== visitors =====
|
||||
|
||||
private:
|
||||
LibCmo::CKDWORD CommonGetObjectCount(std::vector<LibCmo::CK2::CK_ID>& container) {
|
||||
// only available in loader
|
||||
if (!CanExecLoaderVisitor()) return 0;
|
||||
return static_cast<LibCmo::CKDWORD>(container.size());
|
||||
}
|
||||
LibCmo::CK2::CK_ID CommonGetObject(std::vector<LibCmo::CK2::CK_ID>& container, LibCmo::CKDWORD idx) {
|
||||
// only available in loader
|
||||
if (!CanExecLoaderVisitor()) return 0;
|
||||
return container[idx];
|
||||
}
|
||||
LibCmo::CK2::CK_ID CommonCreateObject(std::vector<LibCmo::CK2::CK_ID>& container, LibCmo::CK2::CK_CLASSID cid) {
|
||||
// only available in saver
|
||||
if (!CanExecSaverVisitor()) return 0;
|
||||
|
||||
// try create object and get its pointer
|
||||
LibCmo::CK2::ObjImpls::CKObject* obj = m_Context->CreateObject(cid, nullptr);
|
||||
// check creation validation
|
||||
if (obj == nullptr) return 0;
|
||||
|
||||
// if success, write its id and emplace its id into list
|
||||
LibCmo::CK2::CK_ID objid = obj->GetID();
|
||||
container.emplace_back(objid);
|
||||
return objid;
|
||||
}
|
||||
public:
|
||||
LibCmo::CKDWORD GetGroupCount();
|
||||
LibCmo::CK2::CK_ID GetGroup(LibCmo::CKDWORD idx);
|
||||
LibCmo::CK2::CK_ID CreateGroup();
|
||||
LibCmo::CKDWORD Get3dObjectCount();
|
||||
LibCmo::CK2::CK_ID Get3dObject(LibCmo::CKDWORD idx);
|
||||
LibCmo::CK2::CK_ID Create3dObject();
|
||||
LibCmo::CKDWORD GetMeshCount();
|
||||
LibCmo::CK2::CK_ID GetMesh(LibCmo::CKDWORD idx);
|
||||
LibCmo::CK2::CK_ID CreateMesh();
|
||||
LibCmo::CKDWORD GetMaterialCount();
|
||||
LibCmo::CK2::CK_ID GetMaterial(LibCmo::CKDWORD idx);
|
||||
LibCmo::CK2::CK_ID CreateMaterial();
|
||||
LibCmo::CKDWORD GetTextureCount();
|
||||
LibCmo::CK2::CK_ID GetTexture(LibCmo::CKDWORD idx);
|
||||
LibCmo::CK2::CK_ID CreateTexture();
|
||||
|
||||
private:
|
||||
LibCmo::CK2::CKContext* m_Context;
|
||||
|
||||
std::vector<LibCmo::CK2::CK_ID> m_ObjGroups;
|
||||
std::vector<LibCmo::CK2::CK_ID> m_Obj3dObjects;
|
||||
std::vector<LibCmo::CK2::CK_ID> m_ObjMeshs;
|
||||
std::vector<LibCmo::CK2::CK_ID> m_ObjMaterials;
|
||||
std::vector<LibCmo::CK2::CK_ID> m_ObjTextures;
|
||||
|
||||
};
|
||||
|
||||
class BMMeshTransition {
|
||||
private:
|
||||
struct TransitionVertex {
|
||||
@ -47,11 +168,11 @@ namespace BMap {
|
||||
LibCmo::CKDWORD* PrepareFaceUVIndices();
|
||||
LibCmo::CKDWORD* PrepareFaceMtlSlot();
|
||||
|
||||
bool Parse(LibCmo::CK2::ObjImpls::CKMesh* write_into_mesh);
|
||||
bool Parse(BMFile* bmfile, LibCmo::CK2::CK_ID mesh_id);
|
||||
|
||||
private:
|
||||
void DoRealParse();
|
||||
void ApplyToMesh(LibCmo::CK2::ObjImpls::CKMesh* write_into_mesh);
|
||||
void ApplyToMesh(BMFile* bmfile, LibCmo::CK2::ObjImpls::CKMesh* write_into_mesh);
|
||||
|
||||
bool m_IsVertexOK, m_IsNormalOK, m_IsUVOK, m_IsFaceOK, m_IsMtlSlotOK;
|
||||
bool m_IsParsed;
|
||||
@ -73,79 +194,4 @@ namespace BMap {
|
||||
std::map<TransitionVertex, LibCmo::CKDWORD, TransitionVertexCompare> m_ProcDupRemover;
|
||||
};
|
||||
|
||||
class BMFile {
|
||||
public:
|
||||
BMFile(LibCmo::CKSTRING temp_folder, LibCmo::CKSTRING texture_folder, LibCmo::CKDWORD encoding_count, LibCmo::CKSTRING* encodings, bool is_reader);
|
||||
~BMFile();
|
||||
LIBCMO_DISABLE_COPY_MOVE(BMFile);
|
||||
|
||||
// ===== help functions =====
|
||||
|
||||
/**
|
||||
* @brief Check whether this instance is freezed.
|
||||
* @return True if freezed. This instance should be free immediately.
|
||||
*/
|
||||
bool IsFreezed();
|
||||
bool Load(LibCmo::CKSTRING filename);
|
||||
bool Save(LibCmo::CKSTRING filename, LibCmo::CKINT compress_level);
|
||||
LibCmo::CK2::ObjImpls::CKObject* GetObjectPtr(LibCmo::CK2::CK_ID objid);
|
||||
|
||||
// ===== visitors =====
|
||||
|
||||
private:
|
||||
LibCmo::CKDWORD CommonGetObjectCount(std::vector<LibCmo::CK2::CK_ID>& container) {
|
||||
if (m_IsFreezed || !m_IsReader) return 0;
|
||||
return static_cast<LibCmo::CKDWORD>(container.size());
|
||||
}
|
||||
LibCmo::CK2::CK_ID CommonGetObject(std::vector<LibCmo::CK2::CK_ID>& container, LibCmo::CKDWORD idx) {
|
||||
if (m_IsFreezed || !m_IsReader || idx >= container.size()) return 0;
|
||||
return container[idx];
|
||||
}
|
||||
LibCmo::CK2::CK_ID CommonCreateObject(std::vector<LibCmo::CK2::CK_ID>& container, LibCmo::CK2::CK_CLASSID cid) {
|
||||
// only available in writer
|
||||
if (m_IsFreezed || m_IsReader) return 0;
|
||||
|
||||
// try create object and get its pointer
|
||||
LibCmo::CK2::ObjImpls::CKObject* obj = m_Context->CreateObject(cid, nullptr);
|
||||
// check creation validation
|
||||
if (obj == nullptr) return 0;
|
||||
|
||||
// if success, write its id and emplace its id into list
|
||||
LibCmo::CK2::CK_ID objid = obj->GetID();
|
||||
container.emplace_back(objid);
|
||||
return objid;
|
||||
}
|
||||
public:
|
||||
LibCmo::CKDWORD GetGroupCount();
|
||||
LibCmo::CK2::CK_ID GetGroup(LibCmo::CKDWORD idx);
|
||||
LibCmo::CK2::CK_ID CreateGroup();
|
||||
LibCmo::CKDWORD Get3dObjectCount();
|
||||
LibCmo::CK2::CK_ID Get3dObject(LibCmo::CKDWORD idx);
|
||||
LibCmo::CK2::CK_ID Create3dObject();
|
||||
LibCmo::CKDWORD GetMeshCount();
|
||||
LibCmo::CK2::CK_ID GetMesh(LibCmo::CKDWORD idx);
|
||||
LibCmo::CK2::CK_ID CreateMesh();
|
||||
LibCmo::CKDWORD GetMaterialCount();
|
||||
LibCmo::CK2::CK_ID GetMaterial(LibCmo::CKDWORD idx);
|
||||
LibCmo::CK2::CK_ID CreateMaterial();
|
||||
LibCmo::CKDWORD GetTextureCount();
|
||||
LibCmo::CK2::CK_ID GetTexture(LibCmo::CKDWORD idx);
|
||||
LibCmo::CK2::CK_ID CreateTexture();
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief True if all operation of this instance should be rejected.
|
||||
*/
|
||||
bool m_IsFreezed;
|
||||
bool m_IsReader;
|
||||
LibCmo::CK2::CKContext* m_Context;
|
||||
|
||||
std::vector<LibCmo::CK2::CK_ID> m_ObjGroups;
|
||||
std::vector<LibCmo::CK2::CK_ID> m_Obj3dObjects;
|
||||
std::vector<LibCmo::CK2::CK_ID> m_ObjMeshs;
|
||||
std::vector<LibCmo::CK2::CK_ID> m_ObjMaterials;
|
||||
std::vector<LibCmo::CK2::CK_ID> m_ObjTextures;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
11
README.md
11
README.md
@ -1,6 +1,7 @@
|
||||
# libcmo21
|
||||
|
||||
The Library for CMO File Read/Write. Also the Minimalist Virtools Environment.
|
||||
The Library for CMO (also accept NMO, VMO and NMS) File Read/Write. Also the Minimalist Virtools Environment.
|
||||
Write with one Library and Load Virtools File Everywhere.
|
||||
|
||||
## Status
|
||||
|
||||
@ -43,9 +44,9 @@ There are 3 lists which indicate our accept guideline.
|
||||
|
||||
These features will be accepted as soon as possible.
|
||||
|
||||
* The bug fix of Virtools file reader.
|
||||
* The bug fix of any existing code.
|
||||
* Class layout, `Load()` functions of following `CKObject` based classes.
|
||||
- `CK3dEntity`
|
||||
- None
|
||||
* Class layout, and `LoadData()` functions of following `CKBaseManager` based classes.
|
||||
- `CKAttributeManager`
|
||||
|
||||
@ -53,8 +54,8 @@ These features will be accepted as soon as possible.
|
||||
|
||||
These features are in plan, but not urge to merge.
|
||||
|
||||
* The `CK_ID` remap system of Reader & Writer.
|
||||
* Any Save functions.
|
||||
* The `CK_ID` remap system of Reader.
|
||||
* CK3dEntity hierarchy system.
|
||||
* Other CK classes implementations.
|
||||
* Non-Virtools 2.1 implementations.
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user