[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
Menci 2024-11-25 01:25:19 +08:00 committed by GitHub
parent e9ee31b22a
commit 37044dae01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
217 changed files with 6051 additions and 3040 deletions

3
AquaMai/.gitignore vendored
View File

@ -372,6 +372,3 @@ MigrationBackup/
Output
.idea
Libs/Assembly-CSharp.dll
Libs/AMDaemon.NET.dll
packages

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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));
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>

View File

@ -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);
}

View File

@ -0,0 +1,7 @@
namespace AquaMai.Config.Interfaces;
public interface IConfigMigrationManager
{
public IConfigView Migrate(IConfigView config);
public string GetVersion(IConfigView config);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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);
}

View File

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

View File

@ -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>

View File

@ -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
{}

View File

@ -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}")
};
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}
}

View File

@ -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)}");
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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";
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}

View File

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

View File

@ -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;
}
}
}

View File

@ -0,0 +1,178 @@
using System.Reflection;
using System.Linq;
using System.Collections.Generic;
using AquaMai.Config.Attributes;
using AquaMai.Config.Interfaces;
using System;
namespace AquaMai.Config.Reflection;
public class ReflectionManager : IReflectionManager
{
public record Entry : IReflectionManager.IEntry
{
public string Path { get; init; }
public string Name { get; init; }
public IReflectionField Field { get; init; }
public ConfigEntryAttribute Attribute { get; init; }
}
public record Section : IReflectionManager.ISection
{
public string Path { get; init; }
public IReflectionType Type { get; init; }
public ConfigSectionAttribute Attribute { get; init; }
public List<Entry> entries;
public List<IReflectionManager.IEntry> Entries => entries.Cast<IReflectionManager.IEntry>().ToList();
}
private readonly Dictionary<string, Section> sections = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, Entry> entries = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, Section> sectionsByFullName = [];
public ReflectionManager(IReflectionProvider reflectionProvider)
{
var prefix = "AquaMai.Mods.";
var types = reflectionProvider.GetTypes().Where(t => t.FullName.StartsWith(prefix));
var collapsedNamespaces = new HashSet<string>();
foreach (var type in types)
{
var sectionAttribute = type.GetCustomAttribute<ConfigSectionAttribute>();
if (sectionAttribute == null) continue;
if (collapsedNamespaces.Contains(type.Namespace))
{
throw new Exception($"Collapsed namespace {type.Namespace} contains multiple sections");
}
var path = type.FullName.Substring(prefix.Length);
if (type.GetCustomAttribute<ConfigCollapseNamespaceAttribute>() != null)
{
var separated = path.Split('.');
if (separated[separated.Length - 2] != separated[separated.Length - 1])
{
throw new Exception($"Type {type.FullName} is not collapsable");
}
path = string.Join(".", separated.Take(separated.Length - 1));
collapsedNamespaces.Add(type.Namespace);
}
var sectionEntries = new List<Entry>();
foreach (var field in type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
{
var entryAttribute = field.GetCustomAttribute<ConfigEntryAttribute>();
if (entryAttribute == null) continue;
var transformedName = Utility.ToPascalCase(field.Name);
var entryPath = $"{path}.{transformedName}";
var entry = new Entry()
{
Path = entryPath,
Name = transformedName,
Field = field,
Attribute = entryAttribute
};
sectionEntries.Add(entry);
entries.Add(entryPath, entry);
}
var section = new Section()
{
Path = path,
Type = type,
Attribute = sectionAttribute,
entries = sectionEntries
};
sections.Add(path, section);
sectionsByFullName.Add(type.FullName, section);
}
var order = reflectionProvider.GetEnum("AquaMai.Mods.SetionNameOrder");
sections = sections
.OrderBy(x => x.Key)
.OrderBy(x =>
{
var parts = x.Key.Split('.');
for (int i = parts.Length; i > 0; i--)
{
var key = string.Join("_", parts.Take(i));
if (order.TryGetValue(key, out var value))
{
return (int)value;
}
}
Utility.Log($"Section {x.Key} has no order defined, defaulting to int.MaxValue");
return int.MaxValue;
})
.ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase);
}
public IEnumerable<Section> SectionValues => sections.Values;
public IEnumerable<IReflectionManager.ISection> Sections => sections.Values.Cast<IReflectionManager.ISection>();
public IEnumerable<Entry> EntryValues => entries.Values;
public IEnumerable<IReflectionManager.IEntry> Entries => entries.Values.Cast<IReflectionManager.IEntry>();
public bool ContainsSection(string path)
{
return sections.ContainsKey(path);
}
public bool TryGetSection(string path, out IReflectionManager.ISection section)
{
if (sections.TryGetValue(path, out var sectionValue))
{
section = sectionValue;
return true;
}
section = null;
return false;
}
public bool TryGetSection(Type type, out IReflectionManager.ISection section)
{
bool result = sectionsByFullName.TryGetValue(type.FullName, out var sectionValue);
section = sectionValue;
return result;
}
public IReflectionManager.ISection GetSection(string path)
{
if (!TryGetSection(path, out var section))
{
throw new KeyNotFoundException($"Section {path} not found");
}
return section;
}
public IReflectionManager.ISection GetSection(Type type)
{
if (!TryGetSection(type.FullName, out var section))
{
throw new KeyNotFoundException($"Section {type.FullName} not found");
}
return section;
}
public bool ContainsEntry(string path)
{
return entries.ContainsKey(path);
}
public bool TryGetEntry(string path, out IReflectionManager.IEntry entry)
{
if (entries.TryGetValue(path, out var entryValue))
{
entry = entryValue;
return true;
}
entry = null;
return false;
}
public IReflectionManager.IEntry GetEntry(string path)
{
if (!TryGetEntry(path, out var entry))
{
throw new KeyNotFoundException($"Entry {path} not found");
}
return entry;
}
}

View File

@ -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;
}
}

View File

@ -1,4 +1,4 @@
namespace AquaMai.CustomKeyMap;
namespace AquaMai.Config.Types;
public enum KeyCodeID
{

View File

@ -1,6 +1,6 @@
namespace AquaMai.ModKeyMap;
namespace AquaMai.Config.Types;
public enum ModKeyCode
public enum KeyCodeOrName
{
None,
Alpha0,

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -1,7 +1,7 @@
using System.Reflection;
using MAI2System;
namespace AquaMai.Helpers;
namespace AquaMai.Core.Helpers;
public class GameInfo
{

View File

@ -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;

View File

@ -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, "游戏功能键需要单独处理")
};
}

View File

@ -4,7 +4,7 @@ using Manager;
using MelonLoader;
using Process;
namespace AquaMai.Helpers;
namespace AquaMai.Core.Helpers;
public class MessageHelper
{

View File

@ -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
{

View File

@ -2,7 +2,7 @@
using Main;
using Process;
namespace AquaMai.Helpers;
namespace AquaMai.Core.Helpers;
public class SharedInstances
{

View File

@ -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
{

View File

@ -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];

View File

@ -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+ =&gt; 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 &lt;&lt;.
/// </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 &gt;&gt;.
/// </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 &gt; 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);
}

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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.");
}
}
}

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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
{
// 自定义在歌曲开始界面上显示的难度 (并不是真的自定义难度)

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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")]

View File

@ -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
{
/*

View File

@ -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();
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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));
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -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);

View File

@ -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);
}
}
}
}

View File

@ -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")]

View File

@ -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
{
// 精确到子判定的自定义判定显示, 需要启用自定义皮肤功能 (理论上不启用自定义皮肤不会崩游戏, 只不过此时这个功能显然不会生效)

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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 录谱面确认时用得到, 顺手也写进来了

View File

@ -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]

View File

@ -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.

View File

@ -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;
}
}
}

View File

@ -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();
}
}

View File

@ -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

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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]

View File

@ -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:

View File

@ -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)

View File

@ -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:

View File

@ -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