Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Sources/OvCore/include/OvCore/ECS/Components/Behaviour.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@

#pragma once

#include <map>
#include <set>
#include <string>

#include <OvCore/ECS/Components/CPhysicalObject.h>
#include <OvCore/Scripting/Common/ScriptPropertyValue.h>
#include <OvTools/Utils/OptRef.h>
#include <OvCore/Scripting/ScriptEngine.h>

Expand Down Expand Up @@ -164,6 +169,9 @@ namespace OvCore::ECS::Components

private:
std::unique_ptr<Scripting::Script> m_script;
std::map<std::string, Scripting::ScriptPropertyValue> m_scriptDefaults;
std::map<std::string, Scripting::ScriptPropertyValue> m_scriptProperties;
std::set<std::string> m_unlockedProperties;
};

template<>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @project: Overload
* @author: Overload Tech.
* @licence: MIT
*/

#pragma once

#include <string>
#include <variant>

namespace OvCore::Scripting
{
/**
* Represents a primitive script property value (boolean, number, or string).
* This type is backend-agnostic and used by Behaviour to store and expose script fields.
*/
using ScriptPropertyValue = std::variant<bool, double, std::string>;
}
22 changes: 22 additions & 0 deletions Sources/OvCore/include/OvCore/Scripting/Common/TScript.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@

#pragma once

#include <map>
#include <optional>
#include <string>

#include <OvCore/Scripting/Common/EScriptingLanguage.h>
#include <OvCore/Scripting/Common/ScriptPropertyValue.h>

namespace OvCore::Scripting
{
Expand Down Expand Up @@ -39,6 +44,23 @@ namespace OvCore::Scripting
* Return the context of the script
*/
inline const Context& GetContext() const { return m_context; }
inline Context& GetContext() { return m_context; }

/**
* Returns all primitive properties and their default values as defined by the script.
*/
std::map<std::string, ScriptPropertyValue> GetDefaultProperties() const;

/**
* Returns the live value of a named property from the script's runtime state.
* Returns std::nullopt if the property does not exist or cannot be read.
*/
std::optional<ScriptPropertyValue> GetProperty(const std::string& p_key) const;

/**
* Sets the value of a named property in the script's runtime state.
*/
void SetProperty(const std::string& p_key, const ScriptPropertyValue& p_value);

protected:
Context m_context;
Expand Down
182 changes: 179 additions & 3 deletions Sources/OvCore/src/OvCore/ECS/Components/Behaviour.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,25 @@
* @licence: MIT
*/

#include <limits>
#include <utility>

#include <tinyxml2.h>

#include <OvCore/ECS/Actor.h>
#include <OvCore/ECS/Components/Behaviour.h>
#include <OvCore/Global/ServiceLocator.h>
#include <OvCore/Helpers/GUIDrawer.h>
#include <OvCore/Scripting/ScriptEngine.h>

#include <OvDebug/Logger.h>

#include <OvUI/Plugins/DataDispatcher.h>
#include <OvUI/Widgets/Drags/DragSingleScalar.h>
#include <OvUI/Widgets/InputFields/InputText.h>
#include <OvUI/Widgets/Layout/Dummy.h>
#include <OvUI/Widgets/Layout/Group.h>
#include <OvUI/Widgets/Selection/CheckBox.h>
#include <OvUI/Widgets/Texts/TextColored.h>

OvCore::ECS::Components::Behaviour::Behaviour(ECS::Actor& p_owner, const std::string& p_name) :
Expand All @@ -37,6 +49,35 @@ std::string OvCore::ECS::Components::Behaviour::GetTypeName()
void OvCore::ECS::Components::Behaviour::SetScript(std::unique_ptr<Scripting::Script> &&p_scriptContext)
{
m_script = std::move(p_scriptContext);

if (!m_script || !m_script->IsValid())
{
m_scriptProperties.clear();
m_scriptDefaults.clear();
m_unlockedProperties.clear();
return;
}

m_scriptDefaults = m_script->GetDefaultProperties();

auto old = std::exchange(m_scriptProperties, {});
auto oldUnlocked = std::exchange(m_unlockedProperties, {});

for (const auto& [key, defaultVal] : m_scriptDefaults)
{
const bool wasUnlocked = oldUnlocked.contains(key);
auto it = old.find(key);
const bool canPreserve = wasUnlocked && it != old.end() && it->second.index() == defaultVal.index();

m_scriptProperties[key] = canPreserve ? std::move(it->second) : defaultVal;

if (wasUnlocked)
m_unlockedProperties.insert(key);
}

// Push user overrides back into the live script.
for (const auto& [key, val] : m_scriptProperties)
m_script->SetProperty(key, val);
}

OvTools::Utils::OptRef<OvCore::Scripting::Script> OvCore::ECS::Components::Behaviour::GetScript()
Expand Down Expand Up @@ -126,25 +167,160 @@ void OvCore::ECS::Components::Behaviour::OnTriggerExit(Components::CPhysicalObje

void OvCore::ECS::Components::Behaviour::OnSerialize(tinyxml2::XMLDocument & p_doc, tinyxml2::XMLNode * p_node)
{
if (m_unlockedProperties.empty()) return;

tinyxml2::XMLNode* propsNode = p_doc.NewElement("script_properties");
p_node->InsertEndChild(propsNode);

for (const auto& [fieldKey, fieldValue] : m_scriptProperties)
{
if (!m_unlockedProperties.contains(fieldKey)) continue;

tinyxml2::XMLElement* elem = p_doc.NewElement(fieldKey.c_str());

std::visit([elem]<typename T>(const T& v) {
if constexpr (std::is_same_v<T, bool>)
{ elem->SetAttribute("type", "bool"); elem->SetText(v); }
else if constexpr (std::is_same_v<T, double>)
{ elem->SetAttribute("type", "number"); elem->SetText(v); }
else
{ elem->SetAttribute("type", "string"); elem->SetText(v.c_str()); }
}, fieldValue);

propsNode->InsertEndChild(elem);
}
}

void OvCore::ECS::Components::Behaviour::OnDeserialize(tinyxml2::XMLDocument & p_doc, tinyxml2::XMLNode * p_node)
{
const tinyxml2::XMLElement* propsNode = p_node->FirstChildElement("script_properties");
if (!propsNode) return;

for (const tinyxml2::XMLElement* elem = propsNode->FirstChildElement(); elem; elem = elem->NextSiblingElement())
{
const char* const name = elem->Name();
const char* const type = elem->Attribute("type");
if (!name || !type) continue;

auto it = m_scriptProperties.find(name);
if (it == m_scriptProperties.end()) continue;

const std::string_view typeStr{type};
auto& val = it->second;

if (typeStr == "bool" && std::holds_alternative<bool>(val))
{
bool v = false;
elem->QueryBoolText(&v);
val = v;
}
else if (typeStr == "number" && std::holds_alternative<double>(val))
{
double v = 0.0;
elem->QueryDoubleText(&v);
val = v;
}
else if (typeStr == "string" && std::holds_alternative<std::string>(val))
val = elem->GetText() ? elem->GetText() : "";
else
continue;

m_unlockedProperties.insert(name);

if (m_script)
m_script->SetProperty(name, val);
}
}

void OvCore::ECS::Components::Behaviour::OnInspector(OvUI::Internal::WidgetContainer & p_root)
{
using namespace OvMaths;
using namespace OvCore::Helpers;

if (!m_script)
{
p_root.CreateWidget<OvUI::Widgets::Texts::TextColored>("No scripting context", OvUI::Types::Color::White);
p_root.CreateWidget<OvUI::Widgets::Layout::Dummy>();
}
else if (m_script && m_script->IsValid())
else if (m_script->IsValid())
{
p_root.CreateWidget<OvUI::Widgets::Texts::TextColored>("Ready", OvUI::Types::Color::Green);
p_root.CreateWidget<OvUI::Widgets::Texts::TextColored>("Your script will execute in play mode.", OvUI::Types::Color::White);
p_root.CreateWidget<OvUI::Widgets::Texts::TextColored>("Script properties will appear below", OvUI::Types::Color::White);

for (const auto& [fieldKey, fieldValue] : m_scriptProperties)
{
const bool isUnlocked = m_unlockedProperties.contains(fieldKey);

std::visit([&, key = fieldKey, unlocked = isUnlocked]<typename T>(const T&) {
auto getter = [this, key]() -> T {
if (auto live = m_script->GetProperty(key))
if (auto* val = std::get_if<T>(&*live))
return *val;
auto it = m_scriptProperties.find(key);
return it != m_scriptProperties.end() ? std::get<T>(it->second) : T{};
};
auto setter = [this, key](T newVal) {
m_scriptProperties[key] = newVal;
if (m_script) m_script->SetProperty(key, std::move(newVal));
};

// Label row: [unlock checkbox] [field title], grouped together
auto& labelGroup = p_root.CreateWidget<OvUI::Widgets::Layout::Group>();
auto& unlockBox = labelGroup.CreateWidget<OvUI::Widgets::Selection::CheckBox>(unlocked);
unlockBox.lineBreak = false;
labelGroup.CreateWidget<OvUI::Widgets::Texts::TextColored>(key, GUIDrawer::TitleColor);

// Input widget on the row below
OvUI::Widgets::AWidget* inputPtr = nullptr;
if constexpr (std::is_same_v<T, bool>)
{
auto& w = p_root.CreateWidget<OvUI::Widgets::Selection::CheckBox>();
auto& d = w.template AddPlugin<OvUI::Plugins::DataDispatcher<bool>>();
d.RegisterGatherer([getter]() { bool v = getter(); return reinterpret_cast<bool&>(v); });
d.RegisterProvider([setter](bool v) { setter(reinterpret_cast<bool&>(v)); });
inputPtr = &w;
}
else if constexpr (std::is_same_v<T, double>)
{
auto& w = p_root.CreateWidget<OvUI::Widgets::Drags::DragSingleScalar<double>>(
GUIDrawer::GetDataType<double>(),
std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max(),
static_cast<double>(0), 1.f, "", GUIDrawer::GetFormat<double>()
);
auto& d = w.template AddPlugin<OvUI::Plugins::DataDispatcher<double>>();
d.RegisterGatherer(getter);
d.RegisterProvider(setter);
inputPtr = &w;
}
else
{
auto& w = p_root.CreateWidget<OvUI::Widgets::InputFields::InputText>("");
auto& d = w.template AddPlugin<OvUI::Plugins::DataDispatcher<std::string>>();
d.RegisterGatherer(getter);
d.RegisterProvider(setter);
inputPtr = &w;
}

auto& inputWidget = *inputPtr;
inputWidget.disabled = !unlocked;

unlockBox.ValueChangedEvent += [this, key, &inputWidget](bool checked) {
if (checked)
{
m_unlockedProperties.insert(key);
}
else
{
m_unlockedProperties.erase(key);
if (auto it = m_scriptDefaults.find(key); it != m_scriptDefaults.end())
{
m_scriptProperties[key] = it->second;
if (m_script) m_script->SetProperty(key, it->second);
}
}
inputWidget.disabled = !checked;
};
}, fieldValue);
}
}
else
{
Expand Down
50 changes: 50 additions & 0 deletions Sources/OvCore/src/OvCore/Scripting/Lua/LuaScript.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,53 @@ void OvCore::Scripting::LuaScript::SetOwner(OvCore::ECS::Actor& p_owner)
{
(*m_context.table)["owner"] = &p_owner;
}

template<>
std::map<std::string, OvCore::Scripting::ScriptPropertyValue> OvCore::Scripting::LuaScriptBase::GetDefaultProperties() const
{
std::map<std::string, ScriptPropertyValue> properties;

if (!IsValid()) return properties;

m_context.table->for_each([&](const sol::object& key, const sol::object& value) {
if (!key.is<std::string>()) return;
const std::string keyStr = key.as<std::string>();
if (keyStr.starts_with('_')) return;

switch (value.get_type())
{
case sol::type::boolean: properties[keyStr] = value.as<bool>(); break;
case sol::type::number: properties[keyStr] = value.as<double>(); break;
case sol::type::string: properties[keyStr] = value.as<std::string>(); break;
default: break;
}
});

return properties;
}

template<>
std::optional<OvCore::Scripting::ScriptPropertyValue> OvCore::Scripting::LuaScriptBase::GetProperty(const std::string& p_key) const
{
if (!IsValid()) return std::nullopt;

const sol::object obj = (*m_context.table)[p_key];

switch (obj.get_type())
{
case sol::type::boolean: return obj.as<bool>();
case sol::type::number: return obj.as<double>();
case sol::type::string: return obj.as<std::string>();
default: return std::nullopt;
}
}

template<>
void OvCore::Scripting::LuaScriptBase::SetProperty(const std::string& p_key, const OvCore::Scripting::ScriptPropertyValue& p_value)
{
if (!IsValid()) return;

std::visit([&](auto&& v) {
(*m_context.table)[p_key] = v;
}, p_value);
}
9 changes: 9 additions & 0 deletions Sources/OvCore/src/OvCore/Scripting/Null/NullScript.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,12 @@ OvCore::Scripting::NullScript::~TScript() {}

template<>
bool OvCore::Scripting::NullScript::IsValid() const { return true; }

template<>
std::map<std::string, OvCore::Scripting::ScriptPropertyValue> OvCore::Scripting::NullScript::GetDefaultProperties() const { return {}; }

template<>
std::optional<OvCore::Scripting::ScriptPropertyValue> OvCore::Scripting::NullScript::GetProperty(const std::string&) const { return std::nullopt; }

template<>
void OvCore::Scripting::NullScript::SetProperty(const std::string&, const OvCore::Scripting::ScriptPropertyValue&) {}