Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
110 changes: 110 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
name: Release Dalamud Plugin

on:
push:
tags:
- "v*"

permissions:
contents: write

jobs:
release:
runs-on: windows-2022

steps:
- name: Checkout
uses: actions/checkout@v5

- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x

- name: Download Dalamud dev files
shell: pwsh
run: |
$target = Join-Path $env:AppData "XIVLauncher\addon\Hooks\dev"
New-Item -ItemType Directory -Force -Path $target | Out-Null
Invoke-WebRequest -Uri "https://goatcorp.github.io/dalamud-distrib/latest.zip" -OutFile latest.zip
Expand-Archive -Path latest.zip -DestinationPath $target -Force

- name: Validate tag matches project version
shell: pwsh
run: |
[xml]$project = Get-Content "StarlightBreaker.Dalamud/StarlightBreaker.csproj"
$version = $project.Project.PropertyGroup.Version | Select-Object -First 1
if (-not $version) {
throw "Version not found in StarlightBreaker.Dalamud/StarlightBreaker.csproj."
}

$tag = "${{ github.ref_name }}"
if ($tag -ne "v$version") {
throw "Tag '$tag' does not match project version 'v$version'."
}

- name: Restore
run: dotnet restore StarlightBreaker.Dalamud/StarlightBreaker.csproj --locked-mode

- name: Build
run: dotnet build StarlightBreaker.Dalamud/StarlightBreaker.csproj --configuration Release --no-restore

- name: Package release assets
shell: pwsh
run: |
$tag = "${{ github.ref_name }}"
$assetDir = Join-Path $env:RUNNER_TEMP "release"
New-Item -ItemType Directory -Force -Path $assetDir | Out-Null

$files = @(
"Output/StarlightBreaker.dll",
"Output/StarlightBreaker.deps.json",
"Output/StarlightBreaker.json"
)

foreach ($file in $files) {
if (-not (Test-Path $file)) {
throw "Missing release file: $file"
}

Copy-Item $file -Destination $assetDir
}

$zipPath = Join-Path $env:RUNNER_TEMP "StarlightBreaker-$tag.zip"
if (Test-Path $zipPath) {
Remove-Item $zipPath -Force
}

Compress-Archive -Path (Join-Path $assetDir "*") -DestinationPath $zipPath

- name: Upload workflow artifact
uses: actions/upload-artifact@v4
with:
name: StarlightBreaker-${{ github.ref_name }}
path: |
Output/StarlightBreaker.dll
Output/StarlightBreaker.deps.json
Output/StarlightBreaker.json
${{ runner.temp }}/StarlightBreaker-${{ github.ref_name }}.zip
if-no-files-found: error

- name: Publish GitHub release
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
$tag = "${{ github.ref_name }}"
$zipPath = Join-Path $env:RUNNER_TEMP "StarlightBreaker-$tag.zip"
$assets = @(
$zipPath,
"Output/StarlightBreaker.dll",
"Output/StarlightBreaker.deps.json",
"Output/StarlightBreaker.json"
)

gh release view $tag *> $null
if ($LASTEXITCODE -eq 0) {
gh release upload $tag @assets --clobber
} else {
gh release create $tag @assets --title $tag --generate-notes
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ BenchmarkDotNet.Artifacts/
project.lock.json
project.fragment.lock.json
artifacts/
Output/

# StyleCop
StyleCopReport.xml
Expand Down
66 changes: 66 additions & 0 deletions HANDOFF.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# StarlightBreaker Handoff

## Summary
- Maintenance target is `StarlightBreaker.Dalamud`
- This round of work focused on making the Dalamud plugin load correctly on the current `XIVLauncherCN` environment and wiring up automatic GitHub releases
- The plugin now builds against the active `XIVLauncherCN` dev assemblies and the latest tested output is:
- `Output/StarlightBreaker.dll`
- `Output/StarlightBreaker.deps.json`
- `Output/StarlightBreaker.json`
- GitHub Actions now builds and publishes release assets when a matching `v*` tag is pushed
- A pull request to upstream was opened from fork `main`:
- `https://github.com/Loskh/StarlightBreaker/pull/20`

## What Was Fixed Today
- Adapted the plugin to the current API 15 environment used by `XIVLauncherCN`
- Fixed the plugin loading failure caused by building against the wrong local Dalamud installation
- Removed direct plugin-service injection of the scanner type, because the runtime-exposed scanner API differs across local Dalamud builds
- Switched signature resolution to `IGameInteropProvider.InitializeFromAttributes(...)` with `SignatureAttribute`, which uses the documented public API and avoids private reflection over internal scanner fields
- Fixed local reference resolution by pinning the project to `$(AppData)\XIVLauncherCN\addon\Hooks\dev\`
- Corrected windowing compatibility so the built assembly now targets `WindowSystem.AddWindow(IWindow)` instead of the old `Window` signature
- Updated new-config defaults so these options start enabled:
- `ChatLogConfig.EnableColor`
- `FontConfig.Italics`
- `FontConfig.EnableColor`
- `PartyFinderConfig.Enable` was already enabled by default

## Root Cause
- The machine had multiple local Dalamud installations:
- `XIVLauncher\addon\Hooks\dev`
- `XIVLauncherCN\addon\Hooks\dev`
- The game was running with `XIVLauncherCN`, but the project was originally compiling against the other local `Dalamud.dll`
- Because of that mismatch, the plugin compiled successfully but failed at runtime with API-shape differences such as:
- missing `Dalamud.Game.ISigScanner`
- old/new `WindowSystem.AddWindow(...)` signature mismatch

## Current Build Configuration
- Project file: [StarlightBreaker.Dalamud/StarlightBreaker.csproj](StarlightBreaker.Dalamud/StarlightBreaker.csproj)
- Important settings:
- `DalamudLibPath` prefers `$(AppData)\XIVLauncherCN\addon\Hooks\dev\` when that local path exists
- If `DALAMUD_HOME` is set, the SDK uses that path instead
- Otherwise CI and other environments fall back to the SDK default `$(AppData)\XIVLauncher\addon\Hooks\dev\`
- `TargetFramework` is `net10.0-windows`
- Dalamud-related references use explicit `HintPath` values rooted at `$(DalamudLibPath)`
- Output is written to `Output/`
- Release workflow file is `.github/workflows/release.yml`

## Key Files
- [StarlightBreaker.Dalamud/Plugin.cs](StarlightBreaker.Dalamud/Plugin.cs)
- [StarlightBreaker.Dalamud/Configuration.cs](StarlightBreaker.Dalamud/Configuration.cs)
- [StarlightBreaker.Dalamud/StarlightBreaker.csproj](StarlightBreaker.Dalamud/StarlightBreaker.csproj)
- [.github/workflows/release.yml](.github/workflows/release.yml)

## Validation Performed
- `dotnet build StarlightBreaker.Dalamud\StarlightBreaker.csproj`
- Verified the produced assembly references the active CN dev environment
- Verified the built plugin no longer exposes scanner injection properties that break current runtime loading
- Verified the built plugin references `WindowSystem.AddWindow(IWindow)` in assembly metadata

## Remaining Notes
- Existing local user config is not force-migrated; default option changes only affect new configs
- The repo still contains legacy non-Dalamud targets, but the active work path is the Dalamud plugin
- GitHub release publishing is tag-driven; push a tag that matches the project version in `StarlightBreaker.Dalamud/StarlightBreaker.csproj`, for example `v2.15.0`
- If a future loading error appears, check these first:
- whether `XIVLauncherCN` is still the actual runtime host
- whether local overrides or `DALAMUD_HOME` still point to the same host's `addon\Hooks\dev`
- whether the built output in `Output/` is the file actually loaded by Dalamud
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
FFXIV国服屏蔽词解除
可自动查找偏移,理论上来说,除非大版本更新,插件本体不用更新.~~同时也可以在国际服使用(?)~~

## 交接文档

这次 API 15 / `XIVLauncherCN` 兼容修复与自动发布工作流的完整交接记录见:

- [HANDOFF.md](HANDOFF.md)

使用方法:选择一个喜欢的就行,不可同时选择

* 使用ACT插件管理加载StarlightBreaker.dll即可
Expand Down
6 changes: 3 additions & 3 deletions StarlightBreaker.Dalamud/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace StarlightBreaker
public class ChatLogConfig
{
public bool Enable = true;
public bool EnableColor = false;
public bool EnableColor = true;
}
[Serializable]
public class PartyFinderConfig
Expand All @@ -20,8 +20,8 @@ public class PartyFinderConfig
public class FontConfig
{
public ushort Color = 17;
public bool Italics = false;
public bool EnableColor = false;
public bool Italics = true;
public bool EnableColor = true;
}
[Serializable]
public class Configuration : IPluginConfiguration
Expand Down
53 changes: 29 additions & 24 deletions StarlightBreaker.Dalamud/Plugin.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using Dalamud.Game.Command;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
Expand All @@ -7,17 +8,14 @@
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Component.Text;
using InteropGenerator.Runtime;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using UTF8String = FFXIVClientStructs.FFXIV.Client.System.String.Utf8String;

namespace StarlightBreaker
Expand All @@ -30,8 +28,6 @@ public unsafe class Plugin : IDalamudPlugin
[PluginService]
internal static IDalamudPluginInterface PluginInterface { get; private set; } = null!;
[PluginService]
internal static ISigScanner Scanner { get; private set; } = null!;
[PluginService]
internal static ICommandManager CommandManager { get; private set; } = null!;
[PluginService]
internal static IDataManager DataManager { get; private set; } = null!;
Expand All @@ -58,18 +54,23 @@ public unsafe class Plugin : IDalamudPlugin
//private VulgarCheckDelegate VulgarCheck;

public delegate Utf8String* RaptureTextModuleChatLogFilterDelegate(RaptureTextModule* textModule, Utf8String* text, nint unk, uint bytesNum);
Hook<RaptureTextModuleChatLogFilterDelegate> RaptureTextModuleChatLogFilterHook;
[Signature("40 53 48 83 EC 20 48 8D 99 ?? ?? ?? ?? 48 8B CB E8 ?? ?? ?? ?? 48 8B 0D", DetourName = nameof(RaptureTextModuleChatLogFilterDetour))]
private Hook<RaptureTextModuleChatLogFilterDelegate> RaptureTextModuleChatLogFilterHook = null!;

public delegate uint AgentLookingForGroupTextFilterDelegate(AgentLookingForGroup* agent, Utf8String* text);
Hook<AgentLookingForGroupTextFilterDelegate> AgentLookingForGroupTextFilterHook;
[Signature("48 89 5C 24 ?? 57 48 83 EC 20 C6 81 ?? ?? ?? ?? ?? 48 8B D9 48 8B 49 10 48 8B FA", DetourName = nameof(AgentLookingForGroupTextFilterDetour))]
private Hook<AgentLookingForGroupTextFilterDelegate> AgentLookingForGroupTextFilterHook = null!;

[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
public delegate void RaptureTextModulePartyFinderFilterDelegate(RaptureTextModule* textModule, Utf8String* text, nint unk, bool unk1);
private RaptureTextModulePartyFinderFilterDelegate RaptureTextModulePartyFinderFilterOrigin;
private CallHook<RaptureTextModulePartyFinderFilterDelegate> AgentLookingForGroupDetailedWindowTextFilterHook;
private RaptureTextModulePartyFinderFilterDelegate RaptureTextModulePartyFinderFilterOrigin = null!;
private CallHook<RaptureTextModulePartyFinderFilterDelegate> AgentLookingForGroupDetailedWindowTextFilterHook = null!;

[Signature("E8 ?? ?? ?? ?? 44 38 A3 ?? ?? ?? ?? 74 ?? 48 8D 15", UseFlags = SignatureUseFlags.Pointer)]
private IntPtr AgentLookingForGroupDetailedWindowTextFilterHookAddress = IntPtr.Zero;

public delegate Utf8String* AgentLookingForGroupProcessStringDelegate(AgentLookingForGroup* agent, Utf8String* text, uint bytesNum);
Hook<AgentLookingForGroupProcessStringDelegate> AgentLookingForGroupProcessStringHook;
[Signature("40 53 56 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B F9 49 8B F0", UseFlags = SignatureUseFlags.Pointer)]
private IntPtr RaptureTextModulePartyFinderFilterAddress = IntPtr.Zero;

public unsafe Plugin()
{
Expand All @@ -86,24 +87,22 @@ public unsafe Plugin()
HelpMessage = "Open Config Window for StarlightBreaker"
});

this.RaptureTextModuleChatLogFilterHook = GameInteropProvider.HookFromSignature<RaptureTextModuleChatLogFilterDelegate>("40 53 48 83 EC 20 48 8D 99 ?? ?? ?? ?? 48 8B CB E8 ?? ?? ?? ?? 48 8B 0D", this.RaptureTextModuleChatLogFilterDetour);
GameInteropProvider.InitializeFromAttributes(this);

this.RaptureTextModuleChatLogFilterHook.Enable();

this.AgentLookingForGroupTextFilterHook = GameInteropProvider.HookFromSignature<AgentLookingForGroupTextFilterDelegate>("48 89 5C 24 ?? 57 48 83 EC 20 C6 81 ?? ?? ?? ?? ?? 48 8B D9 48 8B 49 10 48 8B FA", this.AgentLookingForGroupTextFilterDetour);
this.AgentLookingForGroupTextFilterHook.Enable();

var hookAddress = Scanner.ScanAllText("E8 ?? ?? ?? ?? 44 38 A3 ?? ?? ?? ?? 74 ?? 48 8D 15").First();
PluginLog.Debug($"AgentLookingForGroupDetailedWindowTextFilterHook:{Util.DescribeAddress(hookAddress)}");
this.AgentLookingForGroupDetailedWindowTextFilterHook = new CallHook<RaptureTextModulePartyFinderFilterDelegate>(hookAddress, RaptureTextModulePartyFinderFilter);
PluginLog.Debug($"AgentLookingForGroupDetailedWindowTextFilterHook:{Util.DescribeAddress(this.AgentLookingForGroupDetailedWindowTextFilterHookAddress)}");
this.AgentLookingForGroupDetailedWindowTextFilterHook = new CallHook<RaptureTextModulePartyFinderFilterDelegate>(this.AgentLookingForGroupDetailedWindowTextFilterHookAddress, RaptureTextModulePartyFinderFilter);
this.AgentLookingForGroupDetailedWindowTextFilterHook?.Enable();


//由于"E8 ?? ?? ?? ?? 44 38 A3 ?? ?? ?? ?? 74 11"和"E8 ?? ?? ?? ?? 44 38 A3 ?? ?? ?? ?? 74 ?? 48 8D 15"是同一地址的不同签名ffxiv_dx11.exe+0x5419A3 函数地址为ffxiv_dx11.exe+0x97B820
//之前的版本已经被Hook,因此开头不再是E9或E8,因此后面的ScanText会返回ffxiv_dx11.exe+0x5419A3,而不是0x97B820
//var raptureTextModulePartyFinderFilterAddress = Scanner.ScanText("E8 ?? ?? ?? ?? 44 38 A3 ?? ?? ?? ?? 74 11");
var raptureTextModulePartyFinderFilterAddress = Scanner.ScanText("40 53 56 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B F9 49 8B F0");
PluginLog.Debug($"RaptureTextModulePartyFinderFilter:{Util.DescribeAddress(raptureTextModulePartyFinderFilterAddress)}");
this.RaptureTextModulePartyFinderFilterOrigin = Marshal.GetDelegateForFunctionPointer<RaptureTextModulePartyFinderFilterDelegate>(raptureTextModulePartyFinderFilterAddress);
PluginLog.Debug($"RaptureTextModulePartyFinderFilter:{Util.DescribeAddress(this.RaptureTextModulePartyFinderFilterAddress)}");
this.RaptureTextModulePartyFinderFilterOrigin = Marshal.GetDelegateForFunctionPointer<RaptureTextModulePartyFinderFilterDelegate>(this.RaptureTextModulePartyFinderFilterAddress);
//TODO:
//E8 ?? ?? ?? ?? 0F B6 BB ?? ?? ?? ?? 40 84 FF 74 ?? 48 8D 15
//当出现无法处理招募的时候检查文本并显示导致无法发送的地方
Expand Down Expand Up @@ -233,21 +232,27 @@ private void HighlightCensoredParts(Utf8String* original, Utf8String* processed,

var origText = (TextPayload)orig;
var procText = (TextPayload)proc;
var origStr = origText.Text;
var procStr = procText.Text;

if (origStr is null || procStr is null)
{
builder.Add(origText);
continue;
}

if (origText.Text == procText.Text)
if (origStr == procStr)
{
builder.Add(origText);
continue;
}

if (origText.Text.Length != procText.Text.Length)
if (origStr.Length != procStr.Length)
{
builder.Add(origText);
continue;
}

string origStr = origText.Text;
string procStr = procText.Text;
int length = origStr.Length;
int j = 0;

Expand Down
Loading