A course on SFO - chapter 2.1

lib_fn, lib_tools, lib_ietools, lib_sfo: Basic Tools for Programming

Back to contents

This section discusses a collection of tools from the lib_fun, lib_tools, lib_ietools, and lib_sfo libraries.

Here and elsewhere, I list only the most useful or general functions in these libraries: see the documentation for full contents.

2.1.1 String manipulation

return_first_entry

This function takes a string, interprets it as a collection of strings separated by (by default) spaces, and returns the first of those strings and the rest. For instance, the function call

LAF return_first_entry STR_VAR list="Minsc Anomen Jaheira" RET entry list END
returns entry="Minsc", list="Anomen Jaheira". You can wrap strings in quotes or tildes to have them treated as a single string: this code
LAF return_first_entry 
STR_VAR list="~Minsc the Awesome~ ~Anomen the Annoying~ ~Jaheira the Judgemental~" 
RET entry list 
END
returns entry="Minsc the Awesome", list="~Anomen the Annoying~ ~Jaheira the Judgemental~". (Note that the quotes are removed from the first entry.)

You can separate elements by characters other than a space if you like, as in

LAF return_first_entry STR_VAR list="Minsc,Anomen,Jaheira" separator="," RET entry list END

return_first_pair

This is similar to return_first_entry, but it treats the string as a list of key=>value pairs and returns the first key and first value. For instance,
LAF return_first_pair
STR_VAR list="Minsc=>Awesome Anomen=>Annoying Jaheira=>Judgemental" 
RET key value list 
EN3
returns key="Minsc", value="Awesome", list="Anomen=>Annoying Jaheira=>Judgemental".

Keys and values can again be wrapped in quotes or tildes; however, return_first_pair does not support using characters other than a space to separate entries.

trim_string

Given a single string, and a character (space by default, but specified by the STR_VAR 'character') this function removes any occurrences of the character from the beginning and end of the string. For instance

LAF trim_string STR_VAR string="   Minsc    " RET string END
returns string="Minsc".

You get a variant behavior if you set INT_VAR require_both=1. Then the character is only stripped if it is present both at the beginning and the end of the string, and in any case only one character is stripped.

2.1.2 File patching tools

read_whatever

Given an offset in the current file, and given a length that is 1, 2, or 4, this function reads the appropriate-length integer from that offset, e.g.

LPF read_whatever INT_VAR length=2 offset=0x20 RET value END
is equivalent to READ_SHORT 0x20 value.

(The reason to use this is if you have some kind of table of variable-length data to read – the actual function call above would never be sensible!)

write_whatever

The inverse of read_whatever; e.g.

LPF write_whatever INT_VAR length=4 write=0x2e0 offset=0x20 END
is equivalent to WRITE_LONG 0x20 0x2e0.

find_parenthesis_range

Given an index in the current file (0 by default), a left-character ('{' by default), and a right-character ('}' by default), extract the index number of the first occurrence of the left string at or after the index, and the matching occurrence of the right string.

For instance,

OUTER_PATCH "(cat) (dog with a (bone) )" BEGIN
	LPF find_parenthesis_range INT_VAR index=1 STR_VAR left="(" right=")" RET start end END
END
returns start=6, end=25.

data_lines

Given either 'data' (a string), or a file name/location, return either the string or the file separated into an array of lines.

For instance:

<<<<<<<< …/stratagems-inline/poem.txt
Twas brillig, and the slithy toves
Did gyre and gimble in the wabe.
All mimsy were the borogroves
And the mome raths outgrabe.
>>>>>>>>
LAF data_lines STR_VAR path="…/stratagems-inline" file="poem.txt" RET_ARRAY poem_array=lines END
would return
$poem_array(0)="Twas brillig, and the slithy toves"
etc.

2.1.3 Data-logging functions

new_file

Create a new file 'file' at the location specified by location/locbase/path, containing only the string 'arguments' (with no added spaces or line breaks).

e.g.

LAF new_file STR_VAR file=spell_log.txt path="%data_loc%" arguments="SPWI304" END
If you specify no_log=1, the file is created permanently and will not be removed on uninstallation.

log_this

This dumps the string 'input' (synonym: 'arguments') into the text file 'file' (by default placed in the 'data_loc' directory, but you can override this by explicitly specifying 'path'). For instance,

LAF log_this STR_VAR file="new_kit_list.txt" input="DW_ELEMENTALIST_FIRE" END
puts the string 'DW_ELEMENTALIST_FIRE' into the file 'weidu_external/data/%MOD_FOLDER%/new_kit_list.txt'. The file is created if it does not exist already. If it does exist, the input is added as a new line.

There are two optional INT_VARs. If repeat=0 (the default is 1) the string will be added only if it not already present (as an entire line, not just a substring of a line). If new=1 (default is 0) any existing contents of the file will be wiped.

warning

This dumps the string 'warning' (synonym: 'arguments') into the text file '%data_loc%/sfo_warnings.txt, prepended with the component number and mod name. If repeat=0, we do this only if it is not already there.

2.1.4 Script-handling functions

extend

This is a lightweight way to apply EXTEND_TOP or EXTEND_BOTTOM. You specify:

  • an existing BCS script, 'script' (don't include the .bcs)
  • the name of BAF files 'top' and/or 'bottom' (don't include the .baf)
  • the location of 'top' and bottom in the usual SFO (location/locbase/path) format
Any variables in 'top' and 'bottom' are evaluated, then they are compiled and added at the top/bottom of 'script'.

For instance (assuming you haven't set 'component_loc') this code

LAF extend STR_VAR script=mage1 top=mage_add location=resource END

is equivalent to this:

EXTEND_TOP "mage1.bcs" "%MOD_FOLDER%/resource/mage_add.baf" EVALUATE_BUFFER 

'script' can actually be a space-separated list of scripts, all of which have the same top/bottom addition made; e.g.

LAF extend STR_VAR script="mage1 mage2 mage3" top=mage_add location=resource END

If you set INT_VAR inline=1, we use '…/stratagems-inline" as the location of the top / bottom scripts.

extend_area_script

This works like extend, except that instead of specifying a bcs file you specify an are file. The scripts are added to the are file's area script (if it doesn't have one, one is created and set using the usual conventions). For instance,

LAF extend_area_script STR_VAR area=ar2100 top=ar2100_add location=resource END

Again, 'area' can be a list of areas.

extend_worldscripts

This works like extend, except that the extension is made to all worldscripts (i.e. baldur.bcs (on any game), baldur25.bcs (on any BG2-based game), and any worldscript listed in campaign.2da. For instance:

LAF extend_worldscripts STR_VAR top=baldur_add location=resource END

merge_scripts

Takes the existing BCS files '%top%.bcs' and '%bottom%.bcs' and merges them into a new script. You can specify the name of that script via the 'script' STR_VAR or let SFO generate a unique name automatically; either way, the name of the script is returned as 'script'. E.g.

LAF merge_scripts STR_VAR top=wtasight bottom=bdsumm00 RET script END

If one of the scripts does not exist, merge_scripts does nothing and just returns the name of the other one. (If neither exists, it returns the value of 'bottom'.)

2.1.5 Tools to resolve various in-game parameters

resolve_splprot_entry

Given 'stat', 'val', and either 'relation number' or 'relation', this returns an appropriate line number in splprot.2da, adding that line if it is not already present. (See the IESDP documentation for opcode #324.) For instance:

LAF resolve_splprot_entry INT_VAR stat=0x112 val="-1" STR_VAR relation=equal RET value END

would return value=110, since that row is already present in splprot.2da on the unmodded game, whereas

LAF resolve_splprot_entry INT_VAR stat=0x110 val="-1" STR_VAR relation=equal RET value END

would (on the unmodded game) return a new value, 146 and add a new line to splprot.2da:

146       0x110         -1         1

'relation' can have the following values:
STR_VAR relation Equivalent to INT_VAR relation_number
'equal', 'equals' 1
'greater', 'greater_than' 3
'less', 'less_than' 2
'greater_equal', 'greater_equals' 4
'less_than_equal', 'less_than_equals', 'less_equal' 0
not_equal 5

You can specify an optional STR_VAR, 'description', which will be added 2.6-style to splprot.2da. For instance, on the unmodded game this code

LAF resolve_splprot_entry 
      INT_VAR stat=0x110 val="-1" 
      STR_VAR relation=equal description="ALIGNMENT=n" 
      RET value 
END

would add the line

146_ALIGNMENT=n	0x110	    -1		1

ds_resolve_stat

(NB this function is actually in the Detectable Spells library, ds.tph.)

Given a string intended to name an entry in stats.ids or splstate.ids, return the number of that entry, adding it if necessary, as in

LAF ds_resolve_stat STR_VAR id="HOLD_IMMUNITY" RET stat_ind END

On EE, this would add 'HOLD_IMMUNITY' to splstate.ids if it is not already present, and in any case return its number.

ds_resolve_stat determines heuristically where to put the stat. To understand the algorithm, let's briefly review the IE rules for which stat values are available for assignment:

  1. On oBG2 or the EE, the stat.ids entries between 89 and 134 can be set via opcode 233 (set proficiency) and the stat.ids entries between 156 and 165 can be set (using their value – 156) via opcode 282 (set scripting state).
  2. On oBG2 with ToBEx installed, stats from 387 and up can be set via opcode 318 (adding 0x10000 to their value to do so).
  3. On the Enhanced Edition, splstate.ids entries can be set using opcode 328.
The algorithm is now:
  1. If id is in stats.ids and has a legal number for a detectable-spell stat, return that number.
  2. If not, and we're on the Enhanced Edition, use splstate.ids (returning the existing number if the splstate is present already, a new number if not).
  3. If we're not on the Enhanced Edition, but we are on ToBEx, return a new stats entry above 400.
  4. If we're neither on the Enhanced Edition, nor on ToBEx, quit and return -1.

It is possible to force a stat to resolve in splstate.ids by adding ":splstate" to it. For instance (on the unmodded game),

LAF ds_resolve_stat STR_VAR id="DEATH_WARD" RET stat_ind END

returns 157, since DEATH_WARD is in stats.ids, but

LAF ds_resolve_stat STR_VAR id="DEATH_WARD:splstate" RET stat_ind END

returns 8, the DEATH_WARD number in splstate.ids.

ds_resolve stat actually has several return values:

  • stat_ind is the actual number in the IDS file (and the number you will check with CheckStat or CheckSpellState).
  • stat_param is the number you should actually use in an effect to set the spell. It equals:
    • stat_ind for spellstates and for stats in the 89-134 range
    • stat_ind - 156 for stats in the 156-165 range
    • stat_ind + 0x10000 for stats in the 400+ range (for ToBEx)
  • stat_opcode is the opcode used to set the stat/spellstate:
    • 233 for stats in the 89-134 range
    • 282 for stats in the 156-165 range
    • 318 for stats in the 400+ range
    • 328 for spellstates
  • stat_type is 'stats', 'splstate', or 'null' (the latter being set if we can't find a slot)

The following code, for instance, should let you set a stat on a creature:

LPF ds_resolve_stat STR_VAR id=IMMUNE_TO_FIRE RET stat_ind stat_param opcode END
LPF ADD_CRE_EFFECT INT_VAR opcode parameter1=1 parameter2=stat_param timing=9 target=1 END

resolve_dv

Given a cre file (specified by 'creature'), if that creature has a death variable (i.e., scripting name), the function just returns it. If not, the function allocates one and returns it. You can specify a default value (using the 'default' STR_VAR); if not, the default is just the resref of the file. (This is useful for compatibility if you need to assign a death variable to a creature that doesn't have one assigned.)

For instance, in BG2 there is a mad cleric of Cyric, DCLERIC.CRE, with no allocated script name. This code returns a valid script name for him:

LAF resolve_dv STR_VAR creature=DCLERIC RET mad_cleric_dv=dv END

If no other mod has already given him a death variable, the returned value will be DCLERIC; if another mod has given one to him, the returned value will be whatever he was given.

resolve_statdesc

A somewhat complicated function to add lines to statdesc.2da; see the main documentation if you want to use it!

2.1.6 Miscellaneous tools

install

This is a fast installer for files. You feed it a list of files (including extensions), all located at the same path (which you specify SFO-style via location/locbase/path). For each file:

  • If it is a .baf or .d file, variables are evaluated and then it is compiled.
  • If it is any other file, it is copied over to override – unless a file of the same name is already present in the game, in which case we do nothing.
For instance,

LAF install STR_VAR files="dwhobgob.baf dwhobgob.d hobgob.cre" location=resource END

is equivalent (if you haven't set component_loc) to

COMPILE "%MOD_FOLDER%/resource/dwhobgob.baf" EVALUATE_BUFFER
COMPILE "%MOD_FOLDER%/resource/dwhobgob.d" EVALUATE_BUFFER
ACTION_IF !FILE_EXISTS_IN_GAME "hobgob.cre" BEGIN
	COPY "%MOD_FOLDER%/resource/hobgob.cre" override
END

A few more details:

  • 'file' and 'arguments' are synonyms for 'files'.
  • If you set INT_VAR overwrite=1, files will be copied even if they are already in the game.
  • If you set inline=1, the files are assumed to be located at "…\stratagems-inline".

add_basic_spell_ability

This patch function is intended to be used with WEIDU's CREATE command: operating on a newly-created spell, it adds a single ability (treated as innate). This is useful to create lightweight mini-spells to produce specific effects.

For instance, here's code to produce a spell that does 4d6 fire damage:

CREATE spl dw-fire
	LPF add_basic_spell_ability END
	LPF ADD_SPELL_EFFECT 
		INT_VAR opcode=12 parameter2=8*0x10000 timing=1 dicenumber=4 dicesize=6 target=2 
END