diff --git a/AquaMai.Core/Helpers/ErrorFrame.cs b/AquaMai.Core/Helpers/ErrorFrame.cs new file mode 100644 index 00000000..7855c2b8 --- /dev/null +++ b/AquaMai.Core/Helpers/ErrorFrame.cs @@ -0,0 +1,53 @@ +using System; +using HarmonyLib; +using Manager; +using MelonLoader; +using Monitor.Error; +using Process; +using Process.Error; +using TMPro; + +namespace AquaMai.Core.Helpers; + +public static class ErrorFrame +{ + private static int _customErrCode; + private static string _customErrMsg; + private static DateTime _customErrDate; + + public static void Show(ProcessBase process, int errCode, string errMsg) + { + _customErrCode = errCode; + _customErrMsg = errMsg; + _customErrDate = DateTime.Now; + Show(process); + } + + // Display the error frame with AMDaemon's original error message. + public static void Show(ProcessBase process) + { + var tv = Traverse.Create(process); + var ctn = tv.Field("container").GetValue(); + ctn.processManager.AddProcess((ProcessBase) new ErrorProcess(ctn)); + ctn.processManager.ReleaseProcess(process); + GameManager.IsErrorMode = true; + } + + // patch the error monitor so that it can display custom error codes and messages. + [HarmonyPostfix] + [HarmonyPatch(typeof(ErrorMonitor), "Initialize", typeof(int), typeof(bool))] + public static void PostInitialize(ErrorMonitor __instance) + { + var tv = Traverse.Create(__instance); + if (_customErrCode == 0) + { + MelonLogger.Msg($"Displaying error frame with AMDaemon code {AMDaemon.Error.Number}: {AMDaemon.Error.Message}"); + return; + } + MelonLogger.Msg($"Displaying error frame with custom code {_customErrCode}: {_customErrMsg}"); + tv.Field("ErrorID").GetValue().text = _customErrCode.ToString().PadLeft(4, '0'); + tv.Field("ErrorMessage").GetValue().text = _customErrMsg; + tv.Field("ErrorDate").GetValue().text = _customErrDate.ToString(); + } + +} \ No newline at end of file diff --git a/AquaMai.Core/Startup.cs b/AquaMai.Core/Startup.cs index 57c809bf..28ba0b42 100644 --- a/AquaMai.Core/Startup.cs +++ b/AquaMai.Core/Startup.cs @@ -164,6 +164,7 @@ public class Startup CollectWantedPatches(wantedPatches, typeof(KeyListener)); CollectWantedPatches(wantedPatches, typeof(Shim)); CollectWantedPatches(wantedPatches, typeof(NetPacketHook)); + CollectWantedPatches(wantedPatches, typeof(ErrorFrame)); // 使用时才 patch!不要添加这个 // CollectWantedPatches(wantedPatches, typeof(GameSettingsManager)); diff --git a/AquaMai.Mods/UX/HardwareAlert.cs b/AquaMai.Mods/UX/HardwareAlert.cs new file mode 100644 index 00000000..00143177 --- /dev/null +++ b/AquaMai.Mods/UX/HardwareAlert.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using AquaMai.Config.Attributes; +using AquaMai.Core.Helpers; +using AquaMai.Core.Resources; +using AquaMai.Mods.GameSystem; +using HarmonyLib; +using IO; +using MAI2.Util; +using MAI2System; +using Main; +using Manager; +using MelonLoader; +using Monitor.Error; +using Process; +using Process.Error; +using TMPro; +using UnityEngine; + +namespace AquaMai.Mods.UX; + +[ConfigSection( + en: "Custom hardware alert, you can configure to display an error frame upon hardware failure. Toggle the switches below to define the required hardware.", + zh: "自定义硬件警告,可配置在指定硬件自检失败时阻止游戏启动并显示报错画面,开启下方的错误类型以配置需要关注的错误。")] +public class HardwareAlert +{ + [ConfigEntry( + en: "If enabled, all the in-game hardware warnings will be displayed in game's original language, like Japanese for SDEZ. If you have used any translation pack, you should disable this setting.", + zh: "如果启用,所有硬件警告将会使用游戏原本的语言显示,例如 SDEZ 就会用日文显示报错。如果你安装了任何汉化包,你应该关闭这个选项。")] + private static readonly bool UseOriginalGameLanguage = true; + [ConfigEntry( + en: "1P Touch Sensor", + zh: "1P 触摸屏")] + private static readonly bool TouchSensor_1P = false; // Error 3300, 3301 + [ConfigEntry( + en: "2P Touch Sensor", + zh: "2P 触摸屏")] + private static readonly bool TouchSensor_2P = false; // Error 3302, 3303 + [ConfigEntry( + en: "1P LED", + zh: "1P LED")] + private static readonly bool LED_1P = false; // custom 3400 + [ConfigEntry( + en: "2P LED", + zh: "2P LED")] + private static readonly bool LED_2P = false; // custom 3401 + [ConfigEntry( + en: "Player Camera", + zh: "玩家摄像机")] + private static readonly bool PlayerCamera = false; // 3102 + [ConfigEntry( + en: "DX Pass 1P", + zh: "DX Pass 1P")] + private static readonly bool CodeReader_1P = false; // 3101 + [ConfigEntry( + en: "DX Pass 2P", + zh: "DX Pass 2P")] + private static readonly bool CodeReader_2P = false; // 3101 + [ConfigEntry( + en: "WeChat QRCode Camera", + zh: "二维码扫描摄像头")] + public static readonly bool ChimeCamera = false; // 3100 + + private static readonly List CameraTypeList = ["QRLeft", "QRRight", "Photo", "Chime"]; + private static SortedDictionary _cameraIndex = []; + private static bool _isInitialized = false; + + private enum CameraTypeEnumInner + { + QRLeft, + QRRight, + Photo, + Chime, + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(CameraManager), "CameraInitialize")] + public static void PostCameraInitialize(CameraManager __instance) + { + if (_isInitialized) + { + return; + } + + var curCamIdx = 0; + foreach (var cameraTypeName in CameraTypeList) + { + if (Enum.TryParse(cameraTypeName, out var cameraType)) + { + MelonLogger.Msg($"[HardwareAlert] Identified camera type {cameraType} for current game version on idx {curCamIdx}"); + _cameraIndex[cameraType] = curCamIdx; + curCamIdx++; + } + } + + _isInitialized = true; + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(StartupProcess), "OnUpdate")] + public static void OnPostUpdate(StartupProcess __instance) + { + // get current startup state + var tv = Traverse.Create(__instance); + // var state = tv.Field("_state").GetValue(); + var statusSubMsg = tv.Field("_statusSubMsg").GetValue(); + + // Touch sensor check + // The built-in AMDaemon errors are not stable, and cannot be localized. + // So we decided to use another approach to check it. + if (TouchSensor_1P && statusSubMsg[0] == ConstParameter.TestString_Bad) + { + ErrorFrame.Show(__instance, 3300, FaultTouchSensor1P[GetLocale()]); + return; + } + if (TouchSensor_2P && statusSubMsg[1] == ConstParameter.TestString_Bad) + { + ErrorFrame.Show(__instance, 3302, FaultTouchSensor2P[GetLocale()]); + return; + } + + // LED check + if (LED_1P && statusSubMsg[2] == ConstParameter.TestString_Bad) + { + ErrorFrame.Show(__instance, 3400, FaultLED1P[GetLocale()]); + return; + } + if (LED_2P && statusSubMsg[3] == ConstParameter.TestString_Bad) + { + ErrorFrame.Show(__instance, 3401, FaultLED2P[GetLocale()]); + return; + } + + // Camera Check + if (CameraManager.IsReady) + { + var nCam = CameraManager.IsAvailableCameras.Length; + + var pcIdx = _cameraIndex[CameraTypeEnumInner.Photo]; + if (PlayerCamera && pcIdx < nCam && !CameraManager.IsAvailableCameras[pcIdx]) + { + ErrorFrame.Show(__instance, 3102, FaultPlayerCamera[GetLocale()]); + return; + } + + var cr1PIdx = _cameraIndex[CameraTypeEnumInner.QRLeft]; + if (CodeReader_1P && cr1PIdx < nCam && !CameraManager.IsAvailableCameras[cr1PIdx]) + { + ErrorFrame.Show(__instance, 3101, FaultQR1P[GetLocale()]); + return; + } + + var cr2PIdx = _cameraIndex[CameraTypeEnumInner.QRRight]; + if (CodeReader_2P && cr2PIdx < nCam && !CameraManager.IsAvailableCameras[cr2PIdx]) + { + ErrorFrame.Show(__instance, 3101, FaultQR2P[GetLocale()]); + return; + } + + var chimeIdx = _cameraIndex[CameraTypeEnumInner.Chime]; + if (ChimeCamera && chimeIdx < nCam && !CameraManager.IsAvailableCameras[chimeIdx]) + { + ErrorFrame.Show(__instance, 3100, FaultChime[GetLocale()]); + return; + } + } + } + + private static string GetLocale() + { + if (UseOriginalGameLanguage) + { + return GameVersionToLocale[GameInfo.GameId]; + } + MelonLogger.Msg($"[HardwareAlert] Using locale '{Locale.Culture.TwoLetterISOLanguageName}'"); + // MelonLogger.Msg($"[HardwareAlert] Using locale '{Locale.Culture.Name}'"); + return Locale.Culture.TwoLetterISOLanguageName.Equals("zh", + StringComparison.OrdinalIgnoreCase) + ? "zh" + : "en"; + } + + private static readonly Dictionary GameVersionToLocale = new() + { + ["SDEZ"] = "jp", + ["SDGA"] = "en", + ["SDGB"] = "zh", + }; + private static readonly Dictionary FaultTouchSensor1P = new() + { + ["jp"] = "タッチセンサ(1P)はご利用いただけません", + ["en"] = "Touch Sensor (1P) not available", + ["zh"] = "触摸传感器(1P)不可用", + }; + private static readonly Dictionary FaultTouchSensor2P = new() + { + ["jp"] = "タッチセンサ(2P)はご利用いただけません", + ["en"] = "Touch Sensor (2P) not available", + ["zh"] = "触摸传感器(2P)不可用", + }; + private static readonly Dictionary FaultLED1P = new() + { + ["jp"] = "LED(1P)はご利用いただけません", + ["en"] = "LED(1P) not available", + ["zh"] = "LED(1P)不可用", + }; + private static readonly Dictionary FaultLED2P = new() + { + ["jp"] = "LED(2P)はご利用いただけません", + ["en"] = "LED(2P) not available", + ["zh"] = "LED(2P)不可用", + }; + private static readonly Dictionary FaultQR1P = new() + { + ["jp"] = "コードリーダー(1P)はご利用いただけません", + ["en"] = "Code Reader (1P) not available", + ["zh"] = "DX Pass 二维码相机(1P)不可用", // This thing does not exist... + }; + private static readonly Dictionary FaultQR2P = new() + { + ["jp"] = "コードリーダー(2P)はご利用いただけません", + ["en"] = "Code Reader (2P) not available", + ["zh"] = "DX Pass 二维码相机(2P)不可用", // This thing does not exist... + }; + private static readonly Dictionary FaultPlayerCamera = new() + { + ["jp"] = "プレイヤーカメラはご利用いただけません", + ["en"] = "Player Camera not available", + ["zh"] = "玩家相机不可用", + }; + private static readonly Dictionary FaultChime = new() + { + ["jp"] = "コードリーダーは(Chime)ご利用いただけません", // This thing does not exist... + ["en"] = "Code Reader (Chime) not available", // This thing does not exist... + ["zh"] = "二维码相机不可用", + }; +} \ No newline at end of file diff --git a/AquaMai/configSort.yaml b/AquaMai/configSort.yaml index 3c2c7921..65f1e211 100644 --- a/AquaMai/configSort.yaml +++ b/AquaMai/configSort.yaml @@ -75,6 +75,7 @@ - Utils.LogNetworkRequests - Utils.ShowErrorLog - UX.NoAmDaemonAlert + - UX.HardwareAlert 键位和灵敏度: - GameSystem.HidInput