Modding Guides

Script3 Basic Guide

Written by: MrKosjak

Gonna say it, i’m not perfect at making guides or help topics… So i’m sorry for some terrible writing or making dumb logic.

This is a basic help on how to use script3 (ChaiScript).

First of all, scripts files are stored in GAME_INSTALL_PATH\Scripts with file extension .chai (f.e example.chai).

Create a .txt file, then rename it and change extension to .chai

And you’re kind of ready to make some stuff inside of it. I recommend using Notepad++ for it.

To run a script open cheat box and type chai yourfilename

Here is a simple example on logging a text (LOG FILE LOCATED AT %USERNAME$\.Populous\Poptb.log):

log("Test");

This code will be executed once upon loading script.

var a = 5;
log(a.to_string());

Will print 5.

var a;
var b;
a = 5;
b = 1 + a;
log(b.to_string());

Will print 6.

But how do I make a loop?

def OnTurn()
{
log("Test");
}

OnTurn is basically main’s game script loop (can be used as main()), it executes code every game turn inside of it. (12 times per second)

Will print Test 12 times per second. WARNING: May cause lag if you overuse log().

def OnTurn()
{
if (EVERY_2POW_TURNS(2))
{
log("Test");
}
}

This code will be executed every 4 game turns. 2^2 = 4. Power of exponents. -EVERY_2POW_TURNS(N) – where N is exponent. So, EVERY_2POW_TURNS(10) will be every 1024 game turns.

Will print Test 3 times per second. 12/4 = 3.

def OnTurn()
{var a = G_RANDOM(5);
if (EVERY_2POW_TURNS(2))
{
log(a.to_string());
}
}

A simple example of function G_RANDOM(N).

Let’s try to create our first thing.

global create_only_one = false;
def OnTurn()
{
if (!create_only_one)
{
create_only_one = true;
var pos = Coord3D();
createThing(T_PERSON, M_PERSON_BRAVE, TRIBE_BLUE, pos, false, false);
}
}

Let me explain:

global create_only_one = false; – We define a global variable that can be accessed anywhere in the code.

if (!create_only_one) – Checks if variable create_only_one is false.

create_only_one = true; – Setting variable to true, so previous check will never meet it’s condition.

var pos = Coord3D(); – We define a variable with game’s coords struct, it’s required for most of functions/things. Coord3D() has three attributes: Xpos, Zpos, Ypos. Ypos is height. On initial they’re all zero.So, if you wanted to modify it’s coords:

var pos = Coord3D();pos.Xpos = 1024;pos.Zpos = 4096;pos.Ypos = 128; – height.

createThing(T_PERSON, M_PERSON_BRAVE, TRIBE_BLUE, pos, false, false); – Here we create a blue’s brave at 0,0 position, since we didn’t modify pos variable and by default it’s 0,0.

Now in more details: createThing(type, model, owner, coord, local, ghost);

type – Is thing’s type. T_PERSON – all person types – wildman, brave, warrior, priest, firewarrior, shaman, angel of death.
model – Is thing’s model. It’s a member of thing type, for example, if you use T_PERSON, you’d use M_PERSON_ .
owner – Is thing’s owner, TRIBE_BLUE would belong to blue. You can use numbers also: 0 – Blue, 1 – Red, 2 – Yellow, 3 – Green, 4 – HostBot.
coord – Is where thing spawns. For this argument we must have a Coord3D variable, like we did in that code (var pos = Coord3D()).
local – Ignore for now, use a boolean ‘false’ if you don’t know what is this.
ghost – Ignore for now, use a boolean ‘false’ if you don’t know what is this.

Now, what if I wanted to do something with this newly created thing?

You’d have to assign this function to a variable to store thing’s data.

global create_only_one = false;

def OnTurn()
{
if (!create_only_one)
{
create_only_one = true;
var pos = Coord3D();
var my_thing := createThing(T_PERSON, M_PERSON_BRAVE, TRIBE_BLUE, pos, false, false);
set_person_new_state(my_thing, S_PERSON_ON_FIRE);
}
}

Now the created blue brave will be on fire and eventually die. (Similar to walking on lava)

set_person_new_state(thing, state) – Set’s thing a new state. If we didn’t assign our person to a var, we wouldn’t be able to set him a new state.

Let’s make some mercy to our brave and actually set some fancy state instead.

global create_only_one = false;

def OnTurn()
{
if (!create_only_one)
{
create_only_one = true;
var pos = Coord3D();
var my_thing := createThing(T_PERSON, M_PERSON_BRAVE, TRIBE_BLUE, pos, false, false);
set_person_new_state(my_thing, S_PERSON_VICTORY_DANCE);
}
}

But it makes them not selectable at all.

We can quickly fix it by disabling his one flag. TF2_PERSON_NOT_SELECTABLE

But before we do it, check if it’s actually not selectable.

global create_only_one = false;

def OnTurn()
{
if (!create_only_one)
{
create_only_one = true;
var pos = Coord3D();
var my_thing := createThing(T_PERSON, M_PERSON_BRAVE, TRIBE_BLUE, pos, false, false);
set_person_new_state(my_thing, S_PERSON_VICTORY_DANCE);

if (is_person_selectable(my_thing, 0) == 0)
{
disableFlag(my_thing.Flags2, TF2_PERSON_NOT_SELECTABLE);
}
}
}

is_person_selectable(my_thing, allow_ghosts);
my_thing – Is thing we’re checking. In our case my_thing.
allow_ghosts – Allowing ghosts to be checked if set to 1, 0 otherwise.

Also, instead of is_person_selectable you could use IsFlagEnabled(my_thing.Flags2, TF2_PERSON_NOT_SELECTABLE).

global create_only_one = false;

def OnTurn()
{
if (!create_only_one)
{
create_only_one = true;
var pos = Coord3D();
var my_thing := createThing(T_PERSON, M_PERSON_BRAVE, TRIBE_BLUE, pos, false, false);
set_person_new_state(my_thing, S_PERSON_VICTORY_DANCE);

if (isFlagEnabled(my_thing.Flags2, TF2_PERSON_NOT_SELECTABLE))
{
disableFlag(my_thing.Flags2, TF2_PERSON_NOT_SELECTABLE);
}
}
}

Let’s restructure our code a bit.

global create_only_one = false;
global init = true;
global my_thing;def OnTurn

{
if (init)
{
if (!create_only_one)
{
create_only_one = true;
var pos = Coord3D();
my_thing := createThing(T_PERSON, M_PERSON_BRAVE, TRIBE_BLUE, pos, false, false);
set_person_new_state(my_thing, S_PERSON_VICTORY_DANCE);

if (isFlagEnabled(my_thing.Flags2, TF2_PERSON_NOT_SELECTABLE))
{
disableFlag(my_thing.Flags2, TF2_PERSON_NOT_SELECTABLE);
}
}


var check = GetThing(my_thing.ThingNum);
if (!check.is_var_null())
{
log("Not dead!");
}
else
{
create_only_one = false;
}
}
}

What it does now? Script will check our existing thing, if it’s dead, it’ll create another one.

global my_thing; – I made it global so i could access it anywhere in the code and do what ever with it.

var check = GetThing(my_thing.ThingNum); – Here we get data of our thing into a local variable. ThingNum there is required. GetThing(ThingNum)

if (!check.is_var_null()) – Check if thing is not null (dead). This is mandatory, always check if it’s not null before doing anything with it, else game will terminate.

Script3 has also ability to save/load your data. For this we will use two other hooks called OnSave() (deinit) and OnLoad() (reinit).

global notify_once = 0;
global notify_on_load = 0;

def OnLoad(StringVector sv, IntVector iv)
{
var vectSize = iv.size()
if (vectSize > 0)
{
notify_once = iv[0];
notify_on_load = 1;
}
}

def OnSave(StringVector sv, IntVector iv)
{
iv.push_back(notify_once);
}

def OnTurn()
{
if (notify_once == 0)
{
notify_user("Notify me once!");
notify_once = 1;
}

if (notify_on_load == 1)
{
notify_user("Successfully loaded!");
notify_on_load = 0;
}
}

Here i made two global variables and only notify_once is saved, notify_on_load will be set to 1 every time you load the saved game.

OnTurn checks if notify_on_load is equal to 1, if it’s true it’ll set back to 0, so the message on load will be shown again.

iv.push_back(notify_once) – Adds this variable at the end of the vector. So OnLoad you’d use notify_once = iv[0]; Since it’s very first item in the vector.

var vectSize = iv.size() – We set vector’s size in a variable to check if it at least has one element.

Keep in mind, saved and loaded items MUST be in order.

Let’s move onto next hook called OnCreateThing(). As you can already see by it’s name, it will execute code when there’s a thing created.

def OnCreateThing(Thing)
{
if (Thing.Type == T_PERSON)
{
notify_user("There's a person thing spawned with model: " + Thing.Model.to_string());
}
}

Will display a message every time there’s a person type thing created.

We can do something funny with it, why not apply bloodlust and magic shield to all created persons, excluding wilds and angels?

Effects will last for a random amount of time.

Script3 supports a basic drawing of things such as circles, rectangles, text, etc…

These will be drawn in OnFrame() hook.

def OnFrame()
{
LbDraw_Text(150, 150, objectCounts().to_string().c_str(), TbColour());
}

Will draw current amount of objects at 150,150 position with color index 0. Though, without setting a font it’ll don’t use color and also font will be always changing.

To prevent that, we can use SetFont(); before out draw text function. This also will make it use a color from palette depending on index. 0-255.

def OnFrame()
{
SetFont(font(1));
LbDraw_Text(150, 150, objectCounts().to_string().c_str(), TbColour(128));
}

Here’s other draw functions example.

def OnFrame()
{
LbDraw_Circle(200,200,30,COLOUR(CLR_BLUE));
LbDraw_CircleOutline(200,200,30,COLOUR(CLR_GREEN));

LbDraw_CircleFilled(300,300,30,COLOUR(CLR_WHITE));

LbDraw_HorizontalLine(300,400,64, TbColour(5));
LbDraw_VerticalLine(300,400,64, TbColour(15));

LbDraw_Line(12,12,500,500, TbColour(227));


//This one is very laggy. Draws a square of noise.
for (var i = 0; i < 64; ++i)
{
for (var k = 0; k < 64; ++k)
{
if (G_RANDOM(20) == 1)
{
LbDraw_Pixel(k+100,i+50,TbColour(G_RANDOM(255)));
}
}
}

LbDraw_PropText(10,400, objectCounts().to_string().c_str(), COLOUR(CLR_PINK));

var rectangle = TbRect(400,100,450,150)

LbDraw_Rectangle(rectangle, COLOUR(CLR_BLACK));
LbDraw_RectangleOutline(rectangle, COLOUR(CLR_WHITE));
}

Try playing around with these yourself, you might find it a way more useful usage than me.

Even pixel art could be a possibility with these, i’m more than sure.

Oh yeah, i know what you gonna try to draw.

And the last hook for this guide is OnKeyDown(var);

Let’s say you wanted to disable specific loop/function in your script?

global init = true;

def OnTurn()
{
if (init)
{
for (var i = 0; i < 4; ++i)
{
var check_s = getShaman(i);
if (!check_s.is_var_null())
{
createThing(T_EFFECT,M_EFFECT_SMOKE,TRIBE_HOSTBOT,check_s.Pos.D3,false,false);
}
}
}
}

def OnKeyDown(key)
{
// First check if it's not multiplayer game, else it'll desync (NOT TESTED YET)
if (!isFlagEnabled(gnsi.Flags, GNS_NETWORK))
{
if (key == LB_KEY_V)
{
if (init)
{
init = false;
}
else
{
init = true;
}
}
}
}

Now if you press V it’ll change init’s value (true or false). If init is true, it’ll always spawn smoke stuff on everyone’s shamans.

As always, before actually spawning it we check if shaman exists first.

You can make it do literally anything, a very dependant on your creativity and skills, which i don’t have of course.

Well, i think this concludes the basic guide and introduction to script3 in PopTB 1.5.

Hope it at least somehow helped you to understand it… Maybe… Not?… ¯\_(ツ)_/¯

You can also look at other scripts and see how they work.

This link to script 3 spreadsheet contains pretty much most of it’s functions and other useful stuff.

source