-
Notifications
You must be signed in to change notification settings - Fork 0
Chapter 2 Finding and displaying data
This chapter will teach you:
- Different ways of finding what you are looking for
- Different ways of displaying information to verify you found the right thing
We'll apply the same scenario to different ways of finding and displaying information so you can pick the best approach for any situation.
For this chapter, we can continue using the FS22_MyFirstMod folder we created in the previous chapter.
Imagine we want to acquire the name of the vehicle the player is sitting in, so we can display it in some kind of UI
The first source of information is the search function on GDN: https://gdn.giants-software.com/search.php (don't need to be logged in to use it).
Unfortunately, searching vor vehicle.name or similar won't lead to success. Instead, we need to find the class. Let's try "Vehicle":
You'll get a long list of results. Scroll down a bit and see if you can find a class which is just called "Vehicle":
Click the link there to get to the documentation page of the Vehicle Class.
So the LUADOC will just contain a list of properties, right? No, it does not, because in lua, you don't really have classes and every class is basically a table of dynamic properties behind the scenes. This makes auto-documenting properties quite a bit challenging.
The options you have are:
- Check the list of functions and see if it contains what you want to know
- Check if the class has a
load
function, which initializes most properties most of the time.
In this case, there's three things which could be interesting:
- The getName function which seems to draw the name somewhere out of the store .
- The getFullName function which prepends the brand name by the looks of it
- The load function which initializes
self.configFileNameClean
,self.typeName
andself.customEnvironment
, which contains the name of the mod
Ok, but which option is it? We could print them to figure that out, but how do we get the vehicle instance?
Let's explore this in the debugger of GIANTS Studio.
If you haven't done the setting up part yet, now would be a good idea to do so. The tutorial will assume you did.
Unless it's still open, start GIANTS Studio now and open your FS22_MyFirstMod project (it will automatically open whatever had been opened when exiting).
If you still have a breakpoint set in the lua file, remove it now.
Select the save20
configuration in GIANTS Studio next to the green arrow, then click the arrow or press F5. If configured correctly in the previous chapter, this should start the game while skipping the intro videos and directly load you into save game #20.
Once you are loaded into the game, jump into any vehicle, switch to GIANTS Studio and press the pause button:
In the bottom-left area of the screen, switch to the "Globals" tab unless it's already active. In order to find information about vehicles, you could type "vehicle" into the search bar, but that would filter the list to show only things which contain "vehicle" without displaying all of their children. That means you'd find the vehicle class, but nothing related to names.
Instead, scroll down almost all the way to the bottom and unfold the "Vehicle" entry:
Ok, great, but why don't we see any of the things we found in the LUADOC?
Well, two reasons:
- This is only static information about the Vehicle class. If you want to see properties, you need an instance first.
- Functions are hidden by default. You could open the "Filter" dropdown next to the search bar and activate functions, but all it would tell you is that the function exists. This can be helpful in some cases, but in this one, we already know from the LUADOC.
By GIANTS convention, every instance of an object which may need to be accessed by another script is stored directly or as a child of a global variable with the prefix g_
.
The probably most important object of them all is g_currentMission
.
Let's find it in the list of globals and unfold it. If you scroll down a bit after doing so, there's an interesting section here:
"controlledVehicle" sounds like just the thing we're looking for. Additionally, there's a currentVehicleName
property which looks even more so. We could stop right here and continue with our lucky find, but for the sake of learning how to use the debugger, let's continue by unfolding "controlledVehicle".
Let's then take a look at the properties we found in the LUADOC:
-
configFileNameClean
: In my case, that's "pickup1986". Helpful, but not quite there. -
typeName
: "car". Might be useful in some situations. -
customEnvironment
: This is not in the list. Which probably means it isnil
and will only contain the mod name if it's from an actual mod rather than base game.
That's not satisfying yet. What's missing are the getName
and getFullName
functions.
From the Globals tab, we know that g_currentMission.controlledVehicle
is the object we're interested in. So how do we execute any of the object's methods? Do we need to quit the game and add some calls to the script?
There's a faster way in this case:
Switch to the "Script Console" tab in the same window where the "Globals" tab is.
Let's try printing the typeName
first, to figure out how the script console works.
In the bottom half of the Script Console, enter the following code:
print(g_currentMission.controlledVehicle.typeName)
Execute the code with [Ctrl]+[Enter], and look in the "Output" tab of the window in the bottom-right. You should see something like "car", "tractor" or similar, dependent on what you're sitting in.
You'll also realize that the bottom half of the script console is empty. If you needed to modify the script, you could copy it from the top half again, but if you ever close GIANTS Studio, it's gone. It's therefore recommended to get a habit of writing your script somewhere else, maybe in Notepad++ or VSCode, and store it in a file, and only then [Ctrl]+[V] and [Ctrl]+[Enter] in the script console.
Luckily, you can copy the scripts from this tutorial for now. Let's try printing the result of the two functions we're interested in:
print(g_currentMission.controlledVehicle:getName())
print(g_currentMission.controlledVehicle:getFullName())
In my case, this outputs
Pickup 1986
Lizard Pickup 1986
Before we conclude the Script Console, let's take a moment to talk about a very important aspect of lua syntax. If you're not experienced with lua, you might think that object:method()
is the way to call a method in lua. However, the code above could have been written like this:
print(g_currentMission.controlledVehicle.getName(g_currentMission.controlledVehicle))
print(g_currentMission.controlledVehicle.getFullName(g_currentMission.controlledVehicle))
or in fact also like this:
print(Vehicle.getName(g_currentMission.controlledVehicle))
print(Vehicle.getFullName(g_currentMission.controlledVehicle))
so in general, the :
just means that whatever object you are calling a method on, will receive that very object as its first parameter. This fact will be repeated a couple of times in the remaining chapters in order to avoid confusion.
Using the debugger is nice for when you know the exact conditions of when you want to debug some variables. You don't always have the luxury of knowing that, though. Also, using the debugger has significantly larger cycle times.
This is when writing some code for debugging is necessary. Press the "stop" icon in the debugger in order to stop debugging and close the game at the same time. You can close GIANTS Studio for now since VS Code is much more convenient for some of the things we'll do.
Lets jump back to VS Code and create a new "FS22_Tutorial_C2.lua" file in the scripts
folder.
(Don't worry about the debug_local.bat in my screenshot yet)
In order for the game to execute this lua file later, we also need to extend the extraSourceFiles
section in the modDesc.xml
like so:
<extraSourceFiles>
<sourceFile filename="scripts/FS22_Tutorial_C1.lua" />
<sourceFile filename="scripts/FS22_Tutorial_C2.lua" />
</extraSourceFiles>
Now add the following code to the new lua file:
Player.update = Utils.appendedFunction(Player.update, function(...)
if g_currentMission ~= nil then
local vehicle = g_currentMission.controlledVehicle
if vehicle ~= nil then
print(("Player is sitting in %s. This is a %s"):format(vehicle:getFullName(), vehicle.typeName))
end
end
end)
Before we analyze the code, you might notice that VS Code (or probably rather the lua language server plugin) underlines things like Player
and g_currentMission
. This is because VS Code does not know about globals and classes defined in GIANTS Source Code. You might consider jumping into GIANTS Studio instead, but at the time of writing this tutorial, it won't have that, either.
What you can do in VS Code is navigate to the unknown symbols of which you are sure they exist, press [Ctrl]+[.] and select "Mark '...' as defined global". This will add the symbols to your .vscode/settings.json file. If you keep doing this, you'll more likely find typos and the like before launching FS, unless you had a typo in your symbol while marking it as defined global, of course.
Back to the code, let's analyze it step by step:
The main construct here is the following:
Player.update = Utils.appendedFunction(Player.update, function(...)
end)
This basically tells the GIANTS Script Engine to define a function which first calls the original Player.update
function and then calls another function we define here. That new function will replace the original Player.update
function. At this point it's probably worth mentioning that lua doesn't care about methods vs functions, it's all a function in lua, and you not only prepend or append, you can in fact even replace the whole function. This gives script modders a lot of power, but since half of the script modders overwrite functions without calling the original function somewhere in there, this is also the major reason for mod conflicts: Any mod which gets loaded after our mod could just completely replace our function by something else and thus prevent our function from ever getting executed.
There will be tips on how to decrease the likelihood of that happening in future chapters.
We will explore how often and when that method is called later on.
The function(...)
part basically means that at this point we don't care what kind of parameters the function has. Note that it must be Player.update
rather than Player:update
as otherwise appending to the function would not work. Remember Player:update
is short for Player.update(Player)
, which doesn't make sense here.
Next are a couple of nil
checks. If you're coming from a different programming language, in lua, nil
is basically null
.
The output is then generated using
print(("Player is sitting in %s. This is a %s"):format(vehicle:getFullName(), vehicle.typeName))
The format
function is executed on a string with two string placeholders (%s
), and the result of getFullName()
as well as the typeName
will be filled into these placeholders.
You could have also written it like that:
print("Player is sitting in " .. vehicle:getFullName() .. ". This is a " .. vehicle.typeName))
The first form is usually easier to read and maintain, though, and it allows you to define placeholders like %.3f
which would round a floating point number to three decimal digits for you.
You have two options from here:
- Run
copytofs.bat
in the terminal again, start FS22 and select the save game manually. - Find out the path to your game installation and create a debug_local.bat like this, using your own path:
call copytofs.bat
start "" "<your steam path>\Steam\steamapps\common\Farming Simulator 22\x64\FarmingSimulator2022Game.exe" -skipStartVideos -autoStartSavegameId 20
If you just tried this out and it didn't work, you probably did not fill in the path placeholder...
If you went for option 2, you can from this point on simply execute .\debug_local.bat in VS Code's terminal and it will very quickly launch you into your prepared save game.
Once ingame, open the console using the key above your tab key (usually ~
or ^
), then jump into various vehicles. You'll notice you get two calls of the function when entering a vehicle, but you won't get any when tabbing through them, so Player.update
is maybe not the best function to use for productive code. It does its job for debugging this, though.
You might also realize there's not just tractor
and car
, and trucks are considered tractor
s sometimes:
Anyway, you just learnt how to gather information without using the debugger. Close the game again and continue with the next section when ready.
Another useful way for displaying debug information is by rendering it into the 3d world. Let's render the name of every vehicle above the vehicle so we can display it for every nearby vehicle without having to enter them.
Let's try to write a little less spaghetti code this time as well. We want to see the name above the vehicles all the time, not just when the game considers an update necessary. Therefore, Vehicle.update
is not a good fit. By looking at the GDN documentation of the Vehicle
class, we can find a drawUIInfo
function which probably gets called on every frame (that's what draw functions are for in almost every UI application).
Since drawUIInfo
gets called quite often, we probably don't want to call Vehicle.getFullName
inside that method, though, since it would always return the same result for the same Vehicle
instance.
Luckily, we're programming in lua, so we can just add a property to an object in one method and access it from the other.
Let's start by storing the vehicle name in each vehicle instance on load. We therefore need to override the Vehicle.load method.
Easy enough, just append to the function like in the previous case, and store the name? Well, here's a common pitfall for you: If you inspect the implementation of Vehicle.load
, you can see that it returns a value. Appending and prepending to a function unfortunately only works for functions which don't return anything.
This means we need to overwrite the method, which is a bit more complicated.
Disclaimer: There's also the options to write event handlers or write specializations which are more or less like subclasses. This tutorial explains the overwrite approach though, since that's the one most modders get wrong.
Let's look at the parameters of the function in the LUADOC:
function Vehicle:load(vehicleData, asyncCallbackFunction, asyncCallbackObject, asyncCallbackArguments)
If you take a look at the implementation, you can see that many lines operate on an object called self
. However, self
isn't in the list of arguments!? Well, once again, the :
in there is key. The function definition above is basically the short form for:
function Vehicle.load(self, vehicleData, asyncCallbackFunction, asyncCallbackObject, asyncCallbackArguments)
The Utils
class provided by GIANTS also allows overwriting methods, and it will always supply the original function which is being overwritten into the second argument. By convention, this is usually called "superFunc".
Let's define a function which has the right kind of syntax to override the vehicle load method by adding the following code to the FS22_Tutorial_C2.lua file:
local function insteadOfVehicleLoad(vehicle, superFunc, vehicleData, asyncCallbackFunction, asyncCallbackObject, asyncCallbackArguments)
-- Always, always, always call superFunc when overwriting a method. Other modders will thank you.
-- Also, in this case, it would completely break the game when not doing so.
local returnValue = superFunc(vehicle, vehicleData, asyncCallbackFunction, asyncCallbackObject, asyncCallbackArguments)
-- Now store the vehicle name
vehicle.fullName = vehicle:getFullName()
-- Return the original result
return returnValue
end
Nothing fancy, just a function which calls its superfunction and additionally stores the full name in a property. There are some things to consider, though:
Note how I didn't call the first parameter self
? You could, but if this were a function of say a class called VehicleNameDisplay
, you would mislead people and your future self into believing self
is an instance of VehicleNameDisplay
, while it is in fact an instance of Vehicle
instead.
Also note how superFunc
got injected at position 2, compared to the original function's parameters. When calling superFunc
, try to remember you'll have to pass the object the function was called on as a first parameter.
In order for this method to actually overwrite anything, we need to register an overwritten function like so:
Vehicle.load = Utils.overwrittenFunction(Vehicle.load, insteadOfVehicleLoad)
A bit more complicated than appending, right? Luckily, drawUIInfo
has no return value. Additionally, we can actually reuse the GIANTS code which is used to display the distance to the vehicle, by slightly modifying it. Wrapped in a function, it looks like this:
local function afterDrawUIInfo(vehicle)
--- Retrieve the coordinates of the origin of the vehicle
local x, y, z = getWorldTranslation(vehicle.rootNode)
--- Render the vehicle name 4 meters above (y+4) the vehicle
Utils.renderTextAtWorldPosition(x, y+4, z, string.format("%s", vehicle.fullName or "not initialized"), getCorrectTextSize(0.02), 0)
end
Vehicle.drawUIInfo = Utils.appendedFunction(Vehicle.drawUIInfo, afterDrawUIInfo)
Don't mind what's going on here in detail, just know that the first line gets you the correct coordinates, and the second one renders the text in the fourth argument exactly 4 meters above whereever the vehicle defined its origin.
Your whole file should now look somewhat like this:
Player.update = Utils.appendedFunction(Player.update, function(...)
if g_currentMission ~= nil then
local vehicle = g_currentMission.controlledVehicle
if vehicle ~= nil then
print(("Player is sitting in %s. This is a %s"):format(vehicle:getFullName(), vehicle.typeName))
end
end
end)
local function insteadOfVehicleLoad(vehicle, superFunc, vehicleData, asyncCallbackFunction, asyncCallbackObject, asyncCallbackArguments)
-- Always, always, always call superFunc when overwriting a method. Other modders will thank you.
-- Also, in this case, it would completely break the game when not doing so.
local returnValue = superFunc(vehicle, vehicleData, asyncCallbackFunction, asyncCallbackObject, asyncCallbackArguments)
-- Now store the vehicle name
vehicle.fullName = vehicle:getFullName()
-- Return the original result
return returnValue
end
Vehicle.load = Utils.overwrittenFunction(Vehicle.load, insteadOfVehicleLoad)
local function afterDrawUIInfo(vehicle)
--- Retrieve the coordinates of the center of the vehicle
local x, y, z = getWorldTranslation(vehicle.rootNode)
--- Render the vehicle name 4 meters above (y+4) the vehicle
Utils.renderTextAtWorldPosition(x, y+4, z, string.format("%s", vehicle.fullName or "not initialized"), getCorrectTextSize(0.02), 0)
end
Vehicle.drawUIInfo = Utils.appendedFunction(Vehicle.drawUIInfo, afterDrawUIInfo)
Let's test this code by executing .\debug_local.bat
in the terminal again.
After loading in, you should see something similar to this:
Congratulations, you are now able to find properties and functions, append to or overwrite functions and get information from the debugger, the log file or directly ingame.
There are of course more ways of finding and displaying data, but these are the most important ones.