diff --git a/42.13/media/lua/client/TowBar/TowingHooking.lua b/42.13/media/lua/client/TowBar/TowingHooking.lua index c91365c..9b1d7c9 100644 --- a/42.13/media/lua/client/TowBar/TowingHooking.lua +++ b/42.13/media/lua/client/TowBar/TowingHooking.lua @@ -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 - end - - local towingVehicle = vehicle:getVehicleTowedBy() - if not towingVehicle then return false end - - local towingModData = towingVehicle:getModData() - return towingModData and towingModData["isTowingByTowBar"] == true +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 -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 -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 + local part = vehicle:getPartById("towbar") + if part == nil then return 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 + for j = 0, 23 do + part:setModelVisible("towbar" .. j, 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) - - diff --git a/42.13/media/lua/client/TowBar/TowingUI.lua b/42.13/media/lua/client/TowBar/TowingUI.lua index 6631845..c623ed5 100644 --- a/42.13/media/lua/client/TowBar/TowingUI.lua +++ b/42.13/media/lua/client/TowBar/TowingUI.lua @@ -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 diff --git a/42.13/media/lua/client/TowBar/TowingUtils.lua b/42.13/media/lua/client/TowBar/TowingUtils.lua index a66b748..42b51eb 100644 --- a/42.13/media/lua/client/TowBar/TowingUtils.lua +++ b/42.13/media/lua/client/TowBar/TowingUtils.lua @@ -88,13 +88,16 @@ function TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicl towedAttachment:setUpdateConstraint(false) 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() - towedModData["isChangedTowedAttachment"] = true - towedModData["towBarChangedAttachmentId"] = attachmentB - towedModData["towBarChangedOffsetZShift"] = zShift + 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) + towedModData["isChangedTowedAttachment"] = true + towedModData["towBarChangedAttachmentId"] = attachmentB + towedModData["towBarChangedOffsetZShift"] = zShift + end towedVehicle:transmitModData() end diff --git a/42.13/media/lua/server/TowingCommands.lua b/42.13/media/lua/server/TowingCommands.lua index bc68ac5..dbbe5c1 100644 --- a/42.13/media/lua/server/TowingCommands.lua +++ b/42.13/media/lua/server/TowingCommands.lua @@ -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) diff --git a/Migration Guide.md b/Migration Guide.md new file mode 100644 index 0000000..34eb6f9 --- /dev/null +++ b/Migration Guide.md @@ -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. + diff --git a/Migration Guide.pdf b/Migration Guide.pdf new file mode 100644 index 0000000..5d10120 Binary files /dev/null and b/Migration Guide.pdf differ diff --git a/Project Zomboid_ API for Inventory Items.md b/Project Zomboid_ API for Inventory Items.md new file mode 100644 index 0000000..bc5cb06 --- /dev/null +++ b/Project Zomboid_ API for Inventory Items.md @@ -0,0 +1,1375 @@ +# Project Zomboid_ API for Inventory Items + +Auto-converted from PDF using `uv run --with pypdf`. + +## Page 1 + +Project Zomboid API for Inventory Items +Document version 1.0 + +## Page 2 + +Contents +Contents .................................................................................................................................. 1 Introduction ............................................................................................................................ 2 1. General description ........................................................................................................... 3 2. Timed Action Architecture ................................................................................................ 4 2.1 Modification of the ‘new’ function ................................................................................. 5 2.2 Realisation of the “getDuration” function ...................................................................... 8 2.3 Split of the “perform” function into “perform” and “complete” ....................................... 9 3. Realisation features of the long Timed Actions ............................................................ 12 4. The use of the sendClientCommand .............................................................................. 14 5. Synchronisation of the creation, deletion and modification of items and objects .... 16 +1 + +## Page 3 + +Introduction +This document is for the modders for Project Zomboid and it has the new +system +of +Inventory +Items +manipulation +described, +which +was +developed +to +prevent +cheating. +All inventory Items processing in multiplayer are transferred to the server side. +That +means, +that +while +it’s +still +possible +for +the +modification +on +the +client +side +to +create +an +Inventory +Item +and +put +it +to +the +inventory, +such +an +item +won’t +be +available +for +interaction +and +will +be +deleted +from +the +inventory +after +relogin. +All items should be created on the server side, and then transferred to the +client. +Thus +an +Inventory +Item +will +be +in +the +player’s +inventory +on +both +client +and +server +sides. +2 + +## Page 4 + +1. General description +The main way of an Inventory Item creation, deletion and manipulation is to +create +it +in +a +Timed +Action. +The +Inventory +Item +creation, +deletion +and +modification +should +be +done +on +the +server +side, +modifying +the +player’s +inventory +on +the +server +side. +The +player’s +inventory +on +the +client +side +is +modified +subsequently +to +be +identical +to +the +one +on +the +server +side. +An alternative way of an Inventory Item creation, deletion and manipulation is +to +send +a +command +using +“ +sendCommand +“ +or +“ +sendClientCommand +“ +functions. +To +do +that +you +have +to +implement +your +command +processor, +that +will +receive +the +necessary +data, +perform +cheating +checks, +create/delete/modify +the +Inventory +Item +on +the +server +side, +and +send +the +corresponding +packets +to +clients +for +synchronisation. +This +way +could +come +in +handy +while +dealing +with +the +Inventory +Item +manipulation +that +doesn’t +come +from +the +user's +actions, +e.g. +admin +powers. +3 + +## Page 5 + +2. Timed Action Architecture +The new implementation of Timed Action allows to execute Timed Action on +both +the +server +and +the +client. +To adapt the existing Timed Action to the new architecture the following +should +be +done: +1) Move the Timed Action file from media/lua/client folder to +media/lua/shared +one. +This +will +allow +the +script +to +be +loaded +by +the +client +as +well +as +the +server. +2) Make sure there is a variable of the same name with the same value for +each +new +function +argument. +3) Create the getDuration function, which returns the execution time +of +the +Timed +Action. +4) Move some code from the ‘perform’ function to the new ‘complete’ +function. +a) ‘perform’ function: i) performs the actions that are only needed on the client, e.g. +animation +and +sound +management; +ii) doesn’t contain any manipulations with any items or +objects; +iii) is performed on the client as well as in singleplayer mode; b) ‘complete’ function: i) contains manipulations with items and objects only; ii) is performed on the server as well as in single-player mode; iii) is executed after ‘perform’ in single-player mode; 5) Add the call of the function sending changes to clients to the ‘complete’ +function +(see +section +4). +Let’s take a closer look at the required changes. +4 + +## Page 6 + +2.1 Modification of the ‘new’ function +When running a Timed Action, the game sends it to the server side and +restores +the +Timed +Action +Lua +object. +This +is +necessary +for +execution +of +‘getDuration’ +and +‘complete’ +functions +on +the +server +side. +The +server +receives +the +list +of +the +‘new’ +function +arguments +and +searches +for +their +values +in +the +variables +(fields) +of +the +object. +Let’s assume there is a following code: +function ISPlaceTrap:new(playerObj, trap, damage, maxTime) local o = ISBaseTimedAction.new(self, playerObj); o.square = character:getCurrentSquare(); o.weapon = trap; o.damage = damage / 20; o.maxTime = maxTime; return o; end +Code piece 1 - Example of the incorrect ‘new’ function The game won’t be able to restore such an object on the server side, because +the +argument +names +aren’t +the +same +as +the +names +of +the +variables +in +the +object. +The following changes should be made in order for the server to be able to +send +a +Timed +Action +to +the +server: +1) Rename playerObj to character . There is a realisation of the +ISBaseTimedAction.new +function +In +Code +piece +2, +and +we +could +see +that +this +function +saves +the +argument +into +the +variable +named +‘character’. +2) Rename the “ trap ” argument to “ weapon ”, because the value of the +“ +trap +” +argument +is +saved +into +the +“ +weapon +” +variable. +3) It is necessary to get rid of saving the changed “ damage ” value into a +variable +with +the +same +name. +Otherwise, +if +we +run +such +a +timed +action +with +“ +damage +” +equal +to +1000, +then +an +object +will +be +created +with +the +“ +damage +” +variable +equal +to +50. +When +this +object +is +transferred +to +the +server +side, +this +constructor +is +called +again, +and +it’ll +get +the +value +of +50 +as +an +argument +of +damage. +As +a +result, +the +server +will +get +an +object +with +5 + +## Page 7 + +damage value equal to 2. To prevent this from happening, the incoming +values +should +be +kept +unchanged. +4) The execution time of the Timed Action also shouldn’t be transferred as +an +argument, +as +it +created +a +vulnerability +where +a +cheater +can +perform +Timed +Actions +instantly +changing +the +time +argument. +To +avoid +this, +the +game +uses +the +“ +getDuration +” +function +to +calculate +the +Timed +Action +execution +time +on +the +server +side. +function ISBaseTimedAction:new (character) local o = {} setmetatable(o, self) self.__index = self o.character = character; o.stopOnWalk = true; o.stopOnRun = true; o.stopOnAim = true; o.caloriesModifier = 1; o.maxTime = -1; return o end +Code piece 2 - realisation of the ISBaseTimedAction:new function After following all the suggestions you’ll get the code given in Code piece 3. +function ISPlaceTrap:new(character, weapon) local o = ISBaseTimedAction.new(self, character); o.square = character:getCurrentSquare(); o.weapon = weapon; o.maxTime = o:getDuration(); return o; end +Code piece 3 - An example of the correct ‘new’ function implementation Currently the game supports the following data types for the arguments of the +‘new’ +function: +1. BaseVehicle 2. BloodBodyPartType 3. BodyPart 4. Boolean 5. CraftRecipe 6. Double 7. EvolvedRecipe +6 + +## Page 8 + +8. FluidContainer 9. Integer 10. InventoryItem 11. IsoAnimal 12. IsoDeadBody 13. IsoGridSquare 14. IsoHutch.NestBox 15. IsoObject 16. IsoPlayer 17. ItemContainer 18. KahluaTableImpl 19. MultiStageBuilding.Stage 20. PZNetKahluaTableImpl 21. Recipe 22. Resource 23. SpriteConfigManager.ObjectInfo 24. String 25. VehiclePart 26. VehicleWindow 27. null Serialisation realisation is in the PZNetKahluaTableImpl class. +It +should +also +be +noted +that +while +passing +an +object, +the +client +sends +information +to +the +server +to +search +for +the +corresponding +object +on +the +server +side. +After +that +the +server +searches +for +the +corresponding +object +and +uses +it +as +an +argument. +For +this +reason, +the +game +cannot +transfer +as +an +argument +an +object +created +on +the +client +side. +7 + +## Page 9 + +2.2 Realisation of the “getDuration” function +The “ getDuration ” function is used by the server to fetch the time needed +to +execute +the +Timed +Action. +However, +the +client +can +also +use +this +function, +using +the +following +code +in +the +‘new’ +function. +o.maxTime = o:getDuration(); +Code piece 4 - the usage of the getDuration function in the ‘new’ function to +receive +the +execution +time +of +a +Timed +Action +The “ getDuration ” function returns the execution time of the Timed Action +in +cycles. +To +convert +the +time +in +seconds +to +the +value +that +the +“ +getDuration +” +function +should +return, +divide +the +time +by +0.02. +That +is, +for +a +Timed +Action +that +should +last +1 +second, +the +“ +getDuration +” +function +should +return +a +value +of +50. +For +an +action +that +is +executed +instantaneously, +it +is +recommended +to +return +the +value +‘1’. +For infinite Timed Actions the “ getDuration ” function should return the +value +‘-1’. +Also this function should return the value 1 if the TimedActionInstant cheat is +enabled. +Here’s an example usage of this function for Timed Action that lasts 1 second. +function ISPlaceTrap:getDuration() if self.character:isTimedActionInstant() then return 1; end return 50 end +Code piece 5 - Typical realisation of the getDuration function, returning the 1 +second +time +8 + +## Page 10 + +2.3 Split of the “perform” function into “perform” and “complete” It is necessary to split the “ perform ” function into 2 functions: “ perform ” +and +“ +complete +”. +Both +of +these +functions +are +executed +at +the +end +of +Timed +Action +execution. +The +client +executes +only +the +“ +perform +” +function, +and +the +server +executes +only +the +“ +complete +” +one. +Singleplayer +first +executes +“ +perform +” +and +then +“ +complete +”. +The “ perform ” function must contain code that is not related to modifying +items +and +objects. +This +can +be +code +to +control +sounds, +animation, +interaction +with +UI, +etc. +The “ complete ” function must contain code for items and objects +modification. +This +function +cannot +contain +code +that +cannot +be +called +on +the +server +side. +Let’s take a look at such a split, using ISAddFuelAction as an example. +function ISAddFuelAction:perform() self.character:stopOrTriggerSound(self.sound) self.item:setJobDelta(0.0); if self.item:IsDrainable() then self.item:Use() else self.character:removeFromHands(self.item) self.character:getInventory():Remove(self.item) end local cf = self.campfire local args = { x = cf.x, y = cf.y, z = cf.z, fuelAmt = self.fuelAmt } CCampfireSystem.instance:sendCommand(self.character, 'addFuel', args) -- needed to remove from queue / start next. ISBaseTimedAction.perform(self); end +Code piece 6 - The obsolete realisation of the ‘perform’ function for the +ISAddFuelAction +timed +action. +This function contains the code to control the interface and sound, manipulate +objects, +and +the +synchronisation +code +using +the +command. +9 + +## Page 11 + +Let’s keep in this function only the code for interface and sound control. +function ISAddFuelAction:perform() self.character:stopOrTriggerSound(self.sound) self.item:setJobDelta(0.0); -- needed to remove from queue / start next. ISBaseTimedAction.perform(self); end +Code piece 7 - The new realisation of the “perform” function of the +ISAddFuelAction +timed +action +We’ll create a new function called “ complete ” and put the code there to +manipulate +the +objects. +We also have to perform an action that was previously executed by sending the +“ +addFuel +” +command. +If +we +look +at +the +“ +addFuel +” +command +realisation, +we’ll +find +the +following +code: +function SCampfireSystemCommand(command, player, args) if command == 'addFuel' then local campfire = campfireAt(args.x, args.y, args.z) if campfire then campfire:addFuel(args.fuelAmt) end … +Code piece 8 - Implementation of the “addFuel” command handler Apparently, the coordinates of the campfire were sent to the server. Then the +server +was +finding +the +campfire +on +the +map +and +performed +the +“ +addFuel +” +function. +As +the +“ +complete +” +function +is +going +to +be +performed +at +the +server +side, +we +don’t +have +to +send +the +command +anymore. +We +can +directly +get +an +object +and +perform +the +required +function. +So we change the Code piece 9 to the code from the command handler you can +see +in +Code +piece +10: +CCampfireSystem.instance:sendCommand(self.character, 'addFuel', args) +Code piece 9 - Sending of the addFuel command from client to server +local campfire = campfireAt(args.x, args.y, args.z) +10 + +## Page 12 + +if campfire then campfire:addFuel(args.fuelAmt) end +Code piece 10 - Search and modifying of the object We also change the call of the local “ campfireAt ” function to its realisation, +which +is +given +above. +local function campfireAt(x, y, z) return SCampfireSystem.instance:getLuaObjectAt(x, y, z) end +Code piece 11 - Realisation of the campfireAt function As a result we get the following function: +function ISAddFuelAction:complete() if self.item:IsDrainable() then self.item:UseAndSync() else self.character:removeFromHands(self.item) self.character:getInventory():Remove(self.item) sendRemoveItemFromContainer(self.character:getInventory(),self.item) end local campfire = SCampfireSystem.instance:getLuaObjectAt(self.campfire.x, self.campfire.y, self.campfire.z) if campfire then campfire:addFuel(self.fuelAmt) end return true end +Code piece 12 - The new realisation of the ‘complete’ function of the +ISAddFuelAction +timed +action +11 + +## Page 13 + +3. Realisation features of the long +Timed +Actions +To implement Timed Actions that perform actions during execution, such as +pouring +liquids +or +reading +books, +it +is +necessary +to +use +an +additional +Anim +Event +emulation +API. +To implement such an action the implementation of the “ serverStart “ +function +is +needed, +which +is +executed +on +the +server +side +when +the +Timed +Action +starts. +In +this +function, +call +the +“ +emulateAnimEvent +” +function +to +set +up +the +AnimEvent +emulator. +After +that, +you +need +to +write +an +implementation +of +the +“animEvent” +function, +which +will +be +called +periodically +to +execute +a +part +of +the +whole +action. +To +stop +such +an +action, +the +server-side +function +self.netAction:forceComplete() +should +be +called. +You +could +use +the +“ +self.netAction:getProgress() +” +function +to +get +the +progress +of +the +action +in +the +animEvent +function +on +the +server +side. +Let’s take a look at the examples. First here’s the realisation of the serverStart +function: +function ISChopTreeAction:serverStart() self.axe = self.character:getPrimaryHandItem() emulateAnimEvent(self.netAction, 1500, "ChopTree", nil) end +Code piece 13 - Example of realisation of the serverStart function The “ emulateAnimEvent ” function takes the following arguments: 1) “NetTimedAction” - always equals to self.netAction ; 2) “duration” - a period in milliseconds; 3) “event” - a name, that will be sent to “ animEvent ” function; 4) “parameter” - a stock parameter for the “ animEvent ” function. An example of implementation of the “ animEvent ” function is shown in the +code +piece +14. +function ISChopTreeAction:animEvent(event, parameter) +12 + +## Page 14 + +if not isClient() then if event == 'ChopTree' then self.tree:WeaponHit(self.character, self.axe) self:useEndurance() if self.tree:getObjectIndex() == -1 then if isServer() then self.netAction:forceComplete() else self:forceComplete() end end end else if event == 'ChopTree' then self.tree:WeaponHitEffects(self.character, self.axe) end end end +Code piece 14 - Example of realisation of the “animEvent” function This function on the client only reproduces tree chopping effects using the +“ +WeaponHitEffects +” +function. +But +in +singleplayer, +and +on +the +server, +this +function +calculates +the +damage +the +tree +takes +from +axe +hit +using +the +self.tree:WeaponHit(self.character, +self.axe) +function. +Finally, when the tree is chopped down - it stops the infinite Timed Action: +if isServer() then self.netAction:forceComplete() else self:forceComplete() end Code piece 15 - Realisation of the end of the Timed Action +13 + +## Page 15 + +4. The use of the +sendClientCommand +The “ sendClientCommand ” function is executed on the client and sends a +command +whose +handler +is +on +the +server. +If +this +function +is +called +in +a +singleplayer +game +the +handler +of +this +command +will +also +be +called +by +the +game. +The “ sendClientCommand ” function has the following arguments: 1) IsoPlayer “player” - optional argument, player’s object; 2) String “module” - the name of the module for which the command is +sent; +3) String “command” - the command name; 4) KahluaTable “args” - table with arguments. +sendClientCommand(self.player, "vehicle", "getKey", { vehicle = self.vehicle:getId() }) +Code piece 16 - Sending the getKey command for the ‘vehicle’ module with an +argument +vehicle += +self.vehicle:getId() +Upon receiving the client command the server calls an OnClientCommand +event +with +the +following +arguments: +module, +command, +player, +args. +local VehicleCommands = {} local Commands = {} function Commands.getKey(player, args) local vehicle = getVehicleById(args.vehicle) if vehicle and checkPermissions(player, Capability.UseMechanicsCheat) then local item = vehicle:createVehicleKey() if item then player:getInventory():AddItem(item); sendAddItemToContainer(player:getInventory(), item); end else noise('no such vehicle id='..tostring(args.vehicle)) end end VehicleCommands.OnClientCommand = function(module, command, player, args) if module == 'vehicle' and Commands[command] then +14 + +## Page 16 + +Commands[command](player, args) end end Events.OnClientCommand.Add(VehicleCommands.OnClientCommand) +Code piece 17 - Example of implementation of the getKey command handler on +the +server +side +The Events.OnClientCommand.Add() function call sets the +OnClientCommand +event +handler. +After +executing +this +function, +receiving +any +command +will +result +in +calling +the +function +indicated +as +an +argument. +In +this +case +the +VehicleCommands.OnClientCommand +function +is +the +handler +for +the +OnClientCommand +event. +The VehicleCommands.OnClientCommand function checks if the +module +is +set +equal +to +“vehicle” +and +if +there +is +a +function +with +the +name +corresponding +to +the +command +name +in +the +Commands +table. +Then +the +function +calls +the +corresponding +function +by +sending +it +the +“player” +and +“args” +arguments. +The Commands.getKey function is a handler of the “ getKey ” command. +This +function +works +only +if +the +player +has +the +UseMechanicsCheat +permission. +In +case +if +the +player +has +the +appropriate +permission +and +the +required +vehicle +is +found +on +the +server +side +then +it +creates +the +key +InventoryItem +on +the +server +side +using +the +“ +createVehicleKey +” +function, +and +after +that +adds +it +to +the +player’s +inventory +and +sends +it +back +to +the +client +side. +15 + +## Page 17 + +5. Synchronisation of the creation, +deletion +and +modification +of +items +and +objects +All object changes made in the ‘complete’ function should be sent to clients. +The +following +functions +are +needed +for +that: +1. sendAddItemToContainer - adds the InventoryItem to the container; 2. sendRemoveItemFromContainer - deletes the InventoryItem from the +container; +3. syncItemFields - synchronises the following variables: condition, +remoteControlID, +uses, +currentAmmoCount, +haveBeenRepaired, +taintedWater, +wetness, +dirtyness, +bloodLevel, +hungChange, +weight, +alreadyReadPages, +customPages, +customName, +attachedSlot, +attachedSlotType, +attachedToModel, +fluidContainer, +moddata; +4. syncItemModData - synchronises the moddata; 5. syncHandWeaponFields - synchronises the following variables: +currentAmmoCount, +roundChambered, +containsClip, +spentRoundCount, +spentRoundChambered, +isJammed, +maxRange, +minRangeRanged, +clipSize, +reloadTime, +recoilDelay, +aimingTime, +hitChance, +minAngle, +minDamage, +maxDamage, +attachments, +moddata; +6. sendItemStats - synchronises the following variables: uses, usedDelta, +isFood, +frozen, +heat, +cookingTime, +minutesToCook, +minutesToBurn, +hungChange, +calories, +carbohydrates, +lipids, +proteins, +thirstChange, +fluReduction, +painReduction, +endChange, +reduceFoodSickness, +stressChange, +fatigueChange, +unhappyChange, +boredomChange, +poisonPower, +poisonDetectionLevel, +extraItems, +alcoholic, +baseHunger, +customName, +tainted, +fluidAmount, +isFluidContainer, +isCooked, +isBurnt, +freezingTime, +name; +7. transmitCompleteItemToClients - add an object to the map; 8. transmitRemoveItemFromSquare - delete an object from the map; 9. sync - synchronise the object change on the map; 10. transmitUpdatedSpriteToClients - synchronise the changed sprite. +16 + +## Page 18 + +Here are some examples of the code for how to create, delete and manipulate +items: +local candle = instanceItem("Base.Candle") self.character:getInventory():AddItem(candle); sendAddItemToContainer(self.character:getInventory(), candle); +Code piece 18 - Adding InventoryItem to the inventory +self.character:removeFromHands(self.weapon) self.character:getInventory():Remove(self.weapon); sendRemoveItemFromContainer(self.character:getInventory(), self.weapon); +Code piece 19 - Deleting the InventoryItem from the inventory Here are some examples of the code for how to create, delete and manipulate +objects. +local trap = IsoTrap.new(self.weapon, self.square:getCell(), self.square); self.square:AddTileObject(trap); trap:transmitCompleteItemToClients(); +Code piece 20 - Adding the IsoObject to the map +self.trap:getSquare():transmitRemoveItemFromSquare(self.trap); self.trap:removeFromWorld(); self.trap:removeFromSquare(); +Code piece 21 - Deleting the IsoObject from the map +self.generator:setActivated(self.activate) self.generator:sync() +Code piece 22 - IsoObject synchronisation on the map +17 + diff --git a/Project Zomboid_ API for Inventory Items.pdf b/Project Zomboid_ API for Inventory Items.pdf new file mode 100644 index 0000000..c52f6d6 Binary files /dev/null and b/Project Zomboid_ API for Inventory Items.pdf differ