mirror of
https://github.com/MewoLab/AquaDX.git
synced 2026-02-14 18:17:27 +08:00
[RF] AquaMai configuration refactor (#82)
更新了配置文件格式,原有的配置文件将被自动无缝迁移,详情请见新的配置文件中的注释(例外:`SlideJudgeTweak` 不再默认启用) 旧配置文件将被重命名备份,如果更新到此版本遇到 Bug 请联系我们 Updated configuration file schema. The old config file will be migrated automatically and seamlessly. See the comments in the new configuration file for details. (Except for `SlideJudgeTweak` is no longer enabled by default) Your old configuration file will be renamed as a backup. If you encounter any bug with this version, please contact us.
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Config.Interfaces;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
|
||||
namespace AquaMai.Config.Reflection;
|
||||
|
||||
public class MonoCecilReflectionProvider : IReflectionProvider
|
||||
{
|
||||
public record ReflectionField(
|
||||
string Name,
|
||||
Type FieldType,
|
||||
object Value,
|
||||
IDictionary<Type, object> Attributes) : IReflectionField
|
||||
{
|
||||
public object Value { get; set; } = Value;
|
||||
|
||||
public T GetCustomAttribute<T>() where T : Attribute => Attributes.TryGetValue(typeof(T), out var value) ? (T)value : null;
|
||||
public object GetValue(object obj) => Value;
|
||||
public void SetValue(object obj, object value) => Value = value;
|
||||
}
|
||||
|
||||
public record ReflectionType(
|
||||
string FullName,
|
||||
string Namespace,
|
||||
IReflectionField[] Fields,
|
||||
IDictionary<Type, object> Attributes) : IReflectionType
|
||||
{
|
||||
public T GetCustomAttribute<T>() where T : Attribute => Attributes.TryGetValue(typeof(T), out var value) ? (T)value : null;
|
||||
public IReflectionField[] GetFields(BindingFlags bindingAttr) => Fields;
|
||||
}
|
||||
|
||||
private static readonly Type[] attributeTypes =
|
||||
[
|
||||
typeof(ConfigCollapseNamespaceAttribute),
|
||||
typeof(ConfigSectionAttribute),
|
||||
typeof(ConfigEntryAttribute),
|
||||
];
|
||||
|
||||
private readonly IReflectionType[] reflectionTypes = [];
|
||||
private readonly Dictionary<string, Dictionary<string, object>> enums = [];
|
||||
|
||||
public IReflectionType[] GetTypes() => reflectionTypes;
|
||||
public Dictionary<string, object> GetEnum(string enumName) => enums[enumName];
|
||||
|
||||
public MonoCecilReflectionProvider(AssemblyDefinition assembly)
|
||||
{
|
||||
reflectionTypes = assembly.MainModule.Types.Select(cType => {
|
||||
var typeAttributes = InstantiateAttributes(cType.CustomAttributes);
|
||||
var fields = cType.Fields.Select(cField => {
|
||||
try
|
||||
{
|
||||
var fieldAttributes = InstantiateAttributes(cField.CustomAttributes);
|
||||
if (fieldAttributes.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var type = GetRuntimeType(cField.FieldType);
|
||||
var defaultValue = GetFieldDefaultValue(cType, cField, type);
|
||||
return new ReflectionField(cField.Name, type, defaultValue, fieldAttributes);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
return null;
|
||||
}).Where(field => field != null).ToArray();
|
||||
return new ReflectionType(cType.FullName, cType.Namespace, fields, typeAttributes);
|
||||
}).ToArray();
|
||||
enums = assembly.MainModule.Types
|
||||
.Where(cType => cType.IsEnum)
|
||||
.ToDictionary(cType =>
|
||||
cType.FullName,
|
||||
cType => cType.Fields
|
||||
.Where(cField => cField.IsPublic && cField.IsStatic && cField.Constant != null)
|
||||
.ToDictionary(cField => cField.Name, cField => cField.Constant));
|
||||
}
|
||||
|
||||
private Dictionary<Type, object> InstantiateAttributes(ICollection<CustomAttribute> attribute) =>
|
||||
attribute
|
||||
.Select(InstantiateAttribute)
|
||||
.Where(a => a != null)
|
||||
.ToDictionary(a => a.GetType(), a => a);
|
||||
|
||||
private object InstantiateAttribute(CustomAttribute attribute) =>
|
||||
attributeTypes.FirstOrDefault(t => t.FullName == attribute.AttributeType.FullName) switch
|
||||
{
|
||||
Type type => Activator.CreateInstance(type,
|
||||
attribute.Constructor.Parameters
|
||||
.Select((parameter, i) =>
|
||||
{
|
||||
var runtimeType = GetRuntimeType(parameter.ParameterType);
|
||||
var value = attribute.ConstructorArguments[i].Value;
|
||||
if (runtimeType.IsEnum)
|
||||
{
|
||||
return Enum.Parse(runtimeType, value.ToString());
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.ToArray()),
|
||||
_ => null
|
||||
};
|
||||
|
||||
private Type GetRuntimeType(TypeReference typeReference) {
|
||||
if (typeReference.IsGenericInstance)
|
||||
{
|
||||
var genericInstance = (GenericInstanceType)typeReference;
|
||||
var genericType = GetRuntimeType(genericInstance.ElementType);
|
||||
var genericArguments = genericInstance.GenericArguments.Select(GetRuntimeType).ToArray();
|
||||
return genericType.MakeGenericType(genericArguments);
|
||||
}
|
||||
|
||||
var type = Type.GetType(typeReference.FullName);
|
||||
if (type == null)
|
||||
{
|
||||
throw new TypeLoadException($"Type {typeReference.FullName} not found.");
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
private static object GetFieldDefaultValue(TypeDefinition cType, FieldDefinition cField, Type fieldType)
|
||||
{
|
||||
object defaultValue = null;
|
||||
var cctor = cType.Methods.SingleOrDefault(m => m.Name == ".cctor");
|
||||
if (cctor != null)
|
||||
{
|
||||
var store = cctor.Body.Instructions.SingleOrDefault(i => i.OpCode == OpCodes.Stsfld && i.Operand == cField);
|
||||
if (store != null)
|
||||
{
|
||||
var loadOperand = ParseConstantLoadOperand(store.Previous);
|
||||
if (fieldType == typeof(bool))
|
||||
{
|
||||
defaultValue = Convert.ToBoolean(loadOperand);
|
||||
}
|
||||
else
|
||||
{
|
||||
defaultValue = loadOperand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultValue == null && cField.HasDefault)
|
||||
{
|
||||
throw new InvalidOperationException($"Field {cType.FullName}.{cField.Name} has default value but no .cctor stsfld instruction.");
|
||||
}
|
||||
defaultValue ??= GetDefaultValue(fieldType);
|
||||
|
||||
if (fieldType.IsEnum)
|
||||
{
|
||||
var enumType = fieldType.GetEnumUnderlyingType();
|
||||
// Assume casting is safe since we're getting the default value from the field
|
||||
var castedValue = Convert.ChangeType(defaultValue, enumType);
|
||||
if (Enum.IsDefined(fieldType, castedValue))
|
||||
{
|
||||
return Enum.ToObject(fieldType, castedValue);
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private static object ParseConstantLoadOperand(Instruction instruction)
|
||||
{
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_M1)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_1)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_2)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_3)
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_4)
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_5)
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_6)
|
||||
{
|
||||
return 6;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_7)
|
||||
{
|
||||
return 7;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_8)
|
||||
{
|
||||
return 8;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_S)
|
||||
{
|
||||
return Convert.ToInt32((sbyte)instruction.Operand);
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4)
|
||||
{
|
||||
return (int)instruction.Operand;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I8)
|
||||
{
|
||||
return (long)instruction.Operand;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_R4)
|
||||
{
|
||||
return (float)instruction.Operand;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_R8)
|
||||
{
|
||||
return (double)instruction.Operand;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldstr)
|
||||
{
|
||||
return (string)instruction.Operand;
|
||||
}
|
||||
else
|
||||
{
|
||||
var currentMethod = MethodBase.GetCurrentMethod();
|
||||
throw new NotImplementedException($"Unsupported constant load: {instruction}. Please implement in {currentMethod.DeclaringType.FullName}.{currentMethod.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object GetDefaultValue(Type type)
|
||||
{
|
||||
if (type.IsValueType)
|
||||
{
|
||||
return Activator.CreateInstance(type);
|
||||
}
|
||||
else if (type == typeof(string))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
178
AquaMai/AquaMai.Config/Reflection/ReflectionManager.cs
Normal file
178
AquaMai/AquaMai.Config/Reflection/ReflectionManager.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
using System.Reflection;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Config.Interfaces;
|
||||
using System;
|
||||
|
||||
namespace AquaMai.Config.Reflection;
|
||||
|
||||
public class ReflectionManager : IReflectionManager
|
||||
{
|
||||
public record Entry : IReflectionManager.IEntry
|
||||
{
|
||||
public string Path { get; init; }
|
||||
public string Name { get; init; }
|
||||
public IReflectionField Field { get; init; }
|
||||
public ConfigEntryAttribute Attribute { get; init; }
|
||||
}
|
||||
|
||||
public record Section : IReflectionManager.ISection
|
||||
{
|
||||
public string Path { get; init; }
|
||||
public IReflectionType Type { get; init; }
|
||||
public ConfigSectionAttribute Attribute { get; init; }
|
||||
public List<Entry> entries;
|
||||
public List<IReflectionManager.IEntry> Entries => entries.Cast<IReflectionManager.IEntry>().ToList();
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, Section> sections = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, Entry> entries = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, Section> sectionsByFullName = [];
|
||||
|
||||
public ReflectionManager(IReflectionProvider reflectionProvider)
|
||||
{
|
||||
var prefix = "AquaMai.Mods.";
|
||||
var types = reflectionProvider.GetTypes().Where(t => t.FullName.StartsWith(prefix));
|
||||
var collapsedNamespaces = new HashSet<string>();
|
||||
foreach (var type in types)
|
||||
{
|
||||
var sectionAttribute = type.GetCustomAttribute<ConfigSectionAttribute>();
|
||||
if (sectionAttribute == null) continue;
|
||||
if (collapsedNamespaces.Contains(type.Namespace))
|
||||
{
|
||||
throw new Exception($"Collapsed namespace {type.Namespace} contains multiple sections");
|
||||
}
|
||||
var path = type.FullName.Substring(prefix.Length);
|
||||
if (type.GetCustomAttribute<ConfigCollapseNamespaceAttribute>() != null)
|
||||
{
|
||||
var separated = path.Split('.');
|
||||
if (separated[separated.Length - 2] != separated[separated.Length - 1])
|
||||
{
|
||||
throw new Exception($"Type {type.FullName} is not collapsable");
|
||||
}
|
||||
path = string.Join(".", separated.Take(separated.Length - 1));
|
||||
collapsedNamespaces.Add(type.Namespace);
|
||||
}
|
||||
|
||||
var sectionEntries = new List<Entry>();
|
||||
foreach (var field in type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
|
||||
{
|
||||
var entryAttribute = field.GetCustomAttribute<ConfigEntryAttribute>();
|
||||
if (entryAttribute == null) continue;
|
||||
var transformedName = Utility.ToPascalCase(field.Name);
|
||||
var entryPath = $"{path}.{transformedName}";
|
||||
var entry = new Entry()
|
||||
{
|
||||
Path = entryPath,
|
||||
Name = transformedName,
|
||||
Field = field,
|
||||
Attribute = entryAttribute
|
||||
};
|
||||
sectionEntries.Add(entry);
|
||||
entries.Add(entryPath, entry);
|
||||
}
|
||||
|
||||
var section = new Section()
|
||||
{
|
||||
Path = path,
|
||||
Type = type,
|
||||
Attribute = sectionAttribute,
|
||||
entries = sectionEntries
|
||||
};
|
||||
sections.Add(path, section);
|
||||
sectionsByFullName.Add(type.FullName, section);
|
||||
}
|
||||
|
||||
var order = reflectionProvider.GetEnum("AquaMai.Mods.SetionNameOrder");
|
||||
sections = sections
|
||||
.OrderBy(x => x.Key)
|
||||
.OrderBy(x =>
|
||||
{
|
||||
var parts = x.Key.Split('.');
|
||||
for (int i = parts.Length; i > 0; i--)
|
||||
{
|
||||
var key = string.Join("_", parts.Take(i));
|
||||
if (order.TryGetValue(key, out var value))
|
||||
{
|
||||
return (int)value;
|
||||
}
|
||||
}
|
||||
Utility.Log($"Section {x.Key} has no order defined, defaulting to int.MaxValue");
|
||||
return int.MaxValue;
|
||||
})
|
||||
.ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public IEnumerable<Section> SectionValues => sections.Values;
|
||||
public IEnumerable<IReflectionManager.ISection> Sections => sections.Values.Cast<IReflectionManager.ISection>();
|
||||
|
||||
public IEnumerable<Entry> EntryValues => entries.Values;
|
||||
public IEnumerable<IReflectionManager.IEntry> Entries => entries.Values.Cast<IReflectionManager.IEntry>();
|
||||
|
||||
public bool ContainsSection(string path)
|
||||
{
|
||||
return sections.ContainsKey(path);
|
||||
}
|
||||
|
||||
public bool TryGetSection(string path, out IReflectionManager.ISection section)
|
||||
{
|
||||
if (sections.TryGetValue(path, out var sectionValue))
|
||||
{
|
||||
section = sectionValue;
|
||||
return true;
|
||||
}
|
||||
section = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetSection(Type type, out IReflectionManager.ISection section)
|
||||
{
|
||||
bool result = sectionsByFullName.TryGetValue(type.FullName, out var sectionValue);
|
||||
section = sectionValue;
|
||||
return result;
|
||||
}
|
||||
|
||||
public IReflectionManager.ISection GetSection(string path)
|
||||
{
|
||||
if (!TryGetSection(path, out var section))
|
||||
{
|
||||
throw new KeyNotFoundException($"Section {path} not found");
|
||||
}
|
||||
return section;
|
||||
}
|
||||
|
||||
public IReflectionManager.ISection GetSection(Type type)
|
||||
{
|
||||
if (!TryGetSection(type.FullName, out var section))
|
||||
{
|
||||
throw new KeyNotFoundException($"Section {type.FullName} not found");
|
||||
}
|
||||
return section;
|
||||
}
|
||||
|
||||
public bool ContainsEntry(string path)
|
||||
{
|
||||
return entries.ContainsKey(path);
|
||||
}
|
||||
|
||||
public bool TryGetEntry(string path, out IReflectionManager.IEntry entry)
|
||||
{
|
||||
if (entries.TryGetValue(path, out var entryValue))
|
||||
{
|
||||
entry = entryValue;
|
||||
return true;
|
||||
}
|
||||
entry = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public IReflectionManager.IEntry GetEntry(string path)
|
||||
{
|
||||
if (!TryGetEntry(path, out var entry))
|
||||
{
|
||||
throw new KeyNotFoundException($"Entry {path} not found");
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using AquaMai.Config.Interfaces;
|
||||
|
||||
namespace AquaMai.Config.Reflection;
|
||||
|
||||
public class SystemReflectionProvider(Assembly assembly) : IReflectionProvider
|
||||
{
|
||||
public class ReflectionField(FieldInfo field) : IReflectionField
|
||||
{
|
||||
public FieldInfo UnderlyingField { get; } = field;
|
||||
|
||||
public string Name => UnderlyingField.Name;
|
||||
public Type FieldType => UnderlyingField.FieldType;
|
||||
public T GetCustomAttribute<T>() where T : Attribute => UnderlyingField.GetCustomAttribute<T>();
|
||||
public object GetValue(object obj) => UnderlyingField.GetValue(obj);
|
||||
public void SetValue(object obj, object value) => UnderlyingField.SetValue(obj, value);
|
||||
}
|
||||
|
||||
public class ReflectionType(Type type) : IReflectionType
|
||||
{
|
||||
public Type UnderlyingType { get; } = type;
|
||||
|
||||
public string FullName => UnderlyingType.FullName;
|
||||
public string Namespace => UnderlyingType.Namespace;
|
||||
public T GetCustomAttribute<T>() where T : Attribute => UnderlyingType.GetCustomAttribute<T>();
|
||||
public IReflectionField[] GetFields(BindingFlags bindingAttr) => Array.ConvertAll(UnderlyingType.GetFields(bindingAttr), f => new ReflectionField(f));
|
||||
}
|
||||
|
||||
public Assembly UnderlyingAssembly { get; } = assembly;
|
||||
|
||||
public IReflectionType[] GetTypes() => Array.ConvertAll(UnderlyingAssembly.GetTypes(), t => new ReflectionType(t));
|
||||
|
||||
public Dictionary<string, object> GetEnum(string enumName)
|
||||
{
|
||||
var enumType = UnderlyingAssembly.GetType(enumName);
|
||||
if (enumType == null) return null;
|
||||
var enumValues = Enum.GetValues(enumType);
|
||||
var enumDict = new Dictionary<string, object>();
|
||||
foreach (var enumValue in enumValues)
|
||||
{
|
||||
enumDict.Add(enumValue.ToString(), enumValue);
|
||||
}
|
||||
return enumDict;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user