Files
2026-07-13 12:49:17 +08:00

534 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Helpers;
namespace MCPForUnity.Editor.Tools.Physics
{
internal static class PhysicsMaterialOps
{
public static object Create(JObject @params)
{
var p = new ToolParams(@params);
string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant();
var nameResult = p.GetRequired("name");
var nameErr = nameResult.GetOrError(out string name);
if (nameErr != null) return nameErr;
string folder = p.Get("path") ?? "Assets/Physics Materials";
folder = AssetPathUtility.SanitizeAssetPath(folder);
if (string.IsNullOrEmpty(folder))
return new ErrorResponse("Invalid folder path.");
if (!EnsureFolderExists(folder, out string folderError))
return new ErrorResponse(folderError);
if (dimension == "2d")
return Create2D(name, folder, p);
if (dimension != "3d")
return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'.");
return Create3D(name, folder, p);
}
public static object Configure(JObject @params)
{
var p = new ToolParams(@params);
string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant();
var pathResult = p.GetRequired("path");
var pathErr = pathResult.GetOrError(out string path);
if (pathErr != null) return pathErr;
path = AssetPathUtility.SanitizeAssetPath(path);
if (string.IsNullOrEmpty(path))
return new ErrorResponse("Invalid asset path.");
var properties = p.GetRaw("properties") as JObject;
if (properties == null || properties.Count == 0)
return new ErrorResponse("'properties' parameter is required and must be a non-empty object.");
if (dimension != "3d" && dimension != "2d")
return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'.");
if (dimension == "2d")
return Configure2D(path, properties);
return Configure3D(path, properties);
}
public static object Assign(JObject @params)
{
var p = new ToolParams(@params);
var targetToken = p.GetRaw("target");
if (targetToken == null || string.IsNullOrEmpty(targetToken.ToString()))
return new ErrorResponse("'target' parameter is required.");
var matPathResult = p.GetRequired("material_path");
var matPathErr = matPathResult.GetOrError(out string materialPath);
if (matPathErr != null) return matPathErr;
materialPath = AssetPathUtility.SanitizeAssetPath(materialPath);
if (string.IsNullOrEmpty(materialPath))
return new ErrorResponse("Invalid material path.");
string searchMethod = p.Get("search_method") ?? "by_name";
string colliderType = p.Get("collider_type");
int? componentIndex = ParamCoercion.CoerceIntNullable(p.GetRaw("componentIndex") ?? p.GetRaw("component_index"));
var go = GameObjectLookup.FindByTarget(targetToken, searchMethod);
if (go == null)
return new ErrorResponse($"GameObject not found: '{targetToken}'.");
// Try to load as 3D physics material first
#if UNITY_6000_0_OR_NEWER
var mat3D = AssetDatabase.LoadAssetAtPath<PhysicsMaterial>(materialPath);
#else
var mat3D = AssetDatabase.LoadAssetAtPath<PhysicMaterial>(materialPath);
#endif
var mat2D = AssetDatabase.LoadAssetAtPath<PhysicsMaterial2D>(materialPath);
if (mat3D == null && mat2D == null)
return new ErrorResponse($"No physics material found at path: '{materialPath}'.");
// Try 3D colliders first
if (mat3D != null)
{
var collider3D = FindCollider3D(go, colliderType, componentIndex);
if (collider3D != null)
{
Undo.RecordObject(collider3D, "Assign Physics Material");
collider3D.sharedMaterial = mat3D;
EditorUtility.SetDirty(collider3D);
return new
{
success = true,
message = $"Assigned 3D physics material to {collider3D.GetType().Name} on '{go.name}'.",
data = new
{
gameObject = go.name,
collider = collider3D.GetType().Name,
materialPath
}
};
}
if (componentIndex.HasValue)
{
var type3D = !string.IsNullOrEmpty(colliderType) ? UnityTypeResolver.ResolveComponent(colliderType) : typeof(Collider);
if (type3D != null && typeof(Collider).IsAssignableFrom(type3D))
{
int count3D = go.GetComponents(type3D).Length;
return new ErrorResponse($"component_index {componentIndex.Value} out of range. Found {count3D} '{type3D.Name}' collider(s) on '{go.name}'.");
}
else if (!string.IsNullOrEmpty(colliderType))
{
return new ErrorResponse($"Unknown or invalid 3D collider type: '{colliderType}'.");
}
}
}
// Try 2D colliders
if (mat2D != null)
{
var collider2D = FindCollider2D(go, colliderType, componentIndex);
if (collider2D != null)
{
Undo.RecordObject(collider2D, "Assign Physics Material 2D");
collider2D.sharedMaterial = mat2D;
EditorUtility.SetDirty(collider2D);
return new
{
success = true,
message = $"Assigned 2D physics material to {collider2D.GetType().Name} on '{go.name}'.",
data = new
{
gameObject = go.name,
collider = collider2D.GetType().Name,
materialPath
}
};
}
if (componentIndex.HasValue)
{
var type2D = !string.IsNullOrEmpty(colliderType) ? UnityTypeResolver.ResolveComponent(colliderType) : typeof(Collider2D);
if (type2D != null && typeof(Collider2D).IsAssignableFrom(type2D))
{
int count2D = go.GetComponents(type2D).Length;
return new ErrorResponse($"component_index {componentIndex.Value} out of range. Found {count2D} '{type2D.Name}' collider(s) on '{go.name}'.");
}
else if (!string.IsNullOrEmpty(colliderType))
{
return new ErrorResponse($"Unknown or invalid 2D collider type: '{colliderType}'.");
}
}
}
return new ErrorResponse($"No suitable collider found on '{go.name}'.");
}
// =====================================================================
// Create helpers
// =====================================================================
private static object Create3D(string name, string folder, ToolParams p)
{
float dynamicFriction = p.GetFloat("dynamic_friction") ?? 0.6f;
float staticFriction = p.GetFloat("static_friction") ?? 0.6f;
float bounciness = p.GetFloat("bounciness") ?? 0f;
string frictionCombine = p.Get("friction_combine");
string bounceCombine = p.Get("bounce_combine");
string assetPath = $"{folder}/{name}.physicMaterial";
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath) != null)
return new ErrorResponse($"A physics material already exists at '{assetPath}'. Use configure_physics_material to modify it.");
#if UNITY_6000_0_OR_NEWER
var mat = new PhysicsMaterial(name)
{
dynamicFriction = dynamicFriction,
staticFriction = staticFriction,
bounciness = bounciness
};
if (!string.IsNullOrEmpty(frictionCombine) &&
Enum.TryParse<PhysicsMaterialCombine>(frictionCombine, true, out var fc))
mat.frictionCombine = fc;
if (!string.IsNullOrEmpty(bounceCombine) &&
Enum.TryParse<PhysicsMaterialCombine>(bounceCombine, true, out var bc))
mat.bounceCombine = bc;
#else
var mat = new PhysicMaterial(name)
{
dynamicFriction = dynamicFriction,
staticFriction = staticFriction,
bounciness = bounciness
};
if (!string.IsNullOrEmpty(frictionCombine) &&
Enum.TryParse<PhysicMaterialCombine>(frictionCombine, true, out var fc))
mat.frictionCombine = fc;
if (!string.IsNullOrEmpty(bounceCombine) &&
Enum.TryParse<PhysicMaterialCombine>(bounceCombine, true, out var bc))
mat.bounceCombine = bc;
#endif
AssetDatabase.CreateAsset(mat, assetPath);
AssetDatabase.SaveAssets();
return new
{
success = true,
message = $"Created 3D physics material '{name}' at '{assetPath}'.",
data = new
{
path = assetPath,
dimension = "3d",
dynamicFriction,
staticFriction,
bounciness,
frictionCombine = mat.frictionCombine.ToString(),
bounceCombine = mat.bounceCombine.ToString()
}
};
}
private static object Create2D(string name, string folder, ToolParams p)
{
float friction = p.GetFloat("friction") ?? 0.4f;
float bounciness = p.GetFloat("bounciness") ?? 0f;
string assetPath = $"{folder}/{name}.physicsMaterial2D";
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath) != null)
return new ErrorResponse($"A 2D physics material already exists at '{assetPath}'. Use configure_physics_material to modify it.");
var mat = new PhysicsMaterial2D(name)
{
friction = friction,
bounciness = bounciness
};
AssetDatabase.CreateAsset(mat, assetPath);
AssetDatabase.SaveAssets();
return new
{
success = true,
message = $"Created 2D physics material '{name}' at '{assetPath}'.",
data = new
{
path = assetPath,
dimension = "2d",
friction,
bounciness
}
};
}
// =====================================================================
// Configure helpers
// =====================================================================
private static readonly HashSet<string> Valid3DMatKeys = new HashSet<string>
{
"dynamicfriction", "staticfriction", "bounciness", "frictioncombine", "bouncecombine"
};
private static object Configure3D(string path, JObject properties)
{
// Validate all keys before applying any changes
var unknown = new List<string>();
foreach (var prop in properties.Properties())
{
string key = prop.Name.ToLowerInvariant().Replace("_", "");
if (!Valid3DMatKeys.Contains(key))
unknown.Add(prop.Name);
}
if (unknown.Count > 0)
return new ErrorResponse(
$"Unknown 3D physics material property(ies): {string.Join(", ", unknown)}.");
#if UNITY_6000_0_OR_NEWER
var mat = AssetDatabase.LoadAssetAtPath<PhysicsMaterial>(path);
#else
var mat = AssetDatabase.LoadAssetAtPath<PhysicMaterial>(path);
#endif
if (mat == null)
return new ErrorResponse($"No 3D physics material found at: '{path}'.");
Undo.RecordObject(mat, "Configure Physics Material");
var changed = new List<string>();
foreach (var prop in properties.Properties())
{
string key = prop.Name.ToLowerInvariant().Replace("_", "");
switch (key)
{
case "dynamicfriction":
mat.dynamicFriction = prop.Value.Value<float>();
changed.Add("dynamicFriction");
break;
case "staticfriction":
mat.staticFriction = prop.Value.Value<float>();
changed.Add("staticFriction");
break;
case "bounciness":
mat.bounciness = prop.Value.Value<float>();
changed.Add("bounciness");
break;
case "frictioncombine":
{
#if UNITY_6000_0_OR_NEWER
if (!Enum.TryParse<PhysicsMaterialCombine>(prop.Value.ToString(), true, out var fc))
return new ErrorResponse($"Invalid friction_combine value: '{prop.Value}'. Valid values: Average, Minimum, Maximum, Multiply.");
mat.frictionCombine = fc;
changed.Add("frictionCombine");
#else
if (!Enum.TryParse<PhysicMaterialCombine>(prop.Value.ToString(), true, out var fc))
return new ErrorResponse($"Invalid friction_combine value: '{prop.Value}'. Valid values: Average, Minimum, Maximum, Multiply.");
mat.frictionCombine = fc;
changed.Add("frictionCombine");
#endif
break;
}
case "bouncecombine":
{
#if UNITY_6000_0_OR_NEWER
if (!Enum.TryParse<PhysicsMaterialCombine>(prop.Value.ToString(), true, out var bc))
return new ErrorResponse($"Invalid bounce_combine value: '{prop.Value}'. Valid values: Average, Minimum, Maximum, Multiply.");
mat.bounceCombine = bc;
changed.Add("bounceCombine");
#else
if (!Enum.TryParse<PhysicMaterialCombine>(prop.Value.ToString(), true, out var bc))
return new ErrorResponse($"Invalid bounce_combine value: '{prop.Value}'. Valid values: Average, Minimum, Maximum, Multiply.");
mat.bounceCombine = bc;
changed.Add("bounceCombine");
#endif
break;
}
}
}
EditorUtility.SetDirty(mat);
AssetDatabase.SaveAssets();
return new
{
success = true,
message = $"Updated {changed.Count} property(ies) on 3D physics material at '{path}'.",
data = new { path, changed }
};
}
private static readonly HashSet<string> Valid2DMatKeys = new HashSet<string>
{
"friction", "bounciness"
};
private static object Configure2D(string path, JObject properties)
{
// Validate all keys before applying any changes
var unknown = new List<string>();
foreach (var prop in properties.Properties())
{
string key = prop.Name.ToLowerInvariant().Replace("_", "");
if (!Valid2DMatKeys.Contains(key))
unknown.Add(prop.Name);
}
if (unknown.Count > 0)
return new ErrorResponse(
$"Unknown 2D physics material property(ies): {string.Join(", ", unknown)}.");
var mat = AssetDatabase.LoadAssetAtPath<PhysicsMaterial2D>(path);
if (mat == null)
return new ErrorResponse($"No 2D physics material found at: '{path}'.");
Undo.RecordObject(mat, "Configure Physics Material 2D");
var changed = new List<string>();
foreach (var prop in properties.Properties())
{
string key = prop.Name.ToLowerInvariant().Replace("_", "");
switch (key)
{
case "friction":
mat.friction = prop.Value.Value<float>();
changed.Add("friction");
break;
case "bounciness":
mat.bounciness = prop.Value.Value<float>();
changed.Add("bounciness");
break;
}
}
EditorUtility.SetDirty(mat);
AssetDatabase.SaveAssets();
return new
{
success = true,
message = $"Updated {changed.Count} property(ies) on 2D physics material at '{path}'.",
data = new { path, changed }
};
}
// =====================================================================
// Assign helpers
// =====================================================================
private static Collider FindCollider3D(GameObject go, string colliderType, int? index = null)
{
if (!string.IsNullOrEmpty(colliderType))
{
var type = UnityTypeResolver.ResolveComponent(colliderType);
if (type != null && typeof(Collider).IsAssignableFrom(type))
{
if (index.HasValue)
{
var components = go.GetComponents(type);
if (index.Value < 0 || index.Value >= components.Length)
return null;
return components[index.Value] as Collider;
}
return go.GetComponent(type) as Collider;
}
return null;
}
if (index.HasValue)
{
var colliders = go.GetComponents<Collider>();
if (index.Value < 0 || index.Value >= colliders.Length)
return null;
return colliders[index.Value];
}
return go.GetComponent<Collider>();
}
private static Collider2D FindCollider2D(GameObject go, string colliderType, int? index = null)
{
if (!string.IsNullOrEmpty(colliderType))
{
var type = UnityTypeResolver.ResolveComponent(colliderType);
if (type != null && typeof(Collider2D).IsAssignableFrom(type))
{
if (index.HasValue)
{
var components = go.GetComponents(type);
if (index.Value < 0 || index.Value >= components.Length)
return null;
return components[index.Value] as Collider2D;
}
return go.GetComponent(type) as Collider2D;
}
return null;
}
if (index.HasValue)
{
var colliders = go.GetComponents<Collider2D>();
if (index.Value < 0 || index.Value >= colliders.Length)
return null;
return colliders[index.Value];
}
return go.GetComponent<Collider2D>();
}
// =====================================================================
// Folder helpers
// =====================================================================
private static bool EnsureFolderExists(string folderPath, out string error)
{
error = null;
if (string.IsNullOrWhiteSpace(folderPath))
{
error = "Folder path is empty.";
return false;
}
folderPath = folderPath.TrimEnd('/');
if (!folderPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(folderPath, "Assets", StringComparison.OrdinalIgnoreCase))
{
error = "Folder path must be under Assets/.";
return false;
}
if (AssetDatabase.IsValidFolder(folderPath))
return true;
var parts = folderPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
string current = "Assets";
for (int i = 1; i < parts.Length; i++)
{
string next = current + "/" + parts[i];
if (!AssetDatabase.IsValidFolder(next))
{
string guid = AssetDatabase.CreateFolder(current, parts[i]);
if (string.IsNullOrEmpty(guid))
{
error = $"Failed to create folder: {next}";
return false;
}
}
current = next;
}
return true;
}
}
}