--[[
Interface: 1.6.0.0 b9173

Copyright (C) GtX (Andy), 2018

Author: GtX | Andy
Date: 09.12.2018
Version: 1.0.0.0

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

History:
V 1.1.0.0 @ 17.07.2019 - Add support to position 'waterAddon' parts on unsupported mods.
V 1.1.0.1 @ 07.09.2019 - Fix for Animal Pens built into maps in a weird way.
V 1.2.0.. @ 25.09.2020 - Allow placement of milkAddon with Universal Parts, optimisation of code, added ability to add individual parts from different locations (i.e a different chicken trough).

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

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


AnimalPenExtension = {}

local AnimalPenExtension_mt = Class(AnimalPenExtension, Object)
InitObjectClass(AnimalPenExtension, "AnimalPenExtension")

AnimalPenExtension.SALES_OPEN_TIME = 6
AnimalPenExtension.SALES_CLOSED_TIME = 18

AnimalPenExtension.BUILD_WAITING = -2
AnimalPenExtension.BUILD_START = -1
AnimalPenExtension.BUILD_STARTED = 0

AnimalPenExtension.UNIVERSAL_PARTS_VERSION = 4
AnimalPenExtension.DEFAULT_POS = {0, 0, 0}


function AnimalPenExtension:new(isServer, isClient, customMt)
    return Object:new(isServer, isClient, customMt or AnimalPenExtension_mt)
end

function AnimalPenExtension:load(owner, xmlFile, key, isMod, hasUniversalParts, extensionBaseDirectory, texts)
    self.owner = owner
    self.nodeId = owner.nodeId

    self.baseDirectory = owner.baseDirectory
    self.customEnvironment = owner.customEnvironment

    self.universalWaterAddonSet = false
    self.universalMilkAddonSet = false

    self.universalPlacementActive = false
    self.hasUniversalParts = hasUniversalParts

    self.brand = "FARMING INNOVATIONS"

    self.isMod = isMod
    self.texts = texts

    self.animalTypeTitle = ""
    self.dynamicallyLoadedParts = {}

    self.partsBaseDirectory = extensionBaseDirectory
    self.extensionBaseDirectory = extensionBaseDirectory
    self.partsI3dFilename = "sharedParts/animalPenParts.i3d"

    local manager = g_animalPenExtensionManager
    local animalType = owner:getAnimalType()

    if animalType ~= nil then
        local animal = g_animalManager:getAnimalsByType(animalType)

        if animal ~= nil then
            local animalSubType = animal.subTypes[1]

            if animalSubType ~= nil then
                self.animalTypeTitle = animalSubType.fillTypeDesc.title
            end
        end
    end

    if isMod then
        -- Each part can now be listed separate but keep for backwards support
        local partsI3dFilename = getXMLString(xmlFile, key .. "#partsI3DFilename")

        if partsI3dFilename ~= nil then
            if fileExists(self.baseDirectory .. partsI3dFilename) then
                self.partsI3dFilename = partsI3dFilename
                self.partsBaseDirectory = self.baseDirectory

                -- Only allow 'custom brand name' if user is using custom parts i3d.
                local customBrand = getXMLString(xmlFile, key .. "#customBrandName")

                if customBrand ~= nil and customBrand ~= "" then
                    self.brand = customBrand
                end
            else
                manager:logPrint(2, "Water Addon parts using i3D filename '%s' could not be loaded! Using 'Base Extension' parts at (%s)", partsI3dFilename, key)
            end
        end
    end

    local chickenAddonLoaded = self:loadChickenAddon(manager, animalType, xmlFile, key .. ".chickenAddon.waterTrough")
    local waterAddonLoaded = self:loadWaterAddon(manager, owner.modulesByName["water"], xmlFile, key .. ".waterAddon")
    local milkAddonLoaded = self:loadMilkAddon(manager, owner.modulesByName["milk"], xmlFile, key .. ".milkAddon")

    if g_currentMission ~= nil and g_currentMission.environment ~= nil then
        if self.isServer and self.waterAddon ~= nil and self.waterAddon.maintenanceCost > 0 then
            g_currentMission.environment:addDayChangeListener(self)
        end

        if self.waterAddon ~= nil or self.milkAddon ~= nil then
            g_currentMission.environment:addHourChangeListener(self)
        end
    end

    if chickenAddonLoaded or waterAddonLoaded or milkAddonLoaded then
        return true
    end

    return false
end

function AnimalPenExtension:loadChickenAddon(manager, animalType, xmlFile, key)
    if manager.chickenAddonActive and animalType == "CHICKEN" and self.owner.modulesByName["water"] == nil then
        local waterTroughNode, _, _, _, _ = self:loadPartFromI3D(xmlFile, key, self.nodeId, nil, "0 0 0", true)

        if waterTroughNode ~= nil then
            local waterModule = HusbandryModuleBase.createModule("water")
            local nodeIndex = getChildIndex(waterTroughNode)

            if waterModule == nil or nodeIndex == nil then
                delete(waterTroughNode)

                return false
            end

            local exactFillRootNode = tostring(Utils.getNoNil(getUserAttribute(waterTroughNode, "exactFillRootNode"), "0"))
            local minY = tostring(Utils.getNoNil(getUserAttribute(waterTroughNode, "minY"), "0.06"))
            local maxY = tostring(Utils.getNoNil(getUserAttribute(waterTroughNode, "maxY"), "0.135"))

            local memoryXML = '<config node="' .. tostring(nodeIndex) .. '|0" exactFillRootNode="' .. exactFillRootNode .. '" fillTypes="WATER" acceptedToolTypes="undefined trailer">/n' ..
                              '    <fillPlane node="' .. tostring(nodeIndex) .. '|1" minY="' .. minY .. '" maxY="' .. maxY .. '" colorChange="false" />/n</config>'

            local memoryXmlFile = loadXMLFileFromMemory("MemoryXML", memoryXML)

            if waterModule:load(memoryXmlFile, "config", self.nodeId, self.owner, self.baseDirectory) then
                self.chickenAddon = {waterTroughNode = waterTroughNode}

                local levelArea = {}

                levelArea.start = I3DUtil.indexToObject(waterTroughNode, getUserAttribute(waterTroughNode, "levelArea"))

                if levelArea.start ~= nil then
                    levelArea.width = getChildAt(levelArea.start, 0)
                    levelArea.height = getChildAt(levelArea.start, 1)

                    if levelArea.width ~= nil and levelArea.height ~= nil then
                        levelArea.groundType = Utils.getNoNil(getXMLString(xmlFile, key .. "#triggerAreaGroundType"), "grass")
                        table.insert(self.owner.levelAreas, levelArea)
                    end
                end

                local triggerMarker = I3DUtil.indexToObject(waterTroughNode, getUserAttribute(waterTroughNode, "triggerMarker"))

                if triggerMarker ~= nil then
                    self.chickenAddon.triggerMarker = triggerMarker
                    g_currentMission:addTriggerMarker(triggerMarker)
                end

                self.owner.modulesByName["water"] = waterModule
                waterModule.moduleName = "water"

                table.insert(self.owner.modulesById, waterModule)
            else
                delete(waterTroughNode)
            end

            delete(memoryXmlFile)
        end
    end

    return self.chickenAddon ~= nil
end

function AnimalPenExtension:loadWaterAddon(manager, waterModule, xmlFile, key)
    if manager.waterAddonActive and waterModule ~= nil then
        self.waterLinkNode = self.nodeId

        if self.hasUniversalParts and waterModule.unloadPlace ~= nil then
            self:setLinkNode(waterModule.unloadPlace.exactFillRootNode, "waterLinkNode")
        end

        local triggerNode, position, rotation, _, _ = self:loadPartFromI3D(xmlFile, key .. ".trigger", self.waterLinkNode, nil, AnimalPenExtension.DEFAULT_POS, false)

        if triggerNode ~= nil then
            local canAddValve = true  -- You can only have a single valve operation.
            local activatable = WaterAddonActivatable:new(self)

            if activatable ~= nil then
                addTrigger(triggerNode, "waterAddonTriggerCallback", self)

                self.waterAddon = {
                    parts = {},
                    triggerId = triggerNode,
                    activatable = activatable,
                    isActive = false,
                    displayInfo = true,
                    rainWater = 0,
                    updateMinute = 0,
                    handleSoundActive = false,
                    decoIsActive = false,
                    hasBeenPurchased = false,
                    defaultColourIndex = 0,
                    buildHour = AnimalPenExtension.BUILD_WAITING
                }

                self.waterAddon.fillLitersPerSecond = Utils.getNoNil(getXMLInt(xmlFile, key .. "#fillLitersPerSecond"), 450)

                if manager.waterPriceScaleOverride == nil or manager.waterPriceScaleOverride < 0 then
                    -- If you want a more realistic feel then you can set a priceScale. Water Cost is calculated using: Game water cost * waterPriceScale.
                    self.waterAddon.waterPriceScale = Utils.getNoNil(getXMLFloat(xmlFile, key .. "#waterPriceScale"), 0)
                else
                    self.waterAddon.waterPriceScale = manager.waterPriceScaleOverride
                end

                if manager.maintenancePerDayOverride == nil or manager.maintenancePerDayOverride < 0 then
                    -- The daily cost to own the Water Addon. Good option if you do not want a water cost.
                    self.waterAddon.maintenanceCost = Utils.getNoNil(getXMLInt(xmlFile, key .. "#maintenancePerDay"), 0)
                else
                    self.waterAddon.maintenanceCost = manager.maintenancePerDayOverride
                end

                -- Set '0' for indoor troughs.
                self.waterAddon.rainInputPerHour = Utils.getNoNil(getXMLInt(xmlFile, key .. "#rainInputPerHour"), 60)

                -- This is the amount it cost to construct the waterAddon.
                self.waterAddon.buildCost = Utils.getNoNil(getXMLInt(xmlFile, key .. ".construction#buildCost"), 5800)
                self.waterAddon.buildTotalHours = math.max(Utils.getNoNil(getXMLInt(xmlFile, key .. ".construction#buildTotalHours"), 10), 1)

                if manager.purchaseOverride then
                    self.waterAddon.requiresPurchase = false
                else
                    self.waterAddon.requiresPurchase = Utils.getNoNil(getXMLBool(xmlFile, key .. ".construction#requiresPurchase"), false)
                end

                local triggerMarker, _, _, _, _ = self:loadPartFromI3D(xmlFile, key .. ".trigger.marker", self.waterLinkNode, position, rotation, false)

                if triggerMarker ~= nil then
                    self.waterAddon.triggerMarker = triggerMarker
                    g_currentMission:addTriggerMarker(triggerMarker)
                end

                if manager.partsColours ~= nil and #manager.partsColours > 0 then
                    self.waterAddon.defaultColourIndex = Utils.getNoNil(getXMLInt(xmlFile, key .. ".parts#defaultColourIndex"), 1)

                    if self.waterAddon.defaultColourIndex > #manager.partsColours then
                        manager:logPrint(2, "Invalid defualt colour index %d, using 1 instead!", self.waterAddon.defaultColourIndex)
                        self.waterAddon.defaultColourIndex = 1
                    end
                end

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

                    local part = {}

                    if getXMLString(xmlFile, partsKey .. "#sharedI3dNode") ~= nil then
                        local partNode, position, rotation, _, isDefaultPart = self:loadPartFromI3D(xmlFile, partsKey, self.waterLinkNode, nil, AnimalPenExtension.DEFAULT_POS, true)

                        if partNode ~= nil then
                            part.node = partNode
                            part.typeName = "PIPE"
                            part.isPartDelay = false
                            part.position = position
                            part.rotation = rotation
                            part.canScale = true
                            part.allowMaterialChange = isDefaultPart and Utils.getNoNil(getUserAttribute(partNode, "allowMaterialChange"), false)

                            if self.isClient then
                                local isSpout = Utils.getNoNil(getXMLBool(xmlFile, partsKey .. "#isSpout"), false)

                                if isSpout then
                                    part.typeName = "SPOUT"

                                    local effectNode = I3DUtil.indexToObject(partNode, getUserAttribute(partNode, "effectNode"))

                                    if effectNode ~= nil then
                                        local effects = g_effectManager:loadEffect(xmlFile, partsKey .. ".effects", effectNode, self)

                                        if effects ~= nil then
                                            g_effectManager:setFillType(effects, FillType.WATER)

                                            if self.waterAddon.effects == nil then
                                                self.waterAddon.effects = {}
                                            end

                                            table.insert(self.waterAddon.effects, effects)
                                        end
                                    end

                                    local soundNode = I3DUtil.indexToObject(partNode, getUserAttribute(partNode, "soundNode"))

                                    if soundNode ~= nil then
                                        local sample = g_soundManager:loadSampleFromXML(xmlFile, partsKey .. ".sounds", "waterFilling", self.baseDirectory, soundNode, 0, AudioGroup.ENVIRONMENT, nil, nil)

                                        if sample ~= nil then
                                            if self.waterAddon.samplesWater == nil then
                                                self.waterAddon.samplesWater = {}
                                            end

                                            table.insert(self.waterAddon.samplesWater, sample)
                                        end
                                    end
                                else
                                    if canAddValve then
                                        local isValve = Utils.getNoNil(getXMLBool(xmlFile, partsKey .. "#isValve"), false)

                                        if isValve then
                                            part.typeName = "VALVE"
                                            canAddValve = false

                                            local valveNode = I3DUtil.indexToObject(partNode, getUserAttribute(partNode, "valveHandleNode"))
                                            local valveStemNode = I3DUtil.indexToObject(partNode, getUserAttribute(partNode, "valveStemNode"))

                                            if valveNode ~= nil then
                                                self.waterAddon.sampleHandle = g_soundManager:loadSampleFromXML(xmlFile, partsKey .. ".sounds", "valveMoving", self.baseDirectory, valveNode, 0, AudioGroup.ENVIRONMENT, nil, nil)

                                                if hasXMLProperty(xmlFile, partsKey .. ".animation") then
                                                    local clipName = getXMLString(xmlFile, partsKey .. ".animation#clipName")

                                                    if clipName ~= nil then
                                                        local animCharSet = getAnimCharacterSet(valveNode)
                                                        local clip = getAnimClipIndex(animCharSet, clipName)
                                                        if clip ~= nil then
                                                            local animDuration = getAnimClipDuration(animCharSet, clip);
                                                            self.waterAddon.valveAnimationClip = {name = clipName, characterSet = animCharSet, duration = animDuration, speedScale = 0}
                                                            assignAnimTrackClip(animCharSet, 0, clip)
                                                            setAnimTrackLoopState(animCharSet, 0, false)
                                                            setAnimTrackSpeedScale(animCharSet, 0, 1)
                                                        end
                                                    else
                                                        if hasXMLProperty(xmlFile, partsKey .. ".animation.valveHandle") then
                                                            -- For custom valve animation if you do not have a animation clip in custom parts i3d
                                                            local duration = Utils.getNoNil(getXMLFloat(xmlFile, partsKey .. ".animation#duration"), 2) * 1000
                                                            self.waterAddon.valveAnimation = {duration = duration, animTime = 0, direction = 1, parts = {}}

                                                            local startTrans = getXMLString(xmlFile, partsKey .. ".animation.valveHandle#startTrans")
                                                            local endTrans = getXMLString(xmlFile, partsKey .. ".animation.valveHandle#endTrans")
                                                            local startRot = getXMLString(xmlFile, partsKey .. ".animation.valveHandle#startRot")
                                                            local endRot = getXMLString(xmlFile, partsKey .. ".animation.valveHandle#endRot")

                                                            self:addValveAnimationPart(valveNode, startTrans, endTrans, startRot, endRot)

                                                            if valveStemNode ~= nil then
                                                                startTrans = getXMLString(xmlFile, partsKey .. ".animation.valveStem#startTrans")
                                                                endTrans = getXMLString(xmlFile, partsKey .. ".animation.valveStem#endTrans")
                                                                startRot = getXMLString(xmlFile, partsKey .. ".animation.valveStem#startRot")
                                                                endRot = getXMLString(xmlFile, partsKey .. ".animation.valveStem#endRot")
                                                                self:addValveAnimationPart(valveStemNode, startTrans, endTrans, startRot, endRot)
                                                            end
                                                        end
                                                    end
                                                end
                                            end
                                        end
                                    end
                                end

                                part.collisionNode = I3DUtil.indexToObject(part.node, getUserAttribute(part.node, "collisionNode"))

                                if part.collisionNode ~= nil then
                                    part.collisionNodeRigidBody = getRigidBodyType(part.collisionNode)
                                end

                                if self.waterAddon.requiresPurchase then
                                    part.preBuildPosition = StringUtil.getVectorNFromString(getXMLString(xmlFile, partsKey .. "#preBuildPosition"), 3)

                                    if part.preBuildPosition ~= nil then
                                        part.preBuildRotation = Utils.getNoNil(StringUtil.getRadiansFromString(getXMLString(xmlFile, partsKey .. "#preBuildRotation"), 3), AnimalPenExtension.DEFAULT_POS)

                                        setTranslation(part.node, part.preBuildPosition[1], part.preBuildPosition[2], part.preBuildPosition[3])
                                        setRotation(part.node, part.preBuildRotation[1], part.preBuildRotation[2], part.preBuildRotation[3])
                                    end

                                    part.rigidBody = getRigidBodyType(part.node)
                                    setRigidBodyType(part.node, "NoRigidBody")
                                    setVisibility(part.node, false)

                                    if part.collisionNode ~= nil then
                                        setRigidBodyType(part.collisionNode, "NoRigidBody")
                                    end
                                else
                                    setVisibility(part.node, true)
                                end
                            end

                            table.insert(self.waterAddon.parts, part)
                        end
                    else
                        -- Allow fake build parts to be added to extend the time between displayed items.
                        -- This way you can show an item at a specific build time if wanted.

                        if Utils.getNoNil(getXMLBool(xmlFile, partsKey .. "#isPartDelay"), false) then
                            local delayValue = math.max(Utils.getNoNil(getXMLInt(xmlFile, partsKey .. "#delayValue"), 1), 1)

                            for v = 1, delayValue do
                                table.insert(self.waterAddon.parts, {isPartDelay = true})
                            end
                        end
                    end

                    i = i + 1
                end

                -- Set the colour of all the parts.
                self:setPartsColour()

                if self.waterAddon.requiresPurchase then
                    local purchaseMarkerNode, _, _, _, _ = self:loadPartFromI3D(xmlFile, key .. ".construction.marker", self.waterLinkNode, nil, AnimalPenExtension.DEFAULT_POS, false)

                    if purchaseMarkerNode ~= nil then
                        local removeType = Utils.getNoNil(getXMLString(xmlFile, key .. ".construction.marker#removeOn"), "START"):upper()
                        local removeStage = AnimalPenExtension.BUILD_STARTED

                        if removeType == "PURCHASE" then
                            removeStage = AnimalPenExtension.BUILD_START
                        elseif removeType == "FINISH" then
                            removeStage = self.waterAddon.buildTotalHours
                        end

                        self.waterAddon.purchaseMarker = {
                            node = purchaseMarkerNode,
                            removeStage = removeStage
                        }

                        g_messageCenter:subscribe(MessageType.PLAYER_FARM_CHANGED, self.playerFarmChanged, self)
                        g_messageCenter:subscribe(MessageType.PLAYER_CREATED, self.playerFarmChanged, self)

                        self:updateWaterAddonIconVisibility()
                    end

                    i = 0
                    while true do
                        local decorationPartKey = string.format("%s.construction.decorationParts.part(%d)", key, i)
                        if not hasXMLProperty(xmlFile, decorationPartKey) then
                            break
                        end

                        local position = StringUtil.getVectorNFromString(getXMLString(xmlFile, decorationPartKey .. "#position"), 3)

                        if position ~= nil then
                            local extraPart = {
                                isActive = false,
                                position = position,
                                allowMaterialChange = false
                            }

                            extraPart.rotation = StringUtil.getRadiansFromString(Utils.getNoNil(getXMLString(xmlFile, decorationPartKey .. "#rotation"), "0 0 0"), 3)
                            extraPart.scale = StringUtil.getVectorNFromString(getXMLString(xmlFile, decorationPartKey .. "#scale"), 3)

                            local filename = getXMLString(xmlFile, decorationPartKey .. "#filename")

                            if filename ~= nil then
                                -- Allow access to 'animalPenParts.i3d' file also if you wish to load parts for deco from this.

                                if StringUtil.startsWith(filename, "$animalPenParts.i3d") then
                                    extraPart.filename = self.partsI3dFilename
                                    extraPart.baseDirectory = self.extensionBaseDirectory
                                    extraPart.allowMaterialChange = true
                                else
                                    if fileExists(self.baseDirectory .. filename) then
                                        extraPart.filename = filename
                                        extraPart.baseDirectory = self.baseDirectory
                                    else
                                        manager:logPrint(2, "Water Addon parts using i3D filename '%s%s' could not be loaded! (%s)", self.baseDirectory, filename, decorationPartKey)
                                    end
                                end
                            else
                                extraPart.filename = "sharedParts/decoParts.i3d"
                                extraPart.baseDirectory = self.extensionBaseDirectory
                            end

                            if extraPart.filename ~= nil then
                                extraPart.sharedI3dNode = Utils.getNoNil(getXMLString(xmlFile, decorationPartKey .. "#sharedI3dNode"), "0")

                                if self.waterAddon.deco == nil then
                                    self.waterAddon.deco = {}
                                end

                                table.insert(self.waterAddon.deco, extraPart)
                            end
                        end

                        i = i + 1
                    end
                else
                    self.waterAddon.buildHour = self.waterAddon.buildTotalHours
                end
            else
                delete(triggerNode)
            end
        end
    end

    return self.waterAddon ~= nil
end

function AnimalPenExtension:loadMilkAddon(manager, milkModule, xmlFile, key)
    if manager.milkAddonActive and milkModule ~= nil then
        local universalPartsLinkNode = self.nodeId
        self.milkLinkNode = self.nodeId

        if self.hasUniversalParts and milkModule.loadPlace ~= nil then
            self:setLinkNode(milkModule.loadPlace.triggerNode, "milkLinkNode")

            universalPartsLinkNode = createTransformGroup("partsLinkNode")
            link(self.milkLinkNode, universalPartsLinkNode)
        end

        local triggerNode, position, rotation, _, _ = self:loadPartFromI3D(xmlFile, key .. ".trigger", universalPartsLinkNode, nil, AnimalPenExtension.DEFAULT_POS, false)

        if triggerNode ~= nil then
            local activatable = MilkAddonActivatable:new(self)

            if activatable ~= nil then
                addTrigger(triggerNode, "milkAddonTriggerCallback", self)

                local triggerMarker, _, _, _, _ = self:loadPartFromI3D(xmlFile, key .. ".trigger.marker", universalPartsLinkNode, position, rotation, false)

                if triggerMarker ~= nil then
                    g_currentMission:addTriggerMarker(triggerMarker)
                end

                self.milkAddon = {
                    node = universalPartsLinkNode,
                    triggerId = triggerNode,
                    activatable = activatable,
                    triggerMarker = triggerMarker,
                    typeName = "MILK_SALES",
                    allowMaterialChange = false,
                    canScale = false
                }

                local salesAreaNode, _, _, _, _ = self:loadPartFromI3D(xmlFile, key .. ".salesArea", universalPartsLinkNode, AnimalPenExtension.DEFAULT_POS, AnimalPenExtension.DEFAULT_POS, false)

                if salesAreaNode ~= nil then
                    if Utils.getNoNil(getXMLBool(xmlFile, key .. ".salesArea.display#useSalesAreaNode"), false) then
                        self.milkAddon.display = salesAreaNode
                    else
                        self.milkAddon.display = I3DUtil.indexToObject(salesAreaNode, getXMLString(xmlFile, key .. ".salesArea.display#node"))
                    end

                    self.milkAddon.salesOpenSign = I3DUtil.indexToObject(salesAreaNode, getXMLString(xmlFile, key .. ".salesArea.signs#openNode"))
                    self.milkAddon.salesClosedSign = I3DUtil.indexToObject(salesAreaNode, getXMLString(xmlFile, key .. ".salesArea.signs#closedNode"))

                    if self.milkAddon.display ~= nil then
                        I3DUtil.setNumberShaderByValue(self.milkAddon.display, 0, 0, true)
                        milkModule.setFillLevel = Utils.appendedFunction(milkModule.setFillLevel, self.setMilkFillLevel)
                    end
                end

                -- Allow custom mods to add extra delay texts if they want.
                if manager.randomDelayTexts ~= nil then
                    local i = 0

                    while true do
                        local randomTextKey = string.format("%s.randomDelayTexts.text(%d)", key, i)
                        if not hasXMLProperty(xmlFile, randomTextKey) then
                            break
                        end

                        local name = getXMLString(xmlFile, randomTextKey .. "#name")

                        if name ~= nil and g_i18n:hasText(name) then
                            if manager.randomDelayTextsUsed[name] == nil then
                                manager.randomDelayTextsUsed[name] = true

                                table.insert(manager.randomDelayTexts, g_i18n:getText(name))
                            end
                        end

                        i = i + 1
                    end
                end

                self:setMilkSaleSigns((not self:getRandomDelayActive() and self:getIsSalesOpen()))
            end
        end
    end

    return self.milkAddon ~= nil
end

function AnimalPenExtension:loadPartFromI3D(xmlFile, key, linkNode, backupPosition, backupRotation, allowScaling)
    local partNode, position, rotation, filename, isDefault = nil, nil, nil, "", false

    if linkNode ~= nil then
        position = Utils.getNoNil(StringUtil.getVectorNFromString(getXMLString(xmlFile, key .. "#position"), 3), backupPosition)

        if position ~= nil then
            filename, isDefault = self:getPartFilename(xmlFile, key)
            local i3dNode = g_i3DManager:loadSharedI3DFile(filename, "", false, false)

            if i3dNode ~= 0 then
                partNode = I3DUtil.indexToObject(i3dNode, getXMLString(xmlFile, key .. "#sharedI3dNode"))

                if partNode ~= nil then
                    link(linkNode, partNode)

                    setTranslation(partNode, position[1], position[2], position[3])

                    rotation = Utils.getNoNil(StringUtil.getRadiansFromString(getXMLString(xmlFile, key .. "#rotation"), 3), backupRotation)
                    setRotation(partNode, rotation[1], rotation[2], rotation[3])

                    if allowScaling then
                        local scale = StringUtil.getVectorNFromString(getXMLString(xmlFile, key .. "#scale"), 3)

                        if scale ~= nil then
                            setScale(partNode, scale[1], scale[2], scale[3])
                        end
                    end
                end

                -- Store this so it can be cleaned up later.
                table.insert(self.dynamicallyLoadedParts, {node = partNode, filename = filename, baseDirectory = ""})

                delete(i3dNode)
            else
                g_animalPenExtensionManager:logPrint(3, "Failed to load i3d! %s  (%s)", filename, self.customEnvironment)
            end
        end
    end

    return partNode, position, rotation, filename, isDefault
end

function AnimalPenExtension:getPartFilename(xmlFile, key)
    if self.hasUniversalParts then
        return self.extensionBaseDirectory .. "sharedParts/universalParts.i3d", true
    end

    local filename = getXMLString(xmlFile, key .. "#filename")

    if filename ~= nil and not StringUtil.startsWith(filename, "$animalPenParts.i3d") then
        return self.baseDirectory .. filename, false
    end

    return self.partsBaseDirectory .. self.partsI3dFilename, true
end

function AnimalPenExtension:setLinkNode(transformId, linkNodeName)
    if transformId ~= nil and linkNodeName ~= nil then
        local linkNode = self[linkNodeName]

        if linkNode == nil or linkNode == self.nodeId then
            linkNode = createTransformGroup(linkNodeName)
        end

        if getParent(linkNode) ~= self.nodeId then
            link(self.nodeId, linkNode)
        end

        local wlnX, wlnY, wlnZ = localToLocal(transformId, self.nodeId, 0, 0, 0)
        local dx,dy,dz = localDirectionToLocal(transformId, self.nodeId, 0, 0, 1)
        local upx,upy,upz = localDirectionToLocal(transformId, self.nodeId, 0, 1, 0)

        setDirection(linkNode, dx,dy,dz, upx,upy,upz)
        setTranslation(linkNode, wlnX, wlnY, wlnZ)

        self[linkNodeName] = linkNode
    end
end

function AnimalPenExtension:addValveAnimationPart(node, startTrans, endTrans, startRot, endRot)
    local animCurve = AnimCurve:new(linearInterpolatorN)
    local sx, sy, sz = StringUtil.getVectorFromString(startTrans)
    local ex, ey, ez = StringUtil.getVectorFromString(endTrans)
    local srx, sry, srz = StringUtil.getVectorFromString(startRot)
    local erx, ery, erz = StringUtil.getVectorFromString(endRot)
    local nx,ny,nz = getTranslation(node)
    local nrx,nry,nrz = getRotation(node)

    srx = Utils.getNoNilRad(srx, nrx)
    sry = Utils.getNoNilRad(sry, nry)
    srz = Utils.getNoNilRad(srz, nrz)
    sx = Utils.getNoNil(sx, nx)
    sy = Utils.getNoNil(sy, ny)
    sz = Utils.getNoNil(sz, nz)
    animCurve:addKeyframe({sx, sy, sz, srx, sry, srz, time = 0})

    erx = Utils.getNoNilRad(erx, nrx)
    ery = Utils.getNoNilRad(ery, nry)
    erz = Utils.getNoNilRad(erz, nrz)
    ex = Utils.getNoNil(ex, nx)
    ey = Utils.getNoNil(ey, ny)
    ez = Utils.getNoNil(ez, nz)
    animCurve:addKeyframe({ex, ey, ez, erx, ery, erz, time = 1})

    setTranslation(node, sx, sy, sz)
    setRotation(node, srx, sry, srz)

    table.insert(self.waterAddon.valveAnimation.parts, {node = node, animCurve = animCurve})
end

function AnimalPenExtension:delete()
    g_messageCenter:unsubscribeAll(self)

    if self.chickenAddon ~= nil and self.chickenAddon.triggerMarker ~= nil then
        g_currentMission:removeTriggerMarker(self.chickenAddon.triggerMarker)

        self.chickenAddon = nil
    end

    if self.waterAddon ~= nil then
        if self.universalPlacementActive and g_animalPenExtensionManager ~= nil then
            g_animalPenExtensionManager.universalPlacement:cancelOrDelete(false, false)
        end

        if self.waterAddon.triggerId ~= nil then
            if self.waterAddon.playerInRange ~= nil then
                g_currentMission:removeActivatableObject(self.waterAddon.activatable)
                self.waterAddon.playerInRange = nil
            end

            removeTrigger(self.waterAddon.triggerId)
            self.waterAddon.triggerId = nil

            if self.waterAddon.triggerMarker ~= nil then
                g_currentMission:removeTriggerMarker(self.waterAddon.triggerMarker)
            end
        end

        if self.isClient then
            if self.waterAddon.effects ~= nil then
                for id, effect in pairs(self.waterAddon.effects) do
                    g_effectManager:deleteEffects(effect)
                end

                self.waterAddon.effects = nil
            end

            if self.waterAddon.samplesWater ~= nil then
                for _, sample in pairs (self.waterAddon.samplesWater) do
                    g_soundManager:deleteSample(self.waterAddon.samplesWater)
                end

                self.waterAddon.samplesWater = nil
            end

            if self.waterAddon.sampleHandle ~= nil then
                g_soundManager:deleteSample(self.waterAddon.sampleHandle)

                self.waterAddon.sampleHandle = nil
            end
        end

        if self.waterAddon.deco ~= nil then
            for _, extraPart in pairs(self.waterAddon.deco) do
                if extraPart.node ~= nil and extraPart.filename ~= nil and extraPart.baseDirectory ~= nil then
                    delete(extraPart.node)
                    extraPart.node = nil

                    g_i3DManager:releaseSharedI3DFile(extraPart.filename, extraPart.baseDirectory, true)
                end
            end
        end

        self.waterAddon = nil
    end

    if self.milkAddon ~= nil then
        if self.milkAddon.triggerId ~= nil then
            if self.milkAddon.playerInRange ~= nil then
                g_currentMission:removeActivatableObject(self.milkAddon.activatable)
                self.milkAddon.playerInRange = nil
            end

            removeTrigger(self.milkAddon.triggerId)
            self.milkAddon.triggerId = nil

            if self.milkAddon.triggerMarker ~= nil then
                g_currentMission:removeTriggerMarker(self.milkAddon.triggerMarker)
            end
        end

        self.milkAddon = nil
    end

    if self.dynamicallyLoadedParts ~= nil then
        for _, part in pairs(self.dynamicallyLoadedParts) do
            if part.filename ~= nil then
                g_i3DManager:releaseSharedI3DFile(part.filename, part.baseDirectory, true)
            end
        end

        self.dynamicallyLoadedParts = {}
    end

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

        g_currentMission.environment:removeHourChangeListener(self)
    end
end

function AnimalPenExtension:loadFromXMLFile(xmlFile, key)
    if self.waterAddon ~= nil then
        if self.waterAddon.requiresPurchase then
            local buildHour = getXMLInt(xmlFile, key .. ".waterAddon#buildHour")
            if buildHour ~= nil and buildHour >= AnimalPenExtension.BUILD_WAITING then
                self:setWaterAddonBuildHour(buildHour)
            end

            if self.waterAddon.buildHour >= AnimalPenExtension.BUILD_START then
                self.waterAddon.hasBeenPurchased = true
            else
                self.waterAddon.hasBeenPurchased = Utils.getNoNil(getXMLBool(xmlFile, key .. ".waterAddon#hasBeenPurchased"), false)
            end
        end
    end

    if self.hasUniversalParts and hasXMLProperty(xmlFile, key .. ".universalParts") then
        local name = self.customEnvironment
        if name == nil or name == "" then
            name = self.baseDirectory
        end

        local version = getXMLInt(xmlFile, key .. ".universalParts#version")

        if version ~= nil and version == AnimalPenExtension.UNIVERSAL_PARTS_VERSION then
            if self.waterAddon ~= nil and hasXMLProperty(xmlFile, key .. ".universalParts.waterAddonPart(0)") then
                local waterAddonParts = {}
                local numWaterAddonParts = #self.waterAddon.parts

                for i = 0, numWaterAddonParts - 1 do
                    local part = {}

                    part.x, part.y, part.z = StringUtil.getVectorFromString(getXMLString(xmlFile, string.format("%s.universalParts.waterAddonPart(%d)#position", key, i)))
                    part.ry = tonumber(getXMLString(xmlFile, string.format("%s.universalParts.waterAddonPart(%d)#rotation", key, i)))

                    if part.x ~= nil and part.y ~= nil and part.z ~= nil and part.ry ~= nil then
                        part.ry = math.rad(part.ry)
                        part.sx, part.sy, part.sz = StringUtil.getVectorFromString(getXMLString(xmlFile, string.format("%s.universalParts.waterAddonPart(%d)#scale", key, i)))

                        table.insert(waterAddonParts, part)
                    else
                        break
                    end
                end

                local defaultColourIndex = getXMLInt(xmlFile, key .. ".universalParts#waterAddonColourIndex") or 0

                if #waterAddonParts ~= numWaterAddonParts or not self:setUniversalParts(waterAddonParts, true, false, defaultColourIndex) then
                    g_animalPenExtensionManager:logPrint(1, "Water Addon 'Universal Parts' have changed or failed to load correctly from save game! These will be reset for mod '%s'.", name)
                end
            end

            if self.milkAddon ~= nil then
                local part = {}

                part.x, part.y, part.z = StringUtil.getVectorFromString(getXMLString(xmlFile, key .. ".universalParts.milkAddonPart(%d)#position"))
                part.ry = tonumber(getXMLString(xmlFile, key .. ".universalParts.milkAddonPart(%d)#rotation"))

                if part.x ~= nil and part.y ~= nil and part.z ~= nil and part.ry ~= nil then
                    part.ry = math.rad(part.ry)
                    part.sx, part.sy, part.sz = StringUtil.getVectorFromString(getXMLString(xmlFile, key .. ".universalParts.milkAddonPart(%d)#scale"))

                    if not self:setUniversalParts(part, false, false) then
                        g_animalPenExtensionManager:logPrint(1, "Milk Addon 'Universal Parts' have changed or failed to load correctly from save game! These will be reset for mod '%s'.", name)
                    end
                end
            end
        else
            g_animalPenExtensionManager:logPrint(1, "A new version Universal Parts version has been loaded! All parts will be reset to avoid conflicts for mod '%s'.", name)
        end
    end

    return true
end

function AnimalPenExtension:saveToXMLFile(xmlFile, key, usedModNames)
    if self.waterAddon ~= nil then
        if self.waterAddon.requiresPurchase then
            setXMLInt(xmlFile, key .. ".waterAddon#buildHour", self.waterAddon.buildHour)
            setXMLBool(xmlFile, key .. ".waterAddon#hasBeenPurchased", self.waterAddon.hasBeenPurchased)
        end
    end

    if self.hasUniversalParts then
        setXMLInt(xmlFile, key .. ".universalParts#version", AnimalPenExtension.UNIVERSAL_PARTS_VERSION)

        if self.waterAddon ~= nil and self.universalWaterAddonSet then
            for i, part in ipairs (self.waterAddon.parts) do
                local x, y, z, _, ry, _, sx, sy, sz = AnimalPenExtension.getNodePositions(part.node)

                setXMLString(xmlFile, string.format("%s.universalParts.waterAddonPart(%d)#position", key, i - 1), string.format("%.4f %.4f %.4f", x, y, z))
                setXMLString(xmlFile, string.format("%s.universalParts.waterAddonPart(%d)#rotation", key, i - 1), string.format("%.4f", math.deg(ry)))
                setXMLString(xmlFile, string.format("%s.universalParts.waterAddonPart(%d)#scale", key, i - 1), string.format("%.4f %.4f %.4f", sx, sy, sz))
            end

            if self.waterAddon.defaultColourIndex ~= nil and self.waterAddon.defaultColourIndex > 1 then
                setXMLInt(xmlFile, key .. ".universalParts#waterAddonColourIndex", self.waterAddon.defaultColourIndex)
            end
        end

        if self.milkAddon ~= nil and self.universalMilkAddonSet then
            local x, y, z, _, ry, _, sx, sy, sz = AnimalPenExtension.getNodePositions(self.milkAddon.node)

            setXMLString(xmlFile, key .. ".universalParts.milkAddonPart#position", string.format("%.4f %.4f %.4f", x, y, z))
            setXMLString(xmlFile, key .. ".universalParts.milkAddonPart#rotation", string.format("%.4f", math.deg(ry)))
            setXMLString(xmlFile, key .. ".universalParts.milkAddonPart#scale", string.format("%.4f %.4f %.4f", sx, sy, sz))
        end
    end
end

function AnimalPenExtension:readStream(streamId, connection)
    local animalPenExtensionId = NetworkUtil.readNodeObjectId(streamId)

    if self.waterAddon ~= nil then
        local isActive = streamReadBool(streamId)
        self:setFillingState(isActive, true)

        local buildHour = streamReadInt8(streamId)
        self:setWaterAddonBuildHour(buildHour)
        self.waterAddon.hasBeenPurchased = buildHour >= AnimalPenExtension.BUILD_START
        self.waterAddon.defaultColourIndex = streamReadUInt8(streamId)

        if streamReadBool(streamId) then
            local parts = {}
            local numParts = streamReadUInt8(streamId)

            for i = 1, numParts do
                local part = {}

                part.x = streamReadFloat32(streamId)
                part.y = streamReadFloat32(streamId)
                part.z = streamReadFloat32(streamId)

                part.ry = NetworkUtil.readCompressedAngle(streamId)

                part.sx = streamReadFloat32(streamId)
                part.sy = streamReadFloat32(streamId)
                part.sz = streamReadFloat32(streamId)

                table.insert(parts, part)
            end

            self:setUniversalParts(parts, true, false, -1)
        end

        self:setPartsColour()
    end

    if self.milkAddon ~= nil then
        if streamReadBool(streamId) then
            local part = {}

            part.x = streamReadFloat32(streamId)
            part.y = streamReadFloat32(streamId)
            part.z = streamReadFloat32(streamId)

            part.ry = NetworkUtil.readCompressedAngle(streamId)

            part.sx = streamReadFloat32(streamId)
            part.sy = streamReadFloat32(streamId)
            part.sz = streamReadFloat32(streamId)

            self:setUniversalParts(part, false, false)
        end
    end

    g_client:finishRegisterObject(self, animalPenExtensionId)
end

function AnimalPenExtension:writeStream(streamId, connection)
    NetworkUtil.writeNodeObjectId(streamId, NetworkUtil.getObjectId(self))

    if self.waterAddon ~= nil then
        streamWriteBool(streamId, self.waterAddon.isActive)
        streamWriteInt8(streamId, self.waterAddon.buildHour)
        streamWriteUInt8(streamId, self.waterAddon.defaultColourIndex)

        if streamWriteBool(streamId, self.universalWaterAddonSet) then
            local numParts = #self.waterAddon.parts

            streamWriteUInt8(streamId, numParts)

            for i = 1, numParts do
                local  x, y, z, _, ry, _, sx, sy, sz = AnimalPenExtension.getNodePositions(self.waterAddon.parts[i].node)

                streamWriteFloat32(streamId, x)
                streamWriteFloat32(streamId, y)
                streamWriteFloat32(streamId, z)

                NetworkUtil.writeCompressedAngle(streamId, ry)

                streamWriteFloat32(streamId, sx)
                streamWriteFloat32(streamId, sy)
                streamWriteFloat32(streamId, sz)
            end
        end
    end

    if self.milkAddon ~= nil then
        if streamWriteBool(streamId, self.universalMilkAddonSet) then
            local x, y, z, _, ry, _, sx, sy, sz = AnimalPenExtension.getNodePositions(self.milkAddon.node)

            streamWriteFloat32(streamId, x)
            streamWriteFloat32(streamId, y)
            streamWriteFloat32(streamId, z)

            NetworkUtil.writeCompressedAngle(streamId, ry)

            streamWriteFloat32(streamId, sx)
            streamWriteFloat32(streamId, sy)
            streamWriteFloat32(streamId, sz)
        end
    end

    g_server:registerObjectInStream(connection, self)
end

function AnimalPenExtension:hourChanged()
    if self.isClient and self.milkAddon ~= nil then
        local state = not self:getRandomDelayActive() and self:getIsSalesOpen()

        if state ~= self.milkAddon.salesSignState then
            self:setMilkSaleSigns(state)
        end
    end

    if self.isServer and self.waterAddon ~= nil then
        if self.waterAddon.rainInputPerHour > 0 and g_currentMission.environment.weather:getIsRaining() then
            self:raiseActive()
        end

        if self.waterAddon.requiresPurchase and self.waterAddon.buildHour < self.waterAddon.buildTotalHours then
            local currentHour = g_currentMission.environment.currentHour

            if currentHour >= AnimalPenExtension.SALES_OPEN_TIME and currentHour < AnimalPenExtension.SALES_CLOSED_TIME then
                if self.waterAddon.buildHour >= AnimalPenExtension.BUILD_STARTED then
                    self:setWaterAddonBuildHour(self.waterAddon.buildHour + 1)
                else
                    if self.waterAddon.buildHour == AnimalPenExtension.BUILD_START then
                        if currentHour == AnimalPenExtension.SALES_OPEN_TIME then
                            self:setWaterAddonBuildHour(AnimalPenExtension.BUILD_STARTED)
                        end
                    end
                end
            end
        end
    end
end

function AnimalPenExtension:setWaterAddonBuildHour(buildHour)
    self.waterAddon.buildHour = buildHour

    if self.waterAddon.requiresPurchase then
        if g_server ~= nil then
            g_server:broadcastEvent(AnimalPenExtensionBuildHourEvent:new(self, buildHour))

            if buildHour == AnimalPenExtension.BUILD_START and not self.waterAddon.hasBeenPurchased then
                self.waterAddon.hasBeenPurchased = true
                g_currentMission:addMoney(-self.waterAddon.buildCost, self:getOwnerFarmId(), MoneyType.PROPERTY_MAINTENANCE, true, true)
            end
        end

        if self.waterAddon.deco ~= nil and self.waterAddon.buildHour >= AnimalPenExtension.BUILD_STARTED then
            if self.waterAddon.buildHour < self.waterAddon.buildTotalHours then
                if not self.waterAddon.decoIsActive then
                    self.waterAddon.decoIsActive = true

                    for _, extraPart in pairs(self.waterAddon.deco) do
                        if extraPart.node == nil and extraPart.filename ~= nil and extraPart.baseDirectory ~= nil then
                            local i3dNode = g_i3DManager:loadSharedI3DFile(extraPart.filename, extraPart.baseDirectory, false, false)

                            if i3dNode ~= 0 then
                                extraPart.node = I3DUtil.indexToObject(i3dNode, extraPart.sharedI3dNode)

                                setTranslation(extraPart.node, extraPart.position[1], extraPart.position[2], extraPart.position[3])
                                setRotation(extraPart.node, extraPart.rotation[1], extraPart.rotation[2], extraPart.rotation[3])

                                if extraPart.scale ~= nil then
                                    setScale(extraPart.node, extraPart.scale[1], extraPart.scale[2], extraPart.scale[3])
                                end

                                link(self.nodeId, extraPart.node)
                                addToPhysics(extraPart.node)

                                delete(i3dNode)
                            end
                        end
                    end

                    -- Set the colour of all the parts.
                    self:setPartsColour(self.waterAddon.deco)
                end
            else
                for _, extraPart in pairs(self.waterAddon.deco) do
                    if extraPart.node ~= nil and extraPart.filename ~= nil and extraPart.baseDirectory ~= nil then
                        delete(extraPart.node)
                        extraPart.node = nil
                        g_i3DManager:releaseSharedI3DFile(extraPart.filename, extraPart.baseDirectory, true)
                    end
                end

                self.waterAddon.deco = nil
            end
        end

        if self.waterAddon.parts ~= nil then
            local numberOfParts = #self.waterAddon.parts
            local maxNode = math.ceil((numberOfParts * self.waterAddon.buildHour) / self.waterAddon.buildTotalHours)

            for i = 1, numberOfParts do
                local part = self.waterAddon.parts[i]

                if part.node ~= nil then
                    if i <= maxNode then
                        if part.preBuildPosition ~= nil then
                            setVisibility(part.node, buildHour >= AnimalPenExtension.BUILD_STARTED)

                            if not part.posRotSet then
                                setTranslation(part.node, part.position[1], part.position[2], part.position[3])
                                setRotation(part.node, part.rotation[1], part.rotation[2], part.rotation[3])
                                setRigidBodyType(part.node, part.rigidBody or "NoRigidBody")

                                if part.collisionNode ~= nil then
                                    setRigidBodyType(part.collisionNode, part.collisionNodeRigidBody or "NoRigidBody")
                                end

                                part.posRotSet = true
                            end
                        else
                            setVisibility(part.node, true)
                            setRigidBodyType(part.node, part.rigidBody or "NoRigidBody")

                            if part.collisionNode ~= nil then
                                setRigidBodyType(part.collisionNode, part.collisionNodeRigidBody or "NoRigidBody")
                            end
                        end
                    else
                        if part.preBuildPosition ~= nil then
                            setVisibility(part.node, buildHour >= AnimalPenExtension.BUILD_STARTED)

                            if part.posRotSet == nil then
                                if part.collisionNode ~= nil then
                                    setRigidBodyType(part.collisionNode, "NoRigidBody")
                                end

                                setRigidBodyType(part.node, "NoRigidBody")
                                setTranslation(part.node, part.preBuildPosition[1], part.preBuildPosition[2], part.preBuildPosition[3])
                                setRotation(part.node, part.preBuildRotation[1], part.preBuildRotation[2], part.preBuildRotation[3])

                                part.posRotSet = false
                            end
                        else
                            setVisibility(part.node, false)
                            setRigidBodyType(part.node, "NoRigidBody")

                            if part.collisionNode ~= nil then
                                setRigidBodyType(part.collisionNode, "NoRigidBody")
                            end
                        end
                    end
                end
            end
        end

        if self.waterAddon.purchaseMarker ~= nil then
            if buildHour >= self.waterAddon.purchaseMarker.removeStage then
                setVisibility(self.waterAddon.purchaseMarker.node, false)

                delete(self.waterAddon.purchaseMarker.node)
                self.waterAddon.purchaseMarker = nil

                g_messageCenter:unsubscribe(MessageType.PLAYER_FARM_CHANGED, self)
                g_messageCenter:unsubscribe(MessageType.PLAYER_CREATED, self)
            else
                self:updateWaterAddonIconVisibility()
            end
        end
    end
end

function AnimalPenExtension:updateWaterAddonIconVisibility()
    if self.waterAddon ~= nil and self.waterAddon.purchaseMarker ~= nil then
        local farmId = g_currentMission:getFarmId()
        local ownerFarmId, isVisible = self:getCurrentFarmId()

        isVisible = isVisible and farmId ~= FarmManager.SPECTATOR_FARM_ID and farmId == ownerFarmId

        setVisibility(self.waterAddon.purchaseMarker.node, isVisible)
    end
end

function AnimalPenExtension:playerFarmChanged(player)
    if player == g_currentMission.player then
        self:updateWaterAddonIconVisibility()
    end
end

function AnimalPenExtension:setOwnerFarmId(ownerFarmId, noEventSend)
    AnimalPenExtension:superClass().setOwnerFarmId(self, ownerFarmId, noEventSend)

    self:updateWaterAddonIconVisibility()
end

function AnimalPenExtension:getIsWaterAddonOwned()
    if self.hasUniversalParts then
        return self.universalWaterAddonSet
    end

    return self.waterAddon.buildHour >= self.waterAddon.buildTotalHours
end

function AnimalPenExtension:getIsMilkAddonOwned()
    if self.hasUniversalParts then
        return self.universalMilkAddonSet
    end

    return true
end

function AnimalPenExtension:getIsSalesOpen(currentHour)
    local currentHour = g_currentMission.environment.currentHour
    return currentHour >= AnimalPenExtension.SALES_OPEN_TIME and currentHour < AnimalPenExtension.SALES_CLOSED_TIME
end

function AnimalPenExtension:dayChanged()
    if self.isServer then
        if self.waterAddon ~= nil and self.waterAddon.maintenanceCost > 0 then
            local farmId, isValid = self:getCurrentFarmId()

            if isValid then
                local _, capacity = self:getWaterLevels()

                if capacity > 0 then
                    g_currentMission:addMoney(-self.waterAddon.maintenanceCost, farmId, MoneyType.PROPERTY_MAINTENANCE, true)
                end
            end
        end
    end
end

function AnimalPenExtension:update(dt)
    if self.waterAddon ~= nil then
        local playerInTrigger = self.waterAddon.playerInRange ~= nil and self.waterAddon.playerInRange == g_currentMission.player

        if playerInTrigger then
            if self:getIsWaterAddonOwned() then
                if self.owner:getNumOfAnimals() > 0 then
                    local fillLevel, capacity = self:getWaterLevels()
                    g_currentMission:addExtraPrintText(string.format("%s:  %s / %s  (%s%%)", self.texts.waterFillLevel, math.floor(fillLevel), math.floor(capacity), math.floor(100 * fillLevel / capacity)))
                else
                    g_currentMission:addExtraPrintText(self.texts.noAnimalsWarning)
                end
            end

            self:raiseActive()
        end

        if self.waterAddon.isActive then
            if self.isServer then
                local waterModule = self.owner.modulesByName["water"]
                local fillDelta = math.min(waterModule:getFreeCapacity(FillType.WATER), self.waterAddon.fillLitersPerSecond * dt * 0.001)

                self:updateTrough(waterModule, fillDelta, true)
            end

            self:raiseActive()

            self:fillEffectState(true)
        else
            self:fillEffectState(false)
        end

        if self.isClient then
            if self.waterAddon.valveAnimationClip ~= nil then
                if self.waterAddon.isActive or self.waterAddon.valveAnimationClip.speedScale ~= 0 then
                    local currentTime = getAnimTrackTime(self.waterAddon.valveAnimationClip.characterSet, 0)
                    local isClipActive = false

                    if self.waterAddon.valveAnimationClip.speedScale == 1 then
                        isClipActive = currentTime < self.waterAddon.valveAnimationClip.duration
                    elseif self.waterAddon.valveAnimationClip.speedScale == -1 then
                        isClipActive = currentTime > 0.0
                    end

                    if isClipActive then
                        self:raiseActive()
                    else
                        disableAnimTrack(self.waterAddon.valveAnimationClip.characterSet, 0)
                        self.waterAddon.valveAnimationClip.speedScale = 0
                    end

                    self:updateValveSound(isClipActive)
                end
            elseif self.waterAddon.valveAnimation ~= nil then
                if self.waterAddon.isActive or (self.waterAddon.valveAnimation.animTime > 0) or self.waterAddon.handleSoundActive then
                    self:updateValveAnimation(self.waterAddon.isActive, dt)
                end
            end
        end

        if self.isServer and self.waterAddon.rainInputPerHour > 0 then
            if g_currentMission.environment.weather:getIsRaining() then
                self.waterAddon.updateMinute = self.waterAddon.updateMinute + (dt * g_currentMission.missionInfo.timeScale)
                if self.waterAddon.updateMinute >= 60000 then
                    self.waterAddon.updateMinute = 0

                    local rainToAdd = (self.waterAddon.rainInputPerHour / 60) * g_currentMission.environment.weather:getRainFallScale()
                    self.waterAddon.rainWater = self.waterAddon.rainWater + rainToAdd

                    if self.waterAddon.rainWater >= 15 then
                        self:updateTrough(self.owner.modulesByName["water"], self.waterAddon.rainWater)
                        self.waterAddon.rainWater = 0
                    end
                end

                self:raiseActive()
            else
                if self.waterAddon.rainWater ~= 0 then
                    if self.waterAddon.rainWater > 7 then
                        self:updateTrough(self.owner.modulesByName["water"], self.waterAddon.rainWater)
                    end

                    self.waterAddon.rainWater = 0

                    if self.waterAddon.updateMinute ~= 0 then
                        self.waterAddon.updateMinute = 0
                    end
                end
            end
        end
    end
end

function AnimalPenExtension:setFillingState(state, noEventSend)
    AnimalPenExtensionFillEvent.sendEvent(self, state, noEventSend)
    self.waterAddon.isActive = state

    if self.isClient then
        if self.waterAddon.valveAnimationClip ~= nil then
            local animTime, speedScale = 0.0, 1
            enableAnimTrack(self.waterAddon.valveAnimationClip.characterSet, 0)
            local currentTime = getAnimTrackTime(self.waterAddon.valveAnimationClip.characterSet, 0)

            if state then
                animTime = math.max(0.0, currentTime)
            else
                speedScale = -1
                animTime = math.min(currentTime, self.waterAddon.valveAnimationClip.duration)
            end

            setAnimTrackTime(self.waterAddon.valveAnimationClip.characterSet, 0, animTime)
            setAnimTrackSpeedScale(self.waterAddon.valveAnimationClip.characterSet, 0, speedScale)
            self.waterAddon.valveAnimationClip.speedScale = speedScale
        end
    end

    self:raiseActive()
end

function AnimalPenExtension:fillEffectState(isActive)
    if self.isClient then
        if self.waterAddon.effectsActive ~= isActive then
            if self.waterAddon.effects ~= nil then
                for _, effect in pairs (self.waterAddon.effects) do
                    if isActive then
                        g_effectManager:setFillType(effect, FillType.WATER)
                        g_effectManager:startEffects(effect)
                    else
                        g_effectManager:stopEffects(effect)
                    end
                end
            end

            if self.waterAddon.samplesWater ~= nil then
                for _, sample in pairs (self.waterAddon.samplesWater) do
                    if isActive then
                        g_soundManager:playSample(sample)
                    else
                        g_soundManager:stopSample(sample)
                    end
                end
            end
        end

        self.waterAddon.effectsActive = isActive
    end
end

function AnimalPenExtension:updateValveAnimation(positive, dt)
    local direction = -1
    local doUpdate = true
    local valveAnimation = self.waterAddon.valveAnimation

    if positive then
        direction = 1
    end

    if direction > 0 then
        if valveAnimation.animTime == 1 then
            doUpdate = false
        end
    else
        if valveAnimation.animTime == 0 then
            doUpdate = false
        end
    end

    if doUpdate then
        valveAnimation.animTime = MathUtil.clamp(valveAnimation.animTime + direction * dt / valveAnimation.duration, 0, 1)

        for _, part in pairs (valveAnimation.parts) do
            local v = part.animCurve:get(valveAnimation.animTime)
            setTranslation(part.node, v[1], v[2], v[3])
            setRotation(part.node, v[4], v[5], v[6])
        end

        self:raiseActive()
    end

    self:updateValveSound(doUpdate)
end

function AnimalPenExtension:updateValveSound(isActive)
    if self.waterAddon.sampleHandle ~= nil then
        if isActive then
            if not self.waterAddon.handleSoundActive then
                self.waterAddon.handleSoundActive = true
                g_soundManager:playSample(self.waterAddon.sampleHandle)
            end
        else
            if self.waterAddon.handleSoundActive then
                self.waterAddon.handleSoundActive = false
                g_soundManager:stopSample(self.waterAddon.sampleHandle)
            end
        end
    end
end

function AnimalPenExtension:updateTrough(waterModule, fillDelta, isTownWater)
    local farmId, isValid = self:getCurrentFarmId()

    if fillDelta > 0 and fillDelta <= waterModule:getFreeCapacity(FillType.WATER) then
        waterModule:addFillLevelFromTool(farmId, fillDelta, FillType.WATER)
    else
        if self.waterAddon.isActive then
            self:setFillingState(false)
        end
    end

    if isValid then
        if isTownWater == true and self.waterAddon.waterPriceScale > 0 then
            local price = fillDelta * g_currentMission.economyManager:getPricePerLiter(FillType.WATER) * self.waterAddon.waterPriceScale
            g_farmManager:updateFarmStats(farmId, "expenses", price)
            g_currentMission:addMoney(-price, farmId, MoneyType.ANIMAL_UPKEEP, true)
        end
    end
end

function AnimalPenExtension:getWaterLevels()
    local fillLevel, capacity = 0, 0
    local waterModule = self.owner.modulesByName["water"]

    if waterModule ~= nil then
        fillLevel = Utils.getNoNil(waterModule.fillLevels[FillType.WATER], 0)
        capacity = waterModule.fillCapacity or 0
    end

    return fillLevel, capacity
end

function AnimalPenExtension:getCanWaterActivate()
    if self.owner:getNumOfAnimals() > 0 then
        local waterModule = self.owner.modulesByName["water"]

        if waterModule ~= nil then
            local fillLevel = Utils.getNoNil(waterModule.fillLevels[FillType.WATER], 0.0)
            local capacity = waterModule.fillCapacity or 0

            return capacity > 0 and math.floor(100 * (fillLevel / capacity)) < 98
        end
    end

    return false
end

function AnimalPenExtension:setMilkFillLevel(fillTypeIndex, fillLevel)
    if self.owner.animalPenExtension ~= nil then
        local milkAddon = self.owner.animalPenExtension.milkAddon

        if milkAddon ~= nil and milkAddon.display ~= nil then
            I3DUtil.setNumberShaderByValue(milkAddon.display, fillLevel, 0, true)
        end
    end
end

function AnimalPenExtension:setMilkSaleSigns(state)
    self.milkAddon.salesSignState = state

    if self.milkAddon.salesOpenSign ~= nil then
        setVisibility(self.milkAddon.salesOpenSign, state)
    end

    if self.milkAddon.salesClosedSign ~= nil then
        setVisibility(self.milkAddon.salesClosedSign, not state)
    end
end

function AnimalPenExtension:getRandomDelayActive()
    if g_animalPenExtensionManager ~= nil then
        return g_animalPenExtensionManager.randomDelayActive
    end

    return false
end

function AnimalPenExtension:getMilkLevels()
    local fillLevel, capacity = 0, 0
    local milkModule = self.owner.modulesByName["milk"]

   if milkModule ~= nil then
        fillLevel = Utils.getNoNil(milkModule.fillLevels[FillType.MILK], 0.0)
        capacity = milkModule.fillCapacity or 0
    end

    return fillLevel, capacity
end

function AnimalPenExtension:openMilkContractorMenu()
    if self:getIsSalesOpen() then
        if self:getRandomDelayActive() then
            local text = g_animalPenExtensionManager:getNextRandomText()

            g_gui:showInfoDialog({
                visible = true,
                text = text,
                dialogType = DialogElement.TYPE_INFO,
                isCloseAllowed = true
            })
        else
            local dialog = g_gui:showDialog("MilkSaleDialog")

            if dialog ~= nil then
                local fillLevel, capacity = self:getMilkLevels()

                dialog.target:setTitle(self.texts.dialogHeader)
                dialog.target:setData(fillLevel, capacity)
                dialog.target:setCallback(self.sellMilkCallback, self)
            end
        end
    else
        g_gui:showInfoDialog({
            visible = true,
            text = self.texts.salesClosed,
            dialogType = DialogElement.TYPE_INFO,
            isCloseAllowed = true
        })
    end
end

function AnimalPenExtension:sellMilkCallback(amountToSell, contractorFee, sellPoint)
    if amountToSell > 0 and sellPoint ~= nil then
        if g_currentMission:getIsServer() then
            local milkModule = self.owner.modulesByName["milk"]
            local oldMilk = milkModule.fillLevels[FillType.MILK]

            if oldMilk ~= nil and oldMilk >= amountToSell then
                local farmId, isValid = self:getCurrentFarmId()

                if isValid then
                    sellPoint:addFillLevelFromTool(farmId, amountToSell, FillType.MILK)

                    local currentMilk = Utils.getNoNil(milkModule.fillLevels[FillType.MILK], 0.0)
                    milkModule:setFillLevel(FillType.MILK, currentMilk - amountToSell)

                    if contractorFee ~= nil and contractorFee > 0 then
                        g_currentMission:addMoney(-contractorFee, farmId, MoneyType.ANIMAL_UPKEEP, true, true)
                    end
                end
            end
        else
            g_client:getServerConnection():sendEvent(AnimalPenExtensionSellMilkEvent:new(self, amountToSell, Utils.getNoNil(contractorFee, 0), sellPoint))
        end
    end
end

function AnimalPenExtension:waterAddonTriggerCallback(triggerId, otherId, onEnter, onLeave, onStay)
    if onEnter or onLeave then
        if g_currentMission.player ~= nil and otherId == g_currentMission.player.rootNode then
            self:updateTriggerCallback(self.waterAddon, onEnter, self.universalWaterAddonSet)
            self:raiseActive()
        end
    end
end

function AnimalPenExtension:milkAddonTriggerCallback(triggerId, otherId, onEnter, onLeave, onStay)
    if onEnter or onLeave then
        if g_currentMission.player ~= nil and otherId == g_currentMission.player.rootNode then
            self:updateTriggerCallback(self.milkAddon, onEnter, self.universalMilkAddonSet)
        end
    end
end

function AnimalPenExtension:updateTriggerCallback(addon, onEnter, addonSet)
    if g_currentMission ~= nil and addon ~= nil then
        if onEnter then
            if addon.playerInRange == nil then
                local canAddActivatable = false

                if self.hasUniversalParts and not g_currentMission.missionDynamicInfo.isMultiplayer then
                    canAddActivatable = not addonSet
                end

                if not canAddActivatable then
                    local farmId = g_currentMission:getFarmId()
                    local ownerFarmId, isValid = self:getCurrentFarmId()

                    canAddActivatable = isValid and farmId ~= FarmManager.SPECTATOR_FARM_ID and ownerFarmId == farmId
                end

                if canAddActivatable then
                    g_currentMission:addActivatableObject(addon.activatable)
                    addon.playerInRange = g_currentMission.player
                end
            end
        else
            if addon.playerInRange ~= nil then
                g_currentMission:removeActivatableObject(addon.activatable)
                addon.playerInRange = nil
            end
        end
    end
end

function AnimalPenExtension:preBuyText(hasPermission)
    -- Adjust 'precision' if the purchase price is less than '0.1' so it does not just show '0'
    local precision = 2
    local cost = g_currentMission.economyManager:getPricePerLiter(FillType.WATER) * self.waterAddon.waterPriceScale

    if cost < 0.1 then
        if cost > 0.01 then
            precision = 3
        else
            precision = 4
        end
    end

    local buildCost = g_i18n:formatMoney(self.waterAddon.buildCost, 0, true, true)
    local waterCost =  string.format("%s: %s / %s.", self.texts.water, g_i18n:formatMoney(cost, precision, true, true), g_i18n:getVolumeUnit(true))
    local maintenanceCost = string.format(g_i18n:getText("shop_maintenanceValue"), g_i18n:formatMoney(self.waterAddon.maintenanceCost, 0, true, true)) .. "."
    local mainText = string.format(self.texts.purchase, buildCost, self.animalTypeTitle)
    local defualtText = string.format("%s\n\n%s %s\n%s", mainText, g_i18n:getText("shop_maintenance"), maintenanceCost, waterCost)

    if hasPermission then
        g_gui:showYesNoDialog({
            text = defualtText,
            title = self.brand,
            callback = self.onClickYes,
            target = self
        })
    else
        self:showNoPermissionDialog(string.format("%s\n\n%s\n%s", defualtText, self.texts.requiredPermissions, g_i18n:getText("ui_permissions_buyPlaceable")))
    end
end

function AnimalPenExtension:onClickYes(yes)
    if yes then
        if self.isServer then
            self:setWaterAddonBuildHour(AnimalPenExtension.BUILD_START)
            self:showBuyInfo()
        else
            g_client:getServerConnection():sendEvent(AnimalPenExtensionBuildHourEvent:new(self, AnimalPenExtension.BUILD_START))
            self:showBuyInfo()
        end
    end
end

function AnimalPenExtension:showBuyInfo()
    local text, dialogType = "", DialogElement.TYPE_LOADING

    if self.waterAddon.buildHour < AnimalPenExtension.BUILD_STARTED then
        text = string.format(self.texts.bought, self.brand, self.waterAddon.buildTotalHours)
    else
        if self:getIsSalesOpen() then
            text = string.format(self.texts.startedBuild, self.brand, (self.waterAddon.buildTotalHours - self.waterAddon.buildHour))
        else
            dialogType = DialogElement.TYPE_INFO
            text = string.format(self.texts.startedBuildNoPlumber, (self.waterAddon.buildTotalHours - self.waterAddon.buildHour))
        end
    end

    g_gui:showInfoDialog({
        text = text,
        dialogType = dialogType
    })
end

function AnimalPenExtension:showNoPermissionDialog(text)
    if text == nil then
        text = string.format("%s\n%s", self.texts.requiredPermissions, g_i18n:getText("ui_permissions_buyPlaceable"))
    end

    g_gui:showInfoDialog({
        text = text,
        okText = g_i18n:getText("button_back")
    })
end

function AnimalPenExtension:getCurrentFarmId()
    local farmId = self:getOwnerFarmId()

    if farmId ~= nil then
        return farmId, farmId > FarmManager.SPECTATOR_FARM_ID and farmId <= FarmManager.MAX_FARM_ID
    end

    return 0, false
end

function AnimalPenExtension:setPartsColour(parts, defaultColourIndex)
    if g_animalPenExtensionManager ~= nil and self.waterAddon ~= nil then
        defaultColourIndex = Utils.getNoNil(defaultColourIndex, self.waterAddon.defaultColourIndex)
        parts = parts or self.waterAddon.parts

        return g_animalPenExtensionManager:setPartsMaterial(parts, defaultColourIndex)
    end

    return 0, "Blue"
end

function AnimalPenExtension:setUniversalParts(parts, isWaterAddon, isPartsTable, defaultColourIndex)
    local partsSet = false

    if self.hasUniversalParts then
        if isWaterAddon then
            if not self.universalWaterAddonSet and self.waterAddon ~= nil and self.waterAddon.parts ~= nil then
                for i, part in ipairs (parts) do
                    local waterAddonPart = self.waterAddon.parts[i]

                    if waterAddonPart ~= nil and part.ry ~= nil then
                        local rx, _, rz = getRotation(waterAddonPart.node)

                        AnimalPenExtension.setUniversalPart(waterAddonPart.node, part.x, part.y, part.z, rx, part.ry, rz, part.sx, part.sy, part.sz)

                        if waterAddonPart.typeName == "VALVE" then
                            AnimalPenExtension.setUniversalPart(self.waterAddon.triggerId, part.x, part.y, part.z, rx, part.ry, rz)
                        end

                        partsSet = true
                    end
                end

                if defaultColourIndex >= 0 then
                    self.waterAddon.defaultColourIndex = self:setPartsColour(nil, defaultColourIndex)
                end

                self.universalWaterAddonSet = partsSet
            end
        else
            if not self.universalMilkAddonSet and self.milkAddon ~= nil and self.milkAddon.node ~= nil then
                local rx, _, rz = getRotation(self.milkAddon.node)
                local part = parts

                if isPartsTable then
                    part = parts[1]
                end

                if part ~= nil and part.ry ~= nil then
                    AnimalPenExtension.setUniversalPart(self.milkAddon.node, part.x, part.y, part.z, rx, part.ry, rz, part.sx, part.sy, part.sz)
                    partsSet = true
                end

                self.universalMilkAddonSet = partsSet
            end
        end
    else
        g_animalPenExtensionManager:logPrint(1, "Failed to set Universal Parts!")
    end

    return partsSet
end

function AnimalPenExtension.setUniversalPart(node, x, y, z, rx, ry, rz, sx, sy, sz)
    if node ~= nil then
        removeFromPhysics(node)

        setTranslation(node, x, y, z)
        setRotation(node, rx, ry, rz)

        if sx ~= nil and sy ~= nil and sz ~= nil then
            setScale(node, sx, sy, sz)
        end

        addToPhysics(node)

        return true
    end

    return false
end

function AnimalPenExtension.getNodePositions(node)
    if node ~= nil then
        local x, y, z = getTranslation(node)
        local rx, ry, rz = getRotation(node)
        local sx, sy, sz = getScale(node)

        return x, y, z, rx, ry, rz, sx, sy, sz
    end

    return 0, 0, 0, 0, 0, 0, 0, 0, 0
end


WaterAddonActivatable = {}
local WaterAddonActivatable_mt = Class(WaterAddonActivatable)

function WaterAddonActivatable:new(extension)
    local self = {};
    setmetatable(self, WaterAddonActivatable_mt)

    self.extension = extension
    self.texts = extension.texts
    self.activateText = "TEXT ERROR"

    return self
end

function WaterAddonActivatable:getIsActivatable()
    if self.extension:getIsWaterAddonOwned() then
        if self.extension:getCanWaterActivate() then
            self:setActivateText(self.extension.waterAddon.isActive, true)

            return g_currentMission.controlPlayer and g_currentMission.controlledVehicle == nil
        end
    else
        if self.extension.hasUniversalParts then
            self.activateText = g_i18n:getText("button_configurate")
        else
            self:setActivateText(self.extension.waterAddon.buildHour > AnimalPenExtension.BUILD_WAITING, false)
        end

        return g_currentMission.controlPlayer and g_currentMission.controlledVehicle == nil
    end

    return false
end

function WaterAddonActivatable:onActivateObject()
    if self.extension:getIsWaterAddonOwned() then
        self.extension:setFillingState(not self.extension.waterAddon.isActive)
    else
        if self.extension.hasUniversalParts then
            if g_animalPenExtensionManager ~= nil and g_animalPenExtensionManager.universalPlacement ~= nil then
                if g_currentMission:getHasPlayerPermission("buyPlaceable") then
                    local defaultColourIndex = self.extension.waterAddon.defaultColourIndex or 0
                    self.extension.universalPlacementActive = g_animalPenExtensionManager.universalPlacement:activate(self.extension, self.extension.waterAddon.parts, 1, true, defaultColourIndex)

                    if self.extension.universalPlacementActive then
                        g_currentMission:removeActivatableObject(self)
                        self.extension.waterAddon.playerInRange = nil
                    end
                else
                    self.extension:showNoPermissionDialog()
                end
            end
        else
            if self.extension.waterAddon.buildHour > AnimalPenExtension.BUILD_WAITING then
                self.extension:showBuyInfo()
            else
                self.extension:preBuyText(g_currentMission:getHasPlayerPermission("buyPlaceable"))
            end
        end
    end
end

function WaterAddonActivatable:shouldRemoveActivatable()
    return false
end

function WaterAddonActivatable:drawActivate()
    return
end

function WaterAddonActivatable:setActivateText(state, isOwned)
    if isOwned then
        if state then
            self.activateText = self.texts.stopFilling
        else
            self.activateText = self.texts.startFilling
        end
    else
        if state then
            self.activateText = self.texts.checkBuildState
        else
            self.activateText = self.texts.doPurchase
        end
    end
end


MilkAddonActivatable = {}
local MilkAddonActivatable_mt = Class(MilkAddonActivatable)

function MilkAddonActivatable:new(extension)
    local self = {};
    setmetatable(self, MilkAddonActivatable_mt)

    self.extension = extension
    self.activateText = extension.texts.callMilkContractor or "TEXT ERROR"

    return self
end

function MilkAddonActivatable:getIsActivatable()
    if self.extension:getIsMilkAddonOwned() then
        self.activateText = self.extension.texts.callMilkContractor
    else
        self.activateText = g_i18n:getText("button_configurate")
    end

    return g_currentMission.controlPlayer and g_currentMission.controlledVehicle == nil
end

function MilkAddonActivatable:onActivateObject()
    if self.extension:getIsMilkAddonOwned() then
        self.extension:openMilkContractorMenu()
    else
        if g_animalPenExtensionManager ~= nil and g_animalPenExtensionManager.universalPlacement ~= nil then
            if g_currentMission:getHasPlayerPermission("buyPlaceable") then
                self.extension.universalPlacementActive = g_animalPenExtensionManager.universalPlacement:activate(self.extension, {self.extension.milkAddon}, 1, false, 0)

                if self.extension.universalPlacementActive then
                    g_currentMission:removeActivatableObject(self)
                    self.extension.milkAddon.playerInRange = nil
                end
            else
                self.extension:showNoPermissionDialog()
            end
        end
    end
end

function MilkAddonActivatable:shouldRemoveActivatable()
    return false
end

function MilkAddonActivatable:drawActivate()
    return
end
