--[[
Interface: 1.5.1.0 b6732

Copyright (C) GtX (Andy), 2019

Author: GtX | Andy
Date: 25.08.2019
Version: 1.0.0.0

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

History:
V 1.0.0.0 @ 25.08.2019 - Release Version
V 1.1.0.0 @ 07.10.2019 - Fix improper rotation of pallets when spawning.
V 1.2.0.0 @ 10.01.2020 - Fix wrapping compatibility issue with bales created by Class DLC Baler,
                         added support for pallets using fillUnit, fillVolume and design configuration options,
                         added support for contractors to access storage menu.

Important:
Free for use in other mods - no permission needed.
No changes are to be made to this script without permission from GtX | Andy

Frei verwendbar - keine erlaubnis nötig
An diesem Skript dürfen ohne Genehmigung von GtX | Andy keine Änderungen vorgenommen werden
]]


ObjectStorage = {}

ObjectStorage.BUILD_ID = 3
ObjectStorage.STACK_LIMIT = 6

ObjectStorage.STORAGE_TYPES = {
    ["SQUAREBALES"] = {isBales = true, isRoundBales = false},
    ["ROUNDBALES"] = {isBales = true, isRoundBales = true},
    ["PALLETS"] = {isBales = false, isRoundBales = false}
}

ObjectStorage.DISPLAY_TYPES = {
    ["OBJECT_COUNT"] = true,
    ["CAPACITY"] = true,
    ["NEXT_FILL_LEVEL"] = true,
    ["TOTAL_FILL_LEVEL"] = true,
    ["FREE_SPACE"] = true
}

ObjectStorage.PALLET_CONFIGURATION_KEYS = {
    fillUnit = "#fillUnitConfig",
    fillVolume = "#fillVolumeConfig",
    design = "#designConfig"
}

ObjectStorage_mt = Class(ObjectStorage, Placeable)
InitObjectClass(ObjectStorage, "ObjectStorage")

function ObjectStorage:new(isServer, isClient, customMt)
    local self = Placeable:new(isServer, isClient, customMt or ObjectStorage_mt)

    registerObjectClassName(self, "ObjectStorage")

    self.customTexts = {}
    self.activateText = ""
    self.showHelpList = 0

    self.isBaleStorage = false
    self.isRoundBaleStorage = false

    self.hasWrapShader = false

    self.inputTriggers = {}
    self.interactionTriggers = {}
    self.interactionTriggersHelp = {}

    self.storageAreas = {}
    self.protectionToStorageAreas = {}

    self.hasSeasonsSupport = false
    self.numberFermenting = 0

    self.languageNodes = nil

    self.debugSpawnNodes = false

    return self
end

function ObjectStorage:load(xmlFilename, x, y, z, rx, ry, rz, initRandom)
    if not ObjectStorage:superClass().load(self, xmlFilename, x, y, z, rx, ry, rz, initRandom) then
        return false
    end

    local isValid = true
    local spawnerGroup = {}

    local xmlFile = loadXMLFile("TempXML", xmlFilename)
    local customTextsKey = "placeable.objectStorage.customTexts"

    local typeName = Utils.getNoNil(getXMLString(xmlFile, "placeable.objectStorage#type"), "SQUAREBALES")
    self.storageTypeName = typeName:upper()

    local storageType = ObjectStorage.STORAGE_TYPES[self.storageTypeName]
    if storageType ~= nil then
        self.activateText = ObjectStorage.getCustomText(getXMLString(xmlFile, customTextsKey .. ".openMenu#l10n"), "input_MENU", false)

        self.customTexts.storageTitle = ObjectStorage.getCustomText(getXMLString(xmlFile, customTextsKey .. ".storageTitle#l10n"), "shopItem_farmStorage", false)
        self.customTexts.availableHeader = ObjectStorage.getCustomText(getXMLString(xmlFile, customTextsKey .. ".availableHeader#l10n"), "ui_objects", true)
        self.customTexts.capacityHeader = ObjectStorage.getCustomText(getXMLString(xmlFile, customTextsKey .. ".capacityHeader#l10n"), "shop_capacity", true)
        self.customTexts.nextLevelHeader = ObjectStorage.getCustomText(getXMLString(xmlFile, customTextsKey .. ".nextLevelHeader#l10n"), "modHub_object", true)
        self.customTexts.totalLevelHeader = ObjectStorage.getCustomText(getXMLString(xmlFile, customTextsKey .. ".totalLevelHeader#l10n"), "ui_total", true)
        self.customTexts.fermentingHeader = ObjectStorage.getCustomText(getXMLString(xmlFile, customTextsKey .. ".fermentingHeader#l10n"), "info_fermenting", true)
        self.customTexts.numToSpawnHeader = ObjectStorage.getCustomText(getXMLString(xmlFile, customTextsKey .. ".numToSpawnHeader#l10n"), "button_select", true)
        self.customTexts.storageAreaHeader = ObjectStorage.getCustomText(getXMLString(xmlFile, customTextsKey .. ".storageAreaHeader#l10n"), "statistic_fillType", true)
        self.customTexts.fermentingButton = ObjectStorage.getCustomText(getXMLString(xmlFile, customTextsKey .. ".fermentingButton#l10n"), "info_fermenting", false)

        if storageType.isBales then
            self.isBaleStorage = true

            if storageType.isRoundBales then
                self.isRoundBaleStorage = true
            end
        end

        self:loadTriggers(xmlFile, "placeable.objectStorage.inputTriggers.inputTrigger", self.inputTriggers, false)
        if #self.inputTriggers == 0 then
            isValid = false
            g_logManager:xmlWarning(xmlFilename, "No 'inputTrigger' was found! A minimum of one trigger is required.")
        end

        self:loadTriggers(xmlFile, "placeable.objectStorage.interactionTriggers.interactionTrigger", self.interactionTriggers, true)
        if #self.interactionTriggers == 0 then
            isValid = false
            g_logManager:xmlWarning(xmlFilename, "No 'interactionTrigger' was found! A minimum of one trigger is required.")
        end

        local i = 0
        while true do
            local storageAreaKey = string.format("placeable.objectStorage.storageAreas.storageArea(%d)", i)
            if not hasXMLProperty(xmlFile, storageAreaKey) then
                break
            end

            local fillTypeName = getXMLString(xmlFile, storageAreaKey .. "#fillType")
            local fillType = g_fillTypeManager:getFillTypeByName(fillTypeName)
            if fillType ~= nil then
                if self.storageAreas[fillType.index] == nil then

                    self.storageAreas[fillType.index] = {
                        capacity = 0,
                        storedObjects = {},
                        defaultFillLevel = 0,
                        title = fillType.title,
                        image = fillType.hudOverlayFilename,
                        unitType = g_i18n:getText("unit_bale"),
                        objectUnitType = g_i18n:getText("unit_liter"),
                        supportsFermenting = false
                    }

                    local storageArea = self.storageAreas[fillType.index]

                    if self.isBaleStorage and fillType.index == FillType.SILAGE then
                        storageArea.supportsFermenting = (g_seasons ~= nil and g_seasons.grass ~= nil)
                        self.hasSeasonsSupport = storageArea.supportsFermenting
                    end

                    local title = getXMLString(xmlFile, storageAreaKey .. "#title")
                    if title ~= nil then
                        storageArea.title = g_i18n:getText(title)
                    end

                    local image = getXMLString(xmlFile, storageAreaKey .. "#imageFilename")
                    if image ~= nil then
                        storageArea.image = Utils.getFilename(image, self.baseDirectory)
                    end

                    local unitType = getXMLString(xmlFile, storageAreaKey .. "#unitType")
                    if unitType ~= nil then
                        storageArea.unitType = g_i18n:getText(unitType)
                    end

                    local objectUnitType = getXMLString(xmlFile, storageAreaKey .. "#objectUnitType")
                    if objectUnitType ~= nil then
                        storageArea.objectUnitType = g_i18n:getText(objectUnitType)
                    end

                    if hasXMLProperty(xmlFile, storageAreaKey .. ".maximumAcceptedSize") then
                        local width = getXMLFloat(xmlFile, storageAreaKey .. ".maximumAcceptedSize#width")
                        local length = getXMLFloat(xmlFile, storageAreaKey .. ".maximumAcceptedSize#length")

                        if self.isBaleStorage then
                            if self.isRoundBaleStorage then
                                local diameter = getXMLFloat(xmlFile, storageAreaKey .. ".maximumAcceptedSize#diameter")

                                if width ~= nil and diameter ~= nil then
                                    storageArea.acceptedObjectSize = {width = width, diameter = diameter}
                                end
                            else
                                local height = getXMLFloat(xmlFile, storageAreaKey .. ".maximumAcceptedSize#height")

                                if width ~= nil and height ~= nil and length ~= nil then
                                    storageArea.acceptedObjectSize = {width = width, height = height, length = length}
                                end
                            end
                        else
                            if width ~= nil and length ~= nil then
                                storageArea.acceptedObjectSize = {width = width, length = length}
                            end
                        end
                    end

                    local visibilityNodes = I3DUtil.indexToObject(self.nodeId, getXMLString(xmlFile, storageAreaKey .. ".visibilityNodes#node"))
                    if visibilityNodes ~= nil then
                        local numInGroup = getNumOfChildren(visibilityNodes)
                        if numInGroup > 0 then
                            local linkedNodes = {}
                            local linkedShaders = {}
                            storageArea.visibilityNodes = {}

                            local j = 0
                            while true do
                                local linkedNodesKey = string.format("%s.visibilityNodes.linkedNodes.linkedNode(%d)", storageAreaKey, j)
                                if not hasXMLProperty(xmlFile, linkedNodesKey) then
                                    break
                                end

                                local visibilityNode = getXMLInt(xmlFile, linkedNodesKey .. "#visibilityNode")
                                local node = I3DUtil.indexToObject(self.nodeId, getXMLString(xmlFile, linkedNodesKey .. "#node"))

                                if visibilityNode ~= nil and node ~= nil then
                                    linkedNodes[visibilityNode] = node
                                end

                                j = j + 1
                            end

                            j = 0
                            while true do
                                local linkedShaderKey = string.format("%s.visibilityNodes.linkedShaders.linkedShader(%d)", storageAreaKey, j)
                                if not hasXMLProperty(xmlFile, linkedShaderKey) then
                                    break
                                end

                                local visibilityNode = getXMLInt(xmlFile, linkedShaderKey .. "#visibilityNode")
                                local node = I3DUtil.indexToObject(self.nodeId, getXMLString(xmlFile, linkedShaderKey .. "#node"))

                                if visibilityNode ~= nil and node ~= nil then
                                    local parameterName = getXMLString(xmlFile, linkedShaderKey .. "#parameterName")

                                    local baseParameters = StringUtil.getVectorNFromString(getXMLString(xmlFile, linkedShaderKey .. "#baseParameters", 4))
                                    if baseParameters == nil then
                                        baseParameters = {getShaderParameter(node, parameterName)}
                                    end

                                    local targetParameters = StringUtil.getVectorNFromString(getXMLString(xmlFile, linkedShaderKey .. "#targetParameters", 4))
                                    if targetParameters ~= nil then
                                        setShaderParameter(node, parameterName, baseParameters[1], baseParameters[2], baseParameters[3], baseParameters[4], false)

                                        linkedShaders[visibilityNode] = {
                                            node = node,
                                            parameterName = parameterName,
                                            baseParameters = baseParameters,
                                            targetParameters = targetParameters
                                        }
                                    end
                                end

                                j = j + 1
                            end

                            self.hideVisNodesWhenPlaced = Utils.getNoNil(getXMLBool(xmlFile, storageAreaKey .. ".visibilityNodes#hideAfterPlacement"), false)

                            local calculateCapacity = Utils.getNoNil(getXMLBool(xmlFile, storageAreaKey .. ".visibilityNodes#calculateCapacity"), false)

                            local hasWrapShader = Utils.getNoNil(getXMLBool(xmlFile, storageAreaKey .. ".visibilityNodes#hasWrapShader"), false)
                            if hasWrapShader then
                                if calculateCapacity then
                                    if (numInGroup % 2) == 0 then
                                        self.hasWrapShader = true
                                    else
                                        g_logManager:xmlWarning(xmlFilename, "'%s.visibilityNodes#hasWrapShader' is not valid! Feature only available with an even number of visibility nodes.", storageAreaKey)
                                    end
                                else
                                    g_logManager:xmlWarning(xmlFilename, "'%s.visibilityNodes#hasWrapShader' is not valid! Feature only available when 'calculateCapacity' is true.", storageAreaKey)
                                end
                            end

                            for id = 0, numInGroup - 1 do
                                local visNode = {}
                                visNode.node = getChildAt(visibilityNodes, id)
                                visNode.rigidBodyType = getRigidBodyType(visNode.node)

                                setVisibility(visNode.node, self.hideVisNodesWhenPlaced)
                                setRigidBodyType(visNode.node, "NoRigidBody")

                                visNode.hasShader = false
                                if self.hasWrapShader then
                                    if getHasClassId(visNode.node , ClassIds.SHAPE) and getHasShaderParameter(visNode.node, "colorScale") then
                                        visNode.hasShader = true
                                    end
                                end

                                if linkedNodes[id] ~= nil then
                                    visNode.linkedNode = linkedNodes[id]
                                    visNode.rigidBodyType = getRigidBodyType(visNode.linkedNode)
                                    setVisibility(visNode.linkedNode, self.hideVisNodesWhenPlaced)
                                    setRigidBodyType(visNode.linkedNode,"NoRigidBody")
                                end

                                visNode.linkedShader = linkedShaders[id]

                                table.insert(storageArea.visibilityNodes, visNode)
                            end

                            if calculateCapacity then
                                storageArea.capacity = #storageArea.visibilityNodes
                            end

                            linkedNodes = nil
                            linkedShaders = nil
                        end
                    end

                    if storageArea.capacity == 0 then
                        local capacity = getXMLInt(xmlFile, storageAreaKey .. "#capacity")
                        if capacity ~= nil then
                            storageArea.capacity = capacity
                        else
                            isValid = false
                            g_logManager:xmlWarning(xmlFilename, "No 'capacity' set at %s.", storageAreaKey)
                        end
                    end

                    if hasXMLProperty(xmlFile, storageAreaKey .. ".displays") then
                        local displayId = 0
                        while true do
                            local displayKey = string.format("%s.displays.display(%d)", storageAreaKey, displayId)
                            if not hasXMLProperty(xmlFile, displayKey) then
                                break
                            end

                            local display = {}
                            local displayTypeName = Utils.getNoNil(getXMLString(xmlFile, displayKey .. "#type"), "OBJECT_COUNT")
                            if displayTypeName ~= nil then
                                local displayType = displayTypeName:upper()

                                if ObjectStorage.DISPLAY_TYPES[displayType] then
                                    local node = I3DUtil.indexToObject(self.nodeId, getXMLString(xmlFile, displayKey .. "#node"))
                                    if node ~= nil then
                                        local numMeshes = getNumOfChildren(node)
                                        if numMeshes > 0 then
                                            display.node = node
                                            display.showOnEmpty = Utils.getNoNil(getXMLBool(xmlFile, displayKey .. "#showOnEmpty"), true)
                                            display.maxValue = (10 ^ (numMeshes)) - 1

                                            if displayType == "CAPACITY" then
                                                self:setDisplayValue(display, storageArea.capacity)
                                            else
                                                self:setDisplayValue(display, 0)
                                            end

                                            if storageArea.displays == nil then
                                                storageArea.displays = {}
                                            end

                                            if storageArea.displays[displayType] == nil then
                                                storageArea.displays[displayType] = {}
                                            end

                                            table.insert(storageArea.displays[displayType], display)
                                        else
                                            g_logManager:xmlWarning(xmlFilename, "No mesh children found at node! '%s'", displayKey)
                                        end
                                    end
                                else
                                    g_logManager:xmlWarning(xmlFilename, "Display type '%s' is not supported! Use OBJECT_COUNT or CAPACITY or NEXT_FILL_LEVEL or TOTAL_FILL_LEVEL or FREE_SPACE instead.", displayType)
                                end
                            end

                            displayId = displayId + 1
                        end
                    end

                    local spawners = I3DUtil.indexToObject(self.nodeId, getXMLString(xmlFile, storageAreaKey .. ".spawners#node"))
                    if spawners ~= nil then
                        local numInGroup = getNumOfChildren(spawners)
                        if numInGroup > 0 then
                            local stackHeight = Utils.getNoNil(getXMLInt(xmlFile, storageAreaKey .. ".spawners#stackHeight"), 1)
                            stackHeight = math.min(stackHeight, ObjectStorage.STACK_LIMIT)

                            local stackOffset = Utils.getNoNil(getXMLFloat(xmlFile, storageAreaKey .. ".spawners#stackOffset"), 1.0)

                            storageArea.debugSpawnNodes = Utils.getNoNil(getXMLBool(xmlFile, storageAreaKey .. ".spawners#debug"), false)
                            if storageArea.debugSpawnNodes then
                                self.debugSpawnNodes = true
                            end

                            local width = Utils.getNoNil(getXMLFloat(xmlFile, storageAreaKey .. ".spawners.checkSize#width"), 1.0)
                            local height = Utils.getNoNil(getXMLFloat(xmlFile, storageAreaKey .. ".spawners.checkSize#height"), 1.0)
                            local length = Utils.getNoNil(getXMLFloat(xmlFile, storageAreaKey .. ".spawners.checkSize#length"), 1.0)
                            local yOffset = Utils.getNoNil(getXMLFloat(xmlFile, storageAreaKey .. ".spawners.checkSize#yOffset"), 0.0)
                            local initialYOffset = Utils.getNoNil(getXMLFloat(xmlFile, storageAreaKey .. ".spawners.checkSize#initialYOffset"), 0.0)

                            local defaultFilename = getXMLString(xmlFile, storageAreaKey .. ".spawners.defaultObject#filename")
                            local directoryPrefix = getXMLString(xmlFile, storageAreaKey .. ".spawners.defaultObject#directoryPrefix")

                            if directoryPrefix == nil then
                                storageArea.defaultObject = Utils.getFilename(defaultFilename, self.baseDirectory)
                            else
                                -- This allows a dlc directory to be called. This way DLC objects can be set as defaultObject.
                                -- Example: <defaultObject filename="johnDeereCottonPack/objects/cottonModules/roundbaleCotton.i3d" directoryPrefix="$pdlcdir$" fillLevel="10000"/>
                                if (directoryPrefix == "$pdlcdir$") or (directoryPrefix == "$moddir$") then
                                    if defaultFilename ~= nil then
                                        storageArea.defaultObject = NetworkUtil.convertFromNetworkFilename(directoryPrefix .. defaultFilename)
                                    end
                                else
                                    isValid = false
                                    g_logManager:xmlWarning(xmlFilename, "Invalid 'directoryPrefix' given at '%s.spawners.defaultObject'! Use '$pdlcdir$' or '$moddir$' only.", storageAreaKey)
                                end
                            end

                            storageArea.defaultFillLevel = Utils.getNoNil(getXMLInt(xmlFile, storageAreaKey .. ".spawners.defaultObject#fillLevel"), 4000)

                            storageArea.spawnersData = {
                                width = width,
                                height = height,
                                length = length,
                                ex = width * 0.5,
                                ey = height * 0.5,
                                ez = length * 0.5,
                                yOffset = yOffset
                            }

                            if storageArea.defaultObject ~= nil and fileExists(storageArea.defaultObject) then
                                storageArea.spawners = {}

                                for id = 0, numInGroup - 1 do
                                    local node = getChildAt(spawners, id)
                                    table.insert(storageArea.spawners, node)

                                    if stackHeight > 1 then
                                        local x, y, z = getTranslation(node)
                                        local rx, ry, rz = getRotation(node)

                                        y = y + initialYOffset
                                        for i = 1, stackHeight - 1 do
                                            y = y + (stackOffset + 0.1)

                                            local name = string.format("spawnNode%d_%d", id, i)
                                            local transformGroup = createTransformGroup(name)

                                            link(self.nodeId, transformGroup)
                                            setTranslation(transformGroup, x, y, z)
                                            setRotation(transformGroup, rx, ry, rz)

                                            table.insert(storageArea.spawners, transformGroup)
                                        end
                                    end
                                end
                            else
                                isValid = false
                                g_logManager:xmlWarning(xmlFilename, "No 'defaultObject' filename given at '%s'!", storageAreaKey)
                            end
                        else
                            isValid = false
                            g_logManager:xmlWarning(xmlFilename, "Spawner node parent given at '%s' does not contain any children!", storageAreaKey)
                        end
                    else
                        isValid = false
                        g_logManager:xmlWarning(xmlFilename, "No parent spawner node given at '%s'!", storageAreaKey)
                    end

                    local protectionTriggerId = I3DUtil.indexToObject(self.nodeId, getXMLString(xmlFile, storageAreaKey .. ".spawners.protectionTrigger#node"))
                    if protectionTriggerId ~= nil then
                        storageArea.protection = {vehiclesInRange = {}, triggerId = protectionTriggerId}

                        if self.protectionToStorageAreas[protectionTriggerId] == nil then
                            self.protectionToStorageAreas[protectionTriggerId] = {}
                        end

                        table.insert(self.protectionToStorageAreas[protectionTriggerId], storageArea)
                    end
                else
                    g_logManager:xmlWarning(xmlFilename, "Duplicate fillType '%s' given at '%s'!", fillTypeName, storageAreaKey)
                end
            else
                g_logManager:xmlWarning(xmlFilename, "Invalid fillType '%s' given at '%s'!", fillTypeName, storageAreaKey)
            end

            i = i + 1
        end
    else
        isValid = false
        g_logManager:xmlWarning(xmlFilename, "Invalid type '%s' given! Use 'SQUAREBALE' or 'ROUNDBALE' or 'PALLET'.", self.storageTypeName)
    end

    self:loadLanguageNodes(xmlFile, isValid)

    if self.isServer and self.hasSeasonsSupport then
        if g_currentMission ~= nil and g_currentMission.environment ~= nil then
            g_currentMission.environment:addMinuteChangeListener(self)
        end
    end

    delete(xmlFile)

    return isValid
end

function ObjectStorage:loadTriggers(xmlFile, key, triggers, isInteraction)
    local i = 0
    while true do
        local triggerKey = string.format("%s(%d)", key, i)
        if not hasXMLProperty(xmlFile, triggerKey) then
            break
        end

        local triggerId = I3DUtil.indexToObject(self.nodeId, getXMLString(xmlFile, triggerKey .. "#node"))
        if triggerId ~= nil then
            if isInteraction then
                self.interactionTriggersHelp[triggerId] = Utils.getNoNil(getXMLBool(xmlFile, triggerKey .. "#showLevelInformation"), false)
            end

            table.insert(triggers, triggerId)
        end

        i = i + 1
    end
end

function ObjectStorage:loadLanguageNodes(xmlFile, isValid)
    if not isValid then
        return
    end

    local i = 0
    while true do
        local languageKey = string.format("placeable.objectStorage.languageNodes.language(%d)", i)
        if not hasXMLProperty(xmlFile, languageKey) then
            break
        end

        local node = I3DUtil.indexToObject(self.nodeId, getXMLString(xmlFile, languageKey .. "#node"))
        local short = getXMLString(xmlFile, languageKey .. "#short")

        if node ~= nil and short ~= nil then
            if self.languageNodes == nil then
                self.languageNodes = {}
            end

            if self.languageNodes[short] == nil then
                self.languageNodes[short] = node
            else
                g_logManager:xmlWarning(self.configFileName, "Duplicate language given at '%s'. This should be removed.", languageKey)
            end

            setVisibility(node, false)
        end

        i = i + 1
    end
end

function ObjectStorage:finalizePlacement()
    ObjectStorage:superClass().finalizePlacement(self)

    for _, triggerId in pairs (self.inputTriggers) do
        addTrigger(triggerId, "inputTriggerCallback", self)
    end

    for _, triggerId in pairs (self.interactionTriggers) do
        addTrigger(triggerId, "interactionTriggerCallback", self)
    end

    for triggerId, _ in pairs (self.protectionToStorageAreas) do
        addTrigger(triggerId, "protectionTriggerCallback", self)
    end

    if self.hideVisNodesWhenPlaced then
        for _, storageArea in pairs (self.storageAreas) do
            self:updateVisibilityNodes(storageArea)
        end
    end

    if self.languageNodes ~= nil then
        local englishNode = self.languageNodes["en"]
        if englishNode ~= nil then
            local currentShort = g_languageShort
            if self.languageNodes[currentShort] ~= nil then
                setVisibility(self.languageNodes[currentShort], true)
            else
                setVisibility(englishNode, true)
            end
        else
            g_logManager:xmlWarning(self.configFileName, "English (en) language was not found at 'placeable.objectStorage.languageNodes'! This must be set as a backup for other languages not given.")
        end
    end
end

function ObjectStorage:delete()
    if self.inputTriggers ~= nil then
        for _, triggerId in pairs (self.inputTriggers) do
            removeTrigger(triggerId)
        end

        self.inputTriggers = nil
    end

    if self.interactionTriggers ~= nil then
        for _, triggerId in pairs (self.interactionTriggers) do
            removeTrigger(triggerId)
        end

        self.interactionTriggers = nil
    end

    if self.protectionToStorageAreas ~= nil then
        for triggerId, _ in pairs (self.protectionToStorageAreas) do
            removeTrigger(triggerId)
        end

        self.protectionToStorageAreas = nil
    end

    if self.playerInRange then
        self.playerInRange = false
        self.showHelpList = 0
        g_currentMission:removeActivatableObject(self)
    end

    if self.isServer and self.hasSeasonsSupport then
        if g_currentMission ~= nil and g_currentMission.environment ~= nil then
            g_currentMission.environment:removeMinuteChangeListener(self)
        end
    end

    self.hasSeasonsSupport = false

    unregisterObjectClassName(self)
    ObjectStorage:superClass().delete(self)
end

function ObjectStorage:readStream(streamId, connection)
    ObjectStorage:superClass().readStream(self, streamId, connection)

    if connection:getIsServer() then
        local numOfStorageAreas = streamReadUInt8(streamId)

        for i = 1, numOfStorageAreas do
            if streamReadBool(streamId) then
                local fillTypeIndex = streamReadUIntN(streamId, FillTypeManager.SEND_NUM_BITS)
                local numObjects = streamReadUInt16(streamId)

                local storageArea = self.storageAreas[fillTypeIndex]

                for j = 1, numObjects do
                    local fillLevel = streamReadFloat32(streamId)
                    local isFermenting = streamReadBool(streamId)

                    local wrappingColour
                    if streamReadBool(streamId) then
                        local r = streamReadFloat32(streamId)
                        local g = streamReadFloat32(streamId)
                        local b = streamReadFloat32(streamId)
                        local a = streamReadFloat32(streamId)
                        wrappingColour = {r, g, b, a}
                    end

                    table.insert(storageArea.storedObjects, {fillLevel = fillLevel, isFermenting = isFermenting, wrappingColour = wrappingColour})
                end

                self:updateVisibilityNodes(storageArea)
                self:updateDisplays(storageArea)
            end
        end

        self.numberFermenting = streamReadUInt16(streamId)
    end
end

function ObjectStorage:writeStream(streamId, connection)
    ObjectStorage:superClass().writeStream(self, streamId, connection)

    if not connection:getIsServer() then
        local numOfStorageAreas = self:getNumOfStorageAreas()
        streamWriteUInt8(streamId, numOfStorageAreas)

        for fillType, storageArea in pairs (self.storageAreas) do
            local numObjects = #storageArea.storedObjects

            if streamWriteBool(streamId, numObjects > 0) then
                streamWriteUIntN(streamId, fillType, FillTypeManager.SEND_NUM_BITS)
                streamWriteUInt16(streamId, numObjects)

                for i = 1, numObjects do
                    local objectData = storageArea.storedObjects[i]

                    streamWriteFloat32(streamId, objectData.fillLevel)
                    streamWriteBool(streamId, objectData.isFermenting)

                    local isWrapped = self.hasWrapShader and objectData.wrappingColour ~= nil
                    if streamWriteBool(streamId, isWrapped) then
                        streamWriteFloat32(streamId, objectData.wrappingColour[1])
                        streamWriteFloat32(streamId, objectData.wrappingColour[2])
                        streamWriteFloat32(streamId, objectData.wrappingColour[3])
                        streamWriteFloat32(streamId, objectData.wrappingColour[4])
                    end
                end
            end
        end

        streamWriteUInt16(streamId, self.numberFermenting)
    end
end

function ObjectStorage:saveToXMLFile(xmlFile, key, usedModNames)
    ObjectStorage:superClass().saveToXMLFile(self, xmlFile, key, usedModNames)

    local nextFilenameId = 1
    local objectFilenames = {}

    local nextWrappingColourId = 1
    local wrappingColours = {}

    local storageAreasData = {}

    for fillTypeIndex, storageArea in pairs (self.storageAreas) do
        local numObjects = #storageArea.storedObjects

        if numObjects > 0 then
            for i = 1, numObjects do
                local objectData = storageArea.storedObjects[i]
                local filename = objectData.filename

                if filename ~= nil and filename ~= storageArea.defaultObject then
                    if objectFilenames[filename] == nil then
                        local networkFilename = HTMLUtil.encodeToHTML(NetworkUtil.convertToNetworkFilename(filename))
                        objectFilenames[filename] = {filename = networkFilename, modName = objectData.modName, id = nextFilenameId}
                        nextFilenameId = nextFilenameId + 1
                    end

                    objectData.filenameId = objectFilenames[objectData.filename].id
                end

                if self.isBaleStorage then
                    if objectData.wrappingColour ~= nil then
                        local wrappingColor = objectData.wrappingColour or {1, 1, 1, 1}
                        local tableColour = wrappingColor[1] .. wrappingColor[2] .. wrappingColor[3] .. wrappingColor[4]

                        if wrappingColours[tableColour] == nil then
                            wrappingColours[tableColour] = {id = nextWrappingColourId, rgba = wrappingColor}
                            nextWrappingColourId = nextWrappingColourId + 1
                        end

                        objectData.wrappingColourId = wrappingColours[tableColour].id
                    end
                else
                    if (objectData.filenameId ~= nil) or (objectData.fillLevel ~= storageArea.defaultFillLevel) or (objectData.configurations ~= nil) then
                        if storageAreasData[fillTypeIndex] == nil then
                            storageAreasData[fillTypeIndex] = {}
                        end

                        storageAreasData[fillTypeIndex][i] = {
                            fillLevel = objectData.fillLevel,
                            filenameId = objectData.filenameId,
                            configurations = objectData.configurations
                        }
                    end
                end
            end
        end
    end

    setXMLString(xmlFile, key .. ".objectStorage#type", self.storageTypeName)

    local customId = 0
    for _, data in pairs (objectFilenames) do
        local filenameKey = string.format("%s.objectStorage.objectFilenames.objectFilename(%d)", key, customId)

        setXMLInt(xmlFile, filenameKey .. "#id", data.id)
        setXMLString(xmlFile, filenameKey .. "#filename", data.filename)

        if data.modName ~= nil then
            setXMLString(xmlFile, filenameKey .. "#modName", data.modName)
        end

        customId = customId + 1
    end

    if next(wrappingColours) ~= nil then
        customId = 0
        for tableColour, data in pairs (wrappingColours) do
            local wrappingColourKey = string.format("%s.objectStorage.baleWrapping.wrapping(%d)", key, customId)

            setXMLInt(xmlFile, wrappingColourKey .. "#id", data.id)
            local r, g, b, a = data.rgba[1], data.rgba[2], data.rgba[3], data.rgba[4]
            setXMLString(xmlFile, wrappingColourKey .. "#rgba", string.format("%f %f %f %f", r, g, b, a))

            customId = customId + 1
        end
    end

    local i = 0
    for fillTypeIndex, storageArea in pairs (self.storageAreas) do
        local storageAreaKey = string.format("%s.objectStorage.storageArea(%d)", key, i)

        local fillTypeName = g_fillTypeManager:getFillTypeNameByIndex(fillTypeIndex)
        setXMLString(xmlFile, storageAreaKey .. "#fillType", fillTypeName)

        local numObjects = #storageArea.storedObjects
        setXMLInt(xmlFile, storageAreaKey .. "#numObjects", numObjects)

        setXMLInt(xmlFile, storageAreaKey .. "#defaultFillLevel", storageArea.defaultFillLevel)

        if numObjects > 0 then
            local j = 0
            if self.isBaleStorage then
                for id, objectData in pairs (storageArea.storedObjects) do
                    local objectKey = string.format("%s.object(%d)", storageAreaKey, j)

                    setXMLFloat(xmlFile, objectKey .. "#fillLevel", objectData.fillLevel)

                    if objectData.filenameId ~= nil then
                        setXMLInt(xmlFile, objectKey .. "#filenameId", objectData.filenameId)
                    end

                    if objectData.wrappingColour ~= nil then
                        setXMLInt(xmlFile, objectKey .. "#wrappingId", objectData.wrappingColourId or 0)
                    end

                    if g_seasons ~= nil then
                        if objectData.fermentingProcess ~= nil then
                            setXMLFloat(xmlFile, objectKey .. "#fermentingProcess", objectData.fermentingProcess)
                        end

                        if objectData.oldFillTypeIndex ~= nil then
                            local fillTypeName = g_fillTypeManager:getFillTypeNameByIndex(objectData.oldFillTypeIndex)
                            setXMLString(xmlFile, objectKey .. "#fillType", fillTypeName)
                        end

                        setXMLInt(xmlFile, objectKey .. "#baleAge", objectData.baleAge or 0)
                    end

                    j = j + 1
                end
            else
                if storageAreasData[fillTypeIndex] ~= nil then
                    for id, objectData in pairs (storageAreasData[fillTypeIndex]) do
                        local objectKey = string.format("%s.object(%d)", storageAreaKey, j)

                        setXMLInt(xmlFile, objectKey .. "#id", id)
                        setXMLFloat(xmlFile, objectKey .. "#fillLevel", objectData.fillLevel)

                        if objectData.filenameId ~= nil then
                            setXMLInt(xmlFile, objectKey .. "#filenameId", objectData.filenameId)
                        end

                        if objectData.configurations ~= nil then
                            local numConfigTypes = 0

                            for configName, configId in pairs(objectData.configurations) do
                                local configKey = self:getConfigurationKey(configName, configId)

                                if configKey ~= "" then
                                    setXMLInt(xmlFile, objectKey .. configKey, configId)
                                    numConfigTypes = numConfigTypes + 1
                                end

                            end

                            setXMLInt(xmlFile, objectKey .. "#numConfigTypes", numConfigTypes)
                        end

                        j = j + 1
                    end
                end
            end
        end

        i = i + 1
    end
end

function ObjectStorage:loadFromXMLFile(xmlFile, key, resetVehicles)
    if not ObjectStorage:superClass().loadFromXMLFile(self, xmlFile, key, resetVehicles) then
        return false
    end

    local wrappingColours
    local objectFilenames = {}

    local i = 0
    while true do
        local filenameKey = string.format("%s.objectStorage.objectFilenames.objectFilename(%d)", key, i)
        if not hasXMLProperty(xmlFile, filenameKey) then
            break
        end

        local id = getXMLInt(xmlFile, filenameKey .. "#id")
        local networkFilename = getXMLString(xmlFile, filenameKey .. "#filename")

        if id ~= nil and networkFilename ~= nil then
            local modName = getXMLString(xmlFile, filenameKey .. "#modName")
            if modName ~= nil then
                if not g_modIsLoaded[modName] then
                    networkFilename = nil
                    print(string.format("  Warning: [ObjectStorage] Custom object could not be loaded as mod '%s' is missing! Using building default instead. ", modName))
                end
            end

            if networkFilename ~= nil then
                objectFilenames[id] = {filename = NetworkUtil.convertFromNetworkFilename(networkFilename), modName = modName}
            end
        end

        i = i + 1
    end

    if self.isBaleStorage then
        wrappingColours = {}

        i = 0
        while true do
            local wrappingKey = string.format("%s.objectStorage.baleWrapping.wrapping(%d)", key, i)
            if not hasXMLProperty(xmlFile, wrappingKey) then
                break
            end

            local id = getXMLInt(xmlFile, wrappingKey .. "#id")
            local rgba = {StringUtil.getVectorFromString(Utils.getNoNil(getXMLString(xmlFile, wrappingKey .. "#rgba"), "1.0 1.0 1.0 1.0"))}

            if id ~= nil and table.getn(rgba) >= 4 then
                wrappingColours[id] = rgba
            end

            i = i + 1
        end
    end

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

        local numObjects = getXMLInt(xmlFile, storageAreaKey .. "#numObjects")

        local fillTypeName = getXMLString(xmlFile, storageAreaKey .. "#fillType")
        local fillTypeIndex = g_fillTypeManager:getFillTypeIndexByName(fillTypeName)

        if fillTypeIndex ~= nil and numObjects > 0 then
            local storageArea = self.storageAreas[fillTypeIndex]
            if storageArea ~= nil then
                local customObjects = {}

                local j = 0
                while true do
                    local objectKey = string.format("%s.object(%d)", storageAreaKey, j)
                    if not hasXMLProperty(xmlFile, objectKey) then
                        break
                    end

                    local fillLevel = getXMLFloat(xmlFile, objectKey .. "#fillLevel")

                    local filename, modName
                    local filenameId = getXMLInt(xmlFile, objectKey .. "#filenameId")
                    if objectFilenames[filenameId] ~= nil then
                        filename = objectFilenames[filenameId].filename
                        modName = objectFilenames[filenameId].modName
                    end

                    if self.isBaleStorage then
                        local objectData = {}

                        if fillLevel == nil then
                            fillLevel = storageArea.defaultFillLevel
                        end

                        objectData.fillLevel = fillLevel
                        objectData.filename = filename
                        objectData.modName = modName

                        local wrappingColourId = getXMLInt(xmlFile, objectKey .. "#wrappingId")
                        if wrappingColourId ~= nil then
                            objectData.wrappingColour = wrappingColours[wrappingColourId] or {1, 1, 1, 1}
                        end

                        objectData.isFermenting = false

                        if g_seasons ~= nil then
                            objectData.fermentingProcess = getXMLFloat(xmlFile, objectKey .. "#fermentingProcess")
                            if objectData.fermentingProcess ~= nil then
                                local oldFillTypeName = getXMLString(xmlFile, objectKey .. "#fillType")
                                if oldFillTypeName == nil then
                                    oldFillTypeName = "GRASS_WINDROW"
                                end

                                objectData.oldFillTypeIndex = g_fillTypeManager:getFillTypeIndexByName(oldFillTypeName)

                                objectData.isFermenting = true
                                self.hasBalesToFerment = true

                                self.numberFermenting = self.numberFermenting + 1
                            end

                            objectData.baleAge = getXMLInt(xmlFile, objectKey .. "#baleAge") or 0
                        end

                        table.insert(storageArea.storedObjects, objectData)

                        if j >= numObjects then
                            break
                        end
                    else
                        local objectId = getXMLInt(xmlFile, objectKey .. "#id")
                        if objectId ~= nil and fillLevel ~= nil then
                            if filename ~= nil then
                                customObjects[objectId] = {fillLevel = fillLevel, filename = filename, modName = modName, isFermenting = false}
                            else
                                -- Need to reduce 'fillLevel' if greater than 'defaultFillLevel' and original is missing as vehicle/pallet has set capacity.
                                local minFillLevel = math.min(fillLevel, storageArea.defaultFillLevel)
                                customObjects[objectId] = {fillLevel = minFillLevel, isFermenting = false}
                            end

                            local numConfigTypes = getXMLInt(xmlFile, objectKey .. "#numConfigTypes") or 0
                            if numConfigTypes > 0 then

                                for configName, configKey in pairs (ObjectStorage.PALLET_CONFIGURATION_KEYS) do
                                    local configId = getXMLInt(xmlFile, objectKey .. configKey) or 1

                                    if configId > 1 then
                                        if customObjects[objectId].configurations == nil then
                                            customObjects[objectId].configurations = {}
                                        end

                                        customObjects[objectId].configurations[configName] = configId

                                        numConfigTypes = numConfigTypes - 1
                                        if numConfigTypes <= 0 then
                                            break
                                        end
                                    end
                                end

                            end
                        end
                    end

                    j = j + 1
                end

                if not self.isBaleStorage then
                    for i = 1, numObjects do
                        local objectData = customObjects[i]

                        if objectData == nil then
                            objectData = {fillLevel = storageArea.defaultFillLevel, isFermenting = false}
                        end

                        table.insert(storageArea.storedObjects, objectData)
                    end
                end

                if self.isClient then
                    self:updateVisibilityNodes(storageArea)
                    self:updateDisplays(storageArea)
                end
            end
        end

        i = i + 1
    end

    return true
end

function ObjectStorage:update(dt)
    if self:getCanInteract() then
        for index, storageArea in pairs (self.storageAreas) do
            local fillLevel = #storageArea.storedObjects
            local capacity = storageArea.capacity
            local percent = 100 * (fillLevel / capacity)
            g_currentMission:addExtraPrintText(string.format("%s:  %d / %d %s  -  (%d%%)", storageArea.title, fillLevel, capacity, storageArea.unitType, percent))
        end

        self:raiseActive()
    end

    if self.debugSpawnNodes then
        local doRaiseActive = false

        for _, storageArea in pairs (self.storageAreas) do
            if storageArea.debugSpawnNodes then
                for i = 1, #storageArea.spawners do
                    local spawner = storageArea.spawners[i]
                    local data = storageArea.spawnersData

                    local x, y, z = getWorldTranslation(spawner)
                    local rx, ry, rz = getWorldRotation(spawner)

                    DebugUtil.drawDebugNode(spawner, tostring(i), false)
                    DebugUtil.drawOverlapBox(x, y + data.yOffset, z, rx, ry, rz, data.ex, data.ey, data.ez, 0.5, 1.0, 1.0)
                end

                doRaiseActive = true
            end
        end

        if doRaiseActive then
            self:raiseActive()
        end
    end
end

function ObjectStorage:minuteChanged()
    if self.isServer and self.hasSeasonsSupport and self.hasBalesToFerment then
        local storageArea = self.storageAreas[FillType.SILAGE]
        if storageArea ~= nil then
            local numToFerment = 0
            local factor = 60 * 1000
            local duration = g_seasons.grass:getFermentationDuration()

            for i = 1, #storageArea.storedObjects do
                local objectData = storageArea.storedObjects[i]

                if objectData.isFermenting and objectData.fermentingProcess ~= nil then
                    objectData.fermentingProcess = objectData.fermentingProcess + (factor / duration)

                    if objectData.fermentingProcess >= 1 then
                        objectData.oldFillTypeIndex = nil
                        objectData.fermentingProcess = nil
                        objectData.isFermenting = false
                    else
                        numToFerment = numToFerment + 1
                    end
                end
            end

            if numToFerment ~= self.numberFermenting then
                self.numberFermenting = math.max(numToFerment, 0)

                ObjectStorageFermentEvent.sendEvent(self, self.numberFermenting)
            end

            self.hasBalesToFerment = (numToFerment > 0)
        end
    end
end

function ObjectStorage:updateStoredObjects(fillTypeIndex, fillLevel, isFermenting, wrappingColour, objectData, doEventSend, bale)
    local storageArea = self.storageAreas[fillTypeIndex]

    if storageArea ~= nil then
        if fillLevel > 0 then
            if self.isServer then
                if objectData == nil then
                    objectData = {}
                end

                objectData.fillLevel = fillLevel
                objectData.isFermenting = isFermenting

                if isFermenting then
                    self.hasBalesToFerment = true
                end

                table.insert(storageArea.storedObjects, objectData)
            else
                if isFermenting then
                    self.numberFermenting = self.numberFermenting + 1
                end

                table.insert(storageArea.storedObjects, {fillLevel = fillLevel, isFermenting = isFermenting, wrappingColour = wrappingColour})
            end
        else
            local lastId = #storageArea.storedObjects
            storageArea.storedObjects[lastId] = nil

            if isFermenting then
                self.numberFermenting = self.numberFermenting - 1
            end
        end

        if self.isClient then
            self:updateVisibilityNodes(storageArea)
            self:updateDisplays(storageArea)
        end

        if doEventSend then
            if (wrappingColour ~= nil) or isFermenting then
                ObjectStorageSpecialStoreEvent.sendEvent(self, fillTypeIndex, fillLevel, isFermenting, wrappingColour, bale)
            else
                ObjectStorageStoreEvent.sendEvent(self, fillTypeIndex, fillLevel)
            end
        end

        return true
    end

    return false
end

function ObjectStorage:updateVisibilityNodes(storageArea)
    if storageArea ~= nil and storageArea.visibilityNodes ~= nil then
        local objectCount = #storageArea.storedObjects
        local numberVisNodes = #storageArea.visibilityNodes
        local isEqual = (numberVisNodes == storageArea.capacity)
        local visibleNodes = math.ceil((numberVisNodes * objectCount) / storageArea.capacity)

        for i = 1, numberVisNodes do
            local visNode = storageArea.visibilityNodes[i]
            local stateActive = i <= visibleNodes

            if stateActive ~= visNode.stateActive then
                visNode.stateActive = stateActive

                if isEqual and visNode.hasShader then
                    local objectData = storageArea.storedObjects[i]
                    if objectData ~= nil and objectData.wrappingColour ~= nil then
                        local wc = objectData.wrappingColour
                        setShaderParameter(visNode.node, "colorScale", wc[1], wc[2], wc[3], wc[4], false)
                    end
                end

                local rigidBodyType = "NoRigidBody"
                if visNode.stateActive then
                    rigidBodyType = visNode.rigidBodyType
                end

                setVisibility(visNode.node, visNode.stateActive)
                setRigidBodyType(visNode.node, rigidBodyType)

                if visNode.linkedNode ~= nil then
                    setVisibility(visNode.linkedNode, visNode.stateActive)
                    setRigidBodyType(visNode.linkedNode, rigidBodyType)
                end

                if visNode.linkedShader ~= nil then
                    if visNode.stateActive then
                        local tp = visNode.linkedShader.targetParameters
                        setShaderParameter(visNode.linkedShader.node, visNode.linkedShader.parameterName, tp[1], tp[2], tp[3], tp[4], false)
                    else
                        local bp = visNode.linkedShader.baseParameters
                        setShaderParameter(visNode.linkedShader.node, visNode.linkedShader.parameterName, bp[1], bp[2], bp[3], bp[4], false)
                    end
                end
            end
        end
    end
end

function ObjectStorage:updateDisplays(storageArea)
    if storageArea ~= nil and storageArea.displays ~= nil then
        local objectCount = #storageArea.storedObjects

        if storageArea.displays["OBJECT_COUNT"] ~= nil then
            for _, display in pairs (storageArea.displays["OBJECT_COUNT"]) do
                self:setDisplayValue(display, objectCount)
            end
        end

        if storageArea.displays["NEXT_FILL_LEVEL"] ~= nil then
            local nextObjectFillLevel = 0
            if objectCount > 0 then
                local nextObject = storageArea.storedObjects[objectCount]
                if nextObject ~= nil then
                    nextObjectFillLevel = math.floor(nextObject.fillLevel)
                end
            end

            for _, display in pairs (storageArea.displays["NEXT_FILL_LEVEL"]) do
                self:setDisplayValue(display, nextObjectFillLevel)
            end
        end

        if storageArea.displays["TOTAL_FILL_LEVEL"] ~= nil then
            local storedLitres = 0
            for _, objectData in pairs(storageArea.storedObjects) do
                storedLitres = storedLitres + objectData.fillLevel
            end

            for _, display in pairs (storageArea.displays["TOTAL_FILL_LEVEL"]) do
                self:setDisplayValue(display, math.floor(storedLitres))
            end
        end

        if storageArea.displays["FREE_SPACE"] ~= nil then
            local freeSpace = storageArea.capacity - objectCount
            for _, display in pairs (storageArea.displays["FREE_SPACE"]) do
                self:setDisplayValue(display, math.floor(freeSpace))
            end
        end
    end
end

function ObjectStorage:setDisplayValue(display, value)
    if display.lastvalue ~= value then
        display.lastvalue = value
        I3DUtil.setNumberShaderByValue(display.node, math.min(display.maxValue, value), 0, display.showOnEmpty)
    end
end

function ObjectStorage:getFillLevel(fillTypeIndex)
    local storageArea = self.storageAreas[fillTypeIndex]

    if storageArea ~= nil then
        return #storageArea.storedObjects
    end

    return 0
end

function ObjectStorage:getStoredLitres(fillTypeIndex)
    local storedLitres = 0
    local storageArea = self.storageAreas[fillTypeIndex]

    if storageArea ~= nil then
        for _, objectData in pairs(storageArea.storedObjects) do
            storedLitres = storedLitres + objectData.fillLevel
        end
    end

    return storedLitres
end

function ObjectStorage:getFillTypeAccepted(fillTypeIndex)
    return self.storageAreas[fillTypeIndex] ~= nil
end

function ObjectStorage:getStorageArea(fillTypeIndex)
    return self.storageAreas[fillTypeIndex]
end

function ObjectStorage:getFreeCapacity(fillTypeIndex)
    local storageArea = self.storageAreas[fillTypeIndex]

    if storageArea ~= nil then
        return storageArea.capacity - #storageArea.storedObjects
    end

    return 0
end

function ObjectStorage:getCanInteract()
    if g_currentMission.controlPlayer and self.playerInRange then
        return self.showHelpList > 0
    end

    return false
end

function ObjectStorage:spawnNodeCallback(transformId)
    if transformId ~= nil and transformId ~= self.spawner and
        transformId ~= self.protectionTrigger and transformId ~= g_currentMission.terrainRootNode then

        local object = g_currentMission:getNodeObject(transformId)
        if object ~= nil or g_currentMission.players[transformId] ~= nil or getHasClassId(transformId, ClassIds.MESH_SPLIT_SHAPE) then
            self.spawnerObject = transformId
            return
        end
    end
end

function ObjectStorage:interactionTriggerCallback(triggerId, otherId, onEnter, onLeave, onStay, otherShapeId)
    if onEnter or onLeave then
        if g_currentMission.player ~= nil and otherId == g_currentMission.player.rootNode then

            if onEnter then
                if not self.playerInRange and self:getIsActivatable() then
                    self.playerInRange = true

                    if self.interactionTriggersHelp[triggerId] == true then
                        self.showHelpList = self.showHelpList + 1
                    end

                    g_currentMission:addActivatableObject(self)
                end
            else
                if self.playerInRange then
                    self.playerInRange = false

                    if self.interactionTriggersHelp[triggerId] == true then
                        self.showHelpList = math.max(self.showHelpList - 1, 0)
                    end

                    g_currentMission:removeActivatableObject(self)
                end
            end

            self:raiseActive()
        end
    end
end

function ObjectStorage:inputTriggerCallback(triggerId, otherId, onEnter, onLeave, onStay, otherShapeId)
    if self.isServer and onEnter and otherShapeId ~= nil then
        local object = g_currentMission.nodeToObject[otherShapeId]

        if object ~= nil and object.isa ~= nil and g_currentMission.accessHandler:canFarmAccess(self:getOwnerFarmId(), object) then
            local objectData = {}
            local fermentingProcess
            local isFermenting = false

            if self.isBaleStorage then
                if object:isa(Bale) then
                    local isRoundbale = Utils.getNoNil(getUserAttribute(otherShapeId, "isRoundbale"), false)

                    if isRoundbale == self.isRoundBaleStorage then
                        local fillLevel = object:getFillLevel()
                        if fillLevel < 0.01 then
                            return
                        end

                        local filename = object.i3dFilename
                        local fillTypeIndex = object:getFillType()

                        if object.wrappingState >= 0.999 then
                            objectData.wrappingColour = object.wrappingColor

                            if g_seasons ~= nil then
                                if object.fermentingProcess ~= nil and (fillTypeIndex ~= FillType.DRYGRASS_WINDROW and fillTypeIndex ~= FillType.STRAW) then
                                    if self:getFillTypeAccepted(FillType.SILAGE) then
                                        fillTypeIndex = FillType.SILAGE

                                        isFermenting = true
                                        objectData.oldFillTypeIndex = object:getFillType()
                                        objectData.fermentingProcess = object.fermentingProcess
                                    else
                                        self:raiseBlinkingWarning("warning_notAcceptedHere", fillTypeIndex, true)

                                        return
                                    end
                                end

                                objectData.baleAge = object.age
                            end
                        end

                        local storageArea = self:getStorageArea(fillTypeIndex)
                        if storageArea ~= nil then
                            if storageArea.acceptedObjectSize ~= nil then
                                if self.isRoundBaleStorage then
                                    if (object.baleWidth > storageArea.acceptedObjectSize.width) or
                                        (object.baleDiameter > storageArea.acceptedObjectSize.diameter) then

                                        self:raiseBlinkingWarning("warning_notAcceptedHere", "( SIZE )", false)

                                        return
                                    end
                                else
                                    if (object.baleWidth > storageArea.acceptedObjectSize.width) or
                                        (object.baleHeight > storageArea.acceptedObjectSize.height) or
                                        (object.baleLength > storageArea.acceptedObjectSize.length) then

                                        self:raiseBlinkingWarning("warning_notAcceptedHere", "( SIZE )", false)

                                        return
                                    end
                                end
                            end

                            if self:getFreeCapacity(fillTypeIndex) > 0 then
                                if filename ~= storageArea.defaultObject then
                                    objectData.modName = object.customEnvironment
                                    objectData.filename = filename
                                end

                                if self:updateStoredObjects(fillTypeIndex, fillLevel, isFermenting, objectData.wrappingColour, objectData, true) then
                                    object:delete()
                                end
                            else
                                self:raiseBlinkingWarning("warning_noMoreFreeCapacity", fillTypeIndex, true)
                            end
                        else
                            self:raiseBlinkingWarning("warning_notAcceptedHere", fillTypeIndex, true)
                        end
                    else
                        if isRoundbale then
                            self:raiseBlinkingWarning("warning_notAcceptedHere", g_i18n:getText("fillType_roundBale"), false)
                        else
                            self:raiseBlinkingWarning("warning_notAcceptedHere", g_i18n:getText("fillType_squareBale"), false)
                        end
                    end
                end
            else
                if object:isa(Vehicle) and object.typeName == "pallet" and table.getn(object:getFillUnits()) == 1 then
                    local fillLevel = object:getFillUnitFillLevel(1)
                    if fillLevel < 0.01 then
                        return
                    end

                    local filename = object.configFileName
                    local fillTypeIndex = object:getFillUnitFillType(1)

                    local storageArea = self:getStorageArea(fillTypeIndex)
                    if storageArea ~= nil then
                        if storageArea.acceptedObjectSize ~= nil then
                            if (object.sizeWidth > storageArea.acceptedObjectSize.width) or (object.sizeLength > storageArea.acceptedObjectSize.length) then
                                self:raiseBlinkingWarning("warning_notAcceptedHere", "( SIZE )", false)

                                return
                            end
                        end

                        if self:getFreeCapacity(fillTypeIndex) > 0 then
                            if filename ~= storageArea.defaultObject then
                                objectData.modName = object.customEnvironment
                                objectData.filename = filename
                                objectData.configurations = self:getPalletConfigurations(object.configurations)
                            end

                            if self:updateStoredObjects(fillTypeIndex, fillLevel, isFermenting, nil, objectData, true) then
                                object:delete()
                            end
                        else
                            self:raiseBlinkingWarning("warning_noMoreFreeCapacity", fillTypeIndex, true)
                        end
                    else
                        self:raiseBlinkingWarning("warning_notAcceptedHere", fillTypeIndex, true)
                    end
                end
            end
        end
    end
end

function ObjectStorage:protectionTriggerCallback(triggerId, otherId, onEnter, onLeave, onStay, otherShapeId)
    if otherShapeId ~= nil and (onEnter or onLeave) then
        local object = g_currentMission.nodeToObject[otherShapeId]
        if object ~= nil and object:isa(Vehicle) and object.typeName ~= "pallet" then
            local storageAreas = self.protectionToStorageAreas[triggerId]
            if storageAreas ~= nil then
                for _, storageArea in pairs(storageAreas) do
                    if storageArea.protection ~= nil then
                        local vehiclesInRange = storageArea.protection.vehiclesInRange

                        if onEnter then
                            vehiclesInRange[otherShapeId] = true
                        elseif onLeave then
                            vehiclesInRange[otherShapeId] = nil
                        end
                    end
                end

                self:determineProtectionState(storageArea)
            end
        end
    end
end

function ObjectStorage:determineProtectionState(storageArea)
    if storageArea ~= nil then
        if storageArea.protection ~= nil then
            storageArea.protection.currentVehicle = nil

            local vehiclesInRange = storageArea.protection.vehiclesInRange

            for vehicleId, inRange in pairs(vehiclesInRange) do
                if inRange ~= nil then
                    local currentVehicle = g_currentMission.nodeToObject[vehicleId]
                    if currentVehicle ~= nil then
                        storageArea.protection.currentVehicle = currentVehicle
                        return currentVehicle
                    end
                end

                vehiclesInRange[vehicleId] = nil
            end
        end

        return
    else
        for _, storageArea in pairs (self.storageAreas) do
            if storageArea.protection ~= nil then
                storageArea.protection.currentVehicle = nil

                local vehiclesInRange = storageArea.protection.vehiclesInRange

                for vehicleId, inRange in pairs(vehiclesInRange) do
                    if inRange ~= nil then
                        local currentVehicle = g_currentMission.nodeToObject[vehicleId]
                        if currentVehicle ~= nil then
                            storageArea.protection.currentVehicle = currentVehicle
                            break
                        end
                    end

                    vehiclesInRange[vehicleId] = nil
                end
            end
        end
    end
end

function ObjectStorage:raiseBlinkingWarning(warning, text, isFillTypeIndex)
    if warning ~= nil then
        if isFillTypeIndex then
            local fillType = g_fillTypeManager:getFillTypeByIndex(text)
            if fillType ~= nil then
                text = fillType.title
            end
        end

        g_currentMission:showBlinkingWarning(string.format(g_i18n:getText(warning), text))
    end
end

function ObjectStorage:getNumOfStorageAreas()
    local count = 0

    for _ in pairs(self.storageAreas) do
        count = count + 1
    end

    return count
end

function ObjectStorage:getFreeSpawnPlaces(storageArea, limit)
    local freePlaces = {}
    local numFreePlaces = 0

    if storageArea ~= nil and limit ~= nil then
        if storageArea.protection ~= nil and storageArea.protection.currentVehicle ~= nil then
            return freePlaces, numFreePlaces
        end

        for i = 1, #storageArea.spawners do
            local spawner = storageArea.spawners[i]
            local data = storageArea.spawnersData

            local x, y, z = getWorldTranslation(spawner)
            local rx, ry, rz = getWorldRotation(spawner)

            self.spawnerObject = nil
            self.spawner = spawner

            if storageArea.protection ~= nil then
                self.protectionTrigger = storageArea.protection.triggerId
            end

            local numShapes = overlapBox(x, y + data.yOffset, z, rx, ry, rz, data.ex, data.ey, data.ez, "spawnNodeCallback", self, nil, true, false, true)
            self.spawner = nil
            self.protectionTrigger = nil

            if self.spawnerObject == nil then
                numFreePlaces = numFreePlaces + 1
                freePlaces[numFreePlaces] = {x, y, z, rx, ry, rz}

                if numFreePlaces >= limit then
                    return freePlaces, numFreePlaces
                end
            end
        end
    end

    return freePlaces, numFreePlaces
end

function ObjectStorage:getNumFreeSpawnPlaces(storageArea, limit)
    local numFreePlaces = 0

    if storageArea ~= nil then
        if storageArea.protection ~= nil and storageArea.protection.currentVehicle ~= nil then
            return numFreePlaces
        end

        if storageArea.protection ~= nil then
            self.protectionTrigger = storageArea.protection.triggerId
        end

        for i = 1, #storageArea.spawners do
            if limit ~= nil and limit <= numFreePlaces then
                return numFreePlaces
            end

            local spawner = storageArea.spawners[i]
            local data = storageArea.spawnersData

            local x, y, z = getWorldTranslation(spawner)
            local rx, ry, rz = getWorldRotation(spawner)

            self.spawnerObject = nil
            self.spawner = spawner

            local numShapes = overlapBox(x, y + data.yOffset, z, rx, ry, rz, data.ex, data.ey, data.ez, "spawnNodeCallback", self, nil, true, false, true)
            self.spawner = nil

            if self.spawnerObject == nil then
                numFreePlaces = numFreePlaces + 1
            end
        end

        self.protectionTrigger = nil
    end

    return numFreePlaces
end

function ObjectStorage:spawnSelectedObject(fillTypeIndex, numberToSpawn)
    local storageArea = self.storageAreas[fillTypeIndex]

    if self.isServer and storageArea ~= nil and numberToSpawn ~= nil then
        local freePlaces, numFreePlaces = self:getFreeSpawnPlaces(storageArea, numberToSpawn)

        if numFreePlaces > 0 then
            for i = 1, numFreePlaces do
                local lastId = #storageArea.storedObjects
                if lastId == 0 then
                    break
                end

                local fp = freePlaces[i]
                local ownerFarmId = self:getOwnerFarmId()
                local objectData = storageArea.storedObjects[lastId]
                local filename = objectData.filename or storageArea.defaultObject

                if self.isBaleStorage then
                    local bale = Bale:new(self.isServer, self.isClient)
                    if bale ~= nil then
                        bale:load(filename, fp[1], fp[2], fp[3], fp[4], fp[5], fp[6], objectData.fillLevel)
                        bale:setOwnerFarmId(ownerFarmId, true)
                        bale:setCanBeSold(true)
                        bale:register()

                        if objectData.wrappingColour ~= nil then
                            local wc = objectData.wrappingColour
                            bale:setColor(wc[1], wc[2], wc[3], wc[4])
                            bale:setWrappingState(1)
                        end

                        local isFermenting = false
                        if g_seasons ~= nil then
                            if objectData.fermentingProcess ~= nil and objectData.oldFillTypeIndex ~= nil then
                                bale.fillType = objectData.oldFillTypeIndex
                                bale:setFillLevel(bale.fillLevel)

                                bale.fermentingProcess = objectData.fermentingProcess
                                isFermenting = true
                            end

                            bale.age = objectData.baleAge or 0
                        end

                        if not self:updateStoredObjects(fillTypeIndex, 0, isFermenting, nil, nil, true, bale) then
                            break
                        end
                    end
                else
                    local dx, _, dz = mathEulerRotateVector(fp[4], fp[5], fp[6], 0, 0, 1)
                    local rotY = MathUtil.getYRotationFromDirection(dx, dz)

                    local terrainHeight = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, fp[1], 300, fp[3]) + 0.5
                    local y = math.max(terrainHeight, fp[2])

                    local pallet = g_currentMission:loadVehicle(filename, fp[1], y, fp[3], 0, rotY, true, 0, Vehicle.PROPERTY_STATE_OWNED, ownerFarmId, objectData.configurations, nil)
                    if pallet ~= nil then
                        if pallet.addFillUnitFillLevel ~= nil then
                            pallet:addFillUnitFillLevel(ownerFarmId, 1, objectData.fillLevel, fillTypeIndex, ToolType.UNDEFINED)

                            if not self:updateStoredObjects(fillTypeIndex, 0, false, nil, nil, true) then
                                break
                            end
                        else
                            g_logManager:warning("[ObjectStorage] - [%s] Specialisation 'FillUnit' not found for pallet [%s]!", self.customEnvironment, filename)
                            break
                        end
                    end
                end
            end
        end
    end
end

function ObjectStorage:requestObjectSpawn(fillTypeIndex, numberToSpawn)
    if fillTypeIndex ~= nil then
        if numberToSpawn ~= nil then
            numberToSpawn = math.max(math.min(numberToSpawn, 127), 1)
        else
            numberToSpawn = 1
        end

        if self.isServer then
            self:spawnSelectedObject(fillTypeIndex, numberToSpawn)
        else
            g_client:getServerConnection():sendEvent(ObjectStorageSpawnEvent:new(self, fillTypeIndex, numberToSpawn))
        end
    else
        g_logManager:warning("[ObjectStorage] - [%s] Function 'requestObjectSpawn' failed, no matching area found.", self.customEnvironment)
    end
end

function ObjectStorage:onActivateObject()
    local dialog = g_gui:showDialog("ObjectStorageGui")
    if dialog ~= nil then
        dialog.target:setTitle(self.customTexts.storageTitle)
        dialog.target:setCustomTexts(self.customTexts)
        dialog.target:loadCallback(self.requestObjectSpawn, self)
        dialog.target:loadStorageAreas(self.storageAreas)
    end
end

function ObjectStorage:drawActivate()
    return
end

function ObjectStorage:getIsActivatable()
    if g_currentMission.controlPlayer then
        if self.ownerFarmId == nil or self.ownerFarmId == AccessHandler.EVERYONE or
            g_currentMission.accessHandler:canFarmAccessOtherId(g_currentMission:getFarmId(), self.ownerFarmId) then

            return true
        end
    end

    return false
end

function ObjectStorage:shouldRemoveActivatable()
    return false
end

function ObjectStorage:getConfigurationKey(configName, configId)
    if configId > 1 then
        return ObjectStorage.PALLET_CONFIGURATION_KEYS[configName] or ""
    end

    return ""
end

function ObjectStorage:getPalletConfigurations(configurations)
    if configurations ~= nil then
        for configName, configId in pairs (configurations) do
            if configId > 1 and ObjectStorage.PALLET_CONFIGURATION_KEYS[configName] ~= nil then
                return configurations
            end
        end
    end

    return nil
end

function ObjectStorage.getCustomText(l10n, backupL10n, addColon)
    if l10n == nil then
        l10n = backupL10n
    end

    local text = g_i18n:getText(l10n)

    if addColon and text:sub(-1) ~= ":" then
        return text .. ":"
    end

    return text
end
