Введение
Недавно я увлекся игрой Майнкрафт. Minecraft — это строительная игра в жанре «песочница», разработанная компанией Mojang Studios и разделённая на два издания: Java и Bedrock. Игровой процесс включает в себя взаимодействие игроков с игровым миром через размещение и разрушение различных блоков в трёх разных измерениях. В такой среде игроки могут создавать творческие структуры, строения различного типа и даже произведения искусства на серверах для многопользовательской игры и в одиночной игре, имея возможность переключаться между несколькими режимами игры.
Я столкнулся с проблемой, что игроки в многопользовательской игре, говорящие на разных языках, не понимают друг друга. Было принято решение написать свой плагин для сервера на ядре Spigot. Главная функция плагина заключается в переводе сообщений игроков на язык сервера.
Целью моей работы является создание плагина на Java Minecraft для перевода сообщений игроков на язык сервера.
Задачи:
Изучить информационные источники по исследуемой теме;
Проанализировать доступные плагины для перевода сообщений;
Разработать плагин на Java Minecraft;
Провести практический эксперимент с плагином;
Подвести итоги.
Объект исследования - Java Minecraft.
Предмет исследования – плагин Spigot.
С каждым днем наш мир становится более информационным, и чтобы это обеспечить, создается большое количество программного обеспечения с помощью языков программирования. Актуальность работы заключается в том, что с помощью языка Java программирование стало более доступным и простым.
Изучение информационных источников по исследуемой теме
Для изменения игрового процесса чаще всего используют Моды (для серверов и одиночной игры) или Плагины (только для серверов).
Модифика́ция (англ. Modification, сокращённо мод (англ. Mod)) — это дополнение от сторонних разработчиков, которое изменяет или дополняет оригинальный контент Minecraft.
Плагин - программа, которая нужна для расширения функциональных возможностей других программ или игр.
Мой выбор пал на плагины, т. к. они не нагружают сервер и не требуют установки на компьютер игрока.
Плагины в Mайнкрафт нужны для модификации игрового процесса и его мира, установки новых возможностей (3).
Java - строго типизированный объектно-ориентированный язык программирования общего назначения, разработанный компанией Sun Microsystems (7).
Spigot — глобальная серверная модификация, предоставляющая API для взаимодействия с игровым миром и созданная для упрощения создания плагинов к SMP-серверу. Spigot создан на основе ядра CraftBukkit и призван заменить «старшего брата» стабильностью и производительностью. Он имеет ряд преимуществ перед CraftBukkit:
Все плагины, написанные для CraftBukkit, пойдут и на Spigot.
Имеет повышенную производительность и стабильность.
В этом ядре есть свои возможности, которых нет на CraftBukkit.
В ходе изучения информационных источников, был сделан вывод, что удобнее всего использовать ядро сервера Spigot и его плагины.
Анализ доступных плагинов для перевода сообщений
Мной были проанализированы модификации на сайте SpigotMC (специализированное хранилище плагинов для данного ядра сервера).
Среди доступных плагинов самые популярные: translate, translator+, languages и translator.
Translator 1.7.2 и translator – не имеют нужных мне команд (нет возможности перезагрузки плагина и команд помощи) и не обновляются (translator 1.7.2 с апреля 2014 года и translator+ с сентября 2015), у них закрытый исходный код.
Translate и languages – перестали поддерживаться разработчиками (translate в октябре 2016, languages в апреле 2018), сложны в настройке и понимании, имеют закрытый исходный код.
Был сделан вывод, что ни один из существующих плагинов не удовлетворяет всем моим требованиям.
Разработка плагина на JavaMinecraft
Многие серверы создаются для того, чтобы их посещали большое количество людей оставляли донаты –пожертвования за дополнительные преимущества над обычными игроками. Игроки могут запустить свой собственный сервер, установив его на компьютере с помощью программного обеспечения, предоставляемого Mojang, или с помощью хостинг-провайдера, чтобы они могли запускать свой сервер на выделенных машинах с гарантированным временем безотказной работы. Владелец сервера также может настроить и установить плагин, чтобы изменить процесс игры, добавить команды и другие функции.
Таким образом, я решил создать свой плагин, который призван стирать языковые барьеры между игроками, что будет способствовать привлечению новых пользователей на сервер, на которых зарабатывает владелец сервера.
Мной было выбрано издание Minecraft Java потому, что в нем, по моему мнению, больше возможностей для модификаций, эта версия рассчитана для компьютеров (в отличие от Minecraft Bedrock), поэтому требует больше реакции и навыков, у нее больше сообщество игроков из разных стран мира (5).
Я решил написать плагин для своего сервера и не опубликовывать его, но в дальнейшем мной было глубже изучено написание плагинов и добавлен файл конфигурации, в котором можно настроить сообщения и некоторые параметры. Я опубликовал плагин на spigotmc.org с именем TranslatorReloaded и каждый день вносил обновления. Также было снято видео по настройке моего плагина для владельцев сервера.
Плагин - https://www.spigotmc.org/resources/translator.97555/
Видео - https://www.youtube.com/watch?v=H_uohCot5xk
Документация - https://clck.ru/YihVE (Приложение 1)
Исходники - https://github.com/Niqita2008/translator.git (Приложение 2)
В плагине используется конфиг-файл, с его помощью можно изменить сообщения плагина и их цвет.
В ядрах, таких, как Spigot есть поддержка разрешений(permissions) – группе игроков можно выдать разрешение на то или иное действие.
Для перезагрузки плагина используется команда /tra reload – для ее исполнения необходимо иметь право tra.admin
С версии 0.3 есть поддержка обработки событий – когда админ заходит на сервер – проверяется – работает ли все правильно и ему отправляется сообщение, если что – то не так.
Также в этой версии есть проверка обновлений – ее частота указана в конфиг файле. Если админ заходит на сервер – он оповещается об обновлении.
В версии 0.4 появилось авто определение языка и авто обновление, не требующее ручного перехода на страницу, скачивания файла и удаления предыдущей версии с перезапуском сервера, поэтому в 0.4 появилась команда «/tra update», с помощью нее можно обновить плагин до последней версии.
Также в плагине есть команда /tra check – она проверяет, корректна ли: ссылка на Google apps script и в версии 0.4 Detect Language API ключ.
Итак, используя язык программирования Java, с помощью Spigot, был написан плагин в среде разработки IntelliJ Idea с дополнением Minecraft Development. Сборка проекта производится с помощью Maven. Также я написал google apps script для использования translator api бесплатно.
Проведение практического эксперимента с плагином
Плагин начинает свою работу при запуске сервера, и, когда пользователь заходит в игру, методом onAdminJoin() проверяется, есть ли у него разрешение «tra.admin», и если да, ему отправляется сообщение о текущих обновлениях и настроен ли плагин правильно. И если нет, игроку ничего не отправляется.
Если плагин настроен некорректно, в консоль сервера каждую минуту будет писаться сообщение «Translator plugin is configured incorrectly!»
В версии 0.3, при вводе игроком команды «/tra en I Don’t Know Who Is I», в google translator api приходит запрос перевода: «I Don’t Know Who Is I», с английского на язык сервера (в данном случае - русский).
Мы получаем ответ: «Я Не Знаю Кто Я». Далее, к началу фразы приписывается «[{язык, с которого переводили}]» (в данном случае - [en]), и она отправляется в чат от имени игрока.
В версии 0.4 я добавил авто определение языка, и теперь если ввести в чат: «/tra en Я Не Знаю Кто Я», определяется язык предложения с помощью «Detect Language API», и только потом переводится. В игровой чат отправляется: «[en] I Don’t Know Who Is I».
Заключение
Языковой барьер проблема нашего мира. Игроков Minecraft очень много, и они распространены по всему миру. Я столкнулся с проблемой, что игроки в многопользовательской игре, говорящие на разных языках, не понимают друг друга. И я решил написать свой плагин для сервера на ядре Spigot. Главная функция плагина заключается в переводе сообщений игроков на язык сервера.
После изучения информационных источников, был сделан вывод, что удобнее всего использовать ядро сервера Spigot и его плагины. В ходе работы я изучил имеющиеся плагины для перевода сообщений игроков, были сделаны выводы, что ни один из существующих плагинов не удовлетворяет всем моим требованиям, а именно: простота настройки, постоянное обновления, «свежая» версия, наличие поддержки для новых версий игры Minecraft.
Написал свой плагин, используя язык программирования Java, с помощью Spigot, в среде разработки IntelliJ Idea с дополнением Minecraft Development. Сборка проекта производится с помощью Maven. Также я развернул google apps script как Web-приложения для использования translator api бесплатно. Мой плагин был опубликован на spigot, и заинтересовал других игроков, им пользуются и скачивают. Интерес к плагину подтверждают его скачивания – более 80 с 12 ноября.
Я обновляю его функционал, исправляю ошибки и публикую обновления. Мною был получен практический опыт в программировании на языке Java (в частности, написания spigot-плагинов). Уверен, что мой плагин приносит пользу игрокам со всего мира. Его можно использовать, как наглядное пособие на уроках информатики, побуждая школьников изучать языки программирования, применяя знания в компьютерных играх.
Список использованной литературы
Джошуа Блох. Java. Эффективное программирование. - Лори, 2014 – 310 с.
Макарский Д.Д., Никоноров А.В. История компьютерной эры. - Эксмо, 2016 – 256 с.
Миллер Меган. Все секреты Minecraft. Красный камень. - Эксмо, 2020 – 128 с.
Семакин И.А., Информатика: Базовый курс /Семакин И.А., Залогова Л., Русаков С., Шестакова Л. – Москва: БИНОМ, 2005 – 105 с.
О'Брайен Стивен. Minecraft. Полное и исчерпывающее руководство. 4-е издание - Эксмо, 2017 – 352 с.
Островский В. А. Информатика. Теория и практика. - 9-е изд. - М.: Оникс, 2008. - 608с.: ил.
Эккель Брюс. Философия Java. – Питер, 2019 – 1168 с.
Приложение 1
Документациядляверсии 0.3:
For the plugin to work, a link to the Google Apps script is required. (Because scripts with the Google Translate API have quotas)
You can create it like this:
1)Create a new script with code like this in Apps Script – Google Apps Script
function doGet(e){
e = e || mock;
var sourceText =''
if(e.parameter.q){
sourceText = e.parameter.q;
}
var sourceLang ='';
if(e.parameter.source){
sourceLang = e.parameter.source;
}
var targetLang ='en';
if(e.parameter.target){
targetLang = e.parameter.target;
}
var translatedText =LanguageApp.translate(sourceText, sourceLang, targetLang,{contentType:'html'});
returnContentService.createTextOutput(translatedText).setMimeType(ContentService.MimeType.JSON);}
2) Click Publish -> Deploy as Web Application -> Who has access to the application: Anyone, even anonymous -> Deploy.
3) Copy your url web app, paste it into config.yml.
If you are using Windows on the server, set the server_operating_system parameter to windows
Приложение 2
Файлы исходников на версию 0.3:
config.yml:
messages:
notCorrect: '&cThese parameters are incorrect (/tra help or just /tra)'
onlyPlayer: '&cOnly player can execute this command!'
prefix: '[%lang%] '
reloaded: '&aPlugin reloaded!'
noPerms: '&cYou don''t have permission'
adminHelp:
- '/tra check - checks the link to google apps script'
- '/tra reload - reloads plugin'
help:
- '&7Translate your message to %server_lang%'
- '&7Usage - /tra en Hi guys!'
message_length_error: '&aMessage cannot be more than 55 characters and less than 5 characters!'
settings:
time_between_checks_for_updates: 1500
max_message_length: 55
min_message_length: 5
server_lang: 'ru'
server_operating_system: 'linux'
google_apps_script_link: ''
# noPerms - if sender doesn't have tra.admin permission
# %lang% replaced with client language
# don't use %lang% in help messages
# Linux and Windows support (default Linux)
#time in seconds
plugin.yml:
name: Translator
version: '0.3'
main: niqita.translator.Main
authors: [ translator ]
description: Translate player messages into the language of your server
commands:
tra: {}
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>niqita</groupId>
<artifactId>translator</artifactId>
<version>0.1</version>
<packaging>jar</packaging>
<name>Translator</name>
<description>Translate player messages into the language of your server</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<repositories>
<repository>
<id>spigotmc-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/groups/public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.17.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-validator</groupId>
<artifactId>commons-validator</artifactId>
<version>1.3.1</version>
</dependency>
</dependencies>
</project>
EventListener.java:
#
Translator.java:
package niqita.translator;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
public class Translator {
public Translator(String from, String text, String to, String link) throws IOException {
translation = translate(from, text, to, link);
}
private boolean isWorks;
public boolean isWorks() {
return isWorks;
}
private String translation;
public Translator(String linkToCheck) {
isWorks = check(linkToCheck);
}
public String getTranslation() {
return translation;
}
private String translate(String langFrom, String text, String langTo, String link) throws IOException {
StringBuilder response = new StringBuilder();
HttpURLConnection con = (HttpURLConnection) new URL(link + "?q=" + URLEncoder.encode(text, "UTF-8") + "&target=" + langTo + "&source=" + langFrom).openConnection();
con.setRequestProperty("User-Agent", "Mozilla/5.0");
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
while (null != (inputLine = in.readLine())) response.append(inputLine);
in.close();
return response.toString();
}
public boolean check(String link) {
boolean isWorks = true;
try {
if (!new Translator("ru", "с днем стула!", "en", link).getTranslation().equals("happy chair day!"))
throw new MalformedURLException();
} catch (MalformedURLException e) {
isWorks = false;
} catch (IOException e) {
e.printStackTrace();
isWorks = false;
}
return isWorks;
}
}
Main.java:
package niqita.translator;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
public class Main extends JavaPlugin {
private static Main instance;
@Override
public void onEnable() {
reloadConfig();
instance = this;
boolean[] isUpdateFound = {false};
boolean isWorks = new Translator(Objects.requireNonNull(getConfig().getString("settings.google_apps_script_link"))).isWorks();
if (!isWorks) {
Bukkit.getScheduler().scheduleSyncRepeatingTask(this, () -> getServer().getConsoleSender().sendMessage(ChatColor.DARK_RED
+ "Translator plugin is configured incorrectly!"), 50, 1200);
}
Bukkit.getScheduler().scheduleSyncRepeatingTask(this, () -> {
if (getVersion().equals(getDescription().getVersion()))
getServer().getConsoleSender().sendMessage(ChatColor.GREEN + "[Tra] No updates found...");
else {
getServer().getConsoleSender().sendMessage(ChatColor.GOLD + "[Tra] Update found! see https://www.spigotmc.org/resources/translator.97555/");
getServer().getConsoleSender().sendMessage(getVersion());
getServer().getConsoleSender().sendMessage(getDescription().getVersion());
isUpdateFound[0] = true;
}
}, 100, getConfig().getInt("settings.time_between_checks_for_updates") * 20L);
Bukkit.getPluginManager().registerEvents(new EventListener(isWorks, isUpdateFound[0]), this);
Objects.requireNonNull(getCommand("tra")).setExecutor((sender, command, label, args) -> {
try {
boolean isArgsWrong = true;
if (args.length == 1 && args[0].equals("reload")) {
isArgsWrong = false;
if (sender.hasPermission("tra.admin")) {
reloadConfig();
send(sender, "messages.reloaded");
} else {
send(sender, "messages.noPerms");
return false;
}
} else {
if (args.length <= 2) {
if (args.length == 0 || args[0].equals("help") && args.length == 1) {
isArgsWrong = false;
if (!(getConfig().isList("messages.help"))) throw new NullPointerException();
for (int o = 0; o < getConfig().getStringList("messages.help").size(); o++) {
sender.sendMessage(ChatColor.WHITE + getConfig().getStringList("messages.help").get(o)
.replace("%server_lang%", Objects.requireNonNull(getConfig().getString("settings.server_lang"))).replace("&", "§"));
}
} else if (args.length == 1 && args[0].equals("ahelp")) {
isArgsWrong = false;
if (sender.hasPermission("tra.admin")) {
for (int o = 0; o < getConfig().getStringList("messages.adminHelp").size(); o++) {
sender.sendMessage(ChatColor.WHITE + getConfig().getStringList("messages.adminHelp").get(o)
.replace("%server_lang%", Objects.requireNonNull(getConfig().getString("settings.server_lang"))).replace("&", "§"));
}
} else send(sender, "messages.noPerms");
} else if (args[0].equals("check") && args.length == 2) {
isArgsWrong = false;
if (sender.hasPermission("tra.admin")) {
try {
new URL(args[1]).toURI();
if (args[1].startsWith("https://script.google.com")) {
if (new Translator(args[1]).isWorks())
sender.sendMessage(ChatColor.GREEN + "It works!");
else throw new VerifyError();
} else throw new VerifyError();
} catch (MalformedURLException | URISyntaxException exception) {
sender.sendMessage(ChatColor.RED + "Url is not correct");
} catch (VerifyError error) {
sender.sendMessage(ChatColor.RED + "It not works!(");
}
} else {
send(sender, "messages.noPerms");
return false;
}
}
}
if (args[0].length() == 2 && !args[0].equals(getConfig().getString("settings.server_lang")) && args.length > 1) {
isArgsWrong = false;
if (isWorks) {
if (sender instanceof Player) {
StringBuilder translate = new StringBuilder();
for (int i = 1; i < args.length - 1; i++) translate.append(args[i]).append(" ");
translate.append(args[args.length - 1]);
try {
Translator translator = new Translator(args[0], translate.toString(),
Objects.requireNonNull(getConfig().getString("settings.server_lang")),
Objects.requireNonNull(getConfig().getString("settings.google_apps_script_link")));
if (Objects.requireNonNull(getConfig().getString("settings.server_operating_system")).equalsIgnoreCase("windows")) {
translate = new StringBuilder(encoderWindows(translator.getTranslation()));
} else translate = new StringBuilder(translator.getTranslation());
translate = new StringBuilder(Objects.requireNonNull(Objects.requireNonNull(getConfig().getString("messages.prefix"))
.replace("&", "§")).replace("%lang%", args[0]) + translate);
} catch (IOException e) {
e.printStackTrace();
}
if (getConfig().isInt("settings.max_message_length") && getConfig().isInt("settings.min_message_length")) {
if (translate.length() <= getConfig().getInt("settings.max_message_length")
&& translate.length() >= getConfig().getInt("settings.min_message_length")) {
((Player) sender).chat(translate.toString());
} else send(sender, "messages.message_length_error");
} else throw new NullPointerException();
} else send(sender, "messages.onlyPlayer");
}
}
}
if (isArgsWrong) {
send(sender, "messages.notCorrect");
sender.sendMessage(ChatColor.YELLOW + "Hey! Use /tra ahelp to view admin tools!");
}
return true;
} catch (NullPointerException e) {
sender.sendMessage(ChatColor.RED + "Config.yml is invalid, loading default config...");
getServer().getConsoleSender().sendMessage(ChatColor.RED + "Config.yml is invalid, loading default config...");
reloadConfig();
return false;
}
});
}
private void send(CommandSender sender, String path) {
sender.sendMessage(Objects.requireNonNull(getConfig().getString(path)).replace("&", "§"));
}
private String encoderWindows(String data) throws UnsupportedEncodingException {
byte[] buffer = data.getBytes("Windows-1251");
return new String(buffer, StandardCharsets.UTF_8);
}
public void reloadConfig() {
super.reloadConfig();
saveDefaultConfig();
getConfig().options().copyDefaults(true);
saveConfig();
}
public static Main getInstance() {
return instance;
}
@Override
public void onDisable() {
}
public String getVersion() {
StringBuilder sb = new StringBuilder();
try {
URL oracle = new URL("https://api.spigotmc.org/legacy/update.php?resource=97555");
BufferedReader in = new BufferedReader(new InputStreamReader(oracle.openStream()));
String inputLine;
while ((inputLine = in.readLine()) != null)
sb.append(inputLine);
in.close();
} catch (IOException e) {e.printStackTrace();}
return sb.toString();
}
}