Tutorial; Lua - HWRM/KarosGraveyard GitHub Wiki
Lua
Scripts for HWRM are written in Lua 4.1, you can see the official manual here.
💡 One of the main headaches is that the majority of Lua documentation online today is for Lua 5.x, and even the scant few Lua 4.x resources out there will be slightly inaccurate as HWRM does not allow you to use every part of the Lua language, just a subset.
The overwhelming majority of moddable files for HWRM are Lua scripts. This includes:
- scripts with
.lua
extensions, likedeathmatch.lua
- ship definition files with
.ship
extensions, likehgn_interceptor.ship
- missile definition files with
.miss
extensions, likehgn_longrangetorpedo.miss
- weapon definition files with
.wepn
extensions, likehgn_kineticautogun.wepn
There are others, but the main takeaway is that almost every file is actually a Lua script (with major exceptions like .hod
files etc.).
Sandbox
You can set up a Lua sandbox outside of HWRM. This can help with learning the language itself, as you don't need to understand how to invoke a script somehow ingame, nor do you need to continually restart the game to test a script.
Language
Lua is a interpreted scripting language. This means it is read on the fly by the game engine during the game's execution as needed, possibly many times.
Variables
Lua variables are dynamically typed, meaning the type of the variable is just the type of the value its been assigned:
x = 10; -- x is a 'number' type
x = 'hello'; -- x is now a 'string' type
You can see that there is no restriction on changing the type a variable holds. This is very flexible, but can lead to subtle bugs, so be cautious about assigning new values to existing variables.
Variables may also be scoped using the local
keyword:
x = 10;
function f()
local x = 20;
print(x); -- 20
end
print(x); -- 10
Note that, if a variable is defined more than once, the most local version of that variable is used. This concept is called 'shadowing'.
A variable marked local
will only be visible to the current 'scope', which begins and ends within a block. A block is
- a 'chunk', which is typically the entire contents of a script file
- a function:
function f() -- block begins
end -- block ends
- an explicit block using
do
andend
:
do -- block begins
end -- block ends
Blocks may be nested within other blocks.
💡 You should always prefer to keep your variables as well-scoped as possible. This means using local
wherever you can! Having tons of variables all sitting in the same global scope will lead to many hard-to-track bugs.
Upvalues
Lua 4 is somewhat annoying with 'upvalues', which is just a term describing scoped variables from outside the current scope. In most languages, variables from outer scopes are naturally captured:
const x = 10;
console.log(x); // 10
const f = () => {
console.log(x); // 10
}
f();
In Lua 4 however, you must manually indicate that you are capturing an upvalue with the %
syntax:
local x = 10;
print(x); -- 10
function f()
print(x); -- error, x is not defined in this scope
print(%x); -- ok
end
f();
You can also only grab upvals from the immediately surrounding scope:
local x = 10;
local f = function()
print(%x); -- ok
local g = function()
print(%x); -- x is not available in immediately surrounding scope
end
end
Lua also doesn't let you chain this operator, i.e %%x
is not valid. You must manually lift these variables the whole way:
local x = 10;
local f = function()
local x = %x; -- lift it once
print(x); -- ok
local g = function()
print(%x); -- and twice
end
end
Types
Lua comes with a predefined set of 'primitive types':
string
A string is an immutable 'string' of characters:
s = "hello world";
String can be multiline:
s = [[ This is a
multi-line string! ]];
Strings can be appended to eachother:
n = 10;
s = "value of n is currently " .. tostring(n) .. "!"; -- "value of n is currently 10!"
You can 'cast' any value to a string type using tostring
:
nil_type;
num_type = 10;
tbl_type = {};
fn_type = function() end
tostring(nil_type); -- 'nil'
tostring(num_type); -- '10'
tostring(tbl_type); -- 'Table: <table pointer address>'
tostring(fn_type); -- 'Function: <function pointer address>'
There are many stock functions for string manipulation. These generally begin with str
, like strsub
.
number
A double-precision floating point number. Not much surprising about these. Most of the operations you might expect are available:
n = 10;
n = n + 20; -- addition
n = n - 100; -- subtraction
n = n * 2; -- multiplication
n = n / 5; -- division
n = n ^ 2; -- exponentiation
n = -n; -- negation
There are also some predefined math functions such as acos
and sqrt
.
Strings may be cast directly to numbers so long as the Lua interpreter can make sense of the string as a number. The rule of thumb is to imagine the string contents as Lua code you would write otherwise:
s = "10.3";
ss = "ten point three";
n = s + 10; -- 20.3
n = ss + 10; -- error attempting to perform arithmetic on a string
The tonumber
function is a bit more verbose, but also perhaps clearer in intent:
-- `tonumber` is a slightly more expressive method
n = tonumber(s) + 10; -- 20.3
n = tonumber(ss) + 10; -- error
-- tonumber can also cast a number in a different base to base 10
n = tonumber("11", 12); -- 13, as 11 in base 12 is 13 in base 10
table
Tables are Lua's only data structure, and are just associate arrays (key-value pairs).
See a guide on tables here.
function
Functions are reusable blocks of code (see this short guide on functions).
nil
A special 'no-value' type; all variables are nil
before being assigned some other value. nil
will only ever equate to itself (nil == nil
), and no other value.
A nil
value is also the result of a boolean expression returning false (1
is returned otherwise):
truthy = 10 == 10; -- 1
falsy = 10 == 20; -- nil
userdata
Userdata is a special 'custom' type which is defined by the calling C code (for HWRM modding, this is basically a black box). While it's a powerful tool for the authors of the runtime, as modders we have no capability of creating our own custom userdata types.
Userdata is also pretty rare, but appears here and there. For example, Player_GetName
returns a userdata type (which happens to be a wide char string). There is no way to use this value unless there happens to be a stock function which can accept this type of userdata. In this case, wchar strings are accepted by Subtitle_Messagew
and a few others.
Importing Scripts
To avoid all our Lua code being in one megalithic file, we can instead import Lua from another file using dofilepath
like so:
-- path/to/file_a.lua
function doSomething()
end
-- file_b.lua
dofilepath("path/to/file_a.lua"); -- relative to the running executable
doSomething(); -- doSomething is available after importing the file
Also of note is the doscanpath
function, which will load all files from a directory according to a Lua pattern.
❌ As previously noted, most other Lua resources will tell you to load files using functions like dofile
, require
, and others. None of these exist for HWRM's Lua 4 (except dofile
which is occasionally available).
💡 HWRM provides 'relative' file pathing, which is essential for importing files!
Scripting for HWRM
In general, the most time-consuming scripting will be non-trivial (i.e not something like simply defining a ship as seen in a .ship
file).
Perhaps you are writing Rules for a campaign script, or doing some fancy custom behavior for a ship using customcode. In all these cases, the primary concepts to understand are:
And the stock function library (explore it here on Karos using the search functionality).