mirror of https://github.com/hykilpikonna/AquaDX
[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.pull/89/head
parent
e9ee31b22a
commit
37044dae01
|
@ -372,6 +372,3 @@ MigrationBackup/
|
|||
|
||||
Output
|
||||
.idea
|
||||
Libs/Assembly-CSharp.dll
|
||||
Libs/AMDaemon.NET.dll
|
||||
packages
|
|
@ -0,0 +1,50 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{4C0C68C3-8B2E-4CA8-A26D-AE87CF2A38A5}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<RootNamespace>AquaMai.Build</RootNamespace>
|
||||
<AssemblyName>AquaMai.Build</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.HeadlessLoader/AquaMai.Config.HeadlessLoader.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Mono.Cecil">
|
||||
<HintPath>$(LibsPath)Mono.Cecil.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Build.Framework" Version="17.0.0" />
|
||||
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,42 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using AquaMai.Config.Interfaces;
|
||||
using AquaMai.Config.HeadlessLoader;
|
||||
using Microsoft.Build.Framework;
|
||||
using Microsoft.Build.Utilities;
|
||||
|
||||
public class GenerateExampleConfig : Task
|
||||
{
|
||||
[Required]
|
||||
public string DllPath { get; set; }
|
||||
|
||||
[Required]
|
||||
public string OutputPath { get; set; }
|
||||
|
||||
public override bool Execute()
|
||||
{
|
||||
try
|
||||
{
|
||||
var configInterface = HeadlessConfigLoader.LoadFromPacked(DllPath);
|
||||
var config = configInterface.CreateConfig();
|
||||
foreach (var lang in (string[]) ["en", "zh"])
|
||||
{
|
||||
var configSerializer = configInterface.CreateConfigSerializer(new IConfigSerializer.Options()
|
||||
{
|
||||
Lang = lang,
|
||||
IncludeBanner = true,
|
||||
OverrideLocaleValue = true
|
||||
});
|
||||
var example = configSerializer.Serialize(config);
|
||||
File.WriteAllText(Path.Combine(OutputPath, $"AquaMai.{lang}.toml"), example);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.LogErrorFromException(e, true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using Microsoft.Build.Framework;
|
||||
using Microsoft.Build.Utilities;
|
||||
using Mono.Cecil;
|
||||
|
||||
public class PostBuildPatch : Task
|
||||
{
|
||||
[Required]
|
||||
public string DllPath { get; set; }
|
||||
|
||||
public override bool Execute()
|
||||
{
|
||||
try
|
||||
{
|
||||
var assembly = AssemblyDefinition.ReadAssembly(new MemoryStream(File.ReadAllBytes(DllPath)));
|
||||
CompressEmbeddedAssemblies(assembly);
|
||||
var outputStream = new MemoryStream();
|
||||
assembly.Write(outputStream);
|
||||
File.WriteAllBytes(DllPath, outputStream.ToArray());
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.LogErrorFromException(e, true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void CompressEmbeddedAssemblies(AssemblyDefinition assembly)
|
||||
{
|
||||
foreach (var resource in assembly.MainModule.Resources.ToList())
|
||||
{
|
||||
if (resource.Name.EndsWith(".dll") && resource is EmbeddedResource embeddedResource)
|
||||
{
|
||||
using var compressedStream = new MemoryStream();
|
||||
using (var deflateStream = new DeflateStream(compressedStream, CompressionLevel.Optimal))
|
||||
{
|
||||
embeddedResource.GetResourceStream().CopyTo(deflateStream);
|
||||
}
|
||||
var compressedBytes = compressedStream.ToArray();
|
||||
|
||||
Log.LogMessage($"Compressed {resource.Name} from {embeddedResource.GetResourceStream().Length} to {compressedBytes.Length} bytes");
|
||||
|
||||
assembly.MainModule.Resources.Remove(resource);
|
||||
assembly.MainModule.Resources.Add(new EmbeddedResource(resource.Name + ".compressed", resource.Attributes, compressedBytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using Mono.Cecil;
|
||||
|
||||
namespace AquaMai.Config.HeadlessLoader;
|
||||
|
||||
public class CustomAssemblyResolver : DefaultAssemblyResolver
|
||||
{
|
||||
public new void RegisterAssembly(AssemblyDefinition assembly)
|
||||
{
|
||||
base.RegisterAssembly(assembly);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
internal static class IsExternalInit {}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{DF1536F9-3B06-4463-B654-4CC3E708B610}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<RootNamespace>AquaMai.Config.Interfaces</RootNamespace>
|
||||
<AssemblyName>AquaMai.Config.Interfaces</AssemblyName>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
<LangVersion>12</LangVersion>
|
||||
<NoWarn>414</NoWarn>
|
||||
<AssemblySearchPaths>$(ProjectDir)../Libs/;$(AssemblySearchPaths)</AssemblySearchPaths>
|
||||
<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>
|
||||
<Reference Include="mscorlib" />
|
||||
<Reference Include="System" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
|
||||
namespace AquaMai.Config.Interfaces;
|
||||
|
||||
public interface IConfig
|
||||
{
|
||||
public interface IEntryState
|
||||
{
|
||||
public bool IsDefault { get; set; }
|
||||
public object DefaultValue { get; }
|
||||
public object Value { get; set; }
|
||||
}
|
||||
|
||||
public interface ISectionState
|
||||
{
|
||||
public bool IsDefault { get; set; }
|
||||
public bool DefaultEnabled { get; }
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
|
||||
public IReflectionManager ReflectionManager { get; }
|
||||
|
||||
public ISectionState GetSectionState(IReflectionManager.ISection section);
|
||||
public ISectionState GetSectionState(Type type);
|
||||
public void SetSectionEnabled(IReflectionManager.ISection section, bool enabled);
|
||||
public IEntryState GetEntryState(IReflectionManager.IEntry entry);
|
||||
public void SetEntryValue(IReflectionManager.IEntry entry, object value);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
namespace AquaMai.Config.Interfaces;
|
||||
|
||||
public interface IConfigMigrationManager
|
||||
{
|
||||
public IConfigView Migrate(IConfigView config);
|
||||
public string GetVersion(IConfigView config);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
namespace AquaMai.Config.Interfaces;
|
||||
|
||||
public interface IConfigParser
|
||||
{
|
||||
public void Parse(IConfig config, string tomlString);
|
||||
public void Parse(IConfig config, IConfigView configView);
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
namespace AquaMai.Config.Interfaces;
|
||||
|
||||
public interface IConfigSerializer
|
||||
{
|
||||
public record Options
|
||||
{
|
||||
public string Lang { get; init; }
|
||||
public bool IncludeBanner { get; init; }
|
||||
public bool OverrideLocaleValue { get; init; }
|
||||
}
|
||||
|
||||
public string Serialize(IConfig config);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace AquaMai.Config.Interfaces;
|
||||
|
||||
public interface IConfigView
|
||||
{
|
||||
public void SetValue(string path, object value);
|
||||
public T GetValueOrDefault<T>(string path, T defaultValue = default);
|
||||
public bool TryGetValue<T>(string path, out T resultValue);
|
||||
public string ToToml();
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
|
||||
namespace AquaMai.Config.Interfaces;
|
||||
|
||||
public interface IReflectionManager
|
||||
{
|
||||
public interface IEntry
|
||||
{
|
||||
public string Path { get; }
|
||||
public string Name { get; }
|
||||
public IReflectionField Field { get; }
|
||||
}
|
||||
|
||||
public interface ISection
|
||||
{
|
||||
public string Path { get; }
|
||||
public IReflectionType Type { get; }
|
||||
public List<IEntry> Entries { get; }
|
||||
}
|
||||
|
||||
public IEnumerable<ISection> Sections { get; }
|
||||
|
||||
public IEnumerable<IEntry> Entries { get; }
|
||||
|
||||
public bool ContainsSection(string path);
|
||||
|
||||
public bool TryGetSection(string path, out ISection section);
|
||||
|
||||
public bool TryGetSection(Type type, out ISection section);
|
||||
|
||||
public ISection GetSection(string path);
|
||||
|
||||
public ISection GetSection(Type type);
|
||||
|
||||
public bool ContainsEntry(string path);
|
||||
|
||||
public bool TryGetEntry(string path, out IEntry entry);
|
||||
|
||||
public IEntry GetEntry(string path);
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace AquaMai.Config.Interfaces;
|
||||
|
||||
public interface IReflectionField
|
||||
{
|
||||
public string Name { get; }
|
||||
public Type FieldType { get; }
|
||||
|
||||
public T GetCustomAttribute<T>() where T : Attribute;
|
||||
public object GetValue(object objIsNull);
|
||||
public void SetValue(object objIsNull, object value);
|
||||
}
|
||||
|
||||
public interface IReflectionType
|
||||
{
|
||||
public string FullName { get; }
|
||||
public string Namespace { get; }
|
||||
|
||||
public T GetCustomAttribute<T>() where T : Attribute;
|
||||
public IReflectionField[] GetFields(BindingFlags bindingAttr);
|
||||
}
|
||||
|
||||
public interface IReflectionProvider
|
||||
{
|
||||
public IReflectionType[] GetTypes();
|
||||
public Dictionary<string, object> GetEnum(string enumName);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
internal static class IsExternalInit {}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{DF1536F9-3B06-4463-B654-4CC3E708B610}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<RootNamespace>AquaMai.Config</RootNamespace>
|
||||
<AssemblyName>AquaMai.Config</AssemblyName>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
<LangVersion>12</LangVersion>
|
||||
<NoWarn>414</NoWarn>
|
||||
<AssemblySearchPaths>$(ProjectDir)../Libs/;$(AssemblySearchPaths)</AssemblySearchPaths>
|
||||
<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="mscorlib" />
|
||||
<Reference Include="Mono.Cecil" />
|
||||
<Reference Include="System" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="FodyWeavers.xml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Fody" Version="6.8.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="ILMerge.Fody" Version="1.24.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Samboy063.Tomlet" Version="5.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,9 @@
|
|||
using System;
|
||||
|
||||
namespace AquaMai.Config.Attributes;
|
||||
|
||||
// When The most inner namespace is the same name of the class, it should be collapsed.
|
||||
// The class must be the only class in the namespace with a [ConfigSection] attribute.
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class ConfigCollapseNamespaceAttribute : Attribute
|
||||
{}
|
|
@ -0,0 +1,13 @@
|
|||
using System;
|
||||
|
||||
namespace AquaMai.Config.Attributes;
|
||||
|
||||
public record ConfigComment(string CommentEn, string CommentZh)
|
||||
{
|
||||
public string GetLocalized(string lang) => lang switch
|
||||
{
|
||||
"en" => CommentEn ?? "",
|
||||
"zh" => CommentZh ?? "",
|
||||
_ => throw new ArgumentException($"Unsupported language: {lang}")
|
||||
};
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
using System;
|
||||
|
||||
namespace AquaMai.Config.Attributes;
|
||||
|
||||
public enum SpecialConfigEntry
|
||||
{
|
||||
None,
|
||||
Locale
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class ConfigEntryAttribute(
|
||||
string en = null,
|
||||
string zh = null,
|
||||
// NOTE: Don't use this argument to hide any useful options.
|
||||
// Only use it to hide options that really won't be used.
|
||||
bool hideWhenDefault = false,
|
||||
// NOTE: Use this argument to mark special config entries that need special handling.
|
||||
SpecialConfigEntry specialConfigEntry = SpecialConfigEntry.None) : Attribute
|
||||
{
|
||||
public ConfigComment Comment { get; } = new ConfigComment(en, zh);
|
||||
public bool HideWhenDefault { get; } = hideWhenDefault;
|
||||
public SpecialConfigEntry SpecialConfigEntry { get; } = specialConfigEntry;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
|
||||
namespace AquaMai.Config.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class ConfigSectionAttribute(
|
||||
string en = null,
|
||||
string zh = null,
|
||||
// It will be hidden if the default value is preserved.
|
||||
bool exampleHidden = false,
|
||||
// A "Disabled = true" entry is required to disable the section.
|
||||
bool defaultOn = false,
|
||||
// NOTE: You probably shouldn't use this. Only the "General" section is using this.
|
||||
// Implies defaultOn = true.
|
||||
bool alwaysEnabled = false) : Attribute
|
||||
{
|
||||
public ConfigComment Comment { get; } = new ConfigComment(en, zh);
|
||||
public bool ExampleHidden { get; } = exampleHidden;
|
||||
public bool DefaultOn { get; } = defaultOn || alwaysEnabled;
|
||||
public bool AlwaysEnabled { get; } = alwaysEnabled;
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
using System;
|
||||
|
||||
namespace AquaMai.Config.Attributes;
|
||||
|
||||
public enum EnableConditionOperator
|
||||
{
|
||||
Equal,
|
||||
NotEqual,
|
||||
GreaterThan,
|
||||
LessThan,
|
||||
GreaterThanOrEqual,
|
||||
LessThanOrEqual
|
||||
}
|
||||
|
||||
public class EnableCondition(
|
||||
Type referenceType,
|
||||
string referenceMember,
|
||||
EnableConditionOperator @operator,
|
||||
object rightSideValue) : Attribute
|
||||
{
|
||||
public Type ReferenceType { get; } = referenceType;
|
||||
public string ReferenceMember { get; } = referenceMember;
|
||||
public EnableConditionOperator Operator { get; } = @operator;
|
||||
public object RightSideValue { get; } = rightSideValue;
|
||||
|
||||
// Referencing a field in another class and checking if it's true.
|
||||
public EnableCondition(Type referenceType, string referenceMember)
|
||||
: this(referenceType, referenceMember, EnableConditionOperator.Equal, true)
|
||||
{ }
|
||||
|
||||
// Referencing a field in the same class and comparing it with a value.
|
||||
public EnableCondition(string referenceMember, EnableConditionOperator condition, object value)
|
||||
: this(null, referenceMember, condition, value)
|
||||
{ }
|
||||
|
||||
// Referencing a field in the same class and checking if it's true.
|
||||
public EnableCondition(string referenceMember)
|
||||
: this(referenceMember, EnableConditionOperator.Equal, true)
|
||||
{ }
|
||||
|
||||
public bool Evaluate(Type selfType)
|
||||
{
|
||||
var referenceType = ReferenceType ?? selfType;
|
||||
var referenceField = referenceType.GetField(
|
||||
ReferenceMember,
|
||||
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
|
||||
var referenceProperty = referenceType.GetProperty(
|
||||
ReferenceMember,
|
||||
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
|
||||
if (referenceField == null && referenceProperty == null)
|
||||
{
|
||||
throw new ArgumentException($"Field or property {ReferenceMember} not found in {referenceType.FullName}");
|
||||
}
|
||||
var referenceMemberValue = referenceField != null ? referenceField.GetValue(null) : referenceProperty.GetValue(null);
|
||||
switch (Operator)
|
||||
{
|
||||
case EnableConditionOperator.Equal:
|
||||
return referenceMemberValue.Equals(RightSideValue);
|
||||
case EnableConditionOperator.NotEqual:
|
||||
return !referenceMemberValue.Equals(RightSideValue);
|
||||
case EnableConditionOperator.GreaterThan:
|
||||
case EnableConditionOperator.LessThan:
|
||||
case EnableConditionOperator.GreaterThanOrEqual:
|
||||
case EnableConditionOperator.LessThanOrEqual:
|
||||
var comparison = (IComparable)referenceMemberValue;
|
||||
return Operator switch
|
||||
{
|
||||
EnableConditionOperator.GreaterThan => comparison.CompareTo(RightSideValue) > 0,
|
||||
EnableConditionOperator.LessThan => comparison.CompareTo(RightSideValue) < 0,
|
||||
EnableConditionOperator.GreaterThanOrEqual => comparison.CompareTo(RightSideValue) >= 0,
|
||||
EnableConditionOperator.LessThanOrEqual => comparison.CompareTo(RightSideValue) <= 0,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AquaMai.Config.Interfaces;
|
||||
using AquaMai.Config.Reflection;
|
||||
|
||||
namespace AquaMai.Config;
|
||||
|
||||
public class Config : IConfig
|
||||
{
|
||||
// NOTE: If a section's state is default, all underlying entries' states are default as well.
|
||||
|
||||
public record SectionState : IConfig.ISectionState
|
||||
{
|
||||
public bool IsDefault { get; set; }
|
||||
public bool DefaultEnabled { get; init; }
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
|
||||
public record EntryState : IConfig.IEntryState
|
||||
{
|
||||
public bool IsDefault { get; set; }
|
||||
public object DefaultValue { get; init; }
|
||||
public object Value { get; set; }
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, SectionState> sections = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, EntryState> entries = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public readonly ReflectionManager reflectionManager;
|
||||
public IReflectionManager ReflectionManager => reflectionManager;
|
||||
|
||||
public Config(ReflectionManager reflectionManager)
|
||||
{
|
||||
this.reflectionManager = reflectionManager;
|
||||
|
||||
foreach (var section in reflectionManager.SectionValues)
|
||||
{
|
||||
InitializeSection(section);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeSection(ReflectionManager.Section section)
|
||||
{
|
||||
sections.Add(section.Path, new SectionState()
|
||||
{
|
||||
IsDefault = true,
|
||||
DefaultEnabled = section.Attribute.DefaultOn,
|
||||
Enabled = section.Attribute.DefaultOn
|
||||
});
|
||||
|
||||
foreach (var entry in section.Entries)
|
||||
{
|
||||
var defaultValue = entry.Field.GetValue(null);
|
||||
if (defaultValue == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Null default value for entry {entry.Path} is not allowed.");
|
||||
}
|
||||
entries.Add(entry.Path, new EntryState()
|
||||
{
|
||||
IsDefault = true,
|
||||
DefaultValue = defaultValue,
|
||||
Value = defaultValue
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public IConfig.ISectionState GetSectionState(IReflectionManager.ISection section)
|
||||
{
|
||||
return sections[section.Path];
|
||||
}
|
||||
|
||||
public IConfig.ISectionState GetSectionState(Type type)
|
||||
{
|
||||
if (!ReflectionManager.TryGetSection(type, out var section))
|
||||
{
|
||||
throw new ArgumentException($"Type {type.FullName} is not a config section.");
|
||||
}
|
||||
return sections[section.Path];
|
||||
}
|
||||
|
||||
public void SetSectionEnabled(IReflectionManager.ISection section, bool enabled)
|
||||
{
|
||||
sections[section.Path].IsDefault = false;
|
||||
sections[section.Path].Enabled = enabled;
|
||||
}
|
||||
|
||||
public IConfig.IEntryState GetEntryState(IReflectionManager.IEntry entry)
|
||||
{
|
||||
return entries[entry.Path];
|
||||
}
|
||||
|
||||
public void SetEntryValue(IReflectionManager.IEntry entry, object value)
|
||||
{
|
||||
entry.Field.SetValue(null, value);
|
||||
entries[entry.Path].IsDefault = false;
|
||||
entries[entry.Path].Value = value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
using System;
|
||||
using Tomlet.Models;
|
||||
using AquaMai.Config.Interfaces;
|
||||
using AquaMai.Config.Reflection;
|
||||
using AquaMai.Config.Migration;
|
||||
using System.Linq;
|
||||
|
||||
namespace AquaMai.Config;
|
||||
|
||||
public class ConfigParser : IConfigParser
|
||||
{
|
||||
public readonly static ConfigParser Instance = new();
|
||||
|
||||
private readonly static string[] supressUnrecognizedConfigPaths = ["Version"];
|
||||
private readonly static string[] supressUnrecognizedConfigPathSuffixes = [
|
||||
".Disabled", // For section enable state.
|
||||
".Disable", // For section enable state, but the wrong key, warn later.
|
||||
".Enabled", // For section enable state, but the wrong key, warn later.
|
||||
".Enable", // For section enable state, but the wrong key, warn later.
|
||||
];
|
||||
|
||||
private ConfigParser()
|
||||
{}
|
||||
|
||||
public void Parse(IConfig config, string tomlString)
|
||||
{
|
||||
var configView = new ConfigView(tomlString);
|
||||
Parse(config, configView);
|
||||
}
|
||||
|
||||
public void Parse(IConfig config, IConfigView configView)
|
||||
{
|
||||
var configVersion = ConfigMigrationManager.Instance.GetVersion(configView);
|
||||
if (configVersion != ConfigMigrationManager.Instance.latestVersion)
|
||||
{
|
||||
throw new InvalidOperationException($"Config version mismatch: expected {ConfigMigrationManager.Instance.latestVersion}, got {configVersion}");
|
||||
}
|
||||
Hydrate((Config)config, ((ConfigView)configView).root, "");
|
||||
}
|
||||
|
||||
private static void Hydrate(Config config, TomlValue value, string path)
|
||||
{
|
||||
if (config.ReflectionManager.TryGetSection(path, out var section))
|
||||
{
|
||||
ParseSectionEnableState(config, (ReflectionManager.Section)section, value, path);
|
||||
}
|
||||
|
||||
if (value is TomlTable table)
|
||||
{
|
||||
bool isLeaf = true;
|
||||
foreach (var subKey in table.Keys)
|
||||
{
|
||||
var subValue = table.GetValue(subKey);
|
||||
var subPath = path == "" ? subKey : $"{path}.{subKey}";
|
||||
if (subValue is TomlTable)
|
||||
{
|
||||
isLeaf = false;
|
||||
}
|
||||
Hydrate(config, subValue, subPath);
|
||||
}
|
||||
// A leaf dictionary, which has no child dictionaries, must be a section.
|
||||
if (isLeaf && section == null)
|
||||
{
|
||||
Utility.Log($"Unrecognized config section: {path}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// It's an config entry value (or a primitive type for enabling a section).
|
||||
if (!config.ReflectionManager.ContainsSection(path) &&
|
||||
!config.ReflectionManager.ContainsEntry(path) &&
|
||||
!supressUnrecognizedConfigPaths.Any(s => path.Equals(s, StringComparison.OrdinalIgnoreCase)) &&
|
||||
!supressUnrecognizedConfigPathSuffixes.Any(suffix => path.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
Utility.Log($"Unrecognized config entry: {path}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.ReflectionManager.TryGetEntry(path, out var entry))
|
||||
{
|
||||
try
|
||||
{
|
||||
var parsedValue = Utility.ParseTomlValue(entry.Field.FieldType, value);
|
||||
config.SetEntryValue(entry, parsedValue);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Utility.Log($"Error hydrating config ({path} = {value.StringValue}): {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ParseSectionEnableState(
|
||||
Config config,
|
||||
ReflectionManager.Section section,
|
||||
TomlValue value,
|
||||
string path)
|
||||
{
|
||||
if (value is TomlTable table)
|
||||
{
|
||||
foreach (var unexpectedKey in (string[]) ["Enable", "Enabled", "Disable"])
|
||||
{
|
||||
if (Utility.TomlContainsKeyCaseInsensitive(table, unexpectedKey))
|
||||
{
|
||||
Utility.Log($"Unexpected key \"{unexpectedKey}\" for enable status under \"{path}\". Only \"Disabled\" is parsed.");
|
||||
}
|
||||
}
|
||||
|
||||
if (Utility.TomlTryGetValueCaseInsensitive(table, "Disabled", out var disableValue) && !section.Attribute.AlwaysEnabled)
|
||||
{
|
||||
var disabled = Utility.IsTruty(disableValue, path + ".Disabled");
|
||||
config.SetSectionEnabled(section, !disabled);
|
||||
}
|
||||
else
|
||||
{
|
||||
config.SetSectionEnabled(section, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
config.SetSectionEnabled(section, Utility.IsTruty(value, path));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
using System;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Config.Interfaces;
|
||||
using Tomlet.Models;
|
||||
|
||||
namespace AquaMai.Config;
|
||||
|
||||
public class ConfigSerializer(IConfigSerializer.Options Options) : IConfigSerializer
|
||||
{
|
||||
private const string BANNER_ZH =
|
||||
"""
|
||||
这是 AquaMai 的 TOML 配置文件
|
||||
|
||||
- 井号 # 开头的行为注释,被注释掉的内容不会生效
|
||||
- 为方便使用 VSCode 等编辑器进行编辑,被注释掉的配置内容使用一个井号 #,而注释文本使用两个井号 ##
|
||||
- 以方括号包裹的行,如 [OptionalCategory.Section],为一个栏目
|
||||
- 将默认被注释(即默认禁用)的栏目取消注释即可启用
|
||||
- 若要禁用一个默认启用的栏目,请在栏目下添加「Disabled = true」配置项,删除它/注释它不会有效
|
||||
- 形如「键 = 值」为一个配置项
|
||||
- 配置项应用到其上方最近的栏目,请不要在一个栏目被注释掉的情况下开启其配置项(会加到上一个栏目,无效)
|
||||
- 当对应栏目启用时,配置项生效,无论是否将其取消注释
|
||||
- 注释掉的配置项保留其注释中的默认值,默认值可能会随版本更新而变化
|
||||
- 该文件的格式和文字注释是固定的,配置文件将在启动时被重写,无法解析的内容将被删除
|
||||
|
||||
试试使用 MaiChartManager 图形化配置 AquaMai 吧!
|
||||
https://github.com/clansty/MaiChartManager
|
||||
""";
|
||||
|
||||
private const string BANNER_EN =
|
||||
"""
|
||||
This is the TOML configuration file of AquaMai.
|
||||
|
||||
- Lines starting with a hash # are comments. Commented content will not take effect.
|
||||
- For easily editing with editors (e.g. VSCode), commented configuration content uses a single hash #, while comment text uses two hashes ##.
|
||||
- Lines with square brackets like [OptionalCategory.Section] are sections.
|
||||
- Uncomment a section that is commented out by default (i.e. disabled by default) to enable it.
|
||||
- To disable a section that is enabled by default, add a "Disable = true" entry under the section. Removing/commenting it will not work.
|
||||
- Lines like "Key = Value" is a configuration entry.
|
||||
- Configuration entries apply to the nearest section above them. Do not enable a configuration entry when its section is commented out (will be added to the previous section, which is invalid).
|
||||
- Configuration entries take effect when the corresponding section is enabled, regardless of whether they are uncommented.
|
||||
- Commented configuration entries retain their default values (shown in the comment), which may change with version updates.
|
||||
- The format and text comments of this file are fixed. The configuration file will be rewritten at startup, and unrecognizable content will be deleted.
|
||||
""";
|
||||
|
||||
private readonly IConfigSerializer.Options Options = Options;
|
||||
|
||||
public string Serialize(IConfig config)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
if (Options.IncludeBanner)
|
||||
{
|
||||
var banner = Options.Lang == "zh" ? BANNER_ZH : BANNER_EN;
|
||||
if (banner != null)
|
||||
{
|
||||
AppendComment(sb, banner.TrimEnd());
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
// Version
|
||||
AppendEntry(sb, null, "Version", "2.0");
|
||||
|
||||
foreach (var section in ((Config)config).reflectionManager.SectionValues)
|
||||
{
|
||||
var sectionState = config.GetSectionState(section);
|
||||
|
||||
// If the state is default, print the example only. If the example is hidden, skip it.
|
||||
if (sectionState.IsDefault && section.Attribute.ExampleHidden)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
AppendComment(sb, section.Attribute.Comment);
|
||||
|
||||
if (// If the section is hidden and hidden by default, print it as commented.
|
||||
sectionState.IsDefault && !sectionState.Enabled &&
|
||||
// If the section is marked as always enabled, print it normally.
|
||||
!section.Attribute.AlwaysEnabled)
|
||||
{
|
||||
sb.AppendLine($"#[{section.Path}]");
|
||||
}
|
||||
else // If the section is overridden, or is enabled by any means, print it normally.
|
||||
{
|
||||
sb.AppendLine($"[{section.Path}]");
|
||||
}
|
||||
|
||||
if (!section.Attribute.AlwaysEnabled)
|
||||
{
|
||||
// If the section is disabled explicitly, print the "Disabled" meta entry.
|
||||
if (!sectionState.IsDefault && !sectionState.Enabled)
|
||||
{
|
||||
AppendEntry(sb, null, "Disabled", value: true);
|
||||
}
|
||||
// If the section is enabled by default, print the "Disabled" meta entry as commented.
|
||||
else if (sectionState.IsDefault && section.Attribute.DefaultOn)
|
||||
{
|
||||
AppendEntry(sb, null, "Disabled", value: false, commented: true);
|
||||
}
|
||||
// Otherwise, don't print the "Disabled" meta entry.
|
||||
}
|
||||
|
||||
// Print entries.
|
||||
foreach (var entry in section.entries)
|
||||
{
|
||||
var entryState = config.GetEntryState(entry);
|
||||
AppendComment(sb, entry.Attribute.Comment);
|
||||
if (entry.Attribute.SpecialConfigEntry == SpecialConfigEntry.Locale && Options.OverrideLocaleValue)
|
||||
{
|
||||
AppendEntry(sb, section.Path, entry.Name, Options.Lang);
|
||||
}
|
||||
else
|
||||
{
|
||||
AppendEntry(sb, section.Path, entry.Name, entryState.Value, entryState.IsDefault);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string SerializeTomlValue(string diagnosticPath, object value)
|
||||
{
|
||||
var type = value.GetType();
|
||||
if (value is bool b)
|
||||
{
|
||||
return b ? "true" : "false";
|
||||
}
|
||||
else if (value is string str)
|
||||
{
|
||||
return new TomlString(str).SerializedValue;
|
||||
}
|
||||
else if (type.IsEnum)
|
||||
{
|
||||
return new TomlString(value.ToString()).SerializedValue;
|
||||
}
|
||||
else if (Utility.IsIntegerType(type))
|
||||
{
|
||||
return value.ToString();
|
||||
}
|
||||
else if (Utility.IsFloatType(type))
|
||||
{
|
||||
return new TomlDouble(Convert.ToDouble(value)).SerializedValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
var currentMethod = MethodBase.GetCurrentMethod();
|
||||
throw new NotImplementedException($"Unsupported config entry type: {type.FullName} ({diagnosticPath}). Please implement in {currentMethod.DeclaringType.FullName}.{currentMethod.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendComment(StringBuilder sb, ConfigComment comment)
|
||||
{
|
||||
if (comment != null)
|
||||
{
|
||||
AppendComment(sb, comment.GetLocalized(Options.Lang));
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendComment(StringBuilder sb, string comment)
|
||||
{
|
||||
comment = comment.Trim();
|
||||
if (!string.IsNullOrEmpty(comment))
|
||||
{
|
||||
foreach (var line in comment.Split('\n'))
|
||||
{
|
||||
sb.AppendLine($"## {line}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendEntry(StringBuilder sb, string diagnosticsSection, string key, object value, bool commented = false)
|
||||
{
|
||||
if (commented)
|
||||
{
|
||||
sb.Append('#');
|
||||
}
|
||||
var diagnosticsPath = string.IsNullOrEmpty(diagnosticsSection)
|
||||
? key
|
||||
: $"{diagnosticsSection}.{key}";
|
||||
sb.AppendLine($"{key} = {SerializeTomlValue(diagnosticsPath, value)}");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using AquaMai.Config.Interfaces;
|
||||
using Tomlet;
|
||||
using Tomlet.Models;
|
||||
|
||||
namespace AquaMai.Config;
|
||||
|
||||
public class ConfigView : IConfigView
|
||||
{
|
||||
public readonly TomlTable root;
|
||||
|
||||
public ConfigView()
|
||||
{
|
||||
root = new TomlTable();
|
||||
}
|
||||
|
||||
public ConfigView(TomlTable root)
|
||||
{
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
public ConfigView(string tomlString)
|
||||
{
|
||||
var tomlValue = new TomlParser().Parse(tomlString);
|
||||
if (tomlValue is not TomlTable tomlTable)
|
||||
{
|
||||
throw new ArgumentException($"Invalid TOML, expected a table, got: {tomlValue.GetType()}");
|
||||
}
|
||||
root = tomlTable;
|
||||
}
|
||||
|
||||
public TomlTable EnsureDictionary(string path)
|
||||
{
|
||||
var pathComponents = path.Split('.');
|
||||
var current = root;
|
||||
foreach (var component in pathComponents)
|
||||
{
|
||||
if (!current.TryGetValue(component, out var next))
|
||||
{
|
||||
next = new TomlTable();
|
||||
current.Put(component, next);
|
||||
}
|
||||
current = (TomlTable)next;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
public void SetValue(string path, object value)
|
||||
{
|
||||
var pathComponents = path.Split('.');
|
||||
var current = root;
|
||||
foreach (var component in pathComponents.Take(pathComponents.Length - 1))
|
||||
{
|
||||
if (!current.TryGetValue(component, out var next))
|
||||
{
|
||||
next = new TomlTable();
|
||||
current.Put(component, next);
|
||||
}
|
||||
current = (TomlTable)next;
|
||||
}
|
||||
current.Put(pathComponents.Last(), value);
|
||||
}
|
||||
|
||||
public T GetValueOrDefault<T>(string path, T defaultValue = default)
|
||||
{
|
||||
return TryGetValue(path, out T resultValue) ? resultValue : defaultValue;
|
||||
}
|
||||
|
||||
public bool TryGetValue<T>(string path, out T resultValue)
|
||||
{
|
||||
var pathComponents = path.Split('.');
|
||||
var current = root;
|
||||
foreach (var component in pathComponents.Take(pathComponents.Length - 1))
|
||||
{
|
||||
if (!Utility.TomlTryGetValueCaseInsensitive(current, component, out var next) || next is not TomlTable nextTable)
|
||||
{
|
||||
resultValue = default;
|
||||
return false;
|
||||
}
|
||||
current = nextTable;
|
||||
}
|
||||
if (!Utility.TomlTryGetValueCaseInsensitive(current, pathComponents.Last(), out var value))
|
||||
{
|
||||
resultValue = default;
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
resultValue = Utility.ParseTomlValue<T>(value);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Utility.Log($"Failed to parse value at {path}: {e.Message}");
|
||||
resultValue = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public string ToToml()
|
||||
{
|
||||
return root.SerializedValue;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ILMerge>
|
||||
<IncludeAssemblies>tomlet|urgui|numerics</IncludeAssemblies>
|
||||
<NamespacePrefix>$AquaMai$_</NamespacePrefix>
|
||||
<IncludeAssemblies>tomlet</IncludeAssemblies>
|
||||
<NamespacePrefix>$AquaMai.Config$_</NamespacePrefix>
|
||||
</ILMerge>
|
||||
</Weavers>
|
|
@ -0,0 +1,58 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AquaMai.Config.Interfaces;
|
||||
|
||||
namespace AquaMai.Config.Migration;
|
||||
|
||||
public class ConfigMigrationManager : IConfigMigrationManager
|
||||
{
|
||||
public static readonly ConfigMigrationManager Instance = new();
|
||||
|
||||
private readonly Dictionary<string, IConfigMigration> migrationMap =
|
||||
new List<IConfigMigration>
|
||||
{
|
||||
new ConfigMigration_V1_0_V2_0()
|
||||
}.ToDictionary(m => m.FromVersion);
|
||||
|
||||
public readonly string latestVersion;
|
||||
|
||||
private ConfigMigrationManager()
|
||||
{
|
||||
latestVersion = migrationMap.Values
|
||||
.Select(m => m.ToVersion)
|
||||
.OrderByDescending(version =>
|
||||
{
|
||||
var versionParts = version.Split('.').Select(int.Parse).ToArray();
|
||||
return versionParts[0] * 100000 + versionParts[1];
|
||||
})
|
||||
.First();
|
||||
}
|
||||
|
||||
public IConfigView Migrate(IConfigView config)
|
||||
{
|
||||
var currentVersion = GetVersion(config);
|
||||
while (migrationMap.ContainsKey(currentVersion))
|
||||
{
|
||||
var migration = migrationMap[currentVersion];
|
||||
Utility.Log($"Migrating config from v{migration.FromVersion} to v{migration.ToVersion}");
|
||||
config = migration.Migrate(config);
|
||||
currentVersion = migration.ToVersion;
|
||||
}
|
||||
if (currentVersion != latestVersion)
|
||||
{
|
||||
throw new ArgumentException($"Could not migrate the config from v{currentVersion} to v{latestVersion}");
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
public string GetVersion(IConfigView config)
|
||||
{
|
||||
if (config.TryGetValue<string>("Version", out var version))
|
||||
{
|
||||
return version;
|
||||
}
|
||||
// Assume v1.0 if not found
|
||||
return "1.0";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,346 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AquaMai.Config.Interfaces;
|
||||
using AquaMai.Config.Types;
|
||||
|
||||
namespace AquaMai.Config.Migration;
|
||||
|
||||
public class ConfigMigration_V1_0_V2_0 : IConfigMigration
|
||||
{
|
||||
public string FromVersion => "1.0";
|
||||
public string ToVersion => "2.0";
|
||||
|
||||
public IConfigView Migrate(IConfigView src)
|
||||
{
|
||||
var dst = new ConfigView();
|
||||
|
||||
dst.SetValue("Version", ToVersion);
|
||||
|
||||
// UX (legacy)
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.TestProof", "GameSystem.TestProof");
|
||||
if (src.GetValueOrDefault<bool>("UX.QuickSkip"))
|
||||
{
|
||||
// NOTE: UX.QuickSkip was a 4-in-1 large patch in earlier V1, then split since ModKeyMap was introduced.
|
||||
dst.SetValue("UX.OneKeyEntryEnd.Key", "Service");
|
||||
dst.SetValue("UX.OneKeyEntryEnd.LongPress", true);
|
||||
dst.SetValue("UX.OneKeyRetrySkip.RetryKey", "Service");
|
||||
dst.SetValue("UX.OneKeyRetrySkip.RetryLongPress", false);
|
||||
dst.SetValue("UX.OneKeyRetrySkip.SkipKey", "Select1P");
|
||||
dst.SetValue("UX.OneKeyRetrySkip.SkipLongPress", false);
|
||||
dst.EnsureDictionary("GameSystem.QuickRetry");
|
||||
}
|
||||
if (src.GetValueOrDefault<bool>("UX.HideSelfMadeCharts"))
|
||||
{
|
||||
dst.SetValue("UX.HideSelfMadeCharts.Key", "Service");
|
||||
dst.SetValue("UX.HideSelfMadeCharts.LongPress", false);
|
||||
}
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.LoadJacketPng", "GameSystem.Assets.LoadLocalImages");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.SkipWarningScreen", "Tweaks.TimeSaving.SkipStartupWarning");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.SkipToMusicSelection", "Tweaks.TimeSaving.EntryToMusicSelection");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.SkipEventInfo", "Tweaks.TimeSaving.SkipEventInfo");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.SelectionDetail", "UX.SelectionDetail");
|
||||
if (src.GetValueOrDefault<bool>("UX.CustomNoteSkin") ||
|
||||
src.GetValueOrDefault<bool>("UX.CustomSkins"))
|
||||
{
|
||||
dst.SetValue("Fancy.CustomSkins.SkinsDir", "LocalAssets/Skins");
|
||||
}
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.JudgeDisplay4B", "Fancy.GamePlay.JudgeDisplay4B");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.CustomTrackStartDiff", "Fancy.CustomTrackStartDiff");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.TrackStartProcessTweak", "Fancy.GamePlay.TrackStartProcessTweak");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.DisableTrackStartTabs", "Fancy.GamePlay.DisableTrackStartTabs");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.RealisticRandomJudge", "Fancy.GamePlay.RealisticRandomJudge");
|
||||
|
||||
// Utils (legacy)
|
||||
if (src.GetValueOrDefault<bool>("Utils.Windowed") ||
|
||||
src.GetValueOrDefault<int>("Utils.Width") != 0 ||
|
||||
src.GetValueOrDefault<int>("Utils.Height") != 0)
|
||||
{
|
||||
// NOTE: the default "false, 0, 0" was effective earlier in V1, but won't be migrated as enabled in V2.
|
||||
MapValueOrDefaultToEntryValue(src, dst, "Utils.Windowed", "GameSystem.Window.Windowed", false);
|
||||
MapValueOrDefaultToEntryValue(src, dst, "Utils.Width", "GameSystem.Window.Width", 0);
|
||||
MapValueOrDefaultToEntryValue(src, dst, "Utils.Height", "GameSystem.Window.Height", 0);
|
||||
}
|
||||
if (src.GetValueOrDefault<bool>("Utils.PracticeMode") || src.GetValueOrDefault<bool>("Utils.PractiseMode")) // Typo of typo is the correct word
|
||||
{
|
||||
dst.SetValue("UX.PracticeMode.Key", "Test");
|
||||
dst.SetValue("UX.PracticeMode.LongPress", false);
|
||||
}
|
||||
|
||||
// Fix (legacy)
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.SlideJudgeTweak", "Fancy.GamePlay.BreakSlideJudgeBlink");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.BreakSlideJudgeBlink", "Fancy.GamePlay.BreakSlideJudgeBlink");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.SlideJudgeTweak", "Fancy.GamePlay.FanJudgeFlip");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.FanJudgeFlip", "Fancy.GamePlay.FanJudgeFlip");
|
||||
// NOTE: This (FixCircleSlideJudge) was enabled by default in V1, but non-default in V2 since it has visual changes
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.SlideJudgeTweak", "Fancy.GamePlay.AlignCircleSlideJudgeDisplay");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.FixCircleSlideJudge", "Fancy.GamePlay.AlignCircleSlideJudgeDisplay");
|
||||
|
||||
// Performance (legacy)
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Performance.ImproveLoadSpeed", "Tweaks.TimeSaving.SkipStartupDelays");
|
||||
|
||||
// TimeSaving (legacy)
|
||||
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.ShowNetErrorDetail", "Utils.ShowNetErrorDetail");
|
||||
|
||||
// UX
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "UX.Locale", "General.Locale", "");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.SinglePlayer", "GameSystem.SinglePlayer");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.HideMask", "Fancy.HideMask");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.LoadAssetsPng", "GameSystem.Assets.LoadLocalImages");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.LoadAssetBundleWithoutManifest", "GameSystem.Assets.LoadAssetBundleWithoutManifest");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.RandomBgm", "Fancy.RandomBgm");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.DemoMaster", "Fancy.DemoMaster");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.ExtendTimer", "GameSystem.DisableTimeout");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.ImmediateSave", "UX.ImmediateSave");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.LoadLocalBga", "GameSystem.Assets.UseJacketAsDummyMovie");
|
||||
if (src.GetValueOrDefault<bool>("UX.CustomFont"))
|
||||
{
|
||||
dst.SetValue("GameSystem.Assets.Fonts.Paths", "LocalAssets/font.ttf");
|
||||
dst.SetValue("GameSystem.Assets.Fonts.AddAsFallback", false);
|
||||
}
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.TouchToButtonInput", "GameSystem.TouchToButtonInput");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.HideHanabi", "Fancy.GamePlay.HideHanabi");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.SlideFadeInTweak", "Fancy.GamePlay.SlideFadeInTweak");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.JudgeAccuracyInfo", "UX.JudgeAccuracyInfo");
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "UX.CustomVersionString", "Fancy.CustomVersionString.VersionString", "");
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "UX.CustomPlaceName", "Fancy.CustomPlaceName.PlaceName", "");
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "UX.ExecOnIdle", "Fancy.Triggers.ExecOnIdle", "");
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "UX.ExecOnEntry", "Fancy.Triggers.ExecOnEntry", "");
|
||||
|
||||
// Cheat
|
||||
var unlockTickets = src.GetValueOrDefault<bool>("Cheat.TicketUnlock");
|
||||
var unlockMaps = src.GetValueOrDefault<bool>("Cheat.MapUnlock");
|
||||
var unlockUtage = src.GetValueOrDefault<bool>("Cheat.UnlockUtage");
|
||||
if (unlockTickets ||
|
||||
unlockMaps ||
|
||||
unlockUtage)
|
||||
{
|
||||
dst.SetValue("GameSystem.Unlock.Tickets", unlockTickets);
|
||||
dst.SetValue("GameSystem.Unlock.Maps", unlockMaps);
|
||||
dst.SetValue("GameSystem.Unlock.Utage", unlockUtage);
|
||||
}
|
||||
|
||||
// Fix
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.SkipVersionCheck", "Tweaks.SkipUserVersionCheck");
|
||||
if (!src.GetValueOrDefault<bool>("Fix.RemoveEncryption"))
|
||||
{
|
||||
dst.SetValue("GameSystem.RemoveEncryption.Disabled", true); // Enabled by default in V2
|
||||
}
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.ForceAsServer", "GameSettings.ForceAsServer");
|
||||
if (src.GetValueOrDefault<bool>("Fix.ForceFreePlay"))
|
||||
{
|
||||
dst.SetValue("GameSettings.CreditConfig.IsFreePlay", true);
|
||||
}
|
||||
if (src.GetValueOrDefault<bool>("Fix.ForcePaidPlay"))
|
||||
{
|
||||
dst.SetValue("GameSettings.CreditConfig.IsFreePlay", false);
|
||||
dst.SetValue("GameSettings.CreditConfig.LockCredits", 24);
|
||||
}
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "Fix.ExtendNotesPool", "Fancy.GamePlay.ExtendNotesPool.Count", 0);
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.FrameRateLock", "Tweaks.LockFrameRate");
|
||||
if (src.GetValueOrDefault<bool>("Font.FontFix") &&
|
||||
!src.GetValueOrDefault<bool>("UX.CustomFont"))
|
||||
{
|
||||
dst.SetValue("GameSystem.Assets.Fonts.Paths", "%SYSTEMROOT%/Fonts/msyhbd.ttc");
|
||||
dst.SetValue("GameSystem.Assets.Fonts.AddAsFallback", true);
|
||||
}
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.RealisticRandomJudge", "Fancy.GamePlay.RealisticRandomJudge");
|
||||
if (src.GetValueOrDefault<bool>("UX.SinglePlayer"))
|
||||
{
|
||||
if (src.TryGetValue("Fix.HanabiFix", out bool hanabiFix))
|
||||
{
|
||||
// If it's enabled or disabled explicitly, use the value, otherwise left empty use the default V2 value (enabled).
|
||||
dst.SetValue("GameSystem.SinglePlayer.FixHanabi", hanabiFix);
|
||||
}
|
||||
}
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.IgnoreAimeServerError", "Tweaks.IgnoreAimeServerError");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.TouchResetAfterTrack", "Tweaks.ResetTouchAfterTrack");
|
||||
|
||||
// Utils
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Utils.LogUserId", "Utils.LogUserId");
|
||||
MapValueToEntryValueIfNonNullOrDefault<double>(src, dst, "Utils.JudgeAdjustA", "GameSettings.JudgeAdjust.A", 0);
|
||||
MapValueToEntryValueIfNonNullOrDefault<double>(src, dst, "Utils.JudgeAdjustB", "GameSettings.JudgeAdjust.B", 0);
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "Utils.TouchDelay", "GameSettings.JudgeAdjust.TouchDelay", 0);
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Utils.SelectionDetail", "UX.SelectionDetail");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Utils.ShowNetErrorDetail", "Utils.ShowNetErrorDetail");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Utils.ShowErrorLog", "Utils.ShowErrorLog");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Utils.FrameRateDisplay", "Utils.DisplayFrameRate");
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "Utils.TouchPanelBaudRate", "GameSystem.TouchPanelBaudRate.BaudRate", 0);
|
||||
|
||||
// TimeSaving
|
||||
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.SkipWarningScreen", "Tweaks.TimeSaving.SkipStartupWarning");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.ImproveLoadSpeed", "Tweaks.TimeSaving.SkipStartupDelays");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.SkipToMusicSelection", "Tweaks.TimeSaving.EntryToMusicSelection");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.SkipEventInfo", "Tweaks.TimeSaving.SkipEventInfo");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.IWontTapOrSlideVigorously", "Tweaks.TimeSaving.IWontTapOrSlideVigorously");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.SkipGameOverScreen", "Tweaks.TimeSaving.SkipGoodbyeScreen");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.SkipTrackStart", "Tweaks.TimeSaving.SkipTrackStart");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.ShowQuickEndPlay", "UX.QuickEndPlay");
|
||||
|
||||
// Visual
|
||||
if (src.GetValueOrDefault<bool>("Visual.CustomSkins"))
|
||||
{
|
||||
dst.SetValue("Fancy.CustomSkins.SkinsDir", "LocalAssets/Skins");
|
||||
}
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Visual.JudgeDisplay4B", "Fancy.GamePlay.JudgeDisplay4B");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Visual.CustomTrackStartDiff", "Fancy.CustomTrackStartDiff");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Visual.TrackStartProcessTweak", "Fancy.GamePlay.TrackStartProcessTweak");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Visual.DisableTrackStartTabs", "Fancy.GamePlay.DisableTrackStartTabs");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Visual.FanJudgeFlip", "Fancy.GamePlay.FanJudgeFlip");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Visual.BreakSlideJudgeBlink", "Fancy.GamePlay.BreakSlideJudgeBlink");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Visual.SlideArrowAnimation", "Fancy.GamePlay.SlideArrowAnimation");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Visual.SlideLayerReverse", "Fancy.GamePlay.SlideLayerReverse");
|
||||
|
||||
// ModKeyMap
|
||||
var keyQuickSkip = src.GetValueOrDefault("ModKeyMap.QuickSkip", "None");
|
||||
var keyInGameRetry = src.GetValueOrDefault("ModKeyMap.InGameRetry", "None");
|
||||
var keyInGameSkip = src.GetValueOrDefault("ModKeyMap.InGameSkip", "None");
|
||||
var keyPractiseMode = src.GetValueOrDefault("ModKeyMap.PractiseMode", "None");
|
||||
var keyHideSelfMadeCharts = src.GetValueOrDefault("ModKeyMap.HideSelfMadeCharts", "None");
|
||||
if (keyQuickSkip != "None")
|
||||
{
|
||||
dst.SetValue("UX.OneKeyEntryEnd.Key", keyQuickSkip);
|
||||
MapValueToEntryValueIfNonNull<bool>(src, dst, "ModKeyMap.QuickSkipLongPress", "UX.OneKeyEntryEnd.LongPress");
|
||||
}
|
||||
if (keyInGameRetry != "None" || keyInGameSkip != "None")
|
||||
{
|
||||
dst.SetValue("UX.OneKeyRetrySkip.RetryKey", keyInGameRetry);
|
||||
if (keyInGameRetry != "None")
|
||||
{
|
||||
MapValueToEntryValueIfNonNull<bool>(src, dst, "ModKeyMap.InGameRetryLongPress", "UX.OneKeyRetrySkip.RetryLongPress");
|
||||
}
|
||||
dst.SetValue("UX.OneKeyRetrySkip.SkipKey", keyInGameSkip);
|
||||
if (keyInGameSkip != "None")
|
||||
{
|
||||
MapValueToEntryValueIfNonNull<bool>(src, dst, "ModKeyMap.InGameSkipLongPress", "UX.OneKeyRetrySkip.SkipLongPress");
|
||||
}
|
||||
}
|
||||
if (keyPractiseMode != "None")
|
||||
{
|
||||
dst.SetValue("UX.PracticeMode.Key", keyPractiseMode);
|
||||
MapValueToEntryValueIfNonNull<bool>(src, dst, "ModKeyMap.PractiseModeLongPress", "UX.PracticeMode.LongPress");
|
||||
}
|
||||
if (keyHideSelfMadeCharts != "None")
|
||||
{
|
||||
dst.SetValue("UX.HideSelfMadeCharts.Key", keyHideSelfMadeCharts);
|
||||
MapValueToEntryValueIfNonNull<bool>(src, dst, "ModKeyMap.HideSelfMadeChartsLongPress", "UX.HideSelfMadeCharts.LongPress");
|
||||
}
|
||||
MapBooleanTrueToSectionEnable(src, dst, "ModKeyMap.EnableNativeQuickRetry", "GameSystem.QuickRetry");
|
||||
if (src.TryGetValue<string>("ModKeyMap.TestMode", out var testMode) &&
|
||||
testMode != "" &&
|
||||
testMode != "Test")
|
||||
{
|
||||
dst.SetValue("DeprecationWarning.v1_0_ModKeyMap_TestMode", true);
|
||||
}
|
||||
MapBooleanTrueToSectionEnable(src, dst, "ModKeyMap.TestModeLongPress", "GameSystem.TestProof");
|
||||
|
||||
// WindowState
|
||||
if (src.GetValueOrDefault<bool>("WindowState.Enable"))
|
||||
{
|
||||
MapValueOrDefaultToEntryValue(src, dst, "WindowState.Windowed", "GameSystem.Window.Windowed", false);
|
||||
MapValueOrDefaultToEntryValue(src, dst, "WindowState.Width", "GameSystem.Window.Width", 0);
|
||||
MapValueOrDefaultToEntryValue(src, dst, "WindowState.Height", "GameSystem.Window.Height", 0);
|
||||
}
|
||||
|
||||
// CustomCameraId
|
||||
if (src.GetValueOrDefault<bool>("CustomCameraId.Enable"))
|
||||
{
|
||||
dst.EnsureDictionary("GameSystem.CustomCameraId");
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "CustomCameraId.PrintCameraList", "GameSystem.CustomCameraId.PrintCameraList", false);
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "CustomCameraId.LeftQrCamera", "GameSystem.CustomCameraId.LeftQrCamera", 0);
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "CustomCameraId.RightQrCamera", "GameSystem.CustomCameraId.RightQrCamera", 0);
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "CustomCameraId.PhotoCamera", "GameSystem.CustomCameraId.PhotoCamera", 0);
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "CustomCameraId.ChimeCamera", "GameSystem.CustomCameraId.ChimeCamera", 0);
|
||||
}
|
||||
|
||||
// TouchSensitivity
|
||||
if (src.GetValueOrDefault<bool>("TouchSensitivity.Enable"))
|
||||
{
|
||||
dst.EnsureDictionary("GameSettings.TouchSensitivity");
|
||||
var areas = new[]
|
||||
{
|
||||
"A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8",
|
||||
"B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8",
|
||||
"C1", "C2",
|
||||
"D1", "D2", "D3", "D4", "D5", "D6", "D7", "D8",
|
||||
"E1", "E2", "E3", "E4", "E5", "E6", "E7", "E8",
|
||||
};
|
||||
foreach (var area in areas)
|
||||
{
|
||||
MapValueToEntryValueIfNonNull<int>(src, dst, $"TouchSensitivity.{area}", $"GameSettings.TouchSensitivity.{area}");
|
||||
}
|
||||
}
|
||||
|
||||
// CustomKeyMap
|
||||
if (src.GetValueOrDefault<bool>("CustomKeyMap.Enable"))
|
||||
{
|
||||
dst.EnsureDictionary("GameSystem.KeyMap");
|
||||
var keys = new[]
|
||||
{
|
||||
"Test", "Service",
|
||||
"Button1_1P", "Button3_1P", "Button4_1P", "Button2_1P", "Button5_1P", "Button6_1P", "Button7_1P", "Button8_1P",
|
||||
"Select_1P",
|
||||
"Button1_2P", "Button2_2P", "Button3_2P", "Button4_2P", "Button5_2P", "Button6_2P", "Button7_2P", "Button8_2P",
|
||||
"Select_2P"
|
||||
};
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (src.TryGetValue<string>($"CustomKeyMap.{key}", out var value) &&
|
||||
Enum.TryParse<KeyCodeID>(value, out var keyCode))
|
||||
{
|
||||
dst.SetValue($"GameSystem.KeyMap.{key}", keyCode.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MaimaiDX2077 (WTF is the name?)
|
||||
MapBooleanTrueToSectionEnable(src, dst, "MaimaiDX2077.CustomNoteTypePatch", "Fancy.GamePlay.CustomNoteTypes");
|
||||
|
||||
// Default enabled in V2
|
||||
dst.EnsureDictionary("GameSystem.RemoveEncryption");
|
||||
|
||||
return dst;
|
||||
}
|
||||
|
||||
// An value in the old config maps to an entry value in the new config.
|
||||
// Any existing value, including zero, is valid.
|
||||
private void MapValueToEntryValueIfNonNull<T>(IConfigView src, ConfigView dst, string srcKey, string dstKey)
|
||||
{
|
||||
if (src.TryGetValue<T>(srcKey, out var value))
|
||||
{
|
||||
dst.SetValue(dstKey, value);
|
||||
}
|
||||
}
|
||||
|
||||
// An value in the old config maps to an entry value in the new config.
|
||||
// Null or default value is ignored.
|
||||
private void MapValueToEntryValueIfNonNullOrDefault<T>(IConfigView src, ConfigView dst, string srcKey, string dstKey, T defaultValue)
|
||||
{
|
||||
if (src.TryGetValue<T>(srcKey, out var value) && !EqualityComparer<T>.Default.Equals(value, defaultValue))
|
||||
{
|
||||
dst.SetValue(dstKey, value);
|
||||
}
|
||||
}
|
||||
|
||||
// An value in the old config maps to an entry value in the new config.
|
||||
// Null value is replaced with a default value.
|
||||
private void MapValueOrDefaultToEntryValue<T>(IConfigView src, ConfigView dst, string srcKey, string dstKey, T defaultValue)
|
||||
{
|
||||
if (src.TryGetValue<T>(srcKey, out var value))
|
||||
{
|
||||
dst.SetValue(dstKey, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
dst.SetValue(dstKey, defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
// An boolean value in the old config maps to a default-off section's enable in the new config.
|
||||
private void MapBooleanTrueToSectionEnable(IConfigView src, ConfigView dst, string srcKey, string dstKey)
|
||||
{
|
||||
if (src.GetValueOrDefault<bool>(srcKey))
|
||||
{
|
||||
dst.EnsureDictionary(dstKey);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
using AquaMai.Config.Interfaces;
|
||||
|
||||
namespace AquaMai.Config.Migration;
|
||||
|
||||
public interface IConfigMigration
|
||||
{
|
||||
public string FromVersion { get; }
|
||||
public string ToVersion { get; }
|
||||
public IConfigView Migrate(IConfigView config);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
internal static class IsExternalInit {}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
namespace AquaMai.CustomKeyMap;
|
||||
namespace AquaMai.Config.Types;
|
||||
|
||||
public enum KeyCodeID
|
||||
{
|
|
@ -1,6 +1,6 @@
|
|||
namespace AquaMai.ModKeyMap;
|
||||
namespace AquaMai.Config.Types;
|
||||
|
||||
public enum ModKeyCode
|
||||
public enum KeyCodeOrName
|
||||
{
|
||||
None,
|
||||
Alpha0,
|
|
@ -0,0 +1,183 @@
|
|||
using System;
|
||||
using System.Reflection;
|
||||
using Tomlet.Models;
|
||||
|
||||
namespace AquaMai.Config;
|
||||
|
||||
public static class Utility
|
||||
{
|
||||
public static Action<string> LogFunction = Console.WriteLine;
|
||||
|
||||
public static bool IsTruty(TomlValue value, string path = null)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
TomlBoolean boolean => boolean.Value,
|
||||
TomlLong @long => @long.Value != 0,
|
||||
_ => throw new ArgumentException(
|
||||
path == null
|
||||
? $"Non-boolish TOML type {value.GetType().Name} value: {value}"
|
||||
: $"When parsing {path}, got non-boolish TOML type {value.GetType().Name} value: {value}")
|
||||
};
|
||||
}
|
||||
|
||||
public static bool IsIntegerType(Type type)
|
||||
{
|
||||
return type == typeof(sbyte) || type == typeof(short) || type == typeof(int) || type == typeof(long)
|
||||
|| type == typeof(byte) || type == typeof(ushort) || type == typeof(uint) || type == typeof(ulong);
|
||||
}
|
||||
|
||||
public static bool IsFloatType(Type type)
|
||||
{
|
||||
return type == typeof(float) || type == typeof(double);
|
||||
}
|
||||
|
||||
public static bool IsNumberType(Type type)
|
||||
{
|
||||
return IsIntegerType(type) || IsFloatType(type);
|
||||
}
|
||||
|
||||
public static T ParseTomlValue<T>(TomlValue value)
|
||||
{
|
||||
return (T)ParseTomlValue(typeof(T), value);
|
||||
}
|
||||
|
||||
public static object ParseTomlValue(Type type, TomlValue value)
|
||||
{
|
||||
if (type == typeof(bool))
|
||||
{
|
||||
return IsTruty(value);
|
||||
}
|
||||
else if (IsNumberType(type))
|
||||
{
|
||||
if (TryGetTomlNumberObject(value, out var numberObject))
|
||||
{
|
||||
return Convert.ChangeType(numberObject, type);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidCastException($"Non-number TOML type: {value.GetType().Name}");
|
||||
}
|
||||
}
|
||||
else if (type == typeof(string))
|
||||
{
|
||||
if (value is TomlString @string)
|
||||
{
|
||||
return @string.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidCastException($"Non-string TOML type: {value.GetType().Name}");
|
||||
}
|
||||
}
|
||||
else if (type.IsEnum)
|
||||
{
|
||||
if (value is TomlString @string)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Enum.Parse(type, @string.Value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new InvalidCastException($"Invalid enum {type.FullName} value: {@string.SerializedValue}");
|
||||
}
|
||||
}
|
||||
else if (value is TomlLong @long)
|
||||
{
|
||||
if (Enum.IsDefined(type, @long.Value))
|
||||
{
|
||||
try
|
||||
{
|
||||
return Enum.ToObject(type, @long.Value);
|
||||
}
|
||||
catch
|
||||
{}
|
||||
}
|
||||
throw new InvalidCastException($"Invalid enum {type.FullName} value: {@long.Value}");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidCastException($"Non-enum TOML type: {value.GetType().Name}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var currentMethod = MethodBase.GetCurrentMethod();
|
||||
throw new NotImplementedException($"Unsupported config entry type: {type.FullName}. Please implement in {currentMethod.DeclaringType.FullName}.{currentMethod.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetTomlNumberObject(TomlValue value, out object numberObject)
|
||||
{
|
||||
if (value is TomlLong @long)
|
||||
{
|
||||
numberObject = @long.Value;
|
||||
return true;
|
||||
}
|
||||
else if (value is TomlDouble @double)
|
||||
{
|
||||
numberObject = @double.Value;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
numberObject = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TomlTryGetValueCaseInsensitive(TomlTable table, string key, out TomlValue value)
|
||||
{
|
||||
// Prefer exact match
|
||||
if (table.TryGetValue(key, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// Fallback to case-insensitive match
|
||||
foreach (var kvp in table)
|
||||
{
|
||||
if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = kvp.Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TomlContainsKeyCaseInsensitive(TomlTable table, string key)
|
||||
{
|
||||
// Prefer exact match
|
||||
if (table.ContainsKey(key))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// Fallback to case-insensitive match
|
||||
foreach (var kvp in table)
|
||||
{
|
||||
if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string ToPascalCase(string str)
|
||||
{
|
||||
return str.Length switch
|
||||
{
|
||||
0 => str,
|
||||
1 => char.ToUpperInvariant(str[0]).ToString(),
|
||||
_ => char.ToUpperInvariant(str[0]) + str.Substring(1)
|
||||
};
|
||||
}
|
||||
|
||||
// We can test the configuration related code without loading the mod into the game.
|
||||
public static void Log(string message)
|
||||
{
|
||||
LogFunction(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{33C0D4ED-6A84-4659-9A05-12D43D75D0B3}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<RootNamespace>AquaMai.Core</RootNamespace>
|
||||
<AssemblyName>AquaMai.Core</AssemblyName>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
<LangVersion>12</LangVersion>
|
||||
<NoWarn>414</NoWarn>
|
||||
<AssemblySearchPaths>$(ProjectDir)../Libs/;$(AssemblySearchPaths)</AssemblySearchPaths>
|
||||
<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/AquaMai.Config.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="mscorlib" />
|
||||
<Reference Include="0Harmony" />
|
||||
<Reference Include="AMDaemon.NET" />
|
||||
<Reference Include="Assembly-CSharp" />
|
||||
<Reference Include="Assembly-CSharp-firstpass" />
|
||||
<Reference Include="MelonLoader" />
|
||||
<Reference Include="Mono.Cecil" />
|
||||
<Reference Include="Mono.Posix" />
|
||||
<Reference Include="Mono.Security" />
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Security" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="Unity.Analytics.DataPrivacy" />
|
||||
<Reference Include="Unity.TextMeshPro" />
|
||||
<Reference Include="UnityEngine" />
|
||||
<Reference Include="UnityEngine.AccessibilityModule" />
|
||||
<Reference Include="UnityEngine.AIModule" />
|
||||
<Reference Include="UnityEngine.AnimationModule" />
|
||||
<Reference Include="UnityEngine.ARModule" />
|
||||
<Reference Include="UnityEngine.AssetBundleModule" />
|
||||
<Reference Include="UnityEngine.AudioModule" />
|
||||
<Reference Include="UnityEngine.BaselibModule" />
|
||||
<Reference Include="UnityEngine.ClothModule" />
|
||||
<Reference Include="UnityEngine.ClusterInputModule" />
|
||||
<Reference Include="UnityEngine.ClusterRendererModule" />
|
||||
<Reference Include="UnityEngine.CoreModule" />
|
||||
<Reference Include="UnityEngine.CrashReportingModule" />
|
||||
<Reference Include="UnityEngine.DirectorModule" />
|
||||
<Reference Include="UnityEngine.FileSystemHttpModule" />
|
||||
<Reference Include="UnityEngine.GameCenterModule" />
|
||||
<Reference Include="UnityEngine.GridModule" />
|
||||
<Reference Include="UnityEngine.HotReloadModule" />
|
||||
<Reference Include="UnityEngine.ImageConversionModule" />
|
||||
<Reference Include="UnityEngine.IMGUIModule" />
|
||||
<Reference Include="UnityEngine.InputModule" />
|
||||
<Reference Include="UnityEngine.JSONSerializeModule" />
|
||||
<Reference Include="UnityEngine.LocalizationModule" />
|
||||
<Reference Include="UnityEngine.Networking" />
|
||||
<Reference Include="UnityEngine.ParticleSystemModule" />
|
||||
<Reference Include="UnityEngine.PerformanceReportingModule" />
|
||||
<Reference Include="UnityEngine.Physics2DModule" />
|
||||
<Reference Include="UnityEngine.PhysicsModule" />
|
||||
<Reference Include="UnityEngine.ProfilerModule" />
|
||||
<Reference Include="UnityEngine.ScreenCaptureModule" />
|
||||
<Reference Include="UnityEngine.SharedInternalsModule" />
|
||||
<Reference Include="UnityEngine.SpatialTracking" />
|
||||
<Reference Include="UnityEngine.SpriteMaskModule" />
|
||||
<Reference Include="UnityEngine.SpriteShapeModule" />
|
||||
<Reference Include="UnityEngine.StreamingModule" />
|
||||
<Reference Include="UnityEngine.StyleSheetsModule" />
|
||||
<Reference Include="UnityEngine.SubstanceModule" />
|
||||
<Reference Include="UnityEngine.TerrainModule" />
|
||||
<Reference Include="UnityEngine.TerrainPhysicsModule" />
|
||||
<Reference Include="UnityEngine.TextCoreModule" />
|
||||
<Reference Include="UnityEngine.TextRenderingModule" />
|
||||
<Reference Include="UnityEngine.TilemapModule" />
|
||||
<Reference Include="UnityEngine.Timeline" />
|
||||
<Reference Include="UnityEngine.TimelineModule" />
|
||||
<Reference Include="UnityEngine.TLSModule" />
|
||||
<Reference Include="UnityEngine.UI" />
|
||||
<Reference Include="UnityEngine.UIElementsModule" />
|
||||
<Reference Include="UnityEngine.UIModule" />
|
||||
<Reference Include="UnityEngine.UmbraModule" />
|
||||
<Reference Include="UnityEngine.UNETModule" />
|
||||
<Reference Include="UnityEngine.UnityAnalyticsModule" />
|
||||
<Reference Include="UnityEngine.UnityConnectModule" />
|
||||
<Reference Include="UnityEngine.UnityTestProtocolModule" />
|
||||
<Reference Include="UnityEngine.UnityWebRequestAssetBundleModule" />
|
||||
<Reference Include="UnityEngine.UnityWebRequestAudioModule" />
|
||||
<Reference Include="UnityEngine.UnityWebRequestModule" />
|
||||
<Reference Include="UnityEngine.UnityWebRequestTextureModule" />
|
||||
<Reference Include="UnityEngine.UnityWebRequestWWWModule" />
|
||||
<Reference Include="UnityEngine.VehiclesModule" />
|
||||
<Reference Include="UnityEngine.VFXModule" />
|
||||
<Reference Include="UnityEngine.VideoModule" />
|
||||
<Reference Include="UnityEngine.VRModule" />
|
||||
<Reference Include="UnityEngine.WindModule" />
|
||||
<Reference Include="UnityEngine.XRModule" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources/Locale.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Locale.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Resources/Locale.zh.resx" WithCulture="false">
|
||||
<DependentUpon>Locale.resx</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,18 @@
|
|||
using System;
|
||||
|
||||
namespace AquaMai.Core.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
||||
public class EnableGameVersionAttribute(uint minVersion = 0, uint maxVersion = 0, bool noWarn = false) : Attribute
|
||||
{
|
||||
public uint MinVersion { get; } = minVersion;
|
||||
public uint MaxVersion { get; } = maxVersion;
|
||||
public bool NoWarn { get; } = noWarn;
|
||||
|
||||
public bool ShouldEnable(uint gameVersion)
|
||||
{
|
||||
if (MinVersion > 0 && MinVersion > gameVersion) return false;
|
||||
if (MaxVersion > 0 && MaxVersion < gameVersion) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
using System;
|
||||
|
||||
namespace AquaMai.Core.Attributes;
|
||||
|
||||
public enum EnableConditionOperator
|
||||
{
|
||||
Equal,
|
||||
NotEqual,
|
||||
GreaterThan,
|
||||
LessThan,
|
||||
GreaterThanOrEqual,
|
||||
LessThanOrEqual
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
||||
public class EnableIfAttribute(
|
||||
Type referenceType,
|
||||
string referenceMember,
|
||||
EnableConditionOperator @operator,
|
||||
object rightSideValue) : Attribute
|
||||
{
|
||||
public Type ReferenceType { get; } = referenceType;
|
||||
public string ReferenceMember { get; } = referenceMember;
|
||||
public EnableConditionOperator Operator { get; } = @operator;
|
||||
public object RightSideValue { get; } = rightSideValue;
|
||||
|
||||
// Referencing a field in another class and checking if it's true.
|
||||
public EnableIfAttribute(Type referenceType, string referenceMember)
|
||||
: this(referenceType, referenceMember, EnableConditionOperator.Equal, true)
|
||||
{ }
|
||||
|
||||
// Referencing a field in the same class and comparing it with a value.
|
||||
public EnableIfAttribute(string referenceMember, EnableConditionOperator condition, object value)
|
||||
: this(null, referenceMember, condition, value)
|
||||
{ }
|
||||
|
||||
// Referencing a field in the same class and checking if it's true.
|
||||
public EnableIfAttribute(string referenceMember)
|
||||
: this(referenceMember, EnableConditionOperator.Equal, true)
|
||||
{ }
|
||||
|
||||
public bool ShouldEnable(Type selfType)
|
||||
{
|
||||
var referenceType = ReferenceType ?? selfType;
|
||||
var referenceField = referenceType.GetField(
|
||||
ReferenceMember,
|
||||
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
|
||||
var referenceProperty = referenceType.GetProperty(
|
||||
ReferenceMember,
|
||||
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
|
||||
if (referenceField == null && referenceProperty == null)
|
||||
{
|
||||
throw new ArgumentException($"Field or property {ReferenceMember} not found in {referenceType.FullName}");
|
||||
}
|
||||
var referenceMemberValue = referenceField != null ? referenceField.GetValue(null) : referenceProperty.GetValue(null);
|
||||
switch (Operator)
|
||||
{
|
||||
case EnableConditionOperator.Equal:
|
||||
return referenceMemberValue.Equals(RightSideValue);
|
||||
case EnableConditionOperator.NotEqual:
|
||||
return !referenceMemberValue.Equals(RightSideValue);
|
||||
case EnableConditionOperator.GreaterThan:
|
||||
case EnableConditionOperator.LessThan:
|
||||
case EnableConditionOperator.GreaterThanOrEqual:
|
||||
case EnableConditionOperator.LessThanOrEqual:
|
||||
var comparison = (IComparable)referenceMemberValue;
|
||||
return Operator switch
|
||||
{
|
||||
EnableConditionOperator.GreaterThan => comparison.CompareTo(RightSideValue) > 0,
|
||||
EnableConditionOperator.LessThan => comparison.CompareTo(RightSideValue) < 0,
|
||||
EnableConditionOperator.GreaterThanOrEqual => comparison.CompareTo(RightSideValue) >= 0,
|
||||
EnableConditionOperator.LessThanOrEqual => comparison.CompareTo(RightSideValue) <= 0,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
using System;
|
||||
|
||||
namespace AquaMai.Core.Attributes;
|
||||
|
||||
// If the field or property with this name is true, the patch will be implicitly enabled, regardless of the config state.
|
||||
// This is handled outside the config module, while The config state won't be actually set to enabled by it.
|
||||
// Won't bypass the restriction of [EnableIf()] and [EnableGameVersion()].
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class EnableImplicitlyIf(string memberName) : Attribute
|
||||
{
|
||||
public string MemberName { get; } = memberName;
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using MelonLoader;
|
||||
using AquaMai.Config;
|
||||
using AquaMai.Config.Interfaces;
|
||||
using AquaMai.Config.Migration;
|
||||
|
||||
namespace AquaMai.Core;
|
||||
|
||||
public static class ConfigLoader
|
||||
{
|
||||
private static string ConfigFile => "AquaMai.toml";
|
||||
private static string ConfigExampleFile(string lang) => $"AquaMai.{lang}.toml";
|
||||
private static string OldConfigFile(string version) => $"AquaMai.toml.old-v{version}.";
|
||||
|
||||
private static Config.Config config;
|
||||
|
||||
public static Config.Config Config => config;
|
||||
|
||||
public static bool LoadConfig(Assembly modsAssembly)
|
||||
{
|
||||
Utility.LogFunction = MelonLogger.Msg;
|
||||
|
||||
config = new(
|
||||
new Config.Reflection.ReflectionManager(
|
||||
new Config.Reflection.SystemReflectionProvider(modsAssembly)));
|
||||
|
||||
if (!File.Exists(ConfigFile))
|
||||
{
|
||||
var examples = GenerateExamples();
|
||||
foreach (var (lang, example) in examples)
|
||||
{
|
||||
var filename = ConfigExampleFile(lang);
|
||||
File.WriteAllText(filename, example);
|
||||
}
|
||||
MelonLogger.Error("======================================!!!");
|
||||
MelonLogger.Error("AquaMai.toml not found! Please create it.");
|
||||
MelonLogger.Error("找不到配置文件 AquaMai.toml!请创建。");
|
||||
MelonLogger.Error("Example copied to AquaMai.en.toml");
|
||||
MelonLogger.Error("示例已复制到 AquaMai.zh.toml");
|
||||
MelonLogger.Error("=========================================");
|
||||
return false;
|
||||
}
|
||||
|
||||
var configText = File.ReadAllText(ConfigFile);
|
||||
var configView = new ConfigView(configText);
|
||||
var configVersion = ConfigMigrationManager.Instance.GetVersion(configView);
|
||||
if (configVersion != ConfigMigrationManager.Instance.latestVersion)
|
||||
{
|
||||
File.WriteAllText(OldConfigFile(configVersion), configText);
|
||||
configView = (ConfigView)ConfigMigrationManager.Instance.Migrate(configView);
|
||||
}
|
||||
|
||||
// Read AquaMai.toml to load settings
|
||||
ConfigParser.Instance.Parse(config, configView);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void SaveConfig(string lang)
|
||||
{
|
||||
File.WriteAllText(ConfigFile, SerailizeCurrentConfig(lang));
|
||||
}
|
||||
|
||||
private static string SerailizeCurrentConfig(string lang) =>
|
||||
new ConfigSerializer(new IConfigSerializer.Options()
|
||||
{
|
||||
Lang = lang,
|
||||
IncludeBanner = true,
|
||||
OverrideLocaleValue = true
|
||||
}).Serialize(config);
|
||||
|
||||
private static IDictionary<string, string> GenerateExamples()
|
||||
{
|
||||
var examples = new Dictionary<string, string>();
|
||||
foreach (var lang in (string[]) ["en", "zh"])
|
||||
{
|
||||
examples[lang] = SerailizeCurrentConfig(lang);
|
||||
}
|
||||
return examples;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using AquaMai.Core.Attributes;
|
||||
using AquaMai.Core.Resources;
|
||||
using HarmonyLib;
|
||||
using MelonLoader;
|
||||
|
||||
namespace AquaMai.Core.Helpers;
|
||||
|
||||
public class EnableConditionHelper
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch("HarmonyLib.PatchTools", "GetPatchMethod")]
|
||||
public static void PostGetPatchMethod(ref MethodInfo __result)
|
||||
{
|
||||
if (__result != null)
|
||||
{
|
||||
if (ShouldSkipMethodOrClass(__result.GetCustomAttribute, __result.ReflectedType, __result.Name))
|
||||
{
|
||||
__result = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch("HarmonyLib.PatchTools", "GetPatchMethods")]
|
||||
public static void PostGetPatchMethods(ref IList __result)
|
||||
{
|
||||
for (int i = 0; i < __result.Count; i++)
|
||||
{
|
||||
var harmonyMethod = Traverse.Create(__result[i]).Field("info").GetValue() as HarmonyMethod;
|
||||
var method = harmonyMethod.method;
|
||||
if (ShouldSkipMethodOrClass(method.GetCustomAttribute, method.ReflectedType, method.Name))
|
||||
{
|
||||
__result.RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ShouldSkipClass(Type type)
|
||||
{
|
||||
return ShouldSkipMethodOrClass(type.GetCustomAttribute, type);
|
||||
}
|
||||
|
||||
private static bool ShouldSkipMethodOrClass(Func<Type, object> getCustomAttribute, Type type, string methodName = "")
|
||||
{
|
||||
var displayName = type.FullName + (string.IsNullOrEmpty(methodName) ? "" : $".{methodName}");
|
||||
var enableIf = (EnableIfAttribute)getCustomAttribute(typeof(EnableIfAttribute));
|
||||
if (enableIf != null && !enableIf.ShouldEnable(type))
|
||||
{
|
||||
# if DEBUG
|
||||
MelonLogger.Msg($"Skipping {displayName} due to EnableIf condition");
|
||||
# endif
|
||||
return true;
|
||||
}
|
||||
var enableGameVersion = (EnableGameVersionAttribute)getCustomAttribute(typeof(EnableGameVersionAttribute));
|
||||
if (enableGameVersion != null && !enableGameVersion.ShouldEnable(GameInfo.GameVersion))
|
||||
{
|
||||
# if DEBUG
|
||||
MelonLogger.Msg($"Skipping {displayName} due to EnableGameVersion condition");
|
||||
# endif
|
||||
if (!enableGameVersion.NoWarn)
|
||||
{
|
||||
MelonLogger.Warning(string.Format(Locale.SkipIncompatiblePatch, type));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace AquaMai.Core.Helpers;
|
||||
|
||||
public static class FileSystem
|
||||
{
|
||||
public static string ResolvePath(string path)
|
||||
{
|
||||
var varExpanded = Environment.ExpandEnvironmentVariables(path);
|
||||
return Path.IsPathRooted(varExpanded)
|
||||
? varExpanded
|
||||
: Path.Combine(Environment.CurrentDirectory, varExpanded);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
using System.Reflection;
|
||||
using MAI2System;
|
||||
|
||||
namespace AquaMai.Helpers;
|
||||
namespace AquaMai.Core.Helpers;
|
||||
|
||||
public class GameInfo
|
||||
{
|
|
@ -2,15 +2,15 @@
|
|||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using HarmonyLib;
|
||||
using MelonLoader;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Helpers;
|
||||
namespace AquaMai.Core.Helpers;
|
||||
|
||||
public static class GuiSizes
|
||||
{
|
||||
public static bool SinglePlayer { get; set; } = false;
|
||||
public static float PlayerWidth => Screen.height / 1920f * 1080;
|
||||
public static float PlayerCenter => AquaMai.AppConfig.UX.SinglePlayer ? Screen.width / 2f : Screen.width / 2f - PlayerWidth / 2;
|
||||
public static float PlayerCenter => SinglePlayer ? Screen.width / 2f : Screen.width / 2f - PlayerWidth / 2;
|
||||
public static int FontSize => (int)(PlayerWidth * .015f);
|
||||
public static float LabelHeight => FontSize * 1.5f;
|
||||
public static float Margin => PlayerWidth * .005f;
|
|
@ -0,0 +1,146 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using AquaMai.Config.Types;
|
||||
using HarmonyLib;
|
||||
using Main;
|
||||
using Manager;
|
||||
using MelonLoader;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Core.Helpers;
|
||||
|
||||
public static class KeyListener
|
||||
{
|
||||
private static readonly Dictionary<KeyCodeOrName, int> _keyPressFrames = [];
|
||||
private static readonly Dictionary<KeyCodeOrName, int> _keyPressFramesPrev = [];
|
||||
|
||||
static KeyListener()
|
||||
{
|
||||
foreach (KeyCodeOrName key in Enum.GetValues(typeof(KeyCodeOrName)))
|
||||
{
|
||||
_keyPressFrames[key] = 0;
|
||||
_keyPressFramesPrev[key] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(GameMainObject), "Update")]
|
||||
public static void CheckLongPush()
|
||||
{
|
||||
foreach (KeyCodeOrName key in Enum.GetValues(typeof(KeyCodeOrName)))
|
||||
{
|
||||
_keyPressFramesPrev[key] = _keyPressFrames[key];
|
||||
if (GetKeyPush(key))
|
||||
{
|
||||
# if DEBUG
|
||||
MelonLogger.Msg($"CheckLongPush {key} is push {_keyPressFrames[key]}");
|
||||
# endif
|
||||
_keyPressFrames[key]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_keyPressFrames[key] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool GetKeyPush(KeyCodeOrName key) =>
|
||||
key switch
|
||||
{
|
||||
KeyCodeOrName.None => false,
|
||||
< KeyCodeOrName.Select1P => Input.GetKey(key.GetKeyCode()),
|
||||
KeyCodeOrName.Test => InputManager.GetSystemInputPush(InputManager.SystemButtonSetting.ButtonTest),
|
||||
KeyCodeOrName.Service => InputManager.GetSystemInputPush(InputManager.SystemButtonSetting.ButtonService),
|
||||
KeyCodeOrName.Select1P => InputManager.GetButtonPush(0, InputManager.ButtonSetting.Select),
|
||||
KeyCodeOrName.Select2P => InputManager.GetButtonPush(1, InputManager.ButtonSetting.Select),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(key), key, "我也不知道这是什么键")
|
||||
};
|
||||
|
||||
public static bool GetKeyDown(KeyCodeOrName key)
|
||||
{
|
||||
// return key switch
|
||||
// {
|
||||
// KeyCodeOrName.None => false,
|
||||
// < KeyCodeOrName.Select1P => Input.GetKeyDown(key.GetKeyCode()),
|
||||
// KeyCodeOrName.Test => InputManager.GetSystemInputDown(InputManager.SystemButtonSetting.ButtonTest),
|
||||
// KeyCodeOrName.Service => InputManager.GetSystemInputDown(InputManager.SystemButtonSetting.ButtonService),
|
||||
// KeyCodeOrName.Select1P => InputManager.GetButtonDown(0, InputManager.ButtonSetting.Select),
|
||||
// KeyCodeOrName.Select2P => InputManager.GetButtonDown(1, InputManager.ButtonSetting.Select),
|
||||
// _ => throw new ArgumentOutOfRangeException(nameof(key), key, "我也不知道这是什么键")
|
||||
// };
|
||||
|
||||
// 不用这个,我们检测按键是否弹起以及弹起之前按下的时间是否小于 30,这样可以防止要长按时按下的时候就触发
|
||||
return _keyPressFrames[key] == 0 && 0 < _keyPressFramesPrev[key] && _keyPressFramesPrev[key] < 30;
|
||||
}
|
||||
|
||||
public static bool GetKeyDownOrLongPress(KeyCodeOrName key, bool isLongPress)
|
||||
{
|
||||
bool ret;
|
||||
if (isLongPress)
|
||||
{
|
||||
ret = _keyPressFrames[key] == 60;
|
||||
}
|
||||
else
|
||||
{
|
||||
ret = GetKeyDown(key);
|
||||
}
|
||||
|
||||
# if DEBUG
|
||||
if (ret)
|
||||
{
|
||||
MelonLogger.Msg($"Key {key} is pressed, long press: {isLongPress}");
|
||||
MelonLogger.Msg(new StackTrace());
|
||||
}
|
||||
# endif
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static KeyCode GetKeyCode(this KeyCodeOrName keyCodeOrName) =>
|
||||
keyCodeOrName switch
|
||||
{
|
||||
KeyCodeOrName.Alpha0 => KeyCode.Alpha0,
|
||||
KeyCodeOrName.Alpha1 => KeyCode.Alpha1,
|
||||
KeyCodeOrName.Alpha2 => KeyCode.Alpha2,
|
||||
KeyCodeOrName.Alpha3 => KeyCode.Alpha3,
|
||||
KeyCodeOrName.Alpha4 => KeyCode.Alpha4,
|
||||
KeyCodeOrName.Alpha5 => KeyCode.Alpha5,
|
||||
KeyCodeOrName.Alpha6 => KeyCode.Alpha6,
|
||||
KeyCodeOrName.Alpha7 => KeyCode.Alpha7,
|
||||
KeyCodeOrName.Alpha8 => KeyCode.Alpha8,
|
||||
KeyCodeOrName.Alpha9 => KeyCode.Alpha9,
|
||||
KeyCodeOrName.Keypad0 => KeyCode.Keypad0,
|
||||
KeyCodeOrName.Keypad1 => KeyCode.Keypad1,
|
||||
KeyCodeOrName.Keypad2 => KeyCode.Keypad2,
|
||||
KeyCodeOrName.Keypad3 => KeyCode.Keypad3,
|
||||
KeyCodeOrName.Keypad4 => KeyCode.Keypad4,
|
||||
KeyCodeOrName.Keypad5 => KeyCode.Keypad5,
|
||||
KeyCodeOrName.Keypad6 => KeyCode.Keypad6,
|
||||
KeyCodeOrName.Keypad7 => KeyCode.Keypad7,
|
||||
KeyCodeOrName.Keypad8 => KeyCode.Keypad8,
|
||||
KeyCodeOrName.Keypad9 => KeyCode.Keypad9,
|
||||
KeyCodeOrName.F1 => KeyCode.F1,
|
||||
KeyCodeOrName.F2 => KeyCode.F2,
|
||||
KeyCodeOrName.F3 => KeyCode.F3,
|
||||
KeyCodeOrName.F4 => KeyCode.F4,
|
||||
KeyCodeOrName.F5 => KeyCode.F5,
|
||||
KeyCodeOrName.F6 => KeyCode.F6,
|
||||
KeyCodeOrName.F7 => KeyCode.F7,
|
||||
KeyCodeOrName.F8 => KeyCode.F8,
|
||||
KeyCodeOrName.F9 => KeyCode.F9,
|
||||
KeyCodeOrName.F10 => KeyCode.F10,
|
||||
KeyCodeOrName.F11 => KeyCode.F11,
|
||||
KeyCodeOrName.F12 => KeyCode.F12,
|
||||
KeyCodeOrName.Insert => KeyCode.Insert,
|
||||
KeyCodeOrName.Delete => KeyCode.Delete,
|
||||
KeyCodeOrName.Home => KeyCode.Home,
|
||||
KeyCodeOrName.End => KeyCode.End,
|
||||
KeyCodeOrName.PageUp => KeyCode.PageUp,
|
||||
KeyCodeOrName.PageDown => KeyCode.PageDown,
|
||||
KeyCodeOrName.UpArrow => KeyCode.UpArrow,
|
||||
KeyCodeOrName.DownArrow => KeyCode.DownArrow,
|
||||
KeyCodeOrName.LeftArrow => KeyCode.LeftArrow,
|
||||
KeyCodeOrName.RightArrow => KeyCode.RightArrow,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(keyCodeOrName), keyCodeOrName, "游戏功能键需要单独处理")
|
||||
};
|
||||
}
|
|
@ -4,7 +4,7 @@ using Manager;
|
|||
using MelonLoader;
|
||||
using Process;
|
||||
|
||||
namespace AquaMai.Helpers;
|
||||
namespace AquaMai.Core.Helpers;
|
||||
|
||||
public class MessageHelper
|
||||
{
|
|
@ -1,10 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
using MelonLoader;
|
||||
|
||||
namespace AquaMai.Helpers;
|
||||
namespace AquaMai.Core.Helpers;
|
||||
|
||||
public class MusicDirHelper
|
||||
{
|
|
@ -2,7 +2,7 @@
|
|||
using Main;
|
||||
using Process;
|
||||
|
||||
namespace AquaMai.Helpers;
|
||||
namespace AquaMai.Core.Helpers;
|
||||
|
||||
public class SharedInstances
|
||||
{
|
|
@ -9,7 +9,7 @@ using Manager.UserDatas;
|
|||
using Net.Packet;
|
||||
using Net.Packet.Mai2;
|
||||
|
||||
namespace AquaMai.Helpers;
|
||||
namespace AquaMai.Core.Helpers;
|
||||
|
||||
public static class Shim
|
||||
{
|
|
@ -1,9 +1,8 @@
|
|||
using System.Globalization;
|
||||
using System.Resources;
|
||||
using HarmonyLib;
|
||||
using MelonLoader;
|
||||
|
||||
namespace AquaMai.Fix;
|
||||
namespace AquaMai.Core.Resources;
|
||||
|
||||
public class I18nSingleAssemblyHook
|
||||
{
|
||||
|
@ -13,8 +12,8 @@ public class I18nSingleAssemblyHook
|
|||
{
|
||||
var GetResourceFileName = __instance.GetType().GetMethod("GetResourceFileName", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
var resourceFileName = (string)GetResourceFileName.Invoke(__instance, [culture]);
|
||||
var MainAssembly = typeof(AquaMai).Assembly;
|
||||
var manifestResourceStream = MainAssembly.GetManifestResourceStream(resourceFileName);
|
||||
var ResourcesAssembly = typeof(I18nSingleAssemblyHook).Assembly;
|
||||
var manifestResourceStream = ResourcesAssembly.GetManifestResourceStream(resourceFileName);
|
||||
if (manifestResourceStream == null)
|
||||
{
|
||||
return true;
|
||||
|
@ -22,7 +21,7 @@ public class I18nSingleAssemblyHook
|
|||
|
||||
var resourceGroveler = __instance.GetType().GetField("resourceGroveler", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(__instance);
|
||||
var CreateResourceSet = resourceGroveler.GetType().GetMethod("CreateResourceSet", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
var resourceSet = CreateResourceSet.Invoke(resourceGroveler, [manifestResourceStream, MainAssembly]);
|
||||
var resourceSet = CreateResourceSet.Invoke(resourceGroveler, [manifestResourceStream, ResourcesAssembly]);
|
||||
var AddResourceSet = __instance.GetType().GetMethod("AddResourceSet", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
|
||||
var localResourceSets = __instance.GetType().GetField("_resourceSets", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(__instance);
|
||||
object[] args = [localResourceSets, culture.Name, resourceSet];
|
|
@ -7,7 +7,7 @@
|
|||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace AquaMai.Resources {
|
||||
namespace AquaMai.Core.Resources {
|
||||
using System;
|
||||
|
||||
|
||||
|
@ -21,24 +21,24 @@ namespace AquaMai.Resources {
|
|||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Locale {
|
||||
public class Locale {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Locale() {
|
||||
public Locale() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
public static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AquaMai.Resources.Locale", typeof(Locale).Assembly);
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AquaMai.Core.Resources.Locale", typeof(Locale).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
|
@ -50,7 +50,7 @@ namespace AquaMai.Resources {
|
|||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
public static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to You are using AquaMai CI build version. This version is built from the latest mainline code and may contain undocumented configuration changes or potential issues..
|
||||
/// </summary>
|
||||
internal static string CiBuildAlertContent {
|
||||
public static string CiBuildAlertContent {
|
||||
get {
|
||||
return ResourceManager.GetString("CiBuildAlertContent", resourceCulture);
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Important Notice: Test Version.
|
||||
/// </summary>
|
||||
internal static string CiBuildAlertTitle {
|
||||
public static string CiBuildAlertTitle {
|
||||
get {
|
||||
return ResourceManager.GetString("CiBuildAlertTitle", resourceCulture);
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Loaded!.
|
||||
/// </summary>
|
||||
internal static string Loaded {
|
||||
public static string Loaded {
|
||||
get {
|
||||
return ResourceManager.GetString("Loaded", resourceCulture);
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ namespace AquaMai.Resources {
|
|||
///- Are you using a modified Assembly-CSharp.dll, which will cause inconsistent functions and cannot find the functions that need to be modified
|
||||
///- Check for conflicting mods, or enabled incompatible options.
|
||||
/// </summary>
|
||||
internal static string LoadError {
|
||||
public static string LoadError {
|
||||
get {
|
||||
return ResourceManager.GetString("LoadError", resourceCulture);
|
||||
}
|
||||
|
@ -100,7 +100,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to End.
|
||||
/// </summary>
|
||||
internal static string MarkRepeatEnd {
|
||||
public static string MarkRepeatEnd {
|
||||
get {
|
||||
return ResourceManager.GetString("MarkRepeatEnd", resourceCulture);
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Start.
|
||||
/// </summary>
|
||||
internal static string MarkRepeatStart {
|
||||
public static string MarkRepeatStart {
|
||||
get {
|
||||
return ResourceManager.GetString("MarkRepeatStart", resourceCulture);
|
||||
}
|
||||
|
@ -118,7 +118,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Aime reader error.
|
||||
/// </summary>
|
||||
internal static string NetErrIsAliveAimeReader {
|
||||
public static string NetErrIsAliveAimeReader {
|
||||
get {
|
||||
return ResourceManager.GetString("NetErrIsAliveAimeReader", resourceCulture);
|
||||
}
|
||||
|
@ -127,7 +127,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Aime server error.
|
||||
/// </summary>
|
||||
internal static string NetErrIsAliveAimeServer {
|
||||
public static string NetErrIsAliveAimeServer {
|
||||
get {
|
||||
return ResourceManager.GetString("NetErrIsAliveAimeServer", resourceCulture);
|
||||
}
|
||||
|
@ -136,7 +136,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Server communication error.
|
||||
/// </summary>
|
||||
internal static string NetErrIsAliveServer {
|
||||
public static string NetErrIsAliveServer {
|
||||
get {
|
||||
return ResourceManager.GetString("NetErrIsAliveServer", resourceCulture);
|
||||
}
|
||||
|
@ -145,7 +145,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Data download not success.
|
||||
/// </summary>
|
||||
internal static string NetErrWasDownloadSuccessOnce {
|
||||
public static string NetErrWasDownloadSuccessOnce {
|
||||
get {
|
||||
return ResourceManager.GetString("NetErrWasDownloadSuccessOnce", resourceCulture);
|
||||
}
|
||||
|
@ -154,7 +154,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Pause.
|
||||
/// </summary>
|
||||
internal static string Pause {
|
||||
public static string Pause {
|
||||
get {
|
||||
return ResourceManager.GetString("Pause", resourceCulture);
|
||||
}
|
||||
|
@ -163,7 +163,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to 游玩次数:{0}.
|
||||
/// </summary>
|
||||
internal static string PlayCount {
|
||||
public static string PlayCount {
|
||||
get {
|
||||
return ResourceManager.GetString("PlayCount", resourceCulture);
|
||||
}
|
||||
|
@ -172,7 +172,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to SSS+ => DXRating += {0}.
|
||||
/// </summary>
|
||||
internal static string RatingUpWhenSSSp {
|
||||
public static string RatingUpWhenSSSp {
|
||||
get {
|
||||
return ResourceManager.GetString("RatingUpWhenSSSp", resourceCulture);
|
||||
}
|
||||
|
@ -181,7 +181,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Repeat end time cannot be less than repeat start time.
|
||||
/// </summary>
|
||||
internal static string RepeatEndTimeLessThenStartTime {
|
||||
public static string RepeatEndTimeLessThenStartTime {
|
||||
get {
|
||||
return ResourceManager.GetString("RepeatEndTimeLessThenStartTime", resourceCulture);
|
||||
}
|
||||
|
@ -190,7 +190,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Loop Not Set.
|
||||
/// </summary>
|
||||
internal static string RepeatNotSet {
|
||||
public static string RepeatNotSet {
|
||||
get {
|
||||
return ResourceManager.GetString("RepeatNotSet", resourceCulture);
|
||||
}
|
||||
|
@ -199,7 +199,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Reset.
|
||||
/// </summary>
|
||||
internal static string RepeatReset {
|
||||
public static string RepeatReset {
|
||||
get {
|
||||
return ResourceManager.GetString("RepeatReset", resourceCulture);
|
||||
}
|
||||
|
@ -208,7 +208,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Loop Set.
|
||||
/// </summary>
|
||||
internal static string RepeatStartEndSet {
|
||||
public static string RepeatStartEndSet {
|
||||
get {
|
||||
return ResourceManager.GetString("RepeatStartEndSet", resourceCulture);
|
||||
}
|
||||
|
@ -217,7 +217,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Loop Start Set.
|
||||
/// </summary>
|
||||
internal static string RepeatStartSet {
|
||||
public static string RepeatStartSet {
|
||||
get {
|
||||
return ResourceManager.GetString("RepeatStartSet", resourceCulture);
|
||||
}
|
||||
|
@ -226,7 +226,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Please set repeat start time first.
|
||||
/// </summary>
|
||||
internal static string RepeatStartTimeNotSet {
|
||||
public static string RepeatStartTimeNotSet {
|
||||
get {
|
||||
return ResourceManager.GetString("RepeatStartTimeNotSet", resourceCulture);
|
||||
}
|
||||
|
@ -235,7 +235,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Saving... Do not exit the game.
|
||||
/// </summary>
|
||||
internal static string SavingDontExit {
|
||||
public static string SavingDontExit {
|
||||
get {
|
||||
return ResourceManager.GetString("SavingDontExit", resourceCulture);
|
||||
}
|
||||
|
@ -244,7 +244,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Seek <<.
|
||||
/// </summary>
|
||||
internal static string SeekBackward {
|
||||
public static string SeekBackward {
|
||||
get {
|
||||
return ResourceManager.GetString("SeekBackward", resourceCulture);
|
||||
}
|
||||
|
@ -253,7 +253,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Seek >>.
|
||||
/// </summary>
|
||||
internal static string SeekForward {
|
||||
public static string SeekForward {
|
||||
get {
|
||||
return ResourceManager.GetString("SeekForward", resourceCulture);
|
||||
}
|
||||
|
@ -262,7 +262,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Skip.
|
||||
/// </summary>
|
||||
internal static string Skip {
|
||||
public static string Skip {
|
||||
get {
|
||||
return ResourceManager.GetString("Skip", resourceCulture);
|
||||
}
|
||||
|
@ -271,7 +271,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to > Skipping incompatible patch: {0}.
|
||||
/// </summary>
|
||||
internal static string SkipIncompatiblePatch {
|
||||
public static string SkipIncompatiblePatch {
|
||||
get {
|
||||
return ResourceManager.GetString("SkipIncompatiblePatch", resourceCulture);
|
||||
}
|
||||
|
@ -280,7 +280,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Speed.
|
||||
/// </summary>
|
||||
internal static string Speed {
|
||||
public static string Speed {
|
||||
get {
|
||||
return ResourceManager.GetString("Speed", resourceCulture);
|
||||
}
|
||||
|
@ -289,7 +289,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Speed -.
|
||||
/// </summary>
|
||||
internal static string SpeedDown {
|
||||
public static string SpeedDown {
|
||||
get {
|
||||
return ResourceManager.GetString("SpeedDown", resourceCulture);
|
||||
}
|
||||
|
@ -298,7 +298,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Speed Reset.
|
||||
/// </summary>
|
||||
internal static string SpeedReset {
|
||||
public static string SpeedReset {
|
||||
get {
|
||||
return ResourceManager.GetString("SpeedReset", resourceCulture);
|
||||
}
|
||||
|
@ -307,7 +307,7 @@ namespace AquaMai.Resources {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Speed +.
|
||||
/// </summary>
|
||||
internal static string SpeedUp {
|
||||
public static string SpeedUp {
|
||||
get {
|
||||
return ResourceManager.GetString("SpeedUp", resourceCulture);
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using AquaMai.Core.Attributes;
|
||||
using AquaMai.Core.Helpers;
|
||||
using AquaMai.Core.Resources;
|
||||
using MelonLoader;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Core;
|
||||
|
||||
public class Startup
|
||||
{
|
||||
private static HarmonyLib.Harmony _harmony;
|
||||
|
||||
private static bool _hasErrors;
|
||||
|
||||
private enum ModLifecycleMethod
|
||||
{
|
||||
// Invoked before all patches are applied, including core patches
|
||||
OnBeforeAllPatch,
|
||||
// Invoked after all patches are applied
|
||||
OnAfterAllPatch,
|
||||
// Invoked before the current patch is applied
|
||||
OnBeforePatch,
|
||||
// Invoked after the current patch is applied
|
||||
// Subclasses are treated as separate patches
|
||||
OnAfterPatch,
|
||||
// Invoked when an error occurs applying the current patch
|
||||
// Lifecycle methods' excpetions not included
|
||||
// Subclasses' error not included
|
||||
OnPatchError
|
||||
}
|
||||
|
||||
private static bool ShouldEnableImplicitly(Type type)
|
||||
{
|
||||
var implicitEnableAttribute = type.GetCustomAttribute<EnableImplicitlyIf>();
|
||||
if (implicitEnableAttribute == null) return false;
|
||||
var referenceField = type.GetField(implicitEnableAttribute.MemberName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
var referenceProperty = type.GetProperty(implicitEnableAttribute.MemberName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (referenceField == null && referenceProperty == null)
|
||||
{
|
||||
throw new ArgumentException($"Field or property {implicitEnableAttribute.MemberName} not found in {type.FullName}");
|
||||
}
|
||||
var referenceMemberValue = referenceField != null ? referenceField.GetValue(null) : referenceProperty.GetValue(null);
|
||||
if ((bool)referenceMemberValue)
|
||||
{
|
||||
MelonLogger.Msg($"Enabled {type.FullName} implicitly");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void InvokeLifecycleMethod(Type type, ModLifecycleMethod methodName)
|
||||
{
|
||||
var method = type.GetMethod(methodName.ToString(), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
|
||||
if (method == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var parameters = method.GetParameters();
|
||||
var arguments = parameters.Select(p =>
|
||||
{
|
||||
if (p.ParameterType == typeof(HarmonyLib.Harmony)) return _harmony;
|
||||
throw new InvalidOperationException($"Unsupported parameter type {p.ParameterType} in lifecycle method {type.FullName}.{methodName}");
|
||||
}).ToArray();
|
||||
try
|
||||
{
|
||||
method.Invoke(null, arguments);
|
||||
}
|
||||
catch (TargetInvocationException e)
|
||||
{
|
||||
MelonLogger.Error($"Failed to invoke lifecycle method {type.FullName}.{methodName}: {e.InnerException}");
|
||||
_hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectWantedPatches(List<Type> wantedPatches, Type type)
|
||||
{
|
||||
if (EnableConditionHelper.ShouldSkipClass(type))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
wantedPatches.Add(type);
|
||||
foreach (var nested in type.GetNestedTypes())
|
||||
{
|
||||
CollectWantedPatches(wantedPatches, nested);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyPatch(Type type)
|
||||
{
|
||||
MelonLogger.Msg($"> Applying {type}");
|
||||
try
|
||||
{
|
||||
InvokeLifecycleMethod(type, ModLifecycleMethod.OnBeforePatch);
|
||||
_harmony.PatchAll(type);
|
||||
InvokeLifecycleMethod(type, ModLifecycleMethod.OnAfterPatch);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
MelonLogger.Error($"Failed to patch {type}: {e}");
|
||||
InvokeLifecycleMethod(type, ModLifecycleMethod.OnPatchError);
|
||||
_hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveLocale()
|
||||
{
|
||||
var localeConfigEntry = ConfigLoader.Config.ReflectionManager.GetEntry("General.Locale");
|
||||
var localeValue = (string)ConfigLoader.Config.GetEntryState(localeConfigEntry).Value;
|
||||
return localeValue switch
|
||||
{
|
||||
"en" => localeValue,
|
||||
"zh" => localeValue,
|
||||
_ => Application.systemLanguage switch
|
||||
{
|
||||
SystemLanguage.Chinese or SystemLanguage.ChineseSimplified or SystemLanguage.ChineseTraditional => "zh",
|
||||
SystemLanguage.English => "en",
|
||||
_ => "en"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static void Initialize(Assembly modsAssembly, HarmonyLib.Harmony harmony)
|
||||
{
|
||||
MelonLogger.Msg("Loading mod settings...");
|
||||
|
||||
var configLoaded = ConfigLoader.LoadConfig(modsAssembly);
|
||||
var lang = ResolveLocale();
|
||||
if (configLoaded)
|
||||
{
|
||||
ConfigLoader.SaveConfig(lang); // Re-save the config as soon as possible
|
||||
}
|
||||
|
||||
_harmony = harmony;
|
||||
|
||||
// Init locale with patching C# runtime
|
||||
// https://stackoverflow.com/questions/1952638/single-assembly-multi-language-windows-forms-deployment-ilmerge-and-satellite-a
|
||||
ApplyPatch(typeof(I18nSingleAssemblyHook));
|
||||
Locale.Culture = CultureInfo.GetCultureInfo(lang); // Must be called after I18nSingleAssemblyHook patched
|
||||
|
||||
// The patch list is ordered
|
||||
List<Type> wantedPatches = [];
|
||||
|
||||
// Must be patched first to support [EnableIf(...)] and [EnableGameVersion(...)]
|
||||
CollectWantedPatches(wantedPatches, typeof(EnableConditionHelper));
|
||||
// Core helpers patched first
|
||||
CollectWantedPatches(wantedPatches, typeof(MessageHelper));
|
||||
CollectWantedPatches(wantedPatches, typeof(MusicDirHelper));
|
||||
CollectWantedPatches(wantedPatches, typeof(SharedInstances));
|
||||
CollectWantedPatches(wantedPatches, typeof(GuiSizes));
|
||||
CollectWantedPatches(wantedPatches, typeof(KeyListener));
|
||||
|
||||
// Collect patches based on the config
|
||||
var config = ConfigLoader.Config;
|
||||
foreach (var section in config.ReflectionManager.Sections)
|
||||
{
|
||||
var reflectionType = (Config.Reflection.SystemReflectionProvider.ReflectionType)section.Type;
|
||||
var type = reflectionType.UnderlyingType;
|
||||
if (!config.GetSectionState(section).Enabled && !ShouldEnableImplicitly(type)) continue;
|
||||
CollectWantedPatches(wantedPatches, type);
|
||||
}
|
||||
|
||||
foreach (var type in wantedPatches)
|
||||
{
|
||||
InvokeLifecycleMethod(type, ModLifecycleMethod.OnBeforeAllPatch);
|
||||
}
|
||||
foreach (var type in wantedPatches)
|
||||
{
|
||||
ApplyPatch(type);
|
||||
}
|
||||
foreach (var type in wantedPatches)
|
||||
{
|
||||
InvokeLifecycleMethod(type, ModLifecycleMethod.OnAfterAllPatch);
|
||||
}
|
||||
|
||||
if (_hasErrors)
|
||||
{
|
||||
MelonLogger.Warning("========================================================================!!!\n" + Locale.LoadError);
|
||||
MelonLogger.Warning("===========================================================================");
|
||||
}
|
||||
|
||||
# if CI
|
||||
MelonLogger.Warning(Locale.CiBuildAlertTitle);
|
||||
MelonLogger.Warning(Locale.CiBuildAlertContent);
|
||||
# endif
|
||||
|
||||
MelonLogger.Msg(Locale.Loaded);
|
||||
}
|
||||
|
||||
public static void OnGUI()
|
||||
{
|
||||
GuiSizes.SetupStyles();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{8731C0E0-53BE-4B1B-9828-193E738C6865}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<RootNamespace>AquaMai.Mods</RootNamespace>
|
||||
<AssemblyName>AquaMai.Mods</AssemblyName>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
<LangVersion>12</LangVersion>
|
||||
<NoWarn>414</NoWarn>
|
||||
<AssemblySearchPaths>$(ProjectDir)../Libs/;$(AssemblySearchPaths)</AssemblySearchPaths>
|
||||
<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/AquaMai.Config.csproj" />
|
||||
<ProjectReference Include="../AquaMai.Core/AquaMai.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="mscorlib" />
|
||||
<Reference Include="0Harmony" />
|
||||
<Reference Include="AMDaemon.NET" />
|
||||
<Reference Include="Assembly-CSharp" />
|
||||
<Reference Include="Assembly-CSharp-firstpass" />
|
||||
<Reference Include="MelonLoader" />
|
||||
<Reference Include="Mono.Cecil" />
|
||||
<Reference Include="Mono.Posix" />
|
||||
<Reference Include="Mono.Security" />
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Security" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="Unity.Analytics.DataPrivacy" />
|
||||
<Reference Include="Unity.TextMeshPro" />
|
||||
<Reference Include="UnityEngine" />
|
||||
<Reference Include="UnityEngine.AccessibilityModule" />
|
||||
<Reference Include="UnityEngine.AIModule" />
|
||||
<Reference Include="UnityEngine.AnimationModule" />
|
||||
<Reference Include="UnityEngine.ARModule" />
|
||||
<Reference Include="UnityEngine.AssetBundleModule" />
|
||||
<Reference Include="UnityEngine.AudioModule" />
|
||||
<Reference Include="UnityEngine.BaselibModule" />
|
||||
<Reference Include="UnityEngine.ClothModule" />
|
||||
<Reference Include="UnityEngine.ClusterInputModule" />
|
||||
<Reference Include="UnityEngine.ClusterRendererModule" />
|
||||
<Reference Include="UnityEngine.CoreModule" />
|
||||
<Reference Include="UnityEngine.CrashReportingModule" />
|
||||
<Reference Include="UnityEngine.DirectorModule" />
|
||||
<Reference Include="UnityEngine.FileSystemHttpModule" />
|
||||
<Reference Include="UnityEngine.GameCenterModule" />
|
||||
<Reference Include="UnityEngine.GridModule" />
|
||||
<Reference Include="UnityEngine.HotReloadModule" />
|
||||
<Reference Include="UnityEngine.ImageConversionModule" />
|
||||
<Reference Include="UnityEngine.IMGUIModule" />
|
||||
<Reference Include="UnityEngine.InputModule" />
|
||||
<Reference Include="UnityEngine.JSONSerializeModule" />
|
||||
<Reference Include="UnityEngine.LocalizationModule" />
|
||||
<Reference Include="UnityEngine.Networking" />
|
||||
<Reference Include="UnityEngine.ParticleSystemModule" />
|
||||
<Reference Include="UnityEngine.PerformanceReportingModule" />
|
||||
<Reference Include="UnityEngine.Physics2DModule" />
|
||||
<Reference Include="UnityEngine.PhysicsModule" />
|
||||
<Reference Include="UnityEngine.ProfilerModule" />
|
||||
<Reference Include="UnityEngine.ScreenCaptureModule" />
|
||||
<Reference Include="UnityEngine.SharedInternalsModule" />
|
||||
<Reference Include="UnityEngine.SpatialTracking" />
|
||||
<Reference Include="UnityEngine.SpriteMaskModule" />
|
||||
<Reference Include="UnityEngine.SpriteShapeModule" />
|
||||
<Reference Include="UnityEngine.StreamingModule" />
|
||||
<Reference Include="UnityEngine.StyleSheetsModule" />
|
||||
<Reference Include="UnityEngine.SubstanceModule" />
|
||||
<Reference Include="UnityEngine.TerrainModule" />
|
||||
<Reference Include="UnityEngine.TerrainPhysicsModule" />
|
||||
<Reference Include="UnityEngine.TextCoreModule" />
|
||||
<Reference Include="UnityEngine.TextRenderingModule" />
|
||||
<Reference Include="UnityEngine.TilemapModule" />
|
||||
<Reference Include="UnityEngine.Timeline" />
|
||||
<Reference Include="UnityEngine.TimelineModule" />
|
||||
<Reference Include="UnityEngine.TLSModule" />
|
||||
<Reference Include="UnityEngine.UI" />
|
||||
<Reference Include="UnityEngine.UIElementsModule" />
|
||||
<Reference Include="UnityEngine.UIModule" />
|
||||
<Reference Include="UnityEngine.UmbraModule" />
|
||||
<Reference Include="UnityEngine.UNETModule" />
|
||||
<Reference Include="UnityEngine.UnityAnalyticsModule" />
|
||||
<Reference Include="UnityEngine.UnityConnectModule" />
|
||||
<Reference Include="UnityEngine.UnityTestProtocolModule" />
|
||||
<Reference Include="UnityEngine.UnityWebRequestAssetBundleModule" />
|
||||
<Reference Include="UnityEngine.UnityWebRequestAudioModule" />
|
||||
<Reference Include="UnityEngine.UnityWebRequestModule" />
|
||||
<Reference Include="UnityEngine.UnityWebRequestTextureModule" />
|
||||
<Reference Include="UnityEngine.UnityWebRequestWWWModule" />
|
||||
<Reference Include="UnityEngine.VehiclesModule" />
|
||||
<Reference Include="UnityEngine.VFXModule" />
|
||||
<Reference Include="UnityEngine.VideoModule" />
|
||||
<Reference Include="UnityEngine.VRModule" />
|
||||
<Reference Include="UnityEngine.WindModule" />
|
||||
<Reference Include="UnityEngine.XRModule" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Numerics" Private="true" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,29 @@
|
|||
using AquaMai.Config.Attributes;
|
||||
|
||||
namespace AquaMai.Mods;
|
||||
|
||||
[ConfigSection(
|
||||
en: """
|
||||
These options have been deprecated and no longer work in the current version.
|
||||
Remove them to get rid of the warning message at startup.
|
||||
""",
|
||||
zh: """
|
||||
这些配置项已经被废弃,在当前版本不再生效
|
||||
删除它们以去除启动时的警告信息
|
||||
""",
|
||||
exampleHidden: true)]
|
||||
public class DeprecationWarning
|
||||
{
|
||||
[ConfigEntry(hideWhenDefault: true)]
|
||||
public static readonly bool v1_0_ModKeyMap_TestMode;
|
||||
|
||||
// Print friendly warning messages here.
|
||||
// Please keep them up-to-date while refactoring the config.
|
||||
public static void OnBeforeAllPatch()
|
||||
{
|
||||
if (v1_0_ModKeyMap_TestMode)
|
||||
{
|
||||
MelonLoader.MelonLogger.Warning("ModKeyMap.TestMode has been deprecated (> v1.0). Please use GameSystem.KeyMap.Test instead.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +1,37 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Core.Helpers;
|
||||
using HarmonyLib;
|
||||
using Monitor;
|
||||
using Process;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace AquaMai.Visual;
|
||||
namespace AquaMai.Mods.Fancy;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Replace the \"SEGA\" and \"ALL.Net\" logos with custom ones.",
|
||||
zh: "用自定义的图片替换「SEGA」和「ALL.Net」的标志")]
|
||||
public class CustomLogo
|
||||
{
|
||||
private static List<Sprite> segaLogo = new();
|
||||
private static List<Sprite> allNetLogo = new();
|
||||
[ConfigEntry(
|
||||
en: "Replace the \"SEGA\" logo with a random PNG image from this directory.",
|
||||
zh: "从此目录中随机选择一张 PNG 图片用于「SEGA」标志")]
|
||||
private static readonly string segaLogoDir = "LocalAssets/SegaLogo";
|
||||
|
||||
public static void DoCustomPatch(HarmonyLib.Harmony h)
|
||||
[ConfigEntry(
|
||||
en: "Replace the \"ALL.Net\" logo with a random PNG image from this directory.",
|
||||
zh: "从此目录中随机选择一张 PNG 图片用于「ALL.Net」标志")]
|
||||
private static readonly string allNetLogoDir = "LocalAssets/AllNetLogo";
|
||||
|
||||
private readonly static List<Sprite> segaLogo = [];
|
||||
private readonly static List<Sprite> allNetLogo = [];
|
||||
|
||||
public static void OnBeforePatch()
|
||||
{
|
||||
EnumSprite(segaLogo, Path.Combine(Environment.CurrentDirectory, "LocalAssets", "SegaLogo"));
|
||||
EnumSprite(allNetLogo, Path.Combine(Environment.CurrentDirectory, "LocalAssets", "AllNetLogo"));
|
||||
EnumSprite(segaLogo, FileSystem.ResolvePath(segaLogoDir));
|
||||
EnumSprite(allNetLogo, FileSystem.ResolvePath(allNetLogoDir));
|
||||
}
|
||||
|
||||
private static void EnumSprite(List<Sprite> collection, string path)
|
|
@ -0,0 +1,45 @@
|
|||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
|
||||
namespace AquaMai.Mods.Fancy;
|
||||
|
||||
[ConfigSection(
|
||||
en: """
|
||||
Custom shop name in photo.
|
||||
Also enable shop name display in SDGA.
|
||||
""",
|
||||
zh: """
|
||||
自定义拍照的店铺名称
|
||||
同时在 SDGA 中会启用店铺名称的显示(但是不会在游戏里有设置)
|
||||
""")]
|
||||
public class CustomPlaceName
|
||||
{
|
||||
[ConfigEntry]
|
||||
private static readonly string placeName = "";
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(OperationManager), "CheckAuth_Proc")]
|
||||
public static void CheckAuth_Proc(OperationManager __instance)
|
||||
{
|
||||
if (string.IsNullOrEmpty(placeName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
__instance.ShopData.ShopName = placeName;
|
||||
__instance.ShopData.ShopNickName = placeName;
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(ResultCardBaseController), "Initialize")]
|
||||
public static void Initialize(ResultCardBaseController __instance)
|
||||
{
|
||||
if (string.IsNullOrEmpty(placeName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
__instance.SetVisibleStoreName(true);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Core.Helpers;
|
||||
using HarmonyLib;
|
||||
using MelonLoader;
|
||||
using Monitor;
|
||||
|
@ -8,16 +9,28 @@ using Monitor.Game;
|
|||
using Process;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Visual;
|
||||
namespace AquaMai.Mods.Fancy;
|
||||
|
||||
[ConfigSection(
|
||||
en: """
|
||||
Provide the ability to use custom skins (advanced feature).
|
||||
Load skin textures from custom paths.
|
||||
""",
|
||||
zh: """
|
||||
提供自定义皮肤的能力(高级功能)
|
||||
从自定义路径中加载皮肤贴图
|
||||
""")]
|
||||
public class CustomSkins
|
||||
{
|
||||
[ConfigEntry]
|
||||
private static readonly string skinsDir = "LocalAssets/Skins";
|
||||
|
||||
private static readonly List<string> ImageExts = [".png", ".jpg", ".jpeg"];
|
||||
private static readonly List<string> SlideFanFields = ["_normalSlideFan", "_eachSlideFan", "_breakSlideFan", "_breakSlideFanEff"];
|
||||
private static readonly List<string> CustomTrackStartFields = ["_musicBase", "_musicTab", "_musicLvBase", "_musicLvText"];
|
||||
|
||||
private static Sprite customOutline;
|
||||
private static Sprite[,] customSlideFan = new Sprite[4, 11];
|
||||
private readonly static Sprite[,] customSlideFan = new Sprite[4, 11];
|
||||
|
||||
public static readonly Sprite[,] CustomJudge = new Sprite[2, ((int)NoteJudge.ETiming.End + 1)];
|
||||
public static readonly Sprite[,,,] CustomJudgeSlide = new Sprite[2, 3, 2, ((int)NoteJudge.ETiming.End + 1)];
|
||||
|
@ -95,9 +108,10 @@ public class CustomSkins
|
|||
[HarmonyPatch(typeof(GameNotePrefabContainer), "Initialize")]
|
||||
private static void LoadNoteSkin()
|
||||
{
|
||||
if (!Directory.Exists(Path.Combine(Environment.CurrentDirectory, "LocalAssets", "Skins"))) return;
|
||||
var resolvedDir = FileSystem.ResolvePath(skinsDir);
|
||||
if (!Directory.Exists(resolvedDir)) return;
|
||||
|
||||
foreach (var laFile in Directory.EnumerateFiles(Path.Combine(Environment.CurrentDirectory, "LocalAssets", "Skins")))
|
||||
foreach (var laFile in Directory.EnumerateFiles(resolvedDir))
|
||||
{
|
||||
if (!ImageExts.Contains(Path.GetExtension(laFile).ToLowerInvariant())) continue;
|
||||
var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
|
|
@ -1,12 +1,24 @@
|
|||
using System.Collections.Generic;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Monitor;
|
||||
using UI;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace AquaMai.Visual;
|
||||
namespace AquaMai.Mods.Fancy;
|
||||
|
||||
[ConfigSection(
|
||||
en: """
|
||||
Custom track start difficulty image (not really custom difficulty).
|
||||
Requires CustomSkins to be enabled.
|
||||
Will load four image resources through custom skins: musicBase, musicTab, musicLvBase, musicLvText.
|
||||
""",
|
||||
zh: """
|
||||
自定义在歌曲开始界面上显示的难度贴图 (并不是真的自定义难度)
|
||||
需要启用自定义皮肤功能
|
||||
会通过自定义皮肤加载四个图片资源: musicBase, musicTab, musicLvBase, musicLvText
|
||||
""")]
|
||||
public class CustomTrackStartDiff
|
||||
{
|
||||
// 自定义在歌曲开始界面上显示的难度 (并不是真的自定义难度)
|
|
@ -0,0 +1,30 @@
|
|||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace AquaMai.Mods.Fancy;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Set the version string displayed at the top-right corner of the screen.",
|
||||
zh: "把右上角的版本更改为自定义文本")]
|
||||
public class CustomVersionString
|
||||
{
|
||||
[ConfigEntry]
|
||||
private static readonly string versionString = "";
|
||||
|
||||
/*
|
||||
* Patch displayVersionString Property Getter
|
||||
*/
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MAI2System.Config), "displayVersionString", MethodType.Getter)]
|
||||
public static bool GetDisplayVersionString(ref string __result)
|
||||
{
|
||||
if (string.IsNullOrEmpty(versionString))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
__result = versionString;
|
||||
// Return false to block the original method
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
using AquaMai.Config.Attributes;
|
||||
using DB;
|
||||
using HarmonyLib;
|
||||
using MAI2.Util;
|
||||
using Manager;
|
||||
using Process;
|
||||
|
||||
namespace AquaMai.Mods.Fancy;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Play \"Master\" difficulty on Demo screen.",
|
||||
zh: "在闲置时的演示画面上播放紫谱而不是绿谱")]
|
||||
public class DemoMaster
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(AdvDemoProcess), "OnStart")]
|
||||
public static void AdvDemoProcessPostStart()
|
||||
{
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
var userOption = Singleton<GamePlayManager>.Instance.GetGameScore(i).UserOption;
|
||||
userOption.NoteSpeed = OptionNotespeedID.Speed6_5;
|
||||
userOption.TouchSpeed = OptionTouchspeedID.Speed7_0;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(GamePlayManager), "InitializeAdvertise")]
|
||||
public static void PreInitializeAdvertise()
|
||||
{
|
||||
GameManager.SelectDifficultyID[0] = 3;
|
||||
GameManager.SelectDifficultyID[1] = 3;
|
||||
}
|
||||
}
|
|
@ -1,16 +1,25 @@
|
|||
using System;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
using Monitor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Visual;
|
||||
namespace AquaMai.Mods.Fancy.GamePlay;
|
||||
|
||||
public class FixCircleSlideJudge
|
||||
[ConfigSection(
|
||||
en: """
|
||||
Make the judgment display of circular Slides align precisely with the judgment line (originally a bit off).
|
||||
Just like in majdata.
|
||||
""",
|
||||
zh: """
|
||||
让圆弧形的 Slide 的判定显示与判定线精确对齐 (原本会有一点歪)
|
||||
就像 majdata 里那样
|
||||
""")]
|
||||
public class AlignCircleSlideJudgeDisplay
|
||||
{
|
||||
/*
|
||||
* 这个 Patch 让圆弧形的 Slide 的判定显示与判定线精确对齐 (原本会有一点歪), 就像 majdata 里那样
|
||||
* 我觉得这个 Patch 算是无副作用的, 可以默认开启
|
||||
*/
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(SlideRoot), "Initialize")]
|
|
@ -1,9 +1,19 @@
|
|||
using HarmonyLib;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Monitor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Visual;
|
||||
namespace AquaMai.Mods.Fancy.GamePlay;
|
||||
|
||||
[ConfigSection(
|
||||
en: """
|
||||
This Patch makes the Critical judgment of BreakSlide also flicker like BreakTap.
|
||||
Recommended to use with custom skins (otherwise the visual effect may not be good).
|
||||
""",
|
||||
zh: """
|
||||
这个 Patch 让 BreakSlide 的 Critical 判定也可以像 BreakTap 一样闪烁
|
||||
推荐与自定义皮肤一起使用 (否则视觉效果可能并不好)
|
||||
""")]
|
||||
public class BreakSlideJudgeBlink
|
||||
{
|
||||
/*
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
using AquaMai.Config.Attributes;
|
||||
using DB;
|
||||
using HarmonyLib;
|
||||
using MAI2.Util;
|
||||
|
@ -10,10 +11,16 @@ using Manager;
|
|||
using MelonLoader;
|
||||
using Monitor;
|
||||
using UnityEngine;
|
||||
using AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs;
|
||||
|
||||
namespace AquaMai.MaimaiDX2077;
|
||||
namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes;
|
||||
|
||||
public class CustomNoteTypePatch
|
||||
[ConfigCollapseNamespace]
|
||||
[ConfigSection(
|
||||
en: "Custom Note Types.",
|
||||
zh: "自定义 Note 类型"
|
||||
)]
|
||||
public class CustomNoteTypes
|
||||
{
|
||||
/*
|
||||
* ========== ========== ========== ========== ========== ========== ========== ==========
|
||||
|
@ -36,12 +43,12 @@ public class CustomNoteTypePatch
|
|||
public static int TotalMa2RecordCount = -1;
|
||||
public static int LastMa2RecordID = -1;
|
||||
public static Array Ma2FileRecordData;
|
||||
|
||||
public static void DoCustomPatch(HarmonyLib.Harmony h)
|
||||
|
||||
public static void OnAfterPatch()
|
||||
{
|
||||
var arrayTraverse = Traverse.Create(typeof(Ma2fileRecordID)).Field("s_Ma2fileRecord_Data");
|
||||
var targetArray = arrayTraverse.GetValue<Array>();
|
||||
|
||||
|
||||
var nextId = targetArray.Length;
|
||||
object[][] newEntries =
|
||||
[
|
||||
|
@ -51,7 +58,7 @@ public class CustomNoteTypePatch
|
|||
[nextId++, "BXSSS", "过新过热ExBreakSlide", NotesTypeID.Def.ExBreakSlide, SlideType.Slide_MAX, 8, Ma2Category.MA2_Note, 2, 2, 2, 2, 2, 2, 0],
|
||||
[nextId++, "CNSSS", "过新过热ConnSlide", NotesTypeID.Def.ConnectSlide, SlideType.Slide_MAX, 8, Ma2Category.MA2_Note, 2, 2, 2, 2, 2, 2, 0],
|
||||
];
|
||||
|
||||
|
||||
// Ma2fileRecordID.Ma2fileRecord_Data is private, so we need this shit.
|
||||
var structType = targetArray.GetValue(0).GetType();
|
||||
var constructor = AccessTools.Constructor(structType,
|
||||
|
@ -60,7 +67,7 @@ public class CustomNoteTypePatch
|
|||
typeof(Ma2Category), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int),
|
||||
typeof(int)
|
||||
]);
|
||||
|
||||
|
||||
Ma2FileRecordData = Array.CreateInstance(structType, targetArray.Length + newEntries.Length);
|
||||
for (var i = 0; i < targetArray.Length; i++)
|
||||
{
|
||||
|
@ -73,12 +80,12 @@ public class CustomNoteTypePatch
|
|||
var obj = constructor.Invoke(newEntries[i]);
|
||||
Ma2FileRecordData.SetValue(obj, j);
|
||||
}
|
||||
|
||||
|
||||
arrayTraverse.SetValue(Ma2FileRecordData);
|
||||
TotalMa2RecordCount = Ma2FileRecordData.Length;
|
||||
LastMa2RecordID = TotalMa2RecordCount - 1;
|
||||
MelonLogger.Msg($"[CustomNoteType] MA2 record data extended, total count: {TotalMa2RecordCount}");
|
||||
|
||||
|
||||
// Initialize related classes ...
|
||||
SlideDataBuilder.InitializeHitAreasLookup();
|
||||
MelonLogger.Msg($"[CustomNoteType] HitAreasLookup initialized, total count: {SlideDataBuilder.HitAreasLookup.Count}");
|
||||
|
@ -97,10 +104,10 @@ public class CustomNoteTypePatch
|
|||
var item = Ma2FileRecordData.GetValue(i);
|
||||
if (Traverse.Create(item).Field<string>("enumName").Value == enumName)
|
||||
{
|
||||
__result = (Ma2fileRecordID.Def) i;
|
||||
__result = (Ma2fileRecordID.Def)i;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -118,20 +125,20 @@ public class CustomNoteTypePatch
|
|||
AccessTools.Method(typeof(Ma2fileRecordID_Extension), "isValid"),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
|
||||
{
|
||||
|
||||
|
||||
foreach (var inst in instructions)
|
||||
{
|
||||
if (inst.LoadsConstant(142))
|
||||
{
|
||||
var instNew = new CodeInstruction(OpCodes.Ldsfld, AccessTools.Field(typeof(CustomNoteTypePatch), "TotalMa2RecordCount"));
|
||||
var instNew = new CodeInstruction(OpCodes.Ldsfld, AccessTools.Field(typeof(CustomNoteTypes), "TotalMa2RecordCount"));
|
||||
yield return instNew;
|
||||
}
|
||||
else if (inst.LoadsConstant(141))
|
||||
{
|
||||
var instNew = new CodeInstruction(OpCodes.Ldsfld, AccessTools.Field(typeof(CustomNoteTypePatch), "LastMa2RecordID"));
|
||||
var instNew = new CodeInstruction(OpCodes.Ldsfld, AccessTools.Field(typeof(CustomNoteTypes), "LastMa2RecordID"));
|
||||
yield return instNew;
|
||||
}
|
||||
else
|
||||
|
@ -146,7 +153,7 @@ public class CustomNoteTypePatch
|
|||
* ========== ========== ========== ========== ========== ========== ========== ==========
|
||||
* 以下内容是给新的 MA2 语法写解析器
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* 给新建的 noteData 初始化应有的数据, 仅仅是照搬了 NotesReader.loadNote
|
||||
*/
|
||||
|
@ -156,7 +163,7 @@ public class CustomNoteTypePatch
|
|||
noteData.type = record.getType().getNotesTypeId();
|
||||
noteData.time.init(record.getBar(), record.getGrid(), reader);
|
||||
noteData.end = noteData.time;
|
||||
noteData.startButtonPos = MaiGeometry.MirrorInfo[(int) mirrorMode, record.getPos()];
|
||||
noteData.startButtonPos = MaiGeometry.MirrorInfo[(int)mirrorMode, record.getPos()];
|
||||
noteData.index = index;
|
||||
var num = record.getGrid() % 96;
|
||||
if (num == 0)
|
||||
|
@ -182,7 +189,7 @@ public class CustomNoteTypePatch
|
|||
noteData.indexNote = noteIndex;
|
||||
++noteIndex;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* 给新建的 noteData 填入基本的 slide 相关数据, 仅仅是照搬了 NotesReader.loadNote
|
||||
*/
|
||||
|
@ -193,7 +200,7 @@ public class CustomNoteTypePatch
|
|||
var slideData = noteData.slideData;
|
||||
var slideWaitLen = record.getSlideWaitLen();
|
||||
var slideShootLen = record.getSlideShootLen();
|
||||
slideData.targetNote = MaiGeometry.MirrorInfo[(int) mirrorMode, record.getSlideEndPos()];
|
||||
slideData.targetNote = MaiGeometry.MirrorInfo[(int)mirrorMode, record.getSlideEndPos()];
|
||||
slideData.shoot.time.init(record.getBar(), record.getGrid() + slideWaitLen, reader);
|
||||
slideData.shoot.index = noteIndex;
|
||||
slideData.arrive.time.init(record.getBar(), record.getGrid() + slideWaitLen + slideShootLen, reader);
|
||||
|
@ -211,7 +218,7 @@ public class CustomNoteTypePatch
|
|||
// builtin record type
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
MelonLogger.Msg($"[CustomNoteType] Custom note | {rec._str.Count} | {rec.getStr(0)} {rec.getStr(1)} {rec.getStr(2)} {rec.getStr(3)} {rec.getStr(4)} {rec.getStr(5)} {rec.getStr(6)} {rec.getStr(7)} {rec.getStr(8)}");
|
||||
|
||||
var flag = true;
|
||||
|
@ -243,13 +250,13 @@ public class CustomNoteTypePatch
|
|||
__result = flag;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* ========== ========== ========== ========== ========== ========== ========== ==========
|
||||
* 以下内容是为了实现自定义 Slide
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* 把 GetSlidePath 和 GetSlideHitArea 和 GetSlideLength 重定向到我可以控制的函数上, 并且多推几个参数进来
|
||||
*/
|
||||
|
@ -266,21 +273,21 @@ public class CustomNoteTypePatch
|
|||
AccessTools.Method(typeof(BreakStarNote), "Initialize"),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
|
||||
{
|
||||
var methodGetSlidePath = AccessTools.Method(typeof(SlideManager), "GetSlidePath");
|
||||
var methodGetSlidePathRedirect = AccessTools.Method(typeof(CustomNoteTypePatch), "GetSlidePathRedirect");
|
||||
var methodGetSlidePathRedirect = AccessTools.Method(typeof(CustomNoteTypes), "GetSlidePathRedirect");
|
||||
var methodGetSlideHitArea = AccessTools.Method(typeof(SlideManager), "GetSlideHitArea");
|
||||
var methodGetSlideHitAreaRedirect = AccessTools.Method(typeof(CustomNoteTypePatch), "GetSlideHitAreaRedirect");
|
||||
var methodGetSlideHitAreaRedirect = AccessTools.Method(typeof(CustomNoteTypes), "GetSlideHitAreaRedirect");
|
||||
var methodGetSlideLength = AccessTools.Method(typeof(SlideManager), "GetSlideLength");
|
||||
var methodGetSlideLengthRedirect = AccessTools.Method(typeof(CustomNoteTypePatch), "GetSlideLengthRedirect");
|
||||
var methodGetSlideLengthRedirect = AccessTools.Method(typeof(CustomNoteTypes), "GetSlideLengthRedirect");
|
||||
var fieldSlideData = AccessTools.Field(typeof(NoteData), "slideData");
|
||||
|
||||
var oldInstList = new List<CodeInstruction>(instructions);
|
||||
var newInstList = new List<CodeInstruction>();
|
||||
CodeInstruction instToInject = null;
|
||||
|
||||
|
||||
for (var i = 0; i < oldInstList.Count; ++i)
|
||||
{
|
||||
var inst = oldInstList[i];
|
||||
|
@ -295,7 +302,7 @@ public class CustomNoteTypePatch
|
|||
// 所以就记录上一次 ldfld NoteData::slideData 的位置, 往前找一个 IL code
|
||||
// 找到的就是 load 这个 noteData 的位置
|
||||
// 然后在后续调用 GetSlidePath 时, 先重复一遍 load 把这个 noteData 入栈, 然后重定向到一个新的函数上去
|
||||
instToInject = oldInstList[i-1];
|
||||
instToInject = oldInstList[i - 1];
|
||||
newInstList.Add(inst);
|
||||
}
|
||||
else if (inst.Calls(methodGetSlidePath))
|
||||
|
@ -324,7 +331,7 @@ public class CustomNoteTypePatch
|
|||
return newInstList;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static List<Vector4> GetSlidePathRedirect(SlideManager instance, SlideType slideType, int start, int end,
|
||||
int starButton, NoteData noteData)
|
||||
|
@ -351,7 +358,7 @@ public class CustomNoteTypePatch
|
|||
}
|
||||
return instance.GetSlideHitArea(slideType, start, end, starButton);
|
||||
}
|
||||
|
||||
|
||||
public static float GetSlideLengthRedirect(SlideManager instance, SlideType slideType,
|
||||
int start, int end, NoteData noteData)
|
||||
{
|
||||
|
@ -364,9 +371,9 @@ public class CustomNoteTypePatch
|
|||
}
|
||||
return instance.GetSlideLength(slideType, start, end);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[HarmonyPatch]
|
||||
public static class Debuging
|
||||
{
|
||||
|
@ -383,7 +390,7 @@ public class CustomNoteTypePatch
|
|||
// AccessTools.Method(typeof(SlideJudge), "SetJudgeType"),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public static void Prefix(MethodBase __originalMethod, object[] __args)
|
||||
{
|
||||
var msg = "[CustomNoteType] Before ";
|
||||
|
@ -419,8 +426,8 @@ public class CustomNoteTypePatch
|
|||
{
|
||||
return $"<NoteData {data2.indexNote} {data2.indexSlide}>";
|
||||
}
|
||||
|
||||
|
||||
return value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ using Manager;
|
|||
using MelonLoader;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.MaimaiDX2077;
|
||||
namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs;
|
||||
|
||||
public class CustomSlideNoteData: NoteData
|
||||
{
|
||||
|
@ -48,4 +48,4 @@ public class CustomSlideNoteData: NoteData
|
|||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Manager;
|
||||
|
||||
namespace AquaMai.MaimaiDX2077;
|
||||
namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs;
|
||||
|
||||
public static class MaiGeometry
|
||||
{
|
||||
|
@ -106,4 +104,4 @@ public static class MaiGeometry
|
|||
return new Tuple<CircleStruct, double, double>(new CircleStruct(center, TransferRadius),
|
||||
Math.IEEERemainder(startAngle, Math.PI * 2), Math.IEEERemainder(endAngle, Math.PI * 2));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ using System.Numerics;
|
|||
using DB;
|
||||
using Manager;
|
||||
|
||||
namespace AquaMai.MaimaiDX2077;
|
||||
namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs;
|
||||
|
||||
public class ParametricSlidePath
|
||||
{
|
||||
|
@ -223,4 +223,4 @@ public class ParametricSlidePath
|
|||
|
||||
return SlideType.Slide_Straight;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ using System.Linq;
|
|||
using System.Numerics;
|
||||
using MelonLoader;
|
||||
|
||||
namespace AquaMai.MaimaiDX2077;
|
||||
namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs;
|
||||
|
||||
public static class SlideCodeParser
|
||||
{
|
||||
|
@ -257,4 +257,4 @@ public static class SlideCodeParser
|
|||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,11 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using DB;
|
||||
using Manager;
|
||||
using MelonLoader;
|
||||
using Vector4 = UnityEngine.Vector4;
|
||||
|
||||
namespace AquaMai.MaimaiDX2077;
|
||||
namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs;
|
||||
|
||||
public static class SlideDataBuilder
|
||||
{
|
||||
|
@ -472,4 +470,4 @@ public static class SlideDataBuilder
|
|||
}
|
||||
return hitAreaList;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AquaMai.MaimaiDX2077;
|
||||
namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs;
|
||||
|
||||
public class SlidePathGenerator
|
||||
{
|
||||
|
@ -129,4 +128,4 @@ public class SlidePathGenerator
|
|||
{
|
||||
return new ParametricSlidePath(PathSegments);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,21 @@
|
|||
using HarmonyLib;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Monitor;
|
||||
using TMPro;
|
||||
using UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Visual;
|
||||
namespace AquaMai.Mods.Fancy.GamePlay;
|
||||
|
||||
[ConfigSection(
|
||||
en: """
|
||||
Disable the TRACK X text, DX/Standard display box, and the derakkuma at the bottom of the screen in the song start screen.
|
||||
For recording chart confirmation.
|
||||
""",
|
||||
zh: """
|
||||
在歌曲开始界面, 把 TRACK X 字样, DX/标准谱面的显示框, 以及画面下方的滴蜡熊隐藏掉
|
||||
录制谱面确认用
|
||||
""")]
|
||||
public class DisableTrackStartTabs
|
||||
{
|
||||
// 在歌曲开始界面, 把 TRACK X 字样, DX/标准谱面的显示框, 以及画面下方的滴蜡熊隐藏掉, 让他看起来不那么 sinmai, 更像是 majdata
|
|
@ -1,18 +1,26 @@
|
|||
using System.Collections.Generic;
|
||||
using AquaMai.Attributes;
|
||||
using AquaMai.Core.Attributes;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using MAI2.Util;
|
||||
using Manager;
|
||||
using MelonLoader;
|
||||
using Monitor;
|
||||
using Monitor.Game;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Fix;
|
||||
namespace AquaMai.Mods.Fancy.GamePlay;
|
||||
|
||||
[GameVersion(23000)]
|
||||
[ConfigSection(
|
||||
en: "Add notes sprite to the pool to prevent use up.",
|
||||
zh: "增加更多待命的音符贴图,防止奇怪的自制谱用完音符贴图池")]
|
||||
[EnableGameVersion(23000)]
|
||||
public class ExtendNotesPool
|
||||
{
|
||||
[ConfigEntry(
|
||||
en: "Number of objects to add.",
|
||||
zh: "要增加的对象数量")]
|
||||
private readonly static int count = 128;
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(GameCtrl), "CreateNotePool")]
|
||||
public static void CreateNotePool(ref GameCtrl __instance,
|
||||
|
@ -33,7 +41,7 @@ public class ExtendNotesPool
|
|||
List<SpriteRenderer> ____arrowObjectList, List<BreakSlide> ____breakArrowObjectList
|
||||
)
|
||||
{
|
||||
for (var i = 0; i < AquaMai.AppConfig.Fix.ExtendNotesPool; i++)
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var tapNote = Object.Instantiate(GameNotePrefabContainer.Tap, ____tapListParent.transform);
|
||||
tapNote.gameObject.SetActive(false);
|
|
@ -0,0 +1,42 @@
|
|||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Monitor;
|
||||
|
||||
namespace AquaMai.Mods.Fancy.GamePlay;
|
||||
|
||||
[ConfigSection(
|
||||
en: """
|
||||
Make the judgment display of WiFi Slide different in up and down (originally all WiFi judgment displays are towards the center), just like in majdata.
|
||||
The reason for this bug is that SEGA forgot to assign EndButtonId to WiFi.
|
||||
""",
|
||||
zh: """
|
||||
这个 Patch 让 WiFi Slide 的判定显示有上下的区别 (原本所有 WiFi 的判定显示都是朝向圆心的), 就像 majdata 里那样
|
||||
这个 bug 产生的原因是 SBGA 忘记给 WiFi 的 EndButtonId 赋值了
|
||||
""")]
|
||||
public class FanJudgeFlip
|
||||
{
|
||||
/*
|
||||
* 这个 Patch 让 WiFi Slide 的判定显示有上下的区别 (原本所有 WiFi 的判定显示都是朝向圆心的), 就像 majdata 里那样
|
||||
* 这个 bug 产生的原因是 SBGA 忘记给 WiFi 的 EndButtonId 赋值了
|
||||
* 不过需要注意的是, 考虑到圆弧形 Slide 的判定显示就是永远朝向圆心的, 我个人会觉得这个 Patch 关掉更好看一点
|
||||
*/
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(SlideFan), "Initialize")]
|
||||
private static void FixFanJudgeFilp(
|
||||
int[] ___GoalButtonId, SlideJudge ___JudgeObj
|
||||
)
|
||||
{
|
||||
if (null != ___JudgeObj)
|
||||
{
|
||||
if (2 <= ___GoalButtonId[1] && ___GoalButtonId[1] <= 5)
|
||||
{
|
||||
___JudgeObj.Flip(false);
|
||||
___JudgeObj.transform.Rotate(0.0f, 0.0f, 180f);
|
||||
}
|
||||
else
|
||||
{
|
||||
___JudgeObj.Flip(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,14 @@
|
|||
using Fx;
|
||||
using AquaMai.Config.Attributes;
|
||||
using Fx;
|
||||
using HarmonyLib;
|
||||
using Monitor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.UX;
|
||||
namespace AquaMai.Mods.Fancy.GamePlay;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Hide hanabi completely.",
|
||||
zh: "完全隐藏烟花")]
|
||||
public class HideHanabi
|
||||
{
|
||||
[HarmonyPatch(typeof(TapCEffect), "SetUpParticle")]
|
|
@ -1,9 +1,21 @@
|
|||
using HarmonyLib;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Monitor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Visual;
|
||||
namespace AquaMai.Mods.Fancy.GamePlay;
|
||||
|
||||
[ConfigSection(
|
||||
en: """
|
||||
More detailed judgment display.
|
||||
Requires CustomSkins to be enabled and the resource file to be downloaded.
|
||||
https://github.com/hykilpikonna/AquaDX/releases/download/nightly/JudgeDisplay4B.7z
|
||||
""",
|
||||
zh: """
|
||||
更精细的判定表示
|
||||
需开启 CustomSkins 并下载资源文件
|
||||
https://github.com/hykilpikonna/AquaDX/releases/download/nightly/JudgeDisplay4B.7z
|
||||
""")]
|
||||
public class JudgeDisplay4B
|
||||
{
|
||||
// 精确到子判定的自定义判定显示, 需要启用自定义皮肤功能 (理论上不启用自定义皮肤不会崩游戏, 只不过此时这个功能显然不会生效)
|
|
@ -0,0 +1,39 @@
|
|||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
|
||||
namespace AquaMai.Mods.Fancy.GamePlay;
|
||||
|
||||
[ConfigSection(
|
||||
en: """
|
||||
Make the AutoPlay random judgment mode really randomize all judgments (down to sub-judgments).
|
||||
The original random judgment will only produce all 15 judgment results from Miss(TooFast) ~ Critical ~ Miss(TooLate).
|
||||
Here, it is changed to a triangular distribution to produce all 15 judgment results from Miss(TooFast) ~ Critical ~ Miss(TooLate).
|
||||
Of course, it will not consider whether the original Note really has a corresponding judgment (such as Slide should not have non-Critical Prefect).
|
||||
""",
|
||||
zh: """
|
||||
让 AutoPlay 的随机判定模式真的会随机产生所有的判定 (精确到子判定)
|
||||
原本的随机判定只会等概率产生 Critical, LateGreat1st, LateGood, Miss(TooLate)
|
||||
这里改成三角分布产生从 Miss(TooFast) ~ Critical ~ Miss(TooLate) 的所有 15 种判定结果
|
||||
当然, 此处并不会考虑原本那个 Note 是不是真的有对应的判定 (比如 Slide 实际上不应该有小 p 之类的)
|
||||
""")]
|
||||
public class RealisticRandomJudge
|
||||
{
|
||||
// 让 AutoPlay 的随机判定模式真的会随机产生所有的判定 (精确到子判定)
|
||||
// 原本的随机判定只会等概率产生 Critical, LateGreat1st, LateGood, Miss(TooLate)
|
||||
// 这里改成三角分布产生从 Miss(TooFast) ~ Critical ~ Miss(TooLate) 的所有 15 种判定结果
|
||||
// 当然, 此处并不会考虑原本那个 Note 是不是真的有对应的判定 (比如 Slide 实际上不应该有小 p 之类的)
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(GameManager), "AutoJudge")]
|
||||
private static NoteJudge.ETiming RealAutoJudgeRandom(NoteJudge.ETiming retval)
|
||||
{
|
||||
if (GameManager.AutoPlay == GameManager.AutoPlayMode.Random)
|
||||
{
|
||||
var x = UnityEngine.Random.Range(0, 8);
|
||||
x += UnityEngine.Random.Range(0, 8);
|
||||
return (NoteJudge.ETiming) x;
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
using Monitor;
|
||||
|
@ -7,8 +7,11 @@ using Monitor.Game;
|
|||
using Process;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Visual;
|
||||
namespace AquaMai.Mods.Fancy.GamePlay;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Make the Slide Track disappear with an inward-shrinking animation, similar to AstroDX.",
|
||||
zh: "使 Slide Track 消失时有类似 AstroDX 一样的向内缩入的动画")]
|
||||
public class SlideArrowAnimation
|
||||
{
|
||||
private static List<SpriteRenderer> _animatingSpriteRenderers = [];
|
||||
|
@ -112,4 +115,4 @@ public class SlideArrowAnimation
|
|||
{
|
||||
_animatingSpriteRenderers.Clear();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,17 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using MAI2.Util;
|
||||
using Manager;
|
||||
using Monitor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Visual;
|
||||
namespace AquaMai.Mods.Fancy.GamePlay;
|
||||
|
||||
[ConfigSection(
|
||||
zh: "让星星在启动拍等待期间从 50% 透明度渐入为 100%,取代原本在击打星星头时就完成渐入",
|
||||
en: "Slides will fade in instead of instantly appearing.")]
|
||||
public class SlideFadeInTweak
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
|
@ -122,4 +126,4 @@ public class SlideFadeInTweak
|
|||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,10 +1,20 @@
|
|||
using System.Collections.Generic;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Monitor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Visual;
|
||||
namespace AquaMai.Mods.Fancy.GamePlay;
|
||||
|
||||
[ConfigSection(
|
||||
en: """
|
||||
Invert the Slide hierarchy, so that the new Slide appears on top like Maimai classic.
|
||||
Enable to support color changing effects achieved by overlaying multiple stars.
|
||||
""",
|
||||
zh: """
|
||||
反转 Slide 层级, 使新出现的 Slide 像旧框一样显示在上层
|
||||
启用以支持通过叠加多个星星达成的变色效果
|
||||
""")]
|
||||
public class SlideLayerReverse
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
|
@ -63,4 +73,4 @@ public class SlideLayerReverse
|
|||
renderer.sortingOrder = 1000 + orderBase + lastIdx - index;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,19 @@
|
|||
using HarmonyLib;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Process;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Visual;
|
||||
namespace AquaMai.Mods.Fancy.GamePlay;
|
||||
|
||||
[ConfigSection(
|
||||
en: """
|
||||
Delayed the animation of the song start screen.
|
||||
For recording chart confirmation.
|
||||
""",
|
||||
zh: """
|
||||
推迟了歌曲开始界面的动画
|
||||
录制谱面确认用
|
||||
""")]
|
||||
public class TrackStartProcessTweak
|
||||
{
|
||||
// 总之这个 Patch 没啥用, 是我个人用 sinmai 录谱面确认时用得到, 顺手也写进来了
|
|
@ -1,8 +1,12 @@
|
|||
using HarmonyLib;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.UX;
|
||||
namespace AquaMai.Mods.Fancy;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Remove the circle mask of the game screen.",
|
||||
zh: "移除游戏画面的圆形遮罩")]
|
||||
public class HideMask
|
||||
{
|
||||
[HarmonyPrefix]
|
|
@ -0,0 +1,7 @@
|
|||
# Fancy
|
||||
|
||||
All the fancy features, even if not required by most players, are welcomed to this category, whether for personalization, for beautify, for self-made charts or for other uncommon purposes.
|
||||
|
||||
These patches may not well-tested by the project maintainers and could be enabled only if you know what you're doing.
|
||||
|
||||
Patches affect the gameplay should go to the GamePlay subcategory.
|
|
@ -0,0 +1,88 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Core.Helpers;
|
||||
using HarmonyLib;
|
||||
using Mai2.Mai2Cue;
|
||||
using MAI2.Util;
|
||||
using Manager;
|
||||
using MelonLoader;
|
||||
|
||||
namespace AquaMai.Mods.Fancy;
|
||||
|
||||
[ConfigSection(
|
||||
en: """
|
||||
Random BGM.
|
||||
Put Mai2Cue.{acb,awb} of old version of the game in the configured directory and rename them.
|
||||
Won't work with 2P mode.
|
||||
""",
|
||||
zh: """
|
||||
在配置的目录下放置了旧版游戏的 Mai2Cue.{acb,awb} 并重命名的话,可以在播放游戏 BGM 的时候随机播放这里面的旧版游戏 BGM
|
||||
无法在 2P 模式下工作
|
||||
""")]
|
||||
public class RandomBgm
|
||||
{
|
||||
[ConfigEntry]
|
||||
private static readonly string mai2CueDir = "LocalAssets/Mai2Cue";
|
||||
|
||||
private static List<string> _acbs = new List<string>();
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(SoundManager), "Initialize")]
|
||||
public static void Init()
|
||||
{
|
||||
var resolvedDir = FileSystem.ResolvePath(mai2CueDir);
|
||||
if (!Directory.Exists(resolvedDir)) return;
|
||||
var files = Directory.EnumerateFiles(resolvedDir);
|
||||
foreach (var file in files)
|
||||
{
|
||||
if (!file.EndsWith(".acb")) continue;
|
||||
// Seems there's limit for max opened ACB files
|
||||
_acbs.Add(Path.ChangeExtension(file, null));
|
||||
}
|
||||
|
||||
MelonLogger.Msg($"Random BGM loaded {_acbs.Count} files");
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(SoundManager), "Play")]
|
||||
public static void PrePlay(ref SoundManager.AcbID acbID, int cueID)
|
||||
{
|
||||
if (acbID != SoundManager.AcbID.Default) return;
|
||||
if (_acbs.Count == 0) return;
|
||||
var cueIndex = (Cue)cueID;
|
||||
switch (cueIndex)
|
||||
{
|
||||
case Cue.BGM_ENTRY:
|
||||
case Cue.BGM_COLLECTION:
|
||||
case Cue.BGM_RESULT_CLEAR:
|
||||
case Cue.BGM_RESULT:
|
||||
var acb = _acbs[UnityEngine.Random.Range(0, _acbs.Count)];
|
||||
acbID = SoundManager.AcbID.Max;
|
||||
var result = Singleton<SoundCtrl>.Instance.LoadCueSheet((int)acbID, acb);
|
||||
MelonLogger.Msg($"Picked {acb} for {cueIndex}, result: {result}");
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(SoundManager), "PlayBGM")]
|
||||
public static bool PrePlayBGM(ref int target)
|
||||
{
|
||||
switch (target)
|
||||
{
|
||||
case 0:
|
||||
return true;
|
||||
case 1:
|
||||
return false;
|
||||
case 2:
|
||||
target = 0;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Process;
|
||||
|
||||
namespace AquaMai.Mods.Fancy;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Triggers for executing commands at certain events.",
|
||||
zh: "在一定时机执行命令的触发器")]
|
||||
public class Triggers
|
||||
{
|
||||
[ConfigEntry(
|
||||
en: "Execute some command on game idle.",
|
||||
zh: """
|
||||
在游戏闲置的时候执行指定的命令脚本
|
||||
比如说可以在游戏闲置是降低显示器的亮度
|
||||
""")]
|
||||
private static readonly string execOnIdle = "";
|
||||
|
||||
[ConfigEntry(
|
||||
en: "Execute some command on game start.",
|
||||
zh: "在玩家登录的时候执行指定的命令脚本")]
|
||||
private static readonly string execOnEntry = "";
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(AdvertiseProcess), "OnStart")]
|
||||
public static void AdvertiseProcessPreStart()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(execOnIdle))
|
||||
{
|
||||
Exec(execOnIdle);
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(EntryProcess), "OnStart")]
|
||||
public static void EntryProcessPreStart()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(execOnEntry))
|
||||
{
|
||||
Exec(execOnEntry);
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MusicSelectProcess), "OnStart")]
|
||||
public static void MusicSelectProcessPreStart()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(execOnEntry))
|
||||
{
|
||||
Exec(execOnEntry);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Exec(string command)
|
||||
{
|
||||
var process = new System.Diagnostics.Process();
|
||||
process.StartInfo.FileName = "cmd.exe";
|
||||
process.StartInfo.Arguments = "/c " + command;
|
||||
process.StartInfo.UseShellExecute = true;
|
||||
process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
|
||||
process.StartInfo.WorkingDirectory = Environment.CurrentDirectory;
|
||||
|
||||
process.Start();
|
||||
}
|
||||
}
|
|
@ -1,14 +1,20 @@
|
|||
using System.Net;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
using Monitor.MusicSelect.ChainList;
|
||||
using Net;
|
||||
using UnityEngine;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Core.Attributes;
|
||||
|
||||
namespace AquaMai.Fix;
|
||||
namespace AquaMai.Mods.Fix;
|
||||
|
||||
public class BasicFix
|
||||
[ConfigSection(exampleHidden: true, defaultOn: true)]
|
||||
public class Common
|
||||
{
|
||||
[ConfigEntry]
|
||||
private readonly static bool preventIniFileClear = true;
|
||||
|
||||
[EnableIf(nameof(preventIniFileClear))]
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MAI2System.IniFile), "clear")]
|
||||
private static bool PreIniFileClear()
|
||||
|
@ -16,6 +22,10 @@ public class BasicFix
|
|||
return false;
|
||||
}
|
||||
|
||||
[ConfigEntry]
|
||||
private readonly static bool fixDebugInput = true;
|
||||
|
||||
[EnableIf(nameof(fixDebugInput))]
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(DebugInput), "GetKey")]
|
||||
private static bool GetKey(ref bool __result, KeyCode name)
|
||||
|
@ -24,6 +34,7 @@ public class BasicFix
|
|||
return false;
|
||||
}
|
||||
|
||||
[EnableIf(nameof(fixDebugInput))]
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(DebugInput), "GetKeyDown")]
|
||||
private static bool GetKeyDown(ref bool __result, KeyCode name)
|
||||
|
@ -32,6 +43,7 @@ public class BasicFix
|
|||
return false;
|
||||
}
|
||||
|
||||
[EnableIf(nameof(fixDebugInput))]
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(DebugInput), "GetMouseButton")]
|
||||
private static bool GetMouseButton(ref bool __result, int button)
|
||||
|
@ -40,6 +52,7 @@ public class BasicFix
|
|||
return false;
|
||||
}
|
||||
|
||||
[EnableIf(nameof(fixDebugInput))]
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(DebugInput), "GetMouseButtonDown")]
|
||||
private static bool GetMouseButtonDown(ref bool __result, int button)
|
||||
|
@ -48,6 +61,10 @@ public class BasicFix
|
|||
return false;
|
||||
}
|
||||
|
||||
[ConfigEntry]
|
||||
private readonly static bool bypassCakeHashCheck = true;
|
||||
|
||||
[EnableIf(nameof(bypassCakeHashCheck))]
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(NetHttpClient), MethodType.Constructor)]
|
||||
private static void OnNetHttpClientConstructor(NetHttpClient __instance)
|
||||
|
@ -60,6 +77,10 @@ public class BasicFix
|
|||
}
|
||||
}
|
||||
|
||||
[ConfigEntry]
|
||||
private readonly static bool restoreCertificateValidation = true;
|
||||
|
||||
[EnableIf(nameof(restoreCertificateValidation))]
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(NetHttpClient), "Create")]
|
||||
private static void OnNetHttpClientCreate()
|
||||
|
@ -68,27 +89,41 @@ public class BasicFix
|
|||
ServicePointManager.ServerCertificateValidationCallback = null;
|
||||
}
|
||||
|
||||
[ConfigEntry]
|
||||
private readonly static bool forceNonTarget = true;
|
||||
|
||||
[EnableIf(nameof(forceNonTarget))]
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MAI2System.Config), "IsTarget", MethodType.Getter)]
|
||||
private static bool ForceNonTarget(ref bool __result)
|
||||
private static bool PreIsTarget(ref bool __result)
|
||||
{
|
||||
// Who teaching others to set Target=1?!
|
||||
// Who is teaching others to set `Target = 1`?!
|
||||
__result = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
[ConfigEntry]
|
||||
private readonly static bool forceIgnoreError = true;
|
||||
|
||||
[EnableIf(nameof(forceIgnoreError))]
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MAI2System.Config), "IsIgnoreError", MethodType.Getter)]
|
||||
private static bool ForceIgnoreError(ref bool __result)
|
||||
private static bool PreIsIgnoreError(ref bool __result)
|
||||
{
|
||||
__result = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void DoCustomPatch(HarmonyLib.Harmony h)
|
||||
[ConfigEntry]
|
||||
private readonly static bool bypassSpecialNumCheck = true;
|
||||
|
||||
public static void OnAfterPatch(HarmonyLib.Harmony h)
|
||||
{
|
||||
if (typeof(GameManager).GetMethod("CalcSpecialNum") is null) return;
|
||||
h.PatchAll(typeof(CalcSpecialNumPatch));
|
||||
if (bypassSpecialNumCheck)
|
||||
{
|
||||
if (typeof(GameManager).GetMethod("CalcSpecialNum") is null) return;
|
||||
h.PatchAll(typeof(CalcSpecialNumPatch));
|
||||
}
|
||||
}
|
||||
|
||||
private class CalcSpecialNumPatch
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Reflection;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using MAI2.Util;
|
||||
using Manager;
|
||||
|
@ -8,8 +9,9 @@ using Monitor;
|
|||
using Process;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Fix;
|
||||
namespace AquaMai.Mods.Fix;
|
||||
|
||||
[ConfigSection(exampleHidden: true, defaultOn: true)]
|
||||
public class DebugFeature
|
||||
{
|
||||
public static bool IsPolyfill { get; private set; }
|
||||
|
@ -29,7 +31,7 @@ public class DebugFeature
|
|||
PolyFill.timer = 0;
|
||||
}
|
||||
|
||||
public static void DoCustomPatch(HarmonyLib.Harmony h)
|
||||
public static void OnBeforePatch(HarmonyLib.Harmony h)
|
||||
{
|
||||
var original = typeof(GameProcess).GetField("debugFeature", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (original is null)
|
||||
|
@ -90,7 +92,7 @@ public class DebugFeature
|
|||
|
||||
public static double CurrentPlayMsec
|
||||
{
|
||||
[Obsolete("不要用它,它有问题。用 PractiseMode.CurrentPlayMsec")]
|
||||
[Obsolete("不要用它,它有问题。用 PracticeMode.CurrentPlayMsec")]
|
||||
get
|
||||
{
|
||||
if (IsPolyfill)
|
|
@ -0,0 +1,87 @@
|
|||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Manager.Operation;
|
||||
|
||||
namespace AquaMai.Mods.Fix;
|
||||
|
||||
[ConfigSection(exampleHidden: true, defaultOn: true)]
|
||||
public class DisableReboot
|
||||
{
|
||||
// IsAutoRebootNeeded
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MaintenanceTimer), "IsAutoRebootNeeded")]
|
||||
public static bool IsAutoRebootNeeded(ref bool __result)
|
||||
{
|
||||
__result = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// IsUnderServerMaintenance
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MaintenanceTimer), "IsUnderServerMaintenance")]
|
||||
public static bool IsUnderServerMaintenance(ref bool __result)
|
||||
{
|
||||
__result = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// RemainingMinutes
|
||||
// Original: private int RemainingMinutes => (this._secServerMaintenance + 59) / 60;
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MaintenanceTimer), "RemainingMinutes", MethodType.Getter)]
|
||||
public static bool RemainingMinutes(ref int __result)
|
||||
{
|
||||
__result = 600;
|
||||
return false;
|
||||
}
|
||||
|
||||
// GetAutoRebootSec
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MaintenanceTimer), "GetAutoRebootSec")]
|
||||
public static bool GetAutoRebootSec(ref int __result)
|
||||
{
|
||||
__result = 60 * 60 * 10;
|
||||
return false;
|
||||
}
|
||||
|
||||
// GetServerMaintenanceSec
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MaintenanceTimer), "GetServerMaintenanceSec")]
|
||||
public static bool GetServerMaintenanceSec(ref int __result)
|
||||
{
|
||||
__result = 60 * 60 * 10;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Execute
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MaintenanceTimer), "Execute")]
|
||||
public static bool Execute(MaintenanceTimer __instance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// UpdateTimes
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MaintenanceTimer), "UpdateTimes")]
|
||||
public static bool UpdateTimes(MaintenanceTimer __instance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(ClosingTimer), "IsShowRemainingMinutes")]
|
||||
public static bool IsShowRemainingMinutes(ref bool __result)
|
||||
{
|
||||
__result = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(ClosingTimer), "IsClosed")]
|
||||
public static bool IsClosed(ref bool __result)
|
||||
{
|
||||
__result = false;
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
using AMDaemon.Allnet;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
using Manager.Operation;
|
||||
|
||||
namespace AquaMai.Fix;
|
||||
namespace AquaMai.Mods.Fix;
|
||||
|
||||
[ConfigSection(exampleHidden: true, defaultOn: true)]
|
||||
public class FixCheckAuth
|
||||
{
|
||||
[HarmonyPostfix]
|
|
@ -1,14 +1,14 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using AquaMai.Attributes;
|
||||
using AquaMai.Core.Attributes;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
using MelonLoader;
|
||||
using Monitor;
|
||||
|
||||
namespace AquaMai.Fix;
|
||||
namespace AquaMai.Mods.Fix.Legacy;
|
||||
|
||||
[GameVersion(23000)]
|
||||
[ConfigSection(exampleHidden: true, defaultOn: true)]
|
||||
[EnableGameVersion(23000)]
|
||||
public class FixConnSlide
|
||||
{
|
||||
/* 这个 Patch 用于修复以下 bug:
|
|
@ -1,4 +1,5 @@
|
|||
using AquaMai.Attributes;
|
||||
using AquaMai.Core.Attributes;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using MAI2.Util;
|
||||
using Manager;
|
||||
|
@ -6,11 +7,15 @@ using Monitor;
|
|||
using Monitor.MusicSelect.ChainList;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Fix;
|
||||
namespace AquaMai.Mods.Fix;
|
||||
|
||||
[GameVersion(24000)]
|
||||
[EnableGameVersion(24000)]
|
||||
[ConfigSection(exampleHidden: true, defaultOn: true)]
|
||||
public class FixLevelDisplay
|
||||
{
|
||||
// Fix wrong position of level number's display for music levels with non-consistant display level and rate level (difficuly constant)
|
||||
// Stock game charts have no such inconsistency, but custom charts may have (e.g. 10+ but unrated)
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(MusicChainCardObejct), "SetLevel")]
|
||||
private static void FixLevelShiftMusicChainCardObejct(MusicLevelID levelID, SpriteCounter ____digitLevel, SpriteCounter ____doubleDigitLevel, bool utage, GameObject ____difficultyUtageQuesionMarkSingleDigit, GameObject ____difficultyUtageQuesionMarkDoubleDigit)
|
|
@ -1,10 +1,12 @@
|
|||
using System.Collections.Generic;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
using Monitor;
|
||||
|
||||
namespace AquaMai.Visual;
|
||||
namespace AquaMai.Mods.Fix;
|
||||
|
||||
[ConfigSection(exampleHidden: true, defaultOn: true)]
|
||||
public class FixSlideAutoPlay
|
||||
{
|
||||
/* 这个 Patch 用于修复以下 bug:
|
|
@ -1,11 +1,13 @@
|
|||
using AquaMai.Attributes;
|
||||
using AquaMai.Core.Attributes;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
|
||||
namespace AquaMai.Fix;
|
||||
namespace AquaMai.Mods.Fix.Legacy;
|
||||
|
||||
[GameVersion(23000, 23499)]
|
||||
public class FestivalQuickRetryFix
|
||||
[ConfigSection(exampleHidden: true, defaultOn: true)]
|
||||
[EnableGameVersion(23000, 23499, noWarn: true)]
|
||||
public class FixQuickRetry130
|
||||
{
|
||||
// Fix for the game not resetting Fast and Late counts when quick retrying
|
||||
// For game version < 1.35.0
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue