Skip to content

Conversation

@kevin0216
Copy link
Contributor

@kevin0216 kevin0216 commented Dec 22, 2025

簡介

此次新增了對舊框燈版 837-15070-02 中,重低音 LED / 頂板 LED 跟中央 LED 的支持
會將 Sinmai 中對 Billboard (頂板) LED 的指令映射到重低音 LED / 頂板 LED 跟中央 LED

細節

主要是 Hook 了 JvsOutputPwm 來攔截對 Billboard 的設定要求,並將請求改為與按鍵燈同格式的發送方式 (因按鍵燈可以正常運作,而在 micetools/mailed 中提到舊燈板的按鍵燈與重低音/頂板及中央為同一發送格式)
並在最後設定 _gsUpdate 來確保燈板指令更新

另外感謝 $EGA 寫的酷東西
在 Bd15070_4IF._construct 中對按鍵 LED 數量有硬限制(陣列只開 8 格,並 for 迴圈只執行 8 次),有對陣列初始化跟 for 迴圈進行修改,多出 ledPos = 8 及 9 的空間

此模組已在實際機台上測試,確認可用

Special Thanks

2025/12/23
by @kevin0216 & @ptchen314

@gemini-code-assist
Copy link

Summary of Changes

Hello @kevin0216, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取請求新增了一個模組,旨在為舊版燈板 (837-15070-02) 提供兼容性。它透過重新映射新框的頂板 LED 訊號,使其能夠控制舊版硬體上的重低音喇叭、頂板和中央 LED。這項功能是透過在運行時修補現有程式碼並利用反射機制來實現的,以確保舊版設備能夠正確響應燈光指令。

Highlights

  • 新增舊版燈板映射模組: 引入了一個新的 LegacyBoardMapping.cs 檔案,其中包含一個用於舊版燈板映射的新模組。
  • 擴展 Bd15070_4IF 參數: 使用 Harmony Transpiler 將 Bd15070_4IF._switchParam 的大小及其初始化循環從 8 擴展到 10。
  • 重定向 PWM 輸出: 透過 Harmony Prefix 補丁攔截 IO.Jvs.JvsOutputPwm.Set 方法,將其輸出重定向到自定義的 LED 機制。
  • 反射機制控制 LED: 實作 RedirectToButtonLedMechanism 方法,利用反射技術直接存取並控制 Bd15070_4IF 內部元件的重低音喇叭、頂板和中央 LED。
  • 繞過標準 LED 資料路徑: 直接操作 SetLedGs8BitCommand[8]SetLedGs8BitCommand[9] 來設定 LED 顏色,繞過了標準的 IoCtrl.SetLedData() 路徑,以模擬按鈕 LED 的行為。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@kevin0216 kevin0216 changed the title [+] 新增舊框燈版 837-15070-02 重低音 [+] 新增舊框燈版 837-15070-02 重低音 LED / 頂板 LED / 中央 LED 的支持 Dec 22, 2025
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

這份 PR 新增了對舊版燈板 837-15070-02 的支援,透過 Harmony patch 將新框的燈光訊號重新導向到舊版燈板的重低音、頂板和中央 LED。整體實現方式是合理的,但有幾個地方可以改進以提高程式碼的效能、可讀性和正確性。主要建議包括:透過快取反射所需的成員資訊來優化效能、修正一個潛在的燈光指令遺漏問題,以及統一日誌訊息的標籤。

Comment on lines 91 to 204
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] });

// 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}");
        }
    }
}

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

// 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] });

// 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");

Comment on lines 13 to 15
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")]

@clansty
Copy link
Member

clansty commented Dec 22, 2025

有一个 SkipBoardNoCheck(旧框灯板支持)功能,是否应该考虑合并一下呢

@kevin0216
Copy link
Contributor Author

有一个 SkipBoardNoCheck(旧框灯板支持)功能,是否应该考虑合并一下呢

那麼 SkipBoardNoCheck 會不會需要修改模組名稱呢,因為他現在功能已經不只 Skip BoardNoCheck
但是不知道會不會有配置文件的原功能被關閉的問題(名字不同 config 會重寫)

@clansty clansty changed the base branch from main to disableio4 December 24, 2025 04:37
@clansty clansty changed the base branch from disableio4 to config-next-2.4 December 24, 2025 04:39
@clansty clansty merged commit ea9f672 into MuNET-OSS:config-next-2.4 Dec 24, 2025
1 check passed
clansty added a commit that referenced this pull request Dec 26, 2025
* more disableio4

* [+] 新增舊框燈版 837-15070-02 重低音 LED / 頂板 LED / 中央 LED 的支持 (#100)

* [+] Adding support for 837-15070-02 Woofer LED, Roof LED and Center LED

Co-authored-by: PT_Chen <[email protected]>

* [F] Fixing not consistant module name

* [F] Fixed missing command

* [F] Optimized with reducing reflection fetching

* refactor: Merging into SkipBoardNoCheck

---------

Co-authored-by: PT_Chen <[email protected]>

* [O] rename SkipBoardNoCheck to OldCabLightBoardSupport

* [+] MaimollerIO 按键灯光触摸单独设置

* [O] AdxHidInput.DisableButtons

* [O] Add MaiChartManager EN

* Add unstable rate display (#101)

* Add unstable rate display

* Reuse material

* Fix windows line endings

* move UnstableRate

* [O] UnstableRate 2P support

* [O] 使用对象池优化性能

* [O] Change color

* [+] build script configuration

* [+] 增加同时开启的 DisplayTouchInGame

* [O] 力大砖飞 -> ResourcesOverride

* [O] configSort

* [O] convert check to non-AI script

* [O] configSort

* [F] bugs

* [F] minor

* revert

---------

Co-authored-by: Kevin S.H. Chang <[email protected]>
Co-authored-by: PT_Chen <[email protected]>
Co-authored-by: zealain <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants