-
Notifications
You must be signed in to change notification settings - Fork 452
Open and close prefabs in the stage view + create them #283
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+750
−77
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
6b6172f
refactor: remove unused UnityEngine references from menu item classes
msanatan 2d6baeb
Merge branch 'main' into improved-prefab-support
msanatan 08bb831
Add new tools to manage a prefab, particularly, making them staged.
msanatan 7f940db
feat: add AssetPathUtility for asset path normalization and update re…
msanatan 863984c
feat: add prefab management tools and register them with the MCP server
msanatan dfa3272
feat: update prefab management commands to use 'prefabPath' and add '…
msanatan 9002957
fix: update parameter references to 'prefabPath' in ManagePrefabs and…
msanatan fdd1262
fix: clarify error message for missing 'prefabPath' in create_from_ga…
msanatan 10bfe54
fix: ensure pull request triggers for unity tests workflow
msanatan 63b1a42
Revert "fix: ensure pull request triggers for unity tests workflow"
msanatan 745aa32
Remove delayed execution of executing menu item, fixing #279
msanatan 4d70f93
Merge pull request #1 from msanatan/menu-item-fix
msanatan 7972faa
docs: clarify menu item tool description with guidance to use list ac…
msanatan 72c8983
feat: add version update for server_version.txt in bump-version workflow
msanatan f685e26
fix: simplify error message for failed menu item execution
msanatan c72549c
Merge branch 'main' into improved-prefab-support
msanatan 8a7d265
Merge branch 'main' into improved-prefab-support
msanatan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
255 changes: 255 additions & 0 deletions
255
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,255 @@ | ||
using System.IO; | ||
using Newtonsoft.Json.Linq; | ||
using NUnit.Framework; | ||
using UnityEditor; | ||
using UnityEditor.SceneManagement; | ||
using UnityEngine; | ||
using MCPForUnity.Editor.Tools.Prefabs; | ||
using MCPForUnity.Editor.Tools; | ||
|
||
namespace MCPForUnityTests.Editor.Tools | ||
{ | ||
public class ManagePrefabsTests | ||
{ | ||
private const string TempDirectory = "Assets/Temp/ManagePrefabsTests"; | ||
|
||
[SetUp] | ||
public void SetUp() | ||
{ | ||
StageUtility.GoToMainStage(); | ||
EnsureTempDirectoryExists(); | ||
} | ||
|
||
[TearDown] | ||
public void TearDown() | ||
{ | ||
StageUtility.GoToMainStage(); | ||
} | ||
|
||
[OneTimeTearDown] | ||
public void CleanupAll() | ||
{ | ||
StageUtility.GoToMainStage(); | ||
if (AssetDatabase.IsValidFolder(TempDirectory)) | ||
{ | ||
AssetDatabase.DeleteAsset(TempDirectory); | ||
} | ||
} | ||
|
||
[Test] | ||
public void OpenStage_OpensPrefabInIsolation() | ||
{ | ||
string prefabPath = CreateTestPrefab("OpenStageCube"); | ||
|
||
try | ||
{ | ||
var openParams = new JObject | ||
{ | ||
["action"] = "open_stage", | ||
["prefabPath"] = prefabPath | ||
}; | ||
|
||
var openResult = ToJObject(ManagePrefabs.HandleCommand(openParams)); | ||
|
||
Assert.IsTrue(openResult.Value<bool>("success"), "open_stage should succeed for a valid prefab."); | ||
|
||
PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage(); | ||
Assert.IsNotNull(stage, "Prefab stage should be open after open_stage."); | ||
Assert.AreEqual(prefabPath, stage.assetPath, "Opened stage should match prefab path."); | ||
|
||
var stageInfo = ToJObject(ManageEditor.HandleCommand(new JObject { ["action"] = "get_prefab_stage" })); | ||
Assert.IsTrue(stageInfo.Value<bool>("success"), "get_prefab_stage should succeed when stage is open."); | ||
|
||
var data = stageInfo["data"] as JObject; | ||
Assert.IsNotNull(data, "Stage info should include data payload."); | ||
Assert.IsTrue(data.Value<bool>("isOpen")); | ||
Assert.AreEqual(prefabPath, data.Value<string>("assetPath")); | ||
} | ||
finally | ||
{ | ||
StageUtility.GoToMainStage(); | ||
AssetDatabase.DeleteAsset(prefabPath); | ||
} | ||
} | ||
|
||
[Test] | ||
public void CloseStage_ReturnsSuccess_WhenNoStageOpen() | ||
{ | ||
StageUtility.GoToMainStage(); | ||
var closeResult = ToJObject(ManagePrefabs.HandleCommand(new JObject | ||
{ | ||
["action"] = "close_stage" | ||
})); | ||
|
||
Assert.IsTrue(closeResult.Value<bool>("success"), "close_stage should succeed even if no stage is open."); | ||
} | ||
|
||
[Test] | ||
public void CloseStage_ClosesOpenPrefabStage() | ||
{ | ||
string prefabPath = CreateTestPrefab("CloseStageCube"); | ||
|
||
try | ||
{ | ||
ManagePrefabs.HandleCommand(new JObject | ||
{ | ||
["action"] = "open_stage", | ||
["prefabPath"] = prefabPath | ||
}); | ||
|
||
var closeResult = ToJObject(ManagePrefabs.HandleCommand(new JObject | ||
{ | ||
["action"] = "close_stage" | ||
})); | ||
|
||
Assert.IsTrue(closeResult.Value<bool>("success"), "close_stage should succeed when stage is open."); | ||
Assert.IsNull(PrefabStageUtility.GetCurrentPrefabStage(), "Prefab stage should be closed after close_stage."); | ||
} | ||
finally | ||
{ | ||
StageUtility.GoToMainStage(); | ||
AssetDatabase.DeleteAsset(prefabPath); | ||
} | ||
} | ||
|
||
[Test] | ||
public void SaveOpenStage_SavesDirtyChanges() | ||
{ | ||
string prefabPath = CreateTestPrefab("SaveStageCube"); | ||
|
||
try | ||
{ | ||
ManagePrefabs.HandleCommand(new JObject | ||
{ | ||
["action"] = "open_stage", | ||
["prefabPath"] = prefabPath | ||
}); | ||
|
||
PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage(); | ||
Assert.IsNotNull(stage, "Stage should be open before modifying."); | ||
|
||
stage.prefabContentsRoot.transform.localScale = new Vector3(2f, 2f, 2f); | ||
|
||
var saveResult = ToJObject(ManagePrefabs.HandleCommand(new JObject | ||
{ | ||
["action"] = "save_open_stage" | ||
})); | ||
|
||
Assert.IsTrue(saveResult.Value<bool>("success"), "save_open_stage should succeed when stage is open."); | ||
Assert.IsFalse(stage.scene.isDirty, "Stage scene should not be dirty after saving."); | ||
|
||
GameObject reloaded = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath); | ||
Assert.AreEqual(new Vector3(2f, 2f, 2f), reloaded.transform.localScale, "Saved prefab asset should include changes from open stage."); | ||
} | ||
finally | ||
{ | ||
StageUtility.GoToMainStage(); | ||
AssetDatabase.DeleteAsset(prefabPath); | ||
} | ||
} | ||
|
||
[Test] | ||
public void SaveOpenStage_ReturnsError_WhenNoStageOpen() | ||
{ | ||
StageUtility.GoToMainStage(); | ||
|
||
var saveResult = ToJObject(ManagePrefabs.HandleCommand(new JObject | ||
{ | ||
["action"] = "save_open_stage" | ||
})); | ||
|
||
Assert.IsFalse(saveResult.Value<bool>("success"), "save_open_stage should fail when no stage is open."); | ||
} | ||
|
||
[Test] | ||
public void CreateFromGameObject_CreatesPrefabAndLinksInstance() | ||
{ | ||
EnsureTempDirectoryExists(); | ||
StageUtility.GoToMainStage(); | ||
|
||
string prefabPath = Path.Combine(TempDirectory, "SceneObjectSaved.prefab").Replace('\\', '/'); | ||
GameObject sceneObject = new GameObject("ScenePrefabSource"); | ||
|
||
try | ||
{ | ||
var result = ToJObject(ManagePrefabs.HandleCommand(new JObject | ||
{ | ||
["action"] = "create_from_gameobject", | ||
["target"] = sceneObject.name, | ||
["prefabPath"] = prefabPath | ||
})); | ||
|
||
Assert.IsTrue(result.Value<bool>("success"), "create_from_gameobject should succeed for a valid scene object."); | ||
|
||
var data = result["data"] as JObject; | ||
Assert.IsNotNull(data, "Response data should include prefab information."); | ||
|
||
string savedPath = data.Value<string>("prefabPath"); | ||
Assert.AreEqual(prefabPath, savedPath, "Returned prefab path should match the requested path."); | ||
|
||
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(savedPath); | ||
Assert.IsNotNull(prefabAsset, "Prefab asset should exist at the saved path."); | ||
|
||
int instanceId = data.Value<int>("instanceId"); | ||
var linkedInstance = EditorUtility.InstanceIDToObject(instanceId) as GameObject; | ||
Assert.IsNotNull(linkedInstance, "Linked instance should resolve from instanceId."); | ||
Assert.AreEqual(savedPath, PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(linkedInstance), "Instance should be connected to the new prefab."); | ||
|
||
sceneObject = linkedInstance; | ||
} | ||
finally | ||
{ | ||
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(prefabPath) != null) | ||
{ | ||
AssetDatabase.DeleteAsset(prefabPath); | ||
} | ||
|
||
if (sceneObject != null) | ||
{ | ||
if (PrefabUtility.IsPartOfPrefabInstance(sceneObject)) | ||
{ | ||
PrefabUtility.UnpackPrefabInstance( | ||
sceneObject, | ||
PrefabUnpackMode.Completely, | ||
InteractionMode.AutomatedAction | ||
); | ||
} | ||
UnityEngine.Object.DestroyImmediate(sceneObject, true); | ||
} | ||
} | ||
} | ||
|
||
private static string CreateTestPrefab(string name) | ||
{ | ||
EnsureTempDirectoryExists(); | ||
|
||
GameObject temp = GameObject.CreatePrimitive(PrimitiveType.Cube); | ||
temp.name = name; | ||
|
||
string path = Path.Combine(TempDirectory, name + ".prefab").Replace('\\', '/'); | ||
PrefabUtility.SaveAsPrefabAsset(temp, path, out bool success); | ||
UnityEngine.Object.DestroyImmediate(temp); | ||
|
||
Assert.IsTrue(success, "PrefabUtility.SaveAsPrefabAsset should succeed for test prefab."); | ||
return path; | ||
} | ||
|
||
private static void EnsureTempDirectoryExists() | ||
{ | ||
if (!AssetDatabase.IsValidFolder("Assets/Temp")) | ||
{ | ||
AssetDatabase.CreateFolder("Assets", "Temp"); | ||
} | ||
|
||
if (!AssetDatabase.IsValidFolder(TempDirectory)) | ||
{ | ||
AssetDatabase.CreateFolder("Assets/Temp", "ManagePrefabsTests"); | ||
} | ||
} | ||
|
||
private static JObject ToJObject(object result) | ||
{ | ||
return result as JObject ?? JObject.FromObject(result); | ||
} | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsTests.cs.meta
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
using System; | ||
|
||
namespace MCPForUnity.Editor.Helpers | ||
{ | ||
/// <summary> | ||
/// Provides common utility methods for working with Unity asset paths. | ||
/// </summary> | ||
public static class AssetPathUtility | ||
{ | ||
/// <summary> | ||
/// Normalizes a Unity asset path by ensuring forward slashes are used and that it is rooted under "Assets/". | ||
/// </summary> | ||
public static string SanitizeAssetPath(string path) | ||
{ | ||
if (string.IsNullOrEmpty(path)) | ||
{ | ||
return path; | ||
} | ||
|
||
path = path.Replace('\\', '/'); | ||
if (!path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
return "Assets/" + path.TrimStart('/'); | ||
} | ||
|
||
return path; | ||
} | ||
msanatan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Auto-updates the
server-version.txt
file