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, like deathmatch.lua
  • ship definition files with .ship extensions, like hgn_interceptor.ship
  • missile definition files with .miss extensions, like hgn_longrangetorpedo.miss
  • weapon definition files with .wepn extensions, like hgn_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 and end:
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).


By Novaras aka Fear