diff --git a/src/de/steamwar/bungeecore/BungeeCore.java b/src/de/steamwar/bungeecore/BungeeCore.java index ec81983..5307714 100644 --- a/src/de/steamwar/bungeecore/BungeeCore.java +++ b/src/de/steamwar/bungeecore/BungeeCore.java @@ -28,6 +28,7 @@ import de.steamwar.bungeecore.listeners.ping.PingListener; import de.steamwar.bungeecore.network.BungeeNetworkHandler; import de.steamwar.bungeecore.network.NetworkReceiver; import de.steamwar.bungeecore.network.SWScriptSyntaxForwarder; +import de.steamwar.bungeecore.util.SchematicSearch; import de.steamwar.sql.Punishment; import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.UserElo; @@ -122,6 +123,7 @@ public class BungeeCore extends Plugin { new Fabric(); new SubserverProtocolFixer(); new PingListener(); + new SchematicSearchListener(); local = new Node.LocalNode(); if(MAIN_SERVER) { @@ -233,6 +235,7 @@ public class BungeeCore extends Plugin { tablistManager.disable(); errorLogger.unregister(); Statement.closeAll(); + SchematicSearch.abortAll(); } public static BungeeCore get() { diff --git a/src/de/steamwar/bungeecore/commands/StatCommand.java b/src/de/steamwar/bungeecore/commands/StatCommand.java index 33c3653..71a72b2 100644 --- a/src/de/steamwar/bungeecore/commands/StatCommand.java +++ b/src/de/steamwar/bungeecore/commands/StatCommand.java @@ -21,6 +21,7 @@ package de.steamwar.bungeecore.commands; import de.steamwar.bungeecore.Message; import de.steamwar.bungeecore.Node; +import de.steamwar.bungeecore.util.SchematicSearch; import de.steamwar.command.SWCommand; import net.md_5.bungee.api.CommandSender; @@ -37,6 +38,7 @@ public class StatCommand extends SWCommand { @Register public void genericCommand(CommandSender sender) { + Message.send("STAT_SEARCH_QUEUE", sender, SchematicSearch.getQueueSize()); Map serverCount = new HashMap<>(); try { Process process = new ProcessBuilder("ps", "x").start(); diff --git a/src/de/steamwar/bungeecore/listeners/SchematicSearchListener.java b/src/de/steamwar/bungeecore/listeners/SchematicSearchListener.java new file mode 100644 index 0000000..65da859 --- /dev/null +++ b/src/de/steamwar/bungeecore/listeners/SchematicSearchListener.java @@ -0,0 +1,41 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2023 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.bungeecore.listeners; + +import de.steamwar.bungeecore.Message; +import de.steamwar.bungeecore.util.SchematicSearch; +import net.md_5.bungee.api.event.PlayerDisconnectEvent; +import net.md_5.bungee.api.event.ServerSwitchEvent; +import net.md_5.bungee.event.EventHandler; + +public class SchematicSearchListener extends BasicListener { + + @EventHandler + public void onServerSwitch(ServerSwitchEvent event) { + if(SchematicSearch.removeFromQueue(event.getPlayer())) { + Message.send("SCHEMATIC_SEARCH_REMOVED_FROM_QUEUE", event.getPlayer()); + } + } + + @EventHandler + public void onQuit(PlayerDisconnectEvent event) { + SchematicSearch.removeFromQueue(event.getPlayer()); + } +} diff --git a/src/de/steamwar/bungeecore/network/BungeeNetworkHandler.java b/src/de/steamwar/bungeecore/network/BungeeNetworkHandler.java index 1cc4854..ab7a2b3 100644 --- a/src/de/steamwar/bungeecore/network/BungeeNetworkHandler.java +++ b/src/de/steamwar/bungeecore/network/BungeeNetworkHandler.java @@ -31,5 +31,6 @@ public class BungeeNetworkHandler { new ImALobbyHandler().register(); new InventoryCallbackHandler().register(); new PrepareSchemHandler().register(); + new SchematicSearchRequestHandler().register(); } } diff --git a/src/de/steamwar/bungeecore/network/handlers/SchematicSearchRequestHandler.java b/src/de/steamwar/bungeecore/network/handlers/SchematicSearchRequestHandler.java new file mode 100644 index 0000000..45f6834 --- /dev/null +++ b/src/de/steamwar/bungeecore/network/handlers/SchematicSearchRequestHandler.java @@ -0,0 +1,47 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2023 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.bungeecore.network.handlers; + +import de.steamwar.bungeecore.BungeeCore; +import de.steamwar.bungeecore.util.SchematicSearch; +import de.steamwar.network.packets.PacketHandler; +import de.steamwar.network.packets.client.RequestSchematicSearchPacket; +import de.steamwar.sql.SchematicNode; +import de.steamwar.sql.SteamwarUser; +import net.md_5.bungee.api.connection.ProxiedPlayer; + +public class SchematicSearchRequestHandler extends PacketHandler { + + @Handler + public void handle(RequestSchematicSearchPacket packet) { + SteamwarUser user = SteamwarUser.get(packet.getPlayerId()); + SchematicNode node = SchematicNode.getSchematicNode(packet.getSchematicId()); + ProxiedPlayer proxiedPlayer = BungeeCore.get().getProxy().getPlayer(user.getUUID()); + if(proxiedPlayer == null) { + return; + } + SchematicSearch.SchematicSearchBehavior behavior = SchematicSearch.SchematicSearchBehavior.builder() + .airAsAny(packet.isAirAsAny()) + .ignoreAir(packet.isIgnoreAir()) + .ignoreBlockData(packet.isIgnoreBlockData()) + .build(); + SchematicSearch.queueSearch(proxiedPlayer, node, behavior); + } +} diff --git a/src/de/steamwar/bungeecore/util/SchematicSearch.java b/src/de/steamwar/bungeecore/util/SchematicSearch.java new file mode 100644 index 0000000..6d189ea --- /dev/null +++ b/src/de/steamwar/bungeecore/util/SchematicSearch.java @@ -0,0 +1,307 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2023 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.bungeecore.util; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import de.steamwar.bungeecore.BungeeCore; +import de.steamwar.bungeecore.Message; +import de.steamwar.sql.NodeData; +import de.steamwar.sql.SchematicNode; +import de.steamwar.sql.SteamwarUser; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import net.md_5.bungee.api.ChatMessageType; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.scheduler.ScheduledTask; +import org.apache.commons.lang3.time.DurationFormatUtils; + +import java.io.*; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.*; +import java.util.stream.Collectors; +import java.util.zip.GZIPOutputStream; + +public class SchematicSearch { + private static final String SEARCH_BINARY = "/home/chaoscaot/schemsearch/target/release/schemsearch-cli"; + private static final LinkedBlockingQueue searchQueue = new LinkedBlockingQueue<>(); + private static SchematicSearch currentSearch; + + private static void startNext() { + if(currentSearch != null) { + return; + } + + synchronized (searchQueue) { + if(searchQueue.isEmpty()) { + return; + } + currentSearch = searchQueue.poll(); + currentSearch.start(); + } + } + + public static void queueSearch(ProxiedPlayer player, SchematicNode node, SchematicSearchBehavior behavior) { + synchronized (searchQueue) { + searchQueue.add(new SchematicSearch(player, node, behavior)); + startNext(); + if (!searchQueue.isEmpty()) { + Message.send("SCHEMATIC_SEARCH_QUEUED", player); + } + } + } + + public static void abortAll() { + synchronized (searchQueue) { + searchQueue.stream().map(schematicSearch -> schematicSearch.player).collect(Collectors.toSet()).forEach(player -> { + Message.send("SCHEMATIC_SEARCH_REMOVED_FROM_QUEUE_SOFTRELOAD", player); + }); + searchQueue.clear(); + } + if(currentSearch != null) { + Message.send("SCHEMATIC_SEARCH_CANCELED_SOFTRELOAD", currentSearch.player); + currentSearch.end(); + } + } + + public static int getQueueSize() { + synchronized (searchQueue) { + return searchQueue.size(); + } + } + + private static List constructArguments(SteamwarUser user, File pattern, SchematicSearchBehavior behavior) { + List args = new ArrayList<>(Arrays.asList(SEARCH_BINARY, "-T", "2", "-s", "-u", String.valueOf(user.getId()), "-o", "json:std", "-m", "50")); + if(behavior.isAirAsAny()) { + args.add("-A"); + } + if(behavior.isIgnoreAir()) { + args.add("-a"); + } + if(behavior.isIgnoreBlockData()) { + args.add("-d"); + } + args.add(pattern.getAbsolutePath()); + return args; + } + + public static boolean removeFromQueue(ProxiedPlayer player) { + boolean removed; + synchronized (searchQueue) { + removed = searchQueue.removeIf(search -> search.player.equals(player)); + } + + if (currentSearch != null && currentSearch.player.equals(player)) { + currentSearch.end(); + removed = true; + } + + return removed; + } + + private static File schematicNodeToTempFile(NodeData node) { + try { + File f = File.createTempFile("schemsearch", ".schem"); + f.deleteOnExit(); + OutputStream os = new GZIPOutputStream(Files.newOutputStream(f.toPath())); + InputStream is = node.schemData(); + + byte[] buffer = new byte[1024]; + int read; + while((read = is.read(buffer)) != -1) { + os.write(buffer, 0, read); + } + + os.close(); + is.close(); + return f; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private final ProxiedPlayer player; + private final SchematicNode node; + private final SchematicSearchBehavior behavior; + private File pattern; + private ScheduledTask task; + private Process process; + + private SchematicSearch(ProxiedPlayer player, SchematicNode node, SchematicSearchBehavior behavior) { + this.player = player; + this.node = node; + this.behavior = behavior; + } + + private static String readInputStream(InputStream is) throws IOException { + StringBuilder sb = new StringBuilder(); + byte[] buffer = new byte[1024]; + long total = 0; + while (is.available() > 0) { + int read = is.read(buffer); + total += read; + if (read > 0) { + sb.append(new String(buffer, 0, read)); + } + if (total > 1024 * 128) { + break; + } + } + return sb.toString(); + } + + private void start() { + task = BungeeCore.get().getProxy().getScheduler().runAsync(BungeeCore.get(), () -> { + try { + NodeData data = NodeData.get(node); + if (!data.getNodeFormat()) { + Message.send("SCHEMATIC_SEARCH_NOT_SUPPORTED", player); + end(); + return; + } + Message.send("SCHEMATIC_SEARCH_STARTED", player, node.getName()); + pattern = schematicNodeToTempFile(data); + ProcessBuilder builder = new ProcessBuilder(constructArguments(SteamwarUser.get(player.getUniqueId()), pattern, behavior)); + process = builder.start(); + InputStream stdout = process.getInputStream(); + InputStream stderr = process.getErrorStream(); + + String bar = ""; + while (!process.waitFor(200, TimeUnit.MILLISECONDS)) { + String s = readInputStream(stderr); + if (s.length() > 0) { + if (s.contains("s[")) { + s = s.substring(s.lastIndexOf("s[") + 1); + } + s = s.replace("█", "§e█§7"); + bar = s; + } + BungeeCore.send(player, ChatMessageType.ACTION_BAR, "§7" + bar); + } + String s = readInputStream(stderr); + if (s.contains("s[")) { + s = s.substring(s.lastIndexOf("s[") + 1); + } + BungeeCore.send(player, ChatMessageType.ACTION_BAR, "§7" + s.replace("█", "§e█§7")); + s = readInputStream(stdout); + String[] outputs = s.split("\n"); + + List elements = Arrays.stream(outputs).map(JsonParser::parseString).map(JsonElement::getAsJsonObject).collect(Collectors.toList()); + + int searchCount = 0; + long searchTime = 0; + List matches = new ArrayList<>(); + + for (JsonObject element : elements) { + switch (element.get("event").getAsString()) { + case "Init": + searchCount = element.get("total").getAsInt(); + break; + case "Found": + matches.add(new Match(element.get("x").getAsInt(), element.get("y").getAsInt(), element.get("z").getAsInt(), element.get("percent").getAsFloat() * 100, element.get("name").getAsString())); + break; + case "End": + searchTime = element.get("end_time").getAsLong(); + break; + default: + break; + } + } + + Message.send("SCHEMATIC_SEARCH_RESULT_HEADER", player, searchCount, DurationFormatUtils.formatDuration(searchTime, "' 'm'm 's's 'S'ms'") + .replace(" 0ms", "") + .replace(" 0s", "") + .replace(" 0m", "")); + + if (matches.size() <= 1) { + Message.send("SCHEMATIC_SEARCH_NO_RESULTS", player); + end(); + return; + } + + SteamwarUser user = SteamwarUser.get(player.getUniqueId()); + for (Match match : matches) { + String[] nameSplit = match.name.split(" "); + String name = nameSplit[0]; + int id = Integer.parseInt(nameSplit[1].substring(1, nameSplit[1].length() - 1)); + if (id == node.getId()) continue; + Message.sendPrefixless("SCHEMATIC_SEARCH_RESULT", player, Message.parse("SCHEMATIC_SEARCH_RESULT_HOVER", player, name), + new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/schem info " + SchematicNode.byIdAndUser(user, id).generateBreadcrumbs()), + name, match.percent, match.x + 1, match.y + 1, match.z + 1); + } + + if (matches.size() >= 49) { + Message.send("SCHEMATIC_SEARCH_TOO_MANY_RESULTS", player); + } + } catch (Exception e) { + throw new SecurityException(e); + } finally { + end(); + } + }); + } + + public void end() { + if(process != null && process.isAlive()) { + process.destroy(); + } + + if(pattern != null) { + pattern.delete(); + } + + if(task != null) { + task.cancel(); + } + + synchronized (searchQueue) { + if(currentSearch == this) { + currentSearch = null; + } + } + startNext(); + } + + @AllArgsConstructor + private static class Match { + int x; + int y; + int z; + float percent; + String name; + } + + @Builder + @Getter + public static class SchematicSearchBehavior { + @Builder.Default + boolean airAsAny = false; + @Builder.Default + boolean ignoreAir = false; + @Builder.Default + boolean ignoreBlockData = false; + } +} diff --git a/src/de/steamwar/messages/BungeeCore.properties b/src/de/steamwar/messages/BungeeCore.properties index d8bb2fd..4bc9401 100644 --- a/src/de/steamwar/messages/BungeeCore.properties +++ b/src/de/steamwar/messages/BungeeCore.properties @@ -121,6 +121,7 @@ MOD_USE_MODSENDER=§cPlease use the §c§lFabricModSender§c (https://steamwar.d #Various commands ALERT=§f{0} STAT_SERVER=§7Server §e{0}§8: §7Below limit §e{1} §7Server count §e{2} +STAT_SEARCH_QUEUE=§7Schematic search queue: §e{0} #Ban&Mute-Command PUNISHMENT_USAGE=§8/§7{0} §8[§eplayer§8] [§edd§8.§emm§8.§eyyyy §7or §edd§8.§emm§8.§eyyyy§8_§ehh§8:§emm §7or §enumber§8[§eh§7our|§ed§7ay|§ew§7eek|§em§7onth|§ey§7ear§8] §7or §eperma§8] [§ereason§8] @@ -690,3 +691,16 @@ MOD_FORBIDDEN=§eForbidden MOD_AUTOBAN=§cAutoban MOD_YT=§5YT Only MOD_ITEM_BACK=§7Back + +#Schematic Search +SCHEMATIC_SEARCH_QUEUED=§7Your search has been queued and will be executed shortly. +SCHEMATIC_SEARCH_STARTED=§7Your search for "§e{0}§7" has started. +SCHEMATIC_SEARCH_NO_RESULTS=§cNo results found. +SCHEMATIC_SEARCH_RESULT_HEADER=§7Searched §e{0} §7schematics in§e{1}. +SCHEMATIC_SEARCH_RESULT=§7{0}: §e{1}§7% §8(§e{2}§7,§e{3},§e{4}§8) +SCHEMATIC_SEARCH_RESULT_HOVER=§7Click for more info about {0}. +SCHEMATIC_SEARCH_NOT_SUPPORTED=§cThis schematic is not supported because it is on an old format. +SCHEMATIC_SEARCH_REMOVED_FROM_QUEUE=§cYour search has been removed from the queue because you switched servers. +SCHEMATIC_SEARCH_REMOVED_FROM_QUEUE_SOFTRELOAD=§cYour search has been removed from the queue because of a software update. +SCHEMATIC_SEARCH_CANCELED_SOFTRELOAD=§cYour search has been cancelled because of a software update. +SCHEMATIC_SEARCH_TOO_MANY_RESULTS=§cToo many results found. Please be more specific. \ No newline at end of file diff --git a/src/de/steamwar/messages/BungeeCore_de.properties b/src/de/steamwar/messages/BungeeCore_de.properties index 18c043f..8ef29df 100644 --- a/src/de/steamwar/messages/BungeeCore_de.properties +++ b/src/de/steamwar/messages/BungeeCore_de.properties @@ -647,4 +647,16 @@ ADVENT_CALENDAR_TITLE=§eAdventskalender ADVENT_CALENDAR_DAY=§7Tag§8: §e{0} ADVENT_CALENDAR_MESSAGE=§eHast du heute schon dein Geschenk geholt? ADVENT_CALENDAR_MESSAGE_HOVER=§eKlicken zum öffnen! -ADVENT_CALENDAR_OPEN=§7Du hast §e{0}§7 aus dem Adventskalender erhalten! \ No newline at end of file +ADVENT_CALENDAR_OPEN=§7Du hast §e{0}§7 aus dem Adventskalender erhalten! + +#Schematic Search +SCHEMATIC_SEARCH_QUEUED=§7Deine Suche wurde in die Warteschlange eingereiht. +SCHEMATIC_SEARCH_STARTED=§7Deine Suche nach §e{0} §7wurde gestartet. +SCHEMATIC_SEARCH_NO_RESULTS=§cEs wurden keine Ergebnisse gefunden. +SCHEMATIC_SEARCH_RESULT_HEADER=§e{0} §7Schematics in §e{1} §7durchsucht +SCHEMATIC_SEARCH_RESULT_HOVER=§7Klicke für mehr Informationen +SCHEMATIC_SEARCH_NOT_SUPPORTED=§cDiese Schematic ist in einem alten Format und kann nicht genutzt werden. +SCHEMATIC_SEARCH_REMOVED_FROM_QUEUE=§cDeine Suche wurde aus der Warteschlange entfernt, weil du den Server gewechselt hast. +SCHEMATIC_SEARCH_REMOVED_FROM_QUEUE_SOFTRELOAD=§cWegen eines Software Updates wurde deine Suche aus der Warteschlange entfernt. +SCHEMATIC_SEARCH_CANCELED_SOFTRELOAD=§cDeine Suche wurde wegen eines Software Updates abgebrochen. +SCHEMATIC_SEARCH_TOO_MANY_RESULTS=§cEs wurden zu viele Ergebnisse gefunden. Bitte spezifiziere deine Suche. \ No newline at end of file