225 lines
8.4 KiB
Lua
225 lines
8.4 KiB
Lua
local failures = 0
|
|
|
|
local function expect(condition, message)
|
|
if not condition then
|
|
failures = failures + 1
|
|
io.stderr:write("FAIL: " .. message .. "\n")
|
|
end
|
|
end
|
|
|
|
package.path = table.concat({
|
|
"42.20/media/lua/client/?.lua",
|
|
"42.20/media/lua/shared/?.lua",
|
|
package.path
|
|
}, ";")
|
|
|
|
isServer = function() return false end
|
|
isClient = function() return true end
|
|
|
|
local serverCommandCallbacks = {}
|
|
local enterVehicleCallbacks = {}
|
|
local spawnVehicleCallbacks = {}
|
|
Events = {
|
|
OnServerCommand = {
|
|
Add = function(callback)
|
|
serverCommandCallbacks[#serverCommandCallbacks + 1] = callback
|
|
end
|
|
},
|
|
OnEnterVehicle = {
|
|
Add = function(callback)
|
|
enterVehicleCallbacks[#enterVehicleCallbacks + 1] = callback
|
|
end
|
|
},
|
|
OnSpawnVehicleEnd = {
|
|
Add = function(callback)
|
|
spawnVehicleCallbacks[#spawnVehicleCallbacks + 1] = callback
|
|
end
|
|
}
|
|
}
|
|
|
|
local localPlayer = { onlineId = 7 }
|
|
local function attachmentScript()
|
|
local attachments = { trailer = {}, trailerfront = {} }
|
|
return {
|
|
getAttachmentById = function(_, attachmentId)
|
|
return attachments[attachmentId]
|
|
end
|
|
}
|
|
end
|
|
|
|
local function unlink(a, b)
|
|
a.towing, a.towedBy = nil, nil
|
|
b.towing, b.towedBy = nil, nil
|
|
end
|
|
|
|
local function vehicle(id)
|
|
local value = {
|
|
id = id,
|
|
script = attachmentScript(),
|
|
scriptName = "Base.DriverOwnership" .. tostring(id),
|
|
modData = {},
|
|
driver = nil,
|
|
towing = nil,
|
|
towedBy = nil,
|
|
addCalls = 0,
|
|
breakCalls = 0,
|
|
freeRolling = false,
|
|
freeRollingAtAdd = {}
|
|
}
|
|
|
|
function value:getId() return self.id end
|
|
function value:getScript() return self.script end
|
|
function value:getScriptName() return self.scriptName end
|
|
function value:setScriptName(scriptName) self.scriptName = scriptName end
|
|
function value:getModData() return self.modData end
|
|
function value:transmitModData() end
|
|
function value:getMass() return 1000 end
|
|
function value:getBrakingForce() return 20 end
|
|
function value:getVehicleTowing() return self.towing end
|
|
function value:getVehicleTowedBy() return self.towedBy end
|
|
function value:getDriver() return self.driver end
|
|
function value:isDriver(player) return self.driver == player end
|
|
function value:addPointConstraint(player, other, attachmentA, attachmentB, localOnly)
|
|
self.addCalls = self.addCalls + 1
|
|
self.freeRollingAtAdd[#self.freeRollingAtAdd + 1] = other.freeRolling
|
|
expect(player == nil, "client rigid attach must not impersonate a player")
|
|
expect(attachmentA == "trailer" and attachmentB == "trailerfront",
|
|
"deferred attach must preserve its authoritative endpoints")
|
|
expect(localOnly == true, "deferred attach must remain client-local")
|
|
self.towing = other
|
|
other.towedBy = self
|
|
end
|
|
function value:breakConstraint()
|
|
self.breakCalls = self.breakCalls + 1
|
|
local other = self.towing or self.towedBy
|
|
if other then unlink(self, other) end
|
|
end
|
|
|
|
return value
|
|
end
|
|
|
|
local vehicles = {
|
|
[101] = vehicle(101),
|
|
[102] = vehicle(102),
|
|
[201] = vehicle(201),
|
|
[202] = vehicle(202)
|
|
}
|
|
|
|
getVehicleById = function(id) return vehicles[tonumber(id)] end
|
|
getPlayer = function() return localPlayer end
|
|
|
|
TowBarMod = {
|
|
Utils = { updateAttachmentsForRigidTow = function() end },
|
|
Hook = {
|
|
setVehicleScriptWithTowBarHidden = function(target, scriptName)
|
|
target:setScriptName(scriptName)
|
|
return true
|
|
end,
|
|
setVehiclePostAttach = function(_, target)
|
|
-- This stands in for applyFreeRollingTowState(). The ownership
|
|
-- handoff must run it before Bullet receives the local constraint,
|
|
-- matching the working wrecker order.
|
|
target.freeRolling = true
|
|
end,
|
|
cleanupDetachedTowBar = function() end
|
|
}
|
|
}
|
|
|
|
dofile("42.20/media/lua/client/TowBar/TowSyncClient.lua")
|
|
|
|
local function serverCommand(command, vehicleA, vehicleB)
|
|
local args = {
|
|
vehicleA = vehicleA:getId(),
|
|
vehicleB = vehicleB:getId(),
|
|
attachmentA = "trailer",
|
|
attachmentB = "trailerfront"
|
|
}
|
|
for i = 1, #serverCommandCallbacks do
|
|
serverCommandCallbacks[i]("towbar", command, args)
|
|
end
|
|
end
|
|
|
|
local function enterVehicle(player)
|
|
for i = 1, #enterVehicleCallbacks do
|
|
enterVehicleCallbacks[i](player)
|
|
end
|
|
end
|
|
|
|
local function spawnVehicle(value)
|
|
for i = 1, #spawnVehicleCallbacks do
|
|
spawnVehicleCallbacks[i](value)
|
|
end
|
|
end
|
|
|
|
-- Portable installation finishes while the player is standing beside the two
|
|
-- cars. At this point the local client does not own either vehicle's physics.
|
|
local towing, towed = vehicles[101], vehicles[102]
|
|
-- The server's ordinary towing relation may reach this client before the mod
|
|
-- command. It is usable as a visual snap, but replacing it before driver
|
|
-- ownership is precisely the multiplayer race this regression covers.
|
|
towing.towing = towed
|
|
towed.towedBy = towing
|
|
serverCommand("forceAttachSync", towing, towed)
|
|
|
|
expect(type(TowBarMod.Sync.desiredPairs) == "table",
|
|
"authoritative sync must retain desired pairs until this client owns the towing vehicle")
|
|
expect(towing.addCalls == 0 and towing.breakCalls == 0,
|
|
"sync received outside the driver seat must not create or replace local physics")
|
|
|
|
-- Entering as driver transfers physics ownership to this client. That event is
|
|
-- the first safe point to replace the server/native relation with rigid towing.
|
|
towing.driver = localPlayer
|
|
expect(#enterVehicleCallbacks == 1,
|
|
"portable sync must register one driver-entry ownership handoff")
|
|
enterVehicle(localPlayer)
|
|
|
|
expect(towing.addCalls == 1,
|
|
"becoming driver must apply the deferred rigid constraint exactly once")
|
|
expect(towing.breakCalls == 1,
|
|
"driver ownership handoff must replace the pre-existing native relation exactly once")
|
|
expect(towing:getVehicleTowing() == towed and towed:getVehicleTowedBy() == towing,
|
|
"driver ownership handoff must leave the authoritative pair linked")
|
|
expect(towing.freeRollingAtAdd[1] == true,
|
|
"towed free-roll state must be applied before the rigid constraint is created")
|
|
|
|
-- The user explicitly chose driver entry as a forced repair point. A later
|
|
-- entry rebuilds once, while the ordinary server snapshot remains idempotent.
|
|
enterVehicle(localPlayer)
|
|
serverCommand("forceAttachSync", towing, towed)
|
|
expect(towing.addCalls == 2 and towing.breakCalls == 2,
|
|
"each driver entry must force one rebuild while snapshots do not add another")
|
|
|
|
-- A detach can race the later driver entry. It must cancel the desired pair so
|
|
-- that entering the vehicle cannot resurrect a server-rejected tow.
|
|
local canceledTowing, canceledTowed = vehicles[201], vehicles[202]
|
|
serverCommand("forceAttachSync", canceledTowing, canceledTowed)
|
|
expect(canceledTowing.addCalls == 0 and canceledTowing.breakCalls == 0,
|
|
"a second outside-seat sync must also remain pending without touching physics")
|
|
-- The dedicated server may already have replicated its native relation. A
|
|
-- non-owning client must not break that relation while canceling its handoff.
|
|
canceledTowing.towing = canceledTowed
|
|
canceledTowed.towedBy = canceledTowing
|
|
serverCommand("forceDetachSync", canceledTowing, canceledTowed)
|
|
expect(TowBarMod.Sync.desiredPairs["201:202"] == nil,
|
|
"detach must remove the pending desired pair before any later seat event")
|
|
canceledTowing.driver = localPlayer
|
|
enterVehicle(localPlayer)
|
|
expect(canceledTowing.addCalls == 0 and canceledTowing.breakCalls == 0,
|
|
"detach must cancel pending ownership handoff without a non-owner break")
|
|
|
|
-- A restart snapshot can arrive before either vehicle has streamed in. Keep
|
|
-- that desired state and retry it from vehicle spawn when this player already
|
|
-- owns the towing vehicle.
|
|
local streamedTowing, streamedTowed = vehicle(301), vehicle(302)
|
|
serverCommand("forceAttachSync", streamedTowing, streamedTowed)
|
|
expect(TowBarMod.Sync.desiredPairs["301:302"] ~= nil,
|
|
"missing streamed peers must not discard the authoritative attach snapshot")
|
|
streamedTowing.driver = localPlayer
|
|
vehicles[301], vehicles[302] = streamedTowing, streamedTowed
|
|
spawnVehicle(streamedTowed)
|
|
expect(streamedTowing.addCalls == 1,
|
|
"vehicle streaming must apply one saved rigid handoff for an existing local driver")
|
|
|
|
if failures > 0 then os.exit(1) end
|
|
print("PASS: portable multiplayer rigid tow waits for driver physics ownership")
|