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 ascreate
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:
group
: usually calledCustomGroup
in the stock files, this group contains only the callee ship (and its squadron mates)player_index
: orplayerIndex
in the stock files, is the player index of the owner of this shipship_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:
- in
.level
files usingcreateSOBGroup
(before the game sim is running) - in mission scripts using
SobGroup_Create
(during game sim) - in ship scripts (customcode) using
SobGroup_Create
(during game sim)
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.