mirror of https://github.com/hykilpikonna/AquaDX
426 lines
18 KiB
C#
426 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Reflection.Emit;
|
|
using DB;
|
|
using HarmonyLib;
|
|
using MAI2.Util;
|
|
using Manager;
|
|
using MelonLoader;
|
|
using Monitor;
|
|
using UnityEngine;
|
|
|
|
namespace AquaMai.MaimaiDX2077;
|
|
|
|
public class CustomNoteTypePatch
|
|
{
|
|
/*
|
|
* ========== ========== ========== ========== ========== ========== ========== ==========
|
|
* 以下内容是为了添加新的 MA2 语法用于表示自定义的 note 类型
|
|
* The following part is to add new MA2 command to Sinmai (representing custom note types)
|
|
*
|
|
* New note types:
|
|
* 1. Slide Super-new Super-hot (NMSSS, BRSSS, EXSSS, BXSSS, CNSSS):
|
|
* Definition: ??SSS [bar] [grid] [start pos] [wait] [duration] [end pos] [slide code (string)]
|
|
* Represent a slide note with highly customized path (using slide code)
|
|
*
|
|
* TODO (?)
|
|
* Mine notes (P.S. Mine-slides will automatically progress itself)
|
|
* Individual tracing duration in conn. slides
|
|
* Touch-slides / slides not ending in group A
|
|
* Non-C TouchHold
|
|
* Spinning tailless star (something like 1$$)
|
|
* Hyper Speed Definition ?
|
|
*/
|
|
public static int TotalMa2RecordCount = -1;
|
|
public static int LastMa2RecordID = -1;
|
|
public static Array Ma2FileRecordData;
|
|
|
|
public static void DoCustomPatch(HarmonyLib.Harmony h)
|
|
{
|
|
var arrayTraverse = Traverse.Create(typeof(Ma2fileRecordID)).Field("s_Ma2fileRecord_Data");
|
|
var targetArray = arrayTraverse.GetValue<Array>();
|
|
|
|
var nextId = targetArray.Length;
|
|
object[][] newEntries =
|
|
[
|
|
[nextId++, "NMSSS", "过新过热Slide", NotesTypeID.Def.Slide, SlideType.Slide_MAX, 8, Ma2Category.MA2_Note, 2, 2, 2, 2, 2, 2, 0],
|
|
[nextId++, "BRSSS", "过新过热BreakSlide", NotesTypeID.Def.BreakSlide, SlideType.Slide_MAX, 8, Ma2Category.MA2_Note, 2, 2, 2, 2, 2, 2, 0],
|
|
[nextId++, "EXSSS", "过新过热ExSlide", NotesTypeID.Def.ExSlide, SlideType.Slide_MAX, 8, Ma2Category.MA2_Note, 2, 2, 2, 2, 2, 2, 0],
|
|
[nextId++, "BXSSS", "过新过热ExBreakSlide", NotesTypeID.Def.ExBreakSlide, SlideType.Slide_MAX, 8, Ma2Category.MA2_Note, 2, 2, 2, 2, 2, 2, 0],
|
|
[nextId++, "CNSSS", "过新过热ConnSlide", NotesTypeID.Def.ConnectSlide, SlideType.Slide_MAX, 8, Ma2Category.MA2_Note, 2, 2, 2, 2, 2, 2, 0],
|
|
];
|
|
|
|
// Ma2fileRecordID.Ma2fileRecord_Data is private, so we need this shit.
|
|
var structType = targetArray.GetValue(0).GetType();
|
|
var constructor = AccessTools.Constructor(structType,
|
|
[
|
|
typeof(int), typeof(string), typeof(string), typeof(NotesTypeID.Def), typeof(SlideType), typeof(int),
|
|
typeof(Ma2Category), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int),
|
|
typeof(int)
|
|
]);
|
|
|
|
Ma2FileRecordData = Array.CreateInstance(structType, targetArray.Length + newEntries.Length);
|
|
for (var i = 0; i < targetArray.Length; i++)
|
|
{
|
|
Ma2FileRecordData.SetValue(targetArray.GetValue(i), i);
|
|
}
|
|
|
|
for (var i = 0; i < newEntries.Length; i++)
|
|
{
|
|
var j = targetArray.Length + i;
|
|
var obj = constructor.Invoke(newEntries[i]);
|
|
Ma2FileRecordData.SetValue(obj, j);
|
|
}
|
|
|
|
arrayTraverse.SetValue(Ma2FileRecordData);
|
|
TotalMa2RecordCount = Ma2FileRecordData.Length;
|
|
LastMa2RecordID = TotalMa2RecordCount - 1;
|
|
MelonLogger.Msg($"[CustomNoteType] MA2 record data extended, total count: {TotalMa2RecordCount}");
|
|
|
|
// Initialize related classes ...
|
|
SlideDataBuilder.InitializeHitAreasLookup();
|
|
MelonLogger.Msg($"[CustomNoteType] HitAreasLookup initialized, total count: {SlideDataBuilder.HitAreasLookup.Count}");
|
|
}
|
|
|
|
[HarmonyPrefix]
|
|
[HarmonyPatch(typeof(Ma2fileRecordID), "findID")]
|
|
public static bool FindIDPrefix(string enumName, ref Ma2fileRecordID.Def __result)
|
|
{
|
|
// I don't know why but patching findID() leads to a completely invalid result
|
|
// Sometimes it will even throw an exception
|
|
// So I can only prefix it and override it
|
|
__result = Ma2fileRecordID.Def.Invalid;
|
|
for (var i = 0; i < TotalMa2RecordCount; i++)
|
|
{
|
|
var item = Ma2FileRecordData.GetValue(i);
|
|
if (Traverse.Create(item).Field<string>("enumName").Value == enumName)
|
|
{
|
|
__result = (Ma2fileRecordID.Def) i;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
[HarmonyPatch]
|
|
public static class Ma2RecordValidation
|
|
{
|
|
public static IEnumerable<MethodBase> TargetMethods()
|
|
{
|
|
return
|
|
[
|
|
// AccessTools.Method(typeof(Ma2fileRecordID), "findID"),
|
|
AccessTools.Method(typeof(Ma2fileRecordID), "clamp"),
|
|
AccessTools.Method(typeof(Ma2fileRecordID), "getClampValue"),
|
|
AccessTools.Method(typeof(Ma2fileRecordID), "isValid"),
|
|
AccessTools.Method(typeof(Ma2fileRecordID_Extension), "isValid"),
|
|
];
|
|
}
|
|
|
|
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
|
|
{
|
|
|
|
foreach (var inst in instructions)
|
|
{
|
|
if (inst.LoadsConstant(142))
|
|
{
|
|
var instNew = new CodeInstruction(OpCodes.Ldsfld, AccessTools.Field(typeof(CustomNoteTypePatch), "TotalMa2RecordCount"));
|
|
yield return instNew;
|
|
}
|
|
else if (inst.LoadsConstant(141))
|
|
{
|
|
var instNew = new CodeInstruction(OpCodes.Ldsfld, AccessTools.Field(typeof(CustomNoteTypePatch), "LastMa2RecordID"));
|
|
yield return instNew;
|
|
}
|
|
else
|
|
{
|
|
yield return inst;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* ========== ========== ========== ========== ========== ========== ========== ==========
|
|
* 以下内容是给新的 MA2 语法写解析器
|
|
*/
|
|
|
|
/*
|
|
* 给新建的 noteData 初始化应有的数据, 仅仅是照搬了 NotesReader.loadNote
|
|
*/
|
|
public static void PrepareBasicNoteData(NoteData noteData, NotesReader reader,
|
|
MA2Record record, int index, ref int noteIndex, OptionMirrorID mirrorMode)
|
|
{
|
|
noteData.type = record.getType().getNotesTypeId();
|
|
noteData.time.init(record.getBar(), record.getGrid(), reader);
|
|
noteData.end = noteData.time;
|
|
noteData.startButtonPos = MaiGeometry.MirrorInfo[(int) mirrorMode, record.getPos()];
|
|
noteData.index = index;
|
|
var num = record.getGrid() % 96;
|
|
if (num == 0)
|
|
{
|
|
noteData.beatType = NoteData.BeatType.BeatType04;
|
|
}
|
|
else if (num % 48 == 0)
|
|
{
|
|
noteData.beatType = NoteData.BeatType.BeatType08;
|
|
}
|
|
else if (num % 24 == 0)
|
|
{
|
|
noteData.beatType = NoteData.BeatType.BeatType16;
|
|
}
|
|
else if (num % 16 == 0)
|
|
{
|
|
noteData.beatType = NoteData.BeatType.BeatType24;
|
|
}
|
|
else
|
|
{
|
|
noteData.beatType = NoteData.BeatType.BeatTypeOther;
|
|
}
|
|
noteData.indexNote = noteIndex;
|
|
++noteIndex;
|
|
}
|
|
|
|
/*
|
|
* 给新建的 noteData 填入基本的 slide 相关数据, 仅仅是照搬了 NotesReader.loadNote
|
|
*/
|
|
public static void PrepareBasicSlideData(NoteData noteData, NotesReader reader, MA2Record record, int noteIndex,
|
|
ref int slideIndex, OptionMirrorID mirrorMode)
|
|
{
|
|
noteData.indexSlide = slideIndex++;
|
|
var slideData = noteData.slideData;
|
|
var slideWaitLen = record.getSlideWaitLen();
|
|
var slideShootLen = record.getSlideShootLen();
|
|
slideData.targetNote = MaiGeometry.MirrorInfo[(int) mirrorMode, record.getSlideEndPos()];
|
|
slideData.shoot.time.init(record.getBar(), record.getGrid() + slideWaitLen, reader);
|
|
slideData.shoot.index = noteIndex;
|
|
slideData.arrive.time.init(record.getBar(), record.getGrid() + slideWaitLen + slideShootLen, reader);
|
|
slideData.arrive.index = noteIndex;
|
|
noteData.end = slideData.arrive.time;
|
|
}
|
|
|
|
[HarmonyPrefix]
|
|
[HarmonyPatch(typeof(NotesReader), "loadNote")]
|
|
public static bool LoadCustomNote(NotesReader __instance, ref bool __result, NotesData ____note, int ____playerID,
|
|
MA2Record rec, int index, ref int noteIndex, ref int slideIndex)
|
|
{
|
|
if (rec.getType() < Ma2fileRecordID.Def.End)
|
|
{
|
|
// builtin record type
|
|
return true;
|
|
}
|
|
|
|
MelonLogger.Msg($"[CustomNoteType] Custom note | {rec._str.Count} | {rec.getStr(0)} {rec.getStr(1)} {rec.getStr(2)} {rec.getStr(3)} {rec.getStr(4)} {rec.getStr(5)} {rec.getStr(6)} {rec.getStr(7)} {rec.getStr(8)}");
|
|
|
|
var flag = true;
|
|
switch (rec.getType().getEnumName())
|
|
{
|
|
case "NMSSS":
|
|
case "BRSSS":
|
|
case "EXSSS":
|
|
case "BXSSS":
|
|
case "CNSSS":
|
|
var noteData = new CustomSlideNoteData();
|
|
var mirrorMode = Singleton<GamePlayManager>.Instance.GetGameScore(____playerID).UserOption.MirrorMode;
|
|
PrepareBasicNoteData(noteData, __instance, rec, index, ref noteIndex, mirrorMode);
|
|
PrepareBasicSlideData(noteData, __instance, rec, noteIndex, ref slideIndex, mirrorMode);
|
|
var success = noteData.ParseSlideCode(rec.getStr(7), mirrorMode);
|
|
if (success)
|
|
{
|
|
____note._noteData.Add(noteData);
|
|
}
|
|
else
|
|
{
|
|
flag = false;
|
|
}
|
|
break;
|
|
default:
|
|
flag = false;
|
|
break;
|
|
}
|
|
__result = flag;
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* ========== ========== ========== ========== ========== ========== ========== ==========
|
|
* 以下内容是为了实现自定义 Slide
|
|
*
|
|
*/
|
|
|
|
/*
|
|
* 把 GetSlidePath 和 GetSlideHitArea 和 GetSlideLength 重定向到我可以控制的函数上, 并且多推几个参数进来
|
|
*/
|
|
[HarmonyPatch]
|
|
public static class SlideNoteDataHack
|
|
{
|
|
public static IEnumerable<MethodBase> TargetMethods()
|
|
{
|
|
return
|
|
[
|
|
AccessTools.Method(typeof(SlideRoot), "Initialize"),
|
|
AccessTools.Method(typeof(SlideRoot), "GetSlideArrowNum", [typeof(NoteData)]),
|
|
AccessTools.Method(typeof(StarNote), "Initialize"),
|
|
AccessTools.Method(typeof(BreakStarNote), "Initialize"),
|
|
];
|
|
}
|
|
|
|
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
|
|
{
|
|
var methodGetSlidePath = AccessTools.Method(typeof(SlideManager), "GetSlidePath");
|
|
var methodGetSlidePathRedirect = AccessTools.Method(typeof(CustomNoteTypePatch), "GetSlidePathRedirect");
|
|
var methodGetSlideHitArea = AccessTools.Method(typeof(SlideManager), "GetSlideHitArea");
|
|
var methodGetSlideHitAreaRedirect = AccessTools.Method(typeof(CustomNoteTypePatch), "GetSlideHitAreaRedirect");
|
|
var methodGetSlideLength = AccessTools.Method(typeof(SlideManager), "GetSlideLength");
|
|
var methodGetSlideLengthRedirect = AccessTools.Method(typeof(CustomNoteTypePatch), "GetSlideLengthRedirect");
|
|
var fieldSlideData = AccessTools.Field(typeof(NoteData), "slideData");
|
|
|
|
var oldInstList = new List<CodeInstruction>(instructions);
|
|
var newInstList = new List<CodeInstruction>();
|
|
CodeInstruction instToInject = null;
|
|
|
|
for (var i = 0; i < oldInstList.Count; ++i)
|
|
{
|
|
var inst = oldInstList[i];
|
|
if (inst.LoadsField(fieldSlideData))
|
|
{
|
|
// 以 GetSlidePath 为例, 我们需要把下面这个调用:
|
|
// Singleton<SlideManager>.Instance.GetSlidePath(
|
|
// noteData.slideData.type, noteData.startButtonPos,
|
|
// noteData.slideData.targetNote, this.ButtonId
|
|
// )
|
|
// 里的 noteData 拿到手
|
|
// 所以就记录上一次 ldfld NoteData::slideData 的位置, 往前找一个 IL code
|
|
// 找到的就是 load 这个 noteData 的位置
|
|
// 然后在后续调用 GetSlidePath 时, 先重复一遍 load 把这个 noteData 入栈, 然后重定向到一个新的函数上去
|
|
instToInject = oldInstList[i-1];
|
|
newInstList.Add(inst);
|
|
}
|
|
else if (inst.Calls(methodGetSlidePath))
|
|
{
|
|
newInstList.Add(instToInject!.Clone());
|
|
newInstList.Add(new CodeInstruction(OpCodes.Call, methodGetSlidePathRedirect));
|
|
instToInject = null;
|
|
}
|
|
else if (inst.Calls(methodGetSlideHitArea))
|
|
{
|
|
newInstList.Add(instToInject!.Clone());
|
|
newInstList.Add(new CodeInstruction(OpCodes.Call, methodGetSlideHitAreaRedirect));
|
|
instToInject = null;
|
|
}
|
|
else if (inst.Calls(methodGetSlideLength))
|
|
{
|
|
newInstList.Add(instToInject!.Clone());
|
|
newInstList.Add(new CodeInstruction(OpCodes.Call, methodGetSlideLengthRedirect));
|
|
instToInject = null;
|
|
}
|
|
else
|
|
{
|
|
newInstList.Add(inst);
|
|
}
|
|
}
|
|
return newInstList;
|
|
}
|
|
}
|
|
|
|
|
|
public static List<Vector4> GetSlidePathRedirect(SlideManager instance, SlideType slideType, int start, int end,
|
|
int starButton, NoteData noteData)
|
|
{
|
|
// MelonLogger.Msg($"[CustomNoteType] GetSlidePath Redirected!");
|
|
// MelonLogger.Msg($"{noteData.indexNote} {noteData.indexSlide} {slideType} {start} {end} {starButton}");
|
|
if (noteData is CustomSlideNoteData data)
|
|
{
|
|
// MelonLogger.Msg($"[CustomNoteType] Successfully injected custom path {data.SlideCode}");
|
|
return data.SlidePathList[starButton];
|
|
}
|
|
return instance.GetSlidePath(slideType, start, end, starButton);
|
|
}
|
|
|
|
public static List<SlideManager.HitArea> GetSlideHitAreaRedirect(SlideManager instance, SlideType slideType,
|
|
int start, int end, int starButton, NoteData noteData)
|
|
{
|
|
// MelonLogger.Msg($"[CustomNoteType] GetSlideHitArea Redirected!");
|
|
// MelonLogger.Msg($"{noteData.indexNote} {noteData.indexSlide} {slideType} {start} {end} {starButton}");
|
|
if (noteData is CustomSlideNoteData data)
|
|
{
|
|
// MelonLogger.Msg($"[CustomNoteType] Successfully injected custom hit areas {data.SlideCode}");
|
|
return data.SlideHitAreasList[starButton];
|
|
}
|
|
return instance.GetSlideHitArea(slideType, start, end, starButton);
|
|
}
|
|
|
|
public static float GetSlideLengthRedirect(SlideManager instance, SlideType slideType,
|
|
int start, int end, NoteData noteData)
|
|
{
|
|
// MelonLogger.Msg($"[CustomNoteType] GetSlideLength Redirected!");
|
|
// MelonLogger.Msg($"{noteData.indexNote} {noteData.indexSlide} {slideType} {start} {end}");
|
|
if (noteData is CustomSlideNoteData data)
|
|
{
|
|
// MelonLogger.Msg($"[CustomNoteType] Successfully injected custom path length {data.SlideCode}");
|
|
return data.SlidePathLength;
|
|
}
|
|
return instance.GetSlideLength(slideType, start, end);
|
|
}
|
|
|
|
|
|
|
|
[HarmonyPatch]
|
|
public static class Debuging
|
|
{
|
|
public static IEnumerable<MethodBase> TargetMethods()
|
|
{
|
|
return
|
|
[
|
|
AccessTools.Method(typeof(SlideRoot), "Initialize"),
|
|
// AccessTools.Method(typeof(SlideRoot), "GetSlideArrowNum", []),
|
|
// AccessTools.Method(typeof(SlideRoot), "GetSlideArrowNum", [typeof(NoteData)]),
|
|
// AccessTools.Method(typeof(SlideRoot), "GetArrowData"),
|
|
// AccessTools.Method(typeof(SlideRoot), "totalDistance"),
|
|
// AccessTools.Method(typeof(SlideRoot), "GetActiveArrowNum"),
|
|
// AccessTools.Method(typeof(SlideJudge), "SetJudgeType"),
|
|
];
|
|
}
|
|
|
|
public static void Prefix(MethodBase __originalMethod, object[] __args)
|
|
{
|
|
var msg = "[CustomNoteType] Before ";
|
|
msg += __originalMethod.DeclaringType!.FullName + "." + __originalMethod.Name + " (";
|
|
var infos = __originalMethod.GetParameters()
|
|
.Select((x, i) => x.ParameterType.FullName + " " + x.Name + " = " + GetString(__args[i]))
|
|
.ToArray();
|
|
msg += infos.Length > 0 ? infos.Aggregate((a, b) => a + ", " + b) : "void";
|
|
msg += ")";
|
|
MelonLogger.Msg(msg);
|
|
}
|
|
|
|
public static void Postfix(MethodBase __originalMethod, object[] __args)
|
|
{
|
|
var msg = "[CustomNoteType] After ";
|
|
msg += __originalMethod.DeclaringType!.FullName + "." + __originalMethod.Name + " (";
|
|
var infos = __originalMethod.GetParameters()
|
|
.Select((x, i) => x.ParameterType.FullName + " " + x.Name + " = " + GetString(__args[i]))
|
|
.ToArray();
|
|
msg += infos.Length > 0 ? infos.Aggregate((a, b) => a + ", " + b) : "void";
|
|
msg += ")";
|
|
MelonLogger.Msg(msg);
|
|
}
|
|
|
|
public static string GetString(object value)
|
|
{
|
|
if (value is CustomSlideNoteData data)
|
|
{
|
|
return $"<CustomSlideNoteData {data.indexNote} {data.indexSlide} {data.SlideCode}>";
|
|
}
|
|
|
|
if (value is NoteData data2)
|
|
{
|
|
return $"<NoteData {data2.indexNote} {data2.indexSlide}>";
|
|
}
|
|
|
|
return value.ToString();
|
|
}
|
|
}
|
|
} |