diff --git a/AquaMai/.gitignore b/AquaMai/.gitignore
index 4ddf2a82..df21fd82 100644
--- a/AquaMai/.gitignore
+++ b/AquaMai/.gitignore
@@ -372,6 +372,3 @@ MigrationBackup/
Output
.idea
-Libs/Assembly-CSharp.dll
-Libs/AMDaemon.NET.dll
-packages
\ No newline at end of file
diff --git a/AquaMai/AquaMai.Build/AquaMai.Build.csproj b/AquaMai/AquaMai.Build/AquaMai.Build.csproj
new file mode 100644
index 00000000..ff6b804d
--- /dev/null
+++ b/AquaMai/AquaMai.Build/AquaMai.Build.csproj
@@ -0,0 +1,50 @@
+
+
+
+ Release
+ AnyCPU
+ {4C0C68C3-8B2E-4CA8-A26D-AE87CF2A38A5}
+ Library
+ AquaMai.Build
+ AquaMai.Build
+ netstandard2.0
+ 512
+ true
+ 12
+ 414;NU1702
+ $(ProjectDir)../Libs/
+ $(ProjectDir)../Output/
+ false
+ false
+
+
+
+ false
+ None
+ true
+ prompt
+ 4
+ true
+ false
+
+
+
+ DEBUG
+
+
+
+
+
+
+
+
+ $(LibsPath)Mono.Cecil.dll
+
+
+
+
+
+
+
+
+
diff --git a/AquaMai/AquaMai.Build/GenerateExampleConfig.cs b/AquaMai/AquaMai.Build/GenerateExampleConfig.cs
new file mode 100644
index 00000000..4e7b0028
--- /dev/null
+++ b/AquaMai/AquaMai.Build/GenerateExampleConfig.cs
@@ -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;
+ }
+ }
+}
diff --git a/AquaMai/AquaMai.Build/PostBuildPatch.cs b/AquaMai/AquaMai.Build/PostBuildPatch.cs
new file mode 100644
index 00000000..f2bb2a0a
--- /dev/null
+++ b/AquaMai/AquaMai.Build/PostBuildPatch.cs
@@ -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));
+ }
+ }
+ }
+}
diff --git a/AquaMai/AquaMai.Config.HeadlessLoader/AquaMai.Config.HeadlessLoader.csproj b/AquaMai/AquaMai.Config.HeadlessLoader/AquaMai.Config.HeadlessLoader.csproj
new file mode 100644
index 00000000..e23c3ba3
--- /dev/null
+++ b/AquaMai/AquaMai.Config.HeadlessLoader/AquaMai.Config.HeadlessLoader.csproj
@@ -0,0 +1,45 @@
+
+
+
+ Release
+ AnyCPU
+ {6B5E1F3E-D012-4CFB-A2FA-26A6CE06BE66}
+ Library
+ AquaMai.Config.HeadlessLoader
+ AquaMai.Config.HeadlessLoader
+ netstandard2.0
+ 512
+ true
+ 12
+ 414;NU1702
+ $(ProjectDir)../Libs/
+ $(ProjectDir)../Output/
+ false
+ false
+
+
+
+ false
+ None
+ true
+ prompt
+ 4
+ true
+ false
+
+
+
+ DEBUG
+
+
+
+
+
+
+
+
+ $(LibsPath)Mono.Cecil.dll
+
+
+
+
diff --git a/AquaMai/AquaMai.Config.HeadlessLoader/ConfigAssemblyLoader.cs b/AquaMai/AquaMai.Config.HeadlessLoader/ConfigAssemblyLoader.cs
new file mode 100644
index 00000000..22e1f96f
--- /dev/null
+++ b/AquaMai/AquaMai.Config.HeadlessLoader/ConfigAssemblyLoader.cs
@@ -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();
+ 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;
+ };
+ }
+}
diff --git a/AquaMai/AquaMai.Config.HeadlessLoader/CustomAssemblyResolver.cs b/AquaMai/AquaMai.Config.HeadlessLoader/CustomAssemblyResolver.cs
new file mode 100644
index 00000000..4bf96750
--- /dev/null
+++ b/AquaMai/AquaMai.Config.HeadlessLoader/CustomAssemblyResolver.cs
@@ -0,0 +1,11 @@
+using Mono.Cecil;
+
+namespace AquaMai.Config.HeadlessLoader;
+
+public class CustomAssemblyResolver : DefaultAssemblyResolver
+{
+ public new void RegisterAssembly(AssemblyDefinition assembly)
+ {
+ base.RegisterAssembly(assembly);
+ }
+}
diff --git a/AquaMai/AquaMai.Config.HeadlessLoader/HeadlessConfigInterface.cs b/AquaMai/AquaMai.Config.HeadlessLoader/HeadlessConfigInterface.cs
new file mode 100644
index 00000000..ba7797d7
--- /dev/null
+++ b/AquaMai/AquaMai.Config.HeadlessLoader/HeadlessConfigInterface.cs
@@ -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;
+ }
+}
diff --git a/AquaMai/AquaMai.Config.HeadlessLoader/HeadlessConfigLoader.cs b/AquaMai/AquaMai.Config.HeadlessLoader/HeadlessConfigLoader.cs
new file mode 100644
index 00000000..052b5565
--- /dev/null
+++ b/AquaMai/AquaMai.Config.HeadlessLoader/HeadlessConfigLoader.cs
@@ -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 assemblyBinariess) =>
+ LoadFromUnpacked(assemblyBinariess.Select(binary => new MemoryStream(binary)));
+
+ public static HeadlessConfigInterface LoadFromUnpacked(IEnumerable 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);
+ }
+}
diff --git a/AquaMai/AquaMai.Config.HeadlessLoader/Polyfills.cs b/AquaMai/AquaMai.Config.HeadlessLoader/Polyfills.cs
new file mode 100644
index 00000000..eb2da113
--- /dev/null
+++ b/AquaMai/AquaMai.Config.HeadlessLoader/Polyfills.cs
@@ -0,0 +1,4 @@
+namespace System.Runtime.CompilerServices
+{
+ internal static class IsExternalInit {}
+}
diff --git a/AquaMai/AquaMai.Config.HeadlessLoader/ResourceLoader.cs b/AquaMai/AquaMai.Config.HeadlessLoader/ResourceLoader.cs
new file mode 100644
index 00000000..e57a1357
--- /dev/null
+++ b/AquaMai/AquaMai.Config.HeadlessLoader/ResourceLoader.cs
@@ -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 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);
+ }
+}
diff --git a/AquaMai/AquaMai.Config.Interfaces/AquaMai.Config.Interfaces.csproj b/AquaMai/AquaMai.Config.Interfaces/AquaMai.Config.Interfaces.csproj
new file mode 100644
index 00000000..95c911c2
--- /dev/null
+++ b/AquaMai/AquaMai.Config.Interfaces/AquaMai.Config.Interfaces.csproj
@@ -0,0 +1,40 @@
+
+
+
+ Release
+ AnyCPU
+ {DF1536F9-3B06-4463-B654-4CC3E708B610}
+ Library
+ AquaMai.Config.Interfaces
+ AquaMai.Config.Interfaces
+ net472
+ 512
+ true
+ 12
+ 414
+ $(ProjectDir)../Libs/;$(AssemblySearchPaths)
+ $(ProjectDir)../Output/
+ false
+ false
+
+
+
+ false
+ None
+ true
+ prompt
+ 4
+ true
+ false
+
+
+
+ DEBUG
+
+
+
+
+
+
+
+
diff --git a/AquaMai/AquaMai.Config.Interfaces/IConfig.cs b/AquaMai/AquaMai.Config.Interfaces/IConfig.cs
new file mode 100644
index 00000000..4b3859d7
--- /dev/null
+++ b/AquaMai/AquaMai.Config.Interfaces/IConfig.cs
@@ -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);
+}
diff --git a/AquaMai/AquaMai.Config.Interfaces/IConfigMigrationManager.cs b/AquaMai/AquaMai.Config.Interfaces/IConfigMigrationManager.cs
new file mode 100644
index 00000000..aef08812
--- /dev/null
+++ b/AquaMai/AquaMai.Config.Interfaces/IConfigMigrationManager.cs
@@ -0,0 +1,7 @@
+namespace AquaMai.Config.Interfaces;
+
+public interface IConfigMigrationManager
+{
+ public IConfigView Migrate(IConfigView config);
+ public string GetVersion(IConfigView config);
+}
diff --git a/AquaMai/AquaMai.Config.Interfaces/IConfigParser.cs b/AquaMai/AquaMai.Config.Interfaces/IConfigParser.cs
new file mode 100644
index 00000000..5dda2d83
--- /dev/null
+++ b/AquaMai/AquaMai.Config.Interfaces/IConfigParser.cs
@@ -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);
+}
diff --git a/AquaMai/AquaMai.Config.Interfaces/IConfigSerializer.cs b/AquaMai/AquaMai.Config.Interfaces/IConfigSerializer.cs
new file mode 100644
index 00000000..e1b8fe83
--- /dev/null
+++ b/AquaMai/AquaMai.Config.Interfaces/IConfigSerializer.cs
@@ -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);
+}
diff --git a/AquaMai/AquaMai.Config.Interfaces/IConfigView.cs b/AquaMai/AquaMai.Config.Interfaces/IConfigView.cs
new file mode 100644
index 00000000..c1fe089b
--- /dev/null
+++ b/AquaMai/AquaMai.Config.Interfaces/IConfigView.cs
@@ -0,0 +1,9 @@
+namespace AquaMai.Config.Interfaces;
+
+public interface IConfigView
+{
+ public void SetValue(string path, object value);
+ public T GetValueOrDefault(string path, T defaultValue = default);
+ public bool TryGetValue(string path, out T resultValue);
+ public string ToToml();
+}
diff --git a/AquaMai/AquaMai.Config.Interfaces/IReflectionManager.cs b/AquaMai/AquaMai.Config.Interfaces/IReflectionManager.cs
new file mode 100644
index 00000000..cc76c3bd
--- /dev/null
+++ b/AquaMai/AquaMai.Config.Interfaces/IReflectionManager.cs
@@ -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 Entries { get; }
+ }
+
+ public IEnumerable Sections { get; }
+
+ public IEnumerable 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);
+}
diff --git a/AquaMai/AquaMai.Config.Interfaces/IReflectionProvider.cs b/AquaMai/AquaMai.Config.Interfaces/IReflectionProvider.cs
new file mode 100644
index 00000000..19f7fc10
--- /dev/null
+++ b/AquaMai/AquaMai.Config.Interfaces/IReflectionProvider.cs
@@ -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() 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() where T : Attribute;
+ public IReflectionField[] GetFields(BindingFlags bindingAttr);
+}
+
+public interface IReflectionProvider
+{
+ public IReflectionType[] GetTypes();
+ public Dictionary GetEnum(string enumName);
+}
diff --git a/AquaMai/AquaMai.Config.Interfaces/Polyfills.cs b/AquaMai/AquaMai.Config.Interfaces/Polyfills.cs
new file mode 100644
index 00000000..eb2da113
--- /dev/null
+++ b/AquaMai/AquaMai.Config.Interfaces/Polyfills.cs
@@ -0,0 +1,4 @@
+namespace System.Runtime.CompilerServices
+{
+ internal static class IsExternalInit {}
+}
diff --git a/AquaMai/AquaMai.Config/AquaMai.Config.csproj b/AquaMai/AquaMai.Config/AquaMai.Config.csproj
new file mode 100644
index 00000000..d80f92bd
--- /dev/null
+++ b/AquaMai/AquaMai.Config/AquaMai.Config.csproj
@@ -0,0 +1,60 @@
+
+
+
+ Release
+ AnyCPU
+ {DF1536F9-3B06-4463-B654-4CC3E708B610}
+ Library
+ AquaMai.Config
+ AquaMai.Config
+ net472
+ 512
+ true
+ 12
+ 414
+ $(ProjectDir)../Libs/;$(AssemblySearchPaths)
+ $(ProjectDir)../Output/
+ false
+ false
+
+
+
+ false
+ None
+ true
+ prompt
+ 4
+ true
+ false
+
+
+
+ DEBUG
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ all
+
+
+
+
+
diff --git a/AquaMai/AquaMai.Config/Attributes/ConfigCollapseNamespaceAttribute.cs b/AquaMai/AquaMai.Config/Attributes/ConfigCollapseNamespaceAttribute.cs
new file mode 100644
index 00000000..dab14350
--- /dev/null
+++ b/AquaMai/AquaMai.Config/Attributes/ConfigCollapseNamespaceAttribute.cs
@@ -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
+{}
diff --git a/AquaMai/AquaMai.Config/Attributes/ConfigComment.cs b/AquaMai/AquaMai.Config/Attributes/ConfigComment.cs
new file mode 100644
index 00000000..414b4a00
--- /dev/null
+++ b/AquaMai/AquaMai.Config/Attributes/ConfigComment.cs
@@ -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}")
+ };
+}
diff --git a/AquaMai/AquaMai.Config/Attributes/ConfigEntryAttribute.cs b/AquaMai/AquaMai.Config/Attributes/ConfigEntryAttribute.cs
new file mode 100644
index 00000000..05dc5e90
--- /dev/null
+++ b/AquaMai/AquaMai.Config/Attributes/ConfigEntryAttribute.cs
@@ -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;
+}
diff --git a/AquaMai/AquaMai.Config/Attributes/ConfigSectionAttribute.cs b/AquaMai/AquaMai.Config/Attributes/ConfigSectionAttribute.cs
new file mode 100644
index 00000000..34ccd639
--- /dev/null
+++ b/AquaMai/AquaMai.Config/Attributes/ConfigSectionAttribute.cs
@@ -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;
+}
diff --git a/AquaMai/AquaMai.Config/Attributes/EnableCondition.cs b/AquaMai/AquaMai.Config/Attributes/EnableCondition.cs
new file mode 100644
index 00000000..2a1e8d99
--- /dev/null
+++ b/AquaMai/AquaMai.Config/Attributes/EnableCondition.cs
@@ -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();
+ }
+ }
+}
diff --git a/AquaMai/AquaMai.Config/Config.cs b/AquaMai/AquaMai.Config/Config.cs
new file mode 100644
index 00000000..fdbf67eb
--- /dev/null
+++ b/AquaMai/AquaMai.Config/Config.cs
@@ -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 sections = new(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary 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;
+ }
+}
diff --git a/AquaMai/AquaMai.Config/ConfigParser.cs b/AquaMai/AquaMai.Config/ConfigParser.cs
new file mode 100644
index 00000000..e84d8cdb
--- /dev/null
+++ b/AquaMai/AquaMai.Config/ConfigParser.cs
@@ -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));
+ }
+ }
+}
diff --git a/AquaMai/AquaMai.Config/ConfigSerializer.cs b/AquaMai/AquaMai.Config/ConfigSerializer.cs
new file mode 100644
index 00000000..dd4c59ed
--- /dev/null
+++ b/AquaMai/AquaMai.Config/ConfigSerializer.cs
@@ -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)}");
+ }
+}
diff --git a/AquaMai/AquaMai.Config/ConfigView.cs b/AquaMai/AquaMai.Config/ConfigView.cs
new file mode 100644
index 00000000..ff9afbdf
--- /dev/null
+++ b/AquaMai/AquaMai.Config/ConfigView.cs
@@ -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(string path, T defaultValue = default)
+ {
+ return TryGetValue(path, out T resultValue) ? resultValue : defaultValue;
+ }
+
+ public bool TryGetValue(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(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;
+ }
+}
diff --git a/AquaMai/FodyWeavers.xml b/AquaMai/AquaMai.Config/FodyWeavers.xml
similarity index 62%
rename from AquaMai/FodyWeavers.xml
rename to AquaMai/AquaMai.Config/FodyWeavers.xml
index 27cb7738..645a09e7 100644
--- a/AquaMai/FodyWeavers.xml
+++ b/AquaMai/AquaMai.Config/FodyWeavers.xml
@@ -1,7 +1,7 @@
- tomlet|urgui|numerics
- $AquaMai$_
+ tomlet
+ $AquaMai.Config$_
diff --git a/AquaMai/FodyWeavers.xsd b/AquaMai/AquaMai.Config/FodyWeavers.xsd
similarity index 100%
rename from AquaMai/FodyWeavers.xsd
rename to AquaMai/AquaMai.Config/FodyWeavers.xsd
diff --git a/AquaMai/AquaMai.Config/Migration/ConfigMigrationManager.cs b/AquaMai/AquaMai.Config/Migration/ConfigMigrationManager.cs
new file mode 100644
index 00000000..d0be4bfd
--- /dev/null
+++ b/AquaMai/AquaMai.Config/Migration/ConfigMigrationManager.cs
@@ -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 migrationMap =
+ new List
+ {
+ 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("Version", out var version))
+ {
+ return version;
+ }
+ // Assume v1.0 if not found
+ return "1.0";
+ }
+}
diff --git a/AquaMai/AquaMai.Config/Migration/ConfigMigration_V1_0_V2_0.cs b/AquaMai/AquaMai.Config/Migration/ConfigMigration_V1_0_V2_0.cs
new file mode 100644
index 00000000..3b46ca0a
--- /dev/null
+++ b/AquaMai/AquaMai.Config/Migration/ConfigMigration_V1_0_V2_0.cs
@@ -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("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("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("UX.CustomNoteSkin") ||
+ src.GetValueOrDefault("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("Utils.Windowed") ||
+ src.GetValueOrDefault("Utils.Width") != 0 ||
+ src.GetValueOrDefault("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("Utils.PracticeMode") || src.GetValueOrDefault("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("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("Cheat.TicketUnlock");
+ var unlockMaps = src.GetValueOrDefault("Cheat.MapUnlock");
+ var unlockUtage = src.GetValueOrDefault("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("Fix.RemoveEncryption"))
+ {
+ dst.SetValue("GameSystem.RemoveEncryption.Disabled", true); // Enabled by default in V2
+ }
+ MapBooleanTrueToSectionEnable(src, dst, "Fix.ForceAsServer", "GameSettings.ForceAsServer");
+ if (src.GetValueOrDefault("Fix.ForceFreePlay"))
+ {
+ dst.SetValue("GameSettings.CreditConfig.IsFreePlay", true);
+ }
+ if (src.GetValueOrDefault("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("Font.FontFix") &&
+ !src.GetValueOrDefault("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("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(src, dst, "Utils.JudgeAdjustA", "GameSettings.JudgeAdjust.A", 0);
+ MapValueToEntryValueIfNonNullOrDefault(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("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(src, dst, "ModKeyMap.QuickSkipLongPress", "UX.OneKeyEntryEnd.LongPress");
+ }
+ if (keyInGameRetry != "None" || keyInGameSkip != "None")
+ {
+ dst.SetValue("UX.OneKeyRetrySkip.RetryKey", keyInGameRetry);
+ if (keyInGameRetry != "None")
+ {
+ MapValueToEntryValueIfNonNull(src, dst, "ModKeyMap.InGameRetryLongPress", "UX.OneKeyRetrySkip.RetryLongPress");
+ }
+ dst.SetValue("UX.OneKeyRetrySkip.SkipKey", keyInGameSkip);
+ if (keyInGameSkip != "None")
+ {
+ MapValueToEntryValueIfNonNull(src, dst, "ModKeyMap.InGameSkipLongPress", "UX.OneKeyRetrySkip.SkipLongPress");
+ }
+ }
+ if (keyPractiseMode != "None")
+ {
+ dst.SetValue("UX.PracticeMode.Key", keyPractiseMode);
+ MapValueToEntryValueIfNonNull(src, dst, "ModKeyMap.PractiseModeLongPress", "UX.PracticeMode.LongPress");
+ }
+ if (keyHideSelfMadeCharts != "None")
+ {
+ dst.SetValue("UX.HideSelfMadeCharts.Key", keyHideSelfMadeCharts);
+ MapValueToEntryValueIfNonNull(src, dst, "ModKeyMap.HideSelfMadeChartsLongPress", "UX.HideSelfMadeCharts.LongPress");
+ }
+ MapBooleanTrueToSectionEnable(src, dst, "ModKeyMap.EnableNativeQuickRetry", "GameSystem.QuickRetry");
+ if (src.TryGetValue("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("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("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("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(src, dst, $"TouchSensitivity.{area}", $"GameSettings.TouchSensitivity.{area}");
+ }
+ }
+
+ // CustomKeyMap
+ if (src.GetValueOrDefault("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($"CustomKeyMap.{key}", out var value) &&
+ Enum.TryParse(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(IConfigView src, ConfigView dst, string srcKey, string dstKey)
+ {
+ if (src.TryGetValue(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(IConfigView src, ConfigView dst, string srcKey, string dstKey, T defaultValue)
+ {
+ if (src.TryGetValue(srcKey, out var value) && !EqualityComparer.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(IConfigView src, ConfigView dst, string srcKey, string dstKey, T defaultValue)
+ {
+ if (src.TryGetValue(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(srcKey))
+ {
+ dst.EnsureDictionary(dstKey);
+ }
+ }
+}
diff --git a/AquaMai/AquaMai.Config/Migration/IConfigMigration.cs b/AquaMai/AquaMai.Config/Migration/IConfigMigration.cs
new file mode 100644
index 00000000..2c7c9d11
--- /dev/null
+++ b/AquaMai/AquaMai.Config/Migration/IConfigMigration.cs
@@ -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);
+}
diff --git a/AquaMai/AquaMai.Config/Polyfills.cs b/AquaMai/AquaMai.Config/Polyfills.cs
new file mode 100644
index 00000000..eb2da113
--- /dev/null
+++ b/AquaMai/AquaMai.Config/Polyfills.cs
@@ -0,0 +1,4 @@
+namespace System.Runtime.CompilerServices
+{
+ internal static class IsExternalInit {}
+}
diff --git a/AquaMai/AquaMai.Config/Reflection/MonoCecilAssemblyReflectionProvider.cs b/AquaMai/AquaMai.Config/Reflection/MonoCecilAssemblyReflectionProvider.cs
new file mode 100644
index 00000000..69afece2
--- /dev/null
+++ b/AquaMai/AquaMai.Config/Reflection/MonoCecilAssemblyReflectionProvider.cs
@@ -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 Attributes) : IReflectionField
+ {
+ public object Value { get; set; } = Value;
+
+ public T GetCustomAttribute() 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 Attributes) : IReflectionType
+ {
+ public T GetCustomAttribute() 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> enums = [];
+
+ public IReflectionType[] GetTypes() => reflectionTypes;
+ public Dictionary 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 InstantiateAttributes(ICollection 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;
+ }
+ }
+}
diff --git a/AquaMai/AquaMai.Config/Reflection/ReflectionManager.cs b/AquaMai/AquaMai.Config/Reflection/ReflectionManager.cs
new file mode 100644
index 00000000..54d192a6
--- /dev/null
+++ b/AquaMai/AquaMai.Config/Reflection/ReflectionManager.cs
@@ -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 entries;
+ public List Entries => entries.Cast().ToList();
+ }
+
+ private readonly Dictionary sections = new(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary entries = new(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary sectionsByFullName = [];
+
+ public ReflectionManager(IReflectionProvider reflectionProvider)
+ {
+ var prefix = "AquaMai.Mods.";
+ var types = reflectionProvider.GetTypes().Where(t => t.FullName.StartsWith(prefix));
+ var collapsedNamespaces = new HashSet();
+ foreach (var type in types)
+ {
+ var sectionAttribute = type.GetCustomAttribute();
+ 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() != 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();
+ foreach (var field in type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
+ {
+ var entryAttribute = field.GetCustomAttribute();
+ 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 SectionValues => sections.Values;
+ public IEnumerable Sections => sections.Values.Cast();
+
+ public IEnumerable EntryValues => entries.Values;
+ public IEnumerable Entries => entries.Values.Cast();
+
+ 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;
+ }
+}
diff --git a/AquaMai/AquaMai.Config/Reflection/SystemReflectionProvider.cs b/AquaMai/AquaMai.Config/Reflection/SystemReflectionProvider.cs
new file mode 100644
index 00000000..fe03a0de
--- /dev/null
+++ b/AquaMai/AquaMai.Config/Reflection/SystemReflectionProvider.cs
@@ -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() where T : Attribute => UnderlyingField.GetCustomAttribute();
+ 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() where T : Attribute => UnderlyingType.GetCustomAttribute();
+ 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 GetEnum(string enumName)
+ {
+ var enumType = UnderlyingAssembly.GetType(enumName);
+ if (enumType == null) return null;
+ var enumValues = Enum.GetValues(enumType);
+ var enumDict = new Dictionary();
+ foreach (var enumValue in enumValues)
+ {
+ enumDict.Add(enumValue.ToString(), enumValue);
+ }
+ return enumDict;
+ }
+}
diff --git a/AquaMai/CustomKeyMap/KeyCodeID.cs b/AquaMai/AquaMai.Config/Types/KeyCodeID.cs
similarity index 97%
rename from AquaMai/CustomKeyMap/KeyCodeID.cs
rename to AquaMai/AquaMai.Config/Types/KeyCodeID.cs
index b4b9dc8f..f1c7615c 100644
--- a/AquaMai/CustomKeyMap/KeyCodeID.cs
+++ b/AquaMai/AquaMai.Config/Types/KeyCodeID.cs
@@ -1,4 +1,4 @@
-namespace AquaMai.CustomKeyMap;
+namespace AquaMai.Config.Types;
public enum KeyCodeID
{
diff --git a/AquaMai/ModKeyMap/ModKeyCode.cs b/AquaMai/AquaMai.Config/Types/KeyCodeOrName.cs
similarity index 90%
rename from AquaMai/ModKeyMap/ModKeyCode.cs
rename to AquaMai/AquaMai.Config/Types/KeyCodeOrName.cs
index cd8a3830..400de783 100644
--- a/AquaMai/ModKeyMap/ModKeyCode.cs
+++ b/AquaMai/AquaMai.Config/Types/KeyCodeOrName.cs
@@ -1,6 +1,6 @@
-namespace AquaMai.ModKeyMap;
+namespace AquaMai.Config.Types;
-public enum ModKeyCode
+public enum KeyCodeOrName
{
None,
Alpha0,
diff --git a/AquaMai/AquaMai.Config/Utility.cs b/AquaMai/AquaMai.Config/Utility.cs
new file mode 100644
index 00000000..f6dbc605
--- /dev/null
+++ b/AquaMai/AquaMai.Config/Utility.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Reflection;
+using Tomlet.Models;
+
+namespace AquaMai.Config;
+
+public static class Utility
+{
+ public static Action 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(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);
+ }
+}
diff --git a/AquaMai/AquaMai.Core/AquaMai.Core.csproj b/AquaMai/AquaMai.Core/AquaMai.Core.csproj
new file mode 100644
index 00000000..5a2a9837
--- /dev/null
+++ b/AquaMai/AquaMai.Core/AquaMai.Core.csproj
@@ -0,0 +1,132 @@
+
+
+
+ Release
+ AnyCPU
+ {33C0D4ED-6A84-4659-9A05-12D43D75D0B3}
+ Library
+ AquaMai.Core
+ AquaMai.Core
+ net472
+ 512
+ true
+ 12
+ 414
+ $(ProjectDir)../Libs/;$(AssemblySearchPaths)
+ $(ProjectDir)../Output/
+ false
+ false
+
+
+
+ false
+ None
+ true
+ prompt
+ 4
+ true
+ false
+
+
+
+ DEBUG
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PublicResXFileCodeGenerator
+ Locale.Designer.cs
+
+
+ Locale.resx
+
+
+
+
diff --git a/AquaMai/AquaMai.Core/Attributes/EnableGameVersionAttribute.cs b/AquaMai/AquaMai.Core/Attributes/EnableGameVersionAttribute.cs
new file mode 100644
index 00000000..97191007
--- /dev/null
+++ b/AquaMai/AquaMai.Core/Attributes/EnableGameVersionAttribute.cs
@@ -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;
+ }
+}
diff --git a/AquaMai/AquaMai.Core/Attributes/EnableIfAttribute.cs b/AquaMai/AquaMai.Core/Attributes/EnableIfAttribute.cs
new file mode 100644
index 00000000..43f3cc8f
--- /dev/null
+++ b/AquaMai/AquaMai.Core/Attributes/EnableIfAttribute.cs
@@ -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();
+ }
+ }
+}
diff --git a/AquaMai/AquaMai.Core/Attributes/EnableImplicitlyIfAttribute.cs b/AquaMai/AquaMai.Core/Attributes/EnableImplicitlyIfAttribute.cs
new file mode 100644
index 00000000..71fb02bd
--- /dev/null
+++ b/AquaMai/AquaMai.Core/Attributes/EnableImplicitlyIfAttribute.cs
@@ -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;
+}
diff --git a/AquaMai/AquaMai.Core/ConfigLoader.cs b/AquaMai/AquaMai.Core/ConfigLoader.cs
new file mode 100644
index 00000000..92a9eaa1
--- /dev/null
+++ b/AquaMai/AquaMai.Core/ConfigLoader.cs
@@ -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 GenerateExamples()
+ {
+ var examples = new Dictionary();
+ foreach (var lang in (string[]) ["en", "zh"])
+ {
+ examples[lang] = SerailizeCurrentConfig(lang);
+ }
+ return examples;
+ }
+}
diff --git a/AquaMai/AquaMai.Core/Helpers/EnableConditionHelper.cs b/AquaMai/AquaMai.Core/Helpers/EnableConditionHelper.cs
new file mode 100644
index 00000000..475c24ae
--- /dev/null
+++ b/AquaMai/AquaMai.Core/Helpers/EnableConditionHelper.cs
@@ -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 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;
+ }
+}
diff --git a/AquaMai/AquaMai.Core/Helpers/FileSystem.cs b/AquaMai/AquaMai.Core/Helpers/FileSystem.cs
new file mode 100644
index 00000000..3eab6f0b
--- /dev/null
+++ b/AquaMai/AquaMai.Core/Helpers/FileSystem.cs
@@ -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);
+ }
+}
diff --git a/AquaMai/Helpers/GameInfo.cs b/AquaMai/AquaMai.Core/Helpers/GameInfo.cs
similarity index 95%
rename from AquaMai/Helpers/GameInfo.cs
rename to AquaMai/AquaMai.Core/Helpers/GameInfo.cs
index b29614de..117f724b 100644
--- a/AquaMai/Helpers/GameInfo.cs
+++ b/AquaMai/AquaMai.Core/Helpers/GameInfo.cs
@@ -1,7 +1,7 @@
using System.Reflection;
using MAI2System;
-namespace AquaMai.Helpers;
+namespace AquaMai.Core.Helpers;
public class GameInfo
{
diff --git a/AquaMai/Helpers/GuiSizes.cs b/AquaMai/AquaMai.Core/Helpers/GuiSizes.cs
similarity index 89%
rename from AquaMai/Helpers/GuiSizes.cs
rename to AquaMai/AquaMai.Core/Helpers/GuiSizes.cs
index b3012724..30efa7b2 100644
--- a/AquaMai/Helpers/GuiSizes.cs
+++ b/AquaMai/AquaMai.Core/Helpers/GuiSizes.cs
@@ -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;
diff --git a/AquaMai/AquaMai.Core/Helpers/KeyListener.cs b/AquaMai/AquaMai.Core/Helpers/KeyListener.cs
new file mode 100644
index 00000000..0215dd4d
--- /dev/null
+++ b/AquaMai/AquaMai.Core/Helpers/KeyListener.cs
@@ -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 _keyPressFrames = [];
+ private static readonly Dictionary _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, "游戏功能键需要单独处理")
+ };
+}
diff --git a/AquaMai/Helpers/MessageHelper.cs b/AquaMai/AquaMai.Core/Helpers/MessageHelper.cs
similarity index 97%
rename from AquaMai/Helpers/MessageHelper.cs
rename to AquaMai/AquaMai.Core/Helpers/MessageHelper.cs
index 348ddd95..b0e6b64f 100644
--- a/AquaMai/Helpers/MessageHelper.cs
+++ b/AquaMai/AquaMai.Core/Helpers/MessageHelper.cs
@@ -4,7 +4,7 @@ using Manager;
using MelonLoader;
using Process;
-namespace AquaMai.Helpers;
+namespace AquaMai.Core.Helpers;
public class MessageHelper
{
diff --git a/AquaMai/Helpers/MusicDirHelper.cs b/AquaMai/AquaMai.Core/Helpers/MusicDirHelper.cs
similarity index 90%
rename from AquaMai/Helpers/MusicDirHelper.cs
rename to AquaMai/AquaMai.Core/Helpers/MusicDirHelper.cs
index 8d7b4979..112a4e62 100644
--- a/AquaMai/Helpers/MusicDirHelper.cs
+++ b/AquaMai/AquaMai.Core/Helpers/MusicDirHelper.cs
@@ -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
{
diff --git a/AquaMai/Helpers/SharedInstances.cs b/AquaMai/AquaMai.Core/Helpers/SharedInstances.cs
similarity index 95%
rename from AquaMai/Helpers/SharedInstances.cs
rename to AquaMai/AquaMai.Core/Helpers/SharedInstances.cs
index 11017573..6f05f1fa 100644
--- a/AquaMai/Helpers/SharedInstances.cs
+++ b/AquaMai/AquaMai.Core/Helpers/SharedInstances.cs
@@ -2,7 +2,7 @@
using Main;
using Process;
-namespace AquaMai.Helpers;
+namespace AquaMai.Core.Helpers;
public class SharedInstances
{
diff --git a/AquaMai/Helpers/Shim.cs b/AquaMai/AquaMai.Core/Helpers/Shim.cs
similarity index 99%
rename from AquaMai/Helpers/Shim.cs
rename to AquaMai/AquaMai.Core/Helpers/Shim.cs
index 040af605..e2d72e83 100644
--- a/AquaMai/Helpers/Shim.cs
+++ b/AquaMai/AquaMai.Core/Helpers/Shim.cs
@@ -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
{
diff --git a/AquaMai/Fix/I18nSingleAssemblyHook.cs b/AquaMai/AquaMai.Core/Resources/I18nSingleAssemblyHook.cs
similarity index 86%
rename from AquaMai/Fix/I18nSingleAssemblyHook.cs
rename to AquaMai/AquaMai.Core/Resources/I18nSingleAssemblyHook.cs
index aebae280..2fdd490d 100644
--- a/AquaMai/Fix/I18nSingleAssemblyHook.cs
+++ b/AquaMai/AquaMai.Core/Resources/I18nSingleAssemblyHook.cs
@@ -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];
diff --git a/AquaMai/Resources/Locale.Designer.cs b/AquaMai/AquaMai.Core/Resources/Locale.Designer.cs
similarity index 85%
rename from AquaMai/Resources/Locale.Designer.cs
rename to AquaMai/AquaMai.Core/Resources/Locale.Designer.cs
index a3578c5d..5237bcdd 100644
--- a/AquaMai/Resources/Locale.Designer.cs
+++ b/AquaMai/AquaMai.Core/Resources/Locale.Designer.cs
@@ -7,7 +7,7 @@
//
//------------------------------------------------------------------------------
-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() {
}
///
/// Returns the cached ResourceManager instance used by this class.
///
[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.
///
[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 {
///
/// 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..
///
- internal static string CiBuildAlertContent {
+ public static string CiBuildAlertContent {
get {
return ResourceManager.GetString("CiBuildAlertContent", resourceCulture);
}
@@ -71,7 +71,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Important Notice: Test Version.
///
- internal static string CiBuildAlertTitle {
+ public static string CiBuildAlertTitle {
get {
return ResourceManager.GetString("CiBuildAlertTitle", resourceCulture);
}
@@ -80,7 +80,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Loaded!.
///
- 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.
///
- internal static string LoadError {
+ public static string LoadError {
get {
return ResourceManager.GetString("LoadError", resourceCulture);
}
@@ -100,7 +100,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to End.
///
- internal static string MarkRepeatEnd {
+ public static string MarkRepeatEnd {
get {
return ResourceManager.GetString("MarkRepeatEnd", resourceCulture);
}
@@ -109,7 +109,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Start.
///
- internal static string MarkRepeatStart {
+ public static string MarkRepeatStart {
get {
return ResourceManager.GetString("MarkRepeatStart", resourceCulture);
}
@@ -118,7 +118,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Aime reader error.
///
- internal static string NetErrIsAliveAimeReader {
+ public static string NetErrIsAliveAimeReader {
get {
return ResourceManager.GetString("NetErrIsAliveAimeReader", resourceCulture);
}
@@ -127,7 +127,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Aime server error.
///
- internal static string NetErrIsAliveAimeServer {
+ public static string NetErrIsAliveAimeServer {
get {
return ResourceManager.GetString("NetErrIsAliveAimeServer", resourceCulture);
}
@@ -136,7 +136,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Server communication error.
///
- internal static string NetErrIsAliveServer {
+ public static string NetErrIsAliveServer {
get {
return ResourceManager.GetString("NetErrIsAliveServer", resourceCulture);
}
@@ -145,7 +145,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Data download not success.
///
- internal static string NetErrWasDownloadSuccessOnce {
+ public static string NetErrWasDownloadSuccessOnce {
get {
return ResourceManager.GetString("NetErrWasDownloadSuccessOnce", resourceCulture);
}
@@ -154,7 +154,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Pause.
///
- internal static string Pause {
+ public static string Pause {
get {
return ResourceManager.GetString("Pause", resourceCulture);
}
@@ -163,7 +163,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to 游玩次数:{0}.
///
- internal static string PlayCount {
+ public static string PlayCount {
get {
return ResourceManager.GetString("PlayCount", resourceCulture);
}
@@ -172,7 +172,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to SSS+ => DXRating += {0}.
///
- internal static string RatingUpWhenSSSp {
+ public static string RatingUpWhenSSSp {
get {
return ResourceManager.GetString("RatingUpWhenSSSp", resourceCulture);
}
@@ -181,7 +181,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Repeat end time cannot be less than repeat start time.
///
- internal static string RepeatEndTimeLessThenStartTime {
+ public static string RepeatEndTimeLessThenStartTime {
get {
return ResourceManager.GetString("RepeatEndTimeLessThenStartTime", resourceCulture);
}
@@ -190,7 +190,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Loop Not Set.
///
- internal static string RepeatNotSet {
+ public static string RepeatNotSet {
get {
return ResourceManager.GetString("RepeatNotSet", resourceCulture);
}
@@ -199,7 +199,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Reset.
///
- internal static string RepeatReset {
+ public static string RepeatReset {
get {
return ResourceManager.GetString("RepeatReset", resourceCulture);
}
@@ -208,7 +208,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Loop Set.
///
- internal static string RepeatStartEndSet {
+ public static string RepeatStartEndSet {
get {
return ResourceManager.GetString("RepeatStartEndSet", resourceCulture);
}
@@ -217,7 +217,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Loop Start Set.
///
- internal static string RepeatStartSet {
+ public static string RepeatStartSet {
get {
return ResourceManager.GetString("RepeatStartSet", resourceCulture);
}
@@ -226,7 +226,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Please set repeat start time first.
///
- internal static string RepeatStartTimeNotSet {
+ public static string RepeatStartTimeNotSet {
get {
return ResourceManager.GetString("RepeatStartTimeNotSet", resourceCulture);
}
@@ -235,7 +235,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Saving... Do not exit the game.
///
- internal static string SavingDontExit {
+ public static string SavingDontExit {
get {
return ResourceManager.GetString("SavingDontExit", resourceCulture);
}
@@ -244,7 +244,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Seek <<.
///
- internal static string SeekBackward {
+ public static string SeekBackward {
get {
return ResourceManager.GetString("SeekBackward", resourceCulture);
}
@@ -253,7 +253,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Seek >>.
///
- internal static string SeekForward {
+ public static string SeekForward {
get {
return ResourceManager.GetString("SeekForward", resourceCulture);
}
@@ -262,7 +262,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Skip.
///
- internal static string Skip {
+ public static string Skip {
get {
return ResourceManager.GetString("Skip", resourceCulture);
}
@@ -271,7 +271,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to > Skipping incompatible patch: {0}.
///
- internal static string SkipIncompatiblePatch {
+ public static string SkipIncompatiblePatch {
get {
return ResourceManager.GetString("SkipIncompatiblePatch", resourceCulture);
}
@@ -280,7 +280,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Speed.
///
- internal static string Speed {
+ public static string Speed {
get {
return ResourceManager.GetString("Speed", resourceCulture);
}
@@ -289,7 +289,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Speed -.
///
- internal static string SpeedDown {
+ public static string SpeedDown {
get {
return ResourceManager.GetString("SpeedDown", resourceCulture);
}
@@ -298,7 +298,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Speed Reset.
///
- internal static string SpeedReset {
+ public static string SpeedReset {
get {
return ResourceManager.GetString("SpeedReset", resourceCulture);
}
@@ -307,7 +307,7 @@ namespace AquaMai.Resources {
///
/// Looks up a localized string similar to Speed +.
///
- internal static string SpeedUp {
+ public static string SpeedUp {
get {
return ResourceManager.GetString("SpeedUp", resourceCulture);
}
diff --git a/AquaMai/Resources/Locale.resx b/AquaMai/AquaMai.Core/Resources/Locale.resx
similarity index 100%
rename from AquaMai/Resources/Locale.resx
rename to AquaMai/AquaMai.Core/Resources/Locale.resx
diff --git a/AquaMai/Resources/Locale.zh.resx b/AquaMai/AquaMai.Core/Resources/Locale.zh.resx
similarity index 100%
rename from AquaMai/Resources/Locale.zh.resx
rename to AquaMai/AquaMai.Core/Resources/Locale.zh.resx
diff --git a/AquaMai/AquaMai.Core/Startup.cs b/AquaMai/AquaMai.Core/Startup.cs
new file mode 100644
index 00000000..8695ad87
--- /dev/null
+++ b/AquaMai/AquaMai.Core/Startup.cs
@@ -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();
+ 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 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 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();
+ }
+}
diff --git a/AquaMai/AquaMai.Mods/AquaMai.Mods.csproj b/AquaMai/AquaMai.Mods/AquaMai.Mods.csproj
new file mode 100644
index 00000000..cb2fb8dd
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/AquaMai.Mods.csproj
@@ -0,0 +1,127 @@
+
+
+
+ Release
+ AnyCPU
+ {8731C0E0-53BE-4B1B-9828-193E738C6865}
+ Library
+ AquaMai.Mods
+ AquaMai.Mods
+ net472
+ 512
+ true
+ 12
+ 414
+ $(ProjectDir)../Libs/;$(AssemblySearchPaths)
+ $(ProjectDir)../Output/
+ false
+ false
+
+
+
+ false
+ None
+ true
+ prompt
+ 4
+ true
+ false
+
+
+
+ DEBUG
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AquaMai/AquaMai.Mods/DeprecationWarning.cs b/AquaMai/AquaMai.Mods/DeprecationWarning.cs
new file mode 100644
index 00000000..4afacde8
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/DeprecationWarning.cs
@@ -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.");
+ }
+ }
+}
diff --git a/AquaMai/Visual/CustomLogo.cs b/AquaMai/AquaMai.Mods/Fancy/CustomLogo.cs
similarity index 58%
rename from AquaMai/Visual/CustomLogo.cs
rename to AquaMai/AquaMai.Mods/Fancy/CustomLogo.cs
index 65cead94..54c127f9 100644
--- a/AquaMai/Visual/CustomLogo.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/CustomLogo.cs
@@ -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 segaLogo = new();
- private static List 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 segaLogo = [];
+ private readonly static List 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 collection, string path)
diff --git a/AquaMai/AquaMai.Mods/Fancy/CustomPlaceName.cs b/AquaMai/AquaMai.Mods/Fancy/CustomPlaceName.cs
new file mode 100644
index 00000000..a54f3e72
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Fancy/CustomPlaceName.cs
@@ -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);
+ }
+}
diff --git a/AquaMai/Visual/CustomSkins.cs b/AquaMai/AquaMai.Mods/Fancy/CustomSkins.cs
similarity index 95%
rename from AquaMai/Visual/CustomSkins.cs
rename to AquaMai/AquaMai.Mods/Fancy/CustomSkins.cs
index de362869..46f6f422 100644
--- a/AquaMai/Visual/CustomSkins.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/CustomSkins.cs
@@ -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 ImageExts = [".png", ".jpg", ".jpeg"];
private static readonly List SlideFanFields = ["_normalSlideFan", "_eachSlideFan", "_breakSlideFan", "_breakSlideFanEff"];
private static readonly List 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);
diff --git a/AquaMai/Visual/CustomTrackStartDiff.cs b/AquaMai/AquaMai.Mods/Fancy/CustomTrackStartDiff.cs
similarity index 80%
rename from AquaMai/Visual/CustomTrackStartDiff.cs
rename to AquaMai/AquaMai.Mods/Fancy/CustomTrackStartDiff.cs
index 1bd2740c..41f3623c 100644
--- a/AquaMai/Visual/CustomTrackStartDiff.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/CustomTrackStartDiff.cs
@@ -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
{
// 自定义在歌曲开始界面上显示的难度 (并不是真的自定义难度)
diff --git a/AquaMai/AquaMai.Mods/Fancy/CustomVersionString.cs b/AquaMai/AquaMai.Mods/Fancy/CustomVersionString.cs
new file mode 100644
index 00000000..cbada55d
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Fancy/CustomVersionString.cs
@@ -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;
+ }
+}
diff --git a/AquaMai/AquaMai.Mods/Fancy/DemoMaster.cs b/AquaMai/AquaMai.Mods/Fancy/DemoMaster.cs
new file mode 100644
index 00000000..12939c44
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Fancy/DemoMaster.cs
@@ -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.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;
+ }
+}
diff --git a/AquaMai/Visual/FixCircleSlideJudge.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/AlignCircleSlideJudgeDisplay.cs
similarity index 76%
rename from AquaMai/Visual/FixCircleSlideJudge.cs
rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/AlignCircleSlideJudgeDisplay.cs
index 64fdcb34..508e5e88 100644
--- a/AquaMai/Visual/FixCircleSlideJudge.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/AlignCircleSlideJudgeDisplay.cs
@@ -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")]
diff --git a/AquaMai/Visual/BreakSlideJudgeBlink.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/BreakSlideJudgeBlink.cs
similarity index 56%
rename from AquaMai/Visual/BreakSlideJudgeBlink.cs
rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/BreakSlideJudgeBlink.cs
index 3c8ef6cf..175f605c 100644
--- a/AquaMai/Visual/BreakSlideJudgeBlink.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/BreakSlideJudgeBlink.cs
@@ -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
{
/*
diff --git a/AquaMai/MaimaiDX2077/CustomNoteTypePatch.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/CustomNoteTypes.cs
similarity index 94%
rename from AquaMai/MaimaiDX2077/CustomNoteTypePatch.cs
rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/CustomNoteTypes.cs
index 62bb400b..0614c970 100644
--- a/AquaMai/MaimaiDX2077/CustomNoteTypePatch.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/CustomNoteTypes.cs
@@ -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();
-
+
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("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 Transpiler(IEnumerable 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 Transpiler(IEnumerable 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(instructions);
var newInstList = new List();
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 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 $"";
}
-
+
return value.ToString();
}
}
-}
\ No newline at end of file
+}
diff --git a/AquaMai/MaimaiDX2077/CustomSlideNoteData.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/CustomSlideNoteData.cs
similarity index 96%
rename from AquaMai/MaimaiDX2077/CustomSlideNoteData.cs
rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/CustomSlideNoteData.cs
index 1abc3f2f..a7531e8a 100644
--- a/AquaMai/MaimaiDX2077/CustomSlideNoteData.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/CustomSlideNoteData.cs
@@ -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;
}
-}
\ No newline at end of file
+}
diff --git a/AquaMai/MaimaiDX2077/MaiGeometry.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/MaiGeometry.cs
similarity index 97%
rename from AquaMai/MaimaiDX2077/MaiGeometry.cs
rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/MaiGeometry.cs
index d0c4fab7..7866424b 100644
--- a/AquaMai/MaimaiDX2077/MaiGeometry.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/MaiGeometry.cs
@@ -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(new CircleStruct(center, TransferRadius),
Math.IEEERemainder(startAngle, Math.PI * 2), Math.IEEERemainder(endAngle, Math.PI * 2));
}
-}
\ No newline at end of file
+}
diff --git a/AquaMai/MaimaiDX2077/ParametricSlidePath.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/ParametricSlidePath.cs
similarity index 99%
rename from AquaMai/MaimaiDX2077/ParametricSlidePath.cs
rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/ParametricSlidePath.cs
index 5b928025..26832c42 100644
--- a/AquaMai/MaimaiDX2077/ParametricSlidePath.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/ParametricSlidePath.cs
@@ -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;
}
-}
\ No newline at end of file
+}
diff --git a/AquaMai/MaimaiDX2077/SlideCodeParser.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/SlideCodeParser.cs
similarity index 99%
rename from AquaMai/MaimaiDX2077/SlideCodeParser.cs
rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/SlideCodeParser.cs
index fc3ad74e..1aaeb7a4 100644
--- a/AquaMai/MaimaiDX2077/SlideCodeParser.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/SlideCodeParser.cs
@@ -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;
}
}
-}
\ No newline at end of file
+}
diff --git a/AquaMai/MaimaiDX2077/SlideDataBuilder.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/SlideDataBuilder.cs
similarity index 99%
rename from AquaMai/MaimaiDX2077/SlideDataBuilder.cs
rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/SlideDataBuilder.cs
index 857db2c2..6f6abb5b 100644
--- a/AquaMai/MaimaiDX2077/SlideDataBuilder.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/SlideDataBuilder.cs
@@ -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;
}
-}
\ No newline at end of file
+}
diff --git a/AquaMai/MaimaiDX2077/SlidePathGenerator.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/SlidePathGenerator.cs
similarity index 98%
rename from AquaMai/MaimaiDX2077/SlidePathGenerator.cs
rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/SlidePathGenerator.cs
index a22484f6..788567a8 100644
--- a/AquaMai/MaimaiDX2077/SlidePathGenerator.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/SlidePathGenerator.cs
@@ -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);
}
-}
\ No newline at end of file
+}
diff --git a/AquaMai/Visual/DisableTrackStartTabs.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/DisableTrackStartTabs.cs
similarity index 80%
rename from AquaMai/Visual/DisableTrackStartTabs.cs
rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/DisableTrackStartTabs.cs
index 92823a48..439b8579 100644
--- a/AquaMai/Visual/DisableTrackStartTabs.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/DisableTrackStartTabs.cs
@@ -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
diff --git a/AquaMai/Fix/ExtendNotesPool.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/ExtendNotesPool.cs
similarity index 92%
rename from AquaMai/Fix/ExtendNotesPool.cs
rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/ExtendNotesPool.cs
index 5a4edd07..245a01b0 100644
--- a/AquaMai/Fix/ExtendNotesPool.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/ExtendNotesPool.cs
@@ -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 ____arrowObjectList, List ____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);
diff --git a/AquaMai/AquaMai.Mods/Fancy/GamePlay/FanJudgeFlip.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/FanJudgeFlip.cs
new file mode 100644
index 00000000..bad70ed8
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/FanJudgeFlip.cs
@@ -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);
+ }
+ }
+ }
+}
diff --git a/AquaMai/UX/HideHanabi.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/HideHanabi.cs
similarity index 73%
rename from AquaMai/UX/HideHanabi.cs
rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/HideHanabi.cs
index 4a3a6819..e84e02d0 100644
--- a/AquaMai/UX/HideHanabi.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/HideHanabi.cs
@@ -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")]
diff --git a/AquaMai/Visual/JudgeDisplay4B.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/JudgeDisplay4B.cs
similarity index 80%
rename from AquaMai/Visual/JudgeDisplay4B.cs
rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/JudgeDisplay4B.cs
index abaceaac..29c64edd 100644
--- a/AquaMai/Visual/JudgeDisplay4B.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/JudgeDisplay4B.cs
@@ -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
{
// 精确到子判定的自定义判定显示, 需要启用自定义皮肤功能 (理论上不启用自定义皮肤不会崩游戏, 只不过此时这个功能显然不会生效)
diff --git a/AquaMai/AquaMai.Mods/Fancy/GamePlay/RealisticRandomJudge.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/RealisticRandomJudge.cs
new file mode 100644
index 00000000..a73ab94e
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/RealisticRandomJudge.cs
@@ -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;
+ }
+}
diff --git a/AquaMai/Visual/SlideArrowAnimation.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideArrowAnimation.cs
similarity index 93%
rename from AquaMai/Visual/SlideArrowAnimation.cs
rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideArrowAnimation.cs
index 7439ca25..16db4308 100644
--- a/AquaMai/Visual/SlideArrowAnimation.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideArrowAnimation.cs
@@ -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 _animatingSpriteRenderers = [];
@@ -112,4 +115,4 @@ public class SlideArrowAnimation
{
_animatingSpriteRenderers.Clear();
}
-}
\ No newline at end of file
+}
diff --git a/AquaMai/Visual/SlideFadeInTweak.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideFadeInTweak.cs
similarity index 94%
rename from AquaMai/Visual/SlideFadeInTweak.cs
rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideFadeInTweak.cs
index 26f9dec0..1a597c52 100644
--- a/AquaMai/Visual/SlideFadeInTweak.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideFadeInTweak.cs
@@ -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;
}
-}
\ No newline at end of file
+}
diff --git a/AquaMai/Visual/SlideLayerReverse.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideLayerReverse.cs
similarity index 84%
rename from AquaMai/Visual/SlideLayerReverse.cs
rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideLayerReverse.cs
index 1e6958ab..689b32a6 100644
--- a/AquaMai/Visual/SlideLayerReverse.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideLayerReverse.cs
@@ -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;
}
}
-}
\ No newline at end of file
+}
diff --git a/AquaMai/Visual/TrackStartProcessTweak.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/TrackStartProcessTweak.cs
similarity index 88%
rename from AquaMai/Visual/TrackStartProcessTweak.cs
rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/TrackStartProcessTweak.cs
index a95a608e..0774d9df 100644
--- a/AquaMai/Visual/TrackStartProcessTweak.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/TrackStartProcessTweak.cs
@@ -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 录谱面确认时用得到, 顺手也写进来了
diff --git a/AquaMai/UX/HideMask.cs b/AquaMai/AquaMai.Mods/Fancy/HideMask.cs
similarity index 61%
rename from AquaMai/UX/HideMask.cs
rename to AquaMai/AquaMai.Mods/Fancy/HideMask.cs
index 18c26680..013ee7ab 100644
--- a/AquaMai/UX/HideMask.cs
+++ b/AquaMai/AquaMai.Mods/Fancy/HideMask.cs
@@ -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]
diff --git a/AquaMai/AquaMai.Mods/Fancy/README.md b/AquaMai/AquaMai.Mods/Fancy/README.md
new file mode 100644
index 00000000..2f4f6dbc
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Fancy/README.md
@@ -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.
diff --git a/AquaMai/AquaMai.Mods/Fancy/RandomBgm.cs b/AquaMai/AquaMai.Mods/Fancy/RandomBgm.cs
new file mode 100644
index 00000000..bec48456
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Fancy/RandomBgm.cs
@@ -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 _acbs = new List();
+
+ [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.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;
+ }
+ }
+}
diff --git a/AquaMai/AquaMai.Mods/Fancy/Triggers.cs b/AquaMai/AquaMai.Mods/Fancy/Triggers.cs
new file mode 100644
index 00000000..7b4ceaf8
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Fancy/Triggers.cs
@@ -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();
+ }
+}
diff --git a/AquaMai/Fix/BasicFix.cs b/AquaMai/AquaMai.Mods/Fix/Common.cs
similarity index 62%
rename from AquaMai/Fix/BasicFix.cs
rename to AquaMai/AquaMai.Mods/Fix/Common.cs
index 86b9c049..f4e1c54e 100644
--- a/AquaMai/Fix/BasicFix.cs
+++ b/AquaMai/AquaMai.Mods/Fix/Common.cs
@@ -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
diff --git a/AquaMai/Fix/DebugFeature.cs b/AquaMai/AquaMai.Mods/Fix/DebugFeature.cs
similarity index 96%
rename from AquaMai/Fix/DebugFeature.cs
rename to AquaMai/AquaMai.Mods/Fix/DebugFeature.cs
index e9ec44ea..fb4b10aa 100644
--- a/AquaMai/Fix/DebugFeature.cs
+++ b/AquaMai/AquaMai.Mods/Fix/DebugFeature.cs
@@ -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)
diff --git a/AquaMai/AquaMai.Mods/Fix/DisableReboot.cs b/AquaMai/AquaMai.Mods/Fix/DisableReboot.cs
new file mode 100644
index 00000000..04effc6f
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Fix/DisableReboot.cs
@@ -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;
+ }
+}
diff --git a/AquaMai/Fix/FixCheckAuth.cs b/AquaMai/AquaMai.Mods/Fix/FixCheckAuth.cs
similarity index 80%
rename from AquaMai/Fix/FixCheckAuth.cs
rename to AquaMai/AquaMai.Mods/Fix/FixCheckAuth.cs
index 1b350991..136424c1 100644
--- a/AquaMai/Fix/FixCheckAuth.cs
+++ b/AquaMai/AquaMai.Mods/Fix/FixCheckAuth.cs
@@ -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]
diff --git a/AquaMai/Fix/FixConnSlide.cs b/AquaMai/AquaMai.Mods/Fix/FixConnSlide.cs
similarity index 93%
rename from AquaMai/Fix/FixConnSlide.cs
rename to AquaMai/AquaMai.Mods/Fix/FixConnSlide.cs
index 99a251f3..59b38572 100644
--- a/AquaMai/Fix/FixConnSlide.cs
+++ b/AquaMai/AquaMai.Mods/Fix/FixConnSlide.cs
@@ -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:
diff --git a/AquaMai/Visual/FixLevelDisplay.cs b/AquaMai/AquaMai.Mods/Fix/FixLevelDisplay.cs
similarity index 90%
rename from AquaMai/Visual/FixLevelDisplay.cs
rename to AquaMai/AquaMai.Mods/Fix/FixLevelDisplay.cs
index 95eeacb2..7eab928a 100644
--- a/AquaMai/Visual/FixLevelDisplay.cs
+++ b/AquaMai/AquaMai.Mods/Fix/FixLevelDisplay.cs
@@ -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)
diff --git a/AquaMai/Visual/FixSlideAutoPlay.cs b/AquaMai/AquaMai.Mods/Fix/FixSlideAutoPlay.cs
similarity index 97%
rename from AquaMai/Visual/FixSlideAutoPlay.cs
rename to AquaMai/AquaMai.Mods/Fix/FixSlideAutoPlay.cs
index ed04d735..75c28c96 100644
--- a/AquaMai/Visual/FixSlideAutoPlay.cs
+++ b/AquaMai/AquaMai.Mods/Fix/FixSlideAutoPlay.cs
@@ -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:
diff --git a/AquaMai/Fix/FestivalQuickRetryFix.cs b/AquaMai/AquaMai.Mods/Fix/Legacy/FixQuickRetry130.cs
similarity index 78%
rename from AquaMai/Fix/FestivalQuickRetryFix.cs
rename to AquaMai/AquaMai.Mods/Fix/Legacy/FixQuickRetry130.cs
index a41b6986..ea67cdc2 100644
--- a/AquaMai/Fix/FestivalQuickRetryFix.cs
+++ b/AquaMai/AquaMai.Mods/Fix/Legacy/FixQuickRetry130.cs
@@ -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
diff --git a/AquaMai/AquaMai.Mods/Fix/README.md b/AquaMai/AquaMai.Mods/Fix/README.md
new file mode 100644
index 00000000..047dd121
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Fix/README.md
@@ -0,0 +1,7 @@
+# Fix
+
+Fix of the game's bugs or removal of the game's annoying unuseful "features".
+
+Non-removal "Fix" patches should have no (negative, or any visual changing) side-effects on the original game.
+
+All patches under "Fix" should enabled by default and hide in example. They could be still turned off manually in the config.
diff --git a/AquaMai/AquaMai.Mods/Fix/Stability/FixMissingCharaCrash.cs b/AquaMai/AquaMai.Mods/Fix/Stability/FixMissingCharaCrash.cs
new file mode 100644
index 00000000..6baca63f
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Fix/Stability/FixMissingCharaCrash.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using AquaMai.Config.Attributes;
+using HarmonyLib;
+using Process;
+using Util;
+
+namespace AquaMai.Mods.Fix.Stability;
+
+/**
+ * Fix character selection crashing due to missing character data
+ */
+[ConfigSection(exampleHidden: true, defaultOn: true)]
+public class FixMissingCharaCrash
+{
+ // Check if the return is null. If it is, make up a color
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(CharacterSelectProces), "GetMapColorData")]
+ public static void GetMapColorData(ref CharacterSelectProces __instance, ref CharacterMapColorData __result)
+ {
+ if (__result != null) return;
+
+ // 1 is a color that definitely exists
+ if (MapMaster.GetSlotData(1) == null)
+ {
+ MapMaster.GetSlotData(1).Load();
+ }
+ __result = MapMaster.GetSlotData(1);
+ }
+
+ // This is called when loading the music selection screen, to display characters on the top screen
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(Monitor.CommonMonitor), "SetCharacterSlot", new Type[] { typeof(MessageCharactorInfomationData) })]
+ public static bool SetCharacterSlot(ref MessageCharactorInfomationData data, Dictionary ____characterSlotData)
+ {
+ // Some characters are not found in this dictionary. We simply skip loading those characters
+ if (!____characterSlotData.ContainsKey(data.MapKey))
+ {
+ Console.Log($"Could not get CharacterSlotData for character [Index={data.Index}, MapKey={data.MapKey}], ignoring...");
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/AquaMai/AquaMai.Mods/GameSettings/CreditConfig.cs b/AquaMai/AquaMai.Mods/GameSettings/CreditConfig.cs
new file mode 100644
index 00000000..132de3d3
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/GameSettings/CreditConfig.cs
@@ -0,0 +1,49 @@
+using AquaMai.Config.Attributes;
+using AquaMai.Core.Attributes;
+using HarmonyLib;
+
+namespace AquaMai.Mods.GameSettings;
+
+[ConfigSection(
+ en: "Set the game to Paid Play (lock credits) or Free Play.",
+ zh: "设置游戏为付费游玩(锁定可用点数)或免费游玩")]
+public class CreditConfig
+{
+ [ConfigEntry(
+ en: "Set to Free Play (set to false for Paid Play).",
+ zh: "是否免费游玩(设为 false 时为付费游玩)")]
+ private static readonly bool isFreePlay = true;
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(Manager.Credit), "IsFreePlay")]
+ private static bool PreIsFreePlay(ref bool __result)
+ {
+ __result = isFreePlay;
+ return false;
+ }
+
+ [ConfigEntry(
+ en: "Lock credits amount (only valid in Paid Play). Set to 0 to disable.",
+ zh: "锁定可用点数数量(仅在付费游玩时有效),设为 0 以禁用")]
+ private static readonly uint lockCredits = 24;
+
+ private static bool ShouldLockCredits => !isFreePlay && lockCredits > 0;
+
+ [EnableIf(nameof(ShouldLockCredits))]
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(Manager.Credit), "IsGameCostEnough")]
+ private static bool PreIsGameCostEnough(ref bool __result)
+ {
+ __result = true;
+ return false;
+ }
+
+ [EnableIf(nameof(ShouldLockCredits))]
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(AMDaemon.CreditUnit), "Credit", MethodType.Getter)]
+ private static bool PreCredit(ref uint __result)
+ {
+ __result = 24;
+ return false;
+ }
+}
diff --git a/AquaMai/Fix/ForceAsServer.cs b/AquaMai/AquaMai.Mods/GameSettings/ForceAsServer.cs
similarity index 66%
rename from AquaMai/Fix/ForceAsServer.cs
rename to AquaMai/AquaMai.Mods/GameSettings/ForceAsServer.cs
index 71ddbc26..814fc3ee 100644
--- a/AquaMai/Fix/ForceAsServer.cs
+++ b/AquaMai/AquaMai.Mods/GameSettings/ForceAsServer.cs
@@ -1,8 +1,12 @@
using AMDaemon;
+using AquaMai.Config.Attributes;
using HarmonyLib;
-namespace AquaMai.Fix;
+namespace AquaMai.Mods.GameSettings;
+[ConfigSection(
+ en: "If you want to configure in-shop party-link, you should turn this off.",
+ zh: "如果要配置店内招募的话,应该要把这个关闭")]
public class ForceAsServer
{
[HarmonyPrefix]
diff --git a/AquaMai/AquaMai.Mods/GameSettings/JudgeAdjust.cs b/AquaMai/AquaMai.Mods/GameSettings/JudgeAdjust.cs
new file mode 100644
index 00000000..c54adb78
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/GameSettings/JudgeAdjust.cs
@@ -0,0 +1,50 @@
+using System.Threading;
+using AquaMai.Config.Attributes;
+using HarmonyLib;
+using IO;
+using Manager.UserDatas;
+
+namespace AquaMai.Mods.GameSettings;
+
+[ConfigSection(
+ en: "Globally adjust A/B judgment (unit same as in-game options) or increase touch delay.",
+ zh: "全局调整 A/B 判(单位和游戏里一样)或增加触摸延迟")]
+public class JudgeAdjust
+{
+ [ConfigEntry(
+ en: "Adjust A judgment.",
+ zh: "调整 A 判")]
+ private static readonly double a = 0;
+
+ [ConfigEntry(
+ en: "Adjust B judgment.",
+ zh: "调整 B 判")]
+ private static readonly double b = 0;
+
+ [ConfigEntry(
+ en: "Increase touch delay.",
+ zh: "增加触摸延迟")]
+ private static readonly int touchDelay = 0;
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(UserOption), "GetAdjustMSec")]
+ public static void GetAdjustMSec(ref float __result)
+ {
+ __result += (float)(a * 16.666666d);
+ }
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(UserOption), "GetJudgeTimingFrame")]
+ public static void GetJudgeTimingFrame(ref float __result)
+ {
+ __result += (float)b;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(NewTouchPanel), "Recv")]
+ public static void NewTouchPanelRecv()
+ {
+ if (touchDelay <= 0) return;
+ Thread.Sleep(touchDelay);
+ }
+}
diff --git a/AquaMai/AquaMai.Mods/GameSettings/README.md b/AquaMai/AquaMai.Mods/GameSettings/README.md
new file mode 100644
index 00000000..8aa1d897
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/GameSettings/README.md
@@ -0,0 +1,5 @@
+# GameSettings
+
+Overriding or adjusting the game settings that're originally configurable / modifiable, but made into patches for unification, flexibility or convenience.
+
+Patches changing the way the game running / behaving which are not possible in the stock game may need to go to the GameSystem category.
diff --git a/AquaMai/AquaMai.Mods/GameSettings/TouchSensitivity.cs b/AquaMai/AquaMai.Mods/GameSettings/TouchSensitivity.cs
new file mode 100644
index 00000000..181901cf
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/GameSettings/TouchSensitivity.cs
@@ -0,0 +1,154 @@
+using System.Collections.Generic;
+using System.Reflection;
+using AquaMai.Config.Attributes;
+using HarmonyLib;
+using IO;
+using Manager;
+using MelonLoader;
+
+namespace AquaMai.Mods.GameSettings;
+
+[ConfigSection(
+ en: """
+ Use custom touch sensitivity.
+ When enabled, the settings in Test mode will not take effect.
+ When disabled, the settings in Test mode is used.
+
+ Sensitivity adjustments in Test mode are not linear.
+ Default sensitivity in area A: 90, 80, 70, 60, 50, 40, 30, 26, 23, 20, 10.
+ Default sensitivity in other areas: 70, 60, 50, 40, 30, 20, 15, 10, 5, 1, 1.
+ A setting of 0 in Test mode corresponds to 40, 20 here, -5 corresponds to 90, 70, +5 corresponds to 10, 1.
+ The higher the number in Test mode, the lower the number here, resulting in higher sensitivity for official machines.
+ For ADX, the sensitivity is reversed, so the higher the number here, the higher the sensitivity.
+ """,
+ zh: """
+ 使用自定义触摸灵敏度
+ 这里启用之后 Test 里的就不再起作用了
+ 这里禁用之后就还是用 Test 里的调
+
+ 在 Test 模式下调整的灵敏度不是线性的
+ A 区默认灵敏度 90, 80, 70, 60, 50, 40, 30, 26, 23, 20, 10
+ 其他区域默认灵敏度 70, 60, 50, 40, 30, 20, 15, 10, 5, 1, 1
+ Test 里设置的 0 对应的是 40, 20 这一档,-5 是 90, 70,+5 是 10, 1
+ Test 里的数字越大,这里的数字越小,对于官机来说,灵敏度更大
+ 而 ADX 的灵敏度是反的,所以对于 ADX,这里的数字越大,灵敏度越大
+ """)]
+public class TouchSensitivity
+{
+ [ConfigEntry]
+ private static readonly byte A1 = 40;
+
+ [ConfigEntry]
+ private static readonly byte A2 = 40;
+
+ [ConfigEntry]
+ private static readonly byte A3 = 40;
+
+ [ConfigEntry]
+ private static readonly byte A4 = 40;
+
+ [ConfigEntry]
+ private static readonly byte A5 = 40;
+
+ [ConfigEntry]
+ private static readonly byte A6 = 40;
+
+ [ConfigEntry]
+ private static readonly byte A7 = 40;
+
+ [ConfigEntry]
+ private static readonly byte A8 = 40;
+
+ [ConfigEntry]
+ private static readonly byte B1 = 20;
+
+ [ConfigEntry]
+ private static readonly byte B2 = 20;
+
+ [ConfigEntry]
+ private static readonly byte B3 = 20;
+
+ [ConfigEntry]
+ private static readonly byte B4 = 20;
+
+ [ConfigEntry]
+ private static readonly byte B5 = 20;
+
+ [ConfigEntry]
+ private static readonly byte B6 = 20;
+
+ [ConfigEntry]
+ private static readonly byte B7 = 20;
+
+ [ConfigEntry]
+ private static readonly byte B8 = 20;
+
+ [ConfigEntry]
+ private static readonly byte C1 = 20;
+
+ [ConfigEntry]
+ private static readonly byte C2 = 20;
+
+ [ConfigEntry]
+ private static readonly byte D1 = 20;
+
+ [ConfigEntry]
+ private static readonly byte D2 = 20;
+
+ [ConfigEntry]
+ private static readonly byte D3 = 20;
+
+ [ConfigEntry]
+ private static readonly byte D4 = 20;
+
+ [ConfigEntry]
+ private static readonly byte D5 = 20;
+
+ [ConfigEntry]
+ private static readonly byte D6 = 20;
+
+ [ConfigEntry]
+ private static readonly byte D7 = 20;
+
+ [ConfigEntry]
+ private static readonly byte D8 = 20;
+
+ [ConfigEntry]
+ private static readonly byte E1 = 20;
+
+ [ConfigEntry]
+ private static readonly byte E2 = 20;
+
+ [ConfigEntry]
+ private static readonly byte E3 = 20;
+
+ [ConfigEntry]
+ private static readonly byte E4 = 20;
+
+ [ConfigEntry]
+ private static readonly byte E5 = 20;
+
+ [ConfigEntry]
+ private static readonly byte E6 = 20;
+
+ [ConfigEntry]
+ private static readonly byte E7 = 20;
+
+ [ConfigEntry]
+ private static readonly byte E8 = 20;
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(NewTouchPanel), "SetTouchPanelSensitivity")]
+ public static void SetTouchPanelSensitivityPrefix(List sensitivity)
+ {
+ var configType = typeof(TouchSensitivity);
+ for (var i = 0; i < 34; i++)
+ {
+ var area = (InputManager.TouchPanelArea)i;
+ var field = configType.GetField(area.ToString(), BindingFlags.NonPublic | BindingFlags.Static);
+ var value = (byte)field.GetValue(null);
+ sensitivity[i] = value;
+ }
+ MelonLogger.Msg("[TouchSensitivity] Applied");
+ }
+}
diff --git a/AquaMai/AquaMai.Mods/GameSystem/Assets/Fonts.cs b/AquaMai/AquaMai.Mods/GameSystem/Assets/Fonts.cs
new file mode 100644
index 00000000..e05f6218
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/GameSystem/Assets/Fonts.cs
@@ -0,0 +1,146 @@
+using System.Collections.Generic;
+using System.Linq;
+using AquaMai.Config.Attributes;
+using AquaMai.Core.Helpers;
+using HarmonyLib;
+using MelonLoader;
+using TMPro;
+using UnityEngine;
+using UnityEngine.TextCore.LowLevel;
+
+namespace AquaMai.Mods.GameSystem.Assets;
+
+[ConfigSection(
+ en: "Use custom font(s) as fallback or fully replace the original game font.",
+ zh: "使用自定义字体作为回退(解决中文字形缺失问题),或完全替换游戏原字体")]
+public class Fonts
+{
+ [ConfigEntry(
+ en: """
+ Font path(s).
+ Use semicolon to separate multiple paths for a fallback chain.
+ Microsoft YaHei Bold by default.
+ """,
+ zh: """
+ 字体路径
+ 使用分号分隔多个路径以构成 Fallback 链
+ 默认为微软雅黑 Bold
+ """)]
+ private static readonly string paths = "%SYSTEMROOT%/Fonts/msyhbd.ttc";
+
+ [ConfigEntry(
+ en: "Add custom font(s) as fallback, use original game font when possible.",
+ zh: "将自定义字体作为游戏原字体的回退,尽可能使用游戏原字体")]
+ private static readonly bool addAsFallback = true;
+
+ private static List fontAssets = [];
+ private static readonly List processedFonts = [];
+
+ private static TMP_FontAsset replacementFontAsset;
+ private static List fallbackFontAssets = [];
+
+ public static void OnBeforePatch()
+ {
+ var paths = Fonts.paths
+ .Split(';')
+ .Where(p => !string.IsNullOrWhiteSpace(p))
+ .Select(FileSystem.ResolvePath);
+ var fonts = paths
+ .Select(p =>
+ {
+ var font = new Font(p);
+ if (font == null)
+ {
+ MelonLogger.Warning($"[Fonts] Font not found: {p}");
+ }
+ return font;
+ })
+ .Where(f => f != null);
+ fontAssets = fonts
+ .Select(f => TMP_FontAsset.CreateFontAsset(f, 90, 9, GlyphRenderMode.SDFAA, 8192, 8192))
+ .ToList();
+
+ if (fontAssets.Count == 0)
+ {
+ MelonLogger.Warning("[Fonts] No font loaded.");
+ }
+ else if (addAsFallback)
+ {
+ fallbackFontAssets = fontAssets;
+ }
+ else
+ {
+ replacementFontAsset = fontAssets[0];
+ fallbackFontAssets = fontAssets.Skip(1).ToList();
+ }
+ }
+
+ [HarmonyPatch(typeof(TextMeshProUGUI), "Awake")]
+ [HarmonyPostfix]
+ public static void PostAwake(TextMeshProUGUI __instance)
+ {
+ if (fontAssets.Count == 0) return;
+ if (processedFonts.Contains(__instance.font)) return;
+
+ if (replacementFontAsset != null)
+ {
+ ProcessReplacement(__instance);
+ }
+ if (fallbackFontAssets.Count > 0)
+ {
+ ProcessFallback(__instance);
+ }
+
+ processedFonts.Add(__instance.font);
+ }
+
+ private static void ProcessReplacement(TextMeshProUGUI __instance)
+ {
+# if DEBUG
+ MelonLogger.Msg($"{__instance.font.name} {__instance.text}");
+# endif
+
+ var materialOrigin = __instance.fontMaterial;
+ var materialSharedOrigin = __instance.fontSharedMaterial;
+ __instance.font = replacementFontAsset;
+
+# if DEBUG
+ MelonLogger.Msg($"shaderKeywords {materialOrigin.shaderKeywords.Join()} {__instance.fontMaterial.shaderKeywords.Join()}");
+# endif
+ // __instance.fontSharedMaterial = materialSharedOrigin;
+
+ // 这样之后该有描边的地方整个字后面都是阴影,它不知道哪里是边
+ // materialOrigin.mainTexture = __instance.fontMaterial.mainTexture;
+ // materialOrigin.mainTextureOffset = __instance.fontMaterial.mainTextureOffset;
+ // materialOrigin.mainTextureScale = __instance.fontMaterial.mainTextureScale;
+ // __instance.fontMaterial.CopyPropertiesFromMaterial(materialOrigin);
+
+ // 这样了之后有描边了,但是描边很细
+ // __instance.fontMaterial.shader = materialOrigin.shader;
+ foreach (var keyword in materialOrigin.shaderKeywords)
+ {
+ __instance.fontMaterial.EnableKeyword(keyword);
+ }
+ // __instance.fontMaterial.globalIlluminationFlags = materialOrigin.globalIlluminationFlags;
+
+ // 原来是 underlay,但是复制这三个属性之后就又变成整个字后面都是阴影了
+ // __instance.fontMaterial.SetFloat(ShaderUtilities.ID_UnderlayOffsetY, materialOrigin.GetFloat(ShaderUtilities.ID_UnderlayOffsetY));
+ // __instance.fontMaterial.SetFloat(ShaderUtilities.ID_UnderlayOffsetX, materialOrigin.GetFloat(ShaderUtilities.ID_UnderlayOffsetX));
+ // __instance.fontMaterial.SetFloat(ShaderUtilities.ID_UnderlayDilate, materialOrigin.GetFloat(ShaderUtilities.ID_UnderlayDilate));
+
+ // if(materialOrigin.shaderKeywords.Contains(ShaderUtilities.Keyword_Underlay))
+ // {
+ // __instance.fontMaterial.EnableKeyword(ShaderUtilities.Keyword_Glow);
+ // __instance.fontMaterial.SetFloat(ShaderUtilities.ID_GlowOuter, .5f);
+ // // __instance.fontMaterial.SetFloat(ShaderUtilities.ID_UnderlayOffsetX, materialOrigin.GetFloat(ShaderUtilities.ID_UnderlayOffsetX));
+ // }
+ }
+
+ private static void ProcessFallback(TextMeshProUGUI __instance)
+ {
+ foreach (var fontAsset in fallbackFontAssets)
+ {
+ __instance.font.fallbackFontAssetTable.Add(fontAsset);
+ }
+ }
+}
diff --git a/AquaMai/AquaMai.Mods/GameSystem/Assets/LoadAssetBundleWithoutManifest.cs b/AquaMai/AquaMai.Mods/GameSystem/Assets/LoadAssetBundleWithoutManifest.cs
new file mode 100644
index 00000000..02b7f7ac
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/GameSystem/Assets/LoadAssetBundleWithoutManifest.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+using System.Linq;
+using HarmonyLib;
+using UnityEngine;
+using Manager;
+using Util;
+using AquaMai.Config.Attributes;
+
+namespace AquaMai.Mods.GameSystem.Assets;
+
+[ConfigSection(
+ en: "Load all existing \".ab\" image resources regardless of the AssetBundleImages manifest.",
+ zh: """
+ 加载所有存在的 .ab 图片资源(无视 AssetBundleImages.manifest)
+ 导入了删除曲包之类的话,应该需要开启这个
+ """)]
+public class LoadAssetBundleWithoutManifest
+{
+ private static HashSet abFiles = new HashSet();
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(OptionDataManager), "CheckAssetBundle")]
+ public static void PostCheckAssetBundle(ref Safe.ReadonlySortedDictionary abs)
+ {
+ foreach (var ab in abs)
+ {
+ abFiles.Add(ab.Key);
+ }
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(AssetBundleManifest), "GetAllAssetBundles")]
+ public static bool PreGetAllAssetBundles(AssetBundleManifest __instance, ref string[] __result)
+ {
+ __result = abFiles.ToArray();
+ return false;
+ }
+}
diff --git a/AquaMai/UX/LoadAssetsPng.cs b/AquaMai/AquaMai.Mods/GameSystem/Assets/LoadLocalImages.cs
similarity index 91%
rename from AquaMai/UX/LoadAssetsPng.cs
rename to AquaMai/AquaMai.Mods/GameSystem/Assets/LoadLocalImages.cs
index 5a3f5a9d..72152de9 100644
--- a/AquaMai/UX/LoadAssetsPng.cs
+++ b/AquaMai/AquaMai.Mods/GameSystem/Assets/LoadLocalImages.cs
@@ -1,4 +1,3 @@
-using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -10,26 +9,31 @@ using MAI2.Util;
using Manager;
using MelonLoader;
using Monitor;
-using Object = UnityEngine.Object;
+using AquaMai.Config.Attributes;
+using AquaMai.Core.Helpers;
-namespace AquaMai.UX;
+namespace AquaMai.Mods.GameSystem.Assets;
-public class LoadAssetsPng
+[ConfigSection(
+ en: "Load asset images from the configured directory (for self-made charts).",
+ zh: "从指定目录下加载资源图片(自制谱用)")]
+public class LoadLocalImages
{
- private static string[] imageExts = [".jpg", ".png", ".jpeg"];
- private static Dictionary jacketPaths = new();
- private static Dictionary framePaths = new();
- private static Dictionary platePaths = new();
- private static Dictionary framemaskPaths = new();
- private static Dictionary framepatternPaths = new();
- private static Dictionary iconPaths = new();
- private static Dictionary charaPaths = new();
+ [ConfigEntry]
+ private static readonly string localAssetsDir = "LocalAssets";
- private static Dictionary partnerPaths = new();
-
- //private static Dictionary navicharaPaths = new();
- private static Dictionary tabTitlePaths = new();
- private static Dictionary localAssetsContents = new();
+ private static readonly string[] imageExts = [".jpg", ".png", ".jpeg"];
+ private static readonly Dictionary jacketPaths = [];
+ private static readonly Dictionary framePaths = [];
+ private static readonly Dictionary platePaths = [];
+ private static readonly Dictionary framemaskPaths = [];
+ private static readonly Dictionary framepatternPaths = [];
+ private static readonly Dictionary iconPaths = [];
+ private static readonly Dictionary charaPaths = [];
+ private static readonly Dictionary partnerPaths = [];
+ //private static readonly Dictionary navicharaPaths = [];
+ private static readonly Dictionary tabTitlePaths = [];
+ private static readonly Dictionary localAssetsContents = [];
[HarmonyPrefix]
[HarmonyPatch(typeof(DataManager), "LoadMusicBase")]
@@ -116,16 +120,17 @@ public class LoadAssetsPng
}
}
- MelonLogger.Msg($"[LoadAssetsPng] Loaded {jacketPaths.Count} Jacket, {platePaths.Count} NamePlate, {framePaths.Count} Frame, {framemaskPaths.Count} FrameMask, {framepatternPaths.Count} FramePattern, {iconPaths.Count} Icon, {charaPaths.Count} Chara, {partnerPaths.Count} PartnerLogo, {tabTitlePaths.Count} Tab Titles from AssetBundleImages.");
+ MelonLogger.Msg($"[LoadLocalImages] Loaded {jacketPaths.Count} Jacket, {platePaths.Count} NamePlate, {framePaths.Count} Frame, {framemaskPaths.Count} FrameMask, {framepatternPaths.Count} FramePattern, {iconPaths.Count} Icon, {charaPaths.Count} Chara, {partnerPaths.Count} PartnerLogo, {tabTitlePaths.Count} Tab Titles from AssetBundleImages.");
- if (Directory.Exists(Path.Combine(Environment.CurrentDirectory, "LocalAssets")))
- foreach (var laFile in Directory.EnumerateFiles(Path.Combine(Environment.CurrentDirectory, "LocalAssets")))
+ var resolvedDir = FileSystem.ResolvePath(localAssetsDir);
+ if (Directory.Exists(resolvedDir))
+ foreach (var laFile in Directory.EnumerateFiles(resolvedDir))
{
if (!imageExts.Contains(Path.GetExtension(laFile).ToLowerInvariant())) continue;
localAssetsContents[Path.GetFileNameWithoutExtension(laFile).ToLowerInvariant()] = laFile;
}
- MelonLogger.Msg($"[LoadAssetsPng] Loaded {localAssetsContents.Count} LocalAssets.");
+ MelonLogger.Msg($"[LoadLocalImages] Loaded {localAssetsContents.Count} LocalAssets.");
}
private static string GetJacketPath(string id)
@@ -483,7 +488,6 @@ public class LoadAssetsPng
return true;
}
- MelonLogger.Msg(filename);
var id = matches[0].Groups[1].Value;
var texture = GetIconTexture2D(id);
@@ -570,4 +574,4 @@ public class LoadAssetsPng
}
}
*/
-}
\ No newline at end of file
+}
diff --git a/AquaMai/UX/LoadLocalBga.cs b/AquaMai/AquaMai.Mods/GameSystem/Assets/UseJacketAsDummyMovie.cs
similarity index 73%
rename from AquaMai/UX/LoadLocalBga.cs
rename to AquaMai/AquaMai.Mods/GameSystem/Assets/UseJacketAsDummyMovie.cs
index 2f26f422..a39fb265 100644
--- a/AquaMai/UX/LoadLocalBga.cs
+++ b/AquaMai/AquaMai.Mods/GameSystem/Assets/UseJacketAsDummyMovie.cs
@@ -1,4 +1,5 @@
using System.Linq;
+using AquaMai.Config.Attributes;
using HarmonyLib;
using MAI2.Util;
using Manager;
@@ -6,9 +7,18 @@ using MelonLoader;
using Monitor.Game;
using UnityEngine;
-namespace AquaMai.UX;
+namespace AquaMai.Mods.GameSystem.Assets;
-public class LoadLocalBga
+[ConfigSection(
+ en: """
+ Use the png jacket above as MV if no .dat found in the movie folder.
+ Use together with `LoadLocalImages`.
+ """,
+ zh: """
+ 如果 movie 文件夹中没有 dat 格式的 MV 的话,就用歌曲的封面做背景,而不是显示迪拉熊的笑脸
+ 请和 `LoadLocalImages` 一起用
+ """)]
+public class UseJacketAsDummyMovie
{
[HarmonyPostfix]
[HarmonyPatch(typeof(GameCtrl), "IsReady")]
@@ -20,7 +30,7 @@ public class LoadLocalBga
var moviePath = Singleton.Instance.GetMovieDataPath($"{music.movieName.id:000000}") + ".dat";
if (!moviePath.Contains("dummy")) return;
- var jacket = LoadAssetsPng.GetJacketTexture2D(music.movieName.id);
+ var jacket = LoadLocalImages.GetJacketTexture2D(music.movieName.id);
if (jacket is null)
{
MelonLogger.Msg("No jacket found for music " + music);
diff --git a/AquaMai/CustomCameraId/Enable.cs b/AquaMai/AquaMai.Mods/GameSystem/CustomCameraId.cs
similarity index 50%
rename from AquaMai/CustomCameraId/Enable.cs
rename to AquaMai/AquaMai.Mods/GameSystem/CustomCameraId.cs
index 737bc6b9..f659d644 100644
--- a/AquaMai/CustomCameraId/Enable.cs
+++ b/AquaMai/AquaMai.Mods/GameSystem/CustomCameraId.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
@@ -6,12 +6,47 @@ using HarmonyLib;
using Manager;
using MelonLoader;
using UnityEngine;
+using AquaMai.Config.Attributes;
-namespace AquaMai.CustomCameraId;
+namespace AquaMai.Mods.GameSystem;
-public class Enable
+[ConfigSection(
+ en: """
+ Use custom CameraId rather than the default ones.
+ If enabled, you can customize the game to use the specified camera.
+ """,
+ zh: """
+ 使用自定义的摄像头 ID 而不是默认的
+ 启用后可以指定游戏使用的摄像头
+ """)]
+public class CustomCameraId
{
- private static readonly Dictionary CameraTypeMap = new Dictionary
+ [ConfigEntry(
+ en: "Print the camera list to the log when starting, can be used as a basis for modification.",
+ zh: "启动时打印摄像头列表到日志中,可以作为修改的依据")]
+ public static bool printCameraList;
+
+ [ConfigEntry(
+ en: "DX Pass 1P.",
+ zh: "DX Pass 1P")]
+ public static int leftQrCamera;
+
+ [ConfigEntry(
+ en: "DX Pass 2P.",
+ zh: "DX Pass 2P")]
+ public static int rightQrCamera;
+
+ [ConfigEntry(
+ en: "Player Camera.",
+ zh: "玩家摄像头")]
+ public static int photoCamera;
+
+ [ConfigEntry(
+ en: "WeChat QRCode Camera.",
+ zh: "二维码扫描摄像头")]
+ public static int chimeCamera;
+
+ private static readonly Dictionary cameraTypeMap = new()
{
["LeftQrCamera"] = "QRLeft",
["RightQrCamera"] = "QRRight",
@@ -31,9 +66,9 @@ public class Enable
{
var textureCache = new WebCamTexture[WebCamTexture.devices.Length];
SortedDictionary webCamTextures = [];
- foreach (var (configEntry, cameraTypeName) in CameraTypeMap)
+ foreach (var (configEntry, cameraTypeName) in cameraTypeMap)
{
- int deviceId = Traverse.Create(AquaMai.AppConfig.CustomCameraId).Field(configEntry).GetValue();
+ int deviceId = Traverse.Create(typeof(CustomCameraId)).Field(configEntry).GetValue();
if (deviceId < 0 || deviceId >= WebCamTexture.devices.Length)
{
MelonLogger.Warning($"[CustomCameraId] Ignoring custom camera {configEntry}: camera ID {deviceId} out of range");
@@ -75,4 +110,33 @@ public class Enable
CameraManager.IsReady = true;
yield break;
}
+
+ public static void OnBeforePatch()
+ {
+ if (!printCameraList)
+ {
+ return;
+ }
+
+ WebCamDevice[] devices = WebCamTexture.devices;
+ string cameraList = "Connected Web Cameras:\n";
+ for (int i = 0; i < devices.Length; i++)
+ {
+ WebCamDevice webCamDevice = devices[i];
+ WebCamTexture webCamTexture = new WebCamTexture(webCamDevice.name);
+ webCamTexture.Play();
+ cameraList += "==================================================\n";
+ cameraList += "Name: " + webCamDevice.name + "\n";
+ cameraList += $"ID: {i}\n";
+ cameraList += $"Resolution: {webCamTexture.width} * {webCamTexture.height}\n";
+ cameraList += $"FPS: {webCamTexture.requestedFPS}\n";
+ webCamTexture.Stop();
+ }
+ cameraList += "==================================================";
+
+ foreach (var line in cameraList.Split('\n'))
+ {
+ MelonLogger.Msg($"[CustomCameraId] {line}");
+ }
+ }
}
diff --git a/AquaMai/AquaMai.Mods/GameSystem/DisableTimeout.cs b/AquaMai/AquaMai.Mods/GameSystem/DisableTimeout.cs
new file mode 100644
index 00000000..14eee957
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/GameSystem/DisableTimeout.cs
@@ -0,0 +1,65 @@
+using AquaMai.Config.Attributes;
+using HarmonyLib;
+using Manager;
+using Monitor;
+using Process;
+using Process.Entry.State;
+using Process.ModeSelect;
+
+namespace AquaMai.Mods.GameSystem;
+
+[ConfigSection(
+ en: """
+ Disable timers (hidden and set to 65535 seconds).
+ Not recommand to enable when SinglePlayer is off.
+ """,
+ zh: """
+ 去除游戏中的倒计时(隐藏并设为 65535 秒)
+ 没有开启单人模式时,不建议启用
+ """)]
+public class DisableTimeout
+{
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(TimerController), "PrepareTimer")]
+ public static void PrePrepareTimer(ref int second)
+ {
+ second = 65535;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(CommonTimer), "SetVisible")]
+ public static void CommonTimerSetVisible(ref bool isVisible)
+ {
+ isVisible = false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(EntryProcess), "DecrementTimerSecond")]
+ public static bool EntryProcessDecrementTimerSecond(ContextEntry ____context)
+ {
+ SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_SYS_SKIP, 0);
+ ____context.SetState(StateType.DoneEntry);
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(ModeSelectProcess), "UpdateInput")]
+ public static bool ModeSelectProcessUpdateInput(ModeSelectProcess __instance)
+ {
+ if (!InputManager.GetButtonDown(0, InputManager.ButtonSetting.Button05)) return true;
+ __instance.TimeSkipButtonAnim(InputManager.ButtonSetting.Button05);
+ SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_SYS_SKIP, 0);
+ Traverse.Create(__instance).Method("TimeUp").GetValue();
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(PhotoEditProcess), "MainMenuUpdate")]
+ public static void PhotoEditProcess(PhotoEditMonitor[] ____monitors, PhotoEditProcess __instance)
+ {
+ if (!InputManager.GetButtonDown(0, InputManager.ButtonSetting.Button04)) return;
+ SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_SYS_SKIP, 0);
+ ____monitors[0].SetButtonPressed(InputManager.ButtonSetting.Button04);
+ Traverse.Create(__instance).Method("OnTimeUp").GetValue();
+ }
+}
diff --git a/AquaMai/AquaMai.Mods/GameSystem/KeyMap.cs b/AquaMai/AquaMai.Mods/GameSystem/KeyMap.cs
new file mode 100644
index 00000000..49971604
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/GameSystem/KeyMap.cs
@@ -0,0 +1,80 @@
+using System.Reflection;
+using AquaMai.Config.Attributes;
+using AquaMai.Config.Types;
+using HarmonyLib;
+
+namespace AquaMai.Mods.GameSystem;
+
+[ConfigSection(
+ en: "These settings will work regardless of whether you have enabled segatools' io4 emulation.",
+ zh: "这里的设置无论你是否启用了 segatools 的 io4 模拟都会工作")]
+public class KeyMap
+{
+ [ConfigEntry]
+ public static readonly KeyCodeID Test = (KeyCodeID)115;
+
+ [ConfigEntry]
+ private static readonly KeyCodeID Service = (KeyCodeID)5;
+
+ [ConfigEntry]
+ private static readonly KeyCodeID Button1_1P = (KeyCodeID)67;
+
+ [ConfigEntry]
+ private static readonly KeyCodeID Button2_1P = (KeyCodeID)49;
+
+ [ConfigEntry]
+ private static readonly KeyCodeID Button3_1P = (KeyCodeID)48;
+
+ [ConfigEntry]
+ private static readonly KeyCodeID Button4_1P = (KeyCodeID)47;
+
+ [ConfigEntry]
+ private static readonly KeyCodeID Button5_1P = (KeyCodeID)68;
+
+ [ConfigEntry]
+ private static readonly KeyCodeID Button6_1P = (KeyCodeID)70;
+
+ [ConfigEntry]
+ private static readonly KeyCodeID Button7_1P = (KeyCodeID)45;
+
+ [ConfigEntry]
+ private static readonly KeyCodeID Button8_1P = (KeyCodeID)61;
+
+ [ConfigEntry]
+ private static readonly KeyCodeID Select_1P = (KeyCodeID)25;
+
+ [ConfigEntry]
+ private static readonly KeyCodeID Button1_2P = (KeyCodeID)80;
+
+ [ConfigEntry]
+ private static readonly KeyCodeID Button2_2P = (KeyCodeID)81;
+
+ [ConfigEntry]
+ private static readonly KeyCodeID Button3_2P = (KeyCodeID)78;
+
+ [ConfigEntry]
+ private static readonly KeyCodeID Button4_2P = (KeyCodeID)75;
+
+ [ConfigEntry]
+ private static readonly KeyCodeID Button5_2P = (KeyCodeID)74;
+
+ [ConfigEntry]
+ private static readonly KeyCodeID Button6_2P = (KeyCodeID)73;
+
+ [ConfigEntry]
+ private static readonly KeyCodeID Button7_2P = (KeyCodeID)76;
+
+ [ConfigEntry]
+ private static readonly KeyCodeID Button8_2P = (KeyCodeID)79;
+
+ [ConfigEntry]
+ private static readonly KeyCodeID Select_2P = (KeyCodeID)84;
+
+ [HarmonyPatch(typeof(DB.JvsButtonTableRecord), MethodType.Constructor, typeof(int), typeof(string), typeof(string), typeof(int), typeof(string), typeof(int), typeof(int), typeof(int))]
+ [HarmonyPostfix]
+ public static void JvsButtonTableRecordConstructor(DB.JvsButtonTableRecord __instance, string Name)
+ {
+ var prop = (DB.KeyCodeID)typeof(KeyMap).GetField(Name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).GetValue(null);
+ __instance.SubstituteKey = prop;
+ }
+}
diff --git a/AquaMai/AquaMai.Mods/GameSystem/QuickRetry.cs b/AquaMai/AquaMai.Mods/GameSystem/QuickRetry.cs
new file mode 100644
index 00000000..e84f9f65
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/GameSystem/QuickRetry.cs
@@ -0,0 +1,34 @@
+using AquaMai.Core.Attributes;
+using AquaMai.Config.Attributes;
+using HarmonyLib;
+using Manager;
+
+namespace AquaMai.Mods.GameSystem;
+
+[ConfigSection(
+ en: "Hold the bottom four buttons (3456) for quick retry (like in Freedom Mode, default non-utage only).",
+ zh: "按住下方四个按钮(3456)快速重开本局游戏(像在 Freedom Mode 中一样,默认仅对非宴谱有效)")]
+[EnableGameVersion(23000)]
+public class QuickRetry
+{
+ [ConfigEntry(
+ en: "Force enable in Utage.",
+ zh: "在宴谱中强制启用")]
+ private static readonly bool enableInUtage = false;
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(Monitor.QuickRetry), "IsQuickRetryEnable")]
+ public static bool OnQuickRetryIsQuickRetryEnable(ref bool __result)
+ {
+ if (enableInUtage)
+ {
+ __result = true;
+ }
+ else
+ {
+ var isUtageProperty = Traverse.Create(typeof(GameManager)).Property("IsUtage");
+ __result = !isUtageProperty.PropertyExists() || !isUtageProperty.GetValue();
+ }
+ return false;
+ }
+}
diff --git a/AquaMai/AquaMai.Mods/GameSystem/README.md b/AquaMai/AquaMai.Mods/GameSystem/README.md
new file mode 100644
index 00000000..e9b0be73
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/GameSystem/README.md
@@ -0,0 +1,5 @@
+# GameSystem
+
+Patches changing the way the game running / behaving which are not possible in the stock game. See also the [GameSettings README](../GameSettings/README.md) for differences.
+
+Game asset related patches should go to the Assets subcategory (or the Fancy category if they're too fancy).
diff --git a/AquaMai/Fix/RemoveEncryption.cs b/AquaMai/AquaMai.Mods/GameSystem/RemoveEncryption.cs
similarity index 64%
rename from AquaMai/Fix/RemoveEncryption.cs
rename to AquaMai/AquaMai.Mods/GameSystem/RemoveEncryption.cs
index 2a29d9f8..7b1ce8db 100644
--- a/AquaMai/Fix/RemoveEncryption.cs
+++ b/AquaMai/AquaMai.Mods/GameSystem/RemoveEncryption.cs
@@ -1,11 +1,24 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
+using AquaMai.Config.Attributes;
using HarmonyLib;
using Net.Packet;
-namespace AquaMai.Fix;
+namespace AquaMai.Mods.GameSystem;
+[ConfigSection(
+ en: """
+ If you are using an unmodified client, requests to the server will be encrypted by default, but requests to the private server should not be encrypted.
+ With this option enabled, the connection will not be encrypted, and the suffix added by different versions of the client to the API names are also removed.
+ Please keep this option enabled normally.
+ """,
+ zh: """
+ 如果你在用未经修改的客户端,会默认加密到服务器的连接,而连接私服的时候不应该加密
+ 开了这个选项之后就不会加密连接了,同时也会移除不同版本的客户端可能会对 API 接口加的后缀
+ 正常情况下,请保持这个选项开启
+ """,
+ defaultOn: true)]
public class RemoveEncryption
{
[HarmonyPrefix]
diff --git a/AquaMai/AquaMai.Mods/GameSystem/SinglePlayer.cs b/AquaMai/AquaMai.Mods/GameSystem/SinglePlayer.cs
new file mode 100644
index 00000000..be6c1edc
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/GameSystem/SinglePlayer.cs
@@ -0,0 +1,114 @@
+using System.Collections.Generic;
+using System.Reflection;
+using AquaMai.Core;
+using AquaMai.Core.Attributes;
+using AquaMai.Config.Attributes;
+using AquaMai.Mods.Fancy.GamePlay;
+using HarmonyLib;
+using MAI2.Util;
+using Manager;
+using MelonLoader;
+using Monitor;
+using Monitor.Common;
+using Monitor.Entry;
+using Monitor.Entry.Parts.Screens;
+using UnityEngine;
+using Fx;
+using Type = System.Type;
+
+namespace AquaMai.Mods.GameSystem;
+
+// Hides the 2p (right hand side) UI.
+// Note: this is not my original work. I simply interpreted the code and rewrote it as a mod.
+[ConfigSection(
+ en: "Single player: Show 1P only, at the center of the screen.",
+ zh: "单人模式,不显示 2P")]
+public class SinglePlayer
+{
+ [HarmonyPatch]
+ public class WhateverInitialize
+ {
+ public static IEnumerable TargetMethods()
+ {
+ var lateInitialize = AccessTools.Method(typeof(Main.GameMain), "LateInitialize", [typeof(MonoBehaviour), typeof(Transform), typeof(Transform)]);
+ if (lateInitialize is not null) return [lateInitialize];
+ return [AccessTools.Method(typeof(Main.GameMain), "Initialize", [typeof(MonoBehaviour), typeof(Transform), typeof(Transform)])];
+ }
+
+ public static void Prefix(MonoBehaviour gameMainObject, ref Transform left, ref Transform right)
+ {
+ left.transform.position = Vector3.zero;
+ right.localScale = Vector3.zero;
+ GameObject.Find("Mask").transform.position = new Vector3(540f, 0f, 0f);
+ }
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(MeshButton), "IsPointInPolygon", new Type[] { typeof(Vector2[]), typeof(Vector2) })]
+ public static bool IsPointInPolygon(Vector2[] polygon, ref Vector2 point, MeshButton __instance, ref bool __result)
+ {
+ __result = RectTransformUtility.RectangleContainsScreenPoint(__instance.GetComponent(), point, Camera.main);
+ return false;
+ }
+
+ [EnableGameVersion(21500, noWarn: true)]
+ public class SkipTimer
+ {
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(EntryMonitor), "DecideEntry")]
+ public static void PostDecideEntry(EntryMonitor __instance)
+ {
+# if DEBUG
+ MelonLogger.Msg("Confirm Entry");
+# endif
+ TimeManager.MarkGameStartTime();
+ Singleton.Instance.UpdateEvent();
+ Singleton.Instance.UpdateData();
+ __instance.Process.CreateDownloadProcess();
+ __instance.ProcessManager.SendMessage(new Message(ProcessType.CommonProcess, 30001));
+ __instance.ProcessManager.SendMessage(new Message(ProcessType.CommonProcess, 40000, 0, OperationInformationController.InformationType.Hide));
+ __instance.Process.SetNextProcess();
+ }
+
+ // To prevent the "長押受付終了" overlay from appearing
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(WaitPartner), "Open")]
+ public static bool WaitPartnerPreOpen()
+ {
+ return false;
+ }
+ }
+
+ [ConfigEntry(
+ en: "Fix hanabi effect under single-player mode (disabled automatically if HideHanabi is enabled).",
+ zh: "修复单人模式下的烟花效果(如果启用了 HideHanabi,则会自动禁用)")]
+ public static bool fixHanabi = true;
+
+ private static bool fixHanabiDisableImplied = false;
+ private static bool FixHanabiEnabled => fixHanabi && !fixHanabiDisableImplied;
+
+ [EnableIf(nameof(FixHanabiEnabled))]
+ [HarmonyPatch(typeof(TapCEffect), "SetUpParticle")]
+ [HarmonyPostfix]
+ public static void PostSetUpParticle(TapCEffect __instance, FX_Mai2_Note_Color ____particleControler)
+ {
+ var entities = ____particleControler.GetComponentsInChildren(true);
+ foreach (var entity in entities)
+ {
+ entity.maxParticleSize = 1f;
+ }
+ }
+
+ public static void OnBeforePatch()
+ {
+ if (ConfigLoader.Config.GetSectionState(typeof(HideHanabi)).Enabled)
+ {
+ fixHanabiDisableImplied = true;
+ }
+ }
+
+ public static void OnAfterPatch()
+ {
+ Core.Helpers.GuiSizes.SinglePlayer = true;
+ }
+}
diff --git a/AquaMai/AquaMai.Mods/GameSystem/TestProof.cs b/AquaMai/AquaMai.Mods/GameSystem/TestProof.cs
new file mode 100644
index 00000000..0db5db06
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/GameSystem/TestProof.cs
@@ -0,0 +1,69 @@
+using System.Diagnostics;
+using System.Linq;
+using AquaMai.Config.Attributes;
+using AquaMai.Config.Types;
+using AquaMai.Core;
+using AquaMai.Core.Attributes;
+using AquaMai.Core.Helpers;
+using AquaMai.Mods.UX;
+using AquaMai.Mods.UX.PracticeMode;
+using HarmonyLib;
+using Manager;
+
+namespace AquaMai.Mods.GameSystem;
+
+[ConfigSection(
+ en: """
+ When enabled, test button must be long pressed to enter game test mode.
+ When test button is bound to other features, this option is enabled automatically.
+ """,
+ zh: """
+ 启用后,测试键必须长按才能进入游戏测试模式
+ 当测试键被绑定到其它功能时,此选项自动开启
+ """)]
+[EnableImplicitlyIf(nameof(ShouldEnableImplicitly))]
+public class TestProof
+{
+ public static bool ShouldEnableImplicitly
+ {
+ get
+ {
+ (System.Type section, KeyCodeOrName key)[] featureKeys =
+ [
+ (typeof(OneKeyEntryEnd), OneKeyEntryEnd.key),
+ (typeof(OneKeyRetrySkip), OneKeyRetrySkip.retryKey),
+ (typeof(OneKeyRetrySkip), OneKeyRetrySkip.skipKey),
+ (typeof(HideSelfMadeCharts), HideSelfMadeCharts.key),
+ (typeof(PracticeMode), PracticeMode.key),
+ ];
+ var keyMapEnabled = ConfigLoader.Config.GetSectionState(typeof(KeyMap)).Enabled;
+ return featureKeys.Any(it =>
+ // The feature is enabled and...
+ ConfigLoader.Config.GetSectionState(it.section).Enabled &&
+ (
+ // and the key is test, or...
+ it.key == KeyCodeOrName.Test ||
+ // or the key have been mapped to the same key as test.
+ (keyMapEnabled && it.key.ToString() == KeyMap.Test.ToString())));
+ }
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(InputManager), "GetSystemInputDown")]
+ public static bool GetSystemInputDown(ref bool __result, InputManager.SystemButtonSetting button, bool[] ___SystemButtonDown)
+ {
+ __result = ___SystemButtonDown[(int)button];
+ if (button != InputManager.SystemButtonSetting.ButtonTest)
+ return false;
+
+ var stackTrace = new StackTrace(); // get call stack
+ var stackFrames = stackTrace.GetFrames(); // get method calls (frames)
+
+ if (stackFrames.Any(it => it.GetMethod().Name == "DMD"))
+ {
+ __result = KeyListener.GetKeyDownOrLongPress(KeyCodeOrName.Test, true);
+ }
+
+ return false;
+ }
+}
diff --git a/AquaMai/AquaMai.Mods/GameSystem/TouchPanelBaudRate.cs b/AquaMai/AquaMai.Mods/GameSystem/TouchPanelBaudRate.cs
new file mode 100644
index 00000000..e9294249
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/GameSystem/TouchPanelBaudRate.cs
@@ -0,0 +1,30 @@
+using AquaMai.Config.Attributes;
+using HarmonyLib;
+using IO;
+
+namespace AquaMai.Mods.GameSystem;
+
+[ConfigSection(
+ en: """
+ Adjust the baud rate of the touch screen serial port, default value is 9600.
+ Requires hardware support. If you are unsure, don't use it.
+ """,
+ zh: """
+ 调整触摸屏串口波特率,默认值 9600
+ 需要硬件配合,如果你不清楚你是否可以使用,请不要使用
+ """)]
+public class TouchPanelBaudRate
+{
+ [ConfigEntry(
+ en: "Baud rate.",
+ zh: "波特率")]
+ private static readonly int baudRate = 9600;
+
+ [HarmonyPatch(typeof(NewTouchPanel), "Open")]
+ [HarmonyPrefix]
+ private static void OpenPrefix(ref int ___BaudRate)
+ {
+ if (baudRate <= 0) return;
+ ___BaudRate = baudRate;
+ }
+}
diff --git a/AquaMai/UX/TouchToButtonInput.cs b/AquaMai/AquaMai.Mods/GameSystem/TouchToButtonInput.cs
similarity index 90%
rename from AquaMai/UX/TouchToButtonInput.cs
rename to AquaMai/AquaMai.Mods/GameSystem/TouchToButtonInput.cs
index 6d0c09fe..ed8ef966 100644
--- a/AquaMai/UX/TouchToButtonInput.cs
+++ b/AquaMai/AquaMai.Mods/GameSystem/TouchToButtonInput.cs
@@ -1,10 +1,13 @@
-using HarmonyLib;
-using Manager;
+using AquaMai.Config.Attributes;
+using HarmonyLib;
using Process;
using static Manager.InputManager;
-namespace AquaMai.UX;
+namespace AquaMai.Mods.GameSystem;
+[ConfigSection(
+ en: "Map touch actions to buttons.",
+ zh: "映射触摸操作至实体按键")]
public class TouchToButtonInput
{
private static bool _isPlaying = false;
diff --git a/AquaMai/AquaMai.Mods/GameSystem/Unlock.cs b/AquaMai/AquaMai.Mods/GameSystem/Unlock.cs
new file mode 100644
index 00000000..0e867813
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/GameSystem/Unlock.cs
@@ -0,0 +1,90 @@
+using AquaMai.Config.Attributes;
+using AquaMai.Core.Attributes;
+using MAI2System;
+using Manager;
+using Manager.MaiStudio;
+using HarmonyLib;
+
+namespace AquaMai.Mods.GameSystem;
+
+[ConfigSection(
+ en: "Unlock normally locked (including normally non-unlockable) game content.",
+ zh: "解锁原本锁定(包括正常途径无法解锁)的游戏内容")]
+public class Unlock
+{
+ [ConfigEntry(
+ en: "Unlock maps that are not in this version.",
+ zh: "解锁游戏里所有的区域,包括非当前版本的(并不会帮你跑完)")]
+ private static readonly bool maps = true;
+
+ [EnableIf(nameof(maps))]
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(MapData), "get_OpenEventId")]
+ public static bool get_OpenEventId(ref StringID __result)
+ {
+ // For any map, return the event ID 1 to unlock it
+ var id = new Manager.MaiStudio.Serialize.StringID
+ {
+ id = 1,
+ str = "無期限常時解放"
+ };
+
+ var sid = new StringID();
+ sid.Init(id);
+
+ __result = sid;
+ return false;
+ }
+
+ [ConfigEntry(
+ en: "Unlock normally event-only tickets.",
+ zh: "解锁游戏里所有可能的跑图券")]
+ private static readonly bool tickets = true;
+
+ [EnableIf(nameof(tickets))]
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(TicketData), "get_ticketEvent")]
+ public static bool get_ticketEvent(ref StringID __result)
+ {
+ // For any ticket, return the event ID 1 to unlock it
+ var id = new Manager.MaiStudio.Serialize.StringID
+ {
+ id = 1,
+ str = "無期限常時解放"
+ };
+
+ var sid = new StringID();
+ sid.Init(id);
+
+ __result = sid;
+ return false;
+ }
+
+ [EnableIf(nameof(tickets))]
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(TicketData), "get_maxCount")]
+ public static bool get_maxCount(ref int __result)
+ {
+ // Modify the maxTicketNum to 0
+ // this is because TicketManager.GetTicketData adds the ticket to the list if either
+ // the player owns at least one ticket or the maxTicketNum = 0
+ __result = 0;
+ return false;
+ }
+
+ [ConfigEntry(
+ en: "Unlock Utage without the need of DXRating 10000.",
+ zh: "不需要万分也可以进宴会场")]
+ private static readonly bool utage = true;
+
+ [EnableIf(nameof(utage))]
+ [EnableGameVersion(24000)]
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(GameManager), "CanUnlockUtageTotalJudgement")]
+ public static bool CanUnlockUtageTotalJudgement(out ConstParameter.ResultOfUnlockUtageJudgement result1P, out ConstParameter.ResultOfUnlockUtageJudgement result2P)
+ {
+ result1P = ConstParameter.ResultOfUnlockUtageJudgement.Unlocked;
+ result2P = ConstParameter.ResultOfUnlockUtageJudgement.Unlocked;
+ return false;
+ }
+}
diff --git a/AquaMai/WindowState/Enable.cs b/AquaMai/AquaMai.Mods/GameSystem/Window.cs
similarity index 58%
rename from AquaMai/WindowState/Enable.cs
rename to AquaMai/AquaMai.Mods/GameSystem/Window.cs
index 43c9db0c..e203f45b 100644
--- a/AquaMai/WindowState/Enable.cs
+++ b/AquaMai/AquaMai.Mods/GameSystem/Window.cs
@@ -1,30 +1,55 @@
using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
+using AquaMai.Config.Attributes;
using UnityEngine;
-namespace AquaMai.WindowState;
+namespace AquaMai.Mods.GameSystem;
-public class Enable
+[ConfigSection(
+ en: "Windowed Mode / Window Settings.",
+ zh: "窗口化/窗口设置")]
+public class Window
{
+ [ConfigEntry(
+ en: "Window the game.",
+ zh: "窗口化游戏")]
+ private static readonly bool windowed = false;
+
+ [ConfigEntry(
+ en: """
+ Window width (and height) for windowed mode, rendering resolution for fullscreen mode.
+ If set to 0, windowed mode will remember the user-set size, fullscreen mode will use the current display resolution.
+ """,
+ zh: """
+ 宽度(和高度)窗口化时为游戏窗口大小,全屏时为渲染分辨率
+ 如果设为 0,窗口化将记住用户设定的大小,全屏时将使用当前显示器分辨率
+ """)]
+ private static readonly int width = 0;
+
+ [ConfigEntry(
+ en: "Height, as above.",
+ zh: "高度,同上")]
+ private static readonly int height = 0;
+
private const int GWL_STYLE = -16;
private const int WS_WHATEVER = 0x14CF0000;
private static IntPtr hwnd = IntPtr.Zero;
- public static void DoCustomPatch(HarmonyLib.Harmony h)
+ public static void OnBeforePatch()
{
- if (AquaMai.AppConfig.WindowState.Windowed)
+ if (windowed)
{
var alreadyWindowed = Screen.fullScreenMode == FullScreenMode.Windowed;
- if (AquaMai.AppConfig.WindowState.Width == 0 || AquaMai.AppConfig.WindowState.Height == 0)
+ if (width == 0 || height == 0)
{
Screen.fullScreenMode = FullScreenMode.Windowed;
}
else
{
alreadyWindowed = false;
- Screen.SetResolution(AquaMai.AppConfig.WindowState.Width, AquaMai.AppConfig.WindowState.Height, FullScreenMode.Windowed);
+ Screen.SetResolution(width, height, FullScreenMode.Windowed);
}
hwnd = GetWindowHandle();
@@ -44,8 +69,8 @@ public class Enable
}
else
{
- var width = AquaMai.AppConfig.WindowState.Width == 0 ? Display.main.systemWidth : AquaMai.AppConfig.WindowState.Width;
- var height = AquaMai.AppConfig.WindowState.Height == 0 ? Display.main.systemHeight : AquaMai.AppConfig.WindowState.Height;
+ var width = Window.width == 0 ? Display.main.systemWidth : Window.width;
+ var height = Window.height == 0 ? Display.main.systemHeight : Window.height;
Screen.SetResolution(width, height, FullScreenMode.FullScreenWindow);
}
}
diff --git a/AquaMai/AquaMai.Mods/General.cs b/AquaMai/AquaMai.Mods/General.cs
new file mode 100644
index 00000000..6c34191c
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/General.cs
@@ -0,0 +1,42 @@
+using AquaMai.Config.Attributes;
+
+namespace AquaMai.Mods;
+
+// This class is for settings only. Don't patch anything here.
+
+[ConfigSection(
+ en: "AquaMai's general settings.",
+ zh: "AquaMai 的通用设置",
+ alwaysEnabled: true)]
+public class General
+{
+ [ConfigEntry(
+ en: """
+ Language for mod UI (en and zh supported).
+ If empty, the system language will be used.
+ The config file will also be saved in this language.
+ """,
+ zh: """
+ Mod 界面的语言,支持 en 和 zh
+ 如果为空,将使用系统语言
+ 配置文件也将以此语言保存
+ """,
+ specialConfigEntry: SpecialConfigEntry.Locale)]
+ public static readonly string locale = "";
+}
+
+// Please add/remove corresponding entries in SetionNameOrder enum when adding/removing sections.
+public enum SetionNameOrder
+{
+ DeprecationWarning,
+ General,
+ Fix,
+ GameSystem_Assets,
+ GameSystem,
+ GameSettings,
+ Tweaks,
+ Tweaks_TimeSaving,
+ UX,
+ Utils,
+ Fancy
+}
diff --git a/AquaMai/AquaMai.Mods/Tweaks/IgnoreAimeServerError.cs b/AquaMai/AquaMai.Mods/Tweaks/IgnoreAimeServerError.cs
new file mode 100644
index 00000000..99e4fcb3
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Tweaks/IgnoreAimeServerError.cs
@@ -0,0 +1,19 @@
+using AquaMai.Config.Attributes;
+using HarmonyLib;
+using Manager;
+
+namespace AquaMai.Mods.Tweaks;
+
+[ConfigSection(
+ en: "Prevent gray network caused by mistakenly thinking it's an AimeDB server issue.",
+ zh: "防止因错误认为 AimeDB 服务器问题引起的灰网,建议开启")]
+public class IgnoreAimeServerError
+{
+ [HarmonyPatch(typeof(OperationManager), "IsAliveAimeServer", MethodType.Getter)]
+ [HarmonyPrefix]
+ public static bool Prefix(ref bool __result)
+ {
+ __result = true;
+ return false;
+ }
+}
diff --git a/AquaMai/AquaMai.Mods/Tweaks/LockFrameRate.cs b/AquaMai/AquaMai.Mods/Tweaks/LockFrameRate.cs
new file mode 100644
index 00000000..f80eabbb
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Tweaks/LockFrameRate.cs
@@ -0,0 +1,22 @@
+using AquaMai.Config.Attributes;
+using UnityEngine;
+
+namespace AquaMai.Mods.Tweaks;
+
+[ConfigSection(
+ en: """
+ Force the frame rate limit to 60 FPS and disable vSync.
+ Do not use if your game has no issues.
+ """,
+ zh: """
+ 强制设置帧率上限为 60 帧并关闭垂直同步
+ 如果你的游戏没有问题,请不要使用
+ """)]
+public class LockFrameRate
+{
+ public static void OnBeforePatch()
+ {
+ Application.targetFrameRate = 60;
+ QualitySettings.vSyncCount = 0;
+ }
+}
diff --git a/AquaMai/AquaMai.Mods/Tweaks/README.md b/AquaMai/AquaMai.Mods/Tweaks/README.md
new file mode 100644
index 00000000..64233662
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Tweaks/README.md
@@ -0,0 +1,5 @@
+# Tweaks
+
+Patches to make the game more stable, more robust and less annoying. The game is playable at all without them, but sometimes they help a lot.
+
+These patches don't change the way the game behaving, otherwise they may go to the GameSystem category.
diff --git a/AquaMai/Fix/TouchResetAfterTrack.cs b/AquaMai/AquaMai.Mods/Tweaks/ResetTouchAfterTrack.cs
similarity index 73%
rename from AquaMai/Fix/TouchResetAfterTrack.cs
rename to AquaMai/AquaMai.Mods/Tweaks/ResetTouchAfterTrack.cs
index 1f080293..22b0991c 100644
--- a/AquaMai/Fix/TouchResetAfterTrack.cs
+++ b/AquaMai/AquaMai.Mods/Tweaks/ResetTouchAfterTrack.cs
@@ -1,11 +1,15 @@
-using HarmonyLib;
+using AquaMai.Config.Attributes;
+using HarmonyLib;
using MAI2.Util;
using Manager;
using Process;
-namespace AquaMai.Fix;
+namespace AquaMai.Mods.Tweaks;
-public class TouchResetAfterTrack
+[ConfigSection(
+ en: "Reset touch panel after playing track.",
+ zh: "在游玩一首曲目后重置触摸面板")]
+public class ResetTouchAfterTrack
{
[HarmonyPostfix]
[HarmonyPatch(typeof(ResultProcess), "OnStart")]
diff --git a/AquaMai/AquaMai.Mods/Tweaks/SkipUserVersionCheck.cs b/AquaMai/AquaMai.Mods/Tweaks/SkipUserVersionCheck.cs
new file mode 100644
index 00000000..54a7d04e
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Tweaks/SkipUserVersionCheck.cs
@@ -0,0 +1,22 @@
+using AquaMai.Config.Attributes;
+using HarmonyLib;
+using Process.Entry.State;
+
+namespace AquaMai.Mods.Tweaks;
+
+[ConfigSection(
+ en: "Allow login with higher data version.",
+ zh: """
+ 原先如果你的账号版本比当前游戏设定的版本高的话,就会不能登录
+ 开了这个选项之后就可以登录了,不过你的账号版本还是会被设定为当前游戏的版本
+ """)]
+public class SkipUserVersionCheck
+{
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(ConfirmPlay), "IsValidVersion")]
+ public static bool IsValidVersion(ref bool __result)
+ {
+ __result = true;
+ return false;
+ }
+}
diff --git a/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/EntryToMusicSelection.cs b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/EntryToMusicSelection.cs
new file mode 100644
index 00000000..4b00be95
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/EntryToMusicSelection.cs
@@ -0,0 +1,52 @@
+using AquaMai.Config.Attributes;
+using DB;
+using HarmonyLib;
+using MAI2.Util;
+using Manager;
+using Monitor;
+using Process;
+using Process.Information;
+
+namespace AquaMai.Mods.Tweaks.TimeSaving;
+
+[ConfigSection(
+ en: "Directly enter the song selection screen after login.",
+ zh: "登录完成后直接进入选歌界面")]
+public class EntryToMusicSelection
+{
+ /*
+ * Highly experimental, may well break some stuff
+ * Works by overriding the info screen (where it shows new events and stuff)
+ * to directly exit to the music selection screen, skipping character and
+ * event selection, among others
+ */
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(InformationProcess), "OnUpdate")]
+ public static bool OnUpdate(InformationProcess __instance, ProcessDataContainer ___container)
+ {
+ GameManager.SetMaxTrack();
+ // Set headphone volume
+ for (var i = 0; i < 2; i++)
+ {
+ var userData = UserDataManager.Instance.GetUserData(i);
+ if (userData.IsEntry)
+ {
+ OptionHeadphonevolumeID headPhoneVolume = userData.Option.HeadPhoneVolume;
+ SoundManager.SetHeadPhoneVolume(i, headPhoneVolume.GetValue());
+ }
+ }
+ ___container.processManager.AddProcess(new MusicSelectProcess(___container));
+ ___container.processManager.ReleaseProcess(__instance);
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(MapResultMonitor), "Initialize")]
+ public static void MapResultMonitorPreInitialize(int monIndex)
+ {
+ var userData = Singleton.Instance.GetUserData(monIndex);
+ var index = userData.MapList.FindIndex((m) => m.ID == userData.Detail.SelectMapID);
+ if (index >= 0) return;
+ userData.MapList.Clear();
+ }
+}
diff --git a/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/IWontTapOrSlideVigorously.cs b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/IWontTapOrSlideVigorously.cs
new file mode 100644
index 00000000..8ec9f44c
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/IWontTapOrSlideVigorously.cs
@@ -0,0 +1,19 @@
+using AquaMai.Config.Attributes;
+using HarmonyLib;
+using Monitor;
+
+namespace AquaMai.Mods.Tweaks.TimeSaving;
+
+[ConfigSection(
+ en: "Skip the \"Do not tap or slide vigorously\" screen, immediately proceed to the next screen once data is loaded.",
+ zh: "跳过“不要大力拍打或滑动哦”这个界面,数据一旦加载完就立马进入下一个界面")]
+public class IWontTapOrSlideVigorously
+{
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(PlInformationMonitor), "IsPlayPlInfoEnd")]
+ public static bool Patch(ref bool __result)
+ {
+ __result = true;
+ return false;
+ }
+}
diff --git a/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipEventInfo.cs b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipEventInfo.cs
new file mode 100644
index 00000000..70400ed0
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipEventInfo.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using AquaMai.Config.Attributes;
+using HarmonyLib;
+using Process;
+using Process.Information;
+
+namespace AquaMai.Mods.Tweaks.TimeSaving;
+
+[ConfigSection(
+ en: "Skip possible prompts like \"New area discovered\", \"New songs added\", \"There are events\" during game login/registration.",
+ zh: "跳过登录 / 注册游戏时候可能的 “发现了新的区域哟” “乐曲增加” “有活动哟” 之类的提示")]
+public class SkipEventInfo
+{
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(InformationProcess), "OnStart")]
+ public static void InformationProcessPostStart(ref uint ____state)
+ {
+ ____state = 3;
+ }
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(RegionalSelectProcess), "OnStart")]
+ public static void RegionalSelectProcessPreStart(ref Queue[] ____discoverList)
+ {
+ ____discoverList = new Queue[] { new Queue(), new Queue() };
+ }
+}
diff --git a/AquaMai/TimeSaving/SkipGameOverScreen.cs b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipGoodbyeScreen.cs
similarity index 68%
rename from AquaMai/TimeSaving/SkipGameOverScreen.cs
rename to AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipGoodbyeScreen.cs
index 17e65c89..e6597d61 100644
--- a/AquaMai/TimeSaving/SkipGameOverScreen.cs
+++ b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipGoodbyeScreen.cs
@@ -1,10 +1,14 @@
-using HarmonyLib;
+using AquaMai.Config.Attributes;
+using HarmonyLib;
using Monitor;
using Process;
-namespace AquaMai.TimeSaving;
+namespace AquaMai.Mods.Tweaks.TimeSaving;
-public class SkipGameOverScreen
+[ConfigSection(
+ en: "Skip the \"Goodbye\" screen at the end of the game.",
+ zh: "跳过游戏结束的「再见」界面")]
+public class SkipGoodbyeScreen
{
[HarmonyPrefix]
[HarmonyPatch(typeof(GameOverMonitor), "IsPlayEnd")]
diff --git a/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipStartupDelays.cs b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipStartupDelays.cs
new file mode 100644
index 00000000..9517fc9b
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipStartupDelays.cs
@@ -0,0 +1,32 @@
+using System.Diagnostics;
+using AquaMai.Config.Attributes;
+using HarmonyLib;
+using Process;
+
+namespace AquaMai.Mods.Tweaks.TimeSaving;
+
+[ConfigSection(
+ en: "Skip useless 2s delays to speed up the game boot process.",
+ zh: """
+ 在自检界面,每个屏幕结束的时候都会等两秒才进入下一个屏幕,很浪费时间
+ 开了这个选项之后就不会等了
+ """)]
+public class SkipStartupDelays
+{
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(PowerOnProcess), "OnStart")]
+ public static void PrePowerOnStart(ref float ____waitTime)
+ {
+ ____waitTime = 0f;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(StartupProcess), "OnUpdate")]
+ public static void PreStartupUpdate(byte ____state, ref Stopwatch ___timer)
+ {
+ if (____state == 8)
+ {
+ Traverse.Create(___timer).Field("elapsed").SetValue(2 * 10000000L);
+ }
+ }
+}
diff --git a/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipStartupWarning.cs b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipStartupWarning.cs
new file mode 100644
index 00000000..5b816681
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipStartupWarning.cs
@@ -0,0 +1,31 @@
+using AquaMai.Config.Attributes;
+using HarmonyLib;
+using Monitor;
+
+namespace AquaMai.Mods.Tweaks.TimeSaving;
+
+[ConfigSection(
+ en: "Skip SDEZ's warning screen and logo shown after the POST sequence.",
+ zh: "跳过 SDEZ 启动时的 WARNING 界面")]
+public class SkipStartupWarning
+{
+ /*
+ * Patch PlayLogo to disable the warning screen
+ */
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof (WarningMonitor), "PlayLogo")]
+ public static bool PlayLogo()
+ {
+ // Return false to block the original method
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof (WarningMonitor), "IsLogoAnimationEnd")]
+ public static bool IsLogoAnimationEnd(ref bool __result)
+ {
+ // Always return true to indicate the animation has ended
+ __result = true;
+ return false;
+ }
+}
diff --git a/AquaMai/TimeSaving/SkipTrackStart.cs b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipTrackStart.cs
similarity index 55%
rename from AquaMai/TimeSaving/SkipTrackStart.cs
rename to AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipTrackStart.cs
index 26c9c2eb..625cfe32 100644
--- a/AquaMai/TimeSaving/SkipTrackStart.cs
+++ b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipTrackStart.cs
@@ -1,8 +1,12 @@
-using HarmonyLib;
+using AquaMai.Config.Attributes;
+using HarmonyLib;
using Monitor;
-namespace AquaMai.TimeSaving;
+namespace AquaMai.Mods.Tweaks.TimeSaving;
+[ConfigSection(
+ en: "Skip TrackStart screen.",
+ zh: "跳过乐曲开始界面")]
public class SkipTrackStart
{
[HarmonyPrefix]
diff --git a/AquaMai/AquaMai.Mods/UX/CiBuildAlert.cs b/AquaMai/AquaMai.Mods/UX/CiBuildAlert.cs
new file mode 100644
index 00000000..27caa92f
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/UX/CiBuildAlert.cs
@@ -0,0 +1,26 @@
+using AquaMai.Config.Attributes;
+using AquaMai.Core.Attributes;
+using AquaMai.Core.Helpers;
+using AquaMai.Core.Resources;
+using HarmonyLib;
+using Process;
+
+namespace AquaMai.Mods.UX;
+
+[ConfigSection(exampleHidden: true, defaultOn: true)]
+[EnableIf(nameof(isCiBuild))]
+public class CiBuildAlert
+{
+# if CI
+ private static readonly bool isCiBuild = true;
+# else
+ private static readonly bool isCiBuild = false;
+# endif
+
+ [HarmonyPatch(typeof(AdvertiseProcess), "OnStart")]
+ [HarmonyPostfix]
+ public static void OnStart(AdvertiseProcess __instance)
+ {
+ MessageHelper.ShowMessage(Locale.CiBuildAlertContent, title: Locale.CiBuildAlertTitle);
+ }
+}
diff --git a/AquaMai/ModKeyMap/HideSelfMadeCharts.cs b/AquaMai/AquaMai.Mods/UX/HideSelfMadeCharts.cs
similarity index 70%
rename from AquaMai/ModKeyMap/HideSelfMadeCharts.cs
rename to AquaMai/AquaMai.Mods/UX/HideSelfMadeCharts.cs
index 58347a62..38e2f1b6 100644
--- a/AquaMai/ModKeyMap/HideSelfMadeCharts.cs
+++ b/AquaMai/AquaMai.Mods/UX/HideSelfMadeCharts.cs
@@ -1,22 +1,43 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
-using AquaMai.Helpers;
+using AquaMai.Config.Attributes;
+using AquaMai.Config.Types;
+using AquaMai.Core.Helpers;
using HarmonyLib;
using MAI2.Util;
using Manager;
using MelonLoader;
using Process;
-using UnityEngine;
using Util;
-namespace AquaMai.ModKeyMap;
+namespace AquaMai.Mods.UX;
+[ConfigSection(
+ en: "One key to hide all self-made charts in the music select process. Or hide for some users.",
+ zh: "在选曲界面一键隐藏所有自制谱,或对一部分用户进行隐藏")]
public class HideSelfMadeCharts
{
+ [ConfigEntry(
+ en: "Key to toggle self-made charts.",
+ zh: "切换自制谱显示的按键")]
+ public static readonly KeyCodeOrName key = KeyCodeOrName.Test;
+
+ [ConfigEntry]
+ public static readonly bool longPress = false;
+
+ [ConfigEntry(
+ en: "One user ID per line in the file. Hide self-made charts when these users login.",
+ zh: "该文件中每行一个用户 ID,当这些用户登录时隐藏自制谱")]
+ private static readonly string selfMadeChartsDenyUsersFile = "LocalAssets/SelfMadeChartsDenyUsers.txt";
+
+ [ConfigEntry(
+ en: "One user ID per line in the file. Only show self-made charts when these users login.",
+ zh: "该文件中每行一个用户 ID,只有这些用户登录时才显示自制谱")]
+ private static readonly string selfMadeChartsWhiteListUsersFile = "LocalAssets/SelfMadeChartsWhiteListUsers.txt";
+
private static Safe.ReadonlySortedDictionary _musics;
private static Safe.ReadonlySortedDictionary _musicsNoneSelfMade;
@@ -58,7 +79,7 @@ public class HideSelfMadeCharts
public static void MusicSelectProcessOnUpdate(ref MusicSelectProcess __instance)
{
if (isForceDisable) return;
- if (!ModKeyListener.GetKeyDownOrLongPress(AquaMai.AppConfig.ModKeyMap.HideSelfMadeCharts, AquaMai.AppConfig.ModKeyMap.HideSelfMadeChartsLongPress)) return;
+ if (!KeyListener.GetKeyDownOrLongPress(key, longPress)) return;
isShowSelfMadeCharts = !isShowSelfMadeCharts;
MelonLogger.Msg($"[HideSelfMadeCharts] isShowSelfMadeCharts: {isShowSelfMadeCharts}");
SharedInstances.ProcessDataContainer.processManager.AddProcess(new FadeProcess(SharedInstances.ProcessDataContainer, __instance, new MusicSelectProcess(SharedInstances.ProcessDataContainer)));
@@ -73,9 +94,10 @@ public class HideSelfMadeCharts
[HarmonyPatch(typeof(MusicSelectProcess), "OnStart")]
public static void MusicSelectProcessOnStart(ref MusicSelectProcess __instance)
{
- if (File.Exists(Path.Combine(Environment.CurrentDirectory, "LocalAssets", "SelfMadeChartsDenyUsers.txt")))
+ var denyPath = FileSystem.ResolvePath(selfMadeChartsDenyUsersFile);
+ if (File.Exists(denyPath))
{
- var userIds = File.ReadAllLines(Path.Combine(Environment.CurrentDirectory, "LocalAssets", "SelfMadeChartsDenyUsers.txt"));
+ var userIds = File.ReadAllLines(denyPath);
for (var i = 0; i < 2; i++)
{
var user = Singleton.Instance.GetUserData(i);
@@ -86,9 +108,10 @@ public class HideSelfMadeCharts
}
}
- if (File.Exists(Path.Combine(Environment.CurrentDirectory, "LocalAssets", "SelfMadeChartsWhiteListUsers.txt")))
+ var whiteListPath = FileSystem.ResolvePath(selfMadeChartsWhiteListUsersFile);
+ if (File.Exists(whiteListPath))
{
- var userIds = File.ReadAllLines(Path.Combine(Environment.CurrentDirectory, "LocalAssets", "SelfMadeChartsWhiteListUsers.txt"));
+ var userIds = File.ReadAllLines(whiteListPath);
for (var i = 0; i < 2; i++)
{
var user = Singleton.Instance.GetUserData(i);
diff --git a/AquaMai/UX/ImmediateSave.cs b/AquaMai/AquaMai.Mods/UX/ImmediateSave.cs
similarity index 92%
rename from AquaMai/UX/ImmediateSave.cs
rename to AquaMai/AquaMai.Mods/UX/ImmediateSave.cs
index f63b06f3..03e71d79 100644
--- a/AquaMai/UX/ImmediateSave.cs
+++ b/AquaMai/AquaMai.Mods/UX/ImmediateSave.cs
@@ -1,8 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using AquaMai.Helpers;
-using AquaMai.Resources;
+using AquaMai.Config.Attributes;
+using AquaMai.Core.Helpers;
+using AquaMai.Core.Resources;
using DB;
using HarmonyLib;
using MAI2.Util;
@@ -18,8 +19,11 @@ using Process;
using Process.UserDataNet.State.UserDataULState;
using UnityEngine;
-namespace AquaMai.UX;
+namespace AquaMai.Mods.UX;
+[ConfigSection(
+ en: "Save immediate after playing a song.",
+ zh: "打完一首歌的时候立即向服务器保存成绩")]
public class ImmediateSave
{
[HarmonyPrefix]
@@ -65,13 +69,13 @@ public class ImmediateSave
PacketHelper.StartPacket(Shim.CreatePacketUploadUserPlaylog(i, userData, (int)GameManager.MusicTrackNumber - 1,
delegate
{
- MelonLogger.Msg("Playlog saved");
+ MelonLogger.Msg("[ImmediateSave] Playlog saved");
CheckSaveDone();
},
delegate(PacketStatus err)
{
SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_ENTRY_AIME_ERROR, i);
- MelonLogger.Error("Playlog save error");
+ MelonLogger.Error("[ImmediateSave] Playlog save error");
MelonLogger.Error(err);
MessageHelper.ShowMessage("Playlog save error");
CheckSaveDone();
@@ -80,13 +84,13 @@ public class ImmediateSave
{
if (code == 1)
{
- MelonLogger.Msg("UserAll saved");
+ MelonLogger.Msg("[ImmediateSave] UserAll saved");
CheckSaveDone();
}
else
{
SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_ENTRY_AIME_ERROR, i);
- MelonLogger.Error("UserAll upsert error");
+ MelonLogger.Error("[ImmediateSave] UserAll upsert error");
MelonLogger.Error(code);
MessageHelper.ShowMessage("UserAll upsert error");
CheckSaveDone();
@@ -94,7 +98,7 @@ public class ImmediateSave
}, delegate(PacketStatus err)
{
SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_ENTRY_AIME_ERROR, i);
- MelonLogger.Error("UserAll upsert error");
+ MelonLogger.Error("[ImmediateSave] UserAll upsert error");
MelonLogger.Error(err);
MessageHelper.ShowMessage("UserAll upsert error");
CheckSaveDone();
diff --git a/AquaMai/UX/JudgeAccuracyInfo.cs b/AquaMai/AquaMai.Mods/UX/JudgeAccuracyInfo.cs
similarity index 97%
rename from AquaMai/UX/JudgeAccuracyInfo.cs
rename to AquaMai/AquaMai.Mods/UX/JudgeAccuracyInfo.cs
index 1772d0fd..539184b4 100644
--- a/AquaMai/UX/JudgeAccuracyInfo.cs
+++ b/AquaMai/AquaMai.Mods/UX/JudgeAccuracyInfo.cs
@@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
+using AquaMai.Config.Attributes;
using HarmonyLib;
using Manager;
using Monitor;
@@ -13,8 +14,11 @@ using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
-namespace AquaMai.UX;
+namespace AquaMai.Mods.UX;
+[ConfigSection(
+ zh: "在游戏总结的计分板中显示击打误差的详细信息(以帧为单位)",
+ en: "Show detailed accuracy info in the score board.")]
public class JudgeAccuracyInfo
{
public class AccuracyEntryList
@@ -240,4 +244,4 @@ public class JudgeAccuracyInfo
return result;
}
-}
\ No newline at end of file
+}
diff --git a/AquaMai/ModKeyMap/QuickSkip.cs b/AquaMai/AquaMai.Mods/UX/OneKeyEntryEnd.cs
similarity index 61%
rename from AquaMai/ModKeyMap/QuickSkip.cs
rename to AquaMai/AquaMai.Mods/UX/OneKeyEntryEnd.cs
index cb9817e7..5e2dee77 100644
--- a/AquaMai/ModKeyMap/QuickSkip.cs
+++ b/AquaMai/AquaMai.Mods/UX/OneKeyEntryEnd.cs
@@ -1,22 +1,32 @@
using System.Collections.Generic;
-using AquaMai.Helpers;
+using AquaMai.Config.Attributes;
+using AquaMai.Config.Types;
+using AquaMai.Core.Helpers;
using HarmonyLib;
using Mai2.Mai2Cue;
-using MAI2.Util;
using Main;
using Manager;
using MelonLoader;
using Process;
-namespace AquaMai.ModKeyMap;
+namespace AquaMai.Mods.UX;
-public class QuickSkip
+[ConfigSection(
+ en: "One key to proceed to music select (during entry) or end current PC (during music select).",
+ zh: "一键跳过登录过程直接进入选歌界面,或在选歌界面直接结束本局游戏")]
+public class OneKeyEntryEnd
{
+ [ConfigEntry]
+ public static readonly KeyCodeOrName key = KeyCodeOrName.Service;
+
+ [ConfigEntry]
+ public static readonly bool longPress = true;
+
[HarmonyPrefix]
[HarmonyPatch(typeof(GameMainObject), "Update")]
public static void OnGameMainObjectUpdate()
{
- if (!ModKeyListener.GetKeyDownOrLongPress(AquaMai.AppConfig.ModKeyMap.QuickSkip, AquaMai.AppConfig.ModKeyMap.QuickSkipLongPress)) return;
+ if (!KeyListener.GetKeyDownOrLongPress(key, longPress)) return;
MelonLogger.Msg("[QuickSkip] Activated");
var traverse = Traverse.Create(SharedInstances.ProcessDataContainer.processManager);
@@ -52,23 +62,4 @@ public class QuickSkip
SharedInstances.ProcessDataContainer.processManager.AddProcess(new FadeProcess(SharedInstances.ProcessDataContainer, processToRelease, new MusicSelectProcess(SharedInstances.ProcessDataContainer)));
}
}
-
- [HarmonyPostfix]
- [HarmonyPatch(typeof(GameProcess), "OnUpdate")]
- public static void PostGameProcessUpdate(GameProcess __instance, Message[] ____message, ProcessDataContainer ___container)
- {
- if (ModKeyListener.GetKeyDownOrLongPress(AquaMai.AppConfig.ModKeyMap.InGameSkip, AquaMai.AppConfig.ModKeyMap.InGameSkipLongPress))
- {
- var traverse = Traverse.Create(__instance);
- ___container.processManager.SendMessage(____message[0]);
- Singleton.Instance.SetSyncResult(0);
- traverse.Method("SetRelease").GetValue();
- }
-
- if (ModKeyListener.GetKeyDownOrLongPress(AquaMai.AppConfig.ModKeyMap.InGameRetry, AquaMai.AppConfig.ModKeyMap.InGameRetryLongPress) && GameInfo.GameVersion >= 23000)
- {
- // This is original typo in Assembly-CSharp
- Singleton.Instance.SetQuickRetryFrag(flag: true);
- }
- }
}
diff --git a/AquaMai/AquaMai.Mods/UX/OneKeyRetrySkip.cs b/AquaMai/AquaMai.Mods/UX/OneKeyRetrySkip.cs
new file mode 100644
index 00000000..2b7c1305
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/UX/OneKeyRetrySkip.cs
@@ -0,0 +1,46 @@
+using AquaMai.Config.Attributes;
+using AquaMai.Config.Types;
+using AquaMai.Core.Helpers;
+using HarmonyLib;
+using MAI2.Util;
+using Manager;
+using Process;
+
+namespace AquaMai.Mods.UX;
+
+[ConfigSection(
+ en: "One key to retry (1.30+) or skip current chart in gameplay.",
+ zh: "在游戏中途一键重试(1.30+)或跳过当前谱面")]
+public class OneKeyRetrySkip
+{
+ [ConfigEntry]
+ public static readonly KeyCodeOrName retryKey = KeyCodeOrName.Service;
+
+ [ConfigEntry]
+ public static readonly bool retryLongPress = false;
+
+ [ConfigEntry]
+ public static readonly KeyCodeOrName skipKey = KeyCodeOrName.Service;
+
+ [ConfigEntry]
+ public static readonly bool skipLongPress = true;
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(GameProcess), "OnUpdate")]
+ public static void PostGameProcessUpdate(GameProcess __instance, Message[] ____message, ProcessDataContainer ___container)
+ {
+ if (KeyListener.GetKeyDownOrLongPress(skipKey, skipLongPress))
+ {
+ var traverse = Traverse.Create(__instance);
+ ___container.processManager.SendMessage(____message[0]);
+ Singleton.Instance.SetSyncResult(0);
+ traverse.Method("SetRelease").GetValue();
+ }
+
+ if (KeyListener.GetKeyDownOrLongPress(retryKey, retryLongPress) && GameInfo.GameVersion >= 23000)
+ {
+ // This is original typo in Assembly-CSharp
+ Singleton.Instance.SetQuickRetryFrag(flag: true);
+ }
+ }
+}
diff --git a/AquaMai/ModKeyMap/PractiseModeUI.cs b/AquaMai/AquaMai.Mods/UX/PracticeMode/Libs/PractiseModeUI.cs
similarity index 82%
rename from AquaMai/ModKeyMap/PractiseModeUI.cs
rename to AquaMai/AquaMai.Mods/UX/PracticeMode/Libs/PractiseModeUI.cs
index 5d8be53b..8bd17b29 100644
--- a/AquaMai/ModKeyMap/PractiseModeUI.cs
+++ b/AquaMai/AquaMai.Mods/UX/PracticeMode/Libs/PractiseModeUI.cs
@@ -1,13 +1,13 @@
using System;
-using AquaMai.Fix;
-using AquaMai.Helpers;
-using AquaMai.Resources;
+using AquaMai.Mods.Fix;
+using AquaMai.Core.Helpers;
+using AquaMai.Core.Resources;
using Manager;
using UnityEngine;
-namespace AquaMai.ModKeyMap;
+namespace AquaMai.Mods.UX.PracticeMode.Libs;
-public class PractiseModeUI : MonoBehaviour
+public class PracticeModeUI : MonoBehaviour
{
private static float windowTop => Screen.height - GuiSizes.PlayerWidth + GuiSizes.PlayerWidth * .22f;
private static float controlHeight => GuiSizes.PlayerWidth * .13f;
@@ -60,12 +60,12 @@ public class PractiseModeUI : MonoBehaviour
GUI.Button(GetButtonRect(1, 0), Locale.Pause);
GUI.Button(GetButtonRect(2, 0), Locale.SeekForward);
- if (PractiseMode.repeatStart == -1)
+ if (PracticeMode.repeatStart == -1)
{
GUI.Button(GetButtonRect(0, 1), Locale.MarkRepeatStart);
GUI.Label(GetButtonRect(1, 1), Locale.RepeatNotSet);
}
- else if (PractiseMode.repeatEnd == -1)
+ else if (PracticeMode.repeatEnd == -1)
{
GUI.Button(GetButtonRect(0, 1), Locale.MarkRepeatEnd);
GUI.Label(GetButtonRect(1, 1), Locale.RepeatStartSet);
@@ -78,60 +78,60 @@ public class PractiseModeUI : MonoBehaviour
}
GUI.Button(GetButtonRect(0, 2), Locale.SpeedDown);
- GUI.Label(GetButtonRect(1, 2), $"{Locale.Speed} {PractiseMode.speed * 100:000}%");
+ GUI.Label(GetButtonRect(1, 2), $"{Locale.Speed} {PracticeMode.speed * 100:000}%");
GUI.Button(GetButtonRect(2, 2), Locale.SpeedUp);
GUI.Button(GetButtonRect(1, 3), Locale.SpeedReset);
- GUI.Label(GetButtonRect(0, 3), $"{TimeSpan.FromMilliseconds(PractiseMode.CurrentPlayMsec):mm\\:ss\\.fff}\n{TimeSpan.FromMilliseconds(NotesManager.Instance().getPlayFinalMsec()):mm\\:ss\\.fff}");
- GUI.Button(GetButtonRect(2, 3), $"保持流速\n{(PractiseMode.keepNoteSpeed ? "ON" : "OFF")}");
+ GUI.Label(GetButtonRect(0, 3), $"{TimeSpan.FromMilliseconds(PracticeMode.CurrentPlayMsec):mm\\:ss\\.fff}\n{TimeSpan.FromMilliseconds(NotesManager.Instance().getPlayFinalMsec()):mm\\:ss\\.fff}");
+ GUI.Button(GetButtonRect(2, 3), $"保持流速\n{(PracticeMode.keepNoteSpeed ? "ON" : "OFF")}");
}
public void Update()
{
if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.E8))
{
- PractiseMode.Seek(-1000);
+ PracticeMode.Seek(-1000);
}
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.E2))
{
- PractiseMode.Seek(1000);
+ PracticeMode.Seek(1000);
}
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B8) || InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B1))
{
DebugFeature.Pause = !DebugFeature.Pause;
if (!DebugFeature.Pause)
{
- PractiseMode.Seek(0);
+ PracticeMode.Seek(0);
}
}
- else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B7) && PractiseMode.repeatStart == -1)
+ else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B7) && PracticeMode.repeatStart == -1)
{
- PractiseMode.repeatStart = PractiseMode.CurrentPlayMsec;
+ PracticeMode.repeatStart = PracticeMode.CurrentPlayMsec;
}
- else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B7) && PractiseMode.repeatEnd == -1)
+ else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B7) && PracticeMode.repeatEnd == -1)
{
- PractiseMode.SetRepeatEnd(PractiseMode.CurrentPlayMsec);
+ PracticeMode.SetRepeatEnd(PracticeMode.CurrentPlayMsec);
}
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B2))
{
- PractiseMode.ClearRepeat();
+ PracticeMode.ClearRepeat();
}
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B6))
{
- PractiseMode.SpeedDown();
+ PracticeMode.SpeedDown();
}
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B3))
{
- PractiseMode.SpeedUp();
+ PracticeMode.SpeedUp();
}
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B5) || InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B4))
{
- PractiseMode.SpeedReset();
+ PracticeMode.SpeedReset();
}
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.E4))
{
- PractiseMode.keepNoteSpeed = !PractiseMode.keepNoteSpeed;
- PractiseMode.gameCtrl?.ResetOptionSpeed();
+ PracticeMode.keepNoteSpeed = !PracticeMode.keepNoteSpeed;
+ PracticeMode.gameCtrl?.ResetOptionSpeed();
}
else if (
InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.A1) ||
@@ -144,7 +144,7 @@ public class PractiseModeUI : MonoBehaviour
InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.A8)
)
{
- PractiseMode.ui = null;
+ PracticeMode.ui = null;
Destroy(this);
}
}
diff --git a/AquaMai/ModKeyMap/PractiseMode.cs b/AquaMai/AquaMai.Mods/UX/PracticeMode/PracticeMode.cs
similarity index 89%
rename from AquaMai/ModKeyMap/PractiseMode.cs
rename to AquaMai/AquaMai.Mods/UX/PracticeMode/PracticeMode.cs
index a14fafb2..a0878ebc 100644
--- a/AquaMai/ModKeyMap/PractiseMode.cs
+++ b/AquaMai/AquaMai.Mods/UX/PracticeMode/PracticeMode.cs
@@ -4,22 +4,35 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
-using AquaMai.Fix;
-using AquaMai.Helpers;
-using AquaMai.Resources;
-using AquaMai.Utils;
+using AquaMai.Mods.Fix;
+using AquaMai.Core.Helpers;
+using AquaMai.Core.Resources;
+using AquaMai.Mods.UX.PracticeMode.Libs;
using HarmonyLib;
using Manager;
-using MelonLoader;
using Monitor;
using Monitor.Game;
using Process;
using UnityEngine;
+using AquaMai.Config.Attributes;
+using AquaMai.Config.Types;
-namespace AquaMai.ModKeyMap;
+namespace AquaMai.Mods.UX.PracticeMode;
-public class PractiseMode
+[ConfigCollapseNamespace]
+[ConfigSection(
+ en: "Practice Mode.",
+ zh: "练习模式")]
+public class PracticeMode
{
+ [ConfigEntry(
+ en: "Key to show Practice Mode UI.",
+ zh: "显示练习模式 UI 的按键")]
+ public static readonly KeyCodeOrName key = KeyCodeOrName.Test;
+
+ [ConfigEntry]
+ public static readonly bool longPress = false;
+
public static double repeatStart = -1;
public static double repeatEnd = -1;
public static float speed = 1;
@@ -124,7 +137,7 @@ public class PractiseMode
}
}
- public static PractiseModeUI ui;
+ public static PracticeModeUI ui;
[HarmonyPatch]
public class PatchNoteSpeed
@@ -176,7 +189,7 @@ public class PractiseMode
{
if (Input.GetKeyDown(KeyCode.F11))
{
- ____monitors[0].gameObject.AddComponent();
+ ____monitors[0].gameObject.AddComponent();
}
}
# endif
@@ -185,9 +198,9 @@ public class PractiseMode
[HarmonyPostfix]
public static void GameProcessPostUpdate(GameProcess __instance, GameMonitor[] ____monitors)
{
- if (ModKeyListener.GetKeyDownOrLongPress(AquaMai.AppConfig.ModKeyMap.PractiseMode, AquaMai.AppConfig.ModKeyMap.PractiseModeLongPress) && ui is null)
+ if (KeyListener.GetKeyDownOrLongPress(key, longPress) && ui is null)
{
- ui = ____monitors[0].gameObject.AddComponent();
+ ui = ____monitors[0].gameObject.AddComponent();
}
if (repeatStart >= 0 && repeatEnd >= 0)
diff --git a/AquaMai/TimeSaving/ShowQuickEndPlay.cs b/AquaMai/AquaMai.Mods/UX/QuickEndPlay.cs
similarity index 85%
rename from AquaMai/TimeSaving/ShowQuickEndPlay.cs
rename to AquaMai/AquaMai.Mods/UX/QuickEndPlay.cs
index e575ed53..a8469bc5 100644
--- a/AquaMai/TimeSaving/ShowQuickEndPlay.cs
+++ b/AquaMai/AquaMai.Mods/UX/QuickEndPlay.cs
@@ -1,6 +1,6 @@
-using System.Collections.Generic;
-using AquaMai.Helpers;
-using AquaMai.Resources;
+using AquaMai.Config.Attributes;
+using AquaMai.Core.Helpers;
+using AquaMai.Core.Resources;
using HarmonyLib;
using MAI2.Util;
using Manager;
@@ -8,9 +8,12 @@ using Monitor;
using Process;
using UnityEngine;
-namespace AquaMai.TimeSaving;
+namespace AquaMai.Mods.UX;
-public class ShowQuickEndPlay
+[ConfigSection(
+ en: "Show a \"skip\" button like AstroDX after the notes end.",
+ zh: "音符结束之后显示像 AstroDX 一样的「跳过」按钮")]
+public class QuickEndPlay
{
private static int _timer;
diff --git a/AquaMai/AquaMai.Mods/UX/README.md b/AquaMai/AquaMai.Mods/UX/README.md
new file mode 100644
index 00000000..80cc09ec
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/UX/README.md
@@ -0,0 +1,3 @@
+# UX
+
+Features aiming at improving the user experience and can't fit in other categories go here. Generally, UX features are triggered by some user action or let the user perceive.
diff --git a/AquaMai/Utils/SelectionDetail.cs b/AquaMai/AquaMai.Mods/UX/SelectionDetail.cs
similarity index 93%
rename from AquaMai/Utils/SelectionDetail.cs
rename to AquaMai/AquaMai.Mods/UX/SelectionDetail.cs
index 1f1187af..325600fc 100644
--- a/AquaMai/Utils/SelectionDetail.cs
+++ b/AquaMai/AquaMai.Mods/UX/SelectionDetail.cs
@@ -1,8 +1,9 @@
using System.Collections.Generic;
using System.Linq;
-using AquaMai.Attributes;
-using AquaMai.Helpers;
-using AquaMai.Resources;
+using AquaMai.Core.Attributes;
+using AquaMai.Config.Attributes;
+using AquaMai.Core.Helpers;
+using AquaMai.Core.Resources;
using HarmonyLib;
using MAI2.Util;
using Manager;
@@ -12,9 +13,12 @@ using Monitor;
using Process;
using UnityEngine;
-namespace AquaMai.Utils;
+namespace AquaMai.Mods.UX;
-[GameVersion(23500)]
+[EnableGameVersion(23500)]
+[ConfigSection(
+ en: "Show detail of selected song in music selection screen.",
+ zh: "选歌界面显示选择的歌曲的详情")]
public class SelectionDetail
{
private static readonly Window[] window = new Window[2];
diff --git a/AquaMai/Utils/FrameRateDisplay.cs b/AquaMai/AquaMai.Mods/Utils/DisplayFrameRate.cs
similarity index 85%
rename from AquaMai/Utils/FrameRateDisplay.cs
rename to AquaMai/AquaMai.Mods/Utils/DisplayFrameRate.cs
index e5c108c5..4e5a3f1b 100644
--- a/AquaMai/Utils/FrameRateDisplay.cs
+++ b/AquaMai/AquaMai.Mods/Utils/DisplayFrameRate.cs
@@ -1,11 +1,15 @@
-using AquaMai.Helpers;
+using AquaMai.Config.Attributes;
+using AquaMai.Core.Helpers;
using HarmonyLib;
using Main;
using UnityEngine;
-namespace AquaMai.Utils;
+namespace AquaMai.Mods.Utils;
-public class FrameRateDisplay
+[ConfigSection(
+ en: "Display framerate.",
+ zh: "显示帧率")]
+public class DisplayFrameRate
{
[HarmonyPatch(typeof(GameMainObject), "Awake")]
[HarmonyPostfix]
diff --git a/AquaMai/Utils/LogNetworkErrors.cs b/AquaMai/AquaMai.Mods/Utils/LogNetworkErrors.cs
similarity index 92%
rename from AquaMai/Utils/LogNetworkErrors.cs
rename to AquaMai/AquaMai.Mods/Utils/LogNetworkErrors.cs
index 718eb2f3..73f59923 100644
--- a/AquaMai/Utils/LogNetworkErrors.cs
+++ b/AquaMai/AquaMai.Mods/Utils/LogNetworkErrors.cs
@@ -1,12 +1,14 @@
using System.Diagnostics;
+using AquaMai.Config.Attributes;
using HarmonyLib;
using Manager;
using Manager.Operation;
using MelonLoader;
using Net.Packet;
-namespace AquaMai.Utils;
+namespace AquaMai.Mods.Utils;
+[ConfigSection(exampleHidden: true, defaultOn: true)]
public class LogNetworkErrors
{
[HarmonyPostfix]
diff --git a/AquaMai/Utils/LogUserId.cs b/AquaMai/AquaMai.Mods/Utils/LogUserId.cs
similarity index 64%
rename from AquaMai/Utils/LogUserId.cs
rename to AquaMai/AquaMai.Mods/Utils/LogUserId.cs
index ef0ddf13..ff1fc73b 100644
--- a/AquaMai/Utils/LogUserId.cs
+++ b/AquaMai/AquaMai.Mods/Utils/LogUserId.cs
@@ -1,18 +1,22 @@
using System;
+using AquaMai.Config.Attributes;
using HarmonyLib;
using MelonLoader;
using Net.Packet;
using Net.Packet.Mai2;
using Net.VO.Mai2;
-namespace AquaMai.Utils;
+namespace AquaMai.Mods.Utils;
+[ConfigSection(
+ en: "Log user ID on login.",
+ zh: "登录时将 UserID 输出到日志")]
public class LogUserId
{
[HarmonyPostfix]
[HarmonyPatch(typeof(PacketGetUserPreview), MethodType.Constructor, typeof(ulong), typeof(string), typeof(Action), typeof(Action))]
public static void Postfix(ulong userId)
{
- MelonLogger.Msg($"UserLogin: {userId}");
+ MelonLogger.Msg($"[LogUserId] UserLogin: {userId}");
}
}
diff --git a/AquaMai/AquaMai.Mods/Utils/README.md b/AquaMai/AquaMai.Mods/Utils/README.md
new file mode 100644
index 00000000..59023a0e
--- /dev/null
+++ b/AquaMai/AquaMai.Mods/Utils/README.md
@@ -0,0 +1,3 @@
+# Utils
+
+Debugging or diagnostic purpose utilities goes here. They don't affect the game play at all, but may show UIs if needed.
diff --git a/AquaMai/Utils/ShowErrorLog.cs b/AquaMai/AquaMai.Mods/Utils/ShowErrorLog.cs
similarity index 91%
rename from AquaMai/Utils/ShowErrorLog.cs
rename to AquaMai/AquaMai.Mods/Utils/ShowErrorLog.cs
index b0e5fe6d..276a347c 100644
--- a/AquaMai/Utils/ShowErrorLog.cs
+++ b/AquaMai/AquaMai.Mods/Utils/ShowErrorLog.cs
@@ -3,14 +3,18 @@ using System.Collections;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
-using AquaMai.Helpers;
+using AquaMai.Config.Attributes;
+using AquaMai.Core.Helpers;
using HarmonyLib;
using Main;
using MelonLoader;
using UnityEngine;
-namespace AquaMai.Utils;
+namespace AquaMai.Mods.Utils;
+[ConfigSection(
+ en: "Show error log in the game.",
+ zh: "在游戏中显示错误日志窗口而不是关闭游戏进程")]
public class ShowErrorLog
{
private static Ui _errorUi;
@@ -78,7 +82,7 @@ public class ShowErrorLog
GUI.Box(new Rect(x, y, width, height), "", boxStyle);
GUI.Label(new Rect(x, y, width, height), _errorLog, labelStyle);
- if (!AquaMai.AppConfig.UX.SinglePlayer)
+ if (!GuiSizes.SinglePlayer)
{
GUI.Box(new Rect(x + GuiSizes.PlayerWidth, y, width, height), "", boxStyle);
GUI.Label(new Rect(x + GuiSizes.PlayerWidth, y, width, height), _errorLog, labelStyle);
@@ -93,4 +97,4 @@ public class ShowErrorLog
}
}
}
-}
\ No newline at end of file
+}
diff --git a/AquaMai/Utils/ShowNetErrorDetail.cs b/AquaMai/AquaMai.Mods/Utils/ShowNetErrorDetail.cs
similarity index 88%
rename from AquaMai/Utils/ShowNetErrorDetail.cs
rename to AquaMai/AquaMai.Mods/Utils/ShowNetErrorDetail.cs
index c3691689..c852981b 100644
--- a/AquaMai/Utils/ShowNetErrorDetail.cs
+++ b/AquaMai/AquaMai.Mods/Utils/ShowNetErrorDetail.cs
@@ -1,16 +1,19 @@
using System.Collections.Generic;
-using AquaMai.Helpers;
-using AquaMai.Resources;
+using AquaMai.Config.Attributes;
+using AquaMai.Core.Helpers;
+using AquaMai.Core.Resources;
using HarmonyLib;
using MAI2.Util;
using Manager;
-using MelonLoader;
using Monitor;
using Process;
using UnityEngine;
-namespace AquaMai.Utils;
+namespace AquaMai.Mods.Utils;
+[ConfigSection(
+ en: "Show Network error detail in the game when gray network icon appears.",
+ zh: "出现灰网时显示原因")]
public class ShowNetErrorDetail
{
[HarmonyPatch(typeof(CommonProcess), "OnStart")]
diff --git a/AquaMai/AquaMai.csproj b/AquaMai/AquaMai.csproj
deleted file mode 100644
index 41bd7220..00000000
--- a/AquaMai/AquaMai.csproj
+++ /dev/null
@@ -1,320 +0,0 @@
-
-
-
- Release
- AnyCPU
- {788BC472-59F7-46F6-B760-65C18BA74389}
- Library
- AquaMai
- AquaMai
- net472
- 512
- true
- 12
- x64
- $(SolutionDir)Output\
- false
- false
- false
-
-
-
- false
- None
- true
- prompt
- 4
- true
- false
-
-
-
- SDGA145
- true
- true
- x64
-
-
-
- DEBUG
-
-
-
-
- Libs\0Harmony.dll
-
-
- Libs\AMDaemon.NET.dll
-
-
- Libs\Assembly-CSharp.dll
-
-
- Libs\Assembly-CSharp-firstpass.dll
-
-
- packages\ILMerge.Fody.1.24.0\lib\net452\ILMerge.dll
-
-
- Libs\MelonLoader.dll
-
-
- Libs\Mono.Posix.dll
-
-
- Libs\Mono.Security.dll
-
-
- Libs\mscorlib.dll
-
-
-
- Libs\System.Configuration.dll
-
-
- Libs\System.Numerics.dll
-
-
- Libs\System.Core.dll
-
-
- Libs\System.Security.dll
-
-
- Libs\System.Xml.dll
-
-
- packages\Samboy063.Tomlet.5.4.0\lib\net35\Tomlet.dll
-
-
- Libs\Unity.Analytics.DataPrivacy.dll
-
-
- Libs\Unity.TextMeshPro.dll
-
-
- Libs\UnityEngine.dll
-
-
- Libs\UnityEngine.AccessibilityModule.dll
-
-
- Libs\UnityEngine.AIModule.dll
-
-
- Libs\UnityEngine.AnimationModule.dll
-
-
- Libs\UnityEngine.ARModule.dll
-
-
- Libs\UnityEngine.AssetBundleModule.dll
-
-
- Libs\UnityEngine.AudioModule.dll
-
-
- Libs\UnityEngine.BaselibModule.dll
-
-
- Libs\UnityEngine.ClothModule.dll
-
-
- Libs\UnityEngine.ClusterInputModule.dll
-
-
- Libs\UnityEngine.ClusterRendererModule.dll
-
-
- Libs\UnityEngine.CoreModule.dll
-
-
- Libs\UnityEngine.CrashReportingModule.dll
-
-
- Libs\UnityEngine.DirectorModule.dll
-
-
- Libs\UnityEngine.FileSystemHttpModule.dll
-
-
- Libs\UnityEngine.GameCenterModule.dll
-
-
- Libs\UnityEngine.GridModule.dll
-
-
- Libs\UnityEngine.HotReloadModule.dll
-
-
- Libs\UnityEngine.ImageConversionModule.dll
-
-
- Libs\UnityEngine.IMGUIModule.dll
-
-
- Libs\UnityEngine.InputModule.dll
-
-
- Libs\UnityEngine.JSONSerializeModule.dll
-
-
- Libs\UnityEngine.LocalizationModule.dll
-
-
- Libs\UnityEngine.Networking.dll
-
-
- Libs\UnityEngine.ParticleSystemModule.dll
-
-
- Libs\UnityEngine.PerformanceReportingModule.dll
-
-
- Libs\UnityEngine.Physics2DModule.dll
-
-
- Libs\UnityEngine.PhysicsModule.dll
-
-
- Libs\UnityEngine.ProfilerModule.dll
-
-
- Libs\UnityEngine.ScreenCaptureModule.dll
-
-
- Libs\UnityEngine.SharedInternalsModule.dll
-
-
- Libs\UnityEngine.SpatialTracking.dll
-
-
- Libs\UnityEngine.SpriteMaskModule.dll
-
-
- Libs\UnityEngine.SpriteShapeModule.dll
-
-
- Libs\UnityEngine.StreamingModule.dll
-
-
- Libs\UnityEngine.StyleSheetsModule.dll
-
-
- Libs\UnityEngine.SubstanceModule.dll
-
-
- Libs\UnityEngine.TerrainModule.dll
-
-
- Libs\UnityEngine.TerrainPhysicsModule.dll
-
-
- Libs\UnityEngine.TextCoreModule.dll
-
-
- Libs\UnityEngine.TextRenderingModule.dll
-
-
- Libs\UnityEngine.TilemapModule.dll
-
-
- Libs\UnityEngine.Timeline.dll
-
-
- Libs\UnityEngine.TimelineModule.dll
-
-
- Libs\UnityEngine.TLSModule.dll
-
-
- Libs\UnityEngine.UI.dll
-
-
- Libs\UnityEngine.UIElementsModule.dll
-
-
- Libs\UnityEngine.UIModule.dll
-
-
- Libs\UnityEngine.UmbraModule.dll
-
-
- Libs\UnityEngine.UNETModule.dll
-
-
- Libs\UnityEngine.UnityAnalyticsModule.dll
-
-
- Libs\UnityEngine.UnityConnectModule.dll
-
-
- Libs\UnityEngine.UnityTestProtocolModule.dll
-
-
- Libs\UnityEngine.UnityWebRequestAssetBundleModule.dll
-
-
- Libs\UnityEngine.UnityWebRequestAudioModule.dll
-
-
- Libs\UnityEngine.UnityWebRequestModule.dll
-
-
- Libs\UnityEngine.UnityWebRequestTextureModule.dll
-
-
- Libs\UnityEngine.UnityWebRequestWWWModule.dll
-
-
- Libs\UnityEngine.VehiclesModule.dll
-
-
- Libs\UnityEngine.VFXModule.dll
-
-
- Libs\UnityEngine.VideoModule.dll
-
-
- Libs\UnityEngine.VRModule.dll
-
-
- Libs\UnityEngine.WindModule.dll
-
-
- Libs\UnityEngine.XRModule.dll
-
-
-
-
-
- ResXFileCodeGenerator
- Locale.Designer.cs
-
-
- Locale.resx
-
-
-
-
-
-
-
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
- all
-
-
-
-
-
-
- PreserveNewest
-
-
-
-
diff --git a/AquaMai/AquaMai.sln b/AquaMai/AquaMai.sln
index 1804c25b..4145c5cc 100644
--- a/AquaMai/AquaMai.sln
+++ b/AquaMai/AquaMai.sln
@@ -3,7 +3,40 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.6.33815.320
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AquaMai", "AquaMai.csproj", "{788BC472-59F7-46F6-B760-65C18BA74389}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AquaMai.Config.Interfaces", "AquaMai.Config.Interfaces/AquaMai.Config.Interfaces.csproj", "{50C59E41-2939-407E-9CB7-B186F33DDCF9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AquaMai.Config", "AquaMai.Config/AquaMai.Config.csproj", "{B9CA039A-A0F1-4C87-A33E-3F959974BB3A}"
+ ProjectSection(ProjectDependencies) = postProject
+ {50C59E41-2939-407E-9CB7-B186F33DDCF9} = {50C59E41-2939-407E-9CB7-B186F33DDCF9}
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AquaMai.Config.HeadlessLoader", "AquaMai.Config.HeadlessLoader/AquaMai.Config.HeadlessLoader.csproj", "{6B5E1F3E-D012-4CFB-A2FA-26A6CE06BE66}"
+ ProjectSection(ProjectDependencies) = postProject
+ {50C59E41-2939-407E-9CB7-B186F33DDCF9} = {50C59E41-2939-407E-9CB7-B186F33DDCF9}
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AquaMai.Build", "AquaMai.Build/AquaMai.Build.csproj", "{4C0C68C3-8B2E-4CA8-A26D-AE87CF2A38A5}"
+ ProjectSection(ProjectDependencies) = postProject
+ {6B5E1F3E-D012-4CFB-A2FA-26A6CE06BE66} = {6B5E1F3E-D012-4CFB-A2FA-26A6CE06BE66}
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AquaMai.Core", "AquaMai.Core/AquaMai.Core.csproj", "{33C0D4ED-6A84-4659-9A05-12D43D75D0B3}"
+ ProjectSection(ProjectDependencies) = postProject
+ {B9CA039A-A0F1-4C87-A33E-3F959974BB3A} = {B9CA039A-A0F1-4C87-A33E-3F959974BB3A}
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AquaMai.Mods", "AquaMai.Mods/AquaMai.Mods.csproj", "{8731C0E0-53BE-4B1B-9828-193E738C6865}"
+ ProjectSection(ProjectDependencies) = postProject
+ {B9CA039A-A0F1-4C87-A33E-3F959974BB3A} = {B9CA039A-A0F1-4C87-A33E-3F959974BB3A}
+ {33C0D4ED-6A84-4659-9A05-12D43D75D0B3} = {33C0D4ED-6A84-4659-9A05-12D43D75D0B3}
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AquaMai", "AquaMai/AquaMai.csproj", "{788BC472-59F7-46F6-B760-65C18BA74389}"
+ ProjectSection(ProjectDependencies) = postProject
+ {4C0C68C3-8B2E-4CA8-A26D-AE87CF2A38A5} = {4C0C68C3-8B2E-4CA8-A26D-AE87CF2A38A5}
+ {33C0D4ED-6A84-4659-9A05-12D43D75D0B3} = {33C0D4ED-6A84-4659-9A05-12D43D75D0B3}
+ {8731C0E0-53BE-4B1B-9828-193E738C6865} = {8731C0E0-53BE-4B1B-9828-193E738C6865}
+ EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -11,6 +44,30 @@ Global
Debug|Any CPU = Debug|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {50C59E41-2939-407E-9CB7-B186F33DDCF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {50C59E41-2939-407E-9CB7-B186F33DDCF9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {50C59E41-2939-407E-9CB7-B186F33DDCF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {50C59E41-2939-407E-9CB7-B186F33DDCF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B9CA039A-A0F1-4C87-A33E-3F959974BB3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B9CA039A-A0F1-4C87-A33E-3F959974BB3A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B9CA039A-A0F1-4C87-A33E-3F959974BB3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B9CA039A-A0F1-4C87-A33E-3F959974BB3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6B5E1F3E-D012-4CFB-A2FA-26A6CE06BE66}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6B5E1F3E-D012-4CFB-A2FA-26A6CE06BE66}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6B5E1F3E-D012-4CFB-A2FA-26A6CE06BE66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6B5E1F3E-D012-4CFB-A2FA-26A6CE06BE66}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4C0C68C3-8B2E-4CA8-A26D-AE87CF2A38A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4C0C68C3-8B2E-4CA8-A26D-AE87CF2A38A5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4C0C68C3-8B2E-4CA8-A26D-AE87CF2A38A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4C0C68C3-8B2E-4CA8-A26D-AE87CF2A38A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {33C0D4ED-6A84-4659-9A05-12D43D75D0B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {33C0D4ED-6A84-4659-9A05-12D43D75D0B3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {33C0D4ED-6A84-4659-9A05-12D43D75D0B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {33C0D4ED-6A84-4659-9A05-12D43D75D0B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8731C0E0-53BE-4B1B-9828-193E738C6865}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8731C0E0-53BE-4B1B-9828-193E738C6865}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8731C0E0-53BE-4B1B-9828-193E738C6865}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8731C0E0-53BE-4B1B-9828-193E738C6865}.Debug|Any CPU.Build.0 = Debug|Any CPU
{788BC472-59F7-46F6-B760-65C18BA74389}.Release|Any CPU.ActiveCfg = Release|Any CPU
{788BC472-59F7-46F6-B760-65C18BA74389}.Release|Any CPU.Build.0 = Release|Any CPU
{788BC472-59F7-46F6-B760-65C18BA74389}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
diff --git a/AquaMai/AquaMai/AquaMai.csproj b/AquaMai/AquaMai/AquaMai.csproj
new file mode 100644
index 00000000..ee3d948d
--- /dev/null
+++ b/AquaMai/AquaMai/AquaMai.csproj
@@ -0,0 +1,74 @@
+
+
+
+ Release
+ AnyCPU
+ {788BC472-59F7-46F6-B760-65C18BA74389}
+ Library
+ AquaMai
+ AquaMai
+ net472
+ 512
+ true
+ 12
+ 414
+ $(ProjectDir)../Libs/;$(AssemblySearchPaths)
+ $(ProjectDir)../Output/
+ false
+ false
+ false
+
+
+
+ false
+ None
+ true
+ prompt
+ 4
+ true
+ false
+
+
+
+ DEBUG
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AquaMai.Config.Interfaces.dll
+
+
+ AquaMai.Config.dll
+
+
+ AquaMai.Core.dll
+
+
+ AquaMai.Mods.dll
+
+
+
+
diff --git a/AquaMai/AquaMai/AssemblyLoader.cs b/AquaMai/AquaMai/AssemblyLoader.cs
new file mode 100644
index 00000000..e5d21cd5
--- /dev/null
+++ b/AquaMai/AquaMai/AssemblyLoader.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Reflection;
+
+namespace AquaMai;
+
+public static class AssemblyLoader
+{
+ public enum AssemblyName
+ {
+ ConfigInterfaces,
+ Config,
+ Core,
+ Mods,
+ }
+
+ private static readonly Dictionary Assemblies = new()
+ {
+ [AssemblyName.ConfigInterfaces] = "AquaMai.Config.Interfaces.dll",
+ [AssemblyName.Config] = "AquaMai.Config.dll",
+ [AssemblyName.Core] = "AquaMai.Core.dll",
+ [AssemblyName.Mods] = "AquaMai.Mods.dll",
+ };
+
+ private static readonly Dictionary LoadedAssemblies = [];
+
+ public static Assembly GetAssembly(AssemblyName assemblyName) => LoadedAssemblies[assemblyName];
+
+ public static void LoadAssemblies()
+ {
+ foreach (var (assemblyName, assemblyFileName) in Assemblies)
+ {
+# if DEBUG
+ MelonLoader.MelonLogger.Msg($"Loading assembly \"{assemblyFileName}\"...");
+# endif
+ LoadedAssemblies[assemblyName] = LoadAssemblyFromResource(assemblyFileName);
+ }
+ }
+
+ private static Assembly LoadAssemblyFromResource(string assemblyName)
+ {
+ var executingAssembly = Assembly.GetExecutingAssembly();
+ using var decompressedStream = executingAssembly.GetManifestResourceStream(assemblyName);
+ if (decompressedStream != null)
+ {
+ return AppDomain.CurrentDomain.Load(StreamToBytes(decompressedStream));
+ }
+ using var compressedStream = executingAssembly.GetManifestResourceStream($"{assemblyName}.compressed");
+ if (compressedStream != null)
+ {
+ return AppDomain.CurrentDomain.Load(DecompressToBytes(compressedStream));
+ }
+ throw new Exception($"Embedded assembly \"{assemblyName}\" not found.");
+ }
+
+ private static byte[] StreamToBytes(Stream stream)
+ {
+ if (stream == null)
+ {
+ return [];
+ }
+ using var memoryStream = new MemoryStream();
+ stream.CopyTo(memoryStream);
+ return memoryStream.ToArray();
+ }
+
+ private static byte[] DecompressToBytes(Stream stream) => StreamToBytes(new DeflateStream(stream, CompressionMode.Decompress));
+}
diff --git a/AquaMai/AquaMai/BuildInfo.cs b/AquaMai/AquaMai/BuildInfo.cs
new file mode 100644
index 00000000..8f9443cc
--- /dev/null
+++ b/AquaMai/AquaMai/BuildInfo.cs
@@ -0,0 +1,12 @@
+namespace AquaMai;
+
+public static class BuildInfo
+{
+ public const string Name = "AquaMai";
+ public const string Description = "Mod for Sinmai";
+ public const string Author = "Aza ft. Clansty ft. Menci";
+ public const string Company = null;
+ public const string Version = "1.2.2";
+ public const string DownloadLink = null;
+}
+
diff --git a/AquaMai/AquaMai/Main.cs b/AquaMai/AquaMai/Main.cs
new file mode 100644
index 00000000..505f057f
--- /dev/null
+++ b/AquaMai/AquaMai/Main.cs
@@ -0,0 +1,39 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+using MelonLoader;
+
+namespace AquaMai;
+
+public class AquaMai : MelonMod
+{
+ public const string AQUAMAI_SAY = """
+ 如果你在 dnSpy / ILSpy 里看到了这行字,请从 resources 中解包 DLLs。
+ If you see this line in dnSpy / ILSpy, please unpack the DLLs from resources.
+ """;
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool SetConsoleOutputCP(uint wCodePageID);
+
+ public override void OnInitializeMelon()
+ {
+ // Prevent Chinese characters from being garbled
+ SetConsoleOutputCP(65001);
+
+ AssemblyLoader.LoadAssemblies();
+
+ var modsAssembly = AssemblyLoader.GetAssembly(AssemblyLoader.AssemblyName.Mods);
+ var coreAssembly = AssemblyLoader.GetAssembly(AssemblyLoader.AssemblyName.Core);
+ coreAssembly.GetType("AquaMai.Core.Startup")
+ .GetMethod("Initialize", BindingFlags.Public | BindingFlags.Static)
+ .Invoke(null, [modsAssembly, HarmonyInstance]);
+ }
+
+ public override void OnGUI()
+ {
+ var coreAssembly = AssemblyLoader.GetAssembly(AssemblyLoader.AssemblyName.Core);
+ coreAssembly.GetType("AquaMai.Core.Startup")
+ .GetMethod("OnGUI", BindingFlags.Public | BindingFlags.Static)
+ .Invoke(null, []);
+ base.OnGUI();
+ }
+}
diff --git a/AquaMai/Properties/AssemblyInfo.cs b/AquaMai/AquaMai/Properties/AssemblyInfo.cs
similarity index 100%
rename from AquaMai/Properties/AssemblyInfo.cs
rename to AquaMai/AquaMai/Properties/AssemblyInfo.cs
diff --git a/AquaMai/Attributes/ConfigCommentAttribute.cs b/AquaMai/Attributes/ConfigCommentAttribute.cs
deleted file mode 100644
index ee9cee4b..00000000
--- a/AquaMai/Attributes/ConfigCommentAttribute.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System;
-
-namespace AquaMai.Attributes;
-
-[AttributeUsage(AttributeTargets.Property)]
-public class ConfigCommentAttribute(string en = null, string zh = null) : Attribute
-{
- public string CommentEn { get; } = en;
- public string CommentZh { get; } = zh;
-}
diff --git a/AquaMai/Attributes/GameVersionAttribute.cs b/AquaMai/Attributes/GameVersionAttribute.cs
deleted file mode 100644
index b283849b..00000000
--- a/AquaMai/Attributes/GameVersionAttribute.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System;
-
-namespace AquaMai.Attributes;
-
-[AttributeUsage(AttributeTargets.Class)]
-public class GameVersionAttribute(uint minVersion = 0, uint maxVersion = 0) : Attribute
-{
- public uint MinVersion { get; } = minVersion;
- public uint MaxVersion { get; } = maxVersion;
-}
diff --git a/AquaMai/Cheat/Config.cs b/AquaMai/Cheat/Config.cs
deleted file mode 100644
index e9593440..00000000
--- a/AquaMai/Cheat/Config.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using AquaMai.Attributes;
-
-namespace AquaMai.Cheat;
-
-public class Config
-{
- [ConfigComment(
- en: "Unlock normally event-only tickets",
- zh: "解锁游戏里所有可能的跑图券")]
- public bool TicketUnlock { get; set; }
-
- [ConfigComment(
- en: "Unlock maps that are not in this version",
- zh: "解锁游戏里所有的区域,包括非当前版本的(并不会帮你跑完)")]
- public bool MapUnlock { get; set; }
-
- [ConfigComment(
- en: "Unlock Utage without the need of DXRating 10000",
- zh: "不需要万分也可以进宴会场")]
- public bool UnlockUtage { get; set; }
-}
diff --git a/AquaMai/Cheat/MapUnlock.cs b/AquaMai/Cheat/MapUnlock.cs
deleted file mode 100644
index 49ab1a07..00000000
--- a/AquaMai/Cheat/MapUnlock.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using Manager.MaiStudio;
-using HarmonyLib;
-
-namespace AquaMai.Cheat
-{
- public class MapUnlock
- {
- // For any map, return the event ID 1 to unlock it
- [HarmonyPrefix]
- [HarmonyPatch(typeof(MapData), "get_OpenEventId")]
- public static bool get_OpenEventId(ref StringID __result)
- {
- var id = new Manager.MaiStudio.Serialize.StringID
- {
- id = 1,
- str = "無期限常時解放"
- };
-
- var sid = new StringID();
- sid.Init(id);
-
- __result = sid;
- return false;
- }
- }
-}
\ No newline at end of file
diff --git a/AquaMai/Cheat/TicketUnlock.cs b/AquaMai/Cheat/TicketUnlock.cs
deleted file mode 100644
index 697a516c..00000000
--- a/AquaMai/Cheat/TicketUnlock.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using Manager.MaiStudio;
-using HarmonyLib;
-
-namespace AquaMai.Cheat
-{
- /**
- * Unlock tickets that are typically locked unless a specific event is open.
- */
- public class TicketUnlock
- {
- // For any ticket, return the event ID 1 to unlock it
- [HarmonyPrefix]
- [HarmonyPatch(typeof(TicketData), "get_ticketEvent")]
- public static bool get_ticketEvent(ref StringID __result)
- {
- var id = new Manager.MaiStudio.Serialize.StringID
- {
- id = 1,
- str = "無期限常時解放"
- };
-
- var sid = new StringID();
- sid.Init(id);
-
- __result = sid;
- return false;
- }
-
- // Modify the maxTicketNum to 0
- // this is because TicketManager.GetTicketData adds the ticket to the list if either
- // the player owns at least one ticket or the maxTicketNum = 0
- [HarmonyPrefix]
- [HarmonyPatch(typeof(TicketData), "get_maxCount")]
- public static bool get_maxCount(ref int __result)
- {
- __result = 0;
- return false;
- }
- }
-}
\ No newline at end of file
diff --git a/AquaMai/Cheat/UnlockUtage.cs b/AquaMai/Cheat/UnlockUtage.cs
deleted file mode 100644
index 7aa8ace4..00000000
--- a/AquaMai/Cheat/UnlockUtage.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using AquaMai.Attributes;
-using HarmonyLib;
-using MAI2System;
-using Manager;
-
-namespace AquaMai.Cheat
-{
- [GameVersion(24000)]
- public class UnlockUtage
- {
- [HarmonyPrefix]
- [HarmonyPatch(typeof(GameManager), "CanUnlockUtageTotalJudgement")]
- public static bool CanUnlockUtageTotalJudgement(out ConstParameter.ResultOfUnlockUtageJudgement result1P, out ConstParameter.ResultOfUnlockUtageJudgement result2P)
- {
- result1P = ConstParameter.ResultOfUnlockUtageJudgement.Unlocked;
- result2P = ConstParameter.ResultOfUnlockUtageJudgement.Unlocked;
- return false;
- }
- }
-}
diff --git a/AquaMai/Config.cs b/AquaMai/Config.cs
deleted file mode 100644
index e4af3865..00000000
--- a/AquaMai/Config.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using AquaMai.Attributes;
-using AquaMai.CustomKeyMap;
-
-namespace AquaMai;
-
-[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
-public class Config
-{
- [ConfigComment(
- en: "UX: User Experience Improvements",
- zh: """
-
- 试试使用 MaiChartManager 图形化配置 AquaMai 吧!
- https://github.com/clansty/MaiChartManager
-
- 用户体验改进
- """)]
- public UX.Config UX { get; set; } = new();
-
- [ConfigComment(
- en: "Cheat: You control the buttons you press",
- zh: "“作弊”功能")]
- public Cheat.Config Cheat { get; set; } = new();
-
- [ConfigComment(
- en: "Fix: Fix some potential issues",
- zh: "修复一些潜在的问题")]
- public Fix.Config Fix { get; set; } = new();
-
- [ConfigComment(
- zh: "实用工具")]
- public Utils.Config Utils { get; set; } = new();
-
- [ConfigComment(
- en: "Time Saving: Skip some unnecessary screens",
- zh: "节省一些不知道有用没用的时间,跳过一些不必要的界面")]
- public TimeSaving.Config TimeSaving { get; set; } = new();
-
- [ConfigComment(
- en: "Visual effects of notes and judgment display and some other textures",
- zh: "音符和判定表示以及一些其他贴图的视觉效果调整")]
- public Visual.Config Visual { get; set; } = new();
-
- [ConfigComment(
- zh: "Mod 内功能的按键设置")]
- public ModKeyMap.Config ModKeyMap { get; set; } = new();
-
- [ConfigComment(
- zh: "窗口相关设置")]
- public WindowState.Config WindowState { get; set; } = new();
-
- [ConfigComment(
- en: "Custom camera ID settings",
- zh: "自定义摄像头 ID")]
- public CustomCameraId.Config CustomCameraId { get; set; } = new();
-
- [ConfigComment(
- zh: "触摸灵敏度设置")]
- public TouchSensitivity.Config TouchSensitivity { get; set; } = new();
-
- [ConfigComment(
- zh: "自定义按键映射")]
- public CustomKeyMap.Config CustomKeyMap { get; set; } = new();
-}
diff --git a/AquaMai/ConfigGenerator.cs b/AquaMai/ConfigGenerator.cs
deleted file mode 100644
index 2e3a30ce..00000000
--- a/AquaMai/ConfigGenerator.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-using System;
-using System.IO;
-using System.Reflection;
-using AquaMai.Attributes;
-using Tomlet;
-using Tomlet.Models;
-
-namespace AquaMai;
-
-public static class ConfigGenerator
-{
- public static void GenerateConfig()
- {
- var defaultConfig = new Config();
- foreach (var lang in (string[]) ["en", "zh"])
- {
- File.WriteAllText($"AquaMai.{lang}.toml", SerializeConfigWithComments(defaultConfig, lang));
- }
- }
-
- public static string SerializeConfigWithComments(Config config, string lang)
- {
- var tomlDoc = TomletMain.DocumentFrom(config);
- MakeComments(tomlDoc, typeof(Config), lang);
- return tomlDoc.SerializedValue;
- }
-
- private static void MakeComments(TomlTable table, Type configType, string lang)
- {
- foreach (var property in configType.GetProperties())
- {
- var value = table.GetValue(property.Name);
- var comment = property.GetCustomAttribute();
- if (comment != null)
- {
- value.Comments.PrecedingComment = lang switch
- {
- "en" => comment.CommentEn,
- "zh" => comment.CommentZh,
- _ => throw new ArgumentException($"Unsupported language: {lang}")
- };
- }
-
- if (value is TomlTable subTable)
- {
- MakeComments(subTable, property.PropertyType, lang);
- }
- }
- }
-}
diff --git a/AquaMai/CustomCameraId/Config.cs b/AquaMai/CustomCameraId/Config.cs
deleted file mode 100644
index 3e08055e..00000000
--- a/AquaMai/CustomCameraId/Config.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using AquaMai.Attributes;
-
-namespace AquaMai.CustomCameraId;
-
-public class Config
-{
- [ConfigComment(
- en: """
- Enable custom CameraId
- If enabled, you can customize the game to use the specified camera
- """,
- zh: """
- 是否启用自定义摄像头ID
- 启用后可以指定游戏使用的摄像头
- """)]
- public bool Enable { get; set; }
-
- [ConfigComment(
- en: "Print the camera list to the log when starting, can be used as a basis for modification",
- zh: "启动时打印摄像头列表到日志中,可以作为修改的依据")]
- public bool PrintCameraList { get; set; } = false;
-
- [ConfigComment(
- en: "DX Pass 1P",
- zh: "DX Pass 1P")]
- public int LeftQrCamera { get; set; } = 0;
-
- [ConfigComment(
- en: "DX Pass 2P",
- zh: "DX Pass 2P")]
- public int RightQrCamera { get; set; } = 0;
-
- [ConfigComment(
- zh: "玩家摄像头")]
- public int PhotoCamera { get; set; } = 0;
-
- [ConfigComment(
- zh: "二维码扫描摄像头")]
- public int ChimeCamera { get; set; } = 0;
-}
diff --git a/AquaMai/CustomCameraId/PrintCameraList.cs b/AquaMai/CustomCameraId/PrintCameraList.cs
deleted file mode 100644
index 5de15aca..00000000
--- a/AquaMai/CustomCameraId/PrintCameraList.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using MelonLoader;
-using UnityEngine;
-
-namespace AquaMai.CustomCameraId;
-
-public class PrintCameraList
-{
- public static void DoCustomPatch(HarmonyLib.Harmony _)
- {
- WebCamDevice[] devices = WebCamTexture.devices;
- string cameraList = "Connected Web Cameras:\n";
- for (int i = 0; i < devices.Length; i++)
- {
- WebCamDevice webCamDevice = devices[i];
- WebCamTexture webCamTexture = new WebCamTexture(webCamDevice.name);
- webCamTexture.Play();
- cameraList += "==================================================\n";
- cameraList += "Name: " + webCamDevice.name + "\n";
- cameraList += $"ID: {i}\n";
- cameraList += $"Resolution: {webCamTexture.width} * {webCamTexture.height}\n";
- cameraList += $"FPS: {webCamTexture.requestedFPS}\n";
- webCamTexture.Stop();
- }
- cameraList += "==================================================";
-
- foreach (var line in cameraList.Split('\n'))
- {
- MelonLogger.Msg($"[CustomCameraId] {line}");
- }
- }
-}
diff --git a/AquaMai/CustomKeyMap/Config.cs b/AquaMai/CustomKeyMap/Config.cs
deleted file mode 100644
index 7fe5ed45..00000000
--- a/AquaMai/CustomKeyMap/Config.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using AquaMai.Attributes;
-
-namespace AquaMai.CustomKeyMap;
-
-public class Config
-{
- [ConfigComment(
- en: "These settings will work regardless of whether you have enabled segatools' io4 emulation",
- zh: "这里的设置无论你是否启用了 segatools 的 io4 模拟都会工作")]
- public bool Enable { get; set; }
-
- public KeyCodeID Test { get; set; } = (KeyCodeID)115;
- public KeyCodeID Service { get; set; } = (KeyCodeID)5;
- public KeyCodeID Button1_1P { get; set; } = (KeyCodeID)67;
- public KeyCodeID Button2_1P { get; set; } = (KeyCodeID)49;
- public KeyCodeID Button3_1P { get; set; } = (KeyCodeID)48;
- public KeyCodeID Button4_1P { get; set; } = (KeyCodeID)47;
- public KeyCodeID Button5_1P { get; set; } = (KeyCodeID)68;
- public KeyCodeID Button6_1P { get; set; } = (KeyCodeID)70;
- public KeyCodeID Button7_1P { get; set; } = (KeyCodeID)45;
- public KeyCodeID Button8_1P { get; set; } = (KeyCodeID)61;
- public KeyCodeID Select_1P { get; set; } = (KeyCodeID)25;
- public KeyCodeID Button1_2P { get; set; } = (KeyCodeID)80;
- public KeyCodeID Button2_2P { get; set; } = (KeyCodeID)81;
- public KeyCodeID Button3_2P { get; set; } = (KeyCodeID)78;
- public KeyCodeID Button4_2P { get; set; } = (KeyCodeID)75;
- public KeyCodeID Button5_2P { get; set; } = (KeyCodeID)74;
- public KeyCodeID Button6_2P { get; set; } = (KeyCodeID)73;
- public KeyCodeID Button7_2P { get; set; } = (KeyCodeID)76;
- public KeyCodeID Button8_2P { get; set; } = (KeyCodeID)79;
- public KeyCodeID Select_2P { get; set; } = (KeyCodeID)84;
-}
diff --git a/AquaMai/CustomKeyMap/Enable.cs b/AquaMai/CustomKeyMap/Enable.cs
deleted file mode 100644
index 0b305b6a..00000000
--- a/AquaMai/CustomKeyMap/Enable.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using DB;
-using HarmonyLib;
-
-namespace AquaMai.CustomKeyMap;
-
-public class Enable
-{
- [HarmonyPatch(typeof(JvsButtonTableRecord), MethodType.Constructor, typeof(int), typeof(string), typeof(string), typeof(int), typeof(string), typeof(int), typeof(int), typeof(int))]
- [HarmonyPostfix]
- public static void JvsButtonTableRecordConstructor(JvsButtonTableRecord __instance, string Name)
- {
- var prop = (DB.KeyCodeID)typeof(Config).GetProperty(Name).GetValue(AquaMai.AppConfig.CustomKeyMap);
- __instance.SubstituteKey = prop;
- }
-}
diff --git a/AquaMai/Fix/Config.cs b/AquaMai/Fix/Config.cs
deleted file mode 100644
index e63a406f..00000000
--- a/AquaMai/Fix/Config.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-using AquaMai.Attributes;
-
-namespace AquaMai.Fix;
-
-public class Config
-{
- [ConfigComment(
- en: "Allow login with higher data version",
- zh: """
- 原先如果你的账号版本比当前游戏设定的版本高的话,就会不能登录
- 开了这个选项之后就可以登录了,不过你的账号版本还是会被设定为当前游戏的版本
- """)]
- public bool SkipVersionCheck { get; set; }
-
- [ConfigComment(
- zh: """
- 如果你在用未经修改的客户端,会默认加密到服务器的连接,而连接私服的时候不应该加密
- 开了这个选项之后就不会加密连接了,同时也会移除不同版本的客户端可能会对 API 接口加的后缀
- 正常情况下,请保持这个选项开启
- """)]
- public bool RemoveEncryption { get; set; } = true;
-
- [ConfigComment(
- zh: "如果要配置店内招募的话,应该要把这个关闭")]
- public bool ForceAsServer { get; set; } = true;
-
- [ConfigComment(
- en: "Force the game to be in FreePlay mode",
- zh: "强制改为免费游玩(FreePlay)")]
- public bool ForceFreePlay { get; set; } = true;
-
- [ConfigComment(
- en: "Force the game to be in PaidPlay mode with 24 coins locked, conflicts with ForceFreePlay",
- zh: "强制付费游玩并锁定 24 个币,和 ForceFreePlay 冲突")]
- public bool ForcePaidPlay { get; set; }
-
- [ConfigComment(
- en: "Add notes sprite to the pool to prevent use up",
- zh: "增加更多待命的音符贴图,防止奇怪的自制谱用完音符贴图池")]
- public int ExtendNotesPool { get; set; }
-
- [ConfigComment(
- en: "Force the frame rate limit to 60 FPS and disable vSync. Do not use if your game has no issues",
- zh: "强制设置帧率上限为 60 帧并关闭垂直同步。如果你的游戏没有问题,请不要使用")]
- public bool FrameRateLock { get; set; }
-
- [ConfigComment(
- en: """
- Use Microsoft YaHei Bold to display characters not in the font library
- Cannot be used together with CustomFont
- """,
- zh: """
- 在显示字库里没有的字时使用微软雅黑 Bold 显示
- 不可以和 CustomFont 一起使用
- """)]
- public bool FontFix { get; set; }
-
- [ConfigComment(
- 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 bool RealisticRandomJudge { get; set; }
-
- [ConfigComment(
- en: "Cannot be used together with HideHanabi",
- zh: """
- 修复 1p 模式下的烟花大小
- 不能和 HideHanabi 一起使用
- """)]
- public bool HanabiFix { get; set; }
-
- [ConfigComment(
- en: "Prevent gray network caused by mistakenly thinking it's an AimeDB server issue",
- zh: "防止因错误认为 AimeDB 服务器问题引起的灰网,建议开启")]
- public bool IgnoreAimeServerError { get; set; }
-
- [ConfigComment(
- en: "Reset touch panel after playing track",
- zh: "在游玩一首曲目后重置触摸面板")]
- public bool TouchResetAfterTrack { get; set; }
-}
diff --git a/AquaMai/Fix/DisableReboot.cs b/AquaMai/Fix/DisableReboot.cs
deleted file mode 100644
index 984e381b..00000000
--- a/AquaMai/Fix/DisableReboot.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using HarmonyLib;
-using Manager.Operation;
-
-namespace AquaMai.Fix
-{
- 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;
- }
- }
-}
diff --git a/AquaMai/Fix/FixCharaCrash.cs b/AquaMai/Fix/FixCharaCrash.cs
deleted file mode 100644
index 36d58b3f..00000000
--- a/AquaMai/Fix/FixCharaCrash.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System;
-using System.Collections.Generic;
-using HarmonyLib;
-using Process;
-using Util;
-
-namespace AquaMai.Fix
-{
- /**
- * Fix character selection crashing due to missing character data
- */
- public class FixCharaCrash
- {
- // Check if the return is null. If it is, make up a color
- [HarmonyPostfix]
- [HarmonyPatch(typeof(CharacterSelectProces), "GetMapColorData")]
- public static void GetMapColorData(ref CharacterSelectProces __instance, ref CharacterMapColorData __result)
- {
- if (__result != null) return;
-
- // 1 is a color that definitely exists
- if (MapMaster.GetSlotData(1) == null)
- {
- MapMaster.GetSlotData(1).Load();
- }
- __result = MapMaster.GetSlotData(1);
- }
-
- // This is called when loading the music selection screen, to display characters on the top screen
- [HarmonyPrefix]
- [HarmonyPatch(typeof(Monitor.CommonMonitor), "SetCharacterSlot", new Type[] { typeof(MessageCharactorInfomationData) })]
- public static bool SetCharacterSlot(ref MessageCharactorInfomationData data, Dictionary ____characterSlotData)
- {
- // Some characters are not found in this dictionary. We simply skip loading those characters
- if (!____characterSlotData.ContainsKey(data.MapKey))
- {
- Console.Log($"Could not get CharacterSlotData for character [Index={data.Index}, MapKey={data.MapKey}], ignoring...");
- return false;
- }
-
- return true;
- }
- }
-}
diff --git a/AquaMai/Fix/FontFix.cs b/AquaMai/Fix/FontFix.cs
deleted file mode 100644
index 400989c7..00000000
--- a/AquaMai/Fix/FontFix.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using System.Collections.Generic;
-using HarmonyLib;
-using MelonLoader;
-using TMPro;
-using UnityEngine;
-using UnityEngine.TextCore.LowLevel;
-
-namespace AquaMai.Fix;
-
-public class FontFix
-{
- private static TMP_FontAsset fontAsset;
- private static List fixedFonts = [];
-
- public static void DoCustomPatch(HarmonyLib.Harmony h)
- {
- var font = new Font(@"C:\Windows\Fonts\msyhbd.ttc");
- fontAsset = TMP_FontAsset.CreateFontAsset(font, 90, 9, GlyphRenderMode.SDFAA, 8192, 8192);
- }
-
- [HarmonyPatch(typeof(TextMeshProUGUI), "Awake")]
- [HarmonyPostfix]
- public static void PostFix(TextMeshProUGUI __instance)
- {
- if (fixedFonts.Contains(__instance.font)) return;
-# if DEBUG
- MelonLogger.Msg($"[FontFix] Fixing font: {__instance.font.name}");
-# endif
- __instance.font.fallbackFontAssetTable.Add(fontAsset);
- fixedFonts.Add(__instance.font);
- }
-}
diff --git a/AquaMai/Fix/ForceFreePlay.cs b/AquaMai/Fix/ForceFreePlay.cs
deleted file mode 100644
index 430f7148..00000000
--- a/AquaMai/Fix/ForceFreePlay.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using HarmonyLib;
-
-namespace AquaMai.Fix;
-
-public class ForceFreePlay
-{
- [HarmonyPrefix]
- [HarmonyPatch(typeof(Manager.Credit), "IsFreePlay")]
- private static bool PreIsFreePlay(ref bool __result)
- {
- __result = true;
- return false;
- }
-}
diff --git a/AquaMai/Fix/ForcePaidPlay.cs b/AquaMai/Fix/ForcePaidPlay.cs
deleted file mode 100644
index 2e66ccbc..00000000
--- a/AquaMai/Fix/ForcePaidPlay.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using HarmonyLib;
-
-namespace AquaMai.Fix;
-
-public class ForcePaidPlay
-{
- [HarmonyPrefix]
- [HarmonyPatch(typeof(Manager.Credit), "IsFreePlay")]
- private static bool PreIsFreePlay(ref bool __result)
- {
- __result = false;
- return false;
- }
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof(Manager.Credit), "IsGameCostEnough")]
- private static bool PreIsGameCostEnough(ref bool __result)
- {
- __result = true;
- return false;
- }
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof(AMDaemon.CreditUnit), "Credit", MethodType.Getter)]
- private static bool PreCredit(ref uint __result)
- {
- __result = 24;
- return false;
- }
-}
diff --git a/AquaMai/Fix/FrameRateLock.cs b/AquaMai/Fix/FrameRateLock.cs
deleted file mode 100644
index 0375aba4..00000000
--- a/AquaMai/Fix/FrameRateLock.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using UnityEngine;
-
-namespace AquaMai.Fix;
-
-public class FrameRateLock
-{
- public static void DoCustomPatch(HarmonyLib.Harmony h)
- {
- Application.targetFrameRate = 60;
- QualitySettings.vSyncCount = 0;
- }
-}
diff --git a/AquaMai/Fix/HanabiFix.cs b/AquaMai/Fix/HanabiFix.cs
deleted file mode 100644
index 429037fa..00000000
--- a/AquaMai/Fix/HanabiFix.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using Fx;
-using HarmonyLib;
-using Monitor;
-using UnityEngine;
-
-namespace AquaMai.Fix;
-
-public class HanabiFix
-{
- [HarmonyPatch(typeof(TapCEffect), "SetUpParticle")]
- [HarmonyPostfix]
- public static void FixMaxSize(TapCEffect __instance, FX_Mai2_Note_Color ____particleControler)
- {
- var entities = ____particleControler.GetComponentsInChildren(true);
- foreach (var entity in entities)
- {
- entity.maxParticleSize = 1f;
- }
- }
-}
diff --git a/AquaMai/Fix/IgnoreAimeServerError.cs b/AquaMai/Fix/IgnoreAimeServerError.cs
deleted file mode 100644
index 4027026e..00000000
--- a/AquaMai/Fix/IgnoreAimeServerError.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using HarmonyLib;
-using Manager;
-
-namespace AquaMai.Fix;
-
-public class IgnoreAimeServerError
-{
- [HarmonyPatch(typeof(OperationManager), "IsAliveAimeServer", MethodType.Getter)]
- [HarmonyPrefix]
- public static bool Prefix(ref bool __result)
- {
- __result = true;
- return false;
- }
-}
diff --git a/AquaMai/Fix/RealisticRandomJudge.cs b/AquaMai/Fix/RealisticRandomJudge.cs
deleted file mode 100644
index f379f7be..00000000
--- a/AquaMai/Fix/RealisticRandomJudge.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using HarmonyLib;
-using Manager;
-
-namespace AquaMai.Fix;
-
-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;
- }
-}
diff --git a/AquaMai/Fix/SkipVersionCheck.cs b/AquaMai/Fix/SkipVersionCheck.cs
deleted file mode 100644
index 1d1c2933..00000000
--- a/AquaMai/Fix/SkipVersionCheck.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using HarmonyLib;
-using Process.Entry.State;
-
-namespace AquaMai.Fix
-{
- public class SkipVersionCheck
- {
- [HarmonyPrefix]
- [HarmonyPatch(typeof(ConfirmPlay), "IsValidVersion")]
- public static bool IsValidVersion(ref bool __result)
- {
- __result = true;
- return false;
- }
- }
-}
diff --git a/AquaMai/Libs/.gitignore b/AquaMai/Libs/.gitignore
new file mode 100644
index 00000000..0f6e1c13
--- /dev/null
+++ b/AquaMai/Libs/.gitignore
@@ -0,0 +1,2 @@
+/Assembly-CSharp.dll
+/AMDaemon.NET.dll
diff --git a/AquaMai/Libs/Mono.Cecil.dll b/AquaMai/Libs/Mono.Cecil.dll
new file mode 100644
index 00000000..18735f93
Binary files /dev/null and b/AquaMai/Libs/Mono.Cecil.dll differ
diff --git a/AquaMai/Main.cs b/AquaMai/Main.cs
deleted file mode 100644
index e0900af8..00000000
--- a/AquaMai/Main.cs
+++ /dev/null
@@ -1,219 +0,0 @@
-using System;
-using System.Globalization;
-using System.IO;
-using System.Reflection;
-using System.Runtime.InteropServices;
-using AquaMai.Attributes;
-using AquaMai.Fix;
-using AquaMai.Helpers;
-using AquaMai.ModKeyMap;
-using AquaMai.Resources;
-using AquaMai.Utils;
-using AquaMai.UX;
-using AquaMai.Visual;
-using MelonLoader;
-using Tomlet;
-using UnityEngine;
-
-namespace AquaMai
-{
- public static class BuildInfo
- {
- public const string Name = "AquaMai";
- public const string Description = "Mod for Sinmai";
- public const string Author = "Aza";
- public const string Company = null;
- public const string Version = "1.2.2";
- public const string DownloadLink = null;
- }
-
- public class AquaMai : MelonMod
- {
- public static Config AppConfig { get; private set; }
- private static bool _hasErrors;
-
- private void Patch(Type type, bool isNested = false)
- {
- var versionAttr = type.GetCustomAttribute();
- var compatible = true;
- if (versionAttr != null)
- {
- if (versionAttr.MinVersion > 0 && versionAttr.MinVersion > GameInfo.GameVersion) compatible = false;
- if (versionAttr.MaxVersion > 0 && versionAttr.MaxVersion < GameInfo.GameVersion) compatible = false;
- }
-
- if (!compatible)
- {
- if (!isNested)
- {
- MelonLogger.Warning(string.Format(Locale.SkipIncompatiblePatch, type));
- }
-
- return;
- }
-
- MelonLogger.Msg($"> Patching {type}");
- try
- {
- HarmonyInstance.PatchAll(type);
- foreach (var nested in type.GetNestedTypes())
- {
- Patch(nested, true);
- }
-
- var customMethod = type.GetMethod("DoCustomPatch", BindingFlags.Public | BindingFlags.Static);
- customMethod?.Invoke(null, [HarmonyInstance]);
- }
- catch (Exception e)
- {
- MelonLogger.Error($"Failed to patch {type}: {e}");
- _hasErrors = true;
- }
- }
-
- /**
- * Apply patches using reflection, based on the settings
- */
- private void ApplyPatches()
- {
- // Iterate over all properties of AppConfig
- foreach (var categoryProp in AppConfig.GetType().GetProperties())
- {
- // Get the value of the category property (e.g., UX, Cheat)
- var categoryValue = categoryProp.GetValue(AppConfig);
- if (categoryValue == null) continue;
- var categoryType = categoryValue.GetType();
-
- // Iterate over properties in the category (e.g., SkipWarningScreen, SinglePlayer)
- foreach (var settingProp in categoryType.GetProperties())
- {
- // The property should be a boolean
- if (settingProp.PropertyType != typeof(bool)) continue;
-
- // Check if the boolean value is true
- if (!(bool)settingProp.GetValue(categoryValue)) continue;
-
- // Get the Type from the config directive name
- var directiveType = Type.GetType($"AquaMai.{categoryProp.Name}.{settingProp.Name}");
-
- // If the type is found, call the Patch method
- if (directiveType != null)
- {
- Patch(directiveType);
- }
- }
- }
- }
-
- [DllImport("kernel32.dll", SetLastError = true)]
- private static extern bool SetConsoleOutputCP(uint wCodePageID);
-
- private static void InitLocale()
- {
- if (!string.IsNullOrEmpty(AppConfig.UX.Locale))
- {
- Locale.Culture = CultureInfo.GetCultureInfo(AppConfig.UX.Locale);
- return;
- }
-
- Locale.Culture = Application.systemLanguage switch
- {
- SystemLanguage.Chinese or SystemLanguage.ChineseSimplified or SystemLanguage.ChineseTraditional => CultureInfo.GetCultureInfo("zh"),
- SystemLanguage.English => CultureInfo.GetCultureInfo("en"),
- _ => CultureInfo.InvariantCulture
- };
- }
-
- public override void OnInitializeMelon()
- {
- // Prevent Chinese characters from being garbled
- SetConsoleOutputCP(65001);
-
- MelonLogger.Msg("Loading mod settings...");
-
- // Check if AquaMai.toml exists
- if (!File.Exists("AquaMai.toml"))
- {
- ConfigGenerator.GenerateConfig();
- 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;
- }
-
- // Read AquaMai.toml to load settings
- AppConfig = TomletMain.To(File.ReadAllText("AquaMai.toml"));
-
- // Init locale with patching C# runtime
- // https://stackoverflow.com/questions/1952638/single-assembly-multi-language-windows-forms-deployment-ilmerge-and-satellite-a
- Patch(typeof(I18nSingleAssemblyHook));
- InitLocale();
-
- // Fixes that does not have side effects
- // These don't need to be configurable
-
- // Helpers
- Patch(typeof(MessageHelper));
- Patch(typeof(MusicDirHelper));
- Patch(typeof(SharedInstances));
- Patch(typeof(GuiSizes));
- // Fixes
- Patch(typeof(FixCharaCrash));
- Patch(typeof(BasicFix));
- Patch(typeof(DisableReboot));
- Patch(typeof(ExtendNotesPool));
- Patch(typeof(FixCheckAuth));
- Patch(typeof(DebugFeature));
- Patch(typeof(FixConnSlide));
- Patch(typeof(FestivalQuickRetryFix));
- // Visual
- Patch(typeof(FixSlideAutoPlay)); // Rename: SlideAutoPlayTweak -> FixSlideAutoPlay, 不过这个应该无副作用所以不需要改配置文件
- Patch(typeof(FixCircleSlideJudge)); // 这个我觉得算无副作用, 可以常开
- Patch(typeof(FixLevelDisplay));
- Patch(typeof(CustomLogo));
- // UX
- Patch(typeof(CustomVersionString));
- Patch(typeof(CustomPlaceName));
- Patch(typeof(RunCommandOnEvents));
- // Utils
- Patch(typeof(JudgeAdjust));
- Patch(typeof(TouchPanelBaudRate));
- // ModKeyMap
- // TODO: Make it configurable
- Patch(typeof(ModKeyListener));
- Patch(typeof(QuickSkip));
- Patch(typeof(TestProof));
- Patch(typeof(PractiseMode));
- Patch(typeof(HideSelfMadeCharts));
-
-# if CI
- Patch(typeof(CiBuildAlert));
-# endif
-
- // Apply patches based on the settings
- ApplyPatches();
-
- 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 override void OnGUI()
- {
- GuiSizes.SetupStyles();
- base.OnGUI();
- }
- }
-}
diff --git a/AquaMai/ModKeyMap/Config.cs b/AquaMai/ModKeyMap/Config.cs
deleted file mode 100644
index 36443109..00000000
--- a/AquaMai/ModKeyMap/Config.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using AquaMai.Attributes;
-
-namespace AquaMai.ModKeyMap;
-
-public class Config
-{
- [ConfigComment(
- en: "Skip to next step",
- zh: """
- 跳过登录过程中的界面直接进入选歌界面
- 在选歌界面直接结束游戏
- """)]
- public ModKeyCode QuickSkip { get; set; } = ModKeyCode.None;
-
- public bool QuickSkipLongPress { get; set; }
-
- [ConfigComment(
- en: "Quick retry in-game",
- zh: "游戏内快速重试")]
- public ModKeyCode InGameRetry { get; set; } = ModKeyCode.None;
-
- public bool InGameRetryLongPress { get; set; }
-
- [ConfigComment(
- en: "Quick skip in-game",
- zh: "游戏内快速跳过")]
- public ModKeyCode InGameSkip { get; set; } = ModKeyCode.None;
-
- public bool InGameSkipLongPress { get; set; }
-
- [ConfigComment(
- en: "Enter game test mode",
- zh: "进入游戏测试模式")]
- public ModKeyCode TestMode { get; set; } = ModKeyCode.Test;
-
- public bool TestModeLongPress { get; set; }
-
- [ConfigComment(
- zh: "练习模式")]
- public ModKeyCode PractiseMode { get; set; } = ModKeyCode.None;
-
- public bool PractiseModeLongPress { get; set; }
-
- [ConfigComment(
- zh: "选歌界面隐藏所有自制谱")]
- public ModKeyCode HideSelfMadeCharts { get; set; } = ModKeyCode.None;
-
- public bool HideSelfMadeChartsLongPress { get; set; }
-
- [ConfigComment(
- en: "Hold the bottom four buttons (3456) for official quick retry (non-utage only)",
- zh: "按住下方四个按钮(3456)使用官方快速重开(仅对非宴谱有效)")]
- public bool EnableNativeQuickRetry { get; set; }
-}
diff --git a/AquaMai/ModKeyMap/EnableNativeQuickRetry.cs b/AquaMai/ModKeyMap/EnableNativeQuickRetry.cs
deleted file mode 100644
index 30077408..00000000
--- a/AquaMai/ModKeyMap/EnableNativeQuickRetry.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using AquaMai.Attributes;
-using HarmonyLib;
-using Manager;
-using Monitor;
-
-namespace AquaMai.ModKeyMap;
-
-[GameVersion(23000)]
-public class EnableNativeQuickRetry
-{
- [HarmonyPrefix]
- [HarmonyPatch(typeof(QuickRetry), "IsQuickRetryEnable")]
- public static bool OnQuickRetryIsQuickRetryEnable(ref bool __result)
- {
- var isUtageProperty = Traverse.Create(typeof(GameManager)).Property("IsUtage");
- __result = !isUtageProperty.PropertyExists() || !isUtageProperty.GetValue();
- return false;
- }
-}
diff --git a/AquaMai/ModKeyMap/ModKeyListener.cs b/AquaMai/ModKeyMap/ModKeyListener.cs
deleted file mode 100644
index 031437cb..00000000
--- a/AquaMai/ModKeyMap/ModKeyListener.cs
+++ /dev/null
@@ -1,145 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using HarmonyLib;
-using Main;
-using Manager;
-using MelonLoader;
-using UnityEngine;
-
-namespace AquaMai.ModKeyMap;
-
-public static class ModKeyListener
-{
- private static readonly Dictionary _keyPressFrames = new();
- private static readonly Dictionary _keyPressFramesPrev = new();
-
- static ModKeyListener()
- {
- foreach (ModKeyCode key in Enum.GetValues(typeof(ModKeyCode)))
- {
- _keyPressFrames[key] = 0;
- _keyPressFramesPrev[key] = 0;
- }
- }
-
- [HarmonyPostfix]
- [HarmonyPatch(typeof(GameMainObject), "Update")]
- public static void CheckLongPush()
- {
- foreach (ModKeyCode key in Enum.GetValues(typeof(ModKeyCode)))
- {
- _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(ModKeyCode key) =>
- key switch
- {
- ModKeyCode.None => false,
- < ModKeyCode.Select1P => Input.GetKey(key.GetKeyCode()),
- ModKeyCode.Test => InputManager.GetSystemInputPush(InputManager.SystemButtonSetting.ButtonTest),
- ModKeyCode.Service => InputManager.GetSystemInputPush(InputManager.SystemButtonSetting.ButtonService),
- ModKeyCode.Select1P => InputManager.GetButtonPush(0, InputManager.ButtonSetting.Select),
- ModKeyCode.Select2P => InputManager.GetButtonPush(1, InputManager.ButtonSetting.Select),
- _ => throw new ArgumentOutOfRangeException(nameof(key), key, "我也不知道这是什么键")
- };
-
- public static bool GetKeyDown(ModKeyCode key)
- {
- // return key switch
- // {
- // ModKeyCode.None => false,
- // < ModKeyCode.Select1P => Input.GetKeyDown(key.GetKeyCode()),
- // ModKeyCode.Test => InputManager.GetSystemInputDown(InputManager.SystemButtonSetting.ButtonTest),
- // ModKeyCode.Service => InputManager.GetSystemInputDown(InputManager.SystemButtonSetting.ButtonService),
- // ModKeyCode.Select1P => InputManager.GetButtonDown(0, InputManager.ButtonSetting.Select),
- // ModKeyCode.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(ModKeyCode 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");
- MelonLogger.Msg(new StackTrace());
- }
-# endif
- return ret;
- }
-
- private static KeyCode GetKeyCode(this ModKeyCode modKeyCode) =>
- modKeyCode switch
- {
- ModKeyCode.Alpha0 => KeyCode.Alpha0,
- ModKeyCode.Alpha1 => KeyCode.Alpha1,
- ModKeyCode.Alpha2 => KeyCode.Alpha2,
- ModKeyCode.Alpha3 => KeyCode.Alpha3,
- ModKeyCode.Alpha4 => KeyCode.Alpha4,
- ModKeyCode.Alpha5 => KeyCode.Alpha5,
- ModKeyCode.Alpha6 => KeyCode.Alpha6,
- ModKeyCode.Alpha7 => KeyCode.Alpha7,
- ModKeyCode.Alpha8 => KeyCode.Alpha8,
- ModKeyCode.Alpha9 => KeyCode.Alpha9,
- ModKeyCode.Keypad0 => KeyCode.Keypad0,
- ModKeyCode.Keypad1 => KeyCode.Keypad1,
- ModKeyCode.Keypad2 => KeyCode.Keypad2,
- ModKeyCode.Keypad3 => KeyCode.Keypad3,
- ModKeyCode.Keypad4 => KeyCode.Keypad4,
- ModKeyCode.Keypad5 => KeyCode.Keypad5,
- ModKeyCode.Keypad6 => KeyCode.Keypad6,
- ModKeyCode.Keypad7 => KeyCode.Keypad7,
- ModKeyCode.Keypad8 => KeyCode.Keypad8,
- ModKeyCode.Keypad9 => KeyCode.Keypad9,
- ModKeyCode.F1 => KeyCode.F1,
- ModKeyCode.F2 => KeyCode.F2,
- ModKeyCode.F3 => KeyCode.F3,
- ModKeyCode.F4 => KeyCode.F4,
- ModKeyCode.F5 => KeyCode.F5,
- ModKeyCode.F6 => KeyCode.F6,
- ModKeyCode.F7 => KeyCode.F7,
- ModKeyCode.F8 => KeyCode.F8,
- ModKeyCode.F9 => KeyCode.F9,
- ModKeyCode.F10 => KeyCode.F10,
- ModKeyCode.F11 => KeyCode.F11,
- ModKeyCode.F12 => KeyCode.F12,
- ModKeyCode.Insert => KeyCode.Insert,
- ModKeyCode.Delete => KeyCode.Delete,
- ModKeyCode.Home => KeyCode.Home,
- ModKeyCode.End => KeyCode.End,
- ModKeyCode.PageUp => KeyCode.PageUp,
- ModKeyCode.PageDown => KeyCode.PageDown,
- ModKeyCode.UpArrow => KeyCode.UpArrow,
- ModKeyCode.DownArrow => KeyCode.DownArrow,
- ModKeyCode.LeftArrow => KeyCode.LeftArrow,
- ModKeyCode.RightArrow => KeyCode.RightArrow,
- _ => throw new ArgumentOutOfRangeException(nameof(modKeyCode), modKeyCode, "游戏功能键需要单独处理")
- };
-}
diff --git a/AquaMai/ModKeyMap/TestProof.cs b/AquaMai/ModKeyMap/TestProof.cs
deleted file mode 100644
index 3ba70177..00000000
--- a/AquaMai/ModKeyMap/TestProof.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using System.Diagnostics;
-using System.Linq;
-using HarmonyLib;
-using Manager;
-
-namespace AquaMai.ModKeyMap;
-
-public class TestProof
-{
- [HarmonyPrefix]
- [HarmonyPatch(typeof(InputManager), "GetSystemInputDown")]
- public static bool GetSystemInputDown(ref bool __result, InputManager.SystemButtonSetting button, bool[] ___SystemButtonDown)
- {
- __result = ___SystemButtonDown[(int)button];
- if (button != InputManager.SystemButtonSetting.ButtonTest)
- return false;
-
- var stackTrace = new StackTrace(); // get call stack
- var stackFrames = stackTrace.GetFrames(); // get method calls (frames)
-
- if (stackFrames.Any(it => it.GetMethod().Name == "DMD"))
- {
- __result = ModKeyListener.GetKeyDownOrLongPress(AquaMai.AppConfig.ModKeyMap.TestMode, AquaMai.AppConfig.ModKeyMap.TestModeLongPress);
- }
-
- return false;
- }
-}
diff --git a/AquaMai/TimeSaving/Config.cs b/AquaMai/TimeSaving/Config.cs
deleted file mode 100644
index 047600cb..00000000
--- a/AquaMai/TimeSaving/Config.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using AquaMai.Attributes;
-
-namespace AquaMai.TimeSaving;
-
-public class Config
-{
- [ConfigComment(
- en: "Skip the warning screen and logo shown after the POST sequence",
- zh: "跳过日服启动时候的 WARNING 界面")]
- public bool SkipWarningScreen { get; set; }
-
- [ConfigComment(
- en: "Disable some useless delays to speed up the game boot process",
- zh: """
- 在自检界面,每个屏幕结束的时候都会等两秒才进入下一个屏幕,很浪费时间
- 开了这个选项之后就不会等了
- """)]
- public bool ImproveLoadSpeed { get; set; }
-
- [ConfigComment(
- en: "Directly enter the song selection screen after login",
- zh: "登录完成后直接进入选歌界面")]
- public bool SkipToMusicSelection { get; set; }
-
- [ConfigComment(
- en: "Skip possible prompts like \"New area discovered\", \"New songs added\", \"There are events\" during game login/registration",
- zh: "跳过登录 / 注册游戏时候可能的 “发现了新的区域哟” “乐曲增加” “有活动哟” 之类的提示")]
- public bool SkipEventInfo { get; set; }
-
- [ConfigComment(
- en: "Skip the \"Do not tap or slide vigorously\" screen, immediately proceed to the next screen once data is loaded",
- zh: "跳过“不要大力拍打或滑动哦”这个界面,数据一旦加载完就立马进入下一个界面")]
- public bool IWontTapOrSlideVigorously { get; set; }
-
- [ConfigComment(
- en: "Skip the \"Goodbye\" screen at the end of the game",
- zh: "跳过游戏结束的“再见”界面")]
- public bool SkipGameOverScreen { get; set; }
-
- [ConfigComment(
- en: "Skip TrackStart screen",
- zh: "跳过乐曲开始界面")]
- public bool SkipTrackStart { get; set; }
-
- [ConfigComment(
- en: "Show a \"skip\" button like AstroDX after the notes end",
- zh: "音符结束之后显示像 AstroDX 一样的“跳过”按钮")]
- public bool ShowQuickEndPlay { get; set; }
-}
diff --git a/AquaMai/TimeSaving/IWontTapOrSlideVigorously.cs b/AquaMai/TimeSaving/IWontTapOrSlideVigorously.cs
deleted file mode 100644
index 3f6e9d32..00000000
--- a/AquaMai/TimeSaving/IWontTapOrSlideVigorously.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using HarmonyLib;
-using Monitor;
-
-namespace AquaMai.TimeSaving;
-
-public class IWontTapOrSlideVigorously
-{
- [HarmonyPrefix]
- [HarmonyPatch(typeof(PlInformationMonitor), "IsPlayPlInfoEnd")]
- public static bool Patch(ref bool __result)
- {
- __result = true;
- return false;
- }
-}
diff --git a/AquaMai/TimeSaving/ImproveLoadSpeed.cs b/AquaMai/TimeSaving/ImproveLoadSpeed.cs
deleted file mode 100644
index 359cc6f3..00000000
--- a/AquaMai/TimeSaving/ImproveLoadSpeed.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System.Diagnostics;
-using HarmonyLib;
-using Process;
-
-namespace AquaMai.TimeSaving
-{
- public class ImproveLoadSpeed
- {
- [HarmonyPrefix]
- [HarmonyPatch(typeof(PowerOnProcess), "OnStart")]
- public static void PrePowerOnStart(ref float ____waitTime)
- {
- ____waitTime = 0f;
- }
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof(StartupProcess), "OnUpdate")]
- public static void PreStartupUpdate(byte ____state, ref Stopwatch ___timer)
- {
- if (____state == 8)
- {
- Traverse.Create(___timer).Field("elapsed").SetValue(2 * 10000000L);
- }
- }
- }
-}
diff --git a/AquaMai/TimeSaving/SkipEventInfo.cs b/AquaMai/TimeSaving/SkipEventInfo.cs
deleted file mode 100644
index 3dd7ef24..00000000
--- a/AquaMai/TimeSaving/SkipEventInfo.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System.Collections.Generic;
-using HarmonyLib;
-using Process;
-using Process.Information;
-
-namespace AquaMai.TimeSaving
-{
- public class SkipEventInfo
- {
- [HarmonyPostfix]
- [HarmonyPatch(typeof(InformationProcess), "OnStart")]
- public static void InformationProcessPostStart(ref uint ____state)
- {
- ____state = 3;
- }
-
- [HarmonyPostfix]
- [HarmonyPatch(typeof(RegionalSelectProcess), "OnStart")]
- public static void RegionalSelectProcessPreStart(ref Queue[] ____discoverList)
- {
- ____discoverList = new Queue[] { new Queue(), new Queue() };
- }
- }
-}
diff --git a/AquaMai/TimeSaving/SkipToMusicSelection.cs b/AquaMai/TimeSaving/SkipToMusicSelection.cs
deleted file mode 100644
index fe8c435b..00000000
--- a/AquaMai/TimeSaving/SkipToMusicSelection.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using DB;
-using HarmonyLib;
-using MAI2.Util;
-using Manager;
-using Monitor;
-using Process;
-using Process.Information;
-
-namespace AquaMai.TimeSaving
-{
- public class SkipToMusicSelection
- {
- /*
- * Highly experimental, may well break some stuff
- * Works by overriding the info screen (where it shows new events and stuff)
- * to directly exit to the music selection screen, skipping character and
- * event selection, among others
- */
- [HarmonyPrefix]
- [HarmonyPatch(typeof(InformationProcess), "OnUpdate")]
- public static bool OnUpdate(InformationProcess __instance, ProcessDataContainer ___container)
- {
- GameManager.SetMaxTrack();
- // Set headphone volume
- for (var i = 0; i < 2; i++)
- {
- var userData = UserDataManager.Instance.GetUserData(i);
- if (userData.IsEntry)
- {
- OptionHeadphonevolumeID headPhoneVolume = userData.Option.HeadPhoneVolume;
- SoundManager.SetHeadPhoneVolume(i, headPhoneVolume.GetValue());
- }
- }
- ___container.processManager.AddProcess(new MusicSelectProcess(___container));
- ___container.processManager.ReleaseProcess(__instance);
- return false;
- }
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof(MapResultMonitor), "Initialize")]
- public static void MapResultMonitorPreInitialize(int monIndex)
- {
- var userData = Singleton.Instance.GetUserData(monIndex);
- var index = userData.MapList.FindIndex((UserMapData m) => m.ID == userData.Detail.SelectMapID);
- if (index >= 0) return;
- userData.MapList.Clear();
- }
- }
-}
diff --git a/AquaMai/TimeSaving/SkipWarningScreen.cs b/AquaMai/TimeSaving/SkipWarningScreen.cs
deleted file mode 100644
index 3b0a6428..00000000
--- a/AquaMai/TimeSaving/SkipWarningScreen.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using HarmonyLib;
-using Monitor;
-
-namespace AquaMai.TimeSaving
-{
- public class SkipWarningScreen
- {
- /*
- * Patch PlayLogo to disable the warning screen
- */
- [HarmonyPrefix]
- [HarmonyPatch(typeof (WarningMonitor), "PlayLogo")]
- public static bool PlayLogo()
- {
- // Return false to block the original method
- return false;
- }
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof (WarningMonitor), "IsLogoAnimationEnd")]
- public static bool IsLogoAnimationEnd(ref bool __result)
- {
- // Always return true to indicate the animation has ended
- __result = true;
- return false;
- }
- }
-}
diff --git a/AquaMai/TouchSensitivity/Config.cs b/AquaMai/TouchSensitivity/Config.cs
deleted file mode 100644
index 3afd7244..00000000
--- a/AquaMai/TouchSensitivity/Config.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-using AquaMai.Attributes;
-
-namespace AquaMai.TouchSensitivity;
-
-public class Config
-{
- [ConfigComment(
- en: """
- Enable custom sensitivity
- When enabled, the settings in Test mode will not take effect
- When disabled, the settings in Test mode can still be used
- """,
- zh: """
- 是否启用自定义灵敏度
- 这里启用之后 Test 里的就不再起作用了
- 这里禁用之后就还是可以用 Test 里的调
- """)]
- public bool Enable { get; set; }
-
- [ConfigComment(
- en: """
- Sensitivity adjustments in Test mode are not linear
- Default sensitivity in area A: 90, 80, 70, 60, 50, 40, 30, 26, 23, 20, 10
- Default sensitivity in other areas: 70, 60, 50, 40, 30, 20, 15, 10, 5, 1, 1
- A setting of 0 in Test mode corresponds to 40, 20 here, -5 corresponds to 90, 70, +5 corresponds to 10, 1
- The higher the number in Test mode, the lower the number here, resulting in higher sensitivity for official machines
- For ADX, the sensitivity is reversed, so the higher the number here, the higher the sensitivity
- """,
- zh: """
- 在 Test 模式下调整的灵敏度不是线性的
- A 区默认灵敏度 90, 80, 70, 60, 50, 40, 30, 26, 23, 20, 10
- 其他区域默认灵敏度 70, 60, 50, 40, 30, 20, 15, 10, 5, 1, 1
- Test 里设置的 0 对应的是 40, 20 这一档,-5 是 90, 70,+5 是 10, 1
- Test 里的数字越大,这里的数字越小,对于官机来说,灵敏度更大
- 而 ADX 的灵敏度是反的,所以对于 ADX,这里的数字越大,灵敏度越大
- """)]
- public byte A1 { get; set; } = 40;
-
- public byte A2 { get; set; } = 40;
- public byte A3 { get; set; } = 40;
- public byte A4 { get; set; } = 40;
- public byte A5 { get; set; } = 40;
- public byte A6 { get; set; } = 40;
- public byte A7 { get; set; } = 40;
- public byte A8 { get; set; } = 40;
- public byte B1 { get; set; } = 20;
- public byte B2 { get; set; } = 20;
- public byte B3 { get; set; } = 20;
- public byte B4 { get; set; } = 20;
- public byte B5 { get; set; } = 20;
- public byte B6 { get; set; } = 20;
- public byte B7 { get; set; } = 20;
- public byte B8 { get; set; } = 20;
- public byte C1 { get; set; } = 20;
- public byte C2 { get; set; } = 20;
- public byte D1 { get; set; } = 20;
- public byte D2 { get; set; } = 20;
- public byte D3 { get; set; } = 20;
- public byte D4 { get; set; } = 20;
- public byte D5 { get; set; } = 20;
- public byte D6 { get; set; } = 20;
- public byte D7 { get; set; } = 20;
- public byte D8 { get; set; } = 20;
- public byte E1 { get; set; } = 20;
- public byte E2 { get; set; } = 20;
- public byte E3 { get; set; } = 20;
- public byte E4 { get; set; } = 20;
- public byte E5 { get; set; } = 20;
- public byte E6 { get; set; } = 20;
- public byte E7 { get; set; } = 20;
- public byte E8 { get; set; } = 20;
-}
diff --git a/AquaMai/TouchSensitivity/Enable.cs b/AquaMai/TouchSensitivity/Enable.cs
deleted file mode 100644
index c7d4b1f6..00000000
--- a/AquaMai/TouchSensitivity/Enable.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using System.Collections.Generic;
-using HarmonyLib;
-using IO;
-using Manager;
-using MelonLoader;
-
-namespace AquaMai.TouchSensitivity;
-
-public class Enable
-{
- [HarmonyPrefix]
- [HarmonyPatch(typeof(NewTouchPanel), "SetTouchPanelSensitivity")]
- public static void SetTouchPanelSensitivityPrefix(List sensitivity)
- {
- var configType = AquaMai.AppConfig.TouchSensitivity.GetType();
- for (var i = 0; i < 34; i++)
- {
- var area = (InputManager.TouchPanelArea)i;
- var field = configType.GetProperty(area.ToString());
- var value = (byte)field.GetValue(AquaMai.AppConfig.TouchSensitivity);
- sensitivity[i] = value;
- }
- MelonLogger.Msg("[TouchSensitivity] Applied");
- }
-}
diff --git a/AquaMai/UX/CiBuildAlert.cs b/AquaMai/UX/CiBuildAlert.cs
deleted file mode 100644
index b6c2e307..00000000
--- a/AquaMai/UX/CiBuildAlert.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using AquaMai.Helpers;
-using AquaMai.Resources;
-using HarmonyLib;
-using Process;
-
-namespace AquaMai.UX;
-
-public class CiBuildAlert
-{
- [HarmonyPatch(typeof(AdvertiseProcess), "OnStart")]
- [HarmonyPostfix]
- public static void OnStart(AdvertiseProcess __instance)
- {
- MessageHelper.ShowMessage(Locale.CiBuildAlertContent, title: Locale.CiBuildAlertTitle);
- }
-}
diff --git a/AquaMai/UX/Config.cs b/AquaMai/UX/Config.cs
deleted file mode 100644
index 96770a05..00000000
--- a/AquaMai/UX/Config.cs
+++ /dev/null
@@ -1,140 +0,0 @@
-using AquaMai.Attributes;
-
-namespace AquaMai.UX;
-
-public class Config
-{
- [ConfigComment(
- en: "Language for mod UI, supports en and zh",
- zh: "Mod 界面的语言,支持 en 和 zh")]
- public string Locale { get; set; } = "";
-
- [ConfigComment(
- en: "Single player: Show 1P only, at the center of the screen",
- zh: "单人模式,不显示 2P")]
- public bool SinglePlayer { get; set; }
-
- [ConfigComment(
- en: "Remove the circle mask in the game",
- zh: "移除遮罩")]
- public bool HideMask { get; set; }
-
- [ConfigComment(
- en: "Load Jacket image from folder \"LocalAssets\" and filename \"{MusicID}.png\" for self-made charts",
- zh: "通过游戏目录下 `LocalAssets\\000000(歌曲 ID).png` 加载封面,自制谱用")]
- public bool LoadAssetsPng { get; set; }
-
- [ConfigComment(
- en: "Add \".ab\" image resources without the need of rebuilding a manifest",
- zh: """
- 优化图片资源的加载,就算没有 AssetBundleImages.manifest 也可以正常加载 ab 格式的图片资源
- 导入了删除曲包之类的话,应该需要开启这个
- """)]
- public bool LoadAssetBundleWithoutManifest { get; set; }
-
- [ConfigComment(
- en: """
- Random BGM, put Mai2Cue.{acb,awb} of old version of the game in `LocalAssets\Mai2Cue` and rename them
- Do not enable when SinglePlayer is off
- """,
- zh: """
- 在 `LocalAssets\Mai2Cue` 这个目录下放置了旧版游戏的 Mai2Cue.{acb,awb} 并重命名的话,可以在播放游戏 BGM 的时候随机播放这里面的旧版游戏 BGM
- 和 2P 模式有冲突,如果你没有开启 'SinglePlayer' 的话,请关闭这个
- """)]
- public bool RandomBgm { get; set; }
-
- [ConfigComment(
- en: "Play \"Master\" difficulty on Demo screen",
- zh: "在闲置时的演示画面上播放紫谱而不是绿谱")]
- public bool DemoMaster { get; set; }
-
- [ConfigComment(
- en: """
- Disable timers
- Not recommand to enable when SinglePlayer is off
- """,
- zh: """
- 关掉那些游戏中的倒计时
- 如果你没有开启 'SinglePlayer' 的话,不建议开这个,不过要开的话也不是不可以
- """)]
- public bool ExtendTimer { get; set; }
-
- [ConfigComment(
- en: "Save immediate after playing a song",
- zh: "打完一首歌的时候立即向服务器保存成绩")]
- public bool ImmediateSave { get; set; }
-
- [ConfigComment(
- en: """
- Use the png jacket above as BGA if BGA is not found for self-made charts
- Use together with `LoadJacketPng`
- """,
- zh: """
- 如果没有 dat 格式的 BGA 的话,就用歌曲的封面做背景,而不是显示迪拉熊的笑脸
- 请和 `LoadJacketPng` 一起用
- """)]
- public bool LoadLocalBga { get; set; }
-
- [ConfigComment(
- en: """
- Place font.ttf in the LocalAssets directory to replace the game's global font
- Cannot be used together with FontFix
- """,
- zh: """
- 在 LocalAssets 目录下放置 font.ttf 可以替换游戏的全局字体
- 不可以和 FontFix 一起使用
- """)]
- public bool CustomFont { get; set; }
-
- [ConfigComment(
- en: "Map touch actions to buttons",
- zh: "映射触摸操作至实体按键")]
- public bool TouchToButtonInput { get; set; }
-
- [ConfigComment(
- en: "Cannot be used together with HanabiFix",
- zh: """
- 完全隐藏烟花
- 不能和 HanabiFix 一起使用
- """)]
- public bool HideHanabi { get; set; }
-
- [ConfigComment(
- zh: "让星星在启动拍等待期间从 50% 透明度渐入为 100%,取代原本在击打星星头时就完成渐入",
- en: "Slides will fade in instead of instantly appearing")]
- public bool SlideFadeInTweak { get; set; }
-
- [ConfigComment(
- zh: "在游戏总结的计分板中显示击打误差的详细信息(以帧为单位)",
- en: "Show detailed accuracy info in the score board")]
- public bool JudgeAccuracyInfo { get; set; }
-
- [ConfigComment(
- en: "Set the version string displayed at the top-right corner of the screen",
- zh: "把右上角的版本更改为自定义文本")]
- public string CustomVersionString { get; set; } = "";
-
- [ConfigComment(
- en: """
- Custom shop name in photo
- Also enable shop name display in SDGA
- """,
- zh: """
- 自定义拍照的店铺名称
- 同时在 SDGA 中会启用店铺名称的显示(但是不会在游戏里有设置)
- """)]
- public string CustomPlaceName { get; set; } = "";
-
- [ConfigComment(
- en: "Execute some command on game idle",
- zh: """
- 在游戏闲置的时候执行指定的命令脚本
- 比如说可以在游戏闲置是降低显示器的亮度
- """)]
- public string ExecOnIdle { get; set; } = "";
-
- [ConfigComment(
- en: "Execute some command on game start",
- zh: "在玩家登录的时候执行指定的命令脚本")]
- public string ExecOnEntry { get; set; } = "";
-}
\ No newline at end of file
diff --git a/AquaMai/UX/CustomFont.cs b/AquaMai/UX/CustomFont.cs
deleted file mode 100644
index 94661311..00000000
--- a/AquaMai/UX/CustomFont.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using HarmonyLib;
-using MelonLoader;
-using TMPro;
-using UnityEngine;
-using UnityEngine.TextCore.LowLevel;
-
-namespace AquaMai.UX;
-
-public class CustomFont
-{
- private static Font font;
- private static TMP_FontAsset fontAsset;
-
- public static void DoCustomPatch(HarmonyLib.Harmony h)
- {
- var fontPath = Path.Combine(Environment.CurrentDirectory, "LocalAssets", "font.ttf");
- if (!File.Exists(fontPath)) return;
-
- font = new Font(fontPath);
- fontAsset = TMP_FontAsset.CreateFontAsset(font, 90, 9, GlyphRenderMode.SDFAA, 8192, 8192);
- }
-
- [HarmonyPatch(typeof(TextMeshProUGUI), "Awake")]
- [HarmonyPostfix]
- public static void PostFix(TextMeshProUGUI __instance)
- {
- if (font is null) return;
-# if DEBUG
- MelonLogger.Msg($"{__instance.font.name} {__instance.text}");
-# endif
-
- var materialOrigin = __instance.fontMaterial;
- var materialSharedOrigin = __instance.fontSharedMaterial;
- __instance.font = fontAsset;
-# if DEBUG
- MelonLogger.Msg($"shaderKeywords {materialOrigin.shaderKeywords.Join()} {__instance.fontMaterial.shaderKeywords.Join()}");
-# endif
- // __instance.fontSharedMaterial = materialSharedOrigin;
-
- // 这样之后该有描边的地方整个字后面都是阴影,它不知道哪里是边
- // materialOrigin.mainTexture = __instance.fontMaterial.mainTexture;
- // materialOrigin.mainTextureOffset = __instance.fontMaterial.mainTextureOffset;
- // materialOrigin.mainTextureScale = __instance.fontMaterial.mainTextureScale;
- // __instance.fontMaterial.CopyPropertiesFromMaterial(materialOrigin);
-
- // 这样了之后有描边了,但是描边很细
- // __instance.fontMaterial.shader = materialOrigin.shader;
- foreach (var keyword in materialOrigin.shaderKeywords)
- {
- __instance.fontMaterial.EnableKeyword(keyword);
- }
- // __instance.fontMaterial.globalIlluminationFlags = materialOrigin.globalIlluminationFlags;
-
- // 原来是 underlay,但是复制这三个属性之后就又变成整个字后面都是阴影了
- // __instance.fontMaterial.SetFloat(ShaderUtilities.ID_UnderlayOffsetY, materialOrigin.GetFloat(ShaderUtilities.ID_UnderlayOffsetY));
- // __instance.fontMaterial.SetFloat(ShaderUtilities.ID_UnderlayOffsetX, materialOrigin.GetFloat(ShaderUtilities.ID_UnderlayOffsetX));
- // __instance.fontMaterial.SetFloat(ShaderUtilities.ID_UnderlayDilate, materialOrigin.GetFloat(ShaderUtilities.ID_UnderlayDilate));
-
- // if(materialOrigin.shaderKeywords.Contains(ShaderUtilities.Keyword_Underlay))
- // {
- // __instance.fontMaterial.EnableKeyword(ShaderUtilities.Keyword_Glow);
- // __instance.fontMaterial.SetFloat(ShaderUtilities.ID_GlowOuter, .5f);
- // // __instance.fontMaterial.SetFloat(ShaderUtilities.ID_UnderlayOffsetX, materialOrigin.GetFloat(ShaderUtilities.ID_UnderlayOffsetX));
- // }
-
- }
-}
diff --git a/AquaMai/UX/CustomPlaceName.cs b/AquaMai/UX/CustomPlaceName.cs
deleted file mode 100644
index 3fc5e8db..00000000
--- a/AquaMai/UX/CustomPlaceName.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using HarmonyLib;
-using Manager;
-
-namespace AquaMai.UX;
-
-public class CustomPlaceName
-{
- [HarmonyPostfix]
- [HarmonyPatch(typeof(OperationManager), "CheckAuth_Proc")]
- public static void CheckAuth_Proc(OperationManager __instance)
- {
- if (string.IsNullOrEmpty(AquaMai.AppConfig.UX.CustomPlaceName))
- {
- return;
- }
-
- __instance.ShopData.ShopName = AquaMai.AppConfig.UX.CustomPlaceName;
- __instance.ShopData.ShopNickName = AquaMai.AppConfig.UX.CustomPlaceName;
- }
-
- [HarmonyPostfix]
- [HarmonyPatch(typeof(ResultCardBaseController), "Initialize")]
- public static void Initialize(ResultCardBaseController __instance)
- {
- if (string.IsNullOrEmpty(AquaMai.AppConfig.UX.CustomPlaceName))
- {
- return;
- }
-
- __instance.SetVisibleStoreName(true);
- }
-}
diff --git a/AquaMai/UX/CustomVersionString.cs b/AquaMai/UX/CustomVersionString.cs
deleted file mode 100644
index 78282ea9..00000000
--- a/AquaMai/UX/CustomVersionString.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using HarmonyLib;
-
-namespace AquaMai.UX
-{
- public class CustomVersionString
- {
- /*
- * Patch displayVersionString Property Getter
- */
- [HarmonyPrefix]
- [HarmonyPatch(typeof(MAI2System.Config), "displayVersionString", MethodType.Getter)]
- public static bool GetDisplayVersionString(ref string __result)
- {
- if (string.IsNullOrEmpty(AquaMai.AppConfig.UX.CustomVersionString))
- {
- return true;
- }
-
- __result = AquaMai.AppConfig.UX.CustomVersionString;
- // Return false to block the original method
- return false;
- }
- }
-}
\ No newline at end of file
diff --git a/AquaMai/UX/DemoMaster.cs b/AquaMai/UX/DemoMaster.cs
deleted file mode 100644
index e3dd5090..00000000
--- a/AquaMai/UX/DemoMaster.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using DB;
-using HarmonyLib;
-using MAI2.Util;
-using Manager;
-using Process;
-
-namespace AquaMai.UX
-{
- public class DemoMaster
- {
- [HarmonyPostfix]
- [HarmonyPatch(typeof(AdvDemoProcess), "OnStart")]
- public static void AdvDemoProcessPostStart()
- {
- for (int i = 0; i < 2; i++)
- {
- var userOption = Singleton.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;
- }
- }
-}
diff --git a/AquaMai/UX/ExtendTimer.cs b/AquaMai/UX/ExtendTimer.cs
deleted file mode 100644
index e91238a2..00000000
--- a/AquaMai/UX/ExtendTimer.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using HarmonyLib;
-using Manager;
-using Monitor;
-using Process;
-using Process.Entry.State;
-using Process.ModeSelect;
-
-namespace AquaMai.UX
-{
- public class ExtendTimer
- {
- [HarmonyPrefix]
- [HarmonyPatch(typeof(TimerController), "PrepareTimer")]
- public static void PrePrepareTimer(ref int second)
- {
- second = 65535;
- }
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof(CommonTimer), "SetVisible")]
- public static void CommonTimerSetVisible(ref bool isVisible)
- {
- isVisible = false;
- }
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof(EntryProcess), "DecrementTimerSecond")]
- public static bool EntryProcessDecrementTimerSecond(ContextEntry ____context)
- {
- SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_SYS_SKIP, 0);
- ____context.SetState(StateType.DoneEntry);
- return false;
- }
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof(ModeSelectProcess), "UpdateInput")]
- public static bool ModeSelectProcessUpdateInput(ModeSelectProcess __instance)
- {
- if (!InputManager.GetButtonDown(0, InputManager.ButtonSetting.Button05)) return true;
- __instance.TimeSkipButtonAnim(InputManager.ButtonSetting.Button05);
- SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_SYS_SKIP, 0);
- Traverse.Create(__instance).Method("TimeUp").GetValue();
- return false;
- }
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof(PhotoEditProcess), "MainMenuUpdate")]
- public static void PhotoEditProcess(PhotoEditMonitor[] ____monitors, PhotoEditProcess __instance)
- {
- if (!InputManager.GetButtonDown(0, InputManager.ButtonSetting.Button04)) return;
- SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_SYS_SKIP, 0);
- ____monitors[0].SetButtonPressed(InputManager.ButtonSetting.Button04);
- Traverse.Create(__instance).Method("OnTimeUp").GetValue();
- }
- }
-}
diff --git a/AquaMai/UX/LoadAssetBundleWithoutManifest.cs b/AquaMai/UX/LoadAssetBundleWithoutManifest.cs
deleted file mode 100644
index 90dcb2b4..00000000
--- a/AquaMai/UX/LoadAssetBundleWithoutManifest.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using HarmonyLib;
-using UnityEngine;
-using Manager;
-using Util;
-
-namespace AquaMai.UX
-{
- public class LoadAssetBundleWithoutManifest
- {
- private static HashSet abFiles = new HashSet();
-
- [HarmonyPostfix]
- [HarmonyPatch(typeof(OptionDataManager), "CheckAssetBundle")]
- public static void PostCheckAssetBundle(ref Safe.ReadonlySortedDictionary abs)
- {
- foreach (var ab in abs)
- {
- abFiles.Add(ab.Key);
- }
- }
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof(AssetBundleManifest), "GetAllAssetBundles")]
- public static bool PreGetAllAssetBundles(AssetBundleManifest __instance, ref string[] __result)
- {
- __result = abFiles.ToArray();
- return false;
- }
- }
-}
\ No newline at end of file
diff --git a/AquaMai/UX/RandomBgm.cs b/AquaMai/UX/RandomBgm.cs
deleted file mode 100644
index 1b5322cc..00000000
--- a/AquaMai/UX/RandomBgm.cs
+++ /dev/null
@@ -1,73 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using HarmonyLib;
-using Mai2.Mai2Cue;
-using MAI2.Util;
-using Manager;
-using MelonLoader;
-
-namespace AquaMai.UX
-{
- public class RandomBgm
- {
- private static List _acbs = new List();
-
- [HarmonyPostfix]
- [HarmonyPatch(typeof(SoundManager), "Initialize")]
- public static void Init()
- {
- if (!Directory.Exists(Path.Combine(Environment.CurrentDirectory, "LocalAssets", "Mai2Cue"))) return;
- var files = Directory.EnumerateFiles(Path.Combine(Environment.CurrentDirectory, "LocalAssets", "Mai2Cue"));
- 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.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;
- }
- }
- }
-}
diff --git a/AquaMai/UX/RunCommandOnEvents.cs b/AquaMai/UX/RunCommandOnEvents.cs
deleted file mode 100644
index b6a9a791..00000000
--- a/AquaMai/UX/RunCommandOnEvents.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System;
-using System.Diagnostics;
-using HarmonyLib;
-using MelonLoader;
-using Process;
-
-namespace AquaMai.UX
-{
- public class RunCommandOnEvents
- {
- [HarmonyPrefix]
- [HarmonyPatch(typeof(AdvertiseProcess), "OnStart")]
- public static void AdvertiseProcessPreStart()
- {
- if (!string.IsNullOrWhiteSpace(AquaMai.AppConfig.UX.ExecOnIdle))
- {
- Exec(AquaMai.AppConfig.UX.ExecOnIdle);
- }
- }
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof(EntryProcess), "OnStart")]
- public static void EntryProcessPreStart()
- {
- if (!string.IsNullOrWhiteSpace(AquaMai.AppConfig.UX.ExecOnEntry))
- {
- Exec(AquaMai.AppConfig.UX.ExecOnEntry);
- }
- }
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof(MusicSelectProcess), "OnStart")]
- public static void MusicSelectProcessPreStart()
- {
- if (!string.IsNullOrWhiteSpace(AquaMai.AppConfig.UX.ExecOnEntry))
- {
- Exec(AquaMai.AppConfig.UX.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();
- }
- }
-}
\ No newline at end of file
diff --git a/AquaMai/UX/SinglePlayer.cs b/AquaMai/UX/SinglePlayer.cs
deleted file mode 100644
index bdc2b3b4..00000000
--- a/AquaMai/UX/SinglePlayer.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-using System.Collections.Generic;
-using System.Reflection;
-using AquaMai.Attributes;
-using HarmonyLib;
-using MAI2.Util;
-using Manager;
-using MelonLoader;
-using Monitor.Common;
-using Monitor.Entry;
-using Monitor.Entry.Parts.Screens;
-using UnityEngine;
-using Type = System.Type;
-
-namespace AquaMai.UX
-{
- // Hides the 2p (right hand side) UI.
- // Note: this is not my original work. I simply interpreted the code and rewrote it as a mod.
- public class SinglePlayer
- {
- [HarmonyPatch]
- public class WhateverInitialize
- {
- public static IEnumerable TargetMethods()
- {
- var lateInitialize = AccessTools.Method(typeof(Main.GameMain), "LateInitialize", [typeof(MonoBehaviour), typeof(Transform), typeof(Transform)]);
- if (lateInitialize is not null) return [lateInitialize];
- return [AccessTools.Method(typeof(Main.GameMain), "Initialize", [typeof(MonoBehaviour), typeof(Transform), typeof(Transform)])];
- }
-
- public static void Prefix(MonoBehaviour gameMainObject, ref Transform left, ref Transform right)
- {
- left.transform.position = Vector3.zero;
- right.localScale = Vector3.zero;
- GameObject.Find("Mask").transform.position = new Vector3(540f, 0f, 0f);
- }
- }
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof(MeshButton), "IsPointInPolygon", new Type[] { typeof(Vector2[]), typeof(Vector2) })]
- public static bool IsPointInPolygon(Vector2[] polygon, ref Vector2 point, MeshButton __instance, ref bool __result)
- {
- __result = RectTransformUtility.RectangleContainsScreenPoint(__instance.GetComponent(), point, Camera.main);
- return false;
- }
-
- [GameVersion(21500)]
- public class SkipTimer
- {
- [HarmonyPostfix]
- [HarmonyPatch(typeof(EntryMonitor), "DecideEntry")]
- public static void PostDecideEntry(EntryMonitor __instance)
- {
- MelonLogger.Msg("Confirm Entry");
- TimeManager.MarkGameStartTime();
- Singleton.Instance.UpdateEvent();
- Singleton.Instance.UpdateData();
- __instance.Process.CreateDownloadProcess();
- __instance.ProcessManager.SendMessage(new Message(ProcessType.CommonProcess, 30001));
- __instance.ProcessManager.SendMessage(new Message(ProcessType.CommonProcess, 40000, 0, OperationInformationController.InformationType.Hide));
- __instance.Process.SetNextProcess();
- }
-
- // To prevent the "長押受付終了" overlay from appearing
- [HarmonyPrefix]
- [HarmonyPatch(typeof(WaitPartner), "Open")]
- public static bool WaitPartnerPreOpen()
- {
- return false;
- }
- }
- }
-}
diff --git a/AquaMai/Utils/Config.cs b/AquaMai/Utils/Config.cs
deleted file mode 100644
index 23f1b9c6..00000000
--- a/AquaMai/Utils/Config.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-using AquaMai.Attributes;
-
-namespace AquaMai.Utils;
-
-public class Config
-{
- [ConfigComment(
- en: "Log user ID on login",
- zh: "登录时将 UserID 输出到日志")]
- public bool LogUserId { get; set; }
-
- [ConfigComment(
- en: "Globally increase A judgment, unit is the same as in the game",
- zh: "全局增加 A 判,单位和游戏里一样")]
- public double JudgeAdjustA { get; set; }
-
- [ConfigComment(
- en: "Globally increase B judgment, unit is the same as in the game",
- zh: "全局增加 B 判,单位和游戏里一样")]
- public double JudgeAdjustB { get; set; }
-
- [ConfigComment(
- en: "Touch screen delay, unit is milliseconds, one second = 1000 milliseconds. Must be an integer",
- zh: "触摸屏延迟,单位为毫秒,一秒 = 1000 毫秒。必须是整数")]
- public int TouchDelay { get; set; }
-
- [ConfigComment(
- en: "Show detail of selected song in music selection screen",
- zh: "选歌界面显示选择的歌曲的详情")]
- public bool SelectionDetail { get; set; }
-
- [ConfigComment(
- en: "Show Network error detail in the game",
- zh: "出现灰网时显示原因")]
- public bool ShowNetErrorDetail { get; set; }
-
- [ConfigComment(
- en: "Show error log in the game",
- zh: "在游戏中显示错误日志窗口而不是关闭游戏进程")]
- public bool ShowErrorLog { get; set; }
-
- [ConfigComment(
- en: "Display framerate",
- zh: "显示帧率")]
- public bool FrameRateDisplay { get; set; }
-
- [ConfigComment(
- en: """
- Adjust the baud rate of the touch screen serial port, default value is 9600
- Requires hardware support. If you are unsure whether you can use it, you cannot use it
- Set to 0 to disable
- """,
- zh: """
- 调整触摸屏串口波特率,默认值 9600
- 需要硬件配合。如果你不清楚你是否可以使用,那你不能使用
- 改为 0 禁用
- """)]
- public int TouchPanelBaudRate { get; set; }
-}
diff --git a/AquaMai/Utils/JudgeAdjust.cs b/AquaMai/Utils/JudgeAdjust.cs
deleted file mode 100644
index 071c4127..00000000
--- a/AquaMai/Utils/JudgeAdjust.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System.Threading;
-using HarmonyLib;
-using IO;
-using Manager.UserDatas;
-
-namespace AquaMai.Utils;
-
-public class JudgeAdjust
-{
- [HarmonyPostfix]
- [HarmonyPatch(typeof(UserOption), "GetAdjustMSec")]
- public static void GetAdjustMSec(ref float __result)
- {
- __result += (float)AquaMai.AppConfig.Utils.JudgeAdjustA * 16.666666f;
- }
-
- [HarmonyPostfix]
- [HarmonyPatch(typeof(UserOption), "GetJudgeTimingFrame")]
- public static void GetJudgeTimingFrame(ref float __result)
- {
- __result += (float)AquaMai.AppConfig.Utils.JudgeAdjustB;
- }
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof(NewTouchPanel), "Recv")]
- public static void NewTouchPanelRecv()
- {
- if (AquaMai.AppConfig.Utils.TouchDelay <= 0) return;
- Thread.Sleep(AquaMai.AppConfig.Utils.TouchDelay);
- }
-}
diff --git a/AquaMai/Utils/TouchPanelBaudRate.cs b/AquaMai/Utils/TouchPanelBaudRate.cs
deleted file mode 100644
index 57cdb091..00000000
--- a/AquaMai/Utils/TouchPanelBaudRate.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using HarmonyLib;
-using IO;
-
-namespace AquaMai.Utils;
-
-public class TouchPanelBaudRate
-{
- [HarmonyPatch(typeof(NewTouchPanel), "Open")]
- [HarmonyPrefix]
- private static void OpenPrefix(ref int ___BaudRate)
- {
- if (AquaMai.AppConfig.Utils.TouchPanelBaudRate <= 0) return;
- ___BaudRate = AquaMai.AppConfig.Utils.TouchPanelBaudRate;
- }
-}
diff --git a/AquaMai/Visual/Config.cs b/AquaMai/Visual/Config.cs
deleted file mode 100644
index 173419c9..00000000
--- a/AquaMai/Visual/Config.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-using AquaMai.Attributes;
-
-namespace AquaMai.Visual;
-
-public class Config
-{
- [ConfigComment(
- en: """
- Provide the ability to use custom skins (advanced feature)
- Load skin textures from LocalAssets\Skins
- """,
- zh: """
- 提供自定义皮肤的能力(高级功能)
- 从 LocalAssets\Skins 中加载皮肤贴图
- """)]
- public bool CustomSkins { get; set; }
-
- [ConfigComment(
- 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 bool JudgeDisplay4B { get; set; }
-
- [ConfigComment(
- 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 bool CustomTrackStartDiff { get; set; }
-
- [ConfigComment(
- en: """
- Delayed the animation of the song start screen
- For recording chart confirmation
- """,
- zh: """
- 推迟了歌曲开始界面的动画
- 录制谱面确认用
- """)]
- public bool TrackStartProcessTweak { get; set; }
-
- [ConfigComment(
- 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 bool DisableTrackStartTabs { get; set; }
-
- [ConfigComment(
- 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 bool FanJudgeFlip { get; set; }
-
- [ConfigComment(
- 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 bool BreakSlideJudgeBlink { get; set; }
-
- [ConfigComment(
- en: """
- Make the Slide Track disappear with an inward-shrinking animation, similar to AstroDX
- """,
- zh: """
- 使 Slide Track 消失时有类似 AstroDX 一样的向内缩入的动画
- """)]
- public bool SlideArrowAnimation { get; set; }
-
- [ConfigComment(
- 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 bool SlideLayerReverse { get; set; }
-}
diff --git a/AquaMai/Visual/FanJudgeFlip.cs b/AquaMai/Visual/FanJudgeFlip.cs
deleted file mode 100644
index 6defc766..00000000
--- a/AquaMai/Visual/FanJudgeFlip.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using HarmonyLib;
-using Monitor;
-
-namespace AquaMai.Visual;
-
-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);
- }
- }
- }
-}
diff --git a/AquaMai/WindowState/Config.cs b/AquaMai/WindowState/Config.cs
deleted file mode 100644
index 65fbf8ed..00000000
--- a/AquaMai/WindowState/Config.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using AquaMai.Attributes;
-
-namespace AquaMai.WindowState;
-
-public class Config
-{
- [ConfigComment(
- en: "If not enabled, no operations will be performed on the game window",
- zh: "不启用的话,不会对游戏窗口做任何操作")]
- public bool Enable { get; set; }
-
- [ConfigComment(
- en: "Window the game",
- zh: "窗口化游戏")]
- public bool Windowed { get; set; }
-
- [ConfigComment(
- en: """
- Width and height for windowed mode, rendering resolution for fullscreen mode
- If set to 0, windowed mode will remember the user-set size, fullscreen mode will use the current display resolution
- """,
- zh: """
- 宽度和高度窗口化时为游戏窗口大小,全屏时为渲染分辨率
- 如果设为 0,窗口化将记住用户设定的大小,全屏时将使用当前显示器分辨率
- """)]
- public int Width { get; set; }
-
- [ConfigComment(
- zh: "高度")]
- public int Height { get; set; }
-}
diff --git a/AquaMai/genConfig.csx b/AquaMai/genConfig.csx
deleted file mode 100644
index 2c53986b..00000000
--- a/AquaMai/genConfig.csx
+++ /dev/null
@@ -1,3 +0,0 @@
-#r "AquaMai.dll"
-
-AquaMai.ConfigGenerator.GenerateConfig();
diff --git a/config/application.properties b/config/application.properties
index 813c7eb0..94da66f8 100644
--- a/config/application.properties
+++ b/config/application.properties
@@ -29,7 +29,7 @@ allnet.server.redirect=https://aquadx.net
## Http Server Port
## Only change this if you have a reverse proxy running.
## The game rely on 80 port for boot up command
-server.port=80
+server.port=8080
## Static file server
## This is used to server static files in /web/ directory, which is Aquaviewer
@@ -87,15 +87,15 @@ spring.servlet.multipart.max-request-size=20MB
## Database Setting
########## For Sqlite ##########
-spring.datasource.driver-class-name=org.sqlite.JDBC
-spring.datasource.url=jdbc:sqlite:data/db.sqlite
+#spring.datasource.driver-class-name=org.sqlite.JDBC
+#spring.datasource.url=jdbc:sqlite:data/db.sqlite
########## For MariaDB ##########
-#spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
-#spring.datasource.username=
-#spring.datasource.password=
-#spring.datasource.url=jdbc:mariadb://localhost:3306/insert_db_name_here?allowPublicKeyRetrieval=true&useSSL=false
-#spring.datasource.hikari.maximum-pool-size=10
+spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
+spring.datasource.username=aqua
+spring.datasource.password=aqua
+spring.datasource.url=jdbc:mariadb://localhost:3306/aqua?allowPublicKeyRetrieval=true&useSSL=false
+spring.datasource.hikari.maximum-pool-size=10
################################
## AquaNet Settings