Skip to content
Merged
Changes from 1 commit
Commits
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
207 changes: 207 additions & 0 deletions AquaMai.Mods/GameSystem/LegacyBoardMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
using AquaMai.Config.Attributes;
using Comio.BD15070_4;
using HarmonyLib;
using System.Reflection.Emit;
using Mecha;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;

namespace AquaMai.Mods.GameSystem;

[ConfigSection(
name: "舊版燈板映射",
en: "Remapping Billboard LED to 837-15070-02 Woofer LED, Roof LED, Center LED",
zh: "重新映射新框頂板 LED 至 837-15070-02 (舊版燈板) 的重低音喇叭 LED、頂板 LED 以及中央 LED")]
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
name: "舊版燈板映射",
en: "Remapping Billboard LED to 837-15070-02 Woofer LED, Roof LED, Center LED",
zh: "重新映射新框頂板 LED 至 837-15070-02 (舊版燈板) 的重低音喇叭 LED、頂板 LED 以及中央 LED")]
name: "旧框灯板映射",
en: "Remapping Billboard LED to 837-15070-02 Woofer LED, Roof LED, Center LED",
zh: "重新映射新框顶板 LED 至 837-15070-02 (旧版灯板) 的重低音喇叭 LED、顶板 LED 以及中央 LED")]

public class LegacyBoardMapping
{
[HarmonyPatch(typeof(Bd15070_4IF), "_construct")]
public class Bd15070_4IF_Construct_Patch
{
[HarmonyTranspiler]
static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
var codes = new List<CodeInstruction>(instructions);
int patchedNum = 0;
for (int i = 0; i < codes.Count && patchedNum < 2; i++)
{
if (codes[i].opcode == OpCodes.Ldc_I4_8 &&
codes[i].operand == null)
{
codes[i].opcode = OpCodes.Ldc_I4_S;
codes[i].operand = (sbyte)10;
patchedNum++;
}
}
if (patchedNum == 2)
{
MelonLoader.MelonLogger.Msg("[LegacyBoardMapping] Extended Bd15070_4IF._switchParam size and its initialize for loop from 8 to 10!");
}
else
{
MelonLoader.MelonLogger.Warning($"[LegacyBoardMapping] Bd15070_4IF._switchParam patching failed (patched {patchedNum}/2)");
}
return codes;
}
}

[HarmonyPatch]
public class JvsOutputPwmPatch
{
[HarmonyTargetMethod]
static MethodBase TargetMethod()
{
var type = typeof(IO.Jvs).GetNestedType("JvsOutputPwm", BindingFlags.NonPublic | BindingFlags.Instance);
if (type == null)
{
MelonLoader.MelonLogger.Error("[LegacyBoardMapping] JvsOutputPwm type not found");
return null;
}
return type.GetMethod("Set", new[] { typeof(byte), typeof(Color32), typeof(bool) });
}

[HarmonyPrefix]
public static bool Prefix(object __instance, byte index, Color32 color, bool update)
{
RedirectToButtonLedMechanism(index, color);

return false;
}
}

private static void RedirectToButtonLedMechanism(byte playerIndex, Color32 color)
{
// Check if MechaManager is initialized
if (!IO.MechaManager.IsInitialized)
{
MelonLoader.MelonLogger.Warning("[LegacyLedBoardMapping] MechaManager not initialized, cannot set woofer LED");

Choose a reason for hiding this comment

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

medium

此方法中的日誌訊息使用了 [LegacyLedBoardMapping] 作為前綴,但在這個類別的其他地方(例如 Bd15070_4IF_Construct_Patch 中)使用的是 [LegacyBoardMapping]

為了保持一致性並方便日誌過濾和問題排查,建議將所有 [LegacyLedBoardMapping] 都修改為 [LegacyBoardMapping]

            MelonLoader.MelonLogger.Warning("[LegacyBoardMapping] MechaManager not initialized, cannot set woofer LED");

return;
}

// Get the LED interface for the player
var ledIf = IO.MechaManager.LedIf;
if (ledIf == null || playerIndex >= ledIf.Length || ledIf[playerIndex] == null)
{
MelonLoader.MelonLogger.Warning($"[LegacyLedBoardMapping] LED interface not available for player {playerIndex}");
return;
}

// Use reflection to access the IoCtrl and call SetLedGs8BitCommand[8] directly
// Then set _gsUpdate flag so PreExecute() sends the update command (just like buttons do)
try
{
var ledIfType = typeof(Bd15070_4IF);
var controlField = ledIfType.GetField("_control", BindingFlags.NonPublic | BindingFlags.Instance);

if (controlField == null)
{
MelonLoader.MelonLogger.Error("[LegacyLedBoardMapping] _control field not found in Bd15070_4IF");
return;
}

var control = controlField.GetValue(ledIf[playerIndex]);
if (control == null)
{
MelonLoader.MelonLogger.Error("[LegacyLedBoardMapping] Control object is null");
return;
}

// Get _board field from Bd15070_4Control
var controlType = control.GetType();
var boardField = controlType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance);
if (boardField == null)
{
MelonLoader.MelonLogger.Error("[LegacyLedBoardMapping] _board field not found in Bd15070_4Control");
return;
}

var board = boardField.GetValue(control);
if (board == null)
{
MelonLoader.MelonLogger.Error("[LegacyLedBoardMapping] Board object is null");
return;
}

// Get _ctrl field from Board15070_4
var boardType = board.GetType();
var ctrlField = boardType.GetField("_ctrl", BindingFlags.NonPublic | BindingFlags.Instance);
if (ctrlField == null)
{
MelonLoader.MelonLogger.Error("[LegacyLedBoardMapping] _ctrl field not found in Board15070_4");
return;
}

var boardCtrl = ctrlField.GetValue(board);
if (boardCtrl == null)
{
MelonLoader.MelonLogger.Error("[LegacyLedBoardMapping] BoardCtrl object is null");
return;
}

// Get _ioCtrl field from BoardCtrl15070_4
var boardCtrlType = boardCtrl.GetType();
var ioCtrlField = boardCtrlType.GetField("_ioCtrl", BindingFlags.NonPublic | BindingFlags.Instance);
if (ioCtrlField == null)
{
MelonLoader.MelonLogger.Error("[LegacyLedBoardMapping] _ioCtrl field not found in BoardCtrl15070_4");
return;
}

var ioCtrl = ioCtrlField.GetValue(boardCtrl);
if (ioCtrl == null)
{
MelonLoader.MelonLogger.Error("[LegacyLedBoardMapping] IoCtrl object is null");
return;
}

// Get SetLedGs8BitCommand array from IoCtrl (public field)
var ioCtrlType = typeof(IoCtrl);
var setLedGs8BitCommandField = ioCtrlType.GetField("SetLedGs8BitCommand", BindingFlags.Public | BindingFlags.Instance);
if (setLedGs8BitCommandField == null)
{
MelonLoader.MelonLogger.Error("[LegacyLedBoardMapping] SetLedGs8BitCommand field not found in IoCtrl");
return;
}

var setLedGs8BitCommandArray = setLedGs8BitCommandField.GetValue(ioCtrl) as SetLedGs8BitCommand[];
if (setLedGs8BitCommandArray == null || setLedGs8BitCommandArray.Length <= 8)
{
MelonLoader.MelonLogger.Error("[LegacyLedBoardMapping] SetLedGs8BitCommand array is null or too small");
return;
}

// Get SendForceCommand method from BoardCtrl15070_4
var sendForceCommandMethod = boardCtrlType.GetMethod("SendForceCommand", BindingFlags.Public | BindingFlags.Instance);
if (sendForceCommandMethod == null)
{
MelonLoader.MelonLogger.Error("[LegacyLedBoardMapping] SendForceCommand method not found in BoardCtrl15070_4");
return;
}

// Use SetLedGs8BitCommand[8] and SetLedGs8BitCommand[9] directly (same as buttons 0-7, but for ledPos = 8 and 9)
// This bypasses the FET command path in IoCtrl.SetLedData(), as they are not via FET, they are like buttons
// ledPos = 8 == woofer & roof
// ledPos = 9 == center
setLedGs8BitCommandArray[8].setColor(8, color);
setLedGs8BitCommandArray[9].setColor(9, color);
sendForceCommandMethod.Invoke(boardCtrl, new object[] { setLedGs8BitCommandArray[8] });

Choose a reason for hiding this comment

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

high

這裡您為 ledPos = 8ledPos = 9 都設定了顏色,但只對索引為 8 的指令呼叫了 SendForceCommand。根據註解,ledPos = 8 對應重低音和頂板,而 ledPos = 9 對應中央 LED。

如果 SendForceCommand 是用來觸發單個指令的,那麼您可能遺漏了對 setLedGs8BitCommandArray[9] 的呼叫,這會導致中央 LED 的顏色無法被即時更新。

建議您確認 SendForceCommand 的行為。如果需要單獨發送,請為索引 9 也增加一次呼叫。

            sendForceCommandMethod.Invoke(boardCtrl, new object[] { setLedGs8BitCommandArray[8] });
            sendForceCommandMethod.Invoke(boardCtrl, new object[] { setLedGs8BitCommandArray[9] });


// Set the _gsUpdate flag on Bd15070_4IF so PreExecute() sends the update command
// This matches exactly how buttons work - they set _gsUpdate = true
var gsUpdateField = ledIfType.GetField("_gsUpdate", BindingFlags.NonPublic | BindingFlags.Instance);
if (gsUpdateField != null)
{
gsUpdateField.SetValue(ledIf[playerIndex], true);
}
else
{
MelonLoader.MelonLogger.Warning("[LegacyLedBoardMapping] _gsUpdate field not found, LED may not update");
}
}
catch (System.Exception ex)
{
MelonLoader.MelonLogger.Error($"[LegacyLedBoardMapping] Failed to set woofer LED: {ex.Message}\n{ex.StackTrace}");
}

Choose a reason for hiding this comment

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

high

RedirectToButtonLedMechanism 方法中,您使用了大量的反射呼叫來存取私有成員。GetFieldGetMethod 是效能開銷較大的操作,尤其是在可能每幀都會被呼叫的 LED 更新邏輯中。這可能會對遊戲效能產生不必要的影響。

建議將反射獲取的 FieldInfoMethodInfo 物件快取在靜態唯讀欄位中。這樣,反射查詢的開銷只會在類別載入時發生一次,後續呼叫將會快得多。

例如,您可以這樣修改:

public class LegacyBoardMapping
{
    // ... 其他程式碼 ...

    private static readonly System.Reflection.FieldInfo _controlField = 
        typeof(Comio.BD15070_4.Bd15070_4IF).GetField("_control", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
    // ... 為其他欄位和方法建立類似的靜態欄位 ...

    private static void RedirectToButtonLedMechanism(byte playerIndex, Color32 color)
    {
        // ...
        try
        {
            if (_controlField == null)
            {
                MelonLoader.MelonLogger.Error("[LegacyBoardMapping] _control field not found in Bd15070_4IF");
                return;
            }
            var control = _controlField.GetValue(IO.MechaManager.LedIf[playerIndex]);
            // ... 使用其他快取的欄位 ...
        }
        catch (System.Exception ex)
        {
            MelonLoader.MelonLogger.Error($"[LegacyBoardMapping] Failed to set woofer LED: {ex.Message}\n{ex.StackTrace}");
        }
    }
}

這樣做可以顯著提高此方法的執行效率,並使程式碼更清晰。

}
}