Skip to content

Chapter 4 Hooking into things

Timmeey86 edited this page Jul 23, 2024 · 4 revisions

Requirements

This chapter assumes you've completed chapters 1-3.

Overview

These are the currently known ways of triggering code in your mod, some of which were already covered in chapter 2:

  • Executing code before or after a base game method
  • Overwriting/Replacing a base game method
  • Event listener classes
  • Specializations

Appending, prepending and overwriting

Basic syntax

These were already covered in chapter 2, so here's a quick reminder:

Player.update = Utils.prependedFunction(Player.update, function(player)
    print("This is called before Player.update")
end)
Player.update = Utils.appendedFunction(Player.update, function(player)
    print("This is called after Player.update")
end)
Vehicle.udpate = Utils.overwrittenFunction(Vehicle.update, function(vehicle, superFunc, dt)
    print("This is called before Vehicle.update")
    superFunc(vehicle, dt)
    print("This is called after Vehicle.update")
end)

Once again, if you use overwrittenFunction, make sure you call superFunc preferrably in every code path since otherwise:

  • Changes made by GIANTS in the original method will not affect users of your mod, causing all kinds of side effects
  • Other mods which are loaded before your mod and override the same method might get completely broken or even cause your mod to fail
  • Even without other mods or changes by GIANTS you might face issues because a part of the original game code never gets executed

Notes on superFunc

There are some more things to consider for overwrittenFunction:

When overriding

Vehicle:update(dt)

the syntax of a function which could override that would be

function MyMod:onUpdateVehicle(superFunc, dt)

This makes many people believe that superFunc would always be the first argument.

However, since function A:b() is just syntactic sugar for function A.b(self), the arguments are actually

function MyMod.onUpdateVehicle(self, superFunc, dt)

So superFunc is in fact always the second argument. GIANTS probably made it that way so superFunc does not interfere with the : syntax. However, it is advised to use the long form and call the first argument differently like so:

function MyMod.onUpdateVehicle(vehicle, superFunc, dt)
   -- some code
   superFunc(vehicle, dt)
   -- some other code
end

As mentioned in chapter 2, the reason for this is that the first example could mislead you into thinking self was an instance of MyMod rather than Vehicle when bugfixing the code a year after release. Save yourself the headache and don't call it self.

Overriding non-class functions

Whether intended or not, it's also possible to override "static" functions, i.e. functions which are not part of a GIANTS class. Since superFunc is always the second argument, this means a function like

function DebugUtil.drawDebugNode(node, text, alignToGround, offsetY)

would be overridden as

function MyMod.drawDebugNode(node, superFunc, text, alignToGround, offsetY)

which feels weird, of course, but is a tradeoff for allowing the : syntax where applicable.

Event listeners

In addition to appending/prepending/overriding functions, it's also possible listen to certain in-game events. This can be used for initialization and cleanup, for example, but also for key and mouse events, if you'd ever need that.

The way event hooks work in FS is you simply define any class, create an instance of it and register it:

-- Define a class 
MyMod = {}
local MyMod_mt = Class(MyMod)
function MyMod.new()
    local self = setmetatable({}, MyMod_mt)
    return self
end

-- Create an instance
local myMod = MyMod.new()

-- Add the instance as event listener on load, and remove it when the player exits to the main menu
FSBaseMission.load = Utils.prependedFunction(FSBaseMission.load, function() addModEventListener(myMod) end)
FSBaseMission.delete = Utils.appendedFunction(FSBaseMission.delete, function() removeModEventListener(myMod) end)

The reason for handling both add and remove is that your mod would otherwise keep doing things after the player returned to the main menu and loaded a different save without your mod active, or when loading a save game twice, there could be two instances of your mod running.

You can listen to events by adding functions with predefined names to your class. All functions are optional: Only the ones which are defined will be called. There will be no error if a function is not defined.

The currently known functions are:

function MyMod:loadMap(filename)
-- This gets called at some point during the loading screen, when the map itself has finished loading. There might still be further initialization or network synchronization going on after this
end

function MyMod:deleteMap()
-- This gets called when the player returns to the main menu.
-- If your mod has some kind of global state, this could be used to reset/remove that state
end

function MyMod:update(dt)
-- This gets called very often. If your mod causes heavy fps drops, your code which is hooked to this method is potentially the cause.
-- dt is the delta-time since the last update. Useful for anything related to physics or animation.
end

function MyMod:draw()
-- This gets called on every frame. You should do even less calculations than in update(), here
end

function MyMod:mouseEvent(posX, posY, isDown, isUp, button)
-- Reacts to every single mouse event, including mouse move events.
-- Very few mods will need this, it is usually better to make use of input actions (yet another future tutorial chapter) instead.
-- If you override it, keep calculations to a minimum. Imagine 20 mods calculating stuff every time you move the mouse...
end

function MyMod:keyEvent(unicode, sym, modifier, isDown)
-- Similar to mouseEvent(), this is for key presses/releases
end

Specializations

If what you are trying to do affects only certain types of vehicles, implements or buildings, then specializations are the way to go - they are part of the Entity Component System design pattern, if you know it.

As an example, the code in this section will try tracking the number of implements which are attached to a vehicle and display that information near the speedometer when sitting in such a vehicle.

The specialization class

Specialization classes consist of the following parts:

  • A precondition check
  • A one-time initialization procedure
  • Registration of event handlers and functions
  • Implementation of functions which will be called per object instance

There will be a working example towards the end of this page, but if you feel like starting with empty files and transferring the code, feel free to create a scripts\specializations\ImplementCounterSpecialization.lua file now.

The precondition check

After defining your specialization table (so it can have functions), you can - but are not required to - define a function which checks for the preconditions. Even if you don't have any, it's recommended to define the function though, so it's clear you thought about it already.

Preconditions are other specializations which must be present. You can take a look at all available base game specializations here if you are interested (check the tree to the side, not the page itself).

In our case, we want to count the number of attached implements. So we need a vehicle which can have things attached to it. That's that the AttacherJoints specialization represents. The preqreuisite check should therefore be:

ImplementCounterSpecialization = {}

function ImplementCounterSpecialization.prerequisitesPresent(specializations)
    return SpecializationUtil.hasSpecialization(AttacherJoints, specializations)
end

If we mess up the registration code later and apply the specialization to something which doesn't have an attacher joint, we'll get a helpful error message that way.

One-Time initialization

The syntax of the initialization method is

function ImplementCounterSpecialization.initSpecialization()
end

Usually you would register XML schema definitions for attributes which other modders can add to their XML in order to support your mod, or XML schema definitions for saving your own data to a savegame file. We don't need these things for our simple example here, though. While there is no in-depth specialization tutorial available, you can take a look at existing specializations in the LUADOC and see what they're doing in that method.

Registration of event handlers and functions

There are four methods involved with function registration:

  • You can define your own events which you will send
  • You can define new functions which you will call
  • You can listen to events sent by other specializations
  • You can overwrite functions defined by other specializations

The first two cases are mainly required if your specialization is supposed to be an API for other specializations. The latter two cases are far more common and are sufficient in our example.

The general strategy for our counter is:

  1. Initialize the counter to zero for every vehicle
  2. Increase the counter whenever an implement gets attached
  3. Decrease the counter whenever an implement gets detached

Luckily, if we load a savegame with vehicles which have attached implements, the game sends the onPreAttachImplement event for every attached implement, so we don't need to worry about initial state - it behaves just as if the player manually attached every implement during the loading screen.

We can register the three events corresponding to the general strategy above:

function ImplementCounterSpecialization.registerEventListeners(vehicleType)
	SpecializationUtil.registerEventListener(vehicleType, "onLoad", ImplementCounterSpecialization)
	SpecializationUtil.registerEventListener(vehicleType, "onPreAttachImplement", ImplementCounterSpecialization)
	SpecializationUtil.registerEventListener(vehicleType, "onPostDetachImplement", ImplementCounterSpecialization)
end

In order to view the effects, we need to hook into some kind of drawing function. This is not an event, but a function defined in the Vehicle class, which we can override like this:

function ImplementCounterSpecialization.registerOverwrittenFunctions(vehicleType)
    SpecializationUtil.registerOverwrittenFunction(vehicleType, "drawUIInfo", ImplementCounterSpecialization.drawUIInfo)
end

Implementation

In order for our specialization to do anything useful, we need to implement the event listeners and the overridden function from the previous section:

function ImplementCounterSpecialization.onLoad(vehicle, savegame)
    vehicle.implementCounter = 0
end

function ImplementCounterSpecialization.onPreAttachImplement(vehicle)
    vehicle.implementCounter = vehicle.implementCounter + 1
end

function ImplementCounterSpecialization.onPostDetachImplement(vehicle)
    vehicle.implementCounter = vehicle.implementCounter - 1
end

Nothing crazy here. For the draw function, you'd have to research quite a bit to figure out how to write text anywhere near the speedometer like many other mods do.

Basically, we get the coordinates of the HUD's SpeedMeterDisplay class, and slightly modify them. We also only want to paint stuff for the vehicle the player is sitting in (remember, the implementation functions are per-instance so will be called for every vehicle).

You don't need to understand the whole draw function, the important parts are the vehicle == g_currentMission.controlledVehicle check and the renderText() call. The rest just handles where and how text shall be rendered.

function ImplementCounterSpecialization.drawUIInfo(vehicle, superFunc)
    -- Let the base game draw stuff first
    superFunc(vehicle)

    -- Only render the implement counter for the vehicle the player is sitting in
    if vehicle == g_currentMission.controlledVehicle then

        -- Get the position of the speedometer (its bottom left corner in fact)
        local speedMeter = g_currentMission.hud.speedMeter
        local refX, refY = speedMeter.gaugeBackgroundElement:getPosition()
        local textSize = speedMeter:scalePixelToScreenHeight(20)

        -- Render the implement count slightly below the speedometer (0.02 is 2% of the screen height)
        setTextColor(1, 1, 1, 1) -- RGBA
        setTextBold(false)
        setTextAlignment(RenderText.ALIGN_CENTER)
        renderText(refX, refY - .02, textSize, "Implements: " .. tostring(vehicle.implementCounter))
    end
end

That's everything you need for the specialization class.

Registering the specialization

If you added just that file to a mod, nothing would happen - we need to register the specialization first. A good point in time to do that is just before the game starts validating the vehicle types, since it wouldn't make sense to add vehicle types after type validation, right?

In order for easier registration, we need to grab the mod name and mod directory. You can only grab these while the mod is being loaded, so we can just add

MOD_NAME = g_currentModName
MOD_DIRECTORY = g_currentModDirectory

at the top of our main lua file (not the specialization one).

Then, we need to define a unique name for the specialization to store the specialization data. For example for our FS22 Tutorial Chapter 4, let's use FSTC4_ImplCounterSpec. The chances of someone else using that name are slim enough. Then we need to define a couple of things based on the actual table name of our specialization and the path:

local uniqueSpecializationName = "FSTC4_ImplCounterSpec" -- call this however you like. It needs to be unique across all mods, though
local specClassName = "ImplementCounterSpecialization" -- this must match the name of your specialization table/class
local specIdentifier = MOD_NAME .. "." .. uniqueSpecializationName
local specPath = MOD_DIRECTORY .. "scripts/specializations/" .. specClassName .. ".lua"

Once that's defined, we can hook into TypeManager.validateTypes like so:

local function registerSpecialization(manager)
    -- ...
end

TypeManager.validateTypes = Utils.prependedFunction(TypeManager.validateTypes, registerSpecialization)

One thing to note is that this function will get for each type manager, but we only want to execute code once. We can filter for the vehicle type manager using

    if manager.typeName == "vehicle" then
        -- ...
    end

This will make sure vehicle types are definitely ready for specialization registration.

First, we need to tell the specialization manager that we created a new specialization using the local variables defined above:

        g_specializationManager:addSpecialization(uniqueSpecializationName, specClassName, specPath, nil)

Next, we need to add this specialization to every vehicle with an attacher joint. As a safeguard, we only add it to Motorized vehicles - we're interested in tractors, cars, trucks and so on, rather than weird things which can have attachments but don't have a motor. Like in the prerequisitesPresent function in our specialization, we can make use of `SpecializationUtil.hasSpecialization) to check that:

        for typeName, typeEntry in pairs(g_vehicleTypeManager:getTypes()) do
            if typeEntry ~= nil 
              and SpecializationUtil.hasSpecialization(Motorized, typeEntry.specializations) 
              and SpecializationUtil.hasSpecialization(AttacherJoints, typeEntry.specializations) then

                g_vehicleTypeManager:addSpecialization(typeName, specIdentifier)

            end
        end

Working example

If you want a working example, you can use the FS22_Tutorial_C4 folder in the repository which you cloned during chapter 1. Execute the copytofs.bat file in there, load the game, jump into vehicles and experiment with attaching or detaching implements.

You should get something like this (check bottom-right corner):

Screenshot showing the implement counter in the bottom-right corner