В недавних примечаниях к патчам была строка «Исправлена ошибка создания земли под игроком при создании земли в другом месте». Подробнее об этом можно прочитать здесь. Некоторых пользователей Reddit заинтересовало, как вообще мог возникнуть такой баг, они попросили объяснить более развёрнуто и даже предложили использовать эту информацию для нашего блога Factorio Friday Facts. Я подумал: «Да, если я потрачу время на написание этого текста, нам стоит взять его в FFF, чтобы никому другому не пришлось писать о чём-то другом».


Баг с созданием земли, отчёт о котором был отправлен после выпуска версии 0.18.21.

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

Переходы между тайлами… снова


В первые пару лет разработки Factorio вода отрисовывала вокруг тайлов земли переходные тайлы. Графика переходных тайлов занимала у тайлов земли слишком много пространства, поэтому было невозможно нарисовать один тайл земли, окружённый водой, как и мост шириной в один тайл, проходящий по воде. Кроме того, переходная графика сочеталась только тайлами травы. Для учёта этих ограничений генератор карт был дополнен этапом коррекции, цель которого заключалась в том, чтобы рельеф можно было отрисовывать без графических артефактов. (Если вы спросите, почему в игре не использовались плитки Вана, то я не могу ответить на этот вопрос, но знаю, что эту возможность рассматривали. Исходя из того, как выглядели первоначальные переходные тайлы воды, могу предположить, что изначально их планировали использовать, но в конечном итоге они плохо сочетались с генерацией карт на основе шума. Но это всё лишь мои предположения.)

Разумеется, коррекция тайлов выполнялась и в случаях, когда тайлы изменялись не в генераторе карт, например, через скрипт. Появились моды, позволявшие игрокам размещать тайлы. Одним из таких модов был Landfill, который позже перенесли в основную игру; насколько я помню, главной причиной этого стали жалобы игроков, которые начинали игру на большом острове и после 20 часов на карте обнаруживали, что не могут продолжить игру дальше. Добавление возможности создания земли решило эти проблемы, но создало новую. При добавлении земли логика коррекции тайлов могла решить «скорректировать» тайлы, на которых стоит игрок, из-за чего он оказывался в ловушке, окружённый водой (баг-репорт).


Баг-репорт 2016 года — при расположении тайла земли игрок попадает в ловушку

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

Как уже говорилось две недели назад, новая графика переходов и новые ручьи с возможной шириной в один тайл могли приводить к тому, что игрок как будто застревал в невидимых стенах или не мог перешагнуть очень узкую полоску воды. Кроме того, персонаж уже и так немного проскальзывал через углы сущностей, а из-за новых переходов угловых тайлов, которые визуально выглядели диагональными, мне не хватало такого поведения при коллизиях с тайлами. Поэтому я начал перерабатывать способ коллизий персонажа игрока с тайлами и разрешения этих коллизий. Я выяснил, что лучше всего игнорировать ограничивающий прямоугольник персонажа и тестировать рельеф прямо в позиции игрока. Если тип тайла в этой позиции проходим для персонажа, то игрок не вступает в коллизии, а если он непроходим, то мы определяем форму перехода на этом тайле и заставляем персонаж выполнять коллизию только с некоторыми частями тайла. (Примечание: при этом подразумевается, что проходимые тайлы отрисовывают переходы поверх непроходимых тайлов; это на случай, если вы подумываете создавать мод с новым типом рельефа, по которому игрок не может ходить.)


Движение персонажа без учёта переходов между тайлами

Моя цель заключалась в том, чтобы движение персонажа рядом с водой было естественным, а остальные коллизии работали так же, как и раньше, несмотря на то что, например, враги не всегда имеют возможность преследовать игрока. Генератор карт не создавал карты, на которых такое случается часто, а я не был против того, чтобы игроки использовали заполнение землёй для создания проходов, по которым не могут перемещаться враги. Мне казалось, что не имеет смысла дополнять цепную реакцию решения предыдущей проблемы, создавая новую проблему, потому что изменение коллизий игрока тоже имело собственные проблемы (замечали странности со стенами, расположенными рядом с водой?).

Я хотел создать специальную функцию коллизий для персонажа игрока, но вскоре осознал, что у нас есть несколько функций коллизий для разных ситуаций, а мне не хочется делать специальные версии для каждой из них. Поэтому я решил добавить к маске коллизий игрока флаг, модифицирующий способ распознавания коллизий. Естественно, я хотел, чтобы моддеры могли изменять маску коллизий сущности персонажа, поэтому сделал флаг открытым для определений прототипов. [Прим. пер.: прототипы используются в Factorio для указания существующих в игре предметов, сущностей и технологий, а также их свойств.]

В результате моддеры начали использовать флаг и для других сущностей, а после того, как Oxyd переработал алгоритм поиска пути, флаг начал вызывать вылеты… Oxyd исправил ошибку, но изменил мою специальную логику так, чтобы она снова учитывала весь ограничивающий прямоугольник, а не только позицию сущности, из-за чего движение игрока ухудшилось. На этот раз я решил устранить проблему, добавив в функции коллизий дополнительный параметр (вместо ещё одного флага), который будет указывать, что коллизия вычисляется для управляемой игроком сущности, чтобы можно было определить, нужно ли использовать старое поведение.

И вот так появился баг. Помните баг с заполнением земли, когда коррекция тайлов создавала под игроком воду? Так вот, для его исправления нужно было добавить в конец процедуры «build terrain» фрагмент кода, который бы проверял, что у построившего тайлы игрока нет коллизий с водой. Если они есть, то нужно поместить под него землю, чтобы исправить результат коррекции тайлов. Специальная логика коллизий игрока позволяла персонажам подходить так близко к тайлу воды, что их ограничивающий прямоугольник создавал коллизию с водой, и когда я добавил дополнительный параметр к функциям коллизий, я не исправил этот код. Поэтому он использовал поведение коллизии «учитывать весь ограничивающий прямоугольник» и ошибочно определял, что сущность персонажа выполняет коллизию с тайлами. Пытаясь «спасти» персонажа, он помещал под его ноги землю.


Баг с заполнением землёй

И тут мне нужно было признаться и покаяться. Когда появился первый пост с сообщением о баге, я сразу же понял, в чём проблема. Я видел его не в первый раз. Та же проблема возникала у меня, когда я впервые создавал специальную логику коллизий. В то время я отловил её и исправил ещё до того, как вся переработанная система переходов тайлов была объединена с основной кодовой базой. Но я не протестировал её. Я не… протестировал… её.

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

В чешском языке есть поговорка, для которой я не смог найти аналога на английском: «кто ничего не делает, ничего не сломает». Это своего рода подбадривание на случай, когда что-то случайно идёт не так, вызывая затруднения. Единственный способ ничего не сломать — не делать ничего. Иногда эта поговорка звучит как совет… если не можешь разобраться во всех проблемах, которые вызовет новая функция, изменение или устранение бага, или просто невозможно определить количество потенциальных проблем, которые возникнут при внесении изменений, то это становится невыносимым, и самым лучшим вариантом начинает казаться ничегонеделание. Но это всего лишь подкравшийся паралич анализа — нужно просто вспомнить об истинном значении поговорки и прекратить беспокоиться о возможных проблемах, которые в конечном итоге могут и не возникнуть. А если возникнут, то это будут всего лишь затруднения, которые со временем тоже решатся.