[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,26 @@
using AquaMai.Config.Attributes;
using AquaMai.Core.Attributes;
using AquaMai.Core.Helpers;
using AquaMai.Core.Resources;
using HarmonyLib;
using Process;
namespace AquaMai.Mods.UX;
[ConfigSection(exampleHidden: true, defaultOn: true)]
[EnableIf(nameof(isCiBuild))]
public class CiBuildAlert
{
# if CI
private static readonly bool isCiBuild = true;
# else
private static readonly bool isCiBuild = false;
# endif
[HarmonyPatch(typeof(AdvertiseProcess), "OnStart")]
[HarmonyPostfix]
public static void OnStart(AdvertiseProcess __instance)
{
MessageHelper.ShowMessage(Locale.CiBuildAlertContent, title: Locale.CiBuildAlertTitle);
}
}

View File

@@ -0,0 +1,136 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AquaMai.Config.Attributes;
using AquaMai.Config.Types;
using AquaMai.Core.Helpers;
using HarmonyLib;
using MAI2.Util;
using Manager;
using MelonLoader;
using Process;
using Util;
namespace AquaMai.Mods.UX;
[ConfigSection(
en: "One key to hide all self-made charts in the music select process. Or hide for some users.",
zh: "在选曲界面一键隐藏所有自制谱,或对一部分用户进行隐藏")]
public class HideSelfMadeCharts
{
[ConfigEntry(
en: "Key to toggle self-made charts.",
zh: "切换自制谱显示的按键")]
public static readonly KeyCodeOrName key = KeyCodeOrName.Test;
[ConfigEntry]
public static readonly bool longPress = false;
[ConfigEntry(
en: "One user ID per line in the file. Hide self-made charts when these users login.",
zh: "该文件中每行一个用户 ID当这些用户登录时隐藏自制谱")]
private static readonly string selfMadeChartsDenyUsersFile = "LocalAssets/SelfMadeChartsDenyUsers.txt";
[ConfigEntry(
en: "One user ID per line in the file. Only show self-made charts when these users login.",
zh: "该文件中每行一个用户 ID只有这些用户登录时才显示自制谱")]
private static readonly string selfMadeChartsWhiteListUsersFile = "LocalAssets/SelfMadeChartsWhiteListUsers.txt";
private static Safe.ReadonlySortedDictionary<int, Manager.MaiStudio.MusicData> _musics;
private static Safe.ReadonlySortedDictionary<int, Manager.MaiStudio.MusicData> _musicsNoneSelfMade;
private static bool isShowSelfMadeCharts = true;
private static bool isForceDisable;
[HarmonyPostfix]
[HarmonyPatch(typeof(DataManager), "GetMusics")]
public static void GetMusics(ref Safe.ReadonlySortedDictionary<int, Manager.MaiStudio.MusicData> __result, List<string> ____targetDirs)
{
if (_musics is null)
{
// init musics for the first time
if (__result.Count == 0) return;
_musics = __result;
var nonSelfMadeList = new SortedDictionary<int, Manager.MaiStudio.MusicData>();
var officialDirs = ____targetDirs.Where(it => File.Exists(Path.Combine(it, "DataConfig.xml")) || File.Exists(Path.Combine(it, "OfficialChartsMark.txt")));
foreach (var music in __result)
{
if (officialDirs.Any(it => MusicDirHelper.LookupPath(music.Value).StartsWith(it)))
{
nonSelfMadeList.Add(music.Key, music.Value);
}
}
_musicsNoneSelfMade = new Safe.ReadonlySortedDictionary<int, Manager.MaiStudio.MusicData>(nonSelfMadeList);
MelonLogger.Msg($"[HideSelfMadeCharts] All music count: {__result.Count}, Official music count: {_musicsNoneSelfMade.Count}");
}
var stackTrace = new StackTrace(); // get call stack
var stackFrames = stackTrace.GetFrames(); // get method calls (frames)
if (stackFrames.All(it => it.GetMethod().DeclaringType.Name != "MusicSelectProcess")) return;
if (isShowSelfMadeCharts && !isForceDisable) return;
__result = _musicsNoneSelfMade;
}
[HarmonyPostfix]
[HarmonyPatch(typeof(MusicSelectProcess), "OnUpdate")]
public static void MusicSelectProcessOnUpdate(ref MusicSelectProcess __instance)
{
if (isForceDisable) return;
if (!KeyListener.GetKeyDownOrLongPress(key, longPress)) return;
isShowSelfMadeCharts = !isShowSelfMadeCharts;
MelonLogger.Msg($"[HideSelfMadeCharts] isShowSelfMadeCharts: {isShowSelfMadeCharts}");
SharedInstances.ProcessDataContainer.processManager.AddProcess(new FadeProcess(SharedInstances.ProcessDataContainer, __instance, new MusicSelectProcess(SharedInstances.ProcessDataContainer)));
Task.Run(async () =>
{
await Task.Delay(1000);
MessageHelper.ShowMessage($"{(isShowSelfMadeCharts ? "Show" : "Hide")} Self-Made Charts");
});
}
[HarmonyPrefix]
[HarmonyPatch(typeof(MusicSelectProcess), "OnStart")]
public static void MusicSelectProcessOnStart(ref MusicSelectProcess __instance)
{
var denyPath = FileSystem.ResolvePath(selfMadeChartsDenyUsersFile);
if (File.Exists(denyPath))
{
var userIds = File.ReadAllLines(denyPath);
for (var i = 0; i < 2; i++)
{
var user = Singleton<UserDataManager>.Instance.GetUserData(i);
if (!user.IsEntry) continue;
if (!userIds.Contains(user.Detail.UserID.ToString())) continue;
isForceDisable = true;
return;
}
}
var whiteListPath = FileSystem.ResolvePath(selfMadeChartsWhiteListUsersFile);
if (File.Exists(whiteListPath))
{
var userIds = File.ReadAllLines(whiteListPath);
for (var i = 0; i < 2; i++)
{
var user = Singleton<UserDataManager>.Instance.GetUserData(i);
if (!user.IsEntry) continue;
if (userIds.Contains(user.Detail.UserID.ToString())) continue;
isForceDisable = true;
return;
}
}
isForceDisable = false;
}
[HarmonyPostfix]
[HarmonyPatch(typeof(EntryProcess), "OnStart")]
public static void EntryProcessOnStart(ref EntryProcess __instance)
{
// reset status on login
isShowSelfMadeCharts = true;
}
}

View File

@@ -0,0 +1,240 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AquaMai.Config.Attributes;
using AquaMai.Core.Helpers;
using AquaMai.Core.Resources;
using DB;
using HarmonyLib;
using MAI2.Util;
using MAI2System;
using Manager;
using Manager.MaiStudio;
using Manager.UserDatas;
using MelonLoader;
using Monitor.Entry.Parts.Screens;
using Net.Packet;
using Net.Packet.Helper;
using Process;
using Process.UserDataNet.State.UserDataULState;
using UnityEngine;
namespace AquaMai.Mods.UX;
[ConfigSection(
en: "Save immediate after playing a song.",
zh: "打完一首歌的时候立即向服务器保存成绩")]
public class ImmediateSave
{
[HarmonyPrefix]
[HarmonyPatch(typeof(StateULUserAime), "RequestUploadUserPlayLogData")]
public static bool PreRequestUploadUserPlayLogData(StateULUserAime __instance)
{
Traverse.Create(__instance).Method("RequestUploadUserPortraitData").GetValue();
return false;
}
private static SavingUi ui;
[HarmonyPostfix]
[HarmonyPatch(typeof(ResultProcess), "OnStart")]
public static void ResultProcessOnStart()
{
var doneCount = 0;
void CheckSaveDone()
{
doneCount++;
if (doneCount != 4) return;
if (ui == null) return;
UnityEngine.Object.Destroy(ui);
ui = null;
}
for (int i = 0; i < 2; i++)
{
var userData = Singleton<UserDataManager>.Instance.GetUserData(i);
if (!userData.IsEntry || userData.IsGuest())
{
doneCount += 2;
continue;
}
if (ui == null)
{
ui = SharedInstances.GameMainObject.gameObject.AddComponent<SavingUi>();
}
SaveDataFix(userData);
PacketHelper.StartPacket(Shim.CreatePacketUploadUserPlaylog(i, userData, (int)GameManager.MusicTrackNumber - 1,
delegate
{
MelonLogger.Msg("[ImmediateSave] Playlog saved");
CheckSaveDone();
},
delegate(PacketStatus err)
{
SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_ENTRY_AIME_ERROR, i);
MelonLogger.Error("[ImmediateSave] Playlog save error");
MelonLogger.Error(err);
MessageHelper.ShowMessage("Playlog save error");
CheckSaveDone();
}));
PacketHelper.StartPacket(Shim.CreatePacketUpsertUserAll(i, userData, delegate(int code)
{
if (code == 1)
{
MelonLogger.Msg("[ImmediateSave] UserAll saved");
CheckSaveDone();
}
else
{
SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_ENTRY_AIME_ERROR, i);
MelonLogger.Error("[ImmediateSave] UserAll upsert error");
MelonLogger.Error(code);
MessageHelper.ShowMessage("UserAll upsert error");
CheckSaveDone();
}
}, delegate(PacketStatus err)
{
SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_ENTRY_AIME_ERROR, i);
MelonLogger.Error("[ImmediateSave] UserAll upsert error");
MelonLogger.Error(err);
MessageHelper.ShowMessage("UserAll upsert error");
CheckSaveDone();
}));
}
}
private static void SaveDataFix(UserData userData)
{
UserDetail detail = userData.Detail;
detail.EventWatchedDate = TimeManager.GetDateString(TimeManager.PlayBaseTime);
userData.CalcTotalValue();
float num = 0f;
try
{
if (userData.RatingList.RatingList.Any())
{
num = userData.RatingList.RatingList.Last().SingleRate;
}
}
catch
{
}
num = (float)Math.Ceiling((double)((num + 1f) / GameManager.TheoryRateBorderNum) * 10.0);
float num2 = 0f;
try
{
if (userData.RatingList.NextRatingList.Any())
{
num2 = userData.RatingList.NewRatingList.Last().SingleRate;
}
}
catch
{
}
num2 = (float)Math.Ceiling((double)((num2 + 1f) / GameManager.TheoryRateBorderNum) * 10.0);
string logDateString = TimeManager.GetLogDateString(TimeManager.PlayBaseTime);
string timeJp = (string.IsNullOrEmpty(userData.Detail.DailyBonusDate) ? TimeManager.GetLogDateString(0L) : userData.Detail.DailyBonusDate);
if (userData.IsEntry && userData.Detail.IsNetMember >= 2 && !GameManager.IsEventMode && TimeManager.GetUnixTime(logDateString) > TimeManager.GetUnixTime(timeJp) && Singleton<UserDataManager>.Instance.IsSingleUser() && !GameManager.IsFreedomMode && !GameManager.IsCourseMode && !DoneEntry.IsWeekdayBonus(userData))
{
userData.Detail.DailyBonusDate = logDateString;
}
List<UserRate> list = new List<UserRate>();
List<UserRate> list2 = new List<UserRate>();
IEnumerable<UserScore>[] scoreList = Shim.GetUserScoreList(userData);
List<UserRate> ratingList = userData.RatingList.RatingList;
List<UserRate> newRatingList = userData.RatingList.NewRatingList;
int achive = RatingTableID.Rate_22.GetAchive();
for (int j = 0; j < scoreList.Length; j++)
{
if (scoreList[j] == null)
{
continue;
}
foreach (UserScore item2 in scoreList[j])
{
if (achive <= item2.achivement)
{
continue;
}
MusicData music = Singleton<DataManager>.Instance.GetMusic(item2.id);
if (music == null)
{
continue;
}
UserRate item = new UserRate(item2.id, j, item2.achivement, (uint)music.version);
if (item.OldFlag)
{
if (num <= (float)item.Level && !ratingList.Contains(item))
{
list.Add(item);
}
}
else if (num2 <= (float)item.Level && !newRatingList.Contains(item))
{
list2.Add(item);
}
}
}
list.Sort();
list.Reverse();
if (list.Count > 10)
{
list.RemoveRange(10, list.Count - 10);
}
userData.RatingList.NextRatingList = list;
list2.Sort();
list2.Reverse();
if (list2.Count > 10)
{
list2.RemoveRange(10, list2.Count - 10);
}
userData.RatingList.NextNewRatingList = list2;
userData.Detail.LastPlayCredit = 0;
userData.Detail.LastPlayMode = 0;
if (GameManager.IsFreedomMode)
{
userData.Detail.LastPlayMode = 1;
}
if (GameManager.IsCourseMode)
{
userData.Detail.LastPlayMode = 2;
}
userData.Detail.LastGameId = ConstParameter.GameIDStr;
userData.Detail.LastRomVersion = Singleton<SystemConfig>.Instance.config.romVersionInfo.versionNo.versionString;
userData.Detail.LastDataVersion = Singleton<SystemConfig>.Instance.config.dataVersionInfo.versionNo.versionString;
}
private class SavingUi : MonoBehaviour
{
public void OnGUI()
{
var y = Screen.height * .075f;
var width = GuiSizes.FontSize * 20f;
var x = GuiSizes.PlayerCenter + GuiSizes.PlayerWidth / 2f - width;
var rect = new Rect(x, y, width, GuiSizes.LabelHeight * 2.5f);
var labelStyle = GUI.skin.GetStyle("label");
labelStyle.fontSize = (int)(GuiSizes.FontSize * 1.2);
labelStyle.alignment = TextAnchor.MiddleCenter;
GUI.Box(rect, "");
GUI.Label(rect, Locale.SavingDontExit);
}
}
}

View File

@@ -0,0 +1,247 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using AquaMai.Config.Attributes;
using HarmonyLib;
using Manager;
using Monitor;
using Monitor.Result;
using Process;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
namespace AquaMai.Mods.UX;
[ConfigSection(
zh: "在游戏总结的计分板中显示击打误差的详细信息(以帧为单位)",
en: "Show detailed accuracy info in the score board.")]
public class JudgeAccuracyInfo
{
public class AccuracyEntryList
{
public List<float>[] DiffList = new List<float>[TableRowNames.Length];
public List<float>[] RawDiffList = new List<float>[TableRowNames.Length];
public HashSet<int> NoteIndices = new();
public AccuracyEntryList()
{
for (int i = 0; i < TableRowNames.Length; i++)
{
DiffList[i] = new List<float>();
RawDiffList[i] = new List<float>();
}
}
}
public static AccuracyEntryList[] EntryList = new AccuracyEntryList[2];
[HarmonyPostfix]
[HarmonyPatch(typeof(GameProcess), "OnStart")]
private static void OnGameProcessStartFinish()
{
for (int i = 0; i < EntryList.Length; i++)
{
EntryList[i] = new AccuracyEntryList();
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(ResultProcess), "OnRelease")]
private static void OnResultProcessReleaseFinish()
{
for (int i = 0; i < EntryList.Length; i++)
{
EntryList[i] = null;
Controllers[i] = null;
}
}
[HarmonyPatch]
public static class NoteBaseJudgePatch
{
public static IEnumerable<MethodBase> TargetMethods()
{
return
[
AccessTools.Method(typeof(NoteBase), "Judge"),
AccessTools.Method(typeof(HoldNote), "JudgeHoldHead"),
AccessTools.Method(typeof(BreakHoldNote), "JudgeHoldHead"),
AccessTools.Method(typeof(TouchNoteB), "Judge"),
AccessTools.Method(typeof(TouchHoldC), "JudgeHoldHead"),
];
}
public static void Postfix(
NoteBase __instance, bool __result,
float ___JudgeTimingDiffMsec, float ___AppearMsec, NoteJudge.EJudgeType ___JudgeType, int ___NoteIndex
)
{
var monitor = __instance.MonitorId;
if (!__result || EntryList[monitor].NoteIndices.Contains(___NoteIndex)) return;
EntryList[monitor].NoteIndices.Add(___NoteIndex);
var raw = (NotesManager.GetCurrentMsec() - ___AppearMsec) - NoteJudge.JudgeAdjustMs;
switch (___JudgeType)
{
case NoteJudge.EJudgeType.Tap:
case NoteJudge.EJudgeType.Break:
{
EntryList[monitor].DiffList[0].Add(___JudgeTimingDiffMsec);
EntryList[monitor].RawDiffList[0].Add(raw);
break;
}
case NoteJudge.EJudgeType.Touch:
{
EntryList[monitor].DiffList[2].Add(___JudgeTimingDiffMsec);
EntryList[monitor].RawDiffList[2].Add(raw);
break;
}
case NoteJudge.EJudgeType.ExTap:
{
EntryList[monitor].DiffList[3].Add(___JudgeTimingDiffMsec);
EntryList[monitor].RawDiffList[3].Add(raw);
break;
}
}
// MelonLogger.Msg($"{___JudgeType}: {___JudgeTimingDiffMsec}, {raw}");
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(SlideRoot), "Judge")]
private static void SlideRootJudgePatch(
SlideRoot __instance, bool __result,
float ___JudgeTimingDiffMsec, float ___TailMsec, float ___lastWaitTimeForJudge,
NoteJudge.EJudgeType ___JudgeType, int ___NoteIndex
)
{
var monitor = __instance.MonitorId;
if (!__result || EntryList[monitor].NoteIndices.Contains(___NoteIndex)) return;
EntryList[monitor].NoteIndices.Add(___NoteIndex);
var raw = (NotesManager.GetCurrentMsec() - ___TailMsec + ___lastWaitTimeForJudge) - NoteJudge.JudgeAdjustMs;
EntryList[monitor].DiffList[1].Add(___JudgeTimingDiffMsec - NoteJudge.JudgeAdjustMs);
EntryList[monitor].RawDiffList[1].Add(raw);
// MelonLogger.Msg($"{___JudgeType}: {___JudgeTimingDiffMsec}, {raw}");
}
[HarmonyPostfix]
[HarmonyPatch(typeof(ResultProcess), "OnStart")]
private static void OnResultProcessStartFinish(
ResultMonitor[] ____monitors, ResultProcess.ResultScoreViewType[] ____resultScoreViewType, UserData[] ____userData
)
{
foreach (var monitor in ____monitors)
{
var idx = monitor.MonitorIndex;
if (!____userData[idx].IsEntry) continue;
var fileName = $"Acc_Track_{GameManager.MusicTrackNumber}_Player_{idx}.txt";
var filePath = Path.Combine(Environment.CurrentDirectory, fileName);
using (var writer = new StreamWriter(filePath))
{
for (int i = 0; i < TableRowNames.Length; i++)
{
writer.WriteLine($"Row: {TableRowNames[i]}");
writer.WriteLine(" DiffList:");
writer.WriteLine($" {string.Join(", ", EntryList[idx].DiffList[i])}");
writer.WriteLine(" RawDiffList:");
writer.WriteLine($" {string.Join(", ", EntryList[idx].RawDiffList[i])}");
writer.WriteLine();
}
}
var controller = Traverse.Create(monitor).Field<ScoreBoardController>("_scoreBoardController").Value;
var newController = Object.Instantiate(controller, controller.transform);
newController.gameObject.GetComponent<Animator>().enabled = false;
newController.transform.localPosition = Vector3.zero;
var table = ExtractTextObjs(newController);
for (var i = 0; i < TableHead.Length; i++)
{
table[0, i].text = TableHead[i];
}
for (var i = 0; i < TableRowNames.Length; i++)
{
table[i + 1, 0].text = TableRowNames[i];
var num = EntryList[idx].DiffList[i].Count;
table[i + 1, 1].text = num.ToString();
if (num <= 0)
{
table[i + 1, 2].text = "——";
table[i + 1, 3].text = "——";
table[i + 1, 4].text = "——";
continue;
}
var average = EntryList[idx].DiffList[i].Average();
var averageFrame = average * 0.06f;
table[i + 1, 2].text = averageFrame.ToString("+0.00;-0.00;0.00", CultureInfo.InvariantCulture);
var averageRawFrame = EntryList[idx].RawDiffList[i].Average() * 0.06f;
table[i + 1, 3].text = averageRawFrame.ToString("+0.00;-0.00;0.00", CultureInfo.InvariantCulture);
if (num <= 1)
{
table[i + 1, 4].text = "——";
}
else
{
var deviSqr = EntryList[idx].DiffList[i].Sum(x => (x - average) * (x - average)) / (num - 1);
var devi = Mathf.Sqrt(deviSqr) * 0.06f;
table[i + 1, 4].text = devi.ToString("0.00", CultureInfo.InvariantCulture);
}
}
newController.gameObject.SetActive(____resultScoreViewType[idx] == ResultProcess.ResultScoreViewType.VSResult);
Controllers[idx] = newController;
}
}
private static readonly ScoreBoardController[] Controllers = new ScoreBoardController[2];
[HarmonyPostfix]
[HarmonyPatch(typeof(ResultMonitor), "ChangeScoreBoard")]
private static void OnChangeScoreBoard(
ResultMonitor __instance, ResultProcess.ResultScoreViewType resultScoreType
)
{
Controllers[__instance.MonitorIndex].gameObject.SetActive(resultScoreType == ResultProcess.ResultScoreViewType.VSResult);
}
private static readonly string[] RowNames = ["_tap", "_hold", "_slide", "_touch", "_break"];
private static readonly string[] ColumnNames = ["_critical", "_perfect", "_great", "_good", "_miss"];
private static readonly string[] TableHead = ["", "NUM", "AVG", "RAW", "S.D."];
private static readonly string[] TableRowNames = ["TAP", "SLD", "TCH", "EX"];
private static TextMeshProUGUI[,] ExtractTextObjs(ScoreBoardController controller)
{
var result = new TextMeshProUGUI[RowNames.Length, ColumnNames.Length];
for (var i = 0; i < RowNames.Length; i++)
{
for (int j = 0; j < ColumnNames.Length; j++)
{
var trav = Traverse.Create(controller)
.Field(RowNames[i])
.Field(ColumnNames[j]);
var text = trav.Field<TextMeshProUGUI>("_numberText").Value;
text.color = Color.black;
result[i, j] = text;
trav.GetValue<ScoreBoardColumnObject>().SetVisibleCloseBox(false);
}
}
return result;
}
}

View File

@@ -0,0 +1,65 @@
using System.Collections.Generic;
using AquaMai.Config.Attributes;
using AquaMai.Config.Types;
using AquaMai.Core.Helpers;
using HarmonyLib;
using Mai2.Mai2Cue;
using Main;
using Manager;
using MelonLoader;
using Process;
namespace AquaMai.Mods.UX;
[ConfigSection(
en: "One key to proceed to music select (during entry) or end current PC (during music select).",
zh: "一键跳过登录过程直接进入选歌界面,或在选歌界面直接结束本局游戏")]
public class OneKeyEntryEnd
{
[ConfigEntry]
public static readonly KeyCodeOrName key = KeyCodeOrName.Service;
[ConfigEntry]
public static readonly bool longPress = true;
[HarmonyPrefix]
[HarmonyPatch(typeof(GameMainObject), "Update")]
public static void OnGameMainObjectUpdate()
{
if (!KeyListener.GetKeyDownOrLongPress(key, longPress)) return;
MelonLogger.Msg("[QuickSkip] Activated");
var traverse = Traverse.Create(SharedInstances.ProcessDataContainer.processManager);
var processList = traverse.Field("_processList").GetValue<LinkedList<ProcessManager.ProcessControle>>();
ProcessBase processToRelease = null;
foreach (ProcessManager.ProcessControle process in processList)
{
switch (process.Process.ToString())
{
// After login
case "Process.ModeSelect.ModeSelectProcess":
case "Process.LoginBonus.LoginBonusProcess":
case "Process.RegionalSelectProcess":
case "Process.CharacterSelectProcess":
case "Process.TicketSelect.TicketSelectProcess":
processToRelease = process.Process;
break;
case "Process.MusicSelectProcess":
// Skip to save
SoundManager.PreviewEnd();
SoundManager.PlayBGM(Cue.BGM_COLLECTION, 2);
SharedInstances.ProcessDataContainer.processManager.AddProcess(new FadeProcess(SharedInstances.ProcessDataContainer, process.Process, new UnlockMusicProcess(SharedInstances.ProcessDataContainer)));
break;
}
}
if (processToRelease != null)
{
GameManager.SetMaxTrack();
SharedInstances.ProcessDataContainer.processManager.AddProcess(new FadeProcess(SharedInstances.ProcessDataContainer, processToRelease, new MusicSelectProcess(SharedInstances.ProcessDataContainer)));
}
}
}

View File

@@ -0,0 +1,46 @@
using AquaMai.Config.Attributes;
using AquaMai.Config.Types;
using AquaMai.Core.Helpers;
using HarmonyLib;
using MAI2.Util;
using Manager;
using Process;
namespace AquaMai.Mods.UX;
[ConfigSection(
en: "One key to retry (1.30+) or skip current chart in gameplay.",
zh: "在游戏中途一键重试1.30+)或跳过当前谱面")]
public class OneKeyRetrySkip
{
[ConfigEntry]
public static readonly KeyCodeOrName retryKey = KeyCodeOrName.Service;
[ConfigEntry]
public static readonly bool retryLongPress = false;
[ConfigEntry]
public static readonly KeyCodeOrName skipKey = KeyCodeOrName.Service;
[ConfigEntry]
public static readonly bool skipLongPress = true;
[HarmonyPostfix]
[HarmonyPatch(typeof(GameProcess), "OnUpdate")]
public static void PostGameProcessUpdate(GameProcess __instance, Message[] ____message, ProcessDataContainer ___container)
{
if (KeyListener.GetKeyDownOrLongPress(skipKey, skipLongPress))
{
var traverse = Traverse.Create(__instance);
___container.processManager.SendMessage(____message[0]);
Singleton<GamePlayManager>.Instance.SetSyncResult(0);
traverse.Method("SetRelease").GetValue();
}
if (KeyListener.GetKeyDownOrLongPress(retryKey, retryLongPress) && GameInfo.GameVersion >= 23000)
{
// This is original typo in Assembly-CSharp
Singleton<GamePlayManager>.Instance.SetQuickRetryFrag(flag: true);
}
}
}

View File

@@ -0,0 +1,151 @@
using System;
using AquaMai.Mods.Fix;
using AquaMai.Core.Helpers;
using AquaMai.Core.Resources;
using Manager;
using UnityEngine;
namespace AquaMai.Mods.UX.PracticeMode.Libs;
public class PracticeModeUI : MonoBehaviour
{
private static float windowTop => Screen.height - GuiSizes.PlayerWidth + GuiSizes.PlayerWidth * .22f;
private static float controlHeight => GuiSizes.PlayerWidth * .13f;
private static float sideButtonWidth => GuiSizes.PlayerWidth * .1f;
private static float centerButtonWidth => GuiSizes.PlayerWidth * .28f;
private static int fontSize => (int)(GuiSizes.PlayerWidth * .02f);
private static Rect GetButtonRect(int pos, int row)
{
float x;
float width;
switch (pos)
{
case 0:
x = GuiSizes.PlayerCenter - centerButtonWidth / 2 - sideButtonWidth - GuiSizes.Margin;
width = sideButtonWidth;
break;
case 1:
x = GuiSizes.PlayerCenter - centerButtonWidth / 2;
width = centerButtonWidth;
break;
case 2:
x = GuiSizes.PlayerCenter + centerButtonWidth / 2 + GuiSizes.Margin;
width = sideButtonWidth;
break;
default:
throw new ArgumentOutOfRangeException(nameof(pos), pos, null);
}
return new Rect(x, windowTop + (GuiSizes.Margin + controlHeight) * row + GuiSizes.Margin, width, controlHeight);
}
public void OnGUI()
{
var labelStyle = GUI.skin.GetStyle("label");
labelStyle.fontSize = fontSize;
labelStyle.alignment = TextAnchor.MiddleCenter;
var buttonStyle = GUI.skin.GetStyle("button");
buttonStyle.fontSize = fontSize;
GUI.Box(new Rect(
GuiSizes.PlayerCenter - centerButtonWidth / 2 - sideButtonWidth - GuiSizes.Margin * 2,
windowTop,
centerButtonWidth + sideButtonWidth * 2 + GuiSizes.Margin * 4,
controlHeight * 4 + GuiSizes.Margin * 5
), "");
GUI.Button(GetButtonRect(0, 0), Locale.SeekBackward);
GUI.Button(GetButtonRect(1, 0), Locale.Pause);
GUI.Button(GetButtonRect(2, 0), Locale.SeekForward);
if (PracticeMode.repeatStart == -1)
{
GUI.Button(GetButtonRect(0, 1), Locale.MarkRepeatStart);
GUI.Label(GetButtonRect(1, 1), Locale.RepeatNotSet);
}
else if (PracticeMode.repeatEnd == -1)
{
GUI.Button(GetButtonRect(0, 1), Locale.MarkRepeatEnd);
GUI.Label(GetButtonRect(1, 1), Locale.RepeatStartSet);
GUI.Button(GetButtonRect(2, 1), Locale.RepeatReset);
}
else
{
GUI.Label(GetButtonRect(1, 1), Locale.RepeatStartEndSet);
GUI.Button(GetButtonRect(2, 1), Locale.RepeatReset);
}
GUI.Button(GetButtonRect(0, 2), Locale.SpeedDown);
GUI.Label(GetButtonRect(1, 2), $"{Locale.Speed} {PracticeMode.speed * 100:000}%");
GUI.Button(GetButtonRect(2, 2), Locale.SpeedUp);
GUI.Button(GetButtonRect(1, 3), Locale.SpeedReset);
GUI.Label(GetButtonRect(0, 3), $"{TimeSpan.FromMilliseconds(PracticeMode.CurrentPlayMsec):mm\\:ss\\.fff}\n{TimeSpan.FromMilliseconds(NotesManager.Instance().getPlayFinalMsec()):mm\\:ss\\.fff}");
GUI.Button(GetButtonRect(2, 3), $"保持流速\n{(PracticeMode.keepNoteSpeed ? "ON" : "OFF")}");
}
public void Update()
{
if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.E8))
{
PracticeMode.Seek(-1000);
}
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.E2))
{
PracticeMode.Seek(1000);
}
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B8) || InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B1))
{
DebugFeature.Pause = !DebugFeature.Pause;
if (!DebugFeature.Pause)
{
PracticeMode.Seek(0);
}
}
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B7) && PracticeMode.repeatStart == -1)
{
PracticeMode.repeatStart = PracticeMode.CurrentPlayMsec;
}
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B7) && PracticeMode.repeatEnd == -1)
{
PracticeMode.SetRepeatEnd(PracticeMode.CurrentPlayMsec);
}
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B2))
{
PracticeMode.ClearRepeat();
}
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B6))
{
PracticeMode.SpeedDown();
}
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B3))
{
PracticeMode.SpeedUp();
}
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B5) || InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B4))
{
PracticeMode.SpeedReset();
}
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.E4))
{
PracticeMode.keepNoteSpeed = !PracticeMode.keepNoteSpeed;
PracticeMode.gameCtrl?.ResetOptionSpeed();
}
else if (
InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.A1) ||
InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.A2) ||
InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.A3) ||
InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.A4) ||
InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.A5) ||
InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.A6) ||
InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.A7) ||
InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.A8)
)
{
PracticeMode.ui = null;
Destroy(this);
}
}
}

View File

@@ -0,0 +1,282 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using AquaMai.Mods.Fix;
using AquaMai.Core.Helpers;
using AquaMai.Core.Resources;
using AquaMai.Mods.UX.PracticeMode.Libs;
using HarmonyLib;
using Manager;
using Monitor;
using Monitor.Game;
using Process;
using UnityEngine;
using AquaMai.Config.Attributes;
using AquaMai.Config.Types;
namespace AquaMai.Mods.UX.PracticeMode;
[ConfigCollapseNamespace]
[ConfigSection(
en: "Practice Mode.",
zh: "练习模式")]
public class PracticeMode
{
[ConfigEntry(
en: "Key to show Practice Mode UI.",
zh: "显示练习模式 UI 的按键")]
public static readonly KeyCodeOrName key = KeyCodeOrName.Test;
[ConfigEntry]
public static readonly bool longPress = false;
public static double repeatStart = -1;
public static double repeatEnd = -1;
public static float speed = 1;
private static CriAtomExPlayer player;
private static MovieMaterialMai2 movie;
public static GameCtrl gameCtrl;
public static bool keepNoteSpeed = false;
public static void SetRepeatEnd(double time)
{
if (repeatStart == -1)
{
MessageHelper.ShowMessage(Locale.RepeatStartTimeNotSet);
return;
}
if (time < repeatStart)
{
MessageHelper.ShowMessage(Locale.RepeatEndTimeLessThenStartTime);
return;
}
repeatEnd = time;
}
public static void ClearRepeat()
{
repeatStart = -1;
repeatEnd = -1;
}
public static void SetSpeed()
{
player.SetPitch((float)(1200 * Math.Log(speed, 2)));
// player.SetDspTimeStretchRatio(1 / speed);
player.UpdateAll();
movie.player.SetSpeed(speed);
gameCtrl?.ResetOptionSpeed();
}
private static IEnumerator SetSpeedCoroutineInner()
{
yield return null;
SetSpeed();
}
public static void SetSpeedCoroutine()
{
SharedInstances.GameMainObject.StartCoroutine(SetSpeedCoroutineInner());
}
public static void SpeedUp()
{
speed += .05f;
if (speed > 2)
{
speed = 2;
}
SetSpeed();
}
public static void SpeedDown()
{
speed -= .05f;
if (speed < 0.05)
{
speed = 0.05f;
}
SetSpeed();
}
public static void SpeedReset()
{
speed = 1;
SetSpeed();
}
public static void Seek(int addMsec)
{
// Debug feature 里面那个 timer 不能感知变速
// 为了和魔改版本统一polyfill 里面不修这个
// 这里重新实现一个能感知变速的 Seek
var msec = CurrentPlayMsec + addMsec;
if (msec < 0)
{
msec = 0;
}
CurrentPlayMsec = msec;
}
public static double CurrentPlayMsec
{
get => NotesManager.GetCurrentMsec() - 91;
set
{
DebugFeature.CurrentPlayMsec = value;
SetSpeedCoroutine();
}
}
public static PracticeModeUI ui;
[HarmonyPatch]
public class PatchNoteSpeed
{
public static IEnumerable<MethodBase> TargetMethods()
{
yield return AccessTools.Method(typeof(GameManager), "GetNoteSpeed");
yield return AccessTools.Method(typeof(GameManager), "GetTouchSpeed");
}
public static void Postfix(ref float __result)
{
if (!keepNoteSpeed) return;
__result /= speed;
}
}
[HarmonyPatch(typeof(GameProcess), "OnStart")]
[HarmonyPostfix]
public static void GameProcessPostStart()
{
repeatStart = -1;
repeatEnd = -1;
speed = 1;
ui = null;
}
[HarmonyPatch(typeof(GameProcess), "OnRelease")]
[HarmonyPostfix]
public static void GameProcessPostRelease()
{
repeatStart = -1;
repeatEnd = -1;
speed = 1;
ui = null;
}
[HarmonyPatch(typeof(GameCtrl), "Initialize")]
[HarmonyPostfix]
public static void GameCtrlPostInitialize(GameCtrl __instance)
{
gameCtrl = __instance;
}
# if DEBUG
[HarmonyPrefix]
[HarmonyPatch(typeof(GenericProcess), "OnUpdate")]
public static void OnGenericProcessUpdate(GenericMonitor[] ____monitors)
{
if (Input.GetKeyDown(KeyCode.F11))
{
____monitors[0].gameObject.AddComponent<PracticeModeUI>();
}
}
# endif
[HarmonyPatch(typeof(GameProcess), "OnUpdate")]
[HarmonyPostfix]
public static void GameProcessPostUpdate(GameProcess __instance, GameMonitor[] ____monitors)
{
if (KeyListener.GetKeyDownOrLongPress(key, longPress) && ui is null)
{
ui = ____monitors[0].gameObject.AddComponent<PracticeModeUI>();
}
if (repeatStart >= 0 && repeatEnd >= 0)
{
if (CurrentPlayMsec >= repeatEnd)
{
CurrentPlayMsec = repeatStart;
}
}
}
private static float startGap = -1f;
[HarmonyPatch(typeof(NotesManager), "StartPlay")]
[HarmonyPostfix]
public static void NotesManagerPostUpdateTimer(float msecStartGap)
{
startGap = msecStartGap;
}
[HarmonyPatch(typeof(NotesManager), "UpdateTimer")]
[HarmonyPrefix]
public static bool NotesManagerPostUpdateTimer(bool ____isPlaying, Stopwatch ____stopwatch, ref float ____curMSec, ref float ____curMSecPre, float ____msecStartGap)
{
var stackTrace = new StackTrace(); // get call stack
var stackFrames = stackTrace.GetFrames(); // get method calls (frames)
if(stackFrames.Select(it => it.GetMethod().DeclaringType.Name).Contains("AdvDemoProcess"))
{
return true;
}
if (startGap != -1f)
{
____curMSec = startGap;
____curMSecPre = startGap;
____stopwatch?.Reset();
startGap = -1f;
}
else
{
____curMSecPre = ____curMSec;
if (____isPlaying && ____stopwatch != null && !DebugFeature.Pause)
{
var num = (double)____stopwatch.ElapsedTicks / Stopwatch.Frequency * 1000.0 * speed;
____curMSec += (float)num;
____stopwatch.Reset();
____stopwatch.Start();
}
}
return false;
}
[HarmonyPatch(typeof(SoundCtrl), "Initialize")]
[HarmonyPostfix]
public static void SoundCtrlPostInitialize(SoundCtrl.InitParam param, Dictionary<int, object> ____players)
{
var wrapper = ____players[2];
player = (CriAtomExPlayer)wrapper.GetType().GetField("Player").GetValue(wrapper);
// var pool = new CriAtomExStandardVoicePool(1, 8, 96000, true, 2);
// pool.AttachDspTimeStretch();
// player.SetVoicePoolIdentifier(pool.identifier);
// debug
// var wrapper1 = ____players[7];
// var player1 = (CriAtomExPlayer)wrapper1.GetType().GetField("Player").GetValue(wrapper1);
// var pool = new CriAtomExStandardVoicePool(1, 8, 96000, true, 2);
// pool.AttachDspTimeStretch();
// player1.SetVoicePoolIdentifier(pool.identifier);
// player1.SetDspTimeStretchRatio(2);
}
[HarmonyPatch(typeof(MovieController), "Awake")]
[HarmonyPostfix]
public static void MovieControllerPostAwake(MovieMaterialMai2 ____moviePlayers)
{
movie = ____moviePlayers;
}
}

View File

@@ -0,0 +1,70 @@
using AquaMai.Config.Attributes;
using AquaMai.Core.Helpers;
using AquaMai.Core.Resources;
using HarmonyLib;
using MAI2.Util;
using Manager;
using Monitor;
using Process;
using UnityEngine;
namespace AquaMai.Mods.UX;
[ConfigSection(
en: "Show a \"skip\" button like AstroDX after the notes end.",
zh: "音符结束之后显示像 AstroDX 一样的「跳过」按钮")]
public class QuickEndPlay
{
private static int _timer;
[HarmonyPatch(typeof(GameProcess), "OnStart")]
[HarmonyPostfix]
public static void GameProcessPostStart(GameMonitor[] ____monitors)
{
_timer = 0;
____monitors[0].gameObject.AddComponent<Ui>();
}
[HarmonyPatch(typeof(GameProcess), "OnUpdate")]
[HarmonyPostfix]
public static void GameProcessPostUpdate(GameProcess __instance, Message[] ____message, ProcessDataContainer ___container, byte ____sequence)
{
switch (____sequence)
{
case 9:
_timer = 0;
break;
case > 4:
_timer++;
break;
default:
_timer = 0;
break;
}
if (_timer > 60 && (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B4) || InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.E4)))
{
var traverse = Traverse.Create(__instance);
___container.processManager.SendMessage(____message[0]);
Singleton<GamePlayManager>.Instance.SetSyncResult(0);
traverse.Method("SetRelease").GetValue();
}
}
private class Ui : MonoBehaviour
{
public void OnGUI()
{
if (_timer < 60) return;
// 这里重新 setup 一下 style 也可以
var x = GuiSizes.PlayerCenter;
var y = Screen.height - GuiSizes.PlayerWidth * .37f;
var width = GuiSizes.PlayerWidth * .25f;
var height = GuiSizes.PlayerWidth * .13f;
GUI.Box(new Rect(x, y, width, height), "");
GUI.Button(new Rect(x, y, width, height), Locale.Skip);
}
}
}

View File

@@ -0,0 +1,3 @@
# UX
Features aiming at improving the user experience and can't fit in other categories go here. Generally, UX features are triggered by some user action or let the user perceive.

View File

@@ -0,0 +1,143 @@
using System.Collections.Generic;
using System.Linq;
using AquaMai.Core.Attributes;
using AquaMai.Config.Attributes;
using AquaMai.Core.Helpers;
using AquaMai.Core.Resources;
using HarmonyLib;
using MAI2.Util;
using Manager;
using Manager.MaiStudio;
using Manager.UserDatas;
using Monitor;
using Process;
using UnityEngine;
namespace AquaMai.Mods.UX;
[EnableGameVersion(23500)]
[ConfigSection(
en: "Show detail of selected song in music selection screen.",
zh: "选歌界面显示选择的歌曲的详情")]
public class SelectionDetail
{
private static readonly Window[] window = new Window[2];
private static MusicSelectProcess.MusicSelectData SelectData { get; set; }
private static readonly int[] difficulty = new int[2];
[HarmonyPostfix]
[HarmonyPatch(typeof(MusicSelectMonitor), "UpdateRivalScore")]
public static void ScrollUpdate(MusicSelectProcess ____musicSelect, MusicSelectMonitor __instance)
{
int player;
if (__instance == ____musicSelect.MonitorArray[0])
{
player = 0;
}
else if (__instance == ____musicSelect.MonitorArray[1])
{
player = 1;
}
else
{
return;
}
if (window[player] != null)
{
window[player].Close();
}
var userData = Singleton<UserDataManager>.Instance.GetUserData(player);
if (!userData.IsEntry) return;
if (____musicSelect.IsRandomIndex()) return;
SelectData = ____musicSelect.GetMusic(0);
if (SelectData == null) return;
difficulty[player] = ____musicSelect.GetDifficulty(player);
window[player] = player == 0 ? __instance.gameObject.AddComponent<P1Window>() : __instance.gameObject.AddComponent<P2Window>();
}
private class P1Window : Window
{
protected override int player => 0;
}
private class P2Window : Window
{
protected override int player => 1;
}
private abstract class Window : MonoBehaviour
{
protected abstract int player { get; }
private UserData userData => Singleton<UserDataManager>.Instance.GetUserData(player);
public void OnGUI()
{
var dataToShow = new List<string>();
dataToShow.Add($"ID: {SelectData.MusicData.name.id}");
dataToShow.Add(MusicDirHelper.LookupPath(SelectData.MusicData.name.id).Split('/').Reverse().ToArray()[3]);
if (SelectData.MusicData.genreName is not null) // SelectData.MusicData.genreName.str may not correct
dataToShow.Add(Singleton<DataManager>.Instance.GetMusicGenre(SelectData.MusicData.genreName.id)?.genreName);
if (SelectData.MusicData.AddVersion is not null)
dataToShow.Add(Singleton<DataManager>.Instance.GetMusicVersion(SelectData.MusicData.AddVersion.id)?.genreName);
var notesData = SelectData.MusicData.notesData[difficulty[player]];
dataToShow.Add($"{notesData?.level}.{notesData?.levelDecimal}");
var rate = CalcB50(SelectData.MusicData, difficulty[player]);
if (rate > 0)
{
dataToShow.Add(string.Format(Locale.RatingUpWhenSSSp, rate));
}
var playCount = Shim.GetUserScoreList(userData)[difficulty[player]].FirstOrDefault(it => it.id == SelectData.MusicData.name.id)?.playcount ?? 0;
if (playCount > 0)
{
dataToShow.Add(string.Format(Locale.PlayCount, playCount));
}
var width = GuiSizes.FontSize * 15f;
var x = GuiSizes.PlayerCenter - width / 2f + GuiSizes.PlayerWidth * player;
var y = Screen.height * 0.87f;
var labelStyle = GUI.skin.GetStyle("label");
labelStyle.fontSize = GuiSizes.FontSize;
labelStyle.alignment = TextAnchor.MiddleCenter;
GUI.Box(new Rect(x, y, width, dataToShow.Count * GuiSizes.LabelHeight + 2 * GuiSizes.Margin), "");
for (var i = 0; i < dataToShow.Count; i++)
{
GUI.Label(new Rect(x, y + GuiSizes.Margin + i * GuiSizes.LabelHeight, width, GuiSizes.LabelHeight), dataToShow[i]);
}
}
private uint CalcB50(MusicData musicData, int difficulty)
{
var theory = new UserRate(musicData.name.id, difficulty, 1010000, (uint)musicData.version);
var list = theory.OldFlag ? userData.RatingList.RatingList : userData.RatingList.NewRatingList;
var userLowRate = list.Last();
var userSongRate = list.FirstOrDefault(it => it.MusicId == musicData.name.id && it.Level == difficulty);
if (!userSongRate.Equals(default(UserRate)))
{
return theory.SingleRate - userSongRate.SingleRate;
}
if (theory.SingleRate > userLowRate.SingleRate)
{
return theory.SingleRate - userLowRate.SingleRate;
}
return 0;
}
public void Close()
{
Destroy(this);
}
}
}