How to Write a Soldat ScriptThis guide assumes that you know basic programming practices, such as how to use loops, if's, functions, arrays, etc. For good info on this stuff go to:
http://www.taoyue.com/tutorials/pascal/Instead of focusing on that stuff, I will primarily focus on the process of writing a script, as well as many issues that you will surely run across.
Part 1: Getting Set Up:Firstly, if you haven't already, download Notepad++.
http://notepad-plus.sourceforge.net/uk/site.htmUse this to write your code in, the syntax highlighting is awesome.
Note: Notepad++ is not required, any text editor will do. However, the syntax highlighting in it, combined with its speed and ease of use, makes it my favorite.
Next open up a new tab and go to
http://devs.soldat.pl/index.php/Server_ScriptingThis is your home.
In here you can see all the events, functions, and variables that you can use. Warning: many of them are wrong or outdated, be careful.
In your dedicated server, open the scripts folder. Add a new folder to it, name it the name of the script (Yes the
folder name is what the script is called, not the .pas file). In my case I'll just call it "MFS" which stands for "My first script."
Open up that folder and create a new text file in it (right click->new->text thing). Name it "Includes.txt" . Be sure that you capitalize the 'I', or it won't work on linux servers. Open it up once it's made and add in "script.pas". Save it.
Now create a new file called script.pas in that directory. Just to be sure it works, run the soldat server. Somewhere in the scripting part it should say:
[*] Compiling MFS -> script.pas...
If not you did something wrong. Don't forget to set Scripting = 1 in server.ini.
Now its ready to go, open script.pas up with Notepad++, and we'll write our first script.
Part II: Writing a really simple script:This script will do this:
When a player types /asplode then kill every player in the game.
Everything will happen in OnPlayerCommand.
function OnPlayerCommand(ID: Byte; Text: string): boolean;
begin
end;
Now check if they typed /asplode:
function OnPlayerCommand(ID: Byte; Text: string): boolean;
begin
if Text = '/asplode' then begin
end;
end;
Now loop through every player and check if they are active. If so, kill them. You will need to make an integer variable to do this. It goes before the "begin" thing.
function OnPlayerCommand(ID: Byte; Text: string): boolean;
var i: integer;
begin
if Text = '/asplode' then begin
for i := 1 to 32 do begin
if GetPlayerStat(i, 'active') then Command('/kill '+inttostr(i));
end;
end;
end;
This compiles and works.
Part III: A Note on Threads:Don't use them. This includes ThreadFunc and sleep. Just don't use it, or your server will go KAPOW!
Part IV: A Timer:Now we need to improve on our last script by adding in a timer. When a player types /asplode make a timer count down from 5, then kill them all. There are several ways to do this; I will show my favorite.
To start out, make a variable to hold the current time.
var
Timer: integer;
The idea is this:
Every second the scripting thing calls AppOnIdle. We can use this to decrement Timer. Here's our script:
var
Timer: integer;
procedure AppOnIdle(Ticks: integer);
begin
end;
function OnPlayerCommand(ID: Byte; Text: string): boolean;
var i: integer;
begin
if Text = '/asplode' then begin
for i := 1 to 32 do begin
if GetPlayerStat(i, 'active') then Command('/kill '+inttostr(i));
end;
end;
end;
Now we need to make a small leap. Here is the final script.
var
Timer: integer;
procedure AppOnIdle(Ticks: integer);
var i: integer;
begin
if Timer > 0 then
Timer := Timer - 1;
if Timer = 0 then begin
for i := 1 to 32 do
if GetPlayerStat(i, 'active') then Command('/kill '+inttostr(i));
Timer := -1;
end;
end;
function OnPlayerCommand(ID: Byte; Text: string): boolean;
begin
if Text = '/asplode' then begin
Timer := 5;
end;
end;
Going through this:
When a player types /asplode Timer gets set to 5.
When AppOnIdle is next called (every second) it sees that Timer is > 0 so it takes off one.
This gets called every second until it hits 0.
If Timer hits 0 then it kills everyone and sets it to -1. When it's at -1 it won't be seen by any of the if loops in AppOnIdle, so nothing will happen.
Part V: Remembering Player Data:Now lets say you want to do this:
When a player types /save, remember their position.
When the player types /load, move them to that position.
Now the script is going to get pretty complex. Lets start by making an array of X's and Y's to hold player spots in.
var
x: array[1..32] of single;
y: array[1..32] of single;
Now in OnPlayerCommand we shall add the part that saves it:
var
x: array[1..32] of single;
y: array[1..32] of single;
function OnPlayerCommand(ID: Byte; Text: string): boolean;
begin
if Text = '/save' then GetPlayerXY(ID,x[ID],y[ID]);
end;
This saves the X and Y of player ID into x[ID] and y[ID]. Nice!
I like pascal's pass-by-reference a lot more than c++...Now we need to load the position:
var
x: array[1..32] of single;
y: array[1..32] of single;
function OnPlayerCommand(ID: Byte; Text: string): boolean;
begin
if Text = '/save' then GetPlayerXY(ID,x[ID],y[ID]);
if Text = '/load' then MovePlayer(ID,x[ID],y[ID]);
end;
This seems to work. Typing /save then moving then doing /load works. However, what happens if you load before you have saved? Try it.
The best way to fix this is to add another condition to the /load bit.
var
x: array[1..32] of single;
y: array[1..32] of single;
function OnPlayerCommand(ID: Byte; Text: string): boolean;
begin
if Text = '/save' then GetPlayerXY(ID,x[ID],y[ID]);
if (Text = '/load') and (x[ID] <> 0) then MovePlayer(ID,x[ID],y[ID]);
end;
Much better.
Just one more thing now, what happens if someone leaves the server, and someone else comes in and takes their ID spot? If the person that left did /save then the person that just joined can do /load and go to their spot :O. Heres a quick fix:
var
x: array[1..32] of single;
y: array[1..32] of single;
procedure OnJoinTeam(ID, Team: byte);
begin
x[ID] := 0;
end;
function OnPlayerCommand(ID: Byte; Text: string): boolean;
begin
if Text = '/save' then GetPlayerXY(ID,x[ID],y[ID]);
if (Text = '/load') and (x[ID] <> 0) then MovePlayer(ID,x[ID],y[ID]);
end;
Now if they try to do /load after just joining it will notice that their x is 0, and not load.
Part VI: Types:Look at that last script in part V. Notice that you have two arrays[1..32]. What happens if you want to store lots of player data, such as a boolean for each player, or maybe how many times they have said something?
var
x, y: array[1..32] of single;
bool: array[1..32] of boolean;
talktimes: array[1..32] of integer;
Thats ugly and repetitive, and there is a much better way of showing it.
Enter types.
type tPlayer = record
x, y: single;
bool: boolean;
talktimes: integer;
end;
var
Players: array[1..32] of tPlayer;
Now lets say you want to get the x of player 14. Just do:
Players[14].x
For example, here is the script from part V with a type at the beginning.
type
tPlayer = record
x,y: single;
end;
var
Players: array[1..32] of tPlayer;
procedure OnJoinTeam(ID, Team: byte);
begin
Players[ID].x := 0;
end;
function OnPlayerCommand(ID: Byte; Text: string): boolean;
begin
if Text = '/save' then GetPlayerXY(ID,Players[ID].x,Players[ID].y);
if (Text = '/load') and (Players[ID].x<> 0) then MovePlayer(ID,Players[ID].x,Players[ID].y);
end;
Part VII: Conversions:Pascal is strictly-typed, so converting between stuff is necessary very often.
Definitions:
More complete numeric ones...byte: small numbers (0-255)
integer: use it for any numbers that might go above 255.
string: 'Hello'
char: 'H'
single: floating-point number (1.23 for example)
To convert between them:
integer to string: inttostr(integer)
integer to char: chr(integer)
integer to single*: strtofloat(inttostr(integer))
string to integer: strtoint(string)
string to char: string[0]
string to single: strtofloat(string)
char to integer: ord(char)
single to integer: round(single)
single to string: floattostr(single)
* there is probably a better way but idk what it is
These should cover just about anything you need.
Part IIX: Boolean Statements:Good lord this drives me insane.
Would you do this?
if x > y = true then ...;
Lets say that x is greater than y. The computer evaluates it as "if true = true then ..." when it makes more sense for it to evaluate it as "if true then ..."
A boolean statement goes into the condition part of an if thing.Therefore, DO NOT DO THIS:
var
bool: boolean;
begin
if bool = true then ...;
end;
THAT IS UGLY AND MEAN TO ME!
if bool then ...;
IS MUCH BETTER TY!
Same with stuff like this:
var i: integer;
begin
for i := 1 to 32 do
if GetPlayerStat(i, 'active') then ...;
end;
Note: "if GetPlayerStat(i, 'active') then ...;"
I see this written as "if GetPlayerStat(i, 'active) = true then ...;" over and over again and it bugs the living shneidikkies out of me.
Please for the love of good god's goldfish, get rid of those = true's.
Note: Some of GetPlayerStat's return values are borked, be sure to make sure things are working right.
Now lets say you want to test out whether that boolean statement is NOT true.
if not bool then ...;
not is the keyword to use here. This basically takes over = false. Its easier to read as well.
I'll add more if requested... (request stuff and I'll add)