Test/478 matrix4x4 serialization crash (#481)

* Fix #478: Add Matrix4x4Converter to prevent Cinemachine serialization crash

The `get_components` action crashes Unity when serializing Cinemachine
camera components because Newtonsoft.Json accesses computed Matrix4x4
properties (lossyScale, rotation) that call ValidTRS() on non-TRS matrices.

This fix adds a safe Matrix4x4Converter that only accesses raw matrix
elements (m00-m33), avoiding the dangerous computed properties entirely.

Changes:
- Add Matrix4x4Converter to UnityTypeConverters.cs
- Register converter in GameObjectSerializer serializer settings

Tested with Cinemachine 3.1.5 on Unity 6 - get_components now returns
full component data without crashing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add unit tests for Matrix4x4Converter

Tests cover:
- Identity matrix serialization/deserialization
- Translation matrix round-trip
- Degenerate matrix (determinant=0) - key regression test
- Non-TRS matrix (projection) - validates ValidTRS() is never called
- Null handling
- Ensures dangerous properties are not in output

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Address code review feedback

- Fix null handling consistency: return zero matrix instead of identity
  (consistent with missing field defaults of 0f)
- Improve degenerate matrix test to verify:
  - JSON only contains raw mXY properties
  - Values roundtrip correctly
- Rename test to reflect expanded coverage

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Move tests to TestProject per review feedback

Moved Matrix4x4ConverterTests from MCPForUnity/Editor/Tests/ to
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/ as requested.

Also added MCPForUnity.Runtime reference to the test asmdef since
the converter lives in the Runtime assembly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix Matrix4x4 deserialization guard + UI Toolkit USS warning

---------

Co-authored-by: Alexander Mangel <cygnusfear@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
main
dsarno 2025-12-23 15:53:14 -08:00 committed by GitHub
parent 90758f19f4
commit 91b6f4d8d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 209 additions and 2 deletions

View File

@ -524,6 +524,7 @@ namespace MCPForUnity.Editor.Helpers
new ColorConverter(),
new RectConverter(),
new BoundsConverter(),
new Matrix4x4Converter(), // Fix #478: Safe Matrix4x4 serialization for Cinemachine
new UnityEngineObjectConverter() // Handles serialization of references
},
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,

View File

@ -4,7 +4,6 @@
flex-direction: column;
flex-grow: 1;
overflow: hidden;
box-sizing: border-box;
}
/* Title */

View File

@ -160,6 +160,67 @@ namespace MCPForUnity.Runtime.Serialization
}
}
/// <summary>
/// Safe converter for Matrix4x4 that only accesses raw matrix elements (m00-m33).
/// Avoids computed properties (lossyScale, rotation, inverse) that call ValidTRS()
/// and can crash Unity on non-TRS matrices (common in Cinemachine components).
/// Fixes: https://github.com/CoplayDev/unity-mcp/issues/478
/// </summary>
public class Matrix4x4Converter : JsonConverter<Matrix4x4>
{
public override void WriteJson(JsonWriter writer, Matrix4x4 value, JsonSerializer serializer)
{
writer.WriteStartObject();
// Only access raw matrix elements - NEVER computed properties like lossyScale/rotation
writer.WritePropertyName("m00"); writer.WriteValue(value.m00);
writer.WritePropertyName("m01"); writer.WriteValue(value.m01);
writer.WritePropertyName("m02"); writer.WriteValue(value.m02);
writer.WritePropertyName("m03"); writer.WriteValue(value.m03);
writer.WritePropertyName("m10"); writer.WriteValue(value.m10);
writer.WritePropertyName("m11"); writer.WriteValue(value.m11);
writer.WritePropertyName("m12"); writer.WriteValue(value.m12);
writer.WritePropertyName("m13"); writer.WriteValue(value.m13);
writer.WritePropertyName("m20"); writer.WriteValue(value.m20);
writer.WritePropertyName("m21"); writer.WriteValue(value.m21);
writer.WritePropertyName("m22"); writer.WriteValue(value.m22);
writer.WritePropertyName("m23"); writer.WriteValue(value.m23);
writer.WritePropertyName("m30"); writer.WriteValue(value.m30);
writer.WritePropertyName("m31"); writer.WriteValue(value.m31);
writer.WritePropertyName("m32"); writer.WriteValue(value.m32);
writer.WritePropertyName("m33"); writer.WriteValue(value.m33);
writer.WriteEndObject();
}
public override Matrix4x4 ReadJson(JsonReader reader, Type objectType, Matrix4x4 existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return new Matrix4x4(); // Return zero matrix for null (consistent with missing field defaults)
if (reader.TokenType != JsonToken.StartObject)
throw new JsonSerializationException($"Expected JSON object or null when deserializing Matrix4x4, got '{reader.TokenType}'.");
JObject jo = JObject.Load(reader);
var matrix = new Matrix4x4();
matrix.m00 = jo["m00"]?.Value<float>() ?? 0f;
matrix.m01 = jo["m01"]?.Value<float>() ?? 0f;
matrix.m02 = jo["m02"]?.Value<float>() ?? 0f;
matrix.m03 = jo["m03"]?.Value<float>() ?? 0f;
matrix.m10 = jo["m10"]?.Value<float>() ?? 0f;
matrix.m11 = jo["m11"]?.Value<float>() ?? 0f;
matrix.m12 = jo["m12"]?.Value<float>() ?? 0f;
matrix.m13 = jo["m13"]?.Value<float>() ?? 0f;
matrix.m20 = jo["m20"]?.Value<float>() ?? 0f;
matrix.m21 = jo["m21"]?.Value<float>() ?? 0f;
matrix.m22 = jo["m22"]?.Value<float>() ?? 0f;
matrix.m23 = jo["m23"]?.Value<float>() ?? 0f;
matrix.m30 = jo["m30"]?.Value<float>() ?? 0f;
matrix.m31 = jo["m31"]?.Value<float>() ?? 0f;
matrix.m32 = jo["m32"]?.Value<float>() ?? 0f;
matrix.m33 = jo["m33"]?.Value<float>() ?? 0f;
return matrix;
}
}
// Converter for UnityEngine.Object references (GameObjects, Components, Materials, Textures, etc.)
public class UnityEngineObjectConverter : JsonConverter<UnityEngine.Object>
{

View File

@ -0,0 +1,134 @@
using System.Linq;
using MCPForUnity.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEngine;
namespace MCPForUnityTests.EditMode.Helpers
{
/// <summary>
/// Tests for Matrix4x4Converter to ensure it safely serializes matrices
/// without accessing dangerous computed properties (lossyScale, rotation).
/// Regression test for https://github.com/CoplayDev/unity-mcp/issues/478
/// </summary>
public class Matrix4x4ConverterTests
{
private JsonSerializerSettings _settings;
[SetUp]
public void SetUp()
{
_settings = new JsonSerializerSettings
{
Converters = { new Matrix4x4Converter() }
};
}
[Test]
public void Serialize_IdentityMatrix_ReturnsCorrectJson()
{
var matrix = Matrix4x4.identity;
var json = JsonConvert.SerializeObject(matrix, _settings);
Assert.That(json, Does.Contain("\"m00\":1"));
Assert.That(json, Does.Contain("\"m11\":1"));
Assert.That(json, Does.Contain("\"m22\":1"));
Assert.That(json, Does.Contain("\"m33\":1"));
Assert.That(json, Does.Contain("\"m01\":0"));
}
[Test]
public void Deserialize_IdentityMatrix_ReturnsIdentity()
{
var original = Matrix4x4.identity;
var json = JsonConvert.SerializeObject(original, _settings);
var result = JsonConvert.DeserializeObject<Matrix4x4>(json, _settings);
Assert.That(result, Is.EqualTo(original));
}
[Test]
public void Serialize_TranslationMatrix_PreservesValues()
{
var matrix = Matrix4x4.Translate(new Vector3(10, 20, 30));
var json = JsonConvert.SerializeObject(matrix, _settings);
var result = JsonConvert.DeserializeObject<Matrix4x4>(json, _settings);
Assert.That(result.m03, Is.EqualTo(10f));
Assert.That(result.m13, Is.EqualTo(20f));
Assert.That(result.m23, Is.EqualTo(30f));
}
[Test]
public void Serialize_DegenerateMatrix_DoesNotCrashAndRoundtrips()
{
// This is the key test - a degenerate matrix that would crash
// if we accessed lossyScale or rotation properties
var matrix = new Matrix4x4();
matrix.m00 = 0; matrix.m11 = 0; matrix.m22 = 0; // Degenerate - determinant = 0
// This should NOT throw or crash - the old code would fail here
var json = JsonConvert.SerializeObject(matrix, _settings);
var result = JsonConvert.DeserializeObject<Matrix4x4>(json, _settings);
// Verify JSON only contains raw mXY properties
var jo = JObject.Parse(json);
var expectedProps = new[]
{
"m00", "m01", "m02", "m03",
"m10", "m11", "m12", "m13",
"m20", "m21", "m22", "m23",
"m30", "m31", "m32", "m33"
};
CollectionAssert.AreEquivalent(expectedProps, jo.Properties().Select(p => p.Name).ToArray());
// Verify values roundtrip correctly (all zeros for degenerate matrix)
Assert.That(result.m00, Is.EqualTo(0f));
Assert.That(result.m11, Is.EqualTo(0f));
Assert.That(result.m22, Is.EqualTo(0f));
Assert.That(result, Is.EqualTo(matrix));
}
[Test]
public void Serialize_NonTRSMatrix_DoesNotCrash()
{
// Projection matrices are NOT valid TRS matrices
// Accessing lossyScale/rotation on them causes ValidTRS() assertion
var matrix = Matrix4x4.Perspective(60f, 1.77f, 0.1f, 1000f);
// Verify it's not a valid TRS matrix
Assert.That(matrix.ValidTRS(), Is.False, "Test requires non-TRS matrix");
// This should NOT throw - the fix ensures we never access computed properties
Assert.DoesNotThrow(() =>
{
var json = JsonConvert.SerializeObject(matrix, _settings);
var result = JsonConvert.DeserializeObject<Matrix4x4>(json, _settings);
});
}
[Test]
public void Deserialize_NullToken_ReturnsZeroMatrix()
{
var json = "null";
var result = JsonConvert.DeserializeObject<Matrix4x4>(json, _settings);
// Returns zero matrix (consistent with missing field defaults of 0f)
Assert.That(result, Is.EqualTo(new Matrix4x4()));
}
[Test]
public void Serialize_DoesNotContainDangerousProperties()
{
var matrix = Matrix4x4.TRS(Vector3.one, Quaternion.identity, Vector3.one);
var json = JsonConvert.SerializeObject(matrix, _settings);
// Ensure we're not serializing the dangerous computed properties
Assert.That(json, Does.Not.Contain("lossyScale"));
Assert.That(json, Does.Not.Contain("rotation"));
Assert.That(json, Does.Not.Contain("inverse"));
Assert.That(json, Does.Not.Contain("transpose"));
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d2c5dd202b4944675ae606581676a24a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -3,6 +3,7 @@
"rootNamespace": "",
"references": [
"MCPForUnity.Editor",
"MCPForUnity.Runtime",
"TestAsmdef",
"UnityEngine.TestRunner",
"UnityEditor.TestRunner"