Сегодня в Clash Royale случился МЕМ ДНЯ: какой-то человек попробовал повыпускать карты на поле за 0 эликсира и у него... получилось! Я серьезно, вот видео:
https://x.com/sk__555/status/1979011746590339076.
Я разобрался почему возникла такая ошибка и теперь готов рассказать и вам.
tl;dr: в последней версии игры ребята начали в запросе клиента передавать стоимость карты, а сервер использовать это значение без должной валидации.
Начнем с того, что бои в Clash Royale полностью эмулируются на стороне сервера. В начале боя и на сервере, и на клиентах создается одинаковый экземпляр LogicGameMode. Любое действие в бою — это та или иная команда (LogicCommand). Сначала она отправляется на сервер, который пытается ее выполнить в рамках локального LogicGameMode, после чего уже сервер рассылает её всем клиентам, которые выполнят команду в тот же самый момент времени (тик). Таким образом между всеми клиентами и сервером достигается одинаковое состояние в каждый момент времени.
Можно ли выполнить так любую команду? Едва ли. Зачастую внутри каждой команды код выглядит следующим образом:
/* LogicDoSpellCommand.execute */
int execute(LogicGameMode mode) {
/* ... */
LogicSummoner summoner = mode.getSummoner(this.playerId);
LogicSpell spell = summoner.getSpell(this.spellIndex);
int cost = spell.getElixirCost();
if (summoner.getElixirPoints() < cost) {
/* Возвращаем ошибку и ничего не делаем больше */
return -13;
}
/* ... */
summoner.useElixir(cost);
summoner.castSpell(spell, this.x, this.y);
return 0;
}
LogicSummoner здесь представляет короля (игрока), а LogicSpell — конкретную карту в его колоде. Это каноничные названия, так уж сложилось.
Код выше — упрощенный пример. Каждая команда при её выполнении проверяет свою корректность, и выполняется только в том случае, если игрок действительно мог сделать соответствующее действие.
Даже если каким-то образом убедить свой локальный клиент в том, что выпустить "Рыцаря за 0 эликсира" — можно, то остальные клиенты и сервер такую команду не выполнят. А затем, после сверки чексуммы, сервер еще и заставит такого игрока-"читера" загрузить настоящиее состояние. Визуальный "Рыцарь за 0 эликсира" пропадет спустя полсекунды.
Чтож, это была предыстория, рассказывающая, почему читерить в боях в Clash Royale нельзя. Но у кого-то получилось, автор, ты что, нас обманываешь??7🙂
Нет-нет, что вы... вы еще не забыли пример кода, который я скинул выше? Это был реальный отрывок кода, только вот в последней версии его решили отрефакторить и переписать.
Раньше LogicDoSpellCommand содержала в себе координаты (x, y), порядковый номер карты (spellIndex) и всё такое. А вот начиная с последней версии (12.169) ситуация поменялась: вместо старого-доброго spellIndex пришел новый 32-битный Int32, в который запаковали сразу несколько полей:
22 lower bits: unknown purpose
6 bits: spell index (от 0 до 31)
4 bits: elixir cost (от 0 до 15)
Давайте назовем этот волшебный Int32 — SpellReference. Хотя доподлинно неизвестно, как эта структура называется на самом деле.
И как вы можете догадаться — да, стоимость карты (эликсир) теперь берется из этой структуры. Которая в свою очередь формируется клиентом. Очень умно, блинкласс!👍
Из забавного — они пытаются проверять эту структуру на корректность. То есть сначала они берут, достают spellIndex, по нему получают Spell, из Spell получают SpellReference. Только вот код сверки выглядит примерно так:
return ((command.reference ^ spell.reference) & 0x39FF8F) == 0;
Да, это какое-то безумие. Комментарии излишни.