Написание сервера для lineage 2 chronicle 1 на node.js

* Добавлен функционал планировщика (Scheduler)
* Удаление персонажей по запланированным задачам

Удаление персонажа добавлено как тестовое. Планировщик понадобится позже.

Схема работы планировщика Untitled diagram _ Mermaid Chart-2025-07-30-174041.webp
 

* Добавлен функционал планировщика (Scheduler)
* Удаление персонажей по запланированным задачам

Удаление персонажа добавлено как тестовое. Планировщик понадобится позже.

Схема работы планировщикаПосмотреть вложение 88248
Аж в голове закружилось от стрелочек))
 
* Добавлен функционал планировщика (Scheduler)
* Удаление персонажей по запланированным задачам

Удаление персонажа добавлено как тестовое. Планировщик понадобится позже.

Схема работы планировщика
<намалеванная схема>
Какие вы проблемы собираетесь решать используя такой планиривщик?

Почему вы выбрали такой подход для решения проблем удаления персонажей? Можно например использовать отдельную таблицу в БД гдe обозначается дата в epoch time где и когда удалять персонажи, или же отдельное значение в уже использованной таблице. Ведь на самом деле не нужно использовать таймеры или там все что связнно с временем (setTimeout/setInterval), а можно удалять персонажи когда запрашивается список активных персонажей. То есть простая проверка на время, типа сравнить epoch time и просто не показывать, а удалять можно отдельно что-бы не было синхронной связки с соединением клиента (то есть обработка пакетов не страдала от каких-то операций в БД).
 
Какие вы проблемы собираетесь решать используя такой планиривщик?

Почему вы выбрали такой подход для решения проблем удаления персонажей? Можно например использовать отдельную таблицу в БД гдe обозначается дата в epoch time где и когда удалять персонажи, или же отдельное значение в уже использованной таблице. Ведь на самом деле не нужно использовать таймеры или там все что связнно с временем (setTimeout/setInterval), а можно удалять персонажи когда запрашивается список активных персонажей. То есть простая проверка на время, типа сравнить epoch time и просто не показывать, а удалять можно отдельно что-бы не было синхронной связки с соединением клиента (то есть обработка пакетов не страдала от каких-то операций в БД).
* Управление включение, выключение, перезагрузка сервера
* Осады

Промежуточный тест

NPC(AI, Attack, Follow, Проверка на смерть персонажа)

 
как я понял, анимация атаки начинается после определенного временного интервала. например чтобы пробежать расстояние до цели. но что если во время движения изменится скорость бега? ( например бафнут вв. или наоборот дебафнут замедло). Этот таймер атаки собьется ? или задача на атаку будет отменена и пересчитана по новой?
 
как я понял, анимация атаки начинается после определенного временного интервала. например чтобы пробежать расстояние до цели. но что если во время движения изменится скорость бега? ( например бафнут вв. или наоборот дебафнут замедло). Этот таймер атаки собьется ? или задача на атаку будет отменена и пересчитана по новой?
Анимация атаки начинается сразу как состояние станет attack и дистанция будет короткой.

При назначении состояния атака потом идет проверка а далеко ли цель? Если далеко то переключаемся на состояние follow.
Это ответ на вопрос когда идет атака.

Вопрос про что будет при баффе и т.д.
Когда идет состояние follow то каждые 100мс идет перестройка маршрута.
Если цель стоит на месте то изменение маршрута отсутствует так как координаты всегда одинаковые.
Если же цель перемещается то каждые 100мс мы будем получать новые координаты и строить новый маршрут для игрока.
Это касается и остальных стат. Каждые 100мс будет проверка а не поменялись ли статы. Это пока не реализовано но будет.

Когда расстояние будет минимальное то follow переключится на attack.
 
Анимация атаки начинается сразу как состояние станет attack и дистанция будет короткой.

При назначении состояния атака потом идет проверка а далеко ли цель? Если далеко то переключаемся на состояние follow.
Это ответ на вопрос когда идет атака.

Вопрос про что будет при баффе и т.д.
Когда идет состояние follow то каждые 100мс идет перестройка маршрута.
Если цель стоит на месте то изменение маршрута отсутствует так как координаты всегда одинаковые.
Если же цель перемещается то каждые 100мс мы будем получать новые координаты и строить новый маршрут для игрока.
Это касается и остальных стат. Каждые 100мс будет проверка а не поменялись ли статы. Это пока не реализовано но будет.

Когда расстояние будет минимальное то follow переключится на attack.
А логику поведения боя, с птс переносил?
 
С какой версии птс?
 
А логику поведения боя, с птс переносил?
Что вы считаете логикой боя? Я понимаю что это встречный вопрос. Но в принципе то какая разница между поведением боя на ПТС и того что описал автор темы?
 
Что вы считаете логикой боя? Я понимаю что это встречный вопрос. Но в принципе то какая разница между поведением боя на ПТС и того что описал автор темы?
Именно то и считаю, что и написал.
Автор темы, фактически все понимает да и отвечает по факту вопроса (П.с. в разных версиях Л2, есть отличия поведения, но так как ПТС эти в шаре - то и эта инфа есть).

default AI, уникальные AI + база ко всему этому.
 
У игровых объектов в игре, есть такая штука, как hit_time_factor, которая определяет, в какой момент времени наносится непосредственно хит на сервере. У многих мобов и разного типа оружия он отличается от дефолтного 0.6
Это важный момент, на который практически все забивают)
 
Анимация атаки начинается сразу как состояние станет attack и дистанция будет короткой.

При назначении состояния атака потом идет проверка а далеко ли цель? Если далеко то переключаемся на состояние follow.
Это ответ на вопрос когда идет атака.

Вопрос про что будет при баффе и т.д.
Когда идет состояние follow то каждые 100мс идет перестройка маршрута.
Если цель стоит на месте то изменение маршрута отсутствует так как координаты всегда одинаковые.
Если же цель перемещается то каждые 100мс мы будем получать новые координаты и строить новый маршрут для игрока.
Это касается и остальных стат. Каждые 100мс будет проверка а не поменялись ли статы. Это пока не реализовано но будет.

Когда расстояние будет минимальное то follow переключится на attack.
просто я прочитал вашу статью на хабре и видимо думал что на сервере также сделано. там код вида

JavaScript:
const distance = 1500;
const playerSpeed = 126;
const ticks = distance / playerSpeed; // 11.90
const time = ticks * 1000; // 11900mc

setTimeout(() => {
   player.attack(npc);
}, time);

теперь все понятно)
 
просто я прочитал вашу статью на хабре и видимо думал что на сервере также сделано. там код вида

JavaScript:
const distance = 1500;
const playerSpeed = 126;
const ticks = distance / playerSpeed; // 11.90
const time = ticks * 1000; // 11900mc

setTimeout(() => {
   player.attack(npc);
}, time);

теперь все понятно)
Статья описывала старый подход.
Код там не главное. Главный посыл статьи был про то, что надо учитывать скорость ходьбы и бега. В новом подходе я не учитываю скорость ходьбы и все синхронизируется идеально с клиентом и сервером.

В статье это псевдокод для примера.
 
Судя по видео, мобы бьют моментально в ответку. На оригинальном сервере так не бывает, сначала моб должен "раздуплиться" :)
 
Судя по видео, мобы бьют моментально в ответку. На оригинальном сервере так не бывает, сначала моб должен "раздуплиться" :)
А где можно посмотреть про такое раздупление ? Или же почитать? Мне интересно.
 
А где можно посмотреть про такое раздупление ? Или же почитать? Мне интересно.
В наске. Каждое событие в ИИ запускает следующий таймер с определенным таймаутом. Например функция CNPC::AsyncTimerExpired1
Конкретно на атаку таймаут 1000
C++:
case PS_ATTACK:
      v117 = *(_DWORD *)(*((_QWORD *)NtCurrentTeb()->Reserved1[11] + tls_index) + 824736i64);
      v42 = v117;
      v83 = GuardHelper::_callStackDepth[v117]++;
      GuardHelper::_pszCallStack[v42][v83] = _FUNC_NAME___1074;
      GuardHelper::_dwCallStackStartTick[v42][v83] = GetTickCount();
      nLevelDiff = 0;
      if ( (d.m_nTargetID & 0xF8000000) >> 27 == 9 )
      {
        m_nTargetID = d.m_nTargetID;
        v62 = CSmartIdManager<CCreature,120000,8>::GetObjectW(&g_CreatureSmartIdManager, d.m_nTargetID);
        if ( !v62 )
        {
          Attack = CDesireFactory::MakeAttack(&v238, d.m_nTargetID, 0);
          CDesirePQ::RemoveKey(&this->m_DesirePQ, Attack, 1);
          --GuardHelper::_callStackDepth[v42];
          goto LABEL_291;
        }
        this->m_data->m_pTarget = v62->m_data;
        if ( v62 )
          v154 = v62->m_data;
        else
          v154 = 0i64;
        this->top_desire_target = v154;
        nLevelDiff = v62->m_data->m_nLevel - this->m_data->m_nLevel;
        if ( !(unsigned int)CNPC::dPAttack(this, v62->m_data, d.m_nMethod, d.m_amt, d.m_bForce) )
        {
          this->m_nTimerDelay = 1000;
          v17 = CDesireFactory::MakeAttack(&v239, d.m_nTargetID, nLevelDiff);
          CDesirePQ::RemoveKey(&this->m_DesirePQ, v17, 1);
          AttackFinishedEvent = CNPCEventFactory::CreateAttackFinishedEvent(this, v62->m_data);
          if ( AttackFinishedEvent )
          {
            this->HandleEvent(this, AttackFinishedEvent);
            LOBYTE(bForcea) = 0;
            AttackFinishedEvent->Release(AttackFinishedEvent, "NPC.cpp", 5144, ORT_CREATE_DELETE, bForcea);
          }
          else
          {
            LODWORD(bForcea) = 5148;
            CLog::Add(&Log, LOG_ERROR, (wchar_t *)L"[%s][%d] CreateAttackFinished Failed", L"NPC.cpp", bForcea);
          }
        }
        if ( CSharedCreatureData::IsSummon(this->m_data) )
          this->m_nTimerDelay = 500;
        if ( this->m_nAttackTick > 150 && this->m_nAttackTick < 500 )
          this->m_nTimerDelay = this->m_nAttackTick;
        if ( !d.m_bBoost )
          CDesirePQ::BoostUp(&this->m_DesirePQ, &d);
        if ( GetTickCount() > d.m_nTime )
        {
          v18 = CDesireFactory::MakeAttack(&v240, d.m_nTargetID, nLevelDiff);
          CDesirePQ::RemoveKey(&this->m_DesirePQ, v18, 1);
        }
      }
      else if ( (d.m_nTargetID & 0xF8000000) >> 27 == 11 )
      {
        v61 = d.m_nTargetID & 0xFFFFF;
        v214 = CSharedFactory<CStaticObject,CSharedStaticObjectData,CSharedStaticObjectConstant,&CSharedDefine const SharedDefine_StaticObject>::Inst();
        if ( v61 <= -1 || v61 >= SharedDefine_StaticObject.max_data )
          v155 = 0i64;
        else
          v155 = &v214->m_data[v61];
        spTarget = v155;
        if ( !v155 )
        {
          v19 = CDesireFactory::MakeAttack(&v241, d.m_nTargetID, nLevelDiff);
          CDesirePQ::RemoveKey(&this->m_DesirePQ, v19, 1);
          --GuardHelper::_callStackDepth[v42];
          goto LABEL_291;
        }
        this->m_data->m_pTarget = 0i64;
        this->top_desire_target = 0i64;
        if ( !(unsigned int)CNPC::dStaticObjectAttack(this, spTarget, d.m_nMethod, d.m_amt, d.m_bForce) )
        {
          this->m_nTimerDelay = 1000;
          v20 = CDesireFactory::MakeAttack(&v242, d.m_nTargetID, nLevelDiff);
          CDesirePQ::RemoveKey(&this->m_DesirePQ, v20, 1);
        }
        if ( CSharedCreatureData::IsSummon(this->m_data) )
          this->m_nTimerDelay = 400;
        if ( !d.m_bBoost )
          CDesirePQ::BoostUp(&this->m_DesirePQ, &d);
        if ( GetTickCount() > d.m_nTime )
        {
          v21 = CDesireFactory::MakeAttack(&v243, d.m_nTargetID, nLevelDiff);
          CDesirePQ::RemoveKey(&this->m_DesirePQ, v21, 1);
        }
      }
      --GuardHelper::_callStackDepth[v42];
LABEL_291:
      CTimer::EndJob(&g_Timer, v145);
      if ( this->m_nTimerDelay > 0 )
        CNPC::AddTimer(this, this->m_nTimerDelay, 0);
      --GuardHelper::_callStackDepth[v39];
      return;
 
В наске. Каждое событие в ИИ запускает следующий таймер с определенным таймаутом. Например функция CNPC::AsyncTimerExpired1
Конкретно на атаку таймаут 1000
C++:
this->m_nTimerDelay = 1000;
Тут много вопросов скорее как сама атака зависит от таймеров. Например, что случается после того как игрок впервые атаковал нпц? И зачем используется таймер как раз на одну секунду? Почему нe на больше? И что происходит при окончании таймаута?
 
Тут много вопросов скорее как сама атака зависит от таймеров. Например, что случается после того как игрок впервые атаковал нпц? И зачем используется таймер как раз на одну секунду? Почему нe на больше? И что происходит при окончании таймаута?
Если честно, я не готов пересказывать вам содержимое файла, размером в 50 мб чистого кода. Там огромное число всевозможных условий, которые начинают работать с момента, когда персонаж издал шум и вызвал у окружающих существ SEE_CREATURE. Мне, чтобы полностью воспроизвести всю эту логику на Java, потребовалось больше двух лет непрерывной работы. Там далеко не так все просто, как кажется. Ну разумеется, если целью является сделать что-то похожее на оригинальный сервер. В 99% сборок которые я видел, этим не заморачивались.
 
Назад
Сверху