• There is NO official Otland's Discord server and NO official Otland's server list. The Otland's Staff does not manage any Discord server or server list. Moderators or administrator of any Discord server or server lists have NO connection to the Otland's Staff. Do not get scammed!

C++ [Nostalrius] Order of events in combat.cpp

X X X

Newb
Joined
Jul 26, 2015
Messages
148
Reaction score
13
Hello,

I have been trying to change combat.cpp so that spell effects are drawn in the correct order (see Ezzz's post here: The Violet Project - CipSoft Reverse Engineering Project (https://otland.net/threads/the-violet-project-cipsoft-reverse-engineering-project.279275/#post-2682275))

There is an explanation here (TFS 1.X+ - How to set effect to correct order in TFS 1.5 (https://otland.net/threads/how-to-set-effect-to-correct-order-in-tfs-1-5.284266/)) But unfortunately combat.cpp is different enough that this solution does not work for Nostalrius.

For the example of "exori vis":

I understand that essentially the game is dealing the damage to the target first, which creates the "energyhit" effect on the bottom, and then the game is drawing the "teleport" effect after.

I know that the spell effect (so teleport in the case of exori vis) is called here in void Combat::combatTileEffects:
C++:
if (params.impactEffect != CONST_ME_NONE) {
        Game::addMagicEffect(list, tile->getPosition(), params.impactEffect);
    }

Then, the effect is actually created on the map in void Combat::CombatFunc, here:
C++:
    for (Tile* tile : tileList) {
        if (canDoCombat(caster, tile, params.aggressive) != RETURNVALUE_NOERROR) {
            continue;
        }

        combatTileEffects(list, caster, tile, params);

But by the time this CombatFunc runs, the damage has already been done to the target. Does anyone know what modifications need to be made so the spell effect is drawn first, before the damage is added?

Thanks
 
Solution
You are basically correct in your assessment of what is happening: the two events are occurring in the wrong order. After reviewing the call stack with many prints, here is a solution that I tested and found to be working without any issues:

As you pointed out, the area effect from the spell is passed into this function:
C++:
void Combat::combatTileEffects(const SpectatorVec& list, Creature* caster, Tile* tile, const CombatParams& params)
And actually drawn out in the very last 'if' statement of that function:
C++:
if (params.impactEffect != CONST_ME_NONE) {
        Game::addMagicEffect(list, tile->getPosition(), params.impactEffect);
    }

combatTileEffects is called toward the end of this function:
C++:
void...
You are basically correct in your assessment of what is happening: the two events are occurring in the wrong order. After reviewing the call stack with many prints, here is a solution that I tested and found to be working without any issues:

As you pointed out, the area effect from the spell is passed into this function:
C++:
void Combat::combatTileEffects(const SpectatorVec& list, Creature* caster, Tile* tile, const CombatParams& params)
And actually drawn out in the very last 'if' statement of that function:
C++:
if (params.impactEffect != CONST_ME_NONE) {
        Game::addMagicEffect(list, tile->getPosition(), params.impactEffect);
    }

combatTileEffects is called toward the end of this function:
C++:
void Combat::CombatFunc(Creature* caster, const Position& pos, const AreaCombat* area, const CombatParams& params, COMBATFUNC func, CombatDamage* data)
It's called inside a 'for' loop that iterates through all area tiles in whatever spell is being cast. As you already discovered, by the time this happens, the damage has already been done to the target, and the "hit effect" (as it's called in game.cpp) has already ocurred.

From this, we realize that when we move the order of which effect comes first, we cant just throw 'Game::addMagicEffect' anywhere. We have to grab all tiles for the spell and iterate through.

So, let's get to the actual changes:
Create a new function, name it whatever you want, here is mine:
C++:
void Combat::drawEffects(Creature* caster, const Position& pos, const AreaCombat* area, const CombatParams& params)
{
    std::forward_list<Tile*> tileList;

    if (caster) {
        getCombatArea(caster->getPosition(), pos, area, tileList);
    } else {
        getCombatArea(pos, pos, area, tileList);
    }

    SpectatorVec list;
    uint32_t maxX = 0;
    uint32_t maxY = 0;

    // calculate the max viewable range
    for (Tile* tile : tileList) {
        const Position& tilePos = tile->getPosition();

        uint32_t diff = Position::getDistanceX(tilePos, pos);
        if (diff > maxX) {
            maxX = diff;
        }

        diff = Position::getDistanceY(tilePos, pos);
        if (diff > maxY) {
            maxY = diff;
        }
    }

    const int32_t rangeX = maxX + Map::maxViewportX;
    const int32_t rangeY = maxY + Map::maxViewportY;
    g_game.map.getSpectators(list, pos, true, true, rangeX, rangeX, rangeY, rangeY);

    for (Tile* tile : tileList) {
        if (canDoCombat(caster, tile, params.aggressive) != RETURNVALUE_NOERROR) {
            continue;
        }

        if (params.impactEffect != CONST_ME_NONE) {
            Game::addMagicEffect(list, tile->getPosition(), params.impactEffect);
        }

        combatTileEffects(list, caster, tile, params);
    }
}
I placed it right after 'void Combat::CombatFunc' and before 'void Combat::doCombat'. You will need to declare this function in combat.h:
C++:
static void drawEffects(Creature* caster, const Position& pos, const AreaCombat* area, const CombatParams& params);
I placed it after 'void circleShapeSpell'.

Next, comment out the call to combatTileEffects toward the 'bottom of void Combat::CombatFunc', and call your new function before the very last curly bracket:
C++:
for (Tile* tile : tileList) {
        if (canDoCombat(caster, tile, params.aggressive) != RETURNVALUE_NOERROR) {
            continue;
        }

//        combatTileEffects(list, caster, tile, params); originally area effects were drawn here, but this is after the target takes damage
 
        if (CreatureVector* creatures = tile->getCreatures()) {
            const Creature* topCreature = tile->getTopCreature();
            for (Creature* creature : *creatures) {
                if (params.targetCasterOrTopMost) {
                    if (caster && caster->getTile() == tile) {
                        if (creature != caster) {
                            continue;
                        }
                    } else if (creature != topCreature) {
                        continue;
                    }
                }

                if (!params.aggressive || (caster != creature && Combat::canDoCombat(caster, creature) == RETURNVALUE_NOERROR)) {
                    func(caster, creature, params, data);
                    if (params.targetCallback) {
                        params.targetCallback->onTargetCombat(caster, creature);
                    }

                    if (params.targetCasterOrTopMost) {
                        break;
                    }
                }
            }
        }
    }
    drawEffects(caster, pos, area, params); //new function called here, this runs before the target takes damage
}

This is only half of the battle. If you do this fix, area spells like energy strike, energy beam, monster AoE spells will have the correct effect order, but targeted attacks, like LMM, HMM, SD, etc, will still have the wrong order.

For targeted attacks, the magic effect is passed in to this function here:
C++:
bool Combat::doCombatHealth(Creature* caster, Creature* target, CombatDamage& damage, const CombatParams& params)
{
    bool canCombat = !params.aggressive || (caster != target && Combat::canDoCombat(caster, target) == RETURNVALUE_NOERROR);
    if ((caster == target || canCombat) && params.impactEffect != CONST_ME_NONE) {
        g_game.addMagicEffect(target->getPosition(), params.impactEffect); //<--distance effect originally drawn right here, this is too late
    }

    if (canCombat) {
        if (caster && params.distanceEffect != CONST_ANI_NONE) {
            addDistanceEffect(caster, caster->getPosition(), target->getPosition(), params.distanceEffect);
        }

        canCombat = CombatHealthFunc(caster, target, params, &damage);
        if (params.targetCallback) {
            params.targetCallback->onTargetCombat(caster, target);
        }
    }

    return canCombat;
}
Comment out the call to draw the effect in that function: g_game.addMagicEffect(target->getPosition(), params.impactEffect);
And place it instead here in 2 places, within the if statement (we only want to send the magic effect if there is a magic effect, or the Cip client will crash when a player uses a wand/rod for example, the server sends effect 0 to the client, we also want to send the effect even if the attack is blocked by armor):

C++:
bool Combat::CombatHealthFunc(Creature* caster, Creature* target, const CombatParams& params, CombatDamage* data)
{
    assert(data);
    CombatDamage damage = *data;

    if (damage.value == 0) {
        damage.value = normal_random(damage.min, damage.max);
    }

    if (damage.value < 0 && caster) {
        Player* targetPlayer = target->getPlayer();
        if (targetPlayer && caster->getPlayer()) {
            damage.value /= 2;
        }
    }

    if (g_game.combatBlockHit(damage, caster, target, params.blockedByShield, params.blockedByArmor, params.itemId != 0)) {
            if (params.impactEffect > 0) {
                  g_game.addMagicEffect(target->getPosition(), params.impactEffect); //we also want to still draw the effect even if the attack doesnt do damage, i.e. a beholder using their weak SD attack, it is often blocked by the player's shield
             }
        return false;
    }
 
    if (g_game.combatChangeHealth(caster, target, damage)) { //this is the exact spot in game.cpp where it does damage and makes the effect    
         if (params.impactEffect > 0) {
              g_game.addMagicEffect(target->getPosition(), params.impactEffect);//move drawing the end effect here so that it happens BEFORE the damage, but only if it receives an impact effect
        }
        CombatConditionFunc(caster, target, params, &damage);
        CombatDispelFunc(caster, target, params, nullptr);
    }
 
    return true;
}
That's it. Compile and run. Screenshots of before/after:
effect_fix.png
 
Last edited:
Solution
Wow it worked perfectly in nostalrius & sabrehaven, also working for both Cip client AND OTC. i didnt even think of range attcks, and i never would hve figured out the area spells either i was so lost.
very good directions & explanation, tysm!!!
 
Can't edit my original post, but this is a cleaner version of the final code I posted (bool Combat::CombatHealthFunc):
C++:
bool Combat::CombatHealthFunc(Creature* caster, Creature* target, const CombatParams& params, CombatDamage* data)
{
    assert(data);
    CombatDamage damage = *data;

    if (damage.value == 0) {
        damage.value = normal_random(damage.min, damage.max);
    }

    if (damage.value < 0 && caster) {
        Player* targetPlayer = target->getPlayer();
        if (targetPlayer && caster->getPlayer()) {
            damage.value /= 2;
        }
    }
    
    bool canCombat = !params.aggressive || (caster != target && Combat::canDoCombat(caster, target) == RETURNVALUE_NOERROR); //define the canCombatBool first here
    
    if (g_game.combatBlockHit(damage, caster, target, params.blockedByShield, params.blockedByArmor, params.itemId != 0)) {
        if ((caster == target || canCombat) && params.impactEffect != CONST_ME_NONE) { //we also want to still draw the effect even if the attack doesnt do damage, i.e. a beholder using their weak SD attack, it is often blocked by the player's shield
            g_game.addMagicEffect(target->getPosition(), params.impactEffect);
        }
        return false;
    }
    
    if (g_game.combatChangeHealth(caster, target, damage)) { //this is the exact spot in game.cpp where it does damage and makes the effect
        if ((caster == target || canCombat) && params.impactEffect != CONST_ME_NONE) { //move drawing the target effect here so that it happens BEFORE the damage, but only if it receives an impact effect
            g_game.addMagicEffect(target->getPosition(), params.impactEffect);
        }   
        CombatConditionFunc(caster, target, params, &damage);
        CombatDispelFunc(caster, target, params, nullptr);
    }
    
    return true;
}
It works the same, but includes the logic for bool CanCombat, which is present in the other function that originally drew out target effects.
 
You are basically correct in your assessment of what is happening: the two events are occurring in the wrong order. After reviewing the call stack with many prints, here is a solution that I tested and found to be working without any issues:

As you pointed out, the area effect from the spell is passed into this function:
C++:
void Combat::combatTileEffects(const SpectatorVec& list, Creature* caster, Tile* tile, const CombatParams& params)
And actually drawn out in the very last 'if' statement of that function:
C++:
if (params.impactEffect != CONST_ME_NONE) {
        Game::addMagicEffect(list, tile->getPosition(), params.impactEffect);
    }

combatTileEffects is called toward the end of this function:
C++:
void Combat::CombatFunc(Creature* caster, const Position& pos, const AreaCombat* area, const CombatParams& params, COMBATFUNC func, CombatDamage* data)
It's called inside a 'for' loop that iterates through all area tiles in whatever spell is being cast. As you already discovered, by the time this happens, the damage has already been done to the target, and the "hit effect" (as it's called in game.cpp) has already ocurred.

From this, we realize that when we move the order of which effect comes first, we cant just throw 'Game::addMagicEffect' anywhere. We have to grab all tiles for the spell and iterate through.

So, let's get to the actual changes:
Create a new function, name it whatever you want, here is mine:
C++:
void Combat::drawEffects(Creature* caster, const Position& pos, const AreaCombat* area, const CombatParams& params)
{
    std::forward_list<Tile*> tileList;

    if (caster) {
        getCombatArea(caster->getPosition(), pos, area, tileList);
    } else {
        getCombatArea(pos, pos, area, tileList);
    }

    SpectatorVec list;
    uint32_t maxX = 0;
    uint32_t maxY = 0;

    // calculate the max viewable range
    for (Tile* tile : tileList) {
        const Position& tilePos = tile->getPosition();

        uint32_t diff = Position::getDistanceX(tilePos, pos);
        if (diff > maxX) {
            maxX = diff;
        }

        diff = Position::getDistanceY(tilePos, pos);
        if (diff > maxY) {
            maxY = diff;
        }
    }

    const int32_t rangeX = maxX + Map::maxViewportX;
    const int32_t rangeY = maxY + Map::maxViewportY;
    g_game.map.getSpectators(list, pos, true, true, rangeX, rangeX, rangeY, rangeY);

    for (Tile* tile : tileList) {
        if (canDoCombat(caster, tile, params.aggressive) != RETURNVALUE_NOERROR) {
            continue;
        }

        if (params.impactEffect != CONST_ME_NONE) {
            Game::addMagicEffect(list, tile->getPosition(), params.impactEffect);
        }

        combatTileEffects(list, caster, tile, params);
    }
}
I placed it right after 'void Combat::CombatFunc' and before 'void Combat::doCombat'. You will need to declare this function in combat.h:
C++:
static void drawEffects(Creature* caster, const Position& pos, const AreaCombat* area, const CombatParams& params);
I placed it after 'void circleShapeSpell'.

Next, comment out the call to combatTileEffects toward the 'bottom of void Combat::CombatFunc', and call your new function before the very last curly bracket:
C++:
for (Tile* tile : tileList) {
        if (canDoCombat(caster, tile, params.aggressive) != RETURNVALUE_NOERROR) {
            continue;
        }

//        combatTileEffects(list, caster, tile, params); originally area effects were drawn here, but this is after the target takes damage
 
        if (CreatureVector* creatures = tile->getCreatures()) {
            const Creature* topCreature = tile->getTopCreature();
            for (Creature* creature : *creatures) {
                if (params.targetCasterOrTopMost) {
                    if (caster && caster->getTile() == tile) {
                        if (creature != caster) {
                            continue;
                        }
                    } else if (creature != topCreature) {
                        continue;
                    }
                }

                if (!params.aggressive || (caster != creature && Combat::canDoCombat(caster, creature) == RETURNVALUE_NOERROR)) {
                    func(caster, creature, params, data);
                    if (params.targetCallback) {
                        params.targetCallback->onTargetCombat(caster, creature);
                    }

                    if (params.targetCasterOrTopMost) {
                        break;
                    }
                }
            }
        }
    }
    drawEffects(caster, pos, area, params); //new function called here, this runs before the target takes damage
}

This is only half of the battle. If you do this fix, area spells like energy strike, energy beam, monster AoE spells will have the correct effect order, but targeted attacks, like LMM, HMM, SD, etc, will still have the wrong order.

For targeted attacks, the magic effect is passed in to this function here:
C++:
bool Combat::doCombatHealth(Creature* caster, Creature* target, CombatDamage& damage, const CombatParams& params)
{
    bool canCombat = !params.aggressive || (caster != target && Combat::canDoCombat(caster, target) == RETURNVALUE_NOERROR);
    if ((caster == target || canCombat) && params.impactEffect != CONST_ME_NONE) {
        g_game.addMagicEffect(target->getPosition(), params.impactEffect); //<--distance effect originally drawn right here, this is too late
    }

    if (canCombat) {
        if (caster && params.distanceEffect != CONST_ANI_NONE) {
            addDistanceEffect(caster, caster->getPosition(), target->getPosition(), params.distanceEffect);
        }

        canCombat = CombatHealthFunc(caster, target, params, &damage);
        if (params.targetCallback) {
            params.targetCallback->onTargetCombat(caster, target);
        }
    }

    return canCombat;
}
Comment out the call to draw the effect in that function: g_game.addMagicEffect(target->getPosition(), params.impactEffect);
And place it instead here in 2 places, within the if statement (we only want to send the magic effect if there is a magic effect, or the Cip client will crash when a player uses a wand/rod for example, the server sends effect 0 to the client, we also want to send the effect even if the attack is blocked by armor):

C++:
bool Combat::CombatHealthFunc(Creature* caster, Creature* target, const CombatParams& params, CombatDamage* data)
{
    assert(data);
    CombatDamage damage = *data;

    if (damage.value == 0) {
        damage.value = normal_random(damage.min, damage.max);
    }

    if (damage.value < 0 && caster) {
        Player* targetPlayer = target->getPlayer();
        if (targetPlayer && caster->getPlayer()) {
            damage.value /= 2;
        }
    }

    if (g_game.combatBlockHit(damage, caster, target, params.blockedByShield, params.blockedByArmor, params.itemId != 0)) {
            if (params.impactEffect > 0) {
                  g_game.addMagicEffect(target->getPosition(), params.impactEffect); //we also want to still draw the effect even if the attack doesnt do damage, i.e. a beholder using their weak SD attack, it is often blocked by the player's shield
             }
        return false;
    }
 
    if (g_game.combatChangeHealth(caster, target, damage)) { //this is the exact spot in game.cpp where it does damage and makes the effect   
         if (params.impactEffect > 0) {
              g_game.addMagicEffect(target->getPosition(), params.impactEffect);//move drawing the end effect here so that it happens BEFORE the damage, but only if it receives an impact effect
        }
        CombatConditionFunc(caster, target, params, &damage);
        CombatDispelFunc(caster, target, params, nullptr);
    }
 
    return true;
}
That's it. Compile and run. Screenshots of before/after:
View attachment 80685
how fix in tfs 1.3?
 
@wizinx for me it was solved with the first.code posted , check it
Edit: @Crevasse @X X X do you guys have. The code in order to make poison hit like in old-school so if player steps a poison field first step would hit 5 hit points and the next ones only 1?
 
Last edited:
Back
Top