Tutorial - FSOpen and LUA
One of the coolest things that's been added to FreeSpace Open is the Lua scripting language. It allows a certain amount of freedom that SEXPs don't allow, but on the other hand Lua is a lot more, let's say, risky. FreeSpace has a lot of error checking and can handle any dumb SEXP efforts with a shrug. Lua runs a more unregulated campaign and, if you're not careful, can spew out baffling error messages that leave you clueless even after an hour on Google.
There's a lot of basic Lua tutorials around the internet, so I'm not going to get into the specifics too much. Read up on them if you're curious. There's just very little in the way of how FreeSpace and Lua interact with each other. If I want to do X with Lua, how would you even begin?
First we need a few tools. First is a good text editor, I suggest Notepad++! You should also generate a scripting.html file from FreeSpace. Go into your launcher and in the Dev Tool section, tick the "Output scripting to scripting.html" box. Run FreeSpace and then just quit. In your FreeSpace directory will be a scripting.html file. This will be very valuable to us, it lists all of the game accessible information we can use for our nefarious purposes.
So what is our first project going to be? A super lame "Hello World" script? Those are so overdone. Let's do something that is still simple but is also something you could actually use in your campaign, a SEXP library.
The concept of a SEXP library is simple: we have some SEXPs that we want to use in multiple missions, and instead of copy and pasting them in multiple missions, we will store them in a Lua function that can be called upon in any mission and be executed.
The SEXP library we make will contain 2 groups of SEXPs: 1. Lockdown the player's fighter's for in-game cutscenes and 2. Properly cloaking a ship (linking the stealth with the visual effect).
1. In-game Cutscene Lockdown
We've all been there haven't we? Make an in-game cutscene. Watch it, and accidently slip and press primary fire and hear your Subachs fire. Press an ETS key and hear that immersion breaking sound. It's terrible. So what do you do? Add 5 sexps that lock player weapons, afterburner, ETS etc etc. Such a pain, right? Let's rectify this by allowing us to combine all the sexps we would use into a single Lua function. That way we can just use a single script-eval function and, presto! Those sexps will all get run, no muss, no fuss.
There's quite a few ways you can run Lua scripts. This will not be the only correct way to do things, but it’s a way that I feel is easily readable and accessible. You could stuff all your scripts into a single scripting.tbl, but it might get pretty large and unwieldy if you add a lot of Lua. I like to just create smaller modular scripting tables.
Below is the basic framework of a script, saved as sexplibrary-sct.tbm. The -sct.tbm will clue FreeSpace in that this is a Lua scripting file.
#Conditional Hooks $Application: FS2_Open $On Game Init: [ ] #End
First we need to tell the game what type of hook we're going to run. There are Global Hooks, State Hooks, and Conditional Hooks. Global Hooks run regardless of state or condition. I'd advise staying away from Global Hooks unless you really know what you're doing. State Hooks aren't really used anymore, the Conditional Hooks are more flexible and better. Conditional Hooks will only check actions that we define later if the condition is met. There's also other game-wise benefits to using Conditional Hooks, this part from the scripting.tbl wiki page sums it up well enough.
So I put down #Conditional Hooks, and the condition is $Application: FS2_Open, and our action is $On Game Init:. So basically when our application is FS2_Open (which will be… always), during game initialization do this stuff between the square brackets. And then of course our #End token.
I like to put down any function definitions in $On Game Init. That way all the functions will be ready for any script to use right away. If you're really paranoid about memory usage, you could move it to an initialization function, but you still need to define it somewhere!
So next we should define the Lua function that will house our SEXPs.
function Lockdown(name) end
Function will define Lockdown as the function and the () is where we would put in any additional arguments. Our additional argument here is just name, it will be the name of the ship that we lockdown. And the Lua interpreter needs to know when to stop the function, so that's where the end comes in.
Now we need to think what sexps we would call for Lockdown().
- Lock-primary-weapon
- Lock-secondary-weapon
- Lock-afterburner
- Disable-ETS
- Player-use-AI
- Add-Goal ai-play-dead on the player
- (alternatively ship-immobile if you're not going to use the player's ship in the cutscene)
- Lock-perspective
That's 7 sexps, surely this will increase our FREDding power by 700%!
If we look in the scripting.html and go into the mission library section, we'll find this cute little function.
boolean runSEXP(string)
- Runs the defined SEXP script
- Returns: if the operation was successful
runSEXP, as the name implies, allows us to run a SEXP as if it was being called by a mission event. Which is funny because this function will get called by a mission event anyway! The argument to runSEXP (the part that goes in the ()) is the same text that FRED writes to events. If we make an event that is just lock-primary-weapon and look at it in a text editor, it looks like…
$Formula: ( when ( true ) ( lock-primary-weapon "Alpha 1" ) ) +Name: Lock Primary Weapon +Repeat Count: 1 +Interval: 1
But all we're really interested in is...
(lock-primary-weapon "Alpha 1")
If your sexps have line breaks in them, be sure to take those out so they are all on one line. Lua would think we're done with the sexp long before we actually are.
So our first runSEXP function would look like…
mn.runSEXP("(lock-primary-weapon !" .. name .. "!)")
Wait, hold on. What's this extra stuff I just sprung in front of you? Why is that mn there? And why are there EXCLAIMATION MARKS!! AND DOTS!
runSEXP is a function that is part of a larger library, the mission library. The mission library's internal name is just mn. Anytime we want to use a function inside a library, we need to add mn. in front of it so the Lua interpreter knows where to look.
Now the argument for runSEXP is a string, and Lua sees everything between two " as a string. So if we were to just run
mn.runSEXP("(lock-primary-weapon "Alpha 1")")
The Lua Interpreter would see "lock-primary-weapon" and "". It doesn't see the Alpha 1 because it's in the middle of two pairs of "s. So there's a bit of sly substitution that goes on, all !s are replaced with "s during evaluation. So if you're using the runSEXP or evaluateSEXP functions, replace those "s in the SEXP with !s.
name is a variable that has the argument that we specified before in the function definition. If we were to go...
Lockdown('Alpha 1')
Then name inside the function will be replace with "Alpha 1". The .. is called a concatenation operator. Concatenation is when you combine two strings together. So we combine the "lock-primary-weapon !" string with the name variable, and then combine that with the last "!".
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 that's one line, let us do the same thing to other lines. It would probably be a good idea to create the sexps first in a mission, save the mission and then copy and paste the relevant lines into your script. The game expects SEXPs to be without syntax errors because FRED knows how to do it without any reminders. We fleshy humans aren't that good, so take all precautions!
So here's the full function, plus another function that undoes the SEXPs.
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 function UnLockdown(name) 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
To use these functions within the game, we need to call the script-eval sexp. And it would look something like… [PIC]
Notice how I used a single quotation mark instead of a double one. The reason for this is pretty much the same as using the ! before. It’s a way to define the boundaries of a string without actually breaking the string in the process.
It's sort of hard to show these functions off with screenshots, so you'll just have to try them yourself.
2. Cloaking Ships + Stealth
There's a neat sexp in the special-effects category, ship-effect. Some of the options are cloak and decloak. The only problem is that if you cloak a ship, the effect is only visual! You can still target it. How lame is that? Sure we could just do a lot of ship-effects + ship-stealthy, but why couldn't we just do both at the same time like the Lockdown functions?
function Cloak(name) mn.runSEXP("(ship-effect !Cloak! 1000 !" .. name .. "!)") mn.runSEXP("(ship-stealthy !" .. name .. "!)") mn.runSEXP("(friendly-stealth-invisible !" .. name .. "!)") end function Decloak(name) mn.runSEXP("(ship-effect !Decloak! 1000 !" .. name .. "!)") mn.runSEXP("(ship-unstealthy !" .. name .. "!)") mn.runSEXP("(friendly-stealth-visible !" .. name .. "!)") end
See how easy that was? Especially without me having to explain every detail?
So this is a good start into Lua with FreeSpace without actually needing to know Lua. In the next tutorial, we'll actually get into things like object handles and unique Lua magic.
Full Table use for Tutorial
#Conditional Hooks $Application: FS2_Open $On Game Init: [ 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 function UnLockdown(name) 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 function Cloak(name) mn.runSEXP("(ship-effect !Cloak! 1000 !" .. name .. "!)") mn.runSEXP("(ship-stealthy !" .. name .. "!)") mn.runSEXP("(friendly-stealth-invisible !" .. name .. "!)") end function Decloak(name) mn.runSEXP("(ship-effect !Decloak! 1000 !" .. name .. "!)") mn.runSEXP("(ship-unstealthy !" .. name .. "!)") mn.runSEXP("(friendly-stealth-visible !" .. name .. "!)") end ] #End