This commit is contained in:
2026-02-12 09:02:55 -05:00
parent c01fe187d0
commit d4114eb6c6
13 changed files with 1249 additions and 1800 deletions

View File

@@ -0,0 +1,104 @@
param(
[string]$GameRoot = "D:\SteamLibrary\steamapps\common\ProjectZomboid"
)
$ErrorActionPreference = "Stop"
function Get-Sha256([string]$Path) {
if (-not (Test-Path $Path)) { return $null }
return (Get-FileHash $Path -Algorithm SHA256).Hash.ToLowerInvariant()
}
function Get-JarEntrySha256([string]$JarPath, [string]$EntryName) {
if (-not (Test-Path $JarPath)) { return $null }
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip = [System.IO.Compression.ZipFile]::OpenRead($JarPath)
try {
$entry = $zip.GetEntry($EntryName)
if ($null -eq $entry) { return $null }
$stream = $entry.Open()
$memory = New-Object System.IO.MemoryStream
try {
$stream.CopyTo($memory)
} finally {
$stream.Close()
}
$bytes = $memory.ToArray()
$sha = [System.Security.Cryptography.SHA256]::Create()
return (($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString("x2") }) -join "")
} finally {
$zip.Dispose()
}
}
$repoRoot = Split-Path -Parent $PSScriptRoot
$knownPatchedClass = Join-Path $repoRoot "zombie\vehicles\BaseVehicle.class"
$patchedHash = Get-Sha256 $knownPatchedClass
$patchedHashText = if ($null -eq $patchedHash) { "missing" } else { $patchedHash }
$knownHelperClass = Join-Path $repoRoot "zombie\vehicles\LandtrainConstraintAuthHelper.class"
$helperHash = Get-Sha256 $knownHelperClass
$helperHashText = if ($null -eq $helperHash) { "missing" } else { $helperHash }
$targets = @(
@{
Name = "Client override path"
Path = Join-Path $GameRoot "zombie\vehicles\BaseVehicle.class"
},
@{
Name = "Dedicated override path"
Path = Join-Path $GameRoot "java\zombie\vehicles\BaseVehicle.class"
}
)
$jarCandidates = @(
(Join-Path $GameRoot "projectzomboid.jar"),
(Join-Path $GameRoot "java\projectzomboid.jar")
)
Write-Output "GameRoot: $GameRoot"
Write-Output "Known patched hash (repo): $patchedHashText"
Write-Output "Known helper hash (repo): $helperHashText"
Write-Output ""
foreach ($target in $targets) {
$path = $target.Path
$hash = Get-Sha256 $path
if ($null -eq $hash) {
Write-Output "$($target.Name): MISSING ($path)"
continue
}
$status = if ($patchedHash -and $hash -eq $patchedHash) { "PATCHED" } else { "NOT_MATCHING_PATCHED_HASH" }
Write-Output "$($target.Name): $status"
Write-Output " path: $path"
Write-Output " hash: $hash"
}
Write-Output ""
foreach ($helperPath in @(
(Join-Path $GameRoot "zombie\vehicles\LandtrainConstraintAuthHelper.class"),
(Join-Path $GameRoot "java\zombie\vehicles\LandtrainConstraintAuthHelper.class")
) | Select-Object -Unique) {
$hash = Get-Sha256 $helperPath
if ($null -eq $hash) {
Write-Output "Helper class: MISSING ($helperPath)"
continue
}
$status = if ($helperHash -and $hash -eq $helperHash) { "PATCHED" } else { "NOT_MATCHING_HELPER_HASH" }
Write-Output "Helper class: $status"
Write-Output " path: $helperPath"
Write-Output " hash: $hash"
}
Write-Output ""
foreach ($jar in $jarCandidates | Select-Object -Unique) {
$jarHash = Get-JarEntrySha256 $jar "zombie/vehicles/BaseVehicle.class"
if ($null -eq $jarHash) {
Write-Output "Jar class: MISSING ($jar)"
continue
}
Write-Output "Jar class hash:"
Write-Output " jar: $jar"
Write-Output " hash: $jarHash"
}

View File

@@ -3,27 +3,34 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.Opcodes;
/**
* Patches zombie.vehicles.BaseVehicle so addPointConstraint() no longer force-breaks
* both vehicles before creating a new constraint.
* Patches zombie.vehicles.BaseVehicle for Landtrain chain support:
* 1) remove forced breakConstraint() in addPointConstraint()
* 2) route constraintChanged() driver lookups through helper that handles chain middle vehicles
*/
public final class BaseVehicleConstraintPatch {
private static final String TARGET_NAME = "addPointConstraint";
private static final String CONSTRAINT_CHANGED_NAME = "constraintChanged";
private static final String CLINIT_NAME = "<clinit>";
private static final String VOID_NOARG_DESC = "()V";
private static final String PATCH_LOG_LINE = "[Landtrain][BaseVehiclePatch] BaseVehicle override enabled";
private static final String BREAK_DESC_OBJECT_BOOL = "(ZLjava/lang/Boolean;)V";
private static final String BREAK_DESC_PRIMITIVE_BOOL = "(ZZ)V";
private static final String BASE_VEHICLE_OWNER = "zombie/vehicles/BaseVehicle";
private static final String GET_DRIVER_DESC = "()Lzombie/characters/IsoGameCharacter;";
private static final String HELPER_OWNER = "zombie/vehicles/LandtrainConstraintAuthHelper";
private static final String HELPER_METHOD = "resolveConstraintDriver";
private static final String HELPER_DESC =
"(Lzombie/vehicles/BaseVehicle;)Lzombie/characters/IsoGameCharacter;";
private BaseVehicleConstraintPatch() {
}
@@ -43,12 +50,16 @@ public final class BaseVehicleConstraintPatch {
int removedCalls = 0;
int inspectedAddPointMethods = 0;
int patchedConstraintDriverCalls = 0;
for (MethodNode method : classNode.methods) {
if (!TARGET_NAME.equals(method.name) || !isTargetAddPointConstraint(method.desc)) {
continue;
if (TARGET_NAME.equals(method.name) && isTargetAddPointConstraint(method.desc)) {
inspectedAddPointMethods++;
removedCalls += patchAddPointConstraint(method);
} else if (CONSTRAINT_CHANGED_NAME.equals(method.name)
&& VOID_NOARG_DESC.equals(method.desc)) {
patchedConstraintDriverCalls += patchConstraintChangedDriverCalls(method);
}
inspectedAddPointMethods++;
removedCalls += patchAddPointConstraint(method);
}
if (removedCalls < 2) {
@@ -59,6 +70,11 @@ public final class BaseVehicleConstraintPatch {
+ inspectedAddPointMethods
+ ")");
}
if (patchedConstraintDriverCalls < 1) {
throw new IllegalStateException(
"Expected to patch at least 1 constraintChanged getDriver call, patched "
+ patchedConstraintDriverCalls);
}
if (!ensureClassInitLog(classNode)) {
throw new IllegalStateException("Failed to inject BaseVehicle class-init debug log");
}
@@ -71,12 +87,12 @@ public final class BaseVehicleConstraintPatch {
System.out.println(
"Patched BaseVehicle.class; removed breakConstraint calls: "
+ removedCalls
+ ", constraint driver hooks: "
+ patchedConstraintDriverCalls
+ ", class-init debug log: enabled");
}
private static boolean isTargetAddPointConstraint(String methodDesc) {
// We only want the 5-arg overload:
// (IsoPlayer, BaseVehicle, String, String, boolean|Boolean) -> void
return "(Lzombie/characters/IsoPlayer;Lzombie/vehicles/BaseVehicle;Ljava/lang/String;Ljava/lang/String;Z)V"
.equals(methodDesc)
|| "(Lzombie/characters/IsoPlayer;Lzombie/vehicles/BaseVehicle;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)V"
@@ -97,9 +113,8 @@ public final class BaseVehicleConstraintPatch {
node = next;
continue;
}
// Keep stack-map frames valid by preserving stack effect:
// breakConstraint(...) consumes objectref + 2 args and returns void.
// Replace invoke with POP2 + POP (consume 3 category-1 stack slots).
InsnList replacement = new InsnList();
replacement.add(new InsnNode(Opcodes.POP2));
replacement.add(new InsnNode(Opcodes.POP));
@@ -113,6 +128,36 @@ public final class BaseVehicleConstraintPatch {
return patched;
}
private static int patchConstraintChangedDriverCalls(MethodNode method) {
int patched = 0;
InsnList insns = method.instructions;
for (AbstractInsnNode node = insns.getFirst(); node != null; ) {
AbstractInsnNode next = node.getNext();
if (!(node instanceof MethodInsnNode call)) {
node = next;
continue;
}
if (!BASE_VEHICLE_OWNER.equals(call.owner)
|| !"getDriver".equals(call.name)
|| !GET_DRIVER_DESC.equals(call.desc)) {
node = next;
continue;
}
MethodInsnNode replacement =
new MethodInsnNode(
Opcodes.INVOKESTATIC,
HELPER_OWNER,
HELPER_METHOD,
HELPER_DESC,
false);
insns.set(call, replacement);
patched++;
node = next;
}
return patched;
}
private static boolean ensureClassInitLog(ClassNode classNode) {
MethodNode clinit = null;
for (MethodNode method : classNode.methods) {

View File

@@ -0,0 +1,38 @@
package zombie.vehicles;
import zombie.characters.IsoGameCharacter;
/**
* Resolves the effective driver for constraint auth in chained towing.
* For middle vehicles in a chain, prefer the front/lead driver's authority.
*/
public final class LandtrainConstraintAuthHelper {
private LandtrainConstraintAuthHelper() {
}
public static IsoGameCharacter resolveConstraintDriver(BaseVehicle vehicle) {
if (vehicle == null) {
return null;
}
IsoGameCharacter driver = vehicle.getDriver();
if (driver != null) {
return driver;
}
BaseVehicle front = vehicle.getVehicleTowedBy();
if (front != null) {
driver = front.getDriver();
if (driver != null) {
return driver;
}
}
BaseVehicle rear = vehicle.getVehicleTowing();
if (rear != null) {
return rear.getDriver();
}
return null;
}
}

View File

@@ -14,9 +14,27 @@ New-Item -ItemType Directory -Force -Path $classPatchDir | Out-Null
New-Item -ItemType Directory -Force -Path $buildDir | Out-Null
$javaExe = Join-Path $GameRoot "jre64\bin\java.exe"
$gameJar = Join-Path $GameRoot "projectzomboid.jar"
if (-not (Test-Path $javaExe)) { throw "java.exe not found at $javaExe" }
if (-not (Test-Path $gameJar)) { throw "projectzomboid.jar not found at $gameJar" }
$clientJar = Join-Path $GameRoot "projectzomboid.jar"
$dedicatedJar = Join-Path $GameRoot "java\projectzomboid.jar"
$gameJar = $null
$targetClasses = @()
if (Test-Path $clientJar) {
$gameJar = $clientJar
$targetClasses += (Join-Path $GameRoot "zombie\vehicles\BaseVehicle.class")
}
if (Test-Path $dedicatedJar) {
$gameJar = $dedicatedJar
$targetClasses += (Join-Path $GameRoot "java\zombie\vehicles\BaseVehicle.class")
}
if ($null -eq $gameJar) {
throw "projectzomboid.jar not found at either $clientJar or $dedicatedJar"
}
if ($targetClasses.Count -eq 0) {
throw "No valid BaseVehicle.class deployment targets found under $GameRoot"
}
$ecjJar = Join-Path $toolsDir "ecj.jar"
$asmJar = Join-Path $toolsDir "asm.jar"
@@ -34,12 +52,15 @@ if (-not (Test-Path $asmTreeJar)) {
$patcherSource = Join-Path $PSScriptRoot "java\BaseVehicleConstraintPatch.java"
if (-not (Test-Path $patcherSource)) { throw "Missing patcher source: $patcherSource" }
$helperSource = Join-Path $PSScriptRoot "java\LandtrainConstraintAuthHelper.java"
if (-not (Test-Path $helperSource)) { throw "Missing helper source: $helperSource" }
& $javaExe -jar $ecjJar -17 -cp "$asmJar;$asmTreeJar" -d $buildDir $patcherSource
if ($LASTEXITCODE -ne 0) { throw "Failed to compile BaseVehicleConstraintPatch.java" }
& $javaExe -jar $ecjJar -17 -cp "$asmJar;$asmTreeJar;$gameJar" -d $buildDir $patcherSource $helperSource
if ($LASTEXITCODE -ne 0) { throw "Failed to compile BaseVehicleConstraintPatch.java/LandtrainConstraintAuthHelper.java" }
$inputClass = Join-Path $classPatchDir "BaseVehicle.original.class"
$patchedClass = Join-Path $classPatchDir "BaseVehicle.patched.class"
$helperClass = Join-Path $buildDir "zombie\vehicles\LandtrainConstraintAuthHelper.class"
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip = [System.IO.Compression.ZipFile]::OpenRead($gameJar)
@@ -61,26 +82,33 @@ try {
& $javaExe -cp "$buildDir;$asmJar;$asmTreeJar" BaseVehicleConstraintPatch $inputClass $patchedClass
if ($LASTEXITCODE -ne 0) { throw "BaseVehicle class patch failed" }
if (-not (Test-Path $helperClass)) { throw "Missing compiled helper class: $helperClass" }
$targetDir = Join-Path $GameRoot "zombie\vehicles"
$targetClass = Join-Path $targetDir "BaseVehicle.class"
$backupClass = "$targetClass.landtrain.original"
foreach ($targetClass in $targetClasses | Select-Object -Unique) {
$targetDir = Split-Path -Parent $targetClass
$backupClass = "$targetClass.landtrain.original"
$targetHelperClass = Join-Path $targetDir "LandtrainConstraintAuthHelper.class"
New-Item -ItemType Directory -Force -Path $targetDir | Out-Null
if (-not (Test-Path $backupClass)) {
if (Test-Path $targetClass) {
Copy-Item $targetClass $backupClass -Force
} else {
Copy-Item $inputClass $backupClass -Force
New-Item -ItemType Directory -Force -Path $targetDir | Out-Null
if (-not (Test-Path $backupClass)) {
if (Test-Path $targetClass) {
Copy-Item $targetClass $backupClass -Force
} else {
Copy-Item $inputClass $backupClass -Force
}
}
Copy-Item $patchedClass $targetClass -Force
Copy-Item $helperClass $targetHelperClass -Force
Write-Output "Patched BaseVehicle.class deployed to $targetClass"
Write-Output "Deployed LandtrainConstraintAuthHelper.class to $targetHelperClass"
Write-Output "Backup stored at $backupClass"
}
Copy-Item $patchedClass $targetClass -Force
Write-Output "Patched BaseVehicle.class deployed to $targetClass"
Write-Output "Backup stored at $backupClass"
$distDir = Join-Path $repoRoot "zombie\vehicles"
$distClass = Join-Path $distDir "BaseVehicle.class"
$distHelperClass = Join-Path $distDir "LandtrainConstraintAuthHelper.class"
New-Item -ItemType Directory -Force -Path $distDir | Out-Null
Copy-Item $patchedClass $distClass -Force
Copy-Item $helperClass $distHelperClass -Force
Write-Output "Distribution class updated at $distClass"
Write-Output "Distribution helper class updated at $distHelperClass"

View File

@@ -4,15 +4,37 @@ param(
$ErrorActionPreference = "Stop"
$targetClass = Join-Path $GameRoot "zombie\vehicles\BaseVehicle.class"
$backupClass = "$targetClass.landtrain.original"
$targets = @(
(Join-Path $GameRoot "zombie\vehicles\BaseVehicle.class"),
(Join-Path $GameRoot "java\zombie\vehicles\BaseVehicle.class")
)
$helperTargets = @(
(Join-Path $GameRoot "zombie\vehicles\LandtrainConstraintAuthHelper.class"),
(Join-Path $GameRoot "java\zombie\vehicles\LandtrainConstraintAuthHelper.class")
)
if (Test-Path $backupClass) {
Copy-Item $backupClass $targetClass -Force
Write-Output "Restored BaseVehicle.class from $backupClass"
} elseif (Test-Path $targetClass) {
Remove-Item $targetClass -Force
Write-Output "Removed override class at $targetClass (game will use class from projectzomboid.jar)"
} else {
Write-Output "No override or backup found. Nothing to restore."
$handled = $false
foreach ($targetClass in $targets | Select-Object -Unique) {
$backupClass = "$targetClass.landtrain.original"
if (Test-Path $backupClass) {
Copy-Item $backupClass $targetClass -Force
Write-Output "Restored BaseVehicle.class from $backupClass"
$handled = $true
} elseif (Test-Path $targetClass) {
Remove-Item $targetClass -Force
Write-Output "Removed override class at $targetClass (game will use class from projectzomboid.jar)"
$handled = $true
}
}
foreach ($helperClass in $helperTargets | Select-Object -Unique) {
if (Test-Path $helperClass) {
Remove-Item $helperClass -Force
Write-Output "Removed helper override class at $helperClass"
$handled = $true
}
}
if (-not $handled) {
Write-Output "No override or backup found in known client/dedicated paths. Nothing to restore."
}