diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1437410 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Eclipse +.classpath +.project +.settings/ + +# Intellij +.idea/ +*.iml +*.iws + +# Mac +.DS_Store + +# Maven +log/ +target/ + +# Gradle +.gradle/ +build/ + +# Build +out/ +run/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4cbb938 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Hugo Dupanloup (Yeregorix) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..69fcb0c --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# AutoPickup +Automatic pickup for items and experience orbs. + +* Ore page: https://ore.spongepowered.org/Yeregorix/AutoPickup + +To setup your workspace run `gradle setupServer setupDecompWorkspace` +then add run configurations that points to `run\vanilla\server.jar` and `run\forge\server.jar` +and configure them to run `gradle updatePlugin` before each run. \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..b4dd5e8 --- /dev/null +++ b/build.gradle @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2018 Hugo Dupanloup (Yeregorix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +plugins { + id 'java' + id 'com.qixalite.spongestart' version '1.6.2' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +spongestart { + eula true + minecraft '1.12.2' + type 'stable' +} + +repositories { + mavenCentral() + maven { + name = 'sponge' + url = 'https://repo.spongepowered.org/maven' + } +} + +dependencies { + compile 'org.spongepowered:spongeapi:7.1.0-SNAPSHOT' +} + +task updatePlugin <<{ + copy { + from 'build/libs/AutoPickup.jar' + into spongestart.forgeServerFolder + "/mods" + } + copy { + from 'build/libs/AutoPickup.jar' + into spongestart.vanillaServerFolder + "/mods" + } +} +updatePlugin.dependsOn build \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..274f19f --- /dev/null +++ b/settings.gradle @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2018 Hugo Dupanloup (Yeregorix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +rootProject.name = 'AutoPickup' + diff --git a/src/main/java/net/smoofyuniverse/autopickup/AutoPickup.java b/src/main/java/net/smoofyuniverse/autopickup/AutoPickup.java new file mode 100644 index 0000000..e34718e --- /dev/null +++ b/src/main/java/net/smoofyuniverse/autopickup/AutoPickup.java @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2018 Hugo Dupanloup (Yeregorix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.smoofyuniverse.autopickup; + +import com.google.inject.Inject; +import net.smoofyuniverse.autopickup.bstats.MetricsLite; +import net.smoofyuniverse.autopickup.config.global.GlobalConfig; +import net.smoofyuniverse.autopickup.config.world.WorldConfig; +import net.smoofyuniverse.autopickup.event.EntityEventListener; +import net.smoofyuniverse.autopickup.event.PlayerEventListener; +import net.smoofyuniverse.autopickup.event.WorldEventListener; +import net.smoofyuniverse.autopickup.ore.OreAPI; +import net.smoofyuniverse.autopickup.util.IOUtil; +import ninja.leaping.configurate.ConfigurationNode; +import ninja.leaping.configurate.ConfigurationOptions; +import ninja.leaping.configurate.commented.CommentedConfigurationNode; +import ninja.leaping.configurate.hocon.HoconConfigurationLoader; +import ninja.leaping.configurate.loader.ConfigurationLoader; +import ninja.leaping.configurate.objectmapping.GuiceObjectMapperFactory; +import ninja.leaping.configurate.objectmapping.ObjectMappingException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.spongepowered.api.Game; +import org.spongepowered.api.Sponge; +import org.spongepowered.api.config.ConfigDir; +import org.spongepowered.api.event.Listener; +import org.spongepowered.api.event.game.GameReloadEvent; +import org.spongepowered.api.event.game.state.GamePreInitializationEvent; +import org.spongepowered.api.event.game.state.GameStartedServerEvent; +import org.spongepowered.api.plugin.Plugin; +import org.spongepowered.api.plugin.PluginContainer; +import org.spongepowered.api.scheduler.Task; +import org.spongepowered.api.text.Text; +import org.spongepowered.api.text.action.TextActions; +import org.spongepowered.api.text.format.TextColors; +import org.spongepowered.api.world.World; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import static java.lang.Math.max; +import static net.smoofyuniverse.autopickup.util.MathUtil.clamp; + +@Plugin(id = "autopickup", name = "AutoPickup", version = "1.0.0", authors = "Yeregorix", description = "Automatic pickup for items and experience orbs") +public class AutoPickup { + public static final Logger LOGGER = LoggerFactory.getLogger("AutoPickup"); + private static AutoPickup instance; + + @Inject + private Game game; + @Inject + @ConfigDir(sharedRoot = false) + private Path configDir; + @Inject + private PluginContainer container; + @Inject + private GuiceObjectMapperFactory factory; + @Inject + private MetricsLite metrics; + + private ConfigurationOptions configOptions; + private Path worldConfigsDir; + + private Map configs = new HashMap<>(); + private GlobalConfig.Immutable globalConfig; + private Text[] updateMessages = new Text[0]; + + public AutoPickup() { + if (instance != null) + throw new IllegalStateException(); + instance = this; + } + + @Listener + public void onGamePreInit(GamePreInitializationEvent e) { + this.worldConfigsDir = this.configDir.resolve("worlds"); + try { + Files.createDirectories(this.worldConfigsDir); + } catch (IOException ignored) { + } + this.configOptions = ConfigurationOptions.defaults().setObjectMapperFactory(this.factory); + + LOGGER.info("Loading global configuration .."); + try { + loadGlobalConfig(); + } catch (Exception ex) { + LOGGER.error("Failed to load global configuration", ex); + } + + this.game.getEventManager().registerListeners(this, new WorldEventListener()); + this.game.getEventManager().registerListeners(this, new EntityEventListener()); + this.game.getEventManager().registerListeners(this, new PlayerEventListener()); + } + + public void loadGlobalConfig() throws IOException, ObjectMappingException { + if (this.globalConfig != null) + throw new IllegalStateException("Config already loaded"); + + Path file = this.configDir.resolve("global.conf"); + ConfigurationLoader loader = createConfigLoader(file); + + CommentedConfigurationNode root = loader.load(); + int version = root.getNode("Version").getInt(); + if ((version > GlobalConfig.CURRENT_VERSION || version < GlobalConfig.MINIMUM_VERSION) && IOUtil.backupFile(file)) { + LOGGER.info("Your global config version is not supported. A new one will be generated."); + root = loader.createEmptyNode(); + } + + ConfigurationNode cfgNode = root.getNode("Config"); + GlobalConfig cfg = cfgNode.getValue(GlobalConfig.TOKEN, new GlobalConfig()); + + cfg.updateCheck.repetitionInterval = max(cfg.updateCheck.repetitionInterval, 0); + cfg.updateCheck.consoleDelay = clamp(cfg.updateCheck.consoleDelay, -1, 100); + cfg.updateCheck.playerDelay = clamp(cfg.updateCheck.playerDelay, -1, 100); + + if (cfg.updateCheck.consoleDelay == -1 && cfg.updateCheck.playerDelay == -1) + cfg.updateCheck.enabled = false; + + version = GlobalConfig.CURRENT_VERSION; + root.getNode("Version").setValue(version); + cfgNode.setValue(GlobalConfig.TOKEN, cfg); + loader.save(root); + + this.globalConfig = cfg.toImmutable(); + } + + public ConfigurationLoader createConfigLoader(Path file) { + return HoconConfigurationLoader.builder().setPath(file).setDefaultOptions(this.configOptions).build(); + } + + @Listener + public void onGameReload(GameReloadEvent e) { + this.configs.clear(); + this.game.getServer().getWorlds().forEach(this::loadConfig); + } + + public void loadConfig(World world) { + String name = world.getName(); + + LOGGER.info("Loading configuration for world " + name + " .."); + try { + Path file = this.worldConfigsDir.resolve(name + ".conf"); + ConfigurationLoader loader = createConfigLoader(file); + + CommentedConfigurationNode root = loader.load(); + int version = root.getNode("Version").getInt(); + if ((version > WorldConfig.CURRENT_VERSION || version < WorldConfig.MINIMUM__VERSION) && IOUtil.backupFile(file)) { + LOGGER.info("Your config version is not supported. A new one will be generated."); + root = loader.createEmptyNode(); + } + + ConfigurationNode cfgNode = root.getNode("Config"); + WorldConfig cfg = cfgNode.getValue(WorldConfig.TOKEN, new WorldConfig()); + + root.getNode("Version").setValue(WorldConfig.CURRENT_VERSION); + cfgNode.setValue(WorldConfig.TOKEN, cfg); + loader.save(root); + + this.configs.put(name, cfg.toImmutable()); + } catch (Exception e) { + LOGGER.error("Failed to load configuration for world " + name, e); + } + } + + @Listener + public void onServerStarted(GameStartedServerEvent e) { + LOGGER.info("AutoPickup " + this.container.getVersion().orElse("?") + " was loaded successfully."); + + if (this.globalConfig.updateCheck.enabled) + Task.builder().async().interval(this.globalConfig.updateCheck.repetitionInterval, TimeUnit.HOURS).execute(this::checkForUpdate).submit(this); + } + + public void checkForUpdate() { + String version = this.container.getVersion().orElse(null); + if (version == null) + return; + + LOGGER.debug("Checking for update .."); + + String latestVersion = null; + try { + latestVersion = OreAPI.getLatestVersion(OreAPI.getProjectVersions("autopickup"), "7.1.0").orElse(null); + } catch (Exception e) { + LOGGER.info("Failed to check for update", e); + } + + if (latestVersion != null && !latestVersion.equals(version)) { + String downloadUrl = "https://ore.spongepowered.org/Yeregorix/AutoPickup/versions/" + latestVersion; + + Text msg1 = Text.join(Text.of("A new version of AutoPickup is available: "), + Text.builder(latestVersion).color(TextColors.AQUA).build(), + Text.of(". You're currently using version: "), + Text.builder(version).color(TextColors.AQUA).build(), + Text.of(".")); + + Text msg2; + try { + msg2 = Text.builder("Click here to open the download page.").color(TextColors.GOLD) + .onClick(TextActions.openUrl(new URL(downloadUrl))).build(); + } catch (MalformedURLException e) { + msg2 = null; + } + + if (this.globalConfig.updateCheck.consoleDelay != -1) { + Task.builder().delayTicks(this.globalConfig.updateCheck.consoleDelay) + .execute(() -> Sponge.getServer().getConsole().sendMessage(msg1)).submit(this); + } + + if (this.globalConfig.updateCheck.playerDelay != -1) + this.updateMessages = msg2 == null ? new Text[]{msg1} : new Text[]{msg1, msg2}; + } + } + + public Optional getConfig(World world) { + return Optional.ofNullable(this.configs.get(world.getName())); + } + + public boolean isEnabled(World world) { + WorldConfig.Immutable cfg = this.configs.get(world.getName()); + return cfg != null && cfg.enabled; + } + + public GlobalConfig.Immutable getGlobalConfig() { + if (this.globalConfig == null) + throw new IllegalStateException("Config not loaded"); + return this.globalConfig; + } + + public Text[] getUpdateMessages() { + return this.updateMessages; + } + + public PluginContainer getContainer() { + return this.container; + } + + public static AutoPickup get() { + if (instance == null) + throw new IllegalStateException("Instance not available"); + return instance; + } +} diff --git a/src/main/java/net/smoofyuniverse/autopickup/bstats/MetricsLite.java b/src/main/java/net/smoofyuniverse/autopickup/bstats/MetricsLite.java new file mode 100644 index 0000000..2873463 --- /dev/null +++ b/src/main/java/net/smoofyuniverse/autopickup/bstats/MetricsLite.java @@ -0,0 +1,325 @@ +package net.smoofyuniverse.autopickup.bstats; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.inject.Inject; +import ninja.leaping.configurate.commented.CommentedConfigurationNode; +import ninja.leaping.configurate.hocon.HoconConfigurationLoader; +import org.apache.commons.lang3.Validate; +import org.slf4j.Logger; +import org.spongepowered.api.Game; +import org.spongepowered.api.Platform; +import org.spongepowered.api.Sponge; +import org.spongepowered.api.config.ConfigDir; +import org.spongepowered.api.plugin.PluginContainer; +import org.spongepowered.api.scheduler.Task; + +import javax.net.ssl.HttpsURLConnection; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.zip.GZIPOutputStream; + +/** + * bStats collects some data for plugin authors. + *

+ * Check out https://bStats.org/ to learn more about bStats! + */ +public class MetricsLite { + + // The version of this bStats class + public static final int B_STATS_VERSION = 1; + // The url to which the data is sent + private static final String URL = "https://bStats.org/submitData/sponge"; + // A list with all known metrics class objects including this one + private static final List knownMetricsInstances = new ArrayList<>(); + // We use this flag to ensure only one instance of this class exist + private static boolean created = false; + // The plugin + private final PluginContainer plugin; + // The logger + private Logger logger; + // Is bStats enabled on this server? + private boolean enabled; + + // The uuid of the server + private String serverUUID; + + // Should failed requests be logged? + private boolean logFailedRequests = false; + // The config path + private Path configDir; + + // The constructor is not meant to be called by the user himself. + // The instance is created using Dependency Injection (https://docs.spongepowered.org/master/en/plugin/injection.html) + @Inject + private MetricsLite(PluginContainer plugin, Logger logger, @ConfigDir(sharedRoot = true) Path configDir) { + if (created) + throw new IllegalStateException("There's already an instance of this Metrics class!"); + created = true; + + this.plugin = plugin; + this.logger = logger; + this.configDir = configDir; + + try { + loadConfig(); + } catch (IOException e) { + // Failed to load configuration + this.logger.warn("Failed to load bStats config!", e); + return; + } + + // We are not allowed to send data about this server :( + if (!this.enabled) + return; + + Class usedMetricsClass = getFirstBStatsClass(); + if (usedMetricsClass == null) { + // Failed to get first metrics class + return; + } + + if (usedMetricsClass == getClass()) { + // We are the first! :) + linkMetrics(this); + startSubmitting(); + } else { + // We aren't the first so we link to the first metrics class + try { + usedMetricsClass.getMethod("linkMetrics", Object.class).invoke(null, this); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + if (this.logFailedRequests) + this.logger.warn("Failed to link to first metrics class {}!", usedMetricsClass.getName(), e); + } + } + } + + /** + * Loads the bStats configuration. + * + * @throws IOException If something did not work :( + */ + private void loadConfig() throws IOException { + Path configPath = this.configDir.resolve("bStats"), configFile = configPath.resolve("config.conf"); + Files.createDirectories(configPath); + HoconConfigurationLoader configLoader = HoconConfigurationLoader.builder().setPath(configFile).build(); + + CommentedConfigurationNode node; + if (Files.exists(configFile)) { + node = configLoader.load(); + } else { + node = configLoader.createEmptyNode(); + + // Add default values + node.getNode("enabled").setValue(false); + // Every server gets it's unique random id. + node.getNode("serverUuid").setValue(UUID.randomUUID().toString()); + // Should failed request be logged? + node.getNode("logFailedRequests").setValue(false); + + // Add information about bStats + node.getNode("enabled").setComment( + "bStats collects some data for plugin authors like how many servers are using their plugins.\n" + + "To honor their work, you should not disable it.\n" + + "This has nearly no effect on the server performance!\n" + + "Check out https://bStats.org/ to learn more :)" + ); + + configLoader.save(node); + } + + // Load configuration + this.enabled = node.getNode("enabled").getBoolean(false); + this.serverUUID = node.getNode("serverUuid").getString(); + this.logFailedRequests = node.getNode("logFailedRequests").getBoolean(); + } + + /** + * Gets the first bStat Metrics class. + * + * @return The first bStats metrics class. + */ + private Class getFirstBStatsClass() { + Path configPath = this.configDir.resolve("bStats"), tempFile = configPath.resolve("temp.txt"); + + try { + Files.createDirectories(configPath); + + if (Files.exists(tempFile)) { + Optional className = Files.lines(tempFile).findFirst(); + if (className.isPresent()) { + try { + // Let's check if a class with the given name exists. + return Class.forName(className.get()); + } catch (ClassNotFoundException ignored) { + } + } + } + + Files.write(tempFile, Arrays.asList(getClass().getName(), "Note: This class only exists for internal purpose. You can ignore it :)")); + + return getClass(); + } catch (IOException e) { + if (this.logFailedRequests) + this.logger.warn("Failed to get first bStats class!", e); + return null; + } + } + + /** + * Links an other metrics class with this class. + * This method is called using Reflection. + * + * @param metrics An object of the metrics class to link. + */ + public static void linkMetrics(Object metrics) { + knownMetricsInstances.add(metrics); + } + + private void startSubmitting() { + // Submit the data every 30 minutes, first time after 5 minutes to give other plugins enough time to start + // WARNING: Changing the frequency has no effect but your plugin WILL be blocked/deleted! + + Task.builder().execute(t -> { + if (Sponge.getPluginManager().isLoaded(this.plugin.getId())) { + // The data collection (e.g. for custom graphs) is done sync + // Don't be afraid! The connection to the bStats server is still async, only the stats collection is sync ;) + + submitData(); + } else { + t.cancel(); + } + }).delay(5, TimeUnit.MINUTES).interval(30, TimeUnit.MINUTES).submit(this.plugin); + } + + /** + * Collects the data and sends it afterwards. + */ + private void submitData() { + JsonObject data = getServerData(); + + JsonArray pluginData = new JsonArray(); + // Search for all other bStats Metrics classes to get their plugin data + for (Object metrics : knownMetricsInstances) { + try { + Object plugin = metrics.getClass().getMethod("getPluginData").invoke(metrics); + if (plugin instanceof JsonObject) + pluginData.add((JsonObject) plugin); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored) { + } + } + + data.add("plugins", pluginData); + + // Create a new thread for the connection to the bStats server + Task.builder().async().execute(() -> { + try { + // Send the data + sendData(data); + } catch (Exception e) { + // Something went wrong! :( + if (this.logFailedRequests) + this.logger.warn("Could not submit plugin stats!", e); + } + }).submit(this.plugin); + } + + /** + * Gets the server specific data. + * + * @return The server specific data. + */ + private JsonObject getServerData() { + Game game = Sponge.getGame(); + JsonObject data = new JsonObject(); + + data.addProperty("serverUUID", serverUUID); + + // Minecraft specific data + int players = game.getServer().getOnlinePlayers().size(); + data.addProperty("playerAmount", players > 200 ? 200 : players); + data.addProperty("onlineMode", game.getServer().getOnlineMode() ? 1 : 0); + data.addProperty("minecraftVersion", game.getPlatform().getMinecraftVersion().getName()); + data.addProperty("spongeImplementation", game.getPlatform().getContainer(Platform.Component.IMPLEMENTATION).getName()); + + // OS/Java specific data + data.addProperty("javaVersion", System.getProperty("java.version")); + data.addProperty("osName", System.getProperty("os.name")); + data.addProperty("osArch", System.getProperty("os.arch")); + data.addProperty("osVersion", System.getProperty("os.version")); + data.addProperty("coreCount", Runtime.getRuntime().availableProcessors()); + + return data; + } + + /** + * Sends the data to the bStats server. + * + * @param data The data to send. + * @throws Exception If the request failed. + */ + private static void sendData(JsonObject data) throws Exception { + Validate.notNull(data, "Data cannot be null"); + HttpsURLConnection co = (HttpsURLConnection) new URL(URL).openConnection(); + + // Compress the data to save bandwidth + byte[] bytes = compress(data.toString()); + + // Add headers + co.setRequestMethod("POST"); + co.addRequestProperty("Accept", "application/json"); + co.addRequestProperty("Connection", "close"); + co.addRequestProperty("Content-Encoding", "gzip"); // We gzip our request + co.addRequestProperty("Content-Length", String.valueOf(bytes.length)); + co.setRequestProperty("Content-Type", "application/json"); // We send our data in JSON format + co.setRequestProperty("User-Agent", "MC-Server/" + B_STATS_VERSION); + + // Send data + co.setDoOutput(true); + DataOutputStream out = new DataOutputStream(co.getOutputStream()); + out.write(bytes); + out.flush(); + out.close(); + + co.getInputStream().close(); // We don't care about the response - Just send our data :) + } + + /** + * Gzips the given String. + * + * @param str The string to gzip. + * @return The gzipped String. + * @throws IOException If the compression failed. + */ + private static byte[] compress(String str) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + GZIPOutputStream gzip = new GZIPOutputStream(out); + gzip.write(str.getBytes(StandardCharsets.UTF_8)); + gzip.close(); + return out.toByteArray(); + } + + /** + * Gets the plugin specific data. + * This method is called using Reflection. + * + * @return The plugin specific data. + */ + public JsonObject getPluginData() { + JsonObject data = new JsonObject(); + + data.addProperty("pluginName", this.plugin.getName()); + data.addProperty("pluginVersion", this.plugin.getVersion().orElse("unknown")); + data.add("customCharts", new JsonArray()); + + return data; + } +} \ No newline at end of file diff --git a/src/main/java/net/smoofyuniverse/autopickup/config/global/GlobalConfig.java b/src/main/java/net/smoofyuniverse/autopickup/config/global/GlobalConfig.java new file mode 100644 index 0000000..b73df8e --- /dev/null +++ b/src/main/java/net/smoofyuniverse/autopickup/config/global/GlobalConfig.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2018 Hugo Dupanloup (Yeregorix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.smoofyuniverse.autopickup.config.global; + +import com.google.common.reflect.TypeToken; +import ninja.leaping.configurate.objectmapping.Setting; +import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable; + +@ConfigSerializable +public class GlobalConfig { + public static final int CURRENT_VERSION = 1, MINIMUM_VERSION = 1; + public static final TypeToken TOKEN = TypeToken.of(GlobalConfig.class); + + @Setting(value = "UpdateCheck") + public UpdateCheckConfig updateCheck = new UpdateCheckConfig(); + + public Immutable toImmutable() { + return new Immutable(this.updateCheck.toImmutable()); + } + + public static class Immutable { + public final UpdateCheckConfig.Immutable updateCheck; + + public Immutable(UpdateCheckConfig.Immutable updateCheck) { + this.updateCheck = updateCheck; + } + } +} diff --git a/src/main/java/net/smoofyuniverse/autopickup/config/global/UpdateCheckConfig.java b/src/main/java/net/smoofyuniverse/autopickup/config/global/UpdateCheckConfig.java new file mode 100644 index 0000000..e3f282e --- /dev/null +++ b/src/main/java/net/smoofyuniverse/autopickup/config/global/UpdateCheckConfig.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2018 Hugo Dupanloup (Yeregorix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.smoofyuniverse.autopickup.config.global; + +import ninja.leaping.configurate.objectmapping.Setting; +import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable; + +@ConfigSerializable +public class UpdateCheckConfig { + @Setting(value = "Enabled", comment = "Enable or disable automatic update checking") + public boolean enabled = true; + @Setting(value = "RepetitionInterval", comment = "Interval in hours between update checking repetitions, 0 to disable") + public int repetitionInterval = 12; + @Setting(value = "ConsoleDelay", comment = "Delay in ticks before sending a message to the console, between -1 and 100") + public int consoleDelay = 20; + @Setting(value = "PlayerDelay", comment = "Delay in ticks before sending a message after a player connection, between -1 and 100") + public int playerDelay = 20; + + public Immutable toImmutable() { + return new Immutable(this.enabled, this.repetitionInterval, this.consoleDelay, this.playerDelay); + } + + public static class Immutable { + public final boolean enabled; + public final int repetitionInterval, consoleDelay, playerDelay; + + public Immutable(boolean enabled, int repetitionInterval, int consoleDelay, int playerDelay) { + this.enabled = enabled; + this.repetitionInterval = repetitionInterval; + this.consoleDelay = consoleDelay; + this.playerDelay = playerDelay; + } + } +} diff --git a/src/main/java/net/smoofyuniverse/autopickup/config/world/ExtendedTypeConfig.java b/src/main/java/net/smoofyuniverse/autopickup/config/world/ExtendedTypeConfig.java new file mode 100644 index 0000000..deb4e7c --- /dev/null +++ b/src/main/java/net/smoofyuniverse/autopickup/config/world/ExtendedTypeConfig.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018 Hugo Dupanloup (Yeregorix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.smoofyuniverse.autopickup.config.world; + +import ninja.leaping.configurate.objectmapping.Setting; +import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable; + +@ConfigSerializable +public class ExtendedTypeConfig extends TypeConfig { + @Setting(value = "NoDrop-Item", comment = "Enable or disable removing items when it's not caused by a player") + public boolean noDropItem = false; + @Setting(value = "NoDrop-Experience", comment = "Enable or disable removing experience orbs when it's not caused by a player") + public boolean noDropExperience = false; + + @Override + public Immutable toImmutable() { + return new Immutable(this.autoPickupItem, this.autoPickupExperience, this.noDropItem, this.noDropExperience); + } + + public static class Immutable extends TypeConfig.Immutable { + public final boolean noDropItem, noDropExperience; + + public Immutable(boolean autoPickupItem, boolean autoPickupExperience, boolean noDropItem, boolean noDropExperience) { + super(autoPickupItem, autoPickupExperience); + this.noDropItem = noDropItem; + this.noDropExperience = noDropExperience; + } + } +} diff --git a/src/main/java/net/smoofyuniverse/autopickup/config/world/TypeConfig.java b/src/main/java/net/smoofyuniverse/autopickup/config/world/TypeConfig.java new file mode 100644 index 0000000..f7aab10 --- /dev/null +++ b/src/main/java/net/smoofyuniverse/autopickup/config/world/TypeConfig.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2018 Hugo Dupanloup (Yeregorix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.smoofyuniverse.autopickup.config.world; + +import ninja.leaping.configurate.objectmapping.Setting; +import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable; + +@ConfigSerializable +public class TypeConfig { + @Setting(value = "AutoPickup-Item", comment = "Enable or disable automatic pickup for items") + public boolean autoPickupItem = true; + @Setting(value = "AutoPickup-Experience", comment = "Enable or disable automatic pickup for experience orbs") + public boolean autoPickupExperience = true; + + public Immutable toImmutable() { + return new Immutable(this.autoPickupItem, this.autoPickupExperience); + } + + public static class Immutable { + public final boolean autoPickupItem, autoPickupExperience; + + public Immutable(boolean autoPickupItem, boolean autoPickupExperience) { + this.autoPickupItem = autoPickupItem; + this.autoPickupExperience = autoPickupExperience; + } + } +} diff --git a/src/main/java/net/smoofyuniverse/autopickup/config/world/WorldConfig.java b/src/main/java/net/smoofyuniverse/autopickup/config/world/WorldConfig.java new file mode 100644 index 0000000..e85bd97 --- /dev/null +++ b/src/main/java/net/smoofyuniverse/autopickup/config/world/WorldConfig.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2018 Hugo Dupanloup (Yeregorix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.smoofyuniverse.autopickup.config.world; + +import com.google.common.reflect.TypeToken; +import ninja.leaping.configurate.objectmapping.Setting; +import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable; + +@ConfigSerializable +public class WorldConfig { + public static final int CURRENT_VERSION = 1, MINIMUM__VERSION = 1; + public static final TypeToken TOKEN = TypeToken.of(WorldConfig.class); + + @Setting(value = "Enabled", comment = "Enable or disable AutoPickup in this world") + public boolean enabled = true; + @Setting(value = "Entity", comment = "Section related to entities death") + public ExtendedTypeConfig entity = new ExtendedTypeConfig(); + @Setting(value = "Block", comment = "Section related to blocks break") + public TypeConfig block = new TypeConfig(); + + public Immutable toImmutable() { + return new Immutable(this.enabled, this.entity.toImmutable(), this.block.toImmutable()); + } + + public static class Immutable { + public final boolean enabled; + public final ExtendedTypeConfig.Immutable entity; + public final TypeConfig.Immutable block; + + public Immutable(boolean enabled, ExtendedTypeConfig.Immutable entity, TypeConfig.Immutable block) { + this.enabled = enabled; + this.entity = entity; + this.block = block; + } + } +} diff --git a/src/main/java/net/smoofyuniverse/autopickup/event/EntityEventListener.java b/src/main/java/net/smoofyuniverse/autopickup/event/EntityEventListener.java new file mode 100644 index 0000000..3101e2e --- /dev/null +++ b/src/main/java/net/smoofyuniverse/autopickup/event/EntityEventListener.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2018 Hugo Dupanloup (Yeregorix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.smoofyuniverse.autopickup.event; + +import net.smoofyuniverse.autopickup.AutoPickup; +import net.smoofyuniverse.autopickup.config.world.ExtendedTypeConfig; +import net.smoofyuniverse.autopickup.config.world.TypeConfig; +import net.smoofyuniverse.autopickup.config.world.WorldConfig; +import org.spongepowered.api.Sponge; +import org.spongepowered.api.data.key.Keys; +import org.spongepowered.api.entity.Entity; +import org.spongepowered.api.entity.EntitySnapshot; +import org.spongepowered.api.entity.ExperienceOrb; +import org.spongepowered.api.entity.Item; +import org.spongepowered.api.entity.living.ArmorStand; +import org.spongepowered.api.entity.living.Humanoid; +import org.spongepowered.api.entity.living.Living; +import org.spongepowered.api.entity.living.player.Player; +import org.spongepowered.api.event.Listener; +import org.spongepowered.api.event.Order; +import org.spongepowered.api.event.block.ChangeBlockEvent; +import org.spongepowered.api.event.entity.DestructEntityEvent; +import org.spongepowered.api.event.entity.SpawnEntityEvent; +import org.spongepowered.api.event.filter.cause.Root; +import org.spongepowered.api.item.inventory.Inventory; +import org.spongepowered.api.item.inventory.ItemStack; +import org.spongepowered.api.item.inventory.entity.PlayerInventory; +import org.spongepowered.api.scheduler.Task; +import org.spongepowered.api.world.World; + +import java.util.*; + +public class EntityEventListener { + private final Map trackedDatas = new HashMap<>(); + + @Listener(order = Order.POST) + public void onPlayerBreakBlock(ChangeBlockEvent.Break e, @Root Player p) { + if (AutoPickup.get().isEnabled(p.getWorld())) + track(new Data(Type.BLOCK, p.getUniqueId(), p), 2); + } + + private void track(Data data, int ticks) { + this.trackedDatas.put(data.livingId, data); + Task.builder().delayTicks(ticks).execute(() -> this.trackedDatas.remove(data.livingId)).submit(AutoPickup.get()); + } + + @Listener(order = Order.POST) + public void onEntityDeath(DestructEntityEvent.Death e) { + Living living = e.getTargetEntity(); + if (living instanceof Humanoid || living instanceof ArmorStand) + return; + + if (AutoPickup.get().isEnabled(living.getWorld())) + track(new Data(Type.ENTITY, living.getUniqueId(), living.lastAttacker().get().flatMap(EntitySnapshot::getUniqueId).flatMap(id -> Sponge.getServer().getPlayer(id)).orElse(null)), 22); + } + + @Listener(order = Order.EARLY) + public void onSpawnEntity(SpawnEntityEvent e) { + boolean uniqueWorld = true; + World world = null; + + for (Entity y : e.getEntities()) { + if (world == null) + world = y.getWorld(); + else if (world != y.getWorld()) { + uniqueWorld = false; + break; + } + } + + if (!uniqueWorld) { + AutoPickup.LOGGER.debug("A SpawnEntityEvent with entities from different worlds have been detected. This is not supported by the plugin."); + return; + } + + if (world == null || !AutoPickup.get().isEnabled(world)) + return; + + List orbs = new ArrayList<>(); + List items = new ArrayList<>(); + + { + Iterator it = e.getEntities().iterator(); + while (it.hasNext()) { + Entity y = it.next(); + if (y instanceof Item) { + items.add((Item) y); + it.remove(); + } else if (y instanceof ExperienceOrb) { + orbs.add((ExperienceOrb) y); + it.remove(); + } + } + } + + if (!orbs.isEmpty() || !items.isEmpty()) { + Living livingCause = e.getCause().first(Living.class).orElse(null); + if (livingCause != null) { + Data data = this.trackedDatas.get(livingCause.getUniqueId()); + if (data != null) { + WorldConfig.Immutable worldConfig = AutoPickup.get().getConfig(world).get(); + TypeConfig.Immutable typeConfig = data.type == Type.ENTITY ? worldConfig.entity : worldConfig.block; + + if (data.player != null) { + if (typeConfig.autoPickupExperience && !orbs.isEmpty() && data.player.hasPermission("autopickup." + data.type.id + ".experience")) { + int exp = 0; + for (ExperienceOrb orb : orbs) + exp += orb.experience().get(); + + final int expf = exp; + if (data.player.transform(Keys.TOTAL_EXPERIENCE, t -> t + expf).isSuccessful()) + orbs.clear(); + } + + if (typeConfig.autoPickupItem && !items.isEmpty() && data.player.hasPermission("autopickup." + data.type.id + ".item")) { + Inventory inv = ((PlayerInventory) data.player.getInventory()).getMain(); + + Iterator it = items.iterator(); + while (it.hasNext()) { + Item item = it.next(); + ItemStack stack = item.item().get().createStack(); + + inv.offer(stack); + + if (stack.isEmpty()) + it.remove(); + else + item.offer(item.item().set(stack.createSnapshot())); + } + } + } else if (typeConfig instanceof ExtendedTypeConfig.Immutable) { + if (((ExtendedTypeConfig.Immutable) typeConfig).noDropExperience) + orbs.clear(); + + if (((ExtendedTypeConfig.Immutable) typeConfig).noDropItem) + items.clear(); + } + } + } + + e.getEntities().addAll(orbs); + e.getEntities().addAll(items); + } + } + + private static enum Type { + ENTITY("entity"), BLOCK("block"); + + public final String id; + + private Type(String id) { + this.id = id; + } + } + + private static class Data { + public final Type type; + public final UUID livingId; + public final Player player; + + public Data(Type type, UUID livingId, Player player) { + if (type == null) + throw new IllegalArgumentException("type"); + if (livingId == null) + throw new IllegalArgumentException("livingId"); + + this.type = type; + this.livingId = livingId; + this.player = player; + } + } +} diff --git a/src/main/java/net/smoofyuniverse/autopickup/event/PlayerEventListener.java b/src/main/java/net/smoofyuniverse/autopickup/event/PlayerEventListener.java new file mode 100644 index 0000000..0d50a4d --- /dev/null +++ b/src/main/java/net/smoofyuniverse/autopickup/event/PlayerEventListener.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2018 Hugo Dupanloup (Yeregorix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.smoofyuniverse.autopickup.event; + +import net.smoofyuniverse.autopickup.AutoPickup; +import org.spongepowered.api.entity.living.player.Player; +import org.spongepowered.api.event.Listener; +import org.spongepowered.api.event.Order; +import org.spongepowered.api.event.network.ClientConnectionEvent; +import org.spongepowered.api.scheduler.Task; +import org.spongepowered.api.text.Text; + +public class PlayerEventListener { + + @Listener(order = Order.LATE) + public void onClientConnection(ClientConnectionEvent.Join e) { + Text[] messages = AutoPickup.get().getUpdateMessages(); + if (messages.length != 0) { + Player p = e.getTargetEntity(); + if (p.hasPermission("autopickup.update.notify")) { + Task.builder().delayTicks(AutoPickup.get().getGlobalConfig().updateCheck.playerDelay) + .execute(() -> p.sendMessages(messages)).submit(AutoPickup.get()); + } + } + } +} diff --git a/src/main/java/net/smoofyuniverse/autopickup/event/WorldEventListener.java b/src/main/java/net/smoofyuniverse/autopickup/event/WorldEventListener.java new file mode 100644 index 0000000..19bce21 --- /dev/null +++ b/src/main/java/net/smoofyuniverse/autopickup/event/WorldEventListener.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2018 Hugo Dupanloup (Yeregorix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.smoofyuniverse.autopickup.event; + +import net.smoofyuniverse.autopickup.AutoPickup; +import org.spongepowered.api.event.Listener; +import org.spongepowered.api.event.world.LoadWorldEvent; + +public class WorldEventListener { + + @Listener + public void onLoadWorld(LoadWorldEvent e) { + AutoPickup.get().loadConfig(e.getTargetWorld()); + } +} diff --git a/src/main/java/net/smoofyuniverse/autopickup/ore/OreAPI.java b/src/main/java/net/smoofyuniverse/autopickup/ore/OreAPI.java new file mode 100644 index 0000000..c725af5 --- /dev/null +++ b/src/main/java/net/smoofyuniverse/autopickup/ore/OreAPI.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2018 Hugo Dupanloup (Yeregorix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.smoofyuniverse.autopickup.ore; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import net.smoofyuniverse.autopickup.AutoPickup; +import net.smoofyuniverse.autopickup.ore.adapter.InstantAdapter; +import net.smoofyuniverse.autopickup.ore.object.DependencyInfo; +import net.smoofyuniverse.autopickup.ore.object.VersionInfo; + +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +public class OreAPI { + public static final URL URL_BASE = getUrl(); + public static final Gson GSON = new GsonBuilder().registerTypeAdapter(Instant.class, new InstantAdapter()).create(); + + private static URL getUrl() { + try { + return new URL("https://ore.spongepowered.org/api/v1/"); + } catch (MalformedURLException e) { + AutoPickup.LOGGER.info("Failed to load object url", e); + return null; + } + } + + public static List getProjectVersions(String projectId, String... channels) throws Exception { + return getProjectVersions(projectId, 0, 10, channels); + } + + public static List getProjectVersions(String projectId, int offset, int limit, String... channels) throws Exception { + String suffix = "projects/" + projectId + "/versions?offset=" + offset + "&limit=" + limit; + if (channels.length != 0) + suffix += "&channels=" + String.join(",", channels); + + return GSON.fromJson(new InputStreamReader(getUrl(suffix).openStream()), new TypeToken>() {}.getType()); + } + + public static URL getUrl(String suffix) throws MalformedURLException { + return new URL(URL_BASE.getProtocol(), URL_BASE.getHost(), URL_BASE.getPort(), URL_BASE.getFile() + suffix); + } + + public static Optional getLatestVersion(List versions, String... spongeApiVersions) { + String version = null; + Instant date = null; + + for (VersionInfo info : versions) { + if (date == null || info.createdAt.isAfter(date)) { + String spongeApiVersion = null; + for (DependencyInfo d : info.dependencies) { + if (d.pluginId.equals("spongeapi")) { + spongeApiVersion = d.version; + break; + } + } + + if (spongeApiVersion != null) { + boolean valid = false; + for (String v : spongeApiVersions) { + if (spongeApiVersion.startsWith(v)) { + valid = true; + break; + } + } + + if (valid) { + version = info.name; + date = info.createdAt; + } + } + } + } + + return Optional.ofNullable(version); + } +} diff --git a/src/main/java/net/smoofyuniverse/autopickup/ore/adapter/InstantAdapter.java b/src/main/java/net/smoofyuniverse/autopickup/ore/adapter/InstantAdapter.java new file mode 100644 index 0000000..fad61b8 --- /dev/null +++ b/src/main/java/net/smoofyuniverse/autopickup/ore/adapter/InstantAdapter.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2018 Hugo Dupanloup (Yeregorix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.smoofyuniverse.autopickup.ore.adapter; + +import com.google.gson.*; + +import java.lang.reflect.Type; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; + +public final class InstantAdapter implements JsonSerializer, JsonDeserializer { + public static final DateTimeFormatter FORMAT = new DateTimeFormatterBuilder() + .append(DateTimeFormatter.ISO_LOCAL_DATE).appendLiteral(' ') + .append(DateTimeFormatter.ISO_LOCAL_TIME).toFormatter().withZone(ZoneId.systemDefault()); + + @Override + public JsonElement serialize(Instant object, Type type, JsonSerializationContext context) { + return new JsonPrimitive(FORMAT.format(object)); + } + + @Override + public Instant deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { + return FORMAT.parse(json.getAsString(), Instant::from); + } +} diff --git a/src/main/java/net/smoofyuniverse/autopickup/ore/object/ChannelInfo.java b/src/main/java/net/smoofyuniverse/autopickup/ore/object/ChannelInfo.java new file mode 100644 index 0000000..b35ad44 --- /dev/null +++ b/src/main/java/net/smoofyuniverse/autopickup/ore/object/ChannelInfo.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2018 Hugo Dupanloup (Yeregorix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.smoofyuniverse.autopickup.ore.object; + +public class ChannelInfo { + public String name, color; +} diff --git a/src/main/java/net/smoofyuniverse/autopickup/ore/object/DependencyInfo.java b/src/main/java/net/smoofyuniverse/autopickup/ore/object/DependencyInfo.java new file mode 100644 index 0000000..80fca5c --- /dev/null +++ b/src/main/java/net/smoofyuniverse/autopickup/ore/object/DependencyInfo.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2018 Hugo Dupanloup (Yeregorix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.smoofyuniverse.autopickup.ore.object; + +public class DependencyInfo { + public String pluginId, version; +} diff --git a/src/main/java/net/smoofyuniverse/autopickup/ore/object/TagInfo.java b/src/main/java/net/smoofyuniverse/autopickup/ore/object/TagInfo.java new file mode 100644 index 0000000..0fad152 --- /dev/null +++ b/src/main/java/net/smoofyuniverse/autopickup/ore/object/TagInfo.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2018 Hugo Dupanloup (Yeregorix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.smoofyuniverse.autopickup.ore.object; + +public class TagInfo { + public String name, data, backgroundColor, foregroundColor; + public int id; +} diff --git a/src/main/java/net/smoofyuniverse/autopickup/ore/object/VersionInfo.java b/src/main/java/net/smoofyuniverse/autopickup/ore/object/VersionInfo.java new file mode 100644 index 0000000..ac21c83 --- /dev/null +++ b/src/main/java/net/smoofyuniverse/autopickup/ore/object/VersionInfo.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2018 Hugo Dupanloup (Yeregorix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.smoofyuniverse.autopickup.ore.object; + +import java.time.Instant; +import java.util.List; + +public class VersionInfo { + public String name, pluginId, md5, href, author; + public List dependencies; + public List tags; + public ChannelInfo channel; + public Instant createdAt; + public int id, fileSize, downloads; + public boolean staffApproved; +} diff --git a/src/main/java/net/smoofyuniverse/autopickup/util/IOUtil.java b/src/main/java/net/smoofyuniverse/autopickup/util/IOUtil.java new file mode 100644 index 0000000..d0d7539 --- /dev/null +++ b/src/main/java/net/smoofyuniverse/autopickup/util/IOUtil.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2018 Hugo Dupanloup (Yeregorix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.smoofyuniverse.autopickup.util; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class IOUtil { + + public static boolean backupFile(Path file) throws IOException { + if (!Files.exists(file)) + return false; + + String fn = file.getFileName() + ".backup"; + Path backup = null; + for (int i = 0; i < 100; i++) { + backup = file.resolveSibling(fn + i); + if (!Files.exists(backup)) + break; + } + Files.move(file, backup); + return true; + } +} diff --git a/src/main/java/net/smoofyuniverse/autopickup/util/MathUtil.java b/src/main/java/net/smoofyuniverse/autopickup/util/MathUtil.java new file mode 100644 index 0000000..289a29f --- /dev/null +++ b/src/main/java/net/smoofyuniverse/autopickup/util/MathUtil.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2018 Hugo Dupanloup (Yeregorix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.smoofyuniverse.autopickup.util; + +public class MathUtil { + + public static int clamp(int value, int min, int max) { + if (value < min) + return min; + if (value > max) + return max; + return value; + } +}