A Development Tutorial: Chapter 12

From FOnline: Reloaded Wiki
Jump to navigation Jump to search
A Development Tutorial: Chapter 12
Get the developer tools and try building new content for this game!
Season All Seasons
Status Progress Stopped
Completion 75%
Authors Slowhand
This Chapter 12: Understanding Quest Scripts
More Chapters Title Page
Development Kit Setup
Making Maps
The World Map Editor
NPC Dialogues
Create a 2-Map Zone
Monsters, Loot, and Scripts
Tracking Quest Progress
Scenery Scripts
Dialogue-to-Quest Location
Chapter 10
The Scripting Environment
Understanding Quest Scripts
Chapter 13
Dialogue: The "SAY" Menu
Chapter 15
Lockpick Cooldown
Repeatable Locations
Floating FA Text
Roulette Game
Dialogues vs Scripts
Simple Kill Quest
Chapter 21
Mysterious Stranger Perk
Perk Installation
Black Jack Game
Black Jack Installation
Sound Effects
Pro Tips
Notes {{{notes}}}


Introduction to Understanding a Full Quest Script

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 programming head down to1 3. Navigation in the source code for FOnline development specific info.

Compilation

  • The program itself, the executable 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.
      • Usually 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 wherever they are available.
      • They have a scope, which will determine where the variable is usable. (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 "crit.name = "John"; to access the variable. Of course 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 behavior of these entities.
      • 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 occurred 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 function, 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 example: "#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:

/**< 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)
            dogzones.insertLast(zones[i]);
    /**< If no zones with dogs in it found, exit without spawning location.*/
    if(dogzones.length() == 0)
        return;

    /**< 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)
        return;

    /**< 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);
    if(player.Mode[MODE_DEFAULT_COMBAT] == COMBAT_MODE_TURN_BASED)
        SetTurnBasedAvailability(location);
    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].SetScript(null);
        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;
    while(!spawned)
    {
        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. */
            if(valid(doggie))
            {
                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);
    DeleteLocation(var.GetValue());
}

/**< Critters need an initialization function to be functional, well, this is it. */
void critter_init(Critter& cr, bool firstTime)
{
    /**< Disables replication for killed dogs. */
    cr.StatBase[ST_REPLICATION_TIME] = REPLICATION_DELETE;
    /**< 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)
{
    MobIdle(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);
}

Summary

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 difficulty 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.