Terraria: или пишите игры правильно

в 18:13, , рубрики: .net, game development, inject, private, protected, public, xna, информационная безопасность, метки: , , , , ,

Terraria: или пишите игры правильно

Привет, дорогой хабрапользователь!

Надеюсь, все хотя бы раз играли в такую замечательную игрушку, как Terraria, ведь сегодня речь пойдет о ней и о том, как не стоит писать игры с точки зрения безопасности. Если интересно — добро пожаловать под хабракат!

Вступление

Начнем, пожалуй, с того, что такое Terraria и как она появилась.

Феноменальный успех неизменно пребывающей в разработке «песочницы» Minecraft, уже принесшей Маркусу Персону миллионы, не мог остаться незамеченным. Так и случилось, вскоре появляется на свет Terraria. Занимается разработкой один единственный человек, Эндрю Спинкс, главный дизайнер и по совместительству не менее главный программист.

При взгляде на здешние «восьмибитные» пейзажи услужливое подсознание сразу спешит навесить ярлык «Minecraft в 2D». А что? В рюкзаке — кирка и топор, вокруг — случайно сгенерированные просторы. Цель — копать, строить, убивать, добывать.

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

Как оно работает?

Игра написана на языке C# (.NET 4.0) с использованием фреймворка XNA, о котором я достаточно много писал на хабр, например тут, тут и тут.

Изучаем саму игрушку

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

Игра написана с помощью XNA и .NET, а значит — все бинарные файлы и файлы библиотек можно посмотреть насквозь с помощью рефлектора, например: .NET Reflector.

Открываем Terraria.exe, ищем точку входа Main (Program):
Terraria: или пишите игры правильно

Видим забавные строки:

Steam.Init();
if (Steam.SteamInit)
{
       main.Run();
} else {
       MessageBox.Show("Please launch the game from your Steam client.", "Error");
}

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

Для того, чтобы обойти эту «безопасность», достаточно подменить steam_api.dll (откуда импортируются функции) или же можно пересобрать приложение, закоментировав соответствующие строчки. Ведь сам Steam никак не влияет на игру, кроме того, что добавляет туда свой Layout. Но мы пойдем более интересным способом и попробуем даже влиять на саму игру.

Вспомним, что игра написана с использованием — XNA, а значит, у нее должен быть главный класс игры, который наследуется от Microsoft.XNA.Framework.Game, далеко идти не пришлось, это класс Main.

Любая игра, написанная на XNA, имеет в себе, так называемые «компоненты», которые можно туда добавить. Компоненты могут быть как обычными (логика), так и графическими (Drawable).

А теперь давайте подумаем, что можно сделать?

Главный класс у нас Main и он имеет модификатор public (public class Main: Game {… })!
Чем это грозит? Мы можем создать новое приложение, которые будет импортировать наш Terraria.exe в качестве библиотеки и запустит её, а дальше — можно добавить свой компонент игры, и этот компонент будет иметь почти полный доступ к игре.

Пройдясь еще по всяким классам, увидим, что основная идея этих классов — это индийская версия синглтона статический доступ, который, кстати, тоже public.

Стоило бы придать главному классу модификатор доступа отличный от public, как все бы у нас провалилось.

Все, дальше — очень просто, создаем компонент и добавляем его в main.Components. Однако, мне захотелось так же порисовать на spriteBatch'e террарии. С DrawableCompontent возникли сложности, т.к. он рисуется до основной прорисовки класса Main, как бы я не играл с DrawOrder.

Потом, я еще раз взглянул на класс Main, у него отсутствовал модификатор sealed, что так же доставило и упростило мне жизнь. Идея стала куда проще: просто унаследоваться от нашего Main.

Практика, пишем код

Создаем новое консольное приложение, подключаем в качестве библиотек Microsoft.Xna.Framework.*, Terraria.exe.

Теперь создадим класс, который будет наследоваться от Main:

sealed class InjectedMain : Terraria.Main
{
        private SpriteFont font;
        private SpriteBatch spriteBatch;

        internal InjectedMain() : base() { }

        protected override void LoadContent()
        {
            base.LoadContent();

            font = Terraria.Main.fontMouseText; // получаем какой-нибудь шрифт
            spriteBatch = new SpriteBatch(GraphicsDevice);
        }

        protected override void Update(GameTime gameTime)
        {
            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            base.Draw(gameTime);
        }
}

Теперь идем в точку входа и заставим запустится наш игровой класс:

static void Main(string[] args)
{
     try { Program.game = new InjectedMain(); }
     catch { Console.WriteLine("fail, sorry :("); Console.ReadKey(); return; }

     Program.game.Run();
}

Ну и нарисуем что-нибудь, добавим в наш переопределенный Draw:

spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied);
spriteBatch.DrawString(font, "Hello habrahabr!", new Vector2(5f, 5f), Color.White, 0f, Vector2.Zero, 1f, SpriteEffects.None, 1f);
spriteBatch.End();

Результат:
Terraria: или пишите игры правильно

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

Ну и напоследок сделаем что-нибудь эффектное, какой-нибудь хак.

У игрока террарии есть одно интересное свойство: ghost, которое превращает игрока в каспера приведение и позволяет проходить сквозь стены и летать по миру (наверняка, фишка для девелопера). Так сделаем же так, чтобы при нажатии и удержании Left Shift — игрок становился злым и коварным.

Идем в метод Update:

KeyboardState state = Keyboard.GetState();
Player local = Main.player[Main.myPlayer]; // получаем нашего игрока

local.ghost = state.IsKeyDown(Keys.LeftShift);
if (local.ghost)
{
        local.Ghost();
}

// пишем в чат
if (state.IsKeyDown(Keys.LeftShift) && oldKeyboardState.IsKeyUp(Keys.LeftShift))
{ Terraria.Main.NewText("Ghost activated!", 200, 200, 255); }

 if (state.IsKeyUp(Keys.LeftShift) && oldKeyboardState.IsKeyDown(Keys.LeftShift))
{ Terraria.Main.NewText("Ghost deactivaed!", 200, 200, 255); }

            oldKeyboardState = state;

Запускаем игру и становимся приведением по клику на шифт:
Terraria: или пишите игры правильно

Как вы понимаете, рисованием текста и другим преферансом — дело тут не ограничивается, на игру можно влиять почти полностью, отдельно надо сказать про кривость синхронизации мультиплеера — все эти изменения им не пресекаются и дают играть на серверах с этими хаками.

Отдельно хочется сказать про класс Player, где есть функция Save/Load, которая позволяет сохранять и загружать игроков соответственно, принимает и отдает она сам класс игрока Player. Т.е. мы можем изменить игрока чуть менее, чем полностью, сохранить его и использовать в игре. Или же, например, сохранить всех игроков на сервере в файлы, а потом закинуть их в папку Players и играть ими.

Мораль

Всегда используйте модификаторы доступа как надо, а классы, которые конечны — sealed (запрещает наследование). Для таблетки от паранойи верности можно еще и обфусцировать код.

Так же, если реализуете мультиплеер — сделайте достойную синхронизацию и так, чтобы вся логика проверялась на сервере, а в случае резкого несоответствия — отключать игрока. К примеру, как игрок может моментально переместиться из одной точки карты в другую за время, которое меньше секунды? Увы, сервер террарии считает это нормальным.

Эта статья писалась исключительно в ознакомительных целях: как на примере простых модификаторов — можно написать нехилый хак.

Исходники статьи, увы, не буду прикладывать, идея понятна.

До новых встреч!

Автор: ForhaxeD

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js