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