diff --git a/.idea/misc.xml b/.idea/misc.xml index e11175d..eb42ab7 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,8 +1,9 @@ - + + diff --git a/api/src/main/java/team/unnamed/creative/central/pack/ResourcePackStatus.java b/api/src/main/java/team/unnamed/creative/central/pack/ResourcePackStatus.java index 8cdeb7d..399a9e7 100644 --- a/api/src/main/java/team/unnamed/creative/central/pack/ResourcePackStatus.java +++ b/api/src/main/java/team/unnamed/creative/central/pack/ResourcePackStatus.java @@ -127,7 +127,7 @@ public enum ResourcePackStatus { * parse the resource-pack URL. * *

A valid resource-pack URL MUST be parseable by - * the {@link java.net.URL(String))} constructor and + * the {@link java.net.URL#URL(String)} constructor and * MUST have a 'HTTPS' or 'HTTP' protocol. In any other * case, this status is received.

* diff --git a/common/src/main/java/team/unnamed/creative/central/common/util/Streams.java b/common/src/main/java/team/unnamed/creative/central/common/util/Streams.java index 090af4d..f45b4fe 100644 --- a/common/src/main/java/team/unnamed/creative/central/common/util/Streams.java +++ b/common/src/main/java/team/unnamed/creative/central/common/util/Streams.java @@ -1,27 +1,27 @@ -/* - * This file is part of creative-central, licensed under the MIT license - * - * Copyright (c) 2021-2023 Unnamed Team - * - * 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 team.unnamed.creative.central.common.util; +/* + * This file is part of creative-central, licensed under the MIT license + * + * Copyright (c) 2021-2023 Unnamed Team + * + * 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 team.unnamed.creative.central.common.util; import java.io.BufferedOutputStream; import java.io.File; @@ -29,6 +29,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; +import java.nio.file.Path; /** * Utility class for working with @@ -85,7 +86,24 @@ public static void pipeToFile( InputStream input, File outputFile ) throws IOException { - try (OutputStream output = new BufferedOutputStream(Files.newOutputStream(outputFile.toPath()))) { + pipeToFile(input, outputFile.toPath()); + } + + /** + * Reads and writes the data from the + * given {@code input} to the given {@code outputFile} + * + *

Note that this method doesn't close + * the given input stream

+ * + * @throws IOException If an error occurs while + * reading or writing the data + */ + public static void pipeToFile( + InputStream input, + Path outputFile + ) throws IOException { + try (OutputStream output = new BufferedOutputStream(Files.newOutputStream(outputFile))) { pipe(input, output); } } diff --git a/gradle.properties b/gradle.properties index 891ff7c..91b42c9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=team.unnamed -version=1.3.0 +version=1.3.1 description=The resource-pack unifier for Minecraft: Java Edition servers. repositoryName=ossrh diff --git a/settings.gradle.kts b/settings.gradle.kts index 8b1f052..ee05803 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,6 +8,7 @@ includePrefixed("api") includePrefixed("common") includePrefixed("bukkit") //includePrefixed("minestom") +includePrefixed("velocity") fun includePrefixed(name: String) { val kebabName = name.replace(':', '-') diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts new file mode 100644 index 0000000..8702871 --- /dev/null +++ b/velocity/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + id("creative.dist-conventions") +} + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") +} + +dependencies { + implementation(libs.creative.serializer.minecraft) + implementation(project(":creative-central-api")) + implementation(project(":creative-central-common")) + compileOnly("com.velocitypowered:velocity-api:3.3.0-SNAPSHOT") + annotationProcessor("com.velocitypowered:velocity-api:3.3.0-SNAPSHOT") + + + implementation("org.bstats:bstats-velocity:3.0.2") // bstats +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +tasks { + shadowJar { + dependencies { + // all these dependencies are provided by the server + exclude(dependency("com.google.code.gson:gson")) + exclude(dependency("net.kyori:adventure-api")) + exclude(dependency("net.kyori:adventure-key")) + exclude(dependency("net.kyori:examination-api")) + exclude(dependency("net.kyori:examination-string")) + + // relocate bstats + relocate("org.bstats", "team.unnamed.creative.central.bukkit.lib.bstats") + } + } +} \ No newline at end of file diff --git a/velocity/src/main/java/team/unnamed/creative/central/velocity/CreativeCentralPlugin.java b/velocity/src/main/java/team/unnamed/creative/central/velocity/CreativeCentralPlugin.java new file mode 100644 index 0000000..bafb447 --- /dev/null +++ b/velocity/src/main/java/team/unnamed/creative/central/velocity/CreativeCentralPlugin.java @@ -0,0 +1,449 @@ +/* + * This file is part of creative-central, licensed under the MIT license + * + * Copyright (c) 2021-2023 Unnamed Team + * + * 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 team.unnamed.creative.central.velocity; + +import com.google.inject.Inject; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; +import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; +import com.velocitypowered.api.plugin.Plugin; +import com.velocitypowered.api.plugin.PluginDescription; +import com.velocitypowered.api.plugin.annotation.DataDirectory; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import org.bstats.velocity.Metrics; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.spongepowered.configurate.CommentedConfigurationNode; +import team.unnamed.creative.ResourcePack; +import team.unnamed.creative.central.CreativeCentral; +import team.unnamed.creative.central.CreativeCentralProvider; +import team.unnamed.creative.central.common.config.Configuration; +import team.unnamed.creative.central.common.config.ExportConfiguration; +import team.unnamed.creative.central.common.config.YamlConfigurationLoader; +import team.unnamed.creative.central.common.event.EventBusImpl; +import team.unnamed.creative.central.common.event.EventExceptionHandler; +import team.unnamed.creative.central.common.export.ResourcePackExporterFactory; +import team.unnamed.creative.central.common.server.CommonResourcePackServer; +import team.unnamed.creative.central.common.util.Components; +import team.unnamed.creative.central.common.util.LocalAddressProvider; +import team.unnamed.creative.central.common.util.Monitor; +import team.unnamed.creative.central.common.util.Streams; +import team.unnamed.creative.central.event.EventBus; +import team.unnamed.creative.central.event.pack.ResourcePackGenerateEvent; +import team.unnamed.creative.central.event.pack.ResourcePackStatusEvent; +import team.unnamed.creative.central.export.ResourcePackExporter; +import team.unnamed.creative.central.export.ResourcePackLocation; +import team.unnamed.creative.central.request.ResourcePackRequest; +import team.unnamed.creative.central.request.ResourcePackRequestSender; +import team.unnamed.creative.central.server.CentralResourcePackServer; +import team.unnamed.creative.central.server.ServeOptions; +import team.unnamed.creative.central.velocity.command.MainCommand; +import team.unnamed.creative.central.velocity.external.ExternalResourcePackProvider; +import team.unnamed.creative.central.velocity.external.ExternalResourcePackProviders; +import team.unnamed.creative.central.velocity.listener.CreativeResourcePackStatusListener; +import team.unnamed.creative.central.velocity.listener.ResourcePackSendListener; +import team.unnamed.creative.central.velocity.listener.ResourcePackStatusListener; +import team.unnamed.creative.central.velocity.request.VelocityResourcePackRequestSender; +import team.unnamed.creative.central.velocity.util.PluginResources; +import team.unnamed.creative.metadata.pack.PackMeta; +import team.unnamed.creative.resources.MergeStrategy; +import team.unnamed.creative.serialize.minecraft.MinecraftResourcePackReader; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.LogManager; + +@Plugin( + id = "creative-central", version = "1.3.0-dev", authors = {"Unnamed Team", "itsTyrion"}, + description = "Creative central, the resource-pack unifier", + url = "https://unnamed.team/docs/creative/central/creative-central" +) +public final class CreativeCentralPlugin implements CreativeCentral { + + private final ProxyServer proxy; + private final Logger logger; + private final Path dataFolder; + private final PluginDescription description; + private final Metrics.Factory metricsFactory; + private CommentedConfigurationNode rawConfig; + + @Inject + public CreativeCentralPlugin(ProxyServer proxy, Logger logger, @DataDirectory Path dataFolder, + PluginDescription description, Metrics.Factory metricsFactory) { + this.proxy = proxy; + this.logger = logger; + this.dataFolder = dataFolder; + this.description = description; + this.metricsFactory = metricsFactory; + } + + private ServeOptions serveOptions; + private EventBus eventBus; + private ResourcePackRequestSender requestSender; + private CentralResourcePackServer resourcePackServer; + + private Monitor configurationMonitor; + + + @Subscribe + public void onEnable(ProxyInitializeEvent event) { + Configuration config = YamlConfigurationLoader.load(PluginResources.get(this, "config.yml")); + this.configurationMonitor = Monitor.monitor(config); + reloadConfig(); + + metricsFactory.make(this, 20718); // metrics (bstats.org) + + serveOptions = new ServeOptions(); + eventBus = new EventBusImpl<>(Object.class, EventExceptionHandler.logging(logger::warn)); + requestSender = VelocityResourcePackRequestSender.velocity(proxy); + resourcePackServer = new CommonResourcePackServer(); + + // load serve/send options + serveOptions.serve(true); + serveOptions.delay(config.send().delay()); + + // register event listeners + listen( + new ResourcePackStatusListener(this), + new ResourcePackSendListener(this) + ); + + // register our command + proxy.getCommandManager().register("vcentral", new MainCommand(this)); + + // load actions + eventBus.listen(this, ResourcePackStatusEvent.class, new CreativeResourcePackStatusListener(configurationMonitor)); + + // start resource pack server if enabled + loadResourcePackServer(); + + // register service providers + registerService(); + + // generate the resource-pack for the first time + generateFirstLoad(); + } + + public Monitor config() { + return configurationMonitor; + } + + private void registerService() { + // Services are not YET a feature on Velocity, there's an open maintainer PR, though. recheck occasionally? +// Bukkit.getServicesManager().register(CreativeCentral.class, this, this, ServicePriority.High); + CreativeCentralProvider.set(this); + } + + private void unregisterService() { + CreativeCentralProvider.unset(); + // Services are not YET a feature on Velocity, there's an open maintainer PR, though. +// Bukkit.getServicesManager().unregister(CreativeCentral.class, this); + } + + private void loadResourcePackServer() { + ExportConfiguration.LocalHostExportConfiguration config = config().get().export().localHost(); + + if (!config.enabled()) { + return; + } + + getLogger().info("Resource-pack server enabled, starting..."); + + String address = config.address(); + String publicAddress = config.publicAddress(); + int port = config.port(); + + // if address is empty, automatically detect the server's address + if (address.trim().isEmpty()) { + try { + address = LocalAddressProvider.getLocalAddress(configurationMonitor.get().whatIsMyIpServices()); + } catch (IOException e) { + getLogger().error("An exception was caught when trying to get the local server address", e); + } + + if (address == null) { + getLogger().error("Couldn't get the local server address"); + } + } + + if (publicAddress == null || publicAddress.trim().isEmpty()) { + publicAddress = address; + } + + if (address != null) { + try { + resourcePackServer.open(address, publicAddress, port); + getLogger().info("Successfully started the resource-pack server, listening on port " + port); + } catch (IOException e) { + getLogger().error("Failed to open the resource pack server", e); + } + } + } + + private void generateFirstLoad() { + final var allProviders = ExternalResourcePackProviders.get(); + final var awaitingProviders = new ArrayList(allProviders.length); + + for (final var provider : allProviders) { + if (proxy.getPluginManager().getPlugin(provider.pluginName()).isEmpty()) { + continue; + } + + getLogger().info("Found " + provider.pluginName() + ", registering as an external resource pack provider..."); + + final AtomicBoolean generateCalledByChangeOnThisProvider = new AtomicBoolean(false); + + eventBus.listen(this, ResourcePackGenerateEvent.class, event -> { + final var externalResourcePack = provider.load(); + if (externalResourcePack != null) { + getLogger().info("Merging resource pack from external provider: " + provider.pluginName()); + event.resourcePack().merge(externalResourcePack, MergeStrategy.mergeAndKeepFirstOnError()); + } else { + getLogger().warn("Couldn't load resource pack from external provider: " + provider.pluginName() + ": Not found"); + } + }); + + if (provider.awaitOnStart()) { + awaitingProviders.add(provider); + } + } + + if (awaitingProviders.isEmpty()) { + // do not wait for anything + proxy.getScheduler().buildTask(this, () -> generate().whenComplete((pack, throwable) -> { + if (throwable != null) { + getLogger().error("Error while generating resource pack", throwable); + } + })).delay(Duration.ofMillis(50)).schedule(); + } else { + getLogger().info("Waiting on " + awaitingProviders.size() + " external resource-pack providers to finish generating..."); + final var awaitingResourcePackCountDown = new AtomicInteger(awaitingProviders.size()); + for (final var provider : awaitingProviders) { + provider.listenForChanges(this, () -> { + synchronized (awaitingResourcePackCountDown) { + final boolean generatePack; + + if (awaitingResourcePackCountDown.get() == 0) { + // this means that the change on this provider was made + // AFTER the first generation + generatePack = true; + } else { + // this means that the change on this provider was made + // DURING the first generation + getLogger().info(provider.pluginName() + " finished generating its resource-pack..."); + + // will be true if this is the last provider to be loaded + generatePack = awaitingResourcePackCountDown.decrementAndGet() == 0; + } + + if (generatePack) { + getLogger().info("All external resource-packs finished generating..."); + proxy.getScheduler().buildTask(this, () -> generate().whenComplete((pack, throwable) -> { + if (throwable != null) { + getLogger().error("Error while generating resource pack", throwable); + } + })).delay(Duration.ofMillis(50)).schedule(); + } + } + }); + } + } + } + + public ResourcePack generateSync() { + if (eventBus == null) { + throw new IllegalStateException("Unexpected status, event bus was null when trying to" + + " generate the resource pack. Is the server shutting down?"); + } + + Configuration config = configurationMonitor.get(); + + Path resourcesFolder = dataFolder.resolve("resources"); + if (!Files.exists(resourcesFolder)) { + try { + Files.createDirectories(resourcesFolder); + // copy pack.mcmeta and pack.png inside resources + try (InputStream meta = getClass().getResourceAsStream("/resources/pack.mcmeta")) { + Streams.pipeToFile(meta, resourcesFolder.resolve("pack.mcmeta")); + } + + try (InputStream icon = getClass().getResourceAsStream("/resources/pack.png")) { + Streams.pipeToFile(icon, resourcesFolder.resolve("pack.png")); + } + } catch (IOException e) { + getLogger().warn("Failed to copy pack.mcmeta and pack.png files" + + " inside the resources folder", e); + } + } + + ResourcePack resourcePack = Files.exists(resourcesFolder) + ? MinecraftResourcePackReader.minecraft().readFromDirectory(resourcesFolder.toFile()) + : ResourcePack.resourcePack(); + + // process the pack meta + { + PackMeta meta = resourcePack.packMeta(); + if (meta == null) { + getLogger().warn("Couldn't find pack metadata in the generated resource-pack"); + } else { + resourcePack.packMeta( + meta.formats().format(), + // reprocess description using MiniMessage + Components.deserialize(meta.description()) + ); + } + } + + eventBus.call(ResourcePackGenerateEvent.class, new ResourcePackGenerateEvent(resourcePack)); + getLogger().info("The resource pack has been generated successfully"); + + // export resource-pack + @Nullable ResourcePackLocation location = null; + { + getLogger().info("Exporting resource pack..."); + String exportType = config.export().type(); + var julLogger = LogManager.getLogManager().getLogger(description.getId()); + ResourcePackExporter exporter = + ResourcePackExporterFactory.create(exportType, dataFolder.toFile(), resourcePackServer, julLogger); + try { + location = exporter.export(resourcePack); + } catch (IOException e) { + getLogger().error("Failed to export resource pack", e); + } + } + + if (location != null) { + getLogger().info("Exported resource pack to " + location.uri() + " (" + location.hash() + ")"); + serveOptions.request(ResourcePackRequest.of( + location.uri(), + location.hash(), + config.send().request().required(), + Components.deserialize(config.send().request().prompt()) + )); + } else { + serveOptions.request(null); + getLogger().warn("Resource-pack has not been exported to a hosted server, the" + + " resource-pack will not be automatically sent to players."); + } + + if (location != null) { + // apply the resource-pack to online players + for (Player player : proxy.getAllPlayers()) { + requestSender.send(player, serveOptions.request()); + } + } + + return resourcePack; + } + + @Override + public CompletableFuture generate() { + if (eventBus == null) { + throw new IllegalStateException("Unexpected status, event bus was null when trying to" + + " generate the resource pack. Is the server shutting down?"); + } + + return CompletableFuture.supplyAsync( + this::generateSync, + task -> proxy.getScheduler().buildTask(this, task).schedule() + ); + } + + public void reloadConfig() { + try { + rawConfig = org.spongepowered.configurate.yaml.YamlConfigurationLoader.builder() + .path(dataFolder.resolve("config.yml")).build().load(); + } catch (IOException ex) { + logger.error("Unable to load config.yml", ex); + } + } + + @Subscribe + public void onDisable(ProxyShutdownEvent event) { + eventBus = null; + requestSender = null; + serveOptions = null; + + if (resourcePackServer != null) { + try { + resourcePackServer.close(); + } catch (IOException e) { + getLogger().warn("Failed to close resource pack server", e); + } + resourcePackServer = null; + } + + unregisterService(); + } + + public ProxyServer getProxy() { + return proxy; + } + + public Logger getLogger() { + return logger; + } + + public Path getDataFolder() { + return dataFolder; + } + + private void listen(Object... listeners) { + for (Object listener : listeners) { + proxy.getEventManager().register(this, listener); + } + } + + @Override + public CentralResourcePackServer server() { + return resourcePackServer; + } + + @Override + public ServeOptions serveOptions() { + return serveOptions; + } + + @Override + public ResourcePackRequestSender requestSender() { + return requestSender; + } + + @Override + public EventBus eventBus() { + return eventBus; + } + + public CommentedConfigurationNode rawConfig() { + return rawConfig; + } +} \ No newline at end of file diff --git a/velocity/src/main/java/team/unnamed/creative/central/velocity/action/VelocityActionExecutor.java b/velocity/src/main/java/team/unnamed/creative/central/velocity/action/VelocityActionExecutor.java new file mode 100644 index 0000000..7caaca1 --- /dev/null +++ b/velocity/src/main/java/team/unnamed/creative/central/velocity/action/VelocityActionExecutor.java @@ -0,0 +1,50 @@ +/* + * This file is part of creative-central, licensed under the MIT license + * + * Copyright (c) 2021-2023 Unnamed Team + * + * 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 team.unnamed.creative.central.velocity.action; + +import com.velocitypowered.api.proxy.Player; +import team.unnamed.creative.central.common.action.Action; +import team.unnamed.creative.central.common.action.ActionExecutor; +import team.unnamed.creative.central.common.action.AudienceActionExecutor; +import team.unnamed.creative.central.common.action.KickAction; + +public final class VelocityActionExecutor extends AudienceActionExecutor { + private static final ActionExecutor INSTANCE = new VelocityActionExecutor(); + + private VelocityActionExecutor() { + } + + @Override + protected void executeAction(Action action, Player player) { + if (action instanceof KickAction kickAction) { + player.disconnect(kickAction.reason()); + } else { + throw new IllegalArgumentException("Unknown action type: '" + action + "'"); + } + } + + public static ActionExecutor velocity() { + return INSTANCE; + } +} diff --git a/velocity/src/main/java/team/unnamed/creative/central/velocity/command/MainCommand.java b/velocity/src/main/java/team/unnamed/creative/central/velocity/command/MainCommand.java new file mode 100644 index 0000000..d010e52 --- /dev/null +++ b/velocity/src/main/java/team/unnamed/creative/central/velocity/command/MainCommand.java @@ -0,0 +1,170 @@ +/* + * This file is part of creative-central, licensed under the MIT license + * + * Copyright (c) 2021-2023 Unnamed Team + * + * 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 team.unnamed.creative.central.velocity.command; + +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.command.SimpleCommand; +import com.velocitypowered.api.permission.Tristate; +import com.velocitypowered.api.proxy.Player; +import net.kyori.adventure.text.Component; +import team.unnamed.creative.central.velocity.CreativeCentralPlugin; +import team.unnamed.creative.central.common.util.Components; +import team.unnamed.creative.central.request.ResourcePackRequest; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.util.Collection; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class MainCommand implements SimpleCommand { + + private final CreativeCentralPlugin central; + private final Map messageCache = new ConcurrentHashMap<>(); + + public MainCommand(CreativeCentralPlugin central) { + this.central = central; + } + + @Override + @ParametersAreNonnullByDefault + public void execute(Invocation invocation) { + CommandSource sender = invocation.source(); + String[] args = invocation.arguments(); + String subcommand = args.length == 0 ? "help" : args[0].toLowerCase(Locale.ROOT); + + switch (subcommand) { + case "reload" -> { + if (!sender.hasPermission("creative-central.command.reload")) { + send(sender, "command.no-permission.reload"); + return; + } + + // more than one argument, we are being strict, show usage + if (args.length != 1) { + send(sender, "command.usage.reload"); + return; + } + + central.reloadConfig(); + messageCache.clear(); + central.generate().thenAccept(resourcePack -> + send(sender, "command.feedback.reload")); + } + + case "apply" -> { + if (sender.getPermissionValue("creative-central.command.apply") == Tristate.FALSE) { + send(sender, "command.no-permission.apply.self"); + return; + } + + Collection targets; + + if (sender.hasPermission("creative-central.command.apply.others")) { + if (args.length != 1 && args.length != 2) { + send(sender, "command.usage.apply.others"); + return; + } + + if (args.length == 2 && !args[1].equals("@p")) { + // other argument + if (args[1].equals("@a")) { + targets = central.getProxy().getAllPlayers(); + if (targets.isEmpty()) { + send(sender, "command.apply.no-players"); + return; + } + } else { + Player player = central.getProxy().getPlayer(args[1]).orElse(null); + if (player == null) { + send(sender, "command.apply.player-not-found"); + return; + } + + targets = Collections.singleton(player); + } + } else { + if (!(sender instanceof Player player)) { + send(sender, "command.apply.no-player"); + return; + } + + targets = Collections.singleton(player); + } + } else { + // does not have permission to send to others + if (args.length == 2) { + send(sender, "command.no-permission.apply.others"); + return; + } else if (args.length != 1) { + send(sender, "command.usage.apply.self"); + return; + } + + if (!(sender instanceof Player player)) { + send(sender, "command.apply.no-player"); + return; + } + + // apply to self + targets = Collections.singleton(player); + } + + ResourcePackRequest request = central.serveOptions().request(); + if (request == null) { + send(sender, "command.apply.no-resource-pack"); + return; + } + + // send + for (Player target : targets) { + central.requestSender().send(target, request); + } + send(sender, "command.feedback.apply"); + } + + case "help", "?" -> { + if (sender.getPermissionValue("creative-central.command.help") == Tristate.FALSE) { + send(sender, "command.no-permission.help"); + return; + } + + send(sender, "command.help"); + } + + // covers 'help', '?' and any other unknown subcommand + default -> + send(sender, "command.usage.unknown"); + } + } + + + private void send(CommandSource sender, String messageKey) { + Component message = messageCache.computeIfAbsent(messageKey, k -> + Components.deserialize(central.rawConfig().node((Object[]) k.split("\\.")).getString(k))); + sender.sendMessage(message); + } + +} diff --git a/velocity/src/main/java/team/unnamed/creative/central/velocity/external/ExternalResourcePackProvider.java b/velocity/src/main/java/team/unnamed/creative/central/velocity/external/ExternalResourcePackProvider.java new file mode 100644 index 0000000..8f807e8 --- /dev/null +++ b/velocity/src/main/java/team/unnamed/creative/central/velocity/external/ExternalResourcePackProvider.java @@ -0,0 +1,82 @@ +/* + * This file is part of creative-central, licensed under the MIT license + * + * Copyright (c) 2021-2023 Unnamed Team + * + * 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 team.unnamed.creative.central.velocity.external; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import team.unnamed.creative.ResourcePack; +import team.unnamed.creative.central.velocity.CreativeCentralPlugin; + +/** + * Interface representing an external (like from another plugin) + * resource pack provider. + */ +public interface ExternalResourcePackProvider { + /** + * Returns the name of the plugin that provides the resource pack. + * + * @return The name of the plugin that provides the resource pack. + */ + @NotNull String pluginName(); + + /** + * Should we wait for the providers to make an initial resource-pack + * zip/export/build before incorporating them to our own resource-pack? + * + *

When it's set to true, the first resource-pack generation will + * wait until the resource-pack is created for this plugin.

+ * + *

When it's set to false, the first resource-pack generation will + * not wait, but will only check if it can load the current resource-pack

+ * + * @return Whether we should wait for the providers to make an initial + * resource-pack zip/export/build before incorporating them to our own + * resource-pack. + */ + default boolean awaitOnStart() { + return false; + } + + /** + * Listens for changes/rebuilds in the resource pack provided + * by this plugin. + * + *

It MUST be ensured that, when executing the given {@code changeListener}, + * the changes will be applied and calls to {@link #load()} from {@code changeListener} + * MUST return the updated resource-pack.

+ * + * @param plugin The plugin that will be used to register the listener. + * @param changeListener The listener that will be called when the + * resource pack changes. + */ + default void listenForChanges(final @NotNull CreativeCentralPlugin plugin, final @NotNull Runnable changeListener) { + } + + /** + * Loads the resource-pack. Null if not found. + * + * @return The resource-pack. Null if not found. + */ + @Nullable ResourcePack load(); +} diff --git a/velocity/src/main/java/team/unnamed/creative/central/velocity/external/ExternalResourcePackProviders.java b/velocity/src/main/java/team/unnamed/creative/central/velocity/external/ExternalResourcePackProviders.java new file mode 100644 index 0000000..37dccc1 --- /dev/null +++ b/velocity/src/main/java/team/unnamed/creative/central/velocity/external/ExternalResourcePackProviders.java @@ -0,0 +1,36 @@ +/* + * This file is part of creative-central, licensed under the MIT license + * + * Copyright (c) 2021-2023 Unnamed Team + * + * 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 team.unnamed.creative.central.velocity.external; + +import org.jetbrains.annotations.NotNull; + +public final class ExternalResourcePackProviders { + private ExternalResourcePackProviders() { + } + + public static @NotNull ExternalResourcePackProvider @NotNull [] get() { + return new ExternalResourcePackProvider[] { + }; + } +} diff --git a/velocity/src/main/java/team/unnamed/creative/central/velocity/listener/CreativeResourcePackStatusListener.java b/velocity/src/main/java/team/unnamed/creative/central/velocity/listener/CreativeResourcePackStatusListener.java new file mode 100644 index 0000000..24f129b --- /dev/null +++ b/velocity/src/main/java/team/unnamed/creative/central/velocity/listener/CreativeResourcePackStatusListener.java @@ -0,0 +1,60 @@ +/* + * This file is part of creative-central, licensed under the MIT license + * + * Copyright (c) 2021-2023 Unnamed Team + * + * 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 team.unnamed.creative.central.velocity.listener; + +import com.velocitypowered.api.proxy.Player; +import team.unnamed.creative.central.velocity.action.VelocityActionExecutor; +import team.unnamed.creative.central.common.action.Action; +import team.unnamed.creative.central.common.action.ActionExecutor; +import team.unnamed.creative.central.common.config.Configuration; +import team.unnamed.creative.central.common.util.Monitor; +import team.unnamed.creative.central.event.EventListener; +import team.unnamed.creative.central.event.pack.ResourcePackStatusEvent; +import team.unnamed.creative.central.pack.ResourcePackStatus; + +import java.util.Collections; +import java.util.List; + +public class CreativeResourcePackStatusListener implements EventListener { + + private final Monitor config; + private final ActionExecutor actionExecutor; + + public CreativeResourcePackStatusListener(Monitor config) { + this.config = config; + this.actionExecutor = VelocityActionExecutor.velocity(); + } + + @Override + public void on(ResourcePackStatusEvent event) { + ResourcePackStatus status = event.status(); + Player player = (Player) event.player(); + + List actions = config.get().feedback().getOrDefault(status, Collections.emptyList()); + for (Action action : actions) { + actionExecutor.execute(action, player); + } + } + +} diff --git a/velocity/src/main/java/team/unnamed/creative/central/velocity/listener/ResourcePackSendListener.java b/velocity/src/main/java/team/unnamed/creative/central/velocity/listener/ResourcePackSendListener.java new file mode 100644 index 0000000..8452fa9 --- /dev/null +++ b/velocity/src/main/java/team/unnamed/creative/central/velocity/listener/ResourcePackSendListener.java @@ -0,0 +1,75 @@ +/* + * This file is part of creative-central, licensed under the MIT license + * + * Copyright (c) 2021-2023 Unnamed Team + * + * 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 team.unnamed.creative.central.velocity.listener; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.player.ServerPostConnectEvent; +import com.velocitypowered.api.proxy.Player; +import net.kyori.adventure.util.Ticks; +import team.unnamed.creative.central.request.ResourcePackRequest; +import team.unnamed.creative.central.server.ServeOptions; +import team.unnamed.creative.central.velocity.CreativeCentralPlugin; + +public class ResourcePackSendListener { + + private final CreativeCentralPlugin plugin; + + public ResourcePackSendListener(CreativeCentralPlugin plugin) { + this.plugin = plugin; + } + + @SuppressWarnings("UnstableApiUsage") + @Subscribe + public void onJoin(ServerPostConnectEvent event) { + ServeOptions options = plugin.serveOptions(); + if (!options.serve()) { + // we don't send the resource pack on join + return; + } + if (event.getPreviousServer() != null) { + // we don't send the resource pack again + return; + } + + Player player = event.getPlayer(); + ResourcePackRequest request = options.request(); + if (request == null) { + // todo: should we kick the player? + return; + } + + int delay = options.delay(); + if (delay <= 0) { + // send the resource pack request immediately + plugin.requestSender().send(player, request); + } else { + // delay the resource pack request + plugin.getProxy().getScheduler() + .buildTask(plugin, () -> plugin.requestSender().send(player, request)) + .delay(Ticks.duration(delay * 20L)) + .schedule(); + } + } + +} diff --git a/velocity/src/main/java/team/unnamed/creative/central/velocity/listener/ResourcePackStatusListener.java b/velocity/src/main/java/team/unnamed/creative/central/velocity/listener/ResourcePackStatusListener.java new file mode 100644 index 0000000..127cd21 --- /dev/null +++ b/velocity/src/main/java/team/unnamed/creative/central/velocity/listener/ResourcePackStatusListener.java @@ -0,0 +1,60 @@ +/* + * This file is part of creative-central, licensed under the MIT license + * + * Copyright (c) 2021-2023 Unnamed Team + * + * 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 team.unnamed.creative.central.velocity.listener; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; +import com.velocitypowered.api.proxy.Player; +import team.unnamed.creative.central.CreativeCentral; +import team.unnamed.creative.central.event.pack.ResourcePackStatusEvent; +import team.unnamed.creative.central.pack.ResourcePackStatus; + +public class ResourcePackStatusListener { + + private final CreativeCentral central; + + public ResourcePackStatusListener(CreativeCentral central) { + this.central = central; + } + + @Subscribe + public void onResourcePackStatus(PlayerResourcePackStatusEvent event) { + Player player = event.getPlayer(); + + // convert bukkit status to creative status + ResourcePackStatus status = switch (event.getStatus()) { + case ACCEPTED -> ResourcePackStatus.ACCEPTED; + case DECLINED -> ResourcePackStatus.DECLINED; + case FAILED_DOWNLOAD -> ResourcePackStatus.FAILED; + case SUCCESSFUL -> ResourcePackStatus.LOADED; + case DOWNLOADED -> ResourcePackStatus.DOWNLOADED; + case INVALID_URL -> ResourcePackStatus.INVALID_URL; + case FAILED_RELOAD -> ResourcePackStatus.FAILED_RELOAD; + case DISCARDED -> ResourcePackStatus.DISCARDED; + }; + + central.eventBus().call(ResourcePackStatusEvent.class, new ResourcePackStatusEvent(player, status)); + } + +} diff --git a/velocity/src/main/java/team/unnamed/creative/central/velocity/request/VelocityResourcePackRequestSender.java b/velocity/src/main/java/team/unnamed/creative/central/velocity/request/VelocityResourcePackRequestSender.java new file mode 100644 index 0000000..b0599aa --- /dev/null +++ b/velocity/src/main/java/team/unnamed/creative/central/velocity/request/VelocityResourcePackRequestSender.java @@ -0,0 +1,80 @@ +/* + * This file is part of creative-central, licensed under the MIT license + * + * Copyright (c) 2021-2023 Unnamed Team + * + * 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 team.unnamed.creative.central.velocity.request; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; +import team.unnamed.creative.central.request.ResourcePackRequest; +import team.unnamed.creative.central.request.ResourcePackRequestSender; + +import java.util.Arrays; +import java.util.function.BiConsumer; + +public final class VelocityResourcePackRequestSender implements ResourcePackRequestSender { + + private final BiConsumer delegate; + + private VelocityResourcePackRequestSender(BiConsumer delegate) { + this.delegate = delegate; + } + + @Override + public void send(Object playerObject, ResourcePackRequest request) { + if (!(playerObject instanceof Player)) { + throw new IllegalArgumentException("Provided 'player' is not an actual Velocity Player: " + playerObject); + } + + delegate.accept((Player) playerObject, request); + } + + public static ResourcePackRequestSender velocity(ProxyServer proxy) { + + return new VelocityResourcePackRequestSender((player, request) -> { + // convert to hash byte array + String hs = request.hash(); + int len = hs.length(); + byte[] hash = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + hash[i / 2] = (byte) ((Character.digit(hs.charAt(i), 16) << 4) + + Character.digit(hs.charAt(i + 1), 16)); + } + + for (ResourcePackInfo rp : player.getAppliedResourcePacks()) { + if (rp.getId().equals(request.uuid()) && Arrays.equals(rp.getHash(), hash)) { + player.removeResourcePacks(rp.getId()); + } + } + + player.sendResourcePackOffer(proxy.createResourcePackBuilder(request.uri().toString()) + .setHash(hash) + .setShouldForce(request.required()) + .setPrompt(request.prompt()) + .build() + ); + }); + + } + +} diff --git a/velocity/src/main/java/team/unnamed/creative/central/velocity/util/PluginResources.java b/velocity/src/main/java/team/unnamed/creative/central/velocity/util/PluginResources.java new file mode 100644 index 0000000..8f69422 --- /dev/null +++ b/velocity/src/main/java/team/unnamed/creative/central/velocity/util/PluginResources.java @@ -0,0 +1,68 @@ +/* + * This file is part of creative-central, licensed under the MIT license + * + * Copyright (c) 2021-2023 Unnamed Team + * + * 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 team.unnamed.creative.central.velocity.util; + +import org.jetbrains.annotations.Nullable; +import team.unnamed.creative.central.velocity.CreativeCentralPlugin; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +public final class PluginResources { + + private PluginResources() { + } + + public static @Nullable InputStream get(CreativeCentralPlugin plugin, String path) { + Path dataFolder = plugin.getDataFolder(); + Path file = dataFolder.resolve(path); + try { + if (!Files.exists(file)) { + if (!Files.isDirectory(dataFolder)) { + Files.createDirectory(dataFolder); + } + // file doesn't exist in the plugin's data folder, + // try to get it from the jar and copy it to the + // data folder + InputStream stream = plugin.getClass().getResourceAsStream("/" + path); + if (stream == null) { // file doesn't exist in the jar either + if (path.equals("config.yml")) { + throw new IllegalStateException("config.yml not present in JAR"); + } + return null; + } + Files.copy(stream, file); + } + + // file exists in the plugin's data folder, + // read the resource from there + return Files.newInputStream(file); + } catch (IOException e) { + throw new IllegalStateException("Unable to load or create resource:", e); + } + } + +} \ No newline at end of file