[Add] Synaptic AI Pro
https://assetstore.unity.com/packages/tools/generative-ai/synaptic-ai-pro-natural-language-control-for-unity-336030
This commit is contained in:
@@ -0,0 +1,583 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace SynapticPro
|
||||
{
|
||||
/// <summary>
|
||||
/// Arbitrary C# evaluation for the Editor — equivalent of Blender's
|
||||
/// run_python tool. Wraps Mono.CSharp.Evaluator (instance API) so callers
|
||||
/// can execute any C# snippet against the running Editor without
|
||||
/// triggering an AssemblyReload.
|
||||
///
|
||||
/// Static Evaluator.Init/Run on Unity 2022.3+ silently no-ops; the real
|
||||
/// path is `new Evaluator(new CompilerContext(new CompilerSettings(),
|
||||
/// new ConsoleReportPrinter()))` plus injecting every assembly already
|
||||
/// loaded in the AppDomain so UnityEngine / UnityEditor / Newtonsoft.Json
|
||||
/// resolve.
|
||||
///
|
||||
/// Expressions ("1+1", "GameObject.Find(\"X\").name") must NOT end with
|
||||
/// a semicolon — those are evaluated via Evaluate(...). Statements
|
||||
/// ("var x = 1; Debug.Log(x);") run through Run(...).
|
||||
/// </summary>
|
||||
public static class NexusCSharpEval
|
||||
{
|
||||
private static object _evaluator;
|
||||
private static MethodInfo _evaluateMethod;
|
||||
private static MethodInfo _runMethod;
|
||||
private static MethodInfo _referenceAssemblyMethod;
|
||||
private static StringBuilder _captured = new StringBuilder();
|
||||
private static readonly object _lock = new object();
|
||||
// ESC-0107 fix (E): capture Unity Debug.Log output that Console.SetOut
|
||||
// can't catch (UnityEngine.Debug routes through Unity Console, not
|
||||
// managed Console.Out). Subscribe to Application.logMessageReceived
|
||||
// while a Run call is in progress.
|
||||
private static bool _captureUnityLogs = false;
|
||||
private static Application.LogCallback _logCallback;
|
||||
|
||||
// ESC-0107 fix (D, revised): receive the return value from `return X;`
|
||||
// snippets via a static field. The user's `return X;` is rewritten to
|
||||
// `SynapticPro.NexusCSharpEval.__SetResult(X);` and executed through
|
||||
// Evaluator.Run, which lets Mono.CSharp's normal method-body parser
|
||||
// handle the expression (no pointer-type ambiguity, no Evaluate
|
||||
// restrictions). We then read the field back in managed code.
|
||||
public static object __LastResult;
|
||||
public static bool __LastResultSet;
|
||||
|
||||
public static void __SetResult(object value)
|
||||
{
|
||||
__LastResult = value;
|
||||
__LastResultSet = true;
|
||||
}
|
||||
|
||||
public static string Run(Dictionary<string, string> parameters)
|
||||
{
|
||||
var code = parameters != null && parameters.TryGetValue("code", out var c) ? c : "";
|
||||
if (string.IsNullOrEmpty(code))
|
||||
{
|
||||
return JsonConvert.SerializeObject(new
|
||||
{
|
||||
success = false,
|
||||
error = "code parameter is required",
|
||||
example = "GameObject.Find(\"Cube\")?.name"
|
||||
});
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!EnsureInitialized(out var initError))
|
||||
{
|
||||
return JsonConvert.SerializeObject(new
|
||||
{
|
||||
success = false,
|
||||
error = initError
|
||||
});
|
||||
}
|
||||
|
||||
_captured.Length = 0;
|
||||
var oldOut = Console.Out;
|
||||
Console.SetOut(new StringWriter(_captured));
|
||||
// Hook Unity Debug.Log into the capture buffer (ESC-0107 fix E).
|
||||
AttachUnityLogCapture();
|
||||
try
|
||||
{
|
||||
// ESC-0107 fix D (revised again): rewrite the trailing
|
||||
// `return X;` into a call to our static `__SetResult(X)`
|
||||
// sink, then Run the whole thing as statements. This
|
||||
// sidesteps two Mono.CSharp quirks:
|
||||
// - Run(...) discards real `return` values
|
||||
// - Evaluate("x * 1") mis-parses var*literal as a
|
||||
// pointer-type declaration, returning resultSet=false
|
||||
// Inside a normal Run body the parser treats `*` as
|
||||
// unambiguous multiplication and `__SetResult(X)` as a
|
||||
// regular method call. The receiver field is read back
|
||||
// in managed code after Run completes.
|
||||
var trimmed = code.TrimEnd();
|
||||
SplitReturnTail(trimmed, out var prefixStatements, out var returnExpression);
|
||||
|
||||
// For bare expressions (no `;`) we still use Evaluate
|
||||
// — it works fine for that case and avoids a needless
|
||||
// method-call wrap.
|
||||
string expressionToEvaluate = null;
|
||||
string rewrittenStatements = null;
|
||||
if (returnExpression != null)
|
||||
{
|
||||
rewrittenStatements = string.IsNullOrEmpty(prefixStatements)
|
||||
? $"SynapticPro.NexusCSharpEval.__SetResult({returnExpression});"
|
||||
: $"{prefixStatements} SynapticPro.NexusCSharpEval.__SetResult({returnExpression});";
|
||||
}
|
||||
else if (!trimmed.EndsWith(";"))
|
||||
{
|
||||
expressionToEvaluate = code;
|
||||
}
|
||||
|
||||
// Reset the sink before every call so a stale value
|
||||
// from a previous run can't leak through.
|
||||
__LastResult = null;
|
||||
__LastResultSet = false;
|
||||
|
||||
if (rewrittenStatements != null && _runMethod != null)
|
||||
{
|
||||
// First try plain Run. Works for most snippets and
|
||||
// is the cheapest path. If it succeeds but doesn't
|
||||
// set the result (e.g. the user's code contains
|
||||
// generic type syntax `List<int>` which Mono.CSharp
|
||||
// Evaluator's top-level parser chokes on), retry
|
||||
// wrapped in an immediately-invoked Action lambda
|
||||
// — Mono parses the lambda body with the regular
|
||||
// method-body parser, which handles generics fine.
|
||||
object runResultR;
|
||||
try { runResultR = _runMethod.Invoke(_evaluator, new object[] { rewrittenStatements }); }
|
||||
catch (TargetInvocationException tie)
|
||||
{
|
||||
return Error(tie.InnerException ?? tie);
|
||||
}
|
||||
bool runOkR = runResultR is bool rbR && rbR;
|
||||
|
||||
if (!__LastResultSet)
|
||||
{
|
||||
// Reset for the wrapped retry (output already
|
||||
// captured what the failed attempt printed,
|
||||
// which should be nothing — Run prints nothing
|
||||
// on parse failure).
|
||||
__LastResult = null;
|
||||
__LastResultSet = false;
|
||||
var wrapped = $"((System.Action)(() => {{ {rewrittenStatements} }}))();";
|
||||
try { runResultR = _runMethod.Invoke(_evaluator, new object[] { wrapped }); }
|
||||
catch (TargetInvocationException tie)
|
||||
{
|
||||
return Error(tie.InnerException ?? tie);
|
||||
}
|
||||
runOkR = runResultR is bool rbR2 && rbR2;
|
||||
}
|
||||
|
||||
return JsonConvert.SerializeObject(new
|
||||
{
|
||||
success = runOkR,
|
||||
output = _captured.ToString(),
|
||||
result = __LastResultSet ? SafeStringify(__LastResult) : null,
|
||||
resultSet = __LastResultSet
|
||||
});
|
||||
}
|
||||
|
||||
if (expressionToEvaluate != null && _evaluateMethod != null)
|
||||
{
|
||||
var args = new object[] { expressionToEvaluate, null, false };
|
||||
object remainderObj = null;
|
||||
try { remainderObj = _evaluateMethod.Invoke(_evaluator, args); }
|
||||
catch (TargetInvocationException tie)
|
||||
{
|
||||
return Error(tie.InnerException ?? tie);
|
||||
}
|
||||
|
||||
string remainder = remainderObj as string ?? "";
|
||||
object result = args[1];
|
||||
bool resultSet = args[2] is bool b && b;
|
||||
|
||||
if (string.IsNullOrEmpty(remainder))
|
||||
{
|
||||
return JsonConvert.SerializeObject(new
|
||||
{
|
||||
success = true,
|
||||
output = _captured.ToString(),
|
||||
result = resultSet ? SafeStringify(result) : null,
|
||||
resultSet
|
||||
});
|
||||
}
|
||||
// Evaluator returned remainder — the "expression" we
|
||||
// extracted was actually parsed as statements. Fall
|
||||
// through to statement-only path for the full input.
|
||||
}
|
||||
|
||||
// Pure-statement input (no return form, no bare expression)
|
||||
// — fall through to plain Run. Captures stdout but no
|
||||
// value. Note: with the lambda-wrap approach above we
|
||||
// never executed the prefix as a side effect already,
|
||||
// so the original `code` is safe to Run here as-is.
|
||||
|
||||
// Statement mode.
|
||||
if (_runMethod == null)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new
|
||||
{
|
||||
success = false,
|
||||
error = "Evaluator.Run method not found on this Mono.CSharp build"
|
||||
});
|
||||
}
|
||||
|
||||
object runResult;
|
||||
try { runResult = _runMethod.Invoke(_evaluator, new object[] { code }); }
|
||||
catch (TargetInvocationException tie)
|
||||
{
|
||||
return Error(tie.InnerException ?? tie);
|
||||
}
|
||||
|
||||
bool runOk = runResult is bool rb && rb;
|
||||
// Also surface __SetResult writes from user code, even
|
||||
// when we didn't auto-rewrite. Lets advanced callers do
|
||||
// their own SetResult invocation (e.g. inside lambdas
|
||||
// when avoiding the Evaluator generic-parse bug).
|
||||
if (__LastResultSet)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new
|
||||
{
|
||||
success = runOk,
|
||||
output = _captured.ToString(),
|
||||
result = SafeStringify(__LastResult),
|
||||
resultSet = true
|
||||
});
|
||||
}
|
||||
return JsonConvert.SerializeObject(new
|
||||
{
|
||||
success = runOk,
|
||||
output = _captured.ToString(),
|
||||
result = (object)null
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(oldOut);
|
||||
DetachUnityLogCapture();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool EnsureInitialized(out string error)
|
||||
{
|
||||
error = null;
|
||||
if (_evaluator != null) return true;
|
||||
|
||||
Assembly mcs = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.FirstOrDefault(a => a.GetName().Name == "Mono.CSharp");
|
||||
if (mcs == null)
|
||||
{
|
||||
try { mcs = Assembly.Load("Mono.CSharp"); } catch { /* try alt below */ }
|
||||
}
|
||||
if (mcs == null)
|
||||
{
|
||||
error = "Mono.CSharp.dll is not loaded in this Unity build. " +
|
||||
"Add an .asmdef reference to Mono.CSharp or use a Unity " +
|
||||
"version that bundles it (most Unity LTS releases do).";
|
||||
return false;
|
||||
}
|
||||
|
||||
Type settingsType = mcs.GetType("Mono.CSharp.CompilerSettings");
|
||||
Type printerType = mcs.GetType("Mono.CSharp.ConsoleReportPrinter");
|
||||
Type reportType = mcs.GetType("Mono.CSharp.Report");
|
||||
Type contextType = mcs.GetType("Mono.CSharp.CompilerContext");
|
||||
Type evalType = mcs.GetType("Mono.CSharp.Evaluator");
|
||||
|
||||
if (settingsType == null || printerType == null || contextType == null || evalType == null)
|
||||
{
|
||||
error = "Mono.CSharp internal types missing " +
|
||||
$"(settings={settingsType != null}, printer={printerType != null}, " +
|
||||
$"context={contextType != null}, eval={evalType != null}).";
|
||||
return false;
|
||||
}
|
||||
|
||||
object settings = Activator.CreateInstance(settingsType);
|
||||
object printer = Activator.CreateInstance(printerType);
|
||||
|
||||
// Try CompilerContext(CompilerSettings, ReportPrinter)
|
||||
object ctx = null;
|
||||
ConstructorInfo ctxCtor = contextType.GetConstructors()
|
||||
.FirstOrDefault(ci => ci.GetParameters().Length == 2);
|
||||
if (ctxCtor != null)
|
||||
{
|
||||
try { ctx = ctxCtor.Invoke(new object[] { settings, printer }); }
|
||||
catch { ctx = null; }
|
||||
}
|
||||
|
||||
if (ctx == null)
|
||||
{
|
||||
error = "Could not construct Mono.CSharp.CompilerContext.";
|
||||
return false;
|
||||
}
|
||||
|
||||
ConstructorInfo evalCtor = evalType.GetConstructor(new[] { contextType });
|
||||
if (evalCtor == null)
|
||||
{
|
||||
error = "Mono.CSharp.Evaluator(CompilerContext) constructor not found.";
|
||||
return false;
|
||||
}
|
||||
_evaluator = evalCtor.Invoke(new object[] { ctx });
|
||||
|
||||
_evaluateMethod = evalType.GetMethod(
|
||||
"Evaluate",
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
null,
|
||||
new[] { typeof(string), typeof(object).MakeByRefType(), typeof(bool).MakeByRefType() },
|
||||
null);
|
||||
|
||||
_runMethod = evalType.GetMethod(
|
||||
"Run",
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
null,
|
||||
new[] { typeof(string) },
|
||||
null);
|
||||
|
||||
_referenceAssemblyMethod = evalType.GetMethod(
|
||||
"ReferenceAssembly",
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
null,
|
||||
new[] { typeof(Assembly) },
|
||||
null);
|
||||
|
||||
// Inject every already-loaded assembly so user code can reach
|
||||
// UnityEngine / UnityEditor / Newtonsoft.Json / the project's
|
||||
// own scripts without manual `using`.
|
||||
if (_referenceAssemblyMethod != null)
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
try
|
||||
{
|
||||
if (asm.IsDynamic) continue;
|
||||
if (string.IsNullOrEmpty(asm.Location)) continue;
|
||||
_referenceAssemblyMethod.Invoke(_evaluator, new object[] { asm });
|
||||
}
|
||||
catch { /* skip individual failures */ }
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-import common namespaces.
|
||||
if (_runMethod != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_runMethod.Invoke(_evaluator, new object[]
|
||||
{
|
||||
"using System; " +
|
||||
"using System.Collections.Generic; " +
|
||||
"using System.Linq; " +
|
||||
"using System.IO; " +
|
||||
"using UnityEngine; " +
|
||||
"using UnityEditor; " +
|
||||
"using Newtonsoft.Json;"
|
||||
});
|
||||
}
|
||||
catch { /* best-effort */ }
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string Error(Exception e)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new
|
||||
{
|
||||
success = false,
|
||||
error = e.Message,
|
||||
stackTrace = e.StackTrace
|
||||
});
|
||||
}
|
||||
|
||||
private static object SafeStringify(object value)
|
||||
{
|
||||
if (value == null) return null;
|
||||
try
|
||||
{
|
||||
var t = value.GetType();
|
||||
if (t.IsPrimitive || value is string) return value;
|
||||
if (typeof(UnityEngine.Object).IsAssignableFrom(t)) return value.ToString();
|
||||
return JsonConvert.SerializeObject(value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return value?.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Split `[statements...] return X;` into (prefix, expression).
|
||||
/// Locates the LAST top-level `return` keyword (depth 0 from braces,
|
||||
/// parens, brackets, strings and comments) and splits there.
|
||||
///
|
||||
/// Earlier implementations split on the last top-level `;` and then
|
||||
/// checked that the resulting final statement started with `return`.
|
||||
/// That broke on inputs like `foreach (var x in xs) { X(); } return Y;`
|
||||
/// because the only top-level `;` is the trailing one (the `X();`
|
||||
/// inside braces is at depth 1) so the "final statement" became the
|
||||
/// entire body including the foreach — not starting with `return`.
|
||||
///
|
||||
/// The new approach: scan for `return` itself at depth 0, take
|
||||
/// everything before it as the prefix (its predecessor will end in
|
||||
/// `;` or `}` — both are valid statement terminators that Mono.CSharp
|
||||
/// Evaluator.Run accepts), take everything between `return` and the
|
||||
/// trailing `;` as the expression.
|
||||
/// </summary>
|
||||
private static void SplitReturnTail(string trimmed, out string prefix, out string expression)
|
||||
{
|
||||
prefix = "";
|
||||
expression = null;
|
||||
if (string.IsNullOrEmpty(trimmed)) return;
|
||||
if (!trimmed.EndsWith(";")) return;
|
||||
|
||||
var withoutSemi = trimmed.Substring(0, trimmed.Length - 1).TrimEnd();
|
||||
|
||||
int returnIdx = FindLastTopLevelReturnKeyword(withoutSemi);
|
||||
if (returnIdx < 0) return;
|
||||
|
||||
// Expression is the slice after the `return` keyword.
|
||||
var expr = withoutSemi.Substring(returnIdx + "return".Length).Trim();
|
||||
if (string.IsNullOrEmpty(expr)) return;
|
||||
|
||||
expression = expr;
|
||||
if (returnIdx > 0)
|
||||
{
|
||||
// Prefix is everything before `return`. Validate it terminates
|
||||
// with `;` or `}` — anything else (e.g. half-written input)
|
||||
// would be malformed, in which case skip the rewrite.
|
||||
var candidatePrefix = withoutSemi.Substring(0, returnIdx).TrimEnd();
|
||||
if (!candidatePrefix.EndsWith(";") && !candidatePrefix.EndsWith("}"))
|
||||
{
|
||||
expression = null;
|
||||
return;
|
||||
}
|
||||
prefix = candidatePrefix;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the start index of the LAST occurrence of the `return`
|
||||
/// keyword in <paramref name="s"/> that sits at the top level
|
||||
/// (outside any (), [], {} group, and outside string/char/comment
|
||||
/// literals). Also enforces token boundaries so `Return`,
|
||||
/// `myReturn`, `returnX` etc. don't match.
|
||||
///
|
||||
/// Returns -1 when no such keyword exists.
|
||||
/// </summary>
|
||||
private static int FindLastTopLevelReturnKeyword(string s)
|
||||
{
|
||||
const string KW = "return";
|
||||
int paren = 0, bracket = 0, brace = 0;
|
||||
int lastReturn = -1;
|
||||
int i = 0;
|
||||
while (i < s.Length)
|
||||
{
|
||||
char c = s[i];
|
||||
|
||||
// Line comment // ...
|
||||
if (c == '/' && i + 1 < s.Length && s[i + 1] == '/')
|
||||
{
|
||||
while (i < s.Length && s[i] != '\n') i++;
|
||||
continue;
|
||||
}
|
||||
// Block comment /* ... */
|
||||
if (c == '/' && i + 1 < s.Length && s[i + 1] == '*')
|
||||
{
|
||||
i += 2;
|
||||
while (i + 1 < s.Length && !(s[i] == '*' && s[i + 1] == '/')) i++;
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
// String / verbatim / interpolated — skip until closing quote.
|
||||
if (c == '"' || c == '\'')
|
||||
{
|
||||
char quote = c;
|
||||
bool verbatim = i > 0 && (s[i - 1] == '@' || s[i - 1] == '$');
|
||||
i++;
|
||||
while (i < s.Length)
|
||||
{
|
||||
if (!verbatim && s[i] == '\\') { i += 2; continue; }
|
||||
if (s[i] == quote)
|
||||
{
|
||||
if (verbatim && i + 1 < s.Length && s[i + 1] == quote) { i += 2; continue; }
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c == '(') { paren++; i++; continue; }
|
||||
if (c == ')') { paren--; i++; continue; }
|
||||
if (c == '[') { bracket++; i++; continue; }
|
||||
if (c == ']') { bracket--; i++; continue; }
|
||||
if (c == '{') { brace++; i++; continue; }
|
||||
if (c == '}') { brace--; i++; continue; }
|
||||
|
||||
// Check for `return` keyword start at this position.
|
||||
if (paren == 0 && bracket == 0 && brace == 0 &&
|
||||
c == KW[0] && i + KW.Length <= s.Length &&
|
||||
s.Substring(i, KW.Length) == KW)
|
||||
{
|
||||
// Left boundary: must be start of string OR a non-identifier
|
||||
// char (whitespace, `}`, `;`, `{`, etc.).
|
||||
bool leftOk = (i == 0) || !IsIdentChar(s[i - 1]);
|
||||
// Right boundary: must be whitespace or `(` after the keyword.
|
||||
bool rightOk = false;
|
||||
if (i + KW.Length < s.Length)
|
||||
{
|
||||
var nxt = s[i + KW.Length];
|
||||
rightOk = !IsIdentChar(nxt);
|
||||
}
|
||||
else
|
||||
{
|
||||
rightOk = true; // EOF immediately after — `return` with no expr (handled later)
|
||||
}
|
||||
if (leftOk && rightOk)
|
||||
{
|
||||
lastReturn = i;
|
||||
i += KW.Length;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
return lastReturn;
|
||||
}
|
||||
|
||||
private static bool IsIdentChar(char c)
|
||||
{
|
||||
return c == '_' || char.IsLetterOrDigit(c);
|
||||
}
|
||||
|
||||
private static void AttachUnityLogCapture()
|
||||
{
|
||||
if (_captureUnityLogs) return;
|
||||
_captureUnityLogs = true;
|
||||
_logCallback = (string condition, string stackTrace, LogType type) =>
|
||||
{
|
||||
// Mirror Debug.Log / LogWarning / LogError into _captured so the
|
||||
// caller sees what their script printed (NexusCSharpEval is the
|
||||
// only writer to _captured during a Run call, no contention).
|
||||
try
|
||||
{
|
||||
var prefix = type == LogType.Error || type == LogType.Exception ? "[error] "
|
||||
: type == LogType.Warning ? "[warn] "
|
||||
: "";
|
||||
_captured.Append(prefix).Append(condition).Append('\n');
|
||||
}
|
||||
catch { /* best-effort, never throw from log hook */ }
|
||||
};
|
||||
Application.logMessageReceived += _logCallback;
|
||||
}
|
||||
|
||||
private static void DetachUnityLogCapture()
|
||||
{
|
||||
if (!_captureUnityLogs) return;
|
||||
try
|
||||
{
|
||||
if (_logCallback != null) Application.logMessageReceived -= _logCallback;
|
||||
}
|
||||
catch { }
|
||||
_captureUnityLogs = false;
|
||||
_logCallback = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user