Динамический список ссылок в отдельном GUI.

215
1
20
Прошу не кидаться в меня палками. Идея - моя. Возможно, баян или просто мусор. Делюсь с теми, кому понравится и/или пригодится - целиком или кусками.

Описание:
Администратор создает на сайте некий файл-список ссылок и названий к ним. Эти ссылки в виде кнопок доступны для игроков через клиент. Если администратор редактирует список (добавляет/удаляет/временно отключает ссылки), то при очередном входе в гуи список кнопок соответственно изменяется. Как-то так.

Видео с наглядной демонстрацией работы (2 минуты):
[video=youtube]https://www.youtube.com/watch?v=wo-GECFxLKU&feature=youtu.be[/video]

Как это сделано:

В EventsFmlClient я добавил такое:

Код:
 @SubscribeEvent
 public void onGuiOpenEvent(GuiOpenEvent event)
 {
if(event.gui instanceof GuiIngameMenu){event.gui = new GameMenu();}
 }
при этом стандартная кнопка "Открыть для сети" уменьшена вдвое, чтоб было куда засунуть мою кнопку. Строка стала выглядеть так:

Код:
       this.buttonList.add(guibutton = new GuiButton(7, this.width / 2 - 100, this.height / 4 + 72 + b0, 98, 20, I18n.format("menu.shareToLan", new Object[0])));


Далее идем в GameMenu - этот класс является клоном [font=Monaco, Consolas, Courier, monospace]GuiIngameMenu[/font], но с моими изменениями, то есть:

1) В InitGui() добавлена кнопка:
Код:
       this.buttonList.add(new GuiButton(2004, width / 2 + 2, this.height / 4 + 72 + b0, 98, 20, StatCollector.translateToLocal("menu.rhlinks")));

В том же классе, в ActionPerformed(GuiButton btn) добавлен переход в моё гуи:

Код:
           case 2004:
             this.mc.displayGuiScreen(new RHDLinks(this));
             break;

Далее привожу весь класс гуи с подробными комментариями:
Код:
package ru.lao.rha.gui;

import java.awt.Desktop;
import java.util.List;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.util.StatCollector;

import org.apache.commons.io.Charsets;
import org.apache.commons.io.FileUtils;

import scala.Console;

import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;

@SideOnly(Side.CLIENT)
public class RHDLinks extends GuiScreen {
    private static final String __OBFID = "CL_00000700";

 private String mainSiteDomain = "russianhostility.mooo.com"; // сайт
    private String folderOnServer = "extra"; // папка на сайте (без крайних '/'), можно так: extra/files; нельзя так: /extra/files или так extra/files/
    private String listFileOnServer = "links.lst"; // без '/', только название файла
    private String folderInClient = "extra"; // папка в клиенте (без крайних '/'), можно так: extra/files; нельзя так: /extra/files или так extra/files/
    private String listFileInClient = "links.lst"; // без '/', только название файла
    private String divider = " :: "; // разделитель в файле-списке, по которому будет разбиваться строка (пробелы учитываются!!!)
    private final GuiScreen backgui; // гуискрин, откуда пришли и куда вернемся, если нажмем кнопку "Вернуться"
    protected String title = ""; // название гуи
    protected String nolinks = ""; // фраза "список пуст или не найден"
    private Map<String, String> links = new HashMap<String, String>(); // мап с названиями кнопок и ссылками к ним
    private int startid = 2005; // ID первой кнопки, остальные будут назначены подряд, начиная с этого
    private int quitnumber = 5000; // ID кнопки выхода из гуи
    private static boolean debug = true, stacktrace = true; // рапортовать ли о наличии ошибки? выдавать ли содержимое стека ошибки?
    
    public RHDLinks(GuiScreen gui){ backgui = gui; } // вызываем наш гуи извне так: RHDLinks(this);, где this - предыдущий гуи, куда можно будет потом вернуться

    public void initGui(){
     title = StatCollector.translateToLocal("menu.rhlinks.title"); // название берем из нашего lang-файла
     getLinks(); // скачиваем и заполняем лист ссылок
        int btnheight = 20, start = height - 230, row = 0, id = startid, i = 0; // btnheight - высота кнопок, start - Y-координата верхней кнопки, row - ряд кнопок, id - первый ID, i - итератор кнопок 
        if(links.size() == 0){ nolinks = StatCollector.translateToLocal("menu.nolinks"); row += 2;} else {nolinks = "";} // если список пуст, то берем фразу об этом из нашего lang-файла
        buttonList.clear(); // далее расставляем кнопки по экрану в два ряда
        GuiButton btn;
        while(i < links.size()){
        buttonList.add(btn = new GuiButton(id, width / 2 - 100, start + (btnheight + 4) * row, 98, btnheight, getLinkOrButtonName(i, true))); i++; id++;
        if(i < links.size()){
         buttonList.add(new GuiButton(id, width / 2 + 2, start + (btnheight + 4) * row, 98, btnheight, getLinkOrButtonName(i, true))); i++; id++;
        } else { btn.xPosition = width / 2 - 100; btn.width = 200;} // число кнопок нечетное - последнюю кнопку ставим по центру и 
        row++;
        }
        row++;
        buttonList.add(new GuiButton(quitnumber, this.width / 2 - 100, start + (btnheight + 4) * row, StatCollector.translateToLocal("menu.back"))); // название кнопки "Вернуться" берем из нашего lang-файла
    }

    // извлекаем из Map название кнопки (isKey=true) или ссылку(isKey=false)
    private String getLinkOrButtonName(int lineNumber, boolean isKey) {
     int i = 0;
     for (Entry<String, String> linkline : links.entrySet()) {
     if(i != lineNumber){i++; continue;}
     if(isKey){return linkline.getKey();} else {return linkline.getValue();}
     }
 return "";
 }

    // обработка нажатия кнопок
    @Override
 protected void actionPerformed(GuiButton button) {
     int id = button.id;
     if (id == quitnumber) { mc.displayGuiScreen(backgui); }  // кнопка "Вернуться", уходим в предыдущее гуи
     if(links.size() == 0 || id < startid || id > startid + links.size() - 1){return;} // ссылок нет, либо номер кнопки неверный, ничего не делаем
     String link = getLinkOrButtonName(id - startid, false);
     openWebpage(link); // открываем ссылку в браузере игрока по умолчанию
    }
    
    // открытие ссылки
    private void openWebpage(String url){
     try {
     if(!url.startsWith("http://") && !url.startsWith("https://")){url = "http://" + url;}
 Desktop.getDesktop().browse(new URL(url).toURI());
 } catch (MalformedURLException e) {reportError(1, e);} catch (IOException e) {reportError(2, e);} catch (URISyntaxException e) {reportError(3, e);}
    }

    // скачивание и заполнение листа ссылок (Map <название_кнопки, адрес_в_интернете>)
    private boolean getLinks(){
 if(!loadList(folderOnServer + "/" + listFileOnServer))return false; // если не удалось загрузить файл-список в клиент, то уходим
    BufferedReader bufferedreader = null;
    try {// считываем построчно файл-список
        bufferedreader = new BufferedReader(new InputStreamReader(new FileInputStream(folderInClient + "/" + listFileInClient), Charsets.UTF_8));
        String s;
        links.clear(); // очищаем старый лист
        while ((s = bufferedreader.readLine()) != null) {
            s = s.trim(); // убираем случайные паразитные пробелы по краям строки
            if (!s.isEmpty() && !s.startsWith("//")){ // если строка пустая или начинается с '//' (то есть, временно закомментирована администрацией сервера), то пропускаем её
             String [] strings = s.split(divider); // разбиваем строку
             if(strings.length == 2){links.put(strings[0].trim(), strings[1].trim());} // если она разбилась пополам, то считаем ее валидной и забиваем в Map,
             //  попутно убирая случайные паразитные пробелы по краям значений
            }
        }
    } catch (IOException e){reportError(4, e);}
    finally
    { if (bufferedreader != null) { try { bufferedreader.close(); return false;} catch (IOException e){ reportError(5, e); return false; }}}
    return true;
    }
    
    // скачиваем список
    private boolean loadList(String name){
     if(!IsAvailableDomain(mainSiteDomain)){return false;} // сайт не пингуется - не выдаем кнопки со ссылками
 URL url = null;
 File target = new File(folderInClient + "/" + listFileInClient); // путь к файлу-списку в клиенте
 try {
 url = new URL("http://" + mainSiteDomain + "/" +  name); // пытаемся получить доступ по ссылке к файлу
 } catch (MalformedURLException e) { reportError(6, e); return false;} catch (Exception e){ reportError(7, e); return false;}
 if(url != null){
 if(target.exists()){target.delete();} // если файл-лист в клиенте уже есть, удаляем его, чтобы заменить старый список актуальным
 try {
 FileUtils.copyURLToFile(url, target); // пытаемся скопировать файл-список
 } catch (IOException e) { reportError(8, e); return false;}
 }
 return true;
 }

    // пингуем сервер, проверяя его доступность
    public static boolean IsAvailableDomain(String domain) {
        try {
            Process proc = Runtime.getRuntime().exec("ping " + domain); // стандартный пинг через cmd
            StreamGobbler outputGobbler = new StreamGobbler(proc.getInputStream()/*, "OUTPUT"*/);
            outputGobbler.start();
            proc.waitFor();
            return checkAvailability(outputGobbler.getOutputLines()); // возвращаем результат в зависимости от результатов пингования
        } catch(Exception e) {reportError(9, e);}
        return false;
    }
    
    // проверяем полученный от команды ping результат и возвращаем, доступен ли сайт
    private static boolean checkAvailability(List<String> outputLines) {
        for(String line : outputLines) {
            if(line.contains("unreachable")) { return false; }
            if(line.contains("TTL=")) { return true;  }
        }
        return false;
    }

    // отрисовываем: бекграунд, надпись вверху с названием окна, кнопки со ссылками, если они есть, либо надпись, что их нет и только одну кнопку ("Вернуться")
 @Override
    public void drawScreen(int a, int b, float c)
    {
        drawDefaultBackground();
        drawCenteredString(fontRendererObj, title, width / 2, 15, 16777215);
        if(nolinks.length() > 0)drawCenteredString(fontRendererObj, nolinks, width / 2, 50, 16777215);
        super.drawScreen(a, b, c);
    }
 
 // дебаггер с нумерацией ошибок
 private static void reportError(int number, Throwable error){
 if(debug){
 String errorline = "Links error # " + number;
 if(stacktrace)errorline += ", check your code. See stacktrace below to get info:";
 Console.println(errorline);
 }
 if(stacktrace){error.printStackTrace();}
 }
}

И напоследок класс StreamGobbler, который я нагуглил и использовал в реализации этой задумки:
Код:
package ru.lao.rha.gui;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
 
 
public class StreamGobbler extends Thread
{
protected InputStream is;

protected List<String> outputLines;
 
public StreamGobbler(InputStream inputstream) {
    is = inputstream;
    outputLines = new ArrayList<String>();
}
 
public List<String> getOutputLines() {
    return outputLines;
}
 
public void run() 
{
    try {
        InputStreamReader isr = new InputStreamReader(is);
        BufferedReader br = new BufferedReader(isr);
        String line;
        while((line = br.readLine()) != null) {
            outputLines.add(line);
        }
    } catch(IOException ex) {
        Logger.getLogger(StreamGobbler.class.getName()).log(Level.SEVERE, null, ex);
    }
 
}
}
Последний нужен для того, чтобы определить, доступен ли сервер, иначе клиент игры вешается.  :blush:

Если вы посмотрели видео, то заметили, что клиент зависает при нажатии на кнопку вызова гуи на время, которое тратится, чтобы пропинговать сервер с файлом-списком - секунда-две. Не знаю, как это исправить, просто извещаю вас о том, что такой баг существует, и я о нём знаю.

Спасибо за внимание.

Update:
Код плагина, который даст возможность редактировать список через команды в игре. Комментариев меньше и я написал их по-английски машинально... ну там редактировать особо нечего, заменить, если нужно,  divider и listPath и скомпилировать.
Код:
package ru.lao;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;

public class linkseditor extends JavaPlugin {
 private String cap = "[LinksEditor] ", divider = " :: ";
 private String listPath = "X:/Apache2/localhost/www/extra/links.lst";
 private File listFile = new File(listPath);
 
 public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String args[]) {
 if(sender.isOp() || sender.hasPermission("links.edit") || !(sender instanceof Player)){
 if(args.length == 0){ showHelp(sender); return true; }
 if(commandLabel.equalsIgnoreCase("links")){
 if(args.length > 0 && args[0].equalsIgnoreCase("list")){ showList(sender); return true; }
 if(args.length > 2 && args[0].equalsIgnoreCase("add")){ addLine(sender, args); return true; }
 if(args.length == 2 && args[0].equalsIgnoreCase("del")){ removeLine(sender, args[1]); return true; }
 showHelp(sender);
 }
 } else {
 sender.sendMessage(cap + "Access denied.");
 }
 return true;
 }

 // removes line from the file
 private void removeLine(CommandSender sender, String string) {
 if(!listFile.exists() && !createlistFile()){sender.sendMessage(cap + "Can't create list file."); return;}
 int number = 0;
 try {if((number = Integer.parseInt(string)) <= 0){sender.sendMessage(cap + "Number must be > 0."); return;}} catch (Exception e){sender.sendMessage(cap + "Number is incorrect."); return;}
 List<String> contents = readList();
 if(contents.size() < number){sender.sendMessage(cap + "No such line in this file."); return;}
 contents = readListExemptLine(number);
 if(writeList(contents)){
 sender.sendMessage(cap + "Line deleted successfully.");
 } else {
 sender.sendMessage(cap + "Can't write list file.");
 }
 }

 // adds new line to list file
 private void addLine(CommandSender sender, String[] args) {
 if(!listFile.exists() && !createlistFile()){sender.sendMessage(cap + "Can't create list file."); return;}
 List<String> contents = readList();
 contents.add(getButtonName(args) + divider + args[args.length - 1]);
 if(writeList(contents)){
 sender.sendMessage(cap + "New line added successfully.");
 } else {
 sender.sendMessage(cap + "Can't write list file.");
 }
 }

 // shows list of links
 private void showList(CommandSender sender) {
 if(!listFile.exists() && !createlistFile()){sender.sendMessage(cap + "Can't create list file."); return;}
 List<String> contents = readList();
 if(contents.size() > 0){
 sender.sendMessage("Links list content:");
 for(int i = 0; i < contents.size(); i++){sender.sendMessage((i + 1) + " " + contents.get(i));}
 } else {sender.sendMessage(cap + "Links list is empty.");}
 }

 // shows help when you wrong with command
 private void showHelp(CommandSender sender) {
 sender.sendMessage("------------ LINKS EDITOR HELP ------------");
 sender.sendMessage("/links list - shows list of links");
 sender.sendMessage("/links add <buttonname> <url> - adds new line to list");
 sender.sendMessage("links del <linenumber> - deletes line with your number");
 }
 
 // creates empty list file READY
 private boolean createlistFile() {
 try {
 listFile.createNewFile();
 } catch (Exception e){return false;}
 return true;
 }

 // reads list file to the list <String>
 private List<String> readList(){
 List<String> contents = new ArrayList<String>();
 BufferedReader listfile = null;
 try {
 listfile = new BufferedReader(new InputStreamReader(new FileInputStream(listPath),"UTF-8"));
 String line;
 while ((line = listfile.readLine()) != null){
 if(line.isEmpty()){line = "";}
 line = line.trim();
 if(line.isEmpty()){line = "";}
 contents.add(line);
 }
 } catch (Exception e){
 } finally {
 if(listfile != null){try{listfile.close();} catch (Exception e){}}
 }
 return contents;
 }

 // reads list without line to delete
 private List<String> readListExemptLine(int deletenumber) {
 List<String> contents = new ArrayList<String>();
 BufferedReader listfile = null;
 try {
 listfile = new BufferedReader(new InputStreamReader(new FileInputStream(listPath),"UTF-8"));
 String line;
 int i = 1;
 while ((line = listfile.readLine()) != null){
 if(line.isEmpty()){line = "";}
 line = line.trim();
 if(line.isEmpty()){line = "";}
 if (deletenumber != i) contents.add(line);
 i++;
 }
 } catch (Exception e){
 } finally {
 if(listfile != null){try{listfile.close();} catch (Exception e){}}
 }
 return contents;
 }
 
 // writes list <String> to list file and deletes old before
 private boolean writeList(List<String> contents) {
 BufferedOutputStream listfile = null;
 try {
 if(listFile.exists())listFile.delete();
 listfile = new BufferedOutputStream(new FileOutputStream(listPath, true));
 for(int i = 0; i < contents.size(); i++){ listfile.write((contents.get(i) + "\n").getBytes("UTF-8")); }
 } catch (Exception e){
 return false;
 } finally {
 if(listfile != null){try{listfile.close();} catch (Exception e){}}
 }
 return true;
 }

 // makes button name from all words in command, except last, that is link to the web
 private String getButtonName(String[] args) {
 String buttonname = "";
 for (int i = 1; i < args.length - 1; i++){ buttonname += ChatColor.translateAlternateColorCodes('&', args[i]); }
 return buttonname.trim();
 }
}
Команды:
/links - помощь
/links list - показать список
/links add <buttonname> <url> - добавить новую строку
/links del <number> - удалить строку под номером

Редактирование разрешено через консоль, опам и тем, у кого есть пермишн links.edit
 
Очень круто, спасибо, пригодится :)
 
По поводу зависания, как вариант, подгружай свой файл во время загрузки и тогда подвисаний не должно быть.
 
WildHeart написал(а):
По поводу зависания, как вариант, подгружай  свой файл во время загрузки и тогда подвисаний не должно быть.

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

Кстати, можно написать мод или плагин на команды, которыми админ сможет редактировать этот список через чат игры.
 
И ради "свежей информации" стоит жертвовать производительность? Не забывай про динозавров, у них от свежей инфы будет клиент закрывать, либо еще что нибудь. По поводу команд на обновление, решишь делать плагин, придется писать мост для мода. А писать мод с обновление кнопки при выполнении команды глупо, просто тогда дополни ядро.
 
WildHeart написал(а):
И ради "свежей информации" стоит жертвовать производительность? Не забывай про динозавров, у них от свежей инфы будет клиент закрывать, либо еще что нибудь. По поводу команд на обновление, решишь делать плагин, придется писать мост для мода. А писать мод с обновление кнопки при выполнении команды глупо, просто тогда дополни ядро.


Ну, не знаю. Почему ты решил, что я жертвую производительностью?

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

Update: Короче, написал скоренько плагин и добавил его код в шапку темы.
 
LaoTheLizard написал(а):
WildHeart написал(а):
И ради "свежей информации" стоит жертвовать производительность? Не забывай про динозавров, у них от свежей инфы будет клиент закрывать, либо еще что нибудь. По поводу команд на обновление, решишь делать плагин, придется писать мост для мода. А писать мод с обновление кнопки при выполнении команды глупо, просто тогда дополни ядро.


Ну, не знаю. Почему ты решил, что я жертвую производительностью?

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

Update: Короче, написал скоренько плагин и добавил его код в шапку темы.
Сорри, не так понял) Тут скорее не производительностью, а динозаврами.
 
Назад
Сверху