Skip to content

Chapter 3: Interference (under construction)

Teekius edited this page Jun 6, 2026 · 81 revisions

In this chapter we'll learn how to create an Interference spell. The Interference will protect a target from damage directly done with spells during its duration. To simulate this behavior, we will introduce the new type of spell handler - Damage Deal handler. The logic of Interference is supposed to overlap effects of vanilla spells such as Patronize and Shelter. We'll show how to rewrite specific sections of vanilla spells to ensure their mutual compatibility with the Interference.

Cheatsheet

1. Before you start

  • You should create a new Spell Type for the Interference spell in GameData.cff.
  • You should create at least one spell using the new Spell Type.
  • You should link the new spell to an existing scroll or create the new spell scroll for this.

2. Deal damage handler

  • Reduce damage for all occurrences with is_spell_damage being true.
  • Return the full damage for all other cases.

3. Implementing refresh handler

  • Check for all three spells over a target.
  • Make the handler return false, if at least one of spells over a target blocks the spell which triggered the refresh handler.
  • Remove all other spells, except for the spell which triggered the refresh handler.
  • Make the handler return true.

4. Alternating vanilla spells

  • Rewrite spell duration logic for the Shelter.

  • Trigger the refresh handler within the tick 0.

  • Rewrite spell duration logic for the Patronize

  • Trigger the refresh handler for the spellcaster within the tick 0.

  • Make an iterator to scan targets in area.

  • Sort targets in area according to their priority

  • Trigger the refresh handler for correct targets in area.

  • Extend the same spell with addSpellToFigure to valid targets.

  • Make a function to remove spell from all affected targets on its expiration.

5. Refactoring the Shieldwall into a single spell

  • Create spell duration logic for the Shieldwall group.
  • Change the refresh handler to check for the single Spell Type.
  • Extend the same spell with addSpellToFigure to valid targets instead of adding a new spell to them.
  • Make a function to remove spell from all affected targets on its expiration.

Before you start

The Interference is a protective spell. It reduces the damage coming from other spells by a percentage. It lasts for certain duration which we specify within spell parameters. The Interference can be refreshed with recast (we renew the duration with removing the previous instance of a spell). Because the Interference provides the powerful defensive effect, it will be incompatible with the Patronize and the Shelter vanilla spells which provide bonus to the target's resistance to spells. When the Interference is casted, it removes any instances of the Patronize or the Shelter.

We will change the refresh behavior of the Patronize and the Shelter spells as well. The Patronize will refresh itself on recast, and will get cancelled with other two spells. The Shelter will refresh itself, and clear any instances of the Patronize on the target. In its turn, it will get cancelled by the Interference when it's casted.

Before you start working on your code, we recommend that you complete the following steps:

  • You should create a new Spell Type for the Interference spell in GameData.cff.

  • You should create at least one spell using the new Spell Type.

  • You should link the new spell to an existing scroll or create the new spell scroll for this.

  • You should write down Spell Jobs of interfering vanilla spells (the Shelter and the Patronize) as macros (existing Spell Jobs are listed here).

You should create new spell matching the reserved Spell Type. It would use two parameters: duration (milliseconds) and resistance (percentage).

image

Code part

Initialization

The mod initialization remains similar to how it was done in the previous chapters. We declare macros for the custom spell's Spell Type and Spell Job. We also should declare macros for interfering vanilla spells such as the Shelter and the Patronize. We don't need to declare macros for respective Spell Types, we should do this only for Spell Jobs. Vanilla Spell Types can be accessed with kGdSpellLine<Spell Type Name> enum. For given spells enumerations will look like kGdSpellLineAbilityShelter and kGdSpellLineAbilityPatronize respectively.

No such option exists for Spell Job yet. The Spell Job values can be obtained from internal API files here (internal/registry/sf_spelleffect_registry.cpp).

#define PATRONIZE_JOB 0x6b // 107
#define SHELTER_JOB 0x6f   // 111

You should register the Spell Type, Spell Effect, Spell Refresh and Spell End handlers. Notice, that we will use the same refresh handler for custom spell and for vanilla spells interfering with it.

Also, we will use new type of handler: Deal Damage handler. Unlike other handlers, it's not called on spell cast. It will be triggered whenever the creature(s) which is affected with the custom spell is damaged. It gets the damage and allows us to modify it. Then we should return the modified damage, and the game engine will handle the new value as usual. The Deal Damage handler is declared in the following way:

cpp registrationAPI->linkDealDamageHandler(interference_spell, &interference_deal_damage_handler, SpellDamagePhase::PRE);

There is a new argument SpellDamagePhase. This argument is a flag which determines at what moment the Deal Damage Handler will snap up the damage to a creature. The framework provides three possible phases. PRE stands for the initial damage before it was modified in any way. default stands for the damage after it was changed by other spells. post stands for the final damage value after it was changed by other spells, creature's innate resistances and creature's armor.

The Interference provides the similar effect as the Patronize and the Shelter spells from vanilla game. We have to prevent custom and vanilla effects from stacking. The Patronize and the Shelter blind towards the Interference effect by default. We can alternate them by replacing some of their handlers like the Spell Refresh handler. It's worth of noting that ideally it would be possible to alternate the spells by simply replacing their refresh handlers. However, due to the bug in original code we can't do it that simple. We have to modify their Spell Effect handlers as well to make the custom refresh handlers working.

The Spellforce framework will trigger the replacements instead of original handlers whenever the triggering events occur. You don't have to replace other handlers like the Spell Type handler. They will continue to work as usual, we shouldn't touch them.

    SFSpell *patronize_spell = registrationAPI->registerSpell(kGdSpellLineAbilityPatronize);
    registrationAPI->linkEffectHandler(patronize_spell, PATRONIZE_JOB, &patronize_effect_handler);
    registrationAPI->linkRefreshHandler(patronize_spell, &interference_patronize_shelter_refresh_handler);

    SFSpell *shelter_spell = registrationAPI->registerSpell(kGdSpellLineAbilityShelter);
    registrationAPI->linkEffectHandler(shelter_spell, SHELTER_JOB, &shelter_effect_handler);
    registrationAPI->linkRefreshHandler(shelter_spell, &interference_patronize_shelter_refresh_handler);

You also should declare the Spell Type handler for the Interference. There is nothing unusual in it. You should link the spell with Spell Job, and initialize the starting tick.

void __thiscall interference_type_handler(SF_CGdSpell *_this, uint16_t spell_index)
{
    _this->active_spell_list[spell_index].spell_job = INTERFERENCE_JOB;
    spellAPI->setXData(_this, spell_index, SPELL_TICK_COUNT_AUX, 0);
}

It's also important to properly implenet Spell End handler. Because the spell is going to use some flags in order to get access to Deal Damage handler, those flags should be cleared from a unit when the spell ends.

Spell End handler would look like this:

void __thiscall interference_end_handler(SF_CGdSpell *_this, uint16_t spell_index)
{
    spellAPI->figTryClrCHkSPlBfrJob2(_this, spell_index);
    spellAPI->figClrChkSplBfrChkBattle(_this, spell_index, 0); 
    spellAPI->removeDLLNode(_this, spell_index);    // we remove spell from the list of active spells affecting the target
    spellAPI->setEffectDone(_this, spell_index, 0); // we end a spell
}

Spell effect

We are used that the spell logic is implemented within the Spell Effect handler. However, it's going to be other way for the Interference. Within Spell Effect handler we would check only whether the spell can be applied (trigger the Refresh handler) and activate its duration.

The tick 1 will:

  • Refresh the spell (remove the previous instance of interference, effectively renewing the spell duration; remove the vanilla spells Patronize and Shelter).
  • Add visual effect on activation
  • Raise the flag F_CHECK_SPELLS_BEFORE_JOB to activate the Deal Damage handler for the figure affected with the spell.
  • Raise the flag F_CHECK_SPELLS_BEFORE_BATTLE
  • Disable the spell effect from being triggered until specified amount of time passes.

The tick 2 will:

  • Clear flags over a figure affected with the spell for the sake of optimization.
  • Finish the spell and remove it from the list of the active spells.

The tick 1

First of all, we obtain spell index which is necessary for all further checks and obtain its spellcaster index.

    SF_GdSpell *spell = &_this->active_spell_list[spell_index];
    uint16_t source_index = _this->active_spell_list[spell_index].source.entity_index;

Then we initialize the tick counter. We can done it in the following manner:

    uint32_t current_tick = spellAPI->addToXData(_this, spell_index, SPELL_TICK_COUNT_AUX, 1);

The neat thing of this approach is that we don't need to get spell XData, we automatically record it after increasing current spell tick.

It will make tick 1 starting tick, and tick 2 ending tick.

Within the tick 1 let's initiate the refresh. You remember that it's called with the function checkCanApply() which might return 1 or 0. For the Interference it will always return 1, because spell is supposed to remove older instances and renew its duration when it's recasted.

    if (current_tick == 1)
    {
        if (spellAPI->checkCanApply(_this, spell_index))
        {

If the spell has been successfully 'initialized', we can pull its parameters from GameData.cff.

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

Let's add the visual effect for the spell.

            SF_CGdTargetData relative_data;
            relative_data.position = {0, 0};
            relative_data.entity_type = 1;
            relative_data.entity_index = source_index;
            uint32_t unused;

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

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

Then here comes important part. We should add the flag to the figure which would make it possible to trigger the Deal Damage handler when the figure is damaged.

_this->SF_CGdFigure->figures[source_index].flags |= F_CHECK_SPELLS_BEFORE_JOB;

We also should add the flag F_CHECK_SPELL_BEFORE_BATTLE to the spell for the optimization purposes. This flag is added to the spell itself instead of figure.

_this->active_spell_list[spell_index].flags |= 2;

With that everything necessary in the first tick is done. We should pull the spell duration from spell data struct. After we did this, let's disable the Spell Effect handler for a certain amount of in-game ticks to prevent it from being triggered until the spell duration passes.

            uint16_t ticks_interval = spell_data.params[0];
            _this->active_spell_list[spell_index].to_do_count = (uint16_t)((ticks_interval * 10) / 1000);
            return;
        }
    }

The tick 2

The tick 2 will be even shorter than this:

    spellAPI->figTryClrCHkSPlBfrJob2(_this, spell_index);
    spellAPI->figClrChkSplBfrChkBattle(_this, spell_index, 0);
    spellAPI->setEffectDone(_this, spell_index, 0);

We don't even have to check what tick currently is, we can assume that if that's not tick 1, then it's tick 2.

During tick 2 we have to correctly stop the spell. As part of tick 2, we clear flags from the figure. It will become invisible for specific type of checks as long as it's not affected with another spell which logic requires on those checks.

And then we end the spell with setEffectDone().

Deal Damage handler

The Deal Damage handler is triggered when a figure is damaged, provided this figure is currently under the effect of the spell linked to this handler and the creature has the flag CheckSpellsBeforeJob set to TRUE. The Deal Damage handler snaps up the damage and its parameters like source/type, allows us to modify it and then returns the new value to the game engine which applies it to the creature as usual. This handler is declared in the following way:

uint16_t __thiscall interference_deal_damage_handler(SF_CGdFigureToolbox *_toolbox, uint16_t source, uint16_t target,
                                                     uint16_t current_damage, uint16_t is_spell_damage, uint32_t is_ranged_damage, uint16_t spell_id)
{
return current_damage;
}

It gives us quite a lot parameters. The *_toolbox provides an address to the toolbox of a figure which got damaged. The source gives us the index of a figure or an object which damaged our figure. The current_damage returns the amount of damage taken during a specific phase (PRE, default, post). Quick reminder:

  • PRE stands for the initial damage before it was modified in any way.
  • default stands for the damage after it was changed by other spells.
  • post stands for the final damage value after it was changed by other spells, creature innate resistances and creature's armor.

Booleans 'is_spell_damage' and 'is_ranged_damage' show whether the damage was dealt in ranged combat, or with a spell. The spell_id returns as the spell ID of the spell which is linked with the Deal Damage handler. Notice that for some reasons it returns us the Spell ID instead of Spell Index as all other handlers do. The Spell ID corresponds to spell data number within GameData.cff. For example, following the order those chapters were written, the Spell ID for the Interference will be 3551.

It's important to mention, that the Deal Damage handler must always return some value, otherwise the creature will get invulnerable to the damage for the spell duration (or damage goes overflow, which isn't what we want either).

The Interference should reduce only the damage coming from the spells. Let's check for this and if it's true, reduce the damage by a percentage stated within spell parameters. If the check didn't pass, let's just return the full damage. The full function will take this form:

uint16_t __thiscall interference_deal_damage_handler(SF_CGdFigureToolbox *_toolbox, uint16_t source, uint16_t target,
                                                     uint16_t current_damage, uint16_t is_spell_damage, uint32_t is_ranged_damage, uint16_t spell_id)
{

    if (is_spell_damage)
    {
        SF_CGdResourceSpell spell_data;
        spellAPI->getResourceSpellData(_toolbox->CGdResource, &spell_data, spell_id);
        uint16_t reduction = (current_damage * spell_data.params[1]) / 100;
        (reduction > current_damage) ? (current_damage = 0) : (current_damage -= reduction);
        return current_damage;
    }
    return current_damage;
}

If the damage comes from the spell, it will be reduced by a percentage loaded from spell parameters. We also make sure that the damage doesn't become negative, because we're not planning to accidentally heal the target.

In all other cases (damage was ranged, or neither ranged, nor spell) the handler will return the full damage.

Since that moment, the handler is ready. It's quite simple, but it can be expanded to anything your imagination might come with. You can check for inspiration examples of reverse-engineered vanilla Deal Damage handlers in API source code here.

Implementing refresh handler

The Interference spell shouldn't stack neither with itself, nor with the vanilla spells such as the Patronize or the Shelter. The Interference will cancel the effect of the latter two, and renew the its own duration (by clearing the previous instance of itself). It's possible to implement this behavior in different ways: with using the same handler for all three spells, or making the individual handlers for each of the spells.

In this chapter we'll show how to make the common handler for all three spells (the custom and the two of vanilla). The refresh handler will be split into three separate scenarios corresponding to the each of spells, and only one will be called at a time.

The main algorithm will be this:

  1. Find out which spell triggered the refresh handler (presumed we're using the single handler for all three spells).
  2. Check for presence of every of three spells over a target.
  3. Return 0, if there is any spell with higher importance than the spell we're casting currently.
  4. Remove all spells with lesser importance than the spell we're casting currently.
  5. If check shows there is already an instance of the same spell, remove it.
  6. Return 1 (would happen only if the refresh check wasn't interrupted above).

First of all, we should make a check to find out which spell we're casting currently. We can do it by switch-case construction:

    SF_GdSpell *spell = &_this->active_spell_list[spell_index];
    uint16_t source_index = spell->source.entity_index;
    switch (spell->spell_job)
    {
        case INTERFERENCE_JOB:
           {
           }
        case kGdSpellJobAbilityPatronize:
           {
           }
        case kGdSpellJobAbilityShelter:
           {
           }
        default:
        {
            break;
        }
    }
    return 1;

The general construction will look like this. Within each block we'd make three subsections, dedicated to every spell which possibly might be active over the target.

Interference

Let's examine the actual implementation for the Interference case. In our code we begin with the case INTERFERENCE_JOB: block. Inside we perform three checks.

First, we check if the target already has an Interference effect. We use toolboxAPI->hasSpellOnIt passing the source index and the INTERFERENCE_LINE macro.

if (toolboxAPI->hasSpellOnIt(_this->SF_CGdFigureToolBox, source_index, INTERFERENCE_LINE))
{
    uint16_t pruned_spell_index = toolboxAPI->getSpellIndexOfType(_this->SF_CGdFigureToolBox, source_index,
                                                                  INTERFERENCE_LINE, spell_index);
    if (spell_index != pruned_spell_index && pruned_spell_index != 0)
    {
        spellAPI->removeDLLNode(_this, pruned_spell_index);
        spellAPI->setEffectDone(_this, pruned_spell_index, 0);
    }
}

We call getSpellIndexOfType and pass spell_index as the last argument to ignore the spell that triggered the refresh handler. If we get a valid index that is different from the current spell, we remove the older instance with removeDLLNode and setEffectDone.

Next, we check for the Shelter spell. If hasSpellOnIt returns true, we obtain its spell index (passing 0 as the ignore argument because we are sure that there are no two Shelters) and then remove it.

if (toolboxAPI->hasSpellOnIt(_this->SF_CGdFigureToolBox, source_index, kGdSpellLineAbilityShelter))
{
    uint16_t pruned_spell_index = toolboxAPI->getSpellIndexOfType(_this->SF_CGdFigureToolBox, source_index,
                                                                  kGdSpellLineAbilityShelter, 0);
    spellAPI->removeDLLNode(_this, pruned_spell_index);
    spellAPI->setEffectDone(_this, pruned_spell_index, 0);
}

Similarly, we check for the Patronize spell. However, note that here we use toolboxAPI->removeSpellFromList instead of the full removal functions. That's because the Patronize is an AoE spell, and we are dealing with a single target – the source. We simply detach the Patronize instance from this figure.

if (toolboxAPI->hasSpellOnIt(_this->SF_CGdFigureToolBox, source_index, kGdSpellLineAbilityPatronize))
{
    uint16_t pruned_spell_index = toolboxAPI->getSpellIndexOfType(_this->SF_CGdFigureToolBox,
                                                                  source_index,
                                                                  kGdSpellLineAbilityPatronize, 0);
    toolboxAPI->removeSpellFromList(_this->SF_CGdFigureToolBox, source_index, pruned_spell_index);
}

After all checks, we return 1 to allow the new Interference instance to apply.

   return 1;
}

Patronize

Now look at the kGdSpellJobAbilityPatronize case. The Patronize can be blocked by higher‑importance spells – Interference and Shelter. We check for them first and return 0 if any is present. Note that we use spell->target.entity_index instead of source_index because Patronize is an AoE spell and the handler is called per target.

if (toolboxAPI->hasSpellOnIt(_this->SF_CGdFigureToolBox, spell->target.entity_index, INTERFERENCE_LINE))
{
    return 0;
}

if (toolboxAPI->hasSpellOnIt(_this->SF_CGdFigureToolBox, spell->target.entity_index,
                             kGdSpellLineAbilityShelter))
{
    return 0;
}

If neither blocking spell is active, we then check for another instance of Patronize on the same target. If found, we obtain its spell index, ignoring the current spell.

if (toolboxAPI->hasSpellOnIt(_this->SF_CGdFigureToolBox, spell->target.entity_index,
                             kGdSpellLineAbilityPatronize))
{
    uint16_t pruned_spell_index = toolboxAPI->getSpellIndexOfType(_this->SF_CGdFigureToolBox,
                                                                  spell->target.entity_index,
                                                                  kGdSpellLineAbilityPatronize,
                                                                  spell_index);
    if (spell_index != pruned_spell_index && pruned_spell_index != 0)
    {
        if (source_index == spell->target.entity_index)
        {
            ClearPatronizeInArea(_this, pruned_spell_index);
        }
        else
        {
            toolboxAPI->removeSpellFromList(_this->SF_CGdFigureToolBox, spell->target.entity_index,
                                            spell_index);
        }
    }
}

We add this check, because if the target we are checking is the original spellcaster (source index equals target index), we call ClearPatronizeInArea to remove the old Patronize instance from all affected figures. If the target is only affected by Patronize, we simply remove the old instance from just this figure using removeSpellFromList. Finally, we must return 1 to allow the new Patronize effect to be applied.

   return 1;
}

Shelter

The last case is kGdSpellJobAbilityShelter. The Shelter is blocked by Interference, so we check that first and return 0 if Interference is present.

if (toolboxAPI->hasSpellOnIt(_this->SF_CGdFigureToolBox, source_index, INTERFERENCE_LINE))
{
    return 0;
}

Next we check for an existing Shelter on the target. If found, we get its index (ignoring the current spell) and remove the older instance.

if (toolboxAPI->hasSpellOnIt(_this->SF_CGdFigureToolBox, source_index, kGdSpellLineAbilityShelter))
{
    uint16_t pruned_spell_index = toolboxAPI->getSpellIndexOfType(_this->SF_CGdFigureToolBox, source_index,
                                                                  kGdSpellLineAbilityShelter, spell_index);

    if (spell_index != pruned_spell_index && pruned_spell_index != 0)
    {
        spellAPI->removeDLLNode(_this, pruned_spell_index);
        spellAPI->setEffectDone(_this, pruned_spell_index, 0);
    }
}

Finally, we check for the Patronize spell. If it exists, we obtain its spell index and call ClearPatronizeInArea to remove it from all affected figures, because Shelter overrides Patronize completely.

if (toolboxAPI->hasSpellOnIt(_this->SF_CGdFigureToolBox, source_index, kGdSpellLineAbilityPatronize))
{
    uint16_t pruned_spell_index = toolboxAPI->getSpellIndexOfType(_this->SF_CGdFigureToolBox, source_index,
                                                                  kGdSpellLineAbilityPatronize, 0);
    ClearPatronizeInArea(_this, pruned_spell_index);
}
return 1;
}

After all checks we return 1, allowing the new Shelter to be applied. It's also will be better add safety check just in case.

    default:
    {
        break;
    }
 }
return 1; // adding this line for safety, most likely it won't be ever triggered
}

The refresh header is complete.

Alternating vanilla spells

Sadly, it's not enough to rewrite the refresh handlers for the vanilla spells. As it stands for now, due to a bug in the vanilla code, the custom refresh handlers won't be called automatically when the vanilla spells are casted. We have to manually trigger them within the Spell Effect handlers. It means we also have to rewrite the default Spell Effect handlers for the Patronize and the Shelter.

Remaking the Shelter

We have to remake the Spell Effect from the scratch. There are good news for us: the Shelter is automatically taken in account for resistances calculation. We don't need to rewrite any other blocks of code. However, we'll have to implement the usual stuff like visual effects, spell duration and resolving the refresh handler situations.

void __thiscall shelter_effect_handler(SF_CGdSpell *_this, uint16_t spell_index)
{
    SF_GdSpell *spell = &_this->active_spell_list[spell_index];
    uint16_t source_index = _this->active_spell_list[spell_index].source.entity_index;
    uint32_t current_tick = spellAPI->addToXData(_this, spell_index, SPELL_TICK_COUNT_AUX, 1);

We retrieve the spell instance and the source index. Then we increase the current tick counter by 1 and get the new value with addToXData.

    if (current_tick == 1)
    {
        if (spellAPI->checkCanApply(_this, spell_index))
        {

If we are at the first tick (the spell just started), we check whether the spell can be applied to the caster. The refresh handler will decide if the Shelter can coexist with other spells like Interference. If it returns true, we proceed.

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

We load the spell data from the resources.

            SF_CGdTargetData relative_data;
            relative_data.position = {0, 0};
            relative_data.entity_type = 1;
            relative_data.entity_index = source_index;
            uint32_t unused;

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

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

We also add the visual effect. Then we set the spell duration and disable the Spell Effect handler from being triggered until the spell duration ends.

            uint32_t ticks_interval = spell_data.params[0];

            spell->to_do_count = (uint16_t)((ticks_interval * 10) / 1000);
            spell->flags |= SpellFlagKey::CHECK_SPELLS_BEFORE_JOB2;
            _this->SF_CGdFigure->figures[source_index].flags |= F_CHECK_SPELLS_BEFORE_JOB;

We also can add check for safety. If the Refresh handler forbid to apply the spell, we terminate it and do nothing. Notice that we don't add any visuals or duration. The function stops right after.

        }
        else
        {
            spellAPI->setEffectDone(_this, spell_index, 0); // we end a spell
        }
        return;
    }

The second tick will happen no matter what value ticks counter has (we assume that tick 1 would always happen before tick 2, so the check for the tick 2 becomes redundant). We check whether the figure isn't dead yet, and if it's alive, we clear flags set during tick 1 from it.

    if (figureAPI->isAlive(_this->SF_CGdFigure, source_index))
    {
        spellAPI->figTryClrCHkSPlBfrJob2(_this, spell_index);
    }
    spellAPI->setEffectDone(_this, spell_index, 0);     // we end a spell
}

Then we terminate the spell. The Shelter effect is completely remade now.

Remaking the Patronize

In comparison to the Shelter, it will be considerably more difficult to recreate Patronize, because it is an AoE spell. It gets even worse, because unlike the Shieldwall AoE which we made during the previous chapter, the Patronize must be implemented with a single Spell Type as it was made in vanilla.

Moreover, we're going to add function which will prioritize which figures are going to receive buff first. We're going to give buff to figures which have more enemy units near, and close we're going to double the priority for melee figures.

But first of all, let's create a wrapper function that will clear the Patronize from all figures affected by a specific spell instance. We mentioned this function above in Refresh handler.

This wrapper is necessary, because Patronize can be removed from a figure in more than one case:

  1. When spell is finished
  2. When spell is removed from its spellcaster by Shelter or Interference
  3. When the older instance of spell is removed with new one.

Unless we want to copy-paste the following algorithm, it's better to wrap it into a function.

void ClearPatronizeInArea(SF_CGdSpell *_this, uint16_t spell_index)
{
    for (uint16_t target_index = 1; target_index <= _this->SF_CGdFigure->max_used; target_index++)
    {
        if (toolboxAPI->hasSpellOnIt(_this->SF_CGdFigureToolBox, target_index, kGdSpellLineAbilityPatronize) &&
            (figureAPI->getSpellJobStartNode(_this->SF_CGdFigure, target_index) != 0))          
        {
            toolboxAPI->removeSpellFromList(_this->SF_CGdFigureToolBox, target_index, spell_index);
            spellAPI->tryClearCheckSpellsBeforeJob(_this, spell_index, target_index);
        }
    }
}

The function iterates over all figures currently in the game (max_used tells us how many are active). For each figure we check if it has the Patronize spell type on it and if there is an active spell node. If both conditions are true, we remove the spell using the exact spell_index that identifies this instance. We also trigger tryClearCheckSpellsBeforeJob to clean up any refresh flags for that target.

Now we need a way to decide which friendly figures should receive the Patronize when the AoE radius contains more allies than the spell can cover. We will assign each potential target a priority and then sort them.

typedef struct
{
    uint16_t figure_id;
    uint16_t prio;
} patronize_entry;

This simple structure holds a figure identifier and its priority value.

The function which calculates the priority for a figure is a bit complicated. We're going to get target position and then check 8 tiles around it for whether they're occupied with enemies (and skip all tiles which aren't passable).

uint16_t calculatePrio(SF_CGdSpell *_this, uint16_t figure_id)
{
    SF_Coord target_pos = _this->SF_CGdFigure->figures[figure_id].position;
    uint16_t dx = 7;
    uint16_t sec_prio = 0;
    for (int i = 8; i> 0; i--)
    {
        uint16_t near_x = *(uint16_t *)((uint32_t)&_this->SF_CGdWorld->unknown1[0].uknwn1 + dx) + target_pos.X;
        uint16_t near_y = *(uint16_t *)((uint32_t)&_this->SF_CGdWorld->unknown1[0].uknwn2 + dx) + target_pos.Y;
        if (((*(uint8_t *)&_this->SF_CGdWorld->cells[near_y*0x400 + near_x].world_cell_flags) & 0x10) != 0)
        {
            uint16_t sec_target = toolboxAPI->getFigureFromWorld(_this->SF_CGdWorldToolBox, near_x, near_y, 1);

            if ((sec_target != 0)
                && (toolboxAPI->figuresCheckHostile(_this->SF_CGdFigureToolBox, figure_id, sec_target)))
            {
                sec_prio++;
            }
        }
        dx += 7;
    }

We're also going to double the priority for melee figures and increase it by one, to make sure melee units go before ranged even if there are no enemies close to them.

    if (toolboxAPI->isUnitMelee(_this->SF_CGdFigureToolBox, figure_id))
    {
        sec_prio *= 2;
        sec_prio +=1;
    }
    return sec_prio;
}

Another wrapper is done. Now we can write the main effect handler for Patronize.

void __thiscall patronize_effect_handler(SF_CGdSpell *_this, uint16_t spell_index)
{
    SF_GdSpell *spell = &_this->active_spell_list[spell_index];
    uint16_t source_index = _this->active_spell_list[spell_index].source.entity_index;
    uint32_t current_tick = spellAPI->addToXData(_this, spell_index, SPELL_TICK_COUNT_AUX, 1);

As with Shelter, we retrieve the spell, the caster, and increment the tick counter.

    if (current_tick == 1)
    {
        // spell start
        if (spellAPI->checkCanApply(_this, spell_index))
        {

If this is the first tick and the refresh handler allows the spell to exist, we proceed.

We load spell parameters and set flags which allow spell to be applied when the damage is dealt to a figure.

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

            uint32_t ticks_interval = spell_data.params[0];
            uint16_t effect_radius = spell_data.params[2];
            uint16_t figure_count = spell_data.params[3];

            spell->flags |= SpellFlagKey::CHECK_SPELLS_BEFORE_JOB2;
            _this->SF_CGdFigure->figures[source_index].flags |= F_CHECK_SPELLS_BEFORE_JOB;
            _this->active_spell_list[spell_index].to_do_count = (uint16_t)((ticks_interval * 10) / 1000);

Then we disable Spell Effect handler from being triggered for the duration of a spell.

            SF_Coord cast_center;
            figureAPI->getPosition(_this->SF_CGdFigure, &cast_center, source_index);

            SF_CGdTargetData relative_data;
            relative_data.position = {0, 0};
            relative_data.entity_type = 1;
            relative_data.entity_index = source_index;
            uint32_t unused;

            SF_Rectangle hit_area;

            spellAPI->getTargetsRectangle(_this, &hit_area, spell_index, spell_data.params[0], &cast_center);

            spellAPI->addVisualEffect(_this, spell_index, kGdEffectSpellHitWorld, &unused, &relative_data,
                                      _this->OpaqueClass->current_step, 0x25, &hit_area);

            hit_area.partA = 0;
            hit_area.partB = 0;
            spellAPI->addVisualEffect(_this, spell_index, kGdEffectSpellHitTarget, &unused, &relative_data,
                                      _this->OpaqueClass->current_step, 0x96, &hit_area);

We also add visuals which are going to be combination of area effect and targeted effect. Then we setup iterator and start checking potential targets in area.

            CGdFigureIterator figure_iterator;
            iteratorAPI->setupFigureIterator(&figure_iterator, _this);
            iteratorAPI->iteratorSetArea(&figure_iterator, &cast_center, effect_radius);
            std::vector<patronize_entry> targets;
            for (uint16_t target_index = iteratorAPI->getNextFigure(&figure_iterator); target_index != 0;
                 target_index = iteratorAPI->getNextFigure(&figure_iterator))

In this case we use for instead of while, because we are going to scan all valid targets, and then apply spell to them according to their priority. Hence, we need to create the list, sort it and apply spell to the figures with the highest priorities.

            {
                if (((int16_t)(_this->SF_CGdFigure->figures[target_index].owner) ==
                     (int16_t)(_this->SF_CGdFigure->figures[source_index].owner)) &&
                    (((uint8_t)(_this->SF_CGdFigure->figures[target_index].flags) & (IS_DEAD | RESESRVED_ONLY)) == 0) &&
                    (toolboxAPI->isTargetable(_this->SF_CGdFigureToolBox, target_index)))
                {
                    spell->target.entity_index = target_index;
                    if (spellAPI->checkCanApply(_this, spell_index) && (source_index != target_index))
                    {

                        patronize_entry entry;
                        entry.figure_id = target_index;
                        entry.prio = calculatePrio(_this, target_index);
                        targets.push_back(entry);
                    }
                }
            }

After we checked all figures within the iterator, we dispose of the iterator.

            iteratorAPI->disposeFigureIterator(&figure_iterator);

Then we sort the figures within the vector. Figures with more enemies near and melee units should get on the top.

            std::sort(targets.begin(), targets.end(), [](const patronize_entry& a, const patronize_entry& b)
            {
                return a.prio > b.prio;
            });

Then we apply spell to every figure on the list starting from the highest priority and going down. We do this as long as we don't exceed figure count limit determined by spell parameters.

            for (int i = 0; ((i < targets.size()) && (figure_count > 0)); i++)
            {
                SF_CGdTargetData relative_data;
                relative_data.position = {0, 0};
                relative_data.entity_type = 1;         // 1 stands for individual figure
                relative_data.entity_index = targets.at(i).figure_id;        
                uint32_t unused;
                SF_Rectangle aux_data;
                aux_data.partA = 0;
                aux_data.partB = 0;
                spellAPI->addVisualEffect(_this, spell_index, kGdEffectSpellHitTarget, &unused, &relative_data,
                                          _this->OpaqueClass->current_step, 0x96, &aux_data);
                _this->SF_CGdFigure->figures[source_index].flags |= F_CHECK_SPELLS_BEFORE_JOB;
                toolboxAPI->addSpellToFigure(_this->SF_CGdFigureToolBox,  targets.at(i).figure_id, spell_index);
                figure_count--;
            }
        }
        return;
    }

The tick 1 finished. When tick 2 start, we simply clear all instances of a given spell from the spellcaster and affected targets.

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

With that, The Interference mod is completed and ready to be used. The vanilla spells are adapted to the custom spells. They are fully operation imitating the vanilla behavior except for the refresh part (vanilla Patronize and Shelter can be stacked with each other).

The source code for this chapter can be checked here.

If you would like to know how to rework the Shieldwall into a single Spell Type instead of two, feel free to delve into the section below.

BONUS: Refactoring the Shieldwall into a single spell

You have noticed that the Patronize spell is an area spell which it's implemented as a monolyte spell instead of two interrelated spells. This is the way the most of vanilla AoE spells are made. This way of making AoE spells is less flexible than the implementation we used in the Chapter 2, though in the end it provides virtually the same effect. We suggest to take a look into how the Shieldwall could be alternated to work on the basis of the single Spell Type.

This subsection is highly optional - feel free to skip it, if you feel that you understood the general idea behind that. However, it's important to mention that the Shieldwall refresh handler adheres to a bit more complicated logic than the Patronize one. The Shieldwall isn't refreshed when it's applied to the same target. It affects new untouched units in area instead of renewing the duration of the spell over and over the same units. Within this subsection we will explain how to transfer those nuances intact.

Before you begin the refactoring, you should create new spell or adapt existing one to have all parameters necessary. It must provide parameters which were stored across different spells before: spell radius, figures amount, bonus modifier percentage, spell duration. The new spell data will look like this:

image

When the spell data is prepared, let's move on to the code. The initial steps contain nothing new. You should include the framework API, make the macros for Spell Type and Spell Job IDs of the new spell. Declare pointers to the framework methods. Declare all three main functions: DLL loading, registering the module and the module initialization.

Because we're going to use only a single Spell Type, the module initialization should register only the four handlers in total: Spell Type, Effect, Refresh and Spell End.

    /*
    storing pointers to the framework methods and structures for convenience
    */
    SFSpell *shield_wall_universal_spell = registrationAPI->registerSpell(SHIELD_WALL_UNIVERSAL_LINE);
    registrationAPI->linkTypeHandler(shield_wall_universal_spell, &shield_wall_universal_type_handler);
    registrationAPI->linkEffectHandler(shield_wall_universal_spell, SHIELD_WALL_UNIVERSAL_JOB, &shield_wall_universal_effect_handler);
    registrationAPI->linkRefreshHandler(shield_wall_universal_spell, &shield_wall_universal_refresh_handler);
    registrationAPI->linkEndHandler(shield_wall_universal_spell, &shield_wall_universal_end_handler);

The Spell Type handler remains unchanged. You should link the Spell Type with the respective Spell Job (mandatory step for any Spell Type handler). You should initialize two XData keys: one for ticks counter, and another to save the bonus modifier value directly within the spell.

void __thiscall shield_wall_universal_type_handler(SF_CGdSpell *_this, uint16_t spell_index)
{
    _this->active_spell_list[spell_index].spell_job = SHIELD_WALL_UNIVERSAL_JOB;
    spellAPI->setXData(_this, spell_index, SPELL_TICK_COUNT_AUX, 0);
    spellAPI->setXData(_this, spell_index, SPELL_STAT_MUL_MODIFIER, 0);
}

The Spell Effect handler will have slightly different structure than it had before. Because the Shieldwall can be used indefinitely over an area, it won't be stopped if refresh handler returns false. It will simply skip all affected targets, and keep searching for other targets in radius. The general algorithm will be this:

Any tick:

  1. Initialize ticks counter (get current tick, add plus one to amount of ticks passed).

Tick 0:

  1. Apply spell to the source, if the source isn't affected yet.

  2. Apply spell to all valid targets in radius.

  3. Stop the spell from being triggered for the spell duration.

Tick 1:

  1. Remove the spell from all affected targets.

  2. Terminate the spell.

The Spell Effect handler begins like this. We get the spell pointer and update the ticks counter.

void __thiscall shield_wall_universal_effect_handler(SF_CGdSpell *_this, uint16_t spell_index)
{
    SF_GdSpell *spell = &_this->active_spell_list[spell_index];
    uint32_t current_tick = spellAPI->getXData(_this, spell_index, SPELL_TICK_COUNT_AUX);
    spellAPI->addToXData(_this, spell_index, SPELL_TICK_COUNT_AUX, 1);

That's everything we needed in general. Let's proceed to the specific things we have to perform during the ticks.

    if (current_tick == 0)
        {
            SF_CGdResourceSpell spell_data;
            spellAPI->getResourceSpellData(_this->SF_CGdResource, &spell_data, spell->spell_id);

            uint16_t source_index = _this->active_spell_list[spell_index].source.entity_index;

            uint8_t recalc_value = spell_data.params[2];
            spellAPI->setXData(_this, spell_index, SPELL_STAT_MUL_MODIFIER, recalc_value);

We load spell data necessary for spell logic, and immediately save the bonus modifier percentage to the spell XData key.

There is a big difference between the Shieldwall working as a single spell and as two spells combined. As you might remember from the Patronize implementation, the self-targeting spell is automatically considered to be applied to the spell source on cast. It concerns only GUI and internal statuses, the actual spell logic remains under our control. However, this means that before applying the spell logic, we should double check to make sure whether we're working with the original of the Shieldwall or with the second instance in line.

To do so, we will use the function getSpellIndexOfType. The last parameter is the spell index which this function will always ignore, even if the spell with the given index belongs to the same Spell Type as it is searching for. It means the function will return false, if there is only one instance of Shieldwall over the spellcaster (the Shieldwall which triggered the current Spell Effect handler).

        uint16_t spell_index_current = toolboxAPI->getSpellIndexOfType(_this->SF_CGdFigureToolBox, source_index, SHIELD_WALL_UNIVERSAL_LINE, spell_index);

        if (spell_index_current == 0)
            {
                figureAPI->addBonusMultToStatistic(_this->SF_CGdFigure, ARMOR, source_index, recalc_value);
            }

Though, if spell_index_current gets assigned with the value other than 0, it means there are currently more than a single instance of the Shieldwall. In this case, we should merely remove the newer instance from the spellcaster.

        else
            {
                toolboxAPI->removeSpellFromList(_this->SF_CGdFigureToolBox, source_index, spell_index);
            }

However, we don't terminate the spell right now, because it still might be applied to other creatures in the specific radius around the spellcaster.

Let's apply visuals to show that the spell affected the area.

        SF_Rectangle hit_area;
        SF_Coord cast_center;
        figureAPI->getPosition(_this->SF_CGdFigure, &cast_center, source_index);
        SF_CGdTargetData relative_data;
        figureAPI->getPosition(_this->SF_CGdFigure, &relative_data.position, source_index);
        relative_data.entity_type = 4;
        relative_data.entity_index = 0;
        uint32_t unused;
        spellAPI->getTargetsRectangle(_this, &hit_area, spell_index, spell_data.params[0], &cast_center);
        spellAPI->addVisualEffect(_this, spell_index, kGdEffectSpellHitWorld, &unused, &relative_data, _this->OpaqueClass->current_step, 0x19, &hit_area);

The visual part is done (it requires additional editing of game files to add the persistent visual effect to targets under shieldwall spell, so we skip that step here). Let's proceed to the iterator.

        CGdFigureIterator figure_iterator;
        iteratorAPI->setupFigureIterator(&figure_iterator, _this);
        iteratorAPI->iteratorSetArea(&figure_iterator, &cast_center, spell_data.params[0]);
        uint16_t target_index = iteratorAPI->getNextFigure(&figure_iterator);
        if (target_index == source_index)
            {
                target_index = iteratorAPI->getNextFigure(&figure_iterator);
            }
        uint16_t figure_count = spell_data.params[1];

We declared an iterator, and ensured that it won't return the source figure index, since we have already applied spell logic to the spellcaster above for free.

The iterator loop is made in a usual way. We keep scanning for valid targets as long as iterator returns us targets in area, and as long as the spell still has the free usages. We check for whether the target which the iterator returned is valid for the spell (it belongs to the spellcaster's player, it's alive, it's targetable). If the target is valid, we trigger the refresh handler to check whether the target is unaffected with Shieldwall yet.

        while (target_index != 0 && figure_count != 0)
            {
            if (((int16_t)(_this->SF_CGdFigure->figures[target_index].owner) == (int16_t)(_this->SF_CGdFigure->figures[source_index].owner)) &&
                (((uint8_t)(_this->SF_CGdFigure->figures[target_index].flags) & 0xa) == 0) &&
                (toolboxAPI->isTargetable(_this->SF_CGdFigureToolBox, target_index)))
                {
                spell->target.entity_index = target_index;
                    if (spellAPI->checkCanApply(_this, spell_index))

If the target meets all conditions, we extend the spell to it and apply the spell logic. Because we use only a single spell to emulate Shieldwall's logic, we extend the same instance of the spell to the target, instead of starting completely new spell as we did in the Chapter 2.

                        {
                            toolboxAPI->addSpellToFigure(_this->SF_CGdFigureToolBox, target_index, spell_index);
                            figureAPI->addBonusMultToStatistic(_this->SF_CGdFigure, ARMOR, target_index, recalc_value);

After we extended the spell and modified target's statistic, we reduce the figures count by one and move to the next target in iterator.

                            figure_count--;
                        }
                }
                target_index = iteratorAPI->getNextFigure(&figure_iterator);
            }

When the iterator loop was stopped for either of reasons, we finalize the tick 0. We dispose of the iterator. Then, we stop the Spell Effect handler from being triggered until the spell duration passes.

            iteratorAPI->disposeFigureIterator(&figure_iterator);

            uint16_t ticks_interval = spell_data.params[3];
            _this->active_spell_list[spell_index].to_do_count = (uint16_t)((ticks_interval * 10) / 1000);
        }

The tick 1 will clear the spell from the source, and from all targets which were affected with that. In the same manner as persisent AoE vanilla spells do it, we'll have to run a loop through all figures in the map and check whether they're affected with the Shieldwall spell which spell index matches the spell index of the spell which triggered the Tick 1 of Spell Effect handler.

 else
        {
            uint16_t recalc_value = spellAPI->getXData(_this, spell_index, SPELL_STAT_MUL_MODIFIER);

Because we've saved bonus percentage directly within the spell, we can access it directly instead of loading this value from game data again. We should do it before the loop, because this value is the same for all targets affected (and the spellcaster as well).

            for (uint16_t target_index = 1; target_index <= _this->SF_CGdFigure->max_used; target_index++)
                {
                    uint16_t spell_index_current = toolboxAPI->getSpellIndexOfType(_this->SF_CGdFigureToolBox, target_index, SHIELD_WALL_UNIVERSAL_LINE, spell_index);
                        if (toolboxAPI->hasSpellOnIt(_this->SF_CGdFigureToolBox, target_index, SHIELD_WALL_UNIVERSAL_LINE))
                            if (spell_index_current == 0)

We get the spell index of the Shieldwall over a target. We make sure to make the function skip the spell index of the current spell. We check whether the target is affected with any instance of the Shieldwall with hasSpellOnIt function.

If hasSpellOnIt returns true and getSpellIndexOfType returns 0, the target is affected with the Shieldwall instance which spell index matches the spell index we're supposed to remove. We remove the spell from the target, and remove the bonus armor percentage from target's statistic.

                                {
                                    toolboxAPI->removeSpellFromList(_this->SF_CGdFigureToolBox, target_index, spell_index);
                                    figureAPI->addBonusMultToStatistic(_this->SF_CGdFigure, ARMOR, target_index, -recalc_value);
                                }
                }

When the spell was removed from every affected target, we stop it.

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

As you might see, the tick 1 resembles the function ClearPatronizeInArea which we designed to clear the Patronize instances from targets in area. We don't encapsulate this algorithm into a separate function, because we're not planning to trigger it with other spells. We have to copy it additionally to Spell End handler only.

void __thiscall shield_wall_universal_end_handler(SF_CGdSpell *_this, uint16_t spell_index)
{
    SF_GdSpell *spell = &_this->active_spell_list[spell_index];

    uint16_t recalc_value = spellAPI->getXData(_this, spell_index, SPELL_STAT_MUL_MODIFIER);

    for (uint16_t target_index = 1; target_index <= _this->SF_CGdFigure->max_used; target_index++)
        {
            uint16_t spell_index_current = toolboxAPI->getSpellIndexOfType(_this->SF_CGdFigureToolBox, target_index, SHIELD_WALL_UNIVERSAL_LINE, spell_index);
            if (toolboxAPI->hasSpellOnIt(_this->SF_CGdFigureToolBox, target_index, SHIELD_WALL_UNIVERSAL_LINE))
                if (spell_index_current == 0)
                                {
                                    toolboxAPI->removeSpellFromList(_this->SF_CGdFigureToolBox, target_index, spell_index);
                                    figureAPI->addBonusMultToStatistic(_this->SF_CGdFigure, ARMOR, target_index, -recalc_value);
                                }
        }
    spellAPI->removeDLLNode(_this, spell_index);
    spellAPI->setEffectDone(_this, spell_index, 0);
}

We have to implement the Spell Refresh handler as well, because we trigger it with 'checkCanApply' within the tick 0 of the Spell Effect handler. Because the spell is supposed to be blocked by previous instance of itself, the Refresh handler algorithm will be very simple. It would be triggered only for AoE return true or false only.

The nuanced check for whether this is the first or the second instance of the Shieldwall is necessary for the spellcaster only, so we did wise when implemented it directly within the Spell Effect handler.

int __thiscall shield_wall_universal_refresh_handler(SF_CGdSpell *_this, uint16_t spell_index)
{
    SF_GdSpell *spell = &_this->active_spell_list[spell_index];

    uint16_t target_index = spell->target.entity_index;

    if (toolboxAPI->hasSpellOnIt(_this->SF_CGdFigureToolBox, target_index, SHIELD_WALL_UNIVERSAL_LINE))
        {
            return 0;
        }
    else
        {
            return 1;
        }
}

The single Spell Type Shieldwall is complete. It can be compiled and used. Please note, within the scope of this chapter, it's written to use the other Spell Type value than the original Shieldwall. However, for purpose of speeding up the testing process, you could choose any value you would like to.

The source code of this implementation can be downloaded here.

Clone this wiki locally