feat: Display error frame when specific hardware is not available. (#51)

* test: complete all essentials for custom hardware display

* feat: complete all types of hardware error checks

* fix: add multiple source for TouchSensor check to avoid occational AMDaemon Error missing issue

* feat: refactored error frame sections into a global helper tool

* fix: resolve potential weakness

* feat: add an localization string

* fix: camera idx fetch issue
This commit is contained in:
AdemJensen 2025-09-15 15:45:23 +08:00 committed by GitHub
parent 2a09d26117
commit eef8a67e2c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 294 additions and 0 deletions

View File

@ -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<ProcessDataContainer>();
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<TextMeshProUGUI>().text = _customErrCode.ToString().PadLeft(4, '0');
tv.Field("ErrorMessage").GetValue<TextMeshProUGUI>().text = _customErrMsg;
tv.Field("ErrorDate").GetValue<TextMeshProUGUI>().text = _customErrDate.ToString();
}
}

View File

@ -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));

View File

@ -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<string> CameraTypeList = ["QRLeft", "QRRight", "Photo", "Chime"];
private static SortedDictionary<CameraTypeEnumInner, int> _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<CameraTypeEnumInner>(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<byte>();
var statusSubMsg = tv.Field("_statusSubMsg").GetValue<string[]>();
// 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<string, string> GameVersionToLocale = new()
{
["SDEZ"] = "jp",
["SDGA"] = "en",
["SDGB"] = "zh",
};
private static readonly Dictionary<string, string> FaultTouchSensor1P = new()
{
["jp"] = "タッチセンサ1Pはご利用いただけません",
["en"] = "Touch Sensor (1P) not available",
["zh"] = "触摸传感器1P不可用",
};
private static readonly Dictionary<string, string> FaultTouchSensor2P = new()
{
["jp"] = "タッチセンサ2Pはご利用いただけません",
["en"] = "Touch Sensor (2P) not available",
["zh"] = "触摸传感器2P不可用",
};
private static readonly Dictionary<string, string> FaultLED1P = new()
{
["jp"] = "LED1Pはご利用いただけません",
["en"] = "LED(1P) not available",
["zh"] = "LED1P不可用",
};
private static readonly Dictionary<string, string> FaultLED2P = new()
{
["jp"] = "LED2Pはご利用いただけません",
["en"] = "LED(2P) not available",
["zh"] = "LED2P不可用",
};
private static readonly Dictionary<string, string> FaultQR1P = new()
{
["jp"] = "コードリーダー1Pはご利用いただけません",
["en"] = "Code Reader (1P) not available",
["zh"] = "DX Pass 二维码相机1P不可用", // This thing does not exist...
};
private static readonly Dictionary<string, string> FaultQR2P = new()
{
["jp"] = "コードリーダー2Pはご利用いただけません",
["en"] = "Code Reader (2P) not available",
["zh"] = "DX Pass 二维码相机2P不可用", // This thing does not exist...
};
private static readonly Dictionary<string, string> FaultPlayerCamera = new()
{
["jp"] = "プレイヤーカメラはご利用いただけません",
["en"] = "Player Camera not available",
["zh"] = "玩家相机不可用",
};
private static readonly Dictionary<string, string> FaultChime = new()
{
["jp"] = "コードリーダーはChimeご利用いただけません", // This thing does not exist...
["en"] = "Code Reader (Chime) not available", // This thing does not exist...
["zh"] = "二维码相机不可用",
};
}

View File

@ -75,6 +75,7 @@
- Utils.LogNetworkRequests
- Utils.ShowErrorLog
- UX.NoAmDaemonAlert
- UX.HardwareAlert
键位和灵敏度:
- GameSystem.HidInput