DavidW Posted September 10, 2013 Share Posted September 10, 2013 One of the things I find most frustrating about modding the IE is that while we have great tools for editing dialog files, there's very little comparable for scripts - the best you can do is play games with REPLACE_TEXTUALLY and REPLACE_EVALUATE. My scripting language SSL was a partial attempt to rectify this, but while SSL is good for writing entirely new scripts, it has at most rudimentary capabilities for editing existing scripts. In working on IWD-in-BG2, I've become sufficiently irritated by this to write a tool (a set of WEIDU functions, really) to facilitate automated script editing. I thought I'd explain how it works in case it's of any interest to anyone else. (It's not a beginner's tool: you need to be able to write your own functions to use it.) The basic syntax is LAF edit_script STR_VAR script=<list of scripts> action=<list of instructions> END (The "edit_script" function is really just a wrapper for a patch function that acts on the decompiled script.) Alternatively, LAF edit_all_scripts STR_VAR action=<list of instructions> END runs the edit on all scripts in the game. (And so runs a bit slowly, and shouldn't be used too lightly.) An instruction is a pair, task=>function. "task" is one of three possible tasks: "patch", "insert_top", and "insert_bottom", and "function" is the name of a function supplied by the user. (So edit_script is a second-order function - its a function some of whose arguments are other functions.) "patch" is the main useful one (I'm unsure if the others have much use). "function" has to have a very specific form: DEFINE_ACTION_FUNCTION <function> INT_VAR blocknum=0 STR_VAR trig_in="" act_in="" filename="" RET trig_out act_out <main definition END The idea is the following: each block in the script, in turn, is broken down into a "trigger string" (a list of the triggers separated by spaces) and an "action string" (everything between THEN and END, with lines separated by spaces and with RESPONSE #n being replaced with RESPONSE-#n to be one word). The trigger string and action string, along with the numerical position of the block in the script and the name of the file itself, are fed to function, and the outputs are used to construct a new block which is inserted in place of the old one. That's a bit abstract, so here are a couple of examples: (1) Making a list of all scripts that have a block containing CreatureHidden(Myself) as a trigger and HideCreature(Myself,FALSE) as an action. Here's our function: DEFINE_PATCH_FUNCTION BCS_list_hidden STR_VAR trig_in="" act_in="" filename="" RET trig_out act_out BEGIN SPRINT trig_out "%trig_in%" // we don't actually want to modify the block SPRINT act_out "%act_in%" PATCH_IF (!"%trig_in%" STRING_MATCHES_REGEXP "CreatureHidden(Myself)" & !"%act_in%" STRING_MATCHES_REGEXP "HideCreature(Myself,FALSE)") BEGIN PATCH_PRINT "%filename%" // obviously in real applications it would be more sensible to output it to a file rather than the screen END END And the command is just LAF edit_all_scripts STR_VAR action="patch=>list_hidden" END (notice that the BCS_ prefix is optional) (2) Modifying all UseItem and Spell blocks so as to introduce a timer that prevents them being used too closely together The function is DEFINE_PATCH_FUNCTION BCS_item_timer STR_VAR trig_in="" act_in="" RET trig_out act_out BEGIN PATCH_IF !"%act_in%" STRING_MATCHES_REGEXP "\(Spell\|UseItem\)" BEGIN SPRINT trig_out "!GlobalTimerNotExpired("castspell","LOCALS") %trig_in%" LPF insert_after_response STR_VAR act_in to_insert=~SetGlobalTimer("castspell","LOCALS",6)~ RET act_out=act_out END END ELSE BEGIN SPRINT trig_out "%trig_in%" SPRINT act_out "%act_in%" END END On running LAF edit_script STR_VAR script=<the list> action="patch=>item_timer" END, blocks like IF HaveSpell(WIZARD_MAGIC_MISSILE) See(NearestEnemyOf(Myself)) THEN RESPONSE #100 Spell(NearestEnemyOf(Myself),WIZARD_MAGIC_MISSILE) END take the form IF !GlobalTimerNotExpired("castspell","LOCALS") HaveSpell(WIZARD_MAGIC_MISSILE) See(NearestEnemyOf(Myself)) THEN RESPONSE #100 SetGlobalTimer("castspell","LOCALS",6) Spell(NearestEnemyOf(Myself),WIZARD_MAGIC_MISSILE) END (The actual code here is a bit fragile and doesn't deal with indeterministic casting very well, but I don't want to clutter this discussion up.) (3) If you want to delete a block entirely, you can do so by returning empty strings. Here's a function that deletes every block that has both a HideCreature command and a DestroySelf command in the action block (many IWD scripts do this, pointlessly in nearly all cases). DEFINE_PATCH_FUNCTION BCS_kill_superfluous_hide STR_VAR trig_in="" act_in="" RET trig_out act_out BEGIN PATCH_IF !"%act_in%" STRING_MATCHES_REGEXP HideCreature & !"%act_in%" STRING_MATCHES_REGEXP DestroySelf BEGIN SPRINT trig_out "" SPRINT act_out "" END ELSE BEGIN SPRINT trig_out "%trig_in%" SPRINT act_out "%act_in%" END END (4) If you want to duplicate a block (perhaps under certain conditions), just use multiple patches. For instance, IWD has a SetBestWeapon() command that doesn't exist in BG2, but we can (more or less) duplicate it by using EquipMostDamagingMelee if within range 5, EquipRanged otherwise. Here's how to do it: DEFINE_PATCH_FUNCTION BCS_best_weapon_1 STR_VAR trig_in="" act_in="" RET trig_out act_out BEGIN PATCH_IF "%act_in%" STRING_CONTAINS "SetBestWeapon" BEGIN INNER_PATCH_SAVE act_out "%act_in%" BEGIN ReplaceTextually "SetBestWeapon" "EquipMostDamagingMelee" END SPRINT trig_out "Range(NearestEnemyOf(Myself),5) %trig_in%" END ELSE BEGIN SPRINT trig_out "%trig_in%" SPRINT act_out "%act_in%" END END DEFINE_PATCH_FUNCTION BCS_best_weapon_2 STR_VAR trig_in="" act_in="" RET trig_out act_out BEGIN PATCH_IF "%act_in%" STRING_CONTAINS "SetBestWeapon" BEGIN INNER_PATCH_SAVE act_out "%act_in%" BEGIN ReplaceTextually "SetBestWeapon" "EquipRanged" END SPRINT trig_out "!Range(NearestEnemyOf(Myself),5) %trig_in%" END ELSE BEGIN SPRINT trig_out "" SPRINT act_out "" END END Now if we run LAF edit_script STR_VAR script=<list> action="patch=>best_weapon_1 patch=>best_weapon2" END we get the desired effect. (Notice that exactly one of the patches must return the original pair of strings if the condition is not met, and the other(s) must return empty strings.) Link to comment
Ardanis Posted September 11, 2013 Share Posted September 11, 2013 I use this as a template when I need to edit a script. I tried to come up with a friendly FUNCTION "interface", but there're just too wide a range of possible tasks to carry out. REPLACE_EVALUATE ~IF\(\([%MNL%%LNL%WNL%].+\)*\)[%MNL%%LNL%WNL%]THEN\(\([%MNL%%LNL%WNL%].+\)*\)[%MNL%%LNL%WNL%]END~ BEGIN SPRINT if ~%MATCH1%~ SPRINT then ~%MATCH3%~ INNER_PATCH_SAVE if ~%if%~ BEGIN REPLACE_EVALUATE ~[%MNL%%LNL%WNL%]\(.*\)$~ BEGIN SPRINT trigger_block ~%MATCH1%~ END ~ %trigger_block%~ END INNER_PATCH_SAVE then ~%then%~ BEGIN REPLACE_EVALUATE ~[%MNL%%LNL%WNL%]\([%TAB% ]*RESPONSE.+\)\(\([%MNL%%LNL%WNL%].*\)*\)~ BEGIN SPRINT response ~%MATCH1%~ SPRINT action_block ~%MATCH2%~ INNER_PATCH_SAVE action_block ~%action_block%~ BEGIN REPLACE_EVALUATE ~[%MNL%%LNL%WNL%]\(.*\)~ BEGIN SPRINT action ~%MATCH1%~ END ~%action%~ END END ~ %response% %action_block%~ END END ~IF %if% THEN %then% END~ PS I hate this forum engine. Link to comment
Recommended Posts
Archived
This topic is now archived and is closed to further replies.