42.18 support
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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.
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user