291 lines
10 KiB
Lua
291 lines
10 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
|
|
}, ";")
|
|
|
|
local runtime = "client"
|
|
isServer = function() return runtime == "server" end
|
|
isClient = function() return runtime == "client" end
|
|
getDebug = function() return false end
|
|
getTimestampMs = function() return 1000 end
|
|
|
|
local serverCommandCallbacks = {}
|
|
local clientPlayer = { onlineId = 17 }
|
|
Events = {
|
|
OnServerCommand = {
|
|
Add = function(callback)
|
|
serverCommandCallbacks[#serverCommandCallbacks + 1] = callback
|
|
end
|
|
},
|
|
OnSpawnVehicleEnd = { Add = function() end }
|
|
}
|
|
|
|
local function attachmentScript()
|
|
local attachments = { trailer = {}, trailerfront = {} }
|
|
return {
|
|
getAttachmentById = function(_, attachmentId)
|
|
return attachments[attachmentId]
|
|
end
|
|
}
|
|
end
|
|
|
|
local function vehicle(id, sqlId, acknowledgeConstraint)
|
|
local value = {
|
|
id = id,
|
|
sqlId = sqlId,
|
|
script = attachmentScript(),
|
|
scriptName = "Base.MultiplayerRegression" .. tostring(id),
|
|
modData = {},
|
|
towing = nil,
|
|
towedBy = nil,
|
|
constraintKind = nil,
|
|
acknowledgeConstraint = acknowledgeConstraint,
|
|
constraintCalls = 0
|
|
}
|
|
|
|
function value:getId() return self.id end
|
|
function value:getSqlId() return self.sqlId 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:isDriver(player) return self.driver == player end
|
|
function value:getTowAttachmentSelf() return nil end
|
|
function value:attachmentExist(attachmentId)
|
|
return self.script:getAttachmentById(attachmentId) ~= nil
|
|
end
|
|
function value:getX() return 0 end
|
|
function value:getY() return 0 end
|
|
function value:addPointConstraint(_, other)
|
|
self.constraintCalls = self.constraintCalls + 1
|
|
if self.acknowledgeConstraint then
|
|
self.towing = other
|
|
other.towedBy = self
|
|
self.constraintKind = runtime == "client" and "rigid" or "native"
|
|
other.constraintKind = self.constraintKind
|
|
end
|
|
end
|
|
function value:breakConstraint()
|
|
local other = self.towing or self.towedBy
|
|
self.towing, self.towedBy = nil, nil
|
|
self.constraintKind = nil
|
|
if other then
|
|
other.towing, other.towedBy = nil, nil
|
|
other.constraintKind = nil
|
|
end
|
|
end
|
|
|
|
return value
|
|
end
|
|
|
|
local clientVehicles = {
|
|
[101] = vehicle(101, 1001, true),
|
|
[102] = vehicle(102, 1002, true),
|
|
[201] = vehicle(201, 2001, false),
|
|
[202] = vehicle(202, 2002, false)
|
|
}
|
|
clientVehicles[101].driver = clientPlayer
|
|
clientVehicles[201].driver = clientPlayer
|
|
local serverVehicles = {
|
|
-- This models the dedicated-server behavior behind the live regression:
|
|
-- addPointConstraint is accepted, but the server getters do not expose the
|
|
-- relation before the authoritative client sync must be sent.
|
|
[101] = vehicle(101, 1001, false),
|
|
[102] = vehicle(102, 1002, false)
|
|
}
|
|
|
|
getVehicleById = function(id)
|
|
local vehicles = runtime == "server" and serverVehicles or clientVehicles
|
|
return vehicles[tonumber(id)]
|
|
end
|
|
getPlayer = function() return clientPlayer end
|
|
|
|
TowBarMod = {
|
|
Utils = {
|
|
updateAttachmentsForRigidTow = function() end
|
|
},
|
|
Hook = {
|
|
applyFreeRollingTowState = function() end,
|
|
setVehicleScriptWithTowBarHidden = function(target, scriptName)
|
|
target:setScriptName(scriptName)
|
|
return true
|
|
end,
|
|
setVehiclePostAttach = function() end,
|
|
cleanupDetachedTowBar = function() end
|
|
}
|
|
}
|
|
|
|
-- Load the real portable client sync and rigid-constraint implementation.
|
|
dofile("42.20/media/lua/client/TowBar/TowSyncClient.lua")
|
|
|
|
local globalData = {}
|
|
ModData = {
|
|
getOrCreate = function(name)
|
|
globalData[name] = globalData[name] or {}
|
|
return globalData[name]
|
|
end,
|
|
transmit = function() end
|
|
}
|
|
|
|
local serverCallbacks = {}
|
|
Events = {
|
|
OnClientCommand = { Add = function(callback) serverCallbacks.clientCommand = callback end },
|
|
OnTick = { Add = function(callback) serverCallbacks.tick = callback end }
|
|
}
|
|
|
|
runtime = "server"
|
|
|
|
local item = { id = 7001 }
|
|
function item:getID() return self.id end
|
|
function item:getFullType() return "TowBar.TowBar" end
|
|
|
|
local inventory = { items = { item } }
|
|
function inventory:getItemWithID(id)
|
|
return self.items[1] and self.items[1]:getID() == id and self.items[1] or nil
|
|
end
|
|
function inventory:getFirstTypeRecurse(fullType)
|
|
return self.items[1] and self.items[1]:getFullType() == fullType and self.items[1] or nil
|
|
end
|
|
function inventory:contains(candidate) return self.items[1] == candidate end
|
|
function inventory:Remove(candidate)
|
|
if self.items[1] == candidate then self.items[1] = nil end
|
|
end
|
|
function inventory:AddItem() return nil end
|
|
|
|
local player = { vehicle = serverVehicles[101] }
|
|
function player:getInventory() return inventory end
|
|
function player:getVehicle() return self.vehicle end
|
|
function player:getX() return 0 end
|
|
function player:getY() return 0 end
|
|
function player:isPrimaryHandItem() return false end
|
|
function player:isSecondaryHandItem() return false end
|
|
function player:removeFromHands() end
|
|
function player:setPrimaryHandItem() end
|
|
|
|
sendRemoveItemFromContainer = function() end
|
|
sendAddItemToContainer = function() end
|
|
sendEquip = function() end
|
|
|
|
local attachSyncCount = 0
|
|
local attachSyncPhases = {}
|
|
sendServerCommand = function(module, command, args)
|
|
if module == "towbar" and command == "forceAttachSync" then
|
|
attachSyncCount = attachSyncCount + 1
|
|
attachSyncPhases[#attachSyncPhases + 1] = args.phase
|
|
end
|
|
|
|
local previousRuntime = runtime
|
|
runtime = "client"
|
|
for i = 1, #serverCommandCallbacks do
|
|
serverCommandCallbacks[i](module, command, args)
|
|
end
|
|
runtime = previousRuntime
|
|
end
|
|
|
|
getCell = function()
|
|
return { getVehicles = function() return nil end }
|
|
end
|
|
|
|
-- Load the real server command handler after the client event handler has been
|
|
-- captured. Both halves now communicate through sendServerCommand above.
|
|
dofile("42.20/media/lua/server/TowingCommands.lua")
|
|
|
|
serverCallbacks.clientCommand("towbar", "attachTowBar", player, {
|
|
vehicleA = 101,
|
|
vehicleB = 102,
|
|
attachmentA = "trailer",
|
|
attachmentB = "trailerfront",
|
|
itemId = item:getID()
|
|
})
|
|
|
|
expect(serverVehicles[101].modData.isTowingByTowBar == true,
|
|
"accepted MP attach must publish towbar state for its visual")
|
|
expect(serverVehicles[101].constraintCalls == 1,
|
|
"accepted MP attach must still request the server-side logical constraint")
|
|
expect(attachSyncCount == 1,
|
|
"accepted MP attach must immediately send one forceAttachSync even when server towing getters lag")
|
|
expect(attachSyncPhases[1] == nil,
|
|
"portable MP attach must use the same single unphased sync as the working wrecker")
|
|
expect(clientVehicles[101]:getVehicleTowing() == clientVehicles[102]
|
|
and clientVehicles[102]:getVehicleTowedBy() == clientVehicles[101],
|
|
"the same command that shows the MP towbar visual must create the client rigid constraint")
|
|
expect(clientVehicles[101].constraintKind == "rigid",
|
|
"the one-shot sync must create rigid rather than native towing physics")
|
|
|
|
-- Like the working wrecker, server acknowledgement is bookkeeping only. It
|
|
-- must not introduce a second portable-only attach protocol or rebuild.
|
|
serverVehicles[101].towing = serverVehicles[102]
|
|
serverVehicles[102].towedBy = serverVehicles[101]
|
|
serverCallbacks.tick()
|
|
serverCallbacks.tick()
|
|
|
|
expect(attachSyncCount == 1,
|
|
"server acknowledgement must not send a second portable-only attach phase")
|
|
expect(clientVehicles[101].constraintCalls == 1,
|
|
"the wrecker-style attach must submit one local rigid constraint")
|
|
expect(clientVehicles[101].constraintKind == "rigid",
|
|
"the single attach sync must leave the client using rigid towing physics")
|
|
|
|
sendServerCommand("towbar", "forceAttachSync", {
|
|
vehicleA = 101,
|
|
vehicleB = 102,
|
|
attachmentA = "trailer",
|
|
attachmentB = "trailerfront"
|
|
})
|
|
expect(clientVehicles[101].constraintCalls == 1,
|
|
"duplicate snapshots must not repeatedly rebuild rigid physics")
|
|
|
|
-- The local add itself can also precede the engine's towing-getter update.
|
|
-- Reconciliation must adopt the now-visible exact pair without duplicating
|
|
-- the working wrecker-style single attach.
|
|
runtime = "client"
|
|
local delayedProvisional = {
|
|
vehicleA = 201,
|
|
vehicleB = 202,
|
|
attachmentA = "trailer",
|
|
attachmentB = "trailerfront"
|
|
}
|
|
for i = 1, #serverCommandCallbacks do
|
|
serverCommandCallbacks[i]("towbar", "forceAttachSync", delayedProvisional)
|
|
end
|
|
expect(clientVehicles[201].constraintCalls == 1
|
|
and clientVehicles[201]:getVehicleTowing() == nil,
|
|
"a rigid add may be accepted before its local getters update")
|
|
|
|
clientVehicles[201].towing = clientVehicles[202]
|
|
clientVehicles[202].towedBy = clientVehicles[201]
|
|
clientVehicles[201].constraintKind = "rigid"
|
|
clientVehicles[202].constraintKind = "rigid"
|
|
for i = 1, #serverCommandCallbacks do
|
|
serverCommandCallbacks[i]("towbar", "forceAttachSync", delayedProvisional)
|
|
end
|
|
expect(clientVehicles[201].constraintCalls == 1,
|
|
"delayed local getter acknowledgement must make the next snapshot idempotent")
|
|
|
|
clientVehicles[201].acknowledgeConstraint = true
|
|
for i = 1, #serverCommandCallbacks do
|
|
serverCommandCallbacks[i]("towbar", "forceAttachSync", delayedProvisional)
|
|
end
|
|
expect(clientVehicles[201].constraintCalls == 1
|
|
and clientVehicles[201].constraintKind == "rigid",
|
|
"duplicate sync after delayed local acceptance must remain idempotent")
|
|
runtime = "server"
|
|
|
|
if failures > 0 then os.exit(1) end
|
|
print("PASS: multiplayer portable towbar immediately synchronizes its rigid client constraint")
|