Skip to content

Instantly share code, notes, and snippets.

@mrtrizer
Last active April 19, 2021 03:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mrtrizer/a7fbcca23b896bd2f1fb9e575724ebbb to your computer and use it in GitHub Desktop.
Save mrtrizer/a7fbcca23b896bd2f1fb9e575724ebbb to your computer and use it in GitHub Desktop.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Text.RegularExpressions;
public class AttributeManager : EditorWindow
{
static string[] files;
static System.Reflection.Assembly mainAssembly;
[MenuItem("Window/Attribute Manager")]
public static void ShowWindow()
{
EditorWindow.GetWindow(typeof(AttributeManager));
}
abstract class IMeta
{
abstract public int getId();
abstract public int getPos();
abstract public string getName();
}
class FieldMeta : IMeta
{
public override int getId()
{
return attribute != null ? attribute.id : -1;
}
public override int getPos()
{
return pos;
}
public override string getName()
{
return fieldInfo.Name;
}
public System.Reflection.FieldInfo fieldInfo;
public int pos;
public AutoSerializeAttribute attribute;
}
class MethodMeta : IMeta
{
public override int getId()
{
return attribute != null ? attribute.id : -1;
}
public override int getPos()
{
return pos;
}
public override string getName()
{
return methodInfo.Name;
}
public System.Reflection.MethodInfo methodInfo;
public int pos;
public MethodId attribute;
}
class ClassMeta
{
public string path;
public Type type;
public List<FieldMeta> fields;
public List<MethodMeta> methods;
}
class ComponentMeta
{
public ClassMeta classMeta;
public ComponentId attribute;
}
static List<ComponentMeta> components = new List<ComponentMeta>();
static Dictionary<Type, ClassMeta> serializableClasses = new Dictionary<Type, ClassMeta>();
static List<FieldMeta> extractFields(Type type, string content, Scope scope)
{
var fieldsMeta = new List<FieldMeta>();
var fields = type.GetFields(System.Reflection.BindingFlags.Public
| System.Reflection.BindingFlags.NonPublic
| System.Reflection.BindingFlags.Instance);
foreach (var field in fields)
{
var autoSerializeAttributes = field.GetCustomAttributes(typeof(AutoSerializeAttribute), false);
if (autoSerializeAttributes.Length == 1)
{
var attribute = autoSerializeAttributes[0] as AutoSerializeAttribute;
int pos = getPositionInScope(content, scope, "AutoSerialize", field.Name);
fieldsMeta.Add(new FieldMeta { fieldInfo = field, pos = pos, attribute = attribute });
}
}
return fieldsMeta;
}
static List<MethodMeta> extractMethods(Type type, string content, Scope scope)
{
var fieldsMeta = new List<MethodMeta>();
var methods = type.GetMethods(System.Reflection.BindingFlags.Public
| System.Reflection.BindingFlags.NonPublic
| System.Reflection.BindingFlags.Instance);
foreach (var method in methods)
{
var attributes = method.GetCustomAttributes(typeof(MethodId), false);
if (attributes.Length == 1)
{
var attribute = attributes[0] as MethodId;
int pos = getPositionInScope(content, scope, "MethodId", method.Name);
fieldsMeta.Add(new MethodMeta { methodInfo = method, pos = pos, attribute = attribute });
}
}
return fieldsMeta;
}
private static int getPositionInScope(string content, Scope scope, string attributeName, string name)
{
int start = scope.start;
int end;
int pos = -1;
foreach (var sub in scope.subScopes)
{
end = sub.start;
pos = getPosition(content, name, attributeName, start, end);
if (pos != -1)
break;
start = sub.end;
}
end = scope.end;
if (pos == -1)
pos = getPosition(content, name, attributeName, start, end);
return pos;
}
static int findComponentStartPoint(Type type, string content)
{
Regex rx = new Regex(string.Format(@"public\s*?(?:abstract|)(?:virtual|)\s*?class\s*?{0}", type.Name));
MatchCollection matches = rx.Matches(content);
if (matches.Count == 1)
{
return matches[0].Index;
}
return -1;
}
static int findScopeStartPoint(Type type, string content)
{
Regex rx = new Regex(string.Format(@"(?:struct|class)\s*?{0}", type.Name), RegexOptions.Singleline);
MatchCollection matches = rx.Matches(content);
if (matches.Count == 1)
{
int start = content.IndexOf('{', matches[0].Index);
return start;
}
return -1;
}
static ClassMeta findFileDefiningType(Type type)
{
foreach (var file in files)
{
var content = File.ReadAllText(file);
int startPoint = findScopeStartPoint(type, content);
if (startPoint != -1)
{
var scope = getScope(content, startPoint);
var fields = extractFields(type, content, scope);
return new ClassMeta { type = type, path = file, fields = fields };
}
}
return default;
}
static void printParseError(string content, Scope scope)
{
if (scope.subScopes.Count > 0)
{
printParseError(content, scope.subScopes[scope.subScopes.Count - 1]);
}
else
{
Debug.Log(content.Substring(scope.start));
}
}
static AttributeManager()
{
files = Directory.GetFiles(Directory.GetCurrentDirectory() + "/Assets/", "*.cs", SearchOption.AllDirectories);
Debug.Log("Length: " + files.Length);
foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies())
{
if (assembly.GetName().Name == "Assembly-CSharp")
{
mainAssembly = assembly;
}
}
var potentialSerializables = new HashSet<Type>();
foreach (var file in files)
{
var assetsDir = Directory.GetCurrentDirectory() + "\\Assets";
var dir = Path.GetDirectoryName(file);
if (!file.Contains("Assets/Assets") && dir != assetsDir)
continue;
var name = Path.GetFileNameWithoutExtension(file);
var type = mainAssembly.GetType(name);
if (!typeof(Component).IsAssignableFrom(type))
continue;
var content = File.ReadAllText(file);
int startPoint = findScopeStartPoint(type, content);
if (startPoint == -1)
{
Debug.LogError("Can't find component class scope " + type.Name);
continue;
}
var scope = getScope(content, startPoint);
if (!scope.valid)
{
Debug.LogError("Can't parse: " + file);
printParseError(content, scope);
continue;
}
var attributes = type.GetCustomAttributes(typeof(ComponentId), false);
if (attributes.Length > 1)
{
Debug.LogError("Multiple ComponentIds in " + type.Name);
continue;
}
var meta = new ComponentMeta { classMeta = new ClassMeta { path = file, type = type } };
meta.attribute = attributes.Length != 0 ? attributes[0] as ComponentId : null;
meta.classMeta.fields = extractFields(type, content, scope);
meta.classMeta.methods = extractMethods(type, content, scope);
var autoSerializeAttrib = type.GetCustomAttributes(typeof(AutoSerializeAttribute), false);
if (autoSerializeAttrib.Length == 0 && meta.classMeta.fields.Count == 0 && meta.classMeta.methods.Count == 0)
{
continue;
}
components.Add(meta);
foreach (var field in meta.classMeta.fields)
{
var fieldType = field.fieldInfo.FieldType;
if (typeof(IList).IsAssignableFrom(fieldType))
{
fieldType = fieldType.GetGenericArguments()[0];
}
if (!typeof(Component).IsAssignableFrom(fieldType) && !typeof(GameObject).IsAssignableFrom(fieldType))
{
var subFields = fieldType.GetFields(System.Reflection.BindingFlags.Public
| System.Reflection.BindingFlags.NonPublic
| System.Reflection.BindingFlags.Instance);
var autoSerializeFieldI = Array.FindIndex(subFields, item => item.GetCustomAttributes(typeof(AutoSerializeAttribute), true).Length > 0);
if (autoSerializeFieldI != -1)
{
potentialSerializables.Add(fieldType);
}
}
}
}
foreach (var fieldType in potentialSerializables)
{
//Debug.Log("Potential: " + fieldType.Name);
potentialSerializables.Add(fieldType);
serializableClasses[fieldType] = findFileDefiningType(fieldType);
}
}
class Scope
{
public int start;
public int end;
public bool valid { get => end != -1; }
public int count { get => end - start; }
public List<Scope> subScopes = new List<Scope>();
}
static Scope getScope(string content, int start)
{
bool lineComment = false;
bool blockComment = false;
bool strChar = false;
bool escape = false;
char strCharStart = ' ';
Scope scope = new Scope { start = start, end = -1 };
for (int i = start + 1; i < content.Length; i++)
{
if (blockComment)
{
if (content[i] == '/' && content[i - 1] == '*')
blockComment = false;
}
else if (lineComment)
{
if (content[i] == '\n')
lineComment = false;
}
else if (strChar)
{
if (escape)
{
escape = false;
}
else
{
if (content[i] == '\\')
escape = true;
if (content[i] == strCharStart)
strChar = false;
}
}
else if (content[i] == '"' || content[i] == '\'')
{
strChar = true;
strCharStart = content[i];
}
else if (i > 0 && content[i] == '/' && content[i - 1] == '/')
{
lineComment = true;
}
else if (i > 0 && content[i] == '*' && content[i - 1] == '/')
{
blockComment = true;
}
else if (!strChar && !lineComment && !blockComment)
{
if (content[i] == '{')
{
var subScope = getScope(content, i);
scope.subScopes.Add(subScope);
if (subScope.valid)
i = subScope.end;
else
break;
}
else if (content[i] == '}')
{
scope.end = i;
break;
}
}
}
return scope;
}
bool insertClassAttribute(string file, Type type, int id)
{
var content = File.ReadAllText(file);
int insertPoint = findComponentStartPoint(type, content);
if (insertPoint != -1)
{
string modifiedContent = content.Insert(insertPoint, $"[ComponentId({id})]\n");
File.WriteAllText(file, modifiedContent, new System.Text.UTF8Encoding(true));
string relativePath = file.Substring(file.IndexOf("Assets"));
AssetDatabase.ImportAsset(relativePath);
return true;
}
else
{
Debug.LogError("Parse error " + type.Name);
}
return false;
}
static int getPosition(string content, string fieldName, string attributeName, int start, int end)
{
Regex rx = new Regex(string.Format(@"\[{0}()\][^;]*?{1}[\s;=\(]", attributeName, fieldName), RegexOptions.Singleline);
MatchCollection matches = rx.Matches(content, start);
foreach (Match match in matches)
{
if (match.Index < end)
{
GroupCollection groups = match.Groups;
Debug.Log(groups[1].Index);
return groups[1].Index;
}
}
return -1;
}
int findMaxId<T>(List<T> items) where T:IMeta
{
int maxId = 0;
foreach (var item in items)
{
if (maxId < item.getId())
maxId = item.getId();
}
return maxId;
}
delegate bool IsDeclared(IMeta meta);
void insertItemAttributes<T>(string file, string content, Type type, List<T> items, IsDeclared idDeclared) where T : IMeta
{
int offset = 0;
string modifiedContent = content;
int maxId = findMaxId(items);
foreach (var item in items)
{
if (item.getId() == -1)
{
if (!idDeclared(item))
maxId++;
}
}
foreach (var item in items)
{
if (idDeclared(item))
{
if (item.getId() == -1)
{
maxId++;
var maxIdStr = "(" + maxId.ToString() + ")";
modifiedContent = modifiedContent.Insert(item.getPos() + offset, maxIdStr);
offset += maxIdStr.Length;
}
}
}
File.WriteAllText(file, modifiedContent, new System.Text.UTF8Encoding(true));
string relativePath = file.Substring(file.IndexOf("Assets"));
AssetDatabase.ImportAsset(relativePath);
}
void insertFieldAttributes(string file, ClassMeta classMeta)
{
var content = File.ReadAllText(file);
var declaredFields = classMeta.type.GetFields(System.Reflection.BindingFlags.Public
| System.Reflection.BindingFlags.NonPublic
| System.Reflection.BindingFlags.Instance
| System.Reflection.BindingFlags.DeclaredOnly);
int startPoint = findScopeStartPoint(classMeta.type, content);
var scope = getScope(content, startPoint);
if (!scope.valid)
{
Debug.LogError("Can't parse: " + file);
return;
}
var fields = extractFields(classMeta.type, content, scope);
IsDeclared isDeclared = items => {
return Array.FindIndex(declaredFields, i => i == (items as FieldMeta).fieldInfo) != -1;
};
insertItemAttributes(file, content, classMeta.type, fields, isDeclared);
}
void insertMethodAttributes(string file, ClassMeta classMeta)
{
var content = File.ReadAllText(file);
var declaredMethods = classMeta.type.GetMethods(System.Reflection.BindingFlags.Public
| System.Reflection.BindingFlags.NonPublic
| System.Reflection.BindingFlags.Instance
| System.Reflection.BindingFlags.DeclaredOnly);
int startPoint = findScopeStartPoint(classMeta.type, content);
var scope = getScope(content, startPoint);
if (!scope.valid)
{
Debug.LogError("Can't parse: " + file);
return;
}
var methods = extractMethods(classMeta.type, content, scope);
IsDeclared isDeclared = items => {
return Array.FindIndex(declaredMethods, i => i == (items as MethodMeta).methodInfo) != -1;
};
insertItemAttributes(file, content, classMeta.type, methods, isDeclared);
}
void validateFields(ClassMeta classMeta)
{
var fields = classMeta.fields;
foreach (var field in fields)
{
string error = fields.FindAll(item => item.attribute.id == field.attribute.id).Count > 1 ? "DUPLICATE" : "";
EditorGUILayout.LabelField($" {field.fieldInfo.Name} [{field.attribute.id}] {error}");
}
}
void validateMethods(ClassMeta classMeta)
{
foreach (var method in classMeta.methods)
{
string error = classMeta.methods.FindAll(item => item.attribute.id == method.attribute.id).Count > 1 ? "DUPLICATE" : "";
EditorGUILayout.LabelField($" {method.methodInfo.Name} () [{method.attribute.id}] {error}");
}
}
Vector2 scrollPosition;
private void OnGUI()
{
EditorGUILayout.LabelField("Total: " + components.Count);
scrollPosition = GUILayout.BeginScrollView(scrollPosition);
int maxId = 0;
foreach (var component in components)
{
if (component.attribute != null)
{
if (component.attribute.id > maxId)
maxId = component.attribute.id;
}
}
if (GUILayout.Button("Auto"))
{
maxId++;
foreach (var component in components)
{
try
{
if (component.attribute == null)
{
if (insertClassAttribute(component.classMeta.path, component.classMeta.type, maxId))
maxId++;
}
}
catch (System.Exception e)
{
Debug.LogException(e);
}
}
foreach (var component in components)
{
try
{
insertFieldAttributes(component.classMeta.path, component.classMeta);
}
catch (System.Exception e)
{
Debug.LogException(e);
}
}
foreach (var component in components)
{
try
{
insertMethodAttributes(component.classMeta.path, component.classMeta);
}
catch (System.Exception e)
{
Debug.LogException(e);
}
}
foreach (var classMeta in serializableClasses.Values)
{
try
{
insertFieldAttributes(classMeta.path, classMeta);
}
catch (System.Exception e)
{
Debug.LogException(e);
}
}
}
foreach (var component in components)
{
if (component.attribute != null)
{
var componentsWithId = components.FindAll(item => item.attribute != null && item.attribute.id == component.attribute.id);
string error = componentsWithId.Count > 1 ? $"DUPLICATE" : "";
int fieldMaxId = findMaxId(component.classMeta.fields);
EditorGUILayout.LabelField($"{component.classMeta.type.Name} [{component.attribute.id}] {error}");
EditorGUILayout.LabelField($"Fields MaxId: {fieldMaxId} ");
validateFields(component.classMeta);
int methodMaxId = findMaxId(component.classMeta.methods);
EditorGUILayout.LabelField($"Methods MaxId: {methodMaxId} ");
validateMethods(component.classMeta);
if (GUILayout.Button("Insert to fields"))
{
insertFieldAttributes(component.classMeta.path, component.classMeta);
}
if (GUILayout.Button("Insert to methods"))
{
insertMethodAttributes(component.classMeta.path, component.classMeta);
}
}
else
{
EditorGUILayout.LabelField(component.classMeta.type.Name);
if (GUILayout.Button("Insert to component"))
{
insertClassAttribute(component.classMeta.path, component.classMeta.type, maxId + 1);
}
}
}
EditorGUILayout.LabelField($"Serializable classes: ");
foreach (var classMeta in serializableClasses.Values)
{
if (classMeta == null)
continue;
EditorGUILayout.LabelField($"{classMeta.type.Name}");
validateFields(classMeta);
if (GUILayout.Button("Insert to fields"))
{
insertFieldAttributes(classMeta.path, classMeta);
}
}
GUILayout.EndScrollView();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment