Editor tool that reads and edits ProjectSettings/Collision.config JSON to query tags, check effective collision rules between two tags, set explicit pair rules, and list tags or output status. It parses the config, builds tag lists, handles actions: list_tags, check_collision, set_rule, and query_matrix, and writes changes back to the file when setting rules.
#nullable enable
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using Sandbox;
namespace Editor.Mcp
{
public static partial class SboxMcpAssistant
{
/// <summary>
/// Queries, inspects, and modifies project layer/tag collision rules (ProjectSettings/Collision.config)
/// to verify physics interactions between player, solid, trigger, ragdoll, debris, and custom tags.
/// </summary>
[McpTool( "pocketknife_collision_matrix" )]
public static string CollisionMatrix(
[Description( "Action: 'query_matrix', 'check_collision', 'set_rule', 'list_tags', 'status'" )] string action = "query_matrix",
[Description( "First tag (e.g. 'solid', 'playerclip', 'trigger', 'ragdoll')" )] string? tagA = null,
[Description( "Second tag (e.g. 'solid', 'playerclip', 'trigger', 'ragdoll')" )] string? tagB = null,
[Description( "Collision rule to set ('Collide', 'Trigger', 'Ignore') when action is 'set_rule'" )] string? rule = null )
{
try
{
var root = Project.Current?.GetRootPath();
if ( string.IsNullOrWhiteSpace( root ) )
{
return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Unavailable, "project.not_active", "No active project is loaded." ) );
}
var configPath = Path.Combine( root, "ProjectSettings", "Collision.config" );
if ( !File.Exists( configPath ) )
{
return McpJson.Write( ToolOutcome<object>.Failure( new { Path = configPath }, ToolStatus.Unavailable, "collision.config_missing", "ProjectSettings/Collision.config not found." ) );
}
var jsonText = File.ReadAllText( configPath );
var jsonNode = JsonNode.Parse( jsonText ) as JsonObject;
if ( jsonNode == null )
{
return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "collision.invalid_json", "Collision.config contains invalid JSON." ) );
}
var normAction = (action ?? "").ToLowerInvariant().Trim();
if ( normAction == "status" || string.IsNullOrWhiteSpace( normAction ) )
{
normAction = "query_matrix";
}
var defaultsNode = jsonNode["Defaults"] as JsonObject ?? new JsonObject();
var pairsNode = jsonNode["Pairs"] as JsonArray ?? new JsonArray();
// Collect all tags
var tags = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
foreach ( var kvp in defaultsNode )
{
tags.Add( kvp.Key );
}
foreach ( var item in pairsNode )
{
if ( item is JsonObject pairObj )
{
var a = pairObj["a"]?.GetValue<string>();
var b = pairObj["b"]?.GetValue<string>();
if ( !string.IsNullOrWhiteSpace( a ) ) tags.Add( a );
if ( !string.IsNullOrWhiteSpace( b ) ) tags.Add( b );
}
}
if ( normAction == "list_tags" )
{
return McpJson.Write( ToolOutcome<object>.Ready( new
{
Action = "list_tags",
TotalTags = tags.Count,
Tags = tags.OrderBy( t => t ).ToArray()
} ) );
}
if ( normAction == "check_collision" )
{
if ( string.IsNullOrWhiteSpace( tagA ) || string.IsNullOrWhiteSpace( tagB ) )
{
return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "collision.tags_required", "Both tagA and tagB are required to check collision." ) );
}
var cleanA = tagA.Trim().ToLowerInvariant();
var cleanB = tagB.Trim().ToLowerInvariant();
// Find explicit pair rule
string? effectiveRule = null;
bool isExplicit = false;
foreach ( var item in pairsNode )
{
if ( item is JsonObject pairObj )
{
var a = pairObj["a"]?.GetValue<string>()?.ToLowerInvariant();
var b = pairObj["b"]?.GetValue<string>()?.ToLowerInvariant();
var r = pairObj["r"]?.GetValue<string>();
if ( (a == cleanA && b == cleanB) || (a == cleanB && b == cleanA) )
{
effectiveRule = r;
isExplicit = true;
break;
}
}
}
// Fallback to default rule
if ( effectiveRule == null )
{
if ( defaultsNode.TryGetPropertyValue( cleanA, out var defA ) && defA != null )
{
effectiveRule = defA.GetValue<string>();
}
else if ( defaultsNode.TryGetPropertyValue( cleanB, out var defB ) && defB != null )
{
effectiveRule = defB.GetValue<string>();
}
else
{
effectiveRule = "Collide";
}
}
return McpJson.Write( ToolOutcome<object>.Ready( new
{
TagA = cleanA,
TagB = cleanB,
EffectiveRule = effectiveRule,
IsExplicitRule = isExplicit
} ) );
}
if ( normAction == "set_rule" )
{
if ( string.IsNullOrWhiteSpace( tagA ) || string.IsNullOrWhiteSpace( tagB ) || string.IsNullOrWhiteSpace( rule ) )
{
return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "collision.parameters_required", "tagA, tagB, and rule ('Collide', 'Trigger', 'Ignore') are required to set a rule." ) );
}
var cleanA = tagA.Trim().ToLowerInvariant();
var cleanB = tagB.Trim().ToLowerInvariant();
var normRule = rule.Trim();
if ( normRule.Equals( "collide", StringComparison.OrdinalIgnoreCase ) ) normRule = "Collide";
else if ( normRule.Equals( "trigger", StringComparison.OrdinalIgnoreCase ) ) normRule = "Trigger";
else if ( normRule.Equals( "ignore", StringComparison.OrdinalIgnoreCase ) ) normRule = "Ignore";
else
{
return McpJson.Write( ToolOutcome<object>.Failure( new { Rule = rule }, ToolStatus.Failed, "collision.invalid_rule", "Rule must be 'Collide', 'Trigger', or 'Ignore'." ) );
}
JsonObject? existingPair = null;
foreach ( var item in pairsNode )
{
if ( item is JsonObject pairObj )
{
var a = pairObj["a"]?.GetValue<string>()?.ToLowerInvariant();
var b = pairObj["b"]?.GetValue<string>()?.ToLowerInvariant();
if ( (a == cleanA && b == cleanB) || (a == cleanB && b == cleanA) )
{
existingPair = pairObj;
break;
}
}
}
if ( existingPair != null )
{
existingPair["r"] = normRule;
}
else
{
var newPair = new JsonObject
{
["a"] = cleanA,
["b"] = cleanB,
["r"] = normRule
};
pairsNode.Add( newPair );
}
File.WriteAllText( configPath, jsonNode.ToJsonString( new JsonSerializerOptions { WriteIndented = true } ) );
return McpJson.Write( ToolOutcome<object>.Ready( new
{
Action = "set_rule",
TagA = cleanA,
TagB = cleanB,
Rule = normRule,
Updated = true
} ) );
}
// query_matrix default
var defaults = defaultsNode.ToDictionary( kvp => kvp.Key, kvp => kvp.Value?.ToString() ?? "Collide" );
var pairs = pairsNode.OfType<JsonObject>().Select( p => new
{
TagA = p["a"]?.GetValue<string>(),
TagB = p["b"]?.GetValue<string>(),
Rule = p["r"]?.GetValue<string>()
} ).ToArray();
return McpJson.Write( ToolOutcome<object>.Ready( new
{
Action = "query_matrix",
ConfigFile = "ProjectSettings/Collision.config",
TotalTags = tags.Count,
Tags = tags.OrderBy( t => t ).ToArray(),
Defaults = defaults,
ExplicitPairs = pairs,
Success = true
} ) );
}
catch ( Exception ex )
{
return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "collision.operation_failed", "Collision matrix operation failed.", ex ) );
}
}
}
}