From 3d0263c05f33e4c8d622fd0beff1251394ffb328 Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 9 Mar 2024 15:34:34 -0400 Subject: [PATCH] first commit --- .github/dependabot.yml | 11 + .github/workflows/maven.yml | 31 +++ .gitignore | 114 +++++++++ LICENSE | 21 ++ pom.xml | 150 +++++++++++ .../fun/supersmp/codelock/CodeLockPlugin.java | 38 +++ .../command/CommandCodeLockBlock.java | 42 ++++ .../java/fun/supersmp/codelock/core/Keys.java | 14 ++ .../fun/supersmp/codelock/core/Locale.java | 121 +++++++++ .../java/fun/supersmp/codelock/core/Perm.java | 49 ++++ .../java/fun/supersmp/codelock/core/Util.java | 70 ++++++ .../codelock/listener/PlayerListener.java | 236 ++++++++++++++++++ .../codelock/struct/CodeLockBlock.java | 10 + .../codelock/ui/AuthorizedUsersMenu.java | 93 +++++++ .../supersmp/codelock/ui/EditCodeMenu.java | 129 ++++++++++ .../supersmp/codelock/ui/EnterCodeMenu.java | 130 ++++++++++ src/main/resources/plugin.yml | 8 + 17 files changed, 1267 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/maven.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 pom.xml create mode 100644 src/main/java/fun/supersmp/codelock/CodeLockPlugin.java create mode 100644 src/main/java/fun/supersmp/codelock/command/CommandCodeLockBlock.java create mode 100644 src/main/java/fun/supersmp/codelock/core/Keys.java create mode 100644 src/main/java/fun/supersmp/codelock/core/Locale.java create mode 100644 src/main/java/fun/supersmp/codelock/core/Perm.java create mode 100644 src/main/java/fun/supersmp/codelock/core/Util.java create mode 100644 src/main/java/fun/supersmp/codelock/listener/PlayerListener.java create mode 100644 src/main/java/fun/supersmp/codelock/struct/CodeLockBlock.java create mode 100644 src/main/java/fun/supersmp/codelock/ui/AuthorizedUsersMenu.java create mode 100644 src/main/java/fun/supersmp/codelock/ui/EditCodeMenu.java create mode 100644 src/main/java/fun/supersmp/codelock/ui/EnterCodeMenu.java create mode 100644 src/main/resources/plugin.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5b06320 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "maven" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "daily" diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000..d2e7774 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,31 @@ +# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Java CI with Maven + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: mvn -B package --file pom.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8127e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,114 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +target/ + +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next + +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar +.flattened-pom.xml + +# Common working directory +run/ +src/main/resources/temp.txt diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..878e374 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Negative Games + +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. \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..371b63a --- /dev/null +++ b/pom.xml @@ -0,0 +1,150 @@ + + + 4.0.0 + + fun.supersmp + CodeLock + 1.0-SNAPSHOT + jar + + + 17 + 17 + UTF-8 + + + + + CodeLock + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.12.1 + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.2 + + + package + + shade + + + false + + + + + + + games.negative.alumina + fun.supersmp.codelock.libs.alumina + + + + + + + + + + + + src/main/resources + true + + + + + + + + Negative Games + https://repo.negative.games/repository/maven-releases/ + + + + + papermc-repo + https://repo.papermc.io/repository/maven-public/ + + + + + jitpack.io + https://jitpack.io + + + + + opencollab-repository-maven-snapshots + Opencollab Repository + https://repo.opencollab.dev/maven-snapshots + + + + + placeholderapi + https://repo.extendedclip.com/content/repositories/placeholderapi/ + + + + + + + io.papermc.paper + paper-api + 1.20.4-R0.1-SNAPSHOT + provided + + + + + games.negative.alumina + alumina + 1.5.2 + compile + + + + + org.jetbrains + annotations + 24.1.0 + provided + + + + + org.projectlombok + lombok + 1.18.30 + provided + + + + + + + + + + + + + me.clip + placeholderapi + 2.11.5 + provided + + + diff --git a/src/main/java/fun/supersmp/codelock/CodeLockPlugin.java b/src/main/java/fun/supersmp/codelock/CodeLockPlugin.java new file mode 100644 index 0000000..f031ec6 --- /dev/null +++ b/src/main/java/fun/supersmp/codelock/CodeLockPlugin.java @@ -0,0 +1,38 @@ +package fun.supersmp.codelock; + +import fun.supersmp.codelock.command.CommandCodeLockBlock; +import fun.supersmp.codelock.core.Locale; +import fun.supersmp.codelock.listener.PlayerListener; +import games.negative.alumina.AluminaPlugin; +import lombok.SneakyThrows; +import org.jetbrains.annotations.NotNull; + +public class CodeLockPlugin extends AluminaPlugin { + + private static CodeLockPlugin instance; + + @Override + public void load() { + instance = this; + } + + @SneakyThrows + @Override + public void enable() { + Locale.init(this); + + registerCommand(new CommandCodeLockBlock()); + registerListeners(new PlayerListener()); + } + + @Override + public void disable() { + + } + + @NotNull + public static CodeLockPlugin instance() { + return instance; + } + +} diff --git a/src/main/java/fun/supersmp/codelock/command/CommandCodeLockBlock.java b/src/main/java/fun/supersmp/codelock/command/CommandCodeLockBlock.java new file mode 100644 index 0000000..e69cf33 --- /dev/null +++ b/src/main/java/fun/supersmp/codelock/command/CommandCodeLockBlock.java @@ -0,0 +1,42 @@ +package fun.supersmp.codelock.command; + +import fun.supersmp.codelock.core.Keys; +import fun.supersmp.codelock.core.Locale; +import games.negative.alumina.command.Command; +import games.negative.alumina.command.CommandProperties; +import games.negative.alumina.command.Context; +import games.negative.alumina.util.NBTEditor; +import org.bukkit.entity.Player; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class CommandCodeLockBlock extends Command { + + public CommandCodeLockBlock() { + super(CommandProperties.builder().name("codelockblock").aliases(List.of("codeblock")).smartTabComplete(true).build()); + + injectSubCommand(CommandProperties.builder().name("togglealerts").playerOnly(true).build(), context -> { + Player player = context.player().orElseThrow(); + + boolean state = NBTEditor.has(player, Keys.MUTE_NOTIFICATIONS); + if (state) { + // Turn on alerts + NBTEditor.remove(player, Keys.MUTE_NOTIFICATIONS); + + Locale.ALERTS_ON.send(player); + } else { + // Turn off alerts + NBTEditor.set(player, Keys.MUTE_NOTIFICATIONS, PersistentDataType.BYTE, (byte) 1); + + Locale.ALERTS_OFF.send(player); + } + }); + } + + @Override + public void execute(@NotNull Context context) { + Locale.CODE_BLOCK_COMMAND.send(context.sender()); + } +} diff --git a/src/main/java/fun/supersmp/codelock/core/Keys.java b/src/main/java/fun/supersmp/codelock/core/Keys.java new file mode 100644 index 0000000..4c48923 --- /dev/null +++ b/src/main/java/fun/supersmp/codelock/core/Keys.java @@ -0,0 +1,14 @@ +package fun.supersmp.codelock.core; + +import fun.supersmp.codelock.CodeLockPlugin; +import lombok.experimental.UtilityClass; +import org.bukkit.NamespacedKey; + +@UtilityClass +public class Keys { + + public NamespacedKey OWNER = new NamespacedKey(CodeLockPlugin.instance(), "owner"); + public NamespacedKey CODE = new NamespacedKey(CodeLockPlugin.instance(), "code"); + public NamespacedKey AUTHORIZED_USERS = new NamespacedKey(CodeLockPlugin.instance(), "authorized_users"); + public NamespacedKey MUTE_NOTIFICATIONS = new NamespacedKey(CodeLockPlugin.instance(), "mute-notifications"); +} diff --git a/src/main/java/fun/supersmp/codelock/core/Locale.java b/src/main/java/fun/supersmp/codelock/core/Locale.java new file mode 100644 index 0000000..ffc5b35 --- /dev/null +++ b/src/main/java/fun/supersmp/codelock/core/Locale.java @@ -0,0 +1,121 @@ +package fun.supersmp.codelock.core; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Maps; +import fun.supersmp.codelock.CodeLockPlugin; +import games.negative.alumina.logger.Logs; +import me.clip.placeholderapi.PlaceholderAPI; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextReplacementConfig; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public enum Locale { + BLOCK_PLACE_NOTIFY("ATTENTION! You have placed a block that can be locked with a secret access code! Shift + Left-Click the block to open the code menu!"), + CODE_CANNOT_BE_EMPTY("ATTENTION! The code that you have entered cannot be empty!"), + CODE_SAVED("ATTENTION! The code that you have entered has been saved!"), + CODE_SAME("ATTENTION! The code you entered was the same as your current code. No changes were saved."), + CODE_INCORRECT("ATTENTION! The code that you have entered is incorrect. Please try again!"), + CODE_CORRECT("ATTENTION! The code that you have entered is correct. You have been granted access!"), + + ALERTS_ON("ATTENTION! You have toggled alerts on!"), + ALERTS_OFF("ATTENTION! You have toggled alerts off!"), + + CODE_BLOCK_COMMAND(" CODELOCK BLOCKS By Negative Games /codeblock togglealerts - Toggle place notifications."); + + private String content; + + Locale(@NotNull String... defMessage) { + this.content = String.join("\n", defMessage); + } + + public static void init(@NotNull CodeLockPlugin plugin) { + File file = new File(plugin.getDataFolder(), "messages.yml"); + validateFile(file); + + FileConfiguration config = YamlConfiguration.loadConfiguration(file); + + boolean changed = false; + for (Locale entry : values()) { + if (config.isSet(entry.name())) continue; + + List message = List.of(entry.content.split("\n")); + config.set(entry.name(), message); + changed = true; + } + + if (changed) saveFile(file, config); + + for (Locale entry : values()) { + entry.content = String.join("\n", config.getStringList(entry.name())); + } + } + + private static void saveFile(@NotNull File file, @NotNull FileConfiguration config) { + try { + config.save(file); + } catch (IOException e) { + Logs.SEVERE.print("Could not save messages.yml file!", true); + } + } + + private static void validateFile(@NotNull File file) { + if (!file.exists()) { + boolean dirSuccess = file.getParentFile().mkdirs(); + if (dirSuccess) Logs.INFO.print("Created new plugin directory file!"); + + try { + boolean success = file.createNewFile(); + if (!success) return; + + Logs.INFO.print("Created messages.yml file!"); + } catch (IOException e) { + Logs.SEVERE.print("Could not create messages.yml file!", true); + } + } + } + + + public void send(@NotNull CommandSender sender, @Nullable String... placeholders) { + MiniMessage mm = MiniMessage.miniMessage(); + + Map placeholderMap = Maps.newHashMap(); + + Component component = mm.deserialize(sender instanceof Player ? PlaceholderAPI.setPlaceholders((Player) sender, content) : PlaceholderAPI.setPlaceholders(null, content)); + if (placeholders != null) { + Preconditions.checkArgument(placeholders.length % 2 == 0, "Placeholders must be in key-value pairs."); + + for (int i = 0; i < placeholders.length; i += 2) { + placeholderMap.put(placeholders[i], placeholders[i + 1]); + } + } + + for (Map.Entry entry : placeholderMap.entrySet()) { + component = component.replaceText(TextReplacementConfig.builder().matchLiteral(entry.getKey()).replacement(entry.getValue()).build()); + } + + sender.sendMessage(component); + } + + public > void send(T iterable, @Nullable String... placeholders) { + for (CommandSender sender : iterable) { + send(sender, placeholders); + } + } + + public void broadcast(@Nullable String... placeholders) { + send(Bukkit.getOnlinePlayers(), placeholders); + } + +} \ No newline at end of file diff --git a/src/main/java/fun/supersmp/codelock/core/Perm.java b/src/main/java/fun/supersmp/codelock/core/Perm.java new file mode 100644 index 0000000..81369f7 --- /dev/null +++ b/src/main/java/fun/supersmp/codelock/core/Perm.java @@ -0,0 +1,49 @@ +package fun.supersmp.codelock.core; + +import org.bukkit.Bukkit; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +public class Perm extends Permission { + + private static final String PREFIX = "codelock"; + + // Add permissions here + public static final Perm ADMIN = new Perm("admin"); + + public Perm(@NotNull String name, @Nullable String description, @Nullable PermissionDefault defaultValue, @Nullable Map children) { + super(PREFIX + "." + name, description, defaultValue, children); + + Bukkit.getPluginManager().addPermission(this); + } + + public Perm(@NotNull String name) { + this(name, null, null, null); + } + + public Perm(@NotNull String name, @Nullable String description) { + this(name, description, null, null); + } + + public Perm(@NotNull String name, @Nullable String description, @Nullable PermissionDefault defaultValue) { + this(name, description, defaultValue, null); + } + + public Perm(@NotNull String name, @Nullable String description, @Nullable Map children) { + this(name, description, null, children); + } + + public Perm(@NotNull String name, @Nullable PermissionDefault defaultValue) { + this(name, null, defaultValue, null); + } + + public Perm(@NotNull String name, @Nullable Map children) { + this(name, null, null, children); + } + + +} diff --git a/src/main/java/fun/supersmp/codelock/core/Util.java b/src/main/java/fun/supersmp/codelock/core/Util.java new file mode 100644 index 0000000..3c03e39 --- /dev/null +++ b/src/main/java/fun/supersmp/codelock/core/Util.java @@ -0,0 +1,70 @@ +package fun.supersmp.codelock.core; + +import games.negative.alumina.logger.Logs; +import lombok.experimental.UtilityClass; +import org.bukkit.Location; +import org.bukkit.block.BlockFace; +import org.bukkit.block.Jukebox; +import org.bukkit.block.data.Bisected; +import org.bukkit.block.data.type.Door; +import org.bukkit.block.data.type.TrapDoor; +import org.jetbrains.annotations.NotNull; + +@UtilityClass +public class Util { + + /** + * Adjusts the location of the door based on its half. + * + * @param location the current location of the door. Cannot be null. + * @param door the door object whose half is used to adjust the location. Cannot be null. + * @return the adjusted location of the door. If the half is BOTTOM, returns the location moved one block down with y-coordinate subtracted by 1. + * Otherwise, returns the location moved two blocks down with y-coordinate subtracted by 2. + * @throws NullPointerException if either the location or door parameter is null. + */ + @NotNull + public Location adjustDoorLocation(@NotNull Location location, @NotNull Door door) { + Bisected.Half half = door.getHalf(); + + boolean isBottom = half == Bisected.Half.BOTTOM; + + return location.subtract(0, isBottom ? 1 : 2, 0); + } + + /** + * Adjusts the location of the gate by subtracting 1 from the y-coordinate of the given location. + * + * @param location the current location of the gate. Cannot be null. + * @return the adjusted location of the gate with a decreased y-coordinate by 1. + * @throws NullPointerException if the location parameter is null. + */ + @NotNull + public static Location adjustGateLocation(@NotNull Location location) { + Location one = location.subtract(0, 1, 0); + if (one.getBlock().getState() instanceof Jukebox) + return one; + + Location two = location.subtract(0, 1, 0); + if (two.getBlock().getState() instanceof Jukebox) + return two; + + return one; + } + + /** + * Adjusts the location of the trap door based on its facing direction. + * + * @param location the current location of the trap door. Cannot be null. + * @param door the trap door object whose facing direction is used to adjust the location. Cannot be null. + * @return the adjusted location of the trap door based on its facing direction. If the facing direction is NORTH, + * returns the location moved one block to the south. If the facing direction is EAST, returns the location moved one block to the west. + * If the facing direction is SOUTH, returns the location moved one block to the north. If the facing direction is WEST, + * returns the location moved one block to the east. + * @throws NullPointerException if either the location or door parameter is null. + */ + @NotNull + public static Location adjustTrapDoorLocation(@NotNull Location location, @NotNull TrapDoor door) { + BlockFace opposing = door.getFacing().getOppositeFace(); + return location.getBlock().getRelative(opposing).getLocation(); + } +} diff --git a/src/main/java/fun/supersmp/codelock/listener/PlayerListener.java b/src/main/java/fun/supersmp/codelock/listener/PlayerListener.java new file mode 100644 index 0000000..a7c91fe --- /dev/null +++ b/src/main/java/fun/supersmp/codelock/listener/PlayerListener.java @@ -0,0 +1,236 @@ +package fun.supersmp.codelock.listener; + +import com.google.common.collect.Lists; +import fun.supersmp.codelock.core.Keys; +import fun.supersmp.codelock.core.Locale; +import fun.supersmp.codelock.core.Perm; +import fun.supersmp.codelock.core.Util; +import fun.supersmp.codelock.struct.CodeLockBlock; +import fun.supersmp.codelock.ui.EditCodeMenu; +import fun.supersmp.codelock.ui.EnterCodeMenu; +import games.negative.alumina.util.NBTEditor; +import lombok.RequiredArgsConstructor; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.Jukebox; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.type.Door; +import org.bukkit.block.data.type.Gate; +import org.bukkit.block.data.type.TrapDoor; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +public class PlayerListener implements Listener { + + @EventHandler(priority = EventPriority.LOWEST) + public void onBlockPlace(@NotNull BlockPlaceEvent event) { + Player player = event.getPlayer(); + Block block = event.getBlockPlaced(); + + Location location = adjustLocation(block.getLocation(), block.getBlockData()); + if (location == null) return; + + Jukebox jukebox = parse(location); + if (jukebox == null) return; + + String current = NBTEditor.get(jukebox, Keys.OWNER, PersistentDataType.STRING); + if (current != null && !player.getUniqueId().equals(UUID.fromString(current))) { + // The user cannot place a code lock on a jukebox that is already owned by someone else + // this function is primarily for the use-case of fence-gates with a two-high gateway + event.setCancelled(true); + return; + } + + NBTEditor.set(jukebox, Keys.OWNER, PersistentDataType.STRING, player.getUniqueId().toString()); + jukebox.update(true); + + if (hasNotificationsMuted(player)) return; + + Locale.BLOCK_PLACE_NOTIFY.send(player); + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onBlockBreak(@NotNull BlockBreakEvent event) { + Player player = event.getPlayer(); + Block block = event.getBlock(); + + Location location = adjustLocation(block.getLocation(), block.getBlockData()); + if (location == null) return; + + Jukebox jukebox = parse(location); + if (jukebox == null) return; + + CodeLockBlock data = parseCodeLockBlock(jukebox); + if (data == null) return; + + UUID uuid = player.getUniqueId(); + if (!isOwner(data, uuid) && !player.hasPermission(Perm.ADMIN)) { + event.setCancelled(true); + return; + } + + // Remove the data from the jukebox + NBTEditor.remove(jukebox, Keys.OWNER); + NBTEditor.remove(jukebox, Keys.CODE); + NBTEditor.remove(jukebox, Keys.AUTHORIZED_USERS); + jukebox.update(true); + } + + @EventHandler + public void onInteract(@NotNull PlayerInteractEvent event) { + // Ensure only left-click-block and right-click-block events are handled, + // we don't need to handle other events! + Action action = event.getAction(); + if (!action.equals(Action.LEFT_CLICK_BLOCK) && !action.equals(Action.RIGHT_CLICK_BLOCK)) return; + + Player player = event.getPlayer(); + Block block = event.getClickedBlock(); + if (block == null) return; + + Location location = adjustLocation(block.getLocation(), block.getBlockData()); + if (location == null) return; + + Jukebox jukebox = parse(location); + if (jukebox == null) return; + + CodeLockBlock data = parseCodeLockBlock(jukebox); + if (data == null) return; + + UUID uuid = player.getUniqueId(); + if (player.hasPermission(Perm.ADMIN) || data.authorized().contains(uuid)) { + if (!isOwner(data, uuid)) return; + + boolean notHoldingAir = !player.getInventory().getItemInMainHand().getType().isAir(); + + // The owner must be sneaking, left-clicking, and holding nothing to open the code menu + if (!player.isSneaking() || !action.equals(Action.LEFT_CLICK_BLOCK) || notHoldingAir) return; + + // we're going to open the code menu to modify the code and authorized users + new EditCodeMenu(jukebox, data.authorized(), data.code()).open(player); + return; + } + + event.setUseInteractedBlock(Event.Result.DENY); + event.setUseItemInHand(Event.Result.DENY); + event.setCancelled(true); + + new EnterCodeMenu(jukebox, data.code()).open(player); + } + + /** + * Adjusts the location based on the type of BlockData. + * + * @param location the current location of the block. Cannot be null. + * @param data the BlockData object representing the block. Cannot be null. + * @return the adjusted location of the block based on its type. + * If the block is of type Door, returns the location adjusted by Util.adjustDoorLocation(). + * If the block is of type + * Gate, + * returns the location adjusted by Util.adjustGateLocation(). + * If the block is of type TrapDoor, returns the location adjusted by Util.adjustTrapDoorLocation(). + * Otherwise, returns + * null. + * @throws NullPointerException if either the location or data parameter is null. + */ + @Nullable + private Location adjustLocation(@NotNull Location location, @NotNull BlockData data) { + Location loc = location.clone(); + + if (data instanceof Door door) { + // Door specific logic + return Util.adjustDoorLocation(loc, door); + } + + if (data instanceof Gate) { + // Gate specific logic + return Util.adjustGateLocation(loc); + } + + if (data instanceof TrapDoor door) { + // TrapDoor specific logic + return Util.adjustTrapDoorLocation(loc, door); + } + + return null; + } + + /** + * Parses a Location object and returns a Jukebox object if the block at the location is a Jukebox. + * + * @param location the location to parse. Cannot be null. + * @return a Jukebox object if the block at the location is a Jukebox, otherwise null. + * @throws NullPointerException if the location parameter is null. + */ + @Nullable + private Jukebox parse(@NotNull Location location) { + Block block = location.getBlock(); + BlockState state = block.getState(); + + if (state instanceof Jukebox jukebox) { + return jukebox; + } + return null; + } + + /** + * Parses a Jukebox block and returns a CodeLockBlock object. + * + * @param block the Jukebox block to parse. Cannot be null. + * @return a CodeLockBlock object if the block has an owner, otherwise null. + * @throws NullPointerException if the block parameter is null. + */ + @Nullable + private CodeLockBlock parseCodeLockBlock(@NotNull Jukebox block) { + String ownerRaw = NBTEditor.get(block, Keys.OWNER, PersistentDataType.STRING); + if (ownerRaw == null) return null; // we REQUIRE an owner + + UUID owner = UUID.fromString(ownerRaw); + + String code = NBTEditor.get(block, Keys.CODE, PersistentDataType.STRING); // we do not require a code + + List authorizedRaw = NBTEditor.getOrDefault(block, Keys.AUTHORIZED_USERS, PersistentDataType.LIST.strings(), Lists.newArrayList(ownerRaw)); + List authorizedUsers = authorizedRaw.stream().map(UUID::fromString).collect(Collectors.toCollection(Lists::newArrayList)); + if (!authorizedUsers.contains(owner)) authorizedUsers.add(owner); // Ensure the owner is always authorized! + + return new CodeLockBlock(owner, code, authorizedUsers); + } + + /** + * Checks if the given UUID is the owner of the CodeLockBlock. + * + * @param data the CodeLockBlock object representing the block data. Cannot be null. + * @param uuid the UUID of the player to check if they are the owner. Cannot be null. + * @return true if the UUID is the owner of the CodeLockBlock, false otherwise. + * @throws NullPointerException if either the data or uuid parameter is null. + */ + private boolean isOwner(@NotNull CodeLockBlock data, @NotNull UUID uuid) { + return data.owner().equals(uuid); + } + + /** + * Checks if the player has notifications muted. + * + * @param player the player to check. Cannot be null. + * @return true if the player has notifications muted, false otherwise. + * @throws NullPointerException if the player parameter is null. + */ + private boolean hasNotificationsMuted(@NotNull Player player) { + return NBTEditor.has(player, Keys.MUTE_NOTIFICATIONS, PersistentDataType.BYTE); + } +} diff --git a/src/main/java/fun/supersmp/codelock/struct/CodeLockBlock.java b/src/main/java/fun/supersmp/codelock/struct/CodeLockBlock.java new file mode 100644 index 0000000..ce1f07f --- /dev/null +++ b/src/main/java/fun/supersmp/codelock/struct/CodeLockBlock.java @@ -0,0 +1,10 @@ +package fun.supersmp.codelock.struct; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.UUID; + +public record CodeLockBlock(@NotNull UUID owner, @Nullable String code, @NotNull List authorized) { +} diff --git a/src/main/java/fun/supersmp/codelock/ui/AuthorizedUsersMenu.java b/src/main/java/fun/supersmp/codelock/ui/AuthorizedUsersMenu.java new file mode 100644 index 0000000..c6f67ab --- /dev/null +++ b/src/main/java/fun/supersmp/codelock/ui/AuthorizedUsersMenu.java @@ -0,0 +1,93 @@ +package fun.supersmp.codelock.ui; + +import com.google.common.collect.Lists; +import fun.supersmp.codelock.core.Keys; +import games.negative.alumina.builder.ItemBuilder; +import games.negative.alumina.menu.MenuButton; +import games.negative.alumina.menu.PaginatedMenu; +import games.negative.alumina.util.IntList; +import games.negative.alumina.util.NBTEditor; +import games.negative.alumina.util.Tasks; +import lombok.RequiredArgsConstructor; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.block.Jukebox; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +public class AuthorizedUsersMenu extends PaginatedMenu { + + private final Jukebox data; + private final List authorized; + private final String code; + public AuthorizedUsersMenu(@NotNull Jukebox data, @NotNull List authorized, @Nullable String code) { + super("Authorized Users", 5); + setCancelClicks(true); + + this.data = data; + this.authorized = authorized; + this.code = code; + + List fillerSlots = IntList.getList(List.of("0-9", "17-18", "26-27", "35-44")); + for (int slot : fillerSlots) { + addButton(MenuButton.builder().slot(slot).item(new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).setName(" ").build()).build()); + } + + ItemStack next = new ItemBuilder(Material.ARROW).setName("&a&lNext Page &2&l→").build(); + setNextPageButton(MenuButton.builder().slot(26).item(next).action((button, player, event) -> changePage(player, page + 1)).build()); + + ItemStack previous = new ItemBuilder(Material.ARROW).setName("&4&l← &c&lPrevious Page").build(); + setPreviousPageButton(MenuButton.builder().slot(18).item(previous).action((button, player, event) -> changePage(player, page - 1)).build()); + + setPaginatedSlots(IntList.getList(List.of("10-16", "19-25", "28-34"))); + + Collection buttons = generatePaginatedButtons(authorized, input -> { + OfflinePlayer player = Bukkit.getOfflinePlayer(input); + String name = player.getName(); + if (name == null) return null; + + ItemBuilder builder = new ItemBuilder(Material.PLAYER_HEAD); + builder.setSkullOwner(player); + builder.setName("&e" + name); + builder.setLore("&7Click to remove this user"); + + return MenuButton.builder().item(builder.build()).action(new RemoveUserClickHandler(input)).build(); + }); + + setPaginatedButtons(buttons); + } + + @Override + public void onClose(@NotNull Player player, @NotNull InventoryCloseEvent event) { + Tasks.run(() -> new EditCodeMenu(data, authorized, code).open(player), 2); + } + + @RequiredArgsConstructor + public class RemoveUserClickHandler implements MenuButton.ClickAction { + + private final UUID uuid; + + @Override + public void onClick(@NotNull MenuButton button, @NotNull Player player, @NotNull InventoryClickEvent event) { + authorized.remove(uuid); + + List mapped = authorized.stream().map(UUID::toString).collect(Collectors.toCollection(Lists::newArrayList)); + + NBTEditor.set(data, Keys.AUTHORIZED_USERS, PersistentDataType.LIST.strings(), mapped); + data.update(true); + + player.closeInventory(); + } + } +} diff --git a/src/main/java/fun/supersmp/codelock/ui/EditCodeMenu.java b/src/main/java/fun/supersmp/codelock/ui/EditCodeMenu.java new file mode 100644 index 0000000..f006775 --- /dev/null +++ b/src/main/java/fun/supersmp/codelock/ui/EditCodeMenu.java @@ -0,0 +1,129 @@ +package fun.supersmp.codelock.ui; + +import fun.supersmp.codelock.core.Keys; +import fun.supersmp.codelock.core.Locale; +import games.negative.alumina.builder.ItemBuilder; +import games.negative.alumina.menu.ChestMenu; +import games.negative.alumina.menu.MenuButton; +import games.negative.alumina.util.IntList; +import games.negative.alumina.util.NBTEditor; +import lombok.RequiredArgsConstructor; +import org.bukkit.Material; +import org.bukkit.block.Jukebox; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.UUID; + +public class EditCodeMenu extends ChestMenu { + + private static final int CODE_CHARACTER_LIMIT = 5; + + private final Jukebox data; + private final List users; + private final String original; + private String code; + public EditCodeMenu(@NotNull Jukebox data, @NotNull List users, @Nullable String code) { + this.code = code; + this.original = code; + this.data = data; + this.users = users; + + setRows(6); + title(); + setCancelClicks(true); + + List fillerSlots = IntList.getList(List.of("0-11", "15-20", "24-29", "33-39", "41-46", "48", "50", "52-53")); + for (int slot : fillerSlots) { + addButton(MenuButton.builder().slot(slot).item(new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).setName(" ").build()).build()); + } + + // 0-9 combination items + addButton(MenuButton.builder().slot(12).item(new ItemBuilder(Material.RED_WOOL).setName("&a1").build()).action(new NumberClickHandler('1')).build()); + addButton(MenuButton.builder().slot(13).item(new ItemBuilder(Material.ORANGE_WOOL).setName("&a2").build()).action(new NumberClickHandler('2')).build()); + addButton(MenuButton.builder().slot(14).item(new ItemBuilder(Material.YELLOW_WOOL).setName("&a3").build()).action(new NumberClickHandler('3')).build()); + addButton(MenuButton.builder().slot(21).item(new ItemBuilder(Material.GREEN_WOOL).setName("&a4").build()).action(new NumberClickHandler('4')).build()); + addButton(MenuButton.builder().slot(22).item(new ItemBuilder(Material.BLUE_WOOL).setName("&a5").build()).action(new NumberClickHandler('5')).build()); + addButton(MenuButton.builder().slot(23).item(new ItemBuilder(Material.LIME_WOOL).setName("&a6").build()).action(new NumberClickHandler('6')).build()); + addButton(MenuButton.builder().slot(30).item(new ItemBuilder(Material.LIGHT_BLUE_WOOL).setName("&a7").build()).action(new NumberClickHandler('7')).build()); + addButton(MenuButton.builder().slot(31).item(new ItemBuilder(Material.PINK_WOOL).setName("&a8").build()).action(new NumberClickHandler('8')).build()); + addButton(MenuButton.builder().slot(32).item(new ItemBuilder(Material.WHITE_WOOL).setName("&a9").build()).action(new NumberClickHandler('9')).build()); + addButton(MenuButton.builder().slot(40).item(new ItemBuilder(Material.BLACK_WOOL).setName("&a0").build()).action(new NumberClickHandler('0')).build()); + + // Backspace, Save, and Authorized Users buttons + addButton(MenuButton.builder().slot(47).item(new ItemBuilder(Material.ARROW).setName("&4&l← &c&lBackspace").build()).action(new BackspaceClickHandler()).build()); + addButton(MenuButton.builder().slot(49).item(new ItemBuilder(Material.WRITABLE_BOOK).setName("&2&l✔ &a&lSave Code").build()).action(new SaveClickHandler()).build()); + addButton(MenuButton.builder().slot(51).item(new ItemBuilder(Material.PLAYER_HEAD).setName("&3&l✯ &b&lAuthorized Users").build()).action(new AuthorizedUsersMenuClickHandler()).build()); + } + + @RequiredArgsConstructor + public class NumberClickHandler implements MenuButton.ClickAction { + + private final char character; + + @Override + public void onClick(@NotNull MenuButton button, @NotNull Player player, @NotNull InventoryClickEvent event) { + if (code == null) code = ""; + + if (code.length() == CODE_CHARACTER_LIMIT) return; + + code += character; + + title(); + } + } + + public class BackspaceClickHandler implements MenuButton.ClickAction { + + @Override + public void onClick(@NotNull MenuButton button, @NotNull Player player, @NotNull InventoryClickEvent event) { + if (code == null || code.isEmpty()) return; + + code = code.substring(0, code.length() - 1); + title(); + } + } + + public class SaveClickHandler implements MenuButton.ClickAction { + + @Override + public void onClick(@NotNull MenuButton button, @NotNull Player player, @NotNull InventoryClickEvent event) { + if (code.isEmpty()) { + Locale.CODE_CANNOT_BE_EMPTY.send(player); + return; + } + + if (original != null && original.equals(code)) { + player.closeInventory(); + + Locale.CODE_SAME.send(player); + return; + } + + NBTEditor.set(data, Keys.CODE, PersistentDataType.STRING, code); + NBTEditor.remove(data, Keys.AUTHORIZED_USERS); + + data.update(true); + + player.closeInventory(); + + Locale.CODE_SAVED.send(player); + } + } + + public class AuthorizedUsersMenuClickHandler implements MenuButton.ClickAction { + + @Override + public void onClick(@NotNull MenuButton button, @NotNull Player player, @NotNull InventoryClickEvent event) { + new AuthorizedUsersMenu(data, users, code).open(player); + } + } + + private void title() { + updateTitle("Enter Code!" + ((code == null || code.isEmpty()) ? "" : " | " + code)); + } +} diff --git a/src/main/java/fun/supersmp/codelock/ui/EnterCodeMenu.java b/src/main/java/fun/supersmp/codelock/ui/EnterCodeMenu.java new file mode 100644 index 0000000..aa7c849 --- /dev/null +++ b/src/main/java/fun/supersmp/codelock/ui/EnterCodeMenu.java @@ -0,0 +1,130 @@ +package fun.supersmp.codelock.ui; + +import com.google.common.collect.Lists; +import fun.supersmp.codelock.core.Keys; +import fun.supersmp.codelock.core.Locale; +import games.negative.alumina.builder.ItemBuilder; +import games.negative.alumina.menu.ChestMenu; +import games.negative.alumina.menu.MenuButton; +import games.negative.alumina.util.IntList; +import games.negative.alumina.util.NBTEditor; +import lombok.RequiredArgsConstructor; +import org.bukkit.Material; +import org.bukkit.block.Jukebox; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +public class EnterCodeMenu extends ChestMenu { + + private static final int CODE_CHARACTER_LIMIT = 5; + + private final Jukebox data; + private final String code; + private String current; + public EnterCodeMenu(@NotNull Jukebox data, @Nullable String code) { + this.code = (code == null ? "" : code); + this.current = ""; + this.data = data; + + setRows(6); + title(); + setCancelClicks(true); + + List fillerSlots = IntList.getList(List.of("0-11", "15-20", "24-29", "33-39", "41-47", "49", "51-53")); + for (int slot : fillerSlots) { + addButton(MenuButton.builder().slot(slot).item(new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).setName(" ").build()).build()); + } + + // 0-9 combination items + addButton(MenuButton.builder().slot(12).item(new ItemBuilder(Material.RED_WOOL).setName("&a1").build()).action(new NumberClickHandler('1')).build()); + addButton(MenuButton.builder().slot(13).item(new ItemBuilder(Material.ORANGE_WOOL).setName("&a2").build()).action(new NumberClickHandler('2')).build()); + addButton(MenuButton.builder().slot(14).item(new ItemBuilder(Material.YELLOW_WOOL).setName("&a3").build()).action(new NumberClickHandler('3')).build()); + addButton(MenuButton.builder().slot(21).item(new ItemBuilder(Material.GREEN_WOOL).setName("&a4").build()).action(new NumberClickHandler('4')).build()); + addButton(MenuButton.builder().slot(22).item(new ItemBuilder(Material.BLUE_WOOL).setName("&a5").build()).action(new NumberClickHandler('5')).build()); + addButton(MenuButton.builder().slot(23).item(new ItemBuilder(Material.LIME_WOOL).setName("&a6").build()).action(new NumberClickHandler('6')).build()); + addButton(MenuButton.builder().slot(30).item(new ItemBuilder(Material.LIGHT_BLUE_WOOL).setName("&a7").build()).action(new NumberClickHandler('7')).build()); + addButton(MenuButton.builder().slot(31).item(new ItemBuilder(Material.PINK_WOOL).setName("&a8").build()).action(new NumberClickHandler('8')).build()); + addButton(MenuButton.builder().slot(32).item(new ItemBuilder(Material.WHITE_WOOL).setName("&a9").build()).action(new NumberClickHandler('9')).build()); + addButton(MenuButton.builder().slot(40).item(new ItemBuilder(Material.BLACK_WOOL).setName("&a0").build()).action(new NumberClickHandler('0')).build()); + + // Backspace, Save, and Authorized Users buttons + addButton(MenuButton.builder().slot(48).item(new ItemBuilder(Material.ARROW).setName("&4&l← &c&lBackspace").build()).action(new BackspaceClickHandler()).build()); + addButton(MenuButton.builder().slot(50).item(new ItemBuilder(Material.OAK_DOOR).setName("&2&l✔ &a&lSubmit").build()).action(new SubmitClickHandler()).build()); + } + + @RequiredArgsConstructor + public class NumberClickHandler implements MenuButton.ClickAction { + + private final char character; + + @Override + public void onClick(@NotNull MenuButton button, @NotNull Player player, @NotNull InventoryClickEvent event) { + if (current.length() == CODE_CHARACTER_LIMIT) return; + + current += character; + + title(); + } + } + + public class BackspaceClickHandler implements MenuButton.ClickAction { + + @Override + public void onClick(@NotNull MenuButton button, @NotNull Player player, @NotNull InventoryClickEvent event) { + if (current.isEmpty()) return; + + current = current.substring(0, current.length() - 1); + title(); + } + } + + public class SubmitClickHandler implements MenuButton.ClickAction { + + @Override + public void onClick(@NotNull MenuButton button, @NotNull Player player, @NotNull InventoryClickEvent event) { + if (current.isEmpty()) return; + + boolean unlocked = current.equals(code); + if (!unlocked) { + player.closeInventory(); + + Locale.CODE_INCORRECT.send(player); + return; + } + + String ownerRaw = NBTEditor.get(data, Keys.OWNER, PersistentDataType.STRING); + if (ownerRaw == null) return; // something went wrong! + + UUID uuid = player.getUniqueId(); + + // I have to map and remap the authorized users because `authorized` is returned as an immutable list + // and I cannot add the player's UUID to it! + List authorized = NBTEditor.getOrDefault(data, Keys.AUTHORIZED_USERS, PersistentDataType.LIST.strings(), Lists.newArrayList(ownerRaw)); + if (authorized.contains(uuid.toString())) return; + + List mapped = authorized.stream().map(UUID::fromString).collect(Collectors.toCollection(Lists::newArrayList)); + mapped.add(uuid); + + List remapped = mapped.stream().map(UUID::toString).collect(Collectors.toCollection(Lists::newArrayList)); + NBTEditor.set(data, Keys.AUTHORIZED_USERS, PersistentDataType.LIST.strings(), remapped); + + data.update(true); + + player.closeInventory(); + + Locale.CODE_CORRECT.send(player); + } + } + + private void title() { + updateTitle("Enter Code!" + (current.isEmpty() ? "" : " | " + current)); + } + +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..2f9ec00 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,8 @@ +name: CodeLock +main: fun.supersmp.codelock.CodeLockPlugin +version: '${project.version}' +authors: [ "ericlmao" ] +api-version: 1.20 +load: POSTWORLD + +depend: ["PlaceholderAPI"] \ No newline at end of file