Предисловие
В этом блоге я буду рассказывать о том, как я в 3 часа ночи решил разобраться, почему же Тарков такой поломанный?
И раз уж я получил официальную работу, то я с могу с гордостью заявить, что я инженер программист. Хоть моя работа и заключается в программировании микроконтроллеров и мат. моделей, но моё мнение чего-то да стоит.
И важная ремарка — Я НЕ ЛЕЗ В КОД ИГРЫ. Я просто читал логи и документацию к нескольким библиотекам для выяснения «полной» картины мира.
Ну, а теперь Рыба!Автор блога
Логи Таркова, или как их делать не надо
Начнём с формы логов игры Escape From Tarkov. На данный момент (Версия 0.16.1.0.34861) логи представляют собой папку с кучей .log файлов, что содержат сообщения от различных модулей.
Подробнее о каждом:
- Application – содержит информацию об основном процессе программы
- Notifications – содержит информацию о всех уведомлениях, которые приходят во время игры (Исключаем игровые сообщения по типу завершения крафтов или заданий)
- Spatial-audio – логи звукового движка
- Traces – «трассировка событий». Если проще, это подробная запись последовательности действий и изменений состояния в системе
- Network-connection – подключения к серверам
- Network-messages – сообщения об передаче данных (тестовых)
Все сообщения в выше перечисленных логах пишутся по следующей логике:
Дата Время GMT | Версия | Уровень сообщения | тип сообщения | Данные
Вот типичный пример:
2025-02-13 19:01:52.910 +03:00 | 0.16.1.0.34861
| Error | spatial-audio | GO can't be null, can't start occlusion process
А теперь оценим логи. Они не удобные. Если быть точнее, то волнуют две проблемы:
- Избыточность – большинство сообщений дублируются, а некоторые просто не имеют смысла в разделении. Например, network-connection и network-messages можно спокойно объединить потому, что в первом мы пишем «ПОДКЛЮЧЕНО», а во втором «ОТЛИЧНОЕ ПОДКЛЮЧЕНИЕ» (если упростить). Или же spatial-audio, который можно было спокойно кинуть в Application, а notification в traces, ну и так далее.
- Описания – зачастую к данным идут описания в виде json данных или огромные ошибки в 10 – 30 строк. Почему нельзя было написать их в отдельный файл с ссылками, чтобы потом можно было искать через условный Ctrl+F?
А если быть ещё более точным, файл логов должен быть один. Да, тогда вы спросите: как их читать, это же не удобно?! А я отвечу: Log Viewer Plus или же бесплатная версия Log File Viewer. Да, для них не подойдёт «НАШ» формат ввода логов, там нужен более классический и систематизированный «structured logging» (да, такой термин существует :/), который имеет более «табличный» вид:
2023-08-05 09:15:27 ERROR MainApp Ошибка при сохранении данных в базу данных User123
Но если вы внимательно читали, то видели момент об .json описании. Можно подумать, что там в идёт монолитная стена одну строчку (что иногда встречается, но не приветствуется), но нет! Там буквально json с отступами, и ладно если там 5 или 10 строк, я бы мог понять 15, но не 916 (нецензурная лексика). Если что, это было сообщение об начале группового рейда, где описана ПОЛНАЯ информация о всех участниках: кто, с чем, когда, в каком состоянии, ну и там по мелочи.
Теперь поговорим о генерации. Обычно в играх логи могут сохранятся либо динамически, что часто используют в однопользовательских проектах, либо пост-фактум, что можно встретить в онлайн проектах, либо смешанно, когда сообщения идут «партиями». В нашем случае используется первый вариант, к выбору которого, в купе с большим количество одновременно перезаписываемых файлов, есть вопросы, однако это можно понять, если вспомнить о частых вылетах.
Но появляется вопрос:
2025-02-13 19:23:55.839 +03:00 | 0.16.1.0.34861 | Info | network-connection | Send connect (address: 74.119.145.122:17009, syn: True, asc: False)
Они в реальном времени пишут IP сервера, куда ты подключаешься. Интересно, что же будет, если кто-нибудь DDoS кинет! Если судить по логам, то во время игры порт сервера открыт, а это значит, что любой может зайти в рейд, посмотреть в логи, а потом ддосить сервер. Да, возможно, сервер поймёт, что что-то не так, но проверять я это не собираюсь. Однако, сам факт того, что в логах содержится и в реальном времени генерируется столь деликатная информация наводит на определённые мысли.
Поломки, ошибки и вопросы
А теперь самое интересное. Я собрал более 400 лог файлов, что были у меня и моих друзей, благодаря чему получилось собрать график с отношением ошибок к общему числу сообщений.
И тут возникнет вопрос, что это за (нецензурная лексика)? Как это описывать? В некоторых версиях кол-во сообщений об ошибках превышает 80%!
Ну, а если серьёзно, то во время анализа ошибок я заметил, что большинство ошибок будто игнорируются разработчиками (а некоторые, после условного патча, просто заменяются на аналоги). Опираясь на статистику выявлены самые часто появляющееся ошибки (без учёта копий сообщений в других файлах):
- NullReferenceException – ссылка на объект, которго не существует (равен null)
- Can't find way from position to core cover – бот не может построить маршрут к нужной ему точке укрытия
- BitWriterStream::WriteLimitedInt32 Data Loss Error. Min 0, Max 7, Value 8 tag:ToggleTacticalComboPacket0 – вписываемое значение больше допустимого
- Unknown custom item type: 0 – неизвестный тип предмета
- IndexOutOfRangeException – ссылка на индекс в массиве, который выходит за рамки его размерности (для примера: Попытка получения значения номер 10, хотя размер массива 9)
- Already registered object – попытка добавить объект, что уже был добавлен
- KeyNotFoundException – попытка получить значения по ключу, которого не существует. Если не вдаваться в подробности о типах данных, то это ошибка схожа с IndexOutOfRangeException, однако там мы обращались по порядковому номеру, а тут по специальному имени ячейки
- GO can't be null, can't start occlusion process – игровой объект равен null. Да, подобная ошибка уже была, но это ошибка расчёта звуковой окклюзии аудио движка
- SupplyData is null - supplyData равен null. Тут сложно сказать к чему это относится, но предположу, что к системе поиска лута в контейнерах
- No foldable found on … - чаще всего относиться к оружию. Предположу, что это ошибка возникает тогда, когда игра пытается «сложить» оружие, которое не имеет складываемых элементов
Вы заметили один очень схожий «паттерн»? Все ошибки из «топ-10» так или иначе связаны с валидацией данных! Да, классическая и всем нужная проверка данных на то, могут ли они быть использованы, или же стоит запросить данные ещё раз и перепроверить их.
И это подводит нас к другому вопросу: если игра и так работает, то зачем что-то менять? А всё очень просто: скорость, надёжность. Если мы говорим про большие программы, то часто работа сводится к минимизации потерь, как по скорости, так и по ресурсам ПК, и для этого чаще всего добавляют валидацию. В этом случае валидация выглядит как простейшая «таблица», что проверяет данные на правильность. Таким образом, если данные «кривые», то они не участвуют в последующей работе.
Самое интересное впереди: как такую валидацию сделать? В нормальных рабочих программах, как я и говорил, используются таблицы. Самый простой пример — это проверка типа данных при вызове функции. Таким образом код сразу сможет проверить, где и какие типы данных используется, и выдаст ошибку, в случае некорректности. Также могут добавлять if-овые деревья на простейших логических командах сравнения (в идеале), что бы программа могла за счёт незначительного усложнения получить бóльшую стабильность и скорость.
Если разбирать на примерах, то в уже представленных случаях отлично бы подошло следующие:
- if (myObject != null) {} else {…}
- if (agent.CalculatePath(targetPosition, path); …) {agent.SetPath(path);} else {}
- if (value >= 0 && value < 8) {WriteLimitedInt32(value); …} else {}
- if (itemType == 0 || itemType == ‘0’) {} else {…}
- if (index >= 0 && index < array.Length) {var element = array[index]; …} else {}
- HashSet<GameObject> registeredObjects = new HashSet<GameObject>(); if (!registeredObjects.Contains(obj)) {registeredObjects.Add(obj); …} else {}
- if (dictionary.TryGetValue(key, out var value)) {…} else {}
- if (audioSource != null && occlusionObject != null && OculusSpatializer.IsInitialized()) {OculusSpatializer.SetOcclusionEnabled(true);OculusSpatializer.SetOcclusionObject(occlusionObject); … } else {}
- if (supplyData != null) {…} else {}
- if (weapon.HasFoldablePart) {weapon.Fold(); …} else {}
Готово! Это частично исправит положение! Просто впишите за место «…» код, что был раньше и всё! В else можете вписать что-то для исправления данных, но я не разработчик, чтобы в этом разбираться.
И после всего этого возникает вопрос: как я, человек, что пишет на python, C++, maple и mathlab и взявший C# с Unity буквально во время написания статьи, смог что-то придумать, а РАЗРАБОТЧИКИ (нецензурная лексика) нет.
С этого момента у многих людей, как и у меня, должны были возникнуть вопросы к компетенции разработчиков и их проверки качества продукта, что они разрабатывают. Если судить только по этому «топу ошибок», никакой проверки не ведётся! Ну нельзя оставлять ошибки в коде, что появляются ПОСТОЯННО.
Но… ХАХАХАХА! Вы ещё не видели «МАГНУМ ОПУС» разработчиков:
2025-02-13 18:59:40.192 +03:00|0.16.1.0.34861|Error|Default|PlayerBody destroyed without being disposed. Please call Dispose before destroying.
Чтобы вы понимали, это сообщение появлялось 232 раза. И первое её упоминание встречается в версии 0.16.0.1.34481 от 28 декабря 2024. То есть, столь очевидная ошибка находится там уже 3-й месяц! Хотя, не исключаю, что она могла появится в версии .34447, когда происходило обновление Unity.
Проблема в последствиях - «dispose» в Unity используется для освобождения «объектов неуправляемых ресурсов» (к таким относится и playerBody). Подобные объекты могут использовать файлы, сетевые подключения и память абсолютно без контроля. Это делается для того, чтобы объект мог без изощрений использовать сторонние плагины, но из-за этого «автоматический» сборщик не работает, а «dispose» выступает в качестве «мануального» сборщика. Как только этот метод вызывается все данные, связанные с объектом, выбрасываются из памяти чтобы она не заполняла место.
Ещё есть пара интересных ошибок:
- Error insuring item – Ошибка страхования объекта
- Cant find counter for Quest – Не найден счётчик для квеста
Если первое я могу понять - аккаунт старый, некоторые предметы поломались. Но второе нет... Ошибка счётчика квеста? Как?
А это вопрос к валидации данных (да, (нецензурная лексика), опять). Просто, пока рассказывал о «dispose», я решил отвернуться от слона в комнате. Вы и так уже видели огромное кол-во ошибок из разряда: объект не найден или объект равен null. Но как вам это?
- Door with doorId door_Reserve_Base_PTOR_00012 not found!
- FastAccess item 67799795e23b51349807fccd for index Item5 not found
- Item not found: 677945cde05b4721d40f6a7a
- Already registered WindowBreaker's Id …
Да, ошибки WindowBreaker и Item not found уже исправлены, но, как и прежде, это наталкивает на вопросы к разработчикам. У вас есть ETS сервер! Вы на нём ВООБЩЕ, ХОТЬ, ЧТО-ТО ТЕСТИРУЕТЕ? Или просто так, по приколу, пока не (нецензурная лексика)?
Стоит поднять вопрос об исправлении старых ошибок! Вы видели все те ошибки, что я перечислил (во всяком случае паттерны)? Это ошибки почти за 9 месяцев. И знаете сколько из них исправлены? Правильно, две! Те самые WindowBreaker и Item not found.
Ну, если же сравнивать нормально, то за всё время по самым оптимистичным расчётам было исправлено 7373 ошибки, но 1311 из них не были исправлены до конца. При этом за это время, появилось 4586 новых ошибок! Так же стоит учитывать, что под «оптимистичными расчётами» я подразумеваю не сами ошибки, а их «экземпляры». То есть ошибка с двумя разными наборами аргументов будут считаться как две разные ошибки, что, по сути, ошибка одна. Подобное «облегчение» вызвано тем, что некоторые ошибки, просто криво написаны, из-за чего использование какого-то одного метода для парсинга этих «ошибок» весьма затруднено (тратить несколько месяцев на разбор excel таблиц на 10000+ строк «в тупую» ради статьи, я не горю желанием).
И, прежде чем мы перейдём к следующей части статьи, я объясню почему был показан относительный график, а не количественный:
Да, это график. Если вам интересен размер «огромной жёлтой линии», то это 772 422 warn-ов.
Сокрытие и недоработки
Теперь поговорим об «неудобной правде»: ошибок намного больше. Просто, некоторые из них либо не отслеживаются, либо несут более «визуальный характер». К 1-ым относится уже исправленная ошибка с отключением аудио-движка, работа которого просто не отслеживалась. А к визуальным ошибкам и недоработкам чаще всего относятся:
- Кривые границы прогрузки объектов
Думаю, раньше карта имела немного другую геометрию, и данная граница была скрыта, однако после обновления локации она стала видна. - Непрозрачность прозрачных объектов (Не уверен, но вроде это с 0.13.0.0.21469)
Скорее всего вызвана неправильной настройкой материала или же текстур. - Баги текстур
Затрудняюсь ответить в чём именно проблема. Это могут быть как эффекты и текстуры воды, так и неправильная их настройка. - Кривые эффекты тумана, дыма и соответственно затуманивания дальних объектов
- Ошибки синхронизации (по типу уже легендарной 228)
Это проблема сетевого кода, а именно валидации данных. Сервер может понять, где и как произошла ошибка, но он просто не имеет функции «исправления» ошибок. Например, в тех же логах прописано, что сервер вместе с ошибкой отправляет полный json про то, какие действия пошли не по плану и почему, однако же сетевой код просто не рассчитан под «откат изменений», из-за чего он банально перезагружает профиль с последнего «сейва». - Отключения от сервера посреди рейда
Это может быть вызвано целым списком проблем, начиная от банальных проблем с интернетом на серверах, так и более интересными крашами рейдов, вызванных сторонними ошибками. - Ошибки подключения к серверам при запуске рейда
Тут сложно сказать, однако, в отличие от 6-го пункта, сбросить на «проблемы с интернетом» не выйдет. Как правило подобное происходит моментально после подключения к серверу, либо же после «генерации лута», из-за чего я склоняюсь к мнению, что сервер просто выдаёт ошибку и, либо не пускает игроков, либо не запускает рейд вовсе.
Таких ошибок в игре великое множество, а как «вишенка» на торте, это оптимизация.
Если говорить о причинах подобного, то проблема, опять же, в контроле и тестировании, (скорее всего, они либо просто плохие, либо же отсутствуют вовсе).
А теперь и поговорим об оптимизации. Она… плохая. Нет, не ужасная, просто плохая. Вы можете удивиться, но «оптимизация» не является главной причиной низкого фпс. Главная причина низкого фпс - [барабанная дробь] сетевой код. Да, сетевой код, опять. Вся проблема в том, что только по одному Буянову ведомой причине, код, отвечающий за общение с сервером, идёт через «main thread». Этот «поток» отвечает за обработку всех основных событий в игре и, как в нашем случае, за интернет, в частности.
В обычных играх для «обхода» подобного используется асинхронность - во время ожидания запроса от сервера игра продолжает работать, просто с низким TPS. Однако в Таркове всё не так. Хотя движок и старается раскидать игру на несколько потоков, но из-за однопоточной логики кода, игра продолжит виснуть.
В подтверждение могу привезти SPT, где с установленными графическими модификациями фпс редко падает ниже 80, однако на официальной серверах той же версии 30 – 40 фпс норма (15 - 30 без DLSS).
Знаете, что самое удивительное? В Unity уже есть встроенная функция «.GetAsync» относящееся к классу «HttpClient», но видимо её почему-то не используют.
Другая значимая проблема оптимизации - объекты. Я не знаю, что за мания делать по 100 объектов на комнату, но из-за неё возникает проблема в рендеринге. Я понимаю, что это может звучать не логично, однако отрисовать один ОГРОМНЫЙ объект проще, чем много маленьких.
В доказательство большого количества объектов могу привести такие кадры прогрузки:
Проблема в том, что во время прогрузки, Unity опрашивает объект и требует отдельного вызова «отрисовки» для каждого из них. Для одного же объекта только один, поэтому тратиться меньше времени и ресурсов для передачи информации, её приёма и обработки. (прогрузка объектов на выше показанном «слайдшоу» заняла целых 100 мс!)
А теперь поговорим о том, как их исправляют! Проблема с оптимизацией? Понизим графику! Проблема с границами? Закроем дверь! Проблема с сетевым кодом? Поставим очередную заплатку, чинящую один или два специфических случая. А любые жалобы или же замечания встречаются в штыки, из-за чего каждый, кто не является крупной «общественной личностью» просто затыкается (либо баном, либо «замечаниями» и последующим удалением сообщений).
Читеры и за́мок без стен
И тут мы разберём всех волнующий вопрос: а откуда столько читеров? Помните, что я говорил про то, что сервер не проводит никакой валидации данных? Ну, как итог, читер может перехватывать отправляемый на сервер пакет данных и подменить его. Никакого dll и не понадобиться, нужен просто всем известный Cheat Engine! Если быть точнее, то конкретно им взломать не выйдет, но принцип работы схож.
Я нашёл старую версию чита в opensource и прочёл код (слава богу он был на C++ 😊). Если кратко, то он работает по такой логике - мы знаем, что нам нужна ячейка начинающееся с какого-то паттерна (например, TY), а после ищем подходящее значение, читаем его или изменяем. По такому принципу чит создавал полноценную динамическую карту лута и игроков, давал возможность смотреть сквозь стены и многое другое.
Да, версия старая, но если судить по сообщениям разработчика чита на форуме, то никаких критических изменений, влияющих на работу читов, в игре так и не произошло, не смотря на регулярные обновления игры и античита.
Вся проблема в последнем. Античит обычно делают с целью защиты игры от внешнего вмешательства, в виде добавления сторонних скриптов, но изменения данных из ОЗУ подобная программа не отследит, она просто не рассчитана на подобное!
Но, что насчёт СБЭУ-Комаров?! А всё очень просто. Тут играет роль ранее упомянутая валидация. В игре есть защита от спид хака, но она максимально тупая: если скорость выше максимальной, значит игрок читер. Если же игрок будет двигаться в пределах нормальной скорости, но при этом лежать, сервер не будет видеть проблем.
Вся эта ситуации с читерами описывается комедийной сценой:
«Эта дверь даже ядерный взрыв выдержит, вам в жизни её не пробить!»
«А стены?»
«Б***ь…»
Итог
Игру вряд ли починят. Да, скорее всего продолжат выходить патчи, которые будут залатывать дыры, но суть проблемы это не изменит.
Какое-то время назад было обновление Unity, которое было надеждой на улучшение и пересмотр технической стороны игры, но кроме большего количества проблем и костылей особо ничего не было. Аудио движок сломали, старые баги, связанные с кодом на серверах, не починили, «крупное обновление», которое должно было убрать большую часть проблем и быть лучиком света в беспросветной тьме, оказалась очередным проходным патчем. А все речи о большей свободе разработчикам и крошки выеденной не стоят… Пока новые версии не начнут хотя бы нормально тестироваться, и разработчики перестанут умалчивать о проблемах, в лучшую сторону вряд ли что-то изменится. Главное, с чего стоит начать - сетевой код.
Боже, какие-то китайцы с русско-европейской командой под руку смогли сделать ПОЛНОЦЕННО РАБОТАЮЩИЙ БЭКЭНД. При этом, если исключить десинхронизацию открытия дверей и не самую удобную настройку сервера, то, по сравнению с официальным Тарковым, проблем вообще нет. ФПС нормальный, вылеты… бывают, но ни разу не было такого, что кого-то не подключало к серверу. А в купе с поддержкой модов~.
Ладно, вот теперь точно эпилог
Если разработчики не соберутся, и не начнут ответственно подходить к контролю разработки и критической переработке всего кода, то за дело во всю возьмётся сообщество. И это не как с некоторыми играми, как например DayZ, где многие игроки идут на сервера сообщества, чтобы поиграть с модами, нет. Если всё так и продолжиться, то даже те, кто любят исключительно официалку, будут ставить сторонние лаунчеры и патчи, только ради того, чтобы играть без «проблем». А если вспомнить, что в подобные версии можно вставить кряк, то, возможно, игру и вовсе перестанут покупать!
Огромное Спасибо:
Андрей Майоров (МАТ) - редактура
Даниил Грудзинский (abrikos) - консультации по анализу сетевых логов
Григорий Харламов и остальное сообщество TarkovGame | Escape from Tarkov | Tarkov Arena за идею и моральную поддержку
Лучшие комментарии
А так блог хороший, подробный, интересный.(Понимать бы, что там написано ещё /j)
Ну моё дело предупредить — не ко мне Ваня Лоев придёт и за бочок укусит всё равно.
Ну хороший человек, ну зачем в самом последнем предложении перед итогом матернулся? Ну запрещено же. Подтирай быстро, пока не увидели!
Вообще то и так стёрто, не знаю где ты в двух буквах маты увидел? Может я «бухать» или «брить» имел ввиду
А и да, важное замечание:
Когда я говорил о читах, я говорил не об всех программах, а только об одном из типов читов, исходный код которого, я смог найти