Last active
April 19, 2021 03:44
-
-
Save mrtrizer/a7fbcca23b896bd2f1fb9e575724ebbb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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