Prepare script lifecycle and data layer

This commit is contained in:
2026-03-26 20:14:58 +08:00
parent 5ca5ca1f19
commit 0921f2a459
20 changed files with 1367 additions and 26 deletions

View File

@@ -180,6 +180,7 @@ add_library(XCEngine STATIC
${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/OpenGL/OpenGLDescriptorSet.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/OpenGL/OpenGLPipelineLayout.cpp
${CMAKE_CURRENT_SOURCE_DIR}/third_party/GLAD/src/glad.c
${CMAKE_CURRENT_SOURCE_DIR}/third_party/stb/stb_image.cpp
# RHI Factory
${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/RHIFactory.cpp
@@ -239,12 +240,22 @@ add_library(XCEngine STATIC
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/AudioClip/AudioClip.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/AudioClip/AudioLoader.cpp
# Scripting
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/ScriptField.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/ScriptFieldStorage.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/ScriptComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/ScriptField.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/ScriptFieldStorage.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/ScriptComponent.cpp
# Components
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/Component.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/TransformComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/GameObject.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/CameraComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/LightComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/MeshFilterComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/MeshRendererComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/AudioSourceComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/AudioListenerComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/ComponentFactoryRegistry.h
@@ -253,10 +264,29 @@ add_library(XCEngine STATIC
${CMAKE_CURRENT_SOURCE_DIR}/src/Components/GameObject.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Components/CameraComponent.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Components/LightComponent.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Components/MeshFilterComponent.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Components/MeshRendererComponent.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Components/AudioSourceComponent.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Components/AudioListenerComponent.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Components/ComponentFactoryRegistry.cpp
# Rendering
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderContext.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderCameraData.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/VisibleRenderObject.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderSceneExtractor.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderPipeline.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderPipelineAsset.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderSurface.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderResourceCache.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/SceneRenderer.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h
${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/RenderSurface.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/RenderSceneExtractor.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/RenderResourceCache.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/SceneRenderer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp
# Scene
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/Scene.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/SceneManager.h

View File

@@ -34,16 +34,7 @@ public:
Scene* GetScene() const;
bool IsEnabled() const { return m_enabled; }
void SetEnabled(bool enabled) {
if (m_enabled != enabled) {
m_enabled = enabled;
if (m_enabled) {
OnEnable();
} else {
OnDisable();
}
}
}
void SetEnabled(bool enabled);
protected:
Component() = default;
@@ -58,4 +49,4 @@ protected:
};
} // namespace Components
} // namespace XCEngine
} // namespace XCEngine

View File

@@ -208,10 +208,15 @@ public:
void Deserialize(std::istream& is);
private:
void NotifyComponentsBecameActive();
void NotifyComponentsBecameInactive();
void PropagateActiveInHierarchyChange(bool oldParentActiveInHierarchy, bool newParentActiveInHierarchy);
ID m_id = INVALID_ID;
uint64_t m_uuid = 0;
std::string m_name;
bool m_activeSelf = true;
bool m_started = false;
GameObject* m_parent = nullptr;
std::vector<GameObject*> m_children;

View File

@@ -0,0 +1,51 @@
#pragma once
#include <XCEngine/Components/Component.h>
#include <XCEngine/Scripting/ScriptFieldStorage.h>
#include <cstdint>
#include <string>
namespace XCEngine {
namespace Scripting {
class ScriptComponent : public Components::Component {
public:
ScriptComponent();
std::string GetName() const override { return "ScriptComponent"; }
uint64_t GetScriptComponentUUID() const { return m_scriptComponentUUID; }
const std::string& GetAssemblyName() const { return m_assemblyName; }
void SetAssemblyName(const std::string& assemblyName) { m_assemblyName = assemblyName; }
const std::string& GetNamespaceName() const { return m_namespaceName; }
void SetNamespaceName(const std::string& namespaceName) { m_namespaceName = namespaceName; }
const std::string& GetClassName() const { return m_className; }
void SetClassName(const std::string& className) { m_className = className; }
void SetScriptClass(const std::string& namespaceName, const std::string& className);
void SetScriptClass(const std::string& assemblyName, const std::string& namespaceName, const std::string& className);
bool HasScriptClass() const { return !m_className.empty(); }
std::string GetFullClassName() const;
ScriptFieldStorage& GetFieldStorage() { return m_fieldStorage; }
const ScriptFieldStorage& GetFieldStorage() const { return m_fieldStorage; }
void SetFieldStorage(const ScriptFieldStorage& fieldStorage) { m_fieldStorage = fieldStorage; }
void Serialize(std::ostream& os) const override;
void Deserialize(std::istream& is) override;
private:
uint64_t m_scriptComponentUUID = 0;
std::string m_assemblyName = "GameScripts";
std::string m_namespaceName;
std::string m_className;
ScriptFieldStorage m_fieldStorage;
};
} // namespace Scripting
} // namespace XCEngine

View File

@@ -0,0 +1,65 @@
#pragma once
#include <XCEngine/Core/Math/Vector2.h>
#include <XCEngine/Core/Math/Vector3.h>
#include <XCEngine/Core/Math/Vector4.h>
#include <cstdint>
#include <string>
#include <variant>
namespace XCEngine {
namespace Scripting {
enum class ScriptFieldType {
None = 0,
Float,
Double,
Bool,
Int32,
UInt64,
String,
Vector2,
Vector3,
Vector4,
GameObject
};
struct GameObjectReference {
uint64_t gameObjectUUID = 0;
bool operator==(const GameObjectReference& other) const {
return gameObjectUUID == other.gameObjectUUID;
}
bool operator!=(const GameObjectReference& other) const {
return !(*this == other);
}
};
using ScriptFieldValue = std::variant<
std::monostate,
float,
double,
bool,
int32_t,
uint64_t,
std::string,
Math::Vector2,
Math::Vector3,
Math::Vector4,
GameObjectReference>;
std::string ScriptFieldTypeToString(ScriptFieldType type);
bool TryParseScriptFieldType(const std::string& value, ScriptFieldType& outType);
bool IsScriptFieldValueCompatible(ScriptFieldType type, const ScriptFieldValue& value);
ScriptFieldValue CreateDefaultScriptFieldValue(ScriptFieldType type);
std::string SerializeScriptFieldValue(ScriptFieldType type, const ScriptFieldValue& value);
bool TryDeserializeScriptFieldValue(ScriptFieldType type, const std::string& value, ScriptFieldValue& outValue);
std::string EscapeScriptString(const std::string& value);
std::string UnescapeScriptString(const std::string& value);
} // namespace Scripting
} // namespace XCEngine

View File

@@ -0,0 +1,124 @@
#pragma once
#include <XCEngine/Scripting/ScriptField.h>
#include <istream>
#include <ostream>
#include <string>
#include <type_traits>
#include <unordered_map>
#include <vector>
namespace XCEngine {
namespace Scripting {
struct StoredScriptField {
ScriptFieldType type = ScriptFieldType::None;
ScriptFieldValue value = std::monostate{};
};
template<typename T>
struct ScriptFieldTypeResolver;
template<>
struct ScriptFieldTypeResolver<float> {
static constexpr ScriptFieldType value = ScriptFieldType::Float;
};
template<>
struct ScriptFieldTypeResolver<double> {
static constexpr ScriptFieldType value = ScriptFieldType::Double;
};
template<>
struct ScriptFieldTypeResolver<bool> {
static constexpr ScriptFieldType value = ScriptFieldType::Bool;
};
template<>
struct ScriptFieldTypeResolver<int32_t> {
static constexpr ScriptFieldType value = ScriptFieldType::Int32;
};
template<>
struct ScriptFieldTypeResolver<uint64_t> {
static constexpr ScriptFieldType value = ScriptFieldType::UInt64;
};
template<>
struct ScriptFieldTypeResolver<std::string> {
static constexpr ScriptFieldType value = ScriptFieldType::String;
};
template<>
struct ScriptFieldTypeResolver<Math::Vector2> {
static constexpr ScriptFieldType value = ScriptFieldType::Vector2;
};
template<>
struct ScriptFieldTypeResolver<Math::Vector3> {
static constexpr ScriptFieldType value = ScriptFieldType::Vector3;
};
template<>
struct ScriptFieldTypeResolver<Math::Vector4> {
static constexpr ScriptFieldType value = ScriptFieldType::Vector4;
};
template<>
struct ScriptFieldTypeResolver<GameObjectReference> {
static constexpr ScriptFieldType value = ScriptFieldType::GameObject;
};
class ScriptFieldStorage {
public:
template<typename T>
bool SetFieldValue(const std::string& fieldName, const T& value) {
using ValueType = std::decay_t<T>;
return SetFieldValue(fieldName, ScriptFieldTypeResolver<ValueType>::value, ScriptFieldValue(ValueType(value)));
}
bool SetFieldValue(const std::string& fieldName, const char* value) {
return SetFieldValue<std::string>(fieldName, value ? std::string(value) : std::string());
}
bool SetFieldValue(const std::string& fieldName, ScriptFieldType type, const ScriptFieldValue& value);
template<typename T>
bool TryGetFieldValue(const std::string& fieldName, T& outValue) const {
const StoredScriptField* field = FindField(fieldName);
if (!field) {
return false;
}
const auto* storedValue = std::get_if<std::decay_t<T>>(&field->value);
if (!storedValue) {
return false;
}
outValue = *storedValue;
return true;
}
const StoredScriptField* FindField(const std::string& fieldName) const;
StoredScriptField* FindField(const std::string& fieldName);
bool Contains(const std::string& fieldName) const;
bool Remove(const std::string& fieldName);
void Clear();
size_t GetFieldCount() const { return m_fields.size(); }
std::vector<std::string> GetFieldNames() const;
std::string SerializeToString() const;
void DeserializeFromString(const std::string& data);
void Serialize(std::ostream& os) const;
void Deserialize(std::istream& is);
private:
std::unordered_map<std::string, StoredScriptField> m_fields;
};
} // namespace Scripting
} // namespace XCEngine

View File

@@ -8,5 +8,35 @@ TransformComponent& Component::transform() const {
return *m_gameObject->GetTransform();
}
Scene* Component::GetScene() const {
return m_gameObject ? m_gameObject->GetScene() : nullptr;
}
void Component::SetEnabled(bool enabled) {
if (m_enabled == enabled) {
return;
}
bool wasEffectivelyEnabled = m_enabled;
bool isEffectivelyEnabled = enabled;
if (m_gameObject) {
wasEffectivelyEnabled = m_enabled && m_gameObject->IsActiveInHierarchy();
isEffectivelyEnabled = enabled && m_gameObject->IsActiveInHierarchy();
}
m_enabled = enabled;
if (wasEffectivelyEnabled == isEffectivelyEnabled) {
return;
}
if (isEffectivelyEnabled) {
OnEnable();
} else {
OnDisable();
}
}
}
}
}

View File

@@ -5,6 +5,9 @@
#include "Components/CameraComponent.h"
#include "Components/GameObject.h"
#include "Components/LightComponent.h"
#include "Components/MeshFilterComponent.h"
#include "Components/MeshRendererComponent.h"
#include "Scripting/ScriptComponent.h"
namespace XCEngine {
namespace Components {
@@ -28,6 +31,9 @@ ComponentFactoryRegistry::ComponentFactoryRegistry() {
RegisterFactory("Light", &CreateBuiltInComponent<LightComponent>);
RegisterFactory("AudioSource", &CreateBuiltInComponent<AudioSourceComponent>);
RegisterFactory("AudioListener", &CreateBuiltInComponent<AudioListenerComponent>);
RegisterFactory("MeshFilter", &CreateBuiltInComponent<MeshFilterComponent>);
RegisterFactory("MeshRenderer", &CreateBuiltInComponent<MeshRendererComponent>);
RegisterFactory("ScriptComponent", &CreateBuiltInComponent<XCEngine::Scripting::ScriptComponent>);
}
void ComponentFactoryRegistry::RegisterFactory(const std::string& typeName, CreateComponentFn createFn) {

View File

@@ -43,6 +43,41 @@ std::unordered_map<GameObject::ID, GameObject*>& GameObject::GetGlobalRegistry()
return registry;
}
void GameObject::NotifyComponentsBecameActive() {
for (auto& comp : m_components) {
if (comp->IsEnabled()) {
comp->OnEnable();
}
}
}
void GameObject::NotifyComponentsBecameInactive() {
for (auto& comp : m_components) {
if (comp->IsEnabled()) {
comp->OnDisable();
}
}
}
void GameObject::PropagateActiveInHierarchyChange(bool oldParentActiveInHierarchy, bool newParentActiveInHierarchy) {
const bool wasActiveInHierarchy = oldParentActiveInHierarchy && m_activeSelf;
const bool isActiveInHierarchy = newParentActiveInHierarchy && m_activeSelf;
if (wasActiveInHierarchy != isActiveInHierarchy) {
if (isActiveInHierarchy) {
NotifyComponentsBecameActive();
} else {
NotifyComponentsBecameInactive();
}
}
for (auto* child : m_children) {
if (child) {
child->PropagateActiveInHierarchyChange(wasActiveInHierarchy, isActiveInHierarchy);
}
}
}
void GameObject::SetParent(GameObject* parent) {
SetParent(parent, true);
}
@@ -52,6 +87,9 @@ void GameObject::SetParent(GameObject* parent, bool worldPositionStays) {
return;
}
const bool oldParentActiveInHierarchy = m_parent ? m_parent->IsActiveInHierarchy() : true;
const bool wasActiveInHierarchy = oldParentActiveInHierarchy && m_activeSelf;
if (m_parent) {
auto& siblings = m_parent->m_children;
siblings.erase(std::remove(siblings.begin(), siblings.end(), this), siblings.end());
@@ -72,6 +110,21 @@ void GameObject::SetParent(GameObject* parent, bool worldPositionStays) {
}
GetTransform()->SetParent(parent ? parent->GetTransform() : nullptr, worldPositionStays);
const bool newParentActiveInHierarchy = m_parent ? m_parent->IsActiveInHierarchy() : true;
const bool isActiveInHierarchy = newParentActiveInHierarchy && m_activeSelf;
if (wasActiveInHierarchy != isActiveInHierarchy) {
if (isActiveInHierarchy) {
NotifyComponentsBecameActive();
} else {
NotifyComponentsBecameInactive();
}
for (auto* child : m_children) {
if (child) {
child->PropagateActiveInHierarchyChange(wasActiveInHierarchy, isActiveInHierarchy);
}
}
}
}
GameObject* GameObject::GetChild(size_t index) const {
@@ -101,9 +154,26 @@ void GameObject::DetachFromParent() {
}
void GameObject::SetActive(bool active) {
if (m_activeSelf != active) {
m_activeSelf = active;
if (m_parent == nullptr || m_parent->IsActiveInHierarchy()) {
if (m_activeSelf == active) {
return;
}
const bool parentActiveInHierarchy = m_parent ? m_parent->IsActiveInHierarchy() : true;
const bool wasActiveInHierarchy = parentActiveInHierarchy && m_activeSelf;
m_activeSelf = active;
const bool isActiveInHierarchy = parentActiveInHierarchy && m_activeSelf;
if (wasActiveInHierarchy != isActiveInHierarchy) {
if (isActiveInHierarchy) {
NotifyComponentsBecameActive();
} else {
NotifyComponentsBecameInactive();
}
for (auto* child : m_children) {
if (child) {
child->PropagateActiveInHierarchyChange(wasActiveInHierarchy, isActiveInHierarchy);
}
}
}
}
@@ -155,35 +225,78 @@ void GameObject::Awake() {
}
void GameObject::Start() {
for (auto& comp : m_components) {
if (comp->IsEnabled()) {
comp->Start();
if (!IsActiveInHierarchy()) {
return;
}
if (!m_started) {
for (auto& comp : m_components) {
if (comp->IsEnabled()) {
comp->Start();
}
}
m_started = true;
}
for (auto* child : m_children) {
if (child) {
child->Start();
}
}
}
void GameObject::Update(float deltaTime) {
if (!IsActiveInHierarchy()) {
return;
}
for (auto& comp : m_components) {
if (comp->IsEnabled()) {
comp->Update(deltaTime);
}
}
for (auto* child : m_children) {
if (child) {
child->Update(deltaTime);
}
}
}
void GameObject::FixedUpdate() {
if (!IsActiveInHierarchy()) {
return;
}
for (auto& comp : m_components) {
if (comp->IsEnabled()) {
comp->FixedUpdate();
}
}
for (auto* child : m_children) {
if (child) {
child->FixedUpdate();
}
}
}
void GameObject::LateUpdate(float deltaTime) {
if (!IsActiveInHierarchy()) {
return;
}
for (auto& comp : m_components) {
if (comp->IsEnabled()) {
comp->LateUpdate(deltaTime);
}
}
for (auto* child : m_children) {
if (child) {
child->LateUpdate(deltaTime);
}
}
}
void GameObject::OnDestroy() {
@@ -193,9 +306,10 @@ void GameObject::OnDestroy() {
}
void GameObject::Destroy() {
OnDestroy();
if (m_scene) {
m_scene->DestroyGameObject(this);
} else {
OnDestroy();
}
}
@@ -203,6 +317,7 @@ void GameObject::Serialize(std::ostream& os) const {
os << "name=" << m_name << ";";
os << "active=" << (m_activeSelf ? "1" : "0") << ";";
os << "id=" << m_id << ";";
os << "uuid=" << m_uuid << ";";
os << "transform=";
m_transform->Serialize(os);
os << ";";
@@ -233,6 +348,9 @@ void GameObject::Deserialize(std::istream& is) {
} else if (strcmp(key, "id") == 0) {
is >> m_id;
if (is.peek() == ';') is.get();
} else if (strcmp(key, "uuid") == 0) {
is >> m_uuid;
if (is.peek() == ';') is.get();
} else if (strcmp(key, "transform") == 0) {
m_transform->Deserialize(is);
if (is.peek() == ';') is.get();

View File

@@ -18,6 +18,7 @@ struct PendingComponentData {
struct PendingGameObjectData {
GameObject::ID id = GameObject::INVALID_ID;
uint64_t uuid = 0;
std::string name = "GameObject";
bool active = true;
GameObject::ID parentId = GameObject::INVALID_ID;
@@ -70,6 +71,7 @@ void SerializeGameObjectRecursive(std::ostream& os, GameObject* gameObject) {
os << "gameobject_begin\n";
os << "id=" << gameObject->GetID() << "\n";
os << "uuid=" << gameObject->GetUUID() << "\n";
os << "name=" << EscapeString(gameObject->GetName()) << "\n";
os << "active=" << (gameObject->IsActive() ? 1 : 0) << "\n";
os << "parent=" << (gameObject->GetParent() ? gameObject->GetParent()->GetID() : GameObject::INVALID_ID) << "\n";
@@ -219,6 +221,7 @@ std::vector<GameObject*> Scene::GetRootGameObjects() const {
void Scene::Update(float deltaTime) {
for (auto* go : GetRootGameObjects()) {
if (go->IsActiveInHierarchy()) {
go->Start();
go->Update(deltaTime);
}
}
@@ -285,6 +288,8 @@ void Scene::DeserializeFromString(const std::string& data) {
if (key == "id") {
currentObject->id = static_cast<GameObject::ID>(std::stoull(value));
maxId = std::max(maxId, currentObject->id);
} else if (key == "uuid") {
currentObject->uuid = std::stoull(value);
} else if (key == "name") {
currentObject->name = UnescapeString(value);
} else if (key == "active") {
@@ -312,6 +317,9 @@ void Scene::DeserializeFromString(const std::string& data) {
for (const PendingGameObjectData& pending : pendingObjects) {
auto go = std::make_unique<GameObject>(pending.name);
go->m_id = pending.id;
if (pending.uuid != 0) {
go->m_uuid = pending.uuid;
}
go->m_activeSelf = pending.active;
go->m_scene = this;

View File

@@ -0,0 +1,89 @@
#include "Scripting/ScriptComponent.h"
#include <random>
#include <sstream>
namespace XCEngine {
namespace Scripting {
namespace {
uint64_t GenerateScriptComponentUUID() {
static std::random_device rd;
static std::mt19937_64 gen(rd());
static std::uniform_int_distribution<uint64_t> dis(1, UINT64_MAX);
return dis(gen);
}
} // namespace
ScriptComponent::ScriptComponent()
: m_scriptComponentUUID(GenerateScriptComponentUUID()) {
}
void ScriptComponent::SetScriptClass(const std::string& namespaceName, const std::string& className) {
m_namespaceName = namespaceName;
m_className = className;
}
void ScriptComponent::SetScriptClass(const std::string& assemblyName, const std::string& namespaceName, const std::string& className) {
m_assemblyName = assemblyName;
m_namespaceName = namespaceName;
m_className = className;
}
std::string ScriptComponent::GetFullClassName() const {
if (m_className.empty()) {
return std::string();
}
if (m_namespaceName.empty()) {
return m_className;
}
return m_namespaceName + "." + m_className;
}
void ScriptComponent::Serialize(std::ostream& os) const {
os << "scriptComponentUUID=" << m_scriptComponentUUID << ";";
os << "assembly=" << EscapeScriptString(m_assemblyName) << ";";
os << "namespace=" << EscapeScriptString(m_namespaceName) << ";";
os << "class=" << EscapeScriptString(m_className) << ";";
os << "fields=" << EscapeScriptString(m_fieldStorage.SerializeToString()) << ";";
}
void ScriptComponent::Deserialize(std::istream& is) {
std::ostringstream buffer;
buffer << is.rdbuf();
std::istringstream input(buffer.str());
std::string token;
while (std::getline(input, token, ';')) {
if (token.empty()) {
continue;
}
const size_t eqPos = token.find('=');
if (eqPos == std::string::npos) {
continue;
}
const std::string key = token.substr(0, eqPos);
const std::string value = token.substr(eqPos + 1);
if (key == "scriptComponentUUID") {
m_scriptComponentUUID = static_cast<uint64_t>(std::stoull(value));
} else if (key == "assembly") {
m_assemblyName = UnescapeScriptString(value);
} else if (key == "namespace") {
m_namespaceName = UnescapeScriptString(value);
} else if (key == "class") {
m_className = UnescapeScriptString(value);
} else if (key == "fields") {
m_fieldStorage.DeserializeFromString(UnescapeScriptString(value));
}
}
}
} // namespace Scripting
} // namespace XCEngine

View File

@@ -0,0 +1,284 @@
#include "Scripting/ScriptField.h"
#include <algorithm>
#include <cctype>
#include <iomanip>
#include <limits>
#include <sstream>
#include <type_traits>
namespace XCEngine {
namespace Scripting {
namespace {
bool IsSafeScriptChar(unsigned char ch) {
return std::isalnum(ch) != 0 || ch == '_' || ch == '-' || ch == '.' || ch == '/' || ch == ' ';
}
int HexToInt(char ch) {
if (ch >= '0' && ch <= '9') {
return ch - '0';
}
if (ch >= 'A' && ch <= 'F') {
return 10 + (ch - 'A');
}
if (ch >= 'a' && ch <= 'f') {
return 10 + (ch - 'a');
}
return -1;
}
template<typename T>
std::string SerializeScalar(T value) {
std::ostringstream os;
os << std::setprecision(std::numeric_limits<T>::max_digits10) << value;
return os.str();
}
template<typename TVector>
bool TryDeserializeVector(const std::string& value, TVector& outVector, int componentCount) {
std::string normalized = value;
std::replace(normalized.begin(), normalized.end(), ',', ' ');
std::istringstream stream(normalized);
float components[4] = {0.0f, 0.0f, 0.0f, 0.0f};
for (int i = 0; i < componentCount; ++i) {
if (!(stream >> components[i])) {
return false;
}
}
if constexpr (std::is_same_v<TVector, Math::Vector2>) {
outVector = Math::Vector2(components[0], components[1]);
} else if constexpr (std::is_same_v<TVector, Math::Vector3>) {
outVector = Math::Vector3(components[0], components[1], components[2]);
} else {
outVector = Math::Vector4(components[0], components[1], components[2], components[3]);
}
return true;
}
} // namespace
std::string ScriptFieldTypeToString(ScriptFieldType type) {
switch (type) {
case ScriptFieldType::None: return "None";
case ScriptFieldType::Float: return "Float";
case ScriptFieldType::Double: return "Double";
case ScriptFieldType::Bool: return "Bool";
case ScriptFieldType::Int32: return "Int32";
case ScriptFieldType::UInt64: return "UInt64";
case ScriptFieldType::String: return "String";
case ScriptFieldType::Vector2: return "Vector2";
case ScriptFieldType::Vector3: return "Vector3";
case ScriptFieldType::Vector4: return "Vector4";
case ScriptFieldType::GameObject: return "GameObject";
}
return "None";
}
bool TryParseScriptFieldType(const std::string& value, ScriptFieldType& outType) {
if (value == "None") {
outType = ScriptFieldType::None;
} else if (value == "Float") {
outType = ScriptFieldType::Float;
} else if (value == "Double") {
outType = ScriptFieldType::Double;
} else if (value == "Bool") {
outType = ScriptFieldType::Bool;
} else if (value == "Int32") {
outType = ScriptFieldType::Int32;
} else if (value == "UInt64") {
outType = ScriptFieldType::UInt64;
} else if (value == "String") {
outType = ScriptFieldType::String;
} else if (value == "Vector2") {
outType = ScriptFieldType::Vector2;
} else if (value == "Vector3") {
outType = ScriptFieldType::Vector3;
} else if (value == "Vector4") {
outType = ScriptFieldType::Vector4;
} else if (value == "GameObject") {
outType = ScriptFieldType::GameObject;
} else {
return false;
}
return true;
}
bool IsScriptFieldValueCompatible(ScriptFieldType type, const ScriptFieldValue& value) {
switch (type) {
case ScriptFieldType::None: return std::holds_alternative<std::monostate>(value);
case ScriptFieldType::Float: return std::holds_alternative<float>(value);
case ScriptFieldType::Double: return std::holds_alternative<double>(value);
case ScriptFieldType::Bool: return std::holds_alternative<bool>(value);
case ScriptFieldType::Int32: return std::holds_alternative<int32_t>(value);
case ScriptFieldType::UInt64: return std::holds_alternative<uint64_t>(value);
case ScriptFieldType::String: return std::holds_alternative<std::string>(value);
case ScriptFieldType::Vector2: return std::holds_alternative<Math::Vector2>(value);
case ScriptFieldType::Vector3: return std::holds_alternative<Math::Vector3>(value);
case ScriptFieldType::Vector4: return std::holds_alternative<Math::Vector4>(value);
case ScriptFieldType::GameObject: return std::holds_alternative<GameObjectReference>(value);
}
return false;
}
ScriptFieldValue CreateDefaultScriptFieldValue(ScriptFieldType type) {
switch (type) {
case ScriptFieldType::None: return std::monostate{};
case ScriptFieldType::Float: return 0.0f;
case ScriptFieldType::Double: return 0.0;
case ScriptFieldType::Bool: return false;
case ScriptFieldType::Int32: return int32_t(0);
case ScriptFieldType::UInt64: return uint64_t(0);
case ScriptFieldType::String: return std::string();
case ScriptFieldType::Vector2: return Math::Vector2::Zero();
case ScriptFieldType::Vector3: return Math::Vector3::Zero();
case ScriptFieldType::Vector4: return Math::Vector4::Zero();
case ScriptFieldType::GameObject: return GameObjectReference{};
}
return std::monostate{};
}
std::string SerializeScriptFieldValue(ScriptFieldType type, const ScriptFieldValue& value) {
if (!IsScriptFieldValueCompatible(type, value)) {
return std::string();
}
switch (type) {
case ScriptFieldType::None:
return std::string();
case ScriptFieldType::Float:
return SerializeScalar(std::get<float>(value));
case ScriptFieldType::Double:
return SerializeScalar(std::get<double>(value));
case ScriptFieldType::Bool:
return std::get<bool>(value) ? "1" : "0";
case ScriptFieldType::Int32:
return std::to_string(std::get<int32_t>(value));
case ScriptFieldType::UInt64:
return std::to_string(std::get<uint64_t>(value));
case ScriptFieldType::String:
return EscapeScriptString(std::get<std::string>(value));
case ScriptFieldType::Vector2: {
const Math::Vector2& vector = std::get<Math::Vector2>(value);
return SerializeScalar(vector.x) + "," + SerializeScalar(vector.y);
}
case ScriptFieldType::Vector3: {
const Math::Vector3& vector = std::get<Math::Vector3>(value);
return SerializeScalar(vector.x) + "," + SerializeScalar(vector.y) + "," + SerializeScalar(vector.z);
}
case ScriptFieldType::Vector4: {
const Math::Vector4& vector = std::get<Math::Vector4>(value);
return SerializeScalar(vector.x) + "," + SerializeScalar(vector.y) + "," + SerializeScalar(vector.z) + "," + SerializeScalar(vector.w);
}
case ScriptFieldType::GameObject:
return std::to_string(std::get<GameObjectReference>(value).gameObjectUUID);
}
return std::string();
}
bool TryDeserializeScriptFieldValue(ScriptFieldType type, const std::string& value, ScriptFieldValue& outValue) {
try {
switch (type) {
case ScriptFieldType::None:
outValue = std::monostate{};
return true;
case ScriptFieldType::Float:
outValue = std::stof(value);
return true;
case ScriptFieldType::Double:
outValue = std::stod(value);
return true;
case ScriptFieldType::Bool:
outValue = (value == "1" || value == "true" || value == "True");
return true;
case ScriptFieldType::Int32:
outValue = static_cast<int32_t>(std::stoi(value));
return true;
case ScriptFieldType::UInt64:
outValue = static_cast<uint64_t>(std::stoull(value));
return true;
case ScriptFieldType::String:
outValue = UnescapeScriptString(value);
return true;
case ScriptFieldType::Vector2: {
Math::Vector2 parsedValue;
if (!TryDeserializeVector(value, parsedValue, 2)) {
return false;
}
outValue = parsedValue;
return true;
}
case ScriptFieldType::Vector3: {
Math::Vector3 parsedValue;
if (!TryDeserializeVector(value, parsedValue, 3)) {
return false;
}
outValue = parsedValue;
return true;
}
case ScriptFieldType::Vector4: {
Math::Vector4 parsedValue;
if (!TryDeserializeVector(value, parsedValue, 4)) {
return false;
}
outValue = parsedValue;
return true;
}
case ScriptFieldType::GameObject:
outValue = GameObjectReference{static_cast<uint64_t>(std::stoull(value))};
return true;
}
} catch (...) {
return false;
}
return false;
}
std::string EscapeScriptString(const std::string& value) {
std::ostringstream os;
os << std::uppercase << std::hex;
for (unsigned char ch : value) {
if (IsSafeScriptChar(ch)) {
os << static_cast<char>(ch);
} else {
os << '%' << std::setw(2) << std::setfill('0') << static_cast<int>(ch);
}
}
return os.str();
}
std::string UnescapeScriptString(const std::string& value) {
std::string result;
result.reserve(value.size());
for (size_t i = 0; i < value.size(); ++i) {
if (value[i] == '%' && i + 2 < value.size()) {
const int high = HexToInt(value[i + 1]);
const int low = HexToInt(value[i + 2]);
if (high >= 0 && low >= 0) {
result.push_back(static_cast<char>((high << 4) | low));
i += 2;
continue;
}
}
result.push_back(value[i]);
}
return result;
}
} // namespace Scripting
} // namespace XCEngine

View File

@@ -0,0 +1,125 @@
#include "Scripting/ScriptFieldStorage.h"
#include <algorithm>
#include <sstream>
namespace XCEngine {
namespace Scripting {
bool ScriptFieldStorage::SetFieldValue(const std::string& fieldName, ScriptFieldType type, const ScriptFieldValue& value) {
if (fieldName.empty() || !IsScriptFieldValueCompatible(type, value)) {
return false;
}
m_fields[fieldName] = StoredScriptField{type, value};
return true;
}
const StoredScriptField* ScriptFieldStorage::FindField(const std::string& fieldName) const {
const auto it = m_fields.find(fieldName);
return it != m_fields.end() ? &it->second : nullptr;
}
StoredScriptField* ScriptFieldStorage::FindField(const std::string& fieldName) {
const auto it = m_fields.find(fieldName);
return it != m_fields.end() ? &it->second : nullptr;
}
bool ScriptFieldStorage::Contains(const std::string& fieldName) const {
return m_fields.find(fieldName) != m_fields.end();
}
bool ScriptFieldStorage::Remove(const std::string& fieldName) {
return m_fields.erase(fieldName) > 0;
}
void ScriptFieldStorage::Clear() {
m_fields.clear();
}
std::vector<std::string> ScriptFieldStorage::GetFieldNames() const {
std::vector<std::string> names;
names.reserve(m_fields.size());
for (const auto& entry : m_fields) {
names.push_back(entry.first);
}
std::sort(names.begin(), names.end());
return names;
}
std::string ScriptFieldStorage::SerializeToString() const {
std::ostringstream os;
const std::vector<std::string> fieldNames = GetFieldNames();
for (const std::string& fieldName : fieldNames) {
const StoredScriptField* field = FindField(fieldName);
if (!field) {
continue;
}
os << "field="
<< EscapeScriptString(fieldName)
<< "|"
<< ScriptFieldTypeToString(field->type)
<< "|"
<< SerializeScriptFieldValue(field->type, field->value)
<< "\n";
}
return os.str();
}
void ScriptFieldStorage::DeserializeFromString(const std::string& data) {
m_fields.clear();
std::istringstream input(data);
std::string line;
while (std::getline(input, line)) {
if (line.empty()) {
continue;
}
static constexpr const char* fieldPrefix = "field=";
if (line.rfind(fieldPrefix, 0) != 0) {
continue;
}
const std::string payload = line.substr(6);
const size_t firstSeparator = payload.find('|');
const size_t secondSeparator = payload.find('|', firstSeparator == std::string::npos ? firstSeparator : firstSeparator + 1);
if (firstSeparator == std::string::npos || secondSeparator == std::string::npos) {
continue;
}
const std::string fieldName = UnescapeScriptString(payload.substr(0, firstSeparator));
const std::string typeName = payload.substr(firstSeparator + 1, secondSeparator - firstSeparator - 1);
const std::string serializedValue = payload.substr(secondSeparator + 1);
ScriptFieldType type = ScriptFieldType::None;
if (!TryParseScriptFieldType(typeName, type)) {
continue;
}
ScriptFieldValue value = CreateDefaultScriptFieldValue(type);
if (!TryDeserializeScriptFieldValue(type, serializedValue, value)) {
continue;
}
m_fields[fieldName] = StoredScriptField{type, value};
}
}
void ScriptFieldStorage::Serialize(std::ostream& os) const {
os << SerializeToString();
}
void ScriptFieldStorage::Deserialize(std::istream& is) {
std::ostringstream buffer;
buffer << is.rdbuf();
DeserializeFromString(buffer.str());
}
} // namespace Scripting
} // namespace XCEngine