Skip to content
Open
Changes from 12 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
606e372
加入力大砖飞模组
Haisairova-Official Dec 21, 2025
e514c7b
[+] 修改力大砖飞警告说明
Haisairova-Official Dec 21, 2025
5acfb37
Update AquaMai.Mods/Fancy/RsOverride.cs
Haisairova-Official Dec 21, 2025
9af16af
Update AquaMai.Mods/Fancy/RsOverride.cs
Haisairova-Official Dec 21, 2025
a0f4e65
Update AquaMai.Mods/Fancy/RsOverride.cs
Haisairova-Official Dec 21, 2025
7b69c29
Update AquaMai.Mods/Fancy/RsOverride.cs
Haisairova-Official Dec 21, 2025
692d10b
Apply suggestion from @clansty
clansty Dec 21, 2025
17c953e
[F] 修改目录避免模组冲突
Haisairova-Official Dec 21, 2025
f2917f5
Merge branch 'MuNET-OSS:main' into main
Haisairova-Official Dec 23, 2025
ec12ce6
Merge branch 'MuNET-OSS:main' into main
Haisairova-Official Jan 5, 2026
80e835c
[+] 对转场动画功能进行了史诗级加钱。
Haisairova-Official Jan 5, 2026
ebda3da
Update KLD transition config entry description
Haisairova-Official Jan 5, 2026
0ca1e8f
[F] 修复部分曲目无fade out动画的bug
Haisairova-Official Jan 5, 2026
b32ca44
Update AquaMai.Mods/Fancy/SetFade.cs
Haisairova-Official Jan 6, 2026
58b9fe8
Update AquaMai.Mods/Fancy/SetFade.cs
Haisairova-Official Jan 6, 2026
1985afb
[F] 修改JSON读取逻辑,使用Melonloader.TinyJSON。添加默认不修改转场的选项5(其实所有无效数值都可以)
Haisairova-Official Jan 6, 2026
055469f
[+,F] 追加SetTrackStart模组
Haisairova-Official Jan 8, 2026
1f43eac
[+,F] 追加SetTrackStart模组
Haisairova-Official Jan 8, 2026
a76e507
Merge branch 'main' into main
Haisairova-Official Jan 9, 2026
f47d540
Merge branch 'MuNET-OSS:main' into main
Haisairova-Official Jan 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 184 additions & 72 deletions AquaMai.Mods/Fancy/SetFade.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,118 +4,230 @@
using Process;
using UnityEngine;
using UnityEngine.UI;
using System.Reflection;
using System;
using System.IO;
using Manager;
using MelonLoader;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Monitor;

namespace AquaMai.Mods.Fancy;

[ConfigSection(
name: "转场动画",
[ConfigSection(name: "转场动画PLUS",
en: "Set Fade Animation",
zh: "修改转场动画为其他变种"
)]

zh: "修改转场动画为其他变种")]
public class SetFade
{
[ConfigEntry(
name: "转场类型",
en: "Type: Non-Plus 0, Plus 1. (If SDEZ 1.60 can choose Festa 2)",
zh: "类型: Non-Plus 0, Plus 1. (SDEZ 1.60 限定可选 Festa 2)")]
[ConfigEntry(name: "转场类型", zh: "0:Normal, 1:Plus, 2:Festa(仅限1.60+)")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我觉得既然两个功能要合在一起的话,这里应该提供一个选项叫 不更改

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

其实我在想写的时候预留过一个5)

public static readonly int FadeType = 0;

[ConfigEntry(name: "[仅限1.55+]启用特殊KLD转场,需要下载额外JSON文件", zh: "仅在配置过的歌曲启用KLD转场。1.50及以下版本无效。1.50我不想适配了如果有人想适配可以dd我)@力大砖飞")]
public static readonly bool isKLDEnabled = true;

private static readonly string JSONDir = "LocalAssets";
private static readonly string JSONFileName = "CommonFadeList.json";

private static bool isInitialized = false;
private static bool isResourcePatchEnabled = false;
private static bool _isInitialized = false;
private static Sprite[] subBGs = new Sprite[3];
private static List<CommonFadeEntry> cachedEntries = new List<CommonFadeEntry>();

// 计数锁定逻辑变量
private static int _kldRemainingCharges = 0;
private static CommonFadeEntry _activeKldConfig = null;

public class CommonFadeEntry { public int ID; public int isBlack; public int Type; public int FadeType; }

[HarmonyPrepare]
public static bool SetFade_Prepare()
public static bool Prepare()
{
SetFade_Initialize();
if (!isInitialized)
MelonLogger.Msg("[SetFade] Initialization failed, this patch will not be applied.");
return isInitialized;
if (_isInitialized) return true;
subBGs[0] = Resources.Load<Sprite>("Process/ChangeScreen/Sprites/Sub_01");
subBGs[1] = Resources.Load<Sprite>("Process/ChangeScreen/Sprites/Sub_02");
subBGs[2] = (GameInfo.GameVersion >= 26000) ? Resources.Load<Sprite>("Process/ChangeScreen/Sprites/Sub_03") : subBGs[0];
Comment on lines +53 to +55

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

新的 Prepare 方法直接加载了 SubBG 精灵,但没有像旧的 SetFade_Initialize 方法那样检查加载结果是否为 null。如果 Resources.Load 由于某些原因(例如资源路径错误或资源损坏)失败并返回 null,那么在后续调用 ReplaceSubBG 时可能会导致 NullReferenceException

建议恢复对加载资源的空值检查,并在加载失败时记录错误日志,以提高模组的健壮性。

        subBGs[0] = Resources.Load<Sprite>("Process/ChangeScreen/Sprites/Sub_01");
        subBGs[1] = Resources.Load<Sprite>("Process/ChangeScreen/Sprites/Sub_02");
        subBGs[2] = (GameInfo.GameVersion >= 26000) ? Resources.Load<Sprite>("Process/ChangeScreen/Sprites/Sub_03") : subBGs[0];
        if (subBGs[0] == null || subBGs[1] == null || (GameInfo.GameVersion >= 26000 && subBGs[2] == null))
        {
            MelonLogger.Error("[SetFade] Failed to load one or more SubBG sprites. The mod may not function correctly.");
        }

LoadJsonManual();
_isInitialized = true;
return true;
}

private static void SetFade_Initialize()
// --- 1. 实时监听选曲:充能点 ---
[HarmonyPostfix]
[HarmonyPatch(typeof(MusicSelectMonitor), "UpdateRivalScore")]
[HarmonyPatch(typeof(MusicSelectMonitor), "SetRivalScore")]
public static void OnMusicSelectionChanged(MusicSelectProcess ____musicSelect)
{
bool areSubBGsValid;
bool isFadeTypeValid;
if (!isKLDEnabled || ____musicSelect == null) return;

if (GameInfo.GameVersion != 26000)
{
subBGs[0] = Resources.Load<Sprite>("Process/ChangeScreen/Sprites/Sub_01");
subBGs[1] = Resources.Load<Sprite>("Process/ChangeScreen/Sprites/Sub_02");
areSubBGsValid = subBGs[0] != null && subBGs[1] != null;
isFadeTypeValid = FadeType == 0 || FadeType == 1;
}
else
{
subBGs[0] = Resources.Load<Sprite>("Process/ChangeScreen/Sprites/Sub_01");
subBGs[1] = Resources.Load<Sprite>("Process/ChangeScreen/Sprites/Sub_02");
subBGs[2] = Resources.Load<Sprite>("Process/ChangeScreen/Sprites/Sub_03");
areSubBGsValid = subBGs[0] != null && subBGs[1] != null && subBGs[2] != null;
isFadeTypeValid = FadeType == 0 || FadeType == 1 || FadeType == 2;
}

if (!areSubBGsValid)
MelonLogger.Msg($"[SwitchFade] Couldn't find SubBG sprites.");

if (!isFadeTypeValid)
MelonLogger.Msg($"[SwitchFade] Invalid FadeType.");

isInitialized = areSubBGsValid && isFadeTypeValid;
try {
var musicData = ____musicSelect.GetMusic(0)?.MusicData;
if (musicData != null)
{
var matched = cachedEntries.Find(e => e.ID == musicData.name.id);
if (matched != null)
{
if (_activeKldConfig != matched)
{
_activeKldConfig = matched;
_kldRemainingCharges = 3; // 锁定 3 次机会

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

数字 3 在这里是一个“魔术数字”,它的含义没有在代码中明确解释。为了提高代码的可读性和可维护性,建议在类的顶部定义一个常量(例如 private const int KldInitialCharges = 3;),并在此处使用该常量。这样如果以后需要修改充能次数,只需要修改一处即可。

MelonLogger.Msg($"[SetFade] 目标锁定:ID {matched.ID},KLD 已充能 (3次)");
}
}
else
{
_activeKldConfig = null;
_kldRemainingCharges = 0;
}
}
} catch { }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

捕获了异常却不做任何处理(空的 catch 块)是一种危险的做法。如果 OnMusicSelectionChanged 方法内部的逻辑出现任何预料之外的错误,异常会被悄无声息地忽略掉,这会给调试带来巨大困难。

建议至少将异常信息记录下来,以便在出现问题时能够追踪。

} catch (Exception e) { MelonLogger.Error($"[SetFade] OnMusicSelectionChanged error: {e}"); }

}


// 在显示转场前启用patch
// --- 2. 资源拦截触发 ---
[HarmonyPrefix]
[HarmonyPatch(typeof(FadeProcess), "OnStart")]
public static void FadeProcessOnStartPreFix() { isResourcePatchEnabled = true; }
[HarmonyPrefix]
[HarmonyPatch(typeof(AdvertiseProcess), "InitFade")]
public static void AdvertiseProcessInitFadePreFix() { isResourcePatchEnabled = true; }
[HarmonyPrefix]
[HarmonyPatch(typeof(NextTrackProcess), "OnStart")]
public static void NextTrackProcessOnStartPreFix() { isResourcePatchEnabled = true; }
public static void StartFadePrefix()
{
// 只有在有充能次数且配置存在时才开启拦截
isResourcePatchEnabled = (_kldRemainingCharges > 0 && _activeKldConfig != null);
}

[HarmonyPrefix]
[HarmonyPatch(typeof(Resources), "Load", new[] { typeof(string), typeof(global::System.Type) })]
public static bool ResourcesLoadPrefix(ref string path, global::System.Type systemTypeInstance, ref UnityEngine.Object __result)
{
// 如果 KLD 拦截未激活,则执行普通重定向(力大砖飞)
if (!isResourcePatchEnabled)
{
if (FadeType >= 0 && FadeType <= 2)
{
string targetPath = $"Process/ChangeScreen/Prefabs/ChangeScreen_0{FadeType + 1}";
if (path.StartsWith("Process/ChangeScreen/Prefabs/ChangeScreen_0") && path != targetPath)
{
if (GameInfo.GameVersion < 26000 && FadeType == 2) return true;
__result = Resources.Load(targetPath, systemTypeInstance);
return false;
}
}
return true;
}

// KLD 资源拦截
if (path.StartsWith("Process/ChangeScreen/Prefabs/ChangeScreen_0"))
{
__result = Resources.Load("Process/Kaleidxscope/Prefab/UI_KLD_ChangeScreen", systemTypeInstance);
return false;
}
if (path.StartsWith("Process/ChangeScreen/Prefabs/Sub_ChangeScreen"))
{
__result = Resources.Load("Process/Kaleidxscope/Prefab/UI_KLD_Sub_ChangeScreen", systemTypeInstance);
return false;
}
return true;
}

// 在显示转场后禁用patch
// --- 3. 后置处理:消耗次数与动画播放 ---
[HarmonyPostfix]
[HarmonyPatch(typeof(FadeProcess), "OnStart")]
public static void FadeProcessOnStartPostFix(GameObject[] ___fadeObject) { ReplaceSubBG(___fadeObject); }
[HarmonyPostfix]
[HarmonyPatch(typeof(AdvertiseProcess), "InitFade")]
public static void AdvertiseProcessInitFadePostFix(GameObject[] ___fadeObject) { ReplaceSubBG(___fadeObject); }
[HarmonyPostfix]
[HarmonyPatch(typeof(NextTrackProcess), "OnStart")]
public static void NextTrackProcessOnStartPostFix(GameObject[] ___fadeObject) { ReplaceSubBG(___fadeObject); }


private static void ReplaceSubBG(GameObject[] fadeObjects)
public static void GlobalPostfix(GameObject[] ___fadeObject)
{
isResourcePatchEnabled = false;
foreach (var monitor in fadeObjects)
if (isResourcePatchEnabled && _activeKldConfig != null)
{
var subBG = monitor.transform.Find("Canvas/Sub/Sub_ChangeScreen(Clone)/Sub_BG").GetComponent<Image>();
subBG.sprite = subBGs[FadeType];
_kldRemainingCharges--; // 消耗一次
MelonLogger.Msg($"[SetFade] 触发 KLD 成功,剩余次数: {_kldRemainingCharges}");

if (___fadeObject != null)
{
foreach (var monitor in ___fadeObject)
DriveKLDAnimation(monitor, _activeKldConfig);
}
}
else if (___fadeObject != null)
{
// 普通重定向模式下的 SubBG 替换
foreach (var monitor in ___fadeObject)
ReplaceSubBG(monitor);
}

isResourcePatchEnabled = false; // 关闭当次拦截锁

// 次数耗尽清理配置
if (_kldRemainingCharges <= 0) _activeKldConfig = null;
}

[HarmonyPrefix]
[HarmonyPatch(typeof(Resources), "Load", new[] { typeof(string), typeof(Type) })]
public static bool ResourcesLoadPrefix(ref string path, Type systemTypeInstance, ref UnityEngine.Object __result)
private static void ReplaceSubBG(GameObject monitor)
{
if (FadeType < 0 || FadeType >= subBGs.Length) return;
try {
var subBG = monitor.transform.Find("Canvas/Sub/Sub_ChangeScreen(Clone)/Sub_BG")?.GetComponent<Image>();
if (subBG != null) subBG.sprite = subBGs[FadeType];
} catch { }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

这里的空 catch 块同样会隐藏潜在的错误。例如,如果 monitor.transform.Find(...) 返回 null,后续的 GetComponent<Image>() 就会抛出 NullReferenceException。虽然代码中使用了 ?. 操作符来防御,但如果将来逻辑变动引入了其他错误,这个空的 catch 块会使问题排查变得困难。

建议添加日志记录。

} catch (Exception e) { MelonLogger.Warning($"[SetFade] Failed to replace SubBG: {e.Message}"); }

}

private static void DriveKLDAnimation(GameObject monitor, CommonFadeEntry cfg)
{
if (isResourcePatchEnabled)
try
{
if (path.StartsWith("Process/ChangeScreen/Prefabs/ChangeScreen_0") &&
path != $"Process/ChangeScreen/Prefabs/ChangeScreen_0{FadeType + 1}") // 避免无限递归
var main = monitor.transform.Find("Canvas/Main/UI_KLD_ChangeScreen(Clone)");
var sub = monitor.transform.Find("Canvas/Sub/UI_KLD_Sub_ChangeScreen(Clone)");

// 根据你的要求修正动画映射
string animName = cfg.FadeType switch {
1 => "Out",
2 => "Out_02",
3 => "Out_03",
_ => "Out"
};

if (main != null)
{
__result = Resources.Load($"Process/ChangeScreen/Prefabs/ChangeScreen_0{FadeType + 1}", systemTypeInstance);
return false;
var ctrl = main.GetComponent<KaleidxScopeFadeController>();
if (ctrl != null) {
ctrl.SetBackGroundType(cfg.isBlack != 0 ? KaleidxScopeFadeController.BackGroundType.Black : KaleidxScopeFadeController.BackGroundType.Normal);
ctrl.SetSpriteType((KaleidxScopeFadeController.SpriteType)cfg.Type);
if (Enum.TryParse<KaleidxScopeFadeController.AnimState>(animName, out var state))
ctrl.PlayAnimation(state);
}
}
}
return true;
if (sub != null)
{
var sCtrl = sub.GetComponent<KaleidxScopeSubFadeController>();
if (sCtrl != null) {
sCtrl.SetBackGroundType(cfg.isBlack != 0 ? KaleidxScopeSubFadeController.BackGroundType.Black : KaleidxScopeSubFadeController.BackGroundType.Normal);
sCtrl.SetSpriteType((KaleidxScopeSubFadeController.SpriteType)cfg.Type);
sCtrl.PlayAnimation(KaleidxScopeSubFadeController.AnimState.In);
}
}
} catch { }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

同样地,空的 catch 块会隐藏 DriveKLDAnimation 方法中可能发生的所有错误,例如组件查找失败、类型转换失败等。这会使调试非常困难。

建议记录异常信息。

} catch (Exception e) { MelonLogger.Error($"[SetFade] Failed to drive KLD animation: {e}"); }

}

private static void LoadJsonManual()
{
try {
string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, JSONDir, JSONFileName);
if (!File.Exists(path)) return;
string content = File.ReadAllText(path);
cachedEntries.Clear();
var matches = Regex.Matches(content, @"\{[^{}]+\}");
foreach (Match m in matches) {
string raw = m.Value;
var e = new CommonFadeEntry {
ID = ExtractInt(raw, "ID"),
isBlack = ExtractInt(raw, "isBlack"),
Type = ExtractInt(raw, "Type"),
FadeType = ExtractInt(raw, "FadeType")
};
if (e.ID > 0) cachedEntries.Add(e);
}
MelonLogger.Msg($"[SetFade] 共载入 {cachedEntries.Count} 条 KLD 特殊配置。");
} catch (Exception e) { MelonLogger.Error($"[SetFade] JSON加载出错: {e.Message}"); }
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

使用正则表达式来手动解析JSON文件非常脆弱且容易出错。它强依赖于JSON字符串的格式(如空格),并且无法处理嵌套对象或数组等复杂情况。当 CommonFadeList.json 文件格式稍微改变,解析就可能失败。

强烈建议使用一个标准的JSON库(例如 Newtonsoft.Json)来反序列化JSON。这样代码会更简洁、稳健且易于维护。这也使得 ExtractInt 方法变得多余,可以将其删除。

你需要将 Newtonsoft.Json.dll 添加到你的项目引用中,并在文件顶部添加 using Newtonsoft.Json;

    private static void LoadJsonManual()
    {
        try {
            string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, JSONDir, JSONFileName);
            if (!File.Exists(path)) return;
            
            string content = File.ReadAllText(path);
            
            var entries = Newtonsoft.Json.JsonConvert.DeserializeObject<List<CommonFadeEntry>>(content);
            cachedEntries = entries?.FindAll(e => e.ID > 0) ?? new List<CommonFadeEntry>();

            MelonLogger.Msg($"[SetFade] 共载入 {cachedEntries.Count} 条 KLD 特殊配置。");
        } catch (Exception e) { 
            MelonLogger.Error($"[SetFade] JSON加载或解析出错: {e}"); 
        }
    }


private static int ExtractInt(string text, string key) {
var m = Regex.Match(text, $"\"{key}\"\\s*:\\s*\"?(\\d+)\"?");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

真力大砖飞啊
有一个 TinyJson,要合并到主线里肯定是不可以手写 JSON parser 的

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

想办法解析到 dictionary 吧

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

真力大砖飞啊 有一个 TinyJson,要合并到主线里肯定是不可以手写 JSON parser 的

那我这个又想要点自定义的配置)) 写死也可以就是有点太长了吧

想办法解析到 dictionary 吧

没搞过 我理解一下

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

好吧,那我试试给你改一下
其实可以有一个默认的 json,没有的时候自动启用,内置在里面,内容就是门对应的歌

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

好吧,那我试试给你改一下 其实可以有一个默认的 json,没有的时候自动启用,内置在里面,内容就是门对应的歌

我其实也有这个想法)

return (m.Success && int.TryParse(m.Groups[1].Value, out int res)) ? res : 0;
}
}