Tutorial; Custom Coding - HWRM/KarosGraveyard GitHub Wiki

Custom Code

Custom code refers to scripts which are invoked for ships during gametime. This is an extremely powerful tool which allows you have a ship call any arbitary script at various points in its lifetime.

Any mod code is by definition 'custom code', but these ship-scripts in particular are known as custom code due to how the code and ship are linked, which is using the function addCustomCode

To avoid being ambiguous, I'll just call it 'customcode', all one-word.

💡 If you're doing any scripting for this game, make sure to add the -luatrace flag in launch options! This causes useful output to be generated in C:\Program Files (x86)\Steam\steamapps\common\Homeworld\HomeworldRM\Bin\Release\HwRM.log.

Using customcode

To hook a ship up with its own customcode, we need to tell it where to find some specific functions, which are the four main 'hooks'.

Using the Hiigaran Torpedo Frigate as an example, let's put our customcode right besides the ship definition (this is how the stock files do it, but its not strictly necessary).

ship
|- hgn_torpedofrigate
   |- hgn_torpedofrigate.lua
   |- hgn_torpedofrigate.ship

Our new script is the hgn_torpedofrigate.lua file. We can call it anything we want, actually, but the stock files do it like this, so let's copy them for now.

In hgn_torpedofrigate.lua:

function load()
end

function create(group, player_index, ship_id)
end

function update(group, player_index, ship_id)
end

function destroy(group, player_index, ship_id)
end

In order:

  • load: Generally not so useful, load accepts no parameters and appears to run during the loading screen. The best we can do here is to create a sobgroup or something.
  • create: The first of the big three, create is called when a level finishes loading and a ship is spawned, or when a ship is constructed, or is manually spawned in through a script.
  • update: Possibly the most useful of all, update is called periodically (with an interval we can configure in the .ship file), and recieves the same parameters as create
  • destroy: Finally, destroy is called only when a ship dies, and again receives the same params.

The parameters these functions recieve are what allows us to interact with a single ship/squad:

  1. group: usually called CustomGroup in the stock files, this group contains only the callee ship (and its squadron mates)
  2. player_index: or playerIndex in the stock files, is the player index of the owner of this ship
  3. ship_id: this is the internal id of the ship, we can use this to create further unique sobgroups for example

All of this is set up in the hgn_torpedofrigate.ship file:

-- stuff...

addCustomCode(NewShipType, "data:ship/hgn_torpedofrigate/hgn_torpedofrigate.lua", "load", "create", "update", "destroy", "hgn_torpedofrigate", 1);

See addCustomCode

In order, we pass the script location, the four hook functions, the string name of the first param (group), and finally we pass the interval at which the update hook will be called (in seconds).

💡 When you hook a ship up with customcode, these functions will be run for every ship of that type! If ten scouts are calling their update hook, your code will be executed ten times, once for each.

Example: auto-repair vettes

If you read the next section, we will try to create a script from a more ambiguous scope, and will be unable to implement the following:

  • Repair vettes which automatically repair any nearyby player ships in 2000 units radius

With customcode however, we can do this easily:

-- ship/kus_repaircorvette/kus_repaircorvette.lua

function update(group, player_index, ship_id)
  SobGroup_CreateIfNotExist("closeby_player_ships");
  Player_FillProximitySobGroup("closeby_player_ships", 0, group, 2000);
  SobGroup_RepairSobGroup(group, "closeby_player_ships");
end
-- ship/kus_repaircorvette/kus_repaircorvette.ship

addCustomCode(NewShipType, "data:ship/kus_repaircorvette/kus_repaircorvette.lua", "", "", "update", "", "kus_repaircorvette", 1);

You'll notice we don't need to provide all the hooks if we don't plan to use them. We can also name the hooks and the sobgroup anything we like:

-- ship/kus_repaircorvette/kus_repaircorvette.lua

function myUpdateFunction()
  -- ...
end
-- ship/kus_repaircorvette/kus_repaircorvette.ship

addCustomCode(NewShipType, "data:ship/kus_repaircorvette/kus_repaircorvette.lua", "", "", "myUpdateFunction", "", "kus_repvette", 1);

The casing does matter!

Functions, SobGroups, and Players

There are two main entities to understand when coding mission scripts or customcode, and those are SobGroups and Players.

Interacting with these objects is achieved through the large stock library of functions, the majority of which are documented here on Karos (use the search panel on the right side).

Note that Karos is not exhaustive in its coverage. See the full list of SobGroup_ functions here, for example.

SobGroups

A SobGroup (or just 'sobgroup'), is an in-engine collection of ships. As a modder, you cannot interact directly with ships, only sobgroups. As such, most functions which can manipulate ships during gametime are prefixed with SobGroup_. (A full list can be found here.) In reality you never truly interact with sobgroups either; all we really get is a string with the name of the group:

SobGroup_Create("my-sobgroup");

Here we named a new group, 'my-sobgroup'. Further sobgroup calls will need this name in order to know which group they need to interact with.

-- its useful to store a name in a variable so we can reuse it without typing it out exactly:
local name = "my-sobgroup";

SobGroup_Create(name); -- make the new group
SobGroup_FillShipsByType(name, "Player_Ships0", "hgn_torpedofrigate"); -- fill it with player 0 ships of this type
SobGroup_SetHealth(name, 0.1); -- set the health of all ships in the group to 0.1 (10% health)
-- etc..

Note that these functions are extremely picky about their arguments: if you pass too few, or the wrong types, the whole script will crash on that line (the game will continue running normally). To detect stuff like this, make sure to use the -luatrace launch option.

Trying to use a group which doesn't exist will naturally also not work.

Groups can be created in different places:

You should note, however, that mission scope is able to access groups defined in the .level, but customcode is NOT able to access these groups! In fact, customcode and mission (/level) are isolated environments.

Players

Players are a predefined list, as set out in the .level file.

Player 0 will always be 'Player One', i.e

  • the human player during campaign missions
  • the host of a multiplayer game

Just as with sobgroups, you can only interact with players indirectly using their player index (or player id).

Player_GrantAllResearch(0); -- grants all unrestricted research items to player 0

Unlike sobgroups, there is no way to manually create or destroy players, although killing all a players ships is easy:

Player_Kill(0); -- 'kills' player 0, and all their ships

This player still exists, but is considered 'dead'.

There is significant overlap between player and sobgroup when it comes to ship selection etc:

function update(group, player_index, ship_id)
  local player_torps = "torps-group";

  SobGroup_CreateIfNotExist(player_torps);
  SobGroup_Clear(player_torps);
  Player_FillShipsByType(player_torps, 0, "hgn_torpedofrigate");
end

Persistent State

All customcode scripts share the same scope, and state is persisted between update calls.

Being able to track information over many script runs for a specific ship by its unique ID is only possible with custom code.

Here, we implement a custom behavior for ships which can cloak: The ship will cloak if it begins taking damage (from any source), and will uncloak if its cloak was previously triggered by this script and a certain amount of time has passed since the last instance of damage.

SHIP_DATA = {};

DAMAGE_TIMEOUT = 5; -- in seconds

--- Gets the ship data for the given `ship_id`, initialising it if it doesn't exist yet.
function SHIP_DATA:get(ship_id, group)
  self[ship_id] = self[ship_id] or { HP =  SobGroup_GetHealth(group), last_damaged = nil };
  return self[ship_id];
end

-- the update function to refer to in the `addCustomCode` call in the `.ship` file:
function update(group, player_index, ship_id)
  local data = SHIP_DATA:get(ship_id, group);

  -- get the current HP of this ship:
  local current_hp = SobGroup_GetHealth(group);
  -- get the recorded HP of this ship:
  local previous_hp = data.HP;

  -- if current HP is less than previous, we're taking damage! (the values will be equal on the first run)
  if (current_hp < previous_hp) then
    -- if we aren't currently cloaked...
    if (SobGroup_IsCloaked(group) == 0) then
      -- cloak!
      SobGroup_CloakToggle(group);
    end
    data.last_damaged = Universe_GameTime();
  elseif (SobGroup_IsCloaked(group) == 1 and data.last_damaged) then -- we're cloaked and it was due to this script (timeout was set)
    -- if the current time - the recorded time of last damage is more than the timeout...
    local timeout_expired = Universe_GameTime() - data.last_damaged > DAMAGE_TIMEOUT;
    if (timeout_expired) then
      SobGroup_CloakToggle(group); -- uncloak! no damage taken in `DAMAGE_TIMEOUT` seconds
      data.last_damaged = nil;
    end
  end

  data.HP = SobGroup_GetHealth(group);
end

Without being able to store data about each ship by it's unique ID, you would not be able to do things like 'wait for DAMAGE_TIMEOUT seconds to pass since this ship took damage before uncloaking).


Check out modkit.

Original by Novaras aka Fear