Warcraft 3: что такое синхронизация

Warcraft 3, как и большинство игр-продолжений, разрабатывался на движке своего прародителя — Warcraft II. Естественно, от того самого движка практически ничего не осталось с переходом в фул-3D, но глубоко внутри него всё еще можно найти артефакты из 90-х годов. Конечно, в начале 2000-х эти «артефакты» были скорее полезными и реально эффективными решениями, но сегодня они стали просто гирями на ногах.

Обсуждаем — синхронизацию и винхак.

Детерминированность

Детерминированность (определённость). В каждый момент времени следующий шаг работы однозначно определяется состоянием системы. Таким образом, алгоритм выдаёт один и тот же результат (ответ) для одних и тех же исходных данных. В современной трактовке у разных реализаций одного и того же алгоритма должен быть изоморфный граф. С другой стороны, существуют вероятностные алгоритмы, в которых следующий шаг работы зависит от текущего состояния системы и генерируемого случайного числа. Однако при включении метода генерации случайных чисел в список «исходных данных» вероятностный алгоритм становится подвидом обычного.

Перевод — если подать алгоритму, который складывает 2 числа, 2+3, ответ всегда должен быть 5, независимо от погоды на Марсе и толщины чьей-то мамки. Если герой одного игрока бьет героя другого игрока, имея атаку 100-150, то второй игрок должен увидеть тот же урон, что и первый. Все вычисления должны быть одинаковыми на обеих сторонах.

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

С появлением доступного безлимита проблема сетевого общения перестала быть проблемой, что и видно на примерах  DotA2, которую во многих странах третьего эшелона просто невозможно запустить — уж слишком дохера трафика она жрет и канал просит потолще, не говоря уже о пинге. Всё то, что  Wc3 считал у себя дома,  D2 пересчитывает на своих серверах, что сильно замедляет и ухудшает работу игры при плохом соединении. Да что там — в свое время можно было с 52 Кбит модема играть с двух машин на какой-нибудь гарене, не ощущая никакого дискомфорта.

Чтобы обеспечить одинаковые вычисления на всех компьютерах игроков, Blizzard ввели контрольную сумму для всех карт. Когда игрок входит в игру, проверяется, что карта на его стороне имеет тот же hash, что и карта хоста. В случае несовпадения — карта начинает загружаться к новенькому игроку. Но что конкретно проверяется? Отвлечемся на минутку и на этот вопрос.

Поддержка DLC

Чтобы обеспечить воспроизводимость, не нужно проверять вообще все файлы.  WC3 построен на простых классах объектов, которые взаимодействуют друг с другом по строгим правилам и не могут выйти за пределы своей песочницы. Какая бы ни была у юнита модель, он не получит иммунитет к урону и не начнет летать, вместо того, чтобы ходить по земле. Какую бы картинку я не ставил на загрузочный экран, от этого скорость постройки пеонов не увеличится, а крипы не превратятся в рошанов. Более того, есть разные языки и CustomKeys, которые также могут быть абсолютно любыми. Итого —  WC3 нужно подтвердить всего несколько файлов.

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

Костыли и Blizzard

Итак, единственная дырка была закрыта. Теперь карты должны быть синхронными во всех случаях, не так ли? Не так.

По исходникам варкрафта легко заметить, что Blizzard любят костыли. Скорее всего, эффективные менеджеры заставили бросить все дела и выпустить продукт, который просто работал, не позволив довести до ума начатое. В результате в игре оказалось 20 типов урона, которые делятся на 5-6 категорий, ряд способностей без какого-либо эффекта и т.п. Но ключевой проблемой стала частичная синхронизация некоторых параметров юнитов.

Что такое архив игры с данным юнитов? Это специфический текстовый (почти) файл, в котором перечислены юниты и изменения, которые с ними произошли относительно некой основы. Если в редакторе герою настроили атаку, то в этот файл попадет не весь герой целиком, а только его ID (идентификатор), ID героя, от которого он был наследован, и новые значения атаки. Скорость движения, поворота и прочая в файл не сохранятся — игра возьмет их от оригинала, от юнита, указанного в качестве основы. Аналогичным образом работают архивы с данными о других игровых объектах. При загрузке карты эти архивы распаковываются и происходит подгрузка указанных сущностей в память игры.

Как видно по исходникам,  Warcraft 3 работает сугубо с SLK-таблицами. Те, кто любит заниматься моддингом, должны знать, что это стандарт разметки SYLK. Вся информация из архивов карты подгружается в уже готовые таблицы и занимает свои позиции. «Основа» заполняет все поля, изменения переписывают часть этих полей или дописывают их, если изменения затронули оригинального юнита. Все базовые (эталонные) данные о юнитах хранятся внутри главных архивов игры — war3.mpq и war3patch.mpq.

Пример
Создаем юнита H000 на основе Hpal. Меняем ему нижний предел атаки (atk1) на 10. В архиве появится запись [H000, Hpal, atk1=10]. При загрузке  WC3 подгрузит в память данные о Hpal, создаст новую строку H000 и впишет в ней параметр atk1 = 10.

Если же мы изменим самому Hpal атаку, то архив будет выглядеть так: [Hpal, Hpal, atk1=10]. Изменения затронут строку, где хранятся данные Hpal. При этом оригинал Hpal затронут не будет — он всегда остается эталоном. Если создать H000 после этой строки, его atk1 не будет равно 10, потому что он создастся от реального Hpal, а не того, которого мы изменили.

Зачем вообще используются архивы, если можно всё красиво укладывать в таблицы с самого начала? Трудно сказать, возможно, так близзы хотели сэкономить на повторении данных, а может, затруднить reverse engineering. Не исключено, что это специальный формат, с которым проще работать редактору.

В любом случае, близзы поставили проверку хэша строго на файлы архивов. Т.е. в хэш попадают .j, .w3a/b/c/d… z файлы, содержащиеся в карте. Все остальные файлы могут быть любыми — предполагаются, что они не могут оказать влияния на ход игры.

Но где-то в середине 2000-х появился Widgetizer — первая в своем роде программа, которая раскладывает архивы в обычные таблицы с данными. Она произвела настоящий фурор — так получилось, что из-за криворукости Blizzard распаковка данных из архивов занимала вечность, особенно в случае, если используются сотни измененных способностей или юнитов. Поэтому возможность сократить время загрузки карты с минуты до 5 секунд была крайне тепло встречена всеми мапмейкерами. Вместо w3a/b/d… файлов в карте оставались чистые SLK и TXT-файлы с данными. И я не слышал, чтобы хоть кто-то до меня указывал на проблему, что эти файлы никак не синхронизируются и не входят в хэш-сумму.

Проблема здесь кроется в деталях. Некоторые параметры юнитов и заклинаний, например, скорость полета снаряда, не хранятся в SLK-таблицах. Почему? Ответа нет, но я предполагаю, что виновны менеджеры. Эти данные важны для синхронизации, однако — их можно менять, при этом хэш карты не изменится. Игра пойдет в параллельной реальности. И это абсолютно легально и никак не может быть предотвращено.

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

Обратно к синхронизации

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

Так, если кликнуть «идти на мид», серверу будет отправлен пакет-приказ «Move (список ID юнитов) to x/y». Он разошлет его всем игрокам, благодаря чему на их мониторах мой герой тоже пойдет на мид. Ему не нужно знать, где я при этом нахожусь — благодаря синхронизации я априори буду в одном и том же месте для всех игроков, включая себя.

И десинхронизации

А что, если я сломал карту, использовав некорректные внешние параметры, которые влияют на игру? Что, если моя карта незаметно отличается от карты других игроков?

Возьмем для примера мою модифицированную карту aka win-hack. В самом примитивном варианте она предлагает просто взять SF-а и уничтожить всё за считанные секунды. Как это работает?

  1. У SF изменен его ID. Это необходимо, чтобы никто другой, ни при каких обстоятельствах, не мог забрать его у меня. Заказывая юнита, ты отправляешь серверу пакет с информацией о том, кого ты заказал. В моем случае такой ID не присутствовал в тавернах других игроков, поэтому с их точки зрения операция прерывалась и я оставался без героя. Для меня же, если кто-то пикает SF, тоже происходила ошибка — такого ID в таверне нет, поэтому этот игрок для меня оставался без героя. Первое время я не менял ID — в этом случае я оставался реальным SF с точки зрения других игроков. Проблема была лишь в том, что, с их точки зрения я фидил. Об этом ниже.
  2. У SF изменена атака и дальность. Это обман, чтобы набрать лайки ускорить процесс снова. Здесь важно не переборщить — мгновенную смерть трона воспринимают не все боты, а многие и вовсе ждут, чтобы сервер сообщил при помощи триггеров о падении хп трона до некоторого уровня. Поэтому нужно иметь атаки в меру, чтобы трон умирал хотя бы с 10 ударов.
  3. У SF встроен глобальный блинк. Опять же, чисто для ускорения процесса и демонстрации, что всё возможно под луной.

Что вижу я падающие вышки.
Что видят союзники: я не пикнул героя и ничего не происходит. Вообще.

Если бы я не менял ID, игровой процесс с точки зрения клиентов выглядел бы так, словно я фижу. Причины тому очевидны:
  1. В моей игре у меня 100к+ здоровья, на компьютерах союзников — около 500.
  2. Когда я отдаю приказ атаковать юнита, я делаю это сразу, из-за глобальной дальности атаки. На компьютерах других игроков мой SF пытается подойти на дальность атаки.
  3. С каждой атакой, которую я сделал в своей реальности, степень десинхронизации увеличивается. Ведь с каждой атакой моя функция случайных чисел дает новое число, которое на других машинах даже не пришло. Технически, это не играет роли — вернуться в одно состояние со всеми после десинка невозможно. Просто если союзник на  Chaos Knight кинет болт, у других игроков он будет длиться 2 секунды, а у меня может и 4 выпасть.
  4. Все действия игроков повторяются в меру возможности. В моих картах они прибегают на руны, даже дерутся за неё, если я уже не убил врагов, которые там стояли. В любом случае, на их стороне игра идет в прежнем темпе.

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

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

В карте №2 хак работает чуть более изыскано. Незначительно повысив здоровье вышкам, я могу их снести в своей игре, но вот враги и союзники не могут. В том состоянии, что наблюдается на их системах, пушки уже нет, и они могут пушить дальше. Однако, т.к. я хост, только я решаю, когда нужно посылать пакет о победе в игре.

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

Что меняет десинхронизация?

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

Пусть используется карта #1. Каждый раз, когда я убиваю врага, я буду рассылать всем клиентам приказ сохранить этот факт. Но если убийствами будут заниматься другие игроки, эту информацию я подтвердить не смогу — её у меня просто не будет. Однако, как показывает практика, иногда в статистику попадают ассисты и убийства, которые я не делал. Почему так происходит, точно сказать не могу.

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

ТянСихронизация не нужна

В 2010+ годах доверять клиенту такую ценную информацию вроде расположения всех-всех-всех объектов нет нужды. Широченные каналы и терабайтные сервера сами справятся с этими проблемами, рассчитывая игры строго на серверной стороне и делая снимки состояний по необходимости.

Отматывать реплеи назад в  варкрафте нельзя по причине синхронизации — практически все действия необратимые и нужно серьезно запотеть, чтобы переделать их принцип на двусторонний. Зато они весят в 300 раз меньше реплеев  Dota2. Мелочь, но пофиг.

Проблемы в архитектуре  WC3 позволяют уходить в состояния альтернативной реальности и обманывать любого хостера, который не озаботился проработкой этого момента. Большинство готовых GHOST-ботов не забывают сверять хэши таблиц. К сожалению, есть способы обмануть даже эти проверки. Но вот обмануть посаженного наблюдателя от самой платформы, на которого заворачивается подтверждение трафика, я не вижу возможности — он не подтвердит ни одного действия, сделанного в параллельной реальности.

6 комментариев

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.