Server plugins + clientside commands

This commit is contained in:
Anuken 2019-08-28 20:38:12 -04:00
parent 865c3f28d5
commit 5f9370da9a
9 changed files with 200 additions and 14 deletions

View File

@ -121,6 +121,8 @@ public class Vars implements Loadable{
public static FileHandle tmpDirectory;
/** data subdirectory used for saves */
public static FileHandle saveDirectory;
/** data subdirectory used for plugins */
public static FileHandle pluginDirectory;
/** old map file extension, for conversion */
public static final String oldMapExtension = "mmap";
/** map file extension */
@ -237,6 +239,7 @@ public class Vars implements Loadable{
mapPreviewDirectory = dataDirectory.child("previews/");
saveDirectory = dataDirectory.child("saves/");
tmpDirectory = dataDirectory.child("tmp/");
pluginDirectory = dataDirectory.child("plugins/");
maps.load();
}

View File

@ -7,6 +7,7 @@ import io.anuke.arc.collection.IntSet;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.math.RandomXS128;
import io.anuke.arc.util.*;
import io.anuke.arc.util.CommandHandler.*;
import io.anuke.arc.util.io.ReusableByteInStream;
import io.anuke.arc.util.serialization.Base64Coder;
import io.anuke.mindustry.Vars;
@ -151,12 +152,35 @@ public class NetClient implements ApplicationListener{
throw new ValidateException(player, "Player has sent a message above the text limit.");
}
//server console logging
Log.info("&y{0}: &lb{1}", player.name, message);
//check if it's a command
CommandResponse response = netServer.clientCommands.handleMessage(message, player);
if(response.type == ResponseType.noCommand){ //no command to handle
//server console logging
Log.info("&y{0}: &lb{1}", player.name, message);
//invoke event for all clients but also locally
//this is required so other clients get the correct name even if they don't know who's sending it yet
Call.sendMessage(message, colorizeName(player.id, player.name), player);
//invoke event for all clients but also locally
//this is required so other clients get the correct name even if they don't know who's sending it yet
Call.sendMessage(message, colorizeName(player.id, player.name), player);
}else{
//log command to console but with brackets
Log.info("<&y{0}: &lm{1}&lg>", player.name, message);
//a command was sent, now get the output
if(response.type != ResponseType.valid){
String text;
//send usage
if(response.type == ResponseType.manyArguments){
text = "[scarlet]Too many arguments. Usage:[lightgray] " + response.command.text + "[gray] " + response.command.paramText;
}else if(response.type == ResponseType.fewArguments){
text = "[scarlet]Too few arguments. Usage:[lightgray] " + response.command.text + "[gray] " + response.command.paramText;
}else{ //unknown command
text = "[scarlet]Unknown command. Check [lightgray]/help[scarlet].";
}
player.sendMessage(text);
}
}
}
private static String colorizeName(int id, String name){

View File

@ -12,6 +12,7 @@ import io.anuke.arc.math.Mathf;
import io.anuke.arc.math.geom.Rectangle;
import io.anuke.arc.math.geom.Vector2;
import io.anuke.arc.util.*;
import io.anuke.arc.util.CommandHandler.*;
import io.anuke.arc.util.io.*;
import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.core.GameState.State;
@ -47,6 +48,7 @@ public class NetServer implements ApplicationListener{
private final static float correctDist = 16f;
public final Administration admins = new Administration();
public final CommandHandler clientCommands = new CommandHandler("/");
/** Maps connection IDs to players. */
private IntMap<Player> connections = new IntMap<>();
@ -192,6 +194,34 @@ public class NetServer implements ApplicationListener{
if(player == null) return;
RemoteReadServer.readPacket(packet.writeBuffer, packet.type, player);
});
registerCommands();
}
private void registerCommands(){
clientCommands.<Player>register("help", "[page]", "Lists all commands.", (args, player) -> {
if(args.length > 0 && !Strings.canParseInt(args[0])){
player.sendMessage("[scarlet]'page' must be a number.");
return;
}
int commandsPerPage = 6;
int page = args.length > 0 ? Strings.parseInt(args[0]) : 0;
int pages = Mathf.ceil((float)clientCommands.getCommandList().size / commandsPerPage);
if(page > pages || page < 0){
player.sendMessage("[scarlet]'page' must be a number between[orange] 0[] and[orange] " + pages + "[].");
return;
}
StringBuilder result = new StringBuilder();
result.append(Strings.format("[orange]-- Command Page[lightgray] {0}[gray]/[lightgray]{1}[orange] --\n\n", page, commandsPerPage));
for(int i = commandsPerPage * page; i < Math.min(commandsPerPage * (page + 1), clientCommands.getCommandList().size); i++){
Command command = clientCommands.getCommandList().get(i);
result.append("[orange] ").append(command.text).append("[lightgray] ").append(command.paramText).append("[lightgray] - ").append(command.description).append("\n");
}
player.sendMessage(result.toString());
});
}
public Team assignTeam(Player current, Iterable<Player> players){

View File

@ -774,6 +774,10 @@ public class Player extends Unit implements BuilderMinerTrait, ShooterTrait{
//region utility methods
public void sendMessage(String text){
Call.sendMessage(con.id, text, null, null);
}
/** Resets all values of the player. */
public void reset(){
resetNoAdd();

View File

@ -7,7 +7,6 @@ import io.anuke.arc.collection.*;
import static io.anuke.mindustry.Vars.headless;
public class Administration{
/** All player info. Maps UUIDs to info. This persists throughout restarts. */
private ObjectMap<String, PlayerInfo> playerInfo = new ObjectMap<>();
private Array<String> bannedIPs = new Array<>();

View File

@ -22,11 +22,10 @@ public class MindustryServer implements ApplicationListener{
Vars.loadSettings();
Vars.init();
content.createContent();
content.init();
Core.app.addListener(logic = new Logic());
Core.app.addListener(netServer = new NetServer());
Core.app.addListener(new ServerControl(args));
content.init();
}
}

View File

@ -21,6 +21,8 @@ import io.anuke.mindustry.maps.*;
import io.anuke.mindustry.net.Administration.*;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.net.Packets.*;
import io.anuke.mindustry.server.plugin.*;
import io.anuke.mindustry.server.plugin.Plugins.*;
import io.anuke.mindustry.type.*;
import java.io.*;
@ -40,6 +42,7 @@ public class ServerControl implements ApplicationListener{
private final CommandHandler handler = new CommandHandler("");
private final FileHandle logFolder = Core.files.local("logs/");
private final Plugins plugins = new Plugins();
private FileHandle currentLogFile;
private boolean inExtraRound;
@ -107,6 +110,9 @@ public class ServerControl implements ApplicationListener{
Effects.setScreenShakeProvider((a, b) -> {});
Effects.setEffectProvider((a, b, c, d, e, f) -> {});
//load plugins
plugins.load();
registerCommands();
Core.app.post(() -> {
@ -118,7 +124,7 @@ public class ServerControl implements ApplicationListener{
}
for(String s : commands){
Response response = handler.handleMessage(s);
CommandResponse response = handler.handleMessage(s);
if(response.type != ResponseType.valid){
err("Invalid command argument sent: '{0}': {1}", s, response.type.name());
err("Argument usage: &lc<command-1> <command1-args...>,<command-2> <command-2-args2...>");
@ -128,6 +134,7 @@ public class ServerControl implements ApplicationListener{
});
customMapDirectory.mkdirs();
pluginDirectory.mkdirs();
Thread thread = new Thread(this::readCommands, "Server Controls");
thread.setDaemon(true);
@ -166,6 +173,13 @@ public class ServerControl implements ApplicationListener{
}
});
//initialize plugins
plugins.each(Plugin::init);
if(!plugins.all().isEmpty()){
info("&lc{0} plugins loaded.", plugins.all().size);
}
info("&lcServer loaded. Type &ly'help'&lc for help.");
System.out.print("> ");
@ -306,6 +320,31 @@ public class ServerControl implements ApplicationListener{
}
});
handler.register("plugins", "Display all loaded plugins.", arg -> {
if(!plugins.all().isEmpty()){
info("Maps:");
for(LoadedPlugin plugin : plugins.all()){
info(" &ly{0} &lcv{1}", plugin.meta.name, plugin.meta.version);
}
}else{
info("No plugins found.");
}
info("&lyPlugin directory: &lb&fi{0}", pluginDirectory.file().getAbsoluteFile().toString());
});
handler.register("plugin", "<name...>", "Display information about a loaded plugin.", arg -> {
LoadedPlugin plugin = plugins.all().find(p -> p.meta.name.equalsIgnoreCase(arg[0]));
if(plugin != null){
info("Name: &ly{0}", plugin.meta.name);
info("Version: &ly{0}", plugin.meta.version);
info("Author: &ly{0}", plugin.meta.author);
info("Path: &ly{0}", plugin.jarFile.path());
info("Description: &ly{0}", plugin.meta.description);
}else{
info("No plugin with name &ly'{0}'&lg found.");
}
});
handler.register("say", "<message...>", "Send a message to all players.", arg -> {
if(!state.is(State.playing)){
err("Not hosting. Host a game first.");
@ -644,12 +683,15 @@ public class ServerControl implements ApplicationListener{
}
});
handler.register("gc", "Trigger a grabage collection. Testing onlu.", arg -> {
handler.register("gc", "Trigger a grabage collection. Testing only.", arg -> {
int pre = (int)(Core.app.getJavaHeap() / 1024 / 1024);
System.gc();
int post = (int)(Core.app.getJavaHeap() / 1024 / 1024);
info("&ly{0}&lg MB collected. Memory usage now at &ly{1}&lg MB.", pre - post, post);
});
plugins.each(p -> p.registerServerCommands(handler));
plugins.each(p -> p.registerClientCommands(netServer.clientCommands));
}
private void readCommands(){
@ -662,7 +704,7 @@ public class ServerControl implements ApplicationListener{
}
private void handleCommandString(String line){
Response response = handler.handleMessage(line);
CommandResponse response = handler.handleMessage(line);
if(response.type == ResponseType.unknownCommand){

View File

@ -1,5 +1,21 @@
package io.anuke.mindustry.server.plugin;
import io.anuke.arc.util.*;
public abstract class Plugin{
public abstract void init();
/** Called after all plugins have been created and commands have been registered.*/
public void init(){
}
/** Register any commands to be used on the server side, e.g. from the console. */
public void registerServerCommands(CommandHandler handler){
}
/** Register any commands to be used on the client side, e.g. sent from an in-game player.. */
public void registerClientCommands(CommandHandler handler){
}
}

View File

@ -1,13 +1,82 @@
package io.anuke.mindustry.server.plugin;
import io.anuke.arc.collection.*;
import io.anuke.arc.collection.Array;
import io.anuke.arc.files.*;
import io.anuke.arc.function.*;
import io.anuke.arc.util.*;
import io.anuke.mindustry.io.*;
import java.lang.reflect.*;
import java.net.*;
import static io.anuke.mindustry.Vars.pluginDirectory;
public class Plugins{
private Array<Plugin> loaded = new Array<>();
private Array<LoadedPlugin> loaded = new Array<>();
/** Loads all plugins from the folder, but does call any methods on them.*/
public void load(){
for(FileHandle file : pluginDirectory.list()){
if(!file.extension().equals("jar")) continue;
try{
loaded.add(loadPlugin(file));
}catch(IllegalArgumentException ignored){
}catch(Exception e){
Log.err("Failed to load plugin file {0}. Skipping.", file);
e.printStackTrace();
}
}
}
/** @return all loaded plugins. */
public Array<LoadedPlugin> all(){
return loaded;
}
/** Iterates through each plugin.*/
public void each(Consumer<Plugin> cons){
loaded.each(p -> cons.accept(p.plugin));
}
private LoadedPlugin loadPlugin(FileHandle jar) throws Exception{
FileHandle zip = new ZipFileHandle(jar);
FileHandle metaf = zip.child("plugin.json");
if(!metaf.exists()){
Log.warn("Plugin {0} doesn't have a 'plugin.json' file, skipping.", jar);
throw new IllegalArgumentException();
}
PluginMeta meta = JsonIO.read(PluginMeta.class, metaf.readString());
URLClassLoader classLoader = (URLClassLoader)ClassLoader.getSystemClassLoader();
Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
method.setAccessible(true);
method.invoke(classLoader, jar.file().toURI().toURL());
Class<?> main = Class.forName(meta.main);
return new LoadedPlugin(jar, zip, (Plugin)main.newInstance(), meta);
}
/** Represents a plugin that has been loaded from a jar file.*/
public static class LoadedPlugin{
public final FileHandle jarFile;
public final FileHandle zipRoot;
public final Plugin plugin;
public final PluginMeta meta;
public LoadedPlugin(FileHandle jarFile, FileHandle zipRoot, Plugin plugin, PluginMeta meta){
this.zipRoot = zipRoot;
this.jarFile = jarFile;
this.plugin = plugin;
this.meta = meta;
}
}
/** Plugin metadata information.*/
public static class PluginMeta{
public String name, author, main, description;
public String version;
}
}