Skip to content

Scripting Language Guide

szymor edited this page Apr 30, 2023 · 1 revision

Scripting

Basics

The great majority of game's logic is in scripting; unless you want to fix bugs or turn SoulFu into a completely different game, like say, Battle for Wesnoth, you will have to deal with scripting, not programming (thankfully!).

The script system is placing keywords in such a way that it has a meaning, just like creating a sentence.

Another detail: the script system is case insensitive. That means, that GOOD is the same as good which is the same as GoOd. That applies to everything from A to Z.

Comments

If at anytime you want to a note of some kind to your script, you just have to add // in front of the line, and the entire line will be ignored.

Defines

If you want to give a name to a constant value, you can do so with #define. Place your #define at the beginning of the file, and you will be able to access that data from anywhere in the file! For example:

#define cookies 7

Functions

The scripts are divided in functions. A function declaration looks like this:

int ModelSetup()

The first part, 'int' is the return type, the type of variable the function returns; the variable the function returns can be used to get information by other functions.

The second part, 'ModelSetup' is the name of the function, can only be made of a-z characters, upper or lower-case.

The third part(), is the argument. The arguments give information to the function.

The starting point for the script system is in the WSTART.SRC file, the Spawn() function.

After the line of the function's declaration ends, there are commands; in each command line you have to add indentation, which is two spaces at the beginning of the line; this will tell the game that these commands belong to the function that was last declared in the previous indentation level.

Variables

You can use a variable to store information for later use, but unlike definitions, they can be modified after they are declared, for example:

int number
number = 1

Now, every time you write 'number', it is as if you wrote '1'.

The first line is the variable's declaration. 'int' is the variable's type, just like the function's declaration. The type can be int or float. Int is an integer number, while float is a number with decimals; you could use floats for everything, but they are slower, more prone to errors, take longer to type, and other bothersome characteristics, so use floats only when you have to. In the first line 'number' is the name of the variable. Both int and float have limits to how big your numbers can be; however the limit of int goes from -32768 to 32767, and floats have a huge range, but the more decimals you use and the bigger the number is, the precision gets worse (that is, you might want to store 9999999.000001, but most likely you will store an approximate), because float numbers try to store an infinite number in a finite amount of space.

The second line gives a value to number; every time you place a single = after the name of a variable, it will give it the value after = .

An int can also be used to store text, as it will store where in the memory the text is located as a number. You have to put " around text. Here's an example of float and text usage:

int text
float number
text = "Hello world!"
number = 42.42

In a similar way to the text, an int stores any kind of memory value, called pointers. You can get a pointer to a file that can be used with some functions, with a string that has 'FILE:' as a prefix, for example:

int file
file = "FILE:MYFILE.DAT"

You can also declare more than one variable of the same type if you separate them with, like this:

int text, number
text = "Hello world!"
number = 42

Variables initially have an unknown value, so make sure you set to a known value them before using them.

Arithmetic operations

You can add, substract, divide, multiply, modulus, increment, decrement or shift. Addition, substraction, division and multiplication are pretty easy to understand:

int number
// Add
number = 9 + 1
// Substract
number = number - 1
// Divide
number = number / 3
// Multiply
number = number * 3

The result will be 9. Take into account that if you divide an int, the remainder just gets thrown away, that is, with integers, 14 / 3 would give 4, with a remainder of 2 that is lost. Modulus ( % ) allows you to get the remainder, so 14 % 3 would be 2.

Incrementing adds 1 to the variable; decrementing subtracts one:

int number
number = 0
// Increase
number++
// Decrease
number--

The result is 0.

There are two kinds of shifts: shift left ( << ) and shift right ( >> ); shifting will move the bits in your variable to the specified size, for example, if you have the a byte with bits 11000100, using << 1 on it will give 10001000, the left-most bit was lost and in the right most position a 0 is inserted.

One use of shifting is that, in X << Y, the left shift will multiply X by 2^Y, so 2 << 1 would be like 2 * 2 ^ 1, which is 4. 3 << 3 would be 3 * 2 ^ 3 which is 3 * 8, so in the end it would be 24.

X >> Y does X / 2 ^ Y.

So why complicate our simple life with this: shifting is faster than multiplication and WAY faster than division for the computer, so use shifts whenever you can.

Another use, a more important one, is to access only a certain portion of the bits in your variable. Let's say you have a variable that holds a value bigger than 255 (the limit of a byte) like 404, and a function that only works with bytes, so what do you do?

int big_variable
big_variable = 404
MadeUpFunction( big_variable )
MadeUpFunction( big_variable << 8 )

Note by szymor: this script looks buggy, shouldn't it be big_variable >> 8 ?

The function would read the variable as a byte, only taking into account the first 8 bits, and you would pass the variable as two bytes, one at a time. If you don't need the entire variable for the function to succeed, that will work just dandy, as long as your variable doesn't holds a value that needs more than 2 bytes of space.

Note: different computers order the bits within variables in different order; that is, it could be that to access the byte that holds up to 255 needs to be accessed like this:

variable >> 8

The majority of computers today use what is called 'little-endian', which stores the smaller values at the beginning. The 'big-endian' have advantages over little-endian in some situations, so what is best is dependent on application. You can ignore this most of the time, but don't be surprised when you write a file with SoulFu in your PC, copy to a PPC Mac, and it doesn't work.

Note by szymor: I think the author messes up concepts of bit and byte ordering.

If your head hurts now, I'm truly sorry.

Be good girl / boy and be careful with arithmetic operations on pointers, you don't want to screw up the program's memory and crash SoulFu, do you?

Predefined Basic Functions ( PLEASE COMPLETE ME puppy eyes )

All that stuff so far is pretty nice for calculations, but how do we actually use it for something useful? one of the ways to do that is to use the Basic Functions. Here's the full list:

  • if(x)
  • else
  • while(x)

These three functions are special in that they allow you to control the flow of the script; for example, you could make it so that a cat eats apples, but only if there is no tripe near.

X is the logical expression to be evaluated; for this you have several tools, like the arithmetic operations previosly described; but in addition, you now have conditional operators, == (is equal) != (is not equal) < (less than) > (bigger than) <= (equal or less than) >= (equal or bigger than) && (both are not 0) || (one of them is not 0) ! (is 0) & (bitwise AND) | (bitwise OR).

What & and | do is compare bits of variables. Using & will result in 1 if both are equal and 0 if they aren't, while | will give 1 when any variable has a 1 in that bit. For example, if you have variables of 1 bytes named X and Y with bits 01010000 and 00001100, the result of X & Y will be 10100011, because in those positions, both variables have 0s and in no position both have 1s, while X | Y will give 01011100, sort of like merging them.

One use of this is, if you have a variable that is bigger than 1 byte, but you split it into two bytes, to merge them back you could do:

result = X | Y >> 8

Also, just like with the contents of functions, you have to indent the contents of if, else and while, so they know what belongs to them:

int x
x = 1
if( x + 1 > 0 )
  CallMadeUpFunction()
else
  CallAnotherMadeUpFunction()

while does not branches in he same way as 'if' and 'else'; when the end of the while content's is reached, it will check if the condition is still true, and if so, it will repeat again, and again, until the condition is true:

int x
x = 0
while( x < 100 )
  x++

In the end, x will check 101 times for the condition, will go through the contents 100 times, and x will be equal to 100.

Be careful with getting into an infinite while loop; if you do, SoulFu will just freeze. Here's an example of what you should NOT do:

int x, y
x = 0
y = 0
while(x < 100)
  y++ 
x++

The loop will never exit, as you are increasing y in the loop, yet the condition checks x instead of y; x will only be increased after you exit the loop, which will never happen.

  • ToInt

  • ToFloat

  • LocalMessage(X, Y) prints a message to the message window of your computer, in the format 'X : Y', where X and Y are text.

  • NetworkMessage

  • LogMessage

  • FindSelf() returns a pointer to the object you are working on, for example a character or a window. This is so you can use the properties of the object; the types of objects are character, particles and window. To know existing properties, look at the file DEFINE.TXT that comes with SoulFu, and search for 'property data'.

Properties are invoked like this:

int self
self = FindSelf()
self.lasthit

Here, lasthit is the property; it behaves as a normal variable.

  • SystemSet

  • SystemGet

  • DebugMessage( X ) prints a string of text to the message window with the prefix 'DEBUG: ', where X is the text that is printed.

  • sqrt

  • FileOpen( X, Y ) returns a pointer to file X, and executes action Y; check DEFINE.TXT for the actions. Assuming there is an action named FILE_NORMAL that does no special actions, here's how you would use it:

int file
file = FileOpen( "MYFILE.TXT", FILE_NORMAL );
  • FileReadByte( X, Y ) returns a byte from file X at position Y; the file must exist and be big enough.

  • FileWriteByte( X, Y, Z ) writes a byte with value Z to file X at position Y; the file must exist and be big enough.

  • FileInsert( X, Y, Z, A ) inserts or removes bytes of file X at position Y; if inserting, Z is a pointer to the data to insert and A is the amount of bytes to insert, if removing, Z should be NULL and A should be a negative number that refers to the amount of bytes to remove. This function is only available if you have the development tools enabled, so it cannot be used for normal gameplay.

  • Spawn

  • GoPoof( X ) calls return and destroys the object specified by X; for possible values for X, search DEFINE.TXT.

  • Dismount

  • RollDice( X, Y ) returns a random value that can go from 0 to Y - 1, X number of times.

  • PlaySound

  • PlayMegaSound

  • DistanceSound

  • PlayMusic

  • UpdateFiles( MODE ) performs an operation on the datafile.sdf archive, as specified by MODE. Valid MODEs are:

  1. UPDATE_END: unloads and reloads all files in the datafile.sdf archive.
  2. UPDATE_RECOMPILE: recompiles the script files.
  3. UPDATE_SDFSAVE: saves the datafile.sdf archive to the disk, in the directory of the SoulFu executable.
  • sin

Predefined String Functions ( PLEASE COMPLETE ME puppy eyes )

These work with text strings, called strings because they are strings of characters.

  • String( X ) SoulFu provides 16 global strings for any kind of use, besides the ones you can declare. This function returns a pointer to one of those strings, where X is a number from 0 to 15, that refers to which string you are pointing at.

  • StringGetNumber

  • StringClear

  • StringClearAll() clears all the global strings.

  • StringAppend

  • StringCompare

  • StringChopLeft

  • StringChopRight

  • StringSetValue( X, Y, Z) overwrites a byte in a string of characters pointed at by X at position Y with the byte Z. Can be used to write to files.

  • StringRandomName

  • StringSanitize

  • StringLanguage

  • StringUppercase

  • StringLowercase

  • StringAppendNumber( X, Y, Z) converts a number Y into a string and appends it to string X, where Z is the size of the string

Window functions ( PLEASE COMPLETE ME puppy eyes )

  • WindowBorder( X, Y, Z, A, B, C ) creates a new window; buttons and other widgets created now will go inside this window.

If X is 0, you can grab and drag the window around with the mouse. If it's 1, you can.
Y is the X position from the center of the parent window, where Y = 50 equals all the way to the right, and -50 all the way to the left.
Z is the Y position from the center of the parent window.
A is the length of the window. If it's smaller than it's contents, it will grow.
B is the height of the window. If it's smaller than it's contents, it will grow.
C is the type of border. Check DEFINE.TXT for the posibilities.

  • WindowString
  • WindowMinilist
  • WindowSlider
  • WindowImage
  • WindowTracker
  • WindowBook
  • WindowInput
  • WindowEmacs
  • WindowMegaImage
  • Window3DStart
  • Window3DEnd
  • Window3DPosition
  • Window3DModel
  • ModelAssign
  • ParticleBounce
  • WindowEditKanji
  • Window3DRoom

Fast functions ( PLEASE COMPLETE ME puppy eyes )

Fast functions are special; they are not implemented in the sourcecode, but in the scripts! the game calls those functions in under some circumstances, and you can implement it the way you want but you aren't forced to do it.

  • Spawn() Called whenever the script object instance is created.

  • Refresh

  • Event

  • AIScript

  • ButtonEvent

  • GetName

  • Unpressed

  • FrameEvent

  • ModelSetup

  • DefenseRating

  • Setup

  • DirectUsage

  • EnchantUsage

Extra functions ( PLEASE COMPLETE ME puppy eyes )

There are other predefined functions that don't fall into any other category:

  • IndexLocalPlayer

  • FindBinding

  • FindTarget

  • FindOwner

  • FindIndex

  • FindByIndex( X ) returns a pointer to all the data of a character, where X is the index of the character.

  • FindWindow

  • FindParticle

  • AttachToTarget

  • GetDirection

  • DamageTarget

  • ExperienceFunction

  • AdquireTarget

  • FindPath

  • ButtonPress

  • AutoAim

  • RoomHeightXY

  • return

Accessing external script functions

You can easily call a function implemented in a completely different .SRC file, simply use .. For example, if you have an item file called IMANA.SRC which describes a mana potion, and the function int Effect( int self ) gives back half of your max mana, you could easily make that a mage regains half his mana each time it is hit, by placing:

int self
self = FindSelf()
IMANA.Effect(self)

Whenever you detect the mage is hit.

Game conventions

There are times when suggestions are made just to make it easier to understand how the game works; these aren't enforced by the game, so you could easily bypass them, but it's frowned upon.

Filename conventions

Source code C files are written in lowercase.
Game data files( the ones in the sdf directory ), are written in uppercase.
Gamelogic use the .SRC extension
.SRC files have a one character prefix that gives us some info. Here are all prefixes:

  • C creature / object
  • I item
  • W window
  • P particle Function names use camel-case, that is, don't call the function PLAYSOUND, instead refer to it with PlaySound, exceptions are if, while, else, sqrt, sin and return, as they have similar counterparts in the C language.
    Variable names are lowercase.

Miscellaneous tips

Be careful with the names of variables. If you name them like a script filename or like a #define, chances are that you'll use the #define or use a pointer to the script file instead.
If you find something in a script that is not described in this document, look into the file DEFINE.TXT, because most likely it's a global define .