Working 42.20 MP
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
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 callbacks = {}
|
||||
Events = {
|
||||
OnClientCommand = { Add = function(callback) callbacks.clientCommand = callback end },
|
||||
OnTick = { Add = function(callback) callbacks.tick = callback end }
|
||||
}
|
||||
|
||||
isClient = function() return false end
|
||||
isServer = function() return true end
|
||||
getDebug = function() return false end
|
||||
getTimestampMs = function() return 1000 end
|
||||
|
||||
local metrics = {
|
||||
removals = 0,
|
||||
refunds = 0,
|
||||
attachSyncs = 0,
|
||||
detachSyncs = 0
|
||||
}
|
||||
|
||||
local nextItemId = 700
|
||||
local function towbarItem()
|
||||
nextItemId = nextItemId + 1
|
||||
local item = { id = nextItemId }
|
||||
function item:getID() return self.id end
|
||||
function item:getFullType() return "TowBar.TowBar" end
|
||||
return item
|
||||
end
|
||||
|
||||
local inventory = { items = { towbarItem() } }
|
||||
function inventory:getItemWithID(id)
|
||||
for i = 1, #self.items do
|
||||
if self.items[i]:getID() == id then return self.items[i] end
|
||||
end
|
||||
end
|
||||
function inventory:getFirstTypeRecurse(fullType)
|
||||
for i = 1, #self.items do
|
||||
if self.items[i]:getFullType() == fullType then return self.items[i] end
|
||||
end
|
||||
end
|
||||
function inventory:contains(item)
|
||||
for i = 1, #self.items do
|
||||
if self.items[i] == item then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
function inventory:Remove(item)
|
||||
for i = #self.items, 1, -1 do
|
||||
if self.items[i] == item then table.remove(self.items, i) end
|
||||
end
|
||||
end
|
||||
function inventory:AddItem(fullType)
|
||||
expect(fullType == "TowBar.TowBar", "refund must create the correct item type")
|
||||
local item = towbarItem()
|
||||
table.insert(self.items, item)
|
||||
return item
|
||||
end
|
||||
|
||||
local player = { primary = nil }
|
||||
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(item) return self.primary == item end
|
||||
function player:isSecondaryHandItem() return false end
|
||||
function player:removeFromHands(item)
|
||||
if self.primary == item then self.primary = nil end
|
||||
end
|
||||
function player:setPrimaryHandItem(item) self.primary = item end
|
||||
|
||||
sendRemoveItemFromContainer = function()
|
||||
metrics.removals = metrics.removals + 1
|
||||
end
|
||||
sendAddItemToContainer = function()
|
||||
metrics.refunds = metrics.refunds + 1
|
||||
end
|
||||
sendEquip = function() end
|
||||
|
||||
local function vehicle(id, sqlId, acknowledgeAttach)
|
||||
local value = {
|
||||
id = id,
|
||||
sqlId = sqlId,
|
||||
modData = {},
|
||||
towing = nil,
|
||||
towedBy = nil,
|
||||
acknowledgeAttach = acknowledgeAttach
|
||||
}
|
||||
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:getVehicleTowing() return self.towing end
|
||||
function value:getVehicleTowedBy() return self.towedBy end
|
||||
function value:getTowAttachmentSelf() return nil end
|
||||
function value:attachmentExist() return true end
|
||||
function value:getX() return 0 end
|
||||
function value:getY() return 0 end
|
||||
function value:addPointConstraint(_, other)
|
||||
if self.acknowledgeAttach then
|
||||
self.towing = other
|
||||
other.towedBy = self
|
||||
end
|
||||
end
|
||||
function value:breakConstraint()
|
||||
local other = self.towing or self.towedBy
|
||||
self.towing, self.towedBy = nil, nil
|
||||
if other then other.towing, other.towedBy = nil, nil end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
local vehicles = {}
|
||||
local loaded = {}
|
||||
getVehicleById = function(id) return vehicles[tonumber(id)] end
|
||||
getCell = function()
|
||||
return {
|
||||
getVehicles = function()
|
||||
return {
|
||||
iterator = function()
|
||||
local index = 0
|
||||
return {
|
||||
hasNext = function() return index < #loaded end,
|
||||
next = function()
|
||||
index = index + 1
|
||||
return loaded[index]
|
||||
end
|
||||
}
|
||||
end
|
||||
}
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
sendServerCommand = function(module, command)
|
||||
expect(module == "towbar", "accounting sync must use the towbar module")
|
||||
if command == "forceAttachSync" then
|
||||
metrics.attachSyncs = metrics.attachSyncs + 1
|
||||
elseif command == "forceDetachSync" then
|
||||
metrics.detachSyncs = metrics.detachSyncs + 1
|
||||
end
|
||||
end
|
||||
|
||||
dofile("42.20/media/lua/server/TowingCommands.lua")
|
||||
|
||||
local function command(name, args)
|
||||
callbacks.clientCommand("towbar", name, player, args)
|
||||
end
|
||||
|
||||
local function ticks(count)
|
||||
for _ = 1, count do callbacks.tick() end
|
||||
end
|
||||
|
||||
local function attachArgs(a, b, item)
|
||||
return {
|
||||
vehicleA = a:getId(),
|
||||
vehicleB = b:getId(),
|
||||
attachmentA = "trailer",
|
||||
attachmentB = "trailerfront",
|
||||
itemId = item and item:getID() or nil
|
||||
}
|
||||
end
|
||||
|
||||
-- A successful attach consumes exactly one physical item. Duplicate commands
|
||||
-- and confirmation ticks must never consume another one.
|
||||
local a, b = vehicle(101, 1001, true), vehicle(102, 1002, true)
|
||||
vehicles[101], vehicles[102] = a, b
|
||||
loaded = { a, b }
|
||||
player.vehicle = a
|
||||
local firstItem = inventory.items[1]
|
||||
command("attachTowBar", attachArgs(a, b, firstItem))
|
||||
command("attachTowBar", attachArgs(a, b, firstItem))
|
||||
ticks(8)
|
||||
expect(#inventory.items == 0, "successful attach must consume exactly one towbar")
|
||||
expect(metrics.removals == 1, "duplicate attach must not transmit a second item removal")
|
||||
expect(metrics.attachSyncs == 1,
|
||||
"successful pair must broadcast one immediate wrecker-style attach without consuming twice")
|
||||
|
||||
-- A normal detach refunds exactly that one reserved/installed item. Duplicate
|
||||
-- detaches are stale and cannot duplicate the refund.
|
||||
local detachArgs = { towingVehicle = a:getId(), vehicle = b:getId(), vehicleA = a:getId(), vehicleB = b:getId() }
|
||||
command("detachTowBar", detachArgs)
|
||||
command("detachTowBar", detachArgs)
|
||||
ticks(4)
|
||||
expect(#inventory.items == 1, "successful detach must refund exactly one towbar")
|
||||
expect(metrics.refunds == 1, "duplicate detach must not transmit a second refund")
|
||||
|
||||
-- The refunded object can be used immediately on the same pair. No enter/exit
|
||||
-- event or stale completion marker may prevent the second lifecycle.
|
||||
local secondItem = inventory.items[1]
|
||||
command("attachTowBar", attachArgs(a, b, secondItem))
|
||||
ticks(4)
|
||||
expect(#inventory.items == 0, "direct reattach must consume the one refunded towbar")
|
||||
expect(metrics.removals == 2, "direct reattach must account for one new removal")
|
||||
command("detachTowBar", detachArgs)
|
||||
ticks(4)
|
||||
expect(#inventory.items == 1, "second detach must return one towbar, not lose it")
|
||||
expect(metrics.refunds == 2, "second lifecycle must produce one new refund")
|
||||
|
||||
-- An immediate detach after the one-shot attach must remain detached. There is
|
||||
-- no portable-only queued confirmation allowed to recreate a ghost tow.
|
||||
local fastA, fastB = vehicle(151, 1501, true), vehicle(152, 1502, true)
|
||||
vehicles[151], vehicles[152] = fastA, fastB
|
||||
loaded = { fastA, fastB }
|
||||
player.vehicle = fastA
|
||||
local fastItem = inventory.items[1]
|
||||
local syncsBeforeFastDetach = metrics.attachSyncs
|
||||
local refundsBeforeFastDetach = metrics.refunds
|
||||
command("attachTowBar", attachArgs(fastA, fastB, fastItem))
|
||||
command("detachTowBar", {
|
||||
towingVehicle = fastA:getId(), vehicle = fastB:getId(),
|
||||
vehicleA = fastA:getId(), vehicleB = fastB:getId()
|
||||
})
|
||||
ticks(8)
|
||||
expect(fastA:getVehicleTowing() == nil and fastB:getVehicleTowedBy() == nil,
|
||||
"detach before confirmation must not be undone by a stale queued attach")
|
||||
expect(metrics.attachSyncs == syncsBeforeFastDetach + 1,
|
||||
"immediate detach must not be followed by another attach sync")
|
||||
expect(metrics.refunds == refundsBeforeFastDetach + 1 and #inventory.items == 1,
|
||||
"detach before confirmation must refund exactly one reserved towbar")
|
||||
|
||||
-- Match the wrecker's server-owned persistence model: once a validated attach
|
||||
-- is accepted, the towbar is installed and the saved pair keeps retrying. A
|
||||
-- delayed native getter must not refund the installed item behind the player.
|
||||
local failedA, failedB = vehicle(201, 2001, false), vehicle(202, 2002, false)
|
||||
vehicles[201], vehicles[202] = failedA, failedB
|
||||
loaded = { failedA, failedB }
|
||||
player.vehicle = failedA
|
||||
local failedItem = inventory.items[1]
|
||||
local refundsBeforeFailure = metrics.refunds
|
||||
command("attachTowBar", attachArgs(failedA, failedB, failedItem))
|
||||
ticks(30)
|
||||
expect(#inventory.items == 0, "accepted delayed attach must keep its towbar installed")
|
||||
expect(metrics.refunds == refundsBeforeFailure,
|
||||
"persistence retries must not mint an inventory refund")
|
||||
expect(failedA.modData.isTowingByTowBar == true
|
||||
and failedB.modData.isTowingByTowBar == true,
|
||||
"accepted delayed attach must preserve its recoverable logical pair")
|
||||
|
||||
command("detachTowBar", {
|
||||
towingVehicle = failedA:getId(), vehicle = failedB:getId(),
|
||||
vehicleA = failedA:getId(), vehicleB = failedB:getId()
|
||||
})
|
||||
expect(#inventory.items == 1 and metrics.refunds == refundsBeforeFailure + 1,
|
||||
"wrecker-style detach must refund an expected pair even while server getters lag")
|
||||
expect(failedA.modData.isTowingByTowBar == false
|
||||
and failedB.modData.isTowingByTowBar == false,
|
||||
"logical detach must clear delayed pair metadata")
|
||||
|
||||
if failures > 0 then os.exit(1) end
|
||||
print("PASS: towbar attach, retry, detach, and direct reattach account for exactly one item")
|
||||
Reference in New Issue
Block a user