In this chapter, you will build upon your experience with programming, working with models and .i3d files and making simpler mods to make a more complex mod. This mod focuses on creating a mower with rotating blades (see Figure 6-1). Unlike the previous mod example where we made a static structure with a rotating sign, we will be making a moving vehicle with rotating elements that also affects foliage and other elements of the game environment.

Figure 6-1
A 3-d illustration includes a grass field and a tractor with a rotating mower. Several trees and a building are in the background.

The rotating mower is an excellent tool for clearing your fields

Technical Requirements

Like the previous chapter, you will be working entirely in the GIANTS Editor and Studio and must meet the requirements mentioned in the “Technical Requirements” section of Chapter 2, “Getting Started with the GIANTS Editor.” Make sure you are always using the most recent version of the GIANTS Editor. This will ensure that you are able to take advantage of any new features. You can find all the code and assets used in this chapter in the book’s code repository on the GDN at the following link:

A Q R code presents a pattern of pixelated patterns in a square. A concentric square is on 3 corners.

https://gdn.giants-software.com/lp/scriptingBook.php

Creating Mod Scripts

This section will explore all of the scripts necessary for this mod. We will start by looking at the .xml files and then cover the .lua files. Please see the “Preparing the Mod Folder Structure” section of Chapter 5, “Making a Diner with a Rotating Sign,” on how to set up a mod project and use the sample files provided on GDN.

Creating XML Files

Like in the previous chapters, we will need to create a modDesc.xml file. You should be familiar with the basic fields for setting a name, description, and icon for the mod. Let us now look at the contents of modDesc.xml:

<?xml version="1.0" encoding="utf-8" standalone="no" ?> <modDesc descVersion="72">       <Author>GIANTS Software</author>       <version>1.0.0.0</version>       <multiplayer supported="true" />       <title>             <en>Sample Mod - Rotate Mower</en>       </title>       <description>             <en>A sample mod</en>       </description>       <iconFilename>icon_rotateMower.png</iconFilename>       <materialHolders>             <materialHolder filename="effects/particles.i3d" />       </materialHolders>       <extraSourceFiles>             <sourceFile filename="scripts/events/RotorSpeedFactorEvent.lua"/>             <sourceFile filename="scripts/FSDensityMapUtilExtension.lua"/>       </extraSourceFiles>       <specializations>             <specialization name="rotateMower" className="RotateMower"                   filename="scripts/RotateMower.lua" />       </specializations>

In this first section of the file, we define basic information about our mod. The first new field we include is materialHolders which includes one materialHolder field. Note that you can add more materialHolder fields as needed when designing your own mods. This field is used to load a specific material or effect like particles that are then added to a global material or effect database and can be accessed via a script. Here, we reference our particles.i3d file which contains particle effects which will be used with our mower to create an exhaust effect. The path to our file is once again relative to the mod directory. Next, we reference some Lua files to be used in our mod using the extraSourceFiles field. The two scripts we are including are RotorSpeedFactorEvent.lua and FSDensityMapUtilExtension.lua. Lastly, we create a new rotateMower specialization that uses our RotateMower.lua file. We will explore all of these Lua scripts in the following “Creating Lua Files” section. Let us now cover the remaining content of modDesc.xml:

<vehicleTypes>       <type name="rotateMower" parent="baseAttachable"             filename="$dataS/scripts/vehicles/Vehicle.lua">             <specialization name="turnOnVehicle" />             <specialization name="groundReference" />             <specialization name="workArea" />             <specialization name="workParticles" />             <specialization name="rotateMower" />       </type> </vehicleTypes> <storeItems>       <storeItem xmlFilename="vehicle/rotateMower.xml"/> </storeItems> <actions>       <action name="CHANGE_ROTOR_SPEED" axisType="FULL" /> </actions> <inputBinding>       <actionBinding action="CHANGE_ROTOR_SPEED">             <binding device="KB_MOUSE_DEFAULT" input="KEY_n" axisComponent="-" />             <binding device="KB_MOUSE_DEFAULT" input="KEY_m" axisComponent="+" />       </actionBinding> </inputBinding> <l10n filenamePrefix="l10n/l10n" /> </modDesc>

In the second half of the file, we define some additional fields you have not seen previously.

The vehicleTypes field is used to define a specific feature set for a vehicle. The base class for our new rotateMower specialization is Vehicle.lua, and the additional features, such as the ability to attach the vehicle, are all of the baseAttachable type and its specializations.

We also provide functionality to turn the vehicle on or off via the turnOnVehicle specialization. To manipulate the foliage and other elements of the ground, we need support for work areas which use the workArea specialization. We want our mod to only work if it has ground contact, so we need to add the groundReference specialization. Support for particles will be added by using the workParticles specialization. Finally, we link our own rotateMower specialization.

Like before, we make our mod purchasable in game by using the storeItems field. In this field, we reference the rotateMower configuration file called rotateMower.xml which will define all of the information about the physical mower, much like the restaurant.xml file from Chapter 5, “Making a Diner with Rotating Element.” We will cover the contents of this file later in this section.

Another new field in this file is actions, which holds a list of action fields that can be triggered by the player. For our mod, we will define one input for the player which allows them to control the speed of the mower’s rotor. This action is defined as a FULL axis. The game supports half and full axes. FULL means that the action can return values between 1 and 1, while a HALF axis only returns values between 0 and 1. Typical use cases for a HALF axis include simple toggle actions like turning on or off something. In this case, you only want to get the button press. Use cases for a FULL axis include steering or, in our case, the speed control. We want to decrease and increase the speed using this one action. Both axis types support digital (e.g., keyboard) and analog bindings (e.g., joystick).

Next, the inputBinding field allows us to define inputs to control our previously defined actions. In our mod, we will bind the N key to decrease the rotor speed and the M key to increase the rotor speed. The device="KB_MOUSE_DEFAULT" field binds this action to available keyboards. KB_MOUSE_DEFAULT is a wildcard placeholder for all keyboards or mouses. There is also DEFAULT_GAMEPAD that links to all gamepads. There is also the option to link to a specific device with its device UUID, but this is not recommended in practice.

Lastly, we include the l10n field which allows us to provide translations for our mod so that players who speak different languages can still know the controls associated with the mower.

With modDesc.xml now defined, let us now cover the contents of rotateMower.xml and explore how the new fields interact with our mod:

<?xml version="1.0" encoding="utf-8" standalone="no" ?> <vehicle type="rotateMower" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://validation.gdn.giants-software.com/fs22/vehicle.xsd">       <annotation>             Copyright (C) GIANTS Software GmbH, All Rights Reserved.       </annotation>       <storeData>             <name>Rotate Mower</name>             <specs>                   <neededPower>40</neededPower>                   <workingWidth>2.4</workingWidth>             </specs>             <functions>                   <function>$l10n_function_mower</function>             </functions>             <image>vehicle/store_rotateMower.png</image>             <price>12000</price>             <lifetime>600</lifetime>             <rotation>0</rotation>             <brand>LIZARD</brand>             <category>mowers</category>             <shopTranslationOffset>0 0 0</shopTranslationOffset>             <shopRotationOffset>0 0 0</shopRotationOffset>             <vertexBufferMemoryUsage>0</vertexBufferMemoryUsage>             <indexBufferMemoryUsage>0</indexBufferMemoryUsage>             <textureMemoryUsage>0</textureMemoryUsage>             <instanceVertexBufferMemoryUsage>0</instanceVertexBufferMemoryUsage>             <instanceIndexBufferMemoryUsage>0</instanceIndexBufferMemoryUsage>       </storeData>

Most of this section follows from the “Creating XML Files” section of Chapter 5, “Making a Diner with Rotating Element.” Note that the vehicle schema can be found on the GDN at the following link. The vehicle.xsd provides all available elements that are allowed in a vehicle.xml file:

https://validation.gdn.giants-software.com/fs22/vehicle.xsd

You should already be familiar with fields like name, function, price, and several other fields present. We define a new field called specs which holds a neededPower and workingWidth field. The specs field is used to display information about an item in the shop. The two fields we’ve defined with it will be used to determine how much energy the mower consumes and the width of the area it cuts. You may have noticed that we set the brand field to LIZARD – LIZARD is a fictitious brand that many vehicles in Farming Simulator choose to use. Let us now look at more of the contents of rotateMower.xml:

<base>       <typeDesc>$l10n_typeDesc_mower</typeDesc>       <filename>vehicle/rotateMower.i3d</filename>       <size width="3.2" length="1.5" lengthOffset="0.1" />       <speedLimit value="20" />       <components>             <component centerOfMass="0 0.2 0" solverIterationCount="10" mass="440" />       </components>       <schemaOverlay attacherJointPosition="0 0" name="IMPLEMENT" />       <mapHotspot type="TOOL" /> </base> <powerConsumer ptoRpm="470" neededMinPtoPower="10"       neededMaxPtoPower="15"/> <groundReferenceNodes>       <groundReferenceNode node="groundRefNode" threshold="0.2" /> </groundReferenceNodes> <workAreas>       <workArea type="rotateMower" functionName="processRotateMowerArea"                   disableBackwards="false" >             <area startNode="workAreaStart" widthNode="workAreaWidth"                   heightNode="workAreaHeight" />             <groundReferenceNode index="1" />             <onlyActiveWhenLowered value="true"/>       </workArea> </workAreas >

In this portion of the file, we create a base field which holds basic information about the mower. Note how we reference the rotateMower.i3d as the file for the mower model, a speed limit for the mower to move at, and some information for how the model should be handled, including its mass, center of mass, and the overall size of the vehicle which is used to make sure the mower spawns correctly.

Next, we define a powerConsumer field so that the mower consumes engine power from its tractor. The groundReferenceNodes field defines reference points to see if the mower is currently touching the ground. The workAreas field holds workArea fields which define the area that is affected by the mower when it is active. Note that we use one of the ground reference nodes previously defined and specify mowing should only happen when the mower is lowered. Next, we will look at more of the file’s contents:

<attachable>       <inputAttacherJoints>             <inputAttacherJoint node="attacherJoint" jointType="implement"                         topReferenceNode="topReferenceNode" upperRotationOffset="10"                         lowerRotLimitScale="0 0 0" lowerTransLimitScale="0 1 0">                   <distanceToGround lower="0.35" upper="1.0" />             </inputAttacherJoint>       </inputAttacherJoints>       <support animationName="moveSupport" /> </attachable>

In this section, we define an attachment point where the mower utility will connect to the tractor. To do this, we include an attachable field which holds an inputAttacherJoints field, a list of inputAttacherJoint fields. The inputAttacherJoint field includes configurations for how the mower should attach to the tractor. The node attribute defines the position of the physics joint that connects the mower and the tractor – we also use the i3d-mapping for all i3d reference to avoid the more complex and user-unfriendly i3d paths like 0>0|0|0.

The topReferenceNode defines the position for the top bar of the three-point hitch. The jointType defines a preset for the physics joint – in our case, we use implement. By default, the script lowers and lifts the tools parallel to the ground. We want our mower to be a bit tilted when raised, so we add upperRotationOffset="10". This causes the script to tilt our tool by 10 degrees when lifted. The lowerRotLimitScale and lowerTransLimitScale fields scale the joint limits.

Joint limits define the possible free (controlled by gravity and external impacts) movement of a joint for translation and rotation. The distanceToGround element defines the offsets of the tool when lifted or lowered. Lastly, the support element in our case defines an animation that is played when the mower is detached and played in reverse while the mower is attached.

<powerTakeOffs>       <input inputAttacherJointIndices="1" inputNode="ptoInputNode"             aboveAttacher="true" /> </powerTakeOffs> <lights>       <defaultLights>             <defaultLight shaderNode="drum01Knife" lightTypes="0" intensity="300"/>             <defaultLight shaderNode="drum02Knife" lightTypes="0" intensity="300"/>             <defaultLight shaderNode="drum03Knife" lightTypes="0" intensity="300"/>             <defaultLight shaderNode="drum04Knife" lightTypes="0" intensity="300"/>       </defaultLights> </lights>

The inclusion of the powerTakeOffs field determines whether a PTO can be attached to the tractor. For clarity, a PTO is a power shaft that transfers the tractor’s engine power to the tool. Next, we define the blades of the mower to be shader nodes if the lights on it are turned on by using a lights field. For each blade, we define a new defaultLight in a defaultLights field and reference the physical shader elements by name. The lightType 0 defines that the lights should be activated with the default light. There are other light types such as 1, 2, or 3 – all of them are for special light scenarios like frontLight, workLight, etc. They also depend on the tractor the tool is attached to.

<ai>       <needsLowering value="true" />       <areaMarkers leftNode="aiMarkerLeft" rightNode="aiMarkerRight"             backNode="aiMarkerBack" />       <collisionTrigger node="aiCollisionNode" width="2.9" height="1.2"/>       <agentAttachment width="2.3" height="1.2" length="1.2" lengthOffset="0.15"/> </ai> <turnOnVehicle turnOffIfNotAllowed="true"             turnOffText="$l10n_action_turnOffMower"             turnOnText="$l10n_action_turnOnMower" /> <foliageBending>       <bendingNode minX="-1.4" maxX="1.4" minZ="-0.373" maxZ="0.7" yOffset="0.2"/> </foliageBending> <wearable wearDuration="480" workMultiplier="5" fieldMultiplier="2"/> <washable dirtDuration="90" washDuration="1" workMultiplier="4"       fieldMultiplier="2"/>

We also define an ai field, which dictates how AI vehicles and other agents should interact with our mower. For our mod, we define a collisionTrigger which is used by the AI tractor that uses our mower to detect other vehicles and objects in the world and also forces other AI vehicles to stop if they are near the mower. The AI area markers define the cut area of the mower used by the AI system to calculate the routes on the field it has to drive along. The agentAttachment element is used by the street AI system to calculate the correct route if you send a tractor with attached mower to a field or back to the farm. Next, the turnOnVehicle field allows us to display custom text when the mower is turned on or off. Note how we reference the translation .xml files from earlier in this field. We will need to define how foliage behaves when our mower interacts with it. We accomplish this by using the foliageBending field and including a bendingNode with configurations for how the foliage model should deform. As we use the mower, it will become dirty and see the effects of wear and tear. To reflect this, we create wearable fields which set the appearance of these environmental effects. With these general elements now included, we will define our custom element for the rotateMower in the file:

<rotateMower>       <animationNodes>             <animationNode node="drum01" rotSpeed="1000"  rotAxis="2"                   turnOnFadeTime="2.5" turnOffFadeTime="2"                   speedFunc="getRotorSpeedFactor"/>             <animationNode node="drum02" rotSpeed="-1000" rotAxis="2"                   turnOnFadeTime="2.5" turnOffFadeTime="2"                   speedFunc="getRotorSpeedFactor"/>             <animationNode node="drum03" rotSpeed="1000"  rotAxis="2"                   turnOnFadeTime="2.5" turnOffFadeTime="2"                   speedFunc="getRotorSpeedFactor"/>             <animationNode node="drum04" rotSpeed="-1000" rotAxis="2"                   turnOnFadeTime="2.5" turnOffFadeTime="2"                   speedFunc="getRotorSpeedFactor"/>       </animationNodes>       <effects>             <effectNode effectClass="ParticleEffect" effectNode="smokeEmitter"                   particleType="SMOKE" worldSpace="true" />       </effects>

In our rotateMower element, we first define animationNode fields in an animationNodes container. Note that like other nodes and points of reference, these are already physically part of the mower model, and we refer to them by name. Two of the configurations we include in these elements are turnOnFadeTime and turnOffFadeTime which are used to let the blades speed up and slow down when the mower is turned on and off. The rotSpeed defines the rotation speed of the drum, and the rotAxis with value 2 defines that the object will rotate around its local Y axis. Next, we add a smoky particle effect for the dirt and exhaust from the mower by including an effects element with an effectNode field. The game supports different effect classes. We want to spawn a particle effect, so we need to set the value of effectClass to ParticleEffect. Also, notice that the particleType field is set to SMOKE, which directly connects to particle.i3d which holds our materials. In this material holder, we define a real particle system with user attributes and set particleType to SMOKE. Thus, the system can access the material holder and clone the defined particle system to be used in our rotateMower. Let’s continue through the components of the rotateMower field:

<sounds>       <start file="sounds/rotor_start.wav" innerRadius="5.0" outerRadius="65.0"                   fadeOut="0.1" linkNode="rotateMower_main_component1">             <volume indoor="0.45" outdoor="1.1">                   <modifier type="ROTOR_RPM" value="0.00" modifiedValue="0.70" />                   <modifier type="ROTOR_RPM" value="1.00" modifiedValue="1.00" />             </volume>             <pitch indoor="1.00" outdoor="1">                   <modifier type="ROTOR_RPM" value="0.00" modifiedValue="0.50" />                   <modifier type="ROTOR_RPM" value="1.00" modifiedValue="1.0" />             </pitch>             <lowpassGain indoor="0.50" outdoor="1.00" />       </start>       <work file="sounds/rotor_work_loop.wav" innerRadius="5.0" outerRadius="65.0"                   fadeOut="0.1" linkNode="rotateMower_main_component1" > ...       </work>       <stop file="sounds/rotor_stop.wav" innerRadius="5.0" outerRadius="650.0"                   fadeOut="0.1"> ...       </stop> </sounds> </rotateMower>

We use this section of the rotateMower element to define the sounds it should use and how different mower actions affect these sounds. We start by creating a sounds element which includes start, work, and stop fields. Each of these fields references one sound which is used when the mower starts, is working, and when it stops. Importantly, as rotor revolutions per minute (RPM) increases, we want to increase the volume and pitch of the sound, which we achieve by using pitch and volume fields. Note that we can add filters to these sounds and change their behavior depending on whether the player’s perspective is inside the cabin of a tractor or outdoors. This concludes the elements of the rotateMower field. Let us continue through the remaining contents of rotateMower.xml:

      <i3dMappings>             <i3dMapping id="rotateMower_main_component1" node="0>" />             <i3dMapping id="rotateMower_vis" node="0>0" />             <i3dMapping id="attacherJoint" node="0>0|0|0" />             <i3dMapping id="topReferenceNode" node="0>0|0|1" />             <i3dMapping id="ptoInputNode" node="0>0|0|2" /> ...       </i3dMappings> </vehicle>

We conclude our file with an i3dMappings field like we defined for our diner model in Chapter 5, “Making a Diner with Rotating Element.”

With modDesc.xml and rotateMower.xml complete, we only need to create some supporting files before we jump into creating our Lua scripts. Earlier, we referenced two .xml files (l10n_de.xml and l10n_en.xml) in the l10n section of modDesc.xml that let us display custom text to players in different languages. We will start with l10n_en.xml which displays the controls for the mower to the player in English:

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?> <l10n>       <elements>             <e k="input_CHANGE_ROTOR_SPEED_1" v="Decrease Rotor Speed"/>             <e k="input_CHANGE_ROTOR_SPEED_2" v="Increase Rotor Speed"/>             <e k="input_CHANGE_ROTOR_SPEED" v="Change Rotor Speed (%d%%)"/>             <e k="action_turnOffMower" v="Turn off mower"/>             <e k="action_turnOnMower" v="Turn on mower"/>       </elements> </l10n>

In this file, we reference the custom actions we created in modDesc.xml by name and associate text with each input. We can do the same in German so that German-speaking players can more easily engage with the mod. Let us look at the contents of l10n_de.xml:

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?> <l10n>       <elements>             <e k="input_CHANGE_ROTOR_SPEED_1" v="Rotorgeschwindigkeit senken"/>             <e k="input_CHANGE_ROTOR_SPEED_2" v="Rotorgeschwindigkeit erhöhen"/>             <e k="input_CHANGE_ROTOR_SPEED"                   v="Rotorgeschwindigkeit anpassen (%d%%)"/>             <e k="action_turnOffMower" v="Mähwerk ausschalten"/>             <e k="action_turnOnMower" v="Mähwerk anschalten"/>       </elements> </l10n>

You can add support for additional languages by creating a new file with the translations and appropriate suffix. For example, to add support for French, create a new file called l10n_fr.xml. With these files created, we have finished making all of the .xml files for our mod! This is a good point to review everything you have created so far and double-check your understanding before we jump into creating our Lua files and bringing our mower to life.

Creating Lua Files

We’re now ready to create our Lua files – we will start with RotorSpeedFactorEvent.lua. The purpose of this script is to replicate a user’s input to other players over the network if they are playing in multiplayer mode. Let us now examine the contents of the script:

RotorSpeedFactorEvent = {} local RotorSpeedFactorEvent_mt = Class(RotorSpeedFactorEvent, Event) InitEventClass(RotorSpeedFactorEvent, "RotorSpeedFactorEvent") function RotorSpeedFactorEvent.emptyNew()       local self = Event.new(RotorSpeedFactorEvent_mt)       return self end function RotorSpeedFactorEvent.new(vehicle, speedFactor)       local self = RotorSpeedFactorEvent.emptyNew()       self.vehicle = vehicle       self.speedFactor = speedFactor       return self end

We start by creating a new table and using the Event base class to create our new RotorSpeedFactorEvent, a subclass of Event. Next, we define two constructor functions: RotorSpeedFactorEvent.emptyNew() and RotorSpeedFactorEvent.new(). The first constructor takes no arguments and creates an empty event for later use. The second constructor is passed vehicle and speedFactor arguments which reference the vehicle the event is for and the current speed of the rotor, respectively. With our constructors defined, we can begin to implement the main functionality of this event:

function RotorSpeedFactorEvent:writeStream(streamId, connection)       NetworkUtil.writeNodeObject(streamId, self.vehicle)       RotateMower.streamWriteSpeedFactor(streamId, self.speedFactor) End function RotorSpeedFactorEvent:readStream(streamId, connection)       self.vehicle = NetworkUtil.readNodeObject(streamId)       self.speedFactor = RotateMower.streamReadSpeedFactor(streamId)       self:run(connection) end

The writeStream() function writes the event data to the network stream. That is, it communicates the information about the event to all players. This function is largely for internal use but note that we send a signal to the network to update the speedFactor of the mower’s rotors in this function.

We cannot directly send the object reference of a vehicle over the network as it could be different on different PCs connected to the game. Instead, the network creates a mapping of the local vehicle reference and a unique ID (integer). The writeNodeObject() function of NetworkUtil simply gets the network id of the passed vehicle object and writes that integer to the network stream.

Once we have written event information to the stream, we need to be able to read it – we do this through the readStream() function. A strict requirement is that the call order is the same for read and write; otherwise, the network protocol stack will be broken. You can see how the network stream looks like in Figure 6-2.

Figure 6-2
An illustration presents the network stream, which includes other network data, vehicle network i d, speed factor, and other network data. Each has a different packet size.

This figure visualizes the network stream

This function is again mostly for internal purposes, but like the writeStream() function, we must use the readNodeObject() function of NetworkUtil to remap the network ID back to a local vehicle object reference before we update the speedFactor field of the class and call the run() function to update the vehicle itself. The signal to perform this action was sent out at an earlier point in time by the writeStream() function.

function RotorSpeedFactorEvent:run(connection)       if not connection:getIsServer() then             g_server:broadcastEvent(self, false, connection, self.vehicle)       end       if self.vehicle ~= nil then             self.vehicle:setRotorSpeedFactor(self.speedFactor, true)       end end

The run() function is where we execute the main physical changes as a result of the event. In our case, we need to update the vehicle to reflect the values set for speedFactor in our class. If a player (client) requested the change in motor speed via our custom action, we need to tell the server to replicate this action and broadcast the information to all other players in the game. Note that the network does not allow the client to directly tell the server or other users which behavior should be occurring for security purposes.

function RotorSpeedFactorEvent.sendEvent(vehicle, speedFactor, noEventSend)       if noEventSend == nil or noEventSend == false then             if g_server ~= nil then                  g_server:broadcastEvent(RotorSpeedFactorEvent.new(vehicle, speedFactor),                               nil, nil, vehicle)             else                   g_client:getServerConnection():sendEvent(                               RotorSpeedFactorEvent.new(vehicle, speedFactor))             end       end end

Lastly, we define the static helper function sendEvent() which is used by both clients and the server to perform replication. For example, the client can tell the server to replicate their action by sending this event. Additionally, the server can use this function to perform the client’s request and replicate the change to other players from the server.

With RotorSpeedFactorEvent.lua completed, we will need to define an extension to an existing utility so that the mower can affect foliage it’s used on. The GIANTS Engine uses what’s called a density map to define where foliage is present. The purpose of this utility is to let our mod modify this density map based on the physical properties of the mower. Now we will cover the contents of FSDensityMapUtilExtension.lua:

function FSDensityMapUtil.updateRotateMowerArea(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ)       local functionData = FSDensityMapUtil.functionCache.updateRotateMowerArea       if functionData == nil then             local terrainRootNode = g_currentMission.terrainRootNode             functionData = {}             functionData.lastArea = 0             functionData.lastTotalArea = 0             local multiModifier = DensityMapMultiModifier.new()             local modifier, filter             for _, desc in pairs(g_fruitTypeManager:getFruitTypes()) do                   if desc.terrainDataPlaneId ~= nil then                         if modifier == nil then                               modifier = DensityMapModifier.new(desc.terrainDataPlaneId,                                     desc.startStateChannel, desc.numStateChannels, terrainRootNode)                         else                               modifier:resetDensityMapAndChannels(desc.terrainDataPlaneId,                                     desc.startStateChannel, desc.numStateChannels)                         end                         if filter == nil then                               filter = DensityMapFilter.new(desc.terrainDataPlaneId,                                     desc.startStateChannel, desc.numStateChannels, terrainRootNode)                         else                               filter:resetDensityMapAndChannels(desc.terrainDataPlaneId,                                     desc.startStateChannel, desc.numStateChannels)                         end                         filter:setValueCompareParams(DensityValueCompareType.BETWEEN, 2,                               desc.cutState)                         multiModifier:addExecuteSet(desc.cutState or 0, modifier, filter)                   end             end

The program begins by defining a new function, updateRotateMowerArea(). This function will modify the foliage density map within a box as defined by the function’s six arguments. We want to make multiple modifications to the foliage density map so we will need to create a DensityMapMultiModifier. For each type of foliage we want to affect, there is a different density map.

To modify these density maps, a new DensityMapModifier object must be created with the appropriate DensityMap ID and its value range (startStateChannel and numStateChannels). The terrainRootNode passed during the modifier’s construction is used to calculate the affected range of pixels. For example, different density maps could have different sizes – the system must know the relationship between these sizes, so we use the terrainRootNode to calculate these. To save on performance, we only want to create the modifier once and cache (store) it.

We do not want all types of foliage to be affected by the mower, so we must create a new DensityMapFilter object for each type of mowable item. Like the density map modifier, we will also want to cache our filter. With the modifier and filters created, we call the addExecuteSet function of the DensityMapMultiModifier object we previously defined which will use our modifier and filter objects to update the foliage to a cut state. This is the main component of our extension on this utility. Let us now explore the rest of the script:

local weedSystem = g_currentMission.weedSystem if weedSystem ~= nil then       local weedTerrainDataPlaneId, weedStartChannel, weedNumChannels = weedSystem:getDensityMapData()       modifier:resetDensityMapAndChannels(weedTerrainDataPlaneId, weedStartChannel, weedNumChannels)       filter:resetDensityMapAndChannels(weedTerrainDataPlaneId, weedStartChannel, weedNumChannels)       filter:setValueCompareParams(DensityValueCompareType.GREATER, 2)       multiModifier:addExecuteSet(0, modifier, filter) end

Weeds are kept in their own density map separate from the types of foliage we handled previously for our mower to affect. Like before, we will use our modifier and filter objects and update them to focus on the weed foliage layer. Then, using the density map multimodifier, we will update weeds under the mower to a cut state.

if g_currentMission.foliageSystem ~= nil then       local decoFoliages = g_currentMission.foliageSystem:getDecoFoliages()       local grassDesc = g_fruitTypeManager:getFruitTypeByIndex(FruitType.GRASS)       for index, decoFoliage in pairs(decoFoliages) do             if decoFoliage.terrainDataPlaneId ~= nil then                   -- reset the data plane and channels                   modifier:resetDensityMapAndChannels(grassDesc.terrainDataPlaneId,                         grassDesc.startStateChannel, grassDesc.numStateChannels)                   filter:resetDensityMapAndChannels(decoFoliage.terrainDataPlaneId,                         decoFoliage.startStateChannel, decoFoliage.numStateChannels)                   -- limit to visible deco foliage only                   filter:setValueCompareParams(DensityValueCompareType.GREATER, 0)                   -- execute the modifier for data pixels that -- match the filter                   multiModifier:addExecuteSet(grassDesc.cutState, modifier, filter)             end       end end functionData.multiModifier = multiModifier FSDensityMapUtil.functionCache.updateRotateMowerArea = functionData end

Bushes are kept in their own foliage layer separate from the weeds or crops, and we will need to perform a similar process for them to also be cut. For all types of these Deco Foliages, we will once again use our modifier and filter objects to target their respective layers and update them to a cut state via our density map multimodifier.

      DensityMapHeightUtil.clearArea(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ)       local multiModifier = functionData.multiModifier       multiModifier:updateParallelogramWorldCoords(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, DensityCoordType.POINT_POINT_POINT)       multiModifier:execute(false) end

Next, we clear the area by calling the clearArea() function of DensityMapHeightUtil. This function removes everything that was forced to the ground in the area, such as straw or wheat. If a player mowed over some wheat or stones on the ground, the clearArea() function will remove those items. The updateParallelogramWorldCoords() function will update the coordinates of the area that should be affected by the execution of the multiModifier. Lastly, we execute the multiModifier which will run all of the operations we define in the main if statement of the function.

With the additions made to the density map utility, we are left with only RotateMower.lua, the main control script for our mower. Let us now look at the script’s contents:

local modName = g_currentModName RotateMower = {} RotateMower.SPEC_TABLE_NAME = "spec_"..modName..".rotateMower" RotateMower.MIN_SPEED_FACTOR = 0.1 RotateMower.MAX_SPEED_FACTOR = 2 RotateMower.STEP_SIZE = 0.1 RotateMower.NUM_BITS = 5 function RotateMower.streamWriteSpeedFactor(streamId, rotorSpeedFactor)       streamWriteUIntN(streamId, math.floor((rotorSpeedFactor * 10) + 0.5),                   RotateMower.NUM_BITS) end function RotateMower.streamReadSpeedFactor(streamId)       -- read the speed factor from the network stream as -- an integer       local speedFactor = streamReadUIntN(streamId, RotateMower.NUM_BITS)       -- convert it back to float       local rotorSpeedFactor = speedFactor / 10       return rotorSpeedFactor end g_particleSystemManager:addParticleType("smoke")

This first part of our script creates the table for our specialization and adds five values to it. The first value we added defines our namespace like we have done in all previous specializations. The next two values define the min and max speed factors for the range of mower rotor speeds. The STEP_SIZE value defines the rate of change in the mower’s rotor speed when the player makes an input. Lastly, NUM_BITS is used internally to represent a range of speed values; in this case, we will not need more than 5 bits if we treat our number as an unsigned integer. The first function we implement is streamWriteSpeedFactor() which is used in the network portion (writeStream()) of our scripts to write the speedFactor in a simplified way. Normally, the speedFactor is stored as a float. That means we would need 32 bits to send this data over the network. Using this function, we simply convert speedFactor into an integer with a range limited to the minimum and maximum values we have previously defined.

This way, we use only 5 bits to send the value without any floating-point data. Next, we define streamReadSpeedFactor() which reads from the stream the value that the mower’s rotor speedFactor should be. We use the network utility to automatically convert the unsigned integer back into a Lua number.

Lastly, we register the mower’s particle effect to the type defined by the attributes in our .i3d material holder with the particle system manager utility. Let us continue through the contents of the script:

function RotateMower.prerequisitesPresent(specializations)       return SpecializationUtil.hasSpecialization(TurnOnVehicle, specializations) end

Like in the previous mod, we must ensure that the prerequisite specializations for the mod have been loaded before we try to use them. Attempting to use them before they have loaded will result in an error. To do this, we add the prerequisitesPresent() function which uses the internal SpecializationUtil to ensure all the specializations used by the mod have been loaded. In this case, TurnOnVehicle must be loaded.

function RotateMower.registerEventListeners(vehicleType)       SpecializationUtil.registerEventListener(vehicleType, "onLoad", RotateMower)       SpecializationUtil.registerEventListener(vehicleType, "onDelete", RotateMower)       SpecializationUtil.registerEventListener(vehicleType, "onReadStream", RotateMower)       SpecializationUtil.registerEventListener(vehicleType, "onWriteStream", RotateMower)       SpecializationUtil.registerEventListener(vehicleType, "onUpdateTick", RotateMower)       SpecializationUtil.registerEventListener(vehicleType, "onRegisterActionEvents", RotateMower)       SpecializationUtil.registerEventListener(vehicleType, "onTurnedOn", RotateMower)       SpecializationUtil.registerEventListener(vehicleType, "onTurnedOff", RotateMower) end

The registerEventListeners() function is a very important function of our specialization as it registers the events associated with player input to our functions which update mower attributes that should be triggered by the base vehicle script. Events for both the custom functions we defined as well as default events associated with the base Vehicle class specialization are registered in this function. More specifically, it forces the vehicle script to call some of the Vehicle base class events, such as onLoad() when the vehicle is loaded and onDelete() where the vehicle is removed from the game. Additionally, onReadStream() and onWriteStream() if a player joins a multiplayer game. The onUpdateTick() function will be called with a more or less constant tick rate for 30 FPS (~33 ms), and onRegisterActionEvents() will be called if the input context changes, such as it would if the player gets in or out of a tractor. The function will also register a listener to the onTurnedOn and onTurnedOff events defined by the TurnOnVehicle specialization, allowing us to be notified if the mower is turned on or off.

function RotateMower.registerFunctions(vehicleType)       SpecializationUtil.registerFunction(vehicleType, "getRotorSpeedFactor", RotateMower.getRotorSpeedFactor)       SpecializationUtil.registerFunction(vehicleType, "setRotorSpeedFactor",                   RotateMower.setRotorSpeedFactor)       SpecializationUtil.registerFunction(vehicleType, "getRotorSpeedScale",                   RotateMower.getRotorSpeedScale)       SpecializationUtil.registerFunction(vehicleType, "processRotateMowerArea",                   RotateMower.processRotateMowerArea) end

The registerFunctions() function associates our custom defined functions in our specialization with the vehicle type. This is required because the functions are custom and will not be an included functionality of the vehicle by default.

function RotateMower.registerOverwrittenFunctions(vehicleType)       SpecializationUtil.registerOverwrittenFunction(vehicleType, "getRawSpeedLimit",                   RotateMower.getRawSpeedLimit)       SpecializationUtil.registerOverwrittenFunction(vehicleType, "doCheckSpeedLimit",                   RotateMower.doCheckSpeedLimit) end

Lastly, the registerOverwrittenFunctions() function will overwrite the inherited functions from the vehicle specialization with those we have redefined for our vehicle. With these functions implemented, let us continue through the contents of the file:

function RotateMower.initSpecialization()       g_workAreaTypeManager:addWorkAreaType("rotateMower", false)       local schema = Vehicle.xmlSchema       schema:setXMLSpecializationType("RotateMower")       AnimationManager.registerAnimationNodesXMLPaths(schema,                   "vehicle.rotateMower.animationNodes")       EffectManager.registerEffectXMLPaths(schema, "vehicle.rotateMower.effects")       SoundManager.registerSampleXMLPaths(schema,                   "vehicle.rotateMower.sounds", "start")       SoundManager.registerSampleXMLPaths(schema,                   "vehicle.rotateMower.sounds", "stop")       SoundManager.registerSampleXMLPaths(schema,                   "vehicle.rotateMower.sounds", "work")       schema:setXMLSpecializationType()       local schemaSavegame = Vehicle.xmlSchemaSavegame       schemaSavegame:register(XMLValueType.FLOAT,                   "vehicles.vehicle(?)."..modName..".rotateMower#rotorSpeedFactor",                    "Current rotor speed factor") end

initSpecialization() is a function that loads and registers relevant information from our .xml files with our vehicle and Lua script. Particularly, it adds the xml-element-paths from the .xml files for the mower’s sounds, its effects, and animation nodes. We also want the state of the mower to be loaded in from when the player last played the game. This is used at the end of the function where we load the saved rotor speed factor:

function RotateMower:onLoad(savegame)       local spec = self[RotateMower.SPEC_TABLE_NAME]       spec.rotorSpeedFactor = 1       spec.isEffectDirty = false       if self.isClient then             spec.animationNodes = g_animationManager:loadAnimations(self.xmlFile,                         "vehicle.rotateMower.animationNodes", self.components,                         self, self.i3dMappings)             spec.effects = g_effectManager:loadEffect(self.xmlFile,                         "vehicle.rotateMower.effects", self.components, self, self.i3dMappings)             for _, effect in ipairs(spec.effects) do                   effect.currentFillType = nil             end             g_effectManager:setFillType(spec.effects, FillType.UNKNOWN)             for _, effect in ipairs(spec.effects) do                   effect.defaultSpeed = ParticleUtil.getParticleSystemSpeed(effect.particleSystem)             end             spec.samples = {}             spec.samples.start = g_soundManager:loadSampleFromXML(self.xmlFile,                         "vehicle.rotateMower.sounds", "start", self.baseDirectory,                         self.components, 1, AudioGroup.VEHICLE, self.i3dMappings, self)             spec.samples.stop  = g_soundManager:loadSampleFromXML(self.xmlFile,                         "vehicle.rotateMower.sounds", "stop", self.baseDirectory,                         self.components, 1, AudioGroup.VEHICLE, self.i3dMappings, self)             spec.samples.work  = g_soundManager:loadSampleFromXML(self.xmlFile,                         "vehicle.rotateMower.sounds", "work", self.baseDirectory,                         self.components, 0, AudioGroup.VEHICLE, self.i3dMappings, self)             end if savegame ~= nil then       local rotKey = savegame.key.."."..modName..".rotateMower#rotorSpeedFactor"       local rotorSpeedFactor = savegame.xmlFile:getValue(rotKey)       if rotorSpeedFactor ~= nil then             self:setRotorSpeedFactor(rotorSpeedFactor, true)       end end if self.addAIGroundTypeRequirements ~= nil then       self:addAIGroundTypeRequirements(Mulcher.AI_REQUIRED_GROUND_TYPES) end if self.addAIFruitRequirement ~= nil then       self:clearAIFruitRequirements()       for _, fruitType in ipairs(g_fruitTypeManager:getFruitTypes()) do             self:addAIFruitRequirement(fruitType.index, 2, fruitType.cutState-1)       end       local weedSystem = g_currentMission.weedSystem       if weedSystem ~= nil then       local weedTerrainDataPlaneId, weedStartChannel, weedNumChannels =                   weedSystem:getDensityMapData()             local factors = weedSystem:getFactors()             local minFactor = math.huge             local maxFactor = 0             for state, _ in pairs(factors) do                   minFactor = math.min(state, minFactor)                   maxFactor = math.max(state, maxFactor)             end             self:addAIFruitRequirement(nil, minFactor, maxFactor,                         weedTerrainDataPlaneId, weedStartChannel, weedNumChannels)             end       end end

The onLoad() function first references the namespace of our specialization and defines our rotorSpeedFactor field. This field is what will actually be modified when the updating functions we implemented earlier are called. We additionally include isEffectDirty as a field which we use to determine whether the effect should be updated.

Next, we load the mower’s animations and add its effects. We must also set the currentFillType field of each effect to nil manually for them to appear. With the effects loaded, we want to cache the default particle speed of each effect as a point of reference as we will be changing it as the mower operates. Following this, we will load the three sounds for the mower and store them so that they can be easily referenced later.

If the game is being loaded from a save file, we will want to set the attributes of the mower to those that were saved previously. The savegame.xmlFile field holds a reference to the savegame instance’s vehicle.xml file and savegame.key in the xml element of the current vehicle. This allows us to easily read saved data for our mower.

Next, we will want to confine the mower to fields if it is operating autonomously as part of the field worker functionality. Furthermore, we want the worker to only move after fruits and weeds rather than all crops in a field area. Note that we also consider the cut state of the crop so that we do not revisit field areas which have already been mowed. We will now look at more of the script’s contents:

function RotateMower:onDelete()       local spec = self[RotateMower.SPEC_TABLE_NAME]       g_animationManager:deleteAnimations(spec.animationNodes)       g_effectManager:deleteEffects(spec.effects)       g_soundManager:deleteSamples(spec.samples) end

The onDelete() function manages the case where the mower is deleted in the scope of our specialization. In particular, we do not want a memory leak to occur, so we delete the loaded animations, effects, and sounds.

function RotateMower:saveToXMLFile(xmlFile, key, usedModNames)       xmlFile:setValue(key .. "#rotorSpeedFactor", self:getRotorSpeedFactor()) end

The saveToXMLFile() function will save the state of the mower (more specifically the rotorSpeedFactor) to an .xml file so that it can be loaded in when the player next joins the game.

function RotateMower:onReadStream(streamId, connection)       local rotorSpeedFactor = RotateMower.streamReadSpeedFactor(streamId)       self:setRotorSpeedFactor(rotorSpeedFactor, true) end

The onReadStream() function is used to synchronize the current speed factor with new players who join the game by way of the setRotorSpeedFactor() function. We will implement the latter function later in this section.

function RotateMower:onWriteStream(streamId, connection)       local spec = self[RotateMower.SPEC_TABLE_NAME]       RotateMower.streamWriteSpeedFactor(streamId, spec.rotorSpeedFactor) end

Similarly, the onWriteStream() function is called on the server if a player joins the game to sync the speed factor. Let us continue through the file:

function RotateMower:onUpdateTick(dt, isActiveForInput,                               isActiveForInputIgnoreSelection, isSelected)       if self.isClient then             if self:getIsTurnedOn() then                   local spec = self[RotateMower.SPEC_TABLE_NAME]                   if spec.isEffectDirty then                         local scale = MathUtil.lerp(0.05, 1, self:getRotorSpeedScale())                         for _, effect in ipairs(spec.effects) do                               ParticleUtil.setEmitCountScale(effect.particleSystem, scale + 2 * scale)                               ParticleUtil.setParticleSystemSpeed(effect.particleSystem,                                     effect.defaultSpeed * scale)                         end                         spec.isEffectDirty = false                   end                   local workArea = self:getWorkAreaByIndex(1)                   if workArea ~= nil and workArea.requiresGroundContact then                         local hasGroundContact = workArea.groundReferenceNode ~= nil and                         workArea.groundReferenceNode.isActive                         if hasGroundContact then                               g_effectManager:startEffects(spec.effects)                         else                               g_effectManager:stopEffects(spec.effects)                         end                   end             end       end end

The first function of this section of the script is onUpdateTick(). This function is used to make frequent checks regarding the mower’s state and changes that should occur as the mower operates. We first check if the mower is turned on and that the isEffectDirty flag is true – if so, we update the speed of the particles based on the current rotorSpeedScale and either enable or disable the particle effects based on whether the mower attachment is lowered. The value range for rotor speed scale is 0–1, but we always want to spawn a few particles. So, if the speed scale is 0, we use the MathUtil.lerp() function to bring the value to a new range (0.05 to 1). We then use this scale value for particle speed and emit count.

function RotateMower:getRawSpeedLimit(superFunc)       local speedLimit = superFunc(self)       if self:getIsTurnedOn() and (self.getIsLowered == nil or self:getIsLowered()) then             local scale = MathUtil.lerp(0.05, 1, self:getRotorSpeedScale())             speedLimit = speedLimit * scale       end       return speedLimit end

Next, the getRawSpeedLimit() function gets the current speed limit of the vehicle. This function is needed as we want the vehicle’s maximum speed to change based on whether it is currently mowing. That is, if the mower attachment is not lowered, the maximum speed of the tractor should be set to its default value.

function RotateMower:doCheckSpeedLimit(superFunc)     if self:getIsTurnedOn() and (self.getIsLowered == nil or self:getIsLowered()) then         return true     end     return superFunc(self) end

Finally, the doCheckSpeedLimit() function returns whether the speed limit should be checked; if the mower is on and the mower attachment is lowered, we want to check that the maximum speed has been limited. Let us continue:

function RotateMower:getRotorSpeedScale()       local spec = self[RotateMower.SPEC_TABLE_NAME]       return MathUtil.inverseLerp(RotateMower.MIN_SPEED_FACTOR,                   RotateMower.MAX_SPEED_FACTOR, spec.rotorSpeedFactor) end function RotateMower:getRotorSpeedFactor()       local spec = self[RotateMower.SPEC_TABLE_NAME]       return spec.rotorSpeedFactor end function RotateMower:setRotorSpeedFactor(factor, noEventSend)       local spec = self[RotateMower.SPEC_TABLE_NAME]       factor = MathUtil.clamp(factor, RotateMower.MIN_SPEED_FACTOR,                   RotateMower.MAX_SPEED_FACTOR)       if math.abs(spec.rotorSpeedFactor - factor) > 0.0001 then             spec.rotorSpeedFactor = factor             spec.isEffectDirty = true             RotorSpeedFactorEvent.sendEvent(self, factor, noEventSend)             local actionEvent = spec.actionEvents[InputAction.CHANGE_ROTOR_SPEED]             if actionEvent ~= nil then                   g_inputBinding:setActionEventText(actionEvent.actionEventId,                         string.format(g_i18n:getText("input_CHANGE_ROTOR_SPEED"),                         self:getRotorSpeedFactor()*100 + 0.1))             end       end end

The getRotorSpeedFactor() function similarly returns the value of the rotorSpeedFactor field. The setRotorSpeedFactor() function is critical as it ensures the passed factor value is constrained by the minimum and maximum value constants we defined at the beginning of the script.

We check if the set value is different to the old value with the math.abs() function and a subtraction. If so, it sets the new value of the rotorSpeedFactor field and sends the event that the factor has changed. Notably, we set the isEffectDirty field to true to force an update from onUpdateTick().

Lastly, we update the custom text displayed to the user for the input action. Let us continue through more of the file’s contents:

function RotateMower:processRotateMowerArea(workArea, dt)       local startWorldX, _, startWorldZ = getWorldTranslation(workArea.start)       local widthWorldX, _, widthWorldZ = getWorldTranslation(workArea.width)       local heightWorldX, _, heightWorldZ = getWorldTranslation(workArea.height)       FSDensityMapUtil.updateRotateMowerArea(startWorldX, startWorldZ,                   widthWorldX, widthWorldZ, heightWorldX, heightWorldZ)       return 0, 0 end

The processRotateMowerArea() function is called when we need to mow an area of land. We are passed the work area which contains the needed start, width, and height .i3d nodes (transformGroups) for use with our density map utility. By calling the internal getWorldTranslation() function, we can translate these into the six values passed to the updateRotateMowerArea() function of FSDensityMapUtilExtension.lua. Once this function is called, we do not need to do anything further. The work area specialization does expect to receive information about the total area worked, but this is not returned to us by the function, so we can simply return 0 without issue.

function RotateMower:onTurnedOn()       if self.isClient then             local spec = self[RotateMower.SPEC_TABLE_NAME]             g_animationManager:startAnimations(spec.animationNodes)             g_effectManager:setFillType(spec.effects, FillType.UNKNOWN)             g_effectManager:startEffects(spec.effects)             g_soundManager:stopSamples(spec.samples)             g_soundManager:playSample(spec.samples.start)             g_soundManager:playSample(spec.samples.work, 0, spec.samples.start)       end end

Next, the onTurnedOn() function handles behavior for when the mower is turned on. By using the animation, effect, and sound managers, we begin playing the mower’s animations, enable its effects, and start playing the appropriate sounds. We begin by playing the start sound and play the idle work sound immediately after. Note that the work sound call has two additional parameters: a value (0) and another sample (start). This means that the sound is played 0 ms after start has finished.

function RotateMower:onTurnedOff()       if self.isClient then             local spec = self[RotateMower.SPEC_TABLE_NAME]             g_animationManager:stopAnimations(spec.animationNodes)             g_effectManager:stopEffects(spec.effects)             g_soundManager:stopSamples(spec.samples)             g_soundManager:playSample(spec.samples.stop)       end end

When the mower is turned off, the onTurnedOff event is fired by the TurnOnVehicle specialization which calls the onTurnedOff() function. Here, we do the reverse of the previous function and use the managers to stop the animations, disable the effects, and play the stop sound.

Let us now cover the final section of the script:

function RotateMower:onRegisterActionEvents(isActiveForInput,                   isActiveForInputIgnoreSelection)       if self.isClient then             local spec = self[RotateMower.SPEC_TABLE_NAME]             self:clearActionEventsTable(spec.actionEvents)             if isActiveForInputIgnoreSelection then                   local _, actionEventId = self:addActionEvent(spec.actionEvents,                               InputAction.CHANGE_ROTOR_SPEED, self,                               RotateMower.actionEventChangeRotorSpeed, false, true,                               false, true, nil)                   g_inputBinding:setActionEventText(actionEventId,                               string.format(g_i18n:getText("input_CHANGE_ROTOR_SPEED"),                               self:getRotorSpeedFactor()*100 + 0.1))                   g_inputBinding:setActionEventTextPriority(actionEventId, GS_PRIO_HIGH)             end       end end

The onRegisterActionEvents() function is called when the player enters the vehicle or connects or disconnects an attachment to the tractor. The purpose of the function is to set the current available or enabled input actions a user can trigger. In our case, we first clear the old registered action events and check if our input action is possible. For example, if the tool is not selected by the user, we do not want to register the input action. If it is selected, we call the addActionEvent() function of the Vehicle base class. The function takes as arguments our specialization actionEvents table that holds all registered inputActions for our specialization, the new input action (InputAction.CHANGE_ROTOR_SPEED), a callback target object (self), and a callback function RotateMower.actionEventChangeRotorSpeed.

The next three bool values define if the callback should be called triggerUp, triggerDown, or triggerAlways. The last bool value defines if the action should be enabled by default. The last parameter can be ignored for now, and so we simply pass nil.

Next, we set the text that should be displayed in the input help menu Head Up Display (HUD) and also set a priority of this input that is used to sort the registered input actions to display in this part of the HUD.

function RotateMower.actionEventChangeRotorSpeed(self, actionName, inputValue, callbackState, isAnalog)       local spec = self[RotateMower.SPEC_TABLE_NAME]       local step = inputValue * RotateMower.STEP_SIZE       local newFactor = spec.rotorSpeedFactor + step       self:setRotorSpeedFactor(newFactor, false) end

The actionEventChangeRotorSpeed() function is important as it updates the vehicle in response to player input. The function is passed the directional value associated with the player’s input which is 1, 0, or 1 corresponding to increase, do nothing, and decrease. Depending on the bind input device, the inputValue can also be a float in ranging between 1 and 1. For example, if we bind a joystick axis to the input, we get analog input values. The function then calls upon setRotorSpeedFactor() to change the speedFactor field based on the input and by the amount specified by the STEP_SIZE constant we define at the beginning of the script.

function RotateMower.getDefaultSpeedLimit()       return 20 end

The last function we will define is getDefaultSpeedLimit() which simply returns a default value for the speed limit when the mower is attached.

g_soundManager:registerModifierType(       "ROTOR_RPM",       RotateMower.getRotorSpeedScale )

Finally, we register a new sound modifier with the sound manager so that the pitch or volume can be altered in response to changes in the RPM of the mower’s rotor. In the XML config file, there is the rotateMower element. It contains a sound element with start, stop, and work children elements. There we use a modifier of type ROTOR_RPM to modify the pitch and volume of the sound. This line in the Lua code creates this modifier type.

You have now finished writing all of the .xml and .lua files and have completed your first complex mod! Take a moment to look at the progress you’ve made from when you started the book to where you are now. Where you may have had no programming knowledge before, you are now implementing behaviors and effects for full Farming Simulator mods. In the next section, we’ll test the completed mod!

Testing the Mod

We will follow the testing procedure from the previous chapter. From the GIANTS Studio, you can run the game without debugging from the Debug application menu. After you begin a new game on the map of your choice, you should open the vehicle shop. Go to the Tools tab and select the Mowers category. You will find the rotate mower and be able to purchase it. Attach the mower to a tractor of your choice and turn it on and off and drive over a field or meadow. Don’t forget to test the mod in multiplayer with your friends. To do this, include the mod files in a .zip file and send the .zip file to your friends. Note that mods in multiplayer need to be zip files, not folders!

Summary

In this chapter, you made your first complex mod which allows players to attach a mower to tractors and clear foliage from the map. You were introduced to the concept of density maps when we made extensions onto an existing utility for the first time. By saving the state of the mower, you also learned how to create persistence in the player’s world. Importantly, you also learned how to add multiplayer support to your mod.

In the next chapter, we will be taking a step back and making a simpler mod. In this new mod, we will explore using AI and vehicles to add more life to your Farming Simulator world.