diff --git a/BotThread.java b/BotThread.java new file mode 100644 index 0000000..5a2d365 --- /dev/null +++ b/BotThread.java @@ -0,0 +1,73 @@ +package mindustry.plugin; + +import arc.math.Mathf; +import arc.util.CommandHandler; +import mindustry.gen.Groups; +import mindustry.gen.Player; +import mindustry.plugin.commands.ModeratorCommands; +import mindustry.plugin.commands.PublicCommands; +import mindustry.plugin.commands.ReviewerCommands; +import mindustry.plugin.datas.PlayerData; +import mindustry.plugin.discord.ReactionAdd; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Activity; +import org.json.JSONObject; + +import mindustry.plugin.discord.DiscordCommands; + + +import static mindustry.Vars.netServer; +import static mindustry.plugin.ioMain.*; +import static mindustry.plugin.utils.Funcs.*; +import static mindustry.plugin.discord.Loader.*; + +public class BotThread extends Thread { + public JDA api; + private Thread mt; + private JSONObject data; + public DiscordCommands commandHandler = new DiscordCommands(); + public ReactionAdd reactionHandler = new ReactionAdd(); + + public CommandHandler publicHandler = new CommandHandler(prefix); + public CommandHandler reviewerHandler = new CommandHandler(prefix); + public CommandHandler moderatorHandler = new CommandHandler(prefix); + + public BotThread(JDA api, Thread mt, JSONObject data) { + this.api = api; //new DiscordApiBuilder().setToken(data.get(0)).login().join(); + this.mt = mt; + this.data = data; + + // register commands + api.addEventListener(commandHandler); + api.addEventListener(reactionHandler); + + new PublicCommands().registerCommands(publicHandler); + new ReviewerCommands().registerCommands(reviewerHandler); + new ModeratorCommands().registerCommands(moderatorHandler); + } + + public void run(){ + while (this.mt.isAlive()){ + try { + Thread.sleep(30 * 1000); + + for (Player p : Groups.player) { + PlayerData pd = playerDataHashMap.get(p.uuid()); + if (pd != null) + setJedisData(p.uuid(), pd); + } + + if(Mathf.chance(0.01f)){ + api.getPresence().setActivity(Activity.playing("( ͡° ͜ʖ ͡°)")); + } else { + api.getPresence().setActivity(Activity.playing("with " + Groups.player.size() + (netServer.admins.getPlayerLimit() == 0 ? "" : "/" + netServer.admins.getPlayerLimit()) + " players")); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + + api.shutdown(); + } +} diff --git a/ModeratorCommands.java b/ModeratorCommands.java new file mode 100644 index 0000000..64ec2b0 --- /dev/null +++ b/ModeratorCommands.java @@ -0,0 +1,416 @@ +package mindustry.plugin.commands; + +import arc.Core; +import arc.math.Mathf; +import arc.util.CommandHandler; +import mindustry.content.Bullets; +import mindustry.content.UnitTypes; +import mindustry.entities.bullet.BulletType; +import mindustry.game.Team; +import mindustry.gen.Call; +import mindustry.gen.Groups; +import mindustry.gen.Player; +import mindustry.gen.Unit; +import mindustry.net.Administration; +import mindustry.plugin.datas.PlayerData; +import mindustry.plugin.discord.Context; +import mindustry.plugin.ioMain; +import mindustry.type.UnitType; +import net.dv8tion.jda.api.EmbedBuilder; + +import java.lang.reflect.Field; +import java.time.Instant; +import java.util.HashMap; +import java.util.stream.IntStream; + +import static mindustry.Vars.*; +import static mindustry.net.Administration.*; +import static mindustry.net.Packets.*; +import static mindustry.plugin.discord.Loader.serverName; +import static mindustry.plugin.ioMain.*; +import static mindustry.plugin.utils.Funcs.*; + +public class ModeratorCommands { + public ModeratorCommands(){ + } + + public void registerCommands(CommandHandler handler){ + handler.register("announce", "", "Display a message on top of the screen for all players for 10 seconds", (args, ctx) -> { + Call.infoToast(args[0], 10); + ctx.sendEmbed(true, ":round_pushpin: announcement sent successfully!", args[0]); + }); + + handler.register("event", " [force?]", "Set the new event's ip & port. If force is set to true, all players will be forced to join immedietly.", (args, ctx) -> { + eventIp = args[0]; + eventPort = Integer.parseInt(args[1]); + boolean f = false; + if(args.length >= 3 && Boolean.parseBoolean(args[2])) { + f = true; + Groups.player.forEach(player -> { + Call.connect(player.con, eventIp, eventPort); + }); + } + ctx.sendEmbed(true, ":crossed_swords: event ip set successfully!", args[0] + ":" + eventPort + (f ? "\nalso forced everyone to join" : "")); + }); + + handler.register("alert", " ", " " + serverName, (args, ctx) -> { + Player player = findPlayer(args[0]); + if(args[0].toLowerCase().equals("all")){ + Call.infoMessage(args[1]); + ctx.sendEmbed(true, ":round_pushpin: alert to everyone sent successfully!", args[1]); + }else{ + if(player != null){ + Call.infoMessage(player.con, args[1]); + ctx.sendEmbed(true, ":round_pushpin: alert to " + escapeCharacters(player.name) + " sent successfully!", args[1]); + }else{ + ctx.sendEmbed(false, ":round_pushpin: can't find player " + args[1]); + } + } + }); + + handler.register("ban", " [reason...]", "Ban a player by the provided name, id or uuid (do offline bans using uuid)", (args, ctx) -> { + Player player = findPlayer(args[0]); + if(player != null){ + PlayerData pd = playerDataHashMap.get(player.uuid()); + if(pd != null){ + long until = Instant.now().getEpochSecond() + Integer.parseInt(args[1]) * 60; + pd.bannedUntil = until; + pd.banReason = (args.length >= 3 ? args[2] : "not specified") + "\n" + "[accent]Until: " + epochToString(until) + "\n[accent]Ban ID:[] " + player.uuid().substring(0, 4); + playerDataHashMap.put(player.uuid(), pd); + // setJedisData(player.uuid, pd); + HashMap fields = new HashMap<>(); + fields.put("UUID", player.uuid()); + ctx.sendEmbed(true, ":hammer: the ban hammer has been swung at " + escapeCharacters(player.name), "reason: *" + escapeColorCodes(pd.banReason) + "*", fields, false); + player.con.kick(KickReason.banned); + }else{ + ctx.sendEmbed(false, ":interrobang: internal server error, please ping fuzz"); + } + }else{ + PlayerData pd = getJedisData(args[0]); + if(pd != null){ + long until = Instant.now().getEpochSecond() + Integer.parseInt(args[1]) * 60; + pd.bannedUntil = until; + pd.banReason = (args.length >= 3 ? args[2] : "not specified") + "\n" + "[accent]Until: " + epochToString(until) + "\n[accent]Ban ID:[] " + args[0].substring(0, 4); + setJedisData(args[0], pd); + HashMap fields = new HashMap<>(); + fields.put("UUID", args[0]); + ctx.sendEmbed(true, ":hammer: the ban hammer has been swung at " + escapeCharacters(netServer.admins.getInfo(args[0]).lastName),"reason: *" + escapeColorCodes(pd.banReason) + "*", fields, false); + }else{ + ctx.sendEmbed(false, ":hammer: that player or uuid cannot be found"); + } + } + }); + + handler.register("kick", "", "Kick a player from " + serverName, (args, ctx) -> { + Player player = findPlayer(args[0]); + if(player != null){ + player.con.kick(KickReason.kick); + ctx.sendEmbed(true, ":football: kicked " + escapeCharacters(player.name) + " successfully!", player.uuid()); + }else{ + ctx.sendEmbed(false, ":round_pushpin: can't find player " + escapeCharacters(args[0])); + } + }); + + handler.register("unban", "", "Unban the specified player by uuid (works for votekicks as well)", (args, ctx) -> { + PlayerData pd = getJedisData(args[0]); + if(pd!= null){ + PlayerInfo info = netServer.admins.getInfo(args[0]); + info.lastKicked = 0; + pd.bannedUntil = 0; + setJedisData(args[0], pd); + ctx.sendEmbed(true, ":wrench: unbanned " + escapeCharacters(info.lastName) + " successfully!"); + }else{ + ctx.sendEmbed(false, ":wrench: that uuid doesn't exist in the database.."); + } + }); + + handler.register("playersinfo", "Check the information about all players on the server.", (args, ctx) -> { + EmbedBuilder eb = new EmbedBuilder(); + eb.setColor(Pals.progress); + eb.setTitle(":satellite: **players online: **" + Groups.player.size()); + + StringBuilder pi = new StringBuilder(); + int pn = 1; + for(Player p : Groups.player){ + if (!p.admin) { + pi + .append("**") + .append(pn + "•") + .append("** `") + .append(escapeCharacters(p.name)) + .append("` : ") + .append(p.con.address) + .append(" : ") + .append(p.uuid()) + .append("\n"); + } else { + pi + .append("**") + .append(pn + "•") + .append("** `") + .append(escapeCharacters(p.name)) + .append("`") + .append("\n"); + + + } + + pn++; + } + eb.setDescription(pi); + ctx.sendEmbed(eb); + + }); + + handler.register("lookup", "", "Lookup the specified player by uuid or name (name search only works when player is online)", (args, ctx) -> { + EmbedBuilder eb = new EmbedBuilder(); + Administration.PlayerInfo info; + Player player = findPlayer(args[0]); + if (player != null) { + info = netServer.admins.getInfo(player.uuid()); + } else{ + if(args[0].length() == 24) { // uuid length + info = netServer.admins.getInfo(args[0]); + }else{ + ctx.sendEmbed(false, ":mag: can't find that uuid in the database.."); + return; + } + } + eb.setColor(Pals.progress); + eb.setTitle(":mag: " + escapeCharacters(info.lastName) + "'s lookup"); + eb.addField("UUID", info.id, false); + eb.addField("Last used ip", info.lastIP, true); + eb.addField("Times kicked", String.valueOf(info.timesKicked), true); + + + + StringBuilder s = new StringBuilder(); + s.append("**All used names: **\n"); + for (String name : info.names) { + s.append(escapeCharacters(name)).append(" / "); + } + s.append("\n\n**All used IPs: **\n"); + for (String ip : info.ips) { + s.append(escapeCharacters(ip)).append(" / "); + } + eb.setDescription(s.toString()); + ctx.channel.sendMessage(eb.build()).queue(); + }); + + handler.register("setrank", " ", "Set the specified uuid's rank to the one provided.", (args, ctx) -> { + int rank; + try{ + rank = Integer.parseInt(args[1]); + }catch (NumberFormatException e) { + ctx.sendEmbed(false, ":wrench: error parsing rank number"); + return; + } + if(rank < rankNames.size()) { + PlayerData pd = playerDataHashMap.containsKey(args[0]) ? playerDataHashMap.get(args[0]) : getJedisData(args[0]); + if (pd != null) { + pd.rank = rank; + if(playerDataHashMap.containsKey(args[0])){ + playerDataHashMap.put(args[0], pd); + } + setJedisData(args[0], pd); + PlayerInfo info = netServer.admins.getInfo(args[0]); + ctx.sendEmbed(true, ":wrench: set " + escapeCharacters(info.lastName) + "'s rank to " + escapeColorCodes(rankNames.get(rank).name)); + } else { + ctx.sendEmbed(false, ":wrench: that uuid doesn't exist in the database.."); + } + }else{ + ctx.sendEmbed(false, ":wrench: error parsing rank number"); + } + }); + + handler.register("convert", " ", "Change a players unit into the specified one", (args, ctx) -> { + UnitType desiredUnit; + try { + Field field = UnitTypes.class.getDeclaredField(args[1]); + desiredUnit = (UnitType)field.get(null); + } catch (NoSuchFieldException | IllegalAccessException ignored) { + ctx.sendEmbed(false, ":robot: that unit doesn't exist"); + return; + } + Player player = findPlayer(args[0]); + if(player != null){ + + Unit nu = desiredUnit.create(player.team()); + nu.set(player.getX(), player.getY()); + nu.add(); + + player.unit().kill(); + player.unit(nu); + player.afterSync(); + + ctx.sendEmbed(true, ":robot: changed " + escapeCharacters(player.name) + "'s unit to " + desiredUnit.name); + }else if(args[0].toLowerCase().equals("all")){ + for(Player p : Groups.player){ + Unit nu = desiredUnit.create(p.team()); + nu.set(p.getX(), p.getY()); + nu.add(); + + p.unit().kill(); + p.unit(nu); + p.afterSync(); + } + ctx.sendEmbed(true, ":robot: changed everyone's unit to " + desiredUnit.name); + }else{ + ctx.sendEmbed(false, ":robot: can't find " + escapeCharacters(args[0])); + } + }); + + handler.register("team", " ", "Change a players team into the specified team id", (args, ctx) -> { + int teamid; + try{ + teamid = Integer.parseInt(args[1]); + }catch (Exception e){ ctx.sendEmbed(false, ":triangular_flag_on_post: error parsing team id number"); return;} + + Player player = findPlayer(args[0]); + if(player != null){ + player.team(Team.get(teamid)); + ctx.sendEmbed(true, ":triangular_flag_on_post: changed " + escapeCharacters(player.name) + "'s team to " + Team.get(teamid).name); + }else if(args[0].toLowerCase().equals("all")){ + for(Player p : Groups.player){ p.team(Team.get(teamid)); } + ctx.sendEmbed(true, ":triangular_flag_on_post: changed everyone's team to " + Team.get(teamid).name); + }else{ + ctx.sendEmbed(false, ":triangular_flag_on_post: can't find " + escapeCharacters(args[0])); + } + }); + + handler.register("motd", "", "Change the welcome message popup when a new player joins Set to 'none' if you want to disable motd", (args, ctx) -> { + if(args[0].toLowerCase().equals("none")){ + welcomeMessage = ""; + ctx.sendEmbed(true, ":newspaper: disabled welcome message successfully!"); + }else{ + welcomeMessage = args[0]; + ctx.sendEmbed(true, ":newspaper: changed welcome message successfully!", args[0]); + } + Core.settings.put("welcomeMessage", welcomeMessage); + Core.settings.forceSave(); + }); + + handler.register("statmessage", "", "Change the stat message popup when a player uses the /info command", (args, ctx) -> { + if(args[0].toLowerCase().equals("none")){ + statMessage = ""; + ctx.sendEmbed(true, ":newspaper: disabled stat message successfully!"); + }else{ + statMessage = args[0]; + ctx.sendEmbed(true, ":newspaper: changed stat message successfully!", args[0]); + } + Core.settings.put("statMessage", statMessage); + Core.settings.forceSave(); + }); + + handler.register("spawn", " ", "Spawn a specified amount of units near the player's position.", (args, ctx) -> { + int amt; + try{ + amt = Integer.parseInt(args[2]); + }catch (Exception e){ ctx.sendEmbed(false, ":robot: error parsing amount number"); return;} + + UnitType desiredUnitType = UnitTypes.dagger; + try { + Field field = UnitTypes.class.getDeclaredField(args[1]); + desiredUnitType = (UnitType) field.get(null); + } catch (NoSuchFieldException | IllegalAccessException ignored) { + ctx.sendEmbed(false, ":robot: that unit doesn't exist"); + return; + } + Player player = findPlayer(args[0]); + if(player != null){ + UnitType finalDesiredUnitType = desiredUnitType; + IntStream.range(0, amt).forEach(i -> { + Unit unit = finalDesiredUnitType.create(player.team()); + unit.set(player.getX(), player.getY()); + unit.add(); + }); + ctx.sendEmbed(true, ":robot: spawned " + amt + " " + finalDesiredUnitType + "s at " + escapeColorCodes(player.name) + "'s position"); + }else{ + ctx.sendEmbed(false, ":robot: can't find " + escapeCharacters(args[0])); + } + }); + + handler.register("kill", "", "Kill the specified player or all specified units on the map.", (args, ctx) -> { + UnitType desiredUnitType; + try { + Field field = UnitTypes.class.getDeclaredField(args[0]); + desiredUnitType = (UnitType) field.get(null); + int amt = 0; + for(Unit unit : Groups.unit){ + if(unit.type == desiredUnitType){ unit.kill(); amt++; } + } + ctx.sendEmbed(true, ":knife: killed " + amt + " " + args[0] + "s"); + } catch (NoSuchFieldException | IllegalAccessException ignored) { + Player player = findPlayer(args[0]); + if(player != null){ + player.unit().kill(); + ctx.sendEmbed(true, ":knife: killed " + escapeCharacters(player.name)); + }else if(args[0].toLowerCase().equals("all")){ + Groups.player.forEach(p -> p.unit().kill()); + ctx.sendEmbed(true, ":knife: killed everyone, muhahaha"); + }else{ + ctx.sendEmbed(false, ":knife: can't find " + escapeCharacters(args[0])); + } + } + }); + + handler.register("weapon", " [damage] [lifetime] [velocity]", "Modify the specified players weapon with the provided parameters", (args, ctx) -> { + BulletType desiredBulletType; + float dmg = 1f; + float life = 1f; + float vel = 1f; + if(args.length > 2){ + try{ + dmg = Float.parseFloat(args[2]); + }catch (Exception e){ ctx.sendEmbed(false, ":gun: error parsing damage number"); return;} + } + if(args.length > 3){ + try{ + life = Float.parseFloat(args[3]); + }catch (Exception e){ ctx.sendEmbed(false, ":gun: error parsing lifetime number"); return;} + } + if(args.length > 4){ + try{ + vel = Float.parseFloat(args[4]); + }catch (Exception e){ ctx.sendEmbed(false, ":gun: error parsing velocity number"); return;} + } + try { + Field field = Bullets.class.getDeclaredField(args[1]); + desiredBulletType = (BulletType) field.get(null); + } catch (NoSuchFieldException | IllegalAccessException ignored) { + ctx.sendEmbed(false, ":gun: invalid bullet type"); + desiredBulletType = null; + } + HashMap fields = new HashMap<>(); + Player player = findPlayer(args[0]); + + if(player != null){ + PlayerData pd = playerDataHashMap.get(player.uuid()); + pd.bt = desiredBulletType; + pd.sclDamage = dmg; + pd.sclLifetime = life; + pd.sclVelocity = vel; + playerDataHashMap.put(player.uuid(), pd); + fields.put("Bullet", args[1]); + fields.put("Bullet lifetime", args[2]); + fields.put("Bullet velocity", args[3]); + ctx.sendEmbed(true, ":gun: modded " + escapeCharacters(player.name) + "'s gun", fields, true); + }else if(args[0].toLowerCase().equals("all")){ + for(Player p : Groups.player) { + PlayerData pd = playerDataHashMap.get(p.uuid()); + pd.bt = desiredBulletType; + pd.sclDamage = dmg; + pd.sclLifetime = life; + pd.sclVelocity = vel; + playerDataHashMap.put(p.uuid(), pd); + } + fields.put("Bullet", args[1]); + fields.put("Bullet lifetime", args[2]); + fields.put("Bullet velocity", args[3]); + ctx.sendEmbed(true, ":gun: modded everyone's gun", fields, true); + }else{ + ctx.sendEmbed(false, ":gun: can't find " + escapeCharacters(args[0])); + } + }); + } +} \ No newline at end of file diff --git a/PublicCommands.java b/PublicCommands.java new file mode 100644 index 0000000..42dd2e0 --- /dev/null +++ b/PublicCommands.java @@ -0,0 +1,205 @@ +package mindustry.plugin.commands; + +import arc.files.Fi; +import arc.struct.Seq; +import arc.util.CommandHandler; +import mindustry.gen.Groups; +import mindustry.gen.Player; +import mindustry.io.SaveIO; +import mindustry.maps.Map; + +import mindustry.gen.Call; +import mindustry.plugin.datas.ContentHandler; +import mindustry.plugin.discord.Context; +import net.dv8tion.jda.api.EmbedBuilder; + +import javax.imageio.ImageIO; +import java.io.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.zip.InflaterInputStream; + +import static arc.util.CommandHandler.*; +import static mindustry.Vars.*; +import static mindustry.plugin.utils.Funcs.*; +import static mindustry.plugin.discord.Loader.*; +import static net.dv8tion.jda.api.entities.Message.*; + +public class PublicCommands { + public void registerCommands(CommandHandler handler) { + handler.register("chat", "", "Send a message to in-game chat in " + serverName, (args, ctx) -> { + if(args[0].length() < chatMessageMaxSize){ + Call.sendMessage("[sky]" + ctx.author.getAsTag() + " @discord >[] " + args[0]); + ctx.sendEmbed(true, ":mailbox_with_mail: **message sent!**", "``" + escapeCharacters(args[0]) + "``"); + } else{ + ctx.sendEmbed(false, ":exclamation: **message too big!**", "maximum size: **" + chatMessageMaxSize + " characters**"); + } + }); + + handler.register("maps", "Displays all available maps in the playlist. Use " + prefix + "map to download a specific map.", (args, ctx) -> { + Seq mapList = maps.customMaps(); + StringBuilder smallMaps = new StringBuilder(); + StringBuilder mediumMaps = new StringBuilder(); + StringBuilder bigMaps = new StringBuilder(); + + for(Map map : mapList){ + int size = map.height * map.width; + if(size <= 62500) { smallMaps.append("**").append(escapeCharacters(map.name())).append("** ").append(map.width).append("x").append(map.height).append("\n"); } + if(size > 62500 && size < 160000) { mediumMaps.append("**").append(escapeCharacters(map.name())).append("** ").append(map.width).append("x").append(map.height).append("\n"); } + if(size >= 160000) { bigMaps.append("**").append(escapeCharacters(map.name())).append("** ").append(map.width).append("x").append(map.height).append("\n"); } + } + HashMap fields = new HashMap<>(); + if(smallMaps.length() > 0){fields.put("small maps", smallMaps.toString()); } + if(mediumMaps.length() > 0){fields.put("medium maps", mediumMaps.toString()); } + if(bigMaps.length() > 0){fields.put("big maps", bigMaps.toString()); } + + ctx.sendEmbed(true,":map: **" + mapList.size + " maps** in " + serverName + "'s playlist", fields, true); + }); + + handler.register("map","", "Previews and provides a download for the specified map. (check maps with " + prefix + "maps)", (args, ctx) -> { + Map map = getMapBySelector(args[0].trim()); + if (map != null){ + try { + Fi mapFile = map.file; + + ContentHandler.Map visualMap = contentHandler.readMap(map.file.read()); + File imageFile = new File(assets + "image_" + mapFile.name().replaceAll(".msav", ".png")); + ImageIO.write(visualMap.image, "png", imageFile); + + + EmbedBuilder eb = new EmbedBuilder().setColor(Pals.success).setTitle(":map: " + escapeCharacters(map.name())).setFooter(map.width + "x" + map.height).setDescription(escapeCharacters(map.description())).setAuthor(escapeCharacters(map.author())); + eb.setImage("attachment://" + imageFile.getName()); + ctx.channel.sendFile(mapFile.file()).addFile(imageFile).embed(eb.build()).queue(); + //ctx.channel.sendFile(mapFile.file()).embed(eb.build()).queue(); + } catch (Exception e) { + ctx.sendEmbed(false, ":eyes: **internal server error**"); + e.printStackTrace(); + } + }else{ + ctx.sendEmbed(false, ":mag: map **" + escapeCharacters(args[0]) + "** not found"); + } + }); + + handler.register("submitmap", "Submit a map to be added to the server playlist (will be reviewed by a moderator automatically). Must attach a valid .msav file.", (args, ctx) -> { + Attachment attachment = (ctx.event.getMessage().getAttachments().size() == 1 ? ctx.event.getMessage().getAttachments().get(0) : null); + if (attachment == null) { + ctx.sendEmbed(false, ":link: **you need to attach a valid .msav file!**"); + return; + } + File mapFile = new File(assets + attachment.getFileName()); + attachment.downloadToFile(mapFile).thenAccept(file -> { + Fi fi = new Fi(mapFile); + byte[] bytes = fi.readBytes(); + + DataInputStream dis = new DataInputStream(new InflaterInputStream(new ByteArrayInputStream(bytes))); + if (attachment.getFileName().endsWith(".msav") && SaveIO.isSaveValid(dis)) { + try { + + OutputStream os = new FileOutputStream(mapFile); + os.write(bytes); + os.close(); + + ContentHandler.Map map = contentHandler.readMap(fi.read()); + File imageFile = new File(assets + "image_" + attachment.getFileName().replaceAll(".msav", ".png")); + ImageIO.write(map.image, "png", imageFile); + + + EmbedBuilder eb = new EmbedBuilder(); + eb.setColor(Pals.progress); + eb.setTitle(escapeCharacters(map.name)); + eb.setDescription(map.description); + eb.setAuthor(ctx.author.getAsTag(), null, ctx.author.getAvatarUrl()); + eb.setFooter("react to this message accordingly to approve/disapprove this map."); + eb.setImage("attachment://" + imageFile.getName()); + + mapSubmissions.sendFile(mapFile).addFile(imageFile).embed(eb.build()).queue(message -> { + message.addReaction("YES:735555385934741554").queue(); + message.addReaction("NO:735554784534462475").queue(); + }); + + ctx.sendEmbed(true, ":map: **" + escapeCharacters(map.name) + "** submitted successfully!", "a moderator will soon approve or disapprove your map."); + } catch (Exception e) { + e.printStackTrace(); + ctx.sendEmbed(false, ":interrobang: **attachment invalid or corrupted!**"); + } + } else { + ctx.sendEmbed(false, ":interrobang: **attachment invalid or corrupted!**"); + } + }); + }); + + handler.register("players","Get all online in-game players.", (args, ctx) -> { + EmbedBuilder eb = new EmbedBuilder(); + eb.setColor(Pals.progress); + eb.setTitle(":satellite: **players online: **" + Groups.player.size()); + + StringBuilder s = new StringBuilder(); + int pn = 1; + for(Player p : Groups.player){ + s + .append("**") + .append(pn + "•") + .append("** `") + .append(escapeCharacters(p.name)) + .append("`") + .append(" : ") + .append(p.id) + .append("\n"); + pn++; + } + eb.setDescription(s); + ctx.sendEmbed(eb); + }); + + handler.register("status", "View the status of this server.", (args, ctx) -> { + HashMap fields = new HashMap<>(); + fields.put("players", String.valueOf(Groups.player.size())); + fields.put("map", escapeCharacters(state.map.name())); + fields.put("wave", String.valueOf(state.wave)); + + ctx.sendEmbed(true, ":desktop: **" + serverName + "**", fields, false); + }); + + handler.register("help", "[command]", "Display help for a specified command, or all commands.", (args, ctx) -> { + EmbedBuilder eb = new EmbedBuilder(); + eb.setTitle(":newspaper: all available commands"); + eb.setDescription("use help [command] to view more information about a command."); + eb.setColor(Pals.progress); + if(args.length <= 0) { + StringBuilder admin = new StringBuilder(); + StringBuilder mod = new StringBuilder(); + StringBuilder reviewer = new StringBuilder(); + StringBuilder publics = new StringBuilder(); + for (Command cmd : bt.moderatorHandler.getCommandList()) { + mod.append("**").append(cmd.text).append("**").append(" ").append(cmd.paramText).append("\n"); + } + for (Command cmd : bt.reviewerHandler.getCommandList()) { + reviewer.append("**").append(cmd.text).append("**").append(" ").append(cmd.paramText).append("\n"); + } + for (Command cmd : bt.publicHandler.getCommandList()) { + publics.append("**").append(cmd.text).append("**").append(" ").append(cmd.paramText).append("\n"); + } + eb.addField("Moderation", mod.toString(), true); + eb.addField("Maps", reviewer.toString(), true); + eb.addField("Public", publics.toString(), true); + ctx.channel.sendMessage(eb.build()).queue(); + }else{ + Command cmd = null; + for(Command c : bt.moderatorHandler.getCommandList()){ + if(c.text.equals(args[0].toLowerCase())) cmd = c; + } + for(Command c : bt.reviewerHandler.getCommandList()){ + if(c.text.equals(args[0].toLowerCase())) cmd = c; + } + for(Command c : bt.publicHandler.getCommandList()){ + if(c.text.equals(args[0].toLowerCase())) cmd = c; + } + if(cmd != null){ + ctx.sendEmbed(true, ":gear: " + cmd.text + (cmd.paramText.length() > 0 ? " *" + cmd.paramText + "*" : ""), cmd.description); + }else{ + ctx.sendEmbed(false, ":interrobang: that command doesn't exist!"); + } + } + }); + } +} diff --git a/ioMain.java b/ioMain.java new file mode 100644 index 0000000..e04df0a --- /dev/null +++ b/ioMain.java @@ -0,0 +1,343 @@ +package mindustry.plugin; + +import java.time.Instant; +import java.util.HashMap; +import java.util.concurrent.CompletableFuture; + +import arc.Core; +import arc.math.Mathf; +import arc.util.*; +import arc.util.Timer; +import mindustry.content.*; +import mindustry.gen.Groups; +import mindustry.gen.Player; +import mindustry.graphics.Pal; +import mindustry.mod.Plugin; +import mindustry.net.Administration; +import mindustry.plugin.datas.ContentHandler; +import mindustry.plugin.datas.PlayerData; +import mindustry.plugin.datas.TileInfo; +import mindustry.plugin.discord.Loader; +import mindustry.plugin.utils.Funcs; +import mindustry.plugin.utils.MapRules; +import mindustry.plugin.utils.VoteSession; +import mindustry.world.Tile; + +import arc.Events; +import mindustry.Vars; +import mindustry.game.EventType; +import mindustry.gen.Call; + +import static mindustry.Vars.*; +import static mindustry.plugin.utils.Funcs.*; +import static mindustry.plugin.discord.Loader.*; + +public class ioMain extends Plugin { + public static HashMap playerDataHashMap = new HashMap<>(); + public static int minutesPassed = 0; + public static HashMap tileInfoHashMap = new HashMap<>(); + //register event handlers and create variables in the constructor + public ioMain() { + //we can load this before anything else, it doesnt matter + Loader.load(); + + // display on screen messages + float duration = 10f; + int start = 450; + int increment = 30; + + Timer.schedule(() -> { + int currentInc = 0; + for(String msg : onScreenMessages){ + Call.infoPopup(msg, duration, 20, 50, 20, start + currentInc, 0); + currentInc = currentInc + increment; + } + }, 0, 10); + + Events.on(EventType.ServerLoadEvent.class, event -> { + contentHandler = new ContentHandler(); + Log.info("Everything's loaded !"); + }); + + Events.on(EventType.TapEvent.class, tapEvent -> { + if(tapEvent.tile != null) { + Player player = tapEvent.player; + PlayerData pd = playerDataHashMap.get(player.uuid()); + + Tile t = tapEvent.tile; + pd.tapTile = t; + if (pd.inspector) { + Call.effect(player.con, Fx.placeBlock, t.worldx(), t.worldy(), 0.75f, Pal.accent); + player.sendMessage("[orange]--[] [accent]tile [](" + t.x + ", " + t.y + ")[accent] block:[] " + ((t.block() == null || t.block() == Blocks.air) ? "[#545454]none" : t.block().name) + " [orange]--[]"); + TileInfo info = tileInfoHashMap.getOrDefault(t, new TileInfo()); + if (info.placedBy != null) { + String pBy = (player.admin() ? info.placedByUUID : info.placedBy); + player.sendMessage("[accent]last placed by:[] " + escapeColorCodes(pBy)); + } + if (info.destroyedBy != null) { + String dBy = (player.admin() ? info.destroyedByUUID : info.destroyedBy); + player.sendMessage("[accent]last [scarlet]deconstructed[] by:[] " + escapeColorCodes(dBy)); + } + if (t.block() == Blocks.air && info.wasHere != null){ + player.sendMessage("[accent]block that was here:[] " + info.wasHere); + } + if (info.configuredBy != null) { + String cBy = (player.admin() ? info.configuredByUUID : info.configuredBy); + player.sendMessage("[accent]last configured by:[] " + escapeColorCodes(cBy)); + } + } + } + }); + + // player disconnected + Events.on(EventType.PlayerLeave.class, event -> { + String uuid = event.player.uuid(); + if(playerDataHashMap.containsKey(uuid)) + setJedisData(uuid, playerDataHashMap.get(uuid)); + + //free ram + playerDataHashMap.remove(uuid); + }); + + // player joined + Events.on(EventType.PlayerJoin.class, event -> { + CompletableFuture.runAsync(() -> { + Player player = event.player; + PlayerData pd = getJedisData(player.uuid()); + if(pd == null && playerDataHashMap.containsKey(player.uuid())){ + pd = playerDataHashMap.get(player.uuid()); + } + + if (pd != null) { + if (pd.bannedUntil > Instant.now().getEpochSecond()) { + player.con.kick("[scarlet]You are banned.[accent] Reason:\n" + pd.banReason); + return; + } + if(pd.rank > 0){ + pd.tag = rankNames.get(pd.rank).tag; + // todo: figure out a non-intrusive way to do this + // player.name(pd.tag + " " + player.name); + } + } else { // not in database + pd = new PlayerData(0); + setJedisData(player.uuid(), new PlayerData(0)); + } + playerDataHashMap.put(player.uuid(), pd); + + if (welcomeMessage.length() > 0 && !welcomeMessage.equals("none")) { + Call.infoMessage(player.con, formatMessage(player, welcomeMessage)); + } + + if (pd != null){ + if (pd.bannedUntil > Instant.now().getEpochSecond()){ + for (int i=0; i < 30; i++) + Call.infoMessage(player.con, formatMessage(player, welcomeMessage)); + + player.con.kick("[scarlet]You are banned.[accent] Reason:\n" + pd.banReason, 0); + } + } + + if(bannedNames.contains(player.name.trim().toLowerCase())) + player.con.kick("Influx Capacitor failed. Quantom leap needs to be restarted."); + }); + }); + + + Events.on(EventType.BuildSelectEvent.class, event -> { + if(event.builder instanceof Player){ + if(event.tile != null){ + Player player = (Player) event.builder; + PlayerData pd = playerDataHashMap.get(player.uuid()); + + TileInfo info = tileInfoHashMap.getOrDefault(event.tile, new TileInfo()); + if(!event.breaking){ + info.placedBy = player.name; + info.placedByUUID = player.uuid(); + info.wasHere = (event.tile.block() != Blocks.air ? event.tile.block().name : "[#545454]none"); + + pd.buildingsBuilt++; + playerDataHashMap.put(player.uuid(), pd); + } else{ + info.destroyedBy = player.name; + info.destroyedByUUID = player.uuid(); + } + tileInfoHashMap.put(event.tile, info); + } + } + }); + + Events.on(EventType.TapEvent.class, event -> { + if(event.tile != null & event.player != null){ + TileInfo info = tileInfoHashMap.getOrDefault(event.tile, new TileInfo()); + Player player = event.player; + info.configuredBy = player.name; + info.configuredByUUID = player.uuid(); + tileInfoHashMap.put(event.tile, info); + } + }); + + Events.on(EventType.WorldLoadEvent.class, event -> { + Timer.schedule(MapRules::run, 1); // idk + }); + + Events.on(EventType.ServerLoadEvent.class, event -> { + // action filter + Vars.netServer.admins.addActionFilter(action -> { + Player player = action.player; + PlayerData pd = playerDataHashMap.get(player.uuid()); + + if (player == null) return true; + + if (player.admin()) return true; + if (!pd.canInteract) return false; + + return action.type != Administration.ActionType.rotate; + }); + }); + + Events.on(EventType.Trigger.update.getClass(), event -> { + for(Player p : Groups.player){ + PlayerData pd = playerDataHashMap.get(p.uuid()); + if (pd != null && pd.bt != null && p.shooting()) { + Call.createBullet(pd.bt, p.team(), p.getX(), p.getY(), p.unit().rotation, pd.sclDamage, pd.sclVelocity, pd.sclLifetime); + } + } + }); + + Events.on(EventType.GameOverEvent.class, event -> { + for(Player p : Groups.player){ + PlayerData pd = playerDataHashMap.get(p.uuid()); + pd.gamesPlayed++; + playerDataHashMap.put(p.uuid(), pd); + } + }); + } + + //register commands that run on the server + @Override + public void registerServerCommands(CommandHandler handler){ + handler.removeCommand("exit"); + handler.register("exit", "exits the server", ioMain::exit); + } + + //cooldown between map votes + int voteCooldown = 120 * 1; + + //register commands that player can invoke in-game + @Override + public void registerClientCommands(CommandHandler handler){ + if (api != null) { + + handler.register("inspector", "Toggle on tile inspector. (Grief detection)", (args, player) -> { + PlayerData pd = playerDataHashMap.get(player.uuid()); + pd.inspector = !pd.inspector; + player.sendMessage("[accent]Tile inspector " + (pd.inspector ? "enabled" : "disabled") + "."); + }); + + handler.register("stats", "[player...]", "Display stats of the specified player (or yourself, if no player provided)", (args, player) -> { + if(!statMessage.equals("none")) { + if (args.length <= 0) { + Call.infoMessage(player.con, formatMessage(player, statMessage)); + } else { + Player p = findPlayer(args[0]); + if (p != null) { + Call.infoMessage(player.con, formatMessage(p, statMessage)); + } else { + player.sendMessage("[lightgray]Can't find that player!"); + } + } + } + }); + + handler.register("event", "Join an ongoing event (if there is one)", (args, player) -> { + if(eventIp.length() > 0){ + Call.connect(player.con, eventIp, eventPort); + } else{ + player.sendMessage("[accent]There is no ongoing event at this time."); + } + }); + + handler.register("maps","[page]", "Display all maps in the playlist.", (args, player) -> { // self info + if(args.length > 0 && !Strings.canParseInt(args[0])){ + player.sendMessage("[scarlet]'page' must be a number."); + return; + } + int perPage = 6; + int page = args.length > 0 ? Strings.parseInt(args[0]) : 1; + int pages = Mathf.ceil((float)Vars.maps.customMaps().size / perPage); + + page --; + + if(page >= pages || page < 0){ + player.sendMessage("[scarlet]'page' must be a number between[orange] 1[] and[orange] " + pages + "[scarlet]."); + return; + } + + StringBuilder result = new StringBuilder(); + result.append(Strings.format("[orange]-- Maps Page[lightgray] {0}[gray]/[lightgray]{1}[orange] --\n\n", (page+1), pages)); + + for(int i = perPage * page; i < Math.min(perPage * (page + 1), Vars.maps.customMaps().size); i++){ + mindustry.maps.Map map = Vars.maps.customMaps().get(i); + result.append("[white] - [accent]").append(escapeColorCodes(map.name())).append("\n"); + } + player.sendMessage(result.toString()); + }); + + Timekeeper vtime = new Timekeeper(voteCooldown); + VoteSession[] currentlyKicking = {null}; + + handler.register("nominate","", "[regular+] Vote to change to a specific map.", (args, player) -> { + if(!state.rules.pvp || player.admin()) { + mindustry.maps.Map found = getMapBySelector(args[0]); + if(found != null){ + if(!vtime.get()){ + player.sendMessage("[scarlet]You must wait " + voteCooldown/60 + " minutes between nominations."); + return; + } + VoteSession session = new VoteSession(currentlyKicking, found); + + session.vote(player, 1); + vtime.reset(); + currentlyKicking[0] = session; + }else{ + player.sendMessage("[scarlet]No map[orange]'" + args[0] + "'[scarlet] found."); + } + } else { + player.sendMessage("[scarlet]This command is disabled on pvp."); + } + }); + + handler.register("rtv", "Vote to change the map.", (args, player) -> { // self info + if(currentlyKicking[0] == null){ + player.sendMessage("[scarlet]No map is being voted on."); + }else{ + //hosts can vote all they want + if(player.uuid() != null && (currentlyKicking[0].voted.contains(player.uuid()) || currentlyKicking[0].voted.contains(netServer.admins.getInfo(player.uuid()).lastIP))){ + player.sendMessage("[scarlet]You've already voted."); + return; + } + + currentlyKicking[0].vote(player, 1); + } + }); + } + + } + + public static void exit(String[] uselessness){ + exit(); + } + + public static void exit(){ + if(playerDataHashMap != null){ + playerDataHashMap.forEach(Funcs::setJedisData); + } + if(api != null){ + api.shutdownNow(); + } + Vars.net.dispose(); + Core.app.exit(); + System.exit(0); + } + +} \ No newline at end of file diff --git a/mod.json b/mod.json new file mode 100644 index 0000000..fe9ef7a --- /dev/null +++ b/mod.json @@ -0,0 +1,7 @@ +{ + "name": "ioPlugin", + "author": "Various, see https://github.com/fuzzbuck/mindustry.io-plugin/blob/master/CONTRIBUTORS.MD", + "main": "mindustry.plugin.ioMain", + "description": "Allows interaction with discord & vice versa, makes moderation easier.", + "version": 1.61 +}