42.20 and Towtrucks
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
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 broadcast one restore")
|
||||
|
||||
now = 1500
|
||||
fireTicks(callbacks.tick, 5)
|
||||
expect(metrics.addConstraint == 1, "portable pending restore must not duplicate its constraint")
|
||||
expect(metrics.attachSync == 1, "portable pending restore must not duplicate its sync")
|
||||
|
||||
linkPair(towing, towed)
|
||||
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 restore 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")
|
||||
Reference in New Issue
Block a user