Custom Code Examination

I’ve waited too long for official documentation or for someone else to crack this nut, so it’s time for me do my best to brain at them. I figured I ought to post my preliminary results as I go, for general collaboration and so the info will be out there if I get distracted by something else.

So first I cracked open the bigs and searched for every instance of custom code, and realized something that had eluded me so far. There are two things at work here. One is a “CustomCommand” option in addAbility, and one is the all-new addCustomCode. Both refer to a lua file outside the .ship for their logic.

Next I’ve copied every instance of these calls I found, grouped them together, and tried to pretty-print them to a point where I could get an idea of their structure. Functions with this many parameters are always a pain to get a handle on without the parameter names…

The sense I get at first glance is that addCustomCode is used to give constant special behavior or properties to a ship, while addAbility-CustomCommand is just for directly player-activated behavior. I focused on CustomCommand first, since there are fewer instances of it.

I trimmed some repeated or not really helpful info from the collected data, and split it into two blocks for readability. For reference, here’s an example of a raw line:


On to the meat…


    "Drones",       1,  1,  1000,   0,      0.25,   2,      0,  "..Kus_DroneFrigate.lua",
    "Drones",       1,  0,  1000,   200,    0.25,   2,      0,  "..Kus_DroneFrigate.lua",
    "$3191",        1,  0,  1000,   500,    0.55,   0.75,   0,  "..Kus_GravWellGenerator.lua",
    "Speed Burst",  1,  0,  1000,   0,      1,      0.5,    0,  "..Kus_Scout.lua",
    "$3191",        1,  0,  1000,   500,    0.55,   0.75,   0,  "..Tai_GravWellGenerator.lua",
    "Speed Burst",  1,  0,  1000,   0,      1,      0.5,    0,  "..Tai_Scout.lua",
    "Speed Burst",  1,  0,  1000,   0,      1,      0.5,    0,  "..Kus_Scout.lua",

So we have name, sometimes localized so it’ll be what’s shown to the player then a bunch of numeric values. I suspect the ones that are only ever 1 or 0 are true/false flags, but what they’re flagging is hard to guess. My best guess for the mess of numbers would be for the power bar, with capacity, delay, recovery rate, that kind of thing. That’s just a best guess, though. I ought to check out the documentation on the predefined power-bar using abilities and compare to see if the parameters for them line up with these. Then there’s a path to the lua containing the logic, which I’ve truncated for brevity. It seems probable that you could have ships using the same ability calling a common lua file somewhere besides their ship dir, but if so I wonder why gearbox didn’t use that for the duplicated abilities.

    "start_dronefrigate",           "do_dronefrigate",              "finish_dronefrigate",          "kus_dronefrigate",         1.03,   2,  0)
    "Start_DroneFrigate",           "Do_DroneFrigate",              "Finish_DroneFrigate",          "Kus_DroneFrigate",         1.15,   2,  1)
    "Start_Kus_GravWellGenerator",  "Do_Kus_GravWellGenerator",     "Finish_Kus_GravWellGenerator", "Kus_GravWellGenerator",    1.9,    1,  1)
    "Start_Kus_Scout",              "Do_Kus_Scout",                 "Finish_Kus_Scout",             "Kus_Scout",                1.4,    3,  1)
    "Start_Tai_GravWellGenerator",  "Do_Tai_GravWellGenerator",     "Finish_Tai_GravWellGenerator", "Tai_GravWellGenerator",    1.9,    1,  1)
    "Start_Tai_Scout",              "Do_Tai_Scout",                 "Finish_Tai_Scout",             "Tai_Scout",                1.4,    3,  1)
    "Start_Kus_Scout",              "Do_Kus_Scout",                 "Finish_Kus_Scout",             "Kus_Scout",                1.4,    3,  0)

Then there are these parameters, the first three of which are function names in the referenced file. The fourth I’m unsure of, but I presume is passed to the lua. Due to some looking ahead at the addCustomCode parameters, I’m guessing this is entirely cosmetic, but I’ll have to keep an eye out for that in inspecting the lua files.

The function parameters, just from the names, are presumably called at the start of the ability, durring the maintenance of the ability, and at the end of it respectively. It’s also conceivable that the start and end are called at the ship’s creation and destruction, so that’s something to look for context cluse to in the lua files themselves.

Last is three more number parameters. Again, the last one’s only appearing as 1 or 0 makes me think it’s a true/false flag, but that’s only a guess and I have no clue what it controls. The other two I’m similarly clueless about. They could be fed to the functions as parameters, which would be useful if one lua was called by multiple ships, or they could control something about the timing and update rate of the code. Something else to keep an eye out for context clues to.

ed: Xercodo’s findings are that the penultimate parameter is a hotkey and index, referencing Playercfg.lua, commanduidefines.lua, data/scripts/keybindings.lua, and tb_commandpanel.lua. Sastrei noted that gravwell gen in the updated files has an extra parameter that seems to control if something blows up after using it’s ability. :ed

More later. But, here’s the addCustomCode comparison block…


    "...Ben_BentusiExchange.lua",       "",                         "",                                 "Update_Ben_BentusiExchange",       "",                                 "Ben_BentusiExchange",      30)
    "...Kus_DroneFrigate.lua",          "",                         "create_dronefrigate",              "update_dronefrigate",              "destroy_dronefrigate",             "kus_dronefrigate",         1.03)
    "...Hgn_Carrier.lua",               "",                         "Create_Hgn_Carrier",               "Update_Hgn_Carrier",               "",                                 "Hgn_Carrier",              4)
    "...Hgn_Mothership.lua",            "",                         "Create_Hgn_Mothership",            "Update_Hgn_Mothership",            "",                                 "Hgn_Mothership",           4)
    "...Kad_Swarmer.lua",               "",                         "createSwarmerFuel",                "updateSwarmerFuel",                "destroySwarmerFuel",               "AdvancedSwarmer",          0.33)
    "...Kad_MultiBeamFrigate.lua",      "",                         "",                                 "updateKad_MultiBeamFrigate",       "",                                 "Kad_MultiBeamFrigate",     1.25)
    "...Kad_Swarmer.lua",               "",                         "createSwarmerFuel",                "updateSwarmerFuel",                "destroySwarmerFuel",               "Swarmer",                  0.33)
    "...Kus_Carrier.lua",               "Load_Kus_Carrier",         "Create_Kus_Carrier",               "Update_Kus_Carrier",               "",                                 "Kus_Carrier",              10)
    "...Kus_CloakGenerator.lua",        "",                         "",                                 "Update_Kus_CloakGenerator",        "",                                 "Kus_CloakGenerator",       10)
    "...Kus_Drone0.lua",                "",                         "",                                 "Update_Drone0",                    "",                                 "Kus_Drone0",               60)
    "...Kus_DroneFrigate.lua",          "",                         "Create_DroneFrigate",              "Update_DroneFrigate",              "Destroy_DroneFrigate",             "Kus_DroneFrigate",         1.15)
    "...Kus_GravWellGenerator.lua",     "",                         "Create_Kus_GravWellGenerator",     "Update_Kus_GravWellGenerator",     "Destroy_Kus_GravWellGenerator",    "Kus_GravWellGenerator",    1.9)
    "...Kus_Mothership.lua",            "Load_Kus_Mothership",      "Create_Kus_Mothership",            "Update_Kus_Mothership",            "Destroy_Kus_Mothership",           "Kus_Mothership",           4)
    "...Kus_RepairCorvette.lua",        "",                         "Create_Kus_RepairCorvette",        "Update_Kus_RepairCorvette",        "Destroy_Kus_RepairCorvette",       "Kus_RepairCorvette",       15)
    "...Kus_ResearchShip.lua",          "Load_Kus_ResearchShip",    "Create_Kus_ResearchShip",          "Update_Kus_ResearchShip",          "Destroy_Kus_ResearchShip",         "kus_researchship",         2.2)
    "...Kus_SensorArray.lua",           "",                         "",                                 "Update_Kus_SensorArray",           "",                                 "Kus_SensorArray",          10)
    "...Meg_Crate_HW1Container.lua",    "",                         "",                                 "Update_Meg_Crate_HW1Container",    "",                                 "Meg_Crate_HW1Container",   5)
    "...Meg_Relic_RUGenerator.lua",     "",                         "",                                 "Update_Meg_Relic_RUGenerator",     "",                                 "Meg_Relic_RUGenerator",    5)
    "...Tai_Carrier.lua",               "Load_Tai_Carrier",         "Create_Tai_Carrier",               "Update_Tai_Carrier",               "",                                 "Tai_Carrier",              10)
    "...Tai_CloakGenerator.lua",        "",                         "",                                 "Update_Tai_CloakGenerator",        "",                                 "Tai_CloakGenerator",       10)
    "...Tai_DefenseFighter.lua",        "",                         "",                                 "updateDefenseFighter",             "",                                 "DefenseFighter",           1.25)
    "...Tai_Destroyer.lua",             "Load_Tai_Destroyer",       "Create_Tai_Destroyer",             "Update_Tai_Destroyer",             "",                                 "Tai_Destroyer",            10)
    "...Tai_FieldGeneratorDummy.lua",   "",                         "Create_Tai_FieldGeneratorDummy",   "Update_Tai_FieldGeneratorDummy",   "Destroy_Tai_FieldGeneratorDummy",  "Tai_FieldGeneratorDummy",  3/8)
    "...Tai_GravWellGenerator.lua",     "",                         "Create_Tai_GravWellGenerator",     "Update_Tai_GravWellGenerator",     "Destroy_Tai_GravWellGenerator",    "Tai_GravWellGenerator",    1.9)
    "...Tai_Mothership.lua",            "Load_Tai_Mothership",      "Create_Tai_Mothership",            "Update_Tai_Mothership",            "Destroy_Tai_Mothership",           "Tai_Mothership",           4)
    "...Tai_RepairCorvette.lua",        "",                         "Create_Tai_RepairCorvette",        "Update_Tai_RepairCorvette",        "Destroy_Tai_RepairCorvette",       "Tai_RepairCorvette",       15)
    "...Tai_ResearchShip.lua",          "Load_Tai_ResearchShip",    "Create_Tai_ResearchShip",          "Update_Tai_ResearchShip",          "Destroy_Tai_ResearchShip",         "tai_researchship",         2.2)
    "...Tai_SensorArray.lua",           "",                         "",                                 "Update_Tai_SensorArray",           "",                                 "Tai_SensorArray",          10)
    "...Vgr_Carrier.lua",               "",                         "Create_Vgr_Carrier",               "Update_Vgr_Carrier",               "",                                 "Vgr_Carrier",              4)
    "...Vgr_Mothership.lua",            "",                         "Create_Vgr_Mothership",            "Update_Vgr_Mothership",            "",                                 "Vgr_Mothership",           4)

I don’t know about you, but I was certainly surprised by some of the names on that list.


Here’s my findings on the topic: CustomCommand Demystified!...mostly...sorta

Huh. If I read that topic, it totally flew my mind. I’ll have to give it a look over and see where I can build on it any.

Yeah mine was mostly digging into how the key bindings were defined for these custom commands.

A little housekeeping, my results above were from the base data files, not the updated ones. That was a silly oversight, and future excerpts will be from the updated versions if there are any. But I’m not redoing those tables again any time soon.

Taking a look at those addCustomeCode parameters, it’s a familiar pattern to the custom code ones. A filename, a bunch of what are certainly function names, a ship name, and a number. I’d bet on the number being update rate. Also, see how the last string parameter does not match up to the ship name in all cases. That’s why I think it’s cosmetic only, in other parts of the game if a ship type is to be passed around, in the AI for instance, it’s passed around as a variable that shares it’s name with the ship file, in a all capitals. Some of the SCAR functions might take names as strings, but they certainly wouldn’t like getting “Swarmer” instead of “Kad_Swarmer”.

Anyway, two things jump out. In the custom abilities we had start, do, and finish. Here we have load, create, update, and destroy. load is presumably executed on level start, which is exciting because it implies a need to set up some state. I was afraid we wouldn’t have access to any persistent state inside the code, so that’s encouraging. The other interesting thing is ships like the cloakgen, multibeam frigate, and tai destroyer being on the list. No idea what special they could be doing, so… lets open one and look. kad_multibeamfrigate only has update, so it should be simple to look at.


That’s the line from the ship file, for reference. and…

function updateKad_MultiBeamFrigate(CustomGroup, playerID, shipID) 
		if SobGroup_OwnedBy(CustomGroup) ~= 0 then	
			SobGroup_AttackPlayer(CustomGroup, 0)	

The whole lua file. So… the engine passes it some things. What is being passed into CustomGroup, I don’t know exactly. A SOBgroup, it must be, probably just containing this ship. playerID and shipID… the owning player, and an identifier for the ship, almost certainly, but the question is if the shipID identifies this particular ship or the type of ship. Probably the latter, the former would make the latter redundant. PLayerID is odd, because if it’s the player index, the code in here is redundant.

That code, incase it wasn’t clear, gets the index of the owner of the CustomGroup(so most likey, the owner of the ship), and if it’s not 0(the first player slot, or the human in a single-player game), it orders the group to attack player 0. I guess the kadeshi ships weren’t being agressive enough at some stage.

Lets look at another one. I’m curious about destroyers.


function Load_Tai_Destroyer(playerIndex)

function Create_Tai_Destroyer(CustomGroup, playerIndex, shipID) 	

function Update_Tai_Destroyer(CustomGroup, playerIndex, shipID)
	if SobGroup_GroupInGroup("Defector", CustomGroup) == 1 then
		if SobGroup_GetHardPointHealth(CustomGroup, "Engine") < 1 then			
			SobGroup_SetHardPointHealth(CustomGroup, "Engine", 1)

Looks like more SP specific logic. The load and create just make sure a certain sobgroup exists, and update makes EXTRA sure that sobgroup exists, and if this ship is in it, keeps the engine hardpoint at full health. Presumably so Elson’s engines never get broken.

What are those motherships doing with their lua, then? All four are doing things with some overlap, the kus_ one seems to do everything the others do and more so I’ll go with that.

function Load_Kus_Mothership(playerIndex)	

function Create_Kus_Mothership(CustomGroup, playerIndex, shipID)  	
	if playerIndex == Universe_CurrentPlayer() then
		--UI_SetElementSize("NewResearchMenu", "Utility", 0, 0);
		UI_SetElementSize("NewResearchMenu", "Platform", 0, 0);

function Update_Kus_Mothership(CustomGroup, playerIndex, shipID)		
	SobGroup_SobGroupAdd("kus_mothership"..playerIndex, CustomGroup)	
	--preventing to have 2 research ship
	if SobGroup_IsBuilding("kus_mothership"..playerIndex, "kus_researchship") == 1 then
		SobGroup_RestrictBuildOption("kus_carrier"..playerIndex, "kus_researchship")
		SobGroup_UnRestrictBuildOption("kus_carrier"..playerIndex, "kus_researchship")
	for i = 1,5,1 do
		if SobGroup_IsBuilding("kus_mothership"..playerIndex, "kus_researchship_"..i) == 1 then
			SobGroup_RestrictBuildOption("kus_carrier"..playerIndex, "kus_researchship_"..i)
			SobGroup_UnRestrictBuildOption("kus_carrier"..playerIndex, "kus_researchship_"..i)
	if Player_GetNumberOfSquadronsOfTypeAwakeOrSleeping(-1, "Special_Splitter" ) == 0 then		
		SobGroup_AbilityActivate(CustomGroup, AB_Move, 0)
		SobGroup_AbilityActivate(CustomGroup, AB_Dock, 0)				
		--btn hyperspace
		if UI_IsNamedElementVisible("NewTaskbar", "btnHW1SPHyperspace") == 1 then		
			--SobGroup_AbilityActivate("Player_Ships0", AB_Hyperspace, 1)
			SobGroup_AbilityActivate("Player_Ships0", AB_Hyperspace, 0)
		SobGroup_AbilityActivate(CustomGroup, AB_Move, 1)
		SobGroup_AbilityActivate(CustomGroup, AB_Dock, 1)	

function Destroy_Kus_Mothership(CustomGroup, playerIndex, shipID)	

Ho boy. I don’t really like the looks of this code. All clears and adds seem unneeded if CustomGroup is indeed a sob containing just this ship, and don’t make much sense if it isn’t that. And this logic seems prone to breaking if a mod puts more than one mothership in a player’s fleet, so mods should watch out for that. This seems particularly true of the Create_ function, different motherships collapse different parts of the research UI, I’d personally rather that functionality was somewhere in rule or race scripts, it’d be easier to find if someone wanted to modify it, and wouldn’t risk getting weird if mods did unusual things to your fleets.

Outside of that, the update does some interesting things. Manages the complex research ship construction situation, mainly. The SP block looks like it mainly interacts with the end of mission screen.

The Special_Splitter thing has my curiosity, but I suspect that’ll take a dive into the campaign files to understand. Maybe later.

I poked around in several files looking for a juicy file, and boy did I find it.

--A table, by shipIP, of fuel tank values
Kad_Swarmer_FuelTanks = {}
Kad_Swarmer_TurnCounter = 0

function createSwarmerFuel(CustomGroup, playerID, shipID)	
    Kad_Swarmer_FuelTanks[shipID] = 1

function updateSwarmerFuel(CustomGroup, playerID, shipID)
    -- A lot of swarmers can cause the game to chug as they're all updated at once.  Spread them out a bit...
    Kad_Swarmer_TurnCounter = Kad_Swarmer_TurnCounter + 1
    if (Kad_Swarmer_TurnCounter > 4) then
        Kad_Swarmer_TurnCounter = 0

    if (mod(shipID, 4) ~= Kad_Swarmer_TurnCounter) then

    -- Don't do anything while docked (as in, not yet lauched from mothership)
    if (SobGroup_CountByFilterInclude(CustomGroup, "CurrentCommandState", "DOCKSTATE_Docked") > 0) then
        --print("Swarmer "..shipID.." is docked - nothing to do.")

    SobGroup_FillShipsByType("swarmerFuelPodGroup", "Player_Ships"..playerID, "kad_fuelpod")
    if SobGroup_FillProximitySobGroup("swarmerNearbyFuelPodGroup", "swarmerFuelPodGroup", CustomGroup, 40) == 0 then
        local consume = SobGroup_GetActualSpeed(CustomGroup)/8000000


        if Kad_Swarmer_FuelTanks[shipID] <= 0.2 then
            if SobGroup_GetActualSpeed(CustomGroup) > 10 then
                FX_StartEvent(CustomGroup, "EngineFailure")
            local speed = Kad_Swarmer_FuelTanks[shipID] * 2
            if speed < 0.4 then
                speed = 0.4
            SobGroup_SetSpeed(CustomGroup, speed)
            if SobGroup_IsDoingAbility(CustomGroup, AB_Dock) == 0 then
								if SobGroup_FillProximitySobGroup("swarmerNearbyFuelPodGroup", "swarmerFuelPodGroup", CustomGroup, 100000) == 1 then										
										if SobGroup_Count("swarmerNearbyFuelPodGroup") > 1 then
												local r = random(0,SobGroup_Count("swarmerNearbyFuelPodGroup")-1)
												SobGroup_FillShipsByIndexRange("swarmerFuelPodToDock", "swarmerNearbyFuelPodGroup", r, 1)
												SobGroup_DockSobGroup(CustomGroup, "swarmerFuelPodToDock")
												SobGroup_DockSobGroup(CustomGroup, "swarmerNearbyFuelPodGroup")
									SobGroup_SetSpeed(CustomGroup, 0)
        elseif Kad_Swarmer_FuelTanks[shipID] > 0.2 then
            SobGroup_SetSpeed(CustomGroup, 1)
						if SobGroup_OwnedBy(CustomGroup) ~= 0 then
                            --KAS uses the tactics to control retaliation.  Sometimes it's set to non-retaliatory mode via PassiveTactics (2).
                            if (SobGroup_GetTactics(CustomGroup) ~= 2) then
							    SobGroup_AttackPlayer(CustomGroup, 0)	
        Kad_Swarmer_FuelTanks[shipID] = 1
        SobGroup_SetSpeed(CustomGroup, 1)

function destroySwarmerFuel(CustomGroup, playerID, shipID)
    Kad_Swarmer_FuelTanks[shipID] = nil

-- Take some fuel off
function Kad_Swarmer_DecreaseFuel(shipID,value)
    if (Kad_Swarmer_FuelTanks[shipID] == nil) then
        Kad_Swarmer_FuelTanks[shipID] = 1

    if Kad_Swarmer_FuelTanks[shipID] >= value then
        Kad_Swarmer_FuelTanks[shipID] = Kad_Swarmer_FuelTanks[shipID] - value

Jackpot, persistent state! This is seriously exciting stuff. The global state is declared outside of any of the functions, which is fairly normal for HW2 script files. I have to wonder if the code here shares scope with normal gamerule execution, or if they’re walled off from each other. Should be fairly easy to check that later. There’s some interaction in that they both share the same sobgroups, based on the destroyer logic, but that’s interacting with game state so it’s not definitive. If they do share a sope it’d be wise to be careful naming your functions and globals to minimize the odds of accidental collisions

So, the global state set up here includes a table, and then all the functions use the shipID as a table index. This clears up the ambiguity about shipID, for this logic to work it has to be a unique identifier for that instance of that ship, not a ship class identifier.

At this point I think I have enough info to try implementing a feature in HW@. I’ll probably post more here if doing that teaches me anything.

As for CustomCommand, here is the explanation from Gearbox:

The parameters of the custom command more or less followed the parameters of the DefenseField command. The specific parameters for the command are (after the first 3 that are the same for all command definitions in a .ship file):

capacity - maximum energy for command
min level before manual shutdown allowed - usage must be below this to allow command to be turned off
usage - starting energy for command
recharge - rate at which energy replenishes (applied every 1/10th of a second)
max level before restart allowed - usage must be above this to allow command to be turned on
custom code script name - script file that contains custom code for this command.
custom code custom command start function name - LUA function called when command is activated.
custom code custom command update function name - LUA function called periodically while command is active.
custom code custom command stop function name - LUA function called when command is deactivated or runs out of energy.
custom code custom command sob group name - squadron command is running on is added to this sob group which is passed in as a parameter to the LUA support functions.
custom code custom command update function time interval - time in seconds between calls to the LUA update support function.
custom command index - An index used to map this into the UI support for custom commands.
custom command flag to make it run as latent - If true, this custom command can run while another command is active (used to allow commands to keep running while you move ships, attack, etc).
custom command flag to destroy ship when depleted - When usage energy runs out, ship will explode.


Great info. Where’d it come from? That level of documentation for the new stuff is something I’m quite interested in if there’s more of it out there.

It’s several months ago, when we send a message to BitVenom to ask things about that, he told as to ask another guy whose name is pdeupree. And pdeupree gives us those information

1 Like

And this is the information we have found by ourselves:


It seems probable that you could have ships using the same ability calling a common lua file somewhere besides their ship dir

yes, and that’s what we have done in X system, which is used in FX:Galaxy MOD and SEED MOD

As for any function be called in a customcode lua (except the “Load” function ), it receives three parameters from the game engine:CustomGroup, playerIndex, shipID

CustomGroup is a string, which defined as < sob group name> in CustomCommand. It’s the name of a sobgroup which contain the individual ship who has called the function.

and playerIndex is the playerID; shipID is a number which is different for every individual ship.

These three parameters are received from the game engine, which means even the function itself doesn’t use one of the three parameters, it still must receive all of them.

1 Like