17 | function Cutter.initSpecialization() |
18 | g_workAreaTypeManager:addWorkAreaType("cutter", false) |
19 | |
20 | local schema = Vehicle.xmlSchema |
21 | schema:setXMLSpecializationType("Cutter") |
22 | |
23 | schema:register(XMLValueType.STRING, "vehicle.cutter#fruitTypes", "List with supported fruit types") |
24 | schema:register(XMLValueType.STRING, "vehicle.cutter#fruitTypeCategories", "List with supported fruit types categories") |
25 | schema:register(XMLValueType.STRING, "vehicle.cutter#fruitTypeConverter", "Name of fruit type converter") |
26 | schema:register(XMLValueType.BOOL, "vehicle.cutter#useWindrowed", "Uses windrow types") |
27 | |
28 | AnimationManager.registerAnimationNodesXMLPaths(schema, "vehicle.cutter.animationNodes") |
29 | |
30 | schema:register(XMLValueType.NODE_INDEX, "vehicle.cutter.fruitExtraObjects.fruitExtraObject(?)#node", "Name of fruit type converter") |
31 | schema:register(XMLValueType.STRING, "vehicle.cutter.fruitExtraObjects.fruitExtraObject(?)#anim", "Change animation name") |
32 | schema:register(XMLValueType.BOOL, "vehicle.cutter.fruitExtraObjects.fruitExtraObject(?)#isDefault", "Is default active") |
33 | schema:register(XMLValueType.STRING, "vehicle.cutter.fruitExtraObjects.fruitExtraObject(?)#fruitType", "Name of fruit type") |
34 | schema:register(XMLValueType.BOOL, "vehicle.cutter.fruitExtraObjects#hideOnDetach", "Hide extra objects on detach", false) |
35 | |
36 | EffectManager.registerEffectXMLPaths(schema, "vehicle.cutter.effect") |
37 | EffectManager.registerEffectXMLPaths(schema, "vehicle.cutter.fillEffect") |
38 | |
39 | schema:register(XMLValueType.NODE_INDEX, Cutter.CUTTER_TILT_XML_KEY .. ".automaticTiltNode(?)#node", "Automatic tilt node") |
40 | schema:register(XMLValueType.ANGLE, Cutter.CUTTER_TILT_XML_KEY .. ".automaticTiltNode(?)#minAngle", "Min. angle", -5) |
41 | schema:register(XMLValueType.ANGLE, Cutter.CUTTER_TILT_XML_KEY .. ".automaticTiltNode(?)#maxAngle", "Max. angle", 5) |
42 | schema:register(XMLValueType.ANGLE, Cutter.CUTTER_TILT_XML_KEY .. ".automaticTiltNode(?)#maxSpeed", "Max. angle change per second", 1) |
43 | schema:register(XMLValueType.NODE_INDEX, Cutter.CUTTER_TILT_XML_KEY .. "#raycastNode1", "Raycast node 1") |
44 | schema:register(XMLValueType.NODE_INDEX, Cutter.CUTTER_TILT_XML_KEY .. "#raycastNode2", "Raycast node 2") |
45 | |
46 | schema:register(XMLValueType.BOOL, "vehicle.cutter#allowsForageGrowthState", "Allows forage growth state", false) |
47 | schema:register(XMLValueType.BOOL, "vehicle.cutter#allowCuttingWhileRaised", "Allow cutting while raised", false) |
48 | schema:register(XMLValueType.INT, "vehicle.cutter#movingDirection", "Moving direction", 1) |
49 | schema:register(XMLValueType.FLOAT, "vehicle.cutter#strawRatio", "Straw ratio", 1) |
50 | |
51 | schema:register(XMLValueType.NODE_INDEX, "vehicle.cutter.spikedDrums.spikedDrum(?)#node", "Spiked drum node (Needs to rotate on X axis)") |
52 | schema:register(XMLValueType.NODE_INDEX, "vehicle.cutter.spikedDrums.spikedDrum(?)#spline", "Reference spline") |
53 | schema:register(XMLValueType.NODE_INDEX, "vehicle.cutter.spikedDrums.spikedDrum(?).spike(?)#node", "Spike that is translated on Y axis depending on spline") |
54 | |
55 | SoundManager.registerSampleXMLPaths(schema, "vehicle.cutter.sounds", "cut") |
56 | |
57 | schema:register(XMLValueType.INT, WorkArea.WORK_AREA_XML_KEY .. ".chopperArea#index", "Chopper area index") |
58 | schema:register(XMLValueType.INT, WorkArea.WORK_AREA_XML_CONFIG_KEY .. ".chopperArea#index", "Chopper area index") |
59 | schema:register(XMLValueType.BOOL, RandomlyMovingParts.RANDOMLY_MOVING_PART_XML_KEY .. "#moveOnlyIfCut", "Move only if cutters cuts something", false) |
60 | schema:register(XMLValueType.BOOL, SpeedRotatingParts.SPEED_ROTATING_PART_XML_KEY .. "#rotateIfTurnedOn", "Rotate only if turned on", false) |
61 | |
62 | schema:setXMLSpecializationType() |
63 | end |
954 | function Cutter:onEndWorkAreaProcessing(dt, hasProcessed) |
955 | if self.isServer then |
956 | local spec = self.spec_cutter |
957 | |
958 | local lastRealArea = spec.workAreaParameters.lastRealArea |
959 | local lastThreshedArea = spec.workAreaParameters.lastThreshedArea |
960 | local lastStatsArea = spec.workAreaParameters.lastStatsArea |
961 | local lastArea = spec.workAreaParameters.lastArea |
962 | |
963 | if lastRealArea > 0 then |
964 | if spec.workAreaParameters.combineVehicle ~= nil then |
965 | local inputFruitType = spec.workAreaParameters.lastFruitType |
966 | |
967 | -- always use the same input fruit type while the ai is active to prevent situations where the combine is unable to unload a different fruit type |
968 | if self:getIsAIActive() then |
969 | local requirements = self:getAIFruitRequirements() |
970 | -- Assume there is only 1 requirement, because we can't quickly test if all requirements have the same fruit type |
971 | -- and having more than 1 fruit type makes it uncertain which fruit we should be using here. |
972 | local requirement = requirements[1] |
973 | if #requirements == 1 and requirement ~= nil and requirement.fruitType ~= FruitType.UNKNOWN then |
974 | inputFruitType = requirement.fruitType |
975 | end |
976 | end |
977 | |
978 | -- take fill type conversion into consideration |
979 | local conversionFactor = spec.currentConversionFactor or 1 |
980 | local outputFillType = spec.currentOutputFillType |
981 | |
982 | local targetOutputFillType = outputFillType |
983 | |
984 | if spec.lastOutputFillTypes[outputFillType] == nil then |
985 | spec.lastOutputFillTypes[outputFillType] = lastRealArea |
986 | else |
987 | spec.lastOutputFillTypes[outputFillType] = spec.lastOutputFillTypes[outputFillType] + lastRealArea |
988 | end |
989 | |
990 | if spec.lastPrioritizedOutputType ~= FillType.UNKNOWN then |
991 | outputFillType = spec.lastPrioritizedOutputType |
992 | end |
993 | |
994 | lastRealArea = lastRealArea * conversionFactor |
995 | local farmId = self:getLastTouchedFarmlandFarmId() |
996 | local strawGroundType = spec.workAreaParameters.lastChopperValue |
997 | local appliedDelta = spec.workAreaParameters.combineVehicle:addCutterArea(lastArea, lastRealArea, inputFruitType, outputFillType, spec.strawRatio, strawGroundType, farmId, self:getCutterLoad()) |
998 | if appliedDelta > 0 and outputFillType == targetOutputFillType then |
999 | spec.lastValidInputFruitType = inputFruitType |
1000 | end |
1001 | end |
1002 | |
1003 | local ha = MathUtil.areaToHa(lastStatsArea, g_currentMission:getFruitPixelsToSqm()) -- 4096px are mapped to 2048m |
1004 | local stats = g_currentMission:farmStats(self:getLastTouchedFarmlandFarmId()) |
1005 | stats:updateStats("threshedHectares", ha) |
1006 | self:updateLastWorkedArea(lastStatsArea) |
1007 | |
1008 | spec.lastAreaBiggerZero = lastArea > 0 |
1009 | if spec.lastAreaBiggerZero then |
1010 | spec.lastAreaBiggerZeroTime = g_currentMission.time |
1011 | end |
1012 | if spec.lastAreaBiggerZero ~= spec.lastAreaBiggerZeroSent then |
1013 | self:raiseDirtyFlags(spec.dirtyFlag) |
1014 | spec.lastAreaBiggerZeroSent = spec.lastAreaBiggerZero |
1015 | end |
1016 | |
1017 | if spec.currentInputFruitType ~= spec.currentInputFruitTypeSent then |
1018 | self:raiseDirtyFlags(spec.effectDirtyFlag) |
1019 | spec.currentInputFruitTypeSent = spec.currentInputFruitType |
1020 | end |
1021 | |
1022 | if self:getAllowCutterAIFruitRequirements() then |
1023 | if self.setAIFruitRequirements ~= nil then |
1024 | -- we do not allow changes of the required type while working, just on ai start (if the cutter has more than one requirement or no requirement -> only one requirement allowed at the time) |
1025 | -- prevents fruit type changes on field borders |
1026 | local requirements = self:getAIFruitRequirements() |
1027 | local requirement = requirements[1] |
1028 | if #requirements > 1 or requirement == nil or requirement.fruitType == FruitType.UNKNOWN then |
1029 | local fruitType = g_fruitTypeManager:getFruitTypeByIndex(spec.currentInputFruitTypeAI) |
1030 | if fruitType ~= nil then |
1031 | local minState = spec.allowsForageGrowthState and fruitType.minForageGrowthState or fruitType.minHarvestingGrowthState |
1032 | self:setAIFruitRequirements(spec.currentInputFruitTypeAI, minState, fruitType.maxHarvestingGrowthState) |
1033 | end |
1034 | end |
1035 | end |
1036 | |
1037 | spec.aiNoValidGroundTimer = 0 |
1038 | end |
1039 | else |
1040 | if self:getAllowCutterAIFruitRequirements() then |
1041 | if hasProcessed then |
1042 | local rootVehicle = self.rootVehicle |
1043 | if rootVehicle.getAIFieldWorkerIsTurning ~= nil then |
1044 | if rootVehicle:getIsAIActive() and rootVehicle:getLastSpeed() > 5 and not rootVehicle:getAIFieldWorkerIsTurning() then |
1045 | spec.aiNoValidGroundTimer = spec.aiNoValidGroundTimer + dt |
1046 | if spec.aiNoValidGroundTimer > 5000 then |
1047 | if rootVehicle.stopCurrentAIJob ~= nil then |
1048 | rootVehicle:stopCurrentAIJob(AIMessageErrorUnknown.new()) |
1049 | end |
1050 | end |
1051 | else |
1052 | spec.aiNoValidGroundTimer = 0 |
1053 | end |
1054 | end |
1055 | end |
1056 | end |
1057 | end |
1058 | end |
1059 | end |
130 | function Cutter:onLoad(savegame) |
131 | local spec = self.spec_cutter |
132 | |
133 | XMLUtil.checkDeprecatedXMLElements(self.xmlFile, "vehicle.turnedOnRotationNodes.turnedOnRotationNode#type", "vehicle.cutter.animationNodes.animationNode", "cutter") --FS17 to FS19 |
134 | XMLUtil.checkDeprecatedXMLElements(self.xmlFile, "vehicle.turnedOnScrollers", "vehicle.cutter.animationNodes.animationNode") --FS17 to FS19 |
135 | XMLUtil.checkDeprecatedXMLElements(self.xmlFile, "vehicle.cutter.turnedOnScrollers", "vehicle.cutter.animationNodes.animationNode") --FS17 to FS19 |
136 | XMLUtil.checkDeprecatedXMLElements(self.xmlFile, "vehicle.cutter.reelspikes", "vehicle.cutter.rotationNodes.rotationNode or vehicle.turnOnVehicle.turnedOnAnimation") --FS17 to FS19 |
137 | XMLUtil.checkDeprecatedXMLElements(self.xmlFile, "vehicle.cutter.threshingParticleSystems.threshingParticleSystem", "vehicle.cutter.fillEffect.effectNode") --FS17 to FS19 |
138 | XMLUtil.checkDeprecatedXMLElements(self.xmlFile, "vehicle.cutter.threshingParticleSystems.emitterShape", "vehicle.cutter.fillEffect.effectNode") --FS17 to FS19 |
139 | XMLUtil.checkDeprecatedXMLElements(self.xmlFile, "vehicle.cutter#convertedFillTypeCategories", "vehicle.cutter#fruitTypeConverter") --FS17 to FS19 |
140 | XMLUtil.checkDeprecatedXMLElements(self.xmlFile, "vehicle.cutter#startAnimationName", "vehicle.turnOnVehicle.turnOnAnimation#name") --FS17 to FS19 |
141 | XMLUtil.checkDeprecatedXMLElements(self.xmlFile, "vehicle.cutter.testAreas", "vehicle.workAreas.workArea.testAreas") --FS19 to FS22 |
142 | |
143 | -- load fruitTypes |
144 | local fruitTypes = nil |
145 | local fruitTypeNames = self.xmlFile:getValue("vehicle.cutter#fruitTypes") |
146 | local fruitTypeCategories = self.xmlFile:getValue("vehicle.cutter#fruitTypeCategories") |
147 | if fruitTypeCategories ~= nil and fruitTypeNames == nil then |
148 | fruitTypes = g_fruitTypeManager:getFruitTypesByCategoryNames(fruitTypeCategories, "Warning: Cutter has invalid fruitTypeCategory '%s' in '"..self.configFileName.."'") |
149 | elseif fruitTypeCategories == nil and fruitTypeNames ~= nil then |
150 | fruitTypes = g_fruitTypeManager:getFruitTypesByNames(fruitTypeNames, "Warning: Cutter has invalid fruitType '%s' in '"..self.configFileName.."'") |
151 | else |
152 | Logging.xmlWarning(self.xmlFile, "Cutter needs either the 'fruitTypeCategories' or 'fruitTypes' attribute!") |
153 | end |
154 | |
155 | spec.currentCutHeight = 0 |
156 | |
157 | if fruitTypes ~= nil then |
158 | spec.fruitTypes = {} |
159 | for _,fruitType in pairs(fruitTypes) do |
160 | table.insert(spec.fruitTypes, fruitType) |
161 | |
162 | if #spec.fruitTypes == 1 then |
163 | local cutHeight = g_fruitTypeManager:getCutHeightByFruitTypeIndex(fruitType, spec.allowsForageGrowthState) |
164 | self:setCutterCutHeight(cutHeight) |
165 | end |
166 | end |
167 | end |
168 | |
169 | spec.fruitTypeConverters = {} |
170 | local category = self.xmlFile:getValue("vehicle.cutter#fruitTypeConverter") |
171 | if category ~= nil then |
172 | local data = g_fruitTypeManager:getConverterDataByName(category) |
173 | if data ~= nil then |
174 | for input, converter in pairs(data) do |
175 | spec.fruitTypeConverters[input] = converter |
176 | end |
177 | end |
178 | end |
179 | |
180 | spec.fillTypes = {} |
181 | for _, fruitType in ipairs(spec.fruitTypes) do |
182 | if spec.fruitTypeConverters[fruitType] ~= nil then |
183 | table.insert(spec.fillTypes, spec.fruitTypeConverters[fruitType].fillTypeIndex) |
184 | else |
185 | local fillType = g_fruitTypeManager:getFillTypeIndexByFruitTypeIndex(fruitType) |
186 | if fillType ~= nil then |
187 | table.insert(spec.fillTypes, fillType) |
188 | end |
189 | end |
190 | end |
191 | |
192 | if self.isClient then |
193 | spec.animationNodes = g_animationManager:loadAnimations(self.xmlFile, "vehicle.cutter.animationNodes", self.components, self, self.i3dMappings) |
194 | |
195 | spec.fruitExtraObjects = {} |
196 | local i = 0 |
197 | while true do |
198 | local key = string.format("vehicle.cutter.fruitExtraObjects.fruitExtraObject(%d)", i) |
199 | |
200 | XMLUtil.checkDeprecatedXMLElements(self.xmlFile, key.."#index", key.."#node") |
201 | |
202 | local node = self.xmlFile:getValue(key.."#node", nil, self.components, self.i3dMappings) |
203 | local anim = self.xmlFile:getValue(key.."#anim") |
204 | local isDefault = self.xmlFile:getValue(key.."#isDefault", false) |
205 | local fruitType = g_fruitTypeManager:getFruitTypeByName(self.xmlFile:getValue(key.."#fruitType")) |
206 | |
207 | if fruitType == nil or (node == nil and anim == nil) then |
208 | break |
209 | end |
210 | |
211 | if node ~= nil then |
212 | setVisibility(node, false) |
213 | end |
214 | |
215 | local extraObject = {node=node, anim=anim} |
216 | spec.fruitExtraObjects[fruitType.index] = extraObject |
217 | if isDefault then |
218 | spec.fruitExtraObjects[FruitType.UNKNOWN] = extraObject |
219 | end |
220 | |
221 | i = i + 1 |
222 | end |
223 | spec.hideExtraObjectsOnDetach = self.xmlFile:getValue("vehicle.cutter.fruitExtraObjects#hideOnDetach", false) |
224 | |
225 | spec.spikedDrums = {} |
226 | self.xmlFile:iterate("vehicle.cutter.spikedDrums.spikedDrum", function(index, key) |
227 | local entry = {} |
228 | entry.node = self.xmlFile:getValue(key.."#node", nil, self.components, self.i3dMappings) |
229 | if entry.node ~= nil then |
230 | entry.spline = self.xmlFile:getValue(key.."#spline", nil, self.components, self.i3dMappings) |
231 | if entry.spline ~= nil then |
232 | setVisibility(entry.spline, false) |
233 | |
234 | entry.spikes = {} |
235 | self.xmlFile:iterate(key .. ".spike", function(_, spikeKey) |
236 | local spike = {} |
237 | spike.node = self.xmlFile:getValue(spikeKey.."#node", nil, self.components, self.i3dMappings) |
238 | if spike.node ~= nil then |
239 | local parent = createTransformGroup(getName(spike.node).."Parent") |
240 | link(getParent(spike.node), parent, getChildIndex(spike.node)) |
241 | setTranslation(parent, getTranslation(spike.node)) |
242 | setRotation(parent, getRotation(spike.node)) |
243 | link(parent, spike.node) |
244 | setTranslation(spike.node, 0, 0, 0) |
245 | setRotation(spike.node, 0, 0, 0) |
246 | |
247 | local _, y, z = localToLocal(spike.node, entry.node, 0, 0, 0) |
248 | local angle = -MathUtil.getYRotationFromDirection(y, z) |
249 | local initalTime = angle / (2 * math.pi) |
250 | if initalTime < 0 then |
251 | initalTime = initalTime + 1 |
252 | end |
253 | |
254 | spike.initalTime = initalTime |
255 | table.insert(entry.spikes, spike) |
256 | end |
257 | end) |
258 | |
259 | local splineTimes = {} |
260 | for t=0, 1, 0.01 do |
261 | local x, y, z = getSplinePosition(entry.spline, t) |
262 | local _ |
263 | _, y, z = worldToLocal(entry.node, x, y, z) |
264 | local angle = -MathUtil.getYRotationFromDirection(y, z) |
265 | |
266 | local alpha = angle / (2 * math.pi) |
267 | if alpha < 0 then |
268 | alpha = alpha + 1 |
269 | end |
270 | table.insert(splineTimes, {alpha=alpha, time=t}) |
271 | end |
272 | |
273 | table.insert(splineTimes, {alpha=splineTimes[1].alpha - 0.000001, time=1}) |
274 | |
275 | table.sort(splineTimes, function(a, b) |
276 | return a.alpha < b.alpha |
277 | end) |
278 | |
279 | entry.splineCurve = AnimCurve.new(linearInterpolator1) |
280 | for j=1, #splineTimes do |
281 | entry.splineCurve:addKeyframe({splineTimes[j].time, time=splineTimes[j].alpha}) |
282 | end |
283 | |
284 | for j=1, #spec.animationNodes do |
285 | local animationNode = spec.animationNodes[j] |
286 | if animationNode.node == entry.node then |
287 | entry.animationNode = animationNode |
288 | end |
289 | end |
290 | |
291 | if entry.animationNode ~= nil then |
292 | table.insert(spec.spikedDrums, entry) |
293 | else |
294 | Logging.xmlWarning(self.xmlFile, "Could not find animation node for spikedDrum '%s'", getName(entry.node)) |
295 | end |
296 | else |
297 | Logging.xmlWarning(self.xmlFile, "No spline defined for spiked drum '%s'", key) |
298 | end |
299 | else |
300 | Logging.xmlWarning(self.xmlFile, "No drum node defined for spiked drum '%s'", key) |
301 | end |
302 | end) |
303 | |
304 | spec.cutterEffects = g_effectManager:loadEffect(self.xmlFile, "vehicle.cutter.effect", self.components, self, self.i3dMappings) |
305 | spec.fillEffects = g_effectManager:loadEffect(self.xmlFile, "vehicle.cutter.fillEffect", self.components, self, self.i3dMappings) |
306 | |
307 | spec.samples = {} |
308 | spec.samples.cut = g_soundManager:loadSampleFromXML(self.xmlFile, "vehicle.cutter.sounds", "cut", self.baseDirectory, self.components, 0, AudioGroup.VEHICLE, self.i3dMappings, self) |
309 | end |
310 | |
311 | spec.lastAutomaticTiltRaycastPosition = {0, 0, 0} |
312 | |
313 | spec.automaticTilt = {} |
314 | spec.automaticTilt.isAvailable = false |
315 | spec.automaticTilt.hasNodes = false |
316 | if self:loadCutterTiltFromXML(self.xmlFile, Cutter.CUTTER_TILT_XML_KEY, spec.automaticTilt) then |
317 | spec.automaticTilt.currentDelta = 0 |
318 | spec.automaticTilt.lastHit = {0, 0, 0} |
319 | spec.automaticTilt.raycastHit = true |
320 | spec.automaticTilt.isAvailable = true |
321 | spec.automaticTilt.hasNodes = #spec.automaticTilt.nodes > 0 |
322 | end |
323 | |
324 | spec.allowsForageGrowthState = self.xmlFile:getValue("vehicle.cutter#allowsForageGrowthState", false) |
325 | spec.allowCuttingWhileRaised = self.xmlFile:getValue("vehicle.cutter#allowCuttingWhileRaised", false) |
326 | spec.movingDirection = MathUtil.sign(self.xmlFile:getValue("vehicle.cutter#movingDirection", 1)) |
327 | spec.strawRatio = self.xmlFile:getValue("vehicle.cutter#strawRatio", 1) |
328 | |
329 | spec.useWindrow = false |
330 | spec.currentInputFillType = FillType.UNKNOWN |
331 | spec.currentInputFruitType = FruitType.UNKNOWN |
332 | spec.currentInputFruitTypeAI = FruitType.UNKNOWN |
333 | spec.lastValidInputFruitType = FruitType.UNKNOWN |
334 | spec.currentInputFruitTypeSent = FruitType.UNKNOWN |
335 | spec.currentOutputFillType = FillType.UNKNOWN |
336 | spec.currentConversionFactor = 1 |
337 | spec.currentGrowthStateTime = 0 |
338 | spec.currentGrowthStateTimer = 0 |
339 | spec.currentGrowthState = 0 |
340 | |
341 | spec.lastAreaBiggerZero = false |
342 | spec.lastAreaBiggerZeroSent = false |
343 | spec.lastAreaBiggerZeroTime = -1 |
344 | |
345 | spec.workAreaParameters = {} |
346 | spec.workAreaParameters.lastRealArea = 0 |
347 | spec.workAreaParameters.lastArea = 0 |
348 | spec.workAreaParameters.lastGrowthState = 0 |
349 | spec.workAreaParameters.lastGrowthStateArea = 0 |
350 | spec.workAreaParameters.fruitTypesToUse = {} |
351 | spec.workAreaParameters.lastFruitTypeToUse = {} |
352 | |
353 | spec.lastOutputFillTypes = {} |
354 | spec.lastPrioritizedOutputType = FillType.UNKNOWN |
355 | spec.lastOutputTime = 0 |
356 | |
357 | spec.cutterLoad = 0 |
358 | spec.isWorking = false |
359 | |
360 | spec.stoneLastState = 0 |
361 | spec.stoneWearMultiplierData = g_currentMission.stoneSystem:getWearMultiplierByType("CUTTER") |
362 | |
363 | spec.workAreaParameters.countArea = true |
364 | spec.aiNoValidGroundTimer = 0 |
365 | |
366 | spec.dirtyFlag = self:getNextDirtyFlag() |
367 | spec.effectDirtyFlag = self:getNextDirtyFlag() |
368 | end |
488 | function Cutter:onUpdate(dt, isActiveForInput, isActiveForInputIgnoreSelection, isSelected) |
489 | local spec = self.spec_cutter |
490 | if spec.automaticTilt.hasNodes then |
491 | local currentDelta, isActive, doReset = self:getCutterTiltDelta() |
492 | currentDelta = -currentDelta -- inverted on cutter side since we rotate the cutter itself |
493 | |
494 | if self.isActive then |
495 | for i=1, #spec.automaticTilt.nodes do |
496 | local automaticTiltNode = spec.automaticTilt.nodes[i] |
497 | |
498 | local _, _, curZ = getRotation(automaticTiltNode.node) |
499 | if not isActive and doReset then |
500 | currentDelta = -curZ * 0.1 -- return to idle is not active |
501 | end |
502 | |
503 | if math.abs(currentDelta) > 0.00001 then |
504 | local speedScale = math.min(math.pow(math.abs(currentDelta) / 0.01745, 2), 1) * MathUtil.sign(currentDelta) |
505 | local rotSpeed = speedScale * automaticTiltNode.maxSpeed * dt |
506 | |
507 | local newRotZ = MathUtil.clamp(curZ + rotSpeed, automaticTiltNode.minAngle, automaticTiltNode.maxAngle) |
508 | setRotation(automaticTiltNode.node, 0, 0, newRotZ) |
509 | |
510 | if self.setMovingToolDirty ~= nil then |
511 | self:setMovingToolDirty(automaticTiltNode.node) |
512 | end |
513 | end |
514 | end |
515 | end |
516 | end |
517 | |
518 | if self.isClient then |
519 | for i=1, #spec.spikedDrums do |
520 | local spikedDrum = spec.spikedDrums[i] |
521 | if spikedDrum.animationNode.state ~= RotationAnimation.STATE_OFF then |
522 | local rot, _, _ = getRotation(spikedDrum.node) |
523 | if rot < 0 then |
524 | rot = rot + 2 * math.pi |
525 | end |
526 | local alpha = rot / (2 * math.pi) |
527 | |
528 | local numSpikes = #spikedDrum.spikes |
529 | for j=1, numSpikes do |
530 | local spike = spikedDrum.spikes[j] |
531 | local splineTime = spikedDrum.splineCurve:get((alpha + spike.initalTime) % 1) |
532 | |
533 | local x, y, z = getSplinePosition(spikedDrum.spline, splineTime) |
534 | local _, spikeY, _ = worldToLocal(getParent(spike.node), x, y, z) |
535 | setTranslation(spike.node, 0, spikeY, 0) |
536 | end |
537 | end |
538 | end |
539 | end |
540 | end |
544 | function Cutter:onUpdateTick(dt, isActiveForInput, isActiveForInputIgnoreSelection, isSelected) |
545 | local spec = self.spec_cutter |
546 | local isTurnedOn = self:getIsTurnedOn() |
547 | |
548 | local isEffectActive = isTurnedOn |
549 | and self.movingDirection == spec.movingDirection |
550 | and self:getLastSpeed() > 0.5 |
551 | and (spec.allowCuttingWhileRaised or self:getIsLowered(true)) |
552 | and spec.workAreaParameters.combineVehicle ~= nil |
553 | |
554 | if isEffectActive then |
555 | local currentTestAreaMinX, currentTestAreaMaxX, testAreaMinX, testAreaMaxX = self:getTestAreaWidthByWorkAreaIndex(1) |
556 | local testAreaCharge = self:getTestAreaChargeByWorkAreaIndex(1) |
557 | |
558 | if not spec.useWindrow then |
559 | spec.cutterLoad = spec.cutterLoad * 0.95 + testAreaCharge * 0.05 |
560 | end |
561 | |
562 | local reset = false |
563 | if currentTestAreaMinX == -math.huge and currentTestAreaMaxX == math.huge then |
564 | currentTestAreaMinX = 0 |
565 | currentTestAreaMaxX = 0 |
566 | reset = true |
567 | end |
568 | |
569 | if spec.movingDirection > 0 then |
570 | currentTestAreaMinX = currentTestAreaMinX * -1 |
571 | currentTestAreaMaxX = currentTestAreaMaxX * -1 |
572 | if currentTestAreaMaxX < currentTestAreaMinX then |
573 | local t = currentTestAreaMinX |
574 | currentTestAreaMinX = currentTestAreaMaxX |
575 | currentTestAreaMaxX = t |
576 | end |
577 | end |
578 | |
579 | local inputFruitType = spec.currentInputFruitType |
580 | if inputFruitType ~= spec.lastValidInputFruitType then |
581 | -- if we pickup a different fruit type than we are able to proceed to the combine we won't display the effect |
582 | inputFruitType = nil |
583 | end |
584 | |
585 | if inputFruitType ~= nil then |
586 | Cutter.updateExtraObjects(self) |
587 | end |
588 | |
589 | local isCollecting = spec.lastAreaBiggerZeroTime + 300 > g_currentMission.time |
590 | local fillType = spec.currentInputFillType |
591 | |
592 | if spec.useWindrow then |
593 | if isCollecting then |
594 | spec.cutterLoad = spec.cutterLoad * 0.95 + 0.05 |
595 | else |
596 | spec.cutterLoad = spec.cutterLoad * 0.9 |
597 | end |
598 | end |
599 | |
600 | if self.isClient then |
601 | local cutSoundActive = false |
602 | if fillType ~= FillType.UNKNOWN and isCollecting then |
603 | g_effectManager:setFillType(spec.fillEffects, fillType) |
604 | g_effectManager:setMinMaxWidth(spec.fillEffects, currentTestAreaMinX, currentTestAreaMaxX, currentTestAreaMinX / testAreaMinX, currentTestAreaMaxX / testAreaMaxX, reset) |
605 | g_effectManager:startEffects(spec.fillEffects) |
606 | |
607 | cutSoundActive = true |
608 | else |
609 | g_effectManager:stopEffects(spec.fillEffects) |
610 | end |
611 | |
612 | if inputFruitType ~= nil and not reset then |
613 | g_effectManager:setFruitType(spec.cutterEffects, inputFruitType, spec.currentGrowthState) |
614 | g_effectManager:setFillType(spec.cutterEffects, fillType) |
615 | g_effectManager:setMinMaxWidth(spec.cutterEffects, currentTestAreaMinX, currentTestAreaMaxX, currentTestAreaMinX / testAreaMinX, currentTestAreaMaxX / testAreaMaxX, reset) |
616 | g_effectManager:startEffects(spec.cutterEffects) |
617 | |
618 | cutSoundActive = true |
619 | else |
620 | g_effectManager:stopEffects(spec.cutterEffects) |
621 | end |
622 | |
623 | if cutSoundActive then |
624 | if not g_soundManager:getIsSamplePlaying(spec.samples.cut) then |
625 | g_soundManager:playSample(spec.samples.cut) |
626 | end |
627 | else |
628 | if g_soundManager:getIsSamplePlaying(spec.samples.cut) then |
629 | g_soundManager:stopSample(spec.samples.cut) |
630 | end |
631 | end |
632 | end |
633 | else |
634 | if self.isClient then |
635 | g_effectManager:stopEffects(spec.cutterEffects) |
636 | g_effectManager:stopEffects(spec.fillEffects) |
637 | g_soundManager:stopSample(spec.samples.cut) |
638 | end |
639 | |
640 | spec.cutterLoad = spec.cutterLoad * 0.9 |
641 | end |
642 | |
643 | -- lastPrioritizedOutputType is always the fill type that was the most significant fill type of the last 500ms |
644 | -- this fill type is transfered to the combine to avoid quick changes of the fill type if we harvester 2 different fruit types at the same time |
645 | -- e.g. on field borders if can happen that a bit of grass if collected from the cutter |
646 | spec.lastOutputTime = spec.lastOutputTime + dt |
647 | if spec.lastOutputTime > 500 then |
648 | spec.lastPrioritizedOutputType = FillType.UNKNOWN |
649 | |
650 | local max = 0 |
651 | for i, _ in pairs(spec.lastOutputFillTypes) do |
652 | if spec.lastOutputFillTypes[i] > max then |
653 | spec.lastPrioritizedOutputType = i |
654 | max = spec.lastOutputFillTypes[i] |
655 | end |
656 | |
657 | spec.lastOutputFillTypes[i] = 0 |
658 | end |
659 | |
660 | spec.lastOutputTime = 0 |
661 | end |
662 | |
663 | local automaticTilt = spec.automaticTilt |
664 | local isActive, _ = self:getCutterTiltIsActive(automaticTilt) |
665 | if isActive then |
666 | if automaticTilt ~= nil and automaticTilt.raycastNode1 ~= nil and automaticTilt.raycastNode2 ~= nil then |
667 | automaticTilt.currentDelta = 0 |
668 | |
669 | -- raycast 1 |
670 | local rx, ry, rz = localToWorld(automaticTilt.raycastNode1, 0, 1, 0) |
671 | local rDirX, rDirY, rDirZ = localDirectionToWorld(automaticTilt.raycastNode1, 0, -1, 0) |
672 | automaticTilt.raycastHit = false |
673 | raycastAll(rx, ry, rz, rDirX, rDirY, rDirZ, "tiltRaycastDetectionCallback", 2, self, Cutter.AUTO_TILT_COLLISION_MASK) |
674 | local hit1X, hit1Y, hit1Z = automaticTilt.lastHit[1], automaticTilt.lastHit[2], automaticTilt.lastHit[3] |
675 | local node1X, node1Y, node1Z = getWorldTranslation(automaticTilt.raycastNode1) |
676 | if not automaticTilt.raycastHit then |
677 | hit1X, hit1Y, hit1Z = localToWorld(automaticTilt.raycastNode1, 0, -1, 0) |
678 | end |
679 | |
680 | --raycast 2 |
681 | rx, ry, rz = localToWorld(automaticTilt.raycastNode2, 0, 1, 0) |
682 | rDirX, rDirY, rDirZ = localDirectionToWorld(automaticTilt.raycastNode2, 0, -1, 0) |
683 | automaticTilt.raycastHit = false |
684 | raycastAll(rx, ry, rz, rDirX, rDirY, rDirZ, "tiltRaycastDetectionCallback", 2, self, Cutter.AUTO_TILT_COLLISION_MASK) |
685 | local hit2X, hit2Y, hit2Z = automaticTilt.lastHit[1], automaticTilt.lastHit[2], automaticTilt.lastHit[3] |
686 | local node2X, node2Y, node2Z = getWorldTranslation(automaticTilt.raycastNode2) |
687 | if not automaticTilt.raycastHit then |
688 | hit2X, hit2Y, hit2Z = localToWorld(automaticTilt.raycastNode2, 0, -1, 0) |
689 | end |
690 | |
691 | -- calaculate ground angle |
692 | local gHeight = hit1Y - hit2Y |
693 | local gRefX, gRefY, gRefZ = hit2X + rDirX * gHeight, hit2Y + rDirY * gHeight, hit2Z + rDirZ * gHeight |
694 | local gDistance = MathUtil.vector3Length(hit1X-gRefX, hit1Y-gRefY, hit1Z-gRefZ) |
695 | local gDirection = (hit2Y > hit1Y and -1 or 1) |
696 | local gAngle = math.atan(math.abs(gHeight) / gDistance) * gDirection |
697 | |
698 | -- calculate current cutter angle |
699 | local cHeight = node2Y - node1Y |
700 | --#debug local cRefX, cRefY, cRefZ = node2X + rDirX * cHeight, node2Y + rDirY * cHeight, node2Z + rDirZ * cHeight |
701 | local cDistance = MathUtil.vector3Length(node1X-node2X, node1Y-node2Y, node1Z-node2Z) |
702 | local cDirection = (node2Y > node1Y and -1 or 1) |
703 | local cAngle = math.atan(math.abs(cHeight) / cDistance) * cDirection |
704 | |
705 | --#debug if VehicleDebug.state == VehicleDebug.DEBUG then |
706 | --#debug DebugUtil.drawDebugGizmoAtWorldPos(hit1X, hit1Y, hit1Z, 0, 0, 1, 0, 1, 0, "r1", false) |
707 | --#debug DebugUtil.drawDebugGizmoAtWorldPos(hit2X, hit2Y, hit2Z, 0, 0, 1, 0, 1, 0, "r2", false) |
708 | --#debug drawDebugLine(hit1X, hit1Y, hit1Z, 0, 1, 0, gRefX, gRefY, gRefZ, 0, 1, 0) |
709 | --#debug drawDebugLine(hit1X, hit1Y, hit1Z, 1, 1, 0, hit2X, hit2Y, hit2Z, 1, 1, 0) |
710 | --#debug drawDebugLine(node1X, node1Y, node1Z, 0, 1, 0, cRefX, cRefY, cRefZ, 0, 1, 0) |
711 | --#debug drawDebugLine(node1X, node1Y, node1Z, 1, 1, 0, node2X, node2Y, node2Z, 1, 1, 0) |
712 | --#debug end |
713 | |
714 | if gAngle == gAngle and cAngle == cAngle then |
715 | automaticTilt.currentDelta = gAngle-cAngle |
716 | end |
717 | end |
718 | end |
719 | end |
750 | function Cutter:processCutterArea(workArea, dt) |
751 | local spec = self.spec_cutter |
752 | |
753 | if spec.workAreaParameters.combineVehicle ~= nil then |
754 | local xs,_,zs = getWorldTranslation(workArea.start) |
755 | local xw,_,zw = getWorldTranslation(workArea.width) |
756 | local xh,_,zh = getWorldTranslation(workArea.height) |
757 | |
758 | local lastRealArea = 0 |
759 | local lastThreshedArea = 0 |
760 | local lastArea = 0 |
761 | local fieldGroundSystem = g_currentMission.fieldGroundSystem |
762 | for _, fruitTypeIndex in ipairs(spec.workAreaParameters.fruitTypesToUse) do |
763 | local fruitTypeDesc = g_fruitTypeManager:getFruitTypeByIndex(fruitTypeIndex) |
764 | local chopperValue = fieldGroundSystem:getChopperTypeValue(fruitTypeDesc.chopperTypeIndex) |
765 | local realArea, area, sprayFactor, plowFactor, limeFactor, weedFactor, stubbleFactor, rollerFactor, beeYieldBonusPerc, growthState, _, terrainDetailPixelsSum = FSDensityMapUtil.cutFruitArea(fruitTypeIndex, xs,zs, xw,zw, xh,zh, true, spec.allowsForageGrowthState, chopperValue) |
766 | |
767 | if realArea > 0 then |
768 | if self.isServer then |
769 | if growthState ~= spec.currentGrowthState then |
770 | spec.currentGrowthStateTimer = spec.currentGrowthStateTimer + dt |
771 | if spec.currentGrowthStateTimer > 500 or spec.currentGrowthStateTime + 1000 < g_time then |
772 | spec.currentGrowthState = growthState |
773 | spec.currentGrowthStateTimer = 0 |
774 | end |
775 | else |
776 | spec.currentGrowthStateTimer = 0 |
777 | spec.currentGrowthStateTime = g_time |
778 | end |
779 | |
780 | if fruitTypeIndex ~= spec.currentInputFruitType then |
781 | spec.currentInputFruitType = fruitTypeIndex |
782 | |
783 | spec.currentOutputFillType = g_fruitTypeManager:getFillTypeIndexByFruitTypeIndex(spec.currentInputFruitType) |
784 | if spec.fruitTypeConverters[spec.currentInputFruitType] ~= nil then |
785 | spec.currentOutputFillType = spec.fruitTypeConverters[spec.currentInputFruitType].fillTypeIndex |
786 | spec.currentConversionFactor = spec.fruitTypeConverters[spec.currentInputFruitType].conversionFactor |
787 | end |
788 | |
789 | local cutHeight = g_fruitTypeManager:getCutHeightByFruitTypeIndex(fruitTypeIndex, spec.allowsForageGrowthState) |
790 | self:setCutterCutHeight(cutHeight) |
791 | end |
792 | |
793 | self:setTestAreaRequirements(fruitTypeIndex, nil, spec.allowsForageGrowthState) |
794 | |
795 | -- ai only works on terrain detail, so we do not allow the ai to require fruits that are out of a field |
796 | if terrainDetailPixelsSum > 0 then |
797 | spec.currentInputFruitTypeAI = fruitTypeIndex |
798 | end |
799 | spec.currentInputFillType = g_fruitTypeManager:getFillTypeIndexByFruitTypeIndex(fruitTypeIndex) |
800 | spec.useWindrow = false |
801 | end |
802 | |
803 | local multiplier = g_currentMission:getHarvestScaleMultiplier(fruitTypeIndex, sprayFactor, plowFactor, limeFactor, weedFactor, stubbleFactor, rollerFactor, beeYieldBonusPerc) |
804 | lastRealArea = realArea * multiplier |
805 | lastThreshedArea = realArea |
806 | lastArea = area |
807 | |
808 | spec.workAreaParameters.lastFruitType = fruitTypeIndex |
809 | spec.workAreaParameters.lastChopperValue = chopperValue |
810 | break |
811 | end |
812 | end |
813 | |
814 | if lastArea > 0 then |
815 | if workArea.chopperAreaIndex ~= nil and spec.workAreaParameters.lastChopperValue ~= nil then |
816 | local chopperWorkArea = self:getWorkAreaByIndex(workArea.chopperAreaIndex) |
817 | if chopperWorkArea ~= nil then |
818 | xs,_,zs = getWorldTranslation(chopperWorkArea.start) |
819 | xw,_,zw = getWorldTranslation(chopperWorkArea.width) |
820 | xh,_,zh = getWorldTranslation(chopperWorkArea.height) |
821 | |
822 | FSDensityMapUtil.setGroundTypeLayerArea(xs, zs, xw, zw, xh, zh, spec.workAreaParameters.lastChopperValue) |
823 | else |
824 | workArea.chopperAreaIndex = nil |
825 | Logging.xmlWarning(self.xmlFile, "Invalid chopperAreaIndex '%d' for workArea '%d'!", workArea.chopperAreaIndex, workArea.index) |
826 | end |
827 | end |
828 | |
829 | spec.stoneLastState = FSDensityMapUtil.getStoneArea(xs, zs, xw, zw, xh, zh) |
830 | spec.isWorking = true |
831 | end |
832 | |
833 | spec.workAreaParameters.lastRealArea = spec.workAreaParameters.lastRealArea + lastRealArea |
834 | spec.workAreaParameters.lastThreshedArea = spec.workAreaParameters.lastThreshedArea + lastThreshedArea |
835 | spec.workAreaParameters.lastStatsArea = spec.workAreaParameters.lastStatsArea + lastThreshedArea |
836 | spec.workAreaParameters.lastArea = spec.workAreaParameters.lastArea + lastArea |
837 | end |
838 | |
839 | return spec.workAreaParameters.lastRealArea, spec.workAreaParameters.lastArea |
840 | end |
844 | function Cutter:processPickupCutterArea(workArea, dt) |
845 | local spec = self.spec_cutter |
846 | |
847 | if spec.workAreaParameters.combineVehicle ~= nil then |
848 | local sx, sy, sz = getWorldTranslation(workArea.start) |
849 | local wx, wy, wz = getWorldTranslation(workArea.width) |
850 | local hx, hy, hz = getWorldTranslation(workArea.height) |
851 | |
852 | local lsx, lsy, lsz, lex, ley, lez, lineRadius = DensityMapHeightUtil.getLineByAreaDimensions(sx, sy, sz, wx, wy, wz, hx, hy, hz) |
853 | |
854 | for _, fruitType in ipairs(spec.workAreaParameters.fruitTypesToUse) do |
855 | local fillType = g_fruitTypeManager:getWindrowFillTypeIndexByFruitTypeIndex(fruitType) |
856 | if fillType ~= nil then |
857 | local pickedUpLiters = -DensityMapHeightUtil.tipToGroundAroundLine(self, -math.huge, fillType, lsx, lsy, lsz, lex, ley, lez, lineRadius, nil, nil, false, nil) |
858 | |
859 | if self.isServer then |
860 | if pickedUpLiters > 0 then |
861 | local fruitDesc = g_fruitTypeManager:getFruitTypeByIndex(fruitType) |
862 | local literPerSqm = fruitDesc.literPerSqm |
863 | local lastCutterArea = pickedUpLiters / (g_currentMission:getFruitPixelsToSqm() * literPerSqm) |
864 | |
865 | if fruitType ~= spec.currentInputFruitType then |
866 | spec.currentInputFruitType = fruitType |
867 | |
868 | spec.currentOutputFillType = g_fruitTypeManager:getFillTypeIndexByFruitTypeIndex(spec.currentInputFruitType) |
869 | if spec.fruitTypeConverters[spec.currentInputFruitType] ~= nil then |
870 | spec.currentOutputFillType = spec.fruitTypeConverters[spec.currentInputFruitType].fillTypeIndex |
871 | spec.currentConversionFactor = spec.fruitTypeConverters[spec.currentInputFruitType].conversionFactor |
872 | end |
873 | end |
874 | |
875 | spec.useWindrow = true |
876 | spec.currentInputFillType = fillType |
877 | spec.workAreaParameters.lastFruitType = fruitType |
878 | spec.workAreaParameters.lastRealArea = spec.workAreaParameters.lastRealArea + lastCutterArea |
879 | spec.workAreaParameters.lastThreshedArea = spec.workAreaParameters.lastThreshedArea + lastCutterArea |
880 | spec.workAreaParameters.lastStatsArea = spec.workAreaParameters.lastStatsArea + lastCutterArea |
881 | spec.workAreaParameters.lastArea = spec.workAreaParameters.lastArea + lastCutterArea |
882 | spec.stoneLastState = FSDensityMapUtil.getStoneArea(sx, sz, wx, wz, hx, hz) |
883 | spec.isWorking = true |
884 | break |
885 | end |
886 | end |
887 | end |
888 | end |
889 | end |
890 | |
891 | return spec.workAreaParameters.lastRealArea, spec.workAreaParameters.lastArea |
892 | end |