feat(editor): complete static cpp component workflow

This commit is contained in:
ayuan9957 2026-06-05 09:55:12 +08:00
parent 695fc30d34
commit 78544ae5eb
12 changed files with 734 additions and 9 deletions

View File

@ -2588,6 +2588,93 @@ bool MetaCoreDeserializeComponentValue(
return MetaCoreDeserializeFromBytes(payload, component, registry); return MetaCoreDeserializeFromBytes(payload, component, registry);
} }
std::optional<std::vector<std::byte>> MetaCoreSerializeReflectedComponentValue(
const MetaCoreStructDescriptor& descriptor,
const void* component,
const MetaCoreTypeRegistry& registry
) {
if (component == nullptr) {
return std::nullopt;
}
MetaCoreArchiveWriter writer;
writer.WriteBytes(&descriptor.Version, sizeof(descriptor.Version));
const std::uint32_t fieldCount = static_cast<std::uint32_t>(descriptor.Fields.size());
writer.WriteBytes(&fieldCount, sizeof(fieldCount));
for (const MetaCoreFieldDescriptor& field : descriptor.Fields) {
if (!MetaCoreSerializeValue(writer, field.Name, registry)) {
return std::nullopt;
}
MetaCoreArchiveWriter fieldWriter;
if (!field.Serialize || !field.Serialize(component, fieldWriter, registry)) {
return std::nullopt;
}
const std::uint64_t fieldSize = static_cast<std::uint64_t>(fieldWriter.GetSize());
writer.WriteBytes(&fieldSize, sizeof(fieldSize));
writer.WriteSpan(fieldWriter.GetBuffer());
}
return writer.GetBuffer();
}
bool MetaCoreDeserializeReflectedComponentValue(
const MetaCoreStructDescriptor& descriptor,
std::span<const std::byte> payload,
void* component,
const MetaCoreTypeRegistry& registry
) {
if (component == nullptr) {
return false;
}
MetaCoreArchiveReader reader(payload);
std::uint32_t serializedVersion = 0;
std::uint32_t fieldCount = 0;
if (!reader.ReadBytes(&serializedVersion, sizeof(serializedVersion)) ||
!reader.ReadBytes(&fieldCount, sizeof(fieldCount))) {
return false;
}
(void)serializedVersion;
for (std::uint32_t index = 0; index < fieldCount; ++index) {
std::string fieldName;
std::uint64_t fieldSize = 0;
if (!MetaCoreDeserializeValue(reader, fieldName, registry) ||
!reader.ReadBytes(&fieldSize, sizeof(fieldSize))) {
return false;
}
const auto fieldBytes = reader.ReadSpan(static_cast<std::size_t>(fieldSize));
if (!fieldBytes.has_value()) {
return false;
}
const auto fieldIterator = std::find_if(
descriptor.Fields.begin(),
descriptor.Fields.end(),
[&](const MetaCoreFieldDescriptor& field) {
return field.Name == fieldName;
}
);
if (fieldIterator == descriptor.Fields.end()) {
continue;
}
MetaCoreArchiveReader fieldReader(*fieldBytes);
if (!fieldIterator->Deserialize ||
!fieldIterator->Deserialize(component, fieldReader, registry) ||
!fieldReader.IsEof()) {
return false;
}
}
return reader.IsEof();
}
[[nodiscard]] std::string MetaCorePathToPortableString(const std::filesystem::path& path) { [[nodiscard]] std::string MetaCorePathToPortableString(const std::filesystem::path& path) {
return path.lexically_normal().generic_string(); return path.lexically_normal().generic_string();
} }
@ -5823,7 +5910,7 @@ public:
: sceneDocument->Name; : sceneDocument->Name;
SavedSnapshotBytes_ = MetaCoreSerializeSceneSnapshotBytes( SavedSnapshotBytes_ = MetaCoreSerializeSceneSnapshotBytes(
ReflectionRegistry_->GetTypeRegistry(), ReflectionRegistry_->GetTypeRegistry(),
editorContext.GetScene().CaptureSnapshot() editorContext.CaptureStateSnapshot().SceneSnapshot
).value_or(std::vector<std::byte>{}); ).value_or(std::vector<std::byte>{});
SetDirtyState(false); SetDirtyState(false);
@ -5871,9 +5958,11 @@ public:
sceneGuid = MetaCoreAssetGuid::Generate(); sceneGuid = MetaCoreAssetGuid::Generate();
} }
const MetaCoreEditorStateSnapshot savedState = editorContext.CaptureStateSnapshot();
MetaCoreSceneDocument sceneDocument; MetaCoreSceneDocument sceneDocument;
sceneDocument.Name = CurrentSceneName_.empty() ? MetaCoreSceneDisplayNameFromPath(normalizedScenePath) : CurrentSceneName_; sceneDocument.Name = CurrentSceneName_.empty() ? MetaCoreSceneDisplayNameFromPath(normalizedScenePath) : CurrentSceneName_;
sceneDocument.GameObjects = editorContext.GetScene().CaptureSnapshot().GameObjects; sceneDocument.GameObjects = savedState.SceneSnapshot.GameObjects;
sceneDocument.Selection.SelectedObjectIds = editorContext.GetSelectedObjectIds(); sceneDocument.Selection.SelectedObjectIds = editorContext.GetSelectedObjectIds();
sceneDocument.Selection.ActiveObjectId = editorContext.GetActiveObjectId(); sceneDocument.Selection.ActiveObjectId = editorContext.GetActiveObjectId();
sceneDocument.Selection.SelectionAnchorId = editorContext.GetSelectionAnchorId(); sceneDocument.Selection.SelectionAnchorId = editorContext.GetSelectionAnchorId();
@ -5901,7 +5990,7 @@ public:
CurrentSceneName_ = sceneDocument.Name; CurrentSceneName_ = sceneDocument.Name;
SavedSnapshotBytes_ = MetaCoreSerializeSceneSnapshotBytes( SavedSnapshotBytes_ = MetaCoreSerializeSceneSnapshotBytes(
ReflectionRegistry_->GetTypeRegistry(), ReflectionRegistry_->GetTypeRegistry(),
editorContext.GetScene().CaptureSnapshot() savedState.SceneSnapshot
).value_or(std::vector<std::byte>{}); ).value_or(std::vector<std::byte>{});
SetDirtyState(false); SetDirtyState(false);
@ -6167,7 +6256,7 @@ public:
if (!gameObject.HasComponent<MetaCoreCameraComponent>()) { if (!gameObject.HasComponent<MetaCoreCameraComponent>()) {
return false; return false;
} }
gameObject.AddComponent<MetaCoreCameraComponent>(MetaCoreCameraComponent{}); gameObject.GetComponent<MetaCoreCameraComponent>() = MetaCoreCameraComponent{};
return true; return true;
}, },
[](const MetaCoreGameObject& gameObject, const MetaCoreTypeRegistry& registry) -> std::optional<std::vector<std::byte>> { [](const MetaCoreGameObject& gameObject, const MetaCoreTypeRegistry& registry) -> std::optional<std::vector<std::byte>> {
@ -6217,7 +6306,7 @@ public:
if (!gameObject.HasComponent<MetaCoreLightComponent>()) { if (!gameObject.HasComponent<MetaCoreLightComponent>()) {
return false; return false;
} }
gameObject.AddComponent<MetaCoreLightComponent>(MetaCoreLightComponent{}); gameObject.GetComponent<MetaCoreLightComponent>() = MetaCoreLightComponent{};
return true; return true;
}, },
[](const MetaCoreGameObject& gameObject, const MetaCoreTypeRegistry& registry) -> std::optional<std::vector<std::byte>> { [](const MetaCoreGameObject& gameObject, const MetaCoreTypeRegistry& registry) -> std::optional<std::vector<std::byte>> {
@ -6267,7 +6356,7 @@ public:
if (!gameObject.HasComponent<MetaCoreMeshRendererComponent>()) { if (!gameObject.HasComponent<MetaCoreMeshRendererComponent>()) {
return false; return false;
} }
gameObject.AddComponent<MetaCoreMeshRendererComponent>(MetaCoreMeshRendererComponent{}); gameObject.GetComponent<MetaCoreMeshRendererComponent>() = MetaCoreMeshRendererComponent{};
return true; return true;
}, },
[](const MetaCoreGameObject& gameObject, const MetaCoreTypeRegistry& registry) -> std::optional<std::vector<std::byte>> { [](const MetaCoreGameObject& gameObject, const MetaCoreTypeRegistry& registry) -> std::optional<std::vector<std::byte>> {
@ -6318,6 +6407,51 @@ public:
if (descriptor.Category.empty()) { if (descriptor.Category.empty()) {
descriptor.Category = "Scripts"; descriptor.Category = "Scripts";
} }
if (!descriptor.CopyComponentPayload &&
descriptor.ReflectedType != nullptr &&
descriptor.ConstComponent &&
descriptor.HasComponent) {
const MetaCoreStructDescriptor* reflectedType = descriptor.ReflectedType;
auto constComponent = descriptor.ConstComponent;
auto hasComponent = descriptor.HasComponent;
descriptor.CopyComponentPayload =
[reflectedType, constComponent, hasComponent](
const MetaCoreGameObject& gameObject,
const MetaCoreTypeRegistry& registry
) -> std::optional<std::vector<std::byte>> {
if (!hasComponent(gameObject)) {
return std::nullopt;
}
return MetaCoreSerializeReflectedComponentValue(*reflectedType, constComponent(gameObject), registry);
};
}
if (!descriptor.PasteComponentPayload &&
descriptor.ReflectedType != nullptr &&
descriptor.MutableComponent &&
descriptor.HasComponent) {
const MetaCoreStructDescriptor* reflectedType = descriptor.ReflectedType;
auto mutableComponent = descriptor.MutableComponent;
auto hasComponent = descriptor.HasComponent;
auto addComponent = descriptor.AddComponent;
descriptor.PasteComponentPayload =
[reflectedType, mutableComponent, hasComponent, addComponent](
MetaCoreGameObject& gameObject,
std::span<const std::byte> payload,
const MetaCoreTypeRegistry& registry
) {
if (!hasComponent(gameObject)) {
if (!addComponent || !addComponent(gameObject)) {
return false;
}
}
return MetaCoreDeserializeReflectedComponentValue(
*reflectedType,
payload,
mutableComponent(gameObject),
registry
);
};
}
Descriptors_.push_back(std::move(descriptor)); Descriptors_.push_back(std::move(descriptor));
return true; return true;
@ -6365,6 +6499,30 @@ public:
); );
} }
void DrawComponentGizmos(MetaCoreEditorContext& editorContext) override {
const std::vector<MetaCoreId> orderedObjectIds = editorContext.GetScene().BuildHierarchyPreorder();
for (const MetaCoreComponentDescriptor& descriptor : Descriptors_) {
if (!descriptor.HasComponent ||
(!descriptor.Lifecycle.OnDrawGizmos && !descriptor.Lifecycle.OnDrawGizmosSelected)) {
continue;
}
for (MetaCoreId objectId : orderedObjectIds) {
MetaCoreGameObject gameObject = editorContext.GetScene().FindGameObject(objectId);
if (!gameObject || !descriptor.HasComponent(gameObject)) {
continue;
}
if (descriptor.Lifecycle.OnDrawGizmos) {
descriptor.Lifecycle.OnDrawGizmos(editorContext, gameObject);
}
if (descriptor.Lifecycle.OnDrawGizmosSelected && editorContext.IsObjectSelected(objectId)) {
descriptor.Lifecycle.OnDrawGizmosSelected(editorContext, gameObject);
}
}
}
}
private: private:
std::vector<MetaCoreComponentDescriptor> Descriptors_{}; std::vector<MetaCoreComponentDescriptor> Descriptors_{};
std::shared_ptr<MetaCoreIReflectionRegistry> ReflectionRegistry_{}; std::shared_ptr<MetaCoreIReflectionRegistry> ReflectionRegistry_{};

View File

@ -6059,7 +6059,7 @@ public:
ImGui::Spacing(); ImGui::Spacing();
ImGui::Separator(); ImGui::Separator();
if (ImGui::Button("添加组件", ImVec2(-1, 26))) { if (ImGui::Button("Add Component", ImVec2(-1, 26))) {
ImGui::OpenPopup("AddComponentPopup"); ImGui::OpenPopup("AddComponentPopup");
} }

View File

@ -1225,6 +1225,11 @@ void MetaCoreEditorApp::DrawSceneViewWindow() {
ImGui::EndDragDropTarget(); ImGui::EndDragDropTarget();
} }
if (const auto componentRegistry = ModuleRegistry_.ResolveService<MetaCoreIComponentTypeRegistry>();
componentRegistry != nullptr) {
componentRegistry->DrawComponentGizmos(*EditorContext_);
}
bool gizmoHovering = false; bool gizmoHovering = false;
if (GMetaCoreEnableImGuizmo) { if (GMetaCoreEnableImGuizmo) {
SceneInteractionService_.HandleGizmoManipulation(*EditorContext_); SceneInteractionService_.HandleGizmoManipulation(*EditorContext_);

View File

@ -17,6 +17,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <filesystem> #include <filesystem>
#include <span>
#include <unordered_set> #include <unordered_set>
namespace MetaCore { namespace MetaCore {
@ -124,9 +125,26 @@ private:
} }
} }
if (lhs.SerializedComponents.size() != rhs.SerializedComponents.size()) {
return false;
}
for (std::size_t index = 0; index < lhs.SerializedComponents.size(); ++index) {
if (lhs.SerializedComponents[index].TypeId != rhs.SerializedComponents[index].TypeId ||
lhs.SerializedComponents[index].Payload != rhs.SerializedComponents[index].Payload) {
return false;
}
}
return true; return true;
} }
[[nodiscard]] bool MetaCoreIsNativeSceneComponentType(std::string_view typeId) {
return typeId == "Transform" ||
typeId == "Camera" ||
typeId == "Light" ||
typeId == "MeshRenderer";
}
[[nodiscard]] bool MetaCoreStateSnapshotEqual(const MetaCoreEditorStateSnapshot& lhs, const MetaCoreEditorStateSnapshot& rhs) { [[nodiscard]] bool MetaCoreStateSnapshotEqual(const MetaCoreEditorStateSnapshot& lhs, const MetaCoreEditorStateSnapshot& rhs) {
if (lhs.SceneSnapshot.GameObjects.size() != rhs.SceneSnapshot.GameObjects.size()) { if (lhs.SceneSnapshot.GameObjects.size() != rhs.SceneSnapshot.GameObjects.size()) {
return false; return false;
@ -616,6 +634,7 @@ bool MetaCoreEditorContext::RedoCommand() {
MetaCoreEditorStateSnapshot MetaCoreEditorContext::CaptureStateSnapshot() const { MetaCoreEditorStateSnapshot MetaCoreEditorContext::CaptureStateSnapshot() const {
MetaCoreEditorStateSnapshot snapshot; MetaCoreEditorStateSnapshot snapshot;
snapshot.SceneSnapshot = Scene_.CaptureSnapshot(); snapshot.SceneSnapshot = Scene_.CaptureSnapshot();
CaptureSerializedComponents(snapshot.SceneSnapshot);
snapshot.SelectionSnapshot.SelectedObjectIds = SelectedObjectIds_; snapshot.SelectionSnapshot.SelectedObjectIds = SelectedObjectIds_;
snapshot.SelectionSnapshot.ActiveObjectId = ActiveObjectId_; snapshot.SelectionSnapshot.ActiveObjectId = ActiveObjectId_;
snapshot.SelectionSnapshot.SelectionAnchorId = SelectionAnchorId_; snapshot.SelectionSnapshot.SelectionAnchorId = SelectionAnchorId_;
@ -624,6 +643,7 @@ MetaCoreEditorStateSnapshot MetaCoreEditorContext::CaptureStateSnapshot() const
void MetaCoreEditorContext::RestoreStateSnapshot(const MetaCoreEditorStateSnapshot& snapshot) { void MetaCoreEditorContext::RestoreStateSnapshot(const MetaCoreEditorStateSnapshot& snapshot) {
Scene_.RestoreSnapshot(snapshot.SceneSnapshot); Scene_.RestoreSnapshot(snapshot.SceneSnapshot);
RestoreSerializedComponents(snapshot.SceneSnapshot);
SetSelection(snapshot.SelectionSnapshot.SelectedObjectIds, snapshot.SelectionSnapshot.ActiveObjectId); SetSelection(snapshot.SelectionSnapshot.SelectedObjectIds, snapshot.SelectionSnapshot.ActiveObjectId);
if (snapshot.SelectionSnapshot.SelectionAnchorId != 0 && if (snapshot.SelectionSnapshot.SelectionAnchorId != 0 &&
(bool)Scene_.FindGameObject(snapshot.SelectionSnapshot.SelectionAnchorId)) { (bool)Scene_.FindGameObject(snapshot.SelectionSnapshot.SelectionAnchorId)) {
@ -964,6 +984,97 @@ void MetaCoreEditorContext::ClearSelectedAssetSubId() {
void MetaCoreEditorContext::SetDockLayoutBuilt(bool built) { DockLayoutBuilt_ = built; } void MetaCoreEditorContext::SetDockLayoutBuilt(bool built) { DockLayoutBuilt_ = built; }
bool MetaCoreEditorContext::HasDockLayoutBuilt() const { return DockLayoutBuilt_; } bool MetaCoreEditorContext::HasDockLayoutBuilt() const { return DockLayoutBuilt_; }
void MetaCoreEditorContext::CaptureSerializedComponents(MetaCoreSceneSnapshot& snapshot) const {
const auto componentRegistry = ModuleRegistry_.ResolveService<MetaCoreIComponentTypeRegistry>();
const auto reflectionRegistry = ModuleRegistry_.ResolveService<MetaCoreIReflectionRegistry>();
if (componentRegistry == nullptr || reflectionRegistry == nullptr) {
return;
}
const MetaCoreTypeRegistry& typeRegistry = reflectionRegistry->GetTypeRegistry();
for (MetaCoreGameObjectData& objectData : snapshot.GameObjects) {
objectData.SerializedComponents.clear();
const MetaCoreGameObject gameObject = Scene_.FindGameObject(objectData.Id);
if (!gameObject) {
continue;
}
for (const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor : componentRegistry->GetComponentDescriptors()) {
if (MetaCoreIsNativeSceneComponentType(descriptor.TypeId) ||
!descriptor.HasComponent ||
!descriptor.CopyComponentPayload ||
!descriptor.HasComponent(gameObject)) {
continue;
}
const auto payload = descriptor.CopyComponentPayload(gameObject, typeRegistry);
if (!payload.has_value() || payload->empty()) {
continue;
}
objectData.SerializedComponents.push_back(MetaCoreSerializedComponentData{
descriptor.TypeId,
*payload
});
}
std::sort(
objectData.SerializedComponents.begin(),
objectData.SerializedComponents.end(),
[](const MetaCoreSerializedComponentData& lhs, const MetaCoreSerializedComponentData& rhs) {
return lhs.TypeId < rhs.TypeId;
}
);
}
}
void MetaCoreEditorContext::RestoreSerializedComponents(const MetaCoreSceneSnapshot& snapshot) {
const auto componentRegistry = ModuleRegistry_.ResolveService<MetaCoreIComponentTypeRegistry>();
const auto reflectionRegistry = ModuleRegistry_.ResolveService<MetaCoreIReflectionRegistry>();
if (componentRegistry == nullptr || reflectionRegistry == nullptr) {
return;
}
const MetaCoreTypeRegistry& typeRegistry = reflectionRegistry->GetTypeRegistry();
for (const MetaCoreGameObjectData& objectData : snapshot.GameObjects) {
if (objectData.SerializedComponents.empty()) {
continue;
}
MetaCoreGameObject gameObject = Scene_.FindGameObject(objectData.Id);
if (!gameObject) {
continue;
}
for (const MetaCoreSerializedComponentData& componentData : objectData.SerializedComponents) {
if (MetaCoreIsNativeSceneComponentType(componentData.TypeId) || componentData.Payload.empty()) {
continue;
}
const auto* descriptor = componentRegistry->FindDescriptor(componentData.TypeId);
if (descriptor == nullptr || !descriptor->PasteComponentPayload) {
AddConsoleMessage(
MetaCoreLogLevel::Warning,
"Scene",
"Skipped serialized component without registered C++ type: " + componentData.TypeId
);
continue;
}
if (!descriptor->PasteComponentPayload(
gameObject,
std::span<const std::byte>(componentData.Payload.data(), componentData.Payload.size()),
typeRegistry)) {
AddConsoleMessage(
MetaCoreLogLevel::Warning,
"Scene",
"Failed to restore serialized component: " + componentData.TypeId
);
}
}
}
}
void MetaCoreEditorContext::NormalizeSelection() { void MetaCoreEditorContext::NormalizeSelection() {
std::vector<MetaCoreId> sanitizedSelection; std::vector<MetaCoreId> sanitizedSelection;
sanitizedSelection.reserve(SelectedObjectIds_.size()); sanitizedSelection.reserve(SelectedObjectIds_.size());
@ -999,7 +1110,7 @@ void MetaCoreEditorContext::NotifySelectionChanged() {
void MetaCoreEditorContext::RefreshSceneDirtyState() { void MetaCoreEditorContext::RefreshSceneDirtyState() {
if (const auto scenePersistenceService = ModuleRegistry_.ResolveService<MetaCoreIScenePersistenceService>(); if (const auto scenePersistenceService = ModuleRegistry_.ResolveService<MetaCoreIScenePersistenceService>();
scenePersistenceService != nullptr) { scenePersistenceService != nullptr) {
scenePersistenceService->UpdateDirtyState(Scene_.CaptureSnapshot()); scenePersistenceService->UpdateDirtyState(CaptureStateSnapshot().SceneSnapshot);
} }
} }

View File

@ -207,6 +207,8 @@ public:
[[nodiscard]] bool HasDockLayoutBuilt() const; [[nodiscard]] bool HasDockLayoutBuilt() const;
private: private:
void CaptureSerializedComponents(MetaCoreSceneSnapshot& snapshot) const;
void RestoreSerializedComponents(const MetaCoreSceneSnapshot& snapshot);
void NormalizeSelection(); void NormalizeSelection();
void NotifySelectionChanged(); void NotifySelectionChanged();
void RefreshSceneDirtyState(); void RefreshSceneDirtyState();

View File

@ -351,6 +351,7 @@ public:
[[nodiscard]] virtual bool CopyComponent(std::string_view typeId, const MetaCoreGameObject& gameObject) = 0; [[nodiscard]] virtual bool CopyComponent(std::string_view typeId, const MetaCoreGameObject& gameObject) = 0;
[[nodiscard]] virtual bool CanPasteComponent(std::string_view typeId) const = 0; [[nodiscard]] virtual bool CanPasteComponent(std::string_view typeId) const = 0;
[[nodiscard]] virtual bool PasteComponent(std::string_view typeId, MetaCoreGameObject& gameObject) const = 0; [[nodiscard]] virtual bool PasteComponent(std::string_view typeId, MetaCoreGameObject& gameObject) const = 0;
virtual void DrawComponentGizmos(MetaCoreEditorContext& editorContext) = 0;
}; };
enum class MetaCorePlayModeState { enum class MetaCorePlayModeState {

View File

@ -46,6 +46,7 @@ struct MetaCoreFieldEditorMetadata {
std::string Tooltip{}; std::string Tooltip{};
std::string RawSpec{}; std::string RawSpec{};
std::string ResourceType{}; std::string ResourceType{};
std::string DefaultValue{};
std::optional<double> Min{}; std::optional<double> Min{};
std::optional<double> Max{}; std::optional<double> Max{};
std::optional<double> Step{}; std::optional<double> Step{};

View File

@ -32,6 +32,47 @@ static MetaCoreAssetGuid StringToGuid(const std::string& str) {
return str.empty() ? MetaCoreAssetGuid{} : MetaCoreAssetGuid::Parse(str).value_or(MetaCoreAssetGuid{}); return str.empty() ? MetaCoreAssetGuid{} : MetaCoreAssetGuid::Parse(str).value_or(MetaCoreAssetGuid{});
} }
static char MetaCoreHexDigit(std::byte value) {
constexpr char digits[] = "0123456789abcdef";
return digits[static_cast<unsigned int>(value) & 0x0F];
}
static std::string BytesToHexString(const std::vector<std::byte>& bytes) {
std::string encoded;
encoded.reserve(bytes.size() * 2U);
for (std::byte byte : bytes) {
encoded.push_back(MetaCoreHexDigit(byte >> 4));
encoded.push_back(MetaCoreHexDigit(byte));
}
return encoded;
}
static int HexValue(char character) {
if (character >= '0' && character <= '9') return character - '0';
if (character >= 'a' && character <= 'f') return 10 + character - 'a';
if (character >= 'A' && character <= 'F') return 10 + character - 'A';
return -1;
}
static std::vector<std::byte> HexStringToBytes(const std::string& encoded) {
std::vector<std::byte> bytes;
if (encoded.size() % 2U != 0U) {
return bytes;
}
bytes.reserve(encoded.size() / 2U);
for (std::size_t index = 0; index < encoded.size(); index += 2U) {
const int high = HexValue(encoded[index]);
const int low = HexValue(encoded[index + 1U]);
if (high < 0 || low < 0) {
bytes.clear();
return bytes;
}
bytes.push_back(static_cast<std::byte>((high << 4) | low));
}
return bytes;
}
// 辅助方法:将单个 MetaCoreGameObjectData 转换为 json 格式 // 辅助方法:将单个 MetaCoreGameObjectData 转换为 json 格式
static json GameObjectDataToJson(const MetaCoreGameObjectData& obj, const MetaCoreTypeRegistry& registry) { static json GameObjectDataToJson(const MetaCoreGameObjectData& obj, const MetaCoreTypeRegistry& registry) {
json objJson = json::object(); json objJson = json::object();
@ -131,6 +172,20 @@ static json GameObjectDataToJson(const MetaCoreGameObjectData& obj, const MetaCo
} }
objJson["PrefabInstance"] = prefabJson; objJson["PrefabInstance"] = prefabJson;
} }
} else if (objField.Name == "SerializedComponents" && !obj.SerializedComponents.empty()) {
json componentsJson = json::array();
for (const MetaCoreSerializedComponentData& component : obj.SerializedComponents) {
if (component.TypeId.empty() || component.Payload.empty()) {
continue;
}
componentsJson.push_back(json{
{"TypeId", component.TypeId},
{"Payload", BytesToHexString(component.Payload)}
});
}
if (!componentsJson.empty()) {
objJson["Components"] = std::move(componentsJson);
}
} }
} }
return objJson; return objJson;
@ -239,6 +294,18 @@ static MetaCoreGameObjectData JsonToGameObjectData(const json& objJson, const Me
} }
obj.PrefabInstance = prefab; obj.PrefabInstance = prefab;
} }
} else if (objField.Name == "SerializedComponents" && objJson.contains("Components") && objJson["Components"].is_array()) {
for (const auto& componentJson : objJson["Components"]) {
if (!componentJson.contains("TypeId") || !componentJson.contains("Payload")) {
continue;
}
MetaCoreSerializedComponentData component{};
component.TypeId = componentJson["TypeId"].get<std::string>();
component.Payload = HexStringToBytes(componentJson["Payload"].get<std::string>());
if (!component.TypeId.empty() && !component.Payload.empty()) {
obj.SerializedComponents.push_back(std::move(component));
}
}
} }
} }
return obj; return obj;

View File

@ -47,7 +47,7 @@ MC_STRUCT()
struct MetaCoreCameraComponent { struct MetaCoreCameraComponent {
MC_GENERATED_BODY() MC_GENERATED_BODY()
MC_PROPERTY() MC_PROPERTY(DisplayName = "Field Of View", Group = "Camera", Min = 1.0, Max = 179.0, Step = 1.0, DefaultValue = 60.0)
float FieldOfViewDegrees = 60.0F; float FieldOfViewDegrees = 60.0F;
MC_PROPERTY() MC_PROPERTY()
float NearClip = 0.1F; float NearClip = 0.1F;

View File

@ -6,8 +6,10 @@
#include "MetaCoreScene/MetaCoreComponents.h" #include "MetaCoreScene/MetaCoreComponents.h"
#include <entt/entt.hpp> #include <entt/entt.hpp>
#include <cstddef>
#include <optional> #include <optional>
#include <string> #include <string>
#include <vector>
namespace MetaCore { namespace MetaCore {
@ -49,6 +51,17 @@ struct MetaCoreNameComponent {
std::string Name; std::string Name;
}; };
MC_STRUCT()
struct MetaCoreSerializedComponentData {
MC_GENERATED_BODY()
MC_PROPERTY()
std::string TypeId{};
MC_PROPERTY()
std::vector<std::byte> Payload{};
};
// The ECS handle // The ECS handle
class MetaCoreGameObject { class MetaCoreGameObject {
public: public:
@ -129,6 +142,8 @@ struct MetaCoreGameObjectData {
std::optional<MetaCorePrefabInstanceMetadata> PrefabInstance; std::optional<MetaCorePrefabInstanceMetadata> PrefabInstance;
MC_PROPERTY() MC_PROPERTY()
std::optional<MetaCoreModelRootTag> ModelRootTag; std::optional<MetaCoreModelRootTag> ModelRootTag;
MC_PROPERTY()
std::vector<MetaCoreSerializedComponentData> SerializedComponents{};
}; };
} // namespace MetaCore } // namespace MetaCore

View File

@ -48,6 +48,90 @@
namespace { namespace {
struct MetaCoreSmokeScriptComponent {
float Speed = 2.5F;
bool Enabled = true;
std::string Target = "Player";
MetaCore::MetaCoreMeshAlphaMode AlphaMode = MetaCore::MetaCoreMeshAlphaMode::Opaque;
};
void MetaCoreExpect(bool condition, const char* message);
const MetaCore::MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor* MetaCoreRegisterSmokeScriptComponentForTesting(
MetaCore::MetaCoreIComponentTypeRegistry& componentRegistry,
MetaCore::MetaCoreIReflectionRegistry& reflectionRegistry,
std::string typeId = "SmokeScript"
) {
MetaCore::MetaCoreFieldEditorMetadata speedMetadata{};
speedMetadata.DisplayName = "Speed";
speedMetadata.Group = "Script";
speedMetadata.Min = 0.0;
speedMetadata.Max = 10.0;
speedMetadata.Step = 0.25;
speedMetadata.DefaultValue = "2.5";
MetaCore::MetaCoreFieldEditorMetadata targetMetadata{};
targetMetadata.DisplayName = "Target";
targetMetadata.Group = "Script";
targetMetadata.DefaultValue = "Player";
auto smokeScriptBuilder = MetaCore::MetaCoreRegisterGeneratedStruct<MetaCoreSmokeScriptComponent>(
reflectionRegistry.AccessTypeRegistry(),
"MetaCoreSmokeScriptComponent"
);
smokeScriptBuilder
.Field<&MetaCoreSmokeScriptComponent::Speed>("Speed", speedMetadata)
.Field<&MetaCoreSmokeScriptComponent::Enabled>("Enabled")
.Field<&MetaCoreSmokeScriptComponent::Target>("Target", targetMetadata)
.Field<&MetaCoreSmokeScriptComponent::AlphaMode>("AlphaMode");
const MetaCore::MetaCoreStructDescriptor* smokeScriptReflection =
reflectionRegistry.GetTypeRegistry().FindStruct<MetaCoreSmokeScriptComponent>();
MetaCoreExpect(smokeScriptReflection != nullptr, "Static C++ smoke script reflection should be registered");
MetaCore::MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor smokeScriptDescriptor{};
smokeScriptDescriptor.TypeId = std::move(typeId);
smokeScriptDescriptor.DisplayName = "Smoke Script";
smokeScriptDescriptor.ReflectedType = smokeScriptReflection;
smokeScriptDescriptor.MutableComponent = [](MetaCore::MetaCoreGameObject& gameObject) -> void* {
return gameObject.HasComponent<MetaCoreSmokeScriptComponent>() ? &gameObject.GetComponent<MetaCoreSmokeScriptComponent>() : nullptr;
};
smokeScriptDescriptor.ConstComponent = [](const MetaCore::MetaCoreGameObject& gameObject) -> const void* {
return gameObject.HasComponent<MetaCoreSmokeScriptComponent>() ? &gameObject.GetComponent<MetaCoreSmokeScriptComponent>() : nullptr;
};
smokeScriptDescriptor.HasComponent = [](const MetaCore::MetaCoreGameObject& gameObject) {
return gameObject.HasComponent<MetaCoreSmokeScriptComponent>();
};
smokeScriptDescriptor.AddComponent = [](MetaCore::MetaCoreGameObject& gameObject) {
if (gameObject.HasComponent<MetaCoreSmokeScriptComponent>()) {
return false;
}
gameObject.AddComponent<MetaCoreSmokeScriptComponent>();
return true;
};
smokeScriptDescriptor.RemoveComponent = [](MetaCore::MetaCoreGameObject& gameObject) {
if (!gameObject.HasComponent<MetaCoreSmokeScriptComponent>()) {
return false;
}
gameObject.RemoveComponent<MetaCoreSmokeScriptComponent>();
return true;
};
smokeScriptDescriptor.ResetComponent = [](MetaCore::MetaCoreGameObject& gameObject) {
if (!gameObject.HasComponent<MetaCoreSmokeScriptComponent>()) {
return false;
}
gameObject.GetComponent<MetaCoreSmokeScriptComponent>() = MetaCoreSmokeScriptComponent{};
return true;
};
const std::string registeredTypeId = smokeScriptDescriptor.TypeId;
MetaCoreExpect(
componentRegistry.RegisterComponentDescriptor(std::move(smokeScriptDescriptor)),
"Component registry should accept a static C++ smoke script descriptor"
);
return componentRegistry.FindDescriptor(registeredTypeId);
}
void MetaCoreExpect(bool condition, const char* message) { void MetaCoreExpect(bool condition, const char* message) {
if (!condition) { if (!condition) {
std::cerr << "MetaCoreSmokeTests failed: " << message << '\n'; std::cerr << "MetaCoreSmokeTests failed: " << message << '\n';
@ -662,6 +746,121 @@ void MetaCoreTestComponentRegistryDescriptors() {
"Registered component should support reflected field mutation" "Registered component should support reflected field mutation"
); );
MetaCore::MetaCoreFieldEditorMetadata speedMetadata{};
speedMetadata.DisplayName = "Speed";
speedMetadata.Group = "Script";
speedMetadata.Min = 0.0;
speedMetadata.Max = 10.0;
speedMetadata.Step = 0.25;
speedMetadata.DefaultValue = "2.5";
MetaCore::MetaCoreFieldEditorMetadata targetMetadata{};
targetMetadata.DisplayName = "Target";
targetMetadata.Group = "Script";
targetMetadata.DefaultValue = "Player";
auto smokeScriptBuilder = MetaCore::MetaCoreRegisterGeneratedStruct<MetaCoreSmokeScriptComponent>(
reflectionRegistry->AccessTypeRegistry(),
"MetaCoreSmokeScriptComponent"
);
smokeScriptBuilder
.Field<&MetaCoreSmokeScriptComponent::Speed>("Speed", speedMetadata)
.Field<&MetaCoreSmokeScriptComponent::Enabled>("Enabled")
.Field<&MetaCoreSmokeScriptComponent::Target>("Target", targetMetadata)
.Field<&MetaCoreSmokeScriptComponent::AlphaMode>("AlphaMode");
const MetaCore::MetaCoreStructDescriptor* smokeScriptReflection =
reflectionRegistry->GetTypeRegistry().FindStruct<MetaCoreSmokeScriptComponent>();
MetaCoreExpect(smokeScriptReflection != nullptr, "Static C++ smoke script reflection should be registered");
MetaCore::MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor smokeScriptDescriptor{};
smokeScriptDescriptor.TypeId = "SmokeScript";
smokeScriptDescriptor.DisplayName = "Smoke Script";
smokeScriptDescriptor.ReflectedType = smokeScriptReflection;
smokeScriptDescriptor.MutableComponent = [](MetaCore::MetaCoreGameObject& gameObject) -> void* {
return gameObject.HasComponent<MetaCoreSmokeScriptComponent>() ? &gameObject.GetComponent<MetaCoreSmokeScriptComponent>() : nullptr;
};
smokeScriptDescriptor.ConstComponent = [](const MetaCore::MetaCoreGameObject& gameObject) -> const void* {
return gameObject.HasComponent<MetaCoreSmokeScriptComponent>() ? &gameObject.GetComponent<MetaCoreSmokeScriptComponent>() : nullptr;
};
smokeScriptDescriptor.HasComponent = [](const MetaCore::MetaCoreGameObject& gameObject) {
return gameObject.HasComponent<MetaCoreSmokeScriptComponent>();
};
smokeScriptDescriptor.AddComponent = [](MetaCore::MetaCoreGameObject& gameObject) {
if (gameObject.HasComponent<MetaCoreSmokeScriptComponent>()) {
return false;
}
gameObject.AddComponent<MetaCoreSmokeScriptComponent>();
return true;
};
smokeScriptDescriptor.RemoveComponent = [](MetaCore::MetaCoreGameObject& gameObject) {
if (!gameObject.HasComponent<MetaCoreSmokeScriptComponent>()) {
return false;
}
gameObject.RemoveComponent<MetaCoreSmokeScriptComponent>();
return true;
};
smokeScriptDescriptor.ResetComponent = [](MetaCore::MetaCoreGameObject& gameObject) {
if (!gameObject.HasComponent<MetaCoreSmokeScriptComponent>()) {
return false;
}
gameObject.GetComponent<MetaCoreSmokeScriptComponent>() = MetaCoreSmokeScriptComponent{};
return true;
};
MetaCoreExpect(
componentRegistry->RegisterComponentDescriptor(std::move(smokeScriptDescriptor)),
"Component registry should accept a static C++ smoke script descriptor"
);
const auto* registeredSmokeScriptDescriptor = componentRegistry->FindDescriptor("SmokeScript");
MetaCoreExpect(registeredSmokeScriptDescriptor != nullptr, "Static C++ smoke script should be discoverable");
MetaCoreExpect(registeredSmokeScriptDescriptor->Category == "Scripts", "Static C++ smoke script should default to Scripts category");
MetaCoreExpect(
static_cast<bool>(registeredSmokeScriptDescriptor->CopyComponentPayload),
"Static C++ smoke script should receive default reflected copy support"
);
MetaCoreExpect(
static_cast<bool>(registeredSmokeScriptDescriptor->PasteComponentPayload),
"Static C++ smoke script should receive default reflected paste support"
);
MetaCoreExpect(registeredSmokeScriptDescriptor->AddComponent(object), "Static C++ smoke script should be addable");
auto& smokeScript = object.GetComponent<MetaCoreSmokeScriptComponent>();
smokeScript.Speed = 6.75F;
smokeScript.Enabled = false;
smokeScript.Target = "Valve_A";
smokeScript.AlphaMode = MetaCore::MetaCoreMeshAlphaMode::Blend;
const auto speedFieldIterator = std::find_if(
registeredSmokeScriptDescriptor->ReflectedType->Fields.begin(),
registeredSmokeScriptDescriptor->ReflectedType->Fields.end(),
[](const MetaCore::MetaCoreFieldDescriptor& field) {
return field.Name == "Speed";
}
);
MetaCoreExpect(
speedFieldIterator != registeredSmokeScriptDescriptor->ReflectedType->Fields.end(),
"Static C++ smoke script should expose Speed field"
);
MetaCoreExpect(speedFieldIterator->Editor.DefaultValue == "2.5", "Static C++ smoke script Speed should preserve default metadata");
auto* reflectedSpeed = static_cast<float*>(speedFieldIterator->MutableValue(registeredSmokeScriptDescriptor->MutableComponent(object)));
*reflectedSpeed = 7.25F;
MetaCoreExpect(std::abs(object.GetComponent<MetaCoreSmokeScriptComponent>().Speed - 7.25F) <= 0.0001F, "Static C++ smoke script field mutation should work");
MetaCoreExpect(componentRegistry->CopyComponent("SmokeScript", object), "Static C++ smoke script should copy through default reflected payload");
MetaCore::MetaCoreGameObject scriptPasteTarget = scene.CreateGameObject("SmokeScriptPasteTarget");
MetaCoreExpect(!scriptPasteTarget.HasComponent<MetaCoreSmokeScriptComponent>(), "Paste target should not start with smoke script");
MetaCoreExpect(componentRegistry->PasteComponent("SmokeScript", scriptPasteTarget), "Static C++ smoke script should paste through default reflected payload");
MetaCoreExpect(scriptPasteTarget.HasComponent<MetaCoreSmokeScriptComponent>(), "Paste should add static C++ smoke script to target");
const auto& pastedSmokeScript = scriptPasteTarget.GetComponent<MetaCoreSmokeScriptComponent>();
MetaCoreExpect(std::abs(pastedSmokeScript.Speed - 7.25F) <= 0.0001F, "Pasted static C++ smoke script Speed should match");
MetaCoreExpect(!pastedSmokeScript.Enabled, "Pasted static C++ smoke script Enabled should match");
MetaCoreExpect(pastedSmokeScript.Target == "Valve_A", "Pasted static C++ smoke script Target should match");
MetaCoreExpect(pastedSmokeScript.AlphaMode == MetaCore::MetaCoreMeshAlphaMode::Blend, "Pasted static C++ smoke script enum should match");
MetaCoreExpect(registeredSmokeScriptDescriptor->ResetComponent(scriptPasteTarget), "Static C++ smoke script should reset");
MetaCoreExpect(std::abs(scriptPasteTarget.GetComponent<MetaCoreSmokeScriptComponent>().Speed - 2.5F) <= 0.0001F, "Reset static C++ smoke script Speed should return to default");
MetaCoreExpect(scriptPasteTarget.GetComponent<MetaCoreSmokeScriptComponent>().Target == "Player", "Reset static C++ smoke script Target should return to default");
coreServicesModule->Shutdown(moduleRegistry); coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices(); moduleRegistry.ShutdownServices();
} }
@ -706,6 +905,27 @@ void MetaCoreTestReflectionFieldEditorDescriptors() {
const auto* constPosition = static_cast<const glm::vec3*>(positionField->ConstValue(&transform)); const auto* constPosition = static_cast<const glm::vec3*>(positionField->ConstValue(&transform));
MetaCoreExpectVec3Near(*constPosition, glm::vec3(4.0F, 5.0F, 6.0F), "ConstValue should read the field"); MetaCoreExpectVec3Near(*constPosition, glm::vec3(4.0F, 5.0F, 6.0F), "ConstValue should read the field");
const MetaCore::MetaCoreStructDescriptor* cameraDescriptor =
registry.FindStruct<MetaCore::MetaCoreCameraComponent>();
MetaCoreExpect(cameraDescriptor != nullptr, "Camera reflection descriptor should exist");
const auto fovFieldIterator = std::find_if(
cameraDescriptor->Fields.begin(),
cameraDescriptor->Fields.end(),
[](const MetaCore::MetaCoreFieldDescriptor& field) {
return field.Name == "FieldOfViewDegrees";
}
);
MetaCoreExpect(
fovFieldIterator != cameraDescriptor->Fields.end(),
"Camera FOV field descriptor should exist"
);
MetaCoreExpect(fovFieldIterator->Editor.DisplayName == "Field Of View", "Camera FOV should preserve display metadata");
MetaCoreExpect(fovFieldIterator->Editor.Group == "Camera", "Camera FOV should preserve group metadata");
MetaCoreExpect(fovFieldIterator->Editor.Min.has_value() && std::abs(*fovFieldIterator->Editor.Min - 1.0) <= 0.0001, "Camera FOV should preserve min metadata");
MetaCoreExpect(fovFieldIterator->Editor.Max.has_value() && std::abs(*fovFieldIterator->Editor.Max - 179.0) <= 0.0001, "Camera FOV should preserve max metadata");
MetaCoreExpect(fovFieldIterator->Editor.Step.has_value() && std::abs(*fovFieldIterator->Editor.Step - 1.0) <= 0.0001, "Camera FOV should preserve step metadata");
MetaCoreExpect(fovFieldIterator->Editor.DefaultValue == "60.0", "Camera FOV should preserve default value metadata");
const MetaCore::MetaCoreEnumDescriptor* alphaModeEnum = const MetaCore::MetaCoreEnumDescriptor* alphaModeEnum =
registry.FindEnum<MetaCore::MetaCoreMeshAlphaMode>(); registry.FindEnum<MetaCore::MetaCoreMeshAlphaMode>();
MetaCoreExpect(alphaModeEnum != nullptr, "Alpha mode enum reflection descriptor should exist"); MetaCoreExpect(alphaModeEnum != nullptr, "Alpha mode enum reflection descriptor should exist");
@ -782,6 +1002,8 @@ void MetaCoreTestPlayModeLifecycleAndSceneIsolation() {
int startCount = 0; int startCount = 0;
int updateCount = 0; int updateCount = 0;
int destroyCount = 0; int destroyCount = 0;
int drawGizmosCount = 0;
int drawSelectedGizmosCount = 0;
MetaCore::MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor lifecycleDescriptor{}; MetaCore::MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor lifecycleDescriptor{};
lifecycleDescriptor.TypeId = "LifecycleModelRootTag"; lifecycleDescriptor.TypeId = "LifecycleModelRootTag";
@ -822,6 +1044,12 @@ void MetaCoreTestPlayModeLifecycleAndSceneIsolation() {
lifecycleDescriptor.Lifecycle.OnDestroy = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject&) { lifecycleDescriptor.Lifecycle.OnDestroy = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject&) {
++destroyCount; ++destroyCount;
}; };
lifecycleDescriptor.Lifecycle.OnDrawGizmos = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject&) {
++drawGizmosCount;
};
lifecycleDescriptor.Lifecycle.OnDrawGizmosSelected = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject&) {
++drawSelectedGizmosCount;
};
MetaCoreExpect( MetaCoreExpect(
componentRegistry->RegisterComponentDescriptor(std::move(lifecycleDescriptor)), componentRegistry->RegisterComponentDescriptor(std::move(lifecycleDescriptor)),
@ -829,6 +1057,10 @@ void MetaCoreTestPlayModeLifecycleAndSceneIsolation() {
); );
MetaCoreExpect(playModeService->GetState() == MetaCore::MetaCorePlayModeState::Edit, "Initial play mode state should be Edit"); MetaCoreExpect(playModeService->GetState() == MetaCore::MetaCorePlayModeState::Edit, "Initial play mode state should be Edit");
componentRegistry->DrawComponentGizmos(editorContext);
MetaCoreExpect(drawGizmosCount == 1, "OnDrawGizmos should run for registered static C++ components");
MetaCoreExpect(drawSelectedGizmosCount == 1, "OnDrawGizmosSelected should run for selected registered static C++ components");
MetaCoreExpect(playModeService->EnterPlayMode(editorContext), "EnterPlayMode should succeed"); MetaCoreExpect(playModeService->EnterPlayMode(editorContext), "EnterPlayMode should succeed");
MetaCoreExpect(playModeService->GetState() == MetaCore::MetaCorePlayModeState::Playing, "Play mode state should be Playing"); MetaCoreExpect(playModeService->GetState() == MetaCore::MetaCorePlayModeState::Playing, "Play mode state should be Playing");
MetaCoreExpect(startCount == 1, "OnStart should run once on enter play mode"); MetaCoreExpect(startCount == 1, "OnStart should run once on enter play mode");
@ -1172,6 +1404,135 @@ void MetaCoreTestJsonSceneSaveCurrentSceneUsesMcsceneJson() {
std::filesystem::remove_all(tempProjectRoot); std::filesystem::remove_all(tempProjectRoot);
} }
void MetaCoreTestStaticCppComponentScenePersistenceAndUndo() {
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCoreStaticCppComponentSceneProject";
std::filesystem::remove_all(tempProjectRoot);
std::filesystem::create_directories(tempProjectRoot / "Assets");
std::filesystem::create_directories(tempProjectRoot / "Scenes");
std::filesystem::create_directories(tempProjectRoot / "Library");
{
std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc);
projectFile << "{\n"
<< " \"name\": \"StaticCppComponentProject\",\n"
<< " \"version\": \"0.1.0\",\n"
<< " \"scenes\": [],\n"
<< " \"startup_scene\": \"\"\n"
<< "}\n";
}
_putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str());
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
const auto componentRegistry = moduleRegistry.ResolveService<MetaCore::MetaCoreIComponentTypeRegistry>();
const auto reflectionRegistry = moduleRegistry.ResolveService<MetaCore::MetaCoreIReflectionRegistry>();
const auto scenePersistenceService = moduleRegistry.ResolveService<MetaCore::MetaCoreIScenePersistenceService>();
MetaCoreExpect(componentRegistry != nullptr, "Component registry should be available for static C++ persistence");
MetaCoreExpect(reflectionRegistry != nullptr, "Reflection registry should be available for static C++ persistence");
MetaCoreExpect(scenePersistenceService != nullptr, "Scene persistence should be available for static C++ persistence");
const auto* smokeScriptDescriptor = MetaCoreRegisterSmokeScriptComponentForTesting(
*componentRegistry,
*reflectionRegistry,
"PersistentSmokeScript"
);
MetaCoreExpect(smokeScriptDescriptor != nullptr, "Persistent smoke script should be registered");
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
MetaCore::MetaCoreGameObject object = scene.CreateGameObject("ScriptedValve");
const MetaCore::MetaCoreId objectId = object.GetId();
MetaCoreExpect(smokeScriptDescriptor->AddComponent(object), "Static C++ component should be addable before save");
object.GetComponent<MetaCoreSmokeScriptComponent>().Speed = 4.5F;
object.GetComponent<MetaCoreSmokeScriptComponent>().Enabled = false;
object.GetComponent<MetaCoreSmokeScriptComponent>().Target = "Valve_A";
object.GetComponent<MetaCoreSmokeScriptComponent>().AlphaMode = MetaCore::MetaCoreMeshAlphaMode::Blend;
editorContext.SelectOnly(objectId);
const std::filesystem::path scenePath = std::filesystem::path("Scenes") / "StaticCppComponent.mcscene.json";
MetaCoreExpect(scenePersistenceService->SaveSceneAs(editorContext, scenePath), "Static C++ component scene should save");
MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "Static C++ component scene should be clean after save");
const auto loadedDocument = MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson(
tempProjectRoot / scenePath,
reflectionRegistry->GetTypeRegistry()
);
MetaCoreExpect(loadedDocument.has_value(), "Saved static C++ component scene should parse");
const auto documentObjectIterator = std::find_if(
loadedDocument->GameObjects.begin(),
loadedDocument->GameObjects.end(),
[objectId](const MetaCore::MetaCoreGameObjectData& gameObjectData) {
return gameObjectData.Id == objectId;
}
);
MetaCoreExpect(documentObjectIterator != loadedDocument->GameObjects.end(), "Saved scene document should include scripted object");
const bool hasSerializedSmokeScript = std::any_of(
documentObjectIterator->SerializedComponents.begin(),
documentObjectIterator->SerializedComponents.end(),
[](const MetaCore::MetaCoreSerializedComponentData& componentData) {
return componentData.TypeId == "PersistentSmokeScript" && !componentData.Payload.empty();
}
);
MetaCoreExpect(hasSerializedSmokeScript, "Saved scene document should include serialized static C++ component payload");
scene.RestoreSnapshot(MetaCore::MetaCoreSceneSnapshot{});
editorContext.ClearSelection();
MetaCoreExpect(scene.GetGameObjects().empty(), "Scene should be empty before static C++ component reload");
MetaCoreExpect(scenePersistenceService->LoadScene(editorContext, scenePath), "Static C++ component scene should reload");
MetaCore::MetaCoreGameObject reloadedObject = scene.FindGameObject(objectId);
MetaCoreExpect(reloadedObject, "Reloaded scripted object should exist");
MetaCoreExpect(reloadedObject.HasComponent<MetaCoreSmokeScriptComponent>(), "Reloaded object should restore static C++ component");
MetaCoreExpect(std::abs(reloadedObject.GetComponent<MetaCoreSmokeScriptComponent>().Speed - 4.5F) <= 0.0001F, "Reloaded static C++ Speed should match");
MetaCoreExpect(!reloadedObject.GetComponent<MetaCoreSmokeScriptComponent>().Enabled, "Reloaded static C++ Enabled should match");
MetaCoreExpect(reloadedObject.GetComponent<MetaCoreSmokeScriptComponent>().Target == "Valve_A", "Reloaded static C++ Target should match");
MetaCoreExpect(reloadedObject.GetComponent<MetaCoreSmokeScriptComponent>().AlphaMode == MetaCore::MetaCoreMeshAlphaMode::Blend, "Reloaded static C++ enum should match");
const bool modified = editorContext.ExecuteSnapshotCommand("Modify static C++ component", [&]() {
MetaCore::MetaCoreGameObject editedObject = scene.FindGameObject(objectId);
if (!editedObject || !editedObject.HasComponent<MetaCoreSmokeScriptComponent>()) {
return false;
}
editedObject.GetComponent<MetaCoreSmokeScriptComponent>().Speed = 9.0F;
editedObject.GetComponent<MetaCoreSmokeScriptComponent>().Target = "Valve_B";
return true;
});
MetaCoreExpect(modified, "Static C++ component field mutation should create an undoable command");
MetaCoreExpect(scenePersistenceService->IsSceneDirty(), "Static C++ component field mutation should mark scene dirty");
MetaCoreExpect(std::abs(scene.FindGameObject(objectId).GetComponent<MetaCoreSmokeScriptComponent>().Speed - 9.0F) <= 0.0001F, "Static C++ mutation should apply");
MetaCoreExpect(editorContext.UndoCommand(), "Undo static C++ component mutation should succeed");
MetaCoreExpect(scene.FindGameObject(objectId).HasComponent<MetaCoreSmokeScriptComponent>(), "Undo should keep static C++ component present");
MetaCoreExpect(std::abs(scene.FindGameObject(objectId).GetComponent<MetaCoreSmokeScriptComponent>().Speed - 4.5F) <= 0.0001F, "Undo should restore static C++ Speed");
MetaCoreExpect(scene.FindGameObject(objectId).GetComponent<MetaCoreSmokeScriptComponent>().Target == "Valve_A", "Undo should restore static C++ Target");
MetaCoreExpect(editorContext.RedoCommand(), "Redo static C++ component mutation should succeed");
MetaCoreExpect(std::abs(scene.FindGameObject(objectId).GetComponent<MetaCoreSmokeScriptComponent>().Speed - 9.0F) <= 0.0001F, "Redo should restore edited static C++ Speed");
MetaCoreExpect(scene.FindGameObject(objectId).GetComponent<MetaCoreSmokeScriptComponent>().Target == "Valve_B", "Redo should restore edited static C++ Target");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
std::filesystem::remove_all(tempProjectRoot);
}
void MetaCoreTestImportPipelineAndCook() { void MetaCoreTestImportPipelineAndCook() {
const unsigned char ppmDataV1[] = { const unsigned char ppmDataV1[] = {
0x50, 0x36, 0x0A, 0x31, 0x20, 0x31, 0x0A, 0x32, 0x50, 0x36, 0x0A, 0x31, 0x20, 0x31, 0x0A, 0x32,
@ -5169,6 +5530,8 @@ int main() {
MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot(); MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot();
std::cout << "[RUN] MetaCoreTestJsonSceneSaveCurrentSceneUsesMcsceneJson..." << std::endl; std::cout << "[RUN] MetaCoreTestJsonSceneSaveCurrentSceneUsesMcsceneJson..." << std::endl;
MetaCoreTestJsonSceneSaveCurrentSceneUsesMcsceneJson(); MetaCoreTestJsonSceneSaveCurrentSceneUsesMcsceneJson();
std::cout << "[RUN] MetaCoreTestStaticCppComponentScenePersistenceAndUndo..." << std::endl;
MetaCoreTestStaticCppComponentScenePersistenceAndUndo();
std::cout << "[RUN] MetaCoreTestImportPipelineAndCook..." << std::endl; std::cout << "[RUN] MetaCoreTestImportPipelineAndCook..." << std::endl;
MetaCoreTestImportPipelineAndCook(); MetaCoreTestImportPipelineAndCook();
std::cout << "[RUN] MetaCoreTestProjectDescriptorAndGltfImporterSkeleton..." << std::endl; std::cout << "[RUN] MetaCoreTestProjectDescriptorAndGltfImporterSkeleton..." << std::endl;

View File

@ -402,6 +402,8 @@ void MetaCoreEmitBoolMetadataAssignment(
MetaCoreEmitStringMetadataAssignment(output, metadata, metadataVariable, "Tooltip", "Tooltip"); MetaCoreEmitStringMetadataAssignment(output, metadata, metadataVariable, "Tooltip", "Tooltip");
MetaCoreEmitStringMetadataAssignment(output, metadata, metadataVariable, "ResourceType", "ResourceType"); MetaCoreEmitStringMetadataAssignment(output, metadata, metadataVariable, "ResourceType", "ResourceType");
MetaCoreEmitStringMetadataAssignment(output, metadata, metadataVariable, "Resource", "ResourceType"); MetaCoreEmitStringMetadataAssignment(output, metadata, metadataVariable, "Resource", "ResourceType");
MetaCoreEmitStringMetadataAssignment(output, metadata, metadataVariable, "DefaultValue", "DefaultValue");
MetaCoreEmitStringMetadataAssignment(output, metadata, metadataVariable, "Default", "DefaultValue");
MetaCoreEmitOptionalDoubleMetadataAssignment(output, metadata, metadataVariable, "Min", "Min"); MetaCoreEmitOptionalDoubleMetadataAssignment(output, metadata, metadataVariable, "Min", "Min");
MetaCoreEmitOptionalDoubleMetadataAssignment(output, metadata, metadataVariable, "Max", "Max"); MetaCoreEmitOptionalDoubleMetadataAssignment(output, metadata, metadataVariable, "Max", "Max");
MetaCoreEmitOptionalDoubleMetadataAssignment(output, metadata, metadataVariable, "Step", "Step"); MetaCoreEmitOptionalDoubleMetadataAssignment(output, metadata, metadataVariable, "Step", "Step");