ModAPI Tutorial: Classes, Simulator, Event listeners, Schedules and Update scripts

Before starting with this, please read the previous tutorial. Also, you are expected to have a minimum knowledge of how C++ classes and functions work. This utorial requires the ModAPI SDK, version 2.3.1 or higher.

In this tutorial we will cover more advanced topics, useful things that give more flexibility to your mod projects: how to use classes to condense your scripts in one place, how to use the Simulator system, how event listeners work to detect user input, and executing functions every frame.

Finally, in this tutorial we will cover the basics of how to create a script class, which we will name DrivableCarsScript. It will only act when playing adventures, and this is what we it will do: when the plaer presses ‘Z’ for a second next to a vehicle, you will be able to control that vehicle with the mouse. Press Z again and the vehicle will stop.

The first part of the tutorial will be a quick overview of the main features offered to developers. But before getting into that, one thing must be explained: classes.

Classes

I’m not going to explain how C++ classes work, but how they are used within the ModAPI; so, before continuing, you are expected to know how classes work and how to create them. In the first tutorial we saw how to create a cheat; for that, we only used the dllmain.cpp file. But it is obvious that it cannot be always like that; otherwise, for big mods, you would end up with a file of thousands of lines!

What we saw with the cheat is the lambda version of creating a cheat. It is handy because it can be used in the same place where the cheat is added, it does not require additional files and it only takes a few lines of code. But actually, what that lambda function does is create a cheat class, and use the function there. That is the first thing we will do: separate the cheat into its own class.

A cheat class inherits from the type ArgScript::ICommand class. You can do that manually, or you can do Right click -> Add -> New Item.. in your project and use the “ModAPI Console Cheat” item, so that the class is automatically created for you. At the end, you will end up with a class declaration like this:

class HomePlanetCheat : public ArgScript::ICommand
{
public:
  // All the class contents are here
};

When inheriting from a class, it’s interesting to take a look to all its virtual methods (you can see them in the documentation), because those are the methods that we can override and replace in our class to give it our custom functionality. In this case, we are interested in these two methods, which we will override in our class declaration:

virtual void ParseLine(const ArgScript::Line& line) override;
virtual const char* GetDescription(ArgScript::DescriptionMode mode) const override;

Now, we can go to the class .cpp file and implement those methods just as we did in the first tutorial.

Now there’s one last thing to do, which is adding the cheat; that is done in the initialization function in dllmain.cpp, like before. Before doing it, don’ forget we will need to include the .h file of the class we just created.

App::CheatManager()->AddCheat("myCheat", new MyCheatClass());

So, summarizing: to give functionality to your class, you must inherit a Spore class (usually prefixed with I, for interface) and override the methods you need. This technique applies for the other features we will explain in this tutorial. Also, remember that C++ supports inheriting multiple classes at the same time, so can make your class behave as an ICommand, IWinProc, IRenderable,… simultaneously.


Objects, casts and memory management

If you have ever studied C++ pointers, probably you have been told they are sort of “dangerous”: the memory you allocate for them must be deleted manually, and keeping track of that can lead to memory leaks and problems of the sort. Since Spore relies heavily on pointers, it provides a solution for that: reference counting.

I’m not going to explain the details of reference counting, just know the following: if you wrap a pointer to a reference counted class with an intrusive_ptr, its memory will be managed automatically: you won’t have to worry about deleting the pointer (you might need to include EASTL/intrusive_ptr.h). For example:

intrusive_ptr<Window> window = new Window();

Now you might be wondering how to create a reference counted class. The answer is the Object class. Actually, Object is an abstract interface; if you don’t want to implement the reference counting methods yourself (which, believe me, you don’t), you can inherit from the DefaultObject class instead. Most Spore classes inherit from Object or are reference counted, so it is recommendable to use intrusive_ptr if possible.

But that is not the only good thing the Object class has: it also has a system of dynamic casting. Imagine you receive, from a function, a pointer to an Object* class. You don’t know what class it really is, but if it is a cCombatant, you want to execute a certain code for it. You can do it with object_cast. Notice how you have to use get() to get the raw pointer from the intrusive_ptr:

intrusive_ptr<Object> object = GetMyObject();
// If the cast can be done, it will return a cCombatant*, otherwise returns nullptr
auto combatant = object_cast<cCombatant>(object.get());
if (combatant) {
    // Since you have casted to cCombatant, you can use its methods
    combatant->SetTarget(nullptr);
}

Implementing an Object

If you use multiple classes like we discussed earlier, you will need something more than just inheriting from DefaultObject. You will need to override it’s methods as well, but don’t worry; you won’t have to implement them. Furthermore, in order to properly support object casting, we will add a TYPE field:

static const uint32_t TYPE = id("MyClassName");

virtual int AddRef() override;
virtual int Release() override;
virtual void* Cast(uint32_t) const override;

The code for these methods will just call the default implementation.

int MyClassName::AddRef() {
    return DefaultObject::AddRef();
}
int MyClassName::Release() {
    return DefaultObject::Release();
}

The cast method does have some more body, but still it’s quite simple. You just have to check for all the classes you inherit. For example, imagine this class inherits fom IWinProc:

void* MyClassName::Cast(uint32_t type) const {
    if (type == TYPE) return this;
    else if (type == IWinProc::TYPE) return (IWinProc*)this;
    else return DefaultObject::Cast(type);
}

Simulator: the gameplay engine

Before going any further into the ModAPI systems, we need to mention what he Simulator is. Long story short: it’s the gameplay engine used by Spore. Everything you see on the game stages or the adventure mode is controlled by the Simulator; therefore, it is logical that we explain some general features of this system.

Most operations carried out require including the Spore\Simulator.h header file, although some other might require including other specific headers. In the code examples provided, the Simulator namespace is not explicitly specified; remember you can achieve that by typing using namespace Simulator; into your code.

Game Nouns

If you have ever tried videogame development, you may have heard the concept of entity. Spore follows a similar concept: every object in the game (creatures, vehicles, cities, etc) is a game noun (we will also refer to it as entity sometimes). All game noun classes inherit from the Simulator::cGameData class.

As you can guess, there are multiple types of game nouns. Each one of them is identified by a unique ID; you can find all of them in the Simulator::GameNounIDs enum.

Getting all the instances of a noun type

Something that is used very often is getting all the instances of a certain noun type. For example, getting all the vehicle entities that are in the game at the moment. That can be done with the Simulator::GetData() function.

As an example, we will use an existing mod. Imagine we want to make a cheat that makes all creatures in the planet play an animation. First, we would need to include the Spore\Simulator\cCreatureAnimal.h header. Then we can use the following code:

auto creatures = GetData<cCreatureAnimal>();
for (auto creature : creatures) {
    // 0x04FFA018 is the animation where the creature tells a joke
    creature->PlayAnimation(0x04FFA018);
}

GetData returns an unmodifiable vector, so you can use creatures.size(), creatures.empty(), creatures[0].

If you looked at the documentation you might have noticed that not all game noun IDs have an equivalent ModAPI class. In these cases, we need to use a differnet version of GetData that returns a cGameData vector and in which you have to specify the noun type ID. Instead of specifying the noun class (because it does not exist) we can specify one of its base classes (if you are not sure, just use cGameData). For example, if we wanted to get the creature citizens:

// This returns a vector of cCreatureBase objects
auto creatures = GetData<cCreatureBase>(kCreatureCitizen);
for (auto creature : creatures) {
    creature->PlayAnimation(0x04FFA018);
}

Creating and deleting game nouns

Adding objects to the game is actually pretty easy. You just have to create them with simulator_new; once created, the object will already display on the scene (if it can be displayed). It is recommendable that you wrap it into an intrusive pointer, for greater safety:

intrusive_ptr<cVehicle> vehicle = simulator_new<cVehicle>();

Now we have a vehicle, yes, but what vehicle? We must tell the game which creation it uses:

// You must specify the VehicleLocomotion and VehiclePurpose
vehicle->Load(kVehicleLand, kVehicleMilitary, ResourceKey(0x19A3A9AC, TypeIDs::vcl, GroupIDs::VehicleModels));

// For creatures, you don't use simulator_new.
// You must also specify the initial position
auto species = Editors::SpeciesManager()->GetSpeciesProfile(ResourceKey(0x066B8241, TypeIDs::crt, GroupIDs::CreatureModels));
intrusive_ptr<cCreatureAnimal> creature = cCreatureAnimal::Create(Vector3(500.0, 0, 0), species);

// For the rest of cSpatialObjects, use SetModelKey
rock->SetModelKey(ResourceKey(id("EP1_sg_rare_fossils_04"), TypeIDs::prop, GroupIDs::CivicObjects));

As you can see, creatures are not created as other objects. When it comes to vehicles, you must specify the VehicleLocomotion and the VehiclePurpose.

Just as before, we might have the unfortunate case that we want to create a game noun whose class is not yet supported by the ModAPI. For that, we will use the cGameNounManager::CreateInstance() method (that’s what simulator_new does behind the scenes):

intrusive_ptr<cGameData> object = GameNounManager()->CreateInstance(kRaidEvent);

For deleting an object we will use cGameNounManager::DestroyInstance(). It is important that you delete the objects you create when you are done with them.

GameNounManager()->DestroyInstance(object);

Relevant Simulator classes

Game nouns are made up by inheriting multiple “components”, which give them a specific behaviour. Some interesting components are:

  • cSpatialObject: Any object that can be displayed visually and can be placed in a 3d space. They have a model, position, rotation and scale, among other properties.
  • cLocomotiveObject: This represents those spatial objects that can also move. It has properties such as velocity, speed,…
  • cCombatant: For those entities that have health points and can combat.

Most of the Simulator behaviour is controlled by its sub systems, with the relevant ones being:

  • cGameNounManager: As shown before, used to create, destroy and get game nouns.
  • cGameInputManager: Can be used to receive input from the user (know what keys are pressed or the position of the mouse).
  • cToolManager: Can load space tools and even add your own tool strategies code.
  • cStarManager: Used to get empires, stars and planets.
  • cPlanetModel: Among other things, manages positions on the planet.

Event (message) listeners

Alright, we know how to make things with the ModAPI, but how do we get them to execute? Until now, we had only seen cheats, but the ModAPI can do more than that. In this section, we will talk about message listeners, which are divided in two categories: App and UI.

App Messages

Spore defines a “messaging” system which is used to communicate between different parts of the code. Long story short, it is possible to send messages (sometimes referred to as events) from one part of the code, and then there are listeners which will receive that message and execute code accordingly.

For example: when you select a skinpaint in the creature editor, that does not directly change the skinpaint of the creature; instead, the user interface code just sends a message that says “hey, the user selected this skinpaint” (obviously not like that). Then the Editor class, which is listening to that kind of messages, will receive it and change the skinpaint. There has been a communication between different parts of the code that don’t need to acknowledge each other.

For this system, the App::IMessageListener and App::IMessageManager classes are relevant; unfortunately, there are only a few message types documented, so for now it’s not really useful.

User Interface Events

The user interface also has a messaging system, although it works differently than the App one. Here, whenever a message (or event) happens, it is propagated from the source window that originated it through all its window hiearchy. Furthermore, it is possible to register the so called window procedures, which will get executed when messages on a window happen.

This is really useful, because it allows us to receive other types of user inputs instead of just relying on cheats. With this, we can execute code when the user presses a key, moves the mouse,… by adding a window procedure to the main window of the game.

Similar to cheats, there are two ways of adding window procedures: with the lambda or with the class version. We will explain both. First, keep in mind that all the UI-related things are in the UTFWin namespace, so you might want to do the “using namespace” thing again. You also have to include Spore\UserInterface.h.

The first thing we need to do is have an IWindow to which we will add the window procedure. This tutorial will not explain how UI works, so for now we will just work with the main window of the game, which we can get with IWindowManager::GetMainWindow():

auto window = WindowManager()->GetMainWindow()

If we have a class inheriting IWinProc (which we will explain how later), we can add it to the window with IWindow::AddWinProc(). If your window procedure is small and you don’t want to create a class for it, you can use the lambda version:

window->AddWinProcFilter([](IWindow* pWindow, const Message& message) {
    // This code will be executed when the mouse is moved. 
    // We can access different parameters depending on the message type.
    float x = message.Mouse.mouseX;
},
{ UTFWin::kMsgMouseMove });

The available message types are in the MessageType enum. For small procedures the lambda version is okay, but once our code gets more complicated it might need to access other data we might have set before. So for that, creating a class that inherits IWinProc will be useful (as before, the item “ModAPI Window Procedure” might be helpful). There are two methods that we need to implement:

class MyWinProc : public IWinProc
{
public:
    virtual int GetEventFlags() const override;
    virtual bool HandleUIMessage(IWindow* pWindow, const Message& message) override;
}

In GetEventFlags() we must return a combination of flags defined in the EventFlags enum. As you can see in the documentation, each one of it allows listening to certain types of messages, so depending on what your procedure will do you need to use a different set of flags. This is the most common one:

// Note how we use the bitwise or | operator to combine flags.
return UTFWin::kEventFlagBasicInput | UTFWin::kEventFlagAdvanced;

The code of our procedure goes into HandleUIMessage(). This function receives two parameters: the first one, the window that called the method (as the same IWinProc can be listening to multiple windows at once); the second parameter contains information about the event, which you can check in the documentation. Usually, the first thing the handler code does is check what the message type is; more specialized handlers might also want to check which window generated the event. The return value of the method is actually quite relevant: if it returns true, the event will be considered as handled and therefore it will stop propagating.

bool MyWinProc::HandleUIMessage(IWindow* pWindow, const Message& message) {
    // Did the user release the G key? 
    // Note we can only access .Key if the message is of key type
    if (message.IsType(kMsgKeyUp) && message.Key.vkey == 'G') {
        // Do something here
        // We handled the message, stop propagating it
        return true;
    }
    // If we arrive here, the message wasn't for us
    return false;
}

Update scripts

We have seen how to get input from the user (cheats, keys, mouse), but some complex scripts just need to be executed always, regardless of the user. This is when Update functions come to play: they are executed every frame. Most of the logic in game modes happens in update methods. To use Update functions, you need to include Spore\Messaging.h

Similar to cheats and event listeners, we have two approaches for this: either with lambdas or by inheriting the App::IUpdatable class. Both use the App::AddUpdateFunction() method. The lambda version just requires a void function with no arguments to be specified. In the class version, we will need to do the following:

class UpdateScript
    : public App::IUpdatable
    , public DefaultObject
{
public:
    virtual int AddRef() override;
    virtual int Release() override;
    virtual void* Cast(uint32_t) const override;

    virtual void Update() override;
};

As you have seen, we’ve had to override the AddRef(), Release() and Cast() methods. This is because the updatable interface inherits from Object, but those methods are not implemented. You can implement them as we explained before.


Measuring time and scheduled tasks

The ModAPI provides three ways of measuring time:

Clock

The Clock class is the standard way of measuring time. Basically, you store it as a variable in your class, and in your constructor you specify the units (seconds, milliseconds, etc):

myClock = Clock(Clock::Mode::Seconds);
myClock.Start();

Then in our methods we will be able to know the measured time:

// If 2 seconds or more happened before we started the clock
if (myClock.GetElapsed() >= 2.0f) ...

cGonzagoTimer

There is a Simulator version of clock, the Simulator::cGonzagoTimer class. The advantage of using that one is that when the game is paused the clock will pause as well. The disadvantage is that it can only provide the elapsed time in milliseconds.

Scheduled Tasks

The ModAPI allows you to schedule a task, so that a function is executed. There are two versions available, that work under the same concept: one for the App and the other one for a Simulator (uses a cGonzagoTimer, therefore does not count time when the game is paused). Like with Update functions, to use these tasks you need to include Spore\Messaging.h.

Each version (App and Simulator) provide two methods for scheduled tasks:

  • ScheduleTask(): Schedules a function to be executed only once, after X seconds pass.
  • SheduleRepeatedTask(): Executes a function after X seconds pass, and then repeats it periodically after Y seconds.

We can use the lambda version, which takes a no-parameters function like with Update functions. Similar to other features, this returns an object that can later be used to remove this schedule. This example will execute the method only once, after 2.5 seconds:

intrusive_ptr<ScheduledTaskListener> task = Simulator::ScheduleTask([]() {
    GameNounManager()->GetAvatar()->PlayAnimation(0x04FFA018);
}, 2.5f);

// If we want to cancel, it, remove task
Simulator::RemoveScheduledTask(task);

There is also a version for class methods; in this example, we are reusing the UpdateScript class we created earlier, in which we have added a no-parameters void method called PlayAnimation:

void UpdateScript::PlayAnimation() {
    GameNounManager()->GetAvatar()->PlayAnimation(0x04FFA018);
}

// Somewhere else in our code, execute after 3 seconds and repeat every 5 seconds
// Instead of 'this' you can use any pointer to UpdateScript
Simulator::ScheduleRepeatedTask(this, &UpdateScript::PlayAnimation, 3.0f, 5.0f)



Applying all the knowledge: the DrivableCarScript

Promises made, promises kept. After explaining all these ModAPI features, we are going to condense all this new knowledge into a small yet interesting example.

So, this is the plan: we are going to make a script that allows the user to “start” driving a vehicle in an adventure when he presses Z next to it for a second. To be more precise, our class will need the following:

  • IWinProc: We will use this to detect the key presses and releases of the user.
  • IUpdatable: The movement code will have to be executed every frame, if conditions are met.
  • We will also use scheduled tasks, as we want the user to maintain the key pressed for a certain amount of time.

Mind you, this is only one of the possible designs. A more clever design would use gonzago timers instead of scheduled tasks, and could even display some UI when the user gets close to the car; but for the sake of example, we will stick to this simple approach.

The first thing we will do is creating our script class. If you looked at the item templates the ModAPI provides you might think the logical thing is to use the ModAPI Object one, but I won’t lie you: that is unupdated, and will not work correctly. So instead, we will just create it by hand. So, create the .h and .cpp files, which we will call DrivableCarScript.

In the header file, we will have to include Spore\UserInterface.h, Spore\Messaging.h and Spore\Simulator\cVehicle.h. We will be using the App and UTFWin namespaces. We will have to inherit from the classes we specified before:

class DrivableCarScript
    : public DefaultObject
    , public IUpdatable
    , public IWinProc

You also inherit from DefaultObject to help us implementing the object functionality. You will have to override its methods, like we explained before.

There are multiple methods and members we will have to add to get the desired functionality. First, let’s start with the data members. We will use a boolean to tell whether the user is currently driving a vehicle or not; of course, we will also keep a reference to that vehicle. Another boolean will also be required to know if the user is still pressing Z (so he has to press it again to leave the vehicle). Finally, we will keep a reference to the scheduled task we create so we can remove it later.

private:
    // The vehicle we might be driving
    intrusive_ptr<cVehicle> mVehicle;

    // True if the user is "inside" the vehicle, false otherwise
    bool mIsDriving;

    // We keep this to remove the task if the user releases the key
    intrusive_ptr<Simulator::ScheduledTaskListener> mTask;

    // Helps us detect whether the user is still pressing the key
    bool mIsPressing;

Now, time to start with the methods. Don’t forget that we declare them in the header file, and define their code in the .cpp. As to access specifiers, only the overrided methods need to be public, the rest can be private. The first thing we will do is implement the constructor so it initializes all the variables we have declared:

DrivableCarScript::DrivableCarScript()
    : DefaultObject()
    , mVehicle(nullptr)
    , mIsDriving(false)
    , mTask(nullptr)

So, the first method we will define will helps us detect whether the user is playing an adventure or not. In order to do this, we will have to include Spore\GameModes.h and Spore\App\ScenarioMode.h.

bool DrivableCarScript::IsPlayingAdventure() {
    return IsScenarioMode() && 
        ScenarioMode::Get()->GetMode() == ScenarioMode::Mode::PlayMode;
}

The next method will be the one used in our scheduled tasks. If the task is executed, it will switch the (not) driving state and stop the vehicle movement if necessary, using cLocomotiveObject::StopMovement(). Since the task has been executed and we will not use it anymore (every time we use it we generate a new one) we set it to nullptr.

void DrivableCarScript::ToggleDriving() {
    mIsDriving = !mIsDriving;
    mTask = nullptr;

    if (!mIsDriving && mVehicle) mVehicle->StopMovement();
}

Next thing, we will create a method called FindVehicle() that returns a cVehicle*, which will be used to get the candidate car the user might be driving. To do so, we will iterate through all the vehicles in the scene and get the one closest to the player. Finally, if the distance is smaller than some threshold value (e.g. the vehicle is 5 meters away or less from the player) we will return that specific vehicle:

auto playerPos = GameNounManager()->GetAvatar()->GetPosition();
auto vehicles = GetData<cVehicle>();

float closestDistance = 100000.0f;  // Just start with something big
cVehicle* closestVehicle = nullptr;

for (auto v : vehicles) {
    float distance = abs((v->GetPosition() - playerPos).length());
    if (distance < closestDistance) {
        closestDistance = distance;
        // v is an intruisve_ptr, you can get the pointer itself with .get()
        closestVehicle = v.get();
    }
}

// Only accept if player is 5 meters or less from the car
if (closestDistance <= 5.0f) return closestVehicle;
else return nullptr;

Now, to the UI handling. As we explained before, since we only need to detect key presses, we can just return UTFWin::kEventFlagBasicInput in the GetEventFlags() method. Now it is time for HandleUIMessage(). Our strategy will be the following: when the user starts pressing Z, we will add the scheduled task; if we detect a release before the task has been executed, remove the task. To keep the code more clean and organized, we will split this in two methods, one for key presses and another one for key releases:

void DrivableCarScript::OnKeyPress() {
    if (mIsDriving) {
        // Since he was driving and pressed Z, this task will stop the vehicle
        mTask = Simulator::ScheduleTask(
            this, &DrivableCarScript::ToggleDriving, 1.0f);
    }
    else {
        mVehicle = FindVehicle();
        // Only do the task if there is an available vehicle
        if (mVehicle) {
            mTask = Simulator::ScheduleTask(
                this, &DrivableCarScript::ToggleDriving, 1.0f);
        }
    }
}
void DrivableCarScript::OnKeyRelease() { 
    // The user released the key, so cancel the task if it was too soon 
    if (mTask && !mTask->HasExecuted()) { 
        Simulator::RemoveScheduledTask(mTask); 
        mTask = nullptr; 
        mVehicle = nullptr; 
        mIsDriving = false; 
    } 
}

In the HandleUIMessage we just have to detect the key we wanted (if the user is playing an adventure) and execute one of the methods we created. We have to be careful, though: the kMsgKeyDown event is sent every frame while the key is pressed; if we don’t do any control on that, the user might maintain the key pressed for 2 seconds and therefore start and stop the vehicle. We will use the mIsPressing variable we declared in our class to avoid that:

bool DrivableCarScript::HandleUIMessage(IWindow* pWindow, 
         const Message& message) 
{
    if (IsPlayingAdventure()) 
    {
        // Check for mIsPressing to ensure this is called only once during the key press
        if (!mIsPressing && message.IsType(kMsgKeyDown) 
               && message.Key.vkey == 'Z')
        {
            OnKeyPress();
            mIsPressing = true;
        }
        else if (message.IsType(kMsgKeyUp) && message.Key.vkey == 'Z')
        {
            OnKeyPress();
m           mIsPressing = false;
        }
    }

    // If we arrive here, the message wasn't for us
    return false;
}

And finally, the driving code. This will be implemented in the Update method. It is actually quite simple: if the player is in an adventure and we are in the driving state, move the vehicle. If it is not in an adventure anymore, reset our class variables to ensure nothing is bugged.

Since vehicles inherit from cLocomotiveObject, we can use its MoveTo() method. This requires a destination position, we can use cGameViewManager::GetWorldMousePosition() for that.

void DrivableCarScript::Update() {
    if (IsPlayingAdventure()) {
        if (mIsDriving) {
            mVehicle->MoveTo(
                GameViewManager()->GetWorldMousePosition());
        }
    }
    // If we have exited play mode, stop driving
    else {
        mVehicle = nullptr;
        mIsDriving = false;
        mTask = nullptr;
    }
}

We are almost done! Now we have to go to our initialization function in dllmain.cpp. There, we will create an instance of our new class and add it both as a window procedure and as an update function. But it won’t be that plain simple, however: when the initialization function is called, the window manager does not exist yet! This is when one of those message listeners we talked about earlier will be useful: we will listen to the kMsgAppInitialized message and then add our script.

App::MessageManager()->AddListener([](uint32_t messageID, void* data)
{
    intrusive_ptr<DrivableCarScript> script = new DrivableCarScript();
    WindowManager()->GetMainWindow()->AddWinProc(script.get());
    App::AddUpdateFunction(script.get());

    // Message listeners require a return value, but it is irrelevant
    return false;
}, { App::kMsgAppInitialized });

And now yes, compile and test your mod!

Tutorial finished! It wasn’t that hard, right? With the things you have learned in his guide, you know most of the tools that can unleash the ModAPI’s potential. The only thing left would be detourings, but that’s material for a whole new tutorial…

Finally, if there’s something you didn’t understand, jut ask in this post, in Discrod or wherever you can find me. I will also leave here the source code for this script, so you can check if you did everything right:

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Blog at WordPress.com.

Up ↑

%d bloggers like this: