Skip to content

Instantly share code, notes, and snippets.

@JLChnToZ
Last active July 30, 2023 19:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JLChnToZ/69822a2e6f5b0fd8ca9dd8c6989a6257 to your computer and use it in GitHub Desktop.
Save JLChnToZ/69822a2e6f5b0fd8ca9dd8c6989a6257 to your computer and use it in GitHub Desktop.
Udon Assembly Low Level Builder for VRChat, helper for generating Udon Assembly programmatically.
using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using VRC.Udon;
using VRC.Udon.Common;
using VRC.Udon.Common.Interfaces;
using VRC.Udon.Graph;
using VRC.Udon.Editor;
using VRC.Udon.EditorBindings;
namespace JLChnToZ.VRC.UdonLowLevel {
public sealed class UdonAssemblyBuilder {
static readonly Regex autoVariableStripper = new Regex("[\\W_]+", RegexOptions.Compiled);
static readonly object constNull = new object(), constThis = new object(), constThisGameObject = new object(), constThisTransform = new object();
public const uint ReturnAddress = uint.MaxValue - 7;
readonly Dictionary<UdonOpt, string> entryPoints = new Dictionary<UdonOpt, string>();
readonly HashSet<string> entryPointNames = new HashSet<string>();
readonly Dictionary<VariableName, VariableDefinition> variableDefs = new Dictionary<VariableName, VariableDefinition>();
readonly HashSet<string> uniqueExterns = new HashSet<string>();
readonly Dictionary<object, VariableName> constants = new Dictionary<object, VariableName>();
UdonOpt firstOpt, lastOpt;
string nextEntryPointName;
string assembly;
UdonEditorInterface editorInterface;
IUdonProgram program;
public uint Size => lastOpt != null ? lastOpt.offset + lastOpt.Size : 0;
public UdonOpt LastOpt => lastOpt;
public readonly bool typeCheck;
public VariableName ReturnVariable => UdonBehaviour.ReturnVariableName;
public VariableName NullVariable => GetConstant(constNull);
public VariableName ThisVariable => GetConstant(constThis);
public VariableName ThisGameObjectVariable => GetConstant(constThisGameObject);
public VariableName ThisTransformVariable => GetConstant(constThisTransform);
public UdonAssemblyBuilder() : this(true) {}
public UdonAssemblyBuilder(bool typeCheck) {
this.typeCheck = typeCheck;
}
public void DefineVariable<T>(
VariableName variableName,
VariableAttributes attributes = VariableAttributes.None,
T value = default
) => DefineVariable(variableName, new VariableDefinition(typeof(T), attributes, value));
public void DefineVariable(
VariableName variableName,
Type type = null,
VariableAttributes attributes = VariableAttributes.None,
object value = null
) => DefineVariable(variableName, new VariableDefinition(type, attributes, value));
public void DefineVariable(VariableName variableName, VariableDefinition definition) {
if (!variableName.IsValid)
throw new ArgumentNullException(nameof(variableName));
if (variableName.TryGetPredefinedType(out var fixedType)) {
definition.type = fixedType;
definition.attributes &= VariableAttributes.Public;
}
var value = definition.value;
if (value == constNull ||
value == constThis ||
value == constThisGameObject ||
value == constThisTransform)
definition.value = null;
else if (definition.type != null && definition.type != typeof(void)) {
definition.type = definition.type.GetUdonSupportedType();
if (value == null) {
if (definition.type.IsValueType) definition.value = Activator.CreateInstance(definition.type);
} else if (!definition.type.IsInstanceOfType(value))
throw new ArgumentException("Type mismatch");
}
if (definition.attributes.HasFlag(VariableAttributes.Constant)) {
var value2 = value ?? constNull;
if (!constants.ContainsKey(value2)) constants[value2] = variableName;
}
if (definition.type == null) definition.type = typeof(object);
if (variableDefs.TryGetValue(variableName, out var existing) &&
existing.attributes.HasFlag(VariableAttributes.Constant) &&
(definition.type != existing.type || definition.value != existing.value))
throw new ArgumentException("Attempt to modify existing constant type.");
variableDefs[variableName] = definition;
if (entryPointNames.Contains(VariableChangedEvent.EVENT_PREFIX + variableName.key))
variableDefs[variableName.ChangeEventOldVariable] = new VariableDefinition(definition.type, VariableAttributes.Constant);
assembly = null;
}
public bool TryGetVariable(VariableName variableName, out VariableDefinition result) {
if (variableDefs.TryGetValue(variableName, out result)) return true;
if (variableName.TryGetPredefinedType(out var fixedType)) {
DefineVariable(variableName, fixedType);
result = variableDefs[variableName];
return true;
}
return false;
}
public void DefineEvent(string name) => nextEntryPointName = name;
public void DefineEvent(string name, UdonOpt opt) {
entryPointNames.Add(name);
entryPoints[opt] = name;
assembly = null;
}
public void DefineVariableChangeEvent(VariableName target) {
EnsureVariableChangeVariableDefined(target);
DefineEvent(VariableChangedEvent.EVENT_PREFIX + target.key);
}
public void DefineVariableChangeEvent(VariableName target, UdonOpt opt) {
EnsureVariableChangeVariableDefined(target);
DefineEvent(VariableChangedEvent.EVENT_PREFIX + target.key, opt);
}
void EnsureVariableChangeVariableDefined(VariableName target) {
if (!target.IsValid)
throw new ArgumentNullException(nameof(target));
if (!variableDefs.TryGetValue(target, out var definition))
throw new ArgumentException($"Variable {target} is not defined.");
if (typeCheck && (definition.attributes & (VariableAttributes.Public | VariableAttributes.Sync)) == 0)
throw new ArgumentException($"Variable {target} is not public or syncd.");
var oldVariable = target.ChangeEventOldVariable;
if (variableDefs.TryGetValue(oldVariable, out var newDefinition)) {
if (typeCheck) {
if (newDefinition.type != definition.type)
throw new ArgumentException($"Variable {oldVariable} is already defined with different type.");
if (newDefinition.attributes != VariableAttributes.Constant)
throw new ArgumentException($"Variable {oldVariable} is already defined with different attributes.");
}
} else
DefineVariable(oldVariable, definition.type);
}
private VariableName GetConstant(object value) {
if (value == null) value = constNull;
if (!constants.TryGetValue(value, out var varName)) {
string baseName;
var attributes = VariableAttributes.Constant;
Type type;
if (value == constNull) {
type = typeof(void);
baseName = "__const_nil";
} else if (value == constThis) {
type = typeof(UdonBehaviour);
baseName = "__const_this";
attributes |= VariableAttributes.DefaultThis;
} else if (value == constThisGameObject) {
type = typeof(GameObject);
baseName = "__const_thisGameObject";
attributes |= VariableAttributes.DefaultThis;
} else if (value == constThisTransform) {
type = typeof(Transform);
baseName = "__const_thisTransform";
attributes |= VariableAttributes.DefaultThis;
} else {
type = value.GetType();
var temp = autoVariableStripper.Replace($"{type.Name}_{value}", "_").TrimEnd();
if (temp.Length > 64) temp = temp.Substring(0, 64);
baseName = $"__const_{temp}";
}
varName = baseName;
int i = 0;
while (variableDefs.ContainsKey(varName))
varName = $"{baseName}_{i++}";
DefineVariable(varName, type, attributes, value);
}
return varName;
}
public void Emit(UdonOpt opt) {
if (opt == null || opt.next != null)
throw new ArgumentException("Opt is null or already connected.");
if (!string.IsNullOrEmpty(nextEntryPointName))
DefineEvent(nextEntryPointName, opt);
nextEntryPointName = null;
assembly = null;
if (lastOpt == null) {
firstOpt = lastOpt = opt;
} else {
opt.offset = lastOpt.offset + lastOpt.Size;
lastOpt.next = opt;
lastOpt = opt;
}
}
public NopOpt EmitNop() {
var opt = new NopOpt();
Emit(opt);
return opt;
}
public CopyOpt EmitCopy() {
var opt = new CopyOpt();
Emit(opt);
return opt;
}
public PopOpt EmitPop() {
var opt = new PopOpt();
Emit(opt);
return opt;
}
public CopyOpt EmitCopy(object varFrom, VariableName varTo) {
var varFromName = varFrom is VariableName name ? name.key : GetConstant(varFrom);
EmitPush(varFromName, null);
if (typeCheck) DoTypeCheck(varTo, variableDefs[varFromName].type);
EmitPush(varTo, null);
return EmitCopy();
}
public CopyOpt EmitCopyOffset(VariableName dest, int relativeOffset = 0) {
const uint Opt_SIZE = PushOpt.SIZE * 2 + CopyOpt.SIZE;
if (!variableDefs.ContainsKey(dest)) DefineVariable<uint>(dest);
return EmitCopy((uint)(Opt_SIZE + Size + relativeOffset), dest);
}
public PushOpt EmitPush(object parameter) =>
EmitPush(parameter is VariableName name ? name.key : GetConstant(parameter), null);
public PushOpt EmitPush(VariableName variableName) =>
EmitPush(variableName, null);
private PushOpt EmitPush(VariableName variableName, Type type, bool strict = false) {
DoTypeCheck(variableName, type, strict);
var Opt = new PushOpt(variableName);
Emit(Opt);
return Opt;
}
public JumpOpt EmitJump(UdonOpt dest) {
var opt = new JumpOpt(dest);
Emit(opt);
return opt;
}
public JumpOpt EmitJump(uint dest = ReturnAddress) {
var Opt = new JumpOpt(dest);
Emit(Opt);
return Opt;
}
public JumpIndirectOpt EmitJumpIndirect(VariableName address) {
DoTypeCheck(address, typeof(uint), true);
var Opt = new JumpIndirectOpt(address);
Emit(Opt);
return Opt;
}
public JumpIfFalseOpt EmitJumpIfFalse(UdonOpt dest) {
var Opt = new JumpIfFalseOpt(dest);
Emit(Opt);
return Opt;
}
public JumpIfFalseOpt EmitJumpIfFalse(uint dest = ReturnAddress) {
var Opt = new JumpIfFalseOpt(dest);
Emit(Opt);
return Opt;
}
public JumpIfFalseOpt EmitJumpIfFalse(VariableName paramName, UdonOpt dest) {
EmitPush(paramName, typeof(bool), true);
return EmitJumpIfFalse(dest);
}
public JumpIfFalseOpt EmitJumpIfFalse(VariableName paramName, uint dest = ReturnAddress) {
EmitPush(paramName, typeof(bool), true);
return EmitJumpIfFalse(dest);
}
private void DoTypeCheck(VariableName variableName, Type type, bool strict = false) {
if (!variableName.IsValid) throw new ArgumentNullException(nameof(variableName));
if (variableDefs.TryGetValue(variableName, out var def)) {
if (typeCheck && type != null && (strict ? def.type != type : !TypeHelper.IsTypeCompatable(def.type, type)))
throw new ArgumentException($"Type mismatch: expected variable `{variableName}` to be `{type}` but got `{def.type}`.");
} else DefineVariable(variableName, type);
}
public ExternOpt EmitExtern(MethodInfo method) =>
EmitExtern(method.GetUdonSignature());
public ExternOpt EmitExtern(string methodName) {
if (string.IsNullOrEmpty(methodName))
throw new ArgumentNullException(nameof(methodName));
if (typeCheck && UdonEditorManager.Instance.GetNodeDefinition(methodName) == null)
throw new ArgumentException($"Method `{methodName}` does not exists!");
uniqueExterns.Add(methodName);
var Opt = new ExternOpt(methodName);
Emit(Opt);
return Opt;
}
public ExternOpt EmitExtern(MethodInfo method, params object[] parameters) =>
EmitExtern(method.GetUdonSignature(), parameters);
public ExternOpt EmitExtern(string methodName, params object[] parameters) {
if (string.IsNullOrEmpty(methodName))
throw new ArgumentNullException(nameof(methodName));
if (typeCheck) {
var nodeDef = UdonEditorManager.Instance.GetNodeDefinition(methodName);
if (nodeDef == null)
throw new ArgumentException($"Method `{methodName}` does not exists!");
if (parameters == null ? nodeDef.parameters.Count != 0 : parameters.Length != nodeDef.parameters.Count)
throw new ArgumentException($"Invalid number of parameters for `{methodName}`: expected {nodeDef.parameters.Count} but got {parameters?.Length ?? 0}.");
if (parameters != null)
for (int i = 0; i < parameters.Length; i++) {
var parameter = parameters[i];
Type type;
VariableDefinition varDef;
if (parameter is VariableName name) {
if (variableDefs.TryGetValue(name, out varDef)) type = varDef.type;
else {
type = parameter?.GetType();
variableDefs.Add(name, varDef = new VariableDefinition(type, value: parameter));
}
} else {
name = GetConstant(parameter);
varDef = variableDefs[name];
type = varDef.type;
}
var paramDef = nodeDef.parameters[i];
switch (paramDef.parameterType) {
case UdonNodeParameter.ParameterType.IN:
if (!TypeHelper.IsTypeAssignable(paramDef.type, type))
throw new ArgumentException($"Type mismatch: Expected parameter {i} of `{methodName}` to be `{paramDef.type}` but got `{type}`.");
break;
case UdonNodeParameter.ParameterType.OUT:
if (varDef.attributes.HasFlag(VariableAttributes.Constant))
throw new ArgumentException($"Parameter {i} of `{methodName}` is an output but assigned a constant `{parameter}`.");
if (type == null) {
varDef.type = paramDef.type;
variableDefs[name] = varDef;
} else if (!TypeHelper.IsTypeCompatable(paramDef.type, type))
throw new ArgumentException($"Type mismatch: Expected parameter {i} of `{methodName}` to be `{paramDef.type}` but got `{type}`.");
break;
case UdonNodeParameter.ParameterType.IN_OUT:
if (varDef.attributes.HasFlag(VariableAttributes.Constant))
throw new ArgumentException($"Parameter {i} of `{methodName}` is a reference but assigned a constant `{parameter}`.");
if (type == null) {
varDef.type = paramDef.type;
variableDefs[name] = varDef;
} else if (type != paramDef.type)
throw new ArgumentException($"Type mismatch: Expected parameter {i} of `{methodName}` to be `{paramDef.type}` but got `{type}`.");
break;
}
parameters[i] = name;
}
}
if (parameters != null) foreach (var parameter in parameters) EmitPush(parameter);
return EmitExtern(methodName);
}
public string Compile() {
if (string.IsNullOrEmpty(assembly)) {
var sb = new StringBuilder();
sb.AppendLine(".data_start");
foreach (var kv in variableDefs) {
if (kv.Value.attributes.HasFlag(VariableAttributes.Public))
sb.AppendLine($".export {kv.Key}");
switch (kv.Value.attributes & VariableAttributes.Sync) {
case VariableAttributes.SyncNone:
sb.AppendLine($".sync {kv.Key}, none");
break;
case VariableAttributes.SyncLinear:
sb.AppendLine($".sync {kv.Key}, linear");
break;
case VariableAttributes.SyncSmooth:
sb.AppendLine($".sync {kv.Key}, smooth");
break;
}
}
foreach (var kv in variableDefs)
sb.AppendLine($"{kv.Key}: %{kv.Value.type.GetUdonSignature()}, {(kv.Value.attributes.HasFlag(VariableAttributes.DefaultThis) ? "this" : "null")}");
sb.AppendLine(".data_end");
sb.AppendLine(".code_start");
for (var opt = firstOpt; opt != null; opt = opt.next) {
if (entryPoints.TryGetValue(opt, out var exportName)) {
sb.AppendLine($".export {exportName}");
sb.AppendLine($"{exportName}:");
}
sb.AppendLine(opt.ToString());
}
sb.AppendLine(".code_end");
assembly = sb.ToString();
}
return assembly;
}
public IUdonProgram Assemble() {
Compile();
editorInterface = new UdonEditorInterface(
null, new HeapFactory((uint)(variableDefs.Count + uniqueExterns.Count)),
null, null, null, null, null, null, null
);
program = editorInterface.Assemble(assembly);
var symbolTable = program.SymbolTable;
var heap = program.Heap;
foreach (var kv in variableDefs)
if (!kv.Value.attributes.HasFlag(VariableAttributes.DefaultThis) && kv.Value.type != typeof(void))
heap.SetHeapVariable(symbolTable.GetAddressFromSymbol(kv.Key.key), kv.Value.value, kv.Value.type);
return program;
}
public string[] Disassemble() {
if (editorInterface == null || program == null) Assemble();
return editorInterface.DisassembleProgram(program);
}
}
public static class TypeHelper {
static readonly HashSet<Type> supportedTypes;
static readonly Dictionary<MemberInfo, string> udonSignatures;
static readonly Dictionary<VariableName, Type> predefinedVariableTypes;
static TypeHelper() {
supportedTypes = new HashSet<Type>();
predefinedVariableTypes = new Dictionary<VariableName, Type> {
[UdonBehaviour.ReturnVariableName] = typeof(object),
};
udonSignatures = new Dictionary<MemberInfo, string>();
foreach (var def in UdonEditorManager.Instance.GetNodeDefinitions()) {
if (def.fullName.StartsWith("Type_")) {
if (!def.fullName.EndsWith("Ref") && !udonSignatures.ContainsKey(def.type))
udonSignatures[def.type] = def.fullName.Substring(5);
supportedTypes.Add(def.type);
continue;
}
if (def.fullName.StartsWith("Event_")) {
var eventName = $"{char.ToLower(def.fullName[6])}{def.fullName.Substring(7)}";
foreach (var parameter in def.parameters)
if (parameter.parameterType == UdonNodeParameter.ParameterType.OUT)
predefinedVariableTypes[$"{eventName}{char.ToUpper(parameter.name[0])}{parameter.name.Substring(1)}"] = parameter.type;
continue;
}
}
}
public static Type GetUdonSupportedType(this Type type) {
if (type == null) return null;
if (supportedTypes.Contains(type)) return type;
bool isByRef = type.IsByRef, isArray = type.IsArray, isEnum = type.IsEnum;
if (isByRef || isArray) type = type.GetElementType();
else if (isEnum) type = Enum.GetUnderlyingType(type);
if (type.IsInterface) return null;
foreach (var subType in type.GetInterfaces()) {
var tryType = subType;
if (isByRef)
tryType = tryType.MakeByRefType();
else if (isArray)
tryType = tryType.MakeArrayType();
tryType = tryType.GetUdonSupportedType();
if (tryType != null) return tryType;
}
var parentType = type.BaseType;
if (parentType != null) {
if (isByRef)
parentType = parentType.MakeByRefType();
else if (isArray)
parentType = parentType.MakeArrayType();
parentType = parentType.GetUdonSupportedType();
if (parentType != null) return parentType;
}
return null;
}
public static string GetUdonSignature(this Type type) {
if (udonSignatures.TryGetValue(type, out var typeName)) return typeName;
if (type.IsByRef) {
typeName = type.GetElementType().GetUdonSignature();
if (!string.IsNullOrEmpty(typeName)) {
typeName += "Ref";
udonSignatures[type] = typeName;
}
return typeName;
}
if (type.IsArray) {
typeName = type.GetElementType().GetUdonSignature();
if (!string.IsNullOrEmpty(typeName)) {
typeName += "Array";
udonSignatures[type] = typeName;
}
return typeName;
}
return "SystemObject";
}
public static string GetUdonSignature(this MethodInfo method) {
if (method == null)
throw new ArgumentNullException(nameof(method));
if (!udonSignatures.TryGetValue(method, out var signature)) {
var sb = new StringBuilder();
sb.Append(method.DeclaringType.GetUdonSignature());
sb.Append(".__");
sb.Append(method.Name);
var parameters = method.GetParameters();
if (parameters.Length > 0) {
sb.Append('_');
foreach (var parameter in parameters)
sb.Append('_').Append(parameter.ParameterType.GetUdonSignature());
}
sb.Append("__");
sb.Append(method.ReturnType.GetUdonSignature());
udonSignatures[method] = signature = sb.ToString();
}
return signature;
}
public static bool TryGetPredefinedType(this VariableName varName, out Type type) =>
predefinedVariableTypes.TryGetValue(varName, out type) && type != null;
public static bool IsTypeAssignable(Type from, Type to) {
if (to == null || to == typeof(void)) return from == null || !from.IsValueType;
return from.IsAssignableFrom(to);
}
public static bool IsTypeCompatable(Type from, Type to) {
if (from == null || from == typeof(void)) return to == null || !to.IsValueType;
if (to == null || to == typeof(void)) return from == null || !from.IsValueType;
return from.IsAssignableFrom(to) || to.IsAssignableFrom(from);
}
}
public struct VariableName : IEquatable<VariableName> {
public readonly string key;
public bool IsValid => !string.IsNullOrEmpty(key);
public VariableName(string key) => this.key = key;
public bool Equals(VariableName other) => string.Equals(key, other.key);
public override bool Equals(object obj) => obj is VariableName other && Equals(other);
public override int GetHashCode() => key?.GetHashCode() ?? 0;
public override string ToString() => key ?? "";
public static implicit operator VariableName(string key) => new VariableName(key);
public VariableName ChangeEventOldVariable => VariableChangedEvent.OLD_VALUE_PREFIX + key;
}
[Flags]
public enum VariableAttributes {
None = 0x0,
Public = 0x1,
SyncNone = 0x2,
SyncLinear = 0x4,
SyncSmooth = 0x8,
Sync = SyncNone | SyncLinear | SyncSmooth,
DefaultThis = 0x10,
Constant = 0x20,
}
public struct VariableDefinition {
public VariableAttributes attributes;
public Type type;
public object value;
public VariableDefinition(Type type = null, VariableAttributes attributes = VariableAttributes.None, object value = null) {
this.type = type ?? value?.GetType() ?? typeof(object);
this.attributes = attributes;
this.value = value ?? (type != null && type != typeof(void) && type.IsValueType ? Activator.CreateInstance(type) : null);
}
}
public abstract class UdonOpt {
public abstract uint Size { get; }
internal UdonOpt next;
internal uint offset;
}
public class NopOpt : UdonOpt {
public const uint SIZE = 4U;
public override uint Size => SIZE;
public override string ToString() => "NOP";
}
public class PopOpt : UdonOpt {
public const uint SIZE = 4U;
public override uint Size => SIZE;
public override string ToString() => "POP";
}
public class CopyOpt : UdonOpt {
public const uint SIZE = 4U;
public override uint Size => SIZE;
public override string ToString() => "COPY";
}
public class PushOpt : UdonOpt {
public const uint SIZE = 8U;
public override uint Size => SIZE;
public VariableName dest;
public PushOpt(VariableName dest) => this.dest = dest;
public override string ToString() => $"PUSH, {dest}";
}
public abstract class JumpOptBase : UdonOpt {
public const uint SIZE = 8U;
public override uint Size => SIZE;
public UdonOpt destination;
protected uint destinationAddr;
protected JumpOptBase(UdonOpt destination) =>
this.destination = destination;
protected JumpOptBase(uint destinationAddr) =>
this.destinationAddr = destinationAddr;
public uint DestinationAddr => destination != null ? destination.offset + destination.Size : destinationAddr;
}
public class JumpOpt : JumpOptBase {
public JumpOpt(UdonOpt destination) : base(destination) {}
public JumpOpt(uint destinationAddr) : base(destinationAddr) {}
public override string ToString() => $"JUMP, 0x{DestinationAddr:X8}";
}
public class JumpIndirectOpt : UdonOpt {
public const uint SIZE = 8U;
public override uint Size => SIZE;
public readonly VariableName varName;
public JumpIndirectOpt(VariableName varName) => this.varName = varName;
public override string ToString() => $"JUMP_INDIRECT, {varName}";
}
public class JumpIfFalseOpt : JumpOpt {
public JumpIfFalseOpt(UdonOpt destination) : base(destination) {}
public JumpIfFalseOpt(uint destinationAddr) : base(destinationAddr) {}
public override string ToString() => $"JUMP_IF_FALSE, 0x{DestinationAddr:X8}";
}
public class ExternOpt : UdonOpt {
public const uint SIZE = 8U;
public override uint Size => SIZE;
public readonly string methodName;
public ExternOpt(string methodName) => this.methodName = methodName;
public override string ToString() => $"EXTERN, \"{methodName}\"";
}
internal class HeapFactory : IUdonHeapFactory {
public uint DefaultHeapSize { get; set; }
public HeapFactory(uint defaultHeapSize = 512U) => DefaultHeapSize = defaultHeapSize;
public IUdonHeap ConstructUdonHeap() => new UdonHeap(DefaultHeapSize);
IUdonHeap IUdonHeapFactory.ConstructUdonHeap(uint heapSize) => new UdonHeap(Math.Max(heapSize, DefaultHeapSize));
}
internal class UdonNodeDefinitionCompararer : IComparer<UdonNodeDefinition>, IEqualityComparer<UdonNodeDefinition> {
public static readonly UdonNodeDefinitionCompararer instance = new UdonNodeDefinitionCompararer();
public int Compare(UdonNodeDefinition lhs, UdonNodeDefinition rhs) {
if (lhs == rhs) return 0;
if (lhs.parameters.Count != rhs.parameters.Count) return 0;
int score = 0;
for (int i = lhs.parameters.Count - 1; i >= 0; i--) {
var lhsParam = lhs.parameters[i];
var rhsParam = rhs.parameters[i];
if (lhsParam.parameterType == UdonNodeParameter.ParameterType.OUT &&
rhsParam.parameterType == UdonNodeParameter.ParameterType.OUT)
continue;
var lhsType = lhsParam.type;
var rhsType = rhsParam.type;
if (lhsType == rhsType) continue;
if (lhsType.IsArray && rhsType.IsArray) {
score += lhsType.GetElementType().IsValueType ? rhsType.GetElementType().IsValueType ? 0 : -1 : 1;
continue;
}
if (lhsType.IsValueType || rhsType.IsValueType) {
score += lhsType.IsValueType ? rhsType.IsValueType ? 0 : -1 : 1;
continue;
}
if (TypeHelper.IsTypeAssignable(lhsType, rhsType)) {
while (rhsType != lhsType && rhsType != null) {
score++;
rhsType = rhsType.BaseType;
}
continue;
}
if (TypeHelper.IsTypeAssignable(rhsType, lhsType)) {
while (rhsType != lhsType && lhsType != null) {
score--;
lhsType = lhsType.BaseType;
}
continue;
}
}
return score > 0 ? 1 : score < 0 ? -1 : 0;
}
public bool Equals(UdonNodeDefinition lhs, UdonNodeDefinition rhs) {
if (lhs == null) return rhs != null;
if (rhs == null || lhs.parameters.Count != rhs.parameters.Count) return false;
for (int i = lhs.parameters.Count - 1; i >= 0; i--) {
var lhsParam = lhs.parameters[i];
var rhsParam = rhs.parameters[i];
if (lhsParam.parameterType != rhsParam.parameterType)
return false;
if (lhsParam.parameterType == UdonNodeParameter.ParameterType.OUT)
continue;
if (lhsParam.type != rhsParam.type)
return false;
}
return true;
}
public int GetHashCode(UdonNodeDefinition target) {
if (target == null) return 0;
int result = 0;
for (int i = target.parameters.Count - 1; i >= 0; i--) {
var parameter = target.parameters[i];
if (parameter.parameterType == UdonNodeParameter.ParameterType.OUT) continue;
result ^= parameter.type.GetHashCode();
}
return result;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment