Working 42.20 MP

This commit is contained in:
2026-08-19 12:12:12 -04:00
parent e403323017
commit e99f3bbff9
22 changed files with 2262 additions and 825 deletions
+68 -2
View File
@@ -2,6 +2,8 @@ BTtow = {}
BTtow.Create = {}
BTtow.Init = {}
require("TowBar/VehicleCompatibility")
local TowbarVariantSize = 24
local TowbarMaxIndex = TowbarVariantSize - 1
local TowbarFirstZ = 1.0
@@ -13,6 +15,8 @@ local TowbarVisualScale = 2.5
local TowbarModelLength = 0.9714089036
local TowbarScaledModelLength = TowbarModelLength * TowbarVisualScale
local TowbarModelHalfLength = TowbarScaledModelLength / 2
local VanillaScaleMin = 1.5
local VanillaScaleMax = 2.0
local function getTowbarFrontEdgeZ(script)
if not script then return nil end
@@ -53,6 +57,59 @@ local function getTowbarModelSlot(script)
return math.max(0, math.min(TowbarMaxIndex, index))
end
local function getVehicleModelScale(script)
if not script then return nil end
local ok, result = pcall(function() return script:getModelScale() end)
if ok and type(result) == "number" then return result end
ok, result = pcall(function()
local model = script:getModel()
return model and model:getScale() or nil
end)
return ok and type(result) == "number" and result or nil
end
local function isVanillaScale(script)
local modelScale = getVehicleModelScale(script)
if modelScale == nil then return true end
local configuredMin = TowBarMod.Config and tonumber(TowBarMod.Config.vanillaTowbarModelScaleMin)
local configuredMax = TowBarMod.Config and tonumber(TowBarMod.Config.vanillaTowbarModelScaleMax)
return modelScale >= (configuredMin or VanillaScaleMin)
and modelScale <= (configuredMax or VanillaScaleMax)
end
local function getTowbarIndexVanilla(script)
if not script then return nil end
local ok, shape = pcall(function() return script:getPhysicsChassisShape() end)
if not ok or not shape then return nil end
local zOk, shapeZ = pcall(function() return shape:z() end)
if not zOk or type(shapeZ) ~= "number" then return nil end
local z = shapeZ / 2 - 0.1
local index = math.floor((z * 2 / 3 - 1) * 10)
return math.max(0, math.min(TowbarMaxIndex, index))
end
local function getTowbarIndexSmallScale(script)
if not script then return nil end
local maxAbsTowZ = nil
local trailer = script:getAttachmentById("trailer")
if trailer then maxAbsTowZ = math.abs(trailer:getOffset():z()) end
local trailerFront = script:getAttachmentById("trailerfront")
if trailerFront then
local frontAbsZ = math.abs(trailerFront:getOffset():z())
if not maxAbsTowZ or frontAbsZ > maxAbsTowZ then maxAbsTowZ = frontAbsZ end
end
if maxAbsTowZ == nil then return nil end
local index = math.floor((maxAbsTowZ + 0.1 - 1.0) * 10)
return math.max(0, math.min(TowbarMaxIndex, index))
end
local function getLegacyTowbarModelSlot(script)
local useNormalPart = isVanillaScale(script)
local index = getTowbarIndexVanilla(script)
if not useNormalPart then index = getTowbarIndexSmallScale(script) or index end
return index, useNormalPart
end
function BTtow.Create.towbar(vehicle, part)
if part == nil then return end
for j=0, TowbarVariantSize - 1 do
@@ -72,8 +129,17 @@ function BTtow.Init.towbar(vehicle, part)
if modData and modData["isTowingByTowBar"] and modData["towed"] then
local script = vehicle:getScript()
if script then
local index = getTowbarModelSlot(script)
local shouldShowOnThisPart = part:getId() == "towbar"
local isKi5 = TowBarMod.Compatibility.isKi5Vehicle(vehicle)
local index, useNormalPart
if isKi5 then
index = getTowbarModelSlot(script)
else
index, useNormalPart = getLegacyTowbarModelSlot(script)
end
local partId = part:getId()
local shouldShowOnThisPart = (isKi5 and partId == "towbarKI5")
or (not isKi5 and useNormalPart and partId == "towbar")
or (not isKi5 and not useNormalPart and partId == "towbarLarge")
if shouldShowOnThisPart and index ~= nil then
part:setModelVisible("towbar" .. index, true)
end
+19 -150
View File
@@ -4,10 +4,8 @@ require("TowBar/Persistence")
local TowingCommands = {}
local Commands = {}
local TowBarItemType = "TowBar.TowBar"
local SyncDelayTicks = 2
local SnapshotIntervalTicks = 120
local BrokenPairAuditIntervalTicks = 5
local pendingSync = {}
local snapshotTickCounter = 0
local brokenPairAuditTickCounter = 0
local confirmedTowPairs = {}
@@ -24,57 +22,6 @@ local noise = function(msg)
end
end
local function queueSync(kind, player, args, reservedTowBar)
if not args then return end
table.insert(pendingSync, {
kind = kind,
ticks = SyncDelayTicks,
attempts = 0,
reservedTowBar = reservedTowBar == true,
player = player,
args = args
})
end
local function cancelPendingAttach(vehicleA, vehicleB)
if not vehicleA or not vehicleB then return end
local vehicleAId = vehicleA:getId()
local vehicleBId = vehicleB:getId()
local remaining = {}
for i = 1, #pendingSync do
local item = pendingSync[i]
local itemArgs = item.args or {}
local isThisAttach = item.kind == "attach"
and itemArgs.vehicleA == vehicleAId
and itemArgs.vehicleB == vehicleBId
if isThisAttach then
-- The reserved item is about to become the single ground drop.
item.reservedTowBar = false
else
table.insert(remaining, item)
end
end
pendingSync = remaining
end
local function hasPendingAttach(vehicleA, vehicleB)
if not vehicleA or not vehicleB then return false end
local vehicleAId = vehicleA:getId()
local vehicleBId = vehicleB:getId()
for i = 1, #pendingSync do
local item = pendingSync[i]
local itemArgs = item.args or {}
if item.kind == "attach"
and itemArgs.vehicleA == vehicleAId
and itemArgs.vehicleB == vehicleBId then
return true
end
end
return false
end
local function resolveAttachmentA(args, vehicleA)
if args and args.attachmentA then return args.attachmentA end
if vehicleA and vehicleA:getTowAttachmentSelf() then return vehicleA:getTowAttachmentSelf() end
@@ -369,10 +316,16 @@ local function broadcastAttach(vehicleA, vehicleB, attachmentA, attachmentB)
end
local function broadcastDetach(vehicleAId, vehicleBId)
sendServerCommand("towbar", "forceDetachSync", {
local args = {
vehicleA = vehicleAId,
vehicleB = vehicleBId
})
}
if isServer() then
sendServerCommand("towbar", "forceDetachSync", args)
elseif not isClient() and TowBarMod and TowBarMod.Sync
and TowBarMod.Sync.applyDetachSync then
TowBarMod.Sync.applyDetachSync(args)
end
end
local function broadcastSpontaneousDetach(vehicleA, vehicleB)
@@ -458,7 +411,6 @@ local function finalizeBrokenTowBarPair(towingVehicle, towedVehicle, reason)
return false
end
cancelPendingAttach(towingVehicle, towedVehicle)
forgetTowBarPair(towingVehicle, towedVehicle)
breakTowBarConstraint(towingVehicle, towedVehicle)
clearExpectedTowBarPair(towingVehicle, towedVehicle)
@@ -567,54 +519,6 @@ local function reconcileBrokenTowBarPairsServer()
end)
end
local function processAttachSync(item)
local args = item.args or {}
local vehicleA = args.vehicleA and getVehicleById(args.vehicleA) or nil
local vehicleB = args.vehicleB and getVehicleById(args.vehicleB) or nil
if not vehicleA or not vehicleB then
noise("attach sync skipped missing vehicles A=" .. tostring(args.vehicleA) .. " B=" .. tostring(args.vehicleB))
return "failed"
end
local attachmentA = resolveAttachmentA(args, vehicleA)
local attachmentB = resolveAttachmentB(args, vehicleB)
if not isLinked(vehicleA, vehicleB) then
if isTowBarPairConfirmed(vehicleA, vehicleB) then return "broken" end
if item.attempts >= 3 then return "failed" end
vehicleA:addPointConstraint(item.player, vehicleB, attachmentA, attachmentB)
return "retry"
end
markExpectedTowBarPair(vehicleA, vehicleB, attachmentA, attachmentB)
markTowBarPairConfirmed(vehicleA, vehicleB)
broadcastAttach(vehicleA, vehicleB, attachmentA, attachmentB)
return "complete"
end
local function failAttachSync(item)
local args = item.args or {}
local vehicleA = args.vehicleA and getVehicleById(args.vehicleA) or nil
local vehicleB = args.vehicleB and getVehicleById(args.vehicleB) or nil
if isLinked(vehicleA, vehicleB) then
vehicleA:breakConstraint(true, false)
end
clearExpectedTowBarPair(vehicleA, vehicleB)
broadcastDetach(args.vehicleA, args.vehicleB)
if item.reservedTowBar then
giveTowBar(item.player, true)
item.reservedTowBar = false
end
end
local function processDetachSync(item)
local args = item.args or {}
local vehicleAId = args.towingVehicle or args.vehicleA or args.vehicle
local vehicleBId = args.vehicleB or args.vehicle
broadcastDetach(vehicleAId, vehicleBId)
end
local function snapshotActiveTowbarLinksServer()
local cell = getCell()
if not cell then return end
@@ -643,7 +547,7 @@ local function snapshotActiveTowbarLinksServer()
end)
end
local function processPendingSync()
local function processTowBarServerTick()
reconcileBrokenTowBarPairsServer()
snapshotTickCounter = snapshotTickCounter + 1
@@ -651,37 +555,6 @@ local function processPendingSync()
snapshotTickCounter = 0
snapshotActiveTowbarLinksServer()
end
if #pendingSync == 0 then return end
local remaining = {}
for i = 1, #pendingSync do
local item = pendingSync[i]
item.ticks = item.ticks - 1
if item.ticks <= 0 then
if item.kind == "attach" then
local status = processAttachSync(item)
if status == "retry" and item.attempts < 3 then
item.attempts = item.attempts + 1
item.ticks = SyncDelayTicks
table.insert(remaining, item)
elseif status == "broken" then
finalizeBrokenTowBarPair(
item.args.vehicleA and getVehicleById(item.args.vehicleA) or nil,
item.args.vehicleB and getVehicleById(item.args.vehicleB) or nil,
"attach-confirmation-break"
)
elseif status ~= "complete" then
failAttachSync(item)
end
elseif item.kind == "detach" then
processDetachSync(item)
end
else
table.insert(remaining, item)
end
end
pendingSync = remaining
end
function Commands.attachTowBar(player, args)
@@ -707,9 +580,7 @@ function Commands.attachTowBar(player, args)
return
end
if isExpectedTowBarPair(vehicleA, vehicleB)
or isExpectedTowBarPair(vehicleB, vehicleA)
or hasPendingAttach(vehicleA, vehicleB)
or hasPendingAttach(vehicleB, vehicleA) then
or isExpectedTowBarPair(vehicleB, vehicleA) then
noise("rejected duplicate pending towbar attach")
return
end
@@ -724,12 +595,14 @@ function Commands.attachTowBar(player, args)
return
end
-- Constraint creation may complete on a later server tick in multiplayer.
-- Record and queue the reserved item first so an immediate Build 42.20
-- spontaneous break can resolve the pair and consume the reservation once.
-- Match the working wrecker path: persist the accepted logical pair, ask
-- the server for its native relation, then immediately tell clients to
-- replace that relation with the local rigid fake-trailer constraint.
markExpectedTowBarPair(vehicleA, vehicleB, args.attachmentA, args.attachmentB)
queueSync("attach", player, args, true)
vehicleA:addPointConstraint(player, vehicleB, args.attachmentA, args.attachmentB)
if isServer() then
vehicleA:addPointConstraint(player, vehicleB, args.attachmentA, args.attachmentB)
end
broadcastAttach(vehicleA, vehicleB, args.attachmentA, args.attachmentB)
if isLinked(vehicleA, vehicleB) then
markTowBarPairConfirmed(vehicleA, vehicleB)
end
@@ -749,10 +622,6 @@ function Commands.detachTowBar(player, args)
noise("rejected unauthorized towbar detach")
return
end
if not isLinked(towingVehicle, towedVehicle) then
noise("rejected mismatched or stale towbar detach")
return
end
if not isExpectedTowBarPair(towingVehicle, towedVehicle)
and not isLegacyTowBarPair(towingVehicle, towedVehicle) then
noise("rejected detach for a non-towbar or stale linked pair")
@@ -764,7 +633,7 @@ function Commands.detachTowBar(player, args)
breakTowBarConstraint(towingVehicle, towedVehicle)
forgetTowBarPair(towingVehicle, towedVehicle)
clearExpectedTowBarPair(towingVehicle, towedVehicle)
queueSync("detach", player, args)
broadcastDetach(towingVehicle:getId(), towedVehicle:getId())
if shouldRefund then
giveTowBar(player, true)
end
@@ -817,4 +686,4 @@ TowingCommands.OnClientCommand = function(module, command, player, args)
end
Events.OnClientCommand.Add(TowingCommands.OnClientCommand)
Events.OnTick.Add(processPendingSync)
Events.OnTick.Add(processTowBarServerTick)