diff --git a/plugins/railswitch-paper/.editorconfig b/plugins/railswitch-paper/.editorconfig new file mode 100644 index 000000000..c6bb9baec --- /dev/null +++ b/plugins/railswitch-paper/.editorconfig @@ -0,0 +1,16 @@ +# Editorconfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +end_of_line = crlf +insert_final_newline = true +continuation_indent_size = 8 + +[{*.xml, *.yml}] +indent_size = 2 diff --git a/plugins/railswitch-paper/LICENSE.txt b/plugins/railswitch-paper/LICENSE.txt new file mode 100644 index 000000000..48b0ec4e2 --- /dev/null +++ b/plugins/railswitch-paper/LICENSE.txt @@ -0,0 +1,22 @@ + +The MIT License (MIT) + +Copyright (c) 2019 okx-code + +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/plugins/railswitch-paper/README.md b/plugins/railswitch-paper/README.md new file mode 100644 index 000000000..3eb86ebeb --- /dev/null +++ b/plugins/railswitch-paper/README.md @@ -0,0 +1,26 @@ +# RailSwitch + +## How to use + +### Setup + +1. Place a sign in the block above a detector rail. **It must be reinforced on the same group as the detector rail.** +2. The first line must be `[destination]` (case insensitive) +3. The second, third and fourth lines can be destination names. +4. When a player passes over the detector rail, it will only activate and emit redstone if a player set their destination as any of the three on the sign. + +You may also use `[!destination]`, which will activate the rail if a player's destination is **not** on the signs. + +### Usage + +1. Type `/dest ` +2. Go AFK +3. Arrive + +You can also use spaces in a `/dest` command as an "or" operator, like `/dest `. In this command, RailSwitch will route players towards signs that contain either `` or ``. + +Another way to think about it is that `/dest` takes multiple arguments. Each argument is a destination that will be checked as you pass a sign. + +### Example Video + +[![RailSwitch Demo Video](https://img.youtube.com/vi/GKku2fcB-wY/0.jpg)](https://www.youtube.com/watch?v=GKku2fcB-wY) diff --git a/plugins/railswitch-paper/build.gradle.kts b/plugins/railswitch-paper/build.gradle.kts new file mode 100644 index 000000000..f08ec4990 --- /dev/null +++ b/plugins/railswitch-paper/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("io.papermc.paperweight.userdev") +} + +version = "2.0.0-SNAPSHOT" + +dependencies { + paperDevBundle("1.18.2-R0.1-SNAPSHOT") + + compileOnly(project(":plugins:civmodcore-paper")) + compileOnly(project(":plugins:namelayer-paper")) + compileOnly(project(":plugins:citadel-paper")) +} diff --git a/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/RailSwitchPlugin.java b/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/RailSwitchPlugin.java new file mode 100644 index 000000000..597dfa6b9 --- /dev/null +++ b/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/RailSwitchPlugin.java @@ -0,0 +1,39 @@ +package sh.okx.railswitch; + +import org.bukkit.event.Listener; +import sh.okx.railswitch.commands.DestinationCommand; +import sh.okx.railswitch.glue.CitadelGlue; +import sh.okx.railswitch.settings.SettingsManager; +import sh.okx.railswitch.switches.SwitchListener; +import vg.civcraft.mc.civmodcore.ACivMod; +import vg.civcraft.mc.civmodcore.commands.CommandManager; + +/** + * The Rail Switch plugin class + */ +public final class RailSwitchPlugin extends ACivMod implements Listener { + + private CommandManager commandManager; + + @Override + public void onEnable() { + super.onEnable(); + SettingsManager.init(this); + registerListener(new CitadelGlue(this)); + registerListener(new SwitchListener()); + commandManager = new CommandManager(this); + commandManager.init(); + commandManager.registerCommand(new DestinationCommand()); + } + + @Override + public void onDisable() { + SettingsManager.reset(); + if (commandManager != null) { + commandManager.reset(); + commandManager = null; + } + super.onDisable(); + } + +} diff --git a/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/commands/DestinationCommand.java b/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/commands/DestinationCommand.java new file mode 100644 index 000000000..766c3bd96 --- /dev/null +++ b/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/commands/DestinationCommand.java @@ -0,0 +1,23 @@ +package sh.okx.railswitch.commands; + +import co.aikar.commands.BaseCommand; +import co.aikar.commands.annotation.CommandAlias; +import co.aikar.commands.annotation.Description; +import co.aikar.commands.annotation.Optional; +import co.aikar.commands.annotation.Syntax; +import org.bukkit.entity.Player; +import sh.okx.railswitch.settings.SettingsManager; + +/** + * Continued support for setting and resetting your destination via a command. + */ +public final class DestinationCommand extends BaseCommand { + + @CommandAlias("dest|destination|setdestination|switch|setswitch|setsw") + @Description("Set your rail destination(s)") + @Syntax("[destination]") + public void onSetDestination(Player player, @Optional String destination) { + SettingsManager.setDestination(player, destination); + } + +} diff --git a/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/glue/CitadelGlue.java b/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/glue/CitadelGlue.java new file mode 100644 index 000000000..dfea913d1 --- /dev/null +++ b/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/glue/CitadelGlue.java @@ -0,0 +1,66 @@ +package sh.okx.railswitch.glue; + +import org.bukkit.block.Block; +import org.bukkit.plugin.Plugin; +import vg.civcraft.mc.citadel.Citadel; +import vg.civcraft.mc.citadel.ReinforcementManager; +import vg.civcraft.mc.citadel.model.Reinforcement; +import vg.civcraft.mc.civmodcore.utilities.DependencyGlue; +import vg.civcraft.mc.civmodcore.utilities.NullUtils; +import vg.civcraft.mc.civmodcore.world.WorldUtils; + +/** + * Glue for Citadel. + */ +public final class CitadelGlue extends DependencyGlue { + + private ReinforcementManager manager; + + public CitadelGlue(Plugin plugin) { + super(plugin, "Citadel"); + } + + public boolean isSafeToUse() { + if (!super.isDependencyEnabled()) { + return false; + } + if (this.manager == null) { + return false; + } + return true; + } + + /** + * Do two blocks, representing the sign and the rail, have the same reinforcement group? + * + * @param sign The block representing the sign. + * @param rail The block representing the rail. + * @return Returns true both blocks share the same reinforcement group, or if both are un-reinforced. + */ + public boolean doSignAndRailHaveSameReinforcement(Block sign, Block rail) { + if (!isSafeToUse() || !WorldUtils.isValidBlock(sign) || !WorldUtils.isValidBlock(rail)) { + return false; + } + Reinforcement signReinforcement = this.manager.getReinforcement(sign); + Reinforcement railReinforcement = this.manager.getReinforcement(rail); + if (signReinforcement == null && railReinforcement == null) { + return true; + } + if (signReinforcement == null || railReinforcement == null) { + return false; + } + return NullUtils.equalsNotNull( + signReinforcement.getGroup(), + railReinforcement.getGroup()); + } + + @Override + protected void onDependencyEnabled() { + this.manager = Citadel.getInstance().getReinforcementManager(); + } + + @Override + protected void onDependencyDisabled() { + this.manager = null; + } +} diff --git a/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/settings/DestinationSetting.java b/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/settings/DestinationSetting.java new file mode 100644 index 000000000..095298bf8 --- /dev/null +++ b/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/settings/DestinationSetting.java @@ -0,0 +1,27 @@ +package sh.okx.railswitch.settings; + +import com.google.common.base.Strings; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.java.JavaPlugin; +import vg.civcraft.mc.civmodcore.players.settings.impl.StringSetting; + +/** + * Setting representing a player's destination. + */ +public final class DestinationSetting extends StringSetting { + + public DestinationSetting(JavaPlugin plugin) { + super(plugin, "", "Destination", "dest", new ItemStack(Material.MINECART), + "The destination(s) that will be used to route you at rail junctions."); + } + + @Override + public String toText(String value) { + if (Strings.isNullOrEmpty(value)) { + return ""; + } + return value; + } + +} diff --git a/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/settings/RailSwitchMenu.java b/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/settings/RailSwitchMenu.java new file mode 100644 index 000000000..3265958f8 --- /dev/null +++ b/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/settings/RailSwitchMenu.java @@ -0,0 +1,18 @@ +package sh.okx.railswitch.settings; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import vg.civcraft.mc.civmodcore.players.settings.PlayerSettingAPI; +import vg.civcraft.mc.civmodcore.players.settings.gui.MenuSection; + +/** + * The menu for RailSwitch. This is what all RailSwitch settings will be registered to. + */ +public final class RailSwitchMenu extends MenuSection { + + public RailSwitchMenu() { + super("RailSwitch", "Settings relating to RailSwitch", PlayerSettingAPI.getMainMenu(), + new ItemStack(Material.RAIL)); + } + +} diff --git a/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/settings/ResetSetting.java b/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/settings/ResetSetting.java new file mode 100644 index 000000000..5389e002d --- /dev/null +++ b/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/settings/ResetSetting.java @@ -0,0 +1,50 @@ +package sh.okx.railswitch.settings; + +import java.util.UUID; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.java.JavaPlugin; +import vg.civcraft.mc.civmodcore.players.settings.gui.MenuSection; +import vg.civcraft.mc.civmodcore.players.settings.impl.StringSetting; + +/** + * ResetSetting, a setting that functions more like a GUI button than an actual setting. + */ +public final class ResetSetting extends StringSetting { + + private final DestinationSetting destinationSetting; + + public ResetSetting(JavaPlugin plugin, DestinationSetting destinationSetting) { + super(plugin, "", "Clear Destination", "dest", new ItemStack(Material.BARRIER), "Clears your destination."); + this.destinationSetting = destinationSetting; + } + + @Override + public void handleMenuClick(Player player, MenuSection menu) { + resetPlayerDestination(player); + menu.showScreen(player); + } + + /** + * Resets a player's destination value. + * + * @param player The player to reset the destination for. + */ + public void resetPlayerDestination(Player player) { + this.destinationSetting.setValue(player, ""); + player.sendMessage(ChatColor.GREEN + "Your destination has been reset."); + } + + @Override + public String getValue(UUID player) { + return this.destinationSetting.getValue(player); + } + + @Override + public String toText(String value) { + return this.destinationSetting.toText(value); + } + +} diff --git a/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/settings/SettingsManager.java b/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/settings/SettingsManager.java new file mode 100644 index 000000000..d50c6bcb0 --- /dev/null +++ b/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/settings/SettingsManager.java @@ -0,0 +1,88 @@ +package sh.okx.railswitch.settings; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import sh.okx.railswitch.RailSwitchPlugin; + +/** + * Manages the initialisation and registration of menu settings. + */ +public final class SettingsManager { + + private static RailSwitchMenu menu; + + private static DestinationSetting destSetting; + + private static ResetSetting resetSetting; + + /** + * Initialise the settings manager. This should only be called within RailSwitch onEnable(). + * + * @param plugin The enabled RailSwitch plugin instance. + */ + public static void init(RailSwitchPlugin plugin) { + // Create the menu elements + menu = new RailSwitchMenu(); + destSetting = new DestinationSetting(plugin); + resetSetting = new ResetSetting(plugin, destSetting); + // Register those elements + menu.registerToParentMenu(); + menu.registerSetting(destSetting); + menu.registerSetting(resetSetting); + } + + /** + * Gracefully resets the settings manager. This should only be called within RailSwitch onDisable(). + */ + public static void reset() { + // TODO: Deregister and unload all the menu elements once PlayerSettingAPI becomes reload safe + menu = null; + destSetting = null; + resetSetting = null; + } + + /** + * Sets a player's destination. This is for when the player settings don't themselves provide a way to do so. + * + * @param player The player to set the destination to. + * @param destination The destination to set. + */ + public static void setDestination(Player player, String destination) { + Preconditions.checkArgument(player != null); + if (Strings.isNullOrEmpty(destination)) { + if (resetSetting != null) { + resetSetting.resetPlayerDestination(player); + // Do not put a message here since the message is sent in the method above. + } + else { + player.sendMessage(ChatColor.RED + "Could not reset your destination."); + } + } + else { + if (destSetting != null) { + destSetting.setValue(player, destination); + player.sendMessage(ChatColor.GREEN + "Destination set to: " + destination); + } + else { + player.sendMessage(ChatColor.RED + "Could not set your destination."); + } + } + } + + /** + * Gets a player's destination. + * + * @param player The player to get the destination for. + * @return Returns the player's destination, which will never be null. + */ + public static String getDestination(Player player) { + String value = destSetting.getValue(player); + if (value == null) { + return ""; + } + return value; + } + +} diff --git a/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/switches/SwitchListener.java b/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/switches/SwitchListener.java new file mode 100644 index 000000000..ea4d3868e --- /dev/null +++ b/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/switches/SwitchListener.java @@ -0,0 +1,129 @@ +package sh.okx.railswitch.switches; + +import com.google.common.base.Strings; +import java.util.Arrays; +import org.bukkit.Material; +import org.bukkit.Tag; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.Sign; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Minecart; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockRedstoneEvent; +import sh.okx.railswitch.RailSwitchPlugin; +import sh.okx.railswitch.glue.CitadelGlue; +import sh.okx.railswitch.settings.SettingsManager; +import vg.civcraft.mc.civmodcore.world.WorldUtils; + +/** + * Switch listener that implements switch functionality. + */ +public class SwitchListener implements Listener { + + public static final String WILDCARD = "*"; + + public static final CitadelGlue CITADEL_GLUE = new CitadelGlue(RailSwitchPlugin.getPlugin(RailSwitchPlugin.class)); + + /** + * Event handler for rail switches. Will determine if a switch exists at the target location, and if so will process + * it accordingly, allowing it to trigger or not trigger depending on the rider's set destination, the listed + * destinations on the switch, and the switch type. + * + * @param event The block redstone event to base the switch's existence on. + */ + @EventHandler + public void onSwitchTrigger(BlockRedstoneEvent event) { + Block block = event.getBlock(); + // Block must be a detector rail being triggered + if (!WorldUtils.isValidBlock(block) + || block.getType() != Material.DETECTOR_RAIL + || event.getNewCurrent() != 15) { + return; + } + // Check that the block above the rail is a sign + Block above = block.getRelative(BlockFace.UP); + if (!Tag.SIGNS.isTagged(above.getType()) + || !(above.getState() instanceof Sign)) { + return; + } + // Check that the sign has a valid switch type + String[] lines = ((Sign) above.getState()).getLines(); + SwitchType type = SwitchType.find(lines[0]); + if (type == null) { + return; + } + // Check that a player is triggering the switch + // NOTE: The event doesn't provide the information and so the next best thing is searching for a + // player who is nearby and riding a minecart. + Player player = null; { + double searchDistance = Double.MAX_VALUE; + for (Entity entity : block.getWorld().getNearbyEntities(block.getLocation(), 3, 3, 3)) { + if (!(entity instanceof Player)) { + continue; + } + Entity vehicle = entity.getVehicle(); + // TODO: This should be abstracted into CivModCore + if (vehicle == null + || vehicle.getType() != EntityType.MINECART + || !(vehicle instanceof Minecart)) { + continue; + } + double distance = block.getLocation().distanceSquared(entity.getLocation()); + if (distance < searchDistance) { + searchDistance = distance; + player = (Player) entity; + } + } + } + if (player == null) { + return; + } + // If Citadel is enabled, check that the sign and the rail are on the same group + if (CITADEL_GLUE.isSafeToUse()) { + if (!CITADEL_GLUE.doSignAndRailHaveSameReinforcement(above, block)) { + return; + } + } + // Determine whether a player has a destination that matches one of the destinations + // listed on the switch signs, or match if there's a wildcard. + boolean matched = false; + String setDest = SettingsManager.getDestination(player); + if (!Strings.isNullOrEmpty(setDest)) { + String[] playerDestinations = setDest.split(" "); + String[] switchDestinations = Arrays.copyOfRange(lines, 1, lines.length); + matcher: + for (String playerDestination : playerDestinations) { + if (Strings.isNullOrEmpty(playerDestination)) { + continue; + } + if (playerDestination.equals(WILDCARD)) { + matched = true; + break; + } + for (String switchDestination : switchDestinations) { + if (Strings.isNullOrEmpty(switchDestination)) { + continue; + } + if (switchDestination.equals(WILDCARD) + || playerDestination.equalsIgnoreCase(switchDestination)) { + matched = true; + break matcher; + } + } + } + } + switch (type) { + case NORMAL: + event.setNewCurrent(matched ? 15 : 0); + break; + case INVERTED: + event.setNewCurrent(matched ? 0 : 15); + break; + } + } + +} diff --git a/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/switches/SwitchType.java b/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/switches/SwitchType.java new file mode 100644 index 000000000..e5dfeb668 --- /dev/null +++ b/plugins/railswitch-paper/src/main/java/sh/okx/railswitch/switches/SwitchType.java @@ -0,0 +1,43 @@ +package sh.okx.railswitch.switches; + +import org.apache.commons.lang3.StringUtils; + +/** + * Switch type matcher, will match a type to a tag. + */ +public enum SwitchType { + + NORMAL("[destination]"), + INVERTED("[!destination]"); + + private final String tag; + + /** + * Defines a new switch type by its tag. Should there be a duplicate tag, the {@link SwitchType#find(String) find()} + * function will return the first value found. + * + * @param tag The tag to associate with this type. + */ + SwitchType(String tag) { + this.tag = tag; + } + + /** + * Attempts to match a switch type to a switch tag. + * + * @param tag The tag to match with a type. + * @return Returns a match switch type, or null if none are found. + */ + public static SwitchType find(String tag) { + if (tag == null || tag.isEmpty()) { + return null; + } + for (SwitchType type : values()) { + if (StringUtils.equalsIgnoreCase(tag, type.tag)) { + return type; + } + } + return null; + } + +} diff --git a/plugins/railswitch-paper/src/main/resources/plugin.yml b/plugins/railswitch-paper/src/main/resources/plugin.yml new file mode 100644 index 000000000..825105975 --- /dev/null +++ b/plugins/railswitch-paper/src/main/resources/plugin.yml @@ -0,0 +1,10 @@ +name: RailSwitch +version: ${version} +main: sh.okx.railswitch.RailSwitchPlugin +depend: [CivModCore] +softdepend: [Citadel, NameLayer] +api-version: 1.17 +authors: [Okx, Protonull] +description: Easily make rail switches for automated destination selection +commands: + dest: {} diff --git a/settings.gradle.kts b/settings.gradle.kts index ddd982886..42fd4c459 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,3 +28,4 @@ include(":plugins:essenceglue-paper") include(":plugins:hiddenore-paper") include(":plugins:kirabukkitgateway-paper") include(":plugins:namecolors-paper") +include(":plugins:railswitch-paper")