[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:
Menci
2024-11-25 01:25:19 +08:00
committed by GitHub
parent e9ee31b22a
commit 37044dae01
217 changed files with 6051 additions and 3040 deletions

View File

@@ -0,0 +1,45 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{6B5E1F3E-D012-4CFB-A2FA-26A6CE06BE66}</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>AquaMai.Config.HeadlessLoader</RootNamespace>
<AssemblyName>AquaMai.Config.HeadlessLoader</AssemblyName>
<TargetFramework>netstandard2.0</TargetFramework>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<LangVersion>12</LangVersion>
<NoWarn>414;NU1702</NoWarn>
<LibsPath>$(ProjectDir)../Libs/</LibsPath>
<OutputPath>$(ProjectDir)../Output/</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<EnableDefaultEmbeddedResourceItems>false</EnableDefaultEmbeddedResourceItems>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>false</DebugSymbols>
<DebugType>None</DebugType>
<Optimize>true</Optimize>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DefineConstants>DEBUG</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../AquaMai.Config.Interfaces/AquaMai.Config.Interfaces.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Mono.Cecil">
<HintPath>$(LibsPath)Mono.Cecil.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Mono.Cecil;
namespace AquaMai.Config.HeadlessLoader;
class ConfigAssemblyLoader
{
public static Assembly LoadConfigAssembly(AssemblyDefinition assembly)
{
var references = assembly.MainModule.AssemblyReferences;
foreach (var reference in references)
{
if (reference.Name == "mscorlib" || reference.Name == "System" || reference.Name.StartsWith("System."))
{
reference.Name = "netstandard";
reference.Version = new Version(2, 0, 0, 0);
reference.PublicKeyToken = null;
}
}
var targetFrameworkAttribute = assembly.CustomAttributes.FirstOrDefault(attr => attr.AttributeType.Name == "TargetFrameworkAttribute");
if (targetFrameworkAttribute != null)
{
targetFrameworkAttribute.ConstructorArguments.Clear();
targetFrameworkAttribute.ConstructorArguments.Add(new CustomAttributeArgument(
assembly.MainModule.TypeSystem.String, ".NETStandard,Version=v2.0"));
targetFrameworkAttribute.Properties.Clear();
targetFrameworkAttribute.Properties.Add(new Mono.Cecil.CustomAttributeNamedArgument(
"FrameworkDisplayName", new CustomAttributeArgument(assembly.MainModule.TypeSystem.String, ".NET Standard 2.0")));
}
var stream = new MemoryStream();
assembly.Write(stream);
FixLoadedAssemblyResolution();
return AppDomain.CurrentDomain.Load(stream.ToArray());
}
private static bool FixedLoadedAssemblyResolution = false;
// XXX: Why, without this, the already loaded assemblies are not resolved?
public static void FixLoadedAssemblyResolution()
{
if (FixedLoadedAssemblyResolution)
{
return;
}
FixedLoadedAssemblyResolution = true;
var loadedAssemblies = new Dictionary<string, Assembly>();
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
loadedAssemblies[assembly.FullName] = assembly;
}
AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
{
if (loadedAssemblies.TryGetValue(args.Name, out var assembly))
{
return assembly;
}
return null;
};
}
}

View File

@@ -0,0 +1,11 @@
using Mono.Cecil;
namespace AquaMai.Config.HeadlessLoader;
public class CustomAssemblyResolver : DefaultAssemblyResolver
{
public new void RegisterAssembly(AssemblyDefinition assembly)
{
base.RegisterAssembly(assembly);
}
}

View File

@@ -0,0 +1,59 @@
using System;
using System.Reflection;
using AquaMai.Config.Interfaces;
using Mono.Cecil;
namespace AquaMai.Config.HeadlessLoader;
public class HeadlessConfigInterface
{
private readonly Assembly loadedConfigAssembly;
public IReflectionProvider ReflectionProvider { get; init; }
public IReflectionManager ReflectionManager { get; init; }
public HeadlessConfigInterface(Assembly loadedConfigAssembly, AssemblyDefinition modsAssembly)
{
this.loadedConfigAssembly = loadedConfigAssembly;
ReflectionProvider = Activator.CreateInstance(
loadedConfigAssembly.GetType("AquaMai.Config.Reflection.MonoCecilReflectionProvider"), [modsAssembly]) as IReflectionProvider;
ReflectionManager = Activator.CreateInstance(
loadedConfigAssembly.GetType("AquaMai.Config.Reflection.ReflectionManager"), [ReflectionProvider]) as IReflectionManager;
}
public IConfigView CreateConfigView(string tomlString = null)
{
return Activator.CreateInstance(
loadedConfigAssembly.GetType("AquaMai.Config.ConfigView"),
tomlString == null ? [] : [tomlString]) as IConfigView;
}
public IConfig CreateConfig()
{
return Activator.CreateInstance(
loadedConfigAssembly.GetType("AquaMai.Config.Config"), [ReflectionManager]) as IConfig;
}
public IConfigParser GetConfigParser()
{
return loadedConfigAssembly
.GetType("AquaMai.Config.ConfigParser")
.GetField("Instance", BindingFlags.Public | BindingFlags.Static)
.GetValue(null) as IConfigParser;
}
public IConfigSerializer CreateConfigSerializer(IConfigSerializer.Options options)
{
return Activator.CreateInstance(
loadedConfigAssembly.GetType("AquaMai.Config.ConfigSerializer"), [options]) as IConfigSerializer;
}
public IConfigMigrationManager GetConfigMigrationManager()
{
return loadedConfigAssembly
.GetType("AquaMai.Config.Migration.ConfigMigrationManager")
.GetField("Instance", BindingFlags.Public | BindingFlags.Static)
.GetValue(null) as IConfigMigrationManager;
}
}

View File

@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Mono.Cecil;
namespace AquaMai.Config.HeadlessLoader;
public class HeadlessConfigLoader
{
public static HeadlessConfigInterface LoadFromPacked(string fileName)
=> LoadFromPacked(new FileStream(fileName, FileMode.Open));
public static HeadlessConfigInterface LoadFromPacked(byte[] assemblyBinary)
=> LoadFromPacked(new MemoryStream(assemblyBinary));
public static HeadlessConfigInterface LoadFromPacked(Stream assemblyStream)
=> LoadFromPacked(AssemblyDefinition.ReadAssembly(assemblyStream));
public static HeadlessConfigInterface LoadFromPacked(AssemblyDefinition assembly)
{
return LoadFromUnpacked(
ResourceLoader.LoadEmbeddedAssemblies(assembly).Values);
}
public static HeadlessConfigInterface LoadFromUnpacked(IEnumerable<byte[]> assemblyBinariess) =>
LoadFromUnpacked(assemblyBinariess.Select(binary => new MemoryStream(binary)));
public static HeadlessConfigInterface LoadFromUnpacked(IEnumerable<Stream> assemblyStreams)
{
var resolver = new CustomAssemblyResolver();
var assemblies = assemblyStreams
.Select(
assemblyStream =>
AssemblyDefinition.ReadAssembly(
assemblyStream,
new ReaderParameters() {
AssemblyResolver = resolver
}))
.ToArray();
foreach (var assembly in assemblies)
{
resolver.RegisterAssembly(assembly);
}
var configAssembly = assemblies.First(assembly => assembly.Name.Name == "AquaMai.Config");
if (configAssembly == null)
{
throw new InvalidOperationException("AquaMai.Config assembly not found");
}
var loadedConfigAssembly = ConfigAssemblyLoader.LoadConfigAssembly(configAssembly);
var modsAssembly = assemblies.First(assembly => assembly.Name.Name == "AquaMai.Mods");
if (modsAssembly == null)
{
throw new InvalidOperationException("AquaMai.Mods assembly not found");
}
return new(loadedConfigAssembly, modsAssembly);
}
}

View File

@@ -0,0 +1,4 @@
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit {}
}

View File

@@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using Mono.Cecil;
namespace AquaMai.Config.HeadlessLoader;
public class ResourceLoader
{
private const string DLL_SUFFIX = ".dll";
private const string COMPRESSED_SUFFIX = ".compressed";
private const string DLL_COMPRESSED_SUFFIX = $"{DLL_SUFFIX}{COMPRESSED_SUFFIX}";
public static Dictionary<string, Stream> LoadEmbeddedAssemblies(AssemblyDefinition assembly)
{
return assembly.MainModule.Resources
.Where(resource => resource.Name.ToLower().EndsWith(DLL_SUFFIX) || resource.Name.ToLower().EndsWith(DLL_COMPRESSED_SUFFIX))
.Select(LoadResource)
.Where(data => data.Name != null)
.ToDictionary(data => data.Name, data => data.Stream);
}
public static (string Name, Stream Stream) LoadResource(Resource resource)
{
if (resource is EmbeddedResource embeddedResource)
{
if (resource.Name.ToLower().EndsWith(COMPRESSED_SUFFIX))
{
var decompressedStream = new MemoryStream();
using (var deflateStream = new DeflateStream(embeddedResource.GetResourceStream(), CompressionMode.Decompress))
{
deflateStream.CopyTo(decompressedStream);
}
decompressedStream.Position = 0;
return (resource.Name.Substring(0, resource.Name.Length - COMPRESSED_SUFFIX.Length), decompressedStream);
}
return (resource.Name, embeddedResource.GetResourceStream());
}
return (null, null);
}
}