Разработка плагинов Bukkit. «Заставляем» клиент перейти по ссылке

в 9:49, , рубрики: bukkit, java, minecraft, plugin, метки: , , ,

Позавчера, когда нечего было делать, пришла в голову мысль о том, как можно интересно организовывать ивенты на серверах minecraft. Веселые «крысиные» бега это очень интересно и даже местами забавно, но больше интересно это для админов, наблюдать с высоты как копошатся внизу игроки и кидать им хлеб и мясо. Во время подобных ивентов ЧСВ организаторов поднимается до over 9000. Я, лично, подобные мероприятия недолюбливаю (странно, да?).

В чем же заключается моя «супермегаидея»? А в том, что бы организовать те же бега, но более организовано, с хитрыми головоломками. Самих головоломок я, естественно, еще не придумал, но вот способ выдачи подсказок меня и занял. Суть в том, что бы передавая особый сигнал от клиента к серверу заставлять клиент открыть ссылку в броузере по-умолчанию. Задание не архисложное, но за минуту я до него не дошел. Была безумная идея менять что либо в обмене данными сервер-клиент. Но потом, пришла в голову более простая, но, как мне кажется, более красивая идея.

Самый простой способ обмена информацией между клиентом и сервером — это чат. Так что нет смысла изобретать велосипед, если все у же есть. Просто отсылаем игроку сообщение вида: LINK: example.com, в клиенте это обрабатывается и в случае если строка не подходит отправляет ее в чат, если же пришла ссылка, то открывается страничка. Работает все в связке мод-плагин.

Скриншот

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

Нужно декомпилировать клиент при помощи MCP и в файле net.minecraft.src.ChatLine производить все изменения. Первое, что я добавил — функцию для перехода по ссылке:

private void openLink(String link)
{
    try
    {
        Process p = Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + link);//вызов rundll32
    }
    catch (IOException e)
    {
        e.printStackTrace();
    }
}

Я думаю, что вопросов тут возникнуть не должно. И чуть не забыл, код предназначен для Windows, что бы обеспечить универсальность нужно использовать класс Desctop, но этот способ требует подключения дополнительных библиотек.

import java.net.URI;
import java.net.URISyntaxException;
import java.awt.Desktop;

private void openLink(String link)
{
    Desktop desktop;
    if (Desktop.isDesktopSupported())
    {
        desktop = Desktop.getDesktop();
        if (desktop.isSupported(Desktop.Action.BROWSE))
    {
    URI uri;
    try
    {
        uri = new URI(link);
        desktop.browse(uri);
    }
    catch (IOException ioe)
    {
        ioe.printStackTrace();
    }
    catch (URISyntaxException use)
   {
        use.printStackTrace();
    }
}

С переходом разобрались, теперь нужно отфильтровать из потока чата нужную нам ссылку. Плагин будет отсылать код типа LINK: dmwatson.ru:

Фильтрующую процедуру я описал так:

public String ParseLink(String Str2parse)
{
    if(Str2parse.startsWith("LINK: "))//если строка начинается с LINE:, то
    {
        openLink((String) Str2parse.subSequence("LINK: ".length(), Str2parse.length()));//открывает ссылку в броузере
        return "Opening page " + (String) Str2parse.subSequence("LINK: ".length(), Str2parse.length());//и показывает об этом сообщение
    }
    return Str2parse;//иначе просто возвращаем строку как есть.
}
Далее просто цепляем ParseLink в ChatLine:

public ChatLine(String par1Str)
    {
        message = ParseLink(par1Str);
        updateCounter = 0;
    }

Всё. С клиентом закончено.

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

Но на всякий случай серверную часть я тоже опишу. Суть плагина:

Пишем на табличке в первой строке [LINK], далее на трех строках ссылку (знаков там мало, но bit.ly никто не отменял). Так что приступаем. Плагин будет очень простым, так как сложности мне не нужны. Так что к делу.

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

Плагин будет называться LinkSender, через пару дней, когда я его доделаю, отправлю на dev.bukkit.org, пока же опишу его тут.

Итак, любой плагин начинается с plugin.yml:

name: LinkSender
main: ncc.ls.Main
author: Dale Martin Watson
website: http://nextcraft.ru
version: 0.0.1
description: Send link to client.
commands:
    lsend:
        description: Send link to all.
        usage: "/lsend http://nextcraft.ru"
permissions:
    ls.*:
        description: Allows you to use all the functions.
        default: op
    ls.send:
        description: Allows you to send link to all users.
        default: op
    ls.create:
        description: Allows you to create sign.
        default: op

Далее, как я указал во второй строке plugin.yml, создаю класс Main в пакете ncc.ls.

Он наследуется от JavaPlugin и содержит две стандартные функции onEnable и onDisable, а также две функции для отправки сообщений в консоль и чат от имени плагина, и одна для отправки ссылки.

package ncc.ls;

import java.util.logging.Logger;
import ncc.ls.commands.LSendListener;
import ncc.ls.player.InteractListener;
import ncc.ls.player.PlaceListener;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;

public class Main extends JavaPlugin
{
    public static Main statthis;
    public final static Logger log = Logger.getLogger("Minecraft");

    @Override
    public void onEnable()
    {
        this.getCommand("lsend").setExecutor(new LSendListener(this));
        PluginManager pm = getServer().getPluginManager();
        pm.registerEvents(new InteractListener(), this);
        pm.registerEvents(new PlaceListener(), this);

        toLog("Plugin enabled.");
    } 

    @Override
    public void onDisable()
    {
        toLog("Plugin disabled.");
    }   

    public static void toLog(String msg)
    {
        log.info("[LinkSender] " + msg);
    }

    public static void toChat(CommandSender cs, String m)
    {
        cs.sendMessage(ChatColor.GOLD + "[LinkSender] " + m);
    }

    public static void sendLink(CommandSender cs, String link)
    {
        cs.sendMessage("LINK: " + link);
    }

}

В onEnable регистрируется три обработчика для трех событий:

Отправка в чат администратором команды /lsend example.com;
Установка таблички;
Правый клик по табличке;
Для проверки введенных URL'ов также нужна функция. ncc.ls.Validator:

package ncc.ls;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Validator
{
   public static boolean validURL(String URL)
   {
       Pattern pattern = Pattern.compile("(https?:\/\/([a-zA-Z0-9]+\.)+[a-zA-Z0-9]{2,}((\/[a-zA-Z0-9%-]*)*|\?(([a-zA-Z0-9%-]* = [a-zA-Z0-9%-]&?)*)*))");
       Matcher matcher = pattern.matcher(URL);

       return matcher.matches();
   }
}

LSendListener.java:

package ncc.ls.commands;

import ncc.ls.Main;
import ncc.ls.Validator;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;

public class LSendListener implements CommandExecutor
{

    public Main plugin;

    public LSendListener(Main plugin)
    {
        this.plugin = plugin;
    }

    @Override
    public boolean onCommand(CommandSender cs, Command cmd, String label, String[] args)
    {
        if (!cs.hasPermission("ls.send"))
        {
             Main.toChat(cs, "You have no permissions to send link.");
             return false;
        }
        else
        {
            if(!Validator.validURL(args[0]))
            {
                Main.toChat(cs, "Not valid URL.");
                return false;
            }
            else
            {
                sendLinkGlobal(args[0]);
                Main.toChat(cs, "Link sended.");
                return true;
            }

        }
    }

    private void sendLinkGlobal(String URL)
    {
        this.plugin.getServer().broadcastMessage(URL);
    }

}

InteractListener.java

package ncc.ls.player;

import ncc.ls.Main;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.Sign;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;

public class InteractListener implements Listener
{
   private final Main plugin;

   public InteractListener(Main plugin)
   {
	this.plugin = plugin;
   }

   @EventHandler(priority = EventPriority.MONITOR)
   public void onPlayerInteract(PlayerInteractEvent event)
   {
       Block block = event.getClickedBlock();
       if(block.getType().equals(Material.WALL_SIGN) || block.getType().equals(Material.SIGN_POST))
       {
           if (event.getAction() == Action.RIGHT_CLICK_BLOCK)
           {
               Sign s = (Sign) block.getState();
               if (s.getLine(0).equalsIgnoreCase("[Link]"))
               {
                   Main.sendLink(event.getPlayer(), s.getLine(1)+s.getLine(2)+s.getLine(3));
               }
           }
       }
   }
}

PlaceListener.java

package ncc.ls.player;

import ncc.ls.Main;
import ncc.ls.Validator;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.SignChangeEvent;
import org.bukkit.inventory.ItemStack;

public class PlaceListener implements Listener
{

    private Player player;
    private Block block;
	  @EventHandler(priority = EventPriority.MONITOR)
	  public void onSignChange(SignChangeEvent arg0)
          {
              if (!arg0.getLine(0).equalsIgnoreCase("[Link]")) return;

              block = arg0.getBlock();
	      if (block == null) return;

              player = arg0.getPlayer();

              if (!player.hasPermission("ls.create"))
              {
	          signDrop();
                  Main.toChat(player, "You have no permissions to place link.");
                  return;
              }

              if(!Validator.validURL(arg0.getLine(1) + arg0.getLine(2) + arg0.getLine(3)))
              {
	          signDrop();
                  Main.toChat(player, "Not valid URL.");
                  return;
              } 

              Main.toChat(player, "Link successfully created.");
	  }

          private void signDrop()
          {
	      ItemStack sign = new ItemStack(323, 1);

	      player.getWorld().dropItem(block.getLocation(), sign);
              block.setTypeId(0);
	  }

}

Далее просто ставим табличку, вписываем URL и пользуемся. Естественно, что использовать можно для чего угодно, от ивентов, до голосований в топах. Удачи.

P.S. Статья писалась не для профессиональных java-программистов, так как я сам еще новичок и код может смотреться некрасиво, возможно нерационально, с радостью приму все замечания и пожелания.

Автор: DaleMartinWatson

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


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