Tutorial - FSOpen and LUA 2 - Hook Variables and Handles
In the last tutorial, we concerned ourselves with a very simple sexp library that really requires next to no real Lua knowledge at all. We're going to take one of the examples from the last tutorial and actually add a few Lua specific things.
function Lockdown(name) mn.runSEXP("(lock-primary-weapon !" .. name .. "! )") mn.runSEXP("(lock-secondary-weapon !" .. name .. "! )") mn.runSEXP("(lock-afterburner !" .. name .. "! )") mn.runSEXP("(disable-ets !" .. name .. "! )") mn.runSEXP("(player-use-ai )") mn.runSEXP("(add-goal !" .. name .. "! ( ai-play-dead 200 ) )") mn.runSEXP("(lock-perspective ( true) )") end
Remember that? That's the function we were using to lock the player's ship down so we could do cutscene stuff. But couldn't we do something where it could just automatically affect the player's ship without needing us to input the player's ship name? It could save me like 5 seconds!
Hook Variables
Of course there is. And we can do this through what is called a hook variable. Hook variables are variables that hook on to a well-defined situation or action. Most hook variables are specific to an $Action, but there are two that are nearly always valid in a gameplay setting. Player and Viewer. Player will return the player's object (or ship) if it is valid, and Viewer will return the non-player ship we are viewing from (when using target camera commands). We're just interested in the Player hook variable. All hook variables are part of the hv library, so we would refer to the player's ship with hv.Player.
A complete listing of hook variables and which $Action they apply to can be found here, on the scripting.tbl page.
So on a line before we start the mn.runSEXPs let us add…
local player = hv.Player
Whoa, there I go again, adding new things without warning. What's local player and why are we assigning it as hv.Player?
Adding the word local in front of a variable declaration will make that variable local (which means it can only be used in the current block of code). It is good practice to use local variables whenever possible. By default, variables are global in Lua (in contrast to C related languages where variables are already local by default). By making the variable local, we can assign it a generic name without worrying that it might conflict with something else in another script. Also Lua accesses local variables faster than a global variable, so we're doing this not only for us, but for our precious CPU cycles!
Handles
Okay, so we have this player variable. What can we do with it? Remember that scripting.html we might have generated in the last tutorial? (If not, open your launcher, go to the Dev Tool section, tick generate scripting.html, run the game, quit, and there should be a scripting.html in your root FreeSpace directory). Navigate over to it and open it up. Player is a "ship" type handle. A handle is our fancy name for these special variables that contain a lot of "user data", or what is specific to FreeSpace. If we scroll down a bit, in the types listing, we will find a ship type link.
If we click it we'll get jumped to ship:object. The :object part means that ship is sort of a sub-type of object. Everything in object also applies to ship. But that's not important now. Look at all of these fun properties we can use and abuse. Most of these properties are bi-directional. You can read a property or assign something a new value on the fly.
The listings here tell you what type of data will get returned if you try and read data from it, and what type of data is expected when you try and assign it a value. 99.999% of the time, those two are the same thing.
So what were we doing here again? Oh, right. We want to get the player's ship's name. Why look, here is a Name property. (Name is one of the few properties that I would highly highly super strictly never write to, missions aren't going to take the name change very nicely!) So how do we access the property? Just like this…
player.Name
So let's just add this stuff to our script from last time.
function Lockdown(name) local player = hv.Player name = player.Name mn.runSEXP("(lock-primary-weapon !" .. name .. "! )") mn.runSEXP("(lock-secondary-weapon !" .. name .. "! )") mn.runSEXP("(lock-afterburner !" .. name .. "! )") mn.runSEXP("(disable-ets !" .. name .. "! )") mn.runSEXP("(player-use-ai )") mn.runSEXP("(add-goal !" .. name .. "! ( ai-play-dead 200 ) )") mn.runSEXP("(lock-perspective ( true) )") end
Hey hold on, why doesn't name have local in front of it? Well, because name was defined as an argument to the function, the variable name is already local to the function. Adding local in front of name would just confuse Lua anyway.
So now when the function runs, it take the player's handle, throws it into this local variable, then when we're constructing the string for runSEXP to use, we're using the Name property from the player's handle. So wait, why do we even need that name argument? Couldn't we just modify the function declaration to be
function Lockdown()
And then make name a local variable to keep it simple?
Sure, but let's add some intelligence to this function. Let's say if we leave the argument blank, the function will lockdown the player, but if we have a ship name in there, we mean THAT ship (maybe we want to lock down an AI friend?)
So remember what I said last tutorial about empty arguments?
"NOTE: If the argument is empty and does NOT have anything in it, the argument will then "contain" a special value called nil. Nil is Lua's version of null (or empty), it basically means it does not exist. Using something that is nil will result in errors everywhere! In a later tutorial we'll do checks for nil, for now just be mindful of the arguments you pass through functions."
So we'll check if name is nil or not. For that we need an if statement! There's a few ways we can check to see if a variable is carrying something useful for us to use. Generally if its "carrying" nil, then it's not useful. To find out if we entered something into name we could go…
if name ~= nil then
So if name is not nil then we do stuff. But there's a slightly easier way to type that…
if name then
When an if statement evaluates just a lone variable, no comparisons attached or anything it will only return false if the variable is nil or false. So if name contains useful, it would pass the logic test and go through. But we don't want to know if a variable contains something useful, we're interested in the opposite.
if not name then
So now with the not operator, we've flipped the logic around. Now we'll only pass the logic test if name is nil or false. And so we will write…
if not name and hv.Player:isValid() then local player = hv.Player name = player.Name end
Yeesh, what's this new stuff again? Well, we want to make sure the player is actually a valid handle to use. Otherwise, we get errors. But we couldn't just do the same thing as we did with name. hv.Player won't be nil or false if a player dies. The handle just becomes invalid, so while it would pass that logic test, but the data inside is all messed up and wrong. So many handles have a :isValid() check to it that determines if the handle is safe to use.
Well I hope this guides you on your way to lua scripting. The next tutorial will conditional actions and keeping your scripts efficient.
Final Script:
function Lockdown(name) if not name and hv.Player:isValid() then local player = hv.Player name = player.Name end mn.runSEXP("(lock-primary-weapon !" .. name .. "! )") mn.runSEXP("(lock-secondary-weapon !" .. name .. "! )") mn.runSEXP("(lock-afterburner !" .. name .. "! )") mn.runSEXP("(disable-ets !" .. name .. "! )") mn.runSEXP("(player-use-ai )") mn.runSEXP("(add-goal !" .. name .. "! ( ai-play-dead 200 ) )") mn.runSEXP("(lock-perspective ( true) )") end function UnLockdown(name) if not name and hv.Player:isValid() then local player = hv.Player name = player.Name end mn.runSEXP("(unlock-primary-weapon !" .. name .. "!)") mn.runSEXP("(unlock-secondary-weapon !" .. name .. "!)") mn.runSEXP("(unlock-afterburner !" .. name .. "!)" ) mn.runSEXP("(enable-ets !" .. name .. "!)") mn.runSEXP("(player-not-use-ai)") mn.runSEXP("(clear-goals !" .. name .. "!)") mn.runSEXP("(lock-perspective ( false ) )") end