Skip to content
Merged
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
45 changes: 45 additions & 0 deletions .github/workflows/project-refrain-trainer-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: "[Project] Refrain Trainer CI"

on:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
verify:
runs-on: windows-latest
timeout-minutes: 10
defaults:
run:
shell: powershell
working-directory: others/game-tools/refrain-trainer

steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Build and check owned native fixture
run: |
.\check.cmd
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Package and verify archive
run: .\package.ps1

- name: Upload preview and validation output
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: refrain-trainer-windows-preview
if-no-files-found: error
path: |
others/game-tools/refrain-trainer/dist/*.zip
others/game-tools/refrain-trainer/dist/*.sha256
others/game-tools/refrain-trainer/test-output/native-test-report.txt
others/game-tools/refrain-trainer/test-output/trainer-preview.png
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ Personal monorepo for apps, packages, local Python projects, and assorted experi
| `apps/youtube-auto-resume-extension` | Chromium Manifest V3 YouTube playback assistant. | — |
| `packages/*` | Reusable frontend packages. | — |
| `workbench/*` | Local Python utilities and small projects. | — |
| `others/*` | Others. | — |
| `others/*` | Standalone game tools, userscripts, interface studies, and archived experiments. | — |

Archived tools include [Refrain Trainer](./others/game-tools/refrain-trainer/README.md), a one-off Chinese Windows trainer for Labyrinth of Refrain. See the [Others index](./others/README.md) for the full inventory.

## Related repositories

Expand Down
12 changes: 12 additions & 0 deletions apps/landing/src/config/projects/others.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import type { CatalogProject } from "../../types/project"

export const otherProjectEntries = [
{
title: "Refrain Trainer",
path: "others/game-tools/refrain-trainer",
updatedAt: "2026-10-18T17:32:14+08:00",
summary:
"An archived, one-off Chinese Windows trainer for Labyrinth of Refrain, with one-hit kills, guaranteed Witch's Crest drops, and optional Legendary equipment.",
label: "Game trainer · Preview",
lifecycle: "archived",
kind: "other",
presentation: "catalog",
section: "others",
},
{
title: "Google AI Mode Aurora Study",
path: "others/interface-studies/google-ai-mode-aurora",
Expand Down
1 change: 1 addition & 0 deletions docs/repository-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ Current workflow ownership:
| `group-apps-packages-ci.yml` | Baseline dependency audit, check, and build for apps and packages |
| `group-workbench-python-ci.yml` | Workbench Ruff, registered tests, and dependency audits |
| `project-liminal-drift-ci.yml` | Liminal Drift project and browser validation |
| `project-refrain-trainer-ci.yml` | Manual-only reproduction of the archived Windows trainer build, native checks, and ZIP |
| `project-shika-ci.yml` | Shika static, command, migration, schema, and production validation |
| `project-youtube-auto-resume-ci.yml` | YouTube userscript unit, build-drift, and browser validation |
| `project-youtube-auto-resume-extension-ci.yml` | YouTube extension build and browser validation |
Expand Down
3 changes: 3 additions & 0 deletions others/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ Projects that do not belong to deployable apps, reusable packages, or the local

| Path | Purpose |
| ----------------------------------------- | --------------------------------------------------------------------------------------- |
| `game-tools/refrain-trainer` | Archived one-off Chinese Windows trainer for Labyrinth of Refrain. |
| `userscripts/bilibili-follow-cycle` | Archived Bilibili browser userscript. |
| `userscripts/youtube-auto-resume` | Archived YouTube userscript retained as the migration baseline for the extension app. |
| `github-actions/star-cycle` | Retired GitHub Star/Unstar workflow experiment. |
| `interface-studies/google-ai-mode-aurora` | Forensic analysis and dependency-free reproduction of Google Search's Aurora edge glow. |

GitHub only executes workflow files from the repository-level `.github/workflows/` directory. The Star Cycle YAML stored here is intentionally inactive.

Refrain Trainer is an archived, standalone C#/.NET Framework tool, outside the pnpm workspace. Its [README](./game-tools/refrain-trainer/README.md) records Windows build, validation, packaging, and the explicit unlicensed status. The Windows workflow is manual-only for reproduction; ongoing maintenance is not planned. Generated binaries, archives, game data, and save files are not committed.
2 changes: 2 additions & 0 deletions others/game-tools/refrain-trainer/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[*.{cs,py}]
indent_size = 4
4 changes: 4 additions & 0 deletions others/game-tools/refrain-trainer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/dist/
/test-output/
*.exe
*.zip
86 changes: 86 additions & 0 deletions others/game-tools/refrain-trainer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# 鲁弗兰迷宫修改器

适用于 Steam 版 **Labyrinth of Refrain: Coven of Dusk** 的中文 Windows 修改器,提供一击必杀、魔女大纹章固定掉落和传说装备选项。

当前状态:**已归档,中文 v2 测试版**。这是按一次具体需求制作的工具,本仓库保留源码、使用说明和构建方式,不计划持续维护或适配后续游戏版本。已完成一次真实贝可罗尼亚大王战斗和掉落验证;保存后重新读档、装备详情逐项核对及结算耗时对比未完成。详见 [验证说明](./docs/validation.md)。

这是独立的 C#/.NET Framework 工具,不属于 pnpm workspace。仓库归档保存源码和构建脚本;生成的 EXE、ZIP、测试报告、游戏数据和玩家存档不纳入 Git。

## 使用条件

- Windows 10/11,已启用 .NET Framework 4.x。
- Steam App `566540`,Build `3256141`,32 位原始 `refrain.exe`。
- 游戏主程序 SHA-256:`FC0F248BDC42BCFAF9C8D679F99A707FA8D20DC570F2B16203F1403EC9B58087`。
- 游戏与修改器使用相同的运行权限。

运行修改器无需安装 Cheat Engine、Python 或 Node.js。游戏可以安装在任意盘符和目录;连接时从运行中的游戏进程取得路径,读取该目录的装备表。主程序、原始指令或装备表不匹配时会拒绝启用。

## 快速使用

1. 从 Steam 启动游戏并读入存档。
2. 运行 `RefrainTrainer.exe`,点击“连接游戏”。四个开关默认关闭。
3. 刷魔女大纹章时开启“一击必杀”和“魔女大纹章必掉”,其余两个开关保持关闭。
4. 击败贝可罗尼亚大王(King Pecoronia),在战斗结算中领取饰品。
5. 在菜单或战斗指令界面切换功能;不需要修改或准备读档时点击“全部停用”。

| 功能 | 效果 |
| ------------------ | ----------------------------------------------------------------------------------------- |
| 一击必杀 | 敌人受到实际伤害、生命值减少时归零;出生、治疗、零伤害及未命中保留原规则。 |
| 魔女大纹章必掉 | 每名被击败的贝可罗尼亚大王固定生成 1 件已鉴定的传说魔女大纹章,复用游戏原有固定掉落流程。 |
| 额外随机传说装备 | 每名被击败的敌人额外生成 1 件当前区域的随机传说装备,同时保留原掉落;会增加结算装备数量。 |
| 其他新装备也为传说 | 经过装备生成函数的新装备使用传说稀有度;不会主动修改背包里已有装备。 |

传说(Legendary)是最高稀有度,前缀和具体属性仍由游戏随机生成。两个掉落功能自行生成的装备已使用传说稀有度,不依赖“其他新装备也为传说”。额外随机装备覆盖 56 个区域,无法识别的特殊区域使用传说破旧剑(Worn Sword)作为备用底材。

程序只修改运行中的游戏内存,不替换游戏 EXE,也不直接编辑存档。关闭窗口会尝试恢复原指令;已经获得的装备由游戏负责保存。强制结束修改器后,修改可能仍留在游戏进程中;重启游戏可清除。如果提示“未能完整恢复”,先退出游戏,再关闭修改器。

## 构建和打包

构建使用 Windows 自带的 `%WINDIR%\Microsoft.NET\Framework\v4.0.30319\csc.exe`,无需 Visual Studio 或 NuGet 依赖。打包另需 Windows PowerShell 5.1 或 PowerShell 7,以及 .NET 的 ZIP 支持。

在本项目目录执行:

```powershell
.\source\build.cmd
.\check.cmd
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\package.ps1
```

`build.cmd` 生成 `dist/RefrainTrainer.exe`,编译警告按错误处理。`check.cmd` 重新编译并运行独立进程检查;测试前退出游戏,检查程序发现游戏运行时会拒绝继续。

`package.ps1` 重新构建,通过明确的文件清单打包源码、说明和 EXE,并回读 ZIP,核对全部文件的 SHA-256。产物为:

- `dist/RefrainTrainer-Steam-3256141-zh-CN-v2.zip`
- `dist/RefrainTrainer-Steam-3256141-zh-CN-v2.zip.sha256`

压缩包可直接转发,解压后运行根目录的 `RefrainTrainer.exe`。包内源码仍可按上述步骤重新构建,构建结果写入 `dist/`。

尚未建立公开 Release 或安装渠道。归档项目的 Windows CI 仅支持手动触发,用于按需重现构建、检查和打包,并上传该次运行的 artifact;不在提交或拉取请求时自动运行,不创建版本标签或 GitHub Release。

## 验证命令

无需游戏文件的检查:

```powershell
.\check.cmd
```

结果写入 `test-output/native-test-report.txt` 和 `test-output/trainer-preview.png`。测试只向自己创建的 x86 子进程写入测试指令,并检查开关、恢复和未连接窗口状态。

可选的完整指令模拟需要本地合法安装的匹配版本游戏,以及 [uv](https://docs.astral.sh/uv/) 管理的 Python 3.12、`pefile==2024.8.26`、`unicorn==2.1.4`。它只读安装文件,不连接游戏:

```powershell
$gameDirectory = 'D:\SteamLibrary\steamapps\common\Labyrinth of Refrain Coven of Dusk'
$trainerExe = Join-Path $PWD 'dist\RefrainTrainer.exe'
$fixturesDirectory = Join-Path $PWD 'test-output\fixtures'
$exportArguments = '--export-fixtures "{0}" "{1}"' -f $gameDirectory, $fixturesDirectory
$exportProcess = Start-Process -FilePath $trainerExe -ArgumentList $exportArguments -WindowStyle Hidden -Wait -PassThru
if ($exportProcess.ExitCode -ne 0) { throw 'Fixture export failed.' }
uv run --no-project --python 3.12 --with pefile==2024.8.26 --with unicorn==2.1.4 python .\source\tests\test_patches.py --game-exe "$gameDirectory\refrain.exe" --fixtures $fixturesDirectory --report .\test-output\patch-test-report.json
```

首次运行 uv 可能需要网络下载 Python 和依赖。完整模拟不在公共 CI 中执行,因为它需要未随仓库分发的游戏程序和数据。

## 许可证与归属

本项目目前 **未授予复用许可证(UNLICENSED)**,不继承仓库其他目录的许可证。游戏名称及游戏内容归各自权利人所有,本项目是非官方工具;仓库和分发包均不包含游戏程序、装备数据表或玩家存档。
6 changes: 6 additions & 0 deletions others/game-tools/refrain-trainer/check.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@echo off
setlocal
call "%~dp0source\build.cmd"
if errorlevel 1 exit /b 1
call "%~dp0source\tests\run-native.cmd"
exit /b %errorlevel%
49 changes: 49 additions & 0 deletions others/game-tools/refrain-trainer/docs/validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 中文 v2 验证记录

日期:2026-10-18。对象:Steam App 566540 / Build 3256141 / x86 `refrain.exe`。

## 真实战斗

在 Unclean Ornit / Azu-Cardia 开启“一击必杀”和“魔女大纹章必掉”,其余两个功能关闭。

- King Pecoronia 初始生命值为 101390/101390。
- 一次攻击显示 4359 伤害后出现击败提示,正常结算经验 12200。
- 掉落画面显示 `Obtained Stalwart Witch’s Crest from battle!`。
- 修改器显示一击必杀 1 次、指定掉落 1 次、额外装备 0 件,结算后正常返回探索。

本次没有保存新获得的装备,未执行保存后重新读档,也未逐页核对背包中该件装备的稀有度详情和全部属性。

## 工程检查

- C# x86 编译成功,警告按错误处理。
- 原始游戏 EXE 哈希和四处原始指令匹配。
- 587 个 Unicorn 指令模拟用例全部通过,覆盖生命值边界、56 个区域的装备表、调用约定、栈和寄存器恢复、稀有度及掉落分支。
- 指定掉落在 0 至 99 的 100 个随机输入下均生成一次,物品编号为 212035、稀有度参数为 3(Legendary)、已鉴定标记为 1。
- 非目标、存活敌人、浮点无序值保留原分支,指定掉落和额外随机装备独立工作。
- 独立 x86 测试进程的 490 项断言通过,包含 40 次启用和 40 次停用,检查写入回读、原指令和页权限恢复、线程上下文、不匹配拒绝写入和未连接窗口状态。
- 中文界面已渲染检查,并在实际桌面缩放下检查四个选项、按钮、说明和状态区域。

指令模拟执行真实补丁机器码、原始 HP 更新函数及固定掉落后续指令;分配、随机数和战利品追加等调用使用符合调用约定的测试替身。独立进程测试使用自行创建的程序。两种工程检查与真实战斗分别记录。

## 尚未验证

- 保存后重新读档及装备详细属性。
- 所有特殊战斗和普通敌人掉落的完整实战对照。
- 同场景的战斗结算耗时对比。

本版将指定饰品与额外随机装备分开;关闭额外随机装备可以避免该功能追加物品,但尚无耗时数据证明所有结算卡顿已消除。

## 实现位置

以下位置相对游戏模块基址:

| 功能 | 偏移 |
| ------------------ | ----------- |
| 敌人 HP 更新 | `+0x51ED0` |
| 魔女大纹章固定掉落 | `+0x21FC15` |
| 额外随机装备 | `+0x21FE8F` |
| 新装备稀有度 | `+0x8EC12` |

固定掉落识别原物品编号 212035,仅在目标死亡后覆盖判定,复用原生生成路径。切换功能时暂停游戏线程并检查指令位置,写入后回读并恢复页权限;已经暴露给游戏的补丁内存保留至游戏退出。

复现命令、环境要求和 CI 范围见 [项目 README](../README.md)。测试输出只在本地 `test-output/` 或 CI artifact 中保留,不作为源码提交。
91 changes: 91 additions & 0 deletions others/game-tools/refrain-trainer/package.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

& (Join-Path $PSScriptRoot 'source\build.cmd')
if ($LASTEXITCODE -ne 0) { throw 'Trainer build failed.' }

Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
$dist = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot 'dist'))
$packageName = 'RefrainTrainer-zh-CN-v2'
$archiveName = 'RefrainTrainer-Steam-3256141-zh-CN-v2.zip'
$stage = [IO.Path]::GetFullPath((Join-Path $dist ('package-' + [Guid]::NewGuid().ToString('N'))))
if (-not $stage.StartsWith($dist + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) {
throw 'Package staging directory must remain inside dist.'
}
$packageRoot = Join-Path $stage $packageName
$temporaryArchive = Join-Path $stage $archiveName
$archivePath = Join-Path $dist $archiveName
$utf8 = New-Object Text.UTF8Encoding($false)
$sources = @(
'README.md',
'docs/validation.md',
'check.cmd',
'package.ps1',
'source/build.cmd',
'source/Program.cs',
'source/Trainer.cs',
'source/Patches.cs',
'source/tests/NativeChecks.cs',
'source/tests/run-native.cmd',
'source/tests/test_patches.py'
)

try {
New-Item -ItemType Directory -Path $packageRoot -Force | Out-Null
foreach ($relativePath in $sources) {
$target = Join-Path $packageRoot $relativePath
New-Item -ItemType Directory -Path (Split-Path -Parent $target) -Force | Out-Null
Copy-Item -LiteralPath (Join-Path $PSScriptRoot $relativePath) -Destination $target
}
Copy-Item -LiteralPath (Join-Path $dist 'RefrainTrainer.exe') -Destination $packageRoot

$entries = @(
foreach ($relativePath in (($sources + 'RefrainTrainer.exe') | Sort-Object)) {
$file = Get-Item -LiteralPath (Join-Path $packageRoot $relativePath)
[ordered]@{
path = $relativePath
bytes = $file.Length
sha256 = (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash
}
}
)
$manifest = [ordered]@{ release = $packageName; steamBuild = 3256141; files = $entries }
$manifestJson = ($manifest | ConvertTo-Json -Depth 5) + "`n"
[IO.File]::WriteAllText((Join-Path $packageRoot 'SHA256.json'), $manifestJson, $utf8)
[IO.Compression.ZipFile]::CreateFromDirectory($packageRoot, $temporaryArchive, [IO.Compression.CompressionLevel]::Optimal, $true)

$zip = [IO.Compression.ZipFile]::OpenRead($temporaryArchive)
try {
$files = @($zip.Entries | Where-Object { $_.Name.Length -gt 0 })
$expectedNames = @($entries | ForEach-Object { $packageName + '/' + $_.path }) + ($packageName + '/SHA256.json')
$actualNames = @($files | ForEach-Object { $_.FullName.Replace('\', '/') })
if ($files.Count -ne $expectedNames.Count -or (Compare-Object $actualNames $expectedNames)) {
throw 'ZIP file list does not match the package allowlist.'
}
foreach ($file in $files) {
$relativePath = $file.FullName.Replace('\', '/').Substring($packageName.Length + 1)
$expectedHash = (Get-FileHash -LiteralPath (Join-Path $packageRoot $relativePath) -Algorithm SHA256).Hash
$stream = $file.Open()
$hasher = [Security.Cryptography.SHA256]::Create()
try {
$actualHash = [BitConverter]::ToString($hasher.ComputeHash($stream)).Replace('-', '')
if ($actualHash -ne $expectedHash) { throw "ZIP content mismatch: $relativePath" }
}
finally {
$hasher.Dispose()
$stream.Dispose()
}
}
}
finally { $zip.Dispose() }

Move-Item -LiteralPath $temporaryArchive -Destination $archivePath -Force
$archiveHash = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash
[IO.File]::WriteAllText(($archivePath + '.sha256'), "$archiveHash $archiveName`n", $utf8)
Write-Output "Packaged and verified $($entries.Count + 1) files: $archivePath"
Write-Output "SHA-256: $archiveHash"
}
finally {
if (Test-Path -LiteralPath $stage) { Remove-Item -LiteralPath $stage -Recurse -Force }
}
Loading
Loading