Skip to content

[+] BepInEx support #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open

[+] BepInEx support #44

wants to merge 16 commits into from

Conversation

Menci
Copy link
Contributor

@Menci Menci commented Jun 29, 2025

好的,这是翻译成中文的 pull request 总结:

Sourcery 总结

添加 BepInEx 支持并将项目重构为通用的引导和环境层,该层抽象了 MelonLoader 和 BepInEx 的日志记录和初始化,供应商 TinyJSON 解析器,调整构建脚本和命名空间,并相应地更新 CI 工作流程和文档。

新功能:

  • 添加 AquaMai.BepInEx 插件项目以支持 BepInEx 加载器
  • 引入 AquaMai.Common 引导和环境抽象,用于统一初始化和日志记录
  • 将 MelonLoader.TinyJSON 库供应商到存储库中,用于内部 JSON 解析

增强功能:

  • 在所有模块中用 AquaMai.Core.Environment.MelonLogger 包装器替换直接 MelonLoader 调用
  • 添加 AquaMai.MelonLoader 插件项目,用于使用新的通用引导程序的 MelonLoader 兼容性
  • 重构命名空间并重组项目 (AquaMai -> AquaMai.Common) 以分离核心、mod 和加载器实现
  • 在窗口和防延迟模块中添加操作系统平台检查,以防止在非 Windows 平台上执行不支持的操作

构建:

  • 更新 Cake 构建脚本以使用新的解决方案文件名 (.slnx) 并调整解决方案命名空间
  • 重命名 csproj 文件并调整程序集名称以反映 Common 和特定于加载器的项目

CI:

  • 修改 GitHub Actions 工作流程以在 artifacts 中包含 AquaMai.BepInEx.dll 并更新上传步骤
  • 调整 CI 发布步骤以准备 MelonLoader 和 BepInEx 构建以进行分发

文档:

  • 更新 README 以要求 .NET Framework 4.8 Developer Pack 而不是 4.7.2
Original summary in English

Summary by Sourcery

Add BepInEx support and refactor the project into a common bootstrap and environment layer that abstracts logging and initialization for both MelonLoader and BepInEx, vendor the TinyJSON parser, adjust build scripts and namespaces, and update CI workflows and documentation accordingly.

New Features:

  • Add AquaMai.BepInEx plugin project to support BepInEx loader
  • Introduce AquaMai.Common bootstrap and environment abstraction for unified initialization and logging
  • Vendor MelonLoader.TinyJSON library into the repository for internal JSON parsing

Enhancements:

  • Replace direct MelonLoader calls with AquaMai.Core.Environment.MelonLogger wrapper across all modules
  • Add AquaMai.MelonLoader plugin project for MelonLoader compatibility using the new common bootstrap
  • Refactor namespaces and reorganize projects (AquaMai -> AquaMai.Common) to separate core, mods, and loader implementations
  • Add OS platform checks in windowing and anti-lag modules to prevent unsupported operations on non-Windows platforms

Build:

  • Update Cake build script to use the new solution file name (.slnx) and adjust solution namespace
  • Rename csproj files and adjust assembly names to reflect Common and loader-specific projects

CI:

  • Modify GitHub Actions workflow to include AquaMai.BepInEx.dll in artifacts and update upload steps
  • Adjust CI release step to prepare both MelonLoader and BepInEx builds for distribution

Documentation:

  • Update README to require .NET Framework 4.8 Developer Pack instead of 4.7.2

Copy link

sourcery-ai bot commented Jun 29, 2025

## 审查者指南

此 PR 将引导程序和日志记录系统重构为一个共享的通用库,添加了具有专用插件入口的一流 BepInEx 支持,嵌入了 TinyJSON 库以进行内部 JSON 处理,重构了项目命名空间,并更新了 CI 管道以将 BepInEx DLL 与现有工件一起打包和发布。

#### 新的和重构的引导程序和日志记录系统的类图

```mermaid
classDiagram
    class AquaMai.Common.BootstrapOptions {
        +Assembly CurrentAssembly
        +Harmony Harmony
        +Action<string> MsgStringAction
        +Action<object> MsgObjectAction
        +Action<string> ErrorStringAction
        +Action<object> ErrorObjectAction
        +Action<string> WarningStringAction
        +Action<object> WarningObjectAction
    }
    class AquaMai.Common.AquaMai {
        +static void Bootstrap(BootstrapOptions options)
        +static void OnGUI()
        +static void EarlyStageLog(string message)
    }
    class AquaMai.Core.Environment.MelonLogger {
        +static Action<string> MsgStringAction
        +static Action<object> MsgObjectAction
        +static Action<string> ErrorStringAction
        +static Action<object> ErrorObjectAction
        +static Action<string> WarningStringAction
        +static Action<object> WarningObjectAction
        +static void Msg(string message)
        +static void Msg(object message)
        +static void Error(string message)
        +static void Error(object message)
        +static void Warning(string message)
        +static void Warning(object message)
    }
    AquaMai.Common.AquaMai ..> AquaMai.Common.BootstrapOptions : uses
    AquaMai.Common.AquaMai ..> AquaMai.Core.Environment.MelonLogger : sets logger

新的 Mod 加载器入口点(BepInEx 和 MelonLoader)的类图

classDiagram
    class AquaMai.BepInEx.Plugin {
        +const string PluginName
        +ManualLogSource LogSource
        +void Awake()
        +void OnGUI()
    }
    class AquaMai.MelonLoader.AquaMai {
        +void OnInitializeMelon()
        +void OnGUI()
    }
    AquaMai.BepInEx.Plugin --|> BepInEx.BaseUnityPlugin
    AquaMai.MelonLoader.AquaMai --|> MelonLoader.MelonMod
    AquaMai.BepInEx.Plugin ..> AquaMai.Common.AquaMai : calls Bootstrap/OnGUI
    AquaMai.MelonLoader.AquaMai ..> AquaMai.Common.AquaMai : calls Bootstrap/OnGUI
Loading

文件级别更改

变更 详情 文件
将引导程序和日志记录集中到通用库中
  • 创建了一个共享的 AquaMai.Common/AquaMai.cs 用于程序集加载和启动
  • 引入了 AquaMai.Core.Environment/MelonLogger.cs 和 EarlyStageLog 用于抽象日志记录
  • 用新的环境包装器替换了直接的 MelonLoader API 调用和 BuildInfo 引用
AquaMai.Common/AquaMai.cs
AquaMai.Core/Environment/MelonLogger.cs
AquaMai.Core/Startup.cs
AquaMai.Common/AssemblyLoader.cs
AquaMai.Core/BuildInfo.cs
添加 BepInEx 插件集成
  • 带有 BepInEx 属性的新 AquaMai.BepInEx/Plugin.cs 入口点
  • 通过 FodyWeavers.xml 配置 ILMerge 以进行 AquaMai.Common 合并
  • 添加了 BepInEx Properties/AssemblyInfo 以定义插件元数据
AquaMai.BepInEx/Plugin.cs
AquaMai.BepInEx/FodyWeavers.xml
AquaMai.BepInEx/Properties/AssemblyInfo.cs
更新 CI 以生成和发布 BepInEx DLL
  • 将 AquaMai.BepInEx.dll 移动到构建作业中的 Upload 文件夹中
  • 调整了 upload-to-telegram 步骤以附加新的 BepInEx 工件
  • 留下了一个 TODO,以便在 CI 发布运行器中包含 BepInEx
.github/workflows/aquamai.yaml
供应商 TinyJSON 用于内部 JSON 处理
  • 添加了带有 JSON.cs、Encoder.cs、Decoder.cs 的 MelonLoader.TinyJSON 命名空间
  • 包括代理类型(ProxyObject、ProxyArray、ProxyNumber 等)和扩展
  • 添加了 EncodeOptions 枚举和许可文件
MelonLoader.TinyJSON/JSON.cs
MelonLoader.TinyJSON/Encoder.cs
MelonLoader.TinyJSON/Decoder.cs
MelonLoader.TinyJSON/Types/Variant.cs
重构命名空间并重构核心助手
  • 将 AssemblyLoader 命名空间重命名为 AquaMai.Common 并更新了程序集映射
  • 限定了 FileSystem 助手中的 System.Environment 调用
  • 更新了 ConfigLoader 和 EnableConditionHelper 以使用新的环境包装器
  • 调整了构建脚本 (build.cake) 以引用新的解决方案文件
AquaMai.Common/AssemblyLoader.cs
AquaMai.Core/Helpers/FileSystem.cs
AquaMai.Core/ConfigLoader.cs
AquaMai.Core/Helpers/EnableConditionHelper.cs
build.cake

提示和命令

与 Sourcery 互动

  • 触发新的审查: 在 pull request 上评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub issue: 通过回复审查评论,要求 Sourcery 从审查评论创建一个 issue。您也可以回复审查评论并使用 @sourcery-ai issue 从中创建一个 issue。
  • 生成 pull request 标题: 在 pull request 标题中的任何位置写入 @sourcery-ai 以随时生成标题。您也可以在 pull request 上评论 @sourcery-ai title 以随时(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 正文中的任何位置写入 @sourcery-ai summary 以随时在您想要的位置生成 PR 摘要。您也可以在 pull request 上评论 @sourcery-ai summary 以随时(重新)生成摘要。
  • 生成审查者指南: 在 pull request 上评论 @sourcery-ai guide 以随时(重新)生成审查者指南。
  • 解决所有 Sourcery 评论: 在 pull request 上评论 @sourcery-ai resolve 以解决所有 Sourcery 评论。如果您已经解决了所有评论并且不想再看到它们,这将非常有用。
  • 驳回所有 Sourcery 审查: 在 pull request 上评论 @sourcery-ai dismiss 以驳回所有现有的 Sourcery 审查。如果您想重新开始新的审查,这将特别有用 - 不要忘记评论 @sourcery-ai review 以触发新的审查!

自定义您的体验

访问您的 仪表板 以:

  • 启用或禁用审查功能,例如 Sourcery 生成的 pull request 摘要、审查者指南等。
  • 更改审查语言。
  • 添加、删除或编辑自定义审查说明。
  • 调整其他审查设置。

获得帮助

```
Original review guide in English

Reviewer's Guide

This PR refactors the bootstrap and logging system into a shared common library, adds first-class BepInEx support with a dedicated plugin entry, embeds the TinyJSON library for internal JSON handling, restructures project namespaces, and updates the CI pipeline to package and publish the BepInEx DLL alongside existing artifacts.

Class diagram for new and refactored bootstrap and logging system

classDiagram
    class AquaMai.Common.BootstrapOptions {
        +Assembly CurrentAssembly
        +Harmony Harmony
        +Action<string> MsgStringAction
        +Action<object> MsgObjectAction
        +Action<string> ErrorStringAction
        +Action<object> ErrorObjectAction
        +Action<string> WarningStringAction
        +Action<object> WarningObjectAction
    }
    class AquaMai.Common.AquaMai {
        +static void Bootstrap(BootstrapOptions options)
        +static void OnGUI()
        +static void EarlyStageLog(string message)
    }
    class AquaMai.Core.Environment.MelonLogger {
        +static Action<string> MsgStringAction
        +static Action<object> MsgObjectAction
        +static Action<string> ErrorStringAction
        +static Action<object> ErrorObjectAction
        +static Action<string> WarningStringAction
        +static Action<object> WarningObjectAction
        +static void Msg(string message)
        +static void Msg(object message)
        +static void Error(string message)
        +static void Error(object message)
        +static void Warning(string message)
        +static void Warning(object message)
    }
    AquaMai.Common.AquaMai ..> AquaMai.Common.BootstrapOptions : uses
    AquaMai.Common.AquaMai ..> AquaMai.Core.Environment.MelonLogger : sets logger
Loading

Class diagram for new mod loader entry points (BepInEx and MelonLoader)

classDiagram
    class AquaMai.BepInEx.Plugin {
        +const string PluginName
        +ManualLogSource LogSource
        +void Awake()
        +void OnGUI()
    }
    class AquaMai.MelonLoader.AquaMai {
        +void OnInitializeMelon()
        +void OnGUI()
    }
    AquaMai.BepInEx.Plugin --|> BepInEx.BaseUnityPlugin
    AquaMai.MelonLoader.AquaMai --|> MelonLoader.MelonMod
    AquaMai.BepInEx.Plugin ..> AquaMai.Common.AquaMai : calls Bootstrap/OnGUI
    AquaMai.MelonLoader.AquaMai ..> AquaMai.Common.AquaMai : calls Bootstrap/OnGUI
Loading

File-Level Changes

Change Details Files
Centralize bootstrap and logging into common library
  • Created a shared AquaMai.Common/AquaMai.cs for assembly loading and startup
  • Introduced AquaMai.Core.Environment/MelonLogger.cs and EarlyStageLog for abstracted logging
  • Replaced direct MelonLoader API calls and BuildInfo references with the new environment wrapper
AquaMai.Common/AquaMai.cs
AquaMai.Core/Environment/MelonLogger.cs
AquaMai.Core/Startup.cs
AquaMai.Common/AssemblyLoader.cs
AquaMai.Core/BuildInfo.cs
Add BepInEx plugin integration
  • New AquaMai.BepInEx/Plugin.cs entry point with BepInEx attributes
  • Configured ILMerge via FodyWeavers.xml for AquaMai.Common merger
  • Added BepInEx Properties/AssemblyInfo to define plugin metadata
AquaMai.BepInEx/Plugin.cs
AquaMai.BepInEx/FodyWeavers.xml
AquaMai.BepInEx/Properties/AssemblyInfo.cs
Update CI to produce and publish BepInEx DLL
  • Moved AquaMai.BepInEx.dll into the Upload folder in build job
  • Adjusted upload-to-telegram step to attach the new BepInEx artifact
  • Left a TODO to include BepInEx in CI release runner
.github/workflows/aquamai.yaml
Vendor TinyJSON for internal JSON handling
  • Added MelonLoader.TinyJSON namespace with JSON.cs, Encoder.cs, Decoder.cs
  • Included proxy types (ProxyObject, ProxyArray, ProxyNumber, etc.) and Extensions
  • Added EncodeOptions enum and licensing files
MelonLoader.TinyJSON/JSON.cs
MelonLoader.TinyJSON/Encoder.cs
MelonLoader.TinyJSON/Decoder.cs
MelonLoader.TinyJSON/Types/Variant.cs
Restructure namespaces and refactor core helpers
  • Renamed AssemblyLoader namespace to AquaMai.Common and updated assembly maps
  • Qualified System.Environment calls in FileSystem helper
  • Updated ConfigLoader and EnableConditionHelper to use the new environment wrapper
  • Adjusted build scripting (build.cake) to reference new solution file
AquaMai.Common/AssemblyLoader.cs
AquaMai.Core/Helpers/FileSystem.cs
AquaMai.Core/ConfigLoader.cs
AquaMai.Core/Helpers/EnableConditionHelper.cs
build.cake

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

@Menci - 我已经查看了你的更改 - 这里有一些反馈:

  • 将整个 TinyJSON 实现 vendor 会增加大量的维护开销——考虑将其作为 NuGet 依赖项或 git 子模块引用,而不是内联其所有源代码。
  • 你的 CI 工作流程仍然有一个 TODO 来上传 BepInEx DLL——请更新上传和 Releaser 步骤以包含 AquaMai.BepInEx.dll,以便发布是完整的。
  • Cake 脚本已更改为还原 AquaMai.slnx;请验证是否已提交 .slnx 文件,并且所有构建/CI 脚本都一致地引用它,以避免构建中断。
AI 代理的提示
请解决此代码审查中的评论:
## 总体评论
- 将整个 TinyJSON 实现 vendor 会增加大量的维护开销——考虑将其作为 NuGet 依赖项或 git 子模块引用,而不是内联其所有源代码。
- 你的 CI 工作流程仍然有一个 TODO 来上传 BepInEx DLL——请更新上传和 Releaser 步骤以包含 `AquaMai.BepInEx.dll`,以便发布是完整的。
- Cake 脚本已更改为还原 `AquaMai.slnx`;请验证是否已提交 `.slnx` 文件,并且所有构建/CI 脚本都一致地引用它,以避免构建中断。

## 单独评论

### 评论 1
<location> `AquaMai.BepInEx/Plugin.cs:17` </location>
<code_context>
+
+    public readonly static ManualLogSource LogSource = global::BepInEx.Logging.Logger.CreateLogSource(BuildInfo.Name);
+
+    public void Awake()
+    {
+        var harmony = new HarmonyLib.Harmony(PluginName);
</code_context>

<issue_to_address>
Awake 方法不处理来自 Bootstrap 的异常。

将 Bootstrap 调用包装在 try-catch 块中,并使用 LogSource 记录任何异常,以确保错误在 BepInEx 日志中可见。
</issue_to_address>

### 评论 2
<location> `AquaMai.MelonLoader/AquaMai.cs:8` </location>
<code_context>
+
+public class AquaMai : MelonMod
+{
+    public override void OnInitializeMelon()
+    {
+        Common.AquaMai.Bootstrap(new Common.BootstrapOptions
</code_context>

<issue_to_address>
OnInitializeMelon 中没有针对 Bootstrap 失败的错误处理。

将 Bootstrap 调用包装在 try-catch 块中,并使用 MelonLogger.Error 记录异常,以帮助调试并防止模组意外禁用。
</issue_to_address>

### 评论 3
<location> `MelonLoader.TinyJSON/README.md:177` </location>
<code_context>
+* `EncodeOptions.PrettyPrint` will output nicely formatted JSON to make it more readable.
+* `EncodeOptions.NoTypeHints` will disable the outputting of type hints into the JSON output. This may be desirable if you plan to read the JSON into another application that might choke on the type information. You can override this on a per-member basis with the `TinyJSON.TypeHint` attribute.
+* `EncodeOptions.IncludePublicProperties` will include public properties in the output.
+* `EncodeOptions.EnforceHeirarchyOrder` will ensure fields and properties are encoded in class heirarchy order, from the root base class on down, but comes at a slight performance cost.
+
+## Using Variants
</code_context>

<issue_to_address>
将“Heirarchy”的拼写更正为“Hierarchy”。

请更新选项名称及其描述,以使用正确的拼写。
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
* `EncodeOptions.EnforceHeirarchyOrder` will ensure fields and properties are encoded in class heirarchy order, from the root base class on down, but comes at a slight performance cost.
=======
* `EncodeOptions.EnforceHierarchyOrder` will ensure fields and properties are encoded in class hierarchy order, from the root base class on down, but comes at a slight performance cost.
>>>>>>> REPLACE

</suggested_fix>

### 评论 4
<location> `MelonLoader.TinyJSON/Types/Variant.cs:46` </location>
<code_context>
+		}
+
+
+		public virtual object ToType( Type conversionType, IFormatProvider provider )
+		{
+			throw new InvalidCastException( "Cannot convert " + GetType() + " to " + conversionType.Name );
</code_context>

<issue_to_address>
考虑使用 Convert.ChangeType 和集中式异常处理程序将重复的转换和异常逻辑重构为单个泛型方法。

以下是如何将大约 200 行几乎相同的 throws/implicit 运算符折叠为少量方法,而无需更改任何外部行为:

1) 集中“cannot‐convert”异常
2) 通过单个泛型方法 + Convert.ChangeType 实现所有转换
3) 删除所有重复的 ToXxx/implicit 运算符

```csharp
public abstract class Variant : IConvertible
{
    protected static readonly IFormatProvider FormatProvider = NumberFormatInfo.InvariantInfo;

    // each concrete Variant must expose its raw CLR value
    protected abstract object UnderlyingValue { get; }

    // only one place to throw
    private Exception Unsupported(string targetTypeName) =>
      new InvalidCastException($"Cannot convert {GetType().Name} to {targetTypeName}");

    // the only real conversion entry‐point
    public virtual object ToType(Type conversionType, IFormatProvider provider)
    {
      var val = UnderlyingValue;
      if (val is IConvertible ic)
        return Convert.ChangeType(ic, conversionType, provider);
      throw Unsupported(conversionType.Name);
    }

    // IConvertible boilerplate—each call goes through ToType()
    TypeCode IConvertible.GetTypeCode() =>
      UnderlyingValue is IConvertible ic ? ic.GetTypeCode() : TypeCode.Object;

    bool IConvertible.ToBoolean(IFormatProvider p) =>
      (bool)ToType(typeof(bool), p);
    byte IConvertible.ToByte(IFormatProvider p) =>
      (byte)ToType(typeof(byte), p);
    char IConvertible.ToChar(IFormatProvider p) =>
      (char)ToType(typeof(char), p);
    DateTime IConvertible.ToDateTime(IFormatProvider p) =>
      (DateTime)ToType(typeof(DateTime), p);
    decimal IConvertible.ToDecimal(IFormatProvider p) =>
      (decimal)ToType(typeof(decimal), p);
    double IConvertible.ToDouble(IFormatProvider p) =>
      (double)ToType(typeof(double), p);
    short IConvertible.ToInt16(IFormatProvider p) =>
      (short)ToType(typeof(short), p);
    int IConvertible.ToInt32(IFormatProvider p) =>
      (int)ToType(typeof(int), p);
    long IConvertible.ToInt64(IFormatProvider p) =>
      (long)ToType(typeof(long), p);
    sbyte IConvertible.ToSByte(IFormatProvider p) =>
      (sbyte)ToType(typeof(sbyte), p);
    float IConvertible.ToSingle(IFormatProvider p) =>
      (float)ToType(typeof(float), p);
    string IConvertible.ToString(IFormatProvider p) =>
      (string)ToType(typeof(string), p);
    ushort IConvertible.ToUInt16(IFormatProvider p) =>
      (ushort)ToType(typeof(ushort), p);
    uint IConvertible.ToUInt32(IFormatProvider p) =>
      (uint)ToType(typeof(uint), p);
    ulong IConvertible.ToUInt64(IFormatProvider p) =>
      (ulong)ToType(typeof(ulong), p);

    // Now callers can still do:
    //   int x = (int)variant;       // via Convert.ChangeType
    //   string s = variant.ToString();
    // and you’ve removed ~200 lines of duplication.
}
```

— 这保留了每个 IConvertible 和 implicit‐cast 用法,但分解了所有相同的 throw 存根,并删除了每个单独的 `public virtual Xxx(...)` + `implicit operator Xxx(Variant)` 块。
</issue_to_address>

### 评论 5
<location> `MelonLoader.TinyJSON/Types/ProxyArray.cs:57` </location>
<code_context>
+		}
+
+
+		internal bool CanBeMultiRankArray( int[] rankLengths )
+		{
+			return CanBeMultiRankArray( 0, rankLengths );
</code_context>

<issue_to_address>
考虑使用 LINQ 和表达式主体成员重构 CanBeMultiRankArray 方法,以减少样板代码并提高清晰度。

考虑使用 LINQ 和表达式主体成员简化 `CanBeMultiRankArray`。这保持了相同的行为,但减少了样板代码和分支:

```csharp
public sealed class ProxyArray : Variant, IEnumerable<Variant>
{
    readonly List<Variant> list = new List<Variant>();

    public void Add(Variant item) => list.Add(item);
    public Variant this[int i]
    {
        get => list[i];
        set => list[i] = value;
    }
    public int Count => list.Count;

    public IEnumerator<Variant> GetEnumerator() => list.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    internal bool CanBeMultiRankArray(int[] dims)
        => CheckMultiRank(0, dims);

    bool CheckMultiRank(int depth, int[] dims)
    {
        if (dims == null) throw new ArgumentNullException(nameof(dims));
        dims[depth] = Count;

        if (depth == dims.Length - 1)
            return true;

        // must all be ProxyArray with equal counts
        var children = list
            .OfType<ProxyArray>()
            .ToList();

        if (children.Count != Count)
            return false;

        var expected = children[0].Count;
        if (children.Any(c => c.Count != expected))
            return false;

        // recurse
        return children.All(c => c.CheckMultiRank(depth + 1, dims));
    }
}
```

主要变化:
- 将两个重载合并为单个 `CheckMultiRank` 帮助器。
- 使用 `OfType<ProxyArray>()` 进行过滤和强制转换。
- 提前返回不匹配的子计数。
- 用于简单方法/属性的表达式主体成员。
</issue_to_address>

Sourcery 对开源是免费的 - 如果你喜欢我们的评论,请考虑分享它们 ✨
帮助我更有用!请点击每个评论上的 👍 或 👎,我将使用反馈来改进你的评论。
Original comment in English

Hey @Menci - I've reviewed your changes - here's some feedback:

  • Vendoring the entire TinyJSON implementation adds a lot of maintenance overhead—consider referencing it as a NuGet dependency or git submodule instead of inlining all of its source.
  • Your CI workflow still has a TODO to upload the BepInEx DLL—please update the upload and Releaser steps to include AquaMai.BepInEx.dll so releases are complete.
  • The Cake script was changed to restore AquaMai.slnx; please verify that the .slnx file is committed and all build/CI scripts reference it consistently to avoid build breakage.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Vendoring the entire TinyJSON implementation adds a lot of maintenance overhead—consider referencing it as a NuGet dependency or git submodule instead of inlining all of its source.
- Your CI workflow still has a TODO to upload the BepInEx DLL—please update the upload and Releaser steps to include `AquaMai.BepInEx.dll` so releases are complete.
- The Cake script was changed to restore `AquaMai.slnx`; please verify that the `.slnx` file is committed and all build/CI scripts reference it consistently to avoid build breakage.

## Individual Comments

### Comment 1
<location> `AquaMai.BepInEx/Plugin.cs:17` </location>
<code_context>
+
+    public readonly static ManualLogSource LogSource = global::BepInEx.Logging.Logger.CreateLogSource(BuildInfo.Name);
+
+    public void Awake()
+    {
+        var harmony = new HarmonyLib.Harmony(PluginName);
</code_context>

<issue_to_address>
Awake method does not handle exceptions from Bootstrap.

Wrap the Bootstrap call in a try-catch block and log any exceptions using LogSource to ensure errors are visible in BepInEx logs.
</issue_to_address>

### Comment 2
<location> `AquaMai.MelonLoader/AquaMai.cs:8` </location>
<code_context>
+
+public class AquaMai : MelonMod
+{
+    public override void OnInitializeMelon()
+    {
+        Common.AquaMai.Bootstrap(new Common.BootstrapOptions
</code_context>

<issue_to_address>
No error handling in OnInitializeMelon for Bootstrap failures.

Wrap the Bootstrap call in a try-catch block and log exceptions with MelonLogger.Error to aid debugging and prevent the mod from being disabled unexpectedly.
</issue_to_address>

### Comment 3
<location> `MelonLoader.TinyJSON/README.md:177` </location>
<code_context>
+* `EncodeOptions.PrettyPrint` will output nicely formatted JSON to make it more readable.
+* `EncodeOptions.NoTypeHints` will disable the outputting of type hints into the JSON output. This may be desirable if you plan to read the JSON into another application that might choke on the type information. You can override this on a per-member basis with the `TinyJSON.TypeHint` attribute.
+* `EncodeOptions.IncludePublicProperties` will include public properties in the output.
+* `EncodeOptions.EnforceHeirarchyOrder` will ensure fields and properties are encoded in class heirarchy order, from the root base class on down, but comes at a slight performance cost.
+
+## Using Variants
</code_context>

<issue_to_address>
Correct the spelling of 'Heirarchy' to 'Hierarchy'.

Please update both the option name and its description to use the correct spelling.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
* `EncodeOptions.EnforceHeirarchyOrder` will ensure fields and properties are encoded in class heirarchy order, from the root base class on down, but comes at a slight performance cost.
=======
* `EncodeOptions.EnforceHierarchyOrder` will ensure fields and properties are encoded in class hierarchy order, from the root base class on down, but comes at a slight performance cost.
>>>>>>> REPLACE

</suggested_fix>

### Comment 4
<location> `MelonLoader.TinyJSON/Types/Variant.cs:46` </location>
<code_context>
+		}
+
+
+		public virtual object ToType( Type conversionType, IFormatProvider provider )
+		{
+			throw new InvalidCastException( "Cannot convert " + GetType() + " to " + conversionType.Name );
</code_context>

<issue_to_address>
Consider refactoring the repeated conversion and exception logic into a single generic method using Convert.ChangeType and a centralized exception handler.

Here’s how you could collapse ~200 lines of nearly-identical throws/implicit operators down to a handful of methods, without changing any external behavior:

1) centralize the “cannot‐convert” exception  
2) implement all conversions via a single generic method + Convert.ChangeType  
3) drop all the duplicated ToXxx/implicit operators

```csharp
public abstract class Variant : IConvertible
{
    protected static readonly IFormatProvider FormatProvider = NumberFormatInfo.InvariantInfo;

    // each concrete Variant must expose its raw CLR value
    protected abstract object UnderlyingValue { get; }

    // only one place to throw
    private Exception Unsupported(string targetTypeName) =>
      new InvalidCastException($"Cannot convert {GetType().Name} to {targetTypeName}");

    // the only real conversion entry‐point
    public virtual object ToType(Type conversionType, IFormatProvider provider)
    {
      var val = UnderlyingValue;
      if (val is IConvertible ic)
        return Convert.ChangeType(ic, conversionType, provider);
      throw Unsupported(conversionType.Name);
    }

    // IConvertible boilerplate—each call goes through ToType()
    TypeCode IConvertible.GetTypeCode() =>
      UnderlyingValue is IConvertible ic ? ic.GetTypeCode() : TypeCode.Object;

    bool IConvertible.ToBoolean(IFormatProvider p) =>
      (bool)ToType(typeof(bool), p);
    byte IConvertible.ToByte(IFormatProvider p) =>
      (byte)ToType(typeof(byte), p);
    char IConvertible.ToChar(IFormatProvider p) =>
      (char)ToType(typeof(char), p);
    DateTime IConvertible.ToDateTime(IFormatProvider p) =>
      (DateTime)ToType(typeof(DateTime), p);
    decimal IConvertible.ToDecimal(IFormatProvider p) =>
      (decimal)ToType(typeof(decimal), p);
    double IConvertible.ToDouble(IFormatProvider p) =>
      (double)ToType(typeof(double), p);
    short IConvertible.ToInt16(IFormatProvider p) =>
      (short)ToType(typeof(short), p);
    int IConvertible.ToInt32(IFormatProvider p) =>
      (int)ToType(typeof(int), p);
    long IConvertible.ToInt64(IFormatProvider p) =>
      (long)ToType(typeof(long), p);
    sbyte IConvertible.ToSByte(IFormatProvider p) =>
      (sbyte)ToType(typeof(sbyte), p);
    float IConvertible.ToSingle(IFormatProvider p) =>
      (float)ToType(typeof(float), p);
    string IConvertible.ToString(IFormatProvider p) =>
      (string)ToType(typeof(string), p);
    ushort IConvertible.ToUInt16(IFormatProvider p) =>
      (ushort)ToType(typeof(ushort), p);
    uint IConvertible.ToUInt32(IFormatProvider p) =>
      (uint)ToType(typeof(uint), p);
    ulong IConvertible.ToUInt64(IFormatProvider p) =>
      (ulong)ToType(typeof(ulong), p);

    // Now callers can still do:
    //   int x = (int)variant;       // via Convert.ChangeType
    //   string s = variant.ToString();
    // and you’ve removed ~200 lines of duplication.
}
```

— this preserves every IConvertible and implicit‐cast usage, but factors out all the identical throw stubs and removes each individual `public virtual Xxx(...)` + `implicit operator Xxx(Variant)` block.
</issue_to_address>

### Comment 5
<location> `MelonLoader.TinyJSON/Types/ProxyArray.cs:57` </location>
<code_context>
+		}
+
+
+		internal bool CanBeMultiRankArray( int[] rankLengths )
+		{
+			return CanBeMultiRankArray( 0, rankLengths );
</code_context>

<issue_to_address>
Consider refactoring the CanBeMultiRankArray method using LINQ and expression-bodied members to reduce boilerplate and improve clarity.

Consider simplifying `CanBeMultiRankArray` with LINQ and expression‐bodied members. This keeps the same behavior but cuts down on boilerplate and branching:

```csharp
public sealed class ProxyArray : Variant, IEnumerable<Variant>
{
    readonly List<Variant> list = new List<Variant>();

    public void Add(Variant item) => list.Add(item);
    public Variant this[int i]
    {
        get => list[i];
        set => list[i] = value;
    }
    public int Count => list.Count;

    public IEnumerator<Variant> GetEnumerator() => list.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    internal bool CanBeMultiRankArray(int[] dims)
        => CheckMultiRank(0, dims);

    bool CheckMultiRank(int depth, int[] dims)
    {
        if (dims == null) throw new ArgumentNullException(nameof(dims));
        dims[depth] = Count;

        if (depth == dims.Length - 1)
            return true;

        // must all be ProxyArray with equal counts
        var children = list
            .OfType<ProxyArray>()
            .ToList();

        if (children.Count != Count)
            return false;

        var expected = children[0].Count;
        if (children.Any(c => c.Count != expected))
            return false;

        // recurse
        return children.All(c => c.CheckMultiRank(depth + 1, dims));
    }
}
```

Key changes:
- Combine the two overloads into a single `CheckMultiRank` helper.
- Use `OfType<ProxyArray>()` to both filter and cast.
- Early‐return on mismatched child counts.
- Expression‐bodied members for trivial methods/properties.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@Menci
Copy link
Contributor Author

Menci commented Jun 29, 2025

Vendoring MelonLoader.TinyJSON for both platforms since MelonLoader is also deprecating it.

@clansty
Copy link
Contributor

clansty commented Jun 29, 2025

谁会用 bepinex

@clansty clansty self-assigned this Jun 29, 2025
@clansty clansty self-requested a review June 29, 2025 05:43
@clansty
Copy link
Contributor

clansty commented Jun 29, 2025

这 CI 怎么在 main 上跑的

@clansty
Copy link
Contributor

clansty commented Jun 29, 2025

image
image

属实是有点神奇了

@@ -59,6 +59,7 @@ public static void OnBeforePatch()
Screen.SetResolution(width, height, FullScreenMode.Windowed);
}

if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
Copy link
Contributor

Choose a reason for hiding this comment

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

fawo Sinmai.app Sinmai.elf

@clansty
Copy link
Contributor

clansty commented Jun 29, 2025

以及我大概不想在 melonloader 用的 binary 里面也 bundle tinyjson,因为 sinmai 的 melonloader 大概已经停止在 0.6.4 了,更高版本有兼容性问题

@Menci
Copy link
Contributor Author

Menci commented Jun 29, 2025

0.7.0 好像能用

@clansty
Copy link
Contributor

clansty commented Jun 29, 2025

Snipaste_2025-06-29_15-27-08

bepinex 应该用什么版本的

@clansty
Copy link
Contributor

clansty commented Jun 29, 2025

0.7.0 好像能用

搞一份新的 sinmai,把 0.7.0 放进去,控制台里面会什么都没有,然后 melonloader 没有被加载上(0.6.5 及以上版本都是如此)

假如你放了 0.6.4 进去,启动一次,把 0.6.4 删掉,再把 0.7.0 放进去,诶,能用

神奇吧,这个事情挺普遍的,很多人包括我都复现了,但是我没搞明白为什么

所以我现在一般都是让别人用 0.6.4

@Menci
Copy link
Contributor Author

Menci commented Jun 29, 2025

不过好像也不太容易让 MelonLoader 版本去用 MelonLoader 的 TinyJSON(编译没法分开编译,好像只能 patch Assembly),而且 gzipped 十多 K 问题不大((

@Menci
Copy link
Contributor Author

Menci commented Jun 29, 2025

@clansty 我没遇到,我只遇到过有些版本 MelonLoader 用我替换进去的 debug mono 会崩溃的问题

@clansty
Copy link
Contributor

clansty commented Jun 29, 2025

bepinex 应该用什么版本,我这边起不来,没法调试

@Menci
Copy link
Contributor Author

Menci commented Jun 29, 2025

我就是用最新的 bepinex

@Menci
Copy link
Contributor Author

Menci commented Jun 29, 2025

我试试用 segatools 行不行

@clansty
Copy link
Contributor

clansty commented Jun 29, 2025

尝试性只让 BepInEx embed TinyJson,暂存
Subject: [PATCH] Changes
---
Index: AquaMai.Common/AquaMai.Common.csproj
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AquaMai.Common/AquaMai.Common.csproj b/AquaMai.Common/AquaMai.Common.csproj
--- a/AquaMai.Common/AquaMai.Common.csproj	(revision 01f07bad1e1b763499bbc0e60384bbee32f11ffd)
+++ b/AquaMai.Common/AquaMai.Common.csproj	(date 1751184275857)
@@ -37,11 +37,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <ProjectReference Include="../MelonLoader.TinyJSON/MelonLoader.TinyJSON.csproj" />
     <ProjectReference Include="../AquaMai.Config.Interfaces/AquaMai.Config.Interfaces.csproj" />
-    <ProjectReference Include="../AquaMai.Config/AquaMai.Config.csproj" />
-    <ProjectReference Include="../AquaMai.Core/AquaMai.Core.csproj" />
-    <ProjectReference Include="../AquaMai.Mods/AquaMai.Mods.csproj" />
   </ItemGroup>
 
   <ItemGroup>
@@ -60,9 +56,6 @@
   </Target>
 
   <ItemGroup>
-    <EmbeddedResource Include="$(OutputPath)MelonLoader.TinyJSON.dll">
-      <LogicalName>MelonLoader.TinyJSON.dll</LogicalName>
-    </EmbeddedResource>
     <EmbeddedResource Include="$(OutputPath)AquaMai.Config.Interfaces.dll">
       <LogicalName>AquaMai.Config.Interfaces.dll</LogicalName>
     </EmbeddedResource>
Index: AquaMai.MelonLoader/AquaMai.MelonLoader.csproj
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AquaMai.MelonLoader/AquaMai.MelonLoader.csproj b/AquaMai.MelonLoader/AquaMai.MelonLoader.csproj
--- a/AquaMai.MelonLoader/AquaMai.MelonLoader.csproj	(revision 01f07bad1e1b763499bbc0e60384bbee32f11ffd)
+++ b/AquaMai.MelonLoader/AquaMai.MelonLoader.csproj	(date 1751184295424)
@@ -13,7 +13,7 @@
     <LangVersion>12</LangVersion>
     <NoWarn>414</NoWarn>
     <AssemblySearchPaths>$(ProjectDir)../Libs/;$(AssemblySearchPaths)</AssemblySearchPaths>
-    <OutputPath>$(ProjectDir)../Output/</OutputPath>
+    <OutputPath>$(ProjectDir)../Output/MelonLoader/</OutputPath>
     <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <EnableDefaultEmbeddedResourceItems>false</EnableDefaultEmbeddedResourceItems>
@@ -34,10 +34,6 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <ProjectReference Include="../AquaMai.Config.Interfaces/AquaMai.Config.Interfaces.csproj" />
-    <ProjectReference Include="../AquaMai.Config/AquaMai.Config.csproj" />
-    <ProjectReference Include="../AquaMai.Core/AquaMai.Core.csproj" />
-    <ProjectReference Include="../AquaMai.Mods/AquaMai.Mods.csproj" />
     <ProjectReference Include="../AquaMai.Common/AquaMai.Common.csproj" />
   </ItemGroup>
 
Index: AquaMai.BepInEx/AquaMai.BepInEx.csproj
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AquaMai.BepInEx/AquaMai.BepInEx.csproj b/AquaMai.BepInEx/AquaMai.BepInEx.csproj
--- a/AquaMai.BepInEx/AquaMai.BepInEx.csproj	(revision 01f07bad1e1b763499bbc0e60384bbee32f11ffd)
+++ b/AquaMai.BepInEx/AquaMai.BepInEx.csproj	(date 1751184417454)
@@ -12,7 +12,7 @@
     <LangVersion>12</LangVersion>
     <NoWarn>414</NoWarn>
     <AssemblySearchPaths>$(ProjectDir)../Libs/;$(AssemblySearchPaths)</AssemblySearchPaths>
-    <OutputPath>$(ProjectDir)../Output/</OutputPath>
+    <OutputPath>$(ProjectDir)../Output/BepInEx/</OutputPath>
     <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <EnableDefaultEmbeddedResourceItems>false</EnableDefaultEmbeddedResourceItems>
@@ -33,12 +33,15 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <ProjectReference Include="../AquaMai.Config.Interfaces/AquaMai.Config.Interfaces.csproj" />
-    <ProjectReference Include="../AquaMai.Config/AquaMai.Config.csproj" />
-    <ProjectReference Include="../AquaMai.Core/AquaMai.Core.csproj" />
-    <ProjectReference Include="../AquaMai.Mods/AquaMai.Mods.csproj" />
+    <ProjectReference Include="../MelonLoader.TinyJSON/MelonLoader.TinyJSON.csproj" />
     <ProjectReference Include="../AquaMai.Common/AquaMai.Common.csproj" />
   </ItemGroup>
+
+  <ItemGroup>
+    <EmbeddedResource Include="$(OutputPath)MelonLoader.dll">
+      <LogicalName>MelonLoader.dll</LogicalName>
+    </EmbeddedResource>
+  </ItemGroup>
 
   <ItemGroup>
     <Reference Include="mscorlib" />
Index: build.ps1
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/build.ps1 b/build.ps1
--- a/build.ps1	(revision 01f07bad1e1b763499bbc0e60384bbee32f11ffd)
+++ b/build.ps1	(date 1751184343544)
@@ -9,5 +9,7 @@
 dotnet tool restore
 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
 
+taskkill /f /im "dotnet.exe" >$null
+
 dotnet cake @args
 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Index: MelonLoader.TinyJSON/MelonLoader.TinyJSON.csproj
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/MelonLoader.TinyJSON/MelonLoader.TinyJSON.csproj b/MelonLoader.TinyJSON/MelonLoader.TinyJSON.csproj
--- a/MelonLoader.TinyJSON/MelonLoader.TinyJSON.csproj	(revision 01f07bad1e1b763499bbc0e60384bbee32f11ffd)
+++ b/MelonLoader.TinyJSON/MelonLoader.TinyJSON.csproj	(date 1751184466402)
@@ -6,13 +6,14 @@
     <OutputType>Library</OutputType>
     <RootNamespace>MelonLoader.TinyJSON</RootNamespace>
     <AssemblyName>MelonLoader.TinyJSON</AssemblyName>
+    <TargetName>MelonLoader</TargetName>
     <TargetFramework>net48</TargetFramework>
     <FileAlignment>512</FileAlignment>
     <Deterministic>true</Deterministic>
     <LangVersion>12</LangVersion>
     <NoWarn>414</NoWarn>
     <AssemblySearchPaths>$(ProjectDir)../Libs/;$(AssemblySearchPaths)</AssemblySearchPaths>
-    <OutputPath>$(ProjectDir)../Output/</OutputPath>
+    <OutputPath>$(ProjectDir)../Output/BepInEx/</OutputPath>
     <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
     <EnableDefaultEmbeddedResourceItems>false</EnableDefaultEmbeddedResourceItems>
   </PropertyGroup>
Index: AquaMai.Common/AssemblyLoader.cs
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AquaMai.Common/AssemblyLoader.cs b/AquaMai.Common/AssemblyLoader.cs
--- a/AquaMai.Common/AssemblyLoader.cs	(revision 01f07bad1e1b763499bbc0e60384bbee32f11ffd)
+++ b/AquaMai.Common/AssemblyLoader.cs	(date 1751184366753)
@@ -19,7 +19,7 @@
 
     private static readonly Dictionary<AssemblyName, string> Assemblies = new()
     {
-        [AssemblyName.TinyJSON] = "MelonLoader.TinyJSON.dll",
+        [AssemblyName.TinyJSON] = "MelonLoader.dll",
         [AssemblyName.ConfigInterfaces] = "AquaMai.Config.Interfaces.dll",
         [AssemblyName.Config] = "AquaMai.Config.dll",
         [AssemblyName.Core] = "AquaMai.Core.dll",
@@ -54,6 +54,11 @@
         {
             return AppDomain.CurrentDomain.Load(DecompressToBytes(compressedStream));
         }
+        if (assemblyName == Assemblies[AssemblyName.TinyJSON])
+        {
+            // Special case for MelonLoader.TinyJSON.dll, will only be embedded in binary for BepInEx.
+            return null;
+        }
         throw new Exception($"Embedded assembly \"{assemblyName}\" not found.");
     }
 

懒得搞了,晚点我从别的地方抠十几 KB 出来

@clansty
Copy link
Contributor

clansty commented Jun 29, 2025

我觉得你可以考虑自己维护一个支持 BepInEX 的分支叫 BquaMai

说不定我什么时候改了啥就 break 了,比如说我想把 Tomlet Shim 掉,BepInEx 里面也没内置 Tomlet(虽然有个自己的 Toml 解析器)

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.

3 participants