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