#pragma once #include "Application.h" #include "IComponentEditor.h" #include "ScriptComponentEditorUtils.h" #include "Core/IUndoManager.h" #include "UI/UI.h" #include #include #include #include #include #include #include #include #include #include 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 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(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(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(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(field.value); const bool widgetChanged = UI::DrawPropertyInt(field.metadata.name.c_str(), value, 1); return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(static_cast(value)), undoManager); } case ScriptFieldType::UInt64: { uint64_t value = std::get(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(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(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> m_stringFieldStates; }; } // namespace Editor } // namespace XCEngine