General > Features & Articles

Basic Development Tutorial for FOnline.

<< < (3/7) > >>

Coming soonish..

I did not solve this yet, but it seems that map_bosbunker.fos has some nice example, how to make the script for a map, so that the player has to attach a rope to an elevator shaft and climb down on it.

The map for it is, q_bos_oldbunker_level2.fomap, here the elevator shaft does not have a rope attached, but the script "map_bosbunker@s_Shaft" is set up.

11. Setting up the scripting environment.

Scripting from notepad after second day, is no fun. Before touching more of the scripting topics, our first task is to set up a scripting environment, which helps and speeds up our work. We will configure an IDE (Interactive Development Environment) for this, which wil replace the text editor we used to write and read scripts. There are many freee IDE's out there, I tried a few, and here are the results:

* Notepad++:

* This is not an IDE but an advanced text editor. (not the only or best one, but most popular)
* Can be set up for highlighting and functions list.
* I assumed that code completion would not work, so I did not bother with it much, we skip this one.
* Codelite:

* This is a lightweight IDE, which would have been perfect for us, because scripting this game does not require complex IDE's.
* Highlighting worked, fast and easy.
* Code completion did want to become fully functional. If a header file was named .fos, the code completion did not use it.
* Codeblocks:

* This is a more complicated IDE than Codelight, and I only choose it over Codelite because all featured required worked.
* Except compiling because I did not want to go into that, maybe some other time.
* Microsoft Visual Studio 2008:

* The intellisense file as I saw was originally made for this.
* I did not try to set it up, but some other devs did, so it must work. You can give it a try if you want.
Step by step - Configuring Code Blocks for scripting FOnline: Reloaded

* Download Codeblocks
* Set up Codeblocks:

* When the program starts at first time, it will ask for a compiler of your choise. Just choose the one that is on top.
* Set syntax highlighting: Settings->Editor..->Syntax highlighting->File masks - add "*.fos" to the list. (Make sure u add to C++)
* Set code completion: Settings->Editor..->C/C++ parser (adv) - add "fos" to both header and source files.
* Create new project:

* File->New->Project.. - Select Empty Project.
* Enter title (name of the project) and for location select a temporary location. (not the server\scrips folder)
* Select a compiler: Scroll to bottom of the list and select: No compiler.
* Add a new source file:

* File->New->File..->Empty File->Go Select file name and path, for testing leave it in default folder and name it "test.fos"
* Add the file to active projects and into one of the build types and press finish.
* Type into the empty file: "Cri" and press Ctrl+Space. Nothing should happen.
* Add more files to your project:

* Click your project, Add files.. and find your intellisense file: "Server\scripts\Solution\intellisense.h"
* Add another file. This time let it be "Sever\scrips\_vals.fos".
* Test the setup:

* In test.fos press Ctrl+Space again while having the cursor at the end of "Cri". This time a helping popup window should appear, giving an option to select "Critter".
* In test.fos in a new line type: "SPE" and press Ctrl+Space. It should bring up a list of some SPECIAL_XX defines.
* If this happened so far, we are done, the code-completion works.
* Also, whenever editing ".fos" files, the code highlighting/coloring should be in effect, you can check this by seeing some of the text in different color.
* Few more hints:

* When writing scripts, keep the project files out of "Server\script", but make the new script files in that folder.
* To build the script file, use the compile.bat as before.
* Add missing keywords:

* Settings->Editor->Syntax highlighting->Keywords
* Add missing keyword at the end of the list, using space for separator. (f.e: " uint")
* This is how I use it:

* Create a new project, save the project files anywhere.
* Add the intellisense file.
* Add files recursively from the "server\scripts" folder, but not all of them. Use Wildcard select and type "*.fos".
* All scripts should be loaded and fully functional. Use this to go throw scripts instead of notepad.
* Compile from command line, with compile.bat.
This is how my IDE looks like when scripting:

12. Understanding a full quest script. (LA Boneyard dog quest)

Before writing scripts, understanding a few of them could come handy. The repeatable Boneyard dogs quest is perfect for this. It includes new location spawning from script, adding mobs to it and some triggers to run when the quest objective is fulfilled.

I will try to go very detailed on this one, pointing at every feature, or good to know stuff. This might be a bit too basic level, but maybe someone needs it. Having a little bit of C/C++ or any programming experience helps a lot here, but I will assume that you have none. This will be a very long trip, if you get lost, just skip, practice a bit using the previous or next material and come back later.

Very basic general (with examples of  programming info for those who are not proficient with it, or tried to understand the scripts but failed. If you are proficient in proramming head down to1 3. Navigation in the source code for FOnline development specific info.

* Compilation:

* The program itself, the executionble code (*.exe) is a code in machine language. This means it's not readable to humans. The code that is written in (human) programming languages is not readable by the machine. To solve this problem, we use compilers, which translate the human readable code (so called source code) into computer readable code (machine code). The compiler we will use to translate scripts, is "Server\scripts\ASCompiler.exe", however we will use the compile.bat, because it is preconfigured with parameters. To compile a script, just copy the script in the folder of ASCompiler.exe and compile.bat and type in console: "compile.bat scriptName.fos".
* Source code:

* Our source code files have two categories:

* Header files (definition files):

* Contain the structure and contents of the elements and their signature.
* Ususally files with *.h or *_h.fos extension.
* If you check the "Server\script\solution\intellisense.h" file, it contains a lot of one or two lines, because this is just the contents and prototype of the elements.
* Source files (logic):

* Contain the logic by which the program will behave when run.
* Usually files with *.fos extension.
* Uses header files to understand the structure/prototype of other source files, if it is dependent on it.
* Elements:

* Variables:

* These elements as their suggest can take different values at different times of the runtime.
* They have a type, which describes the range of values the variable can take.
* They have a name, which will identify them whereever they are available.
* They have a scope, which will determine where the variable is useable. (If it is existent and accessible at a part of a code)
* Defined with a keyword like "int" and followed by a variable name like "counter".
* Scope depends on where the variable is declared and how. This is one of the important meaning of header files. If a source file includes a header file, then it can use the variables defined there. If the compiler does not find the definition of the variables, functions, it will look for other source files that are in the project and that include the header file.
* Built in types:

* These types are known to the compiler by default.
* Usually numeric type and literals. (int, uint, string, etc)
* Custom types:

* Definition of structures (classes) falls mostly in this category. In the scripts we will not create structures, but we will use some. The FOnline API has some, but we can consider them as given, or built in types.
* For example: Critter, Map, Item, etc, these are all custom types (classes) defined in the FOnline API. If you check the "Server\scripts\solution\Intellisense.h" file you will see the declaration (how it looks like), or better said structure of them (beware, they are not complete).
* Classes/Structures are the encapsulation of more variables and functions into a type.
* To refer to a variable of a class do like: instantiate the class (make an example of that type) "Critter crit;" then use " = "John"; to access the variable. Ofc this class "Critter" has to exist, and has to have a public variable called "name" which can accept string values.
* To call a function of a class, use something like: Critter.SetName("John");
* If the Critter.SetName(string vname) would be defined like: void SetName(string vname) { name = vname }; then both of my examples above would have the same result.
* You might ask the meaning of this, there is a lot, but it's not part of the tutorial to explain Objected Oriented Programming principles.
* Functions:

* Function, were mentioned before at classes, are a logic behind the programs, they are used to boss around the variables to a state we want.
* Most of the time we will define functions, which we will link from the Mapper or Dialog editor to a Critter or other FOnline specific type. These will define the behaviour of these entitites.
* Functions can have parameters and return values as well.
* The return value of a function is a variable type, with this you can use the result of a function.
* Sometimes return values are simple built in variables for the purpose of showing if an error occured or not.
* If the function used it's return type for showing errors, then the parameters can be used to store the changes.
* There are two type of parameters:

* "In" parameters, meaning that the state of these parameters is not interesting for us after the end of the function.
* "Out" parameters, meaning that the state of these parameters is important for us after the end of the funciton, as these will be used after and their state should be saved.
* Macros/Defines:

* Macros are a code part which will be replace by another code part before the compiler processes the source.
* A typical example for Macros are Defines, (which are not called macros because they are too simple for that) usually one literal string, written in capitals followed by a value.
* Define example: "#define MAX_COUNT 10.". Now everywhere in the code, where logically one would write the number 10 because that part of code has to do something with maximum count of something, should write MAX_COUNT instead. This is usefull also because it can be changed later much easier, code is more understandable, etc.
* Macro exampe: "#define max #(a, b) (a < b ? b : a)" , this would replace every "max(a, b)" found in code to "(a < b ? b : a)" which returns the higher number from a and b.
* Navigation in the source code:

* Using Codeblocks on a custom variable type or function:

* Click on (set cursor on) a custom type, variable or function and wait a little hovering withthe mouse above it. A hint will appear showing the declaration of the variable.
* "Ctrl+Space" on a variable, will show the signature of a function.
* "Ctrl+." on a function to jump to it's definition. Another file will be opened if the definition is not in the same file.
* Browsing the FOnline API:

* Go to the "Server\scripts\reference" folder.
* Open Critter.txt (I used TotalComander viewer, just press F3).
* Set the text encoding to russian: Encoding->Cyrilic windows.
* Search for "SetScript". Select and copy the commented lines above it (lines between: "/*" and "*/" and lines starting with //)
* Open google translate in your browser and paste in. Change translation: "Russian" -  "English" and translate.
* You should have more info about how the specific function works. This is all the help the Russians left us, but it should be enough. Unfortunately not everything is there.
* Making sure the API does what is written in the docs:

* The API is not up to date, or is faulty. Some parts are missing, some variables have different type.
* The "code\scripts\solution\intellisense.h" contains a more accurate signature list of the functions the API has.
* In case of doubt, decompile the Angelscript compiler for signatures:

* Download a disassembler and decompile "server\ASCompiler.exe"
* I used Hopper disassembler, because it has free tryouts.
* Download, Launch, Try Demo: File->Read Executable->Open "Server\scripts\ASCompiler.exe".
* Click on Strings and search for the desired function, like: "CountEntire"
* It will give you two lines for results, both of them should have the same signature, one brief, the other more detailed: "uint CountEntire(int entire) const"
* If you check the signature of CountEntire(..) in the API documentation ("Server\scripts\references\map.txt") then you will see that the parameter type does not match.
* In cases like this, the decompiled one is the right one.
Here is the script file, with included comments to explain. Copy it and open with Codeblocks.

--- Code: ---/**< Credits and description */
// FOnline: 2238
// Rotators
// quest_la_dogs.fos

/**< These are includes. They tell the compiler where to look after code not defined in this file. */
#include "quest_killmobs_h.fos"
#include "worldmap_h.fos"
#include "utils_h.fos"
#include "npc_planes_h.fos"
#include "entire.fos"

/**< Same as include, I don't know why this is separated from the other includes. This line includes only one function, not all of them. */
import uint GetZonesWithFlag(uint flag, array<IZone@>@ zones) from "worldmap";

/**< Definitions to help to understand and use the code easier. */
#define ALL_KILLED            (2)

#define IDLE_NORMAL           (3000)
#define DIALOG                (9471)
#define PID                   (82)
// the time after dog group is added to the zone it's been removed from
#define GROUP_RESPAWN_TIME    (REAL_HOUR(Random(20, 30)))

// check if there is some group of dog roaming on the worldmap near LA
/**< Checks the map for roaming dogs. It's use has be shortcut to always return true, that is probably to optimize?. Useless currently.*/
bool d_CheckDogs(Critter& player, Critter@ npc)
    return true;
    /*IZone@[] zones;
       uint num = GetZonesWithFlag(FLAG_LA_Surroundings, zones);
       for(uint i = 0; i < num; i++)
            if(zones[i].GetBaseQuantity(GROUP_Dog) > 0) return true;
       return false;*/

/**< Opposite of checkdogs, always returns false. Useless currently. */
bool d_NoDogs(Critter& player, Critter@ npc)
    return !d_CheckDogs(player, npc);

/**< Spawns the location, sets up the critters, events, timers, initializes everything */
void r_SpawnLoc(Critter& player, Critter@ npc)
    /**< Get collection of zones near LA Boneyard. */
    array<IZone@> zones;
    uint num = GetZonesWithFlag(FLAG_LA_Surroundings, zones);
    array<IZone@> dogzones;
    /**< Cycle through the zones around LA Boneyard and add the zones that contain dogs to another collection called dogzones */
    for(uint i = 0; i < num; i++)
        if(zones[i].GetBaseQuantity(GROUP_Dog) > 0)
    /**< If no zones with dogs in it found, exit without spawning location.*/
    if(dogzones.length() == 0)

    /**< Get a random zone from the dogzones. */
    IZone@ zone = random_from_array(dogzones);

    /**< Generate random World Map coordinates. */
    uint   wx = zone.GetX() * __GlobalMapZoneLength;
    uint   wy = zone.GetY() * __GlobalMapZoneLength;
    wx += Random(0, __GlobalMapZoneLength);
    wy += Random(0, __GlobalMapZoneLength);

    /**< Select a random location on the World Map zone. (square)*/
    array<uint16> pids;
    num = zone.GetLocationPids(pids);
    uint16        locPid = pids[Random(0, num - 1)];

    /**< Create the location, add the player */
    Critter@[] crits = { player };
    int           loc = CreateLocation(locPid, wx, wy, crits);
    if(loc == 0)

    /**< Make the location visible for the player */
    player.SetKnownLoc(true, loc);

    /**< Get the game variable q_la_dogs_locid and store the location id associated with the players quest. */
    GameVar@  locidv = GetLocalVar(LVAR_q_la_dogs_locid, player.Id);

    /**< GameVar is a handle to the _la_dogs_locid game variable, it's value will be stored there when this function terminates. */
    locidv = loc;

    /**< Get the location, allow turn based mode in it, and disable auto garbage, this way the player can revisit the map. */
    Location@ location = GetLocation(loc);
    location.AutoGarbage = false;

    /**< Get the maps of the location and initialize them with default values. */
    array<Map@> maps;
    uint        mapcount = location.GetMaps(maps);
    for(uint c = 0; c < mapcount; c++)
        maps[c].SetEvent(MAP_EVENT_IN_CRITTER, null);
        maps[c].SetEvent(MAP_EVENT_CRITTER_DEAD, null);

    /**< Set the player to be the owner of the first map of the location. */
    Map@ map = GetLocation(loc).GetMapByIndex(0);
    SetOwnerId(map, player.Id);
    /**< Repeat until dogs are spawned successfully on the map. */
    // spawn dogz
    bool spawned = false;
        array<Entire> entires;
        ParseEntires(map, entires, 0);

        /**< Get a random Entire to spawn the player to, when he enters the map. */
        Entire@ ent = random_from_array(entires);

        /**< Get a hex position at a random angle and distance from the player to spawn the dogs to. */
        uint16 hx = ent.HexX;
        uint16 hy = ent.HexY;
        map.GetHexCoord(ent.HexX, ent.HexY, hx, hy, Random(0, 359), Random(10, 40));
        for(uint i = 0, j = Random(7, 12); i < j; i++)
            int[] params =
                ST_DIALOG_ID, DIALOG

            /**< Creates a dog and adds it to the map. Assignes critter_init function to dogs, explained below, at definition.*/
            Critter@ doggie = map.AddNpc(PID, hx, hy, Random(0, 5), params, null, "quest_la_dogs@critter_init");

            /**< If creating and adding at least one dog to the map succeded, then the main cycle stops. */
                spawned = true;

    /**< Makes sure the location is salvaged after 12 hours, if the player does not finish quest until then.
        It will also set q_la_dogs variable to 3 (need to check dialog tree, probably reseting quest) */
    SetQuestGarbager(12 * 60, player.Id, loc, LVAR_q_la_dogs, 3);

/**< Deletes the location the player killed the dogs at. */
void r_DeleteLoc(Critter& player, Critter@ npc)
    GameVar@ var = GetLocalVar(LVAR_q_la_dogs_locid, player.Id);

/**< Critters need an initialization function to be functional, well, this is it. */
void critter_init(Critter& cr, bool firstTime)
    /**< Disables replication for killed dogs. */
    /**< A bunch of event handler: each will set the function to be called when the event happens. */
    cr.SetEvent(CRITTER_EVENT_DEAD, "_DogDead");
    cr.SetEvent(CRITTER_EVENT_ATTACKED, "_DogAttacked");
    cr.SetEvent(CRITTER_EVENT_MESSAGE, "_DogOnMessage");
    cr.SetEvent(CRITTER_EVENT_IDLE, "_DogIdle");
    cr.SetEvent(CRITTER_EVENT_SHOW_CRITTER, "_DogShowCritter");

    /**< I don't understand what this does, if someone finds out, please tell me as well. */
    _CritSetExtMode(cr, MODE_EXT_MOB);

/**< Function to run when mob is idle. It move from time to time, but only a few hexes.*/
void _DogIdle(Critter& mob)

/**< This is called when a new critter is in sight of the dog. */
void _DogShowCritter(Critter& mob, Critter& showCrit)
    /**< If the seen creature is not the same type, it will attack it. */
    MobShowCritter(mob, showCrit);

/**< Checks if all dogs are dead on the map. */
void _DogDead(Critter& cr, Critter@ killer)
    uint16[] pids = { cr.GetProtoId() };
    Map@ map = cr.GetMap();
    if(MobsDead(map, pids))
        GameVar@ var = GetLocalVar(LVAR_q_la_dogs, GetOwnerId(map));
        var = ALL_KILLED;

        /**< These parts have been severed by someone, it seems that some useless code remained. */
        // remove one dog group from given zone
        IZone@ zone = GetZone(cr.WorldX, cr.WorldY);
        // zone.ChangeQuantity(GROUP_Dog, -1);
        // spawn event to restore the doggie
        uint[] values = { cr.WorldX, cr.WorldY };
        CreateTimeEvent(AFTER(GROUP_RESPAWN_TIME), "e_SpawnDogGroup", values, true);

/**< Useless code, it was used for extra reality, but someone changed the core part. */
uint e_SpawnDogGroup(array<uint>@ values)
    IZone@ zone = GetZone(values[0], values[1]);
    // zone.ChangeQuantity(GROUP_Dog, 1);
    return 0;

/**< How dog handles when it is attacked. It will send a message on the map to everyone that he is attacked including itself. (Yeah, I know, lol) */
bool _DogAttacked(Critter& cr, Critter& attacker)
    return MobAttacked(cr, attacker);

/**< If the message is that a dog is attacked, it will attack the attacker. */
void _DogOnMessage(Critter& cr, Critter& fromCr, int message, int value)
    MobOnMessage(cr, fromCr, message, value);

--- End code ---

I have described how to look after specification from the FOnline API and how to find definitions using codeblocks. Use these to navigate throw the code I copied it. It will contain most of the important stuff in comments.

Also, try to make as much as you can out on your own, the code seems to be correct, except at the parts I commented otherwise. I left in the old comments as well, the new ones start like this: "/**<".

The biggest dificulty this script and others presents is that many things are passes through an ID, a numeric value to identify and entity, or message type, etc. Once you get used to it, and you should if you want to write scripts, understanding scripts will go fast and easy. :)

13. A simple kill target critter quest.

14. Using the "Say" menu from dialogues. (Riddle mini quest.)

It is quite simple actually, all you need to do, is at a dialogue you created, link the function to call at the dialogue, instead of results or demands like before. The signature will change to the following: uint dlg_Name(Critter& player, Critter@ npc, string@ say).

When the client accesses the node which you linked the say-script, it will run the script immediately, will not wait for the client to press "Say", so the first line of the code needs to be something that checks if the user entered something using say. (check code example for it)

The return value of the script function will tell which dialogue node to access next. 0 stands for the same dialogue node, running the script again, -1 stands for exiting the dialogue, and different positive values will try to find and access a dialogue node with that number. In our example, that was the 4.

Here is the code, using the previous tutorials, try to install it, do not forget to add the script to the script lists, the dialogue you created to the dialogue scripts and linking the NPC dialog ID to the ID's you gave to your dialogue at _dialogs.fos.

Script file: tut_say_dialogue.fos
Dialogue file: tut_say_dialogue.fodlg

This is how my dialogue file looks like, showing where to add/edit the script functions called (left click on node 3):


[0] Message Index

[#] Next page

[*] Previous page

Go to full version