Files
RustNotMC/game-fork/scripts/prepare-game.ps1
T
sasheg dev bdf0ae866a Форк клиента собирается из исходников, модзагрузчик встроен в игру
Проект начинается с двух вещей: клиент, который мы действительно собрали, и
загрузчик, который является его частью, а не чем-то рядом.

Форк
- prepare-game.ps1 пересобирает клиент целиком: ванильный jar, SRG и MCP,
  SpecialSource, FernFlower, патчи, автофиксеры, javac. Идемпотентен.
- Свой код игры лежит в src-overlay, потому что src пересоздаётся при каждой
  сборке и всё написанное вручную там погибает.
- 116 патчей в patches/decompiler-fixes.txt. Каждый сверялся с оригинальным
  байткодом через javap: компиляция не ловит ошибки декомпиляции.
- struct_diff.py сравнивает исходный байткод с нашей сборкой по классам,
  интерфейсам, суперклассам, методам и полям, и отдельно проверяет, что
  пропавшие классы действительно никому не нужны.

Загрузчик
- Собирается и тестируется без игры: build.ps1 не использует classpath
  вообще, поэтому SelfTest из 128 проверок проходит без запущенного форка.
- Клиентский API не содержит ни одного игрового типа, реализацию даёт игра.
- Политика: мод меняет то, что клиент показывает, но никогда то, что истинно
  в мире. Мир, предметы, игроки и сеть принадлежат серверу.
- Capability приведены в соответствие с этой политикой: осталось шесть, и
  за каждым стоит реальная проверка. Прежние WORLD, PLAYER, NETWORK, TEXT,
  INPUT и COMMAND выдавались, но ничего не проверяли.
- Команды модов имеют область CLIENT, SERVER или BOTH. Серверные не
  выполняются на клиенте никогда.
- Свой jar загрузчика лежит в classpath игры и сборки форка.

Инструменты
- add_patch.py, чтобы каждое исправление переживало передекомпиляцию
- find_self_comparisons.py, find_remove_index_bugs.py, find_callers.py
- stage_run.py и fetch_missing.py для запуска клиента
2026-10-18 21:31:27 +03:00

173 lines
8.2 KiB
PowerShell

# Builds the forked client from the verified vanilla jar.
#
# vanilla client -> MCP names -> source -> patches -> classes
#
# ASCII only: PowerShell 5.1 reads a .ps1 without a byte order mark using the
# system ANSI code page, which mangles UTF-8 and breaks the parser.
#
# Everything here is repeatable. Run it any time; nothing is edited by hand
# except game-fork\patches\decompiler-fixes.txt, which is re-applied at the end.
$ErrorActionPreference = 'Stop'
$Fork = Split-Path -Parent $PSScriptRoot
if (-not $Fork) { $Fork = $PSCommandPath | Split-Path -Parent | Split-Path -Parent }
$Root = Split-Path -Parent $Fork
$Client = Join-Path $Root 'rsx-client'
$VanillaJar = 'G:\rsx-map\vanilla\client-1.12.2.jar'
$Astral = Join-Path $env:APPDATA 'AstralRinthApp\meta'
$Fernflower = 'G:\Paper-Dev\work\BuildData\bin\fernflower.jar'
$SpecialSource = 'G:\Paper-Dev\work\BuildData\bin\SpecialSource.jar'
$McpCs = 'G:\rsx-map\vanilla\mcp-stable'
$Maps = Join-Path $Fork 'mappings'
$Classes = Join-Path $Fork 'classes'
$Src = Join-Path $Fork 'src'
$Out = Join-Path $Fork 'build\classes'
$Temp = Join-Path $env:TEMP 'opencode'
function Say($text) { Write-Host " $text" -ForegroundColor Cyan }
function Need($path, $what) { if (-not (Test-Path -LiteralPath $path)) { throw "missing $what at $path" } }
New-Item -ItemType Directory -Force -Path $Maps, $Temp | Out-Null
Need $VanillaJar 'the verified vanilla client jar'
Need $Fernflower 'fernflower'
Need $SpecialSource 'SpecialSource'
Need $McpCs 'the MCP name CSVs'
# 1. vanilla SRG
$srgZip = Join-Path $Maps 'mcp-1.12.2-srg.zip'
$srgDir = Join-Path $Maps 'mcp-srg-vanilla'
if (-not (Test-Path (Join-Path $srgDir 'joined.srg'))) {
Say 'downloading the vanilla 1.12.2 SRG'
Invoke-WebRequest -Uri 'https://maven.minecraftforge.net/de/oceanlabs/mcp/mcp/1.12.2/mcp-1.12.2-srg.zip' `
-OutFile $srgZip -TimeoutSec 180 -UseBasicParsing
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory($srgZip, $srgDir)
}
# 2. merge SRG with the MCP CSVs into a single obf -> MCP SRG
$srg = Join-Path $Maps 'obf-to-mcp.srg'
Say 'merging SRG with the MCP CSVs'
& python (Join-Path $Client 'tools\merge_srg_mcp.py') `
(Join-Path $srgDir 'joined.srg') $McpCs $srg
if ($LASTEXITCODE -ne 0) { throw 'srg merge failed' }
# 3. remap the vanilla client
$named = Join-Path $Maps 'client-mcp-named.jar'
Say 'remapping the vanilla client to MCP names'
if (Test-Path $named) { Remove-Item $named -Force }
& java -Xmx4g -jar $SpecialSource -m $srg -i $VanillaJar -o $named
if (-not (Test-Path $named)) { throw 'remap produced nothing' }
Say " client-mcp-named.jar ($((Get-Item $named).Length) bytes)"
# 4. unpack for the decompiler
Remove-Item $Classes -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $Classes | Out-Null
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory($named, $Classes)
Say " unpacked $((Get-ChildItem $Classes -Recurse -Filter '*.class' -File).Count) classes"
# 5. decompile
# -dgs=1 decode generic signatures
# -rbr=1 drop bridge methods (otherwise every generic method appears twice
# and the compiler rejects the erasure clash)
# -rsy=1 drop synthetic members
Remove-Item $Src -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $Src | Out-Null
Say 'decompiling (this is the slow part)'
$log = Join-Path $Temp 'fernflower.log'
& cmd /c "java -Xmx4g -jar `"$Fernflower`" -dgs=1 -rbr=1 -rsy=1 `"$Classes`" `"$Src`" > `"$log`" 2>&1"
Say " $((Get-ChildItem $Src -Recurse -Filter '*.java' -File).Count) source files"
# 5b. copy in our own game-side code
#
# Everything above this line is regenerated from the vanilla jar and is wiped on
# every run. Our own classes under net/rustnotmc are not generated and must live
# outside src, or a rebuild deletes them and leaves the patches that call them
# pointing at nothing. They go in before the patches so a hand fix may edit them
# too.
$Overlay = Join-Path $Fork 'src-overlay'
if (Test-Path $Overlay) {
$copied = 0
foreach ($f in (Get-ChildItem $Overlay -Recurse -Filter '*.java' -File)) {
$rel = $f.FullName.Substring($Overlay.Length).TrimStart('\')
$target = Join-Path $Src $rel
$dir = Split-Path -Parent $target
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
Copy-Item -LiteralPath $f.FullName -Destination $target -Force
$copied++
}
Say " overlay: $copied source file(s) from src-overlay"
}
# 6. re-apply the hand fixes
Say 'applying hand fixes'
& python (Join-Path $Client 'tools\apply_patches.py') $Src (Join-Path $Fork 'patches\decompiler-fixes.txt')
if ($LASTEXITCODE -ne 0) { Write-Host ' WARNING: some patches did not apply' -ForegroundColor Yellow }
# 6b. the systematic rewrites, which would be hundreds of patch entries one by one
Say 'applying systematic rules'
& python (Join-Path $Client 'tools\fix_decompiler_rules.py') $Src
# 7. compile, let the automated fixers converge, compile again.
#
# The fixers need javac's diagnostics as their input, and fixing one error can
# reveal the next, so this runs to a fixed point rather than once. It stops as
# soon as a round fails to reduce the count, which is the signal that the
# remaining errors need a human.
$argf = Join-Path $Temp 'fork-build.txt'
$errf = Join-Path $Temp 'fork-build-errors.txt'
$good = New-Object System.Collections.Generic.List[string]
foreach ($j in (Get-ChildItem (Join-Path $Astral 'libraries') -Recurse -Filter '*.jar' -File)) {
if ($j.Length -le 100) { continue }
$ok = $false
try { $z = [System.IO.Compression.ZipFile]::OpenRead($j.FullName); $z.Dispose(); $ok = $true } catch { $ok = $false }
if ($ok) { $good.Add(($j.FullName -replace '\\', '/')) }
}
# The fork calls into the mod loader, so the loader has to be visible to javac
# exactly as it is at runtime.
$Loader = Join-Path $Client 'dist\rsx-loader.jar'
$cp = @($good + $(if (Test-Path $Loader) { $Loader -replace '\\', '/' } else { $null })) -join ';'
$sb = New-Object System.Text.StringBuilder
foreach ($a in @('-nowarn', '-proc:none', '-encoding', 'UTF-8', '--release', '17', '-d', ($Out -replace '\\', '/'))) { [void]$sb.AppendLine($a) }
[void]$sb.AppendLine('-cp'); [void]$sb.AppendLine('"' + $cp + '"')
Get-ChildItem $Src -Recurse -Filter '*.java' | ForEach-Object { [void]$sb.AppendLine(($_.FullName -replace '\\', '/')) }
Set-Content -LiteralPath $argf -Value $sb.ToString() -Encoding ASCII
$javac = (Get-Command javac).Source
function Invoke-Compile {
Remove-Item $Out -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $Out | Out-Null
& cmd /c "`"$javac`" @`"$argf`" -Xmaxerrs 10000 > `"$errf`" 2>&1"
return (Get-Content -LiteralPath $errf | Select-String -Pattern ': error:').Count
}
$count = Invoke-Compile
Say "compiled, $count errors"
$previous = [int]::MaxValue
for ($round = 1; $round -le 8 -and $count -gt 0; $round++) {
if ($count -ge $previous) { Say "no further progress, stopping"; break }
$previous = $count
Say "fix round $round"
& python (Join-Path $Client 'tools\fix_stale_locals.py') $errf --apply | Out-Null
& python (Join-Path $Client 'tools\fix_generic_casts.py') $errf --apply | Out-Null
& python (Join-Path $Client 'tools\fix_decompiler_rules.py') $Src | Out-Null
$count = Invoke-Compile
Say " -> $count errors"
}
$errors = Get-Content -LiteralPath $errf | Select-String -Pattern ': error:'
$files = $errors | ForEach-Object { if ($_.Line -match '^([A-Z]:.*?\.java):\d+:') { $matches[1] } } | Sort-Object -Unique
Write-Host ''
if ($errors.Count -eq 0) {
Write-Host ' FORK COMPILES CLEAN' -ForegroundColor Green
Write-Host " $((Get-ChildItem $Out -Recurse -Filter '*.class' -File).Count) classes"
} else {
Write-Host " $((Get-ChildItem $Out -Recurse -Filter '*.class' -File).Count) classes, $($errors.Count) errors in $($files.Count) files" -ForegroundColor Yellow
$errors | ForEach-Object { if ($_.Line -match '^([A-Z]:.*?\.java):\d+:') { $matches[1] } } |
Group-Object | Sort-Object Count -Descending | Select-Object -First 15 |
ForEach-Object { Write-Host (" {0,4} {1}" -f $_.Count, ($_.Name.Replace(($Src + '\'), ''))) -ForegroundColor Gray }
}