diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json deleted file mode 100644 index b6554f30..00000000 --- a/.config/dotnet-tools.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "cake.tool": { - "version": "5.0.0", - "commands": [ - "dotnet-cake" - ], - "rollForward": false - } - } -} \ No newline at end of file diff --git a/AquaMai.Config/ConfigSerializer.cs b/AquaMai.Config/ConfigSerializer.cs index 144f1469..21fa9832 100644 --- a/AquaMai.Config/ConfigSerializer.cs +++ b/AquaMai.Config/ConfigSerializer.cs @@ -26,7 +26,7 @@ public class ConfigSerializer(IConfigSerializer.Options Options) : IConfigSerial - 该文件的格式和文字注释是固定的,配置文件将在启动时被重写,无法解析的内容将被删除 试试使用 MaiChartManager 图形化配置 AquaMai 吧! - https://github.com/clansty/MaiChartManager + https://github.com/MuNET-OSS/MaiChartManager """; private const string BANNER_EN = @@ -43,6 +43,9 @@ This is the TOML configuration file of AquaMai. - 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. + + Try using MaiChartManager to configure AquaMai graphically! + https://github.com/MuNET-OSS/MaiChartManager """; private readonly IConfigSerializer.Options Options = Options; diff --git a/AquaMai.Config/Migration/ConfigMigrationManager.cs b/AquaMai.Config/Migration/ConfigMigrationManager.cs index 3d5480a8..95a069e1 100644 --- a/AquaMai.Config/Migration/ConfigMigrationManager.cs +++ b/AquaMai.Config/Migration/ConfigMigrationManager.cs @@ -16,6 +16,7 @@ public class ConfigMigrationManager : IConfigMigrationManager new ConfigMigration_V2_0_V2_1(), new ConfigMigration_V2_1_V2_2(), new ConfigMigration_V2_2_V2_3(), + new ConfigMigration_V2_3_V2_4(), }.ToDictionary(m => m.FromVersion); public string LatestVersion { get; } diff --git a/AquaMai.Config/Migration/ConfigMigration_V2_3_V2_4.cs b/AquaMai.Config/Migration/ConfigMigration_V2_3_V2_4.cs new file mode 100644 index 00000000..5aa3693f --- /dev/null +++ b/AquaMai.Config/Migration/ConfigMigration_V2_3_V2_4.cs @@ -0,0 +1,73 @@ +using AquaMai.Config.Interfaces; +using Tomlet.Models; + +namespace AquaMai.Config.Migration; + +public class ConfigMigration_V2_3_V2_4 : IConfigMigration +{ + public string FromVersion => "2.3"; + public string ToVersion => "2.4"; + + public ConfigView Migrate(ConfigView src) + { + var dst = (ConfigView)src.Clone(); + dst.SetValue("Version", ToVersion); + + if (src.TryGetValue("GameSystem.KeyMap.DisableIO4", out var disableIO4)) + { + dst.SetValue("GameSystem.KeyMap.DisableIO4_1P", disableIO4); + dst.SetValue("GameSystem.KeyMap.DisableIO4_2P", disableIO4); + dst.SetValue("GameSystem.KeyMap.DisableIO4System", disableIO4); + dst.Remove("GameSystem.KeyMap.DisableIO4"); + } + + if (src.IsSectionEnabled("GameSystem.SkipBoardNoCheck")) + { + dst.EnsureDictionary("GameSystem.OldCabLightBoardSupport"); + dst.Remove("GameSystem.SkipBoardNoCheck"); + } + + if (src.TryGetValue("GameSystem.MaimollerIO.P1", out var mml1p)) + { + dst.SetValue("GameSystem.MaimollerIO.Touch1p", mml1p); + dst.SetValue("GameSystem.MaimollerIO.Button1p", mml1p); + dst.SetValue("GameSystem.MaimollerIO.Led1p", mml1p); + dst.Remove("GameSystem.MaimollerIO.P1"); + } + + if (src.TryGetValue("GameSystem.MaimollerIO.P2", out var mml2p)) + { + dst.SetValue("GameSystem.MaimollerIO.Touch2p", mml2p); + dst.SetValue("GameSystem.MaimollerIO.Button2p", mml2p); + dst.SetValue("GameSystem.MaimollerIO.Led2p", mml2p); + dst.Remove("GameSystem.MaimollerIO.P2"); + } + + if (src.TryGetValue("GameSystem.AdxHidInput.Io4Compact", out var adxDisableButtons)) + { + dst.SetValue("GameSystem.AdxHidInput.DisableButtons", adxDisableButtons); + dst.Remove("GameSystem.AdxHidInput.Io4Compact"); + } + + if (src.IsSectionEnabled("GameSystem.UnstableRate")) + { + dst.EnsureDictionary("Utils.UnstableRate"); + dst.Remove("GameSystem.UnstableRate"); + } + + if (src.IsSectionEnabled("Fancy.CustomSkinsPlusStatic")) + { + dst.EnsureDictionary("Fancy.ResourcesOverride"); + dst.Remove("Fancy.CustomSkinsPlusStatic"); + } + + if (src.IsSectionEnabled("Fancy.RsOverride")) + { + dst.EnsureDictionary("Fancy.ResourcesOverride"); + dst.Remove("Fancy.RsOverride"); + } + + return dst; + } +} + diff --git a/AquaMai.Mods/Fancy/RsOverride.cs b/AquaMai.Mods/Fancy/ResourcesOverride.cs similarity index 99% rename from AquaMai.Mods/Fancy/RsOverride.cs rename to AquaMai.Mods/Fancy/ResourcesOverride.cs index 9d57ab34..b4d13939 100644 --- a/AquaMai.Mods/Fancy/RsOverride.cs +++ b/AquaMai.Mods/Fancy/ResourcesOverride.cs @@ -14,7 +14,7 @@ namespace AquaMai.Mods.Fancy; name: "力大砖飞", en: "[Dangerous] Full-scene texture / sprite replacement. Static injection.", zh: "【危险功能】适用于便捷魔改的自定义全场景图片。警告:可能对游戏造成未知性能影响,可能与其他模组冲突?")] -public class CustomSkinsPlusStatic +public class ResourcesOverride { [ConfigEntry(name: "资源目录")] private static string skinsDir = "LocalAssets/ResourcesOverride"; diff --git a/AquaMai.Mods/GameSystem/AdxHidInput.cs b/AquaMai.Mods/GameSystem/AdxHidInput.cs index f7b2e3ec..92331761 100644 --- a/AquaMai.Mods/GameSystem/AdxHidInput.cs +++ b/AquaMai.Mods/GameSystem/AdxHidInput.cs @@ -117,7 +117,7 @@ public static void OnBeforeEnableCheck() if (adxController[i] == null) continue; TdInit(i); if (adxController[i].Attributes.ProductId is 0x5767 or 0x5768) continue; - if (io4Compact) continue; + if (disableButtons) continue; keyEnabled = true; var p = i; Thread hidThread = new Thread(() => HidInputThread(p)); @@ -168,8 +168,8 @@ private static bool IsButtonPushed(int playerNo, int buttonIndex1To8) [ConfigEntry(name: "按钮 4(最下方的圆形按键)")] private static readonly IOKeyMap button4 = IOKeyMap.Test; - [ConfigEntry("IO4 兼容模式", zh: "如果你不知道这是什么,请勿开启", hideWhenDefault: true)] - private static readonly bool io4Compact = false; + [ConfigEntry("禁用外键输入")] + private static readonly bool disableButtons = false; private static AuxiliaryState GetAuxiliaryState() { diff --git a/AquaMai.Mods/GameSystem/KeyMap.cs b/AquaMai.Mods/GameSystem/KeyMap.cs index a501c304..c0808696 100644 --- a/AquaMai.Mods/GameSystem/KeyMap.cs +++ b/AquaMai.Mods/GameSystem/KeyMap.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Reflection; using AMDaemon; using AquaMai.Config.Attributes; @@ -24,7 +25,7 @@ DebugInput works independently of IO4 (IO4-compatible board / segatools IO4 emul public class KeyMap { [ConfigEntry( - name: "禁用 IO4", + name: "禁用 IO4(1P)", en: """ Disable IO4 (IO4-compatible board / segatools IO4 emulation) input. With IO4 input disabled, your IO4-compatible board or segatools IO4 emulation is ignored. @@ -33,7 +34,19 @@ Disable IO4 (IO4-compatible board / segatools IO4 emulation) input. 禁用 IO4(兼容 IO4 板 / segatools IO4 模拟)输入。 在禁用 IO4 输入后,你的兼容 IO4 板或 segatools IO4 模拟将被忽略。 """)] - private static readonly bool disableIO4 = false; + private static readonly bool disableIO4_1P = false; + [ConfigEntry("禁用 IO4(2P)")] + private static readonly bool disableIO4_2P = false; + + [ConfigEntry( + name: "禁用 IO4(系统按键)", + en: """ + System buttons (test, service) input. + """, + zh: """ + 禁用系统按键的 IO4 输入,输入源同上。 + """)] + private static readonly bool disableIO4System = false; [ConfigEntry( name: "禁用 DebugInput", @@ -59,30 +72,45 @@ You may want to configure IO4 emulation key mapping in segatools.ini's [io4] and """)] public static readonly bool disableDebugFeatureHotkeys = false; // Implemented in DebugFeature - [EnableIf(nameof(disableIO4))] + private static bool DisableIO4 => disableIO4_1P || disableIO4_2P || disableIO4System; + private static HashSet disabledSwitchInputs = []; + + [EnableIf(nameof(DisableIO4))] [HarmonyPatch("IO.Jvs+JvsSwitch", ".ctor", MethodType.Constructor, [typeof(int), typeof(string), typeof(KeyCode), typeof(bool), typeof(bool)])] - [HarmonyPrefix] - public static void PreJvsSwitchConstructor(ref bool invert) + [HarmonyPostfix] + public static void PostJvsSwitchConstructor(ref bool ____invert, int playerNo, bool systemButton, SwitchInput ____switchInput) { - invert = false; + if ((systemButton && disableIO4System) || (playerNo == 0 && disableIO4_1P) || (playerNo == 1 && disableIO4_2P)) + { + ____invert = false; + disabledSwitchInputs.Add(____switchInput); + } } - [EnableIf(nameof(disableIO4))] - [HarmonyPatch(typeof(SwitchInput), "get_IsOn")] + [EnableIf(nameof(DisableIO4))] + [HarmonyPatch(typeof(SwitchInput), nameof(SwitchInput.IsOn), MethodType.Getter)] [HarmonyPrefix] - public static bool PreGetIsOn(ref bool __result) + public static bool PreGetIsOn(ref bool __result, SwitchInput __instance) { - __result = false; - return false; + if (disabledSwitchInputs.Contains(__instance)) + { + __result = false; + return false; + } + return true; } - [EnableIf(nameof(disableIO4))] - [HarmonyPatch(typeof(SwitchInput), "get_HasOnNow")] + [EnableIf(nameof(DisableIO4))] + [HarmonyPatch(typeof(SwitchInput), nameof(SwitchInput.HasOnNow), MethodType.Getter)] [HarmonyPrefix] - public static bool PreGetHasOnNow(ref bool __result) + public static bool PreGetHasOnNow(ref bool __result, SwitchInput __instance) { - __result = false; - return false; + if (disabledSwitchInputs.Contains(__instance)) + { + __result = false; + return false; + } + return true; } [ConfigEntry] diff --git a/AquaMai.Mods/GameSystem/MaimollerIO/MaimollerIO.cs b/AquaMai.Mods/GameSystem/MaimollerIO/MaimollerIO.cs index 548f3289..1ce28d06 100644 --- a/AquaMai.Mods/GameSystem/MaimollerIO/MaimollerIO.cs +++ b/AquaMai.Mods/GameSystem/MaimollerIO/MaimollerIO.cs @@ -3,14 +3,12 @@ using System.Reflection; using AquaMai.Config.Attributes; using AquaMai.Config.Types; +using AquaMai.Core.Attributes; using AquaMai.Core.Helpers; using AquaMai.Mods.GameSystem.MaimollerIO.Libs; using HarmonyLib; -using IO; using Main; -using Manager; using Mecha; -using MelonLoader; using UnityEngine; namespace AquaMai.Mods.GameSystem.MaimollerIO; @@ -33,16 +31,65 @@ Please remove ADXHIDIOMod.dll (if any) and disable DummyTouchpanel in mai2.ini. public class MaimollerIO { [ConfigEntry( - name: "启用 1P", - en: "Enable 1P (If you mix Maimoller with other protocols, please disable for the side that is not Maimoller)", - zh: "启用 1P(如果混用 Maimoller 与其他协议,请对不是 Maimoller 的一侧禁用)")] - private static readonly bool p1 = true; + name: "启用 1P 触屏", + en: "Enable 1P TouchScreen (If you mix Maimoller with other protocols, please disable for the side that is not Maimoller)", + zh: "如果混用 Maimoller 与其他协议,请对不是 Maimoller 的一侧禁用")] + private static readonly bool touch1p = true; [ConfigEntry( - name: "启用 2P", - en: "Enable 2P (If you mix Maimoller with other protocols, please disable for the side that is not Maimoller)", - zh: "启用 2P(如果混用 Maimoller 与其他协议,请对不是 Maimoller 的一侧禁用)")] - private static readonly bool p2 = true; + name: "启用 1P 按键", + en: "Enable 1P Buttons")] + private static readonly bool button1p = true; + + [ConfigEntry( + name: "启用 1P 灯光", + en: "Enable 1P LEDs")] + private static readonly bool led1p = true; + + [ConfigEntry( + name: "启用 2P 触屏", + en: "Enable 2P")] + private static readonly bool touch2p = true; + + [ConfigEntry( + name: "启用 2P 按键", + en: "Enable 2P Buttons")] + private static readonly bool button2p = true; + + [ConfigEntry( + name: "启用 2P 灯光", + en: "Enable 2P LEDs")] + private static readonly bool led2p = true; + + private static bool ShouldInitForPlayer(int playerNo) => playerNo switch + { + 0 => touch1p || button1p || led1p, + 1 => touch2p || button2p || led2p, + _ => false, + }; + + private static bool IsTouchEnabledForPlayer(int playerNo) => playerNo switch + { + 0 => touch1p, + 1 => touch2p, + _ => false, + }; + + private static bool IsButtonEnabledForPlayer(int playerNo) => playerNo switch + { + 0 => button1p, + 1 => button2p, + _ => false, + }; + + private static bool IsLedEnabledForPlayer(int playerNo) => playerNo switch + { + 0 => led1p, + 1 => led2p, + _ => false, + }; + + private static bool IsAnyLedEnabled => led1p || led2p; [ConfigEntry(name: "按钮 1(三角形)")] private static readonly IOKeyMap button1 = IOKeyMap.Select1P; @@ -64,77 +111,83 @@ public class MaimollerIO /* button4 */ MaimollerInputReport.ButtonMask.COIN, ]; - private static bool ShouldEnableForPlayer(int playerNo) => playerNo switch - { - 0 => p1, - 1 => p2, - _ => false, - }; - private static readonly MaimollerDevice[] _devices = [.. Enumerable.Range(0, 2).Select(i => new MaimollerDevice(i))]; private static readonly MaimollerLedManager[] _ledManagers = [.. Enumerable.Range(0, 2).Select(i => new MaimollerLedManager(_devices[i].output))]; public static void OnBeforePatch() { - if (p1) + for (int i = 0; i < 2; i++) { - _devices[0].Open(); - TouchStatusProvider.RegisterTouchStatusProvider(0, GetTouchState); + if (!ShouldInitForPlayer(i)) continue; + _devices[i].Open(); + + if (IsTouchEnabledForPlayer(i)) + { + TouchStatusProvider.RegisterTouchStatusProvider(i, GetTouchState); + } } - if (p2) + + if (button1p || button2p) { - _devices[1].Open(); - TouchStatusProvider.RegisterTouchStatusProvider(1, GetTouchState); + JvsSwitchHook.RegisterButtonChecker(IsButtonPushed); + JvsSwitchHook.RegisterAuxiliaryStateProvider(GetAuxiliaryState); } - JvsSwitchHook.RegisterButtonChecker(IsButtonPushed); - JvsSwitchHook.RegisterAuxiliaryStateProvider(GetAuxiliaryState); } - private static bool IsButtonPushed(int playerNo, int buttonIndex1To8) => buttonIndex1To8 switch + #region Button + + private static bool IsButtonPushed(int playerNo, int buttonIndex1To8) { - 1 => _devices[playerNo].input.GetSwitchState(MaimollerInputReport.SwitchClass.BUTTON, MaimollerInputReport.ButtonMask.BTN_1) != 0, - 2 => _devices[playerNo].input.GetSwitchState(MaimollerInputReport.SwitchClass.BUTTON, MaimollerInputReport.ButtonMask.BTN_2) != 0, - 3 => _devices[playerNo].input.GetSwitchState(MaimollerInputReport.SwitchClass.BUTTON, MaimollerInputReport.ButtonMask.BTN_3) != 0, - 4 => _devices[playerNo].input.GetSwitchState(MaimollerInputReport.SwitchClass.BUTTON, MaimollerInputReport.ButtonMask.BTN_4) != 0, - 5 => _devices[playerNo].input.GetSwitchState(MaimollerInputReport.SwitchClass.BUTTON, MaimollerInputReport.ButtonMask.BTN_5) != 0, - 6 => _devices[playerNo].input.GetSwitchState(MaimollerInputReport.SwitchClass.BUTTON, MaimollerInputReport.ButtonMask.BTN_6) != 0, - 7 => _devices[playerNo].input.GetSwitchState(MaimollerInputReport.SwitchClass.BUTTON, MaimollerInputReport.ButtonMask.BTN_7) != 0, - 8 => _devices[playerNo].input.GetSwitchState(MaimollerInputReport.SwitchClass.BUTTON, MaimollerInputReport.ButtonMask.BTN_8) != 0, - _ => false, - }; + if (!IsButtonEnabledForPlayer(playerNo)) return false; + return buttonIndex1To8 switch + { + 1 => _devices[playerNo].input.GetSwitchState(MaimollerInputReport.SwitchClass.BUTTON, MaimollerInputReport.ButtonMask.BTN_1) != 0, + 2 => _devices[playerNo].input.GetSwitchState(MaimollerInputReport.SwitchClass.BUTTON, MaimollerInputReport.ButtonMask.BTN_2) != 0, + 3 => _devices[playerNo].input.GetSwitchState(MaimollerInputReport.SwitchClass.BUTTON, MaimollerInputReport.ButtonMask.BTN_3) != 0, + 4 => _devices[playerNo].input.GetSwitchState(MaimollerInputReport.SwitchClass.BUTTON, MaimollerInputReport.ButtonMask.BTN_4) != 0, + 5 => _devices[playerNo].input.GetSwitchState(MaimollerInputReport.SwitchClass.BUTTON, MaimollerInputReport.ButtonMask.BTN_5) != 0, + 6 => _devices[playerNo].input.GetSwitchState(MaimollerInputReport.SwitchClass.BUTTON, MaimollerInputReport.ButtonMask.BTN_6) != 0, + 7 => _devices[playerNo].input.GetSwitchState(MaimollerInputReport.SwitchClass.BUTTON, MaimollerInputReport.ButtonMask.BTN_7) != 0, + 8 => _devices[playerNo].input.GetSwitchState(MaimollerInputReport.SwitchClass.BUTTON, MaimollerInputReport.ButtonMask.BTN_8) != 0, + _ => false, + }; + } // NOTE: Coin button is not supported yet. AquaMai recommands setting fixed number of credits directly in the configuration. + private static AuxiliaryState GetAuxiliaryState() { var auxiliaryState = new AuxiliaryState(); IOKeyMap[] keyMaps = [button1, button2, button3, button4]; for (int i = 0; i < 4; i++) { - var is1PPushed = p1 && _devices[0].input.GetSwitchState(MaimollerInputReport.SwitchClass.SYSTEM, auxiliaryMaskMap[i]) != 0; - var is2PPushed = p2 && _devices[1].input.GetSwitchState(MaimollerInputReport.SwitchClass.SYSTEM, auxiliaryMaskMap[i]) != 0; + var is1PPushed = button1p && _devices[0].input.GetSwitchState(MaimollerInputReport.SwitchClass.SYSTEM, auxiliaryMaskMap[i]) != 0; + var is2PPushed = button2p && _devices[1].input.GetSwitchState(MaimollerInputReport.SwitchClass.SYSTEM, auxiliaryMaskMap[i]) != 0; switch (keyMaps[i]) { - case IOKeyMap.Select1P: - auxiliaryState.select1P |= is1PPushed || is2PPushed; - break; - case IOKeyMap.Select2P: - auxiliaryState.select2P |= is1PPushed || is2PPushed; - break; - case IOKeyMap.Select: - auxiliaryState.select1P |= is1PPushed; - auxiliaryState.select2P |= is2PPushed; - break; - case IOKeyMap.Service: - auxiliaryState.service = is1PPushed || is2PPushed; - break; - case IOKeyMap.Test: - auxiliaryState.test = is1PPushed || is2PPushed; - break; + case IOKeyMap.Select1P: + auxiliaryState.select1P |= is1PPushed || is2PPushed; + break; + case IOKeyMap.Select2P: + auxiliaryState.select2P |= is1PPushed || is2PPushed; + break; + case IOKeyMap.Select: + auxiliaryState.select1P |= is1PPushed; + auxiliaryState.select2P |= is2PPushed; + break; + case IOKeyMap.Service: + auxiliaryState.service = is1PPushed || is2PPushed; + break; + case IOKeyMap.Test: + auxiliaryState.test = is1PPushed || is2PPushed; + break; } } return auxiliaryState; } + #endregion + private static ulong GetTouchState(int i) { ulong s = 0; @@ -153,43 +206,67 @@ public static void PreGameMainUpdate(bool ____isInitialize) if (!____isInitialize) return; for (int i = 0; i < 2; i++) { - if (!ShouldEnableForPlayer(i)) continue; + if (!ShouldInitForPlayer(i)) continue; _devices[i].Update(); } } [HarmonyPatch] + [EnableIf(typeof(MaimollerIO), nameof(IsAnyLedEnabled))] public static class JvsOutputPwmPatch { public static MethodInfo TargetMethod() => typeof(IO.Jvs).GetNestedType("JvsOutputPwm", BindingFlags.NonPublic | BindingFlags.Public).GetMethod("Set"); - public static void Prefix(byte index, Color32 color) => _ledManagers[index].SetBillboardColor(color); + public static void Prefix(byte index, Color32 color) + { + if (!IsLedEnabledForPlayer(index)) return; + _ledManagers[index].SetBillboardColor(color); + } } [HarmonyPostfix] [HarmonyPatch(typeof(Bd15070_4IF), "PreExecute")] - public static void PostPreExecute(Bd15070_4IF.InitParam ____initParam) => + [EnableIf(nameof(IsAnyLedEnabled))] + public static void PostPreExecute(Bd15070_4IF.InitParam ____initParam) + { + if (!IsLedEnabledForPlayer(____initParam.index)) return; _ledManagers[____initParam.index].PreExecute(); + } + [HarmonyPrefix] [HarmonyPatch(typeof(Bd15070_4IF), "_setColor")] - public static void Pre_setColor(byte ledPos, Color32 color, Bd15070_4IF.InitParam ____initParam) => + [EnableIf(nameof(IsAnyLedEnabled))] + public static void Pre_setColor(byte ledPos, Color32 color, Bd15070_4IF.InitParam ____initParam) + { + if (!IsLedEnabledForPlayer(____initParam.index)) return; _ledManagers[____initParam.index].SetButtonColor(ledPos, color); + } [HarmonyPrefix] [HarmonyPatch(typeof(Bd15070_4IF), "_setColorMulti")] - public static void Pre_setColorMulti(Color32 color, byte speed, Bd15070_4IF.InitParam ____initParam) => + [EnableIf(nameof(IsAnyLedEnabled))] + public static void Pre_setColorMulti(Color32 color, byte speed, Bd15070_4IF.InitParam ____initParam) + { + if (!IsLedEnabledForPlayer(____initParam.index)) return; _ledManagers[____initParam.index].SetButtonColor(-1, color); + } [HarmonyPrefix] [HarmonyPatch(typeof(Bd15070_4IF), "_setColorMultiFade")] - public static void Pre_setColorMultiFade(Color32 color, byte speed, Bd15070_4IF.InitParam ____initParam) => + [EnableIf(nameof(IsAnyLedEnabled))] + public static void Pre_setColorMultiFade(Color32 color, byte speed, Bd15070_4IF.InitParam ____initParam) + { + if (!IsLedEnabledForPlayer(____initParam.index)) return; _ledManagers[____initParam.index].SetButtonColorFade(-1, color, GetByte2Msec(speed)); + } [HarmonyPrefix] [HarmonyPatch(typeof(Bd15070_4IF), "_setColorMultiFet")] + [EnableIf(nameof(IsAnyLedEnabled))] public static void Pre_setColorMultiFet(Color32 color, Bd15070_4IF.InitParam ____initParam) { + if (!IsLedEnabledForPlayer(____initParam.index)) return; _ledManagers[____initParam.index].SetBodyIntensity(8, color.r); _ledManagers[____initParam.index].SetBodyIntensity(9, color.g); _ledManagers[____initParam.index].SetBodyIntensity(10, color.b); @@ -197,13 +274,20 @@ public static void Pre_setColorMultiFet(Color32 color, Bd15070_4IF.InitParam ___ [HarmonyPrefix] [HarmonyPatch(typeof(Bd15070_4IF), "_setColorFet")] - public static void Pre_setColorFet(byte ledPos, byte color, Bd15070_4IF.InitParam ____initParam) => + [EnableIf(nameof(IsAnyLedEnabled))] + public static void Pre_setColorFet(byte ledPos, byte color, Bd15070_4IF.InitParam ____initParam) + { + if (!IsLedEnabledForPlayer(____initParam.index)) return; _ledManagers[____initParam.index].SetBodyIntensity(ledPos, color); + } + [HarmonyPrefix] [HarmonyPatch(typeof(Bd15070_4IF), "_setLedAllOff")] + [EnableIf(nameof(IsAnyLedEnabled))] public static void Pre_setLedAllOff(Bd15070_4IF.InitParam ____initParam) { + if (!IsLedEnabledForPlayer(____initParam.index)) return; _ledManagers[____initParam.index].SetBodyIntensity(-1, 0); _ledManagers[____initParam.index].SetButtonColor(-1, new Color32(0, 0, 0, byte.MaxValue)); } diff --git a/AquaMai.Mods/GameSystem/OldCabLightBoardSupport.cs b/AquaMai.Mods/GameSystem/OldCabLightBoardSupport.cs new file mode 100644 index 00000000..2dea2cce --- /dev/null +++ b/AquaMai.Mods/GameSystem/OldCabLightBoardSupport.cs @@ -0,0 +1,290 @@ +using AquaMai.Config.Attributes; +using Comio.BD15070_4; +using HarmonyLib; +using Mecha; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using UnityEngine; + +namespace AquaMai.Mods.GameSystem; + +[ConfigSection( + name: "旧框灯板支持", + en: "Skip BoardNo check to use the old cab light board 837-15070-02, and remapping the Billboard LED to woofer LED, roof LED and center LED", + zh: "跳过 BoardNo 检查以使用 837-15070-02(旧框灯板),并映射新框顶板 LED 至重低音扬声器 LED、顶板 LED 以及中央 LED")] +public class OldCabLightBoardSupport +{ + private static readonly FieldInfo _controlField; + private static readonly FieldInfo _boardField; + private static readonly FieldInfo _ctrlField; + private static readonly FieldInfo _ioCtrlField; + private static readonly FieldInfo _setLedGs8BitCommandField; + private static readonly FieldInfo _gsUpdateField; + private static readonly MethodInfo _sendForceCommandMethod; + + [HarmonyPatch(typeof(BoardCtrl15070_4), "_md_initBoard_GetBoardInfo")] + public class BoardCtrl15070_4__md_initBoard_GetBoardInfo_Patch + { + [HarmonyTranspiler] + static IEnumerable Transpiler1(IEnumerable instructions) + { + var codes = new List(instructions); + bool patched = false; + for (int i = 0; i < codes.Count; i++) + { + if (codes[i].opcode == OpCodes.Callvirt && + codes[i].operand != null && + codes[i].operand.ToString().Contains("IsEqual")) + { + codes[i] = new CodeInstruction(OpCodes.Pop); + codes.Insert(i + 1, new CodeInstruction(OpCodes.Pop)); + codes.Insert(i + 2, new CodeInstruction(OpCodes.Ldc_I4_1)); + + patched = true; + MelonLoader.MelonLogger.Msg("[OldCabLightBoardSupport] 修补 BoardCtrl15070_4._md_initBoard_GetBoardInfo 方法成功"); + break; + } + } + if (!patched) + { + MelonLoader.MelonLogger.Warning("[OldCabLightBoardSupport] 未找到需要修补的代码位置:BoardCtrl15070_4._md_initBoard_GetBoardInfo"); + } + return codes; + } + } + + [HarmonyPatch(typeof(Bd15070_4Control), "IsNeedFirmUpdate")] + public class Bd15070_4Control_IsNeedFirmUpdate_Patch + { + [HarmonyTranspiler] + static IEnumerable Transpiler2(IEnumerable instructions) + { + var codes = new List(instructions); + bool patched = false; + for (int i = 0; i < codes.Count; i++) + { + if (codes[i].opcode == OpCodes.Callvirt && + codes[i].operand != null && + codes[i].operand.ToString().Contains("IsEqual")) + { + codes[i] = new CodeInstruction(OpCodes.Pop); + codes.Insert(i + 1, new CodeInstruction(OpCodes.Pop)); + codes.Insert(i + 2, new CodeInstruction(OpCodes.Ldc_I4_1)); + + patched = true; + MelonLoader.MelonLogger.Msg("[OldCabLightBoardSupport] 修补 Bd15070_4Control.IsNeedFirmUpdate 方法成功"); + break; + } + } + if (!patched) + { + MelonLoader.MelonLogger.Warning("[OldCabLightBoardSupport] 未找到需要修补的代码位置:Bd15070_4Control.IsNeedFirmUpdate"); + } + return codes; + } + } + + static OldCabLightBoardSupport() + { + var ledIfType = typeof(Bd15070_4IF); + _controlField = ledIfType.GetField("_control", BindingFlags.NonPublic | BindingFlags.Instance); + _gsUpdateField = ledIfType.GetField("_gsUpdate", BindingFlags.NonPublic | BindingFlags.Instance); + + var controlType = typeof(Mecha.Bd15070_4Control); + _boardField = controlType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance); + + var boardType = typeof(Comio.BD15070_4.Board15070_4); + _ctrlField = boardType.GetField("_ctrl", BindingFlags.NonPublic | BindingFlags.Instance); + + var boardCtrlType = typeof(Comio.BD15070_4.BoardCtrl15070_4); + _ioCtrlField = boardCtrlType.GetField("_ioCtrl", BindingFlags.NonPublic | BindingFlags.Instance); + _sendForceCommandMethod = boardCtrlType.GetMethod("SendForceCommand", BindingFlags.Public | BindingFlags.Instance); + + var ioCtrlType = typeof(IoCtrl); + _setLedGs8BitCommandField = ioCtrlType.GetField("SetLedGs8BitCommand", BindingFlags.Public | BindingFlags.Instance); + + if (_controlField == null) + { + MelonLoader.MelonLogger.Error("[OldCabLightBoardSupport] Failed to cache _control field"); + } + if (_setLedGs8BitCommandField == null) + { + MelonLoader.MelonLogger.Error("[OldCabLightBoardSupport] Failed to cache SetLedGs8BitCommand field"); + } + if (_sendForceCommandMethod == null) + { + MelonLoader.MelonLogger.Error("[OldCabLightBoardSupport] Failed to cache SendForceCommand method"); + } + } + + [HarmonyPatch(typeof(Bd15070_4IF), "_construct")] + public class Bd15070_4IF_Construct_Patch + { + [HarmonyTranspiler] + static IEnumerable Transpiler(IEnumerable instructions) + { + var codes = new List(instructions); + int patchedNum = 0; + for (int i = 0; i < codes.Count && patchedNum < 2; i++) + { + if (codes[i].opcode == OpCodes.Ldc_I4_8 && + codes[i].operand == null) + { + codes[i].opcode = OpCodes.Ldc_I4_S; + codes[i].operand = (sbyte)10; + patchedNum++; + } + } + if (patchedNum == 2) + { + MelonLoader.MelonLogger.Msg("[OldCabLightBoardSupport] Extended Bd15070_4IF._switchParam size and its initialize for loop from 8 to 10!"); + } + else + { + MelonLoader.MelonLogger.Warning($"[OldCabLightBoardSupport] Bd15070_4IF._switchParam patching failed (patched {patchedNum}/2)"); + } + return codes; + } + } + + [HarmonyPatch] + public class JvsOutputPwmPatch + { + [HarmonyTargetMethod] + static MethodBase TargetMethod() + { + var type = typeof(IO.Jvs).GetNestedType("JvsOutputPwm", BindingFlags.NonPublic | BindingFlags.Public); + if (type == null) + { + MelonLoader.MelonLogger.Error("[OldCabLightBoardSupport] JvsOutputPwm type not found"); + return null; + } + return type.GetMethod("Set", new[] { typeof(byte), typeof(Color32), typeof(bool) }); + } + + [HarmonyPrefix] + public static bool Prefix(object __instance, byte index, Color32 color, bool update) + { + RedirectToButtonLedMechanism(index, color); + + return false; + } + } + + private static void RedirectToButtonLedMechanism(byte playerIndex, Color32 color) + { + // Check if MechaManager is initialized + if (!IO.MechaManager.IsInitialized) + { + MelonLoader.MelonLogger.Warning("[OldCabLightBoardSupport] MechaManager not initialized, cannot set woofer LED"); + return; + } + + // Get the LED interface for the player + var ledIf = IO.MechaManager.LedIf; + if (ledIf == null || playerIndex >= ledIf.Length || ledIf[playerIndex] == null) + { + MelonLoader.MelonLogger.Warning($"[OldCabLightBoardSupport] LED interface not available for player {playerIndex}"); + return; + } + + // Use cached reflection FieldInfo and MethodInfo to access the IoCtrl and call SetLedGs8BitCommand[8] & SetLedGs8BitCommand[9] directly + // Then set _gsUpdate flag so PreExecute() sends the update command (just like buttons do) + try + { + if (_controlField == null) + { + MelonLoader.MelonLogger.Error("[OldCabLightBoardSupport] _control field not found in Bd15070_4IF"); + return; + } + + var control = _controlField.GetValue(ledIf[playerIndex]); + if (control == null) + { + MelonLoader.MelonLogger.Error("[OldCabLightBoardSupport] Control object is null"); + return; + } + + if (_boardField == null) + { + MelonLoader.MelonLogger.Error("[OldCabLightBoardSupport] _board field not found in Bd15070_4Control"); + return; + } + + var board = _boardField.GetValue(control); + if (board == null) + { + MelonLoader.MelonLogger.Error("[OldCabLightBoardSupport] Board object is null"); + return; + } + + if (_ctrlField == null) + { + MelonLoader.MelonLogger.Error("[OldCabLightBoardSupport] _ctrl field not found in Board15070_4"); + return; + } + + var boardCtrl = _ctrlField.GetValue(board); + if (boardCtrl == null) + { + MelonLoader.MelonLogger.Error("[OldCabLightBoardSupport] BoardCtrl object is null"); + return; + } + + if (_ioCtrlField == null) + { + MelonLoader.MelonLogger.Error("[OldCabLightBoardSupport] _ioCtrl field not found in BoardCtrl15070_4"); + return; + } + + var ioCtrl = _ioCtrlField.GetValue(boardCtrl); + if (ioCtrl == null) + { + MelonLoader.MelonLogger.Error("[OldCabLightBoardSupport] IoCtrl object is null"); + return; + } + + if (_setLedGs8BitCommandField == null) + { + MelonLoader.MelonLogger.Error("[OldCabLightBoardSupport] SetLedGs8BitCommand field not found in IoCtrl"); + return; + } + + var setLedGs8BitCommandArray = _setLedGs8BitCommandField.GetValue(ioCtrl) as SetLedGs8BitCommand[]; + if (setLedGs8BitCommandArray == null || setLedGs8BitCommandArray.Length <= 9) + { + MelonLoader.MelonLogger.Error("[OldCabLightBoardSupport] SetLedGs8BitCommand array is null or too small"); + return; + } + + if (_sendForceCommandMethod == null) + { + MelonLoader.MelonLogger.Error("[OldCabLightBoardSupport] SendForceCommand method not found in BoardCtrl15070_4"); + return; + } + + // Use SetLedGs8BitCommand[8] and SetLedGs8BitCommand[9] directly (same as buttons 0-7, but for ledPos = 8 and 9) + // This bypasses the FET command path in IoCtrl.SetLedData(), as they are not via FET, they are like buttons + // ledPos = 8 == woofer & roof + // ledPos = 9 == center + setLedGs8BitCommandArray[8].setColor(8, color); + setLedGs8BitCommandArray[9].setColor(9, color); + _sendForceCommandMethod.Invoke(boardCtrl, new object[] { setLedGs8BitCommandArray[8] }); + _sendForceCommandMethod.Invoke(boardCtrl, new object[] { setLedGs8BitCommandArray[9] }); + + if (_gsUpdateField != null) + { + _gsUpdateField.SetValue(ledIf[playerIndex], true); + } + else + { + MelonLoader.MelonLogger.Warning("[OldCabLightBoardSupport] _gsUpdate field not found, LED may not update"); + } + } + catch (System.Exception ex) + { + MelonLoader.MelonLogger.Error($"[OldCabLightBoardSupport] Failed to set woofer LED: {ex.Message}\n{ex.StackTrace}"); + } + } +} \ No newline at end of file diff --git a/AquaMai.Mods/GameSystem/SkipBoardNoCheck.cs b/AquaMai.Mods/GameSystem/SkipBoardNoCheck.cs deleted file mode 100644 index 967f1a2b..00000000 --- a/AquaMai.Mods/GameSystem/SkipBoardNoCheck.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using AMDaemon; -using AquaMai.Config.Attributes; -using Comio.BD15070_4; -using Mecha; -using HarmonyLib; -using System.Reflection.Emit; -using Manager; -using UnityEngine; - -namespace AquaMai.Mods.GameSystem; - -[ConfigSection( - name: "旧框灯板支持", - en: "Skip BoardNo check to use the old cab light board 837-15070-02", - zh: "跳过 BoardNo 检查以使用 837-15070-02(旧框灯板)")] -public class SkipBoardNoCheck -{ - [HarmonyTranspiler] - [HarmonyPatch(typeof(BoardCtrl15070_4), "_md_initBoard_GetBoardInfo")] - static IEnumerable Transpiler1(IEnumerable instructions) - { - var codes = new List(instructions); - bool patched = false; - for (int i = 0; i < codes.Count; i++) - { - if (codes[i].opcode == OpCodes.Callvirt && - codes[i].operand != null && - codes[i].operand.ToString().Contains("IsEqual")) - { - codes[i] = new CodeInstruction(OpCodes.Pop); - codes.Insert(i + 1, new CodeInstruction(OpCodes.Pop)); - codes.Insert(i + 2, new CodeInstruction(OpCodes.Ldc_I4_1)); - - patched = true; - MelonLoader.MelonLogger.Msg("[SkipBoardNoCheck] 修补 BoardCtrl15070_4._md_initBoard_GetBoardInfo 方法成功"); - break; - } - } - if (!patched) - { - MelonLoader.MelonLogger.Warning("[SkipBoardNoCheck] 未找到需要修补的代码位置:BoardCtrl15070_4._md_initBoard_GetBoardInfo"); - } - return codes; - } - - [HarmonyTranspiler] - [HarmonyPatch(typeof(Bd15070_4Control), "IsNeedFirmUpdate")] - static IEnumerable Transpiler2(IEnumerable instructions) - { - var codes = new List(instructions); - bool patched = false; - for (int i = 0; i < codes.Count; i++) - { - if (codes[i].opcode == OpCodes.Callvirt && - codes[i].operand != null && - codes[i].operand.ToString().Contains("IsEqual")) - { - codes[i] = new CodeInstruction(OpCodes.Pop); - codes.Insert(i + 1, new CodeInstruction(OpCodes.Pop)); - codes.Insert(i + 2, new CodeInstruction(OpCodes.Ldc_I4_1)); - - patched = true; - MelonLoader.MelonLogger.Msg("[SkipBoardNoCheck] 修补 Bd15070_4Control.IsNeedFirmUpdate 方法成功"); - break; - } - } - if (!patched) - { - MelonLoader.MelonLogger.Warning("[SkipBoardNoCheck] 未找到需要修补的代码位置:Bd15070_4Control.IsNeedFirmUpdate"); - } - return codes; - } -} \ No newline at end of file diff --git a/AquaMai.Mods/Utils/DisplayTouchInGame.cs b/AquaMai.Mods/Utils/DisplayTouchInGame.cs index fcb02683..fd84ad0c 100644 --- a/AquaMai.Mods/Utils/DisplayTouchInGame.cs +++ b/AquaMai.Mods/Utils/DisplayTouchInGame.cs @@ -26,11 +26,12 @@ namespace AquaMai.Mods.Utils; public static class DisplayTouchInGame { private static GameObject prefab; - private static GameObject[] canvasGameObjects = new GameObject[2]; + private static readonly List[] canvasGameObjects = new List[] { new(), new() }; private static TextMeshProUGUI tmp; private static TextMeshProUGUI[] tmps = new TextMeshProUGUI[2]; - // 0: 不显示,1: 上框透明底,2: 上框白底,3: 下框 + // 0: 不显示,1: 上框透明底,2: 上框白底,3: 下框,4: 上框透明底+下框,5: 上框白底+下框 public static int[] displayType = [0, 0]; + public const int displayTypeVer = 2; [ConfigEntry( name: "默认显示", @@ -75,14 +76,21 @@ public static void OnGameProcessUpdate() { for (int i = 0; i < 2; i++) { - if (displayType[i] != 2) continue; + // 只有上框白底(2)才会创建 tmps;组合模式(5=2+3)同样需要更新计时 + if (displayType[i] is not (2 or 5)) continue; + if (tmps[i] == null) continue; tmps[i].text = $"{TimeSpan.FromMilliseconds(PracticeMode.CurrentPlayMsec):mm\\:ss\\.fff}"; } if (!KeyListener.GetKeyDownOrLongPress(key, longPress)) return; displayType = displayType[0] == 0 ? [1, 1] : [0, 0]; for (int i = 0; i < 2; i++) { - canvasGameObjects[i].SetActive(displayType[i] != 0); + if (canvasGameObjects[i] == null) continue; + foreach (var go in canvasGameObjects[i]) + { + if (go == null) continue; + go.SetActive(displayType[i] != 0); + } } } @@ -107,100 +115,121 @@ public static void OnGameStart(GameMonitor[] ____monitors) for (int i = 0; i < 2; i++) { - var sub = ____monitors[i].gameObject.transform.Find("Canvas/Sub"); - if (displayType[i] == 3) + canvasGameObjects[i].Clear(); + var type = displayType[i]; + if (type is < 1 or > 5) continue; + if (type is 4 or 5) { - sub = Traverse.Create(____monitors[i]).Field("GameController").Value?.transform; - } - if (sub == null) - { - MelonLogger.Error($"[DisplayTouchInGame] sub is null for monitor {i}"); - continue; + CreateDisplay(3, ____monitors[i]); + type -= 3; } - var canvas = new GameObject("[AquaMai] DisplayTouchInGame"); - canvas.transform.SetParent(sub, false); - canvas.SetActive(displayType[i] > 0); - canvasGameObjects[i] = canvas; - GameObject buttons = null; + if (type > 3) continue; + CreateDisplay(type, ____monitors[i]); + } + } - if (displayType[i] == 3) - { - var rect = canvas.AddComponent(); - rect.sizeDelta = new Vector2(1080, 1080); - rect.localPosition = Vector3.zero; - var canvasComp = canvas.AddComponent(); - canvasComp.renderMode = RenderMode.WorldSpace; - canvasComp.sortingOrder = -32768; - // canvasComp.sortingOrder = 1; - // canvasComp.sortingLayerID = -385436797; // GameMovie - } - if (displayType[i] == 2) - { - var rect = canvas.AddComponent(); - rect.sizeDelta = new Vector2(1080, 450); - rect.localPosition = Vector3.zero; - var img = canvas.AddComponent(); - img.color = Color.white; - - var t = Object.Instantiate(tmp, canvas.transform, false); - t.text = ""; - t.transform.localPosition = new Vector3(-500f, 0f, 0f); - t.transform.localScale = Vector3.one * 2; - tmps[i] = t; - } + // type 只能传入 1 2 3,如果是 4 或 5 的话,拆分成两个 call + private static void CreateDisplay(int type, GameMonitor monitor) + { + if (type is < 1 or > 3) throw new ArgumentException("这不对吧"); + var sub = monitor.gameObject.transform.Find("Canvas/Sub"); + if (type == 3) + { + sub = Traverse.Create(monitor).Field("GameController").Value?.transform; + } + if (sub == null) + { + MelonLogger.Error($"[DisplayTouchInGame] sub is null"); + return; + } + var index = monitor.MonitorIndex; + var canvas = new GameObject("[AquaMai] DisplayTouchInGame"); + canvas.transform.SetParent(sub, false); + canvas.SetActive(type > 0); + canvasGameObjects[index].Add(canvas); + GameObject buttons = null; + + if (type == 3) + { + var rect = canvas.AddComponent(); + rect.sizeDelta = new Vector2(1080, 1080); + rect.localPosition = Vector3.zero; + var canvasComp = canvas.AddComponent(); + canvasComp.renderMode = RenderMode.WorldSpace; + canvasComp.sortingOrder = -32768; + // canvasComp.sortingOrder = 1; + // canvasComp.sortingLayerID = -385436797; // GameMovie + } + if (type == 2) + { + var rect = canvas.AddComponent(); + rect.sizeDelta = new Vector2(1080, 450); + rect.localPosition = Vector3.zero; + var img = canvas.AddComponent(); + img.color = Color.white; + + var t = Object.Instantiate(tmp, canvas.transform, false); + t.text = ""; + t.transform.localPosition = new Vector3(-500f, 0f, 0f); + t.transform.localScale = Vector3.one * 2; + tmps[index] = t; + } - if (displayType[i] != 3) + if (type != 3) + { + // init button display + buttons = new GameObject("Buttons"); + buttons.transform.SetParent(canvas.transform, false); + buttons.transform.localPosition = Vector3.zero; + buttons.transform.localScale = Vector3.one * 450 / 1080f; + } + + var touchPanel = Object.Instantiate(prefab, canvas.transform, false); + Object.Destroy(touchPanel.GetComponent()); + foreach (Transform item in touchPanel.transform) + { + if (item.name.StartsWith("CircleGraphic")) { - // init button display - buttons = new GameObject("Buttons"); - buttons.transform.SetParent(canvas.transform, false); - buttons.transform.localPosition = Vector3.zero; - buttons.transform.localScale = Vector3.one * 450 / 1080f; + Object.Destroy(item.gameObject); + continue; } + Object.Destroy(item.GetComponent()); + Object.Destroy(item.GetComponent()); + } + touchPanel.transform.localPosition = Vector3.zero; + var touchDisplay = touchPanel.AddComponent(); + touchDisplay.player = index; + touchDisplay.type = type; - var touchPanel = Object.Instantiate(prefab, canvas.transform, false); - Object.Destroy(touchPanel.GetComponent()); + if (type != 3) + { foreach (Transform item in touchPanel.transform) { - if (item.name.StartsWith("CircleGraphic")) + var customGraphic = item.GetComponent(); + customGraphic.color = Color.blue; + if (item.name.StartsWith("A")) { - Object.Destroy(item.gameObject); - continue; + var btn = Object.Instantiate(item, buttons.transform, false); + btn.name = item.name; } - Object.Destroy(item.GetComponent()); - Object.Destroy(item.GetComponent()); + var tmp = item.GetComponentInChildren(); + tmp.color = Color.black; } - touchPanel.transform.localPosition = Vector3.zero; - var touchDisplay = touchPanel.AddComponent(); - touchDisplay.player = i; - if (displayType[i] != 3) - { - foreach (Transform item in touchPanel.transform) - { - var customGraphic = item.GetComponent(); - customGraphic.color = Color.blue; - if (item.name.StartsWith("A")) - { - var btn = Object.Instantiate(item, buttons.transform, false); - btn.name = item.name; - } - var tmp = item.GetComponentInChildren(); - tmp.color = Color.black; - } - - touchPanel.transform.localScale = Vector3.one * 0.95f * 450 / 1080f; - var buttonDisplay = buttons.AddComponent(); - buttonDisplay.player = i; - buttonDisplay.isButton = true; - } + touchPanel.transform.localScale = Vector3.one * 0.95f * 450 / 1080f; + var buttonDisplay = buttons.AddComponent(); + buttonDisplay.player = index; + buttonDisplay.isButton = true; + buttonDisplay.type = type; } + } private class Display : MonoBehaviour { public int player; public bool isButton; + public int type; private List _buttonList; private Color _offTouchCol = new Color(0f, 0f, 1f); @@ -218,7 +247,7 @@ private void Start() CustomGraphic component = item.GetComponent(); _buttonList.Add(component); } - if (displayType[player] == 3) + if (type == 3) { _offTouchCol = Color.clear; _onTouchCol = new Color(1f, 0f, 0f, 0.3f); diff --git a/AquaMai.Mods/Utils/UnstableRate.cs b/AquaMai.Mods/Utils/UnstableRate.cs new file mode 100644 index 00000000..5ee5fac2 --- /dev/null +++ b/AquaMai.Mods/Utils/UnstableRate.cs @@ -0,0 +1,282 @@ +using System.Collections.Generic; +using AquaMai.Config.Attributes; +using HarmonyLib; +using MAI2.Util; +using Manager; +using Monitor; +using Process; +using UnityEngine; + +namespace AquaMai.Mods.Utils; + +[ConfigSection( + name: "稳定度指示器", + zh: "在屏幕中心显示每次判定的精确区间", + en: "Show information about the exact timing for each hit during gameplay in the center of the screen.")] +public class UnstableRate +{ + // The playfield goes from bottom left (-1080, -960) to top right (0, 120) + // 使用了 local space + private const float BaselineHeight = -70; + private const float BaselineCenter = 0; + private const float BaselineHScale = 25; + private const float CenterMarkerHeight = 20; + + private const float JudgeHeight = 20; + private const float JudgeFadeDelay = 1; + private const float JudgeFadeTime = 1; + private const float JudgeAlpha = 0.8f; + + private const float LineThickness = 4; + + private const float TimingBin = 16.666666f; + + // 0: 不显示,1: 显示,剩下来留给以后 + public static int[] displayType = [1, 1]; + + private struct Timing + { + // Timings are in multiple of TimingBin (16.666666ms) + public int windowStart; + public int windowEnd; + public Color color; + } + + private static readonly Timing[] Timings = + [ + new() { windowStart = 0, windowEnd = 1, color = new Color(1.000f, 0.843f, 0.000f) }, // Critical (#ffd700) + new() { windowStart = 1, windowEnd = 3, color = new Color(1.000f, 0.647f, 0.000f) }, // Perfect (#ffa500) + new() { windowStart = 3, windowEnd = 6, color = new Color(1.000f, 0.078f, 0.576f) }, // Great (#ff1493) + new() { windowStart = 6, windowEnd = 9, color = new Color(0.000f, 0.502f, 0.000f) }, // Good (#008000) + ]; + private static readonly Timing Miss = new() { windowStart = 999, windowEnd = 999, color = Color.grey }; + private static readonly Material LineMaterial = new(Shader.Find("Sprites/Default")); + + private static GameObject[] baseObjects = new GameObject[2]; + private static LinePool[] linePools = new LinePool[2]; + + [HarmonyPostfix] + [HarmonyPatch(typeof(GameProcess), "OnStart")] + public static void OnGameProcessStart(GameProcess __instance, GameMonitor[] ____monitors) + { + // Set up the baseline (the static part of the display) + for (int i = 0; i < 2; i++) + { + if (displayType[i] == 0) continue; + var userData = UserDataManager.Instance.GetUserData(i); + if (!userData.IsEntry) continue; + var main = ____monitors[i].gameObject.transform.Find("Canvas/Main"); + var go = new GameObject("[AquaMai] UnstableRate"); + go.transform.SetParent(main, false); + baseObjects[i] = go; + linePools[i] = new LinePool(go); + SetupBaseline(go); + } + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(NoteBase), "Judge")] + public static void OnJudge(NoteBase __instance, float ___JudgeTimingDiffMsec) + { + if (displayType[__instance.MonitorId] == 0) return; + + // How many milliseconds early or late the player hit + var msec = ___JudgeTimingDiffMsec; + + // Account for the offset + var optionJudgeTiming = Singleton.Instance.GetGameScore(__instance.MonitorId).UserOption.GetJudgeTimingFrame(); + msec -= optionJudgeTiming * TimingBin; + + // Don't process misses + var timing = GetTiming(msec); + if (timing.windowStart == Miss.windowStart) + { + return; + } + + var pool = linePools[__instance.MonitorId]; + if (pool == null) + { + return; + } + + var line = pool.Get(); + + line.SetPosition(0, new Vector3(BaselineCenter + BaselineHScale * (msec / TimingBin), BaselineHeight + JudgeHeight, 0)); + line.SetPosition(1, new Vector3(BaselineCenter + BaselineHScale * (msec / TimingBin), BaselineHeight - JudgeHeight, 0)); + + line.startColor = timing.color; + line.endColor = timing.color; + + // Setup fade-out + var judgeTick = line.gameObject.GetComponent(); + if (judgeTick == null) + { + judgeTick = line.gameObject.AddComponent(); + } + judgeTick.SetLine(line, pool); + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(HoldNote), "JudgeHoldHead")] + public static void OnJudgeHold(HoldNote __instance, float ___JudgeTimingDiffMsec) + { + // The calculations are the same for the hold note heads + OnJudge(__instance, ___JudgeTimingDiffMsec); + } + + private static void SetupBaseline(GameObject go) + { + LineRenderer line; + + // Draw lines from the center outwards in both directions + for (float sign = -1; sign <= 1; sign += 2) + { + // Draw each timing window in a different color + foreach (var timing in Timings) + { + line = CreateLine(go, flatCaps: true); + + line.SetPosition(0, new Vector3(BaselineCenter + sign * BaselineHScale * timing.windowStart, BaselineHeight, 0)); + line.SetPosition(1, new Vector3(BaselineCenter + sign * BaselineHScale * timing.windowEnd, BaselineHeight, 0)); + + line.startColor = timing.color; + line.endColor = timing.color; + } + } + + // Center marker + line = CreateLine(go); + + // Setting z-coordinate to -1 to make sure it stays in the foreground + line.SetPosition(0, new Vector3(BaselineCenter, BaselineHeight + CenterMarkerHeight, -1)); + line.SetPosition(1, new Vector3(BaselineCenter, BaselineHeight - CenterMarkerHeight, -1)); + + line.startColor = Color.white; + line.endColor = Color.white; + } + + private static LineRenderer CreateLine(GameObject go, bool flatCaps = false) + { + var obj = new GameObject(); + obj.transform.SetParent(go.transform, false); + + // We can't add the line directly as a component of the monitor, because it can only + // have one LineRenderer component at a time. + var line = obj.AddComponent(); + line.material = LineMaterial; + line.useWorldSpace = false; + line.startWidth = LineThickness; + line.endWidth = LineThickness; + line.positionCount = 2; + line.numCapVertices = flatCaps ? 0 : 6; + + return line; + } + + private static Timing GetTiming(float msec) + { + // Convert from milliseconds to multiples of TimingBin, the same unit used in + // the lookup table. + var hitTime = Mathf.Abs(msec) / TimingBin; + + // Search the timing interval that the hit lands in + foreach (var timing in Timings) + { + // Using >= and < just like NoteJudge + if (hitTime >= timing.windowStart && hitTime < timing.windowEnd) + { + return timing; + } + } + + return Miss; + } + + private class LinePool + { + private readonly Queue _pool = new(); + private readonly GameObject _parent; + private const int InitialPoolSize = 128; + + public LinePool(GameObject parent) + { + _parent = parent; + + // 预创建对象 + for (int i = 0; i < InitialPoolSize; i++) + { + var line = CreateLine(_parent); + line.gameObject.SetActive(false); + _pool.Enqueue(line); + } + } + + public LineRenderer Get() + { + LineRenderer line; + if (_pool.Count > 0) + { + line = _pool.Dequeue(); + line.gameObject.SetActive(true); + } + else + { + line = CreateLine(_parent); + } + + return line; + } + + public void Return(LineRenderer line) + { + line.gameObject.SetActive(false); + _pool.Enqueue(line); + } + } + + // 动画 + private class JudgeTick : MonoBehaviour + { + private float _elapsedTime; + private LineRenderer _line; + private LinePool _pool; + private Color _initialColor; + + public void SetLine(LineRenderer line, LinePool pool) + { + _line = line; + _pool = pool; + _initialColor = line.startColor; + _elapsedTime = 0; + + var color = _initialColor; + color.a *= JudgeAlpha; + + _line.startColor = color; + _line.endColor = color; + } + + public void Update() + { + _elapsedTime += Time.deltaTime; + + if (_elapsedTime < JudgeFadeDelay) + return; + + var fadeProgress = (_elapsedTime - JudgeFadeDelay) / JudgeFadeTime; + + if (fadeProgress >= 1.0f) + { + _pool.Return(_line); + return; + } + + Color color = _initialColor; + color.a = JudgeAlpha * (1.0f - fadeProgress); + + _line.startColor = color; + _line.endColor = color; + } + } +} diff --git a/AquaMai/configSort.yaml b/AquaMai/configSort.yaml index 993b206f..d64249fe 100644 --- a/AquaMai/configSort.yaml +++ b/AquaMai/configSort.yaml @@ -12,12 +12,12 @@ - Utils.MoveAnswerSound 新功能!: + - Utils.UnstableRate + - Utils.DisplayTouchInGame + - Fancy.ResourcesOverride + - GameSystem.OldCabLightBoardSupport + - Utils.InstantQuit - GameSystem.MaimollerIO - - Tweaks.LedBrightnessControl - - Fancy.TrackCamouflage - - GameSystem.SoundRouting - - GameSystem.HeadphoneVolumeMultiply - - Utils.LedOffOnQuit 全解和跳过: - GameSystem.Unlock @@ -55,6 +55,7 @@ - Fancy.GamePlay.HideHanabi - Fancy.GamePlay.JudgeDisplay4B - Utils.DisplayTouchInGame + - Utils.UnstableRate - UX.FixTrackNumDisplay - Utils.LedOffOnQuit @@ -88,11 +89,13 @@ 高级设置: - Fancy.HideMask + - GameSystem.OldCabLightBoardSupport - GameSystem.LightBoardPort - GameSystem.TouchPanelPort - GameSystem.TouchPanelBaudRate - GameSystem.SkipBoardNoCheck - Tweaks.TimeSaving.CheckSkip + - Utils.InstantQuit - Fancy.Triggers 过新过热: @@ -109,6 +112,7 @@ - Fancy.GamePlay.TapInHoldFix - Fancy.CustomIntroCinematic - Fancy.TrackCamouflage + - Fancy.ResourcesOverride 不建议关闭: - UX.ImmediateSave @@ -165,4 +169,6 @@ - Fancy.GamePlay.TapInHoldFix - Tweaks.TimeSaving.CheckSkip - Fancy.TrackCamouflage + - Fancy.ResourcesOverride + - GameSystem.OldCabLightBoardSupport diff --git a/checkSort.py b/checkSort.py new file mode 100644 index 00000000..4c44c086 --- /dev/null +++ b/checkSort.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +检查 configSort.yaml 和 AquaMai.zh.toml 的一致性 + +检查内容: +1. AquaMai.zh.toml 中的所有 Sections 是否都在 configSort.yaml 中存在 +2. configSort.yaml 中"社区功能"分类的内容是否同时存在于其他分类中 +""" + +import sys +from pathlib import Path +import yaml + + +def load_toml_sections(toml_path: Path) -> set[str]: + """从 TOML 文件中提取所有 Section 名称(通过解析 [Section] 标记)""" + sections = set() + + with open(toml_path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + # 匹配 [Section] 或 #[Section] 格式 + if line.startswith("[") or line.startswith("#["): + # 去掉注释符号 + if line.startswith("#"): + line = line[1:] + # 提取 Section 名称 + if line.startswith("[") and line.endswith("]"): + section = line[1:-1] + sections.add(section) + + return sections + + +def load_yaml_sections(yaml_path: Path) -> dict[str, list[str]]: + """从 YAML 文件中提取所有分类及其包含的 Sections""" + with open(yaml_path, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) + + return data + + +def main(): + # 文件路径 + toml_path = Path("Output/AquaMai.zh.toml") + yaml_path = Path("AquaMai/configSort.yaml") + + if not toml_path.exists(): + print(f"[错误] 找不到文件 {toml_path}") + return 1 + + if not yaml_path.exists(): + print(f"[错误] 找不到文件 {yaml_path}") + return 1 + + # 加载数据 + toml_sections = load_toml_sections(toml_path) + yaml_categories = load_yaml_sections(yaml_path) + + # 收集 YAML 中所有的 sections + yaml_all_sections = set() + for sections in yaml_categories.values(): + yaml_all_sections.update(sections) + + # 检查 1: TOML 中的所有 Sections 是否都在 YAML 中存在 + missing_in_yaml = toml_sections - yaml_all_sections + + if missing_in_yaml: + print("[失败] 以下 Sections 在 AquaMai.zh.toml 中存在,但不在 configSort.yaml 中:") + for section in sorted(missing_in_yaml): + print(f" - {section}") + return 1 + + # 检查 2: "社区功能" 中的内容是否都存在于其他分类中 + community_sections = set(yaml_categories.get("社区功能", [])) + other_sections = set() + + for category, sections in yaml_categories.items(): + if category != "社区功能": + other_sections.update(sections) + + missing_in_other = community_sections - other_sections + + if missing_in_other: + print("[失败] 以下 Sections 在\"社区功能\"中存在,但不在其他分类中:") + for section in sorted(missing_in_other): + print(f" - {section}") + return 1 + + # 所有检查通过 + print("[通过] 检查通过") + return 0 + + +if __name__ == "__main__": + sys.exit(main())