Рубрики
Без рубрики

Конечная точка настройки плеера – Позволяет интерфейсу запускать внутренний код

Эта статья была первоначально опубликована в техническом блоге InnoGames, и ее можно найти здесь. Я… Помечен java, api, groovy, архитектурой.

Настройка плеера для тестирования (серия из 2 частей)

Эта статья была первоначально опубликована на Технический блог InnoGames и можно найти здесь .

Вступление

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

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

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

Затем у нас есть тесты интеграции серверной системы. У них есть полный доступ к бэкэнд-коду игры, что позволяет повторно использовать классы сущностей и репозиториев из кода игры для настройки игрока. Это самый быстрый подход, потому что они напрямую манипулируют базой данных без накладных расходов на выполнение нескольких мошеннических запросов. Мы абстрагировали настройку плеера в API, похожий на конструктор, который позволяет нам определять состояние всего плеера, настраивая один объект (вы можете найти более подробную информацию об этой архитектуре в более раннем сообщении в блоге: Как сделать ваши тесты более удобочитаемыми и удобными в обслуживании). Вот пример того, как выглядит настройка плеера в бэкэнд-тесте:

@Test
public void testCollectProduction() {
   buildScenario(scenario -> scenario
      .withPlayer(player -> player
         .withResource(ResourceConstants.COINS, 100)
         .withCity("MainCity", city -> city
            .withBuilding("SomeProductionBuilding", building -> building
               .withProduction(production -> production
                  .withResource(ResourceConstants.COINS)
                  .withAmount(20)
                  .withFinishNow()
               )
            )
         )
      )
   );

   // ... test action and assertions are here ...
}

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

Конечная точка настройки Плеера

Чтобы продемонстрировать эту конечную точку, я создал демонстрационный проект, в котором мы можем настроить имя игрока, уровень и экипировку через конечную точку настройки игрока. Вы можете найти полный исходный код на GitHub . Он написан на Java с использованием Spring Boot. Давайте начнем с очень простого примера. Это простой HTTP-запрос к конечной точке:

POST /setup-player HTTP/1.1
Host: localhost:8080
Content-Type: text/plain
Accept: application/json

player
    .withName("Hero")
    .withLevel(2)

Как вы можете видеть, тело запроса на самом деле содержит код. Это тот же код, который мы используем в наших внутренних тестах. Мы настраиваем объект игрока, устанавливая имя игрока на “Герой” и уровень на 2.

Демонстрационный проект также предоставляет конечную точку для отображения текущего состояния проигрывателя в виде SVG-изображения. Просто открой http://localhost:8080/player в браузере. Для нашего Героя со 2 уровнем вы увидите это изображение:

Чтобы дать игроку оружие, просто вызовите с оружием() на объекте игрока. Само оружие можно настроить таким же образом, используя методы “с помощью…” для объекта оружия.

ОТПРАВИТЬ запрос в/настройки-плеер:

player
    .withName("Hero")
    .withLevel(2)
    .withWeapon(weapon -> weapon
        .withType(Weapon.Type.SWORD)
        .withAttackPoints(10)
    )

ПОЛУЧИТЬ запрос/игроку:

Теперь давайте настроим все, что поддерживает демонстрационный проект:

ОТПРАВИТЬ запрос в/настройки-плеер:

player
    .withName("The Mighty")
    .withLevel(99)
    .withWeapon(weapon -> weapon
        .withType(Weapon.Type.AXE)
        .withAttackPoints(20)
        .withColor("#26639b")
        .entityRef(ref("weapon"))
    )
    .withHeadgear(headgear -> headgear
        .withType(Headgear.Type.HELMET)
        .withDefense(35)
        .withColor("#711284")
        .entityRef(ref("helmet"))
    )

ПОЛУЧИТЬ запрос/игроку:

Возможно, вас интересуют вызовы EntityRef() . Метод EntityRef() принимает объект держателя ссылки, который будет заполнен сущностью, созданной во время настройки проигрывателя. Мы активно используем это в внутренних тестах, чтобы впоследствии иметь доступ к вновь созданным объектам (подробнее об этом вы можете прочитать в запись в блоге, о которой я упоминал выше ). В нашей конечной точке мы используем его вместе с функцией ref() , которая является специальной функцией конечной точки настройки плеера. Он создает объект держателя ссылки с присвоенным ему переданным именем. Затем тело ответа содержит все ссылки, созданные таким образом. В последнем примере мы определили две ссылки: “оружие” и “шлем”. Вот как выглядит ответ:

{
    "weapon": {
        "id": "13aafbad-54be-425f-a27a-030935d7852a"
    },
    "helmet": {
        "id": "b9b5fa2d-0a00-40b3-b8c3-c4468943ceff"
    }
}

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

Как это работает под капотом

Чтобы выполнить пользовательский код через конечную точку, мы можем воспользоваться преимуществами языка Groovy/|. К счастью, Groovy совместим с синтаксисом Java, который позволяет нам просто копировать части кода из наших внутренних тестов и вставлять его в тело запроса. Это означает, что тело запроса на самом деле является отличным скриптом!

Для интеграции Groovy в наше Java-приложение мы используем пакет groovy . Он поставляется с оболочкой Groovy , которая не только способна выполнять сценарии Groovy, но также имеет возможность обмениваться объектами между Java и сценарием Groovy. На следующем рисунке показано, как работает конечная точка:

Мы создаем данный объект Player, который представляет собой простой объект данных с API, похожим на конструктор. Затем мы передаем этот объект в оболочку Groovy, чтобы он был доступен с помощью скрипта Groovy. После этого мы выполняем скрипт из тела запроса для настройки объекта player. Когда выполнение скрипта будет завершено, мы сможем использовать тот же объект для окончательной настройки проигрывателя.

Вот полный Java-код метода контроллера:

@PostMapping(value = "/setup-player", produces = MediaType.APPLICATION_JSON_VALUE)
public String setUpPlayer(@RequestBody String request) throws JsonProcessingException {
   Player player = playerRepository.getCurrentPlayer();

   // Create sandboxed groovy shell
   var groovyShell = groovyShellFactory.createSandboxShell();

   // Set player property that can be configured inside the groovy script
   var givenPlayer = new GivenPlayer();
   givenPlayer.setEntity(player);
   groovyShell.setProperty("player", givenPlayer);

   // Run requested groovy script which configures the givenPlayer object
   groovyShell.evaluate(request);

   // Use givenPlayer object to set up the player
   playerSetup.setUp(givenPlayer);

   // Return entities that were referenced in the groovy script
   return objectMapper.writeValueAsString(groovyShell.getProperty("references"));
}

Я не буду вдаваться в подробности о том, как работает данный объект Player или метод player Setup.setUp() . Вы можете найти более подробную информацию об этой идее в сообщении в блоге, о котором я упоминал ранее, или заглянуть в код на GitHub . В общем случае метод player Setup.setUp() заботится о создании реальных объектов путем чтения данного объекта Player.

Давайте сосредоточимся на том, как создается оболочка Groovy. Вот упрощенная версия Заводной фабрики оболочек:

var sandboxClassLoader = new GroovySandboxClassLoader(getClass().getClassLoader());

var compilerConfig = new CompilerConfiguration();
compilerConfig.addCompilationCustomizers(new GroovySandboxImportCustomizer());
compilerConfig.setScriptBaseClass(PlayerSetupScript.class.getName());

var groovyShell = new GroovyShell(sandboxClassLoader, compilerConfig);
groovyShell.setProperty("references", new HashMap<>()); // used to store the references

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

Загрузчик классов

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

public class GroovySandboxClassLoader extends ClassLoader {

   public GroovySandboxClassLoader(ClassLoader parent) {
      super(parent);
   }

   @Override
   protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
      if (name.startsWith("java.")
         || name.startsWith("groovy.")
         || name.startsWith("org.codehaus.groovy.")
         || name.equals(PlayerSetupScript.class.getName())
         || GroovySandboxClassWhitelist.getWhitelist().containsKey(name)) {
         return super.loadClass(name, resolve);
      }
      return null;
   }

}

Чтобы заставить оболочку Groovy работать, мы должны разрешить доступ к пакетам groovy, а также к базовому классу сценариев. Кроме того, мы просто разрешаем всем классам java использовать базовые классы, такие как String. Конечно, вы также можете внести их в белый список, если хотите сделать его более безопасным. Фактический белый список ваших собственных классов реализован в отдельном классе Белый список классов песочницы Groovy , потому что мы также используем этот белый список в ImportCustomizer. Вот упрощенная версия класса белого списка:

public class GroovySandboxClassWhitelist {

   // Mapping of "Class name" -> "Alias" (We need the alias for the ImportCustomizer)
   @Getter
   private static final Map whitelist = new HashMap<>();

   static {
      addWithAlias(GivenPlayer.class);
      addWithAlias(GivenWeapon.class);
      // ...
   }

   private static void addWithAlias(Class clazz) {
      whitelist.put(clazz.getName(), clazz.getSimpleName());
   }

}

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

Импортный настройщик

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

public class GroovySandboxImportCustomizer extends ImportCustomizer {

   public GroovySandboxImportCustomizer() {
      super();

      GroovySandboxClassWhitelist.getWhitelist().entrySet().stream()
         .filter(entry -> !entry.getValue().isBlank())
         .forEach(entry -> addImport(entry.getValue(), entry.getKey()));
   }

}

Это делает класс Groovy Sandbox Белым списком в качестве источника истины для загрузчика классов и ImportCustomizer. Нам просто нужно добавить туда класс, чтобы сделать его доступным в скрипте Groovy, и определить псевдоним для его автоматического импорта.

Базовый класс скрипта

Базовый класс script обеспечивает дополнительное поведение скрипта Groovy, который в нашем случае является только функцией ref() , которую вы видели в примере ранее.

public abstract class PlayerSetupScript extends Script {

   public AtomicReference ref(String name) {
      var references = (Map>) getProperty("references");

      if (!references.containsKey(name)) {
         references.put(name, new AtomicReference<>());
      }

      return references.get(name);
   }

}

Он создает объект держателя ссылки и сохраняет его в свойстве под названием “ссылки” внутри скрипта Groovy. Свойство может быть доступно конечной точке после выполнения сценария, чтобы вернуть все ссылки в ответе.

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

Вывод

Благодаря мощи Groovy мы нашли способ сделать нашу систему настройки плеера доступной для клиента.

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

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

InnoGames принимает на работу! Проверять открытые позиции и присоединяйтесь к нашей потрясающей международной команде в Гамбурге в сертифицированном Great Place to Work ® .

Настройка плеера для тестирования (серия из 2 частей)

Оригинал: “https://dev.to/christianblos/the-player-setup-endpoint-let-the-frontend-run-backend-code-5aj1”