From 7d02d344e5a39b45d39b18f023a267e79abbeb6a Mon Sep 17 00:00:00 2001 From: atdunbg Date: Sun, 4 Oct 2026 00:57:45 +0800 Subject: [PATCH] fix(scripts): prevent duplicate script attachment per entity The per-entity script list allowed picking the same script class twice; the name-keyed runtime silently ignores duplicates, leaving the extra rows dead weight with unusable enable bits. Enforce DisallowMultipleComponent semantics: the dropdown of each row only offers unattached names (own name kept for echo), "+ Add Script" disables when every registered script is attached, and scene loading dedupes as a safety net for hand-edited files. --- App/editor/InspectorPanel.cpp | 60 ++++++++++++++++---- Engine/src/Serialization/SceneSerializer.cpp | 12 +++- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/App/editor/InspectorPanel.cpp b/App/editor/InspectorPanel.cpp index da6e3f8..5e993c5 100644 --- a/App/editor/InspectorPanel.cpp +++ b/App/editor/InspectorPanel.cpp @@ -569,23 +569,41 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) // 多脚本列表(Unity 语义):每行 = 脚本下拉 + 独立启停 + 删除; // 实例生命周期由 ScriptRuntime 对账(删行/改名 → 下帧 OnDestroy) const auto& names = Engine::Scripting::ScriptRegistry::Names(); - std::vector options; - options.push_back(""); - for (const auto& n : names) - { - options.push_back(n.c_str()); - } int removeIdx = -1; for (int i = 0; i < static_cast(sc->items.size()); ++i) { auto& item = sc->items[static_cast(i)]; ImGui::PushID(i); - int cur = 0; - for (size_t k = 0; k < names.size(); ++k) + // 去重下拉(DisallowMultipleComponent 语义):可选项 = + // "" + 未挂名 + 自身名(已挂的别人的名不出现,结构上 + // 选不出重复);自身名置于末尾保证已挂项正常回显 + std::vector options; + options.push_back(""); + for (const auto& n : names) { - if (names[k] == item.name) + if (n == item.name) { - cur = static_cast(k) + 1; + continue; + } + const bool attached = std::any_of( + sc->items.begin(), sc->items.end(), + [&](const Engine::ECS::ScriptComponent::Item& it) + { return it.name == n; }); + if (!attached) + { + options.push_back(n.c_str()); + } + } + if (!item.name.empty()) + { + options.push_back(item.name.c_str()); + } + int cur = 0; + for (size_t k = 0; k < options.size(); ++k) + { + if (item.name == options[k]) + { + cur = static_cast(k); } } ImGui::SetNextItemWidth( @@ -593,7 +611,7 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) if (ImGui::Combo("##script", &cur, options.data(), static_cast(options.size()))) { - item.name = (cur <= 0) ? "" : names[static_cast(cur) - 1]; + item.name = (cur <= 0) ? "" : options[static_cast(cur)]; } ImGui::SameLine(); ImGui::Checkbox("##en", &item.active); @@ -615,7 +633,25 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) { sc->items.erase(sc->items.begin() + removeIdx); } - if (ImGui::Button("+ Add Script")) + // 已注册脚本全部挂上 → 禁止再加(去重语义,无意义项) + const bool allAttached = !names.empty() && + std::all_of(names.begin(), names.end(), + [&](const std::string& n) + { + return std::any_of( + sc->items.begin(), sc->items.end(), + [&](const Engine::ECS::ScriptComponent::Item& it) + { return it.name == n; }); + }); + if (allAttached) + { + ImGui::BeginDisabled(true); + ImGui::Button("+ Add Script"); + ImGui::EndDisabled(); + ImGui::SameLine(); + ImGui::TextDisabled("all registered scripts attached"); + } + else if (ImGui::Button("+ Add Script")) { sc->items.push_back({}); } diff --git a/Engine/src/Serialization/SceneSerializer.cpp b/Engine/src/Serialization/SceneSerializer.cpp index f2977ba..06e0a32 100644 --- a/Engine/src/Serialization/SceneSerializer.cpp +++ b/Engine/src/Serialization/SceneSerializer.cpp @@ -28,6 +28,7 @@ #include #include +#include #include #include #include @@ -672,7 +673,16 @@ namespace Engine::Serialization { ECS::ScriptComponent::Item item; item.name = it["Name"].as(""); item.active = it["Active"].as(true); - sc.items.push_back(std::move(item)); + // 去重兜底:同名项只收第一份(UI 已杜绝重复, + // 手编场景仍可能写出重复) + const bool dup = std::any_of( + sc.items.begin(), sc.items.end(), + [&](const ECS::ScriptComponent::Item& e) { + return e.name == item.name; + }); + if (!dup) { + sc.items.push_back(std::move(item)); + } } } else { // 旧格式:Script: {ScriptName: ..., Active: ...}