42.18 support

This commit is contained in:
2026-06-01 18:21:38 -04:00
parent 4964927944
commit 8c0afd857b
45 changed files with 11689 additions and 1492 deletions
+2 -2
View File
@@ -160,7 +160,8 @@ function TowBarMod.UI.showDevSingleTowbarMenu(playerObj, vehicle)
end
function TowBarMod.UI.addDevOptionsToMenu(playerObj, vehicle)
if not TowBarMod.Config.devMode then return end
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())
@@ -225,4 +226,3 @@ function ISVehicleMenu.showRadialMenu(playerObj)
TowBarMod.UI.addDevOptionsToMenu(playerObj, vehicle)
end
+2 -2
View File
@@ -6,5 +6,5 @@ author=Riggs0
category=vehicle
icon=../common/media/textures/tow_bar_icon.png
url=https://hudsonriggs.systems
modversion=1.0.0
versionMin=42.13.0
modversion=1.0.1
versionMin=42.13.0
+9
View File
@@ -0,0 +1,9 @@
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.devMode = false
TowBarMod.Config.vanillaTowbarModelScaleMin = 1.5
TowBarMod.Config.vanillaTowbarModelScaleMax = 2.0
TowBarMod.Config.smallScaleTowbarIndexOffset = 2
@@ -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,125 @@
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
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
vehicleB:transmitModData()
end
vehicleB:setScriptName("notTowingA_Trailer")
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 safeBreak(vehicle)
if not vehicle then return end
if vehicle:getVehicleTowing() == nil and vehicle:getVehicleTowedBy() == nil then return end
vehicle:breakConstraint(true, true)
end
local function applyDetachSync(args)
if not args then return end
safeBreak(resolveVehicle(args.vehicleA))
safeBreak(resolveVehicle(args.vehicleB))
end
local function onServerCommand(module, command, args)
if module ~= "towbar" then return end
if command == "forceAttachSync" then
applyAttachSync(args)
elseif command == "forceDetachSync" then
applyDetachSync(args)
end
end
Events.OnServerCommand.Add(onServerCommand)
@@ -0,0 +1,694 @@
if not TowBarMod then TowBarMod = {} end
if not TowBarMod.Hook then TowBarMod.Hook = {} end
local TowBarTowMass = 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 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 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
-- Match KI5-size vehicles by anchoring to tow attachment depth.
-- +0.1 keeps the bar slightly beyond the attachment point.
local index = math.floor((maxAbsTowZ + 0.1 - 1.0) * 10)
return math.max(0, math.min(TowbarMaxIndex, index))
end
return nil
end
local function getTowbarModelSlot(script)
local isVanilla = isVanillaScale(script)
local index = getTowbarIndexVanilla(script)
if not isVanilla then
local attachmentIndex = getTowbarIndexSmallScale(script)
if attachmentIndex ~= nil then
index = attachmentIndex
else
local offset = TowBarMod.Config and tonumber(TowBarMod.Config.smallScaleTowbarIndexOffset) or 2
index = math.max(0, math.min(TowbarMaxIndex, index + offset))
end
end
return index, isVanilla
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, isVanilla = getTowbarModelSlot(script)
local part = isVanilla and normalPart or largePart
if part == nil then
part = normalPart or largePart
end
if part then
part:setModelVisible("towbar" .. index, true)
end
vehicle:doDamageOverlay()
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()
if towedModData.towBarOriginalMass == nil then
towedModData.towBarOriginalMass = towedVehicle:getMass()
end
if towedModData.towBarOriginalBrakingForce == nil then
towedModData.towBarOriginalBrakingForce = towedVehicle:getBrakingForce()
end
towingModData["isTowingByTowBar"] = true
towedModData["isTowingByTowBar"] = true
towedModData["towed"] = true
towingVehicle:transmitModData()
towedVehicle:transmitModData()
setTowBarModelVisible(towedVehicle, true)
towedVehicle:setScriptName("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
towedVehicle:setScriptName(towedModData.towBarOriginalScriptName)
end
local towingVehicle = towedVehicle:getVehicleTowedBy()
if towingVehicle then
local attachmentA, attachmentB = resolveTowAttachmentsForPair(towingVehicle, towedVehicle, towedModData)
if attachmentA and attachmentB then
TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)
end
end
towedVehicle:setMass(TowBarTowMass)
towedVehicle:setBrakingForce(0)
towedVehicle:constraintChanged()
towedVehicle:updateTotalMass()
-- 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 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()
towedModData.towBarOriginalMass = towedVehicle:getMass()
towedModData.towBarOriginalBrakingForce = towedVehicle:getBrakingForce()
towingModData["isTowingByTowBar"] = true
towedModData["isTowingByTowBar"] = true
towedModData["towed"] = true
towingVehicle:transmitModData()
towedVehicle:transmitModData()
setTowBarModelVisible(towedVehicle, true)
-- Match the known-good rigid tow path: fake trailer + vanilla attach command.
towedVehicle:setScriptName("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))
end
function TowBarMod.Hook.performDetachTowBar(playerObj, towingVehicle, towedVehicle)
if playerObj == nil or towingVehicle == nil or towedVehicle == nil then return end
TowBarMod.Utils.updateAttachmentsOnDefaultValues(towingVehicle, towedVehicle)
local args = { towingVehicle = towingVehicle:getId(), vehicle = towedVehicle:getId() }
sendClientCommand(playerObj, "towbar", "detachTowBar", args)
local towedModData = towedVehicle:getModData()
if towedModData.towBarOriginalScriptName then
towedVehicle:setScriptName(towedModData.towBarOriginalScriptName)
end
if towedModData.towBarOriginalMass ~= nil then
towedVehicle:setMass(towedModData.towBarOriginalMass)
end
if towedModData.towBarOriginalBrakingForce ~= nil then
towedVehicle:setBrakingForce(towedModData.towBarOriginalBrakingForce)
end
towedVehicle:constraintChanged()
towedVehicle:updateTotalMass()
sendClientCommand(playerObj, "towbar", "giveTowBar", { equipPrimary = true })
local towingModData = towingVehicle:getModData()
towingModData["isTowingByTowBar"] = false
towedModData["isTowingByTowBar"] = false
towedModData["towed"] = false
towedModData.towBarOriginalScriptName = nil
towedModData.towBarOriginalMass = nil
towedModData.towBarOriginalBrakingForce = nil
towingVehicle:transmitModData()
towedVehicle:transmitModData()
TowBarMod.Hook.lastAutoReattachAtByVehicle[towingVehicle:getId()] = nil
setTowBarModelVisible(towedVehicle, false)
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
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()
for i = 0, vehicles:size() - 1 do
recoverTowBarVehicleAfterLoad(playerObj, vehicles:get(i), 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, isVanilla = 0, true
if script then
index, isVanilla = getTowbarModelSlot(script)
end
local selectedPart = isVanilla and "towbar" or "towbarLarge"
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)
+228
View File
@@ -0,0 +1,228 @@
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 = menu.slices
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,252 @@
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
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 towingAttachment = towingVehicle:getScript():getAttachmentById(attachmentA)
local towedAttachment = towedVehicle:getScript():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()
if towingModData["towBarOriginalTowingOffsetY"] == nil then
towingModData["towBarOriginalTowingOffsetY"] = towingAttachment:getOffset():y()
towingModData["towBarOriginalTowingAttachmentId"] = attachmentA
end
local towingOffset = towingAttachment:getOffset()
towingAttachment:getOffset():set(towingOffset:x(), towingHeight, towingOffset:z())
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 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 zDirection = baseZ >= 0 and 1 or -1
local zShift = zDirection * spacingDistance
towedAttachment:getOffset():set(baseX, towedHeight, baseZ + zShift)
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 Y offset that was overridden by dynamic height.
local originalY = tonumber(towingModData["towBarOriginalTowingOffsetY"])
if originalY ~= nil then
local off = towingAttachment:getOffset()
towingAttachment:getOffset():set(off:x(), originalY, off:z())
end
end
towingModData["towBarOriginalTowingOffsetY"] = 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)
+117
View File
@@ -0,0 +1,117 @@
BTtow = {}
BTtow.Create = {}
BTtow.Init = {}
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 and TowBarMod.Config and tonumber(TowBarMod.Config.vanillaTowbarModelScaleMin)
local configuredMax = TowBarMod and 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 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
local index = math.floor((maxAbsTowZ + 0.1 - 1.0) * 10)
return math.max(0, math.min(TowbarMaxIndex, index))
end
return nil
end
local function getTowbarModelSlot(script)
local isVanilla = isVanillaScale(script)
local index = getTowbarIndexVanilla(script)
if not isVanilla then
local attachmentIndex = getTowbarIndexSmallScale(script)
if attachmentIndex ~= nil then
index = attachmentIndex
else
local offset = TowBarMod and TowBarMod.Config and tonumber(TowBarMod.Config.smallScaleTowbarIndexOffset) or 2
index = math.max(0, math.min(TowbarMaxIndex, index + offset))
end
end
return index, isVanilla
end
function BTtow.Create.towbar(vehicle, part)
if part == nil then return end
for j=0, TowbarVariantSize - 1 do
part:setModelVisible("towbar" .. j, false)
end
end
function BTtow.Init.towbar(vehicle, part)
if part == nil then return end
for j=0, TowbarVariantSize - 1 do
part:setModelVisible("towbar" .. j, false)
end
if vehicle:getModData()["isTowingByTowBar"] and vehicle:getModData()["towed"] then
local script = vehicle:getScript()
if script then
local index, isVanilla = getTowbarModelSlot(script)
local partId = part:getId()
local shouldShowOnThisPart = (isVanilla and partId == "towbar") or ((not isVanilla) and partId == "towbarLarge")
if shouldShowOnThisPart then
part:setModelVisible("towbar" .. index, true)
end
end
end
end
@@ -0,0 +1,71 @@
require 'Items/ProceduralDistributions'
require 'Items/SuburbsDistributions'
require 'Items/Distributions'
require 'Items/Distribution_BinJunk'
require 'Items/Distribution_ClosetJunk'
require 'Items/Distribution_DeskJunk'
require 'Items/Distribution_ShelfJunk'
require 'Items/Distribution_CounterJunk'
require 'Items/Distribution_SideTableJunk'
require 'Vehicles/VehicleDistributions'
require 'Vehicles/VehicleDistribution_GloveBoxJunk'
require 'Vehicles/VehicleDistribution_SeatJunk'
require 'Vehicles/VehicleDistribution_TrunkJunk'
----------------- TOW BAR -----------------------
-- Mirror Jack spawn chance into TowBar in container distributions (world + vehicle containers).
-- Intentionally excludes story-clutter floor placement tables (RandomizedWorldContent/StoryClutter).
local TOWBAR_ITEM_TYPE = "TowBar.TowBar"
local JACK_ITEM_TYPES = {
["Jack"] = true,
["Base.Jack"] = true,
}
local function addMissingTowBarsForJack(items)
if type(items) ~= "table" then return end
local jackCountByChance = {}
local towBarCountByChance = {}
for i = 1, #items, 2 do
local itemType = items[i]
local chance = tonumber(items[i + 1])
if type(itemType) == "string" and chance ~= nil then
if JACK_ITEM_TYPES[itemType] then
jackCountByChance[chance] = (jackCountByChance[chance] or 0) + 1
elseif itemType == TOWBAR_ITEM_TYPE then
towBarCountByChance[chance] = (towBarCountByChance[chance] or 0) + 1
end
end
end
for chance, jackCount in pairs(jackCountByChance) do
local missing = jackCount - (towBarCountByChance[chance] or 0)
for _ = 1, missing do
table.insert(items, TOWBAR_ITEM_TYPE)
table.insert(items, chance)
end
end
end
local function walkContainerDistributions(root, seen)
if type(root) ~= "table" or seen[root] then return end
seen[root] = true
for key, value in pairs(root) do
if key == "items" and type(value) == "table" then
addMissingTowBarsForJack(value)
elseif type(value) == "table" then
walkContainerDistributions(value, seen)
end
end
end
local seen = {}
walkContainerDistributions(ProceduralDistributions, seen)
walkContainerDistributions(SuburbsDistributions, seen)
walkContainerDistributions(Distributions, seen)
walkContainerDistributions(VehicleDistributions, seen)
walkContainerDistributions(ClutterTables, seen)
+246
View File
@@ -0,0 +1,246 @@
if isClient() then return end
local TowingCommands = {}
local Commands = {}
local TowBarItemType = "TowBar.TowBar"
local SyncDelayTicks = 2
local SnapshotIntervalTicks = 120
local pendingSync = {}
local snapshotTickCounter = 0
TowingCommands.wantNoise = getDebug() or false
local noise = function(msg)
if TowingCommands.wantNoise then
print("TowBarCommands: " .. msg)
end
end
local function queueSync(kind, player, args)
if not args then return end
table.insert(pendingSync, {
kind = kind,
ticks = SyncDelayTicks,
player = player,
args = args
})
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
return "trailer"
end
local function resolveAttachmentB(args, vehicleB)
if args and args.attachmentB then return args.attachmentB end
if vehicleB and vehicleB:getTowAttachmentSelf() then return vehicleB:getTowAttachmentSelf() end
return "trailerfront"
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 hasTowBarState(vehicle)
if not vehicle then return false end
local md = vehicle:getModData()
if not md then return false end
return md["isTowingByTowBar"] == true
end
local function broadcastAttach(vehicleA, vehicleB, attachmentA, attachmentB)
if not vehicleA or not vehicleB then return end
sendServerCommand("towbar", "forceAttachSync", {
vehicleA = vehicleA:getId(),
vehicleB = vehicleB:getId(),
attachmentA = attachmentA,
attachmentB = attachmentB
})
end
local function broadcastDetach(vehicleAId, vehicleBId)
sendServerCommand("towbar", "forceDetachSync", {
vehicleA = vehicleAId,
vehicleB = vehicleBId
})
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
end
local attachmentA = resolveAttachmentA(args, vehicleA)
local attachmentB = resolveAttachmentB(args, vehicleB)
if not isLinked(vehicleA, vehicleB) then
vehicleA:addPointConstraint(item.player, vehicleB, attachmentA, attachmentB)
end
if isLinked(vehicleA, vehicleB) then
broadcastAttach(vehicleA, vehicleB, attachmentA, attachmentB)
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
broadcastDetach(vehicleAId, vehicleBId)
end
local function snapshotActiveTowbarLinksServer()
local cell = getCell()
if not cell then return end
local list = cell:getVehicles()
if not list then return end
local it = list:iterator()
while it:hasNext() do
local towingVehicle = it:next()
local towedVehicle = towingVehicle and towingVehicle:getVehicleTowing() or nil
if towingVehicle and towedVehicle and towedVehicle:getVehicleTowedBy() == towingVehicle then
if hasTowBarState(towingVehicle) or hasTowBarState(towedVehicle) then
local attachmentA = resolveAttachmentA(nil, towingVehicle)
local towedMd = towedVehicle:getModData()
local attachmentB = (towedMd and towedMd["towBarChangedAttachmentId"]) or resolveAttachmentB(nil, towedVehicle)
if attachmentA == attachmentB then
attachmentA = "trailer"
attachmentB = "trailerfront"
end
broadcastAttach(towingVehicle, towedVehicle, attachmentA, attachmentB)
end
end
end
end
local function processPendingSync()
snapshotTickCounter = snapshotTickCounter + 1
if snapshotTickCounter >= SnapshotIntervalTicks then
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
processAttachSync(item)
elseif item.kind == "detach" then
processDetachSync(item)
end
else
table.insert(remaining, item)
end
end
pendingSync = remaining
end
function Commands.attachTowBar(player, args)
local vehicleA = getVehicleById(args.vehicleA)
local vehicleB = getVehicleById(args.vehicleB)
if not vehicleA then
noise("no such vehicle (A) id=" .. tostring(args.vehicleA))
return
end
if not vehicleB then
noise("no such vehicle (B) id=" .. tostring(args.vehicleB))
return
end
vehicleA:addPointConstraint(player, vehicleB, args.attachmentA, args.attachmentB)
end
function Commands.detachTowBar(player, args)
local towingVehicle = args.towingVehicle and getVehicleById(args.towingVehicle) or nil
local towedVehicle = args.vehicle and getVehicleById(args.vehicle) or nil
if not towingVehicle and towedVehicle then
towingVehicle = towedVehicle:getVehicleTowedBy()
end
if not towedVehicle and towingVehicle then
towedVehicle = towingVehicle:getVehicleTowing()
end
if towedVehicle then
towedVehicle:breakConstraint(true, false)
end
if towingVehicle and towingVehicle ~= towedVehicle then
towingVehicle:breakConstraint(true, false)
end
end
function Commands.consumeTowBar(player, args)
if not player then return end
local inventory = player:getInventory()
if not inventory then return end
local towBarItem = nil
local itemId = args and args.itemId
if itemId then
towBarItem = inventory:getItemWithID(itemId)
end
if not towBarItem then
towBarItem = inventory:getFirstTypeRecurse(TowBarItemType)
end
if not towBarItem then return end
local wasPrimary = player:isPrimaryHandItem(towBarItem)
local wasSecondary = player:isSecondaryHandItem(towBarItem)
player:removeFromHands(towBarItem)
inventory:Remove(towBarItem)
sendRemoveItemFromContainer(inventory, towBarItem)
if wasPrimary or wasSecondary then
sendEquip(player)
end
end
function Commands.giveTowBar(player, args)
if not player then return end
local inventory = player:getInventory()
if not inventory then return end
local towBarItem = inventory:AddItem(TowBarItemType)
if not towBarItem then return end
sendAddItemToContainer(inventory, towBarItem)
if args and args.equipPrimary then
player:setPrimaryHandItem(towBarItem)
sendEquip(player)
end
end
-- Compatibility aliases for older command names.
Commands.attachConstraint = Commands.attachTowBar
Commands.detachConstraint = Commands.detachTowBar
TowingCommands.OnClientCommand = function(module, command, player, args)
-- Only sync explicit towbar commands so vanilla towing stays untouched.
if module == "towbar" and command == "attachTowBar" then
queueSync("attach", player, args)
elseif module == "towbar" and command == "detachTowBar" then
queueSync("detach", player, args)
end
if module == "towbar" and Commands[command] then
local argStr = ""
args = args or {}
for k, v in pairs(args) do
argStr = argStr .. " " .. tostring(k) .. "=" .. tostring(v)
end
noise("received " .. module .. " " .. command .. " " .. tostring(player) .. argStr)
Commands[command](player, args)
end
end
Events.OnClientCommand.Add(TowingCommands.OnClientCommand)
Events.OnTick.Add(processPendingSync)
+3
View File
@@ -0,0 +1,3 @@
-- Build 42 registry file.
-- This mod currently uses only base registries (for example ItemType = base:normal),
-- so no custom identifier registrations are required here.
+15
View File
@@ -0,0 +1,15 @@
module TowBar
{
/*******************Towing Car*******************/
item TowBar
{
DisplayCategory = Tool,
Weight = 8.0,
ItemType = base:normal,
Icon = TowBar,
Tooltip = Tooltip_TowBar,
StaticModel = towbarModel,
WorldStaticModel = towbarModel,
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,315 @@
module Base
{
template vehicle Battery
{
part towbar
{
model towbar0
{
file = towbarModel,
offset = 0 -0.3 1.0,
}
model towbar1
{
file = towbarModel,
offset = 0 -0.3 1.1,
}
model towbar2
{
file = towbarModel,
offset = 0 -0.3 1.2,
}
model towbar3
{
file = towbarModel,
offset = 0 -0.3 1.3,
}
model towbar4
{
file = towbarModel,
offset = 0 -0.3 1.4,
}
model towbar5
{
file = towbarModel,
offset = 0 -0.3 1.5,
}
model towbar6
{
file = towbarModel,
offset = 0 -0.3 1.6,
}
model towbar7
{
file = towbarModel,
offset = 0 -0.3 1.7,
}
model towbar8
{
file = towbarModel,
offset = 0 -0.3 1.8,
}
model towbar9
{
file = towbarModel,
offset = 0 -0.3 1.9,
}
model towbar10
{
file = towbarModel,
offset = 0 -0.3 2.0,
}
model towbar11
{
file = towbarModel,
offset = 0 -0.3 2.1,
}
model towbar12
{
file = towbarModel,
offset = 0 -0.3 2.2,
}
model towbar13
{
file = towbarModel,
offset = 0 -0.3 2.3,
}
model towbar14
{
file = towbarModel,
offset = 0 -0.3 2.4,
}
model towbar15
{
file = towbarModel,
offset = 0 -0.3 2.5,
}
model towbar16
{
file = towbarModel,
offset = 0 -0.3 2.6,
}
model towbar17
{
file = towbarModel,
offset = 0 -0.3 2.7,
}
model towbar18
{
file = towbarModel,
offset = 0 -0.3 2.8,
}
model towbar19
{
file = towbarModel,
offset = 0 -0.3 2.9,
}
model towbar20
{
file = towbarModel,
offset = 0 -0.3 3.0,
}
model towbar21
{
file = towbarModel,
offset = 0 -0.3 3.1,
}
model towbar22
{
file = towbarModel,
offset = 0 -0.3 3.2,
}
model towbar23
{
file = towbarModel,
offset = 0 -0.3 3.3,
}
area = Engine,
mechanicRequireKey = false,
lua
{
create = BTtow.Create.towbar,
init = BTtow.Init.towbar,
}
}
part towbarLarge
{
model towbar0
{
file = towbarModelLarge,
offset = 0 -0.3 1.0,
}
model towbar1
{
file = towbarModelLarge,
offset = 0 -0.3 1.1,
}
model towbar2
{
file = towbarModelLarge,
offset = 0 -0.3 1.2,
}
model towbar3
{
file = towbarModelLarge,
offset = 0 -0.3 1.3,
}
model towbar4
{
file = towbarModelLarge,
offset = 0 -0.3 1.4,
}
model towbar5
{
file = towbarModelLarge,
offset = 0 -0.3 1.5,
}
model towbar6
{
file = towbarModelLarge,
offset = 0 -0.3 1.6,
}
model towbar7
{
file = towbarModelLarge,
offset = 0 -0.3 1.7,
}
model towbar8
{
file = towbarModelLarge,
offset = 0 -0.3 1.8,
}
model towbar9
{
file = towbarModelLarge,
offset = 0 -0.3 1.9,
}
model towbar10
{
file = towbarModelLarge,
offset = 0 -0.3 2.0,
}
model towbar11
{
file = towbarModelLarge,
offset = 0 -0.3 2.1,
}
model towbar12
{
file = towbarModelLarge,
offset = 0 -0.3 2.2,
}
model towbar13
{
file = towbarModelLarge,
offset = 0 -0.3 2.3,
}
model towbar14
{
file = towbarModelLarge,
offset = 0 -0.3 2.4,
}
model towbar15
{
file = towbarModelLarge,
offset = 0 -0.3 2.5,
}
model towbar16
{
file = towbarModelLarge,
offset = 0 -0.3 2.6,
}
model towbar17
{
file = towbarModelLarge,
offset = 0 -0.3 2.7,
}
model towbar18
{
file = towbarModelLarge,
offset = 0 -0.3 2.8,
}
model towbar19
{
file = towbarModelLarge,
offset = 0 -0.3 2.9,
}
model towbar20
{
file = towbarModelLarge,
offset = 0 -0.3 3.0,
}
model towbar21
{
file = towbarModelLarge,
offset = 0 -0.3 3.1,
}
model towbar22
{
file = towbarModelLarge,
offset = 0 -0.3 3.2,
}
model towbar23
{
file = towbarModelLarge,
offset = 0 -0.3 3.3,
}
area = Engine,
mechanicRequireKey = false,
lua
{
create = BTtow.Create.towbar,
init = BTtow.Init.towbar,
}
}
part Battery
{
area = Engine,
itemType = Base.CarBattery,
mechanicRequireKey = true,
category = engine,
table install
{
items
{
1
{
type = Base.Screwdriver,
count = 1,
keep = true,
equip = primary,
}
}
time = 100,
professions = ,
skills = ,
traits = ,
recipes = ,
test = Vehicles.InstallTest.Default,
door = EngineDoor,
}
table uninstall
{
items
{
1
{
type = Base.Screwdriver,
count = 1,
keep = true,
equip = primary,
}
}
time = 100,
test = Vehicles.UninstallTest.Battery,
}
lua
{
create = Vehicles.Create.Battery,
update = Vehicles.Update.Battery,
}
}
}
}
@@ -0,0 +1,284 @@
module Base
{
model towbarModel
{
mesh = vehicles/Towbar,
texture = Vehicles/Towbar_Texture,
scale = 0.01,
}
model towbarModelLarge
{
mesh = vehicles/Towbar,
texture = Vehicles/Towbar_Texture,
scale = 0.02022,
}
template vehicle Towbar
{
part towbar
{
model towbar0
{
file = towbarModel,
offset = 0 -0.3 1.0,
}
model towbar1
{
file = towbarModel,
offset = 0 -0.3 1.1,
}
model towbar2
{
file = towbarModel,
offset = 0 -0.3 1.2,
}
model towbar3
{
file = towbarModel,
offset = 0 -0.3 1.3,
}
model towbar4
{
file = towbarModel,
offset = 0 -0.3 1.4,
}
model towbar5
{
file = towbarModel,
offset = 0 -0.3 1.5,
}
model towbar6
{
file = towbarModel,
offset = 0 -0.3 1.6,
}
model towbar7
{
file = towbarModel,
offset = 0 -0.3 1.7,
}
model towbar8
{
file = towbarModel,
offset = 0 -0.3 1.8,
}
model towbar9
{
file = towbarModel,
offset = 0 -0.3 1.9,
}
model towbar10
{
file = towbarModel,
offset = 0 -0.3 2.0,
}
model towbar11
{
file = towbarModel,
offset = 0 -0.3 2.1,
}
model towbar12
{
file = towbarModel,
offset = 0 -0.3 2.2,
}
model towbar13
{
file = towbarModel,
offset = 0 -0.3 2.3,
}
model towbar14
{
file = towbarModel,
offset = 0 -0.3 2.4,
}
model towbar15
{
file = towbarModel,
offset = 0 -0.3 2.5,
}
model towbar16
{
file = towbarModel,
offset = 0 -0.3 2.6,
}
model towbar17
{
file = towbarModel,
offset = 0 -0.3 2.7,
}
model towbar18
{
file = towbarModel,
offset = 0 -0.3 2.8,
}
model towbar19
{
file = towbarModel,
offset = 0 -0.3 2.9,
}
model towbar20
{
file = towbarModel,
offset = 0 -0.3 3.0,
}
model towbar21
{
file = towbarModel,
offset = 0 -0.3 3.1,
}
model towbar22
{
file = towbarModel,
offset = 0 -0.3 3.2,
}
model towbar23
{
file = towbarModel,
offset = 0 -0.3 3.3,
}
area = Engine,
mechanicRequireKey = false,
lua
{
create = BTtow.Create.towbar,
init = BTtow.Init.towbar,
}
}
part towbarLarge
{
model towbar0
{
file = towbarModelLarge,
offset = 0 -0.3 1.0,
}
model towbar1
{
file = towbarModelLarge,
offset = 0 -0.3 1.1,
}
model towbar2
{
file = towbarModelLarge,
offset = 0 -0.3 1.2,
}
model towbar3
{
file = towbarModelLarge,
offset = 0 -0.3 1.3,
}
model towbar4
{
file = towbarModelLarge,
offset = 0 -0.3 1.4,
}
model towbar5
{
file = towbarModelLarge,
offset = 0 -0.3 1.5,
}
model towbar6
{
file = towbarModelLarge,
offset = 0 -0.3 1.6,
}
model towbar7
{
file = towbarModelLarge,
offset = 0 -0.3 1.7,
}
model towbar8
{
file = towbarModelLarge,
offset = 0 -0.3 1.8,
}
model towbar9
{
file = towbarModelLarge,
offset = 0 -0.3 1.9,
}
model towbar10
{
file = towbarModelLarge,
offset = 0 -0.3 2.0,
}
model towbar11
{
file = towbarModelLarge,
offset = 0 -0.3 2.1,
}
model towbar12
{
file = towbarModelLarge,
offset = 0 -0.3 2.2,
}
model towbar13
{
file = towbarModelLarge,
offset = 0 -0.3 2.3,
}
model towbar14
{
file = towbarModelLarge,
offset = 0 -0.3 2.4,
}
model towbar15
{
file = towbarModelLarge,
offset = 0 -0.3 2.5,
}
model towbar16
{
file = towbarModelLarge,
offset = 0 -0.3 2.6,
}
model towbar17
{
file = towbarModelLarge,
offset = 0 -0.3 2.7,
}
model towbar18
{
file = towbarModelLarge,
offset = 0 -0.3 2.8,
}
model towbar19
{
file = towbarModelLarge,
offset = 0 -0.3 2.9,
}
model towbar20
{
file = towbarModelLarge,
offset = 0 -0.3 3.0,
}
model towbar21
{
file = towbarModelLarge,
offset = 0 -0.3 3.1,
}
model towbar22
{
file = towbarModelLarge,
offset = 0 -0.3 3.2,
}
model towbar23
{
file = towbarModelLarge,
offset = 0 -0.3 3.3,
}
area = Engine,
mechanicRequireKey = false,
lua
{
create = BTtow.Create.towbar,
init = BTtow.Init.towbar,
}
}
}
}
+10
View File
@@ -0,0 +1,10 @@
name=Towbars
id=hrsys_towbars_testing
poster=../common/media/textures/preview.png
description=Towbar Towing Towed Towing Towbars. The thrid
author=Riggs0
category=vehicle
icon=../common/media/textures/tow_bar_icon.png
url=https://hudsonriggs.systems
modversion=1.0.1
versionMin=42.17.0
+9
View File
@@ -0,0 +1,9 @@
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.devMode = false
TowBarMod.Config.vanillaTowbarModelScaleMin = 1.5
TowBarMod.Config.vanillaTowbarModelScaleMax = 2.0
TowBarMod.Config.smallScaleTowbarIndexOffset = 2
@@ -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,125 @@
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
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
vehicleB:transmitModData()
end
vehicleB:setScriptName("notTowingA_Trailer")
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 safeBreak(vehicle)
if not vehicle then return end
if vehicle:getVehicleTowing() == nil and vehicle:getVehicleTowedBy() == nil then return end
vehicle:breakConstraint(true, true)
end
local function applyDetachSync(args)
if not args then return end
safeBreak(resolveVehicle(args.vehicleA))
safeBreak(resolveVehicle(args.vehicleB))
end
local function onServerCommand(module, command, args)
if module ~= "towbar" then return end
if command == "forceAttachSync" then
applyAttachSync(args)
elseif command == "forceDetachSync" then
applyDetachSync(args)
end
end
Events.OnServerCommand.Add(onServerCommand)
@@ -0,0 +1,718 @@
if not TowBarMod then TowBarMod = {} end
if not TowBarMod.Hook then TowBarMod.Hook = {} end
local TowBarTowMass = 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 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 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
-- Match KI5-size vehicles by anchoring to tow attachment depth.
-- +0.1 keeps the bar slightly beyond the attachment point.
local index = math.floor((maxAbsTowZ + 0.1 - 1.0) * 10)
return math.max(0, math.min(TowbarMaxIndex, index))
end
return nil
end
local function getTowbarModelSlot(script)
local isVanilla = isVanillaScale(script)
local index = getTowbarIndexVanilla(script)
if not isVanilla then
local attachmentIndex = getTowbarIndexSmallScale(script)
if attachmentIndex ~= nil then
index = attachmentIndex
else
local offset = TowBarMod.Config and tonumber(TowBarMod.Config.smallScaleTowbarIndexOffset) or 2
index = math.max(0, math.min(TowbarMaxIndex, index + offset))
end
end
return index, isVanilla
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, isVanilla = getTowbarModelSlot(script)
local part = isVanilla and normalPart or largePart
if part == nil then
part = normalPart or largePart
end
if part then
part:setModelVisible("towbar" .. index, true)
end
vehicle:doDamageOverlay()
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()
if towedModData.towBarOriginalMass == nil then
towedModData.towBarOriginalMass = towedVehicle:getMass()
end
if towedModData.towBarOriginalBrakingForce == nil then
towedModData.towBarOriginalBrakingForce = towedVehicle:getBrakingForce()
end
towingModData["isTowingByTowBar"] = true
towedModData["isTowingByTowBar"] = true
towedModData["towed"] = true
towingVehicle:transmitModData()
towedVehicle:transmitModData()
setTowBarModelVisible(towedVehicle, true)
towedVehicle:setScriptName("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
towedVehicle:setScriptName(towedModData.towBarOriginalScriptName)
end
local towingVehicle = towedVehicle:getVehicleTowedBy()
if towingVehicle then
local attachmentA, attachmentB = resolveTowAttachmentsForPair(towingVehicle, towedVehicle, towedModData)
if attachmentA and attachmentB then
TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)
end
end
towedVehicle:setMass(TowBarTowMass)
towedVehicle:setBrakingForce(0)
towedVehicle:constraintChanged()
towedVehicle:updateTotalMass()
-- 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 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()
towedModData.towBarOriginalMass = towedVehicle:getMass()
towedModData.towBarOriginalBrakingForce = towedVehicle:getBrakingForce()
towingModData["isTowingByTowBar"] = true
towedModData["isTowingByTowBar"] = true
towedModData["towed"] = true
towingVehicle:transmitModData()
towedVehicle:transmitModData()
setTowBarModelVisible(towedVehicle, true)
-- Match the known-good rigid tow path: fake trailer + vanilla attach command.
towedVehicle:setScriptName("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))
end
function TowBarMod.Hook.performDetachTowBar(playerObj, towingVehicle, towedVehicle)
if playerObj == nil or towingVehicle == nil or towedVehicle == nil then return end
TowBarMod.Utils.updateAttachmentsOnDefaultValues(towingVehicle, towedVehicle)
local args = { towingVehicle = towingVehicle:getId(), vehicle = towedVehicle:getId() }
sendClientCommand(playerObj, "towbar", "detachTowBar", args)
local towedModData = towedVehicle:getModData()
if towedModData.towBarOriginalScriptName then
towedVehicle:setScriptName(towedModData.towBarOriginalScriptName)
end
if towedModData.towBarOriginalMass ~= nil then
towedVehicle:setMass(towedModData.towBarOriginalMass)
end
if towedModData.towBarOriginalBrakingForce ~= nil then
towedVehicle:setBrakingForce(towedModData.towBarOriginalBrakingForce)
end
towedVehicle:constraintChanged()
towedVehicle:updateTotalMass()
sendClientCommand(playerObj, "towbar", "giveTowBar", { equipPrimary = true })
local towingModData = towingVehicle:getModData()
towingModData["isTowingByTowBar"] = false
towedModData["isTowingByTowBar"] = false
towedModData["towed"] = false
towedModData.towBarOriginalScriptName = nil
towedModData.towBarOriginalMass = nil
towedModData.towBarOriginalBrakingForce = nil
towingVehicle:transmitModData()
towedVehicle:transmitModData()
TowBarMod.Hook.lastAutoReattachAtByVehicle[towingVehicle:getId()] = nil
setTowBarModelVisible(towedVehicle, false)
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
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, isVanilla = 0, true
if script then
index, isVanilla = getTowbarModelSlot(script)
end
local selectedPart = isVanilla and "towbar" or "towbarLarge"
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)
+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,252 @@
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
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 towingAttachment = towingVehicle:getScript():getAttachmentById(attachmentA)
local towedAttachment = towedVehicle:getScript():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()
if towingModData["towBarOriginalTowingOffsetY"] == nil then
towingModData["towBarOriginalTowingOffsetY"] = towingAttachment:getOffset():y()
towingModData["towBarOriginalTowingAttachmentId"] = attachmentA
end
local towingOffset = towingAttachment:getOffset()
towingAttachment:getOffset():set(towingOffset:x(), towingHeight, towingOffset:z())
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 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 zDirection = baseZ >= 0 and 1 or -1
local zShift = zDirection * spacingDistance
towedAttachment:getOffset():set(baseX, towedHeight, baseZ + zShift)
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 Y offset that was overridden by dynamic height.
local originalY = tonumber(towingModData["towBarOriginalTowingOffsetY"])
if originalY ~= nil then
local off = towingAttachment:getOffset()
towingAttachment:getOffset():set(off:x(), originalY, off:z())
end
end
towingModData["towBarOriginalTowingOffsetY"] = 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)
+117
View File
@@ -0,0 +1,117 @@
BTtow = {}
BTtow.Create = {}
BTtow.Init = {}
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 and TowBarMod.Config and tonumber(TowBarMod.Config.vanillaTowbarModelScaleMin)
local configuredMax = TowBarMod and 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 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
local index = math.floor((maxAbsTowZ + 0.1 - 1.0) * 10)
return math.max(0, math.min(TowbarMaxIndex, index))
end
return nil
end
local function getTowbarModelSlot(script)
local isVanilla = isVanillaScale(script)
local index = getTowbarIndexVanilla(script)
if not isVanilla then
local attachmentIndex = getTowbarIndexSmallScale(script)
if attachmentIndex ~= nil then
index = attachmentIndex
else
local offset = TowBarMod and TowBarMod.Config and tonumber(TowBarMod.Config.smallScaleTowbarIndexOffset) or 2
index = math.max(0, math.min(TowbarMaxIndex, index + offset))
end
end
return index, isVanilla
end
function BTtow.Create.towbar(vehicle, part)
if part == nil then return end
for j=0, TowbarVariantSize - 1 do
part:setModelVisible("towbar" .. j, false)
end
end
function BTtow.Init.towbar(vehicle, part)
if part == nil then return end
for j=0, TowbarVariantSize - 1 do
part:setModelVisible("towbar" .. j, false)
end
if vehicle:getModData()["isTowingByTowBar"] and vehicle:getModData()["towed"] then
local script = vehicle:getScript()
if script then
local index, isVanilla = getTowbarModelSlot(script)
local partId = part:getId()
local shouldShowOnThisPart = (isVanilla and partId == "towbar") or ((not isVanilla) and partId == "towbarLarge")
if shouldShowOnThisPart then
part:setModelVisible("towbar" .. index, true)
end
end
end
end
@@ -0,0 +1,71 @@
require 'Items/ProceduralDistributions'
require 'Items/SuburbsDistributions'
require 'Items/Distributions'
require 'Items/Distribution_BinJunk'
require 'Items/Distribution_ClosetJunk'
require 'Items/Distribution_DeskJunk'
require 'Items/Distribution_ShelfJunk'
require 'Items/Distribution_CounterJunk'
require 'Items/Distribution_SideTableJunk'
require 'Vehicles/VehicleDistributions'
require 'Vehicles/VehicleDistribution_GloveBoxJunk'
require 'Vehicles/VehicleDistribution_SeatJunk'
require 'Vehicles/VehicleDistribution_TrunkJunk'
----------------- TOW BAR -----------------------
-- Mirror Jack spawn chance into TowBar in container distributions (world + vehicle containers).
-- Intentionally excludes story-clutter floor placement tables (RandomizedWorldContent/StoryClutter).
local TOWBAR_ITEM_TYPE = "TowBar.TowBar"
local JACK_ITEM_TYPES = {
["Jack"] = true,
["Base.Jack"] = true,
}
local function addMissingTowBarsForJack(items)
if type(items) ~= "table" then return end
local jackCountByChance = {}
local towBarCountByChance = {}
for i = 1, #items, 2 do
local itemType = items[i]
local chance = tonumber(items[i + 1])
if type(itemType) == "string" and chance ~= nil then
if JACK_ITEM_TYPES[itemType] then
jackCountByChance[chance] = (jackCountByChance[chance] or 0) + 1
elseif itemType == TOWBAR_ITEM_TYPE then
towBarCountByChance[chance] = (towBarCountByChance[chance] or 0) + 1
end
end
end
for chance, jackCount in pairs(jackCountByChance) do
local missing = jackCount - (towBarCountByChance[chance] or 0)
for _ = 1, missing do
table.insert(items, TOWBAR_ITEM_TYPE)
table.insert(items, chance)
end
end
end
local function walkContainerDistributions(root, seen)
if type(root) ~= "table" or seen[root] then return end
seen[root] = true
for key, value in pairs(root) do
if key == "items" and type(value) == "table" then
addMissingTowBarsForJack(value)
elseif type(value) == "table" then
walkContainerDistributions(value, seen)
end
end
end
local seen = {}
walkContainerDistributions(ProceduralDistributions, seen)
walkContainerDistributions(SuburbsDistributions, seen)
walkContainerDistributions(Distributions, seen)
walkContainerDistributions(VehicleDistributions, seen)
walkContainerDistributions(ClutterTables, seen)
+268
View File
@@ -0,0 +1,268 @@
if isClient() then return end
local TowingCommands = {}
local Commands = {}
local TowBarItemType = "TowBar.TowBar"
local SyncDelayTicks = 2
local SnapshotIntervalTicks = 120
local pendingSync = {}
local snapshotTickCounter = 0
TowingCommands.wantNoise = getDebug() or false
local noise = function(msg)
if TowingCommands.wantNoise then
print("TowBarCommands: " .. msg)
end
end
local function queueSync(kind, player, args)
if not args then return end
table.insert(pendingSync, {
kind = kind,
ticks = SyncDelayTicks,
player = player,
args = args
})
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
return "trailer"
end
local function resolveAttachmentB(args, vehicleB)
if args and args.attachmentB then return args.attachmentB end
if vehicleB and vehicleB:getTowAttachmentSelf() then return vehicleB:getTowAttachmentSelf() end
return "trailerfront"
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 hasTowBarState(vehicle)
if not vehicle then return false end
local md = vehicle:getModData()
if not md then return false end
return md["isTowingByTowBar"] == true
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 broadcastAttach(vehicleA, vehicleB, attachmentA, attachmentB)
if not vehicleA or not vehicleB then return end
sendServerCommand("towbar", "forceAttachSync", {
vehicleA = vehicleA:getId(),
vehicleB = vehicleB:getId(),
attachmentA = attachmentA,
attachmentB = attachmentB
})
end
local function broadcastDetach(vehicleAId, vehicleBId)
sendServerCommand("towbar", "forceDetachSync", {
vehicleA = vehicleAId,
vehicleB = vehicleBId
})
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
end
local attachmentA = resolveAttachmentA(args, vehicleA)
local attachmentB = resolveAttachmentB(args, vehicleB)
if not isLinked(vehicleA, vehicleB) then
vehicleA:addPointConstraint(item.player, vehicleB, attachmentA, attachmentB)
end
if isLinked(vehicleA, vehicleB) then
broadcastAttach(vehicleA, vehicleB, attachmentA, attachmentB)
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
broadcastDetach(vehicleAId, vehicleBId)
end
local function snapshotActiveTowbarLinksServer()
local cell = getCell()
if not cell then return end
local list = cell:getVehicles()
if not list then return end
forEachCollectionItem(list, function(towingVehicle)
local towedVehicle = towingVehicle and towingVehicle:getVehicleTowing() or nil
if towingVehicle and towedVehicle and towedVehicle:getVehicleTowedBy() == towingVehicle then
if hasTowBarState(towingVehicle) or hasTowBarState(towedVehicle) then
local attachmentA = resolveAttachmentA(nil, towingVehicle)
local towedMd = towedVehicle:getModData()
local attachmentB = (towedMd and towedMd["towBarChangedAttachmentId"]) or resolveAttachmentB(nil, towedVehicle)
if attachmentA == attachmentB then
attachmentA = "trailer"
attachmentB = "trailerfront"
end
broadcastAttach(towingVehicle, towedVehicle, attachmentA, attachmentB)
end
end
end)
end
local function processPendingSync()
snapshotTickCounter = snapshotTickCounter + 1
if snapshotTickCounter >= SnapshotIntervalTicks then
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
processAttachSync(item)
elseif item.kind == "detach" then
processDetachSync(item)
end
else
table.insert(remaining, item)
end
end
pendingSync = remaining
end
function Commands.attachTowBar(player, args)
local vehicleA = getVehicleById(args.vehicleA)
local vehicleB = getVehicleById(args.vehicleB)
if not vehicleA then
noise("no such vehicle (A) id=" .. tostring(args.vehicleA))
return
end
if not vehicleB then
noise("no such vehicle (B) id=" .. tostring(args.vehicleB))
return
end
vehicleA:addPointConstraint(player, vehicleB, args.attachmentA, args.attachmentB)
end
function Commands.detachTowBar(player, args)
local towingVehicle = args.towingVehicle and getVehicleById(args.towingVehicle) or nil
local towedVehicle = args.vehicle and getVehicleById(args.vehicle) or nil
if not towingVehicle and towedVehicle then
towingVehicle = towedVehicle:getVehicleTowedBy()
end
if not towedVehicle and towingVehicle then
towedVehicle = towingVehicle:getVehicleTowing()
end
if towedVehicle then
towedVehicle:breakConstraint(true, false)
end
if towingVehicle and towingVehicle ~= towedVehicle then
towingVehicle:breakConstraint(true, false)
end
end
function Commands.consumeTowBar(player, args)
if not player then return end
local inventory = player:getInventory()
if not inventory then return end
local towBarItem = nil
local itemId = args and args.itemId
if itemId then
towBarItem = inventory:getItemWithID(itemId)
end
if not towBarItem then
towBarItem = inventory:getFirstTypeRecurse(TowBarItemType)
end
if not towBarItem then return end
local wasPrimary = player:isPrimaryHandItem(towBarItem)
local wasSecondary = player:isSecondaryHandItem(towBarItem)
player:removeFromHands(towBarItem)
inventory:Remove(towBarItem)
sendRemoveItemFromContainer(inventory, towBarItem)
if wasPrimary or wasSecondary then
sendEquip(player)
end
end
function Commands.giveTowBar(player, args)
if not player then return end
local inventory = player:getInventory()
if not inventory then return end
local towBarItem = inventory:AddItem(TowBarItemType)
if not towBarItem then return end
sendAddItemToContainer(inventory, towBarItem)
if args and args.equipPrimary then
player:setPrimaryHandItem(towBarItem)
sendEquip(player)
end
end
-- Compatibility aliases for older command names.
Commands.attachConstraint = Commands.attachTowBar
Commands.detachConstraint = Commands.detachTowBar
TowingCommands.OnClientCommand = function(module, command, player, args)
-- Only sync explicit towbar commands so vanilla towing stays untouched.
if module == "towbar" and command == "attachTowBar" then
queueSync("attach", player, args)
elseif module == "towbar" and command == "detachTowBar" then
queueSync("detach", player, args)
end
if module == "towbar" and Commands[command] then
local argStr = ""
args = args or {}
for k, v in pairs(args) do
argStr = argStr .. " " .. tostring(k) .. "=" .. tostring(v)
end
noise("received " .. module .. " " .. command .. " " .. tostring(player) .. argStr)
Commands[command](player, args)
end
end
Events.OnClientCommand.Add(TowingCommands.OnClientCommand)
Events.OnTick.Add(processPendingSync)
@@ -0,0 +1,3 @@
{
"IGUI_VehicleParttowbar": "Towbar Attachment"
}
@@ -0,0 +1,3 @@
{
"ItemName_TowBar.TowBar": "Tow Bar"
}
@@ -0,0 +1,3 @@
{
"Tooltip_TowBar": "A tow bar can be used<br>to tow vehicles like a trailer"
}
@@ -0,0 +1,17 @@
{
"UI_Text_Towing_turnOffParkingBrake": "Turn off parking brake",
"UI_Text_Towing_turnOnParkingBrake": "Turn on parking brake",
"UI_Text_Towing_noAviableVehicles": "No vehicles for attach<br>or can't attach",
"UI_Text_Towing_attach": "Attach",
"UI_Text_Towing_deattach": "Deattach",
"UI_Text_Towing_byRope": "by rope",
"UI_Text_Towing_byTowBar": "by tow bar",
"UI_Text_Towing_byHook": "by hook",
"UI_Text_Towing_flipUpright": "Flip upright",
"UI_Text_Towing_cannotDriveWhileTowed": "Cannot drive while being towed",
"UI_Text_PushByHands": "Push vehicle",
"UI_Text_PushByHands_Left": "Left",
"UI_Text_PushByHands_Right": "Right",
"UI_Text_PushByHands_Front": "Front",
"UI_Text_PushByHands_Behind": "Behind"
}
+3
View File
@@ -0,0 +1,3 @@
-- Build 42 registry file.
-- This mod currently uses only base registries (for example ItemType = base:normal),
-- so no custom identifier registrations are required here.
+15
View File
@@ -0,0 +1,15 @@
module TowBar
{
/*******************Towing Car*******************/
item TowBar
{
DisplayCategory = Tool,
Weight = 8.0,
ItemType = base:normal,
Icon = TowBar,
Tooltip = Tooltip_TowBar,
StaticModel = towbarModel,
WorldStaticModel = towbarModel,
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,315 @@
module Base
{
template vehicle Battery
{
part towbar
{
model towbar0
{
file = towbarModel,
offset = 0 -0.3 1.0,
}
model towbar1
{
file = towbarModel,
offset = 0 -0.3 1.1,
}
model towbar2
{
file = towbarModel,
offset = 0 -0.3 1.2,
}
model towbar3
{
file = towbarModel,
offset = 0 -0.3 1.3,
}
model towbar4
{
file = towbarModel,
offset = 0 -0.3 1.4,
}
model towbar5
{
file = towbarModel,
offset = 0 -0.3 1.5,
}
model towbar6
{
file = towbarModel,
offset = 0 -0.3 1.6,
}
model towbar7
{
file = towbarModel,
offset = 0 -0.3 1.7,
}
model towbar8
{
file = towbarModel,
offset = 0 -0.3 1.8,
}
model towbar9
{
file = towbarModel,
offset = 0 -0.3 1.9,
}
model towbar10
{
file = towbarModel,
offset = 0 -0.3 2.0,
}
model towbar11
{
file = towbarModel,
offset = 0 -0.3 2.1,
}
model towbar12
{
file = towbarModel,
offset = 0 -0.3 2.2,
}
model towbar13
{
file = towbarModel,
offset = 0 -0.3 2.3,
}
model towbar14
{
file = towbarModel,
offset = 0 -0.3 2.4,
}
model towbar15
{
file = towbarModel,
offset = 0 -0.3 2.5,
}
model towbar16
{
file = towbarModel,
offset = 0 -0.3 2.6,
}
model towbar17
{
file = towbarModel,
offset = 0 -0.3 2.7,
}
model towbar18
{
file = towbarModel,
offset = 0 -0.3 2.8,
}
model towbar19
{
file = towbarModel,
offset = 0 -0.3 2.9,
}
model towbar20
{
file = towbarModel,
offset = 0 -0.3 3.0,
}
model towbar21
{
file = towbarModel,
offset = 0 -0.3 3.1,
}
model towbar22
{
file = towbarModel,
offset = 0 -0.3 3.2,
}
model towbar23
{
file = towbarModel,
offset = 0 -0.3 3.3,
}
area = Engine,
mechanicRequireKey = false,
lua
{
create = BTtow.Create.towbar,
init = BTtow.Init.towbar,
}
}
part towbarLarge
{
model towbar0
{
file = towbarModelLarge,
offset = 0 -0.3 1.0,
}
model towbar1
{
file = towbarModelLarge,
offset = 0 -0.3 1.1,
}
model towbar2
{
file = towbarModelLarge,
offset = 0 -0.3 1.2,
}
model towbar3
{
file = towbarModelLarge,
offset = 0 -0.3 1.3,
}
model towbar4
{
file = towbarModelLarge,
offset = 0 -0.3 1.4,
}
model towbar5
{
file = towbarModelLarge,
offset = 0 -0.3 1.5,
}
model towbar6
{
file = towbarModelLarge,
offset = 0 -0.3 1.6,
}
model towbar7
{
file = towbarModelLarge,
offset = 0 -0.3 1.7,
}
model towbar8
{
file = towbarModelLarge,
offset = 0 -0.3 1.8,
}
model towbar9
{
file = towbarModelLarge,
offset = 0 -0.3 1.9,
}
model towbar10
{
file = towbarModelLarge,
offset = 0 -0.3 2.0,
}
model towbar11
{
file = towbarModelLarge,
offset = 0 -0.3 2.1,
}
model towbar12
{
file = towbarModelLarge,
offset = 0 -0.3 2.2,
}
model towbar13
{
file = towbarModelLarge,
offset = 0 -0.3 2.3,
}
model towbar14
{
file = towbarModelLarge,
offset = 0 -0.3 2.4,
}
model towbar15
{
file = towbarModelLarge,
offset = 0 -0.3 2.5,
}
model towbar16
{
file = towbarModelLarge,
offset = 0 -0.3 2.6,
}
model towbar17
{
file = towbarModelLarge,
offset = 0 -0.3 2.7,
}
model towbar18
{
file = towbarModelLarge,
offset = 0 -0.3 2.8,
}
model towbar19
{
file = towbarModelLarge,
offset = 0 -0.3 2.9,
}
model towbar20
{
file = towbarModelLarge,
offset = 0 -0.3 3.0,
}
model towbar21
{
file = towbarModelLarge,
offset = 0 -0.3 3.1,
}
model towbar22
{
file = towbarModelLarge,
offset = 0 -0.3 3.2,
}
model towbar23
{
file = towbarModelLarge,
offset = 0 -0.3 3.3,
}
area = Engine,
mechanicRequireKey = false,
lua
{
create = BTtow.Create.towbar,
init = BTtow.Init.towbar,
}
}
part Battery
{
area = Engine,
itemType = Base.CarBattery,
mechanicRequireKey = true,
category = engine,
table install
{
items
{
1
{
type = Base.Screwdriver,
count = 1,
keep = true,
equip = primary,
}
}
time = 100,
professions = ,
skills = ,
traits = ,
recipes = ,
test = Vehicles.InstallTest.Default,
door = EngineDoor,
}
table uninstall
{
items
{
1
{
type = Base.Screwdriver,
count = 1,
keep = true,
equip = primary,
}
}
time = 100,
test = Vehicles.UninstallTest.Battery,
}
lua
{
create = Vehicles.Create.Battery,
update = Vehicles.Update.Battery,
}
}
}
}
@@ -0,0 +1,284 @@
module Base
{
model towbarModel
{
mesh = vehicles/Towbar,
texture = Vehicles/Towbar_Texture,
scale = 0.01,
}
model towbarModelLarge
{
mesh = vehicles/Towbar,
texture = Vehicles/Towbar_Texture,
scale = 0.02022,
}
template vehicle Towbar
{
part towbar
{
model towbar0
{
file = towbarModel,
offset = 0 -0.3 1.0,
}
model towbar1
{
file = towbarModel,
offset = 0 -0.3 1.1,
}
model towbar2
{
file = towbarModel,
offset = 0 -0.3 1.2,
}
model towbar3
{
file = towbarModel,
offset = 0 -0.3 1.3,
}
model towbar4
{
file = towbarModel,
offset = 0 -0.3 1.4,
}
model towbar5
{
file = towbarModel,
offset = 0 -0.3 1.5,
}
model towbar6
{
file = towbarModel,
offset = 0 -0.3 1.6,
}
model towbar7
{
file = towbarModel,
offset = 0 -0.3 1.7,
}
model towbar8
{
file = towbarModel,
offset = 0 -0.3 1.8,
}
model towbar9
{
file = towbarModel,
offset = 0 -0.3 1.9,
}
model towbar10
{
file = towbarModel,
offset = 0 -0.3 2.0,
}
model towbar11
{
file = towbarModel,
offset = 0 -0.3 2.1,
}
model towbar12
{
file = towbarModel,
offset = 0 -0.3 2.2,
}
model towbar13
{
file = towbarModel,
offset = 0 -0.3 2.3,
}
model towbar14
{
file = towbarModel,
offset = 0 -0.3 2.4,
}
model towbar15
{
file = towbarModel,
offset = 0 -0.3 2.5,
}
model towbar16
{
file = towbarModel,
offset = 0 -0.3 2.6,
}
model towbar17
{
file = towbarModel,
offset = 0 -0.3 2.7,
}
model towbar18
{
file = towbarModel,
offset = 0 -0.3 2.8,
}
model towbar19
{
file = towbarModel,
offset = 0 -0.3 2.9,
}
model towbar20
{
file = towbarModel,
offset = 0 -0.3 3.0,
}
model towbar21
{
file = towbarModel,
offset = 0 -0.3 3.1,
}
model towbar22
{
file = towbarModel,
offset = 0 -0.3 3.2,
}
model towbar23
{
file = towbarModel,
offset = 0 -0.3 3.3,
}
area = Engine,
mechanicRequireKey = false,
lua
{
create = BTtow.Create.towbar,
init = BTtow.Init.towbar,
}
}
part towbarLarge
{
model towbar0
{
file = towbarModelLarge,
offset = 0 -0.3 1.0,
}
model towbar1
{
file = towbarModelLarge,
offset = 0 -0.3 1.1,
}
model towbar2
{
file = towbarModelLarge,
offset = 0 -0.3 1.2,
}
model towbar3
{
file = towbarModelLarge,
offset = 0 -0.3 1.3,
}
model towbar4
{
file = towbarModelLarge,
offset = 0 -0.3 1.4,
}
model towbar5
{
file = towbarModelLarge,
offset = 0 -0.3 1.5,
}
model towbar6
{
file = towbarModelLarge,
offset = 0 -0.3 1.6,
}
model towbar7
{
file = towbarModelLarge,
offset = 0 -0.3 1.7,
}
model towbar8
{
file = towbarModelLarge,
offset = 0 -0.3 1.8,
}
model towbar9
{
file = towbarModelLarge,
offset = 0 -0.3 1.9,
}
model towbar10
{
file = towbarModelLarge,
offset = 0 -0.3 2.0,
}
model towbar11
{
file = towbarModelLarge,
offset = 0 -0.3 2.1,
}
model towbar12
{
file = towbarModelLarge,
offset = 0 -0.3 2.2,
}
model towbar13
{
file = towbarModelLarge,
offset = 0 -0.3 2.3,
}
model towbar14
{
file = towbarModelLarge,
offset = 0 -0.3 2.4,
}
model towbar15
{
file = towbarModelLarge,
offset = 0 -0.3 2.5,
}
model towbar16
{
file = towbarModelLarge,
offset = 0 -0.3 2.6,
}
model towbar17
{
file = towbarModelLarge,
offset = 0 -0.3 2.7,
}
model towbar18
{
file = towbarModelLarge,
offset = 0 -0.3 2.8,
}
model towbar19
{
file = towbarModelLarge,
offset = 0 -0.3 2.9,
}
model towbar20
{
file = towbarModelLarge,
offset = 0 -0.3 3.0,
}
model towbar21
{
file = towbarModelLarge,
offset = 0 -0.3 3.1,
}
model towbar22
{
file = towbarModelLarge,
offset = 0 -0.3 3.2,
}
model towbar23
{
file = towbarModelLarge,
offset = 0 -0.3 3.3,
}
area = Engine,
mechanicRequireKey = false,
lua
{
create = BTtow.Create.towbar,
init = BTtow.Init.towbar,
}
}
}
}
+10
View File
@@ -0,0 +1,10 @@
name=Towbars
id=hrsys_towbars_testing
poster=../common/media/textures/preview.png
description=Tow bars for vehicle-to-vehicle towing.
author=Riggs0
category=vehicle
icon=../common/media/textures/tow_bar_icon.png
url=https://hudsonriggs.systems
modversion=1.0.2
versionMin=42.18.0
-111
View File
@@ -1,111 +0,0 @@
# Migration Guide
Auto-converted from PDF using `uv run --with pypdf`.
## Page 1
Migration Guide
Registries
In version 42.13, the way to add some identifiers has been changed. The IDs are
used in scripts and recipes.
Identifiers:
CharacterTrait
CharacterProfession
ItemTag
Brochure
Flier
ItemBodyLocation
ItemType
MoodleType
WeaponCategory
Newspaper
AmmoType
To add these identifiers and use them in scripts, you need to add them using Lua in
the registries.lua file. This file must be stored in the media folder. IT MUST
HAVE THIS EXACT NAME, and it is loaded before scripts and before any other Lua
files.
Example of adding IDs:
CharacterTrait.register("testmod:nimblefingers")
CharacterProfession.register("testmod:thief")
ItemTag.register("testmod:bobbypin")
Brochure.register("testmod:Village")
Flier.register("testmod:BirdMilk")
ItemBodyLocation.register("testmod:MiddleFinger")
ItemType.register("testmod:gamedev")
MoodleType.register("testmod:Happy")
WeaponCategory.register("testmod:birb")
Newspaper.register("testmod:BirdNews", List.of("BirdKnews_July30",
"BirdKnews_July2"))
local item_key = ItemKey.new("bullets_666", ItemType.NORMAL)
AmmoType.register("testmod:duck_bullets", item_key)
Example of usage in scripts:
## Page 2
character_trait_definition testmod:nimblefingers
{
IsProfessionTrait = false,
DisabledInMultiplayer = false,
CharacterTrait = testmod:nimblefingers,
Cost = 3,
UIName = UI_trait_nimblefingers,
UIDescription = UI_trait_nimblefingersDesc,
XPBoosts = Lockpicking=2,
GrantedRecipes =
Lockpicking;AlarmCheck;CreateBobbyPin;CreateBobbyPin2,
}
craftRecipe CreateBobbyPin
{
timedAction = Making,
Time = 40,
Tags = InHandCraft;CanBeDoneInDark,
needTobeLearn = true,
inputs
{
item 1 tags[base:screwdriver] mode:keep
flags[MayDegradeLight;Prop1],
item 1 [Base.Paperclip],
}
outputs
{
item 1 TestMod.HandmadeBobbyPin,
}
}
character_profession_definition testmod:thief
{
CharacterProfession = testmod:thief,
Cost = 2,
UIName = UI_prof_Thief,
IconPathName = profession_burglar2,
XPBoosts = Nimble=3;Sneak=2;Lightfoot=1;Lockpicking=2,
GrantedTraits = testmod:nimblefingers,
}
item HandmadeBobbyPin
{
Weight = 0.01,
ItemType = base:normal,
Icon = HandmadeBobbyPin,
Tags = testmod:bobbypin,
Tooltip = Tooltip_TestMod_BobbyPin,
WorldStaticModel = Paperclip,
## Page 3
Lua
Scripts
P.S.
Future content patches will include modding changes based on your reports and
requests, and new API documentation will gradually become available.
}
More details check in mod example
Some Lua API has been modified. If something has stopped working for you, check
the decompiled Java code.
There will be more API changes in upcoming unstable patches.
Item Script: DisplayName has been removed. Now translation is taken only
from Module.ItemId.
Item Script: Type has been renamed to ItemType and requires
the ItemType registry.
Tags now require the ItemTag registry.
It will also be useful to study script examples from the base game. They are now
generated from Java code and are read by game as before.
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
+2 -2
View File
@@ -1,10 +1,10 @@
name=Towbars
id=hrsys_towbars
poster=common/media/textures/preview.png
description=Towbar Towing Towed Towing Towbars. The thrid
description=Tow bars for vehicle-to-vehicle towing.
author=Riggs0
category=vehicle
versionMin=42.13.0
url=https://hudsonriggs.systems
modversion=1.0.0
modversion=1.0.2
icon=common/media/textures/tow_bar_icon.png