Skip to content

Quick Start

Ling edited this page Jul 22, 2026 · 4 revisions

快速开始

本页创建一个最小 C# Mixin 插件,包含插件程序集、plugin.json 和 Mixin 配置。

1. 准备 PCL.Core.dll

插件应引用你要支持的启动器版本对应的 PCL.Core.dll。公开发布版只有一个 .NET 单文件 apphost,PCL.Core.dll 被嵌入在 EXE 中,需要先提取。

从 Release apphost 提取

  1. 打开 PCL2-Nex GitHub Releases,选择目标 BaseVersion。
  2. 下载与插件目标架构一致的 PCL2_Nex_Release_x64.exePCL2_Nex_Release_ARM64.exe
  3. 在 EXE 所在目录创建 Extract-PclCore.ps1,内容如下:
param(
    [Parameter(Mandatory)]
    [string] $AppHost,
    [string] $OutputPath
)

$ErrorActionPreference = 'Stop'
$AppHost = (Resolve-Path -LiteralPath $AppHost).Path
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
    $OutputPath = Join-Path (Split-Path -Parent $AppHost) 'PCL.Core.dll'
}
$OutputPath = [IO.Path]::GetFullPath($OutputPath)
$outputDirectory = Split-Path -Parent $OutputPath
if (-not (Test-Path -LiteralPath $outputDirectory)) {
    New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null
}

# .NET apphost bundle signature. The preceding Int64 stores the bundle header offset.
$signature = [byte[]] (
    0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
    0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
    0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
    0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae
)

$image = [IO.File]::ReadAllBytes($AppHost)
$signatureOffset = -1
for ($i = 8; $i -le $image.Length - $signature.Length; $i++) {
    if ($image[$i] -ne $signature[0]) { continue }
    $matched = $true
    for ($j = 1; $j -lt $signature.Length; $j++) {
        if ($image[$i + $j] -ne $signature[$j]) {
            $matched = $false
            break
        }
    }
    if ($matched) {
        $signatureOffset = $i
        break
    }
}

if ($signatureOffset -lt 8) {
    throw '输入文件不是受支持的 .NET 单文件 apphost。'
}

$headerOffset = [BitConverter]::ToInt64($image, $signatureOffset - 8)
$stream = [IO.File]::OpenRead($AppHost)
try {
    $reader = [IO.BinaryReader]::new($stream, [Text.Encoding]::UTF8, $true)
    $stream.Position = $headerOffset
    $major = $reader.ReadUInt32()
    $minor = $reader.ReadUInt32()
    $fileCount = $reader.ReadInt32()
    $null = $reader.ReadString() # Bundle ID

    if ($major -ge 2) {
        $null = $reader.ReadInt64()  # deps.json offset
        $null = $reader.ReadInt64()  # deps.json size
        $null = $reader.ReadInt64()  # runtimeconfig.json offset
        $null = $reader.ReadInt64()  # runtimeconfig.json size
        $null = $reader.ReadUInt64() # Bundle flags
    }

    $entry = $null
    for ($i = 0; $i -lt $fileCount; $i++) {
        $offset = $reader.ReadInt64()
        $size = $reader.ReadInt64()
        $compressedSize = if ($major -ge 6) { $reader.ReadInt64() } else { 0 }
        $null = $reader.ReadByte() # File type
        $relativePath = $reader.ReadString()

        if ($relativePath -ieq 'PCL.Core.dll') {
            $entry = [pscustomobject]@{
                Offset = $offset
                Size = $size
                CompressedSize = $compressedSize
            }
            break
        }
    }

    if ($null -eq $entry) {
        throw 'apphost Bundle 中没有找到 PCL.Core.dll。'
    }

    $stream.Position = $entry.Offset
    $storedSize = if ($entry.CompressedSize -gt 0) {
        $entry.CompressedSize
    } else {
        $entry.Size
    }
    if ($storedSize -gt [int]::MaxValue) {
        throw 'PCL.Core.dll 过大,当前脚本无法提取。'
    }

    $data = $reader.ReadBytes([int] $storedSize)
    if ($data.Length -ne $storedSize) {
        throw '读取 PCL.Core.dll 时意外到达 apphost 末尾。'
    }

    if ($entry.CompressedSize -gt 0) {
        $source = [IO.MemoryStream]::new($data, $false)
        $deflate = [IO.Compression.DeflateStream]::new(
            $source,
            [IO.Compression.CompressionMode]::Decompress
        )
        $target = [IO.File]::Create($OutputPath)
        try {
            $deflate.CopyTo($target)
        } finally {
            $target.Dispose()
            $deflate.Dispose()
            $source.Dispose()
        }
    } else {
        [IO.File]::WriteAllBytes($OutputPath, $data)
    }
} finally {
    $stream.Dispose()
}

$assembly = [Reflection.AssemblyName]::GetAssemblyName($OutputPath)
$version = [Diagnostics.FileVersionInfo]::GetVersionInfo($OutputPath).ProductVersion
Write-Host "已提取: $OutputPath"
Write-Host "程序集: $($assembly.Name)"
Write-Host "BaseVersion: $version"

在 PowerShell 中运行:

powershell -ExecutionPolicy Bypass -File .\Extract-PclCore.ps1 `
  -AppHost .\PCL2_Nex_Release_x64.exe `
  -OutputPath .\sdk\2026.07.1\x64\PCL.Core.dll

脚本末尾必须显示 程序集: PCL.CoreBaseVersion 必须等于 Release Tag 去掉前缀 v 后的值。把这个 BaseVersion 原样写入插件的 pclCoreVersion。不要只设置 DOTNET_BUNDLE_EXTRACT_BASE_DIR:当前 Release apphost 会直接从 Bundle 加载托管程序集,不能依靠该变量取得 PCL.Core.dll

从本地构建取得

如果你正在构建启动器源码,可以直接使用同次构建产生的 DLL,不需要从 apphost 提取。Debug 构建通常位于:

PCL.Core/bin/Debug-x64/net8.0-windows/PCL.Core.dll

Release 构建通常位于:

PCL.Core/bin/Release-x64/net8.0-windows/PCL.Core.dll

不要从另一个启动器版本随意复制 DLL。公开发布插件时优先从对应 Release apphost 提取,确保引用内容与用户实际下载的版本完全一致。

2. 创建项目

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <LangVersion>latest</LangVersion>
    <AssemblyName>Example.Plugin</AssemblyName>
  </PropertyGroup>

  <ItemGroup>
    <Reference Include="PCL.Core">
      <HintPath>C:\path\to\PCL.Core.dll</HintPath>
      <Private>false</Private>
    </Reference>
  </ItemGroup>
</Project>

需要创建或修改 WPF 控件时再加入:

<UseWPF>true</UseWPF>

Private=false 很重要。PCLX 不应携带自己的 PCL.Core.dll

3. 编写第一个 Mixin

下面的示例在 PCL.FormMain 构造完成后执行一次处理器。使用字符串目标可以避免在编译时直接引用非公开主程序类型。

using System.Diagnostics;
using System.Reflection;
using PCL.Mixin;

namespace Example.Plugin.Mixins;

[Mixin("PCL.FormMain")]
internal static class FormMainMixin
{
    [Inject(".ctor", At = MixinAt.Return, Priority = int.MaxValue)]
    private static void AfterCreated([This] object window, MethodBase target)
    {
        Debug.WriteLine($"PCL.Mixin applied: {window.GetType().FullName}.{target.Name}");
    }
}

[Mixin(typeof(SomeType))] 适合你能直接引用的公开类型;[Mixin("Namespace.TypeName")] 适合启动器内部类型或其他插件类型。

4. 创建 Mixin 配置

文件名示例:mixins/example.plugin.mixins.json

{
  "required": true,
  "package": "Example.Plugin.Mixins",
  "mixins": ["FormMainMixin"],
  "priority": 1000,
  "injectors": {
    "defaultRequire": 1
  }
}
  • package 是 Mixin 类型的公共命名空间前缀。
  • mixins 中可以写相对类名,也可以写完整类型名。
  • required: true 表示配置失败时该插件加载失败并被禁用。
  • defaultRequire 是未显式声明 Require 时的默认最少匹配数。

5. 创建 plugin.json

{
  "id": "example.plugin",
  "name": "Example Plugin",
  "version": "1.0.0",
  "author": "Example",
  "description": "Minimal PCL.Mixin plugin.",
  "pclCoreVersion": "2026.07.1",
  "entryAssembly": "lib/Example.Plugin.dll",
  "mixinConfig": "mixins/example.plugin.mixins.json",
  "logo": "assets/logo.png"
}

pclCoreVersion 替换为实际引用的 PCL.Core BaseVersion。格式严格为 yyyy.MM.patch

6. 组装 PCLX

example.plugin.pclx
  plugin.json
  lib/
    Example.Plugin.dll
    Example.Plugin.pdb
  mixins/
    example.plugin.mixins.json
  assets/
    logo.png

PCLX 是 ZIP 内容使用 .pclx 扩展名的包。plugin.json 必须位于包根目录。

7. 安装和验证

可以通过启动器插件页面选择 PCLX,也可以双击已关联的 .pclx 文件。用于本地 Debug 时,也可以在启动器关闭后把已解包目录放到:

<LauncherOutput>/PCL/Plugins/example.plugin/

重启启动器后查看 PCL/Log/ 中的插件和 Mixin 日志。修改插件 DLL 或配置后必须重新启动;当前不支持热卸载和热重载。

下一步阅读 Mixin API 参考注入点与修改操作

Clone this wiki locally