By DavidW - V1.1 12/14/2023
This chapter considers a few (somewhat) more advanced applications of the material discussed in chapters 2-3.
A mod is immutable (the term is drawn from academic computer science) if the actual mod folder and its contents are entirely unchanged when the mod installs. Traditional WEIDU mods were not immutable: they modified their own folders during install and uninstall. But there are significant advantages in immutability, and the modern trend is in that direction. (This thread at Gibberlings 3 is a discussion of the virtues of immutability, and overlaps with my discussion here.)
There are basically three causes of mutability in (most) mods.
The solution is the same in each case: the new or modified files need to placed in an external folder. There is a standard convention for this folder: is is called 'weidu_external'.
Because weidu_external is shared between all mods, a certain amount of discipline is required to make sure your mod doesn't clash with another mod (if your mod and another mod both back up to 'weidu_external/backup', for instance, you will have problems.) There are two available conventions here, and you can use whichever one you like:
Encapsulation (which we touched on briefly in section 2.7) is the principle that a given unit of your mod (in this case, a component) should function the same way, whether or not you have installed other components in the same run. By default, WEIDU components are not encapsulated; the reason is that WEIDU variables by default have global scope, so that a variable set in one component remains set in the next.
Failures of encapsulation lead to subtle, but very annoying bugs, and so in complicated mods I think it's good practice to ensure your components are encapsulated. Here I want to talk about various ways of doing that, and in doing so give some biased advice about how to structure your mod more generally.
The simplest way to make your tp2 encapsulated is just to put 'CLEAR_EVERYTHING' at the start of your mod's ALWAYS block. This works perfectly well and if you don't want to use either of the more sophisticated methods below, you should definitely do it.
Other than being inelegant (which is, to be sure, a matter of taste), the problem with CLEAR_EVERYTHING is that it prevents you using your ALWAYS block to do some setup work for all components. CLEAR_EVERYTHING will clear any variables set (and any functions defined) by your ALWAYS block, requiring you to run it again when you move on to the next component. In some mods, this can be a major source of slowdowns.
A more elegant way to encapsulate a component is to wrap it in WITH_SCOPE. All variables will then have scope restricted to the component. Provided you are careful to use WITH_TRA rather than LOAD_TRA to load tra entries, then (short of some rather obscure bugs involving defining functions) the component is guaranteed to be encapsulated.
While it isn't strictly necessary to do this, I recommend that you put the entire code of each component into a file that you INCLUDE. That way, you have a clean separation of (i) the code that controls which components are installable and which ensures encapsulation, from (ii) the actual code that modifies in-game files. On this basis, a component looks something like this:
BEGIN @44 DESIGNATED 100 SUBCOMPONENT @77 GROUP @9
REQUIRE_COMPONENT "setup-stratagems.tp2" 5900 @205 // require SCS AI initialize
WITH_SCOPE BEGIN
INCLUDE "%MOD_FOLDER%/components/awesome_minsc.tph"
END
Since variables defined in a function are automatically local-scope unless explicitly returned, an alternative way to encapsulate is to make your entire component into a function with no outputs. This is my own preferred way of doing encapsulation, and can usefully be combined with tra management.
More precisely, I define a function 'run', which looks (something) like this:
DEFINE_ACTION_FUNCTION run
STR_VAR
file=""
location=""
tra=""
BEGIN
// sanity check
ACTION_IF "%file%" STR_EQ "" BEGIN
FAIL "run function requires a non-empty argument 'file'"
END
ACTION_IF "%location%" STR_EQ "" BEGIN
FAIL "run function requires a non-empty argument 'location'"
END
// include component
ACTION_IF !FILE_EXISTS "%MOD_FOLDER%/%location%/%file%.tpa" BEGIN
FAIL "run function: File %file%.tpa not found in location %MOD_FOLDER%/%location%"
END
INCLUDE "%MOD_FOLDER%/%location%/%file%.tpa"
// run component
ACTION_IF "%tra%" STR_CMP "" BEGIN
WITH_TRA "%MOD_FOLDER%/lang/%LANGUAGE%/%tra%.tra" BEGIN
LAF "%file%" END
END
END ELSE BEGIN
LAF "%file%" END
END
END
(This assumes your tra files live in 'mymod/lang'; you can adjust if they're somewhere else.)
I then use the convention that any file 'name'.tpa contains an action function called 'name'.
A component then looks like:
BEGIN @44 DESIGNATED 100 SUBCOMPONENT @77 GROUP @9
REQUIRE_COMPONENT "setup-stratagems.tp2" 5900 @205 // require SCS AI initialize
LAF run STR_VAR file=awesome_minsc location=gameplay tra=minsc END
This then looks in the 'mymod/gameplay' folder for 'awesome_minsc.tpa'. If it finds it, it runs it, using tra file 'minsc.tra'. By default this function assumes the tra file has the same name as the folder in which the component function is located, so that
LAF run STR_VAR file=awesome_minsc location=gameplay END
Just as it makes sense to encapsulate your components, in complicated mods it can be a good idea to encapsulate their separate parts. For instance, suppose you have a component which modifies all orcs, all hobgoblins, and all goblins in the game. The traditional way to write the component (here I'll assume the component is wrapped in a big function) would be
DEFINE_ACTION_FUNCTION modify_humanoids BEGIN
// modify orcs
[lots of code]
// modify hobgoblins
[lots of code]
// modify goblins
[[lots of code]
END
DEFINE_ACTION_FUNCTION modify_humanoids BEGIN
LAF modify_orcs END
LAF modify_hobgoblins END
LAF modify_goblins END
END
DEFINE_ACTION_FUNCTION modify_orcs BEGIN
[lots of code]
END
DEFINE_ACTION_FUNCTION modify_hobgoblins BEGIN
[lots of code]
END
DEFINE_ACTION_FUNCTION modify_goblins BEGIN
[lots of code]
END
Now any variables set by the individual bits of code remain confined to those bits of code and don't escape to have unexpected consequences elsewhere. You can also do this with WITH_SCOPE, but I find the code more readable if you use functions (it also makes it easier, during development, to turn bits on and off.)
By 'optimization', I mean, 'making your code run faster'. Mostly you should not worry about this: on a modern computer, most individual WEIDU commands are effectively instant. But in some contexts this is not true, and there it can be useful to pay attention to ways to speed things up.
These are some rules of thumb from my experience:
To work out how long your code runs, a stopwatch often suffices, but sometimes it is more convenient to get a sharp time for a specific part of your mod. You can do so with the ACTION_TIME component, like this:
ACTION_TIME my_timer BEGIN
[some bit of your code]
END
My experience is that WEIDU's time for the same code can vary up to 10-20% between runs, so don't take precise timer values too seriously. It is also occasionally a bit unreliable, especially when doing large numbers of APPENDs. In the following examples, I give the time for each piece of code to execute as recorded by a WEIDU timer, in each case running on my laptop (a 2-year-old Surface laptop) on unmodded BG2EE.
As a case study, suppose you want to make a bespoke copy, DWWP213, of the wizard spell SPWI213, Stinking Cloud, perhaps to use in a kit. Various spells and items grant immunity to Stinking cloud (using one of three opcodes: 206, 318, 324) and so you need to patch those spells and items to also protect from your new spell. If you were applying your mod to the unmodded game you could just use CLONE_EFFECT directly on the files that in fact do this, but in a general modded environment you don't know which spells do it, and so you have to do a COPY_EXISTING_REGEXP through all spells and items.
A simple way to do that with WEIDU's built-in functions is:
COPY_EXISTING_REGEXP ".*\.\(spl\|itm\)" override
LPF CLONE_EFFECT INT_VAR silent=1 match_opcode=206 STR_VAR match_resource=SPWI213 resource=DWWP213 END
LPF CLONE_EFFECT INT_VAR silent=1 match_opcode=318 STR_VAR match_resource=SPWI213 resource=DWWP213 END
LPF CLONE_EFFECT INT_VAR silent=1 match_opcode=324 STR_VAR match_resource=SPWI213 resource=DWWP213 END
BUT_ONLY
(Run-time: 8.6 sec)
This is easy to write and to understand, but as you can see, it takes a relatively long time to run. If this is the only bit of your mod that does something like this, you could just suck it up, but if you do a lot of things like this, your installation time is going to get painful.
The reason the code takes so long is twofold: (i) the CLONE_EFFECT function is quite a lot slower than doing a manual pass through a spell or item's effects; (ii) more straightforwardly, we use the function three times, so the code loops three times through every spell and item.
We could address this by hardcoding the whole thing - but hardcoding the addition or removal of effects is a timeconsuming and bug-prone nuisance. The solution is to do a first, hard-coded look through the spell to see if it needs patching at all, and if it does, use CLONE_EFFECT. Here's an implementation:
COPY_EXISTING_REGEXP ".*\.\(spl\|itm\)" override
match=0
// loop through item equipped effects
PATCH_IF "%SOURCE_EXT%" STR_EQ itm BEGIN
GET_OFFSET_ARRAY fx_arr ITM_V10_GEN_EFFECTS
PHP_EACH fx_arr AS fx_ind=>fx_off BEGIN
PATCH_IF !match BEGIN
READ_SHORT fx_off opcode
PATCH_IF opcode=206 || opcode=318 || opcode=324 BEGIN
READ_ASCII 0x14+fx_off resource
match=("%resource%" STR_EQ SPWI213)
END
END
END
END
// if we haven't found a match already, loop through item/spell casting/use effects
PATCH_IF !match BEGIN
PATCH_IF "%SOURCE_EXT%" STR_EQ itm BEGIN
GET_OFFSET_ARRAY ab_arr ITM_V10_HEADERS
END ELSE BEGIN
GET_OFFSET_ARRAY ab_arr SPL_V10_HEADERS
END
PHP_EACH ab_arr AS ab_ind=>ab_off BEGIN
PATCH_IF !match BEGIN
// ITM_V10_HEAD_EFFECTS and SPL_V10_HEAD_EFFECTS coincide, no need to check if ITM/SPL
GET_OFFSET_ARRAY2 fx_arr ab_off SPL_V10_HEAD_EFFECTS
PHP_EACH fx_arr AS fx_ind=>fx_off BEGIN
PATCH_IF !match BEGIN
READ_SHORT fx_off opcode
PATCH_IF opcode=206 || opcode=318 || opcode=324 BEGIN
READ_ASCII 0x14+fx_off resource
match=("%resource%" STR_EQ SPWI213)
END
END
END
END
END
END
PATCH_IF match BEGIN
LPF CLONE_EFFECT INT_VAR silent=1 match_opcode=206 STR_VAR match_resource=SPWI213 resource=DWWP213 END
LPF CLONE_EFFECT INT_VAR silent=1 match_opcode=318 STR_VAR match_resource=SPWI213 resource=DWWP213 END
LPF CLONE_EFFECT INT_VAR silent=1 match_opcode=324 STR_VAR match_resource=SPWI213 resource=DWWP213 END
END
BUT_ONLY
(Run-time: 0.98 sec)
This is much more complicated, of course (though it's a template for other such tasks, and you can write it very quickly once you get the hang of it). But it's almost 10x faster.
In this particular case, though, this complicated code is overkill. There is a much faster way to pre-sift: just search for the string SPWI213!
COPY_EXISTING_REGEXP ".*\.\(spl\|itm\)" override
PATCH_IF INDEX_BUFFER (SPWI213)>=0 BEGIN
LPF CLONE_EFFECT INT_VAR silent=1 match_opcode=206 STR_VAR match_resource=SPWI213 resource=DWWP213 END
LPF CLONE_EFFECT INT_VAR silent=1 match_opcode=318 STR_VAR match_resource=SPWI213 resource=DWWP213 END
LPF CLONE_EFFECT INT_VAR silent=1 match_opcode=324 STR_VAR match_resource=SPWI213 resource=DWWP213 END
END
BUT_ONLY
(Run-time: 0.47 sec)
Way faster to write, and twice as fast! Notice that this crude filter will produce some false positives - the scroll of Stinking Cloud will be caught by it, for instance - but it doesn't matter. The CLONE_EFFECTs themselves guarantee that we only patch the objects we ought to patch - all the filter has to do is get rid of most of the non-matching files before we do the slower, more careful CLONE_EFFECT. (And the 'silent=1' prevents those false positives from leading to WARNINGs.)
Suppose you want to build a list of every creature in the game along with its class (perhaps to plug into some subsequent bit of code). Here's a simple, direct implementation:
<<<<<<<<.../stratagems-inline/classlist.txt
>>>>>>>>
MKDIR "weidu_external/data/mymod"
COPY ".../stratagems-inline/classlist.txt" "weidu_external/data/mymod"
COPY_EXISTING_REGEXP - ".*\.cre" nowhere
LOOKUP_IDS_SYMBOL_OF_INT classname class (BYTE_AT 0x273)
INNER_ACTION BEGIN
APPEND_OUTER "weidu_external/data/mymod/classlist.txt" "%SOURCE_RES%%TAB%%classname%"
END
(Run-time: officially 7.7 sec according to ACTION_TIME, actually more like 45 sec)
This is simple and gets the job done, but it's painfully slow, almost entirely because of all the APPEND_OUTERs. The solution is to collect all the appended data into a string and append it right at the end, like this:
<<<<<<<<.../stratagems-inline/classlist.txt
>>>>>>>>
MKDIR "weidu_external/data/mymod"
COPY ".../stratagems-inline/classlist.txt" "weidu_external/data/mymod"
OUTER_SPRINT data ""
COPY_EXISTING_REGEXP - ".*\.cre" nowhere
LOOKUP_IDS_SYMBOL_OF_INT classname class (BYTE_AT 0x273)
SPRINT data "%data%%SOURCE_RES%%TAB%%classname%%WNL%"
APPEND_OUTER "weidu_external/data/mymod/classlist.txt" "%data%"
(Run-time: 0.67 sec)
This is almost instant. A variant version is:
<<<<<<<<.../stratagems-inline/classlist.txt
%data%
>>>>>>>>
OUTER_SPRINT data ""
COPY_EXISTING_REGEXP - ".*\.cre" nowhere
LOOKUP_IDS_SYMBOL_OF_INT classname class (BYTE_AT 0x273)
SPRINT data "%data%%SOURCE_RES%%TAB%%classname%%WNL%"
MKDIR "weidu_external/data/mymod"
COPY ".../stratagems-inline/classlist.txt" "weidu_external/data/mymod" EVALUATE_BUFFER
(Run-time: 0.59 sec)
This takes about the same time to run; I find it a bit more elegant, but tastes may differ. (In practice I tend to do the first, because I have a standardly defined blank inline file I can copy over, without having to declare a new one explicitly.)
Suppose we want to introduce a variable,, 'dw_no_fireballs', so that no creature in the game casts Fireball (SPWI304) from their spellbook if the variable is non-zero. Here's a search-and-replace that does that for a specific script (say, mage16b, which casts Fireball):
COPY_EXISTING "mage16b.bcs" override
DECOMPILE_AND_PATCH BEGIN
REPLACE_TEXTUALLY
~HaveSpell(WIZARD_FIREBALL)~
~Global("dw_no_fireballs","GLOBAL",0)HaveSpell(WIZARD_FIREBALL)~
END
BUT_ONLY
COPY_EXISTING_REGEXP ".*\.bcs" override
DECOMPILE_AND_PATCH BEGIN
REPLACE_TEXTUALLY
~HaveSpell(WIZARD_FIREBALL)~
~Global("dw_no_fireballs","GLOBAL",0)HaveSpell(WIZARD_FIREBALL)~
END
BUT_ONLY
(Run-time: 6.7 sec)
But this requires thousands of scripts to be decompiled and recompiled, which takes time. (A lot of time if you have a mod like Sword Coast Stratagems installed, which introduces many long scripts.)
The trick to optimize this is to work out how to search the compiled script to find whatever it is we're planning to search-and-replace. As with the spell-patching examples we considered earlier, it doesn't matter if our method produces the occasional false positive, since the search-and-replace itself will take care of that. All we need to do is get the number of scripts down to a manageable number.
In this case, a simple way is to remember that 'WIZARD_FIREBALL' is an element in spell.ids, and when compiled will be replaced by the associated number (in this case 2304). So we just filter for that:
COPY_EXISTING_REGEXP ".*\.bcs" override
PATCH_IF INDEX_BUFFER (2304)>=0 BEGIN
DECOMPILE_AND_PATCH BEGIN
REPLACE_TEXTUALLY
~HaveSpell(WIZARD_FIREBALL)~
~Global("dw_no_fireballs","GLOBAL",0)HaveSpell(WIZARD_FIREBALL)~
END
END
BUT_ONLY
(Run-time: 1.0 sec)