Fix случайная смерть админа в режиме undying (acis 409)

Hitcher

Знаменитый
Местный
Сообщения
196
Розыгрыши
0
Репутация
1
Реакции
19
Баллы
1 280
Хроники
  1. Interlude
Исходники
Присутствуют
Сборка
acis 409
Делюсь самым минимальным фиксом за всю историю L2j)
***Специальная благодарность к разработчикам ИИ!)

Баг: случайная смерть админа в режиме undying.


DEBUG (reduceHp): --- Entering reduceHp for Veronika ---
DEBUG (reduceHp): Initial HP: 636.28, Damage value: 394.0
DEBUG (reduceHp): isMortal: false, isInvul: false, isDead: false
DEBUG (reduceHp): Applying damage to HP. Initial _hp: 1051.4, value to subtract: 394.0
DEBUG (reduceHp): HP after subtraction: 657.4000000000001
DEBUG (reduceHp): Final HP after setHp call: 657.4000000000001
DEBUG (reduceHp): Checking for death condition. Current _hp: 657.4000000000001
DEBUG (reduceHp): --- Exiting reduceHp for Veronika ---
DEBUG (reduceHp): --- Entering reduceHp for Veronika ---
DEBUG (reduceHp): Initial HP: 657.4000000000001, Damage value: 248.0
DEBUG (reduceHp): isMortal: false, isInvul: false, isDead: false
DEBUG (reduceHp): Applying damage to HP. Initial _hp: 657.4000000000001, value to subtract: 248.0
DEBUG (reduceHp): HP after subtraction: 409.4000000000001
DEBUG (reduceHp): Final HP after setHp call: 409.4000000000001
DEBUG (reduceHp): Checking for death condition. Current _hp: 409.4000000000001
DEBUG (reduceHp): --- Exiting reduceHp for Veronika ---
DEBUG (reduceHp): --- Entering reduceHp for Veronika ---
DEBUG (reduceHp): --- Entering reduceHp for Veronika ---
DEBUG (reduceHp): Initial HP: 409.4000000000001, Damage value: 162.0
DEBUG (reduceHp): isMortal: false, isInvul: false, isDead: false
DEBUG (reduceHp): Applying damage to HP. Initial _hp: 409.4000000000001, value to subtract: 162.0
DEBUG (reduceHp): HP after subtraction: 247.4000000000001
DEBUG (reduceHp): Initial HP: 409.4000000000001, Damage value: 247.0
DEBUG (reduceHp): isMortal: false, isInvul: false, isDead: false
DEBUG (reduceHp): Applying damage to HP. Initial _hp: 247.4000000000001, value to subtract: 247.0
DEBUG (reduceHp): Final HP after setHp call: 247.4000000000001
DEBUG (reduceHp): HP after subtraction: 0.40000000000009095
DEBUG (reduceHp): Checking for death condition. Current _hp: 247.4000000000001
DEBUG (reduceHp): Final HP after setHp call: 0.40000000000009095
DEBUG (reduceHp): _hp is less than 0.5. Initiating death sequence.
DEBUG (reduceHp): Checking for death condition. Current _hp: 0.40000000000009095
DEBUG (reduceHp): _hp is less than 0.5. Initiating death sequence.
DEBUG (reduceHp): Calling doDie for Veronika.
DEBUG (reduceHp): Calling doDie for Veronika.
DEBUG (reduceHp): --- Exiting reduceHp for Veronika ---

Найдите метод reduceHp в PlayerStatus.java
замените:
public final void reduceHp(double value, Creature attacker, boolean awake, boolean isDOT, boolean isHPConsumption, boolean ignoreCP)

на:
public final synchronized void reduceHp(double value, Creature attacker, boolean awake, boolean isDOT, boolean isHPConsumption, boolean ignoreCP)

Проблема: Состояние гонки (Race Condition) или одновременное нанесение урона.

Судя по логам, происходит следующее:

У Вероники было 409.40 HP.

На нее почти одновременно или очень быстро друг за другом действуют два источника урона.

Первый вызов reduceHp (урон 162.0):

Получает Initial HP: 409.40.

Вычисляет новое HP: 409.40 - 162.0 = 247.40.

Устанавливает HP в 247.40.

Второй вызов reduceHp (урон 247.0) - происходит параллельно или очень быстро:

Здесь кроется проблема: Лог Initial HP: 409.4000000000001, Damage value: 247.0 показывает, что этот второй вызов, возможно, начал работу с "устаревшим" значением HP (409.40), которое еще не было обновлено глобально первым вызовом reduceHp (или же произошла другая рассинхронизация).

Однако, лог Applying damage to HP. Initial _hp: 247.4000000000001, value to subtract: 247.0 показывает, что внутри этого второго вызова, когда дошло дело до применения урона, переменная _hp уже отражала результат первого вызова.

Это приводит к тому, что 247.40 - 247.0 = 0.40.

Поскольку 0.40 меньше 0.5, срабатывает условие if (_hp < 0.5), и вызывается _actor.doDie(attacker).

Почему не сработала логика "HP clamp to 1.0"?

Логика value = (_actor.isMortal()) ? 0 : 1; срабатывает только если value <= 0 до вызова setHp(value).
В случае с 0.40, это значение больше 0, поэтому условие value <= 0 не выполняется, и HP не "зажимается" до 1.0, а устанавливается как 0.40. Это и приводит к вызову doDie.

Решение: Синхронизация метода reduceHp

Чтобы избежать такого состояния гонки, вам нужно убедиться, что только один поток может изменять HP существа в любой момент времени. Этого можно достичь, сделав метод reduceHp синхронизированным.

Объяснение synchronized:

Когда метод reduceHp помечен как synchronized, это означает, что только один поток может выполнять этот метод для данного объекта (this - т.е. для конкретного экземпляра CreatureStatus или Creature) в любой момент времени.

Если несколько источников урона (несколько потоков) пытаются одновременно вызвать reduceHp для одной и той же "Вероники", только один из них сможет войти в метод, а остальные будут ждать в очереди, пока первый не закончит.

Это гарантирует, что _hp всегда будет актуальным и не будет никаких "устаревших" значений, которые могли бы привести к неправильным расчетам и обходу логики "бессмертия".

После этого изменения, когда HP админа достигнет значения 1.0 (или любого другого значения, которое приведет к value <= 0 после вычитания урона), логика Actor is IMMORTAL (isMortal=false), HP clamped to 1.0. будет всегда корректно срабатывать, и HP не опустится ниже 1.0, предотвращая вызов doDie.

Итог: Ваш баг вызван не тем, что логика бессмертия не работает, а тем, что несколько источников урона могут одновременно пытаться изменить HP, вызывая состояние гонки. Синхронизация метода reduceHp должна это исправить.
 
upd: фикс не работает) выясняю дальше))) админ стал умирать реже но проблема все еще есть.
 
Весьма опрометчиво ставить синхронизацию на этот метод. Вы можете совершенно спокойно поймать дедлок.
 
После переименования метода и синхронизации - Вы ничего не исправите)
 
Хз даже для чего этот режим, но почему бы просто не сделать проверку на статус админа, и запретить ему умирать в этом состоянии?
 
Сори за оверпостинг!) по другому не умею. Fix 0.2 проверил на парике из сотни мобов, и даже не сдох!=)
Изменения затронули два метода в двух классах:
Код:
Creature.java

    public boolean doDie(Creature killer)
    {
        // killing is only possible one time
        // ИЗМЕНЕНИЕ: Блок synchronized гарантирует, что только один поток
        // может выполнять критические операции смерти для этого существа одновременно.
        synchronized (this)
        {
           if (isDead())
           {
               return false;
           }

           // ИЗМЕНЕНИЕ: Это критически важная проверка.
           // Если актор не смертен (например, в режиме //undying),
           // то фактические шаги смерти (обнуление HP, установка флага isDead)
           // не должны выполняться. Это служит последним барьером безопасности.
           if (isMortal() == false)
           {
               // Возвращаем false, так как фактической смерти не произошло,
               // несмотря на попытку вызова doDie. Это важно для логики, которая
               // может зависеть от успешности вызова doDie.
               return false;
           }

           // now reset currentHp to zero
           getStatus().setHp(0);

           setIsDead(true);
        }

        // Abort attack, cast and move.
        abortAll(true);

        // Stop Regeneration task, and removes all current effects
        getStatus().stopHpMpRegeneration();
        stopAllEffectsExceptThoseThatLastThroughDeath();

        calculateRewards(killer);

        // Send the Server->Client packet StatusUpdate with current HP and MP to all other Player to inform
        getStatus().broadcastStatusUpdate();

        // Notify Creature AI
        getAI().notifyEvent(AiEventType.DEAD, null, null);

        return true; // Возвращаем true, так как смерть произошла (для смертных акторов)
    }



PlayerStatus.java

    // ИЗМЕНЕНИЕ: Добавление ключевого слова 'synchronized' к объявлению метода.
    // Причина и для чего: Это предотвращает состояния гонки, когда несколько источников урона
    // одновременно пытаются изменить HP существа, гарантируя,
    // что только один поток может выполнять этот метод для данного актора в любой момент времени.
    public final synchronized void reduceHp(double value, Creature attacker, boolean awake, boolean isDOT, boolean isHPConsumption, boolean ignoreCP)
    {
        if (_actor.isDead())
        {
            return;
        }

        // invul handling
        if (_actor.isInvul())
        {
            // other chars can't damage
            if (attacker != _actor)
            {
                return;
            }

            // only DOT and HP consumption allowed for damage self
            if (!isDOT && !isHPConsumption)
            {
                return;
            }
        }

        if (!isHPConsumption)
        {
            _actor.stopEffects(EffectType.SLEEP);
            _actor.stopEffects(EffectType.IMMOBILE_UNTIL_ATTACKED);

            // When taking a hit, stand up - except if under shop mode.
            if (_actor.isSitting() && !_actor.isInStoreMode())
                _actor.standUp();

            if (!isDOT && _actor.isStunned() && Rnd.get(10) == 0)
            {
                _actor.stopEffects(EffectType.STUN);

                // Refresh abnormal effects.
                _actor.updateAbnormalEffect();
            }
        }

        if (attacker != null && attacker != _actor)
        {
            final Player attackerPlayer = attacker.getActingPlayer();
            if (attackerPlayer != null && !attackerPlayer.getAccessLevel().canGiveDamage())
            {
                return;
            }

            if (_actor.isInDuel())
            {
                final DuelState playerState = _actor.getDuelState();
                if (playerState == DuelState.DEAD || playerState == DuelState.WINNER)
                {
                    return;
                }

                // Cancel duel if player got hit by another player that is not part of the duel or if player isn't in duel state.
                if (attackerPlayer == null || attackerPlayer.getDuelId() != _actor.getDuelId() || playerState != DuelState.DUELLING)
                {
                    _actor.setDuelState(DuelState.INTERRUPTED);
                }
            }

            int fullValue = (int) value;
            int tDmg = 0;

            // Check and calculate transfered damage, if any.
            final Summon summon = _actor.getSummon();
            if (summon instanceof Servitor && summon.isIn3DRadius(_actor, 900))
            {
                tDmg = (int) (value * calcStat(Stats.TRANSFER_DAMAGE_PERCENT, 0, null, null) / 100.);

                // Only transfer dmg up to current HP, it should not be killed
                tDmg = Math.min((int) summon.getStatus().getHp() - 1, tDmg);
                if (tDmg > 0)
                {
                    summon.reduceCurrentHp(tDmg, attacker, null);
                    value -= tDmg;
                    fullValue = (int) value; // reduce the announced value here as player will get a message about summon damage
                }
            }

            if (!ignoreCP && attacker instanceof Playable)
            {
                if (_cp >= value)
                {
                    setCp(_cp - value); // Set Cp to diff of Cp vs value
                    value = 0; // No need to subtract anything from Hp
                }
                else
                {
                    value -= _cp; // Get diff from value vs Cp; will apply diff to Hp
                    setCp(0, false); // Set Cp to 0
                }
            }

            if (fullValue > 0 && !isDOT)
            {
                _actor.sendPacket(SystemMessage.getSystemMessage(SystemMessageId.S1_GAVE_YOU_S2_DMG).addCharName(attacker).addNumber(fullValue));

                if (tDmg > 0 && attackerPlayer != null)
                    attackerPlayer.sendPacket(SystemMessage.getSystemMessage(SystemMessageId.GIVEN_S1_DAMAGE_TO_YOUR_TARGET_AND_S2_DAMAGE_TO_SERVITOR).addNumber(fullValue).addNumber(tDmg));
            }
        }

        if (value > 0)
        {
            // ИЗМЕНЕНИЕ: Используем новую переменную newHpValue для вычисления.
            // Причина и для чего: Это делает код более читаемым и предотвращает случайные изменения
            // входящего параметра 'value' до того, как он будет полностью обработан.
            double newHpValue = _hp - value;

            // ИЗМЕНЕНИЕ: Добавлена новая логика для ограничения HP бессмертных акторов.
            // Причина и для чего: Если актор не смертен (isMortal() == false) И вычисленное HP
            // опускается ниже 1.0 (но выше 0), то принудительно устанавливаем HP в 1.0.
            // Это гарантирует, что бессмертные существа всегда будут иметь минимум 1.0 HP
            // и не "умрут" из-за того, что их HP оказалось между 0 и 1.0.
            if (!_actor.isMortal() && newHpValue < 1.0)
            {
                newHpValue = 1.0;
            }
            
            // ИЗМЕНЕНИЕ: Пересмотренная логика обработки HP, когда оно опускается до 0 или ниже.
            // Причина и для чего: Этот блок теперь срабатывает после проверки бессмертия.
            // Он обрабатывает случаи, когда HP действительно становится 0 или меньше (для смертных)
            // или специфичную логику дуэлей. Для бессмертных акторов, HP уже будет 1.0,
            // поэтому это условие 'newHpValue <= 0' не будет для них истинным.
            if (newHpValue <= 0)
            {
                if (_actor.isInDuel())
                {
                    if (_actor.getDuelState() == DuelState.DUELLING)
                    {
                        _actor.disableAllSkills();
                        stopHpMpRegeneration();
                        DuelManager.getInstance().onPlayerDefeat(_actor);
                    }
                    newHpValue = 1; // В дуэли HP всегда 1 после поражения
                }
                else if (_actor.isMortal())
                {
                    newHpValue = 0;
                }
            }
            setHp(newHpValue); // Устанавливаем новое HP с учетом всех корректировок
        }

        // Handle die process if value is too low.
        // ИЗМЕНЕНИЕ: Теперь, благодаря новой логике ограничения HP, для бессмертных акторов
        // _hp никогда не будет меньше 0.5 (если оно не должно быть 0 для смертных),
        // что предотвращает ошибочный вызов doDie() для бессмертных.
        if (_hp < 0.5)
        {
            if (_actor.isInOlympiadMode())
            {
                // Abort attack, cast and move.
                _actor.abortAll(false);

                stopHpMpRegeneration();
                _actor.setIsDead(true); // Установка флага смерти

                final Summon summon = _actor.getSummon();
                if (summon != null)
                    summon.getAI().tryToIdle();

                return; // Выход из метода
            }

            _actor.doDie(attacker); // Вызов метода смерти (теперь защищен проверкой isMortal() внутри doDie)

            final QuestState qs = _actor.getQuestList().getQuestState("Tutorial");
            if (qs != null)
                qs.getQuest().notifyEvent("CE30", null, _actor);
        }
    }
Подскажите по синхронизации? лучше убрать?
 
Посмотреть вложение 88030

Это кандидаты на дедлоки я правильно понимаю?
Кандидатом на дедлок станет произвольный поток, который попробует вызвать этот метод, будучи самим вызванным из каких-то методов по стаку уходящих в этот метод. А внутри этого метода вызывается достаточно много всего, чтобы такая ситуация произошла с большой вероятностью.
 
ТС, почему просто не проверять в том же doDie флаг/свойство undying? или даже в isDead() дополнить проверку что толкьо если этого флага нет, то только тогда isDead может вернуть true.
без всяких синхронизаций/локов вобщем обойтись
 
Очень приятно, что настолько крутые ребята помогли админу не умереть - буквально спасли от смерти!) Благодарю вас!
Я обязательно испробую проверку на статус админа и еще вариант с doDie флаг/свойство undying. Все протестирую и отпишусь с результатами.
 
Что если дополнить проверку isAcis и не юзать ее на нормальных проектах. Она написана типа «умными» людьми, но в нормальных условиях обосрется со сборкой скории 2007го года
 
Божественный Свет — это бесконечная энергия Творца, изливающаяся в миры через сосуды. Сосуды не выдержали этого Света и разбились, породив хаос и рассеяв искры Божественности в материальном мире. Миссия человека — собирать эти искры через добрые дела, осознанность и духовную работу, восстанавливая утраченное единство с Божественным.
 
Данный сайт использует cookie. Вы должны принять их для продолжения использования. Узнать больше…