[+] Slide code support & split multiple patches (#77)

* 功能拆分

将不同的功能分拆到不同文件

* Slide code notation support

This is part of Maimai DX 2077 patch set.
New MA2 commands: NMSSS, BRSSS, EXSSS, BXSSS, CNSSS
pull/78/head
Minepig 2024-10-25 20:42:08 +08:00 committed by GitHub
parent 98213cff67
commit d0bb3cc75c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 2078 additions and 111 deletions

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
@ -71,6 +71,9 @@
<Reference Include="System.Configuration">
<HintPath>Libs\System.Configuration.dll</HintPath>
</Reference>
<Reference Include="System.Numerics">
<HintPath>Libs\System.Numerics.dll</HintPath>
</Reference>
<Reference Include="System.Core">
<HintPath>Libs\System.Core.dll</HintPath>
</Reference>
@ -282,7 +285,7 @@
<HintPath>Libs\UnityEngine.XRModule.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="AquaMai.zh.toml" WithCulture="false" />
<EmbeddedResource Include="AquaMai.toml" />

View File

@ -0,0 +1,24 @@
using HarmonyLib;
using Monitor;
using UnityEngine;
namespace AquaMai.Fix;
public class BreakSlideJudgeBlink
{
/*
* Patch BreakSlide Critical BreakTap
* 使 ()
*/
[HarmonyPostfix]
[HarmonyPatch(typeof(SlideJudge), "UpdateBreakEffectAdd")]
private static void FixBreakSlideJudgeBlink(
SpriteRenderer ___SpriteRenderAdd, SpriteRenderer ___SpriteRender,
SlideJudge.SlideJudgeType ____judgeType, SlideJudge.SlideAngle ____angle
)
{
if (!___SpriteRenderAdd.gameObject.activeSelf) return;
float num = ___SpriteRenderAdd.color.r;
___SpriteRenderAdd.color = new Color(num, num, num, 1f);
}
}

View File

@ -0,0 +1,32 @@
using HarmonyLib;
using Monitor;
namespace AquaMai.Fix;
public class FanJudgeFlip
{
/*
* Patch Wifi Slide ( Wifi ), majdata
* bug SBGA Wifi EndButtonId
* , Slide , Patch
*/
[HarmonyPostfix]
[HarmonyPatch(typeof(SlideFan), "Initialize")]
private static void FixFanJudgeFilp(
int[] ___GoalButtonId, SlideJudge ___JudgeObj
)
{
if (null != ___JudgeObj)
{
if (2 <= ___GoalButtonId[1] && ___GoalButtonId[1] <= 5)
{
___JudgeObj.Flip(false);
___JudgeObj.transform.Rotate(0.0f, 0.0f, 180f);
}
else
{
___JudgeObj.Flip(true);
}
}
}
}

View File

@ -0,0 +1,43 @@
using System;
using HarmonyLib;
using Manager;
using Monitor;
using Process;
using UnityEngine;
namespace AquaMai.Fix;
public class FixCircleSlideJudge
{
/*
* Patch Slide 线 (), majdata
* Patch ,
*/
[HarmonyPostfix]
[HarmonyPatch(typeof(SlideRoot), "Initialize")]
private static void FixJudgePosition(
SlideRoot __instance, SlideType ___EndSlideType, SlideJudge ___JudgeObj
)
{
if (null != ___JudgeObj)
{
float z = ___JudgeObj.transform.localPosition.z;
if (___EndSlideType == SlideType.Slide_Circle_L)
{
float angle = -45.0f - 45.0f * __instance.EndButtonId;
double angleRad = Math.PI / 180.0 * (angle + 90 + 22.5 + 2.6415);
___JudgeObj.transform.localPosition = new Vector3(480f * (float)Math.Cos(angleRad), 480f * (float)Math.Sin(angleRad), z);
___JudgeObj.transform.localRotation = Quaternion.Euler(0.0f, 0.0f, angle);
}
else if (___EndSlideType == SlideType.Slide_Circle_R)
{
float angle = -45.0f * __instance.EndButtonId;
double angleRad = Math.PI / 180.0 * (angle + 90 - 22.5 - 2.6415);
___JudgeObj.transform.localPosition = new Vector3(480f * (float)Math.Cos(angleRad), 480f * (float)Math.Sin(angleRad), z);
___JudgeObj.transform.localRotation = Quaternion.Euler(0.0f, 0.0f, angle);
}
}
}
}

View File

@ -5,7 +5,7 @@ using Monitor;
namespace AquaMai.Fix;
public class SlideAutoPlayTweak
public class FixSlideAutoPlay
{
/* Patch bug:
* SlideFan AutoPlay ,

View File

@ -1,89 +0,0 @@
using System;
using HarmonyLib;
using Manager;
using Monitor;
using Process;
using UnityEngine;
namespace AquaMai.Fix;
public class SlideJudgeTweak
{
/*
* Patch BreakSlide Critical BreakTap
*/
[HarmonyPostfix]
[HarmonyPatch(typeof(SlideJudge), "UpdateBreakEffectAdd")]
private static void FixBreakSlideJudgeBlink(
SpriteRenderer ___SpriteRenderAdd, SpriteRenderer ___SpriteRender,
SlideJudge.SlideJudgeType ____judgeType, SlideJudge.SlideAngle ____angle
)
{
if (!___SpriteRenderAdd.gameObject.activeSelf) return;
float num = ___SpriteRenderAdd.color.r;
___SpriteRenderAdd.color = new Color(num, num, num, 0.3f);
if (num > 0.9f)
{
___SpriteRender.sprite = GameNoteImageContainer.JudgeSlideCriticalBreak[(int) ____judgeType, (int) ____angle];
}
else if (num < 0.1f)
{
___SpriteRender.sprite = GameNoteImageContainer.JudgeSlideCritical[(int) ____judgeType, (int) ____angle];
}
}
/*
* Patch Slide 线 (), majdata
*/
[HarmonyPostfix]
[HarmonyPatch(typeof(SlideRoot), "Initialize")]
private static void FixCircleSlideJudgePosition(
SlideRoot __instance, SlideType ___EndSlideType, SlideJudge ___JudgeObj
)
{
if (null != ___JudgeObj)
{
float z = ___JudgeObj.transform.localPosition.z;
if (___EndSlideType == SlideType.Slide_Circle_L)
{
float angle = -45.0f - 45.0f * __instance.EndButtonId;
double angleRad = Math.PI / 180.0 * (angle + 90 + 22.5 + 2.6415);
___JudgeObj.transform.localPosition = new Vector3(480f * (float)Math.Cos(angleRad), 480f * (float)Math.Sin(angleRad), z);
___JudgeObj.transform.localRotation = Quaternion.Euler(0.0f, 0.0f, angle);
}
else if (___EndSlideType == SlideType.Slide_Circle_R)
{
float angle = -45.0f * __instance.EndButtonId;
double angleRad = Math.PI / 180.0 * (angle + 90 - 22.5 - 2.6415);
___JudgeObj.transform.localPosition = new Vector3(480f * (float)Math.Cos(angleRad), 480f * (float)Math.Sin(angleRad), z);
___JudgeObj.transform.localRotation = Quaternion.Euler(0.0f, 0.0f, angle);
}
}
}
/*
* Patch Wifi Slide ( Wifi ), majdata
* bug SBGA Wifi EndButtonId
* , Slide , Patch
* Patch
*/
// [HarmonyPostfix]
// [HarmonyPatch(typeof(SlideFan), "Initialize")]
private static void FixFanJudgeFilp(
int[] ___GoalButtonId, SlideJudge ___JudgeObj
)
{
if (null != ___JudgeObj)
{
if (2 <= ___GoalButtonId[1] && ___GoalButtonId[1] <= 5)
{
___JudgeObj.Flip(false);
___JudgeObj.transform.Rotate(0.0f, 0.0f, 180f);
}
else
{
___JudgeObj.Flip(true);
}
}
}
}

Binary file not shown.

View File

@ -0,0 +1,426 @@
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();
}
}
}

View File

@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.Linq;
using DB;
using Manager;
using MelonLoader;
using UnityEngine;
namespace AquaMai.MaimaiDX2077;
public class CustomSlideNoteData: NoteData
{
public string SlideCode;
public List<List<Vector4>> SlidePathList = new List<List<Vector4>>();
public List<List<SlideManager.HitArea>> SlideHitAreasList = new List<List<SlideManager.HitArea>>();
public float SlidePathLength;
public bool ParseSlideCode(string slideCode, OptionMirrorID mirrorMode)
{
if (string.IsNullOrEmpty(slideCode))
{
return false;
}
SlidePathList.Clear();
SlideHitAreasList.Clear();
this.SlideCode = slideCode;
var path = SlideCodeParser.Parse(slideCode);
if (path == null)
{
return false;
}
var arrowData = SlideDataBuilder.BuildArrowData(path);
SlidePathLength = (float)path.GetPathLength();
var hitAreaData = SlideDataBuilder.BuildHitAreas(path);
for (var i = 0; i < 8; i++)
{
SlidePathList.Add(SlideDataBuilder.ConvertAndRotateArrowData(arrowData, i, mirrorMode));
SlideHitAreasList.Add(SlideDataBuilder.ConvertAndRotateHitAreas(hitAreaData, i, mirrorMode));
}
var msg = string.Join(", ",
hitAreaData.Select(x => x.PanelAreas).Select(x => string.Join("/", x.Cast<InputManager.TouchPanelArea>())));
MelonLogger.Msg(msg);
this.slideData.type = path.GetEndType(mirrorMode);
return true;
}
}

View File

@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Manager;
namespace AquaMai.MaimaiDX2077;
public static class MaiGeometry
{
public struct CircleStruct(Complex center, double radius)
{
public Complex Center = center;
public double Radius = radius;
}
public static readonly double CanvasWidth = 1080.0;
public static readonly double MainRadius = 480.0;
public static readonly double CenterRadius = MainRadius * Math.Cos(Math.PI * 3 / 8);
public static readonly double GroupBRadius = CenterRadius / Math.Cos(Math.PI / 8);
private static readonly double _b = Math.Cos(Math.PI / 8) / 2;
private static readonly double _a = 1 - _b;
private static readonly double _theta = Math.PI / 4;
private static readonly double _s = (_a * _a + _b * _b - 2 * _a * _b * Math.Cos(_theta)) /
(2 * _a - 2 * _b * Math.Cos(_theta));
public static readonly double PPQQRadius = MainRadius * _b;
public static readonly double TransferRadius = MainRadius * (_b + _s);
public static readonly double EdgeTransferAngle = _theta;
public static readonly double PPQQTransferAngle =
Math.Acos((_s * _s + _b * _b - (_a - _s) * (_a - _s)) / (2 * _b * _s));
public static readonly double DefaultDistance = MainRadius * Math.PI / 32;
public static readonly int[,] MirrorInfo = new int[4, 17]
{
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }, // Normal
{ 7, 6, 5, 4, 3, 2, 1, 0, 15, 14, 13, 12, 11, 10, 9, 8, 16 }, // L <-> R
{ 3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12, 16 }, // U <-> D
{ 4, 5, 6, 7, 0, 1, 2, 3, 12, 13, 14, 15, 8, 9, 10, 11, 16 } // rotate 180 deg
};
/// <summary>
/// Note: idx is 1-based, not 0-based
/// </summary>
public static Complex PointGroupA(int idx)
{
var angle = Math.PI * (5.0 / 8.0 - idx / 4.0);
return Complex.FromPolarCoordinates(MainRadius, angle);
}
/// <summary>
/// Note: idx is 1-based, not 0-based
/// </summary>
public static Complex PointGroupB(int idx)
{
var angle = Math.PI * (5.0 / 8.0 - idx / 4.0);
return Complex.FromPolarCoordinates(GroupBRadius, angle);
}
public static Complex Center()
{
return Complex.Zero;
}
/// <summary>
/// idx 0 is center circle, idx 1~8 are ppqq circles, idx 9 is outer circle
/// </summary>
public static CircleStruct GetCircle(int idx)
{
if (idx == 0)
{
return new CircleStruct(Complex.Zero, CenterRadius);
}
if (idx == 9)
{
return new CircleStruct(Complex.Zero, MainRadius);
}
var angle = Math.PI * (3.0 / 4.0 - idx / 4.0);
var center = Complex.FromPolarCoordinates(PPQQRadius, angle);
return new CircleStruct(center, PPQQRadius);
}
/// <summary>
/// Note: idx is 1-based, not 0-based
/// </summary>
/// <returns>CircleStruct TransferCircle, double TransferStartAngle, double TransferEndAngle</returns>
public static Tuple<CircleStruct, double, double> TransferOutData(int idx, bool isccw)
{
var ppqqRad = Math.PI * (3.0 / 4.0 - idx / 4.0);
double startAngle, endAngle;
if (isccw)
{
startAngle = ppqqRad - PPQQTransferAngle;
endAngle = ppqqRad + EdgeTransferAngle;
}
else
{
startAngle = ppqqRad + PPQQTransferAngle;
endAngle = ppqqRad - EdgeTransferAngle;
}
var d = MainRadius - TransferRadius;
var center = Complex.FromPolarCoordinates(d, endAngle);
return new Tuple<CircleStruct, double, double>(new CircleStruct(center, TransferRadius),
Math.IEEERemainder(startAngle, Math.PI * 2), Math.IEEERemainder(endAngle, Math.PI * 2));
}
}

View File

@ -0,0 +1,226 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using DB;
using Manager;
namespace AquaMai.MaimaiDX2077;
public class ParametricSlidePath
{
public enum ParseMarker
{
None = 0,
SmoothAlign,
ForceAlign,
SharpCorner
}
public abstract class PathSegment
{
public ParseMarker ParseMarker = ParseMarker.None;
public double ArrowDistance = MaiGeometry.DefaultDistance;
public abstract bool DoAngleLerp { get; }
public abstract Complex GetPointAt(double t);
public abstract Complex GetTangentAt(double t);
public abstract double GetSegmentLength();
public void SetParseMarker(ParseMarker marker) => ParseMarker = marker;
public void SetArrowDistance(double distance) => ArrowDistance = distance;
}
public class LineSegment(Complex start, Complex end) : PathSegment
{
public readonly Complex StartPoint = start;
public readonly Complex EndPoint = end;
public override bool DoAngleLerp { get; } = false;
public override Complex GetPointAt(double t)
{
return StartPoint + (EndPoint - StartPoint) * t;
}
public override Complex GetTangentAt(double t)
{
var v = EndPoint - StartPoint;
return v / v.Magnitude;
}
public override double GetSegmentLength()
{
return (EndPoint - StartPoint).Magnitude;
}
}
public class ArcSegment(MaiGeometry.CircleStruct circle, double startAngle, double endAngle) : PathSegment
{
public readonly MaiGeometry.CircleStruct Circle = circle;
public readonly double StartAngle = startAngle;
public readonly double EndAngle = endAngle;
public override bool DoAngleLerp { get; } = true;
public override Complex GetPointAt(double t)
{
var angle = StartAngle + t * (EndAngle - StartAngle);
return Circle.Center + Complex.FromPolarCoordinates(Circle.Radius, angle);
}
public override Complex GetTangentAt(double t)
{
var angle = StartAngle + t * (EndAngle - StartAngle);
if (StartAngle < EndAngle)
{
return Complex.FromPolarCoordinates(1, angle) * Complex.ImaginaryOne;
}
else
{
return Complex.FromPolarCoordinates(-1, angle) * Complex.ImaginaryOne;
}
}
public override double GetSegmentLength()
{
return Math.Abs(EndAngle - StartAngle) * Circle.Radius;
}
}
public class CircleSegment(MaiGeometry.CircleStruct circle, double startAngle, bool isCcw) : PathSegment
{
public readonly MaiGeometry.CircleStruct Circle = circle;
public readonly double StartAngle = startAngle;
public readonly bool IsCcw = isCcw;
public override bool DoAngleLerp { get; } = true;
public override Complex GetPointAt(double t)
{
double angle;
if (IsCcw)
{
angle = StartAngle + t * Math.PI * 2f;
}
else
{
angle = StartAngle - t * Math.PI * 2f;
}
return Circle.Center + Complex.FromPolarCoordinates(Circle.Radius, angle);
}
public override Complex GetTangentAt(double t)
{
double angle;
if (IsCcw)
{
angle = StartAngle + t * Math.PI * 2f;
return Complex.FromPolarCoordinates(1, angle) * Complex.ImaginaryOne;
}
else
{
angle = StartAngle - t * Math.PI * 2f;
return Complex.FromPolarCoordinates(-1, angle) * Complex.ImaginaryOne;
}
}
public override double GetSegmentLength()
{
return Math.PI * Circle.Radius * 2;
}
}
public readonly PathSegment[] Segments;
public readonly double[] Fractions;
public readonly double[] AccumulatedLengths;
public ParametricSlidePath(IEnumerable<PathSegment> pathSegments)
{
Segments = pathSegments.ToArray();
if (Segments.Length == 0)
{
throw new ArgumentException("At least one path segment is required.");
}
var lengths = Segments.Select(s => s.GetSegmentLength());
var sum = 0.0;
AccumulatedLengths = lengths.Select(x => (sum += x)).ToArray();
Fractions = AccumulatedLengths.Select(x => x / sum).ToArray();
}
public PathSegment GetSegmentAt(double t, out double segmentT)
{
if (t <= 0.0)
{
segmentT = 0.0;
return Segments[0];
}
if (t >= 1.0)
{
segmentT = 1.0;
return Segments[Segments.Length - 1];
}
var idx = Array.BinarySearch(Fractions, t);
if (idx < 0)
{
idx = ~idx; // first entry > t
}
// if idx >= 0 then idx is the entry == t
// so Fractions[idx-1] < t and Fractions[idx] >= t
// Note: Fractions[i] marks the end point of Segments[i]
if (idx >= Segments.Length)
{
segmentT = 1.0;
return Segments[Segments.Length - 1];
}
if (idx == 0)
{
segmentT = t / Fractions[0];
return Segments[0];
}
segmentT = (t - Fractions[idx - 1]) / (Fractions[idx] - Fractions[idx - 1]);
return Segments[idx];
}
public double GetPathLength() => AccumulatedLengths[AccumulatedLengths.Length - 1];
public Complex GetPointAt(double t)
{
var segment = GetSegmentAt(t, out var segT);
return segment.GetPointAt(segT);
}
public Complex GetTangentAt(double t)
{
var segment = GetSegmentAt(t, out var segT);
return segment.GetTangentAt(segT);
}
public SlideType GetEndType(OptionMirrorID mirrorMode)
{
var lastSegment = Segments[Segments.Length - 1];
var flip = mirrorMode == OptionMirrorID.LR || mirrorMode == OptionMirrorID.UD;
if (lastSegment is CircleSegment circle)
{
return circle.IsCcw != flip ? SlideType.Slide_Circle_L : SlideType.Slide_Circle_R;
}
if (lastSegment is ArcSegment arc)
{
return (arc.EndAngle > arc.StartAngle) != flip ? SlideType.Slide_Circle_L : SlideType.Slide_Circle_R;
}
return SlideType.Slide_Straight;
}
}

View File

@ -0,0 +1,260 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using MelonLoader;
namespace AquaMai.MaimaiDX2077;
public static class SlideCodeParser
{
public enum CommandType
{
Invalid = -1,
NodeA = 0,
NodeB = 1,
NodeC = 2,
OrbitCCW = 3,
OrbitCW = 4,
NodeEnd = 5
}
public struct Command(CommandType type, int value)
{
public CommandType Type = type;
public int Value = value;
public static bool IsSame(Command a, Command b)
{
return a.Type == b.Type && a.Value == b.Value;
}
}
public static readonly char[] CommandChars =
[
'A', 'B', 'C', 'P', 'Q', 'K'
];
public static int TryParseDigit(char c)
{
if (c >= '0' && c <= '9') return c - '0';
return -1;
}
public static List<Command> ParseCommands(string code)
{
if (!CommandChars.Contains(code[1]))
{
throw new ArgumentException($"the 2nd char should be a command");
}
if (code[code.Length - 2] != 'K')
{
throw new ArgumentException($"should end with 'K' command");
}
var commands = new List<Command>();
var currentType = CommandType.NodeA;
var value = TryParseDigit(code[0]);
if (value < 0) throw new ArgumentException($"invalid char '{code[0]}'");
commands.Add(new Command(currentType, value));
for (var ptr = 1; ptr < code.Length; ptr++)
{
var ch = code[ptr];
if (CommandChars.Contains(ch))
{
currentType = (CommandType) Array.IndexOf(CommandChars, ch);
if (currentType == CommandType.NodeC)
{
commands.Add(new Command(CommandType.NodeC, 0));
}
}
else
{
value = TryParseDigit(ch);
if (value < 0) throw new ArgumentException($"invalid char '{ch}'");
if (currentType == CommandType.NodeC)
{
throw new ArgumentException($"digit should not follow 'C'");
}
commands.Add(new Command(currentType, value));
}
}
return commands;
}
public static Complex GetNodePosition(Command cmd)
{
switch (cmd.Type)
{
case CommandType.NodeA:
case CommandType.NodeEnd:
return MaiGeometry.PointGroupA(cmd.Value);
case CommandType.NodeB:
return MaiGeometry.PointGroupB(cmd.Value);
case CommandType.NodeC:
return MaiGeometry.Center();
default:
throw new ArgumentException($"invalid type for node: {cmd.Type}");
}
}
public static void NodeToNode(SlidePathGenerator generator, Command last, Command current)
{
if (Command.IsSame(last, current)) return;
generator.LineToPoint(GetNodePosition(current));
}
public static void NodeToOrbit(SlidePathGenerator generator, Command last, Command current)
{
var isCcw = (current.Type == CommandType.OrbitCCW);
var node = GetNodePosition(last);
var orbit = MaiGeometry.GetCircle(current.Value);
var diff = node - orbit.Center;
if (Math.Abs(diff.Magnitude - orbit.Radius) < 0.1)
{
if (last.Type == CommandType.NodeA && current.Value == 9)
{
generator.TrySetLastParseMarker(ParametricSlidePath.ParseMarker.ForceAlign);
}
return; // node on circle, do nothing
}
if (diff.Magnitude < orbit.Radius)
throw new ArgumentException($"impossible: {last.Type}{last.Value} -> Orbit{current.Value}");
generator.TangentToCircle(orbit, isCcw);
}
public static void OrbitToNode(SlidePathGenerator generator, Command last, Command current)
{
var isCcw = (last.Type == CommandType.OrbitCCW);
var node = GetNodePosition(current);
var orbit = MaiGeometry.GetCircle(last.Value);
var diff = node - orbit.Center;
if (Math.Abs(diff.Magnitude - orbit.Radius) < 0.1)
{
generator.ArcToAngle(orbit.Center, diff.Phase, isCcw, false);
return;
}
if (diff.Magnitude < orbit.Radius)
throw new ArgumentException($"impossible: Orbit{last.Value} -> {current.Type}{current.Value}");
generator.ArcToTangentTowards(node, orbit.Center, isCcw);
generator.LineToPoint(node);
}
public static void OrbitToOrbit(SlidePathGenerator generator, Command last, Command current)
{
if (current.Type != last.Type) throw new ArgumentException($"orbit type mismatch");
var isCcw = (last.Type == CommandType.OrbitCCW);
var lastOrbit = MaiGeometry.GetCircle(last.Value);
var currentOrbit = MaiGeometry.GetCircle(current.Value);
if (current.Value == last.Value)
{
generator.FullCircle(lastOrbit.Center, isCcw);
return;
}
if (last.Value == 0 && current.Value == 9 || last.Value == 9 && current.Value == 0)
throw new ArgumentException($"impossible: Orbit{last.Value} -> Orbit{current.Value}");
if (current.Value == 9)
{
var data = MaiGeometry.TransferOutData(last.Value, isCcw);
generator.ArcToAngle(lastOrbit.Center, data.Item2, isCcw, false);
generator.ArcToAngle(data.Item1.Center, data.Item3, isCcw, false);
generator.TrySetLastParseMarker(ParametricSlidePath.ParseMarker.SmoothAlign);
return;
}
if (last.Value == 9)
{
var data = MaiGeometry.TransferOutData(current.Value, !isCcw);
generator.ArcToAngle(lastOrbit.Center, data.Item3, isCcw, true);
generator.ArcToAngle(data.Item1.Center, data.Item2, isCcw, false);
return;
}
generator.ExternTangentTransfer(lastOrbit.Center, currentOrbit, isCcw);
}
public static ParametricSlidePath Parse(string code)
{
try
{
var commands = ParseCommands(code);
var lastCmd = commands[0];
// The first command is guarantee to be 'A'
var generator = SlidePathGenerator.BeginAt(MaiGeometry.PointGroupA(lastCmd.Value));
for (var i = 1; i < commands.Count; i++)
{
var cmd = commands[i];
switch (cmd.Type)
{
case CommandType.NodeA:
case CommandType.NodeB:
case CommandType.NodeC:
case CommandType.NodeEnd:
switch (lastCmd.Type)
{
case CommandType.NodeA:
case CommandType.NodeB:
case CommandType.NodeC:
NodeToNode(generator, lastCmd, cmd);
break;
case CommandType.OrbitCCW:
case CommandType.OrbitCW:
OrbitToNode(generator, lastCmd, cmd);
break;
case CommandType.NodeEnd:
throw new ArgumentException($"'K' should be the last command");
default:
throw new ArgumentOutOfRangeException();
}
break;
case CommandType.OrbitCCW:
case CommandType.OrbitCW:
switch (lastCmd.Type)
{
case CommandType.NodeA:
case CommandType.NodeB:
case CommandType.NodeC:
NodeToOrbit(generator, lastCmd, cmd);
break;
case CommandType.OrbitCCW:
case CommandType.OrbitCW:
OrbitToOrbit(generator, lastCmd, cmd);
break;
case CommandType.NodeEnd:
throw new ArgumentException($"'K' should be the last command");
default:
throw new ArgumentOutOfRangeException();
}
break;
default:
throw new ArgumentOutOfRangeException();
}
lastCmd = cmd;
}
return generator.GeneratePath();
}
catch (ArgumentException e)
{
var msg = $"Invalid code: {code}";
if (e.Message != "")
{
msg += $", {e.Message}";
}
MelonLogger.Error(msg);
return null;
}
}
}

View File

@ -0,0 +1,475 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using DB;
using Manager;
using MelonLoader;
using Vector4 = UnityEngine.Vector4;
namespace AquaMai.MaimaiDX2077;
public static class SlideDataBuilder
{
public readonly struct ArrowData(Complex point, Complex tangent, double length)
{
public readonly Complex Point = point;
public readonly Complex Tangent = tangent;
public readonly double Length = length;
}
public static List<ArrowData> BuildArrowData(ParametricSlidePath path)
{
var result = new List<ArrowData>();
var totalLength = path.GetPathLength();
var totalSegCount = path.Segments.Length;
var length = 0.0;
var segIdx = 0;
var isSwitching = false;
while (length < totalLength)
{
var t = length / totalLength;
var pt = path.GetPointAt(t);
var tg = path.GetTangentAt(t);
var t2 = (length + 10.0) / totalLength;
if ((path.GetTangentAt(t2) - tg).Magnitude < 0.2) // 0.2 -> ~ 11.48 deg apart
{
// use secant instead of tangent (for better visual quality)
tg = path.GetPointAt(t2) - pt;
tg /= tg.Magnitude;
}
if (isSwitching)
{
// around connecting point of 2 segments, smoothing the transition
var last = result[result.Count - 1];
var vec = pt - last.Point;
vec /= vec.Magnitude;
if ((tg - last.Tangent).Magnitude < 0.2)
{
var x = 0.5 * (last.Tangent + vec);
x /= x.Magnitude;
result[result.Count - 1] = new ArrowData(last.Point, x, last.Length);
tg = 0.5 * (tg + vec);
tg /= tg.Magnitude;
}
}
result.Add(new ArrowData(pt, tg, length));
isSwitching = false;
var nextLength = length + path.Segments[segIdx].ArrowDistance;
if (segIdx < totalSegCount - 1 && nextLength >= path.AccumulatedLengths[segIdx])
{
isSwitching = true;
if (path.Segments[segIdx].ParseMarker == ParametricSlidePath.ParseMarker.ForceAlign)
{
// in this case the next point is forced to be 1 unit after the connecting point
nextLength = path.AccumulatedLengths[segIdx] + path.Segments[segIdx + 1].ArrowDistance;
// P.S. 这种情况一般是出现在一条直线连接到外圈, 这个处理是为了让外圈的箭头对齐
}
if (path.Segments[segIdx + 1].ParseMarker == ParametricSlidePath.ParseMarker.SmoothAlign)
{
// arrow distance of the next segment is tempered in order to align arrow
var delta = path.AccumulatedLengths[segIdx + 1] - length;
var n = Math.Round(delta / MaiGeometry.DefaultDistance);
path.Segments[segIdx + 1].SetArrowDistance(delta / n);
nextLength = length + delta / n;
// P.S. 这种情况出现在 ppqq 圈进入外圈, 可以把转移轨道的箭头间距微调一下, 也是让外圈对齐
}
segIdx++;
}
length = nextLength;
}
// 把路径终点补上
result.Add(new ArrowData(path.GetPointAt(1.0), path.GetTangentAt(1.0), totalLength));
return result;
}
/// <summary>
/// Convert arrow data to sinmai format (Vector4)
/// </summary>
/// <param name="data">arrow data generated by BuildArrowData()</param>
/// <param name="starButton">button index of slide-star</param>
/// <param name="mirrorMode">mirror mode in user option</param>
/// <returns>sinmai format arrow data, referenced to slide-star</returns>
public static List<Vector4> ConvertAndRotateArrowData(IEnumerable<ArrowData> data, int starButton,
OptionMirrorID mirrorMode)
{
// SBGA 用 Vector4 存储了 slide 箭头的坐标与取向
// x, y 是平面坐标, z 是从起点到此处的路径长度 (px), w 是旋转的角度 (0 ~ 360 deg) (注意与切线方向差了 180 度)
// 坐标原点是屏幕中心, x 轴向右, y 轴向上
// w 的零点是朝向正右 (对应于箭头朝向正左), 逆时针为正方向
// 此外, sinmai 实际上是把所有 slide 路径相对于星星头存储的, 再在 SlideRoot 里通过 transform 转到合适的位置
// 判定区也是相对于星星头存储, 用 InputManager.ConvertTouchPanelRotatePush() 执行旋转
// 但是 slide code 定义的是绝对位置, 所以要逆向转回去, 以保证无论星星头在哪个键获取到的路径在处理过后都是一样的
// 然后还需要处理镜像的问题
var arrowList = new List<Vector4>();
var rotor = Complex.FromPolarCoordinates(1.0, Math.PI / 4.0 * starButton);
foreach (var arrow in data)
{
var pos = arrow.Point;
var tangent = arrow.Tangent;
switch (mirrorMode)
{
case OptionMirrorID.Normal:
break;
case OptionMirrorID.LR:
pos = Complex.Conjugate(pos) * -1.0;
tangent = Complex.Conjugate(tangent) * -1.0;
break;
case OptionMirrorID.UD:
pos = Complex.Conjugate(pos);
tangent = Complex.Conjugate(tangent);
break;
case OptionMirrorID.UDLR:
pos *= -1.0;
tangent *= -1.0;
break;
default:
break;
}
pos *= rotor;
tangent *= rotor;
var angle = tangent.Phase * 180.0 / Math.PI + 180.0; // Phase is in [-PI, PI]
arrowList.Add(new Vector4((float) pos.Real, (float) pos.Imaginary, (float) arrow.Length, (float) angle));
}
return arrowList;
}
public readonly struct HitAreaData(double push, double release, int[] areas)
{
public readonly double PushDistance = push;
public readonly double ReleaseDistance = release;
public readonly int[] PanelAreas = areas;
}
public static readonly Dictionary<int, HitAreaData[]> HitAreasLookup = new Dictionary<int, HitAreaData[]>();
public static void InitializeHitAreasLookup()
{
for (var i = 0; i < 8; i++)
{
for (var j = 0; j < 8; j++)
{
var diff = (j - i) & 7; // you know this is actually % 8 ... for same negative number compat
int tmp, tmp2;
// Ai -> Aj
var key = (i << 5) | j;
switch (diff)
{
case 1:
case 7:
HitAreasLookup[key] =
[
new HitAreaData(0.32, 0.68, [i]),
new HitAreaData(1.00, 1.00, [j])
];
break;
case 2:
tmp = (i + 1) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.20, 0.38, [i]),
new HitAreaData(0.62, 0.80, [tmp, tmp | 8]),
new HitAreaData(1.00, 1.00, [j])
];
break;
case 6:
tmp = (i - 1) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.20, 0.38, [i]),
new HitAreaData(0.62, 0.80, [tmp, tmp | 8]),
new HitAreaData(1.00, 1.00, [j])
];
break;
default:
break;
}
// Bi -> Bj
key = ((i | 8) << 5) | (j | 8);
switch (diff)
{
case 1:
case 7:
HitAreasLookup[key] =
[
new HitAreaData(0.44, 0.56, [i | 8]),
new HitAreaData(1.00, 1.00, [j | 8])
];
break;
case 2:
tmp = (i + 1) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.22, 0.35, [i | 8]),
new HitAreaData(0.65, 0.78, [tmp | 8, 16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
break;
case 6:
tmp = (i - 1) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.22, 0.35, [i | 8]),
new HitAreaData(0.65, 0.78, [tmp | 8, 16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
break;
case 3:
tmp = (i + 1) & 7;
tmp2 = (i + 2) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.15, 0.28, [i | 8]),
new HitAreaData(0.48, 0.52, [tmp | 8, 16]),
new HitAreaData(0.72, 0.85, [tmp2 | 8, 16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
break;
case 5:
tmp = (i - 1) & 7;
tmp2 = (i - 2) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.15, 0.28, [i | 8]),
new HitAreaData(0.48, 0.52, [tmp | 8, 16]),
new HitAreaData(0.72, 0.85, [tmp2 | 8, 16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
break;
default:
break;
}
// Ai <-> Bj
key = (i << 5) | (j | 8);
var key2 = ((j | 8) << 5) | i;
switch (diff)
{
case 0:
HitAreasLookup[key] =
[
new HitAreaData(0.60, 0.75, [i]),
new HitAreaData(1.00, 1.00, [j | 8])
];
HitAreasLookup[key2] =
[
new HitAreaData(0.25, 0.40, [j | 8]),
new HitAreaData(1.00, 1.00, [i])
];
break;
case 1:
case 7:
HitAreasLookup[key] =
[
new HitAreaData(0.45, 0.77, [i]),
new HitAreaData(1.00, 1.00, [j | 8])
];
HitAreasLookup[key2] =
[
new HitAreaData(0.23, 0.55, [j | 8]),
new HitAreaData(1.00, 1.00, [i])
];
break;
case 3:
tmp = (i + 1) & 7;
tmp2 = (i + 2) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.25, 0.34, [i]),
new HitAreaData(0.54, 0.68, [i | 8, tmp | 8]),
new HitAreaData(0.85, 0.90, [tmp2 | 8, 16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
HitAreasLookup[key2] =
[
new HitAreaData(0.10, 0.15, [j | 8]),
new HitAreaData(0.32, 0.46, [tmp2 | 8, 16]),
new HitAreaData(0.66, 0.75, [i | 8, tmp | 8]),
new HitAreaData(1.00, 1.00, [i])
];
break;
case 5:
tmp = (i - 1) & 7;
tmp2 = (i - 2) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.25, 0.34, [i]),
new HitAreaData(0.54, 0.68, [i | 8, tmp | 8]),
new HitAreaData(0.85, 0.90, [tmp2 | 8, 16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
HitAreasLookup[key2] =
[
new HitAreaData(0.10, 0.15, [j | 8]),
new HitAreaData(0.32, 0.46, [tmp2 | 8, 16]),
new HitAreaData(0.66, 0.75, [i | 8, tmp | 8]),
new HitAreaData(1.00, 1.00, [i])
];
break;
default:
break;
}
// C <-> Bj
key = (16 << 5) | (j | 8);
key2 = ((j | 8) << 5) | 16;
HitAreasLookup[key] =
[
new HitAreaData(0.50, 0.70, [16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
HitAreasLookup[key2] =
[
new HitAreaData(0.30, 0.50, [j | 8]),
new HitAreaData(1.00, 1.00, [16])
];
}
}
}
public static List<HitAreaData> BuildHitAreas(ParametricSlidePath path)
{
var nodeList = new List<Tuple<int, double>>();
var totalLength = path.GetPathLength();
var count = (int)Math.Round(totalLength / 10.0);
int? lastNode = null;
var enterLength = 0.0;
for (var i = 0; i < count; i++)
{
var t = (double)i / count;
var pt = path.GetPointAt(t);
int? node = null;
if (pt.Magnitude < 55.0)
{
node = 16;
}
else for (var j = 0; j < 8; j++)
{
var phi = Math.PI * (3.0 / 8.0 - j / 4.0);
if ((pt - Complex.FromPolarCoordinates(440.0, phi)).Magnitude < 80.0)
{
node = j;
break;
}
if ((pt - Complex.FromPolarCoordinates(210.0, phi)).Magnitude < 45.0)
{
node = j | 8;
break;
}
}
if (lastNode != node)
{
var length = t * totalLength;
if (lastNode == null)
{
enterLength = length;
}
else
{
nodeList.Add(new Tuple<int, double>(lastNode.Value, (length + enterLength) / 2.0));
if (node != null)
{
enterLength = length;
}
}
}
lastNode = node;
}
nodeList.Add(new Tuple<int, double>(lastNode!.Value, totalLength));
nodeList[0] = new Tuple<int, double>(nodeList[0].Item1, 0.0);
var result = new List<HitAreaData>();
result.Add(new HitAreaData(0.0, 0.0, [nodeList[0].Item1]));
for (var i = 1; i < nodeList.Count; i++)
{
var key = (nodeList[i - 1].Item1 << 5) | nodeList[i].Item1;
var segmentLength = nodeList[i].Item2 - nodeList[i - 1].Item2;
var data = HitAreasLookup[key];
var area = result[result.Count - 1];
result[result.Count - 1] = new HitAreaData(
area.PushDistance + segmentLength * data[0].PushDistance,
area.ReleaseDistance + segmentLength * data[0].ReleaseDistance,
area.PanelAreas
);
for (var j = 1; j < data.Length; j++)
{
result.Add(new HitAreaData(
segmentLength * (data[j].PushDistance - data[j - 1].ReleaseDistance),
segmentLength * (data[j].ReleaseDistance - data[j].PushDistance),
data[j].PanelAreas
));
}
}
double lastPushDistance = 0.0;
if (path.GetEndType(OptionMirrorID.Normal) == SlideType.Slide_Straight)
{
var diff = nodeList[nodeList.Count - 1].Item1 - nodeList[nodeList.Count - 2].Item1;
diff %= 8;
lastPushDistance = diff switch
{
1 or 2 or 6 or 7 => 130.0,
_ => 159.0
};
}
else
{
lastPushDistance = 175.0;
}
var last2ndArea = result[result.Count - 2];
var lastArea = result[result.Count - 1];
var distance = last2ndArea.ReleaseDistance + lastArea.PushDistance + lastArea.ReleaseDistance;
result[result.Count - 2] = new HitAreaData(last2ndArea.PushDistance, distance - lastPushDistance, last2ndArea.PanelAreas);
result[result.Count - 1] = new HitAreaData(lastPushDistance, 0.0, lastArea.PanelAreas);
return result;
}
/// <summary>
/// Convert hit area data to sinmai format (Vector4)
/// </summary>
/// <param name="data">hit area data generated by BuildHitAreas()</param>
/// <param name="starButton">button index of slide-star</param>
/// <param name="mirrorMode">mirror mode in user option</param>
/// <returns>sinmai format arrow data, referenced to slide-star</returns>
public static List<SlideManager.HitArea> ConvertAndRotateHitAreas(IEnumerable<HitAreaData> data, int starButton,
OptionMirrorID mirrorMode)
{
var hitAreaList = new List<SlideManager.HitArea>();
foreach (var hitAreaData in data)
{
var hitArea = new SlideManager.HitArea();
hitArea.PushDistance = hitAreaData.PushDistance;
hitArea.ReleaseDistance = hitAreaData.ReleaseDistance;
foreach (var pad in hitAreaData.PanelAreas)
{
var converted = MaiGeometry.MirrorInfo[(int) mirrorMode, pad];
converted = converted == 16 ? 16 : (converted - starButton) & 0b111 | converted & 0b1000;
hitArea.HitPoints.Add((InputManager.TouchPanelArea) converted);
}
hitAreaList.Add(hitArea);
}
return hitAreaList;
}
}

View File

@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace AquaMai.MaimaiDX2077;
public class SlidePathGenerator
{
public List<ParametricSlidePath.PathSegment> PathSegments = new List<ParametricSlidePath.PathSegment>();
public Complex CurrentEndPoint = Complex.Zero;
public static SlidePathGenerator BeginAt(Complex point)
{
var obj = new SlidePathGenerator();
obj.CurrentEndPoint = point;
return obj;
}
public static double CalcTangentAngle(Complex point, MaiGeometry.CircleStruct circle, bool isCcw)
{
var hypot = point - circle.Center;
var angleDelta = Math.Acos(circle.Radius / hypot.Magnitude);
var tanAngle = hypot.Phase + (isCcw ? angleDelta : -angleDelta);
return Math.IEEERemainder(tanAngle, Math.PI * 2.0);
}
public void TrySetLastParseMarker(ParametricSlidePath.ParseMarker marker)
{
if (PathSegments.Count <= 0) return;
PathSegments[PathSegments.Count - 1].SetParseMarker(marker);
}
public void LineToPoint(Complex point)
{
PathSegments.Add(new ParametricSlidePath.LineSegment(CurrentEndPoint, point));
CurrentEndPoint = point;
}
public void TangentToCircle(MaiGeometry.CircleStruct circle, bool isCcw)
{
var inAngle = CalcTangentAngle(CurrentEndPoint, circle, isCcw);
var inPoint = Complex.FromPolarCoordinates(circle.Radius, inAngle) + circle.Center;
LineToPoint(inPoint);
}
/// <summary>Note: endAngle should be in range [-PI, PI]</summary>
public void ArcToAngle(Complex center, double endAngle, bool isCcw, bool skipIfZero)
{
var diff = CurrentEndPoint - center;
var circle = new MaiGeometry.CircleStruct(center, diff.Magnitude);
var startAngle = diff.Phase;
// startAngle and endAngle in range [-PI, PI]
if (isCcw)
{
if (startAngle > endAngle)
{
startAngle -= 2 * Math.PI;
}
if (Math.Abs(endAngle - startAngle) < 0.001)
{
if (skipIfZero) return;
endAngle += 2 * Math.PI;
}
}
else
{
if (startAngle < endAngle)
{
startAngle += 2 * Math.PI;
}
if (Math.Abs(endAngle - startAngle) < 0.001)
{
if (skipIfZero) return;
endAngle -= 2 * Math.PI;
}
}
var seg = new ParametricSlidePath.ArcSegment(circle, startAngle, endAngle);
PathSegments.Add(seg);
CurrentEndPoint = seg.GetPointAt(1f);
}
public void ArcToTangentTowards(Complex target, Complex center, bool isCcw)
{
var diff = CurrentEndPoint - center;
var endAngle = CalcTangentAngle(target, new MaiGeometry.CircleStruct(center, diff.Magnitude), !isCcw);
ArcToAngle(center, endAngle, isCcw, false);
}
public void FullCircle(Complex center, bool isCcw)
{
var diff = CurrentEndPoint - center;
var circle = new MaiGeometry.CircleStruct(center, diff.Magnitude);
PathSegments.Add(new ParametricSlidePath.CircleSegment(circle, diff.Phase, isCcw));
// CurrentEndPoint not changed
}
public void ExternTangentTransfer(Complex currentCenter, MaiGeometry.CircleStruct targetCircle, bool isCcw)
{
var diff = CurrentEndPoint - currentCenter;
double endAngle;
if (Math.Abs(diff.Magnitude - targetCircle.Radius) < 0.001)
{
// two circles are approximately same radius
var vector = targetCircle.Center - currentCenter;
vector *= isCcw ? -Complex.ImaginaryOne : Complex.ImaginaryOne;
endAngle = vector.Phase;
}
else if (targetCircle.Radius > diff.Magnitude)
{
// target circle larger
var helperCircle = new MaiGeometry.CircleStruct(targetCircle.Center, targetCircle.Radius - diff.Magnitude);
endAngle = CalcTangentAngle(currentCenter, helperCircle, isCcw);
}
else
{
var helperCircle = new MaiGeometry.CircleStruct(currentCenter, diff.Magnitude - targetCircle.Radius);
endAngle = CalcTangentAngle(targetCircle.Center, helperCircle, !isCcw);
}
ArcToAngle(currentCenter, endAngle, isCcw, false);
var inPoint = Complex.FromPolarCoordinates(targetCircle.Radius, endAngle) + targetCircle.Center;
LineToPoint(inPoint);
}
public ParametricSlidePath GeneratePath()
{
return new ParametricSlidePath(PathSegments);
}
}

View File

@ -6,10 +6,12 @@ using System.Runtime.InteropServices;
using System.Threading;
using AquaMai.Fix;
using AquaMai.Helpers;
using AquaMai.MaimaiDX2077;
using AquaMai.Resources;
using AquaMai.Utils;
using AquaMai.UX;
using MelonLoader;
using Monitor;
using Tomlet;
using UnityEngine;
@ -162,7 +164,7 @@ namespace AquaMai
Patch(typeof(DebugFeature));
if (GameInfo.GameVersion >= 23000)
Patch(typeof(FixConnSlide));
Patch(typeof(SlideAutoPlayTweak));
Patch(typeof(FixSlideAutoPlay)); // Rename: SlideAutoPlayTweak -> FixSlideAutoPlay, 不过这个应该无副作用所以不需要改配置文件
if (GameInfo.GameVersion >= 24000)
Patch(typeof(FixLevelDisplay));
// UX
@ -173,6 +175,28 @@ namespace AquaMai
// Utils
Patch(typeof(JudgeAdjust));
Patch(typeof(TouchPanelBaudRate));
// New Features & Changes
// 现在自定义皮肤相关的功能应该有 CustomSkin, JudgeDisplay4B, CustomTrackStartDiff
// 后续应该还会接着做, 所以也许可以考虑把自定义皮肤相关的部分单独分一类 ?
Patch(typeof(CustomSkins)); // Rename: CustomNoteSkin -> CustomSkins
Patch(typeof(JudgeDisplay4B));
Patch(typeof(CustomTrackStartDiff));
Patch(typeof(RealisticRandomJudge)); // 本来是用来调试判定显示4B的, 觉得还挺有趣就单独做成功能了
Patch(typeof(DisableTrackStartTabs)); // 从 TrackStartProcessTweak 里单独拆出来了
// 以下三项拆分自 SlideJudgeTweak
Patch(typeof(FanJudgeFlip));
Patch(typeof(BreakSlideJudgeBlink));
Patch(typeof(FixCircleSlideJudge)); // 这个我觉得算无副作用, 可以常开
// 这是一项往 Sinmai 里加各种新 note 的企划, 目前只完成了可高度自定义形状的星星
// 未来还会缓慢更新, 我建议单开一个功能分类
// 注意需要往 UserLib 里放入 System.Numeric.dll
Patch(typeof(CustomNoteTypePatch));
# if DEBUG
Patch(typeof(LogNetworkErrors));
# endif

View File

@ -10,13 +10,18 @@ using UnityEngine;
namespace AquaMai.UX;
public class CustomNoteSkin
public class CustomSkins
{
private static readonly List<string> ImageExts = [".png", ".jpg", ".jpeg"];
private static readonly List<string> SlideFanFields = ["_normalSlideFan", "_eachSlideFan", "_breakSlideFan", "_breakSlideFanEff"];
private static readonly List<string> CustomTrackStartFields = ["_musicBase", "_musicTab", "_musicLvBase", "_musicLvText"];
private static Sprite customOutline;
private static Sprite[,] customSlideFan = new Sprite[4, 11];
public static readonly Sprite[,] CustomJudge = new Sprite[2, ((int)NoteJudge.ETiming.End + 1)];
public static readonly Sprite[,,,] CustomJudgeSlide = new Sprite[2, 3, 2, ((int)NoteJudge.ETiming.End + 1)];
public static readonly Texture2D[] CustomTrackStart = new Texture2D[4];
private static bool LoadIntoGameNoteImageContainer(string fieldName, int? idx1, int? idx2, Texture2D texture)
{
@ -105,8 +110,17 @@ public class CustomNoteSkin
var fieldName = '_' + args[0];
int? idx1 = (args.Length < 2) ? null : (int.TryParse(args[1], out var temp) ? temp : null);
int? idx2 = (args.Length < 3) ? null : (int.TryParse(args[2], out temp) ? temp : null);
int? idx3 = (args.Length < 4) ? null : (int.TryParse(args[3], out temp) ? temp : null);
Traverse traverse;
if (CustomTrackStartFields.Contains(fieldName))
{
var i = CustomTrackStartFields.IndexOf(fieldName);
CustomTrackStart[i] = texture;
MelonLogger.Msg($"[CustomNoteSkin] Successfully loaded {name}");
continue;
}
if (fieldName == "_outline")
{
@ -115,6 +129,59 @@ public class CustomNoteSkin
continue;
}
if (fieldName == "_judgeNormal" || fieldName == "_judgeBreak")
{
if (!idx1.HasValue)
{
MelonLogger.Msg($"[CustomNoteSkin] Field {fieldName} needs a index");
continue;
}
var i = (fieldName == "_judgeBreak") ? 1 : 0;
CustomJudge[i, idx1.Value] = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f), 1f);
MelonLogger.Msg($"[CustomNoteSkin] Successfully loaded {name}");
continue;
}
if (fieldName == "_judgeSlideNormal" || fieldName == "_judgeSlideBreak")
{
if (!idx1.HasValue || !idx2.HasValue || !idx3.HasValue)
{
MelonLogger.Msg($"[CustomNoteSkin] Field {fieldName} needs 3 indices");
continue;
}
var i = (fieldName == "_judgeSlideBreak") ? 1 : 0;
Vector2 pivot;
switch (idx1.Value)
{
case 0 when idx2.Value == 0:
pivot = new Vector2(0f, 0.5f);
break;
case 0 when idx2.Value == 1:
pivot = new Vector2(1f, 0.5f);
break;
case 1 when idx2.Value == 0:
pivot = new Vector2(0f, 0.3f);
break;
case 1 when idx2.Value == 1:
pivot = new Vector2(1f, 0.3f);
break;
case 2 when idx2.Value == 0:
pivot = new Vector2(0.5f, 0.8f);
break;
case 2 when idx2.Value == 1:
pivot = new Vector2(0.5f, 0.2f);
break;
default:
pivot = new Vector2(0.5f, 0.5f);
break;
}
CustomJudgeSlide[i, idx1.Value, idx2.Value, idx3.Value] = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), pivot, 1f);
MelonLogger.Msg($"[CustomNoteSkin] Successfully loaded {name}");
continue;
}
if (SlideFanFields.Contains(fieldName))
{
if (!idx1.HasValue)

View File

@ -0,0 +1,66 @@
using System.Collections.Generic;
using HarmonyLib;
using Monitor;
using UI;
using UnityEngine;
using UnityEngine.UI;
namespace AquaMai.UX;
public class CustomTrackStartDiff
{
// 自定义在歌曲开始界面上显示的难度 (并不是真的自定义难度)
// 需要启用自定义皮肤功能
// 会加载四个图片资源: musicBase, musicTab, musicLvBase, musicLvText
[HarmonyPostfix]
[HarmonyPatch(typeof(TrackStartMonitor), "SetTrackStart")]
private static void DisableTabs(
MultipleImage ____musicBaseImage,
MultipleImage ____musicTabImage,
SpriteCounter ____difficultySingle,
SpriteCounter ____difficultyDouble,
Image ____levelTextImage,
List<ResultMonitor.SpriteSheet> ____musicLevelSpriteSheets,
TimelineRoot ____musicDetail
)
{
var texture = CustomSkins.CustomTrackStart[0];
if (texture != null)
{
____musicBaseImage.MultiSprites[6] = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f), 100f);
____musicBaseImage.ChangeSprite(6);
}
texture = CustomSkins.CustomTrackStart[1];
if (texture != null)
{
____musicTabImage.MultiSprites[6] = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f), 100f);
____musicTabImage.ChangeSprite(6);
}
texture = CustomSkins.CustomTrackStart[2];
if (texture != null)
{
var lvBase = Traverse.Create(____musicDetail).Field<MultipleImage>("_lv_Base").Value;
lvBase.MultiSprites[6] = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f), 100f);
lvBase.ChangeSprite(6);
}
texture = CustomSkins.CustomTrackStart[3];
if (texture != null)
{
var original = ____musicLevelSpriteSheets[0].Sheet;
var sheet = new Sprite[original.Length];
for (var i = 0; i < original.Length; i++)
{
var sprite = original[i];
sheet[i] = Sprite.Create(texture, sprite.textureRect, new Vector2(0.5f, 0.5f), 100f);
}
____difficultySingle.SetSpriteSheet(sheet);
____difficultyDouble.SetSpriteSheet(sheet);
____levelTextImage.sprite = sheet[14];
}
}
}

View File

@ -0,0 +1,30 @@
using System.Collections.Generic;
using HarmonyLib;
using Monitor;
using UI;
using UnityEngine;
using UnityEngine.UI;
namespace AquaMai.UX;
public class DisableTrackStartTabs
{
// 在歌曲开始界面, 把 TRACK X 字样, DX/标准谱面的显示框, 以及画面下方的滴蜡熊隐藏掉, 让他看起来不那么 sinmai, 更像是 majdata
[HarmonyPostfix]
[HarmonyPatch(typeof(TrackStartMonitor), "SetTrackStart")]
private static void DisableTabs(
SpriteCounter ____trackNumber, SpriteCounter ____bossTrackNumber, SpriteCounter ____utageTrackNumber,
MultipleImage ____musicTabImage, GameObject[] ____musicTabObj, GameObject ____derakkumaRoot
)
{
____trackNumber.transform.parent.gameObject.SetActive(false);
____bossTrackNumber.transform.parent.gameObject.SetActive(false);
____utageTrackNumber.transform.parent.gameObject.SetActive(false);
____musicTabImage.gameObject.SetActive(false);
____musicTabObj[0].gameObject.SetActive(false);
____musicTabObj[1].gameObject.SetActive(false);
____musicTabObj[2].gameObject.SetActive(false);
____derakkumaRoot.SetActive(false);
}
}

View File

@ -0,0 +1,75 @@
using HarmonyLib;
using Manager;
using Monitor;
using UnityEngine;
namespace AquaMai.UX;
public class JudgeDisplay4B
{
// 精确到子判定的自定义判定显示, 需要启用自定义皮肤功能 (理论上不启用自定义皮肤不会崩游戏, 只不过此时这个功能显然不会生效)
[HarmonyPostfix]
[HarmonyPatch(typeof(SlideJudge), "Initialize")]
private static void SlideJudgeDisplay4B(
SpriteRenderer ___SpriteRenderAdd, SpriteRenderer ___SpriteRender,
SlideJudge.SlideJudgeType ____judgeType, SlideJudge.SlideAngle ____angle,
NoteJudge.ETiming judge, float msec, bool isBreak
)
{
var i = isBreak ? 1 : 0;
Sprite sprite = CustomSkins.CustomJudgeSlide[i, (int)____judgeType, (int)____angle, (int)judge];
if (sprite != null) {
___SpriteRender.sprite = sprite;
}
if (isBreak && judge == NoteJudge.ETiming.Critical)
{
sprite = CustomSkins.CustomJudgeSlide[i, (int)____judgeType, (int)____angle, (int) NoteJudge.ETiming.End];
if (sprite != null)
{
___SpriteRenderAdd.sprite = sprite;
}
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(JudgeGrade), "Initialize")]
private static void JudgeGradeDisplay4B(
SpriteRenderer ___SpriteRender,
NoteJudge.ETiming judge, float msec, NoteJudge.EJudgeType type
)
{
var i = (type == NoteJudge.EJudgeType.Break) ? 1 : 0;
Sprite sprite = CustomSkins.CustomJudge[i, (int)judge];
if (sprite != null) {
___SpriteRender.sprite = sprite;
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(JudgeGrade), "InitializeBreak")]
private static void JudgeGradeBreakDisplay4B(
SpriteRenderer ___SpriteRenderAdd,
NoteJudge.ETiming judge, float msec, NoteJudge.EJudgeType type
)
{
if (judge == NoteJudge.ETiming.Critical)
{
var sprite = CustomSkins.CustomJudge[1, (int) NoteJudge.ETiming.End];
if (sprite != null)
{
___SpriteRenderAdd.sprite = sprite;
}
}
}
[HarmonyPrefix]
[HarmonyPatch(typeof(JudgeGrade), "InitializeBreak")]
private static void InitializeBreakFix(ref NoteJudge.EJudgeType type)
{
type = NoteJudge.EJudgeType.Break;
}
}

View File

@ -0,0 +1,25 @@
using HarmonyLib;
using Manager;
namespace AquaMai.UX;
public class RealisticRandomJudge
{
// 让 AutoPlay 的随机判定模式真的会随机产生所有的判定 (精确到子判定)
// 原本的随机判定只会等概率产生 Critical, LateGreat1st, LateGood, Miss(TooLate)
// 这里改成三角分布产生从 Miss(TooFast) ~ Critical ~ Miss(TooLate) 的所有 15 种判定结果
// 当然, 此处并不会考虑原本那个 Note 是不是真的有对应的判定 (比如 Slide 实际上不应该有小 p 之类的)
[HarmonyPostfix]
[HarmonyPatch(typeof(GameManager), "AutoJudge")]
private static NoteJudge.ETiming RealAutoJudgeRandom(NoteJudge.ETiming retval)
{
if (GameManager.AutoPlay == GameManager.AutoPlayMode.Random)
{
var x = UnityEngine.Random.Range(0, 8);
x += UnityEngine.Random.Range(0, 8);
return (NoteJudge.ETiming) x;
}
return retval;
}
}

View File

@ -1,8 +1,10 @@
using HarmonyLib;
using System.Collections.Generic;
using HarmonyLib;
using Monitor;
using Process;
using UI;
using UnityEngine;
using UnityEngine.UI;
namespace AquaMai.UX;
@ -10,7 +12,6 @@ public class TrackStartProcessTweak
{
// 总之这个 Patch 没啥用, 是我个人用 sinmai 录谱面确认时用得到, 顺手也写进来了
// 具体而言就是推迟了歌曲开始界面的动画便于后期剪辑
// 然后把“TRACK X”字样和 DX/标准谱面的显示框隐藏掉, 让他看起来不那么 sinmai, 更像是 majdata
[HarmonyPrefix]
[HarmonyPatch(typeof(TrackStartProcess), "OnUpdate")]
@ -65,19 +66,6 @@ public class TrackStartProcessTweak
return true;
}
[HarmonyPostfix]
[HarmonyPatch(typeof(TrackStartMonitor), "SetTrackStart")]
private static void DisableTabs(
SpriteCounter ____trackNumber, SpriteCounter ____bossTrackNumber, SpriteCounter ____utageTrackNumber,
MultipleImage ____musicTabImage, GameObject[] ____musicTabObj
)
{
____trackNumber.transform.parent.gameObject.SetActive(false);
____bossTrackNumber.transform.parent.gameObject.SetActive(false);
____utageTrackNumber.transform.parent.gameObject.SetActive(false);
____musicTabImage.gameObject.SetActive(false);
____musicTabObj[0].gameObject.SetActive(false);
____musicTabObj[1].gameObject.SetActive(false);
____musicTabObj[2].gameObject.SetActive(false);
}
}