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.