516 lines
22 KiB
C++
516 lines
22 KiB
C++
#pragma once
|
|
|
|
#include "Application.h"
|
|
#include "IComponentEditor.h"
|
|
#include "ScriptComponentEditorUtils.h"
|
|
#include "Core/IUndoManager.h"
|
|
#include "UI/UI.h"
|
|
|
|
#include <XCEngine/Scripting/ScriptComponent.h>
|
|
#include <XCEngine/Scripting/ScriptEngine.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace XCEngine {
|
|
namespace Editor {
|
|
|
|
class ScriptComponentEditor : public IComponentEditor {
|
|
public:
|
|
const char* GetComponentTypeName() const override {
|
|
return "ScriptComponent";
|
|
}
|
|
|
|
const char* GetDisplayName() const override {
|
|
return "Script";
|
|
}
|
|
|
|
bool Render(::XCEngine::Components::Component* component, IUndoManager* undoManager) override {
|
|
auto* scriptComponent = dynamic_cast<::XCEngine::Scripting::ScriptComponent*>(component);
|
|
if (!scriptComponent) {
|
|
return false;
|
|
}
|
|
|
|
bool changed = false;
|
|
changed |= RenderScriptClassSelector(*scriptComponent, undoManager);
|
|
|
|
::XCEngine::Scripting::ScriptFieldModel model;
|
|
if (!::XCEngine::Scripting::ScriptEngine::Get().TryGetScriptFieldModel(scriptComponent, model)) {
|
|
UI::DrawHintText("Failed to query script field metadata.");
|
|
return changed;
|
|
}
|
|
|
|
switch (model.classStatus) {
|
|
case ::XCEngine::Scripting::ScriptFieldClassStatus::Unassigned:
|
|
UI::DrawHintText("Assign a C# script to edit serialized fields.");
|
|
return changed;
|
|
case ::XCEngine::Scripting::ScriptFieldClassStatus::Missing:
|
|
UI::DrawHintText("Assigned script class is not available in the loaded script assemblies.");
|
|
break;
|
|
case ::XCEngine::Scripting::ScriptFieldClassStatus::Available:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (model.fields.empty()) {
|
|
UI::DrawHintText(
|
|
model.classStatus == ::XCEngine::Scripting::ScriptFieldClassStatus::Available
|
|
? "Selected script exposes no supported public instance fields."
|
|
: "No serialized script fields are currently available.");
|
|
return changed;
|
|
}
|
|
|
|
for (const ::XCEngine::Scripting::ScriptFieldSnapshot& field : model.fields) {
|
|
changed |= RenderScriptField(*scriptComponent, model.classStatus, field, undoManager);
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
bool CanAddTo(::XCEngine::Components::GameObject* gameObject) const override {
|
|
return gameObject != nullptr;
|
|
}
|
|
|
|
const char* GetAddDisabledReason(::XCEngine::Components::GameObject* gameObject) const override {
|
|
return gameObject ? nullptr : "Invalid";
|
|
}
|
|
|
|
bool CanRemove(::XCEngine::Components::Component* component) const override {
|
|
return CanEdit(component);
|
|
}
|
|
|
|
private:
|
|
static constexpr size_t kStringBufferSize = 512;
|
|
|
|
struct StringFieldEditState {
|
|
std::array<char, kStringBufferSize> buffer{};
|
|
std::string lastSyncedValue;
|
|
bool initialized = false;
|
|
bool editing = false;
|
|
};
|
|
|
|
bool RenderScriptClassSelector(
|
|
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
|
IUndoManager* undoManager) {
|
|
std::vector<::XCEngine::Scripting::ScriptClassDescriptor> scriptClasses;
|
|
const bool hasLoadedClasses =
|
|
::XCEngine::Scripting::ScriptEngine::Get().TryGetAvailableScriptClasses(scriptClasses);
|
|
|
|
const ::XCEngine::Scripting::ScriptClassDescriptor currentDescriptor{
|
|
scriptComponent.GetAssemblyName(),
|
|
scriptComponent.GetNamespaceName(),
|
|
scriptComponent.GetClassName()
|
|
};
|
|
|
|
std::string currentLabel = "None";
|
|
if (scriptComponent.HasScriptClass()) {
|
|
const auto currentIt = std::find(scriptClasses.begin(), scriptClasses.end(), currentDescriptor);
|
|
currentLabel = currentIt != scriptClasses.end()
|
|
? ScriptComponentEditorUtils::BuildScriptClassDisplayName(*currentIt)
|
|
: ScriptComponentEditorUtils::BuildScriptClassDisplayName(scriptComponent) + " (Missing)";
|
|
}
|
|
|
|
bool changed = false;
|
|
UI::DrawPropertyRow("Script", UI::InspectorPropertyLayout(), [&](const UI::PropertyLayoutMetrics& layout) {
|
|
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
|
|
ImGui::SetNextItemWidth(layout.controlWidth);
|
|
if (!ImGui::BeginCombo("##ScriptClass", currentLabel.c_str())) {
|
|
return false;
|
|
}
|
|
|
|
if (ImGui::Selectable("None", !scriptComponent.HasScriptClass())) {
|
|
changed |= ApplyScriptClassSelection(scriptComponent, nullptr, undoManager);
|
|
}
|
|
|
|
if (!scriptClasses.empty()) {
|
|
ImGui::Separator();
|
|
}
|
|
|
|
for (const ::XCEngine::Scripting::ScriptClassDescriptor& descriptor : scriptClasses) {
|
|
const bool selected = descriptor == currentDescriptor;
|
|
const std::string label =
|
|
ScriptComponentEditorUtils::BuildScriptClassDisplayName(descriptor);
|
|
if (ImGui::Selectable(label.c_str(), selected)) {
|
|
changed |= ApplyScriptClassSelection(scriptComponent, &descriptor, undoManager);
|
|
}
|
|
if (selected) {
|
|
ImGui::SetItemDefaultFocus();
|
|
}
|
|
}
|
|
|
|
if (!hasLoadedClasses) {
|
|
ImGui::Separator();
|
|
ImGui::TextDisabled("No script assemblies are currently loaded.");
|
|
}
|
|
|
|
ImGui::EndCombo();
|
|
return false;
|
|
});
|
|
|
|
if (!hasLoadedClasses) {
|
|
const EditorScriptRuntimeStatus& runtimeStatus = Application::Get().GetScriptRuntimeStatus();
|
|
const std::string hintText =
|
|
ScriptComponentEditorUtils::BuildScriptRuntimeUnavailableHint(runtimeStatus);
|
|
UI::DrawHintText(hintText.c_str());
|
|
|
|
if (ScriptComponentEditorUtils::CanRebuildScriptAssemblies(runtimeStatus)) {
|
|
if (UI::InspectorActionButton("Rebuild Scripts", ImVec2(120.0f, 0.0f))) {
|
|
Application::Get().RebuildScriptingAssemblies();
|
|
}
|
|
}
|
|
|
|
if (ScriptComponentEditorUtils::CanReloadScriptRuntime(runtimeStatus)) {
|
|
if (ScriptComponentEditorUtils::CanRebuildScriptAssemblies(runtimeStatus)) {
|
|
ImGui::SameLine();
|
|
}
|
|
if (UI::InspectorActionButton("Reload Scripts", ImVec2(120.0f, 0.0f))) {
|
|
Application::Get().ReloadScriptingRuntime();
|
|
}
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
bool ApplyScriptClassSelection(
|
|
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
|
const ::XCEngine::Scripting::ScriptClassDescriptor* descriptor,
|
|
IUndoManager* undoManager) const {
|
|
if (!descriptor) {
|
|
if (!scriptComponent.HasScriptClass()) {
|
|
return false;
|
|
}
|
|
|
|
if (undoManager) {
|
|
undoManager->BeginInteractiveChange("Modify Script Component");
|
|
}
|
|
scriptComponent.ClearScriptClass();
|
|
return true;
|
|
}
|
|
|
|
if (scriptComponent.GetAssemblyName() == descriptor->assemblyName &&
|
|
scriptComponent.GetNamespaceName() == descriptor->namespaceName &&
|
|
scriptComponent.GetClassName() == descriptor->className) {
|
|
return false;
|
|
}
|
|
|
|
if (undoManager) {
|
|
undoManager->BeginInteractiveChange("Modify Script Component");
|
|
}
|
|
scriptComponent.SetScriptClass(
|
|
descriptor->assemblyName,
|
|
descriptor->namespaceName,
|
|
descriptor->className);
|
|
return true;
|
|
}
|
|
|
|
bool RenderScriptField(
|
|
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
|
::XCEngine::Scripting::ScriptFieldClassStatus classStatus,
|
|
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
|
|
IUndoManager* undoManager) {
|
|
bool changed = false;
|
|
const bool canEdit = ScriptComponentEditorUtils::CanEditScriptField(classStatus, field);
|
|
|
|
if (canEdit) {
|
|
changed |= RenderScriptFieldEditor(scriptComponent, field, undoManager);
|
|
} else {
|
|
RenderReadOnlyScriptField(field);
|
|
}
|
|
|
|
const std::string issueText = ScriptComponentEditorUtils::BuildScriptFieldIssueText(field);
|
|
if (!issueText.empty()) {
|
|
UI::DrawHintText(issueText.c_str());
|
|
}
|
|
|
|
if (ScriptComponentEditorUtils::CanClearScriptFieldOverride(field)) {
|
|
ImGui::PushID((field.metadata.name + "##Reset").c_str());
|
|
if (UI::InspectorActionButton("Reset", ImVec2(72.0f, 0.0f))) {
|
|
changed |= ClearScriptFieldOverride(scriptComponent, field, undoManager);
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
bool RenderScriptFieldEditor(
|
|
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
|
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
|
|
IUndoManager* undoManager) {
|
|
using namespace ::XCEngine::Scripting;
|
|
|
|
switch (field.metadata.type) {
|
|
case ScriptFieldType::Float: {
|
|
float value = std::get<float>(field.value);
|
|
const bool widgetChanged = UI::DrawPropertyFloat(field.metadata.name.c_str(), value, 0.1f);
|
|
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
|
}
|
|
case ScriptFieldType::Double: {
|
|
double value = std::get<double>(field.value);
|
|
const bool widgetChanged = UI::DrawPropertyRow(
|
|
field.metadata.name.c_str(),
|
|
UI::InspectorPropertyLayout(),
|
|
[&](const UI::PropertyLayoutMetrics& layout) {
|
|
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
|
|
ImGui::SetNextItemWidth(layout.controlWidth);
|
|
return ImGui::InputDouble("##Value", &value, 0.0, 0.0, "%.3f");
|
|
});
|
|
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
|
}
|
|
case ScriptFieldType::Bool: {
|
|
bool value = std::get<bool>(field.value);
|
|
const bool widgetChanged = UI::DrawPropertyBool(field.metadata.name.c_str(), value);
|
|
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
|
}
|
|
case ScriptFieldType::Int32: {
|
|
int value = std::get<int32_t>(field.value);
|
|
const bool widgetChanged = UI::DrawPropertyInt(field.metadata.name.c_str(), value, 1);
|
|
return widgetChanged &&
|
|
ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(static_cast<int32_t>(value)), undoManager);
|
|
}
|
|
case ScriptFieldType::UInt64: {
|
|
uint64_t value = std::get<uint64_t>(field.value);
|
|
const bool widgetChanged = UI::DrawPropertyRow(
|
|
field.metadata.name.c_str(),
|
|
UI::InspectorPropertyLayout(),
|
|
[&](const UI::PropertyLayoutMetrics& layout) {
|
|
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
|
|
ImGui::SetNextItemWidth(layout.controlWidth);
|
|
return ImGui::InputScalar("##Value", ImGuiDataType_U64, &value);
|
|
});
|
|
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
|
}
|
|
case ScriptFieldType::String:
|
|
return RenderStringScriptFieldEditor(scriptComponent, field, undoManager);
|
|
case ScriptFieldType::Vector2: {
|
|
::XCEngine::Math::Vector2 value = std::get<::XCEngine::Math::Vector2>(field.value);
|
|
const bool widgetChanged = UI::DrawPropertyVec2(field.metadata.name.c_str(), value, 0.0f, 0.1f);
|
|
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
|
}
|
|
case ScriptFieldType::Vector3: {
|
|
::XCEngine::Math::Vector3 value = std::get<::XCEngine::Math::Vector3>(field.value);
|
|
const bool widgetChanged = UI::DrawPropertyVec3Input(field.metadata.name.c_str(), value, 0.1f);
|
|
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
|
}
|
|
case ScriptFieldType::Vector4: {
|
|
::XCEngine::Math::Vector4 value = std::get<::XCEngine::Math::Vector4>(field.value);
|
|
const bool widgetChanged = UI::DrawPropertyRow(
|
|
field.metadata.name.c_str(),
|
|
UI::InspectorPropertyLayout(),
|
|
[&](const UI::PropertyLayoutMetrics& layout) {
|
|
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
|
|
ImGui::SetNextItemWidth(layout.controlWidth);
|
|
return ImGui::DragFloat4("##Value", &value.x, 0.1f);
|
|
});
|
|
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
|
|
}
|
|
case ScriptFieldType::GameObject:
|
|
return RenderGameObjectScriptFieldEditor(scriptComponent, field, undoManager);
|
|
case ScriptFieldType::None:
|
|
default:
|
|
RenderReadOnlyScriptField(field);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool RenderStringScriptFieldEditor(
|
|
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
|
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
|
|
IUndoManager* undoManager) {
|
|
StringFieldEditState& editState =
|
|
m_stringFieldStates[scriptComponent.GetScriptComponentUUID()][field.metadata.name];
|
|
const std::string currentValue = std::get<std::string>(field.value);
|
|
|
|
if (!editState.initialized || (!editState.editing && editState.lastSyncedValue != currentValue)) {
|
|
SyncStringFieldEditState(editState, currentValue);
|
|
}
|
|
|
|
bool isEditing = false;
|
|
const bool widgetChanged = UI::DrawPropertyRow(
|
|
field.metadata.name.c_str(),
|
|
UI::InspectorPropertyLayout(),
|
|
[&](const UI::PropertyLayoutMetrics& layout) {
|
|
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
|
|
ImGui::SetNextItemWidth(layout.controlWidth);
|
|
const bool changed = ImGui::InputText("##Value", editState.buffer.data(), editState.buffer.size());
|
|
isEditing = ImGui::IsItemActive();
|
|
return changed;
|
|
});
|
|
editState.editing = isEditing;
|
|
|
|
if (!widgetChanged) {
|
|
return false;
|
|
}
|
|
|
|
const std::string editedValue(editState.buffer.data());
|
|
if (!ApplyScriptFieldWrite(
|
|
scriptComponent,
|
|
field,
|
|
::XCEngine::Scripting::ScriptFieldValue(editedValue),
|
|
undoManager)) {
|
|
SyncStringFieldEditState(editState, currentValue);
|
|
return false;
|
|
}
|
|
|
|
editState.lastSyncedValue = editedValue;
|
|
return true;
|
|
}
|
|
|
|
bool RenderGameObjectScriptFieldEditor(
|
|
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
|
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
|
|
IUndoManager* undoManager) {
|
|
using namespace ::XCEngine::Scripting;
|
|
|
|
const GameObjectReference currentReference = std::get<GameObjectReference>(field.value);
|
|
const auto& sceneRoots = Application::Get().GetEditorContext().GetSceneManager().GetRootEntities();
|
|
const ::XCEngine::Components::GameObject::ID currentGameObjectId =
|
|
ScriptComponentEditorUtils::FindGameObjectIdByUuid(sceneRoots, currentReference.gameObjectUUID);
|
|
|
|
UI::ReferencePickerOptions pickerOptions;
|
|
pickerOptions.popupTitle = "Select GameObject";
|
|
pickerOptions.emptyHint = "None";
|
|
pickerOptions.searchHint = "Search";
|
|
pickerOptions.noSceneText = "No scene objects.";
|
|
pickerOptions.showAssetsTab = false;
|
|
pickerOptions.showSceneTab = true;
|
|
|
|
GameObjectReference pendingReference = currentReference;
|
|
const bool widgetChanged = UI::DrawPropertyRow(
|
|
field.metadata.name.c_str(),
|
|
UI::InspectorPropertyLayout(),
|
|
[&](const UI::PropertyLayoutMetrics& layout) {
|
|
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
|
|
const UI::ReferencePickerInteraction interaction =
|
|
UI::DrawReferencePickerControl(
|
|
std::string(),
|
|
currentGameObjectId,
|
|
pickerOptions,
|
|
layout.controlWidth);
|
|
|
|
if (interaction.clearRequested) {
|
|
pendingReference = GameObjectReference{};
|
|
return true;
|
|
}
|
|
|
|
if (interaction.assignedSceneObjectId == ::XCEngine::Components::GameObject::INVALID_ID ||
|
|
interaction.assignedSceneObjectId == currentGameObjectId) {
|
|
return false;
|
|
}
|
|
|
|
::XCEngine::Components::GameObject* assignedGameObject =
|
|
Application::Get().GetEditorContext().GetSceneManager().GetEntity(interaction.assignedSceneObjectId);
|
|
if (!assignedGameObject) {
|
|
return false;
|
|
}
|
|
|
|
pendingReference = GameObjectReference{assignedGameObject->GetUUID()};
|
|
return true;
|
|
});
|
|
|
|
return widgetChanged &&
|
|
ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(pendingReference), undoManager);
|
|
}
|
|
|
|
void RenderReadOnlyScriptField(const ::XCEngine::Scripting::ScriptFieldSnapshot& field) const {
|
|
const std::string valueText = DescribeScriptFieldValue(field.metadata.type, field.value);
|
|
UI::DrawPropertyRow(
|
|
field.metadata.name.c_str(),
|
|
UI::InspectorPropertyLayout(),
|
|
[&](const UI::PropertyLayoutMetrics& layout) {
|
|
UI::AlignPropertyControlVertically(layout, ImGui::GetTextLineHeight());
|
|
ImGui::TextDisabled("%s", valueText.c_str());
|
|
return false;
|
|
});
|
|
}
|
|
|
|
bool ApplyScriptFieldWrite(
|
|
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
|
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
|
|
const ::XCEngine::Scripting::ScriptFieldValue& value,
|
|
IUndoManager* undoManager) const {
|
|
std::vector<::XCEngine::Scripting::ScriptFieldWriteResult> results;
|
|
if (undoManager) {
|
|
undoManager->BeginInteractiveChange("Modify Script Field");
|
|
}
|
|
|
|
const bool applied = ::XCEngine::Scripting::ScriptEngine::Get().ApplyScriptFieldWrites(
|
|
&scriptComponent,
|
|
{ ::XCEngine::Scripting::ScriptFieldWriteRequest{field.metadata.name, field.metadata.type, value} },
|
|
results);
|
|
|
|
if (!applied || results.empty() ||
|
|
results.front().status != ::XCEngine::Scripting::ScriptFieldWriteStatus::Applied) {
|
|
if (undoManager && undoManager->HasPendingInteractiveChange()) {
|
|
undoManager->CancelInteractiveChange();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ClearScriptFieldOverride(
|
|
::XCEngine::Scripting::ScriptComponent& scriptComponent,
|
|
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
|
|
IUndoManager* undoManager) const {
|
|
std::vector<::XCEngine::Scripting::ScriptFieldClearResult> results;
|
|
if (undoManager) {
|
|
undoManager->BeginInteractiveChange("Clear Script Field Override");
|
|
}
|
|
|
|
const bool cleared = ::XCEngine::Scripting::ScriptEngine::Get().ClearScriptFieldOverrides(
|
|
&scriptComponent,
|
|
{ ::XCEngine::Scripting::ScriptFieldClearRequest{field.metadata.name} },
|
|
results);
|
|
|
|
if (!cleared || results.empty() ||
|
|
results.front().status != ::XCEngine::Scripting::ScriptFieldClearStatus::Applied) {
|
|
if (undoManager && undoManager->HasPendingInteractiveChange()) {
|
|
undoManager->CancelInteractiveChange();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void SyncStringFieldEditState(StringFieldEditState& editState, const std::string& value) {
|
|
editState.buffer.fill('\0');
|
|
const size_t copyLength = (std::min)(value.size(), editState.buffer.size() - 1);
|
|
if (copyLength > 0) {
|
|
std::memcpy(editState.buffer.data(), value.data(), copyLength);
|
|
}
|
|
editState.buffer[copyLength] = '\0';
|
|
editState.lastSyncedValue = value;
|
|
editState.initialized = true;
|
|
}
|
|
|
|
static std::string DescribeScriptFieldValue(
|
|
::XCEngine::Scripting::ScriptFieldType type,
|
|
const ::XCEngine::Scripting::ScriptFieldValue& value) {
|
|
if (type == ::XCEngine::Scripting::ScriptFieldType::GameObject) {
|
|
const auto reference = std::get<::XCEngine::Scripting::GameObjectReference>(value);
|
|
if (reference.gameObjectUUID == 0) {
|
|
return "None";
|
|
}
|
|
|
|
return "GameObject (" + std::to_string(reference.gameObjectUUID) + ")";
|
|
}
|
|
|
|
return ::XCEngine::Scripting::SerializeScriptFieldValue(type, value);
|
|
}
|
|
|
|
std::unordered_map<uint64_t, std::unordered_map<std::string, StringFieldEditState>> m_stringFieldStates;
|
|
};
|
|
|
|
} // namespace Editor
|
|
} // namespace XCEngine
|