Skip to content

Commit 5a46319

Browse files
committed
Update
1 parent 3d33fba commit 5a46319

6 files changed

Lines changed: 115 additions & 67 deletions

File tree

MCPForUnity/Editor/Helpers/ComponentOps.cs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,7 @@ private static bool SetSerializedPropertyRecursive(SerializedProperty prop, JTok
592592
}
593593
}
594594

595-
private static bool SetObjectReference(SerializedProperty prop, JToken value, out string error)
595+
internal static bool SetObjectReference(SerializedProperty prop, JToken value, out string error)
596596
{
597597
error = null;
598598

@@ -785,6 +785,43 @@ private static bool AssignObjectReference(SerializedProperty prop, UnityEngine.O
785785
if (prop.objectReferenceValue != null)
786786
return true;
787787

788+
// Sub-asset fallback: e.g., Texture2D → Sprite
789+
string subAssetPath = AssetDatabase.GetAssetPath(resolved);
790+
if (!string.IsNullOrEmpty(subAssetPath))
791+
{
792+
var subAssets = AssetDatabase.LoadAllAssetsAtPath(subAssetPath);
793+
UnityEngine.Object match = null;
794+
int matchCount = 0;
795+
foreach (var sub in subAssets)
796+
{
797+
if (sub == null || sub == resolved) continue;
798+
prop.objectReferenceValue = sub;
799+
if (prop.objectReferenceValue != null)
800+
{
801+
match = sub;
802+
matchCount++;
803+
if (matchCount > 1) break;
804+
}
805+
}
806+
807+
if (matchCount == 1)
808+
{
809+
prop.objectReferenceValue = match;
810+
return true;
811+
}
812+
813+
// Clean up: probing may have left the property dirty
814+
prop.objectReferenceValue = null;
815+
816+
if (matchCount > 1)
817+
{
818+
error = $"Multiple compatible sub-assets found in '{subAssetPath}'. " +
819+
"Use {\"guid\": \"...\", \"spriteName\": \"<name>\"} or " +
820+
"{\"guid\": \"...\", \"fileID\": <id>} for precise selection.";
821+
return false;
822+
}
823+
}
824+
788825
// If the resolved object is a GameObject but the property expects a Component,
789826
// try each component on the GameObject until one is accepted.
790827
if (resolved is GameObject go)

MCPForUnity/Editor/Tools/ManageScriptableObject.cs

Lines changed: 22 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -737,64 +737,20 @@ private static object ApplySet(SerializedObject so, string propertyPath, JObject
737737

738738
if (prop.propertyType == SerializedPropertyType.ObjectReference)
739739
{
740+
// Legacy "ref" key takes precedence for backward compatibility
740741
var refObj = patchObj["ref"] as JObject;
741742
var objRefValue = patchObj["value"];
742-
UnityEngine.Object newRef = null;
743-
string refGuid = refObj?["guid"]?.ToString();
744-
string refPath = refObj?["path"]?.ToString();
745-
string resolveMethod = "explicit";
743+
JToken resolveToken = refObj ?? objRefValue;
746744

747-
if (refObj == null && objRefValue?.Type == JTokenType.Null)
745+
if (!ComponentOps.SetObjectReference(prop, resolveToken, out string refError))
748746
{
749-
// Explicit null - clear the reference
750-
newRef = null;
751-
resolveMethod = "cleared";
752-
}
753-
else if (!string.IsNullOrEmpty(refGuid) || !string.IsNullOrEmpty(refPath))
754-
{
755-
// Traditional ref object with guid or path
756-
string resolvedPath = !string.IsNullOrEmpty(refGuid)
757-
? AssetDatabase.GUIDToAssetPath(refGuid)
758-
: AssetPathUtility.SanitizeAssetPath(refPath);
759-
760-
if (!string.IsNullOrEmpty(resolvedPath))
761-
{
762-
newRef = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(resolvedPath);
763-
}
764-
resolveMethod = !string.IsNullOrEmpty(refGuid) ? "ref.guid" : "ref.path";
765-
}
766-
else if (objRefValue?.Type == JTokenType.String)
767-
{
768-
// Phase 4: GUID shorthand - allow plain string value
769-
string strVal = objRefValue.ToString();
770-
771-
// Check if it's a GUID (32 hex characters, no dashes)
772-
if (Regex.IsMatch(strVal, @"^[0-9a-fA-F]{32}$"))
773-
{
774-
string guidPath = AssetDatabase.GUIDToAssetPath(strVal);
775-
if (!string.IsNullOrEmpty(guidPath))
776-
{
777-
newRef = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(guidPath);
778-
resolveMethod = "guid-shorthand";
779-
}
780-
}
781-
// Check if it looks like an asset path
782-
else if (strVal.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase) ||
783-
strVal.Contains("/"))
784-
{
785-
string sanitizedPath = AssetPathUtility.SanitizeAssetPath(strVal);
786-
newRef = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(sanitizedPath);
787-
resolveMethod = "path-shorthand";
788-
}
747+
return new { propertyPath, op = "set", ok = false, resolvedPropertyType = prop.propertyType.ToString(), message = refError };
789748
}
790749

791-
if (prop.objectReferenceValue != newRef)
792-
{
793-
prop.objectReferenceValue = newRef;
794-
changed = true;
795-
}
796-
797-
string refMessage = newRef == null ? "Cleared reference." : $"Set reference ({resolveMethod}).";
750+
changed = true;
751+
string refMessage = prop.objectReferenceValue == null
752+
? "Cleared reference."
753+
: $"Set reference to '{prop.objectReferenceValue.name}'.";
798754
return new { propertyPath, op = "set", ok = true, resolvedPropertyType = prop.propertyType.ToString(), message = refMessage };
799755
}
800756

@@ -919,6 +875,20 @@ private static bool TrySetValueRecursive(SerializedProperty prop, JToken valueTo
919875
return true;
920876
}
921877

878+
// ObjectReference - delegate to shared handler
879+
if (prop.propertyType == SerializedPropertyType.ObjectReference)
880+
{
881+
if (!ComponentOps.SetObjectReference(prop, valueToken, out string refError))
882+
{
883+
message = refError;
884+
return false;
885+
}
886+
message = prop.objectReferenceValue == null
887+
? "Cleared reference."
888+
: $"Set reference to '{prop.objectReferenceValue.name}'.";
889+
return true;
890+
}
891+
922892
// Supported Types: Integer, Boolean, Float, String, Enum, Vector2, Vector3, Vector4, Color
923893
// Using shared helpers from ParamCoercion and VectorParsing
924894
switch (prop.propertyType)

MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ private static object ModifyContents(JObject @params)
614614
}
615615

616616
// Apply modifications
617-
var modifyResult = ApplyModificationsToPrefabObject(targetGo, @params, prefabContents);
617+
var modifyResult = ApplyModificationsToPrefabObject(targetGo, @params, prefabContents, sanitizedPath);
618618
if (modifyResult.error != null)
619619
{
620620
return modifyResult.error;
@@ -725,7 +725,7 @@ private static GameObject FindInPrefabContents(GameObject prefabContents, string
725725
/// Applies modifications to a GameObject within loaded prefab contents.
726726
/// Returns (modified: bool, error: ErrorResponse or null).
727727
/// </summary>
728-
private static (bool modified, ErrorResponse error) ApplyModificationsToPrefabObject(GameObject targetGo, JObject @params, GameObject prefabRoot)
728+
private static (bool modified, ErrorResponse error) ApplyModificationsToPrefabObject(GameObject targetGo, JObject @params, GameObject prefabRoot, string editingPrefabPath)
729729
{
730730
bool modified = false;
731731

@@ -879,7 +879,7 @@ private static (bool modified, ErrorResponse error) ApplyModificationsToPrefabOb
879879
{
880880
foreach (var childToken in childArray)
881881
{
882-
var childResult = CreateSingleChildInPrefab(childToken, targetGo, prefabRoot);
882+
var childResult = CreateSingleChildInPrefab(childToken, targetGo, prefabRoot, editingPrefabPath);
883883
if (childResult.error != null)
884884
{
885885
return (false, childResult.error);
@@ -893,7 +893,7 @@ private static (bool modified, ErrorResponse error) ApplyModificationsToPrefabOb
893893
else
894894
{
895895
// Handle single child object
896-
var childResult = CreateSingleChildInPrefab(createChildToken, targetGo, prefabRoot);
896+
var childResult = CreateSingleChildInPrefab(createChildToken, targetGo, prefabRoot, editingPrefabPath);
897897
if (childResult.error != null)
898898
{
899899
return (false, childResult.error);
@@ -957,7 +957,7 @@ private static (bool modified, ErrorResponse error) ApplyModificationsToPrefabOb
957957
/// <summary>
958958
/// Creates a single child GameObject within the prefab contents.
959959
/// </summary>
960-
private static (bool created, ErrorResponse error) CreateSingleChildInPrefab(JToken createChildToken, GameObject defaultParent, GameObject prefabRoot)
960+
private static (bool created, ErrorResponse error) CreateSingleChildInPrefab(JToken createChildToken, GameObject defaultParent, GameObject prefabRoot, string editingPrefabPath)
961961
{
962962
JObject childParams;
963963
if (createChildToken is JObject obj)
@@ -991,8 +991,42 @@ private static (bool created, ErrorResponse error) CreateSingleChildInPrefab(JTo
991991

992992
// Create the GameObject
993993
GameObject newChild;
994+
string sourcePrefabPath = childParams["sourcePrefabPath"]?.ToString() ?? childParams["source_prefab_path"]?.ToString();
994995
string primitiveType = childParams["primitiveType"]?.ToString() ?? childParams["primitive_type"]?.ToString();
995-
if (!string.IsNullOrEmpty(primitiveType))
996+
997+
if (!string.IsNullOrEmpty(sourcePrefabPath) && !string.IsNullOrEmpty(primitiveType))
998+
{
999+
return (false, new ErrorResponse("'source_prefab_path' and 'primitive_type' are mutually exclusive in create_child."));
1000+
}
1001+
1002+
if (!string.IsNullOrEmpty(sourcePrefabPath))
1003+
{
1004+
string sanitizedSourcePath = AssetPathUtility.SanitizeAssetPath(sourcePrefabPath);
1005+
if (string.IsNullOrEmpty(sanitizedSourcePath))
1006+
{
1007+
return (false, new ErrorResponse($"Invalid source_prefab_path '{sourcePrefabPath}'. Path traversal sequences are not allowed."));
1008+
}
1009+
1010+
if (!string.IsNullOrEmpty(editingPrefabPath) &&
1011+
sanitizedSourcePath.Equals(editingPrefabPath, StringComparison.OrdinalIgnoreCase))
1012+
{
1013+
return (false, new ErrorResponse($"Cannot nest prefab '{sanitizedSourcePath}' inside itself. This would create a circular reference."));
1014+
}
1015+
1016+
GameObject sourcePrefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(sanitizedSourcePath);
1017+
if (sourcePrefabAsset == null)
1018+
{
1019+
return (false, new ErrorResponse($"Source prefab not found at path: '{sanitizedSourcePath}'."));
1020+
}
1021+
1022+
newChild = PrefabUtility.InstantiatePrefab(sourcePrefabAsset, parentTransform) as GameObject;
1023+
if (newChild == null)
1024+
{
1025+
return (false, new ErrorResponse($"Failed to instantiate prefab from '{sanitizedSourcePath}' as nested child."));
1026+
}
1027+
newChild.name = childName;
1028+
}
1029+
else if (!string.IsNullOrEmpty(primitiveType))
9961030
{
9971031
try
9981032
{
@@ -1010,7 +1044,7 @@ private static (bool created, ErrorResponse error) CreateSingleChildInPrefab(JTo
10101044
newChild = new GameObject(childName);
10111045
}
10121046

1013-
// Set parent
1047+
// Ensure local-space transform (worldPositionStays=false) for all creation modes
10141048
newChild.transform.SetParent(parentTransform, false);
10151049

10161050
// Apply transform properties

Server/src/services/tools/manage_components.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,12 @@ async def manage_components(
4343
# For set_property action - single property
4444
property: Annotated[str,
4545
"Property name to set (for set_property action)"] | None = None,
46-
value: Annotated[str | int | float | bool | dict | list ,
47-
"Value to set (for set_property action)"] | None = None,
46+
value: Annotated[str | int | float | bool | dict | list,
47+
"Value to set (for set_property action). "
48+
"For object references: instance ID (int), asset path (string), "
49+
"or {\"guid\": \"...\"} / {\"path\": \"...\"}. "
50+
"For Sprite sub-assets: {\"guid\": \"...\", \"spriteName\": \"<name>\"} or "
51+
"{\"guid\": \"...\", \"fileID\": <id>}. Single-sprite textures auto-resolve."] | None = None,
4852
# For add/set_property - multiple properties
4953
properties: Annotated[
5054
dict[str, Any] | str,

Server/src/services/tools/manage_prefabs.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@
2525
"Manages Unity Prefab assets via headless operations (no UI, no prefab stages). "
2626
"Actions: get_info, get_hierarchy, create_from_gameobject, modify_contents. "
2727
"Use modify_contents for headless prefab editing - ideal for automated workflows. "
28-
"Use create_child parameter with modify_contents to add child GameObjects to a prefab "
28+
"Use create_child parameter with modify_contents to add child GameObjects or nested prefab instances to a prefab "
2929
"(single object or array for batch creation in one save). "
3030
"Example: create_child=[{\"name\": \"Child1\", \"primitive_type\": \"Sphere\", \"position\": [1,0,0]}, "
31-
"{\"name\": \"Child2\", \"primitive_type\": \"Cube\", \"parent\": \"Child1\"}]. "
31+
"{\"name\": \"Nested\", \"source_prefab_path\": \"Assets/Prefabs/Bullet.prefab\", \"position\": [0,2,0]}]. "
3232
"Use component_properties with modify_contents to set serialized fields on existing components "
3333
"(e.g. component_properties={\"Rigidbody\": {\"mass\": 5.0}, \"MyScript\": {\"health\": 100}}). "
3434
"Supports object references via {\"guid\": \"...\"}, {\"path\": \"Assets/...\"}, or {\"instanceID\": 123}. "
@@ -66,8 +66,8 @@ async def manage_prefabs(
6666
parent: Annotated[str, "New parent object name/path within prefab for modify_contents."] | None = None,
6767
components_to_add: Annotated[list[str], "Component types to add in modify_contents."] | None = None,
6868
components_to_remove: Annotated[list[str], "Component types to remove in modify_contents."] | None = None,
69-
create_child: Annotated[dict[str, Any] | list[dict[str, Any]], "Create child GameObject(s) in the prefab. Single object or array of objects, each with: name (required), parent (optional, defaults to target), primitive_type (optional: Cube, Sphere, Capsule, Cylinder, Plane, Quad), position, rotation, scale, components_to_add, tag, layer, set_active."] | None = None,
70-
component_properties: Annotated[dict[str, dict[str, Any]], "Set properties on existing components in modify_contents. Keys are component type names, values are dicts of property name to value. Example: {\"Rigidbody\": {\"mass\": 5.0}, \"MyScript\": {\"health\": 100}}. Supports object references via {\"guid\": \"...\"}, {\"path\": \"Assets/...\"}, or {\"instanceID\": 123}."] | None = None,
69+
create_child: Annotated[dict[str, Any] | list[dict[str, Any]], "Create child GameObject(s) in the prefab. Single object or array of objects, each with: name (required), parent (optional, defaults to target), source_prefab_path (optional: asset path to instantiate as nested prefab, e.g. 'Assets/Prefabs/Bullet.prefab'), primitive_type (optional: Cube, Sphere, Capsule, Cylinder, Plane, Quad), position, rotation, scale, components_to_add, tag, layer, set_active. source_prefab_path and primitive_type are mutually exclusive."] | None = None,
70+
component_properties: Annotated[dict[str, dict[str, Any]], "Set properties on existing components in modify_contents. Keys are component type names, values are dicts of property name to value. Example: {\"Rigidbody\": {\"mass\": 5.0}, \"MyScript\": {\"health\": 100}}. Supports object references via {\"guid\": \"...\"}, {\"path\": \"Assets/...\"}, or {\"instanceID\": 123}. For Sprite sub-assets: {\"guid\": \"...\", \"spriteName\": \"<name>\"}. Single-sprite textures auto-resolve."] | None = None,
7171
) -> dict[str, Any]:
7272
# Back-compat: map 'name' → 'target' for create_from_gameobject (Unity accepts both)
7373
if action == "create_from_gameobject" and target is None and name is not None:

Server/src/services/tools/manage_scriptable_object.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ async def manage_scriptable_object(
4747
"Target asset reference {guid|path} (for modify)."] = None,
4848
# --- shared ---
4949
patches: Annotated[list[dict[str, Any]] | str | None,
50-
"Patch list (or JSON string) to apply."] = None,
50+
"Patch list (or JSON string) to apply. "
51+
"For object references: use {\"ref\": {\"guid\": \"...\"}} or {\"value\": {\"guid\": \"...\"}}. "
52+
"For Sprite sub-assets: include \"spriteName\" in the ref/value object. "
53+
"Single-sprite textures auto-resolve from guid/path alone."] = None,
5154
# --- validation ---
5255
dry_run: Annotated[bool | str | None,
5356
"If true, validate patches without applying (modify only)."] = None,

0 commit comments

Comments
 (0)