diff --git a/AquaMai/AquaMai.csproj b/AquaMai/AquaMai.csproj
index 9965e89f..74d23044 100644
--- a/AquaMai/AquaMai.csproj
+++ b/AquaMai/AquaMai.csproj
@@ -292,12 +292,14 @@ DEBUG
+
+
@@ -312,6 +314,9 @@ DEBUG
+
+
+
True
True
diff --git a/AquaMai/CustomSkin/CustomNoteSkin.cs b/AquaMai/CustomSkin/CustomNoteSkin.cs
new file mode 100644
index 00000000..24b0b891
--- /dev/null
+++ b/AquaMai/CustomSkin/CustomNoteSkin.cs
@@ -0,0 +1,314 @@
+using System;
+using System.IO;
+using System.Collections.Generic;
+using System.Reflection;
+using HarmonyLib;
+using MelonLoader;
+using Monitor;
+using Monitor.Game;
+using Process;
+using UnityEngine;
+
+namespace AquaMai.CustomSkin;
+
+public class CustomNoteSkin
+{
+ private static readonly List ImageExts = [".jpg", ".png", ".jpeg"];
+ private static readonly List SlideFanFields = ["_normalSlideFan", "_eachSlideFan", "_breakSlideFan", "_breakSlideFanEff"];
+
+ private static Sprite customOutline;
+ private static Sprite[,] customSlideFan = new Sprite[4, 11];
+
+ private static bool LoadIntoGameNoteImageContainer(string fieldName, int? idx1, int? idx2, Texture2D texture)
+ {
+ // 先确定确实有这个 Field, 如果没有的话可以直接跳过这个文件
+ var fieldTraverse = Traverse.Create(typeof(GameNoteImageContainer)).Field(fieldName);
+ if (!fieldTraverse.FieldExists())
+ {
+ MelonLogger.Msg($"[CustomNoteSkin] Cannot found field {fieldName}");
+ return false;
+ }
+
+ var fieldType = fieldTraverse.GetValueType();
+ if (!idx1.HasValue)
+ {
+ // 目标 Field 应当是单个 Sprite
+ if (fieldType != typeof(Sprite))
+ {
+ MelonLogger.Msg($"[CustomNoteSkin] Field {fieldName} is a {fieldType.Name}, not a Sprite");
+ return false;
+ }
+ var target = fieldTraverse.GetValue();
+ var pivot = new Vector2(target.pivot.x / target.rect.width, target.pivot.y / target.rect.height);
+ var custom = Sprite.Create(
+ texture, new Rect(0, 0, texture.width, texture.height), pivot, 1f,
+ 0, SpriteMeshType.Tight, target.border
+ );
+ fieldTraverse.SetValue(custom);
+ }
+ else if (!idx2.HasValue)
+ {
+ // 目标 Field 是一维数组
+ if (fieldType != typeof(Sprite[]))
+ {
+ MelonLogger.Msg($"[CustomNoteSkin] Field {fieldName} is a {fieldType.Name}, not a Sprite[]");
+ return false;
+ }
+ var targetArray = fieldTraverse.GetValue();
+ var target = targetArray[idx1.Value];
+ var pivot = new Vector2(target.pivot.x / target.rect.width, target.pivot.y / target.rect.height);
+ var custom = Sprite.Create(
+ texture, new Rect(0, 0, texture.width, texture.height), pivot, 1f,
+ 0, SpriteMeshType.Tight, target.border
+ );
+ targetArray[idx1.Value] = custom;
+ }
+ else
+ {
+ // 目标 Field 是二维数组
+ if (fieldType != typeof(Sprite[,]))
+ {
+ MelonLogger.Msg($"[CustomNoteSkin] Field {fieldName} is a {fieldType.Name}, not a Sprite[,]");
+ return false;
+ }
+ var targetArray = fieldTraverse.GetValue();
+ var target = targetArray[idx1.Value, idx2.Value];
+ var pivot = new Vector2(target.pivot.x / target.rect.width, target.pivot.y / target.rect.height);
+ var custom = Sprite.Create(
+ texture, new Rect(0, 0, texture.width, texture.height), pivot, 1f,
+ 0, SpriteMeshType.Tight, target.border
+ );
+ targetArray[idx1.Value, idx2.Value] = custom;
+ }
+
+ return true;
+ }
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(GameNotePrefabContainer), "Initialize")]
+ private static void LoadNoteSkin()
+ {
+ if (!Directory.Exists(Path.Combine(Environment.CurrentDirectory, "Skins"))) return;
+
+ foreach (var laFile in Directory.EnumerateFiles(Path.Combine(Environment.CurrentDirectory, "Skins")))
+ {
+ if (!ImageExts.Contains(Path.GetExtension(laFile).ToLowerInvariant())) continue;
+ var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
+ texture.LoadImage(File.ReadAllBytes(laFile));
+
+ var name = Path.GetFileNameWithoutExtension(laFile);
+ var args = name.Split('_');
+ // 文件名的格式是 XXXXXXXX_A_B 表示 GameNoteImageContainer._XXXXXXXX[A, B]
+ // 视具体情况, A, B 可能不存在
+ 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);
+
+ Traverse traverse;
+
+ if (fieldName == "_outline")
+ {
+ customOutline = 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 (SlideFanFields.Contains(fieldName))
+ {
+ if (!idx1.HasValue)
+ {
+ MelonLogger.Msg($"[CustomNoteSkin] Field {fieldName} needs a index");
+ continue;
+ }
+ var i = SlideFanFields.IndexOf(fieldName);
+ customSlideFan[i, idx1.Value] = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(1f, 0.5f), 1f);
+ MelonLogger.Msg($"[CustomNoteSkin] Successfully loaded {name}");
+ continue;
+ }
+
+ if (fieldName == "_touchJust")
+ {
+ traverse = Traverse.Create(GameNotePrefabContainer.TouchTapB);
+ var noticeObject = traverse.Field("NoticeObject").Value;
+ var target = noticeObject.GetComponent();
+ var pivot = new Vector2(
+ target.sprite.pivot.x / target.sprite.rect.width,
+ target.sprite.pivot.y / target.sprite.rect.height
+ );
+ var custom = Sprite.Create(
+ texture, new Rect(0, 0, texture.width, texture.height), pivot, 1f,
+ 0, SpriteMeshType.Tight, target.sprite.border
+ );
+ target.sprite = custom;
+
+ traverse = Traverse.Create(GameNotePrefabContainer.TouchTapC);
+ noticeObject = traverse.Field("NoticeObject").Value;
+ noticeObject.GetComponent().sprite = custom;
+ MelonLogger.Msg($"[CustomNoteSkin] Successfully loaded {name}");
+ continue;
+ }
+
+ if (fieldName == "_touchHold")
+ {
+ if (!idx1.HasValue)
+ {
+ MelonLogger.Msg($"[CustomNoteSkin] Field {fieldName} needs a index");
+ continue;
+ }
+ traverse = Traverse.Create(GameNotePrefabContainer.TouchHoldC);
+ var target = traverse.Field("ColorsObject").Value;
+ var renderer = target[idx1.Value];
+ var pivot = new Vector2(
+ renderer.sprite.pivot.x / renderer.sprite.rect.width,
+ renderer.sprite.pivot.y / renderer.sprite.rect.height
+ );
+ var custom = Sprite.Create(
+ texture, new Rect(0, 0, texture.width, texture.height), pivot, 1f,
+ 0, SpriteMeshType.Tight, renderer.sprite.border
+ );
+ renderer.sprite = custom;
+ MelonLogger.Msg($"[CustomNoteSkin] Successfully loaded {name}");
+ continue;
+ }
+
+ if (fieldName == "_normalTouchBorder")
+ {
+ if (!idx1.HasValue)
+ {
+ MelonLogger.Msg($"[CustomNoteSkin] Field {fieldName} needs a index");
+ continue;
+ }
+ traverse = Traverse.Create(GameNotePrefabContainer.TouchReserve);
+ var target = traverse.Field("_reserveSingleSprite").Value;
+ var targetSprite = target[idx1.Value - 2];
+ var pivot = new Vector2(
+ targetSprite.pivot.x / targetSprite.rect.width,
+ targetSprite.pivot.y / targetSprite.rect.height
+ );
+ target[idx1.Value - 2] = Sprite.Create(
+ texture, new Rect(0, 0, texture.width, texture.height), pivot, 1f,
+ 0, SpriteMeshType.Tight, targetSprite.border
+ );
+ MelonLogger.Msg($"[CustomNoteSkin] Successfully loaded {name}");
+ continue;
+ }
+
+ if (fieldName == "_eachTouchBorder")
+ {
+ if (!idx1.HasValue)
+ {
+ MelonLogger.Msg($"[CustomNoteSkin] Field {fieldName} needs a index");
+ continue;
+ }
+ traverse = Traverse.Create(GameNotePrefabContainer.TouchReserve);
+ var target = traverse.Field("_reserveEachSprite").Value;
+ var targetSprite = target[idx1.Value - 2];
+ var pivot = new Vector2(
+ targetSprite.pivot.x / targetSprite.rect.width,
+ targetSprite.pivot.y / targetSprite.rect.height
+ );
+ target[idx1.Value - 2] = Sprite.Create(
+ texture, new Rect(0, 0, texture.width, texture.height), pivot, 1f,
+ 0, SpriteMeshType.Tight, targetSprite.border
+ );
+ MelonLogger.Msg($"[CustomNoteSkin] Successfully loaded {name}");
+ continue;
+ }
+
+ if (LoadIntoGameNoteImageContainer(fieldName, idx1, idx2, texture))
+ {
+ MelonLogger.Msg($"[CustomNoteSkin] Successfully loaded {name}");
+ }
+ }
+ }
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(GameCtrl), "Initialize")]
+ private static void ChangeOutlineTexture(GameObject ____guideEndPointObj)
+ {
+ if (____guideEndPointObj != null && customOutline != null)
+ {
+ ____guideEndPointObj.GetComponent().sprite = customOutline;
+ }
+ }
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(SlideFan), "Initialize")]
+ private static void ChangeFanTexture(
+ SpriteRenderer[] ____spriteLines, SpriteRenderer[] ____effectSprites, bool ___BreakFlag, bool ___EachFlag
+ )
+ {
+ Vector3 position;
+ Sprite sprite;
+ if (___BreakFlag)
+ {
+ for (var i = 0; i < 11; i++)
+ {
+ sprite = customSlideFan[2, i];
+ if (sprite != null)
+ {
+ ____spriteLines[2 * i].sprite = sprite;
+ position = ____spriteLines[2 * i].transform.localPosition;
+ ____spriteLines[2 * i].transform.localPosition = new Vector3(0, position.y, position.z);
+ ____spriteLines[2 * i].color = Color.white;
+
+ ____spriteLines[2 * i + 1].sprite = sprite;
+ position = ____spriteLines[2 * i + 1].transform.localPosition;
+ ____spriteLines[2 * i + 1].transform.localPosition = new Vector3(0, position.y, position.z);
+ ____spriteLines[2 * i + 1].color = Color.white;
+ }
+ sprite = customSlideFan[3, i];
+ if (sprite != null)
+ {
+ ____effectSprites[2 * i].sprite = sprite;
+ position = ____effectSprites[2 * i].transform.localPosition;
+ ____effectSprites[2 * i].transform.localPosition = new Vector3(0, position.y, position.z);
+ ____effectSprites[2 * i].color = Color.white;
+
+ ____effectSprites[2 * i + 1].sprite = sprite;
+ position = ____effectSprites[2 * i + 1].transform.localPosition;
+ ____effectSprites[2 * i + 1].transform.localPosition = new Vector3(0, position.y, position.z);
+ ____effectSprites[2 * i + 1].color = Color.white;
+ }
+ }
+ }
+ else if (___EachFlag)
+ {
+ for (var i = 0; i < 11; i++)
+ {
+ sprite = customSlideFan[1, i];
+ if (sprite != null)
+ {
+ ____spriteLines[2 * i].sprite = sprite;
+ position = ____spriteLines[2 * i].transform.localPosition;
+ ____spriteLines[2 * i].transform.localPosition = new Vector3(0, position.y, position.z);
+ ____spriteLines[2 * i].color = Color.white;
+
+ ____spriteLines[2 * i + 1].sprite = sprite;
+ position = ____spriteLines[2 * i + 1].transform.localPosition;
+ ____spriteLines[2 * i + 1].transform.localPosition = new Vector3(0, position.y, position.z);
+ ____spriteLines[2 * i + 1].color = Color.white;
+ }
+ }
+ }
+ else
+ {
+ for (var i = 0; i < 11; i++)
+ {
+ sprite = customSlideFan[0, i];
+ if (sprite != null)
+ {
+ ____spriteLines[2 * i].sprite = sprite;
+ position = ____spriteLines[2 * i].transform.localPosition;
+ ____spriteLines[2 * i].transform.localPosition = new Vector3(0, position.y, position.z);
+ ____spriteLines[2 * i].color = Color.white;
+
+ ____spriteLines[2 * i + 1].sprite = sprite;
+ position = ____spriteLines[2 * i + 1].transform.localPosition;
+ ____spriteLines[2 * i + 1].transform.localPosition = new Vector3(0, position.y, position.z);
+ ____spriteLines[2 * i + 1].color = Color.white;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AquaMai/Fix/FixConnSlide.cs b/AquaMai/Fix/FixConnSlide.cs
new file mode 100644
index 00000000..138716ad
--- /dev/null
+++ b/AquaMai/Fix/FixConnSlide.cs
@@ -0,0 +1,64 @@
+using System.Collections.Generic;
+using System.Reflection;
+using HarmonyLib;
+using Manager;
+using MelonLoader;
+using Monitor;
+
+namespace AquaMai.Fix;
+
+public class FixConnSlide
+{
+ /* 这个 Patch 用于修复以下 bug:
+ * 非 ConnSlide 被错误解析为 ConnSlide (Fes 首日刹那旅程爆机 bug)
+ * 原 method 逻辑如下:
+ *
+ * if (this.IsSlideAll(noteData1.type) && (index1 + 1 < this._note._noteData.Count ? 1 : 0) != 0)
+ * {
+ * int targetNote = noteData1.slideData.targetNote;
+ * if (noteData1.slideData != null)
+ * targetNote = noteData1.slideData.targetNote;
+ * for (int index3 = index1; index3 < this._note._noteData.Count; ++index3)
+ * {
+ * NoteData noteData3 = this._note._noteData[index3];
+ * if (this.IsSlideAll(noteData3.type) && noteData3.time == noteData1.end && noteData3.startButtonPos == targetNote && noteData3.parent == null)
+ * {
+ * noteData3.parent = noteData1.parent;
+ * noteData1.child.Add(noteData3);
+ * noteData3.isUsed = true;
+ * noteData3.isJudged = true;
+ * break;
+ * }
+ * }
+ * }
+ *
+ * 修复 bug 需要把第二次调用 this.IsSlideAll() 更改为 this.IsConnectNote(), 这里使用 Transpiler 解决
+ */
+ [HarmonyTranspiler]
+ [HarmonyPatch(typeof(NotesReader), "calcSlide")]
+ private static IEnumerable Fix(IEnumerable instructions)
+ {
+ List instList = new List(instructions);
+ bool found = false;
+ MethodInfo methodIsSlideAll = AccessTools.Method(typeof(NotesReader), "IsSlideAll");
+ MethodInfo methodIsConnectNote = AccessTools.Method(typeof(NotesReader), "IsConnectNote");
+
+ for (int i = 0; i < instList.Count; i++)
+ {
+ CodeInstruction inst = instList[i];
+ if (!found && inst.Calls(methodIsSlideAll))
+ {
+ found = true;
+ continue;
+ }
+
+ if (found && inst.Calls(methodIsSlideAll))
+ {
+ inst.operand = methodIsConnectNote;
+ // MelonLogger.Msg($"[FixConnSlide] Successfully patched NotesReader::calcSlide");
+ break;
+ }
+ }
+ return instList;
+ }
+}
\ No newline at end of file
diff --git a/AquaMai/Main.cs b/AquaMai/Main.cs
index 7d8619b9..97910f59 100644
--- a/AquaMai/Main.cs
+++ b/AquaMai/Main.cs
@@ -4,8 +4,10 @@
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
+using AquaMai.CustomSkin;
using AquaMai.Fix;
using AquaMai.Helpers;
+using AquaMai.RenderTweak;
using AquaMai.Resources;
using AquaMai.Utils;
using AquaMai.UX;
@@ -164,6 +166,13 @@ public override void OnInitializeMelon()
Patch(typeof(RunCommandOnEvents));
// Utils
Patch(typeof(JudgeAdjust));
+
+ // My Patches
+ Patch(typeof(CustomNoteSkin));
+ Patch(typeof(SlideAutoPlayTweak));
+ Patch(typeof(TrackStartProcessTweak));
+ Patch(typeof(SlideJudgeTweak));
+ Patch(typeof(FixConnSlide));
# if DEBUG
Patch(typeof(LogNetworkErrors));
# endif
diff --git a/AquaMai/RenderTweak/SlideAutoPlayTweak.cs b/AquaMai/RenderTweak/SlideAutoPlayTweak.cs
new file mode 100644
index 00000000..17017b6f
--- /dev/null
+++ b/AquaMai/RenderTweak/SlideAutoPlayTweak.cs
@@ -0,0 +1,102 @@
+using System.Collections.Generic;
+using HarmonyLib;
+using Manager;
+using Monitor;
+
+namespace AquaMai.RenderTweak;
+
+public class SlideAutoPlayTweak
+{
+ /* 这个 Patch 用于修复以下 bug:
+ * SlideFan 在 AutoPlay 时, 只有第一个箭头会消失
+ * 原 method 逻辑如下:
+ *
+ * if (this.IsNoteCheckTimeStartIgnoreJudgeWait())
+ * {
+ * // do something ...
+ * if (!GameManager.IsAutoPlay())
+ * {
+ * // do something ...
+ * for (int index = 0; index < this._arrowPrefubs.Length && (double) index < (double) num2 * 11.0; ++index)
+ * {
+ * // do something about displaying arrows ...
+ * }
+ * }
+ * else
+ * {
+ * float num4 = (currentMsec - this.StarLaunchMsec) / (this.StarArriveMsec - this.StarLaunchMsec - this.lastWaitTime);
+ * for (int index = 0; index < this._arrowPrefubs.Length && (double) index < (double) num4 * 1.0; ++index)
+ * {
+ * // do something about displaying arrows ...
+ * }
+ * if ((double) num4 > 1.0)
+ * num1 = 3;
+ * }
+ * // do something ...
+ * }
+ *
+ * 导致这个 bug 的原因是 else 分支的 for 循环终止条件写错了, 应该是 11.0 (因为有 11 个箭头), SBGA 写成了 1.0
+ * 这个 method 中一共只有 5 处 ldc.r4 的 IL Code, 依次为 10.0, 11.0, 1.0, 1.0, 0.0
+ * 修复 bug 需要把第三处的 1.0 更改为 11.0, 这里使用 Transpiler 解决
+ */
+ [HarmonyTranspiler]
+ [HarmonyPatch(typeof(SlideFan), "NoteCheck")]
+ private static IEnumerable FixFanAutoPlayArrow(IEnumerable instructions)
+ {
+ List instList = new List(instructions);
+ bool found = false;
+ for (int i = 0; i < instList.Count; i++)
+ {
+ CodeInstruction inst = instList[i];
+ if (inst.LoadsConstant(11.0))
+ {
+ found = true;
+ }
+
+ if (found && inst.LoadsConstant(1.0))
+ {
+ inst.operand = 11.0f;
+ break;
+ }
+ }
+ return instList;
+ }
+
+ /* 这个 Patch 让 Slide 在 AutoPlay 的时候, 每个区仍然会分按下和松开两段进行推进 (加上 this._hitIn 的变化)
+ * 原 method 逻辑如下:
+ *
+ * if (!GameManager.IsAutoPlay())
+ * {
+ * // do somethings ...
+ * }
+ * else
+ * {
+ * float num1 = (currentMsec - this.StarLaunchMsec) / (this.StarArriveMsec - this.StarLaunchMsec - this.lastWaitTime);
+ * this._hitIndex = (int) ((double) this._hitAreaList.Count * (double) num1);
+ * if (this._hitIndex >= this._hitAreaList.Count)
+ * this._hitIndex = this._hitAreaList.Count - 1;
+ * if (this._hitIndex < 0)
+ * this._hitIndex = 0;
+ * int num2 = (int) ((double) this._dispLaneNum * this.GetDeleteArrowDistance());
+ * // do somethings ...
+ * }
+ *
+ * 现在要在 this.GetDeleteArrowDistance() 之前插入
+ * this._hitIn = ((float)this._hitAreaList.Count * num1 > (float)this._hitIndex + 0.5f);
+ * 这段代码, 可以采用 Prefix, GetDeleteArrowDistance() 只在两个地方调用过, 另一处就在上面的 if 分支中 (即非 AutoPlay 情况)
+ */
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(SlideRoot), "GetDeleteArrowDistance")]
+ private static void FixSlideAutoPlayArrow(
+ SlideRoot __instance, ref bool ____hitIn, int ____hitIndex, List ____hitAreaList,
+ float ___StarLaunchMsec, float ___StarArriveMsec, float ___lastWaitTime
+ )
+ {
+ if (GameManager.IsAutoPlay())
+ {
+ float prop = (NotesManager.GetCurrentMsec() - ___StarLaunchMsec) / (___StarArriveMsec - ___StarLaunchMsec - ___lastWaitTime);
+ ____hitIn = ____hitAreaList.Count * prop > ____hitIndex + 0.5f;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/AquaMai/RenderTweak/SlideJudgeTweak.cs b/AquaMai/RenderTweak/SlideJudgeTweak.cs
new file mode 100644
index 00000000..25377a11
--- /dev/null
+++ b/AquaMai/RenderTweak/SlideJudgeTweak.cs
@@ -0,0 +1,89 @@
+using System;
+using HarmonyLib;
+using Manager;
+using Monitor;
+using Process;
+using UnityEngine;
+
+namespace AquaMai.RenderTweak;
+
+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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AquaMai/RenderTweak/TrackStartProcessTweak.cs b/AquaMai/RenderTweak/TrackStartProcessTweak.cs
new file mode 100644
index 00000000..d547f595
--- /dev/null
+++ b/AquaMai/RenderTweak/TrackStartProcessTweak.cs
@@ -0,0 +1,83 @@
+using HarmonyLib;
+using Monitor;
+using Process;
+using UI;
+using UnityEngine;
+
+namespace AquaMai.RenderTweak;
+
+public class TrackStartProcessTweak
+{
+ // 总之这个 Patch 没啥用, 是我个人用 sinmai 录谱面确认时用得到, 顺手也写进来了
+ // 具体而言就是推迟了歌曲开始界面的动画便于后期剪辑
+ // 然后把“TRACK X”字样和 DX/标准谱面的显示框隐藏掉, 让他看起来不那么 sinmai, 更像是 majdata
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(TrackStartProcess), "OnUpdate")]
+ private static bool DelayAnimation(
+ TrackStartProcess.TrackStartSequence ____state,
+ ref float ____timeCounter,
+ ProcessDataContainer ___container
+ )
+ {
+ if (____state == TrackStartProcess.TrackStartSequence.Wait)
+ {
+ // 将开始动画(就是“噔噔, 噔 噔噔”)推迟
+ float temp = ____timeCounter + Time.deltaTime;
+ if (____timeCounter < 1.0f && temp >= 1.0f)
+ {
+ // 这是用来让转场动画继续播放的, 原本就是这个时候 notify 的同时开始播放开始动画
+ // 现在把开始动画往后延
+ ___container.processManager.NotificationFadeIn();
+ }
+ ____timeCounter = temp;
+ if (____timeCounter >= 3.0f)
+ {
+ return true;
+ // 原 method 的逻辑是这样
+ // case TrackStartProcess.TrackStartSequence.Wait:
+ // this._timeCounter += Time.deltaTime;
+ // if ((double) this._timeCounter >= 1.0)
+ // {
+ // this._timeCounter = 0.0f;
+ // this._state = TrackStartProcess.TrackStartSequence.Disp;
+ // /* 一些开始播放开始动画的代码 */
+ // this.container.processManager.NotificationFadeIn();
+ // break;
+ // }
+ // break;
+ // 所以只要在 prefix 里面等到 timeCounter 达到我们想要的值以后再执行原 method 就好
+ // 这里有个细节: NotificationFadeIn() 会被执行两遍, 这其实不好, 是个潜在 bug
+ // 不过由于此处把开始动画往后推了 2s, 转场动画已经结束把 Process 释放掉了, 所以第二遍会找不到 Process 就没效果
+ }
+ return false;
+ }
+ else if (____state == TrackStartProcess.TrackStartSequence.DispEnd)
+ {
+ // 将开始动画结束以后的转场动画推迟
+ ____timeCounter += Time.deltaTime; // timeCounter 会在先前由原本的 method 归零
+ if (____timeCounter >= 1.0f)
+ {
+ return true;
+ }
+ return false;
+ }
+ 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);
+ }
+}
\ No newline at end of file
diff --git a/AquaMai/UX/DemoMaster.cs b/AquaMai/UX/DemoMaster.cs
index 052d0fcc..e3dd5090 100644
--- a/AquaMai/UX/DemoMaster.cs
+++ b/AquaMai/UX/DemoMaster.cs
@@ -16,6 +16,7 @@ public static void AdvDemoProcessPostStart()
{
var userOption = Singleton.Instance.GetGameScore(i).UserOption;
userOption.NoteSpeed = OptionNotespeedID.Speed6_5;
+ userOption.TouchSpeed = OptionTouchspeedID.Speed7_0;
}
}