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: фикс не работает) выясняю дальше))) админ стал умирать реже но проблема все еще есть.
 
Весьма опрометчиво ставить синхронизацию на этот метод. Вы можете совершенно спокойно поймать дедлок.
 
После переименования метода и синхронизации - Вы ничего не исправите)
Делюсь самым минимальным фиксом за всю историю 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 должна это исправить.
 
Хз даже для чего этот режим, но почему бы просто не сделать проверку на статус админа, и запретить ему умирать в этом состоянии?
 
Сори за оверпостинг!) по другому не умею. 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го года
 
Что если дополнить проверку isAcis и не юзать ее на нормальных проектах. Она написана типа «умными» людьми, но в нормальных условиях обосрется со сборкой скории 2007го года
Божественный Свет — это бесконечная энергия Творца, изливающаяся в миры через сосуды. Сосуды не выдержали этого Света и разбились, породив хаос и рассеяв искры Божественности в материальном мире. Миссия человека — собирать эти искры через добрые дела, осознанность и духовную работу, восстанавливая утраченное единство с Божественным.
 
Назад
Сверху