--
-- CultivatorSowingMachine - Custom script for Lemken Brilliant
--
-- @author  Decker_MMIV
-- @date    2014-March
--

CultivatorSowingMachine = {};

CultivatorSowingMachine.AreaTypes = {
  AREATYPE_CULTIVATOR    = 1,
  AREATYPE_SOWINGMACHINE = 2,
}
CultivatorSowingMachine.LOWERINGSTATE_RAISEBOTH         = 0
CultivatorSowingMachine.LOWERINGSTATE_LOWERCULTIVATOR   = 1
CultivatorSowingMachine.LOWERINGSTATE_LOWERSEEDER       = 2
CultivatorSowingMachine.LOWERINGSTATE_RAISECULTIVATOR   = 3
CultivatorSowingMachine.LOWERINGSTATE_RAISESEEDER       = 4
CultivatorSowingMachine.LOWERINGSTATE_LOWERBOTH         = 5


function CultivatorSowingMachine.prerequisitesPresent(specializations)
    return SpecializationUtil.hasSpecialization(Fillable, specializations)
       and SpecializationUtil.hasSpecialization(Foldable, specializations)
       and SpecializationUtil.hasSpecialization(ArticulatedAxes, specializations)
       and SpecializationUtil.hasSpecialization(AITractor, specializations)
end;

--DEBUG util
function log(funcName, args, msg)
  local txt = funcName.."("
  local delim=""
  for k,v in pairs(args) do
    txt = txt..delim..tostring(v)
    delim=","
  end
  txt=txt..") "..tostring(msg)
  print(txt)
end
--

function CultivatorSowingMachine:load(xmlFile)

    self.setFoldState = SpecializationUtil.callSpecializationsFunction("setFoldState")

    self.setIsTurnedOn = SpecializationUtil.callSpecializationsFunction("setIsTurnedOn");
    self.setSeedFruitType = SpecializationUtil.callSpecializationsFunction("setSeedFruitType");
    self.setSeedIndex = SpecializationUtil.callSpecializationsFunction("setSeedIndex");
    self.groundContactReport = SpecializationUtil.callSpecializationsFunction("groundContactReport");

    self.getAllowFillFromAir = Utils.overwrittenFunction(self.getAllowFillFromAir, CultivatorSowingMachine.getAllowFillFromAir);
    self.getDirectionSnapAngle = Utils.overwrittenFunction(self.getDirectionSnapAngle, CultivatorSowingMachine.getDirectionSnapAngle);
    self.allowFillType = Utils.overwrittenFunction(self.allowFillType, CultivatorSowingMachine.allowFillType);
    self.setFillLevel = Utils.overwrittenFunction(self.setFillLevel, CultivatorSowingMachine.setFillLevel);
    self.getFillLevel = Utils.overwrittenFunction(self.getFillLevel, CultivatorSowingMachine.getFillLevel);
    self.resetFillLevelIfNeeded = Utils.overwrittenFunction(self.resetFillLevelIfNeeded, CultivatorSowingMachine.resetFillLevelIfNeeded);
    
    assert(self.setIsSowingMachineFilling == nil, "CultivatorSowingMachine needs to be the first specialization which implements setIsSowingMachineFilling");
    self.setIsSowingMachineFilling = CultivatorSowingMachine.setIsSowingMachineFilling;
    self.addSowingMachineFillTrigger = CultivatorSowingMachine.addSowingMachineFillTrigger;
    self.removeSowingMachineFillTrigger = CultivatorSowingMachine.removeSowingMachineFillTrigger;

    self.fillLitersPerSecond = Utils.getNoNil(getXMLFloat(xmlFile, "vehicle.fillLitersPerSecond"), 500);
    self.isSowingMachineFilling = false;
    self.sowingMachineFillTriggers = {};
    self.sowingMachineFillActivatable = SowingMachineFillActivatable:new(self);

    self.canStartAITractor = Utils.overwrittenFunction(self.canStartAITractor, CultivatorSowingMachine.canStartAITractor);
    
    --
    self.speedRotatingParts = {};
    local i=0;
    while true do
        local baseName = string.format("vehicle.speedRotatingParts.speedRotatingPart(%d)", i);
        local index = getXMLString(xmlFile, baseName.. "#index");
        if index == nil then
            break;
        end;
        local node = Utils.indexToObject(self.components, index);
        if node ~= nil then
            local entry = {};
            entry.node = node;
            
            local areaTypeName = getXMLString(xmlFile, baseName.."#areaType")
            if areaTypeName ~= nil then
              entry.areaType = CultivatorSowingMachine.AreaTypes["AREATYPE_"..string.upper(areaTypeName)]
            end
          
            entry.rotationSpeedScale = getXMLFloat(xmlFile, baseName.."#rotationSpeedScale");
            if entry.rotationSpeedScale == nil then
                entry.rotationSpeedScale = 1.0/Utils.getNoNil(getXMLFloat(xmlFile, baseName.."#radius"), 1);
            end;
            entry.rotateOnGroundContact = Utils.getNoNil(getXMLBool(xmlFile, baseName.."#rotateOnGroundContact"), false);
            entry.foldMinLimit = Utils.getNoNil(getXMLFloat(xmlFile, baseName .. "#foldMinLimit"), 0);
            entry.foldMaxLimit = Utils.getNoNil(getXMLFloat(xmlFile, baseName .. "#foldMaxLimit"), 1);
            table.insert(self.speedRotatingParts, entry);
        end;
        i = i+1;
    end;
    --
    self.turnedOnRotationNodes = {};
    local i = 0;
    while true do
        local key = string.format("vehicle.turnedOnRotationNodes.turnedOnRotationNode(%d)", i);
        if not hasXMLProperty(xmlFile, key) then
            break;
        end;
        local node = Utils.indexToObject(self.components, getXMLString(xmlFile, key.."#index"));
        local rotSpeed = math.rad(Utils.getNoNil(getXMLFloat(xmlFile, key.."#rotSpeed"), 1)*0.001);
        local rotAxis = Utils.getNoNil(getXMLInt(xmlFile, key.."#rotAxis"), 2);
        if node ~= nil then
            table.insert(self.turnedOnRotationNodes, {node=node, rotSpeed=rotSpeed, rotAxis=rotAxis});
        end;
        i = i + 1;
    end;
    --
    self.turnedOnScrollers = {};
    local i = 0;
    while true do
        local key = string.format("vehicle.turnedOnScrollers.turnedOnScroller(%d)", i);
        if not hasXMLProperty(xmlFile, key) then
            break;
        end;
        local node = Utils.indexToObject(self.components, getXMLString(xmlFile, key.."#index"));
        local shaderParameterName = getXMLString(xmlFile, key.."#shaderParameterName");
        if node ~= nil and shaderParameterName ~= nil then
            local scrollSpeed = Utils.getNoNil(getXMLFloat(xmlFile, key.."#scrollSpeed"), 1)*0.001;
            local shaderParameterComponent = Utils.getNoNil(getXMLInt(xmlFile, key.."#shaderParameterComponent"), 1);
            local scrollLength = Utils.getNoNil(getXMLFloat(xmlFile, key.."#scrollLength"), 1);
            table.insert(self.turnedOnScrollers, {node=node, scrollSpeed=scrollSpeed, scrollPosition=0, scrollLength=scrollLength, shaderParameterName=shaderParameterName, shaderParameterComponent=shaderParameterComponent});
        end;
        i = i + 1;
    end;
    --
    self.turnOnAnimation = getXMLString(xmlFile, "vehicle.turnOnAnimation#name");
    self.turnOnAnimationSpeed = Utils.getNoNil(getXMLFloat(xmlFile, "vehicle.turnOnAnimation#speed"), 1);
    self.allowFillFromAirWhileTurnedOn = Utils.getNoNil(getXMLBool(xmlFile, "vehicle.allowFillFromAirWhileTurnedOn#value"), true);
    self.sowingDirectionNode = Utils.getNoNil(Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.sowingDirectionNode#index")), self.components[1].node);
    self.useDirectPlanting   = Utils.getNoNil(getXMLBool(xmlFile, "vehicle.useDirectPlanting"), false);
    self.aiUseDirectPlanting = Utils.getNoNil(getXMLBool(xmlFile, "vehicle.aiUseDirectPlanting"), true);
    --
    if self.isClient then
        -- load sowing sound
        self.sowingSoundEnabled = false;
        local sowingSound = getXMLString(xmlFile, "vehicle.sowingSound#file");
        if sowingSound ~= nil and sowingSound ~= "" then
            sowingSound = Utils.getFilename(sowingSound, self.baseDirectory);
            self.sowingSound = createSample("sowingSound");
            loadSample(self.sowingSound, sowingSound, false);
            self.sowingSoundPitchOffset = Utils.getNoNil(getXMLFloat(xmlFile, "vehicle.sowingSound#pitchOffset"), 0);
            self.sowingSoundVolume = Utils.getNoNil(getXMLFloat(xmlFile, "vehicle.sowingSound#volume"), 1.0);
            setSamplePitch(self.sowingSound, self.sowingSoundPitchOffset);
        end;

        -- load air blower sound
        self.airBlowerSoundEnabled = false;
        local airBlowerSound = getXMLString(xmlFile, "vehicle.airBlowerSound#file");
        if airBlowerSound ~= nil and airBlowerSound ~= "" then
            airBlowerSound = Utils.getFilename(airBlowerSound, self.baseDirectory);
            self.airBlowerSound = createSample("airBlowerSound");
            loadSample(self.airBlowerSound, airBlowerSound, false);
            self.airBlowerSoundPitchOffset = Utils.getNoNil(getXMLFloat(xmlFile, "vehicle.airBlowerSound#pitchOffset"), 1);
            self.airBlowerSoundVolume = Utils.getNoNil(getXMLFloat(xmlFile, "vehicle.airBlowerSound#volume"), 1.0);
            setSamplePitch(self.airBlowerSound, self.airBlowerSoundPitchOffset);
        end;

        local changeSeedInputButtonStr = getXMLString(xmlFile, "vehicle.changeSeedInputButton");
        if changeSeedInputButtonStr ~= nil then
            self.changeSeedInputButton = InputBinding[changeSeedInputButtonStr];
        end;
        self.changeSeedInputButton = Utils.getNoNil(self.changeSeedInputButton, InputBinding.IMPLEMENT_EXTRA3);
    end;
    --
    if self.isClient then
      local cultivatorSound = getXMLString(xmlFile, "vehicle.cultivatorSound#file");
      self.cultivatorSoundEnabled = false;
      if cultivatorSound ~= nil and cultivatorSound ~= "" then
          cultivatorSound = Utils.getFilename(cultivatorSound, self.baseDirectory);
          self.cultivatorSound = createSample("cultivatorSound");
          loadSample(self.cultivatorSound, cultivatorSound, false);
          self.cultivatorSoundPitchOffset = Utils.getNoNil(getXMLFloat(xmlFile, "vehicle.cultivatorSound#pitchOffset"), 0);
          self.cultivatorSoundVolume = Utils.getNoNil(getXMLFloat(xmlFile, "vehicle.cultivatorSound#volume"), 1.0);
          setSamplePitch(self.cultivatorSound, self.cultivatorSoundPitchOffset);
      end
    end
    self.cultivatorDirectionNode = Utils.getNoNil(Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.cultivatorDirectionNode#index")), self.components[1].node);
    --
    local numCuttingAreas = Utils.getNoNil(getXMLInt(xmlFile, "vehicle.cuttingAreas#count"), 0);
    for i=1, numCuttingAreas do
        local areanamei = string.format("vehicle.cuttingAreas.cuttingArea%d", i);
        local areaTypeName = getXMLString(xmlFile, areanamei.."#areaType")
        if areaTypeName ~= nil then
          self.cuttingAreas[i].areaType = CultivatorSowingMachine.AreaTypes["AREATYPE_"..string.upper(areaTypeName)]
--print("cuttingArea#"..i..":"..tostring(CultivatorSowingMachine.AreaTypes["AREATYPE_"..string.upper(areaTypeName)]).." ("..tostring(areaTypeName)..")")
        end
    end;
    --
    self.csmContactReportsActive = false;
    self.contactReportNode = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.contactReportNode#index"));
    if self.contactReportNode == nil then
        self.contactReportNode = self.components[1].node;
    end;
    self.contactReportNodes = {};
    local contactReportNodeFound = false;
    local i=0;
    while true do
        local baseName = string.format("vehicle.contactReportNodes.contactReportNode(%d)", i);
        local index = getXMLString(xmlFile, baseName.. "#index");
        if index == nil then
            break;
        end;
        local node = Utils.indexToObject(self.components, index);
        if node ~= nil then
            local entry = {};
            entry.node = node;
            entry.hasGroundContact = false;
            entry.areaType = self.cuttingAreas[i+1].areaType;
--print("contactReportNode#"..i..":"..tostring(self.cuttingAreas[i+1].areaType))
            self.contactReportNodes[node] = entry;
            contactReportNodeFound = true;
        end;
        i = i+1;
    end;
    if not contactReportNodeFound then
        local entry = {};
        entry.node = self.components[1].node;
        entry.hasGroundContact = false;
        self.contactReportNodes[entry.node] = entry;
    end;
    --
    self.groundReference = {}
    self.groundReference.cultivator = {
      threshold = Utils.getNoNil(                       getXMLFloat(xmlFile, "vehicle.groundReferenceCultivator#threshold"), 0.2)
     ,node      = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.groundReferenceCultivator#index"))
    }
    self.groundReference.sowingMachine = {
      threshold = Utils.getNoNil(                       getXMLFloat(xmlFile, "vehicle.groundReferenceSowingMachine#threshold"), 0.2)
     ,node      = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.groundReferenceSowingMachine#index"))
    }
    
    --
    self.onlyActiveWhenLowered = Utils.getNoNil(getXMLBool(xmlFile, "vehicle.onlyActiveWhenLowered#value"), true);
    --
    self.seeds = {};
    local seedFruitTypes = getXMLString(xmlFile, "vehicle.seedFruitTypes#seedFruitTypes");
    if seedFruitTypes ~= nil and seedFruitTypes ~= "" then
        local types = Utils.splitString(" ", seedFruitTypes);
        for _,v in pairs(types) do
            local fruitTypeDesc = FruitUtil.fruitTypes[v];
            if fruitTypeDesc ~= nil and fruitTypeDesc.allowsSeeding then
                table.insert(self.seeds, fruitTypeDesc.index);
            else
                print("Warning: '"..self.configFileName.. "' has invalid seedFruitType '"..v.."'.");
            end;
        end;
    else
        local useSeedingWidth = Utils.getNoNil(getXMLBool(xmlFile, "vehicle.seedFruitTypes#useSeedingWidth"), false);
        for k, fruitTypeDesc in pairs(FruitUtil.fruitTypes) do
            if fruitTypeDesc.allowsSeeding and useSeedingWidth == fruitTypeDesc.useSeedingWidth then
                table.insert(self.seeds, fruitTypeDesc.index);
            end;
        end;
    end;
    --
    self.groundParticleSystems = {};
    local entry = {};
    entry.ps = {};
    Utils.loadParticleSystem(xmlFile, entry.ps, "vehicle.groundParticleSystem", self.components, false, nil, self.baseDirectory);
    if table.getn(entry.ps) > 0 then
        entry.isActive = false;
        table.insert(self.groundParticleSystems, entry);
    end
    local i=0;
    while true do
        local baseName = string.format("vehicle.groundParticleSystems.groundParticleSystem(%d)", i);
        if not hasXMLProperty(xmlFile, baseName) then
            break;
        end;
        local entry = {};
        entry.ps = {};
        Utils.loadParticleSystem(xmlFile, entry.ps, baseName, self.components, false, nil, self.baseDirectory);
        if table.getn(entry.ps) > 0 then
            entry.isActive = false;
            entry.cuttingArea = i+1;
            table.insert(self.groundParticleSystems, entry);
        end
        i = i+1;
    end;
    --
    self.isTurnedOn = false;
    self.needsActivation = Utils.getNoNil(getXMLBool(xmlFile, "vehicle.needsActivation#value"), false);
    
    local turnOnOffInputButtonStr = getXMLString(xmlFile, "vehicle.turnOnOffInputButton");
    if turnOnOffInputButtonStr ~= nil then
        self.turnOnOffInputButton = InputBinding[turnOnOffInputButtonStr];
    end;
    self.turnOnOffInputButton = Utils.getNoNil(self.turnOnOffInputButton, InputBinding.IMPLEMENT_EXTRA);
    --
    self.aiTerrainDetailChannel1 = g_currentMission.ploughChannel;
    self.aiTerrainDetailChannel2 = g_currentMission.sowingChannel;
    self.aiTerrainDetailChannel3 = g_currentMission.sowingWidthChannel;

    self.maxSpeedLevel = math.floor(Utils.clamp(Utils.getNoNil(getXMLInt(xmlFile, "vehicle.maxSpeedLevel#value"), 1), 1,3));
    self.maxSpeedLevelSettings = {
        [1] = {maxKMH=20,  buttonName=InputBinding.SPEED_LEVEL1},
        [2] = {maxKMH=30,  buttonName=InputBinding.SPEED_LEVEL2},
        [3] = {maxKMH=100, buttonName=InputBinding.SPEED_LEVEL3},
    };


    self.speedViolationMaxTime = 2500;
    self.speedViolationTimer = self.speedViolationMaxTime;
    self.startActivationTimeout = 2000;
    self.startActivationTime = 0;

    self.cultivatorHasGroundContact = false;

    self.cultivatorLimitToField = false;
    self.cultivatorForceLimitToField = true;

    self.cultivatorGroundContactFlag = self:getNextDirtyFlag();
    --
    self.fillTypes[Fillable.FILLTYPE_SEEDS] = true;

    --self.lastSowingArea = 0;

    self.currentSeed = 1;
    self.allowsSeedChanging = true;

    self.sowingMachineGroundContactFlag = self:getNextDirtyFlag();
    --
    self.showFieldNotOwnedWarning = false;

    --
    self.loweringState = -1; --CultivatorSowingMachine.LOWERINGSTATE_RAISEBOTH;
    self.loweringStates = {
      [CultivatorSowingMachine.LOWERINGSTATE_RAISEBOTH      ] = { actions = {[1]=-1,[2]=-1}  ,helpText = g_i18n:getText("raiseBoth"      ) },
      [CultivatorSowingMachine.LOWERINGSTATE_LOWERCULTIVATOR] = { actions = {[1]=1}          ,helpText = g_i18n:getText("lowerCultivator") },
      [CultivatorSowingMachine.LOWERINGSTATE_LOWERSEEDER    ] = { actions = {[2]=1}          ,helpText = g_i18n:getText("lowerSeeder"    ) },
      [CultivatorSowingMachine.LOWERINGSTATE_RAISECULTIVATOR] = { actions = {[1]=-1}         ,helpText = g_i18n:getText("raiseCultivator") },
      [CultivatorSowingMachine.LOWERINGSTATE_RAISESEEDER    ] = { actions = {[2]=-1}         ,helpText = g_i18n:getText("raiseSeeder"    ) },
      --
      [CultivatorSowingMachine.LOWERINGSTATE_LOWERBOTH      ] = { actions = {[1]=1,[2]=1}    ,helpText = "LowerBoth" }, -- only for AI lowering
    }

    local nextLoweringStateInputButtonStr = getXMLString(xmlFile, "vehicle.nextLoweringStateInputButton");
    if nextLoweringStateInputButtonStr ~= nil then
        self.nextLoweringStateInputButton = InputBinding[nextLoweringStateInputButtonStr];
    end;
    self.nextLoweringStateInputButton = Utils.getNoNil(self.nextLoweringStateInputButton, InputBinding.LOWER_IMPLEMENT);
    
    local raiseBothInputButtonStr = getXMLString(xmlFile, "vehicle.raiseBothInputButton");
    if raiseBothInputButtonStr ~= nil then
        self.raiseBothInputButton = InputBinding[raiseBothInputButtonStr];
    end;
    --self.raiseBothInputButton = Utils.getNoNil(self.raiseBothInputButton, InputBinding.IMPLEMENT_EXTRA4);
    
    self.getIsLoweringStateAllowed  = CultivatorSowingMachine.getIsLoweringStateAllowed;
    self.setLowerState              = SpecializationUtil.callSpecializationsFunction("setLowerState");
    self.applyInitialAnimation      = Utils.overwrittenFunction(self.applyInitialAnimation, CultivatorSowingMachine.applyInitialAnimation);

    self.loweringSets = {};
    local j=0;
    while true do
        local setName = string.format("vehicle.loweringSets.loweringSet(%d)", j);
        if not hasXMLProperty(xmlFile, setName) then
            break;
        end
        local loweringSet = {}
        loweringSet.startLowerAnimTime = getXMLFloat(xmlFile, setName.."#startLowerAnimTime");
        loweringSet.lowerMoveDirection = 0;
        if loweringSet.startLowerAnimTime == nil then
            loweringSet.startLowerAnimTime = 0;
            local startMoveDirection = Utils.getNoNil(getXMLInt(xmlFile, setName.."#startMoveDirection"), 0);
            if startMoveDirection > 0.1 then
                loweringSet.startLowerAnimTime = 1;
            end
        end
        --loweringSet.turnOnLowerDirection = 1;
        --if loweringSet.startLowerAnimTime > 0.5 then
        --    loweringSet.turnOnLowerDirection = -1;
        --end
        --loweringSet.turnOnLowerDirection = Utils.sign(Utils.getNoNil(getXMLInt(xmlFile, setName.."#turnOnLowerDirection"), loweringSet.turnOnLowerDirection));
        loweringSet.lowerAnimTime = 0;
        loweringSet.maxLowerAnimDuration = 0.0001

        loweringSet.loweringParts = {};
        local i=0;
        while true do
            local baseName = string.format(setName..".loweringPart(%d)", i);
            if not hasXMLProperty(xmlFile, baseName) then
                break;
            end
            local isValid = false;
            local entry = {};
            entry.speedScale = Utils.getNoNil(getXMLFloat(xmlFile, baseName.."#speedScale"), 1);
            local componentJointIndex = getXMLInt(xmlFile, baseName.. "#componentJointIndex");
            local componentJoint = nil;
            if componentJointIndex ~= nil then
                componentJoint = self.componentJoints[componentJointIndex+1];
                entry.componentJoint = componentJoint;
            end
            entry.anchorActor = Utils.getNoNil(getXMLInt(xmlFile,  baseName.."#anchorActor"), 0);
            entry.animCharSet = 0;
            local rootNode = Utils.indexToObject(self.components, getXMLString(xmlFile, baseName.."#rootNode"));
            if rootNode ~= nil then
                local animCharSet = getAnimCharacterSet(rootNode);
                if animCharSet ~= 0 then
                    local clip = getAnimClipIndex(animCharSet, getXMLString(xmlFile, baseName.."#animationClip"));
                    if clip >= 0 then
                        isValid = true;
                        entry.animCharSet = animCharSet;
                        assignAnimTrackClip(entry.animCharSet, 0, clip);
                        setAnimTrackLoopState(entry.animCharSet, 0, false);
                        entry.animDuration = getAnimClipDuration(entry.animCharSet, clip);
                    end
                end
            end
            -- try AnimatedVehicle specialization support
            if not isValid and self.playAnimation ~= nil and self.animations ~= nil then
                local animationName = getXMLString(xmlFile, baseName.."#animationName");
                if animationName ~= nil then
                    if self.animations[animationName] ~= nil then
                        isValid = true;
                        entry.animDuration = self:getAnimationDuration(animationName);
                        entry.animationName = animationName;
                    end
                end
            end
            if isValid then
                loweringSet.maxLowerAnimDuration = math.max(loweringSet.maxLowerAnimDuration, entry.animDuration);
                if componentJoint ~= nil then
                    local node = self.components[componentJoint.componentIndices[((entry.anchorActor+1)%2)+1] ].node;
                    entry.x,entry.y,entry.z = worldToLocal(componentJoint.jointNode, getWorldTranslation(node));
                    entry.upX,entry.upY,entry.upZ = worldDirectionToLocal(componentJoint.jointNode, localDirectionToWorld(node, 0, 1, 0));
                    entry.dirX,entry.dirY,entry.dirZ = worldDirectionToLocal(componentJoint.jointNode, localDirectionToWorld(node, 0, 0, 1));
                end
                table.insert(loweringSet.loweringParts, entry);
            end
            i = i + 1;
        end
        --
        table.insert(self.loweringSets, loweringSet);
        j = j + 1
    end


    --
    
    self.aiTurnOn  = SpecializationUtil.callSpecializationsFunction("aiTurnOn");
    self.aiTurnOff = SpecializationUtil.callSpecializationsFunction("aiTurnOff");
    self.aiLower = SpecializationUtil.callSpecializationsFunction("aiLower");
    self.aiRaise = SpecializationUtil.callSpecializationsFunction("aiRaise");

    self.aiRaiseSeederDelayTime = Utils.getNoNil(getXMLFloat(xmlFile, "vehicle.aiRaiseSeederDelayTime#value"), 2000);
    self.aiStopCultivatorWhenFieldEnds = Utils.getNoNil(getXMLBool(xmlFile, "vehicle.aiStopCultivatorWhenFieldEnds#value"), true);
    
    self.aiLeftMarker = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.aiLeftMarker#index"));
    self.aiRightMarker = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.aiRightMarker#index"));
    self.aiBackMarker = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.aiBackMarker#index"));
    self.aiNeedsLowering = Utils.getNoNil(getXMLBool(xmlFile, "vehicle.aiNeedsLowering#value"), self.needsLowering);
    self.aiForceTurnNoBackward = Utils.getNoNil(getXMLBool(xmlFile, "vehicle.aiForceTurnNoBackward#value"), false);
    --self.aiTrafficCollisionTrigger = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.aiTrafficCollisionTrigger#index"));

    self.aiTerrainDetailChannel1 = g_currentMission.cultivatorChannel;
    self.aiTerrainDetailChannel2 = g_currentMission.ploughChannel;
    self.aiTerrainDetailChannel3 = g_currentMission.sowingChannel;

    self.aiTerrainDetailProhibitedMask = 0;
    self.aiRequiredFruitType = FruitUtil.FRUITTYPE_UNKNOWN;
    self.aiRequiredMinGrowthState = 0;
    self.aiRequiredMaxGrowthState = 0;
    self.aiProhibitedFruitType = FruitUtil.FRUITTYPE_UNKNOWN;
    self.aiProhibitedMinGrowthState = 0;
    self.aiProhibitedMaxGrowthState = 0;
    
    self.firstTimeTickRun = false;
end;

function CultivatorSowingMachine:applyInitialAnimation(superFunc)
    if superFunc ~= nil then
        superFunc(self);
    end

    self:setLowerState(CultivatorSowingMachine.LOWERINGSTATE_RAISEBOTH, true);
end

function CultivatorSowingMachine:delete()

    for _, entry in ipairs(self.groundParticleSystems) do
        Utils.deleteParticleSystem(entry.ps);
        entry.isActive = false;
    end;

    CultivatorSowingMachine.removeContactReports(self);

    if self.cultivatorSound ~= nil then
        delete(self.cultivatorSound);
        self.cultivatorSoundEnabled = false;
    end;

    if self.sowingSound ~= nil then
        delete(self.sowingSound);
        self.sowingSound = nil;
    end;
    if self.airBlowerSound ~= nil then
        delete(self.airBlowerSound);
        self.airBlowerSound = nil;
    end;

end;

function CultivatorSowingMachine:readStream(streamId, connection)
    local seedIndex = streamReadUInt8(streamId);
    local turnedOn = streamReadBool(streamId);
    local isSowingMachineFilling = streamReadBool(streamId);
    --
    --local tempSets = {}
    --for loweringSetIdx=table.getn(self.loweringSets),1,-1 do
    --    local direction = streamReadUIntN(streamId, 2)-1;
    --    local lowerAnimTime = streamReadFloat32(streamId);
    --    tempSets[loweringSetIdx] = {direction=direction, lowerAnimTime=lowerAnimTime}
    --end
    --
    self:setSeedIndex(seedIndex, true);
    self:setIsTurnedOn(turnedOn, true);
    self:setIsSowingMachineFilling(isSowingMachineFilling, true);
    --
    --for loweringSetIdx=table.getn(self.loweringSets),1,-1 do
    --    CultivatorSowingMachine.setLowerAnimTimeEx(self, loweringSetIdx, tempSets[loweringSetIdx].lowerAnimTime)
    --    self:setLowerStateEx(loweringSetIdx, tempSets[loweringSetIdx].direction, true)
    --end
end;

function CultivatorSowingMachine:writeStream(streamId, connection)
    streamWriteUInt8(streamId, self.currentSeed);
    streamWriteBool(streamId, self.isTurnedOn);
    streamWriteBool(streamId, self.isSowingMachineFilling);
    --
    --for loweringSetIdx=table.getn(self.loweringSets),1,-1 do
    --    local loweringSet = self.loweringSets[loweringSetIdx];
    --    local direction = Utils.sign(loweringSet.lowerMoveDirection)+1;
    --    streamWriteUIntN(streamId, direction, 2);
    --    streamWriteFloat32(streamId, loweringSet.lowerAnimTime);
    --end
end;

function CultivatorSowingMachine:readUpdateStream(streamId, timestamp, connection)
    if connection:getIsServer() then
        self.cultivatorHasGroundContact = streamReadBool(streamId);
        self.sowingMachineHasGroundContact = streamReadBool(streamId);
        self.showFieldNotOwnedWarning = streamReadBool(streamId);
    end;
end;

function CultivatorSowingMachine:writeUpdateStream(streamId, connection, dirtyMask)
    if not connection:getIsServer() then
        streamWriteBool(streamId, self.cultivatorHasGroundContact);
        streamWriteBool(streamId, self.sowingMachineHasGroundContact);
        streamWriteBool(streamId, self.showFieldNotOwnedWarning);
    end;
end;

function CultivatorSowingMachine:loadFromAttributesAndNodes(xmlFile, key, resetVehicles)
    local selectedSeedFruitType = getXMLString(xmlFile, key.."#selectedSeedFruitType");
    if selectedSeedFruitType ~= nil then
        local fruitTypeDesc = FruitUtil.fruitTypes[selectedSeedFruitType];
        if fruitTypeDesc ~= nil then
            self:setSeedFruitType(fruitTypeDesc.index, true);
        end;
    end;

    --for loweringSetIdx=table.getn(self.loweringSets),1,-1 do
    --    local loweringSet = self.loweringSets[loweringSetIdx];
    --    local animTime = Utils.getNoNil(getXMLFloat(xmlFile, key..string.format("#lowerAnimTime%d",loweringSetIdx)), loweringSet.startLowerAnimTime)
    --    CultivatorSowingMachine.setLowerAnimTimeEx(self, loweringSetIdx, animTime)
    --end

    -- Assumes Foldable:loadFromAttributesAndNodes() have been executed already
    self:setRotateMaxMinScale(2, 1 - self.foldAnimTime);

    return BaseMission.VEHICLE_LOAD_OK;
end;

function CultivatorSowingMachine:getSaveAttributesAndNodes(nodeIdent)
    local selectedSeedFruitTypeName = "unknown";
    local selectedSeedFruitType = self.seeds[self.currentSeed];
    if selectedSeedFruitType ~= nil and selectedSeedFruitType ~= FruitUtil.FRUITTYPE_UNKNOWN then
        selectedSeedFruitTypeName = FruitUtil.fruitIndexToDesc[selectedSeedFruitType].name;
    end;
    local attributes = 'selectedSeedFruitType="'..selectedSeedFruitTypeName..'"';

    --for loweringSetIdx=table.getn(self.loweringSets),1,-1 do
    --    local loweringSet = self.loweringSets[loweringSetIdx];
    --    attributes = attributes .. string.format(' lowerAnimTime%d="%f"', loweringSetIdx, loweringSet.lowerAnimTime)
    --end

    return attributes, nil;
end;

function CultivatorSowingMachine:mouseEvent(posX, posY, isDown, isUp, button)
end;

function CultivatorSowingMachine:keyEvent(unicode, sym, modifier, isDown)
end;

function CultivatorSowingMachine:update(dt)
    if self:getIsActive() then
--
        if self:getIsActiveForInput() then
            if InputBinding.hasEvent(self.changeSeedInputButton) then
                if self.allowsSeedChanging then
                    local seed = self.currentSeed + 1;
                    if seed > table.getn(self.seeds) then
                        seed = 1;
                    end;
                    self:setSeedIndex(seed);
                end;
            end;
            if self.needsActivation then
                if InputBinding.hasEvent(self.turnOnOffInputButton) then
                    self:setIsTurnedOn(not self.isTurnedOn);
                end;
            end;
            --
            if InputBinding.hasEvent(self.nextLoweringStateInputButton) then
                -- Next lower/raise state
                local wantedLoweringState = (self.loweringState%4) + 1
                if self:getIsLoweringStateAllowed(wantedLoweringState) then
                    self:setLowerState(wantedLoweringState);
                end
            elseif self.raiseBothInputButton ~= nil and InputBinding.hasEvent(self.raiseBothInputButton) then
                -- Raise both
                self:setLowerState(CultivatorSowingMachine.LOWERINGSTATE_RAISEBOTH);
            end
        end;
        if self.isClient and self.isTurnedOn then
            for _, node in pairs(self.turnedOnRotationNodes) do
                if node.rotAxis == 3 then
                    rotate(node.node, 0, 0, dt*node.rotSpeed);
                elseif node.rotAxis == 1 then
                    rotate(node.node, dt*node.rotSpeed, 0, 0);
                else
                    rotate(node.node, 0, dt*node.rotSpeed, 0);
                end
                if self.setMovingToolDirty ~= nil then
                    self:setMovingToolDirty(node.node);
                end
            end
            for _, scroller in pairs(self.turnedOnScrollers) do
                scroller.scrollPosition = (scroller.scrollPosition + dt*scroller.scrollSpeed) % scroller.scrollLength;
                if scroller.shaderParameterComponent == 1 then
                    setShaderParameter(scroller.node, scroller.shaderParameterName, scroller.scrollPosition,0,0,0, false);
                else
                    setShaderParameter(scroller.node, scroller.shaderParameterName, 0,scroller.scrollPosition,0,0, false);
                end
            end
        end
    end
end;

function CultivatorSowingMachine:updateTick(dt)

    if self.isServer and self.isSowingMachineFilling then
        local delta = 0;
        if self.sowingMachineFillTrigger ~= nil then
            delta = self.fillLitersPerSecond*dt*0.001;
            delta = self.sowingMachineFillTrigger:fillSowingMachine(self, delta);
        end
        if delta <= 0 then
            self:setIsSowingMachineFilling(false);
        end;
    end

--
    if self.isServer then
        -- Delayed lifting of seeder part
        if self.aiRaiseSeederTime ~= nil and self.aiRaiseSeederTime < g_currentMission.time then
            self.aiRaiseSeederTime = nil;
            self:aiRaise();
        end
        
        -- Apparently AITractor only checks its implements for the 'aiForceTurnNoBackwards'.
        -- Since this Lemken has no implements attached, then try to "simulate" one.
        self.aiTurnNoBackward = self.aiTurnNoBackward or self.aiForceTurnNoBackward; 
        
        -- 
        if self.isHired then
            if self.foldAnimTime > 0 then
                -- Still unfolding, disallow driving!
                self.waitForTurnTime = self.time + 1000
            elseif self.aiLoweringState < 2 then
                -- First lane, first start lowering
                if self.aiLoweringState == 0 then
                    self.aiLoweringState = 1
                    self:aiLower()
                elseif self.loweringSets[1].lowerAnimTime < 1 then
                    -- Still lowering, disallow driving!
                    self.waitForTurnTime = self.time + 1000
                else
                    self.aiLoweringState = 2
                end
            end
        end
        
        -- Folding seeder, will restrict the rear-wheels pivoting
        if math.abs(self.foldMoveDirection) > 0.1 then
            self:setRotateMaxMinScale(2, 1 - self.foldAnimTime)
        end
    end
    --
    for loweringSetIdx,loweringSet in pairs(self.loweringSets) do
        local isInvalid = not self.firstTimeTickRun;
        if math.abs(loweringSet.lowerMoveDirection) > 0.1 then
            local lowerAnimTime = 0;
            if loweringSet.lowerMoveDirection < -0.1 then
                lowerAnimTime = 1;
            end
            for _,loweringPart in pairs(loweringSet.loweringParts) do
                if loweringSet.lowerMoveDirection > 0 then
                    local animTime = 0;
                    if loweringPart.animCharSet ~= 0 then
                        animTime = getAnimTrackTime(loweringPart.animCharSet, 0);
                    else
                        animTime = self:getRealAnimationTime(loweringPart.animationName);
                    end
                    if animTime < loweringPart.animDuration then
                        isInvalid = true;
                    end
                    lowerAnimTime = math.max(lowerAnimTime, animTime / loweringSet.maxLowerAnimDuration);
                elseif loweringSet.lowerMoveDirection < 0 then
                    local animTime = 0;
                    if loweringPart.animCharSet ~= 0 then
                        animTime = getAnimTrackTime(loweringPart.animCharSet, 0);
                    else
                        animTime = self:getRealAnimationTime(loweringPart.animationName);
                    end
                    if animTime > 0 then
                        isInvalid = true;
                    end
                    lowerAnimTime = math.min(lowerAnimTime, animTime / loweringSet.maxLowerAnimDuration);
                end
            end
            loweringSet.lowerAnimTime = Utils.clamp(lowerAnimTime, 0, 1);
            
            if not isInvalid then
                loweringSet.lowerMoveDirection = 0
            end
        end

        if isInvalid then
            for _,loweringPart in pairs(loweringSet.loweringParts) do
                if loweringPart.componentJoint ~= nil then
                    setJointFrame(loweringPart.componentJoint.jointIndex, loweringPart.anchorActor, loweringPart.componentJoint.jointNode);
                end
            end
            --
            --if self.isServer and loweringSetIdx == 1 then
            --    -- Cultivator rotors activate/deactivate
            --    if loweringSet.lowerMoveDirection < -0.1 then          
            --        -- When raised
            --        self:setAnimationClip(1,false)
            --    else
            --        -- When lowered
            --        self:setAnimationClip(1,true)
            --    end
            --end
        end
    end
    
--    
    if self:getIsActive() then
  
        local showFieldNotOwnedWarning = false;
        local hasDoneGroundManipulation = false;
        --self.lastSowingArea = 0;

        if self.isServer then
            local hasGroundContact = false;
            for k, v in pairs(self.contactReportNodes) do
                if v.areaType == CultivatorSowingMachine.AreaTypes.AREATYPE_SOWINGMACHINE
                and v.hasGroundContact then
                    hasGroundContact = true;
                    break;
                end;
            end;
            if not hasGroundContact then
                if self.groundReference.sowingMachine ~= nil and self.groundReference.sowingMachine.node ~= nil then
                    local x,y,z = getWorldTranslation(self.groundReference.sowingMachine.node);
                    local terrainHeight = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, x, 0, z);
                    if terrainHeight + self.groundReference.sowingMachine.threshold >= y then
                        hasGroundContact = true;
                    end;
                end
            end
            if self.sowingMachineHasGroundContact ~= hasGroundContact then
                self:raiseDirtyFlags(self.sowingMachineGroundContactFlag);
                self.sowingMachineHasGroundContact = hasGroundContact;
            end;
        end;
        local hasGroundContact = self.sowingMachineHasGroundContact;
        local doGroundManipulation = (self.movingDirection > 0 and hasGroundContact and (not self.needsActivation or self.isTurnedOn));

        hasDoneGroundManipulation = hasDoneGroundManipulation or doGroundManipulation

        doGroundManipulation = doGroundManipulation and self.speedViolationTimer > 0 -- Disallow if speeding!

        --local foldAnimTime = self.foldAnimTime;
        if doGroundManipulation then
            if self.isServer then
                local hasSeeds = (self.fillLevel > 0);
                local useFillLevel = true;
                if self.capacity == 0 or self:getIsHired() then
                    useFillLevel = false;
                    hasSeeds = true;
                end;
                -- Special check, in case HiredWorkerConsumesFuelAndSeeds mod is active too.
                if self.isHired and self.fillLevel <= 0 then
                    self:stopAITractor()
                    hasSeeds = false
                end
                --
                if hasSeeds then
                    local cuttingAreasSend = {};
                    for k, cuttingArea in pairs(self.cuttingAreas) do
                        if cuttingArea.areaType == CultivatorSowingMachine.AreaTypes.AREATYPE_SOWINGMACHINE
                        and self:getIsAreaActive(cuttingArea) then
                            local x,_,z = getWorldTranslation(cuttingArea.start);
                            if g_currentMission:getIsFieldOwnedAtWorldPos(x,z) then
                                local x1,_,z1 = getWorldTranslation(cuttingArea.width);
                                local x2,_,z2 = getWorldTranslation(cuttingArea.height);
                                table.insert(cuttingAreasSend, {x,z,x1,z1,x2,z2});
                            else
                                showFieldNotOwnedWarning = true;
                            end
                        end
                    end

                    if (table.getn(cuttingAreasSend) > 0) then
                        local seedsFruitType = self.seeds[self.currentSeed];
                        local dx,dy,dz = localDirectionToWorld(self.sowingDirectionNode, 0, 0, 1);

                        local angleRad = Utils.getYRotationFromDirection(dx, dz)
                        local desc = FruitUtil.fruitIndexToDesc[seedsFruitType];
                        if desc ~= nil and desc.directionSnapAngle ~= 0 then
                            angleRad = math.floor(angleRad / desc.directionSnapAngle + 0.5) * desc.directionSnapAngle;
                        end

                        local angle = Utils.convertToDensityMapAngle(angleRad, g_currentMission.terrainDetailAngleMaxValue);

                        local useDirectPlanting = (self.useDirectPlanting or (self.aiUseDirectPlanting and self.isHired))
                        local area, detailArea = SowingMachineAreaEvent.runLocally(cuttingAreasSend, seedsFruitType, angle, useDirectPlanting)
                        if area > 0 or detailArea > 0 then
                            if area > 0 then
                                local fruitDesc = FruitUtil.fruitIndexToDesc[seedsFruitType];
                                local pixelToSqm = g_currentMission:getFruitPixelsToSqm();
                                local sqm = area*pixelToSqm;
                                local ha = sqm/10000;

                                --self.lastSowingArea = sqm;

                                local usage = fruitDesc.seedUsagePerSqm*sqm;
                                g_currentMission.missionStats.seedUsageTotal = g_currentMission.missionStats.seedUsageTotal + usage;
                                g_currentMission.missionStats.seedUsageSession = g_currentMission.missionStats.seedUsageSession + usage;
                                g_currentMission.missionStats.hectaresSeededTotal = g_currentMission.missionStats.hectaresSeededTotal + ha;
                                g_currentMission.missionStats.hectaresSeededSession = g_currentMission.missionStats.hectaresSeededSession + ha;

                                if useFillLevel then
                                    self:setFillLevel(self.fillLevel - usage, self.currentFillType);
                                else
                                    local fillTypeDesc = Fillable.fillTypeIndexToDesc[Fillable.FILLTYPE_SEEDS]
                                    if fillTypeDesc ~= nil then
                                        local price = usage*fillTypeDesc.pricePerLiter;
                                        g_currentMission.missionStats.expensesTotal = g_currentMission.missionStats.expensesTotal + price;
                                        g_currentMission.missionStats.expensesSession = g_currentMission.missionStats.expensesSession + price;
                                        g_currentMission:addSharedMoney(-price, "other");
                                    end
                                end
                            end
                            g_server:broadcastEvent(SowingMachineAreaEvent:new(cuttingAreasSend, seedsFruitType, angle, useDirectPlanting));
                        end;
                    end;
                end;
                g_currentMission.missionStats.seedingDurationTotal   = g_currentMission.missionStats.seedingDurationTotal   + dt/(1000*60);
                g_currentMission.missionStats.seedingDurationSession = g_currentMission.missionStats.seedingDurationSession + dt/(1000*60);
            end;
        end

        if self.isClient then
            for _,ps in pairs(self.groundParticleSystems) do
                local enabled = (doGroundManipulation and self.lastSpeed*3600 > 5)
                if enabled and ps.cuttingArea ~= nil and self.cuttingAreas[ps.cuttingArea] ~= nil then
                    enabled = self.cuttingAreas[ps.cuttingArea].areaType == CultivatorSowingMachine.AreaTypes.AREATYPE_SOWINGMACHINE
                              and self:getIsAreaActive(self.cuttingAreas[ps.cuttingArea]);
                end
                if ps.isActive ~= enabled then
                    ps.isActive = enabled;
                    Utils.setEmittingState(ps.ps, ps.isActive);
                end
            end

            if self.sowingSound ~= nil then
                if doGroundManipulation and self.lastSpeed*3600 > 3 then
                    if not self.sowingSoundEnabled  and self:getIsActiveForSound() then
                        playSample(self.sowingSound, 0, self.sowingSoundVolume, 0);
                        self.sowingSoundEnabled = true;
                    end
                else
                    if self.sowingSoundEnabled then
                        self.sowingSoundEnabled = false;
                        stopSample(self.sowingSound);
                    end
                end
            end

            if self.isTurnedOn then
                if self.airBlowerSound ~= nil and not self.airBlowerSoundEnabled and self:getIsActiveForSound() then
                    playSample(self.airBlowerSound, 0, self.airBlowerSoundVolume, 0);
                    self.airBlowerSoundEnabled = true;
                end
            end

            if hasGroundContact then
                local foldAnimTime = self.foldAnimTime;
                for k,v in pairs(self.speedRotatingParts) do
                    if v.areaType == CultivatorSowingMachine.AreaTypes.AREATYPE_SOWINGMACHINE then
                      if foldAnimTime == nil or (foldAnimTime <= v.foldMaxLimit and foldAnimTime >= v.foldMinLimit) then
                          rotate(v.node, v.rotationSpeedScale * self.lastSpeedReal * self.movingDirection * dt, 0, 0);
                      end
                    end
                end
            end
        end

---
---

        if self.isServer then
            local hasGroundContact = false;
            for k, v in pairs(self.contactReportNodes) do
                if v.areaType == CultivatorSowingMachine.AreaTypes.AREATYPE_CULTIVATOR
                and v.hasGroundContact then
                    hasGroundContact = true;
                    break;
                end;
            end;
            if not hasGroundContact then
                if self.groundReference.cultivator ~= nil and self.groundReference.cultivator.node ~= nil then
                    local x,y,z = getWorldTranslation(self.groundReference.cultivator.node);
                    local terrainHeight = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, x, 0, z);
                    if terrainHeight + self.groundReference.cultivator.threshold >= y then
                        hasGroundContact = true;
                    end;
                end;
            end;
            if self.cultivatorHasGroundContact ~= hasGroundContact then
                self:raiseDirtyFlags(self.cultivatorGroundContactFlag);
                self.cultivatorHasGroundContact = hasGroundContact;
            end;
        end;
        local hasGroundContact = self.cultivatorHasGroundContact;
        local doGroundManipulation = (hasGroundContact and (not self.onlyActiveWhenLowered or self:isLowered(false)) and self.startActivationTime <= self.time);

        hasDoneGroundManipulation = hasDoneGroundManipulation or doGroundManipulation
        
        doGroundManipulation = doGroundManipulation and self.speedViolationTimer > 0 -- Disallow if speeding!
        doGroundManipulation = doGroundManipulation and not (self.aiStopCultivatorWhenFieldEnds and self.isAITractorActivated and self.turnTimer < self.turnTimeout) -- for AITractor, do not use cultivator when leaving field.

        --local foldAnimTime = self.foldAnimTime;
        if doGroundManipulation then
            if self.isServer then
                local cuttingAreasSend = {};
                for _, cuttingArea in pairs(self.cuttingAreas) do
                    if cuttingArea.areaType == CultivatorSowingMachine.AreaTypes.AREATYPE_CULTIVATOR
                    and self:getIsAreaActive(cuttingArea) then
                        local x,_,z = getWorldTranslation(cuttingArea.start);
                        if g_currentMission:getIsFieldOwnedAtWorldPos(x,z) then
                            local x1,_,z1 = getWorldTranslation(cuttingArea.width);
                            local x2,_,z2 = getWorldTranslation(cuttingArea.height);
                            table.insert(cuttingAreasSend, {x,z,x1,z1,x2,z2});
                        else
                            showFieldNotOwnedWarning = true;
                        end
                    end;
                end;
                if table.getn(cuttingAreasSend) > 0 then
                    local limitToField = self.cultivatorLimitToField or self.cultivatorForceLimitToField;
                    local limitGrassDestructionToField = false;
                    if not g_currentMission:getHasPermission("createFields", self:getOwner()) then
                        limitToField = true;
                        limitGrassDestructionToField = true;
                    end;

                    local dx,dy,dz = localDirectionToWorld(self.cultivatorDirectionNode, 0, 0, 1);
                    local angle = Utils.convertToDensityMapAngle(Utils.getYRotationFromDirection(dx, dz), g_currentMission.terrainDetailAngleMaxValue);

                    local realArea = CultivatorAreaEvent.runLocally(cuttingAreasSend, limitToField, limitGrassDestructionToField, angle);
                    g_server:broadcastEvent(CultivatorAreaEvent:new(cuttingAreasSend, limitToField, limitGrassDestructionToField, angle));

                    local pixelToSqm = g_currentMission:getFruitPixelsToSqm(); -- 4096px are mapped to 2048m
                    local sqm = realArea*pixelToSqm;
                    local ha = sqm/10000;
                    g_currentMission.missionStats.hectaresWorkedTotal = g_currentMission.missionStats.hectaresWorkedTotal + ha;
                    g_currentMission.missionStats.hectaresWorkedSession = g_currentMission.missionStats.hectaresWorkedSession + ha;
                end;
            end;
        end

        if self.isClient then
            for _,ps in pairs(self.groundParticleSystems) do
                local enabled = (doGroundManipulation and self.lastSpeed*3600 > 5)
                if enabled and ps.cuttingArea ~= nil and self.cuttingAreas[ps.cuttingArea] ~= nil then
                    enabled = self.cuttingAreas[ps.cuttingArea].areaType == CultivatorSowingMachine.AreaTypes.AREATYPE_CULTIVATOR
                              and self:getIsAreaActive(self.cuttingAreas[ps.cuttingArea]);
                end
                if ps.isActive ~= enabled then
                    ps.isActive = enabled;
                    Utils.setEmittingState(ps.ps, ps.isActive);
                end
            end

            if self.cultivatorSound ~= nil then
                if doGroundManipulation and self.lastSpeed*3600 > 3 then
                    if not self.cultivatorSoundEnabled and self:getIsActiveForSound() then
                        playSample(self.cultivatorSound, 0, self.cultivatorSoundVolume, 0);
                        self.cultivatorSoundEnabled = true;
                    end;
                else
                    if self.cultivatorSoundEnabled then
                        stopSample(self.cultivatorSound);
                        self.cultivatorSoundEnabled = false;
                    end;
                end;
            end;

            if hasGroundContact then
                local foldAnimTime = self.foldAnimTime;
                for k,v in pairs(self.speedRotatingParts) do
                    if v.areaType == CultivatorSowingMachine.AreaTypes.AREATYPE_CULTIVATOR then
                        if doGroundManipulation or v.rotateOnGroundContact then
                            if foldAnimTime == nil or (foldAnimTime <= v.foldMaxLimit and foldAnimTime >= v.foldMinLimit) then
                                rotate(v.node, v.rotationSpeedScale * self.lastSpeedReal * self.movingDirection * dt, 0, 0);
                            end;
                        end;
                    end;
                end;
            end;
        end;

--

        local speedLimit = self.maxSpeedLevelSettings[self.maxSpeedLevel].maxKMH
        if hasDoneGroundManipulation and self:doCheckSpeedLimit() and self.lastSpeed*3600 > speedLimit then
            self.speedViolationTimer = self.speedViolationTimer - dt;
            if self.isServer then
                if self.speedViolationTimer < 0 then
                    if self.attacherVehicle ~= nil then
                        self.attacherVehicle:detachImplementByObject(self);
                    end;
                end;
            end;
        else
            self.speedViolationTimer = self.speedViolationMaxTime;
        end;

        if self.isServer then
            if showFieldNotOwnedWarning ~= self.showFieldNotOwnedWarning then
                self.showFieldNotOwnedWarning = showFieldNotOwnedWarning;
                self:raiseDirtyFlags(self.cultivatorGroundContactFlag);
            end
        end
    end;
    
    self.firstTimeTickRun = true;
end;

function CultivatorSowingMachine:draw()
    if self.isClient then
        if self:getIsActiveForInput(true) then
            if self.fillLevel <= 0 and self.capacity ~= 0 then
                g_currentMission:addExtraPrintText(g_i18n:getText("FirstFillTheTool"));
            end;
            if self.allowsSeedChanging and table.getn(self.seeds) > 1 then
                g_currentMission:addHelpButtonText(g_i18n:getText("ChooseSeed"), self.changeSeedInputButton);
            end;
            if self.needsActivation then
                if self.isTurnedOn then
                    g_currentMission:addHelpButtonText(string.format(g_i18n:getText("turn_off_OBJECT"), self.typeDesc), self.turnOnOffInputButton);
                else
                    if (self.fillLevel > 0 or self.capacity == 0) then
                        g_currentMission:addHelpButtonText(string.format(g_i18n:getText("turn_on_OBJECT"), self.typeDesc), self.turnOnOffInputButton);
                    end;
                end;
            end;
            --if self:getIsFoldAllowed() then -- TODO - only allow lowering when unfolded
              local nextLoweringState = (self.loweringState%4)+1
              if self:getIsLoweringStateAllowed(nextLoweringState) then
                  g_currentMission:addHelpButtonText(self.loweringStates[nextLoweringState].helpText, self.nextLoweringStateInputButton);
              end
              
              if self.raiseBothInputButton ~= nil and (self.loweringState%4) ~= CultivatorSowingMachine.LOWERINGSTATE_RAISEBOTH then
                  g_currentMission:addHelpButtonText(self.loweringStates[CultivatorSowingMachine.LOWERINGSTATE_RAISEBOTH].helpText, self.raiseBothInputButton);
              end
            --end;
        end

        g_currentMission:setFruitOverlayFruitType(self.seeds[self.currentSeed]);

        if math.abs(self.speedViolationTimer - self.speedViolationMaxTime) > 2 then
            local buttonName = self.maxSpeedLevelSettings[self.maxSpeedLevel].buttonName
            g_currentMission:addWarning(g_i18n:getText("Dont_drive_to_fast") .. "\n" .. string.format(g_i18n:getText("Cruise_control_levelN"), tostring(self.maxSpeedLevel)), 0.07+0.022, 0.019+0.029);
        elseif self.showFieldNotOwnedWarning then
            g_currentMission:addWarning(g_i18n:getText("You_dont_own_this_field"));
        end;
    end;
end;

function CultivatorSowingMachine:onEnter(isControlling)
    if isControlling then
        CultivatorSowingMachine.onActivate(self);
    end;
    CultivatorSowingMachine.addContactReports(self);
end;

function CultivatorSowingMachine:onLeave()
    if self.deactivateOnLeave then
        CultivatorSowingMachine.onDeactivate(self);
        CultivatorSowingMachine.removeContactReports(self);
    else
        CultivatorSowingMachine.onDeactivateSounds(self);
    end;
end;

function CultivatorSowingMachine:onActivate()
end;

function CultivatorSowingMachine:onDeactivate()
    self.speedViolationTimer = self.speedViolationMaxTime;
    self.showFieldNotOwnedWarning = false;
    for _, ps in pairs(self.groundParticleSystems) do
        if ps.isActive then
            ps.isActive = false;
            Utils.setEmittingState(ps.ps, false);
        end;
    end;
    self:setIsTurnedOn(false, true);
    CultivatorSowingMachine.onDeactivateSounds(self);
end;

function CultivatorSowingMachine:getIsLoweringStateAllowed(wantedLoweringState)
  if self.foldAnimTime ~= nil and self.foldAnimTime > 0.01 and
    (  wantedLoweringState == CultivatorSowingMachine.LOWERINGSTATE_LOWERCULTIVATOR
    or wantedLoweringState == CultivatorSowingMachine.LOWERINGSTATE_LOWERSEEDER    
    or wantedLoweringState == CultivatorSowingMachine.LOWERINGSTATE_LOWERBOTH
    ) then
    -- Must be unfolded, before lowering allowed
    return false
  end
  return true;
end

function CultivatorSowingMachine:setFoldState(direction, moveToMiddle, noEventSend)
    -- If folding then also force turn-off and raise of cultivator and seeder
    if self.foldMoveDirection > 0 then
        self:setIsTurnedOn(false, noEventSend);
        self:setLowerState(CultivatorSowingMachine.LOWERINGSTATE_RAISEBOTH, noEventSend)
    end
end

function CultivatorSowingMachine:setIsTurnedOn(turnedOn, noEventSend)
    SetTurnedOnEvent.sendEvent(self, turnedOn, noEventSend)
    self.isTurnedOn = turnedOn;
    if self.turnOnAnimation ~= nil and self.playAnimation ~= nil then
        local speed = self.turnOnAnimationSpeed;
        if not self.isTurnedOn then
            speed = -speed;
        end
        self:playAnimation(self.turnOnAnimation, speed, nil, true);
    end;
    if not turnedOn and self.airBlowerSoundEnabled then
        self.airBlowerSoundEnabled = false;
        stopSample(self.airBlowerSound);
    end;
    -- Cultivator rotors activate/deactivate
    self:setAnimationClip(1,self.isTurnedOn)
end;

function CultivatorSowingMachine:onDeactivateSounds()
    if self.cultivatorSoundEnabled then
        stopSample(self.cultivatorSound);
        self.cultivatorSoundEnabled = false;
    end;
    if self.sowingSoundEnabled then
        self.sowingSoundEnabled = false;
        stopSample(self.sowingSound);
    end;
    if self.airBlowerSoundEnabled then
        self.airBlowerSoundEnabled = false;
        stopSample(self.airBlowerSound);
    end;
end;

function CultivatorSowingMachine:setSeedIndex(seedIndex, noEventSend)
    SowingMachineSetSeedIndex.sendEvent(self, seedIndex, noEventSend);
    self.currentSeed = math.min(math.max(seedIndex, 1), table.getn(self.seeds));
end;

function CultivatorSowingMachine:setSeedFruitType(fruitType, noEventSend)
    for i,v in ipairs(self.seeds) do
        if v == fruitType then
            self:setSeedIndex(i, noEventSend);
            break;
        end;
    end;
end;

function CultivatorSowingMachine:setLowerState(loweringState, noEventSend)
    if self.loweringState ~= loweringState then
        if noEventSend == nil or noEventSend == false then
            if g_server ~= nil then
                g_server:broadcastEvent(SetLowerEvent:new(self, loweringState), nil, nil, self);
            else
                g_client:getServerConnection():sendEvent(SetLowerEvent:new(self, loweringState));
            end
        end
        --
        if self.loweringStates[loweringState] ~= nil and self.loweringStates[loweringState].actions ~= nil then
            for setIdx,direction in pairs(self.loweringStates[loweringState].actions) do
                CultivatorSowingMachine.setLowerStateEx(self, setIdx, direction)
            end
        end
        self.loweringState = loweringState
    end
end

function CultivatorSowingMachine:setLowerStateEx(loweringSetIdx, direction)
    local loweringSet = self.loweringSets[loweringSetIdx];
    if loweringSet ~= nil and (loweringSet.lowerMoveDirection ~= direction) then
        loweringSet.lowerMoveDirection = direction;

        local doneAnims = {}
        for _,loweringPart in pairs(loweringSet.loweringParts) do
            local speedScale = nil;
            if loweringSet.lowerMoveDirection > 0.1 then
                speedScale = loweringPart.speedScale;
            elseif loweringSet.lowerMoveDirection < -0.1 then
                speedScale = -loweringPart.speedScale;
            end

            if loweringPart.animCharSet ~= 0 then
                local charSet = loweringPart.animCharSet;
                if not doneAnims[charSet] then
                    doneAnims[charSet] = true
                    if speedScale ~= nil then
                        if speedScale > 0 then
                            if getAnimTrackTime(charSet, 0) < 0.0 then
                                setAnimTrackTime(charSet, 0, 0.0);
                            end
                        else
                            if getAnimTrackTime(charSet, 0) > loweringPart.animDuration then
                                setAnimTrackTime(charSet, 0, loweringPart.animDuration);
                            end
                        end
                        setAnimTrackSpeedScale(charSet, 0, speedScale);
                        enableAnimTrack(charSet, 0);
                    else
                        disableAnimTrack(charSet, 0);
                    end
                end
            elseif not doneAnims[loweringPart.animationName] then
                doneAnims[loweringPart.animationName] = true
                -- always stop to make sure the animation state is reset
                self:stopAnimation(loweringPart.animationName, true);
                if speedScale ~= nil then
                    local animTime = (loweringSet.lowerAnimTime*loweringSet.maxLowerAnimDuration)/self:getAnimationDuration(loweringPart.animationName);
                    self:playAnimation(loweringPart.animationName, speedScale, animTime, true);
                end
            end
        end
        -- slightly move lower anim time, so that lower limits can trigger for different actions
        if loweringSet.lowerMoveDirection > 0.1 then
            loweringSet.lowerAnimTime = math.min(loweringSet.lowerAnimTime + 0.0001, math.max(loweringSet.lowerAnimTime, 1));
        elseif loweringSet.lowerMoveDirection < -0.1 then
            loweringSet.lowerAnimTime = math.max(loweringSet.lowerAnimTime - 0.0001, math.min(loweringSet.lowerAnimTime, 0));
        end
    end
end;

function CultivatorSowingMachine:canStartAITractor(superFunc)
    -- Requires seeds!
    if self.fillLevel <= 0 then
        return false;
    end
    
    return superFunc(self)
end

function CultivatorSowingMachine:startAITractor(noEventSend)
    
    self.aiLoweringState = 0
    self:setFoldDirection(-1)  -- unfold
    --self:aiLower();
    
    -- do not allow any of the fruit we are seeding
    self.aiProhibitedFruitType = self.seeds[self.currentSeed];
    self.aiProhibitedMinGrowthState = 0;
    self.aiProhibitedMaxGrowthState = FruitUtil.fruitIndexToDesc[self.aiProhibitedFruitType].maxHarvestingGrowthState;
end

function CultivatorSowingMachine:stopAITractor(noEventSend)
    self:aiRaise();
    self.aiLoweringState = nil
    --
    self.aiProhibitedFruitType = FruitUtil.FRUITTYPE_UNKNOWN;
end
 
function CultivatorSowingMachine:setAIImplementsMoveDown(moveDown)
    if moveDown then
        self:aiLower();
    else
        self:aiRaise(true);  -- This delays lifting of the seeder, so the last part would hopefully also be seeded...
    end
end;

function CultivatorSowingMachine:aiTurnOn()
    self.cultivatorLimitToField = true;
    self:setIsTurnedOn(true);
end;

function CultivatorSowingMachine:aiTurnOff()
    self.cultivatorLimitToField = false;
    self:setIsTurnedOn(false);
end;

function CultivatorSowingMachine:aiLower()
    self.cultivatorLimitToField = true;
    self:setIsTurnedOn(true);
    self:setLowerState(CultivatorSowingMachine.LOWERINGSTATE_LOWERBOTH)
end;

function CultivatorSowingMachine:aiRaise(delaySeeder)
    self.cultivatorLimitToField = false;
    if delaySeeder == true then
        self.aiRaiseSeederTime = g_currentMission.time + self.aiRaiseSeederDelayTime
        self:setLowerState(CultivatorSowingMachine.LOWERINGSTATE_RAISECULTIVATOR)
    else
        self:setIsTurnedOn(false);
        self:setLowerState(CultivatorSowingMachine.LOWERINGSTATE_RAISEBOTH)
    end
end;

function CultivatorSowingMachine:getAllowFillFromAir(superFunc)
    if self.isTurnedOn and not self.allowFillFromAirWhileTurnedOn then
        return false;
    end
    return superFunc(self);
end

function CultivatorSowingMachine:getDirectionSnapAngle(superFunc)
    local seedsFruitType = self.seeds[self.currentSeed];
    local desc = FruitUtil.fruitIndexToDesc[seedsFruitType];
    local snapAngle = 0;
    if desc ~= nil then
        snapAngle = desc.directionSnapAngle;
    end
    return math.max(snapAngle, superFunc(self));
end

function CultivatorSowingMachine:addContactReports()
    if self.isServer and not self.csmContactReportsActive then
        for k, v in pairs(self.contactReportNodes) do
            addContactReport(v.node, 0.0001, "groundContactReport", self);
        end;
        self.csmContactReportsActive = true;
    end;
end;

function CultivatorSowingMachine:removeContactReports()
    if self.csmContactReportsActive then
        for k, v in pairs(self.contactReportNodes) do
            removeContactReport(v.node);
            v.hasGroundContact = false;
        end;
        self.csmContactReportsActive = false;
    end;
end;

function CultivatorSowingMachine:groundContactReport(objectId, otherObjectId, isStart, normalForce, tangentialForce)
    if otherObjectId == g_currentMission.terrainRootNode then
        local entry = self.contactReportNodes[objectId];
        if entry ~= nil then
            entry.hasGroundContact = isStart or normalForce > 0 or tangentialForce > 0;
        end;
    end;
end;

-- overwrite Fillable.resetFillLevelIfNeeded
function CultivatorSowingMachine:resetFillLevelIfNeeded(superFunc, fillType)
    -- we convert everything to seeds
    superFunc(self, Fillable.FILLTYPE_SEEDS);
end;

-- overwrite Fillable.allowFillType
function CultivatorSowingMachine:allowFillType(superFunc, fillType, allowEmptying)
    return self.fillTypes[fillType] == true;
end;

-- overwrite Fillable.setFillLevel
function CultivatorSowingMachine:setFillLevel(superFunc, fillLevel, fillType, force)
    -- convert everything to seeds if it is accepted
    if self:allowFillType(fillType, false) then
        fillType = Fillable.FILLTYPE_SEEDS;
    end
    superFunc(self, fillLevel, fillType, force);
end

-- overwrite Fillable.getFillLevel
function CultivatorSowingMachine:getFillLevel(superFunc, fillType)
    return self.fillLevel;
end

function CultivatorSowingMachine:setIsSowingMachineFilling(isFilling, noEventSend)
    SowingMachineSetIsFillingEvent.sendEvent(self, isFilling, noEventSend)
     if self.isSowingMachineFilling ~= isFilling then
        self.isSowingMachineFilling = isFilling;
        self.sowingMachineFillTrigger = nil;
        if isFilling then
            -- find the first trigger which is activable
            for i, trigger in ipairs(self.sowingMachineFillTriggers) do
                if trigger:getIsActivatable(self) then
                    self.sowingMachineFillTrigger = trigger;
                    break;
                end;
            end;
        end
    end;
end;

function CultivatorSowingMachine:addSowingMachineFillTrigger(trigger)
    if table.getn(self.sowingMachineFillTriggers) == 0 then
        g_currentMission:addActivatableObject(self.sowingMachineFillActivatable);
    end;
    table.insert(self.sowingMachineFillTriggers, trigger);
end;

function CultivatorSowingMachine:removeSowingMachineFillTrigger(trigger)
    for i=1, table.getn(self.sowingMachineFillTriggers) do
        if self.sowingMachineFillTriggers[i] == trigger then
            table.remove(self.sowingMachineFillTriggers, i);
            break;
        end;
    end;
    if table.getn(self.sowingMachineFillTriggers) == 0 then
        if self.isServer then
            self:setIsSowingMachineFilling(false);
        end;
        g_currentMission:removeActivatableObject(self.sowingMachineFillActivatable);
    end;
end;

--
--
-- from SowingMachine.LUA

SowingMachineFillActivatable = {}
local SowingMachineFillActivatable_mt = Class(SowingMachineFillActivatable);

function SowingMachineFillActivatable:new(sowingMachine)
    local self = {};
    setmetatable(self, SowingMachineFillActivatable_mt);
    self.sowingMachine = sowingMachine;
    self.activateText = "unknown";
    return self;
end;

function SowingMachineFillActivatable:getIsActivatable()
    if self.sowingMachine:getIsActiveForInput() and self.sowingMachine.fillLevel < self.sowingMachine.capacity then
        -- find the first trigger which is activable
        for i,trigger in ipairs(self.sowingMachine.sowingMachineFillTriggers) do
            if trigger:getIsActivatable(self.sowingMachine) then
                self:updateActivateText();
                return true;
            end;
        end;
    end
    return false;
end;

function SowingMachineFillActivatable:onActivateObject()
    self.sowingMachine:setIsSowingMachineFilling(not self.sowingMachine.isSowingMachineFilling);
    self:updateActivateText();
    g_currentMission:addActivatableObject(self);
end;

function SowingMachineFillActivatable:drawActivate()
    -- TODO draw icon
end;

function SowingMachineFillActivatable:updateActivateText()
    if self.sowingMachine.isSowingMachineFilling then
        self.activateText = string.format(g_i18n:getText("stop_refill_OBJECT"), self.sowingMachine.typeDesc);
    else
        self.activateText = string.format(g_i18n:getText("refill_OBJECT"), self.sowingMachine.typeDesc);
    end;
end;

--
--
-- Customized version of FoldableSetFoldDirectionEvent.lua

SetLowerEvent = {};
SetLowerEvent_mt = Class(SetLowerEvent, Event);

InitEventClass(SetLowerEvent, "SetLowerEvent");

function SetLowerEvent:emptyNew()
    local self = Event:new(SetLowerEvent_mt);
    return self;
end;

function SetLowerEvent:new(object, loweringState)
    local self = SetLowerEvent:emptyNew()
    self.object = object;
    self.loweringState = loweringState;
    return self;
end;

function SetLowerEvent:writeStream(streamId, connection)
    streamWriteInt32(streamId, networkGetObjectId(self.object));
    streamWriteUIntN(streamId, self.loweringState, 3); -- values 0-7 (3 bits)
end;

function SetLowerEvent:readStream(streamId, connection)
    local id = streamReadInt32(streamId);
    self.loweringState = streamReadUIntN(streamId, 3); -- values 0-7 (3 bits)
    self.object = networkGetObject(id);
    self:run(connection);
end;

function SetLowerEvent:run(connection)
    if self.object ~= nil then
        self.object:setLowerState(self.loweringState, true);
        --
        --if not connection:getIsServer() then
        --    g_server:broadcastEvent(SetLowerEvent:new(self.object, self.loweringState), nil, connection, self.object);
        --end;
    end
end;
