[RF] AquaMai configuration refactor (#82)

更新了配置文件格式,原有的配置文件将被自动无缝迁移,详情请见新的配置文件中的注释(例外:`SlideJudgeTweak` 不再默认启用)
旧配置文件将被重命名备份,如果更新到此版本遇到 Bug 请联系我们

Updated configuration file schema. The old config file will be migrated automatically and seamlessly. See the comments in the new configuration file for details. (Except for `SlideJudgeTweak` is no longer enabled by default)
Your old configuration file will be renamed as a backup. If you encounter any bug with this version, please contact us.
This commit is contained in:
Menci
2024-11-25 01:25:19 +08:00
committed by GitHub
parent e9ee31b22a
commit 37044dae01
217 changed files with 6051 additions and 3040 deletions

View File

@@ -0,0 +1,132 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{33C0D4ED-6A84-4659-9A05-12D43D75D0B3}</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>AquaMai.Core</RootNamespace>
<AssemblyName>AquaMai.Core</AssemblyName>
<TargetFramework>net472</TargetFramework>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<LangVersion>12</LangVersion>
<NoWarn>414</NoWarn>
<AssemblySearchPaths>$(ProjectDir)../Libs/;$(AssemblySearchPaths)</AssemblySearchPaths>
<OutputPath>$(ProjectDir)../Output/</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<EnableDefaultEmbeddedResourceItems>false</EnableDefaultEmbeddedResourceItems>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>false</DebugSymbols>
<DebugType>None</DebugType>
<Optimize>true</Optimize>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DefineConstants>DEBUG</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../AquaMai.Config/AquaMai.Config.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="mscorlib" />
<Reference Include="0Harmony" />
<Reference Include="AMDaemon.NET" />
<Reference Include="Assembly-CSharp" />
<Reference Include="Assembly-CSharp-firstpass" />
<Reference Include="MelonLoader" />
<Reference Include="Mono.Cecil" />
<Reference Include="Mono.Posix" />
<Reference Include="Mono.Security" />
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Security" />
<Reference Include="System.Xml" />
<Reference Include="Unity.Analytics.DataPrivacy" />
<Reference Include="Unity.TextMeshPro" />
<Reference Include="UnityEngine" />
<Reference Include="UnityEngine.AccessibilityModule" />
<Reference Include="UnityEngine.AIModule" />
<Reference Include="UnityEngine.AnimationModule" />
<Reference Include="UnityEngine.ARModule" />
<Reference Include="UnityEngine.AssetBundleModule" />
<Reference Include="UnityEngine.AudioModule" />
<Reference Include="UnityEngine.BaselibModule" />
<Reference Include="UnityEngine.ClothModule" />
<Reference Include="UnityEngine.ClusterInputModule" />
<Reference Include="UnityEngine.ClusterRendererModule" />
<Reference Include="UnityEngine.CoreModule" />
<Reference Include="UnityEngine.CrashReportingModule" />
<Reference Include="UnityEngine.DirectorModule" />
<Reference Include="UnityEngine.FileSystemHttpModule" />
<Reference Include="UnityEngine.GameCenterModule" />
<Reference Include="UnityEngine.GridModule" />
<Reference Include="UnityEngine.HotReloadModule" />
<Reference Include="UnityEngine.ImageConversionModule" />
<Reference Include="UnityEngine.IMGUIModule" />
<Reference Include="UnityEngine.InputModule" />
<Reference Include="UnityEngine.JSONSerializeModule" />
<Reference Include="UnityEngine.LocalizationModule" />
<Reference Include="UnityEngine.Networking" />
<Reference Include="UnityEngine.ParticleSystemModule" />
<Reference Include="UnityEngine.PerformanceReportingModule" />
<Reference Include="UnityEngine.Physics2DModule" />
<Reference Include="UnityEngine.PhysicsModule" />
<Reference Include="UnityEngine.ProfilerModule" />
<Reference Include="UnityEngine.ScreenCaptureModule" />
<Reference Include="UnityEngine.SharedInternalsModule" />
<Reference Include="UnityEngine.SpatialTracking" />
<Reference Include="UnityEngine.SpriteMaskModule" />
<Reference Include="UnityEngine.SpriteShapeModule" />
<Reference Include="UnityEngine.StreamingModule" />
<Reference Include="UnityEngine.StyleSheetsModule" />
<Reference Include="UnityEngine.SubstanceModule" />
<Reference Include="UnityEngine.TerrainModule" />
<Reference Include="UnityEngine.TerrainPhysicsModule" />
<Reference Include="UnityEngine.TextCoreModule" />
<Reference Include="UnityEngine.TextRenderingModule" />
<Reference Include="UnityEngine.TilemapModule" />
<Reference Include="UnityEngine.Timeline" />
<Reference Include="UnityEngine.TimelineModule" />
<Reference Include="UnityEngine.TLSModule" />
<Reference Include="UnityEngine.UI" />
<Reference Include="UnityEngine.UIElementsModule" />
<Reference Include="UnityEngine.UIModule" />
<Reference Include="UnityEngine.UmbraModule" />
<Reference Include="UnityEngine.UNETModule" />
<Reference Include="UnityEngine.UnityAnalyticsModule" />
<Reference Include="UnityEngine.UnityConnectModule" />
<Reference Include="UnityEngine.UnityTestProtocolModule" />
<Reference Include="UnityEngine.UnityWebRequestAssetBundleModule" />
<Reference Include="UnityEngine.UnityWebRequestAudioModule" />
<Reference Include="UnityEngine.UnityWebRequestModule" />
<Reference Include="UnityEngine.UnityWebRequestTextureModule" />
<Reference Include="UnityEngine.UnityWebRequestWWWModule" />
<Reference Include="UnityEngine.VehiclesModule" />
<Reference Include="UnityEngine.VFXModule" />
<Reference Include="UnityEngine.VideoModule" />
<Reference Include="UnityEngine.VRModule" />
<Reference Include="UnityEngine.WindModule" />
<Reference Include="UnityEngine.XRModule" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources/Locale.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Locale.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Include="Resources/Locale.zh.resx" WithCulture="false">
<DependentUpon>Locale.resx</DependentUpon>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,18 @@
using System;
namespace AquaMai.Core.Attributes;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class EnableGameVersionAttribute(uint minVersion = 0, uint maxVersion = 0, bool noWarn = false) : Attribute
{
public uint MinVersion { get; } = minVersion;
public uint MaxVersion { get; } = maxVersion;
public bool NoWarn { get; } = noWarn;
public bool ShouldEnable(uint gameVersion)
{
if (MinVersion > 0 && MinVersion > gameVersion) return false;
if (MaxVersion > 0 && MaxVersion < gameVersion) return false;
return true;
}
}

View File

@@ -0,0 +1,79 @@
using System;
namespace AquaMai.Core.Attributes;
public enum EnableConditionOperator
{
Equal,
NotEqual,
GreaterThan,
LessThan,
GreaterThanOrEqual,
LessThanOrEqual
}
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class EnableIfAttribute(
Type referenceType,
string referenceMember,
EnableConditionOperator @operator,
object rightSideValue) : Attribute
{
public Type ReferenceType { get; } = referenceType;
public string ReferenceMember { get; } = referenceMember;
public EnableConditionOperator Operator { get; } = @operator;
public object RightSideValue { get; } = rightSideValue;
// Referencing a field in another class and checking if it's true.
public EnableIfAttribute(Type referenceType, string referenceMember)
: this(referenceType, referenceMember, EnableConditionOperator.Equal, true)
{ }
// Referencing a field in the same class and comparing it with a value.
public EnableIfAttribute(string referenceMember, EnableConditionOperator condition, object value)
: this(null, referenceMember, condition, value)
{ }
// Referencing a field in the same class and checking if it's true.
public EnableIfAttribute(string referenceMember)
: this(referenceMember, EnableConditionOperator.Equal, true)
{ }
public bool ShouldEnable(Type selfType)
{
var referenceType = ReferenceType ?? selfType;
var referenceField = referenceType.GetField(
ReferenceMember,
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
var referenceProperty = referenceType.GetProperty(
ReferenceMember,
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
if (referenceField == null && referenceProperty == null)
{
throw new ArgumentException($"Field or property {ReferenceMember} not found in {referenceType.FullName}");
}
var referenceMemberValue = referenceField != null ? referenceField.GetValue(null) : referenceProperty.GetValue(null);
switch (Operator)
{
case EnableConditionOperator.Equal:
return referenceMemberValue.Equals(RightSideValue);
case EnableConditionOperator.NotEqual:
return !referenceMemberValue.Equals(RightSideValue);
case EnableConditionOperator.GreaterThan:
case EnableConditionOperator.LessThan:
case EnableConditionOperator.GreaterThanOrEqual:
case EnableConditionOperator.LessThanOrEqual:
var comparison = (IComparable)referenceMemberValue;
return Operator switch
{
EnableConditionOperator.GreaterThan => comparison.CompareTo(RightSideValue) > 0,
EnableConditionOperator.LessThan => comparison.CompareTo(RightSideValue) < 0,
EnableConditionOperator.GreaterThanOrEqual => comparison.CompareTo(RightSideValue) >= 0,
EnableConditionOperator.LessThanOrEqual => comparison.CompareTo(RightSideValue) <= 0,
_ => throw new NotImplementedException(),
};
default:
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,12 @@
using System;
namespace AquaMai.Core.Attributes;
// If the field or property with this name is true, the patch will be implicitly enabled, regardless of the config state.
// This is handled outside the config module, while The config state won't be actually set to enabled by it.
// Won't bypass the restriction of [EnableIf()] and [EnableGameVersion()].
[AttributeUsage(AttributeTargets.Class)]
public class EnableImplicitlyIf(string memberName) : Attribute
{
public string MemberName { get; } = memberName;
}

View File

@@ -0,0 +1,83 @@
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using MelonLoader;
using AquaMai.Config;
using AquaMai.Config.Interfaces;
using AquaMai.Config.Migration;
namespace AquaMai.Core;
public static class ConfigLoader
{
private static string ConfigFile => "AquaMai.toml";
private static string ConfigExampleFile(string lang) => $"AquaMai.{lang}.toml";
private static string OldConfigFile(string version) => $"AquaMai.toml.old-v{version}.";
private static Config.Config config;
public static Config.Config Config => config;
public static bool LoadConfig(Assembly modsAssembly)
{
Utility.LogFunction = MelonLogger.Msg;
config = new(
new Config.Reflection.ReflectionManager(
new Config.Reflection.SystemReflectionProvider(modsAssembly)));
if (!File.Exists(ConfigFile))
{
var examples = GenerateExamples();
foreach (var (lang, example) in examples)
{
var filename = ConfigExampleFile(lang);
File.WriteAllText(filename, example);
}
MelonLogger.Error("======================================!!!");
MelonLogger.Error("AquaMai.toml not found! Please create it.");
MelonLogger.Error("找不到配置文件 AquaMai.toml请创建。");
MelonLogger.Error("Example copied to AquaMai.en.toml");
MelonLogger.Error("示例已复制到 AquaMai.zh.toml");
MelonLogger.Error("=========================================");
return false;
}
var configText = File.ReadAllText(ConfigFile);
var configView = new ConfigView(configText);
var configVersion = ConfigMigrationManager.Instance.GetVersion(configView);
if (configVersion != ConfigMigrationManager.Instance.latestVersion)
{
File.WriteAllText(OldConfigFile(configVersion), configText);
configView = (ConfigView)ConfigMigrationManager.Instance.Migrate(configView);
}
// Read AquaMai.toml to load settings
ConfigParser.Instance.Parse(config, configView);
return true;
}
public static void SaveConfig(string lang)
{
File.WriteAllText(ConfigFile, SerailizeCurrentConfig(lang));
}
private static string SerailizeCurrentConfig(string lang) =>
new ConfigSerializer(new IConfigSerializer.Options()
{
Lang = lang,
IncludeBanner = true,
OverrideLocaleValue = true
}).Serialize(config);
private static IDictionary<string, string> GenerateExamples()
{
var examples = new Dictionary<string, string>();
foreach (var lang in (string[]) ["en", "zh"])
{
examples[lang] = SerailizeCurrentConfig(lang);
}
return examples;
}
}

View File

@@ -0,0 +1,72 @@
using System;
using System.Collections;
using System.Reflection;
using AquaMai.Core.Attributes;
using AquaMai.Core.Resources;
using HarmonyLib;
using MelonLoader;
namespace AquaMai.Core.Helpers;
public class EnableConditionHelper
{
[HarmonyPostfix]
[HarmonyPatch("HarmonyLib.PatchTools", "GetPatchMethod")]
public static void PostGetPatchMethod(ref MethodInfo __result)
{
if (__result != null)
{
if (ShouldSkipMethodOrClass(__result.GetCustomAttribute, __result.ReflectedType, __result.Name))
{
__result = null;
}
}
}
[HarmonyPostfix]
[HarmonyPatch("HarmonyLib.PatchTools", "GetPatchMethods")]
public static void PostGetPatchMethods(ref IList __result)
{
for (int i = 0; i < __result.Count; i++)
{
var harmonyMethod = Traverse.Create(__result[i]).Field("info").GetValue() as HarmonyMethod;
var method = harmonyMethod.method;
if (ShouldSkipMethodOrClass(method.GetCustomAttribute, method.ReflectedType, method.Name))
{
__result.RemoveAt(i);
i--;
}
}
}
public static bool ShouldSkipClass(Type type)
{
return ShouldSkipMethodOrClass(type.GetCustomAttribute, type);
}
private static bool ShouldSkipMethodOrClass(Func<Type, object> getCustomAttribute, Type type, string methodName = "")
{
var displayName = type.FullName + (string.IsNullOrEmpty(methodName) ? "" : $".{methodName}");
var enableIf = (EnableIfAttribute)getCustomAttribute(typeof(EnableIfAttribute));
if (enableIf != null && !enableIf.ShouldEnable(type))
{
# if DEBUG
MelonLogger.Msg($"Skipping {displayName} due to EnableIf condition");
# endif
return true;
}
var enableGameVersion = (EnableGameVersionAttribute)getCustomAttribute(typeof(EnableGameVersionAttribute));
if (enableGameVersion != null && !enableGameVersion.ShouldEnable(GameInfo.GameVersion))
{
# if DEBUG
MelonLogger.Msg($"Skipping {displayName} due to EnableGameVersion condition");
# endif
if (!enableGameVersion.NoWarn)
{
MelonLogger.Warning(string.Format(Locale.SkipIncompatiblePatch, type));
}
return true;
}
return false;
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.IO;
namespace AquaMai.Core.Helpers;
public static class FileSystem
{
public static string ResolvePath(string path)
{
var varExpanded = Environment.ExpandEnvironmentVariables(path);
return Path.IsPathRooted(varExpanded)
? varExpanded
: Path.Combine(Environment.CurrentDirectory, varExpanded);
}
}

View File

@@ -0,0 +1,21 @@
using System.Reflection;
using MAI2System;
namespace AquaMai.Core.Helpers;
public class GameInfo
{
public static uint GameVersion { get; } = GetGameVersion();
private static uint GetGameVersion()
{
return (uint)typeof(ConstParameter).GetField("NowGameVersion", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy).GetValue(null);
}
public static string GameId { get; } = GetGameId();
private static string GetGameId()
{
return typeof(ConstParameter).GetField("GameIDStr", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy).GetValue(null) as string;
}
}

View File

@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using UnityEngine;
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 => 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;
private static Color backgroundColor = new(147 / 256f, 160 / 256f, 173 / 256f, .8f);
public static void SetupStyles()
{
var buttonStyle = GUI.skin.button;
buttonStyle.normal.textColor = Color.white;
buttonStyle.normal.background = Texture2D.whiteTexture;
buttonStyle.hover.background = Texture2D.whiteTexture;
buttonStyle.active.background = Texture2D.whiteTexture;
buttonStyle.border = new RectOffset(0, 0, 0, 0);
buttonStyle.margin = new RectOffset(0, 0, 0, 0);
buttonStyle.padding = new RectOffset(10, 10, 10, 10);
buttonStyle.overflow = new RectOffset(0, 0, 0, 0);
var boxStyle = GUI.skin.box;
boxStyle.border = new RectOffset(0, 0, 0, 0);
boxStyle.normal.background = Texture2D.whiteTexture;
GUI.backgroundColor = backgroundColor;
}
[HarmonyPatch]
public class BoxBackground
{
public static IEnumerable<MethodBase> TargetMethods()
{
return typeof(GUI).GetMethods().Where(x => x.Name == "Box");
}
public static void Prefix()
{
GUI.backgroundColor = new Color(62 / 256f, 62 / 256f, 66 / 256f, .6f);
}
public static void Postfix()
{
GUI.backgroundColor = backgroundColor;
}
}
}

View File

@@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using AquaMai.Config.Types;
using HarmonyLib;
using Main;
using Manager;
using MelonLoader;
using UnityEngine;
namespace AquaMai.Core.Helpers;
public static class KeyListener
{
private static readonly Dictionary<KeyCodeOrName, int> _keyPressFrames = [];
private static readonly Dictionary<KeyCodeOrName, int> _keyPressFramesPrev = [];
static KeyListener()
{
foreach (KeyCodeOrName key in Enum.GetValues(typeof(KeyCodeOrName)))
{
_keyPressFrames[key] = 0;
_keyPressFramesPrev[key] = 0;
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(GameMainObject), "Update")]
public static void CheckLongPush()
{
foreach (KeyCodeOrName key in Enum.GetValues(typeof(KeyCodeOrName)))
{
_keyPressFramesPrev[key] = _keyPressFrames[key];
if (GetKeyPush(key))
{
# if DEBUG
MelonLogger.Msg($"CheckLongPush {key} is push {_keyPressFrames[key]}");
# endif
_keyPressFrames[key]++;
}
else
{
_keyPressFrames[key] = 0;
}
}
}
public static bool GetKeyPush(KeyCodeOrName key) =>
key switch
{
KeyCodeOrName.None => false,
< KeyCodeOrName.Select1P => Input.GetKey(key.GetKeyCode()),
KeyCodeOrName.Test => InputManager.GetSystemInputPush(InputManager.SystemButtonSetting.ButtonTest),
KeyCodeOrName.Service => InputManager.GetSystemInputPush(InputManager.SystemButtonSetting.ButtonService),
KeyCodeOrName.Select1P => InputManager.GetButtonPush(0, InputManager.ButtonSetting.Select),
KeyCodeOrName.Select2P => InputManager.GetButtonPush(1, InputManager.ButtonSetting.Select),
_ => throw new ArgumentOutOfRangeException(nameof(key), key, "我也不知道这是什么键")
};
public static bool GetKeyDown(KeyCodeOrName key)
{
// return key switch
// {
// KeyCodeOrName.None => false,
// < KeyCodeOrName.Select1P => Input.GetKeyDown(key.GetKeyCode()),
// KeyCodeOrName.Test => InputManager.GetSystemInputDown(InputManager.SystemButtonSetting.ButtonTest),
// KeyCodeOrName.Service => InputManager.GetSystemInputDown(InputManager.SystemButtonSetting.ButtonService),
// KeyCodeOrName.Select1P => InputManager.GetButtonDown(0, InputManager.ButtonSetting.Select),
// KeyCodeOrName.Select2P => InputManager.GetButtonDown(1, InputManager.ButtonSetting.Select),
// _ => throw new ArgumentOutOfRangeException(nameof(key), key, "我也不知道这是什么键")
// };
// 不用这个,我们检测按键是否弹起以及弹起之前按下的时间是否小于 30这样可以防止要长按时按下的时候就触发
return _keyPressFrames[key] == 0 && 0 < _keyPressFramesPrev[key] && _keyPressFramesPrev[key] < 30;
}
public static bool GetKeyDownOrLongPress(KeyCodeOrName key, bool isLongPress)
{
bool ret;
if (isLongPress)
{
ret = _keyPressFrames[key] == 60;
}
else
{
ret = GetKeyDown(key);
}
# if DEBUG
if (ret)
{
MelonLogger.Msg($"Key {key} is pressed, long press: {isLongPress}");
MelonLogger.Msg(new StackTrace());
}
# endif
return ret;
}
private static KeyCode GetKeyCode(this KeyCodeOrName keyCodeOrName) =>
keyCodeOrName switch
{
KeyCodeOrName.Alpha0 => KeyCode.Alpha0,
KeyCodeOrName.Alpha1 => KeyCode.Alpha1,
KeyCodeOrName.Alpha2 => KeyCode.Alpha2,
KeyCodeOrName.Alpha3 => KeyCode.Alpha3,
KeyCodeOrName.Alpha4 => KeyCode.Alpha4,
KeyCodeOrName.Alpha5 => KeyCode.Alpha5,
KeyCodeOrName.Alpha6 => KeyCode.Alpha6,
KeyCodeOrName.Alpha7 => KeyCode.Alpha7,
KeyCodeOrName.Alpha8 => KeyCode.Alpha8,
KeyCodeOrName.Alpha9 => KeyCode.Alpha9,
KeyCodeOrName.Keypad0 => KeyCode.Keypad0,
KeyCodeOrName.Keypad1 => KeyCode.Keypad1,
KeyCodeOrName.Keypad2 => KeyCode.Keypad2,
KeyCodeOrName.Keypad3 => KeyCode.Keypad3,
KeyCodeOrName.Keypad4 => KeyCode.Keypad4,
KeyCodeOrName.Keypad5 => KeyCode.Keypad5,
KeyCodeOrName.Keypad6 => KeyCode.Keypad6,
KeyCodeOrName.Keypad7 => KeyCode.Keypad7,
KeyCodeOrName.Keypad8 => KeyCode.Keypad8,
KeyCodeOrName.Keypad9 => KeyCode.Keypad9,
KeyCodeOrName.F1 => KeyCode.F1,
KeyCodeOrName.F2 => KeyCode.F2,
KeyCodeOrName.F3 => KeyCode.F3,
KeyCodeOrName.F4 => KeyCode.F4,
KeyCodeOrName.F5 => KeyCode.F5,
KeyCodeOrName.F6 => KeyCode.F6,
KeyCodeOrName.F7 => KeyCode.F7,
KeyCodeOrName.F8 => KeyCode.F8,
KeyCodeOrName.F9 => KeyCode.F9,
KeyCodeOrName.F10 => KeyCode.F10,
KeyCodeOrName.F11 => KeyCode.F11,
KeyCodeOrName.F12 => KeyCode.F12,
KeyCodeOrName.Insert => KeyCode.Insert,
KeyCodeOrName.Delete => KeyCode.Delete,
KeyCodeOrName.Home => KeyCode.Home,
KeyCodeOrName.End => KeyCode.End,
KeyCodeOrName.PageUp => KeyCode.PageUp,
KeyCodeOrName.PageDown => KeyCode.PageDown,
KeyCodeOrName.UpArrow => KeyCode.UpArrow,
KeyCodeOrName.DownArrow => KeyCode.DownArrow,
KeyCodeOrName.LeftArrow => KeyCode.LeftArrow,
KeyCodeOrName.RightArrow => KeyCode.RightArrow,
_ => throw new ArgumentOutOfRangeException(nameof(keyCodeOrName), keyCodeOrName, "游戏功能键需要单独处理")
};
}

View File

@@ -0,0 +1,39 @@
using DB;
using HarmonyLib;
using Manager;
using MelonLoader;
using Process;
namespace AquaMai.Core.Helpers;
public class MessageHelper
{
private static IGenericManager _genericManager = null;
[HarmonyPostfix]
[HarmonyPatch(typeof(ProcessManager), "SetMessageManager")]
private static void OnSetMessageManager(IGenericManager genericManager)
{
_genericManager = genericManager;
}
public static void ShowMessage(string message, WindowSizeID size = WindowSizeID.Middle, string title = null)
{
if (_genericManager is null)
{
MelonLogger.Error($"[MessageHelper] Unable to show message: `{message}` GenericManager is null");
return;
}
_genericManager.Enqueue(0, WindowMessageID.CollectionAttentionEmptyFavorite, new WindowParam()
{
hideTitle = title is null,
replaceTitle = true,
title = title,
replaceText = true,
text = message,
changeSize = true,
sizeID = size,
});
}
}

View File

@@ -0,0 +1,31 @@
using System.Collections.Generic;
using HarmonyLib;
namespace AquaMai.Core.Helpers;
public class MusicDirHelper
{
private static Dictionary<int, string> _map = new();
[HarmonyPostfix]
[HarmonyPatch(typeof(Manager.MaiStudio.Serialize.MusicData), "AddPath")]
private static void AddPath(Manager.MaiStudio.Serialize.MusicData __instance, string parentPath)
{
_map[__instance.GetID()] = parentPath;
}
public static string LookupPath(int id)
{
return _map.GetValueOrDefault(id);
}
public static string LookupPath(Manager.MaiStudio.Serialize.MusicData musicData)
{
return LookupPath(musicData.GetID());
}
public static string LookupPath(Manager.MaiStudio.MusicData musicData)
{
return LookupPath(musicData.GetID());
}
}

View File

@@ -0,0 +1,25 @@
using HarmonyLib;
using Main;
using Process;
namespace AquaMai.Core.Helpers;
public class SharedInstances
{
public static ProcessDataContainer ProcessDataContainer { get; private set; }
public static GameMainObject GameMainObject { get; private set; }
[HarmonyPrefix]
[HarmonyPatch(typeof(ProcessDataContainer), MethodType.Constructor)]
public static void OnCreateProcessDataContainer(ProcessDataContainer __instance)
{
ProcessDataContainer = __instance;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(GameMainObject), "Awake")]
public static void OnCreateGameMainObject(GameMainObject __instance)
{
GameMainObject = __instance;
}
}

View File

@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using MAI2.Util;
using Manager;
using Manager.UserDatas;
using Net.Packet;
using Net.Packet.Mai2;
namespace AquaMai.Core.Helpers;
public static class Shim
{
public delegate string GetAccessTokenMethod(int index);
public static readonly GetAccessTokenMethod GetAccessToken = new Func<GetAccessTokenMethod>(() => {
var tOperationManager = Traverse.Create(Singleton<OperationManager>.Instance);
var tGetAccessToken = tOperationManager.Method("GetAccessToken", [typeof(int)]);
if (!tGetAccessToken.MethodExists())
{
return (index) => throw new MissingMethodException("No matching OperationManager.GetAccessToken() method found");
}
return (index) => tGetAccessToken.GetValue<string>(index);
})();
public delegate PacketUploadUserPlaylog PacketUploadUserPlaylogCreator(int index, UserData src, int trackNo, Action<int> onDone, Action<PacketStatus> onError = null);
public static readonly PacketUploadUserPlaylogCreator CreatePacketUploadUserPlaylog = new Func<PacketUploadUserPlaylogCreator>(() => {
var type = typeof(PacketUploadUserPlaylog);
if (type.GetConstructor([typeof(int), typeof(UserData), typeof(int), typeof(Action<int>), typeof(Action<PacketStatus>)]) is ConstructorInfo ctor1) {
return (index, src, trackNo, onDone, onError) => {
var args = new object[] {index, src, trackNo, onDone, onError};
return (PacketUploadUserPlaylog)ctor1.Invoke(args);
};
}
else if (type.GetConstructor([typeof(int), typeof(UserData), typeof(int), typeof(string), typeof(Action<int>), typeof(Action<PacketStatus>)]) is ConstructorInfo ctor2) {
return (index, src, trackNo, onDone, onError) => {
var accessToken = GetAccessToken(index);
var args = new object[] {index, src, trackNo, accessToken, onDone, onError};
return (PacketUploadUserPlaylog)ctor2.Invoke(args);
};
}
else
{
throw new MissingMethodException("No matching PacketUploadUserPlaylog constructor found");
}
})();
public delegate PacketUpsertUserAll PacketUpsertUserAllCreator(int index, UserData src, Action<int> onDone, Action<PacketStatus> onError = null);
public static readonly PacketUpsertUserAllCreator CreatePacketUpsertUserAll = new Func<PacketUpsertUserAllCreator>(() => {
var type = typeof(PacketUpsertUserAll);
if (type.GetConstructor([typeof(int), typeof(UserData), typeof(Action<int>), typeof(Action<PacketStatus>)]) is ConstructorInfo ctor1) {
return (index, src, onDone, onError) => {
var args = new object[] {index, src, onDone, onError};
return (PacketUpsertUserAll)ctor1.Invoke(args);
};
}
else if (type.GetConstructor([typeof(int), typeof(UserData), typeof(string), typeof(Action<int>), typeof(Action<PacketStatus>)]) is ConstructorInfo ctor2) {
return (index, src, onDone, onError) => {
var accessToken = GetAccessToken(index);
var args = new object[] {index, src, accessToken, onDone, onError};
return (PacketUpsertUserAll)ctor2.Invoke(args);
};
}
else
{
throw new MissingMethodException("No matching PacketUpsertUserAll constructor found");
}
})();
public static IEnumerable<UserScore>[] GetUserScoreList(UserData userData)
{
var tUserData = Traverse.Create(userData);
var tScoreList = tUserData.Property("ScoreList");
if (tScoreList.PropertyExists())
{
return tScoreList.GetValue<List<UserScore>[]>();
}
var tScoreDic = tUserData.Property("ScoreDic");
if (tScoreDic.PropertyExists())
{
var scoreDic = tScoreDic.GetValue<Dictionary<int, UserScore>[]>();
return scoreDic.Select(dic => dic.Values).ToArray();
}
throw new MissingFieldException("No matching UserData.ScoreList/ScoreDic found");
}
}

View File

@@ -0,0 +1,32 @@
using System.Globalization;
using System.Resources;
using HarmonyLib;
namespace AquaMai.Core.Resources;
public class I18nSingleAssemblyHook
{
[HarmonyPatch(typeof(ResourceManager), "InternalGetResourceSet", typeof(CultureInfo), typeof(bool), typeof(bool))]
[HarmonyPrefix]
public static bool GetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents, ref ResourceSet __result, ResourceManager __instance)
{
var GetResourceFileName = __instance.GetType().GetMethod("GetResourceFileName", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var resourceFileName = (string)GetResourceFileName.Invoke(__instance, [culture]);
var ResourcesAssembly = typeof(I18nSingleAssemblyHook).Assembly;
var manifestResourceStream = ResourcesAssembly.GetManifestResourceStream(resourceFileName);
if (manifestResourceStream == null)
{
return true;
}
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, 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];
AddResourceSet.Invoke(null, args);
__result = (ResourceSet)args[2];
return false;
}
}

View File

@@ -0,0 +1,316 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace AquaMai.Core.Resources {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
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")]
public Locale() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AquaMai.Core.Resources.Locale", typeof(Locale).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to You are using AquaMai CI build version. This version is built from the latest mainline code and may contain undocumented configuration changes or potential issues..
/// </summary>
public static string CiBuildAlertContent {
get {
return ResourceManager.GetString("CiBuildAlertContent", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Important Notice: Test Version.
/// </summary>
public static string CiBuildAlertTitle {
get {
return ResourceManager.GetString("CiBuildAlertTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Loaded!.
/// </summary>
public static string Loaded {
get {
return ResourceManager.GetString("Loaded", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Errors detected while loading!
///- Are you using a modified Assembly-CSharp.dll, which will cause inconsistent functions and cannot find the functions that need to be modified
///- Check for conflicting mods, or enabled incompatible options.
/// </summary>
public static string LoadError {
get {
return ResourceManager.GetString("LoadError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to End.
/// </summary>
public static string MarkRepeatEnd {
get {
return ResourceManager.GetString("MarkRepeatEnd", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Start.
/// </summary>
public static string MarkRepeatStart {
get {
return ResourceManager.GetString("MarkRepeatStart", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Aime reader error.
/// </summary>
public static string NetErrIsAliveAimeReader {
get {
return ResourceManager.GetString("NetErrIsAliveAimeReader", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Aime server error.
/// </summary>
public static string NetErrIsAliveAimeServer {
get {
return ResourceManager.GetString("NetErrIsAliveAimeServer", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Server communication error.
/// </summary>
public static string NetErrIsAliveServer {
get {
return ResourceManager.GetString("NetErrIsAliveServer", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Data download not success.
/// </summary>
public static string NetErrWasDownloadSuccessOnce {
get {
return ResourceManager.GetString("NetErrWasDownloadSuccessOnce", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Pause.
/// </summary>
public static string Pause {
get {
return ResourceManager.GetString("Pause", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 游玩次数:{0}.
/// </summary>
public static string PlayCount {
get {
return ResourceManager.GetString("PlayCount", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to SSS+ =&gt; DXRating += {0}.
/// </summary>
public static string RatingUpWhenSSSp {
get {
return ResourceManager.GetString("RatingUpWhenSSSp", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Repeat end time cannot be less than repeat start time.
/// </summary>
public static string RepeatEndTimeLessThenStartTime {
get {
return ResourceManager.GetString("RepeatEndTimeLessThenStartTime", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Loop Not Set.
/// </summary>
public static string RepeatNotSet {
get {
return ResourceManager.GetString("RepeatNotSet", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Reset.
/// </summary>
public static string RepeatReset {
get {
return ResourceManager.GetString("RepeatReset", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Loop Set.
/// </summary>
public static string RepeatStartEndSet {
get {
return ResourceManager.GetString("RepeatStartEndSet", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Loop Start Set.
/// </summary>
public static string RepeatStartSet {
get {
return ResourceManager.GetString("RepeatStartSet", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please set repeat start time first.
/// </summary>
public static string RepeatStartTimeNotSet {
get {
return ResourceManager.GetString("RepeatStartTimeNotSet", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Saving... Do not exit the game.
/// </summary>
public static string SavingDontExit {
get {
return ResourceManager.GetString("SavingDontExit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Seek &lt;&lt;.
/// </summary>
public static string SeekBackward {
get {
return ResourceManager.GetString("SeekBackward", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Seek &gt;&gt;.
/// </summary>
public static string SeekForward {
get {
return ResourceManager.GetString("SeekForward", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Skip.
/// </summary>
public static string Skip {
get {
return ResourceManager.GetString("Skip", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to &gt; Skipping incompatible patch: {0}.
/// </summary>
public static string SkipIncompatiblePatch {
get {
return ResourceManager.GetString("SkipIncompatiblePatch", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Speed.
/// </summary>
public static string Speed {
get {
return ResourceManager.GetString("Speed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Speed -.
/// </summary>
public static string SpeedDown {
get {
return ResourceManager.GetString("SpeedDown", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Speed Reset.
/// </summary>
public static string SpeedReset {
get {
return ResourceManager.GetString("SpeedReset", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Speed +.
/// </summary>
public static string SpeedUp {
get {
return ResourceManager.GetString("SpeedUp", resourceCulture);
}
}
}
}

View File

@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="SeekBackward" xml:space="preserve">
<value>Seek &lt;&lt;</value>
</data>
<data name="SeekForward" xml:space="preserve">
<value>Seek &gt;&gt;</value>
</data>
<data name="Pause" xml:space="preserve">
<value>Pause</value>
</data>
<data name="MarkRepeatStart" xml:space="preserve">
<value>Start</value>
</data>
<data name="MarkRepeatEnd" xml:space="preserve">
<value>End</value>
</data>
<data name="RepeatReset" xml:space="preserve">
<value>Reset</value>
</data>
<data name="RepeatNotSet" xml:space="preserve">
<value>Loop Not Set</value>
</data>
<data name="RepeatStartSet" xml:space="preserve">
<value>Loop Start Set</value>
</data>
<data name="RepeatStartEndSet" xml:space="preserve">
<value>Loop Set</value>
</data>
<data name="SpeedDown" xml:space="preserve">
<value>Speed -</value>
</data>
<data name="SpeedUp" xml:space="preserve">
<value>Speed +</value>
</data>
<data name="Speed" xml:space="preserve">
<value>Speed</value>
</data>
<data name="SpeedReset" xml:space="preserve">
<value>Speed Reset</value>
</data>
<data name="LoadError" xml:space="preserve">
<value>Errors detected while loading!
- 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</value>
</data>
<data name="SavingDontExit" xml:space="preserve">
<value>Saving... Do not exit the game</value>
</data>
<data name="Loaded" xml:space="preserve">
<value>Loaded!</value>
</data>
<data name="NetErrIsAliveServer" xml:space="preserve">
<value>Server communication error</value>
</data>
<data name="NetErrIsAliveAimeReader" xml:space="preserve">
<value>Aime reader error</value>
</data>
<data name="NetErrIsAliveAimeServer" xml:space="preserve">
<value>Aime server error</value>
</data>
<data name="NetErrWasDownloadSuccessOnce" xml:space="preserve">
<value>Data download not success</value>
</data>
<data name="RatingUpWhenSSSp" xml:space="preserve">
<value>SSS+ =&gt; DXRating += {0}</value>
</data>
<data name="Skip" xml:space="preserve">
<value>Skip</value>
</data>
<data name="SkipIncompatiblePatch" xml:space="preserve">
<value>&gt; Skipping incompatible patch: {0}</value>
</data>
<data name="RepeatStartTimeNotSet" xml:space="preserve">
<value>Please set repeat start time first</value>
</data>
<data name="RepeatEndTimeLessThenStartTime" xml:space="preserve">
<value>Repeat end time cannot be less than repeat start time</value>
</data>
<data name="CiBuildAlertTitle" xml:space="preserve">
<value>Important Notice: Test Version</value>
</data>
<data name="CiBuildAlertContent" xml:space="preserve">
<value>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.</value>
</data>
<data name="PlayCount" xml:space="preserve">
<value>游玩次数:{0}</value>
</data>
</root>

View File

@@ -0,0 +1,100 @@
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="SeekBackward" xml:space="preserve">
<value>倒退 &lt;&lt;</value>
</data>
<data name="SeekForward" xml:space="preserve">
<value>快进 &gt;&gt;</value>
</data>
<data name="Pause" xml:space="preserve">
<value>暂停</value>
</data>
<data name="MarkRepeatEnd" xml:space="preserve">
<value>标记结尾</value>
</data>
<data name="MarkRepeatStart" xml:space="preserve">
<value>标记开头</value>
</data>
<data name="RepeatNotSet" xml:space="preserve">
<value>循环未设定</value>
</data>
<data name="RepeatReset" xml:space="preserve">
<value>循环解除</value>
</data>
<data name="RepeatStartEndSet" xml:space="preserve">
<value>循环已设定</value>
</data>
<data name="RepeatStartSet" xml:space="preserve">
<value>循环开头已设定</value>
</data>
<data name="Speed" xml:space="preserve">
<value>速度</value>
</data>
<data name="SpeedDown" xml:space="preserve">
<value>速度 -</value>
</data>
<data name="SpeedReset" xml:space="preserve">
<value>速度重置</value>
</data>
<data name="SpeedUp" xml:space="preserve">
<value>速度 +</value>
</data>
<data name="LoadError" xml:space="preserve">
<value>加载过程中检测到错误!
- 你是否正在使用魔改的 Assembly-CSharp.dll这会导致函数不一致而无法找到需要修改的函数
- 请检查是否有冲突的 Mod或者开启了不兼容的选项</value>
</data>
<data name="SavingDontExit" xml:space="preserve">
<value>正在保存… 请不要关闭游戏</value>
</data>
<data name="Loaded" xml:space="preserve">
<value>加载完成!</value>
</data>
<data name="NetErrIsAliveServer" xml:space="preserve">
<value>主服务器通信错误</value>
</data>
<data name="NetErrIsAliveAimeReader" xml:space="preserve">
<value>Aime 读卡器错误</value>
</data>
<data name="NetErrIsAliveAimeServer" xml:space="preserve">
<value>AimeDB 通信错误</value>
</data>
<data name="NetErrWasDownloadSuccessOnce" xml:space="preserve">
<value>数据下载不成功</value>
</data>
<data name="RatingUpWhenSSSp" xml:space="preserve">
<value>推到鸟加可上 {0} 分</value>
</data>
<data name="Skip" xml:space="preserve">
<value>跳过</value>
</data>
<data name="SkipIncompatiblePatch" xml:space="preserve">
<value>&gt; 已跳过加载不兼容的功能: {0}</value>
</data>
<data name="RepeatEndTimeLessThenStartTime" xml:space="preserve">
<value>循环结束时间不能早于开始时间</value>
</data>
<data name="RepeatStartTimeNotSet" xml:space="preserve">
<value>请先设置循环开始时间</value>
</data>
<data name="CiBuildAlertTitle" xml:space="preserve">
<value>重要提示:测试版本</value>
</data>
<data name="CiBuildAlertContent" xml:space="preserve">
<value>您正在使用的是 AquaMai CI 构建版本。由于该版本基于最新的主线代码构建,可能包含未通知的配置文件变更或潜在问题。</value>
</data>
<data name="PlayCount" xml:space="preserve">
<value>Play Count: {0}</value>
</data>
</root>

View File

@@ -0,0 +1,199 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using AquaMai.Core.Attributes;
using AquaMai.Core.Helpers;
using AquaMai.Core.Resources;
using MelonLoader;
using UnityEngine;
namespace AquaMai.Core;
public class Startup
{
private static HarmonyLib.Harmony _harmony;
private static bool _hasErrors;
private enum ModLifecycleMethod
{
// Invoked before all patches are applied, including core patches
OnBeforeAllPatch,
// Invoked after all patches are applied
OnAfterAllPatch,
// Invoked before the current patch is applied
OnBeforePatch,
// Invoked after the current patch is applied
// Subclasses are treated as separate patches
OnAfterPatch,
// Invoked when an error occurs applying the current patch
// Lifecycle methods' excpetions not included
// Subclasses' error not included
OnPatchError
}
private static bool ShouldEnableImplicitly(Type type)
{
var implicitEnableAttribute = type.GetCustomAttribute<EnableImplicitlyIf>();
if (implicitEnableAttribute == null) return false;
var referenceField = type.GetField(implicitEnableAttribute.MemberName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
var referenceProperty = type.GetProperty(implicitEnableAttribute.MemberName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
if (referenceField == null && referenceProperty == null)
{
throw new ArgumentException($"Field or property {implicitEnableAttribute.MemberName} not found in {type.FullName}");
}
var referenceMemberValue = referenceField != null ? referenceField.GetValue(null) : referenceProperty.GetValue(null);
if ((bool)referenceMemberValue)
{
MelonLogger.Msg($"Enabled {type.FullName} implicitly");
return true;
}
return false;
}
private static void InvokeLifecycleMethod(Type type, ModLifecycleMethod methodName)
{
var method = type.GetMethod(methodName.ToString(), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
if (method == null)
{
return;
}
var parameters = method.GetParameters();
var arguments = parameters.Select(p =>
{
if (p.ParameterType == typeof(HarmonyLib.Harmony)) return _harmony;
throw new InvalidOperationException($"Unsupported parameter type {p.ParameterType} in lifecycle method {type.FullName}.{methodName}");
}).ToArray();
try
{
method.Invoke(null, arguments);
}
catch (TargetInvocationException e)
{
MelonLogger.Error($"Failed to invoke lifecycle method {type.FullName}.{methodName}: {e.InnerException}");
_hasErrors = true;
}
}
private static void CollectWantedPatches(List<Type> wantedPatches, Type type)
{
if (EnableConditionHelper.ShouldSkipClass(type))
{
return;
}
wantedPatches.Add(type);
foreach (var nested in type.GetNestedTypes())
{
CollectWantedPatches(wantedPatches, nested);
}
}
private static void ApplyPatch(Type type)
{
MelonLogger.Msg($"> Applying {type}");
try
{
InvokeLifecycleMethod(type, ModLifecycleMethod.OnBeforePatch);
_harmony.PatchAll(type);
InvokeLifecycleMethod(type, ModLifecycleMethod.OnAfterPatch);
}
catch (Exception e)
{
MelonLogger.Error($"Failed to patch {type}: {e}");
InvokeLifecycleMethod(type, ModLifecycleMethod.OnPatchError);
_hasErrors = true;
}
}
private static string ResolveLocale()
{
var localeConfigEntry = ConfigLoader.Config.ReflectionManager.GetEntry("General.Locale");
var localeValue = (string)ConfigLoader.Config.GetEntryState(localeConfigEntry).Value;
return localeValue switch
{
"en" => localeValue,
"zh" => localeValue,
_ => Application.systemLanguage switch
{
SystemLanguage.Chinese or SystemLanguage.ChineseSimplified or SystemLanguage.ChineseTraditional => "zh",
SystemLanguage.English => "en",
_ => "en"
}
};
}
public static void Initialize(Assembly modsAssembly, HarmonyLib.Harmony harmony)
{
MelonLogger.Msg("Loading mod settings...");
var configLoaded = ConfigLoader.LoadConfig(modsAssembly);
var lang = ResolveLocale();
if (configLoaded)
{
ConfigLoader.SaveConfig(lang); // Re-save the config as soon as possible
}
_harmony = harmony;
// Init locale with patching C# runtime
// https://stackoverflow.com/questions/1952638/single-assembly-multi-language-windows-forms-deployment-ilmerge-and-satellite-a
ApplyPatch(typeof(I18nSingleAssemblyHook));
Locale.Culture = CultureInfo.GetCultureInfo(lang); // Must be called after I18nSingleAssemblyHook patched
// The patch list is ordered
List<Type> wantedPatches = [];
// Must be patched first to support [EnableIf(...)] and [EnableGameVersion(...)]
CollectWantedPatches(wantedPatches, typeof(EnableConditionHelper));
// Core helpers patched first
CollectWantedPatches(wantedPatches, typeof(MessageHelper));
CollectWantedPatches(wantedPatches, typeof(MusicDirHelper));
CollectWantedPatches(wantedPatches, typeof(SharedInstances));
CollectWantedPatches(wantedPatches, typeof(GuiSizes));
CollectWantedPatches(wantedPatches, typeof(KeyListener));
// Collect patches based on the config
var config = ConfigLoader.Config;
foreach (var section in config.ReflectionManager.Sections)
{
var reflectionType = (Config.Reflection.SystemReflectionProvider.ReflectionType)section.Type;
var type = reflectionType.UnderlyingType;
if (!config.GetSectionState(section).Enabled && !ShouldEnableImplicitly(type)) continue;
CollectWantedPatches(wantedPatches, type);
}
foreach (var type in wantedPatches)
{
InvokeLifecycleMethod(type, ModLifecycleMethod.OnBeforeAllPatch);
}
foreach (var type in wantedPatches)
{
ApplyPatch(type);
}
foreach (var type in wantedPatches)
{
InvokeLifecycleMethod(type, ModLifecycleMethod.OnAfterAllPatch);
}
if (_hasErrors)
{
MelonLogger.Warning("========================================================================!!!\n" + Locale.LoadError);
MelonLogger.Warning("===========================================================================");
}
# if CI
MelonLogger.Warning(Locale.CiBuildAlertTitle);
MelonLogger.Warning(Locale.CiBuildAlertContent);
# endif
MelonLogger.Msg(Locale.Loaded);
}
public static void OnGUI()
{
GuiSizes.SetupStyles();
}
}