Files
Purpur/patches/server/0086-Make-the-GUI-better.patch
2020-01-31 04:45:28 -06:00

936 lines
33 KiB
Diff

From 1ef1f86e4ffacc1e104cb34f404d9caff58d51cd Mon Sep 17 00:00:00 2001
From: William Blake Galbreath <Blake.Galbreath@GMail.com>
Date: Thu, 16 Jan 2020 14:59:16 -0600
Subject: [PATCH] Make the GUI better
---
.../net/minecraft/server/DedicatedServer.java | 6 +-
.../net/minecraft/server/MinecraftServer.java | 3 +-
.../java/net/pl3x/purpur/gui/ColorPane.java | 80 ++++++++
.../net/pl3x/purpur/gui/ConsolePanel.java | 128 +++++++++++++
.../java/net/pl3x/purpur/gui/GUIColor.java | 58 ++++++
.../pl3x/purpur/gui/PlayerListComponent.java | 27 +++
.../java/net/pl3x/purpur/gui/ServerGUI.java | 121 ++++++++++++
.../net/pl3x/purpur/gui/info/RAMDetails.java | 52 +++++
.../net/pl3x/purpur/gui/info/RAMGraph.java | 179 ++++++++++++++++++
.../pl3x/purpur/gui/info/ServerInfoPanel.java | 42 ++++
.../purpur/util/HighlightErrorConverter.java | 86 +++++++++
src/main/resources/log4j2.xml | 13 +-
12 files changed, 789 insertions(+), 6 deletions(-)
create mode 100644 src/main/java/net/pl3x/purpur/gui/ColorPane.java
create mode 100644 src/main/java/net/pl3x/purpur/gui/ConsolePanel.java
create mode 100644 src/main/java/net/pl3x/purpur/gui/GUIColor.java
create mode 100644 src/main/java/net/pl3x/purpur/gui/PlayerListComponent.java
create mode 100644 src/main/java/net/pl3x/purpur/gui/ServerGUI.java
create mode 100644 src/main/java/net/pl3x/purpur/gui/info/RAMDetails.java
create mode 100644 src/main/java/net/pl3x/purpur/gui/info/RAMGraph.java
create mode 100644 src/main/java/net/pl3x/purpur/gui/info/ServerInfoPanel.java
create mode 100644 src/main/java/net/pl3x/purpur/util/HighlightErrorConverter.java
diff --git a/src/main/java/net/minecraft/server/DedicatedServer.java b/src/main/java/net/minecraft/server/DedicatedServer.java
index d70c8cab2b..61ccb6079e 100644
--- a/src/main/java/net/minecraft/server/DedicatedServer.java
+++ b/src/main/java/net/minecraft/server/DedicatedServer.java
@@ -51,7 +51,7 @@ public class DedicatedServer extends MinecraftServer implements IMinecraftServer
public DedicatedServerSettings propertyManager;
private EnumGamemode o;
@Nullable
- private ServerGUI p;
+ private net.pl3x.purpur.gui.ServerGUI p; // Purpur
// CraftBukkit start - Signature changed
public DedicatedServer(joptsimple.OptionSet options, DedicatedServerSettings dedicatedserversettings, DataFixer datafixer, YggdrasilAuthenticationService yggdrasilauthenticationservice, MinecraftSessionService minecraftsessionservice, GameProfileRepository gameprofilerepository, UserCache usercache, WorldLoadListenerFactory worldloadlistenerfactory, String s) {
@@ -420,7 +420,7 @@ public class DedicatedServer extends MinecraftServer implements IMinecraftServer
@Override
public void exit() {
if (this.p != null) {
- this.p.b();
+ this.p.close(); // Purpur
}
if (this.remoteControlListener != null) {
@@ -519,7 +519,7 @@ public class DedicatedServer extends MinecraftServer implements IMinecraftServer
public void bc() {
if (this.p == null) {
- this.p = ServerGUI.a(this);
+ this.p = net.pl3x.purpur.gui.ServerGUI.create(this); // Purpur
}
}
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index 9d5ef40a03..105ac8a040 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -105,7 +105,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
private String motd;
private int G;
private int H;
- public final long[] f = new long[100];
+ public final long[] f = new long[100]; public long[] getTickTimes() { return f; } // Purpur
@Nullable
private KeyPair I;
@Nullable
@@ -1298,6 +1298,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
return true;
}
+ public void addTickable(Runnable tickable) { b(tickable); } // Purpur - OBFHELPER
public void b(Runnable runnable) {
this.tickables.add(runnable);
}
diff --git a/src/main/java/net/pl3x/purpur/gui/ColorPane.java b/src/main/java/net/pl3x/purpur/gui/ColorPane.java
new file mode 100644
index 0000000000..e819ecc1b0
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/gui/ColorPane.java
@@ -0,0 +1,80 @@
+package net.pl3x.purpur.gui;
+
+import net.md_5.bungee.api.ChatColor;
+import net.md_5.bungee.api.chat.BaseComponent;
+import net.md_5.bungee.api.chat.TextComponent;
+
+import javax.swing.JTextPane;
+import javax.swing.Timer;
+import javax.swing.text.AttributeSet;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.SimpleAttributeSet;
+import javax.swing.text.StyleConstants;
+import javax.swing.text.StyleContext;
+import java.util.HashSet;
+import java.util.Set;
+
+public class ColorPane extends JTextPane {
+ private static final GUIColor DEFAULT_COLOR = GUIColor.BLACK;
+
+ public void append(String str) {
+ BaseComponent[] components = TextComponent.fromLegacyText(DEFAULT_COLOR.getCode() + str, ChatColor.BLACK);
+ for (BaseComponent component : components) {
+ String text = component.toPlainText();
+ if (text == null || text.isEmpty()) {
+ continue;
+ }
+
+ GUIColor guiColor = GUIColor.getColor(component.getColor());
+
+ StyleContext context = StyleContext.getDefaultStyleContext();
+ AttributeSet attr = context.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, guiColor.getColor());
+ attr = context.addAttribute(attr, StyleConstants.CharacterConstants.Bold, component.isBold() || guiColor != DEFAULT_COLOR);
+ attr = context.addAttribute(attr, StyleConstants.CharacterConstants.Italic, component.isItalic());
+ attr = context.addAttribute(attr, StyleConstants.CharacterConstants.Underline, component.isUnderlined());
+ attr = context.addAttribute(attr, StyleConstants.CharacterConstants.StrikeThrough, component.isStrikethrough());
+ //attr = context.addAttribute(attr, StyleConstants.CharacterConstants.Blink, component.isObfuscated()); // no such thing as Blink, sadly
+
+ try {
+ int pos = getDocument().getLength();
+ getDocument().insertString(pos, text, attr);
+
+ if (component.isObfuscated()) {
+ // dirty hack to blink some text
+ Blink blink = new Blink(pos, text.length(), attr, context.addAttribute(attr, StyleConstants.Foreground, getBackground()));
+ BLINKS.add(blink);
+ }
+ } catch (BadLocationException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private static final Set<Blink> BLINKS = new HashSet<>();
+ private static boolean SYNC_BLINK;
+
+ static {
+ Timer timer = new Timer(500, e -> {
+ SYNC_BLINK = !SYNC_BLINK;
+ BLINKS.forEach(Blink::blink);
+ });
+ timer.start();
+ }
+
+ private class Blink {
+ private final int start, length;
+ private final AttributeSet attr1;
+ private final AttributeSet attr2;
+
+ private Blink(int start, int length, AttributeSet attr1, AttributeSet attr2) {
+ this.start = start;
+ this.length = length;
+ this.attr1 = attr1;
+ this.attr2 = attr2;
+ }
+
+ private void blink() {
+ getStyledDocument().setCharacterAttributes(start, length, SYNC_BLINK ? attr1 : attr2, true);
+ }
+ }
+}
diff --git a/src/main/java/net/pl3x/purpur/gui/ConsolePanel.java b/src/main/java/net/pl3x/purpur/gui/ConsolePanel.java
new file mode 100644
index 0000000000..29d237d9e7
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/gui/ConsolePanel.java
@@ -0,0 +1,128 @@
+package net.pl3x.purpur.gui;
+
+import com.mojang.util.QueueLogAppender;
+import net.minecraft.server.DedicatedServer;
+import net.minecraft.server.DefaultUncaughtExceptionHandler;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.swing.AbstractAction;
+import javax.swing.JPanel;
+import javax.swing.JScrollBar;
+import javax.swing.JScrollPane;
+import javax.swing.JTextField;
+import javax.swing.KeyStroke;
+import javax.swing.ScrollPaneConstants;
+import javax.swing.SwingUtilities;
+import javax.swing.border.EtchedBorder;
+import javax.swing.border.TitledBorder;
+import java.awt.BorderLayout;
+import java.awt.Font;
+import java.awt.event.ActionEvent;
+import java.util.LinkedList;
+
+public class ConsolePanel extends JPanel {
+ private static final Font MONOSPACED = new Font("Monospaced", Font.PLAIN, 12);
+ private static final Logger LOGGER = LogManager.getLogger();
+
+ private final CommandHistory history = new CommandHistory();
+ private String currentCommand = "";
+ private int historyIndex = 0;
+
+ private Thread logAppenderThread;
+
+ ConsolePanel(DedicatedServer server) {
+ super(new BorderLayout());
+
+ ColorPane console = new ColorPane();
+ console.setEditable(false);
+ console.setFont(MONOSPACED);
+
+ JTextField jtextfield = new JTextField();
+ jtextfield.addActionListener((actionevent) -> {
+ String msg = jtextfield.getText().trim();
+ if (!msg.isEmpty()) {
+ server.issueCommand(msg, server.getServerCommandListener());
+ history.add(msg);
+ historyIndex = -1;
+ }
+ jtextfield.setText("");
+ });
+ jtextfield.getInputMap().put(KeyStroke.getKeyStroke("UP"), "up");
+ jtextfield.getInputMap().put(KeyStroke.getKeyStroke("DOWN"), "down");
+ jtextfield.getActionMap().put("up", new AbstractAction() {
+ @Override
+ public void actionPerformed(ActionEvent actionEvent) {
+ if (historyIndex < 0) {
+ currentCommand = jtextfield.getText();
+ }
+ if (historyIndex < history.size() - 1) {
+ jtextfield.setText(history.get(++historyIndex));
+ }
+ }
+ });
+ jtextfield.getActionMap().put("down", new AbstractAction() {
+ @Override
+ public void actionPerformed(ActionEvent actionEvent) {
+ if (historyIndex >= 0) {
+ if (historyIndex == 0) {
+ --historyIndex;
+ jtextfield.setText(currentCommand);
+ } else {
+ --historyIndex;
+ jtextfield.setText(history.get(historyIndex));
+ }
+ }
+ }
+ });
+
+ JScrollPane jscrollpane = new JScrollPane(console, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
+
+ add(jscrollpane, "Center");
+ add(jtextfield, "South");
+ setBorder(new TitledBorder(new EtchedBorder(), "Console"));
+
+ logAppenderThread = new Thread(() -> {
+ String msg;
+ while ((msg = QueueLogAppender.getNextLogEvent("ServerGuiConsole")) != null) {
+ this.print(console, jscrollpane, msg);
+ }
+
+ });
+ logAppenderThread.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(LOGGER));
+ logAppenderThread.setDaemon(true);
+ }
+
+ public void start() {
+ logAppenderThread.start();
+ }
+
+ private void print(ColorPane console, JScrollPane jscrollpane, String str) {
+ if (!SwingUtilities.isEventDispatchThread()) {
+ SwingUtilities.invokeLater(() -> print(console, jscrollpane, str));
+ } else {
+ JScrollBar jscrollbar = jscrollpane.getVerticalScrollBar();
+ boolean scrollToBottom = false;
+
+ if (jscrollpane.getViewport().getView() == console) {
+ scrollToBottom = (double) jscrollbar.getValue() + jscrollbar.getSize().getHeight() + (double) (MONOSPACED.getSize() * 4) > (double) jscrollbar.getMaximum();
+ }
+
+ console.append(str);
+
+ if (scrollToBottom) {
+ jscrollbar.setValue(Integer.MAX_VALUE);
+ }
+ }
+ }
+
+ public static class CommandHistory extends LinkedList<String> {
+ @Override
+ public boolean add(String command) {
+ if (size() > 1000) {
+ remove();
+ }
+ return super.offerFirst(command);
+ }
+ }
+}
diff --git a/src/main/java/net/pl3x/purpur/gui/GUIColor.java b/src/main/java/net/pl3x/purpur/gui/GUIColor.java
new file mode 100644
index 0000000000..2a280ae48a
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/gui/GUIColor.java
@@ -0,0 +1,58 @@
+package net.pl3x.purpur.gui;
+
+import net.md_5.bungee.api.ChatColor;
+
+import java.awt.Color;
+import java.util.HashMap;
+import java.util.Map;
+
+public enum GUIColor {
+ BLACK(ChatColor.BLACK, new Color(0x000000)),
+ DARK_BLUE(ChatColor.DARK_BLUE, new Color(0x0000AA)),
+ DARK_GREEN(ChatColor.DARK_GREEN, new Color(0x00AA00)),
+ DARK_AQUA(ChatColor.DARK_AQUA, new Color(0x009999)),
+ DARK_RED(ChatColor.DARK_RED, new Color(0xAA0000)),
+ DARK_PURPLE(ChatColor.DARK_PURPLE, new Color(0xAA00AA)),
+ GOLD(ChatColor.GOLD, new Color(0xBB8800)),
+ GRAY(ChatColor.GRAY, new Color(0x888888)),
+ DARK_GRAY(ChatColor.DARK_GRAY, new Color(0x444444)),
+ BLUE(ChatColor.BLUE, new Color(0x5555FF)),
+ GREEN(ChatColor.GREEN, new Color(0x55FF55)),
+ AQUA(ChatColor.AQUA, new Color(0x55DDDD)),
+ RED(ChatColor.RED, new Color(0xFF5555)),
+ LIGHT_PURPLE(ChatColor.LIGHT_PURPLE, new Color(0xFF55FF)),
+ YELLOW(ChatColor.YELLOW, new Color(0xFFBB00)),
+ WHITE(ChatColor.WHITE, new Color(0xBBBBBB));
+
+ private final ChatColor chat;
+ private final Color color;
+
+ private static final Map<ChatColor, GUIColor> BY_CHAT = new HashMap<>();
+
+ GUIColor(ChatColor chat, Color color) {
+ this.chat = chat;
+ this.color = color;
+ }
+
+ public Color getColor() {
+ return color;
+ }
+
+ public ChatColor getChatColor() {
+ return chat;
+ }
+
+ public String getCode() {
+ return chat.toString();
+ }
+
+ public static GUIColor getColor(ChatColor chat) {
+ return BY_CHAT.get(chat);
+ }
+
+ static {
+ for (GUIColor color : values()) {
+ BY_CHAT.put(color.chat, color);
+ }
+ }
+}
diff --git a/src/main/java/net/pl3x/purpur/gui/PlayerListComponent.java b/src/main/java/net/pl3x/purpur/gui/PlayerListComponent.java
new file mode 100644
index 0000000000..c97a6f7dc3
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/gui/PlayerListComponent.java
@@ -0,0 +1,27 @@
+package net.pl3x.purpur.gui;
+
+import net.minecraft.server.EntityPlayer;
+import net.minecraft.server.MinecraftServer;
+
+import javax.swing.JList;
+import java.util.Vector;
+
+public class PlayerListComponent extends JList<String> {
+ private final MinecraftServer server;
+ private int tickCount;
+
+ PlayerListComponent(MinecraftServer server) {
+ this.server = server;
+ server.addTickable(this::tick);
+ }
+
+ public void tick() {
+ if (tickCount++ % 20 == 0) {
+ Vector<String> vector = new Vector<>();
+ for (EntityPlayer player : server.getPlayerList().getPlayers()) {
+ vector.add(player.getProfile().getName());
+ }
+ setListData(vector);
+ }
+ }
+}
diff --git a/src/main/java/net/pl3x/purpur/gui/ServerGUI.java b/src/main/java/net/pl3x/purpur/gui/ServerGUI.java
new file mode 100644
index 0000000000..a779fea689
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/gui/ServerGUI.java
@@ -0,0 +1,121 @@
+package net.pl3x.purpur.gui;
+
+import com.google.common.collect.Lists;
+import net.minecraft.server.DedicatedServer;
+import net.pl3x.purpur.gui.info.ServerInfoPanel;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.swing.JComponent;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.ScrollPaneConstants;
+import javax.swing.UIManager;
+import javax.swing.WindowConstants;
+import javax.swing.border.EtchedBorder;
+import javax.swing.border.TitledBorder;
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.Collection;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class ServerGUI extends JComponent {
+ private static final Logger LOGGER = LogManager.getLogger();
+
+ private final DedicatedServer server;
+
+ private final Collection<Runnable> finalizers = Lists.newArrayList();
+ private final AtomicBoolean isClosing = new AtomicBoolean();
+
+ private ConsolePanel consolePanel;
+
+ public static ServerGUI create(final DedicatedServer dedicatedserver) {
+ try {
+ UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+ } catch (Exception ignore) {
+ }
+
+ JFrame window = new JFrame("Purpur Minecraft Server");
+ ServerGUI serverGUI = new ServerGUI(dedicatedserver);
+
+ window.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
+ window.add(serverGUI);
+ window.pack();
+ window.setLocationRelativeTo(null);
+ window.setVisible(true);
+
+ window.addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent windowevent) {
+ if (!serverGUI.isClosing.getAndSet(true)) {
+ window.setTitle("Purpur Minecraft Server - Shutting Down!");
+ dedicatedserver.safeShutdown(true);
+ serverGUI.runFinalizers();
+ }
+
+ }
+ });
+
+ serverGUI.addFinalizer(window::dispose);
+ serverGUI.start();
+
+ return serverGUI;
+ }
+
+ private ServerGUI(DedicatedServer dedicatedserver) {
+ this.server = dedicatedserver;
+
+ setPreferredSize(new Dimension(854, 480));
+ setLayout(new BorderLayout());
+
+ consolePanel = new ConsolePanel(server);
+
+ try {
+ add(consolePanel, "Center");
+ add(buildInfoPanel(), "West");
+ } catch (Exception exception) {
+ LOGGER.error("Couldn't build server GUI", exception);
+ }
+ }
+
+ private void addFinalizer(Runnable runnable) {
+ finalizers.add(runnable);
+ }
+
+ private JComponent buildInfoPanel() {
+ JPanel jpanel = new JPanel(new BorderLayout());
+
+ ServerInfoPanel serverInfo = new ServerInfoPanel(server);
+ finalizers.add(serverInfo::stop);
+
+ jpanel.add(serverInfo, "North");
+ jpanel.add(buildPlayerPanel(), "Center");
+
+ jpanel.setBorder(new TitledBorder(new EtchedBorder(), "Stats"));
+ return jpanel;
+ }
+
+ private JComponent buildPlayerPanel() {
+ JScrollPane jscrollpane = new JScrollPane(new PlayerListComponent(server), ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
+ jscrollpane.setBorder(new TitledBorder(new EtchedBorder(), "Players"));
+ return jscrollpane;
+ }
+
+ public void start() {
+ consolePanel.start();
+ }
+
+ public void close() {
+ if (!isClosing.getAndSet(true)) {
+ runFinalizers();
+ }
+
+ }
+
+ private void runFinalizers() {
+ finalizers.forEach(Runnable::run);
+ }
+}
diff --git a/src/main/java/net/pl3x/purpur/gui/info/RAMDetails.java b/src/main/java/net/pl3x/purpur/gui/info/RAMDetails.java
new file mode 100644
index 0000000000..b1ea91b49b
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/gui/info/RAMDetails.java
@@ -0,0 +1,52 @@
+package net.pl3x.purpur.gui.info;
+
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.SystemUtils;
+
+import javax.swing.DefaultListCellRenderer;
+import javax.swing.JList;
+import javax.swing.border.EmptyBorder;
+import java.awt.Dimension;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.util.Locale;
+import java.util.Vector;
+
+public class RAMDetails extends JList<String> {
+ private static final DecimalFormat DECIMAL_FORMAT = SystemUtils.a(new DecimalFormat("########0.000"), (format)
+ -> format.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ROOT)));
+ private final MinecraftServer server;
+
+ RAMDetails(MinecraftServer server) {
+ this.server = server;
+
+ setBorder(new EmptyBorder(0, 10, 0, 0));
+ setFixedCellHeight(20);
+ setOpaque(false);
+
+ DefaultListCellRenderer renderer = new DefaultListCellRenderer();
+ renderer.setOpaque(false);
+ setCellRenderer(renderer);
+ }
+
+ @Override
+ public Dimension getPreferredSize() {
+ return new Dimension(350, 100);
+ }
+
+ public void update(RAMGraph graph) {
+ Vector<String> vector = new Vector<>();
+ vector.add("Memory use: " + (graph.usedMem / 1024L / 1024L) + " mb (" + (graph.free * 100L / graph.max) + "% free)");
+ vector.add("Heap: " + (graph.total / 1024L / 1024L) + " / " + (graph.max / 1024L / 1024L) + " mb");
+ vector.add("Avg tick: " + DECIMAL_FORMAT.format(getAverage(server.getTickTimes()) * 1.0E-6D) + " ms");
+ setListData(vector);
+ }
+
+ private double getAverage(long[] values) {
+ long total = 0L;
+ for (long value : values) {
+ total += value;
+ }
+ return (double) total / (double) values.length;
+ }
+}
diff --git a/src/main/java/net/pl3x/purpur/gui/info/RAMGraph.java b/src/main/java/net/pl3x/purpur/gui/info/RAMGraph.java
new file mode 100644
index 0000000000..7623088bae
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/gui/info/RAMGraph.java
@@ -0,0 +1,179 @@
+package net.pl3x.purpur.gui.info;
+
+import javax.swing.JComponent;
+import javax.swing.SwingUtilities;
+import javax.swing.Timer;
+import javax.swing.ToolTipManager;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.MouseInfo;
+import java.awt.Point;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.concurrent.TimeUnit;
+
+public class RAMGraph extends JComponent {
+ private final Timer timer;
+
+ private final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
+
+ private final Color[] colors = new Color[101];
+ private final Color[] colorsAlpha = new Color[101];
+
+ private final RAMQueue<Long> usedMemQueue = new RAMQueue<>();
+ private final RAMQueue<Integer> usedPercentQueue = new RAMQueue<>();
+
+ private int currentTick;
+
+ protected long total;
+ protected long free;
+ protected long max;
+
+ long usedMem;
+
+ RAMGraph() {
+ for (int i = 0; i < 350; i++) {
+ usedMemQueue.add(0L);
+ usedPercentQueue.add(0);
+ }
+
+ for (int i = 0; i < 101; i++) {
+ Color color = getColor(i);
+ colors[i] = new Color(color.getRed() / 2, color.getGreen() / 2, color.getBlue() / 2, 255);
+ colorsAlpha[i] = new Color(colors[i].getRed(), colors[i].getGreen(), colors[i].getBlue(), 125);
+ }
+
+ ToolTipManager.sharedInstance().setInitialDelay(0);
+
+ addMouseListener(new MouseAdapter() {
+ final int defaultDismissTimeout = ToolTipManager.sharedInstance().getDismissDelay();
+ final int dismissDelayMinutes = (int) TimeUnit.MINUTES.toMillis(10); // 10 minutes
+
+ @Override
+ public void mouseEntered(MouseEvent me) {
+ ToolTipManager.sharedInstance().setDismissDelay(dismissDelayMinutes);
+ }
+
+ @Override
+ public void mouseExited(MouseEvent me) {
+ ToolTipManager.sharedInstance().setDismissDelay(defaultDismissTimeout);
+ }
+ });
+
+ timer = new Timer(50, (event) -> repaint());
+ timer.start();
+ }
+
+ @Override
+ public Dimension getPreferredSize() {
+ return new Dimension(350, 110);
+ }
+
+ protected void update() {
+ total = Runtime.getRuntime().totalMemory();
+ free = Runtime.getRuntime().freeMemory();
+ max = Runtime.getRuntime().maxMemory();
+ usedMem = total - free;
+
+ usedMemQueue.add(usedMem);
+ usedPercentQueue.add((int) (usedMem * 100L / max));
+
+ Point scr = MouseInfo.getPointerInfo().getLocation();
+ Point loc = new Point(scr);
+ SwingUtilities.convertPointFromScreen(loc, this);
+ if (this.contains(loc)) {
+ ToolTipManager.sharedInstance().mouseMoved(
+ new MouseEvent(this, -1, System.currentTimeMillis(), 0, loc.x, loc.y,
+ scr.x, scr.y, 0, false, 0));
+ }
+
+ currentTick++;
+ }
+
+ @Override
+ public void paint(Graphics graphics) {
+ graphics.setColor(new Color(0xFFFFFFFF));
+ graphics.fillRect(0, 0, 350, 100);
+
+ graphics.setColor(new Color(0x888888));
+ graphics.drawLine(1, 25, 348, 25);
+ graphics.drawLine(1, 50, 348, 50);
+ graphics.drawLine(1, 75, 348, 75);
+
+ int i = 0;
+ for (Integer used : usedPercentQueue) {
+ i++;
+ if ((i + currentTick) % 120 == 0) {
+ graphics.setColor(new Color(0x888888));
+ graphics.drawLine(i, 1, i, 99);
+ }
+
+ if (used > 0) {
+ Color color = colors[used];
+ graphics.setColor(colorsAlpha[used]);
+ graphics.fillRect(i, 100 - used, 1, used);
+ graphics.setColor(color);
+ graphics.fillRect(i, 100 - used, 1, 1);
+ }
+ }
+
+ graphics.setColor(new Color(0xFF000000));
+ graphics.drawRect(0, 0, 348, 100);
+
+ Point m = getMousePosition();
+ if (m != null && m.x > 0 && m.x < 348 && m.y > 0 && m.y < 100) {
+ Integer used = usedPercentQueue.get(m.x);
+ graphics.setColor(new Color(0x000000));
+ graphics.drawLine(m.x, 1, m.x, 99);
+ graphics.drawOval(m.x - 2, 100 - used - 2, 5, 5);
+ graphics.setColor(colors[used]);
+ graphics.fillOval(m.x - 2, 100 - used - 2, 5, 5);
+ setToolTipText(String.format("<html><body>Used: %s mb (%s%%)<br/>%s</body></html>",
+ Math.round(usedMemQueue.get(m.x) / 1024F / 1024F),
+ used, getTime(m.x)));
+ }
+ }
+
+ protected void stop() {
+ timer.stop();
+ }
+
+ private String getTime(int halfSeconds) {
+ int millis = (348 - halfSeconds) / 2 * 1000;
+ return sdf.format(new Date((System.currentTimeMillis() - millis)));
+ }
+
+ private Color getColor(int percent) {
+ if (percent <= 50) {
+ return new Color(0X00FF00);
+ }
+
+ int value = 510 - (int) (Math.min(Math.max(0, ((percent - 50) / 50F)), 1) * 510);
+
+ int red, green;
+ if (value < 255) {
+ red = 255;
+ green = (int) (Math.sqrt(value) * 16);
+ } else {
+ green = 255;
+ value = value - 255;
+ red = 255 - (value * value / 255);
+ }
+
+ return new Color(red, green, 0);
+ }
+
+ public static class RAMQueue<E> extends LinkedList<E> {
+ @Override
+ public boolean add(E e) {
+ if (size() >= 348) {
+ remove();
+ }
+ return super.add(e);
+ }
+ }
+}
diff --git a/src/main/java/net/pl3x/purpur/gui/info/ServerInfoPanel.java b/src/main/java/net/pl3x/purpur/gui/info/ServerInfoPanel.java
new file mode 100644
index 0000000000..c4519794c9
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/gui/info/ServerInfoPanel.java
@@ -0,0 +1,42 @@
+package net.pl3x.purpur.gui.info;
+
+import net.minecraft.server.MinecraftServer;
+
+import javax.swing.JPanel;
+import javax.swing.Timer;
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+
+public class ServerInfoPanel extends JPanel {
+ private final Timer timer;
+ private final RAMGraph ramGraph;
+ private final RAMDetails ramDetails;
+
+ public ServerInfoPanel(MinecraftServer server) {
+ super(new BorderLayout());
+
+ setOpaque(false);
+
+ ramGraph = new RAMGraph();
+ ramDetails = new RAMDetails(server);
+
+ add(ramGraph, "North");
+ add(ramDetails, "Center");
+
+ timer = new Timer(500, (event) -> {
+ ramGraph.update();
+ ramDetails.update(ramGraph);
+ });
+ timer.start();
+ }
+
+ @Override
+ public Dimension getPreferredSize() {
+ return new Dimension(350, 200);
+ }
+
+ public void stop() {
+ timer.stop();
+ ramGraph.stop();
+ }
+}
diff --git a/src/main/java/net/pl3x/purpur/util/HighlightErrorConverter.java b/src/main/java/net/pl3x/purpur/util/HighlightErrorConverter.java
new file mode 100644
index 0000000000..4b340b88a2
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/util/HighlightErrorConverter.java
@@ -0,0 +1,86 @@
+package net.pl3x.purpur.util;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.layout.PatternLayout;
+import org.apache.logging.log4j.core.pattern.ConverterKeys;
+import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
+import org.apache.logging.log4j.core.pattern.PatternConverter;
+import org.apache.logging.log4j.core.pattern.PatternFormatter;
+import org.apache.logging.log4j.core.pattern.PatternParser;
+import org.apache.logging.log4j.util.PerformanceSensitive;
+
+import java.util.List;
+
+@Plugin(name = "highlightGUIError", category = PatternConverter.CATEGORY)
+@ConverterKeys({"highlightGUIError"})
+@PerformanceSensitive("allocation")
+public final class HighlightErrorConverter extends LogEventPatternConverter {
+
+ private static final String ERROR = "\u00A74\u00A7l"; // Bold Red
+ private static final String WARN = "\u00A7e\u00A7l"; // Bold Yellow
+
+ private final List<PatternFormatter> formatters;
+
+ protected HighlightErrorConverter(List<PatternFormatter> formatters) {
+ super("highlightGUIError", null);
+ this.formatters = formatters;
+ }
+
+ @Override
+ public void format(LogEvent event, StringBuilder toAppendTo) {
+ Level level = event.getLevel();
+ if (level.isMoreSpecificThan(Level.ERROR)) {
+ format(ERROR, event, toAppendTo);
+ return;
+ } else if (level.isMoreSpecificThan(Level.WARN)) {
+ format(WARN, event, toAppendTo);
+ return;
+ }
+ for (PatternFormatter formatter : formatters) {
+ formatter.format(event, toAppendTo);
+ }
+ }
+
+ private void format(String style, LogEvent event, StringBuilder toAppendTo) {
+ int start = toAppendTo.length();
+ toAppendTo.append(style);
+ int end = toAppendTo.length();
+
+ for (PatternFormatter formatter : formatters) {
+ formatter.format(event, toAppendTo);
+ }
+
+ if (toAppendTo.length() == end) {
+ toAppendTo.setLength(start);
+ }
+ }
+
+ @Override
+ public boolean handlesThrowable() {
+ for (final PatternFormatter formatter : formatters) {
+ if (formatter.handlesThrowable()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static HighlightErrorConverter newInstance(Configuration config, String[] options) {
+ if (options.length != 1) {
+ LOGGER.error("Incorrect number of options on highlightGUIError. Expected 1 received " + options.length);
+ return null;
+ }
+ if (options[0] == null) {
+ LOGGER.error("No pattern supplied on highlightGUIError");
+ return null;
+ }
+
+ PatternParser parser = PatternLayout.createPatternParser(config);
+ List<PatternFormatter> formatters = parser.parse(options[0]);
+ return new HighlightErrorConverter(formatters);
+ }
+
+}
diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml
index a9bb987652..4be7613d0f 100644
--- a/src/main/resources/log4j2.xml
+++ b/src/main/resources/log4j2.xml
@@ -1,8 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
-<Configuration status="WARN" packages="com.mojang.util">
+<Configuration status="WARN" packages="com.mojang.util,net.pl3x.purpur.util"><!-- Purpur -->
<Appenders>
<Queue name="ServerGuiConsole">
- <PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg%n" />
+ <!-- Purpur start -->
+ <PatternLayout>
+ <LoggerNamePatternSelector defaultPattern="%highlightGUIError{[%d{HH:mm:ss} %level]: [%logger] %msg%n%xEx}">
+ <!-- Log root, Minecraft, Mojang and Bukkit loggers without prefix -->
+ <!-- Disable prefix for various plugins that bypass the plugin logger -->
+ <PatternMatch key=",net.minecraft.,Minecraft,com.mojang.,com.sk89q.,ru.tehkode.,Minecraft.AWE"
+ pattern="%highlightGUIError{[%d{HH:mm:ss} %level]: %msg%n%xEx}" />
+ </LoggerNamePatternSelector>
+ </PatternLayout>
+ <!-- Purpur end -->
</Queue>
<TerminalConsole name="TerminalConsole">
<PatternLayout>
--
2.24.0