Skip to content

Commit dfa3272

Browse files
committed
feat: update prefab management commands to use 'prefabPath' and add 'create_from_gameobject' action
1 parent 863984c commit dfa3272

File tree

3 files changed

+158
-160
lines changed

3 files changed

+158
-160
lines changed

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

Lines changed: 41 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public void OpenStage_OpensPrefabInIsolation()
4646
var openParams = new JObject
4747
{
4848
["action"] = "open_stage",
49-
["path"] = prefabPath
49+
["prefabPath"] = prefabPath
5050
};
5151

5252
var openResult = ToJObject(ManagePrefabs.HandleCommand(openParams));
@@ -94,7 +94,7 @@ public void CloseStage_ClosesOpenPrefabStage()
9494
ManagePrefabs.HandleCommand(new JObject
9595
{
9696
["action"] = "open_stage",
97-
["path"] = prefabPath
97+
["prefabPath"] = prefabPath
9898
});
9999

100100
var closeResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
@@ -122,7 +122,7 @@ public void SaveOpenStage_SavesDirtyChanges()
122122
ManagePrefabs.HandleCommand(new JObject
123123
{
124124
["action"] = "open_stage",
125-
["path"] = prefabPath
125+
["prefabPath"] = prefabPath
126126
});
127127

128128
PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
@@ -162,62 +162,60 @@ public void SaveOpenStage_ReturnsError_WhenNoStageOpen()
162162
}
163163

164164
[Test]
165-
public void ApplyInstanceOverrides_UpdatesPrefabAsset()
165+
public void CreateFromGameObject_CreatesPrefabAndLinksInstance()
166166
{
167-
string prefabPath = CreateTestPrefab("ApplyOverridesCube");
168-
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
167+
EnsureTempDirectoryExists();
168+
StageUtility.GoToMainStage();
169169

170-
GameObject instance = (GameObject)PrefabUtility.InstantiatePrefab(prefabAsset);
171-
instance.name = "ApplyOverridesInstance";
170+
string prefabPath = Path.Combine(TempDirectory, "SceneObjectSaved.prefab").Replace('\\', '/');
171+
GameObject sceneObject = new GameObject("ScenePrefabSource");
172172

173173
try
174174
{
175-
instance.transform.localScale = new Vector3(3f, 3f, 3f);
176-
177-
var applyResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
175+
var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
178176
{
179-
["action"] = "apply_instance_overrides",
180-
["instanceId"] = instance.GetInstanceID()
177+
["action"] = "create_from_gameobject",
178+
["target"] = sceneObject.name,
179+
["prefabPath"] = prefabPath
181180
}));
182181

183-
Assert.IsTrue(applyResult.Value<bool>("success"), "apply_instance_overrides should succeed for prefab instance.");
182+
Assert.IsTrue(result.Value<bool>("success"), "create_from_gameobject should succeed for a valid scene object.");
184183

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-
}
184+
var data = result["data"] as JObject;
185+
Assert.IsNotNull(data, "Response data should include prefab information.");
194186

195-
[Test]
196-
public void RevertInstanceOverrides_RevertsToPrefabDefaults()
197-
{
198-
string prefabPath = CreateTestPrefab("RevertOverridesCube");
199-
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
187+
string savedPath = data.Value<string>("prefabPath");
188+
Assert.AreEqual(prefabPath, savedPath, "Returned prefab path should match the requested path.");
200189

201-
GameObject instance = (GameObject)PrefabUtility.InstantiatePrefab(prefabAsset);
202-
instance.name = "RevertOverridesInstance";
190+
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(savedPath);
191+
Assert.IsNotNull(prefabAsset, "Prefab asset should exist at the saved path.");
203192

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-
}));
193+
int instanceId = data.Value<int>("instanceId");
194+
var linkedInstance = EditorUtility.InstanceIDToObject(instanceId) as GameObject;
195+
Assert.IsNotNull(linkedInstance, "Linked instance should resolve from instanceId.");
196+
Assert.AreEqual(savedPath, PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(linkedInstance), "Instance should be connected to the new prefab.");
213197

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.");
198+
sceneObject = linkedInstance;
216199
}
217200
finally
218201
{
219-
UnityEngine.Object.DestroyImmediate(instance);
220-
AssetDatabase.DeleteAsset(prefabPath);
202+
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(prefabPath) != null)
203+
{
204+
AssetDatabase.DeleteAsset(prefabPath);
205+
}
206+
207+
if (sceneObject != null)
208+
{
209+
if (PrefabUtility.IsPartOfPrefabInstance(sceneObject))
210+
{
211+
PrefabUtility.UnpackPrefabInstance(
212+
sceneObject,
213+
PrefabUnpackMode.Completely,
214+
InteractionMode.AutomatedAction
215+
);
216+
}
217+
UnityEngine.Object.DestroyImmediate(sceneObject, true);
218+
}
221219
}
222220
}
223221

UnityMcpBridge/Editor/Tools/Prefabs/ManagePrefabs.cs

Lines changed: 88 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
using System;
2+
using System.IO;
23
using MCPForUnity.Editor.Helpers;
34
using Newtonsoft.Json.Linq;
45
using UnityEditor;
56
using UnityEditor.SceneManagement;
67
using UnityEngine;
8+
using UnityEngine.SceneManagement;
79

810
namespace MCPForUnity.Editor.Tools.Prefabs
911
{
1012
public static class ManagePrefabs
1113
{
12-
private const string SupportedActions = "open_stage, close_stage, save_open_stage, apply_instance_overrides, revert_instance_overrides";
14+
private const string SupportedActions = "open_stage, close_stage, save_open_stage, create_from_gameobject";
1315

1416
public static object HandleCommand(JObject @params)
1517
{
@@ -34,10 +36,8 @@ public static object HandleCommand(JObject @params)
3436
return CloseStage(@params);
3537
case "save_open_stage":
3638
return SaveOpenStage();
37-
case "apply_instance_overrides":
38-
return ApplyInstanceOverrides(@params);
39-
case "revert_instance_overrides":
40-
return RevertInstanceOverrides(@params);
39+
case "create_from_gameobject":
40+
return CreatePrefabFromGameObject(@params);
4141
default:
4242
return Response.Error($"Unknown action: '{action}'. Valid actions are: {SupportedActions}.");
4343
}
@@ -51,13 +51,13 @@ public static object HandleCommand(JObject @params)
5151

5252
private static object OpenStage(JObject @params)
5353
{
54-
string path = @params["path"]?.ToString();
55-
if (string.IsNullOrEmpty(path))
54+
string prefabPath = @params["prefabPath"]?.ToString();
55+
if (string.IsNullOrEmpty(prefabPath))
5656
{
5757
return Response.Error("'path' parameter is required for open_stage.");
5858
}
5959

60-
string sanitizedPath = AssetPathUtility.SanitizeAssetPath(path);
60+
string sanitizedPath = AssetPathUtility.SanitizeAssetPath(prefabPath);
6161
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(sanitizedPath);
6262
if (prefabAsset == null)
6363
{
@@ -125,119 +125,120 @@ private static void SaveStagePrefab(PrefabStage stage)
125125
}
126126
}
127127

128-
private static object ApplyInstanceOverrides(JObject @params)
128+
private static object CreatePrefabFromGameObject(JObject @params)
129129
{
130-
if (!TryGetPrefabInstance(@params, out GameObject instanceRoot, out string error))
130+
string targetName = @params["target"]?.ToString() ?? @params["name"]?.ToString();
131+
if (string.IsNullOrEmpty(targetName))
131132
{
132-
return Response.Error(error);
133+
return Response.Error("'target' parameter is required for create_from_gameobject.");
133134
}
134135

135-
PrefabUtility.ApplyPrefabInstance(instanceRoot, InteractionMode.AutomatedAction);
136-
string prefabAssetPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(instanceRoot);
136+
bool includeInactive = @params["searchInactive"]?.ToObject<bool>() ?? false;
137+
GameObject sourceObject = FindSceneObjectByName(targetName, includeInactive);
138+
if (sourceObject == null)
139+
{
140+
return Response.Error($"GameObject '{targetName}' not found in the active scene.");
141+
}
137142

138-
return Response.Success(
139-
$"Applied overrides on prefab instance '{instanceRoot.name}'.",
140-
new
141-
{
142-
prefabAssetPath,
143-
instanceId = instanceRoot.GetInstanceID()
144-
}
145-
);
146-
}
143+
if (PrefabUtility.IsPartOfPrefabAsset(sourceObject))
144+
{
145+
return Response.Error(
146+
$"GameObject '{sourceObject.name}' is part of a prefab asset. Open the prefab stage to save changes instead."
147+
);
148+
}
147149

148-
private static object RevertInstanceOverrides(JObject @params)
149-
{
150-
if (!TryGetPrefabInstance(@params, out GameObject instanceRoot, out string error))
150+
PrefabInstanceStatus status = PrefabUtility.GetPrefabInstanceStatus(sourceObject);
151+
if (status != PrefabInstanceStatus.NotAPrefab)
151152
{
152-
return Response.Error(error);
153+
return Response.Error(
154+
$"GameObject '{sourceObject.name}' is already linked to an existing prefab instance."
155+
);
153156
}
154157

155-
PrefabUtility.RevertPrefabInstance(instanceRoot, InteractionMode.AutomatedAction);
158+
string requestedPath = @params["prefabPath"]?.ToString() ?? @params["path"]?.ToString();
159+
if (string.IsNullOrWhiteSpace(requestedPath))
160+
{
161+
return Response.Error("'prefabPath' (or 'path') parameter is required for create_from_gameobject.");
162+
}
156163

157-
return Response.Success(
158-
$"Reverted overrides on prefab instance '{instanceRoot.name}'.",
159-
new
160-
{
161-
instanceId = instanceRoot.GetInstanceID()
162-
}
163-
);
164-
}
164+
string sanitizedPath = AssetPathUtility.SanitizeAssetPath(requestedPath);
165+
if (!sanitizedPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
166+
{
167+
sanitizedPath += ".prefab";
168+
}
165169

166-
private static bool TryGetPrefabInstance(JObject @params, out GameObject instanceRoot, out string error)
167-
{
168-
instanceRoot = null;
169-
error = null;
170+
bool allowOverwrite = @params["allowOverwrite"]?.ToObject<bool>() ?? false;
171+
string finalPath = sanitizedPath;
170172

171-
JToken instanceIdToken = @params["instanceId"] ?? @params["instanceID"];
172-
if (instanceIdToken != null && instanceIdToken.Type == JTokenType.Integer)
173+
if (!allowOverwrite && AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(finalPath) != null)
173174
{
174-
int instanceId = instanceIdToken.Value<int>();
175-
if (!TryResolveInstance(instanceId, out instanceRoot, out error))
176-
{
177-
return false;
178-
}
179-
return true;
175+
finalPath = AssetDatabase.GenerateUniqueAssetPath(finalPath);
180176
}
181177

182-
string targetName = @params["target"]?.ToString();
183-
if (!string.IsNullOrEmpty(targetName))
178+
EnsureAssetDirectoryExists(finalPath);
179+
180+
try
184181
{
185-
GameObject target = GameObject.Find(targetName);
186-
if (target == null)
182+
GameObject connectedInstance = PrefabUtility.SaveAsPrefabAssetAndConnect(
183+
sourceObject,
184+
finalPath,
185+
InteractionMode.AutomatedAction
186+
);
187+
188+
if (connectedInstance == null)
187189
{
188-
error = $"GameObject '{targetName}' not found in the current scene.";
189-
return false;
190+
return Response.Error($"Failed to save prefab asset at '{finalPath}'.");
190191
}
191192

192-
instanceRoot = GetPrefabInstanceRoot(target, out error);
193-
return instanceRoot != null;
194-
}
195-
196-
error = "Parameter 'instanceId' (or 'target') is required for this action.";
197-
return false;
198-
}
199-
200-
private static bool TryResolveInstance(int instanceId, out GameObject instanceRoot, out string error)
201-
{
202-
instanceRoot = null;
203-
error = null;
193+
Selection.activeGameObject = connectedInstance;
204194

205-
GameObject obj = EditorUtility.InstanceIDToObject(instanceId) as GameObject;
206-
if (obj == null)
195+
return Response.Success(
196+
$"Prefab created at '{finalPath}' and instance linked.",
197+
new
198+
{
199+
prefabPath = finalPath,
200+
instanceId = connectedInstance.GetInstanceID()
201+
}
202+
);
203+
}
204+
catch (Exception e)
207205
{
208-
error = $"No GameObject found for instanceId {instanceId}.";
209-
return false;
206+
return Response.Error($"Error saving prefab asset at '{finalPath}': {e.Message}");
210207
}
211-
212-
instanceRoot = GetPrefabInstanceRoot(obj, out error);
213-
return instanceRoot != null;
214208
}
215209

216-
private static GameObject GetPrefabInstanceRoot(GameObject obj, out string error)
210+
private static void EnsureAssetDirectoryExists(string assetPath)
217211
{
218-
error = null;
219-
220-
if (!PrefabUtility.IsPartOfPrefabInstance(obj))
212+
string directory = Path.GetDirectoryName(assetPath);
213+
if (string.IsNullOrEmpty(directory))
221214
{
222-
error = $"GameObject '{obj.name}' is not part of a prefab instance.";
223-
return null;
215+
return;
224216
}
225217

226-
GameObject root = PrefabUtility.GetOutermostPrefabInstanceRoot(obj);
227-
if (root == null)
218+
string fullDirectory = Path.Combine(Directory.GetCurrentDirectory(), directory);
219+
if (!Directory.Exists(fullDirectory))
228220
{
229-
error = $"Failed to resolve prefab instance root for '{obj.name}'.";
230-
return null;
221+
Directory.CreateDirectory(fullDirectory);
222+
AssetDatabase.Refresh();
231223
}
224+
}
232225

233-
PrefabInstanceStatus status = PrefabUtility.GetPrefabInstanceStatus(root);
234-
if (status == PrefabInstanceStatus.NotAPrefab)
226+
private static GameObject FindSceneObjectByName(string name, bool includeInactive)
227+
{
228+
Scene activeScene = SceneManager.GetActiveScene();
229+
foreach (GameObject root in activeScene.GetRootGameObjects())
235230
{
236-
error = $"GameObject '{obj.name}' is not recognised as a prefab instance.";
237-
return null;
231+
foreach (Transform transform in root.GetComponentsInChildren<Transform>(includeInactive))
232+
{
233+
GameObject candidate = transform.gameObject;
234+
if (candidate.name == name)
235+
{
236+
return candidate;
237+
}
238+
}
238239
}
239240

240-
return root;
241+
return null;
241242
}
242243

243244
private static object SerializeStage(PrefabStage stage)

0 commit comments

Comments
 (0)