Skip to content

Commit 08bb831

Browse files
committed
Add new tools to manage a prefab, particularly, making them staged.
This might be enough, but it's possible we may have to extract some logic from ManageGameObject
1 parent 2d6baeb commit 08bb831

File tree

6 files changed

+597
-3
lines changed

6 files changed

+597
-3
lines changed
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
using System.IO;
2+
using Newtonsoft.Json.Linq;
3+
using NUnit.Framework;
4+
using UnityEditor;
5+
using UnityEditor.SceneManagement;
6+
using UnityEngine;
7+
using MCPForUnity.Editor.Tools.Prefabs;
8+
using MCPForUnity.Editor.Tools;
9+
10+
namespace MCPForUnityTests.Editor.Tools
11+
{
12+
public class ManagePrefabsTests
13+
{
14+
private const string TempDirectory = "Assets/Temp/ManagePrefabsTests";
15+
16+
[SetUp]
17+
public void SetUp()
18+
{
19+
StageUtility.GoToMainStage();
20+
EnsureTempDirectoryExists();
21+
}
22+
23+
[TearDown]
24+
public void TearDown()
25+
{
26+
StageUtility.GoToMainStage();
27+
}
28+
29+
[OneTimeTearDown]
30+
public void CleanupAll()
31+
{
32+
StageUtility.GoToMainStage();
33+
if (AssetDatabase.IsValidFolder(TempDirectory))
34+
{
35+
AssetDatabase.DeleteAsset(TempDirectory);
36+
}
37+
}
38+
39+
[Test]
40+
public void OpenStage_OpensPrefabInIsolation()
41+
{
42+
string prefabPath = CreateTestPrefab("OpenStageCube");
43+
44+
try
45+
{
46+
var openParams = new JObject
47+
{
48+
["action"] = "open_stage",
49+
["path"] = prefabPath
50+
};
51+
52+
var openResult = ToJObject(ManagePrefabs.HandleCommand(openParams));
53+
54+
Assert.IsTrue(openResult.Value<bool>("success"), "open_stage should succeed for a valid prefab.");
55+
56+
PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
57+
Assert.IsNotNull(stage, "Prefab stage should be open after open_stage.");
58+
Assert.AreEqual(prefabPath, stage.assetPath, "Opened stage should match prefab path.");
59+
60+
var stageInfo = ToJObject(ManageEditor.HandleCommand(new JObject { ["action"] = "get_prefab_stage" }));
61+
Assert.IsTrue(stageInfo.Value<bool>("success"), "get_prefab_stage should succeed when stage is open.");
62+
63+
var data = stageInfo["data"] as JObject;
64+
Assert.IsNotNull(data, "Stage info should include data payload.");
65+
Assert.IsTrue(data.Value<bool>("isOpen"));
66+
Assert.AreEqual(prefabPath, data.Value<string>("assetPath"));
67+
}
68+
finally
69+
{
70+
StageUtility.GoToMainStage();
71+
AssetDatabase.DeleteAsset(prefabPath);
72+
}
73+
}
74+
75+
[Test]
76+
public void CloseStage_ReturnsSuccess_WhenNoStageOpen()
77+
{
78+
StageUtility.GoToMainStage();
79+
var closeResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
80+
{
81+
["action"] = "close_stage"
82+
}));
83+
84+
Assert.IsTrue(closeResult.Value<bool>("success"), "close_stage should succeed even if no stage is open.");
85+
}
86+
87+
[Test]
88+
public void CloseStage_ClosesOpenPrefabStage()
89+
{
90+
string prefabPath = CreateTestPrefab("CloseStageCube");
91+
92+
try
93+
{
94+
ManagePrefabs.HandleCommand(new JObject
95+
{
96+
["action"] = "open_stage",
97+
["path"] = prefabPath
98+
});
99+
100+
var closeResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
101+
{
102+
["action"] = "close_stage"
103+
}));
104+
105+
Assert.IsTrue(closeResult.Value<bool>("success"), "close_stage should succeed when stage is open.");
106+
Assert.IsNull(PrefabStageUtility.GetCurrentPrefabStage(), "Prefab stage should be closed after close_stage.");
107+
}
108+
finally
109+
{
110+
StageUtility.GoToMainStage();
111+
AssetDatabase.DeleteAsset(prefabPath);
112+
}
113+
}
114+
115+
[Test]
116+
public void SaveOpenStage_SavesDirtyChanges()
117+
{
118+
string prefabPath = CreateTestPrefab("SaveStageCube");
119+
120+
try
121+
{
122+
ManagePrefabs.HandleCommand(new JObject
123+
{
124+
["action"] = "open_stage",
125+
["path"] = prefabPath
126+
});
127+
128+
PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
129+
Assert.IsNotNull(stage, "Stage should be open before modifying.");
130+
131+
stage.prefabContentsRoot.transform.localScale = new Vector3(2f, 2f, 2f);
132+
133+
var saveResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
134+
{
135+
["action"] = "save_open_stage"
136+
}));
137+
138+
Assert.IsTrue(saveResult.Value<bool>("success"), "save_open_stage should succeed when stage is open.");
139+
Assert.IsFalse(stage.scene.isDirty, "Stage scene should not be dirty after saving.");
140+
141+
GameObject reloaded = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
142+
Assert.AreEqual(new Vector3(2f, 2f, 2f), reloaded.transform.localScale, "Saved prefab asset should include changes from open stage.");
143+
}
144+
finally
145+
{
146+
StageUtility.GoToMainStage();
147+
AssetDatabase.DeleteAsset(prefabPath);
148+
}
149+
}
150+
151+
[Test]
152+
public void SaveOpenStage_ReturnsError_WhenNoStageOpen()
153+
{
154+
StageUtility.GoToMainStage();
155+
156+
var saveResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
157+
{
158+
["action"] = "save_open_stage"
159+
}));
160+
161+
Assert.IsFalse(saveResult.Value<bool>("success"), "save_open_stage should fail when no stage is open.");
162+
}
163+
164+
[Test]
165+
public void ApplyInstanceOverrides_UpdatesPrefabAsset()
166+
{
167+
string prefabPath = CreateTestPrefab("ApplyOverridesCube");
168+
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
169+
170+
GameObject instance = (GameObject)PrefabUtility.InstantiatePrefab(prefabAsset);
171+
instance.name = "ApplyOverridesInstance";
172+
173+
try
174+
{
175+
instance.transform.localScale = new Vector3(3f, 3f, 3f);
176+
177+
var applyResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
178+
{
179+
["action"] = "apply_instance_overrides",
180+
["instanceId"] = instance.GetInstanceID()
181+
}));
182+
183+
Assert.IsTrue(applyResult.Value<bool>("success"), "apply_instance_overrides should succeed for prefab instance.");
184+
185+
GameObject reloaded = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
186+
Assert.AreEqual(new Vector3(3f, 3f, 3f), reloaded.transform.localScale, "Prefab asset should reflect applied overrides.");
187+
}
188+
finally
189+
{
190+
UnityEngine.Object.DestroyImmediate(instance);
191+
AssetDatabase.DeleteAsset(prefabPath);
192+
}
193+
}
194+
195+
[Test]
196+
public void RevertInstanceOverrides_RevertsToPrefabDefaults()
197+
{
198+
string prefabPath = CreateTestPrefab("RevertOverridesCube");
199+
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
200+
201+
GameObject instance = (GameObject)PrefabUtility.InstantiatePrefab(prefabAsset);
202+
instance.name = "RevertOverridesInstance";
203+
204+
try
205+
{
206+
instance.transform.localScale = new Vector3(4f, 4f, 4f);
207+
208+
var revertResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
209+
{
210+
["action"] = "revert_instance_overrides",
211+
["instanceId"] = instance.GetInstanceID()
212+
}));
213+
214+
Assert.IsTrue(revertResult.Value<bool>("success"), "revert_instance_overrides should succeed for prefab instance.");
215+
Assert.AreEqual(Vector3.one, instance.transform.localScale, "Prefab instance should revert to default scale.");
216+
}
217+
finally
218+
{
219+
UnityEngine.Object.DestroyImmediate(instance);
220+
AssetDatabase.DeleteAsset(prefabPath);
221+
}
222+
}
223+
224+
private static string CreateTestPrefab(string name)
225+
{
226+
EnsureTempDirectoryExists();
227+
228+
GameObject temp = GameObject.CreatePrimitive(PrimitiveType.Cube);
229+
temp.name = name;
230+
231+
string path = Path.Combine(TempDirectory, name + ".prefab").Replace('\\', '/');
232+
PrefabUtility.SaveAsPrefabAsset(temp, path, out bool success);
233+
UnityEngine.Object.DestroyImmediate(temp);
234+
235+
Assert.IsTrue(success, "PrefabUtility.SaveAsPrefabAsset should succeed for test prefab.");
236+
return path;
237+
}
238+
239+
private static void EnsureTempDirectoryExists()
240+
{
241+
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
242+
{
243+
AssetDatabase.CreateFolder("Assets", "Temp");
244+
}
245+
246+
if (!AssetDatabase.IsValidFolder(TempDirectory))
247+
{
248+
AssetDatabase.CreateFolder("Assets/Temp", "ManagePrefabsTests");
249+
}
250+
}
251+
252+
private static JObject ToJObject(object result)
253+
{
254+
return result as JObject ?? JObject.FromObject(result);
255+
}
256+
}
257+
}

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsTests.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

UnityMcpBridge/Editor/Tools/ManageEditor.cs

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
using Newtonsoft.Json.Linq;
66
using UnityEditor;
77
using UnityEditorInternal; // Required for tag management
8+
using UnityEditor.SceneManagement;
89
using UnityEngine;
9-
using MCPForUnity.Editor.Helpers; // For Response class
10+
using MCPForUnity.Editor.Helpers;
1011

1112
namespace MCPForUnity.Editor.Tools
1213
{
@@ -98,6 +99,8 @@ public static object HandleCommand(JObject @params)
9899
return GetActiveTool();
99100
case "get_selection":
100101
return GetSelection();
102+
case "get_prefab_stage":
103+
return GetPrefabStageInfo();
101104
case "set_active_tool":
102105
string toolName = @params["toolName"]?.ToString();
103106
if (string.IsNullOrEmpty(toolName))
@@ -140,7 +143,7 @@ public static object HandleCommand(JObject @params)
140143

141144
default:
142145
return Response.Error(
143-
$"Unknown action: '{action}'. Supported actions include play, pause, stop, get_state, get_project_root, get_windows, get_active_tool, get_selection, set_active_tool, add_tag, remove_tag, get_tags, add_layer, remove_layer, get_layers."
146+
$"Unknown action: '{action}'. Supported actions include play, pause, stop, get_state, get_project_root, get_windows, get_active_tool, get_selection, get_prefab_stage, set_active_tool, add_tag, remove_tag, get_tags, add_layer, remove_layer, get_layers."
144147
);
145148
}
146149
}
@@ -244,6 +247,35 @@ private static object GetEditorWindows()
244247
}
245248
}
246249

250+
private static object GetPrefabStageInfo()
251+
{
252+
try
253+
{
254+
PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
255+
if (stage == null)
256+
{
257+
return Response.Success
258+
("No prefab stage is currently open.", new { isOpen = false });
259+
}
260+
261+
return Response.Success(
262+
"Prefab stage info retrieved.",
263+
new
264+
{
265+
isOpen = true,
266+
assetPath = stage.assetPath,
267+
prefabRootName = stage.prefabContentsRoot != null ? stage.prefabContentsRoot.name : null,
268+
mode = stage.mode.ToString(),
269+
isDirty = stage.scene.isDirty
270+
}
271+
);
272+
}
273+
catch (Exception e)
274+
{
275+
return Response.Error($"Error getting prefab stage info: {e.Message}");
276+
}
277+
}
278+
247279
private static object GetActiveTool()
248280
{
249281
try
@@ -610,4 +642,3 @@ public static string GetActiveToolName()
610642
}
611643
}
612644
}
613-

UnityMcpBridge/Editor/Tools/Prefabs.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)