chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("MCPForUnity.Editor")]
|
||||
[assembly: InternalsVisibleTo("MCPForUnityTests.EditMode")]
|
||||
[assembly: InternalsVisibleTo("MCPForUnityTests.PlayMode")]
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96306c75346e49089f53d7f73de24a3b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5c8b3a2f4b0e42dba7f6d7c6e8a4c1f2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,782 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Runtime.Helpers
|
||||
//The reason for having another Runtime Utilities in additional to Editor Utilities is to avoid Editor-only dependencies in this runtime code.
|
||||
{
|
||||
public readonly struct ScreenshotCaptureResult
|
||||
{
|
||||
public ScreenshotCaptureResult(string fullPath, string projectRelativePath, int superSize)
|
||||
: this(fullPath, projectRelativePath, superSize, isAsync: false, imageBase64: null, imageWidth: 0, imageHeight: 0)
|
||||
{
|
||||
}
|
||||
|
||||
public ScreenshotCaptureResult(string fullPath, string projectRelativePath, int superSize, bool isAsync)
|
||||
: this(fullPath, projectRelativePath, superSize, isAsync, imageBase64: null, imageWidth: 0, imageHeight: 0)
|
||||
{
|
||||
}
|
||||
|
||||
public ScreenshotCaptureResult(string fullPath, string projectRelativePath, int superSize, bool isAsync,
|
||||
string imageBase64, int imageWidth, int imageHeight)
|
||||
{
|
||||
FullPath = fullPath;
|
||||
ProjectRelativePath = projectRelativePath;
|
||||
SuperSize = superSize;
|
||||
IsAsync = isAsync;
|
||||
ImageBase64 = imageBase64;
|
||||
ImageWidth = imageWidth;
|
||||
ImageHeight = imageHeight;
|
||||
}
|
||||
|
||||
public string FullPath { get; }
|
||||
/// <summary>Path relative to the Unity project root (e.g. "Captures/foo.png"). Suitable for ScreenCapture.CaptureScreenshot.</summary>
|
||||
public string ProjectRelativePath { get; }
|
||||
public int SuperSize { get; }
|
||||
public bool IsAsync { get; }
|
||||
/// <summary>Base64-encoded PNG image data. Only populated when include_image is true.</summary>
|
||||
public string ImageBase64 { get; }
|
||||
public int ImageWidth { get; }
|
||||
public int ImageHeight { get; }
|
||||
}
|
||||
|
||||
public static class ScreenshotUtility
|
||||
{
|
||||
/// <summary>
|
||||
/// Built-in default folder for screenshots, project-relative.
|
||||
/// Callers can override per-call via the <c>folderOverride</c> parameter on capture methods,
|
||||
/// or globally via <c>ScreenshotPreferences</c> in the Editor assembly.
|
||||
/// </summary>
|
||||
public const string DefaultFolder = "Assets/Screenshots";
|
||||
|
||||
private static Camera FindAvailableCamera()
|
||||
{
|
||||
var main = Camera.main;
|
||||
if (main != null)
|
||||
{
|
||||
return main;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var cams = UnityFindObjectsCompat.FindAll<Camera>();
|
||||
return cams.FirstOrDefault();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static ScreenshotCaptureResult CaptureToProjectFolder(
|
||||
string fileName = null,
|
||||
int superSize = 1,
|
||||
bool ensureUniqueFileName = true,
|
||||
string folderOverride = null)
|
||||
{
|
||||
ScreenshotCaptureResult result = PrepareCaptureResult(fileName, superSize, ensureUniqueFileName, folderOverride, isAsync: true);
|
||||
// ScreenCapture.CaptureScreenshot accepts paths relative to the project root.
|
||||
ScreenCapture.CaptureScreenshot(result.ProjectRelativePath, result.SuperSize);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures a screenshot from a specific camera by rendering into a temporary RenderTexture (works in Edit Mode).
|
||||
/// When <paramref name="includeImage"/> is true, the result includes a base64-encoded PNG (optionally
|
||||
/// downscaled so the longest edge is at most <paramref name="maxResolution"/>).
|
||||
/// </summary>
|
||||
public static ScreenshotCaptureResult CaptureFromCameraToProjectFolder(
|
||||
Camera camera,
|
||||
string fileName = null,
|
||||
int superSize = 1,
|
||||
bool ensureUniqueFileName = true,
|
||||
bool includeImage = false,
|
||||
int maxResolution = 0,
|
||||
string folderOverride = null)
|
||||
{
|
||||
if (camera == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(camera));
|
||||
}
|
||||
|
||||
ScreenshotCaptureResult result = PrepareCaptureResult(fileName, superSize, ensureUniqueFileName, folderOverride, isAsync: false);
|
||||
int size = result.SuperSize;
|
||||
|
||||
int width = Mathf.Max(1, camera.pixelWidth > 0 ? camera.pixelWidth : Screen.width);
|
||||
int height = Mathf.Max(1, camera.pixelHeight > 0 ? camera.pixelHeight : Screen.height);
|
||||
width *= size;
|
||||
height *= size;
|
||||
|
||||
RenderTexture prevRT = camera.targetTexture;
|
||||
RenderTexture prevActive = RenderTexture.active;
|
||||
var rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32);
|
||||
Texture2D tex = null;
|
||||
Texture2D downscaled = null;
|
||||
string imageBase64 = null;
|
||||
int imgW = 0, imgH = 0;
|
||||
try
|
||||
{
|
||||
camera.targetTexture = rt;
|
||||
camera.Render();
|
||||
|
||||
RenderTexture.active = rt;
|
||||
tex = new Texture2D(width, height, TextureFormat.RGBA32, false);
|
||||
tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
|
||||
tex.Apply();
|
||||
|
||||
byte[] png = tex.EncodeToPNG();
|
||||
File.WriteAllBytes(result.FullPath, png);
|
||||
|
||||
if (includeImage)
|
||||
{
|
||||
int targetMax = maxResolution > 0 ? maxResolution : 640;
|
||||
if (width > targetMax || height > targetMax)
|
||||
{
|
||||
downscaled = DownscaleTexture(tex, targetMax);
|
||||
byte[] smallPng = downscaled.EncodeToPNG();
|
||||
imageBase64 = System.Convert.ToBase64String(smallPng);
|
||||
imgW = downscaled.width;
|
||||
imgH = downscaled.height;
|
||||
}
|
||||
else
|
||||
{
|
||||
imageBase64 = System.Convert.ToBase64String(png);
|
||||
imgW = width;
|
||||
imgH = height;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
camera.targetTexture = prevRT;
|
||||
RenderTexture.active = prevActive;
|
||||
RenderTexture.ReleaseTemporary(rt);
|
||||
DestroyTexture(tex);
|
||||
DestroyTexture(downscaled);
|
||||
}
|
||||
|
||||
if (includeImage && imageBase64 != null)
|
||||
{
|
||||
return new ScreenshotCaptureResult(
|
||||
result.FullPath, result.ProjectRelativePath, result.SuperSize, false,
|
||||
imageBase64, imgW, imgH);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// Synchronously drive a WaitForEndOfFrame ScreenshotCapturer by pumping the editor's
|
||||
// player loop. Play-mode only; EditorApplication.Step is a no-op in edit mode.
|
||||
private static Texture2D CaptureCompositedAfterFrame(int superSize, int timeoutSteps = 5)
|
||||
{
|
||||
Texture2D result = null;
|
||||
bool done = false;
|
||||
bool callerReturned = false;
|
||||
ScreenshotCapturer.Begin(superSize, tex =>
|
||||
{
|
||||
// Late completion after the spin loop timed out: caller will never consume
|
||||
// the texture, so destroy it here to avoid leaking a Unity object.
|
||||
if (callerReturned)
|
||||
{
|
||||
if (tex != null) DestroyTexture(tex);
|
||||
return;
|
||||
}
|
||||
result = tex;
|
||||
done = true;
|
||||
});
|
||||
// Step() pauses play mode as a side effect; restore the prior state so a screenshot
|
||||
// doesn't leave a running game paused (an already-paused game stays paused).
|
||||
bool wasPaused = UnityEditor.EditorApplication.isPaused;
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < timeoutSteps && !done; i++)
|
||||
{
|
||||
UnityEditor.EditorApplication.Step();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!wasPaused)
|
||||
UnityEditor.EditorApplication.isPaused = false;
|
||||
}
|
||||
callerReturned = true;
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Captures a screenshot using ScreenCapture.CaptureScreenshotAsTexture, which captures the
|
||||
/// final composited frame including UI Toolkit overlays, post-processing, etc.
|
||||
/// Falls back to camera-based capture if ScreenCapture returns null at runtime.
|
||||
/// </summary>
|
||||
public static ScreenshotCaptureResult CaptureComposited(
|
||||
string fileName = null,
|
||||
int superSize = 1,
|
||||
bool ensureUniqueFileName = true,
|
||||
bool includeImage = false,
|
||||
int maxResolution = 0,
|
||||
string folderOverride = null)
|
||||
{
|
||||
ScreenshotCaptureResult result = PrepareCaptureResult(fileName, superSize, ensureUniqueFileName, folderOverride: folderOverride, isAsync: false);
|
||||
Texture2D tex = null;
|
||||
Texture2D downscaled = null;
|
||||
string imageBase64 = null;
|
||||
int imgW = 0, imgH = 0;
|
||||
try
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
// In play mode, inline ScreenCapture reads a backbuffer before UITK has
|
||||
// composited; route through WaitForEndOfFrame instead.
|
||||
tex = Application.isPlaying
|
||||
? CaptureCompositedAfterFrame(result.SuperSize)
|
||||
: ScreenCapture.CaptureScreenshotAsTexture(result.SuperSize);
|
||||
#else
|
||||
tex = ScreenCapture.CaptureScreenshotAsTexture(result.SuperSize);
|
||||
#endif
|
||||
if (tex == null)
|
||||
{
|
||||
// Fallback to camera-based if ScreenCapture fails
|
||||
var cam = FindAvailableCamera();
|
||||
if (cam != null)
|
||||
return CaptureFromCameraToProjectFolder(cam, fileName, superSize, ensureUniqueFileName,
|
||||
includeImage, maxResolution, folderOverride: folderOverride);
|
||||
throw new InvalidOperationException("ScreenCapture.CaptureScreenshotAsTexture returned null and no fallback camera available.");
|
||||
}
|
||||
|
||||
int width = tex.width;
|
||||
int height = tex.height;
|
||||
|
||||
byte[] png = tex.EncodeToPNG();
|
||||
File.WriteAllBytes(result.FullPath, png);
|
||||
|
||||
if (includeImage)
|
||||
{
|
||||
int targetMax = maxResolution > 0 ? maxResolution : 640;
|
||||
if (width > targetMax || height > targetMax)
|
||||
{
|
||||
downscaled = DownscaleTexture(tex, targetMax);
|
||||
byte[] smallPng = downscaled.EncodeToPNG();
|
||||
imageBase64 = System.Convert.ToBase64String(smallPng);
|
||||
imgW = downscaled.width;
|
||||
imgH = downscaled.height;
|
||||
}
|
||||
else
|
||||
{
|
||||
imageBase64 = System.Convert.ToBase64String(png);
|
||||
imgW = width;
|
||||
imgH = height;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
DestroyTexture(tex);
|
||||
DestroyTexture(downscaled);
|
||||
}
|
||||
|
||||
if (includeImage && imageBase64 != null)
|
||||
{
|
||||
return new ScreenshotCaptureResult(
|
||||
result.FullPath, result.ProjectRelativePath, result.SuperSize, false,
|
||||
imageBase64, imgW, imgH);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders a camera to a Texture2D without saving to disk. Used for multi-angle captures.
|
||||
/// Returns the base64-encoded PNG, downscaled to fit within <paramref name="maxResolution"/>.
|
||||
/// </summary>
|
||||
public static (string base64, int width, int height) RenderCameraToBase64(Camera camera, int maxResolution = 640)
|
||||
{
|
||||
if (camera == null) throw new ArgumentNullException(nameof(camera));
|
||||
|
||||
int width = Mathf.Max(1, camera.pixelWidth > 0 ? camera.pixelWidth : Screen.width);
|
||||
int height = Mathf.Max(1, camera.pixelHeight > 0 ? camera.pixelHeight : Screen.height);
|
||||
|
||||
RenderTexture prevRT = camera.targetTexture;
|
||||
RenderTexture prevActive = RenderTexture.active;
|
||||
var rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32);
|
||||
Texture2D tex = null;
|
||||
Texture2D downscaled = null;
|
||||
try
|
||||
{
|
||||
camera.targetTexture = rt;
|
||||
camera.Render();
|
||||
|
||||
RenderTexture.active = rt;
|
||||
tex = new Texture2D(width, height, TextureFormat.RGBA32, false);
|
||||
tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
|
||||
tex.Apply();
|
||||
|
||||
int targetMax = maxResolution > 0 ? maxResolution : 640;
|
||||
if (width > targetMax || height > targetMax)
|
||||
{
|
||||
downscaled = DownscaleTexture(tex, targetMax);
|
||||
string b64 = System.Convert.ToBase64String(downscaled.EncodeToPNG());
|
||||
return (b64, downscaled.width, downscaled.height);
|
||||
}
|
||||
else
|
||||
{
|
||||
string b64 = System.Convert.ToBase64String(tex.EncodeToPNG());
|
||||
return (b64, width, height);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
camera.targetTexture = prevRT;
|
||||
RenderTexture.active = prevActive;
|
||||
RenderTexture.ReleaseTemporary(rt);
|
||||
DestroyTexture(tex);
|
||||
DestroyTexture(downscaled);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders a camera to a Texture2D without saving to disk.
|
||||
/// Caller owns the returned texture and must destroy it.
|
||||
/// </summary>
|
||||
public static Texture2D RenderCameraToTexture(Camera camera, int maxResolution = 640)
|
||||
{
|
||||
if (camera == null) throw new ArgumentNullException(nameof(camera));
|
||||
|
||||
int width = Mathf.Max(1, camera.pixelWidth > 0 ? camera.pixelWidth : Screen.width);
|
||||
int height = Mathf.Max(1, camera.pixelHeight > 0 ? camera.pixelHeight : Screen.height);
|
||||
|
||||
RenderTexture prevRT = camera.targetTexture;
|
||||
RenderTexture prevActive = RenderTexture.active;
|
||||
var rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32);
|
||||
Texture2D tex = null;
|
||||
try
|
||||
{
|
||||
camera.targetTexture = rt;
|
||||
camera.Render();
|
||||
|
||||
RenderTexture.active = rt;
|
||||
tex = new Texture2D(width, height, TextureFormat.RGBA32, false);
|
||||
tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
|
||||
tex.Apply();
|
||||
|
||||
int targetMax = maxResolution > 0 ? maxResolution : 640;
|
||||
if (width > targetMax || height > targetMax)
|
||||
{
|
||||
var downscaled = DownscaleTexture(tex, targetMax);
|
||||
DestroyTexture(tex);
|
||||
tex = null;
|
||||
return downscaled;
|
||||
}
|
||||
var result = tex;
|
||||
tex = null; // transfer ownership to caller
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
camera.targetTexture = prevRT;
|
||||
RenderTexture.active = prevActive;
|
||||
RenderTexture.ReleaseTemporary(rt);
|
||||
DestroyTexture(tex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Composites a list of tile textures into a single contact-sheet grid image.
|
||||
/// Labels are drawn as white text on a dark banner at the bottom of each tile.
|
||||
/// Returns base64 PNG plus dimensions. Destroys all input tile textures.
|
||||
/// </summary>
|
||||
public static (string base64, int width, int height) ComposeContactSheet(
|
||||
List<Texture2D> tiles, List<string> labels, int padding = 4)
|
||||
{
|
||||
if (tiles == null || tiles.Count == 0)
|
||||
throw new ArgumentException("No tiles to compose.", nameof(tiles));
|
||||
|
||||
int tileW = tiles[0].width;
|
||||
int tileH = tiles[0].height;
|
||||
int count = tiles.Count;
|
||||
|
||||
// Calculate grid: prefer wider than tall (cols >= rows)
|
||||
int cols = Mathf.CeilToInt(Mathf.Sqrt(count));
|
||||
int rows = Mathf.CeilToInt((float)count / cols);
|
||||
|
||||
int labelHeight = Mathf.Max(14, tileH / 12);
|
||||
int cellW = tileW + padding;
|
||||
int cellH = tileH + labelHeight + padding;
|
||||
|
||||
int sheetW = cols * cellW + padding;
|
||||
int sheetH = rows * cellH + padding;
|
||||
|
||||
Texture2D sheet = null;
|
||||
try
|
||||
{
|
||||
sheet = new Texture2D(sheetW, sheetH, TextureFormat.RGBA32, false);
|
||||
|
||||
// Build the full sheet in a Color32[] buffer, then upload once
|
||||
var bgColor = new Color32(30, 30, 30, 255);
|
||||
Color32[] sheetPixels = new Color32[sheetW * sheetH];
|
||||
for (int i = 0; i < sheetPixels.Length; i++) sheetPixels[i] = bgColor;
|
||||
|
||||
// Track label draw requests so we can apply them after the bulk upload
|
||||
var labelDraws = new List<(string text, int x, int y, int h)>();
|
||||
|
||||
for (int idx = 0; idx < count; idx++)
|
||||
{
|
||||
int col = idx % cols;
|
||||
int row = idx / cols;
|
||||
// Place tiles top-left to bottom-right (Unity Texture2D y=0 is bottom)
|
||||
int x = padding + col * cellW;
|
||||
int y = sheetH - padding - (row + 1) * cellH + padding;
|
||||
|
||||
// Copy tile pixels row-by-row using bulk operations
|
||||
Color32[] tilePixels = tiles[idx].GetPixels32();
|
||||
for (int ty = 0; ty < tileH; ty++)
|
||||
{
|
||||
int srcOffset = ty * tileW;
|
||||
int dstOffset = (y + labelHeight + ty) * sheetW + x;
|
||||
System.Array.Copy(tilePixels, srcOffset, sheetPixels, dstOffset, tileW);
|
||||
}
|
||||
|
||||
// Draw label banner (dark background strip below tile)
|
||||
var bannerColor = new Color32(20, 20, 20, 220);
|
||||
for (int ly = 0; ly < labelHeight; ly++)
|
||||
{
|
||||
int dstOffset = (y + ly) * sheetW + x;
|
||||
for (int lx = 0; lx < tileW; lx++)
|
||||
{
|
||||
sheetPixels[dstOffset + lx] = bannerColor;
|
||||
}
|
||||
}
|
||||
|
||||
// Queue label text drawing (applied after bulk pixel upload)
|
||||
if (labels != null && idx < labels.Count && !string.IsNullOrEmpty(labels[idx]))
|
||||
{
|
||||
labelDraws.Add((labels[idx], x + 3, y + 2, labelHeight - 4));
|
||||
}
|
||||
}
|
||||
|
||||
// Upload all tile + banner pixels in one SetPixels32 call
|
||||
sheet.SetPixels32(sheetPixels);
|
||||
|
||||
// Draw label text on top (small glyph-based writes, negligible cost)
|
||||
foreach (var (text, lx, ly, lh) in labelDraws)
|
||||
{
|
||||
DrawText(sheet, text, lx, ly, lh, Color.white);
|
||||
}
|
||||
|
||||
sheet.Apply();
|
||||
|
||||
byte[] png = sheet.EncodeToPNG();
|
||||
string b64 = System.Convert.ToBase64String(png);
|
||||
return (b64, sheetW, sheetH);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var tile in tiles) DestroyTexture(tile);
|
||||
DestroyTexture(sheet);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawText(Texture2D tex, string text, int startX, int startY, int charHeight, Color color)
|
||||
{
|
||||
// Simple 5x7 bitmap font for basic ASCII characters
|
||||
int charWidth = Mathf.Max(4, charHeight * 5 / 7);
|
||||
int spacing = Mathf.Max(1, charWidth / 5);
|
||||
int x = startX;
|
||||
|
||||
foreach (char c in text)
|
||||
{
|
||||
if (x + charWidth > tex.width) break;
|
||||
ulong glyph = GetGlyph(c);
|
||||
if (glyph != 0)
|
||||
{
|
||||
for (int row = 0; row < 7; row++)
|
||||
{
|
||||
for (int col = 0; col < 5; col++)
|
||||
{
|
||||
bool on = ((glyph >> ((6 - row) * 5 + (4 - col))) & 1) == 1;
|
||||
if (!on) continue;
|
||||
// Scale the 5x7 glyph to charWidth x charHeight
|
||||
int px0 = x + col * charWidth / 5;
|
||||
int px1 = x + (col + 1) * charWidth / 5;
|
||||
int py0 = startY + (6 - row) * charHeight / 7;
|
||||
int py1 = startY + (7 - row) * charHeight / 7;
|
||||
for (int py = py0; py < py1 && py < tex.height; py++)
|
||||
for (int px = px0; px < px1 && px < tex.width; px++)
|
||||
tex.SetPixel(px, py, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
x += charWidth + spacing;
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong GetGlyph(char c)
|
||||
{
|
||||
// 5x7 pixel font stored as 35-bit values (row0=bits34-30 ... row6=bits4-0)
|
||||
// Each row is 5 wide, MSB=left. Row 0 is top.
|
||||
switch (char.ToUpperInvariant(c))
|
||||
{
|
||||
case 'A': return 0b01110_10001_10001_11111_10001_10001_10001UL;
|
||||
case 'B': return 0b11110_10001_10001_11110_10001_10001_11110UL;
|
||||
case 'C': return 0b01110_10001_10000_10000_10000_10001_01110UL;
|
||||
case 'D': return 0b11100_10010_10001_10001_10001_10010_11100UL;
|
||||
case 'E': return 0b11111_10000_10000_11110_10000_10000_11111UL;
|
||||
case 'F': return 0b11111_10000_10000_11110_10000_10000_10000UL;
|
||||
case 'G': return 0b01110_10001_10000_10111_10001_10001_01110UL;
|
||||
case 'H': return 0b10001_10001_10001_11111_10001_10001_10001UL;
|
||||
case 'I': return 0b01110_00100_00100_00100_00100_00100_01110UL;
|
||||
case 'K': return 0b10001_10010_10100_11000_10100_10010_10001UL;
|
||||
case 'L': return 0b10000_10000_10000_10000_10000_10000_11111UL;
|
||||
case 'M': return 0b10001_11011_10101_10101_10001_10001_10001UL;
|
||||
case 'N': return 0b10001_11001_10101_10011_10001_10001_10001UL;
|
||||
case 'O': return 0b01110_10001_10001_10001_10001_10001_01110UL;
|
||||
case 'R': return 0b11110_10001_10001_11110_10100_10010_10001UL;
|
||||
case 'S': return 0b01110_10001_10000_01110_00001_10001_01110UL;
|
||||
case 'T': return 0b11111_00100_00100_00100_00100_00100_00100UL;
|
||||
case 'U': return 0b10001_10001_10001_10001_10001_10001_01110UL;
|
||||
case 'V': return 0b10001_10001_10001_10001_01010_01010_00100UL;
|
||||
case 'W': return 0b10001_10001_10001_10101_10101_11011_10001UL;
|
||||
case 'Y': return 0b10001_10001_01010_00100_00100_00100_00100UL;
|
||||
case '0': return 0b01110_10011_10101_10101_10101_11001_01110UL;
|
||||
case '1': return 0b00100_01100_00100_00100_00100_00100_01110UL;
|
||||
case '2': return 0b01110_10001_00001_00010_00100_01000_11111UL;
|
||||
case '3': return 0b01110_10001_00001_00110_00001_10001_01110UL;
|
||||
case '4': return 0b00010_00110_01010_10010_11111_00010_00010UL;
|
||||
case '5': return 0b11111_10000_11110_00001_00001_10001_01110UL;
|
||||
case '6': return 0b01110_10001_10000_11110_10001_10001_01110UL;
|
||||
case '7': return 0b11111_00001_00010_00100_01000_01000_01000UL;
|
||||
case '8': return 0b01110_10001_10001_01110_10001_10001_01110UL;
|
||||
case '9': return 0b01110_10001_10001_01111_00001_10001_01110UL;
|
||||
case 'J': return 0b00111_00010_00010_00010_00010_10010_01100UL;
|
||||
case 'P': return 0b11110_10001_10001_11110_10000_10000_10000UL;
|
||||
case 'Q': return 0b01110_10001_10001_10001_10101_10010_01101UL;
|
||||
case 'X': return 0b10001_01010_00100_00100_00100_01010_10001UL;
|
||||
case 'Z': return 0b11111_00001_00010_00100_01000_10000_11111UL;
|
||||
case '-': return 0b00000_00000_00000_11111_00000_00000_00000UL;
|
||||
case '_': return 0b00000_00000_00000_00000_00000_00000_11111UL;
|
||||
case ' ': return 0UL;
|
||||
case '+': return 0b00000_00100_00100_11111_00100_00100_00000UL;
|
||||
default: return 0UL;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downscales a Texture2D so that its longest edge is at most <paramref name="maxEdge"/> pixels.
|
||||
/// Uses bilinear filtering via a temporary RenderTexture blit.
|
||||
/// Caller must destroy the returned Texture2D.
|
||||
/// </summary>
|
||||
public static Texture2D DownscaleTexture(Texture2D source, int maxEdge)
|
||||
{
|
||||
if (source == null)
|
||||
throw new System.ArgumentNullException(nameof(source));
|
||||
if (maxEdge <= 0)
|
||||
throw new System.ArgumentOutOfRangeException(nameof(maxEdge), maxEdge, "maxEdge must be > 0.");
|
||||
|
||||
int srcW = source.width;
|
||||
int srcH = source.height;
|
||||
float scale = Mathf.Min((float)maxEdge / srcW, (float)maxEdge / srcH);
|
||||
scale = Mathf.Min(scale, 1f); // never upscale
|
||||
int dstW = Mathf.Max(1, Mathf.RoundToInt(srcW * scale));
|
||||
int dstH = Mathf.Max(1, Mathf.RoundToInt(srcH * scale));
|
||||
|
||||
RenderTexture prevActive = RenderTexture.active;
|
||||
var rt = RenderTexture.GetTemporary(dstW, dstH, 0, RenderTextureFormat.ARGB32);
|
||||
rt.filterMode = FilterMode.Bilinear;
|
||||
try
|
||||
{
|
||||
Graphics.Blit(source, rt);
|
||||
RenderTexture.active = rt;
|
||||
var dst = new Texture2D(dstW, dstH, TextureFormat.RGBA32, false);
|
||||
dst.ReadPixels(new Rect(0, 0, dstW, dstH), 0, 0);
|
||||
dst.Apply();
|
||||
return dst;
|
||||
}
|
||||
finally
|
||||
{
|
||||
RenderTexture.active = prevActive;
|
||||
RenderTexture.ReleaseTemporary(rt);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DestroyTexture(Texture2D tex)
|
||||
{
|
||||
if (tex == null) return;
|
||||
if (Application.isPlaying)
|
||||
UnityEngine.Object.Destroy(tex);
|
||||
else
|
||||
UnityEngine.Object.DestroyImmediate(tex);
|
||||
}
|
||||
|
||||
private static ScreenshotCaptureResult PrepareCaptureResult(string fileName, int superSize, bool ensureUniqueFileName, string folderOverride, bool isAsync)
|
||||
{
|
||||
int size = Mathf.Max(1, superSize);
|
||||
string resolvedName = BuildFileName(fileName);
|
||||
string folderAbsolute = ResolveFolderAbsolute(folderOverride);
|
||||
Directory.CreateDirectory(folderAbsolute);
|
||||
|
||||
string fullPath = Path.Combine(folderAbsolute, resolvedName);
|
||||
if (ensureUniqueFileName)
|
||||
{
|
||||
fullPath = EnsureUnique(fullPath);
|
||||
}
|
||||
|
||||
string normalizedFullPath = fullPath.Replace('\\', '/');
|
||||
string projectRelativePath = ToProjectRelativePath(normalizedFullPath);
|
||||
|
||||
return new ScreenshotCaptureResult(normalizedFullPath, projectRelativePath, size, isAsync);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a folder spec (which may be project-relative like "Assets/Screenshots",
|
||||
/// absolute, or null/empty) to an absolute filesystem path inside the project root.
|
||||
/// Throws on traversal escape or absolute paths outside the project.
|
||||
/// </summary>
|
||||
public static string ResolveFolderAbsolute(string folderOverride)
|
||||
{
|
||||
string projectRoot = GetProjectRootPath().TrimEnd('/');
|
||||
string requested = string.IsNullOrWhiteSpace(folderOverride) ? DefaultFolder : folderOverride.Trim();
|
||||
requested = requested.Replace('\\', '/').TrimEnd('/');
|
||||
|
||||
string combined = Path.IsPathRooted(requested)
|
||||
? requested
|
||||
: Path.Combine(projectRoot, requested);
|
||||
|
||||
string fullFolder = Path.GetFullPath(combined).Replace('\\', '/').TrimEnd('/');
|
||||
string normalizedRoot = projectRoot;
|
||||
|
||||
// Reject paths that escape the project root (case-insensitive on Windows, exact elsewhere).
|
||||
var rootComparison = Application.platform == RuntimePlatform.WindowsEditor
|
||||
? StringComparison.OrdinalIgnoreCase
|
||||
: StringComparison.Ordinal;
|
||||
if (!fullFolder.Equals(normalizedRoot, rootComparison) &&
|
||||
!fullFolder.StartsWith(normalizedRoot + "/", rootComparison))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Screenshot folder '{folderOverride}' resolves outside the Unity project root ('{fullFolder}'). " +
|
||||
$"Use a project-relative path (e.g. 'Assets/Screenshots' or 'Captures').");
|
||||
}
|
||||
|
||||
return fullFolder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an absolute filesystem path inside the project to a project-relative path
|
||||
/// (forward slashes, no leading separator). Returns the input unchanged when it does
|
||||
/// not live under the project root.
|
||||
/// </summary>
|
||||
public static string ToProjectRelativePath(string normalizedFullPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(normalizedFullPath)) return normalizedFullPath;
|
||||
string projectRoot = GetProjectRootPath();
|
||||
string normalized = normalizedFullPath.Replace('\\', '/');
|
||||
if (normalized.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return normalized.Substring(projectRoot.Length).TrimStart('/');
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when <paramref name="projectRelativePath"/> lives under the Unity Assets/ folder
|
||||
/// (and therefore should be imported via AssetDatabase).
|
||||
/// </summary>
|
||||
public static bool IsUnderAssets(string projectRelativePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(projectRelativePath)) return false;
|
||||
string norm = projectRelativePath.Replace('\\', '/').TrimStart('/');
|
||||
return norm.Equals("Assets", StringComparison.OrdinalIgnoreCase)
|
||||
|| norm.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string BuildFileName(string fileName)
|
||||
{
|
||||
string name = string.IsNullOrWhiteSpace(fileName)
|
||||
? $"screenshot-{DateTime.Now:yyyyMMdd-HHmmss}"
|
||||
: fileName.Trim();
|
||||
|
||||
name = SanitizeFileName(name);
|
||||
|
||||
if (!name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) &&
|
||||
!name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) &&
|
||||
!name.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
name += ".png";
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private static string SanitizeFileName(string fileName)
|
||||
{
|
||||
// GetInvalidFileNameChars() doesn't include '\' or '/' on Unix, so a caller-supplied
|
||||
// name like "foo\bar" would survive and later get spliced into the directory portion.
|
||||
var invalidChars = Path.GetInvalidFileNameChars();
|
||||
string cleaned = new string(
|
||||
fileName.Select(ch => invalidChars.Contains(ch) || ch == '/' || ch == '\\' ? '_' : ch).ToArray());
|
||||
|
||||
return string.IsNullOrWhiteSpace(cleaned) ? "screenshot" : cleaned;
|
||||
}
|
||||
|
||||
private static string EnsureUnique(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
string directory = Path.GetDirectoryName(path) ?? string.Empty;
|
||||
string baseName = Path.GetFileNameWithoutExtension(path);
|
||||
string extension = Path.GetExtension(path);
|
||||
int counter = 1;
|
||||
|
||||
string candidate;
|
||||
do
|
||||
{
|
||||
candidate = Path.Combine(directory, $"{baseName}-{counter}{extension}");
|
||||
counter++;
|
||||
} while (File.Exists(candidate));
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private static string GetProjectRootPath()
|
||||
{
|
||||
string root = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
|
||||
root = root.Replace('\\', '/');
|
||||
if (!root.EndsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
root += "/";
|
||||
}
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transient MonoBehaviour that yields WaitForEndOfFrame, calls
|
||||
/// ScreenCapture.CaptureScreenshotAsTexture, invokes the callback, and self-destructs.
|
||||
/// </summary>
|
||||
public sealed class ScreenshotCapturer : MonoBehaviour
|
||||
{
|
||||
private int _superSize = 1;
|
||||
private Action<Texture2D> _onComplete;
|
||||
|
||||
/// <summary>Spawns a hidden GameObject, attaches a capturer, returns immediately.</summary>
|
||||
public static void Begin(int superSize, Action<Texture2D> onComplete)
|
||||
{
|
||||
var go = new GameObject("__MCP_ScreenshotCapturer__") { hideFlags = HideFlags.HideAndDontSave };
|
||||
var c = go.AddComponent<ScreenshotCapturer>();
|
||||
c._superSize = Mathf.Max(1, superSize);
|
||||
c._onComplete = onComplete;
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator Start()
|
||||
{
|
||||
yield return new WaitForEndOfFrame();
|
||||
Texture2D tex = null;
|
||||
try { tex = ScreenCapture.CaptureScreenshotAsTexture(_superSize); }
|
||||
catch (Exception ex) { Debug.LogError($"[MCP for Unity] CaptureScreenshotAsTexture failed: {ex.Message}"); }
|
||||
_onComplete?.Invoke(tex);
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b33f3e9c912b4f17a5a4374e4f6c2a91
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MCPForUnity.Runtime.Helpers
|
||||
{
|
||||
// Part of MCP for Unity's compat-shim family. See UnityCompatShims.cs in this
|
||||
// folder for the full list of shims, the audit policy, and the reflection pattern.
|
||||
/// <summary>
|
||||
/// Version-compatible wrapper for enumerating loaded assemblies.
|
||||
///
|
||||
/// API timeline:
|
||||
/// Pre-6.8 : AppDomain.CurrentDomain.GetAssemblies()
|
||||
/// 6.8 (CoreCLR) : UnityEngine.Assemblies.CurrentAssemblies.GetLoadedAssemblies()
|
||||
/// (Unity 6.8 replaces Mono with CoreCLR and warns on
|
||||
/// AppDomain.GetAssemblies — see the official Path to
|
||||
/// CoreCLR 2026 upgrade guide.)
|
||||
///
|
||||
/// Uses runtime reflection to discover the new API once and cache a delegate,
|
||||
/// so calling code stays warning-free across versions and survives the
|
||||
/// eventual removal of AppDomain.GetAssemblies if Unity ever takes that step.
|
||||
/// </summary>
|
||||
public static class UnityAssembliesCompat
|
||||
{
|
||||
// Candidate assembly-qualified names to try BEFORE falling back to a full
|
||||
// assembly scan. Probing by name avoids calling AppDomain.GetAssemblies on
|
||||
// CoreCLR (where it emits warnings) in the common case.
|
||||
private static readonly string[] CurrentAssembliesAqns =
|
||||
{
|
||||
"UnityEngine.Assemblies.CurrentAssemblies, UnityEngine.CoreModule",
|
||||
"UnityEngine.Assemblies.CurrentAssemblies, UnityEngine",
|
||||
"UnityEngine.Assemblies.CurrentAssemblies, UnityEditor.CoreModule",
|
||||
"UnityEngine.Assemblies.CurrentAssemblies, UnityEditor",
|
||||
};
|
||||
|
||||
private static Func<Assembly[]> _getLoadedAssemblies;
|
||||
private static bool _probed;
|
||||
|
||||
/// <summary>
|
||||
/// Returns all currently loaded managed assemblies in this Unity process.
|
||||
/// On Unity 6.8+ (CoreCLR) this dispatches to
|
||||
/// <c>UnityEngine.Assemblies.CurrentAssemblies.GetLoadedAssemblies()</c>;
|
||||
/// otherwise falls back to <c>AppDomain.CurrentDomain.GetAssemblies()</c>.
|
||||
/// </summary>
|
||||
public static Assembly[] GetLoadedAssemblies()
|
||||
{
|
||||
if (!_probed)
|
||||
{
|
||||
_probed = true;
|
||||
_getLoadedAssemblies = ResolveCurrentAssembliesDelegate();
|
||||
}
|
||||
|
||||
if (_getLoadedAssemblies != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _getLoadedAssemblies();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If the new API throws for any reason, fall through to the legacy path.
|
||||
}
|
||||
}
|
||||
|
||||
return AppDomain.CurrentDomain.GetAssemblies();
|
||||
}
|
||||
|
||||
private static Func<Assembly[]> ResolveCurrentAssembliesDelegate()
|
||||
{
|
||||
// 1. Try direct AQN lookups first — cheap, side-effect-free, and
|
||||
// avoids touching AppDomain.GetAssemblies on CoreCLR.
|
||||
foreach (var aqn in CurrentAssembliesAqns)
|
||||
{
|
||||
Type type;
|
||||
try { type = Type.GetType(aqn, throwOnError: false); }
|
||||
catch { type = null; }
|
||||
|
||||
var del = TryBindGetLoadedAssemblies(type);
|
||||
if (del != null) return del;
|
||||
}
|
||||
|
||||
// 2. Fallback: scan every loaded assembly. This still uses the legacy
|
||||
// enumeration API but only runs once during the bootstrap probe,
|
||||
// and only when none of the AQNs above resolved.
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
Type type;
|
||||
try { type = asm.GetType("UnityEngine.Assemblies.CurrentAssemblies", throwOnError: false); }
|
||||
catch { continue; }
|
||||
|
||||
var del = TryBindGetLoadedAssemblies(type);
|
||||
if (del != null) return del;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Func<Assembly[]> TryBindGetLoadedAssemblies(Type type)
|
||||
{
|
||||
if (type == null) return null;
|
||||
|
||||
var method = type.GetMethod(
|
||||
"GetLoadedAssemblies",
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null,
|
||||
Type.EmptyTypes,
|
||||
null);
|
||||
|
||||
if (method == null || !typeof(Assembly[]).IsAssignableFrom(method.ReturnType))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return (Func<Assembly[]>)Delegate.CreateDelegate(typeof(Func<Assembly[]>), method);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return () => (Assembly[])method.Invoke(null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: feef0112fe7e7b8b1928f08d0b97272e
|
||||
@@ -0,0 +1,41 @@
|
||||
namespace MCPForUnity.Runtime.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Index of the version-compatibility shims MCP for Unity ships. These wrap Unity APIs
|
||||
/// that have been renamed, deprecated, or scheduled for removal across the Unity versions
|
||||
/// the package targets (2021 LTS → 6.x → CoreCLR 6.8). Routing through a shim keeps
|
||||
/// CS0618 warnings out of the build and survives the eventual property/method removal
|
||||
/// without recompiling call sites.
|
||||
///
|
||||
/// Active shims (deprecated_since → removed_in):
|
||||
/// • <see cref="UnityFindObjectsCompat"/> — Object.FindObjectsOfType → FindObjectsByType (2023.1)
|
||||
/// • <see cref="UnityObjectIdCompat"/> — InstanceID ↔ EntityId (6000.3 → 6000.6 CS0619)
|
||||
/// • <see cref="UnityPhysicsCompat"/> — Physics{,2D}.autoSyncTransforms (6000.0),
|
||||
/// Physics{,2D}.autoSimulation → simulationMode (2022.2)
|
||||
/// • <see cref="UnityAssembliesCompat"/> — AppDomain.GetAssemblies →
|
||||
/// UnityEngine.Assemblies.CurrentAssemblies (Unity 6.8 CoreCLR)
|
||||
///
|
||||
/// When to add a new shim:
|
||||
/// 1. The API is marked [Obsolete] AND the call site can't simply be deleted, OR
|
||||
/// 2. Three or more call sites need version gating for the same API, OR
|
||||
/// 3. A future Unity version has announced rename or removal.
|
||||
///
|
||||
/// What does NOT belong in a shim: hot-path engine APIs (Transform.position, Vector3.*,
|
||||
/// GetComponent<T>), APIs Unity has not threatened to break (Mathf, Quaternion,
|
||||
/// most of AssetDatabase), and editor-internal undocumented APIs (those should break
|
||||
/// loudly so the package maintainers notice).
|
||||
///
|
||||
/// Pattern: prefer #if UNITY_*_OR_NEWER for static dispatch when the new API exists in
|
||||
/// the SDK we compile against; use runtime reflection with a cached MethodInfo /
|
||||
/// PropertyInfo when the new API is in a version we don't yet target, or when the old
|
||||
/// API may eventually be removed (CS0619). Fail-soft: callers should treat missing APIs
|
||||
/// as no-ops, not throw.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class is intentionally empty — its purpose is to anchor the catalog so any
|
||||
/// reader can <c>F12</c> from a shim file and land on the full list and policy.
|
||||
/// </remarks>
|
||||
public static class UnityCompatShims
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f3c6d4e5a7b4f3eb1c2d6a8e9f1b3d5
|
||||
@@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using UObject = UnityEngine.Object;
|
||||
|
||||
namespace MCPForUnity.Runtime.Helpers
|
||||
{
|
||||
// Part of MCP for Unity's compat-shim family. See UnityCompatShims.cs in this
|
||||
// folder for the full list of shims, the audit policy, and the reflection pattern.
|
||||
/// <summary>
|
||||
/// Version-compatible wrappers for the Object.FindObjectsOfType / FindObjectsByType family.
|
||||
///
|
||||
/// API timeline:
|
||||
/// Pre-2022.3 : FindObjectsOfType / FindObjectOfType
|
||||
/// 2022.3-6.4 : FindObjectsByType(sortMode) / FindAnyObjectByType
|
||||
/// 6.5+ : FindObjectsByType() (no sort param) / FindAnyObjectByType
|
||||
///
|
||||
/// Notes:
|
||||
/// - Some Unity 2022.2.x editor versions did not reliably expose FindObjectsByType,
|
||||
/// so the new APIs are gated at 2022.3+ rather than 2022.2+ (issue with 2022.2.1f1).
|
||||
/// - On the legacy branch we dispatch to FindObjectsOfType / FindObjectOfType through
|
||||
/// reflection rather than direct calls. This keeps the file CS0618-clean across all
|
||||
/// SDKs we compile against and lets the package keep working if Unity ever fully
|
||||
/// removes the legacy methods (CS0619).
|
||||
/// </summary>
|
||||
public static class UnityFindObjectsCompat
|
||||
{
|
||||
/// <summary>Find all active objects of type T.</summary>
|
||||
public static T[] FindAll<T>() where T : UObject
|
||||
{
|
||||
#if UNITY_6000_5_OR_NEWER
|
||||
return UObject.FindObjectsByType<T>();
|
||||
#elif UNITY_2022_3_OR_NEWER
|
||||
return UObject.FindObjectsByType<T>(UnityEngine.FindObjectsSortMode.None);
|
||||
#else
|
||||
var arr = LegacyFindObjectsOfType(typeof(T));
|
||||
if (arr == null) return Array.Empty<T>();
|
||||
var typed = new T[arr.Length];
|
||||
for (int i = 0; i < arr.Length; i++) typed[i] = (T)arr[i];
|
||||
return typed;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Find all active objects of the given runtime type.</summary>
|
||||
public static UObject[] FindAll(Type type)
|
||||
{
|
||||
#if UNITY_6000_5_OR_NEWER
|
||||
return UObject.FindObjectsByType(type, UnityEngine.FindObjectsInactive.Exclude);
|
||||
#elif UNITY_2022_3_OR_NEWER
|
||||
return UObject.FindObjectsByType(type, UnityEngine.FindObjectsSortMode.None);
|
||||
#else
|
||||
return LegacyFindObjectsOfType(type) ?? Array.Empty<UObject>();
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Find all objects of the given runtime type, optionally including inactive.</summary>
|
||||
public static UObject[] FindAll(Type type, bool includeInactive)
|
||||
{
|
||||
#if UNITY_6000_5_OR_NEWER
|
||||
return UObject.FindObjectsByType(type,
|
||||
includeInactive ? UnityEngine.FindObjectsInactive.Include : UnityEngine.FindObjectsInactive.Exclude);
|
||||
#elif UNITY_2022_3_OR_NEWER
|
||||
return UObject.FindObjectsByType(type,
|
||||
includeInactive ? UnityEngine.FindObjectsInactive.Include : UnityEngine.FindObjectsInactive.Exclude,
|
||||
UnityEngine.FindObjectsSortMode.None);
|
||||
#else
|
||||
return LegacyFindObjectsOfType(type, includeInactive) ?? Array.Empty<UObject>();
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Find any single object of the given runtime type (no ordering guarantee).</summary>
|
||||
public static UObject FindAny(Type type)
|
||||
{
|
||||
#if UNITY_2022_3_OR_NEWER
|
||||
return UObject.FindAnyObjectByType(type);
|
||||
#else
|
||||
return LegacyFindObjectOfType(type);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !UNITY_2022_3_OR_NEWER
|
||||
// ---------- legacy reflection helpers (pre-2022.3 only) ----------
|
||||
|
||||
private static MethodInfo _findObjectsOfTypeByType;
|
||||
private static bool _findObjectsOfTypeByTypeProbed;
|
||||
|
||||
private static MethodInfo _findObjectsOfTypeWithInactive;
|
||||
private static bool _findObjectsOfTypeWithInactiveProbed;
|
||||
|
||||
private static MethodInfo _findObjectOfTypeByType;
|
||||
private static bool _findObjectOfTypeByTypeProbed;
|
||||
|
||||
private static UObject[] LegacyFindObjectsOfType(Type type)
|
||||
{
|
||||
if (!_findObjectsOfTypeByTypeProbed)
|
||||
{
|
||||
_findObjectsOfTypeByTypeProbed = true;
|
||||
_findObjectsOfTypeByType = typeof(UObject).GetMethod(
|
||||
"FindObjectsOfType",
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null,
|
||||
new[] { typeof(Type) },
|
||||
null);
|
||||
}
|
||||
if (_findObjectsOfTypeByType == null) return null;
|
||||
try { return (UObject[])_findObjectsOfTypeByType.Invoke(null, new object[] { type }); }
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
private static UObject[] LegacyFindObjectsOfType(Type type, bool includeInactive)
|
||||
{
|
||||
if (!_findObjectsOfTypeWithInactiveProbed)
|
||||
{
|
||||
_findObjectsOfTypeWithInactiveProbed = true;
|
||||
_findObjectsOfTypeWithInactive = typeof(UObject).GetMethod(
|
||||
"FindObjectsOfType",
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null,
|
||||
new[] { typeof(Type), typeof(bool) },
|
||||
null);
|
||||
}
|
||||
if (_findObjectsOfTypeWithInactive == null)
|
||||
{
|
||||
// Older Unity versions only had the (Type) overload — fall back without the flag.
|
||||
return LegacyFindObjectsOfType(type);
|
||||
}
|
||||
try { return (UObject[])_findObjectsOfTypeWithInactive.Invoke(null, new object[] { type, includeInactive }); }
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
private static UObject LegacyFindObjectOfType(Type type)
|
||||
{
|
||||
if (!_findObjectOfTypeByTypeProbed)
|
||||
{
|
||||
_findObjectOfTypeByTypeProbed = true;
|
||||
_findObjectOfTypeByType = typeof(UObject).GetMethod(
|
||||
"FindObjectOfType",
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null,
|
||||
new[] { typeof(Type) },
|
||||
null);
|
||||
}
|
||||
if (_findObjectOfTypeByType == null) return null;
|
||||
try { return (UObject)_findObjectOfTypeByType.Invoke(null, new object[] { type }); }
|
||||
catch { return null; }
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c3a9f12e4b60438d9a1c52ef8d07b3e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,75 @@
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace MCPForUnity.Runtime.Helpers
|
||||
{
|
||||
// Part of MCP for Unity's compat-shim family. See UnityCompatShims.cs in this
|
||||
// folder for the full list of shims, the audit policy, and the reflection pattern.
|
||||
/// <summary>
|
||||
/// Version-gated wrappers for the InstanceID ↔ EntityId migration introduced in Unity 6.5
|
||||
/// and tightened in 6.6.
|
||||
/// Forward (Object → int): <see cref="GetInstanceIDCompat"/>
|
||||
/// Reverse (int → Object, Editor-only): <see cref="InstanceIDToObjectCompat"/>
|
||||
/// </summary>
|
||||
public static class UnityObjectIdCompat
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a session-scoped int handle for the object. On 6.5+ truncates the
|
||||
/// EntityId's underlying ulong; lossy but stable within a session and preserves
|
||||
/// the int-shaped wire format that older consumers expect. For deserialization
|
||||
/// round-trips on 6.5+, prefer the full <c>entityID</c> field.
|
||||
/// </summary>
|
||||
public static int GetInstanceIDCompat(this Object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if UNITY_6000_5_OR_NEWER
|
||||
return (int)EntityId.ToULong(obj.GetEntityId());
|
||||
#else
|
||||
return obj.GetInstanceID();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
#if UNITY_6000_6_OR_NEWER
|
||||
private static MethodInfo _instanceIdToObject;
|
||||
private static bool _instanceIdToObjectInitialized;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Resolves an int instance ID handle back to a UnityEngine.Object.
|
||||
/// Pre-6.0 : EditorUtility.InstanceIDToObject(int)
|
||||
/// 6.0–6.5 : EditorUtility.EntityIdToObject(int) (implicit int→EntityId cast)
|
||||
/// 6.6+ : reflection on InstanceIDToObject(int) — the API still exists at runtime
|
||||
/// but is obsolete-as-error; reflection bypasses CS0619 until the public
|
||||
/// EntityId(int) ctor stabilizes.
|
||||
/// </summary>
|
||||
public static Object InstanceIDToObjectCompat(int instanceId)
|
||||
{
|
||||
#if UNITY_6000_6_OR_NEWER
|
||||
if (!_instanceIdToObjectInitialized)
|
||||
{
|
||||
_instanceIdToObject = typeof(EditorUtility).GetMethod(
|
||||
"InstanceIDToObject",
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null,
|
||||
new[] { typeof(int) },
|
||||
null);
|
||||
_instanceIdToObjectInitialized = true;
|
||||
}
|
||||
return _instanceIdToObject?.Invoke(null, new object[] { instanceId }) as Object;
|
||||
#elif UNITY_6000_3_OR_NEWER
|
||||
return EditorUtility.EntityIdToObject(instanceId);
|
||||
#else
|
||||
return EditorUtility.InstanceIDToObject(instanceId);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ea8b580f3e80459cab9019f71d0ccb6
|
||||
@@ -0,0 +1,261 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Runtime.Helpers
|
||||
{
|
||||
// Part of MCP for Unity's compat-shim family. See UnityCompatShims.cs in this
|
||||
// folder for the full list of shims, the audit policy, and the reflection pattern.
|
||||
/// <summary>
|
||||
/// Version-compatible wrappers for Physics / Physics2D properties whose surface
|
||||
/// changes across Unity versions.
|
||||
///
|
||||
/// Currently covered:
|
||||
/// - Physics.autoSyncTransforms (deprecated in Unity 6.x; replacement is Physics.SyncTransforms())
|
||||
/// - Physics2D.autoSyncTransforms (deprecated in Unity 6.x; replacement is Physics2D.SyncTransforms())
|
||||
/// - Physics.autoSimulation (deprecated in 2022.2; replacement is Physics.simulationMode)
|
||||
///
|
||||
/// We use reflection rather than direct property access so calls stay clean of
|
||||
/// CS0618 warnings AND survive eventual removal of the obsolete property without
|
||||
/// a recompile of this package.
|
||||
/// </summary>
|
||||
public static class UnityPhysicsCompat
|
||||
{
|
||||
/// <summary>
|
||||
/// Cross-version description of the 3D physics simulation mode.
|
||||
/// On 2022.2+ this maps onto <c>UnityEngine.SimulationMode</c>; on older
|
||||
/// versions it represents the on/off semantics of <c>autoSimulation</c>.
|
||||
/// </summary>
|
||||
public enum SimulationMode
|
||||
{
|
||||
FixedUpdate,
|
||||
Update,
|
||||
Script,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
// ---------- Physics2D ----------
|
||||
|
||||
private static PropertyInfo _physics2DAutoSync;
|
||||
private static bool _physics2DProbed;
|
||||
|
||||
private static PropertyInfo Physics2DAutoSyncProp
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_physics2DProbed)
|
||||
{
|
||||
_physics2DProbed = true;
|
||||
_physics2DAutoSync = typeof(Physics2D).GetProperty(
|
||||
"autoSyncTransforms",
|
||||
BindingFlags.Public | BindingFlags.Static);
|
||||
}
|
||||
return _physics2DAutoSync;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads <c>Physics2D.autoSyncTransforms</c> if the property exists in this
|
||||
/// Unity version. Returns <c>null</c> if the property has been removed.
|
||||
/// </summary>
|
||||
public static bool? GetPhysics2DAutoSyncTransforms()
|
||||
{
|
||||
var prop = Physics2DAutoSyncProp;
|
||||
if (prop == null || !prop.CanRead) return null;
|
||||
try { return (bool)prop.GetValue(null); }
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes <c>Physics2D.autoSyncTransforms</c> if the property exists and is
|
||||
/// writable. Returns <c>true</c> if the write happened, <c>false</c> if the
|
||||
/// property is unavailable in this Unity version.
|
||||
/// </summary>
|
||||
public static bool TrySetPhysics2DAutoSyncTransforms(bool value)
|
||||
{
|
||||
var prop = Physics2DAutoSyncProp;
|
||||
if (prop == null || !prop.CanWrite) return false;
|
||||
try
|
||||
{
|
||||
prop.SetValue(null, value);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Physics (3D) ----------
|
||||
|
||||
private static PropertyInfo _physicsAutoSync;
|
||||
private static bool _physicsProbed;
|
||||
|
||||
private static PropertyInfo PhysicsAutoSyncProp
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_physicsProbed)
|
||||
{
|
||||
_physicsProbed = true;
|
||||
_physicsAutoSync = typeof(Physics).GetProperty(
|
||||
"autoSyncTransforms",
|
||||
BindingFlags.Public | BindingFlags.Static);
|
||||
}
|
||||
return _physicsAutoSync;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads <c>Physics.autoSyncTransforms</c> if the property exists in this
|
||||
/// Unity version. Returns <c>null</c> if the property has been removed.
|
||||
/// </summary>
|
||||
public static bool? GetPhysicsAutoSyncTransforms()
|
||||
{
|
||||
var prop = PhysicsAutoSyncProp;
|
||||
if (prop == null || !prop.CanRead) return null;
|
||||
try { return (bool)prop.GetValue(null); }
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes <c>Physics.autoSyncTransforms</c> if the property exists and is
|
||||
/// writable. Returns <c>true</c> if the write happened, <c>false</c> if the
|
||||
/// property is unavailable in this Unity version.
|
||||
/// </summary>
|
||||
public static bool TrySetPhysicsAutoSyncTransforms(bool value)
|
||||
{
|
||||
var prop = PhysicsAutoSyncProp;
|
||||
if (prop == null || !prop.CanWrite) return false;
|
||||
try
|
||||
{
|
||||
prop.SetValue(null, value);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Physics simulation mode (3D) ----------
|
||||
// 2022.2+ : UnityEngine.Physics.simulationMode (UnityEngine.SimulationMode enum)
|
||||
// <2022.2 : UnityEngine.Physics.autoSimulation (bool — true ⇔ FixedUpdate, false ⇔ Script)
|
||||
|
||||
private static PropertyInfo _physicsSimulationMode;
|
||||
private static bool _physicsSimulationModeProbed;
|
||||
private static PropertyInfo _physicsAutoSimulation;
|
||||
private static bool _physicsAutoSimulationProbed;
|
||||
|
||||
private static PropertyInfo PhysicsSimulationModeProp
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_physicsSimulationModeProbed)
|
||||
{
|
||||
_physicsSimulationModeProbed = true;
|
||||
_physicsSimulationMode = typeof(Physics).GetProperty(
|
||||
"simulationMode",
|
||||
BindingFlags.Public | BindingFlags.Static);
|
||||
}
|
||||
return _physicsSimulationMode;
|
||||
}
|
||||
}
|
||||
|
||||
private static PropertyInfo PhysicsAutoSimulationProp
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_physicsAutoSimulationProbed)
|
||||
{
|
||||
_physicsAutoSimulationProbed = true;
|
||||
_physicsAutoSimulation = typeof(Physics).GetProperty(
|
||||
"autoSimulation",
|
||||
BindingFlags.Public | BindingFlags.Static);
|
||||
}
|
||||
return _physicsAutoSimulation;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the current 3D physics simulation mode in a Unity-version-agnostic way.
|
||||
/// Returns <see cref="SimulationMode.Unknown"/> if neither API is available.
|
||||
/// </summary>
|
||||
public static SimulationMode GetPhysicsSimulationMode()
|
||||
{
|
||||
var modeProp = PhysicsSimulationModeProp;
|
||||
if (modeProp != null && modeProp.CanRead)
|
||||
{
|
||||
try { return ParseSimulationMode(modeProp.GetValue(null)?.ToString()); }
|
||||
catch { /* fall through */ }
|
||||
}
|
||||
|
||||
var autoProp = PhysicsAutoSimulationProp;
|
||||
if (autoProp != null && autoProp.CanRead)
|
||||
{
|
||||
try
|
||||
{
|
||||
var auto = (bool)autoProp.GetValue(null);
|
||||
return auto ? SimulationMode.FixedUpdate : SimulationMode.Script;
|
||||
}
|
||||
catch { /* fall through */ }
|
||||
}
|
||||
|
||||
return SimulationMode.Unknown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the 3D physics simulation mode in a Unity-version-agnostic way.
|
||||
/// Returns false if the requested mode isn't expressible on this Unity version
|
||||
/// (e.g. Update mode on pre-2022.2).
|
||||
/// </summary>
|
||||
public static bool TrySetPhysicsSimulationMode(SimulationMode mode)
|
||||
{
|
||||
var modeProp = PhysicsSimulationModeProp;
|
||||
if (modeProp != null && modeProp.CanWrite)
|
||||
{
|
||||
try
|
||||
{
|
||||
var enumType = modeProp.PropertyType;
|
||||
var enumValue = Enum.Parse(enumType, mode.ToString(), ignoreCase: true);
|
||||
modeProp.SetValue(null, enumValue);
|
||||
return true;
|
||||
}
|
||||
catch { /* fall through to legacy bool path */ }
|
||||
}
|
||||
|
||||
var autoProp = PhysicsAutoSimulationProp;
|
||||
if (autoProp != null && autoProp.CanWrite)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case SimulationMode.FixedUpdate:
|
||||
autoProp.SetValue(null, true);
|
||||
return true;
|
||||
case SimulationMode.Script:
|
||||
autoProp.SetValue(null, false);
|
||||
return true;
|
||||
// Update mode does not exist pre-2022.2 — caller must handle false.
|
||||
}
|
||||
}
|
||||
catch { /* fall through */ }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static SimulationMode ParseSimulationMode(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s)) return SimulationMode.Unknown;
|
||||
switch (s.ToLowerInvariant())
|
||||
{
|
||||
case "fixedupdate": return SimulationMode.FixedUpdate;
|
||||
case "update": return SimulationMode.Update;
|
||||
case "script": return SimulationMode.Script;
|
||||
default: return SimulationMode.Unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 94833dcb3434af57591bf6eac4b7f9aa
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "MCPForUnity.Runtime",
|
||||
"rootNamespace": "MCPForUnity.Runtime",
|
||||
"references": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 562a750ff18ee4193928e885c708fee1
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7e33d6224fe6473f9bc69fe6d40e508
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,519 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Runtime.Helpers;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor; // Required for AssetDatabase and EditorUtility
|
||||
#endif
|
||||
|
||||
namespace MCPForUnity.Runtime.Serialization
|
||||
{
|
||||
public class Vector3Converter : JsonConverter<Vector3>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Vector3 value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("x");
|
||||
writer.WriteValue(value.x);
|
||||
writer.WritePropertyName("y");
|
||||
writer.WriteValue(value.y);
|
||||
writer.WritePropertyName("z");
|
||||
writer.WriteValue(value.z);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Vector3 ReadJson(JsonReader reader, Type objectType, Vector3 existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JToken token = JToken.Load(reader);
|
||||
if (token is JArray arr && arr.Count >= 3)
|
||||
return new Vector3((float)arr[0], (float)arr[1], (float)arr[2]);
|
||||
if (token is not JObject jo)
|
||||
throw new JsonSerializationException($"Cannot deserialize Vector3 from {token.Type}: '{token}'");
|
||||
return new Vector3(
|
||||
(float)jo["x"],
|
||||
(float)jo["y"],
|
||||
(float)jo["z"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class Vector2Converter : JsonConverter<Vector2>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("x");
|
||||
writer.WriteValue(value.x);
|
||||
writer.WritePropertyName("y");
|
||||
writer.WriteValue(value.y);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JToken token = JToken.Load(reader);
|
||||
if (token is JArray arr && arr.Count >= 2)
|
||||
return new Vector2((float)arr[0], (float)arr[1]);
|
||||
if (token is not JObject jo)
|
||||
throw new JsonSerializationException($"Cannot deserialize Vector2 from {token.Type}: '{token}'");
|
||||
return new Vector2(
|
||||
(float)jo["x"],
|
||||
(float)jo["y"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class QuaternionConverter : JsonConverter<Quaternion>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Quaternion value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("x");
|
||||
writer.WriteValue(value.x);
|
||||
writer.WritePropertyName("y");
|
||||
writer.WriteValue(value.y);
|
||||
writer.WritePropertyName("z");
|
||||
writer.WriteValue(value.z);
|
||||
writer.WritePropertyName("w");
|
||||
writer.WriteValue(value.w);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Quaternion ReadJson(JsonReader reader, Type objectType, Quaternion existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JToken token = JToken.Load(reader);
|
||||
if (token is JArray arr && arr.Count >= 4)
|
||||
return new Quaternion((float)arr[0], (float)arr[1], (float)arr[2], (float)arr[3]);
|
||||
if (token is not JObject jo)
|
||||
throw new JsonSerializationException($"Cannot deserialize Quaternion from {token.Type}: '{token}'");
|
||||
return new Quaternion(
|
||||
(float)jo["x"],
|
||||
(float)jo["y"],
|
||||
(float)jo["z"],
|
||||
(float)jo["w"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class ColorConverter : JsonConverter<Color>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Color value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("r");
|
||||
writer.WriteValue(value.r);
|
||||
writer.WritePropertyName("g");
|
||||
writer.WriteValue(value.g);
|
||||
writer.WritePropertyName("b");
|
||||
writer.WriteValue(value.b);
|
||||
writer.WritePropertyName("a");
|
||||
writer.WriteValue(value.a);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Color ReadJson(JsonReader reader, Type objectType, Color existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
return new Color(
|
||||
(float)jo["r"],
|
||||
(float)jo["g"],
|
||||
(float)jo["b"],
|
||||
(float)jo["a"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class RectConverter : JsonConverter<Rect>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Rect value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("x");
|
||||
writer.WriteValue(value.x);
|
||||
writer.WritePropertyName("y");
|
||||
writer.WriteValue(value.y);
|
||||
writer.WritePropertyName("width");
|
||||
writer.WriteValue(value.width);
|
||||
writer.WritePropertyName("height");
|
||||
writer.WriteValue(value.height);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Rect ReadJson(JsonReader reader, Type objectType, Rect existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
return new Rect(
|
||||
(float)jo["x"],
|
||||
(float)jo["y"],
|
||||
(float)jo["width"],
|
||||
(float)jo["height"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class BoundsConverter : JsonConverter<Bounds>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Bounds value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("center");
|
||||
serializer.Serialize(writer, value.center); // Use serializer to handle nested Vector3
|
||||
writer.WritePropertyName("size");
|
||||
serializer.Serialize(writer, value.size); // Use serializer to handle nested Vector3
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Bounds ReadJson(JsonReader reader, Type objectType, Bounds existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
Vector3 center = jo["center"].ToObject<Vector3>(serializer); // Use serializer to handle nested Vector3
|
||||
Vector3 size = jo["size"].ToObject<Vector3>(serializer); // Use serializer to handle nested Vector3
|
||||
return new Bounds(center, size);
|
||||
}
|
||||
}
|
||||
|
||||
public class Vector4Converter : JsonConverter<Vector4>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Vector4 value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("x");
|
||||
writer.WriteValue(value.x);
|
||||
writer.WritePropertyName("y");
|
||||
writer.WriteValue(value.y);
|
||||
writer.WritePropertyName("z");
|
||||
writer.WriteValue(value.z);
|
||||
writer.WritePropertyName("w");
|
||||
writer.WriteValue(value.w);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Vector4 ReadJson(JsonReader reader, Type objectType, Vector4 existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JToken token = JToken.Load(reader);
|
||||
if (token is JArray arr && arr.Count >= 4)
|
||||
return new Vector4((float)arr[0], (float)arr[1], (float)arr[2], (float)arr[3]);
|
||||
if (token is not JObject jo)
|
||||
throw new JsonSerializationException($"Cannot deserialize Vector4 from {token.Type}: '{token}'");
|
||||
return new Vector4(
|
||||
(float)jo["x"],
|
||||
(float)jo["y"],
|
||||
(float)jo["z"],
|
||||
(float)jo["w"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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.)
|
||||
// Intentionally internal: this converter is meant for MCP-internal serialization only.
|
||||
// Leaving it public lets third-party Newtonsoft converter scanners (e.g. jillejr's
|
||||
// newtonsoft-json-for-unity converters) instantiate it via reflection and bind it into
|
||||
// JsonConvert.DefaultSettings, which silently rewrites any UnityEngine.Object reference in
|
||||
// unrelated project code as an asset path string. See issue #1138.
|
||||
internal class UnityEngineObjectConverter : JsonConverter<UnityEngine.Object>
|
||||
{
|
||||
public override bool CanRead => true; // We need to implement ReadJson
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override void WriteJson(JsonWriter writer, UnityEngine.Object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNull();
|
||||
return;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR // AssetDatabase and EditorUtility are Editor-only
|
||||
if (UnityEditor.AssetDatabase.Contains(value))
|
||||
{
|
||||
// It's an asset (Material, Texture, Prefab, etc.)
|
||||
string path = UnityEditor.AssetDatabase.GetAssetPath(value);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
writer.WriteValue(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Asset exists but path couldn't be found? Write minimal info.
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("name");
|
||||
writer.WriteValue(value.name);
|
||||
WriteSerializedObjectId(writer, value);
|
||||
writer.WritePropertyName("isAssetWithoutPath");
|
||||
writer.WriteValue(true);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// It's a scene object (GameObject, Component, etc.)
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("name");
|
||||
writer.WriteValue(value.name);
|
||||
WriteSerializedObjectId(writer, value);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
#else
|
||||
// Runtime fallback: Write basic info without AssetDatabase
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("name");
|
||||
writer.WriteValue(value.name);
|
||||
WriteSerializedObjectId(writer, value);
|
||||
writer.WritePropertyName("warning");
|
||||
writer.WriteValue("UnityEngineObjectConverter running in non-Editor mode, asset path unavailable.");
|
||||
writer.WriteEndObject();
|
||||
#endif
|
||||
}
|
||||
|
||||
public override UnityEngine.Object ReadJson(JsonReader reader, Type objectType, UnityEngine.Object existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (reader.TokenType == JsonToken.String)
|
||||
{
|
||||
string strValue = reader.Value.ToString();
|
||||
|
||||
// Check if it looks like a GUID (32 hex chars, optionally with hyphens)
|
||||
if (IsValidGuid(strValue))
|
||||
{
|
||||
string path = UnityEditor.AssetDatabase.GUIDToAssetPath(strValue.Replace("-", "").ToLowerInvariant());
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
var asset = UnityEditor.AssetDatabase.LoadAssetAtPath(path, objectType);
|
||||
if (asset != null) return asset;
|
||||
}
|
||||
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Could not load asset with GUID '{strValue}' as type '{objectType.Name}'.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Assume it's an asset path
|
||||
var loadedAsset = UnityEditor.AssetDatabase.LoadAssetAtPath(strValue, objectType);
|
||||
if (loadedAsset == null)
|
||||
{
|
||||
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Could not load asset at path '{strValue}' as type '{objectType.Name}'.");
|
||||
}
|
||||
return loadedAsset;
|
||||
}
|
||||
|
||||
if (reader.TokenType == JsonToken.StartObject)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
|
||||
// Try to resolve by GUID first (for assets like ScriptableObjects, Materials, etc.)
|
||||
if (jo.TryGetValue("guid", out JToken guidToken) && guidToken.Type == JTokenType.String)
|
||||
{
|
||||
string guid = guidToken.ToString().Replace("-", "").ToLowerInvariant();
|
||||
string path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
var asset = UnityEditor.AssetDatabase.LoadAssetAtPath(path, objectType);
|
||||
if (asset != null) return asset;
|
||||
}
|
||||
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Could not load asset with GUID '{guidToken}' as type '{objectType.Name}'.");
|
||||
return null;
|
||||
}
|
||||
|
||||
#if UNITY_6000_5_OR_NEWER
|
||||
// Try to resolve by entityID (Unity 6.5+). Falls through to instanceID/guid/path on failure.
|
||||
if (jo.TryGetValue("entityID", out JToken entityIdToken) && entityIdToken.Type == JTokenType.String)
|
||||
{
|
||||
string serializedEntityId = entityIdToken.ToString();
|
||||
if (ulong.TryParse(serializedEntityId, out ulong rawEntityId))
|
||||
{
|
||||
EntityId eid = EntityId.FromULong(rawEntityId);
|
||||
UnityEngine.Object entityObj = UnityEditor.EditorUtility.EntityIdToObject(eid);
|
||||
if (entityObj != null)
|
||||
{
|
||||
if (objectType.IsAssignableFrom(entityObj.GetType()))
|
||||
{
|
||||
return entityObj;
|
||||
}
|
||||
|
||||
if (objectType == typeof(Transform) && entityObj is GameObject entityGo)
|
||||
{
|
||||
return entityGo.transform;
|
||||
}
|
||||
|
||||
if (typeof(Component).IsAssignableFrom(objectType) && entityObj is GameObject entityGameObj)
|
||||
{
|
||||
var component = entityGameObj.GetComponent(objectType);
|
||||
if (component != null)
|
||||
{
|
||||
return component;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Could not resolve entityID '{serializedEntityId}' to a valid {objectType.Name}. Falling back to instanceID/guid/path.");
|
||||
}
|
||||
#endif
|
||||
|
||||
// Try to resolve by instanceID
|
||||
if (jo.TryGetValue("instanceID", out JToken idToken) && idToken.Type == JTokenType.Integer)
|
||||
{
|
||||
int instanceId = idToken.ToObject<int>();
|
||||
UnityEngine.Object obj = UnityObjectIdCompat.InstanceIDToObjectCompat(instanceId);
|
||||
if (obj != null)
|
||||
{
|
||||
// Direct type match
|
||||
if (objectType.IsAssignableFrom(obj.GetType()))
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Special case: expecting Transform but got GameObject - get its transform
|
||||
if (objectType == typeof(Transform) && obj is GameObject go)
|
||||
{
|
||||
return go.transform;
|
||||
}
|
||||
|
||||
// Special case: expecting a Component type but got GameObject - try to get the component
|
||||
if (typeof(Component).IsAssignableFrom(objectType) && obj is GameObject gameObj)
|
||||
{
|
||||
var component = gameObj.GetComponent(objectType);
|
||||
if (component != null)
|
||||
{
|
||||
return component;
|
||||
}
|
||||
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] GameObject '{gameObj.name}' (ID: {instanceId}) does not have a '{objectType.Name}' component.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Type mismatch with no automatic conversion available
|
||||
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Instance ID {instanceId} resolved to '{obj.GetType().Name}' but expected '{objectType.Name}'.");
|
||||
return null;
|
||||
}
|
||||
// Instance ID lookup failed - this can happen if the object was destroyed or ID is stale
|
||||
string objectName = jo.TryGetValue("name", out JToken nameToken) ? nameToken.ToString() : "unknown";
|
||||
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Could not resolve instance ID {instanceId} (name: '{objectName}') to a valid {objectType.Name}. The object may have been destroyed or the ID is stale.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if there's an asset path in the object
|
||||
if (jo.TryGetValue("path", out JToken pathToken) && pathToken.Type == JTokenType.String)
|
||||
{
|
||||
string path = pathToken.ToString();
|
||||
var asset = UnityEditor.AssetDatabase.LoadAssetAtPath(path, objectType);
|
||||
if (asset != null)
|
||||
{
|
||||
return asset;
|
||||
}
|
||||
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Could not load asset at path '{path}' as type '{objectType.Name}'.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Object format not recognized
|
||||
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] JSON object missing 'instanceID', 'entityID', 'guid', or 'path' field for {objectType.Name} deserialization. Object: {jo.ToString(Formatting.None)}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Unexpected token type
|
||||
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Unexpected token type '{reader.TokenType}' when deserializing {objectType.Name}. Expected Null, String, or Object.");
|
||||
return null;
|
||||
#else
|
||||
// Runtime deserialization is tricky without AssetDatabase/EditorUtility
|
||||
UnityEngine.Debug.LogWarning("UnityEngineObjectConverter cannot deserialize complex objects in non-Editor mode.");
|
||||
// Skip the current token to avoid breaking the reader state
|
||||
reader.Skip();
|
||||
// Return existing value since we can't deserialize without Editor APIs
|
||||
return existingValue;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a string looks like a valid GUID (32 hex chars, with or without hyphens).
|
||||
/// </summary>
|
||||
private static bool IsValidGuid(string str)
|
||||
{
|
||||
if (string.IsNullOrEmpty(str)) return false;
|
||||
string normalized = str.Replace("-", "");
|
||||
if (normalized.Length != 32) return false;
|
||||
foreach (char c in normalized)
|
||||
{
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void WriteSerializedObjectId(JsonWriter writer, UnityEngine.Object value)
|
||||
{
|
||||
// Always emit instanceID so older consumers keep working.
|
||||
writer.WritePropertyName("instanceID");
|
||||
writer.WriteValue(value.GetInstanceIDCompat());
|
||||
#if UNITY_6000_5_OR_NEWER
|
||||
// Additionally emit entityID on Unity 6.5+ as the stable ulong form
|
||||
// (per Unity docs, EntityId.ToString() is NOT a stable serialization format).
|
||||
writer.WritePropertyName("entityID");
|
||||
writer.WriteValue(EntityId.ToULong(value.GetEntityId()).ToString());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e65311c160f0d41d4a1b45a3dba8dd5a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user