Skip to content

Chapter 3 An excursion into OOP with lua for FS

Timmeey86 edited this page Jul 14, 2024 · 2 revisions

Requirements

This chapter assumes you:

  • understand the concepts of object-oriented programming, classes and inheritance from any other programming language.

Without this knowledge, you probably won't understand half of this chapter.

This chapter won't show you how to do cool stuff in Farming Simulator. Unfortunately, you won't understand the other chapters without understanding at least the basics of classes in lua for Farming Simulator, though.

Classes in lua (when coding for Farming Simulator)

Lua doesn't really support object-oriented programming. Most things in lua are just primitive datatypes, tables, functions and the like.

A table can however have a metatable which amongst others allows redirecting functions, and that allows treating tables like they would be classes, and treating other tables like they would be subclassing something.

Defining a base class

GIANTS provide us with a global Class function which handles most of the "acting like it would be a class" code. Using this function, a base class could be defined like so:

-- Define a table which acts like our class
MyBaseClass = {}

-- Create a metatable for this class (this is the naming convention used by GIANTS)
local MyBaseClass_mt = Class(MyBaseClass)

-- Define a counter so we can assign unique IDs (just a random example)
local counter = 0

---Default constructor. Creates a base class object with a unique ID
---@param customMetaTable table @An optional metatable of a subclass.
---@return table @The new object
function MyBaseClass.new(customMetaTable)
    -- Create a new instance without any initial properties.
    -- When calling new(), an instance of MyBaseClass will be created.
    -- If however a different metatable is being supplied as an argument, an instance of that class will be created instead.
    -- You'll see how that works further below
    local self = setmetatable({}, customMetaTable or MyBaseClass_mt)

    -- Set some properties here
    self.id = counter
    counter = counter + 1

    -- Return the instance
    return self
end

---This is how you define something similar to a virtual function: Just define a function which does nothing.
function MyBaseClass:printStuff()
    return
end

---This is how you define default behavior which can then be extended by subclasses.
function MyBaseClass:printClassInfo()
    if self:superClass() ~= nil then print("This object has a superclass when asking the base class") end
    if self:isa(MyBaseClass) then print("This object is compatible with MyBaseClass") end
end

Note how functions are described with three dashes ---? This is where the "Lua Language Server Plugin" comes in handy in VSCode: If you define a function, type three dashes and press the return key, VSCode will provide you with a LUADOC template you can fill in. If you do, you'll see your own documentation when calling a method or displaying a tooltip on one like so:

Image showing a tooltip which displays the function documentation

This can be very helpful for maintaining more complex mods.

Defining a subclass

Similarly to the base class definition above, a subclass could be defined like so:

-- Define a subclass
MySubClass = {}

-- Create a metatable which indicates that MySubClass inherits MyBaseClass
local MySubClass_mt = Class(MySubClass, MyBaseClass)

---Default constructor. Creates a new instance as a subclass of MyBaeClass
---@return table @the new instance
function MySubClass.new()
    -- Create an instance of the base class, but supply the subclass metatable instead.
    local self = MyBaseClass.new(MySubClass_mt)
    self.myFancyProperty = "It works!"
    return self
end

---This is how you override something similar to a virtual function: Just define a function identical to the base class function
function MySubClass:printStuff()
    print(self.myFancyProperty)
end

---This is how you extend base class behavior
function MySubClass:printClassInfo()
    self:superClass():printClassInfo()
    if self:superClass() ~= nil then print("This object has a superclass when asking the subclass") end
    if self:isa(MySubClass) then print("This object is compatible with MySubClass") end
end

In short, we supplied the MyBaseClass table as an additiona argument to the Class() function, supplied our own meta table to MyBaseClass when creating an instance and overrode both base class functions.

Testing OOP

Since variables in lua are untyped, we cannot do something like

MyBaseClass baseClass = MySubClass.new()

Instead, we just put a base class and subclass object into a table and call the same functions on both objects in order to make sure we treat them identically.

For each object, we'll call the printStuff and printClassInfo functions:

-- Create a base class and subclass instance
local baseObject = MyBaseClass.new()
local subObject = MySubClass.new()

-- Create 
local objects = {}
table.insert(objects, baseObject)
table.insert(objects, subObject)

for index, object in pairs(objects) do
    print("Object at index " .. tostring(index) .. ":")
    print("Class info:")
    object:printClassInfo()
    print("Stuff:")
    object:printStuff()
    print("---------------------------------------")
end

Can you guess the output?

Limitations of OOP in lua

Let's take a look at what the code above prints to the logfile. If you want to execute it yourself, just copy all three code snippets into the FS22_Tutorial_C2.lua file of the previous chapter, call .\debug_local.bat, let the game load into your save, exit, and search the output in the log file.

The output:

Object at index 1:
Class info:
This object is compatible with MyBaseClass
Stuff:
Object ID: 0
---------------------------------------
Object at index 2:
Class info:
This object is compatible with MyBaseClass
This object has a superclass when asking the subclass
This object is compatible with MySubClass
Stuff:
It works!
Object ID: 1
---------------------------------------

Nothing surprising for the baseObject: The printClassInfo method claims the object has no base class (it is the base class itself, after all), the empty printStuff method does nothing and the object ID is 0 since it's the first object.

The subclass behaves almost as expected:

  • The object is both considered a MySubClass and MyBaseClass
  • The extended function properly detects that the object has a base class
  • The object ID is 1, so the counter has been successfully incremented

Hold up, why does the line This object has a superclass when asking the base class not show? After all, we're executing the base class method on the sub class object, do we not?

Well, not quite. The subclass implementation calls self:superClass() which does not "cast" to a base class but rather returns the meta table of the superclass. Which means superClass():printClassInfo actually checks if the base class has another base class - which it does not.

Conclusion

Don't worry if you don't understand what's going on here. The key takeaway is: You can't go fully OOP with lua, but most things will still work as expected, so distributing your code over different classes can still be a good idea in regards of maintainability.

If you understood everything and are really curious about what else could be done with metatables now, feel free to take an in-depth look at https://www.lua.org/pil/13.html and the follow-up pages of that link (press the arrow which points to the right). This part is absolutely not needed for 98% of mods, though.