Intial Commit
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
# Local build and verification intermediates
|
||||||
|
work/
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Private signing keys never belong in the repository
|
||||||
|
**/ed25519-private.der
|
||||||
|
**/signing/
|
||||||
Binary file not shown.
@@ -0,0 +1,3 @@
|
|||||||
|
ZBS
|
||||||
|
SteamID64:76561198079674652
|
||||||
|
Signature:4462a248868cd920ba2a75dc609af2fc0b73ec170442a3c798bd3f67d173a639e52cb857b8ad009ae728d0d855605bec0d1048ec56205a3b2733497ab0e1fb02
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
-- Replace only MTK's tracer UI callback. Its physics, hits, ammunition and
|
||||||
|
-- VisualProjectilePC lifecycle stay owned by the original mod.
|
||||||
|
local legacy, replacement
|
||||||
|
local function install()
|
||||||
|
local bridge = MTKViewpointProjectiles
|
||||||
|
if not bridge or not VisualProjectilePC then return end
|
||||||
|
if not bridge.bind(VisualProjectilePC.removeByID, VisualProjectilePC.list) then
|
||||||
|
print("[MTKViewpointTurrets] MTK projectile API differs; original drawing retained.")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if replacement then return end
|
||||||
|
legacy = bridge.legacyRenderer()
|
||||||
|
if not legacy then
|
||||||
|
print("[MTKViewpointTurrets] MTK tracer callback not identified; original drawing retained.")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
replacement = function()
|
||||||
|
if not MTKViewpointCamera or not MTKViewpointCamera.enabled() then return legacy() end
|
||||||
|
end
|
||||||
|
Events.OnPreUIDraw.Remove(legacy)
|
||||||
|
Events.OnPreUIDraw.Add(replacement)
|
||||||
|
print("[MTKViewpointTurrets] 3D projectile renderer registered; isometric tracers retained in iso view.")
|
||||||
|
end
|
||||||
|
Events.OnGameBoot.Add(install)
|
||||||
|
Events.OnGameStart.Add(install)
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
require "Vehicles/Papa_Chad_Tank_Cannon"
|
||||||
|
require "Vehicles/Papa_Chad_Tank_Aim"
|
||||||
|
|
||||||
|
MTKViewpointTurrets = MTKViewpointTurrets or {}
|
||||||
|
local M = MTKViewpointTurrets
|
||||||
|
local seats = { [0] = "SeatFrontLeft", [1] = "SeatGunner", [2] = "SeatFrontRight", [3] = "SeatRearRight" }
|
||||||
|
local scope = nil
|
||||||
|
|
||||||
|
local function enabled()
|
||||||
|
return MTKViewpointCamera and MTKViewpointCamera.enabled()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function allowed(player, vehicle, weapon)
|
||||||
|
return player == getPlayer() and not player:isDead()
|
||||||
|
and MTKViewpointCamera.captured() and getGameSpeed() > 0
|
||||||
|
and not (UIManager and UIManager.isMouseOverInventory and UIManager.isMouseOverInventory())
|
||||||
|
and seats[vehicle:getSeat(player)] == weapon.requiredSeat
|
||||||
|
and player:getCharacterActions():isEmpty()
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.install()
|
||||||
|
-- These local adapters register their own wrappers at boot/start. Install
|
||||||
|
-- them first, then wrap the completed chain, regardless of event order.
|
||||||
|
if KFTFerret and KFTFerret.installClient then KFTFerret.installClient() end
|
||||||
|
if KLTLAV and KLTLAV.installClient then KLTLAV.installClient() end
|
||||||
|
if not tankAimSystemPC or not chooseWeaponPC or not ISCoordConversion then return end
|
||||||
|
|
||||||
|
if ISCoordConversion.ToWorld ~= M.toWorld then
|
||||||
|
local previousToWorld = ISCoordConversion.ToWorld
|
||||||
|
M.toWorld = function(x, y, z)
|
||||||
|
-- Only override conversions inside this turret update. The Ferret
|
||||||
|
-- and LAV wrappers otherwise overwrite our camera heading with a
|
||||||
|
-- floor pick (or the screen-centre isometric fallback).
|
||||||
|
if scope and scope.x then return scope.x, scope.y end
|
||||||
|
return previousToWorld(x, y, z)
|
||||||
|
end
|
||||||
|
ISCoordConversion.ToWorld = M.toWorld
|
||||||
|
end
|
||||||
|
|
||||||
|
if chooseWeaponPC ~= M.choose then
|
||||||
|
local previousChoose = chooseWeaponPC
|
||||||
|
M.choose = function(vehicle)
|
||||||
|
-- MTK still advances existing projectiles when weapon input is
|
||||||
|
-- blocked; no simulation callback is removed or duplicated.
|
||||||
|
if scope and scope.blocked then return nil end
|
||||||
|
return previousChoose(vehicle)
|
||||||
|
end
|
||||||
|
chooseWeaponPC = M.choose
|
||||||
|
end
|
||||||
|
|
||||||
|
if tankAimSystemPC ~= M.aim then
|
||||||
|
local previousAim = tankAimSystemPC
|
||||||
|
Events.OnPlayerUpdate.Remove(previousAim)
|
||||||
|
M.aim = function(player)
|
||||||
|
local vehicle = player and player:getVehicle()
|
||||||
|
if not enabled() or not vehicle then return previousAim(player) end
|
||||||
|
local weapon = chooseWeaponPC(vehicle)
|
||||||
|
if not weapon then return previousAim(player) end
|
||||||
|
|
||||||
|
local oldScope = scope
|
||||||
|
scope = { blocked = not allowed(player, vehicle, weapon) }
|
||||||
|
local oldDirection = nil
|
||||||
|
if not scope.blocked and isMouseButtonDown(1) then
|
||||||
|
local heading = MTKViewpointCamera.heading()
|
||||||
|
if type(heading) == "number" and heading == heading and math.abs(heading) < math.huge then
|
||||||
|
oldDirection = player:getDirectionAngle()
|
||||||
|
local rad = math.rad(heading)
|
||||||
|
scope.x = player:getX() + math.cos(rad) * 100
|
||||||
|
scope.y = player:getY() + math.sin(rad) * 100
|
||||||
|
player:setDirectionAngle(heading)
|
||||||
|
else
|
||||||
|
scope.blocked = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Restore temporary input even if a dependency raises an error.
|
||||||
|
local ok, err = pcall(previousAim, player)
|
||||||
|
scope = oldScope
|
||||||
|
if oldDirection ~= nil then player:setDirectionAngle(oldDirection) end
|
||||||
|
if not ok then error(err) end
|
||||||
|
end
|
||||||
|
tankAimSystemPC = M.aim
|
||||||
|
Events.OnPlayerUpdate.Add(M.aim)
|
||||||
|
print("[MTKViewpointTurrets] MTK camera aiming registered; Ferret/LAV wrappers retained.")
|
||||||
|
end
|
||||||
|
|
||||||
|
if TankCursor and TankCursor.prerender ~= M.cursor then
|
||||||
|
local previousCursor = TankCursor.prerender
|
||||||
|
M.cursor = function(self)
|
||||||
|
if not enabled() then return previousCursor(self) end
|
||||||
|
-- MTK's two iso-projected textures cannot represent the 3D view.
|
||||||
|
-- A centred cross marks camera bearing; green means the turret
|
||||||
|
-- has caught up in yaw, not that ammunition or a hit is guaranteed.
|
||||||
|
local player = getPlayer()
|
||||||
|
local vehicle = player and player:getVehicle()
|
||||||
|
local weapon = vehicle and chooseWeaponPC(vehicle)
|
||||||
|
if not weapon or not allowed(player, vehicle, weapon) or not isMouseButtonDown(1) then return end
|
||||||
|
local heading = MTKViewpointCamera.heading()
|
||||||
|
if not heading then return end
|
||||||
|
local turret = turretAngles and turretAngles[vehicle:getId()]
|
||||||
|
local aligned = turret and math.abs(((turret - heading + 540) % 360) - 180) <= 2
|
||||||
|
local r, g, b = 1, 0.75, 0.25
|
||||||
|
if aligned then r, g, b = 0.3, 1, 0.4 end
|
||||||
|
local x = getCore():getScreenWidth() / 2 - self.x
|
||||||
|
local y = getCore():getScreenHeight() / 2 - self.y
|
||||||
|
self:drawRect(x - 9, y, 5, 1, 0.9, r, g, b)
|
||||||
|
self:drawRect(x + 4, y, 5, 1, 0.9, r, g, b)
|
||||||
|
self:drawRect(x, y - 9, 1, 5, 0.9, r, g, b)
|
||||||
|
self:drawRect(x, y + 4, 1, 5, 0.9, r, g, b)
|
||||||
|
end
|
||||||
|
TankCursor.prerender = M.cursor
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Events.OnGameBoot.Add(M.install)
|
||||||
|
Events.OnGameStart.Add(M.install)
|
||||||
|
Events.OnGameStart.Add(function()
|
||||||
|
if not MTKViewpointCamera then
|
||||||
|
print("[MTKViewpointTurrets] Camera bridge unavailable. Enable this mod and approve its local JAR in ZombieBuddy, then fully restart the game.")
|
||||||
|
end
|
||||||
|
end)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
param([string]$Game = 'D:\SteamLibrary\steamapps\common\ProjectZomboid',
|
||||||
|
[string]$Viewpoint = 'D:\SteamLibrary\steamapps\workshop\content\108600\3809306528\mods\Viewpoint\42\media\java\client\Viewpoint.jar',
|
||||||
|
[string]$BuildDirectory = (Join-Path (Get-Location) 'work\MTKViewpointTurrets-build'),
|
||||||
|
[switch]$Sign)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$buildDir = [System.IO.Path]::GetFullPath($BuildDirectory)
|
||||||
|
New-Item -ItemType Directory -Force -Path $buildDir | Out-Null
|
||||||
|
$jarDir = Join-Path $PSScriptRoot '42.20\media\java\client'
|
||||||
|
New-Item -ItemType Directory -Force -Path $jarDir | Out-Null
|
||||||
|
$sources = @(Get-ChildItem -LiteralPath (Join-Path $PSScriptRoot 'source') -Recurse -Filter '*.java' | ForEach-Object FullName)
|
||||||
|
& javac --release 25 -classpath "$Game\projectzomboid.jar;$Game\ZombieBuddy.jar;$Viewpoint" -d $buildDir @sources
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Java compilation failed.' }
|
||||||
|
& jar --create --file "$jarDir\MTKViewpointTurrets-0.2.0.jar" -C $buildDir .
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'JAR packaging failed.' }
|
||||||
|
Write-Host "Built $jarDir\MTKViewpointTurrets-0.2.0.jar"
|
||||||
|
if ($Sign) { & (Join-Path $PSScriptRoot 'Sign.ps1') }
|
||||||
@@ -1,3 +1,61 @@
|
|||||||
# ViewpointTurrets
|
# ViewpointTurrets
|
||||||
|
|
||||||
Ever been reved in a landy?
|
Ever been reved in a landy?
|
||||||
|
|
||||||
|
Military Tool Kit camera aiming and 3D tracers for Project Viewpoint. Supports standard MTK weapons and HRSys Functional Turrets for the Ferret Mk2 and LAV-300 FSV.
|
||||||
|
|
||||||
|
Current version: **0.2.0**. Targets **Project Zomboid B42.21**, **Viewpoint 0.1.6.1**, **Military Tool Kit 2.0.0**, and **ZombieBuddy 2.3.0 or later**. The `42.20` folder is the version-folder baseline selected by B42.21; it does not claim compatibility with the B42.20 game executable.
|
||||||
|
|
||||||
|
## Mod layout
|
||||||
|
|
||||||
|
The repository root is the mod root, ready to be linked directly into `Zomboid/mods`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ViewpointTurrets/
|
||||||
|
common/mod.info
|
||||||
|
42.20/media/lua/client/
|
||||||
|
42.20/media/java/client/MTKViewpointTurrets-0.2.0.jar
|
||||||
|
42.20/media/java/client/MTKViewpointTurrets-0.2.0.jar.zbs
|
||||||
|
source/
|
||||||
|
tools/SignZBS.java
|
||||||
|
Build.ps1
|
||||||
|
Sign.ps1
|
||||||
|
ZombieBuddy-public-key.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Use a directory symlink or junction from `Zomboid/mods/MTKViewpointTurrets` to the **repository root**, rather than to either version subfolder. The mod ID remains `MTKViewpointTurrets`, regardless of the checkout folder's name. An existing mod folder must be relocated before creating a link at the same path.
|
||||||
|
|
||||||
|
Enable **Military Tool Kit - Viewpoint Turret Aiming** together with MTK, Viewpoint and the relevant vehicle/turret mods. Its metadata orders it after those dependencies. Existing saves have their own mod selection.
|
||||||
|
|
||||||
|
## Controls and rendering
|
||||||
|
|
||||||
|
From the gunner seat, hold **right mouse** and move the camera to traverse the turret. Hold **right mouse + left mouse** to fire. Releasing right mouse stops commanding a turn. Original ammunition, reloads, damage, traverse speed and angle limits remain supplied by MTK and the vehicle adapters.
|
||||||
|
|
||||||
|
The centred cross is amber while traversing and green within two degrees of camera bearing. It indicates horizontal alignment only; camera pitch does not add barrel elevation or guarantee a hit at the crosshair.
|
||||||
|
|
||||||
|
Viewpoint uses depth-tested 3D tracers taken from MTK's live projectile positions. The original isometric tracer drawing is suppressed only in Viewpoint and restored in iso mode. Frame snapshots keep mutable Lua tables off the render thread. No second ballistic simulation or damage processing is added. Viewpoint terrain relief and storey height are included in the coordinate conversion.
|
||||||
|
|
||||||
|
The native Viewpoint rendering path is supported, including the installed TrueWeathers shader pack. The alternate Minecraft/Iris pipeline is not supported. Multiplayer, controllers and split-screen have not been validated.
|
||||||
|
|
||||||
|
## Build and sign
|
||||||
|
|
||||||
|
Requires a Java 25 JDK. Run from the repository root:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Build.ps1
|
||||||
|
.\Build.ps1 -Sign
|
||||||
|
```
|
||||||
|
|
||||||
|
The default game and Viewpoint JAR locations match this workstation. Override them with `-Game` and `-Viewpoint`. Build intermediates go under the ignored `work/` directory. Output goes directly into `42.20/media/java/client/`.
|
||||||
|
|
||||||
|
The distributed JAR is signed for **Riggs0**, SteamID64 **76561198079674652**. Distribute the `.jar.zbs` sidecar with the unchanged JAR. `ZombieBuddy-public-key.txt` contains the public key used for author verification through the Steam profile or ZombieBuddy's known-author list.
|
||||||
|
|
||||||
|
The private signing identity stays outside the repository, by default in `%USERPROFILE%\.zombie_buddy\signing\Riggs0`. Reuse that identity; do not generate a replacement when signing another build. `Sign.ps1 -KeyDirectory <path>` supports a different local key location. Never commit the private key.
|
||||||
|
|
||||||
|
Any rebuilt JAR needs a fresh signature. `Build.ps1 -Sign` handles both steps. Signing an existing JAR without rebuilding uses `Sign.ps1`. Updated Java code and signatures take effect on the next normal game restart; no launcher or automatic game restart is included.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Automated checks exercised the installed MTK code and the game's Kahlua VM for camera traversal, ammunition use, projectile tracking/removal, iso fallback and input gating. A standalone hidden OpenGL test checked tracer visibility, depth occlusion, near-plane clipping and restoration of render state. The two rendering hooks passed ZombieBuddy annotation conversion and offline bytecode weaving against the installed Viewpoint classes. ZombieBuddy's verifier accepted the signature with the supplied public key and rejected a mismatched JAR hash.
|
||||||
|
|
||||||
|
Third-person aiming was confirmed in game by the user. The new tracer appearance still needs an in-game check. No game client or save was started for automated verification.
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
param(
|
||||||
|
[string]$KeyDirectory = (Join-Path $env:USERPROFILE '.zombie_buddy\signing\Riggs0'),
|
||||||
|
[string]$SteamID64 = '76561198079674652',
|
||||||
|
[switch]$Initialize
|
||||||
|
)
|
||||||
|
$ErrorActionPreference='Stop'
|
||||||
|
$signer=Join-Path $PSScriptRoot 'tools\SignZBS.java'
|
||||||
|
if($Initialize) {
|
||||||
|
if(Test-Path -LiteralPath (Join-Path $KeyDirectory 'author.properties')) {throw 'Signing identity already configured.'}
|
||||||
|
New-Item -ItemType Directory -Force -Path $KeyDirectory | Out-Null
|
||||||
|
$identity=[Security.Principal.WindowsIdentity]::GetCurrent().User
|
||||||
|
$acl=[Security.AccessControl.DirectorySecurity]::new()
|
||||||
|
$acl.SetOwner($identity)
|
||||||
|
$acl.SetAccessRuleProtection($true,$false)
|
||||||
|
$inherit=[Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit'
|
||||||
|
$propagate=[Security.AccessControl.PropagationFlags]::None
|
||||||
|
$allow=[Security.AccessControl.AccessControlType]::Allow
|
||||||
|
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new($identity,'FullControl',$inherit,$propagate,$allow))
|
||||||
|
$system=[Security.Principal.SecurityIdentifier]::new('S-1-5-18')
|
||||||
|
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new($system,'FullControl',$inherit,$propagate,$allow))
|
||||||
|
Set-Acl -LiteralPath $KeyDirectory -AclObject $acl
|
||||||
|
& java $signer init $KeyDirectory $SteamID64
|
||||||
|
if($LASTEXITCODE -ne 0) {throw 'Signing identity initialization failed.'}
|
||||||
|
}
|
||||||
|
$info=Get-Content -LiteralPath (Join-Path $PSScriptRoot 'common\mod.info')
|
||||||
|
$jarEntry=($info | Where-Object {$_ -match '^javaJarFile='}) -replace '^javaJarFile=',''
|
||||||
|
$jar=Join-Path $PSScriptRoot ('42.20\'+$jarEntry)
|
||||||
|
& java $signer sign $KeyDirectory $jar
|
||||||
|
if($LASTEXITCODE -ne 0) {throw 'JAR signing failed.'}
|
||||||
|
Copy-Item -LiteralPath (Join-Path $KeyDirectory 'public-profile-key.txt') -Destination (Join-Path $PSScriptRoot 'ZombieBuddy-public-key.txt') -Force
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
JavaModZBS:a4feaa34621f43ec1eb2283771f1120531ccdf44cd2a3f270eaba20515994a21
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
name=Military Tool Kit - Viewpoint Turret Aiming
|
||||||
|
id=MTKViewpointTurrets
|
||||||
|
description=Local B42.21 compatibility: camera aiming and 3D world tracers for MTK turrets, including the HRSys Ferret and LAV adapters. Hold right mouse to traverse; left mouse fires using normal MTK ballistics and ammunition.
|
||||||
|
require=\ZombieBuddy,\Military_Tool_Kit,\Viewpoint
|
||||||
|
loadModAfter=Military_Tool_Kit,Viewpoint,hrsys_functionalturrets_ferret,hrsys_functionalturrets_lav300,KI5FerretTurret,KI5LAVTurret
|
||||||
|
versionMin=42.21
|
||||||
|
versionMax=42.21
|
||||||
|
modversion=0.2.0
|
||||||
|
javaJarFile=media/java/client/MTKViewpointTurrets-0.2.0.jar
|
||||||
|
javaPkgName=localmods.mtkviewpoint
|
||||||
|
ZBVersionMin=2.3.0
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package localmods.mtkviewpoint;
|
||||||
|
|
||||||
|
import me.zed_0xff.zombie_buddy.Exposer.LuaClass;
|
||||||
|
import viewpoint.core.View;
|
||||||
|
import viewpoint.input.FreeCam;
|
||||||
|
import viewpoint.input.Look;
|
||||||
|
|
||||||
|
/** Read-only bridge to Viewpoint 0.1.6.1; never changes capture or camera state. */
|
||||||
|
@LuaClass(name = "MTKViewpointCamera")
|
||||||
|
public final class CameraBridge {
|
||||||
|
private CameraBridge() {}
|
||||||
|
|
||||||
|
public static boolean enabled() {
|
||||||
|
return View.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean captured() {
|
||||||
|
return View.enabled && Look.captured && !FreeCam.active;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Double heading() {
|
||||||
|
float yaw = Look.yaw;
|
||||||
|
if (!captured() || !Float.isFinite(yaw)) return null;
|
||||||
|
// Viewpoint Controls.face uses cos(yaw), sin(yaw) in game-world XY.
|
||||||
|
return (Math.toDegrees(yaw) % 360.0 + 360.0) % 360.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package localmods.mtkviewpoint;
|
||||||
|
|
||||||
|
public final class Main {
|
||||||
|
private Main() {}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println("[MTKViewpointTurrets] 0.2.0 camera bridge and 3D tracers loaded (Viewpoint 0.1.6.1 / B42.21).");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package localmods.mtkviewpoint;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import me.zed_0xff.zombie_buddy.Exposer.LuaClass;
|
||||||
|
import se.krka.kahlua.vm.*;
|
||||||
|
import viewpoint.core.Frame;
|
||||||
|
import viewpoint.render.SceneData;
|
||||||
|
import zombie.Lua.Event;
|
||||||
|
import zombie.Lua.LuaEventManager;
|
||||||
|
|
||||||
|
/** Reads MTK's real projectiles on the game thread; publishes immutable render snapshots. */
|
||||||
|
@LuaClass(name = "MTKViewpointProjectiles")
|
||||||
|
public final class ProjectileBridge {
|
||||||
|
static final int LIMIT = 256;
|
||||||
|
static final double LEVEL = 2.4494895935058594;
|
||||||
|
private static KahluaTable physics, visuals;
|
||||||
|
private static final Map<SceneData, List<TracerMesh.Segment>> frames =
|
||||||
|
Collections.synchronizedMap(new WeakHashMap<>());
|
||||||
|
|
||||||
|
private ProjectileBridge() {}
|
||||||
|
|
||||||
|
public static LuaClosure legacyRenderer() {
|
||||||
|
var events = new ArrayList<Event>();
|
||||||
|
LuaEventManager.getEvents(events, new HashMap<>());
|
||||||
|
LuaClosure found = null;
|
||||||
|
for (var event : events) {
|
||||||
|
if (!"OnPreUIDraw".equals(event.name)) continue;
|
||||||
|
for (var callback : event.callbacks) {
|
||||||
|
var constants = Arrays.asList(callback.prototype.constants);
|
||||||
|
if (constants.containsAll(List.of("renderline","VisualProjectilePC","currentDistance","startHeight","isCovered"))) {
|
||||||
|
if (found != null) return null; // ambiguous: leave the dependency untouched
|
||||||
|
found = callback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean bind(Object removeById, KahluaTable visualList) {
|
||||||
|
// In installed MTK 2.0.0 removeByID closes over exactly one value:
|
||||||
|
// projectilesByID. Read that reference; never replace or modify it.
|
||||||
|
if (!(removeById instanceof LuaClosure fn) || fn.prototype.numParams != 2 || fn.upvalues.length != 1
|
||||||
|
|| !Arrays.asList(fn.prototype.constants).containsAll(List.of("id","list","table","remove"))
|
||||||
|
|| fn.upvalues[0] == null || !(fn.upvalues[0].getValue() instanceof KahluaTable table)
|
||||||
|
|| visualList == null) return false;
|
||||||
|
physics = table;
|
||||||
|
visuals = visualList;
|
||||||
|
frames.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void capture(Frame frame) {
|
||||||
|
if (frame == null) return;
|
||||||
|
if (physics == null || visuals == null) { frames.remove(frame.scene); return; }
|
||||||
|
var result = new ArrayList<TracerMesh.Segment>();
|
||||||
|
for (int i = 1, count = Math.min(visuals.len(), LIMIT); i <= count; i++) {
|
||||||
|
if (!(visuals.rawget(i) instanceof KahluaTable visual)) continue;
|
||||||
|
if (!(physics.rawget(visual.rawget("id")) instanceof KahluaTable shot)) continue;
|
||||||
|
double distance = num(shot, "distance"), travelled = num(shot, "currentDistance");
|
||||||
|
double x = num(shot,"startX"), y = num(shot,"startY"), z = num(shot,"startHeight");
|
||||||
|
double ex = num(shot,"endX"), ey = num(shot,"endY"), ez = num(shot,"endHeight");
|
||||||
|
if (!finite(distance,travelled,x,y,z,ex,ey,ez) || distance <= 0 || travelled <= 0 || travelled >= distance) continue;
|
||||||
|
double scale = num(visual,"scale");
|
||||||
|
if (!Double.isFinite(scale)) scale = 0.1;
|
||||||
|
double length = Math.max(0.45, Math.min(2.5, distance * Math.max(0.1,scale) * 0.08));
|
||||||
|
double head = Math.min(1,travelled/distance), tail = Math.max(0,(travelled-length)/distance);
|
||||||
|
double hx=x+(ex-x)*head, hy=y+(ey-y)*head, hz=z+(ez-z)*head;
|
||||||
|
double tx=x+(ex-x)*tail, ty=y+(ey-y)*tail, tz=z+(ez-z)*tail;
|
||||||
|
float[] h=local(frame.scene,hx,hy,hz), t=local(frame.scene,tx,ty,tz);
|
||||||
|
KahluaTable color = visual.rawget("color") instanceof KahluaTable c ? c : null;
|
||||||
|
result.add(new TracerMesh.Segment(h[0],h[1],h[2],t[0],t[1],t[2],
|
||||||
|
color(color,1,1),color(color,2,0.72f),color(color,3,0.14f),1));
|
||||||
|
}
|
||||||
|
frames.put(frame.scene,List.copyOf(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<TracerMesh.Segment> segments(SceneData scene) {
|
||||||
|
return frames.getOrDefault(scene,List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
static float[] local(SceneData scene, double x, double y, double z) {
|
||||||
|
double lift=scene.relief == null ? 0 : scene.relief.scene(x,y);
|
||||||
|
return new float[]{(float)(-x-scene.originX),(float)(z*LEVEL+lift-scene.originY),(float)(-y-scene.originZ)};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double num(KahluaTable t,String key) {
|
||||||
|
return t.rawget(key) instanceof Number n ? n.doubleValue() : Double.NaN;
|
||||||
|
}
|
||||||
|
private static float color(KahluaTable t,int key,float fallback) {
|
||||||
|
return t != null && t.rawget(key) instanceof Number n && Float.isFinite(n.floatValue())
|
||||||
|
? Math.max(0,Math.min(1,n.floatValue())) : fallback;
|
||||||
|
}
|
||||||
|
private static boolean finite(double... values) {
|
||||||
|
for (double v:values) if (!Double.isFinite(v)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package localmods.mtkviewpoint;
|
||||||
|
|
||||||
|
import me.zed_0xff.zombie_buddy.Patch;
|
||||||
|
import viewpoint.core.Frame;
|
||||||
|
import viewpoint.render.SceneData;
|
||||||
|
|
||||||
|
public final class RenderPatches {
|
||||||
|
private RenderPatches() {}
|
||||||
|
|
||||||
|
@Patch(className="viewpoint.FP",methodName="captureWorld")
|
||||||
|
public static class Snapshot {
|
||||||
|
@Patch.OnExit
|
||||||
|
public static void exit(@Patch.Argument(0) Frame frame) {
|
||||||
|
try { ProjectileBridge.capture(frame); }
|
||||||
|
catch (Throwable error) { TracerRenderer.fail(error); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(className="viewpoint.render.WorldRenderer",methodName="drawWeather")
|
||||||
|
public static class Draw {
|
||||||
|
@Patch.OnExit
|
||||||
|
public static void exit(@Patch.Argument(0) SceneData scene) {
|
||||||
|
TracerRenderer.draw(scene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package localmods.mtkviewpoint;
|
||||||
|
|
||||||
|
import java.nio.FloatBuffer;
|
||||||
|
import org.joml.Matrix4f;
|
||||||
|
import org.joml.Vector4f;
|
||||||
|
|
||||||
|
/** Projected ribbons retain world depth; clipping prevents flashes behind the camera. */
|
||||||
|
public final class TracerMesh {
|
||||||
|
public record Segment(float hx,float hy,float hz,float tx,float ty,float tz,float r,float g,float b,float alpha) {}
|
||||||
|
public static final int STRIDE=10;
|
||||||
|
private TracerMesh() {}
|
||||||
|
|
||||||
|
public static int append(FloatBuffer out, Segment s, Matrix4f vp, int width, int height) {
|
||||||
|
if (width<=0 || height<=0 || out.remaining()<6*STRIDE) return 0;
|
||||||
|
var h=vp.transform(new Vector4f(s.hx,s.hy,s.hz,1));
|
||||||
|
var t=vp.transform(new Vector4f(s.tx,s.ty,s.tz,1));
|
||||||
|
if (!h.isFinite() || !t.isFinite()) return 0;
|
||||||
|
float hn=h.z+h.w, tn=t.z+t.w;
|
||||||
|
if (hn<=0 && tn<=0) return 0;
|
||||||
|
if (hn<=0) h.lerp(t,(-hn+0.00001f)/(tn-hn));
|
||||||
|
else if (tn<=0) t.lerp(h,(-tn+0.00001f)/(hn-tn));
|
||||||
|
if (h.w<=0.000001f || t.w<=0.000001f) return 0;
|
||||||
|
float dx=(h.x/h.w-t.x/t.w)*width, dy=(h.y/h.w-t.y/t.w)*height;
|
||||||
|
float len=(float)Math.hypot(dx,dy);
|
||||||
|
if (len<1) { // End-on rounds still have a small visible core.
|
||||||
|
t.x-=4f/width*t.w; dx=4; dy=0; len=4;
|
||||||
|
}
|
||||||
|
float ox=-dy/len*2.5f/width, oy=dx/len*2.5f/height;
|
||||||
|
vertex(out,t,ox,oy,-1,0,s); vertex(out,h,ox,oy,-1,1,s); vertex(out,h,ox,oy,1,1,s);
|
||||||
|
vertex(out,t,ox,oy,-1,0,s); vertex(out,h,ox,oy,1,1,s); vertex(out,t,ox,oy,1,0,s);
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void vertex(FloatBuffer b,Vector4f p,float ox,float oy,float side,float along,Segment s) {
|
||||||
|
b.put(p.x+ox*side*p.w).put(p.y+oy*side*p.w).put(p.z).put(p.w);
|
||||||
|
b.put(along).put(side).put(s.r).put(s.g).put(s.b).put(s.alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package localmods.mtkviewpoint;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.nio.FloatBuffer;
|
||||||
|
import org.lwjgl.BufferUtils;
|
||||||
|
import static org.lwjgl.opengl.GL33.*;
|
||||||
|
import viewpoint.render.FrameContext;
|
||||||
|
import viewpoint.render.SceneData;
|
||||||
|
import viewpoint.render.WorldRenderer;
|
||||||
|
|
||||||
|
/** Native Viewpoint HDR pass, before post processing, with normal scene depth testing. */
|
||||||
|
public final class TracerRenderer {
|
||||||
|
private static final FloatBuffer vertices=BufferUtils.createFloatBuffer(ProjectileBridge.LIMIT*6*TracerMesh.STRIDE);
|
||||||
|
private static Field frameField,irisField;
|
||||||
|
private static int program,vao,vbo;
|
||||||
|
private static volatile boolean failed;
|
||||||
|
private TracerRenderer() {}
|
||||||
|
|
||||||
|
public static void fail(Throwable error) {
|
||||||
|
if (!failed) System.out.println("[MTKViewpointTurrets] 3D tracers disabled after render error: "+error);
|
||||||
|
failed=true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void draw(SceneData scene) {
|
||||||
|
if (failed || scene==null) return;
|
||||||
|
var shots=ProjectileBridge.segments(scene);
|
||||||
|
if (shots.isEmpty()) return;
|
||||||
|
try {
|
||||||
|
if (frameField==null) {
|
||||||
|
frameField=WorldRenderer.class.getDeclaredField("frame"); frameField.setAccessible(true);
|
||||||
|
irisField=WorldRenderer.class.getDeclaredField("irisDrawn"); irisField.setAccessible(true);
|
||||||
|
}
|
||||||
|
if (irisField.getBoolean(null)) return; // Minecraft/Iris uses separate private targets.
|
||||||
|
FrameContext frame=(FrameContext)frameField.get(null);
|
||||||
|
if (frame==null || frame.scene!=scene) return;
|
||||||
|
vertices.clear();
|
||||||
|
int count=0;
|
||||||
|
for (var shot:shots) count+=TracerMesh.append(vertices,shot,frame.viewProjection,frame.width,frame.height);
|
||||||
|
vertices.flip();
|
||||||
|
if (count==0) return;
|
||||||
|
drawBuffer(vertices,count,frame.width,frame.height,true);
|
||||||
|
} catch (Throwable error) { fail(error); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kept separate so the same production GPU path can be checked in a tiny
|
||||||
|
// hidden OpenGL test context without launching Project Zomboid.
|
||||||
|
static void drawBuffer(FloatBuffer data,int count,int width,int height,boolean bindViewpointTarget) {
|
||||||
|
int oldProgram=glGetInteger(GL_CURRENT_PROGRAM),oldVAO=glGetInteger(GL_VERTEX_ARRAY_BINDING);
|
||||||
|
int oldVBO=glGetInteger(GL_ARRAY_BUFFER_BINDING),oldDraw=glGetInteger(GL_DRAW_FRAMEBUFFER_BINDING);
|
||||||
|
int oldRead=glGetInteger(GL_READ_FRAMEBUFFER_BINDING),depthFunc=glGetInteger(GL_DEPTH_FUNC);
|
||||||
|
int[] viewport=new int[4]; glGetIntegerv(GL_VIEWPORT,viewport);
|
||||||
|
boolean depth=glIsEnabled(GL_DEPTH_TEST),blend=glIsEnabled(GL_BLEND),cull=glIsEnabled(GL_CULL_FACE);
|
||||||
|
boolean scissor=glIsEnabled(GL_SCISSOR_TEST),depthMask=glGetBoolean(GL_DEPTH_WRITEMASK);
|
||||||
|
int sr=glGetInteger(GL_BLEND_SRC_RGB),dr=glGetInteger(GL_BLEND_DST_RGB);
|
||||||
|
int sa=glGetInteger(GL_BLEND_SRC_ALPHA),da=glGetInteger(GL_BLEND_DST_ALPHA);
|
||||||
|
int er=glGetInteger(GL_BLEND_EQUATION_RGB),ea=glGetInteger(GL_BLEND_EQUATION_ALPHA);
|
||||||
|
try {
|
||||||
|
if (program==0) init();
|
||||||
|
if (bindViewpointTarget) WorldRenderer.bindTarget(); else glViewport(0,0,width,height);
|
||||||
|
glUseProgram(program); glBindVertexArray(vao); glBindBuffer(GL_ARRAY_BUFFER,vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER,data,GL_STREAM_DRAW);
|
||||||
|
glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); glDepthMask(false);
|
||||||
|
glDisable(GL_CULL_FACE); glDisable(GL_SCISSOR_TEST); glEnable(GL_BLEND);
|
||||||
|
glBlendEquationSeparate(GL_FUNC_ADD,GL_FUNC_ADD);
|
||||||
|
glBlendFuncSeparate(GL_ONE,GL_ONE_MINUS_SRC_ALPHA,GL_ZERO,GL_ONE);
|
||||||
|
glDrawArrays(GL_TRIANGLES,0,count);
|
||||||
|
} finally {
|
||||||
|
glUseProgram(oldProgram); glBindVertexArray(oldVAO); glBindBuffer(GL_ARRAY_BUFFER,oldVBO);
|
||||||
|
glBlendEquationSeparate(er,ea); glBlendFuncSeparate(sr,dr,sa,da);
|
||||||
|
toggle(GL_BLEND,blend); toggle(GL_DEPTH_TEST,depth); toggle(GL_CULL_FACE,cull); toggle(GL_SCISSOR_TEST,scissor);
|
||||||
|
glDepthFunc(depthFunc); glDepthMask(depthMask);
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER,oldDraw); glBindFramebuffer(GL_READ_FRAMEBUFFER,oldRead);
|
||||||
|
glViewport(viewport[0],viewport[1],viewport[2],viewport[3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void toggle(int cap,boolean on) { if(on) glEnable(cap); else glDisable(cap); }
|
||||||
|
|
||||||
|
private static void init() {
|
||||||
|
int vs=shader(GL_VERTEX_SHADER,"""
|
||||||
|
#version 330 core
|
||||||
|
layout(location=0) in vec4 clipPosition;
|
||||||
|
layout(location=1) in vec2 uv;
|
||||||
|
layout(location=2) in vec4 color;
|
||||||
|
out vec2 texCoord;
|
||||||
|
out vec4 tint;
|
||||||
|
void main(){gl_Position=clipPosition; texCoord=uv; tint=color;}
|
||||||
|
""");
|
||||||
|
int fs=shader(GL_FRAGMENT_SHADER,"""
|
||||||
|
#version 330 core
|
||||||
|
in vec2 texCoord;
|
||||||
|
in vec4 tint;
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main(){
|
||||||
|
float edge=1.0-smoothstep(0.25,1.0,abs(texCoord.y));
|
||||||
|
float a=tint.a*edge*smoothstep(0.0,0.25,texCoord.x);
|
||||||
|
fragColor=vec4(tint.rgb*5.0*a,a);
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
int linked=glCreateProgram();
|
||||||
|
try {
|
||||||
|
glAttachShader(linked,vs); glAttachShader(linked,fs); glLinkProgram(linked);
|
||||||
|
if(glGetProgrami(linked,GL_LINK_STATUS)==0) throw new IllegalStateException(glGetProgramInfoLog(linked));
|
||||||
|
} catch(RuntimeException error) {glDeleteProgram(linked); throw error;}
|
||||||
|
finally {glDeleteShader(vs); glDeleteShader(fs);}
|
||||||
|
vao=glGenVertexArrays(); vbo=glGenBuffers();
|
||||||
|
glBindVertexArray(vao); glBindBuffer(GL_ARRAY_BUFFER,vbo);
|
||||||
|
int stride=TracerMesh.STRIDE*Float.BYTES;
|
||||||
|
glEnableVertexAttribArray(0); glVertexAttribPointer(0,4,GL_FLOAT,false,stride,0L);
|
||||||
|
glEnableVertexAttribArray(1); glVertexAttribPointer(1,2,GL_FLOAT,false,stride,16L);
|
||||||
|
glEnableVertexAttribArray(2); glVertexAttribPointer(2,4,GL_FLOAT,false,stride,24L);
|
||||||
|
program=linked;
|
||||||
|
}
|
||||||
|
private static int shader(int type,String source) {
|
||||||
|
int id=glCreateShader(type); glShaderSource(id,source); glCompileShader(id);
|
||||||
|
if(glGetShaderi(id,GL_COMPILE_STATUS)==0) {
|
||||||
|
String error=glGetShaderInfoLog(id); glDeleteShader(id); throw new IllegalStateException(error);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.security.*;
|
||||||
|
import java.security.spec.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/** Local Ed25519 ZBS signing; private key material is never printed or packaged. */
|
||||||
|
public class SignZBS {
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
if(args.length<3) throw new IllegalArgumentException("init <key-directory> <SteamID64> | sign <key-directory> <jar>");
|
||||||
|
Path dir=Path.of(args[1]);
|
||||||
|
Path privatePath=dir.resolve("ed25519-private.der"), publicPath=dir.resolve("ed25519-public.der");
|
||||||
|
Path authorPath=dir.resolve("author.properties");
|
||||||
|
if(args[0].equals("init")) {
|
||||||
|
if(!args[2].matches("[0-9]{17}")) throw new IllegalArgumentException("Invalid SteamID64");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
if(Files.exists(privatePath)||Files.exists(publicPath)||Files.exists(authorPath))
|
||||||
|
throw new IllegalStateException("Signing identity already exists; reuse it rather than replacing it.");
|
||||||
|
KeyPair pair=KeyPairGenerator.getInstance("Ed25519").generateKeyPair();
|
||||||
|
Files.write(privatePath,pair.getPrivate().getEncoded(),StandardOpenOption.CREATE_NEW);
|
||||||
|
Files.write(publicPath,pair.getPublic().getEncoded(),StandardOpenOption.CREATE_NEW);
|
||||||
|
Files.writeString(authorPath,"steamID64="+args[2]+"\n",StandardOpenOption.CREATE_NEW);
|
||||||
|
Files.writeString(dir.resolve("public-profile-key.txt"),profileKey(pair.getPublic())+"\n",StandardOpenOption.CREATE_NEW);
|
||||||
|
System.out.println("Created dedicated Ed25519 ZombieBuddy signing identity for SteamID64 "+args[2]+".");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!args[0].equals("sign")) throw new IllegalArgumentException("Unknown operation");
|
||||||
|
Properties author=new Properties();
|
||||||
|
try(var reader=Files.newBufferedReader(authorPath)) {author.load(reader);}
|
||||||
|
String sid=author.getProperty("steamID64");
|
||||||
|
if(sid==null||!sid.matches("[0-9]{17}")) throw new IllegalStateException("Invalid signing identity");
|
||||||
|
var factory=KeyFactory.getInstance("Ed25519");
|
||||||
|
var privateKey=factory.generatePrivate(new PKCS8EncodedKeySpec(Files.readAllBytes(privatePath)));
|
||||||
|
var publicKey=factory.generatePublic(new X509EncodedKeySpec(Files.readAllBytes(publicPath)));
|
||||||
|
Path jar=Path.of(args[2]);
|
||||||
|
String sha=HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(jar)));
|
||||||
|
byte[] payload=("ZBS:"+sid+":"+sha).getBytes(StandardCharsets.UTF_8);
|
||||||
|
var signer=Signature.getInstance("Ed25519");signer.initSign(privateKey);signer.update(payload);
|
||||||
|
byte[] signature=signer.sign();
|
||||||
|
var verifier=Signature.getInstance("Ed25519");verifier.initVerify(publicKey);verifier.update(payload);
|
||||||
|
if(!verifier.verify(signature)) throw new IllegalStateException("Signing key pair does not match");
|
||||||
|
Path sidecar=Path.of(jar.toString()+".zbs");
|
||||||
|
Files.writeString(sidecar,"ZBS\nSteamID64:"+sid+"\nSignature:"+HexFormat.of().formatHex(signature)+"\n");
|
||||||
|
System.out.println("Signed and cryptographically verified "+jar.getFileName()+" (SteamID64 "+sid+").");
|
||||||
|
System.out.println("SHA-256: "+sha);
|
||||||
|
}
|
||||||
|
private static String profileKey(PublicKey key) {
|
||||||
|
byte[] encoded=key.getEncoded();
|
||||||
|
byte[] prefix=HexFormat.of().parseHex("302a300506032b6570032100");
|
||||||
|
if(encoded.length!=44||!Arrays.equals(prefix,Arrays.copyOf(encoded,12)))
|
||||||
|
throw new IllegalStateException("Unexpected Ed25519 public key encoding");
|
||||||
|
return "JavaModZBS:"+HexFormat.of().formatHex(Arrays.copyOfRange(encoded,12,44));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user