250 lines
8.5 KiB
Lua
250 lines
8.5 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 = {}
|
|
local tickCallbacks = {}
|
|
Events = {
|
|
OnServerCommand = {
|
|
Add = function(callback)
|
|
serverCommandCallbacks[#serverCommandCallbacks + 1] = callback
|
|
end
|
|
},
|
|
OnEnterVehicle = {
|
|
Add = function(callback)
|
|
enterVehicleCallbacks[#enterVehicleCallbacks + 1] = callback
|
|
end
|
|
},
|
|
OnSwitchVehicleSeat = { Add = function() end },
|
|
OnSpawnVehicleEnd = {
|
|
Add = function(callback)
|
|
spawnVehicleCallbacks[#spawnVehicleCallbacks + 1] = callback
|
|
end
|
|
},
|
|
OnTick = {
|
|
Add = function(callback)
|
|
tickCallbacks[#tickCallbacks + 1] = callback
|
|
end
|
|
}
|
|
}
|
|
|
|
local localPlayer = { vehicle = nil }
|
|
function localPlayer:getVehicle() return self.vehicle end
|
|
|
|
local attachmentIds = {
|
|
trailer = true,
|
|
trailerfront = true,
|
|
towbarWreckerHookLow = true,
|
|
towbarWreckerHookMid = true,
|
|
towbarWreckerHookHigh = true
|
|
}
|
|
|
|
local function unlink(vehicleA, vehicleB)
|
|
vehicleA.towing, vehicleA.towedBy = nil, nil
|
|
vehicleB.towing, vehicleB.towedBy = nil, nil
|
|
end
|
|
|
|
local function link(vehicleA, vehicleB)
|
|
vehicleA.towing, vehicleA.towedBy = vehicleB, nil
|
|
vehicleB.towing, vehicleB.towedBy = nil, vehicleA
|
|
end
|
|
|
|
local function newVehicle(id, sqlId)
|
|
local value = {
|
|
id = id,
|
|
sqlId = sqlId,
|
|
modData = {},
|
|
scriptName = "Base.DriverEntry" .. tostring(id),
|
|
mass = 1000,
|
|
brakingForce = 20,
|
|
towing = nil,
|
|
towedBy = nil,
|
|
driver = nil,
|
|
addCalls = 0,
|
|
breakCalls = 0
|
|
}
|
|
|
|
local script = {
|
|
getAttachmentById = function(_, attachmentId)
|
|
return attachmentIds[attachmentId] and {} or nil
|
|
end
|
|
}
|
|
|
|
function value:getId() return self.id end
|
|
function value:getSqlId() return self.sqlId end
|
|
function value:getModData() return self.modData end
|
|
function value:transmitModData() end
|
|
function value:getScript() return script end
|
|
function value:getScriptName() return self.scriptName end
|
|
function value:setScriptName(scriptName) self.scriptName = scriptName end
|
|
function value:getMass() return self.mass end
|
|
function value:setMass(mass) self.mass = mass end
|
|
function value:getBrakingForce() return self.brakingForce end
|
|
function value:setBrakingForce(force) self.brakingForce = force end
|
|
function value:isParkingBrakeOn() return false end
|
|
function value:setParkingBrakeOn() end
|
|
function value:getParkingBrake() return false end
|
|
function value:setParkingBrake() end
|
|
function value:isHandbrake() return false end
|
|
function value:setHandbrake() end
|
|
function value:getVehicleTowing() return self.towing end
|
|
function value:getVehicleTowedBy() return self.towedBy end
|
|
function value:isDriver(player) return self.driver == player end
|
|
function value:attachmentExist(attachmentId)
|
|
return attachmentIds[attachmentId] == true
|
|
end
|
|
function value:addPointConstraint(_, other)
|
|
self.addCalls = self.addCalls + 1
|
|
link(self, other)
|
|
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 portableTowing = newVehicle(101, 1001)
|
|
local portableTowed = newVehicle(102, 1002)
|
|
local wrecker = newVehicle(201, 2001)
|
|
local wreckerTarget = newVehicle(202, 2002)
|
|
local unrelated = newVehicle(301, 3001)
|
|
local allVehicles = {
|
|
portableTowing,
|
|
portableTowed,
|
|
wrecker,
|
|
wreckerTarget,
|
|
unrelated
|
|
}
|
|
local vehiclesById = {}
|
|
for _, vehicle in ipairs(allVehicles) do vehiclesById[vehicle:getId()] = vehicle end
|
|
|
|
getPlayer = function() return localPlayer end
|
|
getVehicleById = function(id) return vehiclesById[tonumber(id)] end
|
|
getCell = function()
|
|
return { getVehicles = function() return allVehicles end }
|
|
end
|
|
|
|
TowBarMod = {
|
|
Utils = { updateAttachmentsForRigidTow = function() end },
|
|
Hook = {
|
|
applyFreeRollingTowState = function() end,
|
|
setVehiclePostAttach = function() end,
|
|
setVehicleScriptWithTowBarHidden = function(vehicle, scriptName)
|
|
vehicle:setScriptName(scriptName)
|
|
return true
|
|
end,
|
|
cleanupDetachedTowBar = function() end
|
|
},
|
|
Wrecker = {
|
|
getWorldVehicles = function() return allVehicles end,
|
|
getHeightAttachmentId = function(level)
|
|
return ({
|
|
[0] = "towbarWreckerHookLow",
|
|
[1] = "towbarWreckerHookMid",
|
|
[2] = "towbarWreckerHookHigh"
|
|
})[tonumber(level) or 0]
|
|
end
|
|
}
|
|
}
|
|
|
|
-- Model a fully persisted portable pair whose transient sync cache was lost
|
|
-- during reload. The exact native relation is deliberately present: entry
|
|
-- must replace it with one fresh client-owned rigid constraint.
|
|
portableTowing.modData = {
|
|
isTowingByTowBar = true,
|
|
towed = false,
|
|
towBarTowedVehicleId = portableTowed:getId(),
|
|
towBarTowedVehicleSqlId = portableTowed:getSqlId(),
|
|
towBarExpectedAttachment = "trailer"
|
|
}
|
|
portableTowed.modData = {
|
|
isTowingByTowBar = true,
|
|
towed = true,
|
|
towBarTowingVehicleId = portableTowing:getId(),
|
|
towBarTowingVehicleSqlId = portableTowing:getSqlId(),
|
|
towBarExpectedAttachment = "trailerfront",
|
|
towBarOriginalScriptName = portableTowed:getScriptName()
|
|
}
|
|
link(portableTowing, portableTowed)
|
|
|
|
-- Model the equivalent persisted wrecker pair, also without applied-level
|
|
-- cache state. Both members carry the reciprocal identity written by the
|
|
-- authoritative wrecker attach path.
|
|
wrecker.modData = {
|
|
wreckerTowActive = true,
|
|
wreckerTowedVehicleId = wreckerTarget:getId(),
|
|
wreckerTowedVehicleSqlId = wreckerTarget:getSqlId(),
|
|
wreckerTargetAttachment = "trailerfront",
|
|
wreckerHeightLevel = 2
|
|
}
|
|
wreckerTarget.modData = {
|
|
wreckerTowingVehicleId = wrecker:getId(),
|
|
wreckerTowingVehicleSqlId = wrecker:getSqlId(),
|
|
wreckerOriginalScriptName = wreckerTarget:getScriptName()
|
|
}
|
|
link(wrecker, wreckerTarget)
|
|
|
|
dofile("42.20/media/lua/client/TowBar/TowSyncClient.lua")
|
|
dofile("42.20/media/lua/client/TowBar/WreckerSyncClient.lua")
|
|
|
|
expect(next(TowBarMod.Sync.desiredPairs) == nil,
|
|
"portable regression setup must not depend on transient desired-pair sync")
|
|
expect(next(TowBarMod.Sync.appliedPairs) == nil,
|
|
"portable regression setup must begin without an applied-pair cache")
|
|
expect(next(TowBarMod.WreckerSync.appliedLevels) == nil,
|
|
"wrecker regression setup must begin without an applied-level cache")
|
|
|
|
local function enterAsDriver(vehicle)
|
|
for _, candidate in ipairs(allVehicles) do candidate.driver = nil end
|
|
vehicle.driver = localPlayer
|
|
localPlayer.vehicle = vehicle
|
|
for i = 1, #enterVehicleCallbacks do
|
|
enterVehicleCallbacks[i](localPlayer)
|
|
end
|
|
end
|
|
|
|
enterAsDriver(portableTowing)
|
|
expect(portableTowing.breakCalls == 1 and portableTowing.addCalls == 1,
|
|
"portable driver entry must force exactly one rebuild from persisted modData")
|
|
enterAsDriver(portableTowing)
|
|
expect(portableTowing.breakCalls == 2 and portableTowing.addCalls == 2,
|
|
"each later portable driver entry must force one new rebuild, not remain cache-idempotent")
|
|
|
|
enterAsDriver(wrecker)
|
|
expect(wrecker.breakCalls == 1 and wrecker.addCalls == 1,
|
|
"wrecker driver entry must force exactly one rebuild from persisted modData")
|
|
enterAsDriver(wrecker)
|
|
expect(wrecker.breakCalls == 2 and wrecker.addCalls == 2,
|
|
"each later wrecker driver entry must force one new rebuild, not remain cache-idempotent")
|
|
|
|
local portableCounts = portableTowing.breakCalls + portableTowing.addCalls
|
|
local wreckerCounts = wrecker.breakCalls + wrecker.addCalls
|
|
enterAsDriver(unrelated)
|
|
expect(portableTowing.breakCalls + portableTowing.addCalls == portableCounts
|
|
and wrecker.breakCalls + wrecker.addCalls == wreckerCounts,
|
|
"entering an unrelated vehicle must not rebuild any persisted tow pair")
|
|
expect(#tickCallbacks == 0,
|
|
"driver-entry recovery must remain event-driven and must not install a tick retry loop")
|
|
|
|
if failures > 0 then os.exit(1) end
|
|
print("PASS: driver entry force-reattaches persisted portable and wrecker pairs once")
|