фриз между выстрелом скила и перемещением

  • Автор темы Автор темы Hitcher
  • Дата начала Дата начала

Hitcher

Знаменитый
Местный
Старожил I степени
Сообщения
220
Розыгрыши
0
Репутация
13
Реакции
26
Баллы
1 295
Хроники
  1. Chaotic Throne: High Five
Исходники
Присутствуют
Сборка
jts
---------------------------------------------------------------
Здравствуйте Уважаемые! Помогите пожалуйста в расследовании странного фриза ~100-300мс.
---------------------------------------------------------------
=====================================
симптом: Фриз в моменте между окончанием каста + отложенной таске на перемещение.
=====================================
- При юзе луком даблшота например и клике откайтить назад. Фриз между окончанием каста и началом перемещения.
+ При юзе селф скила например rapid shot + таске на перемещение = никаких фризов.

Подобный фриз и у магов.
---------------------------------------------------------------
что было расследовано:
Серверная блокировка движения (isMovementDisabled()) работает корректно — снято с расследования.
Трейсы по 4 независимым прогонам (Double Shot, Burst Shot, Rapid Shot) показали стабильный результат: разница между CastEndTimeTask FIRED и moveNext() first unblocked — 1-2 мс во всех случаях, что укладывается в джиттер потоков (ThreadPoolExecutor/ScheduledThreadPool). Никакого "зависшего" флага (isAttackingNow, isCastingNow, отдельного каст-таймера) после clearCastVars() не найдено
=====================================
Параллельно похожий симптом наблюдается и при подборе хербов - фриз между поднятием и самим применением эффекта-изменением статов.
------------------------------------------------------------------------
Неделю пытаюсь логировать-поймать-понять, без результатов.
-------------------------------------------------------------------------
может на видео не сильно заметно, но по ощущениям прям заметно, особенно когда бежит парик мобов и каждая милисекнда на счету.
-------------------------------------------------------------------------
 
Это skill_cool_time/skillCoolTime (хз как это называется в файлах JTS .xml)

Чтобы объяснить подробнее: полоса времени каста, которую вы видите, это всего лишь визуальное отображение времени каста скилла, общее время каста немного больше (в зависимости от skill_cool_time).
 
Последнее редактирование:
скорее всего
просто в большинстве сборок про данный параметр в скиллах даже не в курсе и в итоге подобное поведение там где этот параметр задействован и учитывается, кажется глюком.

кстати в целом реализация поддержки cool_time минимума кода требует в сборке, ну кроме расписывания значений в самих скиллах
по сути в конец onMagicUseTimer просто докинуть типа такого
Java:
        int skillCoolTime = Various.calcCastSpeed(this, skill, skill.getCoolTime());

        if (skillCoolTime > 0)
            ThreadPoolManager.getInstance().schedule(new CastEndTimeTask(this, castType), skillCoolTime);
        else
            onCastEndTime(true, castType);
т.е. если есть cool_time то просто финализируем каст с нужной задержкой, а не сразу же после вызова callSkill и т.д.

---
но вот с хербами - это же чет другое и реально на глюк похоже или на специально введенную задержку между подбором и использованием скилла в хербе.
 
Последнее редактирование:
Спасибо ОГРОМНОЕ! С помощью Вашего снайперского прицеливания, удалось очень точно локализовать проблему и напрваить ИИшку в нужное русло)

- Специально усугубил кулдаун до 5000 для более явной и наглядной отладки.
После глубокого анализа, система подверглась серьезному рефакторингу:

Фриз движения после каста скиллов с coolTime — полный отчёт​

1. Симптом​

Фриз между окончанием каста и началом перемещения для скиллов лучника с cool_time(Double Shot, Burst Shot, Stunning Shot, Rapid Shot и т.д.). Self-скиллы без cool_time фриза не давали.

В процессе диагностики выявлены два независимых бага, оба вносящие вклад в один и тот же наблюдаемый симптом. Первый — блокировка движения самим флагом каста; второй — механизм переигровки отложенного клика движения. Ниже — оба, по порядку обнаружения, с обоснованием и итоговыми диффами.



2. Баг №1: разблокировка движения привязана к coolTime, а не к hitTime​

2.1 Механизм до фикса​


isMovementDisabled() → isCastingNow() → (_skillTask != null)

_skillTask обнулялся только в clearCastVars(), которая вызывалась из onCastEndTime(), а тот планировался в onMagicUseTimer() с задержкой, рассчитанной из coolTime скилла (через Formulas.calcMAtkSpd()), а не из времени, оставшегося на анимацию (hitTime). В результате движение оставалось заблокированным ещё и всё время coolTime сверх уже прошедшего hitTime.

Для self-скиллов coolTime == 0 → задержка почти нулевая → фриза не видно. Для Double Shot (coolTime=1000, промасштабирован атакспидом до ~1.1с) —фриз в ~1.1 секунды после реального конца анимации выстрела.

2.2 Проверка рисков перед фиксом​


ПроверкаРезультат
Другие потребители isCastingNow()PlayableAI, NpcAI, TradeHelper, Formulas.calcCastBreak, EnterWorld, RequestTargetCanceld, RequestTeleportBookMark, Freya AI — трогать нельзя
reuseDelay (когда скилл снова доступен)Независимый механизм skillReuses/TimeStamp, не связан с _skillTask — фикс безопасен
hitTime vs coolTimeПодтверждены как разные независимые поля Skill._hitTime / Skill._coolTime
Multi-hit скиллы (_scheduledCastCount)Реально существуют (Spell Force, id=427, castCount=10, — блокировка должна сохраняться на всю серию хитов

2.3 Решение​

Введён отдельный флаг _movementBlockedByCast, не завязанный на _skillTask. Снимается сразу по окончании hitTime (в onMagicUseTimer(), после ветки multi-hit reschedule — то есть только на финальном хите серии).isCastingNow() не тронут — все внешние потребители получают то же поведение, что и раньше.​

2.4 Диффы​

Java:
Поле (рядом с _castInterruptTime):

diff private long _castInterruptTime;
+private volatile boolean _movementBlockedByCast = false;
 private long _animationEndTime;
---------------------------------------------------------------------------------
doCast() — установка флага при старте каста:

diff     } else {
         _castInterruptTime = System.currentTimeMillis();
     }
+    _movementBlockedByCast = true;
 }
----------------------------------------------------------------------------------
onMagicUseTimer() — сброс флага после блока multi-hit reschedule
(критично: не раньше, иначе многохитовые скиллы разблокируют движение
на середине серии):

diff     if (_scheduledCastCount > 0) {
         _scheduledCastCount--;
         _skillLaunchedTask = ThreadPoolManager.getInstance().schedule(new MagicLaunchedTask(this, forceUse), _scheduledCastInterval);
         _skillTask = ThreadPoolManager.getInstance().schedule(new MagicUseTask(this, forceUse), _scheduledCastInterval);
+        return; // движение остаётся заблокированным до финального хита
     }
+
+    _movementBlockedByCast = false; // финальный хит — hitTime всей серии завершён, движение разрешено
-----------------------------------------------------------------------------------------
clearCastVars() — подстраховка на случай прерывания каста (стан/смерть/урон):

diff public void clearCastVars() {
     _animationEndTime = 0;
     _castInterruptTime = 0;
+    _movementBlockedByCast = false;
     _scheduledCastCount = 0;
----------------------------------------------------------------------------------------
Новый геттер:

diff public boolean isCastingNow() {
     return _skillTask != null;
 }
 
+public boolean isCastingBlockingMovement() {
+    return _movementBlockedByCast;
+}
-----------------------------------------------------------------------------------
isMovementDisabled() — замена компонента:

diff public boolean isMovementDisabled() {
-    return isBlocked() || isRooted() || isImmobilized() || isAlikeDead() || isStunned() || isSleeping() || isParalyzed() || isAttackingNow() || isCastingNow() || isFrozen();
+    return isBlocked() || isRooted() || isImmobilized() || isAlikeDead() || isStunned() || isSleeping() || isParalyzed() || isCastingBlockingMovement() || isAttackingNow() || isFrozen();
 }


2.5 Верификация (AspectJ LTW-логирование, фильтр по игроку Veronika)​


Прогон подтвердил корректность: _movementBlockedByCast сбрасывается в onMagicUseTimer() практически мгновенно после старта (задержка единицы мс), isMovementDisabled()=false наступает сразу после этого, задолго до onCastEndTime(). Multi-hit кейс (_scheduledCastCount > 0) в логах не триггерил преждевременный сброс — подтверждено логикой return до строки сброса.




3. Баг №2: отложенный клик движения (NextAction.MOVE) ждёт конца coolTime​


3.1 Симптом после фикса №1​


После патча №1 логи показывали, что _movementBlockedByCast иisMovementDisabled() становятся false сразу по окончании hitTime — но пользователь по-прежнему наблюдал полный фриз, если клик на движение был сделан во время каста. Если клик делался после окончания каста —движение срабатывало мгновенно, без всякой задержки на coolTime.

3.2 Диагностика по логам​


Клик во время каста → moveToLocation() возвращает false и не переигрывается вплоть до onCastEndTime() — то есть до конца полного coolTime, даже когда _movementBlockedByCast уже false за секунды до этого. Клик после каста (новый пакет от клиента) идёт напрямую в moveToLocation(), минуя очередь отложенных действий — и срабатывает сразу.

3.3 Найденный механизм​


Отклонённое движение сохраняется в PlayableAI._nextAction черезsetNextAction(NextAction.MOVE, ...). Переигровка выполняется методом setNextIntention(), который:

Java:
Проверяет actor.isActionsDisabled() — метод, использующий старый
isCastingNow() (не наш новый флаг):


java   public boolean isActionsDisabled() {
       return isBlocked() || isAlikeDead() || isFlashed() || isStunned() || isSleeping()
           || isParalyzed() || isAttackingNow() || isCastingNow() || isFrozen();
   }


Вызывается только из onEvtFinishCasting(), который триггерится
событием EVT_FINISH_CASTING, которое, в свою очередь, стреляет только
из onCastEndTime() (то есть в конце coolTime):


java   public void onCastEndTime(SkillEntry se) {
       finishFly(false);
       removeSkillMastery(se);
       clearCastVars();
       ThreadPoolManager.getInstance().execute(new NotifyAITask(this, CtrlEvent.EVT_FINISH_CASTING, se, getTarget(), true));
   }

Итог: даже с корректно работающим флагом движения, отложенный клик
физически не имеет шанса переиграться раньше конца coolTime, потому что
единственная точка replay жёстко привязана к onCastEndTime(), а не
к моменту фактической разблокировки движения.


3.4 Решение​


Два точечных изменения, не затрагивающих остальную логику isActionsDisabled()(ATTACK/CAST по-прежнему корректно ждут полного coolTime):

  1. Новый хук onMagicUseTimerFinished() в AbstractAI (no-op по умолчанию —безопасно для NPC/прочих AI), переопределённый в PlayableAI — вызывает setNextIntention() сразу по окончании hitTime.
  2. В setNextIntention() кейс MOVE проверяется по isMovementDisabled(), а не по общему isActionsDisabled() — остальные nextAction(ATTACK/CAST/и т.д.) не затронуты и продолжают ждать полный coolTime

  • 3.5 Диффы:
  • Java:
    AbstractAI.java — новый хук:
    
    javaprotected void onMagicUseTimerFinished() {
        // no-op по умолчанию для NPC AI и прочих реализаций
    }
    
    PlayableAI.java — переопределение:
    
    java@Override
    protected void onMagicUseTimerFinished() {
        setNextIntention();
    }
    
    PlayableAI.java, setNextIntention() — точечная развязка для кейса MOVE:
    
    diff public boolean setNextIntention() {
         final NextAction nextAction = _nextAction;
         // ... захват аргументов ...
         final Playable actor = getActor();
    
    -    if (nextAction == null || actor.isActionsDisabled()) {
    +    if (nextAction == null) {
             return false;
         }
    +
    +    // Для MOVE используем движенческий флаг, а не общий isActionsDisabled():
    +    // в момент coolTime движение уже разрешено, хотя новое действие (каст/атака) — ещё нет.
    +    if (nextAction == NextAction.MOVE) {
    +        if (actor.isMovementDisabled()) {
    +            return false;
    +        }
    +    } else if (actor.isActionsDisabled()) {
    +        return false;
    +    }
    
         switch (nextAction) {
             // ... без изменений ...
         }
         return true;
    }
    
    Creature.java, onMagicUseTimer() — сразу после сброса _movementBlockedByCast = false:
    
    java_movementBlockedByCast = false; // финальный хит — движение разрешено
    
    if (isPlayable()) {
        ThreadPoolManager.getInstance().execute(new RunnableImpl() {
            @Override
            protected void runImpl() {
                getAI().onMagicUseTimerFinished();
            }
        });
    }

3.6 Обоснование архитектурных решений​


РешениеОбоснование
Хук в AbstractAI + override в PlayableAI, а не прямой каст в CreatureРазделение слоёв: Creature не должен знать о PlayableAI-специфике; для NPC AI хук — no-op, нулевой риск
Асинхронный вызов через ThreadPoolManager.executemoveToLocation() использует moveLock.lock() (ReentrantLock); onMagicUseTimer() уже выполняется в ScheduledThreadPool. Синхронный вызов рискует дедлоком. Паттерн идентичен уже существующему NotifyAITask в onCastEndTime()
isActionsDisabled() не тронут глобальноВсе прочие потребители (ATTACK, CAST в setNextIntention(), вся AI-логика) продолжают корректно ждать полный coolTime — регрессии нет
Развязка сделана точечно только для case MOVEМинимизирует площадь изменения; риск ограничен исключительно движением, а не всей системой отложенных действий



4. Итоговая картина по обоим фиксам​



T=0 doCast() → _movementBlockedByCast=true
T=hitTime onMagicUseTimer():
_movementBlockedByCast=false (баг №1 — исправлено)
→ onMagicUseTimerFinished() (баг №2 — исправлено)
→ setNextIntention()
→ case MOVE: isMovementDisabled()==false → moveToLocation() выполняется
T=hitTime+coolTime onCastEndTime() → clearCastVars() → EVT_FINISH_CASTING
(для ATTACK/CAST — как и раньше)

Движение (в т.ч. отложенный клик, сделанный во время каста) теперь разблокируется сразу по окончании анимации (hitTime), а не после полного coolTime. Прочие типы отложенных действий (атака, повторный каст) и все остальные потребители isCastingNow()/isActionsDisabled() сохраняют прежнее поведение — блокировку на весь hitTime + coolTime.




5. Инструмент верификации​


Отладочный LTW-аспект MovementBlockDebugAspect.aj (privileged aspect, фильтр по игроку Veronika, — логирует все ключевые точки: doCast(), изменения _movementBlockedByCast, onMagicUseTimer(),clearCastVars(), isMovementDisabled() (с разбивкой по всем 9 компонентам), onCastEndTime(). Именно этим инструментом были получены все данные, легшие в основу диагностики обоих багов.
 
Последнее редактирование:

Ethernal

Баллов: 12
Если бы все игроки так излагали проблему..
Назад
Сверху Снизу