Квестовые методы в ПТС ИИ

Aristo

Величайший
Местный
Победитель в номинации 2023
Сообщения
521
Розыгрыши
0
Решения
8
Репутация
547
Реакции
594
Баллы
1 593
Хроники
  1. Chaotic Throne: High Five
Сборка
Свое, на базе L2GW
Всем привет.
Сначала суть:
Сборка Java.
Портировал я как-то давно себе ПТС ИИ. Код перенесен на JAVA структурно без изменений из декомпила ХФ ai.obj
Сейчас провожу у себя ЗБТ и столкнулся с некоторой проблемой.
У корейцев, есть два основных вида проверки и переключения статуса квеста. Буду писать псевдокодом, чтобы было более понятно, в чем суть проблемы.
Пример: Для переключения квеста с 1 на 2 этап, нужно 10 итемов с ИД 9999. В рюкзаке 9 итемов, убиваем моба. У моба обработчик:
Вариант кода 1.
Код:
if (getItemCount(9999) == 9){
   giveItem(9999, 1);
   changeQuestState(2);
}
1679788928506.png
Вариант кода 2.
Код:
giveItem(9999, 1);
if(getItemCount(9999) >= 9){
  changeQuestState(2);
}
1679788882455.png
Оба варианта встречаются в ИИ примерно поровну. Первый вариант чаще в новых ИИ, от ГФ и выше, а второй преимущественно в старых.
Методы в коде вызываются одинаковые. Скорее всего, во втором варианте заложена задержка на передачу инфы туда обратно между NpcServer и Server,
но из-за этого, если я у себя на яве, обрабатываю вызов giveItem в том же потоке, то получаю ситуацию, когда у меня моб закрывает квест на 9 итемах, т.к условие выполняется. Если я выношу обработчик giveItem в отдельный поток, то иногда возникают ситуации, когда мобы выдают лишние итемы, т.е у чара получается 11/10 итемов, а т.к в тех же скриптах проверки в условиях по большей части не >=, а ==, то такие квесты не получится сдать.
Кто-то сталкивался с подобным? Править скрипты очень не хочу, т.к с датапаком ПТС работаю нативно и очень бы не хотелось его править.
Есть идеи, как это можно обойти?
 
Решение
это L2NPC.
похоже, что это Queue атомарных исполняемых операций, которая связана с CScriptEngine, которая затем вызывает функцию Process, которая выполняет операции в Queue.
Из-за того, что GiveItem должен запускаться таском RunNpcScript -> Process, он будет (почти?) всегда запускаться после OwnItemCount.
Не, ну тут консилиум нужно задействовать...
Почему же просто не добавлять ко всем квестовым значениям 1?
Было
giveItem(9999, 1); if(getItemCount(9999) >= 9){ changeQuestState(2); }
Стало
Код:
giveItem(9999, 1); if(getItemCount(9999) >= 10){ changeQuestState(2); }
 
Не, ну тут консилиум нужно задействовать...
Почему же просто не добавлять ко всем квестовым значениям 1?
Было

Стало
Код:
giveItem(9999, 1); if(getItemCount(9999) >= 10){ changeQuestState(2); }
Это плохая идея, т.к ИИ примерно 13000 и переписывать код каждого, со второго метода на первый немного затратно по времени.
Кроме того, там довольно много частных случаев, когда уместно использовать именно второй вариант кода, т.к там есть несколько вложенных проверок.
Мой вопрос не в том, как изменить код скрипта ИИ.
В голове у меня пока единственное решение созрело. Это собрать инфу по каждому не повторяющемуся квесту, с инфой о количестве итемов для сдачи и захардкодить в метод giveItem проверку на предельно допустимое количество итемов на каждый квест, но это крайние меры...
 
перед каждой проверкой брать колличество итемов у персонажа, потом добавлять 1 и проверять по прошлому колличеству.
Java:
long count = getQuestCount(9999);
giveItem(9999, 1);
if (count >= 9)
{
   do something;
}
Или же сделать универсальный метод в квестах, который будет возвращать значерие сразу с -1
Java:
public long getPtsScriptCount(Player player, int itemId)
{
   return player.getInventory().getItemById(itemId, -1) - 1;
}
 
перед каждой проверкой брать колличество итемов у персонажа, потом добавлять 1 и проверять по прошлому колличеству.
Java:
long count = getQuestCount(9999);
giveItem(9999, 1);
if (count >= 9)
{
   do something;
}
Или же сделать универсальный метод в квестах, который будет возвращать значерие сразу с -1
Java:
public long getPtsScriptCount(Player player, int itemId)
{
   return player.getInventory().getItemById(itemId, -1) - 1;
}
Спасибо, но к сожалению, ваше решение не подойдет, т.к в этот же метод используется в обеих версиях кода и он должен отдавать предсказуемое значение. Проблема в том, что количество итемов на момент проверки корректно, с точки зрения скрипта, т.к выдача итема и проверка его количества не идут строго друг за другом во времени.
К тому же, это все опять же изменения кода скрипта, которых я хочу избежать.
 
Я пытался понять чётам у ТСа, но так до конца и не всосал.

Больше всего это попахивает тем, что GiveItem \ OwnItem просто не успевают друг за другом, отсюда и возникают проблемы, что оно даёт 1 лишний предмет. Это по сути одна проблема - он пропускает последний, но замечает на следующем. По крайней мере это выглядит так.

А ещё учтём, на всякий случай, что АИ птса выполняется асинхронно и оба примера это шило на мыло с одной сутью и исходом, где если у тебя все методы отрабатываются вовремя и возвращают корректные значения - всё работает.
 
АИ птса выполняется асинхронно.
Именно в этом моя проблема.
Если я выполняю их у себя на яве синхронно, то косячат ИИ, которые сначала выдают итем авансом, а потом проверяют его количество ниже по коду.
Если я выполняю их у себя на яве асинхронно, то косячат ИИ, которые сначала проверяют количество строгой проверкой, а потом выдают финальный и переключают квест. У меня есть подозрение, что ПТС каким-то образом знает предельное количество итемов для каждого квеста, но это параноя уже, т.к в скриптах я ничего похожего не нашел.
Кстати, возможно стоит убрать задержку исполнения полностью и исполнять мгновенно, но просто в отдельном потоке. Как раз время на его создание и даст ту минимальную задержку, которая нужна.

оба примера это шило на мыло с одной сутью и исходом, где если у тебя все методы отрабатываются вовремя и возвращают корректные значения - всё работает.
1679793528404.png
Вот пример кода с ИИ.
По квесту, требуется 5 итемов. Это квест shards_of_golem за номером 152.
При обработке MY_DYING для голема, он выдает итем 1010, а после этого проверяет его количество.
Если выдавать последовательно, в одном потоке, то проверка в данном коде сработает на переходе с 3 на 4, и отправит в клиент некорректное переключение журнала квеста.
Если зашедулить выдачу итема в другом потоке, то на момент проверки, итем еще не будет выдан и проверка с 3 на 4 не сработает, а сработает только в следующий раз, при переходе с 4 на 5.
Но
Но если, я в другом квесте, где в коде выдача итема идет ПОСЛЕ проверки или ВНУТРИ нее, я убью сразу 2-3-4-5+ мобов(например АОЕ), то каждый из них зашедулит выдачу итема и на момент последней проверки у меня будет не 5 итемов, а 10, что сделает сдачу квеста невозможной, т.к НПЦ хочет строго 5.
1679794024533.png
вот пример, где моб при смерти чекает что итемов ровно 24, выдает последний итем и переключает квест.

У меня есть такое соображение насчет этого.
NASC исполняется на NPC-сервере.
Скорее всего, OwnItemCount посылает запрос на Server и ждет ответ, а после его получения, продолжает свое выполнение. Т.е мы не проваливаемся ниже, пока не получим ответ от Server, который хоть и быстрый, но не мгновенный.
А метод GiveItem1, не ждет ответ от Server, а просто отдаем ему команду. После этого вызов метода завершается и мы переходим к следующему участку кода. Т.е с точки зрения NPC сервера, между вызовами GiveItem1 и OwnItemCount нет паузы и вот код выше, где сначала идет выдача, а после нее проверка, подтверждает мои догадки и скорее всего корейцы знали об этом и учитывали это при написании кода. В моем случае, я пытаюсь понять, как мне усидеть на двух стульях, т.к если я эмулирую задержку выдачи для GiveItem1, то у меня все отлично работает, но периодически случается ситуации, когда мобы выдали итемов больше чем нужно.
 
одно из решений - не выдавать квестовые предметы чаще чем какой-то определенный период.
т.е. при успешной выдаче ставить метку времени, а так же запоминать ид последнего выданного предмета.
ну и проверять это при попытке выдачи - если прошло слишком мало времени и предмет такой же, то не выдавать еще один предмет.

минус способа конечно в том что к примеру на париках мобов в итоге игроки не смогут быстро набивать предметы, т.е. условно с кучи одномоментно убитых мобов получат предметов столько же сколько с одного.
 
одно из решений - не выдавать квестовые предметы чаще чем какой-то определенный период.
т.е. при успешной выдаче ставить метку времени, а так же запоминать ид последнего выданного предмета.
ну и проверять это при попытке выдачи - если прошло слишком мало времени и предмет такой же, то не выдавать еще один предмет.

минус способа конечно в том что к примеру на париках мобов в итоге игроки не смогут быстро набивать предметы, т.е. условно с кучи одномоментно убитых мобов получат предметов столько же сколько с одного.
Кстати, мне тут пришла в голову мысль, что возможно оба эти метода обрабатываются по определенным серверным тикам, и GiveItem1 возможно шедулит не прямую выдачу, а добавляет итемы в очередь выдачи, а они потом обрабатываются одним тиком и собственно исполнение OwnItemCount может так же ждать ответа, который придет на следующий тик. Возможно, это причина, по которой на ПТС квестовые итемы выдаются и обрабатываются с некоторой задержкой даже на селфхостед.
Но это все равно не объясняет, почему не выдаются лишние итемы на ПТС)))
 
Вообще асинхронная выдача айтемов это прямой путь к дюпам.
 
Кстати, мне тут пришла в голову мысль, что возможно оба эти метода обрабатываются по определенным серверным тикам, и GiveItem1 возможно шедулит не прямую выдачу, а добавляет итемы в очередь выдачи, а они потом обрабатываются одним тиком и собственно исполнение OwnItemCount может так же ждать ответа, который придет на следующий тик. Возможно, это причина, по которой на ПТС квестовые итемы выдаются и обрабатываются с некоторой задержкой даже на селфхостед.
Но это все равно не объясняет, почему не выдаются лишние итемы на ПТС)))
толи в хфе, но 100 % в годе они выдаются моментально.
 
Похоже, вызов GiveItem1 является атомарным, а вызов OwnItemCount основан на потоке.
1679844538573.png 1679844605183.png
 
  • Мне нравится
Реакции: Aristo

    Aristo

    Баллов: 14
    Хм. Спасибо. Этот код дал мне еще пару ответов на другие мои не высказанные вопросы)
P.S. Судите по "JobCompositor", может быть, он задерживается?
 
P.S. Судите по "JobCompositor", может быть, он задерживается?
AtomicJobCompositor это класс Server.exe или из Npc?

На текущий момент, в качестве предварительного решения я у себя вместо шедула Runnable на 50мс, сразу его исполняю, но в параллельном потоке, через ThreadPoolManager и его фабрики. По предварительным тестам все ок, но чет я переживаю, что на лагах вылезет у меня чет забавное)
 
это L2NPC.
похоже, что это Queue атомарных исполняемых операций, которая связана с CScriptEngine, которая затем вызывает функцию Process, которая выполняет операции в Queue.
Из-за того, что GiveItem должен запускаться таском RunNpcScript -> Process, он будет (почти?) всегда запускаться после OwnItemCount.
 
Решение
это L2NPC.
похоже, что это Queue атомарных исполняемых операций, которая связана с CScriptEngine, которая затем вызывает функцию Process, которая выполняет операции в Queue.
Из-за того, что GiveItem должен запускаться таском RunNpcScript -> Process, он будет (почти?) всегда запускаться после OwnItemCount.
Да. Я пришел к примерно таким же выводам. Спасибо за информацию, она очень полезна.
Тему можно закрывать, вектор работ понятен. Вопросов к комьюнити больше не имею)
 
Так же отмечу, что у каждого ИИ нпс есть отдельный поток(или есть несколько тредов и там распределены нпсы, я пока не смог разобраться), при убийстве нпс с помощью AOE они не умирают одновременно.
 
Так же отмечу, что у каждого ИИ нпс есть отдельный поток(или есть несколько тредов и там распределены нпсы, я пока не смог разобраться), при убийстве нпс с помощью AOE они не умирают одновременно.
Ну на яве я скорее всего не смогу выделить поток на НПЦ(если конечно виртуальные поток не релизнут на 21 LTS из превью, в чем я сомневаюсь), поэтому ИИ у меня обрабатываются отдельной фабрикой на несколько потоков. При АОЕ смерть наступает не одновременно, но в пределах обработки одного цикла по таргетам в таске скилла. Т, е фактически, одновременно с точки зрения очередности объявления события MY_DYING
 
Ну на яве я скорее всего не смогу выделить поток на НПЦ
Это и не нужно. Приложение не должно создавать потоков больше, чем ядер процессора (x2 если у процессора есть гипертрединг или аналог). Все что больше – уменьшает производительность на переключении контекста и бессмысленных локах. Если коротко – тяжелый поток на любую шляпу = один из худших видов говнокода, потому что сочетает в себе как косорылого "программиста", так и такую сложную для дебага тему как многопоточность.

Для максимальной многопоточной производительности используют пулы потоков, отдельные очереди на каждый поток, work stealing и т.д. Советую всем интересующимся многопоточными приложениями презентацию (смотреть на скорости х2, спикер не совсем торопливый):


Да, она о C++, но общую суть передает очень хорошо, и в итоге показана одна из лучших реализаций пула потоков (не только на С++). Считаю ее одной из лучших презентаций по теме, если не самой лучшей, хоть и не все ее могут найти и понять.
 
Последнее редактирование:
Это и не нужно. Приложение не должно создавать потоков больше, чем ядер процессора (x2 если у процессора есть гипертрединг или аналог). Все что больше – уменьшает производительность на переключении контекста и бессмысленных локах. Если коротко – тяжелый поток на любую шляпу = один из худжих видов говнокода.

Для максимальной многопоточной производительности используют пулы потоков, отдельные очереди на каждый поток, work stealing и т.д. Советую всем интересующимся многопоточными приложениями презентацию:

Да, она о C++, но общую суть передает очень хорошо, и в итоге показана одна из лучших реализаций пула потоков (не только на С++).
В 16 яве, в превью появились виртуальные потоки, которые отвязаны от потоков ОС и являются по сути надстройкой над ними. Затраты на создание VP ничтожны и их можно создавать сотнями тысяч, практически не теряя(а иногда значительно экономя) ресурсы. Проблема в том, что они в превью даже в 20 яве, и скорее всего не выйдут из него на следующем ЛТС релизе. Поэтому использовать их в каком-то мало-мальски серьезном коде я не сильно горю желанием, а уж тем более в ThreadPoolManager’е.
В данный момент, мой тестовый сервер работает на 2 виртуальных ядрах и имеет весьма ограниченные по размеру пулы для задач.
 
Назад
Сверху Снизу