103 — Blueprints.lua hook - The-Balthazar/SupCom-Mod-Tutorials GitHub Wiki
A working example of the tangible results of this tutorial can be found here.
A powerful scripted alternative to the blueprint merge, is hooking the file
Blueprints.lua
.
It requires an amount of Lua programming, however with only some very basic scripts, large sweeping changes can be made across all units in the game, including those added by other mods, and the whole thing basically amounts to navigating tables like in 102 — Blueprint merge.
Finding the file you want to hook
Now, script hooks are much more restrictive than blueprint merges; you can't
just dump the files in your mod folder and expect them to work: To set up a hook
for a given Lua file, first locate the original. This is where your target game
environment can really start to matter depending on what you plan to hook. Refer
to 102 § finding-the-target-original
but substitute units
for lua
.
For the purposes of this tutorial, locating the original file that we will be hooking will basically only serve as context for some of the actions we will be taking, and to show how it would be done for other files.
Setting up your hook
The file we want to hook is located at lua/system/Blueprints.lua
, so in your
mod folder, create a blank file at hook/lua/system/Blueprints.lua
.
A quick aside; don't actually run the mod at this stage: An empty hook file will cause an error.
When loaded by the game, our hook will have access to everything in the original
without having to redefine it, so there's no need to include anything that we
aren't going to be changing. The only thing we're interested in is the function
ModBlueprints
. Now, we could at this point just have the following in our file:
function ModBlueprints(all_blueprints)
end
And write our new stuff in the middle of that. The problem with this is that it will overwrite anything added by any other mod resolved before this one, which is bad. If every mod did this then loading multiple mods would be like playing a game of snap with who's stuff gets ran.
How we solve this is to do:
local OldModBlueprints = ModBlueprints
function ModBlueprints(all_blueprints)
OldModBlueprints(all_blueprints)
end
As you may well be able to see, what we're doing here is storing the old version
of the function, replacing the function, then calling the old stored version.
Now, this is mostly fine, however other mods hooking this file would be able to
see and potentially accidentally modify our local variable OldModBlueprints
,
which we want to avoid. The way we do this is to surround the whole thing with a
do
block:
do
local OldModBlueprints = ModBlueprints
function ModBlueprints(all_blueprints)
OldModBlueprints(all_blueprints)
end
end
The effect of the do
block here is to define the scope of things created
within it. The bit inside it would still be 'done' without the do
block, but
the reference to OldModBlueprints
will stop existing once the do
block is
over, and it will become inaccessible to other things. Which is what we want.
The same isn't true of ModBlueprints
, because that wasn't defined within the
scope of the block; that was already defined; we just changed it.
So what have we got here
Now that we have our basics set up we are ready to start doing things.
In our ModBlueprints
function we have the variable all_blueprints
. Now, the
name of that function doesn't actually matter, since the names of the function
inputs are decided by the function, and bear no explicit resemblance to what is
actually fed into them. We could rename it to all_bps
or something as long as
we rename all instances of it within that function, since as with the do block,
the function defines a scope and things created within it only exist within it.
What is actually fed into the function is a table. The contents of that table is several other tables. These tables split the blueprints into basic groupings, and inside each of those tables is the actual blueprints. The whole thing basically looks like this:
all_blueprints = {
Mesh = {
--All mesh blueprints
},
Unit = {
--All unit blueprints
},
Prop = {
--All prop blueprints
},
Projectile = {
--All projectile blueprints
},
TrailEmitter = {
--All trail emitter blueprints
},
Emitter = {
--All emitter blueprints
},
Beam = {
--All beam blueprints
},
}
A light example
The blueprints within each of those are defined as tables with the blueprint IDs used as keys. So from here, if we wanted to specifically change the health of the Mech Marine to 69, we could with the following:
do
local OldModBlueprints = ModBlueprints
function ModBlueprints(all_blueprints)
OldModBlueprints(all_blueprints)
all_blueprints.Unit.uel0106.Defense.Health = 69
all_blueprints.Unit.uel0106.Defense.MaxHealth = 69
end
end
As may be apparent, the .
operator is what you use when you want to refer to a
thing with a known string name inside the table to the left of the .
. This has
some downsides in that if any of the tables left of a dot don't exist for
whatever reason, this would cause an error and make the game get stuck on the
loading screen with a warning in the game log.
A serious light example
If we were serious about doing some Mech Marine changes, and didn't want to do this in a blueprint merge so we can have some logic associated with it, we could start with something like this:
do
local OldModBlueprints = ModBlueprints
function ModBlueprints(all_blueprints)
OldModBlueprints(all_blueprints)
MechMarineChanges(all_blueprints.Unit.uel0106)
end
function MechMarineChanges(MMBP)
if MMBP and MMBP.Defense then
MMBP.Defense.Health = 69
MMBP.Defense.MaxHealth = 69
end
end
end
Now, what we're doing there is we're taking the blueprint reference for the Mech
Marine, and feeding it into a function we called MechMarineChanges
which we're
defining below ModBlueprints
. Now, if we defined MechMarineChanges
as a
local, it would need to be before ModBlueprints
, but it's defined as a global
within the scope of the do
block we have surrounding our whole document, so
we can access it from within ModBlueprints
above it.
Within MechMarineChanges
we're calling our first input argument MMBP
,
because we can, and it's descriptive enough and short enough to reference a lot
without requiring a lot of space. When we're working with MMBP
within this
function, it's functionally the same as working with
all_blueprints.Unit.uel0106
in ModBlueprints
, but easier to handle.
What we're actually doing with MMBP
in the function is first checking it
exists, which we can't guarantee, then checking the Defense table within it also
exists. The and
operator in between the two will only do the part after it if
the part before exists. Technically this could still error if MMBP
is
something that isn't a table, which we could check for, but honestly if that's
ever true then something gone badly wrong elsewhere, or someone is playing silly
buggers with the blueprints.
Repeated use
Since we're setting the health of the Mech Marine with a function that we just feed a blueprint, we could if we so wanted then also feed the function other blueprints. Say we wanted to also the Cybran and Aeon LABs, we could do this:
do
local OldModBlueprints = ModBlueprints
function ModBlueprints(all_blueprints)
OldModBlueprints(all_blueprints)
MechMarineChanges(all_blueprints.Unit.uel0106)
MechMarineChanges(all_blueprints.Unit.ual0106)
MechMarineChanges(all_blueprints.Unit.url0106)
end
function MechMarineChanges(MMBP)
if MMBP and MMBP.Defense then
MMBP.Defense.Health = 69
MMBP.Defense.MaxHealth = 69
end
end
end
This is all perfectly fine and will work with no issues. From a stylistic point
though, the name of the function is now a lie; it's not Mech Marine Changes any
more; it's just generically making things have 69 health. MMBP
is also
technically a lie; it's no longer just the Mech Marine blueprint, it's just the
blueprint we happen to be feeding it.
It's also veering headfirst into the realms of being a lot of copy pasted code; this isn't really a problem at this point, and is technically faster to execute than the alternative, but this is code executed once at the start of the game, so it's better to pay the price of setting up a loop, than deal with the inherent human-error prone-ness of unnecessary copy pasted code.
The Lööps
This is what those "fixes" look like:
do
local OldModBlueprints = ModBlueprints
function ModBlueprints(all_blueprints)
OldModBlueprints(all_blueprints)
local UnitsToChange = {
'uel0106',
'ual0106',
'url0106',
}
for i, id in UnitsToChange do
SetTo69Health(all_blueprints.Unit[id])
end
end
function SetTo69Health(bp)
if bp and bp.Defense then
bp.Defense.Health = 69
bp.Defense.MaxHealth = 69
end
end
end
This will have the same effect as the previous, but gets about it in a slightly different way. Now what's going on in these new bits:
First we're defining a new array UnitsToChange
which has the IDs of the units
we wish to change. As you hopefully remember from 102, tables without =
in the
cells to define keys are implicitly given number keys starting from 1, which are
often called indexes instead of keys.
Second, we're creating a for
loop. The i, id
bit is what we will be calling
the index and unit ID we will be working with from the table in each iteration.
The i
will be the implicit numbering we just mentioned, and the id
will be
what we actually want from the table. The in UnitsToChange
part is defining
the table we will be iterating over.
A word of warning in UnitsToChange
technically isn't valid Lua; in later
versions you would be expected to declare in ipairs(UnitsToChange)
instead.
You could do that here, but it isn't necessary.
Anyway, inside that loop we have our renamed function call SetTo69Health
, and
at the end of what we are feeding into it, instead of .uel0106
we have [id]
;
the square brackets is equivalent to the .
, except we can feed it things
without it taking them literally. For example, .id
would try to access a table
inside Unit
called 'id'
, which probably doesn't exist, and isn't what we
want if it does. We could rewrite the whole line to
SetTo69Health(all_blueprints['Unit'][id])
if we wanted to, but since we know
we want the table called 'Unit' at the time of writing we might as well hard
code that instead.
We also renamed MMBP
to bp
so it matches its new use better.
But wait, there's more
But what if this isn't enough for us? What if we're not happy with just setting the LAB health values to 69? What if we want to set every units health to 69?
Well, we could just loop over the whole of all_blueprints.Unit
instead:
do
local OldModBlueprints = ModBlueprints
function ModBlueprints(all_blueprints)
OldModBlueprints(all_blueprints)
for id, bp in all_blueprints.Unit do
SetTo69Health(bp)
end
end
function SetTo69Health(bp)
if bp and bp.Defense then
bp.Defense.Health = 69
bp.Defense.MaxHealth = 69
end
end
end
To explain the changes, UnitsToChange
is no longer needed, so that's gone,
all_blueprints.Unit
is keyed with the unit IDs and the value are now the
actual blueprint we want, so where we once had i
and id
to represent the
implied [1] = 'uel0106'
we had in UnitsToChange
, we now have id
and
bp
to represent the uel0106 = { ... }
we now have while looping through
all_blueprints.Unit
. And since the bp
is what we were navigating to before
to feed the SetTo69Health
function, we can just feed it that directly.
We could have left it as SetTo69Health(all_blueprints.Unit[id])
, but since we
already have a direct reference to the blueprint, bp
, we might as well use it.
Another aside: In much the same way that in modern Lua we'd want
ipairs(UnitsToChange)
, here we'd want pairs(all_blueprints.Unit)
. The reason
it's pairs
and not ipairs
is because ipairs
is for when the keys are a
sequential list of numbers starting at 1, and pairs
is just for arbitrary
tables. You could use pairs
on an array instead of ipairs
but it'd be slower
and it wouldn't do them in order.
696969
But what if that's not enough 69 for us? What if instead of giving everything 69 health we wanted to set everything to have an amount of health that's the same length of the original, but all 69's instead? Well for that we could do:
do
local OldModBlueprints = ModBlueprints
function ModBlueprints(all_blueprints)
OldModBlueprints(all_blueprints)
for id, bp in all_blueprints.Unit do
SetTo69Health(bp)
end
end
function SetTo69Health(bp)
if bp.Defense and bp.Defense.MaxHealth then
local mathsWizardry = math.floor(math.pow(10, math.floor(math.log10(bp.Defense.MaxHealth))) * 6.96969696969696969696969696)
bp.Defense.Health = mathsWizardry
bp.Defense.MaxHealth = mathsWizardry
end
end
end
You'll notice we changed bp and bp.Defense
into bp.Defense and bp.Defense.MaxHealth
;
the reason for that being we already know bp
exists because we're working from
the actual blueprints list not a list of ones we want to work from, and we added
bp.Defense.MaxHealth
because we plan to do maths to it, we need to make sure
it exists first.
As for the maths wizardry; math.log10
is basically doing the opposite of what
math.pow(10
is doing. This is a gross simplification and people who know maths
probably just cringed a little. Without the math.floor
operation in between
the two, i.e.: math.pow(10, math.log10(bp.Defense.MaxHealth))
, the result
would be equal to bp.Defense.MaxHealth
, but with the floor, that part is
giving a number that is a 1 and a bunch of 0's. However many 0's would be needed
to make it the same length as the original.
Then the outer parts of that are essentially taking the number from the inner part, which is probably 100 1000 or 10000, and multiplying it by 6.96 etc., and discarding the excess decimals. Giving us, in those examples, 696, 6969 or 69696.
It's done in a local both so that we don't have to calculate it twice, but also so that we don't have to write it twice or copy paste it.
Closing with categories
And at that point we've pushed this meme about as far as it can go without
adding in some extra logic for giving less game warping numbers. So I'll close
with giving some details on how we would effectively add and remove values from
blueprint arrays like Categories
, as promised in 102:
table.insert(bp.Categories, 'NEWCATEGORY')
table.removeByValue(bp.Categories, 'OLDCATEGORY')
Using everything we've learned in this tutorial so far, you should be able to
make effective use of them. Remember to check bp.Categories
exists first, and
well done for making it this far.