Jump to content

Automated script editing


DavidW

Recommended Posts

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

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

Archived

This topic is now archived and is closed to further replies.

×
×
  • Create New...