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.Hook then TowBarMod.Hook = {} end
---------------------------------------------------------------------------
--- Tow bar functions
---------------------------------------------------------------------------
local TowBarTowMass = 200
local CannotDriveWhileTowedFallbackText = "Cannot drive while being towed"
local CannotDriveMessageCooldownHours = 1 / 1800 -- 2 seconds
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 {}
local AutoReattachCooldownHours = 1 / 7200 -- 0.5 seconds
TowBarMod.Hook.lastAutoReattachAtByVehicle = TowBarMod.Hook.lastAutoReattachAtByVehicle or {}
function TowBarMod.Hook.isTowedByTowBar(vehicle)
if not vehicle then return false end
local modData = vehicle:getModData()
if not modData or not modData["isTowingByTowBar"] or not modData["towed"] then
return false
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 towingVehicle = vehicle:getVehicleTowedBy()
if not towingVehicle then return false end
local towingModData = towingVehicle:getModData()
return towingModData and towingModData["isTowingByTowBar"] == true
end
function TowBarMod.Hook.getCannotDriveWhileTowedText()
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)
local function setTowBarModelVisible(vehicle, isVisible)
if not vehicle then return end
local vehicleId = vehicle:getId()
TowBarMod.Hook.reappliedVehicleIds[vehicleId] = true
TowBarMod.Hook.pendingReapplyVehicleIds[vehicleId] = nil
local part = vehicle:getPartById("towbar")
if part == nil then return end
for j = 0, 23 do
part:setModelVisible("towbar" .. j, false)
end
function TowBarMod.Hook.clearReapplied(vehicle)
if not vehicle then return end
local vehicleId = vehicle:getId()
TowBarMod.Hook.reappliedVehicleIds[vehicleId] = nil
TowBarMod.Hook.pendingReapplyVehicleIds[vehicleId] = nil
end
function TowBarMod.Hook.hasTowBarTowData(vehicle)
if not vehicle then return false end
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
local vehicleId = vehicle:getId()
if TowBarMod.Hook.reappliedVehicleIds[vehicleId] then
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
if not isVisible then
vehicle:doDamageOverlay()
return
end
TowBarMod.Hook.queueTowBarReapply(vehicle)
TowBarMod.Hook.tryTowBarReapply(vehicle, getPlayer())
end
function TowBarMod.Hook.processPendingReapplies()
TowBarMod.Hook.reapplyTickCounter = TowBarMod.Hook.reapplyTickCounter + 1
if TowBarMod.Hook.reapplyTickCounter < 15 then
local script = vehicle:getScript()
if not script then
vehicle:doDamageOverlay()
return
end
TowBarMod.Hook.reapplyTickCounter = 0
local playerObj = getPlayer()
if not playerObj then return end
local resolvedVehicleIds = {}
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
local scale = script:getModelScale()
if scale >= 1.5 and scale <= 2 then
local z = script:getPhysicsChassisShape():z()/2 - 0.1
part:setModelVisible("towbar" .. math.floor((z*2/3 - 1)*10), true)
end
for _, vehicleId in ipairs(resolvedVehicleIds) do
TowBarMod.Hook.pendingReapplyVehicleIds[vehicleId] = nil
end
vehicle:doDamageOverlay()
end
function TowBarMod.Hook.OnSpawnVehicle(vehicle)
TowBarMod.Hook.reapplyTowBarPostLoad(vehicle)
function TowBarMod.Hook.setVehiclePostAttach(playerObj, towedVehicle)
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
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 = playerObj:getInventory():getItemFromType("TowBar.TowBar")
local towBarItem = getTowBarItem(playerObj)
if towBarItem ~= nil then
sendClientCommand(playerObj, "towbar", "consumeTowBar", { itemId = towBarItem:getID() })
end
@@ -401,6 +70,7 @@ function TowBarMod.Hook.performAttachTowBar(playerObj, towingVehicle, towedVehic
local towingModData = towingVehicle:getModData()
local towedModData = towedVehicle:getModData()
towedModData.towBarOriginalScriptName = towedVehicle:getScriptName()
towedModData.towBarOriginalMass = towedVehicle:getMass()
towedModData.towBarOriginalBrakingForce = towedVehicle:getBrakingForce()
@@ -411,44 +81,149 @@ function TowBarMod.Hook.performAttachTowBar(playerObj, towingVehicle, towedVehic
towingVehicle:transmitModData()
towedVehicle:transmitModData()
local part = towedVehicle:getPartById("towbar")
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
setTowBarModelVisible(towedVehicle, true)
towedVehicle:doDamageOverlay()
-- Fake a trailer script so the base "attachTrailer" command creates rigid point-constraint towing.
-- 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 }
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))
TowBarMod.Hook.markReapplied(towedVehicle)
end
function TowBarMod.Hook.setVehiclePostAttach(playerObj, towedVehicle)
if not towedVehicle then return 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
towedVehicle:setMass(TowBarTowMass)
towedVehicle:setBrakingForce(0)
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
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
function TowBarMod.Hook.attachByTowBarAction(playerObj, towingVehicle, towedVehicle)
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 #(TowBarMod.Utils.getHookTypeVariants(towingVehicle, towedVehicle, true)) == 0 then return end
local hookPoint = towedVehicle:getAttachmentWorldPos("trailerfront", TowBarMod.Utils.tempVector1)
@@ -482,52 +257,10 @@ function TowBarMod.Hook.attachByTowBarAction(playerObj, towingVehicle, towedVehi
))
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)
local towingVehicle = vehicle
local towedVehicle = vehicle:getVehicleTowing()
if vehicle:getVehicleTowedBy() then
local towedVehicle = vehicle and vehicle:getVehicleTowing() or nil
if vehicle and vehicle:getVehicleTowedBy() then
towingVehicle = vehicle:getVehicleTowedBy()
towedVehicle = vehicle
end
@@ -562,22 +295,32 @@ function TowBarMod.Hook.deattachTowBarAction(playerObj, vehicle)
playerObj,
300,
TowBarMod.Config.lowLevelAnimation,
TowBarMod.Hook.performDeattachTowBar,
TowBarMod.Hook.performDetachTowBar,
towingVehicle,
towedVehicle
))
end
TowBarMod.Hook.installStartEngineBlock()
TowBarMod.Hook.installSeatEntryBlock()
Events.OnGameStart.Add(TowBarMod.Hook.installStartEngineBlock)
Events.OnGameStart.Add(TowBarMod.Hook.installSeatEntryBlock)
Events.OnPlayerUpdate.Add(TowBarMod.Hook.enforceTowedVehicleEngineOff)
Events.OnPlayerUpdate.Add(TowBarMod.Hook.enforceTowedVehicleSeatSafety)
function TowBarMod.Hook.OnSpawnVehicle(vehicle)
if not vehicle then return end
local modData = vehicle:getModData()
if not (modData and modData["isTowingByTowBar"] and modData["towed"]) then
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.OnEnterVehicle.Add(TowBarMod.Hook.OnEnterVehicle)
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
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
---------------------------------------------------------------------------
@@ -146,9 +163,17 @@ end
function ISVehicleMenu.showRadialMenu(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:getModData()["isTowingByTowBar"] then

View File

@@ -88,13 +88,16 @@ function TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicl
towedAttachment:setUpdateConstraint(false)
towedAttachment:setZOffset(0)
local towedModData = towedVehicle:getModData()
local alreadyShifted = towedModData["isChangedTowedAttachment"] and towedModData["towBarChangedAttachmentId"] == attachmentB
if not alreadyShifted then
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()
towedModData["isChangedTowedAttachment"] = true
towedModData["towBarChangedAttachmentId"] = attachmentB
towedModData["towBarChangedOffsetZShift"] = zShift
end
towedVehicle:transmitModData()
end

View File

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