By DavidW - V1.1 12/14/2023
This chapter details some specific sets of commands that WEIDU provides. Unlike chapter 2, the focus here is less on general programming tools, more on specific commands that may be useful in a mod. The discussion here is far from exhaustive; as always, check the WEIDU readme to see the full range of options.
ALWAYS
INCLUDE "%MOD_FOLDER%/lib/set_spell_vars.tph"
LAM set_spell_vars
END
In this case, and often in your ALWAYS block, you only actually want the code to run once: running it twice will either cause problems, or (more often) just take unnecessary time. To avoid this, you can use a variable to make sure the content is only run once, as in:
ALWAYS
ACTION_IF !VARIABLE_IS_SET always_block_installed BEGIN
OUTER_SET always_block_installed=1
INCLUDE "%MOD_FOLDER%/lib/set_spell_vars.tph"
LAM set_spell_vars
END
END
I generally find it more elegant to put the entire ALWAYS block (including any variable that we check to run it only once) into a single include:
ALWAYS
INCLUDE "%MOD_FOLDER%/lib/always.tph"
END
Recall that the basic structure of a tp2 file is
[preamble]
BEGIN "first component" DESIGNATED 100
[contents of first component]
BEGIN "second component" DESIGNATED 200
[contents of second component]
...
It is possible to control the way installation goes by attaching various 'flags' to a component. Flags are commands that directly follow the original "BEGIN x" (DESIGNATED is actually a flag) and precede the main piece of WEIDU code. You can have as many flags as you like on a component. This account is not exhaustive, but it should list most of the more important flags.
Often, in complicated mods, one of your components may only work if another of your components - or a component from another mod - has been installed. You can do this by a REQUIRE_COMPONENT flag, like this:
BEGIN "Make Minsc awesome" DESIGNATED 100
[content]
BEGIN "Give Minsc an awesome sword" DESIGNATED 200
REQUIRE_COMPONENT "mymod.tp2" 100 "This component requires 'Awesome Minsc' to be installed"
[content]
You can also tell WEIDU to install your component only if another component is not installed. The syntax is exactly the same, but you use FORBID_COMPONENT instead of REQUIRE_COMPONENT.
Sometimes you want a more complicated check, e.g. that your mod should only install if at least one from a list of components is installed. You can do this with the powerful REQUIRE_PREDICATE flag. The syntax is 'REQUIRE_PREDICATE expression string', where 'expression' is something that evaluates to an integer (think of it as a Boolean). If value=0, the component is skipped and the string is displayed.
For instance, here's code to require that one or other of the 'Icewind dale arcane' spells from IWDification or SCS are installed:
BEGIN "component that requires IWD arcane spells" DESIGNATED 200
REQUIRE_PREDICATE
MOD_IS_INSTALLED setup-iwdification 40
|| MOD_IS_INSTALLED setup-stratagems 1500
"This component requires the Icewind Dale arcane spells to be present"
REQUIRE_PREDICATE can check for anything, not just component presence or absence; another common scenario is to check whether a particular game type is present, as in:
BEGIN "component that requires EE" DESIGNATED 100
REQUIRE_PREDICATE
GAME_IS "BGEE BG2EE IWDEE EET"
"This component requires the Enhanced Edition version of the game"
[content]
BEGIN "component that requires SoD" DESIGNATED 200
REQUIRE_PREDICATE
GAME_INCLUDES "SOD"
"This component requires Siege of Dragonspear"
[content]
BEGIN "component that doesn't work on PS:T" DESIGNATED 300
REQUIRE_PREDICATE
!GAME_IS "PST PSTEE"
"This component is not available for Planescape: Torment"
Technically the following is legal:
BEGIN "component that requires dw#melee.bcs" DESIGNATED 100
REQUIRE_PREDICATE
FILE_EXISTS_IN_GAME "dw#melee.bcs"
"This component requires SCS AI system to be installed"
In mods with many components, it can be useful to divide them into groups: for instance, Sword Coast Stratagems splits its components into New Spells and Spell Tweaks; Gameplay Tweaks; AI improvements; Tactical challenges. When the player installs the mod, they are first asked which groups they want to view; they are then only offered the choice to install components from those groups.
You assign components to groups using the GROUP flag, like this:
BEGIN "Make Minsc Awesome" DESIGNATED 100 GROUP "Minsc options"
[content]
BEGIN "Give Minsc an awesome battle-cry" DESIGNATED 200 GROUP "Minsc options"
[content]
BEGIN "Rename Anomen 'the annoying'" DESIGNATED 300 GROUP "Anomen options"
[content]
Each component is labelled by a particular string (e.g. "Minsc options"), and all components with the same string are grouped together. As usual, in a live mod these strings (and indeed the component names) should nearly always be tra entries.
Subcomponents allow you to group together a set of mutually-incompatible subcomponents, so that the user picks only one. For instance, maybe you have several different awesome titles to give Minsc; the player can only choose one.
You designate subcomponents using the SUBCOMPONENT flag, like this:
BEGIN "Minsc becomes 'Minsc the Awesome'" DESIGNATED 100
SUBCOMPONENT "Give Minsc an awesome title"
[content]
BEGIN "Minsc becomes 'Minsc the Bodacious'" DESIGNATED 110
SUBCOMPONENT "Give Minsc an awesome title"
[content]
BEGIN "Minsc becomes 'Minsc the Hero of Baldur's Gate" DESIGNATED 120
SUBCOMPONENT "Give Minsc an awesome title"
[content]
When the player installs, they will be asked to pick one of these components (or to skip all of them).
You can put requirements (with REQUIRE_PREDICATE or REQUIRE/FORBID_COMPONENT) for a whole group of subcomponents: just put them on the first component in the list. You can also put a predicate on specific subcomponents. For example, the whole 'give Minsc an awesome title' component only makes sense on a BG or BG2 game, and Minsc can't really be the Hero of Baldur's Gate unless BG has already happened. So we might alter the above example to
BEGIN "Minsc becomes 'Minsc the Awesome'" DESIGNATED 100
SUBCOMPONENT "Give Minsc an awesome title"
REQUIRE_PREDICATE (!GAME_IS "iwd how totlm iwdee pst pstee") "This component requires Minsc and he's not in this game (shame)"
[content]
BEGIN "Minsc becomes 'Minsc the Bodacious'" DESIGNATED 110
SUBCOMPONENT "Give Minsc an awesome title"
[content]
BEGIN "Minsc becomes 'Minsc the Hero of Baldur's Gate" DESIGNATED 120
SUBCOMPONENT "Give Minsc an awesome title" (GAME_IS "bg2 tob bg2ee")
[content]
WEIDU generally refers to components by their component number (as in: REQUIRE_COMPONENT "stratagems.tp2" 5900 "This component requires the core AI component"). That has two problems: (i) it's not very human-readable; (ii) sometimes mod numbers change. It would be helpful to have a way to refer to a component that's human-readable and doesn't change when the component number does.
In principle, this functionality is provided by the LABEL component flag, which allows each component to be labelled with a string. The component number can then be returned by the ID_OF_LABEL WEIDU command: if stratagems component 5900 gets assigned label 'dw_main_ai_component', 'ID_OF_LABEL stratagems.tp2 dw_main_ai_component' evaluates to 5900.
Here's an example (rewriting our previous example of REQUIRE_COMPONENT to use LABELs):
BEGIN "Make Minsc awesome" DESIGNATED 100
LABEL dw_awesome_minsc_initialize
[content]
BEGIN "Give Minsc an awesome sword" DESIGNATED 200
LABEL dw_awesome_minsc_sword
REQUIRE_COMPONENT mymod.tp2 ID_OF_LABEL mymod.tp2 dw_awesome_minsc_initialize
"This component requires 'Awesome Minsc' to be installed"
[content]
Note that LABELs are not readable by the player, and so shouldn't be tra entries. Internal to WEIDU there is no need to use your modder prefix with them (as I have in the above), but it's helpful for Project Infinity.
For mods with many components and component groups, LABEL unfortunately seems to greatly increase the time for WEIDU to parse the tp2; for that reason, I don't myself use it, despite its elegance.
By default, any WEIDU mod with multiple components offers you the chance to install all of them. For some mods (e.g. tweak mods, that offer you various different and perhaps incompatible ways to modify your game) this is inappropriate. You can turn it off by putting 'ASK_EVERY_COMPONENT' in the preamble of your TP2 file.
If you are using component groups, WEIDU automatically acts as if ASK_EVERY_COMPONENT were present.
A ‘table’ is just a text file consisting of text arranged in rows and columns, separated by spaces. The 2da and ids files in the Infinity engine are tables. WEIDU provides several tools to interact with tables. (All the commands discussed in this section are patch-context commands, to be used while editing a table.)
WEIDU identifies a table entry by three numbers: the row, the column, and the minimum column count. Rows and columns both count from 0 up (so the first column is column 0, etc.) - but WEIDU only considers columns at least as long as the maximum column length.
For instance, look at (the EE version of) kitlist.2da. The first row contains two entries, ‘2DA’ and ‘v1.0’ (these aren’t really table entries at all, just part of the way a 2da file is defined). The second row contains only one entry (again, not really a table entry). The third row contains the column headers: ‘ROWNAME’, ‘LOWER’, ‘MIXED’ and so forth – there are 9 of them. The remaining rows contain the actual data, but each row has a row header too, so there are 10 entries in each subsequent row.
That means that (say) "the entry in row 4, column 1" means something different for different minimum column counts. If you are working with a minimum column count of 2, the ‘2DA v1.0’ row and the column-header rows both count, and so the first row of actual data is row 2, and our entry is ‘WIZARD_SLAYER’. If you take minimum column count as 3 (or as 9) the ‘2DA v1.0’ row doesn’t count, but the column-header row does, and so the first row of actual data is row 1, and our entry is ‘KENSAI’. With a minimum column count of 10, the first row of actual data is row 0, and our entry is ‘CAVALIER’. And for a minimum column count of 11 or more, there is no data, and so ‘row 4, column 1’ doesn’t determine an entry.
The simplest way to get table data is to use the READ_2DA_ENTRY command. For instance, suppose you happen to know that ‘CAVALIER’ is row 4 of the actual data in kitlist.2da, and you want to get the description string number for cavaliers (it lives in the ‘help’ column, column 4 of the data). Then you can just do
COPY_EXISTING - "kitlist.2da" nowhere
READ_2DA_ENTRY 4 4 10 cavalier_description
The syntax for READ_2DA_ENTRY is ‘READ_2DA_ENTRY row column min_col_count var’ and the entry in (row,column), given a minimum column count of ‘min_col_count’, is read and stored in ‘var’. (We do ‘COPY_EXISTING -‘ because we don’t want to change the file, just read data from it.)
SET_2DA_ENTRY does exactly the same thing in reverse: it sets a table entry to a string. Here’s code to give cavaliers a new description:
OUTER_SET new_cav_desc=RESOLVE_STR_REF @10 // where ‘10’ is an entry in the .tra file
//containing the new description
COPY_EXISTING "kitlist.2da" override
SET_2DA_ENTRY 4 4 10 "%new_cav_desc%"
In many situations, you won’t know what the exact number of rows is. For instance, kitlist.2da has one row for each kit, and other kits may have been added by mods. Less often, you won’t know how many columns there are: for instance, weapprof.2da has one column per kit. (No, I don’t know why Bioware thought it was a good idea to store some kit data as rows and other kit data as columns.)
You can use COUNT_2DA_ROWS to determine the number of rows. The syntax is ‘COUNT_2DA_ROWS min_col_count var’: var is set equal to the number of rows with at least min_col_count columns.
COUNT_2DA_COLS returns the number of columns. The syntax is simpler: ‘COUNT_2DA_COLS var’ stores the (maximum) number of columns in var.
Here’s some more sophisticated code that defines a function to return the number (column 0) of an arbitrary kit (identified by its rowname entr, e.g. CAVALIER). (We’ll shortly see that there is actually a better way to do this.)
DEFINE_DIMORPHIC_FUNCTION return_kit_number
STR_VAR kit=""
RET kit_number
BEGIN
OUTER_SET kit_number="-1" // default value, indicates no kit present
COUNT_2DA_COLS colcount // we could just rely on it being 10, but I tend to do the
// count explicitly to guard against typos
COUNT_2DA_ROWS colcount rowcount
FOR (row=0;row < rowcount;++row) BEGIN
READ_2DA_ENTRY row 1 colcount this_kit
PATCH_IF "%this_kit%" STR_EQ "%kit%" BEGIN // we found our kit
READ_2DA_ENTRY row 0 colcount kit_number
row=rowcount // since we’ve found it, we can skip to the end and save time
END
END
END
(Like any speedup issue, this isn’t worth worrying about unless you’re doing really large amounts of table-editing – any one-off reading of a few dozen values from a table will take only a fraction of a second, even with READ_2DA_ENTRY. That said, I actually find the alternate functioning easier to use, even if it takes a bit of getting used to.)
Here's how it works. We use READ_2DA_ENTRIES_NOW to collect all the table data at once (or all the table data for a certain column width), like this:
READ_2DA_ENTRIES_NOW my-data colcount
To interrogate the new data structure, we do
READ_2DA_ENTRY_FORMER my-data row col var
Here's our function rewritten to use this method (and made somewhat faster as a consequence):
DEFINE_DIMORPHIC_FUNCTION return_kit_number
STR_VAR kit=""
RET kit_number
BEGIN
OUTER_SET kit_number="-1" // default value, indicates no kit present
COUNT_2DA_COLS colcount // we could just rely on it being 10, but I tend to do the
// count explicitly to guard against typos
READ_2DA_ENTRIES_NOW kitdata colcount
FOR (row=0;row < kitdata;++row) BEGIN
READ_2DA_ENTRY_FORMER kitdata row 1 this_kit
PATCH_IF "%this_kit%" STR_EQ "%kit%" BEGIN // we found our kit
READ_2DA_ENTRY_FORMER kitdata row 0 kit_number
row=kitdata // since we’ve found it, we can skip to the end and save time
END
END
END
You can do the same thing in reverse to write a large amount of data to a table, though you’ll probably use it less often: SET_2DA_ENTRY_LATER puts an entry into a data structure, and SET_2DA_ENTRIES_NOW flushes the data structure into the file. See the WEIDU readme for the detailed syntax.
In chapter 1 we saw how to use COMPILE in order to turn d and baf files into dlg and bcs files. In this section we'll consider some more sophisticated ways to engage with these files.
It's important to recall that WEIDU has an entirely different format for interacting with dlg files in particular: the .d file format itself includes functionality to edit many features of dialog files. That format is outside the scope of this course.
One quite common modding scenario is that a script or dialog already in the game needs to be modified. (For instance, maybe some script uses the Attack() command and needs to be changed to use the AttackOneRound() command.) At least for scripts, the most effective way to do it is (i) decompile the script to a text file; (ii) run a search-and-replace on that text file using REPLACE_TEXTUALLY or REPLACE_EVALUATE; (iii) recompile it. You do it like this:
COPY_EXISTING ascript.bcs override
DECOMPILE_AND_PATCH BEGIN
REPLACE_TEXTUALLY "Attack(" "AttackOneRound("
END
BUT_ONLY
DECOMPILE_AND_PATCH starts with and ends with a compiled script. In rare circumstances, you want to start with a compiled script and end with a decompiled script, or vice versa. For instance, you might want to start with a decompiled script, do some replacements on it, and then copy over the compiled version, or you might want to have a compiled script with a name different from the decompiled script (simple COMPILE commands can't handle this situation).
You do this with the patch-context commands DECOMPILE_BCS_TO_BAF and COMPILE_BAF_TO_BCS (or, for dialogs, DECOMPILE_DLG_TO_D and COMPILE_D_TO_DLG).
While in principle you could use DECOMPILE_BCS_TO_BAF, followed by a REPLACE_TEXTUALLY, followed by COMPILE_BAF_TO_BCS, it's not a good idea: use DECOMPILE_AND_PATCH instead. (Getting it wrong leads to install-time errors rather than run-time errors.)
One perennial problem with doing a search-and-replace on a script is that the exact form of the decompiled script depends both on the compiler and on the system on which it is being decompiled. As for the first: normally you look at decompiled scripts in Near Infinity, but its compilation conventions are not *quite* WEIDU's. So if your apparently-reasonable REPLACE_TEXTUALLY isn't working, try looking at WEIDU's version of the decompilation. You can do this with a quick mod test component and DECOMPILE_BCS_TO_BAF, or you can do it at the command line: weidu ascript.bcs will put ascript.baf into the main game directory.
As to the second: WEIDU uses the contents of the various ids files in the game to carry out its decompilation. If the command 'SetGlobalTimer("mytimer","GLOBAL",EIGHT_HOURS)' is in the decompiled script, it is because there is some line like '2400 EIGHT_HOURS' in gtimes.ids. The problem is that different installs might have slightly different .ids files, and so a script may decompile differently on different systems. The safe thing is to look up the ids value during the course of patching, like this:
COPY_EXISTING ascript.bcs override
// replace 8-hour timer with 4-hour timer
LOOKUP_IDS_SYMBOL_OF_INT eight_hours gtimes 2400
DECOMPILE_AND_PATCH BEGIN
REPLACE_TEXTUALLY
~SetGlobalTimer("mytimer","GLOBAL",%eight_hours%)~
~SetGlobalTimer("mytimer","GLOBAL",1200)~
END
BUT_ONLY
Here I want to briefly consider how we add new text to the bottom of an (existing or new) text file.
Suppose you want to add halflings to the list of hated enemies available for rangers to choose. That list is controlled by the table (text file) haterace.2da, which looks like this (showing only a small part):
BEHOLDER 54770 123 54772
DEMONIC 54760 121 54762
DRAGON 54816 146 54817
Adding halflings to the table just requires us to add this row:
HALFLING 7195 5 9154
APPEND "haterace.2da" "HALFLING 7195 5 9154"
APPEND is used to append a string to existing in-game files: using it involves specifying the name of an in-game file, which WEIDU will then find whether it's still compressed or it's present in the override directory. You can alternately use APPEND_OUTER, which appends to some file whose location is specified by a path relative to your game directory. For instance, you can add a string to the EE ini file like this:
APPEND_OUTER "%USER_DIRECTORY%/baldur.lua"
~SetPrivateProfileString('Script','DMWW_genai_difficulty','5')~
Sometimes a string being appended will have variables in it that need to be evaluated; this should happen automatically given that we're using AUTO_EVAL_STRINGS. Here's code adding Wolves to the hated race list:
OUTER_SET wolf_description=RESOLVE_STR_REF (@100) // @100 is a tra ref giving the description for the ranger select screen
OUTER_SET wolf_code=IDS_OF_SYMBOL (race wolf)
APPEND "haterace.2da" "WOLF 6629 %wolf_code% %wolf_description%"
Very often we only want to APPEND a string if it's not there already. The UNLESS flag handles this, as in:
APPEND "haterace.2da" "HALFLING 7195 5 9154" UNLESS "HALFLING"
By default, APPEND removes any double spaces from the file to which the data is appended; you can override this with the KEEP_CRLF flag. Whether or not you use KEEP_CRLF, APPEND always adds its content as a new line.
All these points apply equally to APPEND_OUTER.
As I noted previously, large amounts of the data in IE files are stored in the so-called ‘extended header’, which contains variably many copies of a data structure – for instance, .cre files have effects, items, memorized spells, and so on. Because this data doesn’t have fixed offsets, you can’t straightforwardly edit it just by using READ_LONG/WRITE_LONG etc.
In modern WEIDU, mostly you can edit this data using either built-in-WEIDU commands or the function library that ships with WEIDU: ADD_CRE_EFFECT and ALTER_EFFECT let you edit effects in .cre files. But it is sometimes useful to know how to do it manually, for two reasons (i) there are not functions for all IE data types; (ii) if you are patching a large number of files (e.g. going through every spell in the game and editing them) it can be quite slow to use WEIDU’s functions and you may want to do it manually for efficiency.
As a concrete example, let’s suppose we want to increase the price of every drink sold by a tavern by a factor of 10. This requires us to edit the ‘drink’ part of the extended header. (You will find this tutorial easier to understand if you have a tavern file – say, TRMER03.STO from BG2 – open in Near Infinity while reading.)
An extended header is normally specified by three numbers:
The entry length is always the same for a given data type. The first entry offset and the number of entries are stored at fixed locations in the file: for .sto files, and for drinks, the first offset is stored in a LONG variable at 0x4c and the number of entries is stored in a LONG variable at 0x50. (All this can either be read off a copy of the file in Near Infinity, or else looked up in IESDP.)
So the offset at the start of the nth entry (counting from zero) is the first entry offset, plus (n x the entry length).
The simplest way to edit all entries is to cycle through them with a FOR loop. Here’s an implementation of our tavern-increasing task:
DEFINE_PATCH_FUNCTION increase_drink_cost
INT_VAR multiplier=10
BEGIN
READ_LONG 0x4c first_offset
READ_LONG 0x50 drink_count
FOR (n=0;n<drink_count;++n) BEGIN
drink_offset=first_offset + n*0x14
WRITE_LONG (0xc + drink_offset) (THIS * multiplier)
END
END
WEIDU offers an elegant alternative to FOR loop cycling, which is much easier to use once you get the hang of it. The GET_OFFSET_ARRAY command automatically reads all the offsets for entries of a given type into the values of an array, with the number of the entry as its key. (So for drinks, if we call the array drink_arr, $drink_arr(0)=0x94 is the offset of the first drink entry, $drink_arr(1)=0xa8 is the offset of the second, and so on. You can then just cycle through them all with PHP_EACH.
To use GET_OFFSET_ARRAY, you need to give it a name for the array (e.g. drink_arr), and then 7 integers, which are in order:
Here’s our drink-repricing function reimplemented with GET_OFFSET_ARRAY:
DEFINE_PATCH_FUNCTION increase_drink_cost
INT_VAR multiplier=10
BEGIN
GET_OFFSET_ARRAY drink_arr 0x4c 4 0x50 4 0 0 0xc
PHP_EACH drink_arr AS drink_ind=>drink_offset BEGIN
WRITE_LONG (0xc + drink_offset) (THIS * multiplier)
END
END
For many data types WEIDU makes it easier for you by hardcoding these offsets. When there is a hardcode, you can just replace the list of 7 integers by the hardcoded string, like this:
DEFINE_PATCH_FUNCTION increase_drink_cost
INT_VAR multiplier=10
BEGIN
GET_OFFSET_ARRAY drink_arr STO_V10_DRINKS
PHP_EACH drink_arr AS drink_ind=>drink_offset BEGIN
WRITE_LONG (0xc + drink_offset) (THIS * multiplier)
END
END
See the WEIDU readme for a list of all the available hardcoded options, but basically anything in a (BG2-version) area, creature, item, spell, or store file is covered.
In a few cases, the extended header system goes one level deeper: some data structures in the extended header have extended headers of their own. The most important example for most uses is ITM and SPL files: they have a variable number of Ability data structures, and each Ability has a variable number of Effect data structures.
In this case, the second-level structures are usually stored in one section of the overall file. Each first-level structure stores two integers: the index of the first second-level header used by that first-level structure, and the number of second-level structures used by that structure.
For instance, look at the Magic Missile spell (SPWI112.spl). In BG2, it has 5 ability entries (corresponding to casting the spell at levels 1,3,5,7,9) and each has 3 associated effect entries. So the index in the first ability is 0 (we always count from zero) and the number in that entry is 3, the index in the first ability is 3 and the number is 3; the index in the third ability is 6 and the number is 3, and so on. So effects 0,1,2 are used by the first ability header; effects 3,4,5 by the second, and so on.
If you want to edit all the second-level headers, then, you need to cycle through the first-level headers, and then check each first-level header to find the first-entry indext and number of entries for the associated second-level headers, and then edit each of those second-level headers. You can do this manually with loops, but this time I’ll skip straight to the neat way to do it, using GET_OFFSET_ARRAY2.
This is quite a lot like GET_OFFSET_ARRAY, but now it needs 8 integers.
As an example, suppose we want to edit Magic Missile so that it does 1d6+1 damage per missile at levels 1-3, 1d8+1 damage at levels 5-7, and 1d10+1 at level 9. (That’s actually a bit fiddly to do with WEIDU’s built-in function library.) Here’s some code to implement it:
COPY_EXISTING "spwi112.spl" override
GET_OFFSET_ARRAY ab_arr SPL_V10_HEADERS
PHP_EACH ab_arr AS ab_ind=>ab_off BEGIN // cycle through abilities
READ_SHORT (ab_off + 0x10) level // read in the level of this header
GET_OFFSET_ARRAY2 fx_arr ab_off 0x6a 4 0x1e 2 0x20 2 0x30
PHP_EACH fx_arr AS fx_ind=>fx_off BEGIN // cycle through effects
PATCH_IF (SHORT_AT fx_off)=12 BEGIN // if this is a ‘damage’ opcode
WRITE_LONG (fx_off+0x20) (level<5?6:(level<9?8:10))
END
END
END
BUT_ONLY
Look at the GET_OFFSET_ARRAY2 arguments more closely:
GET_OFFSET_ARRAY2 fx_arr ab_off 0x6a 4 0x1e 2 0x20 2 0x30
GET_OFFSET_ARRAY2 fx_arr ab_off SPL_V10_HEAD_EFFECTS
It is possible to insert or delete bytes so as to add or remove (primary or secondary) headers. It is very fiddly, however: you need to keep track as you go along of the fact that data has been moved, and you need to update all other offsets in the file once you’re done. If at all possible, use a pre-existing function before doing this.
Most of the time, new strings get into dialog.tlk through either SAY commands or through compiling a script or a dialog. But occasionally you want to know the contents of a particular string identified by number. And sometimes it’s helpful to be able to put a string directly into dialog.tlk, or to edit an existing string. (For instance, changing the description or name of a kit has to be done this way.)
ACTION_GET_STRREF 25201 desc
Some string also come with attached audio files; ACTION_GET_STRREF_S has the same syntax and recovers the name of that file. (GET_STRREF and GET_STRREF_S are patch-context versions).
The patch-context command READ_STRREF offers an occasionally-useful shortcut: READ_STRREF offset var reads in a 4-byte integer at ‘offset’ and sets var to the string (if any) with that index. For instance,
COPY_EXISTING - "minsc.cre" nowhere
READ_STRREF NAME1 minsc_name
sets ‘minsc_name’ equal to whatever actual string names Minsc (‘Minsc’, probably, though it might be different in some languages). READ_STRREF_S is also available and does exactly what you might expect.
The most straightforward way to get a string into dialog.tlk is to use RESOLVE_STR_REF, like this:
OUTER_SET string_val=RESOLVE_STR_REF ("This is a new string")
If you want to edit an existing string, use STRING_SET_EVALUATE: ‘STRING_SET_EVALUATE index var2’ sets the string with index ‘index’ equal to the string ‘var2’. For instance,
STRING_SET_EVALUATE desc_strref "%desc%"
STRING_SET_EVALUATE is an action-context command; slightly annoyingly, there is no patch-context version.