Skip to content

Chapter 1: Ignite spell (under construction)

Teekius edited this page Jun 6, 2026 · 5 revisions

Welcome to Chapter 1 of the Spellforce Spell Framework (SFSF)!

In this chapter, you will learn the basics of how to use this framework for creating your own custom spells. In simple terms, the SFSF is a dynamic link library (DLL) that provides an API necessary for interacting with game data through external programs on the run. The SFSF mods are being libraries of their own. They’re automatically recognized and loaded by the framework when the game starts. To create a new spell, you will need to compile DLL of your own using the API headers coming with the SFSF source code (this process is explained in more detail in Chapter 0).

Let's start by creating our first custom spell: "Ignite." It will work virtually the same as the Fireburst. This spell will affect a single target. It will initially deal a significant amount of damage, followed by a small amount of damage over time for a few additional seconds.

Cheat sheet

(brief step-by-step instruction)

Before the start

  1. Choose an identifier for Spell Type which wouldn't interfere with existing Spell Types
  2. Choose an identifier for Spell Job which wouldn't interfere with existing Spell Jobs

Modify GameData.cff

  1. Create a Spell Type with chosen ID (step 1)
  2. Create at least one Spell of the given Spell Type

Coding part:

  1. Include API and make initial declarations
  2. Create functions to make the mod loadable by the Spell Framework
  3. Create handler of Spell Type (inside of that should link Spell Job to Spell Index, initialize counters like ticks passed)
  4. Create handler of Spell Effect (spell game logic, must have setEffectDone function to make sure the spell will be ended correctly)
  5. Compile the mod

Before the start:

Choosing Free Spell Type and Spell Job

Spells in Spellforce consist of at least three components, which share a nuanced hierarchy. Those components are Spell Type, Spell Data, Spell Job. The former two are placed within game files. Therminology:

  • Spell Type -- ID of the type of the spell, like Fireburst, Pain, Greater Heal and so on.
  • Spell Data -- data of the specific instance of Spell Type, like Fireburst level 1.
  • Spell Job -- ID of logic behind the spell. Most of the time it is unique, but few spells share internal logic, for example all Summon-type spells, elemental waves and buff/debuff auras share logic ID behind the scene.

To make our custom spell work properly, we need to assign it unique Spell Type and Spell Job which wouldn't conflict with previously reserved values. The reserved values for Spell Type are 1 to 0xf1 (241), new Spell Type should start with 0xf2 (242). Reserved Spell Job values go from 1 to 0xa7 (167), thus new Spell Job might start with 0xa8 (168). In this Chapter we'll use numbers 0xf2 and 0xa8 for Spell Type and Spell Job of our custom spell respectively.

Adding spell to GameData.cff

In order to add new spells into game, they also should be added to game files. Game files can be edited with Game Editor (see Chapter 0 for more details). Game data is stored in /data folder of game root folder in file called GameData.cff. It's better to make backup of GameData.cff, before starting to work with that. Open Game Editor and select GameData.cff (File->Open->Select gamedata, Loadmode: Full). To implement testing spell, we should create new Spell Type first and assign number 242 to it. Spell Types are stored in "2054, Spell Type Data" section. Then, we should create a new spell and assign number 242 to its Spell Type. All possible spells are listed in "2002, Spell Data" section. Spell Job doesn't require being added to game data.

Coding part

Initial declarations

To integrate the API into your mod, you must provide API header files within the mod environment. You can include the API using a single header file named "sfsf.h," which is found in the API/ folder of the SFSF source code. This header includes all other necessary headers that grant access to different categories of game objects. However, please remember that you should include all other files (standard C++ libraries, etc.) yourself. For mods created in OS Windows the start of code will look like this:

#include <windows.h>
#include <stdio.h>
#include "../../src/api/sfsf.h"

Now we need to declare a pointer for the Spell Framework structure, as well as pointers for the function groups that we will use to access different elements within the game state.

SpellforceSpellFramework *sfsf;
SpellFunctions *spellAPI;
ToolboxFunctions *toolboxAPI;
FigureFunctions *figureAPI;
RegistrationFunctions *registrationAPI;
SFLog *logger;

Initializing functions

When the declarations are done, we have to add a few functions which will be responsible for mod initialization.

Making mod a dll

To let an OS recognize our mod as loadable library, it’s required to add the function which introduces our mod as DLL. For OS Windows environment the code will look like this:

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    switch (fdwReason)
    {
    case DLL_PROCESS_ATTACH:
        /* Code path executed when DLL is loaded into a process's address space. */
        break;

    case DLL_THREAD_ATTACH:
        /* Code path executed when a new thread is created within the process. */
        break;

    case DLL_THREAD_DETACH:
        /* Code path executed when a thread within the process has exited *cleanly*. */
        break;

    case DLL_PROCESS_DETACH:
        /* Code path executed when DLL is unloaded from a process's address space. */
        break;
    }

    return TRUE;
}

Registering the module

The next function which we add is RegisterMod. This function is automatically called by the Spell Framework when loading the mod. With this function we initialize our mod name, version, authors and description. Please, note that this and the following function must be declared as dll export. The code will look like this:

extern "C" __declspec(dllexport) SFMod *RegisterMod(SpellforceSpellFramework *framework)
{
    return framework->createModInfo("SF First Spell", "1.0.0", "S'Baad", "How-to guide to create the first custom Spellforce spell");
}

Initializing the module

When the registration is done, the Spell Framework calls the next function named InitModule (also should be declared as dll export). Within this function we pull pointers to the Spell Framework alongside with its functions, and store obtained data to pointers which we declared in the beginning.

extern "C" __declspec(dllexport) void InitModule(SpellforceSpellFramework *framework)
{
    sfsf = framework;
    spellAPI = sfsf->spellAPI;
    toolboxAPI = sfsf->toolboxAPI;
    figureAPI = sfsf->figureAPI;
    logger = sfsf->logAPI;
    registrationAPI = sfsf->registrationAPI;

Though we’ve pulled the framework, the function isn’t complete yet. We have to make our custom spell noticed within the Spell Framework. To do this, we use registration API function registerSpell and pass it Spell Type ID as an argument. Spell Type ID should be the same between code and GameData.cff. As of the date this tutorial was written, numbers 1-241 are reserved for default spells, so we should start our custom spell numeration with 242.

    SFSpell *ignite_spell = registrationAPI->registerSpell(242);

It’s worth of notice, that registerSpell kills two birds with one stone. It registers a certain spell in the Spell Framework, and then it returns the pointer to the registered spell. We’ll use this pointer very soon. We have successfully made our custom spell visible to the Spell Framework. The next step is to make it also visible to the game. To do this, we must link our custom spell with the spell handlers. Spell handlers are functions that the Spell Framework calls whenever a spell performs specific action, such as starts working, applies its effect to a creature or ceases to exist. Within this chapter we will use three main handlers. Those are Spell Type, Spell effect and Spell end handlers. Spell Type handler is called when a spell of a certain spell type was cast. It's called only once at the start of the spell.

    registrationAPI->linkTypeHandler(ignite_spell, &ignite_spelltype_handler);

Spell Effect handler applies logic of the custom spell type to game world. It contains main bulk of our spell. Spell Effect handler is repeatedly called as long as the spell remains active.

    registrationAPI->linkEffectHandler(ignite_spell, 0xa8, &ignite_spelleffect_handler); // 0xa8 is spell job, 1-a7 numbers are reserved, can use any number starting at a8

Spell End handler is called when the spell ends. Actually, this spell handler isn’t obligatory for our spell. Its functions can be imitated within the spell effect handler in scenarios when the spell effect should affect the target no more.

    registrationAPI->linkEndHandler(ignite_spell, &ignite_spellend_handler);

In the end, initialization function will look like this:

extern "C" __declspec(dllexport) void InitModule(SpellforceSpellFramework *framework)
{
    // pulling framework and storing it in our own variables
    sfsf = framework;
    spellAPI = sfsf->spellAPI;
    toolboxAPI = sfsf->toolboxAPI;
    figureAPI = sfsf->figureAPI;
    logger = sfsf->logAPI;
    registrationAPI = sfsf->registrationAPI;

  // We register the spell and get its address in memory
    SFSpell *ignite_spell = registrationAPI->registerSpell(242);
    // We initialize three main spell handlers here. Each handler corresponds to a certain phase of a spell.
    registrationAPI->linkTypeHandler(ignite_spell, &ignite_spelltype_handler);
    registrationAPI->linkEffectHandler(ignite_spell, 0xa8, &ignite_spelleffect_handler); // 0xa8 = 168 = new custom job
    registrationAPI->linkEndHandler(ignite_spell, &ignite_spellend_handler);
}

Implementing spell logic

Spell Type Handler

We should declare function for Spell Type handler. The Spell Framework will call this function, when the custom spell is casted. Unlike Spell Effect handler which is called repeatedly as long as the spell is active, this handler is called only once for a single instance of the spell.

void __thiscall ignite_spelltype_handler(SF_CGdSpell *_this, uint16_t spell_index)
{
    // here we link certain spell to its spell_job using its spell_index
    _this->active_spell_list[spell_index].spell_job = 0xa8; // spell_job defines effect logic. Spell jobs from 1 to a7 are reserved for vanilla spells, free slots start since 0xa8 (168).
    // here we make sure that our spell begins tracking its ticks starting at 0
    spellAPI->setXData(_this, spell_index, SPELL_TICK_COUNT_AUX, 0);
    // here we just put message to log for debug purpose
    // log can be viewed via in-game console
    // logging greatly affects performance, so it's better to remove it when publishing your mod
    logger->logInfo("IGNITE SPELL ACTIVATED");
}

From the code above, you might see that the handler is associated with a specific spell itself which we know by its spell_index, rather than with its Spell Type ID. When the spell is cast, we connect it to its Spell Job value, which is distinct from the Spell Type ID.

Spell End Handler

The next spell handler we deal with is spell end handler. That handler is not necessary in this case, but will play its role in later chapters.

void __thiscall ignite_spellend_handler(SF_CGdSpell *_this, uint16_t spell_index)
{
    logger->logInfo("IGNITE HAS ENDED");
    spellAPI->removeDLLNode(_this, spell_index); // this removes spell from a list of spells currently affecting the target
    spellAPI->setEffectDone(_this, spell_index, 0); // this function actually ends a spell and can be used within any other handler to mark the spell as done
}

Spell Effect Handler

Spell Effect handler is responsible for the game logic of the spell. It’s repeatedly called as long as the spell remains active.

void __thiscall ignite_spelleffect_handler(SF_CGdSpell *_this, uint16_t spell_index)
{
    logger->logInfo("IGNITE EFFECT HANDLED");
    SF_GdSpell *spell = &_this->active_spell_list[spell_index];

Before we get to applying spell effect, we have to do a considerable amount of preparatory work. To deal with it conveniently, we make a pointer to the spell which triggered the Spell Effect handler. Then, we store spell source (a spellcaster) and spell target (a hostile creature) indexes. Figure indices can be viewed in-game with debug overlay set to Figures mode.

    uint16_t target_index = spell->target.entity_index;
    uint16_t source_index = spell->source.entity_index;

We fill effect information structure with effect’s spell ID and effect’s spell job respectively. It’s important to note, once again, that spell ID isn’t the same as spell job. The former contains individual parameters of the spell. The latter contains the spell general logic. E. g. “ignite lvl 1”, “ignite lvl 2”, “ignite lvl 3” will have unique IDs and different parameters, but they all will be related to the same spell type - 242, and same spell job - 166.

    SF_SpellEffectInfo effect_info;
    effect_info.spell_id = spell->spell_id;
    effect_info.job_id = spell->spell_job;

We declare a structure for spell effect data. Spell effect data contains spell parameters: initial damage, secondary damage, amount of ticks which the spell will last, time which a single tick occupies. This data is pulled from GameData.cff. We access it with spell_id which we get via spell pointer.

   SF_CGdResourceSpell spell_data;
   spellAPI->getResourceSpellData(_this->SF_CGdResource, &spell_data, spell->spell_id);

Let’s pull some data from game files. The ignite spell is supposed to be persistent, meaning it will last for a certain duration. This duration is determined by the number of spell ticks (the moments when the spell effect is repeatedly applied to a creature) and the length of each individual tick. These values are stored in GameData.cff, with their corresponding keys (array indexes) being 2 and 3.

    uint16_t ticks_total = spell_data.params[2];
    uint16_t ticks_interval = spell_data.params[3];

image Here we declare local variable for damage dealt. We are going to write different values into it, depending on current spell phase, hence we leave it empty for now.

    uint16_t damage;

We should get current tick of the spell. In this example spell start (the moment when the initial damage is applied) corresponds to tick 0.

    uint32_t tick_current = spellAPI->getXData(_this, spell_index, SPELL_TICK_COUNT_AUX);

Let’s update the amount of ticks passed and write this data directly to the spell. addToXData adds specified amount and also returns the new value as an integer, which we could save in local variable.

    uint16_t ticks_passed = spellAPI->addToXData(_this, spell_index, SPELL_TICK_COUNT_AUX, 1) - 1;

We've got a lot of technical conditions which can prevent spell cast, so if we don't meet at least one of them, spell fails. The target must be alive, must be targetable, must be hostile, must be owned by any side on the map. Let’s check every condition and save the results into separate variable.

    uint16_t isAlive = figureAPI->isAlive(_this->SF_CGdFigure, target_index);
    uint16_t isTargetable = toolboxAPI->isTargetable(_this->SF_CGdFigureToolBox, target_index);
    uint16_t isHostile = toolboxAPI->figuresCheckHostile(_this->SF_CGdFigureToolBox, source_index, target_index);
    uint16_t isOwner = _this->SF_CGdFigure->figures[target_index].owner;

Spell target data can be accessed through various methods. We make all but the last check using functions from two different APIs that return a boolean answer to specific request. The final flag is obtained by directly accessing the spell target ownership property. We check flags from above. Target should be alive (1), targetable (1), hostile (1), isOwner returns -1 only for special NPCs or merchants, but all other categories are fine for us.

    if (isAlive != 0 && isTargetable != 0 && isHostile != 0 && isOwner != -1)
    {
        if (tick_current == 0)

We apply the spell for the first time. The spell can be resisted by a target (hostile creature). If the spell is successful, we will deal initial damage to a target and make spell last until its final tick.

        {
            uint32_t resist_chance = spellAPI->getChanceToResistSpell(_this->AutoClass34, source_index, target_index, effect_info); // we get target’s spell resistance
            uint16_t random_roll = spellAPI->getRandom(_this->OpaqueClass, 100); // we roll for random
            if (resist_chance < random_roll) // if roll was successful, the spell resistance is ignored and we move on to initial damage
            {

We start with visual effect processing. We declare structure relative_data to point coordinates for visual effect. This structure stores spell target index, and spell effect relative displacement from the creature center. Because we want our effect to be centered on creature, we put position.X and position.Y to 0 both.

                uint32_t unused;
                SF_CGdTargetData relative_data;
                relative_data.position.X = 0;
                relative_data.position.Y = 0;
                relative_data.entity_type = 1;
                relative_data.entity_index = target_index;

Aux_data is necessary only for AoE spells. It determines an area which will be filled with visual effect. Since our spell targets a single enemy, we set the corners to be both {0, 0} via some low-level coding trick.

                SF_Rectangle aux_data;
                aux_data.partA = 0;
                aux_data.partB = 0;

We apply the visual effect to the target. This effect is only applied once, so we will need to call this function separately for the subsequent spell ticks.

                spellAPI->addVisualEffect(_this, spell_index, kGdEffectSpellHitTarget, &unused, &relative_data, _this->OpaqueClass->current_step, 10, &aux_data);

We deal the initial damage to the target. To do this, we pull the initial damage value to our local variable from Game Data. The initial damage is stored in spell parameters under index 0. Then we deal damage with figure toolbox API (notice, that some values of units are affected with figure API, and another are affected with figure toolbox API). Then, we send command to creature to become aggroed.

                damage = spell_data.params[0];
                toolboxAPI->dealDamage(_this->SF_CGdFigureToolBox, source_index, target_index, damage, 1, 0, 0);
                // we make hostile creature aggro after being damaged
                spellAPI->figureAggro(_this, spell_index, target_index);
            }
            else

The spell was resisted. We apply visual effect which corresponds to spell being resisted, the target notices hostile attempt and becomes aggroed.

            {
                spellAPI->figureAggro(_this, spell_index, target_index);
                uint32_t unused;
                SF_CGdTargetData relative_data;
                relative_data.position.X = 0;
                relative_data.position.Y = 0;
                relative_data.entity_type = 1;
                relative_data.entity_index = target_index;
                SF_Rectangle aux_data;
                aux_data.partA = 0;
                aux_data.partB = 0;
                spellAPI->addVisualEffect(_this, spell_index, kGdEffectSpellTargetResisted,
                                          &unused, &relative_data, _this->OpaqueClass->current_step, 10, &aux_data);

We stop the spell with setEffectDone. It triggers Spell End handler, which clears the rest of data related to terminated spell. This part is highly optional, since SFSF provides the same handler by default for each spell. But we would need it in following chapter, so let's have it now.

                spellAPI->setEffectDone(_this, spell_index, 0);
             }

Since we've done all the logic for initial tick we can leave handler for now.

            return;
        }
        else

The current tick is above than 0, and the spell wasn’t stopped. It means we’ve got to phase when persistent damage should be applied.

        {
           //Spell ticks passed are less than total ticks specified by GameData. The target keeps burning.
            if (ticks_passed <= ticks_total)
            {
                uint32_t unused;
                SF_CGdTargetData relative_data;
                relative_data.position.X = 0;
                relative_data.position.Y = 0;
                relative_data.entity_type = 1;
                relative_data.entity_index = target_index;
                SF_Rectangle aux_data;
                aux_data.partA = 0;
                aux_data.partB = 0;
                spellAPI->addVisualEffect(_this, spell_index, kGdEffectSpellDOTHitTarget,
                                          &unused, &relative_data, _this->OpaqueClass->current_step, 10, &aux_data);

We pull persistent damage from spell data. The persistent damage uses index 1, and it’s lesser than the initial damage.

                damage = spell_data.params[1];

The spell has tick interval in milliseconds. We don’t want Spell Effect handler to trigger each game tick, so we tell the game to not execute this Spell Effect handler for certain amount of internal ticks (internal ticks aren’t the same as the spell ticks, they’re many shorter).

                _this->active_spell_list[spell_index].to_do_count = (uint16_t)((ticks_interval * 10) / 1000);
                toolboxAPI->dealDamage(_this->SF_CGdFigureToolBox, source_index, target_index, damage, 1, 0, 0);
            }
            else
            {

The spell has worked for specified amount of ticks plus one tick for applying initial damage, it should stop now.

                // Last Param for spell effect done should always be 0
                spellAPI->setEffectDone(_this, spell_index, 0);
            }
        }
    }
}

Those are key functions necessary for custom spell to work. The mod is ready to be compiled. The compiled mod should be put in "/sfsf" folder within game root folder, and it will be ready for testing. The spell will deal some damage at start, and then damage enemy over the time for some duration. The exact numbers depend on what you set with Game Data Editor for the spell.

Full source code for this example could be downloaded from here.

Clone this wiki locally