Как написать свой софт для майнкрафт

Время на прочтение
6 мин

Количество просмотров 12K

Еще с детства я начал покорять бесконечные просторы Minecraft. Естественно о разработке в то время никакой речи не шло. Но с недавних пор загорелся идеей создать о свой проект серверов.

На Java до этого никогда не писал, но есть бекграунд на других языках, поэтому осталось только приспособиться. Соотвественно разработка плагинов, Bukkit и другие библиотеки вижу впервые, но посмотрев несколько туторов, стала понятна примерная концепция.

Ранее писал на таких языках как PHP, JS. В данный момент веду разработку на языке Go. Сильно привык к «гошке» и его синтаксису и в процессе написания плагина часто использовал синтаксис Go для написания логических конструкций.

Мне не сильно хотелось использовать какие-то готовые решения, ведь тогда не будет углубленных знаний, которые я получу в процессе написания кода. Хочется одновременно и поучить Java и написать что-то свое (самое главное).

В этой статье я не буду затрагивать процесс настройки окружения, установки IDE и стороннего софта.

Идея плагина

На серверах часто используются постройки, находящиеся в пустоте, например летающие лобби, острова. Такую модель постройки мы выбрали вместе с моим другом: летающие острова. Одной из проблем таких построек — Игрок может провалиться в пустоту и не выбраться.

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

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

Создаем сам плагин

Назвал я плагин просто — VoidTeleport.

Первым делом создал класс для управления конфигурацией плагина.

public class Config {
    private static File file;
    private static FileConfiguration config;

    private static final String fileNameConfig = "config.yml";

    /**
     * Initializes the static Config class.
     */
    public static void init() {
        // Получаем инстанс нашего плагина.
        Plugin plugin = Bukkit.getServer().getPluginManager().getPlugin(VoidTeleport.PluginName);
        if (plugin == null) {
            // На этом моменте что-то пошло не так, 
          	// нужно обработать и залогировать.
            Bukkit.getLogger().log(
                    Level.WARNING,
                    MessageFormat.format("Cannot get plugin {0}", VoidTeleport.PluginName)
            );
            return;
        }

        file = new File(plugin.getDataFolder(), fileNameConfig);

        // Мы не знаем существует ли файл, поэтому пытаемся создать его.
      	// Если файл уже есть, то выражение file.createNewFile() вернет false. 
        try {
            if (file.createNewFile()) {
                plugin.getLogger().log(
                        Level.INFO,
                        MessageFormat.format("New config file with name {0} was created", fileNameConfig)
                );
            }
        } catch (IOException e) {
            plugin.getLogger().log(Level.SEVERE, e.toString());
            return;
        }

        // На данно моменте наш конфиг пустой, 
      	// поэтому подгружаем его из файла.
        reload();
    }

    /**
     * Getter
     * @return FileConfiguration
     */
    public static FileConfiguration get() {
        return config;
    }

    public static void reload() {
      	// Самый простой анмаршаллер YAML из файла.
        config = YamlConfiguration.loadConfiguration(file);
    }

Отлично! Класс для работы с конфигом уже есть, теперь нужно определиться со структурой файла config.yml. Нужно реализовать поддержку для разных миров, поэтому не придумал ничего проще, как просто указать список нужных миров.

worlds:
    # Наименование мира, например spawn, world, world_the_end
  - name: spawn
  	# Координаты для респавна игрока при падении в пустоту
    spawnLocation:
      x: 0
      y: 0
      z: 0

Конфиг есть, теперь можно приступить к созданию обработчика событий. Мой выбор пал на событие EntityDamageByBlockEvent. Можно было бы и слушать событие PlayerMoveEvent, но оно случается гораздо чаще, чем триггер на получение урона. Лишняя нагрузка на сервер не нужна, поэтому стал слушать урон.

public class PlayerDamageListener implements Listener {
  	// Хеш мапа в которой хранится наименования мира и точка телепортации.
    private HashMap<String, Location> worlds = new HashMap<>();

    @EventHandler
    public void onPlayerDamage(EntityDamageByBlockEvent e) {
        if (!(e.getEntity() instanceof Player)) {
            // Это не игрок.
            return;
        }

        if (e.getCause() != EntityDamageEvent.DamageCause.VOID) {
            // Урон не от пустоты.
            return;
        }

        Player player = (Player) e.getEntity();

      	// Получаем мир, в котором находится Игрок.
        World world = player.getWorld();
      	
      	// Пытаемся найти в хеш мапе значение по наименованию мира.
        Location spawnLocation = this.worlds.get(world.getName());
        if (spawnLocation == null) {
            // К этому миру не действует правило телепорта.
            return;
        }

        // Данный код является костылем, который я быстро сообразил.
      	// Проблема в том, что мир может быть = null.
      	// В таком случае устанавливаем мир на тот, в котором находится игрок.
        if (spawnLocation.getWorld() == null) {
            spawnLocation.setWorld(world);
        }

        // Добрались до самого главного.
      	// Отменяем событие, которое наносит урон игроку.
        e.setCancelled(true);

        // Отменяем сам урон от падения, 
				// чтобы при телепортации игрок не разбился.
        player.setFallDistance(0);

        // Телепортируем игрока.
        player.teleport(spawnLocation);

        // Доабвляем анимацию из частиц при попадании на точку телепортации.
        Spiral.spawn(player);
    }

    @SuppressWarnings("unchecked")
    public void updateWorlds(@Nullable ArrayList<HashMap<String, Object>> listWorlds) {
        if (listWorlds == null) {
          	// Ну если null, так null - ничего не делаем.
            return;
        }

        // Очищаем мапу.
        this.worlds = new HashMap<>();

        for (HashMap<String, Object> world: listWorlds) {
            String worldName = (String) world.get("name");
            if (Objects.equals(worldName, "")) {
                // Тут хорошо бы залогировать, но просто скипаем.
                continue;
            }

            Location spawnLocation = Location.deserialize((Map<String, Object>) world.get("spawnLocation"));
            
          	// Т.к. мир у нас не указан, поэтому получаем его.
          	spawnLocation.setWorld(Bukkit.getWorld(worldName));

            // Сохраняем в хеш мапу.
            this.worlds.put(worldName, spawnLocation);
        }
    }
}

Тепер разберем вызов эффекта анимации при телепортации Spiral.spawn(player). Назвал класс Spiral, потому что эффект будет в виде спирали.

Т.к. это мой первый плагин, то не стал заморачиваться с Пакетами и ProtocolLib.

Описываем анимацию в отдельном классе Spiral. Я предпочел реализовать спираль под названием Helix — достаточно простая в реализации модель. Пришлось немного вспомнить тригонометрию, но у меня получилось!

public class Spiral {
    public static void spawn(@NotNull Player player) {
        Location location = player.getLocation();
				
      	// Задаем радиут спирали.
        double radius = 0.5;
				
        for (double y = 0; y <= 23; y += 0.1) {
            double x = radius * Math.cos(y);
            double z = radius * Math.sin(y);

            Location particleLocation = new Location(location.getWorld(), location.getX(), location.getY(), location.getZ());
            player.spawnParticle(Particle.REDSTONE, particleLocation.add(x, y / 10, z), 2, new Particle.DustOptions(Color.AQUA, 1.0F));

            try {
              	// Думаю, что это плохо, но для первого раза сойдет.
                TimeUnit.NANOSECONDS.sleep(1);
            } catch (InterruptedException e) {
                Bukkit.getLogger().log(Level.SEVERE, e.toString());
            }
        }
    }
}

Почему в коде 23? Это число является ограничением для координаты y. Т.е. по сути спираль будет подниматься вверх на y = 2.3. Как можно заметить, при указании смещения particleLocation.add(x, y / 10, z) y делится на 10. Еще одной причиной стало то, что спираль не успевает несколько раз «обернуть» игрока.

Собираем все вместе

Наконец можем собрать наш код в единой точке и протестировать, что получилось.

public final class VoidTeleport extends JavaPlugin {
    public static final String PluginName = "VoidTeleport";

    @Override
    public void onEnable() {
        getLogger().log(Level.INFO, "Plugin enabled!");

        // Инициализируем конфиг
        Config.init();

      	// Регистрируем обработчик событий для входщего урона
        this.registerDamageEvent();
    }

    @Override
    public void onDisable() {
        getLogger().log(Level.INFO, "Plugin disabled!");
    }

    @SuppressWarnings("unchecked")
    private void registerDamageEvent() {
      	// Инициализируем обработчик
        PlayerDamageListener damageListener = new PlayerDamageListener();
      
      	// Достаем из конфига нужные значения и обновляем хеш мапу в обработчике
        damageListener.updateWorlds((ArrayList<HashMap<String, Object>>) Config.get().get("worlds"));
				
      	// Регистрируем новое событие на сервере
        getServer().getPluginManager().registerEvents(damageListener, this);
    }
}

Результат

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

И без указания мира в конфиге.

Решил сделать гайд, буду объяснять внятно на человеческом языке для людей) ?
В этом гайде разжёвано всё и вся вам придётся только проглотить сиё чудо (а в идеале припастись чайком так как читать придётся много))
Если кто-то что-то не понял, обращаться тут или же в ДС(MubiCrazy#7141) | ВК(vk.com/mubi_zero) ?
Пункты:
1) Изначальная подготовка любого кодера! (Обязательная часть)
2) Знакомство с кодом! (Присмотритесь, если поработать с нескольким десятком исходников, будет ясно, что за что отвечает)
3) Для тех кто хочет сразу получить «Свой софт»! (Это называется ReName, не злоупотребляйте)
4) Как обновить оффсеты (Не обязательно, т.к. все софты имеют авто обновление оффсетов)
5) Как обновить индексы! (Необходимы своевременные обновления ибо зачастую краши из-за них)
6) Как дебажить! (Дебаг — функция определения неисправностей в коде, в софтах используется для выяснения: «почему крашит»)

И так, погнали значиться:


1) Надо скачать *Visual Studio 2019* или *2017* не важно если пк старый то *2017* только с дополнениями для языка программирования C++
1.1) Вам предложат скачать сразу несколько дополнений, но как уже написал вам нужен только яп(язык программирования) C++
1.2) После установки можете найти в инете ключи активации, благо их там хуево туча
1.3) Я сам юзаю «Microsoft Visual Studio 2019 Enterprise» (вот лт ключ на него: NYWVH-HT4XC-R2WYW-9Y3CM-X4V3Y)
1.4) Также, вам не обойтись без установки программы «Microsoft DirectX SDK (June 2010)» !Обязательно именно с таким названием!


2) Теперь вам предстоит скачать какой-нибудь исходник чита.
2.1) Если у вас уже есть исходник чита то это гуд
2.2) Если у вас нет исходника, то вот вам исходник чита (CSGOSimple no ad)(там код ясный удобный как раз таки для новичка)
2.3) Хорошечно значится, у вас всё готово для редактирования, теперь заходим в папку с исходником и открываем файл .sln или иногда .vcxproj

3) Как только открыли, у вас посередине пусто, надо справа дважды нажать на какой-нибудь файл!
3.1) Теперь надо нажать ctrl+f и в окошке сверху поискать старое название чита, например тотже Osiris
3.2) Везде где увидите «Staroe nazvanie» (именно в ковычках) можете поменять на своё любое (обязательно писать внутри кавычек)
3.3) Сверху будет 2 окошки Посмотреть вложение 1137 там будет у вас 2 выбора на 2 окошках ! Всегда выбирайте Release и X86!
3.4) Нажмите правой кнопкой мыши сюда: Посмотреть вложение 1138 у вас начнётся компилирование, а в конце снизу выдаст путь до собранной .dll


4) Чтобы не крашило нужно обновить оффсеты и паттерны (т.е. значения смещений и переменных)
4.1) Чтобы найти оффсеты, нужен поиск по таким значениям m_iHealth или m_ArmorValue(они выглядят так: 0x100 или 0x3918)
4.2) После того как нашли, идём в собранный список оффсетов из последних обновлений ксго: HazeDumper (no ad)
4.3) Допустим, вы нашли в исходнике значение «m_iHealth — 0x101», а в списке: а в списке: «m_iHealth — 0x100» , то надо копировать значение из списка и ставить взамен старых значений в исходнике!


5) Как сделать чтоб не крашило, чтоб обновить индексы вам нужно понять как они выглядят. Выглядят они так: 157 или 452.

5.1) Теперь узнать новые индексы, благо раздобыл из UC (no ad) таблицу индексов:

Посмотреть вложение 1139

5.2) Ищем (Ctrl+f) названия из таблицы: IsPlayer, IsWeapon и т.д., если они отличаются от последних указанных в таблице, меняем.

5.3) Индексы обновляются не очень часто, и если и случается такое, то 3-4 штуки за раз.

6) Почему крашит, для начала нужно скомпилировать софт в Debug | x86.

6.1) Запускаем CS:GO и в Visual Studio нажимаем: Посмотреть вложение 1140, выбираем кс.

6.2) Инжектим собранную длл-ку в игру через любой инжектор (я рекомендую Proccess Hacker 2)-

6.3) Если произойдёт краш игры, то вас перебросит в Visual Studio (проще VS или визуалка) и укажет на причину краша.

В этом гайде разжёвано всё и вся вам придётся только проглотить сеё чудо (а в идеале припастись чайком так как читать придётся много))
!ВАЖНО! Если чит крашит после обновления оффсетов, то проверяйте обновлены ли в сурсе паттерны, если индексы и паттерны новые, то та или иная функция работает неисправно, а это уже надо дебажить, проверять все варны и т.д.
P.S Знающие, не надо на меня орать, если я что-то не правильно сказал, просто выскажите своё мнение)
А так, я вам облегчил жизнь от вопросов новичков.
Всем добра и пару тянок :giggle:

Таблица — UnknownCheats(юзера не помню)
Полоски градиента — Ozelotick-PPHUD

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

Здесь я показываю как сделать свой первый плагин, если вы хотите пойти дальше, вам нужно изучить Java, а также внимательно изучить  SpigotAPI

Поехали!

1 Этап. Скачивание софта.

Лично я рекомендую использовать IntelliJ IDEA — ССЫЛКА

Очень удобный IDE для разработки на Java.
Скачивайте Community версию, она бесплатная!

Запускаем программу. Кликаем на кнопку Plugins
image.png.89e00f54eefb796a0d8bb9c3f9d4f882.png

В поисковике пишем Minecraft Development
image.png.8899c117ce9340837704f3d8c85592cd.png

Устанавливаем, перезапускаем программу.

2 Этап. Подготовка проекта.

После запуска программы вас встречает окно выбора проектов, если у вас это первый проект, то никаких других  не будет.
Справа сверху находим кнопку New Project , кликаем
image.thumb.png.e3b55f92bc1351f9cc14eaacab5c0bfa.png

После клика мы видим окно с выбором шаблона проекта. Выбираем Minecraft, далее Spigot Plugin.
Project SDK — это версия Java которую вы хотите выбрать для своего проекта, если это плагин ниже 1.12.2, то выбирайте 1.8, если это плагин выше, то выбирайте 11, в моём случае это 1.8 (Java 8).
image.png.b6d2b1a68b44743d24bd7a12840a7e1c.png

Next
image.png.1873f83b70c909bbf5df51c681f802c9.png

Далее видим, что нам предлагает вписать GroupId, ArtifactId и Version

GroupId — путь ко всем вашим класс файлам плагина.

ArtifactId — название плагина, лучше писать латинскими буквами (желательно без пробелов)

Version — версия плагина

Пример:

image.thumb.png.b9393d687d58683925a72fe5e2a3d354.png

Next

Очень много разных значений, обьясню по порядку.

Plugin Name — название плагина


Main Class Name — путь к класс файлу плагина ( Лучше оставить как есть)


Minecraft Version — версия майнкрафта


Description — описание плагина (Можно оставить пустым)


Authors — разработчики плагина (Можно оставить пустым)


WebSite — сайт плагина (Можно оставить пустым)


Log Prefix — префикс логгирования плагина в консоль (Можно оставить пустым, по умолчанию используется название плагина в качестве префикса)


Load Before — загружать плагин после определённых плагинов (Лучше оставить пустым)


Depend — зависимости плагина (Какие плагины обязательно требуются для работы нашего плагина, например Vault. Лучше оставить пустым, если вы не используете API других плагинов)


Soft Depend — не обязательные зависимости плагина (Какие плагины необязательно требуются для работы нашего плагина)

image.thumb.png.a1ff4460cd2b0a9124b573f06551faf8.png

Next

Project name — название проекта (можно оставить как есть)


Project location — путь к проекту

image.thumb.png.1c7457aec253d7981210d057c88d328b.png

Finish

3 Этап. Написание плагина.

Теперь мы перешли к написанию нашего плагина. Я напишу команду /grant для выдачи доната другому игроку.
Открываю главный класс

import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;

public final class TestPlugin extends JavaPlugin {

    @Override
    public void onEnable() {
        // Регистрируем команду /grant
        getCommand("grant").setExecutor((sender, command, label, args) -> {
            // Если у отправителя нету прав test.plugin.grant выводим сообщение с ошибкой
            if (!sender.hasPermission("testplugin.grant")) {
                sender.sendMessage("§7[§c§l!§7] §cУ вас недостаточно прав!");
                // Завершаем выполнение
                return true;
            }
            if (args.length == 0 || args.length == 1) {
                // При вводе /grant отправляем сообщение
                // также с /grant (ник) ,так как не все аргументы учтены
                sender.sendMessage("§7[§c§l!§7] §c/grant (ник) (группа)");
                return true;
            }
            // Аргумент 0 отвечает за строку игрока (ник)
            String player = args[0];
            // Player (переменная) = Bukkit.getPlayer(player) - получаем переменную с игроков доступным на сервере
            Player target = Bukkit.getPlayer(player);
            String groupname = "Неизвестно";
            String group = "";
            // Если аргумент 1 (аргумент с группой) = premium, то
            if (args[1].equalsIgnoreCase("premium")) {
                // Перменная group = premium
                group = "premium";
                // Переменная groupname = §3Premium
                groupname = "§3Premium";
            }
            if (args[1].equalsIgnoreCase("vip")) {
                group = "vip";
                groupname = "§6Vip";
            }
            // Если игрок оффлайн, выводим сообщение и пытаемся выдать донат
            if (target == null) {
                sender.sendMessage(ChatColor.RED + "§7[§c§l!§7] §cДанный игрок не онлайн!");
                sender.sendMessage(ChatColor.RED + "§7[§c§l!§7] §cПопытка выдачи привилегии...");
                Bukkit.getServer().dispatchCommand(Bukkit.getServer().getConsoleSender(), "lp user " + player + " parent add " + group);
                return true;
            }
            // Если аргумент 0 (ник игрока) совпадает с ником отправителя команды, выводим сообщение
            if (target.getName().equals(sender.getName())) {
                sender.sendMessage("§7[§c§l!§7] §cВы не можете выдать привилегию самому себе!");
                return true;
            }
            // Если количество аргументов равняется 2 и аргумент с группой равняется vip или равняется premium и игрок не оффлайн,
            // то выполняем команду через консоль (lp user (ник) parent add (группа)
            if (args.length == 2 && args[1].equalsIgnoreCase("vip") || args[1].equalsIgnoreCase("premium") && target != null) {
                Bukkit.getServer().dispatchCommand(Bukkit.getServer().getConsoleSender(), "lp user " + target.getName() + " parent add " + group);
                sender.sendMessage("§7[§c§l!§7] §aВы успешно выдали игроку §c" + target.getName() + " §aпривилегию " + groupname);
                target.sendMessage("§7[§c§l!§7] §aАдминистратор §d" + sender.getName() + " §aвыдал вам привилегию " + groupname);
                return true;
            }
            return true;
        });

    }

    @Override
    public void onDisable() {
    }
}

image.png.59fffc8b3ae0a6ff3853f54967ba3a84.png

Далее открываю plugin.yml и в конце файла пишу

commands:
  grant: {}

После всех манипуляций я могу спокойно скомпилировать свой плагин, для этого мне нужно справа сверху кликнуть на кнопку Maven

image.thumb.png.f58b0c7892e1f069a5ec447de536dc16.png

И кликнуть на package

image.png.f949936b4031977f255b8d84daa3c477.png

И вуаля, мой плагин успешно скомпилировался. Теперь заходим в папку с проектом и видим папку target
image.png.18b141aa1b72d9a5540182175484e96a.png

В ней и лежит мой готовый плагин

image.png.ad2a18403a058addc661bdd0f12c5188.png

На этом всё, спасибо за внимание!

image.png


Изменено 20 июля, 2022 пользователем Jodex

Понравилась статья? Поделить с друзьями:
  • Как написать свой винлокер
  • Как написать свой блокчейн
  • Как написать свист на бумаге
  • Как написать самоанализ воспитательной работы
  • Как написать салют