307 lines
11 KiB
Lua
307 lines
11 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 = "42.20/media/lua/shared/?.lua;" .. package.path
|
|
|
|
local globalData = {}
|
|
ModData = {
|
|
getOrCreate = function(name)
|
|
globalData[name] = globalData[name] or {}
|
|
return globalData[name]
|
|
end,
|
|
transmit = function() end
|
|
}
|
|
|
|
TowBarMod = {}
|
|
require("TowBar/Persistence")
|
|
|
|
local function collection(items)
|
|
return {
|
|
iterator = function()
|
|
local index = 0
|
|
return {
|
|
hasNext = function() return index < #items end,
|
|
next = function()
|
|
index = index + 1
|
|
return items[index]
|
|
end
|
|
}
|
|
end
|
|
}
|
|
end
|
|
|
|
local function newMetrics()
|
|
return {
|
|
addConstraint = 0,
|
|
breakConstraint = 0,
|
|
attachSync = 0,
|
|
detachSync = 0,
|
|
worldDrops = 0
|
|
}
|
|
end
|
|
|
|
local function newVehicle(id, sqlId, metrics)
|
|
local vehicle = {
|
|
id = id,
|
|
sqlId = sqlId,
|
|
modData = {},
|
|
towing = nil,
|
|
towedBy = nil
|
|
}
|
|
|
|
function vehicle:getId() return self.id end
|
|
function vehicle:getSqlId() return self.sqlId end
|
|
function vehicle:getModData() return self.modData end
|
|
function vehicle:transmitModData() end
|
|
function vehicle:getVehicleTowing() return self.towing end
|
|
function vehicle:getVehicleTowedBy() return self.towedBy end
|
|
function vehicle:attachmentExist() return true end
|
|
function vehicle:addPointConstraint()
|
|
-- Deliberately do not establish the link. Build 42 may acknowledge a
|
|
-- new native constraint on a later tick.
|
|
metrics.addConstraint = metrics.addConstraint + 1
|
|
end
|
|
function vehicle:breakConstraint()
|
|
metrics.breakConstraint = metrics.breakConstraint + 1
|
|
self.towing = nil
|
|
self.towedBy = nil
|
|
end
|
|
function vehicle:getSquare()
|
|
return {
|
|
AddWorldInventoryItem = function()
|
|
metrics.worldDrops = metrics.worldDrops + 1
|
|
return {}
|
|
end
|
|
}
|
|
end
|
|
|
|
return vehicle
|
|
end
|
|
|
|
local function linkPair(towing, towed)
|
|
towing.towing = towed
|
|
towing.towedBy = nil
|
|
towed.towing = nil
|
|
towed.towedBy = towing
|
|
end
|
|
|
|
local function unlinkPair(towing, towed)
|
|
towing.towing = nil
|
|
towing.towedBy = nil
|
|
towed.towing = nil
|
|
towed.towedBy = nil
|
|
end
|
|
|
|
local function installEvents()
|
|
local callbacks = {}
|
|
Events = {
|
|
OnClientCommand = {
|
|
Add = function(callback) callbacks.clientCommand = callback end
|
|
},
|
|
OnTick = {
|
|
Add = function(callback) callbacks.tick = callback end
|
|
}
|
|
}
|
|
return callbacks
|
|
end
|
|
|
|
local function fireTicks(callback, count)
|
|
for _ = 1, count do callback() end
|
|
end
|
|
|
|
local function resetRegistry()
|
|
globalData["TowBar.PersistentPairsV2"] = { pairs = {} }
|
|
end
|
|
|
|
local function runPortableHandlerSpec()
|
|
resetRegistry()
|
|
local metrics = newMetrics()
|
|
local callbacks = installEvents()
|
|
local now = 1000
|
|
local towing = newVehicle(101, 1001, metrics)
|
|
local towed = newVehicle(202, 2002, metrics)
|
|
local loaded = { towing, towed }
|
|
local byId = { [101] = towing, [202] = towed }
|
|
|
|
isClient = function() return false end
|
|
isServer = function() return true end
|
|
getDebug = function() return false end
|
|
getTimestampMs = function() return now end
|
|
getCell = function()
|
|
return { getVehicles = function() return collection(loaded) end }
|
|
end
|
|
getVehicleById = function(id) return byId[id] end
|
|
sendServerCommand = function(_, command)
|
|
if command == "forceAttachSync" then
|
|
metrics.attachSync = metrics.attachSync + 1
|
|
elseif command == "forceDetachSync" or command == "spontaneousDetachSync" then
|
|
metrics.detachSync = metrics.detachSync + 1
|
|
end
|
|
end
|
|
|
|
expect(
|
|
TowBarMod.Persistence.savePair(
|
|
"towbar", towing, towed, "trailer", "trailerfront"
|
|
),
|
|
"portable setup must persist a stable pair"
|
|
)
|
|
dofile("42.20/media/lua/server/TowingCommands.lua")
|
|
expect(type(callbacks.tick) == "function", "portable server must register its audit tick")
|
|
|
|
fireTicks(callbacks.tick, 5)
|
|
expect(metrics.addConstraint == 1, "portable restart must request one native restore")
|
|
expect(metrics.attachSync == 1,
|
|
"portable restart must immediately synchronize one wrecker-style rigid restore")
|
|
|
|
-- The restore is queued before the native call. Once Build 42 exposes the
|
|
-- reciprocal relation, the pending confirmation broadcasts exactly once.
|
|
linkPair(towing, towed)
|
|
now = 1100
|
|
fireTicks(callbacks.tick, 1)
|
|
expect(metrics.addConstraint == 1,
|
|
"portable confirmation must adopt the first native add without re-adding")
|
|
expect(metrics.attachSync == 1,
|
|
"portable native acknowledgement must not introduce a second sync phase")
|
|
|
|
now = 1500
|
|
fireTicks(callbacks.tick, 4)
|
|
expect(metrics.addConstraint == 1, "portable pending restore must not duplicate its constraint")
|
|
expect(metrics.attachSync == 1, "portable pending restore must not duplicate its sync")
|
|
|
|
now = 2000
|
|
fireTicks(callbacks.tick, 5)
|
|
expect(metrics.addConstraint == 1, "portable delayed acknowledgement must be adopted without re-adding")
|
|
expect(towing.modData.towBarTowedVehicleId == towed:getId(), "portable adoption must refresh the towing live ID")
|
|
expect(towed.modData.towBarTowingVehicleId == towing:getId(), "portable adoption must refresh the towed live ID")
|
|
|
|
unlinkPair(towing, towed)
|
|
loaded = { towing }
|
|
byId[202] = nil
|
|
now = 10000
|
|
fireTicks(callbacks.tick, 5)
|
|
now = 16000
|
|
fireTicks(callbacks.tick, 5)
|
|
expect(metrics.worldDrops == 0, "an unloaded portable peer must not drop a towbar")
|
|
expect(metrics.detachSync == 0, "an unloaded portable peer must not broadcast destructive cleanup")
|
|
expect(towing.modData.isTowingByTowBar == true, "an unloaded portable peer must keep active metadata")
|
|
expect(towing.modData.towBarTowedVehicleSqlId == towed:getSqlId(), "an unloaded portable peer must keep stable identity")
|
|
|
|
loaded = { towing, towed }
|
|
byId[202] = towed
|
|
now = 17000
|
|
fireTicks(callbacks.tick, 5)
|
|
expect(metrics.addConstraint == 2, "a reloaded portable peer must re-enter recovery")
|
|
expect(metrics.attachSync == 2,
|
|
"a reloaded portable peer must receive one fresh wrecker-style restore sync")
|
|
|
|
linkPair(towing, towed)
|
|
now = 17100
|
|
fireTicks(callbacks.tick, 1)
|
|
expect(metrics.addConstraint == 2,
|
|
"reloaded portable confirmation must adopt without another native add")
|
|
expect(metrics.attachSync == 2,
|
|
"reloaded portable native acknowledgement must not duplicate its sync")
|
|
|
|
now = 17500
|
|
fireTicks(callbacks.tick, 4)
|
|
expect(metrics.addConstraint == 2,
|
|
"confirmed reloaded pair must not duplicate its native add")
|
|
expect(metrics.attachSync == 2,
|
|
"confirmed reloaded pair must not duplicate its authoritative sync")
|
|
expect(metrics.worldDrops == 0, "portable peer reload must not be mistaken for a break")
|
|
expect(metrics.breakConstraint == 0, "portable peer reload must not break an unrelated constraint")
|
|
end
|
|
|
|
local function runWreckerHandlerSpec()
|
|
resetRegistry()
|
|
local metrics = newMetrics()
|
|
local callbacks = installEvents()
|
|
local now = 1000
|
|
local wrecker = newVehicle(303, 3003, metrics)
|
|
local target = newVehicle(404, 4004, metrics)
|
|
local loaded = { wrecker, target }
|
|
local byId = { [303] = wrecker, [404] = target }
|
|
|
|
TowBarMod.Wrecker = {
|
|
getWorldVehicles = function() return collection(loaded) end,
|
|
isSupportedWrecker = function(vehicle) return vehicle == wrecker end,
|
|
normalizeHeightLevel = function(level)
|
|
return math.max(0, math.min(2, tonumber(level) or 0))
|
|
end,
|
|
getHeightAttachmentId = function(level)
|
|
return ({ [0] = "towbarWreckerHookLow", [1] = "towbarWreckerHookMid", [2] = "towbarWreckerHookHigh" })[level]
|
|
end
|
|
}
|
|
|
|
isClient = function() return false end
|
|
isServer = function() return true end
|
|
getTimestampMs = function() return now end
|
|
getVehicleById = function(id) return byId[id] end
|
|
sendServerCommand = function(_, command)
|
|
if command == "wreckerAttachSync" then
|
|
metrics.attachSync = metrics.attachSync + 1
|
|
elseif command == "wreckerDetachSync" then
|
|
metrics.detachSync = metrics.detachSync + 1
|
|
end
|
|
end
|
|
|
|
expect(
|
|
TowBarMod.Persistence.savePair(
|
|
"wrecker", wrecker, target,
|
|
"towbarWreckerHookHigh", "trailerfront", 2
|
|
),
|
|
"wrecker setup must persist a stable pair"
|
|
)
|
|
dofile("42.20/media/lua/server/WreckerCommands.lua")
|
|
expect(type(callbacks.tick) == "function", "wrecker server must register its audit tick")
|
|
|
|
fireTicks(callbacks.tick, 30)
|
|
expect(metrics.addConstraint == 1, "wrecker restart must request one native restore")
|
|
expect(metrics.attachSync == 1, "wrecker restart must broadcast one restore")
|
|
|
|
now = 1500
|
|
fireTicks(callbacks.tick, 30)
|
|
expect(metrics.addConstraint == 1, "wrecker pending restore must not duplicate its constraint")
|
|
expect(metrics.attachSync == 1, "wrecker pending restore must not duplicate its sync")
|
|
|
|
linkPair(wrecker, target)
|
|
now = 2000
|
|
fireTicks(callbacks.tick, 30)
|
|
expect(metrics.addConstraint == 1, "wrecker delayed acknowledgement must be adopted without re-adding")
|
|
expect(wrecker.modData.wreckerHeightLevel == 2, "wrecker adoption must preserve the saved hook height")
|
|
expect(wrecker.modData.wreckerTargetAttachment == "trailerfront", "wrecker adoption must preserve the target endpoint")
|
|
|
|
unlinkPair(wrecker, target)
|
|
loaded = { wrecker }
|
|
byId[404] = nil
|
|
now = 10000
|
|
fireTicks(callbacks.tick, 30)
|
|
now = 16000
|
|
fireTicks(callbacks.tick, 30)
|
|
expect(metrics.detachSync == 0, "an unloaded wrecker peer must not broadcast destructive cleanup")
|
|
expect(wrecker.modData.wreckerTowActive == true, "an unloaded wrecker peer must keep active metadata")
|
|
expect(wrecker.modData.wreckerTowedVehicleSqlId == target:getSqlId(), "an unloaded wrecker peer must keep stable identity")
|
|
|
|
loaded = { wrecker, target }
|
|
byId[404] = target
|
|
now = 17000
|
|
fireTicks(callbacks.tick, 30)
|
|
expect(metrics.addConstraint == 2, "a reloaded wrecker peer must re-enter recovery")
|
|
expect(metrics.attachSync == 2, "a reloaded wrecker peer must receive one fresh restore sync")
|
|
expect(metrics.detachSync == 0, "wrecker peer reload must not be mistaken for a break")
|
|
expect(metrics.worldDrops == 0, "wrecker recovery must never create a portable towbar item")
|
|
expect(metrics.breakConstraint == 0, "wrecker peer reload must not break an unrelated constraint")
|
|
end
|
|
|
|
runPortableHandlerSpec()
|
|
runWreckerHandlerSpec()
|
|
|
|
if failures > 0 then os.exit(1) end
|
|
print("PASS: production persistence handlers debounce recovery and survive peer unload")
|