-- date: 16.03.2015
-- author: rafftnix
-- version: v 1.0

-- script for the final version of the Krone Ultima

Ultima = {}
Ultima.modDir = g_currentModDirectory;
Ultima.STATE_usualLoading = 1;
Ultima.STATE_netBinding = 2;
Ultima.STATE_dropBale = 3;
Ultima.STATE_passLoad = 4;

Ultima.WRAPPERSTATE_empty = 1;
Ultima.WRAPPERSTATE_tiltTable = 2;
Ultima.WRAPPERSTATE_fetchBale = 3;
Ultima.WRAPPERSTATE_wrapping = 4;
Ultima.WRAPPERSTATE_dropCheck = 5;
Ultima.WRAPPERSTATE_dropBale = 6;

Ultima.DROPMODE_AUTO = 1;
Ultima.DROPMODE_MANUAL = 2;
Ultima.DROPMODE_COLLECT = 3;

function Ultima.prerequisitesPresent(specializations)
    return SpecializationUtil.hasSpecialization(Attachable, specializations) and SpecializationUtil.hasSpecialization(Fillable, specializations);
end;

-- increase bale view distance in MP
local oldBaleNew = Bale.new;
Bale.new = function(self, isServer, isClient, customMt)
	local bale = oldBaleNew(self, isServer, isClient, customMt);
	self.forcedClipDistance = 200;
	return bale;
end;

function Ultima:load(xmlFile)
	self.setBalerState = SpecializationUtil.callSpecializationsFunction("setBalerState");
	self.setWrapperState = SpecializationUtil.callSpecializationsFunction("setWrapperState");
	self.setBaleSize = SpecializationUtil.callSpecializationsFunction("setBaleSize");
	self.setBaleDropMode = SpecializationUtil.callSpecializationsFunction("setBaleDropMode");
	self.setWrapperActive = SpecializationUtil.callSpecializationsFunction("setWrapperActive");
	self.setBaleCounter = SpecializationUtil.callSpecializationsFunction("setBaleCounter");
	self.setNetRoleOnStoragePlace = SpecializationUtil.callSpecializationsFunction("setNetRoleOnStoragePlace");
	self.setNetRoleFillState = SpecializationUtil.callSpecializationsFunction("setNetRoleFillState");
	self.foilStorageSetRole = SpecializationUtil.callSpecializationsFunction("foilStorageSetRole");
	self.setWrapFoilLength = SpecializationUtil.callSpecializationsFunction("setWrapFoilLength");
	self.playerTakeRole = SpecializationUtil.callSpecializationsFunction("playerTakeRole");
	self.playerRemoveRole = SpecializationUtil.callSpecializationsFunction("playerRemoveRole");	
	self.wrapperDeleteOldBale = SpecializationUtil.callSpecializationsFunction("wrapperDeleteOldBale");
	self.linkBaleToWrapper = SpecializationUtil.callSpecializationsFunction("linkBaleToWrapper");
	self.setPPCFillLevel = SpecializationUtil.callSpecializationsFunction("setPPCFillLevel");
	self.setPickupEffect = SpecializationUtil.callSpecializationsFunction("setPickupEffect");
	self.balerCreateBale = Ultima.balerCreateBale;
	self.wrapperCreateBale = SpecializationUtil.callSpecializationsFunction("wrapperCreateBale");
	self.wrapperDropBale = SpecializationUtil.callSpecializationsFunction("wrapperDropBale");
	self.setFillLevel = Utils.overwrittenFunction(self.setFillLevel, Ultima.setFillLevel);
	self.allowPickingUp = Utils.overwrittenFunction(self.allowPickingUp, Ultima.allowPickingUp);
	self.setPickupState = Utils.overwrittenFunction(self.setPickupState, Ultima.setPickupState);
	self.getRemainingAnimationTime = Ultima.getRemainingAnimationTime;	
	self.dropmatRaycast = Ultima.dropmatRaycast;	
				
	self.usualLoadingStart = SpecializationUtil.callSpecializationsFunction("usualLoadingStart");
	self.usualLoadingUpdate = SpecializationUtil.callSpecializationsFunction("usualLoadingUpdate");
	self.netBindingStart = SpecializationUtil.callSpecializationsFunction("netBindingStart");
	self.netBindingUpdate = SpecializationUtil.callSpecializationsFunction("netBindingUpdate");
	self.dropBaleStart = SpecializationUtil.callSpecializationsFunction("dropBaleStart");
	self.dropBaleUpdate = SpecializationUtil.callSpecializationsFunction("dropBaleUpdate");
	self.passLoadStart = SpecializationUtil.callSpecializationsFunction("passLoadStart");
	self.passLoadUpdate = SpecializationUtil.callSpecializationsFunction("passLoadUpdate");
	
	--self.wrapperEmptyStart = SpecializationUtil.callSpecializationsFunction("wrapperEmptyStart");
	--self.wrapperEmptyUpdate = SpecializationUtil.callSpecializationsFunction("wrapperEmptyUpdate");
	self.wrapperTiltTableStart = SpecializationUtil.callSpecializationsFunction("wrapperTiltTableStart");
	self.wrapperTiltTableUpdate = SpecializationUtil.callSpecializationsFunction("wrapperTiltTableUpdate");
	self.wrapperFetchBaleStart = SpecializationUtil.callSpecializationsFunction("wrapperFetchBaleStart");
	self.wrapperFetchBaleUpdate = SpecializationUtil.callSpecializationsFunction("wrapperFetchBaleUpdate");
	self.wrapperWrappingStart = SpecializationUtil.callSpecializationsFunction("wrapperWrappingStart");
	self.wrapperWrappingUpdate = SpecializationUtil.callSpecializationsFunction("wrapperWrappingUpdate");
	self.wrapperDropCheckStart = SpecializationUtil.callSpecializationsFunction("wrapperDropCheckStart");
	self.wrapperDropCheckUpdate = SpecializationUtil.callSpecializationsFunction("wrapperDropCheckUpdate");
	self.wrapperDropBaleStart = SpecializationUtil.callSpecializationsFunction("wrapperDropBaleStart");
	self.wrapperDropBaleUpdate = SpecializationUtil.callSpecializationsFunction("wrapperDropBaleUpdate");
		
	self.currentBalerState = 1;
	self.currentWrapperState = 1;
	self.currentBaleDropMode = 1;
	self.wrapperIsActive = true;
	self.dropNextBaleDirectly = false;
	self.wrapperBaleSizeIndex = 1;
	self.lastTrackingPos = {0, 0, 0} -- average input tracking
	self.averageInputPerM = 1; -- prevent dividing with 0
	self.inputTracker = {}
	self.trackDistance = 5;
	self.remainingAnimTimeBuffer = 250; 
	self.speedLimitBufferNum = 0;
	self.speedLimitBufferAmount = 0;
	self.maxSpeedLimit = Utils.getNoNil(getXMLFloat(xmlFile, "vehicle.speedLimit#value"), 15);
	self.UVScrollParts = Utils.loadScrollers(self.components, xmlFile, "vehicle.Ultima.uvScrollShader.UVScrollPart", {}, false);
	
	self.ppcFillLevel = 0;
	self.ppcCapacity = Utils.getNoNil(getXMLFloat(xmlFile, "vehicle.Ultima.prePressChamber#capacity"), 100);
	self.currentPPCFillType = Fillable.FILLTYPE_UNKNOWN;
	self.ppcPassThrough = Utils.getNoNil(getXMLFloat(xmlFile, "vehicle.Ultima.prePressChamber#literPerSecond"), 100);
	
	self.baleCountDay = 0;
	self.baleCountLifetime = 0;
	
	self.netBindingTimer = 0;
	self.netBindingTimerTime = 4000; -- dynamic?
	
	self.ultimaTurnOnRotNodes = Utils.loadRotationNodes(xmlFile, {}, "vehicle.turnedOnRotationNodes.turnedOnRotationNode", "baler", self.components);
	
	-- fillTypes fix
	self.balerFillTypes = {}
	self.compressionFactors = {}
	for fillType, enabled in pairs(self.fillTypes) do
		table.insert(self.balerFillTypes, fillType);
		local fillTypeStr = Fillable.fillTypeIntToName[fillType];
		self.compressionFactors[fillType] = Utils.getNoNil(getXMLFloat(xmlFile, "vehicle.Ultima.compressionFactors#"..fillTypeStr), getXMLFloat(xmlFile, "vehicle.Ultima.compressionFactors#default"));
	end;

	-- baler states
	self.balerStates = {}
	function registerBalerState(stateId, startFunction, updateFunction)
		local state = {}
		state.stateId = stateId;
		state.startFunction = startFunction;
		state.updateFunction = updateFunction;
		self.balerStates[stateId] = state;
	end;
	registerBalerState(Ultima.STATE_usualLoading, self.usualLoadingStart, self.usualLoadingUpdate);
	registerBalerState(Ultima.STATE_netBinding, self.netBindingStart, self.netBindingUpdate);
	registerBalerState(Ultima.STATE_dropBale, self.dropBaleStart, self.dropBaleUpdate);
	registerBalerState(Ultima.STATE_passLoad, self.passLoadStart, self.passLoadUpdate);

	-- wrapper states
	self.wrapperStates = {}
	function registerWrapperState(stateId, startFunction, updateFunction)
		local state = {}
		state.stateId = stateId;
		state.startFunction = startFunction;
		state.updateFunction = updateFunction;
		self.wrapperStates[stateId] = state;
	end;
	registerWrapperState(Ultima.WRAPPERSTATE_empty, self.wrapperEmptyStart, self.wrapperEmptyUpdate);
	registerWrapperState(Ultima.WRAPPERSTATE_tiltTable, self.wrapperTiltTableStart, self.wrapperTiltTableUpdate);
	registerWrapperState(Ultima.WRAPPERSTATE_fetchBale, self.wrapperFetchBaleStart, self.wrapperFetchBaleUpdate);
	registerWrapperState(Ultima.WRAPPERSTATE_wrapping, self.wrapperWrappingStart, self.wrapperWrappingUpdate);
	registerWrapperState(Ultima.WRAPPERSTATE_dropCheck, self.wrapperDropCheckStart, self.wrapperDropCheckUpdate);
	registerWrapperState(Ultima.WRAPPERSTATE_dropBale, self.wrapperDropBaleStart, self.wrapperDropBaleUpdate);
	
	-- register i3d animations
	-- times are absolute
	function registerI3dAnimation(xmlFile, xmlPath, loop)
		local i3dAnim = {}
		i3dAnim.nodeId = Utils.indexToObject(self.components, getXMLString(xmlFile, xmlPath.."#nodeIndex"));
		i3dAnim.charSet = getAnimCharacterSet(i3dAnim.nodeId);
		local clip = getAnimClipIndex(i3dAnim.charSet, getXMLString(xmlFile, xmlPath.."#animName"));
		i3dAnim.speedScale = Utils.getNoNil(getXMLFloat(xmlFile, xmlPath.."#speedScale"), 1);
		i3dAnim.trackIndex = Utils.getNoNil(getXMLFloat(xmlFile, xmlPath.."#trackIndex"), 0);
		i3dAnim.duration = getXMLFloat(xmlFile, xmlPath.."#duration");
	
		assignAnimTrackClip(i3dAnim.charSet, i3dAnim.trackIndex, clip);
		setAnimTrackLoopState(i3dAnim.charSet, i3dAnim.trackIndex, loop);
		setAnimTrackSpeedScale(i3dAnim.charSet, i3dAnim.trackIndex, i3dAnim.speedScale);
		
		function i3dAnim:play(absStartTime)
			startTime = Utils.getNoNil(startTime, 0);
			setAnimTrackTime(i3dAnim.charSet, i3dAnim.trackIndex, startTime);
			enableAnimTrack(i3dAnim.charSet, i3dAnim.trackIndex);
		end;
		
		function i3dAnim:stop()
			disableAnimTrack(i3dAnim.charSet, i3dAnim.trackIndex);
		end;
		
		function i3dAnim:getTime()
			return getAnimTrackTime(i3dAnim.charSet, i3dAnim.trackIndex);
		end;
		
		function i3dAnim:setTime(trackTime, immediateUpdate)
			setAnimTrackTime(i3dAnim.charSet, i3dAnim.trackIndex, trackTime, immediateUpdate);
		end;
		
		function i3dAnim:setRelativeTime(relTime)
			i3dAnim:setTime(relTime*i3dAnim.duration);
		end;
		
		function i3dAnim:setSpeedScale(scale)
			i3dAnim.speedScale = scale;
			setAnimTrackSpeedScale(i3dAnim.charSet, i3dAnim.trackIndex, i3dAnim.speedScale);
		end;
		
		function i3dAnim:getIsPlaying()
			return isAnimTrackEnabled(i3dAnim.charSet, i3dAnim.trackIndex);
		end;
		
		return i3dAnim;
	end;
	
	-- chamber fill animation
	self.chamberFillAnim = registerI3dAnimation(xmlFile, "vehicle.Ultima.chamberFillAnim", false);
	self.maxChamberFillLevel = getXMLFloat(xmlFile, "vehicle.Ultima.chamberFillAnim#maxFillLevel");
	
	self.balerBaleConductNode = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.Ultima.balerBaleConductNode#index"));
	self.balerBaleConductNodeRot = Utils.getVectorNFromString(getXMLString(xmlFile, "vehicle.Ultima.balerBaleConductNode#rotSpeed"), 3);
	self.wrapperBaleConductNode = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.Ultima.balerBaleConductNode#wrapperBaleConductNodeIndex"));
	
	self.playerRolePos = Utils.getVectorNFromString(getXMLString(xmlFile, "vehicle.Ultima.foilRoleStorages#playerRolePos"), 3);
	self.playerRoleRot = Utils.getRadiansFromString(getXMLString(xmlFile, "vehicle.Ultima.foilRoleStorages#playerRoleRot"), 3);
	self.foilRoleCloneNode = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.Ultima.foilRoleStorages#cloneNodeIndex"));
	
	self.wrapperRolesAnim = registerI3dAnimation(xmlFile, "vehicle.Ultima.wrapperRollsAnim", false);
	
	-- bale sizes and fillTypes
	self.baleSizeIndex = 1;
	self.baleSizes = {}
	local num = 0;
	while true do
		local key = "vehicle.Ultima.baleSizes.baleSize("..tostring(num)..")";
		if not hasXMLProperty(xmlFile, key) then
			break;
		end;
		local baleSize = {}
		baleSize.size = getXMLFloat(xmlFile, key.."#size")/100;
		baleSize.fillLevel = getXMLFloat(xmlFile, key.."#fillLevel");
		
		baleSize.dropBaleAnimation = registerI3dAnimation(xmlFile, key..".dropBaleAnimation", false);
		baleSize.receiveBaleAnimTime = getXMLFloat(xmlFile, key..".dropBaleAnimation#receiveBaleAnimTime");
		baleSize.wrapperReceiveAnim = getXMLString(xmlFile, key..".wrapperReceiveBaleAnimation#xmlAnimName");
		baleSize.wrapAnimation = registerI3dAnimation(xmlFile, key..".wrapAnimation", false);
		local foilCutIndex1 = Utils.indexToObject(self.components, getXMLString(xmlFile, key..".wrapAnimation#wrapFoilCutIndex1"));
		local foilCutIndex2 = Utils.indexToObject(self.components, getXMLString(xmlFile, key..".wrapAnimation#wrapFoilCutIndex2"));
		baleSize.wrapFoilCuts = {foilCutIndex1, foilCutIndex2}
		baleSize.createPhysicalBaleTime = getXMLFloat(xmlFile, key..".wrapAnimation#createPhysicalBaleTime");
		baleSize.wrapperDropAnimName = getXMLString(xmlFile, key..".wrapperDropAnimation#xmlAnimName");
		baleSize.wrapAnimation:play(0); 
		baleSize.wrapAnimation:setTime(0, true);
		baleSize.wrapAnimation:stop();
		
		baleSize.fillTypes = {}
		local num2 = 0;
		while true do 
			local key2 = "vehicle.Ultima.baleSizes.baleSize("..tostring(num)..").fillTypes.fillType("..tostring(num2)..")";
			if not hasXMLProperty(xmlFile, key2) then
				break;
			end;
			local desc = {}
			desc.fillType = Fillable.fillTypeNameToInt[getXMLString(xmlFile, key2.."#Ftype")];
			desc.fileName = getXMLString(xmlFile, key2.."#fileName");
			if getXMLString(xmlFile, key2.."#wrappedFileName") ~= nil then
				desc.isWrapAble = true;
				desc.wrappedFileName = getXMLString(xmlFile, key2.."#wrappedFileName");
				desc.fileNameForWrapping = getXMLString(xmlFile, key2.."#fileNameForWrapping");
			else
				desc.isWrapAble = false;
			end;
			baleSize.fillTypes[desc.fillType] = desc; 
			num2 = num2 + 1;
		end;
		baleSize.numFillTypes = num2;
		table.insert(self.baleSizes, baleSize);
		num = num + 1;
	end;
	
	-- side doors
	self.sideDoors = {}
	self.numSideDoors = 0;
	while true do
		local key = "vehicle.Ultima.sideDoors.sideDoor("..tostring(self.numSideDoors)..")";
		if not hasXMLProperty(xmlFile, key) then
			break;
		end;
		
		self.numSideDoors = self.numSideDoors + 1;
		local sideDoor = {}
		sideDoor.animName = getXMLString(xmlFile, key.."#animName");
		sideDoor.index = self.numSideDoors;
		sideDoor.vehicle = self;
		sideDoor.triggerId = Utils.indexToObject(self.components, getXMLString(xmlFile, key.."#triggerIndex"));
		sideDoor.playerInTrigger = false;
		addTrigger(sideDoor.triggerId, "triggerCallback", sideDoor);
		
		function sideDoor:open()
			sideDoor.vehicle:playAnimation(sideDoor.animName, 1, 0, false);
		end;
		function sideDoor:close()
			sideDoor.vehicle:playAnimation(sideDoor.animName, -1, 1, false);
		end;
		
		function sideDoor:getIsClosed()
			return sideDoor.vehicle:getAnimationTime(sideDoor.animName) == 0;
		end;
		function sideDoor:getIsOpened()
			return sideDoor.vehicle:getAnimationTime(sideDoor.animName) == 1;
		end;
		
		function sideDoor:triggerCallback(triggerId, otherId, onEnter, onLeave, onStay, otherShapeId)
			if g_currentMission.player ~= nil and otherId == g_currentMission.player.rootNode then
				if onEnter then
					sideDoor.playerInTrigger = true;
				elseif onLeave then
					sideDoor.playerInTrigger = false;
				end;
			end;
		end;
		
		self.sideDoors[sideDoor.index] = sideDoor;
		self.sideDoors[sideDoor.animName] = sideDoor;
	end;	
	
	-- dropmat
	if hasXMLProperty(xmlFile, "vehicle.Ultima.dropmat") then
		local key = "vehicle.Ultima.dropmat";
		self.dropmat = {}
		self.dropmat.triggerId = Utils.indexToObject(self.components, getXMLString(xmlFile, key.."#playerTriggerIndex"));
		self.dropmat.animName = getXMLString(xmlFile, key.."#foldAnimName");
		self.dropmat.raycastNode = Utils.indexToObject(self.components, getXMLString(xmlFile, key.."#raycastNodeIndex"));
		self.dropmat.jointNode = Utils.indexToObject(self.components, getXMLString(xmlFile, key.."#jointIndex"));
		self.dropmat.maxJointY = getXMLFloat(xmlFile, key.."#topJoint");
		self.dropmat.minJointY = getXMLFloat(xmlFile, key.."#bottomJoint");
		self.dropmat.playerInTrigger = false;
		self.dropmat.isFolding = false;
		function self.dropmat:triggerCallback(triggerId, otherId, onEnter, onLeave, onStay, otherShapeId)
			if g_currentMission.player ~= nil and otherId == g_currentMission.player.rootNode then
				if onEnter then
					self.playerInTrigger = true;
				end;
				if onLeave then
					self.playerInTrigger = false;
				end;
			end;		
		end;
		addTrigger(self.dropmat.triggerId, "triggerCallback", self.dropmat);
	end;
	
	-- net roles
	function registerNetRoleStorage(name, index)
		local storage = {}
		local key = "vehicle.Ultima.netRoleStorages."..name.."#nodeIndex";
		storage.nodeId = Utils.indexToObject(self.components, getXMLString(xmlFile, key));
		storage.index = index;
		return storage;
	end;
	
	self.netRoleStorageLeft = registerNetRoleStorage("leftRole", 1);
	self.netRoleStorageMiddle = registerNetRoleStorage("middleRole", 2);
	self.netRoleStorageRight = registerNetRoleStorage("rightRole", 3);
	self.netRoleStorageMiddle.refillAnimName = getXMLString(xmlFile, "vehicle.Ultima.netRoleStorages.middleRole#refillAnimationName");
	self.netRoleStorages = {self.netRoleStorageLeft, self.netRoleStorageMiddle, self.netRoleStorageRight}
	self.netRoleDefaultLenght = getXMLFloat(xmlFile, "vehicle.Ultima.netRoleStorages#defaultLenght")
	-- default: all net roles filled

	self.netRoleTop = {}
	self.netRoleTop.netRole = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.Ultima.netRoleStorages.topRole#netRoleIndex"));
	self.netRoleTop.role = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.Ultima.netRoleStorages.topRole#roleIndex"));
	self.netRoleTop.netRoleRotSpeed = Utils.getVectorNFromString(getXMLString(xmlFile, "vehicle.Ultima.netRoleStorages.topRole#netRoleRotSpeed"), 3);
	self.netRoleTop.fillAnim = getXMLString(xmlFile, "vehicle.Ultima.netRoleStorages.topRole#fillAnim");
	self.netRoleTop.length = self.netRoleDefaultLenght;
	self.netRoleTop.maxLength = self.netRoleDefaultLenght;
	self.numNetWraps = 4;
	self:setNetRoleFillState(self.netRoleTop.length);
	
	local num = 0;
	self.pickupEffects = {}
	self.pickupEffectActiveTime = 0;
	while true do
		local key = "vehicle.Ultima.pickupEffects.pickupEffect("..tostring(num)..")";
		if not hasXMLProperty(xmlFile, key) then
			break;
		end;
		local effect = EffectManager:loadEffect(xmlFile, key, self.components, self);
		effect.isActive = false;
		local fillTypeStr = getXMLString(xmlFile, key.."#fillType");
		local fillType = Fillable.fillTypeNameToInt[fillTypeStr];
		self.pickupEffects[fillType] = effect;
		num = num + 1;
	end;

	local foilWrapMesh1 = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.Ultima.foilWrapMesh#index1"));
	local foilWrapMesh2 = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.Ultima.foilWrapMesh#index2"));
	self.foilWrapMesh = {foilWrapMesh1, foilWrapMesh2}
	
	local mesh1 = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.Ultima.foilWrapCutMesh#index1"));
	local mesh2 = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.Ultima.foilWrapCutMesh#index2"));
	self.foilWrapCutMesh = {mesh1, mesh2}
	
	-- foil roles
	self.foilRoleStorages = {}
	for a=1, 3 do
		local storage = {}
		storage.animName = getXMLString(xmlFile, "vehicle.Ultima.foilRoleStorages.storage("..tostring(a-1)..")#animName");
		storage.holders = {}
		storage.index = a;
		local num = 0;
		while true do
			local key = "vehicle.Ultima.foilRoleStorages.storage("..tostring(a-1)..").roleHolder("..tostring(num)..")#index";
			if not hasXMLProperty(xmlFile, key) then
				break;
			end;
			local nodeId = Utils.indexToObject(self.components, getXMLString(xmlFile, key));
			table.insert(storage.holders, nodeId);
			num = num + 1;
		end;
		self.foilRoleStorages[a] = storage;
		self.foilRoleStorages[storage.animName] = storage;
	end;
	
	self.maxFoilLength = getXMLFloat(xmlFile, "vehicle.Ultima.wrapperFoilHolders#foilLength");
	self.baseFoilUsePerBale = getXMLFloat(xmlFile, "vehicle.Ultima.wrapperFoilHolders#baseFoilUsePerBale");
	self.wrapperFoilHolders = {}
	for a=1, 2 do
		local key = "vehicle.Ultima.wrapperFoilHolders.foilHolder("..tostring(a-1)..")";
		local foilHolder = {}
		foilHolder.nodeId = Utils.indexToObject(self.components, getXMLString(xmlFile, key.."#index"));
		foilHolder.playerTrigger = Utils.indexToObject(self.components, getXMLString(xmlFile, key.."#playerTriggerIndex"));
		foilHolder.emptyAnimName = getXMLString(xmlFile, key.."#emptyAnimName");
		foilHolder.playerInTrigger = false;
		foilHolder.remainingFoilLength = self.maxFoilLength;

		function foilHolder:triggerCallback(triggerId, otherId, onEnter, onLeave, onStay, otherShapeId)
			if g_currentMission.player ~= nil and g_currentMission.player.rootNode == otherId then
				if onEnter then
					foilHolder.playerInTrigger = true;
				end;
				if onLeave then
					foilHolder.playerInTrigger = false;
				end;
			end;
		end;
		addTrigger(foilHolder.playerTrigger, "triggerCallback", foilHolder);
		table.insert(self.wrapperFoilHolders, foilHolder);
	end;
	
	-- sounds 
	local novoGripSoundNode = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.Ultima.sounds.wrapping#nodeIndex"));
	local wrappingSoundNode = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.Ultima.sounds.novoGrip#nodeIndex"));
	self.wrappingStartSound = Utils.loadSample(xmlFile, {}, "vehicle.Ultima.sounds.wrappingStart", nil, self.baseDirectory);
	self.wrappingStopSound = Utils.loadSample(xmlFile, {}, "vehicle.Ultima.sounds.wrappingStop", nil, self.baseDirectory);
	self.wrapping3DSound = Utils.loadSample(xmlFile, {}, "vehicle.Ultima.sounds.wrapping", nil, self.baseDirectory, wrappingSoundNode);
	self.baleDoorOpenSound = Utils.loadSample(xmlFile, {}, "vehicle.Ultima.sounds.baleDoorOpen", nil, self.baseDirectory);
	self.baleDoorCloseSound = Utils.loadSample(xmlFile, {}, "vehicle.Ultima.sounds.baleDoorClose", nil, self.baseDirectory);
	self.novoGripStartSound = Utils.loadSample(xmlFile, {}, "vehicle.Ultima.sounds.novoGripStart", nil, self.baseDirectory);
	self.novoGripStopSound = Utils.loadSample(xmlFile, {}, "vehicle.Ultima.sounds.novoGripStop", nil, self.baseDirectory);
	self.novoGrip3DSound = Utils.loadSample(xmlFile, {}, "vehicle.Ultima.sounds.novoGrip", nil, self.baseDirectory, novoGripSoundNode);
	self.sideDoorOpenSound = Utils.loadSample(xmlFile, {}, "vehicle.Ultima.sounds.sideDoorOpen", nil, self.baseDirectory);
	self.sideDoorCloseSound = Utils.loadSample(xmlFile, {}, "vehicle.Ultima.sounds.sideDoorClose", nil, self.baseDirectory);
	self.netBindingStartSound = Utils.loadSample(xmlFile, {}, "vehicle.Ultima.sounds.netBindingStart", nil, self.baseDirectory);
	self.netBindingStopSound = Utils.loadSample(xmlFile, {}, "vehicle.Ultima.sounds.netBindingStop", nil, self.baseDirectory);
	self.pickupUpSound = Utils.loadSample(xmlFile, {}, "vehicle.Ultima.sounds.pickupUp", nil, self.baseDirectory);
	self.pickupDownSound = Utils.loadSample(xmlFile, {}, "vehicle.Ultima.sounds.pickupDown", nil, self.baseDirectory);
	self.wrapperTiltTableSound = Utils.loadSample(xmlFile, {}, "vehicle.Ultima.sounds.wrapperTiltTable", nil, self.baseDirectory);
	self.wrapperDropBaleSound = Utils.loadSample(xmlFile, {}, "vehicle.Ultima.sounds.wrapperDropBale", nil, self.baseDirectory);
	
	self.hud = RafftnixGUI:new(nil, false, false);
	self.netBindingActiveImg = self.hud:createImage(self.hud.baseElement, 0.8695, 0.16, 0.0435, 0.065, "hud/F1_2.dds");
	self.netBindingPassiveImg = self.hud:createImage(self.hud.baseElement, 0.8695, 0.16, 0.0435, 0.065, "hud/F1_1.dds");
	
	self.ppcOpenImg = self.hud:createImage(self.hud.baseElement, 0.826, 0.16, 0.0435, 0.065, "hud/F4_2.dds");
	self.ppcClosedImg = self.hud:createImage(self.hud.baseElement, 0.826, 0.16, 0.0435, 0.065, "hud/F4_1.dds");
	
	self.baleDoorOpeningImg = self.hud:createImage(self.hud.baseElement, 0.913, 0.16, 0.0435, 0.065, "hud/F5_2.dds");
	self.baleDoorClosedImg = self.hud:createImage(self.hud.baseElement, 0.913, 0.16, 0.0435, 0.065, "hud/F5_1.dds");
	
	self.wrapperActiveImg = self.hud:createImage(self.hud.baseElement, 0.9565, 0.16, 0.0435, 0.065, "hud/F6_2.dds");
	self.wrapperPassiveImg = self.hud:createImage(self.hud.baseElement, 0.9565, 0.16, 0.0435, 0.065, "hud/F6_1.dds");
	
	self.blackBackGroundImg = self.hud:createImage(self.hud.baseElement, 0.826, 0.225, 0.174, 0.065, "hud/balken.dds");
	self.prePressChamberText = self.hud:createText(self.hud.baseElement, 0.85, 0.235, 0, 0, "emty", 0.02, 1);
	self.mainPressChamberText = self.hud:createText(self.hud.baseElement, 0.85, 0.265, 0, 0, "emty", 0.02, 1);

	self.baleCounterAllBG = self.hud:createImage(self.hud.baseElement, 0.826, 0, 0.025, 0.025, "hud/baleCounter_all.dds");
	self.baleCounterSingleBG = self.hud:createImage(self.hud.baseElement, 0.913, 0, 0.025, 0.025, "hud/baleCounter_single.dds");
	self.baleCounterAllText = self.hud:createText(self.hud.baseElement, 0.854, 0.0025, 0, 0, "emty", 0.02, 1);
	self.baleCounterSingleText = self.hud:createText(self.hud.baseElement, 0.942, 0.0025, 0, 0, "emty", 0.02, 1);
end;

function Ultima:delete()
	for a=1, table.getn(self.sideDoors) do
		removeTrigger(self.sideDoors[a].triggerId);
	end;
	for a=1, table.getn(self.wrapperFoilHolders) do
		removeTrigger(self.wrapperFoilHolders[a].playerTrigger);
	end;
	removeTrigger(self.dropmat.triggerId);
	for fillType, effect in pairs(self.pickupEffects) do
		EffectManager:deleteEffect(effect);
	end;
	Utils.deleteSample(self.wrappingStartSound);
	Utils.deleteSample(self.wrappingStopSound);
	Utils.deleteSample(self.wrapping3DSound);
	Utils.deleteSample(self.baleDoorOpenSound);
	Utils.deleteSample(self.baleDoorCloseSound);
	Utils.deleteSample(self.novoGripStartSound);
	Utils.deleteSample(self.novoGripStopSound);
	Utils.deleteSample(self.novoGrip3DSound);
	Utils.deleteSample(self.sideDoorOpenSound);
	Utils.deleteSample(self.sideDoorCloseSound);
	Utils.deleteSample(self.netBindingStartSound);
	Utils.deleteSample(self.netBindingStopSound);
	Utils.deleteSample(self.pickupUpSound);
	Utils.deleteSample(self.pickupDownSound);
	Utils.deleteSample(self.wrapperDropBaleSound);
end;

function Ultima:loadFromAttributesAndNodes(xmlFile, key, resetVehicles)
	local baleCountDay = Utils.getNoNil(getXMLInt(xmlFile, key.."#baleCountDay"), 0);
	local baleCountLifetime = Utils.getNoNil(getXMLInt(xmlFile, key.."#baleCountLifetime"), 0);
	self:setBaleCounter(baleCountDay, baleCountLifetime, true); 

	local wrapperIsActive = Utils.getNoNil(getXMLBool(xmlFile, key.."#wrapperIsActive"), true);
	self:setWrapperActive(wrapperIsActive, true);
	
	local baleSizeIndex = Utils.getNoNil(getXMLInt(xmlFile, key.."#baleSizeIndex"), 1);
	self:setBaleSize(baleSizeIndex, true);
	
	local ppcFillLevel = Utils.getNoNil(getXMLFloat(xmlFile, key.."#prePressChamberFillLevel"), 0);
	local ppcFillTypeStr = Utils.getNoNil(getXMLString(xmlFile, key.."#prePressChamberFilltype"), Fillable.FILLTYPE_UNKNOWN);
	local ppcFillType = Fillable.fillTypeNameToInt[ppcFillTypeStr];
	self:setPPCFillLevel(ppcFillLevel, ppcFillType);
	
	local baleDropMode = Utils.getNoNil(getXMLInt(xmlFile, key.."#baleDropMode"), 1);
	self:setBaleDropMode(baleDropMode, true);
	
	self.dropmat.isFolding = Utils.getNoNil(getXMLBool(xmlFile, key.."#dropmatIsFolding"), false);
	if self.dropmat.isFolding then
		self:playAnimation(self.dropmat.animName, 1, 0, true);
	else
		self:playAnimation(self.dropmat.animName, -1, 1, true);
	end;
	
	-- default: all net roles full, so we switch off the empty roles
	self:setNetRoleOnStoragePlace(1, Utils.getNoNil(getXMLBool(xmlFile, key..".netRoles#left"), false), true);
	self:setNetRoleOnStoragePlace(2, Utils.getNoNil(getXMLBool(xmlFile, key..".netRoles#middle"), false), true);
	self:setNetRoleOnStoragePlace(3, Utils.getNoNil(getXMLBool(xmlFile, key..".netRoles#right"), false), true);
	
	local netRoleLength = Utils.getNoNil(getXMLFloat(xmlFile, key..".netRoles#topRoleLength"), self.netRoleDefaultLenght);
	self:setNetRoleFillState(math.floor(netRoleLength));
	
	for a=1, 3 do
		local storage = self.foilRoleStorages[a];
		local holdersStr = getXMLString(xmlFile, key..".foilRoleStorages.storage"..tostring(a).."#holders");
		if holdersStr ~= nil and holdersStr ~= "" then
			local holders = Utils.splitString(" ", holdersStr);
			for b=1, table.getn(storage.holders) do
				if holders[b] == "1" then
					self:foilStorageSetRole(a, b, true, true);
				else
					self:foilStorageSetRole(a, b, false, true);
				end;
			end;
		end;
	end;
	
	local balerState = Utils.getNoNil(getXMLInt(xmlFile, key.."#balerState"), 1);
	local wrapperState = Utils.getNoNil(getXMLInt(xmlFile, key.."#wrapperState"), 1);
	
	self:setWrapFoilLength(1, Utils.getNoNil(getXMLFloat(xmlFile, key..".wrapperFoilHolders#role1"), self.maxFoilLength));
	self:setWrapFoilLength(2, Utils.getNoNil(getXMLFloat(xmlFile, key..".wrapperFoilHolders#role2"), self.maxFoilLength));
	
	if hasXMLProperty(xmlFile, key..".tempBalerBale") then
		local sizeIndex = getXMLInt(xmlFile, key..".tempBalerBale#sizeIndex");
		local fillTypeStr = getXMLString(xmlFile, key..".tempBalerBale#fillType");
		local fillType = Fillable.fillTypeNameToInt[fillTypeStr];
		self.balerCurrentBale = self:balerCreateBale(sizeIndex, fillType);
	end;
	if hasXMLProperty(xmlFile, key..".wrapperTempBale") then
		local sizeIndex = getXMLInt(xmlFile, key..".wrapperTempBale#sizeIndex");
		local fillTypeStr = getXMLString(xmlFile, key..".wrapperTempBale#fillType");
		local fillType = Fillable.fillTypeNameToInt[fillTypeStr];
		self.wrapperCurrentBale = self:balerCreateBale(sizeIndex, fillType, true);
	end;
	if hasXMLProperty(xmlFile, key..".wrapperBaleObject") then
		local sizeIndex = getXMLInt(xmlFile, key..".wrapperBaleObject#sizeIndex");
		local firstFillTypeStr = getXMLString(xmlFile, key..".wrapperBaleObject#firstFillType");
		local balei3dFilename = getXMLString(xmlFile, key..".wrapperBaleObject#balei3dFilename");
		local firstFillType = Fillable.fillTypeNameToInt[firstFillTypeStr];
		self.wrapperCurrentBale = self:balerCreateBale(sizeIndex, firstFillType, true);
		self:wrapperCreateBale(sizeIndex, firstFillType, balei3dFilename, self.wrapperCurrentBale.node);
	end;
	
	self:setBalerState(balerState, true);
	self:setWrapperState(wrapperState, true);
	
	return BaseMission.VEHICLE_LOAD_OK;
end;

function Ultima:getSaveAttributesAndNodes(nodeIdent)
	local attributes, nodes = "", "";
	local ppcFillTypeName = Fillable.fillTypeIntToName[self.currentPPCFillType];
	
	attributes = attributes ..'baleCountDay="'..tostring(self.baleCountDay)..'" baleCountLifetime="'..tostring(self.baleCountLifetime)..'"';
	attributes = attributes ..' wrapperIsActive="'..tostring(self.wrapperIsActive)..'"';
	attributes = attributes ..' baleSizeIndex="'..tostring(self.baleSizeIndex)..'"';
	attributes = attributes ..' prePressChamberFillLevel="'..tostring(self.ppcFillLevel)..'" prePressChamberFilltype="'..tostring(ppcFillTypeName)..'"';
	attributes = attributes ..' baleDropMode="'..tostring(self.currentBaleDropMode)..'"';
	attributes = attributes ..' balerState="'..tostring(self.currentBalerState)..'"';
	attributes = attributes ..' wrapperState="'..tostring(self.currentWrapperState)..'"';
	attributes = attributes ..' dropmatIsFolding="'..tostring(self.dropmat.isFolding)..'"';
	
	nodes = nodes ..'		<netRoles left="'..tostring(getVisibility(self.netRoleStorageLeft.nodeId))..'" middle="'..tostring(getVisibility(self.netRoleStorageMiddle.nodeId))..'" right="'..tostring(getVisibility(self.netRoleStorageRight.nodeId))..'"';
	nodes = nodes ..' topRoleLength="'..tostring(self.netRoleTop.length)..'"/>\n';
	
	nodes = nodes ..'		<foilRoleStorages>\n';
	for a=1, 3 do
		local storage = self.foilRoleStorages[a];
		nodes = nodes ..'			<storage'..tostring(a)..' holders="';
		for b=1, table.getn(storage.holders) do
			if getVisibility(storage.holders[b]) then
				nodes = nodes .. "1 ";
			else
				nodes = nodes .. "0 ";
			end;
		end;
		nodes = nodes ..'"/>\n';
	end;	
	nodes = nodes ..'		</foilRoleStorages>';

	nodes = nodes ..'\n		<wrapperFoilHolders role1="'..tostring(self.wrapperFoilHolders[1].remainingFoilLength)..'" role2="'..tostring(self.wrapperFoilHolders[2].remainingFoilLength)..'" />';
	
	if self.currentBalerState == Ultima.STATE_dropBale then
		if self.balerCurrentBale ~= nil then
			local fillTypeName = Fillable.fillTypeIntToName[self.balerCurrentBale.fillType];
			nodes = nodes ..'\n 		<tempBalerBale sizeIndex="'..tostring(self.balerCurrentBale.sizeIndex)..'" fillType="'..fillTypeName..'"/>';
		end;
	end;
	if self.wrapperCurrentBale ~= nil then
		if self.wrapperCurrentBale.baleObject == nil then
			local fillTypeName = Fillable.fillTypeIntToName[self.wrapperCurrentBale.fillType];
			nodes = nodes ..'\n 		<wrapperTempBale sizeIndex="'..tostring(self.wrapperCurrentBale.sizeIndex)..'" fillType="'..fillTypeName..'"/>';
		else
			local fillTypeName = Fillable.fillTypeIntToName[self.wrapperCurrentBale.fillType];
			nodes = nodes ..'\n			<wrapperBaleObject sizeIndex="'..tostring(self.wrapperCurrentBale.sizeIndex)..'" firstFillType="'..tostring(fillTypeName)..'" balei3dFilename="'..tostring(self.wrapperCurrentBale.baleObject.baleI3dFilename)..'"/>';
		end;
	end;

	return attributes, nodes;
end;

function Ultima:writeStream(streamId, connection)
	if not connection:getIsServer() then -- sync current data to client while joining 
		streamWriteInt8(streamId, self.currentBaleDropMode);
		streamWriteBool(streamId, self.wrapperIsActive);
		streamWriteInt8(streamId, self.baleSizeIndex);
		streamWriteInt32(streamId, self.baleCountDay);
		streamWriteInt32(streamId, self.baleCountLifetime);
		streamWriteInt32(streamId, math.floor(self.ppcFillLevel));
		streamWriteInt8(streamId, self.ppcFillType);
		streamWriteBool(streamId, self.dropmat.isFolding);
		
		for a=1, table.getn(self.sideDoors) do
			local sideDoor = self.sideDoors[a];
			streamWriteBool(streamId, (sideDoor:getIsOpened() or (not sideDoor:getIsClosed() and self.animations[sideDoor.animName].currentSpeed > 0)));
		end;
		
		for a=1, table.getn(self.netRoleStorages) do
			streamWriteBool(streamId, getVisibility(self.netRoleStorages[a].nodeId));
		end;
		
		streamWriteInt32(streamId, math.floor(self.netRoleTop.length));
		
		for a=1, 3 do
			local storage = self.foilRoleStorages[a];
			for b=1, table.getn(storage.holders) do
				streamWriteBool(streamId, getVisibility(storage.holders[b]));
			end;
		end;
		
		streamWriteInt32(streamId, math.floor(self.wrapperFoilHolders[1].remainingFoilLength));
		streamWriteInt32(streamId, math.floor(self.wrapperFoilHolders[2].remainingFoilLength));

		if self.balerCurrentBale ~= nil then
			streamWriteBool(streamId, true);
			streamWriteInt8(streamId, self.balerCurrentBale.sizeIndex);
			streamWriteInt8(streamId, self.balerCurrentBale.fillType);	
		else
			streamWriteBool(streamId, false);
		end;
		
		if self.wrapperCurrentBale ~= nil then
			streamWriteBool(streamId, true);
			streamWriteInt8(streamId, self.wrapperCurrentBale.sizeIndex);
			streamWriteInt8(streamId, self.wrapperCurrentBale.fillType);	
			if self.wrapperCurrentBale.baleObject ~= nil then
				streamWriteBool(streamId, true);
			else
				streamWriteBool(streamId, false);
			end;
		else
			streamWriteBool(streamId, false);
		end;
		
		streamWriteInt8(streamId, self.currentBalerState);
		streamWriteInt8(streamId, self.currentWrapperState);
		
		for fillType, effect in pairs(self.pickupEffects) do
			streamWriteBool(streamId, effect.isActive);
		end;
	end;
end;

function Ultima:readStream(streamId, connection)
	if connection:getIsServer() then
		self:setBaleDropMode(streamReadInt8(streamId), true);
		self:setWrapperActive(streamReadBool(streamId), true);
		self:setBaleSize(streamReadInt8(streamId), true);
		self:setBaleCounter(streamReadInt32(streamId), streamReadInt32(streamId), true);
		self:setPPCFillLevel(streamReadInt32(streamId), streamReadInt8(streamId));
		self.dropmat.isFolding = streamReadBool(streamId);

		if self.dropmat.isFolding then
			self:playAnimation(self.dropmat.animName, 1, 0, true);
		else
			self:playAnimation(self.dropmat.animName, -1, 1, true);
		end;
				
		for a=1, table.getn(self.sideDoors) do
			local sideDoor = self.sideDoors[a];
			if streamReadBool(streamId) then
				self:setAnimationTime(sideDoor.animName, 1, true);
			else
				self:setAnimationTime(sideDoor.animName, 0, true);
			end;
		end;
		
		for a=1, table.getn(self.netRoleStorages) do
			self:setNetRoleOnStoragePlace(a, streamReadBool(streamId), true);
		end;
		
		local netRoleLength = streamReadInt32(streamId);
		self:setNetRoleFillState(netRoleLength);
		
		for a=1, 3 do
			local storage = self.foilRoleStorages[a];
			for b=1, table.getn(storage.holders) do
				setVisibility(storage.holders[b], streamReadBool(streamId));
			end;
		end;
		
		for a=1, 2 do
			local length = streamReadInt32(streamId);
			self:setWrapFoilLength(a, length, true);
		end;
		
		if streamReadBool(streamId) then
			local sizeIndex = streamReadInt8(streamId);
			local fillType = streamReadInt8(streamId);
			self.balerCurrentBale = self:balerCreateBale(sizeIndex, fillType);
		end;
		
		if streamReadBool(streamId) then
			local sizeIndex = streamReadInt8(streamId);
			local fillType = streamReadInt8(streamId);
			self.wrapperCurrentBale = self:balerCreateBale(sizeIndex, fillType, true);
			if streamReadBool(streamId) then
				self:wrapperDeleteOldBale();
			end;
		end;
		
		self:setBalerState(streamReadInt8(streamId), true);
		self:setWrapperState(streamReadInt8(streamId), true);
		
		for fillType, effect in pairs(self.pickupEffects) do
			if streamReadBool(streamId) then
				self:setPickupEffect(fillType, true);
			end;
		end;
	end;
end;

function Ultima:writeUpdateStream(streamId, connection, dirtyMask)
	if not connection:getIsServer() then
		streamWriteInt8(streamId, self.ppcFillType);
		local percent = (self.ppcFillLevel/self.ppcCapacity)*100;
		streamWriteInt8(streamId, percent);
		for fillType, effect in pairs(self.pickupEffects) do
			streamWriteBool(streamId, effect.isActive);
		end;
	end;
end;

function Ultima:readUpdateStream(streamId, timestamp, connection)
	if connection:getIsServer() then
		local fillType = streamReadInt8(streamId);
		local fillLevel = (streamReadInt8(streamId)/100)*self.ppcCapacity;
		self:setPPCFillLevel(fillLevel, fillType);
		for fillType, effect in pairs(self.pickupEffects) do
			if streamReadBool(streamId) ~= effect.isActive then
				self:setPickupEffect(fillType, true);
			end;
		end;
	end;
end;

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

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

function Ultima:update(dt)
	if self:getIsActiveForInput() then
		if self.baleCountDay > 0 then
			if InputBinding.hasEvent(InputBinding.IMPLEMENT_EXTRA4) then
				self:setBaleCounter(0, self.baleCountLifetime);
			end;
		end;
		if InputBinding.hasEvent(InputBinding.SETBALESIZE) then
			self:setBaleSize(self.baleSizeIndex + 1);
		end;
		if InputBinding.hasEvent(InputBinding.SETWRAPPERACTIVE) then
			self:setWrapperActive(not self.wrapperIsActive);
		end;
		if InputBinding.hasEvent(InputBinding.SETBALEDROPMODE) then
			self:setBaleDropMode(self.currentBaleDropMode+1);
		end;
		
		if self.currentBaleDropMode == Ultima.DROPMODE_MANUAL then
			if self.wrapperCurrentBale ~= nil and self.wrapperCurrentBale.baleObject ~= nil and self.fillLevel > (self.capacity * 0.95) then
				g_currentMission:addWarning(g_i18n:getText("unloadBaleOrSwitchToAuto"), 0.018, 0.033);
			end;
		end;
	end;

	if self:getIsActive() and self.isTurnedOn then
		if self.balerStates[self.currentBalerState].updateFunction ~= nil then
			self.balerStates[self.currentBalerState].updateFunction(self, dt);
		end;
		if self.wrapperStates[self.currentWrapperState].updateFunction ~= nil then
			self.wrapperStates[self.currentWrapperState].updateFunction(self, dt);
		end;
		
		if self.balerCurrentBale ~= nil and self.currentBalerState ~= Ultima.STATE_dropBale then
			rotate(self.balerCurrentBale.node, unpack(self.balerBaleConductNodeRot));
		end;
	end;
	
	for a=1, 4 do
		local sideDoor = self.sideDoors[a];
		if sideDoor:getIsOpened() and sideDoor.playerInTrigger then
			if not (g_gui.currentGui ~= nil or g_currentMission.isPlayerFrozen) then
				-- switch net roles and refill main role
				if self.sideDoors.sideDoorFrontLeft == sideDoor then
					if self.netRoleTop.length <= 0 then
						if getVisibility(self.netRoleStorageMiddle.nodeId) then 
							g_currentMission:addHelpButtonText(g_i18n:getText("refillNetRole"), InputBinding.IMPLEMENT_EXTRA2);
							if InputBinding.hasEvent(InputBinding.IMPLEMENT_EXTRA2) then
								if self:getAnimationTime(self.netRoleStorageMiddle.refillAnimName) == 0 then
									self:playAnimation(self.netRoleStorageMiddle.refillAnimName, 1, 0);
								end;
							end;
						elseif getVisibility(self.netRoleStorageLeft.nodeId) then
							g_currentMission:addHelpButtonText(g_i18n:getText("switchNetRolePosition"), InputBinding.IMPLEMENT_EXTRA2);
							if InputBinding.hasEvent(InputBinding.IMPLEMENT_EXTRA2) then
								self:setNetRoleOnStoragePlace(1, false);
								self:setNetRoleOnStoragePlace(2, true);
							end;
						elseif getVisibility(self.netRoleStorageRight.nodeId) then	
							g_currentMission:addHelpButtonText(g_i18n:getText("switchNetRolePosition"), InputBinding.IMPLEMENT_EXTRA2);
							if InputBinding.hasEvent(InputBinding.IMPLEMENT_EXTRA2) then
								self:setNetRoleOnStoragePlace(3, false);
								self:setNetRoleOnStoragePlace(2, true);
							end;
						else
							g_currentMission:addWarning(g_i18n:getText("youNeedToRefillNetRoles"), 0.018, 0.033);
							if self.isTurnedOn and self:allowPickingUp() then
								self.speedLimit = 0;
							end;
						end;
					else
						local text = string.gsub(g_i18n:getText("netRoleFillState"), "percent" , tostring(math.floor(self.netRoleTop.length/self.netRoleDefaultLenght*100)));

						g_currentMission:addExtraPrintText(text);
					end;
					
					-- refill net roles
					if g_currentMission.player.currentRoleMount ~= nil and g_currentMission.player.currentRoleType == "net" then
						local index;
						if not getVisibility(self.netRoleStorageMiddle.nodeId) then
							index = 2;
						elseif not getVisibility(self.netRoleStorageLeft.nodeId) then
							index = 1;
						elseif not getVisibility(self.netRoleStorageRight.nodeId) then
							index = 3;
						end;
						if index ~= nil then
							g_currentMission:addHelpButtonText(g_i18n:getText("insertNetRole"), InputBinding.IMPLEMENT_EXTRA2);
							if InputBinding.hasEvent(InputBinding.IMPLEMENT_EXTRA2) then
								g_currentMission.player.currentRoleMount:playerRemoveRole(g_currentMission.player);
								self:setNetRoleOnStoragePlace(index, true);
							end;
						end;
					end;
				else	-- foil roles			
					-- find a full and an empty role
					local storage = self.foilRoleStorages[sideDoor.animName];
					local holderFullIndex, holderEmptyIndex;
					if storage ~= nil then
						for b=1, table.getn(storage.holders) do
							if getVisibility(storage.holders[b]) then
								if holderFullIndex == nil then
									holderFullIndex = b;
								end;
							else
								if holderEmptyIndex == nil then
									holderEmptyIndex = b;
								end;
							end;
							if holderFullIndex ~= nil and holderEmptyIndex ~= nil then
								break;
							end;
						end;
					end;
					
					-- fill holder
					if holderEmptyIndex ~= nil then
						if g_currentMission.player.currentRoleMount ~= nil and g_currentMission.player.currentRoleType == "foil" then
							g_currentMission:addHelpButtonText(g_i18n:getText("refillFoilRole"), InputBinding.IMPLEMENT_EXTRA2);
							if InputBinding.hasEvent(InputBinding.IMPLEMENT_EXTRA2) then
								g_currentMission.player.currentRoleMount:playerRemoveRole(g_currentMission.player);
								self:foilStorageSetRole(self.foilRoleStorages[sideDoor.animName].index, holderEmptyIndex, true);
								break;
							end;
						end;
					end;
					-- take role from holder
					if holderFullIndex ~= nil then
						if g_currentMission.player.currentRoleMount == nil then
							g_currentMission:addHelpButtonText(g_i18n:getText("takeFoilRole"), InputBinding.IMPLEMENT_EXTRA2);
							if InputBinding.hasEvent(InputBinding.IMPLEMENT_EXTRA2) then
								self:foilStorageSetRole(self.foilRoleStorages[sideDoor.animName].index, holderFullIndex, false);
								self:playerTakeRole(g_currentMission.player);
								break;
							end;
						end;
					end;
				end;
			end;
		end;
	end;
	
	for a=1, table.getn(self.wrapperFoilHolders) do
		if self.wrapperFoilHolders[a].playerInTrigger then
			if not (g_gui.currentGui ~= nil or g_currentMission.isPlayerFrozen) then
				-- refill wrapper foil holders
				if self.wrapperFoilHolders[a].remainingFoilLength <= 0 then
					if g_currentMission.player.currentRoleMount ~= nil then
						g_currentMission:addHelpButtonText(g_i18n:getText("refillFoilRole"), InputBinding.IMPLEMENT_EXTRA2);
						if InputBinding.hasEvent(InputBinding.IMPLEMENT_EXTRA2) then
							g_currentMission.player.currentRoleMount:playerRemoveRole(g_currentMission.player);
							self:setWrapFoilLength(a, self.maxFoilLength);
						end;
					end;
				else -- show fill state
					local foilLength = self.wrapperFoilHolders[a].remainingFoilLength*2;
					local foilPerBale = self.baseFoilUsePerBale*self.baleSizes[self.baleSizeIndex].size;
				
					local numBales = math.ceil(foilLength / foilPerBale);
					local text = string.gsub(g_i18n:getText("foilHoldersFillState"), "numBales" , tostring(numBales));
					local text = string.gsub(text, "percent" , tostring(math.floor(foilLength/self.maxFoilLength*100/2)));
				
					g_currentMission:addExtraPrintText(text);
				end;
			end;
		end;
	end;
	
	if self.isServer then
		-- trigger chamber role refill
		if self:getAnimationTime(self.netRoleStorageMiddle.refillAnimName) == 1 then
			self:setNetRoleFillState(self.netRoleTop.maxLength);
			self:setNetRoleOnStoragePlace(2, false);
			self:playAnimation(self.netRoleStorageMiddle.refillAnimName, -1, 1);
		end;
	end;
	
	if not (g_gui.currentGui ~= nil or g_currentMission.isPlayerFrozen) then
		-- side door control
		for a=1, self.numSideDoors do
			local sideDoor = self.sideDoors[a];
			if sideDoor.playerInTrigger then
				if g_currentMission.player.isEntered then
					if sideDoor:getIsClosed() then
						g_currentMission:addHelpButtonText(g_i18n:getText("openSideDoor"), InputBinding.IMPLEMENT_EXTRA);
						if InputBinding.hasEvent(InputBinding.IMPLEMENT_EXTRA) then
							sideDoor:open();
							Utils.playSample(self.sideDoorOpenSound, 1, 0);
						end;
					elseif sideDoor:getIsOpened() then
						if (sideDoor.animName == "sideDoorFrontLeft" and self:getAnimationTime(self.netRoleStorageMiddle.refillAnimName) == 0) or sideDoor.animName ~= "sideDoorFrontLeft" then
							g_currentMission:addHelpButtonText(g_i18n:getText("closeSideDoor"), InputBinding.IMPLEMENT_EXTRA);
							if InputBinding.hasEvent(InputBinding.IMPLEMENT_EXTRA) then
								sideDoor:close();
								Utils.playSample(self.sideDoorCloseSound, 1, 0);
							end;
						end;
					end;
				end;
			end;
		end;
		if self.dropmat.playerInTrigger then
			if self.dropmat.isFolding then
				g_currentMission:addHelpButtonText(g_i18n:getText("unfoldDropmat"), InputBinding.IMPLEMENT_EXTRA2);
			else
				g_currentMission:addHelpButtonText(g_i18n:getText("foldDropmat"), InputBinding.IMPLEMENT_EXTRA2);
			end;
			
			if InputBinding.hasEvent(InputBinding.IMPLEMENT_EXTRA2) then
				if self:getAnimationTime(self.dropmat.animName) == 0 then
					self:playAnimation(self.dropmat.animName, 1, 0);
					self.dropmat.isFolding = true;
				elseif self:getAnimationTime(self.dropmat.animName) == 1 then
					self:playAnimation(self.dropmat.animName, -1, 1);
					self.dropmat.isFolding = false;
				elseif self.animations[self.dropmat.animName].currentSpeed > 0 then
					self:playAnimation(self.dropmat.animName, -1);
					self.dropmat.isFolding = false;
				elseif self.animations[self.dropmat.animName].currentSpeed < 0 then
					self:playAnimation(self.dropmat.animName, 1);
					self.dropmat.isFolding = true;
				end;
			end;
		end;
	end;
	if self:getAnimationTime(self.dropmat.animName) == 0 then -- and self.dropmat.isFolding then
		local x, y, z = getWorldTranslation(self.dropmat.raycastNode);
		raycastClosest(x, y, z, 0, -1, 0, "dropmatRaycast", 15, self, 6, true);
	end;
	
	if self.isClient then
		Utils.updateRotationNodes(self, self.ultimaTurnOnRotNodes, dt, self:getIsActive() and self:getIsTurnedOn());
		Utils.updateScrollers(self.UVScrollParts, dt, self:getIsActive() and self:getIsTurnedOn());
	end;
end;

function Ultima:dropmatRaycast(hitObjectId, x, y, z, distance, nx, ny, nz, subShapeIndex)
	local lx, ly, lz = worldToLocal(getParent(self.dropmat.jointNode), x, y, z);
	local height = math.min(self.dropmat.maxJointY, math.max(self.dropmat.minJointY, ly));
	local jx, jy, jz = getTranslation(self.dropmat.jointNode);
	setTranslation(self.dropmat.jointNode, jx, height, jz);
end;

function printContent(content, depth)
	if depth == nil then
		depth = 0;
	end;
		
	if depth < 5 then
		for k, v in pairs(content) do
			local space = "";
			for a=1, depth do
				space = space .. "   ";
			end;

			print(space .. "k: " .. tostring(k));
			
			if type(k) == "table" then
				printContent(k, depth+1)
			end;
			
			print(space .. "v: " .. tostring(v));
			
			if type(v) == "table" then
				printContent(v, depth+1)
			end;
		end;
	end;
end;

function Ultima:updateTick(dt)
	if self.lastTrackingPos[1] == 0 and self.lastTrackingPos[2] == 0 and self.lastTrackingPos[3] == 0 then
		self.lastTrackingPos = {getWorldTranslation(self.components[1].node)}
	end;
	
	if self.hud.baseElement.visibility ~= self:getIsActiveForInput(false) then
		if self:getIsActiveForInput(false) then
			self.hud:open();
		else
			self.hud:close();
		end;
	end;
	
	if self:getIsTurnedOn() and self:allowPickingUp() then 
		if not Utils.isSamplePlaying(self.novoGripStartSound, 1.8*dt) then
			if not Utils.isSamplePlaying(self.novoGrip3DSound, 1.8*dt) then
				Utils.play3DSample(self.novoGrip3DSound);
			end;
		end;	
		if self.isServer then
			local workAreas, fieldNotOwned, areaFound = self:getTypedNetworkAreas(WorkArea.AREATYPE_BALER, false);
			local area, fillType = BalerAreaEvent.runLocally(workAreas, self.balerFillTypes);
			
			if area > 0 then
				g_server:broadcastEvent(BalerAreaEvent:new(workAreas, fillType));

				local literPerPx = FruitUtil.getFillTypeLiterPerSqm(fillType, 1) * g_currentMission:getFruitPixelsToSqm();
				local amount = area * literPerPx * self.compressionFactors[fillType];
			
				if self.currentBalerState == Ultima.STATE_usualLoading or self.currentBalerState == Ultima.STATE_passLoad then
					self:setFillLevel(self.fillLevel + amount, fillType, true);
				else 
					self:setPPCFillLevel(self.ppcFillLevel + amount, fillType);
				end;
				
				-- calculate average input per meter
				local x, y, z = getWorldTranslation(self.components[1].node);
				local tp = self.lastTrackingPos;
				local dist = Utils.vector3Length(tp[1]-x, tp[2]-y, tp[3]-z);
				self.lastTrackingPos = {getWorldTranslation(self.components[1].node)}
				table.insert(self.inputTracker, {distance=dist, input=amount});
				
				local fullDistance = 0;
				local fullInput = 0;
				local numRemoved = 0;
				
				-- !!! queue running backwards through table
				for a=table.getn(self.inputTracker), 1, -1 do
					if fullDistance < self.trackDistance then
						fullDistance = fullDistance + self.inputTracker[a-numRemoved].distance;
						fullInput = fullInput + self.inputTracker[a-numRemoved].input;
					else
						table.remove(self.inputTracker, a-numRemoved);
						numRemoved = numRemoved + 1;
					end;
				end;
				self.averageInputPerM = fullInput/fullDistance;
				
				if amount > 0 then
					self.pickupEffectActiveTime = 600;
					self:setPickupEffect(fillType, true);
				end;
			end;
			
			local capacity, fillLevel;
			if self.currentBalerState == Ultima.STATE_usualLoading or self.currentBalerState == Ultima.STATE_passLoad then
				capacity = self.capacity;
				fillLevel = self.fillLevel;
				if self.currentBaleDropMode == Ultima.DROPMODE_COLLECT and self.fillLevel > (self.capacity*0.95) then
					capacity = capacity + self.ppcCapacity;
					capacity = capacity + self.fillLevel;
				end;
			else
				capacity = self.ppcCapacity;
				fillLevel = self.ppcFillLevel;
			end;
			
			local speedLimit = self.maxSpeedLimit;
			local timeRemaining = math.max(self:getRemainingAnimationTime(),1); -- prevent us from dividing with 0 
			local distanceToCover = (capacity-fillLevel)/self.averageInputPerM; 
			speedLimit = (distanceToCover/(timeRemaining/1000))*3.6;
			speedLimit = math.min(speedLimit, self.maxSpeedLimit);
			
			-- buffer speed limit to drive more smoove
			self.speedLimitBufferNum = self.speedLimitBufferNum + 1;
			self.speedLimitBufferAmount = self.speedLimitBufferAmount + speedLimit;
			
			self.speedLimit = self.speedLimitBufferAmount / self.speedLimitBufferNum;
			if self.speedLimitBufferNum > 50 then
				self.speedLimitBufferNum = 0;
				self.speedLimitBufferAmount = 0;
			end;
			
			-- stop baler in case that the wrapper bale has not been dropped
			if self.currentBaleDropMode == Ultima.DROPMODE_MANUAL then
				if self.wrapperCurrentBale ~= nil and self.fillLevel > (self.capacity * 0.95) then
					self.speedLimit = 0;
				end;
			end;
		end;
	else
		self.speedLimit = math.huge;
	end;
	
	self.pickupEffectActiveTime = math.max(0, self.pickupEffectActiveTime - dt);
	if self.pickupEffectActiveTime <= 0 then
		self:setPickupEffect(Fillable.FILLTYPE_UNKNOWN, false);
	end;
	
	self.netBindingActiveImg:setVisibility(self.currentBalerState == Ultima.STATE_netBinding);
	self.netBindingPassiveImg:setVisibility(self.currentBalerState ~= Ultima.STATE_netBinding);
	
	self.ppcOpenImg:setVisibility(self.currentBalerState == Ultima.STATE_netBinding or self.currentBalerState == Ultima.STATE_dropBale);
	self.ppcClosedImg:setVisibility(self.currentBalerState == Ultima.STATE_passLoad or self.currentBalerState == Ultima.STATE_usualLoading);
	
	self.baleDoorOpeningImg:setVisibility(self.currentBalerState == Ultima.STATE_dropBale);
	self.baleDoorClosedImg:setVisibility(self.currentBalerState ~= Ultima.STATE_dropBale);
	
	self.wrapperActiveImg:setVisibility(self.currentWrapperState == Ultima.WRAPPERSTATE_wrapping);
	self.wrapperPassiveImg:setVisibility(self.currentWrapperState ~= Ultima.WRAPPERSTATE_wrapping);
	
	self.prePressChamberText:setText(string.gsub(g_i18n:getText("prePressChamber"), "percent" , floor2(self.ppcFillLevel/self.ppcCapacity)));
	self.mainPressChamberText:setText(string.gsub(g_i18n:getText("mainPressChamber"), "percent" , floor2(self.fillLevel/self.capacity)));
end;

function floor2(value)
	return tostring(math.floor(value*100));
end;

function Ultima:draw()
	if self:getIsActiveForInput() then
		if self.baleCountDay > 0 then
			g_currentMission:addHelpButtonText(g_i18n:getText("resetBaleCounter"), InputBinding.IMPLEMENT_EXTRA4); 
		end;

		if self.isTurnedOn then
			local text = string.gsub(g_i18n:getText("setBaleSize"), "size", tostring(self.baleSizes[self.baleSizeIndex].size));
			g_currentMission:addHelpButtonText(text, InputBinding.SETBALESIZE); 
			if self.wrapperIsActive then
				g_currentMission:addHelpButtonText(g_i18n:getText("deactivateWrapper"), InputBinding.SETWRAPPERACTIVE); 
			else
				g_currentMission:addHelpButtonText(g_i18n:getText("activateWrapper"), InputBinding.SETWRAPPERACTIVE); 
			end;
			
			local mode;
			if self.currentBaleDropMode == Ultima.DROPMODE_AUTO then
				mode = g_i18n:getText("mode_auto");
			elseif self.currentBaleDropMode == Ultima.DROPMODE_MANUAL then
				mode = g_i18n:getText("mode_manual");
			elseif self.currentBaleDropMode == Ultima.DROPMODE_COLLECT then
				mode = g_i18n:getText("mode_collect");
			end;
			local text = string.gsub(g_i18n:getText("setBaleDropMode"), "modeX", mode);
			g_currentMission:addHelpButtonText(text, InputBinding.SETBALEDROPMODE); 
		end;
	end;
	if self:getIsActiveForInput(false) and self.isTurnedOn then
		if self.currentBaleDropMode == Ultima.DROPMODE_MANUAL then
			if self.currentWrapperState == Ultima.WRAPPERSTATE_dropCheck then
				g_currentMission:addHelpButtonText(g_i18n:getText("dropBale"), InputBinding.WRAPPERDROPBALE); 
			end;
		end;
	end;
end;

function Ultima:foilStorageSetRole(storageIndex, holderIndex, visibility, noEventSend)
	setVisibility(self.foilRoleStorages[storageIndex].holders[holderIndex], visibility);

	if not noEventSend then
		FoilStorageSetRoleEvent.sendEvent(self, storageIndex, holderIndex, visibility);
	end;
end;

function Ultima:setBalerState(state, noEventSend)
	self.currentBalerState = state;
	if self.balerStates[state].startFunction ~= nil then
		self.balerStates[state].startFunction(self);
	end;

	if not noEventSend then
		SetBalerStateEvent.sendEvent(self, state);
	end;
end;

function Ultima:setWrapperState(state, noEventSend)
	self.currentWrapperState = state;
	if self.wrapperStates[state].startFunction ~= nil then
		self.wrapperStates[state].startFunction(self);
	end;
	if not noEventSend then
		SetWrapperStateEvent.sendEvent(self, state);
	end;
end;

function Ultima:setPPCFillLevel(fillLevel, fillType)
	self.ppcFillLevel = math.min(fillLevel, self.ppcCapacity);
	self.currentPPCFillType = fillType;
end;

function Ultima:setFillLevel(oldFunc, fillLevel, fillType, force, fillSourceStruct) 
	oldFunc(self, fillLevel, fillType, force, fillSourceStruct);
	if self.currentBalerState == Ultima.STATE_usualLoading or self.currentBalerState == Ultima.STATE_passLoad then
		if self.balerCurrentBale == nil and fillLevel > 0 and fillType ~= Fillable.FILLTYPE_UNKNOWN then
			-- balerCreateBale is not synched at all
			self.balerCurrentBale = self:balerCreateBale(self.baleSizeIndex, fillType);
			self.capacity = self.balerCurrentBale.fillLevel;
			self.chamberFillAnim:play();
		end;
		self.chamberFillAnim:setRelativeTime(fillLevel/self.maxChamberFillLevel);
	end;
end;

function Ultima:balerCreateBale(baleSizeIndex, fillType, linkToWrapper) -- creates temporary bale used for animations
	local bale = {}
	local fileName;
	bale.sizeIndex = baleSizeIndex;
	bale.fillType = fillType;
	bale.fillLevel = self.baleSizes[baleSizeIndex].fillLevel;
	bale.willBeWrapped = self.wrapperIsActive and self.baleSizes[bale.sizeIndex].fillTypes[bale.fillType].isWrapAble;
	if bale.willBeWrapped and self.baleSizes[baleSizeIndex].fillTypes[fillType].fileNameForWrapping ~= nil then
		-- load rounded wrapper bale for wrap animation
		fileName = self.baleSizes[baleSizeIndex].fillTypes[fillType].fileNameForWrapping;
	else
		fileName = self.baleSizes[baleSizeIndex].fillTypes[fillType].fileName;
	end;
	local baleRoot = Utils.loadSharedI3DFile(fileName, self.baseDirectory, false, false);
	bale.node = getChildAt(baleRoot, 0);
	unlink(bale.node);
	delete(baleRoot);
	bale.fileName = fileName;
	setRigidBodyType(bale.node, "NoRigidBody");
	local linkNode = self.balerBaleConductNode;
	if linkToWrapper ~= nil and linkToWrapper then
		linkNode = self.wrapperBaleConductNode;
	end;
	link(linkNode, bale.node);
	return bale;
end;

function Ultima:setBaleSize(sizeIndex, noEventSend)
	if self.baleSizes[sizeIndex] ~= nil then
		self.baleSizeIndex = sizeIndex;
	else
		self.baleSizeIndex = 1;
	end;
	
	-- fix, since chamber fill anim was made for 1,5er bales only
	local scale = 1.5/self.baleSizes[self.baleSizeIndex].size;
	setScale(self.balerBaleConductNode, scale, scale, 1);
	
	if not noEventSend then
		SetBaleSizeEvent.sendEvent(self, self.baleSizeIndex);
	end;
end;

function Ultima:setWrapperActive(isActive, noEventSend)
	self.wrapperIsActive = isActive;
	if not noEventSend then
		SetWrapperActiveEvent.sendEvent(self, isActive);
	end;
end;

function Ultima:setBaleDropMode(mode, noEventSend)
	if mode <= 3 then
		self.currentBaleDropMode = mode;
	else
		self.currentBaleDropMode = 1;
	end;
	if not noEventSend then
		SetBaleDropModeEvent.sendEvent(self, self.currentBaleDropMode);
	end;
end;

function Ultima:allowPickingUp(oldFunc)
	if self.currentBalerState == Ultima.STATE_usualLoading or self.currentBalerState == Ultima.STATE_passLoad then
		if self.fillLevel >= self.capacity then
			if self.currentBaleDropMode == Ultima.DROPMODE_COLLECT then
				if self.ppcFillLevel < (self.ppcCapacity * 0.75) then
					return true;
				else
					return false;
				end;
			else
				return false;
			end;
		end;
	end;
	if self.currentBalerState == Ultima.STATE_netBinding or self.currentBalerState == Ultima.STATE_dropBale then
		if self.ppcFillLevel >= self.ppcCapacity then
			return false;
		end;
	end;
	return oldFunc(self);
end;

function Ultima:getRemainingAnimationTime()
	local animTime = 0;

	-- pre press chamber
	if self.currentBalerState == Ultima.STATE_netBinding or self.currentBalerState == Ultima.STATE_dropBale then
		if self.currentBalerState == Ultima.STATE_netBinding then
			animTime = animTime + self.netBindingTimer;
		end;
		if self.currentBalerState <= Ultima.STATE_dropBale then
			local baleSizeIndex;
			if self.balerCurrentBale ~= nil then
				baleSizeIndex = self.balerCurrentBale.sizeIndex;
			else
				baleSizeIndex = self.wrapperCurrentBale.sizeIndex;
			end;
		
			if self.currentBalerState == Ultima.STATE_netBinding then
				animTime = animTime + self.baleSizes[baleSizeIndex].dropBaleAnimation.duration; -- anim stops at 100% 
			else
				animTime = animTime + self.baleSizes[baleSizeIndex].dropBaleAnimation.duration - self.baleSizes[baleSizeIndex].dropBaleAnimation:getTime();
			end;
		end;
		if animTime <= 0 then
			return 1;
		else
			return animTime+self.remainingAnimTimeBuffer;
		end;
	end;
	
	-- main press chamber
	if self.currentBalerState == Ultima.STATE_usualLoading or self.currentBalerState == Ultima.STATE_passLoad then
		local baleSize;
		local supportsWrapping;
		if self.wrapperCurrentBale ~= nil then
			baleSize = self.baleSizes[self.wrapperCurrentBale.sizeIndex];
			fillType = self.wrapperCurrentBale.fillType;
		else
			return 1; -- no bale yet
		end;

		if self.currentWrapperState == Ultima.WRAPPERSTATE_fetchBale then
			animTime = animTime + self:getRealAnimationTime(baleSize.wrapperReceiveAnim);
		end;	

		-- wrapper anim
		if self.wrapperCurrentBale.willBeWrapped then
			-- note: animation stops at the end and is not set to start until anim starts
			if self.currentWrapperState < Ultima.WRAPPERSTATE_wrapping then
				animTime = animTime + baleSize.wrapAnimation.duration; 
			elseif self.currentWrapperState == Ultima.WRAPPERSTATE_wrapping then
				animTime = animTime + baleSize.wrapAnimation.duration - baleSize.wrapAnimation:getTime();
			end;
		end;		
	
		-- drop anim
		local currentSpeed = self.animations[baleSize.wrapperDropAnimName].currentSpeed;
		if self.currentWrapperState < Ultima.WRAPPERSTATE_dropBale then
			animTime = animTime + (self:getAnimationDuration(baleSize.wrapperDropAnimName)*2);
		end;
		if self.currentWrapperState == Ultima.WRAPPERSTATE_dropBale then
			if currentSpeed > 0 then -- still not dropped
				animTime = animTime + self:getAnimationDuration(baleSize.wrapperDropAnimName) - self:getRealAnimationTime(baleSize.wrapperDropAnimName);
			else -- already dropped, animation running backwards
				--animTime = animTime + self:getAnimationDuration(baleSize.wrapperDropAnimName) - self:getRealAnimationTime(baleSize.wrapperDropAnimName);
			end;
		end;
		if animTime <= 0 then
			return 1;
		else
			return animTime+self.remainingAnimTimeBuffer;
		end;
	end;
end;

function Ultima:playerTakeRole(player, noEventSend)
	if player ~= nil and player.currentRoleMount == nil then
		local nodeId = clone(self.foilRoleCloneNode, true, false, false);
		link(player.toolsRootNode, nodeId);
		setVisibility(nodeId, true);
		player.currentRoleMount = self;
		player.currentRoleType = "foil";
		player.currentRoleNodeId = nodeId;
		setTranslation(nodeId, unpack(self.playerRolePos));
		setRotation(nodeId, unpack(self.playerRoleRot));
		
		if not noEventSend then
			PlayerTakeRoleEvent.sendEvent(self, player, true);
		end;
	end;
end;

function Ultima:playerRemoveRole(player, noEventSend)
	if player ~= nil and player.currentRoleMount ~= nil then
		unlink(player.currentRoleNodeId);
		player.currentRoleMount = nil;
		player.currentRoleType = nil;
		delete(player.currentRoleNodeId);
		player.currentRoleNodeId = nil;
	end;

	if not noEventSend then
		PlayerTakeRoleEvent.sendEvent(self, player, false);
	end;
end;

function Ultima:setNetRoleOnStoragePlace(placeIndex, visibility, noEventSend)
	local storage = self.netRoleStorages[placeIndex];
	setVisibility(storage.nodeId, visibility);
	if visibility then
		storage.length = self.netRoleDefaultLenght;
	end;
	
	if not noEventSend then
		SetNetRoleOnStorageEvent.sendEvent(self, placeIndex, visibility); 
	end;
end;

function Ultima:setNetRoleFillState(length) -- only server to client
	self.netRoleTop.length = math.max(0, length);
	local animTime = 1-(self.netRoleTop.length/self.netRoleTop.maxLength);
	self:setAnimationTime(self.netRoleTop.fillAnim, animTime);
	if self.isServer then
		g_server:broadcastEvent(SetNetRoleFillStateEvent:new(self, length), nil, nil, self);
	end;
end;

function Ultima:usualLoadingUpdate()
	if self.isServer and self.balerCurrentBale ~= nil then
		if self.fillLevel >= self.balerCurrentBale.fillLevel then
			if self.currentWrapperState == Ultima.WRAPPERSTATE_empty then
				self:setBalerState(Ultima.STATE_netBinding);
			end;
		end; 
	end;
end;

function Ultima:netBindingStart()
	if self:getIsActiveForSound() then
		Utils.playSample(self.netBindingStartSound, 1, 0);
	end;
	self.netBindingTimer = self.netBindingTimerTime;
	self:setWrapperState(Ultima.WRAPPERSTATE_tiltTable);
	self.chamberFillAnim:stop();
	if self.isServer then
		local netNeeded = self.baleSizes[self.balerCurrentBale.sizeIndex].size * math.pi * self.numNetWraps;
		self:setNetRoleFillState(self.netRoleTop.length-netNeeded);
	end;
end;

function Ultima:netBindingUpdate(dt)
	if self.netRoleTop.length > 0 then
		rotate(self.netRoleTop.role, unpack(self.netRoleTop.netRoleRotSpeed));
		self.netBindingTimer = self.netBindingTimer - dt;
		if self.netBindingTimer <= 0 and self.isServer then
			self:setBalerState(Ultima.STATE_dropBale);
		end;
	else
		if self:getIsActiveForInput(false) then
			g_currentMission:addWarning(g_i18n:getText("netRoleEmpty"), 0.018, 0.033);
			if self.isTurnedOn and self:allowPickingUp() then
				self.speedLimit = 0;
			end;
		end;
	end;
end;

function Ultima:dropBaleStart()
	if self:getIsActiveForSound() then
		Utils.playSample(self.netBindingStopSound, 1, 0);
		Utils.playSample(self.baleDoorOpenSound, 1, 0);
	end;
	self.baleSizes[self.balerCurrentBale.sizeIndex].dropBaleAnimation:play();
end;

function Ultima:dropBaleUpdate()
	if self.isServer and self.balerCurrentBale ~= nil then
		if self.baleSizes[self.balerCurrentBale.sizeIndex].dropBaleAnimation:getTime() > self.baleSizes[self.balerCurrentBale.sizeIndex].receiveBaleAnimTime then
			if self.currentWrapperState == Ultima.WRAPPERSTATE_tiltTable then	
				self:linkBaleToWrapper();
				self:setWrapperState(Ultima.WRAPPERSTATE_fetchBale);
				self:setFillLevel(0, Fillable.FILLTYPE_UNKNOWN, true);
			end;
		end;
	end;
	-- note: balerCurrentBale becomes wrapperCurrentBale !!
	if self.wrapperCurrentBale ~= nil then
		if self.baleSizes[self.wrapperCurrentBale.sizeIndex].dropBaleAnimation:getTime() >= self.baleSizes[self.wrapperCurrentBale.sizeIndex].dropBaleAnimation.duration then
			if self.isServer then
				self:setBalerState(Ultima.STATE_passLoad);
			end;
		end;
	end;
end;

function Ultima:linkBaleToWrapper() -- server to client
	-- hier wird der temporäre Ballen von der Presse an den Wickler übergeben
	unlink(self.balerCurrentBale.node);
	link(self.wrapperBaleConductNode, self.balerCurrentBale.node);
	self.wrapperCurrentBale = self.balerCurrentBale;
	self.balerCurrentBale = nil;

	if self.isServer then
		g_server:broadcastEvent(LinkBaleToWrapperEvent:new(self), nil, nil, self);
	end;
end;

function Ultima:passLoadStart()
	if self:getIsActiveForSound() then
		Utils.playSample(self.baleDoorCloseSound, 1, 0, nil);
	end;
	self.baleSizes[self.wrapperCurrentBale.sizeIndex].dropBaleAnimation:stop();
end;

function Ultima:passLoadUpdate(dt)
	if self.isServer then
		if self.ppcFillLevel > 0 then
			local amount = math.min(self.ppcFillLevel, self.ppcPassThrough/1000*dt);
			self:setFillLevel(self.fillLevel+amount, self.currentFillType, true);
			self:setPPCFillLevel(self.ppcFillLevel-amount, self.currentFillType);
		else
			self:setBalerState(Ultima.STATE_usualLoading);
		end;
	end;
end;

function Ultima:wrapperTiltTableStart() -- !! XML Animation !!
	if self.isServer then -- anim wird vom animatedVehicle gesyncht
		self:playAnimation(self.baleSizes[self.balerCurrentBale.sizeIndex].wrapperReceiveAnim, 1, 0, false);
		if self:getAnimationTime(self.dropmat.animName) == 1 then
			self:playAnimation(self.dropmat.animName, -1, 1);
		end;
	end;
end;

function Ultima:wrapperTiltTableUpdate(dt)
	if self:getIsActiveForSound() then
		if self:getAnimationTime(self.baleSizes[self.balerCurrentBale.sizeIndex].wrapperReceiveAnim) > 0.95 and self:getAnimationTime(self.baleSizes[self.balerCurrentBale.sizeIndex].wrapperReceiveAnim) < 1 then
			if not Utils.isSamplePlaying(self.wrapperDropBaleSound, 1.8*dt) then
				Utils.playSample(self.wrapperDropBaleSound, 1, 0);
			end;
		end;
	end;
end;

function Ultima:wrapperFetchBaleStart() -- !! XML Animation !!
	if self.isServer then -- anim wird vom animatedVehicle gesyncht
		self:playAnimation(self.baleSizes[self.wrapperCurrentBale.sizeIndex].wrapperReceiveAnim, -1, 1, false);
	end;
end;

function Ultima:wrapperFetchBaleUpdate() -- !! XML Animation !!
	if self:getAnimationTime(self.baleSizes[self.wrapperCurrentBale.sizeIndex].wrapperReceiveAnim) == 0 then
		if self.wrapperCurrentBale.willBeWrapped then
			if self.wrapperFoilHolders[1].remainingFoilLength > 0 and self.wrapperFoilHolders[2].remainingFoilLength > 0 then
				if self.isServer then
					self:setWrapperState(Ultima.WRAPPERSTATE_wrapping);
				end;
			elseif self:getIsActiveForInput(false) then
				g_currentMission:addWarning(g_i18n:getText("foilRoleEmpty"), 0.018, 0.033);
			end;
		elseif self.isServer then
			self:wrapperCreateBale(self.wrapperCurrentBale.sizeIndex, self.wrapperCurrentBale.fillType, self.wrapperCurrentBale.fileName, self.wrapperCurrentBale.node);
			self:setWrapperState(Ultima.WRAPPERSTATE_dropCheck);
		end;
	end;
end;

function Ultima:wrapperWrappingStart()
	self.baleSizes[self.wrapperCurrentBale.sizeIndex].wrapAnimation:play();
	self.wrapperRolesAnim:play();
	if self:getIsActiveForSound() then
		Utils.playSample(self.wrappingStartSound, 1, 0);
	end;
	for a=1, table.getn(self.foilWrapCutMesh) do
		setVisibility(self.foilWrapCutMesh[a], false);
	end;
	for a=1, table.getn(self.foilWrapMesh) do
		setVisibility(self.foilWrapMesh[a], true);
	end;
end;

function Ultima:wrapperWrappingUpdate(dt)
	if self.baleSizes[self.wrapperCurrentBale.sizeIndex].wrapAnimation:getTime() < 21400 then
		if not Utils.isSamplePlaying(self.wrappingStartSound, 1.8*dt) then
			if not Utils.isSamplePlaying(self.wrapping3DSound, 1.8*dt) then
				Utils.play3DSample(self.wrapping3DSound);
			end;
		end;
	else
		Utils.stop3DSample(self.wrapping3DSound);
		if not Utils.isSamplePlaying(self.wrappingStopSound, 1.8*dt) then
			if self:getIsActiveForSound() then
				Utils.playSample(self.wrappingStopSound, 1, 0);
			end;
		end;
	end;

	if self.isServer then
		local sizeIndex = self.wrapperCurrentBale.sizeIndex;
		
		if self.baleSizes[sizeIndex].wrapAnimation:getTime() >= self.baleSizes[sizeIndex].createPhysicalBaleTime then 
			if self.wrapperCurrentBale.baleObject == nil then
				local fileName = self.baleSizes[sizeIndex].fillTypes[self.wrapperCurrentBale.fillType].wrappedFileName;
				self:wrapperCreateBale(sizeIndex, self.wrapperCurrentBale.fillType, fileName);
			end;
		end;
		if self.baleSizes[sizeIndex].wrapAnimation:getTime() >= self.baleSizes[sizeIndex].wrapAnimation.duration then
			self:setWrapperState(Ultima.WRAPPERSTATE_dropCheck);
			if self.isServer then
				self:setWrapFoilLength(1, self.wrapperFoilHolders[1].remainingFoilLength-(self.baseFoilUsePerBale*self.baleSizes[sizeIndex].size)/2);
				self:setWrapFoilLength(2, self.wrapperFoilHolders[2].remainingFoilLength-(self.baseFoilUsePerBale*self.baleSizes[sizeIndex].size)/2);
			end;
		end;
		
		if self.wrapperCurrentBale.baleObject ~= nil then
			setJointFrame(self.wrapperCurrentBale.baleJointIndex, 0, self.wrapperBaleConductNode);
		end;
	end;
end;

function Ultima:setWrapFoilLength(index, length, noEventSend) 
	local foilHolder = self.wrapperFoilHolders[index];
	foilHolder.remainingFoilLength = math.max(0, math.min(self.maxFoilLength, length));
	self:setAnimationTime(foilHolder.emptyAnimName, 1-(foilHolder.remainingFoilLength/self.maxFoilLength));
	if not noEventSend then
		SetWrapFoilLengthEvent.sendEvent(self, index, length);
	end;
end;

-- currently called only at server
-- create the bale that is actually dropped
-- new bale object bale is only on server known to the baler, client does not have any access
function Ultima:wrapperCreateBale(sizeIndex, firstFillType, baleI3dFilename, baleNode) 
	local fillLevel = self.baleSizes[sizeIndex].fillLevel;
	
	if baleNode == nil then
		local baleRoot = Utils.loadSharedI3DFile(baleI3dFilename, self.baseDirectory, false, false); 
		baleNode = getChildAt(baleRoot, 0);
		unlink(baleNode);
		delete(baleRoot);
	end;
	
	self:wrapperDeleteOldBale(baleNode);
	
	if self.isServer then
		local baleObject = Bale:new(self.isServer, self.isClient);
		baleObject.i3dFilename = Utils.getFilename(baleI3dFilename, self.baseDirectory);
		baleObject:setNodeId(baleNode);
		baleObject:register();
		baleObject.baleI3dFilename = baleI3dFilename;
		baleObject.fillLevel = fillLevel;
		local x, y, z = getWorldTranslation(self.wrapperBaleConductNode);
		local rx, ry, rz = getWorldRotation(self.wrapperBaleConductNode);
		link(getRootNode(), baleObject.nodeId);
		setRigidBodyType(baleObject.nodeId, "Dynamic");
		setTranslation(baleObject.nodeId, x, y, z);
		setRotation(baleObject.nodeId, rx, ry, rz);
		
		local constr = JointConstructor:new();
		constr:setActors(self.components[1].node, baleObject.nodeId);
		constr:setJointTransforms(self.wrapperBaleConductNode, self.wrapperBaleConductNode);
				
		for i=1, 3 do
			constr:setRotationLimit(i-1, 0, 0);
			constr:setTranslationLimit(i-1, true, 0, 0);
		end;
		
		constr:setEnableCollision(true);
		self.wrapperCurrentBale.baleJointIndex = constr:finalize();
		self.wrapperCurrentBale.baleObject = baleObject;
		g_currentMission:removeItemToSave(baleObject);
	end;
end;

function Ultima:wrapperDropBale()
	if self.isServer then
		removeJoint(self.wrapperCurrentBale.baleJointIndex);
		g_currentMission:addItemToSave(self.wrapperCurrentBale.baleObject);
		self.wrapperCurrentBale.baleJointIndex = nil;
		self.wrapperCurrentBale.sizeIndex = nil;
		self.wrapperCurrentBale.fillType = nil;
		self.wrapperCurrentBale = nil;
		self:setBaleCounter(self.baleCountDay+1, self.baleCountLifetime+1);
	end;
end;

function Ultima:wrapperDeleteOldBale(baleNode)
	if self.wrapperCurrentBale ~= nil then
		if not self.isServer or self.wrapperCurrentBale.willBeWrapped then
			delete(self.wrapperCurrentBale.node);
		end;
		
		if self.isServer then
			g_server:broadcastEvent(WrapperDeleteOldBaleEvent:new(self), nil, nil, self);
		end;
	end;
end;

function Ultima:wrapperDropCheckStart()
	if self.baleSizes[self.wrapperCurrentBale.sizeIndex].wrapAnimation:getIsPlaying() then
		self.baleSizes[self.wrapperCurrentBale.sizeIndex].wrapAnimation:stop(); 
	end;
	for a=1, table.getn(self.foilWrapMesh) do
		setVisibility(self.foilWrapMesh[a], false);
	end;
	if self.wrapperCurrentBale.willBeWrapped then
		for a=1, table.getn(self.baleSizes[self.wrapperCurrentBale.sizeIndex].wrapFoilCuts) do
			setVisibility(self.baleSizes[self.wrapperCurrentBale.sizeIndex].wrapFoilCuts[a], true);
		end;
	end;
end;

function Ultima:wrapperDropCheckUpdate()
	if self.isServer then 
		if self.currentBaleDropMode == Ultima.DROPMODE_AUTO then
			self:setWrapperState(Ultima.WRAPPERSTATE_dropBale);
		elseif self.dropNextBaleDirectly then
			self:setWrapperState(Ultima.WRAPPERSTATE_dropBale);
			self.dropNextBaleDirectly = false;
		elseif self.currentBaleDropMode == Ultima.DROPMODE_COLLECT then
			if self.balerCurrentBale ~= nil and self.fillLevel >= self.balerCurrentBale.fillLevel then
				self:setWrapperState(Ultima.WRAPPERSTATE_dropBale);
				self.dropNextBaleDirectly = true;
			end;
		end;
	end;
	
	if self.currentBaleDropMode == Ultima.DROPMODE_MANUAL then
		if self:getIsActiveForInput(false) then
			if InputBinding.hasEvent(InputBinding.WRAPPERDROPBALE) then
				self:setWrapperState(Ultima.WRAPPERSTATE_dropBale);
			end;
		end;
	end;
end;

function Ultima:wrapperDropBaleStart()
	for a=1, table.getn(self.baleSizes[self.wrapperCurrentBale.sizeIndex].wrapFoilCuts) do
		setVisibility(self.baleSizes[self.wrapperCurrentBale.sizeIndex].wrapFoilCuts[a], false);
	end;
	for a=1, table.getn(self.foilWrapCutMesh) do
		setVisibility(self.foilWrapCutMesh[a], true);
	end;
	self:playAnimation(self.baleSizes[self.wrapperCurrentBale.sizeIndex].wrapperDropAnimName, 1, 0, false);
end;

function Ultima:wrapperDropBaleUpdate()
	if self.isServer then
		if self.wrapperCurrentBale ~= nil then
			-- sizeIndex is needed after bale is dropped
			self.wrapperBaleSizeIndex = self.wrapperCurrentBale.sizeIndex;
		end;
		if not self:getIsAnimationPlaying(self.baleSizes[self.wrapperBaleSizeIndex].wrapperDropAnimName) then
			self:setWrapperState(Ultima.WRAPPERSTATE_empty);
		end;
		
		if self.wrapperCurrentBale ~= nil and self.wrapperCurrentBale.baleObject ~= nil then
			setJointFrame(self.wrapperCurrentBale.baleJointIndex, 0, self.wrapperBaleConductNode);
		end;
	end;
	if self:getAnimationTime(self.baleSizes[self.wrapperBaleSizeIndex].wrapperDropAnimName) == 1 then
		if self.isServer then
			self:wrapperDropBale(); 
			self:playAnimation(self.baleSizes[self.wrapperBaleSizeIndex].wrapperDropAnimName, -1, 1, false);
		end;
		if self:getIsActiveForSound() then
			Utils.playSample(self.wrapperDropBaleSound, 1, 0);
		end;
	end;
end;

function Ultima:onTurnedOn(noEventSend)
	if self:getIsActiveForSound() then
		Utils.playSample(self.novoGripStartSound, 1, 0);
	end;
end;

function Ultima:onTurnedOff(noEventSend)
	self:setPickupEffect(Fillable.FILLTYPE_UNKNOWN, false);
	Utils.stop3DSample(self.novoGrip3DSound);
	Utils.stop3DSample(self.wrapping3DSound);
	if self:getIsActiveForSound() then
		Utils.playSample(self.novoGripStopSound, 1, 0);
	end;
end;

function Ultima:setPickupState(oldFunc, isPickupLowered, noEventSend)
	oldFunc(self, isPickupLowered, noEventSend);
	if self:getIsActiveForSound() then
		if isPickupLowered then
			Utils.playSample(self.pickupDownSound, 1, 0, nil);
		else
			Utils.playSample(self.pickupUpSound, 1, 0, nil);
		end;
	end;
end;

function Ultima:setPickupEffect(fillType, enabled)
	--if self.pickupEffects[fillType] ~= nil then
		for fillType2, effect in pairs(self.pickupEffects) do
			if fillType2 == fillType and enabled then
				EffectManager:startEffect(effect);
				effect.isActive = true;
			else
				EffectManager:stopEffect(effect);
				effect.isActive = false;
			end;
		end;
	--end;
end;

function Ultima:setBaleCounter(day, lifeTime, noEventSend) 	-- note: usually triggered by server, only reset triggered by client
	self.baleCountDay = day;
	self.baleCountLifetime = lifeTime;
	self.baleCounterSingleText:setText(tostring(day));
	self.baleCounterAllText:setText(tostring(lifeTime));
	
	if not noEventSend then
		SetBalerCountEvent.sendEvent(self, day, lifeTime);
	end;
end;

function Ultima:getDefaultSpeedLimit() -- to enable speed limit check
	return self.maxSpeedLimit;
end;