--[[
Interface: 1.5.1.0 b6732

Copyright (C) GtX (Andy), 2018

Author: GtX | Andy
Date: 08.10.2018
Version: 1.0.0.0

History:
V 0.9.0.0 @ 08.10.2018 - FS17 Version (Not Released)
V 1.0.0.0 @ 31.07.2019 - Beta Version

Contact:
https://forum.giants-software.com
https://github.com/GtX-Andy

Important:
Not to be added to any mods / maps or modified from its current release form.
No changes are to be made to this script without permission from GtX | Andy

Darf nicht zu Mods / Maps hinzugefügt oder von der aktuellen Release-Form geändert werden.
An diesem Skript dürfen ohne Genehmigung von GtX | Andy keine Änderungen vorgenommen werden
]]


UniversalPassenger = {}
local UniversalPassenger_mt = Class(UniversalPassenger)

local universalPassenger = nil
UniversalPassenger.buildId = 1
UniversalPassenger.versionString = "1.0.1.0"

UniversalPassenger.LOG_INFO = 1
UniversalPassenger.LOG_WARNING = 2
UniversalPassenger.LOG_ERROR = 3
UniversalPassenger.LOG_DEBUG = 4
UniversalPassenger.LOG_DEV_DEBUG = 5

UniversalPassenger.LOG_LEVEL = {
    [1] = true,
    [2] = true,
    [3] = true,
    [4] = false,
    [5] = false
}

UniversalPassenger.ALLOW_ADDDONS = true
UniversalPassenger.DEVELOPMENT_MODE = UniversalPassenger.LOG_LEVEL[UniversalPassenger.LOG_DEV_DEBUG]

UniversalPassenger.LOG_PREFIX = {
    "  Info: [FS19_UniversalPassenger] - ",
    "  Warning: [FS19_UniversalPassenger] - ",
    "  Error: [FS19_UniversalPassenger] - ",
    "  Debug: [FS19_UniversalPassenger] - ",
    "  Dev Debug: [FS19_UniversalPassenger] - "
}

function UniversalPassenger:new(isServer, isClient, customEnvironment, baseDirectory, xmlFilename)
    if g_universalPassenger ~= nil then
        return
    end

    local self = {}
    setmetatable(self, UniversalPassenger_mt)

    self.isServer = isServer
    self.isClient = isClient

    self.customEnvironment = customEnvironment
    self.baseDirectory = baseDirectory

    self.baseI3D = "resources/UniversalPassenger.i3d"

    self.numGlobalXmlFiles = 0
    self.baseGlobalXML = xmlFilename
    self.globalXmlFiles = {xmlFilename}

    self.xmlFilenameToModName = {}
    self.xmlFilenameToModName[xmlFilename] = customEnvironment

    self.universalPassengerVehicles = {}

    self.addonXmlFilesFound = 0

    self.passengerVehicleCount = 0
    self.passengerVehicles = {}

    self.lastInteractionTime = -200

    self.creator = nil

    self.currentPassengerVehicle = nil
    self.currentPassengerSeatId = nil

    self.canEnterPassengerSeat = false

    self.lastUpdateState = nil
    self.enteredVehicles = {}

    self.texts = {}
    self.texts.moveToDriverSeat = g_i18n:getText("action_enterDriverSeat")
    self.texts.originalEnter = g_i18n:getText("action_enter")
    self.texts.enterPassengerSeat = g_i18n:getText("action_enterPassengerSeat")
    self.texts.moveToNextSeat = g_i18n:getText("action_nextPassengerSeat")
    self.texts.exitPassengerSeat = g_i18n:getText("action_exitPassengerSeat")

    self.texts.configurationOne = g_i18n:getText("config_allFarms")
    self.texts.configurationTwo = g_i18n:getText("config_farmOnly")
    self.texts.configurationThree = g_i18n:getText("config_disabled")

    addModEventListener(self)

    return self
end

function UniversalPassenger:loadMap(i3dFilename)
    -- TEST WITH HB MAP
    if UniversalPassenger.loadVehiclesFromXML ~= nil then
        UniversalPassenger.loadVehiclesFromXML = nil
    end

    self:loadGlobalXMLFiles(UniversalPassenger.ALLOW_ADDDONS, false)

    if self.isServer and self.isClient then
        if UniversalPassenger.DEVELOPMENT_MODE then
            self:consoleCommandActivateCreator("TRUE")
        else
            addConsoleCommand("upActivateCreatorMode", "Activate / Initiate creator mode.", "consoleCommandActivateCreator", self)
        end
    end
end

function UniversalPassenger:loadGlobalXMLFiles(loadAddons, isReload)
    if isReload then
        self.universalPassengerVehicles = {}
    end

    if loadAddons then
        self.globalXmlFiles = {self.baseGlobalXML}

        local activeMods = g_modManager:getActiveMods()
        for _, mod in pairs (activeMods) do
            if mod.modName ~= "FS19_UniversalPassenger" then
                local modDesc = loadXMLFile("tempModDesc", mod.modFile)

                if hasXMLProperty(modDesc, "modDesc.universalPassenger") then
                    local xmlFilename = getXMLString(modDesc, "modDesc.universalPassenger#xmlFile")
                    if xmlFilename ~= nil then
                        xmlFilename = mod.modDir .. xmlFilename

                        if self.xmlFilenameToModName[xmlFilename] == nil then
                            self.xmlFilenameToModName[xmlFilename] = mod.modName

                            if fileExists(xmlFilename) then
                                table.insert(self.globalXmlFiles, xmlFilename)
                            else
                                self:logPrint(UniversalPassenger.LOG_ERROR, "File '%s' does not exist in '%s'. Please check path in modDesc.", xmlFilename, mod.modName)
                            end
                        end
                    else
                        self:logPrint(UniversalPassenger.LOG_ERROR, "No XML filename path given at 'modDesc.universalPassenger#xmlFile' in '%s'", mod.modName)
                    end
                end

                delete(modDesc)
            end
        end
    end

    self.numGlobalXmlFiles = #self.globalXmlFiles
    if self.numGlobalXmlFiles > 1 then
        self.addonXmlFilesFound = self.numGlobalXmlFiles - 1
        if self.addonXmlFilesFound > 1 then
            self:logPrint(UniversalPassenger.LOG_INFO, "%d extra XML addon files has been found and loaded successfully.", self.addonXmlFilesFound)
        else
            self:logPrint(UniversalPassenger.LOG_INFO, "1 extra XML addon file has been found and loaded successfully.")
        end
    end

    local vehiclesLoaded, vehicleConflicts, missingCustomTargets = self:loadGlobalXmlVehicles()
    if vehiclesLoaded > 0 then
        if vehicleConflicts <= 0 then
            self:logPrint(UniversalPassenger.LOG_INFO, "Version %s has been loaded into %d Global Vehicles successfully.", UniversalPassenger.versionString, vehiclesLoaded)
        else
            self:logPrint(UniversalPassenger.LOG_INFO, "Version %s has been loaded into %d Global Vehicles successfully. %d vehicle conflicts were ignored!", UniversalPassenger.versionString, vehiclesLoaded, vehicleConflicts)
        end

        if missingCustomTargets > 0 then
            self:logPrint(UniversalPassenger.LOG_DEBUG, "%d Global Vehicles are missing custom targets!", missingCustomTargets)
        end
    end
end

function UniversalPassenger:loadGlobalXmlVehicles()
    local vehiclesLoaded = 0
    local vehicleConflicts = 0
    local missingCustomTargets = 0

    for _, xmlPath in pairs (self.globalXmlFiles) do
        local xmlFile = loadXMLFile("universalPassengerXML", xmlPath)

        if hasXMLProperty(xmlFile, "universalPassengerVehicles") then
            local i = 0
            while true do
                local key = string.format("universalPassengerVehicles.vehicle(%d)", i)
                if not hasXMLProperty(xmlFile, key) then
                    break
                end

                local xmlFilename = getXMLString(xmlFile, key .. "#xmlFilename")
                local modName = getXMLString(xmlFile, key .. "#modName")
                local actualModName = modName

                local pdlc = Utils.getNoNil(getXMLBool(xmlFile, key .. "#pdlc"), false)
                if pdlc then
                    modName = g_uniqueDlcNamePrefix .. modName
                end

                if xmlFilename ~= nil then
                    local directory
                    if modName ~= nil then
                        if g_modIsLoaded[modName] then
                            local mod = g_modManager:getModByName(modName)
                            if mod ~= nil then
                                local modDir = mod.modDir
                                xmlFilename = modDir .. xmlFilename

                                local modDirLength = modDir:len()
                                local modNameLength = actualModName:len()
                                directory = string.sub(modDir, 1, modDirLength - (modNameLength + 1))
                            else
                                xmlFilename = nil
                            end
                        else
                            xmlFilename = nil
                        end
                    end

                    if xmlFilename ~= nil then
                        local xmlFilenameLower = xmlFilename:lower()
                        if self.universalPassengerVehicles[xmlFilenameLower] == nil then
                            self.universalPassengerVehicles[xmlFilenameLower] = {key = key, xmlPath = xmlPath, directory = directory, pdlc = pdlc}

                            local storeItem = g_storeManager:getItemByXMLFilename(xmlFilename)
                            if storeItem ~= nil and storeItem.configurations ~= nil and storeItem.configurations["universalPassenger"] == nil then
                                storeItem.configurations["universalPassenger"] = {
                                    {desc = "", price = 0, dailyUpkeep = 0, isDefault = true, index = 1, name = self.texts.configurationOne},
                                    {desc = "", price = 0, dailyUpkeep = 0, isDefault = false, index = 2, name = self.texts.configurationTwo},
                                    {desc = "", price = 0, dailyUpkeep = 0, isDefault = false, index = 3, name = self.texts.configurationThree}
                                }
                            end

                            if UniversalPassenger.LOG_LEVEL[UniversalPassenger.LOG_DEBUG] then
                                if not Utils.getNoNil(getXMLBool(xmlFile, key .. ".passenger.passengerNode#customTargets"), false) then
                                    missingCustomTargets = missingCustomTargets + 1
                                end
                            end

                            vehiclesLoaded = vehiclesLoaded + 1
                        else
                            self:logPrint(UniversalPassenger.LOG_DEBUG, "Duplicate vehicle entry '%s' found in '%s'.", xmlFilename, xmlPath)
                            vehicleConflicts = vehicleConflicts + 1
                        end
                    end
                else
                    self:logPrint(UniversalPassenger.LOG_DEBUG, "No 'xmlFilename' given at %s in %s", key, xmlPath)
                end

                i = i + 1
            end
        else
            self:logPrint(UniversalPassenger.LOG_ERROR, "XML Property 'universalPassengerVehicles' not found in '%s'.", xmlPath)
        end

        delete(xmlFile)
        xmlFile = nil
    end

    return vehiclesLoaded, vehicleConflicts, missingCustomTargets
end

function UniversalPassenger:finalDelete()
    if self.creator == nil then
        removeConsoleCommand("upActivateCreatorMode")
    end

    removeModEventListener(self)
    -- g_messageCenter:unsubscribeAll(self)

    if g_universalPassenger ~= nil then
        getfenv(0)["g_universalPassenger"] = nil
    end
    universalPassenger = nil
end

function UniversalPassenger:addPassengerVehicle(vehicle)
    vehicle.isPassengerVehicle = true
    table.insert(self.passengerVehicles, vehicle)
    self.passengerVehicleCount = #self.passengerVehicles
end

function UniversalPassenger:removePassengerVehicle(vehicle)
    for i = 1, #self.passengerVehicles do
        if self.passengerVehicles[i] == vehicle then
            vehicle.isPassengerVehicle = nil

            table.remove(self.passengerVehicles, i)
            break
        end
    end

    self.passengerVehicleCount = #self.passengerVehicles

    if self.passengerVehicleCount <= 0 then
        self.passengerVehicleCount = 0
        self:updateActionEvents()
    end
end

function UniversalPassenger:setPlayerToVehicle(player, vehicle, isPassenger, seatId)
    if player == nil then
        self:logPrint(UniversalPassenger.LOG_DEBUG, "[ setPlayerToVehicle ] Failed to send player object!")
        if vehicle ~= nil then
            self:logPrint(UniversalPassenger.LOG_DEBUG, "Failed to enter passenger into vehicle!")
        else
            self:logPrint(UniversalPassenger.LOG_DEBUG, "Failed to exit passenger from vehicle!")
        end

        return false
    end

    if vehicle ~= nil then
        self.enteredVehicles[player] = vehicle

        if isPassenger then
            self:exitVehicles()

            self.currentPassengerVehicle = vehicle
            self.currentPassengerSeatId = seatId

            g_currentMission.player:onLeave()
        end
    else
        self.enteredVehicles[player] = nil

        if isPassenger then
            self.currentPassengerVehicle = nil
            self.currentPassengerSeatId = nil
        end
    end

    return true
end

function UniversalPassenger:onConnectionFinishedLoading(connection)
    -- Do this after all objects are loaded so the player objects are all available.
    -- Can not be part of the spec for this reason. May change for 2.0 with optimisations.
    for _, vehicle in pairs (self.enteredVehicles) do
        local objectId = NetworkUtil.getObjectId(vehicle)
        if objectId ~= nil then
            local spec = vehicle.spec_universalPassenger
            for seatId, seat in pairs (spec.passengerSeats) do
                if seat.used and seat.player ~= nil then
                    connection:sendEvent(UniversalPassengerEnterEvent:new(objectId, seat.player, seatId))
                end
            end
        end
    end
end

function UniversalPassenger:getGlobalVehicleData(filename)
    if filename ~= nil then
        local xmlFilenameLower = filename:lower()
        return self.universalPassengerVehicles[xmlFilenameLower]
    end
end

function UniversalPassenger:getVehicleAndSeatId()
    if self.currentPassengerVehicle ~= nil then
        if self.currentPassengerSeatId ~= nil then
            return self.currentPassengerVehicle, self.currentPassengerSeatId
        else
            local spec = self.currentPassengerVehicle.spec_universalPassenger
            for seatId, seat in pairs (spec.passengerSeats) do
                if seat.player ~= nil and seat.player == g_currentMission.player then
                    return self.currentPassengerVehicle, seatId
                end
            end
        end
    end

    return
end

function UniversalPassenger:getClosestPassengerVehicle()
    if g_currentMission.player == nil or g_currentMission.player.isCarryingObject or
        g_currentMission.controlledVehicle ~= nil or self.currentPassengerVehicle ~= nil then
        return nil
    end

    local passengerVehicle, distance = nil, math.huge
    for _, vehicle in pairs (self.passengerVehicles) do
        if self:getCanEnterAsPassenger(vehicle) then
            local px, _, pz = getWorldTranslation(g_currentMission.player.rootNode)
            local vx, _, vz = getWorldTranslation(vehicle.spec_enterable.enterReferenceNode)
            local vehicleDistance = MathUtil.vector2Length(px - vx, pz - vz)

            if vehicleDistance < 6.0 and vehicleDistance < distance then
                distance = vehicleDistance
                passengerVehicle = vehicle
            end
        end
    end

    return passengerVehicle
end

function UniversalPassenger:update(dt)
    if self.isClient then
        self.closestPassengerVehicle = nil

        if not g_gui:getIsGuiVisible() and not g_currentMission.isPlayerFrozen and self.passengerVehicleCount > 0 then
            self.closestPassengerVehicle = self:getClosestPassengerVehicle()
            self:updateActionEvents()
        end

        -- Only in SP or hosted
        if self.creator ~= nil and self.creator.charMovementActive then
            self.creator:update(dt)
        end
    end
end

function UniversalPassenger:keyEvent(unicode, sym, modifier, isDown)
    if self.creator ~= nil and self.creator.charMovementActive then
        self.creator:keyEvent(unicode, sym, modifier, isDown)
    end
end

function UniversalPassenger:getCanEnterAsPassenger(vehicle)
    if vehicle ~= nil and vehicle.isPassengerVehicle == true then
        local spec = vehicle.spec_universalPassenger

        if spec.farmAccessOnly then
            local farmId = g_currentMission:getFarmId()
            return g_currentMission.accessHandler:canFarmAccess(farmId, vehicle) and not vehicle.isBroken and spec.availablePassengerSeats > 0
        end

        return not vehicle.isBroken and spec.availablePassengerSeats > 0
    end

    return false
end

function UniversalPassenger:updateActionEvents()
    local state, text = 0, " "
    if g_currentMission.controlledVehicle ~= nil then
        local controlledVehicle = g_currentMission.controlledVehicle
        if self:getCanEnterAsPassenger(controlledVehicle) then
            state = 1
            text = self.texts.enterPassengerSeat
            self.closestPassengerVehicle = controlledVehicle
        end
    elseif self.closestPassengerVehicle ~= nil then
        state = 2
        text = self.texts.enterPassengerSeat
    elseif self.currentPassengerVehicle ~= nil then
        state = 3
        text = self.texts.exitPassengerSeat
    end

    if self.lastUpdateState ~= state then
        if self.eventIdEnterPassengerSeat ~= nil then
            g_inputBinding:setActionEventText(self.eventIdEnterPassengerSeat, text)
            g_inputBinding:setActionEventTextVisibility(self.eventIdEnterPassengerSeat, state ~= 0)
        end

        self.lastUpdateState = state
    end
end

function UniversalPassenger:jumpToNextVehicle()
    local vehicleCount = #self.passengerVehicles

    if vehicleCount <= 0 then
        return
    end

    local newIndex = 1
    local canEnter = false
    local originalIndex = 1

    if self.currentPassengerVehicle ~= nil then
        if vehicleCount < 2 then
            return
        end

        for i = 1, vehicleCount do
            if self.passengerVehicles[i] == self.currentPassengerVehicle then
                originalIndex = i
                newIndex = i + 1

                if newIndex > vehicleCount then
                    newIndex = 1
                end
            end
        end
    end

    repeat
        if g_currentMission.accessHandler:canFarmAccess(g_currentMission:getFarmId(), self.passengerVehicles[newIndex]) and not self.passengerVehicles[newIndex].isBroken and
            (self.passengerVehicles[newIndex].spec_enterable.isTabbable or self.passengerVehicles[newIndex].spec_locomotive ~= nil)
        then
            canEnter = true
        else
            newIndex = newIndex + 1

            if newIndex > vehicleCount then
                newIndex = 1
            end
        end
    until canEnter or newIndex == originalIndex

    if canEnter then
        if self:exitVehicles() then
            g_client:getServerConnection():sendEvent(UniversalPassengerRequestEnterEvent:new(self.passengerVehicles[newIndex]))
        end
    end
end

function UniversalPassenger:exitVehicles()
    if g_currentMission.controlledVehicle ~= nil then
        --g_currentMission:onLeaveVehicle()
        Enterable.actionEventLeave(g_currentMission.controlledVehicle)
    end

    local passengerVehicle, seatId = self:getVehicleAndSeatId()
    if passengerVehicle ~= nil then
        if self.isServer then
            passengerVehicle:exitPassengerSeat(seatId, UniversalPassengerSpec.EXIT)
            g_server:broadcastEvent(UniversalPassengerExitEvent:new(passengerVehicle, seatId, UniversalPassengerSpec.EXIT))
        else
            g_client:getServerConnection():sendEvent(UniversalPassengerExitEvent:new(passengerVehicle, seatId, UniversalPassengerSpec.EXIT))
            passengerVehicle:exitPassengerSeat(seatId, UniversalPassengerSpec.EXIT)
        end
    end

    return true
end

function UniversalPassenger:onInputNextPassengerVehicle(_, inputValue)
    if g_currentMission.isToggleVehicleAllowed then
        self:jumpToNextVehicle()
    end
end

function UniversalPassenger:onInputEnterPassengerSeat(_, inputValue)
    if g_time > self.lastInteractionTime + 200 then
        if self.currentPassengerVehicle ~= nil then
            self:exitVehicles()
        elseif self.closestPassengerVehicle ~= nil then
            g_client:getServerConnection():sendEvent(UniversalPassengerRequestEnterEvent:new(self.closestPassengerVehicle))
        end

        self.lastInteractionTime = g_time
        g_currentMission.lastInteractionTime = self.lastInteractionTime
    end
end

function UniversalPassenger:getIsPassenger()
    return self.currentPassengerVehicle ~= nil and self.currentPassengerSeatId ~= nil
end

function UniversalPassenger:registerActionEvents()
    if self.isClient then
        local actionEventId
        local eventAdded = false

        eventAdded, actionEventId = g_inputBinding:registerActionEvent(InputAction.NEXT_PASSENGER_VEHICLE, self, self.onInputNextPassengerVehicle, false, true, false, true)
        g_inputBinding:setActionEventTextVisibility(actionEventId, false)
        self.eventIdNextPassengerVehicle = actionEventId

        eventAdded, actionEventId = g_inputBinding:registerActionEvent(InputAction.ENTER_PASSENGER_SEAT, self, self.onInputEnterPassengerSeat, false, true, false, true)
        g_inputBinding:setActionEventTextVisibility(actionEventId, false)
        self.eventIdEnterPassengerSeat = actionEventId
    end
end

function UniversalPassenger:unregisterActionEvents()
    g_inputBinding:removeActionEventsByTarget(self)
end

-- Moved here so `Vehicle.lua does not wipe the input bindings when driver enters/leaves in MP.
-- This needs to be cleaned up better in the future. Just temporary but spec can be optimised better.
function UniversalPassenger:addPassengerActionEvents(vehicle)
    if self.isClient and (vehicle ~= nil and vehicle == self.currentPassengerVehicle) then
        local spec = vehicle.spec_universalPassenger
        spec.actionEventsTable = {}

        g_inputBinding:beginActionEventsModification(Player.INPUT_CONTEXT_NAME)

        local actionEventId
        local eventAdded = false

        eventAdded, actionEventId = g_inputBinding:registerActionEvent(InputAction.CAMERA_SWITCH, self, self.actionEventCameraSwitch, false, true, false, true)
        g_inputBinding:setActionEventTextPriority(actionEventId, GS_PRIO_LOW)
        g_inputBinding:setActionEventTextVisibility(actionEventId, true)
        spec.actionEventsTable.cameraSwitch = actionEventId

        eventAdded, actionEventId = g_inputBinding:registerActionEvent(InputAction.CAMERA_ZOOM_IN, self, self.actionEventCameraZoomIn, false, true, true, true)
        g_inputBinding:setActionEventTextVisibility(actionEventId, false)
        spec.actionEventsTable.cameraZoomIn = actionEventId

        eventAdded, actionEventId = g_inputBinding:registerActionEvent(InputAction.CAMERA_ZOOM_OUT, self, self.actionEventCameraZoomOut, false, true, true, true)
        g_inputBinding:setActionEventTextVisibility(actionEventId, false)
        spec.actionEventsTable.cameraZoomOut = actionEventId

        eventAdded, actionEventId = g_inputBinding:registerActionEvent(InputAction.RESET_HEAD_TRACKING, self, self.actionEventResetHeadTracking, false, true, false, true)
        g_inputBinding:setActionEventTextVisibility(actionEventId, false)
        spec.actionEventsTable.resetHeadTracking = actionEventId

        if spec.numPassengerSeats > 1 then
            eventAdded, actionEventId = g_inputBinding:registerActionEvent(InputAction.NEXT_PASSENGER_SEAT, self, self.actionEventNextPassengerSeat, false, true, false, true)
            spec.actionEventsTable.nextPassengerSeat = actionEventId
        end

        eventAdded, actionEventId = g_inputBinding:registerActionEvent(InputAction.ENTER, self, self.actionEventMoveToDriverSeat, false, true, false, true)
        g_inputBinding:setActionEventText(actionEventId, self.texts.moveToDriverSeat)
        g_inputBinding:setActionEventTextPriority(actionEventId, GS_PRIO_VERY_HIGH)
        g_inputBinding:setActionEventTextVisibility(actionEventId, false)
        spec.actionEventsTable.moveToDriverSeat = actionEventId

        g_inputBinding:endActionEventsModification()
    end
end

function UniversalPassenger:removePassengerActionEvents(vehicle)
    if vehicle == nil then
        return
    end

    local spec = vehicle.spec_universalPassenger
    for i, actionEventId in pairs (spec.actionEventsTable) do
        g_inputBinding:removeActionEvent(actionEventId)
    end

    spec.actionEventsTable = {}
end

function UniversalPassenger:actionEventCameraSwitch(actionName, inputValue, callbackState, isAnalog)
    if self:getIsPassenger() and not g_gui:getIsGuiVisible() then
        local spec = self.currentPassengerVehicle.spec_universalPassenger
        local seat = spec.passengerSeats[self.currentPassengerSeatId]
        if seat ~= nil then
            self.currentPassengerVehicle:setPassengerCameraIndex(seat, seat.cameraIndex + 1)
        end
    end
end

function UniversalPassenger:actionEventCameraZoomIn(actionName, inputValue, callbackState, isAnalog, isMouse)
    if self:getIsPassenger() and not g_gui:getIsGuiVisible() then
        local spec = self.currentPassengerVehicle.spec_universalPassenger
        if spec.activeCamera ~= nil and not g_gui:getIsGuiVisible() then
            local offset = -0.2
            if isMouse then
                offset = offset * InputBinding.MOUSE_WHEEL_INPUT_FACTOR
            end

            spec.activeCamera:zoomSmoothly(offset)
        end
    end
end

function UniversalPassenger:actionEventCameraZoomOut(actionName, inputValue, callbackState, isAnalog, isMouse)
    if self:getIsPassenger() and not g_gui:getIsGuiVisible() then
        local spec = self.currentPassengerVehicle.spec_universalPassenger
        if spec.activeCamera ~= nil and not g_gui:getIsGuiVisible() then
            local offset = 0.2
            if isMouse then
                offset = offset * InputBinding.MOUSE_WHEEL_INPUT_FACTOR
            end
            spec.activeCamera:zoomSmoothly(offset)
        end
    end
end

function UniversalPassenger:actionEventResetHeadTracking(actionName, inputValue, callbackState, isAnalog)
    if self:getIsPassenger() then
        centerHeadTracking()
    end
end

function UniversalPassenger:actionEventNextPassengerSeat(actionName, inputValue, callbackState, isAnalog)
    if self:getIsPassenger() and not g_gui:getIsGuiVisible() then
        local vehicle = self.currentPassengerVehicle
        if vehicle.spec_universalPassenger.canChangeSeats then
            local oldSeatId = self.currentPassengerSeatId
            if self.isServer then
                local newSeatId = vehicle:getNextSeat(oldSeatId)
                if newSeatId ~= 0 then
                    vehicle:cyclePassengerSeats(oldSeatId, newSeatId)
                    g_server:broadcastEvent(UniversalPassengerNextSeatEvent:new(vehicle, oldSeatId, newSeatId))
                end
            else
                g_client:getServerConnection():sendEvent(UniversalPassengerNextSeatEvent:new(vehicle, oldSeatId, oldSeatId))
            end
        end
    end
end

function UniversalPassenger:actionEventMoveToDriverSeat(actionName, inputValue, callbackState, isAnalog)
    local vehicle = self.currentPassengerVehicle
    if vehicle ~= nil and not g_gui:getIsGuiVisible() then
        if vehicle.spec_universalPassenger.canMoveToDriverSeat then
            g_currentMission:requestToEnterVehicle(vehicle)
        end
    end
end

function UniversalPassenger:logPrint(level, text, ...)
    if UniversalPassenger.LOG_LEVEL[level] == true then
        local prefix = UniversalPassenger.LOG_PREFIX[level]
        print(prefix .. string.format(text, ...))
    end
end

function UniversalPassenger:consoleCommandActivateCreator(suppressReturn)
    if self.creator == nil and UP_Creator ~= nil then
        local creator = UP_Creator:new()
        if creator ~= nil then
            if creator:load() then
                self.creator = creator

                if not Utils.stringToBoolean(suppressReturn) then
                    removeConsoleCommand("upActivateCreatorMode")

                    local activateText = self.creator:consoleCommandEnablePassengerCommands()
                    return "'Universal Passenger - Creator Mode' initiated successfully. \n" .. activateText
                end
            end
        end
    end

    return "Failed to activate Creator Mode!"
end

function universalPassengerInit(name, c)
    if g_universalPassenger == nil then
        if (name == "FS19_UniversalPassenger" or name == "FS19_UniversalPassenger_update") and c:len() == 3 then
            local modDir = g_currentModDirectory
            local xmlFilename = modDir .. "xml/BaseVehicles.xml"
            if fileExists(xmlFilename) then
                universalPassenger = UniversalPassenger:new(g_server ~= nil, g_dedicatedServerInfo == nil, name, modDir, xmlFilename)
                if universalPassenger ~= nil and universalPassengerAddSpec(modDir, name) then
                    getfenv(0)["g_universalPassenger"] = universalPassenger

                    if universalPassenger.isServer and universalPassenger.isClient then
                        source(modDir .. "scripts/misc/UP_Creator.lua")
                    end

                    source(modDir .. "scripts/misc/UP_ChangedFunctions.lua")

                    source(modDir .. "scripts/events/UniversalPassengerExitEvent.lua")
                    source(modDir .. "scripts/events/UniversalPassengerEnterEvent.lua")
                    source(modDir .. "scripts/events/UniversalPassengerNextSeatEvent.lua")
                    source(modDir .. "scripts/events/UniversalPassengerRequestEnterEvent.lua")

                    UP_ChangedFunctions.initFunctions()
                end
            end
        else
            getfenv(0)["g_universalPassengerError"] = {}
            g_universalPassengerError.c = c
            g_universalPassengerError.name = name
            g_universalPassengerError.startUpdateTime = 2000

            g_universalPassengerError.update = function(g_universalPassengerError, dt)
                if g_universalPassengerError ~= nil then
                    if g_universalPassengerError.startUpdateTime > 0 then
                        g_universalPassengerError.startUpdateTime = g_universalPassengerError.startUpdateTime - dt
                    else
                        local title = "Universal Passenger - Version " .. UniversalPassenger.versionString
                        local text = string.format(g_i18n:getText("universalPassenger_loadError"), g_universalPassengerError.name, g_universalPassengerError.c)
                        if g_client ~= nil then
                            if not g_gui:getIsGuiVisible() then
                                g_gui:showYesNoDialog({
                                    title = title,
                                    text = text,
                                    dialogType = DialogElement.TYPE_LOADING,
                                    callback = g_universalPassengerError.openModHubLink,
                                    target = nil,
                                    yesText = g_i18n:getText("button_ok"),
                                    noText = g_i18n:getText("button_modHubDownload")
                                })
                            end
                        else
                            print(title .. "  " .. text .. "\n    - https://farming-simulator.com/mods.php?lang=en&country=be&title=fs2019&filter=org&org_id=129652&page=0")
                            g_universalPassengerError:openModHubLink(true)
                        end
                    end
                end
            end

            g_universalPassengerError.openModHubLink = function(isYes)
                if isYes == false then
                    local language = g_languageShort
                    local link = "mods.php?lang=en&country=be&title=fs2019&filter=org&org_id=129652&page=0"
                    if language == "de" or language == "fr" then
                        link = "mods.php?lang=" .. language .. "&country=be&title=fs2019&filter=org&org_id=129652&page=0"
                    end

                    openWebFile(link, "")
                end

                removeModEventListener(g_universalPassengerError)
                getfenv(0)["g_universalPassengerError"] = nil
            end

            addModEventListener(g_universalPassengerError)
        end
    else
        g_universalPassenger:logPrint(3, "Trying to load mod twice! Only '1' copy is required in your mod folder. Please remove '%s'", name)
    end
end

function universalPassengerAddSpec(modDir, name)
    local inserted = 0

    g_specializationManager:addSpecialization("universalPassenger", "UniversalPassengerSpec", modDir .. "scripts/spec/UniversalPassengerSpec.lua")

    local vehicleTypes = g_vehicleTypeManager:getVehicleTypes()
    local specEnvName = name .. ".universalPassenger"
    local spec = g_specializationManager:getSpecializationObjectByName(specEnvName)

    if spec ~= nil then
        for typeName, vehicleType in pairs(vehicleTypes) do
            if vehicleType ~= nil and vehicleType.specializationsByName ~= nil then

                local doInsert = spec.prerequisitesPresent(vehicleType.specializations)
                if doInsert then
                    for specName, _ in pairs(vehicleType.specializationsByName) do
                        local start, _ = string.find(specName:lower(), "universalpassenger")
                        if start ~= nil then
                            doInsert = false
                            break
                        end
                    end
                end

                if doInsert then
                    g_vehicleTypeManager:addSpecialization(typeName, specEnvName)
                    inserted = inserted + 1
                end
            end
        end
    end

    if inserted > 0 then
        print("  Loaded Universal Passenger")

        return true
    else
        return false
    end
end

universalPassengerInit(g_currentModName, g_modManager:getModByName(g_currentModName).author)
