--[[
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.

Important:
No changes are to be made to this script without permission from GtX | Andy
An diesem Skript dürfen ohne Genehmigung von GtX | Andy keine Änderungen vorgenommen werden
]]


ObjectStorage = {}

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

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
}

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 then
                        if storageAreasData[fillTypeIndex] == nil then
                            storageAreasData[fillTypeIndex] = {}
                        end

                        storageAreasData[fillTypeIndex][i] = {fillLevel = objectData.fillLevel, filenameId = objectData.filenameId}
                    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

                        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
                        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 g_currentMission.accessHandler:canFarmAccessOtherId(g_currentMission:getFarmId(), self:getOwnerFarmId()) then
                if onEnter then
                    if not self.playerInRange 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
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 filename = object.i3dFilename

                        local fillLevel = object:getFillLevel()
                        if fillLevel < 0.01 then
                            return
                        end

                        local fillTypeIndex = object:getFillType()

                        if g_seasons ~= nil then
                            if fillTypeIndex ~= FillType.DRYGRASS_WINDROW and fillTypeIndex ~= FillType.STRAW then
                                if object.wrappingState >= 1 and object.fermentingProcess ~= nil 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
                            end

                            objectData.baleAge = object.age
                        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 object.wrappingState >= 1 then
                                    objectData.wrappingColour = object.wrappingColor
                                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 filename = object.configFileName

                    local fillLevel = object:getFillUnitFillLevel(1)
                    if fillLevel < 0.01 then
                        return
                    end

                    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
                            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, nil, 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()
    return g_currentMission.controlPlayer and (self:getOwnerFarmId() == 0 or g_currentMission:getFarmId() == self:getOwnerFarmId())
end

function ObjectStorage:shouldRemoveActivatable()
    return false
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
