Scripted Effect Bundles

NOTE: This guide assumes solid knowledge of effects and effect bundles, and a beginner level’s knowledge of Lua. Next time you level up, spec in those skills and then come back.

As of the Hunter & the Beast DLC for WH2, there’s a new delightful system in place for modders across the globe. Prior to this patch, whenever a modder wanted to use any effect, it had to have a predefined effect and a predefined value and scope and a predefined effect bundle. Only then, we could apply that pre-packaged, db-defined effect bundle only a character or a region or a faction. This proves especially problematic for any sort of effects that you want to have many different values for – say, randomly applying a modifier to a faction that gives it somewhere between -50 and +50 public order on all regions every turn. That’s a weird mod idea in the first place, but in order to do it you’d need to make 101 effect bundles in the database. Not the end of the world, but not very dynamic.

But no more are we shackled to the tyranny of db (for effect bundles). We can now be let loose and script effects and effect bundles left and right, changing the scope, the target, the values, and straight up adding or removing effects at will and ad nauseum. It’s a beautiful, gorgeous system, and I’ve made use of it a few times already and I’d like to share its usage with you – because yes, it’s definitely a little unwieldy.

How They Work

The system given to us is a new script interface – called the custom_effect_bundle interface. Don’t be deceived – the name is rather inaccurate. More accurately, it’s a_list_of_changes_to_make_to_an_effect_bundle interface, but that’s way more unwieldy and ugly. That is to say – a custom_effect_bundle interface does nothing on its own; it has to be applied onto a game object that can receive an effect bundle, and then it makes a new effect bundle or overwrites an existing one with the same key.

In order to create a list_of_changes_to_make_to_an_effect_bundle interface, you only need two things – a key defined in a effect_bundles_tables db file, and one line of code:

local custom_eb = cm:create_new_custom_effect_bundle("effect_bundle_key")

And we now have a list interface to work with!

Makin’ Edits

The core reference for making edits to a custom_eb can be found here. I’ll show you how to use most of them now.

We’re assuming that, at the start, the custom_eb doesn’t have any db-defined effects on its base effect bundle (no links to the new effect_bundle in effect_bundles_to_effects). First off – we want to add an effect!

local custom_eb = cm:create_new_custom_effect_bundle("effect_bundle_key")
custom_eb:add_effect("cool_effect_key", "faction_to_faction_own", -70)

And that’s it! We can assign any effect to the bundle, with any value and any scope. Be mindful of the scopes here though – if you use faction_to_faction_own and assign the effect_bundle to a region, it very much will not work.

But let’s assume we did assign some effects to the effect_bundle by default in the database, two of them (cool_effect and really_cool_effect), both at value 0. Let’s say we want to set really_cool_effect to value 15, without changing cool_effect.

local custom_eb = cm:create_new_custom_effect_bundle("effect_bundle_key")
-- get a list of all effects linked to the db-defined effect bundle by default:
local effects = custom_eb:effects()
for i = 0, effects:num_items() - 1 do
  -- check which effect we're at in the loop
  local effect = effects:item_at(i)
  if effect:key() == "really_cool_effect" then
    -- set it to 15!
    custom_eb:set_effect_value(effect, 15)
  end
end

Likewise, if we wanted to completely remove the cool_effect effect, for the sake of, say, hiding it from the UI, you could do:

local custom_eb = cm:create_new_custom_effect_bundle("effect_bundle_key")
-- get a list of all effects linked to the db-defined effect bundle by default:
local effects = custom_eb:effects()
for i = 0, effects:num_items() - 1 do
  -- check which effect we're at in the loop
  local effect = effects:item_at(i)
  if effect:key() == "cool_effect" then
    -- get rid of it!
    custom_eb:remove_effect(effect)
  end
end

And that’s basically enough to know when it comes to creating your list_of_changes interface! You can add a bunch of effects, remove a bunch, change the amount of time it can run for. A whole load of stuff can be edited here.

Doin’ The Thing

But! It’s just a list. It’s a nice, beautiful, intricate list that we’ve made. But it’s just a list! We need to actually take the thing and apply it to another thing. Here is when I direct you to yet another link for quick reference.

Applying the list-of-changes-to-be onto an actual game object is probably the simplest part of the operation. You just need to grab your game object, and run one of those commands to apply it. I’ll quickly give an example of giving one to the Reikland faction:

local reikland_obj = cm:get_faction("wh_main_emp_empire")
local custom_eb = cm:create_new_custom_effect_bundle("summon_the_elector_counts")

cm:apply_custom_effect_bundle_to_faction(custom_eb, reikland_obj)

And that’s really it! Of course the above custom_eb isn’t gonna do much since I didn’t make any edits to it.

This is all three text options because it’s important. NOTHING WILL HAPPEN UNLESS YOU ACTUALLY APPLY THE CUSTOM EB TO A GAME OBJECT. DO THE THING.

Reading While Writing

The above is all in the use-case of “I want to add a new effect bundle to a thing”. That use-case is fantastic. What’s even better, though – “I want to edit an existing effect bundle”. WHAT? Yeah, we can do that.

I probably should’ve pitched this earlier on towards the intro but, too late now, I’m not editing this.

So! Let’s take a vanilla example. Let’s say I’m playing the Karaz-a-Karak faction. I want to edit the wh_main_faction_trait_dwarfs effect bundle for ONLY this faction, so instead of editing the actual database or doing any sort of nonsense, I only am doing this through script. I can grab the faction via script, grab this effect bundle via script, and then clone it and make a custom eb and then make my changes. Let’s take a look at that. Together, grab my hand. also1morelink

local faction_obj = cm:get_faction("wh_main_dwf_dwarfs")
local effect_bundle_list = faction_obj:effect_bundles()

-- first, we have to check and grab the already-existing effect bundle!
for i = 0, effect_bundle_list:num_items() - 1 do
  local effect_bundle = effect_bundle_list:item_at(i)
  
  -- found it - make our custom_eb copy!
  if effect_bundle:key() == "wh_main_faction_trait_dwarfs" then
    local custom_eb = effect_bundle:clone_and_create_custom_effect_bundle()
    
    -- find the effect we want to edit in the custom eb
    local custom_effects = custom_eb:effects()
	for j = 0, custom_effects:num_items() - 1
    	local custom_effect = custom_effects:item_at(j)
    
    	-- Make Dat Edit
    	if custom_effect:key() == "wh_main_effect_force_all_campaign_post_battle_loot_mod" then
      		-- set post-battle loot to 5000%!
      		custom_eb:set_effect_value(custom_effect, 5000)
      	end
	end
  	-- NEEDED FOR IT TO DO THE THING DON'T FORGET
  	cm:apply_custom_effect_bundle_to_faction(custom_eb, faction_obj)
  end
end

Now, we can start to see here the slightly prohibitive use case of the custom effect bundles. It involves a lot of loops and deep logic, because there’s nothing like faction:get_eb_with_key(eb_key) or eb:get_effect(effect_key. But it can be easily resolved if you make helper functions in your script to repeatedly do all that work. I’m not gonna do that now for you, though, since use-case will vary.

Weirdness

Aaaand there’s one last thing. These work really well whenever you are editing an effect bundle for the first time. But when you are editing an already script-edited effect bundle, and you clone the effect bundle to make a new custom_eb, the custom_eb will not have the previous script edits.

For example, if I have an effect bundle that sets an effect +1 every single turn, and I have it set up like the following:

local custom_eb = cm:create_new_custom_effect_bundle("increment_every_turn")

-- find the effect
for i = 0, custom_eb:effects():num_items() - 1 do
  local custom_effect = custom_eb:effects():item_at(i)
  
  if custom_effect:key() == "increment_effect_key" then
    -- save the effect's current value
    local current_val = custom_effect:value()
    -- increment the value
    custom_eb:set_effect_value(custom_effect, current_val + 1)
  end
end

-- I'm skipping it but DON'T FORGET TO USE THE cm:apply_custom_effect_bundle FUNCTION AT THE END

then it won’t work. Because, in this instance and in all other possible instances, a custom_eb will only have the values set from the db. If I clone an existing effect bundle, and I’ve already previously script-edited it, the cloned custom_eb will not know those edited values. It will only know the db-set ones. There is, thankfully, a way around this. It’s also slightly complicated, but it’s manageable and definitely worth it in my opinion.

Here’s an example from the Return of the Lichemaster mod, where I set an effect to +5 its previous value based on a few criteria – building completed, specific character leveled up, et cetera. I’ve rewritten it a bit to make it more understandable in context, and also added comments:

-- define our starting variables and the EB key we're looking for
local faction_key = "wh2_dlc11_vmp_the_barrow_legion"
local faction_obj = cm:get_faction(faction_key)
local bundle_key = "AK_hobo_hero_spawn_rank"
local increase = 5 -- how much the effect will increase

-- run through the faction's effect bundles to find the above
local bundle_list = faction_obj:effect_bundles()
for i = 0, bundle_list:num_items() - 1 do
    local bundle = bundle_list:item_at(i)

  	-- found!
    if bundle:key() == bundle_key then
        local effect_list = bundle:effects()
        local effect_val = 0  -- defaults to 0 on db; defined here so it can be read lower

        -- shouild only be one effect!
        local effect = bundle:effects():item_at(0)
        if effect:key() == bundle_key then
            effect_val = effect:value()
        end

        local custom_eb = bundle:clone_and_create_custom_effect_bundle(cm:model())
        local custom_effect = custom_eb:effects():item_at(0)
        if custom_effect:key() == bundle_key then
      		-- sets the effect to +5 its current value, read by the real effect bundle
      		-- if this was "custom_effect:value() + increase", it would only ever be 5!
            custom_eb:set_effect_value(custom_effect, effect_val + increase)
      
      		-- SERIOUSLY DON'T FORGET TO DO THIS I SPENT SO LONG TRYING TO DEBUG
            cm:apply_custom_effect_bundle_to_faction(custom_eb, faction_obj)
        end
    end
end

So – as you see, I have to run through the “real” effect bundle and its effects as it exists on the faction object, and then I make the this_is_just_a_list_and_doesn't_exist_yet custom_eb interface and assign the new value there. If I read custom_effect:value() instead of the effect:value() earlier on, the effect would never increase, since custom_effect:value() will ALWAYS default to the db-defined value.

At least, until hopefully CA fixes it and I can axe this section.

Conclusion

Scripted effect bundles are INCREDIBLY powerful. They can remove the need for using a bunch of unwieldy named_values for tracking numbers via script (exactly what the above code does!). They can remove the need for creating thousands of prohibitive effect bundles for every possible variation of a giant mechanic. They can prevent usage of a data__ table in a few situations, or entirely overwriting an effect_bundle just to make one tiny edit.

I hope this tutorial was useful for you in making your own fancy scripted effect bundles. If there’s anything I could be more clear about, or any further questions you have, please direct them my way!

Leave a Reply