Bump mod version to 1.0.5

This commit is contained in:
2026-08-12 13:36:01 -04:00
parent 40f0296080
commit a6ba4877d0
23 changed files with 6650 additions and 1 deletions
+10
View File
@@ -0,0 +1,10 @@
if not TowBarMod then TowBarMod = {} end
if not TowBarMod.Config then TowBarMod.Config = {} end
TowBarMod.Config.lowLevelAnimation = "RemoveGrass"
TowBarMod.Config.rigidTowbarDistance = 1.0
TowBarMod.Config.towAttachmentExteriorPadding = 0.25
TowBarMod.Config.towedVehicleRollingMass = 75
TowBarMod.Config.devMode = false
TowBarMod.Config.vanillaTowbarModelScaleMin = 1.5
TowBarMod.Config.vanillaTowbarModelScaleMax = 2.0
@@ -0,0 +1,72 @@
require "TimedActions/ISBaseTimedAction"
TowBarCustomPathFind = ISBaseTimedAction:derive("TowBarCustomPathFind")
function TowBarCustomPathFind:isValid()
return true
end
function TowBarCustomPathFind:update()
if instanceof(self.character, "IsoPlayer") and
(self.character:pressedMovement(false) or self.character:pressedCancelAction()) then
self:forceStop()
return
end
local result = self.character:getPathFindBehavior2():update()
if result == BehaviorResult.Succeeded then
self:forceComplete()
end
local x = self.character:getX()
local y = self.character:getY()
if x == self.lastX and y == self.lastY then
self.currentTimeInOnePosition = self.currentTimeInOnePosition + 1
else
self.currentTimeInOnePosition = 0
self.lastX = x
self.lastY = y
end
if self.currentTimeInOnePosition > self.maxTimeInOnePosition then
self:forceComplete()
end
end
function TowBarCustomPathFind:start()
self.character:facePosition(self.goal[2], self.goal[3])
self.character:getPathFindBehavior2():pathToLocationF(self.goal[2], self.goal[3], self.goal[4])
end
function TowBarCustomPathFind:stop()
ISBaseTimedAction.stop(self)
self.character:getPathFindBehavior2():cancel()
self.character:setPath2(nil)
end
function TowBarCustomPathFind:perform()
self.character:getPathFindBehavior2():cancel()
self.character:setPath2(nil)
ISBaseTimedAction.perform(self)
end
function TowBarCustomPathFind:pathToLocationF(character, targetX, targetY, targetZ)
local o = {}
setmetatable(o, self)
self.__index = self
o.character = character
o.stopOnWalk = false
o.stopOnRun = false
o.maxTime = -1
o.maxTimeInOnePosition = 15
o.currentTimeInOnePosition = 0
o.lastX = -1
o.lastY = -1
o.goal = { 'LocationF', targetX, targetY, targetZ }
return o
end
@@ -0,0 +1,56 @@
require('TimedActions/ISBaseTimedAction')
TowBarHookVehicle = ISBaseTimedAction:derive("TowBarHookVehicle")
-- The condition which tells the timed action if it is still valid
function TowBarHookVehicle:isValid()
return true;
end
-- Starts the Timed Action
function TowBarHookVehicle:start()
self:setActionAnim(self.animation)
self.sound = getSoundManager():PlayWorldSound("towbar_hookingSound", false, self.character:getSquare(), 0, 5, 1, true)
end
-- Is called when the time has passed
function TowBarHookVehicle:perform()
self.sound:stop();
if self.performFunc ~= nil then
self.performFunc(self.character, self.arg1, self.arg2, self.arg3, self.arg4)
end
ISBaseTimedAction.perform(self);
end
function TowBarHookVehicle:stop()
if self.sound then
self.sound:stop()
end
ISBaseTimedAction.stop(self)
end
function TowBarHookVehicle:new(character, time, animation, performFunc, arg1, arg2, arg3, arg4)
local o = {};
setmetatable(o, self)
self.__index = self
o.stopOnWalk = true
o.stopOnRun = true
o.maxTime = time
o.character = character;
o.animation = animation
o.performFunc = performFunc
o.arg1 = arg1
o.arg2 = arg2
o.arg3 = arg3
o.arg4 = arg4
return o;
end
@@ -0,0 +1,42 @@
require("TimedActions/ISBaseTimedAction")
TowBarScheduleAction = ISBaseTimedAction:derive("TowBarScheduleAction")
function TowBarScheduleAction:isValid()
return true
end
function TowBarScheduleAction:start()
end
function TowBarScheduleAction:perform()
if self.performFunc ~= nil then
self.performFunc(self.character, self.arg1, self.arg2, self.arg3, self.arg4)
end
ISBaseTimedAction.perform(self)
end
function TowBarScheduleAction:stop()
ISBaseTimedAction.stop(self)
end
function TowBarScheduleAction:new(character, time, performFunc, arg1, arg2, arg3, arg4)
local o = ISBaseTimedAction.new(self, character)
o.useProgressBar = false
o.stopOnWalk = false
o.stopOnRun = false
o.maxTime = time
o.character = character
o.performFunc = performFunc
o.arg1 = arg1
o.arg2 = arg2
o.arg3 = arg3
o.arg4 = arg4
return o
end
@@ -0,0 +1,173 @@
if isServer() then return end
if not TowBarMod then TowBarMod = {} end
TowBarMod.Sync = TowBarMod.Sync or {}
if TowBarMod.Sync._towSyncClientLoaded then return end
TowBarMod.Sync._towSyncClientLoaded = true
local function resolveVehicle(id)
if not id then return nil end
return getVehicleById(id)
end
local function ensureAttachment(vehicle, attachmentId)
if not vehicle or not attachmentId then return false end
local script = vehicle:getScript()
if not script then return false end
if script:getAttachmentById(attachmentId) ~= nil then return true end
local wheelCount = script:getWheelCount()
local yOffset = -0.5
if wheelCount > 0 then
local wheel = script:getWheel(0)
if wheel and wheel:getOffset() then
yOffset = wheel:getOffset():y() + 0.1
end
end
local chassis = script:getPhysicsChassisShape()
if not chassis then return false end
local attach = ModelAttachment.new(attachmentId)
if attachmentId == "trailer" then
attach:getOffset():set(0, yOffset, -chassis:z() / 2 - 0.1)
attach:setZOffset(-1)
else
attach:getOffset():set(0, yOffset, chassis:z() / 2 + 0.1)
attach:setZOffset(1)
end
script:addAttachment(attach)
return true
end
local function isLinked(vehicleA, vehicleB)
if not vehicleA or not vehicleB then return false end
return vehicleA:getVehicleTowing() == vehicleB and vehicleB:getVehicleTowedBy() == vehicleA
end
local function reconcilePairState(vehicleA, vehicleB, attachmentA, attachmentB)
if TowBarMod.Utils and TowBarMod.Utils.updateAttachmentsForRigidTow then
TowBarMod.Utils.updateAttachmentsForRigidTow(vehicleA, vehicleB, attachmentA, attachmentB)
end
local towingMd = vehicleA:getModData()
local towedMd = vehicleB:getModData()
local currentScript = vehicleB:getScriptName()
if towingMd then
towingMd["isTowingByTowBar"] = true
towingMd["towed"] = false
towingMd["towBarTowedVehicleId"] = vehicleB:getId()
towingMd["towBarTowingVehicleId"] = nil
towingMd["towBarExpectedAttachment"] = attachmentA
vehicleA:transmitModData()
end
if towedMd then
if towedMd.towBarOriginalScriptName == nil and currentScript ~= "notTowingA_Trailer" then
towedMd.towBarOriginalScriptName = currentScript
end
if towedMd.towBarOriginalMass == nil then
towedMd.towBarOriginalMass = vehicleB:getMass()
end
if towedMd.towBarOriginalBrakingForce == nil then
towedMd.towBarOriginalBrakingForce = vehicleB:getBrakingForce()
end
towedMd["isTowingByTowBar"] = true
towedMd["towed"] = true
towedMd["towBarTowedVehicleId"] = nil
towedMd["towBarTowingVehicleId"] = vehicleA:getId()
towedMd["towBarExpectedAttachment"] = attachmentB
vehicleB:transmitModData()
end
if TowBarMod.Hook and TowBarMod.Hook.setVehicleScriptWithTowBarHidden then
TowBarMod.Hook.setVehicleScriptWithTowBarHidden(vehicleB, "notTowingA_Trailer")
end
if TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then
pcall(TowBarMod.Hook.setVehiclePostAttach, nil, vehicleB)
end
end
local function applyAttachSync(args)
if not args then return end
local vehicleA = resolveVehicle(args.vehicleA)
local vehicleB = resolveVehicle(args.vehicleB)
if not vehicleA or not vehicleB then return end
local attachmentA = args.attachmentA or "trailer"
local attachmentB = args.attachmentB or "trailerfront"
if not ensureAttachment(vehicleA, attachmentA) or not ensureAttachment(vehicleB, attachmentB) then
return
end
if not isLinked(vehicleA, vehicleB) then
vehicleA:addPointConstraint(nil, vehicleB, attachmentA, attachmentB)
end
reconcilePairState(vehicleA, vehicleB, attachmentA, attachmentB)
end
local function hasConflictingTowLink(vehicle, expectedOther)
if not vehicle or not expectedOther then return false end
local towing = vehicle:getVehicleTowing()
local towedBy = vehicle:getVehicleTowedBy()
if (towing ~= nil and towing ~= expectedOther)
or (towedBy ~= nil and towedBy ~= expectedOther) then
return true
end
-- Also reject a delayed detach while a new tow is being established but
-- has not created its physical constraint yet.
local modData = vehicle:getModData()
if not modData then return false end
local expectedOtherId = expectedOther:getId()
return (modData["towBarTowedVehicleId"] ~= nil
and modData["towBarTowedVehicleId"] ~= expectedOtherId)
or (modData["towBarTowingVehicleId"] ~= nil
and modData["towBarTowingVehicleId"] ~= expectedOtherId)
end
local function breakTowBarPair(vehicleA, vehicleB)
if not vehicleA or not vehicleB then return end
local vehicleAReferencesB = vehicleA:getVehicleTowing() == vehicleB
or vehicleA:getVehicleTowedBy() == vehicleB
local vehicleBReferencesA = vehicleB:getVehicleTowing() == vehicleA
or vehicleB:getVehicleTowedBy() == vehicleA
if vehicleBReferencesA then
vehicleB:breakConstraint(true, true)
end
if vehicleAReferencesB then
vehicleA:breakConstraint(true, true)
end
end
local function applyDetachSync(args)
if not args then return end
local vehicleA = resolveVehicle(args.vehicleA)
local vehicleB = resolveVehicle(args.vehicleB)
if not vehicleA or not vehicleB then return end
if hasConflictingTowLink(vehicleA, vehicleB) or hasConflictingTowLink(vehicleB, vehicleA) then
return
end
breakTowBarPair(vehicleA, vehicleB)
if TowBarMod.Hook and TowBarMod.Hook.cleanupDetachedTowBar then
pcall(TowBarMod.Hook.cleanupDetachedTowBar, vehicleA, vehicleB)
end
end
local function onServerCommand(module, command, args)
if module ~= "towbar" then return end
if command == "forceAttachSync" then
applyAttachSync(args)
elseif command == "forceDetachSync" or command == "spontaneousDetachSync" then
applyDetachSync(args)
end
end
Events.OnServerCommand.Add(onServerCommand)
@@ -0,0 +1,833 @@
if not TowBarMod then TowBarMod = {} end
if not TowBarMod.Hook then TowBarMod.Hook = {} end
local DefaultTowBarTowMass = 200
local AutoReattachCooldownHours = 1 / 7200 -- 0.5 seconds
TowBarMod.Hook.lastAutoReattachAtByVehicle = TowBarMod.Hook.lastAutoReattachAtByVehicle or {}
local AutoReattachPlayerCooldownHours = 1 / 14400 -- 0.25 seconds
TowBarMod.Hook.lastAutoReattachAtByPlayer = TowBarMod.Hook.lastAutoReattachAtByPlayer or {}
local FreeRollTickInterval = 15
local freeRollTickCounter = 0
local function tryVehicleCall(vehicle, methodName, arg)
if not vehicle or not methodName then return false, nil end
local method = vehicle[methodName]
if method == nil then return false, nil end
return pcall(function()
if arg ~= nil then
return method(vehicle, arg)
end
return method(vehicle)
end)
end
local function storeOriginalVehicleCall(vehicle, modData, key, getterName)
if not vehicle or not modData or modData[key] ~= nil then return end
local ok, value = tryVehicleCall(vehicle, getterName)
if ok and value ~= nil then
modData[key] = value
end
end
local function applyFreeRollingTowState(vehicle)
if not vehicle then return end
local modData = vehicle:getModData()
if not modData then return end
if modData.towBarOriginalMass == nil then
modData.towBarOriginalMass = vehicle:getMass()
end
if modData.towBarOriginalBrakingForce == nil then
modData.towBarOriginalBrakingForce = vehicle:getBrakingForce()
end
storeOriginalVehicleCall(vehicle, modData, "towBarOriginalParkingBrakeOn", "isParkingBrakeOn")
storeOriginalVehicleCall(vehicle, modData, "towBarOriginalParkingBrake", "isParkingBrake")
storeOriginalVehicleCall(vehicle, modData, "towBarOriginalHandbrake", "isHandbrake")
local configuredTowMass = TowBarMod.Config and tonumber(TowBarMod.Config.towedVehicleRollingMass)
vehicle:setMass(configuredTowMass or DefaultTowBarTowMass)
vehicle:setBrakingForce(0)
if modData.towBarOriginalParkingBrakeOn ~= nil then
tryVehicleCall(vehicle, "setParkingBrakeOn", false)
end
if modData.towBarOriginalParkingBrake ~= nil then
tryVehicleCall(vehicle, "setParkingBrake", false)
end
if modData.towBarOriginalHandbrake ~= nil then
tryVehicleCall(vehicle, "setHandbrake", false)
end
vehicle:constraintChanged()
vehicle:updateTotalMass()
end
local function restoreFreeRollingTowState(vehicle, modData)
if not vehicle or not modData then return end
if modData.towBarOriginalMass ~= nil then
vehicle:setMass(modData.towBarOriginalMass)
end
if modData.towBarOriginalBrakingForce ~= nil then
vehicle:setBrakingForce(modData.towBarOriginalBrakingForce)
end
if modData.towBarOriginalParkingBrakeOn ~= nil then
tryVehicleCall(vehicle, "setParkingBrakeOn", modData.towBarOriginalParkingBrakeOn)
end
if modData.towBarOriginalParkingBrake ~= nil then
tryVehicleCall(vehicle, "setParkingBrake", modData.towBarOriginalParkingBrake)
end
if modData.towBarOriginalHandbrake ~= nil then
tryVehicleCall(vehicle, "setHandbrake", modData.towBarOriginalHandbrake)
end
vehicle:constraintChanged()
vehicle:updateTotalMass()
end
local function isTowBarTowPair(towingVehicle, towedVehicle)
if not towingVehicle or not towedVehicle then return false end
local towingModData = towingVehicle:getModData()
local towedModData = towedVehicle:getModData()
if not towingModData or not towedModData then return false end
if towingModData["isTowingByTowBar"] and towedModData["isTowingByTowBar"] and towedModData["towed"] then
return true
end
-- Rejoin fallback: original towbar state on the towed vehicle is enough to reapply rigid spacing.
if towedModData.towBarOriginalScriptName ~= nil then
return true
end
return false
end
local function getTowBarItem(playerObj)
if not playerObj then return nil end
local inventory = playerObj:getInventory()
if not inventory then return nil end
return inventory:getItemFromTypeRecurse("TowBar.TowBar")
end
local function sendTowAttachCommand(playerObj, args)
if not playerObj or not args then return end
-- MP-safe/server-authoritative attach path (Landtrain style).
if isClient() and isMultiplayer() then
sendClientCommand(playerObj, "towbar", "attachTowBar", args)
return
end
-- Keep vanilla attach path for SP/local behavior.
sendClientCommand(playerObj, "vehicle", "attachTrailer", args)
end
local TowbarVariantSize = 24
local TowbarNormalStart = 0
local TowbarLargeStart = 24
local TowbarMaxIndex = TowbarVariantSize - 1
local VanillaScaleMin = 1.5
local VanillaScaleMax = 2.0
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()
if model then
return model:getScale()
end
return nil
end)
if ok and type(result) == "number" then
return result
end
return 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)
local minScale = configuredMin or VanillaScaleMin
local maxScale = configuredMax or VanillaScaleMax
return modelScale >= minScale and modelScale <= maxScale
end
local function getTowbarIndexVanilla(script)
local z = script:getPhysicsChassisShape():z() / 2 - 0.1
local index = math.floor((z * 2 / 3 - 1) * 10)
return math.max(0, math.min(TowbarMaxIndex, index))
end
local function getTowbarModelSlot(script)
if not isVanillaScale(script) then
-- KI5/small-scale vehicles use the normal towbar0 mesh at Z=1.
return 0
end
return getTowbarIndexVanilla(script)
end
local function setTowBarModelVisible(vehicle, isVisible)
if not vehicle then return end
local normalPart = vehicle:getPartById("towbar")
local largePart = vehicle:getPartById("towbarLarge")
if normalPart == nil and largePart == nil then return end
for j = 0, TowbarVariantSize - 1 do
if normalPart then normalPart:setModelVisible("towbar" .. j, false) end
if largePart then largePart:setModelVisible("towbar" .. j, false) end
end
if not isVisible then
vehicle:doDamageOverlay()
return
end
local script = vehicle:getScript()
if not script then
vehicle:doDamageOverlay()
return
end
local index = getTowbarModelSlot(script)
local part = normalPart
if part then
part:setModelVisible("towbar" .. index, true)
end
vehicle:doDamageOverlay()
end
function TowBarMod.Hook.setVehicleScriptWithTowBarHidden(vehicle, scriptName)
if not vehicle or not scriptName then return false end
local modData = vehicle:getModData()
setTowBarModelVisible(vehicle, false)
if modData then
modData.towBarModelSwapInProgress = true
end
local ok, err = pcall(function()
vehicle:setScriptName(scriptName)
end)
if modData then
modData.towBarModelSwapInProgress = nil
end
-- Script initialization can reset part visibility, so hide it again before
-- allowing the intended model to be shown by the caller.
setTowBarModelVisible(vehicle, false)
if not ok then
error(err)
end
return true
end
local function resolveTowAttachmentsForPair(towingVehicle, towedVehicle, towedModData)
if not towingVehicle or not towedVehicle then
return nil, nil
end
local attachmentA = towingVehicle:getTowAttachmentSelf() or "trailer"
local attachmentB = towingVehicle:getTowAttachmentOther()
or (towedModData and towedModData["towBarChangedAttachmentId"])
or "trailerfront"
if not towingVehicle:canAttachTrailer(towedVehicle, attachmentA, attachmentB) then
if towingVehicle:canAttachTrailer(towedVehicle, "trailer", "trailerfront") then
attachmentA = "trailer"
attachmentB = "trailerfront"
elseif towingVehicle:canAttachTrailer(towedVehicle, "trailerfront", "trailer") then
attachmentA = "trailerfront"
attachmentB = "trailer"
end
end
return attachmentA, attachmentB
end
local function hasTowBarTowState(modData)
if not modData then
return false
end
if modData["isTowingByTowBar"] and modData["towed"] then
return true
end
-- Rejoin fallback: legacy saves may only have the original-script marker.
if modData.towBarOriginalScriptName ~= nil then
return true
end
return false
end
local function isActiveTowBarTowedVehicle(vehicle, modData)
if not vehicle or not modData then
return false
end
if modData["isTowingByTowBar"] and modData["towed"] then
return true
end
-- Rejoin fallback: if the tow link exists, original-script marker is enough.
if vehicle:getVehicleTowedBy() and modData.towBarOriginalScriptName ~= nil then
return true
end
return false
end
local function reattachTowBarPair(playerObj, towingVehicle, towedVehicle, requireDriver)
if not playerObj or not towingVehicle or not towedVehicle then
return false
end
if requireDriver and not towingVehicle:isDriver(playerObj) then
return false
end
local towingModData = towingVehicle:getModData()
local towedModData = towedVehicle:getModData()
if not towingModData or not towedModData then
return false
end
if requireDriver then
if not isTowBarTowPair(towingVehicle, towedVehicle) then
return false
end
else
if not isActiveTowBarTowedVehicle(towedVehicle, towedModData) then
return false
end
end
local attachmentA, attachmentB = resolveTowAttachmentsForPair(towingVehicle, towedVehicle, towedModData)
if not attachmentA or not attachmentB then
return false
end
local towingScript = towingVehicle:getScript()
local towedScript = towedVehicle:getScript()
if not towingScript or not towedScript then
return false
end
if not towingScript:getAttachmentById(attachmentA) or not towedScript:getAttachmentById(attachmentB) then
return false
end
TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)
towedModData.towBarOriginalScriptName = towedModData.towBarOriginalScriptName or towedVehicle:getScriptName()
applyFreeRollingTowState(towedVehicle)
towingModData["isTowingByTowBar"] = true
towingModData["towed"] = false
towingModData["towBarTowedVehicleId"] = towedVehicle:getId()
towingModData["towBarTowingVehicleId"] = nil
towingModData["towBarExpectedAttachment"] = attachmentA
towedModData["isTowingByTowBar"] = true
towedModData["towed"] = true
towedModData["towBarTowedVehicleId"] = nil
towedModData["towBarTowingVehicleId"] = towingVehicle:getId()
towedModData["towBarExpectedAttachment"] = attachmentB
towingVehicle:transmitModData()
towedVehicle:transmitModData()
TowBarMod.Hook.setVehicleScriptWithTowBarHidden(towedVehicle, "notTowingA_Trailer")
local args = {
vehicleA = towingVehicle:getId(),
vehicleB = towedVehicle:getId(),
attachmentA = attachmentA,
attachmentB = attachmentB
}
sendTowAttachCommand(playerObj, args)
ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle))
return true
end
local function reattachTowBarPairAfterCleanDetach(playerObj, towingVehicle, towedVehicle, requireDriver)
if not playerObj or not towingVehicle or not towedVehicle then
return false
end
if requireDriver and not towingVehicle:isDriver(playerObj) then
return false
end
local detachArgs = {
towingVehicle = towingVehicle:getId(),
vehicle = towedVehicle:getId()
}
sendClientCommand(playerObj, "towbar", "detachTowBar", detachArgs)
-- World load/spawn can restore constraints in a bad state. Reattach one
-- short tick later so the detach is fully applied first.
ISTimedActionQueue.add(TowBarScheduleAction:new(
playerObj,
1,
reattachTowBarPair,
towingVehicle,
towedVehicle,
requireDriver
))
return true
end
local function recoverTowBarVehicleAfterLoad(playerObj, vehicle, retriesLeft)
if not vehicle then return end
local modData = vehicle:getModData()
if not hasTowBarTowState(modData) then
return
end
local retries = tonumber(retriesLeft) or 0
local localPlayer = playerObj or getPlayer()
local towingVehicle = vehicle:getVehicleTowedBy()
if towingVehicle then
-- Apply rigid spacing as soon as the tow link exists to avoid a visible
-- bumper-to-bumper snap while waiting for reattach recovery.
TowBarMod.Hook.setVehiclePostAttach(nil, vehicle)
end
if localPlayer and towingVehicle then
if reattachTowBarPairAfterCleanDetach(localPlayer, towingVehicle, vehicle, false) then
return
end
end
if localPlayer and retries > 0 then
-- During world load, tow links can become available a few ticks later.
ISTimedActionQueue.add(TowBarScheduleAction:new(localPlayer, 10, recoverTowBarVehicleAfterLoad, vehicle, retries - 1))
return
end
-- Fallback: keep original post-attach restoration behavior.
setTowBarModelVisible(vehicle, true)
TowBarMod.Hook.setVehiclePostAttach(nil, vehicle)
end
function TowBarMod.Hook.setVehiclePostAttach(playerObj, towedVehicle, retriesLeft)
if not towedVehicle then return end
local towedModData = towedVehicle:getModData()
if not isActiveTowBarTowedVehicle(towedVehicle, towedModData) then return end
if towedModData and towedModData.towBarOriginalScriptName then
TowBarMod.Hook.setVehicleScriptWithTowBarHidden(towedVehicle, towedModData.towBarOriginalScriptName)
end
local towingVehicle = towedVehicle:getVehicleTowedBy()
if towingVehicle then
local attachmentA, attachmentB = resolveTowAttachmentsForPair(towingVehicle, towedVehicle, towedModData)
if attachmentA and attachmentB then
local towingModData = towingVehicle:getModData()
if towingModData then
towingModData["towBarTowedVehicleId"] = towedVehicle:getId()
towingModData["towBarTowingVehicleId"] = nil
towingModData["towBarExpectedAttachment"] = attachmentA
towedModData["towBarTowingVehicleId"] = towingVehicle:getId()
towedModData["towBarTowedVehicleId"] = nil
towedModData["towBarExpectedAttachment"] = attachmentB
towingVehicle:transmitModData()
towedVehicle:transmitModData()
end
TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)
end
end
applyFreeRollingTowState(towedVehicle)
-- Re-show the towbar model after the script name has been restored.
-- setScriptName() resets model visibility, so we must set it again here.
setTowBarModelVisible(towedVehicle, true)
end
function TowBarMod.Hook.performAttachTowBar(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB)
if playerObj == nil or towingVehicle == nil or towedVehicle == nil then return end
if #(TowBarMod.Utils.getHookTypeVariants(towingVehicle, towedVehicle, true)) == 0 then return end
local towBarItem = getTowBarItem(playerObj)
if towBarItem ~= nil and not (isClient() and isMultiplayer()) then
sendClientCommand(playerObj, "towbar", "consumeTowBar", { itemId = towBarItem:getID() })
end
playerObj:setPrimaryHandItem(nil)
TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)
local towingModData = towingVehicle:getModData()
local towedModData = towedVehicle:getModData()
towedModData.towBarOriginalScriptName = towedVehicle:getScriptName()
applyFreeRollingTowState(towedVehicle)
towingModData["isTowingByTowBar"] = true
towingModData["towed"] = false
towingModData["towBarTowedVehicleId"] = towedVehicle:getId()
towingModData["towBarTowingVehicleId"] = nil
towingModData["towBarExpectedAttachment"] = attachmentA
towedModData["isTowingByTowBar"] = true
towedModData["towed"] = true
towedModData["towBarTowedVehicleId"] = nil
towedModData["towBarTowingVehicleId"] = towingVehicle:getId()
towedModData["towBarExpectedAttachment"] = attachmentB
towingVehicle:transmitModData()
towedVehicle:transmitModData()
-- Match the known-good rigid tow path: fake trailer + vanilla attach command.
TowBarMod.Hook.setVehicleScriptWithTowBarHidden(towedVehicle, "notTowingA_Trailer")
local args = {
vehicleA = towingVehicle:getId(),
vehicleB = towedVehicle:getId(),
attachmentA = attachmentA,
attachmentB = attachmentB,
itemId = towBarItem and towBarItem:getID() or nil
}
sendTowAttachCommand(playerObj, args)
ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle))
end
function TowBarMod.Hook.cleanupDetachedTowBar(towingVehicle, towedVehicle)
if towingVehicle == nil or towedVehicle == nil then return end
TowBarMod.Utils.updateAttachmentsOnDefaultValues(towingVehicle, towedVehicle)
local towingModData = towingVehicle:getModData()
local towedModData = towedVehicle:getModData()
if not towingModData or not towedModData then return end
if towedModData.towBarOriginalScriptName then
TowBarMod.Hook.setVehicleScriptWithTowBarHidden(towedVehicle, towedModData.towBarOriginalScriptName)
end
restoreFreeRollingTowState(towedVehicle, towedModData)
towingModData["isTowingByTowBar"] = false
towingModData["towed"] = false
towingModData["towBarTowedVehicleId"] = nil
towingModData["towBarTowingVehicleId"] = nil
towingModData["towBarExpectedAttachment"] = nil
towedModData["isTowingByTowBar"] = false
towedModData["towed"] = false
towedModData["towBarTowedVehicleId"] = nil
towedModData["towBarTowingVehicleId"] = nil
towedModData["towBarExpectedAttachment"] = nil
towedModData.towBarOriginalScriptName = nil
towedModData.towBarOriginalMass = nil
towedModData.towBarOriginalBrakingForce = nil
towedModData.towBarOriginalParkingBrakeOn = nil
towedModData.towBarOriginalParkingBrake = nil
towedModData.towBarOriginalHandbrake = nil
towingVehicle:transmitModData()
towedVehicle:transmitModData()
TowBarMod.Hook.lastAutoReattachAtByVehicle[towingVehicle:getId()] = nil
setTowBarModelVisible(towedVehicle, false)
end
function TowBarMod.Hook.performDetachTowBar(playerObj, towingVehicle, towedVehicle)
if playerObj == nil or towingVehicle == nil or towedVehicle == nil then return end
local args = { towingVehicle = towingVehicle:getId(), vehicle = towedVehicle:getId() }
sendClientCommand(playerObj, "towbar", "detachTowBar", args)
TowBarMod.Hook.cleanupDetachedTowBar(towingVehicle, towedVehicle)
end
function TowBarMod.Hook.reattachTowBarFromDriverSeat(playerObj, towingVehicle)
if not playerObj or not towingVehicle then return end
local towedVehicle = towingVehicle:getVehicleTowing()
if not towedVehicle then return end
reattachTowBarPair(playerObj, towingVehicle, towedVehicle, true)
end
local function tryAutoReattachFromCharacter(character)
if not character or not instanceof(character, "IsoPlayer") or not character:isLocalPlayer() then return end
local playerObj = character
local nowHours = getGameTime() and getGameTime():getWorldAgeHours() or 0
local playerNum = playerObj:getPlayerNum()
local lastPlayerHours = TowBarMod.Hook.lastAutoReattachAtByPlayer[playerNum]
if lastPlayerHours and (nowHours - lastPlayerHours) < AutoReattachPlayerCooldownHours then
return
end
local towingVehicle = playerObj:getVehicle()
if not towingVehicle then return end
if not towingVehicle:isDriver(playerObj) then return end
local towedVehicle = towingVehicle:getVehicleTowing()
if not towedVehicle then return end
if not isTowBarTowPair(towingVehicle, towedVehicle) then return end
local vehicleId = towingVehicle:getId()
local lastHours = TowBarMod.Hook.lastAutoReattachAtByVehicle[vehicleId]
if lastHours and (nowHours - lastHours) < AutoReattachCooldownHours then
return
end
TowBarMod.Hook.lastAutoReattachAtByPlayer[playerNum] = nowHours
TowBarMod.Hook.lastAutoReattachAtByVehicle[vehicleId] = nowHours
TowBarMod.Hook.reattachTowBarFromDriverSeat(playerObj, towingVehicle)
end
local function forEachCollectionItem(collection, callback)
if not collection then return end
local ok, iterator = pcall(function()
return collection:iterator()
end)
if ok and iterator then
while iterator:hasNext() do
callback(iterator:next())
end
return
end
local size
ok, size = pcall(function()
return collection:size()
end)
if not ok or type(size) ~= "number" then return end
for i = 0, size - 1 do
callback(collection:get(i))
end
end
local function keepTowBarVehiclesFreeRolling()
freeRollTickCounter = freeRollTickCounter + 1
if freeRollTickCounter < FreeRollTickInterval then
return
end
freeRollTickCounter = 0
local cell = getCell()
if not cell then return end
local vehicles = cell:getVehicles()
if not vehicles then return end
forEachCollectionItem(vehicles, function(vehicle)
local modData = vehicle and vehicle:getModData() or nil
if isActiveTowBarTowedVehicle(vehicle, modData) then
applyFreeRollingTowState(vehicle)
end
end)
end
function TowBarMod.Hook.OnEnterVehicle(character)
tryAutoReattachFromCharacter(character)
end
function TowBarMod.Hook.OnSwitchVehicleSeat(character)
tryAutoReattachFromCharacter(character)
end
function TowBarMod.Hook.attachByTowBarAction(playerObj, towingVehicle, towedVehicle)
if playerObj == nil or towingVehicle == nil or towedVehicle == nil then return end
local item = getTowBarItem(playerObj)
if item == nil then return end
if #(TowBarMod.Utils.getHookTypeVariants(towingVehicle, towedVehicle, true)) == 0 then return end
local hookPoint = towedVehicle:getAttachmentWorldPos("trailerfront", TowBarMod.Utils.tempVector1)
if hookPoint == nil then return end
ISTimedActionQueue.add(TowBarCustomPathFind:pathToLocationF(playerObj, hookPoint:x(), hookPoint:y(), hookPoint:z()))
if not playerObj:getInventory():contains("TowBar.TowBar") then
ISTimedActionQueue.add(ISInventoryTransferAction:new(playerObj, item, item:getContainer(), playerObj:getInventory(), nil))
end
local storePrim = playerObj:getPrimaryHandItem()
if storePrim == nil or storePrim ~= item then
ISTimedActionQueue.add(ISEquipWeaponAction:new(playerObj, item, 12, true))
end
ISTimedActionQueue.add(TowBarHookVehicle:new(playerObj, 300, TowBarMod.Config.lowLevelAnimation))
hookPoint = towingVehicle:getAttachmentWorldPos("trailer", TowBarMod.Utils.tempVector1)
if hookPoint == nil then return end
ISTimedActionQueue.add(TowBarCustomPathFind:pathToLocationF(playerObj, hookPoint:x(), hookPoint:y(), hookPoint:z()))
ISTimedActionQueue.add(TowBarHookVehicle:new(
playerObj,
100,
TowBarMod.Config.lowLevelAnimation,
TowBarMod.Hook.performAttachTowBar,
towingVehicle,
towedVehicle,
"trailer",
"trailerfront"
))
end
function TowBarMod.Hook.deattachTowBarAction(playerObj, vehicle)
local towingVehicle = vehicle
local towedVehicle = vehicle and vehicle:getVehicleTowing() or nil
if vehicle and vehicle:getVehicleTowedBy() then
towingVehicle = vehicle:getVehicleTowedBy()
towedVehicle = vehicle
end
if towingVehicle == nil or towedVehicle == nil then return end
local localPoint = towingVehicle:getAttachmentLocalPos(towingVehicle:getTowAttachmentSelf(), TowBarMod.Utils.tempVector1)
local shift = 0
if towingVehicle:getModData()["isChangedTowedAttachment"] then
shift = localPoint:z() > 0 and -1 or 1
end
local hookPoint = towingVehicle:getWorldPos(localPoint:x(), localPoint:y(), localPoint:z() + shift, TowBarMod.Utils.tempVector2)
if hookPoint == nil then return end
ISTimedActionQueue.add(TowBarCustomPathFind:pathToLocationF(playerObj, hookPoint:x(), hookPoint:y(), hookPoint:z()))
local storePrim = playerObj:getPrimaryHandItem()
if storePrim ~= nil then
ISTimedActionQueue.add(ISUnequipAction:new(playerObj, storePrim, 12))
end
ISTimedActionQueue.add(TowBarHookVehicle:new(playerObj, 100, TowBarMod.Config.lowLevelAnimation))
localPoint = towedVehicle:getAttachmentLocalPos(towedVehicle:getTowAttachmentSelf(), TowBarMod.Utils.tempVector1)
shift = 0
if towedVehicle:getModData()["isChangedTowedAttachment"] then
shift = localPoint:z() > 0 and -1 or 1
end
hookPoint = towedVehicle:getWorldPos(localPoint:x(), localPoint:y(), localPoint:z() + shift, TowBarMod.Utils.tempVector2)
if hookPoint == nil then return end
ISTimedActionQueue.add(TowBarCustomPathFind:pathToLocationF(playerObj, hookPoint:x(), hookPoint:y(), hookPoint:z()))
ISTimedActionQueue.add(TowBarHookVehicle:new(
playerObj,
300,
TowBarMod.Config.lowLevelAnimation,
TowBarMod.Hook.performDetachTowBar,
towingVehicle,
towedVehicle
))
end
function TowBarMod.Hook.OnSpawnVehicle(vehicle)
recoverTowBarVehicleAfterLoad(nil, vehicle, 6)
end
function TowBarMod.Hook.OnGameStart()
local cell = getCell()
if not cell then return end
local vehicles = cell:getVehicles()
if not vehicles then return end
local playerObj = getPlayer()
forEachCollectionItem(vehicles, function(vehicle)
recoverTowBarVehicleAfterLoad(playerObj, vehicle, 6)
end)
end
---------------------------------------------------------------------------
--- Dev / debug helpers
---------------------------------------------------------------------------
function TowBarMod.Hook.devShowAllTowbarModels(playerObj, vehicle)
if not vehicle then return end
local normalPart = vehicle:getPartById("towbar")
local largePart = vehicle:getPartById("towbarLarge")
if normalPart == nil and largePart == nil then
print("[TowBar DEV] No 'towbar' or 'towbarLarge' part found on vehicle " .. tostring(vehicle:getScriptName()))
return
end
local script = vehicle:getScript()
local chassisZ = script and script:getPhysicsChassisShape():z() or 0
local halfZ = chassisZ / 2
local modelScale = script and getVehicleModelScale(script) or nil
local index = 0
if script then
index = getTowbarModelSlot(script)
end
local selectedPart = "towbar"
print("[TowBar DEV] Vehicle: " .. tostring(vehicle:getScriptName()))
print("[TowBar DEV] chassisShape.z = " .. tostring(chassisZ) .. ", half = " .. tostring(halfZ))
print("[TowBar DEV] modelScale = " .. tostring(modelScale) .. ", part = " .. selectedPart)
print("[TowBar DEV] Formula picks index = " .. tostring(index) .. " (towbar" .. tostring(index) .. " at Z offset " .. tostring(1.0 + index * 0.1) .. ")")
print("[TowBar DEV] Showing towbar0..towbar23 on both parts")
for j = 0, TowbarVariantSize - 1 do
if normalPart then normalPart:setModelVisible("towbar" .. j, true) end
if largePart then largePart:setModelVisible("towbar" .. j, true) end
end
vehicle:doDamageOverlay()
end
function TowBarMod.Hook.devHideAllTowbarModels(playerObj, vehicle)
if not vehicle then return end
local normalPart = vehicle:getPartById("towbar")
local largePart = vehicle:getPartById("towbarLarge")
if normalPart == nil and largePart == nil then
print("[TowBar DEV] No 'towbar' or 'towbarLarge' part found on vehicle " .. tostring(vehicle:getScriptName()))
return
end
print("[TowBar DEV] Hiding ALL towbar models on " .. tostring(vehicle:getScriptName()))
for j = 0, TowbarVariantSize - 1 do
if normalPart then normalPart:setModelVisible("towbar" .. j, false) end
if largePart then largePart:setModelVisible("towbar" .. j, false) end
end
vehicle:doDamageOverlay()
end
function TowBarMod.Hook.devShowSingleTowbar(playerObj, vehicle, index)
if not vehicle then return end
local normalPart = vehicle:getPartById("towbar")
local largePart = vehicle:getPartById("towbarLarge")
if normalPart == nil and largePart == nil then
print("[TowBar DEV] No 'towbar' or 'towbarLarge' part found on vehicle " .. tostring(vehicle:getScriptName()))
return
end
local localIndex = math.max(0, math.min(TowbarMaxIndex, index % TowbarVariantSize))
local useLargePart = index >= TowbarVariantSize
for j = 0, TowbarVariantSize - 1 do
if normalPart then normalPart:setModelVisible("towbar" .. j, false) end
if largePart then largePart:setModelVisible("towbar" .. j, false) end
end
local part = useLargePart and largePart or normalPart
if part == nil then
part = normalPart or largePart
end
print("[TowBar DEV] Showing only towbar" .. tostring(localIndex) .. " on part " .. tostring(useLargePart and "towbarLarge" or "towbar") .. " (Z offset " .. tostring(1.0 + localIndex * 0.1) .. ") on " .. tostring(vehicle:getScriptName()))
if part then
part:setModelVisible("towbar" .. localIndex, true)
end
vehicle:doDamageOverlay()
end
Events.OnSpawnVehicleEnd.Add(TowBarMod.Hook.OnSpawnVehicle)
if Events.OnGameStart then
Events.OnGameStart.Add(TowBarMod.Hook.OnGameStart)
end
Events.OnEnterVehicle.Add(TowBarMod.Hook.OnEnterVehicle)
Events.OnSwitchVehicleSeat.Add(TowBarMod.Hook.OnSwitchVehicleSeat)
Events.OnTick.Add(keepTowBarVehiclesFreeRolling)
+231
View File
@@ -0,0 +1,231 @@
if not TowBarMod then TowBarMod = {} end
if not TowBarMod.UI then TowBarMod.UI = {} end
---------------------------------------------------------------------------
--- UI functions
---------------------------------------------------------------------------
function TowBarMod.UI.removeDefaultDetachOption(playerObj)
local menu = getPlayerRadialMenu(playerObj:getPlayerNum())
if menu == nil then return end
local tmpSlices = {}
for i, slice in ipairs(menu.slices or {}) do
tmpSlices[i] = slice
end
menu:clear()
for _, slice in ipairs(tmpSlices) do
local command = slice.command and slice.command[1]
local args = slice.command or {}
if command ~= ISVehicleMenu.onDetachTrailer then
menu:addSlice(
slice.text,
slice.texture,
args[1],
args[2],
args[3],
args[4],
args[5],
args[6],
args[7]
)
end
end
end
--- Show menu with available vehicles for tow bar hook.
function TowBarMod.UI.showChooseVehicleMenu(playerObj, vehicle, vehicles, hasTowBar)
local playerIndex = playerObj:getPlayerNum()
local menu = getPlayerRadialMenu(playerIndex)
menu:clear()
local added = 0
for _, veh in ipairs(vehicles) do
local hookTypeVariants = TowBarMod.Utils.getHookTypeVariants(vehicle, veh, hasTowBar)
if #hookTypeVariants > 0 then
local hookType = hookTypeVariants[1]
menu:addSlice(
hookType.name,
getTexture("media/textures/tow_bar_attach.png"),
hookType.func,
playerObj,
hookType.towingVehicle,
hookType.towedVehicle,
hookType.towingPoint,
hookType.towedPoint
)
added = added + 1
end
end
if added == 0 then return end
menu:setX(getPlayerScreenLeft(playerIndex) + getPlayerScreenWidth(playerIndex) / 2 - menu:getWidth() / 2)
menu:setY(getPlayerScreenTop(playerIndex) + getPlayerScreenHeight(playerIndex) / 2 - menu:getHeight() / 2)
menu:addToUIManager()
if JoypadState.players[playerObj:getPlayerNum()+1] then
menu:setHideWhenButtonReleased(Joypad.DPadUp)
setJoypadFocus(playerObj:getPlayerNum(), menu)
playerObj:setJoypadIgnoreAimUntilCentered(true)
end
end
function TowBarMod.UI.addHookOptionToMenu(playerObj, vehicle)
local menu = getPlayerRadialMenu(playerObj:getPlayerNum())
if menu == nil then return end
local hasTowBar = playerObj:getInventory():getItemFromTypeRecurse("TowBar.TowBar") ~= nil
if not hasTowBar then return end
local vehicles = TowBarMod.Utils.getAviableVehicles(vehicle, hasTowBar)
if #vehicles == 0 then
return
elseif #vehicles == 1 then
local hookTypeVariants = TowBarMod.Utils.getHookTypeVariants(vehicle, vehicles[1], hasTowBar)
if #hookTypeVariants > 0 then
local hookType = hookTypeVariants[1]
menu:addSlice(
hookType.name,
getTexture("media/textures/tow_bar_attach.png"),
hookType.func,
playerObj,
hookType.towingVehicle,
hookType.towedVehicle,
hookType.towingPoint,
hookType.towedPoint
)
end
else
menu:addSlice(
getText("UI_Text_Towing_attach") .. "...",
getTexture("media/textures/tow_bar_attach.png"),
TowBarMod.UI.showChooseVehicleMenu,
playerObj,
vehicle,
vehicles,
hasTowBar
)
end
end
function TowBarMod.UI.addUnhookOptionToMenu(playerObj, vehicle)
local menu = getPlayerRadialMenu(playerObj:getPlayerNum())
if menu == nil then return end
if not vehicle:getModData()["isTowingByTowBar"] then return end
if not vehicle:getVehicleTowing() and not vehicle:getVehicleTowedBy() then return end
local towedVehicle = vehicle
if vehicle:getVehicleTowing() then
towedVehicle = vehicle:getVehicleTowing()
end
menu:addSlice(
getText("ContextMenu_Vehicle_DetachTrailer", ISVehicleMenu.getVehicleDisplayName(towedVehicle)),
getTexture("media/textures/tow_bar_detach.png"),
TowBarMod.Hook.deattachTowBarAction,
playerObj,
towedVehicle
)
end
---------------------------------------------------------------------------
--- Dev menu
---------------------------------------------------------------------------
function TowBarMod.UI.showDevSingleTowbarMenu(playerObj, vehicle)
local playerIndex = playerObj:getPlayerNum()
local menu = getPlayerRadialMenu(playerIndex)
menu:clear()
for j = 0, 47 do
local zIndex = j % 24
local modelType = (j >= 24) and "large" or "normal"
menu:addSlice(
"towbar" .. j .. " [" .. modelType .. "] (Z=" .. tostring(1.0 + zIndex * 0.1) .. ")",
getTexture("media/textures/tow_bar_icon.png"),
TowBarMod.Hook.devShowSingleTowbar,
playerObj,
vehicle,
j
)
end
menu:setX(getPlayerScreenLeft(playerIndex) + getPlayerScreenWidth(playerIndex) / 2 - menu:getWidth() / 2)
menu:setY(getPlayerScreenTop(playerIndex) + getPlayerScreenHeight(playerIndex) / 2 - menu:getHeight() / 2)
menu:addToUIManager()
if JoypadState.players[playerObj:getPlayerNum()+1] then
menu:setHideWhenButtonReleased(Joypad.DPadUp)
setJoypadFocus(playerObj:getPlayerNum(), menu)
playerObj:setJoypadIgnoreAimUntilCentered(true)
end
end
function TowBarMod.UI.addDevOptionsToMenu(playerObj, vehicle)
local devModeEnabled = (TowBarMod.Config and TowBarMod.Config.devMode) or getDebug()
if not devModeEnabled then return end
if not vehicle then return end
local menu = getPlayerRadialMenu(playerObj:getPlayerNum())
if menu == nil then return end
menu:addSlice(
"[DEV] Show ALL Towbars",
getTexture("media/textures/tow_bar_icon.png"),
TowBarMod.Hook.devShowAllTowbarModels,
playerObj,
vehicle
)
menu:addSlice(
"[DEV] Hide ALL Towbars",
getTexture("media/textures/tow_bar_icon.png"),
TowBarMod.Hook.devHideAllTowbarModels,
playerObj,
vehicle
)
menu:addSlice(
"[DEV] Pick Single Towbar...",
getTexture("media/textures/tow_bar_icon.png"),
TowBarMod.UI.showDevSingleTowbarMenu,
playerObj,
vehicle
)
end
---------------------------------------------------------------------------
--- Mod compability
---------------------------------------------------------------------------
if getActivatedMods():contains("vehicle_additions") then
require("Vehicles/ISUI/Oven_Mattress_RadialMenu")
require("Vehicles/ISUI/FuelTruckTank_ISVehicleMenu_FillPartMenu")
end
---------------------------------------------------------------------------
--- Attach to default menu method
---------------------------------------------------------------------------
if TowBarMod.UI.defaultShowRadialMenu == nil then
TowBarMod.UI.defaultShowRadialMenu = ISVehicleMenu.showRadialMenu
end
function ISVehicleMenu.showRadialMenu(playerObj)
TowBarMod.UI.defaultShowRadialMenu(playerObj)
if playerObj:getVehicle() then return end
local vehicle = ISVehicleMenu.getVehicleToInteractWith(playerObj)
if vehicle == nil then return end
if vehicle:getModData()["isTowingByTowBar"] then
TowBarMod.UI.removeDefaultDetachOption(playerObj)
TowBarMod.UI.addUnhookOptionToMenu(playerObj, vehicle)
elseif not vehicle:getVehicleTowing() and not vehicle:getVehicleTowedBy() then
TowBarMod.UI.addHookOptionToMenu(playerObj, vehicle)
end
TowBarMod.UI.addDevOptionsToMenu(playerObj, vehicle)
end
@@ -0,0 +1,317 @@
if not TowBarMod then TowBarMod = {} end
if not TowBarMod.Utils then TowBarMod.Utils = {} end
TowBarMod.Utils.tempVector1 = Vector3f.new()
TowBarMod.Utils.tempVector2 = Vector3f.new()
---------------------------------------------------------------------------
--- Util functions
---------------------------------------------------------------------------
--- Compute the attachment Y offset for a vehicle so the towbar sits just
--- above the wheels (i.e. a fixed distance off the ground) regardless of
--- how the vehicle model is configured.
local function computeAttachmentHeight(vehicle)
local script = vehicle:getScript()
if not script then return -0.5 end
local wheelCount = script:getWheelCount()
if wheelCount > 0 then
return script:getWheel(0):getOffset():y() + 0.1
end
return -0.5
end
local function getVehicleHalfLength(script)
if not script then return nil end
local ok, shape = pcall(function()
return script:getPhysicsChassisShape()
end)
if ok and shape then
local zOk, z = pcall(function()
return shape:z()
end)
z = zOk and tonumber(z) or nil
if z and z > 0 then return z / 2 end
end
ok, shape = pcall(function()
return script:getExtents()
end)
if ok and shape then
local zOk, z = pcall(function()
return shape:z()
end)
z = zOk and tonumber(z) or nil
if z and z > 0 then return z / 2 end
end
return nil
end
local function getExteriorAttachmentZ(script, attachmentId, originalZ, extraDistance)
local halfLength = getVehicleHalfLength(script)
if halfLength == nil then return originalZ end
local configuredPadding = TowBarMod.Config and tonumber(TowBarMod.Config.towAttachmentExteriorPadding)
local padding = configuredPadding or 0.25
local direction = originalZ >= 0 and 1 or -1
if originalZ == 0 and attachmentId == "trailer" then
direction = -1
end
local exteriorZ = direction * (halfLength + padding + (extraDistance or 0))
if math.abs(originalZ) > math.abs(exteriorZ) then
return originalZ
end
return exteriorZ
end
function TowBarMod.Utils.isTrailer(vehicle)
return string.match(string.lower(vehicle:getScript():getName()), "trailer")
end
--- Return vehicles from sector that player can tow by tow bar.
function TowBarMod.Utils.getAviableVehicles(mainVehicle, hasTowBar)
local vehicles = {}
if not hasTowBar then return vehicles end
local square = mainVehicle:getSquare()
if square == nil then return vehicles end
-- Match vanilla towing search radius.
for y=square:getY() - 6, square:getY() + 6 do
for x=square:getX() - 6, square:getX() + 6 do
local square2 = getCell():getGridSquare(x, y, square:getZ())
if square2 then
for i=1, square2:getMovingObjects():size() do
local obj = square2:getMovingObjects():get(i-1)
if obj ~= nil
and instanceof(obj, "BaseVehicle")
and obj ~= mainVehicle
and #(TowBarMod.Utils.getHookTypeVariants(mainVehicle, obj, hasTowBar)) ~= 0 then
table.insert(vehicles, obj)
end
end
end
end
end
return vehicles
end
--- Return a table with towbar-only hook options for vehicles.
function TowBarMod.Utils.getHookTypeVariants(vehicleA, vehicleB, hasTowBar)
local hookTypeVariants = {}
if not hasTowBar then return hookTypeVariants end
if vehicleA:getVehicleTowing() or vehicleA:getVehicleTowedBy()
or vehicleB:getVehicleTowing() or vehicleB:getVehicleTowedBy() then
return hookTypeVariants
end
-- Keep tow bars for vehicle-to-vehicle towing only.
if TowBarMod.Utils.isTrailer(vehicleA) or TowBarMod.Utils.isTrailer(vehicleB) then
return hookTypeVariants
end
if vehicleA:canAttachTrailer(vehicleB, "trailerfront", "trailer") then
local hookType = {}
hookType.name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. getText("UI_Text_Towing_byTowBar")
hookType.func = TowBarMod.Hook.attachByTowBarAction
hookType.towingVehicle = vehicleB
hookType.towedVehicle = vehicleA
hookType.textureName = "tow_bar_icon"
table.insert(hookTypeVariants, hookType)
elseif vehicleA:canAttachTrailer(vehicleB, "trailer", "trailerfront") then
local hookType = {}
hookType.name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. getText("UI_Text_Towing_byTowBar")
hookType.func = TowBarMod.Hook.attachByTowBarAction
hookType.towingVehicle = vehicleA
hookType.towedVehicle = vehicleB
hookType.textureName = "tow_bar_icon"
table.insert(hookTypeVariants, hookType)
end
return hookTypeVariants
end
function TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)
local towingScript = towingVehicle:getScript()
local towedScript = towedVehicle:getScript()
if towingScript == nil or towedScript == nil then return end
local towingAttachment = towingScript:getAttachmentById(attachmentA)
local towedAttachment = towedScript:getAttachmentById(attachmentB)
if towingAttachment == nil or towedAttachment == nil then return end
towingAttachment:setUpdateConstraint(false)
towingAttachment:setZOffset(0)
towedAttachment:setUpdateConstraint(false)
towedAttachment:setZOffset(0)
-- Dynamic height: compute Y from wheel offset so the towbar never clips the floor.
local towingHeight = computeAttachmentHeight(towingVehicle)
local towedHeight = computeAttachmentHeight(towedVehicle)
-- Store and update the towing vehicle's attachment Y.
local towingModData = towingVehicle:getModData()
local towingOffset = towingAttachment:getOffset()
if towingModData["towBarOriginalTowingAttachmentId"] ~= attachmentA
or towingModData["towBarOriginalTowingOffsetX"] == nil
or towingModData["towBarOriginalTowingOffsetY"] == nil
or towingModData["towBarOriginalTowingOffsetZ"] == nil then
towingModData["towBarOriginalTowingOffsetX"] = towingOffset:x()
towingModData["towBarOriginalTowingOffsetY"] = towingOffset:y()
towingModData["towBarOriginalTowingOffsetZ"] = towingOffset:z()
towingModData["towBarOriginalTowingAttachmentId"] = attachmentA
end
local towedModData = towedVehicle:getModData()
local spacingDistance = 1.0
if TowBarMod.Config and tonumber(TowBarMod.Config.rigidTowbarDistance) ~= nil then
spacingDistance = tonumber(TowBarMod.Config.rigidTowbarDistance)
end
local towingBaseX = tonumber(towingModData["towBarOriginalTowingOffsetX"]) or towingOffset:x()
local towingBaseZ = tonumber(towingModData["towBarOriginalTowingOffsetZ"]) or towingOffset:z()
local towingExteriorZ = getExteriorAttachmentZ(towingScript, attachmentA, towingBaseZ, 0)
towingAttachment:getOffset():set(towingBaseX, towingHeight, towingExteriorZ)
local offset = towedAttachment:getOffset()
local storedBaseX = tonumber(towedModData["towBarBaseAttachmentOffsetX"])
local storedBaseY = tonumber(towedModData["towBarBaseAttachmentOffsetY"])
local storedBaseZ = tonumber(towedModData["towBarBaseAttachmentOffsetZ"])
local hasStoredBase = towedModData["towBarBaseAttachmentId"] == attachmentB
and storedBaseX ~= nil and storedBaseY ~= nil and storedBaseZ ~= nil
local baseX = hasStoredBase and storedBaseX or offset:x()
local baseY = hasStoredBase and storedBaseY or offset:y()
local baseZ = hasStoredBase and storedBaseZ or offset:z()
if not hasStoredBase then
towedModData["towBarBaseAttachmentId"] = attachmentB
towedModData["towBarBaseAttachmentOffsetX"] = baseX
towedModData["towBarBaseAttachmentOffsetY"] = baseY
towedModData["towBarBaseAttachmentOffsetZ"] = baseZ
end
local towedExteriorZ = getExteriorAttachmentZ(towedScript, attachmentB, baseZ, spacingDistance)
local zShift = towedExteriorZ - baseZ
towedAttachment:getOffset():set(baseX, towedHeight, towedExteriorZ)
towedModData["isChangedTowedAttachment"] = true
towedModData["towBarChangedAttachmentId"] = attachmentB
towedModData["towBarChangedOffsetZShift"] = zShift
towedVehicle:transmitModData()
towingVehicle:transmitModData()
end
function TowBarMod.Utils.updateAttachmentsOnDefaultValues(towingVehicle, towedVehicle)
local towingModData = towingVehicle:getModData()
local towingAttachmentId = towingModData["towBarOriginalTowingAttachmentId"]
or towingVehicle:getTowAttachmentSelf()
local towingAttachment = towingVehicle:getScript():getAttachmentById(towingAttachmentId)
if towingAttachment ~= nil then
towingAttachment:setUpdateConstraint(true)
local zOffset = (towingAttachmentId == "trailer") and -1 or 1
towingAttachment:setZOffset(zOffset)
-- Restore the original offset that was overridden for rigid tow spacing.
local originalX = tonumber(towingModData["towBarOriginalTowingOffsetX"])
local originalY = tonumber(towingModData["towBarOriginalTowingOffsetY"])
local originalZ = tonumber(towingModData["towBarOriginalTowingOffsetZ"])
if originalX ~= nil and originalY ~= nil and originalZ ~= nil then
towingAttachment:getOffset():set(originalX, originalY, originalZ)
elseif originalY ~= nil then
local off = towingAttachment:getOffset()
towingAttachment:getOffset():set(off:x(), originalY, off:z())
end
end
towingModData["towBarOriginalTowingOffsetX"] = nil
towingModData["towBarOriginalTowingOffsetY"] = nil
towingModData["towBarOriginalTowingOffsetZ"] = nil
towingModData["towBarOriginalTowingAttachmentId"] = nil
towingVehicle:transmitModData()
local towedModData = towedVehicle:getModData()
local changedAttachmentId = towedModData["towBarChangedAttachmentId"] or towedVehicle:getTowAttachmentSelf()
local towedAttachment = towedVehicle:getScript():getAttachmentById(changedAttachmentId)
if towedAttachment ~= nil then
towedAttachment:setUpdateConstraint(true)
local zOffset = (changedAttachmentId == "trailer") and -1 or 1
towedAttachment:setZOffset(zOffset)
if towedModData["isChangedTowedAttachment"] then
local storedBaseX = tonumber(towedModData["towBarBaseAttachmentOffsetX"])
local storedBaseY = tonumber(towedModData["towBarBaseAttachmentOffsetY"])
local storedBaseZ = tonumber(towedModData["towBarBaseAttachmentOffsetZ"])
local hasStoredBase = towedModData["towBarBaseAttachmentId"] == changedAttachmentId
and storedBaseX ~= nil and storedBaseY ~= nil and storedBaseZ ~= nil
if hasStoredBase then
towedAttachment:getOffset():set(storedBaseX, storedBaseY, storedBaseZ)
else
local offset = towedAttachment:getOffset()
local storedShift = tonumber(towedModData["towBarChangedOffsetZShift"])
if storedShift ~= nil then
towedAttachment:getOffset():set(offset:x(), offset:y(), offset:z() - storedShift)
else
local zShift = offset:z() > 0 and -1 or 1
towedAttachment:getOffset():set(offset:x(), offset:y(), offset:z() + zShift)
end
end
end
end
towedModData["isChangedTowedAttachment"] = false
towedModData["towBarChangedAttachmentId"] = nil
towedModData["towBarChangedOffsetZShift"] = nil
towedModData["towBarBaseAttachmentId"] = nil
towedModData["towBarBaseAttachmentOffsetX"] = nil
towedModData["towBarBaseAttachmentOffsetY"] = nil
towedModData["towBarBaseAttachmentOffsetZ"] = nil
towedVehicle:transmitModData()
end
-----------------------------------------------------------
--- Fix mods that add vehicles without tow attachments
local function fixTowAttachmentsForOtherVehicleMods()
local scriptManager = getScriptManager()
local vehicleScripts = scriptManager:getAllVehicleScripts()
for i = 0, vehicleScripts:size()-1 do
local script = vehicleScripts:get(i)
local wheelCount = script:getWheelCount()
local attachHeigtOffset = -0.5
if wheelCount > 0 then
attachHeigtOffset = script:getWheel(0):getOffset():y() + 0.1
end
if not string.match(string.lower(script:getName()), "trailer") then
local trailerAttachment = script:getAttachmentById("trailer")
if trailerAttachment == nil then
local attach = ModelAttachment.new("trailer")
attach:getOffset():set(0, attachHeigtOffset, -script:getPhysicsChassisShape():z()/2 - 0.1)
attach:setZOffset(-1)
script:addAttachment(attach)
end
local trailerFrontAttachment = script:getAttachmentById("trailerfront")
if trailerFrontAttachment == nil then
local attach = ModelAttachment.new("trailerfront")
attach:getOffset():set(0, attachHeigtOffset, script:getPhysicsChassisShape():z()/2 + 0.1)
attach:setZOffset(1)
script:addAttachment(attach)
end
end
end
end
Events.OnGameBoot.Add(fixTowAttachmentsForOtherVehicleMods)