diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a6133b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.settings/ +/bin/ +.classpath \ No newline at end of file diff --git a/.project b/.project new file mode 100644 index 0000000..f1d8b60 --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + EaglerMOTD + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/EaglerMOTD.jar b/EaglerMOTD.jar new file mode 100644 index 0000000..6afe606 Binary files /dev/null and b/EaglerMOTD.jar differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e750a01 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# EaglerMOTD + +### This plugin can add animated MOTDs to your Eaglercraft server + +![EaglerMOTD Sample](https://i.gyazo.com/ec23a9c60e9722209246fc2b2acea8e4.gif) + +**It can also add custom "Accept:" query handlers for 3rd party sites to gather more information about your server** + +## How to Install + +**Download [EaglerMOTD.jar](https://raw.githubusercontent.com/LAX1DUDE/eaglercraft-motd/main/EaglerMOTD.jar) and place it in your EaglercraftBungee '/plugins' directory. Then, restart EaglercraftBungee** + +You will find a new 'EaglerMOTD' folder in the plugins folder you put the jar in, once you finish restarting your server. This contains the plugin's configuration files, you can edit any of them and then type `motd-reload` in the EaglercraftBungee console to reload all the variables. + +## Configuration Guide + +Just a minute... + +## Compiling and Contributing + +First, download the latest [EaglercraftBungee jar](https://github.com/LAX1DUDE/eaglercraft/blob/main/stable-download/java/bungee_command/bungee-dist.jar) in stable-download on [LAX1DUDE/eaglercraft](https://github.com/LAX1DUDE/eaglercraft/) + +**Make a new java project in Eclipse/IDEA/etc and add 'src' folder in this repository as the source code folder** + +**Then, add your EaglercraftBungee jar ([bungee-dist.jar](https://github.com/LAX1DUDE/eaglercraft/blob/main/stable-download/java/bungee_command/bungee-dist.jar)) to the java project's Build Path and refresh** + +Export the contents of 'src' folder of the project to a JAR file to compile the plugin + +**For a PR:** Tabs, not spaces, and format the code like the Eclipse auto format tool on factory settings. \ No newline at end of file diff --git a/src/default_frames.json b/src/default_frames.json new file mode 100644 index 0000000..901f731 --- /dev/null +++ b/src/default_frames.json @@ -0,0 +1,42 @@ +{ + "frame1": { + "icon": "server-animation.png", + "icon_spriteX": 0, + "icon_spriteY": 0, + "online": "default", + "max": "default", + "players": "default", + "text0": "&6An Eaglercraft server", + "text1": "&7Running EaglerMOTD plugin" + }, + "frame2": { + "icon": "server-animation.png", + "icon_spriteX": 1, + "icon_spriteY": 0, + "icon_color": [ 1.0, 0.0, 0.0, 0.2 ], + "online": 10, + "players": [ "fake player 1", "fake player 2" ], + "text0": "&bAn &6Eaglercraft server" + }, + "frame3": { + "icon": "server-animation.png", + "icon_spriteX": 2, + "icon_spriteY": 0, + "icon_tint": [ 0.6, 0.6, 1.0, 0.8 ], + "online": 20, + "players": [], + "text0": "&6An &bEaglercraft &6server" + }, + "frame4": { + "icon": "server-animation.png", + "icon_spriteX": 3, + "icon_spriteY": 0, + "icon_color": [ 1.0, 1.0, 0.0, 0.2 ], + "icon_tint": [ 0.5, 0.5, 1.0 ], + "online": 30, + "max": 69, + "players": "default", + "text0": "&6An Eaglercraft &bserver", + "text1": "&7Running &k&oEaglerMOTD&r&7 plugin" + } +} \ No newline at end of file diff --git a/src/default_messages.json b/src/default_messages.json new file mode 100644 index 0000000..59675c6 --- /dev/null +++ b/src/default_messages.json @@ -0,0 +1,25 @@ +{ + "close_socket_after": 1200, + "max_sockets_per_ip": 10, + "max_total_sockets": 256, + "allow_banned_ips": false, + "messages": { + "all": [ + { + "name": "default", + "frames": [ + "frames.frame1", + "frames.frame2", + "frames.frame3", + "frames.frame4" + ], + "interval": 8, + "random": false, + "shuffle": false, + "timeout": 500, + "weight": 1.0, + "next": "any" + } + ] + } +} \ No newline at end of file diff --git a/src/default_queries.json b/src/default_queries.json new file mode 100644 index 0000000..c522042 --- /dev/null +++ b/src/default_queries.json @@ -0,0 +1,37 @@ +{ + "queries": { + "ExampleQuery1": { + "type": "ExampleQuery1_result", + "string": "This is a string" + }, + "ExampleQuery2": { + "type": "ExampleQuery2_result", + "txt": "query2.txt" + }, + "ExampleQuery3": { + "type": "ExampleQuery3_result", + "string": "This query returns binary", + "file": "binary.dat" + }, + "ExampleQuery4": { + "type": "ExampleQuery4_result", + "json": "query4.json" + }, + "ExampleQuery5": { + "type": "ExampleQuery5_result", + "json": { + "key1": "value1", + "key2": "value2" + } + }, + "ExampleQuery6": { + "type": "ExampleQuery6_result", + "json": { + "desc": "This query returns JSON and a file", + "filename": "test_file.dat", + "size": 69 + }, + "file": "test_file.dat" + } + } +} \ No newline at end of file diff --git a/src/net/lax1dude/eaglercraft/eaglermotd/BitmapFile.java b/src/net/lax1dude/eaglercraft/eaglermotd/BitmapFile.java new file mode 100644 index 0000000..24bc6f8 --- /dev/null +++ b/src/net/lax1dude/eaglercraft/eaglermotd/BitmapFile.java @@ -0,0 +1,109 @@ +package net.lax1dude.eaglercraft.eaglermotd; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.imageio.ImageIO; + +public class BitmapFile { + + public final String name; + public final int[][] frame; + public final int w, h; + + public BitmapFile(String name, int[][] frame, int w, int h) { + this.name = name; + this.frame = frame; + this.w = w; + this.h = h; + } + + public static final Map bitmapCache = new HashMap(); + + public static BitmapFile getCachedIcon(String name) { + BitmapFile ret = bitmapCache.get(name); + if(ret == null) { + File f = new File(name); + if(f.exists()) { + try { + BufferedImage img = ImageIO.read(f); + int w = img.getWidth(); + int h = img.getHeight(); + if(w < 64 || h < 64) { + System.err.println("[EaglerMOTD] Icon '" + name + "' must be at least be 64x64 pixels large (it is " + w + "x" + h + ")"); + }else { + int[][] load = new int[w][h]; + for(int y = 0; y < h; ++y) { + for(int x = 0; x < w; ++x) { + load[x][y] = img.getRGB(x, y); + } + } + ret = new BitmapFile(name, load, w, h); + bitmapCache.put(name, ret); + } + } catch (IOException e) { + System.err.println("[EaglerMOTD] Could not load icon file: '" + name + "'"); + System.err.println("[EaglerMOTD] Place the file in the same directory as 'messages.json'"); + e.printStackTrace(); + } + } + } + return ret; + } + + public int[] getSprite(int x, int y) { + if(x < 0 || y < 0) { + return null; + } + int offsetX = x * 64; + int offsetY = y * 64; + if(offsetX + 64 > w || offsetY + 64 > h) { + return null; + } + int[] ret = new int[64 * 64]; + for(int i = 0; i < ret.length; ++i) { + int xx = i % 64; + int yy = i / 64; + ret[i] = frame[offsetX + xx][offsetY + yy]; + } + return ret; + } + + public static int[] makeColor(int[] in, float r, float g, float b, float a) { + int c = ((int)(a*255.0f) << 24) | ((int)(r*255.0f) << 16) | ((int)(g*255.0f) << 8) | (int)(b*255.0f); + for(int i = 0; i < in.length; ++i) { + in[i] = c; + } + return in; + } + + public static int[] applyColor(int[] in, float r, float g, float b, float a) { + for(int i = 0; i < in.length; ++i) { + float rr = ((in[i] >> 16) & 0xFF) / 255.0f; + float gg = ((in[i] >> 8) & 0xFF) / 255.0f; + float bb = (in[i] & 0xFF) / 255.0f; + float aa = ((in[i] >> 24) & 0xFF) / 255.0f; + rr = r * a + rr * (1.0f - a); + gg = g * a + gg * (1.0f - a); + bb = b * a + bb * (1.0f - a); + aa = a + aa * (1.0f - a); + in[i] = ((int)(aa*255.0f) << 24) | ((int)(rr*255.0f) << 16) | ((int)(gg*255.0f) << 8) | (int)(bb*255.0f); + } + return in; + } + + public static int[] applyTint(int[] in, float r, float g, float b, float a) { + for(int i = 0; i < in.length; ++i) { + float rr = ((in[i] >> 16) & 0xFF) / 255.0f * r; + float gg = ((in[i] >> 8) & 0xFF) / 255.0f * g; + float bb = (in[i] & 0xFF) / 255.0f * b; + float aa = ((in[i] >> 24) & 0xFF) / 255.0f * a; + in[i] = ((int)(aa*255.0f) << 24) | ((int)(rr*255.0f) << 16) | ((int)(gg*255.0f) << 8) | (int)(bb*255.0f); + } + return in; + } + +} diff --git a/src/net/lax1dude/eaglercraft/eaglermotd/CommandMOTDReload.java b/src/net/lax1dude/eaglercraft/eaglermotd/CommandMOTDReload.java new file mode 100644 index 0000000..e32be68 --- /dev/null +++ b/src/net/lax1dude/eaglercraft/eaglermotd/CommandMOTDReload.java @@ -0,0 +1,26 @@ +package net.lax1dude.eaglercraft.eaglermotd; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.plugin.Command; + +public class CommandMOTDReload extends Command { + + public final EaglerMOTD plugin; + + public CommandMOTDReload(EaglerMOTD plugin) { + super("motd-reload", "eaglermotd.command.reload"); + this.plugin = plugin; + } + + @Override + public void execute(CommandSender paramCommandSender, String[] paramArrayOfString) { + try { + plugin.loadConfiguration(paramCommandSender); + } catch (Exception e) { + paramCommandSender.sendMessage(ChatColor.RED + "[EaglerMOTD] Failed to reload! " + e.toString()); + e.printStackTrace(); + } + } + +} diff --git a/src/net/lax1dude/eaglercraft/eaglermotd/EaglerMOTD.java b/src/net/lax1dude/eaglercraft/eaglermotd/EaglerMOTD.java new file mode 100644 index 0000000..5284cd9 --- /dev/null +++ b/src/net/lax1dude/eaglercraft/eaglermotd/EaglerMOTD.java @@ -0,0 +1,396 @@ +package net.lax1dude.eaglercraft.eaglermotd; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; + +import org.json.JSONArray; +import org.json.JSONObject; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.config.ListenerInfo; +import net.md_5.bungee.api.event.WebsocketMOTDEvent; +import net.md_5.bungee.api.event.WebsocketQueryEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.command.ConsoleCommandSender; +import net.md_5.bungee.eaglercraft.BanList; +import net.md_5.bungee.event.EventHandler; + +public class EaglerMOTD extends Plugin implements Listener { + + public static final Map> messages = new HashMap(); + public static final Map messagePools = new HashMap(); + public static final Map framesCache = new HashMap(); + public static final Map queryTypes = new HashMap(); + public static EaglerMOTD instance = null; + public static int close_socket_after = 1200; + public static int max_sockets_per_ip = 10; + public static int max_total_sockets = 256; + public static boolean allow_banned_ips = false; + + public final Timer tickTimer = new Timer("MOTD Tick Timer"); + public final List motdConnections = new LinkedList(); + + public EaglerMOTD() { + instance = this; + } + + public void loadConfiguration(CommandSender cs) throws Exception { + messages.clear(); + messagePools.clear(); + framesCache.clear(); + queryTypes.clear(); + + BitmapFile.bitmapCache.clear(); + QueryCache.flush(); + + synchronized(motdConnections) { + if(motdConnections.size() > 0) { + for(MOTDConnection con : motdConnections) { + con.close(); + } + motdConnections.clear(); + } + } + + getDataFolder().mkdirs(); + + byte[] damn = new byte[4096]; + int i; + + File msgs = new File(getDataFolder(), "messages.json"); + + if(!msgs.exists()) { + OutputStream msgsNew = new FileOutputStream(msgs); + InputStream msgsDefault = EaglerMOTD.class.getResourceAsStream("/default_messages.json"); + while((i = msgsDefault.read(damn)) != -1) { + msgsNew.write(damn, 0, i); + } + msgsNew.close(); + msgsDefault.close(); + File f2 = new File(getDataFolder(), "frames.json"); + if(!f2.exists()) { + msgsNew = new FileOutputStream(f2); + msgsDefault = EaglerMOTD.class.getResourceAsStream("/default_frames.json"); + while((i = msgsDefault.read(damn)) != -1) { + msgsNew.write(damn, 0, i); + } + msgsNew.close(); + msgsDefault.close(); + } + f2 = new File(getDataFolder(), "queries.json"); + if(!f2.exists()) { + msgsNew = new FileOutputStream(f2); + msgsDefault = EaglerMOTD.class.getResourceAsStream("/default_queries.json"); + while((i = msgsDefault.read(damn)) != -1) { + msgsNew.write(damn, 0, i); + } + msgsNew.close(); + msgsDefault.close(); + } + f2 = new File("server-animation.png"); + if(!f2.exists()) { + msgsNew = new FileOutputStream(f2); + msgsDefault = EaglerMOTD.class.getResourceAsStream("/server-icons-test.png"); + while((i = msgsDefault.read(damn)) != -1) { + msgsNew.write(damn, 0, i); + } + msgsNew.close(); + msgsDefault.close(); + } + } + if(!msgs.exists()) { + throw new NullPointerException("messages.json is missing and could not be created"); + } + + InputStream is = new FileInputStream(msgs); + ByteArrayOutputStream bao = new ByteArrayOutputStream(is.available()); + while((i = is.read(damn)) != -1) { + bao.write(damn, 0, i); + } + is.close(); + + JSONObject msgsObj = new JSONObject(new String(bao.toByteArray(), StandardCharsets.UTF_8)); + framesCache.put("messages", msgsObj); + close_socket_after = msgsObj.optInt("close_socket_after", 1200); + max_sockets_per_ip = msgsObj.optInt("max_sockets_per_ip", 10); + max_total_sockets = msgsObj.optInt("max_total_sockets", 256); + allow_banned_ips = msgsObj.optBoolean("allow_banned_ips", false); + msgsObj = msgsObj.getJSONObject("messages"); + + for(String ss : msgsObj.keySet()) { + try { + List poolEntries = new LinkedList(); + JSONArray arr = msgsObj.getJSONArray(ss); + for(int j = 0, l = arr.length(); j < l; ++j) { + JSONObject entry = arr.getJSONObject(j); + List frames = new LinkedList(); + JSONArray framesJSON = entry.getJSONArray("frames"); + for(int k = 0, l2 = framesJSON.length(); k < l2; ++k) { + JSONObject frame = resolveFrame(framesJSON.getString(k), cs); + if(frame != null) { + frames.add(frame); + } + } + if(frames.size() > 0) { + poolEntries.add(new MessagePoolEntry(entry.optInt("interval", 0), entry.optInt("timeout", 500), + entry.optBoolean("random", false), entry.optBoolean("shuffle", false), entry.optFloat("weight", 1.0f), + entry.optString("next", null), frames, entry.optString("name", null))); + }else { + cs.sendMessage(ChatColor.RED + "[EaglerMOTD] Message '" + ss + "' has no frames!"); + } + } + if(poolEntries.size() > 0) { + List existingList = messages.get(ss); + if(existingList == null) { + existingList = poolEntries; + messages.put(ss, existingList); + }else { + existingList.addAll(poolEntries); + } + } + }catch(Throwable t) { + cs.sendMessage(ChatColor.RED + "[EaglerMOTD] Could not parse messages for '" + ss + "' " + t.toString()); + } + } + + Collection listeners = getProxy().getConfigurationAdapter().getListeners(); + + String flag = null; + for(String s : messages.keySet()) { + if(!s.equals("all")) { + boolean flag2 = false; + for(ListenerInfo l : listeners) { + if(s.equals(makeListenerString(l.getHost()))) { + flag2 = true; + } + } + if(!flag2) { + flag = s; + break; + } + } + } + + if(flag != null) { + cs.sendMessage(ChatColor.RED + "[EaglerMOTD] Listener '" + flag + "' does not exist!"); + String hostsString = ""; + for(ListenerInfo l : listeners) { + if(hostsString.length() > 0) { + hostsString += " "; + } + hostsString += makeListenerString(l.getHost()); + } + cs.sendMessage(ChatColor.RED + "[EaglerMOTD] Listeners configured: " + ChatColor.YELLOW + hostsString); + } + + for(ListenerInfo l : listeners) { + String name = makeListenerString(l.getHost()); + MessagePool m = new MessagePool(name); + List e = messages.get("all"); + if(e != null) { + m.messagePool.addAll(e); + } + e = messages.get(name); + if(e != null) { + m.messagePool.addAll(e); + } + if(m.messagePool.size() > 0) { + cs.sendMessage(ChatColor.GREEN + "[EaglerMOTD] Loaded " + m.messagePool.size() + " messages for " + name); + messagePools.put(name, m); + } + } + + msgs = new File(getDataFolder(), "queries.json"); + if(msgs.exists()) { + try { + is = new FileInputStream(msgs); + bao = new ByteArrayOutputStream(is.available()); + while((i = is.read(damn)) != -1) { + bao.write(damn, 0, i); + } + is.close(); + JSONObject queriesObject = new JSONObject(new String(bao.toByteArray(), StandardCharsets.UTF_8)); + JSONObject queriesQueriesObject = queriesObject.getJSONObject("queries"); + for(String s : queriesQueriesObject.keySet()) { + queryTypes.put(s.toLowerCase(), new QueryType(s, queriesQueriesObject.getJSONObject(s))); + } + if(queryTypes.size() > 0) { + cs.sendMessage(ChatColor.GREEN + "[EaglerMOTD] Loaded " + queryTypes.size() + " query types"); + } + }catch(Throwable t) { + cs.sendMessage(ChatColor.RED + "[EaglerMOTD] Queries were not loaded: " + t.toString()); + } + } + + } + + public static String makeListenerString(InetSocketAddress addr) { + InetAddress addrHost = addr.getAddress(); + if(addrHost instanceof Inet6Address) { + return "[" + addrHost.getHostAddress() + "]:" + addr.getPort(); + }else { + return addrHost.getHostAddress() + ":" + addr.getPort(); + } + } + + public JSONObject resolveFrame(String s, CommandSender cs) { + int i = s.indexOf('.'); + if(i == -1) { + cs.sendMessage(ChatColor.RED + "[EaglerMOTD] Frame '" + s + "' cannot be found! (it does not specify a filename)"); + return null; + } + String f = s.substring(0, i); + JSONObject fc = framesCache.get(f); + if(fc == null) { + File ff = new File(getDataFolder(), f + ".json"); + if(!ff.exists()) { + cs.sendMessage(ChatColor.RED + "[EaglerMOTD] File '" + f + ".json' cannot be found!"); + return null; + } + try { + byte[] damn = new byte[4096]; + InputStream is = new FileInputStream(ff); + ByteArrayOutputStream bao = new ByteArrayOutputStream(is.available()); + int j; + while((j = is.read(damn)) != -1) { + bao.write(damn, 0, j); + } + is.close(); + fc = new JSONObject(new String(bao.toByteArray(), StandardCharsets.UTF_8)); + framesCache.put(f, fc); + }catch(Throwable t) { + cs.sendMessage(ChatColor.RED + "[EaglerMOTD] File '" + f + ".json' could not be loaded: " + t.toString()); + return null; + } + } + f = s.substring(i + 1).trim(); + if(fc.has(f)) { + return fc.getJSONObject(f); + }else { + cs.sendMessage(ChatColor.RED + "[EaglerMOTD] Frame '" + s + "' cannot be found!"); + return null; + } + } + + public void showRunCmd(CommandSender cs) { + cs.sendMessage(ChatColor.YELLOW + "[EaglerMOTD] Use /motd-reload to reload your MOTD config files"); + } + + public void onLoad() { + try { + getProxy().getPluginManager().registerCommand(this, new CommandMOTDReload(this)); + loadConfiguration(ConsoleCommandSender.getInstance()); + } catch (Exception e) { + System.err.println("[EaglerMOTD] Could not load!"); + e.printStackTrace(); + } + } + + public void onEnable() { + getProxy().getPluginManager().registerListener(this, this); + tickTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + synchronized(motdConnections) { + Iterator itr = motdConnections.iterator(); + while(itr.hasNext()) { + MOTDConnection c = itr.next(); + try { + if(!c.tick()) { + itr.remove(); + } + }catch(Throwable t) { + System.err.println("Error ticking MOTD '" + (c.currentMessage == null ? "null" : c.currentMessage.name) + "' on listener " + c.listenerName); + t.printStackTrace(); + c.close(); + itr.remove(); + } + } + } + } + + }, 0, 50l); + } + + public void onDisable() { + tickTimer.cancel(); + } + + @EventHandler + public void onMOTD(WebsocketMOTDEvent evt) { + MOTDConnection con = new MOTDConnection(evt.getListener(), evt.getMOTD()); + if(con.execute()) { + synchronized(motdConnections) { + while(motdConnections.size() >= max_total_sockets) { + MOTDConnection c = motdConnections.remove(0); + c.close(); + } + } + InetAddress addr = con.motd.getRemoteAddress(); + boolean flag = false; + for(BanList.IPBan b : BanList.blockedBans) { + if(b.checkBan(addr)) { + flag = true; + break; + } + } + if(flag) { + synchronized(motdConnections) { + motdConnections.add(con); + } + }else { + if(allow_banned_ips || !BanList.checkIpBanned(addr).isBanned()) { + synchronized(motdConnections) { + int i = 0; + int c = 0; + while(i < motdConnections.size()) { + if(motdConnections.get(i).motd.getRemoteAddress().equals(addr)) { + ++c; + if(c >= max_sockets_per_ip) { + motdConnections.remove(i).close(); + --i; + } + } + ++i; + } + motdConnections.add(0, con); + } + }else { + con.motd.keepAlive(false); + } + } + } + } + + @EventHandler + public void onQuery(WebsocketQueryEvent evt) { + if(evt.getQuery().isClosed() || evt.getAccept().equalsIgnoreCase("MOTD")) { + return; + } + QueryType t = queryTypes.get(evt.getAccept().toLowerCase()); + if(t != null) { + t.doQuery(evt.getQuery()); + } + } + +} diff --git a/src/net/lax1dude/eaglercraft/eaglermotd/MOTDConnection.java b/src/net/lax1dude/eaglercraft/eaglermotd/MOTDConnection.java new file mode 100644 index 0000000..bdb35ad --- /dev/null +++ b/src/net/lax1dude/eaglercraft/eaglermotd/MOTDConnection.java @@ -0,0 +1,285 @@ +package net.lax1dude.eaglercraft.eaglermotd; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Random; + +import org.json.JSONArray; +import org.json.JSONObject; + +import net.md_5.bungee.BungeeCord; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.MOTD; +import net.md_5.bungee.api.config.ListenerInfo; +import net.md_5.bungee.api.connection.ProxiedPlayer; + +public class MOTDConnection { + + public final ListenerInfo listener; + public final String listenerName; + public final MOTD motd; + + public MessagePoolEntry currentMessage = null; + public int messageTimeTimer = 0; + public int messageIntervalTimer = 0; + public int currentFrame = 0; + public int ageTimer = 0; + + public BitmapFile bitmap = null; + public int spriteX = 0; + public int spriteY = 0; + public float[] color = new float[] { 0.0f, 0.0f, 0.0f, 0.0f }; + public float[] tint = new float[] { 0.0f, 0.0f, 0.0f, 0.0f }; + + private Random rand = null; + + public MOTDConnection(ListenerInfo l, MOTD m) { + this.motd = m; + this.listener = l; + this.listenerName = EaglerMOTD.makeListenerString(l.getHost()); + } + + public boolean execute() { + MessagePool p = EaglerMOTD.messagePools.get(listenerName); + if(p == null) { + return false; + } + + messageTimeTimer = 0; + messageIntervalTimer = 0; + currentMessage = p.pickDefault(); + if(currentMessage.random || currentMessage.shuffle) { + rand = new Random(); + } + + currentFrame = currentMessage.random ? rand.nextInt(currentMessage.frames.size()) : 0; + + applyFrame(currentMessage.frames.get(currentFrame)); + if(currentMessage.interval > 0 || currentMessage.next != null) { + this.motd.keepAlive(true); + return true; + }else { + this.motd.keepAlive(false); + return false; + } + } + + public boolean tick() { + ageTimer++; + if(this.motd.isClosed()) { + return false; + } + if(ageTimer > EaglerMOTD.close_socket_after) { + this.motd.close(); + return false; + } + messageTimeTimer++; + if(messageTimeTimer >= currentMessage.timeout) { + if(currentMessage.next != null) { + if(currentMessage.next.equalsIgnoreCase("any") || currentMessage.next.equalsIgnoreCase("random")) { + MessagePool p = EaglerMOTD.messagePools.get(listenerName); + if(p == null) { + this.motd.close(); + return false; + } + if(p.messagePool.size() > 1) { + MessagePoolEntry m; + do { + m = p.pickNew(); + }while(m == currentMessage); + currentMessage = m; + } + }else { + if(!changeMessageTo(listenerName, currentMessage.next)) { + boolean flag = false; + for(String s : EaglerMOTD.messages.keySet()) { + if(!s.equalsIgnoreCase(listenerName) && changeMessageTo(s, currentMessage.next)) { + flag = true; + break; + } + } + if(!flag) { + this.motd.close(); + return false; + } + } + } + if(currentMessage == null) { + this.motd.close(); + return false; + } + messageTimeTimer = 0; + messageIntervalTimer = 0; + if(rand == null && (currentMessage.random || currentMessage.shuffle)) { + rand = new Random(); + } + currentFrame = currentMessage.random ? rand.nextInt(currentMessage.frames.size()) : 0; + applyFrame(currentMessage.frames.get(currentFrame)); + motd.sendToUser(); + return true; + }else { + this.motd.close(); + return false; + } + }else { + messageIntervalTimer++; + if(currentMessage.interval > 0 && messageIntervalTimer >= currentMessage.interval) { + messageIntervalTimer = 0; + if(currentMessage.frames.size() > 1) { + if(currentMessage.shuffle) { + int i; + do { + i = rand.nextInt(currentMessage.frames.size()); + }while(i == currentFrame); + currentFrame = i; + }else { + ++currentFrame; + if(currentFrame >= currentMessage.frames.size()) { + currentFrame = 0; + } + } + applyFrame(currentMessage.frames.get(currentFrame)); + motd.sendToUser(); + } + } + return true; + } + } + + private boolean changeMessageTo(String group, String s) { + List lst = EaglerMOTD.messages.get(group); + if(lst == null) { + return false; + } + for(MessagePoolEntry m : lst) { + if(m.name.equalsIgnoreCase(s)) { + currentMessage = m; + return true; + } + } + return false; + } + + public void applyFrame(JSONObject frame) { + boolean shouldPush = false; + Object v = frame.opt("online"); + if(v != null) { + if(v instanceof Number) { + motd.setOnlinePlayers(((Number)v).intValue()); + }else { + motd.setOnlinePlayers(BungeeCord.getInstance().getPlayers().size()); + } + shouldPush = true; + } + v = frame.opt("max"); + if(v != null) { + if(v instanceof Number) { + motd.setMaxPlayers(((Number)v).intValue()); + }else { + motd.setMaxPlayers(listener.getMaxPlayers()); + } + shouldPush = true; + } + v = frame.opt("players"); + if(v != null) { + if(v instanceof JSONArray) { + List players = new ArrayList(); + JSONArray vv = (JSONArray) v; + for(int i = 0, l = vv.length(); i < l; ++i) { + players.add(ChatColor.translateAlternateColorCodes('&', vv.getString(i))); + } + motd.setPlayerList(players); + }else { + List players = new ArrayList(); + Collection ppl = BungeeCord.getInstance().getPlayers(); + for(ProxiedPlayer pp : ppl) { + players.add(pp.getDisplayName()); + if(players.size() >= 9) { + players.add("" + ChatColor.GRAY + ChatColor.ITALIC + "(" + (ppl.size() - players.size()) + " more)"); + break; + } + } + motd.setPlayerList(players); + } + shouldPush = true; + } + String line = frame.optString("text0", frame.optString("text", null)); + if(line != null) { + int ix = line.indexOf('\n'); + if(ix != -1) { + motd.setLine1(ChatColor.translateAlternateColorCodes('&', line.substring(0, ix))); + motd.setLine2(ChatColor.translateAlternateColorCodes('&', line.substring(ix + 1))); + }else { + motd.setLine1(ChatColor.translateAlternateColorCodes('&', line)); + } + line = frame.optString("text1", null); + if(line != null) { + motd.setLine2(ChatColor.translateAlternateColorCodes('&', line)); + } + shouldPush = true; + } + boolean shouldRenderIcon = false; + String icon = frame.optString("icon", null); + if(icon != null) { + shouldRenderIcon = true; + if(icon.equalsIgnoreCase("none") || icon.equalsIgnoreCase("default") || icon.equalsIgnoreCase("null") || icon.equalsIgnoreCase("color")) { + bitmap = null; + }else { + bitmap = BitmapFile.getCachedIcon(icon); + } + spriteX = spriteY = 0; + color = new float[] { 0.0f, 0.0f, 0.0f, 0.0f }; + tint = new float[] { 1.0f, 1.0f, 1.0f, 1.0f }; + } + int sprtX = frame.optInt("icon_spriteX", -1); + if(sprtX >= 0 && sprtX != spriteX) { + shouldRenderIcon = true; + spriteX = sprtX; + } + int sprtY = frame.optInt("icon_spriteY", -1); + if(sprtY >= 0 && sprtY != spriteY) { + shouldRenderIcon = true; + spriteY = sprtY; + } + JSONArray colorF = frame.optJSONArray("icon_color"); + if(colorF != null && colorF.length() > 0) { + shouldRenderIcon = true; + color[0] = colorF.getFloat(0); + color[1] = colorF.length() > 1 ? colorF.getFloat(1) : color[1]; + color[2] = colorF.length() > 2 ? colorF.getFloat(2) : color[2]; + color[3] = colorF.length() > 3 ? colorF.getFloat(3) : 1.0f; + } + colorF = frame.optJSONArray("icon_tint"); + if(colorF != null && colorF.length() > 0) { + shouldRenderIcon = true; + tint[0] = colorF.getFloat(0); + tint[1] = colorF.length() > 1 ? colorF.getFloat(1) : tint[1]; + tint[2] = colorF.length() > 2 ? colorF.getFloat(2) : tint[2]; + tint[3] = colorF.length() > 3 ? colorF.getFloat(3) : 1.0f; + } + if(shouldRenderIcon) { + int[] newIcon = null; + if(bitmap != null) { + newIcon = bitmap.getSprite(sprtX, sprtY); + } + if(newIcon == null) { + newIcon = new int[64*64]; + } + newIcon = BitmapFile.applyTint(newIcon, tint[0], tint[1], tint[2], tint[3]); + if(color[3] > 0.0f) { + newIcon = BitmapFile.applyColor(newIcon, color[0], color[1], color[2], color[3]); + } + motd.setBitmap(newIcon); + shouldPush = true; + } + if(shouldPush) { + motd.sendToUser(); + } + } + + public void close() { + motd.close(); + } + +} diff --git a/src/net/lax1dude/eaglercraft/eaglermotd/MessagePool.java b/src/net/lax1dude/eaglercraft/eaglermotd/MessagePool.java new file mode 100644 index 0000000..aefecb1 --- /dev/null +++ b/src/net/lax1dude/eaglercraft/eaglermotd/MessagePool.java @@ -0,0 +1,50 @@ +package net.lax1dude.eaglercraft.eaglermotd; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; + +public class MessagePool { + + public final String poolName; + public final List messagePool = new LinkedList(); + public final Random random = new Random(); + + public MessagePool(String s) { + this.poolName = s; + } + + public void sort() { + Collections.sort(messagePool); + } + + public MessagePoolEntry pickNew() { + if(messagePool.size() <= 0) { + return null; + } + float f = 0.0f; + for(MessagePoolEntry m : messagePool) { + f += m.weight; + } + f *= random.nextFloat(); + float f2 = 0.0f; + for(MessagePoolEntry m : messagePool) { + f2 += m.weight; + if(f2 >= f) { + return m; + } + } + return messagePool.get(0); + } + + public MessagePoolEntry pickDefault() { + for(MessagePoolEntry m : messagePool) { + if("default".equalsIgnoreCase(m.name)) { + return m; + } + } + return pickNew(); + } + +} diff --git a/src/net/lax1dude/eaglercraft/eaglermotd/MessagePoolEntry.java b/src/net/lax1dude/eaglercraft/eaglermotd/MessagePoolEntry.java new file mode 100644 index 0000000..ba9b7fc --- /dev/null +++ b/src/net/lax1dude/eaglercraft/eaglermotd/MessagePoolEntry.java @@ -0,0 +1,34 @@ +package net.lax1dude.eaglercraft.eaglermotd; + +import java.util.List; + +import org.json.JSONObject; + +public class MessagePoolEntry implements Comparable { + + public final String name; + public final int interval; + public final int timeout; + public final boolean random; + public final boolean shuffle; + public final float weight; + public final String next; + public final List frames; + + public MessagePoolEntry(int interval, int timeout, boolean random, boolean shuffle, float weight, String next, List frames, String name) { + this.interval = interval; + this.timeout = timeout; + this.random = random; + this.shuffle = shuffle; + this.weight = weight; + this.next = next; + this.frames = frames; + this.name = name; + } + + @Override + public int compareTo(MessagePoolEntry o) { + return Float.compare(weight, o.weight); + } + +} diff --git a/src/net/lax1dude/eaglercraft/eaglermotd/QueryCache.java b/src/net/lax1dude/eaglercraft/eaglermotd/QueryCache.java new file mode 100644 index 0000000..cc9190b --- /dev/null +++ b/src/net/lax1dude/eaglercraft/eaglermotd/QueryCache.java @@ -0,0 +1,157 @@ +package net.lax1dude.eaglercraft.eaglermotd; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; + +import org.json.JSONObject; + +public class QueryCache { + + private static class CachedFile { + protected final String name; + protected final File file; + protected long lastReload; + protected long lastRescan; + protected CachedFile(String name, File file) { + this.name = name; + this.file = file; + this.lastReload = this.lastRescan = 0l; + } + protected boolean needsReload() { + long l = System.currentTimeMillis(); + if(l - lastRescan > 5000l) { + lastRescan = l; + return file.exists() && lastReload < file.lastModified(); + }else { + return false; + } + } + } + + private static class CachedFileBinary extends CachedFile { + protected byte[] bytes = null; + protected CachedFileBinary(String name, File file) { + super(name, file); + } + protected byte[] getOrReload() { + if(needsReload()) { + if(file.exists()) { + try(FileInputStream fis = new FileInputStream(file)) { + ByteArrayOutputStream read = new ByteArrayOutputStream(fis.available()); + byte[] d = new byte[8192]; + int i; + while((i = fis.read(d)) != -1) { + read.write(d, 0, i); + } + lastRescan = lastReload = System.currentTimeMillis(); + bytes = read.toByteArray(); + }catch(Throwable t) { + bytes = null; + System.err.println("[EaglerMOTD] Failed to load binary: " + name); + t.printStackTrace(); + } + } + } + return bytes; + } + } + + private static class CachedFileString extends CachedFile { + protected String chars = null; + protected CachedFileString(String name, File file) { + super(name, file); + + } + protected String getOrReload() { + if(needsReload()) { + if(file.exists()) { + try(FileInputStream fis = new FileInputStream(file)) { + ByteArrayOutputStream read = new ByteArrayOutputStream(fis.available()); + byte[] d = new byte[8192]; + int i; + while((i = fis.read(d)) != -1) { + read.write(d, 0, i); + } + lastRescan = lastReload = System.currentTimeMillis(); + chars = new String(read.toByteArray(), StandardCharsets.UTF_8); + }catch(Throwable t) { + chars = null; + System.err.println("[EaglerMOTD] Failed to load text: " + name); + t.printStackTrace(); + } + } + } + return chars; + } + } + + private static class CachedFileJSON extends CachedFile { + protected JSONObject json = null; + protected CachedFileJSON(String name, File file) { + super(name, file); + + } + protected JSONObject getOrReload() { + if(needsReload()) { + if(file.exists()) { + try(FileInputStream fis = new FileInputStream(file)) { + ByteArrayOutputStream read = new ByteArrayOutputStream(fis.available()); + byte[] d = new byte[8192]; + int i; + while((i = fis.read(d)) != -1) { + read.write(d, 0, i); + } + lastRescan = lastReload = System.currentTimeMillis(); + json = new JSONObject(new String(read.toByteArray(), StandardCharsets.UTF_8)); + }catch(Throwable t) { + json = null; + System.err.println("[EaglerMOTD] Failed to load json: " + name); + t.printStackTrace(); + } + } + } + return json; + } + } + + private static final HashMap cachedBinary = new HashMap(); + private static final HashMap cachedString = new HashMap(); + private static final HashMap cachedJSON = new HashMap(); + + public static void flush() { + cachedBinary.clear(); + cachedString.clear(); + cachedJSON.clear(); + } + + public static byte[] getBinaryFile(String s) { + CachedFileBinary fb = cachedBinary.get(s); + if(fb == null) { + fb = new CachedFileBinary(s, new File(s)); + cachedBinary.put(s, fb); + } + return fb.getOrReload(); + } + + public static String getStringFile(String s) { + CachedFileString fb = cachedString.get(s); + if(fb == null) { + fb = new CachedFileString(s, new File(s)); + cachedString.put(s, fb); + } + return fb.getOrReload(); + } + + public static JSONObject getJSONFile(String s) { + CachedFileJSON fb = cachedJSON.get(s); + if(fb == null) { + fb = new CachedFileJSON(s, new File(s)); + cachedJSON.put(s, fb); + } + return fb.getOrReload(); + } + +} diff --git a/src/net/lax1dude/eaglercraft/eaglermotd/QueryType.java b/src/net/lax1dude/eaglercraft/eaglermotd/QueryType.java new file mode 100644 index 0000000..90a6042 --- /dev/null +++ b/src/net/lax1dude/eaglercraft/eaglermotd/QueryType.java @@ -0,0 +1,109 @@ +package net.lax1dude.eaglercraft.eaglermotd; + +import org.json.JSONObject; + +import net.md_5.bungee.api.QueryConnection; + +public class QueryType { + + public final String name; + public final String type; + + public final String dataString; + public final String dataJSONFile; + public final JSONObject dataJSONObject; + public final String dataTextFile; + public final String dataBinaryFile; + + public QueryType(String name, JSONObject tag) { + this.name = name; + this.dataJSONObject = tag.optJSONObject("json", null); + if(this.dataJSONObject == null) { + this.dataJSONFile = tag.optString("json", null); + if(this.dataJSONFile == null) { + this.dataTextFile = tag.optString("txt", null); + if(this.dataTextFile == null) { + this.dataString = tag.optString("string", null); + }else { + this.dataString = null; + } + }else { + this.dataTextFile = null; + this.dataString = null; + } + }else { + this.dataJSONFile = null; + this.dataTextFile = null; + this.dataString = null; + } + this.dataBinaryFile = tag.optString("file", null); + String t = tag.optString("type", null); + if(t == null) { + if(this.dataJSONObject != null || this.dataJSONFile != null) { + t = "json"; + }else if(this.dataString != null || this.dataTextFile != null) { + t = "text"; + }else { + t = "binary"; + } + } + this.type = t; + } + + public void doQuery(QueryConnection query) { + byte[] bin = null; + if(dataBinaryFile != null) { + bin = QueryCache.getBinaryFile(dataBinaryFile); + if(bin == null) { + query.setReturnType("error"); + query.writeResponse("Error: could not load binary file '" + dataBinaryFile + "' for query '" + type + "'"); + return; + } + } + boolean flag = false; + if(dataJSONObject != null) { + query.setReturnType(type); + query.writeResponse(dataJSONObject); + flag = true; + }else if(dataJSONFile != null) { + JSONObject obj = QueryCache.getJSONFile(dataJSONFile); + if(obj == null) { + query.setReturnType("error"); + query.writeResponse("Error: could not load or parse JSON file '" + dataJSONFile + "' for query '" + type + "'"); + return; + }else { + query.setReturnType(type); + query.writeResponse(obj); + flag = true; + } + }else if(dataTextFile != null) { + String txt = QueryCache.getStringFile(dataTextFile); + if(txt == null) { + query.setReturnType("error"); + query.writeResponse("Error: could not load text file '" + dataJSONFile + "' for query '" + type + "'"); + return; + }else { + query.setReturnType(type); + query.writeResponse(txt); + flag = true; + } + }else if(dataString != null) { + query.setReturnType(type); + query.writeResponse(dataString); + flag = true; + } + if(!flag) { + query.setReturnType(type); + if(bin != null) { + query.writeResponse((new JSONObject()).put("binary", true).put("file", dataBinaryFile).put("size", bin.length)); + }else { + query.writeResponse(""); + } + } + if(bin != null) { + query.writeResponseBinary(bin); + } + query.close(); + } + +} diff --git a/src/plugin.yml b/src/plugin.yml new file mode 100644 index 0000000..efe4338 --- /dev/null +++ b/src/plugin.yml @@ -0,0 +1,4 @@ +name: EaglerMOTD +main: net.lax1dude.eaglercraft.eaglermotd.EaglerMOTD +version: 1.0 +author: LAX1DUDE \ No newline at end of file diff --git a/src/server-icons-test.png b/src/server-icons-test.png new file mode 100644 index 0000000..54412ce Binary files /dev/null and b/src/server-icons-test.png differ