Working MP

This commit is contained in:
2026-02-07 16:16:23 -05:00
parent c5b4d31f3d
commit 36b6c697e3
8 changed files with 1756 additions and 466 deletions

View File

@@ -1,397 +1,66 @@
if not TowBarMod then TowBarMod = {} end if not TowBarMod then TowBarMod = {} end
if not TowBarMod.Hook then TowBarMod.Hook = {} end if not TowBarMod.Hook then TowBarMod.Hook = {} end
---------------------------------------------------------------------------
--- Tow bar functions
---------------------------------------------------------------------------
local TowBarTowMass = 200 local TowBarTowMass = 200
local CannotDriveWhileTowedFallbackText = "Cannot drive while being towed" local AutoReattachCooldownHours = 1 / 7200 -- 0.5 seconds
local CannotDriveMessageCooldownHours = 1 / 1800 -- 2 seconds TowBarMod.Hook.lastAutoReattachAtByVehicle = TowBarMod.Hook.lastAutoReattachAtByVehicle or {}
local ForcedTowBarReapplyCooldownHours = 1 / 3600 -- 1 second
TowBarMod.Hook.reappliedVehicleIds = TowBarMod.Hook.reappliedVehicleIds or {}
TowBarMod.Hook.pendingReapplyVehicleIds = TowBarMod.Hook.pendingReapplyVehicleIds or {}
TowBarMod.Hook.reapplyTickCounter = TowBarMod.Hook.reapplyTickCounter or 0
TowBarMod.Hook.lastCannotDriveMessageAtByPlayer = TowBarMod.Hook.lastCannotDriveMessageAtByPlayer or {}
TowBarMod.Hook.lastForcedReapplyAtByVehicle = TowBarMod.Hook.lastForcedReapplyAtByVehicle or {}
TowBarMod.Hook.lastTowBarVehicleIdByPlayer = TowBarMod.Hook.lastTowBarVehicleIdByPlayer or {}
function TowBarMod.Hook.isTowedByTowBar(vehicle) local function getTowBarItem(playerObj)
if not vehicle then return false end if not playerObj then return nil end
local inventory = playerObj:getInventory()
local modData = vehicle:getModData() if not inventory then return nil end
if not modData or not modData["isTowingByTowBar"] or not modData["towed"] then return inventory:getItemFromTypeRecurse("TowBar.TowBar")
return false
end
local towingVehicle = vehicle:getVehicleTowedBy()
if not towingVehicle then return false end
local towingModData = towingVehicle:getModData()
return towingModData and towingModData["isTowingByTowBar"] == true
end end
function TowBarMod.Hook.getCannotDriveWhileTowedText() local function setTowBarModelVisible(vehicle, isVisible)
local msg = getText("UI_Text_Towing_cannotDriveWhileTowed")
if not msg or msg == "UI_Text_Towing_cannotDriveWhileTowed" then
return CannotDriveWhileTowedFallbackText
end
return msg
end
function TowBarMod.Hook.showCannotDriveWhileTowed(playerObj)
if not playerObj then return end
local playerNum = playerObj:getPlayerNum()
local nowHours = getGameTime() and getGameTime():getWorldAgeHours() or nil
local lastHours = TowBarMod.Hook.lastCannotDriveMessageAtByPlayer[playerNum]
if nowHours and lastHours and (nowHours - lastHours) < CannotDriveMessageCooldownHours then
return
end
TowBarMod.Hook.lastCannotDriveMessageAtByPlayer[playerNum] = nowHours or 0
-- Match the overhead-style skill/feedback text.
HaloTextHelper.addBadText(playerObj, TowBarMod.Hook.getCannotDriveWhileTowedText())
end
function TowBarMod.Hook.installStartEngineBlock()
if not ISVehicleMenu or not ISVehicleMenu.onStartEngine then
return
end
if TowBarMod.Hook._startEngineBlockInstalled then
return
end
TowBarMod.Hook.defaultOnStartEngine = ISVehicleMenu.onStartEngine
ISVehicleMenu.onStartEngine = function(playerObj)
local vehicle = playerObj and playerObj:getVehicle() or nil
if TowBarMod.Hook.isTowedByTowBar(vehicle) then
TowBarMod.Hook.showCannotDriveWhileTowed(playerObj)
return
end
TowBarMod.Hook.defaultOnStartEngine(playerObj)
end
TowBarMod.Hook._startEngineBlockInstalled = true
end
function TowBarMod.Hook.getVehicleByIdSafe(vehicleId)
if getVehicleById then
return getVehicleById(vehicleId)
end
local cell = getCell()
if not cell then return nil end
local vehicles = cell:getVehicles()
if not vehicles then return nil end
for i = 0, vehicles:size() - 1 do
local vehicle = vehicles:get(i)
if vehicle and vehicle:getId() == vehicleId then
return vehicle
end
end
return nil
end
function TowBarMod.Hook.shouldBlockDriverSeatForTowBar(playerObj, vehicle, seat)
if not playerObj or not vehicle then return false end
if seat ~= 0 then return false end
return TowBarMod.Hook.isTowedByTowBar(vehicle)
end
function TowBarMod.Hook.hasPendingSeatSafetyAction(playerObj)
if not playerObj or not ISTimedActionQueue then return false end
return ISTimedActionQueue.hasActionType(playerObj, "ISSwitchVehicleSeat")
or ISTimedActionQueue.hasActionType(playerObj, "ISExitVehicle")
or ISTimedActionQueue.hasActionType(playerObj, "ISStopVehicle")
end
function TowBarMod.Hook.getBestSeatForDriverKick(playerObj, vehicle)
if not playerObj or not vehicle then return nil end
if ISVehicleMenu and ISVehicleMenu.getBestSwitchSeatExit then
local best = ISVehicleMenu.getBestSwitchSeatExit(playerObj, vehicle, 0)
if best and best > 0 and not vehicle:isSeatOccupied(best) and vehicle:canSwitchSeat(0, best) then
return best
end
end
for seat = 1, vehicle:getMaxPassengers() - 1 do
if not vehicle:isSeatOccupied(seat) and vehicle:canSwitchSeat(0, seat) then
return seat
end
end
return nil
end
function TowBarMod.Hook.tryMoveOrExitTowedDriver(playerObj, vehicle)
if not playerObj or not vehicle then return false end
if not TowBarMod.Hook.isTowedByTowBar(vehicle) then return false end
if not vehicle:isDriver(playerObj) and vehicle:getSeat(playerObj) ~= 0 then return false end
if TowBarMod.Hook.hasPendingSeatSafetyAction(playerObj) then return false end
local seatTo = TowBarMod.Hook.getBestSeatForDriverKick(playerObj, vehicle)
if seatTo then
if ISVehicleMenu and ISVehicleMenu.onSwitchSeat then
ISVehicleMenu.onSwitchSeat(playerObj, seatTo)
elseif ISSwitchVehicleSeat then
ISTimedActionQueue.add(ISSwitchVehicleSeat:new(playerObj, seatTo))
end
return true
end
if not vehicle:isStopped() then
return false
end
if ISVehicleMenu and ISVehicleMenu.onExit then
ISVehicleMenu.onExit(playerObj, 0)
return true
end
if ISExitVehicle then
ISTimedActionQueue.add(ISExitVehicle:new(playerObj))
return true
end
return false
end
function TowBarMod.Hook.forceTowBarReapply(vehicle, playerObj)
if not TowBarMod.Hook.hasTowBarTowData(vehicle) then return false end
local vehicleId = vehicle:getId()
local nowHours = getGameTime() and getGameTime():getWorldAgeHours() or nil
local lastHours = TowBarMod.Hook.lastForcedReapplyAtByVehicle[vehicleId]
if nowHours and lastHours and (nowHours - lastHours) < ForcedTowBarReapplyCooldownHours then
return false
end
TowBarMod.Hook.lastForcedReapplyAtByVehicle[vehicleId] = nowHours or 0
TowBarMod.Hook.clearReapplied(vehicle)
TowBarMod.Hook.queueTowBarReapply(vehicle)
return TowBarMod.Hook.tryTowBarReapply(vehicle, playerObj)
end
function TowBarMod.Hook.installSeatEntryBlock()
if not ISVehicleMenu or not ISVehicleMenu.onEnter or not ISVehicleMenu.onSwitchSeat then
return
end
if TowBarMod.Hook._seatEntryBlockInstalled then
return
end
TowBarMod.Hook.defaultOnEnter = ISVehicleMenu.onEnter
TowBarMod.Hook.defaultOnEnter2 = ISVehicleMenu.onEnter2
TowBarMod.Hook.defaultOnSwitchSeat = ISVehicleMenu.onSwitchSeat
ISVehicleMenu.onEnter = function(playerObj, vehicle, seat)
if TowBarMod.Hook.shouldBlockDriverSeatForTowBar(playerObj, vehicle, seat) then
TowBarMod.Hook.showCannotDriveWhileTowed(playerObj)
TowBarMod.Hook.forceTowBarReapply(vehicle, playerObj)
TowBarMod.Hook.tryMoveOrExitTowedDriver(playerObj, vehicle)
return
end
TowBarMod.Hook.defaultOnEnter(playerObj, vehicle, seat)
end
if TowBarMod.Hook.defaultOnEnter2 then
ISVehicleMenu.onEnter2 = function(playerObj, vehicle, seat)
if TowBarMod.Hook.shouldBlockDriverSeatForTowBar(playerObj, vehicle, seat) then
TowBarMod.Hook.showCannotDriveWhileTowed(playerObj)
TowBarMod.Hook.forceTowBarReapply(vehicle, playerObj)
TowBarMod.Hook.tryMoveOrExitTowedDriver(playerObj, vehicle)
return
end
TowBarMod.Hook.defaultOnEnter2(playerObj, vehicle, seat)
end
end
ISVehicleMenu.onSwitchSeat = function(playerObj, seatTo)
local vehicle = playerObj and playerObj:getVehicle() or nil
if TowBarMod.Hook.shouldBlockDriverSeatForTowBar(playerObj, vehicle, seatTo) then
TowBarMod.Hook.showCannotDriveWhileTowed(playerObj)
TowBarMod.Hook.forceTowBarReapply(vehicle, playerObj)
TowBarMod.Hook.tryMoveOrExitTowedDriver(playerObj, vehicle)
return
end
TowBarMod.Hook.defaultOnSwitchSeat(playerObj, seatTo)
end
TowBarMod.Hook._seatEntryBlockInstalled = true
end
function TowBarMod.Hook.enforceTowedVehicleEngineOff(playerObj)
if not playerObj or not instanceof(playerObj, "IsoPlayer") or not playerObj:isLocalPlayer() then return end
local vehicle = playerObj:getVehicle()
if not TowBarMod.Hook.isTowedByTowBar(vehicle) then return end
if vehicle:isEngineRunning() or vehicle:isEngineStarted() or vehicle:isStarting() then
vehicle:shutOff()
end
end
function TowBarMod.Hook.enforceTowedVehicleSeatSafety(playerObj)
if not playerObj or not instanceof(playerObj, "IsoPlayer") or not playerObj:isLocalPlayer() then return end
local vehicle = playerObj:getVehicle()
if not TowBarMod.Hook.isTowedByTowBar(vehicle) then return end
TowBarMod.Hook.lastTowBarVehicleIdByPlayer[playerObj:getPlayerNum()] = vehicle:getId()
if vehicle:isDriver(playerObj) or vehicle:getSeat(playerObj) == 0 then
TowBarMod.Hook.showCannotDriveWhileTowed(playerObj)
TowBarMod.Hook.forceTowBarReapply(vehicle, playerObj)
TowBarMod.Hook.tryMoveOrExitTowedDriver(playerObj, vehicle)
end
end
function TowBarMod.Hook.handleTowBarSeatEvent(character)
if not character or not instanceof(character, "IsoPlayer") or not character:isLocalPlayer() then return end
local playerObj = character
local vehicle = playerObj:getVehicle()
if not TowBarMod.Hook.isTowedByTowBar(vehicle) then return end
TowBarMod.Hook.lastTowBarVehicleIdByPlayer[playerObj:getPlayerNum()] = vehicle:getId()
TowBarMod.Hook.forceTowBarReapply(vehicle, playerObj)
TowBarMod.Hook.enforceTowedVehicleSeatSafety(playerObj)
end
function TowBarMod.Hook.OnEnterVehicle(character)
TowBarMod.Hook.handleTowBarSeatEvent(character)
end
function TowBarMod.Hook.OnSwitchVehicleSeat(character)
TowBarMod.Hook.handleTowBarSeatEvent(character)
end
function TowBarMod.Hook.OnExitVehicle(character)
if not character or not instanceof(character, "IsoPlayer") or not character:isLocalPlayer() then return end
local playerNum = character:getPlayerNum()
local vehicleId = TowBarMod.Hook.lastTowBarVehicleIdByPlayer[playerNum]
if not vehicleId then return end
local vehicle = TowBarMod.Hook.getVehicleByIdSafe(vehicleId)
if vehicle and TowBarMod.Hook.hasTowBarTowData(vehicle) then
TowBarMod.Hook.forceTowBarReapply(vehicle, character)
end
end
function TowBarMod.Hook.markReapplied(vehicle)
if not vehicle then return end if not vehicle then return end
local vehicleId = vehicle:getId()
TowBarMod.Hook.reappliedVehicleIds[vehicleId] = true
TowBarMod.Hook.pendingReapplyVehicleIds[vehicleId] = nil
end
function TowBarMod.Hook.clearReapplied(vehicle) local part = vehicle:getPartById("towbar")
if not vehicle then return end if part == nil then return end
local vehicleId = vehicle:getId()
TowBarMod.Hook.reappliedVehicleIds[vehicleId] = nil
TowBarMod.Hook.pendingReapplyVehicleIds[vehicleId] = nil
end
function TowBarMod.Hook.hasTowBarTowData(vehicle) for j = 0, 23 do
if not vehicle then return false end part:setModelVisible("towbar" .. j, false)
local modData = vehicle:getModData()
return modData and modData["isTowingByTowBar"] and modData["towed"]
end
function TowBarMod.Hook.queueTowBarReapply(vehicle)
if not TowBarMod.Hook.hasTowBarTowData(vehicle) then return end
local vehicleId = vehicle:getId()
TowBarMod.Hook.pendingReapplyVehicleIds[vehicleId] = true
end
function TowBarMod.Hook.tryTowBarReapply(vehicle, playerObj)
if not TowBarMod.Hook.hasTowBarTowData(vehicle) then
if vehicle then
TowBarMod.Hook.pendingReapplyVehicleIds[vehicle:getId()] = nil
end
return false
end end
local vehicleId = vehicle:getId() if not isVisible then
if TowBarMod.Hook.reappliedVehicleIds[vehicleId] then vehicle:doDamageOverlay()
TowBarMod.Hook.pendingReapplyVehicleIds[vehicleId] = nil
return true
end
local towingVehicle = vehicle:getVehicleTowedBy()
if not towingVehicle then return false end
local towingModData = towingVehicle:getModData()
if not towingModData or towingModData["isTowingByTowBar"] ~= true then return false end
local modData = vehicle:getModData()
if not modData.towBarOriginalScriptName then
modData.towBarOriginalScriptName = vehicle:getScriptName()
vehicle:transmitModData()
end
playerObj = playerObj or getPlayer()
if not playerObj then return false end
local attachmentA = towingVehicle:getTowAttachmentSelf() or "trailer"
local attachmentB = vehicle:getTowAttachmentSelf() or "trailerfront"
-- Re-run the same rigid tow offset + fake-trailer flow used during normal attach.
TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, vehicle, attachmentA, attachmentB)
vehicle:setScriptName("notTowingA_Trailer")
local args = { vehicleA = towingVehicle:getId(), vehicleB = vehicle:getId(), attachmentA = attachmentA, attachmentB = attachmentB }
sendClientCommand(playerObj, "vehicle", "attachTrailer", args)
ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, vehicle))
TowBarMod.Hook.markReapplied(vehicle)
return true
end
function TowBarMod.Hook.reapplyTowBarPostLoad(vehicle)
if not TowBarMod.Hook.hasTowBarTowData(vehicle) then
return return
end end
TowBarMod.Hook.queueTowBarReapply(vehicle)
TowBarMod.Hook.tryTowBarReapply(vehicle, getPlayer())
end
function TowBarMod.Hook.processPendingReapplies() local script = vehicle:getScript()
TowBarMod.Hook.reapplyTickCounter = TowBarMod.Hook.reapplyTickCounter + 1 if not script then
if TowBarMod.Hook.reapplyTickCounter < 15 then vehicle:doDamageOverlay()
return return
end end
TowBarMod.Hook.reapplyTickCounter = 0
local playerObj = getPlayer() local scale = script:getModelScale()
if not playerObj then return end if scale >= 1.5 and scale <= 2 then
local z = script:getPhysicsChassisShape():z()/2 - 0.1
local resolvedVehicleIds = {} part:setModelVisible("towbar" .. math.floor((z*2/3 - 1)*10), true)
for vehicleId, _ in pairs(TowBarMod.Hook.pendingReapplyVehicleIds) do
local vehicle = TowBarMod.Hook.getVehicleByIdSafe(vehicleId)
if vehicle and (TowBarMod.Hook.tryTowBarReapply(vehicle, playerObj) or not TowBarMod.Hook.hasTowBarTowData(vehicle)) then
table.insert(resolvedVehicleIds, vehicleId)
end
end end
for _, vehicleId in ipairs(resolvedVehicleIds) do vehicle:doDamageOverlay()
TowBarMod.Hook.pendingReapplyVehicleIds[vehicleId] = nil
end
end end
function TowBarMod.Hook.OnSpawnVehicle(vehicle) function TowBarMod.Hook.setVehiclePostAttach(playerObj, towedVehicle)
TowBarMod.Hook.reapplyTowBarPostLoad(vehicle) if not towedVehicle then return end
local towedModData = towedVehicle:getModData()
if towedModData and towedModData.towBarOriginalScriptName then
towedVehicle:setScriptName(towedModData.towBarOriginalScriptName)
end
towedVehicle:setMass(TowBarTowMass)
towedVehicle:setBrakingForce(0)
towedVehicle:constraintChanged()
towedVehicle:updateTotalMass()
end end
function TowBarMod.Hook.performAttachTowBar(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB) 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 if #(TowBarMod.Utils.getHookTypeVariants(towingVehicle, towedVehicle, true)) == 0 then return end
local towBarItem = playerObj:getInventory():getItemFromType("TowBar.TowBar") local towBarItem = getTowBarItem(playerObj)
if towBarItem ~= nil then if towBarItem ~= nil then
sendClientCommand(playerObj, "towbar", "consumeTowBar", { itemId = towBarItem:getID() }) sendClientCommand(playerObj, "towbar", "consumeTowBar", { itemId = towBarItem:getID() })
end end
@@ -401,6 +70,7 @@ function TowBarMod.Hook.performAttachTowBar(playerObj, towingVehicle, towedVehic
local towingModData = towingVehicle:getModData() local towingModData = towingVehicle:getModData()
local towedModData = towedVehicle:getModData() local towedModData = towedVehicle:getModData()
towedModData.towBarOriginalScriptName = towedVehicle:getScriptName() towedModData.towBarOriginalScriptName = towedVehicle:getScriptName()
towedModData.towBarOriginalMass = towedVehicle:getMass() towedModData.towBarOriginalMass = towedVehicle:getMass()
towedModData.towBarOriginalBrakingForce = towedVehicle:getBrakingForce() towedModData.towBarOriginalBrakingForce = towedVehicle:getBrakingForce()
@@ -411,44 +81,149 @@ function TowBarMod.Hook.performAttachTowBar(playerObj, towingVehicle, towedVehic
towingVehicle:transmitModData() towingVehicle:transmitModData()
towedVehicle:transmitModData() towedVehicle:transmitModData()
local part = towedVehicle:getPartById("towbar") setTowBarModelVisible(towedVehicle, true)
if part ~= nil then
if towedVehicle:getScript():getModelScale() >= 1.5 and towedVehicle:getScript():getModelScale() <= 2 then
local z = towedVehicle:getScript():getPhysicsChassisShape():z()/2 - 0.1
part:setModelVisible("towbar" .. math.floor((z*2/3-1)*10), true)
end
end
towedVehicle:doDamageOverlay() -- Match the known-good rigid tow path: fake trailer + vanilla attach command.
-- Fake a trailer script so the base "attachTrailer" command creates rigid point-constraint towing.
towedVehicle:setScriptName("notTowingA_Trailer") towedVehicle:setScriptName("notTowingA_Trailer")
local args = { vehicleA = towingVehicle:getId(), vehicleB = towedVehicle:getId(), attachmentA = attachmentA, attachmentB = attachmentB }
local args = {
vehicleA = towingVehicle:getId(),
vehicleB = towedVehicle:getId(),
attachmentA = attachmentA,
attachmentB = attachmentB
}
sendClientCommand(playerObj, "vehicle", "attachTrailer", args) sendClientCommand(playerObj, "vehicle", "attachTrailer", args)
ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle)) ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle))
TowBarMod.Hook.markReapplied(towedVehicle)
end end
function TowBarMod.Hook.setVehiclePostAttach(playerObj, towedVehicle) function TowBarMod.Hook.performDetachTowBar(playerObj, towingVehicle, towedVehicle)
if not towedVehicle then return end 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() local towedModData = towedVehicle:getModData()
if towedModData.towBarOriginalScriptName then if towedModData.towBarOriginalScriptName then
towedVehicle:setScriptName(towedModData.towBarOriginalScriptName) towedVehicle:setScriptName(towedModData.towBarOriginalScriptName)
end end
if towedModData.towBarOriginalMass ~= nil then
towedVehicle:setMass(TowBarTowMass) towedVehicle:setMass(towedModData.towBarOriginalMass)
towedVehicle:setBrakingForce(0) end
if towedModData.towBarOriginalBrakingForce ~= nil then
towedVehicle:setBrakingForce(towedModData.towBarOriginalBrakingForce)
end
towedVehicle:constraintChanged() towedVehicle:constraintChanged()
towedVehicle:updateTotalMass() 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
if not towingVehicle:isDriver(playerObj) then return end
local towedVehicle = towingVehicle:getVehicleTowing()
if not towedVehicle then return end
local towingModData = towingVehicle:getModData()
local towedModData = towedVehicle:getModData()
if not towingModData or not towedModData then return end
if not towingModData["isTowingByTowBar"] then return end
if not towedModData["isTowingByTowBar"] or not towedModData["towed"] then return end
local attachmentA = towingVehicle:getTowAttachmentSelf() or "trailer"
local attachmentB = towingVehicle:getTowAttachmentOther() 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
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
}
sendClientCommand(playerObj, "vehicle", "attachTrailer", args)
ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle))
end
local function tryAutoReattachFromCharacter(character)
if not character or not instanceof(character, "IsoPlayer") or not character:isLocalPlayer() then return end
local playerObj = character
local towingVehicle = playerObj:getVehicle()
if not towingVehicle then return end
if not towingVehicle:isDriver(playerObj) then return end
if not towingVehicle:getVehicleTowing() then return end
local modData = towingVehicle:getModData()
if not modData or not modData["isTowingByTowBar"] then return end
local vehicleId = towingVehicle:getId()
local nowHours = getGameTime() and getGameTime():getWorldAgeHours() or 0
local lastHours = TowBarMod.Hook.lastAutoReattachAtByVehicle[vehicleId]
if lastHours and (nowHours - lastHours) < AutoReattachCooldownHours then
return
end
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 end
function TowBarMod.Hook.attachByTowBarAction(playerObj, towingVehicle, towedVehicle) function TowBarMod.Hook.attachByTowBarAction(playerObj, towingVehicle, towedVehicle)
if playerObj == nil or towingVehicle == nil or towedVehicle == nil then return end if playerObj == nil or towingVehicle == nil or towedVehicle == nil then return end
local item = playerObj:getInventory():getItemFromTypeRecurse("TowBar.TowBar") local item = getTowBarItem(playerObj)
if item == nil then return end if item == nil then return end
if #(TowBarMod.Utils.getHookTypeVariants(towingVehicle, towedVehicle, true)) == 0 then return end if #(TowBarMod.Utils.getHookTypeVariants(towingVehicle, towedVehicle, true)) == 0 then return end
local hookPoint = towedVehicle:getAttachmentWorldPos("trailerfront", TowBarMod.Utils.tempVector1) local hookPoint = towedVehicle:getAttachmentWorldPos("trailerfront", TowBarMod.Utils.tempVector1)
@@ -482,52 +257,10 @@ function TowBarMod.Hook.attachByTowBarAction(playerObj, towingVehicle, towedVehi
)) ))
end end
function TowBarMod.Hook.performDeattachTowBar(playerObj, towingVehicle, towedVehicle)
TowBarMod.Utils.updateAttachmentsOnDefaultValues(towingVehicle, towedVehicle)
-- Detach from the towing side to mirror vanilla trailer detach behavior.
local args = { vehicle = towingVehicle:getId() }
sendClientCommand(playerObj, "vehicle", "detachTrailer", 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 })
towingVehicle:getModData()["isTowingByTowBar"] = false
towedModData["isTowingByTowBar"] = false
towedModData["towed"] = false
towedModData.towBarOriginalScriptName = nil
towedModData.towBarOriginalMass = nil
towedModData.towBarOriginalBrakingForce = nil
towingVehicle:transmitModData()
towedVehicle:transmitModData()
TowBarMod.Hook.clearReapplied(towedVehicle)
TowBarMod.Hook.lastForcedReapplyAtByVehicle[towedVehicle:getId()] = nil
local part = towedVehicle:getPartById("towbar")
if part ~= nil then
for j=0, 23 do
part:setModelVisible("towbar" .. j, false)
end
end
towedVehicle:doDamageOverlay()
end
function TowBarMod.Hook.deattachTowBarAction(playerObj, vehicle) function TowBarMod.Hook.deattachTowBarAction(playerObj, vehicle)
local towingVehicle = vehicle local towingVehicle = vehicle
local towedVehicle = vehicle:getVehicleTowing() local towedVehicle = vehicle and vehicle:getVehicleTowing() or nil
if vehicle:getVehicleTowedBy() then if vehicle and vehicle:getVehicleTowedBy() then
towingVehicle = vehicle:getVehicleTowedBy() towingVehicle = vehicle:getVehicleTowedBy()
towedVehicle = vehicle towedVehicle = vehicle
end end
@@ -562,22 +295,32 @@ function TowBarMod.Hook.deattachTowBarAction(playerObj, vehicle)
playerObj, playerObj,
300, 300,
TowBarMod.Config.lowLevelAnimation, TowBarMod.Config.lowLevelAnimation,
TowBarMod.Hook.performDeattachTowBar, TowBarMod.Hook.performDetachTowBar,
towingVehicle, towingVehicle,
towedVehicle towedVehicle
)) ))
end end
TowBarMod.Hook.installStartEngineBlock() function TowBarMod.Hook.OnSpawnVehicle(vehicle)
TowBarMod.Hook.installSeatEntryBlock() if not vehicle then return end
Events.OnGameStart.Add(TowBarMod.Hook.installStartEngineBlock)
Events.OnGameStart.Add(TowBarMod.Hook.installSeatEntryBlock) local modData = vehicle:getModData()
Events.OnPlayerUpdate.Add(TowBarMod.Hook.enforceTowedVehicleEngineOff) if not (modData and modData["isTowingByTowBar"] and modData["towed"]) then
Events.OnPlayerUpdate.Add(TowBarMod.Hook.enforceTowedVehicleSeatSafety) return
end
-- Keep behavior consistent after load/rejoin for active towbar tows.
vehicle:setScriptName("notTowingA_Trailer")
local playerObj = getPlayer()
if playerObj then
ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, vehicle))
else
TowBarMod.Hook.setVehiclePostAttach(nil, vehicle)
end
setTowBarModelVisible(vehicle, true)
end
Events.OnSpawnVehicleEnd.Add(TowBarMod.Hook.OnSpawnVehicle) Events.OnSpawnVehicleEnd.Add(TowBarMod.Hook.OnSpawnVehicle)
Events.OnEnterVehicle.Add(TowBarMod.Hook.OnEnterVehicle) Events.OnEnterVehicle.Add(TowBarMod.Hook.OnEnterVehicle)
Events.OnSwitchVehicleSeat.Add(TowBarMod.Hook.OnSwitchVehicleSeat) Events.OnSwitchVehicleSeat.Add(TowBarMod.Hook.OnSwitchVehicleSeat)
Events.OnExitVehicle.Add(TowBarMod.Hook.OnExitVehicle)
Events.OnTick.Add(TowBarMod.Hook.processPendingReapplies)

View File

@@ -126,6 +126,23 @@ function TowBarMod.UI.addUnhookOptionToMenu(playerObj, vehicle)
) )
end end
function TowBarMod.UI.addDriverReattachOptionToMenu(playerObj, towingVehicle)
local menu = getPlayerRadialMenu(playerObj:getPlayerNum())
if menu == nil then return end
if not towingVehicle then return end
if not towingVehicle:isDriver(playerObj) then return end
if not towingVehicle:getVehicleTowing() then return end
if not towingVehicle:getModData()["isTowingByTowBar"] then return end
menu:addSlice(
"Reattach Towbar (Debug)",
getTexture("media/textures/tow_bar_attach.png"),
TowBarMod.Hook.reattachTowBarFromDriverSeat,
playerObj,
towingVehicle
)
end
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
--- Mod compability --- Mod compability
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
@@ -146,9 +163,17 @@ end
function ISVehicleMenu.showRadialMenu(playerObj) function ISVehicleMenu.showRadialMenu(playerObj)
TowBarMod.UI.defaultShowRadialMenu(playerObj) TowBarMod.UI.defaultShowRadialMenu(playerObj)
if playerObj:getVehicle() then return end local vehicle = playerObj:getVehicle()
if vehicle then
if vehicle:isDriver(playerObj) and vehicle:getVehicleTowing() and vehicle:getModData()["isTowingByTowBar"] then
TowBarMod.UI.removeDefaultDetachOption(playerObj)
TowBarMod.UI.addUnhookOptionToMenu(playerObj, vehicle)
TowBarMod.UI.addDriverReattachOptionToMenu(playerObj, vehicle)
end
return
end
local vehicle = ISVehicleMenu.getVehicleToInteractWith(playerObj) vehicle = ISVehicleMenu.getVehicleToInteractWith(playerObj)
if vehicle == nil then return end if vehicle == nil then return end
if vehicle:getModData()["isTowingByTowBar"] then if vehicle:getModData()["isTowingByTowBar"] then

View File

@@ -88,13 +88,16 @@ function TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicl
towedAttachment:setUpdateConstraint(false) towedAttachment:setUpdateConstraint(false)
towedAttachment:setZOffset(0) towedAttachment:setZOffset(0)
local offset = towedAttachment:getOffset()
local zShift = offset:z() > 0 and 1 or -1
towedAttachment:getOffset():set(offset:x(), offset:y(), offset:z() + zShift)
local towedModData = towedVehicle:getModData() local towedModData = towedVehicle:getModData()
towedModData["isChangedTowedAttachment"] = true local alreadyShifted = towedModData["isChangedTowedAttachment"] and towedModData["towBarChangedAttachmentId"] == attachmentB
towedModData["towBarChangedAttachmentId"] = attachmentB if not alreadyShifted then
towedModData["towBarChangedOffsetZShift"] = zShift local offset = towedAttachment:getOffset()
local zShift = offset:z() > 0 and 1 or -1
towedAttachment:getOffset():set(offset:x(), offset:y(), offset:z() + zShift)
towedModData["isChangedTowedAttachment"] = true
towedModData["towBarChangedAttachmentId"] = attachmentB
towedModData["towBarChangedOffsetZShift"] = zShift
end
towedVehicle:transmitModData() towedVehicle:transmitModData()
end end

View File

@@ -1,23 +1,49 @@
if isClient() then return end if isClient() then return end
local TowingCommands = {}
local Commands = {} local Commands = {}
local TowBarItemType = "TowBar.TowBar" local TowBarItemType = "TowBar.TowBar"
function Commands.attachConstraint(player, args) TowingCommands.wantNoise = getDebug() or false
local vehicleA = args and getVehicleById(args.vehicleA)
local vehicleB = args and getVehicleById(args.vehicleB)
local attachmentA = args and args.attachmentA
local attachmentB = args and args.attachmentB
if not vehicleA or not vehicleB or not attachmentA or not attachmentB then return end
vehicleA:addPointConstraint(player, vehicleB, attachmentA, attachmentB) local noise = function(msg)
if TowingCommands.wantNoise then
print("TowBarCommands: " .. msg)
end
end end
function Commands.detachConstraint(player, args) function Commands.attachTowBar(player, args)
local vehicle = args and getVehicleById(args.vehicle) local vehicleA = getVehicleById(args.vehicleA)
if not vehicle then return end 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
vehicle:breakConstraint(true, false) 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 end
function Commands.consumeTowBar(player, args) function Commands.consumeTowBar(player, args)
@@ -61,13 +87,20 @@ function Commands.giveTowBar(player, args)
end end
end end
local function onClientCommand(module, command, player, args) -- Compatibility aliases for older command names.
if module ~= "towbar" then return end Commands.attachConstraint = Commands.attachTowBar
Commands.detachConstraint = Commands.detachTowBar
local fn = Commands[command] TowingCommands.OnClientCommand = function(module, command, player, args)
if fn then if module == "towbar" and Commands[command] then
fn(player, args or {}) 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
end end
Events.OnClientCommand.Add(onClientCommand) Events.OnClientCommand.Add(TowingCommands.OnClientCommand)

111
Migration Guide.md Normal file
View File

@@ -0,0 +1,111 @@
# 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.

BIN
Migration Guide.pdf Normal file

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.