Larian Banner: Baldur's Gate Patch 9
Previous Thread
Next Thread
Print Thread
Joined: Mar 2016
Location: Belgium
T
addict
OP Offline
addict
T
Joined: Mar 2016
Location: Belgium
Hi,

After having dabbled a lot in itemScripts/charScripts lately, I'd like to share a couple of things I've noticed that may save some hair tearing for other people.

There is already a good introduction elsewhere, but I'd like to mention a few additions/clarifications:

General
• After creating a new script, only specify a name for the script. Do not try to specify a path, the (EE) editor will automatically put it in the right place and create the necessary folders (and trying to specify a path will mostly result in errors about you trying to select a wrong location)
• After creating/modifying a script, you do not have to reload the module. It is sufficient to reload the story (open the story editor and press F8)
• After reloading a level (via File -> Reload level), you also have to explicitly reload the story if you want the OnInit() event handlers of your charScripts/itemScripts to run again (which is almost always the case!)

Scripting language
• When declaring a variable, the syntax is TYPE:NAME without any spaces before or after the ":"! Adding any whitespace around the ":" will result in a syntax error.
• You can give a global or local variable an initial value via "=", e.g. INT:_Count = 0. The number of spaces around the "="-sign does not matter.
• The "local" variables of an EVENT or BEHAVIOUR are like static variables in C. This means that they keep their value once assigned, even if they are declared with an initial value. E.g. INT:_MyVar = 0 means that the first time this EVENT/BEHAVIOUR is entered _MyVar will be equal to 0, but on subsequent invocations it will have whatever value it had when the EVENT/BEHAVIOUR exited the last time.
• An extra variable type that is not mentioned in the aforementioned tutorial is FIXEDSTRING. Variables of this type can be used to specify variable names to e.g. SetVar()
• Some calls that can take multiples types of data, such the aforementioned SetVar(), may need to explicitly know the type of a constant to determine what kind of data you are passing. You can create typed constants by prepending the type to the constant like with a variable declaration, such as SetVar(_SomeChar,"SomeItemVar",ITEM:null)
• Unlike in the story editor, you cannot replace OUT-parameters to calls in conditional checks with constants to avoid having to declare a variable and checking its value later. Unfortunately, such constructs to not result in compile time or run time errors. I.e., this will compile and run, but the THEN-clause will always run assuming _SomeItem has a global variable called %GlobalntVar, regardless of its value:
Code
IF "c1"
    GetVar(INT:0,_SomeItem,"GlobalIntVar")
THEN
    ItemEvent(__Me,"SomeItem.GlobalIntVar == 0! Or any other value, really.")
ENDIF


Debugging
• Sometimes when saving your script the editor will check it for errors, and sometimes it doesn't. When it doesn't, explicitly choosing Build->Compile or Build->Build All won't help. The only way to check your script for errors in that case is to start the module in the editor (smiley face on the top left of the main editor window). Any detected errors will appear in the MessagePanel at the bottom of the main editor window. After correcting the errors, make sure to reload the story for your corrections to be applied. New errors may be detected after fixing previous ones. Scripts with errors in them are disabled.
• The line numbers mentioned for error messages are wrong if your script contains any completely blank lines: the blank lines are not counted at all. To work around this, ensure every line of your script contains at least a space or tab.
• The error messages in the MessagePanel may be cut off, even after dragging the (invisible) divider to the right. The full messages are logged to errors.txt in the folder containing the editor binary ("Divinity - Original Sin Enhanced Edition"\"The Divinity Engine Enhanced Edition"), but unfortunately the output to that file is buffered. This means you may have to quit the editor to be able to see all messages in it.
• There is a "Debug" checkbox on the top right of item/charScripts, but I have no idea what it does apart from showing a sidebar with script section names.
• Unlike for story scripts, things that happen in item/charScripts are not logged to osirislog.log. You can work around this by making the items/characters to which you attach scripts global, and subsequently adding things like ItemEvent(__Me,"entering event X"), ItemEvent(__Me,"taking then-branch for check X") and even CharacterItemEvent(__Me,_SomeItem,"this is the item I'm getting"). These events will then be logged to osirislog.log along with their concrete parameter values (so you can use other variables besides __Me if you wish to log which items/characters are involved). In all cases, make sure everything you pass to the events is global if you want their values to be logged.
• An alternative debugging method, also mentioned elsewhere on the forum, is to play various effects. My favourite ones are pillar loop effects with different colours, as they can be used to create a persistent visual of the order in which things happened, and to debug locations (FLOAT3, but also character/item locations). E.g. ItemPlayLoopEffect(_,__Me,"FX_GP_LightPillarRed_A") (type PlayLoop in the script editor to see the other variants). I ignore the return value (the first parameter, a handle to the effect) since you only need it to remove the pillar again. If you want to get rid of them at some point, reload the level (followed by reloading your scripts, as mentioned earlier). Other pillar loop effects are FX_GP_LightPillarWhite_A, FX_GP_LightPillarBlue_A, FX_GP_LightPillarGreen_A, FX_GP_LightPillarPurple_A and FX_GP_LightPillarOrange_A.

Design/code patterns
• If you have code that would set or get array elements from a fixed length array in a loop in most imperative programming languages, you can probably achieve the same effect with separate variables combined with GetElement(), and GetVar()/SetVar(). E.g.
Code
INIT

INT:%IntVar1
INT:%IntVar2
INT:%IntVar3

EVENTS

EVENT ResetVars
VARS
  FIXEDSTRING:_VarName
  INT:_Counter
ON
  OnCharacterEvent(__Me,"ResetVars")
THEN
  // don't just declare "INT:_Counter = 0" above, as that initialisation would
  // only apply the first time this code is executed!
  Set(_Counter,0)
  WHILE "c1"
    IsLessThen(_Counter,3) // I really want to know who is responsible for this typo :)
  DO
    // Warning: do NOT use FIXEDSTRING:"IntVar1" etc, that seems to break things
    //   when you get to 4 or 5 elements, and it's not necessary as the compiler will
    // automatically convert these string constants to FIXEDSTRING as needed
    GetElement(_VarName,_Counter,"IntVar1","IntVar2","IntVar3")
    SetVar(__Me,_VarName,0)
  ENDWHILE

• You can also use FIXEDSTRING variables with GetVar() and SetVar() to create you own "OUT"-parameters (or even IN/OUT) to events and functions. Or even for introspection, since GetVar() will return false if a variable with that name does not exist. E.g. (from my "rails" WIP; fragment, may not compile as is):
Code
INIT
	
ITEM:__Me

// Parameters to/result from MaybeAssignTrackConnection function
FLOAT3:%MATC_CheckPos					// position of the endpoint of the current track
ITEM:%MATC_CheckOtherItem				// other track of which the endpoints we should try to match to this track's endpoint
FIXEDSTRING:%MATC_ConnectingItemName	// if we match an endpoint of the other track, connect it to this track's "ConnectingItemName" field

// Names of the fields for the tracks connecting to the current track
// (one incoming and at most two outgoing tracks)
FIXEDSTRING:%IncomingTrackName = "IncomingTrack"
FIXEDSTRING:%OutgoingTrackName1 = "OutgoingTrack1"
FIXEDSTRING:%OutgoingTrackName2 = "OutgoingTrack2"
	
// Names of the fields with the positions of the current track's connections
FIXEDSTRING:%IncomingPosName = "BeginPos"
FIXEDSTRING:%OutgoingPosName1 = "EndPos1"
FIXEDSTRING:%OutgoingPosName2 = "EndPos2"

EVENTS

// For each nearby track, iterate over all of our connection points
EVENT OnIterate_NearbyTracks
VARS
	ITEM:_Item
	ITEM:_TrackConnection
	ITEM:_TrackConnectionPosition
	INT:_Index
	FIXEDSTRING:_PosVarName
ON
	OnIterateItem(_Item,"RAILS_OnIterate_NearbyTracks")
ACTIONS
	// IterateItemsNear(__Me,..) also iterates over __Me itself
	IF "c1"
		IsEqual(__Me,_Item)
	THEN
		RETURN
	ENDIF
	
	Set(%MATC_CheckOtherItem,_Item)
	Set(_Index,0)
	// Loop over all of our connection points
	WHILE "c1"
		IsLessThen(_Index,3)
	DO
		GetElement(%MATC_ConnectingItemName,_Index,%IncomingTrackName,%OutgoingTrackName1,%OutgoingTrackName2)
		GetElement(_PosVarName,_Index,%IncomingPosName,%OutgoingPosName1,%OutgoingPosName2)
		// Connection point not yet connected -> try to connect it to the current iterator item
		IF "c1&c2&c3"
			GetVar(_TrackConnection,__Me,%MATC_ConnectingItemName)
			IsEqual(_TrackConnection,null)			
			GetVar(%MATC_CheckPos,__Me,_PosVarName)
		THEN
			CallFunction("MaybeAssignTrackConnection")
			// don't exit even if connected, since in theory there may be
			// multiple connections between the two items
		ENDIF
		Add(_Index,1)
	ENDWHILE

// Check whether our selected connection point is the same as a connection point
// of another track, and if so assign us to that connection point, and them to ours
//
// Parameters:
//   * ITEM:%MATC_CheckOtherItem 				: the other track
//   * FLOAT3:%MATC_CheckPos 					: the position of our connection point
//   * FIXEDSTRING:%MATC_ConnectingItemName 	: the name of the field representing our connection point
// Result: /
EVENT MaybeAssignTrackConnection
VARS
	FLOAT3:_OtherConnectionPos
	ITEM:_OtherConnectionTrack
	FLOAT:_ConnectionsDistance
	INT:_Index
	FIXEDSTRING:_TrackVarName
	FIXEDSTRING:_PosVarName
ON
	OnFunction("MaybeAssignTrackConnection")
ACTIONS
	// Loop over all possible connection points of the other track
	Set(_Index,0)
	WHILE "c1"
		IsLessThen(_Index,3)
	DO
		GetElement(_TrackVarName,_Index,%IncomingTrackName,%OutgoingTrackName1,%OutgoingTrackName2)
		GetElement(_PosVarName,_Index,%IncomingPosName,%OutgoingPosName1,%OutgoingPosName2)
		// Does this connection point exist (c1), is it not yet assigned (c2) and
		// does it connect to our connection point (c3&c4&c5)?
		IF "c1&c2&c3&c4&c5"
			GetVar(_OtherConnectionTrack,%MATC_CheckOtherItem,_TrackVarName)
			IsEqual(_OtherConnectionTrack,null)
			GetVar(_OtherConnectionPos,%MATC_CheckOtherItem,_PosVarName)
			GetDistance(_ConnectionsDistance,%MATC_CheckPos,_OtherConnectionPos)
			IsLessThen(_ConnectionsDistance,%ConnectionFuzz)
		THEN
			// Connect them to us and us to them
			SetVar(%MATC_CheckOtherItem,_TrackVarName,__Me)
			SetVar(__Me,%MATC_ConnectingItemName,%MATC_CheckOtherItem)
			// Finished for this connection point of ours
			RETURN
		ENDIF
		Add(_Index,1)
	ENDWHILE

This code is part of RAILS_ConnectedTrackBase.itemScript, which is imported/used by all tracks (straight/corner/switch). A track has one incoming track and up to two outgoing tracks. What happens here is that RAILS_OnIterate_NearbyTracks iteratively sets MATC_ConnectingItemName to the different connection points that a track may have, checks whether it exists (via GetVar) and if so, it calls MaybeAssignTrackConnection and passes the name of that variable via MATC_ConnectingItemName. That routine can then use SetVar to change that variable's value if desired.

Last edited by Tinkerer; 16/04/16 05:25 PM. Reason: Minor correctons
Joined: Mar 2016
Location: Belgium
T
addict
OP Offline
addict
T
Joined: Mar 2016
Location: Belgium
Debugging
• If you get errors in the MessagePanel regarding "Removing script that does not exist! C:/full/path/to/scriptThatDoesInFactExist.itemScript", the problem may be that the mentioned script has an #INCLUDE/USING statement for another script and that this other (unnamed) script does not exist.

Joined: Jun 2015
F
enthusiast
Offline
enthusiast
F
Joined: Jun 2015
Very useful information, thanks !

I was just in the editor and accidently copy & pasted a variable so that I ended up having %%variablename. The editor did not complain when I saved.
Have you ever tried, if something like that works ? (It does in scripting languages like PHP or shell scripts I think.)

Joined: Mar 2016
Location: Belgium
T
addict
OP Offline
addict
T
Joined: Mar 2016
Location: Belgium
Originally Posted by FrauBlake

I was just in the editor and accidently copy & pasted a variable so that I ended up having %%variablename. The editor did not complain when I saved.
Have you ever tried, if something like that works ? (It does in scripting languages like PHP or shell scripts I think.)

No, I haven't tried. I'd shy away from such things even if they seem to work at first sight, because you never know what the effect may be given that there is no official syntax or grammar definition for this language. For all we know, variables whose name starts with two %-characters are used internally by the scripting engine for various purposes, and everything will work fine until you accidentally pick a name that's already used that way...

Joined: Jun 2015
F
enthusiast
Offline
enthusiast
F
Joined: Jun 2015
Originally Posted by Tinkerer
Originally Posted by FrauBlake

I was just in the editor and accidently copy & pasted a variable so that I ended up having %%variablename. The editor did not complain when I saved.
Have you ever tried, if something like that works ? (It does in scripting languages like PHP or shell scripts I think.)

No, I haven't tried. I'd shy away from such things even if they seem to work at first sight, because you never know what the effect may be given that there is no official syntax or grammar definition for this language. For all we know, variables whose name starts with two %-characters are used internally by the scripting engine for various purposes, and everything will work fine until you accidentally pick a name that's already used that way...

This is very understandable. I would assume it was actually an error but the editor has a code parser too simple too catch it.

I was just curious and after seeing your moving trains I consider you an absolute expert in character and item scripts ;-)


Link Copied to Clipboard
Powered by UBB.threads™ PHP Forum Software 7.7.5